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.
Files changed (73) hide show
  1. package/AGENTS.md +56 -0
  2. package/CONTRIBUTING.md +35 -0
  3. package/README.md +38 -58
  4. package/SECURITY.md +24 -0
  5. package/bin/orkestrate.ts +2 -0
  6. package/docs/concepts.md +119 -0
  7. package/docs/demo-extension-builder.md +82 -0
  8. package/docs/extensions/adapters.md +57 -0
  9. package/docs/extensions/architecture.md +49 -0
  10. package/docs/extensions/introduction.md +26 -0
  11. package/docs/getting-started.md +85 -0
  12. package/docs/hosted-registry.md +90 -0
  13. package/docs/pack-authoring.md +75 -0
  14. package/docs/roadmap.md +59 -0
  15. package/docs/troubleshooting.md +28 -0
  16. package/extensions/opencode-adapter/index.ts +106 -0
  17. package/extensions/opencode-adapter/orkestrate.extension.json +17 -0
  18. package/package.json +40 -33
  19. package/packs/coding/harnesses/opencode/agents/coding.md +8 -0
  20. package/packs/coding/harnesses/opencode/opencode.json +24 -0
  21. package/packs/coding/harnesses/opencode/skills/orkestrate/SKILL.md +57 -0
  22. package/packs/coding/pack.yaml +5 -0
  23. package/packs/extension-builder/harnesses/opencode/agents/extension-builder.md +8 -0
  24. package/packs/extension-builder/harnesses/opencode/opencode.json +31 -0
  25. package/packs/extension-builder/harnesses/opencode/skills/orkestrate/SKILL.md +54 -0
  26. package/packs/extension-builder/harnesses/opencode/skills/orkestrate-pack-author/SKILL.md +59 -0
  27. package/packs/extension-builder/pack.yaml +5 -0
  28. package/src/cli/cmd/extension-submit.ts +267 -0
  29. package/src/cli/cmd/pack-create.ts +43 -0
  30. package/src/cli/cmd/pack.ts +53 -0
  31. package/src/cli/cmd/profile-create.ts +199 -0
  32. package/src/cli/cmd/profile-submit.ts +236 -0
  33. package/src/cli/cmd/profile-validate.ts +5 -0
  34. package/src/cli/cmd/registry.ts +66 -0
  35. package/src/cli/cmd/run.ts +37 -0
  36. package/src/cli/index.ts +163 -0
  37. package/src/cli/tui.ts +355 -0
  38. package/src/cli/ui/welcome.ts +73 -0
  39. package/src/cli.ts +1 -0
  40. package/src/sdk/cross-platform.ts +25 -0
  41. package/src/sdk/extensions/loader.ts +89 -0
  42. package/src/sdk/extensions/manifest.ts +193 -0
  43. package/src/sdk/extensions/types.ts +12 -0
  44. package/src/sdk/harness/sync-slice.ts +57 -0
  45. package/src/sdk/launch/broker.ts +87 -0
  46. package/src/sdk/launch/runner.ts +57 -0
  47. package/src/sdk/launch/terminal.ts +75 -0
  48. package/src/sdk/launch/types.ts +7 -0
  49. package/src/sdk/launch/windows.ts +109 -0
  50. package/src/sdk/packs/catalog.ts +172 -0
  51. package/src/sdk/packs/create.ts +99 -0
  52. package/src/sdk/packs/fs.ts +52 -0
  53. package/src/sdk/packs/github.ts +249 -0
  54. package/src/sdk/packs/paths.ts +19 -0
  55. package/src/sdk/packs/registry.ts +40 -0
  56. package/src/sdk/packs/schema.ts +51 -0
  57. package/src/sdk/packs/store.ts +172 -0
  58. package/src/sdk/profiles/catalog.ts +199 -0
  59. package/src/sdk/profiles/github.ts +177 -0
  60. package/src/sdk/profiles/install.ts +161 -0
  61. package/src/sdk/profiles/load.ts +209 -0
  62. package/src/sdk/profiles/materialize.ts +85 -0
  63. package/src/sdk/profiles/pack.ts +128 -0
  64. package/src/sdk/profiles/schema.ts +201 -0
  65. package/src/sdk/registry.ts +19 -0
  66. package/src/sdk/runs/registry.ts +142 -0
  67. package/src/sdk/runs/types.ts +15 -0
  68. package/src/sdk/types.ts +39 -0
  69. package/src/version.ts +3 -0
  70. package/dist/cli.js +0 -1668
  71. package/dist/cli.js.map +0 -1
  72. package/dist/mcp-entry.js +0 -181
  73. 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,5 @@
1
+ id: extension-builder
2
+ name: extension-builder
3
+ description: Build Orkestrate packs, drivers, and platform extensions.
4
+ version: "0.1.0"
5
+ harness: opencode
@@ -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
+ }