newo 1.6.1 → 1.7.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/CHANGELOG.md CHANGED
@@ -5,6 +5,51 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [1.7.1] - 2025-09-15
9
+
10
+ ### Enhanced
11
+ - **Multi-Customer Commands**: Improved user experience for multi-customer operations
12
+ - `newo status` now automatically checks all customers when no default is specified
13
+ - `newo push` provides interactive customer selection dialog when multiple customers exist
14
+ - No more error messages for commands that support multi-customer operations
15
+ - Better user guidance with clear options for customer selection
16
+
17
+ ### Fixed
18
+ - **Command Flow**: Moved customer selection logic into command-specific handlers
19
+ - Prevents early exit errors for multi-customer operations
20
+ - Each command now handles customer selection appropriately
21
+ - Maintains backward compatibility with single-customer setups
22
+
23
+ ## [1.7.0] - 2025-09-15
24
+
25
+ ### Added
26
+ - **Customer Attributes Synchronization**: Complete pull/push functionality for customer attributes
27
+ - `GET /api/v1/bff/customer/attributes?include_hidden=true` - Fetches all 233 customer attributes including hidden system attributes
28
+ - `PUT /api/v1/customer/attributes/{attributeId}` - Updates individual customer attributes
29
+ - Saves to `newo_customers/{customerIdn}/attributes.yaml` in customer root directory
30
+ - YAML format matches reference specification exactly with literal blocks, enum types, and proper multi-line formatting
31
+ - Separate ID mapping stored in `.newo/{customerIdn}/attributes-map.json` for push operations
32
+ - Integrated into existing `pull` and `push` commands seamlessly
33
+ - Full TypeScript type safety with `CustomerAttribute` and `CustomerAttributesResponse` interfaces
34
+
35
+ ### Enhanced
36
+ - **YAML Format Compliance**: Perfect format matching with reference files
37
+ - Literal block scalars (`|-`) for multi-line strings
38
+ - Proper enum format (`!enum "AttributeValueTypes.string"`)
39
+ - Complex JSON string formatting with proper line breaks
40
+ - No escaped quotes in output for better readability
41
+ - **Metadata Generation**: Removed legacy JSON metadata files, YAML-only approach
42
+ - Eliminates redundant `metadata.json` files
43
+ - Cleaner file structure with single source of truth
44
+ - Improved performance with fewer file operations
45
+
46
+ ### Technical
47
+ - **API Layer**: Added `updateCustomerAttribute()` and enhanced `getCustomerAttributes()` with `includeHidden` parameter
48
+ - **Sync Engine**: Integrated attributes handling into `pullAll()` and `pushChanged()` functions
49
+ - **File System**: Added `customerAttributesPath()` and `customerAttributesMapPath()` utilities
50
+ - **Type Safety**: Extended type definitions with proper customer attribute interfaces
51
+ - **Error Handling**: Comprehensive error handling for attributes operations with graceful fallbacks
52
+
8
53
  ## [1.6.1] - 2025-09-13
9
54
 
10
55
  ### Fixed
package/dist/api.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { type AxiosInstance } from 'axios';
2
- import type { ProjectMeta, Agent, Skill, FlowEvent, FlowState, AkbImportArticle, CustomerProfile } from './types.js';
2
+ import type { ProjectMeta, Agent, Skill, FlowEvent, FlowState, AkbImportArticle, CustomerProfile, CustomerAttribute, CustomerAttributesResponse } from './types.js';
3
3
  export declare function makeClient(verbose?: boolean, token?: string): Promise<AxiosInstance>;
4
4
  export declare function listProjects(client: AxiosInstance): Promise<ProjectMeta[]>;
5
5
  export declare function listAgents(client: AxiosInstance, projectId: string): Promise<Agent[]>;
@@ -11,4 +11,6 @@ export declare function listFlowEvents(client: AxiosInstance, flowId: string): P
11
11
  export declare function listFlowStates(client: AxiosInstance, flowId: string): Promise<FlowState[]>;
