newo 1.5.0 → 1.5.2

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/dist/types.d.ts CHANGED
@@ -5,9 +5,32 @@ export interface NewoEnvironment {
5
5
  NEWO_BASE_URL?: string;
6
6
  NEWO_PROJECT_ID?: string;
7
7
  NEWO_API_KEY?: string;
8
+ NEWO_API_KEYS?: string;
8
9
  NEWO_ACCESS_TOKEN?: string;
9
10
  NEWO_REFRESH_TOKEN?: string;
10
11
  NEWO_REFRESH_URL?: string;
12
+ NEWO_DEFAULT_CUSTOMER?: string;
13
+ [key: string]: string | undefined;
14
+ }
15
+ export interface ApiKeyConfig {
16
+ key: string;
17
+ project_id?: string;
18
+ }
19
+ export interface CustomerConfig {
20
+ idn: string;
21
+ apiKey: string;
22
+ projectId?: string | undefined;
23
+ }
24
+ export interface CustomerProfile {
25
+ id: string;
26
+ idn: string;
27
+ organization_name: string;
28
+ email: string;
29
+ [key: string]: any;
30
+ }
31
+ export interface MultiCustomerConfig {
32
+ customers: Record<string, CustomerConfig>;
33
+ defaultCustomer?: string | undefined;
11
34
  }
