newo 3.1.0 → 3.2.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.
Files changed (48) hide show
  1. package/CHANGELOG.md +139 -1
  2. package/dist/api.d.ts +14 -1
  3. package/dist/api.js +74 -0
  4. package/dist/cli/commands/help.js +20 -1
  5. package/dist/cli/commands/list-actions.d.ts +3 -0
  6. package/dist/cli/commands/list-actions.js +89 -0
  7. package/dist/cli/commands/profile.d.ts +3 -0
  8. package/dist/cli/commands/profile.js +62 -0
  9. package/dist/cli/commands/pull-akb.d.ts +3 -0
  10. package/dist/cli/commands/pull-akb.js +19 -0
  11. package/dist/cli/commands/pull-attributes.js +7 -0
  12. package/dist/cli/commands/pull-integrations.d.ts +3 -0
  13. package/dist/cli/commands/pull-integrations.js +19 -0
  14. package/dist/cli/commands/push-akb.d.ts +3 -0
  15. package/dist/cli/commands/push-akb.js +19 -0
  16. package/dist/cli/commands/push-integrations.d.ts +3 -0
  17. package/dist/cli/commands/push-integrations.js +19 -0
  18. package/dist/cli.js +24 -0
  19. package/dist/sync/akb.d.ts +14 -0
  20. package/dist/sync/akb.js +175 -0
  21. package/dist/sync/attributes.d.ts +19 -0
  22. package/dist/sync/attributes.js +221 -2
  23. package/dist/sync/integrations.d.ts +23 -0
  24. package/dist/sync/integrations.js +340 -0
  25. package/dist/sync/projects.js +171 -1
  26. package/dist/sync/push.js +15 -0
  27. package/dist/sync/skill-files.js +1 -1
  28. package/dist/sync/status.js +4 -2
  29. package/dist/types.d.ts +128 -0
  30. package/package.json +11 -3
  31. package/src/api.ts +132 -1
  32. package/src/cli/commands/help.ts +20 -1
  33. package/src/cli/commands/list-actions.ts +112 -0
  34. package/src/cli/commands/profile.ts +79 -0
  35. package/src/cli/commands/pull-akb.ts +27 -0
  36. package/src/cli/commands/pull-attributes.ts +8 -0
  37. package/src/cli/commands/pull-integrations.ts +27 -0
  38. package/src/cli/commands/push-akb.ts +27 -0
  39. package/src/cli/commands/push-integrations.ts +27 -0
  40. package/src/cli.ts +30 -0
  41. package/src/sync/akb.ts +205 -0
  42. package/src/sync/attributes.ts +269 -2
  43. package/src/sync/integrations.ts +403 -0
  44. package/src/sync/projects.ts +207 -1
  45. package/src/sync/push.ts +17 -0
  46. package/src/sync/skill-files.ts +1 -1
  47. package/src/sync/status.ts +4 -2
  48. package/src/types.ts +150 -0
