newo 1.4.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 +17 -6
- package/CHANGELOG.md +102 -0
- package/README.md +96 -1
- package/dist/akb.d.ts +10 -0
- package/dist/akb.js +88 -0
- package/dist/api.d.ts +14 -0
- package/dist/api.js +103 -0
- package/dist/auth.d.ts +6 -0
- package/dist/auth.js +361 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.js +307 -0
- 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 +20 -0
- package/dist/fsutil.js +51 -0
- package/dist/hash.d.ts +5 -0
- package/dist/hash.js +40 -0
- package/dist/sync.d.ts +7 -0
- package/dist/sync.js +376 -0
- package/dist/types.d.ts +229 -0
- package/dist/types.js +5 -0
- package/package.json +35 -14
- package/src/{akb.js → akb.ts} +35 -39
- package/src/api.ts +130 -0
- package/src/auth.ts +415 -0
- package/src/cli.ts +316 -0
- 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 +73 -0
- package/src/hash.ts +41 -0
- package/src/{sync.js → sync.ts} +179 -81
- package/src/types.ts +276 -0
- package/src/api.js +0 -103
- package/src/auth.js +0 -92
- package/src/cli.js +0 -108
- package/src/fsutil.js +0 -34
- package/src/hash.js +0 -17
package/src/env.ts
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import type { NewoEnvironment } from './types.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Validated environment configuration
|
|
5
|
+
*/
|
|
6
|
+
export interface ValidatedEnv {
|
|
7
|
+
readonly NEWO_BASE_URL: string;
|
|
8
|
+
readonly NEWO_PROJECT_ID: string | undefined;
|
|
9
|
+
readonly NEWO_API_KEY: string | undefined;
|
|
10
|
+
readonly NEWO_API_KEYS: string | undefined;
|
|
11
|
+
readonly NEWO_ACCESS_TOKEN: string | undefined;
|
|
12
|
+
readonly NEWO_REFRESH_TOKEN: string | undefined;
|
|
13
|
+
readonly NEWO_REFRESH_URL: string | undefined;
|
|
14
|
+
readonly NEWO_DEFAULT_CUSTOMER: string | undefined;
|
|
15
|
+
// Dynamic customer entries will be detected at runtime
|
|
16
|
+
readonly [key: string]: string | undefined;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Environment validation errors with clear messaging
|
|
21
|
+
*/
|
|
22
|
+
export class EnvValidationError extends Error {
|
|
23
|
+
constructor(message: string) {
|
|
24
|
+
super(`Environment validation failed: ${message}`);
|
|
25
|
+
this.name = 'EnvValidationError';
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Validates required environment variables and returns typed configuration
|
|
31
|
+
*/
|
|
32
|
+
export function validateEnvironment(): ValidatedEnv {
|
|
33
|
+
const env = process.env as NewoEnvironment;
|
|
34
|
+
|
|
35
|
+
const baseUrl = env.NEWO_BASE_URL?.trim() || 'https://app.newo.ai';
|
|
36
|
+
const projectId = env.NEWO_PROJECT_ID?.trim();
|
|
37
|
+
const apiKey = env.NEWO_API_KEY?.trim();
|
|
38
|
+
const accessToken = env.NEWO_ACCESS_TOKEN?.trim();
|
|
39
|
+
const refreshToken = env.NEWO_REFRESH_TOKEN?.trim();
|
|
40
|
+
const refreshUrl = env.NEWO_REFRESH_URL?.trim();
|
|
41
|
+
|
|
42
|
+
// Base URL validation
|
|
43
|
+
if (!isValidUrl(baseUrl)) {
|
|
44
|
+
throw new EnvValidationError(
|
|
45
|
+
`NEWO_BASE_URL must be a valid URL. Received: ${baseUrl}`
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Project ID is optional - if not set, pull all projects
|
|
50
|
+
// If provided, validate UUID format
|
|
51
|
+
if (projectId && !isValidUuid(projectId)) {
|
|
52
|
+
throw new EnvValidationError(
|
|
53
|
+
`NEWO_PROJECT_ID must be a valid UUID when provided. Received: ${projectId}`
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Authentication validation - at least one method required
|
|
58
|
+
const hasApiKey = !!apiKey;
|
|
59
|
+
const hasApiKeys = !!env.NEWO_API_KEYS?.trim();
|
|
60
|
+
const hasDirectTokens = !!(accessToken && refreshToken);
|
|
61
|
+
|
|
62
|
+
if (!hasApiKey && !hasApiKeys && !hasDirectTokens) {
|
|
63
|
+
throw new EnvValidationError(
|
|
64
|
+
'Authentication required: Set NEWO_API_KEY, NEWO_API_KEYS (recommended), or both NEWO_ACCESS_TOKEN and NEWO_REFRESH_TOKEN'
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// If refresh URL is provided, validate it
|
|
69
|
+
if (refreshUrl && !isValidUrl(refreshUrl)) {
|
|
70
|
+
throw new EnvValidationError(
|
|
71
|
+
`NEWO_REFRESH_URL must be a valid URL when provided. Received: ${refreshUrl}`
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
NEWO_BASE_URL: baseUrl,
|
|
77
|
+
NEWO_PROJECT_ID: projectId || undefined,
|
|
78
|
+
NEWO_API_KEY: apiKey,
|
|
79
|
+
NEWO_API_KEYS: env.NEWO_API_KEYS?.trim(),
|
|
80
|
+
NEWO_ACCESS_TOKEN: accessToken,
|
|
81
|
+
NEWO_REFRESH_TOKEN: refreshToken,
|
|
82
|
+
NEWO_REFRESH_URL: refreshUrl,
|
|
83
|
+
NEWO_DEFAULT_CUSTOMER: env.NEWO_DEFAULT_CUSTOMER?.trim(),
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Validates if a string is a valid URL
|
|
89
|
+
*/
|
|
90
|
+
function isValidUrl(urlString: string): boolean {
|
|
91
|
+
try {
|
|
92
|
+
new URL(urlString);
|
|
93
|
+
return true;
|
|
94
|
+
} catch {
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Validates if a string is a valid UUID (v4 format)
|
|
101
|
+
*/
|
|
102
|
+
function isValidUuid(uuid: string): boolean {
|
|
103
|
+
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
104
|
+
return uuidRegex.test(uuid);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Global validated environment - call validateEnvironment() once at startup
|
|
109
|
+
*/
|
|
110
|
+
export let ENV: ValidatedEnv;
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Initialize environment validation - must be called at application startup
|
|
114
|
+
*/
|
|
115
|
+
export function initializeEnvironment(): ValidatedEnv {
|
|
116
|
+
ENV = validateEnvironment();
|
|
117
|
+
return ENV;
|
|
118
|
+
}
|
package/src/fsutil.ts
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import fs from 'fs-extra';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import type { RunnerType } from './types.js';
|
|
4
|
+
|
|
5
|
+
export const NEWO_CUSTOMERS_DIR = path.posix.join(process.cwd(), 'newo_customers');
|
|
6
|
+
export const STATE_DIR = path.join(process.cwd(), '.newo');
|
|
7
|
+
|
|
8
|
+
export function customerDir(customerIdn: string): string {
|
|
9
|
+
return path.posix.join(NEWO_CUSTOMERS_DIR, customerIdn);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function customerProjectsDir(customerIdn: string): string {
|
|
13
|
+
return path.posix.join(customerDir(customerIdn), 'projects');
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function customerStateDir(customerIdn: string): string {
|
|
17
|
+
return path.join(STATE_DIR, customerIdn);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function mapPath(customerIdn: string): string {
|
|
21
|
+
return path.join(customerStateDir(customerIdn), 'map.json');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function hashesPath(customerIdn: string): string {
|
|
25
|
+
return path.join(customerStateDir(customerIdn), 'hashes.json');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export async function ensureState(customerIdn: string): Promise<void> {
|
|
29
|
+
await fs.ensureDir(STATE_DIR);
|
|
30
|
+
await fs.ensureDir(customerStateDir(customerIdn));
|
|
31
|
+
await fs.ensureDir(customerProjectsDir(customerIdn));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function projectDir(customerIdn: string, projectIdn: string): string {
|
|
35
|
+
return path.posix.join(customerProjectsDir(customerIdn), projectIdn);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function flowsYamlPath(customerIdn: string): string {
|
|
39
|
+
return path.posix.join(customerProjectsDir(customerIdn), 'flows.yaml');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function skillPath(
|
|
43
|
+
customerIdn: string,
|
|
44
|
+
projectIdn: string,
|
|
45
|
+
agentIdn: string,
|
|
46
|
+
flowIdn: string,
|
|
47
|
+
skillIdn: string,
|
|
48
|
+
runnerType: RunnerType = 'guidance'
|
|
49
|
+
): string {
|
|
50
|
+
const extension = runnerType === 'nsl' ? '.jinja' : '.guidance';
|
|
51
|
+
return path.posix.join(customerProjectsDir(customerIdn), projectIdn, agentIdn, flowIdn, `${skillIdn}${extension}`);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function metadataPath(customerIdn: string, projectIdn: string): string {
|
|
55
|
+
return path.posix.join(customerProjectsDir(customerIdn), projectIdn, 'metadata.json');
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Legacy support - will be deprecated
|
|
59
|
+
export const ROOT_DIR = path.posix.join(process.cwd(), 'projects');
|
|
60
|
+
export const MAP_PATH = path.join(STATE_DIR, 'map.json');
|
|
61
|
+
export const HASHES_PATH = path.join(STATE_DIR, 'hashes.json');
|
|
62
|
+
|
|
63
|
+
export async function writeFileSafe(filepath: string, content: string): Promise<void> {
|
|
64
|
+
await fs.ensureDir(path.dirname(filepath));
|
|
65
|
+
await fs.writeFile(filepath, content, 'utf8');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Deprecated: use writeFileSafe instead
|
|
69
|
+
export const writeFileAtomic = writeFileSafe;
|
|
70
|
+
|
|
71
|
+
export async function readIfExists(filepath: string): Promise<string | null> {
|
|
72
|
+
return (await fs.pathExists(filepath)) ? fs.readFile(filepath, 'utf8') : null;
|
|
73
|
+
}
|
package/src/hash.ts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import crypto from 'crypto';
|
|
2
|
+
import fs from 'fs-extra';
|
|
3
|
+
import { ensureState, hashesPath, 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(customerIdn?: string): Promise<HashStore> {
|
|
11
|
+
if (customerIdn) {
|
|
12
|
+
await ensureState(customerIdn);
|
|
13
|
+
try {
|
|
14
|
+
return await fs.readJson(hashesPath(customerIdn)) as HashStore;
|
|
15
|
+
} catch (error: unknown) {
|
|
16
|
+
if (error instanceof Error && 'code' in error && error.code === 'ENOENT') {
|
|
17
|
+
return {};
|
|
18
|
+
}
|
|
19
|
+
throw error;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Legacy support
|
|
24
|
+
try {
|
|
25
|
+
return await fs.readJson(HASHES_PATH) as HashStore;
|
|
26
|
+
} catch (error: unknown) {
|
|
27
|
+
if (error instanceof Error && 'code' in error && error.code === 'ENOENT') {
|
|
28
|
+
return {};
|
|
29
|
+
}
|
|
30
|
+
throw error;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export async function saveHashes(hashes: HashStore, customerIdn?: string): Promise<void> {
|
|
35
|
+
if (customerIdn) {
|
|
36
|
+
await fs.writeJson(hashesPath(customerIdn), hashes, { spaces: 2 });
|
|
37
|
+
} else {
|
|
38
|
+
// Legacy support
|
|
39
|
+
await fs.writeJson(HASHES_PATH, hashes, { spaces: 2 });
|
|
40
|
+
}
|
|
41
|
+
}
|
package/src/{sync.js → sync.ts}
RENAMED
|
@@ -1,99 +1,156 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import {
|
|
2
|
+
listProjects,
|
|
3
|
+
listAgents,
|
|
4
|
+
listFlowSkills,
|
|
5
|
+
updateSkill,
|
|
6
|
+
listFlowEvents,
|
|
7
|
+
listFlowStates,
|
|
8
|
+
getProjectMeta
|
|
9
|
+
} from './api.js';
|
|
10
|
+
import {
|
|
11
|
+
ensureState,
|
|
12
|
+
skillPath,
|
|
13
|
+
writeFileSafe,
|
|
14
|
+
readIfExists,
|
|
15
|
+
mapPath,
|
|
16
|
+
metadataPath,
|
|
17
|
+
flowsYamlPath
|
|
18
|
+
} from './fsutil.js';
|
|
3
19
|
import fs from 'fs-extra';
|
|
4
20
|
import { sha256, loadHashes, saveHashes } from './hash.js';
|
|
5
21
|
import yaml from 'js-yaml';
|
|
6
|
-
import
|
|
22
|
+
import pLimit from 'p-limit';
|
|
23
|
+
import type { AxiosInstance } from 'axios';
|
|
24
|
+
import type {
|
|
25
|
+
Agent,
|
|
26
|
+
ProjectData,
|
|
27
|
+
ProjectMap,
|
|
28
|
+
LegacyProjectMap,
|
|
29
|
+
HashStore,
|
|
30
|
+
FlowsYamlData,
|
|
31
|
+
FlowsYamlAgent,
|
|
32
|
+
FlowsYamlFlow,
|
|
33
|
+
FlowsYamlSkill,
|
|
34
|
+
FlowsYamlEvent,
|
|
35
|
+
FlowsYamlState,
|
|
36
|
+
CustomerConfig
|
|
37
|
+
} from './types.js';
|
|
7
38
|
|
|
8
|
-
|
|
9
|
-
|
|
39
|
+
// Concurrency limits for API operations
|
|
40
|
+
const concurrencyLimit = pLimit(5);
|
|
41
|
+
|
|
42
|
+
// Type guards for better type safety
|
|
43
|
+
function isProjectMap(x: unknown): x is ProjectMap {
|
|
44
|
+
return !!x && typeof x === 'object' && 'projects' in x;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function isLegacyProjectMap(x: unknown): x is LegacyProjectMap {
|
|
48
|
+
return !!x && typeof x === 'object' && 'agents' in x;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export async function pullSingleProject(
|
|
52
|
+
client: AxiosInstance,
|
|
53
|
+
customer: CustomerConfig,
|
|
54
|
+
projectId: string,
|
|
55
|
+
projectIdn: string,
|
|
56
|
+
verbose: boolean = false
|
|
57
|
+
): Promise<ProjectData> {
|
|
58
|
+
if (verbose) console.log(`🔍 Fetching agents for project ${projectId} (${projectIdn}) for customer ${customer.idn}...`);
|
|
10
59
|
const agents = await listAgents(client, projectId);
|
|
11
60
|
if (verbose) console.log(`📦 Found ${agents.length} agents`);
|
|
12
61
|
|
|
13
62
|
// Get and save project metadata
|
|
14
63
|
const projectMeta = await getProjectMeta(client, projectId);
|
|
15
|
-
await
|
|
64
|
+
await writeFileSafe(metadataPath(customer.idn, projectIdn), JSON.stringify(projectMeta, null, 2));
|
|
16
65
|
if (verbose) console.log(`✓ Saved metadata for ${projectIdn}`);
|
|
17
66
|
|
|
18
|
-
const projectMap = { projectId, projectIdn, agents: {} };
|
|
67
|
+
const projectMap: ProjectData = { projectId, projectIdn, agents: {} };
|
|
19
68
|
|
|
20
69
|
for (const agent of agents) {
|
|
21
70
|
const aKey = agent.idn;
|
|
22
71
|
projectMap.agents[aKey] = { id: agent.id, flows: {} };
|
|
23
72
|
|
|
24
73
|
for (const flow of agent.flows ?? []) {
|
|
25
|
-
projectMap.agents[aKey]
|
|
74
|
+
projectMap.agents[aKey]!.flows[flow.idn] = { id: flow.id, skills: {} };
|
|
26
75
|
|
|
27
76
|
const skills = await listFlowSkills(client, flow.id);
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
77
|
+
|
|
78
|
+
// Process skills concurrently with limited concurrency
|
|
79
|
+
await Promise.all(skills.map(skill => concurrencyLimit(async () => {
|
|
80
|
+
const file = skillPath(customer.idn, projectIdn, agent.idn, flow.idn, skill.idn, skill.runner_type);
|
|
81
|
+
await writeFileSafe(file, skill.prompt_script || '');
|
|
82
|
+
|
|
31
83
|
// Store complete skill metadata for push operations
|
|
32
|
-
projectMap.agents[aKey]
|
|
33
|
-
id:
|
|
34
|
-
title:
|
|
35
|
-
idn:
|
|
36
|
-
runner_type:
|
|
37
|
-
model:
|
|
38
|
-
parameters:
|
|
39
|
-
path:
|
|
84
|
+
projectMap.agents[aKey]!.flows[flow.idn]!.skills[skill.idn] = {
|
|
85
|
+
id: skill.id,
|
|
86
|
+
title: skill.title,
|
|
87
|
+
idn: skill.idn,
|
|
88
|
+
runner_type: skill.runner_type,
|
|
89
|
+
model: skill.model,
|
|
90
|
+
parameters: [...skill.parameters],
|
|
91
|
+
path: skill.path || undefined
|
|
40
92
|
};
|
|
41
93
|
console.log(`✓ Pulled ${file}`);
|
|
42
|
-
}
|
|
94
|
+
})));
|
|
43
95
|
}
|
|
44
96
|
}
|
|
45
97
|
|
|
46
98
|
// Generate flows.yaml for this project
|
|
47
|
-
if (verbose) console.log(`📄 Generating flows.yaml
|
|
48
|
-
await generateFlowsYaml(client,
|
|
99
|
+
if (verbose) console.log(`📄 Generating flows.yaml...`);
|
|
100
|
+
await generateFlowsYaml(client, customer, agents, verbose);
|
|
49
101
|
|
|
50
102
|
return projectMap;
|
|
51
103
|
}
|
|
52
104
|
|
|
53
|
-
export async function pullAll(
|
|
54
|
-
|
|
105
|
+
export async function pullAll(
|
|
106
|
+
client: AxiosInstance,
|
|
107
|
+
customer: CustomerConfig,
|
|
108
|
+
projectId: string | null = null,
|
|
109
|
+
verbose: boolean = false
|
|
110
|
+
): Promise<void> {
|
|
111
|
+
await ensureState(customer.idn);
|
|
55
112
|
|
|
56
113
|
if (projectId) {
|
|
57
114
|
// Single project mode
|
|
58
115
|
const projectMeta = await getProjectMeta(client, projectId);
|
|
59
|
-
const projectMap = await pullSingleProject(client, projectId, projectMeta.idn, verbose);
|
|
116
|
+
const projectMap = await pullSingleProject(client, customer, projectId, projectMeta.idn, verbose);
|
|
60
117
|
|
|
61
|
-
const idMap = { projects: { [projectMeta.idn]: projectMap } };
|
|
62
|
-
await fs.writeJson(
|
|
118
|
+
const idMap: ProjectMap = { projects: { [projectMeta.idn]: projectMap } };
|
|
119
|
+
await fs.writeJson(mapPath(customer.idn), idMap, { spaces: 2 });
|
|
63
120
|
|
|
64
121
|
// Generate hash tracking for this project
|
|
65
|
-
const hashes = {};
|
|
122
|
+
const hashes: HashStore = {};
|
|
66
123
|
for (const [agentIdn, agentObj] of Object.entries(projectMap.agents)) {
|
|
67
124
|
for (const [flowIdn, flowObj] of Object.entries(agentObj.flows)) {
|
|
68
125
|
for (const [skillIdn, skillMeta] of Object.entries(flowObj.skills)) {
|
|
69
|
-
const p = skillPath(projectMeta.idn, agentIdn, flowIdn, skillIdn, skillMeta.runner_type);
|
|
126
|
+
const p = skillPath(customer.idn, projectMeta.idn, agentIdn, flowIdn, skillIdn, skillMeta.runner_type);
|
|
70
127
|
const content = await fs.readFile(p, 'utf8');
|
|
71
128
|
hashes[p] = sha256(content);
|
|
72
129
|
}
|
|
73
130
|
}
|
|
74
131
|
}
|
|
75
|
-
await saveHashes(hashes);
|
|
132
|
+
await saveHashes(hashes, customer.idn);
|
|
76
133
|
return;
|
|
77
134
|
}
|
|
78
135
|
|
|
79
136
|
// Multi-project mode
|
|
80
|
-
if (verbose) console.log(`🔍 Fetching all projects...`);
|
|
137
|
+
if (verbose) console.log(`🔍 Fetching all projects for customer ${customer.idn}...`);
|
|
81
138
|
const projects = await listProjects(client);
|
|
82
139
|
if (verbose) console.log(`📦 Found ${projects.length} projects`);
|
|
83
140
|
|
|
84
|
-
const idMap = { projects: {} };
|
|
85
|
-
const allHashes = {};
|
|
141
|
+
const idMap: ProjectMap = { projects: {} };
|
|
142
|
+
const allHashes: HashStore = {};
|
|
86
143
|
|
|
87
144
|
for (const project of projects) {
|
|
88
145
|
if (verbose) console.log(`\n📁 Processing project: ${project.idn} (${project.title})`);
|
|
89
|
-
const projectMap = await pullSingleProject(client, project.id, project.idn, verbose);
|
|
146
|
+
const projectMap = await pullSingleProject(client, customer, project.id, project.idn, verbose);
|
|
90
147
|
idMap.projects[project.idn] = projectMap;
|
|
91
148
|
|
|
92
149
|
// Collect hashes for this project
|
|
93
150
|
for (const [agentIdn, agentObj] of Object.entries(projectMap.agents)) {
|
|
94
151
|
for (const [flowIdn, flowObj] of Object.entries(agentObj.flows)) {
|
|
95
152
|
for (const [skillIdn, skillMeta] of Object.entries(flowObj.skills)) {
|
|
96
|
-
const p = skillPath(project.idn, agentIdn, flowIdn, skillIdn, skillMeta.runner_type);
|
|
153
|
+
const p = skillPath(customer.idn, project.idn, agentIdn, flowIdn, skillIdn, skillMeta.runner_type);
|
|
97
154
|
const content = await fs.readFile(p, 'utf8');
|
|
98
155
|
allHashes[p] = sha256(content);
|
|
99
156
|
}
|
|
@@ -101,28 +158,32 @@ export async function pullAll(client, projectId = null, verbose = false) {
|
|
|
101
158
|
}
|
|
102
159
|
}
|
|
103
160
|
|
|
104
|
-
await fs.writeJson(
|
|
105
|
-
await saveHashes(allHashes);
|
|
161
|
+
await fs.writeJson(mapPath(customer.idn), idMap, { spaces: 2 });
|
|
162
|
+
await saveHashes(allHashes, customer.idn);
|
|
106
163
|
}
|
|
107
164
|
|
|
108
|
-
export async function pushChanged(client, verbose = false) {
|
|
109
|
-
await ensureState();
|
|
110
|
-
if (!(await fs.pathExists(
|
|
111
|
-
throw new Error(
|
|
165
|
+
export async function pushChanged(client: AxiosInstance, customer: CustomerConfig, verbose: boolean = false): Promise<void> {
|
|
166
|
+
await ensureState(customer.idn);
|
|
167
|
+
if (!(await fs.pathExists(mapPath(customer.idn)))) {
|
|
168
|
+
throw new Error(`Missing .newo/${customer.idn}/map.json. Run \`newo pull --customer ${customer.idn}\` first.`);
|
|
112
169
|
}
|
|
113
170
|
|
|
114
|
-
if (verbose) console.log(
|
|
115
|
-
const
|
|
171
|
+
if (verbose) console.log(`📋 Loading project mapping for customer ${customer.idn}...`);
|
|
172
|
+
const idMapData = await fs.readJson(mapPath(customer.idn)) as unknown;
|
|
116
173
|
if (verbose) console.log('🔍 Loading file hashes...');
|
|
117
|
-
const oldHashes = await loadHashes();
|
|
118
|
-
const newHashes = { ...oldHashes };
|
|
174
|
+
const oldHashes = await loadHashes(customer.idn);
|
|
175
|
+
const newHashes: HashStore = { ...oldHashes };
|
|
119
176
|
|
|
120
177
|
if (verbose) console.log('🔄 Scanning for changes...');
|
|
121
178
|
let pushed = 0;
|
|
122
179
|
let scanned = 0;
|
|
123
180
|
|
|
124
|
-
// Handle both old single-project format and new multi-project format
|
|
125
|
-
const projects =
|
|
181
|
+
// Handle both old single-project format and new multi-project format with type guards
|
|
182
|
+
const projects = isProjectMap(idMapData) && idMapData.projects
|
|
183
|
+
? idMapData.projects
|
|
184
|
+
: isLegacyProjectMap(idMapData)
|
|
185
|
+
? { '': idMapData as ProjectData }
|
|
186
|
+
: (() => { throw new Error('Invalid project map format'); })();
|
|
126
187
|
|
|
127
188
|
for (const [projectIdn, projectData] of Object.entries(projects)) {
|
|
128
189
|
if (verbose && projectIdn) console.log(`📁 Scanning project: ${projectIdn}`);
|
|
@@ -133,8 +194,8 @@ export async function pushChanged(client, verbose = false) {
|
|
|
133
194
|
if (verbose) console.log(` 📁 Scanning flow: ${flowIdn}`);
|
|
134
195
|
for (const [skillIdn, skillMeta] of Object.entries(flowObj.skills)) {
|
|
135
196
|
const p = projectIdn ?
|
|
136
|
-
skillPath(projectIdn, agentIdn, flowIdn, skillIdn, skillMeta.runner_type) :
|
|
137
|
-
skillPath('', agentIdn, flowIdn, skillIdn, skillMeta.runner_type);
|
|
197
|
+
skillPath(customer.idn, projectIdn, agentIdn, flowIdn, skillIdn, skillMeta.runner_type) :
|
|
198
|
+
skillPath(customer.idn, '', agentIdn, flowIdn, skillIdn, skillMeta.runner_type);
|
|
138
199
|
scanned++;
|
|
139
200
|
if (verbose) console.log(` 📄 Checking: ${p}`);
|
|
140
201
|
|
|
@@ -164,7 +225,7 @@ export async function pushChanged(client, verbose = false) {
|
|
|
164
225
|
runner_type: skillMeta.runner_type,
|
|
165
226
|
model: skillMeta.model,
|
|
166
227
|
parameters: skillMeta.parameters,
|
|
167
|
-
path: skillMeta.path
|
|
228
|
+
path: skillMeta.path || undefined
|
|
168
229
|
};
|
|
169
230
|
|
|
170
231
|
if (verbose) {
|
|
@@ -189,24 +250,28 @@ export async function pushChanged(client, verbose = false) {
|
|
|
189
250
|
}
|
|
190
251
|
|
|
191
252
|
if (verbose) console.log(`🔄 Scanned ${scanned} files, found ${pushed} changes`);
|
|
192
|
-
await saveHashes(newHashes);
|
|
253
|
+
await saveHashes(newHashes, customer.idn);
|
|
193
254
|
console.log(pushed ? `✅ Push complete. ${pushed} file(s) updated.` : '✅ Nothing to push.');
|
|
194
255
|
}
|
|
195
256
|
|
|
196
|
-
export async function status(verbose = false) {
|
|
197
|
-
await ensureState();
|
|
198
|
-
if (!(await fs.pathExists(
|
|
199
|
-
console.log(
|
|
257
|
+
export async function status(customer: CustomerConfig, verbose: boolean = false): Promise<void> {
|
|
258
|
+
await ensureState(customer.idn);
|
|
259
|
+
if (!(await fs.pathExists(mapPath(customer.idn)))) {
|
|
260
|
+
console.log(`No map for customer ${customer.idn}. Run \`newo pull --customer ${customer.idn}\` first.`);
|
|
200
261
|
return;
|
|
201
262
|
}
|
|
202
263
|
|
|
203
|
-
if (verbose) console.log(
|
|
204
|
-
const
|
|
205
|
-
const hashes = await loadHashes();
|
|
264
|
+
if (verbose) console.log(`📋 Loading project mapping and hashes for customer ${customer.idn}...`);
|
|
265
|
+
const idMapData = await fs.readJson(mapPath(customer.idn)) as unknown;
|
|
266
|
+
const hashes = await loadHashes(customer.idn);
|
|
206
267
|
let dirty = 0;
|
|
207
268
|
|
|
208
|
-
// Handle both old single-project format and new multi-project format
|
|
209
|
-
const projects =
|
|
269
|
+
// Handle both old single-project format and new multi-project format with type guards
|
|
270
|
+
const projects = isProjectMap(idMapData) && idMapData.projects
|
|
271
|
+
? idMapData.projects
|
|
272
|
+
: isLegacyProjectMap(idMapData)
|
|
273
|
+
? { '': idMapData as ProjectData }
|
|
274
|
+
: (() => { throw new Error('Invalid project map format'); })();
|
|
210
275
|
|
|
211
276
|
for (const [projectIdn, projectData] of Object.entries(projects)) {
|
|
212
277
|
if (verbose && projectIdn) console.log(`📁 Checking project: ${projectIdn}`);
|
|
@@ -217,8 +282,8 @@ export async function status(verbose = false) {
|
|
|
217
282
|
if (verbose) console.log(` 📁 Checking flow: ${flowIdn}`);
|
|
218
283
|
for (const [skillIdn, skillMeta] of Object.entries(flowObj.skills)) {
|
|
219
284
|
const p = projectIdn ?
|
|
220
|
-
skillPath(projectIdn, agentIdn, flowIdn, skillIdn, skillMeta.runner_type) :
|
|
221
|
-
skillPath('', agentIdn, flowIdn, skillIdn, skillMeta.runner_type);
|
|
285
|
+
skillPath(customer.idn, projectIdn, agentIdn, flowIdn, skillIdn, skillMeta.runner_type) :
|
|
286
|
+
skillPath(customer.idn, '', agentIdn, flowIdn, skillIdn, skillMeta.runner_type);
|
|
222
287
|
const exists = await fs.pathExists(p);
|
|
223
288
|
if (!exists) {
|
|
224
289
|
console.log(`D ${p}`);
|
|
@@ -248,23 +313,49 @@ export async function status(verbose = false) {
|
|
|
248
313
|
console.log(dirty ? `${dirty} changed file(s).` : 'Clean.');
|
|
249
314
|
}
|
|
250
315
|
|
|
251
|
-
async function generateFlowsYaml(
|
|
252
|
-
|
|
316
|
+
async function generateFlowsYaml(
|
|
317
|
+
client: AxiosInstance,
|
|
318
|
+
customer: CustomerConfig,
|
|
319
|
+
agents: Agent[],
|
|
320
|
+
verbose: boolean = false
|
|
321
|
+
): Promise<void> {
|
|
322
|
+
const flowsData: FlowsYamlData = { flows: [] };
|
|
323
|
+
|
|
324
|
+
// Calculate total flows for progress tracking
|
|
325
|
+
const totalFlows = agents.reduce((sum, agent) => sum + (agent.flows?.length || 0), 0);
|
|
326
|
+
let processedFlows = 0;
|
|
327
|
+
|
|
328
|
+
if (!verbose && totalFlows > 0) {
|
|
329
|
+
console.log(`📄 Generating flows.yaml (${totalFlows} flows)...`);
|
|
330
|
+
}
|
|
253
331
|
|
|
254
332
|
for (const agent of agents) {
|
|
255
333
|
if (verbose) console.log(` 📁 Processing agent: ${agent.idn}`);
|
|
256
334
|
|
|
257
|
-
const agentFlows = [];
|
|
335
|
+
const agentFlows: FlowsYamlFlow[] = [];
|
|
258
336
|
|
|
259
337
|
for (const flow of agent.flows ?? []) {
|
|
260
|
-
|
|
338
|
+
processedFlows++;
|
|
339
|
+
|
|
340
|
+
if (verbose) {
|
|
341
|
+
console.log(` 📄 Processing flow: ${flow.idn}`);
|
|
342
|
+
} else {
|
|
343
|
+
// Simple progress indicator without verbose mode
|
|
344
|
+
const percent = Math.round((processedFlows / totalFlows) * 100);
|
|
345
|
+
const progressBar = '█'.repeat(Math.floor(percent / 5)) + '░'.repeat(20 - Math.floor(percent / 5));
|
|
346
|
+
const progressText = ` [${progressBar}] ${percent}% (${processedFlows}/${totalFlows}) ${flow.idn}`;
|
|
347
|
+
|
|
348
|
+
// Pad the line to clear any leftover text from longer previous lines
|
|
349
|
+
const padding = ' '.repeat(Math.max(0, 80 - progressText.length));
|
|
350
|
+
process.stdout.write(`\r${progressText}${padding}`);
|
|
351
|
+
}
|
|
261
352
|
|
|
262
353
|
// Get skills for this flow
|
|
263
354
|
const skills = await listFlowSkills(client, flow.id);
|
|
264
|
-
const skillsData = skills.map(skill => ({
|
|
355
|
+
const skillsData: FlowsYamlSkill[] = skills.map(skill => ({
|
|
265
356
|
idn: skill.idn,
|
|
266
357
|
title: skill.title || "",
|
|
267
|
-
prompt_script: `flows/${flow.idn}/${skill.idn}.${skill.runner_type === 'nsl' ? 'jinja' : '
|
|
358
|
+
prompt_script: `flows/${flow.idn}/${skill.idn}.${skill.runner_type === 'nsl' ? 'jinja' : 'guidance'}`,
|
|
268
359
|
runner_type: `!enum "RunnerType.${skill.runner_type}"`,
|
|
269
360
|
model: {
|
|
270
361
|
model_idn: skill.model.model_idn,
|
|
@@ -277,17 +368,17 @@ async function generateFlowsYaml(client, agents, projectIdn, verbose = false) {
|
|
|
277
368
|
}));
|
|
278
369
|
|
|
279
370
|
// Get events for this flow
|
|
280
|
-
let eventsData = [];
|
|
371
|
+
let eventsData: FlowsYamlEvent[] = [];
|
|
281
372
|
try {
|
|
282
373
|
const events = await listFlowEvents(client, flow.id);
|
|
283
374
|
eventsData = events.map(event => ({
|
|
284
375
|
title: event.description,
|
|
285
376
|
idn: event.idn,
|
|
286
377
|
skill_selector: `!enum "SkillSelector.${event.skill_selector}"`,
|
|
287
|
-
skill_idn: event.skill_idn,
|
|
288
|
-
state_idn: event.state_idn,
|
|
289
|
-
integration_idn: event.integration_idn,
|
|
290
|
-
connector_idn: event.connector_idn,
|
|
378
|
+
skill_idn: event.skill_idn || undefined,
|
|
379
|
+
state_idn: event.state_idn || undefined,
|
|
380
|
+
integration_idn: event.integration_idn || undefined,
|
|
381
|
+
connector_idn: event.connector_idn || undefined,
|
|
291
382
|
interrupt_mode: `!enum "InterruptMode.${event.interrupt_mode}"`
|
|
292
383
|
}));
|
|
293
384
|
if (verbose) console.log(` 📋 Found ${events.length} events`);
|
|
@@ -296,13 +387,13 @@ async function generateFlowsYaml(client, agents, projectIdn, verbose = false) {
|
|
|
296
387
|
}
|
|
297
388
|
|
|
298
389
|
// Get state fields for this flow
|
|
299
|
-
let stateFieldsData = [];
|
|
390
|
+
let stateFieldsData: FlowsYamlState[] = [];
|
|
300
391
|
try {
|
|
301
392
|
const states = await listFlowStates(client, flow.id);
|
|
302
393
|
stateFieldsData = states.map(state => ({
|
|
303
394
|
title: state.title,
|
|
304
395
|
idn: state.idn,
|
|
305
|
-
default_value: state.default_value,
|
|
396
|
+
default_value: state.default_value || undefined,
|
|
306
397
|
scope: `!enum "StateFieldScope.${state.scope}"`
|
|
307
398
|
}));
|
|
308
399
|
if (verbose) console.log(` 📊 Found ${states.length} state fields`);
|
|
@@ -323,11 +414,18 @@ async function generateFlowsYaml(client, agents, projectIdn, verbose = false) {
|
|
|
323
414
|
});
|
|
324
415
|
}
|
|
325
416
|
|
|
326
|
-
|
|
417
|
+
const agentData: FlowsYamlAgent = {
|
|
327
418
|
agent_idn: agent.idn,
|
|
328
|
-
agent_description: agent.description,
|
|
419
|
+
agent_description: agent.description || undefined,
|
|
329
420
|
agent_flows: agentFlows
|
|
330
|
-
}
|
|
421
|
+
};
|
|
422
|
+
|
|
423
|
+
flowsData.flows.push(agentData);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// Clear progress bar and move to new line
|
|
427
|
+
if (!verbose && totalFlows > 0) {
|
|
428
|
+
process.stdout.write('\n');
|
|
331
429
|
}
|
|
332
430
|
|
|
333
431
|
// Convert to YAML and write to file with custom enum handling
|
|
@@ -343,7 +441,7 @@ async function generateFlowsYaml(client, agents, projectIdn, verbose = false) {
|
|
|
343
441
|
// Post-process to fix enum formatting
|
|
344
442
|
yamlContent = yamlContent.replace(/"(!enum "[^"]+")"/g, '$1');
|
|
345
443
|
|
|
346
|
-
const yamlPath =
|
|
347
|
-
await
|
|
348
|
-
console.log(`✓ Generated flows.yaml
|
|
444
|
+
const yamlPath = flowsYamlPath(customer.idn);
|
|
445
|
+
await writeFileSafe(yamlPath, yamlContent);
|
|
446
|
+
console.log(`✓ Generated flows.yaml`);
|
|
349
447
|
}
|