@edspencer/paddock 0.56.0 → 0.58.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/package.json +1 -1
- package/packages/server/dist/cli/import-chats.js +6 -1
- package/packages/server/dist/cli/paddock.d.ts +29 -1
- package/packages/server/dist/cli/paddock.js +136 -44
- package/packages/server/dist/config.d.ts +26 -0
- package/packages/server/dist/config.js +32 -0
- package/packages/server/dist/environment-prompt.d.ts +52 -0
- package/packages/server/dist/environment-prompt.js +61 -0
- package/packages/server/dist/herdctl.d.ts +51 -0
- package/packages/server/dist/herdctl.js +61 -0
- package/packages/server/dist/instance-config.d.ts +13 -5
- package/packages/server/dist/instance-config.js +40 -0
- package/packages/server/package.json +1 -1
- package/packages/web/dist/assets/InstanceConfigPage-BKE6j_Wy.js +2 -0
- package/packages/web/dist/assets/{MarkdownRenderer-CJZFLj_8.js → MarkdownRenderer-B7_FErIN.js} +1 -1
- package/packages/web/dist/assets/{ProjectRedirect-BHwVbbqB.js → ProjectRedirect-DOaB53YM.js} +1 -1
- package/packages/web/dist/assets/{ProjectView-BOvuAITg.js → ProjectView-Dz_QUyPG.js} +3 -3
- package/packages/web/dist/assets/{ProjectsGrid-BCojZOGY.js → ProjectsGrid-CjfDQ37q.js} +1 -1
- package/packages/web/dist/assets/{arc-BVyxvnkl.js → arc-BxAGgBjd.js} +1 -1
- package/packages/web/dist/assets/{architectureDiagram-3BPJPVTR-Cd9Puqg1.js → architectureDiagram-3BPJPVTR-CBz2e-c3.js} +1 -1
- package/packages/web/dist/assets/{blockDiagram-GPEHLZMM-B_AQt_Rx.js → blockDiagram-GPEHLZMM-ZqClBxB7.js} +1 -1
- package/packages/web/dist/assets/{c4Diagram-AAUBKEIU-7e0TQtXB.js → c4Diagram-AAUBKEIU-CvFOBN1E.js} +1 -1
- package/packages/web/dist/assets/channel-BHzPWerJ.js +2 -0
- package/packages/web/dist/assets/{chunk-2J33WTMH-Dz4WZmUZ.js → chunk-2J33WTMH-C44u7-Xg.js} +1 -1
- package/packages/web/dist/assets/{chunk-4BX2VUAB-DVWYldo6.js → chunk-4BX2VUAB-DXG9UM3Z.js} +1 -1
- package/packages/web/dist/assets/{chunk-55IACEB6-BIMrizQd.js → chunk-55IACEB6-BLkJFttb.js} +1 -1
- package/packages/web/dist/assets/{chunk-727SXJPM-CL2q9fF1.js → chunk-727SXJPM-DZidMKHZ.js} +1 -1
- package/packages/web/dist/assets/{chunk-AQP2D5EJ-BSq3HlQ9.js → chunk-AQP2D5EJ-DVowfDYg.js} +1 -1
- package/packages/web/dist/assets/{chunk-FMBD7UC4-Bgt64pLE.js → chunk-FMBD7UC4-zeHzmwLI.js} +1 -1
- package/packages/web/dist/assets/{chunk-ND2GUHAM-C8Pr2A38.js → chunk-ND2GUHAM-Pcd02Wpz.js} +1 -1
- package/packages/web/dist/assets/{chunk-QZHKN3VN-BAOqh-MN.js → chunk-QZHKN3VN-yxlVeu8W.js} +1 -1
- package/packages/web/dist/assets/classDiagram-4FO5ZUOK-hNchnAu0.js +2 -0
- package/packages/web/dist/assets/classDiagram-v2-Q7XG4LA2-hNchnAu0.js +2 -0
- package/packages/web/dist/assets/{core-lJeGACQg.js → core-CSvc61jo.js} +1 -1
- package/packages/web/dist/assets/{cose-bilkent-S5V4N54A-BF4tameA.js → cose-bilkent-S5V4N54A-6MJqrbdr.js} +1 -1
- package/packages/web/dist/assets/{dagre-BM42HDAG-DdsLM6ql.js → dagre-BM42HDAG-B3pDB8_y.js} +1 -1
- package/packages/web/dist/assets/{diagram-2AECGRRQ-BQ78S_RM.js → diagram-2AECGRRQ-BJA5EERJ.js} +1 -1
- package/packages/web/dist/assets/{diagram-5GNKFQAL-CAKE6nHE.js → diagram-5GNKFQAL-CcyfxhVS.js} +1 -1
- package/packages/web/dist/assets/{diagram-KO2AKTUF-fEvHRXgr.js → diagram-KO2AKTUF-CPNyNU1j.js} +1 -1
- package/packages/web/dist/assets/{diagram-LMA3HP47-CtWsgeSm.js → diagram-LMA3HP47-Q4klJqX8.js} +1 -1
- package/packages/web/dist/assets/{diagram-OG6HWLK6-Buog8k7_.js → diagram-OG6HWLK6-D0ZBTRoA.js} +1 -1
- package/packages/web/dist/assets/{erDiagram-TEJ5UH35-CWvVVPZS.js → erDiagram-TEJ5UH35-DQrCT17u.js} +1 -1
- package/packages/web/dist/assets/{flowDiagram-I6XJVG4X-DsCGXtp4.js → flowDiagram-I6XJVG4X-CK7nM7wz.js} +1 -1
- package/packages/web/dist/assets/{format-DamJQ9LO.js → format-GJRFyZIp.js} +1 -1
- package/packages/web/dist/assets/{ganttDiagram-6RSMTGT7-CsdwIvvC.js → ganttDiagram-6RSMTGT7-CFsNGWGG.js} +1 -1
- package/packages/web/dist/assets/{gitGraphDiagram-PVQCEYII-hZvrX1sD.js → gitGraphDiagram-PVQCEYII-xP0bbv60.js} +1 -1
- package/packages/web/dist/assets/{index-CGbZdrm8.js → index-CDiCRIso.js} +2 -2
- package/packages/web/dist/assets/index-CvGiQddO.css +1 -0
- package/packages/web/dist/assets/{infoDiagram-5YYISTIA-nE_Vtz_l.js → infoDiagram-5YYISTIA-BaKZDvMw.js} +1 -1
- package/packages/web/dist/assets/{ishikawaDiagram-YF4QCWOH-2Nlqs0oH.js → ishikawaDiagram-YF4QCWOH-Dr0LK4iy.js} +1 -1
- package/packages/web/dist/assets/{journeyDiagram-JHISSGLW-mjU1fMc4.js → journeyDiagram-JHISSGLW-CxusdThR.js} +1 -1
- package/packages/web/dist/assets/{kanban-definition-UN3LZRKU-C_9xNaa3.js → kanban-definition-UN3LZRKU-CJYlFeaB.js} +1 -1
- package/packages/web/dist/assets/{linear-BxKd91ho.js → linear-D0XMlvYu.js} +1 -1
- package/packages/web/dist/assets/{mermaid.core-C-CuhD8I.js → mermaid.core-CcPpo_y5.js} +4 -4
- package/packages/web/dist/assets/{mindmap-definition-RKZ34NQL-Bs6yfcZv.js → mindmap-definition-RKZ34NQL-w064Pb6X.js} +1 -1
- package/packages/web/dist/assets/{pieDiagram-4H26LBE5-DNdqX7O5.js → pieDiagram-4H26LBE5-D_qwlox1.js} +1 -1
- package/packages/web/dist/assets/{quadrantDiagram-W4KKPZXB-ZMJl9OdZ.js → quadrantDiagram-W4KKPZXB-BuMUhsEZ.js} +1 -1
- package/packages/web/dist/assets/{requirementDiagram-4Y6WPE33-RmxMdm2q.js → requirementDiagram-4Y6WPE33-Bp1BqfXY.js} +1 -1
- package/packages/web/dist/assets/{sankeyDiagram-5OEKKPKP-OAAhWcfK.js → sankeyDiagram-5OEKKPKP-BQR4HfjD.js} +1 -1
- package/packages/web/dist/assets/{sequenceDiagram-3UESZ5HK-BWF7rCyN.js → sequenceDiagram-3UESZ5HK-DIAHV11i.js} +1 -1
- package/packages/web/dist/assets/{stateDiagram-AJRCARHV-M3b_zqqo.js → stateDiagram-AJRCARHV-BHMJl8rE.js} +1 -1
- package/packages/web/dist/assets/stateDiagram-v2-BHNVJYJU-CScn8fuQ.js +2 -0
- package/packages/web/dist/assets/{timeline-definition-PNZ67QCA-BpOCh63N.js → timeline-definition-PNZ67QCA-CB5sHevP.js} +1 -1
- package/packages/web/dist/assets/{vennDiagram-CIIHVFJN-D_iPH45Q.js → vennDiagram-CIIHVFJN-BeS1AXva.js} +1 -1
- package/packages/web/dist/assets/{wardley-L42UT6IY-DVD9I2Om.js → wardley-L42UT6IY-CJ9C5W6N.js} +1 -1
- package/packages/web/dist/assets/{wardleyDiagram-YWT4CUSO-Bn1IKwjn.js → wardleyDiagram-YWT4CUSO-Bf75XvJ_.js} +1 -1
- package/packages/web/dist/assets/{xychartDiagram-2RQKCTM6-DwtI0bkR.js → xychartDiagram-2RQKCTM6-0Qiya8rY.js} +1 -1
- package/packages/web/dist/index.html +2 -2
- package/packages/web/dist/sw.js +1 -1
- package/packages/web/dist/assets/InstanceConfigPage-4cQGTj9Y.js +0 -2
- package/packages/web/dist/assets/channel-C_1aISNn.js +0 -2
- package/packages/web/dist/assets/classDiagram-4FO5ZUOK-B8JE7cHJ.js +0 -2
- package/packages/web/dist/assets/classDiagram-v2-Q7XG4LA2-B8JE7cHJ.js +0 -2
- package/packages/web/dist/assets/index-DJDQ210-.css +0 -1
- package/packages/web/dist/assets/stateDiagram-v2-BHNVJYJU-CI7saFDa.js +0 -2
package/package.json
CHANGED
|
@@ -37,6 +37,7 @@
|
|
|
37
37
|
* Applied to the environment BEFORE the config is read.
|
|
38
38
|
* --json Emit the raw result as JSON instead of prose.
|
|
39
39
|
*/
|
|
40
|
+
import { pathToFileURL } from "node:url";
|
|
40
41
|
import { loadPaddockConfig } from "../config.js";
|
|
41
42
|
import { ProjectStore, ROOT_KEY } from "../projects.js";
|
|
42
43
|
import { HerdctlService } from "../herdctl.js";
|
|
@@ -187,7 +188,11 @@ async function main() {
|
|
|
187
188
|
}
|
|
188
189
|
}
|
|
189
190
|
// Only run when invoked directly, so the arg parser stays unit-testable.
|
|
190
|
-
|
|
191
|
+
// `pathToFileURL`, not `"file://" + argv[1]`: the naive concatenation leaves any
|
|
192
|
+
// space or non-ASCII character in the path un-encoded, so the comparison fails
|
|
193
|
+
// and the script silently does nothing when run from such a directory — the same
|
|
194
|
+
// percent-encoding trap as #636.
|
|
195
|
+
if (process.argv[1] !== undefined && pathToFileURL(process.argv[1]).href === import.meta.url) {
|
|
191
196
|
main()
|
|
192
197
|
.then((code) => process.exit(code))
|
|
193
198
|
.catch((err) => {
|
|
@@ -1,2 +1,30 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
export {
|
|
2
|
+
export interface CliOptions {
|
|
3
|
+
port?: string;
|
|
4
|
+
host?: string;
|
|
5
|
+
dataDir?: string;
|
|
6
|
+
open: boolean;
|
|
7
|
+
verbose: boolean;
|
|
8
|
+
help: boolean;
|
|
9
|
+
version: boolean;
|
|
10
|
+
}
|
|
11
|
+
/** A usage error. Thrown rather than exiting, so `parseArgs` stays testable. */
|
|
12
|
+
export declare class CliError extends Error {
|
|
13
|
+
}
|
|
14
|
+
export declare function parseArgs(argv: string[]): CliOptions;
|
|
15
|
+
/**
|
|
16
|
+
* Why this Node is unusable, or `undefined` if it is fine.
|
|
17
|
+
*
|
|
18
|
+
* Split from the exit so it can be tested. `engines` only warns by default, and
|
|
19
|
+
* the failure it eventually produces is an unexplained syntax error deep inside
|
|
20
|
+
* a dependency — which is nobody's idea of a first-run experience.
|
|
21
|
+
*/
|
|
22
|
+
export declare function nodeVersionProblem(nodeVersion: string): string | undefined;
|
|
23
|
+
/**
|
|
24
|
+
* Translate a listen failure into something a human can act on.
|
|
25
|
+
*
|
|
26
|
+
* `EADDRINUSE` on 4000 is the single most likely first-run failure — it is a
|
|
27
|
+
* popular port and Paddock's own default — and Node's raw error names neither
|
|
28
|
+
* the port nor the flag that fixes it.
|
|
29
|
+
*/
|
|
30
|
+
export declare function explainListenError(err: unknown, host: string, port: string): string;
|
|
@@ -1,34 +1,28 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* `paddock` — the npx / global-install entrypoint (#
|
|
3
|
+
* `paddock` — the npx / global-install entrypoint (#637, #638).
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* uses.
|
|
5
|
+
* The user-facing front door for `npx @edspencer/paddock`: someone who has never
|
|
6
|
+
* seen this repo runs one command and gets a working instance on localhost.
|
|
7
|
+
* Everything here is preflight, defaults and presentation — the server itself is
|
|
8
|
+
* untouched, and `start()` is the same lifecycle `node dist/index.js` uses.
|
|
10
9
|
*
|
|
11
|
-
* The design constraint that shapes this file: the server is already
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
10
|
+
* The design constraint that shapes this file: the server is already *tolerant*
|
|
11
|
+
* — every setting has a default, an empty data dir self-initializes, and auth
|
|
12
|
+
* defaults to `none` on a loopback bind. So a bare `start()` would in fact boot.
|
|
13
|
+
* What it would NOT do is tell a first-time user why their chats fail, where
|
|
14
|
+
* their data went, or which of the several hundred lines of JSON that just
|
|
15
|
+
* scrolled past contained the URL.
|
|
16
16
|
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
* relative to cwd and would litter whatever directory you happened to run
|
|
21
|
-
* npx from (and silently give you a different instance next time).
|
|
22
|
-
* 3. `claude` on PATH — prepended from our own node_modules/.bin. Chats do not
|
|
23
|
-
* need it (the SDK runtime resolves its own bundled binary), but the
|
|
24
|
-
* sweeper and triggers shell out to `claude` by name.
|
|
25
|
-
* 4. Credentials — a warning, never a hard failure: we cannot reliably detect
|
|
26
|
-
* a macOS Keychain login, so refusing to start would lock out valid users.
|
|
17
|
+
* Everything below the arg parser is side-effecting by nature, so the pure parts
|
|
18
|
+
* (`parseArgs`, `nodeVersionProblem`, `explainListenError`) are exported and
|
|
19
|
+
* unit-tested; the rest is exercised by running the built binary.
|
|
27
20
|
*/
|
|
28
21
|
import fs from "node:fs";
|
|
29
22
|
import os from "node:os";
|
|
30
23
|
import path from "node:path";
|
|
31
|
-
import {
|
|
24
|
+
import { spawn } from "node:child_process";
|
|
25
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
32
26
|
const MIN_NODE_MAJOR = 22;
|
|
33
27
|
/** This module's own directory: `<…>/packages/server/dist/cli`. */
|
|
34
28
|
const moduleDir = path.dirname(fileURLToPath(import.meta.url));
|
|
@@ -43,8 +37,7 @@ const moduleDir = path.dirname(fileURLToPath(import.meta.url));
|
|
|
43
37
|
* deps at `<pkg>/node_modules` only — two levels FURTHER up
|
|
44
38
|
*
|
|
45
39
|
* A fixed `../..` hop is right for the repo and wrong for the published tarball,
|
|
46
|
-
* where it lands on `packages/server` and finds no `node_modules` at all.
|
|
47
|
-
* up is correct for both without branching on which one we are in.
|
|
40
|
+
* where it lands on `packages/server` and finds no `node_modules` at all.
|
|
48
41
|
*/
|
|
49
42
|
function findUp(rel) {
|
|
50
43
|
let dir = moduleDir;
|
|
@@ -59,14 +52,17 @@ function findUp(rel) {
|
|
|
59
52
|
}
|
|
60
53
|
/** Nearest ancestor holding a `package.json` — `packages/server` in both layouts. */
|
|
61
54
|
const packageRoot = findUp("package.json") ?? path.resolve(moduleDir, "../..");
|
|
62
|
-
|
|
63
|
-
|
|
55
|
+
/** A usage error. Thrown rather than exiting, so `parseArgs` stays testable. */
|
|
56
|
+
export class CliError extends Error {
|
|
57
|
+
}
|
|
58
|
+
export function parseArgs(argv) {
|
|
59
|
+
const opts = { open: false, verbose: false, help: false, version: false };
|
|
64
60
|
for (let i = 0; i < argv.length; i++) {
|
|
65
61
|
const arg = argv[i];
|
|
66
62
|
const next = () => {
|
|
67
63
|
const v = argv[++i];
|
|
68
64
|
if (v === undefined)
|
|
69
|
-
|
|
65
|
+
throw new CliError(`${arg} needs a value`);
|
|
70
66
|
return v;
|
|
71
67
|
};
|
|
72
68
|
switch (arg) {
|
|
@@ -81,6 +77,13 @@ function parseArgs(argv) {
|
|
|
81
77
|
case "--data-dir":
|
|
82
78
|
opts.dataDir = next();
|
|
83
79
|
break;
|
|
80
|
+
case "-o":
|
|
81
|
+
case "--open":
|
|
82
|
+
opts.open = true;
|
|
83
|
+
break;
|
|
84
|
+
case "--verbose":
|
|
85
|
+
opts.verbose = true;
|
|
86
|
+
break;
|
|
84
87
|
case "-h":
|
|
85
88
|
case "--help":
|
|
86
89
|
opts.help = true;
|
|
@@ -90,7 +93,7 @@ function parseArgs(argv) {
|
|
|
90
93
|
opts.version = true;
|
|
91
94
|
break;
|
|
92
95
|
default:
|
|
93
|
-
|
|
96
|
+
throw new CliError(`unknown option: ${arg}\nRun \`paddock --help\` for usage.`);
|
|
94
97
|
}
|
|
95
98
|
}
|
|
96
99
|
return opts;
|
|
@@ -117,6 +120,8 @@ Options
|
|
|
117
120
|
-p, --port <port> HTTP/WS port (default 4000, or $PORT)
|
|
118
121
|
--host <host> Bind address (default 127.0.0.1)
|
|
119
122
|
-d, --data-dir <path> Projects + state (default ~/.paddock, or $PADDOCK_DATA_DIR)
|
|
123
|
+
-o, --open Open the app in your browser once it is listening
|
|
124
|
+
--verbose Show the server's own logs (quiet by default)
|
|
120
125
|
-v, --version Print the Paddock version and exit
|
|
121
126
|
-h, --help Show this help
|
|
122
127
|
|
|
@@ -126,20 +131,31 @@ Credentials
|
|
|
126
131
|
billing). If you already use Claude Code on this machine, an existing login
|
|
127
132
|
under ~/.claude is picked up automatically. Otherwise run \`claude setup-token\`.
|
|
128
133
|
|
|
134
|
+
Your data
|
|
135
|
+
Everything lives in one directory — ~/.paddock unless you pass --data-dir.
|
|
136
|
+
Projects, chat transcripts and settings all persist there between runs. Move
|
|
137
|
+
that directory to move your instance; delete it to start over. Nothing is
|
|
138
|
+
stored anywhere else.
|
|
139
|
+
|
|
129
140
|
Notes
|
|
130
141
|
Binds loopback with authentication disabled, which is safe for a laptop. To
|
|
131
142
|
expose it on a network, set PADDOCK_AUTH_MODE first — Paddock refuses to bind
|
|
132
143
|
a routable interface wide open. See AUTH.md.
|
|
133
144
|
|
|
134
145
|
Docs: https://github.com/edspencer/paddock`;
|
|
135
|
-
/**
|
|
136
|
-
*
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
146
|
+
/**
|
|
147
|
+
* Why this Node is unusable, or `undefined` if it is fine.
|
|
148
|
+
*
|
|
149
|
+
* Split from the exit so it can be tested. `engines` only warns by default, and
|
|
150
|
+
* the failure it eventually produces is an unexplained syntax error deep inside
|
|
151
|
+
* a dependency — which is nobody's idea of a first-run experience.
|
|
152
|
+
*/
|
|
153
|
+
export function nodeVersionProblem(nodeVersion) {
|
|
154
|
+
const major = Number(nodeVersion.split(".")[0]);
|
|
155
|
+
if (!Number.isFinite(major) || major >= MIN_NODE_MAJOR)
|
|
156
|
+
return undefined;
|
|
157
|
+
return (`Node ${nodeVersion} is too old — Paddock needs Node ${MIN_NODE_MAJOR}+.\n` +
|
|
158
|
+
`Upgrade Node, then re-run. (nodejs.org, or \`nvm install ${MIN_NODE_MAJOR}\`)`);
|
|
143
159
|
}
|
|
144
160
|
/**
|
|
145
161
|
* Put our bundled `claude` on PATH for child processes.
|
|
@@ -209,8 +225,54 @@ function noteFirstRun(dataDir) {
|
|
|
209
225
|
"",
|
|
210
226
|
].join("\n"));
|
|
211
227
|
}
|
|
228
|
+
/** Open `url` in the default browser. Best-effort: failing is never fatal. */
|
|
229
|
+
function openBrowser(url) {
|
|
230
|
+
const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
|
|
231
|
+
try {
|
|
232
|
+
const child = spawn(cmd, [url], {
|
|
233
|
+
stdio: "ignore",
|
|
234
|
+
detached: true,
|
|
235
|
+
shell: process.platform === "win32", // `start` is a cmd builtin, not a binary
|
|
236
|
+
});
|
|
237
|
+
// A headless box has no xdg-open. The URL is printed either way, so an
|
|
238
|
+
// unopenable browser is a non-event rather than a failed run.
|
|
239
|
+
child.on("error", () => { });
|
|
240
|
+
child.unref();
|
|
241
|
+
}
|
|
242
|
+
catch {
|
|
243
|
+
/* as above */
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* Translate a listen failure into something a human can act on.
|
|
248
|
+
*
|
|
249
|
+
* `EADDRINUSE` on 4000 is the single most likely first-run failure — it is a
|
|
250
|
+
* popular port and Paddock's own default — and Node's raw error names neither
|
|
251
|
+
* the port nor the flag that fixes it.
|
|
252
|
+
*/
|
|
253
|
+
export function explainListenError(err, host, port) {
|
|
254
|
+
const code = err?.code;
|
|
255
|
+
if (code === "EADDRINUSE") {
|
|
256
|
+
return (`port ${port} is already in use.\n` +
|
|
257
|
+
`Something else is listening on ${host}:${port} — possibly another Paddock.\n` +
|
|
258
|
+
`Pick a different port: paddock --port ${Number(port) + 1}`);
|
|
259
|
+
}
|
|
260
|
+
if (code === "EACCES") {
|
|
261
|
+
return (`not allowed to bind ${host}:${port}.\n` +
|
|
262
|
+
`Ports below 1024 need elevated privileges — use a higher one: paddock --port 4000`);
|
|
263
|
+
}
|
|
264
|
+
return String(err?.message ?? err);
|
|
265
|
+
}
|
|
212
266
|
async function main() {
|
|
213
|
-
|
|
267
|
+
let opts;
|
|
268
|
+
try {
|
|
269
|
+
opts = parseArgs(process.argv.slice(2));
|
|
270
|
+
}
|
|
271
|
+
catch (err) {
|
|
272
|
+
if (err instanceof CliError)
|
|
273
|
+
fail(err.message);
|
|
274
|
+
throw err;
|
|
275
|
+
}
|
|
214
276
|
if (opts.help) {
|
|
215
277
|
console.log(USAGE);
|
|
216
278
|
return;
|
|
@@ -219,7 +281,9 @@ async function main() {
|
|
|
219
281
|
console.log(readVersion());
|
|
220
282
|
return;
|
|
221
283
|
}
|
|
222
|
-
|
|
284
|
+
const problem = nodeVersionProblem(process.versions.node);
|
|
285
|
+
if (problem !== undefined)
|
|
286
|
+
fail(problem);
|
|
223
287
|
addBundledBinsToPath();
|
|
224
288
|
// Apply CLI defaults as env vars. This is the whole integration surface: the
|
|
225
289
|
// server resolves config inside `buildApp()`, so anything set before the
|
|
@@ -232,16 +296,44 @@ async function main() {
|
|
|
232
296
|
process.env.PORT = opts.port;
|
|
233
297
|
if (opts.host)
|
|
234
298
|
process.env.HOST = opts.host;
|
|
299
|
+
// Quiet by default. The server emits a few hundred lines on the way up, which
|
|
300
|
+
// scrolls the one line the user actually needs — the URL — off the top of the
|
|
301
|
+
// terminal. TWO loggers have to be told, and they are unrelated:
|
|
302
|
+
// LOG_LEVEL Paddock's own pino logger (config.ts).
|
|
303
|
+
// HERDCTL_LOG_LEVEL @herdctl/core's `createLogger`, which writes the
|
|
304
|
+
// `[fleet-manager] …` lines via console.info — pino's
|
|
305
|
+
// level cannot reach those.
|
|
306
|
+
// An explicit value for either is left alone, and `--verbose` skips both.
|
|
307
|
+
if (!opts.verbose) {
|
|
308
|
+
if (process.env.LOG_LEVEL === undefined)
|
|
309
|
+
process.env.LOG_LEVEL = "warn";
|
|
310
|
+
if (process.env.HERDCTL_LOG_LEVEL === undefined)
|
|
311
|
+
process.env.HERDCTL_LOG_LEVEL = "warn";
|
|
312
|
+
}
|
|
235
313
|
warnIfNoCredentials(process.env.CLAUDE_HOME ?? path.join(os.homedir(), ".claude"));
|
|
236
314
|
const port = process.env.PORT ?? "4000";
|
|
237
315
|
const host = process.env.HOST ?? "127.0.0.1";
|
|
238
316
|
// Imported dynamically, AFTER the env above is set: a static import would
|
|
239
317
|
// pull in app.ts -> config.ts and resolve config against the wrong data dir.
|
|
240
318
|
const { start } = await import("../start.js");
|
|
241
|
-
|
|
242
|
-
|
|
319
|
+
try {
|
|
320
|
+
await start();
|
|
321
|
+
}
|
|
322
|
+
catch (err) {
|
|
323
|
+
fail(explainListenError(err, host, port));
|
|
324
|
+
}
|
|
325
|
+
const url = `http://${host}:${port}`;
|
|
326
|
+
console.log(`\n Paddock is running at ${url}\n Data: ${dataDir}\n Press Ctrl-C to stop.\n`);
|
|
327
|
+
if (opts.open)
|
|
328
|
+
openBrowser(url);
|
|
329
|
+
}
|
|
330
|
+
// Run only when executed directly, so the unit tests can import the pure parts.
|
|
331
|
+
// `pathToFileURL`, not `"file://" + argv[1]`: the naive concatenation breaks on
|
|
332
|
+
// any path containing a space or non-ASCII character — the same percent-encoding
|
|
333
|
+
// trap as #636.
|
|
334
|
+
if (process.argv[1] !== undefined && pathToFileURL(process.argv[1]).href === import.meta.url) {
|
|
335
|
+
main().catch((err) => {
|
|
336
|
+
console.error("fatal:", err);
|
|
337
|
+
process.exit(1);
|
|
338
|
+
});
|
|
243
339
|
}
|
|
244
|
-
main().catch((err) => {
|
|
245
|
-
console.error("fatal:", err);
|
|
246
|
-
process.exit(1);
|
|
247
|
-
});
|
|
@@ -168,6 +168,25 @@ export interface PaddockConfig {
|
|
|
168
168
|
* instance CLAUDE.md up by walk-up. That closes #512.
|
|
169
169
|
*/
|
|
170
170
|
nativeSystemPrompt: boolean;
|
|
171
|
+
/**
|
|
172
|
+
* The **environment** prompt appended to every keeper turn's system prompt
|
|
173
|
+
* (issue #635) — the short note telling the agent it renders into a browser as
|
|
174
|
+
* GFM, not a terminal. Resolved from `PADDOCK_ENVIRONMENT_PROMPT` over the
|
|
175
|
+
* `environmentPrompt:` file key over
|
|
176
|
+
* {@link import("./environment-prompt.js").DEFAULT_ENVIRONMENT_PROMPT}.
|
|
177
|
+
*
|
|
178
|
+
* This is the RESOLVED, effective text, so it has only two meaningful states
|
|
179
|
+
* at the point of use: non-empty (append it) and `""` (append nothing — the
|
|
180
|
+
* instance opted out). The three *authoring* states — unset, a string, an
|
|
181
|
+
* empty string — collapse here, which is deliberate: every consumer just asks
|
|
182
|
+
* "is there anything to append?" and the Settings screen shows the text that
|
|
183
|
+
* is actually in force rather than a tri-state it would have to explain.
|
|
184
|
+
*
|
|
185
|
+
* Orthogonal to {@link nativeSystemPrompt}: that switch chooses the agent's
|
|
186
|
+
* *role* prompt (native preset vs Paddock's terse replace text); this states
|
|
187
|
+
* environmental fact about the deployment and applies on top of either.
|
|
188
|
+
*/
|
|
189
|
+
environmentPrompt: string;
|
|
171
190
|
/**
|
|
172
191
|
* Whether keeper turns are handed the read-only self-management MCP server
|
|
173
192
|
* (issue #214 Phase 1) — the `mcp__paddock_manage__*` tools that let a keeper
|
|
@@ -378,6 +397,13 @@ export interface PaddockConfigFile {
|
|
|
378
397
|
models?: string[] | string;
|
|
379
398
|
driveMode?: string;
|
|
380
399
|
nativeSystemPrompt?: boolean | string;
|
|
400
|
+
/**
|
|
401
|
+
* Environment system prompt override (issue #635). Absent ⇒ Paddock's built-in
|
|
402
|
+
* text; a non-empty string ⇒ that text instead; an EMPTY string ⇒ append
|
|
403
|
+
* nothing. Unlike almost every other key here, blank is meaningful and is NOT
|
|
404
|
+
* folded to the default — see {@link loadEnvironmentPrompt}.
|
|
405
|
+
*/
|
|
406
|
+
environmentPrompt?: string;
|
|
381
407
|
selfMcpEnabled?: boolean | string;
|
|
382
408
|
selfMcpWriteEnabled?: boolean | string;
|
|
383
409
|
selfMcpProjectsEnabled?: boolean | string;
|
|
@@ -24,6 +24,7 @@ import { fileURLToPath } from "node:url";
|
|
|
24
24
|
import YAML from "yaml";
|
|
25
25
|
import { DEFAULT_DRIVE_MODE, isKnownDriveMode, isKnownModel, } from "./models.js";
|
|
26
26
|
import { DEFAULT_MAX_SPAWN_DEPTH, isValidMaxSpawnDepth } from "./spawn-capability.js";
|
|
27
|
+
import { DEFAULT_ENVIRONMENT_PROMPT } from "./environment-prompt.js";
|
|
27
28
|
import { DEFAULT_RECOVERY } from "./recovery-config.js";
|
|
28
29
|
import { DEFAULT_CURATION } from "./curation-config.js";
|
|
29
30
|
import { resolveManagementApiConfig, } from "./management-config.js";
|
|
@@ -290,6 +291,7 @@ export function loadPaddockConfig() {
|
|
|
290
291
|
models: loadModels(file.models),
|
|
291
292
|
driveMode: loadDriveMode(file.driveMode),
|
|
292
293
|
nativeSystemPrompt: loadNativeSystemPrompt(file.nativeSystemPrompt),
|
|
294
|
+
environmentPrompt: loadEnvironmentPrompt(file.environmentPrompt),
|
|
293
295
|
selfMcpEnabled: loadSelfMcpEnabled(file.selfMcpEnabled),
|
|
294
296
|
selfMcpWriteEnabled: loadSelfMcpEnabled(file.selfMcpEnabled) && loadSelfMcpWriteEnabled(file.selfMcpWriteEnabled),
|
|
295
297
|
// Projects tool implies write implies read — the tool is appended in the write
|
|
@@ -534,6 +536,36 @@ function loadNativeSystemPrompt(file) {
|
|
|
534
536
|
const raw = envOr("PADDOCK_NATIVE_PROMPT", fileOr(file, "true")).toLowerCase();
|
|
535
537
|
return !(raw === "0" || raw === "false" || raw === "no");
|
|
536
538
|
}
|
|
539
|
+
/**
|
|
540
|
+
* Resolve the environment system prompt (issue #635): env
|
|
541
|
+
* `PADDOCK_ENVIRONMENT_PROMPT` over the `environmentPrompt:` file key over the
|
|
542
|
+
* built-in {@link DEFAULT_ENVIRONMENT_PROMPT}.
|
|
543
|
+
*
|
|
544
|
+
* Deliberately does NOT use `envOr`/`fileOr`. Those fold a blank value to the
|
|
545
|
+
* fallback, which would make opting out impossible — blank is the opt-out here,
|
|
546
|
+
* so precedence is decided on *definedness*, not on emptiness:
|
|
547
|
+
*
|
|
548
|
+
* - `PADDOCK_ENVIRONMENT_PROMPT` **defined at all** (even empty) wins outright.
|
|
549
|
+
* That is why the field is declared `envShadowWhenDefined` in
|
|
550
|
+
* instance-config.ts — same rule as `PADDOCK_BROWSER_MCP` — so the Settings
|
|
551
|
+
* screen renders it read-only in exactly the cases where it really is.
|
|
552
|
+
* - Otherwise a `string` file value wins, empty included. A non-string (a YAML
|
|
553
|
+
* number, a mapping, `null` from a bare `environmentPrompt:`) is ignored
|
|
554
|
+
* rather than stringified — `"null"` is not a prompt anyone meant to write.
|
|
555
|
+
* - Otherwise the built-in default.
|
|
556
|
+
*
|
|
557
|
+
* The value is used verbatim: no trimming, no normalization. An operator's
|
|
558
|
+
* trailing newline or leading indentation survives the round-trip, because the
|
|
559
|
+
* text is going into a prompt where whitespace is content.
|
|
560
|
+
*/
|
|
561
|
+
function loadEnvironmentPrompt(file) {
|
|
562
|
+
const env = process.env.PADDOCK_ENVIRONMENT_PROMPT;
|
|
563
|
+
if (env !== undefined)
|
|
564
|
+
return env;
|
|
565
|
+
if (typeof file === "string")
|
|
566
|
+
return file;
|
|
567
|
+
return DEFAULT_ENVIRONMENT_PROMPT;
|
|
568
|
+
}
|
|
537
569
|
/**
|
|
538
570
|
* Resolve the instance model allow-list (issue #457 Step 2), precedence
|
|
539
571
|
* env `PADDOCK_MODELS` (comma-separated ids) over YAML `models:` (a string array,
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Paddock's built-in **environment** system prompt (issue #635).
|
|
3
|
+
*
|
|
4
|
+
* Paddock injects no system prompt of its own: on a default instance the keeper
|
|
5
|
+
* runs on Claude Code's stock `claude_code` preset, which is written for a
|
|
6
|
+
* terminal. Nothing anywhere tells the model that its replies are rendered as
|
|
7
|
+
* GitHub-Flavored Markdown in a browser (`MarkdownRenderer.tsx` = react-markdown
|
|
8
|
+
* + remark-gfm), or that `mcp__paddock__send_file` renders a file inline.
|
|
9
|
+
*
|
|
10
|
+
* This text is a small **append** to whatever system prompt the agent already
|
|
11
|
+
* has — it states environmental fact about the deployment, and deliberately does
|
|
12
|
+
* not instruct on role, tone or workflow (that is the `CLAUDE.md` hierarchy's
|
|
13
|
+
* layer, and it stays the user's).
|
|
14
|
+
*
|
|
15
|
+
* ## Why exactly two rules
|
|
16
|
+
*
|
|
17
|
+
* The text is not written from first principles — an earlier draft was, and the
|
|
18
|
+
* audit in issue #635 refuted most of it. It comes from auditing the 100 most
|
|
19
|
+
* recent chats on the dogfood instance (3,944 assistant text blocks, 2.1 MB of
|
|
20
|
+
* user-facing prose). Two findings carried ~90% of the measured value:
|
|
21
|
+
*
|
|
22
|
+
* - **`#123` is dead text.** 4,440 bare refs vs 155 markdown links. Bare URLs
|
|
23
|
+
* autolink under GFM and came out fine (only 4 inert, all backticked), so the
|
|
24
|
+
* defect is specifically issue/PR refs — and it is not inability: single
|
|
25
|
+
* messages link some refs and leave others bare.
|
|
26
|
+
* - **Visual work is described, never shown.** 194 image reads, 138
|
|
27
|
+
* screenshots, and **zero** images ever sent to a user. `send_file` was used
|
|
28
|
+
* 10 times, none of them an image. In one chat this cost a full re-work round
|
|
29
|
+
* — the agent read 17 QC frames, showed none, misread one, and shipped a
|
|
30
|
+
* regression the user then had to screenshot themselves.
|
|
31
|
+
*
|
|
32
|
+
* Things measured and deliberately **cut**, so they don't get re-added: "no ANSI
|
|
33
|
+
* colour" (0 escapes in 2.1 MB), "don't paste long content" (0 fences ≥40 lines;
|
|
34
|
+
* median fence is 3), "use markdown structure" (580 headings, 892 bullets, 101
|
|
35
|
+
* well-formed tables — already excellent), `file.ts:123` guidance (real but
|
|
36
|
+
* low-stakes; 72 of 75 sampled sit in backticks beside the symbol name), and
|
|
37
|
+
* mermaid (0 genuine missed opportunities; an instruction would over-trigger).
|
|
38
|
+
*
|
|
39
|
+
* ## Keep in sync
|
|
40
|
+
*
|
|
41
|
+
* The `send_file` tool description (`send-file-mcp.ts`) is the only other
|
|
42
|
+
* Paddock-aware signal in a normal turn. These two are the same message told
|
|
43
|
+
* twice; change one and re-read the other.
|
|
44
|
+
*/
|
|
45
|
+
/**
|
|
46
|
+
* The default environment prompt appended to every keeper turn unless the
|
|
47
|
+
* instance overrides or opts out (see `environmentPrompt` in config.ts).
|
|
48
|
+
*
|
|
49
|
+
* Kept verbatim from issue #635's evidence pass. Resist adding a third rule
|
|
50
|
+
* without measurement — several plausible candidates were cut above.
|
|
51
|
+
*/
|
|
52
|
+
export declare const DEFAULT_ENVIRONMENT_PROMPT = "You are running in Paddock, a web app \u2014 your replies render as GitHub-Flavored\nMarkdown in a browser, not as terminal output.\n\n- Show, don't describe. A screenshot, rendering, chart or file the user is meant\n to look at should be sent with `mcp__paddock__send_file`, which renders it\n inline. A filesystem path, or \"it's up on port 5026\", leaves them unable to see\n your evidence \u2014 or to catch you misreading it.\n- Make clickable things clickable. Bare URLs autolink; `#123` does not \u2014 write\n `[#123](https://github.com/owner/repo/issues/123)`. Prefer a full URL to a bare\n port number, and link a chat or project rather than quoting its id.";
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Paddock's built-in **environment** system prompt (issue #635).
|
|
3
|
+
*
|
|
4
|
+
* Paddock injects no system prompt of its own: on a default instance the keeper
|
|
5
|
+
* runs on Claude Code's stock `claude_code` preset, which is written for a
|
|
6
|
+
* terminal. Nothing anywhere tells the model that its replies are rendered as
|
|
7
|
+
* GitHub-Flavored Markdown in a browser (`MarkdownRenderer.tsx` = react-markdown
|
|
8
|
+
* + remark-gfm), or that `mcp__paddock__send_file` renders a file inline.
|
|
9
|
+
*
|
|
10
|
+
* This text is a small **append** to whatever system prompt the agent already
|
|
11
|
+
* has — it states environmental fact about the deployment, and deliberately does
|
|
12
|
+
* not instruct on role, tone or workflow (that is the `CLAUDE.md` hierarchy's
|
|
13
|
+
* layer, and it stays the user's).
|
|
14
|
+
*
|
|
15
|
+
* ## Why exactly two rules
|
|
16
|
+
*
|
|
17
|
+
* The text is not written from first principles — an earlier draft was, and the
|
|
18
|
+
* audit in issue #635 refuted most of it. It comes from auditing the 100 most
|
|
19
|
+
* recent chats on the dogfood instance (3,944 assistant text blocks, 2.1 MB of
|
|
20
|
+
* user-facing prose). Two findings carried ~90% of the measured value:
|
|
21
|
+
*
|
|
22
|
+
* - **`#123` is dead text.** 4,440 bare refs vs 155 markdown links. Bare URLs
|
|
23
|
+
* autolink under GFM and came out fine (only 4 inert, all backticked), so the
|
|
24
|
+
* defect is specifically issue/PR refs — and it is not inability: single
|
|
25
|
+
* messages link some refs and leave others bare.
|
|
26
|
+
* - **Visual work is described, never shown.** 194 image reads, 138
|
|
27
|
+
* screenshots, and **zero** images ever sent to a user. `send_file` was used
|
|
28
|
+
* 10 times, none of them an image. In one chat this cost a full re-work round
|
|
29
|
+
* — the agent read 17 QC frames, showed none, misread one, and shipped a
|
|
30
|
+
* regression the user then had to screenshot themselves.
|
|
31
|
+
*
|
|
32
|
+
* Things measured and deliberately **cut**, so they don't get re-added: "no ANSI
|
|
33
|
+
* colour" (0 escapes in 2.1 MB), "don't paste long content" (0 fences ≥40 lines;
|
|
34
|
+
* median fence is 3), "use markdown structure" (580 headings, 892 bullets, 101
|
|
35
|
+
* well-formed tables — already excellent), `file.ts:123` guidance (real but
|
|
36
|
+
* low-stakes; 72 of 75 sampled sit in backticks beside the symbol name), and
|
|
37
|
+
* mermaid (0 genuine missed opportunities; an instruction would over-trigger).
|
|
38
|
+
*
|
|
39
|
+
* ## Keep in sync
|
|
40
|
+
*
|
|
41
|
+
* The `send_file` tool description (`send-file-mcp.ts`) is the only other
|
|
42
|
+
* Paddock-aware signal in a normal turn. These two are the same message told
|
|
43
|
+
* twice; change one and re-read the other.
|
|
44
|
+
*/
|
|
45
|
+
/**
|
|
46
|
+
* The default environment prompt appended to every keeper turn unless the
|
|
47
|
+
* instance overrides or opts out (see `environmentPrompt` in config.ts).
|
|
48
|
+
*
|
|
49
|
+
* Kept verbatim from issue #635's evidence pass. Resist adding a third rule
|
|
50
|
+
* without measurement — several plausible candidates were cut above.
|
|
51
|
+
*/
|
|
52
|
+
export const DEFAULT_ENVIRONMENT_PROMPT = `You are running in Paddock, a web app — your replies render as GitHub-Flavored
|
|
53
|
+
Markdown in a browser, not as terminal output.
|
|
54
|
+
|
|
55
|
+
- Show, don't describe. A screenshot, rendering, chart or file the user is meant
|
|
56
|
+
to look at should be sent with \`mcp__paddock__send_file\`, which renders it
|
|
57
|
+
inline. A filesystem path, or "it's up on port 5026", leaves them unable to see
|
|
58
|
+
your evidence — or to catch you misreading it.
|
|
59
|
+
- Make clickable things clickable. Bare URLs autolink; \`#123\` does not — write
|
|
60
|
+
\`[#123](https://github.com/owner/repo/issues/123)\`. Prefer a full URL to a bare
|
|
61
|
+
port number, and link a chat or project rather than quoting its id.`;
|
|
@@ -147,6 +147,57 @@ export declare class HerdctlService {
|
|
|
147
147
|
private jobsIndexOrNull;
|
|
148
148
|
private get jobsIndex();
|
|
149
149
|
constructor(cfg: PaddockConfig);
|
|
150
|
+
/**
|
|
151
|
+
* The environment system prompt to append to a keeper turn (issue #635), or
|
|
152
|
+
* `undefined` when the instance opted out (`environmentPrompt: ""`) — herdctl
|
|
153
|
+
* treats a falsy `systemPromptAppend` as "don't touch the system prompt", so
|
|
154
|
+
* an opted-out instance is byte-identical to pre-#635 behaviour.
|
|
155
|
+
*
|
|
156
|
+
* Read from `this.cfg` here rather than taken as a per-turn option **on
|
|
157
|
+
* purpose**. Every user-visible turn shape — a human send, a slash command, a
|
|
158
|
+
* scheduled wake, a spawned sub-chat, a `driveMode: batch` trigger — funnels
|
|
159
|
+
* through {@link chat}, {@link chatSession} or {@link runCommand}, so applying
|
|
160
|
+
* it at that choke point makes coverage structural. Threading it through
|
|
161
|
+
* `ChatTurnOptions` instead would make it one more thing five call sites have
|
|
162
|
+
* to remember, which is exactly how `isSidechainMessage` ended up wired into
|
|
163
|
+
* 1 of 5 turn paths.
|
|
164
|
+
*
|
|
165
|
+
* It is orthogonal to `nativeSystemPrompt`: herdctl appends to whatever prompt
|
|
166
|
+
* the agent has, so this rides on top of the native preset AND of Paddock's
|
|
167
|
+
* terse replace prompt (see `buildSystemPrompt` in core's sdk-runtime, which
|
|
168
|
+
* folds an append into a preset's `append` field or concatenates onto a
|
|
169
|
+
* literal string).
|
|
170
|
+
*/
|
|
171
|
+
private get environmentPromptAppend();
|
|
172
|
+
/**
|
|
173
|
+
* The same value for the **CLI** runtime (`driveMode: batch`), where it is NOT
|
|
174
|
+
* always safe to send.
|
|
175
|
+
*
|
|
176
|
+
* herdctl's CLI runtime has no `--append-system-prompt`; it folds
|
|
177
|
+
* `systemPromptAppend` into the single `--system-prompt` flag
|
|
178
|
+
* (`cli-runtime.ts`), and `--system-prompt` REPLACES Claude Code's preset:
|
|
179
|
+
*
|
|
180
|
+
* agent.system_prompt && append → --system-prompt "<agent>\n\n<append>" ✅ appends
|
|
181
|
+
* append only → --system-prompt "<append>" ❌ REPLACES
|
|
182
|
+
*
|
|
183
|
+
* Paddock sets `agent.system_prompt` only when `nativeSystemPrompt` is false
|
|
184
|
+
* (herdctl-agent-config.ts), so on a DEFAULT batch instance the second branch
|
|
185
|
+
* fires and a two-rule environment note would silently become the agent's
|
|
186
|
+
* entire system prompt — the whole coding preset gone. That is the same
|
|
187
|
+
* replace-vs-append trap #635 warns about, one layer down.
|
|
188
|
+
*
|
|
189
|
+
* So: on the CLI path we send the append only when it will genuinely be
|
|
190
|
+
* appended. A default (native) batch instance is left byte-identical to
|
|
191
|
+
* pre-#635 and simply doesn't get the environment prompt — a missing hint is
|
|
192
|
+
* survivable; a missing coding prompt is not. The SDK runtime has no such
|
|
193
|
+
* problem (it folds the text into the preset's `append` field), and the SDK
|
|
194
|
+
* runtime is what every chat actually uses: `openChatSession` hard-codes it,
|
|
195
|
+
* and `session` is the default drive mode. Batch is the legacy path.
|
|
196
|
+
*
|
|
197
|
+
* Lifting this needs `--append-system-prompt` support in core's CLI runtime;
|
|
198
|
+
* tracked upstream in edspencer/herdctl.
|
|
199
|
+
*/
|
|
200
|
+
private get environmentPromptAppendForCli();
|
|
150
201
|
/**
|
|
151
202
|
* Construct + initialize the FleetManager against a minimal zero-agent
|
|
152
203
|
* config (fleet + defaults only). Agents are then registered programmatically
|