granclaw 0.0.1-beta.97 → 0.0.1-beta.99
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/dist/backend/agent/browser-bin.js +58 -0
- package/dist/backend/agent/runner-pi.js +59 -10
- package/dist/backend/orchestrator/server.js +63 -5
- package/dist/backend/tasks-db.js +75 -9
- package/dist/backend/workspace-pool.js +32 -12
- package/dist/frontend/assets/index-CmfZy9pJ.css +1 -0
- package/dist/frontend/assets/index-zkuIH-fR.js +158 -0
- package/dist/frontend/index.html +2 -2
- package/package.json +1 -1
- package/dist/frontend/assets/index-6s2NVOlC.js +0 -158
- package/dist/frontend/assets/index-DExQ2dsz.css +0 -1
|
@@ -1,9 +1,44 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
2
35
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
36
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
37
|
};
|
|
5
38
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
39
|
exports.registerBrowserProvider = registerBrowserProvider;
|
|
40
|
+
exports.registerBrowserKiller = registerBrowserKiller;
|
|
41
|
+
exports.killBrowser = killBrowser;
|
|
7
42
|
exports._resetBrowserProvidersForTests = _resetBrowserProvidersForTests;
|
|
8
43
|
exports.buildArgv = buildArgv;
|
|
9
44
|
exports.cdpNavigate = cdpNavigate;
|
|
@@ -16,8 +51,31 @@ const providers = [];
|
|
|
16
51
|
function registerBrowserProvider(provider) {
|
|
17
52
|
providers.push(provider);
|
|
18
53
|
}
|
|
54
|
+
const killers = [];
|
|
55
|
+
function registerBrowserKiller(killer) {
|
|
56
|
+
killers.push(killer);
|
|
57
|
+
}
|
|
58
|
+
async function killBrowser(agentId) {
|
|
59
|
+
for (const killer of killers) {
|
|
60
|
+
try {
|
|
61
|
+
await killer(agentId);
|
|
62
|
+
}
|
|
63
|
+
catch { }
|
|
64
|
+
}
|
|
65
|
+
try {
|
|
66
|
+
fs_1.default.unlinkSync(`/tmp/granclaw-cdp-${agentId}.url`);
|
|
67
|
+
}
|
|
68
|
+
catch { }
|
|
69
|
+
try {
|
|
70
|
+
const { execFileSync } = await Promise.resolve().then(() => __importStar(require('child_process')));
|
|
71
|
+
const bin = process.env.AGENT_BROWSER_BIN ?? 'agent-browser';
|
|
72
|
+
execFileSync(bin, ['--session', agentId, 'close'], { timeout: 5000, stdio: 'pipe' });
|
|
73
|
+
}
|
|
74
|
+
catch { }
|
|
75
|
+
}
|
|
19
76
|
function _resetBrowserProvidersForTests() {
|
|
20
77
|
providers.length = 0;
|
|
78
|
+
killers.length = 0;
|
|
21
79
|
}
|
|
22
80
|
function buildArgv(res, command, args) {
|
|
23
81
|
return [...res.preCommandArgs, command, ...args, ...res.postCommandArgs];
|
|
@@ -579,19 +579,31 @@ async function runAgent(agent, message, onChunk, options) {
|
|
|
579
579
|
pi.registerTool({
|
|
580
580
|
name: 'list_tasks',
|
|
581
581
|
label: 'List Tasks',
|
|
582
|
-
description: 'List tasks from the kanban board. Optionally filter by status.',
|
|
582
|
+
description: 'List tasks from the kanban board. Optionally filter by status, search text, or tags.',
|
|
583
583
|
promptSnippet: 'List tasks',
|
|
584
|
-
promptGuidelines: [
|
|
584
|
+
promptGuidelines: [
|
|
585
|
+
'Use to see tasks across columns. Filter by column status, search title/description, or filter by tags.',
|
|
586
|
+
'Default columns are to_do, in_progress, done — but custom columns may exist.',
|
|
587
|
+
],
|
|
585
588
|
parameters: {
|
|
586
589
|
type: 'object',
|
|
587
590
|
properties: {
|
|
588
|
-
status: { type: 'string',
|
|
591
|
+
status: { type: 'string', description: 'Filter by column status (e.g. to_do, in_progress, done)' },
|
|
592
|
+
search: { type: 'string', description: 'Search text — matches title or description' },
|
|
593
|
+
tags: { type: 'array', items: { type: 'string' }, description: 'Filter by tags — tasks must have ALL listed tags' },
|
|
589
594
|
},
|
|
590
595
|
},
|
|
591
596
|
async execute(_id, params) {
|
|
592
597
|
try {
|
|
593
|
-
const
|
|
594
|
-
|
|
598
|
+
const qp = new URLSearchParams();
|
|
599
|
+
if (params.status)
|
|
600
|
+
qp.set('status', params.status);
|
|
601
|
+
if (params.search)
|
|
602
|
+
qp.set('search', params.search);
|
|
603
|
+
if (params.tags?.length)
|
|
604
|
+
qp.set('tags', params.tags.join(','));
|
|
605
|
+
const qs = qp.toString() ? `?${qp.toString()}` : '';
|
|
606
|
+
const data = await fetchJson(`${taskBase()}${qs}`);
|
|
595
607
|
return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
|
|
596
608
|
}
|
|
597
609
|
catch (e) {
|
|
@@ -629,14 +641,15 @@ async function runAgent(agent, message, onChunk, options) {
|
|
|
629
641
|
promptSnippet: 'Create a task',
|
|
630
642
|
promptGuidelines: [
|
|
631
643
|
'Use when breaking down work into subtasks or tracking a new action item.',
|
|
632
|
-
'Status defaults to
|
|
644
|
+
'Status defaults to to_do. Use markdown in description. Tags help organize tasks.',
|
|
633
645
|
],
|
|
634
646
|
parameters: {
|
|
635
647
|
type: 'object',
|
|
636
648
|
properties: {
|
|
637
649
|
title: { type: 'string', description: 'Short task title (under 80 chars)' },
|
|
638
650
|
description: { type: 'string', description: 'Full description in markdown (optional)' },
|
|
639
|
-
status: { type: 'string',
|
|
651
|
+
status: { type: 'string', description: 'Column status (default: to_do)' },
|
|
652
|
+
tags: { type: 'array', items: { type: 'string' }, description: 'Tags for categorization (optional)' },
|
|
640
653
|
},
|
|
641
654
|
required: ['title'],
|
|
642
655
|
},
|
|
@@ -657,11 +670,11 @@ async function runAgent(agent, message, onChunk, options) {
|
|
|
657
670
|
pi.registerTool({
|
|
658
671
|
name: 'update_task',
|
|
659
672
|
label: 'Update Task',
|
|
660
|
-
description: 'Update a task\'s title, description, or
|
|
673
|
+
description: 'Update a task\'s title, description, status, or tags.',
|
|
661
674
|
promptSnippet: 'Update a task',
|
|
662
675
|
promptGuidelines: [
|
|
663
676
|
'Only send fields you want to change.',
|
|
664
|
-
'Move to in_progress when starting,
|
|
677
|
+
'Move to in_progress when starting, done when complete.',
|
|
665
678
|
],
|
|
666
679
|
parameters: {
|
|
667
680
|
type: 'object',
|
|
@@ -669,7 +682,8 @@ async function runAgent(agent, message, onChunk, options) {
|
|
|
669
682
|
taskId: { type: 'string', description: 'Task ID, e.g. TSK-001' },
|
|
670
683
|
title: { type: 'string', description: 'New title (optional)' },
|
|
671
684
|
description: { type: 'string', description: 'New description in markdown (optional)' },
|
|
672
|
-
status: { type: 'string',
|
|
685
|
+
status: { type: 'string', description: 'New column status (optional)' },
|
|
686
|
+
tags: { type: 'array', items: { type: 'string' }, description: 'Replace tags (optional)' },
|
|
673
687
|
},
|
|
674
688
|
required: ['taskId'],
|
|
675
689
|
},
|
|
@@ -838,6 +852,41 @@ async function runAgent(agent, message, onChunk, options) {
|
|
|
838
852
|
},
|
|
839
853
|
});
|
|
840
854
|
});
|
|
855
|
+
extensionFactories.push((pi) => {
|
|
856
|
+
pi.registerTool({
|
|
857
|
+
name: 'browser_restart',
|
|
858
|
+
label: 'Restart Browser',
|
|
859
|
+
description: 'Kill the browser process and force a fresh start on the next browser command. ' +
|
|
860
|
+
'Cookies, logins, and profile data are preserved — only the process is restarted. ' +
|
|
861
|
+
'Use when the browser is hung, unresponsive, showing stale state, or after a proxy change.',
|
|
862
|
+
promptSnippet: 'Kill and respawn the browser daemon (cookies survive)',
|
|
863
|
+
promptGuidelines: [
|
|
864
|
+
'Use when browser commands timeout repeatedly or the browser appears hung.',
|
|
865
|
+
'Use after you receive BROWSER_BLOCKED if you want a clean process before retrying.',
|
|
866
|
+
'Cookies and saved logins persist — only the process is restarted.',
|
|
867
|
+
'The next browser command after this will spawn a fresh browser automatically.',
|
|
868
|
+
],
|
|
869
|
+
parameters: { type: 'object', properties: {} },
|
|
870
|
+
async execute() {
|
|
871
|
+
try {
|
|
872
|
+
if (browserState.handle) {
|
|
873
|
+
try {
|
|
874
|
+
await (0, session_manager_js_1.finalizeSession)(browserState.handle, 'closed');
|
|
875
|
+
}
|
|
876
|
+
catch { }
|
|
877
|
+
browserState.handle = null;
|
|
878
|
+
}
|
|
879
|
+
await (0, browser_bin_js_1.killBrowser)(agent.id);
|
|
880
|
+
return { content: [{ type: 'text', text: 'Browser killed. Cookies and logins are preserved. The next browser command will spawn a fresh instance.' }] };
|
|
881
|
+
}
|
|
882
|
+
catch (err) {
|
|
883
|
+
browserState.handle = null;
|
|
884
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
885
|
+
return { content: [{ type: 'text', text: `browser_restart partial: ${msg} — next browser call will still spawn fresh.` }] };
|
|
886
|
+
}
|
|
887
|
+
},
|
|
888
|
+
});
|
|
889
|
+
});
|
|
841
890
|
extensionFactories.push((pi) => {
|
|
842
891
|
pi.registerTool({
|
|
843
892
|
name: 'request_human_browser_takeover',
|
|
@@ -722,6 +722,51 @@ function createServer() {
|
|
|
722
722
|
content,
|
|
723
723
|
});
|
|
724
724
|
});
|
|
725
|
+
app.get('/agents/:id/task-columns', (req, res) => {
|
|
726
|
+
const managed = (0, agent_manager_js_1.getManagedAgent)(req.params.id);
|
|
727
|
+
if (!managed) {
|
|
728
|
+
res.status(404).json({ error: 'Agent not found' });
|
|
729
|
+
return;
|
|
730
|
+
}
|
|
731
|
+
res.json((0, tasks_db_js_1.listColumns)(req.params.id));
|
|
732
|
+
});
|
|
733
|
+
app.post('/agents/:id/task-columns', (req, res) => {
|
|
734
|
+
const managed = (0, agent_manager_js_1.getManagedAgent)(req.params.id);
|
|
735
|
+
if (!managed) {
|
|
736
|
+
res.status(404).json({ error: 'Agent not found' });
|
|
737
|
+
return;
|
|
738
|
+
}
|
|
739
|
+
const { label } = req.body;
|
|
740
|
+
if (!label) {
|
|
741
|
+
res.status(400).json({ error: 'label required' });
|
|
742
|
+
return;
|
|
743
|
+
}
|
|
744
|
+
try {
|
|
745
|
+
const column = (0, tasks_db_js_1.createColumn)(req.params.id, { label });
|
|
746
|
+
res.status(201).json(column);
|
|
747
|
+
}
|
|
748
|
+
catch (e) {
|
|
749
|
+
res.status(409).json({ error: e.message });
|
|
750
|
+
}
|
|
751
|
+
});
|
|
752
|
+
app.delete('/agents/:id/task-columns/:columnId', (req, res) => {
|
|
753
|
+
const managed = (0, agent_manager_js_1.getManagedAgent)(req.params.id);
|
|
754
|
+
if (!managed) {
|
|
755
|
+
res.status(404).json({ error: 'Agent not found' });
|
|
756
|
+
return;
|
|
757
|
+
}
|
|
758
|
+
try {
|
|
759
|
+
const deleted = (0, tasks_db_js_1.deleteColumn)(req.params.id, req.params.columnId);
|
|
760
|
+
if (!deleted) {
|
|
761
|
+
res.status(404).json({ error: 'Column not found' });
|
|
762
|
+
return;
|
|
763
|
+
}
|
|
764
|
+
res.json({ ok: true });
|
|
765
|
+
}
|
|
766
|
+
catch (e) {
|
|
767
|
+
res.status(400).json({ error: e.message });
|
|
768
|
+
}
|
|
769
|
+
});
|
|
725
770
|
app.get('/agents/:id/tasks', (req, res) => {
|
|
726
771
|
const managed = (0, agent_manager_js_1.getManagedAgent)(req.params.id);
|
|
727
772
|
if (!managed) {
|
|
@@ -729,7 +774,10 @@ function createServer() {
|
|
|
729
774
|
return;
|
|
730
775
|
}
|
|
731
776
|
const status = req.query.status;
|
|
732
|
-
|
|
777
|
+
const search = req.query.search;
|
|
778
|
+
const tagsParam = req.query.tags;
|
|
779
|
+
const tags = tagsParam ? tagsParam.split(',') : undefined;
|
|
780
|
+
res.json((0, tasks_db_js_1.listTasks)(req.params.id, { status, search, tags }));
|
|
733
781
|
});
|
|
734
782
|
app.post('/agents/:id/tasks', (req, res) => {
|
|
735
783
|
const managed = (0, agent_manager_js_1.getManagedAgent)(req.params.id);
|
|
@@ -737,14 +785,23 @@ function createServer() {
|
|
|
737
785
|
res.status(404).json({ error: 'Agent not found' });
|
|
738
786
|
return;
|
|
739
787
|
}
|
|
740
|
-
const { title, description, status } = req.body;
|
|
788
|
+
const { title, description, status, tags } = req.body;
|
|
741
789
|
if (!title) {
|
|
742
790
|
res.status(400).json({ error: 'title required' });
|
|
743
791
|
return;
|
|
744
792
|
}
|
|
745
|
-
const task = (0, tasks_db_js_1.createTask)(req.params.id, { title, description, status
|
|
793
|
+
const task = (0, tasks_db_js_1.createTask)(req.params.id, { title, description, status, tags });
|
|
746
794
|
res.status(201).json(task);
|
|
747
795
|
});
|
|
796
|
+
app.delete('/agents/:id/tasks', (req, res) => {
|
|
797
|
+
const managed = (0, agent_manager_js_1.getManagedAgent)(req.params.id);
|
|
798
|
+
if (!managed) {
|
|
799
|
+
res.status(404).json({ error: 'Agent not found' });
|
|
800
|
+
return;
|
|
801
|
+
}
|
|
802
|
+
const count = (0, tasks_db_js_1.clearTasks)(req.params.id);
|
|
803
|
+
res.json({ ok: true, deleted: count });
|
|
804
|
+
});
|
|
748
805
|
app.get('/agents/:id/tasks/:taskId', (req, res) => {
|
|
749
806
|
const managed = (0, agent_manager_js_1.getManagedAgent)(req.params.id);
|
|
750
807
|
if (!managed) {
|
|
@@ -765,8 +822,8 @@ function createServer() {
|
|
|
765
822
|
res.status(404).json({ error: 'Agent not found' });
|
|
766
823
|
return;
|
|
767
824
|
}
|
|
768
|
-
const { title, description, status } = req.body;
|
|
769
|
-
const task = (0, tasks_db_js_1.updateTask)(req.params.id, req.params.taskId, { title, description, status
|
|
825
|
+
const { title, description, status, tags } = req.body;
|
|
826
|
+
const task = (0, tasks_db_js_1.updateTask)(req.params.id, req.params.taskId, { title, description, status, tags });
|
|
770
827
|
if (!task) {
|
|
771
828
|
res.status(404).json({ error: 'Task not found' });
|
|
772
829
|
return;
|
|
@@ -1530,6 +1587,7 @@ function createServer() {
|
|
|
1530
1587
|
(0, loader_js_1.loadExtensions)({
|
|
1531
1588
|
app,
|
|
1532
1589
|
registerBrowserProvider: browser_bin_js_1.registerBrowserProvider,
|
|
1590
|
+
registerBrowserKiller: browser_bin_js_1.registerBrowserKiller,
|
|
1533
1591
|
registerCdpSession: browser_live_js_1.registerExternalCdpSession,
|
|
1534
1592
|
removeCdpSession: browser_live_js_1.removeExternalCdpSession,
|
|
1535
1593
|
registerTakeoverResolvedListener: takeover_listeners_js_1.registerTakeoverResolvedListener,
|
package/dist/backend/tasks-db.js
CHANGED
|
@@ -8,6 +8,10 @@ exports.getTask = getTask;
|
|
|
8
8
|
exports.createTask = createTask;
|
|
9
9
|
exports.updateTask = updateTask;
|
|
10
10
|
exports.deleteTask = deleteTask;
|
|
11
|
+
exports.clearTasks = clearTasks;
|
|
12
|
+
exports.listColumns = listColumns;
|
|
13
|
+
exports.createColumn = createColumn;
|
|
14
|
+
exports.deleteColumn = deleteColumn;
|
|
11
15
|
exports.listComments = listComments;
|
|
12
16
|
exports.createComment = createComment;
|
|
13
17
|
exports.closeTasksDb = closeTasksDb;
|
|
@@ -22,11 +26,13 @@ function getDb(agentId) {
|
|
|
22
26
|
return (0, workspace_pool_js_1.getWorkspaceDb)(path_1.default.resolve(config_js_1.REPO_ROOT, agent.workspaceDir));
|
|
23
27
|
}
|
|
24
28
|
function rowToTask(r) {
|
|
29
|
+
const tagsStr = r.tags || '';
|
|
25
30
|
return {
|
|
26
31
|
id: r.id,
|
|
27
32
|
title: r.title,
|
|
28
33
|
description: r.description,
|
|
29
34
|
status: r.status,
|
|
35
|
+
tags: tagsStr ? tagsStr.split(',') : [],
|
|
30
36
|
source: r.source,
|
|
31
37
|
updatedBy: r.updated_by ?? null,
|
|
32
38
|
createdAt: r.created_at,
|
|
@@ -42,16 +48,42 @@ function rowToComment(r) {
|
|
|
42
48
|
createdAt: r.created_at,
|
|
43
49
|
};
|
|
44
50
|
}
|
|
51
|
+
function rowToColumn(r) {
|
|
52
|
+
return {
|
|
53
|
+
id: r.id,
|
|
54
|
+
label: r.label,
|
|
55
|
+
position: r.position,
|
|
56
|
+
createdAt: r.created_at,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
function slugify(label) {
|
|
60
|
+
return label.toLowerCase().replace(/[^a-z0-9]+/g, '_').replace(/^_|_$/g, '') || 'column';
|
|
61
|
+
}
|
|
45
62
|
function nextTaskId(db) {
|
|
46
63
|
const row = db.prepare(`SELECT COALESCE(MAX(CAST(SUBSTR(id, 5) AS INTEGER)), 0) + 1 AS next FROM tasks`).get();
|
|
47
64
|
return `TSK-${String(row.next).padStart(3, '0')}`;
|
|
48
65
|
}
|
|
49
|
-
function listTasks(agentId,
|
|
66
|
+
function listTasks(agentId, opts) {
|
|
50
67
|
const db = getDb(agentId);
|
|
51
|
-
|
|
52
|
-
|
|
68
|
+
const conditions = [];
|
|
69
|
+
const params = [];
|
|
70
|
+
if (opts?.status) {
|
|
71
|
+
conditions.push('status = ?');
|
|
72
|
+
params.push(opts.status);
|
|
73
|
+
}
|
|
74
|
+
if (opts?.search) {
|
|
75
|
+
conditions.push('(title LIKE ? OR description LIKE ?)');
|
|
76
|
+
const term = `%${opts.search}%`;
|
|
77
|
+
params.push(term, term);
|
|
78
|
+
}
|
|
79
|
+
if (opts?.tags?.length) {
|
|
80
|
+
for (const tag of opts.tags) {
|
|
81
|
+
conditions.push("(',' || tags || ',' LIKE ?)");
|
|
82
|
+
params.push(`%,${tag},%`);
|
|
83
|
+
}
|
|
53
84
|
}
|
|
54
|
-
|
|
85
|
+
const where = conditions.length ? ` WHERE ${conditions.join(' AND ')}` : '';
|
|
86
|
+
return db.prepare(`SELECT * FROM tasks${where} ORDER BY created_at`).all(...params).map(rowToTask);
|
|
55
87
|
}
|
|
56
88
|
function getTask(agentId, taskId) {
|
|
57
89
|
const db = getDb(agentId);
|
|
@@ -62,10 +94,11 @@ function createTask(agentId, data) {
|
|
|
62
94
|
const db = getDb(agentId);
|
|
63
95
|
const id = nextTaskId(db);
|
|
64
96
|
const now = Math.floor(Date.now() / 1000);
|
|
97
|
+
const tagsStr = (data.tags ?? []).join(',');
|
|
65
98
|
db.prepare(`
|
|
66
|
-
INSERT INTO tasks (id, title, description, status, source, created_at, updated_at)
|
|
67
|
-
VALUES (?, ?, ?, ?, 'human', ?, ?)
|
|
68
|
-
`).run(id, data.title, data.description ?? '', data.status ?? '
|
|
99
|
+
INSERT INTO tasks (id, title, description, status, tags, source, created_at, updated_at)
|
|
100
|
+
VALUES (?, ?, ?, ?, ?, 'human', ?, ?)
|
|
101
|
+
`).run(id, data.title, data.description ?? '', data.status ?? 'to_do', tagsStr, now, now);
|
|
69
102
|
return getTask(agentId, id);
|
|
70
103
|
}
|
|
71
104
|
function updateTask(agentId, taskId, data) {
|
|
@@ -74,10 +107,11 @@ function updateTask(agentId, taskId, data) {
|
|
|
74
107
|
if (!existing)
|
|
75
108
|
return null;
|
|
76
109
|
const now = Math.floor(Date.now() / 1000);
|
|
110
|
+
const tagsStr = data.tags !== undefined ? data.tags.join(',') : existing.tags.join(',');
|
|
77
111
|
db.prepare(`
|
|
78
|
-
UPDATE tasks SET title = ?, description = ?, status = ?, updated_by = 'human', updated_at = ?
|
|
112
|
+
UPDATE tasks SET title = ?, description = ?, status = ?, tags = ?, updated_by = 'human', updated_at = ?
|
|
79
113
|
WHERE id = ?
|
|
80
|
-
`).run(data.title ?? existing.title, data.description ?? existing.description, data.status ?? existing.status, now, taskId);
|
|
114
|
+
`).run(data.title ?? existing.title, data.description ?? existing.description, data.status ?? existing.status, tagsStr, now, taskId);
|
|
81
115
|
return getTask(agentId, taskId);
|
|
82
116
|
}
|
|
83
117
|
function deleteTask(agentId, taskId) {
|
|
@@ -85,6 +119,38 @@ function deleteTask(agentId, taskId) {
|
|
|
85
119
|
const result = db.prepare(`DELETE FROM tasks WHERE id = ?`).run(taskId);
|
|
86
120
|
return result.changes > 0;
|
|
87
121
|
}
|
|
122
|
+
function clearTasks(agentId) {
|
|
123
|
+
const db = getDb(agentId);
|
|
124
|
+
const result = db.prepare('DELETE FROM tasks').run();
|
|
125
|
+
return result.changes;
|
|
126
|
+
}
|
|
127
|
+
function listColumns(agentId) {
|
|
128
|
+
const db = getDb(agentId);
|
|
129
|
+
return db.prepare('SELECT * FROM task_columns ORDER BY position').all().map(rowToColumn);
|
|
130
|
+
}
|
|
131
|
+
function createColumn(agentId, data) {
|
|
132
|
+
const db = getDb(agentId);
|
|
133
|
+
const id = slugify(data.label);
|
|
134
|
+
const existing = db.prepare('SELECT id FROM task_columns WHERE id = ?').get(id);
|
|
135
|
+
if (existing)
|
|
136
|
+
throw new Error(`Column "${id}" already exists`);
|
|
137
|
+
const maxPos = db.prepare('SELECT COALESCE(MAX(position), -1) + 1 AS next FROM task_columns').get().next;
|
|
138
|
+
const now = Math.floor(Date.now() / 1000);
|
|
139
|
+
db.prepare('INSERT INTO task_columns (id, label, position, created_at) VALUES (?, ?, ?, ?)').run(id, data.label, maxPos, now);
|
|
140
|
+
return { id, label: data.label, position: maxPos, createdAt: now };
|
|
141
|
+
}
|
|
142
|
+
function deleteColumn(agentId, columnId) {
|
|
143
|
+
const db = getDb(agentId);
|
|
144
|
+
const count = db.prepare('SELECT COUNT(*) as n FROM task_columns').get().n;
|
|
145
|
+
if (count <= 1)
|
|
146
|
+
throw new Error('Cannot delete the last column');
|
|
147
|
+
const firstCol = db.prepare('SELECT id FROM task_columns WHERE id != ? ORDER BY position LIMIT 1').get(columnId);
|
|
148
|
+
if (firstCol) {
|
|
149
|
+
db.prepare('UPDATE tasks SET status = ? WHERE status = ?').run(firstCol.id, columnId);
|
|
150
|
+
}
|
|
151
|
+
const result = db.prepare('DELETE FROM task_columns WHERE id = ?').run(columnId);
|
|
152
|
+
return result.changes > 0;
|
|
153
|
+
}
|
|
88
154
|
function listComments(agentId, taskId) {
|
|
89
155
|
const db = getDb(agentId);
|
|
90
156
|
return db.prepare(`SELECT * FROM comments WHERE task_id = ? ORDER BY created_at ASC`).all(taskId).map(rowToComment);
|
|
@@ -49,8 +49,8 @@ function getWorkspaceDb(workspaceDir) {
|
|
|
49
49
|
id TEXT PRIMARY KEY,
|
|
50
50
|
title TEXT NOT NULL,
|
|
51
51
|
description TEXT NOT NULL DEFAULT '',
|
|
52
|
-
status TEXT NOT NULL DEFAULT '
|
|
53
|
-
|
|
52
|
+
status TEXT NOT NULL DEFAULT 'to_do',
|
|
53
|
+
tags TEXT NOT NULL DEFAULT '',
|
|
54
54
|
source TEXT NOT NULL DEFAULT 'agent'
|
|
55
55
|
CHECK(source IN ('agent','human')),
|
|
56
56
|
updated_by TEXT DEFAULT NULL
|
|
@@ -65,6 +65,12 @@ function getWorkspaceDb(workspaceDir) {
|
|
|
65
65
|
source TEXT NOT NULL CHECK(source IN ('agent','human')),
|
|
66
66
|
created_at INTEGER NOT NULL
|
|
67
67
|
);
|
|
68
|
+
CREATE TABLE IF NOT EXISTS task_columns (
|
|
69
|
+
id TEXT PRIMARY KEY,
|
|
70
|
+
label TEXT NOT NULL,
|
|
71
|
+
position INTEGER NOT NULL,
|
|
72
|
+
created_at INTEGER NOT NULL
|
|
73
|
+
);
|
|
68
74
|
CREATE INDEX IF NOT EXISTS idx_tasks_status ON tasks(status);
|
|
69
75
|
CREATE INDEX IF NOT EXISTS idx_comments_task ON comments(task_id, created_at);
|
|
70
76
|
`);
|
|
@@ -122,15 +128,17 @@ function getWorkspaceDb(workspaceDir) {
|
|
|
122
128
|
db.exec(`ALTER TABLE run_steps ADD COLUMN events TEXT`);
|
|
123
129
|
console.log('[workspace-pool] migrated run_steps table (added events column)');
|
|
124
130
|
}
|
|
125
|
-
const
|
|
126
|
-
if (
|
|
131
|
+
const taskCols = db.pragma('table_info(tasks)').map(c => c.name);
|
|
132
|
+
if (taskCols.length > 0 && !taskCols.includes('tags')) {
|
|
127
133
|
db.exec(`
|
|
128
|
-
|
|
134
|
+
DROP TABLE IF EXISTS comments;
|
|
135
|
+
DROP TABLE IF EXISTS tasks;
|
|
136
|
+
CREATE TABLE tasks (
|
|
129
137
|
id TEXT PRIMARY KEY,
|
|
130
138
|
title TEXT NOT NULL,
|
|
131
139
|
description TEXT NOT NULL DEFAULT '',
|
|
132
|
-
status TEXT NOT NULL DEFAULT '
|
|
133
|
-
|
|
140
|
+
status TEXT NOT NULL DEFAULT 'to_do',
|
|
141
|
+
tags TEXT NOT NULL DEFAULT '',
|
|
134
142
|
source TEXT NOT NULL DEFAULT 'agent'
|
|
135
143
|
CHECK(source IN ('agent','human')),
|
|
136
144
|
updated_by TEXT DEFAULT NULL
|
|
@@ -138,12 +146,24 @@ function getWorkspaceDb(workspaceDir) {
|
|
|
138
146
|
created_at INTEGER NOT NULL,
|
|
139
147
|
updated_at INTEGER NOT NULL
|
|
140
148
|
);
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
149
|
+
CREATE TABLE comments (
|
|
150
|
+
id TEXT PRIMARY KEY,
|
|
151
|
+
task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
|
|
152
|
+
body TEXT NOT NULL,
|
|
153
|
+
source TEXT NOT NULL CHECK(source IN ('agent','human')),
|
|
154
|
+
created_at INTEGER NOT NULL
|
|
155
|
+
);
|
|
156
|
+
CREATE INDEX idx_tasks_status ON tasks(status);
|
|
157
|
+
CREATE INDEX idx_comments_task ON comments(task_id, created_at);
|
|
145
158
|
`);
|
|
146
|
-
console.log('[workspace-pool]
|
|
159
|
+
console.log('[workspace-pool] recreated tasks table (v2: tags + custom columns)');
|
|
160
|
+
}
|
|
161
|
+
const colCount = db.prepare('SELECT COUNT(*) as n FROM task_columns').get().n;
|
|
162
|
+
if (colCount === 0) {
|
|
163
|
+
const seedNow = Math.floor(Date.now() / 1000);
|
|
164
|
+
db.prepare('INSERT INTO task_columns (id, label, position, created_at) VALUES (?, ?, ?, ?)').run('to_do', 'To Do', 0, seedNow);
|
|
165
|
+
db.prepare('INSERT INTO task_columns (id, label, position, created_at) VALUES (?, ?, ?, ?)').run('in_progress', 'In Progress', 1, seedNow);
|
|
166
|
+
db.prepare('INSERT INTO task_columns (id, label, position, created_at) VALUES (?, ?, ?, ?)').run('done', 'Done', 2, seedNow);
|
|
147
167
|
}
|
|
148
168
|
const stepsSchema = db.prepare(`SELECT sql FROM sqlite_master WHERE type='table' AND name='steps'`).get();
|
|
149
169
|
if (stepsSchema?.sql && !stepsSchema.sql.includes('agent')) {
|