privateer-agent 0.12.14 → 0.12.15
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/README.md
CHANGED
|
@@ -396,6 +396,12 @@ Download for [macOS](https://privateer.pro/download/mac) (Apple silicon),
|
|
|
396
396
|
[macOS Intel](https://privateer.pro/download/mac-intel), or
|
|
397
397
|
[Windows](https://privateer.pro/download/windows).
|
|
398
398
|
|
|
399
|
+
Once it's installed, **`/desktop`** in the terminal brings it up — no Spotlight detour. It
|
|
400
|
+
opens the app, not a copy of this conversation: the desktop hosts its own session, so pick
|
|
401
|
+
the folder you were working in from **File ▸ Spawn Privateer at…** and it starts on the same
|
|
402
|
+
model and connectors this terminal uses (the per-folder defaults live in `~/.privateer`,
|
|
403
|
+
which both read).
|
|
404
|
+
|
|
399
405
|
It's an early release and **not yet code-signed or notarized** — macOS will warn on first
|
|
400
406
|
open. Routines and channels deliberately aren't hosted here: those belong to the always-on
|
|
401
407
|
harbor, so background work still wants `privateer harbor install`.
|
|
@@ -640,6 +646,7 @@ drop your own into `~/.privateer/agent/extensions/` and it loads the same way, g
|
|
|
640
646
|
| `/extensions` | list loaded Pi extensions |
|
|
641
647
|
| `/web-tools` | point `web_search`/`web_fetch` at a search provider of your own (signed in, they already work on your account) |
|
|
642
648
|
| `/init` | scaffold a starter `PRIVATEER.md` in this directory |
|
|
649
|
+
| `/desktop` | open the [desktop app](#desktop-app) — same login, same per-folder defaults |
|
|
643
650
|
| `/update` · `/privateer` | update to the latest release / Privateer status and posture |
|
|
644
651
|
|
|
645
652
|
Shell subcommands: `privateer` (interactive), `privateer update`, `privateer harbor …`,
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
// /desktop — bring up the Privateer desktop app from the terminal.
|
|
2
|
+
//
|
|
3
|
+
// The two front ends already share everything that matters: one ~/.privateer home,
|
|
4
|
+
// so one login, one model config, one MCP catalog, one set of per-folder spawn
|
|
5
|
+
// defaults. What was missing was a way across. Getting from a terminal to the app
|
|
6
|
+
// meant leaving the terminal — Spotlight, the Dock, a Start-menu hunt — which is
|
|
7
|
+
// exactly the kind of small friction that leaves a shipped app unopened.
|
|
8
|
+
//
|
|
9
|
+
// What it does NOT do is carry the conversation over: the desktop hosts its own
|
|
10
|
+
// in-process session and takes no folder argument (see src/config/desktopApp.ts),
|
|
11
|
+
// so this opens the app and says how to point a window at the folder you were just
|
|
12
|
+
// working in. When the app isn't installed we say so once, with the download page
|
|
13
|
+
// for this platform, and never again unasked — the working-line tip in
|
|
14
|
+
// privateer-hints.ts only fires on a machine that HAS it.
|
|
15
|
+
|
|
16
|
+
import {
|
|
17
|
+
desktopAppPath,
|
|
18
|
+
desktopDownloadAltUrl,
|
|
19
|
+
desktopDownloadUrl,
|
|
20
|
+
openDesktopApp,
|
|
21
|
+
} from "../src/config/desktopApp.ts";
|
|
22
|
+
|
|
23
|
+
export default function privateerDesktop(pi: any): void {
|
|
24
|
+
pi.registerCommand?.("desktop", {
|
|
25
|
+
description: "Open the Privateer desktop app — same login, same per-folder defaults",
|
|
26
|
+
handler: (_args: string, ctx: any) => {
|
|
27
|
+
const app = desktopAppPath();
|
|
28
|
+
|
|
29
|
+
if (app) {
|
|
30
|
+
const cwd = process.cwd();
|
|
31
|
+
if (openDesktopApp(app)) {
|
|
32
|
+
ctx?.ui?.notify?.(
|
|
33
|
+
`Opening the Privateer desktop app — same login and per-folder defaults as this terminal. ` +
|
|
34
|
+
`File ▸ Spawn Privateer at… points a window at ${cwd}.`,
|
|
35
|
+
"info",
|
|
36
|
+
);
|
|
37
|
+
} else {
|
|
38
|
+
ctx?.ui?.notify?.(`Could not launch ${app} — open it yourself and this terminal keeps working.`, "error");
|
|
39
|
+
}
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const url = desktopDownloadUrl();
|
|
44
|
+
const alt = desktopDownloadAltUrl(); // the other Mac build — see desktopApp.ts
|
|
45
|
+
ctx?.ui?.notify?.(
|
|
46
|
+
url
|
|
47
|
+
? `The Privateer desktop app isn't installed here. Download: ${url}${alt ? ` (other Macs: ${alt})` : ""} — ` +
|
|
48
|
+
`it reads the same ~/.privateer, so it starts already signed in with your models and connectors.`
|
|
49
|
+
: `The desktop app ships for macOS and Windows only — on this platform the terminal agent is the app.`,
|
|
50
|
+
"info",
|
|
51
|
+
);
|
|
52
|
+
},
|
|
53
|
+
});
|
|
54
|
+
}
|
|
@@ -25,6 +25,7 @@ import { readFileSync, writeFileSync, mkdirSync } from "node:fs";
|
|
|
25
25
|
import { join } from "node:path";
|
|
26
26
|
import { keyText } from "@earendil-works/pi-coding-agent";
|
|
27
27
|
import { configPath, globalDir } from "../src/config/paths.ts";
|
|
28
|
+
import { desktopAppPath } from "../src/config/desktopApp.ts";
|
|
28
29
|
|
|
29
30
|
const FIRST_MS = 6_000; // a turn shorter than this never shows a tip
|
|
30
31
|
const EVERY_MS = 12_000;
|
|
@@ -76,9 +77,28 @@ const HINTS: Array<() => string> = [
|
|
|
76
77
|
? `${k} is push-to-talk — /speak on reads the answer back`
|
|
77
78
|
: `/talk types what you say — /speak on reads the answer back`;
|
|
78
79
|
},
|
|
80
|
+
() => (haveDesktopApp() ? `/desktop opens the Privateer desktop app — same login, same folder defaults` : ""),
|
|
79
81
|
() => `these tips are /hints — /hints off silences them`,
|
|
80
82
|
];
|
|
81
83
|
|
|
84
|
+
// A hint returns "" when it doesn't apply to THIS machine, and the rotation skips
|
|
85
|
+
// it. The desktop tip is the case that needs it: naming a command for an app the
|
|
86
|
+
// user has is discoverability, advertising one they haven't is an ad. Memoised
|
|
87
|
+
// because the rotation asks every 12 s and the answer is a stat() on a path that
|
|
88
|
+
// doesn't change under a running terminal (installing the app mid-session is worth
|
|
89
|
+
// a restart, not a filesystem poll).
|
|
90
|
+
let desktopSeen: boolean | undefined;
|
|
91
|
+
function haveDesktopApp(): boolean {
|
|
92
|
+
if (desktopSeen === undefined) {
|
|
93
|
+
try {
|
|
94
|
+
desktopSeen = desktopAppPath() !== null;
|
|
95
|
+
} catch {
|
|
96
|
+
desktopSeen = false;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return desktopSeen;
|
|
100
|
+
}
|
|
101
|
+
|
|
82
102
|
// Default ON: absent file, absent block, or unreadable JSON all mean enabled.
|
|
83
103
|
// Only an explicit { hints: { enabled: false } } turns the rotation off.
|
|
84
104
|
function hintsEnabled(): boolean {
|
|
@@ -119,9 +139,20 @@ export default function privateerHints(pi: any): void {
|
|
|
119
139
|
if (restoreDefault) uiRef?.setWorkingMessage?.();
|
|
120
140
|
};
|
|
121
141
|
|
|
142
|
+
// The next hint that applies here, advancing the cursor past any that opted out.
|
|
143
|
+
// Bounded by the list length, so an all-empty list ends the rotation instead of
|
|
144
|
+
// spinning through it forever.
|
|
145
|
+
const nextHint = (): string => {
|
|
146
|
+
for (let i = 0; i < HINTS.length; i++) {
|
|
147
|
+
const text = HINTS[cursor++ % HINTS.length]();
|
|
148
|
+
if (text) return text;
|
|
149
|
+
}
|
|
150
|
+
return "";
|
|
151
|
+
};
|
|
152
|
+
|
|
122
153
|
const showNext = (): void => {
|
|
123
|
-
const hint =
|
|
124
|
-
|
|
154
|
+
const hint = nextHint();
|
|
155
|
+
if (!hint) return; // nothing applies on this machine — leave "Working..." alone
|
|
125
156
|
uiRef?.setWorkingMessage?.(`Working... · tip: ${hint}`);
|
|
126
157
|
timer = setTimeout(showNext, EVERY_MS);
|
|
127
158
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "privateer-agent",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.15",
|
|
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",
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
// Where the Privateer DESKTOP app lives on this machine, and how to bring it up.
|
|
2
|
+
//
|
|
3
|
+
// The desktop app and this terminal are two front ends over one home: both read
|
|
4
|
+
// ~/.privateer, so they share the account login, the model config, the MCP catalog
|
|
5
|
+
// and the per-folder spawn defaults (config/spawns.ts). That makes "open the app"
|
|
6
|
+
// a genuinely useful thing for a terminal to offer — nothing is handed over, the
|
|
7
|
+
// state is already common — which is what /desktop (extensions/privateer-desktop.ts)
|
|
8
|
+
// does, and what the working-line tip in privateer-hints.ts points at.
|
|
9
|
+
//
|
|
10
|
+
// WHY DETECTION AND NOT JUST A DOWNLOAD LINK. A tip that advertises software the
|
|
11
|
+
// user hasn't got is an ad; one that names a command for software they HAVE is
|
|
12
|
+
// discoverability. So both consumers ask this module first, and the hint stays
|
|
13
|
+
// silent on a machine with no app installed.
|
|
14
|
+
//
|
|
15
|
+
// NO FOLDER HANDOFF. The app takes no path argument today — its second-instance
|
|
16
|
+
// handler just focuses the running window (desktop/src/main/main.mjs) — so /desktop
|
|
17
|
+
// opens the app, and the user picks this folder from File ▸ Spawn Privateer at…,
|
|
18
|
+
// where the spawn record this terminal already shares makes it start on the same
|
|
19
|
+
// model and connectors. If the app ever learns a folder argv, this is the one place
|
|
20
|
+
// that has to change.
|
|
21
|
+
//
|
|
22
|
+
// IMPORT-SAFETY: node builtins only, no Pi imports, no side effects — safe from an
|
|
23
|
+
// extension under jiti and from a pre-boot entry alike.
|
|
24
|
+
|
|
25
|
+
import { spawn } from "node:child_process";
|
|
26
|
+
import { existsSync } from "node:fs";
|
|
27
|
+
import { homedir } from "node:os";
|
|
28
|
+
import { basename, join } from "node:path";
|
|
29
|
+
|
|
30
|
+
/** Per-platform download pages (README ▸ Desktop app). No Linux build exists. */
|
|
31
|
+
const DOWNLOAD_MAC = "https://privateer.pro/download/mac";
|
|
32
|
+
const DOWNLOAD_MAC_INTEL = "https://privateer.pro/download/mac-intel";
|
|
33
|
+
const DOWNLOAD_WINDOWS = "https://privateer.pro/download/windows";
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* The download page to lead with on THIS machine, or null where we ship no desktop
|
|
37
|
+
* build. macOS is split by CPU family — the arm64 dmg won't run on an Intel Mac —
|
|
38
|
+
* and process.arch is the interpreter's answer, not the machine's: an Apple-silicon
|
|
39
|
+
* Mac running a Rosetta node reports x64 and would be led to the Intel build. That
|
|
40
|
+
* is survivable rather than solved (detecting the translation means shelling out to
|
|
41
|
+
* `sysctl sysctl.proc_translated` for one link) because the OTHER build is offered
|
|
42
|
+
* alongside it — see desktopDownloadAltUrl.
|
|
43
|
+
*/
|
|
44
|
+
export function desktopDownloadUrl(
|
|
45
|
+
platform: NodeJS.Platform = process.platform,
|
|
46
|
+
arch: string = process.arch,
|
|
47
|
+
): string | null {
|
|
48
|
+
if (platform === "darwin") return arch === "x64" ? DOWNLOAD_MAC_INTEL : DOWNLOAD_MAC;
|
|
49
|
+
if (platform === "win32") return DOWNLOAD_WINDOWS;
|
|
50
|
+
return null; // electron-builder.yml targets mac + win only
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* The macOS build for the OTHER CPU family, so a Mac user is never one wrong arch
|
|
55
|
+
* away from a download that won't launch. Null everywhere else: Windows ships one
|
|
56
|
+
* x64 installer and there is no Linux build at all.
|
|
57
|
+
*/
|
|
58
|
+
export function desktopDownloadAltUrl(
|
|
59
|
+
platform: NodeJS.Platform = process.platform,
|
|
60
|
+
arch: string = process.arch,
|
|
61
|
+
): string | null {
|
|
62
|
+
if (platform !== "darwin") return null;
|
|
63
|
+
return arch === "x64" ? DOWNLOAD_MAC : DOWNLOAD_MAC_INTEL;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* The installed app, or null. Ordered by confidence:
|
|
68
|
+
*
|
|
69
|
+
* 1. OUR OWN interpreter, when the terminal is running on the app's bundled Node.
|
|
70
|
+
* The desktop's CLI shim runs `privateer` through the app binary itself with
|
|
71
|
+
* ELECTRON_RUN_AS_NODE=1 (desktop/src/main/cliShim.mjs), so process.execPath IS
|
|
72
|
+
* the app — and it's the copy the user actually installed, wherever they put it.
|
|
73
|
+
* 2. The standard install locations. macOS drag-install goes to /Applications or
|
|
74
|
+
* ~/Applications; the Windows installer is per-user NSIS (perMachine: false) so
|
|
75
|
+
* %LOCALAPPDATA%\Programs\Privateer is the default, with Program Files covered
|
|
76
|
+
* for an install that chose it (allowToChangeInstallationDirectory: true).
|
|
77
|
+
*
|
|
78
|
+
* A user who installed somewhere else entirely reads as "not installed" — which
|
|
79
|
+
* costs them a download link they don't need, and never a wrong app launched.
|
|
80
|
+
*/
|
|
81
|
+
export function desktopAppPath(
|
|
82
|
+
platform: NodeJS.Platform = process.platform,
|
|
83
|
+
env: Record<string, string | undefined> = process.env,
|
|
84
|
+
execPath: string = process.execPath,
|
|
85
|
+
): string | null {
|
|
86
|
+
const hit = (p: string): string | null => (p && existsSync(p) ? p : null);
|
|
87
|
+
|
|
88
|
+
if (platform === "darwin") {
|
|
89
|
+
// …/Privateer.app/Contents/MacOS/Privateer → …/Privateer.app
|
|
90
|
+
const marker = "/Privateer.app/";
|
|
91
|
+
const at = execPath.indexOf(marker);
|
|
92
|
+
if (at >= 0) {
|
|
93
|
+
const bundle = hit(execPath.slice(0, at + marker.length - 1));
|
|
94
|
+
if (bundle) return bundle;
|
|
95
|
+
}
|
|
96
|
+
return hit("/Applications/Privateer.app") ?? hit(join(homedir(), "Applications", "Privateer.app"));
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (platform === "win32") {
|
|
100
|
+
if (/^privateer\.exe$/i.test(basename(execPath))) {
|
|
101
|
+
const self = hit(execPath);
|
|
102
|
+
if (self) return self;
|
|
103
|
+
}
|
|
104
|
+
const local = env.LOCALAPPDATA;
|
|
105
|
+
const files = env.ProgramFiles;
|
|
106
|
+
return (
|
|
107
|
+
(local ? hit(join(local, "Programs", "Privateer", "Privateer.exe")) : null) ??
|
|
108
|
+
(files ? hit(join(files, "Privateer", "Privateer.exe")) : null)
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Launch (or focus) the desktop app. Best-effort and never throws — the caller
|
|
117
|
+
* reports the failure as text, the way openBrowser.ts does.
|
|
118
|
+
*
|
|
119
|
+
* ELECTRON_RUN_AS_NODE IS STRIPPED, and that is load-bearing rather than tidy: when
|
|
120
|
+
* this terminal was itself started by the desktop's CLI shim, that variable is set
|
|
121
|
+
* in our environment, and a child inheriting it starts the app binary as a bare Node
|
|
122
|
+
* that exits without ever drawing a window. `open` goes through LaunchServices and
|
|
123
|
+
* wouldn't pass it on anyway; the Windows path spawns the exe directly and would.
|
|
124
|
+
*/
|
|
125
|
+
export function openDesktopApp(appPath: string, platform: NodeJS.Platform = process.platform): boolean {
|
|
126
|
+
const { ELECTRON_RUN_AS_NODE: _drop, ...env } = process.env;
|
|
127
|
+
try {
|
|
128
|
+
const child =
|
|
129
|
+
platform === "darwin"
|
|
130
|
+
? spawn("open", ["-a", appPath], { detached: true, stdio: "ignore", env })
|
|
131
|
+
: spawn(appPath, [], { detached: true, stdio: "ignore", env });
|
|
132
|
+
child.on("error", () => {}); // a spawn failure must not raise on the event loop
|
|
133
|
+
child.unref(); // never hold the CLI's exit open
|
|
134
|
+
return true;
|
|
135
|
+
} catch {
|
|
136
|
+
return false;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
{ "name": "privateer-privacy", "entry": "extensions/privateer-privacy.ts", "note": "pi-privacy + account tier resolver" },
|
|
12
12
|
{ "name": "privateer-connect", "entry": "extensions/privateer-connect.ts", "note": "/connect — MCP connector manager" },
|
|
13
13
|
{ "name": "privateer-media", "entry": "extensions/privateer-media.ts", "note": "image/video/speech/music + ffmpeg compose" },
|
|
14
|
+
{ "name": "privateer-desktop", "entry": "extensions/privateer-desktop.ts", "note": "/desktop — open the Privateer desktop app" },
|
|
14
15
|
{ "name": "privateer-hints", "entry": "extensions/privateer-hints.ts", "note": "rotating tips in the working line + /hints" },
|
|
15
16
|
{ "name": "privateer-update", "entry": "extensions/privateer-update.ts", "note": "tool pack updates in place — banner flag + /update" },
|
|
16
17
|
{ "name": "privateer-speak", "entry": "extensions/privateer-speak.ts", "note": "spoken responses (/speak) + voice input (/talk) — pi-speak + confidential account TTS/STT" },
|