newo 3.7.3 → 3.7.5

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.
@@ -14,13 +14,14 @@
14
14
  * skills/{SkillIdn}.nsl|.nslg
15
15
  */
16
16
  import fs from 'fs-extra';
17
- import { listProjects, listAgents, listFlowSkills, listFlowEvents, listFlowStates, updateSkill, publishFlow, getProjectAttributes, getCustomerAttributes, listLibraries, updateLibrarySkill, getFlow, } from '../../../api.js';
17
+ import path from 'path';
18
+ import { listProjects, listAgents, listFlowSkills, listFlowEvents, listFlowStates, createSkill, createSkillParameter, updateSkill, publishFlow, getProjectAttributes, getCustomerAttributes, listLibraries, updateLibrarySkill, getFlow, } from '../../../api.js';
18
19
  import { syncFlowMetadata, emptyFlowSyncCounts, totalFlowSyncOps, describeFlowSyncCounts } from '../../../sync/flow-metadata.js';
19
20
  import { ensureStateOnly, writeFileSafe, mapPath, } from '../../../fsutil.js';
20
21
  import { sha256, saveHashes, loadHashes } from '../../../hash.js';
21
- import { v2ImportVersionPath, v2ProjectYamlPath, v2AgentYamlPath, v2FlowYamlPath, v2SkillScriptPath, v2SkillRelativePath, v2ProjectAttributesPath, v2CustomerAttributesPath, v2AkbDir, v2AkbPath, v2LibraryYamlPath, v2LibrarySkillScriptPath, v2LibrarySkillRelativePath, } from '../../../format/paths-v2.js';
22
+ import { v2ImportVersionPath, v2ProjectYamlPath, v2AgentDir, v2AgentYamlPath, v2FlowYamlPath, v2SkillScriptPath, v2SkillRelativePath, v2ProjectAttributesPath, v2CustomerAttributesPath, v2AkbDir, v2AkbPath, v2LibraryYamlPath, v2LibrarySkillScriptPath, v2LibrarySkillRelativePath, } from '../../../format/paths-v2.js';
22
23
  import { V2_IMPORT_VERSION, } from '../../../format/types.js';
23
- import { generateV2FlowYaml, generateV2ProjectYaml, generateV2AgentYaml, buildV2InlineSkill, buildV2FlowEvent, buildV2StateField, parseV2FlowYaml, } from '../../../format/v2-yaml.js';
24
+ import { generateV2FlowYaml, generateV2ProjectYaml, generateV2AgentYaml, parseV2FlowYaml, buildV2InlineSkill, buildV2FlowEvent, buildV2StateField, } from '../../../format/v2-yaml.js';
24
25
  import { isContentDifferent } from '../../../sync/skill-files.js';
25
26
  import yaml from 'js-yaml';
26
27
  import { patchYamlToPyyaml } from '../../../format/yaml-patch.js';
@@ -438,8 +439,15 @@ export class V2ProjectSyncStrategy {
438
439
  return result;
439
440
  }
440
441
  const mapData = await fs.readJson(mapFile);