12
12
  export declare function importAkbArticle(client: AxiosInstance, articleData: AkbImportArticle): Promise<unknown>;
13
13
  export declare function getCustomerProfile(client: AxiosInstance): Promise<CustomerProfile>;
14
+ export declare function getCustomerAttributes(client: AxiosInstance, includeHidden?: boolean): Promise<CustomerAttributesResponse>;
15
+ export declare function updateCustomerAttribute(client: AxiosInstance, attribute: CustomerAttribute): Promise<void>;
14
16
  //# sourceMappingURL=api.d.ts.map
package/dist/api.js CHANGED
@@ -100,4 +100,25 @@ export async function getCustomerProfile(client) {
100
100
  const response = await client.get('/api/v1/customer/profile');
101
101
  return response.data;
102
102
  }
103
+ export async function getCustomerAttributes(client, includeHidden = true) {
104
+ const response = await client.get('/api/v1/bff/customer/attributes', {
105
+ params: { include_hidden: includeHidden }
106
+ });
107
+ return response.data;
108
+ }
109
+ export async function updateCustomerAttribute(client, attribute) {
110
+ if (!attribute.id) {
111
+ throw new Error(`Attribute ${attribute.idn} is missing ID - cannot update`);
112
+ }
113
+ await client.put(`/api/v1/customer/attributes/${attribute.id}`, {
114
+ idn: attribute.idn,
115
+ value: attribute.value,
116
+ title: attribute.title,
117
+ description: attribute.description,
118
+ group: attribute.group,
119
+ is_hidden: attribute.is_hidden,
120
+ possible_values: attribute.possible_values,
121
+ value_type: attribute.value_type
122
+ });
123
+ }
103
124
  //# sourceMappingURL=api.js.map
package/dist/cli.js CHANGED
@@ -2,7 +2,7 @@
2
2
  import minimist from 'minimist';
3
3
  import dotenv from 'dotenv';
4
4
  import { makeClient, getProjectMeta, importAkbArticle } from './api.js';
5
- import { pullAll, pushChanged, status } from './sync.js';
5
+ import { pullAll, pushChanged, status, saveCustomerAttributes } from './sync.js';
6
6
  import { parseAkbFile, prepareArticlesForImport } from './akb.js';
7
7
  import { initializeEnvironment, ENV, EnvValidationError } from './env.js';
8
8
  import { parseCustomerConfigAsync, listCustomers, getCustomer, getDefaultCustomer, tryGetDefaultCustomer, getAllCustomers, validateCustomerConfig } from './customerAsync.js';
@@ -151,45 +151,9 @@ async function main() {
151
151
  }
152
152
  return;
153
153
  }
