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 +21 -0
- package/README.md +171 -0
- package/bin/cli.js +112 -0
- package/core/gitSync.js +131 -0
- package/core/init.js +96 -0
- package/core/stateManager.js +327 -0
- package/core/watcher.js +93 -0
- package/index.js +16 -0
- package/package.json +42 -0
- package/server/routes.js +50 -0
- package/server/server.js +49 -0
- package/templates/brain.template.txt +9 -0
- package/templates/changelog.template.json +3 -0
- package/templates/context.template.md +18 -0
- package/templates/state.template.json +12 -0
- package/utils/logger.js +46 -0
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();
|
package/core/gitSync.js
ADDED
|
@@ -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
|
+
};
|
package/core/watcher.js
ADDED
|
@@ -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
|
+
}
|
package/server/routes.js
ADDED
|
@@ -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
|
+
};
|
package/server/server.js
ADDED
|
@@ -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,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.
|
package/utils/logger.js
ADDED
|
@@ -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
|
+
};
|