|
|
@@ -1,5 +1,5 @@
|
|
|
import type { Plugin } from "@kirincode-ai/plugin"
|
|
|
-import type { Part } from "@kirincode-ai/sdk"
|
|
|
+import { tool } from "@kirincode-ai/plugin"
|
|
|
import { CronScheduler } from "./scheduler"
|
|
|
import { FileWatcher } from "./watcher"
|
|
|
import { WorkflowStore } from "./store"
|
|
|
@@ -35,15 +35,20 @@ export const KirinCodeWorkflowPlugin: Plugin = async ({ client, directory, workt
|
|
|
run.sessionId = sessionId
|
|
|
store.saveRun(run)
|
|
|
|
|
|
- await client.session.prompt({
|
|
|
- path: { id: sessionId },
|
|
|
- body: {
|
|
|
- parts: [{ type: "text", text: def.prompt }],
|
|
|
- model: def.model && def.agent
|
|
|
- ? { providerID: def.model.split("/")[0], modelID: def.model.split("/").slice(1).join("/") }
|
|
|
- : undefined,
|
|
|
- },
|
|
|
- })
|
|
|
+ const promptBody: Record<string, unknown> = {
|
|
|
+ parts: [{ type: "text", text: def.prompt }],
|
|
|
+ }
|
|
|
+ if (def.model) {
|
|
|
+ const idx = def.model.indexOf("/")
|
|
|
+ if (idx > 0) {
|
|
|
+ promptBody.model = {
|
|
|
+ providerID: def.model.slice(0, idx),
|
|
|
+ modelID: def.model.slice(idx + 1),
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ await client.session.prompt({ path: { id: sessionId }, body: promptBody })
|
|
|
|
|
|
run.status = "completed"
|
|
|
run.endedAt = Date.now()
|
|
|
@@ -54,7 +59,6 @@ export const KirinCodeWorkflowPlugin: Plugin = async ({ client, directory, workt
|
|
|
} finally {
|
|
|
RUNNING_TASKS.delete(run.id)
|
|
|
store.saveRun(run)
|
|
|
-
|
|
|
const state = store.loadState(def.name)
|
|
|
state.lastRun = run.startedAt
|
|
|
state.runCount++
|
|
|
@@ -62,27 +66,20 @@ export const KirinCodeWorkflowPlugin: Plugin = async ({ client, directory, workt
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- // Load and register all workflow definitions
|
|
|
function reloadWorkflows(): void {
|
|
|
scheduler.stopAll()
|
|
|
watcher.stopAll()
|
|
|
-
|
|
|
const defs = store.loadDefs()
|
|
|
for (const def of defs) {
|
|
|
if (def.enabled === false) continue
|
|
|
-
|
|
|
if (def.schedule) {
|
|
|
- const cron = scheduler.parse(def.schedule)
|
|
|
- const intervalMs = 30000 // check every 30s
|
|
|
- scheduler.start(`wf_${def.name}`, intervalMs, () => {
|
|
|
- const next = cron.next()
|
|
|
- const now = Date.now()
|
|
|
- if (Math.abs(next - now) < intervalMs) {
|
|
|
- executeWorkflow(def)
|
|
|
- }
|
|
|
- })
|
|
|
+ try {
|
|
|
+ const cron = scheduler.parse(def.schedule)
|
|
|
+ scheduler.start(`wf_${def.name}`, 30000, () => {
|
|
|
+ if (Math.abs(cron.next() - Date.now()) < 30000) executeWorkflow(def)
|
|
|
+ })
|
|
|
+ } catch { /* invalid cron, skip */ }
|
|
|
}
|
|
|
-
|
|
|
if (def.trigger === "file_change") {
|
|
|
watcher.watch(def, worktree, () => executeWorkflow(def))
|
|
|
}
|
|
|
@@ -92,11 +89,11 @@ export const KirinCodeWorkflowPlugin: Plugin = async ({ client, directory, workt
|
|
|
reloadWorkflows()
|
|
|
|
|
|
return {
|
|
|
- "command.execute.before": async (input, output) => {
|
|
|
- const args = input.arguments.trim()
|
|
|
-
|
|
|
- if (input.command === "workflow") {
|
|
|
- if (!args || args === "list") {
|
|
|
+ tool: {
|
|
|
+ workflow_list: tool({
|
|
|
+ description: "List all defined workflows and their status. Shows workflow name, schedule/trigger, run count, and recent execution history. Use before workflow_run or workflow_create.",
|
|
|
+ args: {},
|
|
|
+ async execute() {
|
|
|
const defs = store.loadDefs()
|
|
|
const runs = store.listRuns(10)
|
|
|
const lines = ["## Workflows", ""]
|
|
|
@@ -107,57 +104,67 @@ export const KirinCodeWorkflowPlugin: Plugin = async ({ client, directory, workt
|
|
|
const trig = d.trigger ? ` trigger: ${d.trigger}` : ""
|
|
|
lines.push(`- ${icon} **${d.name}** — ${d.description || d.prompt.slice(0, 60)}${sch}${trig} (runs: ${state.runCount})`)
|
|
|
}
|
|
|
- if (defs.length === 0) lines.push("*No workflows defined. Create .kirincode/workflows/*.yml files.*")
|
|
|
+ if (defs.length === 0) return "No workflows defined. Create .kirincode/workflows/*.yml files."
|
|
|
lines.push("", "## Recent Runs", "")
|
|
|
for (const r of runs) {
|
|
|
const icon = r.status === "completed" ? "✅" : r.status === "failed" ? "❌" : "🔄"
|
|
|
- lines.push(`- ${icon} ${r.workflow} — ${new Date(r.startedAt).toLocaleTimeString()}`)
|
|
|
+ const err = r.error ? ` — ${r.error}` : ""
|
|
|
+ lines.push(`- ${icon} ${r.workflow} — ${new Date(r.startedAt).toLocaleTimeString()}${err}`)
|
|
|
}
|
|
|
- output.parts = [{ type: "text", text: lines.join("\n") }]
|
|
|
- return
|
|
|
- }
|
|
|
-
|
|
|
- if (args === "reload") {
|
|
|
- reloadWorkflows()
|
|
|
- output.parts = [{ type: "text", text: `Workflows reloaded. ${store.loadDefs().length} active.` }]
|
|
|
- return
|
|
|
- }
|
|
|
+ return lines.join("\n")
|
|
|
+ },
|
|
|
+ }),
|
|
|
|
|
|
- if (args.startsWith("run ")) {
|
|
|
- const name = args.slice(4).trim()
|
|
|
- const def = store.loadDefs().find((d) => d.name === name)
|
|
|
- if (!def) {
|
|
|
- output.parts = [{ type: "text", text: `Workflow "${name}" not found.` }]
|
|
|
- return
|
|
|
- }
|
|
|
+ workflow_run: tool({
|
|
|
+ description: "Execute a workflow immediately by name. Use workflow_list first to see available workflows.",
|
|
|
+ args: {
|
|
|
+ name: tool.schema.string().describe("Name of the workflow to run"),
|
|
|
+ },
|
|
|
+ async execute(args) {
|
|
|
+ const def = store.loadDefs().find((d) => d.name === args.name)
|
|
|
+ if (!def) return `Workflow "${args.name}" not found. Use workflow_list to see available workflows.`
|
|
|
executeWorkflow(def)
|
|
|
- output.parts = [{ type: "text", text: `Started workflow: ${name}` }]
|
|
|
- return
|
|
|
- }
|
|
|
-
|
|
|
- if (args.startsWith("create ")) {
|
|
|
- const parts = args.slice(7).split(/\s+/)
|
|
|
- if (parts.length < 2) {
|
|
|
- output.parts = [{ type: "text", text: "Usage: /workflow create <name> <cron|file_change> [prompt...]" }]
|
|
|
- return
|
|
|
- }
|
|
|
- const name = parts[0]
|
|
|
- const trigger = parts[1]
|
|
|
- const prompt = parts.slice(2).join(" ") || "TODO: add your prompt here"
|
|
|
-
|
|
|
- const def: WorkflowDef = { name, prompt }
|
|
|
- if (trigger.match(/^[\d\*,\-\/ ]{9,}$/)) def.schedule = trigger
|
|
|
- else if (trigger === "file_change") def.trigger = "file_change"
|
|
|
- else {
|
|
|
- output.parts = [{ type: "text", text: "Invalid trigger. Use a cron expression or 'file_change'." }]
|
|
|
- return
|
|
|
+ return `Started workflow: ${args.name}`
|
|
|
+ },
|
|
|
+ }),
|
|
|
+
|
|
|
+ workflow_create: tool({
|
|
|
+ description: "Create a new workflow definition. Workflows can be scheduled (cron format) or triggered by file changes.",
|
|
|
+ args: {
|
|
|
+ name: tool.schema.string().describe("Unique workflow name"),
|
|
|
+ trigger: tool.schema.string().describe("Cron expression (e.g. '0 9 * * *') or 'file_change'"),
|
|
|
+ prompt: tool.schema.string().describe("The prompt to send to the AI agent when triggered"),
|
|
|
+ description: tool.schema.string().optional().describe("Optional description of what this workflow does"),
|
|
|
+ debounce: tool.schema.number().optional().describe("For file_change: debounce in seconds (default 60)"),
|
|
|
+ model: tool.schema.string().optional().describe("Optional model (e.g. 'anthropic/claude-haiku-4-5')"),
|
|
|
+ },
|
|
|
+ async execute(args) {
|
|
|
+ const def: WorkflowDef = { name: args.name, prompt: args.prompt }
|
|
|
+ if (args.description) def.description = args.description
|
|
|
+ if (args.model) def.model = args.model
|
|
|
+ if (args.debounce) def.debounce = args.debounce
|
|
|
+ if (args.trigger === "file_change") {
|
|
|
+ def.trigger = "file_change"
|
|
|
+ } else if (/^[\d\*,\-\/ ]{9,}$/.test(args.trigger)) {
|
|
|
+ try { scheduler.parse(args.trigger); def.schedule = args.trigger }
|
|
|
+ catch { return `Invalid cron: ${args.trigger}. Use format "min hour day month weekday" (e.g. "0 9 * * *")` }
|
|
|
+ } else {
|
|
|
+ return `Invalid trigger: ${args.trigger}. Use a cron expression or "file_change".`
|
|
|
}
|
|
|
store.saveDef(def)
|
|
|
reloadWorkflows()
|
|
|
- output.parts = [{ type: "text", text: `Workflow '${name}' created at .kirincode/workflows/${name}.yml` }]
|
|
|
- return
|
|
|
- }
|
|
|
- }
|
|
|
+ return `Workflow '${args.name}' created at .kirincode/workflows/${args.name}.yml`
|
|
|
+ },
|
|
|
+ }),
|
|
|
+
|
|
|
+ workflow_reload: tool({
|
|
|
+ description: "Reload all workflow definitions from disk. Use after manually editing .yml files.",
|
|
|
+ args: {},
|
|
|
+ async execute() {
|
|
|
+ reloadWorkflows()
|
|
|
+ return `Workflows reloaded. ${store.loadDefs().length} active.`
|
|
|
+ },
|
|
|
+ }),
|
|
|
},
|
|
|
|
|
|
dispose: async () => {
|