teamcopilot 0.3.6 → 0.4.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.
Files changed (65) hide show
  1. package/dist/chat/index.js +104 -81
  2. package/dist/cronjob/index.js +2 -0
  3. package/dist/cronjobs/index.js +822 -0
  4. package/dist/cronjobs/scheduler.js +936 -0
  5. package/dist/frontend/assets/{cssMode-BRVRAYCz.js → cssMode-Cqdl5sUM.js} +1 -1
  6. package/dist/frontend/assets/{freemarker2-B5FvHwsO.js → freemarker2-ykAhuplU.js} +1 -1
  7. package/dist/frontend/assets/{handlebars-DWX2asql.js → handlebars-DX_JwRM8.js} +1 -1
  8. package/dist/frontend/assets/{html-BEBxxD9G.js → html-Bi_zOcbU.js} +1 -1
  9. package/dist/frontend/assets/{htmlMode-B2LbPTwC.js → htmlMode-CkAUoAah.js} +1 -1
  10. package/dist/frontend/assets/index-Ba9bElZm.css +1 -0
  11. package/dist/frontend/assets/{index-D3TE04C5.js → index-Cgozj4fx.js} +245 -242
  12. package/dist/frontend/assets/{javascript-Bh4JwoPV.js → javascript-D3Rjwp97.js} +1 -1
  13. package/dist/frontend/assets/{jsonMode-7j-aplXT.js → jsonMode-K4i6LjP2.js} +1 -1
  14. package/dist/frontend/assets/{liquid-BP4OxkO7.js → liquid-D8F4-sAz.js} +1 -1
  15. package/dist/frontend/assets/{mdx-C1OIcGbY.js → mdx-C2xw8PNz.js} +1 -1
  16. package/dist/frontend/assets/{python-BO8Wy5jz.js → python-CqTGfu2v.js} +1 -1
  17. package/dist/frontend/assets/{razor-BDtqXvAH.js → razor-DFSsPzdZ.js} +1 -1
  18. package/dist/frontend/assets/{tsMode-D22HcCuX.js → tsMode-BkLQEtPb.js} +1 -1
  19. package/dist/frontend/assets/{typescript-CagwEzRw.js → typescript-CE_GQ-M1.js} +1 -1
  20. package/dist/frontend/assets/{xml-fE5sGZ5z.js → xml-CGjMtNcA.js} +1 -1
  21. package/dist/frontend/assets/{yaml-CZMoG4WG.js → yaml-Zju9kuFB.js} +1 -1
  22. package/dist/frontend/index.html +2 -2
  23. package/dist/index.js +3 -0
  24. package/dist/types/cronjob.js +2 -0
  25. package/dist/utils/chat-prompt-context.js +65 -0
  26. package/dist/utils/chat-session.js +12 -0
  27. package/dist/utils/index.js +27 -0
  28. package/dist/utils/workflow-interruption.js +5 -2
  29. package/dist/utils/workflow-run-validation.js +25 -0
  30. package/dist/utils/workspace-sync.js +17 -0
  31. package/dist/workflows/index.js +24 -25
  32. package/dist/workspace_files/.opencode/plugins/apply-patch-session-diff.ts +2 -2
  33. package/dist/workspace_files/.opencode/plugins/askCronjobUser.ts +106 -0
  34. package/dist/workspace_files/.opencode/plugins/manageCronjobTodos.ts +190 -0
  35. package/dist/workspace_files/.opencode/plugins/manageCronjobs.ts +376 -0
  36. package/dist/workspace_files/.opencode/plugins/markCronjobCompleted.ts +107 -0
  37. package/dist/workspace_files/.opencode/plugins/markCronjobFailed.ts +107 -0
  38. package/dist/workspace_files/AGENTS.md +51 -1
  39. package/package.json +1 -1
  40. package/prisma/generated/client/edge.js +50 -3
  41. package/prisma/generated/client/index-browser.js +47 -0
  42. package/prisma/generated/client/index.d.ts +13918 -7530
  43. package/prisma/generated/client/index.js +50 -3
  44. package/prisma/generated/client/package.json +1 -1
  45. package/prisma/generated/client/schema.prisma +72 -1
  46. package/prisma/generated/client/wasm.js +50 -3
  47. package/prisma/migrations/20260508050030_add_cronjobs/migration.sql +78 -0
  48. package/prisma/migrations/20260508093158_add_structured_cronjob_schedules/migration.sql +23 -0
  49. package/prisma/migrations/20260508105129_add_cronjob_targets/migration.sql +50 -0
  50. package/prisma/migrations/20260509044545_flatten_cronjob_schema/migration.sql +88 -0
  51. package/prisma/migrations/20260509052232_simplify_cronjob_schedule_storage/migration.sql +42 -0
  52. package/prisma/migrations/20260509054000_remove_chat_session_source_add_cronjob_run_indexes/migration.sql +28 -0
  53. package/prisma/migrations/20260509061000_cascade_cronjob_run_links/migration.sql +29 -0
  54. package/prisma/migrations/20260513073541_add_cronjob_run_todos/migration.sql +18 -0
  55. package/prisma/migrations/20260513133021_add_cronjob_user_response_wait/migration.sql +29 -0
  56. package/prisma/migrations/20260513135733_add_cronjob_user_handoff_state/migration.sql +30 -0
  57. package/prisma/migrations/20260513142511_drop_awaiting_user_response/migration.sql +35 -0
  58. package/prisma/migrations/20260514032204_simplify_cronjob_run_lifecycle/migration.sql +34 -0
  59. package/prisma/migrations/20260514043000_clear_cronjob_run_history/migration.sql +6 -0
  60. package/prisma/migrations/20260515094618_add_todo_list_version/migration.sql +32 -0
  61. package/prisma/migrations/20260516082714_add_cronjob_monitor_timeout/migration.sql +38 -0
  62. package/prisma/migrations/20260516083452_allow_decimal_cronjob_timeout/migration.sql +37 -0
  63. package/prisma/migrations/20260516084455_add_cronjob_timeout_defaults/migration.sql +31 -0
  64. package/prisma/schema.prisma +71 -1
  65. package/dist/frontend/assets/index-D1Hcz_bo.css +0 -1
