@unifiedmemory/cli 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +9 -0
- package/CHANGELOG.md +34 -0
- package/HOOK_SETUP.md +338 -0
- package/LICENSE +51 -0
- package/README.md +220 -0
- package/bin/um-mcp-serve +62 -0
- package/commands/init.js +315 -0
- package/commands/login.js +390 -0
- package/commands/org.js +111 -0
- package/commands/record.js +114 -0
- package/index.js +215 -0
- package/lib/clerk-api.js +172 -0
- package/lib/config.js +39 -0
- package/lib/hooks.js +43 -0
- package/lib/mcp-proxy.js +227 -0
- package/lib/mcp-server.js +284 -0
- package/lib/provider-detector.js +291 -0
- package/lib/token-refresh.js +113 -0
- package/lib/token-storage.js +63 -0
- package/lib/token-validation.js +47 -0
- package/package.json +49 -0
package/bin/um-mcp-serve
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -e
|
|
3
|
+
|
|
4
|
+
# Find script location (works even when symlinked)
|
|
5
|
+
if command -v readlink >/dev/null 2>&1; then
|
|
6
|
+
SCRIPT_PATH="$(readlink -f "$0" 2>/dev/null || echo "$0")"
|
|
7
|
+
else
|
|
8
|
+
SCRIPT_PATH="$0"
|
|
9
|
+
fi
|
|
10
|
+
SCRIPT_DIR="$(cd "$(dirname "$SCRIPT_PATH")" && pwd)"
|
|
11
|
+
CLI_ROOT="$(dirname "$SCRIPT_DIR")"
|
|
12
|
+
|
|
13
|
+
# Config file paths
|
|
14
|
+
CONFIG_FILE=".um/config.json"
|
|
15
|
+
AUTH_FILE="$HOME/.um/auth.json"
|
|
16
|
+
|
|
17
|
+
# Export org/project IDs from config if available
|
|
18
|
+
if [ -f "$CONFIG_FILE" ]; then
|
|
19
|
+
if command -v jq >/dev/null 2>&1; then
|
|
20
|
+
UM_ORG_ID=$(jq -r '.org_id // empty' "$CONFIG_FILE" 2>/dev/null || echo "")
|
|
21
|
+
UM_PROJECT_ID=$(jq -r '.project_id // empty' "$CONFIG_FILE" 2>/dev/null || echo "")
|
|
22
|
+
|
|
23
|
+
if [ -n "$UM_ORG_ID" ]; then
|
|
24
|
+
export UM_ORG_ID
|
|
25
|
+
echo "✓ Loaded UM_ORG_ID: $UM_ORG_ID" >&2
|
|
26
|
+
fi
|
|
27
|
+
|
|
28
|
+
if [ -n "$UM_PROJECT_ID" ]; then
|
|
29
|
+
export UM_PROJECT_ID
|
|
30
|
+
echo "✓ Loaded UM_PROJECT_ID: $UM_PROJECT_ID" >&2
|
|
31
|
+
fi
|
|
32
|
+
else
|
|
33
|
+
echo "⚠️ jq not found, cannot parse config files" >&2
|
|
34
|
+
echo " Install jq: brew install jq (macOS) or apt-get install jq (Linux)" >&2
|
|
35
|
+
fi
|
|
36
|
+
fi
|
|
37
|
+
|
|
38
|
+
# Export user ID from auth file if available
|
|
39
|
+
if [ -f "$AUTH_FILE" ]; then
|
|
40
|
+
if command -v jq >/dev/null 2>&1; then
|
|
41
|
+
UM_USER_ID=$(jq -r '.decoded.sub // empty' "$AUTH_FILE" 2>/dev/null || echo "")
|
|
42
|
+
|
|
43
|
+
if [ -n "$UM_USER_ID" ]; then
|
|
44
|
+
export UM_USER_ID
|
|
45
|
+
echo "✓ Loaded UM_USER_ID: $UM_USER_ID" >&2
|
|
46
|
+
fi
|
|
47
|
+
fi
|
|
48
|
+
fi
|
|
49
|
+
|
|
50
|
+
# Warn if environment variables not set
|
|
51
|
+
if [ -z "$UM_ORG_ID" ] || [ -z "$UM_USER_ID" ]; then
|
|
52
|
+
echo "⚠️ Warning: Some auth context not available for hooks" >&2
|
|
53
|
+
echo " UM_ORG_ID: ${UM_ORG_ID:-<not set>}" >&2
|
|
54
|
+
echo " UM_USER_ID: ${UM_USER_ID:-<not set>}" >&2
|
|
55
|
+
echo " UM_PROJECT_ID: ${UM_PROJECT_ID:-<not set>}" >&2
|
|
56
|
+
echo "" >&2
|
|
57
|
+
echo " This may cause hook-triggered vault tool calls to fail." >&2
|
|
58
|
+
echo " Run 'um login' and 'um init' to configure authentication." >&2
|
|
59
|
+
fi
|
|
60
|
+
|
|
61
|
+
# Start MCP server
|
|
62
|
+
exec node "$CLI_ROOT/index.js" mcp serve
|
package/commands/init.js
ADDED
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
import inquirer from 'inquirer';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import fs from 'fs-extra';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import axios from 'axios';
|
|
6
|
+
import { getToken, saveToken } from '../lib/token-storage.js';
|
|
7
|
+
import { loadAndRefreshToken } from '../lib/token-validation.js';
|
|
8
|
+
import { login } from './login.js';
|
|
9
|
+
import { ProviderDetector } from '../lib/provider-detector.js';
|
|
10
|
+
|
|
11
|
+
export async function init(options = {}) {
|
|
12
|
+
console.log(chalk.cyan('\n🚀 UnifiedMemory Initialization\n'));
|
|
13
|
+
|
|
14
|
+
// Step 1: Check/refresh authentication
|
|
15
|
+
const authData = await ensureAuthenticated(options);
|
|
16
|
+
if (!authData) {
|
|
17
|
+
console.error(chalk.red('❌ Authentication failed'));
|
|
18
|
+
process.exit(1);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
console.log(chalk.green(`✓ Logged in as: ${authData.user_id}`));
|
|
22
|
+
if (authData.org_id) {
|
|
23
|
+
console.log(chalk.green(`✓ Organization: ${authData.org_id}`));
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Step 2: Select/create project
|
|
27
|
+
const projectData = await selectOrCreateProject(authData, options);
|
|
28
|
+
if (!projectData) {
|
|
29
|
+
console.error(chalk.red('❌ Project setup failed'));
|
|
30
|
+
process.exit(1);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
console.log(chalk.green(`✓ Project: ${projectData.project_name} (${projectData.project_id})`));
|
|
34
|
+
|
|
35
|
+
// Step 3: Save project config
|
|
36
|
+
await saveProjectConfig(authData, projectData);
|
|
37
|
+
|
|
38
|
+
// Step 4: Configure AI tools
|
|
39
|
+
if (!options.skipConfigure) {
|
|
40
|
+
await configureProviders(authData, projectData);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
console.log(chalk.green('\n✅ Initialization complete!\n'));
|
|
44
|
+
console.log(chalk.cyan('Next steps:'));
|
|
45
|
+
console.log(' 1. Restart your AI code assistant (Claude, Cursor, Cline)');
|
|
46
|
+
console.log(' 2. Start working - context is automatically shared!');
|
|
47
|
+
console.log(' 3. Run `um status` to verify configuration\n');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async function ensureAuthenticated(options) {
|
|
51
|
+
// Try to load and refresh token if expired
|
|
52
|
+
const stored = await loadAndRefreshToken(false); // Don't throw, allow new login
|
|
53
|
+
|
|
54
|
+
if (stored && stored.decoded) {
|
|
55
|
+
console.log(chalk.gray('Using saved session'));
|
|
56
|
+
|
|
57
|
+
// Extract user_id and org_id from token
|
|
58
|
+
const userId = stored.decoded.sub;
|
|
59
|
+
const orgId = stored.selectedOrg?.id || userId; // Fallback to user_id if no org
|
|
60
|
+
const expirationTime = stored.decoded.exp * 1000;
|
|
61
|
+
|
|
62
|
+
return {
|
|
63
|
+
user_id: userId,
|
|
64
|
+
org_id: orgId,
|
|
65
|
+
access_token: stored.idToken || stored.accessToken,
|
|
66
|
+
api_url: 'https://rose-asp-main-1c0b114.d2.zuplo.dev',
|
|
67
|
+
expires_at: expirationTime,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Need to login
|
|
72
|
+
console.log(chalk.yellow('⚠ Authentication required\n'));
|
|
73
|
+
|
|
74
|
+
if (options.apiKey) {
|
|
75
|
+
return await loginWithApiKey(options.apiKey);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// For now, just use OAuth
|
|
79
|
+
console.log(chalk.blue('Initiating OAuth login...'));
|
|
80
|
+
const tokenData = await login();
|
|
81
|
+
|
|
82
|
+
if (!tokenData) {
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Extract from saved token
|
|
87
|
+
const savedToken = getToken();
|
|
88
|
+
const userId = savedToken?.decoded?.sub;
|
|
89
|
+
const orgId = savedToken?.selectedOrg?.id || userId;
|
|
90
|
+
|
|
91
|
+
return {
|
|
92
|
+
user_id: userId,
|
|
93
|
+
org_id: orgId,
|
|
94
|
+
access_token: savedToken.idToken || savedToken.accessToken,
|
|
95
|
+
api_url: 'https://rose-asp-main-1c0b114.d2.zuplo.dev',
|
|
96
|
+
expires_at: savedToken.decoded?.exp * 1000,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async function selectOrCreateProject(authData, options) {
|
|
101
|
+
const apiUrl = authData.api_url || 'https://rose-asp-main-1c0b114.d2.zuplo.dev';
|
|
102
|
+
|
|
103
|
+
// Fetch existing projects
|
|
104
|
+
const projects = await fetchProjects(authData, apiUrl);
|
|
105
|
+
|
|
106
|
+
console.log(chalk.cyan('\n📋 Project setup:\n'));
|
|
107
|
+
console.log(chalk.green(` 1. Create new project`));
|
|
108
|
+
console.log(chalk.green(` 2. Select existing project (${projects.length} available)`));
|
|
109
|
+
|
|
110
|
+
const { selection } = await inquirer.prompt([{
|
|
111
|
+
type: 'input',
|
|
112
|
+
name: 'selection',
|
|
113
|
+
message: 'Choose option (1-2):',
|
|
114
|
+
default: projects.length > 0 ? '2' : '1',
|
|
115
|
+
validate: (input) => {
|
|
116
|
+
const num = parseInt(input, 10);
|
|
117
|
+
if (isNaN(num) || num < 1 || num > 2) {
|
|
118
|
+
return 'Please enter 1 or 2';
|
|
119
|
+
}
|
|
120
|
+
return true;
|
|
121
|
+
},
|
|
122
|
+
}]);
|
|
123
|
+
|
|
124
|
+
const action = parseInt(selection, 10) === 1 ? 'create' : 'select';
|
|
125
|
+
|
|
126
|
+
if (action === 'create') {
|
|
127
|
+
const { name, description } = await inquirer.prompt([
|
|
128
|
+
{
|
|
129
|
+
type: 'input',
|
|
130
|
+
name: 'name',
|
|
131
|
+
message: 'Project name:',
|
|
132
|
+
validate: input => input.length > 0 || 'Name is required',
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
type: 'input',
|
|
136
|
+
name: 'description',
|
|
137
|
+
message: 'Project description (optional):',
|
|
138
|
+
},
|
|
139
|
+
]);
|
|
140
|
+
|
|
141
|
+
return await createProject(authData, apiUrl, name, description);
|
|
142
|
+
} else {
|
|
143
|
+
if (projects.length === 0) {
|
|
144
|
+
console.log(chalk.yellow('No projects found. Creating first project...'));
|
|
145
|
+
return await selectOrCreateProject(authData, { ...options, action: 'create' });
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Display numbered list
|
|
149
|
+
console.log(chalk.cyan('\n📋 Available projects:\n'));
|
|
150
|
+
projects.forEach((p, index) => {
|
|
151
|
+
console.log(chalk.green(` ${index + 1}. ${p.display_name || p.name}`) + chalk.gray(` (${p.slug || p.id})`));
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
const { projectSelection } = await inquirer.prompt([{
|
|
155
|
+
type: 'input',
|
|
156
|
+
name: 'projectSelection',
|
|
157
|
+
message: `Choose project (1-${projects.length}):`,
|
|
158
|
+
default: '1',
|
|
159
|
+
validate: (input) => {
|
|
160
|
+
const num = parseInt(input, 10);
|
|
161
|
+
if (isNaN(num) || num < 1 || num > projects.length) {
|
|
162
|
+
return `Please enter a number between 1 and ${projects.length}`;
|
|
163
|
+
}
|
|
164
|
+
return true;
|
|
165
|
+
},
|
|
166
|
+
}]);
|
|
167
|
+
|
|
168
|
+
const selectedIndex = parseInt(projectSelection, 10) - 1;
|
|
169
|
+
const project = projects[selectedIndex];
|
|
170
|
+
|
|
171
|
+
return {
|
|
172
|
+
project_id: project.id,
|
|
173
|
+
project_name: project.display_name || project.name,
|
|
174
|
+
project_slug: project.slug,
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
async function fetchProjects(authData, apiUrl) {
|
|
180
|
+
try {
|
|
181
|
+
const response = await axios.get(
|
|
182
|
+
`${apiUrl}/v1/orgs/${authData.org_id}/projects`,
|
|
183
|
+
{
|
|
184
|
+
headers: {
|
|
185
|
+
'Authorization': `Bearer ${authData.access_token}`,
|
|
186
|
+
'X-Org-Id': authData.org_id,
|
|
187
|
+
},
|
|
188
|
+
}
|
|
189
|
+
);
|
|
190
|
+
// API returns {items: [...], next_cursor, has_more}
|
|
191
|
+
return response.data?.items || [];
|
|
192
|
+
} catch (error) {
|
|
193
|
+
console.error(chalk.red(`Failed to fetch projects: ${error.message}`));
|
|
194
|
+
return [];
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
async function createProject(authData, apiUrl, name, description) {
|
|
199
|
+
try {
|
|
200
|
+
const response = await axios.post(
|
|
201
|
+
`${apiUrl}/v1/orgs/${authData.org_id}/projects`,
|
|
202
|
+
{
|
|
203
|
+
display_name: name,
|
|
204
|
+
description: description || '',
|
|
205
|
+
},
|
|
206
|
+
{
|
|
207
|
+
headers: {
|
|
208
|
+
'Authorization': `Bearer ${authData.access_token}`,
|
|
209
|
+
'X-Org-Id': authData.org_id,
|
|
210
|
+
},
|
|
211
|
+
}
|
|
212
|
+
);
|
|
213
|
+
|
|
214
|
+
const project = response.data;
|
|
215
|
+
return {
|
|
216
|
+
project_id: project.id,
|
|
217
|
+
project_name: project.display_name || project.name,
|
|
218
|
+
project_slug: project.slug,
|
|
219
|
+
};
|
|
220
|
+
} catch (error) {
|
|
221
|
+
console.error(chalk.red(`Failed to create project: ${error.message}`));
|
|
222
|
+
return null;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
async function saveProjectConfig(authData, projectData) {
|
|
227
|
+
const umDir = path.join(process.cwd(), '.um');
|
|
228
|
+
const configPath = path.join(umDir, 'config.json');
|
|
229
|
+
|
|
230
|
+
const config = {
|
|
231
|
+
version: '1.0',
|
|
232
|
+
org_id: authData.org_id,
|
|
233
|
+
project_id: projectData.project_id,
|
|
234
|
+
project_name: projectData.project_name,
|
|
235
|
+
api_url: authData.api_url || 'https://rose-asp-main-1c0b114.d2.zuplo.dev',
|
|
236
|
+
created_at: new Date().toISOString(),
|
|
237
|
+
mcp_config: {
|
|
238
|
+
inject_headers: true,
|
|
239
|
+
auto_context: true,
|
|
240
|
+
},
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
fs.ensureDirSync(umDir);
|
|
244
|
+
fs.writeJSONSync(configPath, config, { spaces: 2 });
|
|
245
|
+
|
|
246
|
+
console.log(chalk.green(`✓ Project config saved: ${configPath}`));
|
|
247
|
+
|
|
248
|
+
// Add .um and .claude to .gitignore
|
|
249
|
+
const gitignorePath = path.join(process.cwd(), '.gitignore');
|
|
250
|
+
if (fs.existsSync(gitignorePath)) {
|
|
251
|
+
let gitignore = fs.readFileSync(gitignorePath, 'utf-8');
|
|
252
|
+
let modified = false;
|
|
253
|
+
|
|
254
|
+
if (!gitignore.includes('.um/')) {
|
|
255
|
+
gitignore += '\n# UnifiedMemory CLI\n.um/\n';
|
|
256
|
+
modified = true;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
if (!gitignore.includes('.claude/')) {
|
|
260
|
+
gitignore += '.claude/\n';
|
|
261
|
+
modified = true;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
if (modified) {
|
|
265
|
+
fs.writeFileSync(gitignorePath, gitignore);
|
|
266
|
+
console.log(chalk.gray(' Updated .gitignore'));
|
|
267
|
+
}
|
|
268
|
+
} else {
|
|
269
|
+
// Create .gitignore if it doesn't exist
|
|
270
|
+
fs.writeFileSync(gitignorePath, '# UnifiedMemory CLI\n.um/\n.claude/\n');
|
|
271
|
+
console.log(chalk.gray(' Created .gitignore with .um/ and .claude/ entries'));
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
async function configureProviders(authData, projectData) {
|
|
276
|
+
console.log(chalk.cyan('\n🔧 Configuring AI code assistants...\n'));
|
|
277
|
+
|
|
278
|
+
// Pass current directory for project-level configs (like Claude Code)
|
|
279
|
+
const detector = new ProviderDetector(process.cwd());
|
|
280
|
+
const detected = detector.detectAll();
|
|
281
|
+
|
|
282
|
+
if (detected.length === 0) {
|
|
283
|
+
console.log(chalk.yellow('⚠ No AI code assistants detected'));
|
|
284
|
+
console.log(chalk.gray(' Install Claude Code, Cursor, Cline, Codex CLI, or Gemini CLI'));
|
|
285
|
+
console.log(chalk.gray(' to enable auto-configuration'));
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// NEW APPROACH: Configure local MCP server (no tokens in config files)
|
|
290
|
+
for (const provider of detected) {
|
|
291
|
+
const success = provider.configureMCP();
|
|
292
|
+
|
|
293
|
+
if (success) {
|
|
294
|
+
console.log(chalk.green(`✓ Configured ${provider.name}`));
|
|
295
|
+
} else {
|
|
296
|
+
console.log(chalk.red(`✗ Failed to configure ${provider.name}`));
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
console.log(chalk.cyan('\n💡 Important: Restart your AI assistant to load the new configuration'));
|
|
301
|
+
console.log(chalk.gray(' The MCP server will automatically use your authentication and project context'));
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
async function loginWithApiKey(apiKey) {
|
|
305
|
+
// TODO: Implement API key authentication
|
|
306
|
+
// Exchange API key for JWT token
|
|
307
|
+
console.log(chalk.yellow('API key authentication not yet implemented'));
|
|
308
|
+
return null;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
async function refreshToken(refreshToken) {
|
|
312
|
+
// TODO: Implement token refresh
|
|
313
|
+
console.log(chalk.yellow('Token refresh not yet implemented'));
|
|
314
|
+
return null;
|
|
315
|
+
}
|