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/.env.example +17 -6
- package/CHANGELOG.md +91 -0
- package/README.md +502 -105
- package/dist/akb.d.ts +1 -1
- package/dist/akb.js +21 -17
- package/dist/api.d.ts +3 -2
- package/dist/api.js +24 -21
- package/dist/auth.d.ts +5 -5
- package/dist/auth.js +332 -75
- package/dist/cli.js +225 -29
- package/dist/customer.d.ts +23 -0
- package/dist/customer.js +87 -0
- package/dist/customerAsync.d.ts +22 -0
- package/dist/customerAsync.js +67 -0
- package/dist/customerInit.d.ts +10 -0
- package/dist/customerInit.js +78 -0
- package/dist/env.d.ts +33 -0
- package/dist/env.js +82 -0
- package/dist/fsutil.d.ts +14 -6
- package/dist/fsutil.js +35 -12
- package/dist/hash.d.ts +2 -2
- package/dist/hash.js +31 -8
- package/dist/sync.d.ts +5 -5
- package/dist/sync.js +91 -52
- package/dist/types.d.ts +76 -53
- package/package.json +16 -9
- package/src/akb.ts +23 -18
- package/src/api.ts +27 -24
- package/src/auth.ts +367 -94
- package/src/cli.ts +234 -33
- package/src/customer.ts +102 -0
- package/src/customerAsync.ts +78 -0
- package/src/customerInit.ts +97 -0
- package/src/env.ts +118 -0
- package/src/fsutil.ts +43 -11
- package/src/hash.ts +29 -8
- package/src/sync.ts +105 -54
- package/src/types.ts +82 -54
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.
|
|
4
|
-
"description": "NEWO CLI: sync
|
|
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 &&
|
|
71
|
-
"test:
|
|
72
|
-
"test:
|
|
73
|
-
"test:
|
|
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.
|
|
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
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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)
|
|
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
|
-
|
|
14
|
+
// Per-request retry tracking to avoid shared state issues
|
|
15
|
+
const RETRY_SYMBOL = Symbol('retried');
|
|
15
16
|
|
|
16
|
-
|
|
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
|
-
|
|
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
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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
|
}
|