@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/commands/org.js CHANGED
@@ -1,8 +1,8 @@
1
- import inquirer from 'inquirer';
2
1
  import chalk from 'chalk';
3
2
  import { updateSelectedOrg, getSelectedOrg } from '../lib/token-storage.js';
4
3
  import { loadAndRefreshToken } from '../lib/token-validation.js';
5
- import { getUserOrganizations, getOrganizationsFromToken, formatOrganization } from '../lib/clerk-api.js';
4
+ import { getUserOrganizations, getOrganizationsFromToken } from '../lib/clerk-api.js';
5
+ import { promptOrganizationSelection, displayOrganizationSelection } from '../lib/org-selection-ui.js';
6
6
 
7
7
  /**
8
8
  * Switch organization context
@@ -40,49 +40,20 @@ export async function switchOrg() {
40
40
  process.exit(0);
41
41
  }
42
42
 
43
- console.log(chalk.green(`\nFound ${memberships.length} organization(s)!`));
44
-
45
- // Format organizations for display
46
- const formattedOrgs = memberships.map(formatOrganization);
47
-
48
43
  // Get current selection
49
44
  const currentOrg = getSelectedOrg();
50
- const currentOrgId = currentOrg?.id;
51
-
52
- // Build choices for inquirer
53
- const choices = [
54
- {
55
- name: chalk.cyan('Personal Account') + chalk.gray(' (no organization)') + (currentOrgId ? '' : chalk.green(' ← current')),
56
- value: null,
57
- short: 'Personal Account',
58
- },
59
- new inquirer.Separator(chalk.gray('--- Organizations ---')),
60
- ...formattedOrgs.map(org => ({
61
- name: `${chalk.green(org.name)} ${chalk.gray(`(${org.slug})`)} ${chalk.yellow(`[${org.role}]`)}${org.id === currentOrgId ? chalk.green(' ← current') : ''}`,
62
- value: org,
63
- short: org.name,
64
- })),
65
- ];
66
45
 
67
46
  // Prompt user to select
68
- const answer = await inquirer.prompt([
69
- {
70
- type: 'list',
71
- name: 'organization',
72
- message: 'Select account context:',
73
- choices: choices,
74
- pageSize: 15,
75
- default: currentOrgId ? formattedOrgs.findIndex(org => org.id === currentOrgId) + 2 : 0, // +2 for personal + separator
76
- },
77
- ]);
47
+ const selectedOrg = await promptOrganizationSelection(memberships, currentOrg);
78
48
 
79
49
  // Update selected organization
80
- updateSelectedOrg(answer.organization);
50
+ updateSelectedOrg(selectedOrg);
81
51
 
82
- if (answer.organization) {
83
- console.log(chalk.green(`\n✅ Switched to organization: ${chalk.bold(answer.organization.name)}`));
84
- console.log(chalk.gray(` Organization ID: ${answer.organization.id}`));
85
- console.log(chalk.gray(` Your role: ${answer.organization.role}`));
52
+ // Display result (with "Switched to" instead of "Selected")
53
+ if (selectedOrg) {
54
+ console.log(chalk.green(`\n✅ Switched to organization: ${chalk.bold(selectedOrg.name)}`));
55
+ console.log(chalk.gray(` Organization ID: ${selectedOrg.id}`));
56
+ console.log(chalk.gray(` Your role: ${selectedOrg.role}`));
86
57
  } else {
87
58
  console.log(chalk.green('\n✅ Switched to personal account context'));
88
59
  }
package/index.js CHANGED
@@ -11,13 +11,14 @@ import { record } from './commands/record.js';
11
11
  import { config } from './lib/config.js';
12
12
  import { getSelectedOrg } from './lib/token-storage.js';
13
13
  import { loadAndRefreshToken } from './lib/token-validation.js';
14
+ import { showWelcome } from './lib/welcome.js';
14
15
 
15
16
  const program = new Command();
16
17
 
17
18
  program
18
19
  .name('um')
19
20
  .description('UnifiedMemory CLI - AI code assistant integration')
20
- .version('1.0.0');
21
+ .version('1.1.0');
21
22
 
22
23
  // Unified command (primary)
23
24
  program
@@ -36,11 +37,19 @@ program
36
37
  }
37
38
  });
38
39
 
40
+ // Welcome command
41
+ program
42
+ .command('welcome')
43
+ .description('Show welcome message')
44
+ .action(() => {
45
+ showWelcome();
46
+ process.exit(0);
47
+ });
48
+
39
49
  // Individual commands (power users)
40
50
  program
41
51
  .command('login')
42
52
  .description('Login to UnifiedMemory')
43
- .option('--device', 'Use device flow for headless environments')
44
53
  .action(async (options) => {
45
54
  try {
46
55
  await login();
@@ -113,30 +122,6 @@ program
113
122
  }
114
123
  });
115
124
 
116
- // Project management command
117
- program
118
- .command('project')
119
- .description('Manage project configuration')
120
- .option('--create', 'Create new project')
121
- .option('--link <id>', 'Link existing project')
122
- .action(async (options) => {
123
- console.log(chalk.yellow('Project management not yet implemented'));
124
- console.log(chalk.gray('Use `um init` to configure a project'));
125
- process.exit(0);
126
- });
127
-
128
- // Configure command
129
- program
130
- .command('configure')
131
- .description('Configure AI code assistants')
132
- .option('--all', 'Configure all detected assistants')
133
- .option('--provider <name>', 'Configure specific provider')
134
- .action(async (options) => {
135
- console.log(chalk.yellow('Provider configuration not yet implemented'));
136
- console.log(chalk.gray('Use `um init` to auto-configure all providers'));
137
- process.exit(0);
138
- });
139
-
140
125
  // Organization management
141
126
  const orgCommand = program
142
127
  .command('org')
@@ -212,4 +197,10 @@ noteCommand
212
197
  }
213
198
  });
214
199
 
200
+ // Show welcome splash if no command provided
201
+ if (process.argv.length === 2) {
202
+ showWelcome();
203
+ program.help();
204
+ }
205
+
215
206
  program.parse();
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
+ }