154
- if (args.customer) {
155
- const customer = getCustomer(customerConfig, args.customer);
156
- if (!customer) {
157
- console.error(`Unknown customer: ${args.customer}`);
158
- console.error(`Available customers: ${listCustomers(customerConfig).join(', ')}`);
159
- process.exit(1);
160
- }
161
- selectedCustomer = customer;
162
- }
163
- else {
164
- // For pull command, try to get default but fall back to all customers if multiple exist
165
- if (cmd === 'pull') {
166
- try {
167
- selectedCustomer = tryGetDefaultCustomer(customerConfig);
168
- if (!selectedCustomer) {
169
- // Multiple customers exist with no default, pull from all
170
- allCustomers = getAllCustomers(customerConfig);
171
- if (verbose)
172
- console.log(`šŸ“„ No default customer specified, pulling from all ${allCustomers.length} customers`);
173
- }
174
- }
175
- catch (error) {
176
- const message = error instanceof Error ? error.message : String(error);
177
- console.error(message);
178
- process.exit(1);
179
- }
180
- }
181
- else {
182
- // For other commands, require explicit customer selection
183
- try {
184
- selectedCustomer = getDefaultCustomer(customerConfig);
185
- }
186
- catch (error) {
187
- const message = error instanceof Error ? error.message : String(error);
188
- console.error(message);
189
- process.exit(1);
190
- }
191
- }
192
- }
154
+ // Customer selection logic moved inside command processing to avoid early failures
155
+ if (verbose)
156
+ console.log(`šŸ” Command parsed: "${cmd}"`);
193
157
  if (!cmd || ['help', '-h', '--help'].includes(cmd)) {
194
158
  console.log(`NEWO CLI - Multi-Customer Support
195
159
  Usage:
@@ -201,7 +165,7 @@ Usage:
201
165
  newo import-akb <file> <persona_id> [--customer <idn>] # import AKB articles from file
202
166
 
203
167
  Flags:
204
- --customer <idn> # specify customer (if not set, uses default or all for pull)
168
+ --customer <idn> # specify customer (if not set, uses default or interactive selection)
205
169
  --verbose, -v # enable detailed logging
206
170
 
207
171
  Environment Variables:
@@ -219,8 +183,9 @@ Multi-Customer Examples:
219
183
  # Commands:
220
184
  newo pull # Pull from all customers (if no default set)
221
185
  newo pull --customer acme # Pull projects for Acme only
222
- newo push --customer globex # Push changes for Globex
223
- newo status # Status for default customer
186
+ newo status # Status for all customers (if no default set)
187
+ newo push # Interactive selection for multiple customers
188
+ newo push --customer globex # Push changes for Globex only
224
189
 
225
190
  File Structure:
226
191
  newo_customers/
@@ -233,7 +198,28 @@ File Structure:
233
198
  `);
234
199
  return;
235
200
  }
201
+ if (verbose)
202
+ console.log(`šŸ” Starting command processing for: ${cmd}`);
236
203
  if (cmd === 'pull') {
204
+ // Handle customer selection for pull command
205
+ if (args.customer) {
206
+ const customer = getCustomer(customerConfig, args.customer);
207
+ if (!customer) {
208
+ console.error(`Unknown customer: ${args.customer}`);
209
+ console.error(`Available customers: ${listCustomers(customerConfig).join(', ')}`);
210
+ process.exit(1);
211
+ }
212
+ selectedCustomer = customer;
213
+ }
214
+ else {
215
+ // Try to get default, fall back to all customers
216
+ selectedCustomer = tryGetDefaultCustomer(customerConfig);
217
+ if (!selectedCustomer) {
218
+ allCustomers = getAllCustomers(customerConfig);
219
+ if (verbose)
220
+ console.log(`šŸ“„ No default customer specified, pulling from all ${allCustomers.length} customers`);
221
+ }
222
+ }
237
223
  if (selectedCustomer) {
238
224
  // Single customer pull
239
225
  const accessToken = await getValidAccessToken(selectedCustomer);
@@ -255,7 +241,128 @@ File Structure:
255
241
  }
256
242
  return;
257
243
  }
