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/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
CHANGED
|
@@ -2,21 +2,45 @@ import fs from 'fs-extra';
|
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import type { RunnerType } from './types.js';
|
|
4
4
|
|
|
5
|
-
export const
|
|
5
|
+
export const NEWO_CUSTOMERS_DIR = path.posix.join(process.cwd(), 'newo_customers');
|
|
6
6
|
export const STATE_DIR = path.join(process.cwd(), '.newo');
|
|
7
|
-
export const MAP_PATH = path.join(STATE_DIR, 'map.json');
|
|
8
|
-
export const HASHES_PATH = path.join(STATE_DIR, 'hashes.json');
|
|
9
7
|
|
|
10
|
-
export
|
|
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> {
|
|
11
29
|
await fs.ensureDir(STATE_DIR);
|
|
12
|
-
await fs.ensureDir(
|
|
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);
|
|
13
36
|
}
|
|
14
37
|
|
|
15
|
-
export function
|
|
16
|
-
return path.join(
|
|
38
|
+
export function flowsYamlPath(customerIdn: string): string {
|
|
39
|
+
return path.posix.join(customerProjectsDir(customerIdn), 'flows.yaml');
|
|
17
40
|
}
|
|
18
41
|
|
|
19
42
|
export function skillPath(
|
|
43
|
+
customerIdn: string,
|
|
20
44
|
projectIdn: string,
|
|
21
45
|
agentIdn: string,
|
|
22
46
|
flowIdn: string,
|
|
@@ -24,18 +48,26 @@ export function skillPath(
|
|
|
24
48
|
runnerType: RunnerType = 'guidance'
|
|
25
49
|
): string {
|
|
26
50
|
const extension = runnerType === 'nsl' ? '.jinja' : '.guidance';
|
|
27
|
-
return path.join(
|
|
51
|
+
return path.posix.join(customerProjectsDir(customerIdn), projectIdn, agentIdn, flowIdn, `${skillIdn}${extension}`);
|
|
28
52
|
}
|
|
29
53
|
|
|
30
|
-
export function metadataPath(projectIdn: string): string {
|
|
31
|
-
return path.join(
|
|
54
|
+
export function metadataPath(customerIdn: string, projectIdn: string): string {
|
|
55
|
+
return path.posix.join(customerProjectsDir(customerIdn), projectIdn, 'metadata.json');
|
|
32
56
|
}
|
|
33
57
|
|
|
34
|
-
|
|
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> {
|
|
35
64
|
await fs.ensureDir(path.dirname(filepath));
|
|
36
65
|
await fs.writeFile(filepath, content, 'utf8');
|
|
37
66
|
}
|
|
38
67
|
|
|
68
|
+
// Deprecated: use writeFileSafe instead
|
|
69
|
+
export const writeFileAtomic = writeFileSafe;
|
|
70
|
+
|
|
39
71
|
export async function readIfExists(filepath: string): Promise<string | null> {
|
|
40
72
|
return (await fs.pathExists(filepath)) ? fs.readFile(filepath, 'utf8') : null;
|
|
41
73
|
}
|
package/src/hash.ts
CHANGED
|
@@ -1,20 +1,41 @@
|
|
|
1
1
|
import crypto from 'crypto';
|
|
2
2
|
import fs from 'fs-extra';
|
|
3
|
-
import { ensureState, HASHES_PATH } from './fsutil.js';
|
|
3
|
+
import { ensureState, hashesPath, HASHES_PATH } from './fsutil.js';
|
|
4
4
|
import type { HashStore } from './types.js';
|
|
5
5
|
|
|
6
6
|
export function sha256(str: string): string {
|
|
7
7
|
return crypto.createHash('sha256').update(str, 'utf8').digest('hex');
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
-
export async function loadHashes(): Promise<HashStore> {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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;
|
|
14
31
|
}
|
|
15
|
-
return {};
|
|
16
32
|
}
|
|
17
33
|
|
|
18
|
-
export async function saveHashes(hashes: HashStore): Promise<void> {
|
|
19
|
-
|
|
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
|
+
}
|
|
20
41
|
}
|
package/src/sync.ts
CHANGED
|
@@ -10,16 +10,16 @@ import {
|
|
|
10
10
|
import {
|
|
11
11
|
ensureState,
|
|
12
12
|
skillPath,
|
|
13
|
-
|
|
13
|
+
writeFileSafe,
|
|
14
14
|
readIfExists,
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
15
|
+
mapPath,
|
|
16
|
+
metadataPath,
|
|
17
|
+
flowsYamlPath
|
|
18
18
|
} from './fsutil.js';
|
|
19
19
|
import fs from 'fs-extra';
|
|
20
20
|
import { sha256, loadHashes, saveHashes } from './hash.js';
|
|
21
21
|
import yaml from 'js-yaml';
|
|
22
|
-
import
|
|
22
|
+
import pLimit from 'p-limit';
|
|
23
23
|
import type { AxiosInstance } from 'axios';
|
|
24
24
|
import type {
|
|
25
25
|
Agent,
|
|
@@ -32,22 +32,36 @@ import type {
|
|
|
32
32
|
FlowsYamlFlow,
|
|
33
33
|
FlowsYamlSkill,
|
|
34
34
|
FlowsYamlEvent,
|
|
35
|
-
FlowsYamlState
|
|
35
|
+
FlowsYamlState,
|
|
36
|
+
CustomerConfig
|
|
36
37
|
} from './types.js';
|
|
37
38
|
|
|
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
|
+
|
|
38
51
|
export async function pullSingleProject(
|
|
39
52
|
client: AxiosInstance,
|
|
53
|
+
customer: CustomerConfig,
|
|
40
54
|
projectId: string,
|
|
41
55
|
projectIdn: string,
|
|
42
56
|
verbose: boolean = false
|
|
43
57
|
): Promise<ProjectData> {
|
|
44
|
-
if (verbose) console.log(`🔍 Fetching agents for project ${projectId} (${projectIdn})...`);
|
|
58
|
+
if (verbose) console.log(`🔍 Fetching agents for project ${projectId} (${projectIdn}) for customer ${customer.idn}...`);
|
|
45
59
|
const agents = await listAgents(client, projectId);
|
|
46
60
|
if (verbose) console.log(`📦 Found ${agents.length} agents`);
|
|
47
61
|
|
|
48
62
|
// Get and save project metadata
|
|
49
63
|
const projectMeta = await getProjectMeta(client, projectId);
|
|
50
|
-
await
|
|
64
|
+
await writeFileSafe(metadataPath(customer.idn, projectIdn), JSON.stringify(projectMeta, null, 2));
|
|
51
65
|
if (verbose) console.log(`✓ Saved metadata for ${projectIdn}`);
|
|
52
66
|
|
|
53
67
|
const projectMap: ProjectData = { projectId, projectIdn, agents: {} };
|
|
@@ -60,9 +74,11 @@ export async function pullSingleProject(
|
|
|
60
74
|
projectMap.agents[aKey]!.flows[flow.idn] = { id: flow.id, skills: {} };
|
|
61
75
|
|
|
62
76
|
const skills = await listFlowSkills(client, flow.id);
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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 || '');
|
|
66
82
|
|
|
67
83
|
// Store complete skill metadata for push operations
|
|
68
84
|
projectMap.agents[aKey]!.flows[flow.idn]!.skills[skill.idn] = {
|
|
@@ -71,53 +87,54 @@ export async function pullSingleProject(
|
|
|
71
87
|
idn: skill.idn,
|
|
72
88
|
runner_type: skill.runner_type,
|
|
73
89
|
model: skill.model,
|
|
74
|
-
parameters: skill.parameters,
|
|
90
|
+
parameters: [...skill.parameters],
|
|
75
91
|
path: skill.path || undefined
|
|
76
92
|
};
|
|
77
93
|
console.log(`✓ Pulled ${file}`);
|
|
78
|
-
}
|
|
94
|
+
})));
|
|
79
95
|
}
|
|
80
96
|
}
|
|
81
97
|
|
|
82
98
|
// Generate flows.yaml for this project
|
|
83
|
-
if (verbose) console.log(`📄 Generating flows.yaml
|
|
84
|
-
await generateFlowsYaml(client,
|
|
99
|
+
if (verbose) console.log(`📄 Generating flows.yaml...`);
|
|
100
|
+
await generateFlowsYaml(client, customer, agents, verbose);
|
|
85
101
|
|
|
86
102
|
return projectMap;
|
|
87
103
|
}
|
|
88
104
|
|
|
89
105
|
export async function pullAll(
|
|
90
106
|
client: AxiosInstance,
|
|
107
|
+
customer: CustomerConfig,
|
|
91
108
|
projectId: string | null = null,
|
|
92
109
|
verbose: boolean = false
|
|
93
110
|
): Promise<void> {
|
|
94
|
-
await ensureState();
|
|
111
|
+
await ensureState(customer.idn);
|
|
95
112
|
|
|
96
113
|
if (projectId) {
|
|
97
114
|
// Single project mode
|
|
98
115
|
const projectMeta = await getProjectMeta(client, projectId);
|
|
99
|
-
const projectMap = await pullSingleProject(client, projectId, projectMeta.idn, verbose);
|
|
116
|
+
const projectMap = await pullSingleProject(client, customer, projectId, projectMeta.idn, verbose);
|
|
100
117
|
|
|
101
118
|
const idMap: ProjectMap = { projects: { [projectMeta.idn]: projectMap } };
|
|
102
|
-
await fs.writeJson(
|
|
119
|
+
await fs.writeJson(mapPath(customer.idn), idMap, { spaces: 2 });
|
|
103
120
|
|
|
104
121
|
// Generate hash tracking for this project
|
|
105
122
|
const hashes: HashStore = {};
|
|
106
123
|
for (const [agentIdn, agentObj] of Object.entries(projectMap.agents)) {
|
|
107
124
|
for (const [flowIdn, flowObj] of Object.entries(agentObj.flows)) {
|
|
108
125
|
for (const [skillIdn, skillMeta] of Object.entries(flowObj.skills)) {
|
|
109
|
-
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);
|
|
110
127
|
const content = await fs.readFile(p, 'utf8');
|
|
111
128
|
hashes[p] = sha256(content);
|
|
112
129
|
}
|
|
113
130
|
}
|
|
114
131
|
}
|
|
115
|
-
await saveHashes(hashes);
|
|
132
|
+
await saveHashes(hashes, customer.idn);
|
|
116
133
|
return;
|
|
117
134
|
}
|
|
118
135
|
|
|
119
136
|
// Multi-project mode
|
|
120
|
-
if (verbose) console.log(
|
|
137
|
+
if (verbose) console.log(`🔍 Fetching all projects for customer ${customer.idn}...`);
|
|
121
138
|
const projects = await listProjects(client);
|
|
122
139
|
if (verbose) console.log(`📦 Found ${projects.length} projects`);
|
|
123
140
|
|
|
@@ -126,14 +143,14 @@ export async function pullAll(
|
|
|
126
143
|
|
|
127
144
|
for (const project of projects) {
|
|
128
145
|
if (verbose) console.log(`\n📁 Processing project: ${project.idn} (${project.title})`);
|
|
129
|
-
const projectMap = await pullSingleProject(client, project.id, project.idn, verbose);
|
|
146
|
+
const projectMap = await pullSingleProject(client, customer, project.id, project.idn, verbose);
|
|
130
147
|
idMap.projects[project.idn] = projectMap;
|
|
131
148
|
|
|
132
149
|
// Collect hashes for this project
|
|
133
150
|
for (const [agentIdn, agentObj] of Object.entries(projectMap.agents)) {
|
|
134
151
|
for (const [flowIdn, flowObj] of Object.entries(agentObj.flows)) {
|
|
135
152
|
for (const [skillIdn, skillMeta] of Object.entries(flowObj.skills)) {
|
|
136
|
-
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);
|
|
137
154
|
const content = await fs.readFile(p, 'utf8');
|
|
138
155
|
allHashes[p] = sha256(content);
|
|
139
156
|
}
|
|
@@ -141,28 +158,32 @@ export async function pullAll(
|
|
|
141
158
|
}
|
|
142
159
|
}
|
|
143
160
|
|
|
144
|
-
await fs.writeJson(
|
|
145
|
-
await saveHashes(allHashes);
|
|
161
|
+
await fs.writeJson(mapPath(customer.idn), idMap, { spaces: 2 });
|
|
162
|
+
await saveHashes(allHashes, customer.idn);
|
|
146
163
|
}
|
|
147
164
|
|
|
148
|
-
export async function pushChanged(client: AxiosInstance, verbose: boolean = false): Promise<void> {
|
|
149
|
-
await ensureState();
|
|
150
|
-
if (!(await fs.pathExists(
|
|
151
|
-
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.`);
|
|
152
169
|
}
|
|
153
170
|
|
|
154
|
-
if (verbose) console.log(
|
|
155
|
-
const
|
|
171
|
+
if (verbose) console.log(`📋 Loading project mapping for customer ${customer.idn}...`);
|
|
172
|
+
const idMapData = await fs.readJson(mapPath(customer.idn)) as unknown;
|
|
156
173
|
if (verbose) console.log('🔍 Loading file hashes...');
|
|
157
|
-
const oldHashes = await loadHashes();
|
|
174
|
+
const oldHashes = await loadHashes(customer.idn);
|
|
158
175
|
const newHashes: HashStore = { ...oldHashes };
|
|
159
176
|
|
|
160
177
|
if (verbose) console.log('🔄 Scanning for changes...');
|
|
161
178
|
let pushed = 0;
|
|
162
179
|
let scanned = 0;
|
|
163
180
|
|
|
164
|
-
// Handle both old single-project format and new multi-project format
|
|
165
|
-
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'); })();
|
|
166
187
|
|
|
167
188
|
for (const [projectIdn, projectData] of Object.entries(projects)) {
|
|
168
189
|
if (verbose && projectIdn) console.log(`📁 Scanning project: ${projectIdn}`);
|
|
@@ -173,8 +194,8 @@ export async function pushChanged(client: AxiosInstance, verbose: boolean = fals
|
|
|
173
194
|
if (verbose) console.log(` 📁 Scanning flow: ${flowIdn}`);
|
|
174
195
|
for (const [skillIdn, skillMeta] of Object.entries(flowObj.skills)) {
|
|
175
196
|
const p = projectIdn ?
|
|
176
|
-
skillPath(projectIdn, agentIdn, flowIdn, skillIdn, skillMeta.runner_type) :
|
|
177
|
-
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);
|
|
178
199
|
scanned++;
|
|
179
200
|
if (verbose) console.log(` 📄 Checking: ${p}`);
|
|
180
201
|
|
|
@@ -229,24 +250,28 @@ export async function pushChanged(client: AxiosInstance, verbose: boolean = fals
|
|
|
229
250
|
}
|
|
230
251
|
|
|
231
252
|
if (verbose) console.log(`🔄 Scanned ${scanned} files, found ${pushed} changes`);
|
|
232
|
-
await saveHashes(newHashes);
|
|
253
|
+
await saveHashes(newHashes, customer.idn);
|
|
233
254
|
console.log(pushed ? `✅ Push complete. ${pushed} file(s) updated.` : '✅ Nothing to push.');
|
|
234
255
|
}
|
|
235
256
|
|
|
236
|
-
export async function status(verbose: boolean = false): Promise<void> {
|
|
237
|
-
await ensureState();
|
|
238
|
-
if (!(await fs.pathExists(
|
|
239
|
-
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.`);
|
|
240
261
|
return;
|
|
241
262
|
}
|
|
242
263
|
|
|
243
|
-
if (verbose) console.log(
|
|
244
|
-
const
|
|
245
|
-
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);
|
|
246
267
|
let dirty = 0;
|
|
247
268
|
|
|
248
|
-
// Handle both old single-project format and new multi-project format
|
|
249
|
-
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'); })();
|
|
250
275
|
|
|
251
276
|
for (const [projectIdn, projectData] of Object.entries(projects)) {
|
|
252
277
|
if (verbose && projectIdn) console.log(`📁 Checking project: ${projectIdn}`);
|
|
@@ -257,8 +282,8 @@ export async function status(verbose: boolean = false): Promise<void> {
|
|
|
257
282
|
if (verbose) console.log(` 📁 Checking flow: ${flowIdn}`);
|
|
258
283
|
for (const [skillIdn, skillMeta] of Object.entries(flowObj.skills)) {
|
|
259
284
|
const p = projectIdn ?
|
|
260
|
-
skillPath(projectIdn, agentIdn, flowIdn, skillIdn, skillMeta.runner_type) :
|
|
261
|
-
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);
|
|
262
287
|
const exists = await fs.pathExists(p);
|
|
263
288
|
if (!exists) {
|
|
264
289
|
console.log(`D ${p}`);
|
|
@@ -290,26 +315,47 @@ export async function status(verbose: boolean = false): Promise<void> {
|
|
|
290
315
|
|
|
291
316
|
async function generateFlowsYaml(
|
|
292
317
|
client: AxiosInstance,
|
|
318
|
+
customer: CustomerConfig,
|
|
293
319
|
agents: Agent[],
|
|
294
|
-
projectIdn: string,
|
|
295
320
|
verbose: boolean = false
|
|
296
321
|
): Promise<void> {
|
|
297
322
|
const flowsData: FlowsYamlData = { flows: [] };
|
|
298
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
|
+
}
|
|
331
|
+
|
|
299
332
|
for (const agent of agents) {
|
|
300
333
|
if (verbose) console.log(` 📁 Processing agent: ${agent.idn}`);
|
|
301
334
|
|
|
302
335
|
const agentFlows: FlowsYamlFlow[] = [];
|
|
303
336
|
|
|
304
337
|
for (const flow of agent.flows ?? []) {
|
|
305
|
-
|
|
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
|
+
}
|
|
306
352
|
|
|
307
353
|
// Get skills for this flow
|
|
308
354
|
const skills = await listFlowSkills(client, flow.id);
|
|
309
355
|
const skillsData: FlowsYamlSkill[] = skills.map(skill => ({
|
|
310
356
|
idn: skill.idn,
|
|
311
357
|
title: skill.title || "",
|
|
312
|
-
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'}`,
|
|
313
359
|
runner_type: `!enum "RunnerType.${skill.runner_type}"`,
|
|
314
360
|
model: {
|
|
315
361
|
model_idn: skill.model.model_idn,
|
|
@@ -376,6 +422,11 @@ async function generateFlowsYaml(
|
|
|
376
422
|
|
|
377
423
|
flowsData.flows.push(agentData);
|
|
378
424
|
}
|
|
425
|
+
|
|
426
|
+
// Clear progress bar and move to new line
|
|
427
|
+
if (!verbose && totalFlows > 0) {
|
|
428
|
+
process.stdout.write('\n');
|
|
429
|
+
}
|
|
379
430
|
|
|
380
431
|
// Convert to YAML and write to file with custom enum handling
|
|
381
432
|
let yamlContent = yaml.dump(flowsData, {
|
|
@@ -390,7 +441,7 @@ async function generateFlowsYaml(
|
|
|
390
441
|
// Post-process to fix enum formatting
|
|
391
442
|
yamlContent = yamlContent.replace(/"(!enum "[^"]+")"/g, '$1');
|
|
392
443
|
|
|
393
|
-
const yamlPath =
|
|
394
|
-
await
|
|
395
|
-
console.log(`✓ Generated flows.yaml
|
|
444
|
+
const yamlPath = flowsYamlPath(customer.idn);
|
|
445
|
+
await writeFileSafe(yamlPath, yamlContent);
|
|
446
|
+
console.log(`✓ Generated flows.yaml`);
|
|
396
447
|
}
|