newo 3.7.4 → 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.
- package/CHANGELOG.md +15 -2
- package/README.md +70 -2
- package/dist/cli/commands/get-skill.d.ts +3 -0
- package/dist/cli/commands/get-skill.js +72 -0
- package/dist/cli/commands/help.js +29 -0
- package/dist/cli/commands/logs.d.ts +6 -1
- package/dist/cli/commands/logs.js +62 -14
- package/dist/cli/commands/sandbox.d.ts +10 -4
- package/dist/cli/commands/sandbox.js +182 -51
- package/dist/cli/commands/update-skill.d.ts +4 -0
- package/dist/cli/commands/update-skill.js +119 -0
- package/dist/cli.js +8 -0
- package/dist/domain/strategies/sync/V2ProjectSyncStrategy.d.ts +3 -0
- package/dist/domain/strategies/sync/V2ProjectSyncStrategy.js +67 -19
- package/dist/sandbox/chat.d.ts +23 -3
- package/dist/sandbox/chat.js +83 -30
- package/dist/sync/remote-skill.d.ts +33 -0
- package/dist/sync/remote-skill.js +52 -0
- package/package.json +1 -1
- package/src/cli/commands/get-skill.ts +84 -0
- package/src/cli/commands/help.ts +29 -0
- package/src/cli/commands/logs.ts +83 -15
- package/src/cli/commands/sandbox.ts +238 -60
- package/src/cli/commands/update-skill.ts +139 -0
- package/src/cli.ts +10 -0
- package/src/domain/strategies/sync/V2ProjectSyncStrategy.ts +101 -20
- package/src/sandbox/chat.ts +106 -29
- package/src/sync/remote-skill.ts +92 -0
|
@@ -40,6 +40,7 @@ import type {
|
|
|
40
40
|
} from '../../../types.js';
|
|
41
41
|
import type { LocalProjectData, LocalAgentData, LocalFlowData, LocalSkillData, ApiClientFactory } from './ProjectSyncStrategy.js';
|
|
42
42
|
import fs from 'fs-extra';
|
|
43
|
+
import path from 'path';
|
|
43
44
|
import {
|
|
44
45
|
listProjects,
|
|
45
46
|
listAgents,
|
|
@@ -105,6 +106,14 @@ import yaml from 'js-yaml';
|
|
|
105
106
|
import { patchYamlToPyyaml } from '../../../format/yaml-patch.js';
|
|
106
107
|
import type { RunnerType, SkillParameter } from '../../../types.js';
|
|
107
108
|
|
|
109
|
+
interface V2FlowSkillTarget {
|
|
110
|
+
projectIdn: string;
|
|
111
|
+
agentIdn: string;
|
|
112
|
+
flowIdn: string;
|
|
113
|
+
skillIdn: string;
|
|
114
|
+
skillData: SkillMetadata | undefined;
|
|
115
|
+
}
|
|
116
|
+
|
|
108
117
|
/**
|
|
109
118
|
* V2ProjectSyncStrategy - same API, newo_v2 file layout
|
|
110
119
|
*/
|
|
@@ -684,7 +693,7 @@ export class V2ProjectSyncStrategy implements ISyncStrategy<ProjectMeta, LocalPr
|
|
|
684
693
|
const isLibrary = change.path.includes('/libraries/');
|
|
685
694
|
const count = isLibrary
|
|
686
695
|
? await this.pushV2LibrarySkillUpdate(client, change, mapData, newHashes)
|
|
687
|
-
: await this.pushV2SkillUpdate(client, change, mapData, newHashes);
|
|
696
|
+
: await this.pushV2SkillUpdate(client, change, mapData, newHashes, customer.idn);
|
|
688
697
|
result.updated += count;
|
|
689
698
|
}
|
|
690
699
|
} catch (error) {
|
|
@@ -1140,26 +1149,16 @@ export class V2ProjectSyncStrategy implements ISyncStrategy<ProjectMeta, LocalPr
|
|
|
1140
1149
|
client: AxiosInstance,
|
|
1141
1150
|
change: ChangeItem<LocalProjectData>,
|
|
1142
1151
|
mapData: ProjectMap,
|
|
1143
|
-
newHashes: HashStore
|
|
1152
|
+
newHashes: HashStore,
|
|
1153
|
+
customerIdn: string
|
|
1144
1154
|
): Promise<number> {
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
const
|
|
1149
|
-
const skillIdn = skillFileName.replace(/\.(nsl|nslg|jinja|guidance)$/, '');
|
|
1150
|
-
// skills/ -> flow/ -> flows/ -> agent/ -> agents/ -> project/
|
|
1151
|
-
const flowIdn = pathParts[pathParts.length - 3] || '';
|
|
1152
|
-
const agentIdn = pathParts[pathParts.length - 5] || '';
|
|
1153
|
-
const projectIdn = pathParts[pathParts.length - 7] || '';
|
|
1154
|
-
|
|
1155
|
-
// Look up skill in map
|
|
1156
|
-
const projectData = mapData.projects[projectIdn];
|
|
1157
|
-
const agentData = projectData?.agents[agentIdn];
|
|
1158
|
-
const flowData = agentData?.flows[flowIdn];
|
|
1159
|
-
const skillData = flowData?.skills[skillIdn];
|
|
1155
|
+
const target =
|
|
1156
|
+
await this.resolveV2SkillTargetForScriptPath(customerIdn, change.path, mapData) ||
|
|
1157
|
+
this.resolveV2SkillTargetFromCanonicalPath(change.path, mapData);
|
|
1158
|
+
const skillData = target?.skillData;
|
|
1160
1159
|
|
|
1161
|
-
if (!skillData) {
|
|
1162
|
-
throw new Error(`Skill
|
|
1160
|
+
if (!target || !skillData) {
|
|
1161
|
+
throw new Error(`Skill not found in project map (path: ${change.path})`);
|
|
1163
1162
|
}
|
|
1164
1163
|
|
|
1165
1164
|
// Read updated script content
|
|
@@ -1178,10 +1177,92 @@ export class V2ProjectSyncStrategy implements ISyncStrategy<ProjectMeta, LocalPr
|
|
|
1178
1177
|
});
|
|
1179
1178
|
|
|
1180
1179
|
newHashes[change.path] = sha256(content);
|
|
1181
|
-
this.logger.info(`[newo_v2] Pushed: ${skillIdn}`);
|
|
1180
|
+
this.logger.info(`[newo_v2] Pushed: ${target.skillIdn}`);
|
|
1182
1181
|
return 1;
|
|
1183
1182
|
}
|
|
1184
1183
|
|
|
1184
|
+
private normalizePathForComparison(filePath: string): string {
|
|
1185
|
+
return path.resolve(filePath).replace(/\\/g, '/');
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
private async resolveV2SkillTargetForScriptPath(
|
|
1189
|
+
customerIdn: string,
|
|
1190
|
+
scriptPath: string,
|
|
1191
|
+
mapData: ProjectMap
|
|
1192
|
+
): Promise<V2FlowSkillTarget | null> {
|
|
1193
|
+
const normalizedScriptPath = this.normalizePathForComparison(scriptPath);
|
|
1194
|
+
|
|
1195
|
+
for (const [projectIdn, projectData] of Object.entries(mapData.projects)) {
|
|
1196
|
+
for (const [agentIdn, agentData] of Object.entries(projectData.agents)) {
|
|
1197
|
+
for (const [flowIdn, flowData] of Object.entries(agentData.flows)) {
|
|
1198
|
+
const flowYamlPath = v2FlowYamlPath(customerIdn, projectIdn, agentIdn, flowIdn);
|
|
1199
|
+
if (!(await fs.pathExists(flowYamlPath))) {
|
|
1200
|
+
continue;
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
let flowDef;
|
|
1204
|
+
try {
|
|
1205
|
+
flowDef = await parseV2FlowYaml(flowYamlPath);
|
|
1206
|
+
} catch {
|
|
1207
|
+
continue;
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
for (const skill of flowDef.skills || []) {
|
|
1211
|
+
const runnerType = this.normalizeRunnerType(skill.runner_type || flowData.skills[skill.idn]?.runner_type);
|
|
1212
|
+
const resolvedScriptPath = await this.resolveV2FlowSkillScriptPath(
|
|
1213
|
+
customerIdn,
|
|
1214
|
+
projectIdn,
|
|
1215
|
+
agentIdn,
|
|
1216
|
+
flowIdn,
|
|
1217
|
+
skill.idn,
|
|
1218
|
+
runnerType,
|
|
1219
|
+
skill.prompt_script
|
|
1220
|
+
);
|
|
1221
|
+
|
|
1222
|
+
if (this.normalizePathForComparison(resolvedScriptPath) === normalizedScriptPath) {
|
|
1223
|
+
return {
|
|
1224
|
+
projectIdn,
|
|
1225
|
+
agentIdn,
|
|
1226
|
+
flowIdn,
|
|
1227
|
+
skillIdn: skill.idn,
|
|
1228
|
+
skillData: flowData.skills[skill.idn]
|
|
1229
|
+
};
|
|
1230
|
+
}
|
|
1231
|
+
}
|
|
1232
|
+
}
|
|
1233
|
+
}
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1236
|
+
return null;
|
|
1237
|
+
}
|
|
1238
|
+
|
|
1239
|
+
private resolveV2SkillTargetFromCanonicalPath(
|
|
1240
|
+
scriptPath: string,
|
|
1241
|
+
mapData: ProjectMap
|
|
1242
|
+
): V2FlowSkillTarget | null {
|
|
1243
|
+
// Parse canonical V2 path:
|
|
1244
|
+
// .../newo_customers/{cust}/{proj}/agents/{agent}/flows/{flow}/skills/{skillFile}
|
|
1245
|
+
const pathParts = scriptPath.split('/');
|
|
1246
|
+
const skillFileName = pathParts[pathParts.length - 1] || '';
|
|
1247
|
+
const skillIdn = skillFileName.replace(/\.(nsl|nslg|jinja|guidance)$/, '');
|
|
1248
|
+
// skills/ -> flow/ -> flows/ -> agent/ -> agents/ -> project/
|
|
1249
|
+
const flowIdn = pathParts[pathParts.length - 3] || '';
|
|
1250
|
+
const agentIdn = pathParts[pathParts.length - 5] || '';
|
|
1251
|
+
const projectIdn = pathParts[pathParts.length - 7] || '';
|
|
1252
|
+
|
|
1253
|
+
const projectData = mapData.projects[projectIdn];
|
|
1254
|
+
const agentData = projectData?.agents[agentIdn];
|
|
1255
|
+
const flowData = agentData?.flows[flowIdn];
|
|
1256
|
+
|
|
1257
|
+
return {
|
|
1258
|
+
projectIdn,
|
|
1259
|
+
agentIdn,
|
|
1260
|
+
flowIdn,
|
|
1261
|
+
skillIdn,
|
|
1262
|
+
skillData: flowData?.skills[skillIdn]
|
|
1263
|
+
};
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1185
1266
|
/**
|
|
1186
1267
|
* Push a V2 library skill update
|
|
1187
1268
|
* Path: .../newo_customers/{cust}/{proj}/libraries/{lib}/skills/{skillFile}
|
package/src/sandbox/chat.ts
CHANGED
|
@@ -23,7 +23,17 @@ import type {
|
|
|
23
23
|
const SANDBOX_INTEGRATION_IDN = 'sandbox';
|
|
24
24
|
const DEFAULT_TIMEZONE = 'America/Los_Angeles';
|
|
25
25
|
const POLL_INTERVAL_MS = 1000; // 1 second
|
|
26
|
-
const
|
|
26
|
+
const DEFAULT_TIMEOUT_MS = 60_000; // Default max wait for agent response
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Options for selecting which connector to chat through
|
|
30
|
+
*/
|
|
31
|
+
export interface ConnectorSelectionOptions {
|
|
32
|
+
/** Integration IDN to search connectors in (default: 'sandbox') */
|
|
33
|
+
integrationIdn?: string;
|
|
34
|
+
/** Exact connector_idn to use; when omitted, the first running connector is used */
|
|
35
|
+
connectorIdn?: string;
|
|
36
|
+
}
|
|
27
37
|
|
|
28
38
|
/**
|
|
29
39
|
* Generate a random external ID for chat session
|
|
@@ -41,41 +51,81 @@ function generatePersonaName(): string {
|
|
|
41
51
|
}
|
|
42
52
|
|
|
43
53
|
/**
|
|
44
|
-
*
|
|
54
|
+
* List running connectors of an integration (default: sandbox).
|
|
55
|
+
* Used by `newo sandbox --list-connectors` and connector selection.
|
|
45
56
|
*/
|
|
46
|
-
export async function
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
57
|
+
export async function listRunningSandboxConnectors(
|
|
58
|
+
client: AxiosInstance,
|
|
59
|
+
integrationIdn: string = SANDBOX_INTEGRATION_IDN
|
|
60
|
+
): Promise<Connector[]> {
|
|
50
61
|
const integrations = await listIntegrations(client);
|
|
51
|
-
const
|
|
62
|
+
const integration = integrations.find(i => i.idn === integrationIdn);
|
|
52
63
|
|
|
53
|
-
if (!
|
|
54
|
-
|
|
55
|
-
|
|
64
|
+
if (!integration) {
|
|
65
|
+
throw new Error(
|
|
66
|
+
`Integration '${integrationIdn}' not found. Available integrations: ${integrations.map(i => i.idn).join(', ') || '(none)'}`
|
|
67
|
+
);
|
|
56
68
|
}
|
|
57
69
|
|
|
58
|
-
|
|
70
|
+
const connectors = await listConnectors(client, integration.id);
|
|
71
|
+
return connectors.filter(c => c.status === 'running');
|
|
72
|
+
}
|
|
59
73
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
74
|
+
/**
|
|
75
|
+
* Find a sandbox connector from the customer's connectors list.
|
|
76
|
+
*
|
|
77
|
+
* Without options, preserves legacy behavior: first running connector of the
|
|
78
|
+
* 'sandbox' integration. With options.connectorIdn, selects that exact
|
|
79
|
+
* connector and throws a descriptive error (listing available connectors)
|
|
80
|
+
* when it is not found or not running.
|
|
81
|
+
*/
|
|
82
|
+
export async function findSandboxConnector(
|
|
83
|
+
client: AxiosInstance,
|
|
84
|
+
verbose: boolean = false,
|
|
85
|
+
options: ConnectorSelectionOptions = {}
|
|
86
|
+
): Promise<Connector | null> {
|
|
87
|
+
const integrationIdn = options.integrationIdn || SANDBOX_INTEGRATION_IDN;
|
|
88
|
+
|
|
89
|
+
if (verbose) console.log(`🔍 Searching for ${integrationIdn} integration...`);
|
|
90
|
+
|
|
91
|
+
let runningConnectors: Connector[];
|
|
92
|
+
try {
|
|
93
|
+
runningConnectors = await listRunningSandboxConnectors(client, integrationIdn);
|
|
94
|
+
} catch (error) {
|
|
95
|
+
if (options.connectorIdn) throw error;
|
|
96
|
+
if (verbose) console.log(`❌ ${error instanceof Error ? error.message : String(error)}`);
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
64
99
|
|
|
65
|
-
if (
|
|
66
|
-
if (
|
|
100
|
+
if (runningConnectors.length === 0) {
|
|
101
|
+
if (options.connectorIdn) {
|
|
102
|
+
throw new Error(`No running connectors found in integration '${integrationIdn}'`);
|
|
103
|
+
}
|
|
104
|
+
if (verbose) console.log(`❌ No running ${integrationIdn} connectors found`);
|
|
67
105
|
return null;
|
|
68
106
|
}
|
|
69
107
|
|
|
108
|
+
if (options.connectorIdn) {
|
|
109
|
+
const match = runningConnectors.find(c => c.connector_idn === options.connectorIdn);
|
|
110
|
+
if (!match) {
|
|
111
|
+
const available = runningConnectors.map(c => c.connector_idn).join(', ');
|
|
112
|
+
throw new Error(
|
|
113
|
+
`Connector '${options.connectorIdn}' not found among running connectors of integration '${integrationIdn}'. Available: ${available}`
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
if (verbose) console.log(`✓ Using connector: ${match.connector_idn}`);
|
|
117
|
+
return match;
|
|
118
|
+
}
|
|
119
|
+
|
|
70
120
|
if (verbose) {
|
|
71
|
-
console.log(`✓ Found ${
|
|
72
|
-
const firstConnector =
|
|
121
|
+
console.log(`✓ Found ${runningConnectors.length} running ${integrationIdn} connector(s)`);
|
|
122
|
+
const firstConnector = runningConnectors[0];
|
|
73
123
|
if (firstConnector) {
|
|
74
124
|
console.log(` Using: ${firstConnector.connector_idn}`);
|
|
75
125
|
}
|
|
76
126
|
}
|
|
77
127
|
|
|
78
|
-
return
|
|
128
|
+
return runningConnectors[0] || null;
|
|
79
129
|
}
|
|
80
130
|
|
|
81
131
|
/**
|
|
@@ -105,7 +155,7 @@ export async function createChatSession(
|
|
|
105
155
|
const actorResponse = await createActor(client, personaResponse.id, {
|
|
106
156
|
name: personaName,
|
|
107
157
|
external_id: externalId,
|
|
108
|
-
integration_idn: SANDBOX_INTEGRATION_IDN,
|
|
158
|
+
integration_idn: connector.integration_idn || SANDBOX_INTEGRATION_IDN,
|
|
109
159
|
connector_idn: connector.connector_idn,
|
|
110
160
|
time_zone_identifier: DEFAULT_TIMEZONE
|
|
111
161
|
});
|
|
@@ -132,7 +182,10 @@ export async function sendMessage(
|
|
|
132
182
|
text: string,
|
|
133
183
|
verbose: boolean = false
|
|
134
184
|
): Promise<Date> {
|
|
135
|
-
if (verbose)
|
|
185
|
+
if (verbose) {
|
|
186
|
+
const preview = text.length > 200 ? `${text.slice(0, 200)}… (${text.length} chars)` : text;
|
|
187
|
+
console.log(`💬 Sending message: "${preview}"`);
|
|
188
|
+
}
|
|
136
189
|
|
|
137
190
|
const sentAt = new Date();
|
|
138
191
|
|
|
@@ -146,6 +199,17 @@ export async function sendMessage(
|
|
|
146
199
|
return sentAt;
|
|
147
200
|
}
|
|
148
201
|
|
|
202
|
+
/**
|
|
203
|
+
* Parse an act datetime that may lack timezone info (assume UTC)
|
|
204
|
+
*/
|
|
205
|
+
function parseActDatetimeMs(datetime: string): number {
|
|
206
|
+
let d = datetime;
|
|
207
|
+
if (!d.endsWith('Z') && !d.includes('+') && !d.includes('-', 10)) {
|
|
208
|
+
d = d + 'Z';
|
|
209
|
+
}
|
|
210
|
+
return new Date(d).getTime();
|
|
211
|
+
}
|
|
212
|
+
|
|
149
213
|
/**
|
|
150
214
|
* Poll for new conversation acts (messages and debug info)
|
|
151
215
|
* Continues polling until we get an agent response, not just any new message
|
|
@@ -154,20 +218,23 @@ export async function pollForResponse(
|
|
|
154
218
|
client: AxiosInstance,
|
|
155
219
|
session: SandboxChatSession,
|
|
156
220
|
messageSentAt: Date | null = null,
|
|
157
|
-
verbose: boolean = false
|
|
158
|
-
|
|
221
|
+
verbose: boolean = false,
|
|
222
|
+
timeoutMs: number = DEFAULT_TIMEOUT_MS
|
|
223
|
+
): Promise<{ acts: ConversationAct[]; agentPersonaId: string | null; userAct: ConversationAct | null }> {
|
|
159
224
|
let attempts = 0;
|
|
160
225
|
let agentPersonaId = session.agent_persona_id;
|
|
226
|
+
let userAct: ConversationAct | null = null;
|
|
227
|
+
const maxPollAttempts = Math.max(1, Math.ceil(timeoutMs / POLL_INTERVAL_MS));
|
|
161
228
|
|
|
162
|
-
if (verbose) console.log(
|
|
229
|
+
if (verbose) console.log(`⏳ Waiting for agent response (timeout: ${Math.round(timeoutMs / 1000)}s)...`);
|
|
163
230
|
|
|
164
231
|
// Add small delay before first poll to allow message to be processed
|
|
165
232
|
await new Promise(resolve => setTimeout(resolve, 500));
|
|
166
233
|
|
|
167
|
-
while (attempts <
|
|
234
|
+
while (attempts < maxPollAttempts) {
|
|
168
235
|
try {
|
|
169
236
|
if (verbose && attempts % 5 === 0) {
|
|
170
|
-
console.log(` [Poll attempt ${attempts + 1}/${
|
|
237
|
+
console.log(` [Poll attempt ${attempts + 1}/${maxPollAttempts}] Checking for messages...`);
|
|
171
238
|
}
|
|
172
239
|
|
|
173
240
|
// Use Chat History API instead of acts API (doesn't require account_id)
|
|
@@ -221,6 +288,16 @@ export async function pollForResponse(
|
|
|
221
288
|
}
|
|
222
289
|
}
|
|
223
290
|
|
|
291
|
+
// Track the user act of our sent message (newest non-agent act at/after sentAt).
|
|
292
|
+
// Its external_event_id is the correlation key for `newo logs --event-id`.
|
|
293
|
+
for (const act of convertedActs) {
|
|
294
|
+
if (act.is_agent) continue;
|
|
295
|
+
if (messageSentAt && parseActDatetimeMs(act.datetime) - messageSentAt.getTime() <= -100) continue;
|
|
296
|
+
if (!userAct || parseActDatetimeMs(act.datetime) >= parseActDatetimeMs(userAct.datetime)) {
|
|
297
|
+
userAct = act;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
224
301
|
// Filter for agent messages that came AFTER our message was sent
|
|
225
302
|
const agentMessages = convertedActs.filter(act => {
|
|
226
303
|
if (!act.is_agent) return false;
|
|
@@ -262,7 +339,7 @@ export async function pollForResponse(
|
|
|
262
339
|
// Return ONLY the single newest agent message (first one, since API returns newest first)
|
|
263
340
|
const latestAgentMessage = agentMessages[0];
|
|
264
341
|
if (latestAgentMessage) {
|
|
265
|
-
return { acts: [latestAgentMessage], agentPersonaId };
|
|
342
|
+
return { acts: [latestAgentMessage], agentPersonaId, userAct };
|
|
266
343
|
}
|
|
267
344
|
} else if (verbose && attempts % 10 === 0) {
|
|
268
345
|
console.log(` No new agent messages yet (checked ${response.items.length} total messages, sentAt: ${messageSentAt?.toISOString()}), continuing...`);
|
|
@@ -280,7 +357,7 @@ export async function pollForResponse(
|
|
|
280
357
|
}
|
|
281
358
|
|
|
282
359
|
if (verbose) console.log('⏱️ Timeout waiting for response');
|
|
283
|
-
return { acts: [], agentPersonaId };
|
|
360
|
+
return { acts: [], agentPersonaId, userAct };
|
|
284
361
|
}
|
|
285
362
|
|
|
286
363
|
/**
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Remote skill resolution by IDN path (project/agent/flow/skill).
|
|
3
|
+
*
|
|
4
|
+
* Used by `newo get-skill` and `newo update-skill` to address a single skill
|
|
5
|
+
* on the platform without requiring a pulled local workspace.
|
|
6
|
+
*/
|
|
7
|
+
import type { AxiosInstance } from 'axios';
|
|
8
|
+
import { listProjects, listAgents, listFlowSkills, getSkill } from '../api.js';
|
|
9
|
+
import type { ProjectMeta, Agent, Flow, Skill } from '../types.js';
|
|
10
|
+
|
|
11
|
+
export interface RemoteSkillPath {
|
|
12
|
+
projectIdn: string;
|
|
13
|
+
agentIdn: string;
|
|
14
|
+
flowIdn: string;
|
|
15
|
+
skillIdn: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface ResolvedRemoteSkill {
|
|
19
|
+
project: ProjectMeta;
|
|
20
|
+
agent: Agent;
|
|
21
|
+
flow: Flow;
|
|
22
|
+
skill: Skill;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Resolve a skill on the platform by its IDN path.
|
|
27
|
+
* Throws descriptive errors listing available IDNs at the failing level.
|
|
28
|
+
*/
|
|
29
|
+
export async function resolveRemoteSkill(
|
|
30
|
+
client: AxiosInstance,
|
|
31
|
+
path: RemoteSkillPath
|
|
32
|
+
): Promise<ResolvedRemoteSkill> {
|
|
33
|
+
const projects = await listProjects(client);
|
|
34
|
+
const project = projects.find(p => p.idn === path.projectIdn);
|
|
35
|
+
if (!project) {
|
|
36
|
+
throw new Error(
|
|
37
|
+
`Project '${path.projectIdn}' not found. Available projects: ${projects.map(p => p.idn).join(', ') || '(none)'}`
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const agents = await listAgents(client, project.id);
|
|
42
|
+
const agent = agents.find(a => a.idn === path.agentIdn);
|
|
43
|
+
if (!agent) {
|
|
44
|
+
throw new Error(
|
|
45
|
+
`Agent '${path.agentIdn}' not found in project '${path.projectIdn}'. Available agents: ${agents.map(a => a.idn).join(', ') || '(none)'}`
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const flows = agent.flows || [];
|
|
50
|
+
const flow = flows.find(f => f.idn === path.flowIdn);
|
|
51
|
+
if (!flow) {
|
|
52
|
+
throw new Error(
|
|
53
|
+
`Flow '${path.flowIdn}' not found in agent '${path.agentIdn}'. Available flows: ${flows.map(f => f.idn).join(', ') || '(none)'}`
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const skills = await listFlowSkills(client, flow.id);
|
|
58
|
+
const skillStub = skills.find(s => s.idn === path.skillIdn);
|
|
59
|
+
if (!skillStub) {
|
|
60
|
+
throw new Error(
|
|
61
|
+
`Skill '${path.skillIdn}' not found in flow '${path.flowIdn}'. Available skills: ${skills.map(s => s.idn).join(', ') || '(none)'}`
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// The flow-skills list endpoint already returns full skills including
|
|
66
|
+
// prompt_script (pull relies on this). Only fall back to the by-id
|
|
67
|
+
// endpoint when prompt_script is absent — and tolerate a 404 there,
|
|
68
|
+
// since /api/v1/designer/skills/{id} is not available on all accounts.
|
|
69
|
+
let skill = skillStub;
|
|
70
|
+
if (skill.prompt_script === undefined || skill.prompt_script === null) {
|
|
71
|
+
try {
|
|
72
|
+
skill = await getSkill(client, skillStub.id);
|
|
73
|
+
} catch {
|
|
74
|
+
skill = skillStub;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return { project, agent, flow, skill };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Parse a `--model provider_idn/model_idn` flag value
|
|
83
|
+
*/
|
|
84
|
+
export function parseModelFlag(value: string): { provider_idn: string; model_idn: string } {
|
|
85
|
+
const parts = value.split('/');
|
|
86
|
+
if (parts.length !== 2 || !parts[0] || !parts[1]) {
|
|
87
|
+
throw new Error(
|
|
88
|
+
`Invalid --model value '${value}'. Expected format: <provider_idn>/<model_idn> (e.g. openai/gpt4o)`
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
return { provider_idn: parts[0], model_idn: parts[1] };
|
|
92
|
+
}
|