newo 1.5.0 → 1.5.1

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/.env.example CHANGED
@@ -1,14 +1,25 @@
1
1
  # NEWO endpoints
2
2
  NEWO_BASE_URL=https://app.newo.ai
3
3
 
4
- # Project you want to sync (optional - leave blank to sync all accessible projects)
5
- # NEWO_PROJECT_ID=b78188ba-0df0-46a8-8713-f0d7cff0a06e
4
+ # Multi-customer configuration - Array format
5
+ # Define API keys as JSON array - customer IDN will be auto-detected from API response
6
+ NEWO_API_KEYS=["api_key_1", "api_key_2", "api_key_3"]
7
+
8
+ # Or with optional project IDs per API key
9
+ # NEWO_API_KEYS=[{"key":"api_key_1","project_id":"project_uuid_1"}, {"key":"api_key_2","project_id":"project_uuid_2"}]
10
+
11
+ # Optional: specify default customer IDN after keys are loaded
12
+ # NEWO_DEFAULT_CUSTOMER=NEWO_ESd2BC95
6
13
 
7
- # Auth (choose one)
8
- # 1) Recommended: API key that can be exchanged for tokens:
9
- NEWO_API_KEY=put_api_key_here
14
+ # Legacy format (still supported)
15
+ # NEWO_CUSTOMER_[IDN]_API_KEY=api_key
16
+ # NEWO_CUSTOMER_acme_API_KEY=put_acme_api_key_here
17
+
18
+ # Legacy single-customer mode (still supported)
19
+ # NEWO_API_KEY=put_api_key_here
20
+ # NEWO_PROJECT_ID=b78188ba-0df0-46a8-8713-f0d7cff0a06e
10
21
 
11
- # 2) Optional bootstrap tokens (used if present)
22
+ # Optional bootstrap tokens (for legacy mode or manual setup)
12
23
  NEWO_ACCESS_TOKEN=
13
24
  NEWO_REFRESH_TOKEN=
14
25
 
package/CHANGELOG.md CHANGED
@@ -5,6 +5,67 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [1.5.1] - 2025-01-14
9
+
10
+ ### Added
11
+ - **Comprehensive Test Coverage**: Added extensive test suites for all major modules
12
+ - `test/auth.test.js`: 500+ lines covering authentication, token management, multi-customer support
13
+ - `test/hash.test.js`: 400+ lines covering SHA256 hashing, hash storage, and cross-platform compatibility
14
+ - `test/fsutil.test.js`: 400+ lines covering file system utilities and path handling
15
+ - `test/akb.test.js`: 600+ lines covering AKB article parsing and import workflows
16
+ - Added missing test dependencies: `chai`, `sinon`, `c8` for coverage reporting
17
+ - **Enhanced Authentication Validation**:
18
+ - `validateApiKey()`: Comprehensive API key format and length validation
19
+ - `validateTokens()`: Token format and structure validation with detailed error messages
20
+ - `validateUrl()`: URL format validation for API endpoints
21
+ - Sensitive data sanitization in logs (API keys and tokens masked)
22
+ - **Structured Logging System**:
23
+ - `logAuthEvent()`: Structured authentication event logging with metadata
24
+ - Automatic sensitive data sanitization (keys/tokens/secrets masked in logs)
25
+ - JSON-formatted logs with timestamp, level, module, and context information
26
+ - **Enhanced Error Handling**:
27
+ - User-friendly CLI error messages with troubleshooting tips
28
+ - Specific error handling for authentication, network, environment, and file system errors
29
+ - Verbose mode support for detailed debugging information
30
+ - Context-aware error messages with suggested solutions
31
+
32
+ ### Enhanced
33
+ - **Authentication Robustness** (`src/auth.ts`):
34
+ - Added comprehensive input validation with detailed error messages
35
+ - Enhanced network error handling with specific status code interpretation
36
+ - Added request timeouts (30 seconds) and retry logic for reliability
37
+ - Improved token expiry handling with 60-second buffer for refresh
38
+ - Better handling of connection errors, timeouts, and server errors
39
+ - **CLI Error Experience** (`src/cli.ts`):
40
+ - Added `handleCliError()` function with categorized error types
41
+ - User-friendly error messages with emoji indicators and troubleshooting tips
42
+ - Verbose mode toggle for detailed technical information vs. clean user messages
43
+ - Specific guidance for common issues (API key, network, configuration)
44
+ - **Testing Infrastructure**:
45
+ - Fixed ES module/CommonJS compatibility issues in test files
46
+ - Enhanced `TestEnvironment` class with comprehensive cleanup and mocking
47
+ - Added MockHttpClient, MockFileSystem, and MockLogger utilities
48
+ - Comprehensive assertion helpers and test data generators
49
+
50
+ ### Fixed
51
+ - **Module System Compatibility**: Resolved ES module/CommonJS conflicts in test environment
52
+ - **Test Dependencies**: Added missing testing dependencies that were imported but not declared
53
+ - **Integration Test Paths**: Fixed paths from `src/cli.js` to `dist/cli.js` for proper compiled code testing
54
+ - **Error Message Consistency**: Standardized error messages across authentication and CLI modules
55
+
56
+ ### Technical Details
57
+ - **Validation Constants**: Added security-focused validation thresholds (API_KEY_MIN_LENGTH, TOKEN_MIN_LENGTH)
58
+ - **Request Configuration**: Added proper timeout handling (30s) and user-agent headers
59
+ - **Error Recovery**: Comprehensive fallback strategies for different failure scenarios
60
+ - **Logging Standards**: JSON-structured logs with automatic PII/sensitive data protection
61
+ - **Test Coverage**: Achieved comprehensive test coverage across all core modules with realistic scenarios
62
+
63
+ ### Developer Experience
64
+ - **Enhanced Debugging**: Verbose mode provides detailed technical information for troubleshooting
65
+ - **Better Error Messages**: Clear, actionable error messages instead of generic API errors
66
+ - **Comprehensive Testing**: Full test suite covering authentication, file operations, hashing, and AKB import
67
+ - **Type Safety**: All improvements maintain strict TypeScript compliance with proper error types
68
+
8
69
  ## [1.5.0] - 2025-09-03
