@chemx/starter-kit 26.9.20-228 → 26.9.20-369

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/blueprints/_package.json +17 -0
  2. package/blueprints/_tsconfig.json +15 -0
  3. package/blueprints/_vitest.config.ts +9 -0
  4. package/cli/generator.d.ts +3 -2
  5. package/cli/generator.js +59 -0
  6. package/cli/generator.spec.js +17 -0
  7. package/cli/help.js +18 -0
  8. package/cli/mcp/installer.spec.js +4 -6
  9. package/cli/patcher.js +42 -0
  10. package/cli/patcher.spec.js +19 -0
  11. package/cli/reader.js +25 -0
  12. package/cli/scaffold.js +27 -7
  13. package/cli/team/team-multi-model-telemetry.spec.js +80 -0
  14. package/cli/team/team-schema.js +6 -0
  15. package/cli/team/team-telemetry.js +61 -38
  16. package/cli/theme.js +1 -0
  17. package/cli/ui-actions-codebase.js +75 -0
  18. package/cli/ui-actions-studio.js +91 -0
  19. package/cli/ui-actions-tasks.js +87 -0
  20. package/cli/ui-actions.js +11 -27
  21. package/cli/ui-canonical-routes.spec.js +130 -0
  22. package/cli/ui-client-api.js +58 -20
  23. package/cli/ui-client-filetree.js +93 -0
  24. package/cli/ui-client-forum.js +85 -0
  25. package/cli/ui-client-script.js +52 -37
  26. package/cli/ui-client-studio.js +95 -0
  27. package/cli/ui-e2e-verification.spec.js +186 -0
  28. package/cli/ui-filetree.spec.js +175 -0
  29. package/cli/ui-forum-data.js +97 -0
  30. package/cli/ui-forum-topics.js +65 -0
  31. package/cli/ui-handlers.js +61 -65
  32. package/cli/ui-html.js +6 -0
  33. package/cli/ui-kanban.spec.js +206 -0
  34. package/cli/ui-server-routes.js +56 -28
  35. package/cli/ui-server.js +12 -6
  36. package/cli/ui-styles.js +80 -27
  37. package/cli/ui-template-agents.js +64 -0
  38. package/cli/ui-template-dbstudio.js +82 -0
  39. package/cli/ui-template-filetree.js +92 -0
  40. package/cli/ui-template-forum.js +86 -0
  41. package/cli/ui-template-kanban.js +83 -0
  42. package/cli/ui-template-thread.js +51 -0
  43. package/cli/ui-template-timeline.js +35 -0
  44. package/cli/ui-template-workbench.js +55 -0
  45. package/cli/ui-template.js +14 -34
  46. package/cli/ui-vbulletin.spec.js +189 -0
  47. package/cli/ui-workbench-dbstudio.spec.js +172 -0
  48. package/cli/verify.js +107 -16
  49. package/package.json +1 -1
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Chemical X UI Action Handlers: Codebase Explorer & Symbol Inspector
3
+ */
4
+
5
+ export const buildFileTree = (files = []) => {
6
+ const root = { name: 'root', path: '', isFolder: true, children: [] };
7
+ for (const f of files) {
8
+ const parts = (f.path || '').split('/');
9
+ let cur = root;
10
+ for (let i = 0; i < parts.length; i++) {
11
+ const isFile = i === parts.length - 1;
12
+ if (isFile) {
13
+ cur.children.push({
14
+ name: parts[i], path: f.path, isFolder: false, lines: Number(f.lines || 0),
15
+ tier: f.tier || 'utility', healthScore: Number(f.healthScore ?? 100), hazardCount: Number(f.hazardCount ?? 0)
16
+ });
17
+ } else {
18
+ const p = parts.slice(0, i + 1).join('/');
19
+ let folder = cur.children.find((c) => c.isFolder && c.name === parts[i]);
20
+ if (!folder) {
21
+ folder = { name: parts[i], path: p, isFolder: true, children: [] };
22
+ cur.children.push(folder);
23
+ }
24
+ cur = folder;
25
+ }
26
+ }
27
+ }
28
+ return root.children;
29
+ };
30
+
31
+ export const handleCodebaseIndex = (db) => {
32
+ if (!db) return { success: false, error: 'Database unavailable' };
33
+ const files = db.prepare('SELECT path, tier, lines, health_score, hazard_count FROM files ORDER BY path ASC').all().map((f) => ({
34
+ path: f.path, tier: f.tier || 'utility', lines: Number(f.lines || 0),
35
+ healthScore: Number(f.health_score ?? 100), hazardCount: Number(f.hazard_count ?? 0)
36
+ }));
37
+ const violations = db.prepare('SELECT file_path, rule, severity, line, hazard, directive FROM violations ORDER BY id DESC LIMIT 50').all().map((v) => ({
38
+ filePath: v.file_path, rule: v.rule, severity: v.severity, line: Number(v.line || 0), hazard: v.hazard, directive: v.directive
39
+ }));
40
+ return { success: true, files, violations };
41
+ };
42
+
43
+ export const handleCodebaseTree = (db) => {
44
+ const res = handleCodebaseIndex(db);
45
+ if (!res.success) return res;
46
+ return { success: true, tree: buildFileTree(res.files), files: res.files, count: res.files.length };
47
+ };
48
+
49
+ export const handleCodebaseFile = (db, targetPath) => {
50
+ if (!db) return { success: false, error: 'Database unavailable' };
51
+ if (!targetPath) return { success: false, error: 'File path required' };
52
+ const p = targetPath.trim();
53
+ const f = db.prepare('SELECT path, tier, lines, chars, health_score, hazard_count FROM files WHERE path = ?').get(p);
54
+ if (!f) return { success: false, error: 'File not indexed', path: p };
55
+
56
+ const symbols = db.prepare('SELECT name, kind, is_export, start_line, end_line, signature FROM symbols WHERE file_path = ? ORDER BY start_line ASC').all(p).map((s) => ({
57
+ name: s.name, kind: s.kind, isExport: Boolean(s.is_export), startLine: Number(s.start_line || 1), endLine: Number(s.end_line || 1), signature: s.signature || ''
58
+ }));
59
+ const imports = db.prepare('SELECT imported_symbol, source_module, line FROM imports WHERE importer_path = ? ORDER BY line ASC').all(p).map((i) => ({
60
+ symbol: i.imported_symbol, source: i.source_module, line: Number(i.line || 1)
61
+ }));
62
+ const base = p.split('/').pop().replace(/\.[^.]+$/, '');
63
+ const connections = db.prepare('SELECT DISTINCT i.importer_path, i.imported_symbol, i.source_module, i.line, f.tier FROM imports i LEFT JOIN files f ON i.importer_path = f.path WHERE i.source_module LIKE ? OR i.source_module LIKE ? ORDER BY i.importer_path ASC').all(`%/${base}%`, `%${base}%`).map((c) => ({
64
+ importerPath: c.importer_path, importedSymbol: c.imported_symbol, sourceModule: c.source_module, line: Number(c.line || 1), tier: c.tier || 'utility'
65
+ }));
66
+ const violations = db.prepare('SELECT rule, severity, line, hazard, directive FROM violations WHERE file_path = ? ORDER BY line ASC').all(p).map((v) => ({
67
+ rule: v.rule, severity: v.severity, line: Number(v.line || 1), hazard: v.hazard, directive: v.directive
68
+ }));
69
+
70
+ return {
71
+ success: true,
72
+ file: { path: f.path, tier: f.tier || 'utility', lines: Number(f.lines || 0), healthScore: Number(f.health_score ?? 100), hazardCount: Number(f.hazard_count ?? 0) },
73
+ symbols, exports: symbols.filter((s) => s.isExport), imports, connections, violations
74
+ };
75
+ };
@@ -0,0 +1,91 @@
1
+ /**
2
+ * Chemical X UI Studio Actions: Prompt Workbench & Database Studio (R7/R8)
3
+ */
4
+ import fs from 'node:fs';
5
+ import path from 'node:path';
6
+ import { performance } from 'node:perf_hooks';
7
+ import { runAudit } from './audit.js';
8
+ import {
9
+ buildMasterPrompt,
10
+ buildGradeFPrompt,
11
+ buildGradeDPrompt,
12
+ buildGradeCPrompt,
13
+ buildGradeBPrompt,
14
+ buildAiSlopPrompt,
15
+ buildHotspotsPrompt
16
+ } from './audit/prompts.js';
17
+
18
+ const PROMPT_BUILDERS = {
19
+ 'master': buildMasterPrompt,
20
+ 'grade-f': buildGradeFPrompt,
21
+ 'grade-d': buildGradeDPrompt,
22
+ 'grade-c': buildGradeCPrompt,
23
+ 'grade-b': buildGradeBPrompt,
24
+ 'ai-slop': buildAiSlopPrompt,
25
+ 'hotspots': buildHotspotsPrompt
26
+ };
27
+
28
+ export const handleGeneratePrompt = (db, body = {}, cwd = process.cwd()) => {
29
+ const rawScope = String(body.scope || 'master').toLowerCase().trim().replace(/[\s_]+/g, '-');
30
+ const builder = PROMPT_BUILDERS[rawScope] || buildMasterPrompt;
31
+ const targetDir = body.dir || (fs.existsSync(path.resolve(cwd, 'src')) ? 'src' : '.');
32
+ const report = body.report || runAudit(targetDir, { cwd });
33
+ const prompt = builder(report, { excludeAiSlop: Boolean(body.excludeAiSlop) }) || '';
34
+ const estimatedTokens = Math.round(prompt.length / 3.8);
35
+ return { success: true, scope: rawScope, prompt: prompt || 'No violations found for scope.', estimatedTokens, charCount: prompt.length };
36
+ };
37
+
38
+ const getTableNames = (db) => {
39
+ if (!db) return [];
40
+ return db.prepare("SELECT name FROM sqlite_master WHERE type = 'table' AND name NOT LIKE 'sqlite_%' ORDER BY name ASC").all().map((r) => r.name);
41
+ };
42
+
43
+ export const handleDbTables = (db) => {
44
+ if (!db) return { success: false, error: 'Database unavailable' };
45
+ const tables = getTableNames(db).map((name) => {
46
+ const row = db.prepare(`SELECT COUNT(*) as c FROM ${name}`).get();
47
+ return { name, rowCount: row?.c || 0 };
48
+ });
49
+ return { success: true, tables, totalTables: tables.length };
50
+ };
51
+
52
+ export const handleDbBrowse = (db, queryParams = {}) => {
53
+ if (!db) return { success: false, error: 'Database unavailable' };
54
+ const tableNames = getTableNames(db);
55
+ const table = queryParams.table || tableNames[0] || 'agent_tasks';
56
+ if (!tableNames.includes(table)) return { success: false, error: `Invalid table: ${table}` };
57
+
58
+ const page = Math.max(1, parseInt(queryParams.page || '1', 10));
59
+ const pageSize = Math.min(100, Math.max(1, parseInt(queryParams.pageSize || queryParams.limit || '25', 10)));
60
+ const offset = (page - 1) * pageSize;
61
+ const totalRows = db.prepare(`SELECT COUNT(*) as c FROM ${table}`).get()?.c || 0;
62
+ const rows = db.prepare(`SELECT * FROM ${table} LIMIT ? OFFSET ?`).all(pageSize, offset);
63
+ const columns = rows.length > 0 ? Object.keys(rows[0]) : db.prepare(`PRAGMA table_info(${table})`).all().map((c) => c.name);
64
+
65
+ return { success: true, table, columns, rows, totalRows, page, pageSize, totalPages: Math.ceil(totalRows / pageSize) || 1 };
66
+ };
67
+
68
+ export const handleDbStructure = (db, queryParams = {}) => {
69
+ if (!db) return { success: false, error: 'Database unavailable' };
70
+ const tableNames = getTableNames(db);
71
+ const table = queryParams.table || tableNames[0] || 'agent_tasks';
72
+ if (!tableNames.includes(table)) return { success: false, error: `Invalid table: ${table}` };
73
+
74
+ const columns = db.prepare(`PRAGMA table_info(${table})`).all();
75
+ const indexes = db.prepare(`PRAGMA index_list(${table})`).all();
76
+ return { success: true, table, columns, indexes };
77
+ };
78
+
79
+ export const handleDbQuery = (db, body = {}) => {
80
+ if (!db) return { success: false, error: 'Database unavailable' };
81
+ const sql = (body.sql || body.query || '').trim();
82
+ if (!sql) return { success: false, error: 'Empty SQL query' };
83
+ if (!/^(SELECT|PRAGMA|EXPLAIN)\b/i.test(sql)) {
84
+ return { success: false, error: 'Only SELECT, PRAGMA, and EXPLAIN queries are permitted.' };
85
+ }
86
+ const start = performance.now();
87
+ const rows = db.prepare(sql).all().slice(0, 100);
88
+ const durationMs = Number((performance.now() - start).toFixed(2));
89
+ const columns = rows.length > 0 ? Object.keys(rows[0]) : [];
90
+ return { success: true, query: sql, columns, rows, rowCount: rows.length, durationMs };
91
+ };
@@ -0,0 +1,87 @@
1
+ /**
2
+ * Chemical X UI Task & Lock Actions
3
+ * Status updates, agent reassignments, and lock overrides
4
+ */
5
+
6
+ import { getTask, updateTaskStatus, postFeedEvent } from './team/team-db.js';
7
+ import { promoteNextWaiter } from './team/team-db-locks.js';
8
+
9
+ const formatAgentHandle = (id) => {
10
+ const hasId = Boolean(id);
11
+ if (!hasId) return null;
12
+ const isPrefixed = id.startsWith('@');
13
+ return isPrefixed ? id : `@${id}`;
14
+ };
15
+
16
+ export const handleUpdateTaskStatus = (db, body = {}) => {
17
+ if (!db) return { success: false, error: 'Database unavailable' };
18
+ const taskId = Number(body.taskId || body.id);
19
+ const status = body.status;
20
+ const hasTaskId = Boolean(taskId);
21
+ const hasStatus = Boolean(status);
22
+ const isValid = hasTaskId && hasStatus;
23
+ if (!isValid) return { success: false, error: 'taskId and status required' };
24
+
25
+ const blockedReason = body.blocked_reason || body.blockedReason || '';
26
+ const agentId = formatAgentHandle(body.agentId || body.agent) || '@ui-operator';
27
+ const updated = updateTaskStatus(db, taskId, status, { blockedReason });
28
+ if (!updated) return { success: false, error: 'Task not found' };
29
+
30
+ postFeedEvent(db, {
31
+ author_id: agentId,
32
+ event_type: 'task_status_updated',
33
+ task_id: taskId,
34
+ message: `Task #${taskId} status updated to '${status}'` + (blockedReason ? `: ${blockedReason}` : '')
35
+ });
36
+
37
+ return { success: true, task: updated };
38
+ };
39
+
40
+ export const handleAssignTask = (db, body = {}) => {
41
+ if (!db) return { success: false, error: 'Database unavailable' };
42
+ const taskId = Number(body.taskId || body.id);
43
+ if (!taskId) return { success: false, error: 'taskId required' };
44
+
45
+ const rawAgent = body.assigned_agent_id || body.assignedAgentId || body.agentId || body.agent || null;
46
+ const cleanAgent = formatAgentHandle(rawAgent);
47
+ const now = Date.now();
48
+
49
+ db.prepare('UPDATE agent_tasks SET assigned_agent_id = ?, updated_at = ? WHERE id = ?')
50
+ .run(cleanAgent, now, taskId);
51
+
52
+ if (cleanAgent) {
53
+ db.prepare('UPDATE agents SET current_task_id = ?, status = ? WHERE id = ?')
54
+ .run(taskId, 'busy', cleanAgent);
55
+ }
56
+
57
+ postFeedEvent(db, {
58
+ author_id: '@ui-operator',
59
+ recipient_id: cleanAgent,
60
+ event_type: 'task_reassigned',
61
+ task_id: taskId,
62
+ message: `Task #${taskId} reassigned to ${cleanAgent || 'unassigned'}`
63
+ });
64
+
65
+ return { success: true, taskId, assigned_agent_id: cleanAgent, task: getTask(db, taskId) };
66
+ };
67
+
68
+ export const handleOverrideLock = (db, body = {}) => {
69
+ if (!db) return { success: false, error: 'Database unavailable' };
70
+ const filePath = body.filePath || body.path;
71
+ if (!filePath) return { success: false, error: 'filePath is required' };
72
+ const operator = formatAgentHandle(body.agentId || body.agent) || '@ui-operator';
73
+
74
+ const existing = db.prepare('SELECT * FROM file_leases WHERE file_path = ?').get(filePath);
75
+ db.prepare('DELETE FROM file_leases WHERE file_path = ?').run(filePath);
76
+
77
+ postFeedEvent(db, {
78
+ author_id: operator,
79
+ event_type: 'lock_override',
80
+ file_path: filePath,
81
+ message: `${operator} overrode lock lease on ${filePath}` + (existing ? ` (held by ${existing.locked_by})` : '')
82
+ });
83
+
84
+ const next = promoteNextWaiter(db, filePath);
85
+ const promotedWaiter = next ? next.waiter.agent_id : null;
86
+ return { success: true, overridden: true, filePath, previousHolder: existing?.locked_by || null, promotedWaiter };
87
+ };
package/cli/ui-actions.js CHANGED
@@ -7,33 +7,7 @@ import { createTask, claimTask, requestFileLock, releaseFileLock } from './team/
7
7
  import { completeTaskWithAudit } from './team/team-triage.js';
