newo 1.8.0 → 1.9.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,57 @@ 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.9.1] - 2025-09-16
9
+
10
+ ### Fixed
11
+ - **Clean Chat History Implementation**: Remove conversations acts API fallback entirely
12
+ - Eliminates all 403 "Invalid token or account_id field missing" errors
13
+ - Uses only `/api/v1/chat/history` endpoint which works with current API keys
14
+ - Removed unused `getConversationActs()` function and related types
15
+ - Clean implementation without permission-dependent fallbacks
16
+
17
+ ### Removed
18
+ - **Obsolete Code Cleanup**: Remove unused conversation acts API components
19
+ - `getConversationActs()` function (unused after chat history integration)
20
+ - `ConversationActsParams` and `ConversationActsResponse` interfaces
21
+ - Fallback logic that caused 403 errors for personas without proper permissions
22
+
23
+ ## [1.9.0] - 2025-09-16
24
+
25
+ ### Added
26
+ - **User Conversations Pull Functionality**: Complete conversation history extraction
27
+ - New `newo conversations` command to download user conversations and personas
28
+ - Multi-customer conversation support with `--customer <idn>` flag
29
+ - Chat History API integration (`/api/v1/chat/history`) with fallback to conversations acts API
30
+ - Automatic phone number extraction from persona actors
31
+ - Comprehensive pagination handling for large conversation datasets
32
+ - Clean YAML output format in `newo_customers/{customerIdn}/conversations.yaml`
33
+
34
+ ### Enhanced
35
+ - **Conversation Data Processing**: Optimized structure and chronological ordering
36
+ - Acts sorted by datetime ascending (chronological conversation flow)
37
+ - Personas sorted by most recent activity (descending)
38
+ - Redundant fields removed (`is_agent`, `session_id: unknown`, etc.)
39
+ - Clean persona structure: `id` → `name` → `phone` → `act_count` → `acts`
40
+ - Proper datetime extraction from chat history API responses
41
+
42
+ ### Technical
43
+ - **New API Functions**: Type-safe conversation API integration
44
+ - `listUserPersonas()` - Get all user personas with pagination
45
+ - `getChatHistory()` - Get conversation history for user actors
46
+ - `getConversationActs()` - Fallback for accounts with proper permissions
47
+ - `pullConversations()` - Complete conversation sync orchestration
48
+ - **NPM Scripts**: Added convenient conversation commands
49
+ - `npm run conversations` - Build and pull conversations
50
+ - `npm run conversations:all` - Legacy alias for compatibility
51
+
52
+ ### Performance
53
+ - **Concurrent Processing**: Efficient conversation data extraction
54
+ - Parallel API calls with concurrency limiting (p-limit)
55
+ - Graceful error handling with persona-level fault tolerance
56
+ - No artificial limits on personas or acts (loads all available data)
57
+ - Multi-customer support with authentication reuse
58
+
8
59
  ## [1.8.0] - 2025-09-15
9
60
 
10
61
  ### Added
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, CustomerAttribute, CustomerAttributesResponse } from './types.js';
2
+ import type { ProjectMeta, Agent, Skill, FlowEvent, FlowState, AkbImportArticle, CustomerProfile, CustomerAttribute, CustomerAttributesResponse, UserPersonaResponse, UserPersona, ChatHistoryParams, ChatHistoryResponse } 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[]>;
@@ -13,4 +13,11 @@ export declare function importAkbArticle(client: AxiosInstance, articleData: Akb
13
13
  export declare function getCustomerProfile(client: AxiosInstance): Promise<CustomerProfile>;
14
14
  export declare function getCustomerAttributes(client: AxiosInstance, includeHidden?: boolean): Promise<CustomerAttributesResponse>;
15
15
  export declare function updateCustomerAttribute(client: AxiosInstance, attribute: CustomerAttribute): Promise<void>;
16
+ export declare function listUserPersonas(client: AxiosInstance, page?: number, per?: number): Promise<UserPersonaResponse>;
17
+ export declare function getUserPersona(client: AxiosInstance, personaId: string): Promise<UserPersona>;
18
+ export declare function getAccount(client: AxiosInstance): Promise<{
19
+ id: string;
20
+ [key: string]: any;
21
+ }>;
22
+ export declare function getChatHistory(client: AxiosInstance, params: ChatHistoryParams): Promise<ChatHistoryResponse>;
16
23
  //# sourceMappingURL=api.d.ts.map
package/dist/api.js CHANGED
@@ -121,4 +121,34 @@ export async function updateCustomerAttribute(client, attribute) {
121
121
  value_type: attribute.value_type
122
122
  });
123
123
  }
124
+ // Conversation API Functions
125
+ export async function listUserPersonas(client, page = 1, per = 50) {
126
+ const response = await client.get('/api/v1/bff/conversations/user-personas', {
127
+ params: { page, per }
128
+ });
129
+ return response.data;
130
+ }
131
+ export async function getUserPersona(client, personaId) {
132
+ const response = await client.get(`/api/v1/bff/conversations/user-personas/${personaId}`);
133
+ return response.data;
134
+ }
135
+ export async function getAccount(client) {
136
+ const response = await client.get('/api/v1/account');
137
+ return response.data;
138
+ }
139
+ export async function getChatHistory(client, params) {
140
+ const queryParams = {
141
+ user_actor_id: params.user_actor_id,
142
+ page: params.page || 1,
143
+ per: params.per || 50
144
+ };
145
+ // Only add agent_actor_id if provided
146
+ if (params.agent_actor_id) {
147
+ queryParams.agent_actor_id = params.agent_actor_id;
148
+ }
149
+ const response = await client.get('/api/v1/chat/history', {
150
+ params: queryParams
151
+ });
152
+ return response.data;
153
+ }
124
154
  //# 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, saveCustomerAttributes } from './sync.js';
5
+ import { pullAll, pushChanged, status, saveCustomerAttributes, pullConversations } 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';
@@ -160,12 +160,14 @@ Usage:
160
160
  newo pull [--customer <idn>] # download projects -> ./newo_customers/<idn>/projects/
