newo 3.6.2 → 3.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 +44 -3
- package/README.md +61 -0
- package/dist/cli/commands/check.d.ts +3 -0
- package/dist/cli/commands/check.js +15 -0
- package/dist/cli/commands/format.d.ts +3 -0
- package/dist/cli/commands/format.js +105 -0
- package/dist/cli/commands/help.js +13 -0
- package/dist/cli/commands/lint.d.ts +3 -0
- package/dist/cli/commands/lint.js +195 -0
- package/dist/cli-new/di/tokens.d.ts +1 -1
- package/dist/cli.js +45 -9
- package/dist/domain/strategies/sync/AttributeSyncStrategy.js +38 -8
- package/dist/lint/config.d.ts +4 -0
- package/dist/lint/config.js +14 -0
- package/dist/lint/discovery.d.ts +34 -0
- package/dist/lint/discovery.js +112 -0
- package/dist/lint/live-schema.d.ts +20 -0
- package/dist/lint/live-schema.js +52 -0
- package/dist/lint/reporters/index.d.ts +4 -0
- package/dist/lint/reporters/index.js +19 -0
- package/dist/lint/reporters/json.d.ts +3 -0
- package/dist/lint/reporters/json.js +6 -0
- package/dist/lint/reporters/sarif.d.ts +3 -0
- package/dist/lint/reporters/sarif.js +47 -0
- package/dist/lint/reporters/text.d.ts +3 -0
- package/dist/lint/reporters/text.js +51 -0
- package/dist/lint/reporters/types.d.ts +6 -0
- package/dist/lint/reporters/types.js +2 -0
- package/dist/sync/attributes.js +38 -12
- package/dist/sync/conversations.d.ts +1 -1
- package/dist/sync/conversations.js +240 -193
- package/dist/sync/json-attr-utils.d.ts +67 -0
- package/dist/sync/json-attr-utils.js +98 -0
- package/package.json +3 -1
- package/src/cli/commands/check.ts +21 -0
- package/src/cli/commands/format.ts +131 -0
- package/src/cli/commands/help.ts +13 -0
- package/src/cli/commands/lint.ts +246 -0
- package/src/cli.ts +50 -9
- package/src/domain/strategies/sync/AttributeSyncStrategy.ts +45 -8
- package/src/lint/config.ts +17 -0
- package/src/lint/discovery.ts +148 -0
- package/src/lint/live-schema.ts +62 -0
- package/src/lint/reporters/index.ts +22 -0
- package/src/lint/reporters/json.ts +12 -0
- package/src/lint/reporters/sarif.ts +59 -0
- package/src/lint/reporters/text.ts +58 -0
- package/src/lint/reporters/types.ts +7 -0
- package/src/sync/attributes.ts +43 -14
- package/src/sync/conversations.ts +265 -212
- package/src/sync/json-attr-utils.ts +95 -0
|
@@ -1,218 +1,265 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Conversations synchronization module
|
|
3
|
+
*
|
|
4
|
+
* Incremental/resumable conversation pull:
|
|
5
|
+
* - Writes per-persona JSON files to newo_customers/<idn>/conversations/<persona_id>.json as they arrive
|
|
6
|
+
* - Updates conversations.yaml aggregate after each persona finishes
|
|
7
|
+
* - Skips personas already fully fetched (resume support) unless --force passed via env NEWO_CONV_FORCE=1
|
|
8
|
+
* - Graceful on partial failure: individual persona errors do not abort the batch, state is preserved
|
|
3
9
|
*/
|
|
4
10
|
import { listUserPersonas, getChatHistory } from '../api.js';
|
|
5
11
|
import { writeFileSafe } from '../fsutil.js';
|
|
12
|
+
import fs from 'fs-extra';
|
|
13
|
+
import path from 'path';
|
|
6
14
|
import yaml from 'js-yaml';
|
|
7
15
|
import pLimit from 'p-limit';
|
|
8
|
-
// Concurrency limit for API calls
|
|
9
16
|
const concurrencyLimit = pLimit(5);
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
17
|
+
function personaFilePath(customerIdn, personaId) {
|
|
18
|
+
return path.join('newo_customers', customerIdn, 'conversations', `${personaId}.json`);
|
|
19
|
+
}
|
|
20
|
+
function aggregateYamlPath(customerIdn) {
|
|
21
|
+
return path.join('newo_customers', customerIdn, 'conversations.yaml');
|
|
22
|
+
}
|
|
23
|
+
async function readPersonaState(customerIdn, personaId) {
|
|
24
|
+
const p = personaFilePath(customerIdn, personaId);
|
|
25
|
+
if (!(await fs.pathExists(p)))
|
|
26
|
+
return null;
|
|
16
27
|
try {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
28
|
+
return JSON.parse(await fs.readFile(p, 'utf8'));
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
async function writePersonaState(customerIdn, state) {
|
|
35
|
+
await writeFileSafe(personaFilePath(customerIdn, state.id), JSON.stringify(state, null, 2));
|
|
36
|
+
}
|
|
37
|
+
async function writeAggregateYaml(customerIdn) {
|
|
38
|
+
const dir = path.join('newo_customers', customerIdn, 'conversations');
|
|
39
|
+
const files = (await fs.pathExists(dir)) ? await fs.readdir(dir) : [];
|
|
40
|
+
const personas = [];
|
|
41
|
+
for (const f of files) {
|
|
42
|
+
if (!f.endsWith('.json'))
|
|
43
|
+
continue;
|
|
44
|
+
try {
|
|
45
|
+
const state = JSON.parse(await fs.readFile(path.join(dir, f), 'utf8'));
|
|
46
|
+
personas.push({
|
|
47
|
+
id: state.id,
|
|
48
|
+
name: state.name,
|
|
49
|
+
phone: state.phone,
|
|
50
|
+
act_count: state.act_count,
|
|
51
|
+
acts: state.acts
|
|
52
|
+
});
|
|
29
53
|
}
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
if (verbose)
|
|
33
|
-
console.log(`⚠️ Limited to ${options.maxPersonas} personas as requested`);
|
|
54
|
+
catch {
|
|
55
|
+
// skip corrupted file
|
|
34
56
|
}
|
|
57
|
+
}
|
|
58
|
+
personas.sort((a, b) => {
|
|
59
|
+
const aLatestTime = a.acts.length > 0 ? a.acts[a.acts.length - 1].datetime : '1970-01-01T00:00:00.000Z';
|
|
60
|
+
const bLatestTime = b.acts.length > 0 ? b.acts[b.acts.length - 1].datetime : '1970-01-01T00:00:00.000Z';
|
|
61
|
+
return new Date(bLatestTime).getTime() - new Date(aLatestTime).getTime();
|
|
62
|
+
});
|
|
63
|
+
const totalActs = personas.reduce((sum, p) => sum + p.acts.length, 0);
|
|
64
|
+
const data = {
|
|
65
|
+
personas,
|
|
66
|
+
total_personas: personas.length,
|
|
67
|
+
total_acts: totalActs,
|
|
68
|
+
generated_at: new Date().toISOString()
|
|
69
|
+
};
|
|
70
|
+
const yamlContent = yaml.dump(data, {
|
|
71
|
+
indent: 2,
|
|
72
|
+
quotingType: '"',
|
|
73
|
+
forceQuotes: false,
|
|
74
|
+
lineWidth: 120,
|
|
75
|
+
noRefs: true,
|
|
76
|
+
sortKeys: false,
|
|
77
|
+
flowLevel: -1
|
|
78
|
+
});
|
|
79
|
+
await writeFileSafe(aggregateYamlPath(customerIdn), yamlContent);
|
|
80
|
+
return { personas: personas.length, acts: totalActs };
|
|
81
|
+
}
|
|
82
|
+
function buildProcessedActs(raw) {
|
|
83
|
+
const sorted = [...raw].sort((a, b) => new Date(a.datetime).getTime() - new Date(b.datetime).getTime());
|
|
84
|
+
return sorted.map(act => {
|
|
85
|
+
const processedAct = {
|
|
86
|
+
datetime: act.datetime,
|
|
87
|
+
type: act.reference_idn,
|
|
88
|
+
message: act.source_text
|
|
89
|
+
};
|
|
90
|
+
if (act.contact_information)
|
|
91
|
+
processedAct.contact_information = act.contact_information;
|
|
92
|
+
if (act.flow_idn && act.flow_idn !== 'unknown')
|
|
93
|
+
processedAct.flow_idn = act.flow_idn;
|
|
94
|
+
if (act.skill_idn && act.skill_idn !== 'unknown')
|
|
95
|
+
processedAct.skill_idn = act.skill_idn;
|
|
96
|
+
if (act.session_id && act.session_id !== 'unknown')
|
|
97
|
+
processedAct.session_id = act.session_id;
|
|
98
|
+
return processedAct;
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Pull conversations for a customer and save incrementally.
|
|
103
|
+
*/
|
|
104
|
+
export async function pullConversations(client, customer, options = {}, verbose = false) {
|
|
105
|
+
const force = process.env.NEWO_CONV_FORCE === '1';
|
|
106
|
+
console.log(`💬 Fetching conversations for ${customer.idn}${force ? ' (force re-fetch)' : ' (resume mode)'}...`);
|
|
107
|
+
// Ensure output dirs exist
|
|
108
|
+
await fs.ensureDir(path.join('newo_customers', customer.idn, 'conversations'));
|
|
109
|
+
// 1. Enumerate all personas
|
|
110
|
+
const allPersonas = [];
|
|
111
|
+
let page = 1;
|
|
112
|
+
const perPage = 50;
|
|
113
|
+
let hasMore = true;
|
|
114
|
+
while (hasMore) {
|
|
115
|
+
const response = await listUserPersonas(client, page, perPage);
|
|
116
|
+
allPersonas.push(...response.items);
|
|
35
117
|
if (verbose)
|
|
36
|
-
console.log(
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
118
|
+
console.log(`📋 Page ${page}: ${response.items.length} personas (${allPersonas.length}/${response.metadata.total})`);
|
|
119
|
+
hasMore = response.items.length === perPage && allPersonas.length < response.metadata.total;
|
|
120
|
+
page++;
|
|
121
|
+
}
|
|
122
|
+
if (options.maxPersonas && allPersonas.length > options.maxPersonas) {
|
|
123
|
+
allPersonas.splice(options.maxPersonas);
|
|
124
|
+
}
|
|
125
|
+
const total = allPersonas.length;
|
|
126
|
+
console.log(`👥 Found ${total} personas. Processing with concurrency=5...`);
|
|
127
|
+
let done = 0;
|
|
128
|
+
let skipped = 0;
|
|
129
|
+
let failed = 0;
|
|
130
|
+
await Promise.all(allPersonas.map(persona => concurrencyLimit(async () => {
|
|
131
|
+
try {
|
|
132
|
+
// Resume: skip if already complete
|
|
133
|
+
const existing = await readPersonaState(customer.idn, persona.id);
|
|
134
|
+
if (!force && existing && existing.complete) {
|
|
135
|
+
skipped++;
|
|
136
|
+
done++;
|
|
137
|
+
if (verbose)
|
|
138
|
+
console.log(`⏭️ [${done}/${total}] ${persona.name}: already complete (${existing.acts.length} acts)`);
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
const phoneActor = persona.actors.find(actor => actor.integration_idn === 'newo_voice' &&
|
|
142
|
+
actor.connector_idn === 'newo_voice_connector' &&
|
|
143
|
+
actor.contact_information?.startsWith('+'));
|
|
144
|
+
const phone = phoneActor?.contact_information || null;
|
|
145
|
+
const userActors = persona.actors.filter(actor => actor.integration_idn === 'newo_voice' &&
|
|
146
|
+
actor.connector_idn === 'newo_voice_connector');
|
|
147
|
+
if (userActors.length === 0) {
|
|
148
|
+
const state = {
|
|
149
|
+
id: persona.id,
|
|
150
|
+
name: persona.name,
|
|
151
|
+
phone,
|
|
152
|
+
act_count: persona.act_count,
|
|
153
|
+
acts: [],
|
|
154
|
+
fetched_at: new Date().toISOString(),
|
|
155
|
+
complete: true
|
|
156
|
+
};
|
|
157
|
+
await writePersonaState(customer.idn, state);
|
|
158
|
+
done++;
|
|
159
|
+
if (verbose)
|
|
160
|
+
console.log(`✓ [${done}/${total}] ${persona.name}: no voice actors`);
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
// Fetch acts paginated
|
|
164
|
+
const allActs = [];
|
|
165
|
+
let actPage = 1;
|
|
166
|
+
const actsPerPage = 100;
|
|
167
|
+
let hasMoreActs = true;
|
|
168
|
+
const maxPages = 50;
|
|
169
|
+
let lastError;
|
|
170
|
+
while (hasMoreActs && actPage <= maxPages) {
|
|
171
|
+
try {
|
|
172
|
+
const chatResponse = await getChatHistory(client, {
|
|
173
|
+
user_actor_id: userActors[0].id,
|
|
174
|
+
page: actPage,
|
|
175
|
+
per: actsPerPage
|
|
64
176
|
});
|
|
65
|
-
if (
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
177
|
+
if (chatResponse.items && chatResponse.items.length > 0) {
|
|
178
|
+
const convertedActs = chatResponse.items.map((item) => ({
|
|
179
|
+
id: item.id || `chat_${Math.random()}`,
|
|
180
|
+
command_act_id: null,
|
|
181
|
+
external_event_id: item.external_event_id || 'chat_history',
|
|
182
|
+
arguments: [],
|
|
183
|
+
reference_idn: (item.is_agent === true) ? 'agent_message' : 'user_message',
|
|
184
|
+
runtime_context_id: item.runtime_context_id || 'chat_history',
|
|
185
|
+
source_text: item.payload?.text || item.message || item.content || item.text || '',
|
|
186
|
+
original_text: item.payload?.text || item.message || item.content || item.text || '',
|
|
187
|
+
datetime: item.datetime || item.created_at || item.timestamp || new Date().toISOString(),
|
|
74
188
|
user_actor_id: userActors[0].id,
|
|
75
|
-
|
|
76
|
-
|
|
189
|
+
agent_actor_id: null,
|
|
190
|
+
user_persona_id: persona.id,
|
|
191
|
+
user_persona_name: persona.name,
|
|
192
|
+
agent_persona_id: item.agent_persona_id || 'unknown',
|
|
193
|
+
external_id: item.external_id || null,
|
|
194
|
+
integration_idn: 'newo_voice',
|
|
195
|
+
connector_idn: 'newo_voice_connector',
|
|
196
|
+
to_integration_idn: null,
|
|
197
|
+
to_connector_idn: null,
|
|
198
|
+
is_agent: Boolean(item.is_agent === true),
|
|
199
|
+
project_idn: null,
|
|
200
|
+
flow_idn: item.flow_idn || 'unknown',
|
|
201
|
+
skill_idn: item.skill_idn || 'unknown',
|
|
202
|
+
session_id: item.session_id || 'unknown',
|
|
203
|
+
recordings: item.recordings || [],
|
|
204
|
+
contact_information: item.contact_information || null
|
|
205
|
+
}));
|
|
206
|
+
allActs.push(...convertedActs);
|
|
207
|
+
// Save partial progress every page
|
|
208
|
+
const partialState = {
|
|
209
|
+
id: persona.id,
|
|
210
|
+
name: persona.name,
|
|
211
|
+
phone,
|
|
212
|
+
act_count: persona.act_count,
|
|
213
|
+
acts: buildProcessedActs(allActs),
|
|
214
|
+
fetched_at: new Date().toISOString(),
|
|
215
|
+
complete: false
|
|
77
216
|
};
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
// Convert chat history format to acts format - create minimal ConversationAct objects
|
|
83
|
-
const convertedActs = chatResponse.items.map((item) => ({
|
|
84
|
-
id: item.id || `chat_${Math.random()}`,
|
|
85
|
-
command_act_id: null,
|
|
86
|
-
external_event_id: item.external_event_id || 'chat_history',
|
|
87
|
-
arguments: [],
|
|
88
|
-
reference_idn: (item.is_agent === true) ? 'agent_message' : 'user_message',
|
|
89
|
-
runtime_context_id: item.runtime_context_id || 'chat_history',
|
|
90
|
-
source_text: item.payload?.text || item.message || item.content || item.text || '',
|
|
91
|
-
original_text: item.payload?.text || item.message || item.content || item.text || '',
|
|
92
|
-
datetime: item.datetime || item.created_at || item.timestamp || new Date().toISOString(),
|
|
93
|
-
user_actor_id: userActors[0].id,
|
|
94
|
-
agent_actor_id: null,
|
|
95
|
-
user_persona_id: persona.id,
|
|
96
|
-
user_persona_name: persona.name,
|
|
97
|
-
agent_persona_id: item.agent_persona_id || 'unknown',
|
|
98
|
-
external_id: item.external_id || null,
|
|
99
|
-
integration_idn: 'newo_voice',
|
|
100
|
-
connector_idn: 'newo_voice_connector',
|
|
101
|
-
to_integration_idn: null,
|
|
102
|
-
to_connector_idn: null,
|
|
103
|
-
is_agent: Boolean(item.is_agent === true),
|
|
104
|
-
project_idn: null,
|
|
105
|
-
flow_idn: item.flow_idn || 'unknown',
|
|
106
|
-
skill_idn: item.skill_idn || 'unknown',
|
|
107
|
-
session_id: item.session_id || 'unknown',
|
|
108
|
-
recordings: item.recordings || [],
|
|
109
|
-
contact_information: item.contact_information || null
|
|
110
|
-
}));
|
|
111
|
-
allActs.push(...convertedActs);
|
|
112
|
-
if (verbose && convertedActs.length > 0) {
|
|
113
|
-
console.log(` 👤 ${persona.name}: Chat History - ${convertedActs.length} messages (${allActs.length} total)`);
|
|
114
|
-
}
|
|
115
|
-
// Check if we should continue paginating
|
|
116
|
-
const hasMetadata = chatResponse.metadata?.total !== undefined;
|
|
117
|
-
const currentTotal = chatResponse.metadata?.total || 0;
|
|
118
|
-
hasMoreActs = chatResponse.items.length === actsPerPage &&
|
|
119
|
-
hasMetadata &&
|
|
120
|
-
allActs.length < currentTotal;
|
|
121
|
-
actPage++;
|
|
122
|
-
if (verbose)
|
|
123
|
-
console.log(` 📊 ${persona.name}: Page ${actPage - 1} done, ${allActs.length}/${currentTotal} total acts`);
|
|
124
|
-
}
|
|
125
|
-
else {
|
|
126
|
-
// No more items
|
|
127
|
-
hasMoreActs = false;
|
|
128
|
-
if (verbose)
|
|
129
|
-
console.log(` 📊 ${persona.name}: No more chat history items`);
|
|
130
|
-
}
|
|
217
|
+
await writePersonaState(customer.idn, partialState);
|
|
218
|
+
const currentTotal = chatResponse.metadata?.total || 0;
|
|
219
|
+
hasMoreActs = chatResponse.items.length === actsPerPage && allActs.length < currentTotal;
|
|
220
|
+
actPage++;
|
|
131
221
|
}
|
|
132
|
-
|
|
133
|
-
if (verbose)
|
|
134
|
-
console.log(` ⚠️ Chat history failed for ${persona.name}: ${chatError instanceof Error ? chatError.message : String(chatError)}`);
|
|
222
|
+
else {
|
|
135
223
|
hasMoreActs = false;
|
|
136
224
|
}
|
|
137
225
|
}
|
|
138
|
-
|
|
226
|
+
catch (chatError) {
|
|
227
|
+
lastError = chatError instanceof Error ? chatError.message : String(chatError);
|
|
139
228
|
if (verbose)
|
|
140
|
-
console.log(
|
|
229
|
+
console.log(`⚠️ ${persona.name} page ${actPage}: ${lastError}`);
|
|
230
|
+
hasMoreActs = false;
|
|
141
231
|
}
|
|
142
|
-
// Sort acts by datetime ascending (chronological order)
|
|
143
|
-
allActs.sort((a, b) => new Date(a.datetime).getTime() - new Date(b.datetime).getTime());
|
|
144
|
-
// Process acts into simplified format - exclude redundant fields
|
|
145
|
-
const processedActs = allActs.map(act => {
|
|
146
|
-
const processedAct = {
|
|
147
|
-
datetime: act.datetime,
|
|
148
|
-
type: act.reference_idn,
|
|
149
|
-
message: act.source_text
|
|
150
|
-
};
|
|
151
|
-
// Only include non-redundant fields
|
|
152
|
-
if (act.contact_information) {
|
|
153
|
-
processedAct.contact_information = act.contact_information;
|
|
154
|
-
}
|
|
155
|
-
if (act.flow_idn && act.flow_idn !== 'unknown') {
|
|
156
|
-
processedAct.flow_idn = act.flow_idn;
|
|
157
|
-
}
|
|
158
|
-
if (act.skill_idn && act.skill_idn !== 'unknown') {
|
|
159
|
-
processedAct.skill_idn = act.skill_idn;
|
|
160
|
-
}
|
|
161
|
-
if (act.session_id && act.session_id !== 'unknown') {
|
|
162
|
-
processedAct.session_id = act.session_id;
|
|
163
|
-
}
|
|
164
|
-
return processedAct;
|
|
165
|
-
});
|
|
166
|
-
processedPersonas.push({
|
|
167
|
-
id: persona.id,
|
|
168
|
-
name: persona.name,
|
|
169
|
-
phone,
|
|
170
|
-
act_count: persona.act_count,
|
|
171
|
-
acts: processedActs
|
|
172
|
-
});
|
|
173
|
-
if (verbose)
|
|
174
|
-
console.log(` ✓ Processed ${persona.name}: ${processedActs.length} acts`);
|
|
175
232
|
}
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
generated_at: new Date().toISOString()
|
|
195
|
-
};
|
|
196
|
-
// Save to YAML file
|
|
197
|
-
const conversationsPath = `newo_customers/${customer.idn}/conversations.yaml`;
|
|
198
|
-
const yamlContent = yaml.dump(conversationsData, {
|
|
199
|
-
indent: 2,
|
|
200
|
-
quotingType: '"',
|
|
201
|
-
forceQuotes: false,
|
|
202
|
-
lineWidth: 120,
|
|
203
|
-
noRefs: true,
|
|
204
|
-
sortKeys: false,
|
|
205
|
-
flowLevel: -1
|
|
206
|
-
});
|
|
207
|
-
await writeFileSafe(conversationsPath, yamlContent);
|
|
208
|
-
if (verbose) {
|
|
209
|
-
console.log(`✓ Saved conversations to ${conversationsPath}`);
|
|
210
|
-
console.log(`📊 Summary: ${processedPersonas.length} personas, ${totalActs} total acts`);
|
|
233
|
+
const finalState = {
|
|
234
|
+
id: persona.id,
|
|
235
|
+
name: persona.name,
|
|
236
|
+
phone,
|
|
237
|
+
act_count: persona.act_count,
|
|
238
|
+
acts: buildProcessedActs(allActs),
|
|
239
|
+
fetched_at: new Date().toISOString(),
|
|
240
|
+
complete: !lastError
|
|
241
|
+
};
|
|
242
|
+
if (lastError)
|
|
243
|
+
finalState.last_error = lastError;
|
|
244
|
+
await writePersonaState(customer.idn, finalState);
|
|
245
|
+
// Incremental YAML aggregate every persona
|
|
246
|
+
const agg = await writeAggregateYaml(customer.idn);
|
|
247
|
+
done++;
|
|
248
|
+
if (lastError)
|
|
249
|
+
failed++;
|
|
250
|
+
console.log(`${lastError ? '⚠️ ' : '✓'} [${done}/${total}] ${persona.name}: ${finalState.acts.length} acts${lastError ? ` (partial: ${lastError})` : ''} | total so far: ${agg.personas} personas / ${agg.acts} acts`);
|
|
211
251
|
}
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
252
|
+
catch (error) {
|
|
253
|
+
failed++;
|
|
254
|
+
done++;
|
|
255
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
256
|
+
console.error(`❌ [${done}/${total}] ${persona.name}: ${msg}`);
|
|
257
|
+
}
|
|
258
|
+
})));
|
|
259
|
+
// Final aggregate write
|
|
260
|
+
const final = await writeAggregateYaml(customer.idn);
|
|
261
|
+
console.log(`\n✅ Done. ${final.personas} personas, ${final.acts} acts. Skipped ${skipped} (already cached), ${failed} had errors.`);
|
|
262
|
+
console.log(` Aggregate: ${aggregateYamlPath(customer.idn)}`);
|
|
263
|
+
console.log(` Per-persona: newo_customers/${customer.idn}/conversations/<id>.json`);
|
|
217
264
|
}
|
|
218
265
|
//# sourceMappingURL=conversations.js.map
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JSON-typed attribute helpers.
|
|
3
|
+
*
|
|
4
|
+
* Why this exists:
|
|
5
|
+
*
|
|
6
|
+
* The NEWO platform stores some attributes (e.g.
|
|
7
|
+
* `project_attributes_private_dynamic_workflow_builder_canvas`) as
|
|
8
|
+
* `value_type: json`. The API may return the `value` field as either a
|
|
9
|
+
* STRING containing JSON or as an already-parsed OBJECT.
|
|
10
|
+
*
|
|
11
|
+
* Without normalization, two bugs leak through:
|
|
12
|
+
*
|
|
13
|
+
* 1. When the API returns the value as an OBJECT, `yaml.dump` serializes
|
|
14
|
+
* it as a YAML structure (mappings/sequences). Pushing back then sends
|
|
15
|
+
* `{"value": {...}}` instead of `{"value": "..."}`, breaking the
|
|
16
|
+
* Workflow Builder which expects the canvas as a JSON STRING.
|
|
17
|
+
*
|
|
18
|
+
* 2. The push-time change check used `String(localAttr.value)` for
|
|
19
|
+
* comparison. With objects this collapses to `"[object Object]"` on
|
|
20
|
+
* both sides — silently masking real changes — and with mismatched
|
|
21
|
+
* string vs object representations it triggers spurious pushes that
|
|
22
|
+
* overwrite the canvas with the wrong shape (Builder shows blank).
|
|
23
|
+
*
|
|
24
|
+
* The fix is conservative: for `value_type: json` only, always coerce the
|
|
25
|
+
* value to a STRING when persisting and when pushing, and use canonical
|
|
26
|
+
* JSON for comparisons. String-typed values in the wild are left
|
|
27
|
+
* untouched, so no churn for the majority of attributes.
|
|
28
|
+
*/
|
|
29
|
+
/**
|
|
30
|
+
* True if the attribute is a JSON-typed attribute (case- and
|
|
31
|
+
* format-insensitive: handles `json`, `JSON`, `AttributeValueTypes.json`,
|
|
32
|
+
* `ValueType.JSON`, etc.).
|
|
33
|
+
*/
|
|
34
|
+
export declare function isJsonValueType(valueType: unknown): boolean;
|
|
35
|
+
/**
|
|
36
|
+
* Coerce a JSON-typed attribute's value to a STRING suitable for storage
|
|
37
|
+
* in attributes.yaml and for sending to the platform.
|
|
38
|
+
*
|
|
39
|
+
* - `null` / `undefined` → `''`
|
|
40
|
+
* - object → compact JSON string (`JSON.stringify(value)`)
|
|
41
|
+
* - string → returned as-is (we trust the platform's existing format)
|
|
42
|
+
* - other → `String(value)`
|
|
43
|
+
*
|
|
44
|
+
* We deliberately do NOT re-format string values, even when they look
|
|
45
|
+
* like JSON. Many existing canvases are stored pretty-printed and
|
|
46
|
+
* reformatting would create huge spurious diffs in users' repos.
|
|
47
|
+
*/
|
|
48
|
+
export declare function normalizeJsonValueForStorage(value: unknown): string;
|
|
49
|
+
/**
|
|
50
|
+
* Canonical comparison for JSON-typed attribute values.
|
|
51
|
+
*
|
|
52
|
+
* Returns the canonical form (compact JSON if parseable, otherwise the
|
|
53
|
+
* raw string). Use this on both sides of a comparison so that pretty- vs
|
|
54
|
+
* compact-printed JSON does not register as a change, and so that an
|
|
55
|
+
* object on one side equals its stringified form on the other side.
|
|
56
|
+
*/
|
|
57
|
+
export declare function canonicalJsonValue(value: unknown): string;
|
|
58
|
+
/**
|
|
59
|
+
* True if two JSON-typed attribute values are semantically equal.
|
|
60
|
+
*
|
|
61
|
+
* Handles the four mismatched representations that can occur during a
|
|
62
|
+
* pull/push cycle:
|
|
63
|
+
* string vs string (different whitespace/indent), object vs string,
|
|
64
|
+
* string vs object, object vs object.
|
|
65
|
+
*/
|
|
66
|
+
export declare function jsonValuesEqual(a: unknown, b: unknown): boolean;
|
|
67
|
+
//# sourceMappingURL=json-attr-utils.d.ts.map
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JSON-typed attribute helpers.
|
|
3
|
+
*
|
|
4
|
+
* Why this exists:
|
|
5
|
+
*
|
|
6
|
+
* The NEWO platform stores some attributes (e.g.
|
|
7
|
+
* `project_attributes_private_dynamic_workflow_builder_canvas`) as
|
|
8
|
+
* `value_type: json`. The API may return the `value` field as either a
|
|
9
|
+
* STRING containing JSON or as an already-parsed OBJECT.
|
|
10
|
+
*
|
|
11
|
+
* Without normalization, two bugs leak through:
|
|
12
|
+
*
|
|
13
|
+
* 1. When the API returns the value as an OBJECT, `yaml.dump` serializes
|
|
14
|
+
* it as a YAML structure (mappings/sequences). Pushing back then sends
|
|
15
|
+
* `{"value": {...}}` instead of `{"value": "..."}`, breaking the
|
|
16
|
+
* Workflow Builder which expects the canvas as a JSON STRING.
|
|
17
|
+
*
|
|
18
|
+
* 2. The push-time change check used `String(localAttr.value)` for
|
|
19
|
+
* comparison. With objects this collapses to `"[object Object]"` on
|
|
20
|
+
* both sides — silently masking real changes — and with mismatched
|
|
21
|
+
* string vs object representations it triggers spurious pushes that
|
|
22
|
+
* overwrite the canvas with the wrong shape (Builder shows blank).
|
|
23
|
+
*
|
|
24
|
+
* The fix is conservative: for `value_type: json` only, always coerce the
|
|
25
|
+
* value to a STRING when persisting and when pushing, and use canonical
|
|
26
|
+
* JSON for comparisons. String-typed values in the wild are left
|
|
27
|
+
* untouched, so no churn for the majority of attributes.
|
|
28
|
+
*/
|
|
29
|
+
/**
|
|
30
|
+
* True if the attribute is a JSON-typed attribute (case- and
|
|
31
|
+
* format-insensitive: handles `json`, `JSON`, `AttributeValueTypes.json`,
|
|
32
|
+
* `ValueType.JSON`, etc.).
|
|
33
|
+
*/
|
|
34
|
+
export function isJsonValueType(valueType) {
|
|
35
|
+
if (typeof valueType !== 'string')
|
|
36
|
+
return false;
|
|
37
|
+
const lower = valueType.toLowerCase();
|
|
38
|
+
return lower === 'json' || lower.endsWith('.json');
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Coerce a JSON-typed attribute's value to a STRING suitable for storage
|
|
42
|
+
* in attributes.yaml and for sending to the platform.
|
|
43
|
+
*
|
|
44
|
+
* - `null` / `undefined` → `''`
|
|
45
|
+
* - object → compact JSON string (`JSON.stringify(value)`)
|
|
46
|
+
* - string → returned as-is (we trust the platform's existing format)
|
|
47
|
+
* - other → `String(value)`
|
|
48
|
+
*
|
|
49
|
+
* We deliberately do NOT re-format string values, even when they look
|
|
50
|
+
* like JSON. Many existing canvases are stored pretty-printed and
|
|
51
|
+
* reformatting would create huge spurious diffs in users' repos.
|
|
52
|
+
*/
|
|
53
|
+
export function normalizeJsonValueForStorage(value) {
|
|
54
|
+
if (value == null)
|
|
55
|
+
return '';
|
|
56
|
+
if (typeof value === 'string')
|
|
57
|
+
return value;
|
|
58
|
+
if (typeof value === 'object') {
|
|
59
|
+
try {
|
|
60
|
+
return JSON.stringify(value);
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
return String(value);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return String(value);
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Canonical comparison for JSON-typed attribute values.
|
|
70
|
+
*
|
|
71
|
+
* Returns the canonical form (compact JSON if parseable, otherwise the
|
|
72
|
+
* raw string). Use this on both sides of a comparison so that pretty- vs
|
|
73
|
+
* compact-printed JSON does not register as a change, and so that an
|
|
74
|
+
* object on one side equals its stringified form on the other side.
|
|
75
|
+
*/
|
|
76
|
+
export function canonicalJsonValue(value) {
|
|
77
|
+
const stringified = normalizeJsonValueForStorage(value);
|
|
78
|
+
if (stringified === '')
|
|
79
|
+
return '';
|
|
80
|
+
try {
|
|
81
|
+
return JSON.stringify(JSON.parse(stringified));
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
return stringified;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* True if two JSON-typed attribute values are semantically equal.
|
|
89
|
+
*
|
|
90
|
+
* Handles the four mismatched representations that can occur during a
|
|
91
|
+
* pull/push cycle:
|
|
92
|
+
* string vs string (different whitespace/indent), object vs string,
|
|
93
|
+
* string vs object, object vs object.
|
|
94
|
+
*/
|
|
95
|
+
export function jsonValuesEqual(a, b) {
|
|
96
|
+
return canonicalJsonValue(a) === canonicalJsonValue(b);
|
|
97
|
+
}
|
|
98
|
+
//# sourceMappingURL=json-attr-utils.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "newo",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.7.1",
|
|
4
4
|
"description": "NEWO CLI: Professional command-line tool with modular architecture for NEWO AI Agent development. Features account migration, integration management, webhook automation, AKB knowledge base, project attributes, sandbox testing, IDN-based file management, real-time progress tracking, intelligent sync operations, and comprehensive multi-customer support.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -60,6 +60,8 @@
|
|
|
60
60
|
"node": ">=18"
|
|
61
61
|
},
|
|
62
62
|
"dependencies": {
|
|
63
|
+
"newo-dsl-analyzer": "^1.0.0",
|
|
64
|
+
"newo-dsl-core": "^1.0.0",
|
|
63
65
|
"axios": "^1.7.7",
|
|
64
66
|
"chokidar": "^5.0.0",
|
|
65
67
|
"dotenv": "^16.4.5",
|