crewos 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/app/.env.example +1 -0
- package/app/index.html +50 -0
- package/app/package.json +25 -0
- package/app/public/favicon.svg +1 -0
- package/app/public/images/cursor-ide-guiiding.png +0 -0
- package/app/public/images/gpt.jpg +0 -0
- package/app/src/app.jsx +22 -0
- package/app/src/components/ConfirmModal.jsx +50 -0
- package/app/src/components/Icons.jsx +377 -0
- package/app/src/components/RedirectRoute.jsx +14 -0
- package/app/src/components/SplashScreen.jsx +15 -0
- package/app/src/hooks/useAuth.js +28 -0
- package/app/src/index.css +268 -0
- package/app/src/main.jsx +5 -0
- package/app/src/navigations/AuthRoutes.jsx +15 -0
- package/app/src/navigations/MainRoutes.jsx +15 -0
- package/app/src/navigations/OnboardingRoutes.jsx +15 -0
- package/app/src/navigations/index.jsx +37 -0
- package/app/src/pages/Home/index.jsx +2095 -0
- package/app/src/pages/Login/index.jsx +118 -0
- package/app/src/pages/Onboarding/index.jsx +550 -0
- package/app/src/services/api.js +46 -0
- package/app/src/services/auth.service.js +3 -0
- package/app/src/services/config.service.js +13 -0
- package/app/src/services/member.service.js +7 -0
- package/app/src/services/onboarding.service.js +17 -0
- package/app/src/services/role.service.js +6 -0
- package/app/src/services/task.service.js +22 -0
- package/app/src/stores/auth.store.js +7 -0
- package/app/src/utils/environments.js +5 -0
- package/app/vite.config.js +10 -0
- package/app/yarn.lock +1337 -0
- package/backend/package-lock.json +918 -0
- package/backend/package.json +18 -0
- package/backend/src/configs/db.config.js +40 -0
- package/backend/src/controllers/auth.controller.js +19 -0
- package/backend/src/controllers/config.controller.js +23 -0
- package/backend/src/controllers/member.controller.js +30 -0
- package/backend/src/controllers/models.controller.js +25 -0
- package/backend/src/controllers/onboarding.controller.js +49 -0
- package/backend/src/controllers/role.controller.js +17 -0
- package/backend/src/controllers/task.controller.js +63 -0
- package/backend/src/index.js +36 -0
- package/backend/src/middlewares/onboarding.guard.js +14 -0
- package/backend/src/routes/auth.route.js +8 -0
- package/backend/src/routes/config.route.js +11 -0
- package/backend/src/routes/index.js +22 -0
- package/backend/src/routes/member.route.js +11 -0
- package/backend/src/routes/models.route.js +8 -0
- package/backend/src/routes/onboarding.route.js +13 -0
- package/backend/src/routes/role.route.js +9 -0
- package/backend/src/routes/task.route.js +20 -0
- package/backend/src/services/auth.service.js +14 -0
- package/backend/src/services/config.service.js +176 -0
- package/backend/src/services/data/roles.json +474 -0
- package/backend/src/services/member.service.js +77 -0
- package/backend/src/services/onboarding.service.js +328 -0
- package/backend/src/services/role.service.js +23 -0
- package/backend/src/services/task.service.js +665 -0
- package/backend/src/utils/catcher.js +9 -0
- package/backend/src/utils/sanitize.js +13 -0
- package/backend/yarn.lock +513 -0
- package/bin/crewos.js +307 -0
- package/package.json +11 -0
|
@@ -0,0 +1,665 @@
|
|
|
1
|
+
import crypto from 'crypto';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import os from 'os';
|
|
4
|
+
import fs from 'fs';
|
|
5
|
+
import { spawn } from 'child_process';
|
|
6
|
+
|
|
7
|
+
import { getDB } from '../configs/db.config.js';
|
|
8
|
+
import { getCachedModels } from './config.service.js';
|
|
9
|
+
import { getProjectKnowledge } from './onboarding.service.js';
|
|
10
|
+
import { sanitizeProgressLine } from '../utils/sanitize.js';
|
|
11
|
+
|
|
12
|
+
const CREWOS_CONFIG_PATH = path.join(os.homedir(), '.crewos', 'config.json');
|
|
13
|
+
const TASK_TIMEOUT_MS = 15 * 60 * 1_000;
|
|
14
|
+
|
|
15
|
+
const KNOWLEDGE_SECTIONS = [
|
|
16
|
+
{ key: 'folderStructure', label: 'Project Structure' },
|
|
17
|
+
{ key: 'techStack', label: 'Tech Stack' },
|
|
18
|
+
{ key: 'codingConventions', label: 'Coding Conventions' },
|
|
19
|
+
{ key: 'databaseDesign', label: 'Database Design' },
|
|
20
|
+
{ key: 'apiDesign', label: 'API Design' },
|
|
21
|
+
{ key: 'configSystem', label: 'Configuration System' },
|
|
22
|
+
{ key: 'keyPatterns', label: 'Key Patterns & Conventions' },
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
function buildKnowledgeContext(knowledge) {
|
|
26
|
+
if (!knowledge || typeof knowledge !== 'object') return '';
|
|
27
|
+
|
|
28
|
+
const sections = KNOWLEDGE_SECTIONS.map(({ key, label }) => {
|
|
29
|
+
const content = (knowledge[key] || '').trim();
|
|
30
|
+
if (!content || content === 'N/A') return null;
|
|
31
|
+
return `## ${label}\n${content}`;
|
|
32
|
+
}).filter(Boolean);
|
|
33
|
+
|
|
34
|
+
if (sections.length === 0) return '';
|
|
35
|
+
|
|
36
|
+
return `<project_context>\nThe following is the project knowledge base. Use it to inform your work:\n\n${sections.join('\n\n')}\n</project_context>`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
let activeProcess = null;
|
|
40
|
+
let activeTaskId = null;
|
|
41
|
+
let progressLines = [];
|
|
42
|
+
let stopped = false;
|
|
43
|
+
let pendingTokens = null;
|
|
44
|
+
|
|
45
|
+
let autoRunning = false;
|
|
46
|
+
let autoQueue = [];
|
|
47
|
+
let autoStopRequested = false;
|
|
48
|
+
|
|
49
|
+
export const isTaskRunning = () => activeProcess !== null;
|
|
50
|
+
export const isAutoRunning = () => autoRunning;
|
|
51
|
+
|
|
52
|
+
export const getProgress = () => ({ lines: progressLines, tokens: pendingTokens });
|
|
53
|
+
|
|
54
|
+
function parseTokensFromOutput(lines) {
|
|
55
|
+
if (pendingTokens && (pendingTokens.inputTokens > 0 || pendingTokens.outputTokens > 0)) {
|
|
56
|
+
return pendingTokens;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const combined = lines.join('\n');
|
|
60
|
+
|
|
61
|
+
const jsonMatch =
|
|
62
|
+
combined.match(/"input_tokens":\s*(\d+).*?"output_tokens":\s*(\d+)/s) ||
|
|
63
|
+
combined.match(/"output_tokens":\s*(\d+).*?"input_tokens":\s*(\d+)/s);
|
|
64
|
+
if (jsonMatch) {
|
|
65
|
+
const inputIdx =
|
|
66
|
+
jsonMatch[0].indexOf('input_tokens') <
|
|
67
|
+
jsonMatch[0].indexOf('output_tokens')
|
|
68
|
+
? 1
|
|
69
|
+
: 2;
|
|
70
|
+
const outputIdx = inputIdx === 1 ? 2 : 1;
|
|
71
|
+
return {
|
|
72
|
+
inputTokens: parseInt(jsonMatch[inputIdx], 10),
|
|
73
|
+
outputTokens: parseInt(jsonMatch[outputIdx], 10),
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const lineMatch =
|
|
78
|
+
combined.match(/Input tokens:\s*([\d,]+).*?Output tokens:\s*([\d,]+)/is) ||
|
|
79
|
+
combined.match(/Output tokens:\s*([\d,]+).*?Input tokens:\s*([\d,]+)/is);
|
|
80
|
+
if (lineMatch) {
|
|
81
|
+
const inputIdx =
|
|
82
|
+
lineMatch[0].toLowerCase().indexOf('input tokens:') <
|
|
83
|
+
lineMatch[0].toLowerCase().indexOf('output tokens:')
|
|
84
|
+
? 1
|
|
85
|
+
: 2;
|
|
86
|
+
const outputIdx = inputIdx === 1 ? 2 : 1;
|
|
87
|
+
return {
|
|
88
|
+
inputTokens: parseInt(lineMatch[inputIdx].replace(/,/g, ''), 10),
|
|
89
|
+
outputTokens: parseInt(lineMatch[outputIdx].replace(/,/g, ''), 10),
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const inputMatch = combined.match(/Input tokens:\s*([\d,]+)/i);
|
|
94
|
+
const outputMatch = combined.match(/Output tokens:\s*([\d,]+)/i);
|
|
95
|
+
if (inputMatch && outputMatch) {
|
|
96
|
+
return {
|
|
97
|
+
inputTokens: parseInt(inputMatch[1].replace(/,/g, ''), 10),
|
|
98
|
+
outputTokens: parseInt(outputMatch[1].replace(/,/g, ''), 10),
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export const getAutoStatus = () => ({
|
|
106
|
+
running: autoRunning,
|
|
107
|
+
queue: autoQueue,
|
|
108
|
+
processing: activeTaskId,
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
export const get = ({ status }) => {
|
|
112
|
+
const db = getDB();
|
|
113
|
+
let tasks = db.data.tasks;
|
|
114
|
+
if (status) {
|
|
115
|
+
tasks = tasks.filter((t) => t.status === status);
|
|
116
|
+
}
|
|
117
|
+
return tasks;
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
export const getById = (id) => {
|
|
121
|
+
const db = getDB();
|
|
122
|
+
return db.data.tasks.find((t) => t.id === id) || null;
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
export const create = ({ priority, description, assignee }) => {
|
|
126
|
+
const db = getDB();
|
|
127
|
+
|
|
128
|
+
if (assignee && !db.data.members.find((m) => m.id === assignee)) {
|
|
129
|
+
return { error: `Assignee "${assignee}" not found` };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const task = {
|
|
133
|
+
id: crypto.randomUUID(),
|
|
134
|
+
priority: priority || 'medium',
|
|
135
|
+
description,
|
|
136
|
+
assignee: assignee || null,
|
|
137
|
+
status: 'todo',
|
|
138
|
+
inputTokens: null,
|
|
139
|
+
outputTokens: null,
|
|
140
|
+
createdAt: new Date().toISOString(),
|
|
141
|
+
updatedAt: new Date().toISOString(),
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
db.data.tasks.push(task);
|
|
145
|
+
db.write();
|
|
146
|
+
|
|
147
|
+
return task;
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
export const update = (id, updates) => {
|
|
151
|
+
const db = getDB();
|
|
152
|
+
const idx = db.data.tasks.findIndex((t) => t.id === id);
|
|
153
|
+
|
|
154
|
+
if (idx === -1) return null;
|
|
155
|
+
|
|
156
|
+
const allowed = ['priority', 'description', 'assignee', 'status'];
|
|
157
|
+
for (const key of allowed) {
|
|
158
|
+
if (updates[key] !== undefined) {
|
|
159
|
+
db.data.tasks[idx][key] = updates[key];
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
db.data.tasks[idx].updatedAt = new Date().toISOString();
|
|
164
|
+
db.write();
|
|
165
|
+
return db.data.tasks[idx];
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
export const remove = (id) => {
|
|
169
|
+
const db = getDB();
|
|
170
|
+
const idx = db.data.tasks.findIndex((t) => t.id === id);
|
|
171
|
+
|
|
172
|
+
if (idx === -1) return false;
|
|
173
|
+
|
|
174
|
+
db.data.tasks.splice(idx, 1);
|
|
175
|
+
db.write();
|
|
176
|
+
return true;
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
export const startTask = (id) => {
|
|
180
|
+
const db = getDB();
|
|
181
|
+
|
|
182
|
+
if (activeProcess) {
|
|
183
|
+
return { error: 'Another task is already running' };
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const task = db.data.tasks.find((t) => t.id === id);
|
|
187
|
+
if (!task) return { error: 'Task not found' };
|
|
188
|
+
|
|
189
|
+
const { baseURL, apiKey, model } = db.data.meta;
|
|
190
|
+
if (!baseURL || !apiKey) {
|
|
191
|
+
return { error: 'Base URL and API Key must be configured in Settings' };
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const effectiveModel = model && model !== 'auto' ? model : null;
|
|
195
|
+
const models = getCachedModels();
|
|
196
|
+
const resolvedModelId =
|
|
197
|
+
effectiveModel || (models.length > 0 ? models[0].id : null);
|
|
198
|
+
|
|
199
|
+
if (!resolvedModelId) {
|
|
200
|
+
return { error: 'No model available. Fetch models in Settings first.' };
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
task.status = 'doing';
|
|
204
|
+
task.updatedAt = new Date().toISOString();
|
|
205
|
+
db.write();
|
|
206
|
+
|
|
207
|
+
const startedAt = new Date().toISOString();
|
|
208
|
+
console.log(`[crewOS:${id}] ====== TASK STARTED ======`);
|
|
209
|
+
console.log(`[crewOS:${id}] time: ${startedAt}`);
|
|
210
|
+
console.log(`[crewOS:${id}] description: ${task.description}`);
|
|
211
|
+
console.log(`[crewOS:${id}] model: ${resolvedModelId}`);
|
|
212
|
+
|
|
213
|
+
let workingDir = process.cwd();
|
|
214
|
+
if (fs.existsSync(CREWOS_CONFIG_PATH)) {
|
|
215
|
+
try {
|
|
216
|
+
const cfg = JSON.parse(fs.readFileSync(CREWOS_CONFIG_PATH, 'utf-8'));
|
|
217
|
+
workingDir = cfg.workingDir || process.cwd();
|
|
218
|
+
} catch {
|
|
219
|
+
/* ignore */
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
console.log(`[crewOS:${id}] cwd: ${workingDir}`);
|
|
223
|
+
|
|
224
|
+
const knowledge = getProjectKnowledge();
|
|
225
|
+
const knowledgeBlock = buildKnowledgeContext(knowledge);
|
|
226
|
+
|
|
227
|
+
let roleBlock = '';
|
|
228
|
+
if (task.assignee) {
|
|
229
|
+
const assignee = db.data.members.find((m) => m.id === task.assignee);
|
|
230
|
+
if (assignee) {
|
|
231
|
+
const roleParts = [];
|
|
232
|
+
if (assignee.role) roleParts.push(`# Role: ${assignee.role}`);
|
|
233
|
+
if (assignee.skills) roleParts.push(`## Skills\n${assignee.skills}`);
|
|
234
|
+
if (assignee.rules) roleParts.push(`## Rules\n${assignee.rules}`);
|
|
235
|
+
if (assignee.memory) roleParts.push(`## Memory\n${assignee.memory}`);
|
|
236
|
+
if (roleParts.length > 0) {
|
|
237
|
+
roleBlock = `<role_context>\nYou are acting as a "${assignee.name}" (${assignee.role}). Follow these instructions strictly:\n\n${roleParts.join('\n\n')}\n\nStay within the scope of your role. Do not edit files outside your domain.\n</role_context>`;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
let prompt = task.description;
|
|
243
|
+
if (knowledgeBlock || roleBlock) {
|
|
244
|
+
prompt = [knowledgeBlock, roleBlock, `<task>\n${task.description}\n</task>`]
|
|
245
|
+
.filter(Boolean)
|
|
246
|
+
.join('\n\n');
|
|
247
|
+
console.log(
|
|
248
|
+
`[crewOS:${id}] prompt built (knowledge=${knowledgeBlock.length} chars, role=${roleBlock.length} chars)`,
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const args = ['run', '--dangerously-skip-permissions', '--format', 'json', prompt];
|
|
253
|
+
|
|
254
|
+
const child = spawn('opencode', args, {
|
|
255
|
+
cwd: workingDir,
|
|
256
|
+
env: process.env,
|
|
257
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
console.log(`[crewOS:${id}] spawned pid=${child.pid}`);
|
|
261
|
+
|
|
262
|
+
activeProcess = child;
|
|
263
|
+
activeTaskId = id;
|
|
264
|
+
progressLines = [];
|
|
265
|
+
stopped = false;
|
|
266
|
+
pendingTokens = null;
|
|
267
|
+
|
|
268
|
+
let disposition = null;
|
|
269
|
+
let timeout = null;
|
|
270
|
+
let stdoutBytes = 0;
|
|
271
|
+
let stderrBytes = 0;
|
|
272
|
+
|
|
273
|
+
timeout = setTimeout(() => {
|
|
274
|
+
if (disposition) {
|
|
275
|
+
console.log(
|
|
276
|
+
`[crewOS:${id}] timeout ignored (already disposed: ${disposition})`,
|
|
277
|
+
);
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
if (stopped) {
|
|
281
|
+
console.log(`[crewOS:${id}] timeout ignored (stopped flag set)`);
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
disposition = 'timeout';
|
|
286
|
+
console.warn(
|
|
287
|
+
`[crewOS:${id}] ====== TIMEOUT (${TASK_TIMEOUT_MS / 60000}m) ======`,
|
|
288
|
+
);
|
|
289
|
+
|
|
290
|
+
try {
|
|
291
|
+
const freshDb = getDB();
|
|
292
|
+
const t = freshDb.data.tasks.find((task) => task.id === id);
|
|
293
|
+
if (t) {
|
|
294
|
+
t.status = 'todo';
|
|
295
|
+
t.updatedAt = new Date().toISOString();
|
|
296
|
+
freshDb.write();
|
|
297
|
+
console.log(`[crewOS:${id}] status -> todo (timeout)`);
|
|
298
|
+
} else {
|
|
299
|
+
console.warn(`[crewOS:${id}] task not found in DB during timeout`);
|
|
300
|
+
}
|
|
301
|
+
} catch (e) {
|
|
302
|
+
console.error(`[crewOS:${id}] DB write failed on timeout: ${e.message}`);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
try {
|
|
306
|
+
child.kill('SIGTERM');
|
|
307
|
+
const sigkillTimer = setTimeout(() => {
|
|
308
|
+
if (!child.killed) {
|
|
309
|
+
try {
|
|
310
|
+
child.kill('SIGKILL');
|
|
311
|
+
} catch {
|
|
312
|
+
/* noop */
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
}, 5000);
|
|
316
|
+
sigkillTimer.unref();
|
|
317
|
+
} catch (e) {
|
|
318
|
+
console.error(`[crewOS:${id}] kill failed on timeout: ${e.message}`);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
activeProcess = null;
|
|
322
|
+
activeTaskId = null;
|
|
323
|
+
}, TASK_TIMEOUT_MS);
|
|
324
|
+
|
|
325
|
+
child.stdout.on('data', (data) => {
|
|
326
|
+
const text = data.toString();
|
|
327
|
+
stdoutBytes += Buffer.byteLength(text, 'utf-8');
|
|
328
|
+
const lines = text.split('\n').filter(Boolean);
|
|
329
|
+
for (const line of lines) {
|
|
330
|
+
try {
|
|
331
|
+
const event = JSON.parse(line);
|
|
332
|
+
const part = event.part;
|
|
333
|
+
if (!part) continue;
|
|
334
|
+
|
|
335
|
+
if (part.type === 'text' && part.text) {
|
|
336
|
+
const sanitized = sanitizeProgressLine(part.text);
|
|
337
|
+
if (sanitized) progressLines.push(sanitized);
|
|
338
|
+
} else if (part.type === 'step-finish' && part.tokens) {
|
|
339
|
+
const prev = pendingTokens || { inputTokens: 0, outputTokens: 0 };
|
|
340
|
+
pendingTokens = {
|
|
341
|
+
inputTokens: prev.inputTokens + (part.tokens.input || 0),
|
|
342
|
+
outputTokens: prev.outputTokens + (part.tokens.output || 0),
|
|
343
|
+
};
|
|
344
|
+
} else {
|
|
345
|
+
const label = part.type || event.type || 'step';
|
|
346
|
+
progressLines.push(`[${label}]`);
|
|
347
|
+
}
|
|
348
|
+
} catch {
|
|
349
|
+
const sanitized = sanitizeProgressLine(line);
|
|
350
|
+
if (sanitized) progressLines.push(sanitized);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
child.stderr.on('data', (data) => {
|
|
356
|
+
const text = data.toString();
|
|
357
|
+
stderrBytes += Buffer.byteLength(text, 'utf-8');
|
|
358
|
+
const lines = text.split('\n').filter(Boolean);
|
|
359
|
+
const sanitized = lines.map(sanitizeProgressLine).filter(Boolean);
|
|
360
|
+
progressLines.push(...sanitized);
|
|
361
|
+
console.log(`[crewOS:${id} stderr]`, text.slice(0, 500));
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
child.on('exit', (code, signal) => {
|
|
365
|
+
console.log(
|
|
366
|
+
`[crewOS:${id}] ====== EXIT ====== code=${code} signal=${signal} disposition=${disposition} stopped=${stopped}`,
|
|
367
|
+
);
|
|
368
|
+
console.log(
|
|
369
|
+
`[crewOS:${id}] io: stdout=${stdoutBytes}B stderr=${stderrBytes}B progressLines=${progressLines.length}`,
|
|
370
|
+
);
|
|
371
|
+
|
|
372
|
+
if (disposition) {
|
|
373
|
+
console.log(
|
|
374
|
+
`[crewOS:${id}] exit ignored — already disposed as '${disposition}'`,
|
|
375
|
+
);
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
if (timeout) clearTimeout(timeout);
|
|
380
|
+
|
|
381
|
+
if (stopped) {
|
|
382
|
+
console.log(`[crewOS:${id}] exit ignored — task was stopped by user`);
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
if (code === 0) {
|
|
387
|
+
disposition = 'done';
|
|
388
|
+
try {
|
|
389
|
+
const freshDb = getDB();
|
|
390
|
+
const t = freshDb.data.tasks.find((task) => task.id === id);
|
|
391
|
+
if (t) {
|
|
392
|
+
t.status = 'done';
|
|
393
|
+
t.updatedAt = new Date().toISOString();
|
|
394
|
+
|
|
395
|
+
const tokens = parseTokensFromOutput(progressLines);
|
|
396
|
+
if (tokens) {
|
|
397
|
+
t.inputTokens = tokens.inputTokens;
|
|
398
|
+
t.outputTokens = tokens.outputTokens;
|
|
399
|
+
console.log(
|
|
400
|
+
`[crewOS:${id}] tokens: input=${tokens.inputTokens} output=${tokens.outputTokens}`,
|
|
401
|
+
);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
freshDb.write();
|
|
405
|
+
console.log(`[crewOS:${id}] status -> done (exit 0)`);
|
|
406
|
+
} else {
|
|
407
|
+
console.warn(`[crewOS:${id}] task not found in DB on exit`);
|
|
408
|
+
}
|
|
409
|
+
} catch (e) {
|
|
410
|
+
console.error(`[crewOS:${id}] DB write failed on exit: ${e.message}`);
|
|
411
|
+
disposition = null;
|
|
412
|
+
}
|
|
413
|
+
} else {
|
|
414
|
+
disposition = 'error';
|
|
415
|
+
console.warn(`[crewOS:${id}] non-zero exit code, reverting to todo`);
|
|
416
|
+
try {
|
|
417
|
+
const freshDb = getDB();
|
|
418
|
+
const t = freshDb.data.tasks.find((task) => task.id === id);
|
|
419
|
+
if (t) {
|
|
420
|
+
t.status = 'todo';
|
|
421
|
+
t.updatedAt = new Date().toISOString();
|
|
422
|
+
freshDb.write();
|
|
423
|
+
console.log(`[crewOS:${id}] status -> todo (exit ${code})`);
|
|
424
|
+
} else {
|
|
425
|
+
console.warn(`[crewOS:${id}] task not found in DB on error exit`);
|
|
426
|
+
}
|
|
427
|
+
} catch (e) {
|
|
428
|
+
console.error(
|
|
429
|
+
`[crewOS:${id}] DB write failed on error exit: ${e.message}`,
|
|
430
|
+
);
|
|
431
|
+
disposition = null;
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
child.on('close', (code, signal) => {
|
|
437
|
+
console.log(
|
|
438
|
+
`[crewOS:${id}] ====== CLOSE ====== code=${code} signal=${signal} disposition=${disposition} stopped=${stopped}`,
|
|
439
|
+
);
|
|
440
|
+
|
|
441
|
+
if (
|
|
442
|
+
disposition !== 'done' &&
|
|
443
|
+
disposition !== 'error' &&
|
|
444
|
+
disposition !== 'timeout'
|
|
445
|
+
) {
|
|
446
|
+
if (!stopped) {
|
|
447
|
+
console.warn(
|
|
448
|
+
`[crewOS:${id}] close fired without prior exit! code=${code}`,
|
|
449
|
+
);
|
|
450
|
+
if (timeout) clearTimeout(timeout);
|
|
451
|
+
disposition = 'close-fallback';
|
|
452
|
+
try {
|
|
453
|
+
const freshDb = getDB();
|
|
454
|
+
const t = freshDb.data.tasks.find((task) => task.id === id);
|
|
455
|
+
if (t) {
|
|
456
|
+
t.status = code === 0 ? 'done' : 'todo';
|
|
457
|
+
t.updatedAt = new Date().toISOString();
|
|
458
|
+
|
|
459
|
+
if (code === 0) {
|
|
460
|
+
const tokens = parseTokensFromOutput(progressLines);
|
|
461
|
+
if (tokens) {
|
|
462
|
+
t.inputTokens = tokens.inputTokens;
|
|
463
|
+
t.outputTokens = tokens.outputTokens;
|
|
464
|
+
console.log(
|
|
465
|
+
`[crewOS:${id}] tokens (close-fallback): input=${tokens.inputTokens} output=${tokens.outputTokens}`,
|
|
466
|
+
);
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
freshDb.write();
|
|
471
|
+
console.log(
|
|
472
|
+
`[crewOS:${id}] status -> ${t.status} (close-fallback)`,
|
|
473
|
+
);
|
|
474
|
+
}
|
|
475
|
+
} catch (e) {
|
|
476
|
+
console.error(
|
|
477
|
+
`[crewOS:${id}] DB write failed on close-fallback: ${e.message}`,
|
|
478
|
+
);
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
if (timeout) clearTimeout(timeout);
|
|
484
|
+
activeProcess = null;
|
|
485
|
+
activeTaskId = null;
|
|
486
|
+
console.log(`[crewOS:${id}] ====== TASK COMPLETE (${disposition}) ======`);
|
|
487
|
+
});
|
|
488
|
+
|
|
489
|
+
child.on('error', (err) => {
|
|
490
|
+
console.error(`[crewOS:${id}] ====== SPAWN ERROR ====== ${err.message}`);
|
|
491
|
+
if (disposition) {
|
|
492
|
+
console.log(
|
|
493
|
+
`[crewOS:${id}] spawn error ignored (already disposed: ${disposition})`,
|
|
494
|
+
);
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
497
|
+
if (timeout) clearTimeout(timeout);
|
|
498
|
+
|
|
499
|
+
disposition = 'spawn-error';
|
|
500
|
+
try {
|
|
501
|
+
const freshDb = getDB();
|
|
502
|
+
const t = freshDb.data.tasks.find((task) => task.id === id);
|
|
503
|
+
if (t) {
|
|
504
|
+
t.status = 'todo';
|
|
505
|
+
t.updatedAt = new Date().toISOString();
|
|
506
|
+
freshDb.write();
|
|
507
|
+
console.log(`[crewOS:${id}] status -> todo (spawn error)`);
|
|
508
|
+
}
|
|
509
|
+
} catch (e) {
|
|
510
|
+
console.error(
|
|
511
|
+
`[crewOS:${id}] DB write failed on spawn error: ${e.message}`,
|
|
512
|
+
);
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
activeProcess = null;
|
|
516
|
+
activeTaskId = null;
|
|
517
|
+
});
|
|
518
|
+
|
|
519
|
+
return task;
|
|
520
|
+
};
|
|
521
|
+
|
|
522
|
+
export const stopTask = () => {
|
|
523
|
+
if (!activeProcess) {
|
|
524
|
+
return { error: 'No task is running' };
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
const taskId = activeTaskId;
|
|
528
|
+
console.log(`[crewOS:${taskId}] ====== STOP REQUESTED ======`);
|
|
529
|
+
|
|
530
|
+
stopped = true;
|
|
531
|
+
try {
|
|
532
|
+
activeProcess.kill('SIGTERM');
|
|
533
|
+
console.log(`[crewOS:${taskId}] SIGTERM sent`);
|
|
534
|
+
} catch (e) {
|
|
535
|
+
console.warn(`[crewOS:${taskId}] SIGTERM failed: ${e.message}`);
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
const db = getDB();
|
|
539
|
+
const t = db.data.tasks.find((task) => task.id === taskId);
|
|
540
|
+
if (t) {
|
|
541
|
+
t.status = 'todo';
|
|
542
|
+
t.updatedAt = new Date().toISOString();
|
|
543
|
+
db.write();
|
|
544
|
+
console.log(`[crewOS:${taskId}] status -> todo (stopped)`);
|
|
545
|
+
} else {
|
|
546
|
+
console.warn(`[crewOS:${taskId}] task not found in DB on stop`);
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
activeProcess = null;
|
|
550
|
+
activeTaskId = null;
|
|
551
|
+
|
|
552
|
+
return { success: true };
|
|
553
|
+
};
|
|
554
|
+
|
|
555
|
+
/* ---- auto-do ---- */
|
|
556
|
+
|
|
557
|
+
function processNextAutoTask() {
|
|
558
|
+
if (autoStopRequested || !autoRunning) {
|
|
559
|
+
autoRunning = false;
|
|
560
|
+
autoStopRequested = false;
|
|
561
|
+
autoQueue = [];
|
|
562
|
+
console.log('[crewOS] Auto-do: cancelled');
|
|
563
|
+
return;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
if (autoQueue.length === 0) {
|
|
567
|
+
autoRunning = false;
|
|
568
|
+
console.log('[crewOS] Auto-do: queue empty, done');
|
|
569
|
+
return;
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
const nextId = autoQueue.shift();
|
|
573
|
+
const db = getDB();
|
|
574
|
+
const task = db.data.tasks.find((t) => t.id === nextId);
|
|
575
|
+
|
|
576
|
+
if (!task || task.status === 'done') {
|
|
577
|
+
console.log(
|
|
578
|
+
`[crewOS] Auto-do: skipping ${nextId} (${!task ? 'not found' : 'already done'})`,
|
|
579
|
+
);
|
|
580
|
+
processNextAutoTask();
|
|
581
|
+
return;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
if (task.status !== 'todo') {
|
|
585
|
+
task.status = 'todo';
|
|
586
|
+
task.updatedAt = new Date().toISOString();
|
|
587
|
+
db.write();
|
|
588
|
+
console.log(`[crewOS] Auto-do: reset ${nextId} to todo`);
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
console.log(
|
|
592
|
+
`[crewOS] Auto-do: starting "${task.description}" (${autoQueue.length} remaining)`,
|
|
593
|
+
);
|
|
594
|
+
const result = startTask(nextId);
|
|
595
|
+
|
|
596
|
+
if (result.error) {
|
|
597
|
+
console.log(`[crewOS] Auto-do: failed to start — ${result.error}`);
|
|
598
|
+
processNextAutoTask();
|
|
599
|
+
return;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
const poll = () => {
|
|
603
|
+
if (autoStopRequested || !autoRunning) {
|
|
604
|
+
autoRunning = false;
|
|
605
|
+
autoStopRequested = false;
|
|
606
|
+
autoQueue = [];
|
|
607
|
+
return;
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
if (!activeProcess) {
|
|
611
|
+
processNextAutoTask();
|
|
612
|
+
return;
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
setTimeout(poll, 500);
|
|
616
|
+
};
|
|
617
|
+
|
|
618
|
+
setTimeout(poll, 500);
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
export const startAutoDo = (taskIds) => {
|
|
622
|
+
if (!Array.isArray(taskIds) || taskIds.length === 0) {
|
|
623
|
+
return { error: 'No tasks provided' };
|
|
624
|
+
}
|
|
625
|
+
if (activeProcess) {
|
|
626
|
+
return { error: 'Another task is already running' };
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
const db = getDB();
|
|
630
|
+
const validIds = taskIds.filter((id) => {
|
|
631
|
+
const t = db.data.tasks.find((task) => task.id === id);
|
|
632
|
+
return t && t.status !== 'done';
|
|
633
|
+
});
|
|
634
|
+
|
|
635
|
+
if (validIds.length === 0) {
|
|
636
|
+
return { error: 'No valid tasks to process' };
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
autoQueue = [...validIds];
|
|
640
|
+
autoRunning = true;
|
|
641
|
+
autoStopRequested = false;
|
|
642
|
+
|
|
643
|
+
processNextAutoTask();
|
|
644
|
+
return { success: true, queue: autoQueue };
|
|
645
|
+
};
|
|
646
|
+
|
|
647
|
+
export const stopAutoDo = () => {
|
|
648
|
+
if (!autoRunning) return { error: 'No auto process running' };
|
|
649
|
+
|
|
650
|
+
console.log('[crewOS] ====== AUTO-DO STOP REQUESTED ======');
|
|
651
|
+
|
|
652
|
+
autoStopRequested = true;
|
|
653
|
+
autoQueue = [];
|
|
654
|
+
|
|
655
|
+
if (activeProcess) {
|
|
656
|
+
console.log('[crewOS] Auto-do: stopping active task');
|
|
657
|
+
stopTask();
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
autoRunning = false;
|
|
661
|
+
autoStopRequested = false;
|
|
662
|
+
|
|
663
|
+
console.log('[crewOS] Auto-do: stopped');
|
|
664
|
+
return { success: true };
|
|
665
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
const ANSI_RE = /[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g;
|
|
2
|
+
|
|
3
|
+
const CONTROL_RE = /[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g;
|
|
4
|
+
|
|
5
|
+
export function sanitizeProgressLine(line) {
|
|
6
|
+
let cleaned = line
|
|
7
|
+
.replace(ANSI_RE, '')
|
|
8
|
+
.replace(/\r/g, '')
|
|
9
|
+
.replace(CONTROL_RE, '')
|
|
10
|
+
.trim();
|
|
11
|
+
|
|
12
|
+
return cleaned;
|
|
13
|
+
}
|