contextstore-app 1.0.0

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.
@@ -0,0 +1,6 @@
1
+ export interface FileOutline {
2
+ filePath: string;
3
+ outline: string;
4
+ snippet: string;
5
+ }
6
+ export declare function generateFileOutline(filePath: string): FileOutline;
@@ -0,0 +1,181 @@
1
+ import * as fs from 'fs';
2
+ import * as path from 'path';
3
+ export function generateFileOutline(filePath) {
4
+ try {
5
+ const ext = path.extname(filePath).toLowerCase();
6
+ const content = fs.readFileSync(filePath, 'utf-8');
7
+ if (ext === '.ts' || ext === '.tsx' || ext === '.js' || ext === '.jsx') {
8
+ return parseJsTsOutline(filePath, content);
9
+ }
10
+ else if (ext === '.py') {
11
+ return parsePythonOutline(filePath, content);
12
+ }
13
+ else if (ext === '.md') {
14
+ return parseMarkdownOutline(filePath, content);
15
+ }
16
+ else {
17
+ return parseGenericOutline(filePath, content);
18
+ }
19
+ }
20
+ catch (error) {
21
+ return {
22
+ filePath,
23
+ outline: 'Error reading file structural outline.',
24
+ snippet: ''
25
+ };
26
+ }
27
+ }
28
+ function parseJsTsOutline(filePath, content) {
29
+ const lines = content.split(/\r?\n/);
30
+ const outlineLines = [];
31
+ const snippetLines = [];
32
+ let inDocstring = false;
33
+ let docstringBuffer = [];
34
+ for (let index = 0; index < lines.length; index += 1) {
35
+ const line = lines[index];
36
+ const trimmed = line.trim();
37
+ // Catch JSDoc comments
38
+ if (trimmed.startsWith('/**')) {
39
+ inDocstring = true;
40
+ docstringBuffer = [trimmed];
41
+ continue;
42
+ }
43
+ if (inDocstring) {
44
+ docstringBuffer.push(trimmed);
45
+ if (trimmed.endsWith('*/')) {
46
+ inDocstring = false;
47
+ }
48
+ continue;
49
+ }
50
+ // Match exports, classes, interfaces, types, functions, and arrow function definitions
51
+ const isExport = trimmed.startsWith('export ');
52
+ const isClass = trimmed.match(/^(?:export\s+)?class\s+(\w+)/);
53
+ const isInterface = trimmed.match(/^(?:export\s+)?interface\s+(\w+)/);
54
+ const isType = trimmed.match(/^(?:export\s+)?type\s+(\w+)/);
55
+ const isFunction = trimmed.match(/^(?:export\s+|async\s+|export\s+async\s+)?function\s+(\w+)/);
56
+ const isArrowFunction = trimmed.match(/^(?:export\s+)?const\s+(\w+)\s*=\s*(?:async\s*)?\([^)]*\)\s*=>/);
57
+ if (isExport || isClass || isInterface || isType || isFunction || isArrowFunction) {
58
+ // If we have docstring buffer, prepend it
59
+ if (docstringBuffer.length > 0) {
60
+ snippetLines.push(...docstringBuffer);
61
+ docstringBuffer = [];
62
+ }
63
+ snippetLines.push(trimmed);
64
+ if (isClass)
65
+ outlineLines.push(`class ${isClass[1]}`);
66
+ else if (isInterface)
67
+ outlineLines.push(`interface ${isInterface[1]}`);
68
+ else if (isType)
69
+ outlineLines.push(`type ${isType[1]}`);
70
+ else if (isFunction)
71
+ outlineLines.push(`function ${isFunction[1]}()`);
72
+ else if (isArrowFunction)
73
+ outlineLines.push(`const ${isArrowFunction[1]}()`);
74
+ }
75
+ else {
76
+ // Clear docstring buffer if we hit general code
77
+ if (!trimmed.startsWith('//') && trimmed !== '') {
78
+ docstringBuffer = [];
79
+ }
80
+ }
81
+ }
82
+ const outline = outlineLines.length > 0
83
+ ? `Exports: ${outlineLines.join(', ')}`
84
+ : 'Generic Script File (no explicit class/function exports found)';
85
+ return {
86
+ filePath,
87
+ outline,
88
+ snippet: snippetLines.slice(0, 50).join('\n') // Cap snippet to 50 structural lines
89
+ };
90
+ }
91
+ function parsePythonOutline(filePath, content) {
92
+ const lines = content.split(/\r?\n/);
93
+ const outlineLines = [];
94
+ const snippetLines = [];
95
+ let currentClass = '';
96
+ let inDocstring = false;
97
+ let docstringBuffer = [];
98
+ for (let index = 0; index < lines.length; index += 1) {
99
+ const line = lines[index];
100
+ const trimmed = line.trim();
101
+ // Catch docstrings (triple quotes)
102
+ if (trimmed.startsWith('"""') || trimmed.startsWith("'''")) {
103
+ if (trimmed.endsWith('"""') && trimmed.length > 3 || trimmed.endsWith("'''") && trimmed.length > 3) {
104
+ // Single-line docstring
105
+ docstringBuffer.push(trimmed);
106
+ }
107
+ else {
108
+ inDocstring = !inDocstring;
109
+ docstringBuffer.push(trimmed);
110
+ }
111
+ continue;
112
+ }
113
+ if (inDocstring) {
114
+ docstringBuffer.push(trimmed);
115
+ if (trimmed.endsWith('"""') || trimmed.endsWith("'''")) {
116
+ inDocstring = false;
117
+ }
118
+ continue;
119
+ }
120
+ const isClass = line.match(/^class\s+(\w+)/);
121
+ const isDef = line.match(/^\s*def\s+(\w+)/);
122
+ if (isClass) {
123
+ currentClass = isClass[1];
124
+ outlineLines.push(`class ${currentClass}`);
125
+ if (docstringBuffer.length > 0) {
126
+ snippetLines.push(...docstringBuffer);
127
+ docstringBuffer = [];
128
+ }
129
+ snippetLines.push(trimmed);
130
+ }
131
+ else if (isDef) {
132
+ const funcName = isDef[1];
133
+ outlineLines.push(currentClass ? ` def ${funcName}()` : `def ${funcName}()`);
134
+ if (docstringBuffer.length > 0) {
135
+ snippetLines.push(...docstringBuffer.map(d => ' ' + d));
136
+ docstringBuffer = [];
137
+ }
138
+ snippetLines.push(trimmed);
139
+ }
140
+ else {
141
+ if (!trimmed.startsWith('#') && trimmed !== '') {
142
+ docstringBuffer = [];
143
+ }
144
+ }
145
+ }
146
+ const outline = outlineLines.length > 0
147
+ ? outlineLines.join('\n')
148
+ : 'Generic Python File (no classes/functions defined)';
149
+ return {
150
+ filePath,
151
+ outline,
152
+ snippet: snippetLines.slice(0, 50).join('\n')
153
+ };
154
+ }
155
+ function parseMarkdownOutline(filePath, content) {
156
+ const lines = content.split(/\r?\n/);
157
+ const headings = [];
158
+ for (const line of lines) {
159
+ if (line.startsWith('#')) {
160
+ headings.push(line.trim());
161
+ }
162
+ }
163
+ const outline = headings.length > 0
164
+ ? `Markdown Sections: ${headings.slice(0, 10).join(' -> ')}`
165
+ : 'Markdown Document';
166
+ return {
167
+ filePath,
168
+ outline,
169
+ snippet: headings.slice(0, 30).join('\n')
170
+ };
171
+ }
172
+ function parseGenericOutline(filePath, content) {
173
+ // Take first 5 lines as a quick identifier outline, first 1000 characters as snippet
174
+ const lines = content.split(/\r?\n/).slice(0, 10).filter(l => l.trim().length > 0);
175
+ const outline = lines.length > 0 ? `Snippet: ${lines[0].substring(0, 80)}` : 'Empty File';
176
+ return {
177
+ filePath,
178
+ outline,
179
+ snippet: content.substring(0, 1500)
180
+ };
181
+ }
@@ -0,0 +1,8 @@
1
+ import { Config } from './config.js';
2
+ export interface SyncResult {
3
+ filePath: string;
4
+ status: 'synced' | 'skipped' | 'failed';
5
+ error?: string;
6
+ }
7
+ export declare function computeFileHash(filePath: string): string;
8
+ export declare function syncFile(filePath: string, workspacePath: string, config: Config): Promise<SyncResult>;
package/dist/syncer.js ADDED
@@ -0,0 +1,77 @@
1
+ import axios from 'axios';
2
+ import * as crypto from 'crypto';
3
+ import * as fs from 'fs';
4
+ import * as path from 'path';
5
+ import { generateFileOutline } from './indexer.js';
6
+ export function computeFileHash(filePath) {
7
+ try {
8
+ const content = fs.readFileSync(filePath);
9
+ return crypto.createHash('sha256').update(content).digest('hex');
10
+ }
11
+ catch {
12
+ return '';
13
+ }
14
+ }
15
+ export async function syncFile(filePath, workspacePath, config) {
16
+ const relativePath = path.relative(workspacePath, filePath).replace(/\\/g, '/');
17
+ try {
18
+ const fileHash = computeFileHash(filePath);
19
+ if (!fileHash) {
20
+ return { filePath: relativePath, status: 'failed', error: 'Could not read file hash' };
21
+ }
22
+ // Generate AST-style outline and structural snippet
23
+ const { outline, snippet } = generateFileOutline(filePath);
24
+ // Format raw text for client-side LLM consumption (free processing design)
25
+ const rawText = `WORKSPACE FILE SYNC:
26
+ Path: ${relativePath}
27
+ Checksum: ${fileHash}
28
+ Outline:
29
+ ${outline}
30
+
31
+ Snippet:
32
+ ${snippet}`;
33
+ // POST ingest request to backend API
34
+ const response = await axios.post(`${config.serverUrl}/sync/ingest`, {
35
+ text: rawText,
36
+ source: `codebase:${relativePath}`
37
+ }, {
38
+ headers: {
39
+ Authorization: `Bearer ${config.authToken}`,
40
+ 'Content-Type': 'application/json'
41
+ }
42
+ });
43
+ if (response.status === 200 && response.data.success) {
44
+ return { filePath: relativePath, status: 'synced' };
45
+ }
46
+ else {
47
+ return {
48
+ filePath: relativePath,
49
+ status: 'failed',
50
+ error: response.data.message || 'Server rejected request'
51
+ };
52
+ }
53
+ }
54
+ catch (error) {
55
+ let errMsg = 'Unknown error';
56
+ if (error.response?.data?.detail) {
57
+ errMsg = error.response.data.detail;
58
+ }
59
+ else if (error.response?.data?.message) {
60
+ errMsg = error.response.data.message;
61
+ }
62
+ else if (error.message) {
63
+ errMsg = error.message;
64
+ }
65
+ else if (error.code) {
66
+ errMsg = `Network Error (${error.code})`;
67
+ }
68
+ else if (error.toString) {
69
+ errMsg = error.toString();
70
+ }
71
+ return {
72
+ filePath: relativePath,
73
+ status: 'failed',
74
+ error: errMsg
75
+ };
76
+ }
77
+ }
@@ -0,0 +1 @@
1
+ export declare function detectProjectType(workspacePath: string): string;
@@ -0,0 +1,53 @@
1
+ import * as fs from 'fs';
2
+ import * as path from 'path';
3
+ export function detectProjectType(workspacePath) {
4
+ try {
5
+ const files = fs.readdirSync(workspacePath);
6
+ if (files.includes('package.json')) {
7
+ // Check if React, Next.js, Vite, Vue, etc. are specified
8
+ try {
9
+ const pkgContent = fs.readFileSync(path.join(workspacePath, 'package.json'), 'utf-8');
10
+ const pkg = JSON.parse(pkgContent);
11
+ const deps = { ...pkg.dependencies, ...pkg.devDependencies };
12
+ if (deps['next'])
13
+ return 'Next.js';
14
+ if (deps['react'])
15
+ return 'React';
16
+ if (deps['vue'])
17
+ return 'Vue';
18
+ if (deps['nuxt'])
19
+ return 'Nuxt';
20
+ if (deps['svelte'])
21
+ return 'Svelte';
22
+ return 'NodeJS';
23
+ }
24
+ catch {
25
+ return 'NodeJS';
26
+ }
27
+ }
28
+ if (files.includes('requirements.txt') || files.includes('pyproject.toml') || files.includes('Pipfile') || files.includes('setup.py')) {
29
+ return 'Python';
30
+ }
31
+ if (files.includes('Cargo.toml')) {
32
+ return 'Rust';
33
+ }
34
+ if (files.includes('go.mod')) {
35
+ return 'Go';
36
+ }
37
+ if (files.includes('pom.xml') || files.includes('build.gradle')) {
38
+ return 'Java';
39
+ }
40
+ if (files.includes('Gemfile')) {
41
+ return 'Ruby';
42
+ }
43
+ // Check if it's primarily a Docs workspace
44
+ const mdFiles = files.filter(f => f.endsWith('.md'));
45
+ if (mdFiles.length > 3 || files.includes('docs') || files.includes('wiki')) {
46
+ return 'Docs';
47
+ }
48
+ }
49
+ catch {
50
+ // Ignore read errors
51
+ }
52
+ return 'Generic';
53
+ }
@@ -0,0 +1,4 @@
1
+ export interface WalkOptions {
2
+ maxFiles?: number;
3
+ }
4
+ export declare function walkWorkspace(workspacePath: string, options?: WalkOptions): string[];
package/dist/walker.js ADDED
@@ -0,0 +1,103 @@
1
+ import * as fs from 'fs';
2
+ import * as path from 'path';
3
+ import { minimatch } from 'minimatch';
4
+ const DEFAULT_IGNORES = [
5
+ '**/node_modules/**',
6
+ '**/.git/**',
7
+ '**/dist/**',
8
+ '**/build/**',
9
+ '**/out/**',
10
+ '**/.next/**',
11
+ '**/.nuxt/**',
12
+ '**/.venv/**',
13
+ '**/venv/**',
14
+ '**/env/**',
15
+ '**/__pycache__/**',
16
+ '**/.pytest_cache/**',
17
+ '**/.DS_Store',
18
+ '**/Thumbs.db'
19
+ ];
20
+ const BINARY_EXTENSIONS = new Set([
21
+ '.png', '.jpg', '.jpeg', '.gif', '.ico', '.pdf', '.zip', '.tar', '.gz', '.tgz',
22
+ '.mp4', '.mp3', '.wav', '.avi', '.mov', '.exe', '.dll', '.so', '.dylib', '.bin',
23
+ '.woff', '.woff2', '.eot', '.ttf', '.otf', '.svg', '.db', '.sqlite', '.pyc'
24
+ ]);
25
+ export function walkWorkspace(workspacePath, options = {}) {
26
+ const maxFiles = options.maxFiles || 1000;
27
+ const filesList = [];
28
+ const gitignorePatterns = [...DEFAULT_IGNORES];
29
+ // Load root .gitignore patterns
30
+ const gitignorePath = path.join(workspacePath, '.gitignore');
31
+ if (fs.existsSync(gitignorePath)) {
32
+ try {
33
+ const content = fs.readFileSync(gitignorePath, 'utf-8');
34
+ const lines = content.split(/\r?\n/);
35
+ for (const line of lines) {
36
+ const trimmed = line.trim();
37
+ if (trimmed && !trimmed.startsWith('#')) {
38
+ // Normalize gitignore patterns to glob patterns
39
+ let globPattern = trimmed;
40
+ if (trimmed.endsWith('/')) {
41
+ globPattern = `**/${trimmed}**`;
42
+ }
43
+ else {
44
+ globPattern = `**/${trimmed}`;
45
+ }
46
+ gitignorePatterns.push(globPattern);
47
+ }
48
+ }
49
+ }
50
+ catch {
51
+ // Ignore gitignore read errors
52
+ }
53
+ }
54
+ function shouldIgnore(relativeFilePath) {
55
+ const normalizedPath = relativeFilePath.replace(/\\/g, '/');
56
+ // Check custom and default ignores
57
+ for (const pattern of gitignorePatterns) {
58
+ if (minimatch(normalizedPath, pattern, { dot: true })) {
59
+ return true;
60
+ }
61
+ }
62
+ // Check extensions
63
+ const ext = path.extname(normalizedPath).toLowerCase();
64
+ if (BINARY_EXTENSIONS.has(ext)) {
65
+ return true;
66
+ }
67
+ return false;
68
+ }
69
+ function traverse(currentDir) {
70
+ if (filesList.length >= maxFiles)
71
+ return;
72
+ try {
73
+ const items = fs.readdirSync(currentDir);
74
+ for (const item of items) {
75
+ const fullPath = path.join(currentDir, item);
76
+ const relPath = path.relative(workspacePath, fullPath);
77
+ if (shouldIgnore(relPath)) {
78
+ continue;
79
+ }
80
+ try {
81
+ const stat = fs.statSync(fullPath);
82
+ if (stat.isDirectory()) {
83
+ traverse(fullPath);
84
+ }
85
+ else if (stat.isFile()) {
86
+ filesList.push(fullPath);
87
+ if (filesList.length >= maxFiles) {
88
+ break;
89
+ }
90
+ }
91
+ }
92
+ catch {
93
+ // Ignore stats errors
94
+ }
95
+ }
96
+ }
97
+ catch {
98
+ // Ignore read errors
99
+ }
100
+ }
101
+ traverse(workspacePath);
102
+ return filesList;
103
+ }
@@ -0,0 +1,3 @@
1
+ import chokidar from 'chokidar';
2
+ import { Config } from './config.js';
3
+ export declare function startWatcher(workspacePath: string, config: Config): chokidar.FSWatcher;
@@ -0,0 +1,71 @@
1
+ import chokidar from 'chokidar';
2
+ import * as path from 'path';
3
+ import { syncFile } from './syncer.js';
4
+ export function startWatcher(workspacePath, config) {
5
+ console.log(`Watching for file changes in: ${workspacePath}...`);
6
+ const ignoredPatterns = [
7
+ '**/node_modules/**',
8
+ '**/.git/**',
9
+ '**/dist/**',
10
+ '**/build/**',
11
+ '**/out/**',
12
+ '**/.venv/**',
13
+ '**/venv/**',
14
+ '**/env/**',
15
+ '**/__pycache__/**',
16
+ '**/.DS_Store'
17
+ ];
18
+ const watcher = chokidar.watch(workspacePath, {
19
+ ignored: (filePath) => {
20
+ const relative = path.relative(workspacePath, filePath).replace(/\\/g, '/');
21
+ return ignoredPatterns.some(pattern => {
22
+ // Simple pattern matching for ignoring during watch
23
+ return relative.includes('node_modules') ||
24
+ relative.includes('.git') ||
25
+ relative.includes('dist') ||
26
+ relative.includes('build') ||
27
+ relative.includes('venv') ||
28
+ relative.includes('__pycache__');
29
+ });
30
+ },
31
+ persistent: true,
32
+ ignoreInitial: true, // Don't trigger add events for existing files on startup
33
+ awaitWriteFinish: {
34
+ stabilityThreshold: 1000,
35
+ pollInterval: 100
36
+ }
37
+ });
38
+ watcher.on('add', async (filePath) => {
39
+ const relative = path.relative(workspacePath, filePath);
40
+ console.log(`[Watcher] File added: ${relative}`);
41
+ const res = await syncFile(filePath, workspacePath, config);
42
+ if (res.status === 'synced') {
43
+ console.log(`[Watcher] Successfully synced new file: ${relative}`);
44
+ }
45
+ else {
46
+ console.error(`[Watcher] Failed to sync new file: ${relative}. Error: ${res.error}`);
47
+ }
48
+ });
49
+ watcher.on('change', async (filePath) => {
50
+ const relative = path.relative(workspacePath, filePath);
51
+ console.log(`[Watcher] File changed: ${relative}`);
52
+ const res = await syncFile(filePath, workspacePath, config);
53
+ if (res.status === 'synced') {
54
+ console.log(`[Watcher] Successfully re-synced modified file: ${relative}`);
55
+ }
56
+ else {
57
+ console.error(`[Watcher] Failed to sync changes for: ${relative}. Error: ${res.error}`);
58
+ }
59
+ });
60
+ watcher.on('unlink', (filePath) => {
61
+ const relative = path.relative(workspacePath, filePath);
62
+ console.log(`[Watcher] File deleted: ${relative} (queued for index cleanup)`);
63
+ // Note: We can notify the server if we add a dedicated clean endpoint,
64
+ // otherwise the next full sync or AI check-in cleans obsolete files.
65
+ });
66
+ process.on('SIGINT', () => {
67
+ void watcher.close();
68
+ process.exit(0);
69
+ });
70
+ return watcher;
71
+ }
package/package.json ADDED
@@ -0,0 +1,28 @@
1
+ {
2
+ "name": "contextstore-app",
3
+ "version": "1.0.0",
4
+ "description": "CLI tool to sync local workspaces to persistent AI memory via MCP.",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "bin": {
8
+ "contextstore": "dist/index.js"
9
+ },
10
+ "scripts": {
11
+ "build": "tsc && node fix-shebang.js",
12
+ "watch": "tsc -w",
13
+ "start": "node ./dist/index.js"
14
+ },
15
+ "dependencies": {
16
+ "@clack/prompts": "^0.7.0",
17
+ "axios": "^1.7.2",
18
+ "chokidar": "^3.6.0",
19
+ "commander": "^12.1.0",
20
+ "dotenv": "^16.4.5",
21
+ "minimatch": "^9.0.4"
22
+ },
23
+ "devDependencies": {
24
+ "@types/node": "^20.14.2",
25
+ "@types/minimatch": "^5.1.2",
26
+ "typescript": "^5.4.5"
27
+ }
28
+ }