aibridge-context 1.0.3 → 1.2.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 +56 -1
- package/bin/cli.js +107 -12
- package/core/gitSync.js +205 -12
- package/core/init.js +48 -11
- package/core/stateManager.js +15 -1
- package/index.js +15 -2
- package/package.json +2 -1
- package/server/server.js +0 -4
package/README.md
CHANGED
|
@@ -4,13 +4,28 @@
|
|
|
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
|
|
10
16
|
npx aibridge-context init
|
|
17
|
+
npx aibridge-context link-github
|
|
11
18
|
npx aibridge-context start
|
|
12
19
|
```
|
|
13
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
|
+
|
|
14
29
|
## Usage
|
|
15
30
|
|
|
16
31
|
This package exposes a CLI command:
|
|
@@ -29,6 +44,7 @@ After installing globally, you can use:
|
|
|
29
44
|
|
|
30
45
|
```bash
|
|
31
46
|
aibridge init
|
|
47
|
+
aibridge link-github
|
|
32
48
|
```
|
|
33
49
|
|
|
34
50
|
## Features
|
|
@@ -50,6 +66,7 @@ To use the local CLI in this repository:
|
|
|
50
66
|
|
|
51
67
|
```bash
|
|
52
68
|
npx aibridge-context init
|
|
69
|
+
npx aibridge-context link-github
|
|
53
70
|
npx aibridge-context start
|
|
54
71
|
```
|
|
55
72
|
|
|
@@ -57,6 +74,37 @@ If published to npm, the package exposes the `aibridge` binary.
|
|
|
57
74
|
|
|
58
75
|
`ai-context` is supported as a legacy alias.
|
|
59
76
|
|
|
77
|
+
## Using With AI
|
|
78
|
+
|
|
79
|
+
After enabling GitHub sync, use:
|
|
80
|
+
|
|
81
|
+
```text
|
|
82
|
+
https://raw.githubusercontent.com/<user>/<repo>/main/.ai-context/state.json
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
This URL always returns the latest project state.
|
|
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
|
+
|
|
94
|
+
You can also share:
|
|
95
|
+
|
|
96
|
+
```text
|
|
97
|
+
https://raw.githubusercontent.com/<user>/<repo>/main/.ai-context/brain.txt
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Typical flow:
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
npx aibridge-context init
|
|
104
|
+
aibridge link-github
|
|
105
|
+
npx aibridge-context start
|
|
106
|
+
```
|
|
107
|
+
|
|
60
108
|
## Commands
|
|
61
109
|
|
|
62
110
|
### `aibridge init`
|
|
@@ -83,6 +131,10 @@ Default server port: `3333`
|
|
|
83
131
|
|
|
84
132
|
Triggers a manual context refresh and optional git sync.
|
|
85
133
|
|
|
134
|
+
### `aibridge link-github`
|
|
135
|
+
|
|
136
|
+
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.
|
|
137
|
+
|
|
86
138
|
## Generated files
|
|
87
139
|
|
|
88
140
|
### `.ai-context/state.json`
|
|
@@ -124,7 +176,10 @@ Default configuration:
|
|
|
124
176
|
"gitSync": {
|
|
125
177
|
"enabled": false,
|
|
126
178
|
"push": true,
|
|
127
|
-
"commitMessage": "auto: update AI context"
|
|
179
|
+
"commitMessage": "auto: update AI context",
|
|
180
|
+
"remote": "origin",
|
|
181
|
+
"branch": "main",
|
|
182
|
+
"repoUrl": ""
|
|
128
183
|
}
|
|
129
184
|
}
|
|
130
185
|
```
|
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 {
|
|
8
|
-
|
|
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
|
|
|
@@ -28,7 +34,12 @@ async function run() {
|
|
|
28
34
|
|
|
29
35
|
try {
|
|
30
36
|
if (command === 'init') {
|
|
31
|
-
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
|
+
});
|
|
32
43
|
return;
|
|
33
44
|
}
|
|
34
45
|
|
|
@@ -51,14 +62,67 @@ async function run() {
|
|
|
51
62
|
return;
|
|
52
63
|
}
|
|
53
64
|
|
|
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');
|
|
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
|
+
|
|
80
|
+
const repoUrl = process.argv[3] || (await promptForRepoUrl());
|
|
81
|
+
|
|
82
|
+
if (!repoUrl) {
|
|
83
|
+
logger.error('A GitHub repository URL is required.');
|
|
84
|
+
process.exitCode = 1;
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
|
|
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
|
+
}
|
|
101
|
+
|
|
102
|
+
await syncContextToGit(projectRoot, nextConfig.gitSync, logger);
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
|
|
54
106
|
if (command === 'start') {
|
|
55
107
|
await initProject(projectRoot, { logger });
|
|
56
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...');
|
|
57
111
|
const serverHandle = await startServer({
|
|
58
112
|
projectRoot,
|
|
59
|
-
port
|
|
113
|
+
port,
|
|
60
114
|
logger
|
|
61
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
|
+
|
|
62
126
|
const watcherHandle = await startWatcher(projectRoot, { logger });
|
|
63
127
|
|
|
64
128
|
const shutdown = async (signal) => {
|
|
@@ -97,16 +161,47 @@ async function run() {
|
|
|
97
161
|
function printHelp() {
|
|
98
162
|
console.log(`aibridge-context
|
|
99
163
|
|
|
100
|
-
Usage:
|
|
101
|
-
aibridge init
|
|
102
|
-
aibridge start
|
|
103
|
-
aibridge update
|
|
104
|
-
|
|
105
164
|
Commands:
|
|
106
|
-
init
|
|
107
|
-
|
|
108
|
-
|
|
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.
|
|
109
172
|
`);
|
|
110
173
|
}
|
|
111
174
|
|
|
175
|
+
async function promptForRepoUrl() {
|
|
176
|
+
const rl = readline.createInterface({
|
|
177
|
+
input: stdin,
|
|
178
|
+
output: stdout
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
try {
|
|
182
|
+
const response = await rl.question('[aibridge] GitHub repository URL: ');
|
|
183
|
+
return response.trim();
|
|
184
|
+
} finally {
|
|
185
|
+
rl.close();
|
|
186
|
+
}
|
|
187
|
+
}
|
|
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
|
+
|
|
112
207
|
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,159 @@ async function hasStagedContextChanges(projectRoot) {
|
|
|
45
74
|
}
|
|
46
75
|
}
|
|
47
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
|
+
|
|
84
|
+
async function ensureGitInitialized(projectRoot, logger) {
|
|
85
|
+
const repositoryReady = await isGitRepository(projectRoot);
|
|
86
|
+
|
|
87
|
+
if (repositoryReady) {
|
|
88
|
+
return {
|
|
89
|
+
initialized: false
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
try {
|
|
94
|
+
await runGit(projectRoot, ['init']);
|
|
95
|
+
await runGit(projectRoot, ['add', '.']);
|
|
96
|
+
try {
|
|
97
|
+
await runGit(projectRoot, ['commit', '-m', 'initial commit']);
|
|
98
|
+
} catch (error) {
|
|
99
|
+
if (!/nothing to commit/i.test(error.stderr || error.message)) {
|
|
100
|
+
throw error;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
await ensureMainBranch(projectRoot);
|
|
105
|
+
|
|
106
|
+
if (logger) {
|
|
107
|
+
logger.info('Git initialized');
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return {
|
|
111
|
+
initialized: true
|
|
112
|
+
};
|
|
113
|
+
} catch (error) {
|
|
114
|
+
if (logger) {
|
|
115
|
+
logger.warn(`Git initialization failed gracefully: ${formatGitError(error)}`);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return {
|
|
119
|
+
initialized: false,
|
|
120
|
+
error: error.message
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function parseGitHubRepo(repoUrl) {
|
|
126
|
+
if (!repoUrl) {
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const cleanedUrl = repoUrl.trim().replace(/\.git$/, '');
|
|
131
|
+
let match = cleanedUrl.match(/^https?:\/\/github\.com\/([^/]+)\/([^/]+)$/i);
|
|
132
|
+
|
|
133
|
+
if (!match) {
|
|
134
|
+
match = cleanedUrl.match(/^git@github\.com:([^/]+)\/([^/]+)$/i);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (!match) {
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return {
|
|
142
|
+
owner: match[1],
|
|
143
|
+
repo: match[2]
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function buildPublicAiUrls(repoUrl, branch) {
|
|
148
|
+
const parsedRepo = parseGitHubRepo(repoUrl);
|
|
149
|
+
|
|
150
|
+
if (!parsedRepo) {
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const baseUrl = `https://raw.githubusercontent.com/${parsedRepo.owner}/${parsedRepo.repo}/${branch}`;
|
|
155
|
+
|
|
156
|
+
return {
|
|
157
|
+
stateUrl: `${baseUrl}/.ai-context/state.json`,
|
|
158
|
+
brainUrl: `${baseUrl}/.ai-context/brain.txt`
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function logMissingRemoteInstructions(logger) {
|
|
163
|
+
if (!logger) {
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
logger.warn('GitHub remote not found.');
|
|
168
|
+
logger.info('Run:');
|
|
169
|
+
logger.info('git remote add origin <repo-url>');
|
|
170
|
+
logger.info('git push -u origin main');
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function logPublicAiEndpoints(logger, urls) {
|
|
174
|
+
if (!logger || !urls) {
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
logger.info('\u2705 AI Context Synced Successfully');
|
|
179
|
+
logger.info('\u{1F310} Public AI Endpoint:');
|
|
180
|
+
logger.info(urls.stateUrl);
|
|
181
|
+
logger.info('\u{1F9E0} AI Instructions Endpoint:');
|
|
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."');
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
async function linkGithubRepository(projectRoot, repoUrl, logger) {
|
|
190
|
+
const normalizedRepoUrl = repoUrl.trim();
|
|
191
|
+
await ensureGitInitialized(projectRoot, logger);
|
|
192
|
+
|
|
193
|
+
try {
|
|
194
|
+
const existingRemoteUrl = await getRemoteOriginUrl(projectRoot);
|
|
195
|
+
|
|
196
|
+
if (!existingRemoteUrl) {
|
|
197
|
+
await runGit(projectRoot, ['remote', 'add', 'origin', normalizedRepoUrl]);
|
|
198
|
+
} else if (existingRemoteUrl !== normalizedRepoUrl) {
|
|
199
|
+
await runGit(projectRoot, ['remote', 'set-url', 'origin', normalizedRepoUrl]);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
await ensureMainBranch(projectRoot);
|
|
203
|
+
await runGit(projectRoot, ['push', '-u', 'origin', 'main']);
|
|
204
|
+
|
|
205
|
+
return {
|
|
206
|
+
ok: true,
|
|
207
|
+
urls: buildPublicAiUrls(normalizedRepoUrl, 'main')
|
|
208
|
+
};
|
|
209
|
+
} catch (error) {
|
|
210
|
+
if (logger) {
|
|
211
|
+
logger.warn(`GitHub link failed gracefully: ${formatGitError(error)}`);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return {
|
|
215
|
+
ok: false,
|
|
216
|
+
error: error.message
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
48
221
|
async function syncContextToGit(projectRoot, config, logger) {
|
|
49
222
|
const settings = Object.assign(
|
|
50
223
|
{
|
|
51
224
|
enabled: false,
|
|
52
225
|
push: true,
|
|
53
|
-
commitMessage: 'auto: update AI context'
|
|
226
|
+
commitMessage: 'auto: update AI context',
|
|
227
|
+
remote: 'origin',
|
|
228
|
+
branch: 'main',
|
|
229
|
+
repoUrl: ''
|
|
54
230
|
},
|
|
55
231
|
config
|
|
56
232
|
);
|
|
@@ -76,10 +252,25 @@ async function syncContextToGit(projectRoot, config, logger) {
|
|
|
76
252
|
}
|
|
77
253
|
|
|
78
254
|
try {
|
|
79
|
-
await
|
|
255
|
+
const branchName = await ensureMainBranch(projectRoot);
|
|
256
|
+
const remoteUrl = (await getRemoteOriginUrl(projectRoot)) || settings.repoUrl;
|
|
257
|
+
|
|
258
|
+
if (!remoteUrl) {
|
|
259
|
+
logMissingRemoteInstructions(logger);
|
|
260
|
+
return {
|
|
261
|
+
skipped: true,
|
|
262
|
+
reason: 'missing_remote'
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
await runGit(projectRoot, ['add', '-f', '.ai-context']);
|
|
80
267
|
|
|
81
268
|
const hasChanges = await hasStagedContextChanges(projectRoot);
|
|
82
269
|
if (!hasChanges) {
|
|
270
|
+
if (logger) {
|
|
271
|
+
logger.info('No .ai-context changes to sync.');
|
|
272
|
+
}
|
|
273
|
+
|
|
83
274
|
return {
|
|
84
275
|
skipped: true,
|
|
85
276
|
reason: 'no_changes'
|
|
@@ -92,9 +283,7 @@ async function syncContextToGit(projectRoot, config, logger) {
|
|
|
92
283
|
const remoteExists = await hasRemote(projectRoot);
|
|
93
284
|
|
|
94
285
|
if (!remoteExists) {
|
|
95
|
-
|
|
96
|
-
logger.warn('Git sync committed locally, but no remote is configured for push.');
|
|
97
|
-
}
|
|
286
|
+
logMissingRemoteInstructions(logger);
|
|
98
287
|
|
|
99
288
|
return {
|
|
100
289
|
ok: true,
|
|
@@ -102,20 +291,20 @@ async function syncContextToGit(projectRoot, config, logger) {
|
|
|
102
291
|
};
|
|
103
292
|
}
|
|
104
293
|
|
|
105
|
-
await runGit(projectRoot, ['push']);
|
|
294
|
+
await runGit(projectRoot, ['push', '-u', settings.remote || 'origin', branchName]);
|
|
106
295
|
}
|
|
107
296
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
}
|
|
297
|
+
const urls = buildPublicAiUrls(remoteUrl, branchName);
|
|
298
|
+
logPublicAiEndpoints(logger, urls);
|
|
111
299
|
|
|
112
300
|
return {
|
|
113
301
|
ok: true,
|
|
114
|
-
pushed: Boolean(settings.push)
|
|
302
|
+
pushed: Boolean(settings.push),
|
|
303
|
+
urls
|
|
115
304
|
};
|
|
116
305
|
} catch (error) {
|
|
117
306
|
if (logger) {
|
|
118
|
-
logger.warn(`Git sync failed gracefully: ${error
|
|
307
|
+
logger.warn(`Git sync failed gracefully: ${formatGitError(error)}`);
|
|
119
308
|
}
|
|
120
309
|
|
|
121
310
|
return {
|
|
@@ -126,6 +315,10 @@ async function syncContextToGit(projectRoot, config, logger) {
|
|
|
126
315
|
}
|
|
127
316
|
|
|
128
317
|
module.exports = {
|
|
318
|
+
buildPublicAiUrls,
|
|
319
|
+
ensureGitInitialized,
|
|
320
|
+
getRemoteOriginUrl,
|
|
129
321
|
isGitRepository,
|
|
322
|
+
linkGithubRepository,
|
|
130
323
|
syncContextToGit
|
|
131
324
|
};
|
package/core/init.js
CHANGED
|
@@ -11,12 +11,22 @@ 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
|
-
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
|
+
);
|
|
20
30
|
const logger = settings.logger;
|
|
21
31
|
const contextDir = await ensureContextDirectory(projectRoot);
|
|
22
32
|
const paths = getContextPaths(projectRoot);
|
|
@@ -34,6 +44,7 @@ async function initProject(projectRoot, options) {
|
|
|
34
44
|
const existingChangelog = await readJsonFile(paths.changelogFile, null);
|
|
35
45
|
const templateState = JSON.parse(stateTemplate);
|
|
36
46
|
const templateChangelog = JSON.parse(changelogTemplate);
|
|
47
|
+
let enableGitSync = false;
|
|
37
48
|
|
|
38
49
|
const initialState = Object.assign({}, templateState, existingState || createDefaultState(projectRoot), {
|
|
39
50
|
project: metadata.project,
|
|
@@ -60,18 +71,21 @@ async function initProject(projectRoot, options) {
|
|
|
60
71
|
await writeTextAtomic(paths.contextFile, initialContext);
|
|
61
72
|
}
|
|
62
73
|
|
|
63
|
-
if (!existingConfig) {
|
|
64
|
-
await
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
enabled: false,
|
|
69
|
-
push: true,
|
|
70
|
-
commitMessage: 'auto: update AI context'
|
|
71
|
-
}
|
|
72
|
-
});
|
|
74
|
+
if (!existingConfig && settings.requestPublicSyncConsent) {
|
|
75
|
+
enableGitSync = await requestPublicSyncConsent(
|
|
76
|
+
logger,
|
|
77
|
+
settings.promptForPublicSyncConsent
|
|
78
|
+
);
|
|
73
79
|
}
|
|
74
80
|
|
|
81
|
+
await updateRuntimeConfig(projectRoot, existingConfig ? null : {
|
|
82
|
+
gitSync: {
|
|
83
|
+
enabled: enableGitSync
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
await ensureGitInitialized(projectRoot, logger);
|
|
88
|
+
|
|
75
89
|
if (logger) {
|
|
76
90
|
logger.info(`Initialized AI context in ${contextDir}`);
|
|
77
91
|
}
|
|
@@ -82,6 +96,29 @@ async function initProject(projectRoot, options) {
|
|
|
82
96
|
};
|
|
83
97
|
}
|
|
84
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
|
+
|
|
85
122
|
async function fileExists(filePath) {
|
|
86
123
|
try {
|
|
87
124
|
await fsp.access(filePath);
|
package/core/stateManager.js
CHANGED
|
@@ -14,7 +14,10 @@ const DEFAULT_CONFIG = {
|
|
|
14
14
|
gitSync: {
|
|
15
15
|
enabled: false,
|
|
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 {
|
|
5
|
+
const {
|
|
6
|
+
updateProjectState,
|
|
7
|
+
loadRuntimeConfig,
|
|
8
|
+
updateRuntimeConfig
|
|
9
|
+
} = require('./core/stateManager');
|
|
6
10
|
const { startServer } = require('./server/server');
|
|
7
|
-
const {
|
|
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
|
+
"version": "1.2.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
|
},
|
package/server/server.js
CHANGED