@memnexus-ai/cli 0.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.
Files changed (148) hide show
  1. package/.env.example +13 -0
  2. package/.eslintrc.js +24 -0
  3. package/.github/ISSUE_TEMPLATE/phase-1-foundation.md +1078 -0
  4. package/.github/workflows/publish.yml +277 -0
  5. package/.github/workflows/test-app-token.yml +54 -0
  6. package/.npmrc.backup +3 -0
  7. package/.npmrc.example +6 -0
  8. package/.prettierignore +4 -0
  9. package/.prettierrc +8 -0
  10. package/CHANGELOG.md +138 -0
  11. package/PLATFORM_TESTING.md +243 -0
  12. package/README.md +986 -0
  13. package/RELEASE.md +428 -0
  14. package/RELEASE_READINESS.md +253 -0
  15. package/USAGE.md +1373 -0
  16. package/bin/mx.js +2 -0
  17. package/dist/commands/apikeys.d.ts +7 -0
  18. package/dist/commands/apikeys.d.ts.map +1 -0
  19. package/dist/commands/apikeys.js +133 -0
  20. package/dist/commands/apikeys.js.map +1 -0
  21. package/dist/commands/artifacts.d.ts +7 -0
  22. package/dist/commands/artifacts.d.ts.map +1 -0
  23. package/dist/commands/artifacts.js +277 -0
  24. package/dist/commands/artifacts.js.map +1 -0
  25. package/dist/commands/auth.d.ts +7 -0
  26. package/dist/commands/auth.d.ts.map +1 -0
  27. package/dist/commands/auth.js +119 -0
  28. package/dist/commands/auth.js.map +1 -0
  29. package/dist/commands/communities.d.ts +7 -0
  30. package/dist/commands/communities.d.ts.map +1 -0
  31. package/dist/commands/communities.js +137 -0
  32. package/dist/commands/communities.js.map +1 -0
  33. package/dist/commands/config.d.ts +7 -0
  34. package/dist/commands/config.d.ts.map +1 -0
  35. package/dist/commands/config.js +138 -0
  36. package/dist/commands/config.js.map +1 -0
  37. package/dist/commands/conversations.d.ts +7 -0
  38. package/dist/commands/conversations.d.ts.map +1 -0
  39. package/dist/commands/conversations.js +160 -0
  40. package/dist/commands/conversations.js.map +1 -0
  41. package/dist/commands/facts.d.ts +7 -0
  42. package/dist/commands/facts.d.ts.map +1 -0
  43. package/dist/commands/facts.js +298 -0
  44. package/dist/commands/facts.js.map +1 -0
  45. package/dist/commands/graphrag.d.ts +7 -0
  46. package/dist/commands/graphrag.d.ts.map +1 -0
  47. package/dist/commands/graphrag.js +139 -0
  48. package/dist/commands/graphrag.js.map +1 -0
  49. package/dist/commands/memories.d.ts +7 -0
  50. package/dist/commands/memories.d.ts.map +1 -0
  51. package/dist/commands/memories.js +304 -0
  52. package/dist/commands/memories.js.map +1 -0
  53. package/dist/commands/patterns.d.ts +7 -0
  54. package/dist/commands/patterns.d.ts.map +1 -0
  55. package/dist/commands/patterns.js +227 -0
  56. package/dist/commands/patterns.js.map +1 -0
  57. package/dist/commands/system.d.ts +7 -0
  58. package/dist/commands/system.d.ts.map +1 -0
  59. package/dist/commands/system.js +97 -0
  60. package/dist/commands/system.js.map +1 -0
  61. package/dist/commands/topics.d.ts +7 -0
  62. package/dist/commands/topics.d.ts.map +1 -0
  63. package/dist/commands/topics.js +314 -0
  64. package/dist/commands/topics.js.map +1 -0
  65. package/dist/index.d.ts +3 -0
  66. package/dist/index.d.ts.map +1 -0
  67. package/dist/index.js +44 -0
  68. package/dist/index.js.map +1 -0
  69. package/dist/lib/api-client.d.ts +29 -0
  70. package/dist/lib/api-client.d.ts.map +1 -0
  71. package/dist/lib/api-client.js +64 -0
  72. package/dist/lib/api-client.js.map +1 -0
  73. package/dist/lib/auth.d.ts +10 -0
  74. package/dist/lib/auth.d.ts.map +1 -0
  75. package/dist/lib/auth.js +47 -0
  76. package/dist/lib/auth.js.map +1 -0
  77. package/dist/lib/config.d.ts +19 -0
  78. package/dist/lib/config.d.ts.map +1 -0
  79. package/dist/lib/config.js +59 -0
  80. package/dist/lib/config.js.map +1 -0
  81. package/dist/lib/errors.d.ts +7 -0
  82. package/dist/lib/errors.d.ts.map +1 -0
  83. package/dist/lib/errors.js +133 -0
  84. package/dist/lib/errors.js.map +1 -0
  85. package/dist/lib/formatters.d.ts +12 -0
  86. package/dist/lib/formatters.d.ts.map +1 -0
  87. package/dist/lib/formatters.js +103 -0
  88. package/dist/lib/formatters.js.map +1 -0
  89. package/dist/lib/spinner.d.ts +54 -0
  90. package/dist/lib/spinner.d.ts.map +1 -0
  91. package/dist/lib/spinner.js +108 -0
  92. package/dist/lib/spinner.js.map +1 -0
  93. package/dist/lib/validators.d.ts +92 -0
  94. package/dist/lib/validators.d.ts.map +1 -0
  95. package/dist/lib/validators.js +257 -0
  96. package/dist/lib/validators.js.map +1 -0
  97. package/dist/types/index.d.ts +13 -0
  98. package/dist/types/index.d.ts.map +1 -0
  99. package/dist/types/index.js +3 -0
  100. package/dist/types/index.js.map +1 -0
  101. package/docs/README.md +219 -0
  102. package/docs/code-generation-strategy.md +560 -0
  103. package/docs/prd.md +748 -0
  104. package/docs/sync-strategy.md +533 -0
  105. package/jest.config.js +30 -0
  106. package/package.json +67 -0
  107. package/scripts/install-deps.sh +38 -0
  108. package/src/commands/apikeys.ts +144 -0
  109. package/src/commands/artifacts.ts +296 -0
  110. package/src/commands/auth.ts +122 -0
  111. package/src/commands/communities.ts +153 -0
  112. package/src/commands/config.ts +144 -0
  113. package/src/commands/conversations.ts +176 -0
  114. package/src/commands/facts.ts +320 -0
  115. package/src/commands/graphrag.ts +149 -0
  116. package/src/commands/memories.ts +332 -0
  117. package/src/commands/patterns.ts +251 -0
  118. package/src/commands/system.ts +102 -0
  119. package/src/commands/topics.ts +354 -0
  120. package/src/index.ts +43 -0
  121. package/src/lib/api-client.ts +68 -0
  122. package/src/lib/auth.ts +42 -0
  123. package/src/lib/config.ts +68 -0
  124. package/src/lib/errors.ts +143 -0
  125. package/src/lib/formatters.ts +123 -0
  126. package/src/lib/spinner.ts +113 -0
  127. package/src/lib/validators.ts +302 -0
  128. package/src/types/index.ts +17 -0
  129. package/tests/__mocks__/chalk.ts +16 -0
  130. package/tests/__mocks__/cli-table3.ts +37 -0
  131. package/tests/__mocks__/configstore.ts +38 -0
  132. package/tests/commands/apikeys.test.ts +179 -0
  133. package/tests/commands/artifacts.test.ts +194 -0
  134. package/tests/commands/auth.test.ts +120 -0
  135. package/tests/commands/communities.test.ts +154 -0
  136. package/tests/commands/config.test.ts +154 -0
  137. package/tests/commands/conversations.test.ts +136 -0
  138. package/tests/commands/facts.test.ts +210 -0
  139. package/tests/commands/graphrag.test.ts +194 -0
  140. package/tests/commands/memories.test.ts +215 -0
  141. package/tests/commands/patterns.test.ts +201 -0
  142. package/tests/commands/system.test.ts +172 -0
  143. package/tests/commands/topics.test.ts +274 -0
  144. package/tests/lib/auth.test.ts +77 -0
  145. package/tests/lib/config.test.ts +50 -0
  146. package/tests/lib/errors.test.ts +126 -0
  147. package/tests/lib/formatters.test.ts +87 -0
  148. package/tsconfig.json +20 -0
