privateer-agent 0.6.9 → 0.7.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.
Files changed (41) hide show
  1. package/README.md +20 -20
  2. package/SECURITY.md +1 -1
  3. package/bin/{privateer-daemon.mjs → privateer-harbor.mjs} +7 -7
  4. package/bin/privateer-launch.mjs +7 -6
  5. package/bin/privateer-subagent.mjs +1 -1
  6. package/extensions/privateer-brand.ts +1 -1
  7. package/extensions/privateer-connect.ts +14 -3
  8. package/extensions/privateer-tools.ts +1 -1
  9. package/package.json +1 -1
  10. package/src/auth/privateer.ts +2 -2
  11. package/src/channels/run.ts +5 -5
  12. package/src/channels/status.ts +7 -7
  13. package/src/cli/chat.ts +29 -3
  14. package/src/cli/{daemonCli.ts → harborCli.ts} +14 -14
  15. package/src/config/hosted.ts +5 -5
  16. package/src/crypto/accountTrust.ts +2 -2
  17. package/src/crypto/accountVerify.ts +1 -1
  18. package/src/{daemon → harbor}/index.ts +37 -37
  19. package/src/{daemon → harbor}/ipc.ts +19 -19
  20. package/src/{daemon → harbor}/service.ts +61 -28
  21. package/src/main.ts +1 -1
  22. package/src/providers/account.ts +1 -1
  23. package/src/providers/defaultModel.ts +1 -1
  24. package/src/remote/channelsControl.ts +8 -8
  25. package/src/remote/controlAuth.ts +1 -1
  26. package/src/remote/liveTaskSession.ts +4 -4
  27. package/src/remote/mcpControl.ts +2 -2
  28. package/src/remote/relayClient.ts +41 -21
  29. package/src/remote/remoteBridge.ts +17 -7
  30. package/src/remote/routinesControl.ts +7 -7
  31. package/src/remote/workflowsControl.ts +6 -6
  32. package/src/routines/delivery.ts +6 -6
  33. package/src/routines/schema.ts +3 -3
  34. package/src/routines/store.ts +3 -3
  35. package/src/routines/trigger.ts +1 -1
  36. package/src/tools/routine.ts +8 -8
  37. package/src/util/fileMentions.ts +232 -0
  38. package/src/workflows/expr.ts +1 -1
  39. package/src/workflows/runner.ts +3 -3
  40. package/src/workflows/schema.ts +1 -1
  41. package/src/workflows/store.ts +1 -1
package/README.md CHANGED
@@ -38,8 +38,8 @@ servers, sub-agents, scheduled routines, multi-step workflows, chat-app bridges,
38
38
  one-tap approval from your phone are included — and every one of the agent's actions runs
39
39
  through a **safe-by-default permission gate**.
40
40
 
