openfox 2.0.113 → 2.0.115
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/CHANGELOG.md +43 -0
- package/dist/CHANGELOG.md +43 -0
- package/dist/agent-defaults/builder.agent.md +1 -0
- package/dist/agent-defaults/planner.agent.md +1 -0
- package/dist/{backend-P3GHHLHJ.js → backend-LVYHLGQJ.js} +2 -2
- package/dist/{chat-handler-PEK5JDHO.js → chat-handler-5SP265CR.js} +32 -26
- package/dist/{chunk-HKD4IMOE.js → chunk-27NABDXB.js} +2 -2
- package/dist/{chunk-FF2EKUQO.js → chunk-2FCLCUVR.js} +6 -6
- package/dist/{chunk-ONONXV3N.js → chunk-37QLXLML.js} +4 -4
- package/dist/{chunk-YB6WYVNY.js → chunk-3KBRMBTP.js} +2 -2
- package/dist/{chunk-YGPPFVLJ.js → chunk-3NPLERPQ.js} +17 -17
- package/dist/{chunk-XQR76MQ5.js → chunk-3PG65AXG.js} +18 -3
- package/dist/chunk-3QQAIUZI.js +957 -0
- package/dist/{chunk-PWXBIUEY.js → chunk-5PT4UMRR.js} +89 -1
- package/dist/chunk-5V3BF67X.js +7 -0
- package/dist/{chunk-6UPBZ2ZR.js → chunk-6KPJLELC.js} +4 -4
- package/dist/chunk-6RGACY4E.js +1081 -0
- package/dist/{chunk-BNFGDTEU.js → chunk-AESOGRFK.js} +2 -2
- package/dist/{chunk-YHEQTVFV.js → chunk-AZHWLLPP.js} +1 -1
- package/dist/{chunk-TCGWZBTS.js → chunk-BVVCRIVM.js} +4 -4
- package/dist/{chunk-EMI3YYLP.js → chunk-BWVO5WFK.js} +4 -4
- package/dist/{chunk-CP44U7BT.js → chunk-ERNWAKUB.js} +7 -7
- package/dist/{chunk-WWTYWLDM.js → chunk-HD6Y77RO.js} +2 -2
- package/dist/{chunk-4NMGX5F2.js → chunk-I7FY2LTR.js} +2 -2
- package/dist/{chunk-5VYSKVZT.js → chunk-JFK4GKQD.js} +321 -54
- package/dist/{chunk-43M7QZMX.js → chunk-JGWHRYWG.js} +4 -4
- package/dist/{chunk-Y64PEWKP.js → chunk-JLAR2CMV.js} +2 -2
- package/dist/{chunk-BYSUSSSC.js → chunk-LNNBLB6G.js} +48 -151
- package/dist/{chunk-V4664YKR.js → chunk-LTYL6ZGH.js} +2 -2
- package/dist/{chunk-R2MTWC7C.js → chunk-M6X6AIEG.js} +6 -4
- package/dist/{chunk-VGRVPYZ5.js → chunk-O6DU226D.js} +2 -2
- package/dist/{chunk-TEQEDOWI.js → chunk-OCBBAEMT.js} +288 -316
- package/dist/{chunk-SNLLOXCM.js → chunk-OCSTEGK7.js} +3 -3
- package/dist/{chunk-HGOTZWON.js → chunk-OKVHMKE2.js} +60 -1064
- package/dist/chunk-OSXPQSLW.js +128 -0
- package/dist/{chunk-O2YSYW2F.js → chunk-TFF5T3K3.js} +10 -10
- package/dist/{chunk-3R43CMLS.js → chunk-TGLVWAFC.js} +11 -11
- package/dist/{chunk-L737Y63F.js → chunk-TOKQ2UVG.js} +2 -2
- package/dist/{chunk-AZFZE36I.js → chunk-VWEATA7R.js} +18 -8
- package/dist/{chunk-BKCW3JLL.js → chunk-X25Q6RAV.js} +1 -1
- package/dist/{chunk-JR54MDZI.js → chunk-YX3OQBNX.js} +3 -3
- package/dist/{chunk-EC5T5A6O.js → chunk-ZTOG6LJD.js} +3 -3
- package/dist/cli/dev.js +1 -1
- package/dist/cli/index.js +1 -1
- package/dist/client-KE7EDI2D.js +16 -0
- package/dist/{client-pure-VFZIIK4S.js → client-pure-66Z6FWS6.js} +2 -2
- package/dist/command-defaults/mark-done.command.md +14 -0
- package/dist/compactor-SKVC2VNL.js +32 -0
- package/dist/{dynamic-context-F2XTQJR5.js → dynamic-context-QNB4ELLP.js} +10 -9
- package/dist/{events-224IO5NJ.js → events-5PLO4QJU.js} +9 -7
- package/dist/{folding-YTTTVS56.js → folding-CJWYFUKD.js} +7 -5
- package/dist/http-client-KLODRQC2.js +11 -0
- package/dist/{inspect-proxy-B7LGGTM7.js → inspect-proxy-K6QKEYDT.js} +4 -4
- package/dist/launch-EOXVWLOP.js +52 -0
- package/dist/{manager-4QGXI6WZ.js → manager-LZ3JQ443.js} +5 -5
- package/dist/{model-overrides-OY2YKUNV.js → model-overrides-5UVW3KTE.js} +4 -4
- package/dist/orchestrator-ED3KFPXO.js +63 -0
- package/dist/package.json +3 -3
- package/dist/{path-security-PFMZY7IA.js → path-security-B6EZC63L.js} +13 -11
- package/dist/{platform-QNGCKX4L.js → platform-6RPPBHKH.js} +4 -4
- package/dist/{processor-NYPJ5PFA.js → processor-226ZOTRI.js} +31 -25
- package/dist/{project-creator-AWP2JUFS.js → project-creator-VBFE35NI.js} +3 -3
- package/dist/{projects-C4353IQK.js → projects-AXHZOBR2.js} +5 -3
- package/dist/{protocol-CBT1d98i.d.ts → protocol-JdxRpPxM.d.ts} +21 -3
- package/dist/{protocol-LZZ7C4VQ.js → protocol-RKSWKNXI.js} +3 -3
- package/dist/provider/index.d.ts +4 -4
- package/dist/{provider-PIRPN2CC.js → provider-DL4ZWQY7.js} +8 -8
- package/dist/{provider-manager-GNEPR6UN.js → provider-manager-6CLRMGDE.js} +8 -8
- package/dist/{registry-VCKDSVKA.js → registry-5XI5NDBF.js} +6 -4
- package/dist/{serve-6DRHQLCG.js → serve-DARV4NJG.js} +36 -33
- package/dist/server/index.d.ts +13 -6
- package/dist/server/index.js +35 -32
- package/dist/server-LQJ7MBDS.js +55 -0
- package/dist/service-4JKTGPRG.js +25 -0
- package/dist/{session-overrides-RNKV35MU.js → session-overrides-AZKV6BRD.js} +8 -6
- package/dist/{sessions-D5CEPQGU.js → sessions-752IZIIK.js} +7 -5
- package/dist/{settings-BNRNX6IW.js → settings-2AHDKCD7.js} +3 -3
- package/dist/shared/index.d.ts +3 -3
- package/dist/shared/index.js +1 -1
- package/dist/skill-defaults/workflows/SKILL.md +24 -6
- package/dist/tasks-XJXHU623.js +217 -0
- package/dist/{tools-2B363F7L.js → tools-QYYNFMGU.js} +31 -23
- package/dist/{types-cVfqOj6M.d.ts → types-CIVhQMvD.d.ts} +1 -1
- package/dist/{types-BAxh20ZL.d.ts → types-Df7NM9b5.d.ts} +76 -1
- package/dist/{update-IQMRYN7J.js → update-2REP2WB6.js} +2 -2
- package/dist/web/assets/{index-CjWC6EQ4.css → index-BmILAZrT.css} +1 -1
- package/dist/web/assets/index-M7ToECa4.js +324 -0
- package/dist/web/index.html +2 -2
- package/dist/web/sw.js +1 -1
- package/dist/workflow-defaults/default.workflow.json +5 -3
- package/dist/{workspace-YUNHV4WS.js → workspace-RMZVTFJD.js} +4 -4
- package/package.json +3 -3
- package/dist/chunk-B2PHPBFU.js +0 -7
- package/dist/client-3LJGD6KY.js +0 -16
- package/dist/compactor-ZBNKANHV.js +0 -30
- package/dist/http-client-YC3UA5OZ.js +0 -11
- package/dist/orchestrator-JKBY7B3P.js +0 -57
- package/dist/server-HUHFJW7U.js +0 -48
- package/dist/web/assets/index-DBUtUSHK.js +0 -323
|
@@ -0,0 +1,1081 @@
|
|
|
1
|
+
import {
|
|
2
|
+
deleteItemFromDir,
|
|
3
|
+
getDefaultIds,
|
|
4
|
+
jsonSerializer,
|
|
5
|
+
loadItemsFromDir,
|
|
6
|
+
pathExists,
|
|
7
|
+
saveItemToDir
|
|
8
|
+
} from "./chunk-OSXPQSLW.js";
|
|
9
|
+
import {
|
|
10
|
+
getProject
|
|
11
|
+
} from "./chunk-3PG65AXG.js";
|
|
12
|
+
import {
|
|
13
|
+
parseDefaultModelSelection
|
|
14
|
+
} from "./chunk-BVVCRIVM.js";
|
|
15
|
+
import {
|
|
16
|
+
getDatabase
|
|
17
|
+
} from "./chunk-5PT4UMRR.js";
|
|
18
|
+
import {
|
|
19
|
+
logger
|
|
20
|
+
} from "./chunk-K44MW7JJ.js";
|
|
21
|
+
|
|
22
|
+
// src/server/commands/registry.ts
|
|
23
|
+
import { writeFile, mkdir, unlink } from "fs/promises";
|
|
24
|
+
import { join, dirname } from "path";
|
|
25
|
+
import { fileURLToPath } from "url";
|
|
26
|
+
import matter from "gray-matter";
|
|
27
|
+
var __bundleDir = dirname(fileURLToPath(import.meta.url));
|
|
28
|
+
var DEFAULTS_DIR = join(__bundleDir, "defaults");
|
|
29
|
+
var DEFAULTS_DIR_ALT = join(__bundleDir, "command-defaults");
|
|
30
|
+
var COMMAND_EXTENSION = ".command.md";
|
|
31
|
+
function getCommandsDir(configDir) {
|
|
32
|
+
return join(configDir, "commands");
|
|
33
|
+
}
|
|
34
|
+
function getProjectCommandsDir(projectDir) {
|
|
35
|
+
return join(projectDir, ".openfox", "commands");
|
|
36
|
+
}
|
|
37
|
+
async function loadDefaultCommands() {
|
|
38
|
+
let defaults = await loadItemsFromDir(DEFAULTS_DIR, {
|
|
39
|
+
extension: COMMAND_EXTENSION,
|
|
40
|
+
logName: "command"
|
|
41
|
+
});
|
|
42
|
+
if (!defaults.length) {
|
|
43
|
+
defaults = await loadItemsFromDir(DEFAULTS_DIR_ALT, {
|
|
44
|
+
extension: COMMAND_EXTENSION,
|
|
45
|
+
logName: "command"
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
return defaults;
|
|
49
|
+
}
|
|
50
|
+
async function loadUserCommands(configDir) {
|
|
51
|
+
return loadItemsFromDir(getCommandsDir(configDir), {
|
|
52
|
+
extension: COMMAND_EXTENSION,
|
|
53
|
+
logName: "command"
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
async function loadProjectCommands(projectDir) {
|
|
57
|
+
return loadItemsFromDir(getProjectCommandsDir(projectDir), {
|
|
58
|
+
extension: COMMAND_EXTENSION,
|
|
59
|
+
logName: "command"
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
async function loadAllCommands(configDir, projectDir) {
|
|
63
|
+
const [defaultCommands, userCommands] = await Promise.all([loadDefaultCommands(), loadUserCommands(configDir)]);
|
|
64
|
+
const commandMap = /* @__PURE__ */ new Map();
|
|
65
|
+
for (const cmd of defaultCommands) {
|
|
66
|
+
commandMap.set(cmd.metadata.id, cmd);
|
|
67
|
+
}
|
|
68
|
+
for (const cmd of userCommands) {
|
|
69
|
+
commandMap.set(cmd.metadata.id, cmd);
|
|
70
|
+
}
|
|
71
|
+
if (projectDir) {
|
|
72
|
+
const projectCommands = await loadProjectCommands(projectDir);
|
|
73
|
+
for (const cmd of projectCommands) {
|
|
74
|
+
commandMap.set(cmd.metadata.id, cmd);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return Array.from(commandMap.values());
|
|
78
|
+
}
|
|
79
|
+
async function getDefaultCommandIds() {
|
|
80
|
+
const ids = await getDefaultIds(DEFAULTS_DIR, COMMAND_EXTENSION);
|
|
81
|
+
if (ids.length) return ids;
|
|
82
|
+
return getDefaultIds(DEFAULTS_DIR_ALT, COMMAND_EXTENSION);
|
|
83
|
+
}
|
|
84
|
+
async function isDefaultCommand(commandId) {
|
|
85
|
+
const defaultIds = await getDefaultCommandIds();
|
|
86
|
+
return defaultIds.includes(commandId);
|
|
87
|
+
}
|
|
88
|
+
function findCommandById(commandId, commands) {
|
|
89
|
+
return commands.find((c) => c.metadata.id === commandId);
|
|
90
|
+
}
|
|
91
|
+
async function commandExists(configDir, commandId, projectDir) {
|
|
92
|
+
if (await pathExists(join(getCommandsDir(configDir), `${commandId}${COMMAND_EXTENSION}`))) return true;
|
|
93
|
+
if (projectDir && await pathExists(join(getProjectCommandsDir(projectDir), `${commandId}${COMMAND_EXTENSION}`)))
|
|
94
|
+
return true;
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
async function saveCommand(configDir, command) {
|
|
98
|
+
const commandsDir = getCommandsDir(configDir);
|
|
99
|
+
if (!await pathExists(commandsDir)) {
|
|
100
|
+
await mkdir(commandsDir, { recursive: true });
|
|
101
|
+
}
|
|
102
|
+
const filePath = join(commandsDir, `${command.metadata.id}${COMMAND_EXTENSION}`);
|
|
103
|
+
const content = matter.stringify(command.prompt, command.metadata);
|
|
104
|
+
await writeFile(filePath, content, "utf-8");
|
|
105
|
+
}
|
|
106
|
+
async function saveCommandToProject(projectDir, command) {
|
|
107
|
+
await saveItemToDir(
|
|
108
|
+
getProjectCommandsDir(projectDir),
|
|
109
|
+
command,
|
|
110
|
+
COMMAND_EXTENSION,
|
|
111
|
+
(c) => matter.stringify(c.prompt, c.metadata)
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
async function deleteProjectCommand(projectDir, commandId) {
|
|
115
|
+
return deleteItemFromDir(getProjectCommandsDir(projectDir), commandId, COMMAND_EXTENSION);
|
|
116
|
+
}
|
|
117
|
+
async function deleteCommand(configDir, commandId) {
|
|
118
|
+
const filePath = join(getCommandsDir(configDir), `${commandId}${COMMAND_EXTENSION}`);
|
|
119
|
+
if (!await pathExists(filePath)) {
|
|
120
|
+
if (await isDefaultCommand(commandId)) {
|
|
121
|
+
return { success: false, reason: "Cannot delete built-in defaults" };
|
|
122
|
+
}
|
|
123
|
+
return { success: false };
|
|
124
|
+
}
|
|
125
|
+
try {
|
|
126
|
+
await unlink(filePath);
|
|
127
|
+
return { success: true };
|
|
128
|
+
} catch {
|
|
129
|
+
return { success: false, reason: "Failed to delete command" };
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// src/server/workflows/registry.ts
|
|
134
|
+
import { readdir, readFile, writeFile as writeFile2, mkdir as mkdir2, access, unlink as unlink2 } from "fs/promises";
|
|
135
|
+
import { join as join2, dirname as dirname2 } from "path";
|
|
136
|
+
import { constants } from "fs";
|
|
137
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
138
|
+
var __bundleDir2 = dirname2(fileURLToPath2(import.meta.url));
|
|
139
|
+
var DEFAULTS_DIR2 = join2(__bundleDir2, "defaults");
|
|
140
|
+
var DEFAULTS_DIR_ALT2 = join2(__bundleDir2, "workflow-defaults");
|
|
141
|
+
var WORKFLOW_EXTENSION = ".workflow.json";
|
|
142
|
+
function normalizeWorkflowScope(value) {
|
|
143
|
+
return typeof value === "string" && (value === "builtin" || value === "user" || value === "project" || value === "auto") ? value : "auto";
|
|
144
|
+
}
|
|
145
|
+
function getWorkflowsDir(configDir) {
|
|
146
|
+
return join2(configDir, "workflows");
|
|
147
|
+
}
|
|
148
|
+
function getProjectWorkflowsDir(projectDir) {
|
|
149
|
+
return join2(projectDir, ".openfox", "workflows");
|
|
150
|
+
}
|
|
151
|
+
async function pathExists2(path) {
|
|
152
|
+
try {
|
|
153
|
+
await access(path, constants.R_OK);
|
|
154
|
+
return true;
|
|
155
|
+
} catch {
|
|
156
|
+
return false;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
async function loadWorkflowsFromDir(dir) {
|
|
160
|
+
if (!await pathExists2(dir)) {
|
|
161
|
+
return [];
|
|
162
|
+
}
|
|
163
|
+
let files;
|
|
164
|
+
try {
|
|
165
|
+
files = (await readdir(dir)).filter((f) => f.endsWith(WORKFLOW_EXTENSION));
|
|
166
|
+
} catch {
|
|
167
|
+
return [];
|
|
168
|
+
}
|
|
169
|
+
const workflows = [];
|
|
170
|
+
for (const file of files) {
|
|
171
|
+
try {
|
|
172
|
+
const raw = await readFile(join2(dir, file), "utf-8");
|
|
173
|
+
const parsed = JSON.parse(raw);
|
|
174
|
+
if (parsed.metadata?.id && parsed.steps?.length > 0) {
|
|
175
|
+
workflows.push(parsed);
|
|
176
|
+
} else {
|
|
177
|
+
logger.warn("Skipping invalid workflow file", { file });
|
|
178
|
+
}
|
|
179
|
+
} catch (err) {
|
|
180
|
+
logger.warn("Failed to parse workflow file", { file, error: err instanceof Error ? err.message : String(err) });
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
return workflows;
|
|
184
|
+
}
|
|
185
|
+
async function loadDefaultWorkflows() {
|
|
186
|
+
let defaults = await loadWorkflowsFromDir(DEFAULTS_DIR2);
|
|
187
|
+
if (!defaults.length) {
|
|
188
|
+
defaults = await loadWorkflowsFromDir(DEFAULTS_DIR_ALT2);
|
|
189
|
+
}
|
|
190
|
+
return defaults;
|
|
191
|
+
}
|
|
192
|
+
async function loadUserWorkflows(configDir) {
|
|
193
|
+
return loadWorkflowsFromDir(getWorkflowsDir(configDir));
|
|
194
|
+
}
|
|
195
|
+
async function loadProjectWorkflows(projectDir) {
|
|
196
|
+
return loadWorkflowsFromDir(getProjectWorkflowsDir(projectDir));
|
|
197
|
+
}
|
|
198
|
+
async function loadAllWorkflows(configDir, projectDir) {
|
|
199
|
+
const [defaultWorkflows, userWorkflows] = await Promise.all([loadDefaultWorkflows(), loadUserWorkflows(configDir)]);
|
|
200
|
+
const workflowMap = /* @__PURE__ */ new Map();
|
|
201
|
+
for (const workflow of defaultWorkflows) {
|
|
202
|
+
workflowMap.set(workflow.metadata.id, workflow);
|
|
203
|
+
}
|
|
204
|
+
for (const workflow of userWorkflows) {
|
|
205
|
+
workflowMap.set(workflow.metadata.id, workflow);
|
|
206
|
+
}
|
|
207
|
+
if (projectDir) {
|
|
208
|
+
const projectWorkflows = await loadProjectWorkflows(projectDir);
|
|
209
|
+
for (const workflow of projectWorkflows) {
|
|
210
|
+
workflowMap.set(workflow.metadata.id, workflow);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
return Array.from(workflowMap.values());
|
|
214
|
+
}
|
|
215
|
+
async function saveWorkflowToProject(projectDir, workflow) {
|
|
216
|
+
await saveItemToDir(getProjectWorkflowsDir(projectDir), workflow, WORKFLOW_EXTENSION, jsonSerializer);
|
|
217
|
+
}
|
|
218
|
+
async function deleteProjectWorkflow(projectDir, workflowId) {
|
|
219
|
+
return deleteItemFromDir(getProjectWorkflowsDir(projectDir), workflowId, WORKFLOW_EXTENSION);
|
|
220
|
+
}
|
|
221
|
+
async function getDefaultWorkflowIds() {
|
|
222
|
+
for (const dir of [DEFAULTS_DIR2, DEFAULTS_DIR_ALT2]) {
|
|
223
|
+
try {
|
|
224
|
+
const files = (await readdir(dir)).filter((f) => f.endsWith(WORKFLOW_EXTENSION));
|
|
225
|
+
return files.map((f) => f.replace(WORKFLOW_EXTENSION, ""));
|
|
226
|
+
} catch {
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
return [];
|
|
230
|
+
}
|
|
231
|
+
async function isDefaultWorkflow(workflowId) {
|
|
232
|
+
const defaultIds = await getDefaultWorkflowIds();
|
|
233
|
+
return defaultIds.includes(workflowId);
|
|
234
|
+
}
|
|
235
|
+
function findWorkflowById(workflowId, workflows) {
|
|
236
|
+
return workflows.find((p) => p.metadata.id === workflowId);
|
|
237
|
+
}
|
|
238
|
+
async function workflowExists(configDir, workflowId, projectDir) {
|
|
239
|
+
if (await pathExists2(join2(getWorkflowsDir(configDir), `${workflowId}${WORKFLOW_EXTENSION}`))) return true;
|
|
240
|
+
if (projectDir && await pathExists2(join2(getProjectWorkflowsDir(projectDir), `${workflowId}${WORKFLOW_EXTENSION}`)))
|
|
241
|
+
return true;
|
|
242
|
+
return false;
|
|
243
|
+
}
|
|
244
|
+
async function saveWorkflow(configDir, workflow) {
|
|
245
|
+
const workflowsDir = getWorkflowsDir(configDir);
|
|
246
|
+
if (!await pathExists2(workflowsDir)) {
|
|
247
|
+
await mkdir2(workflowsDir, { recursive: true });
|
|
248
|
+
}
|
|
249
|
+
const filePath = join2(workflowsDir, `${workflow.metadata.id}${WORKFLOW_EXTENSION}`);
|
|
250
|
+
await writeFile2(filePath, JSON.stringify(workflow, null, 2) + "\n", "utf-8");
|
|
251
|
+
}
|
|
252
|
+
async function deleteWorkflow(configDir, workflowId) {
|
|
253
|
+
const isDefault = await isDefaultWorkflow(workflowId);
|
|
254
|
+
if (isDefault) {
|
|
255
|
+
return { success: false, reason: "Cannot delete built-in defaults" };
|
|
256
|
+
}
|
|
257
|
+
const filePath = join2(getWorkflowsDir(configDir), `${workflowId}${WORKFLOW_EXTENSION}`);
|
|
258
|
+
try {
|
|
259
|
+
await unlink2(filePath);
|
|
260
|
+
return { success: true };
|
|
261
|
+
} catch {
|
|
262
|
+
return { success: false };
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// src/server/tasks/slash.ts
|
|
267
|
+
function parseSlashInvocation(prompt) {
|
|
268
|
+
const trimmed = prompt.trim();
|
|
269
|
+
if (!trimmed.startsWith("/")) return null;
|
|
270
|
+
const parts = trimmed.slice(1).split(/\s+/);
|
|
271
|
+
const id = parts[0];
|
|
272
|
+
if (!id) return null;
|
|
273
|
+
return { id, args: parts.slice(1) };
|
|
274
|
+
}
|
|
275
|
+
function workflowParamsFromArgs(parameters, args) {
|
|
276
|
+
const params = {};
|
|
277
|
+
if (!parameters || parameters.length === 0) {
|
|
278
|
+
args.forEach((value, index) => {
|
|
279
|
+
params[String(index)] = value;
|
|
280
|
+
});
|
|
281
|
+
return params;
|
|
282
|
+
}
|
|
283
|
+
const sorted = [...parameters].sort((a, b) => (a.position ?? 0) - (b.position ?? 0));
|
|
284
|
+
sorted.forEach((param, index) => {
|
|
285
|
+
const value = args[index];
|
|
286
|
+
if (value !== void 0) params[param.id] = value;
|
|
287
|
+
});
|
|
288
|
+
return params;
|
|
289
|
+
}
|
|
290
|
+
function extractTemplateParams(template) {
|
|
291
|
+
const seen = [];
|
|
292
|
+
const regex = /\{\{(\w+)\}\}/g;
|
|
293
|
+
let match;
|
|
294
|
+
while ((match = regex.exec(template)) !== null) {
|
|
295
|
+
const key = match[1];
|
|
296
|
+
if (!seen.includes(key)) seen.push(key);
|
|
297
|
+
}
|
|
298
|
+
return seen;
|
|
299
|
+
}
|
|
300
|
+
function expandCommandPrompt(template, args) {
|
|
301
|
+
const paramNames = extractTemplateParams(template);
|
|
302
|
+
const named = {};
|
|
303
|
+
paramNames.forEach((name, index) => {
|
|
304
|
+
const value = args[index];
|
|
305
|
+
if (value !== void 0) named[name] = value;
|
|
306
|
+
});
|
|
307
|
+
let prompt = template;
|
|
308
|
+
for (const [key, value] of Object.entries(named)) {
|
|
309
|
+
prompt = prompt.replaceAll(`{{${key}}}`, value);
|
|
310
|
+
}
|
|
311
|
+
const unfilledParams = paramNames.filter((name) => !(name in named));
|
|
312
|
+
return { prompt, unfilledParams };
|
|
313
|
+
}
|
|
314
|
+
async function resolveSlashLaunch(configDir, projectDir, prompt) {
|
|
315
|
+
const invocation = parseSlashInvocation(prompt);
|
|
316
|
+
if (!invocation) return null;
|
|
317
|
+
const workflows = await loadAllWorkflows(configDir, projectDir);
|
|
318
|
+
const workflow = findWorkflowById(invocation.id, workflows);
|
|
319
|
+
if (workflow) {
|
|
320
|
+
const params = workflowParamsFromArgs(workflow.metadata.parameters, invocation.args);
|
|
321
|
+
const required = (workflow.metadata.parameters ?? []).filter((p) => p.required).map((p) => p.id);
|
|
322
|
+
const missingRequired = required.filter((id) => !(id in params));
|
|
323
|
+
if (missingRequired.length > 0) {
|
|
324
|
+
return null;
|
|
325
|
+
}
|
|
326
|
+
return {
|
|
327
|
+
kind: "workflow",
|
|
328
|
+
workflowId: workflow.metadata.id,
|
|
329
|
+
params
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
const commands = await loadAllCommands(configDir, projectDir);
|
|
333
|
+
const command = findCommandById(invocation.id, commands);
|
|
334
|
+
if (!command) return null;
|
|
335
|
+
const expanded = expandCommandPrompt(command.prompt, invocation.args);
|
|
336
|
+
if (expanded.unfilledParams.length > 0) return null;
|
|
337
|
+
return {
|
|
338
|
+
kind: "command",
|
|
339
|
+
prompt: expanded.prompt,
|
|
340
|
+
...command.metadata.agentMode ? { agentMode: command.metadata.agentMode } : {}
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// src/server/db/tasks.ts
|
|
345
|
+
function createTask(projectId, input) {
|
|
346
|
+
const db = getDatabase();
|
|
347
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
348
|
+
const id = crypto.randomUUID();
|
|
349
|
+
const position = db.prepare(`SELECT COALESCE(MAX(position), -1) + 1 AS pos FROM tasks WHERE project_id = ? AND status = 'todo'`).get(projectId);
|
|
350
|
+
db.prepare(
|
|
351
|
+
`INSERT INTO tasks (id, project_id, prompt, attachments, status, run_state, position, version, agent_id, provider_id, model, created_at, updated_at)
|
|
352
|
+
VALUES (?, ?, ?, ?, 'todo', NULL, ?, 1, ?, ?, ?, ?, ?)`
|
|
353
|
+
).run(
|
|
354
|
+
id,
|
|
355
|
+
projectId,
|
|
356
|
+
input.prompt,
|
|
357
|
+
JSON.stringify(input.attachments ?? []),
|
|
358
|
+
position.pos,
|
|
359
|
+
input.agentId ?? null,
|
|
360
|
+
input.providerId ?? null,
|
|
361
|
+
input.model ?? null,
|
|
362
|
+
now,
|
|
363
|
+
now
|
|
364
|
+
);
|
|
365
|
+
return getTask(id);
|
|
366
|
+
}
|
|
367
|
+
function getTask(id) {
|
|
368
|
+
const db = getDatabase();
|
|
369
|
+
const row = db.prepare(`SELECT * FROM tasks WHERE id = ?`).get(id);
|
|
370
|
+
if (!row) return null;
|
|
371
|
+
return hydrateTask(row);
|
|
372
|
+
}
|
|
373
|
+
function listTasks(projectId) {
|
|
374
|
+
const db = getDatabase();
|
|
375
|
+
const rows = db.prepare(`SELECT * FROM tasks WHERE project_id = ? ORDER BY position ASC`).all(projectId);
|
|
376
|
+
return rows.map(hydrateTask);
|
|
377
|
+
}
|
|
378
|
+
function updateTask(id, patch) {
|
|
379
|
+
const db = getDatabase();
|
|
380
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
381
|
+
const sets = ["updated_at = ?", "version = version + 1"];
|
|
382
|
+
const values = [now];
|
|
383
|
+
if (patch.prompt !== void 0) {
|
|
384
|
+
sets.push("prompt = ?");
|
|
385
|
+
values.push(patch.prompt);
|
|
386
|
+
}
|
|
387
|
+
if (patch.attachments !== void 0) {
|
|
388
|
+
sets.push("attachments = ?");
|
|
389
|
+
values.push(JSON.stringify(patch.attachments));
|
|
390
|
+
}
|
|
391
|
+
if (patch.agentId !== void 0) {
|
|
392
|
+
sets.push("agent_id = ?");
|
|
393
|
+
values.push(patch.agentId);
|
|
394
|
+
}
|
|
395
|
+
if (patch.providerId !== void 0) {
|
|
396
|
+
sets.push("provider_id = ?");
|
|
397
|
+
values.push(patch.providerId);
|
|
398
|
+
}
|
|
399
|
+
if (patch.model !== void 0) {
|
|
400
|
+
sets.push("model = ?");
|
|
401
|
+
values.push(patch.model);
|
|
402
|
+
}
|
|
403
|
+
values.push(id);
|
|
404
|
+
db.prepare(`UPDATE tasks SET ${sets.join(", ")} WHERE id = ?`).run(...values);
|
|
405
|
+
return getTask(id);
|
|
406
|
+
}
|
|
407
|
+
function setTaskStatus(id, status) {
|
|
408
|
+
const db = getDatabase();
|
|
409
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
410
|
+
db.prepare(`UPDATE tasks SET status = ?, run_state = NULL, version = version + 1, updated_at = ? WHERE id = ?`).run(
|
|
411
|
+
status,
|
|
412
|
+
now,
|
|
413
|
+
id
|
|
414
|
+
);
|
|
415
|
+
}
|
|
416
|
+
function setTaskRunState(id, runState) {
|
|
417
|
+
const db = getDatabase();
|
|
418
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
419
|
+
db.prepare(`UPDATE tasks SET run_state = ?, version = version + 1, updated_at = ? WHERE id = ?`).run(
|
|
420
|
+
runState,
|
|
421
|
+
now,
|
|
422
|
+
id
|
|
423
|
+
);
|
|
424
|
+
}
|
|
425
|
+
function deleteTask(id) {
|
|
426
|
+
const db = getDatabase();
|
|
427
|
+
db.prepare(`DELETE FROM tasks WHERE id = ?`).run(id);
|
|
428
|
+
}
|
|
429
|
+
function appendToBottom(id, status) {
|
|
430
|
+
const db = getDatabase();
|
|
431
|
+
const task = getTask(id);
|
|
432
|
+
if (!task) return;
|
|
433
|
+
const row = db.prepare(`SELECT COALESCE(MAX(position), -1) + 1 AS pos FROM tasks WHERE project_id = ? AND status = ?`).get(task.projectId, status);
|
|
434
|
+
db.prepare(`UPDATE tasks SET position = ?, version = version + 1, updated_at = ? WHERE id = ?`).run(
|
|
435
|
+
row.pos,
|
|
436
|
+
(/* @__PURE__ */ new Date()).toISOString(),
|
|
437
|
+
id
|
|
438
|
+
);
|
|
439
|
+
}
|
|
440
|
+
function cloneTask(sourceId) {
|
|
441
|
+
const source = getTask(sourceId);
|
|
442
|
+
if (!source) return null;
|
|
443
|
+
return createTask(source.projectId, {
|
|
444
|
+
prompt: source.prompt,
|
|
445
|
+
attachments: source.attachments,
|
|
446
|
+
...source.agentId ? { agentId: source.agentId } : {},
|
|
447
|
+
...source.providerId ? { providerId: source.providerId } : {},
|
|
448
|
+
...source.model ? { model: source.model } : {}
|
|
449
|
+
});
|
|
450
|
+
}
|
|
451
|
+
function reorderTask(id, status, toIndex) {
|
|
452
|
+
const db = getDatabase();
|
|
453
|
+
const task = getTask(id);
|
|
454
|
+
if (!task) return;
|
|
455
|
+
const rows = db.prepare(`SELECT id FROM tasks WHERE project_id = ? AND status = ? ORDER BY position ASC, created_at ASC`).all(task.projectId, status);
|
|
456
|
+
const ids = rows.map((r) => r.id).filter((tid) => tid !== id);
|
|
457
|
+
const clamped = Math.max(0, Math.min(toIndex, ids.length));
|
|
458
|
+
ids.splice(clamped, 0, id);
|
|
459
|
+
const update = db.prepare(`UPDATE tasks SET position = ?, version = version + 1, updated_at = ? WHERE id = ?`);
|
|
460
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
461
|
+
for (const [idx, tid] of ids.entries()) {
|
|
462
|
+
update.run(idx, now, tid);
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
function addTaskLink(taskId, sessionId, active) {
|
|
466
|
+
const db = getDatabase();
|
|
467
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
468
|
+
db.prepare(`INSERT OR IGNORE INTO task_links (task_id, session_id, active, created_at) VALUES (?, ?, ?, ?)`).run(
|
|
469
|
+
taskId,
|
|
470
|
+
sessionId,
|
|
471
|
+
active ? 1 : 0,
|
|
472
|
+
now
|
|
473
|
+
);
|
|
474
|
+
if (active) {
|
|
475
|
+
db.prepare(`UPDATE task_links SET active = 0 WHERE task_id = ? AND session_id != ?`).run(taskId, sessionId);
|
|
476
|
+
db.prepare(`UPDATE task_links SET active = 1 WHERE task_id = ? AND session_id = ?`).run(taskId, sessionId);
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
function listTaskLinks(taskId) {
|
|
480
|
+
const db = getDatabase();
|
|
481
|
+
return db.prepare(`SELECT * FROM task_links WHERE task_id = ? ORDER BY created_at ASC`).all(taskId);
|
|
482
|
+
}
|
|
483
|
+
function clearActiveTaskLink(taskId) {
|
|
484
|
+
const db = getDatabase();
|
|
485
|
+
db.prepare(`UPDATE task_links SET active = 0 WHERE task_id = ?`).run(taskId);
|
|
486
|
+
}
|
|
487
|
+
function removeTaskLinks(taskId, sessionId) {
|
|
488
|
+
const db = getDatabase();
|
|
489
|
+
if (sessionId) {
|
|
490
|
+
db.prepare(`DELETE FROM task_links WHERE task_id = ? AND session_id = ?`).run(taskId, sessionId);
|
|
491
|
+
} else {
|
|
492
|
+
db.prepare(`DELETE FROM task_links WHERE task_id = ?`).run(taskId);
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
function getGateConfig(projectId) {
|
|
496
|
+
const db = getDatabase();
|
|
497
|
+
const rows = db.prepare(`SELECT * FROM task_gates WHERE project_id = ? ORDER BY sort_order ASC, created_at ASC`).all(projectId);
|
|
498
|
+
return rows.map((r) => ({
|
|
499
|
+
id: r.id,
|
|
500
|
+
name: r.name,
|
|
501
|
+
description: r.description,
|
|
502
|
+
required: Boolean(r.required),
|
|
503
|
+
variant: r.variant === "ready" ? "ready" : "done"
|
|
504
|
+
}));
|
|
505
|
+
}
|
|
506
|
+
function replaceGateConfig(projectId, gates) {
|
|
507
|
+
const db = getDatabase();
|
|
508
|
+
const tx = db.transaction(() => {
|
|
509
|
+
db.prepare(`DELETE FROM task_gates WHERE project_id = ?`).run(projectId);
|
|
510
|
+
const insert = db.prepare(
|
|
511
|
+
`INSERT INTO task_gates (id, project_id, name, description, required, variant, sort_order, created_at)
|
|
512
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`
|
|
513
|
+
);
|
|
514
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
515
|
+
gates.forEach((gate, i) => {
|
|
516
|
+
insert.run(gate.id, projectId, gate.name, gate.description, gate.required ? 1 : 0, gate.variant, i, now);
|
|
517
|
+
});
|
|
518
|
+
const keepIds = gates.map((g) => g.id);
|
|
519
|
+
const prune = db.prepare(
|
|
520
|
+
`DELETE FROM task_gate_values
|
|
521
|
+
WHERE task_id IN (SELECT id FROM tasks WHERE project_id = ?)
|
|
522
|
+
AND gate_id NOT IN (${keepIds.map(() => "?").join(", ") || "''"})`
|
|
523
|
+
);
|
|
524
|
+
prune.run(projectId, ...keepIds);
|
|
525
|
+
});
|
|
526
|
+
tx();
|
|
527
|
+
return getGateConfig(projectId);
|
|
528
|
+
}
|
|
529
|
+
function getGateValues(taskId) {
|
|
530
|
+
const db = getDatabase();
|
|
531
|
+
const rows = db.prepare(`SELECT * FROM task_gate_values WHERE task_id = ? ORDER BY created_at ASC`).all(taskId);
|
|
532
|
+
return rows.map((r) => ({
|
|
533
|
+
gateId: r.gate_id,
|
|
534
|
+
value: r.value,
|
|
535
|
+
actor: r.actor,
|
|
536
|
+
...r.actor_name ? { actorName: r.actor_name } : {},
|
|
537
|
+
...r.session_id ? { sessionId: r.session_id } : {},
|
|
538
|
+
timestamp: r.created_at
|
|
539
|
+
}));
|
|
540
|
+
}
|
|
541
|
+
function setGateValue(taskId, gateId, value, actor, actorName, sessionId) {
|
|
542
|
+
const db = getDatabase();
|
|
543
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
544
|
+
db.prepare(`DELETE FROM task_gate_values WHERE task_id = ? AND gate_id = ?`).run(taskId, gateId);
|
|
545
|
+
db.prepare(
|
|
546
|
+
`INSERT INTO task_gate_values (id, task_id, gate_id, value, actor, actor_name, session_id, created_at)
|
|
547
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`
|
|
548
|
+
).run(crypto.randomUUID(), taskId, gateId, value, actor, actorName ?? null, sessionId ?? null, now);
|
|
549
|
+
db.prepare(`UPDATE tasks SET version = version + 1, updated_at = ? WHERE id = ?`).run(now, taskId);
|
|
550
|
+
}
|
|
551
|
+
function addAuditEntry(taskId, actor, action, detail, actorName) {
|
|
552
|
+
const db = getDatabase();
|
|
553
|
+
db.prepare(
|
|
554
|
+
`INSERT INTO task_audit (id, task_id, actor, actor_name, action, detail, created_at)
|
|
555
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)`
|
|
556
|
+
).run(crypto.randomUUID(), taskId, actor, actorName ?? null, action, detail, (/* @__PURE__ */ new Date()).toISOString());
|
|
557
|
+
}
|
|
558
|
+
function getAuditTrail(taskId) {
|
|
559
|
+
const db = getDatabase();
|
|
560
|
+
const rows = db.prepare(`SELECT * FROM task_audit WHERE task_id = ? ORDER BY created_at ASC`).all(taskId);
|
|
561
|
+
return rows.map((r) => ({
|
|
562
|
+
id: r.id,
|
|
563
|
+
timestamp: r.created_at,
|
|
564
|
+
actor: r.actor,
|
|
565
|
+
...r.actor_name ? { actorName: r.actor_name } : {},
|
|
566
|
+
action: r.action,
|
|
567
|
+
detail: r.detail
|
|
568
|
+
}));
|
|
569
|
+
}
|
|
570
|
+
function getTaskSettings(projectId) {
|
|
571
|
+
const db = getDatabase();
|
|
572
|
+
const row = db.prepare(`SELECT * FROM project_task_settings WHERE project_id = ?`).get(projectId);
|
|
573
|
+
if (!row) return { slotLimit: 1, queuePaused: false };
|
|
574
|
+
return { slotLimit: row.slot_limit, queuePaused: Boolean(row.queue_paused) };
|
|
575
|
+
}
|
|
576
|
+
function setTaskSettings(projectId, settings) {
|
|
577
|
+
const db = getDatabase();
|
|
578
|
+
const current = getTaskSettings(projectId);
|
|
579
|
+
const slotLimit = settings.slotLimit ?? current.slotLimit;
|
|
580
|
+
const queuePaused = settings.queuePaused ?? current.queuePaused;
|
|
581
|
+
db.prepare(
|
|
582
|
+
`INSERT INTO project_task_settings (project_id, slot_limit, queue_paused) VALUES (?, ?, ?)
|
|
583
|
+
ON CONFLICT(project_id) DO UPDATE SET slot_limit = excluded.slot_limit, queue_paused = excluded.queue_paused`
|
|
584
|
+
).run(projectId, slotLimit, queuePaused ? 1 : 0);
|
|
585
|
+
return { slotLimit, queuePaused };
|
|
586
|
+
}
|
|
587
|
+
function hydrateTask(row) {
|
|
588
|
+
const links = listTaskLinks(row.id);
|
|
589
|
+
const sessionIds = links.map((l) => l.session_id);
|
|
590
|
+
const activeLink = links.find((l) => Boolean(l.active));
|
|
591
|
+
const runState = row.status === "in_progress" ? row.run_state : void 0;
|
|
592
|
+
const task = {
|
|
593
|
+
id: row.id,
|
|
594
|
+
projectId: row.project_id,
|
|
595
|
+
prompt: row.prompt,
|
|
596
|
+
attachments: safeParseAttachments(row.attachments),
|
|
597
|
+
status: row.status,
|
|
598
|
+
...runState ? { runState } : {},
|
|
599
|
+
position: row.position,
|
|
600
|
+
version: row.version,
|
|
601
|
+
...row.agent_id ? { agentId: row.agent_id } : {},
|
|
602
|
+
...row.provider_id ? { providerId: row.provider_id } : {},
|
|
603
|
+
...row.model ? { model: row.model } : {},
|
|
604
|
+
sessionIds,
|
|
605
|
+
...activeLink ? { activeSessionId: activeLink.session_id } : {},
|
|
606
|
+
gateValues: getGateValues(row.id),
|
|
607
|
+
auditTrail: getAuditTrail(row.id),
|
|
608
|
+
createdAt: row.created_at,
|
|
609
|
+
updatedAt: row.updated_at
|
|
610
|
+
};
|
|
611
|
+
return withQueuePosition(task);
|
|
612
|
+
}
|
|
613
|
+
function withQueuePosition(task) {
|
|
614
|
+
if (task.status !== "in_progress" || task.runState !== "queued") return task;
|
|
615
|
+
try {
|
|
616
|
+
const db = getDatabase();
|
|
617
|
+
const rows = db.prepare(
|
|
618
|
+
`SELECT id FROM tasks WHERE project_id = ? AND status = 'in_progress' AND run_state = 'queued'
|
|
619
|
+
ORDER BY position ASC, created_at ASC`
|
|
620
|
+
).all(task.projectId);
|
|
621
|
+
const rank = rows.findIndex((r) => r.id === task.id);
|
|
622
|
+
if (rank < 0) return task;
|
|
623
|
+
return { ...task, queuePosition: rank + 1 };
|
|
624
|
+
} catch {
|
|
625
|
+
return task;
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
function safeParseAttachments(raw) {
|
|
629
|
+
try {
|
|
630
|
+
const parsed = JSON.parse(raw);
|
|
631
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
632
|
+
} catch {
|
|
633
|
+
return [];
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
// src/server/tasks/service.ts
|
|
638
|
+
function isTaskConflictError(err) {
|
|
639
|
+
return err instanceof Error && err.code === "CONFLICT";
|
|
640
|
+
}
|
|
641
|
+
function isTaskGateError(err) {
|
|
642
|
+
return err instanceof Error && err.code === "GATE_BLOCKED";
|
|
643
|
+
}
|
|
644
|
+
var REMINDER_COLOR = "#3b82f6";
|
|
645
|
+
var locks = /* @__PURE__ */ new Map();
|
|
646
|
+
function withLock(projectId, fn) {
|
|
647
|
+
const prev = locks.get(projectId) ?? Promise.resolve();
|
|
648
|
+
const next = prev.then(() => fn());
|
|
649
|
+
locks.set(
|
|
650
|
+
projectId,
|
|
651
|
+
next.then(
|
|
652
|
+
() => void 0,
|
|
653
|
+
() => void 0
|
|
654
|
+
)
|
|
655
|
+
);
|
|
656
|
+
return next;
|
|
657
|
+
}
|
|
658
|
+
function createTasksService(deps) {
|
|
659
|
+
const { sessionManager, config, broadcast, configDir, launchWorkflow } = deps;
|
|
660
|
+
function snapshot(projectId) {
|
|
661
|
+
const tasks = listTasks(projectId);
|
|
662
|
+
const settings = getTaskSettings(projectId);
|
|
663
|
+
const counts2 = computeCounts(tasks);
|
|
664
|
+
const gates = getGateConfig(projectId);
|
|
665
|
+
return { tasks, settings, counts: counts2, gates };
|
|
666
|
+
}
|
|
667
|
+
function publish(projectId, changedTaskId, autoLaunched) {
|
|
668
|
+
const { tasks, settings, counts: counts2, gates } = snapshot(projectId);
|
|
669
|
+
broadcast(projectId, {
|
|
670
|
+
projectId,
|
|
671
|
+
tasks,
|
|
672
|
+
settings,
|
|
673
|
+
counts: counts2,
|
|
674
|
+
gates,
|
|
675
|
+
...changedTaskId ? { changedTaskId } : {},
|
|
676
|
+
...autoLaunched ? { autoLaunched } : {}
|
|
677
|
+
});
|
|
678
|
+
}
|
|
679
|
+
function list(projectId) {
|
|
680
|
+
return listTasks(projectId);
|
|
681
|
+
}
|
|
682
|
+
function get(projectId, taskId) {
|
|
683
|
+
const task = getTask(taskId);
|
|
684
|
+
return task && task.projectId === projectId ? task : null;
|
|
685
|
+
}
|
|
686
|
+
function assertOwned(projectId, taskId) {
|
|
687
|
+
const task = getTask(taskId);
|
|
688
|
+
if (!task || task.projectId !== projectId) {
|
|
689
|
+
throw new Error(`Task not found: ${taskId}`);
|
|
690
|
+
}
|
|
691
|
+
return task;
|
|
692
|
+
}
|
|
693
|
+
function assertNotStale(task, expectedVersion) {
|
|
694
|
+
if (expectedVersion !== void 0 && task.version !== expectedVersion) {
|
|
695
|
+
const err = new Error("Task changed, refresh and retry");
|
|
696
|
+
err.code = "CONFLICT";
|
|
697
|
+
err.task = task;
|
|
698
|
+
throw err;
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
function create(projectId, input, actor) {
|
|
702
|
+
const hasContent = input.prompt.trim().length > 0;
|
|
703
|
+
const hasAttachments = (input.attachments?.length ?? 0) > 0;
|
|
704
|
+
if (!hasContent && !hasAttachments) {
|
|
705
|
+
throw new Error("Task requires at least a prompt or an attachment");
|
|
706
|
+
}
|
|
707
|
+
const task = createTask(projectId, {
|
|
708
|
+
prompt: input.prompt,
|
|
709
|
+
attachments: input.attachments ?? [],
|
|
710
|
+
...input.agentId ? { agentId: input.agentId } : {},
|
|
711
|
+
...input.providerId ? { providerId: input.providerId } : {},
|
|
712
|
+
...input.model ? { model: input.model } : {}
|
|
713
|
+
});
|
|
714
|
+
addAuditEntry(task.id, actor.actor, "create", `Task created in To Do`, actor.actorName);
|
|
715
|
+
const fresh = getTask(task.id);
|
|
716
|
+
publish(projectId, fresh.id);
|
|
717
|
+
return fresh;
|
|
718
|
+
}
|
|
719
|
+
function update(projectId, taskId, patch, actor, expectedVersion) {
|
|
720
|
+
return withLock(projectId, () => {
|
|
721
|
+
const task = assertOwned(projectId, taskId);
|
|
722
|
+
assertNotStale(task, expectedVersion);
|
|
723
|
+
const next = updateTask(taskId, patch) ?? getTask(taskId);
|
|
724
|
+
const changed = summarizeChanges(task, next);
|
|
725
|
+
addAuditEntry(taskId, actor.actor, "edit", changed || "Task updated", actor.actorName);
|
|
726
|
+
const fresh = getTask(taskId);
|
|
727
|
+
publish(projectId, fresh.id);
|
|
728
|
+
return { task: fresh, conflict: false };
|
|
729
|
+
});
|
|
730
|
+
}
|
|
731
|
+
function duplicate(projectId, taskId, actor) {
|
|
732
|
+
const source = assertOwned(projectId, taskId);
|
|
733
|
+
const copy = cloneTask(taskId);
|
|
734
|
+
if (!copy) throw new Error(`Task not found: ${taskId}`);
|
|
735
|
+
addAuditEntry(
|
|
736
|
+
copy.id,
|
|
737
|
+
actor.actor,
|
|
738
|
+
"duplicate",
|
|
739
|
+
`Duplicated from "${promptLabel(source.prompt)}"`,
|
|
740
|
+
actor.actorName
|
|
741
|
+
);
|
|
742
|
+
const fresh = getTask(copy.id);
|
|
743
|
+
publish(projectId, fresh.id);
|
|
744
|
+
return fresh;
|
|
745
|
+
}
|
|
746
|
+
function remove(projectId, taskId, actor) {
|
|
747
|
+
return withLock(projectId, async () => {
|
|
748
|
+
const task = assertOwned(projectId, taskId);
|
|
749
|
+
const freedSlot = task.status === "in_progress" && task.runState === "running";
|
|
750
|
+
addAuditEntry(taskId, actor.actor, "delete", `Task deleted`, actor.actorName);
|
|
751
|
+
deleteTask(taskId);
|
|
752
|
+
publish(projectId);
|
|
753
|
+
const autoLaunched = freedSlot ? await maybeAutoLaunch(projectId) : void 0;
|
|
754
|
+
if (autoLaunched) publish(projectId, void 0, autoLaunched);
|
|
755
|
+
});
|
|
756
|
+
}
|
|
757
|
+
function move(projectId, taskId, to, opts) {
|
|
758
|
+
return withLock(projectId, async () => {
|
|
759
|
+
const task = assertOwned(projectId, taskId);
|
|
760
|
+
assertNotStale(task, opts.expectedVersion);
|
|
761
|
+
const from = task.status;
|
|
762
|
+
if (from === to) {
|
|
763
|
+
return { task };
|
|
764
|
+
}
|
|
765
|
+
let sessionId;
|
|
766
|
+
const wasRunning = task.status === "in_progress" && task.runState === "running";
|
|
767
|
+
if (to === "in_progress") {
|
|
768
|
+
const readyGates = requiredGates(projectId, "ready");
|
|
769
|
+
const missing = missingGateFields(task, readyGates);
|
|
770
|
+
if (missing.length > 0) {
|
|
771
|
+
throw gateBlockedError(task, "In Progress", missing);
|
|
772
|
+
}
|
|
773
|
+
setTaskStatus(taskId, "in_progress");
|
|
774
|
+
const settings = getTaskSettings(projectId);
|
|
775
|
+
if (opts.actor === "agent") {
|
|
776
|
+
if (!opts.sessionId) {
|
|
777
|
+
throw new Error("Agent moves must bind to the current session (sessionId is required)");
|
|
778
|
+
}
|
|
779
|
+
setTaskRunState(taskId, "running");
|
|
780
|
+
addTaskLink(taskId, opts.sessionId, true);
|
|
781
|
+
addAuditEntry(taskId, "agent", "move", `Moved to In Progress (running)`, opts.actorName);
|
|
782
|
+
emitReminder(opts.sessionId, reminderForInProgress(task, opts.sessionId, from));
|
|
783
|
+
sessionId = opts.sessionId;
|
|
784
|
+
} else {
|
|
785
|
+
const runningCount = activeCount(projectId);
|
|
786
|
+
const launched = runningCount < settings.slotLimit && !settings.queuePaused;
|
|
787
|
+
setTaskRunState(taskId, launched ? "running" : "queued");
|
|
788
|
+
if (launched) {
|
|
789
|
+
const seeded = await seedSession(projectId, task, from);
|
|
790
|
+
addTaskLink(taskId, seeded.session.id, true);
|
|
791
|
+
addAuditEntry(taskId, "human", "move", `Moved to In Progress (running)`, opts.actorName);
|
|
792
|
+
sessionId = seeded.session.id;
|
|
793
|
+
} else {
|
|
794
|
+
appendToBottom(taskId, "in_progress");
|
|
795
|
+
addAuditEntry(taskId, "human", "move", `Moved to In Progress (queued)`, opts.actorName);
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
} else if (to === "done") {
|
|
799
|
+
const doneGates = requiredGates(projectId, "done");
|
|
800
|
+
const missing = missingGateFields(task, doneGates);
|
|
801
|
+
if (missing.length > 0) {
|
|
802
|
+
throw gateBlockedError(task, "Done", missing);
|
|
803
|
+
}
|
|
804
|
+
setTaskStatus(taskId, "done");
|
|
805
|
+
clearActiveTaskLink(taskId);
|
|
806
|
+
addAuditEntry(taskId, opts.actor, "move", `Moved to Done`, opts.actorName);
|
|
807
|
+
if (task.activeSessionId) {
|
|
808
|
+
emitReminder(task.activeSessionId, reminderForDone(task));
|
|
809
|
+
}
|
|
810
|
+
} else {
|
|
811
|
+
setTaskStatus(taskId, "todo");
|
|
812
|
+
removeTaskLinks(taskId);
|
|
813
|
+
const detailParts = [`Moved back to To Do`];
|
|
814
|
+
if (opts.reason) detailParts.push(`Reason: ${opts.reason}`);
|
|
815
|
+
addAuditEntry(taskId, opts.actor, "move", detailParts.join(". "), opts.actorName);
|
|
816
|
+
if (task.activeSessionId) {
|
|
817
|
+
emitReminder(task.activeSessionId, reminderForTodo(task));
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
const fresh = getTask(taskId);
|
|
821
|
+
let autoLaunched;
|
|
822
|
+
if (wasRunning || to === "todo" && from === "in_progress") {
|
|
823
|
+
autoLaunched = await maybeAutoLaunch(projectId);
|
|
824
|
+
}
|
|
825
|
+
publish(projectId, fresh.id, autoLaunched);
|
|
826
|
+
return {
|
|
827
|
+
task: fresh,
|
|
828
|
+
...sessionId ? { sessionId } : {},
|
|
829
|
+
...autoLaunched ? { autoLaunched } : {}
|
|
830
|
+
};
|
|
831
|
+
});
|
|
832
|
+
}
|
|
833
|
+
function setGateValue2(projectId, taskId, gateId, value, actor, sessionId, expectedVersion) {
|
|
834
|
+
return withLock(projectId, () => {
|
|
835
|
+
const task = assertOwned(projectId, taskId);
|
|
836
|
+
assertNotStale(task, expectedVersion);
|
|
837
|
+
const gates = getGateConfig(projectId);
|
|
838
|
+
const gate = gates.find((g) => g.id === gateId);
|
|
839
|
+
if (!gate) {
|
|
840
|
+
throw new Error(`Unknown gate: ${gateId}`);
|
|
841
|
+
}
|
|
842
|
+
setGateValue(taskId, gateId, value, actor.actor, actor.actorName, sessionId);
|
|
843
|
+
addAuditEntry(taskId, actor.actor, "gate_value", `Set gate "${gate.name}" = "${value}"`, actor.actorName);
|
|
844
|
+
const fresh = getTask(taskId);
|
|
845
|
+
publish(projectId, fresh.id);
|
|
846
|
+
return { task: fresh, conflict: false };
|
|
847
|
+
});
|
|
848
|
+
}
|
|
849
|
+
function setGateConfig(projectId, gates, actor) {
|
|
850
|
+
const normalized = gates.map((g) => ({
|
|
851
|
+
...g,
|
|
852
|
+
name: g.name.trim(),
|
|
853
|
+
description: g.description.trim()
|
|
854
|
+
}));
|
|
855
|
+
replaceGateConfig(projectId, normalized);
|
|
856
|
+
void actor;
|
|
857
|
+
publish(projectId);
|
|
858
|
+
return getGateConfig(projectId);
|
|
859
|
+
}
|
|
860
|
+
async function setSettings(projectId, settings) {
|
|
861
|
+
const saved = setTaskSettings(projectId, settings);
|
|
862
|
+
publish(projectId);
|
|
863
|
+
const autoLaunched = await maybeAutoLaunch(projectId);
|
|
864
|
+
if (autoLaunched) publish(projectId, void 0, autoLaunched);
|
|
865
|
+
return saved;
|
|
866
|
+
}
|
|
867
|
+
function reorder(projectId, taskId, status, toIndex) {
|
|
868
|
+
const task = assertOwned(projectId, taskId);
|
|
869
|
+
if (task.status !== status) {
|
|
870
|
+
throw new Error(`Cannot reorder task: status mismatch (${task.status} != ${status})`);
|
|
871
|
+
}
|
|
872
|
+
reorderTask(taskId, status, toIndex);
|
|
873
|
+
const fresh = getTask(taskId);
|
|
874
|
+
publish(projectId, fresh.id);
|
|
875
|
+
return fresh;
|
|
876
|
+
}
|
|
877
|
+
function counts(projectId) {
|
|
878
|
+
return computeCounts(listTasks(projectId));
|
|
879
|
+
}
|
|
880
|
+
function computeCounts(tasks) {
|
|
881
|
+
const todo = tasks.filter((t) => t.status === "todo").length;
|
|
882
|
+
const inProgress = tasks.filter((t) => t.status === "in_progress").length;
|
|
883
|
+
const running = tasks.filter((t) => t.status === "in_progress" && t.runState === "running").length;
|
|
884
|
+
const queued = tasks.filter((t) => t.status === "in_progress" && t.runState === "queued").length;
|
|
885
|
+
const done = tasks.filter((t) => t.status === "done").length;
|
|
886
|
+
return { open: todo + inProgress, todo, inProgress, running, queued, done };
|
|
887
|
+
}
|
|
888
|
+
function requiredGates(projectId, variant) {
|
|
889
|
+
return getGateConfig(projectId).filter((g) => g.variant === variant && g.required);
|
|
890
|
+
}
|
|
891
|
+
function missingGateFields(task, gates) {
|
|
892
|
+
const filled = new Set(task.gateValues.filter((v) => v.value.trim().length > 0).map((v) => v.gateId));
|
|
893
|
+
return gates.filter((g) => !filled.has(g.id)).map((g) => ({ gateId: g.id, name: g.name, description: g.description }));
|
|
894
|
+
}
|
|
895
|
+
function gateBlockedError(task, destination, missing) {
|
|
896
|
+
const err = new Error(
|
|
897
|
+
`Cannot move to ${destination}: missing required gate fields: ${missing.map((m) => m.name).join(", ")}`
|
|
898
|
+
);
|
|
899
|
+
err.code = "GATE_BLOCKED";
|
|
900
|
+
err.missing = missing;
|
|
901
|
+
err.task = task;
|
|
902
|
+
return err;
|
|
903
|
+
}
|
|
904
|
+
function activeCount(projectId) {
|
|
905
|
+
return listTasks(projectId).filter((t) => t.status === "in_progress" && t.runState === "running").length;
|
|
906
|
+
}
|
|
907
|
+
async function seedSession(projectId, task, from) {
|
|
908
|
+
const defaults = parseDefaultModelSelection(config.defaultModelSelection);
|
|
909
|
+
const providerId = task.providerId ?? defaults.providerId ?? null;
|
|
910
|
+
const model = task.model ?? defaults.model ?? null;
|
|
911
|
+
const session = sessionManager.createSession(projectId, void 0, providerId, model);
|
|
912
|
+
if (task.agentId) {
|
|
913
|
+
try {
|
|
914
|
+
sessionManager.setMode(session.id, task.agentId);
|
|
915
|
+
} catch {
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
emitReminder(session.id, reminderForInProgress(task, session.id, from));
|
|
919
|
+
const attachments = task.attachments.length > 0 ? task.attachments : void 0;
|
|
920
|
+
const projectDir = getProject(projectId)?.workdir;
|
|
921
|
+
let slash;
|
|
922
|
+
try {
|
|
923
|
+
slash = await resolveSlashLaunch(configDir, projectDir, task.prompt);
|
|
924
|
+
} catch {
|
|
925
|
+
slash = null;
|
|
926
|
+
}
|
|
927
|
+
if (slash?.kind === "workflow") {
|
|
928
|
+
if (launchWorkflow) {
|
|
929
|
+
launchWorkflow(session.id, {
|
|
930
|
+
workflowId: slash.workflowId,
|
|
931
|
+
params: slash.params,
|
|
932
|
+
scope: "auto",
|
|
933
|
+
...attachments ? { attachments } : {}
|
|
934
|
+
});
|
|
935
|
+
} else {
|
|
936
|
+
sessionManager.queueMessage(session.id, "asap", task.prompt, attachments);
|
|
937
|
+
}
|
|
938
|
+
} else if (slash?.kind === "command") {
|
|
939
|
+
if (slash.agentMode) {
|
|
940
|
+
try {
|
|
941
|
+
sessionManager.setMode(session.id, slash.agentMode);
|
|
942
|
+
} catch {
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
sessionManager.queueMessage(session.id, "asap", slash.prompt, attachments, "command");
|
|
946
|
+
} else {
|
|
947
|
+
sessionManager.queueMessage(session.id, "asap", task.prompt, attachments);
|
|
948
|
+
}
|
|
949
|
+
return { session };
|
|
950
|
+
}
|
|
951
|
+
async function maybeAutoLaunch(projectId) {
|
|
952
|
+
const settings = getTaskSettings(projectId);
|
|
953
|
+
if (settings.queuePaused) return void 0;
|
|
954
|
+
if (activeCount(projectId) >= settings.slotLimit) return void 0;
|
|
955
|
+
const queued = listTasks(projectId).filter((t) => t.status === "in_progress" && t.runState === "queued").sort((a, b) => a.position - b.position);
|
|
956
|
+
const next = queued[0];
|
|
957
|
+
if (!next) return void 0;
|
|
958
|
+
setTaskRunState(next.id, "running");
|
|
959
|
+
const seeded = await seedSession(projectId, next, "queued");
|
|
960
|
+
addTaskLink(next.id, seeded.session.id, true);
|
|
961
|
+
addAuditEntry(next.id, "system", "auto_launch", `Auto-launched into session ${seeded.session.id}`, "system");
|
|
962
|
+
return {
|
|
963
|
+
taskId: next.id,
|
|
964
|
+
taskTitle: promptLabel(next.prompt),
|
|
965
|
+
sessionId: seeded.session.id,
|
|
966
|
+
projectId
|
|
967
|
+
};
|
|
968
|
+
}
|
|
969
|
+
function emitReminder(sessionId, content) {
|
|
970
|
+
try {
|
|
971
|
+
sessionManager.addMessage(sessionId, {
|
|
972
|
+
role: "user",
|
|
973
|
+
content: `<system-reminder>
|
|
974
|
+
${content}
|
|
975
|
+
</system-reminder>`,
|
|
976
|
+
isSystemGenerated: true,
|
|
977
|
+
messageKind: "auto-prompt",
|
|
978
|
+
metadata: { type: "task", name: "Task", color: REMINDER_COLOR, kind: "definition" }
|
|
979
|
+
});
|
|
980
|
+
} catch {
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
function reminderForInProgress(task, sessionId, from) {
|
|
984
|
+
const gates = requiredGates(task.projectId, "done");
|
|
985
|
+
const gateLine = gates.length > 0 ? `Outstanding gates: ${gates.map((g) => g.name).join(", ")}.` : "No gates configured.";
|
|
986
|
+
return [
|
|
987
|
+
`This session is now working on task "${promptLabel(task.prompt)}" from the project task board.`,
|
|
988
|
+
`Previous state: ${prettyStatus(from)} \u2192 In Progress. Session: ${sessionId.slice(0, 8)}.`,
|
|
989
|
+
gateLine
|
|
990
|
+
].join("\n");
|
|
991
|
+
}
|
|
992
|
+
function reminderForDone(task) {
|
|
993
|
+
const evidence = task.gateValues.filter((v) => v.value.trim().length > 0).map((v) => ` - ${v.gateId}: ${v.value} (set by ${v.actor})`);
|
|
994
|
+
const evidenceLine = evidence.length > 0 ? evidence.join("\n") : " - (no gate values recorded)";
|
|
995
|
+
return [
|
|
996
|
+
`Task "${promptLabel(task.prompt)}" was marked Done.`,
|
|
997
|
+
`Gate evidence that satisfied it:
|
|
998
|
+
${evidenceLine}`,
|
|
999
|
+
`Wind down cleanly \u2014 this task is no longer active.`
|
|
1000
|
+
].join("\n");
|
|
1001
|
+
}
|
|
1002
|
+
function reminderForTodo(task) {
|
|
1003
|
+
return `Task "${promptLabel(task.prompt)}" is no longer active in this session (reverted to To Do). Do not continue working on it.`;
|
|
1004
|
+
}
|
|
1005
|
+
function prettyStatus(s) {
|
|
1006
|
+
switch (s) {
|
|
1007
|
+
case "todo":
|
|
1008
|
+
return "To Do";
|
|
1009
|
+
case "in_progress":
|
|
1010
|
+
return "In Progress";
|
|
1011
|
+
case "done":
|
|
1012
|
+
return "Done";
|
|
1013
|
+
case "queued":
|
|
1014
|
+
return "Queued";
|
|
1015
|
+
default:
|
|
1016
|
+
return s;
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
function summarizeChanges(before, after) {
|
|
1020
|
+
const parts = [];
|
|
1021
|
+
if (before.prompt !== after.prompt) parts.push("prompt updated");
|
|
1022
|
+
if (before.agentId !== after.agentId || before.model !== after.model) parts.push("agent/model updated");
|
|
1023
|
+
if (before.attachments.length !== after.attachments.length) parts.push("attachments updated");
|
|
1024
|
+
return parts.length > 0 ? `Edited: ${parts.join(", ")}` : "";
|
|
1025
|
+
}
|
|
1026
|
+
return {
|
|
1027
|
+
snapshot,
|
|
1028
|
+
list,
|
|
1029
|
+
get,
|
|
1030
|
+
create,
|
|
1031
|
+
update,
|
|
1032
|
+
duplicate,
|
|
1033
|
+
remove,
|
|
1034
|
+
move,
|
|
1035
|
+
setGateValue: setGateValue2,
|
|
1036
|
+
setGateConfig,
|
|
1037
|
+
setSettings,
|
|
1038
|
+
reorder,
|
|
1039
|
+
counts
|
|
1040
|
+
};
|
|
1041
|
+
}
|
|
1042
|
+
function promptLabel(prompt) {
|
|
1043
|
+
const firstLine = prompt.split("\n").find((line) => line.trim().length > 0)?.trim() ?? "";
|
|
1044
|
+
const cleaned = firstLine.replace(/^[#>*\-\s]+/, "");
|
|
1045
|
+
if (cleaned.length <= 52) return cleaned;
|
|
1046
|
+
return `${cleaned.slice(0, 49).trimEnd()}\u2026`;
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
export {
|
|
1050
|
+
loadDefaultCommands,
|
|
1051
|
+
loadUserCommands,
|
|
1052
|
+
loadProjectCommands,
|
|
1053
|
+
loadAllCommands,
|
|
1054
|
+
getDefaultCommandIds,
|
|
1055
|
+
isDefaultCommand,
|
|
1056
|
+
findCommandById,
|
|
1057
|
+
commandExists,
|
|
1058
|
+
saveCommand,
|
|
1059
|
+
saveCommandToProject,
|
|
1060
|
+
deleteProjectCommand,
|
|
1061
|
+
deleteCommand,
|
|
1062
|
+
normalizeWorkflowScope,
|
|
1063
|
+
loadDefaultWorkflows,
|
|
1064
|
+
loadUserWorkflows,
|
|
1065
|
+
loadProjectWorkflows,
|
|
1066
|
+
loadAllWorkflows,
|
|
1067
|
+
saveWorkflowToProject,
|
|
1068
|
+
deleteProjectWorkflow,
|
|
1069
|
+
getDefaultWorkflowIds,
|
|
1070
|
+
isDefaultWorkflow,
|
|
1071
|
+
findWorkflowById,
|
|
1072
|
+
workflowExists,
|
|
1073
|
+
saveWorkflow,
|
|
1074
|
+
deleteWorkflow,
|
|
1075
|
+
getGateConfig,
|
|
1076
|
+
getTaskSettings,
|
|
1077
|
+
isTaskConflictError,
|
|
1078
|
+
isTaskGateError,
|
|
1079
|
+
createTasksService
|
|
1080
|
+
};
|
|
1081
|
+
//# sourceMappingURL=chunk-6RGACY4E.js.map
|