@edspencer/paddock 0.79.1 → 0.81.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/app.d.ts +7 -0
- package/packages/server/dist/app.js +19 -15
- package/packages/server/dist/auth.d.ts +3 -2
- package/packages/server/dist/auth.js +11 -0
- package/packages/server/dist/background-live.d.ts +17 -0
- package/packages/server/dist/background-live.js +24 -0
- package/packages/server/dist/busy.d.ts +96 -0
- package/packages/server/dist/busy.js +88 -0
- package/packages/server/dist/cli/args.d.ts +63 -7
- package/packages/server/dist/cli/args.js +227 -20
- package/packages/server/dist/cli/busy-command.d.ts +58 -0
- package/packages/server/dist/cli/busy-command.js +108 -0
- package/packages/server/dist/cli/busy.d.ts +69 -0
- package/packages/server/dist/cli/busy.js +150 -0
- package/packages/server/dist/cli/exit-codes.d.ts +49 -0
- package/packages/server/dist/cli/exit-codes.js +48 -0
- package/packages/server/dist/cli/output.d.ts +52 -0
- package/packages/server/dist/cli/output.js +84 -0
- package/packages/server/dist/cli/paddock.js +98 -37
- package/packages/server/dist/cli/service/backend.d.ts +48 -4
- package/packages/server/dist/cli/service/index.d.ts +68 -2
- package/packages/server/dist/cli/service/index.js +321 -60
- package/packages/server/dist/cli/service/launchd.d.ts +23 -1
- package/packages/server/dist/cli/service/launchd.js +145 -25
- package/packages/server/dist/cli/service/logs.d.ts +39 -0
- package/packages/server/dist/cli/service/logs.js +115 -0
- package/packages/server/dist/cli/service/path-env.d.ts +27 -0
- package/packages/server/dist/cli/service/path-env.js +120 -0
- package/packages/server/dist/cli/service/ready.d.ts +22 -0
- package/packages/server/dist/cli/service/ready.js +30 -0
- package/packages/server/dist/cli/service/spec.d.ts +45 -6
- package/packages/server/dist/cli/service/spec.js +34 -7
- package/packages/server/dist/cli/service/systemd.d.ts +47 -4
- package/packages/server/dist/cli/service/systemd.js +127 -22
- package/packages/server/dist/herdctl.d.ts +19 -0
- package/packages/server/dist/herdctl.js +26 -0
- package/packages/server/dist/management-mcp-server.d.ts +21 -3
- package/packages/server/dist/management-mcp-server.js +4 -13
- package/packages/server/dist/routes/meta.js +56 -3
- package/packages/server/dist/runtime-file.d.ts +26 -0
- package/packages/server/dist/runtime-file.js +91 -0
- package/packages/server/dist/service-log.d.ts +29 -0
- package/packages/server/dist/service-log.js +104 -0
- package/packages/server/dist/start.d.ts +33 -0
- package/packages/server/dist/start.js +126 -8
- package/packages/server/dist/version.d.ts +3 -0
- package/packages/server/dist/version.js +32 -0
- package/packages/server/dist/ws-context.d.ts +9 -0
- package/packages/server/dist/ws-turn.js +1 -0
- package/packages/server/package.json +1 -1
- package/packages/web/dist/assets/{DeleteProjectDialog-BcxJMA1w.js → DeleteProjectDialog-D8oIJ8HC.js} +1 -1
- package/packages/web/dist/assets/DiscoverPage-CQlzxsn4.js +2 -0
- package/packages/web/dist/assets/{DiscoverView-DOmTmiVv.js → DiscoverView-o0KCr3aB.js} +1 -1
- package/packages/web/dist/assets/{InstanceConfigPage-DWLQqHW0.js → InstanceConfigPage-RSJz7oL3.js} +1 -1
- package/packages/web/dist/assets/{MarkdownRenderer-Y25JWlMR.js → MarkdownRenderer-39mafH8b.js} +1 -1
- package/packages/web/dist/assets/{ProjectRedirect-CCI1VWM6.js → ProjectRedirect-Bq7GVguY.js} +1 -1
- package/packages/web/dist/assets/{ProjectView-DzfVhjya.js → ProjectView-BoBIru1g.js} +3 -3
- package/packages/web/dist/assets/{ProjectsGrid-yIyyb2HA.js → ProjectsGrid-mJ4gqbr6.js} +1 -1
- package/packages/web/dist/assets/{RootHome-x0Om9-Fd.js → RootHome-C0kwFyYp.js} +1 -1
- package/packages/web/dist/assets/{Surfaces-wbcMQIsK.js → Surfaces-MkhXlB8z.js} +1 -1
- package/packages/web/dist/assets/{arc-tWN7FYIq.js → arc-DExlwAwO.js} +1 -1
- package/packages/web/dist/assets/{architectureDiagram-3BPJPVTR-Ccj6Vlyy.js → architectureDiagram-3BPJPVTR-CZly3W79.js} +1 -1
- package/packages/web/dist/assets/{blockDiagram-GPEHLZMM-B7iGXwqY.js → blockDiagram-GPEHLZMM-AgES73i3.js} +1 -1
- package/packages/web/dist/assets/{c4Diagram-AAUBKEIU-SkLKFSso.js → c4Diagram-AAUBKEIU-N9mD8Pfk.js} +1 -1
- package/packages/web/dist/assets/channel-ExsAG7qT.js +2 -0
- package/packages/web/dist/assets/{chunk-2J33WTMH-BrS8C-YK.js → chunk-2J33WTMH-Dsx_kfUl.js} +1 -1
- package/packages/web/dist/assets/{chunk-4BX2VUAB-BUR6nG8w.js → chunk-4BX2VUAB-BQjqcjPj.js} +1 -1
- package/packages/web/dist/assets/{chunk-55IACEB6-BRBNJb-u.js → chunk-55IACEB6-CCxpYQpY.js} +1 -1
- package/packages/web/dist/assets/{chunk-727SXJPM-DKeNPTXz.js → chunk-727SXJPM-DpFRdnUa.js} +1 -1
- package/packages/web/dist/assets/{chunk-AQP2D5EJ-BOS1K-DX.js → chunk-AQP2D5EJ-D2BAlzoO.js} +1 -1
- package/packages/web/dist/assets/{chunk-FMBD7UC4-D3PZL8fo.js → chunk-FMBD7UC4-BFaMRzUF.js} +1 -1
- package/packages/web/dist/assets/{chunk-ND2GUHAM-DA9Y77Zj.js → chunk-ND2GUHAM-B9NGJ9xZ.js} +1 -1
- package/packages/web/dist/assets/{chunk-QZHKN3VN-wPph-weq.js → chunk-QZHKN3VN-CF1IHyMS.js} +1 -1
- package/packages/web/dist/assets/classDiagram-4FO5ZUOK-si7g9okq.js +2 -0
- package/packages/web/dist/assets/classDiagram-v2-Q7XG4LA2-si7g9okq.js +2 -0
- package/packages/web/dist/assets/{core-I1PQdGMJ.js → core-XjElsvQ7.js} +1 -1
- package/packages/web/dist/assets/{cose-bilkent-S5V4N54A-BA7N2vVK.js → cose-bilkent-S5V4N54A-CF972Flx.js} +1 -1
- package/packages/web/dist/assets/{dagre-BM42HDAG-DKpON5p2.js → dagre-BM42HDAG-DsneI8YE.js} +1 -1
- package/packages/web/dist/assets/{diagram-2AECGRRQ-Db0I-eEL.js → diagram-2AECGRRQ-BHyrVzIk.js} +1 -1
- package/packages/web/dist/assets/{diagram-5GNKFQAL-BEuqOTfn.js → diagram-5GNKFQAL-Dj4q8xEi.js} +1 -1
- package/packages/web/dist/assets/{diagram-KO2AKTUF-CnyX_0Xk.js → diagram-KO2AKTUF-BHe3Puv_.js} +1 -1
- package/packages/web/dist/assets/{diagram-LMA3HP47-CYvknzDd.js → diagram-LMA3HP47-wzt6G2FE.js} +1 -1
- package/packages/web/dist/assets/{diagram-OG6HWLK6-DlgKxbYz.js → diagram-OG6HWLK6-CZMbMRJq.js} +1 -1
- package/packages/web/dist/assets/{documentTitle-Cvob-HVs.js → documentTitle-BwTIA8rI.js} +1 -1
- package/packages/web/dist/assets/{erDiagram-TEJ5UH35-BkO5JmiM.js → erDiagram-TEJ5UH35-Dtu14BL1.js} +1 -1
- package/packages/web/dist/assets/{flowDiagram-I6XJVG4X-DG0o_zc1.js → flowDiagram-I6XJVG4X-T34ql9j0.js} +1 -1
- package/packages/web/dist/assets/{ganttDiagram-6RSMTGT7-Bs71No2S.js → ganttDiagram-6RSMTGT7-DQLOeCXK.js} +1 -1
- package/packages/web/dist/assets/{gitGraphDiagram-PVQCEYII-CxODhRKd.js → gitGraphDiagram-PVQCEYII-Bk11zea6.js} +1 -1
- package/packages/web/dist/assets/{index-C4tsPSvG.js → index-CvaWErFv.js} +2 -2
- package/packages/web/dist/assets/{infoDiagram-5YYISTIA-C412TQaw.js → infoDiagram-5YYISTIA-CsE83lu0.js} +1 -1
- package/packages/web/dist/assets/{ishikawaDiagram-YF4QCWOH-DUlcZXQQ.js → ishikawaDiagram-YF4QCWOH-DNVgA93n.js} +1 -1
- package/packages/web/dist/assets/{journeyDiagram-JHISSGLW-iL0klVhn.js → journeyDiagram-JHISSGLW-CLpSNjM3.js} +1 -1
- package/packages/web/dist/assets/{kanban-definition-UN3LZRKU-CO02Zl_Q.js → kanban-definition-UN3LZRKU-BRzuXcKr.js} +1 -1
- package/packages/web/dist/assets/{linear-DtmLY8AG.js → linear-BxOawONt.js} +1 -1
- package/packages/web/dist/assets/{mermaid.core-Dvzxug80.js → mermaid.core-Dvs1QCcz.js} +4 -4
- package/packages/web/dist/assets/{mindmap-definition-RKZ34NQL-q950AAWL.js → mindmap-definition-RKZ34NQL-Dmt7pYwF.js} +1 -1
- package/packages/web/dist/assets/{pieDiagram-4H26LBE5-BVrv8A-H.js → pieDiagram-4H26LBE5-BHiZaJD-.js} +1 -1
- package/packages/web/dist/assets/{quadrantDiagram-W4KKPZXB-Bp5D2MFq.js → quadrantDiagram-W4KKPZXB-U0A1noRE.js} +1 -1
- package/packages/web/dist/assets/{requirementDiagram-4Y6WPE33-yZfQU4Ix.js → requirementDiagram-4Y6WPE33-CZLsr6yi.js} +1 -1
- package/packages/web/dist/assets/{sankeyDiagram-5OEKKPKP-5R8_OZir.js → sankeyDiagram-5OEKKPKP-BTlLGILU.js} +1 -1
- package/packages/web/dist/assets/{sequenceDiagram-3UESZ5HK-C3A9rinG.js → sequenceDiagram-3UESZ5HK-DpSS_Gb1.js} +1 -1
- package/packages/web/dist/assets/{stateDiagram-AJRCARHV-Cshrt_XB.js → stateDiagram-AJRCARHV-D9srE-tV.js} +1 -1
- package/packages/web/dist/assets/stateDiagram-v2-BHNVJYJU-AxWqsDh-.js +2 -0
- package/packages/web/dist/assets/{timeline-definition-PNZ67QCA-kNkiddCX.js → timeline-definition-PNZ67QCA-BYQ5CKab.js} +1 -1
- package/packages/web/dist/assets/{vennDiagram-CIIHVFJN-BiNJm17g.js → vennDiagram-CIIHVFJN-B4W2CtSV.js} +1 -1
- package/packages/web/dist/assets/{wardley-L42UT6IY-CjExbCuX.js → wardley-L42UT6IY--XUV7GvD.js} +1 -1
- package/packages/web/dist/assets/{wardleyDiagram-YWT4CUSO-CMn3z-G2.js → wardleyDiagram-YWT4CUSO-Bl2VIEGY.js} +1 -1
- package/packages/web/dist/assets/{xychartDiagram-2RQKCTM6-B-UhnooW.js → xychartDiagram-2RQKCTM6-Bj2H9Rcf.js} +1 -1
- package/packages/web/dist/index.html +1 -1
- package/packages/web/dist/sw.js +1 -1
- package/packages/web/dist/assets/DiscoverPage-CSPRrFVk.js +0 -2
- package/packages/web/dist/assets/channel-B2NSmlBB.js +0 -2
- package/packages/web/dist/assets/classDiagram-4FO5ZUOK-B3tLFeZC.js +0 -2
- package/packages/web/dist/assets/classDiagram-v2-Q7XG4LA2-B3tLFeZC.js +0 -2
- package/packages/web/dist/assets/stateDiagram-v2-BHNVJYJU-DgiazF78.js +0 -2
package/package.json
CHANGED
|
@@ -17,6 +17,7 @@ import { GitService } from "./git.js";
|
|
|
17
17
|
import { GithubAuth } from "./github-auth.js";
|
|
18
18
|
import { type Transcriber } from "./transcribe.js";
|
|
19
19
|
import type { SessionHub } from "./session-hub.js";
|
|
20
|
+
import type { BackgroundRegistry } from "./background-live.js";
|
|
20
21
|
import { SweepService } from "./sweep.js";
|
|
21
22
|
import { ArchiveStore } from "./archive.js";
|
|
22
23
|
import { StarStore } from "./star.js";
|
|
@@ -57,6 +58,12 @@ export interface BuiltApp {
|
|
|
57
58
|
* same way, via the route context).
|
|
58
59
|
*/
|
|
59
60
|
hub: SessionHub;
|
|
61
|
+
/**
|
|
62
|
+
* The live background-work registry (#604), exposed for the same reason `hub`
|
|
63
|
+
* is — it is WS-layer state a non-socket caller has to read. `GET /api/busy`
|
|
64
|
+
* reaches it through the route context; a test reaches it here.
|
|
65
|
+
*/
|
|
66
|
+
background: BackgroundRegistry;
|
|
60
67
|
/** Tear down the fleet + close the server (no process.exit, for tests). */
|
|
61
68
|
close: () => Promise<void>;
|
|
62
69
|
}
|
|
@@ -15,7 +15,6 @@ import fastifyStatic from "@fastify/static";
|
|
|
15
15
|
import fastifyMultipart from "@fastify/multipart";
|
|
16
16
|
import fastifySwagger from "@fastify/swagger";
|
|
17
17
|
import fastifySwaggerUi from "@fastify/swagger-ui";
|
|
18
|
-
import { createRequire } from "node:module";
|
|
19
18
|
import { promises as fs } from "node:fs";
|
|
20
19
|
import path from "node:path";
|
|
21
20
|
import { loadPaddockConfig } from "./config.js";
|
|
@@ -50,17 +49,9 @@ import { TriggerSessionStore } from "./trigger-session.js";
|
|
|
50
49
|
import { PaddockEventBus } from "./event-bus.js";
|
|
51
50
|
import { TriggerService } from "./triggers.js";
|
|
52
51
|
import { buildSwaggerOptions, buildSwaggerUiOptions } from "./openapi.js";
|
|
53
|
-
//
|
|
54
|
-
//
|
|
55
|
-
|
|
56
|
-
try {
|
|
57
|
-
const require = createRequire(import.meta.url);
|
|
58
|
-
return require("../package.json").version ?? "0.0.0";
|
|
59
|
-
}
|
|
60
|
-
catch {
|
|
61
|
-
return "0.0.0";
|
|
62
|
-
}
|
|
63
|
-
})();
|
|
52
|
+
// One source of truth for "which Paddock is this?" — the OpenAPI document,
|
|
53
|
+
// /api/health and the management MCP handshake all read it (version.ts).
|
|
54
|
+
import { PADDOCK_VERSION } from "./version.js";
|
|
64
55
|
/**
|
|
65
56
|
* True when the (query-stripped) path's last segment carries a file extension —
|
|
66
57
|
* e.g. `/assets/index-ABC.js`, `/favicon.ico`, `/sw.js`. Used by the SPA
|
|
@@ -85,7 +76,20 @@ export async function buildApp(opts = {}) {
|
|
|
85
76
|
// out here. `PADDOCK_QUIET` is set by `cli/paddock.ts` unless `--verbose`.
|
|
86
77
|
installHerdctlLogBridge({ quiet: (process.env.PADDOCK_QUIET ?? "") !== "" });
|
|
87
78
|
const app = Fastify({
|
|
88
|
-
|
|
79
|
+
// **Logs go to stderr, not stdout**, and that is load-bearing rather than
|
|
80
|
+
// tidy. SPEC §3 of the CLI contract requires `serve`'s FIRST stdout line to
|
|
81
|
+
// be `Paddock <version>: <url>` — the line a script reads to find the URL —
|
|
82
|
+
// and boot logging is emitted before the server can know it has bound. With
|
|
83
|
+
// pino on stdout, a machine with no Claude login (every CI runner, every
|
|
84
|
+
// fresh install) put `{"level":40,…,"msg":"no Claude credentials found…"}`
|
|
85
|
+
// there first, so the first line was a JSON log record. The conformance
|
|
86
|
+
// runner caught it; nothing else would have.
|
|
87
|
+
//
|
|
88
|
+
// Nothing loses the logs: Docker and Kubernetes collect both streams, and
|
|
89
|
+
// the service units send stdout and stderr to the same file (SPEC §7). A log
|
|
90
|
+
// shipper configured for stdout ALONE is the one thing that changes, which
|
|
91
|
+
// is why the changeset says so.
|
|
92
|
+
logger: { level: cfg.logLevel, stream: process.stderr },
|
|
89
93
|
});
|
|
90
94
|
// --- safe-by-default bind guard (#435) --------------------------------
|
|
91
95
|
// Fail closed if the resolved bind host is non-loopback AND auth is `none`:
|
|
@@ -313,7 +317,7 @@ export async function buildApp(opts = {}) {
|
|
|
313
317
|
readImage(path.join("icons", "icon-192.png"), "image/png"),
|
|
314
318
|
readImage(path.join("icons", "favicon-32.png"), "image/png"),
|
|
315
319
|
]);
|
|
316
|
-
await app.register(fastifySwagger, buildSwaggerOptions(
|
|
320
|
+
await app.register(fastifySwagger, buildSwaggerOptions(PADDOCK_VERSION, cfg.auth));
|
|
317
321
|
await app.register(fastifySwaggerUi, buildSwaggerUiOptions({
|
|
318
322
|
routePrefix: cfg.openapi.path,
|
|
319
323
|
logo: swaggerLogo,
|
|
@@ -436,5 +440,5 @@ export async function buildApp(opts = {}) {
|
|
|
436
440
|
// `hub` rides along like every other built piece: it is the WS layer's turn
|
|
437
441
|
// bookkeeping, and the only place a caller can observe or annotate a turn
|
|
438
442
|
// without a live socket (see SessionHub.noteCancel).
|
|
439
|
-
return { app, cfg, projects, herdctl, git, githubAuth, sweep, archive, star, readState, unread, parentDetach, runProvenance, queuedMessage, transcriber, events, triggers, hub: chatHandler.managementOpsContext.hub, close };
|
|
443
|
+
return { app, cfg, projects, herdctl, git, githubAuth, sweep, archive, star, readState, unread, parentDetach, runProvenance, queuedMessage, transcriber, events, triggers, hub: chatHandler.managementOpsContext.hub, background: chatHandler.managementOpsContext.background, close };
|
|
440
444
|
}
|
|
@@ -19,8 +19,9 @@
|
|
|
19
19
|
* rejects missing/invalid/expired tokens with 401.
|
|
20
20
|
*
|
|
21
21
|
* Paddock's health route (`/api/health`) is always exempt so the proxy and
|
|
22
|
-
* monitoring can poll a server that is otherwise locked down
|
|
23
|
-
*
|
|
22
|
+
* monitoring can poll a server that is otherwise locked down, as is the CLI
|
|
23
|
+
* contract's `/api/busy` — which carries counts and nothing else. Only
|
|
24
|
+
* registered routes may be exempted — see HEALTH_PATHS below for why (#569).
|
|
24
25
|
*
|
|
25
26
|
* Registered in app.ts BEFORE the routes so the `onRequest` hook guards every
|
|
26
27
|
* REST + WS request. The decorator + hook are added directly to the root app
|
|
@@ -16,6 +16,17 @@ import { createRemoteJWKSet, jwtVerify, } from "jose";
|
|
|
16
16
|
*/
|
|
17
17
|
export const HEALTH_PATHS = new Set([
|
|
18
18
|
"/api/health", // paddock's own health route (routes/meta.ts)
|
|
19
|
+
// The CLI contract's busy check (SPEC §4, §8). Exempt for the same reason
|
|
20
|
+
// health is, and safe for a reason health does not need: the answer is three
|
|
21
|
+
// integers and the version, with no chat title, session id or project name in
|
|
22
|
+
// it — SPEC §10 keeps `reasons` out until there is a way for a caller to prove
|
|
23
|
+
// it is local. `paddock service stop`, `paddock update` and Watchtower's
|
|
24
|
+
// pre-update hook all ask it with no credential to offer, and a 401 would be
|
|
25
|
+
// read by `cli/busy.ts` as exit 1 rather than as "not busy" — so gating it
|
|
26
|
+
// would fail every stop on an authenticated instance.
|
|
27
|
+
//
|
|
28
|
+
// If this answer ever grows a label, this line has to be revisited with it.
|
|
29
|
+
"/api/busy", // routes/meta.ts
|
|
19
30
|
]);
|
|
20
31
|
/**
|
|
21
32
|
* Strip the query string and trailing slash so exemption matching is robust.
|
|
@@ -110,6 +110,23 @@ export declare class BackgroundRegistry {
|
|
|
110
110
|
}[];
|
|
111
111
|
/** Does this session have live background work? The #604 predicate. */
|
|
112
112
|
isBusy(sessionId: string): boolean;
|
|
113
|
+
/**
|
|
114
|
+
* How many live background tasks there are, across the sessions `keep`
|
|
115
|
+
* accepts.
|
|
116
|
+
*
|
|
117
|
+
* The `/api/busy` check (CLI contract SPEC §4) passes herdctl's SDK liveness
|
|
118
|
+
* as `keep`, for the reason the per-process caveat in the module header gives:
|
|
119
|
+
* nothing resets this set when a one-shot `claude` exits, so on
|
|
120
|
+
* `driveMode: batch` a finished turn's tasks sit here indefinitely. Filtering
|
|
121
|
+
* on a live session is what stops a stale entry reporting a batch instance
|
|
122
|
+
* busy for good — and it costs nothing in `session` mode, where the reaper
|
|
123
|
+
* holds a session open precisely BECAUSE it has background work.
|
|
124
|
+
*
|
|
125
|
+
* Counted rather than listed so the busy check allocates nothing per task
|
|
126
|
+
* (§4: in-memory, well under a second). {@link snapshot} is the one that
|
|
127
|
+
* builds arrays, and it is for the connect-time frame.
|
|
128
|
+
*/
|
|
129
|
+
liveTaskCount(keep?: (sessionId: string) => boolean): number;
|
|
113
130
|
/**
|
|
114
131
|
* Forget a session's set. Called when the session's process restarts or its
|
|
115
132
|
* stream ends — see the per-process caveat in the module header.
|
|
@@ -142,6 +142,30 @@ export class BackgroundRegistry {
|
|
|
142
142
|
const e = this.bySession.get(sessionId);
|
|
143
143
|
return !!e && e.tasks.size > 0;
|
|
144
144
|
}
|
|
145
|
+
/**
|
|
146
|
+
* How many live background tasks there are, across the sessions `keep`
|
|
147
|
+
* accepts.
|
|
148
|
+
*
|
|
149
|
+
* The `/api/busy` check (CLI contract SPEC §4) passes herdctl's SDK liveness
|
|
150
|
+
* as `keep`, for the reason the per-process caveat in the module header gives:
|
|
151
|
+
* nothing resets this set when a one-shot `claude` exits, so on
|
|
152
|
+
* `driveMode: batch` a finished turn's tasks sit here indefinitely. Filtering
|
|
153
|
+
* on a live session is what stops a stale entry reporting a batch instance
|
|
154
|
+
* busy for good — and it costs nothing in `session` mode, where the reaper
|
|
155
|
+
* holds a session open precisely BECAUSE it has background work.
|
|
156
|
+
*
|
|
157
|
+
* Counted rather than listed so the busy check allocates nothing per task
|
|
158
|
+
* (§4: in-memory, well under a second). {@link snapshot} is the one that
|
|
159
|
+
* builds arrays, and it is for the connect-time frame.
|
|
160
|
+
*/
|
|
161
|
+
liveTaskCount(keep = () => true) {
|
|
162
|
+
let n = 0;
|
|
163
|
+
for (const [sessionId, e] of this.bySession) {
|
|
164
|
+
if (e.tasks.size > 0 && keep(sessionId))
|
|
165
|
+
n += e.tasks.size;
|
|
166
|
+
}
|
|
167
|
+
return n;
|
|
168
|
+
}
|
|
145
169
|
/**
|
|
146
170
|
* Forget a session's set. Called when the session's process restarts or its
|
|
147
171
|
* stream ends — see the per-process caveat in the module header.
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What `GET /api/busy` answers (CLI contract SPEC §4, ROADMAP "Busy, per tool").
|
|
3
|
+
*
|
|
4
|
+
* ## What "busy" means here
|
|
5
|
+
*
|
|
6
|
+
* Only work **a restart would lose or cut short** — the kind someone would be
|
|
7
|
+
* annoyed to find killed. For Paddock that is three things, and the ROADMAP row
|
|
8
|
+
* names them:
|
|
9
|
+
*
|
|
10
|
+
* - **`chat`** — a chat turn running. This is the one that matters: a turn is a
|
|
11
|
+
* live `claude` subprocess writing a transcript, and `herdctl.stop()` ends it
|
|
12
|
+
* with `waitForJobs: false`. A fired trigger or a cron schedule is NOT a
|
|
13
|
+
* fourth kind, because every one of them runs as a first-class chat on the
|
|
14
|
+
* hub (`ws-triggers.ts` — one `startAgentTurn` for every trigger type), so it
|
|
15
|
+
* is already counted here.
|
|
16
|
+
* - **`background`** — background work a live SDK session is still holding
|
|
17
|
+
* after its turn ended (#604): a `run_in_background` shell, a sub-agent, a
|
|
18
|
+
* `Monitor`. These outlive the `Turn` record, so the hub cannot see them.
|
|
19
|
+
* - **`job`** — herdctl's scheduler running a job. Paddock routes a keeper's
|
|
20
|
+
* schedule fire back onto the hub, so in practice this double-counts with
|
|
21
|
+
* `chat` for the window where both are true; a count that is occasionally
|
|
22
|
+
* high by one is the right failure direction for a check whose answer is
|
|
23
|
+
* "wait a moment".
|
|
24
|
+
*
|
|
25
|
+
* ## What is deliberately NOT busy
|
|
26
|
+
*
|
|
27
|
+
* - **Idle live sessions.** A session the reaper is holding open with no turn
|
|
28
|
+
* and no background task resumes after a restart; that is the whole point of
|
|
29
|
+
* a resumable session.
|
|
30
|
+
* - **The post-turn sweeper.** It is re-runnable from its watermark, so losing
|
|
31
|
+
* one is a re-sweep rather than lost work. The ROADMAP row leaves it out too.
|
|
32
|
+
* - **Open browsers.** A page reconnects to `/ws` and replays.
|
|
33
|
+
*
|
|
34
|
+
* ## Cheap, and in-memory only
|
|
35
|
+
*
|
|
36
|
+
* §4 requires this to read in-memory state, never walk the disk, and answer in
|
|
37
|
+
* well under a second — it is called by Watchtower's pre-update hook and by
|
|
38
|
+
* every `service stop`. So each source here is a `Map.size` or a filtered
|
|
39
|
+
* iteration over one, and {@link collectBusy} is synchronous.
|
|
40
|
+
*
|
|
41
|
+
* That rules out the obvious source for `job`. `fleetStatus().counts.runningJobs`
|
|
42
|
+
* is what the ROADMAP names, and `FleetStatus` is built by `getAgentInfo()`,
|
|
43
|
+
* which **reads the fleet state file off disk** on every call (herdctl's
|
|
44
|
+
* `StatusQueries.readFleetStateSnapshot`) — the same re-parse that was 61% of
|
|
45
|
+
* this server's CPU once. `counts.runningJobs` reduces to
|
|
46
|
+
* `Σ scheduler.getRunningJobCount(agent)`, and the scheduler also publishes
|
|
47
|
+
* `getTotalRunningJobCount()` over the same in-memory map, so Paddock asks the
|
|
48
|
+
* scheduler directly for an identical number with no file read.
|
|
49
|
+
*
|
|
50
|
+
* ## No labels, ids or titles
|
|
51
|
+
*
|
|
52
|
+
* Counts only, for every caller, until SPEC §10's first question (how a CLI
|
|
53
|
+
* proves it is local) is designed. That is why this route needs no auth: there
|
|
54
|
+
* is nothing in the answer to protect. A chat's TITLE would be a different
|
|
55
|
+
* matter, and is exactly what `reasons` will carry once there is a way to know
|
|
56
|
+
* who is asking — so nothing here may grow a string field by accident.
|
|
57
|
+
*/
|
|
58
|
+
/** The non-zero kinds the CLI's message names: `Paddock is busy: 2 chats are running.` */
|
|
59
|
+
export interface BusyCounts {
|
|
60
|
+
/** Chat turns running — a human turn, a fired trigger, a cron fire. */
|
|
61
|
+
chat: number;
|
|
62
|
+
/** Background tasks live sessions are still holding after their turn (#604). */
|
|
63
|
+
background: number;
|
|
64
|
+
/** herdctl scheduler jobs in flight. */
|
|
65
|
+
job: number;
|
|
66
|
+
}
|
|
67
|
+
/** The `/api/busy` body (SPEC §4). */
|
|
68
|
+
export interface BusyBody {
|
|
69
|
+
busy: boolean;
|
|
70
|
+
version: string;
|
|
71
|
+
counts: BusyCounts;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* The three in-memory readings, injected so the computation is unit-testable
|
|
75
|
+
* without a fleet, a hub or a socket. Each must be cheap enough to call on every
|
|
76
|
+
* request — see the module header.
|
|
77
|
+
*/
|
|
78
|
+
export interface BusySources {
|
|
79
|
+
/** Sessions with a running turn: `hub.runningSessions().length`. */
|
|
80
|
+
runningChats: () => number;
|
|
81
|
+
/** Live background tasks, counted only in sessions whose SDK session is live. */
|
|
82
|
+
backgroundTasks: () => number;
|
|
83
|
+
/** Scheduler jobs running right now. */
|
|
84
|
+
runningJobs: () => number;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Read the three counts and decide busy.
|
|
88
|
+
*
|
|
89
|
+
* `busy` is `counts` having any non-zero member rather than a separately
|
|
90
|
+
* computed predicate, so the sentence the CLI prints can never disagree with the
|
|
91
|
+
* boolean it printed it for. A source that throws counts as zero: a busy check
|
|
92
|
+
* that 500s is worse than one that under-reports, because a 500 is read by
|
|
93
|
+
* `busy-client.ts` as "something answered, but not as Paddock's busy check" —
|
|
94
|
+
* exit 1 — which fails a `service stop` outright instead of letting it proceed.
|
|
95
|
+
*/
|
|
96
|
+
export declare function collectBusy(sources: BusySources, version: string): BusyBody;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What `GET /api/busy` answers (CLI contract SPEC §4, ROADMAP "Busy, per tool").
|
|
3
|
+
*
|
|
4
|
+
* ## What "busy" means here
|
|
5
|
+
*
|
|
6
|
+
* Only work **a restart would lose or cut short** — the kind someone would be
|
|
7
|
+
* annoyed to find killed. For Paddock that is three things, and the ROADMAP row
|
|
8
|
+
* names them:
|
|
9
|
+
*
|
|
10
|
+
* - **`chat`** — a chat turn running. This is the one that matters: a turn is a
|
|
11
|
+
* live `claude` subprocess writing a transcript, and `herdctl.stop()` ends it
|
|
12
|
+
* with `waitForJobs: false`. A fired trigger or a cron schedule is NOT a
|
|
13
|
+
* fourth kind, because every one of them runs as a first-class chat on the
|
|
14
|
+
* hub (`ws-triggers.ts` — one `startAgentTurn` for every trigger type), so it
|
|
15
|
+
* is already counted here.
|
|
16
|
+
* - **`background`** — background work a live SDK session is still holding
|
|
17
|
+
* after its turn ended (#604): a `run_in_background` shell, a sub-agent, a
|
|
18
|
+
* `Monitor`. These outlive the `Turn` record, so the hub cannot see them.
|
|
19
|
+
* - **`job`** — herdctl's scheduler running a job. Paddock routes a keeper's
|
|
20
|
+
* schedule fire back onto the hub, so in practice this double-counts with
|
|
21
|
+
* `chat` for the window where both are true; a count that is occasionally
|
|
22
|
+
* high by one is the right failure direction for a check whose answer is
|
|
23
|
+
* "wait a moment".
|
|
24
|
+
*
|
|
25
|
+
* ## What is deliberately NOT busy
|
|
26
|
+
*
|
|
27
|
+
* - **Idle live sessions.** A session the reaper is holding open with no turn
|
|
28
|
+
* and no background task resumes after a restart; that is the whole point of
|
|
29
|
+
* a resumable session.
|
|
30
|
+
* - **The post-turn sweeper.** It is re-runnable from its watermark, so losing
|
|
31
|
+
* one is a re-sweep rather than lost work. The ROADMAP row leaves it out too.
|
|
32
|
+
* - **Open browsers.** A page reconnects to `/ws` and replays.
|
|
33
|
+
*
|
|
34
|
+
* ## Cheap, and in-memory only
|
|
35
|
+
*
|
|
36
|
+
* §4 requires this to read in-memory state, never walk the disk, and answer in
|
|
37
|
+
* well under a second — it is called by Watchtower's pre-update hook and by
|
|
38
|
+
* every `service stop`. So each source here is a `Map.size` or a filtered
|
|
39
|
+
* iteration over one, and {@link collectBusy} is synchronous.
|
|
40
|
+
*
|
|
41
|
+
* That rules out the obvious source for `job`. `fleetStatus().counts.runningJobs`
|
|
42
|
+
* is what the ROADMAP names, and `FleetStatus` is built by `getAgentInfo()`,
|
|
43
|
+
* which **reads the fleet state file off disk** on every call (herdctl's
|
|
44
|
+
* `StatusQueries.readFleetStateSnapshot`) — the same re-parse that was 61% of
|
|
45
|
+
* this server's CPU once. `counts.runningJobs` reduces to
|
|
46
|
+
* `Σ scheduler.getRunningJobCount(agent)`, and the scheduler also publishes
|
|
47
|
+
* `getTotalRunningJobCount()` over the same in-memory map, so Paddock asks the
|
|
48
|
+
* scheduler directly for an identical number with no file read.
|
|
49
|
+
*
|
|
50
|
+
* ## No labels, ids or titles
|
|
51
|
+
*
|
|
52
|
+
* Counts only, for every caller, until SPEC §10's first question (how a CLI
|
|
53
|
+
* proves it is local) is designed. That is why this route needs no auth: there
|
|
54
|
+
* is nothing in the answer to protect. A chat's TITLE would be a different
|
|
55
|
+
* matter, and is exactly what `reasons` will carry once there is a way to know
|
|
56
|
+
* who is asking — so nothing here may grow a string field by accident.
|
|
57
|
+
*/
|
|
58
|
+
/**
|
|
59
|
+
* Read the three counts and decide busy.
|
|
60
|
+
*
|
|
61
|
+
* `busy` is `counts` having any non-zero member rather than a separately
|
|
62
|
+
* computed predicate, so the sentence the CLI prints can never disagree with the
|
|
63
|
+
* boolean it printed it for. A source that throws counts as zero: a busy check
|
|
64
|
+
* that 500s is worse than one that under-reports, because a 500 is read by
|
|
65
|
+
* `busy-client.ts` as "something answered, but not as Paddock's busy check" —
|
|
66
|
+
* exit 1 — which fails a `service stop` outright instead of letting it proceed.
|
|
67
|
+
*/
|
|
68
|
+
export function collectBusy(sources, version) {
|
|
69
|
+
const counts = {
|
|
70
|
+
chat: safeCount(sources.runningChats),
|
|
71
|
+
background: safeCount(sources.backgroundTasks),
|
|
72
|
+
job: safeCount(sources.runningJobs),
|
|
73
|
+
};
|
|
74
|
+
return {
|
|
75
|
+
busy: counts.chat > 0 || counts.background > 0 || counts.job > 0,
|
|
76
|
+
version,
|
|
77
|
+
counts,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
function safeCount(read) {
|
|
81
|
+
try {
|
|
82
|
+
const n = read();
|
|
83
|
+
return Number.isInteger(n) && n > 0 ? n : 0;
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
return 0;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
@@ -67,9 +67,43 @@ export interface CliOptions {
|
|
|
67
67
|
* supplies. Off by default — see the argument in config.ts.
|
|
68
68
|
*/
|
|
69
69
|
includeEnv?: boolean;
|
|
70
|
+
/** `service logs`: keep printing what gets appended (`-f`). */
|
|
71
|
+
follow?: boolean;
|
|
72
|
+
/** `service logs`: how many lines to print first (`-n`, default 50). */
|
|
73
|
+
lines?: number;
|
|
74
|
+
/**
|
|
75
|
+
* `service stop` / `service restart`: go ahead even though the instance is
|
|
76
|
+
* busy (SPEC §1, §4).
|
|
77
|
+
*
|
|
78
|
+
* The refusal it skips is the only thing it skips. It does not change what the
|
|
79
|
+
* busy check printed — a forced stop prints the same sentence, prefixed
|
|
80
|
+
* `Overriding:` — and in wave C2 it will not skip `update`'s health check or
|
|
81
|
+
* its rollback either.
|
|
82
|
+
*/
|
|
83
|
+
force?: boolean;
|
|
70
84
|
}
|
|
71
|
-
/**
|
|
85
|
+
/**
|
|
86
|
+
* A failure with an exit code attached (SPEC §2).
|
|
87
|
+
*
|
|
88
|
+
* Thrown rather than exiting, so parsing stays testable. The code travels with
|
|
89
|
+
* the error because the thing that KNOWS which code applies is whatever threw —
|
|
90
|
+
* `main` cannot tell an unsupported platform (a failure, 1) from a misspelled
|
|
91
|
+
* flag (usage, 2) by looking at a string.
|
|
92
|
+
*/
|
|
72
93
|
export declare class CliError extends Error {
|
|
94
|
+
readonly exitCode: number;
|
|
95
|
+
constructor(message: string, exitCode?: number);
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* An unknown command, an unknown flag, or a bad value: exit 2 (SPEC §2).
|
|
99
|
+
*
|
|
100
|
+
* **This is a behaviour change, and a deliberate one.** Every one of these used
|
|
101
|
+
* to exit 1, which is also what a genuine failure exits — so a script could not
|
|
102
|
+
* tell "you typed it wrong" from "it tried and could not". The contract splits
|
|
103
|
+
* them, and a script that tested `$? -eq 1` for a typo now needs `-eq 2`.
|
|
104
|
+
*/
|
|
105
|
+
export declare class UsageError extends CliError {
|
|
106
|
+
constructor(message: string);
|
|
73
107
|
}
|
|
74
108
|
export declare const MIN_NODE_MAJOR = 22;
|
|
75
109
|
/**
|
|
@@ -81,7 +115,7 @@ export declare const MIN_NODE_MAJOR = 22;
|
|
|
81
115
|
* because `parseCommand` dispatches on `argv[0]` alone and only then reads an
|
|
82
116
|
* action, so the two never compete for the same token.
|
|
83
117
|
*/
|
|
84
|
-
export declare const SERVICE_ACTIONS: readonly ["install", "uninstall", "status", "start", "stop", "restart"];
|
|
118
|
+
export declare const SERVICE_ACTIONS: readonly ["install", "uninstall", "status", "start", "stop", "restart", "logs"];
|
|
85
119
|
export type ServiceAction = (typeof SERVICE_ACTIONS)[number];
|
|
86
120
|
/**
|
|
87
121
|
* What `paddock config` can be asked to do (#878).
|
|
@@ -93,8 +127,15 @@ export type ServiceAction = (typeof SERVICE_ACTIONS)[number];
|
|
|
93
127
|
*/
|
|
94
128
|
export declare const CONFIG_ACTIONS: readonly ["show", "eject"];
|
|
95
129
|
export type ConfigAction = (typeof CONFIG_ACTIONS)[number];
|
|
96
|
-
/**
|
|
97
|
-
|
|
130
|
+
/**
|
|
131
|
+
* The leading words {@link parseCommand} recognises. Anything else is a flag.
|
|
132
|
+
*
|
|
133
|
+
* `serve` is the contract's name for running in the foreground (SPEC §1), and
|
|
134
|
+
* `start` is kept as an alias **for good** — the spec says so explicitly for
|
|
135
|
+
* Paddock and Managers, every existing service unit's `ExecStart` names it, and
|
|
136
|
+
* it is in two years of other people's scripts. They parse to the same command.
|
|
137
|
+
*/
|
|
138
|
+
export declare const VERBS: readonly ["serve", "start", "service", "config", "busy"];
|
|
98
139
|
/**
|
|
99
140
|
* A parsed invocation: which verb, plus the flags that followed it.
|
|
100
141
|
*
|
|
@@ -103,7 +144,7 @@ export declare const VERBS: readonly ["start", "service", "config"];
|
|
|
103
144
|
* is the whole request. It is `undefined` only when `opts.help` is true.
|
|
104
145
|
*/
|
|
105
146
|
export type Command = {
|
|
106
|
-
verb: "
|
|
147
|
+
verb: "serve";
|
|
107
148
|
opts: CliOptions;
|
|
108
149
|
} | {
|
|
109
150
|
verb: "service";
|
|
@@ -113,6 +154,11 @@ export type Command = {
|
|
|
113
154
|
verb: "config";
|
|
114
155
|
action: ConfigAction | undefined;
|
|
115
156
|
opts: CliOptions;
|
|
157
|
+
}
|
|
158
|
+
/** `busy` takes no action — it is one question with one answer (SPEC §1). */
|
|
159
|
+
| {
|
|
160
|
+
verb: "busy";
|
|
161
|
+
opts: CliOptions;
|
|
116
162
|
};
|
|
117
163
|
/**
|
|
118
164
|
* Split a leading verb off the argv, then parse the rest as flags.
|
|
@@ -157,6 +203,16 @@ export declare function nodeVersionProblem(nodeVersion: string): string | undefi
|
|
|
157
203
|
* names neither the port nor the flag that fixes it.
|
|
158
204
|
*/
|
|
159
205
|
export declare function explainListenError(err: unknown, host: string, port: string): string;
|
|
160
|
-
|
|
161
|
-
|
|
206
|
+
/**
|
|
207
|
+
* The command list, on its own.
|
|
208
|
+
*
|
|
209
|
+
* Printed after a usage error, because SPEC §2 says the usage follows the
|
|
210
|
+
* message — and because {@link USAGE} is a thousand words of guide that would
|
|
211
|
+
* bury the one line explaining what went wrong. The full text is one
|
|
212
|
+
* `--help` away, and the last line here says so.
|
|
213
|
+
*/
|
|
214
|
+
export declare const USAGE_SYNOPSIS = "Usage\n paddock [serve] [options] run the server in this terminal\n paddock service <action> [options] install|uninstall|status|start|stop|restart|logs\n paddock busy [--json] is the running instance busy?\n paddock config <show|eject> [options] what this instance's config resolved to\n paddock -v | --version print the version and exit\n paddock -h | --help the full help";
|
|
215
|
+
export declare const USAGE = "paddock \u2014 run a Paddock instance locally\n\nUsage\n npx @edspencer/paddock [options] run the server (the default)\n paddock serve [options] the same thing, said out loud\n (`paddock start` is a permanent alias)\n paddock service install [options] run it in the background from login\n paddock service uninstall stop it and remove the unit\n paddock service status [--json] installed? running? which version?\n paddock service start start an installed service\n paddock service stop [--force] stop it, leave it installed\n paddock service restart [--force] stop it and start it again\n paddock service logs [-f] [-n <lines>] the service's log\n (`paddock service --help`)\n paddock busy [--json] is it busy? exit 0 no, 75 yes, 3\n nothing answering\n (`paddock busy --help`)\n paddock config show [--resolved] what this instance's config resolved\n to, and where each value came from\n paddock config eject [--write] freeze that resolution into\n paddock.config.yaml\n (`paddock config --help`)\n\nOptions\n -p, --port <port> HTTP/WS port (default 7233, or $PORT)\n --host <host> Bind address (default 127.0.0.1)\n -d, --data-dir <path> Projects + state (default ~/.paddock, or $PADDOCK_DATA_DIR)\n -o, --open Open the app in your browser once it is listening\n --verbose Show the server's own logs (quiet by default)\n -v, --version Print the Paddock version and exit\n -h, --help Show this help\n\nExit codes\n 0 done; running and answering; not busy\n 1 failed\n 2 usage: an unknown command or flag, or a bad value\n 3 not running: installed, but not running or not answering; nothing answers\n 4 not installed: there is no service unit\n 75 busy: refused, and why was printed. Try later, or use --force\n\nOpening your own directories\n Where you run this from does not matter \u2014 the instance is decided by its data\n dir alone. Directories are added inside the app, on the Discover screen: it\n reads your Claude Code history, lists the directories you have worked in, and\n links the ones you tick as projects. A new instance opens on it.\n\n Importing a directory does not write into it. No .paddock/, no .chats/, no\n .gitignore edits, no CLAUDE.md \u2014 the project record and the copied transcripts\n both live in the data dir, and the project just points at the path. Your own\n ~/.claude transcripts are copied, never moved or deleted, so your terminal\n `claude` keeps working exactly as before.\n\nCredentials\n Paddock drives Claude Code, so it needs Claude credentials \u2014 and if you\n already use Claude Code on this machine, it uses the login you already have.\n That is a macOS Keychain entry on a Mac, or your ~/.claude/.credentials.json\n elsewhere (symlinked in, never copied). Reading a login writes nothing.\n\n Otherwise: a CLAUDE_CODE_OAUTH_TOKEN or ANTHROPIC_API_KEY in the environment,\n or a one-off `CLAUDE_CONFIG_DIR=<data-dir>/claude-home claude login`. With no\n login at all anywhere, run `claude setup-token`. Paddock says at startup when\n it can find none.\n\nPosture profiles\n One key picks how much Paddock shares and how much its agents may do:\n\n profile: balanced # paranoid | balanced | yolo (PADDOCK_PROFILE)\n\n paranoid shares nothing but the login and turns every capability off \u2014 the\n behaviour Paddock had before profiles existed. balanced (the default)\n inherits the CAPABILITIES your CLI already has \u2014 instructions, MCP servers,\n plugins \u2014 and adds the read-only self-management tools, while keeping your\n chat history Paddock's own. yolo turns the rest on: your transcripts, host\n hooks, the write and project tools, schedule mutation, deeper spawning, the\n browser.\n\n A profile only sets defaults \u2014 any single key still overrides it, and an\n individual key in the config file beats PADDOCK_PROFILE in the environment.\n Profiles never touch your port, bind address, auth or model list: yolo does\n NOT open the bind or relax auth, which stay a separate, explicit decision.\n\n What host actually reaches depends on the machine \u2014 a workstation has a real\n ~/.claude, a container usually has none. The profile sets the levers, the\n environment sets the blast radius.\n\n To see what your profile actually expanded to on THIS machine, with the layer\n each value came from: paddock config show --resolved\n\nSharing your Claude Code state\n Apart from that login, Paddock writes nothing outside its data dir by\n default: transcripts go to each project's .chats/, and your ~/.claude is read\n for config only. Each thing it can share is one key in\n <data-dir>/paddock.config.yaml, and each defaults to whatever your profile\n says (see Posture profiles below):\n\n claude:\n transcripts: host # own | host host on yolo only\n credentials: host # own | host host on every profile\n instructions: host # own | host own on paranoid, else host\n hooks: host # own | host host on yolo only\n mcpServers: host # own | host own on paranoid, else host\n\n transcripts: host makes a chat and a `claude --resume` in the same directory\n the same file, live in both directions; deleting such a chat in Paddock\n releases it rather than removing it, because it is your history rather than\n Paddock's copy. credentials: own is the opt-out from sharing the login above.\n\n instructions: host loads your ~/.claude CLAUDE.md, agents/, commands/ and\n plugins/. On from balanced up, so your curated CLAUDE.md does reach Paddock's\n agents; profile: paranoid or instructions: own keeps it out. Each project's\n own CLAUDE.md applies either way.\n\n hooks: host runs the shell commands your ~/.claude/settings.json binds to\n tool use. Off by default \u2014 inheriting someone's hooks is not something to\n discover after the fact. Its other keys (permissions, model, statusline)\n apply either way: under hooks: own Paddock writes its own settings.json\n carrying them with hooks dropped, regenerated at startup.\n\n mcpServers: host attaches the MCP servers declared in your ~/.claude.json \u2014\n the top-level ones, plus a project's own when its directory matches. To give\n this instance a server your machine doesn't have, declare it instead in a\n sibling mcpServers: block of the same file; use env:VAR_NAME anywhere a\n string goes so tokens stay out of it. That keeps a token out of the file, not\n out of `ps`: leave driveMode on its default (session) for a server holding\n one, since batch passes the definition to claude as a command-line argument.\n\nYour data\n Everything lives in one directory \u2014 ~/.paddock unless you pass --data-dir.\n Projects, chat transcripts and settings all persist there between runs. Move\n that directory to move your instance; delete it to start over. Nothing is\n stored anywhere else.\n\nNotes\n Binds loopback with authentication disabled, which is safe for a laptop. To\n expose it on a network, set PADDOCK_AUTH_MODE first \u2014 Paddock refuses to bind\n a routable interface wide open. See AUTH.md.\n\nDocs: https://github.com/edspencer/paddock";
|
|
216
|
+
export declare const SERVICE_USAGE = "paddock service \u2014 keep Paddock running in the background\n\nUsage\n paddock service install [options] register it and start it now\n paddock service uninstall stop it and deregister it\n paddock service status [--json] is it registered, is it running, which\n version, where are the logs\n paddock service start start an installed service that is stopped\n paddock service stop [--force] stop it, and leave it installed\n paddock service restart [--force] stop it and start it again, re-reading config\n paddock service logs [-f] [-n <n>] the service's log (the last 50 lines)\n\nOptions (logs)\n -f, --follow keep printing what gets appended, until Ctrl-C\n -n, --lines <n> how many lines to print first (default 50)\n\nOptions (stop, restart)\n --force stop it even though it is busy\n\nBusy\n `stop` and `restart` ask the running instance whether it is busy first, and\n refuse with exit 75 if it is \u2014 a chat turn in flight, a background task, a\n scheduled job. `paddock busy` asks the same question on its own.\n\n Being refused is the useful case: a restart kills a chat turn mid-sentence,\n and the turn cannot be resumed from where it was. Wait, or --force.\n\n `uninstall` does not ask. Removing the service is an explicit act, and\n someone uninstalling it has already decided.\n\nOptions (install only \u2014 recorded in the generated unit)\n -p, --port <port> HTTP/WS port (default 7233)\n --host <host> Bind address (default 127.0.0.1)\n -d, --data-dir <path> Only if you want an instance SEPARATE from your\n terminal one. Left out of the unit when you omit it\n AND `PADDOCK_DATA_DIR` is unset in this shell, so\n `paddock service` and a bare `paddock` are the same\n ~/.paddock instance reached two ways. If\n `PADDOCK_DATA_DIR` IS set here, that path is recorded\n in the unit \u2014 the service would otherwise point\n somewhere your terminal does not.\n --verbose Record the server's own logs, not just warnings\n\nAt login, not at boot\n This registers a per-USER service \u2014 a launchd LaunchAgent on macOS, a\n `systemd --user` unit on Linux \u2014 so it runs as you, with your own Claude\n login. That is not incidental: on macOS your Claude login is a Keychain item\n that only a logged-in user session can read. A boot-time system daemon has no\n such session and could not use it.\n\n So Paddock starts when you LOG IN, not when the machine boots. After a\n restart that nobody logs into, Paddock is not running. That is the design, not\n a fault.\n\n On Linux, a user service is also stopped when you log out. To keep it up:\n\n loginctl enable-linger $USER\n\nWhere it lives\n macOS ~/Library/LaunchAgents/net.edspencer.paddock.plist\n Linux ~/.config/systemd/user/paddock.service\n\n Both write stdout and stderr to ONE file, <data-dir>/logs/service.log, which\n is what `paddock service logs` reads. Not the journal on Linux: a volatile\n journal has nothing in it after the reboot you are asking about, and one file\n means one answer to \"where are the logs\" on every OS. Paddock rotates it\n itself at 8 MB, keeping three older generations beside it.\n\nExit codes\n 0 it is installed, running and answering\n 3 installed, but not running or not answering\n 4 not installed: there is no unit (status, logs, start, stop, restart)\n 1 something failed\n 2 usage: an unknown action or flag, or a bad value\n 75 busy, and --force was not given (stop, restart)\n\nInstalled from npx?\n `service install` refuses. An npx cache path is hash-keyed and npm may prune\n it, so the unit would work until it silently didn't, at some future login.\n Install properly first:\n\n npm i -g @edspencer/paddock && paddock service install\n\nA note on access\n Paddock binds loopback with authentication off, which is right for a laptop.\n A service is up for as long as you are logged in rather than as long as a\n terminal tab, so that window is longer \u2014 but it is not wider: any local\n process that could reach the port could already read the same Claude login as\n you. Set PADDOCK_AUTH_MODE if you want a credential on it anyway.\n\nDocs: https://github.com/edspencer/paddock";
|
|
217
|
+
export declare const BUSY_USAGE = "paddock busy \u2014 is the running instance in the middle of something?\n\nUsage\n paddock busy ask, and say so in a sentence\n paddock busy --json the same answer as one JSON document\n paddock busy -d <path> ask a particular instance\n\nExit codes \u2014 this command is FOR its exit code\n 0 not busy. Nothing a restart would cut short.\n 75 busy. What is running was printed.\n 3 nothing answered. No instance is running, so there is nothing to wait for.\n 1 something answered, but not as Paddock's busy check.\n\nOptions\n -d, --data-dir <path> Which instance to ask (default ~/.paddock, or\n $PADDOCK_DATA_DIR) \u2014 the same rule `paddock serve`\n uses, so the two always mean the same instance\n --json One JSON document on stdout: running, busy, url,\n version, counts. Prose goes to stderr\n -h, --help Show this help\n\nWhat counts as busy\n Only work a restart would LOSE or cut short:\n\n chat a chat turn running \u2014 a human turn, a fired trigger, a cron\n fire. A turn is a live claude subprocess writing a transcript,\n and it cannot be resumed from where it was interrupted.\n background a background task a live session is still holding after its\n turn ended: a background shell, a sub-agent, a Monitor.\n job a scheduled job herdctl is running.\n\n Not busy: an idle session (it resumes), an open browser tab (it reconnects),\n a pending curation sweep (it re-runs from its watermark).\n\nWhere to run it from\n From a terminal. A Paddock chat that runs this is itself one of the chat turns\n it counts, so it will always see itself as busy \u2014 and `service stop --force`\n from inside a chat stops the service that is running the chat, which does not\n come back with an explanation. That is why the refusal says so.\n\nFor scripts, containers and Watchtower\n `busy` is there for callers with no service to ask. 75 is EX_TEMPFAIL, which\n is the code Watchtower's pre-update hook reads as \"skip this one\":\n\n labels:\n com.centurylinklabs.watchtower.lifecycle.pre-update: paddock busy\n\n An Ansible task does the same thing with a retry. Nothing here contacts the\n network or the npm registry, and the question is answered from memory \u2014 it is\n cheap enough to ask in a loop.\n\nHow it finds the instance\n `<data-dir>/runtime.json`, written 0600 when the server starts listening and\n removed when it stops. There is no fallback to a default port: guessing one\n would ask whatever else happened to be listening there, and get an answer.\n\n A Paddock from before this feature shipped writes no such file, so it reads as\n not running (exit 3). Upgrade it and restart it once.\n\nDocs: https://github.com/edspencer/paddock";
|
|
162
218
|
export declare const CONFIG_USAGE = "paddock config \u2014 what this instance's configuration actually resolved to\n\nUsage\n paddock config show the decisions: your profile, the keys your\n config file sets, the variables your\n environment sets\n paddock config show --resolved EVERY effective value, and which layer it\n came from\n paddock config show --json the same report as JSON, long values in\n full (sensitive ones still hidden)\n paddock config eject what freezing that resolution into\n paddock.config.yaml would write \u2014 a\n preview; writes nothing\n paddock config eject --write actually write it\n\nOptions\n -d, --data-dir <path> Which instance to inspect (default ~/.paddock, or\n $PADDOCK_DATA_DIR) \u2014 the same rule `paddock start`\n uses, so the two always read the same instance\n --resolved show: every field, not just the ones someone set\n --json show: the whole report as JSON (implies --resolved's\n scope)\n --show-sensitive show: print the values of fields marked sensitive\n --write eject: apply the plan. Without it, eject only prints\n --include-env eject: also freeze values an environment variable\n currently supplies (see below \u2014 off by default)\n -h, --help Show this help\n\nWhere a value can come from\n Config resolves in four layers, and each row of --resolved names the one that\n won:\n\n default Paddock's built-in default.\n profile (<name>) Your posture profile. The twelve levers a profile governs\n have no code default of their own any more \u2014 the profile\n IS their default \u2014 so this is a distinct answer from\n \"default\", and switching profile would change it.\n file A key in paddock.config.yaml.\n env <NAME> An environment variable, which beats the file for the\n same key.\n\n One wrinkle worth knowing, and the command shows it plainly: an individual key\n in the FILE beats PADDOCK_PROFILE in the environment. Specific beats general \u2014\n PADDOCK_PROFILE speaks for the levers you did not mention.\n\nWhy show, and not just write it all into the file\n `show` is computed by the loader the server boots with, so it cannot drift\n from what the process would actually do. A materialized file can: it stops\n inheriting improved defaults, says nothing about the variables your container\n sets on top, and no longer describes the whole surface the day a lever is\n added. That is why the thin file is the default and `show` is the answer to\n \"what am I actually running?\".\n\n `eject` is there for when you want it written down anyway \u2014 frozen, in the\n file, reviewable in git. It is a real tradeoff rather than a better `show`,\n so it prints what it would cost you and writes nothing until you pass --write.\n\nWhat eject writes, and what it leaves alone\n It writes the posture levers your profile expanded to, plus the behavioural\n settings the Config screen already round-trips through this file. It does NOT\n write:\n\n the bindings port, host, data dir, projects root, state dir, web dist.\n These resolve to absolute paths on THIS machine, and the\n data dir resolves to the directory holding the file \u2014 so\n ejecting them makes the file unportable and unmountable.\n sensitive keys transcription.endpoint can carry credentials in its URL.\n Bulk-writing a secret-shaped value to disk should not be\n one keystroke away; write that key by hand.\n env-owned keys by default. An environment variable BEATS the file, so\n writing its value in changes nothing today and everything\n the day the variable goes away \u2014 a deferred, silent change\n of provenance. eject names each one it skipped instead.\n --include-env writes them, which is the right flag if you\n are deliberately migrating from env vars to a file.\n\n It always writes `profile:` as well, even though every key that profile\n governs is now explicit. That line is what a lever added in a FUTURE release\n falls back to: without it, a new capability toggle would resolve against the\n built-in default profile rather than the posture you actually ejected from.\n\n Your comments and any keys Paddock does not manage survive \u2014 the file is\n round-tripped, not regenerated \u2014 and the write is atomic.\n\nReading nothing, writing nothing\n `show` resolves config and prints it. It starts no server, contacts nothing,\n and writes no file \u2014 including the data dir itself, which it will report as\n missing rather than create. `eject` writes exactly one file, and only with\n --write.\n\n Values are printed to a terminal that often ends up pasted into an issue, so\n fields marked sensitive are shown as (hidden). None of them is a secret \u2014\n Paddock keeps secrets out of this surface entirely, so there is no API key or\n JWT signing material here to hide or reveal.\n\nDocs: https://github.com/edspencer/paddock";
|