161
161
  newo push [--customer <idn>] # upload modified *.guidance/*.jinja back to NEWO
162
162
  newo status [--customer <idn>] # show modified files
163
+ newo conversations [--customer <idn>] [--all] # download user conversations -> ./newo_customers/<idn>/conversations.yaml
163
164
  newo list-customers # list available customers
164
165
  newo meta [--customer <idn>] # get project metadata (debug)
165
166
  newo import-akb <file> <persona_id> [--customer <idn>] # import AKB articles from file
166
167
 
167
168
  Flags:
168
169
  --customer <idn> # specify customer (if not set, uses default or interactive selection)
170
+ --all # include all available data (for conversations: all personas and acts)
169
171
  --verbose, -v # enable detailed logging
170
172
 
171
173
  Environment Variables:
@@ -343,6 +345,53 @@ File Structure:
343
345
  }
344
346
  return;
345
347
  }
348
+ if (cmd === 'conversations') {
349
+ // Handle customer selection for conversations command
350
+ if (args.customer) {
351
+ const customer = getCustomer(customerConfig, args.customer);
352
+ if (!customer) {
353
+ console.error(`Unknown customer: ${args.customer}`);
354
+ console.error(`Available customers: ${listCustomers(customerConfig).join(', ')}`);
355
+ process.exit(1);
356
+ }
357
+ selectedCustomer = customer;
358
+ }
359
+ else {
360
+ // Try to get default, fall back to all customers
361
+ selectedCustomer = tryGetDefaultCustomer(customerConfig);
362
+ if (!selectedCustomer) {
363
+ allCustomers = getAllCustomers(customerConfig);
364
+ if (verbose)
365
+ console.log(`💬 No default customer specified, pulling conversations from all ${allCustomers.length} customers`);
366
+ }
367
+ }
368
+ // Parse conversation-specific options - load all data by default
369
+ const conversationOptions = {
370
+ includeAll: true, // Always include all data for conversations
371
+ maxPersonas: undefined, // No limit on personas
372
+ maxActsPerPersona: undefined // No limit on acts per persona
373
+ };
374
+ if (selectedCustomer) {
375
+ // Single customer conversations
376
+ const accessToken = await getValidAccessToken(selectedCustomer);
377
+ const client = await makeClient(verbose, accessToken);
378
+ console.log(`💬 Pulling conversations for customer: ${selectedCustomer.idn} (all data)`);
379
+ await pullConversations(client, selectedCustomer, conversationOptions, verbose);
380
+ console.log(`✅ Conversations saved to newo_customers/${selectedCustomer.idn}/conversations.yaml`);
381
+ }
382
+ else if (allCustomers.length > 0) {
383
+ // Multi-customer conversations
384
+ console.log(`💬 Pulling conversations from ${allCustomers.length} customers (all data)...`);
385
+ for (const customer of allCustomers) {
386
+ console.log(`\n💬 Pulling conversations for customer: ${customer.idn}`);
387
+ const accessToken = await getValidAccessToken(customer);
388
+ const client = await makeClient(verbose, accessToken);
389
+ await pullConversations(client, customer, conversationOptions, verbose);
390
+ }
391
+ console.log(`\n✅ Conversations pull completed for all ${allCustomers.length} customers`);
392
+ }
393
+ return;
394
+ }
346
395
  // For all other commands, require a single selected customer
