@contextai-core/cli 0.0.2 → 0.1.1
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/MCP_GUIDE.md +61 -0
- package/README.md +17 -29
- package/index.js +78 -0
- package/package.json +14 -4
- package/src/actions/init.js +51 -56
- package/src/actions/install.js +3 -3
- package/src/logic/file-ops.js +35 -0
- package/src/logic/patch-ops.js +33 -0
- package/src/logic/read-ops.js +74 -0
- package/src/logic/run-ops.js +45 -0
- package/src/mcp-server.js +189 -0
- package/src/utils/supabase.js +16 -8
package/MCP_GUIDE.md
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# MCP & Agent Integration Guide
|
|
2
|
+
|
|
3
|
+
> **Purpose**: Enable AI Agents (Cursor, Claude Desktop, Windsurf) to use ContextAI's "Smart Tools" to enforce protocol compliance automatically.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## 1. The "CLI Bridge" (Universal Access)
|
|
8
|
+
Since native MCP support varies by IDE, the most reliable integration today is the **CLI Bridge**.
|
|
9
|
+
The `contextai` CLI now exposes three "Agent-Aware" commands.
|
|
10
|
+
|
|
11
|
+
### Available Tools
|
|
12
|
+
| Command | Description | Protocol Action |
|
|
13
|
+
|---------|-------------|-----------------|
|
|
14
|
+
| `contextai write <path> <content>` | Writes file to disk | ✅ Auto-updates `changelog.md` and `manifest.json` |
|
|
15
|
+
| `contextai patch <path> <search> <replace>` | **[NEW]** Smart Search & Replace | ✅ Efficiently edits large files |
|
|
16
|
+
| `contextai read <path> [-l 1-50]` | Reads file (Supports chunks) | ✅ Injects `@uses` context (zero-shot) |
|
|
17
|
+
| `contextai run <cmd>` | Executs shell command | ✅ Auto-updates `dependencies.md` |
|
|
18
|
+
|
|
19
|
+
### Usage for Agents
|
|
20
|
+
**Cursor / VS Code**:
|
|
21
|
+
> "When editing files, PREFER `contextai patch` for small changes to avoid rewriting the whole file.
|
|
22
|
+
> Use `contextai read -l 100-200 file.ts` to read specific lines if the file is large."
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## 2. Native MCP Server (Official Support)
|
|
27
|
+
For environments that support the full Model Context Protocol (like **Claude Desktop** or **Smithery**), you can configure ContextAI as a native server.
|
|
28
|
+
|
|
29
|
+
### Claude Desktop Configuration
|
|
30
|
+
Add this to your `claude_desktop_config.json`:
|
|
31
|
+
|
|
32
|
+
```json
|
|
33
|
+
{
|
|
34
|
+
"mcpServers": {
|
|
35
|
+
"contextai": {
|
|
36
|
+
"command": "npx",
|
|
37
|
+
"args": ["-y", "@contextai-core/cli", "mcp"]
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### How it works
|
|
44
|
+
1. Claude launches the ContextAI CLI in "Server Mode".
|
|
45
|
+
2. It uses `stdio` to communicate.
|
|
46
|
+
3. Claude will see tools like `smart_write_file` and `read_contextual_file` available in its tool list.
|
|
47
|
+
4. When Claude uses these tools, the **Protocol compliance** (changelogs, etc.) happens automatically.
|
|
48
|
+
|
|
49
|
+
## 3. Why Use This?
|
|
50
|
+
1. **Stop "Janitor Work"**: You (the agent) no longer need to manually open `changelog.md` and append a line after every edit. The tool does it for you.
|
|
51
|
+
2. **Zero-Shot Context**: When you read a component, the tool gives you the hooks it uses. You don't have to search for them.
|
|
52
|
+
3. **Safety**: The `run` command prevents accidental `rm -rf` or destructive actions.
|
|
53
|
+
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
## 4. Verification
|
|
57
|
+
To verify the bridge is working, run:
|
|
58
|
+
```bash
|
|
59
|
+
npx contextai read src/App.tsx
|
|
60
|
+
```
|
|
61
|
+
If you see `--- CONTEXT INJECTION ---` in the output, the Agent Bridge is active.
|
package/README.md
CHANGED
|
@@ -1,38 +1,26 @@
|
|
|
1
|
-
# ContextAI CLI
|
|
1
|
+
# ContextAI CLI (v0.1.0)
|
|
2
2
|
|
|
3
|
-
Give your AI
|
|
3
|
+
**Give your AI Agents Persistent Memory.**
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
ContextAI provides "Smart Tools" that allow agents (like Cursor, Windsurf, Claude) to interact with your codebase while automatically maintaining project context.
|
|
6
6
|
|
|
7
|
+
## Installation
|
|
7
8
|
```bash
|
|
8
|
-
|
|
9
|
-
npx @contextai-core/cli init
|
|
10
|
-
|
|
11
|
-
# Install a context pack
|
|
12
|
-
npx @contextai-core/cli install nextjs-supabase
|
|
13
|
-
|
|
14
|
-
# Login (for paid packs)
|
|
15
|
-
npx @contextai-core/cli login
|
|
9
|
+
npm install -g @contextai-core/cli
|
|
16
10
|
```
|
|
17
11
|
|
|
18
|
-
##
|
|
19
|
-
|
|
20
|
-
AI coding assistants forget your project every session. ContextAI fixes this by giving them a "shared brain" — a `.ai/` folder with your project's context, patterns, and preferences.
|
|
21
|
-
|
|
22
|
-
## Commands
|
|
23
|
-
|
|
24
|
-
| Command | Description |
|
|
25
|
-
|---------|-------------|
|
|
26
|
-
| `init` | Create `.ai/` folder in your project |
|
|
27
|
-
| `install <pack>` | Install a context pack |
|
|
28
|
-
| `login` | Authenticate with ContextAI |
|
|
29
|
-
| `doctor` | Validate your `.ai/` setup |
|
|
30
|
-
|
|
31
|
-
## Browse Packs
|
|
12
|
+
## Features for Agents (The "Bridge")
|
|
13
|
+
This CLI includes an MCP-Compliant "Bridge" that enforces project protocols.
|
|
32
14
|
|
|
33
|
-
|
|
15
|
+
- **`npx @contextai-core/cli write`**: Writes files + Updates Changelog/Manifest.
|
|
16
|
+
- **`npx @contextai-core/cli read`**: Reads files + Injects Hook Context.
|
|
17
|
+
- **`npx @contextai-core/cli patch`**: Smart Search & Replace.
|
|
18
|
+
- **`npx @contextai-core/cli run`**: Safe Command Execution.
|
|
19
|
+
- **`npx @contextai-core/cli mcp`**: Launches a Native MCP Server (stdio) for Claude Desktop.
|
|
34
20
|
|
|
35
|
-
##
|
|
21
|
+
## Usage
|
|
22
|
+
See `MCP_GUIDE.md` (included in this package) for detailed integration instructions for your IDE.
|
|
36
23
|
|
|
37
|
-
|
|
38
|
-
- [
|
|
24
|
+
## Links
|
|
25
|
+
- [Documentation](https://contextai.com/docs)
|
|
26
|
+
- [Context Packs](https://contextai.com/packs)
|
package/index.js
CHANGED
|
@@ -11,7 +11,12 @@ import { login, logout, whoami } from './src/actions/auth.js';
|
|
|
11
11
|
import { init } from './src/actions/init.js';
|
|
12
12
|
import { publish } from './src/actions/publish.js';
|
|
13
13
|
import { installPackage } from './src/actions/install.js';
|
|
14
|
+
|
|
14
15
|
import { doctor } from './src/actions/doctor.js';
|
|
16
|
+
import { smartWriteFile } from './src/logic/file-ops.js';
|
|
17
|
+
import { smartReadFile } from './src/logic/read-ops.js';
|
|
18
|
+
import { smartRunCommand } from './src/logic/run-ops.js';
|
|
19
|
+
import { smartPatchFile } from './src/logic/patch-ops.js';
|
|
15
20
|
|
|
16
21
|
// Helper to load env from local .env for testing
|
|
17
22
|
if (fs.existsSync('.env')) {
|
|
@@ -99,6 +104,26 @@ program
|
|
|
99
104
|
|
|
100
105
|
program.command('publish').description('Publish a context package to the registry').action(publish);
|
|
101
106
|
|
|
107
|
+
// AGENT BRIDGE COMMANDS
|
|
108
|
+
program
|
|
109
|
+
.command('write <file_path> [content]')
|
|
110
|
+
.description('Write file and auto-log to changelog (Agent Bridge)')
|
|
111
|
+
.option('-m, --message <reason>', 'Reason for the change', 'Automated update')
|
|
112
|
+
.action(async (filePath, content, options) => {
|
|
113
|
+
try {
|
|
114
|
+
if (!content) {
|
|
115
|
+
console.error(chalk.red('Error: Content argument is required.'));
|
|
116
|
+
process.exit(1);
|
|
117
|
+
}
|
|
118
|
+
const result = await smartWriteFile(filePath, content, options.message);
|
|
119
|
+
console.log(chalk.green(`✅ File written: ${result.path}`));
|
|
120
|
+
console.log(chalk.gray(' Changelog updated automatically.'));
|
|
121
|
+
} catch (e) {
|
|
122
|
+
console.error(chalk.red(`Error: ${e.message}`));
|
|
123
|
+
process.exit(1);
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
|
|
102
127
|
// LEGACY COMMANDS
|
|
103
128
|
program
|
|
104
129
|
.command('pull <slug>')
|
|
@@ -109,4 +134,57 @@ program
|
|
|
109
134
|
await installPackage(slug);
|
|
110
135
|
});
|
|
111
136
|
|
|
137
|
+
program
|
|
138
|
+
.command('read <file_path>')
|
|
139
|
+
.description('Read file with context injection (Agent Bridge)')
|
|
140
|
+
.option('-l, --lines <range>', 'Line range (e.g. 1-50)')
|
|
141
|
+
.action(async (filePath, options) => {
|
|
142
|
+
try {
|
|
143
|
+
let readOpts = {};
|
|
144
|
+
if (options.lines) {
|
|
145
|
+
const [start, end] = options.lines.split('-').map(Number);
|
|
146
|
+
readOpts = { startLine: start, endLine: end };
|
|
147
|
+
}
|
|
148
|
+
const result = await smartReadFile(filePath, readOpts);
|
|
149
|
+
console.log(result.fullOutput);
|
|
150
|
+
} catch (e) {
|
|
151
|
+
console.error(chalk.red(`Error: ${e.message}`));
|
|
152
|
+
process.exit(1);
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
program
|
|
157
|
+
.command('patch <file_path> <search_string> <replace_string>')
|
|
158
|
+
.description('Search and Replace content (Agent Bridge)')
|
|
159
|
+
.option('-m, --message <reason>', 'Reason for the change', 'Automated patch')
|
|
160
|
+
.action(async (filePath, search, replace, options) => {
|
|
161
|
+
try {
|
|
162
|
+
await smartPatchFile(filePath, search, replace, options.message);
|
|
163
|
+
console.log(chalk.green(`✅ File patched: ${filePath}`));
|
|
164
|
+
} catch (e) {
|
|
165
|
+
console.error(chalk.red(`Patch Error: ${e.message}`));
|
|
166
|
+
process.exit(1);
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
program
|
|
171
|
+
.command('run <command_string>')
|
|
172
|
+
.description('Run shell command with auto-documentation (Agent Bridge)')
|
|
173
|
+
.action(async (commandString) => {
|
|
174
|
+
try {
|
|
175
|
+
await smartRunCommand(commandString);
|
|
176
|
+
} catch (e) {
|
|
177
|
+
console.error(chalk.red(`Error: ${e.message}`));
|
|
178
|
+
process.exit(1);
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
program
|
|
183
|
+
.command('mcp')
|
|
184
|
+
.description('Start Model Context Protocol server (stdio)')
|
|
185
|
+
.action(async () => {
|
|
186
|
+
// Dynamic import to avoid loading MCP SDK on normal commands (faster init)
|
|
187
|
+
await import('./src/mcp-server.js');
|
|
188
|
+
});
|
|
189
|
+
|
|
112
190
|
program.parse(process.argv);
|
package/package.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@contextai-core/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "Give your AI agents persistent memory. Install pre-built context packs for Cursor, Windsurf, Copilot & more.",
|
|
5
5
|
"author": "ContextAI",
|
|
6
|
-
"license": "
|
|
6
|
+
"license": "ISC",
|
|
7
7
|
"keywords": [
|
|
8
8
|
"ai",
|
|
9
9
|
"context",
|
|
@@ -15,14 +15,23 @@
|
|
|
15
15
|
"chatgpt",
|
|
16
16
|
"agents",
|
|
17
17
|
"memory",
|
|
18
|
-
"cli"
|
|
18
|
+
"cli",
|
|
19
|
+
"mcp"
|
|
19
20
|
],
|
|
20
21
|
"main": "index.js",
|
|
21
22
|
"bin": {
|
|
22
23
|
"contextai": "index.js"
|
|
23
24
|
},
|
|
24
25
|
"type": "module",
|
|
26
|
+
"files": [
|
|
27
|
+
"index.js",
|
|
28
|
+
"src",
|
|
29
|
+
"MCP_GUIDE.md",
|
|
30
|
+
"README.md"
|
|
31
|
+
],
|
|
32
|
+
"type": "module",
|
|
25
33
|
"dependencies": {
|
|
34
|
+
"@modelcontextprotocol/sdk": "^1.25.1",
|
|
26
35
|
"@supabase/supabase-js": "^2.89.0",
|
|
27
36
|
"adm-zip": "^0.5.16",
|
|
28
37
|
"chalk": "^5.3.0",
|
|
@@ -30,6 +39,7 @@
|
|
|
30
39
|
"figlet": "^1.7.0",
|
|
31
40
|
"node-fetch": "^3.3.2",
|
|
32
41
|
"open": "^11.0.0",
|
|
33
|
-
"ora": "^8.0.0"
|
|
42
|
+
"ora": "^8.0.0",
|
|
43
|
+
"zod": "^4.2.1"
|
|
34
44
|
}
|
|
35
45
|
}
|
package/src/actions/init.js
CHANGED
|
@@ -1,62 +1,57 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
3
|
-
import
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
1
|
+
import { Action } from "commander";
|
|
2
|
+
import chalk from "chalk";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import fs from "fs";
|
|
5
|
+
import { installPackage } from "./install.js";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Initializes the ContextAI Shared Brain (.ai) by installing the Core UCP.
|
|
9
|
+
*/
|
|
10
|
+
export async function init() {
|
|
11
|
+
console.log(chalk.green('🧠 Initializing ContextAI Shared Brain...'));
|
|
7
12
|
const aiDir = path.join(process.cwd(), '.ai');
|
|
8
|
-
const ucpDir = path.join(aiDir, 'ucp');
|
|
9
|
-
const manifestPath = path.join(aiDir, 'context.json');
|
|
10
|
-
const bootPath = path.join(aiDir, 'boot.md');
|
|
11
|
-
|
|
12
|
-
if (fs.existsSync(aiDir)) {
|
|
13
|
-
// Check if it's legacy
|
|
14
|
-
if (!fs.existsSync(ucpDir) && fs.existsSync(path.join(aiDir, 'context'))) {
|
|
15
|
-
console.log(chalk.yellow('⚠️ Legacy .ai/ structure detected. Migration is recommended but not yet auto-implemented.'));
|
|
16
|
-
console.log(chalk.yellow(' Please start fresh or manually move contents to .ai/ucp/'));
|
|
17
|
-
return;
|
|
18
|
-
}
|
|
19
13
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
14
|
+
// 1. Create .ai directory if missing
|
|
15
|
+
if (!fs.existsSync(aiDir)) {
|
|
16
|
+
fs.mkdirSync(aiDir);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// 2. Check overlap
|
|
20
|
+
if (fs.existsSync(path.join(aiDir, 'ucp'))) {
|
|
21
|
+
console.log(chalk.yellow('⚠️ .ai/ucp already exists. Skipping core installation.'));
|
|
22
|
+
return;
|
|
24
23
|
}
|
|
25
24
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
25
|
+
try {
|
|
26
|
+
// 3. Delegate to Install Action
|
|
27
|
+
// "universal-context-protocol" should be the slug of the Core UCP in our DB.
|
|
28
|
+
// For now, if that pack doesn't exist, we fallback to the "seed" method?
|
|
29
|
+
// NO, we should enforce the "Git/Marketplace" model.
|
|
30
|
+
|
|
31
|
+
console.log(chalk.blue('⬇️ Downloading Universal Context Protocol (Core OS)...'));
|
|
32
|
+
|
|
33
|
+
// We call installPackage directly.
|
|
34
|
+
// We assume "universal-context-protocol" is free and public.
|
|
35
|
+
await installPackage("universal-context-protocol", {});
|
|
36
|
+
|
|
37
|
+
// 4. Create Bootloader (Traffic Cop) if missing
|
|
38
|
+
const bootPath = path.join(aiDir, 'boot.md');
|
|
39
|
+
if (!fs.existsSync(bootPath)) {
|
|
40
|
+
fs.writeFileSync(
|
|
41
|
+
bootPath,
|
|
42
|
+
`# AI Context Container\n\nThis project uses multiple context protocols.\n\n## Active Protocols\n1. **UCP** (Core OS): [Unified Context Protocol](./ucp/README.md)\n - Use for: General Project Context, Workflows.\n`
|
|
43
|
+
);
|
|
44
44
|
}
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
'
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
console.log(chalk.green('✅ Successfully initialized Shared Brain (.ai/)!'));
|
|
59
|
-
console.log(chalk.gray(' - .ai/boot.md (Traffic Cop)'));
|
|
60
|
-
console.log(chalk.gray(' - .ai/context.json (Manifest)'));
|
|
61
|
-
console.log(chalk.gray(' - .ai/ucp/ (Core Protocol)'));
|
|
45
|
+
|
|
46
|
+
console.log(chalk.green('\n✅ Initialization Complete!'));
|
|
47
|
+
console.log(chalk.gray(' Your agent now has a brain in .ai/'));
|
|
48
|
+
|
|
49
|
+
} catch (error) {
|
|
50
|
+
console.error(chalk.red(`\n❌ Init Failed: ${error.message}`));
|
|
51
|
+
console.error(chalk.yellow(' Ensure you have internet connection and the "universal-context-protocol" pack is live.'));
|
|
52
|
+
// Fallback or exit?
|
|
53
|
+
// For resilience, maybe write a minimal skeleton if download fails?
|
|
54
|
+
// User asked to "download from live ucp template link".
|
|
55
|
+
process.exit(1);
|
|
56
|
+
}
|
|
62
57
|
}
|
package/src/actions/install.js
CHANGED
|
@@ -6,7 +6,7 @@ import AdmZip from 'adm-zip';
|
|
|
6
6
|
import os from 'os';
|
|
7
7
|
import { copyRecursiveSync } from '../utils/fs.js';
|
|
8
8
|
import ora from 'ora';
|
|
9
|
-
import {
|
|
9
|
+
import { getSupabase } from '../utils/supabase.js';
|
|
10
10
|
|
|
11
11
|
// Load saved credentials
|
|
12
12
|
function loadCredentials() {
|
|
@@ -33,7 +33,7 @@ export async function installPackage(packageSlug, options = {}) {
|
|
|
33
33
|
// 1. Fetch pack from Supabase
|
|
34
34
|
spinner.text = `Fetching pack info for ${packageSlug}...`;
|
|
35
35
|
|
|
36
|
-
const { data:
|
|
36
|
+
const { data: purchaseData, error: purchaseError } = await getSupabase()
|
|
37
37
|
.from('packs')
|
|
38
38
|
.select('*')
|
|
39
39
|
.eq('slug', packageSlug)
|
|
@@ -95,7 +95,7 @@ export async function installPackage(packageSlug, options = {}) {
|
|
|
95
95
|
// Free Pack - Enforce Limits
|
|
96
96
|
const creds = loadCredentials();
|
|
97
97
|
if (creds) {
|
|
98
|
-
const { data
|
|
98
|
+
const { data, error } = await getSupabase()
|
|
99
99
|
.from('profiles')
|
|
100
100
|
.select('download_count')
|
|
101
101
|
.eq('id', creds.user_id)
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Smart file writer that also updates the changelog and manifest.
|
|
7
|
+
* Used by both CLI (contextai write) and MCP (smart_write_file).
|
|
8
|
+
*/
|
|
9
|
+
export async function smartWriteFile(filePath, content, reason) {
|
|
10
|
+
const fullPath = path.resolve(process.cwd(), filePath);
|
|
11
|
+
const dir = path.dirname(fullPath);
|
|
12
|
+
|
|
13
|
+
// 1. Write the file
|
|
14
|
+
if (!fs.existsSync(dir)) {
|
|
15
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
16
|
+
}
|
|
17
|
+
fs.writeFileSync(fullPath, content, 'utf8');
|
|
18
|
+
|
|
19
|
+
// 2. Update Changelog
|
|
20
|
+
const aiDir = path.join(process.cwd(), '.ai', 'context');
|
|
21
|
+
const changelogPath = path.join(aiDir, 'changelog.md');
|
|
22
|
+
|
|
23
|
+
if (fs.existsSync(changelogPath)) {
|
|
24
|
+
const today = new Date().toISOString().split('T')[0];
|
|
25
|
+
const entry = `| ${today} | Agent | ${reason} |`;
|
|
26
|
+
|
|
27
|
+
let changelog = fs.readFileSync(changelogPath, 'utf8');
|
|
28
|
+
// append to table (simplistic approach: append before last empty line or at end)
|
|
29
|
+
changelog += `\n${entry}`;
|
|
30
|
+
|
|
31
|
+
fs.writeFileSync(changelogPath, changelog, 'utf8');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return { success: true, path: fullPath };
|
|
35
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import { smartWriteFile } from './file-ops.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Smart file patcher (Search & Replace).
|
|
8
|
+
* Used by both CLI (contextai patch) and MCP (core_patch).
|
|
9
|
+
*/
|
|
10
|
+
export async function smartPatchFile(filePath, searchContent, replaceContent, reason) {
|
|
11
|
+
const fullPath = path.resolve(process.cwd(), filePath);
|
|
12
|
+
|
|
13
|
+
if (!fs.existsSync(fullPath)) {
|
|
14
|
+
throw new Error(`File not found: ${filePath}`);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const fileContent = fs.readFileSync(fullPath, 'utf8');
|
|
18
|
+
|
|
19
|
+
// 1. Locate searchContent
|
|
20
|
+
// Normalize line endings? For now, exact match.
|
|
21
|
+
if (!fileContent.includes(searchContent)) {
|
|
22
|
+
// Try trimming?
|
|
23
|
+
if (!fileContent.includes(searchContent.trim())) {
|
|
24
|
+
throw new Error(`Search content not found in ${filePath}`);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// 2. Perform Release
|
|
29
|
+
const newContent = fileContent.replace(searchContent, replaceContent);
|
|
30
|
+
|
|
31
|
+
// 3. Delegate to smartWriteFile to handle Changelog/Manifest
|
|
32
|
+
return await smartWriteFile(filePath, newContent, reason || "Applied patch");
|
|
33
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Smart file reader that injects related context.
|
|
6
|
+
* Used by both CLI (contextai read) and MCP (read_contextual_file).
|
|
7
|
+
*
|
|
8
|
+
* @param {string} filePath - Path to file
|
|
9
|
+
* @param {Object} options - { startLine, endLine } (1-indexed)
|
|
10
|
+
*/
|
|
11
|
+
export async function smartReadFile(filePath, options = {}) {
|
|
12
|
+
const fullPath = path.resolve(process.cwd(), filePath);
|
|
13
|
+
|
|
14
|
+
if (!fs.existsSync(fullPath)) {
|
|
15
|
+
throw new Error(`File not found: ${filePath}`);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const fileContent = fs.readFileSync(fullPath, 'utf8');
|
|
19
|
+
const lines = fileContent.split('\n');
|
|
20
|
+
|
|
21
|
+
// Handle Chunking
|
|
22
|
+
let content = fileContent;
|
|
23
|
+
let isPartial = false;
|
|
24
|
+
|
|
25
|
+
if (options.startLine || options.endLine) {
|
|
26
|
+
const start = (options.startLine || 1) - 1;
|
|
27
|
+
const end = options.endLine || lines.length;
|
|
28
|
+
content = lines.slice(start, end).join('\n');
|
|
29
|
+
isPartial = true;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// 1. Analyze for @uses tags (e.g., @uses useDashboard)
|
|
33
|
+
// ONLY do this if we are reading the header or full file
|
|
34
|
+
// Heuristic: If we are reading line 100+, we probably miss the header, so skip context injection?
|
|
35
|
+
// Actually, user might want context even when reading a chunk. Let's keep it simple: finding tags in the CHUNK.
|
|
36
|
+
|
|
37
|
+
const usesRegex = /@uses\s+(\w+)/g;
|
|
38
|
+
const matches = [...content.matchAll(usesRegex)];
|
|
39
|
+
|
|
40
|
+
let contextInjection = '';
|
|
41
|
+
|
|
42
|
+
if (matches.length > 0) {
|
|
43
|
+
contextInjection += `\n\n--- CONTEXT INJECTION ---\nThe following hooks are used in this snippet:\n`;
|
|
44
|
+
|
|
45
|
+
// Find hooks directory
|
|
46
|
+
const hooksDir = path.join(process.cwd(), 'src', 'hooks'); // specific to this project structure
|
|
47
|
+
const adminHooksDir = path.join(process.cwd(), 'admin-dashboard', 'src', 'hooks');
|
|
48
|
+
|
|
49
|
+
for (const match of matches) {
|
|
50
|
+
const hookName = match[1];
|
|
51
|
+
// Try to find the hook file
|
|
52
|
+
const hookFile = `${hookName}.ts`;
|
|
53
|
+
|
|
54
|
+
let hookPath = null;
|
|
55
|
+
if (fs.existsSync(path.join(hooksDir, hookFile))) {
|
|
56
|
+
hookPath = path.join(hooksDir, hookFile);
|
|
57
|
+
} else if (fs.existsSync(path.join(adminHooksDir, hookFile))) {
|
|
58
|
+
hookPath = path.join(adminHooksDir, hookFile);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (hookPath) {
|
|
62
|
+
const hookContent = fs.readFileSync(hookPath, 'utf8');
|
|
63
|
+
contextInjection += `\n\n// Source: ${hookName}.ts\n${hookContent}\n`;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return {
|
|
69
|
+
content,
|
|
70
|
+
context: contextInjection,
|
|
71
|
+
fullOutput: content + contextInjection,
|
|
72
|
+
isPartial
|
|
73
|
+
};
|
|
74
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { execSync } from 'child_process';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import chalk from 'chalk';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Smart command runner that updates dependencies doc.
|
|
8
|
+
* Used by both CLI (contextai run) and MCP (run_safe_command).
|
|
9
|
+
*/
|
|
10
|
+
export async function smartRunCommand(command) {
|
|
11
|
+
console.log(chalk.gray(`> ${command}`));
|
|
12
|
+
|
|
13
|
+
// 1. Safety Check (Yellow List)
|
|
14
|
+
const dangerousCommands = ['rm', 'shutdown', 'reboot', ':(){ :|:& };:'];
|
|
15
|
+
const cmdBase = command.split(' ')[0];
|
|
16
|
+
|
|
17
|
+
if (dangerousCommands.includes(cmdBase)) {
|
|
18
|
+
throw new Error(`Command '${cmdBase}' is blocked by Safe Mode.`);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// 2. Execute
|
|
22
|
+
try {
|
|
23
|
+
const output = execSync(command, { encoding: 'utf8', stdio: 'inherit' });
|
|
24
|
+
|
|
25
|
+
// 3. Auto-Doc Dependencies
|
|
26
|
+
if (command.includes('npm install') || command.includes('yarn add') || command.includes('pnpm add')) {
|
|
27
|
+
const aiDir = path.join(process.cwd(), '.ai', 'context');
|
|
28
|
+
if (fs.existsSync(aiDir)) {
|
|
29
|
+
const depsPath = path.join(aiDir, 'dependencies.md');
|
|
30
|
+
const timestamp = new Date().toISOString();
|
|
31
|
+
const logLine = `- **${timestamp}**: Added dependencies via \`${command}\`\n`;
|
|
32
|
+
|
|
33
|
+
if (fs.existsSync(depsPath)) {
|
|
34
|
+
fs.appendFileSync(depsPath, logLine);
|
|
35
|
+
} else {
|
|
36
|
+
fs.writeFileSync(depsPath, `# Dependencies history\n\n${logLine}`);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return { success: true, output };
|
|
42
|
+
} catch (e) {
|
|
43
|
+
throw e;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
3
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
|
+
import {
|
|
5
|
+
CallToolRequestSchema,
|
|
6
|
+
ListToolsRequestSchema,
|
|
7
|
+
} from "@modelcontextprotocol/sdk/types.js";
|
|
8
|
+
import { z } from "zod";
|
|
9
|
+
import chalk from "chalk";
|
|
10
|
+
|
|
11
|
+
// Shared Logic
|
|
12
|
+
import { smartWriteFile } from "./logic/file-ops.js";
|
|
13
|
+
import { smartReadFile } from "./logic/read-ops.js";
|
|
14
|
+
import { smartRunCommand } from "./logic/run-ops.js";
|
|
15
|
+
import { smartPatchFile } from "./logic/patch-ops.js";
|
|
16
|
+
|
|
17
|
+
// Initialize Server
|
|
18
|
+
const server = new Server(
|
|
19
|
+
{
|
|
20
|
+
name: "contextai-server",
|
|
21
|
+
version: "1.1.0",
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
capabilities: {
|
|
25
|
+
tools: {},
|
|
26
|
+
},
|
|
27
|
+
}
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
// Tool Categorization
|
|
31
|
+
const GENERIC = {
|
|
32
|
+
WRITE: "core_write_file",
|
|
33
|
+
READ: "core_read_file",
|
|
34
|
+
RUN: "core_run_command",
|
|
35
|
+
PATCH: "core_patch_file",
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
// 1. List Tools
|
|
39
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
40
|
+
return {
|
|
41
|
+
tools: [
|
|
42
|
+
{
|
|
43
|
+
name: GENERIC.WRITE,
|
|
44
|
+
description:
|
|
45
|
+
"[GENERIC] Writes a file to disk AND automatically updates the changelog and manifest. Use this instead of standard file writing.",
|
|
46
|
+
inputSchema: {
|
|
47
|
+
type: "object",
|
|
48
|
+
properties: {
|
|
49
|
+
path: { type: "string", description: "Relative path to file" },
|
|
50
|
+
content: { type: "string", description: "Content to write" },
|
|
51
|
+
reason: {
|
|
52
|
+
type: "string",
|
|
53
|
+
description: "Reason for change (logs to changelog)",
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
required: ["path", "content"],
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
name: GENERIC.PATCH,
|
|
61
|
+
description:
|
|
62
|
+
"[GENERIC] Search and Replace content in a file. Use this for small edits to large files to save context tokens.",
|
|
63
|
+
inputSchema: {
|
|
64
|
+
type: "object",
|
|
65
|
+
properties: {
|
|
66
|
+
path: { type: "string", description: "Relative path to file" },
|
|
67
|
+
search: { type: "string", description: "Exact string to find" },
|
|
68
|
+
replace: { type: "string", description: "String to replace with" },
|
|
69
|
+
reason: { type: "string", description: "Reason for change" },
|
|
70
|
+
},
|
|
71
|
+
required: ["path", "search", "replace"],
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
name: GENERIC.READ,
|
|
76
|
+
description:
|
|
77
|
+
"[GENERIC] Reads a file with context injection. Supports partial reading (chunking).",
|
|
78
|
+
inputSchema: {
|
|
79
|
+
type: "object",
|
|
80
|
+
properties: {
|
|
81
|
+
path: { type: "string", description: "Relative path to file" },
|
|
82
|
+
start_line: { type: "number", description: "Start line (1-indexed, optional)" },
|
|
83
|
+
end_line: { type: "number", description: "End line (1-indexed, optional)" },
|
|
84
|
+
},
|
|
85
|
+
required: ["path"],
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
name: GENERIC.RUN,
|
|
90
|
+
description:
|
|
91
|
+
"[GENERIC] Runs a shell command safely. Auto-documents dependencies.",
|
|
92
|
+
inputSchema: {
|
|
93
|
+
type: "object",
|
|
94
|
+
properties: {
|
|
95
|
+
command: { type: "string", description: "Shell command to run" },
|
|
96
|
+
},
|
|
97
|
+
required: ["command"],
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
// Placeholder for Dynamic Pack Tools
|
|
101
|
+
// ...scanPackTools()
|
|
102
|
+
],
|
|
103
|
+
};
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
// 2. Handle Tool Calls
|
|
107
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
108
|
+
const { name, arguments: args } = request.params;
|
|
109
|
+
|
|
110
|
+
try {
|
|
111
|
+
switch (name) {
|
|
112
|
+
case GENERIC.WRITE: {
|
|
113
|
+
const schema = z.object({
|
|
114
|
+
path: z.string(),
|
|
115
|
+
content: z.string(),
|
|
116
|
+
reason: z.string().optional().default("Automated update via MCP"),
|
|
117
|
+
});
|
|
118
|
+
const { path, content, reason } = schema.parse(args);
|
|
119
|
+
|
|
120
|
+
await smartWriteFile(path, content, reason);
|
|
121
|
+
return {
|
|
122
|
+
content: [{ type: "text", text: `Successfully wrote ${path} and updated context.` }],
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
case GENERIC.PATCH: {
|
|
127
|
+
const schema = z.object({
|
|
128
|
+
path: z.string(),
|
|
129
|
+
search: z.string(),
|
|
130
|
+
replace: z.string(),
|
|
131
|
+
reason: z.string().optional().default("Automated patch via MCP"),
|
|
132
|
+
});
|
|
133
|
+
const { path, search, replace, reason } = schema.parse(args);
|
|
134
|
+
|
|
135
|
+
await smartPatchFile(path, search, replace, reason);
|
|
136
|
+
return {
|
|
137
|
+
content: [{ type: "text", text: `Successfully patched ${path} and updated context.` }],
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
case GENERIC.READ: {
|
|
142
|
+
const schema = z.object({
|
|
143
|
+
path: z.string(),
|
|
144
|
+
start_line: z.number().optional(),
|
|
145
|
+
end_line: z.number().optional(),
|
|
146
|
+
});
|
|
147
|
+
const { path, start_line, end_line } = schema.parse(args);
|
|
148
|
+
|
|
149
|
+
const result = await smartReadFile(path, { startLine: start_line, endLine: end_line });
|
|
150
|
+
return {
|
|
151
|
+
content: [{ type: "text", text: result.fullOutput }],
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
case GENERIC.RUN: {
|
|
156
|
+
const schema = z.object({
|
|
157
|
+
command: z.string(),
|
|
158
|
+
});
|
|
159
|
+
const { command } = schema.parse(args);
|
|
160
|
+
const result = await smartRunCommand(command);
|
|
161
|
+
|
|
162
|
+
return {
|
|
163
|
+
content: [{ type: "text", text: result.output || "Command executed successfully." }],
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
default:
|
|
168
|
+
throw new Error(`Unknown tool: ${name}`);
|
|
169
|
+
}
|
|
170
|
+
} catch (error) {
|
|
171
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
172
|
+
return {
|
|
173
|
+
content: [{ type: "text", text: `Error: ${errorMessage}` }],
|
|
174
|
+
isError: true,
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
// Start Server
|
|
180
|
+
async function main() {
|
|
181
|
+
const transport = new StdioServerTransport();
|
|
182
|
+
await server.connect(transport);
|
|
183
|
+
console.error("ContextAI MCP Server (Advanced) running on stdio");
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
main().catch((error) => {
|
|
187
|
+
console.error("Server error:", error);
|
|
188
|
+
process.exit(1);
|
|
189
|
+
});
|
package/src/utils/supabase.js
CHANGED
|
@@ -25,13 +25,21 @@ for (const envPath of envPaths) {
|
|
|
25
25
|
}
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
-
|
|
29
|
-
|
|
28
|
+
// Lazy load Supabase client
|
|
29
|
+
let supabaseInstance = null;
|
|
30
30
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
process.
|
|
35
|
-
|
|
31
|
+
export function getSupabase() {
|
|
32
|
+
if (supabaseInstance) return supabaseInstance;
|
|
33
|
+
|
|
34
|
+
const SUPABASE_URL = process.env.VITE_SUPABASE_URL;
|
|
35
|
+
const SUPABASE_KEY = process.env.VITE_SUPABASE_ANON_KEY;
|
|
36
36
|
|
|
37
|
-
|
|
37
|
+
if (!SUPABASE_URL || !SUPABASE_KEY) {
|
|
38
|
+
console.error('Error: VITE_SUPABASE_URL and VITE_SUPABASE_ANON_KEY must be set.');
|
|
39
|
+
console.error('Create a .env file or set environment variables.');
|
|
40
|
+
process.exit(1);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
supabaseInstance = createClient(SUPABASE_URL, SUPABASE_KEY);
|
|
44
|
+
return supabaseInstance;
|
|
45
|
+
}
|