@unifiedmemory/cli 1.3.1 → 1.3.7

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,96 @@
1
+ name: Test and Publish
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+ branches: [main]
8
+
9
+ jobs:
10
+ test:
11
+ runs-on: ubuntu-latest
12
+ strategy:
13
+ matrix:
14
+ node-version: [18, 20, 22]
15
+
16
+ steps:
17
+ - uses: actions/checkout@v4
18
+
19
+ - name: Setup Node.js ${{ matrix.node-version }}
20
+ uses: actions/setup-node@v4
21
+ with:
22
+ node-version: ${{ matrix.node-version }}
23
+ cache: 'npm'
24
+
25
+ - name: Install dependencies
26
+ run: npm ci
27
+
28
+ - name: Run tests
29
+ run: npm run test:ci
30
+ env:
31
+ CI: true
32
+
33
+ - name: Upload test results
34
+ uses: actions/upload-artifact@v4
35
+ if: always()
36
+ with:
37
+ name: test-results-node-${{ matrix.node-version }}
38
+ path: test-results.xml
39
+
40
+ publish:
41
+ needs: test
42
+ runs-on: ubuntu-latest
43
+ if: github.ref == 'refs/heads/main' && github.event_name == 'push'
44
+ permissions:
45
+ contents: write
46
+
47
+ steps:
48
+ - uses: actions/checkout@v4
49
+
50
+ - name: Setup Node.js
51
+ uses: actions/setup-node@v4
52
+ with:
53
+ node-version: 24
54
+ registry-url: 'https://registry.npmjs.org'
55
+ cache: 'npm'
56
+
57
+ - name: Install dependencies
58
+ run: npm ci
59
+
60
+ - name: Check if version changed
61
+ id: version-check
62
+ run: |
63
+ CURRENT_VERSION=$(node -p "require('./package.json').version")
64
+ NPM_VERSION=$(npm view @unifiedmemory/cli version 2>/dev/null || echo "0.0.0")
65
+ echo "current=$CURRENT_VERSION" >> $GITHUB_OUTPUT
66
+ echo "published=$NPM_VERSION" >> $GITHUB_OUTPUT
67
+ if [ "$CURRENT_VERSION" != "$NPM_VERSION" ]; then
68
+ echo "version_changed=true" >> $GITHUB_OUTPUT
69
+ echo "✅ Version changed: $NPM_VERSION → $CURRENT_VERSION"
70
+ else
71
+ echo "version_changed=false" >> $GITHUB_OUTPUT
72
+ echo "ℹ️ Version unchanged ($CURRENT_VERSION) - skipping publish"
73
+ fi
74
+
75
+ - name: Publish to npm
76
+ if: steps.version-check.outputs.version_changed == 'true'
77
+ run: npm publish --access public
78
+ env:
79
+ NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
80
+
81
+ - name: Create Git tag
82
+ if: steps.version-check.outputs.version_changed == 'true'
83
+ run: |
84
+ git config user.name "github-actions[bot]"
85
+ git config user.email "github-actions[bot]@users.noreply.github.com"
86
+ if ! git ls-remote --tags origin | grep -q "refs/tags/v${{ steps.version-check.outputs.current }}"; then
87
+ git tag v${{ steps.version-check.outputs.current }}
88
+ git push origin v${{ steps.version-check.outputs.current }}
89
+ else
90
+ echo "Tag v${{ steps.version-check.outputs.current }} already exists, skipping"
91
+ fi
92
+
93
+ - name: Skip publish notification
94
+ if: steps.version-check.outputs.version_changed == 'false'
95
+ run: |
96
+ echo "::notice::Skipped npm publish - version ${{ steps.version-check.outputs.current }} already published"
package/index.js CHANGED
@@ -4,6 +4,9 @@ import { Command } from 'commander';
4
4
  import chalk from 'chalk';
5
5
  import fs from 'fs-extra';
6
6
  import path from 'path';
7
+ import { readFileSync } from 'fs';
8
+ import { dirname, join } from 'path';
9
+ import { fileURLToPath } from 'url';
7
10
  import { login } from './commands/login.js';
8
11
  import { init } from './commands/init.js';
9
12
  import { switchOrg, showOrg } from './commands/org.js';
@@ -13,12 +16,15 @@ import { getSelectedOrg } from './lib/token-storage.js';
13
16
  import { loadAndRefreshToken } from './lib/token-validation.js';
14
17
  import { showWelcome } from './lib/welcome.js';
15
18
 