244
+ if (cmd === 'status') {
245
+ // Handle customer selection for status command
246
+ if (args.customer) {
247
+ const customer = getCustomer(customerConfig, args.customer);
248
+ if (!customer) {
249
+ console.error(`Unknown customer: ${args.customer}`);
250
+ console.error(`Available customers: ${listCustomers(customerConfig).join(', ')}`);
251
+ process.exit(1);
252
+ }
253
+ selectedCustomer = customer;
254
+ }
255
+ else {
256
+ // Try to get default, fall back to all customers
257
+ selectedCustomer = tryGetDefaultCustomer(customerConfig);
258
+ if (!selectedCustomer) {
259
+ allCustomers = getAllCustomers(customerConfig);
260
+ console.log(`šŸ”„ Checking status for ${allCustomers.length} customers...`);
261
+ }
262
+ }
263
+ if (selectedCustomer) {
264
+ // Single customer status
265
+ await status(selectedCustomer, verbose);
266
+ }
267
+ else if (allCustomers.length > 0) {
268
+ // Multi-customer status
269
+ for (const customer of allCustomers) {
270
+ console.log(`\nšŸ“‹ Status for customer: ${customer.idn}`);
271
+ await status(customer, verbose);
272
+ }
273
+ console.log(`\nāœ… Status check completed for all ${allCustomers.length} customers`);
274
+ }
275
+ return;
276
+ }
277
+ if (cmd === 'push') {
278
+ // Handle customer selection for push command
279
+ if (args.customer) {
280
+ const customer = getCustomer(customerConfig, args.customer);
281
+ if (!customer) {
282
+ console.error(`Unknown customer: ${args.customer}`);
283
+ console.error(`Available customers: ${listCustomers(customerConfig).join(', ')}`);
284
+ process.exit(1);
285
+ }
286
+ selectedCustomer = customer;
287
+ }
288
+ else {
289
+ // Try to get default, provide interactive selection if multiple exist
290
+ selectedCustomer = tryGetDefaultCustomer(customerConfig);
291
+ if (!selectedCustomer) {
292
+ // Multiple customers exist with no default, ask user
293
+ allCustomers = getAllCustomers(customerConfig);
294
+ console.log(`\nšŸ“¤ Multiple customers available for push:`);
295
+ allCustomers.forEach((customer, index) => {
296
+ console.log(` ${index + 1}. ${customer.idn}`);
297
+ });
298
+ console.log(` ${allCustomers.length + 1}. All customers`);
299
+ const readline = await import('readline');
300
+ const rl = readline.createInterface({
301
+ input: process.stdin,
302
+ output: process.stdout
303
+ });
304
+ const choice = await new Promise((resolve) => {
305
+ rl.question(`\nSelect customer to push (1-${allCustomers.length + 1}): `, resolve);
306
+ });
307
+ rl.close();
308
+ const choiceNum = parseInt(choice.trim());
309
+ if (choiceNum === allCustomers.length + 1) {
310
+ // User selected "All customers"
311
+ console.log(`šŸ”„ Pushing to all ${allCustomers.length} customers...`);
312
+ }
313
+ else if (choiceNum >= 1 && choiceNum <= allCustomers.length) {
314
+ // User selected specific customer
315
+ selectedCustomer = allCustomers[choiceNum - 1] || null;
316
+ allCustomers = []; // Clear to indicate single customer mode
317
+ if (selectedCustomer) {
318
+ console.log(`šŸ”„ Pushing to customer: ${selectedCustomer.idn}`);
319
+ }
320
+ }
321
+ else {
322
+ console.error('Invalid choice. Exiting.');
323
+ process.exit(1);
324
+ }
325
+ }
326
+ }
327
+ if (selectedCustomer) {
328
+ // Single customer push
329
+ const accessToken = await getValidAccessToken(selectedCustomer);
330
+ const client = await makeClient(verbose, accessToken);
331
+ await pushChanged(client, selectedCustomer, verbose);
332
+ }
333
+ else if (allCustomers.length > 0) {
334
+ // Multi-customer push (user selected "All customers")
335
+ console.log(`šŸ”„ Pushing to ${allCustomers.length} customers...`);
336
+ for (const customer of allCustomers) {
337
+ console.log(`\nšŸ“¤ Pushing for customer: ${customer.idn}`);
338
+ const accessToken = await getValidAccessToken(customer);
339
+ const client = await makeClient(verbose, accessToken);
340
+ await pushChanged(client, customer, verbose);
341
+ }
342
+ console.log(`\nāœ… Push completed for all ${allCustomers.length} customers`);
343
+ }
344
+ return;
345
+ }
258
346
  // For all other commands, require a single selected customer
