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/core/watcher.js CHANGED
@@ -1,9 +1,10 @@
1
1
  'use strict';
2
2
 
3
- const path = require('path');
3
+ const path = require('path');
4
4
  const chokidar = require('chokidar');
5
5
 
6
- const { syncContextToGit } = require('./gitSync');
6
+ const { syncContextToGit } = require('./gitSync');
7
+ const { captureSnapshot, deleteSnapshot } = require('./fileSnapshot');
7
8
  const {
8
9
  createDebouncedStateUpdater,
9
10
  loadRuntimeConfig,
@@ -12,82 +13,110 @@ const {
12
13
  updateProjectState
13
14
  } = require('./stateManager');
14
15
 
15
- function normalizeFilePath(projectRoot, filePath) {
16
- return path.relative(projectRoot, filePath).split(path.sep).join('/');
16
+ function normPath(root, filePath) {
17
+ return path.relative(root, filePath).split(path.sep).join('/');
17
18
  }
18
19
 
19
20
  async function startWatcher(projectRoot, options) {
20
- const settings = Object.assign({ logger: null }, options);
21
- const logger = settings.logger;
22
- const config = await loadRuntimeConfig(projectRoot);
23
- const syncCallback = async () => syncContextToGit(projectRoot, config.gitSync, logger);
24
- const debouncedUpdater = createDebouncedStateUpdater(projectRoot, {
25
- debounceMs: config.debounceMs,
21
+ const settings = Object.assign({ logger: null }, options);
22
+ const logger = settings.logger;
23
+ const config = await loadRuntimeConfig(projectRoot);
24
+ const syncCb = async () => syncContextToGit(projectRoot, config.gitSync, logger);
25
+ const debounced = createDebouncedStateUpdater(projectRoot, {
26
+ debounceMs: config.debounceMs,
26
27
  logger,
27
- syncCallback
28
+ syncCallback: syncCb
28
29
  });
29
30
 
30
31
  const watcher = chokidar.watch(projectRoot, {
31
32
  ignored(filePath) {
32
- const normalizedPath = normalizeFilePath(projectRoot, filePath);
33
- return shouldIgnoreProjectFile(normalizedPath);
33
+ const rel = normPath(projectRoot, filePath);
34
+ return shouldIgnoreProjectFile(rel);
34
35
  },
35
36
  ignoreInitial: true,
36
- persistent: true
37
+ persistent: true,
38
+ awaitWriteFinish: { stabilityThreshold: 80, pollInterval: 30 }
37
39
  });
38
40
 
39
- function handleEvent(action, filePath) {
40
- const normalizedPath = normalizeFilePath(projectRoot, filePath);
41
+ // ── ADD: capture snapshot BEFORE chokidar fires for 'change'
42
+ // We pre-read when the watch starts for existing files via 'ready' + 'add'
43
+ watcher.on('add', (filePath) => {
44
+ const rel = normPath(projectRoot, filePath);
45
+ if (!rel || shouldIgnoreProjectFile(rel) || scoreEvent(rel) < 2) return;
41
46
 
42
- if (!normalizedPath || shouldIgnoreProjectFile(normalizedPath) || scoreEvent(normalizedPath) < 2) {
43
- return;
44
- }
47
+ // Snapshot what the file looks like when first seen
48
+ captureSnapshot(filePath);
45
49
 
46
- if (logger) {
47
- logger.debug(`Queued meaningful ${action} for ${normalizedPath}`);
48
- }
50
+ if (logger) logger.debug(`Tracking new file: ${rel}`);
49
51
 
50
- debouncedUpdater.enqueue({
51
- timestamp: new Date().toISOString(),
52
- action,
53
- file: normalizedPath
52
+ debounced.enqueue({
53
+ timestamp: new Date().toISOString(),
54
+ action: 'add',
55
+ file: rel,
56
+ oldContent: null // brand new file
57
+ // newContent left undefined → watcher will read it fresh
58
+ });
59
+ });
60
+
61
+ watcher.on('change', (filePath) => {
62
+ const rel = normPath(projectRoot, filePath);
63
+ if (!rel || shouldIgnoreProjectFile(rel) || scoreEvent(rel) < 2) return;
64
+
65
+ // Grab the PREVIOUS content BEFORE it's overwritten on disk
66
+ const oldContent = require('./fileSnapshot').getSnapshot(filePath);
67
+
68
+ // Immediately update the snapshot to the latest saved version
69
+ captureSnapshot(filePath);
70
+
71
+ if (logger) logger.debug(`Change detected: ${rel}`);
72
+
73
+ debounced.enqueue({
74
+ timestamp: new Date().toISOString(),
75
+ action: 'change',
76
+ file: rel,
77
+ oldContent // what it looked like before this save
78
+ // newContent left undefined → stateManager reads from disk (just written)
54
79
  });
55
- }
56
-
57
- watcher.on('add', (filePath) => handleEvent('add', filePath));
58
- watcher.on('change', (filePath) => handleEvent('change', filePath));
59
- watcher.on('unlink', (filePath) => handleEvent('delete', filePath));
60
- watcher.on('error', (error) => {
61
- if (logger) {
62
- logger.error(`Watcher error: ${error.message}`);
63
- }
64
80
  });
65
81
 
82
+ watcher.on('unlink', (filePath) => {
83
+ const rel = normPath(projectRoot, filePath);
84
+ if (!rel || shouldIgnoreProjectFile(rel)) return;
85
+
86
+ deleteSnapshot(filePath);
87
+
88
+ if (logger) logger.debug(`Deleted: ${rel}`);
89
+
90
+ debounced.enqueue({
91
+ timestamp: new Date().toISOString(),
92
+ action: 'delete',
93
+ file: rel,
94
+ oldContent: null,
95
+ newContent: ''
96
+ });
97
+ });
98
+
99
+ watcher.on('error', (err) => {
100
+ if (logger) logger.error(`Watcher error: ${err.message}`);
101
+ });
102
+
103
+ // Initial state update on startup
66
104
  await updateProjectState(
67
105
  projectRoot,
68
- {
69
- timestamp: new Date().toISOString(),
70
- action: 'watcher_started',
71
- file: '.'
72
- },
73
- {
74
- logger,
75
- syncCallback
76
- }
106
+ { timestamp: new Date().toISOString(), action: 'watcher_started', file: '.' },
107
+ { logger, syncCallback: syncCb }
77
108
  );
78
109
 
79
110
  return {
80
111
  watcher,
81
112
  async close() {
82
- await debouncedUpdater.flushNow();
113
+ await debounced.flushNow();
83
114
  await watcher.close();
84
115
  },
85
116
  async flush() {
86
- await debouncedUpdater.flushNow();
117
+ await debounced.flushNow();
87
118
  }
88
119
  };
89
120
  }
90
121
 
91
- module.exports = {
92
- startWatcher
93
- };
122
+ module.exports = { startWatcher };
package/index.js CHANGED
@@ -1,14 +1,17 @@
1
1
  'use strict';
2
2
 
3
- const { initProject } = require('./core/init');
4
- const { startWatcher } = require('./core/watcher');
3
+ const { initProject } = require('./core/init');
4
+ const { startWatcher } = require('./core/watcher');
5
+ const { generateBriefing } = require('./core/briefingGenerator');
5
6
  const {
6
7
  bootstrapProjectAnalysis,
7
8
  updateProjectState,
8
9
  loadRuntimeConfig,
9
- updateRuntimeConfig
10
+ updateRuntimeConfig,
11
+ createDefaultState,
12
+ getContextPaths
10
13
  } = require('./core/stateManager');
11
- const { startServer } = require('./server/server');
14
+ const { startServer } = require('./server/server');
12
15
  const {
13
16
  buildPublicAiUrls,
14
17
  ensureGitInitialized,
@@ -17,14 +20,25 @@ const {
17
20
  } = require('./core/gitSync');
18
21
 
19
22
  module.exports = {
20
- buildPublicAiUrls,
21
- ensureGitInitialized,
23
+ // Core lifecycle
22
24
  initProject,
23
- linkGithubRepository,
24
25
  startWatcher,
26
+ startServer,
27
+
28
+ // State management
25
29
  updateProjectState,
26
30
  loadRuntimeConfig,
27
31
  updateRuntimeConfig,
28
- startServer,
32
+ createDefaultState,
33
+ getContextPaths,
34
+ bootstrapProjectAnalysis,
35
+
36
+ // Briefing
37
+ generateBriefing,
38
+
39
+ // Git
40
+ buildPublicAiUrls,
41
+ ensureGitInitialized,
42
+ linkGithubRepository,
29
43
  syncContextToGit
30
- };
44
+ };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "aibridge-context",
3
- "version": "1.5.2",
4
- "description": "Zero-config CLI and library for generating AI-readable project context, serving it locally, and syncing it with git.",
3
+ "version": "2.0.0",
4
+ "description": "Zero-config CLI that auto-generates an AI briefing file for any project. Tracks code changes with real diffs, errors, fixes, file structure, API routes and dependencies. One command: npx aibridge-context start",
5
5
  "main": "./index.js",
6
6
  "bin": {
7
7
  "aibridge-context": "./bin/cli.js",
@@ -29,7 +29,11 @@
29
29
  "watcher",
30
30
  "express",
31
31
  "developer-tools",
32
- "git"
32
+ "git",
33
+ "briefing",
34
+ "diff",
35
+ "changelog",
36
+ "code-tracking"
33
37
  ],
34
38
  "author": "",
35
39
  "license": "MIT",
package/server/routes.js CHANGED
@@ -1,50 +1,46 @@
1
1
  'use strict';
2
2
 
3
- const path = require('path');
3
+ const path = require('path');
4
4
  const express = require('express');
5
5
  const { getContextPaths } = require('../core/stateManager');
6
6
 
7
7
  function createRoutes(projectRoot, logger) {
8
8
  const router = express.Router();
9
- const paths = getContextPaths(projectRoot);
9
+ const paths = getContextPaths(projectRoot);
10
10
 
11
11
  function sendFile(filePath, contentType) {
12
- return function routeHandler(request, response) {
13
- response.type(contentType);
14
- response.sendFile(
15
- path.resolve(filePath),
16
- {
17
- dotfiles: 'allow'
18
- },
19
- (error) => {
20
- if (!error) {
21
- return;
22
- }
23
-
24
- if (logger) {
25
- logger.error(`Failed to serve ${request.path}: ${error.message}`);
26
- }
27
-
28
- if (response.headersSent) {
29
- return;
30
- }
31
-
32
- response.status(error.statusCode || 500).json({
33
- error: 'Unable to read AI context file.'
34
- });
35
- }
36
- );
12
+ return function routeHandler(req, res) {
13
+ res.type(contentType);
14
+ res.sendFile(path.resolve(filePath), { dotfiles: 'allow' }, (err) => {
15
+ if (!err) return;
16
+ if (logger) logger.error(`Failed to serve ${req.path}: ${err.message}`);
17
+ if (res.headersSent) return;
18
+ res.status(err.statusCode || 500).json({ error: 'Unable to read AI context file.' });
19
+ });
37
20
  };
38
21
  }
39
22
 
40
- router.get('/state.json', sendFile(paths.stateFile, 'application/json'));
41
- router.get('/brain.txt', sendFile(paths.brainFile, 'text/plain'));
42
- router.get('/context.md', sendFile(paths.contextFile, 'text/markdown'));
43
- router.get('/changelog.json', sendFile(paths.changelogFile, 'application/json'));
23
+ router.get('/state.json', sendFile(paths.stateFile, 'application/json'));
24
+ router.get('/brain.txt', sendFile(paths.brainFile, 'text/plain'));
25
+ router.get('/context.md', sendFile(paths.contextFile, 'text/markdown'));
26
+ router.get('/changelog.json',sendFile(paths.changelogFile, 'application/json'));
27
+ router.get('/briefing.md', sendFile(paths.briefingFile, 'text/markdown'));
28
+
29
+ // Convenience root — lists all available endpoints
30
+ router.get('/', (req, res) => {
31
+ res.json({
32
+ project: require('../core/stateManager').detectProjectMetadata(projectRoot).project,
33
+ endpoints: {
34
+ briefing: '/briefing.md – Full AI briefing (paste into any AI)',
35
+ state: '/state.json – Machine-readable full state',
36
+ changelog: '/changelog.json – Code change history with diffs',
37
+ brain: '/brain.txt – AI instructions',
38
+ context: '/context.md – Human-readable project summary'
39
+ }
40
+ });
41
+ });
44
42
 
45
43
  return router;
46
44
  }
47
45
 
48
- module.exports = {
49
- createRoutes
50
- };
46
+ module.exports = { createRoutes };