aibridge-context 1.5.2 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/cli.js +469 -170
- package/core/briefingGenerator.js +368 -0
- package/core/codeDiff.js +304 -0
- package/core/fileSnapshot.js +178 -0
- package/core/stateManager.js +803 -1369
- package/core/watcher.js +78 -49
- package/index.js +23 -9
- package/package.json +7 -3
- package/server/routes.js +30 -34
package/bin/cli.js
CHANGED
|
@@ -1,207 +1,506 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
'use strict';
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
4
|
+
/**
|
|
5
|
+
* cli.js — aibridge-context
|
|
6
|
+
*
|
|
7
|
+
* One command to rule them all:
|
|
8
|
+
* npx aibridge-context start
|
|
9
|
+
*
|
|
10
|
+
* That single command:
|
|
11
|
+
* 1. Auto-inits .ai-context/ if it doesn't exist yet
|
|
12
|
+
* 2. Scans the whole project and builds state.json, changelog.json,
|
|
13
|
+
* brain.txt, context.md, and briefing.md
|
|
14
|
+
* 3. Starts the file watcher (real diffs on every save)
|
|
15
|
+
* 4. Starts the local HTTP server (port 3333)
|
|
16
|
+
* 5. Auto-syncs to GitHub if a remote is already configured
|
|
17
|
+
* 6. Regenerates briefing.md on every file change automatically
|
|
18
|
+
*
|
|
19
|
+
* Other commands:
|
|
20
|
+
* error <msg> Record an active error
|
|
21
|
+
* fix <msg> Mark an error resolved
|
|
22
|
+
* note <text> Append a session note
|
|
23
|
+
* focus <text> Set current_focus
|
|
24
|
+
* decide <text> Record an architectural decision
|
|
25
|
+
* question <text> Add an open question
|
|
26
|
+
* status Print a summary
|
|
27
|
+
* briefing Print the path to briefing.md
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
const path = require('path');
|
|
31
|
+
const fs = require('fs');
|
|
32
|
+
const fsp = require('fs/promises');
|
|
33
|
+
const readline = require('readline');
|
|
34
|
+
|
|
35
|
+
const projectRoot = process.cwd();
|
|
36
|
+
|
|
37
|
+
function sm() { return require('../core/stateManager'); }
|
|
38
|
+
function initer(){ return require('../core/init'); }
|
|
39
|
+
function gs() { return require('../core/gitSync'); }
|
|
40
|
+
|
|
41
|
+
// ─────────────────────────────────────────────────────────────────
|
|
42
|
+
// Argument parsing
|
|
43
|
+
// ─────────────────────────────────────────────────────────────────
|
|
44
|
+
|
|
45
|
+
function parseArgs(argv) {
|
|
46
|
+
const args = argv.slice(2);
|
|
47
|
+
const cmd = args[0] || 'help';
|
|
48
|
+
const flags = {};
|
|
49
|
+
const pos = [];
|
|
50
|
+
for (let i = 1; i < args.length; i++) {
|
|
51
|
+
if (args[i].startsWith('--')) {
|
|
52
|
+
const key = args[i].slice(2);
|
|
53
|
+
flags[key] = (args[i+1] && !args[i+1].startsWith('--')) ? args[++i] : true;
|
|
54
|
+
} else {
|
|
55
|
+
pos.push(args[i]);
|
|
56
|
+
}
|
|
26
57
|
}
|
|
58
|
+
return { cmd, positional: pos, flags };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// ─────────────────────────────────────────────────────────────────
|
|
62
|
+
// Logger
|
|
63
|
+
// ─────────────────────────────────────────────────────────────────
|
|
64
|
+
|
|
65
|
+
const logger = require('../utils/logger').createLogger({ level: 'info' });
|
|
27
66
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
67
|
+
// ─────────────────────────────────────────────────────────────────
|
|
68
|
+
// Helpers
|
|
69
|
+
// ─────────────────────────────────────────────────────────────────
|
|
70
|
+
|
|
71
|
+
async function readState() {
|
|
72
|
+
const { getContextPaths, readJsonFile } = sm();
|
|
73
|
+
return readJsonFile(getContextPaths(projectRoot).stateFile, null);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async function patchState(patchFn) {
|
|
77
|
+
const { getContextPaths, readJsonFile, writeJsonAtomic } = sm();
|
|
78
|
+
const paths = getContextPaths(projectRoot);
|
|
79
|
+
const state = await readJsonFile(paths.stateFile, null);
|
|
80
|
+
if (!state) {
|
|
81
|
+
logger.error('No .ai-context/state.json found. Run: npx aibridge-context start');
|
|
82
|
+
process.exit(1);
|
|
33
83
|
}
|
|
84
|
+
const next = patchFn(state);
|
|
85
|
+
await writeJsonAtomic(paths.stateFile, next);
|
|
86
|
+
// Regenerate briefing after any patch
|
|
87
|
+
try {
|
|
88
|
+
const { generateBriefing } = require('../core/briefingGenerator');
|
|
89
|
+
const { writeTextAtomic } = sm();
|
|
90
|
+
await writeTextAtomic(paths.briefingFile, generateBriefing(next, projectRoot));
|
|
91
|
+
} catch (_) {}
|
|
92
|
+
return next;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function prompt(question) {
|
|
96
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
97
|
+
return new Promise((resolve) => { rl.question(question, (a) => { rl.close(); resolve(a.trim()); }); });
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function contextExists() {
|
|
101
|
+
const { getContextPaths } = sm();
|
|
102
|
+
return fs.existsSync(getContextPaths(projectRoot).stateFile);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// ─────────────────────────────────────────────────────────────────
|
|
106
|
+
// Auto-init (runs silently inside start if .ai-context missing)
|
|
107
|
+
// ─────────────────────────────────────────────────────────────────
|
|
108
|
+
|
|
109
|
+
async function autoInit() {
|
|
110
|
+
logger.info('No .ai-context/ found — initialising automatically…');
|
|
111
|
+
const { initProject } = initer();
|
|
112
|
+
const result = await initProject(projectRoot, {
|
|
113
|
+
logger,
|
|
114
|
+
force: false,
|
|
115
|
+
requestPublicSyncConsent: false // silent auto-init, no public sync prompt
|
|
116
|
+
});
|
|
117
|
+
logger.info(`✅ Auto-initialised: ${result.metadata.project} v${result.metadata.version}`);
|
|
118
|
+
logger.info(` Stack: ${result.metadata.stackLabel}`);
|
|
119
|
+
return result;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// ─────────────────────────────────────────────────────────────────
|
|
123
|
+
// Auto GitHub sync check
|
|
124
|
+
// ─────────────────────────────────────────────────────────────────
|
|
125
|
+
|
|
126
|
+
async function autoGitSync(config) {
|
|
127
|
+
// If sync already enabled, nothing to do — gitSync.js handles it
|
|
128
|
+
if (config.gitSync && config.gitSync.enabled) return;
|
|
34
129
|
|
|
130
|
+
// Check if a git remote exists and offer to enable
|
|
35
131
|
try {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
requestPublicSyncConsent: true,
|
|
40
|
-
promptForPublicSyncConsent: () =>
|
|
41
|
-
promptYesNo('[aibridge] Do you want to enable GitHub sync and public AI access? (y/n) ')
|
|
42
|
-
});
|
|
43
|
-
return;
|
|
44
|
-
}
|
|
132
|
+
const { getRemoteOriginUrl } = gs();
|
|
133
|
+
const remoteUrl = await getRemoteOriginUrl(projectRoot);
|
|
134
|
+
if (!remoteUrl) return; // no remote, skip silently
|
|
45
135
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
projectRoot,
|
|
51
|
-
{
|
|
52
|
-
timestamp: new Date().toISOString(),
|
|
53
|
-
action: 'manual_update',
|
|
54
|
-
file: '.'
|
|
55
|
-
},
|
|
56
|
-
{
|
|
57
|
-
logger,
|
|
58
|
-
syncCallback: async () => syncContextToGit(projectRoot, config.gitSync, logger)
|
|
59
|
-
}
|
|
60
|
-
);
|
|
61
|
-
logger.info('Manual AI context update completed.');
|
|
62
|
-
return;
|
|
63
|
-
}
|
|
136
|
+
logger.info(`💡 Git remote found: ${remoteUrl}`);
|
|
137
|
+
logger.info(' Run: npx aibridge-context link-github to enable public AI sync');
|
|
138
|
+
} catch (_) {}
|
|
139
|
+
}
|
|
64
140
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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');
|
|
141
|
+
// ─────────────────────────────────────────────────────────────────
|
|
142
|
+
// START — the main command, does everything automatically
|
|
143
|
+
// ─────────────────────────────────────────────────────────────────
|
|
72
144
|
|
|
73
|
-
|
|
145
|
+
async function cmdStart() {
|
|
146
|
+
// ── Step 1: Auto-init if needed ────────────────────────────────
|
|
147
|
+
if (!contextExists()) {
|
|
148
|
+
await autoInit();
|
|
149
|
+
}
|
|
74
150
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
}
|
|
151
|
+
const { startWatcher } = require('../core/watcher');
|
|
152
|
+
const { startServer } = require('../server/server');
|
|
153
|
+
const config = await sm().loadRuntimeConfig(projectRoot);
|
|
79
154
|
|
|
80
|
-
|
|
155
|
+
// ── Step 2: Check git sync ──────────────────────────────────────
|
|
156
|
+
await autoGitSync(config);
|
|
81
157
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
158
|
+
// ── Step 3: Start watcher + server in parallel ──────────────────
|
|
159
|
+
logger.info('Starting watcher and server…');
|
|
160
|
+
const [watcherHandle, serverHandle] = await Promise.all([
|
|
161
|
+
startWatcher(projectRoot, { logger }),
|
|
162
|
+
startServer({ port: config.port, projectRoot, logger })
|
|
163
|
+
]);
|
|
87
164
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
push: true,
|
|
92
|
-
repoUrl: repoUrl.trim()
|
|
93
|
-
}
|
|
94
|
-
});
|
|
95
|
-
const linkResult = await linkGithubRepository(projectRoot, repoUrl.trim(), logger);
|
|
96
|
-
|
|
97
|
-
if (!linkResult.ok) {
|
|
98
|
-
process.exitCode = 1;
|
|
99
|
-
return;
|
|
100
|
-
}
|
|
165
|
+
// ── Step 4: Print status ────────────────────────────────────────
|
|
166
|
+
const { getContextPaths } = sm();
|
|
167
|
+
const paths = getContextPaths(projectRoot);
|
|
101
168
|
|
|
102
|
-
|
|
103
|
-
|
|
169
|
+
logger.info('');
|
|
170
|
+
logger.info('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
171
|
+
logger.info(' ✅ aibridge-context running');
|
|
172
|
+
logger.info('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
173
|
+
logger.info('');
|
|
174
|
+
logger.info(' 📄 AI Briefing (paste into any AI):');
|
|
175
|
+
logger.info(` ${paths.briefingFile}`);
|
|
176
|
+
logger.info(` http://localhost:${config.port}/briefing.md`);
|
|
177
|
+
logger.info('');
|
|
178
|
+
logger.info(' 🌐 All endpoints:');
|
|
179
|
+
logger.info(` http://localhost:${config.port}/briefing.md ← start here`);
|
|
180
|
+
logger.info(` http://localhost:${config.port}/state.json`);
|
|
181
|
+
logger.info(` http://localhost:${config.port}/changelog.json`);
|
|
182
|
+
logger.info(` http://localhost:${config.port}/brain.txt`);
|
|
183
|
+
logger.info(` http://localhost:${config.port}/context.md`);
|
|
184
|
+
logger.info('');
|
|
185
|
+
logger.info(' 🔄 Watching for file changes — briefing.md auto-updates on every save');
|
|
186
|
+
if (config.gitSync && config.gitSync.enabled) {
|
|
187
|
+
logger.info(' ☁️ GitHub sync enabled — changes push automatically');
|
|
188
|
+
}
|
|
189
|
+
logger.info('');
|
|
190
|
+
logger.info(' Press Ctrl+C to stop');
|
|
191
|
+
logger.info('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
192
|
+
|
|
193
|
+
// ── Step 5: Graceful shutdown ───────────────────────────────────
|
|
194
|
+
async function shutdown() {
|
|
195
|
+
logger.info('\nShutting down…');
|
|
196
|
+
await watcherHandle.close();
|
|
197
|
+
await serverHandle.close();
|
|
198
|
+
process.exit(0);
|
|
199
|
+
}
|
|
200
|
+
process.on('SIGINT', shutdown);
|
|
201
|
+
process.on('SIGTERM', shutdown);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// ─────────────────────────────────────────────────────────────────
|
|
205
|
+
// INIT (explicit, with GitHub consent prompt)
|
|
206
|
+
// ─────────────────────────────────────────────────────────────────
|
|
207
|
+
|
|
208
|
+
async function cmdInit() {
|
|
209
|
+
const { initProject } = initer();
|
|
210
|
+
const result = await initProject(projectRoot, {
|
|
211
|
+
logger,
|
|
212
|
+
force: false,
|
|
213
|
+
requestPublicSyncConsent: true,
|
|
214
|
+
promptForPublicSyncConsent: async () => {
|
|
215
|
+
const answer = await prompt('Enable GitHub sync? This makes your context PUBLIC. (y/N): ');
|
|
216
|
+
return answer.toLowerCase() === 'y';
|
|
104
217
|
}
|
|
218
|
+
});
|
|
219
|
+
const { getContextPaths } = sm();
|
|
220
|
+
const paths = getContextPaths(projectRoot);
|
|
221
|
+
logger.info('');
|
|
222
|
+
logger.info(`✅ Initialised: ${result.metadata.project} v${result.metadata.version}`);
|
|
223
|
+
logger.info(` Stack: ${result.metadata.stackLabel}`);
|
|
224
|
+
logger.info(` Briefing: ${paths.briefingFile}`);
|
|
225
|
+
logger.info('');
|
|
226
|
+
logger.info('Next step: npx aibridge-context start');
|
|
227
|
+
}
|
|
105
228
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
logger.info('GitHub sync is DISABLED');
|
|
123
|
-
logger.info('Data is only available locally');
|
|
124
|
-
}
|
|
229
|
+
// ─────────────────────────────────────────────────────────────────
|
|
230
|
+
// UPDATE
|
|
231
|
+
// ─────────────────────────────────────────────────────────────────
|
|
232
|
+
|
|
233
|
+
async function cmdUpdate() {
|
|
234
|
+
const { updateProjectState } = sm();
|
|
235
|
+
logger.info('Refreshing AI context…');
|
|
236
|
+
const state = await updateProjectState(
|
|
237
|
+
projectRoot,
|
|
238
|
+
{ timestamp: new Date().toISOString(), action: 'manual_update', file: '.' },
|
|
239
|
+
{ logger }
|
|
240
|
+
);
|
|
241
|
+
const { getContextPaths } = sm();
|
|
242
|
+
logger.info(`✅ Done — ${state.file_list.length} files tracked`);
|
|
243
|
+
logger.info(` Briefing updated: ${getContextPaths(projectRoot).briefingFile}`);
|
|
244
|
+
}
|
|
125
245
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
process.exit(1);
|
|
146
|
-
});
|
|
147
|
-
});
|
|
148
|
-
|
|
149
|
-
return;
|
|
246
|
+
// ─────────────────────────────────────────────────────────────────
|
|
247
|
+
// LINK GITHUB
|
|
248
|
+
// ─────────────────────────────────────────────────────────────────
|
|
249
|
+
|
|
250
|
+
async function cmdLinkGithub() {
|
|
251
|
+
const repoUrl = await prompt('GitHub repository URL (e.g. https://github.com/user/repo): ');
|
|
252
|
+
if (!repoUrl) { logger.error('No URL provided.'); process.exit(1); }
|
|
253
|
+
|
|
254
|
+
logger.info('Linking GitHub repository…');
|
|
255
|
+
const { linkGithubRepository } = gs();
|
|
256
|
+
const result = await linkGithubRepository(projectRoot, repoUrl, logger);
|
|
257
|
+
|
|
258
|
+
if (result.ok) {
|
|
259
|
+
await sm().updateRuntimeConfig(projectRoot, { gitSync: { enabled: true, repoUrl } });
|
|
260
|
+
logger.info('✅ Linked and sync enabled');
|
|
261
|
+
if (result.urls) {
|
|
262
|
+
logger.info('');
|
|
263
|
+
logger.info(' Share this with any AI:');
|
|
264
|
+
logger.info(` "${result.urls.stateUrl}"`);
|
|
150
265
|
}
|
|
266
|
+
} else {
|
|
267
|
+
logger.error(`Failed: ${result.error}`);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// ─────────────────────────────────────────────────────────────────
|
|
272
|
+
// BRIEFING — just print the path
|
|
273
|
+
// ─────────────────────────────────────────────────────────────────
|
|
151
274
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
logger.error(
|
|
157
|
-
process.
|
|
275
|
+
async function cmdBriefing() {
|
|
276
|
+
const { getContextPaths } = sm();
|
|
277
|
+
const paths = getContextPaths(projectRoot);
|
|
278
|
+
if (!fs.existsSync(paths.briefingFile)) {
|
|
279
|
+
logger.error('briefing.md not found. Run: npx aibridge-context start');
|
|
280
|
+
process.exit(1);
|
|
281
|
+
}
|
|
282
|
+
console.log('');
|
|
283
|
+
console.log('📄 AI Briefing file:');
|
|
284
|
+
console.log(` ${paths.briefingFile}`);
|
|
285
|
+
console.log('');
|
|
286
|
+
console.log(' Open this file and paste its contents into any AI assistant.');
|
|
287
|
+
console.log(' It contains everything the AI needs to know about your project.');
|
|
288
|
+
console.log('');
|
|
289
|
+
if (fs.existsSync(paths.stateFile)) {
|
|
290
|
+
try {
|
|
291
|
+
const state = JSON.parse(fs.readFileSync(paths.stateFile, 'utf8'));
|
|
292
|
+
const errs = (state.issue_tracker || {}).active_errors || [];
|
|
293
|
+
if (errs.length > 0) {
|
|
294
|
+
console.log(` ⚠️ ${errs.length} active error(s) recorded. AI will see these first.`);
|
|
295
|
+
}
|
|
296
|
+
} catch (_) {}
|
|
158
297
|
}
|
|
159
298
|
}
|
|
160
299
|
|
|
161
|
-
|
|
162
|
-
|
|
300
|
+
// ─────────────────────────────────────────────────────────────────
|
|
301
|
+
// ERROR / FIX
|
|
302
|
+
// ─────────────────────────────────────────────────────────────────
|
|
163
303
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
304
|
+
async function cmdError(positional, flags) {
|
|
305
|
+
const message = positional.join(' ') || flags.message;
|
|
306
|
+
if (!message) {
|
|
307
|
+
logger.error('Usage: npx aibridge-context error "<message>" [--file <path>] [--stack "<trace>"]');
|
|
308
|
+
process.exit(1);
|
|
309
|
+
}
|
|
310
|
+
await sm().updateProjectState(projectRoot, [{
|
|
311
|
+
type: 'error',
|
|
312
|
+
timestamp: new Date().toISOString(),
|
|
313
|
+
message,
|
|
314
|
+
file: flags.file || '',
|
|
315
|
+
stack: flags.stack || ''
|
|
316
|
+
}], { logger });
|
|
317
|
+
logger.info(`🔴 Error recorded: "${message}"`);
|
|
318
|
+
logger.info(' Run "npx aibridge-context status" to see all active errors.');
|
|
319
|
+
}
|
|
169
320
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
321
|
+
async function cmdFix(positional, flags) {
|
|
322
|
+
const message = positional.join(' ') || flags.message;
|
|
323
|
+
const resolution = flags.resolution || flags.r || 'Fixed';
|
|
324
|
+
if (!message && !flags.id) {
|
|
325
|
+
logger.error('Usage: npx aibridge-context fix "<message>" --resolution "<what fixed it>"');
|
|
326
|
+
process.exit(1);
|
|
327
|
+
}
|
|
328
|
+
await sm().updateProjectState(projectRoot, [{
|
|
329
|
+
type: 'resolve',
|
|
330
|
+
timestamp: new Date().toISOString(),
|
|
331
|
+
message,
|
|
332
|
+
errorId: flags.id || undefined,
|
|
333
|
+
file: flags.file || '',
|
|
334
|
+
resolution
|
|
335
|
+
}], { logger });
|
|
336
|
+
logger.info(`✅ Resolved: "${message}"`);
|
|
337
|
+
if (resolution !== 'Fixed') logger.info(` Fix: ${resolution}`);
|
|
173
338
|
}
|
|
174
339
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
output: stdout
|
|
179
|
-
});
|
|
340
|
+
// ─────────────────────────────────────────────────────────────────
|
|
341
|
+
// NOTE / FOCUS / DECIDE / QUESTION
|
|
342
|
+
// ─────────────────────────────────────────────────────────────────
|
|
180
343
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
}
|
|
344
|
+
async function cmdNote(positional) {
|
|
345
|
+
const text = positional.join(' ');
|
|
346
|
+
if (!text) { logger.error('Usage: npx aibridge-context note "<text>"'); process.exit(1); }
|
|
347
|
+
await patchState((s) => Object.assign({}, s, {
|
|
348
|
+
session_notes: [{ timestamp: new Date().toISOString(), note: text }, ...(s.session_notes||[])].slice(0,50)
|
|
349
|
+
}));
|
|
350
|
+
logger.info(`📝 Note saved: "${text}"`);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
async function cmdFocus(positional) {
|
|
354
|
+
const text = positional.join(' ');
|
|
355
|
+
if (!text) { logger.error('Usage: npx aibridge-context focus "<text>"'); process.exit(1); }
|
|
356
|
+
await patchState((s) => Object.assign({}, s, { current_focus: text }));
|
|
357
|
+
logger.info(`🎯 Focus: "${text}"`);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
async function cmdDecide(positional) {
|
|
361
|
+
const text = positional.join(' ');
|
|
362
|
+
if (!text) { logger.error('Usage: npx aibridge-context decide "<text>"'); process.exit(1); }
|
|
363
|
+
await patchState((s) => Object.assign({}, s, {
|
|
364
|
+
decisions_made: [{ timestamp: new Date().toISOString(), decision: text }, ...(s.decisions_made||[])].slice(0,50)
|
|
365
|
+
}));
|
|
366
|
+
logger.info(`📌 Decision: "${text}"`);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
async function cmdQuestion(positional) {
|
|
370
|
+
const text = positional.join(' ');
|
|
371
|
+
if (!text) { logger.error('Usage: npx aibridge-context question "<text>"'); process.exit(1); }
|
|
372
|
+
await patchState((s) => Object.assign({}, s, {
|
|
373
|
+
open_questions: [{ timestamp: new Date().toISOString(), question: text }, ...(s.open_questions||[])].slice(0,30)
|
|
374
|
+
}));
|
|
375
|
+
logger.info(`❓ Question: "${text}"`);
|
|
187
376
|
}
|
|
188
377
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
378
|
+
// ─────────────────────────────────────────────────────────────────
|
|
379
|
+
// STATUS
|
|
380
|
+
// ─────────────────────────────────────────────────────────────────
|
|
381
|
+
|
|
382
|
+
async function cmdStatus() {
|
|
383
|
+
const state = await readState();
|
|
384
|
+
if (!state) {
|
|
385
|
+
logger.error('No .ai-context/state.json found. Run: npx aibridge-context start');
|
|
386
|
+
process.exit(1);
|
|
192
387
|
}
|
|
388
|
+
const it = state.issue_tracker || {};
|
|
389
|
+
const active = (it.active_errors || []).length;
|
|
390
|
+
const fixed = (it.resolved_issues || []).length;
|
|
391
|
+
const { getContextPaths } = sm();
|
|
193
392
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
393
|
+
console.log('');
|
|
394
|
+
console.log(`📦 ${state.project} v${state.version} [${state.current_stage}]`);
|
|
395
|
+
console.log(` ${state.ai_summary}`);
|
|
396
|
+
console.log('');
|
|
397
|
+
console.log(`🗂 Files tracked: ${(state.file_list||[]).length}`);
|
|
398
|
+
console.log(`🔀 Code changes: ${(state.code_changes||[]).length}`);
|
|
399
|
+
console.log(`🛤 API routes: ${(state.api_routes||[]).length}`);
|
|
400
|
+
console.log(`📦 Dependencies: ${Object.keys((state.dependencies||{}).production||{}).length} prod`);
|
|
401
|
+
console.log('');
|
|
402
|
+
console.log(`🔴 Active errors: ${active}`);
|
|
403
|
+
for (const e of (it.active_errors||[]).slice(0,5)) {
|
|
404
|
+
console.log(` • [${e.id}] ${e.message}${e.file ? ' ('+e.file+')' : ''}`);
|
|
405
|
+
}
|
|
406
|
+
console.log(`✅ Resolved issues: ${fixed}`);
|
|
407
|
+
console.log('');
|
|
408
|
+
if (state.current_focus) console.log(`🎯 Focus: ${state.current_focus}`);
|
|
409
|
+
if (state.working_branch) console.log(`🌿 Branch: ${state.working_branch}`);
|
|
198
410
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
411
|
+
const qs = state.open_questions || [];
|
|
412
|
+
if (qs.length) {
|
|
413
|
+
console.log(`❓ Open questions:`);
|
|
414
|
+
for (const q of qs.slice(0,3)) console.log(` • ${typeof q==='string'?q:q.question}`);
|
|
415
|
+
}
|
|
416
|
+
const recent = state.recent_updates || [];
|
|
417
|
+
if (recent.length) {
|
|
418
|
+
console.log('');
|
|
419
|
+
console.log(`📝 Recent changes:`);
|
|
420
|
+
for (const u of recent.slice(0,4)) {
|
|
421
|
+
console.log(` • ${u.summary}`);
|
|
422
|
+
for (const s of (u.signals||[]).slice(0,2)) console.log(` ${s}`);
|
|
423
|
+
}
|
|
204
424
|
}
|
|
425
|
+
console.log('');
|
|
426
|
+
console.log(`📄 Briefing: ${getContextPaths(projectRoot).briefingFile}`);
|
|
427
|
+
console.log('');
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// ─────────────────────────────────────────────────────────────────
|
|
431
|
+
// HELP
|
|
432
|
+
// ─────────────────────────────────────────────────────────────────
|
|
433
|
+
|
|
434
|
+
function printHelp() {
|
|
435
|
+
console.log(`
|
|
436
|
+
aibridge-context — AI context bridge
|
|
437
|
+
|
|
438
|
+
USAGE
|
|
439
|
+
npx aibridge-context <command>
|
|
440
|
+
|
|
441
|
+
MAIN COMMAND (does everything automatically)
|
|
442
|
+
start Auto-init if needed, start watcher + server,
|
|
443
|
+
generate briefing.md, watch for changes
|
|
444
|
+
|
|
445
|
+
OTHER COMMANDS
|
|
446
|
+
init Explicit init with GitHub sync prompt
|
|
447
|
+
update Manual context refresh
|
|
448
|
+
link-github Link GitHub remote, enable public sync
|
|
449
|
+
briefing Show path to the AI briefing file
|
|
450
|
+
status Summary of current project state
|
|
451
|
+
|
|
452
|
+
ISSUE TRACKING
|
|
453
|
+
error "<msg>" Record an active error
|
|
454
|
+
--file <path>
|
|
455
|
+
--stack "<trace>"
|
|
456
|
+
fix "<msg>" Resolve a recorded error
|
|
457
|
+
--resolution "<what fixed it>"
|
|
458
|
+
|
|
459
|
+
WORKING CONTEXT
|
|
460
|
+
focus "<text>" Set what you are working on right now
|
|
461
|
+
decide "<text>" Record an architectural decision
|
|
462
|
+
question "<text>" Add an open question
|
|
463
|
+
note "<text>" Append a session note
|
|
464
|
+
|
|
465
|
+
HOW TO USE WITH AN AI
|
|
466
|
+
1. Run: npx aibridge-context start
|
|
467
|
+
2. Open: .ai-context/briefing.md
|
|
468
|
+
3. Copy the entire file and paste it into any AI chat
|
|
469
|
+
4. The AI now knows everything about your project
|
|
470
|
+
`);
|
|
205
471
|
}
|
|
206
472
|
|
|
207
|
-
|
|
473
|
+
// ─────────────────────────────────────────────────────────────────
|
|
474
|
+
// Entry point
|
|
475
|
+
// ─────────────────────────────────────────────────────────────────
|
|
476
|
+
|
|
477
|
+
(async () => {
|
|
478
|
+
const { cmd, positional, flags } = parseArgs(process.argv);
|
|
479
|
+
try {
|
|
480
|
+
switch (cmd) {
|
|
481
|
+
case 'start': await cmdStart(); break;
|
|
482
|
+
case 'init': await cmdInit(); break;
|
|
483
|
+
case 'update': await cmdUpdate(); break;
|
|
484
|
+
case 'link-github': await cmdLinkGithub(); break;
|
|
485
|
+
case 'briefing': await cmdBriefing(); break;
|
|
486
|
+
case 'status': await cmdStatus(); break;
|
|
487
|
+
case 'error': await cmdError(positional, flags); break;
|
|
488
|
+
case 'fix': await cmdFix(positional, flags); break;
|
|
489
|
+
case 'note': await cmdNote(positional); break;
|
|
490
|
+
case 'focus': await cmdFocus(positional); break;
|
|
491
|
+
case 'decide': await cmdDecide(positional); break;
|
|
492
|
+
case 'question': await cmdQuestion(positional); break;
|
|
493
|
+
case 'help':
|
|
494
|
+
case '--help':
|
|
495
|
+
case '-h': printHelp(); break;
|
|
496
|
+
default:
|
|
497
|
+
logger.error(`Unknown command: "${cmd}"`);
|
|
498
|
+
printHelp();
|
|
499
|
+
process.exit(1);
|
|
500
|
+
}
|
|
501
|
+
} catch (err) {
|
|
502
|
+
logger.error(`Failed: ${err.message}`);
|
|
503
|
+
if (process.env.DEBUG) console.error(err.stack);
|
|
504
|
+
process.exit(1);
|
|
505
|
+
}
|
|
506
|
+
})();
|