newo 2.0.6 → 3.1.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 (63) hide show
  1. package/CHANGELOG.md +338 -224
  2. package/README.md +257 -0
  3. package/dist/api.d.ts +23 -1
  4. package/dist/api.js +114 -0
  5. package/dist/auth.js +4 -0
  6. package/dist/cli/commands/create-agent.d.ts +3 -0
  7. package/dist/cli/commands/create-agent.js +75 -0
  8. package/dist/cli/commands/create-attribute.d.ts +3 -0
  9. package/dist/cli/commands/create-attribute.js +63 -0
  10. package/dist/cli/commands/create-event.d.ts +3 -0
  11. package/dist/cli/commands/create-event.js +66 -0
  12. package/dist/cli/commands/create-flow.d.ts +3 -0
  13. package/dist/cli/commands/create-flow.js +100 -0
  14. package/dist/cli/commands/create-parameter.d.ts +3 -0
  15. package/dist/cli/commands/create-parameter.js +47 -0
  16. package/dist/cli/commands/create-persona.d.ts +3 -0
  17. package/dist/cli/commands/create-persona.js +43 -0
  18. package/dist/cli/commands/create-project.d.ts +3 -0
  19. package/dist/cli/commands/create-project.js +55 -0
  20. package/dist/cli/commands/create-skill.d.ts +3 -0
  21. package/dist/cli/commands/create-skill.js +115 -0
  22. package/dist/cli/commands/create-state.d.ts +3 -0
  23. package/dist/cli/commands/create-state.js +58 -0
  24. package/dist/cli/commands/delete-agent.d.ts +3 -0
  25. package/dist/cli/commands/delete-agent.js +70 -0
  26. package/dist/cli/commands/delete-flow.d.ts +3 -0
  27. package/dist/cli/commands/delete-flow.js +83 -0
  28. package/dist/cli/commands/delete-skill.d.ts +3 -0
  29. package/dist/cli/commands/delete-skill.js +87 -0
  30. package/dist/cli/commands/help.js +114 -22
  31. package/dist/cli/commands/push.js +4 -3
  32. package/dist/cli/commands/sandbox.d.ts +14 -0
  33. package/dist/cli/commands/sandbox.js +306 -0
  34. package/dist/cli.js +57 -0
  35. package/dist/sandbox/chat.d.ts +40 -0
  36. package/dist/sandbox/chat.js +280 -0
  37. package/dist/sync/push.d.ts +1 -1
  38. package/dist/sync/push.js +372 -4
  39. package/dist/sync/status.js +178 -1
  40. package/dist/types.d.ts +181 -1
  41. package/package.json +6 -3
  42. package/src/api.ts +172 -1
  43. package/src/auth.ts +7 -2
  44. package/src/cli/commands/create-agent.ts +96 -0
  45. package/src/cli/commands/create-attribute.ts +75 -0
  46. package/src/cli/commands/create-event.ts +79 -0
  47. package/src/cli/commands/create-flow.ts +124 -0
  48. package/src/cli/commands/create-parameter.ts +59 -0
  49. package/src/cli/commands/create-persona.ts +54 -0
  50. package/src/cli/commands/create-project.ts +66 -0
  51. package/src/cli/commands/create-skill.ts +144 -0
  52. package/src/cli/commands/create-state.ts +71 -0
  53. package/src/cli/commands/delete-agent.ts +90 -0
  54. package/src/cli/commands/delete-flow.ts +105 -0
  55. package/src/cli/commands/delete-skill.ts +110 -0
  56. package/src/cli/commands/help.ts +114 -22
  57. package/src/cli/commands/push.ts +5 -3
  58. package/src/cli/commands/sandbox.ts +365 -0
  59. package/src/cli.ts +71 -0
  60. package/src/sandbox/chat.ts +339 -0
  61. package/src/sync/push.ts +413 -5
  62. package/src/sync/status.ts +183 -1
  63. package/src/types.ts +220 -2
package/dist/sync/push.js CHANGED
@@ -1,19 +1,131 @@
1
1
  /**
2
2
  * Push operations for changed files
3
3
  */