@@ -0,0 +1,1078 @@
1
+ ---
2
+ name: Phase 1 - Foundation (Week 1)
3
+ about: Set up project infrastructure and core utilities for mx-cli v1.0
4
+ title: 'Phase 1: Foundation - Project Infrastructure & Core Utilities'
5
+ labels: 'enhancement, infrastructure, phase-1'
6
+ assignees: ''
7
+ ---
8
+
9
+ # Phase 1: Foundation (Week 1)
10
+
11
+ ## Goal
12
+ Set up project infrastructure and core utilities to enable rapid CLI command development in subsequent phases.
13
+
14
+ ## Overview
15
+ This phase establishes the foundational architecture for the MemNexus CLI. By the end of this phase, we'll have a working TypeScript project structure with authentication, configuration management, and output formatting ready for command implementations.
16
+
17
+ ## Success Criteria
18
+ - ✅ TypeScript project compiles without errors
19
+ - ✅ Can install `@memnexus-ai/contracts` and use types
20
+ - ✅ Configuration can be saved/loaded from disk
21
+ - ✅ API client connects to mx-core-api successfully
22
+ - ✅ Output formatters work for JSON, table, and YAML
23
+ - ✅ All utility tests pass with >80% coverage
24
+ - ✅ ESLint and Prettier configured and passing
25
+
26
+ ---
27
+
28
+ ## Tasks
29
+
30
+ ### 1. Initialize TypeScript Project
31
+ **Estimated Time**: 1-2 hours
32
+
33
+ **Description**: Set up the basic Node.js + TypeScript project structure
34
+
35
+ **Steps**:
36
+ ```bash
37
+ cd /workspaces/memnexus/mx-cli
38
+
39
+ # Initialize package.json
40
+ npm init -y
41
+
42
+ # Install TypeScript and core dependencies
43
+ npm install --save-dev typescript @types/node ts-node
44
+
45
+ # Install production dependencies
46
+ npm install commander chalk ora cli-table3 inquirer configstore dotenv
47
+
48
+ # Install type definitions
49
+ npm install --save-dev @types/inquirer
50
+ ```
51
+
52
+ **Create `package.json`**:
53
+ ```json
54
+ {
55
+ "name": "@memnexus-ai/cli",
56
+ "version": "0.1.0",
57
+ "description": "Command-line interface for MemNexus Core API",
58
+ "main": "dist/index.js",
59
+ "bin": {
60
+ "mx": "./bin/mx.js"
61
+ },
62
+ "scripts": {
63
+ "build": "tsc",
64
+ "dev": "ts-node src/index.ts",
65
+ "test": "jest",
66
+ "test:watch": "jest --watch",
67
+ "test:coverage": "jest --coverage",
68
+ "lint": "eslint 'src/**/*.ts'",
69
+ "lint:fix": "eslint 'src/**/*.ts' --fix",
70
+ "format": "prettier --write 'src/**/*.ts'",
71
+ "prepublishOnly": "npm run build"
72
+ },
73
+ "keywords": ["memnexus", "cli", "api", "memory", "ai"],
74
+ "author": "MemNexus Team",
75
+ "license": "MIT",
76
+ "repository": {
77
+ "type": "git",
78
+ "url": "https://github.com/memnexus-ai/memnexus.git",
79
+ "directory": "mx-cli"
80
+ },
81
+ "publishConfig": {
82
+ "registry": "https://npm.pkg.github.com",
83
+ "@memnexus-ai:registry": "https://npm.pkg.github.com"
84
+ },
85
+ "engines": {
86
+ "node": ">=18.0.0"
87
+ }
88
+ }
89
+ ```
90
+
91
+ **Create `tsconfig.json`**:
92
+ ```json
93
+ {
94
+ "compilerOptions": {
95
+ "target": "ES2022",
96
+ "module": "commonjs",
97
+ "lib": ["ES2022"],
98
+ "outDir": "./dist",
99
+ "rootDir": "./src",
100
+ "strict": true,
101
+ "esModuleInterop": true,
102
+ "skipLibCheck": true,
103
+ "forceConsistentCasingInFileNames": true,
104
+ "resolveJsonModule": true,
105
+ "declaration": true,
106
+ "declarationMap": true,
107
+ "sourceMap": true,
108
+ "moduleResolution": "node"
109
+ },
110
+ "include": ["src/**/*"],
111
+ "exclude": ["node_modules", "dist", "tests"]
112
+ }
113
+ ```
114
+
115
+ **Acceptance Criteria**:
116
+ - [x] `npm run build` compiles successfully
117
+ - [x] TypeScript strict mode enabled
118
+ - [x] Source maps generated for debugging
119
+
120
+ ---
121
+
122
+ ### 2. Add @memnexus-ai/contracts Dependency
123
+ **Estimated Time**: 30 minutes
124
+
125
+ **Description**: Install and configure the contracts package for API client access
126
+
127
+ **Steps**:
128
+ ```bash
129
+ # Configure npm to use GitHub Packages
130
+ echo "@memnexus-ai:registry=https://npm.pkg.github.com" >> .npmrc
131
+
132
+ # Install contracts package (requires GitHub authentication)
133
+ npm install @memnexus-ai/contracts@^1.0.4
134
+ ```
135
+
136
+ **Create `.npmrc`**:
137
+ ```
138
+ @memnexus-ai:registry=https://npm.pkg.github.com
139
+ //npm.pkg.github.com/:_authToken=${GITHUB_TOKEN}
140
+ ```
141
+
142
+ **Test import**:
143
+ ```typescript
144
+ // src/lib/api-client.ts
145
+ import { CoreApiClient, createClient } from '@memnexus-ai/contracts';
146
+
147
+ // Verify types are available
148
+ const client: CoreApiClient = createClient({
149
+ baseURL: 'http://localhost:3000',
150
+ apiKey: 'test'
151
+ });
152
+ ```
153
+
154
+ **Acceptance Criteria**:
155
+ - [x] Package installs without errors
156
+ - [x] TypeScript recognizes all imports
157
+ - [x] Types from contracts package are available
158
+
159
+ ---
160
+
161
+ ### 3. Configure TypeScript, ESLint, Prettier
162
+ **Estimated Time**: 1-2 hours
163
+
164
+ **Description**: Set up code quality tools for consistent style and error prevention
165
+
166
+ **Install Dev Dependencies**:
167
+ ```bash
168
+ npm install --save-dev \
169
+ eslint \
170
+ @typescript-eslint/parser \
171
+ @typescript-eslint/eslint-plugin \
172
+ prettier \
173
+ eslint-config-prettier \
174
+ eslint-plugin-prettier
175
+ ```
176
+
177
+ **Create `.eslintrc.js`**:
178
+ ```javascript
179
+ module.exports = {
180
+ parser: '@typescript-eslint/parser',
181
+ extends: [
182
+ 'eslint:recommended',
183
+ 'plugin:@typescript-eslint/recommended',
184
+ 'prettier'
185
+ ],
186
+ plugins: ['@typescript-eslint', 'prettier'],
187
+ parserOptions: {
188
+ ecmaVersion: 2022,
189
+ sourceType: 'module',
190
+ project: './tsconfig.json'
191
+ },
192
+ rules: {
193
+ 'prettier/prettier': 'error',
194
+ '@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
195
+ '@typescript-eslint/explicit-function-return-type': 'off',
196
+ '@typescript-eslint/no-explicit-any': 'warn'
197
+ },
198
+ env: {
199
+ node: true,
200
+ es2022: true
201
+ }
202
+ };
203
+ ```
204
+
205
+ **Create `.prettierrc`**:
206
+ ```json
207
+ {
208
+ "semi": true,
209
+ "trailingComma": "es5",
210
+ "singleQuote": true,
211
+ "printWidth": 100,
212
+ "tabWidth": 2,
213
+ "arrowParens": "always"
214
+ }
215
+ ```
216
+
217
+ **Create `.prettierignore`**:
218
+ ```
219
+ node_modules
220
+ dist
221
+ coverage
222
+ *.md
223
+ ```
224
+
225
+ **Acceptance Criteria**:
226
+ - [x] `npm run lint` passes
227
+ - [x] `npm run format` formats all files
228
+ - [x] ESLint catches type errors and style issues
229
+
230
+ ---
231
+
232
+ ### 4. Set Up Project Structure
233
+ **Estimated Time**: 30 minutes
234
+
235
+ **Description**: Create the directory structure for organized code
236
+
237
+ **Create directories**:
238
+ ```bash
239
+ mkdir -p src/{commands,lib,types}
240
+ mkdir -p tests/{commands,lib}
241
+ mkdir -p bin
242
+ ```
243
+
244
+ **Create initial files**:
245
+
246
+ `src/index.ts`:
247
+ ```typescript
248
+ #!/usr/bin/env node
249
+ import { Command } from 'commander';
250
+ import packageJson from '../package.json';
251
+
252
+ const program = new Command();
253
+
254
+ program
255
+ .name('mx')
256
+ .description('MemNexus CLI - Command-line interface for MemNexus Core API')
257
+ .version(packageJson.version);
258
+
259
+ // Commands will be registered here
260
+
261
+ program.parse(process.argv);
262
+
263
+ // Show help if no command provided
264
+ if (!process.argv.slice(2).length) {
265
+ program.outputHelp();
266
+ }
267
+ ```
268
+
269
+ `bin/mx.js`:
270
+ ```javascript
271
+ #!/usr/bin/env node
272
+ require('../dist/index.js');
273
+ ```
274
+
275
+ `src/types/index.ts`:
276
+ ```typescript
277
+ // Re-export types from contracts
278
+ export type * from '@memnexus-ai/contracts';
279
+
280
+ // CLI-specific types
281
+ export interface CLIConfig {
282
+ apiUrl: string;
283
+ apiKey?: string;
284
+ defaultFormat: 'json' | 'table' | 'yaml';
285
+ defaultPageSize: number;
286
+ }
287
+
288
+ export interface CommandOptions {
289
+ format?: 'json' | 'table' | 'yaml';
290
+ verbose?: boolean;
291
+ }
292
+ ```
293
+
294
+ **Acceptance Criteria**:
295
+ - [x] All directories created
296
+ - [x] CLI entry point exists
297
+ - [x] Can run `npm run dev` without errors
298
+
299
+ ---
300
+
301
+ ### 5. Implement Configuration Management
302
+ **Estimated Time**: 2-3 hours
303
+
304
+ **Description**: Create config storage using configstore for persistent settings
305
+
306
+ **Create `src/lib/config.ts`**:
307
+ ```typescript
308
+ import Configstore from 'configstore';
309
+ import { CLIConfig } from '../types';
310
+ import packageJson from '../../package.json';
311
+
312
+ const DEFAULT_CONFIG: CLIConfig = {
313
+ apiUrl: 'https://api.memnexus.io',
314
+ defaultFormat: 'table',
315
+ defaultPageSize: 20,
316
+ };
317
+
318
+ class ConfigManager {
319
+ private store: Configstore;
320
+
321
+ constructor() {
322
+ this.store = new Configstore(packageJson.name, DEFAULT_CONFIG);
323
+ }
324
+
325
+ get(key?: keyof CLIConfig): CLIConfig | string | number | undefined {
326
+ if (key) {
327
+ return this.store.get(key);
328
+ }
329
+ return this.store.all as CLIConfig;
330
+ }
331
+
332
+ set(key: keyof CLIConfig, value: string | number): void {
333
+ this.store.set(key, value);
334
+ }
335
+
336
+ delete(key: keyof CLIConfig): void {
337
+ this.store.delete(key);
338
+ }
339
+
340
+ clear(): void {
341
+ this.store.clear();
342
+ }
343
+
344
+ reset(): void {
345
+ this.store.all = DEFAULT_CONFIG;
346
+ }
347
+
348
+ getApiUrl(): string {
349
+ return process.env.MX_API_URL || this.store.get('apiUrl') || DEFAULT_CONFIG.apiUrl;
350
+ }
351
+
352
+ getApiKey(): string | undefined {
353
+ return process.env.MX_API_KEY || this.store.get('apiKey');
354
+ }
355
+
356
+ getFormat(): 'json' | 'table' | 'yaml' {
357
+ return (
358
+ (process.env.MX_OUTPUT_FORMAT as 'json' | 'table' | 'yaml') ||
359
+ this.store.get('defaultFormat') ||
360
+ DEFAULT_CONFIG.defaultFormat
361
+ );
362
+ }
363
+
364
+ setApiKey(apiKey: string): void {
365
+ // TODO: Encrypt API key before storing
366
+ this.store.set('apiKey', apiKey);
367
+ }
368
+
369
+ hasApiKey(): boolean {
370
+ return !!(process.env.MX_API_KEY || this.store.get('apiKey'));
371
+ }
372
+ }
373
+
374
+ export const config = new ConfigManager();
375
+ export const getConfig = (): CLIConfig => config.get() as CLIConfig;
376
+ ```
377
+
378
+ **Create tests `tests/lib/config.test.ts`**:
379
+ ```typescript
380
+ import { config } from '../../src/lib/config';
381
+
382
+ describe('ConfigManager', () => {
383
+ beforeEach(() => {
384
+ config.reset();
385
+ });
386
+
387
+ it('should return default config', () => {
388
+ const cfg = config.get();
389
+ expect(cfg).toHaveProperty('apiUrl');
390
+ expect(cfg).toHaveProperty('defaultFormat');
391
+ });
392
+
393
+ it('should set and get values', () => {
394
+ config.set('apiUrl', 'http://localhost:3000');
395
+ expect(config.get('apiUrl')).toBe('http://localhost:3000');
396
+ });
397
+
398
+ it('should respect environment variables', () => {
399
+ process.env.MX_API_URL = 'http://env-url:3000';
400
+ expect(config.getApiUrl()).toBe('http://env-url:3000');
401
+ delete process.env.MX_API_URL;
402
+ });
403
+
404
+ it('should reset to defaults', () => {
405
+ config.set('apiUrl', 'http://custom:3000');
406
+ config.reset();
407
+ expect(config.get('apiUrl')).toBe('https://api.memnexus.io');
408
+ });
409
+ });
410
+ ```
411
+
412
+ **Acceptance Criteria**:
413
+ - [x] Config persists between CLI runs
414
+ - [x] Environment variables override config
415
+ - [x] Can reset to defaults
416
+ - [x] Tests pass
417
+
418
+ ---
419
+
420
+ ### 6. Create API Client Wrapper
421
+ **Estimated Time**: 1-2 hours
422
+
423
+ **Description**: Wrapper around @memnexus-ai/contracts with config integration
424
+
425
+ **Create `src/lib/api-client.ts`**:
426
+ ```typescript
427
+ import { CoreApiClient, createClient } from '@memnexus-ai/contracts';
428
+ import { config } from './config';
429
+ import chalk from 'chalk';
430
+
431
+ let cachedClient: CoreApiClient | null = null;
432
+
433
+ export function getApiClient(): CoreApiClient {
434
+ if (cachedClient) {
435
+ return cachedClient;
436
+ }
437
+
438
+ const apiKey = config.getApiKey();
439
+ if (!apiKey) {
440
+ console.error(chalk.red('Error: No API key configured.'));
441
+ console.log(chalk.yellow('Run: mx auth login --api-key <your-key>'));
442
+ process.exit(1);
443
+ }
444
+
445
+ const apiUrl = config.getApiUrl();
446
+
447
+ cachedClient = createClient({
448
+ baseURL: apiUrl,
449
+ apiKey: apiKey,
450
+ timeout: 30000,
451
+ });
452
+
453
+ return cachedClient;
454
+ }
455
+
456
+ export function resetApiClient(): void {
457
+ cachedClient = null;
458
+ }
459
+
460
+ export { CoreApiClient };
461
+ ```
462
+
463
+ **Create tests `tests/lib/api-client.test.ts`**:
464
+ ```typescript
465
+ import { getApiClient, resetApiClient } from '../../src/lib/api-client';
466
+ import { config } from '../../src/lib/config';
467
+
468
+ describe('API Client', () => {
469
+ beforeEach(() => {
470
+ resetApiClient();
471
+ config.reset();
472
+ });
473
+
474
+ it('should create client with config', () => {
475
+ config.setApiKey('test-key');
476
+ const client = getApiClient();
477
+ expect(client).toBeDefined();
478
+ });
479
+
480
+ it('should exit if no API key', () => {
481
+ const exitSpy = jest.spyOn(process, 'exit').mockImplementation();
482
+ getApiClient();
483
+ expect(exitSpy).toHaveBeenCalledWith(1);
484
+ exitSpy.mockRestore();
485
+ });
486
+
487
+ it('should cache client instance', () => {
488
+ config.setApiKey('test-key');
489
+ const client1 = getApiClient();
490
+ const client2 = getApiClient();
491
+ expect(client1).toBe(client2);
492
+ });
493
+ });
494
+ ```
495
+
496
+ **Acceptance Criteria**:
497
+ - [x] Client initializes with config values
498
+ - [x] Error shown if API key missing
499
+ - [x] Client is cached (singleton pattern)
500
+ - [x] Tests pass
501
+
502
+ ---
503
+
504
+ ### 7. Create Authentication Helpers
505
+ **Estimated Time**: 1-2 hours
506
+
507
+ **Description**: Helper functions for API key storage and validation
508
+
509
+ **Create `src/lib/auth.ts`**:
510
+ ```typescript
511
+ import { config } from './config';
512
+ import chalk from 'chalk';
513
+
514
+ export function storeApiKey(apiKey: string): void {
515
+ if (!apiKey || apiKey.trim().length === 0) {
516
+ throw new Error('API key cannot be empty');
517
+ }
518
+
519
+ // TODO: Add encryption before storing
520
+ config.setApiKey(apiKey);
521
+ console.log(chalk.green('✓ API key saved successfully'));
522
+ }
523
+
524
+ export function removeApiKey(): void {
525
+ config.delete('apiKey');
526
+ console.log(chalk.green('✓ API key removed'));
527
+ }
528
+
529
+ export function hasApiKey(): boolean {
530
+ return config.hasApiKey();
531
+ }
532
+
533
+ export function getApiKeyStatus(): { configured: boolean; source: 'env' | 'config' | 'none' } {
534
+ if (process.env.MX_API_KEY) {
535
+ return { configured: true, source: 'env' };
536
+ }
537
+ if (config.get('apiKey')) {
538
+ return { configured: true, source: 'config' };
539
+ }
540
+ return { configured: false, source: 'none' };
541
+ }
542
+
543
+ // TODO: Implement encryption/decryption
544
+ export function encryptApiKey(apiKey: string): string {
545
+ // Placeholder - implement proper encryption
546
+ return apiKey;
547
+ }
548
+
549
+ export function decryptApiKey(encryptedKey: string): string {
550
+ // Placeholder - implement proper decryption
551
+ return encryptedKey;
552
+ }
553
+ ```
554
+
555
+ **Create tests `tests/lib/auth.test.ts`**:
556
+ ```typescript
557
+ import { storeApiKey, removeApiKey, hasApiKey, getApiKeyStatus } from '../../src/lib/auth';
558
+ import { config } from '../../src/lib/config';
559
+
560
+ describe('Authentication Helpers', () => {
561
+ beforeEach(() => {
562
+ config.reset();
563
+ delete process.env.MX_API_KEY;
564
+ });
565
+
566
+ it('should store API key', () => {
567
+ storeApiKey('test-key-123');
568
+ expect(hasApiKey()).toBe(true);
569
+ });
570
+
571
+ it('should reject empty API key', () => {
572
+ expect(() => storeApiKey('')).toThrow();
573
+ });
574
+
575
+ it('should remove API key', () => {
576
+ storeApiKey('test-key');
577
+ removeApiKey();
578
+ expect(hasApiKey()).toBe(false);
579
+ });
580
+
581
+ it('should detect API key source', () => {
582
+ expect(getApiKeyStatus().source).toBe('none');
583
+
584
+ storeApiKey('config-key');
585
+ expect(getApiKeyStatus().source).toBe('config');
586
+
587
+ process.env.MX_API_KEY = 'env-key';
588
+ expect(getApiKeyStatus().source).toBe('env');
589
+ });
590
+ });
591
+ ```
592
+
593
+ **Acceptance Criteria**:
594
+ - [x] Can store/remove API keys
595
+ - [x] Status detection works correctly
596
+ - [x] Tests pass
597
+ - [x] TODO comments for encryption
598
+
599
+ ---
600
+
601
+ ### 8. Set Up Error Handling Framework
602
+ **Estimated Time**: 2-3 hours
603
+
604
+ **Description**: Centralized error handling with user-friendly messages
605
+
606
+ **Create `src/lib/errors.ts`**:
607
+ ```typescript
608
+ import chalk from 'chalk';
609
+ import { AxiosError } from 'axios';
610
+
611
+ export class CLIError extends Error {
612
+ constructor(
613
+ message: string,
614
+ public exitCode: number = 1,
615
+ public details?: unknown
616
+ ) {
617
+ super(message);
618
+ this.name = 'CLIError';
619
+ }
620
+ }
621
+
622
+ export function handleError(error: unknown): never {
623
+ if (error instanceof CLIError) {
624
+ console.error(chalk.red(`Error: ${error.message}`));
625
+ if (error.details && process.env.DEBUG) {
626
+ console.error(chalk.gray(JSON.stringify(error.details, null, 2)));
627
+ }
628
+ process.exit(error.exitCode);
629
+ }
630
+
631
+ if (isAxiosError(error)) {
632
+ handleApiError(error);
633
+ }
634
+
635
+ // Unknown error
636
+ console.error(chalk.red('An unexpected error occurred'));
637
+ if (error instanceof Error) {
638
+ console.error(chalk.gray(error.message));
639
+ if (process.env.DEBUG && error.stack) {
640
+ console.error(chalk.gray(error.stack));
641
+ }
642
+ }
643
+ process.exit(1);
644
+ }
645
+
646
+ function isAxiosError(error: unknown): error is AxiosError {
647
+ return (error as AxiosError).isAxiosError === true;
648
+ }
649
+
650
+ function handleApiError(error: AxiosError): never {
651
+ if (!error.response) {
652
+ console.error(chalk.red('Network Error: Unable to connect to API'));
653
+ console.log(chalk.yellow('Check your internet connection and API URL'));
654
+ process.exit(4);
655
+ }
656
+
657
+ const status = error.response.status;
658
+ const data = error.response.data as { error?: string; message?: string };
659
+
660
+ switch (status) {
661
+ case 401:
662
+ console.error(chalk.red('Authentication Error: Invalid or missing API key'));
663
+ console.log(chalk.yellow('Run: mx auth login --api-key <your-key>'));
664
+ process.exit(3);
665
+
666
+ case 403:
667
+ console.error(chalk.red('Authorization Error: Insufficient permissions'));
668
+ process.exit(3);
669
+
670
+ case 404:
671
+ console.error(chalk.red('Not Found: Resource does not exist'));
672
+ process.exit(1);
673
+
674
+ case 422:
675
+ console.error(chalk.red('Validation Error: Invalid input'));
676
+ if (data.message) {
677
+ console.error(chalk.yellow(data.message));
678
+ }
679
+ process.exit(2);
680
+
681
+ case 429:
682
+ console.error(chalk.red('Rate Limit Exceeded: Too many requests'));
683
+ console.log(chalk.yellow('Please wait a moment and try again'));
684
+ process.exit(5);
685
+
686
+ default:
687
+ console.error(chalk.red(`API Error: ${status} ${error.response.statusText}`));
688
+ if (data.error || data.message) {
689
+ console.error(chalk.yellow(data.error || data.message));
690
+ }
691
+ process.exit(5);
692
+ }
693
+ }
694
+ ```
695
+
696
+ **Create tests `tests/lib/errors.test.ts`**:
697
+ ```typescript
698
+ import { CLIError, handleError } from '../../src/lib/errors';
699
+ import { AxiosError } from 'axios';
700
+
701
+ describe('Error Handling', () => {
702
+ let exitSpy: jest.SpyInstance;
703
+ let consoleErrorSpy: jest.SpyInstance;
704
+
705
+ beforeEach(() => {
706
+ exitSpy = jest.spyOn(process, 'exit').mockImplementation();
707
+ consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation();
708
+ });
709
+
710
+ afterEach(() => {
711
+ exitSpy.mockRestore();
712
+ consoleErrorSpy.mockRestore();
713
+ });
714
+
715
+ it('should handle CLIError', () => {
716
+ const error = new CLIError('Test error', 2);
717
+ handleError(error);
718
+ expect(exitSpy).toHaveBeenCalledWith(2);
719
+ });
720
+
721
+ it('should handle 401 API errors', () => {
722
+ const error: AxiosError = {
723
+ isAxiosError: true,
724
+ response: { status: 401, statusText: 'Unauthorized', data: {} },
725
+ } as AxiosError;
726
+
727
+ handleError(error);
728
+ expect(exitSpy).toHaveBeenCalledWith(3);
729
+ });
730
+
731
+ it('should handle network errors', () => {
732
+ const error: AxiosError = {
733
+ isAxiosError: true,
734
+ response: undefined,
735
+ } as AxiosError;
736
+
737
+ handleError(error);
738
+ expect(exitSpy).toHaveBeenCalledWith(4);
739
+ });
740
+ });
741
+ ```
742
+
743
+ **Acceptance Criteria**:
744
+ - [x] All error types handled appropriately
745
+ - [x] User-friendly error messages
746
+ - [x] Correct exit codes used
747
+ - [x] Tests pass
748
+
749
+ ---
750
+
751
+ ### 9. Create Output Formatters
752
+ **Estimated Time**: 3-4 hours
753
+
754
+ **Description**: Format command output as JSON, table, or YAML
755
+
756
+ **Install dependencies**:
757
+ ```bash
758
+ npm install js-yaml cli-table3
759
+ npm install --save-dev @types/js-yaml
760
+ ```
761
+
762
+ **Create `src/lib/formatters.ts`**:
763
+ ```typescript
764
+ import chalk from 'chalk';
765
+ import Table from 'cli-table3';
766
+ import yaml from 'js-yaml';
767
+ import { config } from './config';
768
+
769
+ export type OutputFormat = 'json' | 'table' | 'yaml';
770
+
771
+ export function formatOutput(
772
+ data: unknown,
773
+ format?: OutputFormat,
774
+ tableOptions?: TableOptions
775
+ ): string {
776
+ const outputFormat = format || config.getFormat();
777
+
778
+ switch (outputFormat) {
779
+ case 'json':
780
+ return formatJson(data);
781
+ case 'yaml':
782
+ return formatYaml(data);
783
+ case 'table':
784
+ return formatTable(data, tableOptions);
785
+ default:
786
+ return formatJson(data);
787
+ }
788
+ }
789
+
790
+ export function formatJson(data: unknown): string {
791
+ return JSON.stringify(data, null, 2);
792
+ }
793
+
794
+ export function formatYaml(data: unknown): string {
795
+ return yaml.dump(data, { indent: 2, lineWidth: 120 });
796
+ }
797
+
798
+ interface TableOptions {
799
+ columns?: string[];
800
+ maxColumnWidth?: number;
801
+ }
802
+
803
+ export function formatTable(data: unknown, options?: TableOptions): string {
804
+ if (!data) {
805
+ return 'No data';
806
+ }
807
+
808
+ // Single object
809
+ if (typeof data === 'object' && !Array.isArray(data)) {
810
+ return formatObjectAsTable(data as Record<string, unknown>);
811
+ }
812
+
813
+ // Array of objects
814
+ if (Array.isArray(data)) {
815
+ if (data.length === 0) {
816
+ return 'No results';
817
+ }
818
+ return formatArrayAsTable(data, options);
819
+ }
820
+
821
+ // Primitive value
822
+ return String(data);
823
+ }
824
+
825
+ function formatObjectAsTable(obj: Record<string, unknown>): string {
826
+ const table = new Table({
827
+ head: [chalk.cyan('Property'), chalk.cyan('Value')],
828
+ colWidths: [30, 70],
829
+ wordWrap: true,
830
+ });
831
+
832
+ Object.entries(obj).forEach(([key, value]) => {
833
+ table.push([key, formatValue(value)]);
834
+ });
835
+
836
+ return table.toString();
837
+ }
838
+
839
+ function formatArrayAsTable(data: unknown[], options?: TableOptions): string {
840
+ const firstItem = data[0];
841
+ if (typeof firstItem !== 'object' || firstItem === null) {
842
+ // Array of primitives
843
+ const table = new Table({ head: [chalk.cyan('Value')] });
844
+ data.forEach((item) => table.push([formatValue(item)]));
845
+ return table.toString();
846
+ }
847
+
848
+ // Array of objects - use keys from first object
849
+ const keys = options?.columns || Object.keys(firstItem as Record<string, unknown>);
850
+ const table = new Table({
851
+ head: keys.map((k) => chalk.cyan(k)),
852
+ wordWrap: true,
853
+ colWidths: keys.map(() => options?.maxColumnWidth || 30),
854
+ });
855
+
856
+ data.forEach((item) => {
857
+ const row = keys.map((key) => {
858
+ const value = (item as Record<string, unknown>)[key];
859
+ return formatValue(value);
860
+ });
861
+ table.push(row);
862
+ });
863
+
864
+ return table.toString();
865
+ }
866
+
867
+ function formatValue(value: unknown): string {
868
+ if (value === null || value === undefined) {
869
+ return chalk.gray('null');
870
+ }
871
+ if (typeof value === 'boolean') {
872
+ return value ? chalk.green('true') : chalk.red('false');
873
+ }
874
+ if (typeof value === 'object') {
875
+ return JSON.stringify(value);
876
+ }
877
+ return String(value);
878
+ }
879
+
880
+ export function printOutput(data: unknown, format?: OutputFormat, tableOptions?: TableOptions): void {
881
+ const output = formatOutput(data, format, tableOptions);
882
+ console.log(output);
883
+ }
884
+ ```
885
+
886
+ **Create tests `tests/lib/formatters.test.ts`**:
887
+ ```typescript
888
+ import { formatJson, formatYaml, formatTable, formatOutput } from '../../src/lib/formatters';
889
+
890
+ describe('Output Formatters', () => {
891
+ const testData = { id: '123', name: 'Test', active: true };
892
+
893
+ it('should format as JSON', () => {
894
+ const output = formatJson(testData);
895
+ expect(output).toContain('"id"');
896
+ expect(output).toContain('"123"');
897
+ });
898
+
899
+ it('should format as YAML', () => {
900
+ const output = formatYaml(testData);
901
+ expect(output).toContain('id: ');
902
+ expect(output).toContain('name: Test');
903
+ });
904
+
905
+ it('should format object as table', () => {
906
+ const output = formatTable(testData);
907
+ expect(output).toContain('Property');
908
+ expect(output).toContain('Value');
909
+ });
910
+
911
+ it('should format array as table', () => {
912
+ const output = formatTable([testData, testData]);
913
+ expect(output).toContain('id');
914
+ expect(output).toContain('name');
915
+ });
916
+
917
+ it('should use default format from config', () => {
918
+ const output = formatOutput(testData);
919
+ expect(output).toBeDefined();
920
+ });
921
+ });
922
+ ```
923
+
924
+ **Acceptance Criteria**:
925
+ - [x] JSON formatting works
926
+ - [x] YAML formatting works
927
+ - [x] Table formatting works for objects and arrays
928
+ - [x] Format selection respects config/flags
929
+ - [x] Tests pass
930
+
931
+ ---
932
+
933
+ ### 10. Write Basic Tests for Utilities
934
+ **Estimated Time**: 2-3 hours
935
+
936
+ **Description**: Set up Jest and write comprehensive tests
937
+
938
+ **Install Jest**:
939
+ ```bash
940
+ npm install --save-dev jest ts-jest @types/jest
941
+ ```
942
+
943
+ **Create `jest.config.js`**:
944
+ ```javascript
945
+ module.exports = {
946
+ preset: 'ts-jest',
947
+ testEnvironment: 'node',
948
+ roots: ['<rootDir>/tests'],
949
+ testMatch: ['**/*.test.ts'],
950
+ collectCoverageFrom: [
951
+ 'src/**/*.ts',
952
+ '!src/index.ts',
953
+ '!src/**/*.d.ts'
954
+ ],
955
+ coverageThreshold: {
956
+ global: {
957
+ branches: 80,
958
+ functions: 80,
959
+ lines: 80,
960
+ statements: 80
961
+ }
962
+ },
963
+ moduleNameMapper: {
964
+ '^@/(.*)$': '<rootDir>/src/$1'
965
+ }
966
+ };
967
+ ```
968
+
969
+ **Run tests**:
970
+ ```bash
971
+ npm test
972
+ npm run test:coverage
973
+ ```
974
+
975
+ **Acceptance Criteria**:
976
+ - [x] All tests pass
977
+ - [x] Coverage >80% for utility modules
978
+ - [x] `npm test` works in CI/CD
979
+
980
+ ---
981
+
982
+ ## Validation Checklist
983
+
984
+ Before marking this phase complete, verify:
985
+
986
+ - [ ] `npm install` succeeds without errors
987
+ - [ ] `npm run build` compiles TypeScript successfully
988
+ - [ ] `npm run lint` passes without errors
989
+ - [ ] `npm run format` formats code correctly
990
+ - [ ] `npm test` passes all tests with >80% coverage
991
+ - [ ] `npm run dev` runs CLI without crashing
992
+ - [ ] Can import from `@memnexus-ai/contracts`
993
+ - [ ] Config saves and loads correctly
994
+ - [ ] API client initializes (with dummy key)
995
+ - [ ] All three formatters work (JSON, table, YAML)
996
+ - [ ] Error handling shows user-friendly messages
997
+
998
+ ---
999
+
1000
+ ## Documentation
1001
+
1002
+ **Create/Update**:
1003
+ - [ ] Add basic usage to `README.md`
1004
+ - [ ] Document config file location and format
1005
+ - [ ] Document environment variables
1006
+ - [ ] Add development setup instructions
1007
+
1008
+ ---
1009
+
1010
+ ## Dependencies
1011
+
1012
+ **Production**:
1013
+ - `@memnexus-ai/contracts@^1.0.4`
1014
+ - `commander@^11.1.0`
1015
+ - `chalk@^5.3.0`
1016
+ - `ora@^7.0.1`
1017
+ - `cli-table3@^0.6.3`
1018
+ - `inquirer@^9.2.12`
1019
+ - `configstore@^7.0.0`
1020
+ - `dotenv@^16.3.1`
1021
+ - `js-yaml@^4.1.0`
1022
+
1023
+ **Development**:
1024
+ - `typescript@^5.3.3`
1025
+ - `@types/node@^20.10.0`
1026
+ - `ts-node@^10.9.2`
1027
+ - `jest@^29.7.0`
1028
+ - `ts-jest@^29.1.1`
1029
+ - `@types/jest@^29.5.8`
1030
+ - `eslint@^8.55.0`
1031
+ - `@typescript-eslint/parser@^6.13.2`
1032
+ - `@typescript-eslint/eslint-plugin@^6.13.2`
1033
+ - `prettier@^3.1.0`
1034
+ - `eslint-config-prettier@^9.1.0`
1035
+ - `@types/js-yaml@^4.0.9`
1036
+
1037
+ ---
1038
+
1039
+ ## Timeline
1040
+
1041
+ **Day 1-2**: Tasks 1-4 (Setup, dependencies, project structure)
1042
+ **Day 3-4**: Tasks 5-7 (Config, API client, auth)
1043
+ **Day 4-5**: Tasks 8-10 (Errors, formatters, tests)
1044
+
1045
+ **Total**: 5 working days (1 week)
1046
+
1047
+ ---
1048
+
1049
+ ## Next Steps
1050
+
1051
+ After Phase 1 completion:
1052
+ - **Phase 2**: Implement core commands (memories, conversations, facts)
1053
+ - Set up CI/CD workflows
1054
+ - Begin integration testing with real API
1055
+
1056
+ ---
1057
+
1058
+ ## Related Documentation
1059
+
1060
+ - [PRD - Phase 1 Section](../docs/prd.md#phase-1-foundation-week-1)
1061
+ - [CLI-API Sync Strategy](../docs/sync-strategy.md)
1062
+ - [Code Generation Strategy](../docs/code-generation-strategy.md)
1063
+
1064
+ ---
1065
+
1066
+ ## Questions or Issues?
1067
+
1068
+ If you encounter any blockers or need clarification:
1069
+ 1. Check the [PRD documentation](../docs/prd.md)
1070
+ 2. Review existing code in `@memnexus-ai/contracts`
1071
+ 3. Comment on this issue or create a new one
1072
+ 4. Tag relevant team members
1073
+
1074
+ ---
1075
+
1076
+ **Estimated Total Time**: 15-20 hours (1 week)
1077
+ **Priority**: High (Blocks Phase 2)
1078
+ **Complexity**: Medium