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/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 ROOT_DIR = path.join(process.cwd(), 'projects');
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 async function ensureState(): Promise<void> {
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(ROOT_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);
13
36
  }
14
37
 
15
- export function projectDir(projectIdn: string): string {
16
- return path.join(ROOT_DIR, projectIdn);
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(ROOT_DIR, projectIdn, agentIdn, flowIdn, `${skillIdn}${extension}`);
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(ROOT_DIR, projectIdn, 'metadata.json');
54
+ export function metadataPath(customerIdn: string, projectIdn: string): string {
55
+ return path.posix.join(customerProjectsDir(customerIdn), projectIdn, 'metadata.json');
32
56
  }
33
57
 
34
- export async function writeFileAtomic(filepath: string, content: string): Promise<void> {
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
- await ensureState();
12
- if (await fs.pathExists(HASHES_PATH)) {
13
- return fs.readJson(HASHES_PATH) as Promise<HashStore>;
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
- await fs.writeJson(HASHES_PATH, hashes, { spaces: 2 });
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
- writeFileAtomic,
13
+ writeFileSafe,
14
14
  readIfExists,
15
- MAP_PATH,
16
- projectDir,
17
- metadataPath
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 path from 'path';
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 writeFileAtomic(metadataPath(projectIdn), JSON.stringify(projectMeta, null, 2));
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
- for (const skill of skills) {
64
- const file = skillPath(projectIdn, agent.idn, flow.idn, skill.idn, skill.runner_type);
65
- await writeFileAtomic(file, skill.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 || '');
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 for ${projectIdn}...`);
84
- await generateFlowsYaml(client, agents, projectIdn, verbose);
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(MAP_PATH, idMap, { spaces: 2 });
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('🔍 Fetching all projects...');
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(MAP_PATH, idMap, { spaces: 2 });
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(MAP_PATH))) {
151
- 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.`);
152
169
  }
153
170
 
154
- if (verbose) console.log('📋 Loading project mapping...');
155
- const idMap = await fs.readJson(MAP_PATH) as ProjectMap | LegacyProjectMap;
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 = 'projects' in idMap && idMap.projects ? idMap.projects : { '': idMap as ProjectData };
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(MAP_PATH))) {
239
- 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.`);
240
261
  return;
241
262
  }
242
263
 
243
- if (verbose) console.log('📋 Loading project mapping and hashes...');
244
- const idMap = await fs.readJson(MAP_PATH) as ProjectMap | LegacyProjectMap;
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 = 'projects' in idMap && idMap.projects ? idMap.projects : { '': idMap as ProjectData };
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
- 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
+ }
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' : 'nsl'}`,
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 = path.join(projectDir(projectIdn), 'flows.yaml');
394
- await writeFileAtomic(yamlPath, yamlContent);
395
- console.log(`✓ Generated flows.yaml for ${projectIdn}`);
444
+ const yamlPath = flowsYamlPath(customer.idn);
445
+ await writeFileSafe(yamlPath, yamlContent);
446
+ console.log(`✓ Generated flows.yaml`);
396
447
  }