@@ -0,0 +1,376 @@
1
+ import { type Plugin, tool } 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 PermissionResponse {
12
+ approved: boolean
13
+ status: string
14
+ }
15
+
16
+ interface SessionLookupResponse {
17
+ error?: unknown
18
+ data?: {
19
+ id?: string
20
+ parentID?: string
21
+ }
22
+ }
23
+
24
+ async function readErrorMessageFromResponse(
25
+ response: Response,
26
+ fallbackMessage: string
27
+ ): Promise<string> {
28
+ try {
29
+ const text = await response.text()
30
+ if (!text) return fallbackMessage
31
+ try {
32
+ const parsed: unknown = JSON.parse(text)
33
+ if (parsed && typeof parsed === "object" && "message" in parsed) {
34
+ const msg = (parsed as { message?: unknown }).message
35
+ if (typeof msg === "string" && msg.trim().length > 0) return msg
36
+ }
37
+ } catch {
38
+ // fall back to text
39
+ }
40
+ return text.trim().length > 0 ? text : fallbackMessage
41
+ } catch {
42
+ return fallbackMessage
43
+ }
44
+ }
45
+
46
+ function extractMessageId(context: unknown): string | null {
47
+ if (!context || typeof context !== "object") {
48
+ return null
49
+ }
50
+
51
+ const candidate = (context as { messageID?: unknown; messageId?: unknown; message_id?: unknown })
52
+ const raw = candidate.messageID ?? candidate.messageId ?? candidate.message_id
53
+ if (typeof raw === "string" && raw.trim().length > 0) {
54
+ return raw
55
+ }
56
+ if (typeof raw === "number" && Number.isFinite(raw)) {
57
+ return String(raw)
58
+ }
59
+ return null
60
+ }
61
+
62
+ function extractCallId(context: unknown): string | null {
63
+ if (!context || typeof context !== "object") {
64
+ return null
65
+ }
66
+
67
+ const candidate = (context as { callID?: unknown; callId?: unknown; call_id?: unknown })
68
+ const raw = candidate.callID ?? candidate.callId ?? candidate.call_id
69
+ if (typeof raw === "string" && raw.trim().length > 0) {
70
+ return raw
71
+ }
72
+ return null
73
+ }
74
+
75
+ function assertTargetType(targetType: string): void {
76
+ if (targetType !== "prompt" && targetType !== "workflow") {
77
+ throw new Error('target_type must be "prompt" or "workflow".')
78
+ }
79
+ }
80
+
81
+ function assertNonEmpty(value: string, label: string): void {
82
+ if (value.trim().length === 0) {
83
+ throw new Error(`${label} is required.`)
84
+ }
85
+ }
86
+
87
+ function ensureMessageAndCallIds(context: unknown): { messageId: string; callId: string } {
88
+ const messageId = extractMessageId(context)
89
+ const callId = extractCallId(context)
90
+
91
+ if (!messageId) {
92
+ throw new Error("Could not determine message id from tool context.")
93
+ }
94
+
95
+ if (!callId) {
96
+ throw new Error("Could not determine call id from tool context.")
97
+ }
98
+
99
+ return { messageId, callId }
100
+ }
101
+
102
+ async function rejectCronjobPermission(
103
+ sessionID: string,
104
+ permissionId: string
105
+ ): Promise<void> {
106
+ await fetch(`${getApiBaseUrl()}/api/workflows/permission-reject/${permissionId}`, {
107
+ method: "POST",
108
+ headers: {
109
+ Authorization: `Bearer ${sessionID}`,
110
+ },
111
+ })
112
+ }
113
+
114
+ async function requestCronjobPermission(
115
+ sessionID: string,
116
+ messageID: string,
117
+ callID: string,
118
+ action: string
119
+ ): Promise<void> {
120
+ const response = await fetch(`${getApiBaseUrl()}/api/workflows/request-permission`, {
121
+ method: "POST",
122
+ headers: {
123
+ "Content-Type": "application/json",
124
+ Authorization: `Bearer ${sessionID}`,
125
+ },
126
+ body: JSON.stringify({
127
+ message_id: messageID,
128
+ call_id: callID,
129
+ }),
130
+ })
131
+
132
+ if (!response.ok) {
133
+ const message = await readErrorMessageFromResponse(
134
+ response,
135
+ `Failed to request permission (HTTP ${response.status})`
136
+ )
137
+ throw new Error(message)
138
+ }
139
+
140
+ const data = (await response.json()) as { permission_id: string }
141
+ const permissionId = data.permission_id
142
+
143
+ const maxAttempts = 300
144
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
145
+ await new Promise((resolve) => setTimeout(resolve, 1000))
146
+
147
+ const statusResponse = await fetch(
148
+ `${getApiBaseUrl()}/api/workflows/permission-status/${permissionId}`,
149
+ {
150
+ headers: {
151
+ Authorization: `Bearer ${sessionID}`,
152
+ },
153
+ }
154
+ )
155
+
156
+ if (!statusResponse.ok) {
157
+ continue
158
+ }
159
+
160
+ const statusData = (await statusResponse.json()) as PermissionResponse
161
+
162
+ if (statusData.status === "approved") {
163
+ return
164
+ }
165
+ if (statusData.status === "rejected") {
166
+ throw new Error(`User denied permission to ${action}.`)
167
+ }
168
+ }
169
+
170
+ try {
171
+ await rejectCronjobPermission(sessionID, permissionId)
172
+ } catch {
173
+ // Best-effort cleanup only; preserve the timeout error below.
174
+ }
175
+ throw new Error("Permission request timed out")
176
+ }
177
+
178
+ async function fetchJsonWithError(
179
+ url: string,
180
+ init: RequestInit,
181
+ fallbackMessage: string
182
+ ): Promise<unknown> {
183
+ const response = await fetch(url, init)
184
+ if (!response.ok) {
185
+ const errorMessage = await readErrorMessageFromResponse(response, fallbackMessage)
186
+ throw new Error(errorMessage)
187
+ }
188
+ return await response.json()
189
+ }
190
+
191
+ export const ManageCronjobsPlugin: Plugin = async ({ client }) => {
192
+ async function resolveRootSessionID(sessionID: string): Promise<string> {
193
+ let currentSessionID = sessionID
194
+
195
+ while (true) {
196
+ const response = (await client.session.get({
197
+ path: {
198
+ id: currentSessionID,
199
+ },
200
+ })) as SessionLookupResponse
201
+ if (response.error) {
202
+ throw new Error(`Failed to resolve root session for ${currentSessionID}`)
203
+ }
204
+
205
+ const parentID = response.data?.parentID
206
+ if (!parentID) {
207
+ return currentSessionID
208
+ }
209
+
210
+ currentSessionID = parentID
211
+ }
212
+ }
213
+
214
+ return {
215
+ tool: {
216
+ listCronjobs: tool({
217
+ description:
218
+ "List the current user's TeamCopilot cronjobs, including ids needed for editCronjob and runCronjobNow. This is read-only and does not require approval.",
219
+ args: {},
220
+ async execute(_args, context) {
221
+ const { sessionID } = context
222
+ const authSessionID = await resolveRootSessionID(sessionID)
223
+
224
+ const payload = await fetchJsonWithError(
225
+ `${getApiBaseUrl()}/api/cronjobs`,
226
+ {
227
+ method: "GET",
228
+ headers: {
229
+ Authorization: `Bearer ${authSessionID}`,
230
+ },
231
+ },
232
+ "Failed to list cronjobs"
233
+ )
234
+
235
+ return JSON.stringify(payload, null, 2)
236
+ },
237
+ }),
238
+ createCronjob: tool({
239
+ description:
240
+ "Create and schedule a TeamCopilot cronjob. Requires user approval for this tool call. Future scheduled runs of the created cronjob will not ask for approval again.",
241
+ args: {
242
+ name: tool.schema.string().describe("Human-readable cronjob name."),
243
+ enabled: tool.schema.boolean().describe("Whether the cronjob should be scheduled immediately after creation."),
244
+ target_type: tool.schema.enum(["prompt", "workflow"]).describe("Use prompt for an agent prompt cronjob, or workflow for a direct workflow cronjob."),
245
+ prompt: tool.schema.string().optional().describe("Required when target_type is prompt. The unattended cronjob prompt to run."),
246
+ allow_workflow_runs_without_permission: tool.schema.boolean().optional().default(true).describe("Prompt cronjobs only. If true, workflows invoked by that cronjob run without additional user approval during scheduled execution."),
247
+ workflow_slug: tool.schema.string().optional().describe("Required when target_type is workflow. Workflow slug to run."),
248
+ workflow_inputs: tool.schema.record(tool.schema.string(), tool.schema.unknown()).optional().default({}).describe("Workflow cronjobs only. Saved workflow input arguments."),
249
+ cron_expression: tool.schema.string().describe("Five-field cron expression, for example '0 9 * * 1-5'."),
250
+ timezone: tool.schema.string().describe("IANA timezone, for example 'UTC', 'Asia/Kolkata', or 'America/Los_Angeles'."),
251
+ },
252
+ async execute(args, context) {
253
+ const { sessionID } = context
254
+ const authSessionID = await resolveRootSessionID(sessionID)
255
+ const { messageId, callId } = ensureMessageAndCallIds(context)
256
+
257
+ assertNonEmpty(args.name, "name")
258
+ assertTargetType(args.target_type)
259
+ assertNonEmpty(args.cron_expression, "cron_expression")
260
+ assertNonEmpty(args.timezone, "timezone")
261
+ if (args.target_type === "prompt") {
262
+ assertNonEmpty(args.prompt ?? "", "prompt")
263
+ }
264
+ if (args.target_type === "workflow") {
265
+ assertNonEmpty(args.workflow_slug ?? "", "workflow_slug")
266
+ }
267
+
268
+ await requestCronjobPermission(authSessionID, messageId, callId, "create this cronjob")
269
+
270
+ const payload = await fetchJsonWithError(
271
+ `${getApiBaseUrl()}/api/cronjobs`,
272
+ {
273
+ method: "POST",
274
+ headers: {
275
+ "Content-Type": "application/json",
276
+ Authorization: `Bearer ${authSessionID}`,
277
+ },
278
+ body: JSON.stringify(args),
279
+ },
280
+ "Failed to create cronjob"
281
+ )
282
+
283
+ return JSON.stringify(payload, null, 2)
284
+ },
285
+ }),
286
+ editCronjob: tool({
287
+ description:
288
+ "Edit an existing TeamCopilot cronjob. Requires user approval for this tool call. Future scheduled runs of the edited cronjob will not ask for approval again.",
289
+ args: {
290
+ id: tool.schema.string().describe("Cronjob id to edit."),
291
+ name: tool.schema.string().optional().describe("New human-readable cronjob name."),
292
+ enabled: tool.schema.boolean().optional().describe("Whether the cronjob should be scheduled."),
293
+ target_type: tool.schema.enum(["prompt", "workflow"]).optional().describe("Use prompt for an agent prompt cronjob, or workflow for a direct workflow cronjob."),
294
+ prompt: tool.schema.string().optional().describe("Prompt cronjob task text."),
295
+ allow_workflow_runs_without_permission: tool.schema.boolean().optional().describe("Prompt cronjobs only. If true, workflows invoked by that cronjob run without additional user approval during scheduled execution."),
296
+ workflow_slug: tool.schema.string().optional().describe("Workflow slug for workflow cronjobs."),
297
+ workflow_inputs: tool.schema.record(tool.schema.string(), tool.schema.unknown()).optional().describe("Saved workflow input arguments."),
298
+ cron_expression: tool.schema.string().optional().describe("Five-field cron expression, for example '0 9 * * 1-5'."),
299
+ timezone: tool.schema.string().optional().describe("IANA timezone, for example 'UTC', 'Asia/Kolkata', or 'America/Los_Angeles'."),
300
+ },
301
+ async execute(args, context) {
302
+ const { sessionID } = context
303
+ const authSessionID = await resolveRootSessionID(sessionID)
304
+ const { messageId, callId } = ensureMessageAndCallIds(context)
305
+
306
+ assertNonEmpty(args.id, "id")
307
+ if (args.target_type !== undefined) {
308
+ assertTargetType(args.target_type)
309
+ }
310
+ if (args.name !== undefined) {
311
+ assertNonEmpty(args.name, "name")
312
+ }
313
+ if (args.cron_expression !== undefined) {
314
+ assertNonEmpty(args.cron_expression, "cron_expression")
315
+ }
316
+ if (args.timezone !== undefined) {
317
+ assertNonEmpty(args.timezone, "timezone")
318
+ }
319
+
320
+ const { id, ...patch } = args
321
+ if (Object.keys(patch).length === 0) {
322
+ throw new Error("At least one cronjob field must be provided to edit.")
323
+ }
324
+
325
+ await requestCronjobPermission(authSessionID, messageId, callId, "edit this cronjob")
326
+
327
+ const payload = await fetchJsonWithError(
328
+ `${getApiBaseUrl()}/api/cronjobs/${encodeURIComponent(id)}`,
329
+ {
330
+ method: "PATCH",
331
+ headers: {
332
+ "Content-Type": "application/json",
333
+ Authorization: `Bearer ${authSessionID}`,
334
+ },
335
+ body: JSON.stringify(patch),
336
+ },
337
+ "Failed to edit cronjob"
338
+ )
339
+
340
+ return JSON.stringify(payload, null, 2)
341
+ },
342
+ }),
343
+ runCronjobNow: tool({
344
+ description:
345
+ "Run an existing TeamCopilot cronjob immediately. Requires user approval for this tool call.",
346
+ args: {
347
+ id: tool.schema.string().describe("Cronjob id to run now."),
348
+ },
349
+ async execute(args, context) {
350
+ const { sessionID } = context
351
+ const authSessionID = await resolveRootSessionID(sessionID)
352
+ const { messageId, callId } = ensureMessageAndCallIds(context)
353
+
354
+ assertNonEmpty(args.id, "id")
355
+
356
+ await requestCronjobPermission(authSessionID, messageId, callId, "run this cronjob now")
357
+
358
+ const payload = await fetchJsonWithError(
359
+ `${getApiBaseUrl()}/api/cronjobs/${encodeURIComponent(args.id)}/run-now`,
360
+ {
361
+ method: "POST",
362
+ headers: {
363
+ Authorization: `Bearer ${authSessionID}`,
364
+ },
365
+ },
366
+ "Failed to run cronjob now"
367
+ )
368
+
369
+ return JSON.stringify(payload, null, 2)
370
+ },
371
+ }),
372
+ },
373
+ }
374
+ }
375
+
376
+ export default ManageCronjobsPlugin
@@ -0,0 +1,107 @@
1
+ import { type Plugin, tool } 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 SessionLookupResponse {
12
+ error?: unknown
13
+ data?: {
14
+ id?: string
15
+ parentID?: string
16
+ }
17
+ }
18
+
19
+ async function readErrorMessageFromResponse(
20
+ response: Response,
21
+ fallbackMessage: string
22
+ ): Promise<string> {
23
+ try {
24
+ const text = await response.text()
25
+ if (!text) return fallbackMessage
26
+ try {
27
+ const parsed: unknown = JSON.parse(text)
28
+ if (parsed && typeof parsed === "object" && "message" in parsed) {
29
+ const msg = (parsed as { message?: unknown }).message
30
+ if (typeof msg === "string" && msg.trim().length > 0) return msg
31
+ }
32
+ } catch {
33
+ // fall back to text
34
+ }
35
+ return text.trim().length > 0 ? text : fallbackMessage
36
+ } catch {
37
+ return fallbackMessage
38
+ }
39
+ }
40
+
41
+ export const MarkCronjobCompletedPlugin: Plugin = async ({ client }) => {
42
+ async function resolveRootSessionID(sessionID: string): Promise<string> {
43
+ let currentSessionID = sessionID
44
+
45
+ while (true) {
46
+ const response = (await client.session.get({
47
+ path: {
48
+ id: currentSessionID,
49
+ },
50
+ })) as SessionLookupResponse
51
+ if (response.error) {
52
+ throw new Error(`Failed to resolve root session for ${currentSessionID}`)
53
+ }
54
+
55
+ const parentID = response.data?.parentID
56
+ if (!parentID) {
57
+ return currentSessionID
58
+ }
59
+
60
+ currentSessionID = parentID
61
+ }
62
+ }
63
+
64
+ return {
65
+ tool: {
66
+ markCronjobCompleted: tool({
67
+ description:
68
+ "Mark the current TeamCopilot cronjob run as successfully completed. Use this exactly once, only after all requested cronjob work is complete and all TeamCopilot cronjob todos have been finished. This tool fails while any cronjob todo is still pending or in progress.",
69
+ args: {
70
+ summary: tool.schema
71
+ .string()
72
+ .describe("A concise summary of what the cronjob completed, suitable for the cronjob run history."),
73
+ },
74
+ async execute(args, context) {
75
+ const { sessionID } = context
76
+ const authSessionID = await resolveRootSessionID(sessionID)
77
+ const summary = args.summary?.trim()
78
+
79
+ if (!summary) {
80
+ throw new Error("summary is required")
81
+ }
82
+
83
+ const response = await fetch(`${getApiBaseUrl()}/api/cronjobs/runs/complete-current`, {
84
+ method: "POST",
85
+ headers: {
86
+ "Content-Type": "application/json",
87
+ Authorization: `Bearer ${authSessionID}`,
88
+ },
89
+ body: JSON.stringify({ summary }),
90
+ })
91
+
92
+ if (!response.ok) {
93
+ const errorMessage = await readErrorMessageFromResponse(
94
+ response,
95
+ `Failed to mark cronjob completed (HTTP ${response.status})`
96
+ )
97
+ throw new Error(errorMessage)
98
+ }
99
+
100
+ return JSON.stringify({ success: true, summary })
101
+ },
102
+ }),
103
+ },
104
+ }
105
+ }
106
+
107
+ export default MarkCronjobCompletedPlugin
@@ -0,0 +1,107 @@
1
+ import { type Plugin, tool } 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 SessionLookupResponse {
12
+ error?: unknown
13
+ data?: {
14
+ id?: string
15
+ parentID?: string
16
+ }
17
+ }
18
+
19
+ async function readErrorMessageFromResponse(
20
+ response: Response,
21
+ fallbackMessage: string
22
+ ): Promise<string> {
23
+ try {
24
+ const text = await response.text()
25
+ if (!text) return fallbackMessage
26
+ try {
27
+ const parsed: unknown = JSON.parse(text)
28
+ if (parsed && typeof parsed === "object" && "message" in parsed) {
29
+ const msg = (parsed as { message?: unknown }).message
30
+ if (typeof msg === "string" && msg.trim().length > 0) return msg
31
+ }
32
+ } catch {
33
+ // fall back to text
34
+ }
35
+ return text.trim().length > 0 ? text : fallbackMessage
36
+ } catch {
37
+ return fallbackMessage
38
+ }
39
+ }
40
+
41
+ export const MarkCronjobFailedPlugin: Plugin = async ({ client }) => {
42
+ async function resolveRootSessionID(sessionID: string): Promise<string> {
43
+ let currentSessionID = sessionID
44
+
45
+ while (true) {
46
+ const response = (await client.session.get({
47
+ path: {
48
+ id: currentSessionID,
49
+ },
50
+ })) as SessionLookupResponse
51
+ if (response.error) {
52
+ throw new Error(`Failed to resolve root session for ${currentSessionID}`)
53
+ }
54
+
55
+ const parentID = response.data?.parentID
56
+ if (!parentID) {
57
+ return currentSessionID
58
+ }
59
+
60
+ currentSessionID = parentID
61
+ }
62
+ }
63
+
64
+ return {
65
+ tool: {
66
+ markCronjobFailed: tool({
67
+ description:
68
+ "Mark the current TeamCopilot cronjob run as failed. Use this exactly once when the cronjob task cannot be completed because of a non-recoverable issue, and include a concise reason suitable for cronjob run history.",
69
+ args: {
70
+ summary: tool.schema
71
+ .string()
72
+ .describe("A concise reason explaining why the cronjob could not complete."),
73
+ },
74
+ async execute(args, context) {
75
+ const { sessionID } = context
76
+ const authSessionID = await resolveRootSessionID(sessionID)
77
+ const summary = args.summary?.trim()
78
+
79
+ if (!summary) {
80
+ throw new Error("summary is required")
81
+ }
82
+
83
+ const response = await fetch(`${getApiBaseUrl()}/api/cronjobs/runs/fail-current`, {
84
+ method: "POST",
85
+ headers: {
86
+ "Content-Type": "application/json",
87
+ Authorization: `Bearer ${authSessionID}`,
88
+ },
89
+ body: JSON.stringify({ summary }),
90
+ })
91
+
92
+ if (!response.ok) {
93
+ const errorMessage = await readErrorMessageFromResponse(
94
+ response,
95
+ `Failed to mark cronjob failed (HTTP ${response.status})`
96
+ )
97
+ throw new Error(errorMessage)
98
+ }
99
+
100
+ return JSON.stringify({ success: true, summary })
101
+ },
102
+ }),
103
+ },
104
+ }
105
+ }
106
+
107
+ export default MarkCronjobFailedPlugin
@@ -1,6 +1,6 @@
1
1
  # TeamCopilot Agent Instructions