19
+ const __dirname = dirname(fileURLToPath(import.meta.url));
20
+ const packageJson = JSON.parse(readFileSync(join(__dirname, 'package.json'), 'utf8'));
21
+
16
22
  const program = new Command();
17
23
 
18
24
  program
19
25
  .name('um')
20
26
  .description('UnifiedMemory CLI - AI code assistant integration')
21
- .version('1.1.0');
27
+ .version(packageJson.version);
22
28
 
23
29
  // Unified command (primary)
24
30
  program
@@ -199,7 +205,7 @@ noteCommand
199
205
 
200
206
  // Show welcome splash if no command provided
201
207
  if (process.argv.length === 2) {
202
- showWelcome();
208
+ await showWelcome();
203
209
  program.help();
204
210
  }
205
211
 
package/lib/welcome.js CHANGED
@@ -2,22 +2,50 @@ import chalk from 'chalk';
2
2
  import { readFileSync } from 'fs';
3
3
  import { dirname, join } from 'path';
4
4
  import { fileURLToPath } from 'url';
5
+ import axios from 'axios';
5
6
 
6
7
  const __dirname = dirname(fileURLToPath(import.meta.url));
7
8
  const packageJson = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf8'));
8
9
  const version = packageJson.version;
9
10
 
10
- export function showWelcome() {
11
- const pink = chalk.hex('#FF69B4');
12
- const lightPink = chalk.hex('#FFB6C1');
13
- const darkPink = chalk.hex('#C71585');
11
+ const PACKAGE_NAME = '@unifiedmemory/cli';
12
+
13
+ /**
14
+ * Check npm registry for latest version (non-blocking)
15
+ * @returns {Promise<{latest: string, isOutdated: boolean} | null>}
16
+ */
17
+ export async function checkLatestVersion() {
18
+ try {
19
+ const response = await axios.get(
20
+ `https://registry.npmjs.org/${PACKAGE_NAME}/latest`,
21
+ { timeout: 2000 }
22
+ );
23
+ const latest = response.data.version;
24
+ const isOutdated = latest !== version;
25
+ return { latest, isOutdated };
26
+ } catch {
27
+ return null; // Silently fail - don't block CLI
28
+ }
29
+ }
30
+
31
+ export async function showWelcome() {
14
32
  const gray = chalk.gray;
15
33
  const white = chalk.white;
34
+ const yellow = chalk.yellow;
16
35
 
17
36
  console.log('');
18
37
  console.log(white.bold(' UnifiedMemory CLI ') + gray(`v${version}`));
19
38
  console.log(gray(' AI-powered knowledge assistant'));
20
39
  console.log('');
40
+
41
+ // Check for updates (non-blocking with short timeout)
42
+ const versionInfo = await checkLatestVersion();
43
+ if (versionInfo?.isOutdated) {
44
+ console.log(yellow(' ⚠ Update available: ') + white(`v${versionInfo.latest}`));
45
+ console.log(gray(' Run: ') + white('npm install -g @unifiedmemory/cli'));
46
+ console.log('');
47
+ }
48
+
21
49
  console.log(white(' Quick Start:'));
22
50
  console.log(gray(' um init ') + white('Initialize in current project'));
23
51
  console.log(gray(' um status ') + white('Check configuration'));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@unifiedmemory/cli",
3
- "version": "1.3.1",
3
+ "version": "1.3.7",
4
4
  "description": "UnifiedMemory CLI - AI code assistant integration",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -9,7 +9,13 @@
9
9
  },
10
10
  "scripts": {
11
11
  "start": "node index.js",
12
- "build": "pkg . --targets node18-linux-x64,node18-macos-x64,node18-win-x64 --output dist/um"
12
+ "build": "pkg . --targets node18-linux-x64,node18-macos-x64,node18-win-x64 --output dist/um",
13
+ "test": "vitest run",
14
+ "test:watch": "vitest",
15
+ "test:coverage": "vitest run --coverage",
16
+ "test:unit": "vitest run tests/unit",
17
+ "test:integration": "vitest run tests/integration",
18
+ "test:ci": "vitest run --reporter=junit --outputFile=test-results.xml"
13
19
  },
14
20
  "keywords": [
15
21
  "ai",
@@ -42,10 +48,16 @@
42
48
  "open": "^10.0.3"
43
49
  },
44
50
  "devDependencies": {
45
- "pkg": "^5.8.1"
51
+ "@vitest/coverage-v8": "^3.0.0",
52
+ "msw": "^2.7.0",
53
+ "pkg": "^5.8.1",
54
+ "vitest": "^3.0.0"
46
55
  },
47
56
  "repository": {
48
57
  "type": "git",
49
- "url": "https://github.com/unifiedmemory/um-cli.git"
58
+ "url": "https://github.com/Episodic-Solutions/um-cli.git"
59
+ },
60
+ "publishConfig": {
61
+ "access": "public"
50
62
  }
51
63
  }
