aibridge-context 1.0.3 → 1.1.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 CHANGED
@@ -8,6 +8,7 @@ Think of it as Git for AI context.
8
8
 
9
9
  ```bash
10
10
  npx aibridge-context init
11
+ npx aibridge-context link-github
11
12
  npx aibridge-context start
12
13
  ```
13
14
 
@@ -29,6 +30,7 @@ After installing globally, you can use:
29
30
 
30
31
  ```bash
31
32
  aibridge init
33
+ aibridge link-github
32
34
  ```
33
35
 
34
36
  ## Features
@@ -50,6 +52,7 @@ To use the local CLI in this repository:
50
52
 
51
53
  ```bash
52
54
  npx aibridge-context init
55
+ npx aibridge-context link-github
53
56
  npx aibridge-context start
54
57
  ```
55
58
 
@@ -57,6 +60,30 @@ If published to npm, the package exposes the `aibridge` binary.
57
60
 
58
61
  `ai-context` is supported as a legacy alias.
59
62
 
63
+ ## Use With AI
64
+
65
+ After enabling GitHub sync, use:
66
+
67
+ ```text
68
+ https://raw.githubusercontent.com/<user>/<repo>/main/.ai-context/state.json
69
+ ```
70
+
71
+ This URL always returns the latest project state.
72
+
73
+ You can also share:
74
+
75
+ ```text
76
+ https://raw.githubusercontent.com/<user>/<repo>/main/.ai-context/brain.txt
77
+ ```
78
+
79
+ Typical flow:
80
+
81
+ ```bash
82
+ npx aibridge-context init
83
+ aibridge link-github
84
+ npx aibridge-context start
85
+ ```
86
+
60
87
  ## Commands
61
88
 
62
89
  ### `aibridge init`
@@ -83,6 +110,10 @@ Default server port: `3333`
83
110
 
84
111
  Triggers a manual context refresh and optional git sync.
85
112
 
113
+ ### `aibridge link-github`
114
+
115
+ Prompts for a GitHub repository URL, links `origin`, pushes `main`, saves the repo URL to `.ai-context/config.json`, and enables public AI sync output.
116
+
86
117
  ## Generated files
87
118
 
88
119
  ### `.ai-context/state.json`
package/bin/cli.js CHANGED
@@ -2,10 +2,16 @@
2
2
  'use strict';
3
3
 
4
4
  const path = require('path');
5
+ const readline = require('readline/promises');
6
+ const { stdin, stdout } = require('process');
5
7
  const { initProject } = require('../core/init');
6
8
  const { startWatcher } = require('../core/watcher');
7
- const { loadRuntimeConfig, updateProjectState } = require('../core/stateManager');
8
- const { syncContextToGit } = require('../core/gitSync');
9
+ const {
10
+ loadRuntimeConfig,
11
+ updateProjectState,
12
+ updateRuntimeConfig
13
+ } = require('../core/stateManager');
14
+ const { linkGithubRepository, syncContextToGit } = require('../core/gitSync');
9
15
  const { startServer } = require('../server/server');
10
16
  const { createLogger } = require('../utils/logger');
11
17
 
@@ -51,6 +57,34 @@ async function run() {
51
57
  return;
52
58
  }
53
59
 
