@porchestra/cli 1.0.0 → 1.0.1
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/bin/porchestra.js +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1602 -0
- package/dist/index.js.map +1 -0
- package/package.json +11 -1
- package/src/agents/testPrompt/ast.json +0 -71
- package/src/agents/testPrompt/config.ts +0 -18
- package/src/agents/testPrompt/index.ts +0 -64
- package/src/agents/testPrompt/schemas.ts +0 -45
- package/src/agents/testPrompt/tools.ts +0 -88
- package/src/commands/agents.ts +0 -173
- package/src/commands/config.ts +0 -97
- package/src/commands/explore.ts +0 -160
- package/src/commands/login.ts +0 -101
- package/src/commands/logout.ts +0 -52
- package/src/commands/pull.ts +0 -220
- package/src/commands/status.ts +0 -78
- package/src/commands/whoami.ts +0 -56
- package/src/core/api/client.ts +0 -133
- package/src/core/auth/auth-service.ts +0 -176
- package/src/core/auth/token-manager.ts +0 -47
- package/src/core/config/config-manager.ts +0 -107
- package/src/core/config/config-schema.ts +0 -56
- package/src/core/config/project-tracker.ts +0 -158
- package/src/core/generators/code-generator.ts +0 -329
- package/src/core/generators/schema-generator.ts +0 -59
- package/src/index.ts +0 -85
- package/src/types/index.ts +0 -214
- package/src/utils/date.ts +0 -23
- package/src/utils/errors.ts +0 -38
- package/src/utils/logger.ts +0 -11
- package/src/utils/path-utils.ts +0 -47
- package/tests/unit/config-manager.test.ts +0 -74
- package/tests/unit/config-schema.test.ts +0 -61
- package/tests/unit/path-utils.test.ts +0 -53
- package/tests/unit/schema-generator.test.ts +0 -82
- package/tsconfig.json +0 -30
- package/tsup.config.ts +0 -19
- package/vitest.config.ts +0 -20
package/dist/index.js
ADDED
|
@@ -0,0 +1,1602 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
4
|
+
var __esm = (fn, res) => function __init() {
|
|
5
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
6
|
+
};
|
|
7
|
+
var __export = (target, all) => {
|
|
8
|
+
for (var name in all)
|
|
9
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
// src/core/config/project-tracker.ts
|
|
13
|
+
var ProjectTracker;
|
|
14
|
+
var init_project_tracker = __esm({
|
|
15
|
+
"src/core/config/project-tracker.ts"() {
|
|
16
|
+
"use strict";
|
|
17
|
+
ProjectTracker = class {
|
|
18
|
+
constructor(configManager) {
|
|
19
|
+
this.configManager = configManager;
|
|
20
|
+
}
|
|
21
|
+
async getTrackedProjects() {
|
|
22
|
+
const config = await this.configManager.get();
|
|
23
|
+
return config.trackedProjects || [];
|
|
24
|
+
}
|
|
25
|
+
async getTrackedAgents(projectId) {
|
|
26
|
+
const config = await this.configManager.get();
|
|
27
|
+
const project = config.trackedProjects?.find((p) => p.projectId === projectId);
|
|
28
|
+
return project?.agents || [];
|
|
29
|
+
}
|
|
30
|
+
async listTrackedAgents() {
|
|
31
|
+
const projects = await this.getTrackedProjects();
|
|
32
|
+
return projects.flatMap(
|
|
33
|
+
(project) => project.agents.map((agent) => ({ project, agent }))
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
async trackProject(project, agents) {
|
|
37
|
+
await this.configManager.update((config) => {
|
|
38
|
+
const existingIndex = config.trackedProjects.findIndex(
|
|
39
|
+
(p) => p.projectId === project.id
|
|
40
|
+
);
|
|
41
|
+
const trackedAgents = agents.map((agent) => ({
|
|
42
|
+
agentId: agent.id,
|
|
43
|
+
agentName: agent.name,
|
|
44
|
+
agentSlug: agent.slug,
|
|
45
|
+
folderPath: agent.folderPath,
|
|
46
|
+
selectedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
47
|
+
}));
|
|
48
|
+
const trackedProject = {
|
|
49
|
+
projectId: project.id,
|
|
50
|
+
projectName: project.name,
|
|
51
|
+
projectSlug: project.slug,
|
|
52
|
+
selectedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
53
|
+
agents: trackedAgents
|
|
54
|
+
};
|
|
55
|
+
const newProjects = [...config.trackedProjects];
|
|
56
|
+
if (existingIndex >= 0) {
|
|
57
|
+
newProjects[existingIndex] = trackedProject;
|
|
58
|
+
} else {
|
|
59
|
+
newProjects.push(trackedProject);
|
|
60
|
+
}
|
|
61
|
+
return { ...config, trackedProjects: newProjects };
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
async untrackProject(projectId) {
|
|
65
|
+
await this.configManager.update((config) => ({
|
|
66
|
+
...config,
|
|
67
|
+
trackedProjects: config.trackedProjects.filter(
|
|
68
|
+
(p) => p.projectId !== projectId
|
|
69
|
+
)
|
|
70
|
+
}));
|
|
71
|
+
}
|
|
72
|
+
async untrackAgent(projectId, agentId) {
|
|
73
|
+
let removed = false;
|
|
74
|
+
await this.configManager.update((config) => {
|
|
75
|
+
const projectIndex = config.trackedProjects.findIndex(
|
|
76
|
+
(p) => p.projectId === projectId
|
|
77
|
+
);
|
|
78
|
+
if (projectIndex === -1) return config;
|
|
79
|
+
const project = config.trackedProjects[projectIndex];
|
|
80
|
+
const remainingAgents = project.agents.filter(
|
|
81
|
+
(agent) => agent.agentId !== agentId
|
|
82
|
+
);
|
|
83
|
+
if (remainingAgents.length === project.agents.length) {
|
|
84
|
+
return config;
|
|
85
|
+
}
|
|
86
|
+
removed = true;
|
|
87
|
+
const newProjects = [...config.trackedProjects];
|
|
88
|
+
if (remainingAgents.length === 0) {
|
|
89
|
+
newProjects.splice(projectIndex, 1);
|
|
90
|
+
} else {
|
|
91
|
+
newProjects[projectIndex] = { ...project, agents: remainingAgents };
|
|
92
|
+
}
|
|
93
|
+
return { ...config, trackedProjects: newProjects };
|
|
94
|
+
});
|
|
95
|
+
return removed;
|
|
96
|
+
}
|
|
97
|
+
async updateLastPulled(projectId, agentId, version) {
|
|
98
|
+
await this.configManager.update((config) => {
|
|
99
|
+
const projectIndex = config.trackedProjects.findIndex(
|
|
100
|
+
(p) => p.projectId === projectId
|
|
101
|
+
);
|
|
102
|
+
if (projectIndex === -1) return config;
|
|
103
|
+
const project = config.trackedProjects[projectIndex];
|
|
104
|
+
const agentIndex = project.agents.findIndex(
|
|
105
|
+
(a) => a.agentId === agentId
|
|
106
|
+
);
|
|
107
|
+
if (agentIndex === -1) return config;
|
|
108
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
109
|
+
const newAgents = [...project.agents];
|
|
110
|
+
newAgents[agentIndex] = {
|
|
111
|
+
...newAgents[agentIndex],
|
|
112
|
+
lastPulledAt: now,
|
|
113
|
+
lastPulledVersion: version
|
|
114
|
+
};
|
|
115
|
+
const newProjects = [...config.trackedProjects];
|
|
116
|
+
newProjects[projectIndex] = {
|
|
117
|
+
...project,
|
|
118
|
+
agents: newAgents,
|
|
119
|
+
lastPulledAt: now
|
|
120
|
+
};
|
|
121
|
+
return { ...config, trackedProjects: newProjects };
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
async getSummary() {
|
|
125
|
+
const projects = await this.getTrackedProjects();
|
|
126
|
+
return {
|
|
127
|
+
projectCount: projects.length,
|
|
128
|
+
agentCount: projects.reduce((sum, p) => sum + p.agents.length, 0)
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
// src/utils/date.ts
|
|
136
|
+
function formatDistanceToNow(date) {
|
|
137
|
+
const now = /* @__PURE__ */ new Date();
|
|
138
|
+
const diffMs = now.getTime() - date.getTime();
|
|
139
|
+
const diffSecs = Math.floor(diffMs / 1e3);
|
|
140
|
+
const diffMins = Math.floor(diffSecs / 60);
|
|
141
|
+
const diffHours = Math.floor(diffMins / 60);
|
|
142
|
+
const diffDays = Math.floor(diffHours / 24);
|
|
143
|
+
if (diffSecs < 60) return "just now";
|
|
144
|
+
if (diffMins < 60) return `${diffMins}m ago`;
|
|
145
|
+
if (diffHours < 24) return `${diffHours}h ago`;
|
|
146
|
+
if (diffDays < 30) return `${diffDays}d ago`;
|
|
147
|
+
return date.toISOString().split("T")[0];
|
|
148
|
+
}
|
|
149
|
+
function formatDate(dateStr) {
|
|
150
|
+
const date = new Date(dateStr);
|
|
151
|
+
return date.toLocaleDateString("en-US", {
|
|
152
|
+
year: "numeric",
|
|
153
|
+
month: "short",
|
|
154
|
+
day: "numeric"
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
var init_date = __esm({
|
|
158
|
+
"src/utils/date.ts"() {
|
|
159
|
+
"use strict";
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
// src/commands/explore.ts
|
|
164
|
+
var explore_exports = {};
|
|
165
|
+
__export(explore_exports, {
|
|
166
|
+
createExploreCommand: () => createExploreCommand
|
|
167
|
+
});
|
|
168
|
+
import { Command as Command4 } from "commander";
|
|
169
|
+
import { select, checkbox, confirm as confirm2, Separator } from "@inquirer/prompts";
|
|
170
|
+
import pc4 from "picocolors";
|
|
171
|
+
import ora from "ora";
|
|
172
|
+
function createExploreCommand(configManager, apiClient) {
|
|
173
|
+
const projectTracker = new ProjectTracker(configManager);
|
|
174
|
+
return new Command4("explore").description("Explore and select projects/agents to track").option("-p, --project <id>", "Start with specific project").action(async (options) => {
|
|
175
|
+
const spinner = ora("Fetching projects...").start();
|
|
176
|
+
try {
|
|
177
|
+
const response = await apiClient.getProjectsBrief();
|
|
178
|
+
const projects = response.projects;
|
|
179
|
+
spinner.stop();
|
|
180
|
+
if (projects.length === 0) {
|
|
181
|
+
console.log(pc4.yellow("No projects found. Create one in the web app first."));
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
let exploring = true;
|
|
185
|
+
while (exploring) {
|
|
186
|
+
const projectChoices = projects.map((p) => ({
|
|
187
|
+
name: `${p.name} ${pc4.gray(`(${p.agentCount} agents, last modified ${formatDate(p.lastModifiedAt)})`)}`,
|
|
188
|
+
value: p,
|
|
189
|
+
short: p.name
|
|
190
|
+
}));
|
|
191
|
+
let selectedProject;
|
|
192
|
+
if (options.project) {
|
|
193
|
+
selectedProject = projects.find((p) => p.id === options.project || p.slug === options.project);
|
|
194
|
+
} else {
|
|
195
|
+
const choices = [
|
|
196
|
+
...projectChoices,
|
|
197
|
+
new Separator(),
|
|
198
|
+
{ name: pc4.yellow("Done exploring"), value: "DONE" }
|
|
199
|
+
];
|
|
200
|
+
selectedProject = await select({
|
|
201
|
+
message: "Select a project to explore:",
|
|
202
|
+
choices
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
if (selectedProject === "DONE") {
|
|
206
|
+
exploring = false;
|
|
207
|
+
break;
|
|
208
|
+
}
|
|
209
|
+
if (!selectedProject) {
|
|
210
|
+
console.log(pc4.red("Project not found"));
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
const agentSpinner = ora(`Fetching agents for ${selectedProject.name}...`).start();
|
|
214
|
+
const agentsResponse = await apiClient.getProjectAgents(selectedProject.id);
|
|
215
|
+
const agents = agentsResponse;
|
|
216
|
+
agentSpinner.succeed(`Found ${agents.agents.length} agents`);
|
|
217
|
+
console.log(pc4.bold(`
|
|
218
|
+
\u{1F4C1} ${selectedProject.name}
|
|
219
|
+
`));
|
|
220
|
+
agents.agents.forEach((agent) => {
|
|
221
|
+
const isPublished = agent.isPublished ? pc4.green("\u25CF") : pc4.gray("\u25CB");
|
|
222
|
+
const versionInfo = `${agent.version} ${pc4.gray(`(${agents.versionResolution.source})`)}`;
|
|
223
|
+
console.log(` ${isPublished} ${pc4.cyan(agent.name)}`);
|
|
224
|
+
console.log(` ${pc4.gray("Path:")} ${agent.folderPath}`);
|
|
225
|
+
console.log(` ${pc4.gray("Version:")} ${versionInfo}`);
|
|
226
|
+
console.log(` ${pc4.gray("Tools:")} ${agent.toolCount}`);
|
|
227
|
+
if (agents.versionResolution.source !== "PRODUCTION") {
|
|
228
|
+
console.log(` ${pc4.yellow("\u26A0")} ${agents.versionResolution.note}`);
|
|
229
|
+
}
|
|
230
|
+
console.log();
|
|
231
|
+
});
|
|
232
|
+
const currentlyTracked = await projectTracker.getTrackedAgents(selectedProject.id);
|
|
233
|
+
const trackedIds = new Set(currentlyTracked.map((a) => a.agentId));
|
|
234
|
+
const selectedAgentIds = await checkbox({
|
|
235
|
+
message: "Select agents to track (space to toggle, enter to confirm):",
|
|
236
|
+
choices: agents.agents.map((agent) => ({
|
|
237
|
+
name: `${agent.name} ${pc4.gray(`(${agent.folderPath})`)}`,
|
|
238
|
+
value: agent.id,
|
|
239
|
+
checked: trackedIds.has(agent.id)
|
|
240
|
+
}))
|
|
241
|
+
});
|
|
242
|
+
if (selectedAgentIds.length > 0) {
|
|
243
|
+
const selectedAgents = agents.agents.filter((a) => selectedAgentIds.includes(a.id));
|
|
244
|
+
try {
|
|
245
|
+
await projectTracker.trackProject(selectedProject, selectedAgents);
|
|
246
|
+
console.log(pc4.green(`
|
|
247
|
+
\u2713 Auto-saved: Tracking ${selectedAgents.length} agents from ${selectedProject.name}`));
|
|
248
|
+
} catch (error) {
|
|
249
|
+
console.error(pc4.red(`
|
|
250
|
+
\u2717 Failed to save tracking: ${error.message}`));
|
|
251
|
+
throw error;
|
|
252
|
+
}
|
|
253
|
+
} else {
|
|
254
|
+
try {
|
|
255
|
+
await projectTracker.untrackProject(selectedProject.id);
|
|
256
|
+
console.log(pc4.yellow(`
|
|
257
|
+
\u2713 Auto-saved: No longer tracking ${selectedProject.name}`));
|
|
258
|
+
} catch (error) {
|
|
259
|
+
console.error(pc4.red(`
|
|
260
|
+
\u2717 Failed to untrack: ${error.message}`));
|
|
261
|
+
throw error;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
if (!options.project) {
|
|
265
|
+
const continueExploring = await confirm2({
|
|
266
|
+
message: "Explore another project?",
|
|
267
|
+
default: true
|
|
268
|
+
});
|
|
269
|
+
if (!continueExploring) exploring = false;
|
|
270
|
+
} else {
|
|
271
|
+
exploring = false;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
const summary = await projectTracker.getSummary();
|
|
275
|
+
console.log(pc4.bold("\n\u{1F4CA} Tracking Summary\n"));
|
|
276
|
+
console.log(` Projects: ${summary.projectCount}`);
|
|
277
|
+
console.log(` Agents: ${summary.agentCount}`);
|
|
278
|
+
console.log(pc4.gray("\nRun `porchestra pull` to generate code for tracked agents\n"));
|
|
279
|
+
} catch (error) {
|
|
280
|
+
spinner.fail("Failed to fetch projects");
|
|
281
|
+
console.error(pc4.red(`
|
|
282
|
+
\u2717 ${error.message}`));
|
|
283
|
+
process.exit(1);
|
|
284
|
+
}
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
var init_explore = __esm({
|
|
288
|
+
"src/commands/explore.ts"() {
|
|
289
|
+
"use strict";
|
|
290
|
+
init_project_tracker();
|
|
291
|
+
init_date();
|
|
292
|
+
}
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
// src/index.ts
|
|
296
|
+
import { Command as Command9 } from "commander";
|
|
297
|
+
import pc9 from "picocolors";
|
|
298
|
+
|
|
299
|
+
// src/core/config/config-manager.ts
|
|
300
|
+
import { promises as fs } from "fs";
|
|
301
|
+
import path from "path";
|
|
302
|
+
import os from "os";
|
|
303
|
+
import { z as z2 } from "zod";
|
|
304
|
+
|
|
305
|
+
// src/core/config/config-schema.ts
|
|
306
|
+
import { z } from "zod";
|
|
307
|
+
var DateTimeStringSchema = z.string().optional();
|
|
308
|
+
var TrackedAgentSchema = z.object({
|
|
309
|
+
agentId: z.string().uuid(),
|
|
310
|
+
agentName: z.string(),
|
|
311
|
+
agentSlug: z.string(),
|
|
312
|
+
folderPath: z.string(),
|
|
313
|
+
selectedAt: DateTimeStringSchema,
|
|
314
|
+
lastPulledAt: DateTimeStringSchema.optional(),
|
|
315
|
+
lastPulledVersion: z.string().optional()
|
|
316
|
+
});
|
|
317
|
+
var TrackedProjectSchema = z.object({
|
|
318
|
+
projectId: z.string().uuid(),
|
|
319
|
+
projectName: z.string(),
|
|
320
|
+
projectSlug: z.string(),
|
|
321
|
+
selectedAt: DateTimeStringSchema,
|
|
322
|
+
agents: z.array(TrackedAgentSchema),
|
|
323
|
+
lastPulledAt: DateTimeStringSchema.optional()
|
|
324
|
+
});
|
|
325
|
+
var CliConfigSchema = z.object({
|
|
326
|
+
auth: z.object({
|
|
327
|
+
token: z.string().optional(),
|
|
328
|
+
tokenId: z.string().uuid().optional(),
|
|
329
|
+
expiresAt: DateTimeStringSchema.optional(),
|
|
330
|
+
deviceName: z.string().optional()
|
|
331
|
+
}).optional(),
|
|
332
|
+
api: z.object({
|
|
333
|
+
baseUrl: z.string().url().default("https://api.porchestra.io/v1"),
|
|
334
|
+
skipTlsVerify: z.boolean().default(false)
|
|
335
|
+
}).default({}),
|
|
336
|
+
trackedProjects: z.array(TrackedProjectSchema).default([]),
|
|
337
|
+
output: z.object({
|
|
338
|
+
baseDir: z.string().default("./src/agents"),
|
|
339
|
+
createIndexFiles: z.boolean().default(true)
|
|
340
|
+
}).default({}),
|
|
341
|
+
cli: z.object({
|
|
342
|
+
lastLoginAt: DateTimeStringSchema.optional(),
|
|
343
|
+
lastVersionCheck: DateTimeStringSchema.optional(),
|
|
344
|
+
latestKnownVersion: z.string().optional()
|
|
345
|
+
}).optional(),
|
|
346
|
+
version: z.literal("1.0.0")
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
// src/core/config/config-manager.ts
|
|
350
|
+
var DEFAULT_CONFIG_DIR = path.join(os.homedir(), ".porchestra");
|
|
351
|
+
var CONFIG_FILE = "config.json";
|
|
352
|
+
var ConfigManager = class {
|
|
353
|
+
configPath;
|
|
354
|
+
config = null;
|
|
355
|
+
constructor() {
|
|
356
|
+
const configDir = process.env.PORCHESTRA_CONFIG_DIR || DEFAULT_CONFIG_DIR;
|
|
357
|
+
this.configPath = path.join(configDir, CONFIG_FILE);
|
|
358
|
+
}
|
|
359
|
+
async load() {
|
|
360
|
+
try {
|
|
361
|
+
const data = await fs.readFile(this.configPath, "utf-8");
|
|
362
|
+
const parsed = JSON.parse(data);
|
|
363
|
+
const validated = CliConfigSchema.parse(parsed);
|
|
364
|
+
this.config = validated;
|
|
365
|
+
return validated;
|
|
366
|
+
} catch (error) {
|
|
367
|
+
if (error.code === "ENOENT") {
|
|
368
|
+
const defaultConfig = { version: "1.0.0" };
|
|
369
|
+
this.config = CliConfigSchema.parse(defaultConfig);
|
|
370
|
+
return this.config;
|
|
371
|
+
}
|
|
372
|
+
if (error instanceof z2.ZodError) {
|
|
373
|
+
console.error("\u274C Config validation failed:");
|
|
374
|
+
error.errors.forEach((err) => {
|
|
375
|
+
console.error(` - ${err.path.join(".")}: ${err.message}`);
|
|
376
|
+
});
|
|
377
|
+
throw new Error(`Invalid config file at ${this.configPath}. Please run 'porchestra config reset' to fix.`);
|
|
378
|
+
}
|
|
379
|
+
throw error;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
async save(config) {
|
|
383
|
+
try {
|
|
384
|
+
const validated = CliConfigSchema.parse(config);
|
|
385
|
+
const configDir = path.dirname(this.configPath);
|
|
386
|
+
await fs.mkdir(configDir, { recursive: true });
|
|
387
|
+
const tempPath = `${this.configPath}.tmp`;
|
|
388
|
+
await fs.writeFile(
|
|
389
|
+
tempPath,
|
|
390
|
+
JSON.stringify(validated, null, 2),
|
|
391
|
+
"utf-8"
|
|
392
|
+
);
|
|
393
|
+
await fs.rename(tempPath, this.configPath);
|
|
394
|
+
this.config = validated;
|
|
395
|
+
} catch (error) {
|
|
396
|
+
if (error instanceof z2.ZodError) {
|
|
397
|
+
console.error("\u274C Config validation failed on save:");
|
|
398
|
+
error.errors.forEach((err) => {
|
|
399
|
+
console.error(` - ${err.path.join(".")}: ${err.message}`);
|
|
400
|
+
});
|
|
401
|
+
throw new Error("Failed to save config: validation error");
|
|
402
|
+
}
|
|
403
|
+
throw error;
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
async get() {
|
|
407
|
+
if (!this.config) {
|
|
408
|
+
return this.load();
|
|
409
|
+
}
|
|
410
|
+
return this.config;
|
|
411
|
+
}
|
|
412
|
+
async update(updater) {
|
|
413
|
+
const config = await this.get();
|
|
414
|
+
const updated = updater({ ...config });
|
|
415
|
+
await this.save(updated);
|
|
416
|
+
}
|
|
417
|
+
async clear() {
|
|
418
|
+
try {
|
|
419
|
+
await fs.unlink(this.configPath);
|
|
420
|
+
} catch (error) {
|
|
421
|
+
if (error.code !== "ENOENT") {
|
|
422
|
+
throw error;
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
this.config = null;
|
|
426
|
+
}
|
|
427
|
+
getConfigPath() {
|
|
428
|
+
return this.configPath;
|
|
429
|
+
}
|
|
430
|
+
};
|
|
431
|
+
|
|
432
|
+
// src/utils/errors.ts
|
|
433
|
+
var PorchestraError = class extends Error {
|
|
434
|
+
constructor(message, code) {
|
|
435
|
+
super(message);
|
|
436
|
+
this.code = code;
|
|
437
|
+
this.name = "PorchestraError";
|
|
438
|
+
}
|
|
439
|
+
};
|
|
440
|
+
var AuthenticationError = class extends PorchestraError {
|
|
441
|
+
constructor(message, retryable = false) {
|
|
442
|
+
super(message, "AUTH_ERROR");
|
|
443
|
+
this.retryable = retryable;
|
|
444
|
+
this.name = "AuthenticationError";
|
|
445
|
+
}
|
|
446
|
+
};
|
|
447
|
+
var NetworkError = class extends PorchestraError {
|
|
448
|
+
constructor(message, statusCode, retryable = true) {
|
|
449
|
+
super(message, "NETWORK_ERROR");
|
|
450
|
+
this.statusCode = statusCode;
|
|
451
|
+
this.retryable = retryable;
|
|
452
|
+
this.name = "NetworkError";
|
|
453
|
+
}
|
|
454
|
+
};
|
|
455
|
+
|
|
456
|
+
// src/core/auth/auth-service.ts
|
|
457
|
+
import got from "got";
|
|
458
|
+
import os2 from "os";
|
|
459
|
+
var AuthService = class {
|
|
460
|
+
configManager;
|
|
461
|
+
constructor(configManager) {
|
|
462
|
+
this.configManager = configManager;
|
|
463
|
+
}
|
|
464
|
+
async login(credentials) {
|
|
465
|
+
const config = await this.configManager.get();
|
|
466
|
+
const baseUrl = credentials.apiUrl || config.api?.baseUrl || "https://api.porchestra.io/v1";
|
|
467
|
+
const deviceInfo = {
|
|
468
|
+
os: os2.platform(),
|
|
469
|
+
version: os2.release(),
|
|
470
|
+
cliVersion: "1.0.0"
|
|
471
|
+
};
|
|
472
|
+
const deviceName = credentials.deviceName || `${os2.hostname()} - ${deviceInfo.os}`;
|
|
473
|
+
try {
|
|
474
|
+
const response = await got.post(`${baseUrl}/cli/login`, {
|
|
475
|
+
json: {
|
|
476
|
+
email: credentials.email,
|
|
477
|
+
password: credentials.password,
|
|
478
|
+
deviceName,
|
|
479
|
+
deviceInfo
|
|
480
|
+
},
|
|
481
|
+
headers: {
|
|
482
|
+
"Content-Type": "application/json"
|
|
483
|
+
},
|
|
484
|
+
https: {
|
|
485
|
+
rejectUnauthorized: !credentials.skipTlsVerify
|
|
486
|
+
}
|
|
487
|
+
}).json();
|
|
488
|
+
return response.data;
|
|
489
|
+
} catch (error) {
|
|
490
|
+
if (error.response?.statusCode) {
|
|
491
|
+
if (error.response.statusCode === 401) {
|
|
492
|
+
throw new AuthenticationError("Invalid email or password");
|
|
493
|
+
}
|
|
494
|
+
throw new AuthenticationError(`Login failed: ${error.message}`);
|
|
495
|
+
}
|
|
496
|
+
if (error.code === "ECONNREFUSED" || error.code === "ENOTFOUND" || error.code === "ETIMEDOUT") {
|
|
497
|
+
throw new NetworkError(`Network error: ${error.message}`);
|
|
498
|
+
}
|
|
499
|
+
throw error;
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
async refreshToken(tokenId, currentToken) {
|
|
503
|
+
const config = await this.configManager.get();
|
|
504
|
+
const baseUrl = config.api?.baseUrl || "https://api.porchestra.io/v1";
|
|
505
|
+
try {
|
|
506
|
+
const response = await got.post(`${baseUrl}/cli/token/refresh`, {
|
|
507
|
+
json: {
|
|
508
|
+
tokenId,
|
|
509
|
+
currentToken
|
|
510
|
+
},
|
|
511
|
+
headers: {
|
|
512
|
+
"Authorization": `Bearer ${currentToken}`,
|
|
513
|
+
"Content-Type": "application/json"
|
|
514
|
+
}
|
|
515
|
+
}).json();
|
|
516
|
+
return response.data;
|
|
517
|
+
} catch (error) {
|
|
518
|
+
if (error.response?.statusCode === 401) {
|
|
519
|
+
throw new AuthenticationError("Token refresh failed. Please login again.");
|
|
520
|
+
}
|
|
521
|
+
throw error;
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
async revokeToken(token, revokeAll = false) {
|
|
525
|
+
const config = await this.configManager.get();
|
|
526
|
+
const baseUrl = config.api?.baseUrl || "https://api.porchestra.io/v1";
|
|
527
|
+
try {
|
|
528
|
+
const response = await got.delete(`${baseUrl}/cli/token`, {
|
|
529
|
+
json: {
|
|
530
|
+
revokeAll
|
|
531
|
+
},
|
|
532
|
+
headers: {
|
|
533
|
+
"Authorization": `Bearer ${token}`,
|
|
534
|
+
"Content-Type": "application/json"
|
|
535
|
+
}
|
|
536
|
+
}).json();
|
|
537
|
+
return response.data;
|
|
538
|
+
} catch (error) {
|
|
539
|
+
if (error.response?.statusCode === 401) {
|
|
540
|
+
throw new AuthenticationError("Token already revoked or invalid");
|
|
541
|
+
}
|
|
542
|
+
throw error;
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
async getCurrentUser(token) {
|
|
546
|
+
const config = await this.configManager.get();
|
|
547
|
+
const baseUrl = config.api?.baseUrl || "https://api.porchestra.io/v1";
|
|
548
|
+
try {
|
|
549
|
+
const response = await got.get(`${baseUrl}/auth/me`, {
|
|
550
|
+
headers: {
|
|
551
|
+
"Authorization": `Bearer ${token}`
|
|
552
|
+
}
|
|
553
|
+
}).json();
|
|
554
|
+
return response.data;
|
|
555
|
+
} catch (error) {
|
|
556
|
+
if (error.response?.statusCode === 401) {
|
|
557
|
+
throw new AuthenticationError("Token is invalid or expired");
|
|
558
|
+
}
|
|
559
|
+
throw error;
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
async checkAndRefreshTokenIfNeeded() {
|
|
563
|
+
const config = await this.configManager.get();
|
|
564
|
+
if (!config.auth?.token || !config.auth?.tokenId || !config.auth?.expiresAt) {
|
|
565
|
+
return false;
|
|
566
|
+
}
|
|
567
|
+
const expiresAt = new Date(config.auth.expiresAt);
|
|
568
|
+
const now = /* @__PURE__ */ new Date();
|
|
569
|
+
const sevenDaysFromNow = new Date(now.getTime() + 7 * 24 * 60 * 60 * 1e3);
|
|
570
|
+
if (expiresAt < sevenDaysFromNow) {
|
|
571
|
+
try {
|
|
572
|
+
const refreshed = await this.refreshToken(
|
|
573
|
+
config.auth.tokenId,
|
|
574
|
+
config.auth.token
|
|
575
|
+
);
|
|
576
|
+
await this.configManager.update((cfg) => ({
|
|
577
|
+
...cfg,
|
|
578
|
+
auth: {
|
|
579
|
+
...cfg.auth,
|
|
580
|
+
token: refreshed.token,
|
|
581
|
+
expiresAt: refreshed.expiresAt
|
|
582
|
+
}
|
|
583
|
+
}));
|
|
584
|
+
return true;
|
|
585
|
+
} catch {
|
|
586
|
+
return false;
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
return true;
|
|
590
|
+
}
|
|
591
|
+
};
|
|
592
|
+
|
|
593
|
+
// src/core/auth/token-manager.ts
|
|
594
|
+
var TokenManager = class {
|
|
595
|
+
constructor(configManager) {
|
|
596
|
+
this.configManager = configManager;
|
|
597
|
+
}
|
|
598
|
+
async getToken() {
|
|
599
|
+
const config = await this.configManager.get();
|
|
600
|
+
return config.auth?.token;
|
|
601
|
+
}
|
|
602
|
+
async getTokenId() {
|
|
603
|
+
const config = await this.configManager.get();
|
|
604
|
+
return config.auth?.tokenId;
|
|
605
|
+
}
|
|
606
|
+
async getExpiresAt() {
|
|
607
|
+
const config = await this.configManager.get();
|
|
608
|
+
return config.auth?.expiresAt;
|
|
609
|
+
}
|
|
610
|
+
async isAuthenticated() {
|
|
611
|
+
const token = await this.getToken();
|
|
612
|
+
if (!token) return false;
|
|
613
|
+
const expiresAt = await this.getExpiresAt();
|
|
614
|
+
if (!expiresAt) return false;
|
|
615
|
+
const expiryDate = new Date(expiresAt);
|
|
616
|
+
return expiryDate > /* @__PURE__ */ new Date();
|
|
617
|
+
}
|
|
618
|
+
async getDaysUntilExpiry() {
|
|
619
|
+
const expiresAt = await this.getExpiresAt();
|
|
620
|
+
if (!expiresAt) return null;
|
|
621
|
+
const expiryDate = new Date(expiresAt);
|
|
622
|
+
const now = /* @__PURE__ */ new Date();
|
|
623
|
+
const diffMs = expiryDate.getTime() - now.getTime();
|
|
624
|
+
return Math.ceil(diffMs / (1e3 * 60 * 60 * 24));
|
|
625
|
+
}
|
|
626
|
+
async isExpiringSoon(days = 7) {
|
|
627
|
+
const daysUntilExpiry = await this.getDaysUntilExpiry();
|
|
628
|
+
if (daysUntilExpiry === null) return false;
|
|
629
|
+
return daysUntilExpiry <= days;
|
|
630
|
+
}
|
|
631
|
+
};
|
|
632
|
+
|
|
633
|
+
// src/core/api/client.ts
|
|
634
|
+
import got2 from "got";
|
|
635
|
+
var MAX_RETRIES = 3;
|
|
636
|
+
var RETRY_DELAY = 1e3;
|
|
637
|
+
var ApiClient = class {
|
|
638
|
+
configManager;
|
|
639
|
+
constructor(configManager) {
|
|
640
|
+
this.configManager = configManager;
|
|
641
|
+
}
|
|
642
|
+
async getBaseUrl() {
|
|
643
|
+
const config = await this.configManager.get();
|
|
644
|
+
return config.api?.baseUrl || "https://api.porchestra.io/v1";
|
|
645
|
+
}
|
|
646
|
+
async getToken() {
|
|
647
|
+
const config = await this.configManager.get();
|
|
648
|
+
return config.auth?.token;
|
|
649
|
+
}
|
|
650
|
+
async requestWithRetry(url, options, attempt = 1) {
|
|
651
|
+
try {
|
|
652
|
+
const token = await this.getToken();
|
|
653
|
+
const headers = {
|
|
654
|
+
"Content-Type": "application/json",
|
|
655
|
+
...token && { Authorization: `Bearer ${token}` },
|
|
656
|
+
...options.headers || {}
|
|
657
|
+
};
|
|
658
|
+
const response = await got2(url, {
|
|
659
|
+
method: options.method,
|
|
660
|
+
headers,
|
|
661
|
+
retry: { limit: 0 }
|
|
662
|
+
}).json();
|
|
663
|
+
return response.data;
|
|
664
|
+
} catch (error) {
|
|
665
|
+
if (error.response?.statusCode === 401) {
|
|
666
|
+
throw new AuthenticationError("Authentication failed. Please login again.");
|
|
667
|
+
}
|
|
668
|
+
const isRetryable = error.code === "ECONNRESET" || error.code === "ETIMEDOUT" || error.code === "ENOTFOUND" || error.response?.statusCode && error.response.statusCode >= 500;
|
|
669
|
+
if (isRetryable && attempt < MAX_RETRIES) {
|
|
670
|
+
await new Promise((resolve) => setTimeout(resolve, RETRY_DELAY * attempt));
|
|
671
|
+
return this.requestWithRetry(url, options, attempt + 1);
|
|
672
|
+
}
|
|
673
|
+
throw new NetworkError(
|
|
674
|
+
`API request failed: ${error.message}`,
|
|
675
|
+
error.response?.statusCode,
|
|
676
|
+
isRetryable
|
|
677
|
+
);
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
async getProjectsBrief() {
|
|
681
|
+
const baseUrl = await this.getBaseUrl();
|
|
682
|
+
return this.requestWithRetry(
|
|
683
|
+
`${baseUrl}/projects/brief`,
|
|
684
|
+
{ method: "GET" }
|
|
685
|
+
);
|
|
686
|
+
}
|
|
687
|
+
async getProjectAgents(projectId, environment) {
|
|
688
|
+
const baseUrl = await this.getBaseUrl();
|
|
689
|
+
const query = environment ? `?environment=${environment}` : "";
|
|
690
|
+
return this.requestWithRetry(
|
|
691
|
+
`${baseUrl}/projects/${projectId}/agents${query}`,
|
|
692
|
+
{ method: "GET" }
|
|
693
|
+
);
|
|
694
|
+
}
|
|
695
|
+
async getAgentTools(projectId, agentId, environment) {
|
|
696
|
+
const baseUrl = await this.getBaseUrl();
|
|
697
|
+
const query = environment ? `?environment=${environment}` : "";
|
|
698
|
+
return this.requestWithRetry(
|
|
699
|
+
`${baseUrl}/projects/${projectId}/agents/${agentId}/tools${query}`,
|
|
700
|
+
{ method: "GET" }
|
|
701
|
+
);
|
|
702
|
+
}
|
|
703
|
+
async getCurrentUser(token) {
|
|
704
|
+
const baseUrl = await this.getBaseUrl();
|
|
705
|
+
return this.requestWithRetry(
|
|
706
|
+
`${baseUrl}/auth/me`,
|
|
707
|
+
{
|
|
708
|
+
method: "GET",
|
|
709
|
+
headers: { Authorization: `Bearer ${token}` }
|
|
710
|
+
}
|
|
711
|
+
);
|
|
712
|
+
}
|
|
713
|
+
async getCliConfig() {
|
|
714
|
+
const baseUrl = await this.getBaseUrl();
|
|
715
|
+
return this.requestWithRetry(
|
|
716
|
+
`${baseUrl}/cli/config`,
|
|
717
|
+
{ method: "GET" }
|
|
718
|
+
);
|
|
719
|
+
}
|
|
720
|
+
};
|
|
721
|
+
|
|
722
|
+
// src/core/generators/code-generator.ts
|
|
723
|
+
import fs2 from "fs/promises";
|
|
724
|
+
import path3 from "path";
|
|
725
|
+
import Handlebars from "handlebars";
|
|
726
|
+
|
|
727
|
+
// src/utils/path-utils.ts
|
|
728
|
+
import path2 from "path";
|
|
729
|
+
function normalizeFolderPath(folderPath) {
|
|
730
|
+
let normalized = folderPath.replace(/^\//, "");
|
|
731
|
+
normalized = normalized.replace(/\/+/g, "/");
|
|
732
|
+
normalized = normalized.split("/").map((segment) => sanitizePathSegment(segment)).join("/");
|
|
733
|
+
normalized = normalized.replace(/\/$/, "");
|
|
734
|
+
return normalized;
|
|
735
|
+
}
|
|
736
|
+
function sanitizePathSegment(segment) {
|
|
737
|
+
return segment.replace(/[^a-zA-Z0-9-_]/g, "-").replace(/^-+|-+$/g, "").replace(/-+/g, "-");
|
|
738
|
+
}
|
|
739
|
+
function calculateAgentOutputPath(baseDir, folderPath, agentName, filename) {
|
|
740
|
+
const normalizedFolder = normalizeFolderPath(folderPath);
|
|
741
|
+
const normalizedAgent = sanitizePathSegment(agentName);
|
|
742
|
+
const fullPath = path2.join(baseDir, normalizedFolder, normalizedAgent);
|
|
743
|
+
return filename ? path2.join(fullPath, filename) : fullPath;
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
// src/core/generators/code-generator.ts
|
|
747
|
+
Handlebars.registerHelper("pascalCase", function(str) {
|
|
748
|
+
return str.replace(/[-_\s](.)/g, (_, char) => char.toUpperCase()).replace(/^(.)/, (_, char) => char.toUpperCase());
|
|
749
|
+
});
|
|
750
|
+
Handlebars.registerHelper("camelCase", function(str) {
|
|
751
|
+
return str.replace(/[-_\s](.)/g, (_, char) => char.toUpperCase()).replace(/^(.)/, (_, char) => char.toLowerCase());
|
|
752
|
+
});
|
|
753
|
+
Handlebars.registerHelper("zodType", function(schema) {
|
|
754
|
+
return jsonSchemaToZodType(schema);
|
|
755
|
+
});
|
|
756
|
+
Handlebars.registerHelper("json", function(value) {
|
|
757
|
+
return JSON.stringify(value ?? {}, null, 2);
|
|
758
|
+
});
|
|
759
|
+
function jsonSchemaToZodType(schema) {
|
|
760
|
+
if (!schema) return "z.any()";
|
|
761
|
+
switch (schema.type) {
|
|
762
|
+
case "string": {
|
|
763
|
+
let zod = "z.string()";
|
|
764
|
+
if (schema.minLength) zod += `.min(${schema.minLength})`;
|
|
765
|
+
if (schema.maxLength) zod += `.max(${schema.maxLength})`;
|
|
766
|
+
if (schema.pattern) zod += `.regex(/${schema.pattern}/)`;
|
|
767
|
+
if (schema.enum) zod = `z.enum([${schema.enum.map((e) => `'${e}'`).join(", ")}])`;
|
|
768
|
+
if (schema.nullable) zod += ".nullable()";
|
|
769
|
+
return zod;
|
|
770
|
+
}
|
|
771
|
+
case "number":
|
|
772
|
+
case "integer": {
|
|
773
|
+
let zod = schema.type === "integer" ? "z.number().int()" : "z.number()";
|
|
774
|
+
if (schema.minimum !== void 0) zod += `.min(${schema.minimum})`;
|
|
775
|
+
if (schema.maximum !== void 0) zod += `.max(${schema.maximum})`;
|
|
776
|
+
if (schema.nullable) zod += ".nullable()";
|
|
777
|
+
return zod;
|
|
778
|
+
}
|
|
779
|
+
case "boolean": {
|
|
780
|
+
let zod = "z.boolean()";
|
|
781
|
+
if (schema.nullable) zod += ".nullable()";
|
|
782
|
+
return zod;
|
|
783
|
+
}
|
|
784
|
+
case "array": {
|
|
785
|
+
const itemType = jsonSchemaToZodType(schema.items);
|
|
786
|
+
let zod = `z.array(${itemType})`;
|
|
787
|
+
if (schema.minItems) zod += `.min(${schema.minItems})`;
|
|
788
|
+
if (schema.maxItems) zod += `.max(${schema.maxItems})`;
|
|
789
|
+
if (schema.nullable) zod += ".nullable()";
|
|
790
|
+
return zod;
|
|
791
|
+
}
|
|
792
|
+
case "object": {
|
|
793
|
+
if (!schema.properties) return "z.record(z.any())";
|
|
794
|
+
const required = new Set(schema.required || []);
|
|
795
|
+
const props = Object.entries(schema.properties).map(([key, prop]) => {
|
|
796
|
+
const propType = jsonSchemaToZodType(prop);
|
|
797
|
+
const isRequired = required.has(key);
|
|
798
|
+
const formattedKey = /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(key) ? key : JSON.stringify(key);
|
|
799
|
+
return ` ${formattedKey}: ${isRequired ? propType : `${propType}.optional()`}`;
|
|
800
|
+
});
|
|
801
|
+
let zod = `z.object({
|
|
802
|
+
${props.join(",\n")}
|
|
803
|
+
})`;
|
|
804
|
+
if (schema.additionalProperties === false) zod += ".strict()";
|
|
805
|
+
if (schema.nullable) zod += ".nullable()";
|
|
806
|
+
return zod;
|
|
807
|
+
}
|
|
808
|
+
default:
|
|
809
|
+
return "z.any()";
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
var CodeGenerator = class {
|
|
813
|
+
schemasTemplate;
|
|
814
|
+
toolsTemplate;
|
|
815
|
+
indexTemplate;
|
|
816
|
+
constructor() {
|
|
817
|
+
this.schemasTemplate = Handlebars.compile(schemasTemplateSource);
|
|
818
|
+
this.toolsTemplate = Handlebars.compile(toolsTemplateSource);
|
|
819
|
+
this.indexTemplate = Handlebars.compile(indexTemplateSource);
|
|
820
|
+
}
|
|
821
|
+
async generate(options) {
|
|
822
|
+
const { agent, tools, components, outputDir } = options;
|
|
823
|
+
await fs2.mkdir(outputDir, { recursive: true });
|
|
824
|
+
const responseJsonSchema = components?.responseSchema?.content || {};
|
|
825
|
+
const templateData = {
|
|
826
|
+
agentName: agent?.name || "Unknown Agent",
|
|
827
|
+
version: agent?.version || "unknown",
|
|
828
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
829
|
+
tools: (tools || []).filter((t) => t && !t.isBuiltin && typeof t.name === "string" && t.name.length > 0).map((t) => ({
|
|
830
|
+
name: t.name,
|
|
831
|
+
description: t.description || "",
|
|
832
|
+
parameters: t.parameters || {},
|
|
833
|
+
returns: t.returns ?? null,
|
|
834
|
+
isBuiltin: t.isBuiltin,
|
|
835
|
+
builtinType: t.builtinType
|
|
836
|
+
})),
|
|
837
|
+
variableSchema: components?.variableSchema?.content || null,
|
|
838
|
+
inputSchema: components?.inputSchema?.content || null,
|
|
839
|
+
responseSchema: components?.responseSchema?.content || null,
|
|
840
|
+
responseJsonSchema
|
|
841
|
+
};
|
|
842
|
+
const astPath = path3.join(outputDir, "ast.json");
|
|
843
|
+
const astContent = JSON.stringify(components?.ast?.content ?? {}, null, 2);
|
|
844
|
+
await fs2.writeFile(astPath, astContent, "utf-8");
|
|
845
|
+
const configPath = path3.join(outputDir, "config.ts");
|
|
846
|
+
const configContent = `/**
|
|
847
|
+
* Auto-generated model config for ${templateData.agentName}
|
|
848
|
+
* Generated: ${templateData.generatedAt}
|
|
849
|
+
* Version: ${templateData.version}
|
|
850
|
+
* DO NOT MODIFY - This file is auto-generated by Porchestra CLI
|
|
851
|
+
*/
|
|
852
|
+
|
|
853
|
+
export const ModelConfig = ${JSON.stringify(components?.modelConfig?.content ?? {}, null, 2)} as const;
|
|
854
|
+
`;
|
|
855
|
+
await fs2.writeFile(configPath, configContent, "utf-8");
|
|
856
|
+
const schemasPath = path3.join(outputDir, "schemas.ts");
|
|
857
|
+
const schemasContent = this.schemasTemplate(templateData);
|
|
858
|
+
await fs2.writeFile(schemasPath, schemasContent, "utf-8");
|
|
859
|
+
const toolsPath = path3.join(outputDir, "tools.ts");
|
|
860
|
+
const toolsContent = this.toolsTemplate(templateData);
|
|
861
|
+
await fs2.writeFile(toolsPath, toolsContent, "utf-8");
|
|
862
|
+
const indexPath = path3.join(outputDir, "index.ts");
|
|
863
|
+
const indexContent = this.indexTemplate(templateData);
|
|
864
|
+
await fs2.writeFile(indexPath, indexContent, "utf-8");
|
|
865
|
+
}
|
|
866
|
+
calculateOutputPath(baseDir, folderPath, agentName) {
|
|
867
|
+
return calculateAgentOutputPath(baseDir, folderPath, agentName);
|
|
868
|
+
}
|
|
869
|
+
};
|
|
870
|
+
var schemasTemplateSource = `/**
|
|
871
|
+
* Auto-generated Zod schemas for {{agentName}}
|
|
872
|
+
* Generated: {{generatedAt}}
|
|
873
|
+
* Version: {{version}}
|
|
874
|
+
* DO NOT MODIFY - This file is auto-generated by Porchestra CLI
|
|
875
|
+
*/
|
|
876
|
+
|
|
877
|
+
import { z } from 'zod';
|
|
878
|
+
|
|
879
|
+
// 1. System Prompt Variables
|
|
880
|
+
export const VariableSchema = {{{zodType variableSchema}}};
|
|
881
|
+
export type AgentVariables = z.infer<typeof VariableSchema>;
|
|
882
|
+
|
|
883
|
+
// 2. User Input
|
|
884
|
+
export const InputSchema = {{{zodType inputSchema}}};
|
|
885
|
+
export type AgentInput = z.infer<typeof InputSchema>;
|
|
886
|
+
|
|
887
|
+
// 3. Model Response
|
|
888
|
+
export const ResponseSchema = {{{zodType responseSchema}}};
|
|
889
|
+
export type AgentResponse = z.infer<typeof ResponseSchema>;
|
|
890
|
+
|
|
891
|
+
// 4. Raw JSON Schema (for LLM Provider 'response_format')
|
|
892
|
+
export const ResponseJsonSchema = {{{json responseJsonSchema}}} as const;
|
|
893
|
+
`;
|
|
894
|
+
var toolsTemplateSource = `/**
|
|
895
|
+
* Auto-generated tools for {{agentName}}
|
|
896
|
+
* Generated: {{generatedAt}}
|
|
897
|
+
* Version: {{version}}
|
|
898
|
+
* DO NOT MODIFY - This file is auto-generated by Porchestra CLI
|
|
899
|
+
*/
|
|
900
|
+
|
|
901
|
+
import { z } from 'zod';
|
|
902
|
+
|
|
903
|
+
{{#each tools}}
|
|
904
|
+
// Schema for {{name}}
|
|
905
|
+
export const {{pascalCase name}}Params = {{{zodType parameters}}};
|
|
906
|
+
export type {{pascalCase name}}ParamsType = z.infer<typeof {{pascalCase name}}Params>;
|
|
907
|
+
|
|
908
|
+
{{/each}}
|
|
909
|
+
// The Interface the user MUST implement
|
|
910
|
+
export interface ToolImplementation {
|
|
911
|
+
{{#each tools}}
|
|
912
|
+
{{camelCase name}}: (args: {{pascalCase name}}ParamsType) => Promise<unknown>;
|
|
913
|
+
{{/each}}
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
// Raw Tool Definitions for provider SDKs
|
|
917
|
+
export const ToolDefinitions = [
|
|
918
|
+
{{#each tools}}
|
|
919
|
+
{
|
|
920
|
+
name: '{{name}}',
|
|
921
|
+
description: {{{json description}}},
|
|
922
|
+
parameters: {{{json parameters}}},
|
|
923
|
+
},
|
|
924
|
+
{{/each}}
|
|
925
|
+
] as const;
|
|
926
|
+
|
|
927
|
+
// Dispatcher with validation
|
|
928
|
+
export async function dispatch(
|
|
929
|
+
name: string,
|
|
930
|
+
args: unknown,
|
|
931
|
+
impl: ToolImplementation
|
|
932
|
+
): Promise<unknown> {
|
|
933
|
+
switch (name) {
|
|
934
|
+
{{#each tools}}
|
|
935
|
+
case '{{name}}':
|
|
936
|
+
return await impl.{{camelCase name}}({{pascalCase name}}Params.parse(args));
|
|
937
|
+
{{/each}}
|
|
938
|
+
default:
|
|
939
|
+
throw new Error(\`Unknown tool: \${name}\`);
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
`;
|
|
943
|
+
var indexTemplateSource = `/**
|
|
944
|
+
* Auto-generated Agent class for {{agentName}}
|
|
945
|
+
* Generated: {{generatedAt}}
|
|
946
|
+
* Version: {{version}}
|
|
947
|
+
* DO NOT MODIFY - This file is auto-generated by Porchestra CLI
|
|
948
|
+
*/
|
|
949
|
+
|
|
950
|
+
import { PorchestraRuntime } from '@porchestra/core';
|
|
951
|
+
import rawAst from './ast.json';
|
|
952
|
+
import { ModelConfig } from './config.js';
|
|
953
|
+
import * as Schemas from './schemas.js';
|
|
954
|
+
import * as Tools from './tools.js';
|
|
955
|
+
|
|
956
|
+
export class {{pascalCase agentName}}Agent {
|
|
957
|
+
/**
|
|
958
|
+
* @param tools - The implementation of the tool logic. Required if the agent uses tools.
|
|
959
|
+
*/
|
|
960
|
+
constructor(private readonly tools?: Tools.ToolImplementation) {}
|
|
961
|
+
|
|
962
|
+
/**
|
|
963
|
+
* Renders the System Prompt.
|
|
964
|
+
* Throws ZodError if variables are invalid.
|
|
965
|
+
*/
|
|
966
|
+
public renderPrompt(variables: Schemas.AgentVariables): string {
|
|
967
|
+
const safeVars = Schemas.VariableSchema.parse(variables);
|
|
968
|
+
return PorchestraRuntime.render(rawAst as any, safeVars);
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
/**
|
|
972
|
+
* Routes an LLM Tool Call to your implementation.
|
|
973
|
+
* Validates arguments automatically.
|
|
974
|
+
*/
|
|
975
|
+
public async handleTool(name: string, args: unknown): Promise<unknown> {
|
|
976
|
+
if (!this.tools) {
|
|
977
|
+
throw new Error('No tool implementation provided to Agent constructor.');
|
|
978
|
+
}
|
|
979
|
+
return Tools.dispatch(name, args, this.tools);
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
/** Validates User Input (e.g. from API Request) */
|
|
983
|
+
public static validateInput(data: unknown): Schemas.AgentInput {
|
|
984
|
+
return Schemas.InputSchema.parse(data);
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
/** Validates LLM Output (Post-generation check) */
|
|
988
|
+
public static validateResponse(data: unknown): Schemas.AgentResponse {
|
|
989
|
+
return Schemas.ResponseSchema.parse(data);
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
/** Get Provider Configuration */
|
|
993
|
+
public static get config() {
|
|
994
|
+
return ModelConfig;
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
/** Get Tool Definitions for the LLM Provider */
|
|
998
|
+
public static get toolDefinitions() {
|
|
999
|
+
return Tools.ToolDefinitions;
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
/** Get JSON Schema for Structured Outputs */
|
|
1003
|
+
public static get responseFormat() {
|
|
1004
|
+
return Schemas.ResponseJsonSchema;
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
`;
|
|
1008
|
+
|
|
1009
|
+
// src/commands/login.ts
|
|
1010
|
+
import { Command } from "commander";
|
|
1011
|
+
import pc from "picocolors";
|
|
1012
|
+
import { input, password } from "@inquirer/prompts";
|
|
1013
|
+
function createLoginCommand(configManager, authService) {
|
|
1014
|
+
return new Command("login").description("Authenticate with your Porchestra account").option("--api-url <url>", "Override API URL").option("--skip-tls-verify", "Skip TLS certificate verification (insecure)").action(async (options) => {
|
|
1015
|
+
console.log(pc.bold("\n \u{1F510} Porchestra CLI Login\n"));
|
|
1016
|
+
const apiUrl = options.apiUrl || process.env.PORCHESTRA_API_URL;
|
|
1017
|
+
if (options.skipTlsVerify) {
|
|
1018
|
+
console.log(pc.yellow("\n \u26A0\uFE0F WARNING: TLS verification disabled. Not recommended for production.\n"));
|
|
1019
|
+
}
|
|
1020
|
+
const email = await input({
|
|
1021
|
+
message: " Email:",
|
|
1022
|
+
validate: (value) => value.includes("@") || "Please enter a valid email"
|
|
1023
|
+
});
|
|
1024
|
+
const pwd = await password({
|
|
1025
|
+
message: " Password:",
|
|
1026
|
+
mask: "*"
|
|
1027
|
+
});
|
|
1028
|
+
try {
|
|
1029
|
+
const result = await authService.login({
|
|
1030
|
+
email,
|
|
1031
|
+
password: pwd,
|
|
1032
|
+
apiUrl,
|
|
1033
|
+
skipTlsVerify: options.skipTlsVerify
|
|
1034
|
+
});
|
|
1035
|
+
console.log(pc.yellow("\n [DEBUG] Raw API response:"));
|
|
1036
|
+
console.log(pc.yellow(` token: ${result.token?.substring(0, 20)}...`));
|
|
1037
|
+
console.log(pc.yellow(` tokenId: ${result.tokenId}`));
|
|
1038
|
+
console.log(pc.yellow(` expiresAt: ${result.expiresAt}`));
|
|
1039
|
+
console.log(pc.yellow(` issuedAt: ${result.issuedAt}`));
|
|
1040
|
+
console.log(pc.yellow(` deviceName: ${result.deviceName}`));
|
|
1041
|
+
try {
|
|
1042
|
+
await configManager.update((cfg) => ({
|
|
1043
|
+
...cfg,
|
|
1044
|
+
auth: {
|
|
1045
|
+
token: result.token,
|
|
1046
|
+
tokenId: result.tokenId,
|
|
1047
|
+
expiresAt: result.expiresAt,
|
|
1048
|
+
deviceName: result.deviceName
|
|
1049
|
+
},
|
|
1050
|
+
api: {
|
|
1051
|
+
baseUrl: apiUrl || cfg.api?.baseUrl || "https://api.porchestra.io/v1",
|
|
1052
|
+
skipTlsVerify: options.skipTlsVerify || false
|
|
1053
|
+
},
|
|
1054
|
+
cli: {
|
|
1055
|
+
...cfg.cli,
|
|
1056
|
+
lastLoginAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1057
|
+
}
|
|
1058
|
+
}));
|
|
1059
|
+
const savedConfig = await configManager.get();
|
|
1060
|
+
console.log(pc.yellow("\n [DEBUG] Saved config auth:"));
|
|
1061
|
+
console.log(pc.yellow(` ${JSON.stringify(savedConfig.auth, null, 2)}`));
|
|
1062
|
+
} catch (saveError) {
|
|
1063
|
+
console.error(pc.red("\n [DEBUG] Save error:"), saveError);
|
|
1064
|
+
throw saveError;
|
|
1065
|
+
}
|
|
1066
|
+
const expiresAt = new Date(result.expiresAt);
|
|
1067
|
+
const daysUntilExpiry = Math.ceil((expiresAt.getTime() - Date.now()) / (1e3 * 60 * 60 * 24));
|
|
1068
|
+
console.log(pc.green(`
|
|
1069
|
+
\u2713 Login successful
|
|
1070
|
+
`));
|
|
1071
|
+
console.log(pc.gray(` Token expires in ${daysUntilExpiry} days (${result.expiresAt})`));
|
|
1072
|
+
console.log(pc.gray(` Device: ${result.deviceName}`));
|
|
1073
|
+
if (apiUrl) {
|
|
1074
|
+
console.log(pc.gray(` API: ${apiUrl}`));
|
|
1075
|
+
}
|
|
1076
|
+
console.log(pc.gray(` Auto-refresh: 7 days before expiry`));
|
|
1077
|
+
console.log();
|
|
1078
|
+
} catch (error) {
|
|
1079
|
+
console.error(pc.red(`
|
|
1080
|
+
\u2717 Login failed: ${error.message}`));
|
|
1081
|
+
process.exit(1);
|
|
1082
|
+
}
|
|
1083
|
+
});
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
// src/commands/logout.ts
|
|
1087
|
+
import { Command as Command2 } from "commander";
|
|
1088
|
+
import pc2 from "picocolors";
|
|
1089
|
+
import { confirm } from "@inquirer/prompts";
|
|
1090
|
+
function createLogoutCommand(configManager, authService) {
|
|
1091
|
+
return new Command2("logout").description("Logout and revoke CLI token").option("--all", "Revoke all device tokens (logout everywhere)").action(async (options) => {
|
|
1092
|
+
const config = await configManager.get();
|
|
1093
|
+
if (!config.auth?.token) {
|
|
1094
|
+
console.log(pc2.yellow("Not currently logged in"));
|
|
1095
|
+
return;
|
|
1096
|
+
}
|
|
1097
|
+
if (options.all) {
|
|
1098
|
+
const confirmed = await confirm({
|
|
1099
|
+
message: "This will revoke ALL your CLI tokens on ALL devices. Continue?",
|
|
1100
|
+
default: false
|
|
1101
|
+
});
|
|
1102
|
+
if (!confirmed) return;
|
|
1103
|
+
}
|
|
1104
|
+
try {
|
|
1105
|
+
await authService.revokeToken(config.auth.token, options.all);
|
|
1106
|
+
await configManager.clear();
|
|
1107
|
+
if (options.all) {
|
|
1108
|
+
console.log(pc2.green("\u2713 Revoked all tokens and logged out all devices"));
|
|
1109
|
+
} else {
|
|
1110
|
+
console.log(pc2.green("\u2713 Logged out successfully"));
|
|
1111
|
+
}
|
|
1112
|
+
} catch (error) {
|
|
1113
|
+
await configManager.clear();
|
|
1114
|
+
console.log(pc2.yellow("\u2713 Cleared local credentials (server revoke may have failed)"));
|
|
1115
|
+
}
|
|
1116
|
+
});
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
// src/commands/whoami.ts
|
|
1120
|
+
init_project_tracker();
|
|
1121
|
+
import { Command as Command3 } from "commander";
|
|
1122
|
+
import pc3 from "picocolors";
|
|
1123
|
+
function createWhoamiCommand(configManager, authService) {
|
|
1124
|
+
const projectTracker = new ProjectTracker(configManager);
|
|
1125
|
+
return new Command3("whoami").description("Show current user and token information").action(async () => {
|
|
1126
|
+
const config = await configManager.get();
|
|
1127
|
+
if (!config.auth?.token) {
|
|
1128
|
+
console.log(pc3.yellow("Not logged in. Run `porchestra login`"));
|
|
1129
|
+
return;
|
|
1130
|
+
}
|
|
1131
|
+
try {
|
|
1132
|
+
const userInfo = await authService.getCurrentUser(config.auth.token);
|
|
1133
|
+
const expiresAt = new Date(config.auth.expiresAt);
|
|
1134
|
+
const isExpiringSoon = expiresAt.getTime() - Date.now() < 7 * 24 * 60 * 60 * 1e3;
|
|
1135
|
+
console.log(pc3.bold("\n\u{1F464} User Information\n"));
|
|
1136
|
+
console.log(` Email: ${pc3.cyan(userInfo.email)}`);
|
|
1137
|
+
console.log(` Name: ${userInfo.name || "N/A"}`);
|
|
1138
|
+
console.log(pc3.bold("\n\u{1F511} Token Information\n"));
|
|
1139
|
+
console.log(` Device: ${config.auth.deviceName || "N/A"}`);
|
|
1140
|
+
console.log(` Expires: ${isExpiringSoon ? pc3.yellow(expiresAt.toISOString()) : pc3.green(expiresAt.toISOString())}`);
|
|
1141
|
+
if (isExpiringSoon) {
|
|
1142
|
+
console.log(pc3.yellow(" \u26A0\uFE0F Expires within 7 days - will auto-refresh"));
|
|
1143
|
+
}
|
|
1144
|
+
if (config.api?.baseUrl !== "https://api.porchestra.io/v1") {
|
|
1145
|
+
console.log(pc3.bold("\n\u{1F517} API Configuration\n"));
|
|
1146
|
+
console.log(` URL: ${config.api?.baseUrl}`);
|
|
1147
|
+
}
|
|
1148
|
+
const summary = await projectTracker.getSummary();
|
|
1149
|
+
console.log(pc3.bold("\n\u{1F4C1} Tracked Projects\n"));
|
|
1150
|
+
console.log(` Projects: ${summary.projectCount}`);
|
|
1151
|
+
console.log(` Agents: ${summary.agentCount}`);
|
|
1152
|
+
} catch (error) {
|
|
1153
|
+
console.log(pc3.red("Failed to fetch user info. Token may be invalid."));
|
|
1154
|
+
console.log(pc3.gray("Run `porchestra login` to re-authenticate."));
|
|
1155
|
+
}
|
|
1156
|
+
});
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
// src/index.ts
|
|
1160
|
+
init_explore();
|
|
1161
|
+
|
|
1162
|
+
// src/commands/pull.ts
|
|
1163
|
+
init_project_tracker();
|
|
1164
|
+
import { Command as Command5 } from "commander";
|
|
1165
|
+
import path4 from "path";
|
|
1166
|
+
import pc5 from "picocolors";
|
|
1167
|
+
import ora2 from "ora";
|
|
1168
|
+
function createPullCommand(configManager, apiClient, codeGenerator) {
|
|
1169
|
+
const projectTracker = new ProjectTracker(configManager);
|
|
1170
|
+
return new Command5("pull").description("Generate tool code for tracked agents").option("-p, --project <id>", "Pull specific project only").option("-a, --agent <id>", "Pull specific agent only").option("-e, --env <environment>", "Target environment (production|staging|development)").option("-o, --output <path>", "Override output directory").option("--force", "Overwrite implementation files (WARNING: may lose code)").action(async (options) => {
|
|
1171
|
+
const config = await configManager.get();
|
|
1172
|
+
if (!config.auth?.token) {
|
|
1173
|
+
console.log(pc5.red("Not logged in. Run `porchestra login` first."));
|
|
1174
|
+
process.exit(1);
|
|
1175
|
+
}
|
|
1176
|
+
let trackedProjects = await projectTracker.getTrackedProjects();
|
|
1177
|
+
if (trackedProjects.length === 0) {
|
|
1178
|
+
console.log(pc5.yellow("\n\u26A0 No projects selected for tracking.\n"));
|
|
1179
|
+
console.log(pc5.gray("Starting project explorer...\n"));
|
|
1180
|
+
const { createExploreCommand: createExploreCommand2 } = await Promise.resolve().then(() => (init_explore(), explore_exports));
|
|
1181
|
+
const exploreCmd = createExploreCommand2(configManager, apiClient);
|
|
1182
|
+
await exploreCmd.parseAsync(["node", "script", "explore"]);
|
|
1183
|
+
trackedProjects = await projectTracker.getTrackedProjects();
|
|
1184
|
+
if (trackedProjects.length === 0) {
|
|
1185
|
+
console.log(pc5.red("\n\u274C No projects selected. Run `porchestra explore` to select projects."));
|
|
1186
|
+
process.exit(1);
|
|
1187
|
+
}
|
|
1188
|
+
}
|
|
1189
|
+
if (options.project) {
|
|
1190
|
+
trackedProjects = trackedProjects.filter(
|
|
1191
|
+
(p) => p.projectId === options.project || p.projectSlug === options.project
|
|
1192
|
+
);
|
|
1193
|
+
if (trackedProjects.length === 0) {
|
|
1194
|
+
console.log(pc5.red(`Project "${options.project}" not found in tracked projects`));
|
|
1195
|
+
process.exit(1);
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1198
|
+
const baseOutputDir = options.output || config.output?.baseDir || path4.resolve(process.cwd(), "src/porchestra/agents");
|
|
1199
|
+
let totalAgents = 0;
|
|
1200
|
+
let successCount = 0;
|
|
1201
|
+
for (const project of trackedProjects) {
|
|
1202
|
+
let agents = project.agents;
|
|
1203
|
+
if (options.agent) {
|
|
1204
|
+
agents = agents.filter(
|
|
1205
|
+
(a) => a.agentId === options.agent || a.agentSlug === options.agent
|
|
1206
|
+
);
|
|
1207
|
+
}
|
|
1208
|
+
if (agents.length === 0) continue;
|
|
1209
|
+
console.log(pc5.bold(`
|
|
1210
|
+
\u{1F4E6} ${project.projectName}
|
|
1211
|
+
`));
|
|
1212
|
+
totalAgents += agents.length;
|
|
1213
|
+
for (let i = 0; i < agents.length; i++) {
|
|
1214
|
+
const agent = agents[i];
|
|
1215
|
+
const progress = `[${i + 1}/${agents.length}]`;
|
|
1216
|
+
const spinner = ora2(`${progress} Fetching ${agent.agentName}...`).start();
|
|
1217
|
+
try {
|
|
1218
|
+
const toolsResponse = await apiClient.getAgentTools(
|
|
1219
|
+
project.projectId,
|
|
1220
|
+
agent.agentId,
|
|
1221
|
+
options.env
|
|
1222
|
+
);
|
|
1223
|
+
spinner.text = `${progress} Generating code...`;
|
|
1224
|
+
const normalizeToolset = (toolset = []) => toolset.map((t, idx) => ({
|
|
1225
|
+
id: t.id ?? `toolset-${idx}`,
|
|
1226
|
+
name: t.name,
|
|
1227
|
+
description: t.description ?? "",
|
|
1228
|
+
parameters: t.parameters ?? {},
|
|
1229
|
+
returns: t.returns ?? null,
|
|
1230
|
+
isBuiltin: t.isBuiltin ?? false,
|
|
1231
|
+
builtinType: t.builtinType
|
|
1232
|
+
}));
|
|
1233
|
+
const rawToolConfig = toolsResponse?.components?.toolConfig?.content;
|
|
1234
|
+
const parsedToolConfig = (() => {
|
|
1235
|
+
if (typeof rawToolConfig === "string") {
|
|
1236
|
+
try {
|
|
1237
|
+
return JSON.parse(rawToolConfig);
|
|
1238
|
+
} catch {
|
|
1239
|
+
return void 0;
|
|
1240
|
+
}
|
|
1241
|
+
}
|
|
1242
|
+
return rawToolConfig;
|
|
1243
|
+
})();
|
|
1244
|
+
const findToolArray = (value) => {
|
|
1245
|
+
if (Array.isArray(value)) {
|
|
1246
|
+
const hasNamedObjects = value.every((v) => v && typeof v === "object" && "name" in v);
|
|
1247
|
+
return hasNamedObjects ? value : void 0;
|
|
1248
|
+
}
|
|
1249
|
+
if (value && typeof value === "object") {
|
|
1250
|
+
if (Array.isArray(value.toolset)) return value.toolset;
|
|
1251
|
+
if (Array.isArray(value.tools)) return value.tools;
|
|
1252
|
+
for (const key of Object.keys(value)) {
|
|
1253
|
+
const found = findToolArray(value[key]);
|
|
1254
|
+
if (found) return found;
|
|
1255
|
+
}
|
|
1256
|
+
}
|
|
1257
|
+
return void 0;
|
|
1258
|
+
};
|
|
1259
|
+
const toolsetFromConfig = findToolArray(parsedToolConfig) ?? [];
|
|
1260
|
+
const resolvedTools = toolsResponse?.tools && toolsResponse.tools.length > 0 ? toolsResponse.tools : Array.isArray(toolsResponse?.toolset) ? normalizeToolset(toolsResponse.toolset) : normalizeToolset(toolsetFromConfig);
|
|
1261
|
+
if (!toolsResponse?.agent || !toolsResponse?.components) {
|
|
1262
|
+
throw new Error("Invalid tools response from API");
|
|
1263
|
+
}
|
|
1264
|
+
const outputDir = codeGenerator.calculateOutputPath(
|
|
1265
|
+
baseOutputDir,
|
|
1266
|
+
agent.folderPath,
|
|
1267
|
+
toolsResponse.agent.name
|
|
1268
|
+
);
|
|
1269
|
+
await codeGenerator.generate({
|
|
1270
|
+
agent: toolsResponse.agent,
|
|
1271
|
+
tools: resolvedTools,
|
|
1272
|
+
components: toolsResponse.components,
|
|
1273
|
+
outputDir,
|
|
1274
|
+
forceOverwrite: options.force || false
|
|
1275
|
+
});
|
|
1276
|
+
spinner.succeed(`${progress} ${agent.agentName} \u2192 ${pc5.gray(outputDir)}`);
|
|
1277
|
+
successCount++;
|
|
1278
|
+
await projectTracker.updateLastPulled(
|
|
1279
|
+
project.projectId,
|
|
1280
|
+
agent.agentId,
|
|
1281
|
+
toolsResponse.agent.version
|
|
1282
|
+
);
|
|
1283
|
+
} catch (error) {
|
|
1284
|
+
spinner.fail(`${progress} ${agent.agentName}`);
|
|
1285
|
+
console.error(pc5.red(` \u2717 ${error.message}`));
|
|
1286
|
+
}
|
|
1287
|
+
}
|
|
1288
|
+
}
|
|
1289
|
+
console.log(pc5.bold("\n\u2728 Pull Complete\n"));
|
|
1290
|
+
if (successCount === totalAgents) {
|
|
1291
|
+
console.log(` ${pc5.green("\u2713")} Generated: ${successCount} / ${totalAgents} agents`);
|
|
1292
|
+
} else if (successCount > 0) {
|
|
1293
|
+
console.log(` ${pc5.yellow("\u26A0")} Generated: ${successCount} / ${totalAgents} agents`);
|
|
1294
|
+
console.log(` ${pc5.red("\u2717")} Failed: ${totalAgents - successCount} agents`);
|
|
1295
|
+
} else {
|
|
1296
|
+
console.log(` ${pc5.red("\u2717")} Failed: All ${totalAgents} agents`);
|
|
1297
|
+
}
|
|
1298
|
+
console.log(` Output: ${pc5.gray(baseOutputDir)}`);
|
|
1299
|
+
if (successCount > 0) {
|
|
1300
|
+
console.log(pc5.gray("\nNext steps:"));
|
|
1301
|
+
console.log(pc5.gray(" 1. Implement tool functions in tool-impl.ts files"));
|
|
1302
|
+
console.log(pc5.gray(" 2. Import and use the tool dispatcher in your code"));
|
|
1303
|
+
}
|
|
1304
|
+
console.log();
|
|
1305
|
+
if (successCount < totalAgents) {
|
|
1306
|
+
process.exit(1);
|
|
1307
|
+
}
|
|
1308
|
+
});
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1311
|
+
// src/commands/config.ts
|
|
1312
|
+
import { Command as Command6 } from "commander";
|
|
1313
|
+
import pc6 from "picocolors";
|
|
1314
|
+
import { confirm as confirm3 } from "@inquirer/prompts";
|
|
1315
|
+
function createConfigCommand(configManager) {
|
|
1316
|
+
const config = new Command6("config").description("Manage CLI configuration");
|
|
1317
|
+
config.command("get <key>").description("Get a configuration value").action(async (key) => {
|
|
1318
|
+
const cfg = await configManager.get();
|
|
1319
|
+
const value = getNestedValue(cfg, key);
|
|
1320
|
+
console.log(value !== void 0 ? value : pc6.gray("(not set)"));
|
|
1321
|
+
});
|
|
1322
|
+
config.command("set <key> <value>").description("Set a configuration value").action(async (key, value) => {
|
|
1323
|
+
await configManager.update((cfg) => {
|
|
1324
|
+
return setNestedValue(cfg, key, parseValue(value));
|
|
1325
|
+
});
|
|
1326
|
+
console.log(pc6.green(`\u2713 Set ${key} = ${value}`));
|
|
1327
|
+
});
|
|
1328
|
+
config.command("list").description("List all configuration values").action(async () => {
|
|
1329
|
+
const cfg = await configManager.get();
|
|
1330
|
+
console.log(pc6.bold("\nConfiguration:"));
|
|
1331
|
+
printConfig(cfg);
|
|
1332
|
+
});
|
|
1333
|
+
config.command("reset").description("Reset all configuration (WARNING: clears auth)").option("--force", "Skip confirmation").action(async (options) => {
|
|
1334
|
+
if (!options.force) {
|
|
1335
|
+
const confirmed = await confirm3({
|
|
1336
|
+
message: "This will clear all config including login. Continue?",
|
|
1337
|
+
default: false
|
|
1338
|
+
});
|
|
1339
|
+
if (!confirmed) return;
|
|
1340
|
+
}
|
|
1341
|
+
await configManager.clear();
|
|
1342
|
+
console.log(pc6.green("\u2713 Configuration reset"));
|
|
1343
|
+
});
|
|
1344
|
+
return config;
|
|
1345
|
+
}
|
|
1346
|
+
function getNestedValue(obj, key) {
|
|
1347
|
+
return key.split(".").reduce((o, k) => o?.[k], obj);
|
|
1348
|
+
}
|
|
1349
|
+
function setNestedValue(obj, key, value) {
|
|
1350
|
+
const keys = key.split(".");
|
|
1351
|
+
const last = keys.pop();
|
|
1352
|
+
const target = keys.reduce((o, k) => {
|
|
1353
|
+
if (!o[k]) o[k] = {};
|
|
1354
|
+
return o[k];
|
|
1355
|
+
}, obj);
|
|
1356
|
+
target[last] = value;
|
|
1357
|
+
return obj;
|
|
1358
|
+
}
|
|
1359
|
+
function parseValue(value) {
|
|
1360
|
+
if (value === "true") return true;
|
|
1361
|
+
if (value === "false") return false;
|
|
1362
|
+
if (value === "null") return null;
|
|
1363
|
+
if (!isNaN(Number(value))) return Number(value);
|
|
1364
|
+
try {
|
|
1365
|
+
return JSON.parse(value);
|
|
1366
|
+
} catch {
|
|
1367
|
+
return value;
|
|
1368
|
+
}
|
|
1369
|
+
}
|
|
1370
|
+
function printConfig(obj, prefix = "") {
|
|
1371
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
1372
|
+
const fullKey = prefix ? `${prefix}.${key}` : key;
|
|
1373
|
+
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
1374
|
+
console.log(`
|
|
1375
|
+
${pc6.cyan(fullKey)}:`);
|
|
1376
|
+
printConfig(value, fullKey);
|
|
1377
|
+
} else {
|
|
1378
|
+
const displayValue = key.includes("token") ? pc6.gray("***hidden***") : value;
|
|
1379
|
+
console.log(` ${fullKey} = ${displayValue}`);
|
|
1380
|
+
}
|
|
1381
|
+
}
|
|
1382
|
+
}
|
|
1383
|
+
|
|
1384
|
+
// src/commands/status.ts
|
|
1385
|
+
init_project_tracker();
|
|
1386
|
+
init_date();
|
|
1387
|
+
import { Command as Command7 } from "commander";
|
|
1388
|
+
import pc7 from "picocolors";
|
|
1389
|
+
function createStatusCommand(configManager) {
|
|
1390
|
+
const projectTracker = new ProjectTracker(configManager);
|
|
1391
|
+
return new Command7("status").description("Show CLI status and tracked projects").action(async () => {
|
|
1392
|
+
const config = await configManager.get();
|
|
1393
|
+
console.log(pc7.bold("\n\u{1F4CA} Porchestra CLI Status\n"));
|
|
1394
|
+
if (config.auth?.token) {
|
|
1395
|
+
const expiresAt = new Date(config.auth.expiresAt);
|
|
1396
|
+
const daysUntilExpiry = Math.ceil(
|
|
1397
|
+
(expiresAt.getTime() - Date.now()) / (1e3 * 60 * 60 * 24)
|
|
1398
|
+
);
|
|
1399
|
+
const expiryColor = daysUntilExpiry < 7 ? pc7.yellow : pc7.green;
|
|
1400
|
+
console.log(pc7.bold("\u{1F511} Authentication"));
|
|
1401
|
+
console.log(` Status: ${pc7.green("\u2713 Logged in")}`);
|
|
1402
|
+
console.log(` Device: ${config.auth.deviceName}`);
|
|
1403
|
+
console.log(` Expires: ${expiryColor(daysUntilExpiry + " days")} (${config.auth.expiresAt})`);
|
|
1404
|
+
} else {
|
|
1405
|
+
console.log(pc7.bold("\u{1F511} Authentication"));
|
|
1406
|
+
console.log(` Status: ${pc7.yellow("\u2717 Not logged in")}`);
|
|
1407
|
+
console.log(pc7.gray(" Run `porchestra login` to authenticate"));
|
|
1408
|
+
}
|
|
1409
|
+
console.log(pc7.bold("\n\u{1F517} API Configuration"));
|
|
1410
|
+
console.log(` URL: ${config.api?.baseUrl || pc7.gray("(default)")}`);
|
|
1411
|
+
if (config.api?.skipTlsVerify) {
|
|
1412
|
+
console.log(pc7.yellow(` \u26A0\uFE0F TLS verification disabled`));
|
|
1413
|
+
}
|
|
1414
|
+
const projects = await projectTracker.getTrackedProjects();
|
|
1415
|
+
console.log(pc7.bold("\n\u{1F4C1} Tracked Projects"));
|
|
1416
|
+
if (projects.length === 0) {
|
|
1417
|
+
console.log(pc7.gray(" No projects tracked"));
|
|
1418
|
+
console.log(pc7.gray(" Run `porchestra explore` to select projects"));
|
|
1419
|
+
} else {
|
|
1420
|
+
console.log(` Projects: ${projects.length}`);
|
|
1421
|
+
console.log(` Agents: ${projects.reduce((sum, p) => sum + p.agents.length, 0)}`);
|
|
1422
|
+
projects.forEach((project) => {
|
|
1423
|
+
const lastPulled = project.lastPulledAt ? formatDistanceToNow(new Date(project.lastPulledAt)) : pc7.gray("never");
|
|
1424
|
+
console.log(pc7.gray(`
|
|
1425
|
+
${project.projectName}`));
|
|
1426
|
+
project.agents.forEach((agent) => {
|
|
1427
|
+
const agentPulled = agent.lastPulledAt ? formatDistanceToNow(new Date(agent.lastPulledAt)) : pc7.gray("pending");
|
|
1428
|
+
console.log(pc7.gray(` \u2514\u2500 ${agent.agentName} ${pc7.cyan(agent.folderPath)} (${agentPulled})`));
|
|
1429
|
+
});
|
|
1430
|
+
console.log(pc7.gray(` Last pulled: ${lastPulled}`));
|
|
1431
|
+
});
|
|
1432
|
+
}
|
|
1433
|
+
console.log(pc7.bold("\n\u{1F4C2} Output Configuration"));
|
|
1434
|
+
console.log(` Base dir: ${config.output?.baseDir || "./src/agents"}`);
|
|
1435
|
+
console.log(` Index files: ${config.output?.createIndexFiles ? "yes" : "no"}`);
|
|
1436
|
+
console.log();
|
|
1437
|
+
});
|
|
1438
|
+
}
|
|
1439
|
+
|
|
1440
|
+
// src/commands/agents.ts
|
|
1441
|
+
init_project_tracker();
|
|
1442
|
+
init_date();
|
|
1443
|
+
import { Command as Command8 } from "commander";
|
|
1444
|
+
import { select as select2, confirm as confirm4 } from "@inquirer/prompts";
|
|
1445
|
+
import pc8 from "picocolors";
|
|
1446
|
+
function createAgentsCommand(configManager) {
|
|
1447
|
+
const projectTracker = new ProjectTracker(configManager);
|
|
1448
|
+
const listAction = async (options = {}) => {
|
|
1449
|
+
let projects = await projectTracker.getTrackedProjects();
|
|
1450
|
+
if (options.project) {
|
|
1451
|
+
projects = projects.filter(
|
|
1452
|
+
(project) => project.projectId === options.project || project.projectSlug === options.project
|
|
1453
|
+
);
|
|
1454
|
+
if (projects.length === 0) {
|
|
1455
|
+
console.log(pc8.red(`No tracked project found for "${options.project}"`));
|
|
1456
|
+
return;
|
|
1457
|
+
}
|
|
1458
|
+
}
|
|
1459
|
+
if (options.json) {
|
|
1460
|
+
console.log(JSON.stringify(projects, null, 2));
|
|
1461
|
+
return;
|
|
1462
|
+
}
|
|
1463
|
+
console.log(pc8.bold("\n\u{1F4C1} Tracked Agents\n"));
|
|
1464
|
+
if (projects.length === 0) {
|
|
1465
|
+
console.log(pc8.gray(" No agents tracked. Run `porchestra explore` to add agents."));
|
|
1466
|
+
console.log();
|
|
1467
|
+
return;
|
|
1468
|
+
}
|
|
1469
|
+
let totalAgents = 0;
|
|
1470
|
+
projects.forEach((project) => {
|
|
1471
|
+
totalAgents += project.agents.length;
|
|
1472
|
+
console.log(pc8.bold(` ${project.projectName}`));
|
|
1473
|
+
project.agents.forEach((agent) => {
|
|
1474
|
+
const lastPulled = agent.lastPulledAt ? formatDistanceToNow(new Date(agent.lastPulledAt)) : "pending";
|
|
1475
|
+
const version = agent.lastPulledVersion ? ` @ ${agent.lastPulledVersion}` : "";
|
|
1476
|
+
console.log(` \u2514\u2500 ${pc8.cyan(agent.agentName)}${version ? pc8.gray(version) : ""}`);
|
|
1477
|
+
console.log(pc8.gray(` ${agent.folderPath} (${lastPulled})`));
|
|
1478
|
+
});
|
|
1479
|
+
console.log();
|
|
1480
|
+
});
|
|
1481
|
+
console.log(pc8.gray(`Total: ${projects.length} project(s), ${totalAgents} agent(s)
|
|
1482
|
+
`));
|
|
1483
|
+
};
|
|
1484
|
+
const command = new Command8("agents").description("Manage tracked agents").action(async () => listAction());
|
|
1485
|
+
command.command("list").description("List tracked agents").option("-p, --project <id>", "Filter by project id or slug").option("--json", "Output raw JSON for scripting").action(listAction);
|
|
1486
|
+
command.command("remove [agent]").description("Stop tracking a specific agent").option("-p, --project <id>", "Project id or slug (disambiguates agents with the same slug)").option("-f, --force", "Skip confirmation prompt").action(async (agentArg, options) => {
|
|
1487
|
+
let projects = await projectTracker.getTrackedProjects();
|
|
1488
|
+
if (projects.length === 0) {
|
|
1489
|
+
console.log(pc8.gray("No agents tracked. Run `porchestra explore` first."));
|
|
1490
|
+
return;
|
|
1491
|
+
}
|
|
1492
|
+
if (options.project) {
|
|
1493
|
+
projects = projects.filter(
|
|
1494
|
+
(project) => project.projectId === options.project || project.projectSlug === options.project
|
|
1495
|
+
);
|
|
1496
|
+
if (projects.length === 0) {
|
|
1497
|
+
console.log(pc8.red(`No tracked project found for "${options.project}"`));
|
|
1498
|
+
return;
|
|
1499
|
+
}
|
|
1500
|
+
}
|
|
1501
|
+
const candidates = projects.flatMap(
|
|
1502
|
+
(project) => project.agents.map((agent) => ({
|
|
1503
|
+
project,
|
|
1504
|
+
agent,
|
|
1505
|
+
value: `${project.projectId}:${agent.agentId}`
|
|
1506
|
+
}))
|
|
1507
|
+
);
|
|
1508
|
+
let filtered = candidates;
|
|
1509
|
+
if (agentArg) {
|
|
1510
|
+
filtered = candidates.filter(
|
|
1511
|
+
({ agent }) => agent.agentId === agentArg || agent.agentSlug === agentArg
|
|
1512
|
+
);
|
|
1513
|
+
}
|
|
1514
|
+
if (filtered.length === 0) {
|
|
1515
|
+
const message = agentArg ? `No tracked agent found for "${agentArg}".` : "No tracked agents found.";
|
|
1516
|
+
console.log(pc8.red(message));
|
|
1517
|
+
return;
|
|
1518
|
+
}
|
|
1519
|
+
let target = filtered[0];
|
|
1520
|
+
if (filtered.length > 1) {
|
|
1521
|
+
const choice = await select2({
|
|
1522
|
+
message: "Select an agent to stop tracking:",
|
|
1523
|
+
choices: filtered.map(({ project, agent, value }) => ({
|
|
1524
|
+
name: `${agent.agentName} ${pc8.gray(`(${project.projectName}) ${agent.folderPath}`)}`,
|
|
1525
|
+
value
|
|
1526
|
+
}))
|
|
1527
|
+
});
|
|
1528
|
+
target = filtered.find((candidate) => candidate.value === choice);
|
|
1529
|
+
}
|
|
1530
|
+
if (!options.force) {
|
|
1531
|
+
const confirmed = await confirm4({
|
|
1532
|
+
message: `Remove ${target.agent.agentName} from ${target.project.projectName}?`,
|
|
1533
|
+
default: false
|
|
1534
|
+
});
|
|
1535
|
+
if (!confirmed) return;
|
|
1536
|
+
}
|
|
1537
|
+
const removed = await projectTracker.untrackAgent(
|
|
1538
|
+
target.project.projectId,
|
|
1539
|
+
target.agent.agentId
|
|
1540
|
+
);
|
|
1541
|
+
if (!removed) {
|
|
1542
|
+
console.log(pc8.red("Agent was not removed (not found)."));
|
|
1543
|
+
return;
|
|
1544
|
+
}
|
|
1545
|
+
const projectRemoved = target.project.agents.length === 1;
|
|
1546
|
+
console.log(pc8.green(`\u2713 Stopped tracking ${target.agent.agentName}`));
|
|
1547
|
+
console.log(pc8.gray(` Project: ${target.project.projectName}`));
|
|
1548
|
+
if (projectRemoved) {
|
|
1549
|
+
console.log(pc8.gray(" Project removed from tracking because it has no remaining agents."));
|
|
1550
|
+
}
|
|
1551
|
+
});
|
|
1552
|
+
return command;
|
|
1553
|
+
}
|
|
1554
|
+
|
|
1555
|
+
// src/index.ts
|
|
1556
|
+
var packageJson = { version: "1.0.0" };
|
|
1557
|
+
process.on("unhandledRejection", (error) => {
|
|
1558
|
+
console.error(pc9.red("\n\u2717 Unexpected error:"));
|
|
1559
|
+
console.error(error);
|
|
1560
|
+
process.exit(1);
|
|
1561
|
+
});
|
|
1562
|
+
process.on("uncaughtException", (error) => {
|
|
1563
|
+
console.error(pc9.red("\n\u2717 Fatal error:"));
|
|
1564
|
+
console.error(error);
|
|
1565
|
+
process.exit(1);
|
|
1566
|
+
});
|
|
1567
|
+
async function main() {
|
|
1568
|
+
const configManager = new ConfigManager();
|
|
1569
|
+
const authService = new AuthService(configManager);
|
|
1570
|
+
new TokenManager(configManager);
|
|
1571
|
+
const apiClient = new ApiClient(configManager);
|
|
1572
|
+
const codeGenerator = new CodeGenerator();
|
|
1573
|
+
await authService.checkAndRefreshTokenIfNeeded();
|
|
1574
|
+
const program = new Command9().name("porchestra").description("CLI for Porchestra - Generate LLM tool handlers").version(packageJson.version).configureOutput({
|
|
1575
|
+
writeErr: (str) => process.stderr.write(str),
|
|
1576
|
+
outputError: (str, write) => write(pc9.red(str))
|
|
1577
|
+
});
|
|
1578
|
+
program.option("--api-url <url>", "Override API URL").option("--config-dir <dir>", "Override config directory");
|
|
1579
|
+
program.addCommand(createLoginCommand(configManager, authService));
|
|
1580
|
+
program.addCommand(createLogoutCommand(configManager, authService));
|
|
1581
|
+
program.addCommand(createWhoamiCommand(configManager, authService));
|
|
1582
|
+
program.addCommand(createExploreCommand(configManager, apiClient));
|
|
1583
|
+
program.addCommand(createPullCommand(configManager, apiClient, codeGenerator));
|
|
1584
|
+
program.addCommand(createConfigCommand(configManager));
|
|
1585
|
+
program.addCommand(createStatusCommand(configManager));
|
|
1586
|
+
program.addCommand(createAgentsCommand(configManager));
|
|
1587
|
+
await program.parseAsync(process.argv);
|
|
1588
|
+
}
|
|
1589
|
+
main().catch((error) => {
|
|
1590
|
+
if (error instanceof PorchestraError) {
|
|
1591
|
+
console.error(pc9.red(`
|
|
1592
|
+
\u2717 ${error.message}`));
|
|
1593
|
+
if (process.env.DEBUG) {
|
|
1594
|
+
console.error(pc9.gray(error.stack));
|
|
1595
|
+
}
|
|
1596
|
+
} else {
|
|
1597
|
+
console.error(pc9.red(`
|
|
1598
|
+
\u2717 Unexpected error: ${error.message}`));
|
|
1599
|
+
}
|
|
1600
|
+
process.exit(1);
|
|
1601
|
+
});
|
|
1602
|
+
//# sourceMappingURL=index.js.map
|