41
- Privateer runs in three places, over one account and one config: the **terminal**, a
42
- **background daemon** for unattended work, and the **Privateer app** on
41
+ Privateer runs in three places, over one account and one config: the **terminal**,
42
+ **Harbor** (a background service) for unattended work, and the **Privateer app** on
43
43
  [phone, web](https://privateer.pro), and [desktop](#the-privateer-app).
44
44
 
45
45
  ## Why Privateer?
@@ -108,7 +108,7 @@ silently. The moat is swappable; the floor under it holds.
108
108
  terminal. See [The Privateer app](#the-privateer-app).
109
109
  - **A desktop app.** The same agent hosted inside a local Electron shell — no relay hop, works
110
110
  offline, multi-window with per-window MCP connectors. Shares your CLI login and config.
111
- - **Scheduled routines.** A background daemon runs approved tasks unattended — cron or
111
+ - **Scheduled routines.** Harbor, a background service, runs approved tasks unattended — cron or
112
112
  one-off — and the agent can schedule its own follow-up work. Results deliver to a file,
113
113
  the next session, your phone, email, or a webhook.
114
114
  - **Declarative workflows.** Multi-step agent pipelines as YAML — typed steps, conditional
@@ -298,7 +298,7 @@ and a management surface for it.
298
298
  | | |
299
299
  |---|---|
300
300
  | **Drive a session** | Send prompts, watch streamed output, and **Allow/Deny** each proposed action — including actions from sub-agents the session spawned |
301
- | **Spawn an agent** | Start a one-shot task on the daemon: *background* (headless, read-only toolset, result sealed to your outbox) or *live* (a fresh drivable session) |
301
+ | **Spawn an agent** | Start a one-shot task on the harbor: *background* (headless, read-only toolset, result sealed to your outbox) or *live* (a fresh drivable session) |
302
302
  | **Routines** | Create, edit, pause, run, and delete scheduled unattended tasks |
303
303
  | **Workflows** | List, run, and monitor multi-step workflows; answer `human_gate` steps to resume a paused run |
304
304
  | **MCP connectors** | Add, edit, and enable MCP servers; credentials are sealed to the terminal and write-only |
@@ -325,24 +325,24 @@ Download for [macOS](https://privateer.pro/download/mac) (Apple silicon),
325
325
 
326
326
  It's an early release and **not yet code-signed or notarized** — macOS will warn on first
327
327
  open. Routines and channels deliberately aren't hosted here: those belong to the always-on
328
- daemon, so background work still wants `privateer daemon install`.
328
+ harbor, so background work still wants `privateer harbor install`.
329
329
 
330
- ## Run it unattended — the daemon
330
+ ## Run it unattended — Harbor
331
331
 
332
- A resident background daemon runs scheduled routines, executes workflows, and accepts task
333
- spawns from the app — with no terminal open.
332
+ **Harbor** is a resident background service that runs scheduled routines, executes workflows,
333
+ and accepts task spawns from the app — with no terminal open.
334
334
 
335
335
  ```bash
336
- privateer daemon install # install as a login service (auto-starts, reachable from the app)
337
- privateer daemon status # service installed? daemon answering?
338
- privateer daemon run # or just run it in the foreground
339
- privateer daemon uninstall
336
+ privateer harbor install # install as a login service (auto-starts, reachable from the app)
337
+ privateer harbor status # service installed? harbor answering?
338
+ privateer harbor run # or just run it in the foreground
339
+ privateer harbor uninstall
340
340
  ```
341
341
 
342
342
  Installs as a **launchd user agent** on macOS or a **`systemd --user` unit** on Linux — no
343
- root, no sudo. (There's no Windows service path yet; use `privateer daemon run`.)
343
+ root, no sudo. (There's no Windows service path yet; use `privateer harbor run`.)
344
344
 
345
- Everything the daemon does still runs through the permission gate. Actions needing approval
345
+ Everything the harbor does still runs through the permission gate. Actions needing approval
346
346
  surface in the app; routines you approved once run on their own schedule.
347
347
 
348
348
  ## Workflows
@@ -354,12 +354,12 @@ resumes when you answer it, including from your phone.
354
354
 
355
355
  The engine ships in the standalone
356
356
  [`privateer-workflow`](https://www.npmjs.com/package/privateer-workflow) package. Today the
357
- user-facing surface is the **app** (save, run, monitor, share) and the **daemon** that
357
+ user-facing surface is the **app** (save, run, monitor, share) and the **harbor** that
358
358
  executes them — there's no `/workflow` command in the terminal yet. Schedule one by pointing
359
359
  a routine at it.
360
360
 
361
361
  Because a workflow can carry `script` steps, saving one from the app requires your **account
362
- signature** — the server can't inject a workflow onto your daemon.
362
+ signature** — the server can't inject a workflow onto your harbor.
363
363
 
364
364
  ## Chat-app channels
365
365
 
@@ -410,14 +410,14 @@ drop your own into `~/.privateer/agent/extensions/` and it loads the same way, g
410
410
 
411
411
  - **MCP servers** (`pi-mcp-adapter`) — declare them and their tools become first-class, gated
412
412
  like the rest (local stdio, or remote HTTP with interactive OAuth). One catalog at
413
- `~/.privateer/agent/mcp-desktop.json` is shared by the CLI, the daemon, and the desktop
413
+ `~/.privateer/agent/mcp-desktop.json` is shared by the CLI, the harbor, and the desktop
414
414
  app, so a machine has one coherent connector config.
415
415
  - **Sub-agents** (`pi-subagents`) — delegate investigations to bounded parallel agents. Children
416
416
  run as headless child processes that **inherit the moat**, so their actions hit the same
417
417
  permission gate and their approvals surface on your phone.
418
- - **Routines** — saved tasks the daemon runs unattended; ask the agent to schedule work and
418
+ - **Routines** — saved tasks the harbor runs unattended; ask the agent to schedule work and
419
419
  approve it once.
420
- - **Workflows** — declarative multi-step pipelines the daemon executes; see
420
+ - **Workflows** — declarative multi-step pipelines the harbor executes; see
421
421
  [Workflows](#workflows).
422
422
  - **Web tools** (`rpiv-web-tools`) — private-by-default web search/fetch with pluggable backends
423
423
  (self-hosted SearXNG for fully private search).
@@ -435,7 +435,7 @@ drop your own into `~/.privateer/agent/extensions/` and it loads the same way, g
435
435
  | `/init` | scaffold a starter `PRIVATEER.md` in this directory |
436
436
  | `/update` · `/privateer` | update to the latest release / Privateer status and posture |
437
437
 
438
- Shell subcommands: `privateer` (interactive), `privateer update`, `privateer daemon …`,
438
+ Shell subcommands: `privateer` (interactive), `privateer update`, `privateer harbor …`,
439
439
  `privateer --no-quarter`, `privateer --version`.
440
440
 
441
441
  ## Develop
package/SECURITY.md CHANGED
@@ -55,4 +55,4 @@ are sent only to the provider you selected. Bot tokens for messaging channels ar
55
55
  to a terminal keypair before they reach our relay, and channel configuration is verified
56
56
  against a link-pinned account key — the relay can neither read those tokens nor forge
57
57
  configuration. Architecture and residual risks are documented in
58
- `docs/daemon-channels-and-app.md`.
58
+ `docs/harbor-channels-and-app.md`.
@@ -1,11 +1,11 @@
1
1
  #!/usr/bin/env node
2
- // Launcher for the resident Privateer daemon (routines + app-driven headless task
2
+ // Launcher for the resident Privateer harbor (routines + app-driven headless task
3
3
  // spawns). Mirrors bin/privateer.mjs: load dev keys from the repo .env WITHOUT
4
4
  // changing cwd, register tsx so TS resolves regardless of the invocation cwd, then
5
- // hand off to the daemon CLI dispatcher (which imports ./boot.ts before any Pi code).
5
+ // hand off to the harbor CLI dispatcher (which imports ./boot.ts before any Pi code).
6
6
  //
7
- // Invoked two ways: interactively via the bash launcher (`privateer daemon …`), and
8
- // by the installed launchd/systemd service (`node privateer-daemon.mjs run`).
7
+ // Invoked two ways: interactively via the bash launcher (`privateer harbor …`), and
8
+ // by the installed launchd/systemd service (`node privateer-harbor.mjs run`).
9
9
  import { register } from "tsx/esm/api";
10
10
  import { fileURLToPath } from "node:url";
11
11
  import { dirname, resolve } from "node:path";
@@ -19,12 +19,12 @@ try {
19
19
  /* no .env — rely on the ambient environment / ~/.privateer */
20
20
  }
21
21
 
22
- // The daemon loads the moat as in-code factories, so its subagent children (routines /
22
+ // The harbor loads the moat as in-code factories, so its subagent children (routines /
23
23
  // task sessions) can't inherit it and `pi` isn't on PATH. Point pi-subagents at our
24
24
  // moat-injecting wrapper so those children spawn gated + private with no double-load.
25
25
  // Process-global is safe here: the wrapper is stateless (unlike a per-parent channel).
26
26
  process.env.PI_SUBAGENT_PI_BINARY ??= resolve(repo, "bin/privateer-subagent.mjs");
27
27
 
28
28
  register();
29
- const { runDaemonCli } = await import(resolve(repo, "src/cli/daemonCli.ts"));
30
- await runDaemonCli(process.argv.slice(2));
29
+ const { runHarborCli } = await import(resolve(repo, "src/cli/harborCli.ts"));
30
+ await runHarborCli(process.argv.slice(2));
@@ -111,13 +111,14 @@ if (sub === "update") {
111
111
  // runToCompletion exits via the child's exit handler.
112
112
  }
113
113
 
114
- // --- `privateer daemon [run|install|uninstall|status]` ---------------------
115
- // The resident background daemon (routines + app-driven headless task spawns). Boots
116
- // straight into src/daemon via bin/privateer-daemon.mjs — no moat-shim install (the
117
- // daemon loads the moat as in-code factories, not interactive extensions).
118
- else if (sub === "daemon") {
114
+ // --- `privateer harbor [run|install|uninstall|status]` ---------------------
115
+ // The resident background harbor (routines + app-driven headless task spawns). Boots
116
+ // straight into src/harbor via bin/privateer-harbor.mjs — no moat-shim install (the
117
+ // harbor loads the moat as in-code factories, not interactive extensions).
118
+ // `daemon` is a hidden back-compat alias for the pre-rename command name.
119
+ else if (sub === "harbor" || sub === "daemon") {
119
120
  const nodeArgs = fs.existsSync(ENV_FILE) ? [`--env-file=${ENV_FILE}`] : [];
120
- runToCompletion(NODE_BIN, [...nodeArgs, path.join(REPO, "bin", "privateer-daemon.mjs"), ...args.slice(1)]);
121
+ runToCompletion(NODE_BIN, [...nodeArgs, path.join(REPO, "bin", "privateer-harbor.mjs"), ...args.slice(1)]);
121
122
  }
122
123
 
123
124
  // --- normal launch: install the moat, then exec Pi's TUI -------------------
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  // The binary pi-subagents spawns for each subagent child (via PI_SUBAGENT_PI_BINARY)
3
3
  // when the PARENT loaded privateer's moat as IN-CODE extension factories (the lean
4
- // REPL, the daemon, live task sessions) rather than agent-dir discovery.
4
+ // REPL, the harbor, live task sessions) rather than agent-dir discovery.
5
5
  //
6
6
  // Why a wrapper here (vs the plain cli.js the TUI uses): a subagent child is a fresh
7
7
  // `pi` subprocess that can't inherit the parent's in-code factories. It CAN auto-
@@ -593,7 +593,7 @@ export default function privateerBrand(pi: any): void {
593
593
  // in src/auth/privateer.ts): revokeLocalSessions kills the account session
594
594
  // server-side, so leaving the persisted copy behind would make the NEXT launch
595
595
  // reuse a token that's already dead and dead-end on its first prompt (Pi doesn't
596
- // refresh on a 401). Mirrors the daemon's shutdown (daemon/index.ts).
596
+ // refresh on a 401). Mirrors the harbor's shutdown (harbor/index.ts).
597
597
  dropPersistedAccount(ctxRef);
598
598
  });
599
599
 
@@ -12,7 +12,7 @@
12
12
  * makeMcpControl() the relay uses (src/remote/mcpControl.ts) — which is why that
13
13
  * module was written framework-agnostic. A connector added here lands in
14
14
  * `agent/mcp-desktop.json` and is projected into `agent/mcp.json`, so it shows up in
15
- * the app's MCP screen, survives a toggle from the phone, and is read by the daemon's
15
+ * the app's MCP screen, survives a toggle from the phone, and is read by the harbor's
16
16
  * routine runs. Nothing about this panel is terminal-specific except the pixels.
17
17
  *
18
18
  * SECRETS: env values are typed into a masked field and written in PLAINTEXT to the
@@ -74,6 +74,17 @@ class SecretField {
74
74
  // are swallowed rather than inserted — the naive "strip control chars" approach
75
75
  // would leave "[A" behind from an up-arrow and silently corrupt the token.
76
76
  handleInput(data: string): boolean {
77
+ // A PASTE MUST BE UNWRAPPED FIRST. pi-tui's Terminal re-wraps pasted text in
78
+ // bracketed-paste markers and delivers it as ONE chunk (`\x1b[200~…\x1b[201~`,
79
+ // see pi-tui terminal.ts) — which starts with ESC, so the arrow-key guard below
80
+ // would otherwise swallow the entire token and leave the field empty.
81
+ const paste = /^\x1b\[200~([\s\S]*?)\x1b\[201~$/.exec(data);
82
+ if (paste) {
83
+ // Control chars (a trailing newline from the clipboard, most often) are stripped
84
+ // the same way typed input is — a token never legitimately contains one.
85
+ this.value += paste[1].replace(/[\x00-\x1f\x7f]/g, "");
86
+ return true;
87
+ }
77
88
  if (data.startsWith("\x1b")) return true;
78
89
  if (data === "\x7f" || data === "\b") {
79
90
  this.value = this.value.slice(0, -1);
@@ -84,7 +95,7 @@ class SecretField {
84
95
  this.value = "";
85
96
  return true;
86
97
  }
87
- // Everything printable, including a bracketed-paste chunk arriving at once.
98
+ // Ordinary typed input (pastes took the branch above).
88
99
  const printable = data.replace(/[\x00-\x1f\x7f]/g, "");
89
100
  if (!printable) return false;
90
101
  this.value += printable;
@@ -383,7 +394,7 @@ class ConnectPanel extends Container {
383
394
  const t = this.theme;
384
395
  this.body.addChild(new Text(t.fg("accent", t.bold("Connect a service")), 1, 0));
385
396
  this.body.addChild(
386
- new Text(t.fg("muted", "MCP connectors on this machine — shared with the app and the daemon."), 1, 0),
397
+ new Text(t.fg("muted", "MCP connectors on this machine — shared with the app and the harbor."), 1, 0),
387
398
  );
388
399
  this.body.addChild(new Spacer(1));
389
400
 
@@ -1,5 +1,5 @@
1
1
  // Privateer-specific custom tools for Pi's TUI (Phase 5). Today: create_routine
2
- // (schedule unattended tasks → the daemon runs them). The generic tools (read/edit/
2
+ // (schedule unattended tasks → the harbor runs them). The generic tools (read/edit/
3
3
  // bash/grep, web, subagents, todo) come from Pi builtins + adopted packages, so only
4
4
  // the privateer-only tools live here. Gated by our permission-gate extension.
5
5
  import { routineToolDefinition } from "../src/tools/routine.ts";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "privateer-agent",
3
- "version": "0.6.9",
3
+ "version": "0.7.0",
4
4
  "description": "Privateer — a provider-agnostic, safe-by-default terminal coding agent with TEE/Tinfoil attestation, rebuilt on the Pi toolkit. Bring your own model across 20 providers.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -152,7 +152,7 @@ let _refreshInFlight: Promise<ChildSession> | null = null;
152
152
  // remove the "privateer" entry from Pi's authStorage (authStorage.remove("privateer"))
153
153
  // right after revokeLocalSessions() so the next launch spawns a fresh session instead
154
154
  // of reusing the revoked one. Doing both is safe; doing only one is not. See
155
- // revokeLocalSessions and its callers (cli/chat.ts, daemon/index.ts).
155
+ // revokeLocalSessions and its callers (cli/chat.ts, harbor/index.ts).
156
156
  //
157
157
  // That pairing only covers a CLEAN exit, though. A terminal killed without running its
158
158
  // shutdown hook leaves its row alive server-side for the full TTL, and the next launch
@@ -625,7 +625,7 @@ export async function revokeAccountSession(timeoutMs = 1500): Promise<void> {
625
625
  * launch without a reactive-on-401 refresh, so the caller MUST also drop the persisted
626
626
  * copy right after this resolves — `authStorage.remove("privateer")` — or the next run
627
627
  * will reuse the token we just revoked and dead-end on a 401 (see the _account note).
628
- * Callers: cli/chat.ts cleanup() and daemon/index.ts shutdown().
628
+ * Callers: cli/chat.ts cleanup() and harbor/index.ts shutdown().
629
629
  */
630
630
  export async function revokeLocalSessions(timeoutMs = 1500): Promise<void> {
631
631
  await Promise.all([revokeChildSession(timeoutMs), revokeAccountSession(timeoutMs)]);
@@ -1,6 +1,6 @@
1
1
  // Headless entry for the messaging channels — `npm run channels` (or
2
2
  // `node --env-file=.env --import tsx src/channels/run.ts`). Boots the Pi stack the
3
- // same way the routines daemon does, then bridges any configured chat platform
3
+ // same way the routines harbor does, then bridges any configured chat platform
4
4
  // (Telegram, Slack) to per-conversation agent sessions.
5
5
  //
6
6
  // AUTHORIZATION MODEL (per channel):
@@ -23,7 +23,7 @@
23
23
  // posture). Every prompt/approval is appended to ~/.privateer/channels-audit.log.
24
24
  // Tokens live in config.json in plaintext — protect that file's permissions.
25
25
  //
26
- // Config lives in ~/.privateer/config.json (the same file the daemon reads):
26
+ // Config lives in ~/.privateer/config.json (the same file the harbor reads):
27
27
  // {
28
28
  // "defaultModel": "openrouter/openai/gpt-4o-mini",
29
29
  // "channels": {
@@ -47,7 +47,7 @@
47
47
  import "../boot.ts"; // env + attestation dispatcher, before any Pi import
48
48
  import { AsyncLocalStorage } from "node:async_hooks";
49
49
 
50
- // Read-only default toolset — same rationale as the routines daemon's SAFE_TOOLS:
50
+ // Read-only default toolset — same rationale as the routines harbor's SAFE_TOOLS:
51
51
  // a turn nobody is watching can't mutate the filesystem or shell out. Now that the
52
52
  // gate routes approvals into the chat (see below), a user can safely widen this per
53
53
  // channel via `channels.tools` — e.g. add "edit","write","bash" and each risky call
@@ -64,7 +64,7 @@ const SAFE_TOOLS = ["read", "grep", "find", "ls"];
64
64
  type Posture = "readonly" | "approve" | "auto";
65
65
  const POSTURES: Posture[] = ["readonly", "approve", "auto"];
66
66
 
67
- // Bound the live session map so a long-running daemon can't grow without limit or
67
+ // Bound the live session map so a long-running harbor can't grow without limit or
68
68
  // hold stale context forever.
69
69
  const MAX_SESSIONS = 500;
70
70
  const SESSION_IDLE_MS = 30 * 60 * 1000; // evict a conversation unused for 30 min
@@ -255,7 +255,7 @@ async function main() {
255
255
  // ── build a bridge per configured platform ───────────────────────────────────
256
256
  const bridges: { stop(): void }[] = [];
257
257
  // Platforms with a live bridge — written to the heartbeat file so the app's
258
- // channels manager (running on the daemon's relay, a separate process) can show
258
+ // channels manager (running on the harbor's relay, a separate process) can show
259
259
  // a live/offline badge without talking to this process.
260
260
  const startedPlatforms: string[] = [];
261
261
 
@@ -1,9 +1,9 @@
1
- // Channels daemon heartbeat — the one bit of shared state between the channels
2
- // daemon (channels/run.ts, the WRITER) and the always-on management relay
3
- // (daemon/index.ts → channelsControl, the READER).
1
+ // Channels harbor heartbeat — the one bit of shared state between the channels
2
+ // harbor (channels/run.ts, the WRITER) and the always-on management relay
3
+ // (harbor/index.ts → channelsControl, the READER).
4
4
  //
5
5
  // The two run as SEPARATE processes, so the manager can't ask the channels
6
- // daemon directly whether it's live. Instead the channels daemon writes a small
6
+ // harbor directly whether it's live. Instead the channels harbor writes a small
7
7
  // heartbeat file with the platforms it's currently serving; the reader treats a
8
8
  // FRESH heartbeat as "running" and a stale/absent one as "not running". This is
9
9
  // best-effort presence, never a dependency: a missing file just means the app
@@ -13,7 +13,7 @@ import { readFileSync, writeFileSync } from "node:fs";
13
13
  import { join } from "node:path";
14
14
  import { globalDir } from "../config/paths.ts";
15
15
 
16
- // Heartbeat cadence + freshness window. The channels daemon rewrites the file
16
+ // Heartbeat cadence + freshness window. The channels harbor rewrites the file
17
17
  // every HEARTBEAT_MS; a heartbeat older than STALE_MS is treated as dead (the
18
18
  // process exited without clearing it, or wedged).
19
19
  export const HEARTBEAT_MS = 30_000;
@@ -40,8 +40,8 @@ export function writeChannelsStatus(platforms: string[]): void {
40
40
  }
41
41
  }
42
42
 
43
- // READER: the set of platforms the channels daemon is currently serving, or an
44
- // empty set when the daemon is down / the heartbeat is stale. Never throws.
43
+ // READER: the set of platforms the channels harbor is currently serving, or an
44
+ // empty set when the harbor is down / the heartbeat is stale. Never throws.
45
45
  export function readRunningPlatforms(): Set<string> {
46
46
  try {
47
47
  const status = JSON.parse(readFileSync(statusPath(), "utf8")) as ChannelsStatus;
package/src/cli/chat.ts CHANGED
@@ -36,6 +36,7 @@ async function main() {
36
36
  const { makeExtensionsControl } = await import("../remote/extensionsControl.ts");
37
37
  const { makeSkillsControl } = await import("../remote/skillsControl.ts");
38
38
  const { authorizeControl } = await import("../remote/controlAuth.ts");
39
+ const { resolveMentions, completeMention, searchFiles } = await import("../util/fileMentions.ts");
39
40
  const priv = await import("../auth/privateer.ts");
40
41
  const { makeAccountProvider, accountPosture } = await import("../providers/account.ts");
41
42
  const { agentVersion } = await import("../config/version.ts");
@@ -59,7 +60,14 @@ async function main() {
59
60
  // /model switches, so the app banner + picker reflect what's actually selected.
60
61
  let currentSpec = spec;
61
62
 
62
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
63
+ // Tab-completes an `@path` mention against the cwd tree (files the user can
64
+ // reference in a prompt). readline calls this with the line up to the cursor; a
65
+ // non-mention line returns no hits, leaving normal input untouched. Async form
66
+ // (callback) so the filesystem read doesn't block the event loop.
67
+ const completer = (line: string, cb: (err: null, result: [string[], string]) => void): void => {
68
+ void completeMention(line, cwd).then((r) => cb(null, r)).catch(() => cb(null, [[], line]));
69
+ };
70
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout, completer });
63
71
  let closed = false;
64
72
  rl.on("close", () => (closed = true));
65
73
  // Local terminal output is coalesced and prompt-aware. Two stutter sources it kills:
@@ -152,6 +160,13 @@ async function main() {
152
160
  relay?.sendCommands(availableCommands());
153
161
  },
154
162
  onStatus: (t) => console.log(`\n${DIM}⟿ ${t}${RESET}`),
163
+ // The app composer is autocompleting an `@file` mention — list the cwd entries
164
+ // matching the query and reply. Read-only + cwd-constrained (searchFiles never
165
+ // escapes the subtree); resolution of the picked path happens on the prompt turn.
166
+ onFilesSearch: (id, query) => void (async () => {
167
+ try { bridge.sendFileMatches(id, await searchFiles(query, cwd)); }
168
+ catch { bridge.sendFileMatches(id, []); }
169
+ })(),
155
170
  // The app's extensions manager: list the user's installed Pi extensions, or
156
171
  // add/remove one. add/remove persist immediately but only load on the next
157
172
  // terminal launch — so the final frame flags needsRestart. See runExtMutation.
@@ -258,7 +273,18 @@ async function main() {
258
273
  turnActive = true;
259
274
  if (remote && echo) console.log(`\n${DIM}⟿ [app] ${text.slice(0, 80)}${RESET}`);
260
275
  try {
261
- await session.prompt(text);
276
+ // Expand any `@path` mentions into appended <file> blocks + image attachments,
277
+ // resolved against this terminal's cwd (constrained to the cwd subtree). Both a
278
+ // locally-typed prompt and an app-driven one land here, so both get it. A prompt
279
+ // with no resolvable mention passes through unchanged. Unresolved tokens stay
280
+ // inline; note them so a typo'd path isn't silently ignored.
281
+ const mentions = await resolveMentions(text, cwd);
282
+ if (mentions.skipped.length) {
283
+ const m = `Couldn't attach: ${mentions.skipped.join(", ")} (must be a file inside ${cwd})`;
284
+ console.log(`${DIM}${m}${RESET}`);
285
+ if (remote) relay?.sendNotice(m);
286
+ }
287
+ await session.prompt(mentions.text, mentions.images.length ? { images: mentions.images } : undefined);
262
288
  } catch (e) {
263
289
  console.log(`\n${RED}${(e as Error).message}${RESET}`);
264
290
  } finally {
@@ -500,7 +526,7 @@ async function main() {
500
526
  );
501
527
 
502
528
  const HELP = "Commands: /remote-access <on|off> /login /model <provider/id> /models [filter] /verify /mode <…> /quit";
503
- console.log(`${DIM}Ready. Type a prompt. ${HELP}${RESET}`);
529
+ console.log(`${DIM}Ready. Type a prompt — reference a file with @path (Tab completes). ${HELP}${RESET}`);
504
530
  await showPosture();
505
531
 
506
532
  // The available model catalog as sorted "provider/id" specs. Same source the
@@ -1,7 +1,7 @@
1
- // `privateer daemon [run|install|uninstall|status]` dispatcher.
1
+ // `privateer harbor [run|install|uninstall|status]` dispatcher.
2
2
  //
3
3
  // ORDERING CONTRACT: import ./boot.ts FIRST (env pin + attestation dispatcher), then
4
- // DYNAMICALLY import the daemon (which pulls the Pi session stack) only when we
4
+ // DYNAMICALLY import the harbor (which pulls the Pi session stack) only when we
5
5
  // actually run it — so boot's side effects are guaranteed to precede any Pi import.
6
6
  // The service/status subcommands touch no Pi code, so they can run without paying the
7
7
  // session-stack import cost.
@@ -9,30 +9,30 @@ import "../boot.ts";
9
9
 
10
10
  function usage(): string {
11
11
  return [
12
- "Usage: privateer daemon [command]",
12
+ "Usage: privateer harbor [command]",
13
13
  "",
14
- " run Run the daemon in the foreground (default).",
14
+ " run Run Harbor in the foreground (default).",
15
15
  " install Install it as a login service so it auto-starts and stays",
16
16
  " reachable from the app even with no terminal open.",
17
17
  " uninstall Remove the login service.",
18
- " status Show whether the service is installed and the daemon is live.",
18
+ " status Show whether the service is installed and Harbor is live.",
19
19
  ].join("\n");
20
20
  }
21
21
 
22
- export async function runDaemonCli(argv: string[]): Promise<void> {
22
+ export async function runHarborCli(argv: string[]): Promise<void> {
23
23
  const sub = argv[0] ?? "run";
24
24
  switch (sub) {
25
25
  case "run": {
26
26
  // Pi-touching — dynamic import AFTER boot.
27
- const { runDaemon } = await import("../daemon/index.ts");
28
- runDaemon(); // installs its own SIGINT/SIGTERM handlers and blocks
27
+ const { runHarbor } = await import("../harbor/index.ts");
28
+ runHarbor(); // installs its own SIGINT/SIGTERM handlers and blocks
29
29
  return;
30
30
  }
31
31
  case "install": {
32
- const { installService } = await import("../daemon/service.ts");
32
+ const { installService } = await import("../harbor/service.ts");
33
33
  try {
34
34
  const info = installService();
35
- process.stdout.write(`Privateer daemon installed as a login service.\n unit: ${info.unitPath}\n logs: ${info.logPath}\nIt will start now and on every login. Manage with \`privateer daemon status|uninstall\`.\n`);
35
+ process.stdout.write(`Harbor installed as a login service.\n unit: ${info.unitPath}\n logs: ${info.logPath}\nIt will start now and on every login. Manage with \`privateer harbor status|uninstall\`.\n`);
36
36
  } catch (err) {
37
37
  process.stderr.write(`${err instanceof Error ? err.message : String(err)}\n`);
38
38
  process.exit(1);
@@ -40,10 +40,10 @@ export async function runDaemonCli(argv: string[]): Promise<void> {
40
40
  return;
41
41
  }
42
42
  case "uninstall": {
43
- const { uninstallService } = await import("../daemon/service.ts");
43
+ const { uninstallService } = await import("../harbor/service.ts");
44
44
  try {
45
45
  uninstallService();
46
- process.stdout.write("Privateer daemon login service removed.\n");
46
+ process.stdout.write("Harbor login service removed.\n");
47
47
  } catch (err) {
48
48
  process.stderr.write(`${err instanceof Error ? err.message : String(err)}\n`);
49
49
  process.exit(1);
@@ -51,7 +51,7 @@ export async function runDaemonCli(argv: string[]): Promise<void> {
51
51
  return;
52
52
  }
53
53
  case "status": {
54
- const { statusReport } = await import("../daemon/service.ts");
54
+ const { statusReport } = await import("../harbor/service.ts");
55
55
  process.stdout.write((await statusReport()) + "\n");
56
56
  return;
57
57
  }
@@ -61,7 +61,7 @@ export async function runDaemonCli(argv: string[]): Promise<void> {
61
61
  process.stdout.write(usage() + "\n");
62
62
  return;
63
63
  default:
64
- process.stderr.write(`Unknown daemon command: ${sub}\n\n${usage()}\n`);
64
+ process.stderr.write(`Unknown harbor command: ${sub}\n\n${usage()}\n`);
65
65
  process.exit(1);
66
66
  }
67
67
  }
@@ -5,11 +5,11 @@ import { terminalPublicKeyBase64 } from "../crypto/terminalKey.ts";
5
5
 
6
6
  // Harbor hosted mode.
7
7
  //
8
- // When true, this daemon is running inside Privateer's confidential-VM fleet
8
+ // When true, this harbor is running inside Privateer's confidential-VM fleet
9
9
  // (the host orchestrator sets HARBOR_HOSTED=1), not on a user's own machine.
10
- // Hosted daemons run on-demand: they report their next routine fire time to the
10
+ // Hosted harbors run on-demand: they report their next routine fire time to the
11
11
  // server and idle-suspend when there's no work, so the server can wake them
12
- // again in time. A daemon on a user's laptop leaves this off and keeps running
12
+ // again in time. A harbor on a user's laptop leaves this off and keeps running
13
13
  // its own cron continuously.
14
14
  //
15
15
  // Read via process.env at call time, mirroring PRIVATEER_HOME / PRIVATEER_SERVER_URL.
@@ -18,7 +18,7 @@ export function isHosted(): boolean {
18
18
  }
19
19
 
20
20
  /**
21
- * Publish this daemon's relay identity key so the Harbor host can attest it.
21
+ * Publish this harbor's relay identity key so the Harbor host can attest it.
22
22
  *
23
23
  * ATTESTATION CONTRACT (host side: treeview `server/services/harborOrchestrator/`):
24
24
  * the orchestrator mints the SEV-SNP report on the CVM host — configfs-tsm is a
@@ -34,7 +34,7 @@ export function isHosted(): boolean {
34
34
  * before the relay registers.
35
35
  *
36
36
  * Hosted-only and best-effort: on a user's own machine this is a no-op, and a write
37
- * failure must never take the daemon down — attestation simply fail-closes host-side
37
+ * failure must never take the harbor down — attestation simply fail-closes host-side
38
38
  * with HARBOR_ATTEST_NO_KEY rather than reporting a false "attested".
39
39
  */
40
40
  export function publishRelayPub(): void {
@@ -72,11 +72,11 @@ export function clearAccountSignKey(): void {
72
72
  // watermark for its terminal is a replay/rollback of an older signed envelope and is
73
73
  // refused; at-or-above is accepted (an idempotent replay of the latest is harmless).
74
74
  //
75
- // Keyed by termId — NOT global — so the always-on daemon (stable routines-… id) and
75
+ // Keyed by termId — NOT global — so the always-on harbor (stable routines-… id) and
76
76
  // each interactive terminal (its own id) don't cross-reject each other's frames when
77
77
  // the app drives them near-simultaneously with independent ts streams. Each signed
78
78
  // frame binds its termId (see accountVerify.controlMessage), so a per-terminal
79
- // watermark is the matching granularity. Persisted so it survives a daemon restart;
79
+ // watermark is the matching granularity. Persisted so it survives a harbor restart;
80
80
  // deliberately NOT cleared on logout (see clearAccountSignKey — M1).
81
81
  function tsPath(): string {
82
82
  return join(globalDir(), "control-sig.json");
@@ -32,7 +32,7 @@ function canonicalize(value: unknown): string {
32
32
 
33
33
  // The signed envelope. `termId` binds the intended recipient terminal (so a signature
34
34
  // for terminal X can't be replayed against Y — verification uses OUR termId). `ts`
35
- // binds freshness (the daemon rejects a ts it has already applied → no replay/rollback).
35
+ // binds freshness (the harbor rejects a ts it has already applied → no replay/rollback).
36
36
  export interface ChannelSaveEnvelope {
37
37
  termId: string;
38
38
  ts: number;