@worca/app 1.2.0-rc.1 → 1.2.0-rc.3
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/README.md +36 -2
- package/package.json +1 -1
- package/src/cli/worca-cc.mjs +358 -19
- package/src/core/ask/models.mjs +7 -1
- package/src/core/ask/turn.mjs +5 -0
- package/src/core/config.mjs +33 -4
- package/src/core/folder-dialog.mjs +21 -12
- package/src/core/marketplaces.mjs +13 -0
- package/src/core/model-env.mjs +41 -0
- package/src/core/model-test.mjs +2 -1
- package/src/core/plugin-manifest.mjs +45 -8
- package/src/core/run-harness.mjs +7 -0
- package/src/core/settings.mjs +61 -0
- package/src/core/title.mjs +72 -9
- package/src/core/ui-instance.mjs +262 -0
- package/src/core/workflow-export.mjs +1169 -0
- package/src/core/workflow-share.mjs +191 -0
- package/src/core/workflows.mjs +35 -4
- package/src/shared/graph/validate.mjs +5 -3
- package/ui/public/app.js +653 -22
- package/ui/public/ask-panel.mjs +4 -2
- package/ui/public/export-slug.mjs +30 -0
- package/ui/public/graph/composer.mjs +24 -3
- package/ui/public/graph/inspector.mjs +4 -1
- package/ui/public/index.html +127 -1
- package/ui/public/models-view.mjs +34 -3
- package/ui/public/style.css +65 -5
- package/ui/server.mjs +305 -90
package/README.md
CHANGED
|
@@ -90,6 +90,15 @@ and durations, the clarify Q&A, agent transcripts, and logs:
|
|
|
90
90
|
runs out). Flow cards — **Task** (the run's request), **End** (the result),
|
|
91
91
|
**AND**, **OR**, **Combine** — express joins, choices and merges without any
|
|
92
92
|
code. Saved pipelines appear in the New Pipeline picker.
|
|
93
|
+
- **Share a pipeline** — *Export…* on a saved pipeline offers three formats.
|
|
94
|
+
A *JSON file* passes it to another Worca user, who picks it up with
|
|
95
|
+
*Import…* (a taken name gets a ` (2)` suffix; nothing is ever overwritten).
|
|
96
|
+
A *Claude Code skill* turns it into a runnable `/command` under `.claude/`
|
|
97
|
+
so it runs without Worca. A *Worca plugin* folder bundles the pipeline with
|
|
98
|
+
your own agents it uses and the skills they need, so the recipient runs
|
|
99
|
+
`worca plugin link <folder>` and updates later with `worca plugin reimport`.
|
|
100
|
+
Built-in agents are never copied — a plugin pipeline may reference them
|
|
101
|
+
directly. The saved list is tabbed by domain; click a card to open it.
|
|
93
102
|
|
|
94
103
|

|
|
95
104
|
|
|
@@ -143,6 +152,12 @@ and durations, the clarify Q&A, agent transcripts, and logs:
|
|
|
143
152
|
per-model routing environment (e.g. `ANTHROPIC_BASE_URL`) that is merged
|
|
144
153
|
into that model's agent spawns. Share a model catalog as a plugin, with
|
|
145
154
|
secrets required at install time.
|
|
155
|
+
- **No first-party account needed** — run and chat titles are written by the
|
|
156
|
+
model the run or chat itself uses (Settings › General › Title generation picks
|
|
157
|
+
a fixed one instead), endpoint-routed models carry Claude Code's internal
|
|
158
|
+
haiku/sonnet/opus/fable tier keys so nothing falls back to the Anthropic API,
|
|
159
|
+
and *Hide built-in models* (Settings › Models) drops the built-ins from every
|
|
160
|
+
picker.
|
|
146
161
|
|
|
147
162
|
### Storage
|
|
148
163
|
|
|
@@ -177,7 +192,7 @@ Requirements:
|
|
|
177
192
|
### Web UI
|
|
178
193
|
|
|
179
194
|
```bash
|
|
180
|
-
worca --ui
|
|
195
|
+
worca ui # start it (worca --ui does the same)
|
|
181
196
|
```
|
|
182
197
|
|
|
183
198
|
Open the printed URL (default `http://localhost:4317`), add a project, and
|
|
@@ -186,6 +201,20 @@ a task from a plugin source like GitHub Issues), pick a workflow and
|
|
|
186
201
|
guardrails, and run. Answer clarify questions and loop gates as they come —
|
|
187
202
|
in the browser or from chat.
|
|
188
203
|
|
|
204
|
+
The UI is one process per machine. Starting it while it is already up is not
|
|
205
|
+
an error — Worca prints the URL and how to restart it, and exits 0:
|
|
206
|
+
|
|
207
|
+
```bash
|
|
208
|
+
worca ui status # is it running? (exit 0 = yes, 1 = no)
|
|
209
|
+
worca ui restart # stop the running one, start it again
|
|
210
|
+
worca ui stop # stop it gracefully
|
|
211
|
+
worca ui --port 4318 --open # another port; open the browser when up
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
`--port` (or the `PORT` env var) picks the port; `stop`, `restart` and
|
|
215
|
+
`status` remember the port of the last started UI, so they usually need no
|
|
216
|
+
flag. See `worca ui help`.
|
|
217
|
+
|
|
189
218
|
### CLI
|
|
190
219
|
|
|
191
220
|
```bash
|
|
@@ -200,10 +229,15 @@ worca resume <pipelineId>
|
|
|
200
229
|
|
|
201
230
|
# offline demo — full pipeline, no tokens
|
|
202
231
|
worca --project /path/to/your/project --prompt "demo task" --mock --yes
|
|
232
|
+
|
|
233
|
+
# share a saved pipeline: as JSON, or as a plugin folder bundling your agents + skills
|
|
234
|
+
worca workflow export wf_my-flow --format json --out my-flow.json
|
|
235
|
+
worca workflow import my-flow.json
|
|
236
|
+
worca workflow export wf_my-flow --format plugin --target ./my-flow-plugin
|
|
203
237
|
```
|
|
204
238
|
|
|
205
239
|
Run `worca --help` for all subcommands (projects, plugins, marketplaces,
|
|
206
|
-
config, doctor) and flags.
|
|
240
|
+
workflows, config, doctor) and flags.
|
|
207
241
|
|
|
208
242
|
Exit codes, for scripts and CI wrappers: `0` the run finished (or an
|
|
209
243
|
interactive run paused and you can resume it); `1` a hard error, a stop, or an
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@worca/app",
|
|
3
|
-
"version": "1.2.0-rc.
|
|
3
|
+
"version": "1.2.0-rc.3",
|
|
4
4
|
"description": "Worca — deterministic multi-agent pipeline that drives Claude Code (headless) through Plan -> Refine -> Implement -> Review, with a CLI, an installable /worca skill, and a web UI.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Sinisha Djukic",
|
package/src/cli/worca-cc.mjs
CHANGED
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
// CLI entry point. Parses flags, creates a core orchestrator, subscribes to its events,
|
|
5
5
|
// renders a phase tracker + streamed agent logs to the terminal, and drives interactive
|
|
6
6
|
// Q&A (clarify) and loop gates via node:readline. Supports --yes (auto), --mock,
|
|
7
|
-
// --install <dir> (delegates to scripts/install.mjs),
|
|
7
|
+
// --install <dir> (delegates to scripts/install.mjs), ui start|stop|restart|status
|
|
8
|
+
// (--ui is an alias of `ui start`; see cmdUi),
|
|
8
9
|
// and -v/-V/--version (also the bare word `version`).
|
|
9
10
|
//
|
|
10
11
|
// ESM, no external dependencies.
|
|
@@ -28,6 +29,10 @@ import {
|
|
|
28
29
|
import { projectKey } from '../core/store.mjs';
|
|
29
30
|
import { formatExecLine, formatGateHeader, formatRunSummary } from './render.mjs';
|
|
30
31
|
import { pauseExitCode, describePauseReason, promptOptions, REASON } from '../core/failure-policy.mjs';
|
|
32
|
+
import { effectiveDebugSpawn } from '../core/settings.mjs';
|
|
33
|
+
import {
|
|
34
|
+
DEFAULT_UI_HOST, DEFAULT_UI_PORT, probeUi, stopUi, readUiInstance, uiUrl, waitForUiState,
|
|
35
|
+
} from '../core/ui-instance.mjs';
|
|
31
36
|
|
|
32
37
|
// ── node:sqlite runtime guard + warning filter ──────────────────────────────────
|
|
33
38
|
// Drop ONLY the one-time ExperimentalWarning emitted by node:sqlite (the module is
|
|
@@ -204,7 +209,7 @@ Usage:
|
|
|
204
209
|
worca --prompt "<task>" [--project <dir>] [options]
|
|
205
210
|
worca --file <task.md> [--project <dir>] [options]
|
|
206
211
|
worca "<task>" [--project <dir>] [options] (bare prompt; quote it)
|
|
207
|
-
worca --
|
|
212
|
+
worca ui [start|stop|restart|status] [--port <n>] [--open]
|
|
208
213
|
worca --install <targetDir> [--force]
|
|
209
214
|
|
|
210
215
|
Subcommands:
|
|
@@ -218,6 +223,9 @@ Subcommands:
|
|
|
218
223
|
disable|doctor|link|reimport|init|validate|exec. See: worca plugin help
|
|
219
224
|
marketplace <cmd> [...] Manage plugin marketplaces: add|list|refresh|remove. See: worca marketplace help
|
|
220
225
|
config [get|set|unset] Budget & cost-limit settings
|
|
226
|
+
ui [start|stop|restart|status]
|
|
227
|
+
Run the web UI (default http://localhost:4317). See: worca ui help
|
|
228
|
+
workflow <cmd> [...] Export a workflow (Claude Code skill, JSON, or plugin) / import JSON: list|export|import. See: worca workflow help
|
|
221
229
|
help Print this help (same as --help).
|
|
222
230
|
version Print the version (same as --version).
|
|
223
231
|
|
|
@@ -236,7 +244,7 @@ Options:
|
|
|
236
244
|
--branch <name> Feature branch name (default: claude proposes one)
|
|
237
245
|
--mock Offline mock mode (no claude, no tokens)
|
|
238
246
|
--yes, --non-interactive Auto-answer clarify (first option) and gates (continue)
|
|
239
|
-
--ui
|
|
247
|
+
--ui Same as "worca ui start" (accepts --port, --open, --mock)
|
|
240
248
|
--install <targetDir> Copy agents + /worca skill into <targetDir>/.claude
|
|
241
249
|
-h, --help Show this help
|
|
242
250
|
-v, -V, --version Print the version (worca <semver>) and exit
|
|
@@ -629,11 +637,97 @@ async function attachAndDrive(orch, flags, start) {
|
|
|
629
637
|
|
|
630
638
|
// ── subcommands ──────────────────────────────────────────────────────────────────
|
|
631
639
|
|
|
632
|
-
|
|
633
|
-
|
|
640
|
+
// ── web UI lifecycle ─────────────────────────────────────────────────────────────
|
|
641
|
+
//
|
|
642
|
+
// The UI is a singleton over the machine-wide store, so `worca ui` never blindly
|
|
643
|
+
// binds: it probes the port first (src/core/ui-instance.mjs). A Worca UI already
|
|
644
|
+
// answering there is the EXPECTED state — print where it is and how to restart
|
|
645
|
+
// it, exit 0. Only a port held by some other program is an error (exit 1).
|
|
646
|
+
|
|
647
|
+
const UI_HELP = `worca ui — the web UI server
|
|
648
|
+
|
|
649
|
+
Usage:
|
|
650
|
+
worca ui [start] [--port <n>] [--open] [--mock] Start the UI (default http://localhost:${DEFAULT_UI_PORT})
|
|
651
|
+
worca ui stop [--port <n>] Stop the running UI gracefully
|
|
652
|
+
worca ui restart [--port <n>] [--open] [--mock] Stop it if running, then start it again
|
|
653
|
+
worca ui status [--port <n>] Report whether it is running (exit 0 = running, 1 = not)
|
|
654
|
+
worca ui help Show this help
|
|
655
|
+
|
|
656
|
+
Options:
|
|
657
|
+
--port <n> Port to bind (start) or to look at (stop/restart/status).
|
|
658
|
+
Default: the PORT env var, then ${DEFAULT_UI_PORT}. stop/restart/status
|
|
659
|
+
also read the port of the last started UI (<worca home>/ui.json).
|
|
660
|
+
--open Open the UI in your browser once it is up
|
|
661
|
+
--mock Start in offline mock mode (same as WORCA_MOCK=1)
|
|
662
|
+
|
|
663
|
+
\`worca --ui\` is an alias of \`worca ui start\`.
|
|
664
|
+
`;
|
|
665
|
+
|
|
666
|
+
/** Bind/probe port for a `worca ui` verb: --port > (instance file) > PORT env > default. */
|
|
667
|
+
function resolveUiPort(a, { preferInstanceFile = false } = {}) {
|
|
668
|
+
if (a.port !== undefined) {
|
|
669
|
+
const n = Number(a.port);
|
|
670
|
+
if (!Number.isInteger(n) || n < 1 || n > 65535) fail(`--port must be an integer between 1 and 65535, got: ${a.port}`);
|
|
671
|
+
return { port: n, host: process.env.WORCA_HOST || DEFAULT_UI_HOST };
|
|
672
|
+
}
|
|
673
|
+
if (preferInstanceFile) {
|
|
674
|
+
const inst = readUiInstance();
|
|
675
|
+
if (inst) return { port: inst.port, host: inst.host || process.env.WORCA_HOST || DEFAULT_UI_HOST };
|
|
676
|
+
}
|
|
677
|
+
const env = Number(process.env.PORT);
|
|
678
|
+
const port = Number.isInteger(env) && env > 0 ? env : DEFAULT_UI_PORT;
|
|
679
|
+
return { port, host: process.env.WORCA_HOST || DEFAULT_UI_HOST };
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
/** Best-effort `open`/`start`/`xdg-open`; never throws, never keeps the CLI alive. */
|
|
683
|
+
function openBrowser(url) {
|
|
684
|
+
const [cmd, args] = process.platform === 'darwin' ? ['open', [url]]
|
|
685
|
+
: process.platform === 'win32' ? ['cmd', ['/c', 'start', '', url]]
|
|
686
|
+
: ['xdg-open', [url]];
|
|
687
|
+
try {
|
|
688
|
+
const p = spawn(cmd, args, { stdio: 'ignore', detached: true });
|
|
689
|
+
p.on('error', () => {});
|
|
690
|
+
p.unref();
|
|
691
|
+
} catch { /* no browser opener on this box — the URL is printed anyway */ }
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
/** Spawn ui/server.mjs on `port` and inherit its stdio. Resolves with its exit code. */
|
|
695
|
+
async function uiStart(a) {
|
|
696
|
+
const { port, host } = resolveUiPort(a);
|
|
697
|
+
const url = uiUrl({ host, port });
|
|
698
|
+
const probe = await probeUi({ host, port });
|
|
699
|
+
if (probe.state === 'worca') {
|
|
700
|
+
out(`Worca UI is already running at ${c('bold', url)}`);
|
|
701
|
+
out('');
|
|
702
|
+
out(` Open it: ${url}`);
|
|
703
|
+
out(` Restart it: ${c('bold', 'worca ui restart')}`);
|
|
704
|
+
out(` Another port: ${c('bold', `worca ui --port ${port + 1}`)}`);
|
|
705
|
+
if (a.open) openBrowser(url);
|
|
706
|
+
return 0;
|
|
707
|
+
}
|
|
708
|
+
if (probe.state === 'busy') {
|
|
709
|
+
process.stderr.write(`worca: port ${port} is in use by another program, so the Worca UI cannot start.\n\n`
|
|
710
|
+
+ ` Pick a free port: worca ui --port ${port + 1} (or set PORT)\n`);
|
|
711
|
+
return 1;
|
|
712
|
+
}
|
|
713
|
+
// A UI started earlier on some OTHER port is worth a note, not a refusal.
|
|
714
|
+
const inst = readUiInstance();
|
|
715
|
+
if (inst && inst.port !== port) {
|
|
716
|
+
const other = await probeUi({ host: inst.host, port: inst.port });
|
|
717
|
+
if (other.state === 'worca') out(c('gray', `Note: another Worca UI is running at ${uiUrl({ host: inst.host, port: inst.port })}`));
|
|
718
|
+
}
|
|
719
|
+
|
|
634
720
|
const server = join(REPO_ROOT, 'ui', 'server.mjs');
|
|
635
|
-
out(c('cyan', `
|
|
636
|
-
|
|
721
|
+
out(c('cyan', `Starting Worca UI on ${url}`));
|
|
722
|
+
if (effectiveDebugSpawn().enabled) out(c('gray', ` node ${server}`));
|
|
723
|
+
const env = { ...process.env, PORT: String(port) };
|
|
724
|
+
if (a.mock) env.WORCA_MOCK = '1';
|
|
725
|
+
const child = spawn(process.execPath, [server], { stdio: 'inherit', env });
|
|
726
|
+
if (a.open) {
|
|
727
|
+
waitForUiState({ host, port, states: ['worca'], timeoutMs: 20000 })
|
|
728
|
+
.then((r) => { if (r) openBrowser(url); })
|
|
729
|
+
.catch(() => {});
|
|
730
|
+
}
|
|
637
731
|
return new Promise((res) => {
|
|
638
732
|
child.on('exit', (code) => res(code ?? 0));
|
|
639
733
|
child.on('error', (err) => {
|
|
@@ -643,6 +737,75 @@ function launchUi() {
|
|
|
643
737
|
});
|
|
644
738
|
}
|
|
645
739
|
|
|
740
|
+
/** Stop the UI on the resolved port. Idempotent: "not running" exits 0. */
|
|
741
|
+
async function uiStop(a, { quiet = false } = {}) {
|
|
742
|
+
const { port, host } = resolveUiPort(a, { preferInstanceFile: true });
|
|
743
|
+
const r = await stopUi({ host, port });
|
|
744
|
+
switch (r.status) {
|
|
745
|
+
case 'stopped':
|
|
746
|
+
out(`Stopped Worca UI on port ${port}${r.pid ? ` (pid ${r.pid})` : ''}.`);
|
|
747
|
+
return 0;
|
|
748
|
+
case 'not-running':
|
|
749
|
+
if (!quiet) out(`Worca UI is not running on port ${port}.`);
|
|
750
|
+
return 0;
|
|
751
|
+
case 'busy':
|
|
752
|
+
process.stderr.write(`worca: port ${port} is in use by another program, not a Worca UI — nothing to stop.\n`);
|
|
753
|
+
return 1;
|
|
754
|
+
case 'timeout':
|
|
755
|
+
process.stderr.write(`worca: the Worca UI on port ${port}${r.pid ? ` (pid ${r.pid})` : ''} did not exit in time.\n`);
|
|
756
|
+
return 1;
|
|
757
|
+
default:
|
|
758
|
+
process.stderr.write(`worca: could not stop the Worca UI on port ${port}: ${r.reason || r.status}\n`);
|
|
759
|
+
return 1;
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
async function uiStatus(a) {
|
|
764
|
+
const { port, host } = resolveUiPort(a, { preferInstanceFile: true });
|
|
765
|
+
const probe = await probeUi({ host, port });
|
|
766
|
+
if (probe.state === 'worca') {
|
|
767
|
+
const info = probe.info || {};
|
|
768
|
+
const detail = [info.pid ? `pid ${info.pid}` : null, info.version ? `v${info.version}` : null].filter(Boolean).join(', ');
|
|
769
|
+
out(`Worca UI is running at ${c('bold', uiUrl({ host, port }))}${detail ? ` (${detail})` : ''}`);
|
|
770
|
+
return 0;
|
|
771
|
+
}
|
|
772
|
+
if (probe.state === 'busy') {
|
|
773
|
+
out(`Worca UI is not running on port ${port} (the port is in use by another program).`);
|
|
774
|
+
return 1;
|
|
775
|
+
}
|
|
776
|
+
out(`Worca UI is not running on port ${port}.`);
|
|
777
|
+
return 1;
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
/** `worca ui [start|stop|restart|status|help] [--port <n>] [--open] [--mock]` */
|
|
781
|
+
async function cmdUi(argv) {
|
|
782
|
+
const verbs = new Set(['start', 'stop', 'restart', 'status']);
|
|
783
|
+
let verb = 'start';
|
|
784
|
+
let rest = argv;
|
|
785
|
+
if (argv[0] === 'help' || argv[0] === '--help' || argv[0] === '-h') {
|
|
786
|
+
process.stdout.write(UI_HELP);
|
|
787
|
+
return 0;
|
|
788
|
+
}
|
|
789
|
+
if (argv[0] && !argv[0].startsWith('-')) {
|
|
790
|
+
if (!verbs.has(argv[0])) fail(`unknown ui command "${argv[0]}" — expected one of: start, stop, restart, status (see: worca ui help)`);
|
|
791
|
+
verb = argv[0];
|
|
792
|
+
rest = argv.slice(1);
|
|
793
|
+
}
|
|
794
|
+
const a = pluginArgs(rest, ['--port'], ['--open', '--mock']);
|
|
795
|
+
if (a._.length) fail(`unexpected argument: ${a._[0]} (see: worca ui help)`);
|
|
796
|
+
if (verb === 'stop') return uiStop(a);
|
|
797
|
+
if (verb === 'status') return uiStatus(a);
|
|
798
|
+
if (verb === 'restart') {
|
|
799
|
+
// Pin the port stop resolved (possibly from the instance file) so start reuses it.
|
|
800
|
+
const { port } = resolveUiPort(a, { preferInstanceFile: true });
|
|
801
|
+
a.port = String(port);
|
|
802
|
+
const code = await uiStop(a, { quiet: true });
|
|
803
|
+
if (code !== 0) return code;
|
|
804
|
+
return uiStart(a);
|
|
805
|
+
}
|
|
806
|
+
return uiStart(a);
|
|
807
|
+
}
|
|
808
|
+
|
|
646
809
|
/** Delegate to scripts/install.mjs, forwarding the target dir and any passthrough args. */
|
|
647
810
|
function runInstall(targetDir, passthrough) {
|
|
648
811
|
const script = join(REPO_ROOT, 'scripts', 'install.mjs');
|
|
@@ -1041,6 +1204,43 @@ working, including updates (install provenance lives in plugins.lock.json).
|
|
|
1041
1204
|
Exit codes: 0 ok, 1 failure, 2 usage/validation errors.
|
|
1042
1205
|
`;
|
|
1043
1206
|
|
|
1207
|
+
const WORKFLOW_HELP = `worca workflow — export a saved Composer workflow (as a Claude Code skill, as
|
|
1208
|
+
shareable JSON, or as a Worca plugin) and import one shared as JSON
|
|
1209
|
+
|
|
1210
|
+
Usage:
|
|
1211
|
+
worca workflow list List workflows (id, name, domain)
|
|
1212
|
+
worca workflow export <id> [options] Export a workflow (see --format)
|
|
1213
|
+
worca workflow import <file> [--name <name>] Import a JSON export into your library ('-' = stdin)
|
|
1214
|
+
|
|
1215
|
+
Export formats (--format):
|
|
1216
|
+
claude (default) A runnable Claude Code skill tree under <dest>/.claude/
|
|
1217
|
+
json The shareable v2 graph, to stdout (or --out <file>)
|
|
1218
|
+
plugin A Worca plugin folder (--target <dir>) bundling the workflow,
|
|
1219
|
+
your own agents it uses and the skills they require; the
|
|
1220
|
+
recipient runs: worca plugin link <dir>
|
|
1221
|
+
|
|
1222
|
+
Options (claude):
|
|
1223
|
+
--global Export to the home dir (~/.claude), not a project
|
|
1224
|
+
--target <dir> Export into <dir>/.claude (default: cwd)
|
|
1225
|
+
--slug <name> Skill slug (default: slugified workflow name)
|
|
1226
|
+
--dry-run Show the classification (create/update/no-op/conflict); write nothing
|
|
1227
|
+
--on-conflict <mode> skip (default) | overwrite | namespace conflicting files
|
|
1228
|
+
Options (json):
|
|
1229
|
+
--out <file> Write to <file> instead of stdout
|
|
1230
|
+
Options (plugin):
|
|
1231
|
+
--target <dir> The plugin folder (created, or updated in place)
|
|
1232
|
+
--name <plugin> Plugin name (default: an existing manifest's name, else the folder name)
|
|
1233
|
+
--keep-version Do not patch-bump the manifest version on an update
|
|
1234
|
+
--dry-run Show the classification; write nothing
|
|
1235
|
+
|
|
1236
|
+
Import: the id is minted from the name; a name already in use gets a " (2)" suffix —
|
|
1237
|
+
nothing is ever overwritten. A workflow that needs agents you do not have is refused;
|
|
1238
|
+
ask its author for the plugin export instead.
|
|
1239
|
+
|
|
1240
|
+
Export always prints the plan first, then applies (unless --dry-run). A re-export
|
|
1241
|
+
of an unchanged workflow is an all-no-op. Exit codes: 0 ok, 1 failure, 2 usage/validation errors.
|
|
1242
|
+
`;
|
|
1243
|
+
|
|
1044
1244
|
/** Tiny per-verb arg parser: positionals plus declared --value / --bool flags. */
|
|
1045
1245
|
function pluginArgs(argv, valueFlags = [], boolFlags = []) {
|
|
1046
1246
|
const out = { _: [] };
|
|
@@ -1058,7 +1258,7 @@ function pluginArgs(argv, valueFlags = [], boolFlags = []) {
|
|
|
1058
1258
|
out[a.slice(2)] = v;
|
|
1059
1259
|
} else if (boolFlags.includes(a)) {
|
|
1060
1260
|
out[a.slice(2)] = true;
|
|
1061
|
-
} else if (a.startsWith('-')) {
|
|
1261
|
+
} else if (a.startsWith('-') && a !== '-') { // a lone '-' is the stdin positional
|
|
1062
1262
|
fail(`Unknown flag: ${a}`);
|
|
1063
1263
|
} else {
|
|
1064
1264
|
out._.push(a);
|
|
@@ -1143,9 +1343,8 @@ async function pluginInit(rest) {
|
|
|
1143
1343
|
for (const part of withParts) {
|
|
1144
1344
|
if (!INIT_PARTS.includes(part)) fail(`unknown --with part "${part}" (known: ${INIT_PARTS.join(', ')})`);
|
|
1145
1345
|
}
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
}
|
|
1346
|
+
// `--with workflows` no longer requires `agents`: a plugin template may reference
|
|
1347
|
+
// built-in agents (#421), so a workflow-only plugin scaffolds a built-in chain.
|
|
1149
1348
|
const target = resolve(process.cwd(), a.dir || name);
|
|
1150
1349
|
const { mkdir, writeFile, chmod, readdir } = await import('node:fs/promises');
|
|
1151
1350
|
try {
|
|
@@ -1254,19 +1453,24 @@ async function pluginInit(rest) {
|
|
|
1254
1453
|
}
|
|
1255
1454
|
if (withParts.includes('workflows')) {
|
|
1256
1455
|
// A v2 graph: the Task and End cards are mandatory (V20/V21) and every input
|
|
1257
|
-
// takes exactly one wire (V7).
|
|
1456
|
+
// takes exactly one wire (V7). With agents, ports come from the sidecar above;
|
|
1457
|
+
// without, the chain runs the built-in planner (a plugin template may
|
|
1458
|
+
// reference built-ins — they are on every host).
|
|
1459
|
+
const withAgent = withParts.includes('agents');
|
|
1460
|
+
const key = withAgent ? agentKey : 'planner';
|
|
1461
|
+
const outPort = withAgent ? 'notes' : 'plan';
|
|
1258
1462
|
files.set('workflows/example-flow.json', JSON.stringify({
|
|
1259
1463
|
name: `${name} example flow`,
|
|
1260
1464
|
version: 2,
|
|
1261
1465
|
domain: 'general',
|
|
1262
1466
|
nodes: [
|
|
1263
1467
|
{ id: 'n_task', kind: 'task', x: 40, y: 200, config: {} },
|
|
1264
|
-
{ id: 'n_helper', kind: 'agent', key
|
|
1468
|
+
{ id: 'n_helper', kind: 'agent', key, x: 320, y: 200, config: {} },
|
|
1265
1469
|
{ id: 'n_end', kind: 'end', x: 600, y: 200, config: {} },
|
|
1266
1470
|
],
|
|
1267
1471
|
wires: [
|
|
1268
1472
|
{ id: 'w1', from: { node: 'n_task', port: 'task' }, to: { node: 'n_helper', port: 'task' } },
|
|
1269
|
-
{ id: 'w2', from: { node: 'n_helper', port:
|
|
1473
|
+
{ id: 'w2', from: { node: 'n_helper', port: outPort }, to: { node: 'n_end', port: 'result' } },
|
|
1270
1474
|
],
|
|
1271
1475
|
}, null, 2) + '\n');
|
|
1272
1476
|
}
|
|
@@ -1676,9 +1880,141 @@ async function cmdMarketplace(argv) {
|
|
|
1676
1880
|
}
|
|
1677
1881
|
}
|
|
1678
1882
|
|
|
1883
|
+
async function cmdWorkflow(argv) {
|
|
1884
|
+
const verb = argv[0];
|
|
1885
|
+
const rest = argv.slice(1);
|
|
1886
|
+
if (!verb || verb === 'help') { process.stdout.write(WORKFLOW_HELP); return 0; }
|
|
1887
|
+
const wf = await import('../core/workflows.mjs');
|
|
1888
|
+
const xp = await import('../core/workflow-export.mjs');
|
|
1889
|
+
const share = await import('../core/workflow-share.mjs');
|
|
1890
|
+
const { formatIssue } = await import('../shared/graph/validate.mjs');
|
|
1891
|
+
try {
|
|
1892
|
+
switch (verb) {
|
|
1893
|
+
case 'list': {
|
|
1894
|
+
// GRAPH_DEFAULT_WORKFLOW (the built-in default) is not in the user store, so
|
|
1895
|
+
// prepend it — mirrors the server/UI, which always show it first.
|
|
1896
|
+
const items = [wf.GRAPH_DEFAULT_WORKFLOW, ...(await wf.listWorkflows())];
|
|
1897
|
+
for (const w of items) out(`${w.id}\t${w.name}\t${(w.domain || 'general')}`);
|
|
1898
|
+
return 0;
|
|
1899
|
+
}
|
|
1900
|
+
case 'import': {
|
|
1901
|
+
const a = pluginArgs(rest, ['--name'], []);
|
|
1902
|
+
const file = a._[0];
|
|
1903
|
+
if (!file) fail('Usage: worca workflow import <file> [--name <name>]');
|
|
1904
|
+
const { readFile } = await import('node:fs/promises');
|
|
1905
|
+
let text;
|
|
1906
|
+
try {
|
|
1907
|
+
text = file === '-'
|
|
1908
|
+
? await new Promise((res, rej) => { let s = ''; process.stdin.setEncoding('utf8'); process.stdin.on('data', (c) => (s += c)); process.stdin.on('end', () => res(s)); process.stdin.on('error', rej); })
|
|
1909
|
+
: await readFile(resolve(process.cwd(), file), 'utf8');
|
|
1910
|
+
} catch (e) { process.stderr.write(`worca workflow import: cannot read ${file}: ${e.message}\n`); return 1; }
|
|
1911
|
+
let obj;
|
|
1912
|
+
try { obj = JSON.parse(text); } catch (e) { process.stderr.write(`worca workflow import: ${file} is not valid JSON (${e.message})\n`); return 2; }
|
|
1913
|
+
try {
|
|
1914
|
+
const r = await share.importGraphWorkflow(obj, { name: a.name });
|
|
1915
|
+
out(`imported\t${r.workflow.id}\t${r.workflow.name}`);
|
|
1916
|
+
if (r.renamed) out(c('yellow', `renamed: "${r.requestedName}" was already taken — saved as "${r.workflow.name}"`));
|
|
1917
|
+
for (const w of r.warnings || []) out(`${c('yellow', 'warn')}\t${formatIssue(w)}`);
|
|
1918
|
+
return 0;
|
|
1919
|
+
} catch (e) {
|
|
1920
|
+
if (e && e.code === 'INVALID_GRAPH') {
|
|
1921
|
+
process.stderr.write(`worca workflow import: ${e.summary || 'invalid graph'}\n`);
|
|
1922
|
+
for (const issue of e.errors || []) process.stderr.write(` - ${formatIssue(issue)}\n`);
|
|
1923
|
+
return 2;
|
|
1924
|
+
}
|
|
1925
|
+
if (e && (e.code === 'BAD_REQUEST' || e.code === 'RESERVED_NAME')) { process.stderr.write(`worca workflow import: ${e.message}\n`); return 2; }
|
|
1926
|
+
throw e;
|
|
1927
|
+
}
|
|
1928
|
+
}
|
|
1929
|
+
case 'export': {
|
|
1930
|
+
const a = pluginArgs(rest, ['--target', '--slug', '--on-conflict', '--format', '--out', '--name'], ['--global', '--dry-run', '--keep-version']);
|
|
1931
|
+
const id = a._[0];
|
|
1932
|
+
if (!id) fail('Usage: worca workflow export <id> [--format claude|json|plugin] [--global | --target <dir>] [--slug <name>] [--out <file>] [--name <plugin>] [--keep-version] [--dry-run] [--on-conflict=skip|overwrite|namespace]');
|
|
1933
|
+
const format = a.format || 'claude';
|
|
1934
|
+
if (!['claude', 'json', 'plugin'].includes(format)) fail(`--format must be claude|json|plugin (got ${format})`);
|
|
1935
|
+
|
|
1936
|
+
if (format === 'json') {
|
|
1937
|
+
const payload = await share.exportGraphJson(id);
|
|
1938
|
+
const text = JSON.stringify(payload, null, 2) + '\n';
|
|
1939
|
+
if (a.out) {
|
|
1940
|
+
const { writeFile } = await import('node:fs/promises');
|
|
1941
|
+
const dest = resolve(process.cwd(), a.out);
|
|
1942
|
+
await writeFile(dest, text, 'utf8');
|
|
1943
|
+
out(`${c('green', 'wrote')}\t${dest}`);
|
|
1944
|
+
} else {
|
|
1945
|
+
process.stdout.write(text);
|
|
1946
|
+
}
|
|
1947
|
+
return 0;
|
|
1948
|
+
}
|
|
1949
|
+
|
|
1950
|
+
if (format === 'plugin') {
|
|
1951
|
+
if (!a.target) fail('--format plugin requires --target <dir> (the plugin folder)');
|
|
1952
|
+
if (a.global) fail('--global does not apply to --format plugin');
|
|
1953
|
+
const r = await xp.exportWorkflowPlugin({
|
|
1954
|
+
workflowId: id, targetDir: resolve(process.cwd(), a.target), pluginName: a.name,
|
|
1955
|
+
keepVersion: !!a['keep-version'], dryRun: !!a['dry-run'],
|
|
1956
|
+
});
|
|
1957
|
+
for (const p of r.created) out(`${c('green', 'create')}\t${p}`);
|
|
1958
|
+
for (const p of r.updated) out(`${c('cyan', 'update')}\t${p}`);
|
|
1959
|
+
for (const p of r.noop) out(`${c('gray', 'no-op')}\t${p}`);
|
|
1960
|
+
for (const s of r.skipped) out(`${c('gray', 'skip')}\t${s.path}\t(${s.reason})`);
|
|
1961
|
+
for (const w of r.warnings || []) out(`${c('yellow', 'warn')}\t${w}`);
|
|
1962
|
+
for (const p of r.written) out(`${c('green', 'wrote')}\t${p}`);
|
|
1963
|
+
if (r.validation && !r.validation.ok) {
|
|
1964
|
+
out(c('yellow', `\nthe plugin folder does not validate — fix the errors above before sharing it`));
|
|
1965
|
+
return 1;
|
|
1966
|
+
}
|
|
1967
|
+
if (!a['dry-run']) {
|
|
1968
|
+
out(`\nplugin "${r.name}" v${r.version} at ${r.dir}`);
|
|
1969
|
+
out(`share the folder; the recipient runs: worca plugin link ${r.dir}`);
|
|
1970
|
+
}
|
|
1971
|
+
return 0;
|
|
1972
|
+
}
|
|
1973
|
+
|
|
1974
|
+
if (a.global && a.target) fail('--global and --target are mutually exclusive');
|
|
1975
|
+
const onConflict = a['on-conflict'] || 'skip';
|
|
1976
|
+
if (!xp.ON_CONFLICT_MODES.includes(onConflict)) fail(`--on-conflict must be ${xp.ON_CONFLICT_MODES.join('|')} (got ${onConflict})`);
|
|
1977
|
+
const destination = a.global ? 'global' : 'project';
|
|
1978
|
+
const projectDir = a.global ? undefined : (a.target || process.cwd());
|
|
1979
|
+
const common = { workflowId: id, destination, projectDir, slug: a.slug };
|
|
1980
|
+
|
|
1981
|
+
// Always show the classification first. A dry-run stops at planExport (writes nothing);
|
|
1982
|
+
// an apply reuses the buckets applyExport already computes, so the full resolve+classify
|
|
1983
|
+
// pipeline runs ONCE, not twice.
|
|
1984
|
+
const printPlan = (p) => {
|
|
1985
|
+
for (const path of p.created) out(`${c('green', 'create')}\t${path}`);
|
|
1986
|
+
for (const path of p.updated) out(`${c('cyan', 'update')}\t${path}`);
|
|
1987
|
+
for (const path of p.noop) out(`${c('gray', 'no-op')}\t${path}`);
|
|
1988
|
+
for (const cf of p.conflicts) out(`${c('yellow', 'conflict')}\t${cf.path}\t(${cf.reason})`);
|
|
1989
|
+
for (const w of p.warnings || []) out(`${c('yellow', 'warn')}\t${w}`);
|
|
1990
|
+
for (const o of p.orphans || []) out(`${c('gray', 'orphan')}\t${o}`);
|
|
1991
|
+
};
|
|
1992
|
+
|
|
1993
|
+
if (a['dry-run']) { printPlan(await xp.planExport(common)); return 0; }
|
|
1994
|
+
|
|
1995
|
+
const applied = await xp.applyExport({ ...common, onConflict });
|
|
1996
|
+
printPlan(applied);
|
|
1997
|
+
for (const p of applied.written) out(`${c('green', 'wrote')}\t${p}`);
|
|
1998
|
+
for (const p of applied.skipped) out(`${c('gray', 'skip')}\t${p}`);
|
|
1999
|
+
if (applied.conflicts.length) {
|
|
2000
|
+
// A partial export (conflicts skipped) can leave a non-runnable skill on disk. Warn AND
|
|
2001
|
+
// exit non-zero so a CI/script that only checks the exit code does not treat it as success.
|
|
2002
|
+
out(c('yellow', `\n${applied.conflicts.length} conflict(s) left unresolved (--on-conflict=${onConflict}). Re-run with --on-conflict=overwrite|namespace.`));
|
|
2003
|
+
return 1;
|
|
2004
|
+
}
|
|
2005
|
+
return 0;
|
|
2006
|
+
}
|
|
2007
|
+
default: fail(`unknown workflow verb "${verb}" — see: worca workflow help`);
|
|
2008
|
+
}
|
|
2009
|
+
} catch (err) {
|
|
2010
|
+
process.stderr.write(`worca workflow ${verb}: ${err && err.message ? err.message : err}\n`);
|
|
2011
|
+
return 1;
|
|
2012
|
+
}
|
|
2013
|
+
}
|
|
2014
|
+
|
|
1679
2015
|
// ── main ──────────────────────────────────────────────────────────────────────────
|
|
1680
2016
|
|
|
1681
|
-
const SUBCOMMANDS = new Set(['add', 'list', 'remove', 'resume', 'doctor', 'plugin', 'marketplace', 'config']);
|
|
2017
|
+
const SUBCOMMANDS = new Set(['add', 'list', 'remove', 'resume', 'doctor', 'plugin', 'marketplace', 'config', 'ui', 'workflow']);
|
|
1682
2018
|
|
|
1683
2019
|
/** Levenshtein distance, two-row. Only ever called on short argv tokens. */
|
|
1684
2020
|
function editDistance(a, b) {
|
|
@@ -1736,6 +2072,13 @@ async function main() {
|
|
|
1736
2072
|
if (sub === 'plugin') return cmdPlugin(rest);
|
|
1737
2073
|
if (sub === 'marketplace') return cmdMarketplace(rest);
|
|
1738
2074
|
if (sub === 'config') return cmdConfig(rest);
|
|
2075
|
+
if (sub === 'ui') return cmdUi(rest);
|
|
2076
|
+
if (sub === 'workflow') return cmdWorkflow(rest);
|
|
2077
|
+
}
|
|
2078
|
+
// `worca --ui [...]` is the historical spelling of `worca ui start [...]`; hand the
|
|
2079
|
+
// remaining tokens to the ui parser so --port/--open/--mock work with either.
|
|
2080
|
+
if (process.argv.slice(2).includes('--ui')) {
|
|
2081
|
+
return cmdUi(process.argv.slice(2).filter((t) => t !== '--ui'));
|
|
1739
2082
|
}
|
|
1740
2083
|
|
|
1741
2084
|
const flags = parseArgs(process.argv.slice(2));
|
|
@@ -1752,10 +2095,6 @@ async function main() {
|
|
|
1752
2095
|
return runInstall(flags.install, passthrough);
|
|
1753
2096
|
}
|
|
1754
2097
|
|
|
1755
|
-
if (flags.ui) {
|
|
1756
|
-
return launchUi();
|
|
1757
|
-
}
|
|
1758
|
-
|
|
1759
2098
|
if (flags.mock) {
|
|
1760
2099
|
process.env.WORCA_MOCK = '1';
|
|
1761
2100
|
}
|
package/src/core/ask/models.mjs
CHANGED
|
@@ -47,7 +47,12 @@ export function createAskModels({
|
|
|
47
47
|
/** The D8 initial pick, validated against the live catalog (D5). */
|
|
48
48
|
function pickDefault(models) {
|
|
49
49
|
const want = String(defaults.defaultModel || '').toLowerCase();
|
|
50
|
-
|
|
50
|
+
// Hidden built-ins (#422) are never the initial pick: on an install that
|
|
51
|
+
// hides them the default is the first model the user actually owns. They
|
|
52
|
+
// stay in `models` so a stored thread on one keeps validating.
|
|
53
|
+
const visible = models.filter((m) => !m.hidden);
|
|
54
|
+
const pool = visible.length ? visible : models;
|
|
55
|
+
const hit = pool.find((m) => m.id.toLowerCase() === want) || pool[0] || null;
|
|
51
56
|
if (!hit) return null;
|
|
52
57
|
const efforts = hit.efforts.length ? hit.efforts : [...EFFORTS];
|
|
53
58
|
const effort = efforts.includes(defaults.defaultEffort)
|
|
@@ -83,6 +88,7 @@ export function createAskModels({
|
|
|
83
88
|
hasEnv: m.hasEnv === true,
|
|
84
89
|
};
|
|
85
90
|
if (custom === 'plugin' && typeof m.plugin === 'string' && m.plugin) entry.plugin = m.plugin;
|
|
91
|
+
if (m.hidden === true) entry.hidden = true; // the picker skips it; validation does not (#422)
|
|
86
92
|
// Only globals and plugin entries can arrive flagged: composeCatalog emits an
|
|
87
93
|
// UNSHADOWED built-in as {...m, custom:false, hasEnv:false} with no
|
|
88
94
|
// ...unreliable(lc) (src/core/config.mjs:200), so a built-in in model_cost_flags
|
package/src/core/ask/turn.mjs
CHANGED
|
@@ -439,6 +439,11 @@ class AskTurn extends EventEmitter {
|
|
|
439
439
|
tools: [], strictMcpConfig: true, settingSources: ['project'],
|
|
440
440
|
disableSlashCommands: true, envScrub: true, envAllowlist: [],
|
|
441
441
|
permissionMode: 'dontAsk',
|
|
442
|
+
// The chat's own model is the title default (#422) — a chat on a
|
|
443
|
+
// custom endpoint titles itself there, not on a first-party Haiku.
|
|
444
|
+
runModel: this.model,
|
|
445
|
+
onError: ({ model, error }) => console.warn(
|
|
446
|
+
`[worca-ask] thread ${this.threadId}: title generation failed (model ${model}): ${error?.message || error} — keeping the fallback title`),
|
|
442
447
|
}))
|
|
443
448
|
.then((generated) => {
|
|
444
449
|
// The route stamps NOTHING before the 202 (the header reads "Ask Worca"
|