9
70
 
10
71
  ### Changed
package/README.md CHANGED
@@ -93,8 +93,40 @@ Hashes are tracked in `.newo/hashes.json` so only changed files are pushed.
93
93
  - **AKB import**: Import knowledge base articles from structured text files
94
94
  - **Project structure export**: Generates `flows.yaml` with complete project metadata
95
95
  - **Robust authentication**: API key exchange with automatic token refresh
96
+ - **Enhanced error handling**: User-friendly error messages with troubleshooting guidance
97
+ - **Comprehensive testing**: Full test suite covering all major functionality
96
98
  - **CI/CD ready**: GitHub Actions workflow included
97
99
 
100
+ ## Robustness & Error Handling
101
+
102
+ NEWO CLI v1.5.1+ includes comprehensive error handling and validation:
103
+
104
+ ### User-Friendly Error Messages
105
+ - **Authentication Errors**: Clear guidance when API keys are invalid or missing
106
+ - **Network Issues**: Helpful tips for connection problems and timeouts
107
+ - **Configuration Errors**: Step-by-step setup instructions for common issues
108
+ - **File System Errors**: Actionable guidance for permission and path problems
109
+
110
+ ### Verbose Debugging
111
+ Use the `--verbose` or `-v` flag with any command for detailed technical information:
112
+ ```bash
113
+ npx newo pull --verbose # Detailed pull operation logs
114
+ npx newo push -v # Verbose push with full error context
115
+ ```
116
+
117
+ ### Enhanced Validation
118
+ - **API Key Validation**: Format and length validation with specific error messages
119
+ - **Token Security**: Automatic sanitization of sensitive data in logs
120
+ - **Network Timeouts**: 30-second request timeouts with proper error handling
121
+ - **Input Validation**: Comprehensive validation for all user inputs and configuration
122
+
123
+ ### Troubleshooting Tips
124
+ When errors occur, NEWO CLI provides:
125
+ - 🔍 **Problem diagnosis** with specific error categories
126
+ - 💡 **Solution suggestions** for common configuration issues
127
+ - 📋 **Step-by-step guidance** for resolving authentication and network problems
128
+ - 🔧 **Configuration validation** to ensure proper setup
129
+
98
130
  ## CI/CD (GitHub Actions)
99
131
  Create `.github/workflows/deploy.yml`:
100
132
  ```yaml
@@ -179,8 +211,31 @@ npm run typecheck
179
211
 
180
212
  # Run tests
181
213
  npm test
214
+
215
+ # Run tests with coverage reporting
216
+ npm run test:coverage
217
+
218
+ # Run specific test suites
219
+ npm run test:unit # Core module tests (api, sync, auth, hash, fsutil, akb)
220
+ npm run test:integration # End-to-end integration tests
182
221
  ```
183
222
 
223
+ ### Test Coverage
224
+ NEWO CLI includes comprehensive test suites:
225
+ - **Authentication Tests** (`test/auth.test.js`): Token management, API key validation, multi-customer support
226
+ - **Hashing Tests** (`test/hash.test.js`): SHA256 operations, hash storage, change detection
227
+ - **File System Tests** (`test/fsutil.test.js`): Path utilities, directory management, atomic operations
228
+ - **AKB Import Tests** (`test/akb.test.js`): Article parsing, Unicode handling, import workflows
229
+ - **API Tests** (`test/api.test.js`): HTTP client functionality, NEWO API integration
230
+ - **Sync Tests** (`test/sync.test.js`): Pull/push operations, project synchronization
231
+ - **Integration Tests** (`test/integration.test.js`): End-to-end CLI functionality
232
+
233
+ Test infrastructure includes:
234
+ - **MockHttpClient**: HTTP request/response simulation
235
+ - **MockFileSystem**: File system operation mocking
236
+ - **TestEnvironment**: Isolated test environments with automatic cleanup
237
+ - **Coverage Reporting**: HTML and text coverage reports via c8
238
+
184
239
  ### Project Structure
185
240
  - `src/` - TypeScript source files
186
241
  - `dist/` - Compiled JavaScript output (generated by `npm run build`)
package/dist/akb.d.ts CHANGED
@@ -2,7 +2,7 @@ import type { ParsedArticle, AkbImportArticle } from './types.js';
2
2
  /**
3
3
  * Parse AKB file and extract articles
4
4
  */
5
- export declare function parseAkbFile(filePath: string): ParsedArticle[];
5
+ export declare function parseAkbFile(filePath: string): Promise<ParsedArticle[]>;
6
6
  /**
7
7
  * Convert parsed articles to API format for bulk import
8
8
  */
package/dist/akb.js CHANGED
@@ -2,8 +2,8 @@ import fs from 'fs-extra';
2
2
  /**
3
3
  * Parse AKB file and extract articles
4
4
  */
