@worca/ui 0.8.1 → 0.9.0-rc.1
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/main.bundle.js +1424 -755
- package/app/main.bundle.js.map +4 -4
- package/app/styles.css +399 -23
- package/package.json +1 -1
- package/server/app.js +341 -6
- package/server/dispatch-events-aggregator.js +161 -0
- package/server/ensure-webhook.js +66 -0
- package/server/index.js +22 -0
- package/server/process-manager.js +61 -2
- package/server/project-registry.js +37 -0
- package/server/project-routes.js +175 -6
- package/server/settings-validator.js +279 -2
- package/server/subagents-discovery.js +116 -0
- package/server/version-check.js +35 -0
- package/server/watcher.js +37 -10
- package/server/worca-setup.js +15 -1
- package/server/ws-modular.js +6 -2
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Subagent discovery for the settings dispatch-rule editor.
|
|
3
|
+
*
|
|
4
|
+
* Walks three sources (built-ins, user-global, plugin cache) and returns a
|
|
5
|
+
* deduplicated list matching the shape used by `worca-ui/app/views/
|
|
6
|
+
* dispatch-tag-state.js` (`{name, label, group}`).
|
|
7
|
+
*
|
|
8
|
+
* The three sources:
|
|
9
|
+
* 1. Built-ins — hardcoded Claude Code types that are not on disk.
|
|
10
|
+
* 2. User — `<userDir>/*.md`, one file per subagent.
|
|
11
|
+
* 3. Plugins — `<pluginCacheDir>/<marketplace>/<plugin>/<version>/agents/*.md`.
|
|
12
|
+
* Deduped by the qualified name `<plugin>:<agent>` — first file wins
|
|
13
|
+
* across versions (the set of agents within a plugin is stable in
|
|
14
|
+
* practice; when two versions disagree we prefer filesystem order for
|
|
15
|
+
* determinism rather than trying to parse semver from directory names).
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { existsSync, readdirSync, statSync } from 'node:fs';
|
|
19
|
+
import { basename, join } from 'node:path';
|
|
20
|
+
|
|
21
|
+
// Built-in Claude Code subagents — shipped with a factory CC install, no
|
|
22
|
+
// plugins required. Mirror this list in worca-ui/app/views/
|
|
23
|
+
// dispatch-tag-state.js (KNOWN_TYPES) so the UI falls back to the same set
|
|
24
|
+
// when the /api/subagents fetch fails.
|
|
25
|
+
export const BUILTINS = [
|
|
26
|
+
{ name: 'Explore', label: '(built-in)', group: 'Built-in' },
|
|
27
|
+
{ name: 'general-purpose', label: '(built-in)', group: 'Built-in' },
|
|
28
|
+
{ name: 'Plan', label: '(built-in)', group: 'Built-in' },
|
|
29
|
+
{ name: 'statusline-setup', label: '(built-in)', group: 'Built-in' },
|
|
30
|
+
{ name: 'claude-code-guide', label: '(built-in)', group: 'Built-in' },
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
function listMarkdownBasenames(dir) {
|
|
34
|
+
if (!dir || !existsSync(dir)) return [];
|
|
35
|
+
try {
|
|
36
|
+
return readdirSync(dir)
|
|
37
|
+
.filter((n) => n.endsWith('.md'))
|
|
38
|
+
.map((n) => basename(n, '.md'));
|
|
39
|
+
} catch {
|
|
40
|
+
return [];
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function listSubdirs(dir) {
|
|
45
|
+
if (!existsSync(dir)) return [];
|
|
46
|
+
try {
|
|
47
|
+
return readdirSync(dir).filter((n) => {
|
|
48
|
+
try {
|
|
49
|
+
return statSync(join(dir, n)).isDirectory();
|
|
50
|
+
} catch {
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
} catch {
|
|
55
|
+
return [];
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Discover all subagent types reachable from the given directories.
|
|
61
|
+
*
|
|
62
|
+
* @param {object} options
|
|
63
|
+
* @param {Array<{name:string,label:string,group:string}>} [options.builtins]
|
|
64
|
+
* @param {string} [options.userDir] e.g. ~/.claude/agents
|
|
65
|
+
* @param {string} [options.pluginCacheDir] e.g. ~/.claude/plugins/cache
|
|
66
|
+
* @param {string} [options.projectAgentsDir] e.g. <project>/.claude/agents
|
|
67
|
+
* @returns {Array<{name:string,label:string,group:string}>}
|
|
68
|
+
*/
|
|
69
|
+
export function discoverSubagents({
|
|
70
|
+
builtins = BUILTINS,
|
|
71
|
+
userDir,
|
|
72
|
+
pluginCacheDir,
|
|
73
|
+
projectAgentsDir,
|
|
74
|
+
} = {}) {
|
|
75
|
+
const result = [...builtins];
|
|
76
|
+
const seen = new Set(result.map((t) => t.name));
|
|
77
|
+
|
|
78
|
+
for (const name of listMarkdownBasenames(userDir)) {
|
|
79
|
+
if (!seen.has(name)) {
|
|
80
|
+
seen.add(name);
|
|
81
|
+
result.push({ name, label: '(user)', group: 'User' });
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (pluginCacheDir && existsSync(pluginCacheDir)) {
|
|
86
|
+
for (const marketplace of listSubdirs(pluginCacheDir)) {
|
|
87
|
+
const marketplaceDir = join(pluginCacheDir, marketplace);
|
|
88
|
+
for (const plugin of listSubdirs(marketplaceDir)) {
|
|
89
|
+
const pluginDir = join(marketplaceDir, plugin);
|
|
90
|
+
for (const version of listSubdirs(pluginDir)) {
|
|
91
|
+
const agentsDir = join(pluginDir, version, 'agents');
|
|
92
|
+
for (const agent of listMarkdownBasenames(agentsDir)) {
|
|
93
|
+
const qualified = `${plugin}:${agent}`;
|
|
94
|
+
if (!seen.has(qualified)) {
|
|
95
|
+
seen.add(qualified);
|
|
96
|
+
result.push({
|
|
97
|
+
name: qualified,
|
|
98
|
+
label: '(plugin)',
|
|
99
|
+
group: 'Plugin',
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
for (const name of listMarkdownBasenames(projectAgentsDir)) {
|
|
109
|
+
if (!seen.has(name)) {
|
|
110
|
+
seen.add(name);
|
|
111
|
+
result.push({ name, label: '(project)', group: 'Project' });
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return result;
|
|
116
|
+
}
|
package/server/version-check.js
CHANGED
|
@@ -40,6 +40,41 @@ export function meetsMinimum(installed, minimum) {
|
|
|
40
40
|
return true; // equal
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
+
/**
|
|
44
|
+
* Parse a version string into comparable parts, tracking RC suffixes.
|
|
45
|
+
* "0.6.0rc7" → { parts: [0, 6, 0], rc: 7 }
|
|
46
|
+
* "0.6.0" → { parts: [0, 6, 0], rc: Infinity } (stable > any rc)
|
|
47
|
+
* "0.1.0-rc.5" → { parts: [0, 1, 0], rc: 5 }
|
|
48
|
+
*/
|
|
49
|
+
export function parseVersion(v) {
|
|
50
|
+
if (!v) return { parts: [], rc: Infinity };
|
|
51
|
+
const rcMatch = v.match(/^(.+?)[-.]?rc\.?(\d+)$/);
|
|
52
|
+
const base = rcMatch ? rcMatch[1] : v;
|
|
53
|
+
const rc = rcMatch ? parseInt(rcMatch[2], 10) : Infinity;
|
|
54
|
+
const parts = base.split('.').map((s) => parseInt(s, 10) || 0);
|
|
55
|
+
return { parts, rc };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Returns true if `project` version is strictly behind `active`.
|
|
60
|
+
* RC-aware: "0.6.0rc3" is behind "0.6.0". Returns false if either arg is falsy.
|
|
61
|
+
*/
|
|
62
|
+
export function isVersionBehind(project, active) {
|
|
63
|
+
if (!project || !active) return false;
|
|
64
|
+
const p = parseVersion(project);
|
|
65
|
+
const a = parseVersion(active);
|
|
66
|
+
const len = Math.max(p.parts.length, a.parts.length);
|
|
67
|
+
for (let i = 0; i < len; i++) {
|
|
68
|
+
const pv = p.parts[i] || 0;
|
|
69
|
+
const av = a.parts[i] || 0;
|
|
70
|
+
if (pv < av) return true;
|
|
71
|
+
if (pv > av) return false;
|
|
72
|
+
}
|
|
73
|
+
// Same base version — compare RC numbers
|
|
74
|
+
if (p.rc < a.rc) return true;
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
|
|
43
78
|
/**
|
|
44
79
|
* Run `worca --version` and check compatibility.
|
|
45
80
|
* @returns {Promise<{ok: boolean, installed: string|null, minimum: string, message: string}>}
|
package/server/watcher.js
CHANGED
|
@@ -2,6 +2,25 @@ import { createHash } from 'node:crypto';
|
|
|
2
2
|
import { existsSync, readdirSync, readFileSync, watch } from 'node:fs';
|
|
3
3
|
import { readdir, readFile } from 'node:fs/promises';
|
|
4
4
|
import { join } from 'node:path';
|
|
5
|
+
import {
|
|
6
|
+
assignEventsToIterations,
|
|
7
|
+
readDispatchEventsFromJsonl,
|
|
8
|
+
} from './dispatch-events-aggregator.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Enrich a status object with dispatch events read from events.jsonl in the
|
|
12
|
+
* same run directory. Mutates `status.stages` by adding `dispatch_events` to
|
|
13
|
+
* matching iterations. No-op when events.jsonl is missing (e.g. a run that
|
|
14
|
+
* started before the emit was wired, or a run with no dispatches).
|
|
15
|
+
*/
|
|
16
|
+
function enrichWithDispatchEvents(status, runDir) {
|
|
17
|
+
if (!status?.stages) return status;
|
|
18
|
+
const eventsPath = join(runDir, 'events.jsonl');
|
|
19
|
+
const events = readDispatchEventsFromJsonl(eventsPath);
|
|
20
|
+
if (events.length === 0) return status;
|
|
21
|
+
status.stages = assignEventsToIterations(events, status.stages);
|
|
22
|
+
return status;
|
|
23
|
+
}
|
|
5
24
|
|
|
6
25
|
export function createRunId(status) {
|
|
7
26
|
// Prefer run_id from status (new per-run format)
|
|
@@ -35,9 +54,11 @@ export function discoverRuns(worcaDir) {
|
|
|
35
54
|
if (existsSync(activeRunPath)) {
|
|
36
55
|
try {
|
|
37
56
|
const activeId = readFileSync(activeRunPath, 'utf8').trim();
|
|
38
|
-
const
|
|
57
|
+
const runDir = join(worcaDir, 'runs', activeId);
|
|
58
|
+
const candidate = join(runDir, 'status.json');
|
|
39
59
|
if (existsSync(candidate)) {
|
|
40
|
-
|
|
60
|
+
let status = JSON.parse(readFileSync(candidate, 'utf8'));
|
|
61
|
+
status = enrichWithDispatchEvents(status, runDir);
|
|
41
62
|
const active =
|
|
42
63
|
!isTerminal(status) && status.pipeline_status === 'running';
|
|
43
64
|
const id = createRunId(status);
|
|
@@ -53,10 +74,12 @@ export function discoverRuns(worcaDir) {
|
|
|
53
74
|
const runsDir = join(worcaDir, 'runs');
|
|
54
75
|
if (existsSync(runsDir)) {
|
|
55
76
|
for (const entry of readdirSync(runsDir)) {
|
|
56
|
-
const
|
|
77
|
+
const runDir = join(runsDir, entry);
|
|
78
|
+
const statusPath = join(runDir, 'status.json');
|
|
57
79
|
if (!existsSync(statusPath)) continue;
|
|
58
80
|
try {
|
|
59
|
-
|
|
81
|
+
let status = JSON.parse(readFileSync(statusPath, 'utf8'));
|
|
82
|
+
status = enrichWithDispatchEvents(status, runDir);
|
|
60
83
|
const id = createRunId(status);
|
|
61
84
|
if (seenIds.has(id)) continue;
|
|
62
85
|
seenIds.add(id);
|
|
@@ -140,8 +163,10 @@ export async function discoverRunsAsync(worcaDir) {
|
|
|
140
163
|
const activeRunPath = join(worcaDir, 'active_run');
|
|
141
164
|
try {
|
|
142
165
|
const activeId = (await readFile(activeRunPath, 'utf8')).trim();
|
|
143
|
-
const
|
|
144
|
-
const
|
|
166
|
+
const runDir = join(worcaDir, 'runs', activeId);
|
|
167
|
+
const candidate = join(runDir, 'status.json');
|
|
168
|
+
let status = JSON.parse(await readFile(candidate, 'utf8'));
|
|
169
|
+
status = enrichWithDispatchEvents(status, runDir);
|
|
145
170
|
const active = !isTerminal(status) && status.pipeline_status === 'running';
|
|
146
171
|
const id = createRunId(status);
|
|
147
172
|
runs.push({ id, active, ...status });
|
|
@@ -156,15 +181,17 @@ export async function discoverRunsAsync(worcaDir) {
|
|
|
156
181
|
const entries = await readdir(runsDir);
|
|
157
182
|
const readPromises = entries.map(async (entry) => {
|
|
158
183
|
try {
|
|
159
|
-
const
|
|
184
|
+
const runDir = join(runsDir, entry);
|
|
185
|
+
const statusPath = join(runDir, 'status.json');
|
|
160
186
|
const status = JSON.parse(await readFile(statusPath, 'utf8'));
|
|
161
|
-
return status;
|
|
187
|
+
return { status, runDir };
|
|
162
188
|
} catch {
|
|
163
189
|
return null;
|
|
164
190
|
}
|
|
165
191
|
});
|
|
166
|
-
for (const
|
|
167
|
-
if (!
|
|
192
|
+
for (const result of await Promise.all(readPromises)) {
|
|
193
|
+
if (!result) continue;
|
|
194
|
+
const status = enrichWithDispatchEvents(result.status, result.runDir);
|
|
168
195
|
const id = createRunId(status);
|
|
169
196
|
if (seenIds.has(id)) continue;
|
|
170
197
|
seenIds.add(id);
|
package/server/worca-setup.js
CHANGED
|
@@ -20,10 +20,24 @@ export function checkWorcaInstalled(projectPath) {
|
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
/**
|
|
23
|
-
* Read the worca-cc version from a project's
|
|
23
|
+
* Read the worca-cc version from a project's worca installation.
|
|
24
|
+
* Tries .claude/worca/version.json first, then falls back to __init__.py.
|
|
24
25
|
* Returns the version string or null if not found.
|
|
25
26
|
*/
|
|
26
27
|
export function readProjectWorcaVersion(projectPath) {
|
|
28
|
+
// Try version.json first (preferred format)
|
|
29
|
+
try {
|
|
30
|
+
const versionJson = JSON.parse(
|
|
31
|
+
readFileSync(
|
|
32
|
+
join(projectPath, '.claude', 'worca', 'version.json'),
|
|
33
|
+
'utf8',
|
|
34
|
+
),
|
|
35
|
+
);
|
|
36
|
+
if (versionJson.version) return versionJson.version;
|
|
37
|
+
} catch {
|
|
38
|
+
// fall through to __init__.py
|
|
39
|
+
}
|
|
40
|
+
// Fall back to __init__.py
|
|
27
41
|
try {
|
|
28
42
|
const initPy = readFileSync(
|
|
29
43
|
join(projectPath, '.claude', 'worca', '__init__.py'),
|
package/server/ws-modular.js
CHANGED
|
@@ -11,6 +11,7 @@ import { join } from 'node:path';
|
|
|
11
11
|
import { WebSocketServer } from 'ws';
|
|
12
12
|
import { readProjects, synthesizeDefaultProject } from './project-registry.js';
|
|
13
13
|
import { TIER_FULL, TIER_POLLING, WatcherSet } from './watcher-set.js';
|
|
14
|
+
import { readProjectWorcaVersion } from './worca-setup.js';
|
|
14
15
|
import { createBroadcaster } from './ws-broadcaster.js';
|
|
15
16
|
import { createClientManager } from './ws-client-manager.js';
|
|
16
17
|
import { createMessageRouter } from './ws-message-router.js';
|
|
@@ -157,9 +158,12 @@ export function attachWsServer(httpServer, config) {
|
|
|
157
158
|
}
|
|
158
159
|
|
|
159
160
|
// Broadcast projects-updated to all clients
|
|
161
|
+
// Shape must match GET /api/projects so frontend state stays consistent
|
|
162
|
+
// (include worcaVersion — without it, clients would show "unknown" after
|
|
163
|
+
// the WS event clobbers the enriched REST response on add/remove)
|
|
160
164
|
const projectList = freshProjects.map((p) => ({
|
|
161
|
-
|
|
162
|
-
|
|
165
|
+
...p,
|
|
166
|
+
worcaVersion: readProjectWorcaVersion(p.path),
|
|
163
167
|
}));
|
|
164
168
|
broadcaster.broadcast('projects-updated', { projects: projectList });
|
|
165
169
|
}
|