4
- import { updateSkill } from '../api.js';
5
- import { ensureState, mapPath, skillMetadataPath } from '../fsutil.js';
6
- import { validateSkillFolder, getSingleSkillFile } from './skill-files.js';
4
+ import { updateSkill, createAgent, createFlow, createSkill, publishFlow } from '../api.js';
5
+ import { ensureState, mapPath, skillMetadataPath, projectDir, agentMetadataPath } from '../fsutil.js';
6
+ import { validateSkillFolder, getSingleSkillFile, getExtensionForRunner } from './skill-files.js';
7
7
  import fs from 'fs-extra';
8
8
  import { sha256, loadHashes, saveHashes } from '../hash.js';
9
9
  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
+ /**
14
+ * Scan filesystem for local-only entities not in the project map yet
15
+ */
16
+ async function scanForLocalOnlyEntities(customer, projects, verbose = false) {
17
+ const localEntities = [];
18
+ let agentCount = 0;
19
+ let flowCount = 0;
20
+ let skillCount = 0;
21
+ // Scan each project directory
22
+ for (const [projectIdn] of Object.entries(projects)) {
23
+ const projDir = projectDir(customer.idn, projectIdn);
24
+ if (!(await fs.pathExists(projDir)))
25
+ continue;
26
+ if (verbose)
27
+ console.log(`🔍 Scanning project directory: ${projDir}`);
28
+ // Get all subdirectories in the project (these should be agents)
29
+ const agentDirs = await fs.readdir(projDir);
30
+ for (const agentIdn of agentDirs) {
31
+ const agentPath = `${projDir}/${agentIdn}`;
32
+ const agentStat = await fs.stat(agentPath);
33
+ // Skip files, only process directories
34
+ if (!agentStat.isDirectory())
35
+ continue;
36
+ // Skip if it's not really an agent directory (no metadata.yaml)
37
+ const agentMetaPath = agentMetadataPath(customer.idn, projectIdn, agentIdn);
38
+ if (!(await fs.pathExists(agentMetaPath)))
39
+ continue;
40
+ // Check if this agent is already in the project map
41
+ const projectData = projects[projectIdn];
42
+ if (!projectData?.agents[agentIdn]) {
43
+ // This is a local-only agent!
44
+ localEntities.push({
45
+ type: 'agent',
46
+ path: agentMetaPath,
47
+ idn: agentIdn,
48
+ projectIdn
49
+ });
50
+ agentCount++;
51
+ if (verbose)
52
+ console.log(` 🆕 Found local-only agent: ${agentIdn}`);
53
+ }
54
+ // Now scan for flows within this agent (regardless of whether agent is local-only or not)
55
+ try {
56
+ const flowDirs = await fs.readdir(agentPath);
57
+ for (const flowIdn of flowDirs) {
58
+ const flowPath = `${agentPath}/${flowIdn}`;
59
+ const flowStat = await fs.stat(flowPath);
60
+ // Skip files, only process directories
61
+ if (!flowStat.isDirectory())
62
+ continue;
63
+ // Skip if it's not really a flow directory (no metadata.yaml)
64
+ const flowMetaPath = `${flowPath}/metadata.yaml`;
65
+ if (!(await fs.pathExists(flowMetaPath)))
66
+ continue;
67
+ // Check if this flow exists in the project map
68
+ const agentData = projectData?.agents[agentIdn];
69
+ if (!agentData?.flows[flowIdn]) {
70
+ // This is a local-only flow!
71
+ localEntities.push({
72
+ type: 'flow',
73
+ path: flowMetaPath,
74
+ idn: flowIdn,
75
+ projectIdn,
76
+ agentIdn
77
+ });
78
+ flowCount++;
79
+ if (verbose)
80
+ console.log(` 🆕 Found local-only flow: ${agentIdn}/${flowIdn}`);
81
+ }
82
+ // Now scan for skills within this flow (regardless of whether flow is local-only or not)
83
+ try {
84
+ const skillDirs = await fs.readdir(flowPath);
85
+ for (const skillIdn of skillDirs) {
86
+ const skillPath = `${flowPath}/${skillIdn}`;
87
+ const skillStat = await fs.stat(skillPath);
88
+ // Skip files, only process directories
89
+ if (!skillStat.isDirectory())
90
+ continue;
91
+ // Skip if it's not really a skill directory (no metadata.yaml)
92
+ const skillMetaPath = `${skillPath}/metadata.yaml`;
93
+ if (!(await fs.pathExists(skillMetaPath)))
94
+ continue;
95
+ // Check if this skill exists in the project map
96
+ const flowData = agentData?.flows[flowIdn];
97
+ if (!flowData?.skills[skillIdn]) {
98
+ // This is a local-only skill!
99
+ localEntities.push({
100
+ type: 'skill',
101
+ path: skillMetaPath,
102
+ idn: skillIdn,
103
+ projectIdn,
104
+ agentIdn,
105
+ flowIdn
106
+ });
107
+ skillCount++;
108
+ if (verbose)
109
+ console.log(` 🆕 Found local-only skill: ${agentIdn}/${flowIdn}/${skillIdn}`);
110
+ }
111
+ }
112
+ }
113
+ catch (error) {
114
+ // Ignore errors reading flow directory
115
+ }
116
+ }
117
+ }
118
+ catch (error) {
119
+ // Ignore errors reading agent directory
120
+ }
121
+ }
122
+ }
123
+ return { agentCount, flowCount, skillCount, entities: localEntities };
124
+ }
13
125
  /**
14
126
  * Push changed files to NEWO platform
15
127
  */
