privateer-agent 0.12.28 → 0.12.30

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.
@@ -35,6 +35,21 @@ const HERE = path.dirname(fileURLToPath(import.meta.url)); // bin/
35
35
  const REPO = path.resolve(HERE, "..");
36
36
  const isWin = process.platform === "win32";
37
37
 
38
+ // On Windows, ensure the console output code page is UTF-8 (65001).
39
+ // Without this, raw UTF-8 writes to stdout/stderr (such as fs.writeSync(2, ...) in the
40
+ // boot splash worker) are decoded through the OEM code page (CP437/850), turning
41
+ // Unicode wave blocks and emojis into 3-byte mojibake that wraps and floods the console.
42
+ if (isWin) {
43
+ try {
44
+ const chcp = process.env.SystemRoot
45
+ ? path.join(process.env.SystemRoot, "System32", "chcp.com")
46
+ : "chcp.com";
47
+ spawnSync(chcp, ["65001"], { stdio: "ignore", windowsHide: true });
48
+ } catch {
49
+ /* best effort */
50
+ }
51
+ }
52
+
38
53
  const PRIVATEER_HOME = process.env.PRIVATEER_HOME || path.join(os.homedir(), ".privateer");
39
54
  const ENV_FILE = path.join(REPO, ".env"); // dev-only; a real install has none
40
55
  const AGENT_DIR = path.join(PRIVATEER_HOME, "agent");
@@ -38,6 +38,8 @@
38
38
  //
39
39
  // The wave is drawn on STDERR; stdout belongs to the TUI's canvas.
40
40
 
41
+ import { spawnSync } from "node:child_process";
42
+ import path from "node:path";
41
43
  import { Worker } from "node:worker_threads";
42
44
 
43
45
  const enabled =
@@ -46,6 +48,20 @@ const enabled =
46
48
  !process.env.PRIVATEER_NO_SPLASH &&
47
49
  !process.env.CI;
48
50
 
51
+ // On Windows, ensure the console output code page is UTF-8 (65001) before the worker
52
+ // thread starts drawing. If privateer was launched via npm/npx or direct node rather
53
+ // than privateer.cmd, the console might still be on OEM CP437.
54
+ if (enabled && process.platform === "win32") {
55
+ try {
56
+ const chcp = process.env.SystemRoot
57
+ ? path.join(process.env.SystemRoot, "System32", "chcp.com")
58
+ : "chcp.com";
59
+ spawnSync(chcp, ["65001"], { stdio: "ignore", windowsHide: true });
60
+ } catch {
61
+ /* best effort */
62
+ }
63
+ }
64
+
49
65
  // Bytes of stdout after TUI.start() that mean "this is the first frame, not a control
50
66
  // sequence". Everything Pi writes between raw mode and the frame is short (the paste
51
67
  // toggle, a Kitty protocol query, the cursor hide, an OSC window title — 42 bytes all