442
+ const metadataSync = await this.syncV2FlowYamlDefinitions(client, customer, mapData, newHashes);
443
+ result.created += metadataSync.created;
444
+ result.updated += metadataSync.updated;
445
+ result.errors.push(...metadataSync.errors);
441
446
  for (const change of changes) {
442
447
  try {
448
+ if (metadataSync.syncedPaths.has(change.path)) {
449
+ continue;
450
+ }
443
451
  if (change.operation === 'modified') {
444
452
  // V2 flow YAML: newo_customers/{cust}/{proj}/agents/{agent}/flows/{flow}/{flow}.yaml
445
453
  // The flow YAML carries title, events, and state_fields inline, so
@@ -453,7 +461,7 @@ export class V2ProjectSyncStrategy {
453
461
  const isLibrary = change.path.includes('/libraries/');
454
462
  const count = isLibrary
455
463
  ? await this.pushV2LibrarySkillUpdate(client, change, mapData, newHashes)
456
- : await this.pushV2SkillUpdate(client, change, mapData, newHashes);
464
+ : await this.pushV2SkillUpdate(client, change, mapData, newHashes, customer.idn);
457
465
  result.updated += count;
458
466
  }
459
467
  }
@@ -461,6 +469,9 @@ export class V2ProjectSyncStrategy {
461
469
  result.errors.push(`Failed to push ${change.path}: ${error instanceof Error ? error.message : String(error)}`);
462
470
  }
463
471
  }
472
+ if (metadataSync.created > 0 || metadataSync.updated > 0) {
473
+ await writeFileSafe(mapFile, JSON.stringify(mapData, null, 2));
474
+ }
464
475
  await saveHashes(newHashes, customer.idn);
465
476
  if (result.created > 0 || result.updated > 0) {
466
477
  await this.publishAllFlows(client, mapData);
@@ -564,28 +575,256 @@ export class V2ProjectSyncStrategy {
564
575
  newHashes[change.path] = sha256(content);
565
576
  return total;
566
577
  }
578
+ /**
579
+ * Reconcile inline skill definitions from V2 flow YAML before pushing scripts.
580
+ *
581
+ * V2 keeps skill metadata (model, runner_type, parameters) in the flow YAML,
582
+ * not in a separate skill metadata file. The map only contains the remote IDs
583
+ * from a previous pull, so new local skills must be created before their
584
+ * callers can be published.
585
+ */
586
+ async syncV2FlowYamlDefinitions(client, customer, mapData, newHashes) {
587
+ let created = 0;
588
+ let updated = 0;
589
+ const syncedPaths = new Set();
590
+ const errors = [];
591
+ for (const [projectIdn, projectData] of Object.entries(mapData.projects)) {
592
+ for (const [agentIdn, agentData] of Object.entries(projectData.agents)) {
593
+ for (const [flowIdn, flowData] of Object.entries(agentData.flows)) {
594
+ const flowYamlPath = v2FlowYamlPath(customer.idn, projectIdn, agentIdn, flowIdn);
595
+ if (!(await fs.pathExists(flowYamlPath))) {
596
+ continue;
597
+ }
598
+ let flowDef;
599
+ try {
600
+ flowDef = await parseV2FlowYaml(flowYamlPath);
601
+ }
602
+ catch (error) {
603
+ this.logger.warn(`[newo_v2] Failed to parse flow YAML ${flowYamlPath}: ${error instanceof Error ? error.message : String(error)}`);
604
+ continue;
605
+ }
606
+ for (const skill of flowDef.skills || []) {
607
+ const skillLocator = `${projectIdn}/${agentIdn}/${flowIdn}/${skill.idn}`;
608
+ // Per-skill failure isolation: one broken skill must not abort the
609
+ // push of every other project/flow in the workspace.
610
+ try {
611
+ const runnerType = this.normalizeRunnerType(skill.runner_type);
612
+ const scriptPath = await this.resolveV2FlowSkillScriptPath(customer.idn, projectIdn, agentIdn, flowIdn, skill.idn, runnerType, skill.prompt_script);
613
+ if (!(await fs.pathExists(scriptPath))) {
614
+ errors.push(`[newo_v2] Missing script for skill ${skillLocator}: ${scriptPath}`);
615
+ continue;
616
+ }
617
+ const content = await fs.readFile(scriptPath, 'utf8');
618
+ const localMetadata = this.buildV2SkillMetadataFromYaml(skill, flowDef, runnerType, flowData.skills[skill.idn]);
619
+ const existingSkill = flowData.skills[skill.idn];
620
+ if (!existingSkill) {
621
+ this.assertSkillModelResolved(localMetadata, skillLocator);
622
+ try {
623
+ const createdSkill = await createSkill(client, flowData.id, {
624
+ idn: localMetadata.idn,
625
+ title: localMetadata.title,
626
+ prompt_script: content,
627
+ runner_type: localMetadata.runner_type,
628
+ model: localMetadata.model,
629
+ parameters: localMetadata.parameters,
630
+ path: localMetadata.path || ''
631
+ });
632
+ // The create endpoint ignores inline `parameters` (verified
633
+ // against the live platform) — create them explicitly.
634
+ await this.createMissingSkillParameters(client, { ...localMetadata, id: createdSkill.id, parameters: [] }, localMetadata);
635
+ flowData.skills[skill.idn] = {
636
+ ...localMetadata,
637
+ id: createdSkill.id
638
+ };
639
+ newHashes[scriptPath] = sha256(content);
640
+ syncedPaths.add(scriptPath);
641
+ created++;
642
+ this.logger.info(`[newo_v2] Created skill: ${flowIdn}/${skill.idn}`);
643
+ }
644
+ catch (error) {
645
+ if (!this.isAlreadyExistsApiError(error)) {
646
+ throw error;
647
+ }
648
+ const remoteSkills = await listFlowSkills(client, flowData.id);
649
+ const remoteSkill = remoteSkills.find(s => s.idn === skill.idn);
650
+ if (!remoteSkill) {
651
+ throw error;
652
+ }
653
+ const remoteMetadata = {
654
+ id: remoteSkill.id,
655
+ idn: remoteSkill.idn,
656
+ title: remoteSkill.title,
657
+ runner_type: remoteSkill.runner_type,
658
+ model: remoteSkill.model,
659
+ parameters: this.normalizeParameters(remoteSkill.parameters),
660
+ path: remoteSkill.path
661
+ };
662
+ await this.createMissingSkillParameters(client, remoteMetadata, localMetadata);
663
+ await updateSkill(client, {
664
+ id: remoteSkill.id,
665
+ title: localMetadata.title,
666
+ idn: localMetadata.idn,
667
+ prompt_script: content,
668
+ runner_type: localMetadata.runner_type,
669
+ model: localMetadata.model,
670
+ parameters: localMetadata.parameters,
671
+ path: remoteSkill.path || localMetadata.path
672
+ });
673
+ flowData.skills[skill.idn] = {
674
+ ...localMetadata,
675
+ id: remoteSkill.id,
676
+ path: remoteSkill.path || localMetadata.path
677
+ };
678
+ newHashes[scriptPath] = sha256(content);
679
+ syncedPaths.add(scriptPath);
680
+ updated++;
681
+ this.logger.info(`[newo_v2] Reused existing skill: ${flowIdn}/${skill.idn}`);
682
+ }
683
+ continue;
684
+ }
685
+ const createdParameters = await this.createMissingSkillParameters(client, existingSkill, localMetadata);
686
+ if (createdParameters > 0 || this.skillMetadataDiffers(existingSkill, localMetadata)) {
687
+ this.assertSkillModelResolved(localMetadata, skillLocator);
688
+ await updateSkill(client, {
689
+ id: existingSkill.id,
690
+ title: localMetadata.title,
691
+ idn: localMetadata.idn,
692
+ prompt_script: content,
693
+ runner_type: localMetadata.runner_type,
694
+ model: localMetadata.model,
695
+ parameters: localMetadata.parameters,
696
+ path: localMetadata.path
697
+ });
698
+ flowData.skills[skill.idn] = {
699
+ ...localMetadata,
700
+ id: existingSkill.id
701
+ };
702
+ newHashes[scriptPath] = sha256(content);
703
+ syncedPaths.add(scriptPath);
704
+ updated++;
705
+ this.logger.info(`[newo_v2] Updated skill metadata: ${flowIdn}/${skill.idn}`);
706
+ }
707
+ }
708
+ catch (error) {
709
+ errors.push(`Failed to sync skill ${skillLocator}: ${error instanceof Error ? error.message : String(error)}`);
710
+ }
711
+ }
712
+ }
713
+ }
714
+ }
715
+ return { created, updated, syncedPaths, errors };
716
+ }
717
+ async createMissingSkillParameters(client, existing, local) {
718
+ const existingNames = new Set(this.normalizeParameters(existing.parameters).map(p => p.name));
719
+ let created = 0;
720
+ for (const parameter of local.parameters) {
721
+ if (existingNames.has(parameter.name)) {
722
+ continue;
723
+ }
724
+ try {
725
+ await createSkillParameter(client, existing.id, {
726
+ name: parameter.name,
727
+ default_value: parameter.default_value ?? ''
728
+ });
729
+ created++;
730
+ this.logger.info(`[newo_v2] Created skill parameter: ${local.idn}/${parameter.name}`);
731
+ }
732
+ catch (error) {
733
+ if (!this.isAlreadyExistsApiError(error)) {
734
+ throw error;
735
+ }
736
+ }
737
+ existingNames.add(parameter.name);
738
+ }
739
+ return created;
740
+ }
741
+ /**
742
+ * Detect "resource already exists" API errors.
743
+ *
744
+ * Matches only on the precise phrases the platform actually returns
745
+ * ("already exists", "duplicate key"). Loose substrings like "exist"
746
+ * would otherwise sweep up unrelated "does not exist" / "doesn't exist"
747
+ * errors and trigger an incorrect reuse fallback.
748
+ */
749
+ isAlreadyExistsApiError(error) {
750
+ const response = error?.response;
751
+ const status = response?.status;
752
+ if (status !== 400 && status !== 409 && status !== 422) {
753
+ return false;
754
+ }
755
+ const haystack = JSON.stringify(response?.data ?? (error instanceof Error ? error.message : String(error))).toLowerCase();
756
+ return haystack.includes('already exists') || haystack.includes('duplicate key');
757
+ }
758
+ normalizeRunnerType(runnerType) {
759
+ return runnerType === 'nsl' ? 'nsl' : 'guidance';
760
+ }
761
+ normalizeParameters(parameters) {
762
+ return (parameters || []).map(p => ({
763
+ name: p.name,
764
+ default_value: p.default_value ?? ''
765
+ }));
766
+ }
767
+ /**
768
+ * Fail fast if no model could be resolved for a V2 skill.
769
+ *
770
+ * `buildV2SkillMetadataFromYaml` falls back to empty strings when neither
771
+ * the skill nor the flow declare a model. The platform rejects empty
772
+ * model_idn/provider_idn at creation/update time, but the error it returns
773
+ * is generic — we surface a clearer message before issuing the request.
774
+ */
775
+ assertSkillModelResolved(metadata, locator) {
776
+ if (!metadata.model.model_idn || !metadata.model.provider_idn) {
777
+ throw new Error(`[newo_v2] Cannot resolve model for skill ${locator}: ` +
778
+ `model_idn="${metadata.model.model_idn}", provider_idn="${metadata.model.provider_idn}". ` +
779
+ `Set either skill.model.* or flow default_model_idn/default_provider_idn in the flow YAML.`);
780
+ }
781
+ }
782
+ buildV2SkillMetadataFromYaml(skill, flowDef, runnerType, existing) {
783
+ return {
784
+ id: existing?.id || '',
785
+ idn: skill.idn,
786
+ title: skill.title || '',
787
+ runner_type: runnerType,
788
+ model: {
789
+ model_idn: skill.model?.model_idn || flowDef.default_model_idn || '',
790
+ provider_idn: skill.model?.provider_idn || flowDef.default_provider_idn || ''
791
+ },
792
+ parameters: this.normalizeParameters(skill.parameters),
793
+ path: existing?.path || ''
794
+ };
795
+ }
796
+ skillMetadataDiffers(existing, local) {
797
+ // Compare model/parameters field-by-field, never via JSON.stringify of the
798
+ // raw objects: the map stores model keys in platform API order
799
+ // (provider_idn first) while YAML-built metadata uses model_idn first, and
800
+ // a key-order-sensitive comparison flags every skill as changed.
801
+ const paramsKey = (params) => JSON.stringify(this.normalizeParameters(params).sort((a, b) => a.name.localeCompare(b.name)));
802
+ return (existing.title !== local.title ||
803
+ existing.runner_type !== local.runner_type ||
804
+ existing.model.model_idn !== local.model.model_idn ||
805
+ existing.model.provider_idn !== local.model.provider_idn ||
806
+ paramsKey(existing.parameters) !== paramsKey(local.parameters));
807
+ }
808
+ async resolveV2FlowSkillScriptPath(customerIdn, projectIdn, agentIdn, flowIdn, skillIdn, runnerType, promptScript) {
809
+ if (promptScript) {
810
+ const fromPromptScript = `${v2AgentDir(customerIdn, projectIdn, agentIdn)}/${promptScript}`;
811
+ if (await fs.pathExists(fromPromptScript)) {
812
+ return fromPromptScript;
813
+ }
814
+ }
815
+ return v2SkillScriptPath(customerIdn, projectIdn, agentIdn, flowIdn, skillIdn, runnerType);
816
+ }
567
817
  /**
568
818
  * Push a V2 skill update
569
819
  *
570
820
  * V2 path: newo_customers/{cust}/{proj}/agents/{agent}/flows/{flow}/skills/{skill}.nsl
571
821
  */
572
- async pushV2SkillUpdate(client, change, mapData, newHashes) {
573
- // Parse V2 path to extract entity hierarchy
574
- // Path: .../newo_customers/{cust}/{proj}/agents/{agent}/flows/{flow}/skills/{skillFile}
575
- const pathParts = change.path.split('/');
576
- const skillFileName = pathParts[pathParts.length - 1] || '';
577
- const skillIdn = skillFileName.replace(/\.(nsl|nslg|jinja|guidance)$/, '');
578
- // skills/ -> flow/ -> flows/ -> agent/ -> agents/ -> project/
579
- const flowIdn = pathParts[pathParts.length - 3] || '';
580
- const agentIdn = pathParts[pathParts.length - 5] || '';
581
- const projectIdn = pathParts[pathParts.length - 7] || '';
582
- // Look up skill in map
583
- const projectData = mapData.projects[projectIdn];
584
- const agentData = projectData?.agents[agentIdn];
585
- const flowData = agentData?.flows[flowIdn];
586
- const skillData = flowData?.skills[skillIdn];
587
- if (!skillData) {
588
- throw new Error(`Skill ${skillIdn} not found in project map (path: ${change.path})`);
822
+ async pushV2SkillUpdate(client, change, mapData, newHashes, customerIdn) {
823
+ const target = await this.resolveV2SkillTargetForScriptPath(customerIdn, change.path, mapData) ||
824
+ this.resolveV2SkillTargetFromCanonicalPath(change.path, mapData);
825
+ const skillData = target?.skillData;
826
+ if (!target || !skillData) {
827
+ throw new Error(`Skill not found in project map (path: ${change.path})`);
589
828
  }
590
829
  // Read updated script content
591
830
  const content = await fs.readFile(change.path, 'utf8');
@@ -601,9 +840,67 @@ export class V2ProjectSyncStrategy {
601
840
  path: skillData.path
602
841
  });
603
842
  newHashes[change.path] = sha256(content);
604
- this.logger.info(`[newo_v2] Pushed: ${skillIdn}`);
843
+ this.logger.info(`[newo_v2] Pushed: ${target.skillIdn}`);
605
844
  return 1;
606
845
  }
846
+ normalizePathForComparison(filePath) {
847
+ return path.resolve(filePath).replace(/\\/g, '/');
848
+ }
849
+ async resolveV2SkillTargetForScriptPath(customerIdn, scriptPath, mapData) {
850
+ const normalizedScriptPath = this.normalizePathForComparison(scriptPath);
851
+ for (const [projectIdn, projectData] of Object.entries(mapData.projects)) {
852
+ for (const [agentIdn, agentData] of Object.entries(projectData.agents)) {
853
+ for (const [flowIdn, flowData] of Object.entries(agentData.flows)) {
854
+ const flowYamlPath = v2FlowYamlPath(customerIdn, projectIdn, agentIdn, flowIdn);
855
+ if (!(await fs.pathExists(flowYamlPath))) {
856
+ continue;
857
+ }
858
+ let flowDef;
859
+ try {
860
+ flowDef = await parseV2FlowYaml(flowYamlPath);
861
+ }
862
+ catch {
863
+ continue;
864
+ }
865
+ for (const skill of flowDef.skills || []) {
866
+ const runnerType = this.normalizeRunnerType(skill.runner_type || flowData.skills[skill.idn]?.runner_type);
867
+ const resolvedScriptPath = await this.resolveV2FlowSkillScriptPath(customerIdn, projectIdn, agentIdn, flowIdn, skill.idn, runnerType, skill.prompt_script);
868
+ if (this.normalizePathForComparison(resolvedScriptPath) === normalizedScriptPath) {
869
+ return {
870
+ projectIdn,
871
+ agentIdn,
872
+ flowIdn,
873
+ skillIdn: skill.idn,
874
+ skillData: flowData.skills[skill.idn]
875
+ };
876
+ }
877
+ }
878
+ }
879
+ }
880
+ }
881
+ return null;
882
+ }
883
+ resolveV2SkillTargetFromCanonicalPath(scriptPath, mapData) {
884
+ // Parse canonical V2 path:
885
+ // .../newo_customers/{cust}/{proj}/agents/{agent}/flows/{flow}/skills/{skillFile}
886
+ const pathParts = scriptPath.split('/');
887
+ const skillFileName = pathParts[pathParts.length - 1] || '';
888
+ const skillIdn = skillFileName.replace(/\.(nsl|nslg|jinja|guidance)$/, '');
889
+ // skills/ -> flow/ -> flows/ -> agent/ -> agents/ -> project/
890
+ const flowIdn = pathParts[pathParts.length - 3] || '';
891
+ const agentIdn = pathParts[pathParts.length - 5] || '';
892
+ const projectIdn = pathParts[pathParts.length - 7] || '';
893
+ const projectData = mapData.projects[projectIdn];
894
+ const agentData = projectData?.agents[agentIdn];
895
+ const flowData = agentData?.flows[flowIdn];
896
+ return {
897
+ projectIdn,
898
+ agentIdn,
899
+ flowIdn,
900
+ skillIdn,
901
+ skillData: flowData?.skills[skillIdn]
902
+ };
903
+ }
607
904
  /**
608
905
  * Push a V2 library skill update
609
906
  * Path: .../newo_customers/{cust}/{proj}/libraries/{lib}/skills/{skillFile}
@@ -686,8 +983,18 @@ export class V2ProjectSyncStrategy {
686
983
  }
687
984
  }
688
985
  for (const [flowIdn, flowData] of Object.entries(agentData.flows)) {
689
- for (const [skillIdn, skillMeta] of Object.entries(flowData.skills)) {
690
- const scriptPath = v2SkillScriptPath(customer.idn, projectIdn, agentIdn, flowIdn, skillIdn, skillMeta.runner_type);
986
+ const flowYamlSkills = await this.loadLocalV2FlowSkills(customer.idn, projectIdn, agentIdn, flowIdn);
987
+ const skillIdns = new Set([
988
+ ...Object.keys(flowData.skills),
989
+ ...flowYamlSkills.keys()
990
+ ]);
991
+ for (const skillIdn of skillIdns) {
992
+ const yamlSkill = flowYamlSkills.get(skillIdn);
993
+ const skillMeta = flowData.skills[skillIdn];
994
+ const runnerType = this.normalizeRunnerType(yamlSkill?.runner_type || skillMeta?.runner_type);
995
+ const scriptPath = yamlSkill
996
+ ? await this.resolveV2FlowSkillScriptPath(customer.idn, projectIdn, agentIdn, flowIdn, skillIdn, runnerType, yamlSkill.prompt_script)
997
+ : v2SkillScriptPath(customer.idn, projectIdn, agentIdn, flowIdn, skillIdn, runnerType);
691
998
  if (await fs.pathExists(scriptPath)) {
692
999
  const content = await fs.readFile(scriptPath, 'utf8');
693
1000
  const currentHash = sha256(content);
@@ -726,6 +1033,19 @@ export class V2ProjectSyncStrategy {
726
1033
  }
727
1034
  return changes;
728
1035
  }
1036
+ async loadLocalV2FlowSkills(customerIdn, projectIdn, agentIdn, flowIdn) {
1037
+ const flowYamlPath = v2FlowYamlPath(customerIdn, projectIdn, agentIdn, flowIdn);
1038
+ if (!(await fs.pathExists(flowYamlPath))) {
1039
+ return new Map();
1040
+ }
1041
+ try {
1042
+ const flowDef = await parseV2FlowYaml(flowYamlPath);
1043
+ return new Map((flowDef.skills || []).map(skill => [skill.idn, skill]));
1044
+ }
1045
+ catch {
1046
+ return new Map();
1047
+ }
1048
+ }
729
1049
  async validate(customer, _items) {
730
1050
  const errors = [];
731
1051
  const mapFile = mapPath(customer.idn);
@@ -741,7 +1061,46 @@ export class V2ProjectSyncStrategy {
741
1061
  for (const [projectIdn, projectData] of Object.entries(mapData.projects)) {
742
1062
  for (const [agentIdn, agentData] of Object.entries(projectData.agents)) {
743
1063
  for (const [flowIdn, flowData] of Object.entries(agentData.flows)) {
1064
+ const flowYamlPath = v2FlowYamlPath(customer.idn, projectIdn, agentIdn, flowIdn);
1065
+ let localYamlSkills;
1066
+ if (await fs.pathExists(flowYamlPath)) {
1067
+ try {
1068
+ const flowDef = await parseV2FlowYaml(flowYamlPath);
1069
+ localYamlSkills = new Map((flowDef.skills || []).map(s => [s.idn, s]));
1070
+ const skillIdns = new Set([
1071
+ ...Object.keys(flowData.skills),
1072
+ ...localYamlSkills.keys()
1073
+ ]);
1074
+ for (const skillIdn of skillIdns) {
1075
+ const localYamlSkill = localYamlSkills.get(skillIdn);
1076
+ const skillMeta = flowData.skills[skillIdn];
1077
+ if (!localYamlSkill) {
1078
+ errors.push({
1079
+ field: `skill.${skillIdn}`,
1080
+ message: `Skill exists in project map but is missing from flow YAML: ${flowYamlPath}`,
1081
+ path: flowYamlPath
1082
+ });
1083
+ continue;
1084
+ }
1085
+ const runnerType = this.normalizeRunnerType(localYamlSkill.runner_type || skillMeta?.runner_type);
1086
+ const scriptPath = await this.resolveV2FlowSkillScriptPath(customer.idn, projectIdn, agentIdn, flowIdn, skillIdn, runnerType, localYamlSkill.prompt_script);
1087
+ if (!(await fs.pathExists(scriptPath))) {
1088
+ errors.push({
1089
+ field: `skill.${localYamlSkill.idn}`,
1090
+ message: `Script file not found: ${scriptPath}`,
1091
+ path: scriptPath
1092
+ });
1093
+ }
1094
+ }
1095
+ }
1096
+ catch {
1097
+ localYamlSkills = undefined;
1098
+ }
1099
+ }
744
1100
  for (const [skillIdn, skillMeta] of Object.entries(flowData.skills)) {
1101
+ if (localYamlSkills) {
1102
+ continue;
1103
+ }
745
1104
  const scriptPath = v2SkillScriptPath(customer.idn, projectIdn, agentIdn, flowIdn, skillIdn, skillMeta.runner_type);
746
1105
  if (!(await fs.pathExists(scriptPath))) {
747
1106
  errors.push({
@@ -5,9 +5,28 @@
5
5
  import type { AxiosInstance } from 'axios';
6
6
  import type { SandboxChatSession, Connector, ConversationAct, ChatDebugInfo } from '../types.js';
7
7
  /**
8
- * Find a sandbox connector from the customer's connectors list
8
+ * Options for selecting which connector to chat through
9
9
  */
10
- export declare function findSandboxConnector(client: AxiosInstance, verbose?: boolean): Promise<Connector | null>;
10
+ export interface ConnectorSelectionOptions {
11
+ /** Integration IDN to search connectors in (default: 'sandbox') */
12
+ integrationIdn?: string;
13
+ /** Exact connector_idn to use; when omitted, the first running connector is used */
14
+ connectorIdn?: string;
15
+ }
16
+ /**
17
+ * List running connectors of an integration (default: sandbox).
18
+ * Used by `newo sandbox --list-connectors` and connector selection.
19
+ */
20
+ export declare function listRunningSandboxConnectors(client: AxiosInstance, integrationIdn?: string): Promise<Connector[]>;
21
+ /**
22
+ * Find a sandbox connector from the customer's connectors list.
23
+ *
24
+ * Without options, preserves legacy behavior: first running connector of the
25
+ * 'sandbox' integration. With options.connectorIdn, selects that exact
26
+ * connector and throws a descriptive error (listing available connectors)
27
+ * when it is not found or not running.
28
+ */
29
+ export declare function findSandboxConnector(client: AxiosInstance, verbose?: boolean, options?: ConnectorSelectionOptions): Promise<Connector | null>;
11
30
  /**
12
31
  * Create a new sandbox chat session
13
32
  */
@@ -21,9 +40,10 @@ export declare function sendMessage(client: AxiosInstance, session: SandboxChatS
21
40
  * Poll for new conversation acts (messages and debug info)
22
41
  * Continues polling until we get an agent response, not just any new message
23
42
  */
24
- export declare function pollForResponse(client: AxiosInstance, session: SandboxChatSession, messageSentAt?: Date | null, verbose?: boolean): Promise<{
43
+ export declare function pollForResponse(client: AxiosInstance, session: SandboxChatSession, messageSentAt?: Date | null, verbose?: boolean, timeoutMs?: number): Promise<{
25
44
  acts: ConversationAct[];
26
45
  agentPersonaId: string | null;
46
+ userAct: ConversationAct | null;
27
47
  }>;
28
48
  /**
29
49
  * Extract agent messages from acts