@catheadowl/ukp 0.2.3 → 0.2.5

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.
@@ -48,15 +48,15 @@ export function printTaskPreflightError(input: {
48
48
  allowAnonymous: boolean;
49
49
  }): string | undefined {
50
50
  if (input.platform !== "win32") {
51
- return "--print-task is Windows-only; on Linux prefer systemd socket activation (ukp serve --systemd-socket) — the deployment handbook carries that recipe";
51
+ return "--print-task is Windows-only; on Linux prefer systemd socket activation (ukp serve --systemd-socket) - the deployment handbook carries that recipe";
52
52
  }
53
53
  const loopback =
54
54
  input.host === "127.0.0.1" || input.host === "localhost" || input.host === "::1" || input.host === "[::1]";
55
55
  if (!loopback && !input.hasTls) {
56
- return "refusing to print an off-loopback door without TLS: clients refuse to register plain-http urls — pass --tls (self-signed, pinned at registration) or --tls-cert/--tls-key, or bind loopback behind a TLS-terminating proxy";
56
+ return "refusing to print an off-loopback door without TLS: clients refuse to register plain-http urls - pass --tls (self-signed, pinned at registration) or --tls-cert/--tls-key, or bind loopback behind a TLS-terminating proxy";
57
57
  }
58
58
  if (input.allowAnonymous && !loopback) {
59
- return "refusing to print this shape: --allow-anonymous is loopback-only and serve would refuse it at task start — set the token in the generated script instead";
59
+ return "refusing to print this shape: --allow-anonymous is loopback-only and serve would refuse it at task start - set the token in the generated script instead";
60
60
  }
61
61
  return undefined;
62
62
  }
