newo 3.6.2 → 3.7.0
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 +37 -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/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/conversations.d.ts +1 -1
- package/dist/sync/conversations.js +240 -193
- 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/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/conversations.ts +265 -212
|
@@ -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
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "newo",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.7.0",
|
|
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",
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `newo check` - umbrella command equivalent to running lint + format --check.
|
|
3
|
+
*
|
|
4
|
+
* A failing check exits non-zero if any of the sub-checks fail, so CI
|
|
5
|
+
* pipelines can gate merges on a single invocation.
|
|
6
|
+
*/
|
|
7
|
+
import { handleLintCommand } from './lint.js';
|
|
8
|
+
import { handleFormatCommand } from './format.js';
|
|
9
|
+
import type { MultiCustomerConfig, CliArgs } from '../../types.js';
|
|
10
|
+
|
|
11
|
+
export async function handleCheckCommand(
|
|
12
|
+
customerConfig: MultiCustomerConfig,
|
|
13
|
+
args: CliArgs,
|
|
14
|
+
verbose: boolean,
|
|
15
|
+
): Promise<void> {
|
|
16
|
+
const lintArgs = { ...args };
|
|
17
|
+
await handleLintCommand(customerConfig, lintArgs as CliArgs, verbose);
|
|
18
|
+
|
|
19
|
+
const formatCheckArgs = { ...args, check: true } as CliArgs;
|
|
20
|
+
await handleFormatCommand(customerConfig, formatCheckArgs, verbose);
|
|
21
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `newo format` - apply canonical formatting to DSL files.
|
|
3
|
+
*
|
|
4
|
+
* Invokes newo-dsl-analyzer's Formatter. In v1 the formatter is an
|
|
5
|
+
* identity transform (just ensures a final newline). Concrete rules
|
|
6
|
+
* land in subsequent versions; the command surface is stable now so
|
|
7
|
+
* CI pipelines and pre-commit hooks can wire `newo format --check`
|
|
8
|
+
* immediately.
|
|
9
|
+
*/
|
|
10
|
+
import fs from 'fs-extra';
|
|
11
|
+
import path from 'path';
|
|
12
|
+
import { createFormatter } from 'newo-dsl-analyzer';
|
|
13
|
+
|
|
14
|
+
import { selectSingleCustomer } from '../customer-selection.js';
|
|
15
|
+
import { handleCliError } from '../errors.js';
|
|
16
|
+
import { resolveFormat } from '../../format/detect.js';
|
|
17
|
+
import { discoverCustomerFiles, discoverFromPath, defaultRoot } from '../../lint/discovery.js';
|
|
18
|
+
import type { MultiCustomerConfig, CliArgs, CustomerConfig } from '../../types.js';
|
|
19
|
+
import type { FormatVersion } from '../../format/types.js';
|
|
20
|
+
|
|
21
|
+
interface FormatArgs {
|
|
22
|
+
positional: string[];
|
|
23
|
+
formatVersion: string | undefined;
|
|
24
|
+
check: boolean;
|
|
25
|
+
customer: string | undefined;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export async function handleFormatCommand(
|
|
29
|
+
customerConfig: MultiCustomerConfig,
|
|
30
|
+
args: CliArgs,
|
|
31
|
+
verbose: boolean,
|
|
32
|
+
): Promise<void> {
|
|
33
|
+
try {
|
|
34
|
+
const fmtArgs = parseArgs(args);
|
|
35
|
+
const formatter = createFormatter();
|
|
36
|
+
|
|
37
|
+
const hasCustomerContext =
|
|
38
|
+
fmtArgs.customer !== undefined ||
|
|
39
|
+
Object.keys(customerConfig.customers ?? {}).length > 0;
|
|
40
|
+
|
|
41
|
+
const { selectedCustomer, allCustomers, isMultiCustomer } = hasCustomerContext
|
|
42
|
+
? selectSingleCustomer(customerConfig, fmtArgs.customer)
|
|
43
|
+
: { selectedCustomer: null, allCustomers: [] as CustomerConfig[], isMultiCustomer: false };
|
|
44
|
+
|
|
45
|
+
const targetCustomer = selectedCustomer ?? (isMultiCustomer ? null : allCustomers[0] ?? null);
|
|
46
|
+
void targetCustomer;
|
|
47
|
+
|
|
48
|
+
const files = await resolveFiles(targetCustomer, allCustomers, fmtArgs, isMultiCustomer);
|
|
49
|
+
if (files.length === 0) {
|
|
50
|
+
console.log('No files matched.');
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
let touched = 0;
|
|
55
|
+
let needsFormat = 0;
|
|
56
|
+
for (const file of files) {
|
|
57
|
+
const source = await fs.readFile(file.absPath, 'utf8');
|
|
58
|
+
const result = formatter.format(source, file.absPath);
|
|
59
|
+
if (!result.changed) continue;
|
|
60
|
+
needsFormat++;
|
|
61
|
+
if (fmtArgs.check) {
|
|
62
|
+
console.log(`would rewrite ${path.relative(process.cwd(), file.absPath)}`);
|
|
63
|
+
} else {
|
|
64
|
+
await fs.writeFile(file.absPath, result.formatted, 'utf8');
|
|
65
|
+
touched++;
|
|
66
|
+
if (verbose) console.log(`formatted ${path.relative(process.cwd(), file.absPath)}`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (fmtArgs.check) {
|
|
71
|
+
if (needsFormat === 0) {
|
|
72
|
+
console.log('All files are properly formatted.');
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
console.log(`${needsFormat} file(s) would be reformatted.`);
|
|
76
|
+
process.exit(1);
|
|
77
|
+
} else {
|
|
78
|
+
console.log(`Formatted ${touched} file(s).`);
|
|
79
|
+
}
|
|
80
|
+
} catch (err) {
|
|
81
|
+
handleCliError(err, 'format');
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function parseArgs(args: CliArgs): FormatArgs {
|
|
86
|
+
const positional = args._.slice(1).filter((p): p is string => typeof p === 'string');
|
|
87
|
+
return {
|
|
88
|
+
positional,
|
|
89
|
+
formatVersion: args.format as string | undefined,
|
|
90
|
+
check: Boolean(args.check),
|
|
91
|
+
customer: args.customer as string | undefined,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async function resolveFiles(
|
|
96
|
+
selected: CustomerConfig | null,
|
|
97
|
+
all: CustomerConfig[],
|
|
98
|
+
args: FormatArgs,
|
|
99
|
+
isMultiCustomer: boolean,
|
|
100
|
+
) {
|
|
101
|
+
if (args.positional.length > 0) {
|
|
102
|
+
const files = [];
|
|
103
|
+
for (const p of args.positional) {
|
|
104
|
+
files.push(
|
|
105
|
+
...(await discoverFromPath(p, {
|
|
106
|
+
...(args.formatVersion ? { format: toFormatVersion(args.formatVersion) } : {}),
|
|
107
|
+
})),
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
return files;
|
|
111
|
+
}
|
|
112
|
+
if (selected) {
|
|
113
|
+
const formatVersion = resolveFormat(selected.idn, args.formatVersion).version;
|
|
114
|
+
return discoverCustomerFiles(selected, { format: formatVersion });
|
|
115
|
+
}
|
|
116
|
+
if (isMultiCustomer) {
|
|
117
|
+
const files = [];
|
|
118
|
+
for (const customer of all) {
|
|
119
|
+
const formatVersion = resolveFormat(customer.idn, args.formatVersion).version;
|
|
120
|
+
files.push(...(await discoverCustomerFiles(customer, { format: formatVersion })));
|
|
121
|
+
}
|
|
122
|
+
return files;
|
|
123
|
+
}
|
|
124
|
+
return discoverFromPath(defaultRoot(), {
|
|
125
|
+
...(args.formatVersion ? { format: toFormatVersion(args.formatVersion) } : {}),
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function toFormatVersion(v: string): FormatVersion {
|
|
130
|
+
return v === 'newo_v2' ? 'newo_v2' : 'cli_v1';
|
|
131
|
+
}
|
package/src/cli/commands/help.ts
CHANGED
|
@@ -22,6 +22,19 @@ Core Commands:
|
|
|
22
22
|
newo meta [--customer <idn>] # get project metadata (debug)
|
|
23
23
|
newo import-akb <file> <persona_id> [--customer <idn>] # import AKB articles
|
|
24
24
|
|
|
25
|
+
Linting & Formatting (NEW):
|
|
26
|
+
newo lint [paths...] [--format <fmt>] [--reporter <text|json|sarif>] # static-analysis on DSL files
|
|
27
|
+
newo lint --changed # lint only files modified since last push
|
|
28
|
+
newo lint --live # refresh action catalog from NEWO API
|
|
29
|
+
newo lint --rule <code> --no-rule <code> # selectively enable/disable rules
|
|
30
|
+
newo lint --max-warnings <n> # fail when warnings exceed threshold
|
|
31
|
+
newo format [paths...] [--check] # apply canonical formatting (in-place or --check)
|
|
32
|
+
newo check [paths...] # umbrella: lint + format --check (CI gate)
|
|
33
|
+
|
|
34
|
+
Powered by newo-dsl-analyzer. Exit codes: 0 clean, 1 lint errors, 2 runtime error.
|
|
35
|
+
Discover rules via the Diagnostic code column (E100, W101, ...); configure
|
|
36
|
+
them in .neworc.yaml at your repo root. Plugin authors: depend on newo-dsl-core.
|
|
37
|
+
|
|
25
38
|
Project Management:
|
|
26
39
|
newo create-project <idn> [--title <title>] [--description <desc>] [--version <version>] [--auto-update] # create empty project on platform
|
|
27
40
|
newo list-registries [--customer <idn>] # list available project registries (production, staging, etc.)
|