orkestrate 0.1.14 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +56 -0
- package/CONTRIBUTING.md +35 -0
- package/README.md +38 -58
- package/SECURITY.md +24 -0
- package/bin/orkestrate.ts +2 -0
- package/docs/concepts.md +119 -0
- package/docs/demo-extension-builder.md +82 -0
- package/docs/extensions/adapters.md +57 -0
- package/docs/extensions/architecture.md +49 -0
- package/docs/extensions/introduction.md +26 -0
- package/docs/getting-started.md +85 -0
- package/docs/hosted-registry.md +90 -0
- package/docs/pack-authoring.md +75 -0
- package/docs/roadmap.md +59 -0
- package/docs/troubleshooting.md +28 -0
- package/extensions/opencode-adapter/index.ts +106 -0
- package/extensions/opencode-adapter/orkestrate.extension.json +17 -0
- package/package.json +40 -33
- package/packs/coding/harnesses/opencode/agents/coding.md +8 -0
- package/packs/coding/harnesses/opencode/opencode.json +24 -0
- package/packs/coding/harnesses/opencode/skills/orkestrate/SKILL.md +57 -0
- package/packs/coding/pack.yaml +5 -0
- package/packs/extension-builder/harnesses/opencode/agents/extension-builder.md +8 -0
- package/packs/extension-builder/harnesses/opencode/opencode.json +31 -0
- package/packs/extension-builder/harnesses/opencode/skills/orkestrate/SKILL.md +54 -0
- package/packs/extension-builder/harnesses/opencode/skills/orkestrate-pack-author/SKILL.md +59 -0
- package/packs/extension-builder/pack.yaml +5 -0
- package/src/cli/cmd/extension-submit.ts +267 -0
- package/src/cli/cmd/pack-create.ts +43 -0
- package/src/cli/cmd/pack.ts +53 -0
- package/src/cli/cmd/profile-create.ts +199 -0
- package/src/cli/cmd/profile-submit.ts +236 -0
- package/src/cli/cmd/profile-validate.ts +5 -0
- package/src/cli/cmd/registry.ts +66 -0
- package/src/cli/cmd/run.ts +37 -0
- package/src/cli/index.ts +163 -0
- package/src/cli/tui.ts +355 -0
- package/src/cli/ui/welcome.ts +73 -0
- package/src/cli.ts +1 -0
- package/src/sdk/cross-platform.ts +25 -0
- package/src/sdk/extensions/loader.ts +89 -0
- package/src/sdk/extensions/manifest.ts +193 -0
- package/src/sdk/extensions/types.ts +12 -0
- package/src/sdk/harness/sync-slice.ts +57 -0
- package/src/sdk/launch/broker.ts +87 -0
- package/src/sdk/launch/runner.ts +57 -0
- package/src/sdk/launch/terminal.ts +75 -0
- package/src/sdk/launch/types.ts +7 -0
- package/src/sdk/launch/windows.ts +109 -0
- package/src/sdk/packs/catalog.ts +172 -0
- package/src/sdk/packs/create.ts +99 -0
- package/src/sdk/packs/fs.ts +52 -0
- package/src/sdk/packs/github.ts +249 -0
- package/src/sdk/packs/paths.ts +19 -0
- package/src/sdk/packs/registry.ts +40 -0
- package/src/sdk/packs/schema.ts +51 -0
- package/src/sdk/packs/store.ts +172 -0
- package/src/sdk/profiles/catalog.ts +199 -0
- package/src/sdk/profiles/github.ts +177 -0
- package/src/sdk/profiles/install.ts +161 -0
- package/src/sdk/profiles/load.ts +209 -0
- package/src/sdk/profiles/materialize.ts +85 -0
- package/src/sdk/profiles/pack.ts +128 -0
- package/src/sdk/profiles/schema.ts +201 -0
- package/src/sdk/registry.ts +19 -0
- package/src/sdk/runs/registry.ts +142 -0
- package/src/sdk/runs/types.ts +15 -0
- package/src/sdk/types.ts +39 -0
- package/src/version.ts +3 -0
- package/dist/cli.js +0 -1668
- package/dist/cli.js.map +0 -1
- package/dist/mcp-entry.js +0 -181
- package/dist/mcp-entry.js.map +0 -1
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: orkestrate-pack-author
|
|
3
|
+
description: >
|
|
4
|
+
Author Orkestrate packs — pack.yaml, harnesses/opencode native config, skills,
|
|
5
|
+
plugins, validate and install. Use when creating or editing agent packs for
|
|
6
|
+
Orkestrate, scaffolding pack layout, or preparing GitHub/registry publish.
|
|
7
|
+
For launching runs use orkestrate skill instead.
|
|
8
|
+
compatibility: opencode
|
|
9
|
+
metadata:
|
|
10
|
+
platform: orkestrate
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
# Orkestrate pack author
|
|
14
|
+
|
|
15
|
+
## Pack layout
|
|
16
|
+
|
|
17
|
+
```text
|
|
18
|
+
my-pack/
|
|
19
|
+
pack.yaml
|
|
20
|
+
harnesses/
|
|
21
|
+
opencode/
|
|
22
|
+
opencode.json
|
|
23
|
+
agents/<agent>.md
|
|
24
|
+
skills/<skill-name>/SKILL.md
|
|
25
|
+
plugins/
|
|
26
|
+
scaffold/
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## pack.yaml (required fields)
|
|
30
|
+
|
|
31
|
+
- `id` — lowercase slug (matches folder name)
|
|
32
|
+
- `name`, `description`, `harness` (e.g. `opencode`)
|
|
33
|
+
- `version` optional
|
|
34
|
+
|
|
35
|
+
## Harness slice (A)
|
|
36
|
+
|
|
37
|
+
All OpenCode behavior lives in **`harnesses/opencode/`**:
|
|
38
|
+
|
|
39
|
+
- `opencode.json` — permissions, agents, MCP, plugins
|
|
40
|
+
- `agents/*.md` — agent prompts (frontmatter + body)
|
|
41
|
+
- `skills/*/SKILL.md` — OpenCode skills with YAML frontmatter (`name`, `description`)
|
|
42
|
+
|
|
43
|
+
No Orkestrate tool DSL — native OpenCode config only.
|
|
44
|
+
|
|
45
|
+
## Workflow
|
|
46
|
+
|
|
47
|
+
1. Create directory under workspace or copy a seed pack
|
|
48
|
+
2. Edit `pack.yaml` and `harnesses/opencode/*`
|
|
49
|
+
3. `orkestrate pack validate <id>`
|
|
50
|
+
4. `orkestrate pack install <id>` if needed locally
|
|
51
|
+
5. `orkestrate run launch <id>` to test (new terminal)
|
|
52
|
+
|
|
53
|
+
## Orchestrator packs
|
|
54
|
+
|
|
55
|
+
Include skills `orkestrate` and `orkestrate-pack-author` in `permission.skill` in `opencode.json`.
|
|
56
|
+
|
|
57
|
+
## Publish
|
|
58
|
+
|
|
59
|
+
Push to public GitHub; registry stores index + `source_url` + ref + `pack_path` (later phase).
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
import { validateManifest, createManifestTemplate, type ExtensionManifest, type ExtensionType } from "../../sdk/extensions/manifest";
|
|
2
|
+
|
|
3
|
+
const GREEN = "\x1b[32m";
|
|
4
|
+
const RED = "\x1b[31m";
|
|
5
|
+
const YELLOW = "\x1b[33m";
|
|
6
|
+
const BOLD = "\x1b[1m";
|
|
7
|
+
const RESET = "\x1b[0m";
|
|
8
|
+
const BLUE = "\x1b[34m";
|
|
9
|
+
|
|
10
|
+
const REGISTRY_URL = process.env.ORKESTRATE_REGISTRY_URL || "https://orkestrate.space/api/registry";
|
|
11
|
+
|
|
12
|
+
export async function runExtensionSubmit(options: {
|
|
13
|
+
path?: string;
|
|
14
|
+
type?: ExtensionType;
|
|
15
|
+
init?: boolean;
|
|
16
|
+
outputPath?: string;
|
|
17
|
+
}): Promise<void> {
|
|
18
|
+
const { path, type, init, outputPath } = options;
|
|
19
|
+
|
|
20
|
+
if (init) {
|
|
21
|
+
if (!type) {
|
|
22
|
+
console.error(`${RED}Error:${RESET} --type required with --init`);
|
|
23
|
+
console.error(`Types: adapter, profile-pack, skill-pack, mcp-pack, command-pack`);
|
|
24
|
+
process.exitCode = 1;
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
await initManifest(type, outputPath ?? "orkestrate.extension.json");
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (!path) {
|
|
33
|
+
console.error(`${RED}Error:${RESET} Usage: orkestrate extension submit <path-to-manifest> [--init --type <type>]`);
|
|
34
|
+
process.exitCode = 1;
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
console.log(`${BOLD}Validating extension manifest...${RESET}`);
|
|
39
|
+
console.log("");
|
|
40
|
+
|
|
41
|
+
// Load manifest
|
|
42
|
+
let manifest: ExtensionManifest;
|
|
43
|
+
try {
|
|
44
|
+
const file = Bun.file(path);
|
|
45
|
+
if (!await file.exists()) {
|
|
46
|
+
console.error(`${RED}Error:${RESET} Manifest file not found: ${path}`);
|
|
47
|
+
process.exitCode = 1;
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
manifest = await file.json();
|
|
51
|
+
} catch (error) {
|
|
52
|
+
console.error(`${RED}Error:${RESET} Failed to load manifest:`, error instanceof Error ? error.message : String(error));
|
|
53
|
+
process.exitCode = 1;
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Validate
|
|
58
|
+
const { valid, errors } = validateManifest(manifest);
|
|
59
|
+
if (!valid) {
|
|
60
|
+
console.error(`${RED}Validation failed:${RESET}`);
|
|
61
|
+
for (const err of errors) {
|
|
62
|
+
console.error(` ${RED}✗${RESET} ${err}`);
|
|
63
|
+
}
|
|
64
|
+
process.exitCode = 1;
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
console.log(`${GREEN}✓${RESET} Manifest validation passed`);
|
|
69
|
+
console.log("");
|
|
70
|
+
|
|
71
|
+
// Show summary
|
|
72
|
+
console.log(`${BOLD}Extension:${RESET} ${manifest.name} (${manifest.id})`);
|
|
73
|
+
console.log(`${BOLD}Version:${RESET} ${manifest.version}`);
|
|
74
|
+
console.log(`${BOLD}Type:${RESET} ${manifest.type}`);
|
|
75
|
+
console.log(`${BOLD}Entry:${RESET} ${manifest.entry}`);
|
|
76
|
+
if (manifest.harness) console.log(`${BOLD}Harness:${RESET} ${manifest.harness}`);
|
|
77
|
+
if (manifest.profiles?.length) console.log(`${BOLD}Profiles:${RESET} ${manifest.profiles.join(", ")}`);
|
|
78
|
+
if (manifest.skills?.length) console.log(`${BOLD}Skills:${RESET} ${manifest.skills.join(", ")}`);
|
|
79
|
+
console.log("");
|
|
80
|
+
|
|
81
|
+
// Get auth token
|
|
82
|
+
const token = await getAuthToken();
|
|
83
|
+
if (!token) {
|
|
84
|
+
console.log(`${YELLOW}Authentication required${RESET}`);
|
|
85
|
+
const proceed = await confirm("Continue with GitHub authentication?");
|
|
86
|
+
if (!proceed) {
|
|
87
|
+
console.log("Cancelled.");
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
const authResult = await startAuthFlow();
|
|
91
|
+
if (!authResult) {
|
|
92
|
+
console.error(`${RED}Error:${RESET} Authentication failed`);
|
|
93
|
+
process.exitCode = 1;
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Submit
|
|
99
|
+
console.log(`${BLUE}Submitting extension to registry...${RESET}`);
|
|
100
|
+
try {
|
|
101
|
+
const response = await fetch(`${REGISTRY_URL}/extensions/submit`, {
|
|
102
|
+
method: "POST",
|
|
103
|
+
headers: {
|
|
104
|
+
"Content-Type": "application/json",
|
|
105
|
+
"Authorization": `Bearer ${token}`,
|
|
106
|
+
},
|
|
107
|
+
body: JSON.stringify({
|
|
108
|
+
manifest,
|
|
109
|
+
source: "cli",
|
|
110
|
+
}),
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
if (!response.ok) {
|
|
114
|
+
const error = await response.json().catch(() => ({ message: "Unknown error" }));
|
|
115
|
+
throw new Error(`Registry error: ${response.status} - ${error.message ?? "Unknown"}`);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const result = await response.json();
|
|
119
|
+
console.log(`${GREEN}✓${RESET} Extension submitted successfully!`);
|
|
120
|
+
console.log("");
|
|
121
|
+
console.log(`Submission ID: ${result.submission_id}`);
|
|
122
|
+
console.log(`Status: ${result.status}`);
|
|
123
|
+
if (result.review_url) console.log(`Review URL: ${result.review_url}`);
|
|
124
|
+
|
|
125
|
+
} catch (error) {
|
|
126
|
+
console.error(`${RED}Error:${RESET} Submission failed:`, error instanceof Error ? error.message : String(error));
|
|
127
|
+
console.log("");
|
|
128
|
+
console.log("Manual submission:");
|
|
129
|
+
console.log(` 1. Go to ${BLUE}https://orkestrate.space/submit${RESET}`);
|
|
130
|
+
console.log(" 2. Sign in with GitHub");
|
|
131
|
+
console.log(" 3. Select 'Extension' and upload manifest");
|
|
132
|
+
process.exitCode = 1;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
async function initManifest(type: ExtensionType, outputPath: string): Promise<void> {
|
|
137
|
+
const template = createManifestTemplate(type);
|
|
138
|
+
|
|
139
|
+
// Check if file exists
|
|
140
|
+
const file = Bun.file(outputPath);
|
|
141
|
+
if (await file.exists()) {
|
|
142
|
+
const overwrite = await confirm(`${YELLOW}File exists: ${outputPath}. Overwrite?${RESET}`);
|
|
143
|
+
if (!overwrite) {
|
|
144
|
+
console.log("Cancelled.");
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
await Bun.write(outputPath, JSON.stringify(template, null, 2) + "\n");
|
|
150
|
+
console.log(`${GREEN}✓${RESET} Created manifest template: ${outputPath}`);
|
|
151
|
+
console.log("");
|
|
152
|
+
console.log("Next steps:");
|
|
153
|
+
console.log(` 1. Edit ${outputPath} with your extension details`);
|
|
154
|
+
console.log(` 2. Run ${BOLD}orkestrate extension validate ${outputPath}${RESET} to verify`);
|
|
155
|
+
console.log(` 3. Run ${BOLD}orkestrate extension submit ${outputPath}${RESET} to publish`);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
async function getAuthToken(): Promise<string | null> {
|
|
159
|
+
const authPath = `${process.env.HOME || process.env.USERPROFILE}/.orkestrate/auth.json`;
|
|
160
|
+
try {
|
|
161
|
+
const file = Bun.file(authPath);
|
|
162
|
+
if (await file.exists()) {
|
|
163
|
+
const auth = await file.json();
|
|
164
|
+
if (auth.access_token && auth.expires_at && Date.now() < auth.expires_at) {
|
|
165
|
+
return auth.access_token;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
} catch {}
|
|
169
|
+
|
|
170
|
+
if (process.env.ORKESTRATE_AUTH_TOKEN) {
|
|
171
|
+
return process.env.ORKESTRATE_AUTH_TOKEN;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
async function startAuthFlow(): Promise<string | null> {
|
|
178
|
+
console.log(`${BLUE}Opening browser for GitHub authentication...${RESET}`);
|
|
179
|
+
|
|
180
|
+
const authUrl = `${REGISTRY_URL}/auth/device`;
|
|
181
|
+
try {
|
|
182
|
+
const response = await fetch(authUrl, { method: "POST" });
|
|
183
|
+
const data = await response.json();
|
|
184
|
+
|
|
185
|
+
if (data.device_code && data.verification_uri_complete) {
|
|
186
|
+
const { spawn } = await import("node:child_process");
|
|
187
|
+
const url = data.verification_uri_complete;
|
|
188
|
+
|
|
189
|
+
let opened = false;
|
|
190
|
+
if (process.platform === "darwin") {
|
|
191
|
+
spawn("open", [url]);
|
|
192
|
+
opened = true;
|
|
193
|
+
} else if (process.platform === "win32") {
|
|
194
|
+
spawn("cmd", ["/c", "start", url]);
|
|
195
|
+
opened = true;
|
|
196
|
+
} else if (process.platform === "linux") {
|
|
197
|
+
spawn("xdg-open", [url]);
|
|
198
|
+
opened = true;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (!opened) {
|
|
202
|
+
console.log(`Please open: ${BLUE}${url}${RESET}`);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
console.log(`Or visit: ${BLUE}${data.verification_uri}${RESET} and enter code: ${BOLD}${data.user_code}${RESET}`);
|
|
206
|
+
console.log("Waiting for authentication...");
|
|
207
|
+
|
|
208
|
+
return await pollForToken(data.device_code, data.interval ?? 5);
|
|
209
|
+
}
|
|
210
|
+
} catch (error) {
|
|
211
|
+
console.error("Auth flow error:", error);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return null;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
async function pollForToken(deviceCode: string, interval: number): Promise<string | null> {
|
|
218
|
+
const maxAttempts = 180 / interval;
|
|
219
|
+
|
|
220
|
+
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
221
|
+
await new Promise(r => setTimeout(r, interval * 1000));
|
|
222
|
+
|
|
223
|
+
try {
|
|
224
|
+
const response = await fetch(`${REGISTRY_URL}/auth/token`, {
|
|
225
|
+
method: "POST",
|
|
226
|
+
headers: { "Content-Type": "application/json" },
|
|
227
|
+
body: JSON.stringify({ device_code: deviceCode, grant_type: "urn:ietf:params:oauth:grant-type:device_code" }),
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
if (response.ok) {
|
|
231
|
+
const data = await response.json();
|
|
232
|
+
if (data.access_token) {
|
|
233
|
+
const authPath = `${process.env.HOME || process.env.USERPROFILE}/.orkestrate/auth.json`;
|
|
234
|
+
await Bun.write(authPath, JSON.stringify({
|
|
235
|
+
access_token: data.access_token,
|
|
236
|
+
refresh_token: data.refresh_token,
|
|
237
|
+
expires_at: Date.now() + (data.expires_in * 1000),
|
|
238
|
+
}, null, 2));
|
|
239
|
+
|
|
240
|
+
console.log(`${GREEN}✓${RESET} Authentication successful!`);
|
|
241
|
+
return data.access_token;
|
|
242
|
+
}
|
|
243
|
+
} else if (response.status === 400) {
|
|
244
|
+
const error = await response.json().catch(() => ({}));
|
|
245
|
+
if (error.error === "authorization_pending") continue;
|
|
246
|
+
if (error.error === "slow_down") { interval += 5; continue; }
|
|
247
|
+
if (error.error === "expired_token") { console.log("Expired."); return null; }
|
|
248
|
+
if (error.error === "access_denied") { console.log("Denied."); return null; }
|
|
249
|
+
}
|
|
250
|
+
} catch (error) {
|
|
251
|
+
console.error("Polling error:", error);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
console.log("Authentication timed out.");
|
|
256
|
+
return null;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function confirm(message: string): Promise<boolean> {
|
|
260
|
+
return new Promise(resolve => {
|
|
261
|
+
console.log(`${message} (y/N): `);
|
|
262
|
+
process.stdin.once("data", data => {
|
|
263
|
+
const input = data.toString().trim().toLowerCase();
|
|
264
|
+
resolve(input === "y" || input === "yes");
|
|
265
|
+
});
|
|
266
|
+
});
|
|
267
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { createPackFromTemplate } from "../../sdk/packs/create";
|
|
2
|
+
import { validatePackLayout } from "../../sdk/packs/store";
|
|
3
|
+
import { toPack, parsePackManifest } from "../../sdk/packs/schema";
|
|
4
|
+
import { findPackManifestInDir, parseManifestYaml } from "../../sdk/packs/fs";
|
|
5
|
+
import { PACK_MANIFEST } from "../../sdk/packs/paths";
|
|
6
|
+
|
|
7
|
+
export async function runPackCreate(
|
|
8
|
+
id: string,
|
|
9
|
+
options: { template?: string; description?: string; global?: boolean }
|
|
10
|
+
): Promise<void> {
|
|
11
|
+
const dest = await createPackFromTemplate({
|
|
12
|
+
id,
|
|
13
|
+
template: options.template,
|
|
14
|
+
description: options.description,
|
|
15
|
+
target: options.global ? "global" : "workspace",
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
const manifestPath = await findPackManifestInDir(dest);
|
|
19
|
+
if (!manifestPath) {
|
|
20
|
+
throw new Error(`Created pack is missing ${PACK_MANIFEST}`);
|
|
21
|
+
}
|
|
22
|
+
const raw = await Bun.file(manifestPath).text();
|
|
23
|
+
const manifest = parsePackManifest(parseManifestYaml(raw));
|
|
24
|
+
const pack = toPack(manifest, dest, manifestPath);
|
|
25
|
+
const { errors, warnings } = await validatePackLayout(pack);
|
|
26
|
+
|
|
27
|
+
console.log(`Created pack at:\n ${dest}\n`);
|
|
28
|
+
if (warnings.length) {
|
|
29
|
+
console.log("Warnings:");
|
|
30
|
+
for (const w of warnings) console.log(` ⚠ ${w}`);
|
|
31
|
+
}
|
|
32
|
+
if (errors.length) {
|
|
33
|
+
console.log("Fix these before launch:");
|
|
34
|
+
for (const e of errors) console.log(` ✗ ${e}`);
|
|
35
|
+
process.exitCode = 1;
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
console.log("Next:");
|
|
40
|
+
console.log(` orkestrate pack validate ${id}`);
|
|
41
|
+
console.log(` orkestrate run launch ${id}`);
|
|
42
|
+
console.log(` bun run dev # TUI → select ${id} → Enter`);
|
|
43
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { installCatalogPack, listBundledCatalog, listInstalledPacks } from "../../sdk/packs/catalog";
|
|
2
|
+
import { resolvePack, validatePackLayout } from "../../sdk/packs/store";
|
|
3
|
+
import { seedBundledToGlobalIfMissing } from "../../sdk/packs/store";
|
|
4
|
+
|
|
5
|
+
export async function runPackList(): Promise<void> {
|
|
6
|
+
await seedBundledToGlobalIfMissing();
|
|
7
|
+
const installed = await listInstalledPacks();
|
|
8
|
+
console.log("Installed packs:\n");
|
|
9
|
+
for (const { pack, scope } of installed) {
|
|
10
|
+
console.log(` ${pack.id} (${scope}) harness=${pack.harness}`);
|
|
11
|
+
console.log(` ${pack.description}`);
|
|
12
|
+
}
|
|
13
|
+
const bundled = await listBundledCatalog();
|
|
14
|
+
const notInstalled = bundled.filter((b) => !b.installed);
|
|
15
|
+
if (notInstalled.length > 0) {
|
|
16
|
+
console.log("\nBundled catalog (not installed):\n");
|
|
17
|
+
for (const entry of notInstalled) {
|
|
18
|
+
console.log(` ${entry.slug}`);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export async function runPackInstall(slug: string, global = false): Promise<void> {
|
|
24
|
+
const pack = await installCatalogPack(slug, { target: global ? "global" : "workspace" });
|
|
25
|
+
console.log(`Installed pack: ${pack.id}`);
|
|
26
|
+
console.log(` ${pack.packRoot}`);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export async function runPackValidate(packId: string): Promise<void> {
|
|
30
|
+
const pack = await resolvePack(packId);
|
|
31
|
+
const { errors, warnings } = await validatePackLayout(pack);
|
|
32
|
+
const adapter = (await import("../../sdk/registry")).getAdapter(pack.harness);
|
|
33
|
+
if (!adapter) {
|
|
34
|
+
errors.push(`No driver for harness "${pack.harness}"`);
|
|
35
|
+
} else {
|
|
36
|
+
const status = await adapter.detect();
|
|
37
|
+
if (!status.installed) {
|
|
38
|
+
errors.push(`Harness not installed: ${status.error ?? "unknown"}`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
if (warnings.length) {
|
|
42
|
+
console.log("Warnings:");
|
|
43
|
+
for (const w of warnings) console.log(` ⚠ ${w}`);
|
|
44
|
+
}
|
|
45
|
+
if (errors.length) {
|
|
46
|
+
console.error("Validation failed:");
|
|
47
|
+
for (const e of errors) console.error(` ✗ ${e}`);
|
|
48
|
+
process.exitCode = 1;
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
console.log(`Pack "${pack.id}" is valid.`);
|
|
52
|
+
console.log(` ${pack.packRoot}`);
|
|
53
|
+
}
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import { listProfiles, saveProfile, workspaceProfilesDir } from "../../sdk/profiles/load";
|
|
2
|
+
import { parseProfile, type Profile } from "../../sdk/profiles/schema";
|
|
3
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
|
|
6
|
+
const GREEN = "\x1b[32m";
|
|
7
|
+
const RED = "\x1b[31m";
|
|
8
|
+
const YELLOW = "\x1b[33m";
|
|
9
|
+
const BOLD = "\x1b[1m";
|
|
10
|
+
const RESET = "\x1b[0m";
|
|
11
|
+
|
|
12
|
+
const TEMPLATES: Record<string, Omit<Profile, "name">> = {
|
|
13
|
+
"extension-builder": {
|
|
14
|
+
description: "Specialized profile for building Orkestrate extensions and adapters",
|
|
15
|
+
harness: "opencode",
|
|
16
|
+
info: `I am a profile builder for the Orkestrate workbench. I help create new agent profiles, extensions, and harness adapters.
|
|
17
|
+
|
|
18
|
+
## Capabilities
|
|
19
|
+
- **Profile Authoring**: Design valid agent profiles with proper metadata and config
|
|
20
|
+
- **Extension Development**: Write OrkExtension modules with activation hooks
|
|
21
|
+
- **Adapter Creation**: Build HarnessAdapter implementations for new runtimes
|
|
22
|
+
- **Validation**: Verify profiles/extensions compile and conform to interfaces
|
|
23
|
+
|
|
24
|
+
## Key References
|
|
25
|
+
- \`src/sdk/profiles/schema.ts\` - Profile schema and validation
|
|
26
|
+
- \`src/sdk/extensions/types.ts\` - Extension interface
|
|
27
|
+
- \`src/sdk/types.ts\` - HarnessAdapter interface
|
|
28
|
+
- \`extensions/opencode-adapter/index.ts\` - Example adapter
|
|
29
|
+
|
|
30
|
+
## Workflow
|
|
31
|
+
1. Define profile purpose and target harness
|
|
32
|
+
2. Write \`info\` intro (this field) + \`config\` (model, tools, resources)
|
|
33
|
+
3. Run \`orkestrate profile validate <name>\` to verify
|
|
34
|
+
4. Test launch via \`orkestrate\` TUI
|
|
35
|
+
5. Submit to registry when ready`,
|
|
36
|
+
config: {
|
|
37
|
+
workspace: { root: ".", policy: "current-directory" },
|
|
38
|
+
model: { provider: "default", id: "default", thinking: "high", cycle: [] },
|
|
39
|
+
prompt: "Help create, validate, and package Orkestrate profiles and extensions.",
|
|
40
|
+
resources: { skills: ["orkestrate"], prompts: [], extensions: [], mcpServers: [] },
|
|
41
|
+
tools: { allow: ["read", "write", "edit", "bash", "grep", "glob"], deny: [] },
|
|
42
|
+
session: { dir: ".orkestrate/opencode-sessions/profile-builder" },
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
"coding": {
|
|
46
|
+
description: "General-purpose coding agent",
|
|
47
|
+
harness: "opencode",
|
|
48
|
+
info: `I am a general-purpose software development agent.
|
|
49
|
+
|
|
50
|
+
## Capabilities
|
|
51
|
+
- **Code Editing**: Read, write, edit files with full language support
|
|
52
|
+
- **Shell Access**: Run commands, tests, builds, git operations
|
|
53
|
+
- **Search & Navigate**: Grep, glob, LSP-powered code intelligence
|
|
54
|
+
- **Task Management**: Todo tracking for complex multi-step work
|
|
55
|
+
|
|
56
|
+
## Workflow
|
|
57
|
+
1. Understand the task and repository context
|
|
58
|
+
2. Make small, reviewable changes
|
|
59
|
+
3. Verify with tests and typechecks
|
|
60
|
+
4. Follow repository conventions
|
|
61
|
+
|
|
62
|
+
## Constraints
|
|
63
|
+
- Prefer minimal, focused changes
|
|
64
|
+
- Ask before destructive operations
|
|
65
|
+
- Respect existing code style`,
|
|
66
|
+
config: {
|
|
67
|
+
workspace: { root: ".", policy: "current-directory" },
|
|
68
|
+
model: { provider: "default", id: "default", thinking: "default", cycle: [] },
|
|
69
|
+
prompt: "Use the Orkestrate coding profile. Prioritize small, reviewable changes, clear verification, and repository instructions.",
|
|
70
|
+
resources: { skills: ["orkestrate"], prompts: [], extensions: [], mcpServers: [] },
|
|
71
|
+
tools: { allow: ["read", "grep", "glob", "bash", "edit", "write"], deny: [] },
|
|
72
|
+
session: { dir: ".orkestrate/opencode-sessions/coding" },
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
"research": {
|
|
76
|
+
description: "Research and analysis agent",
|
|
77
|
+
harness: "opencode",
|
|
78
|
+
info: `I am a research agent for deep analysis, literature review, and synthesis.
|
|
79
|
+
|
|
80
|
+
## Capabilities
|
|
81
|
+
- **Web Research**: Fetch and analyze online sources
|
|
82
|
+
- **Document Analysis**: Read and summarize PDFs, papers, docs
|
|
83
|
+
- **Synthesis**: Combine findings into structured reports
|
|
84
|
+
- **Fact Checking**: Verify claims against sources
|
|
85
|
+
|
|
86
|
+
## Workflow
|
|
87
|
+
1. Define research question and scope
|
|
88
|
+
2. Gather sources (web, local files, papers)
|
|
89
|
+
3. Analyze and extract key findings
|
|
90
|
+
4. Synthesize into structured output with citations
|
|
91
|
+
|
|
92
|
+
## Tools
|
|
93
|
+
- Web fetch/search for live data
|
|
94
|
+
- File reading for local documents
|
|
95
|
+
- Todo tracking for multi-phase research`,
|
|
96
|
+
config: {
|
|
97
|
+
workspace: { root: ".", policy: "current-directory" },
|
|
98
|
+
model: { provider: "default", id: "default", thinking: "high", cycle: [] },
|
|
99
|
+
prompt: "Conduct thorough research. Cite sources. Distinguish facts from speculation.",
|
|
100
|
+
resources: { skills: ["orkestrate"], prompts: [], extensions: [], mcpServers: [] },
|
|
101
|
+
tools: { allow: ["read", "write", "edit", "bash", "grep", "glob", "webfetch", "websearch"], deny: [] },
|
|
102
|
+
session: { dir: ".orkestrate/opencode-sessions/research" },
|
|
103
|
+
},
|
|
104
|
+
},
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
export async function runProfileCreate(options: {
|
|
108
|
+
name: string;
|
|
109
|
+
template?: string;
|
|
110
|
+
interactive?: boolean;
|
|
111
|
+
global?: boolean;
|
|
112
|
+
}): Promise<void> {
|
|
113
|
+
const { name, template, interactive, global } = options;
|
|
114
|
+
|
|
115
|
+
// Validate name
|
|
116
|
+
if (!/^[a-z0-9][a-z0-9-_]*$/.test(name)) {
|
|
117
|
+
console.error(`${RED}Error:${RESET} Profile name must use lowercase letters, numbers, "-", or "_" (e.g., "my-profile")`);
|
|
118
|
+
process.exitCode = 1;
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Check if profile already exists
|
|
123
|
+
const existing = await listProfiles({ warn: false });
|
|
124
|
+
if (existing.some(p => p.name === name)) {
|
|
125
|
+
console.error(`${RED}Error:${RESET} Profile "${name}" already exists`);
|
|
126
|
+
process.exitCode = 1;
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
let profileData: Profile;
|
|
131
|
+
|
|
132
|
+
if (template && TEMPLATES[template]) {
|
|
133
|
+
// Use template
|
|
134
|
+
profileData = { name, ...TEMPLATES[template] };
|
|
135
|
+
console.log(`${GREEN}✓${RESET} Created from template: ${template}`);
|
|
136
|
+
} else if (interactive) {
|
|
137
|
+
// Interactive mode - prompt for fields
|
|
138
|
+
profileData = await interactiveCreate(name);
|
|
139
|
+
} else {
|
|
140
|
+
// Minimal default
|
|
141
|
+
profileData = {
|
|
142
|
+
name,
|
|
143
|
+
description: `${name} profile`,
|
|
144
|
+
harness: "opencode",
|
|
145
|
+
info: `I am the ${name} profile. Describe my capabilities and purpose here.`,
|
|
146
|
+
config: {
|
|
147
|
+
workspace: { root: ".", policy: "current-directory" },
|
|
148
|
+
model: { provider: "default", id: "default", thinking: "default", cycle: [] },
|
|
149
|
+
prompt: `Use the ${name} profile.`,
|
|
150
|
+
resources: { skills: [], prompts: [], extensions: [], mcpServers: [] },
|
|
151
|
+
tools: { allow: [], deny: [] },
|
|
152
|
+
session: { dir: `.orkestrate/opencode-sessions/${name}` },
|
|
153
|
+
},
|
|
154
|
+
};
|
|
155
|
+
console.log(`${YELLOW}⚠${RESET} Created minimal profile. Edit with \`orkestrate\` TUI (press 'e') to customize.`);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Validate
|
|
159
|
+
try {
|
|
160
|
+
parseProfile(profileData);
|
|
161
|
+
} catch (error) {
|
|
162
|
+
console.error(`${RED}Error:${RESET} Generated profile invalid:`, error instanceof Error ? error.message : String(error));
|
|
163
|
+
process.exitCode = 1;
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Save
|
|
168
|
+
try {
|
|
169
|
+
await saveProfile(profileData, { isGlobal: global });
|
|
170
|
+
const location = global ? "global (~/.orkestrate/profiles)" : "workspace (.orkestrate/profiles)";
|
|
171
|
+
console.log(`${GREEN}✓${RESET} Profile saved to ${location}`);
|
|
172
|
+
console.log("");
|
|
173
|
+
console.log(`Next steps:`);
|
|
174
|
+
console.log(` orkestrate profile validate ${name} # Verify configuration`);
|
|
175
|
+
console.log(` orkestrate # Launch via TUI (select profile, press Enter)`);
|
|
176
|
+
} catch (error) {
|
|
177
|
+
console.error(`${RED}Error:${RESET} Failed to save profile:`, error instanceof Error ? error.message : String(error));
|
|
178
|
+
process.exitCode = 1;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
async function interactiveCreate(name: string): Promise<Profile> {
|
|
183
|
+
// For now, return minimal - full interactive would need inquirer or similar
|
|
184
|
+
console.log("Interactive mode not fully implemented yet. Use --template or edit after creation.");
|
|
185
|
+
return {
|
|
186
|
+
name,
|
|
187
|
+
description: `${name} profile`,
|
|
188
|
+
harness: "opencode",
|
|
189
|
+
info: `I am the ${name} profile. Describe my capabilities and purpose here.`,
|
|
190
|
+
config: {
|
|
191
|
+
workspace: { root: ".", policy: "current-directory" },
|
|
192
|
+
model: { provider: "default", id: "default", thinking: "default", cycle: [] },
|
|
193
|
+
prompt: `Use the ${name} profile.`,
|
|
194
|
+
resources: { skills: [], prompts: [], extensions: [], mcpServers: [] },
|
|
195
|
+
tools: { allow: [], deny: [] },
|
|
196
|
+
session: { dir: `.orkestrate/opencode-sessions/${name}` },
|
|
197
|
+
},
|
|
198
|
+
};
|
|
199
|
+
}
|