8
8
  import { executeSettingsAction } from './ui-actions-helpers.js';
9
9
 
10
- export const handleCodebaseIndex = (db) => {
11
- if (!db) return { success: false, error: 'Database unavailable' };
12
- const files = db.prepare(`
13
- SELECT path, tier, lines, health_score, hazard_count
14
- FROM files ORDER BY health_score ASC, lines DESC
15
- `).all().map((f) => ({
16
- path: f.path,
17
- tier: f.tier || 'utility',
18
- lines: Number(f.lines || 0),
19
- healthScore: Number(f.health_score || 0),
20
- hazardCount: Number(f.hazard_count || 0)
21
- }));
22
-
23
- const violations = db.prepare(`
24
- SELECT file_path, rule, severity, line, hazard, directive
25
- FROM violations ORDER BY id DESC LIMIT 50
26
- `).all().map((v) => ({
27
- filePath: v.file_path,
28
- rule: v.rule,
29
- severity: v.severity,
30
- line: Number(v.line || 0),
31
- hazard: v.hazard,
32
- directive: v.directive
33
- }));
34
-
35
- return { success: true, files, violations };
36
- };
10
+ export { handleCodebaseIndex, handleCodebaseTree, handleCodebaseFile } from './ui-actions-codebase.js';
37
11
 