2
2
 
3
- You are TeamCopilot agent. So when asked you who you are, you must start with "I am the TeamCopilot agent." and then continue with your reply.
3
+ You are TeamCopilot agent.
4
4
 
5
5
  TeamCopilot is a multi-user AI agent platform for teams that want shared agent capabilities with permissions. Users can create custom agent skills and workflows (python scripts), and share them with members of their team in a secure and controlled environment. This document is your operating manual for working within this directory (called workspace). Follow these conventions strictly when creating, updating, or running workflows and custom skills.
6
6
 
@@ -531,6 +531,56 @@ When you need a quick inventory before semantic search or selection:
531
531
  2. If needed, then use `findSimilarWorkflow` to rank by semantic relevance.
532
532
  3. Choose the best candidate and proceed with `runWorkflow` (or update/create flow as needed).
533
533
 
534
+ ---
535
+
536
+ ## Section 3: Cronjobs
537
+
538
+ ### What is a Cronjob?
539
+
540
+ A **cronjob** is a scheduled TeamCopilot task owned by the current user. Cronjobs can run either:
541
+ - A prompt-based agent session on a recurring schedule
542
+ - A workflow directly with saved workflow input arguments
543
+
544
+ Cronjobs are configured through platform APIs via dedicated cronjob tools. Do not create, edit, or run cronjobs with raw HTTP calls, direct database writes, or shell scripts.
545
+
546
+ ### Required Cronjob Tools
547
+
548
+ - `listCronjobs` — list the current user's cronjobs. Use this to find cronjob ids before editing or running an existing cronjob.
549
+ - `createCronjob` — create and schedule a cronjob. This tool requires explicit user permission during execution.
550
+ - `editCronjob` — edit an existing cronjob. This tool requires explicit user permission during execution.
551
+ - `runCronjobNow` — run an existing cronjob immediately. This tool requires explicit user permission during execution.
552
+
553
+ ### Cronjob Rules
554
+
555
+ - Use cronjob tools when the user asks you to schedule recurring work.
556
+ - After a cronjob is created or edited, its future scheduled runs are already authorized by that approved tool call. Do not ask the user to approve each future scheduled run.
557
+ - If the user asks to delete a cronjob, tell them that you cannot delete it and that they should delete it using the UI. You can only disable it if they want to.
558
+ - For workflow cronjobs, provide all required workflow input arguments during creation or editing.
559
+ - For prompt cronjobs, write prompts so the scheduled agent can finish without user input unless user attention is truly required.
560
+ - In prompt cronjobs, unless it's a very simple task, prefer creating a custom skill first, and then mentioning to use that skill in the prompt. We want to keep the cronjob prompt as small as possible.
561
+
562
+ ### Example: Creating a Cronjob
563
+
564
+ When asked to "Schedule a repo health check every weekday at 9 AM":
565
+
566
+ 1. Use `createCronjob` with:
567
+ - `name`
568
+ - `enabled`
569
+ - `target_type`
570
+ - `prompt` or `workflow_slug` plus `workflow_inputs`
571
+ - `cron_expression`
572
+ - `timezone`
573
+ 2. Wait for the user to approve the tool permission prompt.
574
+ 3. Tell the user that future scheduled runs will not ask for approval again.
575
+
576
+ ### Example: Editing or Running a Cronjob
577
+
578
+ When asked to change or immediately run an existing cronjob:
579
+
580
+ 1. Use `listCronjobs` to find the correct cronjob id.
581
+ 2. Use `editCronjob` or `runCronjobNow`.
582
+ 3. Wait for the user to approve the tool permission prompt.
583
+
534
584
  ## Example: Finding and Using a Skill
535
585
 
536
586
  When a user asks for behavior that may already be captured as reusable instructions:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "teamcopilot",
3
- "version": "0.3.6",
3
+ "version": "0.4.1",
4
4
  "description": "A shared AI Agent for Teams",
5
5
  "homepage": "https://teamcopilot.ai",
6
6
  "repository": {