newo 1.3.0 → 1.5.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.
- package/.env.example +2 -2
- package/CHANGELOG.md +99 -2
- package/README.md +59 -10
- package/dist/akb.d.ts +10 -0
- package/dist/akb.js +84 -0
- package/dist/api.d.ts +13 -0
- package/dist/api.js +100 -0
- package/dist/auth.d.ts +6 -0
- package/dist/auth.js +104 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.js +111 -0
- package/dist/fsutil.d.ts +12 -0
- package/dist/fsutil.js +28 -0
- package/dist/hash.d.ts +5 -0
- package/dist/hash.js +17 -0
- package/dist/sync.d.ts +7 -0
- package/dist/sync.js +337 -0
- package/dist/types.d.ts +206 -0
- package/dist/types.js +5 -0
- package/package.json +32 -9
- package/src/{akb.js → akb.ts} +16 -25
- package/src/api.ts +127 -0
- package/src/auth.ts +142 -0
- package/src/{cli.js → cli.ts} +29 -15
- package/src/fsutil.ts +41 -0
- package/src/hash.ts +20 -0
- package/src/sync.ts +396 -0
- package/src/types.ts +248 -0
- package/src/api.js +0 -98
- package/src/auth.js +0 -92
- package/src/fsutil.js +0 -26
- package/src/hash.js +0 -17
- package/src/sync.js +0 -284
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Comprehensive type definitions for NEWO CLI
|
|
3
|
+
*/
|
|
4
|
+
export interface NewoEnvironment {
|
|
5
|
+
NEWO_BASE_URL?: string;
|
|
6
|
+
NEWO_PROJECT_ID?: string;
|
|
7
|
+
NEWO_API_KEY?: string;
|
|
8
|
+
NEWO_ACCESS_TOKEN?: string;
|
|
9
|
+
NEWO_REFRESH_TOKEN?: string;
|
|
10
|
+
NEWO_REFRESH_URL?: string;
|
|
11
|
+
}
|
|
12
|
+
export interface TokenResponse {
|
|
13
|
+
access_token?: string;
|
|
14
|
+
token?: string;
|
|
15
|
+
accessToken?: string;
|
|
16
|
+
refresh_token?: string;
|
|
17
|
+
refreshToken?: string;
|
|
18
|
+
expires_in?: number;
|
|
19
|
+
expiresIn?: number;
|
|
20
|
+
}
|
|
21
|
+
export interface StoredTokens {
|
|
22
|
+
access_token: string;
|
|
23
|
+
refresh_token: string;
|
|
24
|
+
expires_at: number;
|
|
25
|
+
}
|
|
26
|
+
export interface ProjectMeta {
|
|
27
|
+
id: string;
|
|
28
|
+
idn: string;
|
|
29
|
+
title: string;
|
|
30
|
+
description?: string;
|
|
31
|
+
created_at?: string;
|
|
32
|
+
updated_at?: string;
|
|
33
|
+
}
|
|
34
|
+
export interface Agent {
|
|
35
|
+
id: string;
|
|
36
|
+
idn: string;
|
|
37
|
+
title?: string;
|
|
38
|
+
description?: string;
|
|
39
|
+
flows?: Flow[];
|
|
40
|
+
}
|
|
41
|
+
export interface Flow {
|
|
42
|
+
id: string;
|
|
43
|
+
idn: string;
|
|
44
|
+
title: string;
|
|
45
|
+
description?: string;
|
|
46
|
+
default_runner_type: RunnerType;
|
|
47
|
+
default_model: ModelConfig;
|
|
48
|
+
}
|
|
49
|
+
export interface ModelConfig {
|
|
50
|
+
model_idn: string;
|
|
51
|
+
provider_idn: string;
|
|
52
|
+
}
|
|
53
|
+
export interface SkillParameter {
|
|
54
|
+
name: string;
|
|
55
|
+
default_value?: string;
|
|
56
|
+
}
|
|
57
|
+
export interface Skill {
|
|
58
|
+
id: string;
|
|
59
|
+
idn: string;
|
|
60
|
+
title: string;
|
|
61
|
+
prompt_script?: string;
|
|
62
|
+
runner_type: RunnerType;
|
|
63
|
+
model: ModelConfig;
|
|
64
|
+
parameters: SkillParameter[];
|
|
65
|
+
path?: string | undefined;
|
|
66
|
+
}
|
|
67
|
+
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;
|
|
77
|
+
}
|
|
78
|
+
export interface FlowState {
|
|
79
|
+
id: string;
|
|
80
|
+
idn: string;
|
|
81
|
+
title: string;
|
|
82
|
+
default_value?: string;
|
|
83
|
+
scope: StateFieldScope;
|
|
84
|
+
}
|
|
85
|
+
export type RunnerType = 'guidance' | 'nsl';
|
|
86
|
+
export type SkillSelector = 'first' | 'last' | 'random' | 'all';
|
|
87
|
+
export type InterruptMode = 'allow' | 'deny' | 'queue';
|
|
88
|
+
export type StateFieldScope = 'flow' | 'agent' | 'project' | 'global';
|
|
89
|
+
export interface SkillMetadata {
|
|
90
|
+
id: string;
|
|
91
|
+
title: string;
|
|
92
|
+
idn: string;
|
|
93
|
+
runner_type: RunnerType;
|
|
94
|
+
model: ModelConfig;
|
|
95
|
+
parameters: SkillParameter[];
|
|
96
|
+
path?: string | undefined;
|
|
97
|
+
}
|
|
98
|
+
export interface FlowData {
|
|
99
|
+
id: string;
|
|
100
|
+
skills: Record<string, SkillMetadata>;
|
|
101
|
+
}
|
|
102
|
+
export interface AgentData {
|
|
103
|
+
id: string;
|
|
104
|
+
flows: Record<string, FlowData>;
|
|
105
|
+
}
|
|
106
|
+
export interface ProjectData {
|
|
107
|
+
projectId: string;
|
|
108
|
+
projectIdn: string;
|
|
109
|
+
agents: Record<string, AgentData>;
|
|
110
|
+
}
|
|
111
|
+
export interface ProjectMap {
|
|
112
|
+
projects: Record<string, ProjectData>;
|
|
113
|
+
}
|
|
114
|
+
export interface LegacyProjectMap extends ProjectData {
|
|
115
|
+
projects?: Record<string, ProjectData>;
|
|
116
|
+
}
|
|
117
|
+
export interface HashStore {
|
|
118
|
+
[filePath: string]: string;
|
|
119
|
+
}
|
|
120
|
+
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[];
|
|
128
|
+
}
|
|
129
|
+
export interface AkbImportArticle extends Omit<ParsedArticle, 'persona_id'> {
|
|
130
|
+
persona_id: string;
|
|
131
|
+
}
|
|
132
|
+
export interface CliArgs {
|
|
133
|
+
_: string[];
|
|
134
|
+
verbose?: boolean;
|
|
135
|
+
v?: boolean;
|
|
136
|
+
[key: string]: unknown;
|
|
137
|
+
}
|
|
138
|
+
export interface FlowsYamlSkill {
|
|
139
|
+
idn: string;
|
|
140
|
+
title: string;
|
|
141
|
+
prompt_script: string;
|
|
142
|
+
runner_type: string;
|
|
143
|
+
model: ModelConfig;
|
|
144
|
+
parameters: Array<{
|
|
145
|
+
name: string;
|
|
146
|
+
default_value: string;
|
|
147
|
+
}>;
|
|
148
|
+
}
|
|
149
|
+
export interface FlowsYamlEvent {
|
|
150
|
+
title: string;
|
|
151
|
+
idn: string;
|
|
152
|
+
skill_selector: string;
|
|
153
|
+
skill_idn?: string | undefined;
|
|
154
|
+
state_idn?: string | undefined;
|
|
155
|
+
integration_idn?: string | undefined;
|
|
156
|
+
connector_idn?: string | undefined;
|
|
157
|
+
interrupt_mode: string;
|
|
158
|
+
}
|
|
159
|
+
export interface FlowsYamlState {
|
|
160
|
+
title: string;
|
|
161
|
+
idn: string;
|
|
162
|
+
default_value?: string | undefined;
|
|
163
|
+
scope: string;
|
|
164
|
+
}
|
|
165
|
+
export interface FlowsYamlFlow {
|
|
166
|
+
idn: string;
|
|
167
|
+
title: string;
|
|
168
|
+
description: string | null;
|
|
169
|
+
default_runner_type: string;
|
|
170
|
+
default_provider_idn: string;
|
|
171
|
+
default_model_idn: string;
|
|
172
|
+
skills: FlowsYamlSkill[];
|
|
173
|
+
events: FlowsYamlEvent[];
|
|
174
|
+
state_fields: FlowsYamlState[];
|
|
175
|
+
}
|
|
176
|
+
export interface FlowsYamlAgent {
|
|
177
|
+
agent_idn: string;
|
|
178
|
+
agent_description?: string | undefined;
|
|
179
|
+
agent_flows: FlowsYamlFlow[];
|
|
180
|
+
}
|
|
181
|
+
export interface FlowsYamlData {
|
|
182
|
+
flows: FlowsYamlAgent[];
|
|
183
|
+
}
|
|
184
|
+
export interface AxiosClientConfig {
|
|
185
|
+
baseURL?: string;
|
|
186
|
+
headers?: Record<string, string>;
|
|
187
|
+
}
|
|
188
|
+
export interface NewoApiError extends Error {
|
|
189
|
+
response?: {
|
|
190
|
+
status: number;
|
|
191
|
+
data: unknown;
|
|
192
|
+
};
|
|
193
|
+
config?: {
|
|
194
|
+
method?: string;
|
|
195
|
+
url?: string;
|
|
196
|
+
headers?: Record<string, string>;
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
export type FileStatus = 'M' | 'D' | 'clean';
|
|
200
|
+
export interface StatusResult {
|
|
201
|
+
filePath: string;
|
|
202
|
+
status: FileStatus;
|
|
203
|
+
oldHash?: string;
|
|
204
|
+
newHash?: string;
|
|
205
|
+
}
|
|
206
|
+
//# sourceMappingURL=types.d.ts.map
|
package/dist/types.js
ADDED
package/package.json
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "newo",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "NEWO CLI: sync flows/skills between NEWO and local files, import AKB articles",
|
|
3
|
+
"version": "1.5.0",
|
|
4
|
+
"description": "NEWO CLI: sync flows/skills between NEWO and local files, multi-project support, import AKB articles",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
|
-
"newo": "
|
|
7
|
+
"newo": "dist/cli.js"
|
|
8
8
|
},
|
|
9
9
|
"files": [
|
|
10
|
-
"
|
|
10
|
+
"dist/**/*.js",
|
|
11
|
+
"dist/**/*.d.ts",
|
|
12
|
+
"src/**/*.ts",
|
|
11
13
|
"README.md",
|
|
12
14
|
"CHANGELOG.md",
|
|
13
15
|
".env.example"
|
|
@@ -22,7 +24,9 @@
|
|
|
22
24
|
"local-development",
|
|
23
25
|
"akb",
|
|
24
26
|
"knowledge-base",
|
|
25
|
-
"import"
|
|
27
|
+
"import",
|
|
28
|
+
"multi-project",
|
|
29
|
+
"workspace"
|
|
26
30
|
],
|
|
27
31
|
"author": "sabbah13",
|
|
28
32
|
"license": "MIT",
|
|
@@ -44,10 +48,29 @@
|
|
|
44
48
|
"js-yaml": "^4.1.0",
|
|
45
49
|
"minimist": "^1.2.8"
|
|
46
50
|
},
|
|
51
|
+
"devDependencies": {
|
|
52
|
+
"@types/fs-extra": "^11.0.4",
|
|
53
|
+
"@types/js-yaml": "^4.0.9",
|
|
54
|
+
"@types/minimist": "^1.2.5",
|
|
55
|
+
"@types/node": "^22.5.4",
|
|
56
|
+
"mocha": "^10.2.0",
|
|
57
|
+
"typescript": "^5.6.2"
|
|
58
|
+
},
|
|
47
59
|
"scripts": {
|
|
48
|
-
"
|
|
49
|
-
"
|
|
50
|
-
"
|
|
51
|
-
"
|
|
60
|
+
"build": "tsc",
|
|
61
|
+
"build:watch": "tsc --watch",
|
|
62
|
+
"dev": "npm run build && node ./dist/cli.js",
|
|
63
|
+
"dev:watch": "tsc --watch & nodemon --watch dist dist/cli.js",
|
|
64
|
+
"pull": "npm run build && node ./dist/cli.js pull",
|
|
65
|
+
"push": "npm run build && node ./dist/cli.js push",
|
|
66
|
+
"status": "npm run build && node ./dist/cli.js status",
|
|
67
|
+
"clean": "rm -rf dist",
|
|
68
|
+
"typecheck": "tsc --noEmit",
|
|
69
|
+
"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",
|
|
74
|
+
"prepublishOnly": "npm run clean && npm run build"
|
|
52
75
|
}
|
|
53
76
|
}
|
package/src/{akb.js → akb.ts}
RENAMED
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
import fs from 'fs-extra';
|
|
2
|
+
import type { ParsedArticle, AkbImportArticle } from './types.js';
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Parse AKB file and extract articles
|
|
5
|
-
* @param {string} filePath - Path to AKB file
|
|
6
|
-
* @returns {Array} Array of parsed articles
|
|
7
6
|
*/
|
|
8
|
-
function parseAkbFile(filePath) {
|
|
7
|
+
export function parseAkbFile(filePath: string): ParsedArticle[] {
|
|
9
8
|
const content = fs.readFileSync(filePath, 'utf8');
|
|
10
|
-
const articles = [];
|
|
9
|
+
const articles: ParsedArticle[] = [];
|
|
11
10
|
|
|
12
11
|
// Split by article separators (---)
|
|
13
12
|
const sections = content.split(/^---\s*$/gm).filter(section => section.trim());
|
|
@@ -27,10 +26,8 @@ function parseAkbFile(filePath) {
|
|
|
27
26
|
|
|
28
27
|
/**
|
|
29
28
|
* Parse individual article section
|
|
30
|
-
* @param {Array} lines - Lines of the article section
|
|
31
|
-
* @returns {Object|null} Parsed article object
|
|
32
29
|
*/
|
|
33
|
-
function parseArticleSection(lines) {
|
|
30
|
+
function parseArticleSection(lines: string[]): ParsedArticle | null {
|
|
34
31
|
let topicName = '';
|
|
35
32
|
let category = '';
|
|
36
33
|
let summary = '';
|
|
@@ -53,7 +50,7 @@ function parseArticleSection(lines) {
|
|
|
53
50
|
const summaryLineIndex = lines.findIndex(line => line.startsWith('## ') && line.includes(' / '));
|
|
54
51
|
if (summaryLineIndex >= 0 && summaryLineIndex + 1 < lines.length) {
|
|
55
52
|
const nextLine = lines[summaryLineIndex + 1];
|
|
56
|
-
if (nextLine.startsWith('## ') && !nextLine.includes(' / ')) {
|
|
53
|
+
if (nextLine && nextLine.startsWith('## ') && !nextLine.includes(' / ')) {
|
|
57
54
|
summary = nextLine.replace(/^##\s+/, '').trim();
|
|
58
55
|
}
|
|
59
56
|
}
|
|
@@ -63,7 +60,10 @@ function parseArticleSection(lines) {
|
|
|
63
60
|
index > summaryLineIndex + 1 && line.startsWith('## ') && !line.includes(' / ')
|
|
64
61
|
);
|
|
65
62
|
if (keywordsLineIndex >= 0) {
|
|
66
|
-
|
|
63
|
+
const keywordsLine = lines[keywordsLineIndex];
|
|
64
|
+
if (keywordsLine) {
|
|
65
|
+
keywords = keywordsLine.replace(/^##\s+/, '').trim();
|
|
66
|
+
}
|
|
67
67
|
}
|
|
68
68
|
|
|
69
69
|
// Extract category content
|
|
@@ -76,11 +76,7 @@ function parseArticleSection(lines) {
|
|
|
76
76
|
}
|
|
77
77
|
|
|
78
78
|
// Create topic_facts array
|
|
79
|
-
const topicFacts = [
|
|
80
|
-
category,
|
|
81
|
-
summary,
|
|
82
|
-
keywords
|
|
83
|
-
].filter(fact => fact.trim() !== '');
|
|
79
|
+
const topicFacts = [category, summary, keywords].filter(fact => fact.trim() !== '');
|
|
84
80
|
|
|
85
81
|
return {
|
|
86
82
|
topic_name: category, // Use the descriptive title as topic_name
|
|
@@ -89,24 +85,19 @@ function parseArticleSection(lines) {
|
|
|
89
85
|
topic_facts: topicFacts,
|
|
90
86
|
confidence: 100,
|
|
91
87
|
source: topicName, // Use the ID (r001) as source
|
|
92
|
-
labels: [
|
|
88
|
+
labels: ['rag_context']
|
|
93
89
|
};
|
|
94
90
|
}
|
|
95
91
|
|
|
96
92
|
/**
|
|
97
93
|
* Convert parsed articles to API format for bulk import
|
|
98
|
-
* @param {Array} articles - Parsed articles
|
|
99
|
-
* @param {string} personaId - Target persona ID
|
|
100
|
-
* @returns {Array} Array of articles ready for API import
|
|
101
94
|
*/
|
|
102
|
-
function prepareArticlesForImport(
|
|
95
|
+
export function prepareArticlesForImport(
|
|
96
|
+
articles: ParsedArticle[],
|
|
97
|
+
personaId: string
|
|
98
|
+
): AkbImportArticle[] {
|
|
103
99
|
return articles.map(article => ({
|
|
104
100
|
...article,
|
|
105
101
|
persona_id: personaId
|
|
106
102
|
}));
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
export {
|
|
110
|
-
parseAkbFile,
|
|
111
|
-
prepareArticlesForImport
|
|
112
|
-
};
|
|
103
|
+
}
|
package/src/api.ts
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import axios, { type AxiosInstance, type InternalAxiosRequestConfig, type AxiosResponse, type AxiosError } from 'axios';
|
|
2
|
+
import dotenv from 'dotenv';
|
|
3
|
+
import { getValidAccessToken, forceReauth } from './auth.js';
|
|
4
|
+
import type {
|
|
5
|
+
NewoEnvironment,
|
|
6
|
+
ProjectMeta,
|
|
7
|
+
Agent,
|
|
8
|
+
Skill,
|
|
9
|
+
FlowEvent,
|
|
10
|
+
FlowState,
|
|
11
|
+
AkbImportArticle
|
|
12
|
+
} from './types.js';
|
|
13
|
+
|
|
14
|
+
dotenv.config();
|
|
15
|
+
|
|
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();
|
|
20
|
+
if (verbose) console.log('✓ Access token obtained');
|
|
21
|
+
|
|
22
|
+
if (!NEWO_BASE_URL) {
|
|
23
|
+
throw new Error('NEWO_BASE_URL is not set in environment variables');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const client = axios.create({
|
|
27
|
+
baseURL: NEWO_BASE_URL,
|
|
28
|
+
headers: { accept: 'application/json' }
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
client.interceptors.request.use(async (config: InternalAxiosRequestConfig) => {
|
|
32
|
+
config.headers = config.headers || {};
|
|
33
|
+
config.headers.Authorization = `Bearer ${accessToken}`;
|
|
34
|
+
|
|
35
|
+
if (verbose) {
|
|
36
|
+
console.log(`→ ${config.method?.toUpperCase()} ${config.url}`);
|
|
37
|
+
if (config.data) console.log(' Data:', JSON.stringify(config.data, null, 2));
|
|
38
|
+
if (config.params) console.log(' Params:', config.params);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return config;
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
let retried = false;
|
|
45
|
+
client.interceptors.response.use(
|
|
46
|
+
(response: AxiosResponse) => {
|
|
47
|
+
if (verbose) {
|
|
48
|
+
console.log(`← ${response.status} ${response.config.method?.toUpperCase()} ${response.config.url}`);
|
|
49
|
+
if (response.data && Object.keys(response.data).length < 20) {
|
|
50
|
+
console.log(' Response:', JSON.stringify(response.data, null, 2));
|
|
51
|
+
} else if (response.data) {
|
|
52
|
+
console.log(` Response: [${typeof response.data}] ${Array.isArray(response.data) ? response.data.length + ' items' : 'large object'}`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return response;
|
|
56
|
+
},
|
|
57
|
+
async (error: AxiosError) => {
|
|
58
|
+
const status = error?.response?.status;
|
|
59
|
+
if (verbose) {
|
|
60
|
+
console.log(`← ${status} ${error.config?.method?.toUpperCase()} ${error.config?.url} - ${error.message}`);
|
|
61
|
+
if (error.response?.data) console.log(' Error data:', error.response.data);
|
|
62
|
+
}
|
|
63
|
+
|
|
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);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
throw error;
|
|
77
|
+
}
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
return client;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export async function listProjects(client: AxiosInstance): Promise<ProjectMeta[]> {
|
|
84
|
+
const response = await client.get<ProjectMeta[]>('/api/v1/designer/projects');
|
|
85
|
+
return response.data;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export async function listAgents(client: AxiosInstance, projectId: string): Promise<Agent[]> {
|
|
89
|
+
const response = await client.get<Agent[]>('/api/v1/bff/agents/list', {
|
|
90
|
+
params: { project_id: projectId }
|
|
91
|
+
});
|
|
92
|
+
return response.data;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export async function getProjectMeta(client: AxiosInstance, projectId: string): Promise<ProjectMeta> {
|
|
96
|
+
const response = await client.get<ProjectMeta>(`/api/v1/designer/projects/by-id/${projectId}`);
|
|
97
|
+
return response.data;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export async function listFlowSkills(client: AxiosInstance, flowId: string): Promise<Skill[]> {
|
|
101
|
+
const response = await client.get<Skill[]>(`/api/v1/designer/flows/${flowId}/skills`);
|
|
102
|
+
return response.data;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export async function getSkill(client: AxiosInstance, skillId: string): Promise<Skill> {
|
|
106
|
+
const response = await client.get<Skill>(`/api/v1/designer/skills/${skillId}`);
|
|
107
|
+
return response.data;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export async function updateSkill(client: AxiosInstance, skillObject: Skill): Promise<void> {
|
|
111
|
+
await client.put(`/api/v1/designer/flows/skills/${skillObject.id}`, skillObject);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export async function listFlowEvents(client: AxiosInstance, flowId: string): Promise<FlowEvent[]> {
|
|
115
|
+
const response = await client.get<FlowEvent[]>(`/api/v1/designer/flows/${flowId}/events`);
|
|
116
|
+
return response.data;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export async function listFlowStates(client: AxiosInstance, flowId: string): Promise<FlowState[]> {
|
|
120
|
+
const response = await client.get<FlowState[]>(`/api/v1/designer/flows/${flowId}/states`);
|
|
121
|
+
return response.data;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export async function importAkbArticle(client: AxiosInstance, articleData: AkbImportArticle): Promise<unknown> {
|
|
125
|
+
const response = await client.post('/api/v1/akb/append-manual', articleData);
|
|
126
|
+
return response.data;
|
|
127
|
+
}
|
package/src/auth.ts
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import fs from 'fs-extra';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import axios from 'axios';
|
|
4
|
+
import dotenv from 'dotenv';
|
|
5
|
+
import type { NewoEnvironment, TokenResponse, StoredTokens } from './types.js';
|
|
6
|
+
|
|
7
|
+
dotenv.config();
|
|
8
|
+
|
|
9
|
+
const {
|
|
10
|
+
NEWO_BASE_URL,
|
|
11
|
+
NEWO_API_KEY,
|
|
12
|
+
NEWO_ACCESS_TOKEN,
|
|
13
|
+
NEWO_REFRESH_TOKEN,
|
|
14
|
+
NEWO_REFRESH_URL
|
|
15
|
+
} = process.env as NewoEnvironment;
|
|
16
|
+
|
|
17
|
+
const STATE_DIR = path.join(process.cwd(), '.newo');
|
|
18
|
+
const TOKENS_PATH = path.join(STATE_DIR, 'tokens.json');
|
|
19
|
+
|
|
20
|
+
async function saveTokens(tokens: StoredTokens): Promise<void> {
|
|
21
|
+
await fs.ensureDir(STATE_DIR);
|
|
22
|
+
await fs.writeJson(TOKENS_PATH, tokens, { spaces: 2 });
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async function loadTokens(): Promise<StoredTokens | null> {
|
|
26
|
+
if (await fs.pathExists(TOKENS_PATH)) {
|
|
27
|
+
return fs.readJson(TOKENS_PATH) as Promise<StoredTokens>;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (NEWO_ACCESS_TOKEN || NEWO_REFRESH_TOKEN) {
|
|
31
|
+
const tokens: StoredTokens = {
|
|
32
|
+
access_token: NEWO_ACCESS_TOKEN || '',
|
|
33
|
+
refresh_token: NEWO_REFRESH_TOKEN || '',
|
|
34
|
+
expires_at: Date.now() + 10 * 60 * 1000
|
|
35
|
+
};
|
|
36
|
+
await saveTokens(tokens);
|
|
37
|
+
return tokens;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function isExpired(tokens: StoredTokens | null): boolean {
|
|
44
|
+
if (!tokens?.expires_at) return false;
|
|
45
|
+
return Date.now() >= tokens.expires_at - 10_000;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export async function exchangeApiKeyForToken(): Promise<StoredTokens> {
|
|
49
|
+
if (!NEWO_API_KEY) {
|
|
50
|
+
throw new Error('NEWO_API_KEY not set. Provide an API key in .env');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const url = `${NEWO_BASE_URL}/api/v1/auth/api-key/token`;
|
|
54
|
+
const response = await axios.post<TokenResponse>(
|
|
55
|
+
url,
|
|
56
|
+
{},
|
|
57
|
+
{
|
|
58
|
+
headers: {
|
|
59
|
+
'x-api-key': NEWO_API_KEY,
|
|
60
|
+
'accept': 'application/json'
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
const data = response.data;
|
|
66
|
+
const access = data.access_token || data.token || data.accessToken;
|
|
67
|
+
const refresh = data.refresh_token || data.refreshToken || '';
|
|
68
|
+
const expiresInSec = data.expires_in || data.expiresIn || 3600;
|
|
69
|
+
|
|
70
|
+
if (!access) {
|
|
71
|
+
throw new Error('Failed to get access token from API key exchange');
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const tokens: StoredTokens = {
|
|
75
|
+
access_token: access,
|
|
76
|
+
refresh_token: refresh,
|
|
77
|
+
expires_at: Date.now() + expiresInSec * 1000
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
await saveTokens(tokens);
|
|
81
|
+
return tokens;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export async function refreshWithEndpoint(refreshToken: string): Promise<StoredTokens> {
|
|
85
|
+
if (!NEWO_REFRESH_URL) {
|
|
86
|
+
throw new Error('NEWO_REFRESH_URL not set');
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const response = await axios.post<TokenResponse>(
|
|
90
|
+
NEWO_REFRESH_URL,
|
|
91
|
+
{ refresh_token: refreshToken },
|
|
92
|
+
{ headers: { 'accept': 'application/json' } }
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
const data = response.data;
|
|
96
|
+
const access = data.access_token || data.token || data.accessToken;
|
|
97
|
+
const refresh = data.refresh_token ?? refreshToken;
|
|
98
|
+
const expiresInSec = data.expires_in || 3600;
|
|
99
|
+
|
|
100
|
+
if (!access) {
|
|
101
|
+
throw new Error('Failed to get access token from refresh');
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const tokens: StoredTokens = {
|
|
105
|
+
access_token: access,
|
|
106
|
+
refresh_token: refresh,
|
|
107
|
+
expires_at: Date.now() + expiresInSec * 1000
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
await saveTokens(tokens);
|
|
111
|
+
return tokens;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export async function getValidAccessToken(): Promise<string> {
|
|
115
|
+
let tokens = await loadTokens();
|
|
116
|
+
|
|
117
|
+
if (!tokens || !tokens.access_token) {
|
|
118
|
+
tokens = await exchangeApiKeyForToken();
|
|
119
|
+
return tokens.access_token;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (!isExpired(tokens)) {
|
|
123
|
+
return tokens.access_token;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (NEWO_REFRESH_URL && tokens.refresh_token) {
|
|
127
|
+
try {
|
|
128
|
+
tokens = await refreshWithEndpoint(tokens.refresh_token);
|
|
129
|
+
return tokens.access_token;
|
|
130
|
+
} catch (error) {
|
|
131
|
+
console.warn('Refresh failed, falling back to API key exchange…');
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
tokens = await exchangeApiKeyForToken();
|
|
136
|
+
return tokens.access_token;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export async function forceReauth(): Promise<string> {
|
|
140
|
+
const tokens = await exchangeApiKeyForToken();
|
|
141
|
+
return tokens.access_token;
|
|
142
|
+
}
|