@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.
@@ -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
@@ -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
+ }