newo 1.4.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.
@@ -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
- keywords = lines[keywordsLineIndex].replace(/^##\s+/, '').trim();
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: ["rag_context"]
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(articles, personaId) {
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
+ }
@@ -5,14 +5,15 @@ import { makeClient, getProjectMeta, importAkbArticle } from './api.js';
5
5
  import { pullAll, pushChanged, status } from './sync.js';
6
6
  import { parseAkbFile, prepareArticlesForImport } from './akb.js';
7
7
  import path from 'path';
8
+ import type { NewoEnvironment, CliArgs, NewoApiError } from './types.js';
8
9
 
9
10
  dotenv.config();
10
- const { NEWO_PROJECT_ID } = process.env;
11
+ const { NEWO_PROJECT_ID } = process.env as NewoEnvironment;
11
12
 
12
- async function main() {
13
- const args = minimist(process.argv.slice(2));
13
+ async function main(): Promise<void> {
14
+ const args = minimist(process.argv.slice(2)) as CliArgs;
14
15
  const cmd = args._[0];
15
- const verbose = args.verbose || args.v;
16
+ const verbose = Boolean(args.verbose || args.v);
16
17
 
17
18
  if (!cmd || ['help', '-h', '--help'].includes(cmd)) {
18
19
  console.log(`NEWO CLI
@@ -49,7 +50,9 @@ Notes:
49
50
  } else if (cmd === 'status') {
50
51
  await status(verbose);
51
52
  } else if (cmd === 'meta') {
52
- if (!NEWO_PROJECT_ID) throw new Error('NEWO_PROJECT_ID is not set in env');
53
+ if (!NEWO_PROJECT_ID) {
54
+ throw new Error('NEWO_PROJECT_ID is not set in env');
55
+ }
53
56
  const meta = await getProjectMeta(client, NEWO_PROJECT_ID);
54
57
  console.log(JSON.stringify(meta, null, 2));
55
58
  } else if (cmd === 'import-akb') {
@@ -79,13 +82,16 @@ Notes:
79
82
 
80
83
  for (const [index, article] of preparedArticles.entries()) {
81
84
  try {
82
- if (verbose) console.log(` [${index + 1}/${preparedArticles.length}] Importing ${article.topic_name}...`);
85
+ if (verbose) {
86
+ console.log(` [${index + 1}/${preparedArticles.length}] Importing ${article.topic_name}...`);
87
+ }
83
88
  await importAkbArticle(client, article);
84
89
  successCount++;
85
90
  if (!verbose) process.stdout.write('.');
86
91
  } catch (error) {
87
92
  errorCount++;
88
- console.error(`\n❌ Failed to import ${article.topic_name}:`, error?.response?.data || error.message);
93
+ const errorMessage = (error as NewoApiError)?.response?.data || (error as Error).message;
94
+ console.error(`\n❌ Failed to import ${article.topic_name}:`, errorMessage);
89
95
  }
90
96
  }
91
97
 
@@ -93,7 +99,7 @@ Notes:
93
99
  console.log(`✅ Import complete: ${successCount} successful, ${errorCount} failed`);
94
100
 
95
101
  } catch (error) {
96
- console.error('❌ AKB import failed:', error.message);
102
+ console.error('❌ AKB import failed:', (error as Error).message);
97
103
  process.exit(1);
98
104
  }
99
105
  } else {
@@ -102,7 +108,8 @@ Notes:
102
108
  }
103
109
  }
104
110
 
105
- main().catch((e) => {
106
- console.error(e?.response?.data || e);
111
+ main().catch((error: NewoApiError | Error) => {
112
+ const errorData = 'response' in error ? error?.response?.data : error;
113
+ console.error(errorData || error);
107
114
  process.exit(1);
108
115
  });
@@ -1,34 +1,41 @@
1
1
  import fs from 'fs-extra';
2
2
  import path from 'path';
3
+ import type { RunnerType } from './types.js';
3
4
 
4
5
  export const ROOT_DIR = path.join(process.cwd(), 'projects');
5
6
  export const STATE_DIR = path.join(process.cwd(), '.newo');
6
7
  export const MAP_PATH = path.join(STATE_DIR, 'map.json');
7
8
  export const HASHES_PATH = path.join(STATE_DIR, 'hashes.json');
8
9
 
9
- export async function ensureState() {
10
+ export async function ensureState(): Promise<void> {
10
11
  await fs.ensureDir(STATE_DIR);
11
12
  await fs.ensureDir(ROOT_DIR);
12
13
  }
13
14
 
14
- export function projectDir(projectIdn) {
15
+ export function projectDir(projectIdn: string): string {
15
16
  return path.join(ROOT_DIR, projectIdn);
16
17
  }
17
18
 
18
- export function skillPath(projectIdn, agentIdn, flowIdn, skillIdn, runnerType = 'guidance') {
19
+ export function skillPath(
20
+ projectIdn: string,
21
+ agentIdn: string,
22
+ flowIdn: string,
23
+ skillIdn: string,
24
+ runnerType: RunnerType = 'guidance'
25
+ ): string {
19
26
  const extension = runnerType === 'nsl' ? '.jinja' : '.guidance';
20
27
  return path.join(ROOT_DIR, projectIdn, agentIdn, flowIdn, `${skillIdn}${extension}`);
21
28
  }
22
29
 
23
- export function metadataPath(projectIdn) {
30
+ export function metadataPath(projectIdn: string): string {
24
31
  return path.join(ROOT_DIR, projectIdn, 'metadata.json');
25
32
  }
26
33
 
27
- export async function writeFileAtomic(filepath, content) {
34
+ export async function writeFileAtomic(filepath: string, content: string): Promise<void> {
28
35
  await fs.ensureDir(path.dirname(filepath));
29
36
  await fs.writeFile(filepath, content, 'utf8');
30
37
  }
31
38
 
32
- export async function readIfExists(filepath) {
39
+ export async function readIfExists(filepath: string): Promise<string | null> {
33
40
  return (await fs.pathExists(filepath)) ? fs.readFile(filepath, 'utf8') : null;
34
- }
41
+ }
package/src/hash.ts ADDED
@@ -0,0 +1,20 @@
1
+ import crypto from 'crypto';
2
+ import fs from 'fs-extra';
3
+ import { ensureState, HASHES_PATH } from './fsutil.js';
4
+ import type { HashStore } from './types.js';
5
+
6
+ export function sha256(str: string): string {
7
+ return crypto.createHash('sha256').update(str, 'utf8').digest('hex');
8
+ }
9
+
10
+ export async function loadHashes(): Promise<HashStore> {
11
+ await ensureState();
12
+ if (await fs.pathExists(HASHES_PATH)) {
13
+ return fs.readJson(HASHES_PATH) as Promise<HashStore>;
14
+ }
15
+ return {};
16
+ }
17
+
18
+ export async function saveHashes(hashes: HashStore): Promise<void> {
19
+ await fs.writeJson(HASHES_PATH, hashes, { spaces: 2 });
20
+ }