@@ -86,10 +86,10 @@ function parentDirOf(path: string): string | undefined {
86
86
  export function renderServeTaskArtifacts(input: ServeTaskInput): string {
87
87
  const serveLine = serveCommandLine(input);
88
88
  const out: string[] = [
89
- "ukp serve --print-task — Windows resident-door artifacts",
89
+ "ukp serve --print-task - Windows resident-door artifacts",
90
90
  "",
91
91
  "Print-only: review these, then apply them yourself. UKP installs nothing,",
92
- "starts nothing, and supervises nothing — the door's lifecycle belongs to",
92
+ "starts nothing, and supervises nothing - the door's lifecycle belongs to",
93
93
  "you and your process manager.",
94
94
  "",
95
95
  ];
@@ -97,7 +97,7 @@ export function renderServeTaskArtifacts(input: ServeTaskInput): string {
97
97
  if (input.tls?.mode === "certificates") {
98
98
  const certDir = parentDirOf(input.tls.certPath);
99
99
  out.push(
100
- "0) Certificate — generate ONCE (Git for Windows' openssl works; this",
100
+ "0) Certificate - generate ONCE (Git for Windows' openssl works; this",
101
101
  " avoids any runtime openssl dependency of --tls):",
102
102
  "",
103
103
  " openssl req -x509 -newkey rsa:2048 -nodes -days 825 -keyout",
@@ -115,7 +115,7 @@ export function renderServeTaskArtifacts(input: ServeTaskInput): string {
115
115
  }
116
116
 
117
117
  out.push(
118
- "1) Start script — save as %USERPROFILE%\\.ukp\\start-door.cmd",
118
+ "1) Start script - save as %USERPROFILE%\\.ukp\\start-door.cmd",
119
119
  " (write it with a real editor and CRLF line endings; writing cmd",
120
120
  " files over ssh echo mangles % escaping)",
121
121
  "",
@@ -125,13 +125,13 @@ export function renderServeTaskArtifacts(input: ServeTaskInput): string {
125
125
  " rem (e.g. openssl rand -hex 32), paste it in, keep this file private.",
126
126
  " set \"UKP_SERVE_TOKEN=<paste-your-token>\"",
127
127
  " rem ukp resolves through the logged-on task's per-user PATH; the",
128
- " rem launcher finds bun itself — no PATH export needed.",
128
+ " rem launcher finds bun itself - no PATH export needed.",
129
129
  " if not exist \"%USERPROFILE%\\.ukp\" mkdir \"%USERPROFILE%\\.ukp\"",
130
130
  ` ${serveLine} >> "%USERPROFILE%\\.ukp\\door.log" 2>&1`,
131
131
  "",
132
- "2) Hidden launcher — save as %USERPROFILE%\\.ukp\\start-door-hidden.vbs",
132
+ "2) Hidden launcher - save as %USERPROFILE%\\.ukp\\start-door-hidden.vbs",
133
133
  " (CRLF and a real editor again: the vbs carries literal % signs that",
134
- " ssh echo would expand — copy the file instead)",
134
+ " ssh echo would expand - copy the file instead)",
135
135
  "",
136
136
  " ' UKP door hidden launcher: runs start-door.cmd with NO visible",
137
137
  " ' window (hidden at process creation - nothing flashes), captures",
@@ -150,7 +150,7 @@ export function renderServeTaskArtifacts(input: ServeTaskInput): string {
150
150
  " Next",
151
151
  " WScript.Quit rc",
152
152
  "",
153
- "3) Scheduled task — starts the door at LOGON, hidden",
153
+ "3) Scheduled task - starts the door at LOGON, hidden",
154
154
  " (create it from an elevated shell: creating an ONLOGON task from a",
155
155
  " plain shell fails with Access denied)",
156
156
  "",
@@ -167,29 +167,29 @@ export function renderServeTaskArtifacts(input: ServeTaskInput): string {
167
167
  "- --tls self-signs at first start using an openssl on PATH (Git",
168
168
  " for Windows' openssl qualifies). On a host with no openssl",
169
169
  " anywhere, pre-generate the certificate once instead: re-run with",
170
- " --tls-cert/--tls-key — the handbook's Windows section carries",
170
+ " --tls-cert/--tls-key - the handbook's Windows section carries",
171
171
  " that recipe.",
172
172
  ]
173
173
  : []),
174
174
  "- No visible window, by design: the hidden launcher starts the console",
175
175
  " hidden at process creation (nothing ever flashes on the desktop) and",
176
- " captures everything to door.log — that redirect is the paper trail (a",
176
+ " captures everything to door.log - that redirect is the paper trail (a",
177
177
  " silent EADDRINUSE exit leaves a line).",
178
178
  "- Crash self-recovery is the launcher's bounded retry: a non-zero exit",
179
179
  " is retried up to 3 times, 30s apart; exhausted retries leave the",
180
180
  " failure visible (non-zero task result + door.log). Do not rely on",
181
- " Task Scheduler's restart-on-failure setting — it does not fire on",
181
+ " Task Scheduler's restart-on-failure setting - it does not fire on",
182
182
  " exit codes.",
183
183
  "- To restart the door by hand (new token, ukp upgrade, new cert):",
184
184
  " taskkill /PID <door-pid> /T /F, then schtasks /Run /TN ukp-door",
185
185
  " (schtasks /End does not reliably kill the process tree).",
186
- "- LOGON, not boot: the door starts when someone logs on — nobody",
186
+ "- LOGON, not boot: the door starts when someone logs on - nobody",
187
187
  " logged on means no door. Unattended-boot always-on needs a service",
188
- " wrapper (WinSW-class) — deliberately outside this recipe; prefer the",
188
+ " wrapper (WinSW-class) - deliberately outside this recipe; prefer the",
189
189
  " ssh:// path or a Linux host (systemd socket activation) for that.",
190
190
  "- Re-run ukp serve --print-task with different flags to regenerate;",
191
191
  " edits land by saving the files and re-running schtasks /Run.",
192
- "- Full recipes and hardening: the deployment handbook —",
192
+ "- Full recipes and hardening: the deployment handbook -",
193
193
  " docs/remote-deployment.md in this package, or",
194
194
  " https://github.com/CatheadOwl/ukp/blob/main/docs/remote-deployment.md",
195
195
  "",
@@ -34,23 +34,23 @@ function parseServeTokens(raw: string | undefined): string[] {
34
34
  * over `/e/<name>/` routing. */
35
35
  export const SERVE_SPEC: UkpCommandSpec = {
36
36
  name: "serve",
37
- summary: "serve one endpoint — or the whole registry as a host door, one HTTP server for every endpoint — for remote UKP clients",
37
+ summary: "serve one endpoint - or the whole registry as a host door, one HTTP server for every endpoint - for remote UKP clients",
38
38
  group: "operations",
39
39
  description:
40
- "Expose a registered endpoint over HTTP using the ukp-remote wire, or — without --endpoint — serve every local endpoint as one host door routed by name.",
41
- usage: "[--endpoint <name>] [--host <addr>] [--port <n>] [--tls [--tls-san <ip|dns>]… | --tls-cert <pem> --tls-key <pem>] [--max-idle <seconds>] [--systemd-socket] [--print-task]",
40
+ "Expose a registered endpoint over HTTP using the ukp-remote wire, or - without --endpoint - serve every local endpoint as one host door routed by name.",
41
+ usage: "[--endpoint <name>] [--host <addr>] [--port <n>] [--tls [--tls-san <ip|dns>]... | --tls-cert <pem> --tls-key <pem>] [--max-idle <seconds>] [--systemd-socket] [--print-task]",
42
42
  options: [
43
43
  { flags: "--endpoint <name>", help: "the registered endpoint to expose; omit it to serve the whole registry as a host door (/e/<name>/ routing, all endpoints, one port)" },
44
44
  { flags: "--host <addr>", help: "listen address (default 127.0.0.1, loopback only without a token)" },
45
45
  { flags: "--port <n>", help: "listen port (default 8570)" },
46
46
  { flags: "--tls", help: "serve HTTPS with a self-signed identity (auto-generated under .ukp/tls/, SAN covers this host's addresses; clients pin it at registration)" },
47
47
  { flags: "--tls-san <ip|dns>", help: "extra SAN entry for the --tls self-signed identity (repeatable): a public/NAT IP or hostname no NIC of this host carries; a persisted certificate missing an entry is re-signed over the same key (pin unchanged, pinned clients re-anchor)", multi: true },
48
- { flags: "--tls-cert <pem>", help: "TLS certificate (chain) PEM path — Let's Encrypt, mkcert, or a private CA; pair with --tls-key" },
48
+ { flags: "--tls-cert <pem>", help: "TLS certificate (chain) PEM path - Let's Encrypt, mkcert, or a private CA; pair with --tls-key" },
49
49
  { flags: "--tls-key <pem>", help: "TLS private key PEM path; pair with --tls-cert" },
50
50
  { flags: "--allow-anonymous", help: "permit tokenless access on loopback (local testing, or an ssh-forwarded host door where SSH carries encryption and auth; reverse-proxy deployments still require UKP_SERVE_TOKEN)" },
51
- { flags: "--max-idle <seconds>", help: "exit after <seconds> without requests (self-reap; the orphan backstop for on-demand-woken doors — fractional values accepted for tests)" },
52
- { flags: "--systemd-socket", help: "serve on the systemd socket-activation listener (LISTEN_FDS fd 3) instead of binding a port — the port belongs to your .socket unit; Linux-only" },
53
- { flags: "--print-task", help: "Windows: print this door's process-manager artifacts (start script, hidden launcher, Task Scheduler command, firewall rule) instead of serving — review and apply them yourself; nothing is installed or started for you, and the token is yours to paste (never printed)" },
51
+ { flags: "--max-idle <seconds>", help: "exit after <seconds> without requests (self-reap; the orphan backstop for on-demand-woken doors - fractional values accepted for tests)" },
52
+ { flags: "--systemd-socket", help: "serve on the systemd socket-activation listener (LISTEN_FDS fd 3) instead of binding a port - the port belongs to your .socket unit; Linux-only" },
53
+ { flags: "--print-task", help: "Windows: print this door's process-manager artifacts (start script, hidden launcher, Task Scheduler command, firewall rule) instead of serving - review and apply them yourself; nothing is installed or started for you, and the token is yours to paste (never printed)" },
54
54
  ],
55
55
  helpSuffix: [
56
56
  "",
@@ -58,9 +58,9 @@ export const SERVE_SPEC: UkpCommandSpec = {
58
58
  ` GET ${DISCOVERY_PATH} discovery document (manifest projection,`,
59
59
  " protocol version, instance identity)",
60
60
  " POST /v1/search {query, limit} -> ukp.search.v1 envelope",
61
- " GET /v1/read?ref=…|uri=… endpoint-relative ref or ukp:// URI",
61
+ " GET /v1/read?ref=...|uri=... endpoint-relative ref or ukp:// URI",
62
62
  " GET /v1/nav?path=&depth= markdown route view (ukp.nav.v1)",
63
- " GET /v1/rg?query=… lexical search (ukp.rg.v1, ukp_uri handoff)",
63
+ " GET /v1/rg?query=... lexical search (ukp.rg.v1, ukp_uri handoff)",
64
64
  "",
65
65
  "Wire (ukp-remote v1), host door (no --endpoint):",
66
66
  ` GET ${DISCOVERY_PATH} door document (scope:"host", endpoint roster)`,
@@ -75,26 +75,26 @@ export const SERVE_SPEC: UkpCommandSpec = {
75
75
  " --allow-anonymous, loopback binds only. The discovery document stays",
76
76
  " public. A reverse proxy on the same host forwards from the public side",
77
77
  " to the loopback bind, so proxy deployments treat the token as",
78
- " mandatory — serve cannot see past its own bind address.",
78
+ " mandatory - serve cannot see past its own bind address.",
79
79
  "",
80
80
  " Loopback-without-token covers testing/dogfood AND the ssh door",
81
81
  " deployment: bind the door on the remote host's loopback and let ssh",
82
82
  " forwarding carry encryption and authentication (clients register",
83
- " 'ukp register --url ssh://<host>' — zero tokens, docker",
83
+ " 'ukp register --url ssh://<host>' - zero tokens, docker",
84
84
  " DOCKER_HOST=ssh:// posture). It is not the way to consume a",
85
- " same-machine endpoint — register its local path instead.",
85
+ " same-machine endpoint - register its local path instead.",
86
86
  " TLS: pass --tls to serve HTTPS with a self-signed identity",
87
- " (generated under .ukp/tls/ — under the registry directory in door",
88
- " mode —, SAN covers this host's addresses; remote clients TOFU-pin it",
87
+ " (generated under .ukp/tls/ - under the registry directory in door",
88
+ " mode -, SAN covers this host's addresses; remote clients TOFU-pin it",
89
89
  " at registration and refresh by re-registering), or --tls-cert/--tls-key",
90
- " for your own certificate (Let's Encrypt — IP certs available since",
91
- " 2026-01 —, mkcert, a private CA). Plain HTTP remains loopback-only by",
90
+ " for your own certificate (Let's Encrypt - IP certs available since",
91
+ " 2026-01 -, mkcert, a private CA). Plain HTTP remains loopback-only by",
92
92
  " admission; public exposure needs TLS or the ssh:// transport.",
93
93
  "",
94
94
  " A cloud NAT/EIP host's public IP is on no NIC (--tls alone cannot",
95
95
  " cover it): add --tls-san <public-ip> next to --tls. Repeatable; DNS",
96
96
  " names accepted. The persisted certificate re-signs over the same key",
97
- " when a new entry appears — the pin is unchanged and pinned clients",
97
+ " when a new entry appears - the pin is unchanged and pinned clients",
98
98
  " re-anchor transparently; dropping entries never re-signs.",
99
99
  "",
100
100
  ].join("\n"),
@@ -136,7 +136,7 @@ function toParsedServe(parsed: KitParsed): ParsedServe {
136
136
  }
137
137
  const printTask = options.printTask === true;
138
138
  if (printTask && systemdSocket) {
139
- throw new KitUsageError("--print-task is the Windows resident form; --systemd-socket is the Linux one (systemd holds the port — see the deployment handbook)");
139
+ throw new KitUsageError("--print-task is the Windows resident form; --systemd-socket is the Linux one (systemd holds the port - see the deployment handbook)");
140
140
  }
141
141
  let maxIdleSeconds: number | undefined;
142
142
  if (options.maxIdle !== undefined) {
@@ -183,10 +183,10 @@ export function renderServeBanner(info: ServeInfo): string {
183
183
  `serving host door (ukp-remote v1)`,
184
184
  ` listening: ${info.url}`,
185
185
  ` discovery: ${info.url.startsWith("http") ? `${info.url}${DISCOVERY_PATH}` : info.url} (host door)`,
186
- ` endpoints: ${info.door!.endpoints.length > 0 ? info.door!.endpoints.join(", ") : "(none — register endpoints on this host)"}`,
186
+ ` endpoints: ${info.door!.endpoints.length > 0 ? info.door!.endpoints.join(", ") : "(none - register endpoints on this host)"}`,
187
187
  ` write: ${info.door!.write.length > 0 ? info.door!.write.join(", ") + " (propose via PUT /e/<name>/v1/propose/<id>)" : "(no endpoint declares propose)"}`,
188
188
  ` auth: ${info.authRequired ? "bearer token required" : "no token (loopback bind; ssh-forwarded clients authenticate by SSH key)"}`,
189
- ` rg: ${info.rg === "ok" ? "ok" : "missing (rg calls skip with a warning — install ripgrep on this door's PATH)"}`,
189
+ ` rg: ${info.rg === "ok" ? "ok" : "missing (rg calls skip with a warning - install ripgrep on this door's PATH)"}`,
190
190
  ...(info.maxIdleSeconds !== undefined
191
191
  ? [` idle: exits after ${info.maxIdleSeconds}s without requests (--max-idle)`]
192
192
  : []),
@@ -201,7 +201,7 @@ export function renderServeBanner(info: ServeInfo): string {
201
201
  ` listening: ${info.url}`,
202
202
  ` discovery: ${info.url.startsWith("http") ? `${info.url}${DISCOVERY_PATH}` : info.url}`,
203
203
  ` auth: ${info.authRequired ? "bearer token required" : "no token (loopback only)"}`,
204
- ` rg: ${info.rg === "ok" ? "ok" : "missing (rg calls skip with a warning — install ripgrep on this door's PATH)"}`,
204
+ ` rg: ${info.rg === "ok" ? "ok" : "missing (rg calls skip with a warning - install ripgrep on this door's PATH)"}`,
205
205
  ...(info.maxIdleSeconds !== undefined
206
206
  ? [` idle: exits after ${info.maxIdleSeconds}s without requests (--max-idle)`]
207
207
  : []),
@@ -295,7 +295,7 @@ export function executeServeCommand(
295
295
  // premise of --allow-anonymous is unverifiable here (RQ-18 review P1).
296
296
  if (systemdSocket && tokens.length === 0) {
297
297
  throw new Error(
298
- "refusing to serve on --systemd-socket without a token: the listener's bind address belongs to the socket unit and may be public — tokenless loopback serving cannot be verified here; set UKP_SERVE_TOKEN",
298
+ "refusing to serve on --systemd-socket without a token: the listener's bind address belongs to the socket unit and may be public - tokenless loopback serving cannot be verified here; set UKP_SERVE_TOKEN",
299
299
  );
300
300
  }
301
301
  const decision = serveAuthDecision(host, tokens, allowAnonymous);
package/src/registry.ts CHANGED
@@ -230,7 +230,7 @@ export function assertRegistrableRemoteUrl(raw: string): void {
230
230
  + `${parts.endpointName !== undefined ? `/${parts.endpointName}` : ""}`;
231
231
  throw new RegistryError(
232
232
  `ssh:// urls take no port: the woken door listens on a client-chosen loopback port and the url port selects nothing`
233
- + ` — register '${portless}' instead`
233
+ + ` - register '${portless}' instead`
234
234
  + ` (for a non-standard sshd port, use an ssh config Host alias)`,
235
235
  );
236
236
  }
@@ -240,7 +240,7 @@ export class RegistryBusyError extends RegistryError {
240
240
  constructor(path: string) {
241
241
  super(
242
242
  `registry_busy: ${path} (another ukp process holds the registry lock; ` +
243
- `stale locks are reclaimed automatically — close other ukp commands or retry shortly)`,
243
+ `stale locks are reclaimed automatically - close other ukp commands or retry shortly)`,
244
244
  );
245
245
  this.name = "RegistryBusyError";
246
246
  }
package/src/server.ts CHANGED
@@ -33,6 +33,7 @@ import {
33
33
  runRg,
34
34
  projectRgEnvelope,
35
35
  RG_DEFAULT_LIMIT,
36
+ RG_FILES_DEFAULT_LIMIT,
36
37
  RG_MAX_LIMIT,
37
38
  validateRgPassthrough,
38
39
  RgUsageError,
@@ -407,7 +408,7 @@ function parseReadParams(
407
408
  pin: string | null,
408
409
  ): { request: ReadRequest } | { error: string; class?: string } {
409
410
  if ((ref === null) === (uri === null)) {
410
- return { error: "exactly one of 'ref' (endpoint-relative) or 'uri' (ukp://…) is required" };
411
+ return { error: "exactly one of 'ref' (endpoint-relative) or 'uri' (ukp://...) is required" };
411
412
  }
412
413
  const args = uri !== null
413
414
  ? [
@@ -463,18 +464,44 @@ function parseNavParams(
463
464
  }
464
465
 
465
466
  /** Wire rg params → ParsedRg with serve-fixed scope. Params are flat (no
466
- * argv reconstruction needed): query/limit/glob/type/i/count map onto
467
+ * argv reconstruction needed): query/limit/glob/type/i/count/files map onto
467
468
  * ParsedRg directly and passthrough re-validates against the same
468
- * allowlist (ADR-RG-002) before any endpoint work starts. */
469
+ * allowlist (ADR-RG-002) before any endpoint work starts. Files mode
470
+ * (ADR-RG-005) mirrors the CLI parse layer's mutual exclusions so the wire
471
+ * keeps the same strictness as query/limit. */
469
472
  function parseRgParams(
470
473
  endpointName: string,
471
474
  url: URL,
472
475
  ): { parsed: ParsedRg } | { error: string } {
476
+ // Boolean params are strictly "1"/"0" — same strictness as query/limit, so
477
+ // a hand-crafted `i=true` is a visible usage error, not a silent mode miss.
478
+ const booleanOf = (name: string): boolean | { error: string } => {
479
+ const raw = url.searchParams.get(name);
480
+ if (raw === null) return false;
481
+ if (raw === "1") return true;
482
+ if (raw === "0") return false;
483
+ return { error: `'${name}' must be 1 or 0` };
484
+ };
485
+ const files = booleanOf("files");
486
+ if (typeof files === "object") return files;
487
+ const ignoreCase = booleanOf("i");
488
+ if (typeof ignoreCase === "object") return ignoreCase;
489
+ const count = booleanOf("count");
490
+ if (typeof count === "object") return count;
473
491
  const query = url.searchParams.get("query");
474
- if (query === null || query.length === 0) {
475
- return { error: "'query' is required and must be a non-empty pattern" };
492
+ if (files && count) {
493
+ return { error: "'files' and 'count' cannot be used together (pick one output mode)" };
494
+ }
495
+ if (files && ignoreCase) {
496
+ return { error: "'files' cannot be combined with 'i': pass an --iglob passthrough arg for a case-insensitive glob filter" };
497
+ }
498
+ if (files && query !== null) {
499
+ return { error: "'files' takes no 'query' - use 'glob' to filter by name" };
476
500
  }
477
- let limit = RG_DEFAULT_LIMIT;
501
+ if (!files && (query === null || query.length === 0)) {
502
+ return { error: "'query' is required and must be a non-empty pattern (or pass files=1 for enumeration mode)" };
503
+ }
504
+ let limit = files ? RG_FILES_DEFAULT_LIMIT : RG_DEFAULT_LIMIT;
478
505
  const rawLimit = url.searchParams.get("limit");
479
506
  if (rawLimit !== null) {
480
507
  if (!/^[0-9]+$/.test(rawLimit)) return { error: "'limit' must be a decimal integer" };
@@ -490,31 +517,19 @@ function parseRgParams(
490
517
  if (error instanceof RgUsageError) return { error: error.message };
491
518
  throw error;
492
519
  }
493
- // Boolean params are strictly "1"/"0" — same strictness as query/limit, so
494
- // a hand-crafted `i=true` is a visible usage error, not a silent mode miss.
495
- const booleanOf = (name: string): boolean | { error: string } => {
496
- const raw = url.searchParams.get(name);
497
- if (raw === null) return false;
498
- if (raw === "1") return true;
499
- if (raw === "0") return false;
500
- return { error: `'${name}' must be 1 or 0` };
501
- };
502
- const ignoreCase = booleanOf("i");
503
- if (typeof ignoreCase === "object") return ignoreCase;
504
- const count = booleanOf("count");
505
- if (typeof count === "object") return count;
506
- const glob = url.searchParams.get("glob");
520
+ const globs = url.searchParams.getAll("glob");
507
521
  const type = url.searchParams.get("type");
508
522
  return {
509
523
  parsed: {
510
- request: { query, limit },
524
+ request: { query: files ? "" : query!, limit },
511
525
  options: {
512
526
  explicitEndpoints: [endpointName],
513
527
  global: false,
514
- ...(glob !== null ? { glob } : {}),
528
+ ...(globs.length > 0 ? { globs } : {}),
515
529
  ...(type !== null ? { type } : {}),
516
530
  ...(ignoreCase ? { ignoreCase: true } : {}),
517
531
  ...(count ? { count: true } : {}),
532
+ ...(files ? { files: true } : {}),
518
533
  passthrough,
519
534
  },
520
535
  warnings: [],
@@ -546,14 +561,14 @@ export function parseListenFds(env: NodeJS.ProcessEnv): { fd: number } | { error
546
561
  };
547
562
  }
548
563
  if (Number(env.LISTEN_PID) !== process.pid) {
549
- return { error: `LISTEN_PID ${env.LISTEN_PID} does not match this process (${process.pid}) — inherited activation variables belong to another process` };
564
+ return { error: `LISTEN_PID ${env.LISTEN_PID} does not match this process (${process.pid}) - inherited activation variables belong to another process` };
550
565
  }
551
566
  const count = Number(env.LISTEN_FDS);
552
567
  if (!Number.isSafeInteger(count) || count < 1) {
553
568
  return { error: `LISTEN_FDS=${String(env.LISTEN_FDS)} is not a positive integer` };
554
569
  }
555
570
  if (count !== 1) {
556
- return { error: `expected exactly one listening fd (LISTEN_FDS=1), got ${count} — run one socket unit per serve instance` };
571
+ return { error: `expected exactly one listening fd (LISTEN_FDS=1), got ${count} - run one socket unit per serve instance` };
557
572
  }
558
573
  delete env.LISTEN_FDS;
559
574
  delete env.LISTEN_PID;
@@ -645,7 +660,7 @@ function startServeListener(
645
660
  isSocket = false;
646
661
  }
647
662
  if (!isSocket) {
648
- throw new ServeSetupError(`fd ${parsed.fd} is not an open socket — socket activation must pass a bound listening socket`);
663
+ throw new ServeSetupError(`fd ${parsed.fd} is not an open socket - socket activation must pass a bound listening socket`);
649
664
  }
650
665
  const server = Bun.serve({ hostname: "127.0.0.1", port: 0, ...tls, fetch });
651
666
  const acceptor = netCreateServer();
@@ -1170,7 +1185,7 @@ function handleRg(url: URL, request: Request, config: ServeConfig, endpointName:
1170
1185
  currentDirectory: config.currentDirectory,
1171
1186
  registryPath: config.registryPath,
1172
1187
  });
1173
- return jsonResponse(projectRgEnvelope(result, parsed.parsed.options.count === true));
1188
+ return jsonResponse(projectRgEnvelope(result));
1174
1189
  }
1175
1190
 
1176
1191
  /** PUT /v1/propose/{id} (W8 / ADR-REM-005): the write face. Body = the
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Win32 tool-spawn relay (2026-09-22 finding, ukp_remote TODO
3
+ * 20260922-win32-ssh-spawn-tax): Windows Defender holds the DIRECT
4
+ * bun-to-<tool>.exe child edge for seconds per spawn — measured 9.7-10.7s for
5
+ * ssh.exe and ~5.4s for rg.exe across stdio modes, windowsHide, and absolute
6
+ * paths, while the same binaries start in 0.16-0.26s from bash and in
7
+ * 0.55-0.8s from bun through ANY intermediary parent (cmd, node, powershell).
8
+ * The inspection keys on the parent-child edge, so routing the spawn through
9
+ * a one-level intermediary escapes it.
10
+ *
11
+ * The intermediary is powershell.exe driven by -EncodedCommand: the script
12
+ * rides as one base64 token (no quoting stress from the spawning side), and
13
+ * the script splats a single-quoted array so argv reaches the tool
14
+ * byte-identical — the only escape in a PS single-quoted literal is doubling
15
+ * inner single quotes; double quotes, backslashes, and cmd metacharacters
16
+ * are all literal (verified E2E: the pinned wake command's load-bearing
17
+ * quotes survive, door READY ~1.4s). powershell.exe ships with every
18
+ * Windows; no extra prerequisite is imposed.
19
+ *
20
+ * Exit-code contract: the script exits with the tool's own exit status. With
21
+ * the not-found guard requested, a tool that does not resolve on PATH exits
22
+ * TOOL_NOT_FOUND_EXIT before any invocation — distinct from every real tool
23
+ * exit, so callers can keep treating "tool missing" as a degradation instead
24
+ * of a fault.
25
+ */
26
+
27
+ /** Distinct not-found sentinel: no real tool invocation ran when this is the
28
+ * exit status (ripgrep itself only ever exits 0/1/2; ssh diagnostics arrive
29
+ * via stderr instead). */
30
+ export const TOOL_NOT_FOUND_EXIT = 127;
31
+
32
+ /** PS single-quote literal: the only escape is doubling inner single quotes. */
33
+ function psSingleQuoted(arg: string): string {
34
+ return `'${arg.replaceAll("'", "''")}'`;
35
+ }
36
+
37
+ /** The relay script. `notFoundGuard` adds a Get-Command pre-check that exits
38
+ * TOOL_NOT_FOUND_EXIT when argv[0] does not resolve — for callers that must
39
+ * distinguish a missing tool from a tool's own failure exit. */
40
+ export function toolRelayScript(argv: readonly string[], options: { notFoundGuard?: boolean } = {}): string {
41
+ const guard = options.notFoundGuard === true
42
+ ? " if (-not (Get-Command -LiteralName $a[0] -ErrorAction SilentlyContinue)) { exit 127 };"
43
+ : "";
44
+ return `$a = @(${argv.map(psSingleQuoted).join(",")});${guard} & $a[0] $a[1..($a.Count-1)]; exit $LASTEXITCODE`;
45
+ }
46
+
47
+ /** The powershell carrier argv for a tool invocation: spawn THIS instead of
48
+ * the tool when the direct bun-to-tool edge is taxed (win32 real-tool
49
+ * spawns). Test-injected commands must not be wrapped. */
50
+ export function toolRelayArgv(argv: readonly string[], options: { notFoundGuard?: boolean } = {}): string[] {
51
+ return [
52
+ "powershell",
53
+ "-NoProfile",
54
+ "-EncodedCommand",
55
+ Buffer.from(toolRelayScript(argv, options), "utf16le").toString("base64"),
56
+ ];
57
+ }