@gotza02/sequential-thinking 2026.2.17 → 2026.2.19

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/README.md CHANGED
@@ -4,7 +4,7 @@ Advanced MCP Server enabling AI to act as a Software Engineer with Deep Thinking
4
4
 
5
5
  ## ✨ Key Features
6
6
  - **Deep Thinking:** Step-by-step reasoning with auto-correction (Loop Breaker).
7
- - **Code Intelligence:** Dependency graph mapping (`build_project_graph`) & surgical code editing.
7
+ - **Code Intelligence:** Dependency graph mapping (`build_project_graph`) with **Incremental Caching** & surgical code editing.
8
8
  - **Memory:** Long-term project notes, reusable code database, and thought history.
9
9
  - **Research:** Integrated Web search (Brave/Exa) & webpage reading.
10
10
  - **REST API:** Built-in HTTP server wrapper for easy integration with web tools and external services.
@@ -62,7 +62,7 @@ Add this to your MCP settings (`~/.gemini/settings.json` or `claude_desktop_conf
62
62
 
63
63
  ## 🛠️ Tools Summary
64
64
  - **Thinking:** `sequentialthinking`, `summarize_history`, `clear_thought_history`
65
- - **Code:** `build_project_graph`, `deep_code_analyze`, `deep_code_edit`, `search_code`
65
+ - **Code:** `build_project_graph`, `deep_code_analyze`, `deep_code_edit`, `search_code` (Regex/Line Numbers)
66
66
  - **Memory:** `add_code_snippet`, `search_code_db`, `manage_notes`
67
67
  - **Web:** `web_search`, `read_webpage`, `fetch`
68
68
  - **System:** `read_file`, `write_file`, `edit_file`, `shell_execute`
@@ -113,22 +113,40 @@ describe('FileSystem Tools', () => {
113
113
  });
114
114
  });
115
115
  describe('search_code', () => {
116
- it('should find pattern in single file', async () => {
116
+ it('should find pattern in single file with line number', async () => {
117
117
  registerFileSystemTools(mockServer);
118
118
  const handler = registeredTools['search_code'];
119
119
  fs.stat.mockResolvedValue({ isFile: () => true });
120
- fs.readFile.mockResolvedValue('const x = "target";');
120
+ fs.readFile.mockResolvedValue('line1\nconst x = "target";\nline3');
121
121
  const result = await handler({ pattern: 'target', path: 'file.ts' });
122
- expect(result.content[0].text).toContain('Found "target" in');
123
- expect(result.content[0].text).toContain('file.ts');
122
+ expect(result.content[0].text).toContain('Found matches for "target"');
123
+ expect(result.content[0].text).toContain('file.ts:2: const x = "target";');
124
+ });
125
+ it('should use regex when requested', async () => {
126
+ registerFileSystemTools(mockServer);
127
+ const handler = registeredTools['search_code'];
128
+ fs.stat.mockResolvedValue({ isFile: () => true });
129
+ fs.readFile.mockResolvedValue('const x = 123;');
130
+ const result = await handler({ pattern: '\\d+', path: 'file.ts', useRegex: true });
131
+ expect(result.content[0].text).toContain('Found matches');
132
+ expect(result.content[0].text).toContain('file.ts:1: const x = 123;');
133
+ });
134
+ it('should handle case sensitivity', async () => {
135
+ registerFileSystemTools(mockServer);
136
+ const handler = registeredTools['search_code'];
137
+ fs.stat.mockResolvedValue({ isFile: () => true });
138
+ fs.readFile.mockResolvedValue('TARGET');
139
+ // Case sensitive search for lowercase 'target' should fail
140
+ const result = await handler({ pattern: 'target', path: 'file.ts', caseSensitive: true });
141
+ expect(result.content[0].text).toContain('No matches found');
142
+ // Case insensitive (default) should pass
143
+ const result2 = await handler({ pattern: 'target', path: 'file.ts', caseSensitive: false });
144
+ expect(result2.content[0].text).toContain('Found matches');
124
145
  });
125
146
  it('should recursively search directory ignoring node_modules', async () => {
126
147
  registerFileSystemTools(mockServer);
127
148
  const handler = registeredTools['search_code'];
128
- // Mock file system structure
129
- // Root -> [src (dir), node_modules (dir), root.ts (file)]
130
- // src -> [deep.ts (file)]
131
- fs.stat.mockResolvedValue({ isFile: () => false }); // Root is dir
149
+ fs.stat.mockResolvedValue({ isFile: () => false });
132
150
  fs.readdir.mockImplementation(async (dirPath) => {
133
151
  if (dirPath.endsWith('src')) {
134
152
  return [{ name: 'deep.ts', isDirectory: () => false }];
@@ -136,7 +154,6 @@ describe('FileSystem Tools', () => {
136
154
  if (dirPath.endsWith('node_modules')) {
137
155
  return [{ name: 'lib.ts', isDirectory: () => false }];
138
156
  }
139
- // Root
140
157
  return [
141
158
  { name: 'src', isDirectory: () => true },
142
159
  { name: 'node_modules', isDirectory: () => true },
@@ -148,13 +165,11 @@ describe('FileSystem Tools', () => {
148
165
  return 'no match';
149
166
  if (filePath.includes('deep.ts'))
150
167
  return 'const a = "target";';
151
- if (filePath.includes('lib.ts'))
152
- return 'const a = "target";'; // Should be ignored
153
168
  return '';
154
169
  });
155
170
  const result = await handler({ pattern: 'target', path: '.' });
156
- expect(result.content[0].text).toContain('deep.ts');
157
- expect(result.content[0].text).not.toContain('lib.ts'); // Should ignore node_modules
171
+ expect(result.content[0].text).toContain('deep.ts:1: const a = "target";');
172
+ expect(result.content[0].text).not.toContain('lib.ts');
158
173
  });
159
174
  });
160
175
  // Keeping original security tests if needed, or merging them.
package/dist/graph.js CHANGED
@@ -4,16 +4,20 @@ import ts from 'typescript';
4
4
  export class ProjectKnowledgeGraph {
5
5
  nodes = new Map();
6
6
  rootDir = '';
7
+ cache = { version: '1.0', files: {} };
8
+ cachePath = '';
7
9
  constructor() { }
8
10
  async build(rootDir) {
9
11
  try {
10
12
  this.rootDir = path.resolve(rootDir);
13
+ this.cachePath = path.join(this.rootDir, '.gemini_graph_cache.json');
11
14
  // Check if rootDir exists and is a directory
12
15
  const stats = await fs.stat(this.rootDir);
13
16
  if (!stats.isDirectory()) {
14
17
  throw new Error(`Path '${rootDir}' is not a directory.`);
15
18
  }
16
19
  this.nodes.clear();
20
+ await this.loadCache();
17
21
  const files = await this.getAllFiles(this.rootDir);
18
22
  // Step 1: Initialize nodes
19
23
  for (const file of files) {
@@ -24,15 +28,56 @@ export class ProjectKnowledgeGraph {
24
28
  symbols: []
25
29
  });
26
30
  }
27
- // Step 2: Parse imports and build edges in parallel with concurrency limit
31
+ // Step 2: Identify files to parse vs cache hits
32
+ const filesToParse = [];
33
+ const extractionMap = new Map();
34
+ for (const file of files) {
35
+ try {
36
+ const stats = await fs.stat(file);
37
+ const cached = this.cache.files[file];
38
+ if (cached && cached.mtime === stats.mtimeMs) {
39
+ extractionMap.set(file, { imports: cached.imports, symbols: cached.symbols });
40
+ }
41
+ else {
42
+ filesToParse.push(file);
43
+ }
44
+ }
45
+ catch (e) {
46
+ filesToParse.push(file);
47
+ }
48
+ }
49
+ // Step 3: Parse new/modified files concurrently
28
50
  const CONCURRENCY_LIMIT = 20;
29
- for (let i = 0; i < files.length; i += CONCURRENCY_LIMIT) {
30
- const chunk = files.slice(i, i + CONCURRENCY_LIMIT);
31
- await Promise.all(chunk.map(file => this.parseFile(file)));
51
+ for (let i = 0; i < filesToParse.length; i += CONCURRENCY_LIMIT) {
52
+ const chunk = filesToParse.slice(i, i + CONCURRENCY_LIMIT);
53
+ await Promise.all(chunk.map(async (file) => {
54
+ const data = await this.parseFile(file);
55
+ extractionMap.set(file, data);
56
+ // Update cache
57
+ const stats = await fs.stat(file);
58
+ this.cache.files[file] = {
59
+ mtime: stats.mtimeMs,
60
+ imports: data.imports,
61
+ symbols: data.symbols
62
+ };
63
+ }));
64
+ }
65
+ // Prune deleted files from cache
66
+ for (const cachedFile of Object.keys(this.cache.files)) {
67
+ if (!this.nodes.has(cachedFile)) {
68
+ delete this.cache.files[cachedFile];
69
+ }
70
+ }
71
+ await this.saveCache();
72
+ // Step 4: Link Graph (Resolve imports and build importedBy)
73
+ for (const [filePath, data] of extractionMap.entries()) {
74
+ await this.linkFileNodes(filePath, data.imports, data.symbols);
32
75
  }
33
76
  return {
34
77
  nodeCount: this.nodes.size,
35
- totalFiles: files.length
78
+ totalFiles: files.length,
79
+ cachedFiles: files.length - filesToParse.length,
80
+ parsedFiles: filesToParse.length
36
81
  };
37
82
  }
38
83
  catch (error) {
@@ -40,6 +85,27 @@ export class ProjectKnowledgeGraph {
40
85
  throw error;
41
86
  }
42
87
  }
88
+ async loadCache() {
89
+ try {
90
+ const content = await fs.readFile(this.cachePath, 'utf-8');
91
+ const data = JSON.parse(content);
92
+ if (data.version === '1.0') {
93
+ this.cache = data;
94
+ }
95
+ }
96
+ catch (e) {
97
+ // Ignore cache errors (start fresh)
98
+ this.cache = { version: '1.0', files: {} };
99
+ }
100
+ }
101
+ async saveCache() {
102
+ try {
103
+ await fs.writeFile(this.cachePath, JSON.stringify(this.cache, null, 2), 'utf-8');
104
+ }
105
+ catch (e) {
106
+ console.error('Failed to save graph cache:', e);
107
+ }
108
+ }
43
109
  async getAllFiles(dir) {
44
110
  try {
45
111
  const entries = await fs.readdir(dir, { withFileTypes: true });
@@ -58,6 +124,9 @@ export class ProjectKnowledgeGraph {
58
124
  }
59
125
  else {
60
126
  if (/\.(ts|js|tsx|jsx|json|py|go|rs|java|c|cpp|h)$/.test(entry.name)) {
127
+ // Ignore cache file itself
128
+ if (entry.name === '.gemini_graph_cache.json')
129
+ continue;
61
130
  files.push(res);
62
131
  }
63
132
  }
@@ -72,10 +141,10 @@ export class ProjectKnowledgeGraph {
72
141
  async parseFile(filePath) {
73
142
  const ext = path.extname(filePath);
74
143
  if (['.ts', '.js', '.tsx', '.jsx'].includes(ext)) {
75
- await this.parseTypeScript(filePath);
144
+ return await this.parseTypeScript(filePath);
76
145
  }
77
146
  else {
78
- await this.parseGeneric(filePath);
147
+ return await this.parseGeneric(filePath);
79
148
  }
80
149
  }
81
150
  async parseTypeScript(filePath) {
@@ -138,10 +207,11 @@ export class ProjectKnowledgeGraph {
138
207
  ts.forEachChild(node, visit);
139
208
  };
140
209
  visit(sourceFile);
141
- await this.finalizeFileNodes(filePath, imports, symbols);
210
+ return { imports, symbols };
142
211
  }
143
212
  catch (error) {
144
213
  console.error(`Error parsing TypeScript file ${filePath}:`, error);
214
+ return { imports: [], symbols: [] };
145
215
  }
146
216
  }
147
217
  async parseGeneric(filePath) {
@@ -176,14 +246,14 @@ export class ProjectKnowledgeGraph {
176
246
  else if (ext === '.go') {
177
247
  // 1. Go Imports
178
248
  // Single line: import "fmt"
179
- const singleImportMatches = content.matchAll(/import\s+"([^"]+)"/g);
249
+ const singleImportMatches = content.matchAll(/import\s+\"([^\"]+)\"/g);
180
250
  for (const match of singleImportMatches)
181
251
  imports.push(match[1]);
182
252
  // Block: import ( "fmt"; "os" )
183
253
  const blockImportMatches = content.matchAll(/import\s+\(([\s\S]*?)\)/g);
184
254
  for (const match of blockImportMatches) {
185
255
  const block = match[1];
186
- const innerMatches = block.matchAll(/"([^"]+)"/g);
256
+ const innerMatches = block.matchAll(/"([^\"]+)"/g);
187
257
  for (const im of innerMatches)
188
258
  imports.push(im[1]);
189
259
  }
@@ -197,18 +267,19 @@ export class ProjectKnowledgeGraph {
197
267
  for (const match of typeMatches)
198
268
  symbols.push(`type:${match[1]}`);
199
269
  }
200
- await this.finalizeFileNodes(filePath, imports, symbols);
270
+ return { imports, symbols };
201
271
  }
202
272
  catch (error) {
203
273
  console.error(`Error parsing generic file ${filePath}:`, error);
274
+ return { imports: [], symbols: [] };
204
275
  }
205
276
  }
206
- async finalizeFileNodes(filePath, imports, symbols) {
277
+ async linkFileNodes(filePath, rawImports, symbols) {
207
278
  const currentNode = this.nodes.get(filePath);
208
279
  if (!currentNode)
209
280
  return;
210
281
  currentNode.symbols = symbols;
211
- for (const importPath of imports) {
282
+ for (const importPath of rawImports) {
212
283
  let resolvedPath = null;
213
284
  if (importPath.startsWith('.')) {
214
285
  resolvedPath = await this.resolvePath(path.dirname(filePath), importPath);
@@ -225,9 +296,12 @@ export class ProjectKnowledgeGraph {
225
296
  }
226
297
  }
227
298
  else {
228
- // If we can't resolve to a local file, keep the original import string as an external dependency
229
- if (!currentNode.imports.includes(importPath)) {
230
- currentNode.imports.push(importPath);
299
+ // If we can't resolve to a local file, keep the original import string ONLY if it looks like an external package
300
+ // Ignore relative paths that failed to resolve (broken links)
301
+ if (!importPath.startsWith('.') && !path.isAbsolute(importPath)) {
302
+ if (!currentNode.imports.includes(importPath)) {
303
+ currentNode.imports.push(importPath);
304
+ }
231
305
  }
232
306
  }
233
307
  }
@@ -7,6 +7,12 @@ describe('ProjectKnowledgeGraph', () => {
7
7
  beforeEach(() => {
8
8
  graph = new ProjectKnowledgeGraph();
9
9
  vi.resetAllMocks();
10
+ // Default mocks for stat and writeFile
11
+ fs.stat.mockResolvedValue({
12
+ isDirectory: () => true,
13
+ mtimeMs: 1000
14
+ });
15
+ fs.writeFile.mockResolvedValue(undefined);
10
16
  });
11
17
  it('should ignore imports in comments', async () => {
12
18
  const mockFiles = ['/app/index.ts', '/app/utils.ts', '/app/oldUtils.ts'];
@@ -22,6 +28,8 @@ describe('ProjectKnowledgeGraph', () => {
22
28
  { name: 'oldUtils.ts', isDirectory: () => false }
23
29
  ]);
24
30
  fs.readFile.mockImplementation(async (path) => {
31
+ if (path.includes('graph_cache.json'))
32
+ return '{}'; // Mock empty cache
25
33
  if (path.includes('index.ts'))
26
34
  return mockContentIndex;
27
35
  return '';
@@ -50,6 +58,8 @@ describe('ProjectKnowledgeGraph', () => {
50
58
  { name: 'lib.ts', isDirectory: () => false }
51
59
  ]);
52
60
  fs.readFile.mockImplementation(async (filePath) => {
61
+ if (filePath.includes('graph_cache.json'))
62
+ return '{}';
53
63
  if (filePath.endsWith('index.ts'))
54
64
  return mockContentIndex;
55
65
  return '';
@@ -68,6 +78,8 @@ describe('ProjectKnowledgeGraph', () => {
68
78
  { name: 'Button.jsx', isDirectory: () => false }
69
79
  ]);
70
80
  fs.readFile.mockImplementation(async (filePath) => {
81
+ if (filePath.includes('graph_cache.json'))
82
+ return '{}';
71
83
  if (filePath.endsWith('index.js'))
72
84
  return mockContentIndex;
73
85
  return '';
@@ -84,6 +96,8 @@ describe('ProjectKnowledgeGraph', () => {
84
96
  { name: 'b.ts', isDirectory: () => false }
85
97
  ]);
86
98
  fs.readFile.mockImplementation(async (filePath) => {
99
+ if (filePath.includes('graph_cache.json'))
100
+ return '{}';
87
101
  if (filePath.endsWith('a.ts'))
88
102
  return contentA;
89
103
  if (filePath.endsWith('b.ts'))
@@ -104,6 +118,8 @@ describe('ProjectKnowledgeGraph', () => {
104
118
  { name: 'a.ts', isDirectory: () => false }
105
119
  ]);
106
120
  fs.readFile.mockImplementation(async (filePath) => {
121
+ if (filePath.includes('graph_cache.json'))
122
+ return '{}';
107
123
  if (filePath.endsWith('a.ts'))
108
124
  return contentA;
109
125
  return '';
@@ -120,6 +136,8 @@ describe('ProjectKnowledgeGraph', () => {
120
136
  { name: 'a.ts', isDirectory: () => false }
121
137
  ]);
122
138
  fs.readFile.mockImplementation(async (filePath) => {
139
+ if (filePath.includes('graph_cache.json'))
140
+ return '{}';
123
141
  if (filePath.endsWith('a.ts'))
124
142
  return contentA;
125
143
  return '';
@@ -7,7 +7,7 @@ export function registerFileSystemTools(server) {
7
7
  server.tool("shell_execute", "Execute a shell command. SECURITY WARNING: Use this ONLY for safe, non-destructive commands. Avoid 'rm -rf /', format, or destructive operations.", {
8
8
  command: z.string().describe("The bash command to execute")
9
9
  }, async ({ command }) => {
10
- const dangerousPatterns = [/rm\s+-rf\s+\//, /mkfs/, /dd\s+if=/];
10
+ const dangerousPatterns = [new RegExp('rm\\s+-rf\\s+\\/'), /mkfs/, /dd\s+if=/];
11
11
  if (dangerousPatterns.some(p => p.test(command))) {
12
12
  return {
13
13
  content: [{ type: "text", text: "Error: Dangerous command pattern detected. Execution blocked for safety." }],
@@ -68,47 +68,65 @@ export function registerFileSystemTools(server) {
68
68
  }
69
69
  });
70
70
  // 10. search_code
71
- server.tool("search_code", "Search for a text pattern in project files (excludes node_modules, etc.).", {
72
- pattern: z.string().describe("The text to search for"),
73
- path: z.string().optional().default('.').describe("Root directory to search")
74
- }, async ({ pattern, path: searchPath }) => {
71
+ server.tool("search_code", "Search for a text pattern in project files with advanced options (regex, case sensitivity).", {
72
+ pattern: z.string().describe("The text or regex pattern to search for"),
73
+ path: z.string().optional().default('.').describe("Root directory to search"),
74
+ useRegex: z.boolean().optional().default(false).describe("Treat pattern as a regular expression"),
75
+ caseSensitive: z.boolean().optional().default(false).describe("Match case sensitive (default: false)")
76
+ }, async ({ pattern, path: searchPath, useRegex, caseSensitive }) => {
75
77
  try {
76
78
  const resolvedPath = validatePath(searchPath || '.');
77
79
  const stats = await fs.stat(resolvedPath);
80
+ const results = [];
81
+ const searchFile = async (filePath) => {
82
+ try {
83
+ const content = await fs.readFile(filePath, 'utf-8');
84
+ const lines = content.split('\n');
85
+ let regex;
86
+ if (useRegex) {
87
+ regex = new RegExp(pattern, caseSensitive ? 'g' : 'gi');
88
+ }
89
+ else {
90
+ // Escape regex special chars if not using regex
91
+ const escaped = pattern.replace(/[.*+?^${}()|[\\]/g, '\\$&');
92
+ regex = new RegExp(escaped, caseSensitive ? 'g' : 'gi');
93
+ }
94
+ lines.forEach((line, index) => {
95
+ if (regex.test(line)) {
96
+ // Reset lastIndex if global (not strictly needed for test() but good practice)
97
+ regex.lastIndex = 0;
98
+ results.push(`${filePath}:${index + 1}: ${line.trim()}`);
99
+ }
100
+ });
101
+ }
102
+ catch (err) {
103
+ // Ignore read errors (binary files etc)
104
+ }
105
+ };
78
106
  if (stats.isFile()) {
79
- const content = await fs.readFile(resolvedPath, 'utf-8');
80
- const matches = content.includes(pattern) ? [resolvedPath] : [];
81
- return {
82
- content: [{
83
- type: "text",
84
- text: matches.length > 0 ? `Found "${pattern}" in:\n${matches.join('\n')}` : `No matches found for "${pattern}"`
85
- }]
86
- };
107
+ await searchFile(resolvedPath);
87
108
  }
88
- async function searchDir(dir) {
89
- const results = [];
90
- const entries = await fs.readdir(dir, { withFileTypes: true });
91
- for (const entry of entries) {
92
- const fullPath = path.join(dir, entry.name);
93
- if (entry.isDirectory()) {
94
- if (['node_modules', '.git', 'dist', 'coverage', '.gemini'].includes(entry.name))
95
- continue;
96
- results.push(...await searchDir(fullPath));
97
- }
98
- else if (/\.(ts|js|json|md|txt|html|css|py|java|c|cpp|h|rs|go)$/.test(entry.name)) {
99
- const content = await fs.readFile(fullPath, 'utf-8');
100
- if (content.includes(pattern)) {
101
- results.push(fullPath);
109
+ else {
110
+ const searchDir = async (dir) => {
111
+ const entries = await fs.readdir(dir, { withFileTypes: true });
112
+ for (const entry of entries) {
113
+ const fullPath = path.join(dir, entry.name);
114
+ if (entry.isDirectory()) {
115
+ if (['node_modules', '.git', 'dist', 'coverage', '.gemini'].includes(entry.name))
116
+ continue;
117
+ await searchDir(fullPath);
118
+ }
119
+ else if (/\.(ts|js|json|md|txt|html|css|py|java|c|cpp|h|rs|go|sh|yaml|yml)$/.test(entry.name)) {
120
+ await searchFile(fullPath);
102
121
  }
103
122
  }
104
- }
105
- return results;
123
+ };
124
+ await searchDir(resolvedPath);
106
125
  }
107
- const matches = await searchDir(resolvedPath);
108
126
  return {
109
127
  content: [{
110
128
  type: "text",
111
- text: matches.length > 0 ? `Found "${pattern}" in:\n${matches.join('\n')}` : `No matches found for "${pattern}"`
129
+ text: results.length > 0 ? `Found matches for "${pattern}":\n${results.join('\n')}` : `No matches found for "${pattern}"`
112
130
  }]
113
131
  };
114
132
  }
@@ -0,0 +1,27 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { ProjectKnowledgeGraph } from './graph.js';
3
+ import * as fs from 'fs/promises';
4
+ import * as path from 'path';
5
+ describe('Graph Caching Verification', () => {
6
+ it('should use cache on second run', async () => {
7
+ const graph = new ProjectKnowledgeGraph();
8
+ const root = process.cwd();
9
+ // Cleanup existing cache
10
+ const cachePath = path.join(root, '.gemini_graph_cache.json');
11
+ try {
12
+ await fs.unlink(cachePath);
13
+ }
14
+ catch (e) { }
15
+ console.log('--- Run 1 (Fresh) ---');
16
+ const res1 = await graph.build(root);
17
+ console.log('Result 1:', res1);
18
+ expect(res1.parsedFiles).toBeGreaterThan(0);
19
+ expect(res1.cachedFiles).toBe(0);
20
+ console.log('--- Run 2 (Cached) ---');
21
+ const res2 = await graph.build(root);
22
+ console.log('Result 2:', res2);
23
+ expect(res2.parsedFiles).toBe(0);
24
+ expect(res2.cachedFiles).toBeGreaterThan(0);
25
+ expect(res2.nodeCount).toBe(res1.nodeCount);
26
+ });
27
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gotza02/sequential-thinking",
3
- "version": "2026.2.17",
3
+ "version": "2026.2.19",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },