teamcopilot 0.0.3 → 0.1.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/dist/frontend/assets/{cssMode-BBJnYJ3k.js → cssMode-DPbQk08M.js} +1 -1
- package/dist/frontend/assets/{freemarker2-D_OsaDj3.js → freemarker2-DFE4Z2hp.js} +1 -1
- package/dist/frontend/assets/{handlebars-OZHda73C.js → handlebars-Dw_PK21Y.js} +1 -1
- package/dist/frontend/assets/{html-CdGHj-sb.js → html-DxZRNRpl.js} +1 -1
- package/dist/frontend/assets/{htmlMode-DmOnHDtb.js → htmlMode-iGgmnmtJ.js} +1 -1
- package/dist/frontend/assets/{index-DNOGp3R4.js → index-CphuwuEr.js} +4 -4
- package/dist/frontend/assets/index-Ds8n3J4W.css +1 -0
- package/dist/frontend/assets/{javascript--es-Ez5N.js → javascript-Bw_u6aiq.js} +1 -1
- package/dist/frontend/assets/{jsonMode-C3d51Wej.js → jsonMode-iBiD9w9m.js} +1 -1
- package/dist/frontend/assets/{liquid-D2JYL9PO.js → liquid-nnkE2sbw.js} +1 -1
- package/dist/frontend/assets/{mdx-Ch04DugU.js → mdx-CHi7ETpD.js} +1 -1
- package/dist/frontend/assets/{python-D-iNySlZ.js → python--OTnmJv2.js} +1 -1
- package/dist/frontend/assets/{razor-Bq45_Fpy.js → razor-C6OgnQD9.js} +1 -1
- package/dist/frontend/assets/{tsMode-CC8kGhhz.js → tsMode-DzsKqqZD.js} +1 -1
- package/dist/frontend/assets/{typescript-CtgyIznM.js → typescript-BMZ0H3W7.js} +1 -1
- package/dist/frontend/assets/{xml-D5joow3e.js → xml-HTDEz_LP.js} +1 -1
- package/dist/frontend/assets/{yaml-BsRI11pd.js → yaml-CVa5ypgE.js} +1 -1
- package/dist/frontend/index.html +2 -2
- package/dist/opencode-server.js +3 -0
- package/dist/utils/approval-snapshot-common.js +29 -16
- package/dist/utils/resource-files.js +22 -21
- package/dist/utils/skill.js +39 -9
- package/dist/utils/workspace-sync.js +5 -7
- package/dist/workspace_files/.opencode/opencode.json +4 -1
- package/dist/workspace_files/.opencode/package.json +2 -1
- package/dist/workspace_files/.opencode/plugins/createSkill.ts +38 -7
- package/dist/workspace_files/.opencode/plugins/createWorkflow.ts +34 -3
- package/dist/workspace_files/.opencode/plugins/findSimilarWorkflow.ts +33 -2
- package/dist/workspace_files/.opencode/plugins/findSkill.ts +36 -5
- package/dist/workspace_files/.opencode/plugins/getSkillContent.ts +34 -3
- package/dist/workspace_files/.opencode/plugins/honeytoken-protection.ts +1 -1
- package/dist/workspace_files/.opencode/plugins/listAvailableSkills.ts +33 -2
- package/dist/workspace_files/.opencode/plugins/listAvailableWorkflows.ts +33 -2
- package/dist/workspace_files/.opencode/plugins/runWorkflow.ts +34 -3
- package/dist/workspace_files/.opencode/plugins/skill-command-guard.ts +138 -0
- package/dist/workspace_files/AGENTS.md +3 -3
- package/dist/workspace_files/package-lock.json +2 -1
- package/dist/workspace_files/package.json +3 -1
- package/package.json +1 -1
- package/dist/frontend/assets/index-CnBE4Efj.css +0 -1
|
@@ -18,6 +18,14 @@ interface WorkflowSummary {
|
|
|
18
18
|
can_edit: boolean
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
+
interface SessionLookupResponse {
|
|
22
|
+
error?: unknown
|
|
23
|
+
data?: {
|
|
24
|
+
id?: string
|
|
25
|
+
parentID?: string
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
21
29
|
async function readErrorMessageFromResponse(
|
|
22
30
|
response: Response,
|
|
23
31
|
fallbackMessage: string
|
|
@@ -40,7 +48,29 @@ async function readErrorMessageFromResponse(
|
|
|
40
48
|
}
|
|
41
49
|
}
|
|
42
50
|
|
|
43
|
-
export const ListAvailableWorkflowsPlugin: Plugin = async (
|
|
51
|
+
export const ListAvailableWorkflowsPlugin: Plugin = async ({ client }) => {
|
|
52
|
+
async function resolveRootSessionID(sessionID: string): Promise<string> {
|
|
53
|
+
let currentSessionID = sessionID
|
|
54
|
+
|
|
55
|
+
while (true) {
|
|
56
|
+
const response = (await client.session.get({
|
|
57
|
+
path: {
|
|
58
|
+
id: currentSessionID,
|
|
59
|
+
},
|
|
60
|
+
})) as SessionLookupResponse
|
|
61
|
+
if (response.error) {
|
|
62
|
+
throw new Error(`Failed to resolve root session for ${currentSessionID}`)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const parentID = response.data?.parentID
|
|
66
|
+
if (!parentID) {
|
|
67
|
+
return currentSessionID
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
currentSessionID = parentID
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
44
74
|
return {
|
|
45
75
|
tool: {
|
|
46
76
|
listAvailableWorkflows: tool({
|
|
@@ -49,10 +79,11 @@ export const ListAvailableWorkflowsPlugin: Plugin = async (_ctx) => {
|
|
|
49
79
|
args: {},
|
|
50
80
|
async execute(_args, context) {
|
|
51
81
|
const { sessionID } = context
|
|
82
|
+
const authSessionID = await resolveRootSessionID(sessionID)
|
|
52
83
|
|
|
53
84
|
const response = await fetch(`${getApiBaseUrl()}/api/workflows`, {
|
|
54
85
|
headers: {
|
|
55
|
-
Authorization: `Bearer ${
|
|
86
|
+
Authorization: `Bearer ${authSessionID}`,
|
|
56
87
|
},
|
|
57
88
|
})
|
|
58
89
|
|
|
@@ -64,7 +64,37 @@ function sleep(ms: number): Promise<void> {
|
|
|
64
64
|
return new Promise((resolve) => setTimeout(resolve, ms))
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
-
|
|
67
|
+
interface SessionLookupResponse {
|
|
68
|
+
error?: unknown
|
|
69
|
+
data?: {
|
|
70
|
+
id?: string
|
|
71
|
+
parentID?: string
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export const RunWorkflowPlugin: Plugin = async ({ client }) => {
|
|
76
|
+
async function resolveRootSessionID(sessionID: string): Promise<string> {
|
|
77
|
+
let currentSessionID = sessionID
|
|
78
|
+
|
|
79
|
+
while (true) {
|
|
80
|
+
const response = (await client.session.get({
|
|
81
|
+
path: {
|
|
82
|
+
id: currentSessionID,
|
|
83
|
+
},
|
|
84
|
+
})) as SessionLookupResponse
|
|
85
|
+
if (response.error) {
|
|
86
|
+
throw new Error(`Failed to resolve root session for ${currentSessionID}`)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const parentID = response.data?.parentID
|
|
90
|
+
if (!parentID) {
|
|
91
|
+
return currentSessionID
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
currentSessionID = parentID
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
68
98
|
return {
|
|
69
99
|
tool: {
|
|
70
100
|
runWorkflow: tool({
|
|
@@ -86,6 +116,7 @@ export const RunWorkflowPlugin: Plugin = async (_ctx) => {
|
|
|
86
116
|
},
|
|
87
117
|
async execute(args, context) {
|
|
88
118
|
const { sessionID } = context
|
|
119
|
+
const authSessionID = await resolveRootSessionID(sessionID)
|
|
89
120
|
const { slug, inputs = {} } = args
|
|
90
121
|
const messageId = extractMessageId(context)
|
|
91
122
|
const callId = extractCallId(context)
|
|
@@ -102,7 +133,7 @@ export const RunWorkflowPlugin: Plugin = async (_ctx) => {
|
|
|
102
133
|
method: "POST",
|
|
103
134
|
headers: {
|
|
104
135
|
"Content-Type": "application/json",
|
|
105
|
-
Authorization: `Bearer ${
|
|
136
|
+
Authorization: `Bearer ${authSessionID}`,
|
|
106
137
|
},
|
|
107
138
|
body: JSON.stringify({
|
|
108
139
|
slug,
|
|
@@ -134,7 +165,7 @@ export const RunWorkflowPlugin: Plugin = async (_ctx) => {
|
|
|
134
165
|
{
|
|
135
166
|
method: "GET",
|
|
136
167
|
headers: {
|
|
137
|
-
Authorization: `Bearer ${
|
|
168
|
+
Authorization: `Bearer ${authSessionID}`,
|
|
138
169
|
},
|
|
139
170
|
}
|
|
140
171
|
)
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import type { Plugin } from "@opencode-ai/plugin"
|
|
2
|
+
|
|
3
|
+
function getApiBaseUrl(): string {
|
|
4
|
+
const port = process.env.TEAMCOPILOT_PORT?.trim()
|
|
5
|
+
if (!port) {
|
|
6
|
+
throw new Error("TEAMCOPILOT_PORT must be set.")
|
|
7
|
+
}
|
|
8
|
+
return `http://localhost:${port}`
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
interface SkillDetailsResponse {
|
|
12
|
+
skill?: {
|
|
13
|
+
is_approved?: boolean
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async function readErrorMessageFromResponse(
|
|
18
|
+
response: Response,
|
|
19
|
+
fallbackMessage: string
|
|
20
|
+
): Promise<string> {
|
|
21
|
+
try {
|
|
22
|
+
const text = await response.text()
|
|
23
|
+
if (!text) return fallbackMessage
|
|
24
|
+
try {
|
|
25
|
+
const parsed: unknown = JSON.parse(text)
|
|
26
|
+
if (parsed && typeof parsed === "object" && "message" in parsed) {
|
|
27
|
+
const msg = (parsed as { message?: unknown }).message
|
|
28
|
+
if (typeof msg === "string" && msg.trim().length > 0) return msg
|
|
29
|
+
}
|
|
30
|
+
} catch {
|
|
31
|
+
// fall back to plain text
|
|
32
|
+
}
|
|
33
|
+
return text.trim().length > 0 ? text : fallbackMessage
|
|
34
|
+
} catch {
|
|
35
|
+
return fallbackMessage
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function normalizeCommandSlug(command: unknown): string {
|
|
40
|
+
if (typeof command !== "string") {
|
|
41
|
+
return ""
|
|
42
|
+
}
|
|
43
|
+
const trimmed = command.trim()
|
|
44
|
+
const match = trimmed.match(/^\/+([^\s]+)/)
|
|
45
|
+
return match?.[1] ?? ""
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function getTaskCommand(args: unknown): string | null {
|
|
49
|
+
if (!args || typeof args !== "object" || !("command" in args)) {
|
|
50
|
+
return null
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const command = (args as { command?: unknown }).command
|
|
54
|
+
const normalized = normalizeCommandSlug(command)
|
|
55
|
+
return normalized.length > 0 ? normalized : null
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
interface SessionLookupResponse {
|
|
59
|
+
error?: unknown
|
|
60
|
+
data?: {
|
|
61
|
+
id?: string
|
|
62
|
+
parentID?: string
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export const SkillCommandGuard: Plugin = async ({ client }) => {
|
|
67
|
+
async function resolveRootSessionID(sessionID: string): Promise<string> {
|
|
68
|
+
let currentSessionID = sessionID
|
|
69
|
+
|
|
70
|
+
while (true) {
|
|
71
|
+
const response = (await client.session.get({
|
|
72
|
+
path: {
|
|
73
|
+
id: currentSessionID,
|
|
74
|
+
},
|
|
75
|
+
})) as SessionLookupResponse
|
|
76
|
+
if (response.error) {
|
|
77
|
+
throw new Error(`Failed to resolve root session for ${currentSessionID}`)
|
|
78
|
+
}
|
|
79
|
+
const parentID = response.data?.parentID
|
|
80
|
+
if (!parentID) {
|
|
81
|
+
return currentSessionID
|
|
82
|
+
}
|
|
83
|
+
currentSessionID = parentID
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async function assertAuthorizedSkillCommand(sessionID: string, slug: string): Promise<void> {
|
|
88
|
+
const rootSessionID = await resolveRootSessionID(sessionID)
|
|
89
|
+
const skillDetailsResponse = await fetch(
|
|
90
|
+
`${getApiBaseUrl()}/api/skills/${encodeURIComponent(slug)}`,
|
|
91
|
+
{
|
|
92
|
+
headers: {
|
|
93
|
+
Authorization: `Bearer ${rootSessionID}`,
|
|
94
|
+
},
|
|
95
|
+
}
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
if (skillDetailsResponse.status === 404) {
|
|
99
|
+
return
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (!skillDetailsResponse.ok) {
|
|
103
|
+
const errorMessage = await readErrorMessageFromResponse(
|
|
104
|
+
skillDetailsResponse,
|
|
105
|
+
`Failed to fetch skill metadata for ${slug} (HTTP ${skillDetailsResponse.status})`
|
|
106
|
+
)
|
|
107
|
+
throw new Error(errorMessage)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const skillDetailsPayload =
|
|
111
|
+
(await skillDetailsResponse.json()) as SkillDetailsResponse
|
|
112
|
+
const isApproved = skillDetailsPayload.skill?.is_approved === true
|
|
113
|
+
|
|
114
|
+
if (!isApproved) {
|
|
115
|
+
throw new Error(
|
|
116
|
+
`Skill "${slug}" is not approved yet. Only approved skills can be used.`
|
|
117
|
+
)
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return {
|
|
122
|
+
"tool.execute.before": async (input, output) => {
|
|
123
|
+
if (input.tool !== "task") {
|
|
124
|
+
return
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const sessionID = typeof input.sessionID === "string" ? input.sessionID.trim() : ""
|
|
128
|
+
const slug = getTaskCommand(output.args)
|
|
129
|
+
if (!sessionID || !slug) {
|
|
130
|
+
return
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
await assertAuthorizedSkillCommand(sessionID, slug)
|
|
134
|
+
},
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export default SkillCommandGuard
|
|
@@ -76,7 +76,7 @@ Before implementing custom instructions or creating new workflow logic, you MUST
|
|
|
76
76
|
|
|
77
77
|
### What is a Custom Skill?
|
|
78
78
|
|
|
79
|
-
A **custom skill** is a reusable instruction package for the agent that lives in
|
|
79
|
+
A **custom skill** is a reusable instruction package for the agent that lives in `.agents/skills/<slug>/`.
|
|
80
80
|
Each custom skill:
|
|
81
81
|
- Has a unique slug (lowercase, hyphenated, e.g., `triage-support-ticket`)
|
|
82
82
|
- Uses `SKILL.md` as the canonical manifest/instruction file
|
|
@@ -375,7 +375,7 @@ These rules exist to prevent data loss, secret leakage, and unsafe behavior. Vio
|
|
|
375
375
|
|
|
376
376
|
### Never delete custom skills
|
|
377
377
|
|
|
378
|
-
- You must **NEVER delete a custom skill** (folders or files under
|
|
378
|
+
- You must **NEVER delete a custom skill** (folders or files under `.agents/skills/<slug>/`).
|
|
379
379
|
- Custom skill deletions are only supposed to happen via the **UI**.
|
|
380
380
|
- If cleanup is requested, prefer **deprecating** or updating skill instructions rather than deleting anything.
|
|
381
381
|
|
|
@@ -398,7 +398,7 @@ These rules exist to prevent data loss, secret leakage, and unsafe behavior. Vio
|
|
|
398
398
|
|
|
399
399
|
- You MAY read files outside managed workspace areas when needed to understand user requirements, gather context, or answer questions.
|
|
400
400
|
- Keep any cloned repos, downloaded assets, fixtures, or vendored code **inside** `workflows/<slug>/` only.
|
|
401
|
-
- Keep skill artifacts and instruction files inside
|
|
401
|
+
- Keep skill artifacts and instruction files inside `.agents/skills/<slug>/` only.
|
|
402
402
|
|
|
403
403
|
### No destructive shell actions
|
|
404
404
|
|