@@ -0,0 +1,340 @@
1
+ /**
2
+ * Integration and connector synchronization module
3
+ * Handles pull/push of integrations and connectors to/from NEWO platform
4
+ */
5
+ import path from 'path';
6
+ import fs from 'fs-extra';
7
+ import yaml from 'js-yaml';
8
+ import { listIntegrations, listConnectors, getIntegrationSettings, createConnector, updateConnector, deleteConnector, listOutgoingWebhooks, listIncomingWebhooks } from '../api.js';
9
+ /**
10
+ * Pull all integrations and connectors from NEWO platform
11
+ */
12
+ export async function pullIntegrations(client, customerDir, verbose = false) {
13
+ if (verbose)
14
+ console.log('\nšŸ“¦ Pulling integrations from NEWO platform...\n');
15
+ // Create integrations directory
16
+ const integrationsDir = path.join(customerDir, 'integrations');
17
+ await fs.ensureDir(integrationsDir);
18
+ // Fetch all integrations
19
+ const integrations = await listIntegrations(client);
20
+ if (verbose)
21
+ console.log(`āœ“ Found ${integrations.length} integrations`);
22
+ const integrationsMetadata = [];
23
+ // Process each integration
24
+ for (const integration of integrations) {
25
+ if (verbose)
26
+ console.log(`\n šŸ“¦ Processing: ${integration.title} (${integration.idn})`);
27
+ // Add to metadata list
28
+ integrationsMetadata.push({
29
+ id: integration.id,
30
+ idn: integration.idn,
31
+ title: integration.title,
32
+ description: integration.description,
33
+ channel: integration.channel,
34
+ is_disabled: integration.is_disabled
35
+ });
36
+ // Create integration directory
37
+ const integrationDir = path.join(integrationsDir, integration.idn);
38
+ await fs.ensureDir(integrationDir);
39
+ // Fetch integration settings
40
+ let integrationSettings = [];
41
+ try {
42
+ integrationSettings = await getIntegrationSettings(client, integration.id);
43
+ }
44
+ catch (error) {
45
+ // Settings endpoint may not be available for all integrations
46
+ if (verbose && error.response?.status !== 404) {
47
+ console.log(` ⚠ Could not fetch settings: ${error.message}`);
48
+ }
49
+ }
50
+ // Save combined integration file (metadata + settings)
51
+ const integrationFile = path.join(integrationDir, `${integration.idn}.yaml`);
52
+ const integrationData = {
53
+ id: integration.id,
54
+ idn: integration.idn,
55
+ title: integration.title,
56
+ description: integration.description,
57
+ channel: integration.channel,
58
+ is_disabled: integration.is_disabled
59
+ };
60
+ // Add settings array if any settings exist
61
+ if (integrationSettings.length > 0) {
62
+ integrationData.settings = integrationSettings;
63
+ }
64
+ await fs.writeFile(integrationFile, yaml.dump(integrationData, { lineWidth: -1 }));
65
+ if (verbose)
66
+ console.log(` āœ“ Saved integration → ${integration.idn}.yaml (${integrationSettings.length} settings)`);
67
+ // Fetch and save connectors
68
+ const connectors = await listConnectors(client, integration.id);
69
+ if (verbose)
70
+ console.log(` Connectors: ${connectors.length} found`);
71
+ if (connectors.length > 0) {
72
+ const connectorsDir = path.join(integrationDir, 'connectors');
73
+ await fs.ensureDir(connectorsDir);
74
+ for (const connector of connectors) {
75
+ const connectorMetadata = {
76
+ id: connector.id,
77
+ connector_idn: connector.connector_idn,
78
+ title: connector.title,
79
+ status: connector.status,
80
+ integration_idn: integration.idn,
81
+ settings: connector.settings
82
+ };
83
+ // Create subdirectory for this connector
84
+ const connectorDir = path.join(connectorsDir, connector.connector_idn);
85
+ await fs.ensureDir(connectorDir);
86
+ // Save connector YAML file inside its subdirectory
87
+ const connectorFile = path.join(connectorDir, `${connector.connector_idn}.yaml`);
88
+ await fs.writeFile(connectorFile, yaml.dump(connectorMetadata, { lineWidth: -1 }));
89
+ if (verbose)
90
+ console.log(` āœ“ Saved: ${connector.title} → connectors/${connector.connector_idn}/${connector.connector_idn}.yaml`);
91
+ }
92
+ }
93
+ }
94
+ // Fetch and save webhooks (for API integration connectors only)
95
+ if (verbose)
96
+ console.log(`\nšŸ“” Fetching webhooks...`);
97
+ try {
98
+ const outgoingWebhooks = await listOutgoingWebhooks(client);
99
+ const incomingWebhooks = await listIncomingWebhooks(client);
100
+ if (verbose)
101
+ console.log(`āœ“ Found ${outgoingWebhooks.length} outgoing webhooks`);
102
+ if (verbose)
103
+ console.log(`āœ“ Found ${incomingWebhooks.length} incoming webhooks`);
104
+ // Group webhooks by connector_idn
105
+ const outgoingByConnector = new Map();
106
+ const incomingByConnector = new Map();
107
+ outgoingWebhooks.forEach(webhook => {
108
+ if (!outgoingByConnector.has(webhook.connector_idn)) {
109
+ outgoingByConnector.set(webhook.connector_idn, []);
110
+ }
111
+ outgoingByConnector.get(webhook.connector_idn).push(webhook);
112
+ });
113
+ incomingWebhooks.forEach(webhook => {
114
+ if (!incomingByConnector.has(webhook.connector_idn)) {
115
+ incomingByConnector.set(webhook.connector_idn, []);
116
+ }
117
+ incomingByConnector.get(webhook.connector_idn).push(webhook);
118
+ });
119
+ // Save webhooks to appropriate connector directories
120
+ for (const integration of integrations) {
121
+ const integrationDir = path.join(integrationsDir, integration.idn);
122
+ const connectorsDir = path.join(integrationDir, 'connectors');
123
+ if (await fs.pathExists(connectorsDir)) {
124
+ const connectors = await listConnectors(client, integration.id);
125
+ for (const connector of connectors) {
126
+ const connectorWebhooksDir = path.join(connectorsDir, connector.connector_idn, 'webhooks');
127
+ const outgoing = outgoingByConnector.get(connector.connector_idn) || [];
128
+ const incoming = incomingByConnector.get(connector.connector_idn) || [];
129
+ if (outgoing.length > 0 || incoming.length > 0) {
130
+ await fs.ensureDir(connectorWebhooksDir);
131
+ if (outgoing.length > 0) {
132
+ const outgoingFile = path.join(connectorWebhooksDir, 'outgoing.yaml');
133
+ await fs.writeFile(outgoingFile, yaml.dump({ webhooks: outgoing }, { lineWidth: -1 }));
134
+ if (verbose)
135
+ console.log(` āœ“ Saved: ${outgoing.length} outgoing webhooks → ${connector.connector_idn}/webhooks/outgoing.yaml`);
136
+ }
137
+ if (incoming.length > 0) {
138
+ const incomingFile = path.join(connectorWebhooksDir, 'incoming.yaml');
139
+ await fs.writeFile(incomingFile, yaml.dump({ webhooks: incoming }, { lineWidth: -1 }));
140
+ if (verbose)
141
+ console.log(` āœ“ Saved: ${incoming.length} incoming webhooks → ${connector.connector_idn}/webhooks/incoming.yaml`);
142
+ }
143
+ }
144
+ }
145
+ }
146
+ }
147
+ }
148
+ catch (error) {
149
+ if (verbose)
150
+ console.log(`⚠ Could not fetch webhooks: ${error.message}`);
151
+ }
152
+ // Save master integrations list
153
+ const integrationsData = { integrations: integrationsMetadata };
154
+ const integrationsFile = path.join(integrationsDir, 'integrations.yaml');
155
+ await fs.writeFile(integrationsFile, yaml.dump(integrationsData, { lineWidth: -1 }));
156
+ if (verbose)
157
+ console.log(`\nāœ… Saved ${integrations.length} integrations to integrations/integrations.yaml\n`);
158
+ }
159
+ /**
160
+ * Push integration changes from local files to NEWO platform
161
+ */
162
+ export async function pushIntegrations(client, customerDir, verbose = false) {
163
+ if (verbose)
164
+ console.log('\nšŸ“¤ Pushing integration changes to NEWO platform...\n');
165
+ const integrationsDir = path.join(customerDir, 'integrations');
166
+ // Check if integrations directory exists
167
+ if (!await fs.pathExists(integrationsDir)) {
168
+ if (verbose)
169
+ console.log('⚠ No integrations directory found. Run pull-integrations first.');
170
+ return;
171
+ }
172
+ // Load remote integrations for ID mapping
173
+ const remoteIntegrations = await listIntegrations(client);
174
+ const integrationMap = new Map(); // idn -> id
175
+ remoteIntegrations.forEach(int => integrationMap.set(int.idn, int.id));
176
+ let updatedCount = 0;
177
+ let createdCount = 0;
178
+ let deletedCount = 0;
179
+ // Read integrations directory
180
+ const integrationFolders = await fs.readdir(integrationsDir);
181
+ for (const folder of integrationFolders) {
182
+ if (folder === 'integrations.yaml')
183
+ continue; // Skip master file
184
+ const integrationDir = path.join(integrationsDir, folder);
185
+ const stat = await fs.stat(integrationDir);
186
+ if (!stat.isDirectory())
187
+ continue;
188
+ const integrationIdn = folder;
189
+ const integrationId = integrationMap.get(integrationIdn);
190
+ if (!integrationId) {
191
+ if (verbose)
192
+ console.log(`⚠ Integration ${integrationIdn} not found on platform, skipping...`);
193
+ continue;
194
+ }
195
+ if (verbose)
196
+ console.log(`\n šŸ“¦ Processing: ${integrationIdn}`);
197
+ // Process connectors
198
+ const connectorsDir = path.join(integrationDir, 'connectors');
199
+ if (await fs.pathExists(connectorsDir)) {
200
+ // Load remote connectors for comparison
201
+ const remoteConnectors = await listConnectors(client, integrationId);
202
+ const remoteConnectorMap = new Map();
203
+ remoteConnectors.forEach(conn => remoteConnectorMap.set(conn.connector_idn, conn));
204
+ // Read connector subdirectories
205
+ const connectorDirs = await fs.readdir(connectorsDir);
206
+ const localConnectorIdns = new Set();
207
+ for (const connectorDirName of connectorDirs) {
208
+ const connectorPath = path.join(connectorsDir, connectorDirName);
209
+ const stat = await fs.stat(connectorPath);
210
+ if (!stat.isDirectory())
211
+ continue; // Skip non-directories
212
+ // Read connector YAML file from within the subdirectory
213
+ const connectorFile = path.join(connectorPath, `${connectorDirName}.yaml`);
214
+ if (!await fs.pathExists(connectorFile)) {
215
+ if (verbose)
216
+ console.log(` ⚠ No YAML file found in ${connectorDirName}/, skipping...`);
217
+ continue;
218
+ }
219
+ const connectorData = yaml.load(await fs.readFile(connectorFile, 'utf-8'));
220
+ localConnectorIdns.add(connectorData.connector_idn);
221
+ const remoteConnector = remoteConnectorMap.get(connectorData.connector_idn);
222
+ if (!remoteConnector) {
223
+ // Create new connector
224
+ if (verbose)
225
+ console.log(` āž• Creating connector: ${connectorData.title}`);
226
+ try {
227
+ await createConnector(client, integrationId, {
228
+ title: connectorData.title,
229
+ connector_idn: connectorData.connector_idn,
230
+ integration_idn: integrationIdn,
231
+ settings: connectorData.settings
232
+ });
233
+ createdCount++;
234
+ if (verbose)
235
+ console.log(` āœ… Created: ${connectorData.title}`);
236
+ }
237
+ catch (error) {
238
+ console.error(` āŒ Failed to create connector: ${error.message}`);
239
+ }
240
+ }
241
+ else {
242
+ // Check if connector needs update
243
+ const needsUpdate = hasConnectorChanged(remoteConnector, connectorData);
244
+ if (needsUpdate) {
245
+ if (verbose)
246
+ console.log(` šŸ”„ Updating connector: ${connectorData.title}`);
247
+ try {
248
+ await updateConnector(client, remoteConnector.id, {
249
+ title: connectorData.title,
250
+ status: connectorData.status,
251
+ settings: connectorData.settings
252
+ });
253
+ updatedCount++;
254
+ if (verbose)
255
+ console.log(` āœ… Updated: ${connectorData.title}`);
256
+ }
257
+ catch (error) {
258
+ console.error(` āŒ Failed to update connector: ${error.message}`);
259
+ }
260
+ }
261
+ else {
262
+ if (verbose)
263
+ console.log(` āœ“ No changes: ${connectorData.title}`);
264
+ }
265
+ }
266
+ }
267
+ // Delete connectors that exist remotely but not locally
268
+ for (const [connectorIdn, remoteConnector] of remoteConnectorMap) {
269
+ if (!localConnectorIdns.has(connectorIdn)) {
270
+ if (verbose)
271
+ console.log(` šŸ—‘ļø Deleting connector: ${remoteConnector.title}`);
272
+ try {
273
+ await deleteConnector(client, remoteConnector.id);
274
+ deletedCount++;
275
+ if (verbose)
276
+ console.log(` āœ… Deleted: ${remoteConnector.title}`);
277
+ }
278
+ catch (error) {
279
+ console.error(` āŒ Failed to delete connector: ${error.message}`);
280
+ }
281
+ }
282
+ }
283
+ }
284
+ }
285
+ // Always show summary if changes were made, not just in verbose mode
286
+ if (createdCount > 0 || updatedCount > 0 || deletedCount > 0) {
287
+ console.log(`\nāœ… Integration push completed:`);
288
+ console.log(` Created: ${createdCount} connector(s)`);
289
+ console.log(` Updated: ${updatedCount} connector(s)`);
290
+ console.log(` Deleted: ${deletedCount} connector(s)`);
291
+ }
292
+ else {
293
+ console.log(`\nāœ“ No connector changes to push`);
294
+ }
295
+ }
296
+ /**
297
+ * Check if connector has changed compared to remote version
298
+ */
299
+ function hasConnectorChanged(remote, local) {
300
+ // Check title
301
+ if (remote.title !== local.title)
302
+ return true;
303
+ // Check status
304
+ if (remote.status !== local.status)
305
+ return true;
306
+ // Check settings
307
+ if (remote.settings.length !== local.settings.length)
308
+ return true;
309
+ // Compare each setting
310
+ const remoteSettingsMap = new Map();
311
+ remote.settings.forEach(s => remoteSettingsMap.set(s.idn, s.value));
312
+ for (const localSetting of local.settings) {
313
+ const remoteValue = remoteSettingsMap.get(localSetting.idn);
314
+ if (remoteValue !== localSetting.value)
315
+ return true;
316
+ }
317
+ return false;
318
+ }
319
+ /**
320
+ * List all local integrations from file system
321
+ */
322
+ export async function listLocalIntegrations(customerDir) {
323
+ const integrationsFile = path.join(customerDir, 'integrations', 'integrations.yaml');
324
+ if (!await fs.pathExists(integrationsFile)) {
325
+ return [];
326
+ }
327
+ const data = yaml.load(await fs.readFile(integrationsFile, 'utf-8'));
328
+ return data.integrations;
329
+ }
330
+ /**
331
+ * Get connector details from local file
332
+ */
333
+ export async function getLocalConnector(customerDir, integrationIdn, connectorIdn) {
334
+ const connectorFile = path.join(customerDir, 'integrations', integrationIdn, 'connectors', connectorIdn, `${connectorIdn}.yaml`);
335
+ if (!await fs.pathExists(connectorFile)) {
336
+ return null;
337
+ }
338
+ return yaml.load(await fs.readFile(connectorFile, 'utf-8'));
339
+ }
340
+ //# sourceMappingURL=integrations.js.map
@@ -2,7 +2,7 @@
2
2
  * Project synchronization operations
