@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.
@@ -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.vault = {
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
- installHook(hookType, scriptContent) {
75
- // Implemented by providers that support hooks
76
- return false;
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 vault is in the enabled servers list
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('vault')) {
139
- settings.enabledMcpjsonServers.push('vault');
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
- installHook(hookType, scriptContent) {
153
- if (hookType !== 'prompt-submit' || !this.hooksDir) return false;
154
-
155
- const hookPath = path.join(this.hooksDir, 'prompt-submit');
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 this.parseTOML(content);
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 = this.generateTOML(config);
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.vault = {
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 {
@@ -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
- }
@@ -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.1",
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"