5
- export function parseAkbFile(filePath) {
6
- const content = fs.readFileSync(filePath, 'utf8');
5
+ export async function parseAkbFile(filePath) {
6
+ const content = await fs.readFile(filePath, 'utf8');
7
7
  const articles = [];
8
8
  // Split by article separators (---)
9
9
  const sections = content.split(/^---\s*$/gm).filter(section => section.trim());
@@ -22,27 +22,31 @@ export function parseAkbFile(filePath) {
22
22
  * Parse individual article section
23
23
  */
24
24
  function parseArticleSection(lines) {
25
- let topicName = '';
26
- let category = '';
27
- let summary = '';
28
- let keywords = '';
29
- let topicSummary = '';
25
+ const state = {
26
+ topicName: '',
27
+ category: '',
28
+ summary: '',
29
+ keywords: '',
30
+ topicSummary: ''
31
+ };
30
32
  // Find topic name (# r001)
31
33
  const topicLine = lines.find(line => line.match(/^#\s+r\d+/));
32
- if (!topicLine)
34
+ if (!topicLine) {
35
+ console.warn('No topic line found in section');
33
36
  return null;
34
- topicName = topicLine.replace(/^#\s+/, '').trim();
37
+ }
38
+ state.topicName = topicLine.replace(/^#\s+/, '').trim();
35
39
  // Extract category/subcategory/description (first ## line)
36
40
  const categoryLine = lines.find(line => line.startsWith('## ') && line.includes(' / '));
37
41
  if (categoryLine) {
38
- category = categoryLine.replace(/^##\s+/, '').trim();
42
+ state.category = categoryLine.replace(/^##\s+/, '').trim();
39
43
  }
40
44
  // Extract summary (second ## line)
41
45
  const summaryLineIndex = lines.findIndex(line => line.startsWith('## ') && line.includes(' / '));
42
46
  if (summaryLineIndex >= 0 && summaryLineIndex + 1 < lines.length) {
43
47
  const nextLine = lines[summaryLineIndex + 1];
44
48
  if (nextLine && nextLine.startsWith('## ') && !nextLine.includes(' / ')) {
45
- summary = nextLine.replace(/^##\s+/, '').trim();
49
+ state.summary = nextLine.replace(/^##\s+/, '').trim();
46
50
  }
47
51
  }
48
52
  // Extract keywords (third ## line)
@@ -50,7 +54,7 @@ function parseArticleSection(lines) {
50
54
  if (keywordsLineIndex >= 0) {
51
55
  const keywordsLine = lines[keywordsLineIndex];
52
56
  if (keywordsLine) {
53
- keywords = keywordsLine.replace(/^##\s+/, '').trim();
57
+ state.keywords = keywordsLine.replace(/^##\s+/, '').trim();
54
58
  }
55
59
  }
56
60
  // Extract category content
@@ -58,17 +62,17 @@ function parseArticleSection(lines) {
58
62
  const categoryEndIndex = lines.findIndex(line => line.includes('</Category>'));
59
63
  if (categoryStartIndex >= 0 && categoryEndIndex >= 0) {
60
64
  const categoryLines = lines.slice(categoryStartIndex, categoryEndIndex + 1);
61
- topicSummary = categoryLines.join('\n');
65
+ state.topicSummary = categoryLines.join('\n');
62
66
  }
63
67
  // Create topic_facts array
64
- const topicFacts = [category, summary, keywords].filter(fact => fact.trim() !== '');
68
+ const topicFacts = [state.category, state.summary, state.keywords].filter(fact => fact.trim() !== '');
65
69
  return {
66
- topic_name: category, // Use the descriptive title as topic_name
70
+ topic_name: state.category, // Use the descriptive title as topic_name
67
71
  persona_id: null, // Will be set when importing
68
- topic_summary: topicSummary,
72
+ topic_summary: state.topicSummary,
69
73
  topic_facts: topicFacts,
70
74
  confidence: 100,
71
- source: topicName, // Use the ID (r001) as source
75
+ source: state.topicName, // Use the ID (r001) as source
72
76
  labels: ['rag_context']
73
77
  };
74
78
  }
package/dist/api.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { type AxiosInstance } from 'axios';
2
- import type { ProjectMeta, Agent, Skill, FlowEvent, FlowState, AkbImportArticle } from './types.js';
3
- export declare function makeClient(verbose?: boolean): Promise<AxiosInstance>;
2
+ import type { ProjectMeta, Agent, Skill, FlowEvent, FlowState, AkbImportArticle, CustomerProfile } from './types.js';
3
+ export declare function makeClient(verbose?: boolean, token?: string): Promise<AxiosInstance>;
4
4
  export declare function listProjects(client: AxiosInstance): Promise<ProjectMeta[]>;
5
5
  export declare function listAgents(client: AxiosInstance, projectId: string): Promise<Agent[]>;
6
6
  export declare function getProjectMeta(client: AxiosInstance, projectId: string): Promise<ProjectMeta>;
@@ -10,4 +10,5 @@ export declare function updateSkill(client: AxiosInstance, skillObject: Skill):
10
10
  export declare function listFlowEvents(client: AxiosInstance, flowId: string): Promise<FlowEvent[]>;
11
11
  export declare function listFlowStates(client: AxiosInstance, flowId: string): Promise<FlowState[]>;
12
12
  export declare function importAkbArticle(client: AxiosInstance, articleData: AkbImportArticle): Promise<unknown>;
13
+ export declare function getCustomerProfile(client: AxiosInstance): Promise<CustomerProfile>;
13
14
  //# sourceMappingURL=api.d.ts.map
package/dist/api.js CHANGED
@@ -1,17 +1,14 @@
1
- import axios from 'axios';
2
- import dotenv from 'dotenv';
1
+ import axios, {} from 'axios';
3
2
  import { getValidAccessToken, forceReauth } from './auth.js';
4
- dotenv.config();
5
- const { NEWO_BASE_URL } = process.env;
6
- export async function makeClient(verbose = false) {
7
- let accessToken = await getValidAccessToken();
3
+ import { ENV } from './env.js';
4
+ // Per-request retry tracking to avoid shared state issues
5
+ const RETRY_SYMBOL = Symbol('retried');
6
+ export async function makeClient(verbose = false, token) {
7
+ let accessToken = token || await getValidAccessToken();
8
8
  if (verbose)
9
9
  console.log('✓ Access token obtained');
10
- if (!NEWO_BASE_URL) {
11
- throw new Error('NEWO_BASE_URL is not set in environment variables');
12
- }
13
10
  const client = axios.create({
14
- baseURL: NEWO_BASE_URL,
11
+ baseURL: ENV.NEWO_BASE_URL,
15
12
  headers: { accept: 'application/json' }
16
13
  });
17
14
  client.interceptors.request.use(async (config) => {
@@ -26,7 +23,6 @@ export async function makeClient(verbose = false) {
26
23
  }
27
24
  return config;
28
25
  });
29
- let retried = false;
30
26
  client.interceptors.response.use((response) => {
31
27
  if (verbose) {
32
28
  console.log(`← ${response.status} ${response.config.method?.toUpperCase()} ${response.config.url}`);
@@ -34,7 +30,8 @@ export async function makeClient(verbose = false) {
34
30
  console.log(' Response:', JSON.stringify(response.data, null, 2));
35
31
  }
36
32
  else if (response.data) {
37
- console.log(` Response: [${typeof response.data}] ${Array.isArray(response.data) ? response.data.length + ' items' : 'large object'}`);
33
+ const itemCount = Array.isArray(response.data) ? response.data.length : Object.keys(response.data).length;
34
+ console.log(` Response: [${typeof response.data}] ${Array.isArray(response.data) ? itemCount + ' items' : 'large object'}`);
38
35
  }
39
36
  }
40
37
  return response;
@@ -45,15 +42,17 @@ export async function makeClient(verbose = false) {
45
42
  if (error.response?.data)
46
43
  console.log(' Error data:', error.response.data);
47
44
  }
48
- if (status === 401 && !retried) {
49
- retried = true;
50
- if (verbose)
51
- console.log('🔄 Retrying with fresh token...');
52
- accessToken = await forceReauth();
53
- if (error.config) {
54
- error.config.headers = error.config.headers || {};
55
- error.config.headers.Authorization = `Bearer ${accessToken}`;
56
- return client.request(error.config);
45
+ // Use per-request retry tracking to avoid shared state issues
46
+ const config = error.config;
47
+ if (status === 401 && !config?.[RETRY_SYMBOL]) {
48
+ if (config) {
49
+ config[RETRY_SYMBOL] = true;
50
+ if (verbose)
51
+ console.log('🔄 Retrying with fresh token...');
52
+ accessToken = await forceReauth();
53
+ config.headers = config.headers || {};
54
+ config.headers.Authorization = `Bearer ${accessToken}`;
55
+ return client.request(config);
57
56
  }
58
57
  }
59
58
  throw error;
@@ -97,4 +96,8 @@ export async function importAkbArticle(client, articleData) {
97
96
  const response = await client.post('/api/v1/akb/append-manual', articleData);
98
97
  return response.data;
99
98
  }
99
+ export async function getCustomerProfile(client) {
100
+ const response = await client.get('/api/v1/customer/profile');
101
+ return response.data;
102
+ }
100
103
  //# sourceMappingURL=api.js.map
package/dist/auth.d.ts CHANGED
@@ -1,6 +1,6 @@
1
- import type { StoredTokens } from './types.js';
2
- export declare function exchangeApiKeyForToken(): Promise<StoredTokens>;
3
- export declare function refreshWithEndpoint(refreshToken: string): Promise<StoredTokens>;
4
- export declare function getValidAccessToken(): Promise<string>;
5
- export declare function forceReauth(): Promise<string>;
1
+ import type { StoredTokens, CustomerConfig } from './types.js';
2
+ export declare function exchangeApiKeyForToken(customer?: CustomerConfig): Promise<StoredTokens>;
3
+ export declare function refreshWithEndpoint(refreshToken: string, customer?: CustomerConfig): Promise<StoredTokens>;
4
+ export declare function getValidAccessToken(customer?: CustomerConfig): Promise<string>;
5
+ export declare function forceReauth(customer?: CustomerConfig): Promise<string>;
6
6
  //# sourceMappingURL=auth.d.ts.map