60
+ if (command === 'link-github') {
61
+ await initProject(projectRoot, { logger });
62
+ const repoUrl = process.argv[3] || (await promptForRepoUrl());
63
+
64
+ if (!repoUrl) {
65
+ logger.error('A GitHub repository URL is required.');
66
+ process.exitCode = 1;
67
+ return;
68
+ }
69
+
70
+ const nextConfig = await updateRuntimeConfig(projectRoot, {
71
+ gitSync: {
72
+ enabled: true,
73
+ push: true,
74
+ repoUrl: repoUrl.trim()
75
+ }
76
+ });
77
+ const linkResult = await linkGithubRepository(projectRoot, repoUrl.trim(), logger);
78
+
79
+ if (!linkResult.ok) {
80
+ process.exitCode = 1;
81
+ return;
82
+ }
83
+
84
+ await syncContextToGit(projectRoot, nextConfig.gitSync, logger);
85
+ return;
86
+ }
87
+
54
88
  if (command === 'start') {
55
89
  await initProject(projectRoot, { logger });
56
90
  const config = await loadRuntimeConfig(projectRoot);
@@ -99,14 +133,30 @@ function printHelp() {
99
133
 
100
134
  Usage:
101
135
  aibridge init
136
+ aibridge link-github
102
137
  aibridge start
103
138
  aibridge update
104
139
 
105
140
  Commands:
106
141
  init Create .ai-context and initialize AI context files
142
+ link-github Connect a GitHub remote and enable public AI sync
107
143
  start Start the watcher and local server on port 3333
108
144
  update Trigger a manual state update
109
145
  `);
110
146
  }
111
147
 
148
+ async function promptForRepoUrl() {
149
+ const rl = readline.createInterface({
150
+ input: stdin,
151
+ output: stdout
152
+ });
153
+
154
+ try {
155
+ const response = await rl.question('GitHub repository URL: ');
156
+ return response.trim();
157
+ } finally {
158
+ rl.close();
159
+ }
160
+ }
161
+
112
162
  run();
package/core/gitSync.js CHANGED
@@ -29,13 +29,42 @@ async function isGitRepository(projectRoot) {
29
29
 
30
30
  async function hasRemote(projectRoot) {
31
31
  try {
32
- const result = await runGit(projectRoot, ['remote']);
32
+ const result = await runGit(projectRoot, ['remote', 'get-url', 'origin']);
33
33
  return result.stdout.trim().length > 0;
34
34
  } catch (error) {
35
35
  return false;
36
36
  }
37
37
  }
38
38
 
39
+ async function getRemoteOriginUrl(projectRoot) {
40
+ try {
41
+ const result = await runGit(projectRoot, ['remote', 'get-url', 'origin']);
42
+ return result.stdout.trim();
43
+ } catch (error) {
44
+ return '';
45
+ }
46
+ }
47
+
48
+ async function getCurrentBranch(projectRoot) {
49
+ try {
50
+ const result = await runGit(projectRoot, ['branch', '--show-current']);
51
+ return result.stdout.trim();
52
+ } catch (error) {
53
+ return '';
54
+ }
55
+ }
56
+
57
+ async function ensureMainBranch(projectRoot) {
58
+ const currentBranch = await getCurrentBranch(projectRoot);
59
+
60
+ if (currentBranch === 'main') {
61
+ return 'main';
62
+ }
63
+
64
+ await runGit(projectRoot, ['branch', '-M', 'main']);
65
+ return 'main';
66
+ }
67
+
39
68
  async function hasStagedContextChanges(projectRoot) {
40
69
  try {
41
70
  await runGit(projectRoot, ['diff', '--cached', '--quiet', '--', '.ai-context']);
@@ -45,12 +74,149 @@ async function hasStagedContextChanges(projectRoot) {
45
74
  }
46
75
  }
47
76
 
77
+ async function ensureGitInitialized(projectRoot, logger) {
78
+ const repositoryReady = await isGitRepository(projectRoot);
79
+
80
+ if (repositoryReady) {
81
+ return {
82
+ initialized: false
83
+ };
84
+ }
85
+
86
+ try {
87
+ await runGit(projectRoot, ['init']);
88
+ await runGit(projectRoot, ['add', '.']);
89
+ try {
90
+ await runGit(projectRoot, ['commit', '-m', 'initial commit']);
91
+ } catch (error) {
92
+ if (!/nothing to commit/i.test(error.stderr || error.message)) {
93
+ throw error;
94
+ }
95
+ }
96
+ await ensureMainBranch(projectRoot);
97
+
98
+ if (logger) {
99
+ logger.info('Git initialized');
100
+ }
101
+
102
+ return {
103
+ initialized: true
104
+ };
105
+ } catch (error) {
106
+ if (logger) {
107
+ logger.warn(`Git initialization failed gracefully: ${error.message}`);
108
+ }
109
+
110
+ return {
111
+ initialized: false,
112
+ error: error.message
113
+ };
114
+ }
115
+ }
116
+
117
+ function parseGitHubRepo(repoUrl) {
118
+ if (!repoUrl) {
119
+ return null;
120
+ }
121
+
122
+ const cleanedUrl = repoUrl.trim().replace(/\.git$/, '');
123
+ let match = cleanedUrl.match(/^https?:\/\/github\.com\/([^/]+)\/([^/]+)$/i);
124
+
125
+ if (!match) {
126
+ match = cleanedUrl.match(/^git@github\.com:([^/]+)\/([^/]+)$/i);
127
+ }
128
+
129
+ if (!match) {
130
+ return null;
131
+ }
132
+
133
+ return {
134
+ owner: match[1],
135
+ repo: match[2]
136
+ };
137
+ }
138
+
139
+ function buildPublicAiUrls(repoUrl, branch) {
140
+ const parsedRepo = parseGitHubRepo(repoUrl);
141
+
142
+ if (!parsedRepo) {
143
+ return null;
144
+ }
145
+
146
+ const baseUrl = `https://raw.githubusercontent.com/${parsedRepo.owner}/${parsedRepo.repo}/${branch}`;
147
+
148
+ return {
149
+ stateUrl: `${baseUrl}/.ai-context/state.json`,
150
+ brainUrl: `${baseUrl}/.ai-context/brain.txt`
151
+ };
152
+ }
153
+
154
+ function logMissingRemoteInstructions(logger) {
155
+ if (!logger) {
156
+ return;
157
+ }
158
+
159
+ logger.warn('No GitHub remote detected.');
160
+ logger.info('Run:');
161
+ logger.info('git remote add origin <repo-url>');
162
+ logger.info('git push -u origin main');
163
+ }
164
+
165
+ function logPublicAiEndpoints(logger, urls) {
166
+ if (!logger || !urls) {
167
+ return;
168
+ }
169
+
170
+ logger.info('Public AI endpoint:');
171
+ logger.info(urls.stateUrl);
172
+ logger.info('Use this with AI:');
173
+ logger.info(urls.brainUrl);
174
+ }
175
+
176
+ async function linkGithubRepository(projectRoot, repoUrl, logger) {
177
+ const normalizedRepoUrl = repoUrl.trim();
178
+ await ensureGitInitialized(projectRoot, logger);
179
+
180
+ try {
181
+ const existingRemoteUrl = await getRemoteOriginUrl(projectRoot);
182
+
183
+ if (!existingRemoteUrl) {
184
+ await runGit(projectRoot, ['remote', 'add', 'origin', normalizedRepoUrl]);
185
+ } else if (existingRemoteUrl !== normalizedRepoUrl) {
186
+ await runGit(projectRoot, ['remote', 'set-url', 'origin', normalizedRepoUrl]);
187
+ }
188
+
189
+ await ensureMainBranch(projectRoot);
190
+ await runGit(projectRoot, ['push', '-u', 'origin', 'main']);
191
+
192
+ const urls = buildPublicAiUrls(normalizedRepoUrl, 'main');
193
+ logPublicAiEndpoints(logger, urls);
194
+
195
+ return {
196
+ ok: true,
197
+ urls
198
+ };
199
+ } catch (error) {
200
+ if (logger) {
201
+ logger.warn(`GitHub link failed gracefully: ${error.message}`);
202
+ }
203
+
204
+ return {
205
+ ok: false,
206
+ error: error.message
207
+ };
208
+ }
209
+ }
210
+
48
211
  async function syncContextToGit(projectRoot, config, logger) {
49
212
  const settings = Object.assign(
50
213
  {
51
- enabled: false,
214
+ enabled: true,
52
215
  push: true,
53
- commitMessage: 'auto: update AI context'
216
+ commitMessage: 'auto: update AI context',
217
+ remote: 'origin',
218
+ branch: 'main',
219
+ repoUrl: ''
54
220
  },
55
221
  config
56
222
  );
@@ -76,10 +242,25 @@ async function syncContextToGit(projectRoot, config, logger) {
76
242
  }
77
243
 
78
244
  try {
79
- await runGit(projectRoot, ['add', '.ai-context']);
245
+ const branchName = await ensureMainBranch(projectRoot);
246
+ const remoteUrl = (await getRemoteOriginUrl(projectRoot)) || settings.repoUrl;
247
+
248
+ if (!remoteUrl) {
249
+ logMissingRemoteInstructions(logger);
250
+ return {
251
+ skipped: true,
252
+ reason: 'missing_remote'
253
+ };
254
+ }
255
+
256
+ await runGit(projectRoot, ['add', '-f', '.ai-context']);
80
257
 
81
258
  const hasChanges = await hasStagedContextChanges(projectRoot);
82
259
  if (!hasChanges) {
260
+ if (logger) {
261
+ logger.info('No .ai-context changes to sync.');
262
+ }
263
+
83
264
  return {
84
265
  skipped: true,
85
266
  reason: 'no_changes'
@@ -92,9 +273,7 @@ async function syncContextToGit(projectRoot, config, logger) {
92
273
  const remoteExists = await hasRemote(projectRoot);
93
274
 
94
275
  if (!remoteExists) {
95
- if (logger) {
96
- logger.warn('Git sync committed locally, but no remote is configured for push.');
97
- }
276
+ logMissingRemoteInstructions(logger);
98
277
 
99
278
  return {
100
279
  ok: true,
@@ -102,16 +281,21 @@ async function syncContextToGit(projectRoot, config, logger) {
102
281
  };
103
282
  }
104
283
 
105
- await runGit(projectRoot, ['push']);
284
+ await runGit(projectRoot, ['push', '-u', settings.remote || 'origin', branchName]);
106
285
  }
107
286
 
287
+ const urls = buildPublicAiUrls(remoteUrl, branchName);
288
+
108
289
  if (logger) {
109
290
  logger.info('Synced .ai-context changes to git.');
110
291
  }
111
292
 
293
+ logPublicAiEndpoints(logger, urls);
294
+
112
295
  return {
113
296
  ok: true,
114
- pushed: Boolean(settings.push)
297
+ pushed: Boolean(settings.push),
298
+ urls
115
299
  };
116
300
  } catch (error) {
117
301
  if (logger) {
@@ -126,6 +310,10 @@ async function syncContextToGit(projectRoot, config, logger) {
126
310
  }
127
311
 
128
312
  module.exports = {
313
+ buildPublicAiUrls,
314
+ ensureGitInitialized,
315
+ getRemoteOriginUrl,
129
316
  isGitRepository,
317
+ linkGithubRepository,
130
318
  syncContextToGit
131
319
  };
package/core/init.js CHANGED
@@ -11,9 +11,11 @@ const {
11
11
  getContextPaths,
12
12
  readJsonFile,
13
13
  renderTemplate,
14
+ updateRuntimeConfig,
14
15
  writeJsonAtomic,
15
16
  writeTextAtomic
16
17
  } = require('./stateManager');
18
+ const { ensureGitInitialized } = require('./gitSync');
17
19
 
18
20
  async function initProject(projectRoot, options) {
19
21
  const settings = Object.assign({ logger: null, force: false }, options);
@@ -60,17 +62,20 @@ async function initProject(projectRoot, options) {
60
62
  await writeTextAtomic(paths.contextFile, initialContext);
61
63
  }
62
64
 
63
- if (!existingConfig) {
64
- await writeJsonAtomic(paths.configFile, {
65
- port: 3333,
66
- debounceMs: 600,
67
- gitSync: {
68
- enabled: false,
69
- push: true,
70
- commitMessage: 'auto: update AI context'
71
- }
72
- });
73
- }
65
+ await updateRuntimeConfig(
66
+ projectRoot,
67
+ existingConfig
68
+ ? null
69
+ : {
70
+ gitSync: {
71
+ enabled: true,
72
+ push: true,
73
+ commitMessage: 'auto: update AI context'
74
+ }
75
+ }
76
+ );
77
+
78
+ await ensureGitInitialized(projectRoot, logger);
74
79
 
75
80
  if (logger) {
76
81
  logger.info(`Initialized AI context in ${contextDir}`);
@@ -12,9 +12,12 @@ const DEFAULT_CONFIG = {
12
12
  port: 3333,
13
13
  debounceMs: 600,
14
14
  gitSync: {
15
- enabled: false,
15
+ enabled: true,
16
16
  push: true,
17
- commitMessage: 'auto: update AI context'
17
+ commitMessage: 'auto: update AI context',
18
+ remote: 'origin',
19
+ branch: 'main',
20
+ repoUrl: ''
18
21
  }
19
22
  };
20
23
 
@@ -181,6 +184,16 @@ async function loadRuntimeConfig(projectRoot) {
181
184
  return deepMerge(DEFAULT_CONFIG, userConfig);
182
185
  }
183
186
 
187
+ async function updateRuntimeConfig(projectRoot, updates) {
188
+ const { configFile } = getContextPaths(projectRoot);
189
+ const currentConfig = await readJsonFile(configFile, {});
190
+ const mergedCurrentConfig = deepMerge(DEFAULT_CONFIG, currentConfig);
191
+ const nextConfig = deepMerge(mergedCurrentConfig, updates || {});
192
+
193
+ await writeJsonAtomic(configFile, nextConfig);
194
+ return nextConfig;
195
+ }
196
+
184
197
  async function updateProjectState(projectRoot, changeEvent, options) {
185
198
  const settings = Object.assign(
186
199
  {
@@ -321,6 +334,7 @@ module.exports = {
321
334
  loadRuntimeConfig,
322
335
  readJsonFile,
323
336
  renderTemplate,
337
+ updateRuntimeConfig,
324
338
  updateProjectState,
325
339
  writeJsonAtomic,
326
340
  writeTextAtomic
package/index.js CHANGED
@@ -2,15 +2,28 @@
2
2
 
3
3
  const { initProject } = require('./core/init');
4
4
  const { startWatcher } = require('./core/watcher');
5
- const { updateProjectState, loadRuntimeConfig } = require('./core/stateManager');
5
+ const {
6
+ updateProjectState,
7
+ loadRuntimeConfig,
8
+ updateRuntimeConfig
9
+ } = require('./core/stateManager');
6
10
  const { startServer } = require('./server/server');
7
- const { syncContextToGit } = require('./core/gitSync');
11
+ const {
12
+ buildPublicAiUrls,
13
+ ensureGitInitialized,
14
+ linkGithubRepository,
15
+ syncContextToGit
16
+ } = require('./core/gitSync');
8
17
 
9
18
  module.exports = {
19
+ buildPublicAiUrls,
20
+ ensureGitInitialized,
10
21
  initProject,
22
+ linkGithubRepository,
11
23
  startWatcher,
12
24
  updateProjectState,
13
25
  loadRuntimeConfig,
26
+ updateRuntimeConfig,
14
27
  startServer,
15
28
  syncContextToGit
16
29
  };
package/package.json CHANGED
@@ -1,9 +1,10 @@
1
1
  {
2
2
  "name": "aibridge-context",
3
- "version": "1.0.3",
3
+ "version": "1.1.0",
4
4
  "description": "Zero-config CLI and library for generating AI-readable project context, serving it locally, and syncing it with git.",
5
5
  "main": "./index.js",
6
6
  "bin": {
7
+ "aibridge-context": "./bin/cli.js",
7
8
  "aibridge": "./bin/cli.js",
8
9
  "ai-context": "./bin/cli.js"
9
10
  },