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/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
+ }
@@ -1,99 +1,156 @@
1
- import { listProjects, listAgents, listFlowSkills, updateSkill, listFlowEvents, listFlowStates, getProjectMeta } from './api.js';
2
- import { ensureState, skillPath, writeFileAtomic, readIfExists, MAP_PATH, projectDir, metadataPath } from './fsutil.js';
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 path from 'path';
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
- export async function pullSingleProject(client, projectId, projectIdn, verbose = false) {
9
- if (verbose) console.log(`🔍 Fetching agents for project ${projectId} (${projectIdn})...`);
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 writeFileAtomic(metadataPath(projectIdn), JSON.stringify(projectMeta, null, 2));
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].flows[flow.idn] = { id: flow.id, skills: {} };
74
+ projectMap.agents[aKey]!.flows[flow.idn] = { id: flow.id, skills: {} };
26
75
 
27
76
  const skills = await listFlowSkills(client, flow.id);
28
- for (const s of skills) {
29
- const file = skillPath(projectIdn, agent.idn, flow.idn, s.idn, s.runner_type);
30
- await writeFileAtomic(file, s.prompt_script || '');
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].flows[flow.idn].skills[s.idn] = {
33
- id: s.id,
34
- title: s.title,
35
- idn: s.idn,
36
- runner_type: s.runner_type,
37
- model: s.model,
38
- parameters: s.parameters,
39
- path: s.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 for ${projectIdn}...`);
48
- await generateFlowsYaml(client, agents, projectIdn, verbose);
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(client, projectId = null, verbose = false) {
54
- await ensureState();
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(MAP_PATH, idMap, { spaces: 2 });
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(MAP_PATH, idMap, { spaces: 2 });
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(MAP_PATH))) {
111
- throw new Error('Missing .newo/map.json. Run `newo pull` first.');
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('📋 Loading project mapping...');
115
- const idMap = await fs.readJson(MAP_PATH);
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 = idMap.projects || { '': idMap };
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(MAP_PATH))) {
199
- console.log('No map. Run `newo pull` first.');
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('📋 Loading project mapping and hashes...');
204
- const idMap = await fs.readJson(MAP_PATH);
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 = idMap.projects || { '': idMap };
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(client, agents, projectIdn, verbose = false) {
252
- const flowsData = { flows: [] };
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
- if (verbose) console.log(` 📄 Processing flow: ${flow.idn}`);
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' : 'nsl'}`,
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
- flowsData.flows.push({
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 = path.join(projectDir(projectIdn), 'flows.yaml');
347
- await writeFileAtomic(yamlPath, yamlContent);
348
- console.log(`✓ Generated flows.yaml for ${projectIdn}`);
444
+ const yamlPath = flowsYamlPath(customer.idn);
445
+ await writeFileSafe(yamlPath, yamlContent);
446
+ console.log(`✓ Generated flows.yaml`);
349
447
  }