teamcopilot 0.0.2 → 0.1.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/README.md +6 -26
- package/dist/frontend/assets/{cssMode-BDT3WbVs.js → cssMode-Bsa7tmw2.js} +1 -1
- package/dist/frontend/assets/{freemarker2-C7-hEgID.js → freemarker2-CrKZYA9-.js} +1 -1
- package/dist/frontend/assets/{handlebars-4cwTkPir.js → handlebars-BgBTN99O.js} +1 -1
- package/dist/frontend/assets/{html-YNfE1Q0A.js → html-D36ptcmC.js} +1 -1
- package/dist/frontend/assets/{htmlMode-opTQ1HoB.js → htmlMode-CGVK_FcF.js} +1 -1
- package/dist/frontend/assets/{index-DWyaVa1h.js → index-BcB_fBN5.js} +133 -133
- package/dist/frontend/assets/index-Ds8n3J4W.css +1 -0
- package/dist/frontend/assets/{javascript-BEwGzk7T.js → javascript-C6sCQzuh.js} +1 -1
- package/dist/frontend/assets/{jsonMode-CGhIS5Al.js → jsonMode-Bh2liG7i.js} +1 -1
- package/dist/frontend/assets/{liquid-QekTGCGJ.js → liquid-CvdwL9kp.js} +1 -1
- package/dist/frontend/assets/{mdx-BAVDaB7v.js → mdx-CdZzWG3e.js} +1 -1
- package/dist/frontend/assets/{python-BQlHw7XO.js → python-CSUJS9Bk.js} +1 -1
- package/dist/frontend/assets/{razor-Be3Wwc2E.js → razor-ypTGnW2v.js} +1 -1
- package/dist/frontend/assets/{tsMode-CIBFoN3z.js → tsMode-Cr9H7DaY.js} +1 -1
- package/dist/frontend/assets/{typescript-BuV9wEIE.js → typescript-wuTifRF-.js} +1 -1
- package/dist/frontend/assets/{xml-DcDKYaM4.js → xml-DoFtLqDh.js} +1 -1
- package/dist/frontend/assets/{yaml-CuBNmOuI.js → yaml-CrPwKSmj.js} +1 -1
- package/dist/frontend/index.html +2 -2
- package/dist/opencode-auth/index.js +26 -3
- package/dist/opencode-server.js +5 -0
- package/dist/utils/approval-snapshot-common.js +29 -16
- package/dist/utils/opencode-auth.js +112 -0
- package/dist/utils/resource-files.js +22 -21
- package/dist/utils/skill.js +39 -9
- package/dist/utils/workspace-sync.js +12 -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-lXrsgeTF.css +0 -1
|
@@ -26,6 +26,14 @@ interface WorkflowSummary {
|
|
|
26
26
|
intent_summary: string
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
+
interface SessionLookupResponse {
|
|
30
|
+
error?: unknown
|
|
31
|
+
data?: {
|
|
32
|
+
id?: string
|
|
33
|
+
parentID?: string
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
29
37
|
async function readErrorMessageFromResponse(
|
|
30
38
|
response: Response,
|
|
31
39
|
fallbackMessage: string
|
|
@@ -85,7 +93,29 @@ function cosineSimilarity(vecA: number[], vecB: number[]): number {
|
|
|
85
93
|
// Plugin
|
|
86
94
|
// ============================================================================
|
|
87
95
|
|
|
88
|
-
export const FindSimilarWorkflowPlugin: Plugin = async (
|
|
96
|
+
export const FindSimilarWorkflowPlugin: Plugin = async ({ client }) => {
|
|
97
|
+
async function resolveRootSessionID(sessionID: string): Promise<string> {
|
|
98
|
+
let currentSessionID = sessionID
|
|
99
|
+
|
|
100
|
+
while (true) {
|
|
101
|
+
const response = (await client.session.get({
|
|
102
|
+
path: {
|
|
103
|
+
id: currentSessionID,
|
|
104
|
+
},
|
|
105
|
+
})) as SessionLookupResponse
|
|
106
|
+
if (response.error) {
|
|
107
|
+
throw new Error(`Failed to resolve root session for ${currentSessionID}`)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const parentID = response.data?.parentID
|
|
111
|
+
if (!parentID) {
|
|
112
|
+
return currentSessionID
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
currentSessionID = parentID
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
89
119
|
return {
|
|
90
120
|
tool: {
|
|
91
121
|
findSimilarWorkflow: tool({
|
|
@@ -105,11 +135,12 @@ export const FindSimilarWorkflowPlugin: Plugin = async (_ctx) => {
|
|
|
105
135
|
},
|
|
106
136
|
async execute(args, context) {
|
|
107
137
|
const { sessionID } = context
|
|
138
|
+
const authSessionID = await resolveRootSessionID(sessionID)
|
|
108
139
|
const { description, limit = 5 } = args
|
|
109
140
|
|
|
110
141
|
const workflowsResponse = await fetch(`${getApiBaseUrl()}/api/workflows`, {
|
|
111
142
|
headers: {
|
|
112
|
-
Authorization: `Bearer ${
|
|
143
|
+
Authorization: `Bearer ${authSessionID}`,
|
|
113
144
|
},
|
|
114
145
|
})
|
|
115
146
|
|
|
@@ -31,6 +31,14 @@ interface SkillMatch {
|
|
|
31
31
|
similarity: number
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
+
interface SessionLookupResponse {
|
|
35
|
+
error?: unknown
|
|
36
|
+
data?: {
|
|
37
|
+
id?: string
|
|
38
|
+
parentID?: string
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
34
42
|
async function readErrorMessageFromResponse(
|
|
35
43
|
response: Response,
|
|
36
44
|
fallbackMessage: string
|
|
@@ -90,14 +98,14 @@ function cosineSimilarity(vecA: number[], vecB: number[]): number {
|
|
|
90
98
|
}
|
|
91
99
|
|
|
92
100
|
async function readSkillMarkdown(
|
|
93
|
-
|
|
101
|
+
authSessionID: string,
|
|
94
102
|
slug: string
|
|
95
103
|
): Promise<string> {
|
|
96
104
|
const response = await fetch(
|
|
97
105
|
`${getApiBaseUrl()}/api/skills/${encodeURIComponent(slug)}/files/content?path=${encodeURIComponent("SKILL.md")}`,
|
|
98
106
|
{
|
|
99
107
|
headers: {
|
|
100
|
-
Authorization: `Bearer ${
|
|
108
|
+
Authorization: `Bearer ${authSessionID}`,
|
|
101
109
|
},
|
|
102
110
|
}
|
|
103
111
|
)
|
|
@@ -118,7 +126,29 @@ async function readSkillMarkdown(
|
|
|
118
126
|
return payload.content ?? ""
|
|
119
127
|
}
|
|
120
128
|
|
|
121
|
-
export const FindSkillPlugin: Plugin = async (
|
|
129
|
+
export const FindSkillPlugin: Plugin = async ({ client, directory }) => {
|
|
130
|
+
async function resolveRootSessionID(sessionID: string): Promise<string> {
|
|
131
|
+
let currentSessionID = sessionID
|
|
132
|
+
|
|
133
|
+
while (true) {
|
|
134
|
+
const response = (await client.session.get({
|
|
135
|
+
path: {
|
|
136
|
+
id: currentSessionID,
|
|
137
|
+
},
|
|
138
|
+
})) as SessionLookupResponse
|
|
139
|
+
if (response.error) {
|
|
140
|
+
throw new Error(`Failed to resolve root session for ${currentSessionID}`)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const parentID = response.data?.parentID
|
|
144
|
+
if (!parentID) {
|
|
145
|
+
return currentSessionID
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
currentSessionID = parentID
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
122
152
|
return {
|
|
123
153
|
tool: {
|
|
124
154
|
findSkill: tool({
|
|
@@ -143,9 +173,10 @@ export const FindSkillPlugin: Plugin = async (_ctx) => {
|
|
|
143
173
|
throw new Error("description is required")
|
|
144
174
|
}
|
|
145
175
|
|
|
176
|
+
const authSessionID = await resolveRootSessionID(sessionID)
|
|
146
177
|
const skillsResponse = await fetch(`${getApiBaseUrl()}/api/skills`, {
|
|
147
178
|
headers: {
|
|
148
|
-
Authorization: `Bearer ${
|
|
179
|
+
Authorization: `Bearer ${authSessionID}`,
|
|
149
180
|
},
|
|
150
181
|
})
|
|
151
182
|
|
|
@@ -180,7 +211,7 @@ export const FindSkillPlugin: Plugin = async (_ctx) => {
|
|
|
180
211
|
const matches: SkillMatch[] = []
|
|
181
212
|
|
|
182
213
|
for (const skill of candidateSkills) {
|
|
183
|
-
const markdown = await readSkillMarkdown(
|
|
214
|
+
const markdown = await readSkillMarkdown(authSessionID, skill.slug)
|
|
184
215
|
const searchableText = `${skill.description}\n\n${stripLeadingFrontmatter(markdown)}`
|
|
185
216
|
const skillEmbedding = await getEmbedding(searchableText)
|
|
186
217
|
const similarity = cosineSimilarity(queryEmbedding, skillEmbedding)
|
|
@@ -23,6 +23,14 @@ interface SkillDetailsResponse {
|
|
|
23
23
|
}
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
interface SessionLookupResponse {
|
|
27
|
+
error?: unknown
|
|
28
|
+
data?: {
|
|
29
|
+
id?: string
|
|
30
|
+
parentID?: string
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
26
34
|
async function readErrorMessageFromResponse(
|
|
27
35
|
response: Response,
|
|
28
36
|
fallbackMessage: string
|
|
@@ -45,7 +53,29 @@ async function readErrorMessageFromResponse(
|
|
|
45
53
|
}
|
|
46
54
|
}
|
|
47
55
|
|
|
48
|
-
export const GetSkillContentPlugin: Plugin = async (
|
|
56
|
+
export const GetSkillContentPlugin: Plugin = async ({ client }) => {
|
|
57
|
+
async function resolveRootSessionID(sessionID: string): Promise<string> {
|
|
58
|
+
let currentSessionID = sessionID
|
|
59
|
+
|
|
60
|
+
while (true) {
|
|
61
|
+
const response = (await client.session.get({
|
|
62
|
+
path: {
|
|
63
|
+
id: currentSessionID,
|
|
64
|
+
},
|
|
65
|
+
})) as SessionLookupResponse
|
|
66
|
+
if (response.error) {
|
|
67
|
+
throw new Error(`Failed to resolve root session for ${currentSessionID}`)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const parentID = response.data?.parentID
|
|
71
|
+
if (!parentID) {
|
|
72
|
+
return currentSessionID
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
currentSessionID = parentID
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
49
79
|
return {
|
|
50
80
|
tool: {
|
|
51
81
|
getSkillContent: tool({
|
|
@@ -59,6 +89,7 @@ export const GetSkillContentPlugin: Plugin = async (_ctx) => {
|
|
|
59
89
|
async execute(args, context) {
|
|
60
90
|
const { sessionID } = context
|
|
61
91
|
const slug = args.slug.trim()
|
|
92
|
+
const authSessionID = await resolveRootSessionID(sessionID)
|
|
62
93
|
|
|
63
94
|
if (!SLUG_REGEX.test(slug)) {
|
|
64
95
|
throw new Error(
|
|
@@ -70,7 +101,7 @@ export const GetSkillContentPlugin: Plugin = async (_ctx) => {
|
|
|
70
101
|
`${getApiBaseUrl()}/api/skills/${encodeURIComponent(slug)}`,
|
|
71
102
|
{
|
|
72
103
|
headers: {
|
|
73
|
-
Authorization: `Bearer ${
|
|
104
|
+
Authorization: `Bearer ${authSessionID}`,
|
|
74
105
|
},
|
|
75
106
|
}
|
|
76
107
|
)
|
|
@@ -97,7 +128,7 @@ export const GetSkillContentPlugin: Plugin = async (_ctx) => {
|
|
|
97
128
|
`${getApiBaseUrl()}/api/skills/${encodeURIComponent(slug)}/files/content?path=${encodeURIComponent("SKILL.md")}`,
|
|
98
129
|
{
|
|
99
130
|
headers: {
|
|
100
|
-
Authorization: `Bearer ${
|
|
131
|
+
Authorization: `Bearer ${authSessionID}`,
|
|
101
132
|
},
|
|
102
133
|
}
|
|
103
134
|
)
|
|
@@ -18,7 +18,7 @@ async function ensureHoneytokenFile(workspaceDirectory: string): Promise<void> {
|
|
|
18
18
|
const workflowsHoneytokenPath = path.join(workflowsDirectory, HONEYTOKEN_FILE_NAME)
|
|
19
19
|
await fs.writeFile(workflowsHoneytokenPath, honeytokenValue, "utf-8")
|
|
20
20
|
|
|
21
|
-
const skillsDirectory = path.join(workspaceDirectory, "
|
|
21
|
+
const skillsDirectory = path.join(workspaceDirectory, ".agents", "skills")
|
|
22
22
|
await fs.mkdir(skillsDirectory, { recursive: true })
|
|
23
23
|
const honeytokenPath = path.join(skillsDirectory, HONEYTOKEN_FILE_NAME)
|
|
24
24
|
await fs.writeFile(honeytokenPath, honeytokenValue, "utf-8")
|
|
@@ -18,6 +18,14 @@ interface SkillSummary {
|
|
|
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 ListAvailableSkillsPlugin: Plugin = async (
|
|
51
|
+
export const ListAvailableSkillsPlugin: 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
|
listAvailableSkills: tool({
|
|
@@ -49,10 +79,11 @@ export const ListAvailableSkillsPlugin: 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/skills`, {
|
|
54
85
|
headers: {
|
|
55
|
-
Authorization: `Bearer ${
|
|
86
|
+
Authorization: `Bearer ${authSessionID}`,
|
|
56
87
|
},
|
|
57
88
|
})
|
|
58
89
|
|
|
@@ -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
|
|