@anirudh242/contextbridge 0.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 ADDED
@@ -0,0 +1,62 @@
1
+ # ContextBridge
2
+
3
+ Git-style version control for your AI session context. Works across every coding tool you use.
4
+
5
+ [![npm version](https://img.shields.io/npm/v/contextbridge.svg)](https://npmjs.org/package/contextbridge) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
6
+
7
+ ## The Problem
8
+
9
+ Working with AI assistants involves constantly running into context limits or losing the thread when switching between tools. Every time you start a new conversation, you have to painstakingly rebuild the project's current state and goals.
10
+
11
+ ## The Solution
12
+
13
+ ContextBridge captures your exact working state (git diffs, active tasks, blockers) and compresses it into an LLM-generated summary. You can then instantly inject that context into any agent or browser-based AI tool.
14
+
15
+ ```bash
16
+ cb init # set up once per project
17
+ cb capture # after any AI session — auto-reads your git diff
18
+ cb inject # before starting a new session — injects context everywhere
19
+ ```
20
+
21
+ ## Installation
22
+
23
+ **Prerequisite:** Node.js >= 18
24
+
25
+ ```bash
26
+ npm install -g contextbridge
27
+ # or run without installing:
28
+ npx contextbridge init
29
+ ```
30
+
31
+ ## Core Commands
32
+
33
+ - `cb capture` - Analyzes your `git diff` and summarizes the current session context. Add `-m "note"` to manually steer the summary, or `--paste` to include clipboard content.
34
+ - `cb inject` - Injects the latest trusted context into your AI tools.
35
+ - `cb log` - View the version history of your project's captured sessions.
36
+ - `cb status` - Show the current ContextBridge status and configuration for this project.
37
+
38
+ ## Provider Setup
39
+
40
+ ContextBridge works best with an LLM for summarisation. `cb init` will walk you through the initial setup.
41
+
42
+ | Provider | Model used | Note |
43
+ | ----------- | --------------------------- | --------------------------------------------------- |
44
+ | `anthropic` | `claude-haiku-4-5-20251001` | Recommended. Fast and highly accurate. |
45
+ | `openai` | `gpt-4o-mini` | Good alternative. |
46
+ | `gemini` | `gemini-2.0-flash` | Google's high-speed option. |
47
+ | `ollama` | `qwen2.5-coder:7b` | Free and completely private. Requires local Ollama. |
48
+
49
+ _You can change your active provider at any time by running `cb provider`._
50
+ _If you are using Ollama, you can configure the timeout limit by running `cb timeout <ms>`._
51
+
52
+ ## Tool-Specific Injection
53
+
54
+ What happens when you run `cb inject`:
55
+
56
+ - **Antigravity:** `AGENTS.md` is written automatically to your project root. Antigravity reads it on every agent task. Nothing else needed.
57
+ - **Codex:** `cb inject --codex | codex exec -` pipes context directly into a new session.
58
+ - **Browser tools (Claude, ChatGPT, Gemini):** Context is copied to your clipboard automatically. Paste it at the start of your conversation.
59
+
60
+ ## Privacy & Storage
61
+
62
+ All context and configuration are stored locally in `~/.contextbridge/` on your machine. When using a cloud provider, only the LLM summary request is sent — not your raw git diff or clipboard content. When using Ollama, no data leaves your machine.
@@ -0,0 +1,160 @@
1
+ import clipboard from 'clipboardy';
2
+ import { randomUUID } from 'node:crypto';
3
+ import ora from 'ora';
4
+ import { loadConfig } from '../lib/config.js';
5
+ import { addEntryToProjectStore, ensureProjectStore } from '../lib/store.js';
6
+ import { getCurrentBranch, getGitContext } from '../lib/git.js';
7
+ import { summariseContent } from '../lib/llm.js';
8
+ import { buildProjectGrounding } from '../lib/project.js';
9
+ import { formatSummaryLines, printSuccess, printWarning } from '../utils/display.js';
10
+ function hasContent(value) {
11
+ return value.trim().length > 0;
12
+ }
13
+ function buildRawContent(raw) {
14
+ const parts = [];
15
+ if (hasContent(raw.note)) {
16
+ parts.push(`Manual note:\n${raw.note}`);
17
+ }
18
+ if (hasContent(raw.clipboard)) {
19
+ parts.push(`Clipboard:\n${raw.clipboard}`);
20
+ }
21
+ if (hasContent(raw.repoGrounding)) {
22
+ parts.push(raw.repoGrounding);
23
+ }
24
+ if (hasContent(raw.gitLog)) {
25
+ parts.push(`Git log:\n${raw.gitLog}`);
26
+ }
27
+ if (hasContent(raw.gitStatus)) {
28
+ parts.push(`Git status:\n${raw.gitStatus}`);
29
+ }
30
+ if (hasContent(raw.gitDiff)) {
31
+ parts.push(`Git diff:\n${raw.gitDiff}`);
32
+ }
33
+ return parts.join('\n\n');
34
+ }
35
+ function extractStackHint(repoGrounding) {
36
+ const match = repoGrounding.match(/dependencies:\s*(.+)/i);
37
+ return match?.[1]?.trim() || 'Unknown';
38
+ }
39
+ function extractLabeledValue(note, label, stopLabels) {
40
+ const escapedLabel = label.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
41
+ const escapedStops = stopLabels.map((item) => item.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'));
42
+ const pattern = new RegExp(`${escapedLabel}\\s*:\\s*([\\s\\S]*?)(?=\\s+(?:${escapedStops.join('|')})\\s*:|$)`, 'i');
43
+ return note.match(pattern)?.[1]?.trim() ?? '';
44
+ }
45
+ function splitList(value) {
46
+ return value
47
+ .split(/;|\n/)
48
+ .map((item) => item.replace(/^[-*]\s*/, '').trim())
49
+ .filter(Boolean)
50
+ .slice(0, 3);
51
+ }
52
+ function buildManualSummary(note, repoGrounding) {
53
+ const labels = ['Project goal', 'Current direction', 'Current focus', 'Decisions', 'Blockers', 'Next steps'];
54
+ const projectGoal = extractLabeledValue(note, 'Project goal', labels)
55
+ || note.split(/current direction:/i)[0]?.trim()
56
+ || 'Unknown';
57
+ const currentDirection = extractLabeledValue(note, 'Current direction', labels)
58
+ || note.split(/current direction:/i)[1]?.trim()
59
+ || note.trim();
60
+ const currentFocus = extractLabeledValue(note, 'Current focus', labels) || currentDirection;
61
+ const decisions = splitList(extractLabeledValue(note, 'Decisions', labels));
62
+ const blockers = splitList(extractLabeledValue(note, 'Blockers', labels));
63
+ const nextSteps = splitList(extractLabeledValue(note, 'Next steps', labels));
64
+ return {
65
+ projectGoal,
66
+ currentDirection,
67
+ currentFocus,
68
+ decisions,
69
+ blockers: blockers.length > 0 ? blockers : ['None'],
70
+ stack: extractStackHint(repoGrounding),
71
+ nextSteps,
72
+ };
73
+ }
74
+ function shouldUseManualSummary(raw) {
75
+ return hasContent(raw.note) && !hasContent(raw.clipboard);
76
+ }
77
+ function getErrorMessage(error) {
78
+ return error instanceof Error ? error.message : String(error);
79
+ }
80
+ export function registerCaptureCommand(program) {
81
+ program
82
+ .command('capture')
83
+ .description('Capture the latest session context')
84
+ .option('--paste', 'Read clipboard content as an additional capture source')
85
+ .option('-m, --message <text>', 'Add a quick manual note')
86
+ .action(async (options) => {
87
+ const spinner = ora('Capturing session...').start();
88
+ const projectPath = process.cwd();
89
+ try {
90
+ const config = await loadConfig();
91
+ await ensureProjectStore(projectPath);
92
+ const gitContext = await getGitContext(projectPath);
93
+ const repoGrounding = await buildProjectGrounding(projectPath);
94
+ const raw = {
95
+ gitDiff: gitContext.diff ?? '',
96
+ gitLog: gitContext.log ?? '',
97
+ gitStatus: gitContext.status ?? '',
98
+ repoGrounding,
99
+ clipboard: '',
100
+ note: options.message ?? '',
101
+ };
102
+ if (options.paste) {
103
+ try {
104
+ raw.clipboard = await clipboard.read();
105
+ }
106
+ catch {
107
+ spinner.warn('Clipboard read failed. Continuing without clipboard content.');
108
+ spinner.start('Capturing session...');
109
+ }
110
+ }
111
+ const warnings = [...gitContext.warnings];
112
+ const content = buildRawContent(raw);
113
+ const sources = [raw.gitDiff, raw.gitLog, raw.gitStatus, raw.repoGrounding, raw.clipboard, raw.note].filter(hasContent);
114
+ if (sources.length === 0) {
115
+ spinner.fail('Nothing to capture. Add a note, copy context, or make code changes first.');
116
+ return;
117
+ }
118
+ let summary = null;
119
+ if (shouldUseManualSummary(raw)) {
120
+ summary = buildManualSummary(raw.note, raw.repoGrounding);
121
+ spinner.succeed('Captured manual note as the primary direction signal.');
122
+ }
123
+ else {
124
+ try {
125
+ summary = await summariseContent(content, config);
126
+ spinner.succeed('Session captured and summarised.');
127
+ }
128
+ catch (error) {
129
+ const message = getErrorMessage(error);
130
+ spinner.warn(`Summarisation failed: ${message}`);
131
+ warnings.push(`Summarisation failed: ${message}`);
132
+ if (shouldUseManualSummary(raw)) {
133
+ summary = buildManualSummary(raw.note, raw.repoGrounding);
134
+ warnings.push('Fell back to the manual note because summarisation was not trustworthy.');
135
+ }
136
+ }
137
+ }
138
+ const entry = {
139
+ id: randomUUID(),
140
+ timestamp: new Date().toISOString(),
141
+ branch: (await getCurrentBranch(projectPath)) ?? 'unknown',
142
+ summary,
143
+ };
144
+ await addEntryToProjectStore(projectPath, entry, raw);
145
+ if (summary) {
146
+ printSuccess('Context saved', formatSummaryLines(summary));
147
+ }
148
+ else {
149
+ printWarning('Raw context saved without summary.');
150
+ }
151
+ for (const warning of warnings) {
152
+ printWarning(warning);
153
+ }
154
+ }
155
+ catch (error) {
156
+ spinner.fail(`Capture failed: ${getErrorMessage(error)}`);
157
+ process.exitCode = 1;
158
+ }
159
+ });
160
+ }
@@ -0,0 +1,83 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import readline from 'node:readline/promises';
4
+ import { ensureConfig, getConfigPath, registerProjectInConfig, setProvider, } from '../lib/config.js';
5
+ import { ensureProjectStore, getStorePath } from '../lib/store.js';
6
+ import { isGitRepository } from '../lib/git.js';
7
+ import { printInfo, printSuccess, printWarning } from '../utils/display.js';
8
+ function getErrorCode(error) {
9
+ return error instanceof Error && 'code' in error
10
+ ? String(error.code)
11
+ : undefined;
12
+ }
13
+ async function ensureAgentsGitignore(projectPath) {
14
+ const gitignorePath = path.join(projectPath, '.gitignore');
15
+ try {
16
+ const existing = await fs.readFile(gitignorePath, 'utf8');
17
+ if (!existing.split('\n').map((line) => line.trim()).includes('AGENTS.md')) {
18
+ const prefix = existing.endsWith('\n') || existing.length === 0 ? '' : '\n';
19
+ await fs.writeFile(gitignorePath, `${existing}${prefix}AGENTS.md\n`, 'utf8');
20
+ }
21
+ return true;
22
+ }
23
+ catch (error) {
24
+ if (getErrorCode(error) === 'ENOENT') {
25
+ return false;
26
+ }
27
+ throw error;
28
+ }
29
+ }
30
+ export function registerInitCommand(program) {
31
+ program
32
+ .command('init')
33
+ .description('Initialise ContextBridge for the current project')
34
+ .action(async () => {
35
+ const projectPath = process.cwd();
36
+ const projectName = path.basename(projectPath);
37
+ const gitRepo = await isGitRepository(projectPath);
38
+ if (!gitRepo) {
39
+ printWarning('No git repository detected in the current directory. Git capture will degrade gracefully.');
40
+ }
41
+ console.log('\nContextBridge works best with an LLM for summarisation.\n');
42
+ console.log(' [1] Anthropic (Claude Haiku) — recommended, fast, accurate');
43
+ console.log(' [2] OpenAI (GPT-4o Mini) — good alternative');
44
+ console.log(' [3] Gemini (Gemini Flash) — Google\'s option');
45
+ console.log(' [4] Ollama (local) — private, no API key needed, requires Ollama installed\n');
46
+ const rl = readline.createInterface({
47
+ input: process.stdin,
48
+ output: process.stdout,
49
+ });
50
+ const choice = await rl.question('Choose a provider [1-4, default: 4]: ');
51
+ let providerName = 'ollama';
52
+ let apiKey;
53
+ if (choice.trim() === '1') {
54
+ providerName = 'anthropic';
55
+ apiKey = await rl.question('Enter your Anthropic API key: ');
56
+ }
57
+ else if (choice.trim() === '2') {
58
+ providerName = 'openai';
59
+ apiKey = await rl.question('Enter your OpenAI API key: ');
60
+ }
61
+ else if (choice.trim() === '3') {
62
+ providerName = 'gemini';
63
+ apiKey = await rl.question('Enter your Gemini API key: ');
64
+ }
65
+ rl.close();
66
+ await setProvider({ provider: providerName, apiKey: apiKey?.trim() || undefined });
67
+ const config = await ensureConfig();
68
+ await registerProjectInConfig(projectName, projectPath);
69
+ await ensureProjectStore(projectPath, projectName);
70
+ const gitignoreUpdated = await ensureAgentsGitignore(projectPath);
71
+ printSuccess('ContextBridge initialised', [
72
+ `Project: ${projectName}`,
73
+ `Config: ${getConfigPath()}`,
74
+ `Store: ${getStorePath()}`,
75
+ `Provider: ${config.provider}`,
76
+ 'Next: cb capture, then cb inject',
77
+ `AGENTS.md gitignored: ${gitignoreUpdated ? 'yes' : 'no .gitignore found'}`,
78
+ ]);
79
+ if (config.provider === 'ollama') {
80
+ printInfo('Make sure Ollama is running before you use `cb capture`. Start it with: ollama serve');
81
+ }
82
+ });
83
+ }
@@ -0,0 +1,63 @@
1
+ import clipboard from 'clipboardy';
2
+ import path from 'node:path';
3
+ import { getHeadEntry, getProjectStore } from '../lib/store.js';
4
+ import { formatContextForInjection, writeAgentsFile } from '../lib/inject.js';
5
+ import { printSuccess, printWarning } from '../utils/display.js';
6
+ function getErrorMessage(error) {
7
+ return error instanceof Error ? error.message : String(error);
8
+ }
9
+ export function registerInjectCommand(program) {
10
+ program
11
+ .command('inject')
12
+ .description('Inject the latest captured context into your next AI tool')
13
+ .option('--codex', 'Output raw context to stdout for piping into codex exec -')
14
+ .option('--no-agents-md', 'Skip writing AGENTS.md')
15
+ .option('--clip-only', 'Only copy to clipboard')
16
+ .action(async (options) => {
17
+ try {
18
+ const projectPath = process.cwd();
19
+ const projectName = path.basename(projectPath);
20
+ const project = await getProjectStore(projectPath);
21
+ const entry = await getHeadEntry(projectPath);
22
+ if (!entry) {
23
+ printWarning('No captured context found for this project. Run `cb capture` first.');
24
+ return;
25
+ }
26
+ const output = await formatContextForInjection({
27
+ projectName,
28
+ entry,
29
+ entries: project?.entries,
30
+ });
31
+ const actions = [];
32
+ const shouldWriteAgents = !options.clipOnly && options.agentsMd !== false;
33
+ if (shouldWriteAgents) {
34
+ const agentsPath = await writeAgentsFile(projectPath, output);
35
+ actions.push(`AGENTS.md written: ${agentsPath}`);
36
+ }
37
+ try {
38
+ await clipboard.write(output);
39
+ actions.push('Clipboard updated');
40
+ }
41
+ catch (error) {
42
+ actions.push(`Clipboard skipped: ${getErrorMessage(error)}`);
43
+ }
44
+ if (options.codex) {
45
+ process.stdout.write(output);
46
+ }
47
+ const confirmation = [
48
+ ...actions,
49
+ 'Paste the copied context into browser-based tools as needed.',
50
+ ];
51
+ if (options.codex) {
52
+ console.error(confirmation.join('\n'));
53
+ }
54
+ else {
55
+ printSuccess('Context injected', confirmation);
56
+ }
57
+ }
58
+ catch (error) {
59
+ printWarning(`Inject failed: ${getErrorMessage(error)}`);
60
+ process.exitCode = 1;
61
+ }
62
+ });
63
+ }
@@ -0,0 +1,38 @@
1
+ import { getProjectStore } from '../lib/store.js';
2
+ import { formatRelativeTime, printPanel, printWarning } from '../utils/display.js';
3
+ export function registerLogCommand(program) {
4
+ program
5
+ .command('log')
6
+ .description('Show version history for captured sessions')
7
+ .option('--limit <number>', 'Show the last N entries', '10')
8
+ .option('--branch <name>', 'Filter entries by git branch')
9
+ .action(async (options) => {
10
+ const projectPath = process.cwd();
11
+ const project = await getProjectStore(projectPath);
12
+ if (!project || project.entries.length === 0) {
13
+ printWarning('No captured sessions found for this project.');
14
+ return;
15
+ }
16
+ const limit = Number.parseInt(options.limit ?? '10', 10);
17
+ const filtered = project.entries
18
+ .filter((entry) => !options.branch || entry.branch === options.branch)
19
+ .sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime())
20
+ .slice(0, Number.isNaN(limit) ? 10 : limit);
21
+ if (filtered.length === 0) {
22
+ printWarning('No captured sessions match the selected filters.');
23
+ return;
24
+ }
25
+ filtered.forEach((entry, index) => {
26
+ const summary = entry.summary;
27
+ const task = summary?.currentFocus ?? 'Unknown';
28
+ const lines = [
29
+ `#${index + 1} ${entry.branch ?? 'unknown'} ${formatRelativeTime(entry.timestamp)}`,
30
+ `Focus: ${task}`,
31
+ `Direction: ${summary?.currentDirection || 'Unknown'}`,
32
+ `Decisions: ${summary?.decisions.slice(0, 3).join(' | ') || 'Unknown'}`,
33
+ `Next steps: ${summary?.nextSteps.slice(0, 3).join(' | ') || 'Unknown'}`,
34
+ ];
35
+ printPanel(lines.join('\n'));
36
+ });
37
+ });
38
+ }
@@ -0,0 +1,49 @@
1
+ import readline from 'node:readline/promises';
2
+ import { setProvider } from '../lib/config.js';
3
+ import { printSuccess, printWarning } from '../utils/display.js';
4
+ function getErrorMessage(error) {
5
+ return error instanceof Error ? error.message : String(error);
6
+ }
7
+ export function registerProviderCommand(program) {
8
+ program
9
+ .command('provider')
10
+ .description('Change the LLM provider for summarisation')
11
+ .action(async () => {
12
+ try {
13
+ console.log('\nContextBridge works best with an LLM for summarisation.\n');
14
+ console.log(' [1] Anthropic (Claude Haiku) — recommended, fast, accurate');
15
+ console.log(' [2] OpenAI (GPT-4o Mini) — good alternative');
16
+ console.log(" [3] Gemini (Gemini Flash) — Google's option");
17
+ console.log(' [4] Ollama (local) — private, no API key needed, requires Ollama installed\n');
18
+ const rl = readline.createInterface({
19
+ input: process.stdin,
20
+ output: process.stdout,
21
+ });
22
+ const choice = await rl.question('Choose a provider [1-4, default: 4]: ');
23
+ let providerName = 'ollama';
24
+ let apiKey;
25
+ if (choice.trim() === '1') {
26
+ providerName = 'anthropic';
27
+ apiKey = await rl.question('Enter your Anthropic API key: ');
28
+ }
29
+ else if (choice.trim() === '2') {
30
+ providerName = 'openai';
31
+ apiKey = await rl.question('Enter your OpenAI API key: ');
32
+ }
33
+ else if (choice.trim() === '3') {
34
+ providerName = 'gemini';
35
+ apiKey = await rl.question('Enter your Gemini API key: ');
36
+ }
37
+ rl.close();
38
+ const config = await setProvider({
39
+ provider: providerName,
40
+ apiKey: apiKey?.trim() || undefined,
41
+ });
42
+ printSuccess('Provider updated', [`New provider: ${config.provider}`]);
43
+ }
44
+ catch (error) {
45
+ printWarning(`Provider update failed: ${getErrorMessage(error)}`);
46
+ process.exitCode = 1;
47
+ }
48
+ });
49
+ }
@@ -0,0 +1,44 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { loadConfig } from '../lib/config.js';
4
+ import { getCurrentBranch } from '../lib/git.js';
5
+ import { getHeadEntry, getProjectStore } from '../lib/store.js';
6
+ import { formatRelativeTime, printInfoPanel } from '../utils/display.js';
7
+ function formatTimeout(timeoutMs) {
8
+ return timeoutMs === null ? 'none' : `${timeoutMs}ms`;
9
+ }
10
+ export function registerStatusCommand(program) {
11
+ program
12
+ .command('status')
13
+ .description('Show the current ContextBridge status for this project')
14
+ .action(async () => {
15
+ const projectPath = process.cwd();
16
+ const projectName = path.basename(projectPath);
17
+ const config = await loadConfig();
18
+ const project = await getProjectStore(projectPath);
19
+ const entry = await getHeadEntry(projectPath);
20
+ const branch = await getCurrentBranch(projectPath);
21
+ let agentsPresent = false;
22
+ try {
23
+ await fs.access(path.join(projectPath, 'AGENTS.md'));
24
+ agentsPresent = true;
25
+ }
26
+ catch {
27
+ agentsPresent = false;
28
+ }
29
+ printInfoPanel('ContextBridge Status', [
30
+ `Project: ${projectName}`,
31
+ `Branch: ${branch ?? 'unknown'}`,
32
+ `Current focus: ${entry?.summary?.currentFocus ?? 'Unknown'}`,
33
+ `Last captured: ${entry ? formatRelativeTime(entry.timestamp) : 'Never'}`,
34
+ `Total sessions: ${project?.entries.length ?? 0}`,
35
+ `AGENTS.md: ${agentsPresent ? 'present' : 'missing'}`,
36
+ `LLM provider: ${config.provider}`,
37
+ `Model: ${config.model ?? 'default'}`,
38
+ ...(config.provider === 'ollama' ? [
39
+ `Ollama URL: ${config.baseUrl ?? 'http://127.0.0.1:11434'}`,
40
+ `Ollama timeout: ${formatTimeout(config.ollamaTimeoutMs)}`,
41
+ ] : []),
42
+ ]);
43
+ });
44
+ }
@@ -0,0 +1,50 @@
1
+ import { loadConfig, setTimeout } from '../lib/config.js';
2
+ import { printInfoPanel, printSuccess, printWarning } from '../utils/display.js';
3
+ function formatTimeout(timeoutMs) {
4
+ return timeoutMs === null ? 'none' : `${timeoutMs}ms`;
5
+ }
6
+ function parseTimeout(input) {
7
+ const normalised = input.trim().toLowerCase();
8
+ if (normalised === 'none' || normalised === 'off' || normalised === 'disabled') {
9
+ return null;
10
+ }
11
+ const parsed = Number.parseInt(normalised, 10);
12
+ if (Number.isNaN(parsed) || parsed <= 0) {
13
+ throw new Error('Timeout must be a positive number of milliseconds or `none`.');
14
+ }
15
+ return parsed;
16
+ }
17
+ function getErrorMessage(error) {
18
+ return error instanceof Error ? error.message : String(error);
19
+ }
20
+ export function registerTimeoutCommand(program) {
21
+ program
22
+ .command('timeout')
23
+ .description('Show or set the Ollama summarisation timeout (Ollama provider only)')
24
+ .argument('[value]', 'Timeout in milliseconds, or `none` to disable the timeout entirely')
25
+ .action(async (value) => {
26
+ try {
27
+ const config = await loadConfig();
28
+ if (config.provider !== 'ollama') {
29
+ printWarning('Timeout settings only apply when the Ollama provider is active.');
30
+ return;
31
+ }
32
+ if (!value) {
33
+ printInfoPanel('Ollama Timeout', [
34
+ `Current timeout: ${formatTimeout(config.ollamaTimeoutMs)}`,
35
+ 'Set a new value with `cb timeout <milliseconds>` or disable it with `cb timeout none`.',
36
+ ]);
37
+ return;
38
+ }
39
+ const timeoutMs = parseTimeout(value);
40
+ const updatedConfig = await setTimeout(timeoutMs);
41
+ printSuccess('Timeout updated', [
42
+ `Ollama timeout: ${formatTimeout(updatedConfig.ollamaTimeoutMs)}`,
43
+ ]);
44
+ }
45
+ catch (error) {
46
+ printWarning(`Timeout update failed: ${getErrorMessage(error)}`);
47
+ process.exitCode = 1;
48
+ }
49
+ });
50
+ }
File without changes
package/dist/index.js ADDED
@@ -0,0 +1,26 @@
1
+ #!/usr/bin/env node
2
+ import { Command } from 'commander';
3
+ import { registerInitCommand } from './commands/init.js';
4
+ import { registerCaptureCommand } from './commands/capture.js';
5
+ import { registerInjectCommand } from './commands/inject.js';
6
+ import { registerLogCommand } from './commands/log.js';
7
+ import { registerStatusCommand } from './commands/status.js';
8
+ import { registerTimeoutCommand } from './commands/timeout.js';
9
+ import { registerProviderCommand } from './commands/provider.js';
10
+ const program = new Command();
11
+ program
12
+ .name('cb')
13
+ .description('Persistent, versioned AI project context for every coding tool you use.')
14
+ .version('0.1.0');
15
+ registerInitCommand(program);
16
+ registerCaptureCommand(program);
17
+ registerInjectCommand(program);
18
+ registerLogCommand(program);
19
+ registerStatusCommand(program);
20
+ registerTimeoutCommand(program);
21
+ registerProviderCommand(program);
22
+ program.parseAsync(process.argv).catch((error) => {
23
+ const message = error instanceof Error ? error.message : String(error);
24
+ console.error(`ContextBridge failed: ${message}`);
25
+ process.exitCode = 1;
26
+ });
@@ -0,0 +1,75 @@
1
+ import fs from 'node:fs/promises';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+ const CONFIG_DIR = path.join(os.homedir(), '.contextbridge');
5
+ const CONFIG_PATH = path.join(CONFIG_DIR, 'config.json');
6
+ const DEFAULT_CONFIG = {
7
+ provider: 'ollama',
8
+ ollamaTimeoutMs: 120000,
9
+ projects: {},
10
+ };
11
+ function getErrorCode(error) {
12
+ return error instanceof Error && 'code' in error
13
+ ? String(error.code)
14
+ : undefined;
15
+ }
16
+ export function getConfigDir() {
17
+ return CONFIG_DIR;
18
+ }
19
+ export function getConfigPath() {
20
+ return CONFIG_PATH;
21
+ }
22
+ export async function saveConfig(config) {
23
+ await fs.mkdir(CONFIG_DIR, { recursive: true });
24
+ await fs.writeFile(CONFIG_PATH, `${JSON.stringify(config, null, 2)}\n`, 'utf8');
25
+ }
26
+ export async function ensureConfig() {
27
+ await fs.mkdir(CONFIG_DIR, { recursive: true });
28
+ try {
29
+ const existing = await fs.readFile(CONFIG_PATH, 'utf8');
30
+ const parsed = JSON.parse(existing);
31
+ const merged = {
32
+ ...DEFAULT_CONFIG,
33
+ ...parsed,
34
+ projects: {
35
+ ...DEFAULT_CONFIG.projects,
36
+ ...(parsed.projects ?? {}),
37
+ },
38
+ };
39
+ if (JSON.stringify(merged) !== JSON.stringify(parsed)) {
40
+ await saveConfig(merged);
41
+ }
42
+ return merged;
43
+ }
44
+ catch (error) {
45
+ if (getErrorCode(error) !== 'ENOENT') {
46
+ throw error;
47
+ }
48
+ await saveConfig(DEFAULT_CONFIG);
49
+ return DEFAULT_CONFIG;
50
+ }
51
+ }
52
+ export async function loadConfig() {
53
+ return ensureConfig();
54
+ }
55
+ export async function registerProjectInConfig(projectName, projectPath) {
56
+ const config = await ensureConfig();
57
+ config.projects[projectName] = projectPath;
58
+ await saveConfig(config);
59
+ return config;
60
+ }
61
+ export async function setProvider(providerConfig) {
62
+ const config = await ensureConfig();
63
+ config.provider = providerConfig.provider;
64
+ config.apiKey = providerConfig.apiKey;
65
+ config.model = providerConfig.model;
66
+ config.baseUrl = providerConfig.baseUrl;
67
+ await saveConfig(config);
68
+ return config;
69
+ }
70
+ export async function setTimeout(timeoutMs) {
71
+ const config = await ensureConfig();
72
+ config.ollamaTimeoutMs = timeoutMs;
73
+ await saveConfig(config);
74
+ return config;
75
+ }