@@ -0,0 +1,20 @@
1
+ {
2
+ "accessToken": "mock_expired_access_token",
3
+ "idToken": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VyXzEyMzQ1Njc4OSIsImVtYWlsIjoidGVzdEB0ZXN0LmNvbSIsImV4cCI6MTYwMDAwMDAwMCwiaWF0IjoxNTk5OTk2NDAwLCJzaWQiOiJzZXNzXzEyMzQ1Njc4OSJ9.mock_signature",
4
+ "refresh_token": "mock_refresh_token_for_expired",
5
+ "tokenType": "Bearer",
6
+ "expiresIn": 3600,
7
+ "receivedAt": 1599996400000,
8
+ "decoded": {
9
+ "sub": "user_123456789",
10
+ "email": "test@test.com",
11
+ "exp": 1600000000,
12
+ "iat": 1599996400,
13
+ "sid": "sess_123456789"
14
+ },
15
+ "selectedOrg": {
16
+ "id": "org_456789012",
17
+ "name": "Test Organization",
18
+ "role": "admin"
19
+ }
20
+ }
@@ -0,0 +1,21 @@
1
+ {
2
+ "accessToken": "mock_access_token_abc123",
3
+ "idToken": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VyXzEyMzQ1Njc4OSIsImVtYWlsIjoidGVzdEB0ZXN0LmNvbSIsImV4cCI6OTk5OTk5OTk5OSwiaWF0IjoxNjAwMDAwMDAwLCJzaWQiOiJzZXNzXzEyMzQ1Njc4OSIsIm9yZ19pZCI6Im9yZ180NTY3ODkwMTIifQ.mock_signature",
4
+ "refresh_token": "mock_refresh_token_xyz789",
5
+ "tokenType": "Bearer",
6
+ "expiresIn": 3600,
7
+ "receivedAt": 1700000000000,
8
+ "decoded": {
9
+ "sub": "user_123456789",
10
+ "email": "test@test.com",
11
+ "exp": 9999999999,
12
+ "iat": 1600000000,
13
+ "sid": "sess_123456789",
14
+ "org_id": "org_456789012"
15
+ },
16
+ "selectedOrg": {
17
+ "id": "org_456789012",
18
+ "name": "Test Organization",
19
+ "role": "admin"
20
+ }
21
+ }
@@ -0,0 +1,12 @@
1
+ {
2
+ "version": "1.0",
3
+ "org_id": "org_456789012",
4
+ "project_id": "proj_987654321",
5
+ "project_name": "test-project",
6
+ "api_url": "https://api.test.unifiedmemory.ai",
7
+ "created_at": "2024-01-01T00:00:00.000Z",
8
+ "mcp_config": {
9
+ "inject_headers": true,
10
+ "auto_context": true
11
+ }
12
+ }
@@ -0,0 +1,200 @@
1
+ /**
2
+ * MSW (Mock Service Worker) handlers for API mocking
3
+ *
4
+ * These handlers intercept HTTP requests during tests and return
5
+ * predefined responses without hitting real APIs.
6
+ */
7
+
8
+ import { http, HttpResponse } from 'msw';
9
+
10
+ const API_BASE_URL = 'https://rose-asp-main-1c0b114.d2.zuplo.dev';
11
+
12
+ // Sample project data
13
+ const mockProjects = [
14
+ {
15
+ id: 'proj_001',
16
+ name: 'project-one',
17
+ display_name: 'Project One',
18
+ slug: 'project-one',
19
+ description: 'First test project',
20
+ created_at: '2024-01-01T00:00:00.000Z',
21
+ },
22
+ {
23
+ id: 'proj_002',
24
+ name: 'project-two',
25
+ display_name: 'Project Two',
26
+ slug: 'project-two',
27
+ description: 'Second test project',
28
+ created_at: '2024-01-02T00:00:00.000Z',
29
+ },
30
+ ];
31
+
32
+ // Sample MCP tools data
33
+ const mockMCPTools = [
34
+ {
35
+ name: 'create_note',
36
+ description: 'Create a new note in the vault',
37
+ inputSchema: {
38
+ type: 'object',
39
+ properties: {
40
+ content: { type: 'string', description: 'Note content' },
41
+ pathParams: {
42
+ type: 'object',
43
+ properties: {
44
+ org: { type: 'string' },
45
+ proj: { type: 'string' },
46
+ },
47
+ required: ['org', 'proj'],
48
+ },
49
+ headers: {
50
+ type: 'object',
51
+ properties: {
52
+ 'X-Org-Id': { type: 'string' },
53
+ },
54
+ },
55
+ },
56
+ required: ['content', 'pathParams'],
57
+ },
58
+ },
59
+ {
60
+ name: 'search_notes',
61
+ description: 'Search notes in the vault',
62
+ inputSchema: {
63
+ type: 'object',
64
+ properties: {
65
+ query: { type: 'string', description: 'Search query' },
66
+ pathParams: {
67
+ type: 'object',
68
+ properties: {
69
+ org: { type: 'string' },
70
+ proj: { type: 'string' },
71
+ },
72
+ required: ['org', 'proj'],
73
+ },
74
+ },
75
+ required: ['query', 'pathParams'],
76
+ },
77
+ },
78
+ ];
79
+
80
+ /**
81
+ * Default API handlers for common endpoints
82
+ */
83
+ export const handlers = [
84
+ // GET /v1/orgs/:org/projects - List projects
85
+ http.get(`${API_BASE_URL}/v1/orgs/:org/projects`, ({ request, params }) => {
86
+ const authHeader = request.headers.get('Authorization');
87
+
88
+ if (!authHeader || !authHeader.startsWith('Bearer ')) {
89
+ return HttpResponse.json(
90
+ { error: 'Unauthorized', message: 'Missing or invalid authorization header' },
91
+ { status: 401 }
92
+ );
93
+ }
94
+
95
+ return HttpResponse.json({ items: mockProjects });
96
+ }),
97
+
98
+ // POST /v1/orgs/:org/projects - Create project
99
+ http.post(`${API_BASE_URL}/v1/orgs/:org/projects`, async ({ request, params }) => {
100
+ const authHeader = request.headers.get('Authorization');
101
+
102
+ if (!authHeader || !authHeader.startsWith('Bearer ')) {
103
+ return HttpResponse.json(
104
+ { error: 'Unauthorized', message: 'Missing or invalid authorization header' },
105
+ { status: 401 }
106
+ );
107
+ }
108
+
109
+ const body = await request.json();
110
+
111
+ return HttpResponse.json({
112
+ id: `proj_${Date.now()}`,
113
+ name: body.display_name.toLowerCase().replace(/\s+/g, '-'),
114
+ display_name: body.display_name,
115
+ slug: body.display_name.toLowerCase().replace(/\s+/g, '-'),
116
+ description: body.description || '',
117
+ created_at: new Date().toISOString(),
118
+ });
119
+ }),
120
+
121
+ // POST /mcp - MCP endpoint for tools/list
122
+ http.post(`${API_BASE_URL}/mcp`, async ({ request }) => {
123
+ const body = await request.json();
124
+
125
+ if (body.method === 'tools/list') {
126
+ return HttpResponse.json({
127
+ jsonrpc: '2.0',
128
+ id: body.id,
129
+ result: {
130
+ tools: mockMCPTools,
131
+ },
132
+ });
133
+ }
134
+
135
+ if (body.method === 'tools/call') {
136
+ return HttpResponse.json({
137
+ jsonrpc: '2.0',
138
+ id: body.id,
139
+ result: {
140
+ content: [
141
+ {
142
+ type: 'text',
143
+ text: `Tool ${body.params.name} executed successfully`,
144
+ },
145
+ ],
146
+ },
147
+ });
148
+ }
149
+
150
+ return HttpResponse.json(
151
+ {
152
+ jsonrpc: '2.0',
153
+ id: body.id,
154
+ error: { code: -32601, message: 'Method not found' },
155
+ },
156
+ { status: 400 }
157
+ );
158
+ }),
159
+
160
+ // GET /health - Health check
161
+ http.get(`${API_BASE_URL}/health`, () => {
162
+ return HttpResponse.json({ status: 'healthy' });
163
+ }),
164
+ ];
165
+
166
+ /**
167
+ * Handler that returns 401 for all requests (unauthorized)
168
+ */
169
+ export const unauthorizedHandlers = [
170
+ http.all(`${API_BASE_URL}/*`, () => {
171
+ return HttpResponse.json(
172
+ { error: 'Unauthorized', message: 'Token expired or invalid' },
173
+ { status: 401 }
174
+ );
175
+ }),
176
+ ];
177
+
178
+ /**
179
+ * Handler that returns 500 for all requests (server error)
180
+ */
181
+ export const serverErrorHandlers = [
182
+ http.all(`${API_BASE_URL}/*`, () => {
183
+ return HttpResponse.json(
184
+ { error: 'Internal Server Error', message: 'Something went wrong' },
185
+ { status: 500 }
186
+ );
187
+ }),
188
+ ];
189
+
190
+ /**
191
+ * Handler that simulates network failure
192
+ */
193
+ export const networkErrorHandlers = [
194
+ http.all(`${API_BASE_URL}/*`, () => {
195
+ return HttpResponse.error();
196
+ }),
197
+ ];
198
+
199
+ // Export mock data for use in tests
200
+ export { mockProjects, mockMCPTools };
@@ -0,0 +1,79 @@
1
+ /**
2
+ * Mock for token-storage.js
3
+ *
4
+ * Provides mock implementations of token storage functions
5
+ * that don't interact with the real filesystem.
6
+ */
7
+
8
+ import { vi } from 'vitest';
9
+ import mockAuthData from '../fixtures/mock-auth.json' with { type: 'json' };
10
+ import expiredAuthData from '../fixtures/expired-auth.json' with { type: 'json' };
11
+
12
+ /**
13
+ * Creates a mock token storage module with configurable behavior
14
+ * @param {Object} options - Configuration options
15
+ * @param {Object|null} options.initialToken - Initial token data to return (use null for no token)
16
+ * @param {boolean} options.expired - Whether to use expired token fixture
17
+ * @param {boolean} options.empty - Whether to start with no token
18
+ * @returns {Object} Mock token storage functions
19
+ */
20
+ export function createTokenStorageMock(options = {}) {
21
+ let tokenData;
22
+
23
+ if (options.empty) {
24
+ tokenData = null;
25
+ } else if (options.expired) {
26
+ tokenData = { ...expiredAuthData };
27
+ } else if (options.initialToken !== undefined) {
28
+ tokenData = options.initialToken;
29
+ } else {
30
+ tokenData = { ...mockAuthData };
31
+ }
32
+
33
+ return {
34
+ saveToken: vi.fn((data) => {
35
+ tokenData = data;
36
+ }),
37
+
38
+ getToken: vi.fn(() => tokenData),
39
+
40
+ clearToken: vi.fn(() => {
41
+ tokenData = null;
42
+ }),
43
+
44
+ updateSelectedOrg: vi.fn((orgData) => {
45
+ if (tokenData) {
46
+ tokenData.selectedOrg = orgData;
47
+ } else {
48
+ throw new Error('No token found. Please login first.');
49
+ }
50
+ }),
51
+
52
+ getSelectedOrg: vi.fn(() => tokenData?.selectedOrg || null),
53
+
54
+ // Helper to reset state between tests
55
+ _reset: (newData = null) => {
56
+ tokenData = newData ?? { ...mockAuthData };
57
+ },
58
+
59
+ // Helper to get current state for assertions
60
+ _getState: () => tokenData,
61
+ };
62
+ }
63
+
64
+ /**
65
+ * Creates mock token storage that returns null (no token)
66
+ */
67
+ export function createEmptyTokenStorageMock() {
68
+ return createTokenStorageMock({ empty: true });
69
+ }
70
+
71
+ /**
72
+ * Creates mock token storage with expired token
73
+ */
74
+ export function createExpiredTokenStorageMock() {
75
+ return createTokenStorageMock({ expired: true });
76
+ }
77
+
78
+ // Export fixtures for direct use in tests
79
+ export { mockAuthData, expiredAuthData };
package/tests/setup.js ADDED
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Vitest Global Test Setup
3
+ *
4
+ * This file runs before all tests and sets up the test environment.
5
+ * It configures mocks and ensures tests don't interact with real auth.
6
+ */
7
+
8
+ import { vi, beforeAll, afterAll, afterEach } from 'vitest';
9
+
10
+ // Ensure we're in test mode
11
+ process.env.NODE_ENV = 'test';
12
+
13
+ // Mock console.error to reduce noise in tests (optional)
14
+ // Uncomment if you want to suppress console output during tests:
15
+ // vi.spyOn(console, 'error').mockImplementation(() => {});
16
+ // vi.spyOn(console, 'log').mockImplementation(() => {});
17
+
18
+ beforeAll(() => {
19
+ // Global setup before all tests
20
+ });
21
+
22
+ afterEach(() => {
23
+ // Clean up mocks after each test
24
+ vi.restoreAllMocks();
25
+ });
26
+
27
+ afterAll(() => {
28
+ // Global cleanup after all tests
29
+ });