347
396
  if (args.customer) {
348
397
  const customer = getCustomer(customerConfig, args.customer);
package/dist/sync.d.ts CHANGED
@@ -1,8 +1,9 @@
1
1
  import type { AxiosInstance } from 'axios';
2
- import type { ProjectData, CustomerConfig } from './types.js';
2
+ import type { ProjectData, CustomerConfig, ConversationOptions } from './types.js';
3
3
  export declare function saveCustomerAttributes(client: AxiosInstance, customer: CustomerConfig, verbose?: boolean): Promise<void>;
4
4
  export declare function pullSingleProject(client: AxiosInstance, customer: CustomerConfig, projectId: string, projectIdn: string, verbose?: boolean): Promise<ProjectData>;
5
5
  export declare function pullAll(client: AxiosInstance, customer: CustomerConfig, projectId?: string | null, verbose?: boolean): Promise<void>;
6
6
  export declare function pushChanged(client: AxiosInstance, customer: CustomerConfig, verbose?: boolean): Promise<void>;
7
7
  export declare function status(customer: CustomerConfig, verbose?: boolean): Promise<void>;
8
+ export declare function pullConversations(client: AxiosInstance, customer: CustomerConfig, options?: ConversationOptions, verbose?: boolean): Promise<void>;
8
9
  //# sourceMappingURL=sync.d.ts.map
package/dist/sync.js CHANGED
@@ -1,4 +1,4 @@
1
- import { listProjects, listAgents, listFlowSkills, updateSkill, listFlowEvents, listFlowStates, getProjectMeta, getCustomerAttributes, updateCustomerAttribute } from './api.js';
1
+ import { listProjects, listAgents, listFlowSkills, updateSkill, listFlowEvents, listFlowStates, getProjectMeta, getCustomerAttributes, updateCustomerAttribute, listUserPersonas, getChatHistory } from './api.js';
2
2
  import { ensureState, skillPath, skillScriptPath, writeFileSafe, readIfExists, mapPath, projectMetadataPath, agentMetadataPath, flowMetadataPath, skillMetadataPath, flowsYamlPath, customerAttributesPath, customerAttributesMapPath, customerAttributesBackupPath } from './fsutil.js';
3
3
  import fs from 'fs-extra';
4
4
  import { sha256, loadHashes, saveHashes } from './hash.js';
@@ -1018,4 +1018,181 @@ async function generateFlowsYaml(client, customer, agents, verbose = false) {
1018
1018
  await writeFileSafe(yamlPath, yamlContent);
1019
1019
  console.log(`✓ Generated flows.yaml`);
1020
1020
  }
1021
+ // Conversation sync functions
1022
+ export async function pullConversations(client, customer, options = {}, verbose = false) {
1023
+ if (verbose)
1024
+ console.log(`💬 Fetching conversations for customer ${customer.idn}...`);
1025
+ try {
1026
+ // Get all user personas with pagination
1027
+ const allPersonas = [];
1028
+ let page = 1;
1029
+ const perPage = 50;
1030
+ let hasMore = true;
1031
+ while (hasMore) {
1032
+ const response = await listUserPersonas(client, page, perPage);
1033
+ allPersonas.push(...response.items);
1034
+ if (verbose)
1035
+ console.log(`📋 Page ${page}: Found ${response.items.length} personas (${allPersonas.length}/${response.metadata.total} total)`);
1036
+ hasMore = response.items.length === perPage && allPersonas.length < response.metadata.total;
1037
+ page++;
1038
+ }
1039
+ if (options.maxPersonas && allPersonas.length > options.maxPersonas) {
1040
+ allPersonas.splice(options.maxPersonas);
1041
+ if (verbose)
1042
+ console.log(`⚠️ Limited to ${options.maxPersonas} personas as requested`);
1043
+ }
1044
+ if (verbose)
1045
+ console.log(`👥 Processing ${allPersonas.length} personas...`);
1046
+ // Process personas concurrently with limited concurrency
1047
+ const processedPersonas = [];
1048
+ await Promise.all(allPersonas.map(persona => concurrencyLimit(async () => {
1049
+ try {
1050
+ // Extract phone number from actors
1051
+ const phoneActor = persona.actors.find(actor => actor.integration_idn === 'newo_voice' &&
1052
+ actor.connector_idn === 'newo_voice_connector' &&
1053
+ actor.contact_information?.startsWith('+'));
1054
+ const phone = phoneActor?.contact_information || null;
1055
+ // Get acts for this persona
1056
+ const allActs = [];
1057
+ let actPage = 1;
1058
+ const actsPerPage = 100; // Higher limit for acts
1059
+ let hasMoreActs = true;
1060
+ while (hasMoreActs) {
1061
+ // First try the chat history API as alternative
1062
+ try {
1063
+ // Get user actor IDs from persona actors
1064
+ const userActors = persona.actors.filter(actor => actor.integration_idn === 'newo_voice' &&
1065
+ actor.connector_idn === 'newo_voice_connector');
1066
+ if (userActors.length > 0) {
1067
+ const chatHistoryParams = {
1068
+ user_actor_id: userActors[0].id,
1069
+ page: actPage,
1070
+ per: actsPerPage
1071
+ };
1072
+ const chatResponse = await getChatHistory(client, chatHistoryParams);
1073
+ if (chatResponse.items && chatResponse.items.length > 0) {
1074
+ // Convert chat history format to acts format - create minimal ConversationAct objects
1075
+ const convertedActs = chatResponse.items.map((item) => ({
1076
+ id: item.id || `chat_${Math.random()}`,
1077
+ command_act_id: null,
1078
+ external_event_id: item.external_event_id || 'chat_history',
1079
+ arguments: [],
1080
+ reference_idn: (item.is_agent === true) ? 'agent_message' : 'user_message',
1081
+ runtime_context_id: item.runtime_context_id || 'chat_history',
1082
+ source_text: item.payload?.text || item.message || item.content || item.text || '',
1083
+ original_text: item.payload?.text || item.message || item.content || item.text || '',
1084
+ datetime: item.datetime || item.created_at || item.timestamp || new Date().toISOString(),
1085
+ user_actor_id: userActors[0].id,
1086
+ agent_actor_id: null,
1087
+ user_persona_id: persona.id,
1088
+ user_persona_name: persona.name,
1089
+ agent_persona_id: item.agent_persona_id || 'unknown',
1090
+ external_id: item.external_id || null,
1091
+ integration_idn: 'newo_voice',
1092
+ connector_idn: 'newo_voice_connector',
1093
+ to_integration_idn: null,
1094
+ to_connector_idn: null,
1095
+ is_agent: Boolean(item.is_agent === true),
1096
+ project_idn: null,
1097
+ flow_idn: item.flow_idn || 'unknown',
1098
+ skill_idn: item.skill_idn || 'unknown',
1099
+ session_id: item.session_id || 'unknown',
1100
+ recordings: item.recordings || [],
1101
+ contact_information: item.contact_information || null
1102
+ }));
1103
+ allActs.push(...convertedActs);
1104
+ if (verbose && convertedActs.length > 0) {
1105
+ console.log(` 👤 ${persona.name}: Chat History - ${convertedActs.length} messages (${allActs.length} total)`);
1106
+ }
1107
+ hasMoreActs = chatResponse.items.length === actsPerPage &&
1108
+ chatResponse.metadata?.total !== undefined &&
1109
+ allActs.length < chatResponse.metadata.total;
1110
+ actPage++;
1111
+ continue; // Skip the original acts API call
1112
+ }
1113
+ }
1114
+ }
1115
+ catch (chatError) {
1116
+ if (verbose)
1117
+ console.log(` ⚠️ Chat history failed for ${persona.name}: ${chatError instanceof Error ? chatError.message : String(chatError)}`);
1118
+ // No fallback - only use chat history API
1119
+ hasMoreActs = false;
1120
+ }
1121
+ }
1122
+ // Sort acts by datetime ascending (chronological order)
1123
+ allActs.sort((a, b) => new Date(a.datetime).getTime() - new Date(b.datetime).getTime());
1124
+ // Process acts into simplified format - exclude redundant fields
1125
+ const processedActs = allActs.map(act => {
1126
+ const processedAct = {
1127
+ datetime: act.datetime,
1128
+ type: act.reference_idn,
1129
+ message: act.source_text
1130
+ };
1131
+ // Only include non-redundant fields
1132
+ if (act.contact_information) {
1133
+ processedAct.contact_information = act.contact_information;
1134
+ }
1135
+ if (act.flow_idn && act.flow_idn !== 'unknown') {
1136
+ processedAct.flow_idn = act.flow_idn;
1137
+ }
1138
+ if (act.skill_idn && act.skill_idn !== 'unknown') {
1139
+ processedAct.skill_idn = act.skill_idn;
1140
+ }
1141
+ if (act.session_id && act.session_id !== 'unknown') {
1142
+ processedAct.session_id = act.session_id;
1143
+ }
1144
+ return processedAct;
1145
+ });
1146
+ processedPersonas.push({
1147
+ id: persona.id,
1148
+ name: persona.name,
1149
+ phone,
1150
+ act_count: persona.act_count,
1151
+ acts: processedActs
1152
+ });
1153
+ if (verbose)
1154
+ console.log(` ✓ Processed ${persona.name}: ${processedActs.length} acts`);
1155
+ }
1156
+ catch (error) {
1157
+ console.error(`❌ Failed to process persona ${persona.name}:`, error);
1158
+ // Continue with other personas
1159
+ }
1160
+ })));
1161
+ // Sort personas by most recent act time (descending) - use latest act from acts array
1162
+ processedPersonas.sort((a, b) => {
1163
+ const aLatestTime = a.acts.length > 0 ? a.acts[a.acts.length - 1].datetime : '1970-01-01T00:00:00.000Z';
1164
+ const bLatestTime = b.acts.length > 0 ? b.acts[b.acts.length - 1].datetime : '1970-01-01T00:00:00.000Z';
1165
+ return new Date(bLatestTime).getTime() - new Date(aLatestTime).getTime();
1166
+ });
1167
+ // Calculate totals
1168
+ const totalActs = processedPersonas.reduce((sum, persona) => sum + persona.acts.length, 0);
1169
+ // Create final conversations data
1170
+ const conversationsData = {
1171
+ personas: processedPersonas,
1172
+ total_personas: processedPersonas.length,
1173
+ total_acts: totalActs,
1174
+ generated_at: new Date().toISOString()
1175
+ };
1176
+ // Save to YAML file
1177
+ const conversationsPath = `newo_customers/${customer.idn}/conversations.yaml`;
1178
+ const yamlContent = yaml.dump(conversationsData, {
1179
+ indent: 2,
1180
+ quotingType: '"',
1181
+ forceQuotes: false,
1182
+ lineWidth: 120,
1183
+ noRefs: true,
1184
+ sortKeys: false,
1185
+ flowLevel: -1
1186
+ });
1187
+ await writeFileSafe(conversationsPath, yamlContent);
1188
+ if (verbose) {
1189
+ console.log(`✓ Saved conversations to ${conversationsPath}`);
1190
+ console.log(`📊 Summary: ${processedPersonas.length} personas, ${totalActs} total acts`);
1191
+ }
1192
+ }
1193
+ catch (error) {
1194
+ console.error(`❌ Failed to pull conversations for ${customer.idn}:`, error);
1195
+ throw error;
1196
+ }
1197
+ }
1021
1198
  //# sourceMappingURL=sync.js.map
package/dist/types.d.ts CHANGED
@@ -281,4 +281,105 @@ export interface SkillMetadata {
281
281
  created_at?: string;
282
282
  updated_at?: string;
283
283
  }
284
+ export interface Actor {
285
+ readonly id: string;
286
+ readonly conversation_is_active: boolean;
287
+ readonly act_count: number;
288
+ readonly external_id: string;
289
+ readonly integration_idn: string;
290
+ readonly connector_idn: string;
291
+ readonly contact_information: string;
292
+ }
293
+ export interface UserPersona {
294
+ readonly id: string;
295
+ readonly name: string;
296
+ readonly last_session_id: string;
297
+ readonly session_is_active: boolean;
298
+ readonly last_act_time: string;
299
+ readonly last_act_text: string;
300
+ readonly act_count: number;
301
+ readonly actors: readonly Actor[];
302
+ readonly not_found: boolean;
303
+ }
304
+ export interface ConversationAct {
305
+ readonly id: string;
306
+ readonly command_act_id: string | null;
307
+ readonly external_event_id: string;
308
+ readonly arguments: readonly any[];
309
+ readonly reference_idn: string;
310
+ readonly runtime_context_id: string;
311
+ readonly source_text: string;
312
+ readonly original_text: string;
313
+ readonly datetime: string;
314
+ readonly user_actor_id: string;
315
+ readonly agent_actor_id: string | null;
316
+ readonly user_persona_id: string;
317
+ readonly user_persona_name: string;
318
+ readonly agent_persona_id: string;
319
+ readonly external_id: string | null;
320
+ readonly integration_idn: string;
321
+ readonly connector_idn: string;
322
+ readonly to_integration_idn: string | null;
323
+ readonly to_connector_idn: string | null;
324
+ readonly is_agent: boolean;
325
+ readonly project_idn: string | null;
326
+ readonly flow_idn: string;
327
+ readonly skill_idn: string;
328
+ readonly session_id: string;
329
+ readonly recordings: readonly any[];
330
+ readonly contact_information: string | null;
331
+ }
332
+ export interface UserPersonaResponse {
333
+ readonly items: readonly UserPersona[];
334
+ readonly metadata: {
335
+ readonly page: number;
336
+ readonly per: number;
337
+ readonly total: number;
338
+ };
339
+ }
340
+ export interface ChatHistoryParams {
341
+ readonly user_actor_id: string;
342
+ readonly agent_actor_id?: string;
343
+ readonly page?: number;
344
+ readonly per?: number;
345
+ }
346
+ export interface ChatHistoryResponse {
347
+ readonly items: readonly any[];
348
+ readonly metadata?: {
349
+ readonly page: number;
350
+ readonly per: number;
351
+ readonly total: number;
352
+ };
353
+ }
354
+ export interface ConversationOptions {
355
+ readonly includeAll?: boolean;
356
+ readonly connectors?: string[];
357
+ readonly fromDate?: string;
358
+ readonly toDate?: string;
359
+ readonly fields?: string[];
360
+ readonly maxPersonas?: number | undefined;
361
+ readonly maxActsPerPersona?: number | undefined;
362
+ }
363
+ export interface ProcessedAct {
364
+ readonly datetime: string;
365
+ readonly type: string;
366
+ readonly message: string;
367
+ readonly contact_information?: string | null;
368
+ readonly flow_idn?: string;
369
+ readonly skill_idn?: string;
370
+ readonly session_id?: string;
371
+ }
372
+ export interface ProcessedPersona {
373
+ readonly id: string;
374
+ readonly name: string;
375
+ readonly phone: string | null;
376
+ readonly act_count: number;
377
+ readonly acts: readonly ProcessedAct[];
378
+ }
379
+ export interface ConversationsData {
380
+ readonly personas: readonly ProcessedPersona[];
381
+ readonly total_personas: number;
382
+ readonly total_acts: number;
383
+ readonly generated_at: string;
384
+ }
284
385
  //# sourceMappingURL=types.d.ts.map
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "newo",
3
- "version": "1.8.0",
4
- "description": "NEWO CLI: comprehensive sync for AI Agent skills, customer attributes, and metadata between NEWO platform and local files. Multi-customer workspaces, complete change tracking, Git-first workflows.",
3
+ "version": "1.9.1",
4
+ "description": "NEWO CLI: comprehensive sync for AI Agent skills, customer attributes, conversations, and metadata between NEWO platform and local files. Multi-customer workspaces, conversation history extraction, complete change tracking, Git-first workflows.",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "newo": "dist/cli.js"
@@ -26,7 +26,10 @@
26
26
  "knowledge-base",
27
27
  "import",
28
28
  "multi-project",
29
- "workspace"
29
+ "workspace",
30
+ "conversations",
31
+ "chat-history",
32
+ "personas"
30
33
  ],
