teamcopilot 0.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/.env.example +10 -0
- package/LICENSE.md +21 -0
- package/README.md +131 -0
- package/bin/teamcopilot.js +281 -0
- package/dist/auth/index.js +189 -0
- package/dist/change-user-role.js +77 -0
- package/dist/chat/index.js +849 -0
- package/dist/constants.js +2 -0
- package/dist/create-user.js +98 -0
- package/dist/cronjob/index.js +16 -0
- package/dist/cronjob/resource-reconciliation.js +33 -0
- package/dist/delete-user.js +66 -0
- package/dist/frontend/assets/abap-CRCWOmpq.js +1 -0
- package/dist/frontend/assets/apex-DnsZk_dE.js +1 -0
- package/dist/frontend/assets/azcli-1IWB1ccx.js +1 -0
- package/dist/frontend/assets/bat-DPkNLes8.js +1 -0
- package/dist/frontend/assets/bicep-Corcdgou.js +2 -0
- package/dist/frontend/assets/cameligo-CGrWLZr3.js +1 -0
- package/dist/frontend/assets/clojure-D9WOWImG.js +1 -0
- package/dist/frontend/assets/codicon-DCmgc-ay.ttf +0 -0
- package/dist/frontend/assets/coffee-B7EJu28W.js +1 -0
- package/dist/frontend/assets/cpp-SEyurbux.js +1 -0
- package/dist/frontend/assets/csharp-BoL64M5l.js +1 -0
- package/dist/frontend/assets/csp-C46ZqvIl.js +1 -0
- package/dist/frontend/assets/css-DQU6DXDx.js +3 -0
- package/dist/frontend/assets/cssMode-BDT3WbVs.js +4 -0
- package/dist/frontend/assets/cypher-D84EuPTj.js +1 -0
- package/dist/frontend/assets/dart-D8lhlL1r.js +1 -0
- package/dist/frontend/assets/dockerfile-DLk6rpji.js +1 -0
- package/dist/frontend/assets/ecl-BO6FnfXk.js +1 -0
- package/dist/frontend/assets/editor.worker-B4pQIWZD.js +12 -0
- package/dist/frontend/assets/elixir-BRjLKONM.js +1 -0
- package/dist/frontend/assets/flow9-Cac8vKd7.js +1 -0
- package/dist/frontend/assets/freemarker2-C7-hEgID.js +3 -0
- package/dist/frontend/assets/fsharp-fd1GTHhf.js +1 -0
- package/dist/frontend/assets/go-O9LJTZXk.js +1 -0
- package/dist/frontend/assets/graphql-LQdxqEYJ.js +1 -0
- package/dist/frontend/assets/handlebars-4cwTkPir.js +1 -0
- package/dist/frontend/assets/hcl-DxDQ3s82.js +1 -0
- package/dist/frontend/assets/html-YNfE1Q0A.js +1 -0
- package/dist/frontend/assets/htmlMode-opTQ1HoB.js +4 -0
- package/dist/frontend/assets/index-DWyaVa1h.js +782 -0
- package/dist/frontend/assets/index-lXrsgeTF.css +1 -0
- package/dist/frontend/assets/ini-BvajGCUy.js +1 -0
- package/dist/frontend/assets/java-SYsfObOQ.js +1 -0
- package/dist/frontend/assets/javascript-BEwGzk7T.js +1 -0
- package/dist/frontend/assets/jsonMode-CGhIS5Al.js +10 -0
- package/dist/frontend/assets/julia-DQXNmw_w.js +1 -0
- package/dist/frontend/assets/kotlin-qQ0MG-9I.js +1 -0
- package/dist/frontend/assets/less-GGFNNJHn.js +2 -0
- package/dist/frontend/assets/lexon-Canl7DCW.js +1 -0
- package/dist/frontend/assets/liquid-QekTGCGJ.js +1 -0
- package/dist/frontend/assets/lua-D28Ae8-K.js +1 -0
- package/dist/frontend/assets/m3-DPitgjJI.js +1 -0
- package/dist/frontend/assets/markdown-B811l8j2.js +1 -0
- package/dist/frontend/assets/mdx-BAVDaB7v.js +1 -0
- package/dist/frontend/assets/mips-CdjsipkG.js +1 -0
- package/dist/frontend/assets/msdax-CYqgjx_P.js +1 -0
- package/dist/frontend/assets/mysql-BHd6q0vd.js +1 -0
- package/dist/frontend/assets/objective-c-B1aVtJYH.js +1 -0
- package/dist/frontend/assets/pascal-BhNW15KB.js +1 -0
- package/dist/frontend/assets/pascaligo-5jv8CcQD.js +1 -0
- package/dist/frontend/assets/perl-DlYyT36c.js +1 -0
- package/dist/frontend/assets/pgsql-Dy0bjov7.js +1 -0
- package/dist/frontend/assets/php-120yhfDK.js +1 -0
- package/dist/frontend/assets/pla-CjnFlu4u.js +1 -0
- package/dist/frontend/assets/postiats-CQpG440k.js +1 -0
- package/dist/frontend/assets/powerquery-DdJtto1Z.js +1 -0
- package/dist/frontend/assets/powershell-Bu_VLpJB.js +1 -0
- package/dist/frontend/assets/protobuf-IBS6jZEB.js +2 -0
- package/dist/frontend/assets/pug-kFxLfcjb.js +1 -0
- package/dist/frontend/assets/python-BQlHw7XO.js +1 -0
- package/dist/frontend/assets/qsharp-q7JyzKFN.js +1 -0
- package/dist/frontend/assets/r-BIFz-_sK.js +1 -0
- package/dist/frontend/assets/razor-Be3Wwc2E.js +1 -0
- package/dist/frontend/assets/redis-CHOsPHWR.js +1 -0
- package/dist/frontend/assets/redshift-CBifECDb.js +1 -0
- package/dist/frontend/assets/restructuredtext-CghPJEOS.js +1 -0
- package/dist/frontend/assets/ruby-CYWGW-b1.js +1 -0
- package/dist/frontend/assets/rust-DMDD0SHb.js +1 -0
- package/dist/frontend/assets/sb-BYAiYHFx.js +1 -0
- package/dist/frontend/assets/scala-Bqvq8jcR.js +1 -0
- package/dist/frontend/assets/scheme-Dhb-2j9p.js +1 -0
- package/dist/frontend/assets/scss-CTwUZ5N7.js +3 -0
- package/dist/frontend/assets/shell-CsDZo4DB.js +1 -0
- package/dist/frontend/assets/solidity-CME5AdoB.js +1 -0
- package/dist/frontend/assets/sophia-RYC1BQQz.js +1 -0
- package/dist/frontend/assets/sparql-KEyrF7De.js +1 -0
- package/dist/frontend/assets/sql-BdTr02Mf.js +1 -0
- package/dist/frontend/assets/st-C7iG7M4S.js +1 -0
- package/dist/frontend/assets/swift-D7IUmUK8.js +1 -0
- package/dist/frontend/assets/systemverilog-DgMryOEJ.js +1 -0
- package/dist/frontend/assets/tcl-PloMZuKG.js +1 -0
- package/dist/frontend/assets/tsMode-CIBFoN3z.js +11 -0
- package/dist/frontend/assets/twig-BfRIq3la.js +1 -0
- package/dist/frontend/assets/typescript-BuV9wEIE.js +1 -0
- package/dist/frontend/assets/typespec-CzxlYoT_.js +1 -0
- package/dist/frontend/assets/vb-BwAE3J76.js +1 -0
- package/dist/frontend/assets/wgsl-B_1kOXbF.js +298 -0
- package/dist/frontend/assets/xml-DcDKYaM4.js +1 -0
- package/dist/frontend/assets/yaml-CuBNmOuI.js +1 -0
- package/dist/frontend/index.html +14 -0
- package/dist/frontend/logo.svg +50 -0
- package/dist/index.js +169 -0
- package/dist/logging.js +30 -0
- package/dist/opencode-auth/index.js +122 -0
- package/dist/opencode-server.js +91 -0
- package/dist/prisma/client.js +38 -0
- package/dist/reset-password.js +73 -0
- package/dist/rotate-jwt-secret.js +20 -0
- package/dist/scripts/prisma-workspace.js +34 -0
- package/dist/skills/index.js +311 -0
- package/dist/types/permissions.js +2 -0
- package/dist/types/shared/permissions.js +17 -0
- package/dist/types/shared/skill.js +17 -0
- package/dist/types/shared/workflow-files.js +17 -0
- package/dist/types/shared/workflow.js +17 -0
- package/dist/types/skill.js +2 -0
- package/dist/types/workflow-files.js +2 -0
- package/dist/types/workflow.js +2 -0
- package/dist/users/index.js +22 -0
- package/dist/utils/approval-snapshot-common.js +596 -0
- package/dist/utils/assert.js +20 -0
- package/dist/utils/chat-session.js +44 -0
- package/dist/utils/cli-bootstrap.js +26 -0
- package/dist/utils/index.js +95 -0
- package/dist/utils/jwt-secret.js +63 -0
- package/dist/utils/opencode-auth.js +126 -0
- package/dist/utils/opencode-client.js +109 -0
- package/dist/utils/password-policy.js +12 -0
- package/dist/utils/permission-common.js +280 -0
- package/dist/utils/redact.js +108 -0
- package/dist/utils/resource-access.js +37 -0
- package/dist/utils/resource-file-routes.js +115 -0
- package/dist/utils/resource-files.js +572 -0
- package/dist/utils/runtime-paths.js +61 -0
- package/dist/utils/session-abort.js +52 -0
- package/dist/utils/skill-approval-snapshot.js +39 -0
- package/dist/utils/skill-files.js +17 -0
- package/dist/utils/skill-permissions.js +15 -0
- package/dist/utils/skill.js +217 -0
- package/dist/utils/user-role.js +14 -0
- package/dist/utils/workflow-approval-snapshot.js +38 -0
- package/dist/utils/workflow-files.js +17 -0
- package/dist/utils/workflow-interruption.js +50 -0
- package/dist/utils/workflow-permissions.js +27 -0
- package/dist/utils/workflow-runner.js +414 -0
- package/dist/utils/workflow.js +158 -0
- package/dist/utils/workspace-sync.js +204 -0
- package/dist/workflows/index.js +751 -0
- package/dist/workspace_files/.opencode/opencode.json +17 -0
- package/dist/workspace_files/.opencode/package.json +14 -0
- package/dist/workspace_files/.opencode/plugins/createSkill.ts +339 -0
- package/dist/workspace_files/.opencode/plugins/createWorkflow.ts +345 -0
- package/dist/workspace_files/.opencode/plugins/findSimilarWorkflow.ts +173 -0
- package/dist/workspace_files/.opencode/plugins/findSkill.ts +211 -0
- package/dist/workspace_files/.opencode/plugins/getSkillContent.ts +135 -0
- package/dist/workspace_files/.opencode/plugins/honeytoken-protection.ts +64 -0
- package/dist/workspace_files/.opencode/plugins/listAvailableSkills.ts +93 -0
- package/dist/workspace_files/.opencode/plugins/listAvailableWorkflows.ts +93 -0
- package/dist/workspace_files/.opencode/plugins/python-protection.ts +184 -0
- package/dist/workspace_files/.opencode/plugins/runWorkflow.ts +168 -0
- package/dist/workspace_files/.opencode/tsconfig.json +16 -0
- package/dist/workspace_files/AGENTS.md +483 -0
- package/dist/workspace_files/package-lock.json +167 -0
- package/dist/workspace_files/package.json +5 -0
- package/package.json +86 -0
- package/prisma/migrations/20260203040755_init/migration.sql +20 -0
- package/prisma/migrations/20260204034845_replace_google_auth_with_email_password/migration.sql +25 -0
- package/prisma/migrations/20260207022226_add_user_role/migration.sql +25 -0
- package/prisma/migrations/20260210161254_add_workflow_runs/migration.sql +16 -0
- package/prisma/migrations/20260211050606_adds_workflow_table/migration.sql +40 -0
- package/prisma/migrations/20260211050750_adds_fkey_constraint/migration.sql +21 -0
- package/prisma/migrations/20260211051912_removes_workflow_table/migration.sql +34 -0
- package/prisma/migrations/20260211052238_changes_workflow_id_to_slug/migration.sql +27 -0
- package/prisma/migrations/20260212051912_add_output_to_workflow_runs/migration.sql +2 -0
- package/prisma/migrations/20260213073006_add_chat_sessions/migration.sql +13 -0
- package/prisma/migrations/20260216053202_add_chat_sessions_opencode_session_id_idx/migration.sql +2 -0
- package/prisma/migrations/20260216053237_drop_redundant_chat_sessions_opencode_idx/migration.sql +2 -0
- package/prisma/migrations/20260219060705_makes/migration.sql +24 -0
- package/prisma/migrations/20260222040542_add_workflow_execution_permissions/migration.sql +18 -0
- package/prisma/migrations/20260222040815_remove_workflow_execution_permissions/migration.sql +10 -0
- package/prisma/migrations/20260222041348_add_workflow_execution_permissions_final/migration.sql +17 -0
- package/prisma/migrations/20260222041741_rename_to_tool_execution_permissions/migration.sql +30 -0
- package/prisma/migrations/20260222041826_simplify_tool_execution_permissions/migration.sql +29 -0
- package/prisma/migrations/20260222041950_add_fields_for_standalone_permissions/migration.sql +32 -0
- package/prisma/migrations/20260222042954_simplify_tool_permissions_table/migration.sql +27 -0
- package/prisma/migrations/20260223073902_add_workflow_run_permissions_tables/migration.sql +23 -0
- package/prisma/migrations/20260225025151_add_workflow_metadata/migration.sql +16 -0
- package/prisma/migrations/20260225031035_merge_workflow_permissions_into_metadata/migration.sql +44 -0
- package/prisma/migrations/20260225031752_removes_default_for_run_permission_mode/migration.sql +20 -0
- package/prisma/migrations/20260225033603_remove_workflow_metadata_user_fkeys/migration.sql +18 -0
- package/prisma/migrations/20260225043032_restore_workflow_metadata_user_fkeys/migration.sql +20 -0
- package/prisma/migrations/20260225091423_add_workflow_approved_snapshots/migration.sql +28 -0
- package/prisma/migrations/20260226032121_add_is_approved_to_workflow_metadata/migration.sql +21 -0
- package/prisma/migrations/20260226032444_undoes_last_db_change/migration.sql +26 -0
- package/prisma/migrations/20260227120000_remove_snapshot_hash_from_approved_snapshots/migration.sql +16 -0
- package/prisma/migrations/20260228071125_adds_workspace_path_to_snapshot_table/migration.sql +22 -0
- package/prisma/migrations/20260228071217_modifies_index_and_removes_default_value/migration.sql +22 -0
- package/prisma/migrations/20260228071710_undoes_previous/migration.sql +27 -0
- package/prisma/migrations/20260228105022_add_must_change_password_first_login/migration.sql +20 -0
- package/prisma/migrations/20260301115439_add_workflow_run_log_refs/migration.sql +8 -0
- package/prisma/migrations/20260301122557_add_workflow_aborted_sessions/migration.sql +5 -0
- package/prisma/migrations/20260302045545_move_workflow_run_log_refs_into_workflow_runs/migration.sql +17 -0
- package/prisma/migrations/20260303040318_add_skill_tables/migration.sql +61 -0
- package/prisma/migrations/20260303051533_unify_resource_permissions/migration.sql +97 -0
- package/prisma/migrations/20260303064255_unify_resource_metadata_and_snapshots/migration.sql +179 -0
- package/prisma/migrations/migration_lock.toml +3 -0
- package/prisma/schema.prisma +147 -0
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://opencode.ai/config.json",
|
|
3
|
+
"mcp": {
|
|
4
|
+
"brave-search": {
|
|
5
|
+
"type": "local",
|
|
6
|
+
"command": [
|
|
7
|
+
"npx",
|
|
8
|
+
"-y",
|
|
9
|
+
"@brave/brave-search-mcp-server"
|
|
10
|
+
],
|
|
11
|
+
"enabled": false,
|
|
12
|
+
"environment": {
|
|
13
|
+
"BRAVE_API_KEY": "YOUR_API_KEY_HERE"
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "flowpal-opencode-plugins",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "OpenCode plugins for FlowPal workflow management",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"dependencies": {
|
|
7
|
+
"@huggingface/transformers": "^3.0.0",
|
|
8
|
+
"@opencode-ai/plugin": "1.1.65"
|
|
9
|
+
},
|
|
10
|
+
"devDependencies": {
|
|
11
|
+
"@types/node": "^20.0.0",
|
|
12
|
+
"typescript": "^5.0.0"
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
import { type Plugin, tool } from "@opencode-ai/plugin"
|
|
2
|
+
import * as fs from "fs/promises"
|
|
3
|
+
import * as path from "path"
|
|
4
|
+
|
|
5
|
+
function getApiBaseUrl(): string {
|
|
6
|
+
const port = process.env.TEAMCOPILOT_PORT?.trim()
|
|
7
|
+
if (!port) {
|
|
8
|
+
throw new Error("TEAMCOPILOT_PORT must be set.")
|
|
9
|
+
}
|
|
10
|
+
return `http://localhost:${port}`
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
const SLUG_REGEX = /^[a-z0-9]+(?:-[a-z0-9]+)*$/
|
|
15
|
+
|
|
16
|
+
interface SkillFileContentResponse {
|
|
17
|
+
path: string
|
|
18
|
+
kind: "text" | "binary"
|
|
19
|
+
content?: string
|
|
20
|
+
etag: string
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface PermissionResponse {
|
|
24
|
+
approved: boolean
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async function readErrorMessageFromResponse(
|
|
28
|
+
response: Response,
|
|
29
|
+
fallbackMessage: string
|
|
30
|
+
): Promise<string> {
|
|
31
|
+
try {
|
|
32
|
+
const text = await response.text()
|
|
33
|
+
if (!text) return fallbackMessage
|
|
34
|
+
try {
|
|
35
|
+
const parsed: unknown = JSON.parse(text)
|
|
36
|
+
if (parsed && typeof parsed === "object" && "message" in parsed) {
|
|
37
|
+
const msg = (parsed as { message?: unknown }).message
|
|
38
|
+
if (typeof msg === "string" && msg.trim().length > 0) return msg
|
|
39
|
+
}
|
|
40
|
+
} catch {
|
|
41
|
+
// fall back to plain text
|
|
42
|
+
}
|
|
43
|
+
return text.trim().length > 0 ? text : fallbackMessage
|
|
44
|
+
} catch {
|
|
45
|
+
return fallbackMessage
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function extractMessageId(context: unknown): string | null {
|
|
50
|
+
if (!context || typeof context !== "object") {
|
|
51
|
+
return null
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const candidate = (context as { messageID?: unknown; messageId?: unknown; message_id?: unknown })
|
|
55
|
+
const raw = candidate.messageID ?? candidate.messageId ?? candidate.message_id
|
|
56
|
+
if (typeof raw === "string" && raw.trim().length > 0) {
|
|
57
|
+
return raw
|
|
58
|
+
}
|
|
59
|
+
if (typeof raw === "number" && Number.isFinite(raw)) {
|
|
60
|
+
return String(raw)
|
|
61
|
+
}
|
|
62
|
+
return null
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function extractCallId(context: unknown): string | null {
|
|
66
|
+
if (!context || typeof context !== "object") {
|
|
67
|
+
return null
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const candidate = (context as { callID?: unknown; callId?: unknown; call_id?: unknown })
|
|
71
|
+
const raw = candidate.callID ?? candidate.callId ?? candidate.call_id
|
|
72
|
+
if (typeof raw === "string" && raw.trim().length > 0) {
|
|
73
|
+
return raw
|
|
74
|
+
}
|
|
75
|
+
return null
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async function pathExists(p: string): Promise<boolean> {
|
|
79
|
+
try {
|
|
80
|
+
await fs.access(p)
|
|
81
|
+
return true
|
|
82
|
+
} catch {
|
|
83
|
+
return false
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function isPathInside(childPath: string, parentPath: string): boolean {
|
|
88
|
+
const parent = path.resolve(parentPath) + path.sep
|
|
89
|
+
const child = path.resolve(childPath) + path.sep
|
|
90
|
+
return child.startsWith(parent)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async function rejectCreationPermission(
|
|
94
|
+
sessionID: string,
|
|
95
|
+
permissionId: string
|
|
96
|
+
): Promise<void> {
|
|
97
|
+
await fetch(`${getApiBaseUrl()}/api/workflows/permission-reject/${permissionId}`, {
|
|
98
|
+
method: "POST",
|
|
99
|
+
headers: {
|
|
100
|
+
Authorization: `Bearer ${sessionID}`,
|
|
101
|
+
},
|
|
102
|
+
})
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async function requestCreationPermission(
|
|
106
|
+
sessionID: string,
|
|
107
|
+
messageID: string,
|
|
108
|
+
callID: string
|
|
109
|
+
): Promise<void> {
|
|
110
|
+
const response = await fetch(`${getApiBaseUrl()}/api/workflows/request-permission`, {
|
|
111
|
+
method: "POST",
|
|
112
|
+
headers: {
|
|
113
|
+
"Content-Type": "application/json",
|
|
114
|
+
Authorization: `Bearer ${sessionID}`,
|
|
115
|
+
},
|
|
116
|
+
body: JSON.stringify({
|
|
117
|
+
opencode_session_id: sessionID,
|
|
118
|
+
message_id: messageID,
|
|
119
|
+
call_id: callID,
|
|
120
|
+
}),
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
if (!response.ok) {
|
|
124
|
+
const message = await readErrorMessageFromResponse(
|
|
125
|
+
response,
|
|
126
|
+
`Failed to request permission (HTTP ${response.status})`
|
|
127
|
+
)
|
|
128
|
+
throw new Error(message)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const data = (await response.json()) as { permission_id: string }
|
|
132
|
+
const permissionId = data.permission_id
|
|
133
|
+
|
|
134
|
+
const maxAttempts = 300
|
|
135
|
+
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
136
|
+
await new Promise((resolve) => setTimeout(resolve, 1000))
|
|
137
|
+
|
|
138
|
+
const statusResponse = await fetch(
|
|
139
|
+
`${getApiBaseUrl()}/api/workflows/permission-status/${permissionId}`,
|
|
140
|
+
{
|
|
141
|
+
headers: {
|
|
142
|
+
Authorization: `Bearer ${sessionID}`,
|
|
143
|
+
},
|
|
144
|
+
}
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
if (!statusResponse.ok) {
|
|
148
|
+
continue
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const statusData = (await statusResponse.json()) as PermissionResponse & {
|
|
152
|
+
status: string
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (statusData.status === "approved") {
|
|
156
|
+
return
|
|
157
|
+
}
|
|
158
|
+
if (statusData.status === "rejected") {
|
|
159
|
+
throw new Error("User denied permission to create this skill.")
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
try {
|
|
164
|
+
await rejectCreationPermission(sessionID, permissionId)
|
|
165
|
+
} catch {
|
|
166
|
+
// Best-effort cleanup only; preserve the timeout error below.
|
|
167
|
+
}
|
|
168
|
+
throw new Error("Permission request timed out")
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function stripLeadingFrontmatter(markdown: string): string {
|
|
172
|
+
const trimmedStart = markdown.trimStart()
|
|
173
|
+
if (!trimmedStart.startsWith("---\n")) {
|
|
174
|
+
return markdown.trim()
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const frontmatterEnd = trimmedStart.indexOf("\n---\n", 4)
|
|
178
|
+
if (frontmatterEnd < 0) {
|
|
179
|
+
return markdown.trim()
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return trimmedStart.slice(frontmatterEnd + "\n---\n".length).trim()
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function buildSkillMarkdown(
|
|
186
|
+
slug: string,
|
|
187
|
+
description: string,
|
|
188
|
+
markdownContent: string
|
|
189
|
+
): string {
|
|
190
|
+
const body = stripLeadingFrontmatter(markdownContent)
|
|
191
|
+
return `---\nname: ${JSON.stringify(slug)}\ndescription: ${JSON.stringify(description)}\n---\n\n${body}\n`
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export const CreateSkillPlugin: Plugin = async (_ctx) => {
|
|
195
|
+
return {
|
|
196
|
+
tool: {
|
|
197
|
+
createSkill: tool({
|
|
198
|
+
description:
|
|
199
|
+
"Create a new custom skill. Creates the skill directory, updates database metadata, and writes SKILL.md with provided description and markdown content.",
|
|
200
|
+
args: {
|
|
201
|
+
slug: tool.schema
|
|
202
|
+
.string()
|
|
203
|
+
.describe("Skill slug (lowercase letters/numbers with hyphens)"),
|
|
204
|
+
description: tool.schema
|
|
205
|
+
.string()
|
|
206
|
+
.describe("Short description of what this skill does"),
|
|
207
|
+
content: tool.schema
|
|
208
|
+
.string()
|
|
209
|
+
.describe("Markdown content to store in SKILL.md"),
|
|
210
|
+
},
|
|
211
|
+
async execute(args, context) {
|
|
212
|
+
const { directory, sessionID } = context
|
|
213
|
+
const slug = args.slug.trim()
|
|
214
|
+
const description = args.description.trim()
|
|
215
|
+
const content = args.content.trim()
|
|
216
|
+
const messageId = extractMessageId(context)
|
|
217
|
+
const callId = extractCallId(context)
|
|
218
|
+
|
|
219
|
+
if (!SLUG_REGEX.test(slug)) {
|
|
220
|
+
throw new Error(
|
|
221
|
+
`Invalid slug format: "${slug}". Slug must be lowercase alphanumeric with hyphens (e.g., "my-skill-name").`
|
|
222
|
+
)
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
if (!description) {
|
|
226
|
+
throw new Error("description is required")
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if (!content) {
|
|
230
|
+
throw new Error("content is required")
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if (!messageId) {
|
|
234
|
+
throw new Error("Could not determine message id from tool context.")
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
if (!callId) {
|
|
238
|
+
throw new Error("Could not determine call id from tool context.")
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const skillsDir = path.join(directory, "custom-skills")
|
|
242
|
+
const skillDir = path.join(skillsDir, slug)
|
|
243
|
+
|
|
244
|
+
if (!isPathInside(skillDir, skillsDir)) {
|
|
245
|
+
throw new Error("Path traversal detected. Invalid slug.")
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
if (await pathExists(skillDir)) {
|
|
249
|
+
throw new Error(`Skill "${slug}" already exists at ${skillDir}`)
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
await requestCreationPermission(
|
|
253
|
+
sessionID,
|
|
254
|
+
messageId,
|
|
255
|
+
callId
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
const createResponse = await fetch(`${getApiBaseUrl()}/api/skills`, {
|
|
259
|
+
method: "POST",
|
|
260
|
+
headers: {
|
|
261
|
+
"Content-Type": "application/json",
|
|
262
|
+
Authorization: `Bearer ${sessionID}`,
|
|
263
|
+
},
|
|
264
|
+
body: JSON.stringify({
|
|
265
|
+
name: slug,
|
|
266
|
+
}),
|
|
267
|
+
})
|
|
268
|
+
|
|
269
|
+
if (!createResponse.ok) {
|
|
270
|
+
const errorMessage = await readErrorMessageFromResponse(
|
|
271
|
+
createResponse,
|
|
272
|
+
`Failed to create skill (HTTP ${createResponse.status})`
|
|
273
|
+
)
|
|
274
|
+
throw new Error(errorMessage)
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const readResponse = await fetch(
|
|
278
|
+
`${getApiBaseUrl()}/api/skills/${encodeURIComponent(slug)}/files/content?path=${encodeURIComponent("SKILL.md")}`,
|
|
279
|
+
{
|
|
280
|
+
headers: {
|
|
281
|
+
Authorization: `Bearer ${sessionID}`,
|
|
282
|
+
},
|
|
283
|
+
}
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
if (!readResponse.ok) {
|
|
287
|
+
const errorMessage = await readErrorMessageFromResponse(
|
|
288
|
+
readResponse,
|
|
289
|
+
`Failed to load SKILL.md for ${slug} (HTTP ${readResponse.status})`
|
|
290
|
+
)
|
|
291
|
+
throw new Error(errorMessage)
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const filePayload = (await readResponse.json()) as SkillFileContentResponse
|
|
295
|
+
if (filePayload.kind !== "text") {
|
|
296
|
+
throw new Error(`SKILL.md for ${slug} is not a text file.`)
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
const nextContent = buildSkillMarkdown(slug, description, content)
|
|
300
|
+
|
|
301
|
+
const saveResponse = await fetch(`${getApiBaseUrl()}/api/skills/${encodeURIComponent(slug)}/files/content`, {
|
|
302
|
+
method: "PUT",
|
|
303
|
+
headers: {
|
|
304
|
+
"Content-Type": "application/json",
|
|
305
|
+
Authorization: `Bearer ${sessionID}`,
|
|
306
|
+
},
|
|
307
|
+
body: JSON.stringify({
|
|
308
|
+
path: "SKILL.md",
|
|
309
|
+
content: nextContent,
|
|
310
|
+
base_etag: filePayload.etag,
|
|
311
|
+
}),
|
|
312
|
+
})
|
|
313
|
+
|
|
314
|
+
if (!saveResponse.ok) {
|
|
315
|
+
const errorMessage = await readErrorMessageFromResponse(
|
|
316
|
+
saveResponse,
|
|
317
|
+
`Failed to save SKILL.md for ${slug} (HTTP ${saveResponse.status})`
|
|
318
|
+
)
|
|
319
|
+
throw new Error(errorMessage)
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
return JSON.stringify(
|
|
323
|
+
{
|
|
324
|
+
skill: {
|
|
325
|
+
slug,
|
|
326
|
+
description,
|
|
327
|
+
file_path: `custom-skills/${slug}/SKILL.md`,
|
|
328
|
+
},
|
|
329
|
+
},
|
|
330
|
+
null,
|
|
331
|
+
2
|
|
332
|
+
)
|
|
333
|
+
},
|
|
334
|
+
}),
|
|
335
|
+
},
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
export default CreateSkillPlugin
|
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
import { type Plugin, tool } from "@opencode-ai/plugin"
|
|
2
|
+
import * as fs from "fs/promises"
|
|
3
|
+
import * as path from "path"
|
|
4
|
+
|
|
5
|
+
function getApiBaseUrl(): string {
|
|
6
|
+
const port = process.env.TEAMCOPILOT_PORT?.trim()
|
|
7
|
+
if (!port) {
|
|
8
|
+
throw new Error("TEAMCOPILOT_PORT must be set.")
|
|
9
|
+
}
|
|
10
|
+
return `http://localhost:${port}`
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
// ============================================================================
|
|
15
|
+
// Types
|
|
16
|
+
// ============================================================================
|
|
17
|
+
|
|
18
|
+
interface WorkflowInput {
|
|
19
|
+
type: "string" | "number" | "boolean"
|
|
20
|
+
required?: boolean
|
|
21
|
+
default?: string | number | boolean
|
|
22
|
+
description?: string
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
interface WorkflowManifest {
|
|
26
|
+
intent_summary: string
|
|
27
|
+
inputs?: Record<string, WorkflowInput>
|
|
28
|
+
triggers?: {
|
|
29
|
+
manual?: boolean
|
|
30
|
+
}
|
|
31
|
+
runtime?: {
|
|
32
|
+
timeout_seconds?: number
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
interface PermissionResponse {
|
|
37
|
+
approved: boolean
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// ============================================================================
|
|
41
|
+
// Constants
|
|
42
|
+
// ============================================================================
|
|
43
|
+
|
|
44
|
+
const SLUG_REGEX = /^[a-z0-9]+(?:-[a-z0-9]+)*$/
|
|
45
|
+
|
|
46
|
+
// ============================================================================
|
|
47
|
+
// Helper Functions
|
|
48
|
+
// ============================================================================
|
|
49
|
+
|
|
50
|
+
async function pathExists(p: string): Promise<boolean> {
|
|
51
|
+
try {
|
|
52
|
+
await fs.access(p)
|
|
53
|
+
return true
|
|
54
|
+
} catch {
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function isPathInside(childPath: string, parentPath: string): boolean {
|
|
60
|
+
const parent = path.resolve(parentPath) + path.sep
|
|
61
|
+
const child = path.resolve(childPath) + path.sep
|
|
62
|
+
return child.startsWith(parent)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async function readErrorMessageFromResponse(
|
|
66
|
+
response: Response,
|
|
67
|
+
fallbackMessage: string
|
|
68
|
+
): Promise<string> {
|
|
69
|
+
try {
|
|
70
|
+
const text = await response.text()
|
|
71
|
+
if (!text) return fallbackMessage
|
|
72
|
+
try {
|
|
73
|
+
const parsed: unknown = JSON.parse(text)
|
|
74
|
+
if (parsed && typeof parsed === "object" && "message" in parsed) {
|
|
75
|
+
const msg = (parsed as { message?: unknown }).message
|
|
76
|
+
if (typeof msg === "string" && msg.trim().length > 0) return msg
|
|
77
|
+
}
|
|
78
|
+
} catch {
|
|
79
|
+
// Non-JSON body, fall back to plain text below.
|
|
80
|
+
}
|
|
81
|
+
return text.trim().length > 0 ? text : fallbackMessage
|
|
82
|
+
} catch {
|
|
83
|
+
return fallbackMessage
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function extractMessageId(context: unknown): string | null {
|
|
88
|
+
if (!context || typeof context !== "object") {
|
|
89
|
+
return null
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const candidate = (context as { messageID?: unknown; messageId?: unknown; message_id?: unknown })
|
|
93
|
+
const raw = candidate.messageID ?? candidate.messageId ?? candidate.message_id
|
|
94
|
+
if (typeof raw === "string" && raw.trim().length > 0) {
|
|
95
|
+
return raw
|
|
96
|
+
}
|
|
97
|
+
if (typeof raw === "number" && Number.isFinite(raw)) {
|
|
98
|
+
return String(raw)
|
|
99
|
+
}
|
|
100
|
+
return null
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function extractCallId(context: unknown): string | null {
|
|
104
|
+
if (!context || typeof context !== "object") {
|
|
105
|
+
return null
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const candidate = (context as { callID?: unknown; callId?: unknown; call_id?: unknown })
|
|
109
|
+
const raw = candidate.callID ?? candidate.callId ?? candidate.call_id
|
|
110
|
+
if (typeof raw === "string" && raw.trim().length > 0) {
|
|
111
|
+
return raw
|
|
112
|
+
}
|
|
113
|
+
return null
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async function rejectWorkflowPermission(
|
|
117
|
+
sessionID: string,
|
|
118
|
+
permissionId: string
|
|
119
|
+
): Promise<void> {
|
|
120
|
+
await fetch(`${getApiBaseUrl()}/api/workflows/permission-reject/${permissionId}`, {
|
|
121
|
+
method: "POST",
|
|
122
|
+
headers: {
|
|
123
|
+
Authorization: `Bearer ${sessionID}`,
|
|
124
|
+
},
|
|
125
|
+
})
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async function requestWorkflowPermission(
|
|
129
|
+
sessionID: string,
|
|
130
|
+
messageID: string,
|
|
131
|
+
callID: string
|
|
132
|
+
): Promise<void> {
|
|
133
|
+
const response = await fetch(`${getApiBaseUrl()}/api/workflows/request-permission`, {
|
|
134
|
+
method: "POST",
|
|
135
|
+
headers: {
|
|
136
|
+
"Content-Type": "application/json",
|
|
137
|
+
Authorization: `Bearer ${sessionID}`,
|
|
138
|
+
},
|
|
139
|
+
body: JSON.stringify({
|
|
140
|
+
opencode_session_id: sessionID,
|
|
141
|
+
message_id: messageID,
|
|
142
|
+
call_id: callID,
|
|
143
|
+
}),
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
if (!response.ok) {
|
|
147
|
+
const message = await readErrorMessageFromResponse(
|
|
148
|
+
response,
|
|
149
|
+
`Failed to request permission (HTTP ${response.status})`
|
|
150
|
+
)
|
|
151
|
+
throw new Error(message)
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const data = (await response.json()) as { permission_id: string }
|
|
155
|
+
const permissionId = data.permission_id
|
|
156
|
+
|
|
157
|
+
const maxAttempts = 300
|
|
158
|
+
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
159
|
+
await new Promise((resolve) => setTimeout(resolve, 1000))
|
|
160
|
+
|
|
161
|
+
const statusResponse = await fetch(
|
|
162
|
+
`${getApiBaseUrl()}/api/workflows/permission-status/${permissionId}`,
|
|
163
|
+
{
|
|
164
|
+
headers: {
|
|
165
|
+
Authorization: `Bearer ${sessionID}`,
|
|
166
|
+
},
|
|
167
|
+
}
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
if (!statusResponse.ok) {
|
|
171
|
+
continue
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const statusData = (await statusResponse.json()) as PermissionResponse & {
|
|
175
|
+
status: string
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (statusData.status === "approved") {
|
|
179
|
+
return
|
|
180
|
+
}
|
|
181
|
+
if (statusData.status === "rejected") {
|
|
182
|
+
throw new Error("User denied permission to create this workflow.")
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
try {
|
|
187
|
+
await rejectWorkflowPermission(sessionID, permissionId)
|
|
188
|
+
} catch {
|
|
189
|
+
// Best-effort cleanup only; preserve the timeout error below.
|
|
190
|
+
}
|
|
191
|
+
throw new Error("Permission request timed out")
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// ============================================================================
|
|
195
|
+
// Plugin
|
|
196
|
+
// ============================================================================
|
|
197
|
+
|
|
198
|
+
export const CreateWorkflowPlugin: Plugin = async (_ctx) => {
|
|
199
|
+
return {
|
|
200
|
+
tool: {
|
|
201
|
+
createWorkflow: tool({
|
|
202
|
+
description:
|
|
203
|
+
"Create a new workflow with the specified slug and configuration. Creates the workflow folder with all required files (workflow.json, run.py, requirements.txt, etc.). The workflow will need admin approval before it can be executed.",
|
|
204
|
+
args: {
|
|
205
|
+
slug: tool.schema
|
|
206
|
+
.string()
|
|
207
|
+
.describe(
|
|
208
|
+
"The workflow slug (lowercase letters/numbers with hyphens, e.g., 'failed-stripe-payments')"
|
|
209
|
+
),
|
|
210
|
+
intent_summary: tool.schema
|
|
211
|
+
.string()
|
|
212
|
+
.describe(
|
|
213
|
+
"Human-readable description of what this workflow does"
|
|
214
|
+
),
|
|
215
|
+
inputs: tool.schema
|
|
216
|
+
.record(
|
|
217
|
+
tool.schema.string(),
|
|
218
|
+
tool.schema.object({
|
|
219
|
+
type: tool.schema.enum(["string", "number", "boolean"]),
|
|
220
|
+
required: tool.schema.boolean().optional(),
|
|
221
|
+
default: tool.schema.unknown().optional(),
|
|
222
|
+
description: tool.schema.string().optional(),
|
|
223
|
+
})
|
|
224
|
+
)
|
|
225
|
+
.optional()
|
|
226
|
+
.default({})
|
|
227
|
+
.describe(
|
|
228
|
+
"Input parameter schema defining the workflow's expected inputs"
|
|
229
|
+
),
|
|
230
|
+
timeout_seconds: tool.schema
|
|
231
|
+
.number()
|
|
232
|
+
.optional()
|
|
233
|
+
.default(300)
|
|
234
|
+
.describe(
|
|
235
|
+
"Maximum execution time in seconds (1-86400, default: 300)"
|
|
236
|
+
),
|
|
237
|
+
},
|
|
238
|
+
async execute(args, context) {
|
|
239
|
+
const { directory, sessionID } = context
|
|
240
|
+
const { slug, intent_summary, inputs = {}, timeout_seconds = 300 } = args
|
|
241
|
+
const messageId = extractMessageId(context)
|
|
242
|
+
const callId = extractCallId(context)
|
|
243
|
+
|
|
244
|
+
if (!messageId) {
|
|
245
|
+
throw new Error("Could not determine message id from tool context.")
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
if (!callId) {
|
|
249
|
+
throw new Error("Could not determine call id from tool context.")
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Validate slug format
|
|
253
|
+
if (!SLUG_REGEX.test(slug)) {
|
|
254
|
+
throw new Error(
|
|
255
|
+
`Invalid slug format: "${slug}". Slug must be lowercase, alphanumeric with hyphens (e.g., "my-workflow-name").`
|
|
256
|
+
)
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Validate timeout
|
|
260
|
+
if (timeout_seconds < 1 || timeout_seconds > 86400) {
|
|
261
|
+
throw new Error(
|
|
262
|
+
`Invalid timeout: ${timeout_seconds}. Must be between 1 and 86400 seconds.`
|
|
263
|
+
)
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Path traversal protection
|
|
267
|
+
const workflowsDir = path.join(directory, "workflows")
|
|
268
|
+
const workflowDir = path.join(workflowsDir, slug)
|
|
269
|
+
|
|
270
|
+
if (!isPathInside(workflowDir, workflowsDir)) {
|
|
271
|
+
throw new Error("Path traversal detected. Invalid slug.")
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Check if workflow already exists
|
|
275
|
+
if (await pathExists(workflowDir)) {
|
|
276
|
+
throw new Error(`Workflow "${slug}" already exists at ${workflowDir}`)
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Request permission after basic validation and existence checks pass.
|
|
280
|
+
await requestWorkflowPermission(
|
|
281
|
+
sessionID,
|
|
282
|
+
messageId,
|
|
283
|
+
callId
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
// Ensure workflows directory exists
|
|
287
|
+
await fs.mkdir(workflowsDir, { recursive: true })
|
|
288
|
+
|
|
289
|
+
// Create workflow directory
|
|
290
|
+
await fs.mkdir(workflowDir, { recursive: true })
|
|
291
|
+
|
|
292
|
+
// Create workflow.json
|
|
293
|
+
const workflowJson: WorkflowManifest = {
|
|
294
|
+
intent_summary,
|
|
295
|
+
inputs: inputs as Record<string, WorkflowInput>,
|
|
296
|
+
triggers: { manual: true },
|
|
297
|
+
runtime: { timeout_seconds },
|
|
298
|
+
}
|
|
299
|
+
await fs.writeFile(
|
|
300
|
+
path.join(workflowDir, "workflow.json"),
|
|
301
|
+
JSON.stringify(workflowJson, null, 2),
|
|
302
|
+
"utf-8"
|
|
303
|
+
)
|
|
304
|
+
|
|
305
|
+
// Create empty files
|
|
306
|
+
await fs.writeFile(path.join(workflowDir, "run.py"), "", "utf-8")
|
|
307
|
+
await fs.writeFile(path.join(workflowDir, "requirements.txt"), "", "utf-8")
|
|
308
|
+
await fs.writeFile(path.join(workflowDir, "requirements.lock.txt"), "", "utf-8")
|
|
309
|
+
await fs.writeFile(path.join(workflowDir, ".env"), "", "utf-8")
|
|
310
|
+
await fs.writeFile(path.join(workflowDir, ".env.example"), "", "utf-8")
|
|
311
|
+
await fs.writeFile(path.join(workflowDir, "README.md"), "", "utf-8")
|
|
312
|
+
|
|
313
|
+
const creatorResponse = await fetch(
|
|
314
|
+
`${getApiBaseUrl()}/api/workflows/${encodeURIComponent(slug)}/creator`,
|
|
315
|
+
{
|
|
316
|
+
method: "POST",
|
|
317
|
+
headers: {
|
|
318
|
+
"Content-Type": "application/json",
|
|
319
|
+
Authorization: `Bearer ${sessionID}`,
|
|
320
|
+
},
|
|
321
|
+
}
|
|
322
|
+
)
|
|
323
|
+
|
|
324
|
+
if (!creatorResponse.ok) {
|
|
325
|
+
const message = await readErrorMessageFromResponse(
|
|
326
|
+
creatorResponse,
|
|
327
|
+
`Failed to set workflow creator (HTTP ${creatorResponse.status})`
|
|
328
|
+
)
|
|
329
|
+
throw new Error(
|
|
330
|
+
`Workflow "${slug}" was created, but saving creator metadata failed: ${message}`
|
|
331
|
+
)
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
return JSON.stringify({
|
|
335
|
+
success: true,
|
|
336
|
+
message: `Workflow "${slug}" created successfully. Remember: this workflow needs admin approval before it can be executed.`,
|
|
337
|
+
workflow_path: path.relative(directory, workflowDir),
|
|
338
|
+
})
|
|
339
|
+
},
|
|
340
|
+
}),
|
|
341
|
+
},
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
export default CreateWorkflowPlugin
|