@unifiedmemory/cli 1.0.1 → 1.1.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/lib/config.js CHANGED
@@ -1,39 +1,57 @@
1
- import fs from 'fs';
2
- import path from 'path';
3
1
  import { fileURLToPath } from 'url';
4
- import { dirname } from 'path';
2
+ import { dirname, join } from 'path';
3
+ import { config as dotenvConfig } from 'dotenv';
5
4
 
6
5
  const __filename = fileURLToPath(import.meta.url);
7
6
  const __dirname = dirname(__filename);
8
7
 
9
- // Load environment variables from .env file if it exists
10
- const envPath = path.join(__dirname, '..', '.env');
11
- if (fs.existsSync(envPath)) {
12
- const envContent = fs.readFileSync(envPath, 'utf8');
13
- envContent.split('\n').forEach(line => {
14
- const [key, ...valueParts] = line.split('=');
15
- if (key && valueParts.length > 0) {
16
- const value = valueParts.join('=').trim();
17
- if (!process.env[key]) {
18
- process.env[key] = value;
19
- }
20
- }
21
- });
22
- }
8
+ // Load environment variables from .env file using dotenv
9
+ dotenvConfig({ path: join(__dirname, '..', '.env') });
23
10
 
24
11
  export const config = {
25
- // Production Clerk OAuth client (safe to embed - public key only)
26
- clerkClientId: process.env.CLERK_CLIENT_ID || 'nULlnomaKB9rRGP2',
12
+ // Required: Clerk OAuth configuration
13
+ clerkClientId: process.env.CLERK_CLIENT_ID,
27
14
  clerkClientSecret: process.env.CLERK_CLIENT_SECRET, // Optional for PKCE flow
28
- clerkDomain: process.env.CLERK_DOMAIN || 'clear-caiman-45.clerk.accounts.dev',
29
- apiEndpoint: process.env.API_ENDPOINT || 'https://rose-asp-main-1c0b114.d2.zuplo.dev',
15
+ clerkDomain: process.env.CLERK_DOMAIN,
16
+
17
+ // Required: API configuration
18
+ apiEndpoint: process.env.API_ENDPOINT,
19
+
20
+ // Optional: OAuth flow configuration (non-sensitive defaults OK)
30
21
  redirectUri: process.env.REDIRECT_URI || 'http://localhost:3333/callback',
31
22
  port: parseInt(process.env.PORT || '3333', 10)
32
23
  };
33
24
 
34
- // Validation function (no longer throws - hardcoded defaults are valid)
25
+ // Validation function - ensures required configuration is present
35
26
  export function validateConfig() {
36
- // All required values now have defaults - validation always passes
37
- // Keep function for backward compatibility with existing code
27
+ const missing = [];
28
+
29
+ if (!config.clerkClientId) {
30
+ missing.push('CLERK_CLIENT_ID');
31
+ }
32
+
33
+ if (!config.clerkDomain) {
34
+ missing.push('CLERK_DOMAIN');
35
+ }
36
+
37
+ if (!config.apiEndpoint) {
38
+ missing.push('API_ENDPOINT');
39
+ }
40
+
41
+ if (missing.length > 0) {
42
+ throw new Error(
43
+ `Missing required environment variables: ${missing.join(', ')}\n\n` +
44
+ `Please create a .env file in the project root with these values.\n` +
45
+ `See .env.example for a template.`
46
+ );
47
+ }
48
+
49
+ // Validate URL format for apiEndpoint
50
+ try {
51
+ new URL(config.apiEndpoint);
52
+ } catch (e) {
53
+ throw new Error(`API_ENDPOINT must be a valid URL (got: ${config.apiEndpoint})`);
54
+ }
55
+
38
56
  return true;
39
57
  }