31
34
  "author": "sabbah13",
32
35
  "license": "MIT",
@@ -71,6 +74,8 @@
71
74
  "pull": "npm run build && node ./dist/cli.js pull",
72
75
  "push": "npm run build && node ./dist/cli.js push",
73
76
  "status": "npm run build && node ./dist/cli.js status",
77
+ "conversations": "npm run build && node ./dist/cli.js conversations",
78
+ "conversations:all": "npm run build && node ./dist/cli.js conversations --all",
74
79
  "clean": "rm -rf dist coverage",
75
80
  "typecheck": "tsc --noEmit",
76
81
  "lint": "tsc --noEmit --strict",
package/src/api.ts CHANGED
@@ -10,7 +10,11 @@ import type {
10
10
  AkbImportArticle,
11
11
  CustomerProfile,
12
12
  CustomerAttribute,
13
- CustomerAttributesResponse
13
+ CustomerAttributesResponse,
14
+ UserPersonaResponse,
15
+ UserPersona,
16
+ ChatHistoryParams,
17
+ ChatHistoryResponse
14
18
  } from './types.js';
15
19
 
16
20
  // Per-request retry tracking to avoid shared state issues
@@ -152,4 +156,42 @@ export async function updateCustomerAttribute(client: AxiosInstance, attribute:
152
156
  possible_values: attribute.possible_values,
153
157
  value_type: attribute.value_type
154
158
  });
