aibridge-context 1.1.0 → 1.3.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.
- package/README.md +26 -2
- package/bin/cli.js +58 -13
- package/core/gitSync.js +21 -16
- package/core/init.js +45 -13
- package/core/stateManager.js +680 -70
- package/core/watcher.js +9 -9
- package/package.json +1 -1
- package/server/server.js +0 -4
- package/templates/state.template.json +9 -4
package/README.md
CHANGED
|
@@ -4,6 +4,12 @@
|
|
|
4
4
|
|
|
5
5
|
Think of it as Git for AI context.
|
|
6
6
|
|
|
7
|
+
## ⚠️ Public AI Access Warning
|
|
8
|
+
|
|
9
|
+
When GitHub sync is enabled, your project context becomes publicly accessible via a URL.
|
|
10
|
+
|
|
11
|
+
Do NOT use this tool with sensitive data.
|
|
12
|
+
|
|
7
13
|
## Quick Start
|
|
8
14
|
|
|
9
15
|
```bash
|
|
@@ -12,6 +18,14 @@ npx aibridge-context link-github
|
|
|
12
18
|
npx aibridge-context start
|
|
13
19
|
```
|
|
14
20
|
|
|
21
|
+
## How It Works
|
|
22
|
+
|
|
23
|
+
1. Tracks project changes
|
|
24
|
+
2. Generates structured AI context
|
|
25
|
+
3. Syncs to GitHub (optional)
|
|
26
|
+
4. Creates a public URL
|
|
27
|
+
5. AI tools read this URL for context
|
|
28
|
+
|
|
15
29
|
## Usage
|
|
16
30
|
|
|
17
31
|
This package exposes a CLI command:
|
|
@@ -60,7 +74,7 @@ If published to npm, the package exposes the `aibridge` binary.
|
|
|
60
74
|
|
|
61
75
|
`ai-context` is supported as a legacy alias.
|
|
62
76
|
|
|
63
|
-
##
|
|
77
|
+
## Using With AI
|
|
64
78
|
|
|
65
79
|
After enabling GitHub sync, use:
|
|
66
80
|
|
|
@@ -70,6 +84,13 @@ https://raw.githubusercontent.com/<user>/<repo>/main/.ai-context/state.json
|
|
|
70
84
|
|
|
71
85
|
This URL always returns the latest project state.
|
|
72
86
|
|
|
87
|
+
Paste this into any AI:
|
|
88
|
+
|
|
89
|
+
```text
|
|
90
|
+
Use this as source of truth:
|
|
91
|
+
https://raw.githubusercontent.com/<user>/<repo>/main/.ai-context/state.json
|
|
92
|
+
```
|
|
93
|
+
|
|
73
94
|
You can also share:
|
|
74
95
|
|
|
75
96
|
```text
|
|
@@ -155,7 +176,10 @@ Default configuration:
|
|
|
155
176
|
"gitSync": {
|
|
156
177
|
"enabled": false,
|
|
157
178
|
"push": true,
|
|
158
|
-
"commitMessage": "auto: update AI context"
|
|
179
|
+
"commitMessage": "auto: update AI context",
|
|
180
|
+
"remote": "origin",
|
|
181
|
+
"branch": "main",
|
|
182
|
+
"repoUrl": ""
|
|
159
183
|
}
|
|
160
184
|
}
|
|
161
185
|
```
|
package/bin/cli.js
CHANGED
|
@@ -34,7 +34,12 @@ async function run() {
|
|
|
34
34
|
|
|
35
35
|
try {
|
|
36
36
|
if (command === 'init') {
|
|
37
|
-
await initProject(projectRoot, {
|
|
37
|
+
await initProject(projectRoot, {
|
|
38
|
+
logger,
|
|
39
|
+
requestPublicSyncConsent: true,
|
|
40
|
+
promptForPublicSyncConsent: () =>
|
|
41
|
+
promptYesNo('[aibridge] Do you want to enable GitHub sync and public AI access? (y/n) ')
|
|
42
|
+
});
|
|
38
43
|
return;
|
|
39
44
|
}
|
|
40
45
|
|
|
@@ -59,6 +64,19 @@ async function run() {
|
|
|
59
64
|
|
|
60
65
|
if (command === 'link-github') {
|
|
61
66
|
await initProject(projectRoot, { logger });
|
|
67
|
+
logger.warn('You are about to connect a GitHub repository.');
|
|
68
|
+
logger.info('This will:');
|
|
69
|
+
logger.info('* Push .ai-context to GitHub');
|
|
70
|
+
logger.info('* Make project context publicly accessible');
|
|
71
|
+
logger.info('* Allow AI systems to read your project state');
|
|
72
|
+
|
|
73
|
+
const shouldContinue = await promptYesNo('[aibridge] Continue? (y/n) ');
|
|
74
|
+
|
|
75
|
+
if (!shouldContinue) {
|
|
76
|
+
logger.info('GitHub linking cancelled. Public sync remains unchanged.');
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
|
|
62
80
|
const repoUrl = process.argv[3] || (await promptForRepoUrl());
|
|
63
81
|
|
|
64
82
|
if (!repoUrl) {
|
|
@@ -88,11 +106,23 @@ async function run() {
|
|
|
88
106
|
if (command === 'start') {
|
|
89
107
|
await initProject(projectRoot, { logger });
|
|
90
108
|
const config = await loadRuntimeConfig(projectRoot);
|
|
109
|
+
const port = Number(process.env.AI_CONTEXT_PORT || config.port || 3333);
|
|
110
|
+
logger.info('Watching project for changes...');
|
|
91
111
|
const serverHandle = await startServer({
|
|
92
112
|
projectRoot,
|
|
93
|
-
port
|
|
113
|
+
port,
|
|
94
114
|
logger
|
|
95
115
|
});
|
|
116
|
+
logger.info(`Local server: http://localhost:${port}`);
|
|
117
|
+
|
|
118
|
+
if (config.gitSync.enabled) {
|
|
119
|
+
logger.info('Auto-sync to GitHub is ENABLED');
|
|
120
|
+
logger.warn('Changes will be publicly available to AI');
|
|
121
|
+
} else {
|
|
122
|
+
logger.info('GitHub sync is DISABLED');
|
|
123
|
+
logger.info('Data is only available locally');
|
|
124
|
+
}
|
|
125
|
+
|
|
96
126
|
const watcherHandle = await startWatcher(projectRoot, { logger });
|
|
97
127
|
|
|
98
128
|
const shutdown = async (signal) => {
|
|
@@ -131,17 +161,14 @@ async function run() {
|
|
|
131
161
|
function printHelp() {
|
|
132
162
|
console.log(`aibridge-context
|
|
133
163
|
|
|
134
|
-
Usage:
|
|
135
|
-
aibridge init
|
|
136
|
-
aibridge link-github
|
|
137
|
-
aibridge start
|
|
138
|
-
aibridge update
|
|
139
|
-
|
|
140
164
|
Commands:
|
|
141
|
-
init
|
|
142
|
-
link-github
|
|
143
|
-
start
|
|
144
|
-
update
|
|
165
|
+
init Initialize AI context (with optional public sync)
|
|
166
|
+
link-github Connect GitHub for public AI access
|
|
167
|
+
start Start watcher and server
|
|
168
|
+
update Manually update context
|
|
169
|
+
|
|
170
|
+
Description:
|
|
171
|
+
This tool creates an AI-readable version of your project and can expose it via a public URL for AI tools.
|
|
145
172
|
`);
|
|
146
173
|
}
|
|
147
174
|
|
|
@@ -152,11 +179,29 @@ async function promptForRepoUrl() {
|
|
|
152
179
|
});
|
|
153
180
|
|
|
154
181
|
try {
|
|
155
|
-
const response = await rl.question('GitHub repository URL: ');
|
|
182
|
+
const response = await rl.question('[aibridge] GitHub repository URL: ');
|
|
156
183
|
return response.trim();
|
|
157
184
|
} finally {
|
|
158
185
|
rl.close();
|
|
159
186
|
}
|
|
160
187
|
}
|
|
161
188
|
|
|
189
|
+
async function promptYesNo(question) {
|
|
190
|
+
if (!stdin.isTTY || !stdout.isTTY) {
|
|
191
|
+
return false;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const rl = readline.createInterface({
|
|
195
|
+
input: stdin,
|
|
196
|
+
output: stdout
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
try {
|
|
200
|
+
const response = await rl.question(question);
|
|
201
|
+
return response.trim().toLowerCase() === 'y';
|
|
202
|
+
} finally {
|
|
203
|
+
rl.close();
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
162
207
|
run();
|
package/core/gitSync.js
CHANGED
|
@@ -74,6 +74,13 @@ async function hasStagedContextChanges(projectRoot) {
|
|
|
74
74
|
}
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
+
function formatGitError(error) {
|
|
78
|
+
const stderr = typeof error.stderr === 'string' ? error.stderr.trim() : '';
|
|
79
|
+
const stdout = typeof error.stdout === 'string' ? error.stdout.trim() : '';
|
|
80
|
+
const message = stderr || stdout || error.message || 'Unknown git error.';
|
|
81
|
+
return message.split('\n')[0];
|
|
82
|
+
}
|
|
83
|
+
|
|
77
84
|
async function ensureGitInitialized(projectRoot, logger) {
|
|
78
85
|
const repositoryReady = await isGitRepository(projectRoot);
|
|
79
86
|
|
|
@@ -93,6 +100,7 @@ async function ensureGitInitialized(projectRoot, logger) {
|
|
|
93
100
|
throw error;
|
|
94
101
|
}
|
|
95
102
|
}
|
|
103
|
+
|
|
96
104
|
await ensureMainBranch(projectRoot);
|
|
97
105
|
|
|
98
106
|
if (logger) {
|
|
@@ -104,7 +112,7 @@ async function ensureGitInitialized(projectRoot, logger) {
|
|
|
104
112
|
};
|
|
105
113
|
} catch (error) {
|
|
106
114
|
if (logger) {
|
|
107
|
-
logger.warn(`Git initialization failed gracefully: ${error
|
|
115
|
+
logger.warn(`Git initialization failed gracefully: ${formatGitError(error)}`);
|
|
108
116
|
}
|
|
109
117
|
|
|
110
118
|
return {
|
|
@@ -156,7 +164,7 @@ function logMissingRemoteInstructions(logger) {
|
|
|
156
164
|
return;
|
|
157
165
|
}
|
|
158
166
|
|
|
159
|
-
logger.warn('
|
|
167
|
+
logger.warn('GitHub remote not found.');
|
|
160
168
|
logger.info('Run:');
|
|
161
169
|
logger.info('git remote add origin <repo-url>');
|
|
162
170
|
logger.info('git push -u origin main');
|
|
@@ -167,10 +175,15 @@ function logPublicAiEndpoints(logger, urls) {
|
|
|
167
175
|
return;
|
|
168
176
|
}
|
|
169
177
|
|
|
170
|
-
logger.info('
|
|
178
|
+
logger.info('\u2705 AI Context Synced Successfully');
|
|
179
|
+
logger.info('\u{1F310} Public AI Endpoint:');
|
|
171
180
|
logger.info(urls.stateUrl);
|
|
172
|
-
logger.info('
|
|
181
|
+
logger.info('\u{1F9E0} AI Instructions Endpoint:');
|
|
173
182
|
logger.info(urls.brainUrl);
|
|
183
|
+
logger.warn('\u26A0\uFE0F IMPORTANT:');
|
|
184
|
+
logger.warn('This data is PUBLIC. Anyone with this link can access it.');
|
|
185
|
+
logger.info('\u{1F916} To use with AI:');
|
|
186
|
+
logger.info('"Use this URL as the source of truth for my project."');
|
|
174
187
|
}
|
|
175
188
|
|
|
176
189
|
async function linkGithubRepository(projectRoot, repoUrl, logger) {
|
|
@@ -189,16 +202,13 @@ async function linkGithubRepository(projectRoot, repoUrl, logger) {
|
|
|
189
202
|
await ensureMainBranch(projectRoot);
|
|
190
203
|
await runGit(projectRoot, ['push', '-u', 'origin', 'main']);
|
|
191
204
|
|
|
192
|
-
const urls = buildPublicAiUrls(normalizedRepoUrl, 'main');
|
|
193
|
-
logPublicAiEndpoints(logger, urls);
|
|
194
|
-
|
|
195
205
|
return {
|
|
196
206
|
ok: true,
|
|
197
|
-
urls
|
|
207
|
+
urls: buildPublicAiUrls(normalizedRepoUrl, 'main')
|
|
198
208
|
};
|
|
199
209
|
} catch (error) {
|
|
200
210
|
if (logger) {
|
|
201
|
-
logger.warn(`GitHub link failed gracefully: ${error
|
|
211
|
+
logger.warn(`GitHub link failed gracefully: ${formatGitError(error)}`);
|
|
202
212
|
}
|
|
203
213
|
|
|
204
214
|
return {
|
|
@@ -211,7 +221,7 @@ async function linkGithubRepository(projectRoot, repoUrl, logger) {
|
|
|
211
221
|
async function syncContextToGit(projectRoot, config, logger) {
|
|
212
222
|
const settings = Object.assign(
|
|
213
223
|
{
|
|
214
|
-
enabled:
|
|
224
|
+
enabled: false,
|
|
215
225
|
push: true,
|
|
216
226
|
commitMessage: 'auto: update AI context',
|
|
217
227
|
remote: 'origin',
|
|
@@ -285,11 +295,6 @@ async function syncContextToGit(projectRoot, config, logger) {
|
|
|
285
295
|
}
|
|
286
296
|
|
|
287
297
|
const urls = buildPublicAiUrls(remoteUrl, branchName);
|
|
288
|
-
|
|
289
|
-
if (logger) {
|
|
290
|
-
logger.info('Synced .ai-context changes to git.');
|
|
291
|
-
}
|
|
292
|
-
|
|
293
298
|
logPublicAiEndpoints(logger, urls);
|
|
294
299
|
|
|
295
300
|
return {
|
|
@@ -299,7 +304,7 @@ async function syncContextToGit(projectRoot, config, logger) {
|
|
|
299
304
|
};
|
|
300
305
|
} catch (error) {
|
|
301
306
|
if (logger) {
|
|
302
|
-
logger.warn(`Git sync failed gracefully: ${error
|
|
307
|
+
logger.warn(`Git sync failed gracefully: ${formatGitError(error)}`);
|
|
303
308
|
}
|
|
304
309
|
|
|
305
310
|
return {
|
package/core/init.js
CHANGED
|
@@ -18,7 +18,15 @@ const {
|
|
|
18
18
|
const { ensureGitInitialized } = require('./gitSync');
|
|
19
19
|
|
|
20
20
|
async function initProject(projectRoot, options) {
|
|
21
|
-
const settings = Object.assign(
|
|
21
|
+
const settings = Object.assign(
|
|
22
|
+
{
|
|
23
|
+
logger: null,
|
|
24
|
+
force: false,
|
|
25
|
+
requestPublicSyncConsent: false,
|
|
26
|
+
promptForPublicSyncConsent: null
|
|
27
|
+
},
|
|
28
|
+
options
|
|
29
|
+
);
|
|
22
30
|
const logger = settings.logger;
|
|
23
31
|
const contextDir = await ensureContextDirectory(projectRoot);
|
|
24
32
|
const paths = getContextPaths(projectRoot);
|
|
@@ -36,6 +44,7 @@ async function initProject(projectRoot, options) {
|
|
|
36
44
|
const existingChangelog = await readJsonFile(paths.changelogFile, null);
|
|
37
45
|
const templateState = JSON.parse(stateTemplate);
|
|
38
46
|
const templateChangelog = JSON.parse(changelogTemplate);
|
|
47
|
+
let enableGitSync = false;
|
|
39
48
|
|
|
40
49
|
const initialState = Object.assign({}, templateState, existingState || createDefaultState(projectRoot), {
|
|
41
50
|
project: metadata.project,
|
|
@@ -62,18 +71,18 @@ async function initProject(projectRoot, options) {
|
|
|
62
71
|
await writeTextAtomic(paths.contextFile, initialContext);
|
|
63
72
|
}
|
|
64
73
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
);
|
|
74
|
+
if (!existingConfig && settings.requestPublicSyncConsent) {
|
|
75
|
+
enableGitSync = await requestPublicSyncConsent(
|
|
76
|
+
logger,
|
|
77
|
+
settings.promptForPublicSyncConsent
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
await updateRuntimeConfig(projectRoot, existingConfig ? null : {
|
|
82
|
+
gitSync: {
|
|
83
|
+
enabled: enableGitSync
|
|
84
|
+
}
|
|
85
|
+
});
|
|
77
86
|
|
|
78
87
|
await ensureGitInitialized(projectRoot, logger);
|
|
79
88
|
|
|
@@ -87,6 +96,29 @@ async function initProject(projectRoot, options) {
|
|
|
87
96
|
};
|
|
88
97
|
}
|
|
89
98
|
|
|
99
|
+
async function requestPublicSyncConsent(logger, promptForPublicSyncConsent) {
|
|
100
|
+
if (logger) {
|
|
101
|
+
logger.warn('This tool can make parts of your project publicly accessible for AI tools.');
|
|
102
|
+
logger.info('It will:');
|
|
103
|
+
logger.info('* Track project changes');
|
|
104
|
+
logger.info('* Generate AI-readable context');
|
|
105
|
+
logger.info('* Optionally sync to GitHub');
|
|
106
|
+
logger.info('* Create a PUBLIC URL accessible by AI systems');
|
|
107
|
+
logger.warn('WARNING:');
|
|
108
|
+
logger.warn('Anyone with the URL can read this data.');
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (typeof promptForPublicSyncConsent !== 'function') {
|
|
112
|
+
if (logger) {
|
|
113
|
+
logger.info('GitHub sync will remain disabled until you explicitly enable it.');
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return false;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return Boolean(await promptForPublicSyncConsent());
|
|
120
|
+
}
|
|
121
|
+
|
|
90
122
|
async function fileExists(filePath) {
|
|
91
123
|
try {
|
|
92
124
|
await fsp.access(filePath);
|
package/core/stateManager.js
CHANGED
|
@@ -5,14 +5,16 @@ const fsp = require('fs/promises');
|
|
|
5
5
|
const path = require('path');
|
|
6
6
|
|
|
7
7
|
const CONTEXT_DIR_NAME = '.ai-context';
|
|
8
|
-
const MAX_RECENT_UPDATES =
|
|
9
|
-
const MAX_CHANGELOG_ENTRIES =
|
|
8
|
+
const MAX_RECENT_UPDATES = 8;
|
|
9
|
+
const MAX_CHANGELOG_ENTRIES = 50;
|
|
10
|
+
const IMPORTANT_DIRECTORIES = ['core/', 'server/', 'bin/'];
|
|
11
|
+
const IMPORTANT_EXTENSIONS = new Set(['.js', '.ts', '.py']);
|
|
10
12
|
|
|
11
13
|
const DEFAULT_CONFIG = {
|
|
12
14
|
port: 3333,
|
|
13
15
|
debounceMs: 600,
|
|
14
16
|
gitSync: {
|
|
15
|
-
enabled:
|
|
17
|
+
enabled: false,
|
|
16
18
|
push: true,
|
|
17
19
|
commitMessage: 'auto: update AI context',
|
|
18
20
|
remote: 'origin',
|
|
@@ -59,10 +61,14 @@ function isObject(value) {
|
|
|
59
61
|
function detectProjectMetadata(projectRoot) {
|
|
60
62
|
const packageJsonPath = path.join(projectRoot, 'package.json');
|
|
61
63
|
const packageManager = detectPackageManager(projectRoot);
|
|
64
|
+
const techStack = detectTechStack(projectRoot);
|
|
62
65
|
const metadata = {
|
|
63
66
|
project: path.basename(projectRoot),
|
|
64
67
|
version: '0.1.0',
|
|
65
|
-
|
|
68
|
+
techStack: Object.assign({}, techStack, {
|
|
69
|
+
package_manager: packageManager
|
|
70
|
+
}),
|
|
71
|
+
stackLabel: buildStackLabel(techStack),
|
|
66
72
|
packageManager
|
|
67
73
|
};
|
|
68
74
|
|
|
@@ -73,53 +79,134 @@ function detectProjectMetadata(projectRoot) {
|
|
|
73
79
|
try {
|
|
74
80
|
const rawPackage = fs.readFileSync(packageJsonPath, 'utf8');
|
|
75
81
|
const parsedPackage = JSON.parse(rawPackage);
|
|
76
|
-
const dependencies = Object.assign(
|
|
77
|
-
{},
|
|
78
|
-
parsedPackage.dependencies || {},
|
|
79
|
-
parsedPackage.devDependencies || {}
|
|
80
|
-
);
|
|
81
82
|
|
|
82
83
|
metadata.project = parsedPackage.name || metadata.project;
|
|
83
84
|
metadata.version = parsedPackage.version || metadata.version;
|
|
84
|
-
metadata.stackLabel = describeStack(dependencies);
|
|
85
|
-
metadata.packageManager = packageManager;
|
|
86
85
|
} catch (error) {
|
|
87
|
-
metadata.stackLabel =
|
|
86
|
+
metadata.stackLabel = buildStackLabel(metadata.techStack);
|
|
88
87
|
}
|
|
89
88
|
|
|
90
89
|
return metadata;
|
|
91
90
|
}
|
|
92
91
|
|
|
93
|
-
function
|
|
94
|
-
|
|
95
|
-
|
|
92
|
+
function detectTechStack(projectRoot) {
|
|
93
|
+
const packageJsonPath = path.join(projectRoot, 'package.json');
|
|
94
|
+
const pythonMarkers = ['pyproject.toml', 'requirements.txt', 'setup.py'];
|
|
95
|
+
const hasPackageJson = fs.existsSync(packageJsonPath);
|
|
96
|
+
const hasPythonMarker = pythonMarkers.some((marker) =>
|
|
97
|
+
fs.existsSync(path.join(projectRoot, marker))
|
|
98
|
+
);
|
|
99
|
+
let dependencies = {};
|
|
100
|
+
|
|
101
|
+
if (hasPackageJson) {
|
|
102
|
+
try {
|
|
103
|
+
const rawPackage = fs.readFileSync(packageJsonPath, 'utf8');
|
|
104
|
+
const parsedPackage = JSON.parse(rawPackage);
|
|
105
|
+
dependencies = Object.assign(
|
|
106
|
+
{},
|
|
107
|
+
parsedPackage.dependencies || {},
|
|
108
|
+
parsedPackage.devDependencies || {}
|
|
109
|
+
);
|
|
110
|
+
} catch (error) {
|
|
111
|
+
dependencies = {};
|
|
112
|
+
}
|
|
96
113
|
}
|
|
97
114
|
|
|
98
|
-
|
|
99
|
-
|
|
115
|
+
let language = '';
|
|
116
|
+
let runtime = '';
|
|
117
|
+
|
|
118
|
+
if (hasPackageJson || hasAnyFileExtension(projectRoot, ['.js', '.ts', '.mjs', '.cjs'])) {
|
|
119
|
+
language = 'Node.js';
|
|
120
|
+
runtime = 'Node.js';
|
|
121
|
+
} else if (hasPythonMarker || hasAnyFileExtension(projectRoot, ['.py'])) {
|
|
122
|
+
language = 'Python';
|
|
123
|
+
runtime = 'Python';
|
|
100
124
|
}
|
|
101
125
|
|
|
102
|
-
return
|
|
126
|
+
return {
|
|
127
|
+
language,
|
|
128
|
+
framework: detectFramework(dependencies),
|
|
129
|
+
runtime,
|
|
130
|
+
package_manager: detectPackageManager(projectRoot)
|
|
131
|
+
};
|
|
103
132
|
}
|
|
104
133
|
|
|
105
|
-
function
|
|
134
|
+
function detectFramework(dependencies) {
|
|
106
135
|
if (dependencies.next) {
|
|
107
|
-
return '
|
|
136
|
+
return 'Next.js';
|
|
108
137
|
}
|
|
109
138
|
|
|
110
139
|
if (dependencies.react) {
|
|
111
|
-
return '
|
|
140
|
+
return 'React';
|
|
112
141
|
}
|
|
113
142
|
|
|
114
143
|
if (dependencies.express) {
|
|
115
|
-
return '
|
|
144
|
+
return 'Express';
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return '';
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function buildStackLabel(techStack) {
|
|
151
|
+
const parts = [techStack.language, techStack.framework].filter(Boolean);
|
|
152
|
+
return parts.length > 0 ? parts.join(' + ') : 'Project';
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function hasAnyFileExtension(projectRoot, extensions) {
|
|
156
|
+
return scanProjectFiles(projectRoot, 2).some((filePath) =>
|
|
157
|
+
extensions.includes(path.extname(filePath).toLowerCase())
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function scanProjectFiles(projectRoot, maxDepth) {
|
|
162
|
+
const results = [];
|
|
163
|
+
|
|
164
|
+
function visit(currentDir, depth) {
|
|
165
|
+
if (depth > maxDepth) {
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
let entries = [];
|
|
170
|
+
|
|
171
|
+
try {
|
|
172
|
+
entries = fs.readdirSync(currentDir, { withFileTypes: true });
|
|
173
|
+
} catch (error) {
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
for (const entry of entries) {
|
|
178
|
+
const fullPath = path.join(currentDir, entry.name);
|
|
179
|
+
const relativePath = normalizeProjectPath(path.relative(projectRoot, fullPath));
|
|
180
|
+
|
|
181
|
+
if (entry.isDirectory()) {
|
|
182
|
+
if (shouldIgnoreProjectFile(relativePath)) {
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
visit(fullPath, depth + 1);
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (!shouldIgnoreProjectFile(relativePath)) {
|
|
191
|
+
results.push(relativePath);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
116
194
|
}
|
|
117
195
|
|
|
118
|
-
|
|
119
|
-
|
|
196
|
+
visit(projectRoot, 0);
|
|
197
|
+
return results;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function detectPackageManager(projectRoot) {
|
|
201
|
+
if (fs.existsSync(path.join(projectRoot, 'pnpm-lock.yaml'))) {
|
|
202
|
+
return 'pnpm';
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (fs.existsSync(path.join(projectRoot, 'yarn.lock'))) {
|
|
206
|
+
return 'yarn';
|
|
120
207
|
}
|
|
121
208
|
|
|
122
|
-
return '
|
|
209
|
+
return 'npm';
|
|
123
210
|
}
|
|
124
211
|
|
|
125
212
|
async function ensureContextDirectory(projectRoot) {
|
|
@@ -150,19 +237,26 @@ async function writeTextAtomic(filePath, content) {
|
|
|
150
237
|
|
|
151
238
|
function createDefaultState(projectRoot) {
|
|
152
239
|
const metadata = detectProjectMetadata(projectRoot);
|
|
153
|
-
|
|
154
|
-
|
|
240
|
+
const keyFeatures = deriveKeyFeatures(projectRoot);
|
|
241
|
+
const knownIssues = deriveKnownIssues(projectRoot, metadata.techStack);
|
|
242
|
+
const currentStage = determineCurrentStage(keyFeatures);
|
|
243
|
+
const state = {
|
|
155
244
|
project: metadata.project,
|
|
156
245
|
version: metadata.version,
|
|
157
246
|
last_updated: new Date(0).toISOString(),
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
},
|
|
247
|
+
ai_summary: '',
|
|
248
|
+
tech_stack: metadata.techStack,
|
|
249
|
+
current_stage: currentStage,
|
|
162
250
|
recent_updates: [],
|
|
163
|
-
|
|
251
|
+
key_features: keyFeatures,
|
|
252
|
+
known_issues: knownIssues,
|
|
164
253
|
next_steps: []
|
|
165
254
|
};
|
|
255
|
+
|
|
256
|
+
state.ai_summary = generateAiSummary(state);
|
|
257
|
+
state.next_steps = generateNextSteps(state);
|
|
258
|
+
|
|
259
|
+
return state;
|
|
166
260
|
}
|
|
167
261
|
|
|
168
262
|
function createDefaultChangelog() {
|
|
@@ -204,62 +298,575 @@ async function updateProjectState(projectRoot, changeEvent, options) {
|
|
|
204
298
|
);
|
|
205
299
|
const logger = settings.logger;
|
|
206
300
|
const contextPaths = getContextPaths(projectRoot);
|
|
207
|
-
const
|
|
208
|
-
const
|
|
301
|
+
const metadata = detectProjectMetadata(projectRoot);
|
|
302
|
+
const existingState = await readJsonFile(contextPaths.stateFile, createDefaultState(projectRoot));
|
|
303
|
+
const existingChangelog = await readJsonFile(
|
|
304
|
+
contextPaths.changelogFile,
|
|
305
|
+
createDefaultChangelog()
|
|
306
|
+
);
|
|
209
307
|
const normalizedEvents = Array.isArray(changeEvent) ? changeEvent : [changeEvent];
|
|
210
308
|
const validEvents = normalizedEvents.filter(Boolean);
|
|
309
|
+
const timestamp = determineUpdateTimestamp(validEvents);
|
|
310
|
+
const meaningfulEvents = collapseEventsByFile(
|
|
311
|
+
validEvents.filter((event) => isMeaningfulEvent(event))
|
|
312
|
+
);
|
|
313
|
+
const interpretedEvents = meaningfulEvents
|
|
314
|
+
.map((event) => interpretChange(event))
|
|
315
|
+
.filter(Boolean);
|
|
316
|
+
const previousRecentUpdates = normalizeStoredUpdates(existingState.recent_updates);
|
|
317
|
+
const previousHistoryEntries = normalizeStoredHistoryEntries(existingChangelog.entries);
|
|
318
|
+
const recentUpdates = dedupeRecentUpdates(
|
|
319
|
+
interpretedEvents.map(toStateUpdate).concat(previousRecentUpdates)
|
|
320
|
+
).slice(0, MAX_RECENT_UPDATES);
|
|
321
|
+
const historyEntries = dedupeHistoryEntries(
|
|
322
|
+
interpretedEvents.concat(previousHistoryEntries)
|
|
323
|
+
).slice(0, MAX_CHANGELOG_ENTRIES);
|
|
324
|
+
const keyFeatures = deriveKeyFeatures(projectRoot);
|
|
325
|
+
const knownIssues = deriveKnownIssues(projectRoot, metadata.techStack);
|
|
326
|
+
const nextState = {
|
|
327
|
+
project: metadata.project,
|
|
328
|
+
version: metadata.version,
|
|
329
|
+
last_updated: timestamp,
|
|
330
|
+
ai_summary: '',
|
|
331
|
+
tech_stack: metadata.techStack,
|
|
332
|
+
current_stage: determineCurrentStage(keyFeatures),
|
|
333
|
+
recent_updates: recentUpdates,
|
|
334
|
+
key_features: keyFeatures,
|
|
335
|
+
known_issues: knownIssues,
|
|
336
|
+
next_steps: []
|
|
337
|
+
};
|
|
338
|
+
|
|
339
|
+
nextState.ai_summary = generateAiSummary(nextState);
|
|
340
|
+
nextState.next_steps = generateNextSteps(nextState);
|
|
341
|
+
|
|
342
|
+
await writeJsonAtomic(contextPaths.stateFile, nextState);
|
|
343
|
+
await writeJsonAtomic(contextPaths.changelogFile, { entries: historyEntries });
|
|
211
344
|
|
|
212
|
-
if (
|
|
213
|
-
|
|
345
|
+
if (logger) {
|
|
346
|
+
logger.debug(`Updated AI context with ${interpretedEvents.length} meaningful change(s).`);
|
|
214
347
|
}
|
|
215
348
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
const changelogEntries = Array.isArray(changelog.entries) ? changelog.entries.slice() : [];
|
|
349
|
+
if (typeof settings.syncCallback === 'function') {
|
|
350
|
+
await settings.syncCallback();
|
|
351
|
+
}
|
|
220
352
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
timestamp: event.timestamp || timestamp,
|
|
224
|
-
action: event.action || 'updated',
|
|
225
|
-
file: event.file || ''
|
|
226
|
-
};
|
|
353
|
+
return nextState;
|
|
354
|
+
}
|
|
227
355
|
|
|
228
|
-
|
|
229
|
-
|
|
356
|
+
function determineUpdateTimestamp(events) {
|
|
357
|
+
if (!Array.isArray(events) || events.length === 0) {
|
|
358
|
+
return new Date().toISOString();
|
|
230
359
|
}
|
|
231
360
|
|
|
232
|
-
const
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
361
|
+
const latestEvent = events[events.length - 1];
|
|
362
|
+
return latestEvent.timestamp || new Date().toISOString();
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
function normalizeProjectPath(filePath) {
|
|
366
|
+
return String(filePath || '').split(path.sep).join('/').replace(/^\.\/+/, '');
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
function shouldIgnoreProjectFile(filePath) {
|
|
370
|
+
const normalizedPath = normalizeProjectPath(filePath).toLowerCase();
|
|
371
|
+
|
|
372
|
+
if (!normalizedPath) {
|
|
373
|
+
return false;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
const segments = normalizedPath.split('/');
|
|
377
|
+
const baseName = segments[segments.length - 1];
|
|
378
|
+
|
|
379
|
+
if (
|
|
380
|
+
segments.includes('node_modules') ||
|
|
381
|
+
segments.includes('.git') ||
|
|
382
|
+
segments.includes('.ai-context') ||
|
|
383
|
+
segments.includes('dist') ||
|
|
384
|
+
segments.includes('build')
|
|
385
|
+
) {
|
|
386
|
+
return true;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
if (baseName.startsWith('.start')) {
|
|
390
|
+
return true;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
if (
|
|
394
|
+
baseName.endsWith('.log') ||
|
|
395
|
+
baseName.endsWith('.tmp') ||
|
|
396
|
+
baseName.endsWith('.lock') ||
|
|
397
|
+
baseName === 'package-lock.json' ||
|
|
398
|
+
baseName === 'yarn.lock' ||
|
|
399
|
+
baseName === 'pnpm-lock.yaml' ||
|
|
400
|
+
/^tmp[._-]/i.test(baseName) ||
|
|
401
|
+
/^temp[._-]/i.test(baseName) ||
|
|
402
|
+
/^debug[._-]/i.test(baseName)
|
|
403
|
+
) {
|
|
404
|
+
return true;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
return false;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
function scoreEvent(filePath) {
|
|
411
|
+
const normalizedPath = normalizeProjectPath(filePath);
|
|
412
|
+
const lowerPath = normalizedPath.toLowerCase();
|
|
413
|
+
const baseName = path.basename(normalizedPath).toLowerCase();
|
|
414
|
+
let score = 0;
|
|
415
|
+
|
|
416
|
+
if (shouldIgnoreProjectFile(normalizedPath)) {
|
|
417
|
+
return -5;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
if (IMPORTANT_DIRECTORIES.some((directory) => lowerPath.startsWith(directory))) {
|
|
421
|
+
score += 3;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
if (IMPORTANT_EXTENSIONS.has(path.extname(baseName))) {
|
|
425
|
+
score += 2;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
if (lowerPath === 'package.json') {
|
|
429
|
+
score += 2;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
if (lowerPath === 'readme.md') {
|
|
433
|
+
score += 1;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
return score;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
function isMeaningfulEvent(event) {
|
|
440
|
+
if (!event || !event.file) {
|
|
441
|
+
return false;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
return scoreEvent(event.file) >= 2;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
function collapseEventsByFile(events) {
|
|
448
|
+
const collapsedEvents = new Map();
|
|
449
|
+
|
|
450
|
+
for (const event of events) {
|
|
451
|
+
const normalizedFile = normalizeProjectPath(event.file).toLowerCase();
|
|
452
|
+
collapsedEvents.set(
|
|
453
|
+
normalizedFile,
|
|
454
|
+
Object.assign({}, event, {
|
|
455
|
+
file: normalizeProjectPath(event.file)
|
|
456
|
+
})
|
|
457
|
+
);
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
return Array.from(collapsedEvents.values());
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
function interpretChange(event) {
|
|
464
|
+
const filePath = normalizeProjectPath(event.file);
|
|
465
|
+
const area = classifyChangeArea(filePath);
|
|
466
|
+
const subject = describeChangeSubject(filePath, area);
|
|
467
|
+
const type = mapActionToType(event.action);
|
|
468
|
+
const title = `${mapActionToVerb(event.action)} ${subject}`;
|
|
469
|
+
|
|
470
|
+
return {
|
|
471
|
+
timestamp: event.timestamp || new Date().toISOString(),
|
|
472
|
+
file: filePath,
|
|
473
|
+
title,
|
|
474
|
+
type,
|
|
475
|
+
impact: describeImpact(area, subject, event.action)
|
|
245
476
|
};
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
function classifyChangeArea(filePath) {
|
|
480
|
+
const lowerPath = filePath.toLowerCase();
|
|
481
|
+
|
|
482
|
+
if (lowerPath === 'package.json') {
|
|
483
|
+
return 'dependencies';
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
if (lowerPath === 'readme.md') {
|
|
487
|
+
return 'documentation';
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
if (lowerPath.startsWith('core/')) {
|
|
491
|
+
return 'logic';
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
if (lowerPath.startsWith('server/')) {
|
|
495
|
+
return 'backend';
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
if (lowerPath.startsWith('bin/')) {
|
|
499
|
+
return 'CLI';
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
if (lowerPath.startsWith('templates/')) {
|
|
503
|
+
return 'templates';
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
return 'project';
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
function describeChangeSubject(filePath, area) {
|
|
510
|
+
const lowerPath = filePath.toLowerCase();
|
|
511
|
+
const baseName = path.basename(filePath);
|
|
512
|
+
|
|
513
|
+
if (lowerPath === 'package.json') {
|
|
514
|
+
return 'dependency configuration';
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
if (lowerPath === 'readme.md') {
|
|
518
|
+
return 'documentation';
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
if (lowerPath === 'core/gitsync.js') {
|
|
522
|
+
return 'GitHub sync logic';
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
if (lowerPath === 'core/statemanager.js') {
|
|
526
|
+
return 'state intelligence logic';
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
if (lowerPath === 'core/watcher.js') {
|
|
530
|
+
return 'watcher logic';
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
if (lowerPath === 'core/init.js') {
|
|
534
|
+
return 'initialization flow';
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
if (lowerPath === 'server/server.js') {
|
|
538
|
+
return 'backend server';
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
if (lowerPath === 'server/routes.js') {
|
|
542
|
+
return 'backend routes';
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
if (lowerPath === 'bin/cli.js') {
|
|
546
|
+
return 'CLI workflow';
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
if (area === 'templates') {
|
|
550
|
+
return `${humanizeFileName(baseName)} template`;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
if (area === 'logic') {
|
|
554
|
+
return `${humanizeFileName(baseName)} logic`;
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
if (area === 'backend') {
|
|
558
|
+
return `${humanizeFileName(baseName)} backend`;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
if (area === 'CLI') {
|
|
562
|
+
return 'CLI workflow';
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
return humanizeFileName(baseName);
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
function humanizeFileName(fileName) {
|
|
569
|
+
return fileName
|
|
570
|
+
.replace(path.extname(fileName), '')
|
|
571
|
+
.replace(/[-_.]+/g, ' ')
|
|
572
|
+
.replace(/\s+/g, ' ')
|
|
573
|
+
.trim();
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
function mapActionToType(action) {
|
|
577
|
+
if (action === 'add') {
|
|
578
|
+
return 'feature';
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
if (action === 'delete') {
|
|
582
|
+
return 'removal';
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
return 'improvement';
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
function mapActionToVerb(action) {
|
|
589
|
+
if (action === 'add') {
|
|
590
|
+
return 'Added';
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
if (action === 'delete') {
|
|
594
|
+
return 'Removed';
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
return 'Updated';
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
function describeImpact(area, subject, action) {
|
|
601
|
+
if (action === 'delete') {
|
|
602
|
+
return `Removes ${subject.toLowerCase()} from the project workflow.`;
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
if (subject === 'GitHub sync logic') {
|
|
606
|
+
return 'Improves reliability of context syncing.';
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
if (subject === 'state intelligence logic') {
|
|
610
|
+
return 'Improves the quality of AI-readable project state.';
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
if (subject === 'watcher logic') {
|
|
614
|
+
return 'Improves how meaningful project changes are detected.';
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
if (area === 'backend') {
|
|
618
|
+
return 'Improves local AI context delivery.';
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
if (area === 'CLI') {
|
|
622
|
+
return 'Improves command-line workflow clarity and usability.';
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
if (area === 'documentation') {
|
|
626
|
+
return 'Improves onboarding and usage clarity.';
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
if (area === 'dependencies') {
|
|
630
|
+
return 'Updates package behavior and dependency management.';
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
if (area === 'templates') {
|
|
634
|
+
return 'Improves generated AI context defaults.';
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
return 'Improves core project intelligence and automation.';
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
function normalizeStoredUpdates(updates) {
|
|
641
|
+
if (!Array.isArray(updates)) {
|
|
642
|
+
return [];
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
return dedupeRecentUpdates(
|
|
646
|
+
updates
|
|
647
|
+
.map((update) => normalizeStoredUpdate(update))
|
|
648
|
+
.filter(Boolean)
|
|
649
|
+
);
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
function normalizeStoredUpdate(update) {
|
|
653
|
+
if (!update) {
|
|
654
|
+
return null;
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
if (update.title && update.type && update.impact) {
|
|
658
|
+
return {
|
|
659
|
+
title: update.title,
|
|
660
|
+
type: update.type,
|
|
661
|
+
impact: update.impact
|
|
662
|
+
};
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
if (update.file && update.action && isMeaningfulEvent(update)) {
|
|
666
|
+
return toStateUpdate(interpretChange(update));
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
return null;
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
function normalizeStoredHistoryEntries(entries) {
|
|
673
|
+
if (!Array.isArray(entries)) {
|
|
674
|
+
return [];
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
return dedupeHistoryEntries(
|
|
678
|
+
entries
|
|
679
|
+
.map((entry) => normalizeStoredHistoryEntry(entry))
|
|
680
|
+
.filter(Boolean)
|
|
681
|
+
);
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
function normalizeStoredHistoryEntry(entry) {
|
|
685
|
+
if (!entry) {
|
|
686
|
+
return null;
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
if (entry.title && entry.type && entry.impact) {
|
|
690
|
+
return {
|
|
691
|
+
timestamp: entry.timestamp || new Date(0).toISOString(),
|
|
692
|
+
file: normalizeProjectPath(entry.file || ''),
|
|
693
|
+
title: entry.title,
|
|
694
|
+
type: entry.type,
|
|
695
|
+
impact: entry.impact
|
|
696
|
+
};
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
if (entry.file && entry.action && isMeaningfulEvent(entry)) {
|
|
700
|
+
return interpretChange(entry);
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
return null;
|
|
704
|
+
}
|
|
246
705
|
|
|
247
|
-
|
|
248
|
-
|
|
706
|
+
function toStateUpdate(update) {
|
|
707
|
+
return {
|
|
708
|
+
title: update.title,
|
|
709
|
+
type: update.type,
|
|
710
|
+
impact: update.impact
|
|
249
711
|
};
|
|
712
|
+
}
|
|
250
713
|
|
|
251
|
-
|
|
252
|
-
|
|
714
|
+
function dedupeRecentUpdates(updates) {
|
|
715
|
+
const seenUpdates = new Set();
|
|
716
|
+
const result = [];
|
|
253
717
|
|
|
254
|
-
|
|
255
|
-
|
|
718
|
+
for (const update of updates) {
|
|
719
|
+
const key = `${update.title}::${update.type}::${update.impact}`;
|
|
720
|
+
|
|
721
|
+
if (seenUpdates.has(key)) {
|
|
722
|
+
continue;
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
seenUpdates.add(key);
|
|
726
|
+
result.push(update);
|
|
256
727
|
}
|
|
257
728
|
|
|
258
|
-
|
|
259
|
-
|
|
729
|
+
return result;
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
function dedupeHistoryEntries(entries) {
|
|
733
|
+
const seenEntries = new Set();
|
|
734
|
+
const result = [];
|
|
735
|
+
|
|
736
|
+
for (const entry of entries) {
|
|
737
|
+
const key = `${entry.title}::${entry.type}::${entry.file}`;
|
|
738
|
+
|
|
739
|
+
if (seenEntries.has(key)) {
|
|
740
|
+
continue;
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
seenEntries.add(key);
|
|
744
|
+
result.push(entry);
|
|
260
745
|
}
|
|
261
746
|
|
|
262
|
-
return
|
|
747
|
+
return result;
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
function deriveKeyFeatures(projectRoot) {
|
|
751
|
+
const features = [];
|
|
752
|
+
|
|
753
|
+
if (fs.existsSync(path.join(projectRoot, 'bin', 'cli.js'))) {
|
|
754
|
+
features.push('CLI commands for initializing, linking, starting, and updating AI context');
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
if (fs.existsSync(path.join(projectRoot, 'core', 'watcher.js'))) {
|
|
758
|
+
features.push('Noise-filtered watcher that turns file changes into meaningful project updates');
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
if (
|
|
762
|
+
fs.existsSync(path.join(projectRoot, 'server', 'server.js')) &&
|
|
763
|
+
fs.existsSync(path.join(projectRoot, 'server', 'routes.js'))
|
|
764
|
+
) {
|
|
765
|
+
features.push('Local Express server for AI-readable context endpoints');
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
if (fs.existsSync(path.join(projectRoot, 'core', 'gitSync.js'))) {
|
|
769
|
+
features.push('Optional GitHub sync for publishing public AI-readable context');
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
if (fs.existsSync(path.join(projectRoot, 'core', 'stateManager.js'))) {
|
|
773
|
+
features.push('Intelligent state engine that summarizes project evolution for AI tools');
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
return features;
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
function deriveKnownIssues(projectRoot, techStack) {
|
|
780
|
+
const knownIssues = [];
|
|
781
|
+
|
|
782
|
+
if (!hasTestIndicators(projectRoot)) {
|
|
783
|
+
knownIssues.push('No automated test suite is detected yet.');
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
if (!techStack.framework) {
|
|
787
|
+
knownIssues.push('No common application framework dependency is currently detected.');
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
return knownIssues;
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
function hasTestIndicators(projectRoot) {
|
|
794
|
+
const testPaths = [
|
|
795
|
+
'test',
|
|
796
|
+
'tests',
|
|
797
|
+
'__tests__',
|
|
798
|
+
'vitest.config.js',
|
|
799
|
+
'jest.config.js',
|
|
800
|
+
'jest.config.cjs',
|
|
801
|
+
'jest.config.mjs'
|
|
802
|
+
];
|
|
803
|
+
|
|
804
|
+
return testPaths.some((relativePath) => fs.existsSync(path.join(projectRoot, relativePath)));
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
function determineCurrentStage(keyFeatures) {
|
|
808
|
+
const hasCli = keyFeatures.some((feature) => feature.includes('CLI commands'));
|
|
809
|
+
const hasSync = keyFeatures.some((feature) => feature.includes('GitHub sync'));
|
|
810
|
+
const hasServer = keyFeatures.some((feature) => feature.includes('Express server'));
|
|
811
|
+
const hasWatcher = keyFeatures.some((feature) => feature.includes('watcher'));
|
|
812
|
+
const hasIntelligence = keyFeatures.some((feature) => feature.includes('Intelligent state engine'));
|
|
813
|
+
|
|
814
|
+
if (hasCli && hasSync && hasServer && hasWatcher && hasIntelligence) {
|
|
815
|
+
return 'Production-ready';
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
if (hasCli && hasSync) {
|
|
819
|
+
return 'Functional prototype';
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
return 'Early development';
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
function generateAiSummary(state) {
|
|
826
|
+
const language = state.tech_stack.language || 'Project';
|
|
827
|
+
const hasServer = state.key_features.some((feature) => feature.includes('Express server'));
|
|
828
|
+
const hasSync = state.key_features.some((feature) => feature.includes('GitHub sync'));
|
|
829
|
+
const clauses = ['tracks meaningful project evolution', 'generates structured AI-readable context'];
|
|
830
|
+
|
|
831
|
+
if (hasServer) {
|
|
832
|
+
clauses.push('serves project context locally through Express');
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
if (hasSync) {
|
|
836
|
+
clauses.push('can publish public AI-readable context through optional GitHub sync');
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
return `AI-powered ${language} CLI that ${clauses.join(', ')}.`;
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
function generateNextSteps(state) {
|
|
843
|
+
const nextSteps = [];
|
|
844
|
+
|
|
845
|
+
if (state.key_features.length === 0) {
|
|
846
|
+
nextSteps.push('Define core features for the AI context workflow.');
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
if (state.known_issues.includes('No automated test suite is detected yet.')) {
|
|
850
|
+
nextSteps.push('Add automated tests for the state engine, watcher, and GitHub sync flows.');
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
if (state.current_stage === 'Early development') {
|
|
854
|
+
nextSteps.push('Implement the next core workflow milestone and document how AI should use it.');
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
if (state.current_stage === 'Functional prototype') {
|
|
858
|
+
nextSteps.push('Harden the current feature set with tests and release validation.');
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
if (!state.tech_stack.framework) {
|
|
862
|
+
nextSteps.push('Document the intended framework or extend stack detection for this project.');
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
if (state.recent_updates.length === 0) {
|
|
866
|
+
nextSteps.push('Capture the first meaningful project milestone to seed AI context history.');
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
return Array.from(new Set(nextSteps)).slice(0, 4);
|
|
263
870
|
}
|
|
264
871
|
|
|
265
872
|
function createDebouncedStateUpdater(projectRoot, options) {
|
|
@@ -331,9 +938,12 @@ module.exports = {
|
|
|
331
938
|
detectProjectMetadata,
|
|
332
939
|
ensureContextDirectory,
|
|
333
940
|
getContextPaths,
|
|
941
|
+
interpretChange,
|
|
334
942
|
loadRuntimeConfig,
|
|
335
943
|
readJsonFile,
|
|
336
944
|
renderTemplate,
|
|
945
|
+
scoreEvent,
|
|
946
|
+
shouldIgnoreProjectFile,
|
|
337
947
|
updateRuntimeConfig,
|
|
338
948
|
updateProjectState,
|
|
339
949
|
writeJsonAtomic,
|
package/core/watcher.js
CHANGED
|
@@ -7,6 +7,8 @@ const { syncContextToGit } = require('./gitSync');
|
|
|
7
7
|
const {
|
|
8
8
|
createDebouncedStateUpdater,
|
|
9
9
|
loadRuntimeConfig,
|
|
10
|
+
scoreEvent,
|
|
11
|
+
shouldIgnoreProjectFile,
|
|
10
12
|
updateProjectState
|
|
11
13
|
} = require('./stateManager');
|
|
12
14
|
|
|
@@ -18,8 +20,7 @@ async function startWatcher(projectRoot, options) {
|
|
|
18
20
|
const settings = Object.assign({ logger: null }, options);
|
|
19
21
|
const logger = settings.logger;
|
|
20
22
|
const config = await loadRuntimeConfig(projectRoot);
|
|
21
|
-
const syncCallback = async () =>
|
|
22
|
-
syncContextToGit(projectRoot, config.gitSync, logger);
|
|
23
|
+
const syncCallback = async () => syncContextToGit(projectRoot, config.gitSync, logger);
|
|
23
24
|
const debouncedUpdater = createDebouncedStateUpdater(projectRoot, {
|
|
24
25
|
debounceMs: config.debounceMs,
|
|
25
26
|
logger,
|
|
@@ -27,11 +28,10 @@ async function startWatcher(projectRoot, options) {
|
|
|
27
28
|
});
|
|
28
29
|
|
|
29
30
|
const watcher = chokidar.watch(projectRoot, {
|
|
30
|
-
ignored
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
],
|
|
31
|
+
ignored(filePath) {
|
|
32
|
+
const normalizedPath = normalizeFilePath(projectRoot, filePath);
|
|
33
|
+
return shouldIgnoreProjectFile(normalizedPath);
|
|
34
|
+
},
|
|
35
35
|
ignoreInitial: true,
|
|
36
36
|
persistent: true
|
|
37
37
|
});
|
|
@@ -39,12 +39,12 @@ async function startWatcher(projectRoot, options) {
|
|
|
39
39
|
function handleEvent(action, filePath) {
|
|
40
40
|
const normalizedPath = normalizeFilePath(projectRoot, filePath);
|
|
41
41
|
|
|
42
|
-
if (!normalizedPath || normalizedPath
|
|
42
|
+
if (!normalizedPath || shouldIgnoreProjectFile(normalizedPath) || scoreEvent(normalizedPath) < 2) {
|
|
43
43
|
return;
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
if (logger) {
|
|
47
|
-
logger.
|
|
47
|
+
logger.debug(`Queued meaningful ${action} for ${normalizedPath}`);
|
|
48
48
|
}
|
|
49
49
|
|
|
50
50
|
debouncedUpdater.enqueue({
|
package/package.json
CHANGED
package/server/server.js
CHANGED
|
@@ -2,11 +2,16 @@
|
|
|
2
2
|
"project": "",
|
|
3
3
|
"version": "",
|
|
4
4
|
"last_updated": "1970-01-01T00:00:00.000Z",
|
|
5
|
-
"
|
|
6
|
-
|
|
7
|
-
"
|
|
5
|
+
"ai_summary": "",
|
|
6
|
+
"tech_stack": {
|
|
7
|
+
"language": "",
|
|
8
|
+
"framework": "",
|
|
9
|
+
"runtime": "",
|
|
10
|
+
"package_manager": ""
|
|
8
11
|
},
|
|
12
|
+
"current_stage": "",
|
|
9
13
|
"recent_updates": [],
|
|
10
|
-
"
|
|
14
|
+
"key_features": [],
|
|
15
|
+
"known_issues": [],
|
|
11
16
|
"next_steps": []
|
|
12
17
|
}
|