aibridge-context 1.0.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 BharatAdhana
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,171 @@
1
+ # aibridge-context
2
+
3
+ `aibridge-context` is a zero-config CLI and Node.js library that turns a project into an AI-readable workspace. It maintains a live `.ai-context/` folder, serves it locally, and can optionally sync context updates through git.
4
+
5
+ Think of it as Git for AI context.
6
+
7
+ ## Features
8
+
9
+ - Zero-config startup for Node.js projects
10
+ - Debounced file watching powered by `chokidar`
11
+ - Atomic writes to avoid corrupted JSON files
12
+ - Local Express server for AI-friendly endpoints
13
+ - Optional git auto-sync for `.ai-context/*`
14
+ - Library exports for embedding in other tooling
15
+
16
+ ## Install
17
+
18
+ ```bash
19
+ npm install
20
+ ```
21
+
22
+ To use the local CLI in this repository:
23
+
24
+ ```bash
25
+ npx aibridge init
26
+ npx aibridge start
27
+ ```
28
+
29
+ If published to npm, the package exposes the `aibridge` binary.
30
+
31
+ `ai-context` is supported as a legacy alias.
32
+
33
+ ## Commands
34
+
35
+ ### `aibridge init`
36
+
37
+ Creates `.ai-context/` and writes:
38
+
39
+ - `state.json`
40
+ - `brain.txt`
41
+ - `context.md`
42
+ - `changelog.json`
43
+ - `config.json`
44
+
45
+ ### `aibridge start`
46
+
47
+ Starts:
48
+
49
+ - A debounced file watcher
50
+ - A local Express server
51
+ - Automatic state updates on add/change/delete events
52
+
53
+ Default server port: `3333`
54
+
55
+ ### `aibridge update`
56
+
57
+ Triggers a manual context refresh and optional git sync.
58
+
59
+ ## Generated files
60
+
61
+ ### `.ai-context/state.json`
62
+
63
+ Tracks:
64
+
65
+ - project name
66
+ - version
67
+ - last update time
68
+ - change statistics
69
+ - recent updates
70
+ - project features
71
+ - next steps
72
+
73
+ ### `.ai-context/brain.txt`
74
+
75
+ Provides instructions any AI assistant should follow before responding.
76
+
77
+ ### `.ai-context/context.md`
78
+
79
+ Stores a human-readable summary including:
80
+
81
+ - project purpose
82
+ - detected stack
83
+ - AI usage guidance
84
+
85
+ ### `.ai-context/changelog.json`
86
+
87
+ Stores historical change entries captured by the watcher.
88
+
89
+ ### `.ai-context/config.json`
90
+
91
+ Default configuration:
92
+
93
+ ```json
94
+ {
95
+ "port": 3333,
96
+ "debounceMs": 600,
97
+ "gitSync": {
98
+ "enabled": false,
99
+ "push": true,
100
+ "commitMessage": "auto: update AI context"
101
+ }
102
+ }
103
+ ```
104
+
105
+ ## HTTP endpoints
106
+
107
+ When `aibridge start` is running:
108
+
109
+ - `GET /state.json`
110
+ - `GET /brain.txt`
111
+ - `GET /context.md`
112
+ - `GET /changelog.json`
113
+
114
+ ## Example usage
115
+
116
+ ```bash
117
+ npx aibridge init
118
+ npx aibridge start
119
+ ```
120
+
121
+ Then point your AI tool to:
122
+
123
+ - `http://localhost:3333/state.json`
124
+ - `http://localhost:3333/context.md`
125
+ - `http://localhost:3333/changelog.json`
126
+ - `http://localhost:3333/brain.txt`
127
+
128
+ ## Git sync
129
+
130
+ Git sync is optional and controlled by `.ai-context/config.json`.
131
+
132
+ Enable it like this:
133
+
134
+ ```json
135
+ {
136
+ "gitSync": {
137
+ "enabled": true,
138
+ "push": true,
139
+ "commitMessage": "auto: update AI context"
140
+ }
141
+ }
142
+ ```
143
+
144
+ On every successful update, the tool will attempt to:
145
+
146
+ ```bash
147
+ git add .ai-context
148
+ git commit -m "auto: update AI context"
149
+ git push
150
+ ```
151
+
152
+ Failures are handled gracefully and will not stop the watcher or server.
153
+
154
+ ## Library usage
155
+
156
+ ```js
157
+ const {
158
+ initProject,
159
+ startWatcher,
160
+ updateProjectState,
161
+ startServer,
162
+ syncContextToGit
163
+ } = require('aibridge-context');
164
+ ```
165
+
166
+ ## Development notes
167
+
168
+ - CommonJS is used for simplicity and broad compatibility.
169
+ - The watcher ignores `node_modules`, `.git`, and `.ai-context`.
170
+ - Writes are atomic via temporary-file rename.
171
+ - Updates are debounced to reduce noisy file churn.
package/bin/cli.js ADDED
@@ -0,0 +1,112 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const path = require('path');
5
+ const { initProject } = require('../core/init');
6
+ const { startWatcher } = require('../core/watcher');
7
+ const { loadRuntimeConfig, updateProjectState } = require('../core/stateManager');
8
+ const { syncContextToGit } = require('../core/gitSync');
9
+ const { startServer } = require('../server/server');
10
+ const { createLogger } = require('../utils/logger');
11
+
12
+ async function run() {
13
+ const logger = createLogger();
14
+ const projectRoot = process.cwd();
15
+ const command = process.argv[2];
16
+
17
+ if (!command || command === '--help' || command === '-h') {
18
+ printHelp();
19
+ return;
20
+ }
21
+
22
+ if (command === '--version' || command === '-v') {
23
+ // eslint-disable-next-line global-require
24
+ const packageJson = require(path.join(__dirname, '..', 'package.json'));
25
+ console.log(packageJson.version);
26
+ return;
27
+ }
28
+
29
+ try {
30
+ if (command === 'init') {
31
+ await initProject(projectRoot, { logger });
32
+ return;
33
+ }
34
+
35
+ if (command === 'update') {
36
+ await initProject(projectRoot, { logger });
37
+ const config = await loadRuntimeConfig(projectRoot);
38
+ await updateProjectState(
39
+ projectRoot,
40
+ {
41
+ timestamp: new Date().toISOString(),
42
+ action: 'manual_update',
43
+ file: '.'
44
+ },
45
+ {
46
+ logger,
47
+ syncCallback: async () => syncContextToGit(projectRoot, config.gitSync, logger)
48
+ }
49
+ );
50
+ logger.info('Manual AI context update completed.');
51
+ return;
52
+ }
53
+
54
+ if (command === 'start') {
55
+ await initProject(projectRoot, { logger });
56
+ const config = await loadRuntimeConfig(projectRoot);
57
+ const serverHandle = await startServer({
58
+ projectRoot,
59
+ port: Number(process.env.AI_CONTEXT_PORT || config.port || 3333),
60
+ logger
61
+ });
62
+ const watcherHandle = await startWatcher(projectRoot, { logger });
63
+
64
+ const shutdown = async (signal) => {
65
+ logger.info(`Received ${signal}; shutting down cleanly.`);
66
+ await watcherHandle.close();
67
+ await serverHandle.close();
68
+ process.exit(0);
69
+ };
70
+
71
+ process.on('SIGINT', () => {
72
+ shutdown('SIGINT').catch((error) => {
73
+ logger.error(`Shutdown failed: ${error.message}`);
74
+ process.exit(1);
75
+ });
76
+ });
77
+
78
+ process.on('SIGTERM', () => {
79
+ shutdown('SIGTERM').catch((error) => {
80
+ logger.error(`Shutdown failed: ${error.message}`);
81
+ process.exit(1);
82
+ });
83
+ });
84
+
85
+ return;
86
+ }
87
+
88
+ logger.error(`Unknown command: ${command}`);
89
+ printHelp();
90
+ process.exitCode = 1;
91
+ } catch (error) {
92
+ logger.error(error.stack || error.message);
93
+ process.exitCode = 1;
94
+ }
95
+ }
96
+
97
+ function printHelp() {
98
+ console.log(`aibridge-context
99
+
100
+ Usage:
101
+ aibridge init
102
+ aibridge start
103
+ aibridge update
104
+
105
+ Commands:
106
+ init Create .ai-context and initialize AI context files
107
+ start Start the watcher and local server on port 3333
108
+ update Trigger a manual state update
109
+ `);
110
+ }
111
+
112
+ run();
@@ -0,0 +1,131 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const { execFile } = require('child_process');
6
+ const { promisify } = require('util');
7
+
8
+ const execFileAsync = promisify(execFile);
9
+
10
+ async function runGit(projectRoot, args) {
11
+ return execFileAsync('git', args, {
12
+ cwd: projectRoot,
13
+ windowsHide: true
14
+ });
15
+ }
16
+
17
+ async function isGitRepository(projectRoot) {
18
+ if (fs.existsSync(path.join(projectRoot, '.git'))) {
19
+ return true;
20
+ }
21
+
22
+ try {
23
+ const result = await runGit(projectRoot, ['rev-parse', '--is-inside-work-tree']);
24
+ return result.stdout.trim() === 'true';
25
+ } catch (error) {
26
+ return false;
27
+ }
28
+ }
29
+
30
+ async function hasRemote(projectRoot) {
31
+ try {
32
+ const result = await runGit(projectRoot, ['remote']);
33
+ return result.stdout.trim().length > 0;
34
+ } catch (error) {
35
+ return false;
36
+ }
37
+ }
38
+
39
+ async function hasStagedContextChanges(projectRoot) {
40
+ try {
41
+ await runGit(projectRoot, ['diff', '--cached', '--quiet', '--', '.ai-context']);
42
+ return false;
43
+ } catch (error) {
44
+ return true;
45
+ }
46
+ }
47
+
48
+ async function syncContextToGit(projectRoot, config, logger) {
49
+ const settings = Object.assign(
50
+ {
51
+ enabled: false,
52
+ push: true,
53
+ commitMessage: 'auto: update AI context'
54
+ },
55
+ config
56
+ );
57
+
58
+ if (!settings.enabled) {
59
+ return {
60
+ skipped: true,
61
+ reason: 'disabled'
62
+ };
63
+ }
64
+
65
+ const repositoryReady = await isGitRepository(projectRoot);
66
+
67
+ if (!repositoryReady) {
68
+ if (logger) {
69
+ logger.warn('Git sync skipped because this project is not a git repository.');
70
+ }
71
+
72
+ return {
73
+ skipped: true,
74
+ reason: 'not_a_repo'
75
+ };
76
+ }
77
+
78
+ try {
79
+ await runGit(projectRoot, ['add', '.ai-context']);
80
+
81
+ const hasChanges = await hasStagedContextChanges(projectRoot);
82
+ if (!hasChanges) {
83
+ return {
84
+ skipped: true,
85
+ reason: 'no_changes'
86
+ };
87
+ }
88
+
89
+ await runGit(projectRoot, ['commit', '-m', settings.commitMessage]);
90
+
91
+ if (settings.push) {
92
+ const remoteExists = await hasRemote(projectRoot);
93
+
94
+ if (!remoteExists) {
95
+ if (logger) {
96
+ logger.warn('Git sync committed locally, but no remote is configured for push.');
97
+ }
98
+
99
+ return {
100
+ ok: true,
101
+ pushed: false
102
+ };
103
+ }
104
+
105
+ await runGit(projectRoot, ['push']);
106
+ }
107
+
108
+ if (logger) {
109
+ logger.info('Synced .ai-context changes to git.');
110
+ }
111
+
112
+ return {
113
+ ok: true,
114
+ pushed: Boolean(settings.push)
115
+ };
116
+ } catch (error) {
117
+ if (logger) {
118
+ logger.warn(`Git sync failed gracefully: ${error.message}`);
119
+ }
120
+
121
+ return {
122
+ ok: false,
123
+ error: error.message
124
+ };
125
+ }
126
+ }
127
+
128
+ module.exports = {
129
+ isGitRepository,
130
+ syncContextToGit
131
+ };
package/core/init.js ADDED
@@ -0,0 +1,96 @@
1
+ 'use strict';
2
+
3
+ const path = require('path');
4
+ const fsp = require('fs/promises');
5
+
6
+ const {
7
+ createDefaultChangelog,
8
+ createDefaultState,
9
+ detectProjectMetadata,
10
+ ensureContextDirectory,
11
+ getContextPaths,
12
+ readJsonFile,
13
+ renderTemplate,
14
+ writeJsonAtomic,
15
+ writeTextAtomic
16
+ } = require('./stateManager');
17
+
18
+ async function initProject(projectRoot, options) {
19
+ const settings = Object.assign({ logger: null, force: false }, options);
20
+ const logger = settings.logger;
21
+ const contextDir = await ensureContextDirectory(projectRoot);
22
+ const paths = getContextPaths(projectRoot);
23
+ const metadata = detectProjectMetadata(projectRoot);
24
+ const templateDir = path.join(__dirname, '..', 'templates');
25
+ const stateTemplate = await fsp.readFile(path.join(templateDir, 'state.template.json'), 'utf8');
26
+ const changelogTemplate = await fsp.readFile(
27
+ path.join(templateDir, 'changelog.template.json'),
28
+ 'utf8'
29
+ );
30
+ const brainTemplate = await fsp.readFile(path.join(templateDir, 'brain.template.txt'), 'utf8');
31
+ const contextTemplate = await fsp.readFile(path.join(templateDir, 'context.template.md'), 'utf8');
32
+ const existingConfig = await readJsonFile(paths.configFile, null);
33
+ const existingState = await readJsonFile(paths.stateFile, null);
34
+ const existingChangelog = await readJsonFile(paths.changelogFile, null);
35
+ const templateState = JSON.parse(stateTemplate);
36
+ const templateChangelog = JSON.parse(changelogTemplate);
37
+
38
+ const initialState = Object.assign({}, templateState, existingState || createDefaultState(projectRoot), {
39
+ project: metadata.project,
40
+ version: metadata.version
41
+ });
42
+
43
+ const initialContext = renderTemplate(contextTemplate, {
44
+ PROJECT_NAME: metadata.project,
45
+ PROJECT_VERSION: metadata.version,
46
+ STACK_LABEL: metadata.stackLabel,
47
+ PACKAGE_MANAGER: metadata.packageManager
48
+ });
49
+ const shouldWriteBrain = settings.force || !(await fileExists(paths.brainFile));
50
+ const shouldWriteContext = settings.force || !(await fileExists(paths.contextFile));
51
+
52
+ await writeJsonAtomic(paths.stateFile, initialState);
53
+ await writeJsonAtomic(paths.changelogFile, existingChangelog || templateChangelog || createDefaultChangelog());
54
+
55
+ if (shouldWriteBrain) {
56
+ await writeTextAtomic(paths.brainFile, brainTemplate);
57
+ }
58
+
59
+ if (shouldWriteContext) {
60
+ await writeTextAtomic(paths.contextFile, initialContext);
61
+ }
62
+
63
+ if (!existingConfig) {
64
+ await writeJsonAtomic(paths.configFile, {
65
+ port: 3333,
66
+ debounceMs: 600,
67
+ gitSync: {
68
+ enabled: false,
69
+ push: true,
70
+ commitMessage: 'auto: update AI context'
71
+ }
72
+ });
73
+ }
74
+
75
+ if (logger) {
76
+ logger.info(`Initialized AI context in ${contextDir}`);
77
+ }
78
+
79
+ return {
80
+ contextDir,
81
+ metadata
82
+ };
83
+ }
84
+
85
+ async function fileExists(filePath) {
86
+ try {
87
+ await fsp.access(filePath);
88
+ return true;
89
+ } catch (error) {
90
+ return false;
91
+ }
92
+ }
93
+
94
+ module.exports = {
95
+ initProject
96
+ };
@@ -0,0 +1,327 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const fsp = require('fs/promises');
5
+ const path = require('path');
6
+
7
+ const CONTEXT_DIR_NAME = '.ai-context';
8
+ const MAX_RECENT_UPDATES = 50;
9
+ const MAX_CHANGELOG_ENTRIES = 200;
10
+
11
+ const DEFAULT_CONFIG = {
12
+ port: 3333,
13
+ debounceMs: 600,
14
+ gitSync: {
15
+ enabled: false,
16
+ push: true,
17
+ commitMessage: 'auto: update AI context'
18
+ }
19
+ };
20
+
21
+ function getContextPaths(projectRoot) {
22
+ const contextDir = path.join(projectRoot, CONTEXT_DIR_NAME);
23
+
24
+ return {
25
+ contextDir,
26
+ stateFile: path.join(contextDir, 'state.json'),
27
+ brainFile: path.join(contextDir, 'brain.txt'),
28
+ contextFile: path.join(contextDir, 'context.md'),
29
+ changelogFile: path.join(contextDir, 'changelog.json'),
30
+ configFile: path.join(contextDir, 'config.json')
31
+ };
32
+ }
33
+
34
+ function deepMerge(baseValue, overrideValue) {
35
+ if (Array.isArray(baseValue) || Array.isArray(overrideValue)) {
36
+ return overrideValue === undefined ? baseValue : overrideValue;
37
+ }
38
+
39
+ if (isObject(baseValue) && isObject(overrideValue)) {
40
+ const merged = Object.assign({}, baseValue);
41
+
42
+ for (const [key, value] of Object.entries(overrideValue)) {
43
+ merged[key] = deepMerge(baseValue[key], value);
44
+ }
45
+
46
+ return merged;
47
+ }
48
+
49
+ return overrideValue === undefined ? baseValue : overrideValue;
50
+ }
51
+
52
+ function isObject(value) {
53
+ return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
54
+ }
55
+
56
+ function detectProjectMetadata(projectRoot) {
57
+ const packageJsonPath = path.join(projectRoot, 'package.json');
58
+ const packageManager = detectPackageManager(projectRoot);
59
+ const metadata = {
60
+ project: path.basename(projectRoot),
61
+ version: '0.1.0',
62
+ stackLabel: 'Node.js project',
63
+ packageManager
64
+ };
65
+
66
+ if (!fs.existsSync(packageJsonPath)) {
67
+ return metadata;
68
+ }
69
+
70
+ try {
71
+ const rawPackage = fs.readFileSync(packageJsonPath, 'utf8');
72
+ const parsedPackage = JSON.parse(rawPackage);
73
+ const dependencies = Object.assign(
74
+ {},
75
+ parsedPackage.dependencies || {},
76
+ parsedPackage.devDependencies || {}
77
+ );
78
+
79
+ metadata.project = parsedPackage.name || metadata.project;
80
+ metadata.version = parsedPackage.version || metadata.version;
81
+ metadata.stackLabel = describeStack(dependencies);
82
+ metadata.packageManager = packageManager;
83
+ } catch (error) {
84
+ metadata.stackLabel = 'Node.js project';
85
+ }
86
+
87
+ return metadata;
88
+ }
89
+
90
+ function detectPackageManager(projectRoot) {
91
+ if (fs.existsSync(path.join(projectRoot, 'pnpm-lock.yaml'))) {
92
+ return 'pnpm';
93
+ }
94
+
95
+ if (fs.existsSync(path.join(projectRoot, 'yarn.lock'))) {
96
+ return 'yarn';
97
+ }
98
+
99
+ return 'npm';
100
+ }
101
+
102
+ function describeStack(dependencies) {
103
+ if (dependencies.next) {
104
+ return 'Node.js + Next.js';
105
+ }
106
+
107
+ if (dependencies.react) {
108
+ return 'Node.js + React';
109
+ }
110
+
111
+ if (dependencies.express) {
112
+ return 'Node.js + Express';
113
+ }
114
+
115
+ if (dependencies.typescript) {
116
+ return 'Node.js + TypeScript';
117
+ }
118
+
119
+ return 'Node.js project';
120
+ }
121
+
122
+ async function ensureContextDirectory(projectRoot) {
123
+ const { contextDir } = getContextPaths(projectRoot);
124
+ await fsp.mkdir(contextDir, { recursive: true });
125
+ return contextDir;
126
+ }
127
+
128
+ async function readJsonFile(filePath, fallbackValue) {
129
+ try {
130
+ const raw = await fsp.readFile(filePath, 'utf8');
131
+ return JSON.parse(raw);
132
+ } catch (error) {
133
+ return fallbackValue;
134
+ }
135
+ }
136
+
137
+ async function writeJsonAtomic(filePath, value) {
138
+ const content = `${JSON.stringify(value, null, 2)}\n`;
139
+ await writeTextAtomic(filePath, content);
140
+ }
141
+
142
+ async function writeTextAtomic(filePath, content) {
143
+ const tempFilePath = `${filePath}.tmp`;
144
+ await fsp.writeFile(tempFilePath, content, 'utf8');
145
+ await fsp.rename(tempFilePath, filePath);
146
+ }
147
+
148
+ function createDefaultState(projectRoot) {
149
+ const metadata = detectProjectMetadata(projectRoot);
150
+
151
+ return {
152
+ project: metadata.project,
153
+ version: metadata.version,
154
+ last_updated: new Date(0).toISOString(),
155
+ stats: {
156
+ files_changed: 0,
157
+ last_file: ''
158
+ },
159
+ recent_updates: [],
160
+ features: [],
161
+ next_steps: []
162
+ };
163
+ }
164
+
165
+ function createDefaultChangelog() {
166
+ return {
167
+ entries: []
168
+ };
169
+ }
170
+
171
+ function renderTemplate(template, variables) {
172
+ return Object.entries(variables).reduce((accumulator, [key, value]) => {
173
+ const safeValue = value == null ? '' : String(value);
174
+ return accumulator.split(`{{${key}}}`).join(safeValue);
175
+ }, template);
176
+ }
177
+
178
+ async function loadRuntimeConfig(projectRoot) {
179
+ const { configFile } = getContextPaths(projectRoot);
180
+ const userConfig = await readJsonFile(configFile, {});
181
+ return deepMerge(DEFAULT_CONFIG, userConfig);
182
+ }
183
+
184
+ async function updateProjectState(projectRoot, changeEvent, options) {
185
+ const settings = Object.assign(
186
+ {
187
+ logger: null,
188
+ syncCallback: null
189
+ },
190
+ options
191
+ );
192
+ const logger = settings.logger;
193
+ const contextPaths = getContextPaths(projectRoot);
194
+ const state = await readJsonFile(contextPaths.stateFile, createDefaultState(projectRoot));
195
+ const changelog = await readJsonFile(contextPaths.changelogFile, createDefaultChangelog());
196
+ const normalizedEvents = Array.isArray(changeEvent) ? changeEvent : [changeEvent];
197
+ const validEvents = normalizedEvents.filter(Boolean);
198
+
199
+ if (validEvents.length === 0) {
200
+ return state;
201
+ }
202
+
203
+ const latestEvent = validEvents[validEvents.length - 1];
204
+ const timestamp = latestEvent.timestamp || new Date().toISOString();
205
+ const recentUpdates = Array.isArray(state.recent_updates) ? state.recent_updates.slice() : [];
206
+ const changelogEntries = Array.isArray(changelog.entries) ? changelog.entries.slice() : [];
207
+
208
+ for (const event of validEvents) {
209
+ const normalizedEvent = {
210
+ timestamp: event.timestamp || timestamp,
211
+ action: event.action || 'updated',
212
+ file: event.file || ''
213
+ };
214
+
215
+ recentUpdates.push(normalizedEvent);
216
+ changelogEntries.push(normalizedEvent);
217
+ }
218
+
219
+ const nextState = {
220
+ project: state.project || createDefaultState(projectRoot).project,
221
+ version: state.version || createDefaultState(projectRoot).version,
222
+ last_updated: timestamp,
223
+ stats: {
224
+ files_changed: (state.stats && typeof state.stats.files_changed === 'number'
225
+ ? state.stats.files_changed
226
+ : 0) + validEvents.length,
227
+ last_file: latestEvent.file || ''
228
+ },
229
+ recent_updates: recentUpdates.slice(-MAX_RECENT_UPDATES),
230
+ features: Array.isArray(state.features) ? state.features : [],
231
+ next_steps: Array.isArray(state.next_steps) ? state.next_steps : []
232
+ };
233
+
234
+ const nextChangelog = {
235
+ entries: changelogEntries.slice(-MAX_CHANGELOG_ENTRIES)
236
+ };
237
+
238
+ await writeJsonAtomic(contextPaths.stateFile, nextState);
239
+ await writeJsonAtomic(contextPaths.changelogFile, nextChangelog);
240
+
241
+ if (logger) {
242
+ logger.debug(`Updated AI context for ${validEvents.length} change(s).`);
243
+ }
244
+
245
+ if (typeof settings.syncCallback === 'function') {
246
+ await settings.syncCallback();
247
+ }
248
+
249
+ return nextState;
250
+ }
251
+
252
+ function createDebouncedStateUpdater(projectRoot, options) {
253
+ const settings = Object.assign(
254
+ {
255
+ debounceMs: DEFAULT_CONFIG.debounceMs,
256
+ logger: null,
257
+ syncCallback: null
258
+ },
259
+ options
260
+ );
261
+
262
+ let timer = null;
263
+ let pendingEvents = [];
264
+ let activeFlush = Promise.resolve();
265
+
266
+ async function flush() {
267
+ if (pendingEvents.length === 0) {
268
+ return;
269
+ }
270
+
271
+ const events = pendingEvents.slice();
272
+ pendingEvents = [];
273
+
274
+ activeFlush = activeFlush.then(() =>
275
+ updateProjectState(projectRoot, events, {
276
+ logger: settings.logger,
277
+ syncCallback: settings.syncCallback
278
+ })
279
+ );
280
+
281
+ await activeFlush;
282
+ }
283
+
284
+ return {
285
+ enqueue(event) {
286
+ pendingEvents.push(event);
287
+
288
+ if (timer) {
289
+ clearTimeout(timer);
290
+ }
291
+
292
+ timer = setTimeout(() => {
293
+ timer = null;
294
+ flush().catch((error) => {
295
+ if (settings.logger) {
296
+ settings.logger.error(`Failed to flush AI context updates: ${error.message}`);
297
+ }
298
+ });
299
+ }, settings.debounceMs);
300
+ },
301
+ async flushNow() {
302
+ if (timer) {
303
+ clearTimeout(timer);
304
+ timer = null;
305
+ }
306
+
307
+ await flush();
308
+ }
309
+ };
310
+ }
311
+
312
+ module.exports = {
313
+ CONTEXT_DIR_NAME,
314
+ DEFAULT_CONFIG,
315
+ createDebouncedStateUpdater,
316
+ createDefaultChangelog,
317
+ createDefaultState,
318
+ detectProjectMetadata,
319
+ ensureContextDirectory,
320
+ getContextPaths,
321
+ loadRuntimeConfig,
322
+ readJsonFile,
323
+ renderTemplate,
324
+ updateProjectState,
325
+ writeJsonAtomic,
326
+ writeTextAtomic
327
+ };
@@ -0,0 +1,93 @@
1
+ 'use strict';
2
+
3
+ const path = require('path');
4
+ const chokidar = require('chokidar');
5
+
6
+ const { syncContextToGit } = require('./gitSync');
7
+ const {
8
+ createDebouncedStateUpdater,
9
+ loadRuntimeConfig,
10
+ updateProjectState
11
+ } = require('./stateManager');
12
+
13
+ function normalizeFilePath(projectRoot, filePath) {
14
+ return path.relative(projectRoot, filePath).split(path.sep).join('/');
15
+ }
16
+
17
+ async function startWatcher(projectRoot, options) {
18
+ const settings = Object.assign({ logger: null }, options);
19
+ const logger = settings.logger;
20
+ const config = await loadRuntimeConfig(projectRoot);
21
+ const syncCallback = async () =>
22
+ syncContextToGit(projectRoot, config.gitSync, logger);
23
+ const debouncedUpdater = createDebouncedStateUpdater(projectRoot, {
24
+ debounceMs: config.debounceMs,
25
+ logger,
26
+ syncCallback
27
+ });
28
+
29
+ const watcher = chokidar.watch(projectRoot, {
30
+ ignored: [
31
+ /(^|[\\/])\.git([\\/]|$)/,
32
+ /(^|[\\/])node_modules([\\/]|$)/,
33
+ /(^|[\\/])\.ai-context([\\/]|$)/
34
+ ],
35
+ ignoreInitial: true,
36
+ persistent: true
37
+ });
38
+
39
+ function handleEvent(action, filePath) {
40
+ const normalizedPath = normalizeFilePath(projectRoot, filePath);
41
+
42
+ if (!normalizedPath || normalizedPath.startsWith('.ai-context/')) {
43
+ return;
44
+ }
45
+
46
+ if (logger) {
47
+ logger.info(`${action} ${normalizedPath}`);
48
+ }
49
+
50
+ debouncedUpdater.enqueue({
51
+ timestamp: new Date().toISOString(),
52
+ action,
53
+ file: normalizedPath
54
+ });
55
+ }
56
+
57
+ watcher.on('add', (filePath) => handleEvent('add', filePath));
58
+ watcher.on('change', (filePath) => handleEvent('change', filePath));
59
+ watcher.on('unlink', (filePath) => handleEvent('delete', filePath));
60
+ watcher.on('error', (error) => {
61
+ if (logger) {
62
+ logger.error(`Watcher error: ${error.message}`);
63
+ }
64
+ });
65
+
66
+ await updateProjectState(
67
+ projectRoot,
68
+ {
69
+ timestamp: new Date().toISOString(),
70
+ action: 'watcher_started',
71
+ file: '.'
72
+ },
73
+ {
74
+ logger,
75
+ syncCallback
76
+ }
77
+ );
78
+
79
+ return {
80
+ watcher,
81
+ async close() {
82
+ await debouncedUpdater.flushNow();
83
+ await watcher.close();
84
+ },
85
+ async flush() {
86
+ await debouncedUpdater.flushNow();
87
+ }
88
+ };
89
+ }
90
+
91
+ module.exports = {
92
+ startWatcher
93
+ };
package/index.js ADDED
@@ -0,0 +1,16 @@
1
+ 'use strict';
2
+
3
+ const { initProject } = require('./core/init');
4
+ const { startWatcher } = require('./core/watcher');
5
+ const { updateProjectState, loadRuntimeConfig } = require('./core/stateManager');
6
+ const { startServer } = require('./server/server');
7
+ const { syncContextToGit } = require('./core/gitSync');
8
+
9
+ module.exports = {
10
+ initProject,
11
+ startWatcher,
12
+ updateProjectState,
13
+ loadRuntimeConfig,
14
+ startServer,
15
+ syncContextToGit
16
+ };
package/package.json ADDED
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "aibridge-context",
3
+ "version": "1.0.1",
4
+ "description": "Zero-config CLI and library for generating AI-readable project context, serving it locally, and syncing it with git.",
5
+ "main": "./index.js",
6
+ "bin": {
7
+ "aibridge": "./bin/cli.js",
8
+ "ai-context": "./bin/cli.js"
9
+ },
10
+ "files": [
11
+ "bin",
12
+ "core",
13
+ "server",
14
+ "templates",
15
+ "utils",
16
+ "index.js",
17
+ "README.md"
18
+ ],
19
+ "scripts": {
20
+ "start": "node ./bin/cli.js start",
21
+ "init:context": "node ./bin/cli.js init",
22
+ "update:context": "node ./bin/cli.js update"
23
+ },
24
+ "keywords": [
25
+ "ai",
26
+ "context",
27
+ "cli",
28
+ "watcher",
29
+ "express",
30
+ "developer-tools",
31
+ "git"
32
+ ],
33
+ "author": "",
34
+ "license": "MIT",
35
+ "engines": {
36
+ "node": ">=18"
37
+ },
38
+ "dependencies": {
39
+ "chokidar": "^4.0.3",
40
+ "express": "^5.1.0"
41
+ }
42
+ }
@@ -0,0 +1,50 @@
1
+ 'use strict';
2
+
3
+ const path = require('path');
4
+ const express = require('express');
5
+ const { getContextPaths } = require('../core/stateManager');
6
+
7
+ function createRoutes(projectRoot, logger) {
8
+ const router = express.Router();
9
+ const paths = getContextPaths(projectRoot);
10
+
11
+ function sendFile(filePath, contentType) {
12
+ return function routeHandler(request, response) {
13
+ response.type(contentType);
14
+ response.sendFile(
15
+ path.resolve(filePath),
16
+ {
17
+ dotfiles: 'allow'
18
+ },
19
+ (error) => {
20
+ if (!error) {
21
+ return;
22
+ }
23
+
24
+ if (logger) {
25
+ logger.error(`Failed to serve ${request.path}: ${error.message}`);
26
+ }
27
+
28
+ if (response.headersSent) {
29
+ return;
30
+ }
31
+
32
+ response.status(error.statusCode || 500).json({
33
+ error: 'Unable to read AI context file.'
34
+ });
35
+ }
36
+ );
37
+ };
38
+ }
39
+
40
+ router.get('/state.json', sendFile(paths.stateFile, 'application/json'));
41
+ router.get('/brain.txt', sendFile(paths.brainFile, 'text/plain'));
42
+ router.get('/context.md', sendFile(paths.contextFile, 'text/markdown'));
43
+ router.get('/changelog.json', sendFile(paths.changelogFile, 'application/json'));
44
+
45
+ return router;
46
+ }
47
+
48
+ module.exports = {
49
+ createRoutes
50
+ };
@@ -0,0 +1,49 @@
1
+ 'use strict';
2
+
3
+ const express = require('express');
4
+ const { createRoutes } = require('./routes');
5
+
6
+ async function startServer(options) {
7
+ const settings = Object.assign(
8
+ {
9
+ port: 3333,
10
+ projectRoot: process.cwd(),
11
+ logger: null
12
+ },
13
+ options
14
+ );
15
+ const app = express();
16
+
17
+ app.disable('x-powered-by');
18
+ app.use(createRoutes(settings.projectRoot, settings.logger));
19
+
20
+ const server = await new Promise((resolve, reject) => {
21
+ const instance = app.listen(settings.port, () => resolve(instance));
22
+ instance.on('error', reject);
23
+ });
24
+
25
+ if (settings.logger) {
26
+ settings.logger.info(`AI context server is running on http://localhost:${settings.port}`);
27
+ }
28
+
29
+ return {
30
+ app,
31
+ server,
32
+ async close() {
33
+ await new Promise((resolve, reject) => {
34
+ server.close((error) => {
35
+ if (error) {
36
+ reject(error);
37
+ return;
38
+ }
39
+
40
+ resolve();
41
+ });
42
+ });
43
+ }
44
+ };
45
+ }
46
+
47
+ module.exports = {
48
+ startServer
49
+ };
@@ -0,0 +1,9 @@
1
+ You are assisting in this project.
2
+
3
+ Before responding:
4
+
5
+ 1. Read /state.json
6
+ 2. Read /context.md
7
+ 3. Read /changelog.json
8
+
9
+ Use them as source of truth.
@@ -0,0 +1,3 @@
1
+ {
2
+ "entries": []
3
+ }
@@ -0,0 +1,18 @@
1
+ # AI Context for {{PROJECT_NAME}}
2
+
3
+ ## Project purpose
4
+
5
+ This project is tracked by aibridge-context so AI tools can reason about the current codebase state with minimal setup.
6
+
7
+ ## Stack detection
8
+
9
+ - Detected stack: {{STACK_LABEL}}
10
+ - Package manager: {{PACKAGE_MANAGER}}
11
+ - Project version: {{PROJECT_VERSION}}
12
+
13
+ ## Instructions for AI usage
14
+
15
+ 1. Read `/state.json` before making changes.
16
+ 2. Read `/changelog.json` to understand recent edits.
17
+ 3. Use `/context.md` as the durable project summary.
18
+ 4. Treat these files as the current source of truth unless the repository clearly indicates otherwise.
@@ -0,0 +1,12 @@
1
+ {
2
+ "project": "",
3
+ "version": "",
4
+ "last_updated": "1970-01-01T00:00:00.000Z",
5
+ "stats": {
6
+ "files_changed": 0,
7
+ "last_file": ""
8
+ },
9
+ "recent_updates": [],
10
+ "features": [],
11
+ "next_steps": []
12
+ }
@@ -0,0 +1,46 @@
1
+ 'use strict';
2
+
3
+ function createLogger(options) {
4
+ const settings = Object.assign({ level: 'info' }, options);
5
+ const levels = {
6
+ debug: 10,
7
+ info: 20,
8
+ warn: 30,
9
+ error: 40
10
+ };
11
+
12
+ function shouldLog(level) {
13
+ return levels[level] >= levels[settings.level];
14
+ }
15
+
16
+ function format(level, message) {
17
+ return `[ai-context] ${new Date().toISOString()} ${level.toUpperCase()} ${message}`;
18
+ }
19
+
20
+ return {
21
+ debug(message) {
22
+ if (shouldLog('debug')) {
23
+ console.debug(format('debug', message));
24
+ }
25
+ },
26
+ info(message) {
27
+ if (shouldLog('info')) {
28
+ console.log(format('info', message));
29
+ }
30
+ },
31
+ warn(message) {
32
+ if (shouldLog('warn')) {
33
+ console.warn(format('warn', message));
34
+ }
35
+ },
36
+ error(message) {
37
+ if (shouldLog('error')) {
38
+ console.error(format('error', message));
39
+ }
40
+ }
41
+ };
42
+ }
43
+
44
+ module.exports = {
45
+ createLogger
46
+ };