347
+ if (args.customer) {
348
+ const customer = getCustomer(customerConfig, args.customer);
349
+ if (!customer) {
350
+ console.error(`Unknown customer: ${args.customer}`);
351
+ console.error(`Available customers: ${listCustomers(customerConfig).join(', ')}`);
352
+ process.exit(1);
353
+ }
354
+ selectedCustomer = customer;
355
+ }
356
+ else {
357
+ try {
358
+ selectedCustomer = getDefaultCustomer(customerConfig);
359
+ }
360
+ catch (error) {
361
+ const message = error instanceof Error ? error.message : String(error);
362
+ console.error(message);
363
+ process.exit(1);
364
+ }
365
+ }
259
366
  if (!selectedCustomer) {
260
367
  console.error('Customer selection required for this command');
261
368
  process.exit(1);
@@ -263,13 +370,7 @@ File Structure:
263
370
  // Get access token for the selected customer
264
371
  const accessToken = await getValidAccessToken(selectedCustomer);
265
372
  const client = await makeClient(verbose, accessToken);
266
- if (cmd === 'push') {
267
- await pushChanged(client, selectedCustomer, verbose);
268
- }
269
- else if (cmd === 'status') {
270
- await status(selectedCustomer, verbose);
271
- }
272
- else if (cmd === 'meta') {
373
+ if (cmd === 'meta') {
273
374
  if (!selectedCustomer.projectId) {
274
375
  console.error(`No project ID configured for customer ${selectedCustomer.idn}`);
275
376
  console.error(`Set NEWO_CUSTOMER_${selectedCustomer.idn.toUpperCase()}_PROJECT_ID in your .env file`);
@@ -278,6 +379,11 @@ File Structure:
278
379
  const meta = await getProjectMeta(client, selectedCustomer.projectId);
279
380
  console.log(JSON.stringify(meta, null, 2));
280
381
  }
382
+ else if (cmd === 'pull-attributes') {
383
+ console.log(`šŸ” Fetching customer attributes for ${selectedCustomer.idn}...`);
384
+ await saveCustomerAttributes(client, selectedCustomer, verbose);
385
+ console.log(`āœ… Customer attributes saved to newo_customers/${selectedCustomer.idn}/attributes.yaml`);
386
+ }
281
387
  else if (cmd === 'import-akb') {
282
388
  const akbFile = args._[1];
283
389
  const personaId = args._[2];
package/dist/fsutil.d.ts CHANGED
@@ -9,7 +9,15 @@ export declare function hashesPath(customerIdn: string): string;
9
9
  export declare function ensureState(customerIdn: string): Promise<void>;
10
10
  export declare function projectDir(customerIdn: string, projectIdn: string): string;
11
11
  export declare function flowsYamlPath(customerIdn: string): string;
12
+ export declare function customerAttributesPath(customerIdn: string): string;
13
+ export declare function customerAttributesMapPath(customerIdn: string): string;
12
14
  export declare function skillPath(customerIdn: string, projectIdn: string, agentIdn: string, flowIdn: string, skillIdn: string, runnerType?: RunnerType): string;
15
+ export declare function skillFolderPath(customerIdn: string, projectIdn: string, agentIdn: string, flowIdn: string, skillIdn: string): string;
16
+ export declare function skillScriptPath(customerIdn: string, projectIdn: string, agentIdn: string, flowIdn: string, skillIdn: string, runnerType?: RunnerType): string;
17
+ export declare function projectMetadataPath(customerIdn: string, projectIdn: string): string;
18
+ export declare function agentMetadataPath(customerIdn: string, projectIdn: string, agentIdn: string): string;
19
+ export declare function flowMetadataPath(customerIdn: string, projectIdn: string, agentIdn: string, flowIdn: string): string;
20
+ export declare function skillMetadataPath(customerIdn: string, projectIdn: string, agentIdn: string, flowIdn: string, skillIdn: string): string;
13
21
  export declare function metadataPath(customerIdn: string, projectIdn: string): string;
14
22
  export declare const ROOT_DIR: string;
15
23
  export declare const MAP_PATH: string;
package/dist/fsutil.js CHANGED
@@ -28,10 +28,39 @@ export function projectDir(customerIdn, projectIdn) {
28
28
  export function flowsYamlPath(customerIdn) {
29
29
  return path.posix.join(customerProjectsDir(customerIdn), 'flows.yaml');
30
30
  }
31
+ export function customerAttributesPath(customerIdn) {
32
+ return path.posix.join(customerDir(customerIdn), 'attributes.yaml');
33
+ }
34
+ export function customerAttributesMapPath(customerIdn) {
35
+ return path.join(customerStateDir(customerIdn), 'attributes-map.json');
36
+ }
37
+ // Legacy skill path - direct file
31
38
  export function skillPath(customerIdn, projectIdn, agentIdn, flowIdn, skillIdn, runnerType = 'guidance') {
32
39
  const extension = runnerType === 'nsl' ? '.jinja' : '.guidance';
33
40
  return path.posix.join(customerProjectsDir(customerIdn), projectIdn, agentIdn, flowIdn, `${skillIdn}${extension}`);
34
41
  }
42
+ // New hierarchical structure paths
43
+ export function skillFolderPath(customerIdn, projectIdn, agentIdn, flowIdn, skillIdn) {
44
+ return path.posix.join(customerProjectsDir(customerIdn), projectIdn, agentIdn, flowIdn, skillIdn);
45
+ }
46
+ export function skillScriptPath(customerIdn, projectIdn, agentIdn, flowIdn, skillIdn, runnerType = 'guidance') {
47
+ const extension = runnerType === 'nsl' ? '.jinja' : '.guidance';
48
+ return path.posix.join(skillFolderPath(customerIdn, projectIdn, agentIdn, flowIdn, skillIdn), `skill${extension}`);
49
+ }
50
+ // Metadata paths for hierarchical structure
51
+ export function projectMetadataPath(customerIdn, projectIdn) {
52
+ return path.posix.join(customerProjectsDir(customerIdn), projectIdn, 'metadata.yaml');
53
+ }
54
+ export function agentMetadataPath(customerIdn, projectIdn, agentIdn) {
55
+ return path.posix.join(customerProjectsDir(customerIdn), projectIdn, agentIdn, 'metadata.yaml');
56
+ }
57
+ export function flowMetadataPath(customerIdn, projectIdn, agentIdn, flowIdn) {
58
+ return path.posix.join(customerProjectsDir(customerIdn), projectIdn, agentIdn, flowIdn, 'metadata.yaml');
59
+ }
60
+ export function skillMetadataPath(customerIdn, projectIdn, agentIdn, flowIdn, skillIdn) {
61
+ return path.posix.join(skillFolderPath(customerIdn, projectIdn, agentIdn, flowIdn, skillIdn), 'metadata.yaml');
62
+ }
63
+ // Legacy metadata path - keep for backwards compatibility
35
64
  export function metadataPath(customerIdn, projectIdn) {
36
65
  return path.posix.join(customerProjectsDir(customerIdn), projectIdn, 'metadata.json');
37
66
  }
package/dist/sync.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import type { AxiosInstance } from 'axios';
2
2
  import type { ProjectData, CustomerConfig } from './types.js';
3
+ export declare function saveCustomerAttributes(client: AxiosInstance, customer: CustomerConfig, verbose?: boolean): Promise<void>;
3
4
  export declare function pullSingleProject(client: AxiosInstance, customer: CustomerConfig, projectId: string, projectIdn: string, verbose?: boolean): Promise<ProjectData>;
4
5
  export declare function pullAll(client: AxiosInstance, customer: CustomerConfig, projectId?: string | null, verbose?: boolean): Promise<void>;
5
6
  export declare function pushChanged(client: AxiosInstance, customer: CustomerConfig, verbose?: boolean): Promise<void>;