16
- export async function pushChanged(client, customer, verbose = false) {
128
+ export async function pushChanged(client, customer, verbose = false, shouldPublish = true) {
17
129
  await ensureState(customer.idn);
18
130
  if (!(await fs.pathExists(mapPath(customer.idn)))) {
19
131
  console.log(`No map for customer ${customer.idn}. Run \`newo pull --customer ${customer.idn}\` first.`);
@@ -33,6 +145,184 @@ export async function pushChanged(client, customer, verbose = false) {
33
145
  : isLegacyProjectMap(idMapData)
34
146
  ? { '': idMapData }
35
147
  : (() => { throw new Error('Invalid project map format'); })();
148
+ // First, handle any local-only entities (created locally but not yet pushed)
149
+ const localScan = await scanForLocalOnlyEntities(customer, projects, verbose);
150
+ const totalLocalEntities = localScan.agentCount + localScan.flowCount + localScan.skillCount;
151
+ if (totalLocalEntities > 0) {
152
+ console.log(`📤 Found ${localScan.agentCount} new agent(s), ${localScan.flowCount} new flow(s), ${localScan.skillCount} new skill(s) to create...`);
153
+ // Process in order: agents first, then flows, then skills
154
+ const sortedEntities = localScan.entities.sort((a, b) => {
155
+ const typeOrder = { 'agent': 0, 'flow': 1, 'skill': 2 };
156
+ return typeOrder[a.type] - typeOrder[b.type];
157
+ });
158
+ for (const entity of sortedEntities) {
159
+ if (entity.type === 'agent') {
160
+ try {
161
+ // Read agent metadata
162
+ const metadataContent = await fs.readFile(entity.path, 'utf8');
163
+ const metadata = yaml.load(metadataContent);
164
+ if (verbose)
165
+ console.log(`📤 Creating agent: ${entity.idn}`);
166
+ // Get project ID from the project map
167
+ const projectData = projects[entity.projectIdn];
168
+ if (!projectData?.projectId) {
169
+ console.error(`❌ Project ID not found for project: ${entity.projectIdn}`);
170
+ continue;
171
+ }
172
+ // Create agent on NEWO platform using project-specific v2 endpoint
173
+ const createAgentRequest = {
174
+ idn: metadata.idn,
175
+ title: metadata.title || metadata.idn,
176
+ description: metadata.description || null,
177
+ persona_id: metadata.persona_id || null
178
+ };
179
+ const createResponse = await createAgent(client, projectData.projectId, createAgentRequest);
180
+ console.log(`✅ Agent created: ${entity.idn} (ID: ${createResponse.id})`);
181
+ pushed++;
182
+ metadataChanged = true;
183
+ // Update the metadata with the new ID
184
+ metadata.id = createResponse.id;
185
+ metadata.updated_at = new Date().toISOString();
186
+ const updatedMetadataYaml = yaml.dump(metadata, { indent: 2, quotingType: '"', forceQuotes: false });
187
+ await fs.writeFile(entity.path, updatedMetadataYaml);
188
+ // Update the project map to include the new agent
189
+ if (!projectData.agents[entity.idn]) {
190
+ projectData.agents[entity.idn] = {
191
+ id: createResponse.id,
192
+ flows: {}
193
+ };
194
+ }
195
+ }
196
+ catch (error) {
197
+ console.error(`❌ Failed to create agent ${entity.idn}:`, error.response?.data?.message || error.message);
198
+ }
199
+ }
200
+ else if (entity.type === 'flow') {
201
+ try {
202
+ // Read flow metadata
203
+ const metadataContent = await fs.readFile(entity.path, 'utf8');
204
+ const metadata = yaml.load(metadataContent);
205
+ if (verbose)
206
+ console.log(`📤 Creating flow: ${entity.agentIdn}/${entity.idn}`);
207
+ // Get agent ID from the project map
208
+ const projectData = projects[entity.projectIdn];
209
+ if (!entity.agentIdn) {
210
+ console.error(`❌ Agent IDN missing for flow: ${entity.idn}`);
211
+ continue;
212
+ }
213
+ const agentData = projectData?.agents[entity.agentIdn];
214
+ if (!agentData?.id) {
215
+ console.error(`❌ Agent ID not found for agent: ${entity.agentIdn}`);
216
+ continue;
217
+ }
218
+ // Create flow on NEWO platform
219
+ const createFlowRequest = {
220
+ idn: metadata.idn,
221
+ title: metadata.title || metadata.idn
222
+ };
223
+ const createResponse = await createFlow(client, agentData.id, createFlowRequest);
224
+ console.log(`✅ Flow created: ${entity.idn} (ID: ${createResponse.id})`);
225
+ pushed++;
226
+ metadataChanged = true;
227
+ // Handle the special case where NEWO flow API returns empty response
228
+ if (createResponse.id === 'pending-sync') {
229
+ console.log(`✅ Flow created: ${entity.idn} (ID will be synced on next pull)`);
230
+ // Mark flow as created but pending ID sync
231
+ metadata.id = ''; // Keep empty until sync
232
+ metadata.updated_at = new Date().toISOString();
233
+ const updatedMetadataYaml = yaml.dump(metadata, { indent: 2, quotingType: '"', forceQuotes: false });
234
+ await fs.writeFile(entity.path, updatedMetadataYaml);
235
+ // Update the project map with empty ID (will be filled by pull)
236
+ if (!agentData.flows[entity.idn]) {
237
+ agentData.flows[entity.idn] = {
238
+ id: '', // Empty until synced
239
+ skills: {}
240
+ };
241
+ }
242
+ }
243
+ else {
244
+ // Normal case with ID returned
245
+ metadata.id = createResponse.id;
246
+ metadata.updated_at = new Date().toISOString();
247
+ const updatedMetadataYaml = yaml.dump(metadata, { indent: 2, quotingType: '"', forceQuotes: false });
248
+ await fs.writeFile(entity.path, updatedMetadataYaml);
249
+ // Update the project map to include the new flow
250
+ if (!agentData.flows[entity.idn]) {
251
+ agentData.flows[entity.idn] = {
252
+ id: createResponse.id,
253
+ skills: {}
254
+ };
255
+ }
256
+ }
257
+ }
258
+ catch (error) {
259
+ console.error(`❌ Failed to create flow ${entity.idn}:`, error.response?.data?.message || error.message);
260
+ }
261
+ }
262
+ else if (entity.type === 'skill') {
263
+ try {
264
+ // Read skill metadata
265
+ const metadataContent = await fs.readFile(entity.path, 'utf8');
266
+ const metadata = yaml.load(metadataContent);
267
+ if (verbose)
268
+ console.log(`📤 Creating skill: ${entity.agentIdn}/${entity.flowIdn}/${entity.idn}`);
269
+ // Get flow ID from the project map
270
+ const projectData = projects[entity.projectIdn];
271
+ if (!entity.agentIdn || !entity.flowIdn) {
272
+ console.error(`❌ Agent IDN or Flow IDN missing for skill: ${entity.idn}`);
273
+ continue;
274
+ }
275
+ const agentData = projectData?.agents[entity.agentIdn];
276
+ const flowData = agentData?.flows[entity.flowIdn];
277
+ if (!flowData?.id) {
278
+ console.error(`❌ Flow ID not found for flow: ${entity.flowIdn}`);
279
+ continue;
280
+ }
281
+ // Read the skill script content
282
+ const skillFolderBase = entity.path.replace('/metadata.yaml', '');
283
+ const scriptExtension = getExtensionForRunner(metadata.runner_type);
284
+ const scriptPath = `${skillFolderBase}/${entity.idn}.${scriptExtension}`;
285
+ let scriptContent = '';
286
+ if (await fs.pathExists(scriptPath)) {
287
+ scriptContent = await fs.readFile(scriptPath, 'utf8');
288
+ }
289
+ // Create skill on NEWO platform
290
+ const createSkillRequest = {
291
+ idn: metadata.idn,
292
+ title: metadata.title || metadata.idn,
293
+ prompt_script: scriptContent,
294
+ runner_type: metadata.runner_type,
295
+ model: metadata.model,
296
+ path: "", // Empty path as shown in curl example
297
+ parameters: metadata.parameters || []
298
+ };
299
+ const createResponse = await createSkill(client, flowData.id, createSkillRequest);
300
+ console.log(`✅ Skill created: ${entity.idn} (ID: ${createResponse.id})`);
301
+ pushed++;
302
+ metadataChanged = true;
303
+ // Update the metadata with the new ID
304
+ metadata.id = createResponse.id;
305
+ metadata.updated_at = new Date().toISOString();
306
+ const updatedMetadataYaml = yaml.dump(metadata, { indent: 2, quotingType: '"', forceQuotes: false });
307
+ await fs.writeFile(entity.path, updatedMetadataYaml);
308
+ // Update the project map to include the new skill
309
+ if (!flowData.skills[entity.idn]) {
310
+ flowData.skills[entity.idn] = {
311
+ id: createResponse.id,
312
+ idn: metadata.idn,
313
+ title: metadata.title || metadata.idn,
314
+ runner_type: metadata.runner_type,
315
+ model: metadata.model,
316
+ parameters: metadata.parameters || []
317
+ };
318
+ }
319
+ }
320
+ catch (error) {
321
+ console.error(`❌ Failed to create skill ${entity.idn}:`, error.response?.data?.message || error.message);
322
+ }
323
+ }
324
+ }
325
+ }
36
326
  for (const [projectIdn, projectData] of Object.entries(projects)) {
37
327
  if (verbose && projectIdn)
38
328
  console.log(`📁 Checking project: ${projectIdn}`);
@@ -164,8 +454,86 @@ export async function pushChanged(client, customer, verbose = false) {
164
454
  const flowsYamlFilePath = flowsYamlPath(customer.idn);
165
455
  newHashes[flowsYamlFilePath] = sha256(flowsYamlContent);
166
456
  }
457
+ // Save updated project map if metadata changed (new agents added)
458
+ if (metadataChanged) {
459
+ const updatedMapData = isProjectMap(idMapData)
460
+ ? { projects }
461
+ : projects['']; // Legacy format
462
+ if (verbose)
463
+ console.log(`💾 Saving updated project map...`);
464
+ await fs.writeJson(mapPath(customer.idn), updatedMapData, { spaces: 2 });
465
+ }
167
466
  // Save updated hashes
168
467
  await saveHashes(newHashes, customer.idn);
169
468
  console.log(pushed ? `${pushed} file(s) pushed.` : 'No changes to push.');
469
+ // Publish flows if requested (default behavior)
470
+ if (shouldPublish && pushed > 0) {
471
+ if (verbose)
472
+ console.log('\n🚀 Publishing flows...');
473
+ let publishedFlows = 0;
474
+ let failedFlows = 0;
475
+ const publishErrors = [];
476
+ for (const [, projectData] of Object.entries(projects)) {
477
+ for (const [, agentObj] of Object.entries(projectData.agents)) {
478
+ for (const [flowIdn, flowObj] of Object.entries(agentObj.flows)) {
479
+ if (flowObj.id) {
480
+ try {
481
+ const publishData = {
482
+ version: "1.0",
483
+ description: "Published via NEWO CLI",
484
+ type: "public"
485
+ };
486
+ await publishFlow(client, flowObj.id, publishData);
487
+ if (verbose)
488
+ console.log(`📤 Published flow: ${flowIdn} (${flowObj.id})`);
489
+ publishedFlows++;
490
+ }
491
+ catch (error) {
492
+ failedFlows++;
493
+ // Extract detailed error information from API response
494
+ const errorMessage = error.response?.data?.message || error.message || 'Unknown error';
495
+ const errorDetails = error.response?.data?.reasons || error.response?.data?.errors || error.response?.data?.detail;
496
+ publishErrors.push({
497
+ flowIdn,
498
+ error: errorMessage,
499
+ details: errorDetails
500
+ });
501
+ // Always show publish errors (not just in verbose mode)
502
+ console.error(`❌ Failed to publish flow '${flowIdn}': ${errorMessage}`);
503
+ if (errorDetails) {
504
+ if (Array.isArray(errorDetails)) {
505
+ console.error(` Reasons:`);
506
+ errorDetails.forEach((reason) => {
507
+ console.error(` • ${reason}`);
508
+ });
509
+ }
510
+ else if (typeof errorDetails === 'object') {
511
+ console.error(` Details: ${JSON.stringify(errorDetails, null, 2)}`);
512
+ }
513
+ else {
514
+ console.error(` Details: ${errorDetails}`);
515
+ }
516
+ }
517
+ }
518
+ }
519
+ }
520
+ }
521
+ }
522
+ // Summary message
523
+ if (publishedFlows > 0 || failedFlows > 0) {
524
+ console.log(`\n🚀 Publish summary: ${publishedFlows} succeeded, ${failedFlows} failed.`);
525
+ if (failedFlows > 0) {
526
+ console.log(`\n⚠️ ${failedFlows} flow(s) failed to publish due to validation errors.`);
527
+ console.log(` Fix the errors above and run 'npm run push' again.`);
528
+ }
529
+ }
530
+ else if (verbose) {
531
+ console.log('\n💡 No flows to publish.');
532
+ }
533
+ }
534
+ // If we created flows, recommend a pull to sync flow IDs
535
+ if (localScan.flowCount > 0) {
536
+ console.log('\n💡 Tip: Run "newo pull" to sync flow IDs and enable skill creation.');
537
+ }
170
538
  }
171
539
  //# sourceMappingURL=push.js.map
@@ -4,7 +4,7 @@
4
4
  import fs from 'fs-extra';
5
5
  import yaml from 'js-yaml';
6
6
  import { sha256, loadHashes } from '../hash.js';
7
- import { ensureState, mapPath, skillMetadataPath, customerAttributesPath, customerAttributesBackupPath, flowsYamlPath } from '../fsutil.js';
7
+ import { ensureState, mapPath, skillMetadataPath, customerAttributesPath, customerAttributesBackupPath, flowsYamlPath, projectDir, agentMetadataPath, flowMetadataPath } from '../fsutil.js';
8
8
  import { validateSkillFolder, getSingleSkillFile } from './skill-files.js';
9
9
  // Type guards for project map formats
10
10
  function isProjectMap(x) {
@@ -13,6 +13,118 @@ function isProjectMap(x) {
13
13
  function isLegacyProjectMap(x) {
14
14
  return typeof x === 'object' && x !== null && 'projectId' in x && 'agents' in x;
15
15
  }
16
+ /**
17
+ * Scan filesystem for local-only entities not in the project map yet
18
+ */
19
+ async function scanForLocalOnlyEntities(customer, projects, verbose = false) {
20
+ const localEntities = [];
21
+ let agentCount = 0;
22
+ let flowCount = 0;
23
+ let skillCount = 0;
24
+ // Scan each project directory
25
+ for (const [projectIdn] of Object.entries(projects)) {
26
+ const projDir = projectDir(customer.idn, projectIdn);
27
+ if (!(await fs.pathExists(projDir)))
28
+ continue;
29
+ if (verbose)
30
+ console.log(`🔍 Scanning project directory: ${projDir}`);
31
+ // Get all subdirectories in the project (these should be agents)
32
+ const agentDirs = await fs.readdir(projDir);
33
+ for (const agentIdn of agentDirs) {
34
+ const agentPath = `${projDir}/${agentIdn}`;
35
+ const agentStat = await fs.stat(agentPath);
36
+ // Skip files, only process directories
37
+ if (!agentStat.isDirectory())
38
+ continue;
39
+ // Skip if it's not really an agent directory (no metadata.yaml)
40
+ const agentMetaPath = agentMetadataPath(customer.idn, projectIdn, agentIdn);
41
+ if (!(await fs.pathExists(agentMetaPath)))
42
+ continue;
43
+ // Check if this agent is already in the project map
44
+ const projectData = projects[projectIdn];
45
+ if (!projectData?.agents[agentIdn]) {
46
+ // This is a local-only agent!
47
+ localEntities.push({
48
+ type: 'agent',
49
+ path: agentMetaPath,
50
+ idn: agentIdn,
51
+ projectIdn
52
+ });
53
+ agentCount++;
54
+ if (verbose)
55
+ console.log(` 🆕 Found local-only agent: ${agentIdn}`);
56
+ }
57
+ // Now scan for flows within this agent (regardless of whether agent is local-only or not)
58
+ try {
59
+ const flowDirs = await fs.readdir(agentPath);
60
+ for (const flowIdn of flowDirs) {
61
+ const flowPath = `${agentPath}/${flowIdn}`;
62
+ const flowStat = await fs.stat(flowPath);
63
+ // Skip files, only process directories
64
+ if (!flowStat.isDirectory())
65
+ continue;
66
+ // Skip if it's not really a flow directory (no metadata.yaml)
67
+ const flowMetaPath = flowMetadataPath(customer.idn, projectIdn, agentIdn, flowIdn);
68
+ if (!(await fs.pathExists(flowMetaPath)))
69
+ continue;
70
+ // Check if this flow exists in the project map
71
+ const agentData = projectData?.agents[agentIdn];
72
+ if (!agentData?.flows[flowIdn]) {
73
+ // This is a local-only flow!
74
+ localEntities.push({
75
+ type: 'flow',
76
+ path: flowMetaPath,
77
+ idn: flowIdn,
78
+ projectIdn,
79
+ agentIdn
80
+ });
81
+ flowCount++;
82
+ if (verbose)
83
+ console.log(` 🆕 Found local-only flow: ${agentIdn}/${flowIdn}`);
84
+ }
85
+ // Now scan for skills within this flow (regardless of whether flow is local-only or not)
86
+ try {
87
+ const skillDirs = await fs.readdir(flowPath);
88
+ for (const skillIdn of skillDirs) {
89
+ const skillPath = `${flowPath}/${skillIdn}`;
90
+ const skillStat = await fs.stat(skillPath);
91
+ // Skip files, only process directories
92
+ if (!skillStat.isDirectory())
93
+ continue;
94
+ // Skip if it's not really a skill directory (no metadata.yaml)
95
+ const skillMetaPath = skillMetadataPath(customer.idn, projectIdn, agentIdn, flowIdn, skillIdn);
96
+ if (!(await fs.pathExists(skillMetaPath)))
97
+ continue;
98
+ // Check if this skill exists in the project map
99
+ const flowData = agentData?.flows[flowIdn];
100
+ if (!flowData?.skills[skillIdn]) {
101
+ // This is a local-only skill!
102
+ localEntities.push({
103
+ type: 'skill',
104
+ path: skillMetaPath,
105
+ idn: skillIdn,
106
+ projectIdn,
107
+ agentIdn,
108
+ flowIdn
109
+ });
110
+ skillCount++;
111
+ if (verbose)
112
+ console.log(` 🆕 Found local-only skill: ${agentIdn}/${flowIdn}/${skillIdn}`);
113
+ }
114
+ }
115
+ }
116
+ catch (error) {
117
+ // Ignore errors reading flow directory
118
+ }
119
+ }
120
+ }
121
+ catch (error) {
122
+ // Ignore errors reading agent directory
123
+ }
124
+ }
125
+ }
126
+ return { agentCount, flowCount, skillCount, entities: localEntities };
127
+ }
16
128
  /**
17
129
  * Check status of files for a customer
18
130
  */
@@ -33,6 +145,71 @@ export async function status(customer, verbose = false) {
33
145
  : isLegacyProjectMap(idMapData)
34
146
  ? { '': idMapData }
35
147
  : (() => { throw new Error('Invalid project map format'); })();
148
+ // First, scan for any local-only entities (created locally but not yet pushed)
149
+ const localScan = await scanForLocalOnlyEntities(customer, projects, verbose);
150
+ const totalLocalEntities = localScan.agentCount + localScan.flowCount + localScan.skillCount;
151
+ if (totalLocalEntities > 0) {
152
+ dirty += totalLocalEntities;
153
+ for (const entity of localScan.entities) {
154
+ if (entity.type === 'agent') {
155
+ console.log(`A ${entity.projectIdn}/${entity.idn}/metadata.yaml (new agent)`);
156
+ if (verbose) {
157
+ try {
158
+ const metadataContent = await fs.readFile(entity.path, 'utf8');
159
+ const metadata = yaml.load(metadataContent);
160
+ console.log(` 📊 New Agent: ${entity.idn}`);
161
+ if (metadata?.title && metadata.title !== entity.idn) {
162
+ console.log(` • Title: ${metadata.title}`);
163
+ }
164
+ if (metadata?.description) {
165
+ console.log(` • Description: ${metadata.description}`);
166
+ }
167
+ }
168
+ catch (e) {
169
+ // Ignore parsing errors
170
+ }
171
+ }
172
+ }
173
+ else if (entity.type === 'flow') {
174
+ console.log(`A ${entity.projectIdn}/${entity.agentIdn}/${entity.idn}/metadata.yaml (new flow)`);
175
+ if (verbose) {
176
+ try {
177
+ const metadataContent = await fs.readFile(entity.path, 'utf8');
178
+ const metadata = yaml.load(metadataContent);
179
+ console.log(` 📊 New Flow: ${entity.idn}`);
180
+ if (metadata?.title && metadata.title !== entity.idn) {
181
+ console.log(` • Title: ${metadata.title}`);
182
+ }
183
+ if (metadata?.default_runner_type) {
184
+ console.log(` • Runner: ${metadata.default_runner_type}`);
185
+ }
186
+ }
187
+ catch (e) {
188
+ // Ignore parsing errors
189
+ }
190
+ }
191
+ }
192
+ else if (entity.type === 'skill') {
193
+ console.log(`A ${entity.projectIdn}/${entity.agentIdn}/${entity.flowIdn}/${entity.idn}/metadata.yaml (new skill)`);
194
+ if (verbose) {
195
+ try {
196
+ const metadataContent = await fs.readFile(entity.path, 'utf8');
197
+ const metadata = yaml.load(metadataContent);
198
+ console.log(` 📊 New Skill: ${entity.idn}`);
199
+ if (metadata?.title && metadata.title !== entity.idn) {
200
+ console.log(` • Title: ${metadata.title}`);
201
+ }
202
+ if (metadata?.runner_type) {
203
+ console.log(` • Runner: ${metadata.runner_type}`);
204
+ }
205
+ }
206
+ catch (e) {
207
+ // Ignore parsing errors
208
+ }
209
+ }
210
+ }
211
+ }
212
+ }
36
213
  for (const [projectIdn, projectData] of Object.entries(projects)) {
37
214
  if (verbose && projectIdn)
38
215
  console.log(`📁 Checking project: ${projectIdn}`);