159
+ }
160
+
161
+ // Conversation API Functions
162
+
163
+ export async function listUserPersonas(client: AxiosInstance, page: number = 1, per: number = 50): Promise<UserPersonaResponse> {
164
+ const response = await client.get<UserPersonaResponse>('/api/v1/bff/conversations/user-personas', {
165
+ params: { page, per }
166
+ });
167
+ return response.data;
168
+ }
169
+
170
+ export async function getUserPersona(client: AxiosInstance, personaId: string): Promise<UserPersona> {
171
+ const response = await client.get<UserPersona>(`/api/v1/bff/conversations/user-personas/${personaId}`);
172
+ return response.data;
173
+ }
174
+
175
+ export async function getAccount(client: AxiosInstance): Promise<{ id: string; [key: string]: any }> {
176
+ const response = await client.get<{ id: string; [key: string]: any }>('/api/v1/account');
177
+ return response.data;
178
+ }
179
+
180
+
181
+ export async function getChatHistory(client: AxiosInstance, params: ChatHistoryParams): Promise<ChatHistoryResponse> {
182
+ const queryParams: Record<string, any> = {
183
+ user_actor_id: params.user_actor_id,
184
+ page: params.page || 1,
185
+ per: params.per || 50
186
+ };
187
+
188
+ // Only add agent_actor_id if provided
189
+ if (params.agent_actor_id) {
190
+ queryParams.agent_actor_id = params.agent_actor_id;
191
+ }
192
+
193
+ const response = await client.get<ChatHistoryResponse>('/api/v1/chat/history', {
194
+ params: queryParams
195
+ });
196
+ return response.data;
155
197
  }
package/src/cli.ts 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, saveCustomerAttributes } from './sync.js';
5
+ import { pullAll, pushChanged, status, saveCustomerAttributes, pullConversations } 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';
@@ -168,12 +168,14 @@ Usage:
168
168
  newo pull [--customer <idn>] # download projects -> ./newo_customers/<idn>/projects/
169
169
  newo push [--customer <idn>] # upload modified *.guidance/*.jinja back to NEWO
170
170
  newo status [--customer <idn>] # show modified files
171
+ newo conversations [--customer <idn>] [--all] # download user conversations -> ./newo_customers/<idn>/conversations.yaml
171
172
  newo list-customers # list available customers
172
173
  newo meta [--customer <idn>] # get project metadata (debug)
173
174
  newo import-akb <file> <persona_id> [--customer <idn>] # import AKB articles from file
174
175
 
175
176
  Flags:
