@plurnk/plurnk 0.74.0 → 0.76.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.defaults +3 -4
- package/README.md +60 -23
- package/completions/_plurnk +13 -0
- package/completions/plurnk.bash +14 -0
- package/completions/plurnk.fish +37 -0
- package/conformance/agui-client.json +863 -0
- package/dist/agents.d.ts +11 -0
- package/dist/agents.d.ts.map +1 -0
- package/dist/agents.js +170 -0
- package/dist/agents.js.map +1 -0
- package/dist/agui.d.ts +1 -0
- package/dist/agui.d.ts.map +1 -1
- package/dist/agui.js +45 -5
- package/dist/agui.js.map +1 -1
- package/dist/agui_cli.d.ts +1 -4
- package/dist/agui_cli.d.ts.map +1 -1
- package/dist/agui_cli.js +29 -9
- package/dist/agui_cli.js.map +1 -1
- package/dist/build-info.json +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +12 -7
- package/dist/cli.js.map +1 -1
- package/dist/color.d.ts +2 -0
- package/dist/color.d.ts.map +1 -0
- package/dist/color.js +5 -0
- package/dist/color.js.map +1 -0
- package/dist/completion.js +4 -4
- package/dist/completion.js.map +1 -1
- package/dist/diagnostics.d.ts +1 -1
- package/dist/diagnostics.d.ts.map +1 -1
- package/dist/diagnostics.js +4 -2
- package/dist/diagnostics.js.map +1 -1
- package/dist/dispatcher.d.ts +8 -8
- package/dist/dispatcher.d.ts.map +1 -1
- package/dist/dispatcher.js +196 -104
- package/dist/dispatcher.js.map +1 -1
- package/dist/envdefaults.d.ts.map +1 -1
- package/dist/envdefaults.js +4 -6
- package/dist/envdefaults.js.map +1 -1
- package/dist/markdown.d.ts +17 -0
- package/dist/markdown.d.ts.map +1 -0
- package/dist/markdown.js +234 -0
- package/dist/markdown.js.map +1 -0
- package/dist/mcp.d.ts +11 -1
- package/dist/mcp.d.ts.map +1 -1
- package/dist/mcp.js +213 -69
- package/dist/mcp.js.map +1 -1
- package/dist/paste.d.ts +1 -0
- package/dist/paste.d.ts.map +1 -1
- package/dist/paste.js +5 -0
- package/dist/paste.js.map +1 -1
- package/dist/paths.d.ts +2 -0
- package/dist/paths.d.ts.map +1 -0
- package/dist/paths.js +12 -0
- package/dist/paths.js.map +1 -0
- package/dist/plan.d.ts +12 -0
- package/dist/plan.d.ts.map +1 -0
- package/dist/plan.js +30 -0
- package/dist/plan.js.map +1 -0
- package/dist/proposal.d.ts +3 -5
- package/dist/proposal.d.ts.map +1 -1
- package/dist/proposal.js +54 -34
- package/dist/proposal.js.map +1 -1
- package/dist/reasoning-events.d.ts +23 -0
- package/dist/reasoning-events.d.ts.map +1 -0
- package/dist/reasoning-events.js +40 -0
- package/dist/reasoning-events.js.map +1 -0
- package/dist/reasoning.d.ts +11 -0
- package/dist/reasoning.d.ts.map +1 -0
- package/dist/reasoning.js +10 -0
- package/dist/reasoning.js.map +1 -0
- package/dist/render.d.ts +4 -0
- package/dist/render.d.ts.map +1 -1
- package/dist/render.js +93 -50
- package/dist/render.js.map +1 -1
- package/dist/skills.d.ts +7 -0
- package/dist/skills.d.ts.map +1 -0
- package/dist/skills.js +152 -0
- package/dist/skills.js.map +1 -0
- package/dist/stream.d.ts.map +1 -1
- package/dist/stream.js +11 -11
- package/dist/stream.js.map +1 -1
- package/dist/subcommands.d.ts +3 -0
- package/dist/subcommands.d.ts.map +1 -1
- package/dist/subcommands.js +32 -11
- package/dist/subcommands.js.map +1 -1
- package/dist/transport.d.ts +12 -4
- package/dist/transport.d.ts.map +1 -1
- package/dist/transport.js +77 -21
- package/dist/transport.js.map +1 -1
- package/dist/tui.d.ts +23 -13
- package/dist/tui.d.ts.map +1 -1
- package/dist/tui.js +335 -92
- package/dist/tui.js.map +1 -1
- package/man/plurnk.1 +153 -0
- package/package.json +16 -9
package/dist/dispatcher.js
CHANGED
|
@@ -3,19 +3,20 @@
|
|
|
3
3
|
// §2 (CLI mode) and §3 (TUI mode).
|
|
4
4
|
import { parseArgs } from "node:util";
|
|
5
5
|
import { readFile } from "node:fs/promises";
|
|
6
|
-
import { isAbsolute,
|
|
7
|
-
import { homedir } from "node:os";
|
|
6
|
+
import { isAbsolute, resolve } from "node:path";
|
|
8
7
|
import { createRequire } from "node:module";
|
|
9
|
-
import { parseAliasesFromEnv } from "@plurnk/plurnk-aliases";
|
|
10
8
|
import { buildJsonError } from "./cli.js";
|
|
11
9
|
import { loadFloor } from "./envdefaults.js";
|
|
12
10
|
import { runCliViaBridge, runScriptViaBridge } from "./agui_cli.js";
|
|
13
11
|
import { BridgeTransport } from "./transport.js";
|
|
14
12
|
import { actionViaBridge, resolveWorld } from "./agui.js";
|
|
15
13
|
import { runTui } from "./tui.js";
|
|
14
|
+
import { handleMcp } from "./mcp.js";
|
|
15
|
+
import { formatWorkerReasoning, readWorkerReasoning, setWorkerReasoning, } from "./reasoning.js";
|
|
16
16
|
import { runModels, runWorkspaceList, runWorkspaceWorkers, runWorkspaceRename, runLogRead, runRead } from "./subcommands.js";
|
|
17
17
|
import { ProblemError, report, clientConnectionRefused, isUnreachable, clientProblem, clientFlagInvalid, clientFlagMissingDependency, clientRuntimeError, clientSubcommandMissingArgument, clientSubcommandUnknownVerb, clientWorkerNotFound, } from "./diagnostics.js";
|
|
18
18
|
import { formatBuildInfo, getBuildInfo } from "./build-info.js";
|
|
19
|
+
import { userConfigFile } from "./paths.js";
|
|
19
20
|
// Read all of stdin to EOF. Called when stdin is piped (not a TTY) — never
|
|
20
21
|
// blocks an interactive workspace because we gate on isTTY upstream.
|
|
21
22
|
const readStdin = async () => {
|
|
@@ -43,19 +44,6 @@ export const resolveLoopFlags = (rawJson, auto = false) => {
|
|
|
43
44
|
}
|
|
44
45
|
return { ...parsed, ...(auto ? { auto: true } : {}) };
|
|
45
46
|
};
|
|
46
|
-
// #90 — resolve a model alias to a concrete "<provider>/<model>" from the CLIENT's
|
|
47
|
-
// (always-fresh) env, so a long-lived daemon launched before the user set
|
|
48
|
-
// PLURNK_MODEL_<alias> doesn't reject loop.run with "unknown alias" (the daemon's
|
|
49
|
-
// launch env is frozen; ours isn't). First-slash split is lossless — provider is
|
|
50
|
-
// before the first "/", the model id is the rest (may itself contain "/"). baseUrl
|
|
51
|
-
// stays daemon-side. null → send bare {alias} and let the daemon resolve or fail.
|
|
52
|
-
// parseAliasesFromEnv is fail-hard on a duplicate/dangling env config — let it throw.
|
|
53
|
-
export const resolveModelSpec = (alias, env = process.env) => {
|
|
54
|
-
if (alias === undefined)
|
|
55
|
-
return undefined;
|
|
56
|
-
const match = parseAliasesFromEnv(env).find((a) => a.alias === alias.toLowerCase());
|
|
57
|
-
return match !== undefined ? `${match.provider}/${match.model}` : undefined;
|
|
58
|
-
};
|
|
59
47
|
// #132 — the client's per-workspace exec-policy layer: forward the closed
|
|
60
48
|
// enable/disable grammar (PLURNK_EXECS_ONLY, PLURNK_EXECS_<TAG>=0|false) so the
|
|
61
49
|
// daemon intersects it with its own ceiling (service ∧ client — subtractive,
|
|
@@ -77,6 +65,23 @@ export const collectExecsPolicy = (env = process.env) => {
|
|
|
77
65
|
}
|
|
78
66
|
return out;
|
|
79
67
|
};
|
|
68
|
+
const MCP_CONFIGURATION_PREFIX = "PLURNK_MCP_";
|
|
69
|
+
const MCP_SERVICE_CONTROLS = new Set([
|
|
70
|
+
"PLURNK_MCP_CONNECT_TIMEOUT",
|
|
71
|
+
"PLURNK_MCP_REQUEST_TIMEOUT",
|
|
72
|
+
"PLURNK_MCP_ENABLED",
|
|
73
|
+
]);
|
|
74
|
+
export const collectMcpConfiguration = (env = process.env) => {
|
|
75
|
+
const configuration = {};
|
|
76
|
+
for (const [key, value] of Object.entries(env)) {
|
|
77
|
+
if (value === undefined || !key.startsWith(MCP_CONFIGURATION_PREFIX))
|
|
78
|
+
continue;
|
|
79
|
+
if (MCP_SERVICE_CONTROLS.has(key.toUpperCase()))
|
|
80
|
+
continue;
|
|
81
|
+
configuration[key] = value;
|
|
82
|
+
}
|
|
83
|
+
return configuration;
|
|
84
|
+
};
|
|
80
85
|
// projectRoot resolution: empty string = explicit headless (null on wire);
|
|
81
86
|
// otherwise must be an absolute path. Caller passes cwd as default.
|
|
82
87
|
export const resolveProjectRoot = (raw) => {
|
|
@@ -88,15 +93,18 @@ export const resolveProjectRoot = (raw) => {
|
|
|
88
93
|
throw new ProblemError(clientFlagInvalid("--project-root", raw, "must be an absolute path"));
|
|
89
94
|
return raw;
|
|
90
95
|
};
|
|
91
|
-
const USAGE = `usage: plurnk [--json] [--workspace <name>] [--worker <name>] [--model <
|
|
96
|
+
export const USAGE = `usage: plurnk [--json] [--workspace <name>] [--worker <name>] [--model <selector>] [--reasoning <policy>] [prompt...]
|
|
92
97
|
<piped stdin> | plurnk [options] [prompt...]
|
|
93
|
-
plurnk models [--json]
|
|
98
|
+
plurnk models [search...] [--provider <name>] [--all] [--offset <n>] [--limit <n>] [--json]
|
|
94
99
|
plurnk workspace list [--json]
|
|
95
100
|
plurnk workspace workers <name> [--json]
|
|
96
101
|
plurnk workspace rename <name> <newname> [--json]
|
|
97
102
|
plurnk log read --workspace <name> [--worker <name>]
|
|
98
103
|
[--loop <id>] [--turn <id>] [--since <id>] [--limit <n>] [--json]
|
|
99
104
|
plurnk read <loop>/<turn>/<seq> --workspace <name> [--worker <name>] [--json]
|
|
105
|
+
plurnk reasoning [policy] --workspace <name> [--worker <name>] [--json]
|
|
106
|
+
plurnk mcp [add <alias> <target> [options.json] | enable <alias> [options.json]
|
|
107
|
+
| disable|remove <alias> | oauth <alias> <callback-url>]
|
|
100
108
|
|
|
101
109
|
Connects to the plurnk-service daemon. Run a single prompt one-shot
|
|
102
110
|
(positional args, piped stdin, or both — positionals come first, stdin
|
|
@@ -104,17 +112,12 @@ is appended after a blank line). With no positionals and a TTY stdin,
|
|
|
104
112
|
enters the interactive REPL. Read-only subcommands (models / workspace list /
|
|
105
113
|
log read / read <coord>) inspect daemon state without running a loop.
|
|
106
114
|
|
|
107
|
-
env (cascade,
|
|
108
|
-
<
|
|
115
|
+
env (cascade, low → high: packaged .env.defaults < $XDG_CONFIG_HOME/plurnk/.env
|
|
116
|
+
< ./.env < repeated --env-file flags (last wins) < shell):
|
|
109
117
|
Works with no config at all.
|
|
110
118
|
PLURNK_CLIENT_WORKSPACE resume/create a workspace by name. UNSET = the daemon
|
|
111
119
|
mints a fresh, uniquely-named workspace per invocation.
|
|
112
120
|
PLURNK_CLIENT_WORKER resume (or create) a named worker within that workspace
|
|
113
|
-
PLURNK_MODEL model alias to use for every loop.run on this invocation.
|
|
114
|
-
Shared with the daemon (user-level preference). --model
|
|
115
|
-
overrides for this invocation only.
|
|
116
|
-
PLURNK_MODEL_CHILD optional WORK/FORK/BARE provider alias. Unset = inherit the
|
|
117
|
-
spawning loop's provider; interactive /child overrides.
|
|
118
121
|
PLURNK_CLIENT_PROJECT_ROOT absolute path passed to workspace.create as the workspace's
|
|
119
122
|
project_root (workspace for file ops). Default: cwd.
|
|
120
123
|
Empty string = headless (no project_root, file ops 400).
|
|
@@ -122,9 +125,9 @@ env (cascade, highest first: shell < --env-file < ./.env < ~/.plurnk/.env
|
|
|
122
125
|
PLURNK_AUTO when truthy, keep proposal authority inside the loop.
|
|
123
126
|
Client-side only — proposals still go through the wire.
|
|
124
127
|
PLURNK_CLIENT_JSON when truthy, same as --json for one-shot runs.
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
--
|
|
128
|
+
PLURNK_REQUEST_USER_INPUT when truthy, the conversation worker may ask you
|
|
129
|
+
through the question tool. TUI/nvim default on; the
|
|
130
|
+
one-shot CLI defaults off. --request_user_input overrides.
|
|
128
131
|
PLURNK_AGUI_URL plurnk-agui bridge URL (e.g. http://127.0.0.1:8787). When
|
|
129
132
|
set, a one-shot (text AND --json) runs THROUGH the bridge
|
|
130
133
|
instead of raw daemon WS (the exclusive-portal path).
|
|
@@ -136,7 +139,10 @@ env (cascade, highest first: shell < --env-file < ./.env < ~/.plurnk/.env
|
|
|
136
139
|
never forwarded. Subtractive only — the
|
|
137
140
|
daemon intersects with its ceiling; the client can narrow,
|
|
138
141
|
never re-enable. Shares the daemon's grammar; a workspace's
|
|
139
|
-
.env carries its own.
|
|
142
|
+
.env carries its own.
|
|
143
|
+
PLURNK_MCP_* raw server declarations accompany MCP list and enable.
|
|
144
|
+
Service controls do not. The daemon owns parsing,
|
|
145
|
+
activation, persistence, and credential expansion.
|
|
140
146
|
|
|
141
147
|
options:
|
|
142
148
|
-h, --help print this message and exit
|
|
@@ -151,10 +157,14 @@ options:
|
|
|
151
157
|
auto-named workspace is created. Overrides PLURNK_CLIENT_WORKSPACE.
|
|
152
158
|
--worker <name> resume (or create) the named worker within the workspace.
|
|
153
159
|
Requires --workspace. Overrides PLURNK_CLIENT_WORKER.
|
|
154
|
-
--model <
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
160
|
+
--model <selector> persistently select the conversation worker's model
|
|
161
|
+
before the first loop (worker.model.set). A selector is
|
|
162
|
+
a declared alias or exact provider/model route. Without
|
|
163
|
+
this, the worker's durable model or the daemon's
|
|
164
|
+
boot-time default runs.
|
|
165
|
+
--reasoning <policy> persistently select the conversation worker's reasoning
|
|
166
|
+
policy before the first loop. The daemon validates the
|
|
167
|
+
policy against the selected parent and child models.
|
|
158
168
|
--project-root <p> absolute path. Sent on workspace.create only; ignored
|
|
159
169
|
on --workspace attach (daemon preserves stored value).
|
|
160
170
|
Default: cwd. Empty string = headless. Overrides
|
|
@@ -166,10 +176,11 @@ options:
|
|
|
166
176
|
--flags <json> raw LoopFlags JSON passthrough on every loop.run
|
|
167
177
|
(e.g. '{"auto":true}' for unattended
|
|
168
178
|
benchmark/automation runs).
|
|
169
|
-
--
|
|
170
|
-
decision
|
|
171
|
-
or an open question. Off by default
|
|
172
|
-
|
|
179
|
+
--request_user_input let the model ask you through the question tool when it
|
|
180
|
+
needs a decision (multiple choice with a free-response
|
|
181
|
+
escape, or an open question). Off by default for the
|
|
182
|
+
one-shot CLI; on by default for the TUI. Overrides
|
|
183
|
+
PLURNK_REQUEST_USER_INPUT.
|
|
173
184
|
--env-file <p> load env from <p> (errors if missing). Repeatable.
|
|
174
185
|
--env-file-if-exists <p> same, but silently skip a missing file. Repeatable.
|
|
175
186
|
--max-turns <n> per-loop turn cap (daemon default PLURNK_MAX_TURNS).
|
|
@@ -192,14 +203,19 @@ options:
|
|
|
192
203
|
--turn <id> (log read) filter to a single turn id
|
|
193
204
|
--since <id> (log read) return entries with id > <id>
|
|
194
205
|
--limit <n> (log read) max entries to return (default 100)
|
|
206
|
+
--provider <name> (models) restrict the catalog to one provider
|
|
207
|
+
--all (models) include unconfigured models with readiness causes
|
|
208
|
+
--offset <n> (models) catalog page offset (default 0)
|
|
195
209
|
|
|
196
210
|
subcommands:
|
|
197
|
-
models
|
|
211
|
+
models [search...] list the bounded daemon model catalog (models.list)
|
|
198
212
|
workspace list list workspaces on the daemon (workspace.list)
|
|
199
213
|
workspace workers <name> list workers in the named workspace (workspace.workers)
|
|
200
214
|
workspace rename <a> <b> rename workspace <a> to <b> (workspace.rename — a workspace's
|
|
201
215
|
name is a mutable handle; workers are immutable)
|
|
202
216
|
log read --workspace ... read log entries from the named workspace's worker
|
|
217
|
+
reasoning [policy] inspect or set a worker's durable reasoning policy
|
|
218
|
+
mcp ... list and manage MCP servers for --workspace
|
|
203
219
|
script <file.plk> run a .plk file: feed its DSL to op.parse, render the
|
|
204
220
|
trace, exit by worst op status. Honors --workspace/--yolo
|
|
205
221
|
/--project-root + membership flags. The daemon owns the
|
|
@@ -215,32 +231,45 @@ const dieJson = (code, problem) => {
|
|
|
215
231
|
process.stdout.write(`${JSON.stringify(buildJsonError(problem))}\n`);
|
|
216
232
|
process.exit(code);
|
|
217
233
|
};
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
234
|
+
export const orderedEnvFiles = (args) => {
|
|
235
|
+
const files = [];
|
|
236
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
237
|
+
const arg = args[index];
|
|
238
|
+
if (arg === "--")
|
|
239
|
+
break;
|
|
240
|
+
for (const [flag, required] of [["--env-file", true], ["--env-file-if-exists", false]]) {
|
|
241
|
+
if (arg === flag) {
|
|
242
|
+
const path = args[index + 1];
|
|
243
|
+
if (path !== undefined)
|
|
244
|
+
files.push({ path, required });
|
|
245
|
+
index += 1;
|
|
246
|
+
break;
|
|
247
|
+
}
|
|
248
|
+
if (arg.startsWith(`${flag}=`))
|
|
249
|
+
files.push({ path: arg.slice(flag.length + 1), required });
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
return files;
|
|
253
|
+
};
|
|
254
|
+
export const loadEnvCascade = (explicitFiles, userConfig = userConfigFile()) => {
|
|
226
255
|
const ifExists = (p) => { try {
|
|
227
256
|
process.loadEnvFile(p);
|
|
228
257
|
}
|
|
229
258
|
catch { /* optional layer */ } };
|
|
230
|
-
for (const
|
|
259
|
+
for (const { path, required } of explicitFiles.toReversed()) {
|
|
260
|
+
if (!required) {
|
|
261
|
+
ifExists(path);
|
|
262
|
+
continue;
|
|
263
|
+
}
|
|
231
264
|
try {
|
|
232
|
-
process.loadEnvFile(
|
|
265
|
+
process.loadEnvFile(path);
|
|
233
266
|
}
|
|
234
267
|
catch {
|
|
235
|
-
dieWith(64, clientFlagInvalid("--env-file",
|
|
268
|
+
dieWith(64, clientFlagInvalid("--env-file", path, "file not found"));
|
|
236
269
|
}
|
|
237
270
|
}
|
|
238
|
-
for (const f of envFilesIfExists)
|
|
239
|
-
ifExists(f);
|
|
240
271
|
ifExists(".env");
|
|
241
|
-
|
|
242
|
-
ifExists(join(home, ".env"));
|
|
243
|
-
ifExists(join(home, ".env.defaults"));
|
|
272
|
+
ifExists(userConfig);
|
|
244
273
|
loadFloor();
|
|
245
274
|
};
|
|
246
275
|
// Map repeatable membership flags to workspace constraints.
|
|
@@ -254,11 +283,6 @@ export const buildSettings = async (values, cwd, env = process.env) => {
|
|
|
254
283
|
const execs = collectExecsPolicy(env);
|
|
255
284
|
if (Object.keys(execs).length > 0)
|
|
256
285
|
settings.execs = execs;
|
|
257
|
-
// svc#346 — enable model→user SEND signal 300 questions (per-workspace; the daemon
|
|
258
|
-
// injects questions.md teaching + intersects its PLURNK_QUESTIONS ceiling).
|
|
259
|
-
// Flag or bare env (shared user intent). The daemon owns refusal when off.
|
|
260
|
-
if (values.questions === true || ["1", "true", "yes", "on"].includes((env.PLURNK_QUESTIONS ?? "").toLowerCase()))
|
|
261
|
-
settings.questions = true;
|
|
262
286
|
const mc = values["max-commands"];
|
|
263
287
|
if (mc !== undefined) {
|
|
264
288
|
const n = Number(mc);
|
|
@@ -277,25 +301,6 @@ export const buildSettings = async (values, cwd, env = process.env) => {
|
|
|
277
301
|
}
|
|
278
302
|
settings.filesItems = n;
|
|
279
303
|
}
|
|
280
|
-
const mdSpecs = values.md ?? [];
|
|
281
|
-
if (mdSpecs.length > 0) {
|
|
282
|
-
const mdDocs = [];
|
|
283
|
-
for (const spec of mdSpecs) {
|
|
284
|
-
const eq = spec.indexOf("=");
|
|
285
|
-
if (eq <= 0)
|
|
286
|
-
throw new ProblemError(clientFlagInvalid("--md", spec, "must be NAME=path"));
|
|
287
|
-
const alias = spec.slice(0, eq);
|
|
288
|
-
const raw = spec.slice(eq + 1);
|
|
289
|
-
const abs = isAbsolute(raw) ? raw : resolve(cwd, raw);
|
|
290
|
-
try {
|
|
291
|
-
mdDocs.push({ alias, content: await readFile(abs, "utf8") });
|
|
292
|
-
}
|
|
293
|
-
catch (cause) {
|
|
294
|
-
throw new ProblemError(clientFlagInvalid("--md", spec, `file not readable: ${cause instanceof Error ? cause.message : String(cause)}`));
|
|
295
|
-
}
|
|
296
|
-
}
|
|
297
|
-
settings.mdDocs = mdDocs;
|
|
298
|
-
}
|
|
299
304
|
return settings;
|
|
300
305
|
};
|
|
301
306
|
// svc#235: discover.versions { service:{installed, latest?}, client:{latest?} }.
|
|
@@ -359,10 +364,22 @@ const runSubcommand = async (rpc, positionals, opts) => {
|
|
|
359
364
|
const verb = positionals[0];
|
|
360
365
|
const sub = positionals[1];
|
|
361
366
|
if (verb === "models") {
|
|
362
|
-
|
|
363
|
-
|
|
367
|
+
const offset = parseIntFlag(opts.values.offset, "--offset");
|
|
368
|
+
const limit = parseIntFlag(opts.values.limit, "--limit");
|
|
369
|
+
if (limit !== undefined && (limit < 1 || limit > 100)) {
|
|
370
|
+
throw new ProblemError(clientFlagInvalid("--limit", String(limit), "must be between 1 and 100 for models"));
|
|
364
371
|
}
|
|
365
|
-
|
|
372
|
+
const search = positionals.slice(1).join(" ").trim();
|
|
373
|
+
return await runModels(rpc, {
|
|
374
|
+
json: opts.json,
|
|
375
|
+
query: {
|
|
376
|
+
...(typeof opts.values.provider === "string" ? { provider: opts.values.provider } : {}),
|
|
377
|
+
...(search.length > 0 ? { search } : {}),
|
|
378
|
+
...(opts.values.all === true ? { availability: "all" } : {}),
|
|
379
|
+
...(offset !== undefined ? { offset } : {}),
|
|
380
|
+
...(limit !== undefined ? { limit } : {}),
|
|
381
|
+
},
|
|
382
|
+
});
|
|
366
383
|
}
|
|
367
384
|
if (verb === "workspace") {
|
|
368
385
|
if (sub === "list") {
|
|
@@ -394,6 +411,32 @@ const runSubcommand = async (rpc, positionals, opts) => {
|
|
|
394
411
|
}
|
|
395
412
|
throw new ProblemError(clientSubcommandUnknownVerb(`workspace ${sub ?? "(missing)"}`, ["list", "workers", "rename"]));
|
|
396
413
|
}
|
|
414
|
+
if (verb === "mcp") {
|
|
415
|
+
if (opts.workspaceName === undefined) {
|
|
416
|
+
throw new ProblemError(clientFlagMissingDependency("plurnk mcp", "--workspace (or PLURNK_CLIENT_WORKSPACE)"));
|
|
417
|
+
}
|
|
418
|
+
const result = await handleMcp(positionals.slice(1), rpc, opts.json ? () => undefined : (text) => process.stdout.write(text), { overlay: opts.mcpConfiguration });
|
|
419
|
+
if (result === null)
|
|
420
|
+
return 64;
|
|
421
|
+
if (opts.json)
|
|
422
|
+
process.stdout.write(`${JSON.stringify(result)}\n`);
|
|
423
|
+
return 0;
|
|
424
|
+
}
|
|
425
|
+
if (verb === "reasoning") {
|
|
426
|
+
if (opts.workspaceName === undefined) {
|
|
427
|
+
throw new ProblemError(clientFlagMissingDependency("plurnk reasoning", "--workspace (or PLURNK_CLIENT_WORKSPACE)"));
|
|
428
|
+
}
|
|
429
|
+
if (positionals.length > 2) {
|
|
430
|
+
throw new ProblemError(clientSubcommandUnknownVerb(`reasoning ${positionals.slice(1).join(" ")}`));
|
|
431
|
+
}
|
|
432
|
+
const reasoning = sub === undefined
|
|
433
|
+
? await readWorkerReasoning(rpc)
|
|
434
|
+
: await setWorkerReasoning(rpc, sub);
|
|
435
|
+
process.stdout.write(opts.json
|
|
436
|
+
? `${JSON.stringify(reasoning)}\n`
|
|
437
|
+
: formatWorkerReasoning(reasoning));
|
|
438
|
+
return 0;
|
|
439
|
+
}
|
|
397
440
|
if (verb === "log") {
|
|
398
441
|
if (sub !== "read") {
|
|
399
442
|
throw new ProblemError(clientSubcommandUnknownVerb(`log ${sub ?? "(missing)"}`, ["read"]));
|
|
@@ -403,7 +446,8 @@ const runSubcommand = async (rpc, positionals, opts) => {
|
|
|
403
446
|
}
|
|
404
447
|
// The caller's threadId (--workspace) scopes the action to that workspace; the
|
|
405
448
|
// module defaults reads to the conversation (model worker); --worker pins by name.
|
|
406
|
-
const
|
|
449
|
+
const workerId = await resolveWorkerId(rpc, opts.workerName);
|
|
450
|
+
const filters = { ...(workerId === undefined ? {} : { workerId }) };
|
|
407
451
|
const loopId = parseIntFlag(opts.values.loop, "--loop");
|
|
408
452
|
const turnId = parseIntFlag(opts.values.turn, "--turn");
|
|
409
453
|
const sinceId = parseIntFlag(opts.values.since, "--since");
|
|
@@ -450,11 +494,12 @@ export const main = async (argv) => {
|
|
|
450
494
|
workspace: { type: "string" },
|
|
451
495
|
worker: { type: "string" },
|
|
452
496
|
model: { type: "string" },
|
|
497
|
+
reasoning: { type: "string" },
|
|
453
498
|
"project-root": { type: "string" },
|
|
454
499
|
yolo: { type: "boolean" },
|
|
455
500
|
auto: { type: "boolean" },
|
|
456
501
|
flags: { type: "string" },
|
|
457
|
-
|
|
502
|
+
"request-user-input": { type: "boolean" }, // --request_user_input: the worker may ask through the question tool
|
|
458
503
|
"max-turns": { type: "string" },
|
|
459
504
|
timeout: { type: "string" },
|
|
460
505
|
// membership overlay — repeatable globs; service vocabulary
|
|
@@ -463,7 +508,6 @@ export const main = async (argv) => {
|
|
|
463
508
|
view: { type: "string", multiple: true },
|
|
464
509
|
// workspace-open settings (svc#231) + tighten-only ceilings (svc#232)
|
|
465
510
|
"files-items": { type: "string" },
|
|
466
|
-
md: { type: "string", multiple: true },
|
|
467
511
|
"max-commands": { type: "string" },
|
|
468
512
|
"no-git": { type: "boolean" },
|
|
469
513
|
// log read filters
|
|
@@ -471,6 +515,9 @@ export const main = async (argv) => {
|
|
|
471
515
|
turn: { type: "string" },
|
|
472
516
|
since: { type: "string" },
|
|
473
517
|
limit: { type: "string" },
|
|
518
|
+
provider: { type: "string" },
|
|
519
|
+
all: { type: "boolean" },
|
|
520
|
+
offset: { type: "string" },
|
|
474
521
|
},
|
|
475
522
|
});
|
|
476
523
|
if (values.help) {
|
|
@@ -482,17 +529,17 @@ export const main = async (argv) => {
|
|
|
482
529
|
process.stdout.write(`${formatBuildInfo(buildInfo)}\n`);
|
|
483
530
|
process.exit(0);
|
|
484
531
|
}
|
|
485
|
-
// Shared
|
|
486
|
-
loadEnvCascade(
|
|
532
|
+
// Shared XDG user env cascade (after parse so --env-file flags participate).
|
|
533
|
+
loadEnvCascade(orderedEnvFiles(argv.slice(2)));
|
|
534
|
+
const mcpConfiguration = collectMcpConfiguration(process.env);
|
|
487
535
|
// json OUTPUT MODE — flag or env (user-level, same name client+daemon would
|
|
488
536
|
// read). One complete document on stdout, stderr silent, structured errors.
|
|
489
537
|
const json = values.json === true || ["1", "true", "yes", "on"].includes((process.env.PLURNK_CLIENT_JSON ?? "").toLowerCase());
|
|
490
538
|
if (!json)
|
|
491
539
|
process.stderr.write(`plurnk: ${formatBuildInfo(buildInfo)}\n`);
|
|
492
|
-
//
|
|
493
|
-
//
|
|
494
|
-
|
|
495
|
-
const SUBCOMMANDS = ["models", "workspace", "log", "read", "script"];
|
|
540
|
+
// State-command routing happens BEFORE prompt assembly, so inspection and
|
|
541
|
+
// deliberate configuration never consume stdin or become model prompts.
|
|
542
|
+
const SUBCOMMANDS = ["models", "workspace", "log", "read", "script", "mcp", "reasoning"];
|
|
496
543
|
const subcommand = positionals[0];
|
|
497
544
|
const isSubcommand = subcommand !== undefined && SUBCOMMANDS.includes(subcommand);
|
|
498
545
|
// Assemble the prompt only if we're NOT running a subcommand.
|
|
@@ -510,11 +557,16 @@ export const main = async (argv) => {
|
|
|
510
557
|
// PLURNK_CLIENT_JSON with no prompt is the interactive TUI — env shouldn't force CLI mode.
|
|
511
558
|
}
|
|
512
559
|
}
|
|
513
|
-
//
|
|
560
|
+
// Client flags select client behavior. Provider defaults remain daemon-owned.
|
|
514
561
|
const workspaceName = values.workspace ?? process.env.PLURNK_CLIENT_WORKSPACE;
|
|
515
562
|
const workerName = values.worker ?? process.env.PLURNK_CLIENT_WORKER;
|
|
516
|
-
const
|
|
517
|
-
const
|
|
563
|
+
const modelSelector = values.model;
|
|
564
|
+
const reasoningPolicy = values.reasoning;
|
|
565
|
+
// {§worker-settings} — the worker's request-user-input rule: TUI/nvim default on,
|
|
566
|
+
// the one-shot CLI defaults off; the explicit flag always wins, then the env.
|
|
567
|
+
const requestUserInputEnv = ["1", "true", "yes", "on"].includes((process.env.PLURNK_REQUEST_USER_INPUT ?? "").toLowerCase());
|
|
568
|
+
const requestUserInputCli = values["request-user-input"] === true || (values["request-user-input"] === undefined && requestUserInputEnv);
|
|
569
|
+
const requestUserInputTui = values["request-user-input"] ?? (requestUserInputEnv || true);
|
|
518
570
|
const yolo = values.yolo === true || ["1", "true", "yes", "on"].includes((process.env.PLURNK_CLIENT_YOLO ?? "").toLowerCase());
|
|
519
571
|
if (workerName !== undefined && workspaceName === undefined) {
|
|
520
572
|
dieWith(64, clientFlagMissingDependency("--worker (or PLURNK_CLIENT_WORKER)", "--workspace (or PLURNK_CLIENT_WORKSPACE)"));
|
|
@@ -535,9 +587,10 @@ export const main = async (argv) => {
|
|
|
535
587
|
dieWith(cause.exitCode, cause.problem);
|
|
536
588
|
dieWith(64, clientRuntimeError(cause));
|
|
537
589
|
}
|
|
538
|
-
// --
|
|
539
|
-
//
|
|
540
|
-
//
|
|
590
|
+
// --request_user_input / PLURNK_REQUEST_USER_INPUT is the worker's own
|
|
591
|
+
// behavioral rule ({§worker-settings}): it rides the run's forwardedProps, the
|
|
592
|
+
// AG-UI thread binding persists it on the conversation worker, and it stays
|
|
593
|
+
// flippable between loops via worker.settings.set.
|
|
541
594
|
const projectRootRaw = values["project-root"] ?? process.env.PLURNK_CLIENT_PROJECT_ROOT;
|
|
542
595
|
const projectRoot = (() => {
|
|
543
596
|
try {
|
|
@@ -592,7 +645,16 @@ export const main = async (argv) => {
|
|
|
592
645
|
// thread == world (the model worker).
|
|
593
646
|
const w = await world();
|
|
594
647
|
const { constraints, settings } = await workspaceOptions();
|
|
595
|
-
|
|
648
|
+
// {§worker-model-selection} — an explicit --model is a durable selection:
|
|
649
|
+
// persist it onto the conversation worker before the run, then run WITHOUT
|
|
650
|
+
// a per-loop model selector (the worker owns the model).
|
|
651
|
+
if (values.model !== undefined && modelSelector !== undefined) {
|
|
652
|
+
await actionViaBridge({ bridgeUrl, token: process.env.PLURNK_AGUI_TOKEN }, { threadId: workerName ?? w, workspace: w, kind: "worker.model.set", params: { selector: modelSelector } });
|
|
653
|
+
}
|
|
654
|
+
if (reasoningPolicy !== undefined) {
|
|
655
|
+
await actionViaBridge({ bridgeUrl, token: process.env.PLURNK_AGUI_TOKEN }, { threadId: workerName ?? w, workspace: w, kind: "worker.reasoning.set", params: { policy: reasoningPolicy } });
|
|
656
|
+
}
|
|
657
|
+
const code = await runCliViaBridge({ bridgeUrl, token: process.env.PLURNK_AGUI_TOKEN }, prompt, { threadId: workerName ?? w, workspace: w, requestUserInput: requestUserInputCli, ...(loopFlags !== undefined ? { flags: loopFlags } : {}), ...(maxTurns !== undefined ? { maxTurns } : {}), ...(timeoutSec !== undefined ? { timeoutSec } : {}), yolo, json, projectRoot, constraints, settings });
|
|
596
658
|
// Let Node drain stdout before termination. A forced exit truncated large
|
|
597
659
|
// --json records mid-string when notices made the pipe exceed its buffer.
|
|
598
660
|
process.exitCode = code;
|
|
@@ -624,7 +686,7 @@ export const main = async (argv) => {
|
|
|
624
686
|
// TUI through the bridge (no prompt): skip the WS connect + workspace.create — a
|
|
625
687
|
// pure-bridge client has no direct daemon WS. The bridge owns the workspace; we
|
|
626
688
|
// pass a threadId-named stub (the daemon workspace id is bridge-created). projectRoot
|
|
627
|
-
// rides forwardedProps
|
|
689
|
+
// rides forwardedProps.
|
|
628
690
|
// (Per-workspace constraints/settings over the bridge are a follow-up.)
|
|
629
691
|
if (bridgeUrl !== undefined && bridgeUrl.length > 0 && !isSubcommand && subcommand !== "script" && prompt.length === 0) {
|
|
630
692
|
const w = await world();
|
|
@@ -641,15 +703,45 @@ export const main = async (argv) => {
|
|
|
641
703
|
constraints,
|
|
642
704
|
settings,
|
|
643
705
|
});
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
706
|
+
try {
|
|
707
|
+
await runTui(transport, { id: 0, name: w }, {
|
|
708
|
+
modelSelector,
|
|
709
|
+
modelExplicit: values.model !== undefined,
|
|
710
|
+
reasoningPolicy,
|
|
711
|
+
reasoningExplicit: reasoningPolicy !== undefined,
|
|
712
|
+
requestUserInput: requestUserInputTui,
|
|
713
|
+
yolo,
|
|
714
|
+
loopFlags,
|
|
715
|
+
maxTurns,
|
|
716
|
+
projectRoot,
|
|
717
|
+
workerName,
|
|
718
|
+
client: CLIENT_ID_TUI,
|
|
719
|
+
mcpConfiguration,
|
|
720
|
+
});
|
|
721
|
+
process.exitCode = 0;
|
|
722
|
+
return;
|
|
723
|
+
}
|
|
724
|
+
catch (cause) {
|
|
725
|
+
transport.shutdown();
|
|
726
|
+
if (cause instanceof ProblemError)
|
|
727
|
+
dieWith(cause.exitCode, cause.problem);
|
|
728
|
+
if (isUnreachable(cause))
|
|
729
|
+
dieWith(1, clientConnectionRefused(bridgeUrl, cause));
|
|
730
|
+
dieWith(1, clientRuntimeError(cause));
|
|
731
|
+
}
|
|
647
732
|
}
|
|
648
733
|
// AG-UI+ is the ONLY wire (the WS transport is deleted). Subcommands + script
|
|
649
734
|
// speak the action surface through a structural Caller.
|
|
650
735
|
const target = { bridgeUrl, token: process.env.PLURNK_AGUI_TOKEN };
|
|
651
|
-
const callerThread = workspaceName ?? "cli";
|
|
652
|
-
const caller = {
|
|
736
|
+
const callerThread = workerName ?? workspaceName ?? "cli";
|
|
737
|
+
const caller = {
|
|
738
|
+
call: (method, params) => actionViaBridge(target, {
|
|
739
|
+
threadId: callerThread,
|
|
740
|
+
...(workspaceName !== undefined ? { workspace: workspaceName } : {}),
|
|
741
|
+
kind: method,
|
|
742
|
+
params,
|
|
743
|
+
}),
|
|
744
|
+
};
|
|
653
745
|
try {
|
|
654
746
|
// `plurnk script foo.plk` — feed a .plk file to op.parse over the action
|
|
655
747
|
// surface. The client never parses the file; the module owns the grammar.
|
|
@@ -668,7 +760,7 @@ export const main = async (argv) => {
|
|
|
668
760
|
}
|
|
669
761
|
if (isSubcommand) {
|
|
670
762
|
const exitCode = await runSubcommand(caller, positionals, {
|
|
671
|
-
json, workspaceName, workerName, projectRoot, values,
|
|
763
|
+
json, workspaceName, workerName, projectRoot, values, mcpConfiguration,
|
|
672
764
|
});
|
|
673
765
|
process.exitCode = exitCode;
|
|
674
766
|
return;
|