@@ -72,7 +88,8 @@ if (enabled) {
72
88
 
73
89
  // Room for " ⚓ " + wave + message + elapsed, clamped so a narrow terminal doesn't
74
90
  // wrap (a wrapped line survives our `\r\x1b[K` erase only on its last row).
75
- const width = Math.max(12, Math.min(28, (err.columns || 80) - 34));
91
+ const cols = err.columns && err.columns > 0 ? err.columns : 80;
92
+ const width = Math.max(6, Math.min(28, cols - 34));
76
93
 
77
94
  // The worker source is plain logic with no escape sequences of its own — every ANSI
78
95
  // string is handed over in workerData, so nothing here has to survive two rounds of
package/bin/privateer.cmd CHANGED
@@ -1,4 +1,7 @@
1
1
  @echo off
2
+ REM Ensure console output code page is UTF-8 so Unicode blocks and emojis render
3
+ REM correctly without CP437 mojibake or line-wrapping cascades.
4
+ chcp 65001 >nul 2>&1
2
5
  REM Windows entry point for a Privateer bundle. Mirrors the unix bin/privateer-tui
3
6
  REM shim: run the bundled Node against the shared cross-platform launcher. %~dp0 is
4
7
  REM this file's dir (<app>\bin\), so ..\node.exe is the bundled runtime.
@@ -13,25 +13,49 @@
13
13
  // advertises /init. After /init we emit the shared context-changed signal so that line
14
14
  // refreshes at once. See src/context.ts for the discovery/formatting details.
15
15
 
16
- import { contextBlock, writeTemplate, emitContextChanged, CONTEXT_BLOCK_MARKER } from "../src/context.ts";
16
+ import {
17
+ contextBlock,
18
+ writeTemplate,
19
+ emitContextChanged,
20
+ CONTEXT_BLOCK_MARKER,
21
+ RUNTIME_GUIDELINES_MARKER,
22
+ runtimeGuidelinesBlock,
23
+ } from "../src/context.ts";
17
24
 
18
25
  // Honor Pi's own "disable context files" switch, so --no-context-files / -nc silences
19
- // PRIVATEER.md too (not just AGENTS.md/CLAUDE.md) — otherwise the flag would half-work.
26
+ // everything this extension injects, not just PRIVATEER.md — otherwise the flag would
27
+ // half-work. A user who asks for a bare prompt gets a bare prompt.
20
28
  const CONTEXT_FILES_DISABLED =
21
29
  process.argv.includes("--no-context-files") || process.argv.includes("-nc");
22
30
 
23
31
  export default function privateerContext(pi: any): void {
24
- // Inject PRIVATEER.md into every turn's system prompt. The prompt is rebuilt per turn
25
- // and chained across before_agent_start handlers, so appending here is idempotent for
26
- // the turn; the marker guard makes it a no-op if an earlier handler already added it.
32
+ // Inject the runtime guidelines + PRIVATEER.md into every turn's system prompt. The
33
+ // prompt is rebuilt per turn and chained across before_agent_start handlers, so
34
+ // appending here is idempotent for the turn; each marker guard makes its own block a
35
+ // no-op if an earlier handler already added it.
36
+ //
37
+ // ONLY EVER APPEND, AND ONLY RETURN WHEN WE ADDED SOMETHING. Pi chains these handlers
38
+ // and a returned `systemPrompt` REPLACES what the chain has built so far, so both
39
+ // halves matter:
40
+ //
41
+ // • A host that doesn't populate `event.systemPrompt` must not be handed a prompt
42
+ // synthesised from "" — that gives back our two blocks as the ENTIRE system prompt
43
+ // and silently drops the real one. `typeof base !== "string"` is what separates
44
+ // "here is a prompt to chain onto" (possibly empty, legitimate) from "no field".
45
+ // • When both markers are already present (a re-entrant chain) we have nothing to
46
+ // contribute, and undefined leaves what is there alone.
27
47
  pi.on("before_agent_start", (event: any) => {
28
48
  if (CONTEXT_FILES_DISABLED) return;
29
- const cwd = event?.systemPromptOptions?.cwd ?? process.cwd();
30
- const base: string = event?.systemPrompt ?? "";
31
- if (base.includes(CONTEXT_BLOCK_MARKER)) return; // already injected this chain
32
- const block = contextBlock(cwd);
33
- if (!block) return; // no PRIVATEER.md anywhere — leave the prompt untouched
34
- return { systemPrompt: base + block };
49
+ const base = event?.systemPrompt;
50
+ if (typeof base !== "string") return;
51
+ let prompt = base;
52
+ if (!prompt.includes(RUNTIME_GUIDELINES_MARKER)) prompt += runtimeGuidelinesBlock();
53
+ if (!prompt.includes(CONTEXT_BLOCK_MARKER)) {
54
+ const cwd = event?.systemPromptOptions?.cwd ?? process.cwd();
55
+ prompt += contextBlock(cwd); // "" when there is no PRIVATEER.md anywhere
56
+ }
57
+ if (prompt === base) return; // nothing to add — leave the chain alone
58
+ return { systemPrompt: prompt };
35
59
  });
36
60
 
37
61
  // /init — scaffold a PRIVATEER.md in the working directory. Never clobbers an existing
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "privateer-agent",
3
- "version": "0.12.28",
3
+ "version": "0.12.30",
4
4
  "description": "Privacy-first terminal coding agent — bring your own model across 20 providers (Anthropic, OpenAI, OpenRouter, Google, local Ollama…). Safe-by-default permissions, MCP, sub-agents, workflows, and verifiable TEE inference. Built on the Pi toolkit.",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/src/context.ts CHANGED
@@ -93,6 +93,15 @@ export function discoverContextFiles(cwd: string = process.cwd()): ContextFile[]
93
93
  // A unique sentinel opening the injected block, so before_agent_start can no-op if the
94
94
  // block is already present in the chained system prompt (defensive against re-entrancy).
95
95
  export const CONTEXT_BLOCK_MARKER = "<!-- privateer:PRIVATEER.md -->";
96
+ export const RUNTIME_GUIDELINES_MARKER = "<!-- privateer:runtime-guidelines -->";
97
+
98
+ export function runtimeGuidelinesBlock(): string {
99
+ return `\n\n${RUNTIME_GUIDELINES_MARKER}\n<environment_guidelines>
100
+ - Node.js environment: Node.js (v22+) is guaranteed to be available in Privateer. Prefer \`node -e "..."\` or small Node.js scripts for quick scripting, calculations, or JSON processing instead of assuming \`python\` or \`python3\` is installed.
101
+ - Search fallback: If \`rg\` (ripgrep) is missing or returns "command not found", fall back to standard POSIX \`grep -rn <pattern> <path>\` or \`find <path>\`.
102
+ - Missing system dependencies: If an essential external tool (e.g. \`git\`, \`python\`, \`rg\`) is missing and needed, check its presence (\`command -v <tool>\`), explain clearly what is missing, and offer to install it using the host package manager (e.g. \`brew install\`, \`xcode-select --install\`, \`winget install\`, \`apt install\`) upon user approval.
103
+ </environment_guidelines>\n`;
104
+ }
96
105
 
97
106
  // Format the discovered files into a system-prompt fragment using the same framing Pi
98
107
  // applies to AGENTS.md (see core/system-prompt.js), so the model can't tell the two
@@ -11,6 +11,7 @@
11
11
  // to "deny".
12
12
 
13
13
  import { randomUUID } from "node:crypto";
14
+ import { noQuarterActive, setNoQuarter } from "../permissions/noQuarter.ts";
14
15
  import type { EngineEvent } from "../engine/events.ts";
15
16
  import type { PermissionRequest } from "../permissions/gate.ts";
16
17
  import type { AskOutcome } from "../permissions/modeGate.ts";
@@ -169,7 +170,7 @@ export interface RemoteBridgeConfig {
169
170
  export class RemoteBridge {
170
171
  private relay?: RelayLike;
171
172
  private remote = false;
172
- private noQuarter = false;
173
+ private noQuarter = noQuarterActive();
173
174
  private readonly pending = new Map<string, (d: AskOutcome) => void>();
174
175
  private readonly pendingSelects = new Map<string, (v: string | null) => void>();
175
176
  private readonly pendingInputs = new Map<string, (v: string | null) => void>();
@@ -282,9 +283,15 @@ export class RemoteBridge {
282
283
  onFilesSearch: (id, query) => this.cfg.onFilesSearch?.(id, query),
283
284
  onNoQuarter: (on) => {
284
285
  this.noQuarter = on;
286
+ setNoQuarter(on);
285
287
  this.relay?.sendNoQuarter(on); // echo the ack back so the app's toggle syncs
286
288
  },
287
- onControllerAttached: () => this.cfg.onControllerAttached?.(),
289
+ onControllerAttached: () => {
290
+ if (this.noQuarter || noQuarterActive()) {
291
+ this.relay?.sendNoQuarter(true);
292
+ }
293
+ this.cfg.onControllerAttached?.();
294
+ },
288
295
  // The app left while we're still running. Same posture as a dropped socket: stop
289
296
  // treating the turn as remote (the gate must not wait on a controller that isn't
290
297
  // there) and fail every pending approval closed. The turn itself keeps going —
@@ -310,7 +317,7 @@ export class RemoteBridge {
310
317
  // ── gate hooks (passed into the GateController) ─────────────────────────────
311
318
 
312
319
  getRemote = (): boolean => this.remote;
313
- getNoQuarter = (): boolean => this.noQuarter;
320
+ getNoQuarter = (): boolean => this.noQuarter || noQuarterActive();
314
321
 
315
322
  // The gate's remote approver: relay the request to the app and await its
316
323
  // allow/deny. Fail closed if no controller, on abort, or on disconnect. (The gate