@magic-ingredients/tiny-brain-local 0.17.0 → 0.18.1
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.
- package/dist/core/mcp-server.d.ts +9 -0
- package/dist/core/mcp-server.d.ts.map +1 -1
- package/dist/core/mcp-server.js +108 -10
- package/dist/services/analyse-service.d.ts.map +1 -1
- package/dist/services/analyse-service.js +1 -3
- package/dist/services/credential-storage.service.d.ts +19 -0
- package/dist/services/credential-storage.service.d.ts.map +1 -1
- package/dist/services/credential-storage.service.js +52 -3
- package/dist/services/repo-service.d.ts.map +1 -1
- package/dist/services/repo-service.js +41 -31
- package/dist/tools/config/config.tool.d.ts.map +1 -1
- package/dist/tools/config/config.tool.js +1 -2
- package/package.json +2 -2
- package/dist/services/tech-context-service.d.ts +0 -106
- package/dist/services/tech-context-service.d.ts.map +0 -1
- package/dist/services/tech-context-service.js +0 -365
|
@@ -114,5 +114,14 @@ export declare class MCPServer {
|
|
|
114
114
|
private handleListPrompts;
|
|
115
115
|
private handleGetPrompt;
|
|
116
116
|
private handleSetLogLevel;
|
|
117
|
+
/**
|
|
118
|
+
* Get the TTY by walking up the process tree
|
|
119
|
+
* Child processes don't inherit TTY directly, but we can find it from parent processes
|
|
120
|
+
*/
|
|
121
|
+
private getTtyFromProcessTree;
|
|
122
|
+
/**
|
|
123
|
+
* Update sessions.json with the active persona for the current TTY
|
|
124
|
+
*/
|
|
125
|
+
private updatePersonaSession;
|
|
117
126
|
}
|
|
118
127
|
//# sourceMappingURL=mcp-server.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mcp-server.d.ts","sourceRoot":"","sources":["../../src/core/mcp-server.ts"],"names":[],"mappings":"AAAA;;;;GAIG;
|
|
1
|
+
{"version":3,"file":"mcp-server.d.ts","sourceRoot":"","sources":["../../src/core/mcp-server.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AA0BH,OAAO,KAAK,EAA4B,QAAQ,EAAkC,MAAM,oCAAoC,CAAC;AAE7H,OAAO,EAAE,gBAAgB,EAAE,MAAM,0CAA0C,CAAC;AAQ5E,OAAO,EAAE,iBAAiB,EAAE,MAAM,2CAA2C,CAAC;AAE9E;;GAEG;AACH,MAAM,WAAW,SAAS;IACxB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,aAAa,CAAC,EAAE,MAAM,CAAC;IAEvB,OAAO,CAAC,EAAE;QACR,QAAQ,CAAC,EAAE,MAAM,CAAC;QAClB,YAAY,CAAC,EAAE,MAAM,CAAC;KACvB,CAAC;IAEF,KAAK,CAAC,EAAE;QACN,OAAO,CAAC,EAAE,MAAM,CAAC;QACjB,QAAQ,CAAC,EAAE,QAAQ,CAAC;KACrB,CAAC;CACH;AAED;;GAEG;AACH,qBAAa,SAAS;IACpB,OAAO,CAAC,MAAM,CAAY;IAC1B,OAAO,CAAC,OAAO,CAA2B;IAC1C,OAAO,CAAC,MAAM,CAAa;IAC3B,OAAO,CAAC,MAAM,CAAY;IAC1B,OAAO,CAAC,SAAS,CAAS;IAC1B,OAAO,CAAC,aAAa,CAAiC;IACtD,OAAO,CAAC,MAAM,CAAwB;IACtC,OAAO,CAAC,sBAAsB,CAA0D;IACxF,OAAO,CAAC,mBAAmB,CAA2G;IACtI,OAAO,CAAC,UAAU,CAA6E;IAC/F,OAAO,CAAC,iBAAiB,CAAkC;IAC3D,OAAO,CAAC,WAAW,CAAiB;IACpC,OAAO,CAAC,SAAS,CAAC,CAAY;IAC9B,OAAO,CAAC,gBAAgB,CAAC,CAAmB;gBAEhC,MAAM,GAAE,SAAc;IAwFlC,OAAO,CAAC,aAAa;IAarB;;OAEG;IACH,SAAS,IAAI,SAAS;IAItB;;OAEG;IACH,mBAAmB,IAAI,gBAAgB,GAAG,SAAS;IAInD;;OAEG;IACH,oBAAoB,IAAI,iBAAiB,GAAG,IAAI;IAIhD;;OAEG;IACG,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC;IAsBjC;;;;;;;OAOG;YACW,sBAAsB;IA+DpC;;OAEG;YACW,iBAAiB;IAW/B;;;OAGG;YACW,oBAAoB;IA6BlC;;OAEG;YACW,oBAAoB;IAkHlC;;;;;;OAMG;YACW,yBAAyB;IA+CvC;;OAEG;YACW,kBAAkB;IAsBhC;;OAEG;YACW,wBAAwB;IA8BtC;;OAEG;YACW,mBAAmB;IA8BjC;;OAEG;IACG,OAAO,CAAC,SAAS,EAAE,GAAG,GAAG,OAAO,CAAC,IAAI,CAAC;IAK5C;;OAEG;IACG,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC;YAiBjB,eAAe;YAMf,cAAc;YA+Fd,iBAAiB;YAMjB,eAAe;YAqEf,iBAAiB;IAS/B;;;OAGG;IACH,OAAO,CAAC,qBAAqB;IAoB7B;;OAEG;IACH,OAAO,CAAC,oBAAoB;CAkD7B"}
|
package/dist/core/mcp-server.js
CHANGED
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
* Simplified MCP server for local execution without deployment complexity
|
|
5
5
|
*/
|
|
6
6
|
import * as path from 'path';
|
|
7
|
-
import { existsSync, cpSync, readdirSync } from 'fs';
|
|
7
|
+
import { existsSync, cpSync, readdirSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
|
|
8
|
+
import { execSync } from 'child_process';
|
|
8
9
|
import { Server as McpServer } from '@modelcontextprotocol/sdk/server/index.js';
|
|
9
10
|
import { ListToolsRequestSchema, CallToolRequestSchema, ListPromptsRequestSchema, GetPromptRequestSchema, SetLevelRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
|
|
10
11
|
import { isDevelopment as checkIsDevelopment } from '@magic-ingredients/tiny-brain-core';
|
|
@@ -275,12 +276,17 @@ export class MCPServer {
|
|
|
275
276
|
hasClientSecret: !!this.config.account?.clientSecret,
|
|
276
277
|
configKeys: Object.keys(this.config)
|
|
277
278
|
});
|
|
279
|
+
// Create credential service to check for credentials and LLM API key
|
|
280
|
+
const credentialService = new CredentialStorageService();
|
|
281
|
+
// Check LLM API key availability (for dashboard to disable/enable skill invocation buttons)
|
|
282
|
+
const hasLlmApiKey = await credentialService.hasLlmApiKey();
|
|
283
|
+
// Create getter function for the actual API key (for skill invocation)
|
|
284
|
+
const getLlmApiKey = () => credentialService.getLlmApiKey();
|
|
278
285
|
// Check for remote auth credentials
|
|
279
286
|
if (!this.config.account?.clientId || !this.config.account?.clientSecret) {
|
|
280
287
|
this.logger.info('No remote credentials configured in MCP config, checking tiny-brain CLI config...');
|
|
281
288
|
// Fallback to tiny-brain CLI config (like what CLI status command uses)
|
|
282
289
|
try {
|
|
283
|
-
const credentialService = new CredentialStorageService();
|
|
284
290
|
const credentials = await credentialService.getCredentials();
|
|
285
291
|
if (credentials && credentials.clientId && credentials.clientSecret) {
|
|
286
292
|
this.logger.info('Found credentials in CLI config, using for authentication');
|
|
@@ -292,12 +298,28 @@ export class MCPServer {
|
|
|
292
298
|
}
|
|
293
299
|
else {
|
|
294
300
|
this.logger.info('No credentials found in CLI config either, running in local-only mode');
|
|
301
|
+
// Still set libraryAuth with hasLlmApiKey for dashboard
|
|
302
|
+
this.baseContext.libraryAuth = {
|
|
303
|
+
clientId: undefined,
|
|
304
|
+
hasStoredSecret: false,
|
|
305
|
+
hasLlmApiKey,
|
|
306
|
+
token: undefined,
|
|
307
|
+
getLlmApiKey
|
|
308
|
+
};
|
|
295
309
|
return;
|
|
296
310
|
}
|
|
297
311
|
}
|
|
298
312
|
catch (error) {
|
|
299
313
|
this.logger.warn('Error reading CLI config:', error);
|
|
300
314
|
this.logger.info('Running in local-only mode');
|
|
315
|
+
// Still set libraryAuth with hasLlmApiKey for dashboard
|
|
316
|
+
this.baseContext.libraryAuth = {
|
|
317
|
+
clientId: undefined,
|
|
318
|
+
hasStoredSecret: false,
|
|
319
|
+
hasLlmApiKey,
|
|
320
|
+
token: undefined,
|
|
321
|
+
getLlmApiKey
|
|
322
|
+
};
|
|
301
323
|
return;
|
|
302
324
|
}
|
|
303
325
|
}
|
|
@@ -321,7 +343,9 @@ export class MCPServer {
|
|
|
321
343
|
this.baseContext.libraryAuth = {
|
|
322
344
|
clientId: this.config.account?.clientId,
|
|
323
345
|
hasStoredSecret: true,
|
|
324
|
-
|
|
346
|
+
hasLlmApiKey,
|
|
347
|
+
token: token.token,
|
|
348
|
+
getLlmApiKey
|
|
325
349
|
};
|
|
326
350
|
}
|
|
327
351
|
else {
|
|
@@ -329,18 +353,26 @@ export class MCPServer {
|
|
|
329
353
|
this.logger.info('Continuing in local-only mode - authentication returned null');
|
|
330
354
|
this.authToken = undefined;
|
|
331
355
|
// Still populate libraryAuth with credentials (but no token)
|
|
332
|
-
|
|
333
|
-
this.
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
}
|
|
356
|
+
this.baseContext.libraryAuth = {
|
|
357
|
+
clientId: this.config.account?.clientId,
|
|
358
|
+
hasStoredSecret: !!this.config.account?.clientSecret,
|
|
359
|
+
hasLlmApiKey,
|
|
360
|
+
token: undefined,
|
|
361
|
+
getLlmApiKey
|
|
362
|
+
};
|
|
339
363
|
}
|
|
340
364
|
}
|
|
341
365
|
catch (error) {
|
|
342
366
|
this.logger.error('Unexpected error during remote authentication', error);
|
|
343
367
|
// Continue in local mode - don't fail the entire initialization
|
|
368
|
+
// Still set libraryAuth with hasLlmApiKey for dashboard
|
|
369
|
+
this.baseContext.libraryAuth = {
|
|
370
|
+
clientId: this.config.account?.clientId,
|
|
371
|
+
hasStoredSecret: !!this.config.account?.clientSecret,
|
|
372
|
+
hasLlmApiKey,
|
|
373
|
+
token: undefined,
|
|
374
|
+
getLlmApiKey
|
|
375
|
+
};
|
|
344
376
|
}
|
|
345
377
|
}
|
|
346
378
|
/**
|
|
@@ -530,6 +562,8 @@ export class MCPServer {
|
|
|
530
562
|
const newPersonaId = persona?.id;
|
|
531
563
|
this.activePersona = newPersonaId;
|
|
532
564
|
this.logger.info(`[${this.instanceId}] Active persona updated to: ${newPersonaId || 'none'}, notifying ${this.personaChangeListeners.length} listeners`);
|
|
565
|
+
// Update session tracking for statusline
|
|
566
|
+
this.updatePersonaSession(newPersonaId || null);
|
|
533
567
|
// Notify all registered listeners
|
|
534
568
|
for (const listener of this.personaChangeListeners) {
|
|
535
569
|
try {
|
|
@@ -623,6 +657,8 @@ export class MCPServer {
|
|
|
623
657
|
const newPersonaId = persona?.id;
|
|
624
658
|
this.activePersona = newPersonaId;
|
|
625
659
|
this.logger.info(`[${this.instanceId}] Active persona updated to: ${newPersonaId || 'none'}, notifying ${this.personaChangeListeners.length} listeners`);
|
|
660
|
+
// Update session tracking for statusline
|
|
661
|
+
this.updatePersonaSession(newPersonaId || null);
|
|
626
662
|
// Notify all registered listeners
|
|
627
663
|
for (const listener of this.personaChangeListeners) {
|
|
628
664
|
try {
|
|
@@ -667,4 +703,66 @@ export class MCPServer {
|
|
|
667
703
|
this.logger.info(`Log level set to: ${level}`);
|
|
668
704
|
return {};
|
|
669
705
|
}
|
|
706
|
+
/**
|
|
707
|
+
* Get the TTY by walking up the process tree
|
|
708
|
+
* Child processes don't inherit TTY directly, but we can find it from parent processes
|
|
709
|
+
*/
|
|
710
|
+
getTtyFromProcessTree() {
|
|
711
|
+
try {
|
|
712
|
+
let pid = process.pid;
|
|
713
|
+
for (let i = 0; i < 15; i++) {
|
|
714
|
+
const info = execSync(`ps -p ${pid} -o ppid=,tty=`, { encoding: 'utf-8' }).trim();
|
|
715
|
+
const [ppid, tty] = info.split(/\s+/);
|
|
716
|
+
if (tty && tty !== '??' && tty !== '-') {
|
|
717
|
+
return '/dev/' + tty;
|
|
718
|
+
}
|
|
719
|
+
pid = parseInt(ppid);
|
|
720
|
+
if (pid <= 1)
|
|
721
|
+
break;
|
|
722
|
+
}
|
|
723
|
+
return null;
|
|
724
|
+
}
|
|
725
|
+
catch {
|
|
726
|
+
return null;
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
/**
|
|
730
|
+
* Update sessions.json with the active persona for the current TTY
|
|
731
|
+
*/
|
|
732
|
+
updatePersonaSession(personaId) {
|
|
733
|
+
const tty = this.getTtyFromProcessTree();
|
|
734
|
+
if (!tty) {
|
|
735
|
+
this.logger.debug('No TTY available for persona session tracking');
|
|
736
|
+
return;
|
|
737
|
+
}
|
|
738
|
+
try {
|
|
739
|
+
const basePath = process.cwd();
|
|
740
|
+
const sessionsPath = path.join(basePath, '.tiny-brain', 'sessions.json');
|
|
741
|
+
let sessions = {};
|
|
742
|
+
try {
|
|
743
|
+
const content = readFileSync(sessionsPath, 'utf-8');
|
|
744
|
+
sessions = JSON.parse(content);
|
|
745
|
+
}
|
|
746
|
+
catch {
|
|
747
|
+
// File doesn't exist or is invalid, start fresh
|
|
748
|
+
sessions = {};
|
|
749
|
+
}
|
|
750
|
+
// Update this TTY's entry, preserving PRD/Fix
|
|
751
|
+
const existingEntry = sessions[tty];
|
|
752
|
+
sessions[tty] = {
|
|
753
|
+
activePrdId: existingEntry?.activePrdId ?? null,
|
|
754
|
+
activeFixId: existingEntry?.activeFixId ?? null,
|
|
755
|
+
activePersona: personaId,
|
|
756
|
+
updatedAt: new Date().toISOString(),
|
|
757
|
+
};
|
|
758
|
+
// Ensure directory exists
|
|
759
|
+
mkdirSync(path.dirname(sessionsPath), { recursive: true });
|
|
760
|
+
// Write sessions file
|
|
761
|
+
writeFileSync(sessionsPath, JSON.stringify(sessions, null, 2), 'utf-8');
|
|
762
|
+
this.logger.debug(`Updated persona session: ${personaId} for TTY ${tty}`);
|
|
763
|
+
}
|
|
764
|
+
catch (err) {
|
|
765
|
+
this.logger.error('Failed to update persona session:', err);
|
|
766
|
+
}
|
|
767
|
+
}
|
|
670
768
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"analyse-service.d.ts","sourceRoot":"","sources":["../../src/services/analyse-service.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAC;AAClE,OAAO,EAEL,KAAK,YAAY,EAEjB,KAAK,aAAa,
|
|
1
|
+
{"version":3,"file":"analyse-service.d.ts","sourceRoot":"","sources":["../../src/services/analyse-service.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAC;AAClE,OAAO,EAEL,KAAK,YAAY,EAEjB,KAAK,aAAa,EAMnB,MAAM,oCAAoC,CAAC;AAS5C,MAAM,WAAW,cAAc;IAC7B,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAGD,MAAM,WAAW,aAAa;IAC5B,QAAQ,EAAE,YAAY,CAAC;IACvB,eAAe,EAAE,OAAO,CAAC;IACzB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;IACpB,mBAAmB,CAAC,EAAE,OAAO,CAAC;IAC9B,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,mBAAmB,CAAC,EAAE,OAAO,CAAC;IAC9B,OAAO,CAAC,EAAE;QACR,cAAc,EAAE,MAAM,EAAE,CAAC;QACzB,gBAAgB,EAAE,MAAM,EAAE,CAAC;KAC5B,CAAC;IACF,qBAAqB,CAAC,EAAE,MAAM,EAAE,CAAC;IACjC,mBAAmB,CAAC,EAAE,MAAM,EAAE,CAAC;CAChC;AAGD;;;GAGG;AACH,qBAAa,cAAc;IAMb,OAAO,CAAC,OAAO;IAL3B,OAAO,CAAC,WAAW,CAAc;IACjC,OAAO,CAAC,aAAa,CAAgB;IACrC,OAAO,CAAC,aAAa,CAAgB;IACrC,OAAO,CAAC,kBAAkB,CAAqB;gBAE3B,OAAO,EAAE,cAAc;IAO3C;;OAEG;IACG,eAAe,CAAC,OAAO,GAAE,cAAmB,GAAG,OAAO,CAAC,aAAa,CAAC;IAwJ3E;;;OAGG;YACW,kBAAkB;IAoBhC;;OAEG;YACW,eAAe;IAc7B;;;OAGG;YACW,sBAAsB;IAwBpC;;;OAGG;YACW,sBAAsB;IAwBpC;;;OAGG;YACW,0BAA0B;IAwBxC;;;OAGG;YACW,wBAAwB;IAwBtC;;;;OAIG;YACW,kBAAkB;IA+EhC;;;;OAIG;YACW,sBAAsB;IAiEpC;;;OAGG;YACW,yBAAyB;IAkCvC;;OAEG;IACH,oBAAoB,CAAC,MAAM,EAAE,aAAa,GAAG,MAAM;IAiHnD;;;OAGG;IACH,iBAAiB,CAAC,gBAAgB,EAAE,YAAY,GAAG,IAAI,EAAE,eAAe,EAAE,YAAY,GAAG,aAAa;IA8BtG;;;;;OAKG;IACH,OAAO,CAAC,oBAAoB;IAY5B;;;;;OAKG;IACH,OAAO,CAAC,eAAe;CAWxB"}
|
|
@@ -4,10 +4,8 @@
|
|
|
4
4
|
* Handles repository tech stack analysis.
|
|
5
5
|
* Focused solely on analysis - no context file management.
|
|
6
6
|
*/
|
|
7
|
-
import { analyseRepository, formatTechStackChanges, detectTechStackChanges, ConfigService, RepoConfigService } from '@magic-ingredients/tiny-brain-core';
|
|
7
|
+
import { analyseRepository, formatTechStackChanges, detectTechStackChanges, ConfigService, RepoConfigService, LibraryClient, TechContextService, } from '@magic-ingredients/tiny-brain-core';
|
|
8
8
|
import { RepoService } from './repo-service.js';
|
|
9
|
-
import { LibraryClient } from '@magic-ingredients/tiny-brain-core';
|
|
10
|
-
import { TechContextService } from './tech-context-service.js';
|
|
11
9
|
import { fileURLToPath } from 'url';
|
|
12
10
|
import { dirname, join } from 'path';
|
|
13
11
|
import { existsSync } from 'fs';
|
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
export interface Credentials {
|
|
2
2
|
clientId?: string;
|
|
3
3
|
clientSecret?: string;
|
|
4
|
+
llmProvider?: string;
|
|
5
|
+
llmApiKey?: string;
|
|
4
6
|
}
|
|
5
7
|
export interface DisplayCredentials {
|
|
6
8
|
clientId?: string;
|
|
7
9
|
clientSecret: string | null;
|
|
10
|
+
llmProvider: string | null;
|
|
11
|
+
llmApiKey: string | null;
|
|
8
12
|
}
|
|
9
13
|
export declare class CredentialStorageService {
|
|
10
14
|
private readonly credentialsPath;
|
|
@@ -15,6 +19,21 @@ export declare class CredentialStorageService {
|
|
|
15
19
|
setCredential(key: string, value: string): Promise<void>;
|
|
16
20
|
getCredentials(): Promise<Credentials | null>;
|
|
17
21
|
showCredentials(): Promise<DisplayCredentials | null>;
|
|
22
|
+
/**
|
|
23
|
+
* Get the LLM provider name
|
|
24
|
+
* @returns The provider name or null if not configured
|
|
25
|
+
*/
|
|
26
|
+
getLlmProvider(): Promise<string | null>;
|
|
27
|
+
/**
|
|
28
|
+
* Get the LLM API key, with fallback to environment variable
|
|
29
|
+
* @returns The API key or null if not configured
|
|
30
|
+
*/
|
|
31
|
+
getLlmApiKey(): Promise<string | null>;
|
|
32
|
+
/**
|
|
33
|
+
* Check if an LLM API key is available (stored or in env)
|
|
34
|
+
* @returns true if API key is configured
|
|
35
|
+
*/
|
|
36
|
+
hasLlmApiKey(): Promise<boolean>;
|
|
18
37
|
clearCredentials(): Promise<void>;
|
|
19
38
|
validateCredential(key: string, value: string): void;
|
|
20
39
|
private encryptSecret;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"credential-storage.service.d.ts","sourceRoot":"","sources":["../../src/services/credential-storage.service.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"credential-storage.service.d.ts","sourceRoot":"","sources":["../../src/services/credential-storage.service.ts"],"names":[],"mappings":"AAqBA,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;CAC1B;AAED,qBAAa,wBAAwB;IACnC,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAS;IACzC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAiB;IAC3C,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAU;IACrC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAM;;IAO1B,aAAa,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAkCxD,cAAc,IAAI,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC;IAyB7C,eAAe,IAAI,OAAO,CAAC,kBAAkB,GAAG,IAAI,CAAC;IAc3D;;;OAGG;IACG,cAAc,IAAI,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;IAK9C;;;OAGG;IACG,YAAY,IAAI,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;IAe5C;;;OAGG;IACG,YAAY,IAAI,OAAO,CAAC,OAAO,CAAC;IAKhC,gBAAgB,IAAI,OAAO,CAAC,IAAI,CAAC;IAWvC,kBAAkB,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI;IAkBpD,OAAO,CAAC,aAAa;IAwBrB,OAAO,CAAC,aAAa;IAmBrB,OAAO,CAAC,SAAS;YAcH,qBAAqB;YAYrB,qBAAqB;CAgBpC"}
|
|
@@ -12,7 +12,8 @@ export class CredentialStorageService {
|
|
|
12
12
|
this.credentialsPath = path.join(homeDir, '.tiny-brain', 'config', 'credentials.json');
|
|
13
13
|
}
|
|
14
14
|
async setCredential(key, value) {
|
|
15
|
-
|
|
15
|
+
const validKeys = ['clientId', 'clientSecret', 'llmProvider', 'llmApiKey'];
|
|
16
|
+
if (!validKeys.includes(key)) {
|
|
16
17
|
throw new Error(`Invalid credential key: ${key}`);
|
|
17
18
|
}
|
|
18
19
|
this.validateCredential(key, value);
|
|
@@ -31,6 +32,12 @@ export class CredentialStorageService {
|
|
|
31
32
|
else if (key === 'clientSecret') {
|
|
32
33
|
stored.clientSecret = this.encryptSecret(value);
|
|
33
34
|
}
|
|
35
|
+
else if (key === 'llmProvider') {
|
|
36
|
+
stored.llmProvider = value;
|
|
37
|
+
}
|
|
38
|
+
else if (key === 'llmApiKey') {
|
|
39
|
+
stored.llmApiKey = this.encryptSecret(value);
|
|
40
|
+
}
|
|
34
41
|
stored.updatedAt = new Date().toISOString();
|
|
35
42
|
// Save credentials
|
|
36
43
|
await this.saveStoredCredentials(stored);
|
|
@@ -63,9 +70,45 @@ export class CredentialStorageService {
|
|
|
63
70
|
}
|
|
64
71
|
return {
|
|
65
72
|
clientId: stored.clientId,
|
|
66
|
-
clientSecret: stored.clientSecret ? '[STORED]' : null
|
|
73
|
+
clientSecret: stored.clientSecret ? '[STORED]' : null,
|
|
74
|
+
llmProvider: stored.llmProvider || null,
|
|
75
|
+
llmApiKey: stored.llmApiKey ? '[STORED]' : null
|
|
67
76
|
};
|
|
68
77
|
}
|
|
78
|
+
/**
|
|
79
|
+
* Get the LLM provider name
|
|
80
|
+
* @returns The provider name or null if not configured
|
|
81
|
+
*/
|
|
82
|
+
async getLlmProvider() {
|
|
83
|
+
const stored = await this.loadStoredCredentials();
|
|
84
|
+
return stored?.llmProvider || null;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Get the LLM API key, with fallback to environment variable
|
|
88
|
+
* @returns The API key or null if not configured
|
|
89
|
+
*/
|
|
90
|
+
async getLlmApiKey() {
|
|
91
|
+
// First try stored credentials
|
|
92
|
+
const stored = await this.loadStoredCredentials();
|
|
93
|
+
if (stored?.llmApiKey) {
|
|
94
|
+
try {
|
|
95
|
+
return this.decryptSecret(stored.llmApiKey);
|
|
96
|
+
}
|
|
97
|
+
catch (error) {
|
|
98
|
+
console.error('Failed to decrypt LLM API key:', error);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
// Fall back to environment variable
|
|
102
|
+
return process.env.LLM_API_KEY || null;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Check if an LLM API key is available (stored or in env)
|
|
106
|
+
* @returns true if API key is configured
|
|
107
|
+
*/
|
|
108
|
+
async hasLlmApiKey() {
|
|
109
|
+
const apiKey = await this.getLlmApiKey();
|
|
110
|
+
return apiKey !== null;
|
|
111
|
+
}
|
|
69
112
|
async clearCredentials() {
|
|
70
113
|
try {
|
|
71
114
|
await fs.unlink(this.credentialsPath);
|
|
@@ -80,7 +123,13 @@ export class CredentialStorageService {
|
|
|
80
123
|
validateCredential(key, value) {
|
|
81
124
|
const trimmed = value.trim();
|
|
82
125
|
if (!trimmed) {
|
|
83
|
-
const
|
|
126
|
+
const displayNames = {
|
|
127
|
+
clientId: 'Client ID',
|
|
128
|
+
clientSecret: 'Client Secret',
|
|
129
|
+
llmProvider: 'LLM Provider',
|
|
130
|
+
llmApiKey: 'LLM API Key'
|
|
131
|
+
};
|
|
132
|
+
const displayName = displayNames[key] || key;
|
|
84
133
|
throw new Error(`${displayName} cannot be empty`);
|
|
85
134
|
}
|
|
86
135
|
// Could add format validation here if needed
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"repo-service.d.ts","sourceRoot":"","sources":["../../src/services/repo-service.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAMH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAC;AAIlE,MAAM,WAAW,4BAA4B;IAC3C,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED;;;;GAIG;AACH,qBAAa,WAAW;IAMV,OAAO,CAAC,OAAO;IAL3B,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,gBAAgB,CAA2B;IACnE,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,cAAc,CAAyB;IAE/D,OAAO,CAAC,aAAa,CAAgB;gBAEjB,OAAO,EAAE,cAAc;IAS3C;;;OAGG;IACH,OAAO,CAAC,iBAAiB;IAQzB;;OAEG;IACG,4BAA4B,CAChC,eAAe,GAAE,MAAoB,GACpC,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;IAmCzB;;;;OAIG;IACH,uBAAuB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI;IAOvD;;OAEG;IACH,cAAc,IAAI,OAAO;IAUzB;;;OAGG;IACG,uBAAuB,IAAI,OAAO,CAAC,OAAO,CAAC;IAcjD;;OAEG;IACH,kBAAkB,IAAI,MAAM;IAa5B;;;;;;;;OAQG;IACG,qBAAqB,CAAC,OAAO,GAAE,4BAAiC,GAAG,OAAO,CAAC,IAAI,CAAC;IAwFtF;;OAEG;IACH,OAAO,CAAC,0BAA0B;
|
|
1
|
+
{"version":3,"file":"repo-service.d.ts","sourceRoot":"","sources":["../../src/services/repo-service.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAMH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAC;AAIlE,MAAM,WAAW,4BAA4B;IAC3C,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED;;;;GAIG;AACH,qBAAa,WAAW;IAMV,OAAO,CAAC,OAAO;IAL3B,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,gBAAgB,CAA2B;IACnE,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,cAAc,CAAyB;IAE/D,OAAO,CAAC,aAAa,CAAgB;gBAEjB,OAAO,EAAE,cAAc;IAS3C;;;OAGG;IACH,OAAO,CAAC,iBAAiB;IAQzB;;OAEG;IACG,4BAA4B,CAChC,eAAe,GAAE,MAAoB,GACpC,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;IAmCzB;;;;OAIG;IACH,uBAAuB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI;IAOvD;;OAEG;IACH,cAAc,IAAI,OAAO;IAUzB;;;OAGG;IACG,uBAAuB,IAAI,OAAO,CAAC,OAAO,CAAC;IAcjD;;OAEG;IACH,kBAAkB,IAAI,MAAM;IAa5B;;;;;;;;OAQG;IACG,qBAAqB,CAAC,OAAO,GAAE,4BAAiC,GAAG,OAAO,CAAC,IAAI,CAAC;IAwFtF;;OAEG;IACH,OAAO,CAAC,0BAA0B;IA4GlC;;OAEG;IACH,OAAO,CAAC,wBAAwB;IAoChC;;OAEG;IACH,OAAO,CAAC,6BAA6B;IAwErC;;OAEG;IACH,OAAO,CAAC,gCAAgC;CAqCzC"}
|
|
@@ -229,7 +229,7 @@ export class RepoService {
|
|
|
229
229
|
**BEFORE EVERY COMMIT**, check if you're working on tracked work:
|
|
230
230
|
|
|
231
231
|
1. **Active PRDs?** Check \`.tiny-brain/progress/\` for \`in_progress\` status
|
|
232
|
-
2. **Open Fixes?** Check \`.tiny-brain/fixes/progress.json\` for \`
|
|
232
|
+
2. **Open Fixes?** Check \`.tiny-brain/fixes/progress.json\` for \`documented\` or \`in_progress\` status
|
|
233
233
|
|
|
234
234
|
**If YES, you MUST include tracking headers or the commit will be REJECTED.**
|
|
235
235
|
|
|
@@ -265,42 +265,53 @@ Description of changes...
|
|
|
265
265
|
|
|
266
266
|
**WARNING:** The commit-msg hook will reject commits missing required headers.
|
|
267
267
|
|
|
268
|
-
###
|
|
269
|
-
|
|
270
|
-
**IMMEDIATELY after your commit is accepted**, you MUST update the source markdown file. This is NOT optional.
|
|
268
|
+
### Updating Markdown After Commits
|
|
271
269
|
|
|
272
270
|
**For PRD tasks:**
|
|
273
271
|
1. Open the feature file: \`docs/prd/{prd-id}/features/{feature-id}.md\`
|
|
274
|
-
2.
|
|
275
|
-
\`\`\`
|
|
276
|
-
|
|
277
|
-
|
|
272
|
+
2. Update the task with status and commitSha:
|
|
273
|
+
\`\`\`markdown
|
|
274
|
+
### 1. Task description
|
|
275
|
+
status: completed
|
|
276
|
+
commitSha: abc1234
|
|
278
277
|
\`\`\`
|
|
279
|
-
3.
|
|
280
|
-
4. If ALL tasks in the feature are complete,
|
|
281
|
-
|
|
278
|
+
3. Run: \`npx tiny-brain sync-file docs/prd/{prd-id}/features/{feature-id}.md\`
|
|
279
|
+
4. If ALL tasks in the feature are complete, update the PRD status
|
|
280
|
+
|
|
281
|
+
### Fix Status Workflow
|
|
282
|
+
|
|
283
|
+
Fix documents have three statuses: \`documented\` → \`in_progress\` → \`resolved\`
|
|
282
284
|
|
|
283
|
-
**
|
|
285
|
+
**When starting work on a fix:**
|
|
284
286
|
1. Open the fix file: \`.tiny-brain/fixes/{fix-id}.md\`
|
|
285
|
-
2.
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
3.
|
|
297
|
-
|
|
298
|
-
|
|
287
|
+
2. Update frontmatter: \`status: in_progress\`
|
|
288
|
+
3. Run: \`npx tiny-brain sync-file .tiny-brain/fixes/{fix-id}.md\`
|
|
289
|
+
|
|
290
|
+
**After each commit:**
|
|
291
|
+
1. Update the completed task(s) in the markdown:
|
|
292
|
+
\`\`\`markdown
|
|
293
|
+
### 1. Task description
|
|
294
|
+
status: completed
|
|
295
|
+
commitSha: abc1234
|
|
296
|
+
\`\`\`
|
|
297
|
+
2. If one commit addresses multiple tasks, use the same commitSha for all of them
|
|
298
|
+
3. If a task is no longer needed (work done elsewhere or obsolete), mark it superseded:
|
|
299
|
+
\`\`\`markdown
|
|
300
|
+
### 3. Obsolete task
|
|
301
|
+
status: superseded
|
|
302
|
+
commitSha: null
|
|
303
|
+
\`\`\`
|
|
304
|
+
4. Run: \`npx tiny-brain sync-file .tiny-brain/fixes/{fix-id}.md\`
|
|
305
|
+
|
|
306
|
+
**When all tasks are complete:**
|
|
307
|
+
1. **ONLY set \`status: resolved\`** when ALL tasks are accounted for:
|
|
308
|
+
- 100% of tasks must have either \`status: completed\` (with commitSha) or \`status: superseded\`
|
|
309
|
+
- Example: A fix with 5 tasks could be: 3 completed + 2 superseded = resolved
|
|
299
310
|
- A fix with incomplete tasks stays \`in_progress\`
|
|
300
|
-
|
|
311
|
+
2. Update YAML frontmatter:
|
|
301
312
|
- Set \`status: resolved\`
|
|
302
313
|
- Set \`resolved: YYYY-MM-DDTHH:mm:ss.sssZ\` (ISO timestamp)
|
|
303
|
-
- Add \`resolution\` object
|
|
314
|
+
- Add \`resolution\` object:
|
|
304
315
|
\`\`\`yaml
|
|
305
316
|
resolution:
|
|
306
317
|
rootCause: Brief description of what caused the issue
|
|
@@ -311,10 +322,9 @@ Description of changes...
|
|
|
311
322
|
- path/to/file1.ts
|
|
312
323
|
- path/to/file2.ts
|
|
313
324
|
\`\`\`
|
|
325
|
+
3. Run: \`npx tiny-brain sync-file .tiny-brain/fixes/{fix-id}.md\`
|
|
314
326
|
|
|
315
|
-
**The
|
|
316
|
-
|
|
317
|
-
**Failure to update markdown files is a workflow violation.**
|
|
327
|
+
**Note:** The markdown file is the source of truth. The \`sync-file\` command updates \`progress.json\` from the markdown.
|
|
318
328
|
|
|
319
329
|
`;
|
|
320
330
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config.tool.d.ts","sourceRoot":"","sources":["../../../src/tools/config/config.tool.ts"],"names":[],"mappings":"AACA,OAAO,EAA0C,KAAK,UAAU,EAAE,MAAM,aAAa,CAAC;AACtF,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,gCAAgC,CAAC;AACrE,OAAO,EAAE,IAAI,IAAI,OAAO,EAAE,MAAM,oCAAoC,CAAC;
|
|
1
|
+
{"version":3,"file":"config.tool.d.ts","sourceRoot":"","sources":["../../../src/tools/config/config.tool.ts"],"names":[],"mappings":"AACA,OAAO,EAA0C,KAAK,UAAU,EAAE,MAAM,aAAa,CAAC;AACtF,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,gCAAgC,CAAC;AACrE,OAAO,EAAE,IAAI,IAAI,OAAO,EAAE,MAAM,oCAAoC,CAAC;AAUrE,qBAAa,UAAU;IACrB,MAAM,CAAC,iBAAiB,IAAI,OAAO;WA4DtB,OAAO,CAClB,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC7B,OAAO,EAAE,cAAc,GACtB,OAAO,CAAC,UAAU,CAAC;mBA6BD,UAAU;mBAwCV,SAAS;mBAoCT,iBAAiB;mBAgEjB,SAAS;mBAuDT,WAAW;CAqBjC"}
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import { createSuccessResult, createErrorResult } from '../index.js';
|
|
3
|
-
import { ConfigService } from '@magic-ingredients/tiny-brain-core';
|
|
4
|
-
import { TechContextService } from '../../services/tech-context-service.js';
|
|
3
|
+
import { ConfigService, TechContextService } from '@magic-ingredients/tiny-brain-core';
|
|
5
4
|
const ConfigArgsSchema = z.object({
|
|
6
5
|
operation: z.enum(['list', 'get', 'show-sources', 'set', 'reset']),
|
|
7
6
|
key: z.string().optional(),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@magic-ingredients/tiny-brain-local",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.18.1",
|
|
4
4
|
"description": "MCP server for Tiny Brain AI assistant",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"dxt:init": "cd dxt && dxt init"
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
|
-
"@magic-ingredients/tiny-brain-core": "^0.
|
|
34
|
+
"@magic-ingredients/tiny-brain-core": "^0.18.1",
|
|
35
35
|
"@magic-ingredients/tiny-brain-dashboard": "file:../tiny-brain-dashboard",
|
|
36
36
|
"@modelcontextprotocol/sdk": "^1.0.6",
|
|
37
37
|
"chalk": "^5.3.0",
|
|
@@ -1,106 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tech Context Service
|
|
3
|
-
*
|
|
4
|
-
* Manages reading and writing of tech context files:
|
|
5
|
-
* - .tiny-brain/analysis.json - repo analysis data
|
|
6
|
-
* - .tiny-brain/tech/*.md - per-tech expertise files
|
|
7
|
-
* - .tiny-brain/tech/config.json - agent mode configuration
|
|
8
|
-
* - .claude/agents/tech-*.md - tech agent files (when enableAgentic=true)
|
|
9
|
-
*/
|
|
10
|
-
import type { RepoAnalysisFile, TechExpertiseFrontmatter, TechExpertiseFile, TechConfig } from '@magic-ingredients/tiny-brain-core';
|
|
11
|
-
/** Input analysis data from repository analysis */
|
|
12
|
-
export interface AnalysisInput {
|
|
13
|
-
languages: string[];
|
|
14
|
-
frameworks: string[];
|
|
15
|
-
testingTools: string[];
|
|
16
|
-
buildTools: string[];
|
|
17
|
-
hasTests: boolean;
|
|
18
|
-
testFileCount: number;
|
|
19
|
-
testPatterns: string[];
|
|
20
|
-
isPolyglot?: boolean;
|
|
21
|
-
primaryLanguage?: string;
|
|
22
|
-
documentationPattern?: 'single-readme' | 'folder-readmes' | 'docs-folder' | 'mixed';
|
|
23
|
-
documentationLocations?: string[];
|
|
24
|
-
}
|
|
25
|
-
export declare class TechContextService {
|
|
26
|
-
private readonly tinyBrainDir;
|
|
27
|
-
private readonly techDir;
|
|
28
|
-
private readonly agentsDir;
|
|
29
|
-
constructor(repoPath: string);
|
|
30
|
-
/** Get the .tiny-brain directory path */
|
|
31
|
-
getTinyBrainDir(): string;
|
|
32
|
-
/** Get the .tiny-brain/tech directory path */
|
|
33
|
-
getTechDir(): string;
|
|
34
|
-
/** Get the .claude/agents directory path */
|
|
35
|
-
getAgentsDir(): string;
|
|
36
|
-
/** Ensure required directories exist */
|
|
37
|
-
ensureDirectories(): Promise<void>;
|
|
38
|
-
/**
|
|
39
|
-
* Write analysis data to .tiny-brain/analysis.json
|
|
40
|
-
*/
|
|
41
|
-
writeAnalysis(analysis: AnalysisInput): Promise<void>;
|
|
42
|
-
/**
|
|
43
|
-
* Write a tech expertise file with YAML frontmatter
|
|
44
|
-
*/
|
|
45
|
-
writeTechFile(name: string, frontmatter: TechExpertiseFrontmatter, content: string): Promise<void>;
|
|
46
|
-
/**
|
|
47
|
-
* Write raw markdown content to a tech file
|
|
48
|
-
* Used when receiving complete markdown from TBR API
|
|
49
|
-
*/
|
|
50
|
-
writeTechFileRaw(name: string, content: string): Promise<void>;
|
|
51
|
-
/**
|
|
52
|
-
* Read analysis data from .tiny-brain/analysis.json
|
|
53
|
-
*/
|
|
54
|
-
readAnalysis(): Promise<RepoAnalysisFile | null>;
|
|
55
|
-
/**
|
|
56
|
-
* Read all tech expertise files from .tiny-brain/tech/
|
|
57
|
-
*/
|
|
58
|
-
readTechFiles(): Promise<TechExpertiseFile[]>;
|
|
59
|
-
/**
|
|
60
|
-
* Get tech files that match a given file path based on filePatterns
|
|
61
|
-
*/
|
|
62
|
-
getTechForFile(filePath: string): Promise<TechExpertiseFile[]>;
|
|
63
|
-
/**
|
|
64
|
-
* Check if the tech stack has changed compared to previous analysis
|
|
65
|
-
*/
|
|
66
|
-
hasStackChanged(analysis: AnalysisInput): Promise<boolean>;
|
|
67
|
-
/**
|
|
68
|
-
* Write config to .tiny-brain/tech/config.json
|
|
69
|
-
*/
|
|
70
|
-
writeConfig(config: Omit<TechConfig, 'lastSynced'>): Promise<void>;
|
|
71
|
-
/**
|
|
72
|
-
* Read config from .tiny-brain/tech/config.json
|
|
73
|
-
*/
|
|
74
|
-
readConfig(): Promise<TechConfig>;
|
|
75
|
-
/**
|
|
76
|
-
* Install tech agents to .claude/agents/
|
|
77
|
-
* Converts tech context files into proper Claude Code sub-agents with:
|
|
78
|
-
* - name: tech-{name} (matches subagent_type="tech-react")
|
|
79
|
-
* - description: for Claude Code auto-delegation
|
|
80
|
-
* - Sub-agent invocation context
|
|
81
|
-
*/
|
|
82
|
-
installTechAgents(): Promise<void>;
|
|
83
|
-
/**
|
|
84
|
-
* Remove only tech-*.md files from .claude/agents/
|
|
85
|
-
*/
|
|
86
|
-
removeTechAgents(): Promise<void>;
|
|
87
|
-
/**
|
|
88
|
-
* Sync agents based on enableAgentic preference
|
|
89
|
-
*/
|
|
90
|
-
syncAgents(enableAgentic: boolean): Promise<void>;
|
|
91
|
-
/**
|
|
92
|
-
* Get versions of all local tech context files
|
|
93
|
-
* @returns Map of tech name → version
|
|
94
|
-
*/
|
|
95
|
-
getLocalTechVersions(): Promise<Map<string, string>>;
|
|
96
|
-
/**
|
|
97
|
-
* Compare versions to determine if TBS version is newer
|
|
98
|
-
* @returns true if tbsVersion is newer than localVersion
|
|
99
|
-
*/
|
|
100
|
-
shouldUpdateTech(localVersion: string, tbsVersion: string): boolean;
|
|
101
|
-
/**
|
|
102
|
-
* Parse YAML frontmatter from a markdown file
|
|
103
|
-
*/
|
|
104
|
-
private parseFrontmatter;
|
|
105
|
-
}
|
|
106
|
-
//# sourceMappingURL=tech-context-service.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"tech-context-service.d.ts","sourceRoot":"","sources":["../../src/services/tech-context-service.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAMH,OAAO,KAAK,EACV,gBAAgB,EAGhB,wBAAwB,EACxB,iBAAiB,EACjB,UAAU,EACX,MAAM,oCAAoC,CAAC;AAE5C,mDAAmD;AACnD,MAAM,WAAW,aAAa;IAC5B,SAAS,EAAE,MAAM,EAAE,CAAC;IACpB,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,QAAQ,EAAE,OAAO,CAAC;IAClB,aAAa,EAAE,MAAM,CAAC;IACtB,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,oBAAoB,CAAC,EAAE,eAAe,GAAG,gBAAgB,GAAG,aAAa,GAAG,OAAO,CAAC;IACpF,sBAAsB,CAAC,EAAE,MAAM,EAAE,CAAC;CACnC;AAED,qBAAa,kBAAkB;IAC7B,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAS;IACtC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;IACjC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;gBAEvB,QAAQ,EAAE,MAAM;IAM5B,yCAAyC;IACzC,eAAe,IAAI,MAAM;IAIzB,8CAA8C;IAC9C,UAAU,IAAI,MAAM;IAIpB,4CAA4C;IAC5C,YAAY,IAAI,MAAM;IAItB,wCAAwC;IAClC,iBAAiB,IAAI,OAAO,CAAC,IAAI,CAAC;IAKxC;;OAEG;IACG,aAAa,CAAC,QAAQ,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC;IAoC3D;;OAEG;IACG,aAAa,CACjB,IAAI,EAAE,MAAM,EACZ,WAAW,EAAE,wBAAwB,EACrC,OAAO,EAAE,MAAM,GACd,OAAO,CAAC,IAAI,CAAC;IAmBhB;;;OAGG;IACG,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAMpE;;OAEG;IACG,YAAY,IAAI,OAAO,CAAC,gBAAgB,GAAG,IAAI,CAAC;IAWtD;;OAEG;IACG,aAAa,IAAI,OAAO,CAAC,iBAAiB,EAAE,CAAC;IA0BnD;;OAEG;IACG,cAAc,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,iBAAiB,EAAE,CAAC;IAwBpE;;OAEG;IACG,eAAe,CAAC,QAAQ,EAAE,aAAa,GAAG,OAAO,CAAC,OAAO,CAAC;IA4BhE;;OAEG;IACG,WAAW,CAAC,MAAM,EAAE,IAAI,CAAC,UAAU,EAAE,YAAY,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC;IAYxE;;OAEG;IACG,UAAU,IAAI,OAAO,CAAC,UAAU,CAAC;IAWvC;;;;;;OAMG;IACG,iBAAiB,IAAI,OAAO,CAAC,IAAI,CAAC;IAkCxC;;OAEG;IACG,gBAAgB,IAAI,OAAO,CAAC,IAAI,CAAC;IAcvC;;OAEG;IACG,UAAU,CAAC,aAAa,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC;IAUvD;;;OAGG;IACG,oBAAoB,IAAI,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAW1D;;;OAGG;IACH,gBAAgB,CAAC,YAAY,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO;IAsBnE;;OAEG;IACH,OAAO,CAAC,gBAAgB;CA+CzB"}
|
|
@@ -1,365 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tech Context Service
|
|
3
|
-
*
|
|
4
|
-
* Manages reading and writing of tech context files:
|
|
5
|
-
* - .tiny-brain/analysis.json - repo analysis data
|
|
6
|
-
* - .tiny-brain/tech/*.md - per-tech expertise files
|
|
7
|
-
* - .tiny-brain/tech/config.json - agent mode configuration
|
|
8
|
-
* - .claude/agents/tech-*.md - tech agent files (when enableAgentic=true)
|
|
9
|
-
*/
|
|
10
|
-
import { promises as fs } from 'fs';
|
|
11
|
-
import path from 'path';
|
|
12
|
-
import crypto from 'crypto';
|
|
13
|
-
import minimatch from 'minimatch';
|
|
14
|
-
export class TechContextService {
|
|
15
|
-
tinyBrainDir;
|
|
16
|
-
techDir;
|
|
17
|
-
agentsDir;
|
|
18
|
-
constructor(repoPath) {
|
|
19
|
-
this.tinyBrainDir = path.join(repoPath, '.tiny-brain');
|
|
20
|
-
this.techDir = path.join(this.tinyBrainDir, 'tech');
|
|
21
|
-
this.agentsDir = path.join(repoPath, '.claude', 'agents');
|
|
22
|
-
}
|
|
23
|
-
/** Get the .tiny-brain directory path */
|
|
24
|
-
getTinyBrainDir() {
|
|
25
|
-
return this.tinyBrainDir;
|
|
26
|
-
}
|
|
27
|
-
/** Get the .tiny-brain/tech directory path */
|
|
28
|
-
getTechDir() {
|
|
29
|
-
return this.techDir;
|
|
30
|
-
}
|
|
31
|
-
/** Get the .claude/agents directory path */
|
|
32
|
-
getAgentsDir() {
|
|
33
|
-
return this.agentsDir;
|
|
34
|
-
}
|
|
35
|
-
/** Ensure required directories exist */
|
|
36
|
-
async ensureDirectories() {
|
|
37
|
-
await fs.mkdir(this.tinyBrainDir, { recursive: true });
|
|
38
|
-
await fs.mkdir(this.techDir, { recursive: true });
|
|
39
|
-
}
|
|
40
|
-
/**
|
|
41
|
-
* Write analysis data to .tiny-brain/analysis.json
|
|
42
|
-
*/
|
|
43
|
-
async writeAnalysis(analysis) {
|
|
44
|
-
await this.ensureDirectories();
|
|
45
|
-
const stack = {
|
|
46
|
-
languages: analysis.languages,
|
|
47
|
-
frameworks: analysis.frameworks,
|
|
48
|
-
testing: analysis.testingTools,
|
|
49
|
-
build: analysis.buildTools,
|
|
50
|
-
};
|
|
51
|
-
const analysisData = {
|
|
52
|
-
hasTests: analysis.hasTests,
|
|
53
|
-
testFileCount: analysis.testFileCount,
|
|
54
|
-
testPatterns: analysis.testPatterns,
|
|
55
|
-
isPolyglot: analysis.isPolyglot ?? false,
|
|
56
|
-
primaryLanguage: analysis.primaryLanguage ?? analysis.languages[0] ?? 'unknown',
|
|
57
|
-
documentationPattern: analysis.documentationPattern,
|
|
58
|
-
documentationLocations: analysis.documentationLocations,
|
|
59
|
-
};
|
|
60
|
-
// Generate hash from stable analysis input
|
|
61
|
-
const hashInput = JSON.stringify({ stack, analysis: analysisData });
|
|
62
|
-
const analysisHash = crypto.createHash('sha256').update(hashInput).digest('hex');
|
|
63
|
-
const file = {
|
|
64
|
-
version: '1.0',
|
|
65
|
-
detectedAt: new Date().toISOString(),
|
|
66
|
-
analysisHash,
|
|
67
|
-
stack,
|
|
68
|
-
analysis: analysisData,
|
|
69
|
-
};
|
|
70
|
-
const filePath = path.join(this.tinyBrainDir, 'analysis.json');
|
|
71
|
-
await fs.writeFile(filePath, JSON.stringify(file, null, 2), 'utf-8');
|
|
72
|
-
}
|
|
73
|
-
/**
|
|
74
|
-
* Write a tech expertise file with YAML frontmatter
|
|
75
|
-
*/
|
|
76
|
-
async writeTechFile(name, frontmatter, content) {
|
|
77
|
-
await this.ensureDirectories();
|
|
78
|
-
const yamlFrontmatter = [
|
|
79
|
-
'---',
|
|
80
|
-
`name: ${frontmatter.name}`,
|
|
81
|
-
`version: ${frontmatter.version}`,
|
|
82
|
-
`domain: ${frontmatter.domain}`,
|
|
83
|
-
`filePatterns:`,
|
|
84
|
-
...frontmatter.filePatterns.map(p => ` - "${p}"`),
|
|
85
|
-
frontmatter.description ? `description: "${frontmatter.description}"` : null,
|
|
86
|
-
'---',
|
|
87
|
-
].filter(Boolean).join('\n');
|
|
88
|
-
const fileContent = `${yamlFrontmatter}\n\n${content}`;
|
|
89
|
-
const filePath = path.join(this.techDir, `${name}.md`);
|
|
90
|
-
await fs.writeFile(filePath, fileContent, 'utf-8');
|
|
91
|
-
}
|
|
92
|
-
/**
|
|
93
|
-
* Write raw markdown content to a tech file
|
|
94
|
-
* Used when receiving complete markdown from TBR API
|
|
95
|
-
*/
|
|
96
|
-
async writeTechFileRaw(name, content) {
|
|
97
|
-
await this.ensureDirectories();
|
|
98
|
-
const filePath = path.join(this.techDir, `${name}.md`);
|
|
99
|
-
await fs.writeFile(filePath, content, 'utf-8');
|
|
100
|
-
}
|
|
101
|
-
/**
|
|
102
|
-
* Read analysis data from .tiny-brain/analysis.json
|
|
103
|
-
*/
|
|
104
|
-
async readAnalysis() {
|
|
105
|
-
const filePath = path.join(this.tinyBrainDir, 'analysis.json');
|
|
106
|
-
try {
|
|
107
|
-
const content = await fs.readFile(filePath, 'utf-8');
|
|
108
|
-
return JSON.parse(content);
|
|
109
|
-
}
|
|
110
|
-
catch {
|
|
111
|
-
return null;
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
/**
|
|
115
|
-
* Read all tech expertise files from .tiny-brain/tech/
|
|
116
|
-
*/
|
|
117
|
-
async readTechFiles() {
|
|
118
|
-
try {
|
|
119
|
-
const files = await fs.readdir(this.techDir);
|
|
120
|
-
const techFiles = [];
|
|
121
|
-
for (const file of files) {
|
|
122
|
-
if (!file.endsWith('.md'))
|
|
123
|
-
continue;
|
|
124
|
-
const filePath = path.join(this.techDir, file);
|
|
125
|
-
const content = await fs.readFile(filePath, 'utf-8');
|
|
126
|
-
const parsed = this.parseFrontmatter(content);
|
|
127
|
-
if (parsed) {
|
|
128
|
-
techFiles.push({
|
|
129
|
-
...parsed,
|
|
130
|
-
filePath,
|
|
131
|
-
});
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
return techFiles;
|
|
135
|
-
}
|
|
136
|
-
catch {
|
|
137
|
-
return [];
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
/**
|
|
141
|
-
* Get tech files that match a given file path based on filePatterns
|
|
142
|
-
*/
|
|
143
|
-
async getTechForFile(filePath) {
|
|
144
|
-
const techFiles = await this.readTechFiles();
|
|
145
|
-
const matches = [];
|
|
146
|
-
const basename = path.basename(filePath);
|
|
147
|
-
for (const techFile of techFiles) {
|
|
148
|
-
for (const pattern of techFile.frontmatter.filePatterns) {
|
|
149
|
-
// Check multiple matching strategies:
|
|
150
|
-
// 1. Full path glob match
|
|
151
|
-
// 2. Basename match (for patterns like *.tsx)
|
|
152
|
-
// 3. Directory prefix match (for patterns like components/)
|
|
153
|
-
if (minimatch(filePath, pattern) ||
|
|
154
|
-
minimatch(basename, pattern) ||
|
|
155
|
-
minimatch(filePath, `**/${pattern}`) ||
|
|
156
|
-
filePath.includes(pattern.replace(/\/$/, ''))) {
|
|
157
|
-
matches.push(techFile);
|
|
158
|
-
break; // Don't add same file multiple times
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
return matches;
|
|
163
|
-
}
|
|
164
|
-
/**
|
|
165
|
-
* Check if the tech stack has changed compared to previous analysis
|
|
166
|
-
*/
|
|
167
|
-
async hasStackChanged(analysis) {
|
|
168
|
-
const existing = await this.readAnalysis();
|
|
169
|
-
if (!existing)
|
|
170
|
-
return true;
|
|
171
|
-
// Generate hash for new analysis using same logic as writeAnalysis
|
|
172
|
-
const stack = {
|
|
173
|
-
languages: analysis.languages,
|
|
174
|
-
frameworks: analysis.frameworks,
|
|
175
|
-
testing: analysis.testingTools,
|
|
176
|
-
build: analysis.buildTools,
|
|
177
|
-
};
|
|
178
|
-
const analysisData = {
|
|
179
|
-
hasTests: analysis.hasTests,
|
|
180
|
-
testFileCount: analysis.testFileCount,
|
|
181
|
-
testPatterns: analysis.testPatterns,
|
|
182
|
-
isPolyglot: analysis.isPolyglot ?? false,
|
|
183
|
-
primaryLanguage: analysis.primaryLanguage ?? analysis.languages[0] ?? 'unknown',
|
|
184
|
-
documentationPattern: analysis.documentationPattern,
|
|
185
|
-
documentationLocations: analysis.documentationLocations,
|
|
186
|
-
};
|
|
187
|
-
const hashInput = JSON.stringify({ stack, analysis: analysisData });
|
|
188
|
-
const newHash = crypto.createHash('sha256').update(hashInput).digest('hex');
|
|
189
|
-
return newHash !== existing.analysisHash;
|
|
190
|
-
}
|
|
191
|
-
/**
|
|
192
|
-
* Write config to .tiny-brain/tech/config.json
|
|
193
|
-
*/
|
|
194
|
-
async writeConfig(config) {
|
|
195
|
-
await this.ensureDirectories();
|
|
196
|
-
const fullConfig = {
|
|
197
|
-
...config,
|
|
198
|
-
lastSynced: new Date().toISOString(),
|
|
199
|
-
};
|
|
200
|
-
const filePath = path.join(this.techDir, 'config.json');
|
|
201
|
-
await fs.writeFile(filePath, JSON.stringify(fullConfig, null, 2), 'utf-8');
|
|
202
|
-
}
|
|
203
|
-
/**
|
|
204
|
-
* Read config from .tiny-brain/tech/config.json
|
|
205
|
-
*/
|
|
206
|
-
async readConfig() {
|
|
207
|
-
const filePath = path.join(this.techDir, 'config.json');
|
|
208
|
-
try {
|
|
209
|
-
const content = await fs.readFile(filePath, 'utf-8');
|
|
210
|
-
return JSON.parse(content);
|
|
211
|
-
}
|
|
212
|
-
catch {
|
|
213
|
-
return { useAgents: false };
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
/**
|
|
217
|
-
* Install tech agents to .claude/agents/
|
|
218
|
-
* Converts tech context files into proper Claude Code sub-agents with:
|
|
219
|
-
* - name: tech-{name} (matches subagent_type="tech-react")
|
|
220
|
-
* - description: for Claude Code auto-delegation
|
|
221
|
-
* - Sub-agent invocation context
|
|
222
|
-
*/
|
|
223
|
-
async installTechAgents() {
|
|
224
|
-
await fs.mkdir(this.agentsDir, { recursive: true });
|
|
225
|
-
const techFiles = await this.readTechFiles();
|
|
226
|
-
for (const techFile of techFiles) {
|
|
227
|
-
const name = techFile.frontmatter.name;
|
|
228
|
-
const agentFileName = `tech-${name}.md`;
|
|
229
|
-
const agentPath = path.join(this.agentsDir, agentFileName);
|
|
230
|
-
// Build description from frontmatter or generate default
|
|
231
|
-
const description = techFile.frontmatter.description
|
|
232
|
-
|| `${name} development specialist. Use for ${techFile.frontmatter.domain} tasks involving ${name}.`;
|
|
233
|
-
// Create agent file with proper Claude Code sub-agent format
|
|
234
|
-
const agentContent = `---
|
|
235
|
-
name: tech-${name}
|
|
236
|
-
description: ${description}
|
|
237
|
-
version: ${techFile.frontmatter.version}
|
|
238
|
-
domain: ${techFile.frontmatter.domain}
|
|
239
|
-
---
|
|
240
|
-
|
|
241
|
-
# ${name} Sub-Agent
|
|
242
|
-
|
|
243
|
-
You are a specialized ${name} development agent invoked by the developer agent. Apply the expertise and patterns below to the task you've been given.
|
|
244
|
-
|
|
245
|
-
## Tech Expertise
|
|
246
|
-
|
|
247
|
-
${techFile.content}`;
|
|
248
|
-
await fs.writeFile(agentPath, agentContent, 'utf-8');
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
/**
|
|
252
|
-
* Remove only tech-*.md files from .claude/agents/
|
|
253
|
-
*/
|
|
254
|
-
async removeTechAgents() {
|
|
255
|
-
try {
|
|
256
|
-
const files = await fs.readdir(this.agentsDir);
|
|
257
|
-
for (const file of files) {
|
|
258
|
-
if (file.startsWith('tech-') && file.endsWith('.md')) {
|
|
259
|
-
await fs.unlink(path.join(this.agentsDir, file));
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
catch {
|
|
264
|
-
// Directory may not exist, ignore
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
/**
|
|
268
|
-
* Sync agents based on enableAgentic preference
|
|
269
|
-
*/
|
|
270
|
-
async syncAgents(enableAgentic) {
|
|
271
|
-
await this.writeConfig({ useAgents: enableAgentic });
|
|
272
|
-
if (enableAgentic) {
|
|
273
|
-
await this.installTechAgents();
|
|
274
|
-
}
|
|
275
|
-
else {
|
|
276
|
-
await this.removeTechAgents();
|
|
277
|
-
}
|
|
278
|
-
}
|
|
279
|
-
/**
|
|
280
|
-
* Get versions of all local tech context files
|
|
281
|
-
* @returns Map of tech name → version
|
|
282
|
-
*/
|
|
283
|
-
async getLocalTechVersions() {
|
|
284
|
-
const techFiles = await this.readTechFiles();
|
|
285
|
-
const versions = new Map();
|
|
286
|
-
for (const file of techFiles) {
|
|
287
|
-
versions.set(file.frontmatter.name, file.frontmatter.version);
|
|
288
|
-
}
|
|
289
|
-
return versions;
|
|
290
|
-
}
|
|
291
|
-
/**
|
|
292
|
-
* Compare versions to determine if TBS version is newer
|
|
293
|
-
* @returns true if tbsVersion is newer than localVersion
|
|
294
|
-
*/
|
|
295
|
-
shouldUpdateTech(localVersion, tbsVersion) {
|
|
296
|
-
const parseVersion = (v) => {
|
|
297
|
-
return v.split('.').map(s => parseInt(s, 10) || 0);
|
|
298
|
-
};
|
|
299
|
-
const local = parseVersion(localVersion);
|
|
300
|
-
const tbs = parseVersion(tbsVersion);
|
|
301
|
-
// Pad arrays to same length
|
|
302
|
-
const maxLen = Math.max(local.length, tbs.length);
|
|
303
|
-
while (local.length < maxLen)
|
|
304
|
-
local.push(0);
|
|
305
|
-
while (tbs.length < maxLen)
|
|
306
|
-
tbs.push(0);
|
|
307
|
-
// Compare each segment
|
|
308
|
-
for (let i = 0; i < maxLen; i++) {
|
|
309
|
-
if (tbs[i] > local[i])
|
|
310
|
-
return true;
|
|
311
|
-
if (tbs[i] < local[i])
|
|
312
|
-
return false;
|
|
313
|
-
}
|
|
314
|
-
return false; // Versions are equal
|
|
315
|
-
}
|
|
316
|
-
/**
|
|
317
|
-
* Parse YAML frontmatter from a markdown file
|
|
318
|
-
*/
|
|
319
|
-
parseFrontmatter(content) {
|
|
320
|
-
const match = content.match(/^---\n([\s\S]*?)\n---\n\n?([\s\S]*)$/);
|
|
321
|
-
if (!match)
|
|
322
|
-
return null;
|
|
323
|
-
const yamlContent = match[1];
|
|
324
|
-
const markdownContent = match[2];
|
|
325
|
-
// Simple YAML parsing for our known structure
|
|
326
|
-
const frontmatter = {};
|
|
327
|
-
const lines = yamlContent.split('\n');
|
|
328
|
-
let currentKey = '';
|
|
329
|
-
let filePatterns = [];
|
|
330
|
-
for (const line of lines) {
|
|
331
|
-
const keyMatch = line.match(/^(\w+):\s*(.*)$/);
|
|
332
|
-
if (keyMatch) {
|
|
333
|
-
currentKey = keyMatch[1];
|
|
334
|
-
const value = keyMatch[2].replace(/^["']|["']$/g, '');
|
|
335
|
-
if (currentKey === 'filePatterns') {
|
|
336
|
-
filePatterns = [];
|
|
337
|
-
}
|
|
338
|
-
else if (currentKey === 'name') {
|
|
339
|
-
frontmatter.name = value;
|
|
340
|
-
}
|
|
341
|
-
else if (currentKey === 'version') {
|
|
342
|
-
frontmatter.version = value;
|
|
343
|
-
}
|
|
344
|
-
else if (currentKey === 'domain') {
|
|
345
|
-
frontmatter.domain = value;
|
|
346
|
-
}
|
|
347
|
-
else if (currentKey === 'description') {
|
|
348
|
-
frontmatter.description = value;
|
|
349
|
-
}
|
|
350
|
-
}
|
|
351
|
-
else if (currentKey === 'filePatterns' && line.trim().startsWith('-')) {
|
|
352
|
-
const pattern = line.trim().replace(/^-\s*/, '').replace(/^["']|["']$/g, '');
|
|
353
|
-
filePatterns.push(pattern);
|
|
354
|
-
}
|
|
355
|
-
}
|
|
356
|
-
frontmatter.filePatterns = filePatterns;
|
|
357
|
-
if (!frontmatter.name || !frontmatter.version || !frontmatter.domain) {
|
|
358
|
-
return null;
|
|
359
|
-
}
|
|
360
|
-
return {
|
|
361
|
-
frontmatter: frontmatter,
|
|
362
|
-
content: markdownContent,
|
|
363
|
-
};
|
|
364
|
-
}
|
|
365
|
-
}
|