38
12
  export const handleCreateTask = (db, body = {}) => {
39
13
  if (!db) return { success: false, error: 'Database unavailable' };
@@ -83,3 +57,13 @@ export const handleReleaseLock = (db, body = {}) => {
83
57
  export const handleSettingsAction = (db, body = {}) => {
84
58
  return executeSettingsAction(db, body.action, body);
85
59
  };
60
+
61
+ export { handleUpdateTaskStatus, handleAssignTask, handleOverrideLock } from './ui-actions-tasks.js';
62
+ export {
63
+ handleGeneratePrompt,
64
+ handleDbTables,
65
+ handleDbBrowse,
66
+ handleDbStructure,
67
+ handleDbQuery
68
+ } from './ui-actions-studio.js';
69
+
@@ -0,0 +1,130 @@
1
+ import test from 'node:test';
2
+ import assert from 'node:assert';
3
+ import { DatabaseSync } from 'node:sqlite';
4
+ import { initTeamSchema } from './team/team-schema.js';
5
+ import { handleSwarmStatus } from './ui-handlers.js';
6
+ import { routeGet, routePost } from './ui-server-routes.js';
7
+ import { startUiServer } from './ui-server.js';
8
+
9
+ const setupTestDb = () => {
10
+ const db = new DatabaseSync(':memory:');
11
+ db.exec(`
12
+ CREATE TABLE IF NOT EXISTS files (
13
+ path TEXT PRIMARY KEY,
14
+ mtime INTEGER NOT NULL,
15
+ size INTEGER NOT NULL,
16
+ tier TEXT NOT NULL,
17
+ lines INTEGER NOT NULL,
18
+ chars INTEGER NOT NULL,
19
+ health_score INTEGER NOT NULL DEFAULT 100,
20
+ hazard_count INTEGER NOT NULL DEFAULT 0
21
+ );
22
+ `);
23
+ initTeamSchema(db);
24
+ return db;
25
+ };
26
+
27
+ test('ui-handlers: handleSwarmStatus returns clean arrays for leases, waitingLocks, tasks, and agents', () => {
28
+ const db = setupTestDb();
29
+ const now = Date.now();
30
+ db.prepare("INSERT INTO agents (id, name, role, heartbeat) VALUES ('@agent-1', 'Agent 1', 'worker', ?)").run(now);
31
+ db.prepare("INSERT INTO file_leases (file_path, locked_by, acquired_at, expires_at, purpose) VALUES ('src/test.ts', '@agent-1', ?, ?, 'editing')").run(now, now + 60000);
32
+ db.prepare("INSERT INTO file_lock_queue (file_path, agent_id, requested_at, status, priority, purpose) VALUES ('src/test.ts', '@agent-2', ?, 'waiting', 1, 'waiting lock')").run(now);
33
+ db.prepare("INSERT INTO agent_tasks (title, tier, status, created_at, updated_at) VALUES ('Canonical Task', 'backend', 'todo', ?, ?)").run(now, now);
34
+
35
+ const status = handleSwarmStatus(db);
36
+ assert.strictEqual(status.success, true);
37
+ assert.ok(Array.isArray(status.leases));
38
+ assert.ok(Array.isArray(status.waitingLocks));
39
+ assert.ok(Array.isArray(status.tasks));
40
+ assert.ok(Array.isArray(status.agents));
41
+
42
+ assert.strictEqual(status.leases.length, 1);
43
+ assert.strictEqual(status.leases[0].filePath, 'src/test.ts');
44
+ assert.strictEqual(status.leases[0].lockedBy, '@agent-1');
45
+ assert.strictEqual(status.leases[0].waitingCount, 1);
46
+
47
+ assert.strictEqual(status.waitingLocks.length, 1);
48
+ assert.strictEqual(status.waitingLocks[0].filePath, 'src/test.ts');
49
+ assert.strictEqual(status.waitingLocks[0].agentId, '@agent-2');
50
+
51
+ assert.strictEqual(status.tasks.length, 1);
52
+ assert.strictEqual(status.tasks[0].title, 'Canonical Task');
53
+
54
+ assert.strictEqual(status.agents.length, 1);
55
+ assert.strictEqual(status.agents[0].id, '@agent-1');
56
+ });
57
+
58
+ test('ui-server-routes: routeGet resolves canonical aliases /api/status, /api/feed, /api/tasks', () => {
59
+ const db = setupTestDb();
60
+ const now = Date.now();
61
+ db.prepare("INSERT INTO agent_tasks (title, tier, status, created_at, updated_at) VALUES ('Route Task', 'backend', 'todo', ?, ?)").run(now, now);
62
+ db.prepare("INSERT INTO agent_feed (author_id, event_type, message, timestamp) VALUES ('@agent-1', 'broadcast', 'Route Feed Message', ?)").run(now);
63
+
64
+ const statusRes = routeGet('/api/status', db);
65
+ assert.ok(statusRes);
66
+ assert.strictEqual(statusRes.success, true);
67
+ assert.ok(Array.isArray(statusRes.leases));
68
+ assert.ok(Array.isArray(statusRes.waitingLocks));
69
+
70
+ const feedRes = routeGet('/api/feed', db);
71
+ assert.ok(feedRes);
72
+ assert.strictEqual(feedRes.success, true);
73
+ assert.ok(Array.isArray(feedRes.feed));
74
+ assert.ok(feedRes.feed.some((f) => f.message === 'Route Feed Message'));
75
+
76
+ const tasksRes = routeGet('/api/tasks', db);
77
+ assert.ok(tasksRes);
78
+ assert.strictEqual(tasksRes.success, true);
79
+ assert.ok(Array.isArray(tasksRes.tasks));
80
+ assert.ok(tasksRes.tasks.some((t) => t.title === 'Route Task'));
81
+ });
82
+
83
+ test('ui-server-routes: routePost resolves canonical aliases /api/feed and /api/tasks', () => {
84
+ const db = setupTestDb();
85
+ const now = Date.now();
86
+ db.prepare("INSERT INTO agents (id, name, role, heartbeat) VALUES ('@tester', 'Tester', 'qa', ?)").run(now);
87
+
88
+ const postFeedRes = routePost('/api/feed', db, {
89
+ author: '@tester',
90
+ message: 'Canonical post test',
91
+ channel: 'general'
92
+ });
93
+ assert.ok(postFeedRes);
94
+ assert.strictEqual(postFeedRes.success, true);
95
+
96
+ const postTaskRes = routePost('/api/tasks', db, {
97
+ title: 'Created via canonical route',
98
+ tier: 'backend'
99
+ });
100
+ assert.ok(postTaskRes);
101
+ assert.strictEqual(postTaskRes.success, true);
102
+
103
+ const tasksRes = routeGet('/api/tasks', db);
104
+ assert.ok(tasksRes.tasks.some((t) => t.title === 'Created via canonical route'));
105
+ });
106
+
107
+ test('ui-server: HTTP integration serves canonical routes /api/status, /api/feed, /api/tasks', async () => {
108
+ const running = await startUiServer({ port: 0, cwd: process.cwd() });
109
+ try {
110
+ const resStatus = await fetch(`http://localhost:${running.port}/api/status`);
111
+ assert.strictEqual(resStatus.status, 200);
112
+ const jsonStatus = await resStatus.json();
113
+ assert.strictEqual(jsonStatus.success, true);
114
+ assert.ok(Array.isArray(jsonStatus.waitingLocks));
115
+
116
+ const resFeed = await fetch(`http://localhost:${running.port}/api/feed`);
117
+ assert.strictEqual(resFeed.status, 200);
118
+ const jsonFeed = await resFeed.json();
119
+ assert.strictEqual(jsonFeed.success, true);
120
+ assert.ok(Array.isArray(jsonFeed.feed));
121
+
122
+ const resTasks = await fetch(`http://localhost:${running.port}/api/tasks`);
123
+ assert.strictEqual(resTasks.status, 200);
124
+ const jsonTasks = await resTasks.json();
125
+ assert.strictEqual(jsonTasks.success, true);
126
+ assert.ok(Array.isArray(jsonTasks.tasks));
127
+ } finally {
128
+ running.server.close();
129
+ }
130
+ });
@@ -4,46 +4,84 @@
4
4
  export const UI_CLIENT_API_SCRIPT = `
5
5
  window.chemxApi = {
6
6
  fetchStatus: async () => {
7
- try {
8
- const res = await fetch('/api/swarm/status');
9
- if (res.ok) return await res.json();
10
- } catch {}
7
+ try { const res = await fetch('/api/swarm/status'); if (res.ok) return await res.json(); } catch (_err) { return null; }
11
8
  return null;
12
9
  },
13
10
  fetchCodebase: async () => {
14
- try {
15
- const res = await fetch('/api/swarm/codebase');
16
- if (res.ok) { const d = await res.json(); return d.files || []; }
17
- } catch {}
11
+ try { const res = await fetch('/api/swarm/codebase'); if (res.ok) { const d = await res.json(); return d.files || []; } } catch (_err) { return []; }
18
12
  return [];
19
13
  },
14
+ fetchFileTree: async () => {
15
+ try { const res = await fetch('/api/codebase/tree'); if (res.ok) return await res.json(); } catch (_err) { return { success: false, tree: [], files: [] }; }
16
+ return { success: false, tree: [], files: [] };
17
+ },
18
+ fetchFileDetails: async (path) => {
19
+ try { const res = await fetch('/api/codebase/file?path=' + encodeURIComponent(path)); if (res.ok) return await res.json(); } catch (_err) { return { success: false, error: 'Network error' }; }
20
+ return { success: false, error: 'Network error' };
21
+ },
20
22
  fetchAttention: async () => {
21
- try {
22
- const res = await fetch('/api/swarm/attention');
23
- if (res.ok) return await res.json();
24
- } catch {}
23
+ try { const res = await fetch('/api/swarm/attention'); if (res.ok) return await res.json(); } catch (_err) { return { items: [], antigravityRunning: false }; }
25
24
  return { items: [], antigravityRunning: false };
26
25
  },
27
26
  confirmAttention: async (itemId, action) => {
28
27
  await fetch('/api/swarm/attention/action', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ itemId, action }) });
29
28
  },
30
29
  fetchDbMetrics: async () => {
31
- try {
32
- const res = await fetch('/api/swarm/database/metrics');
33
- if (res.ok) return await res.json();
34
- } catch {}
30
+ try { const res = await fetch('/api/swarm/database/metrics'); if (res.ok) return await res.json(); } catch (_err) { return null; }
35
31
  return null;
36
32
  },
33
+ fetchDbTables: async () => {
34
+ try { const res = await fetch('/api/db/tables'); if (res.ok) return await res.json(); } catch (_err) { return { success: false, tables: [] }; }
35
+ return { success: false, tables: [] };
36
+ },
37
+ fetchDbBrowse: async (table, page = 1, pageSize = 25) => {
38
+ try { const res = await fetch('/api/db/browse?table=' + encodeURIComponent(table) + '&page=' + page + '&pageSize=' + pageSize); if (res.ok) return await res.json(); } catch (_err) { return { success: false, rows: [], columns: [] }; }
39
+ return { success: false, rows: [], columns: [] };
40
+ },
41
+ fetchDbStructure: async (table) => {
42
+ try { const res = await fetch('/api/db/structure?table=' + encodeURIComponent(table)); if (res.ok) return await res.json(); } catch (_err) { return { success: false, columns: [] }; }
43
+ return { success: false, columns: [] };
44
+ },
45
+ generatePrompt: async (scope = 'master', excludeAiSlop = false) => {
46
+ try { const res = await fetch('/api/prompts/generate', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ scope, excludeAiSlop }) }); if (res.ok) return await res.json(); } catch (_err) { return { success: false, error: 'Network error' }; }
47
+ return { success: false, error: 'Failed to generate prompt' };
48
+ },
37
49
  runDbQuery: async (query) => {
38
50
  try {
39
- const res = await fetch('/api/swarm/database/query', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ query }) });
51
+ const res = await fetch('/api/db/query', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ query }) });
40
52
  return await res.json();
41
- } catch (err) {
42
- return { success: false, error: err.message };
43
- }
53
+ } catch (err) { return { success: false, error: err.message }; }
44
54
  },
45
55
  postJson: async (url, body) => {
46
56
  return fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) });
57
+ },
58
+ updateTaskStatus: async (taskId, status, blockedReason = '') => {
59
+ const res = await fetch('/api/tasks/update', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ taskId, status, blockedReason }) });
60
+ return await res.json();
61
+ },
62
+ assignTask: async (taskId, agentId) => {
63
+ const res = await fetch('/api/tasks/assign', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ taskId, agentId }) });
64
+ return await res.json();
65
+ },
66
+ overrideLock: async (filePath, agentId = '@ui-operator') => {
67
+ const res = await fetch('/api/locks/override', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ filePath, agentId }) });
68
+ return await res.json();
69
+ },
70
+ createTask: async (payload) => {
71
+ const res = await fetch('/api/tasks', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) });
72
+ return await res.json();
73
+ },
74
+ fetchTopics: async (category = '') => {
75
+ try { const res = await fetch('/api/topics' + (category ? '?category=' + encodeURIComponent(category) : '')); if (res.ok) return await res.json(); } catch (_err) { return { success: false, topics: [] }; }
76
+ return { success: false, topics: [] };
77
+ },
78
+ fetchTopicPosts: async (topicId) => {
79
+ try { const res = await fetch('/api/topics/posts?topicId=' + encodeURIComponent(topicId)); if (res.ok) return await res.json(); } catch (_err) { return { success: false, posts: [] }; }
80
+ return { success: false, posts: [] };
81
+ },
82
+ createTopic: async (payload) => {
83
+ const res = await fetch('/api/topics', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) });
84
+ return await res.json();
47
85
  }
48
86
  };
49
87
  `;
@@ -0,0 +1,93 @@
1
+ /**
2
+ * Chemical X UI File Tree & Inspector Reactive State
3
+ */
4
+ export const UI_CLIENT_FILETREE_SCRIPT = `
5
+ window.createFileTreeState = (Vue, api, codebaseFiles) => {
6
+ const { ref, computed } = Vue;
7
+ const selectedFilePath = ref(''), selectedFileDetail = ref(null), selectedFileLoading = ref(false);
8
+ const inspectorTab = ref('outline'), collapsedFolders = ref(new Set()), fileFilterQuery = ref('');
9
+
10
+ const flattenedNodes = computed(() => {
11
+ const files = codebaseFiles.value || [], folderMap = new Map(), nodes = [];
12
+ for (const f of files) {
13
+ const parts = (f.path || '').split('/');
14
+ let cur = '';
15
+ for (let i = 0; i < parts.length - 1; i++) {
16
+ cur = cur ? cur + '/' + parts[i] : parts[i];
17
+ if (!folderMap.has(cur)) folderMap.set(cur, { name: parts[i], path: cur, isFolder: true, depth: i, fileCount: 0 });
18
+ folderMap.get(cur).fileCount++;
19
+ }
20
+ }
21
+ const allPaths = Array.from(new Set([...folderMap.keys(), ...files.map((f) => f.path)])).sort();
22
+ for (const p of allPaths) {
23
+ if (folderMap.has(p)) {
24
+ nodes.push(folderMap.get(p));
25
+ } else {
26
+ const fileObj = files.find((x) => x.path === p);
27
+ if (fileObj) {
28
+ const parts = p.split('/');
29
+ nodes.push({
30
+ name: parts[parts.length - 1], path: p, isFolder: false, depth: parts.length - 1,
31
+ lines: Number(fileObj.lines || 0), tier: fileObj.tier || 'utility',
32
+ healthScore: Number(fileObj.healthScore ?? 100), hazardCount: Number(fileObj.hazardCount ?? 0)
33
+ });
34
+ }
35
+ }
36
+ }
37
+ return nodes;
38
+ });
39
+
40
+ const filteredFileTree = computed(() => {
41
+ const q = fileFilterQuery.value.trim().toLowerCase();
42
+ const collapsed = collapsedFolders.value;
43
+ return flattenedNodes.value.filter((node) => {
44
+ const parts = node.path.split('/');
45
+ let prefix = '';
46
+ for (let i = 0; i < parts.length - 1; i++) {
47
+ prefix = prefix ? prefix + '/' + parts[i] : parts[i];
48
+ if (collapsed.has(prefix)) return false;
49
+ }
50
+ if (!q) return true;
51
+ if (node.isFolder) return true;
52
+ return node.name.toLowerCase().includes(q) || (node.tier && node.tier.toLowerCase().includes(q));
53
+ });
54
+ });
55
+
56
+ const cleanFileCount = computed(() => (codebaseFiles.value || []).filter((f) => Number(f.lines || 0) <= 100).length);
57
+ const monolithFileCount = computed(() => (codebaseFiles.value || []).filter((f) => Number(f.lines || 0) > 100).length);
58
+ const codebaseHazardCount = computed(() => (codebaseFiles.value || []).reduce((acc, f) => acc + Number(f.hazardCount || 0), 0));
59
+
60
+ const isFolderExpanded = (path) => !collapsedFolders.value.has(path);
61
+ const toggleFolder = (path) => {
62
+ const next = new Set(collapsedFolders.value);
63
+ if (next.has(path)) next.delete(path); else next.add(path);
64
+ collapsedFolders.value = next;
65
+ };
66
+ const expandAllFolders = () => { collapsedFolders.value = new Set(); };
67
+ const collapseAllFolders = () => {
68
+ const all = new Set();
69
+ for (const n of flattenedNodes.value) { if (n.isFolder) all.add(n.path); }
70
+ collapsedFolders.value = all;
71
+ };
72
+
73
+ const selectFile = async (path) => {
74
+ selectedFilePath.value = path;
75
+ selectedFileLoading.value = true;
76
+ const res = await api.fetchFileDetails(path);
77
+ selectedFileLoading.value = false;
78
+ if (res?.success) selectedFileDetail.value = res;
79
+ };
80
+
81
+ const refreshCodebase = async () => {
82
+ const res = await api.fetchFileTree();
83
+ if (res?.files) codebaseFiles.value = res.files;
84
+ else codebaseFiles.value = await api.fetchCodebase();
85
+ };
86
+
87
+ return {
88
+ selectedFilePath, selectedFileDetail, selectedFileLoading, inspectorTab, fileFilterQuery,
89
+ filteredFileTree, cleanFileCount, monolithFileCount, codebaseHazardCount,
90
+ isFolderExpanded, toggleFolder, expandAllFolders, collapseAllFolders, selectFile, refreshCodebase
91
+ };
92
+ };
93
+ `;