@unifiedmemory/cli 1.0.1 → 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/.env.example +27 -6
- package/CHANGELOG.md +264 -0
- package/README.md +64 -3
- package/commands/init.js +370 -71
- package/commands/login.js +9 -95
- package/commands/org.js +9 -38
- package/index.js +17 -26
- package/lib/config.js +42 -24
- package/lib/jwt-utils.js +63 -0
- package/lib/mcp-server.js +1 -1
- package/lib/memory-instructions.js +72 -0
- package/lib/org-selection-ui.js +104 -0
- package/lib/provider-detector.js +91 -79
- package/lib/token-refresh.js +1 -18
- package/lib/token-storage.js +15 -2
- package/lib/welcome.js +40 -0
- package/package.json +6 -4
- package/HOOK_SETUP.md +0 -338
- package/lib/hooks.js +0 -43
package/lib/provider-detector.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import os from 'os';
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import fs from 'fs-extra';
|
|
4
|
+
import TOML from '@iarna/toml';
|
|
5
|
+
import { getMemoryInstructions, MEMORY_INSTRUCTIONS_MARKER } from './memory-instructions.js';
|
|
4
6
|
|
|
5
7
|
class ProviderDetector {
|
|
6
8
|
constructor(projectDir = null) {
|
|
@@ -57,13 +59,14 @@ class BaseProvider {
|
|
|
57
59
|
}
|
|
58
60
|
}
|
|
59
61
|
|
|
60
|
-
configureMCP() {
|
|
62
|
+
configureMCP(toolPermissions = null) {
|
|
61
63
|
// NEW APPROACH: Configure local server instead of HTTP
|
|
62
64
|
// No parameters needed - local server reads from filesystem
|
|
65
|
+
// toolPermissions parameter for Claude Code compatibility (unused in base class)
|
|
63
66
|
const config = this.readConfig() || { mcpServers: {} };
|
|
64
67
|
|
|
65
68
|
config.mcpServers = config.mcpServers || {};
|
|
66
|
-
config.mcpServers.
|
|
69
|
+
config.mcpServers.unifiedmemory = {
|
|
67
70
|
command: "um",
|
|
68
71
|
args: ["mcp", "serve"]
|
|
69
72
|
};
|
|
@@ -71,9 +74,37 @@ class BaseProvider {
|
|
|
71
74
|
return this.writeConfig(config);
|
|
72
75
|
}
|
|
73
76
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
+
/**
|
|
78
|
+
* Write memory instructions to specified file
|
|
79
|
+
* @param {string} filePath - Path to instruction file
|
|
80
|
+
* @param {string} assistantName - Display name for logging
|
|
81
|
+
* @returns {object} - Status object with status and optional error
|
|
82
|
+
*/
|
|
83
|
+
writeMemoryInstructions(filePath, assistantName = 'assistant') {
|
|
84
|
+
try {
|
|
85
|
+
const instructions = getMemoryInstructions();
|
|
86
|
+
|
|
87
|
+
// Check if file exists
|
|
88
|
+
if (fs.existsSync(filePath)) {
|
|
89
|
+
// Check if marker already present
|
|
90
|
+
const existing = fs.readFileSync(filePath, 'utf8');
|
|
91
|
+
if (existing.includes(MEMORY_INSTRUCTIONS_MARKER)) {
|
|
92
|
+
// Instructions already present, skip
|
|
93
|
+
return { status: 'skipped', reason: 'already_present' };
|
|
94
|
+
}
|
|
95
|
+
// Append to existing file
|
|
96
|
+
fs.appendFileSync(filePath, '\n\n' + instructions);
|
|
97
|
+
return { status: 'appended' };
|
|
98
|
+
} else {
|
|
99
|
+
// Create new file
|
|
100
|
+
fs.ensureFileSync(filePath);
|
|
101
|
+
fs.writeFileSync(filePath, instructions);
|
|
102
|
+
return { status: 'created' };
|
|
103
|
+
}
|
|
104
|
+
} catch (e) {
|
|
105
|
+
console.error(`Failed to write memory instructions: ${e.message}`);
|
|
106
|
+
return { status: 'error', error: e.message };
|
|
107
|
+
}
|
|
77
108
|
}
|
|
78
109
|
}
|
|
79
110
|
|
|
@@ -106,19 +137,19 @@ class ClaudeProvider extends BaseProvider {
|
|
|
106
137
|
}
|
|
107
138
|
}
|
|
108
139
|
|
|
109
|
-
configureMCP() {
|
|
140
|
+
configureMCP(toolPermissions = null) {
|
|
110
141
|
// Create .mcp.json at project root with local server config
|
|
111
142
|
const success = super.configureMCP();
|
|
112
143
|
|
|
113
144
|
if (success) {
|
|
114
145
|
// Update .claude/settings.local.json to enable the MCP server
|
|
115
|
-
this.updateClaudeSettings();
|
|
146
|
+
this.updateClaudeSettings(toolPermissions);
|
|
116
147
|
}
|
|
117
148
|
|
|
118
149
|
return success;
|
|
119
150
|
}
|
|
120
151
|
|
|
121
|
-
updateClaudeSettings() {
|
|
152
|
+
updateClaudeSettings(toolPermissions = null) {
|
|
122
153
|
if (!this.settingsPath) return false;
|
|
123
154
|
|
|
124
155
|
try {
|
|
@@ -131,12 +162,29 @@ class ClaudeProvider extends BaseProvider {
|
|
|
131
162
|
// Add or update the required settings
|
|
132
163
|
settings.enableAllProjectMcpServers = true;
|
|
133
164
|
|
|
134
|
-
// Ensure
|
|
165
|
+
// Ensure unifiedmemory is in the enabled servers list
|
|
135
166
|
if (!settings.enabledMcpjsonServers) {
|
|
136
167
|
settings.enabledMcpjsonServers = [];
|
|
137
168
|
}
|
|
138
|
-
if (!settings.enabledMcpjsonServers.includes('
|
|
139
|
-
settings.enabledMcpjsonServers.push('
|
|
169
|
+
if (!settings.enabledMcpjsonServers.includes('unifiedmemory')) {
|
|
170
|
+
settings.enabledMcpjsonServers.push('unifiedmemory');
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Add tool permissions if provided
|
|
174
|
+
if (toolPermissions && Array.isArray(toolPermissions) && toolPermissions.length > 0) {
|
|
175
|
+
if (!settings.permissions) {
|
|
176
|
+
settings.permissions = { allow: [] };
|
|
177
|
+
}
|
|
178
|
+
if (!settings.permissions.allow) {
|
|
179
|
+
settings.permissions.allow = [];
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Add new permissions, avoiding duplicates
|
|
183
|
+
toolPermissions.forEach(permission => {
|
|
184
|
+
if (!settings.permissions.allow.includes(permission)) {
|
|
185
|
+
settings.permissions.allow.push(permission);
|
|
186
|
+
}
|
|
187
|
+
});
|
|
140
188
|
}
|
|
141
189
|
|
|
142
190
|
// Write settings
|
|
@@ -149,19 +197,10 @@ class ClaudeProvider extends BaseProvider {
|
|
|
149
197
|
}
|
|
150
198
|
}
|
|
151
199
|
|
|
152
|
-
|
|
153
|
-
if (
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
try {
|
|
158
|
-
fs.ensureDirSync(this.hooksDir);
|
|
159
|
-
fs.writeFileSync(hookPath, scriptContent, { mode: 0o755 });
|
|
160
|
-
return true;
|
|
161
|
-
} catch (e) {
|
|
162
|
-
console.error(`Failed to install hook: ${e.message}`);
|
|
163
|
-
return false;
|
|
164
|
-
}
|
|
200
|
+
configureMemoryInstructions() {
|
|
201
|
+
if (!this.projectDir) return { status: 'skipped', reason: 'no_project_dir' };
|
|
202
|
+
const instructionPath = path.join(this.projectDir, 'CLAUDE.md');
|
|
203
|
+
return this.writeMemoryInstructions(instructionPath, 'Claude Code');
|
|
165
204
|
}
|
|
166
205
|
}
|
|
167
206
|
|
|
@@ -172,6 +211,12 @@ class CursorProvider extends BaseProvider {
|
|
|
172
211
|
// Detect by directory existence, not config file
|
|
173
212
|
super('Cursor', configPath, false, baseDir);
|
|
174
213
|
}
|
|
214
|
+
|
|
215
|
+
configureMemoryInstructions() {
|
|
216
|
+
// Use project-level file (assuming we're in a project directory)
|
|
217
|
+
const instructionPath = path.join(process.cwd(), 'CURSOR.md');
|
|
218
|
+
return this.writeMemoryInstructions(instructionPath, 'Cursor');
|
|
219
|
+
}
|
|
175
220
|
}
|
|
176
221
|
|
|
177
222
|
class ClineProvider extends BaseProvider {
|
|
@@ -181,6 +226,12 @@ class ClineProvider extends BaseProvider {
|
|
|
181
226
|
// Detect by directory existence, not config file
|
|
182
227
|
super('Cline', configPath, false, baseDir);
|
|
183
228
|
}
|
|
229
|
+
|
|
230
|
+
configureMemoryInstructions() {
|
|
231
|
+
// Use project-level file
|
|
232
|
+
const instructionPath = path.join(process.cwd(), 'CLINE.md');
|
|
233
|
+
return this.writeMemoryInstructions(instructionPath, 'Cline');
|
|
234
|
+
}
|
|
184
235
|
}
|
|
185
236
|
|
|
186
237
|
class CodexProvider extends BaseProvider {
|
|
@@ -194,7 +245,7 @@ class CodexProvider extends BaseProvider {
|
|
|
194
245
|
if (!fs.existsSync(this.configPath)) return null;
|
|
195
246
|
try {
|
|
196
247
|
const content = fs.readFileSync(this.configPath, 'utf8');
|
|
197
|
-
return
|
|
248
|
+
return TOML.parse(content);
|
|
198
249
|
} catch (e) {
|
|
199
250
|
return null;
|
|
200
251
|
}
|
|
@@ -203,8 +254,8 @@ class CodexProvider extends BaseProvider {
|
|
|
203
254
|
writeConfig(config) {
|
|
204
255
|
try {
|
|
205
256
|
fs.ensureFileSync(this.configPath);
|
|
206
|
-
const tomlContent =
|
|
207
|
-
fs.writeFileSync(this.configPath, tomlContent);
|
|
257
|
+
const tomlContent = TOML.stringify(config);
|
|
258
|
+
fs.writeFileSync(this.configPath, tomlContent, 'utf8');
|
|
208
259
|
return true;
|
|
209
260
|
} catch (e) {
|
|
210
261
|
console.error(`Failed to write config: ${e.message}`);
|
|
@@ -212,65 +263,20 @@ class CodexProvider extends BaseProvider {
|
|
|
212
263
|
}
|
|
213
264
|
}
|
|
214
265
|
|
|
215
|
-
parseTOML(content) {
|
|
216
|
-
// Simple TOML parser for [mcp_servers.*] sections
|
|
217
|
-
const servers = {};
|
|
218
|
-
const lines = content.split('\n');
|
|
219
|
-
let currentServer = null;
|
|
220
|
-
|
|
221
|
-
for (const line of lines) {
|
|
222
|
-
const trimmed = line.trim();
|
|
223
|
-
if (trimmed.startsWith('[mcp_servers.')) {
|
|
224
|
-
const match = trimmed.match(/\[mcp_servers\.([^\]]+)\]/);
|
|
225
|
-
if (match) {
|
|
226
|
-
currentServer = match[1];
|
|
227
|
-
servers[currentServer] = {};
|
|
228
|
-
}
|
|
229
|
-
} else if (currentServer && trimmed.includes('=')) {
|
|
230
|
-
const [key, ...valueParts] = trimmed.split('=');
|
|
231
|
-
const value = valueParts.join('=').trim();
|
|
232
|
-
if (key.trim() === 'command') {
|
|
233
|
-
servers[currentServer].command = value.replace(/"/g, '');
|
|
234
|
-
} else if (key.trim() === 'args') {
|
|
235
|
-
// Parse array: ["arg1", "arg2"]
|
|
236
|
-
const argsMatch = value.match(/\[(.*)\]/);
|
|
237
|
-
if (argsMatch) {
|
|
238
|
-
servers[currentServer].args = argsMatch[1]
|
|
239
|
-
.split(',')
|
|
240
|
-
.map(s => s.trim().replace(/"/g, ''));
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
return { mcp_servers: servers };
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
generateTOML(config) {
|
|
250
|
-
let toml = '';
|
|
251
|
-
const servers = config.mcp_servers || {};
|
|
252
|
-
|
|
253
|
-
for (const [name, serverConfig] of Object.entries(servers)) {
|
|
254
|
-
toml += `[mcp_servers.${name}]\n`;
|
|
255
|
-
toml += `command = "${serverConfig.command}"\n`;
|
|
256
|
-
if (serverConfig.args && serverConfig.args.length > 0) {
|
|
257
|
-
const argsStr = serverConfig.args.map(arg => `"${arg}"`).join(', ');
|
|
258
|
-
toml += `args = [${argsStr}]\n`;
|
|
259
|
-
}
|
|
260
|
-
toml += '\n';
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
return toml;
|
|
264
|
-
}
|
|
265
|
-
|
|
266
266
|
configureMCP() {
|
|
267
267
|
const config = this.readConfig() || { mcp_servers: {} };
|
|
268
|
-
config.mcp_servers.
|
|
268
|
+
config.mcp_servers.unifiedmemory = {
|
|
269
269
|
command: "um",
|
|
270
270
|
args: ["mcp", "serve"]
|
|
271
271
|
};
|
|
272
272
|
return this.writeConfig(config);
|
|
273
273
|
}
|
|
274
|
+
|
|
275
|
+
configureMemoryInstructions() {
|
|
276
|
+
// Use project-level file
|
|
277
|
+
const instructionPath = path.join(process.cwd(), 'CODEX.md');
|
|
278
|
+
return this.writeMemoryInstructions(instructionPath, 'Codex CLI');
|
|
279
|
+
}
|
|
274
280
|
}
|
|
275
281
|
|
|
276
282
|
class GeminiProvider extends BaseProvider {
|
|
@@ -279,6 +285,12 @@ class GeminiProvider extends BaseProvider {
|
|
|
279
285
|
const detectionPath = path.dirname(configPath); // Detect by directory
|
|
280
286
|
super('Gemini CLI', configPath, false, detectionPath);
|
|
281
287
|
}
|
|
288
|
+
|
|
289
|
+
configureMemoryInstructions() {
|
|
290
|
+
// Use project-level file
|
|
291
|
+
const instructionPath = path.join(process.cwd(), 'GEMINI.md');
|
|
292
|
+
return this.writeMemoryInstructions(instructionPath, 'Gemini CLI');
|
|
293
|
+
}
|
|
282
294
|
}
|
|
283
295
|
|
|
284
296
|
export {
|
package/lib/token-refresh.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { getToken, saveToken } from './token-storage.js';
|
|
2
2
|
import { config } from './config.js';
|
|
3
|
+
import { parseJWT } from './jwt-utils.js';
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* Check if token has expired
|
|
@@ -93,21 +94,3 @@ export async function refreshAccessToken(tokenData) {
|
|
|
93
94
|
throw new Error(`Token refresh failed: ${error.message}`);
|
|
94
95
|
}
|
|
95
96
|
}
|
|
96
|
-
|
|
97
|
-
/**
|
|
98
|
-
* Parse JWT token payload
|
|
99
|
-
* @param {string} token - JWT token
|
|
100
|
-
* @returns {Object|null} - Decoded payload
|
|
101
|
-
*/
|
|
102
|
-
function parseJWT(token) {
|
|
103
|
-
try {
|
|
104
|
-
const parts = token.split('.');
|
|
105
|
-
if (parts.length !== 3) {
|
|
106
|
-
return null;
|
|
107
|
-
}
|
|
108
|
-
const payload = Buffer.from(parts[1], 'base64').toString('utf8');
|
|
109
|
-
return JSON.parse(payload);
|
|
110
|
-
} catch (error) {
|
|
111
|
-
return null;
|
|
112
|
-
}
|
|
113
|
-
}
|
package/lib/token-storage.js
CHANGED
|
@@ -6,18 +6,24 @@ const TOKEN_DIR = path.join(os.homedir(), '.um');
|
|
|
6
6
|
const TOKEN_FILE = path.join(TOKEN_DIR, 'auth.json');
|
|
7
7
|
|
|
8
8
|
export function saveToken(tokenData) {
|
|
9
|
-
// Create directory if it doesn't exist
|
|
9
|
+
// Create directory if it doesn't exist (owner-only permissions)
|
|
10
10
|
if (!fs.existsSync(TOKEN_DIR)) {
|
|
11
|
-
fs.mkdirSync(TOKEN_DIR, { recursive: true });
|
|
11
|
+
fs.mkdirSync(TOKEN_DIR, { recursive: true, mode: 0o700 });
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
+
// Ensure directory permissions are correct (in case it was created with wrong permissions)
|
|
15
|
+
fs.chmodSync(TOKEN_DIR, 0o700);
|
|
16
|
+
|
|
14
17
|
// Preserve existing organization context if not explicitly overwritten
|
|
15
18
|
const existingData = getToken();
|
|
16
19
|
if (existingData && existingData.selectedOrg && !tokenData.selectedOrg) {
|
|
17
20
|
tokenData.selectedOrg = existingData.selectedOrg;
|
|
18
21
|
}
|
|
19
22
|
|
|
23
|
+
// Write token file with owner-only read/write permissions (0600)
|
|
20
24
|
fs.writeFileSync(TOKEN_FILE, JSON.stringify(tokenData, null, 2));
|
|
25
|
+
// Explicitly set file permissions (writeFileSync mode option doesn't always work with extended attributes)
|
|
26
|
+
fs.chmodSync(TOKEN_FILE, 0o600);
|
|
21
27
|
}
|
|
22
28
|
|
|
23
29
|
export function getToken() {
|
|
@@ -49,8 +55,15 @@ export function updateSelectedOrg(orgData) {
|
|
|
49
55
|
throw new Error('No token found. Please login first.');
|
|
50
56
|
}
|
|
51
57
|
|
|
58
|
+
// Ensure directory permissions are correct
|
|
59
|
+
if (fs.existsSync(TOKEN_DIR)) {
|
|
60
|
+
fs.chmodSync(TOKEN_DIR, 0o700);
|
|
61
|
+
}
|
|
62
|
+
|
|
52
63
|
tokenData.selectedOrg = orgData;
|
|
64
|
+
// Write with owner-only read/write permissions (0600)
|
|
53
65
|
fs.writeFileSync(TOKEN_FILE, JSON.stringify(tokenData, null, 2));
|
|
66
|
+
fs.chmodSync(TOKEN_FILE, 0o600);
|
|
54
67
|
}
|
|
55
68
|
|
|
56
69
|
/**
|
package/lib/welcome.js
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { readFileSync } from 'fs';
|
|
3
|
+
import { dirname, join } from 'path';
|
|
4
|
+
import { fileURLToPath } from 'url';
|
|
5
|
+
|
|
6
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
7
|
+
const packageJson = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf8'));
|
|
8
|
+
const version = packageJson.version;
|
|
9
|
+
|
|
10
|
+
export function showWelcome() {
|
|
11
|
+
const pink = chalk.hex('#FF69B4');
|
|
12
|
+
const lightPink = chalk.hex('#FFB6C1');
|
|
13
|
+
const darkPink = chalk.hex('#C71585');
|
|
14
|
+
const gray = chalk.gray;
|
|
15
|
+
const white = chalk.white;
|
|
16
|
+
|
|
17
|
+
console.log('');
|
|
18
|
+
console.log(white.bold(' UnifiedMemory CLI ') + gray(`v${version}`));
|
|
19
|
+
console.log(gray(' AI-powered knowledge assistant'));
|
|
20
|
+
console.log('');
|
|
21
|
+
console.log(white(' Quick Start:'));
|
|
22
|
+
console.log(gray(' um init ') + white('Initialize in current project'));
|
|
23
|
+
console.log(gray(' um status ') + white('Check configuration'));
|
|
24
|
+
console.log(gray(' um login ') + white('Authenticate with UnifiedMemory'));
|
|
25
|
+
console.log(gray(' um --help ') + white('Show all commands'));
|
|
26
|
+
console.log('');
|
|
27
|
+
console.log(gray(' Learn more: ') + chalk.cyan.underline('https://unifiedmemory.ai'));
|
|
28
|
+
console.log('');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function showShortWelcome() {
|
|
32
|
+
const pink = chalk.hex('#FF69B4');
|
|
33
|
+
const gray = chalk.gray;
|
|
34
|
+
const white = chalk.white;
|
|
35
|
+
|
|
36
|
+
console.log('');
|
|
37
|
+
console.log(pink(' 🦎 ') + white.bold('UnifiedMemory CLI ') + gray(`v${version}`));
|
|
38
|
+
console.log(gray(' AI-powered knowledge assistant'));
|
|
39
|
+
console.log('');
|
|
40
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@unifiedmemory/cli",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "UnifiedMemory CLI - AI code assistant integration",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -31,13 +31,15 @@
|
|
|
31
31
|
},
|
|
32
32
|
"homepage": "https://unifiedmemory.ai",
|
|
33
33
|
"dependencies": {
|
|
34
|
+
"@iarna/toml": "^2.2.5",
|
|
34
35
|
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
36
|
+
"axios": "^1.6.2",
|
|
35
37
|
"chalk": "^5.3.0",
|
|
36
38
|
"commander": "^11.1.0",
|
|
39
|
+
"dotenv": "^16.6.1",
|
|
40
|
+
"fs-extra": "^11.2.0",
|
|
37
41
|
"inquirer": "^13.0.1",
|
|
38
|
-
"open": "^10.0.3"
|
|
39
|
-
"axios": "^1.6.2",
|
|
40
|
-
"fs-extra": "^11.2.0"
|
|
42
|
+
"open": "^10.0.3"
|
|
41
43
|
},
|
|
42
44
|
"devDependencies": {
|
|
43
45
|
"pkg": "^5.8.1"
|