12
35
  export interface TokenResponse {
13
36
  access_token?: string;
@@ -24,63 +47,63 @@ export interface StoredTokens {
24
47
  expires_at: number;
25
48
  }
26
49
  export interface ProjectMeta {
27
- id: string;
28
- idn: string;
29
- title: string;
30
- description?: string;
31
- created_at?: string;
32
- updated_at?: string;
50
+ readonly id: string;
51
+ readonly idn: string;
52
+ readonly title: string;
53
+ readonly description?: string;
54
+ readonly created_at?: string;
55
+ readonly updated_at?: string;
33
56
  }
34
57
  export interface Agent {
35
- id: string;
36
- idn: string;
37
- title?: string;
38
- description?: string;
39
- flows?: Flow[];
58
+ readonly id: string;
59
+ readonly idn: string;
60
+ readonly title?: string;
61
+ readonly description?: string;
62
+ readonly flows?: readonly Flow[];
40
63
  }
41
64
  export interface Flow {
42
- id: string;
43
- idn: string;
44
- title: string;
45
- description?: string;
46
- default_runner_type: RunnerType;
47
- default_model: ModelConfig;
65
+ readonly id: string;
66
+ readonly idn: string;
67
+ readonly title: string;
68
+ readonly description?: string;
69
+ readonly default_runner_type: RunnerType;
70
+ readonly default_model: ModelConfig;
48
71
  }
49
72
  export interface ModelConfig {
50
- model_idn: string;
51
- provider_idn: string;
73
+ readonly model_idn: string;
74
+ readonly provider_idn: string;
52
75
  }
53
76
  export interface SkillParameter {
54
- name: string;
55
- default_value?: string;
77
+ readonly name: string;
78
+ readonly default_value?: string;
56
79
  }
57
80
  export interface Skill {
58
- id: string;
59
- idn: string;
60
- title: string;
81
+ readonly id: string;
82
+ readonly idn: string;
83
+ readonly title: string;
61
84
  prompt_script?: string;
62
- runner_type: RunnerType;
63
- model: ModelConfig;
64
- parameters: SkillParameter[];
65
- path?: string | undefined;
85
+ readonly runner_type: RunnerType;
86
+ readonly model: ModelConfig;
87
+ readonly parameters: readonly SkillParameter[];
88
+ readonly path?: string | undefined;
66
89
  }
67
90
  export interface FlowEvent {
68
- id: string;
69
- idn: string;
70
- description: string;
71
- skill_selector: SkillSelector;
72
- skill_idn?: string;
73
- state_idn?: string;
74
- integration_idn?: string;
75
- connector_idn?: string;
76
- interrupt_mode: InterruptMode;
91
+ readonly id: string;
92
+ readonly idn: string;
93
+ readonly description: string;
94
+ readonly skill_selector: SkillSelector;
95
+ readonly skill_idn?: string;
96
+ readonly state_idn?: string;
97
+ readonly integration_idn?: string;
98
+ readonly connector_idn?: string;
99
+ readonly interrupt_mode: InterruptMode;
77
100
  }
78
101
  export interface FlowState {
79
- id: string;
80
- idn: string;
81
- title: string;
82
- default_value?: string;
83
- scope: StateFieldScope;
102
+ readonly id: string;
103
+ readonly idn: string;
104
+ readonly title: string;
105
+ readonly default_value?: string;
106
+ readonly scope: StateFieldScope;
84
107
  }
85
108
  export type RunnerType = 'guidance' | 'nsl';
86
109
  export type SkillSelector = 'first' | 'last' | 'random' | 'all';
@@ -118,22 +141,22 @@ export interface HashStore {
118
141
  [filePath: string]: string;
119
142
  }
120
143
  export interface ParsedArticle {
121
- topic_name: string;
122
- persona_id: string | null;
123
- topic_summary: string;
124
- topic_facts: string[];
125
- confidence: number;
126
- source: string;
127
- labels: string[];
144
+ readonly topic_name: string;
145
+ readonly persona_id: string | null;
146
+ readonly topic_summary: string;
147
+ readonly topic_facts: readonly string[];
148
+ readonly confidence: number;
149
+ readonly source: string;
150
+ readonly labels: readonly string[];
128
151
  }
129
152
  export interface AkbImportArticle extends Omit<ParsedArticle, 'persona_id'> {
130
153
  persona_id: string;
131
154
  }
132
155
  export interface CliArgs {
133
- _: string[];
134
- verbose?: boolean;
135
- v?: boolean;
136
- [key: string]: unknown;
156
+ readonly _: readonly string[];
157
+ readonly verbose?: boolean;
158
+ readonly v?: boolean;
159
+ readonly [key: string]: unknown;
137
160
  }
138
161
  export interface FlowsYamlSkill {
139
162
  idn: string;
package/package.json CHANGED
@@ -1,8 +1,7 @@
1
1
  {
2
2
  "name": "newo",
3
- "version": "1.5.0",
4
- "description": "NEWO CLI: sync flows/skills between NEWO and local files, multi-project support, import AKB articles",
5
- "type": "module",
3
+ "version": "1.5.2",
4
+ "description": "NEWO CLI: sync AI Agent skills between NEWO platform and local files. Multi-customer workspaces, Git-first workflows, comprehensive project management.",
6
5
  "bin": {
7
6
  "newo": "dist/cli.js"
8
7
  },
@@ -46,14 +45,21 @@
46
45
  "dotenv": "^16.4.5",
47
46
  "fs-extra": "^11.2.0",
48
47
  "js-yaml": "^4.1.0",
49
- "minimist": "^1.2.8"
48
+ "minimist": "^1.2.8",
49
+ "p-limit": "^5.0.0"
50
50
  },
51
51
  "devDependencies": {
52
+ "@types/chai": "^4.3.11",
52
53
  "@types/fs-extra": "^11.0.4",
53
54
  "@types/js-yaml": "^4.0.9",
54
55
  "@types/minimist": "^1.2.5",
55
56
  "@types/node": "^22.5.4",
57
+ "@types/sinon": "^17.0.3",
58
+ "c8": "^9.1.0",
59
+ "chai": "^5.0.2",
56
60
  "mocha": "^10.2.0",
61
+ "sinon": "^18.0.1",
62
+ "tsx": "^4.20.5",
57
63
  "typescript": "^5.6.2"
58
64
  },
59
65
  "scripts": {
@@ -64,13 +70,14 @@
64
70
  "pull": "npm run build && node ./dist/cli.js pull",
65
71
  "push": "npm run build && node ./dist/cli.js push",
66
72
  "status": "npm run build && node ./dist/cli.js status",
67
- "clean": "rm -rf dist",
73
+ "clean": "rm -rf dist coverage",
68
74
  "typecheck": "tsc --noEmit",
69
75
  "lint": "tsc --noEmit --strict",
70
- "test": "npm run build && mocha test/*.test.js --timeout 60000",
71
- "test:api": "npm run build && mocha test/api.test.js --timeout 30000",
72
- "test:sync": "npm run build && mocha test/sync.test.js --timeout 60000",
73
- "test:integration": "npm run build && mocha test/integration.test.js --timeout 120000",
76
+ "test": "npm run build && node --test test/*.test.js",
77
+ "test:unit": "npm run build && node --test test/{api,sync,auth,hash,fsutil,akb}.test.js",
78
+ "test:integration": "npm run build && node --test test/integration.test.js",
79
+ "test:coverage": "npm run build && c8 --reporter=html --reporter=text node --test test/*.test.js",
80
+ "test:mocha": "npm run build && c8 mocha test/*.test.js --timeout 60000",
74
81
  "prepublishOnly": "npm run clean && npm run build"
75
82
  }
76
83
  }
package/src/akb.ts CHANGED
@@ -4,8 +4,8 @@ import type { ParsedArticle, AkbImportArticle } from './types.js';
4
4
  /**
5
5
  * Parse AKB file and extract articles
6
6
  */
7
- export function parseAkbFile(filePath: string): ParsedArticle[] {
8
- const content = fs.readFileSync(filePath, 'utf8');
7
+ export async function parseAkbFile(filePath: string): Promise<ParsedArticle[]> {
8
+ const content = await fs.readFile(filePath, 'utf8');
9
9
  const articles: ParsedArticle[] = [];
10
10
 
11
11
  // Split by article separators (---)
@@ -27,23 +27,28 @@ export function parseAkbFile(filePath: string): ParsedArticle[] {
27
27
  /**
28
28
  * Parse individual article section
29
29
  */
30
- function parseArticleSection(lines: string[]): ParsedArticle | null {
31
- let topicName = '';
32
- let category = '';
33
- let summary = '';
34
- let keywords = '';
35
- let topicSummary = '';
30
+ function parseArticleSection(lines: readonly string[]): ParsedArticle | null {
31
+ const state = {
32
+ topicName: '',
33
+ category: '',
34
+ summary: '',
35
+ keywords: '',
36
+ topicSummary: ''
37
+ };
36
38
 
37
39
  // Find topic name (# r001)
38
40
  const topicLine = lines.find(line => line.match(/^#\s+r\d+/));
39
- if (!topicLine) return null;
41
+ if (!topicLine) {
42
+ console.warn('No topic line found in section');
43
+ return null;
44
+ }
40
45
 
41
- topicName = topicLine.replace(/^#\s+/, '').trim();
46
+ state.topicName = topicLine.replace(/^#\s+/, '').trim();
42
47
 
43
48
  // Extract category/subcategory/description (first ## line)
44
49
  const categoryLine = lines.find(line => line.startsWith('## ') && line.includes(' / '));
45
50
  if (categoryLine) {
46
- category = categoryLine.replace(/^##\s+/, '').trim();
51
+ state.category = categoryLine.replace(/^##\s+/, '').trim();
47
52
  }
48
53
 
49
54
  // Extract summary (second ## line)
@@ -51,7 +56,7 @@ function parseArticleSection(lines: string[]): ParsedArticle | null {
51
56
  if (summaryLineIndex >= 0 && summaryLineIndex + 1 < lines.length) {
52
57
  const nextLine = lines[summaryLineIndex + 1];
53
58
  if (nextLine && nextLine.startsWith('## ') && !nextLine.includes(' / ')) {
54
- summary = nextLine.replace(/^##\s+/, '').trim();
59
+ state.summary = nextLine.replace(/^##\s+/, '').trim();
55
60
  }
56
61
  }
57
62
 
@@ -62,7 +67,7 @@ function parseArticleSection(lines: string[]): ParsedArticle | null {
62
67
  if (keywordsLineIndex >= 0) {
63
68
  const keywordsLine = lines[keywordsLineIndex];
64
69
  if (keywordsLine) {
65
- keywords = keywordsLine.replace(/^##\s+/, '').trim();
70
+ state.keywords = keywordsLine.replace(/^##\s+/, '').trim();
66
71
  }
67
72
  }
68
73
 
@@ -72,19 +77,19 @@ function parseArticleSection(lines: string[]): ParsedArticle | null {
72
77
 
73
78
  if (categoryStartIndex >= 0 && categoryEndIndex >= 0) {
74
79
  const categoryLines = lines.slice(categoryStartIndex, categoryEndIndex + 1);
75
- topicSummary = categoryLines.join('\n');
80
+ state.topicSummary = categoryLines.join('\n');
76
81
  }
77
82
 
78
83
  // Create topic_facts array
79
- const topicFacts = [category, summary, keywords].filter(fact => fact.trim() !== '');
84
+ const topicFacts = [state.category, state.summary, state.keywords].filter(fact => fact.trim() !== '');
80
85
 
81
86
  return {
82
- topic_name: category, // Use the descriptive title as topic_name
87
+ topic_name: state.category, // Use the descriptive title as topic_name
83
88
  persona_id: null, // Will be set when importing
84
- topic_summary: topicSummary,
89
+ topic_summary: state.topicSummary,
85
90
  topic_facts: topicFacts,
86
91
  confidence: 100,
87
- source: topicName, // Use the ID (r001) as source
92
+ source: state.topicName, // Use the ID (r001) as source
88
93
  labels: ['rag_context']
89
94
  };
90
95
  }
package/src/api.ts CHANGED
@@ -1,30 +1,25 @@
1
1
  import axios, { type AxiosInstance, type InternalAxiosRequestConfig, type AxiosResponse, type AxiosError } from 'axios';
2
- import dotenv from 'dotenv';
3
2
  import { getValidAccessToken, forceReauth } from './auth.js';
3
+ import { ENV } from './env.js';
4
4
  import type {
5
- NewoEnvironment,
6
5
  ProjectMeta,
7
6
  Agent,
8
7
  Skill,
9
8
  FlowEvent,
10
9
  FlowState,
11
- AkbImportArticle
10
+ AkbImportArticle,
11
+ CustomerProfile
12
12
  } from './types.js';
13
13
 
14
- dotenv.config();
14
+ // Per-request retry tracking to avoid shared state issues
15
+ const RETRY_SYMBOL = Symbol('retried');
15
16
 
16
- const { NEWO_BASE_URL } = process.env as NewoEnvironment;
17
-
18
- export async function makeClient(verbose: boolean = false): Promise<AxiosInstance> {
19
- let accessToken = await getValidAccessToken();
17
+ export async function makeClient(verbose: boolean = false, token?: string): Promise<AxiosInstance> {
18
+ let accessToken = token || await getValidAccessToken();
20
19
  if (verbose) console.log('✓ Access token obtained');
21
20
 
22
- if (!NEWO_BASE_URL) {
23
- throw new Error('NEWO_BASE_URL is not set in environment variables');
24
- }
25
-
26
21
  const client = axios.create({
27
- baseURL: NEWO_BASE_URL,
22
+ baseURL: ENV.NEWO_BASE_URL,
28
23
  headers: { accept: 'application/json' }
29
24
  });
30
25
 
@@ -41,7 +36,6 @@ export async function makeClient(verbose: boolean = false): Promise<AxiosInstanc
41
36
  return config;
42
37
  });
43
38
 
44
- let retried = false;
45
39
  client.interceptors.response.use(
46
40
  (response: AxiosResponse) => {
47
41
  if (verbose) {
@@ -49,7 +43,8 @@ export async function makeClient(verbose: boolean = false): Promise<AxiosInstanc
49
43
  if (response.data && Object.keys(response.data).length < 20) {
50
44
  console.log(' Response:', JSON.stringify(response.data, null, 2));
51
45
  } else if (response.data) {
52
- console.log(` Response: [${typeof response.data}] ${Array.isArray(response.data) ? response.data.length + ' items' : 'large object'}`);
46
+ const itemCount = Array.isArray(response.data) ? response.data.length : Object.keys(response.data).length;
47
+ console.log(` Response: [${typeof response.data}] ${Array.isArray(response.data) ? itemCount + ' items' : 'large object'}`);
53
48
  }
54
49
  }
55
50
  return response;
@@ -61,15 +56,18 @@ export async function makeClient(verbose: boolean = false): Promise<AxiosInstanc
61
56
  if (error.response?.data) console.log(' Error data:', error.response.data);
62
57
  }
63
58
 
64
- if (status === 401 && !retried) {
65
- retried = true;
66
- if (verbose) console.log('🔄 Retrying with fresh token...');
67
- accessToken = await forceReauth();
68
-
69
- if (error.config) {
70
- error.config.headers = error.config.headers || {};
71
- error.config.headers.Authorization = `Bearer ${accessToken}`;
72
- return client.request(error.config);
59
+ // Use per-request retry tracking to avoid shared state issues
60
+ const config = error.config as InternalAxiosRequestConfig & { [RETRY_SYMBOL]?: boolean };
61
+
62
+ if (status === 401 && !config?.[RETRY_SYMBOL]) {
63
+ if (config) {
64
+ config[RETRY_SYMBOL] = true;
65
+ if (verbose) console.log('🔄 Retrying with fresh token...');
66
+ accessToken = await forceReauth();
67
+
68
+ config.headers = config.headers || {};
69
+ config.headers.Authorization = `Bearer ${accessToken}`;
70
+ return client.request(config);
73
71
  }
74
72
  }
75
73
 
@@ -124,4 +122,9 @@ export async function listFlowStates(client: AxiosInstance, flowId: string): Pro
124
122
  export async function importAkbArticle(client: AxiosInstance, articleData: AkbImportArticle): Promise<unknown> {
125
123
  const response = await client.post('/api/v1/akb/append-manual', articleData);
126
124
  return response.data;
125
+ }
126
+
127
+ export async function getCustomerProfile(client: AxiosInstance): Promise<CustomerProfile> {
128
+ const response = await client.get<CustomerProfile>('/api/v1/customer/profile');
129
+ return response.data;
127
130
  }