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 CHANGED
@@ -1,207 +1,506 @@
1
1
  #!/usr/bin/env node
2
2
  'use strict';
3
3
 
4
- const path = require('path');
5
- const readline = require('readline/promises');
6
- const { stdin, stdout } = require('process');
7
- const { initProject } = require('../core/init');
8
- const { startWatcher } = require('../core/watcher');
9
- const {
10
- loadRuntimeConfig,
11
- updateProjectState,
12
- updateRuntimeConfig
13
- } = require('../core/stateManager');
14
- const { linkGithubRepository, syncContextToGit } = require('../core/gitSync');
15
- const { startServer } = require('../server/server');
16
- const { createLogger } = require('../utils/logger');
17
-
18
- async function run() {
19
- const logger = createLogger();
20
- const projectRoot = process.cwd();
21
- const command = process.argv[2];
22
-
23
- if (!command || command === '--help' || command === '-h') {
24
- printHelp();
25
- return;
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
- if (command === '--version' || command === '-v') {
29
- // eslint-disable-next-line global-require
30
- const packageJson = require(path.join(__dirname, '..', 'package.json'));
31
- console.log(packageJson.version);
32
- return;
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
- if (command === 'init') {
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
- });
43
- return;
44
- }
132
+ const { getRemoteOriginUrl } = gs();
133
+ const remoteUrl = await getRemoteOriginUrl(projectRoot);
134
+ if (!remoteUrl) return; // no remote, skip silently
45
135
 
46
- if (command === 'update') {
47
- await initProject(projectRoot, { logger });
48
- const config = await loadRuntimeConfig(projectRoot);
49
- await updateProjectState(
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
- if (command === 'link-github') {
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');
141
+ // ─────────────────────────────────────────────────────────────────
142
+ // START — the main command, does everything automatically
143
+ // ─────────────────────────────────────────────────────────────────
72
144
 
73
- const shouldContinue = await promptYesNo('[aibridge] Continue? (y/n) ');
145
+ async function cmdStart() {
146
+ // ── Step 1: Auto-init if needed ────────────────────────────────
147
+ if (!contextExists()) {
148
+ await autoInit();
149
+ }
74
150
 
75
- if (!shouldContinue) {
76
- logger.info('GitHub linking cancelled. Public sync remains unchanged.');
77
- return;
78
- }
151
+ const { startWatcher } = require('../core/watcher');
152
+ const { startServer } = require('../server/server');
153
+ const config = await sm().loadRuntimeConfig(projectRoot);
79
154
 
80
- const repoUrl = process.argv[3] || (await promptForRepoUrl());
155
+ // ── Step 2: Check git sync ──────────────────────────────────────
156
+ await autoGitSync(config);
81
157
 
82
- if (!repoUrl) {
83
- logger.error('A GitHub repository URL is required.');
84
- process.exitCode = 1;
85
- return;
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
- const nextConfig = await updateRuntimeConfig(projectRoot, {
89
- gitSync: {
90
- enabled: true,
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
- await syncContextToGit(projectRoot, nextConfig.gitSync, logger);
103
- return;
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
- if (command === 'start') {
107
- await initProject(projectRoot, { logger });
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...');
111
- const serverHandle = await startServer({
112
- projectRoot,
113
- port,
114
- logger
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
- }
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
- const watcherHandle = await startWatcher(projectRoot, { logger });
127
-
128
- const shutdown = async (signal) => {
129
- logger.info(`Received ${signal}; shutting down cleanly.`);
130
- await watcherHandle.close();
131
- await serverHandle.close();
132
- process.exit(0);
133
- };
134
-
135
- process.on('SIGINT', () => {
136
- shutdown('SIGINT').catch((error) => {
137
- logger.error(`Shutdown failed: ${error.message}`);
138
- process.exit(1);
139
- });
140
- });
141
-
142
- process.on('SIGTERM', () => {
143
- shutdown('SIGTERM').catch((error) => {
144
- logger.error(`Shutdown failed: ${error.message}`);
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
- logger.error(`Unknown command: ${command}`);
153
- printHelp();
154
- process.exitCode = 1;
155
- } catch (error) {
156
- logger.error(error.stack || error.message);
157
- process.exitCode = 1;
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
- function printHelp() {
162
- console.log(`aibridge-context
300
+ // ─────────────────────────────────────────────────────────────────
301
+ // ERROR / FIX
302
+ // ─────────────────────────────────────────────────────────────────
163
303
 
164
- Commands:
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
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
- Description:
171
- This tool creates an AI-readable version of your project and can expose it via a public URL for AI tools.
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
- async function promptForRepoUrl() {
176
- const rl = readline.createInterface({
177
- input: stdin,
178
- output: stdout
179
- });
340
+ // ─────────────────────────────────────────────────────────────────
341
+ // NOTE / FOCUS / DECIDE / QUESTION
342
+ // ─────────────────────────────────────────────────────────────────
180
343
 
181
- try {
182
- const response = await rl.question('[aibridge] GitHub repository URL: ');
183
- return response.trim();
184
- } finally {
185
- rl.close();
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
- async function promptYesNo(question) {
190
- if (!stdin.isTTY || !stdout.isTTY) {
191
- return false;
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
- const rl = readline.createInterface({
195
- input: stdin,
196
- output: stdout
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
- try {
200
- const response = await rl.question(question);
201
- return response.trim().toLowerCase() === 'y';
202
- } finally {
203
- rl.close();
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
- run();
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
+ })();