3
3
  */
4
4
  import { listProjects, listAgents, listFlowSkills, listFlowEvents, listFlowStates } from '../api.js';
5
- import { ensureState, writeFileSafe, mapPath, projectMetadataPath, agentMetadataPath, flowMetadataPath, skillMetadataPath, skillScriptPath, skillFolderPath, flowsYamlPath, customerAttributesPath } from '../fsutil.js';
5
+ import { ensureState, writeFileSafe, mapPath, projectMetadataPath, agentMetadataPath, flowMetadataPath, skillMetadataPath, skillScriptPath, skillFolderPath, flowsYamlPath, customerAttributesPath, customerProjectsDir, projectDir } from '../fsutil.js';
6
6
  import { findSkillScriptFiles, isContentDifferent, askForOverwrite, getExtensionForRunner } from './skill-files.js';
7
7
  import fs from 'fs-extra';
8
8
  import { sha256, saveHashes } from '../hash.js';
@@ -16,6 +16,174 @@ export function isProjectMap(x) {
16
16
  export function isLegacyProjectMap(x) {
17
17
  return typeof x === 'object' && x !== null && 'projectId' in x && 'agents' in x;
18
18
  }
19
+ /**
20
+ * Ask user for deletion confirmation
21
+ */
22
+ async function askForDeletion(entityType, entityPath) {
23
+ const readline = await import('readline');
24
+ const rl = readline.createInterface({
25
+ input: process.stdin,
26
+ output: process.stdout
27
+ });
28
+ const answer = await new Promise((resolve) => {
29
+ rl.question(`\nšŸ—‘ļø Delete ${entityType}: ${entityPath}? (y)es/(n)o/(a)ll/(q)uit: `, resolve);
30
+ });
31
+ rl.close();
32
+ const choice = answer.toLowerCase().trim();
33
+ if (choice === 'q' || choice === 'quit') {
34
+ return 'quit';
35
+ }
36
+ if (choice === 'a' || choice === 'all') {
37
+ return 'all';
38
+ }
39
+ if (choice === 'y' || choice === 'yes') {
40
+ return 'yes';
41
+ }
42
+ return 'no';
43
+ }
44
+ /**
45
+ * Clean up deleted entities (projects, agents, flows, skills) that no longer exist remotely
46
+ */
47
+ async function cleanupDeletedEntities(customerIdn, projectMap, verbose = false) {
48
+ const projectsDir = customerProjectsDir(customerIdn);
49
+ if (!(await fs.pathExists(projectsDir))) {
50
+ return;
51
+ }
52
+ const deletedEntities = [];
53
+ // Scan local filesystem for entities
54
+ const localProjects = await fs.readdir(projectsDir);
55
+ for (const projectIdn of localProjects) {
56
+ const projectPath = projectDir(customerIdn, projectIdn);
57
+ const projectStat = await fs.stat(projectPath).catch(() => null);
58
+ // Skip files
59
+ if (!projectStat || !projectStat.isDirectory())
60
+ continue;
61
+ // Skip flows.yaml
62
+ if (projectIdn === 'flows.yaml')
63
+ continue;
64
+ // Check if project exists in map
65
+ const projectData = projectMap.projects[projectIdn];
66
+ if (!projectData) {
67
+ // Entire project was deleted remotely
68
+ deletedEntities.push({
69
+ type: 'project',
70
+ path: projectPath,
71
+ displayPath: projectIdn
72
+ });
73
+ continue;
74
+ }
75
+ // Scan for agents within this project
76
+ try {
77
+ const localAgents = await fs.readdir(projectPath);
78
+ for (const agentIdn of localAgents) {
79
+ const agentPath = `${projectPath}/${agentIdn}`;
80
+ const agentStat = await fs.stat(agentPath).catch(() => null);
81
+ // Skip files and metadata.yaml
82
+ if (!agentStat || !agentStat.isDirectory())
83
+ continue;
84
+ // Check if agent exists in map
85
+ const agentData = projectData.agents[agentIdn];
86
+ if (!agentData) {
87
+ // Agent was deleted remotely
88
+ deletedEntities.push({
89
+ type: 'agent',
90
+ path: agentPath,
91
+ displayPath: `${projectIdn}/${agentIdn}`
92
+ });
93
+ continue;
94
+ }
95
+ // Scan for flows within this agent
96
+ try {
97
+ const localFlows = await fs.readdir(agentPath);
98
+ for (const flowIdn of localFlows) {
99
+ const flowPath = `${agentPath}/${flowIdn}`;
100
+ const flowStat = await fs.stat(flowPath).catch(() => null);
101
+ // Skip files and metadata.yaml
102
+ if (!flowStat || !flowStat.isDirectory())
103
+ continue;
104
+ // Check if flow exists in map
105
+ const flowData = agentData.flows[flowIdn];
106
+ if (!flowData) {
107
+ // Flow was deleted remotely
108
+ deletedEntities.push({
109
+ type: 'flow',
110
+ path: flowPath,
111
+ displayPath: `${projectIdn}/${agentIdn}/${flowIdn}`
112
+ });
113
+ continue;
114
+ }
115
+ // Scan for skills within this flow
116
+ try {
117
+ const localSkills = await fs.readdir(flowPath);
118
+ for (const skillIdn of localSkills) {
119
+ const skillPath = `${flowPath}/${skillIdn}`;
120
+ const skillStat = await fs.stat(skillPath).catch(() => null);
121
+ // Skip files and metadata.yaml
122
+ if (!skillStat || !skillStat.isDirectory())
123
+ continue;
124
+ // Check if skill exists in map
125
+ const skillData = flowData.skills[skillIdn];
126
+ if (!skillData) {
127
+ // Skill was deleted remotely
128
+ deletedEntities.push({
129
+ type: 'skill',
130
+ path: skillPath,
131
+ displayPath: `${projectIdn}/${agentIdn}/${flowIdn}/${skillIdn}`
132
+ });
133
+ }
134
+ }
135
+ }
136
+ catch (error) {
137
+ // Ignore errors reading flow directory
138
+ }
139
+ }
140
+ }
141
+ catch (error) {
142
+ // Ignore errors reading agent directory
143
+ }
144
+ }
145
+ }
146
+ catch (error) {
147
+ // Ignore errors reading project directory
148
+ }
149
+ }
150
+ if (deletedEntities.length === 0) {
151
+ if (verbose)
152
+ console.log('āœ… No deleted entities found');
153
+ return;
154
+ }
155
+ console.log(`\nšŸ” Found ${deletedEntities.length} entity(ies) that no longer exist remotely:`);
156
+ for (const entity of deletedEntities) {
157
+ console.log(` ${entity.type.padEnd(8)}: ${entity.displayPath}`);
158
+ }
159
+ console.log('\nThese entities will be deleted from your local filesystem.');
160
+ let globalDeleteAll = false;
161
+ for (const entity of deletedEntities) {
162
+ let shouldDelete = globalDeleteAll;
163
+ if (!globalDeleteAll) {
164
+ const choice = await askForDeletion(entity.type, entity.displayPath);
165
+ if (choice === 'quit') {
166
+ console.log('āŒ Deletion cancelled by user');
167
+ return;
168
+ }
169
+ else if (choice === 'all') {
170
+ globalDeleteAll = true;
171
+ shouldDelete = true;
172
+ }
173
+ else if (choice === 'yes') {
174
+ shouldDelete = true;
175
+ }
176
+ }
177
+ if (shouldDelete) {
178
+ await fs.remove(entity.path);
179
+ console.log(`šŸ—‘ļø Deleted: ${entity.displayPath}`);
180
+ }
181
+ else {
182
+ console.log(`ā­ļø Skipped: ${entity.displayPath}`);
183
+ }
184
+ }
185
+ console.log(`\nāœ… Cleanup completed`);
186
+ }
19
187
  /**
20
188
  * Pull a single project and all its data
21
189
  */
@@ -284,6 +452,8 @@ export async function pullSingleProject(client, customer, projectId, verbose = f
284
452
  newHashes[flowsYamlFilePath] = sha256(flowsYamlContent);
285
453
  // Save hashes (now including flows.yaml and attributes.yaml)
286
454
  await saveHashes(newHashes, customer.idn);
455
+ // Detect and clean up deleted entities
456
+ await cleanupDeletedEntities(customer.idn, existingMap, verbose);
287
457
  }
288
458
  /**
289
459
  * Pull all projects for a customer
package/dist/sync/push.js CHANGED
@@ -10,6 +10,7 @@ import yaml from 'js-yaml';
10
10
  import { generateFlowsYaml } from './metadata.js';
11
11
  import { isProjectMap, isLegacyProjectMap } from './projects.js';
12
12
  import { flowsYamlPath } from '../fsutil.js';
13
+ import { pushAllProjectAttributes } from './attributes.js';
13
14
  /**
14
15
  * Scan filesystem for local-only entities not in the project map yet
15
16
  */
@@ -445,6 +446,20 @@ export async function pushChanged(client, customer, verbose = false, shouldPubli
445
446
  }
446
447
  if (verbose)
447
448
  console.log(`šŸ”„ Scanned ${scanned} files, found ${pushed} changes`);
449
+ // Push project attributes for all projects
450
+ const projectsInfoMap = {};
451
+ for (const [projectIdn, projectData] of Object.entries(projects)) {
452
+ if (projectIdn && projectData.projectId) {
453
+ projectsInfoMap[projectIdn] = {
454
+ projectId: projectData.projectId,
455
+ projectIdn: projectData.projectIdn || projectIdn
456
+ };
457
+ }
458
+ }
459
+ const attributesUpdated = await pushAllProjectAttributes(client, customer, projectsInfoMap, verbose);
460
+ if (attributesUpdated > 0) {
461
+ pushed += attributesUpdated;
462
+ }
448
463
  // Regenerate flows.yaml if metadata was changed
449
464
  if (metadataChanged) {
450
465
  if (verbose)
@@ -63,7 +63,7 @@ export async function validateSkillFolder(customerIdn, projectIdn, agentIdn, flo
63
63
  const warnings = [];
64
64
  const errors = [];
65
65
  if (files.length === 0) {
66
- errors.push(`No script files found in skill folder: ${skillIdn}`);
66
+ errors.push(`No script files found in skill folder: ${folderPath}`);
67
67
  }
68
68
  else if (files.length > 1) {
69
69
  errors.push(`Multiple script files found in skill ${skillIdn}: ${files.map(f => f.fileName).join(', ')}`);
@@ -238,7 +238,8 @@ export async function status(customer, verbose = false) {
238
238
  console.warn(` Status check skipped - please keep only one script file.`);
239
239
  }
240
240
  else if (validation.files.length === 0) {
241
- console.log(`D ${skillIdn}/ (no script files)`);
241
+ const displayPath = projectIdn ? `${projectIdn}/${agentIdn}/${flowIdn}/${skillIdn}` : `${agentIdn}/${flowIdn}/${skillIdn}`;
242
+ console.log(`D ${displayPath}/ (no script files)`);
242
243
  dirty++;
243
244
  }
244
245
  continue;
@@ -246,7 +247,8 @@ export async function status(customer, verbose = false) {
246
247
  // Get the single valid script file
247
248
  const skillFile = await getSingleSkillFile(customer.idn, projectIdn, agentIdn, flowIdn, skillIdn);
248
249
  if (!skillFile) {
249
- console.log(`D ${skillIdn}/ (no valid script file)`);
250
+ const displayPath = projectIdn ? `${projectIdn}/${agentIdn}/${flowIdn}/${skillIdn}` : `${agentIdn}/${flowIdn}/${skillIdn}`;
251
+ console.log(`D ${displayPath}/ (no valid script file)`);
250
252
  dirty++;
251
253
  if (verbose)
252
254
  console.log(` āŒ No valid script file found: ${skillIdn}`);