176
177
  --customer <idn> # specify customer (if not set, uses default or interactive selection)
178
+ --all # include all available data (for conversations: all personas and acts)
177
179
  --verbose, -v # enable detailed logging
178
180
 
179
181
  Environment Variables:
@@ -352,6 +354,53 @@ File Structure:
352
354
  return;
353
355
  }
354
356
 
357
+ if (cmd === 'conversations') {
358
+ // Handle customer selection for conversations command
359
+ if (args.customer) {
360
+ const customer = getCustomer(customerConfig, args.customer as string);
361
+ if (!customer) {
362
+ console.error(`Unknown customer: ${args.customer}`);
363
+ console.error(`Available customers: ${listCustomers(customerConfig).join(', ')}`);
364
+ process.exit(1);
365
+ }
366
+ selectedCustomer = customer;
367
+ } else {
368
+ // Try to get default, fall back to all customers
369
+ selectedCustomer = tryGetDefaultCustomer(customerConfig);
370
+ if (!selectedCustomer) {
371
+ allCustomers = getAllCustomers(customerConfig);
372
+ if (verbose) console.log(`💬 No default customer specified, pulling conversations from all ${allCustomers.length} customers`);
373
+ }
374
+ }
375
+
376
+ // Parse conversation-specific options - load all data by default
377
+ const conversationOptions = {
378
+ includeAll: true, // Always include all data for conversations
379
+ maxPersonas: undefined, // No limit on personas
380
+ maxActsPerPersona: undefined // No limit on acts per persona
381
+ };
382
+
383
+ if (selectedCustomer) {
384
+ // Single customer conversations
385
+ const accessToken = await getValidAccessToken(selectedCustomer);
386
+ const client = await makeClient(verbose, accessToken);
387
+ console.log(`💬 Pulling conversations for customer: ${selectedCustomer.idn} (all data)`);
388
+ await pullConversations(client, selectedCustomer, conversationOptions, verbose);
389
+ console.log(`✅ Conversations saved to newo_customers/${selectedCustomer.idn}/conversations.yaml`);
390
+ } else if (allCustomers.length > 0) {
391
+ // Multi-customer conversations
392
+ console.log(`💬 Pulling conversations from ${allCustomers.length} customers (all data)...`);
393
+ for (const customer of allCustomers) {
394
+ console.log(`\n💬 Pulling conversations for customer: ${customer.idn}`);
395
+ const accessToken = await getValidAccessToken(customer);
396
+ const client = await makeClient(verbose, accessToken);
397
+ await pullConversations(client, customer, conversationOptions, verbose);
398
+ }
399
+ console.log(`\n✅ Conversations pull completed for all ${allCustomers.length} customers`);
400
+ }
401
+ return;
402
+ }
403
+
355
404
  // For all other commands, require a single selected customer
