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.
- package/bin/privateer-launch.mjs +15 -0
- package/bin/privateer-splash.mjs +18 -1
- package/bin/privateer.cmd +3 -0
- package/extensions/privateer-context.ts +35 -11
- package/package.json +1 -1
- package/src/context.ts +9 -0
- package/src/remote/remoteBridge.ts +10 -3
package/bin/privateer-launch.mjs
CHANGED
|
@@ -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");
|
package/bin/privateer-splash.mjs
CHANGED
|
@@ -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
|
|
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 {
|
|
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
|
-
//
|
|
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
|
|
25
|
-
// and chained across before_agent_start handlers, so
|
|
26
|
-
// the turn;
|
|
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
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
if (!
|
|
34
|
-
|
|
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.
|
|
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 =
|
|
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: () =>
|
|
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
|