@@ -0,0 +1,63 @@
1
+ /**
2
+ * JWT Utility Functions
3
+ *
4
+ * Centralized JWT parsing, validation, and expiration checking.
5
+ * Extracted from duplicate implementations in login.js and token-refresh.js.
6
+ */
7
+
8
+ /**
9
+ * Parse a JWT token and return its payload
10
+ * @param {string} token - The JWT token to parse
11
+ * @returns {Object|null} Decoded JWT payload or null if invalid
12
+ */
13
+ export function parseJWT(token) {
14
+ try {
15
+ const parts = token.split('.');
16
+ if (parts.length !== 3) {
17
+ return null;
18
+ }
19
+ const payload = Buffer.from(parts[1], 'base64').toString('utf8');
20
+ return JSON.parse(payload);
21
+ } catch (error) {
22
+ return null;
23
+ }
24
+ }
25
+
26
+ /**
27
+ * Check if a JWT token is expired
28
+ * @param {Object} decoded - Decoded JWT payload with exp claim
29
+ * @param {number} bufferMs - Optional expiration buffer in milliseconds (default: 0)
30
+ * @returns {boolean} True if token is expired (including buffer)
31
+ */
32
+ export function isJWTExpired(decoded, bufferMs = 0) {
33
+ if (!decoded?.exp) {
34
+ return true;
35
+ }
36
+ const expirationTime = decoded.exp * 1000;
37
+ return Date.now() >= (expirationTime - bufferMs);
38
+ }
39
+
40
+ /**
41
+ * Validate JWT structure has required fields
42
+ * @param {Object} decoded - Decoded JWT payload
43
+ * @returns {boolean} True if JWT has valid structure (sub and exp claims)
44
+ */
45
+ export function validateJWTStructure(decoded) {
46
+ return decoded &&
47
+ typeof decoded === 'object' &&
48
+ decoded.sub &&
49
+ decoded.exp;
50
+ }
51
+
52
+ /**
53
+ * Get time until JWT expiration
54
+ * @param {Object} decoded - Decoded JWT payload with exp claim
55
+ * @returns {number} Milliseconds until expiration (negative if already expired)
56
+ */
57
+ export function getTimeUntilExpiration(decoded) {
58
+ if (!decoded?.exp) {
59
+ return -1;
60
+ }
61
+ const expirationTime = decoded.exp * 1000;
62
+ return expirationTime - Date.now();
63
+ }
package/lib/mcp-server.js CHANGED
@@ -35,7 +35,7 @@ export async function startMCPServer() {
35
35
  // 5. Create MCP server
36
36
  const server = new Server(
37
37
  {
38
- name: "unifiedmemory-vault",
38
+ name: "unifiedmemory",
39
39
  version: "1.0.0",
40
40
  },
41
41
  {
@@ -0,0 +1,72 @@
1
+ // Unique marker to detect if instructions already present
2
+ export const MEMORY_INSTRUCTIONS_MARKER = '<!-- UNIFIEDMEMORY_INSTRUCTIONS_V1 -->';
3
+
4
+ export function getMemoryInstructions() {
5
+ return `${MEMORY_INSTRUCTIONS_MARKER}
6
+
7
+ # UnifiedMemory Automatic Integration
8
+
9
+ When working in this project, automatically integrate with UnifiedMemory to maintain project knowledge:
10
+
11
+ ## 1. Search Before Responding
12
+
13
+ **When**: User submits any query or request
14
+ **Action**: Use the \`unifiedmemory\` MCP tool to search for relevant context
15
+ **Usage**:
16
+ - Send the user's query to the search tool
17
+ - Review the search results for relevant prior work, decisions, or context
18
+ - Incorporate findings into your response
19
+
20
+ Example:
21
+ \`\`\`
22
+ User: "Add dark mode to the dashboard"
23
+ → Search: unifiedmemory search --query "dark mode dashboard styling"
24
+ → Review results for existing patterns or decisions
25
+ → Respond with context-aware answer
26
+ \`\`\`
27
+
28
+ ## 2. Record Plans
29
+
30
+ **When**: Creating an implementation plan or design approach
31
+ **Action**: Record the plan as a note using the \`unifiedmemory\` tool
32
+ **Format**:
33
+ - Include: objective, approach, key steps, important decisions
34
+ - Keep concise but informative
35
+ - Limit to under 1000 characters for optimal indexing
36
+
37
+ Example:
38
+ \`\`\`
39
+ Plan created for dark mode feature
40
+ → unifiedmemory create_note --content "Plan: Dark mode implementation. Approach: CSS variables + localStorage persistence. Steps: 1) Define color tokens, 2) Add toggle component, 3) Persist preference. Decision: Use system preference as default."
41
+ \`\`\`
42
+
43
+ ## 3. Record Task Completions
44
+
45
+ **When**: After completing any task, feature, or significant milestone
46
+ **Action**: Create a summary note using the \`unifiedmemory\` tool
47
+ **Format**:
48
+ - Summarize what was accomplished and why
49
+ - Include key technical decisions or gotchas
50
+ - **IMPORTANT**: Limit to under 1000 characters
51
+ - Focus on outcomes, not detailed code
52
+
53
+ Example:
54
+ \`\`\`
55
+ Completed dark mode feature
56
+ → unifiedmemory create_note --content "Completed: Dark mode toggle in dashboard. Implemented CSS custom properties with theme switcher component. User preference stored in localStorage. Respects system prefers-color-scheme by default. All components updated to use theme tokens."
57
+ \`\`\`
58
+
59
+ ## Summary Guidelines
60
+
61
+ - ✅ **DO**: Search context before responding
62
+ - ✅ **DO**: Record plans and completions automatically
63
+ - ✅ **DO**: Keep notes under 1000 characters
64
+ - ✅ **DO**: Focus on decisions, outcomes, and context
65
+ - ❌ **DON'T**: Record trivial tasks (typo fixes, minor edits)
66
+ - ❌ **DON'T**: Include full code in notes
67
+ - ❌ **DON'T**: Duplicate information already in git commits
68
+
69
+ ---
70
+ *Auto-generated by UnifiedMemory CLI v1.0*
71
+ `;
72
+ }
@@ -0,0 +1,104 @@
1
+ /**
2
+ * Organization Selection UI
3
+ *
4
+ * Shared UI for prompting users to select an organization context.
5
+ * Extracted from duplicate implementations in login.js and org.js.
6
+ */
7
+
8
+ import inquirer from 'inquirer';
9
+ import chalk from 'chalk';
10
+ import { formatOrganization } from './clerk-api.js';
11
+
12
+ /**
13
+ * Prompt user to select an organization context
14
+ * @param {Array} memberships - Array of organization memberships from Clerk
15
+ * @param {Object|null} currentOrg - Currently selected organization (optional)
16
+ * @param {Object} options - Display options
17
+ * @param {boolean} options.allowPersonal - Whether to show "Personal Account" option (default: true)
18
+ * @param {string} options.message - Custom prompt message (default: "Select account context:")
19
+ * @returns {Promise<Object|null>} Selected organization data or null for personal context
20
+ */
21
+ export async function promptOrganizationSelection(memberships, currentOrg = null, options = {}) {
22
+ const {
23
+ allowPersonal = true,
24
+ message = 'Select account context (use arrow keys):',
25
+ } = options;
26
+
27
+ if (memberships.length === 0) {
28
+ if (!allowPersonal) {
29
+ throw new Error('No organizations available');
30
+ }
31
+ console.log(chalk.gray('No organizations found. Using personal account context.'));
32
+ return null;
33
+ }
34
+
35
+ console.log(chalk.green(`\nFound ${memberships.length} organization(s)!`));
36
+ console.log(chalk.cyan('💡 Use ↑/↓ arrow keys to navigate, Enter to select'));
37
+
38
+ // Format organizations for display
39
+ const formattedOrgs = memberships.map(formatOrganization);
40
+
41
+ const currentOrgId = currentOrg?.id;
42
+
43
+ // Build choices for inquirer
44
+ const choices = [];
45
+
46
+ if (allowPersonal) {
47
+ choices.push({
48
+ name: chalk.cyan('Personal Account') +
49
+ chalk.gray(' (no organization)') +
50
+ (currentOrgId ? '' : chalk.green(' ← current')),
51
+ value: null,
52
+ short: 'Personal Account',
53
+ });
54
+ choices.push(new inquirer.Separator(chalk.gray('--- Organizations ---')));
55
+ }
56
+
57
+ choices.push(...formattedOrgs.map(org => ({
58
+ name: `${chalk.green(org.name)} ${chalk.gray(`(${org.slug})`)} ${chalk.yellow(`[${org.role}]`)}${org.id === currentOrgId ? chalk.green(' ← current') : ''}`,
59
+ value: org,
60
+ short: org.name,
61
+ })));
62
+
63
+ // Calculate default selection
64
+ let defaultIndex = 0;
65
+ if (currentOrgId && allowPersonal) {
66
+ const orgIndex = formattedOrgs.findIndex(org => org.id === currentOrgId);
67
+ if (orgIndex >= 0) {
68
+ defaultIndex = orgIndex + 2; // +2 for personal + separator
69
+ }
70
+ } else if (currentOrgId && !allowPersonal) {
71
+ const orgIndex = formattedOrgs.findIndex(org => org.id === currentOrgId);
72
+ if (orgIndex >= 0) {
73
+ defaultIndex = orgIndex;
74
+ }
75
+ }
76
+
77
+ // Prompt user to select
78
+ const answer = await inquirer.prompt([
79
+ {
80
+ type: 'select',
81
+ name: 'organization',
82
+ message: message,
83
+ choices: choices,
84
+ pageSize: 15,
85
+ default: defaultIndex,
86
+ },
87
+ ]);
88
+
89
+ return answer.organization;
90
+ }
91
+
92
+ /**
93
+ * Display organization selection result
94
+ * @param {Object|null} selectedOrg - Selected organization or null for personal account
95
+ */
96
+ export function displayOrganizationSelection(selectedOrg) {
97
+ if (selectedOrg) {
98
+ console.log(chalk.green(`\n✅ Selected organization: ${chalk.bold(selectedOrg.name)}`));
99
+ console.log(chalk.gray(` Organization ID: ${selectedOrg.id}`));
100
+ console.log(chalk.gray(` Your role: ${selectedOrg.role}`));
101
+ } else {
102
+ console.log(chalk.green('\n✅ Using personal account context'));
103
+ }
104
+ }
@@ -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) {
@@ -63,7 +65,7 @@ class BaseProvider {
63
65
  const config = this.readConfig() || { mcpServers: {} };
64
66
 
65
67
  config.mcpServers = config.mcpServers || {};
66
- config.mcpServers.vault = {
68
+ config.mcpServers.unifiedmemory = {
67
69
  command: "um",
68
70
  args: ["mcp", "serve"]
69
71
  };
@@ -71,9 +73,37 @@ class BaseProvider {
71
73
  return this.writeConfig(config);
72
74
  }
73
75
 
74
- installHook(hookType, scriptContent) {
75
- // Implemented by providers that support hooks
76
- return false;
76
+ /**
77
+ * Write memory instructions to specified file
78
+ * @param {string} filePath - Path to instruction file
79
+ * @param {string} assistantName - Display name for logging
80
+ * @returns {object} - Status object with status and optional error
81
+ */
82
+ writeMemoryInstructions(filePath, assistantName = 'assistant') {
83
+ try {
84
+ const instructions = getMemoryInstructions();
85
+
86
+ // Check if file exists
87
+ if (fs.existsSync(filePath)) {
88
+ // Check if marker already present
89
+ const existing = fs.readFileSync(filePath, 'utf8');
90
+ if (existing.includes(MEMORY_INSTRUCTIONS_MARKER)) {
91
+ // Instructions already present, skip
92
+ return { status: 'skipped', reason: 'already_present' };
93
+ }
94
+ // Append to existing file
95
+ fs.appendFileSync(filePath, '\n\n' + instructions);
96
+ return { status: 'appended' };
97
+ } else {
98
+ // Create new file
99
+ fs.ensureFileSync(filePath);
100
+ fs.writeFileSync(filePath, instructions);
101
+ return { status: 'created' };
102
+ }
103
+ } catch (e) {
104
+ console.error(`Failed to write memory instructions: ${e.message}`);
105
+ return { status: 'error', error: e.message };
106
+ }
77
107
  }
78
108
  }
79
109
 
@@ -131,12 +161,12 @@ class ClaudeProvider extends BaseProvider {
131
161
  // Add or update the required settings
132
162
  settings.enableAllProjectMcpServers = true;
133
163
 
134
- // Ensure vault is in the enabled servers list
164
+ // Ensure unifiedmemory is in the enabled servers list
135
165
  if (!settings.enabledMcpjsonServers) {
136
166
  settings.enabledMcpjsonServers = [];
137
167
  }
138
- if (!settings.enabledMcpjsonServers.includes('vault')) {
139
- settings.enabledMcpjsonServers.push('vault');
168
+ if (!settings.enabledMcpjsonServers.includes('unifiedmemory')) {
169
+ settings.enabledMcpjsonServers.push('unifiedmemory');
140
170
  }
141
171
 
142
172
  // Write settings
@@ -149,19 +179,10 @@ class ClaudeProvider extends BaseProvider {
149
179
  }
150
180
  }
151
181
 
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
- }
182
+ configureMemoryInstructions() {
183
+ if (!this.projectDir) return { status: 'skipped', reason: 'no_project_dir' };
184
+ const instructionPath = path.join(this.projectDir, 'CLAUDE.md');
185
+ return this.writeMemoryInstructions(instructionPath, 'Claude Code');
165
186
  }
166
187
  }
167
188
 
@@ -172,6 +193,12 @@ class CursorProvider extends BaseProvider {
172
193
  // Detect by directory existence, not config file
173
194
  super('Cursor', configPath, false, baseDir);
174
195
  }
196
+
197
+ configureMemoryInstructions() {
198
+ // Use project-level file (assuming we're in a project directory)
199
+ const instructionPath = path.join(process.cwd(), 'CURSOR.md');
200
+ return this.writeMemoryInstructions(instructionPath, 'Cursor');
201
+ }
175
202
  }
176
203
 
177
204
  class ClineProvider extends BaseProvider {
@@ -181,6 +208,12 @@ class ClineProvider extends BaseProvider {
181
208
  // Detect by directory existence, not config file
182
209
  super('Cline', configPath, false, baseDir);
183
210
  }
211
+
212
+ configureMemoryInstructions() {
213
+ // Use project-level file
214
+ const instructionPath = path.join(process.cwd(), 'CLINE.md');
215
+ return this.writeMemoryInstructions(instructionPath, 'Cline');
216
+ }
184
217
  }
185
218
 
186
219
  class CodexProvider extends BaseProvider {
@@ -194,7 +227,7 @@ class CodexProvider extends BaseProvider {
194
227
  if (!fs.existsSync(this.configPath)) return null;
195
228
  try {
196
229
  const content = fs.readFileSync(this.configPath, 'utf8');
197
- return this.parseTOML(content);
230
+ return TOML.parse(content);
198
231
  } catch (e) {
199
232
  return null;
200
233
  }
@@ -203,8 +236,8 @@ class CodexProvider extends BaseProvider {
203
236
  writeConfig(config) {
204
237
  try {
205
238
  fs.ensureFileSync(this.configPath);
206
- const tomlContent = this.generateTOML(config);
207
- fs.writeFileSync(this.configPath, tomlContent);
239
+ const tomlContent = TOML.stringify(config);
240
+ fs.writeFileSync(this.configPath, tomlContent, 'utf8');
208
241
  return true;
209
242
  } catch (e) {
210
243
  console.error(`Failed to write config: ${e.message}`);
@@ -212,65 +245,20 @@ class CodexProvider extends BaseProvider {
212
245
  }
213
246
  }
214
247
 
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
248
  configureMCP() {
267
249
  const config = this.readConfig() || { mcp_servers: {} };
268
- config.mcp_servers.vault = {
250
+ config.mcp_servers.unifiedmemory = {
269
251
  command: "um",
270
252
  args: ["mcp", "serve"]
271
253
  };
272
254
  return this.writeConfig(config);
273
255
  }
256
+
257
+ configureMemoryInstructions() {
258
+ // Use project-level file
259
+ const instructionPath = path.join(process.cwd(), 'CODEX.md');
260
+ return this.writeMemoryInstructions(instructionPath, 'Codex CLI');
261
+ }
274
262
  }
275
263
 
276
264
  class GeminiProvider extends BaseProvider {
@@ -279,6 +267,12 @@ class GeminiProvider extends BaseProvider {
279
267
  const detectionPath = path.dirname(configPath); // Detect by directory
280
268
  super('Gemini CLI', configPath, false, detectionPath);
281
269
  }
270
+
271
+ configureMemoryInstructions() {
272
+ // Use project-level file
273
+ const instructionPath = path.join(process.cwd(), 'GEMINI.md');
274
+ return this.writeMemoryInstructions(instructionPath, 'Gemini CLI');
275
+ }
282
276
  }
283
277
 
284
278
  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
+ }