356
405
  if (args.customer) {
357
406
  const customer = getCustomer(customerConfig, args.customer as string);
package/src/sync.ts CHANGED
@@ -7,7 +7,9 @@ import {
7
7
  listFlowStates,
8
8
  getProjectMeta,
9
9
  getCustomerAttributes,
10
- updateCustomerAttribute
10
+ updateCustomerAttribute,
11
+ listUserPersonas,
12
+ getChatHistory
11
13
  } from './api.js';
12
14
  import {
13
15
  ensureState,
@@ -49,7 +51,13 @@ import type {
49
51
  SkillMetadata,
50
52
  FlowEvent,
51
53
  FlowState,
52
- CustomerAttribute
54
+ CustomerAttribute,
55
+ UserPersona,
56
+ ConversationAct,
57
+ ConversationOptions,
58
+ ConversationsData,
59
+ ProcessedPersona,
60
+ ProcessedAct
53
61
  } from './types.js';
54
62
 
55
63
  // Concurrency limits for API operations
@@ -1171,4 +1179,211 @@ async function generateFlowsYaml(
1171
1179
  const yamlPath = flowsYamlPath(customer.idn);
1172
1180
  await writeFileSafe(yamlPath, yamlContent);
1173
1181
  console.log(`✓ Generated flows.yaml`);
1182
+ }
1183
+
1184
+ // Conversation sync functions
1185
+
1186
+ export async function pullConversations(
1187
+ client: AxiosInstance,
1188
+ customer: CustomerConfig,
1189
+ options: ConversationOptions = {},
1190
+ verbose: boolean = false
1191
+ ): Promise<void> {
1192
+ if (verbose) console.log(`💬 Fetching conversations for customer ${customer.idn}...`);
1193
+
1194
+ try {
1195
+ // Get all user personas with pagination
1196
+ const allPersonas: UserPersona[] = [];
1197
+ let page = 1;
1198
+ const perPage = 50;
1199
+ let hasMore = true;
1200
+
1201
+ while (hasMore) {
1202
+ const response = await listUserPersonas(client, page, perPage);
1203
+ allPersonas.push(...response.items);
1204
+
1205
+ if (verbose) console.log(`📋 Page ${page}: Found ${response.items.length} personas (${allPersonas.length}/${response.metadata.total} total)`);
1206
+
1207
+ hasMore = response.items.length === perPage && allPersonas.length < response.metadata.total;
1208
+ page++;
1209
+ }
1210
+
1211
+ if (options.maxPersonas && allPersonas.length > options.maxPersonas) {
1212
+ allPersonas.splice(options.maxPersonas);
1213
+ if (verbose) console.log(`⚠️ Limited to ${options.maxPersonas} personas as requested`);
1214
+ }
1215
+
1216
+ if (verbose) console.log(`👥 Processing ${allPersonas.length} personas...`);
1217
+
1218
+ // Process personas concurrently with limited concurrency
1219
+ const processedPersonas: ProcessedPersona[] = [];
1220
+
1221
+ await Promise.all(allPersonas.map(persona => concurrencyLimit(async () => {
1222
+ try {
1223
+ // Extract phone number from actors
1224
+ const phoneActor = persona.actors.find(actor =>
1225
+ actor.integration_idn === 'newo_voice' &&
1226
+ actor.connector_idn === 'newo_voice_connector' &&
1227
+ actor.contact_information?.startsWith('+')
1228
+ );
1229
+ const phone = phoneActor?.contact_information || null;
1230
+
1231
+ // Get acts for this persona
1232
+ const allActs: ConversationAct[] = [];
1233
+ let actPage = 1;
1234
+ const actsPerPage = 100; // Higher limit for acts
1235
+ let hasMoreActs = true;
1236
+
1237
+ while (hasMoreActs) {
1238
+ // First try the chat history API as alternative
1239
+ try {
1240
+ // Get user actor IDs from persona actors
1241
+ const userActors = persona.actors.filter(actor =>
1242
+ actor.integration_idn === 'newo_voice' &&
1243
+ actor.connector_idn === 'newo_voice_connector'
1244
+ );
1245
+
1246
+ if (userActors.length > 0) {
1247
+ const chatHistoryParams = {
1248
+ user_actor_id: userActors[0]!.id,
1249
+ page: actPage,
1250
+ per: actsPerPage
1251
+ };
1252
+
1253
+ const chatResponse = await getChatHistory(client, chatHistoryParams);
1254
+ if (chatResponse.items && chatResponse.items.length > 0) {
1255
+ // Convert chat history format to acts format - create minimal ConversationAct objects
1256
+ const convertedActs: ConversationAct[] = chatResponse.items.map((item: any) => ({
1257
+ id: item.id || `chat_${Math.random()}`,
1258
+ command_act_id: null,
1259
+ external_event_id: item.external_event_id || 'chat_history',
1260
+ arguments: [],
1261
+ reference_idn: (item.is_agent === true) ? 'agent_message' : 'user_message',
1262
+ runtime_context_id: item.runtime_context_id || 'chat_history',
1263
+ source_text: item.payload?.text || item.message || item.content || item.text || '',
1264
+ original_text: item.payload?.text || item.message || item.content || item.text || '',
1265
+ datetime: item.datetime || item.created_at || item.timestamp || new Date().toISOString(),
1266
+ user_actor_id: userActors[0]!.id,
1267
+ agent_actor_id: null,
1268
+ user_persona_id: persona.id,
1269
+ user_persona_name: persona.name,
1270
+ agent_persona_id: item.agent_persona_id || 'unknown',
1271
+ external_id: item.external_id || null,
1272
+ integration_idn: 'newo_voice',
1273
+ connector_idn: 'newo_voice_connector',
1274
+ to_integration_idn: null,
1275
+ to_connector_idn: null,
1276
+ is_agent: Boolean(item.is_agent === true),
1277
+ project_idn: null,
1278
+ flow_idn: item.flow_idn || 'unknown',
1279
+ skill_idn: item.skill_idn || 'unknown',
1280
+ session_id: item.session_id || 'unknown',
1281
+ recordings: item.recordings || [],
1282
+ contact_information: item.contact_information || null
1283
+ }));
1284
+
1285
+ allActs.push(...convertedActs);
1286
+
1287
+ if (verbose && convertedActs.length > 0) {
1288
+ console.log(` 👤 ${persona.name}: Chat History - ${convertedActs.length} messages (${allActs.length} total)`);
1289
+ }
1290
+
1291
+ hasMoreActs = chatResponse.items.length === actsPerPage &&
1292
+ chatResponse.metadata?.total !== undefined &&
1293
+ allActs.length < chatResponse.metadata.total;
1294
+ actPage++;
1295
+ continue; // Skip the original acts API call
1296
+ }
1297
+ }
1298
+ } catch (chatError) {
1299
+ if (verbose) console.log(` ⚠️ Chat history failed for ${persona.name}: ${chatError instanceof Error ? chatError.message : String(chatError)}`);
1300
+ // No fallback - only use chat history API
1301
+ hasMoreActs = false;
1302
+ }
1303
+ }
1304
+
1305
+ // Sort acts by datetime ascending (chronological order)
1306
+ allActs.sort((a, b) => new Date(a.datetime).getTime() - new Date(b.datetime).getTime());
1307
+
1308
+ // Process acts into simplified format - exclude redundant fields
1309
+ const processedActs: ProcessedAct[] = allActs.map(act => {
1310
+ const processedAct: ProcessedAct = {
1311
+ datetime: act.datetime,
1312
+ type: act.reference_idn,
1313
+ message: act.source_text
1314
+ };
1315
+
1316
+ // Only include non-redundant fields
1317
+ if (act.contact_information) {
1318
+ (processedAct as any).contact_information = act.contact_information;
1319
+ }
1320
+ if (act.flow_idn && act.flow_idn !== 'unknown') {
1321
+ (processedAct as any).flow_idn = act.flow_idn;
1322
+ }
1323
+ if (act.skill_idn && act.skill_idn !== 'unknown') {
1324
+ (processedAct as any).skill_idn = act.skill_idn;
1325
+ }
1326
+ if (act.session_id && act.session_id !== 'unknown') {
1327
+ (processedAct as any).session_id = act.session_id;
1328
+ }
1329
+
1330
+ return processedAct;
1331
+ });
1332
+
1333
+ processedPersonas.push({
1334
+ id: persona.id,
1335
+ name: persona.name,
1336
+ phone,
1337
+ act_count: persona.act_count,
1338
+ acts: processedActs
1339
+ });
1340
+
1341
+ if (verbose) console.log(` ✓ Processed ${persona.name}: ${processedActs.length} acts`);
1342
+ } catch (error) {
1343
+ console.error(`❌ Failed to process persona ${persona.name}:`, error);
1344
+ // Continue with other personas
1345
+ }
1346
+ })));
1347
+
1348
+ // Sort personas by most recent act time (descending) - use latest act from acts array
1349
+ processedPersonas.sort((a, b) => {
1350
+ const aLatestTime = a.acts.length > 0 ? a.acts[a.acts.length - 1]!.datetime : '1970-01-01T00:00:00.000Z';
1351
+ const bLatestTime = b.acts.length > 0 ? b.acts[b.acts.length - 1]!.datetime : '1970-01-01T00:00:00.000Z';
1352
+ return new Date(bLatestTime).getTime() - new Date(aLatestTime).getTime();
1353
+ });
1354
+
1355
+ // Calculate totals
1356
+ const totalActs = processedPersonas.reduce((sum, persona) => sum + persona.acts.length, 0);
1357
+
1358
+ // Create final conversations data
1359
+ const conversationsData: ConversationsData = {
1360
+ personas: processedPersonas,
1361
+ total_personas: processedPersonas.length,
1362
+ total_acts: totalActs,
1363
+ generated_at: new Date().toISOString()
1364
+ };
1365
+
1366
+ // Save to YAML file
1367
+ const conversationsPath = `newo_customers/${customer.idn}/conversations.yaml`;
1368
+ const yamlContent = yaml.dump(conversationsData, {
1369
+ indent: 2,
1370
+ quotingType: '"',
1371
+ forceQuotes: false,
1372
+ lineWidth: 120,
1373
+ noRefs: true,
1374
+ sortKeys: false,
1375
+ flowLevel: -1
1376
+ });
1377
+
1378
+ await writeFileSafe(conversationsPath, yamlContent);
1379
+
1380
+ if (verbose) {
1381
+ console.log(`✓ Saved conversations to ${conversationsPath}`);
1382
+ console.log(`📊 Summary: ${processedPersonas.length} personas, ${totalActs} total acts`);
1383
+ }
1384
+
1385
+ } catch (error) {
1386
+ console.error(`❌ Failed to pull conversations for ${customer.idn}:`, error);
1387
+ throw error;
1388
+ }
1174
1389
  }
package/src/types.ts CHANGED
@@ -336,4 +336,118 @@ export interface SkillMetadata {
336
336
  path?: string;
337
337
  created_at?: string;
338
338
  updated_at?: string;
339
+ }
340
+
341
+ // Conversation Types
342
+ export interface Actor {
343
+ readonly id: string;
344
+ readonly conversation_is_active: boolean;
345
+ readonly act_count: number;
346
+ readonly external_id: string;
347
+ readonly integration_idn: string;
348
+ readonly connector_idn: string;
349
+ readonly contact_information: string;
350
+ }
351
+
352
+ export interface UserPersona {
353
+ readonly id: string;
354
+ readonly name: string;
355
+ readonly last_session_id: string;
356
+ readonly session_is_active: boolean;
357
+ readonly last_act_time: string;
358
+ readonly last_act_text: string;
359
+ readonly act_count: number;
360
+ readonly actors: readonly Actor[];
361
+ readonly not_found: boolean;
362
+ }
363
+
364
+ export interface ConversationAct {
365
+ readonly id: string;
366
+ readonly command_act_id: string | null;
367
+ readonly external_event_id: string;
368
+ readonly arguments: readonly any[];
369
+ readonly reference_idn: string;
370
+ readonly runtime_context_id: string;
371
+ readonly source_text: string;
372
+ readonly original_text: string;
373
+ readonly datetime: string;
374
+ readonly user_actor_id: string;
375
+ readonly agent_actor_id: string | null;
376
+ readonly user_persona_id: string;
377
+ readonly user_persona_name: string;
378
+ readonly agent_persona_id: string;
379
+ readonly external_id: string | null;
380
+ readonly integration_idn: string;
381
+ readonly connector_idn: string;
382
+ readonly to_integration_idn: string | null;
383
+ readonly to_connector_idn: string | null;
384
+ readonly is_agent: boolean;
385
+ readonly project_idn: string | null;
386
+ readonly flow_idn: string;
387
+ readonly skill_idn: string;
388
+ readonly session_id: string;
389
+ readonly recordings: readonly any[];
390
+ readonly contact_information: string | null;
391
+ }
392
+
393
+ export interface UserPersonaResponse {
394
+ readonly items: readonly UserPersona[];
395
+ readonly metadata: {
396
+ readonly page: number;
397
+ readonly per: number;
398
+ readonly total: number;
399
+ };
400
+ }
401
+
402
+
403
+ export interface ChatHistoryParams {
404
+ readonly user_actor_id: string;
405
+ readonly agent_actor_id?: string;
406
+ readonly page?: number;
407
+ readonly per?: number;
408
+ }
409
+
410
+ export interface ChatHistoryResponse {
411
+ readonly items: readonly any[]; // We'll define this after seeing the response structure
412
+ readonly metadata?: {
413
+ readonly page: number;
414
+ readonly per: number;
415
+ readonly total: number;
416
+ };
417
+ }
418
+
419
+ export interface ConversationOptions {
420
+ readonly includeAll?: boolean;
421
+ readonly connectors?: string[];
422
+ readonly fromDate?: string;
423
+ readonly toDate?: string;
424
+ readonly fields?: string[];
425
+ readonly maxPersonas?: number | undefined;
426
+ readonly maxActsPerPersona?: number | undefined;
427
+ }
428
+
429
+ // Processed conversation data for YAML output
430
+ export interface ProcessedAct {
431
+ readonly datetime: string;
432
+ readonly type: string;
433
+ readonly message: string;
434
+ readonly contact_information?: string | null;
435
+ readonly flow_idn?: string;
436
+ readonly skill_idn?: string;
437
+ readonly session_id?: string;
438
+ }
439
+
440
+ export interface ProcessedPersona {
441
+ readonly id: string;
442
+ readonly name: string;
443
+ readonly phone: string | null;
444
+ readonly act_count: number;
445
+ readonly acts: readonly ProcessedAct[];
446
+ }
447
+
448
+ export interface ConversationsData {
449
+ readonly personas: readonly ProcessedPersona[];
450
+ readonly total_personas: number;
451
+ readonly total_acts: number;
452
+ readonly generated_at: string;
339
453
  }