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
@@ -1,9 +1,9 @@
1
- // A live, app-drivable agent session spawned on demand by the daemon (task_spawn,
2
- // mode:"live"). Unlike a headless task (daemon/index.ts runTask → restricted tools +
1
+ // A live, app-drivable agent session spawned on demand by the harbor (task_spawn,
2
+ // mode:"live"). Unlike a headless task (harbor/index.ts runTask → restricted tools +
3
3
  // bypass gate + outbox), this stands up a FULL interactive session behind its own relay
4
4
  // terminal so the app can attach and drive it in real time: stream tokens, approve each
5
5
  // tool, interrupt. It is the same wiring cli/chat.ts uses for `/remote-access`, factored
6
- // so the daemon can create one without a TTY.
6
+ // so the harbor can create one without a TTY.
7
7
  //
8
8
  // SAFETY: the gate runs in "default" mode, so on a driven turn every gated tool relays to
9
9
  // the app for allow/deny (bridge.remoteAsk) and fail-closes if the controller is gone —
@@ -70,7 +70,7 @@ export async function createLiveTaskSession(spec: TaskSpec, deps: LiveTaskDeps):
70
70
  if (lifeTimer) clearTimeout(lifeTimer);
71
71
  try { relay?.stop(); } catch { /* already stopped */ }
72
72
  // Revoke ONLY this session's account inference session so it doesn't linger in the
73
- // app's Linked Devices; the daemon's own child session stays alive. Best-effort.
73
+ // app's Linked Devices; the harbor's own child session stays alive. Best-effort.
74
74
  if (spawnedAccount) {
75
75
  try { await revokeAccountSession(); } catch { /* server TTL is the fallback */ }
76
76
  try { servicesRef?.authStorage?.remove?.("privateer"); } catch { /* nothing persisted */ }
@@ -2,7 +2,7 @@
2
2
  * MCP connector management for the app — the sibling of channelsControl.ts, but for
3
3
  * MCP server config rather than messaging channels. It is what lets the phone/web
4
4
  * client add, toggle, and remove MCP connectors on a Node HOST it drives over the
5
- * relay (the daemon today; an interactive terminal by the same shape).
5
+ * relay (the harbor today; an interactive terminal by the same shape).
6
6
  *
7
7
  * The client itself can NEVER run MCP — a browser tab / RN runtime can't spawn a
8
8
  * stdio child or hold the adapter. So "serving MCP to phone/web" means MANAGING the
@@ -20,7 +20,7 @@
20
20
  * returns an env VALUE — only which env keys exist (`envKeys`) and which are non-empty
21
21
  * (`secretsSet`), by name. save() persists whatever env VALUES it is handed in
22
22
  * `draft.env`; the seal/open of those values in transit is the caller's job (the
23
- * daemon opens a sealed-box addressed to its terminal, mirroring applyChannelSave), so
23
+ * harbor opens a sealed-box addressed to its terminal, mirroring applyChannelSave), so
24
24
  * this module only ever deals in the plaintext files it already owns.
25
25
  *
26
26
  * Framework-agnostic: nothing here imports React or the relay. The caller owns the
@@ -45,7 +45,7 @@ function tsOf(frame: { ts?: unknown }): number | undefined {
45
45
  }
46
46
 
47
47
  // Parse a task_submit/task_spawn frame into a TaskSpec, keeping ONLY well-typed,
48
- // present fields (absent → left undefined). The daemon re-derives the canonical signed
48
+ // present fields (absent → left undefined). The harbor re-derives the canonical signed
49
49
  // args from this (taskControlArgs, undefined → null), so it must not invent fields.
50
50
  export function parseTaskSpec(frame: {
51
51
  prompt?: string;
@@ -79,10 +79,10 @@ function safe(s: string, max: number): string {
79
79
  return clip(redactSecrets(s), max);
80
80
  }
81
81
 
82
- // An ad-hoc task the app asks the daemon to run headlessly (task_submit) or to spawn
82
+ // An ad-hoc task the app asks the harbor to run headlessly (task_submit) or to spawn
83
83
  // as a live-drivable session (task_spawn). Only `prompt` is required; the rest fall back
84
- // to daemon defaults. The SAME field set is what the app canonical-signs into the control
85
- // envelope (client/services/accountSign.ts) and the daemon re-derives to verify (index.ts
84
+ // to harbor defaults. The SAME field set is what the app canonical-signs into the control
85
+ // envelope (client/services/accountSign.ts) and the harbor re-derives to verify (index.ts
86
86
  // taskControlArgs) — keep the two in sync, byte for byte, like the other signed frames.
87
87
  export interface TaskSpec {
88
88
  prompt: string;
@@ -99,7 +99,7 @@ export interface RelayCallbacks {
99
99
  onInterrupt: () => void;
100
100
  // The app asked to turn remote access OFF entirely (the in-app "End remote
101
101
  // access" action). The owner should disable /remote-access — i.e. stop this
102
- // client and not reconnect. Optional: the routines daemon handles it too, but
102
+ // client and not reconnect. Optional: the routines harbor handles it too, but
103
103
  // callbacks that predate it keep compiling.
104
104
  onTerminate?: () => void;
105
105
  // The account signed this terminal out server-side (revoked from the app's
@@ -127,6 +127,10 @@ export interface RelayCallbacks {
127
127
  // The app answered a CLI-initiated text-input prompt (the id from requestInput).
128
128
  // A null value means the app dismissed the prompt without submitting.
129
129
  onInputResponse?: (id: string, value: string | null) => void;
130
+ // The app's composer is autocompleting an `@file` mention — reply with the cwd
131
+ // files/dirs matching `query` (a sendFileMatches frame, keyed by the same id).
132
+ // Read-only; resolution of the picked path still happens on the prompt turn.
133
+ onFilesSearch?: (id: string, query: string) => void;
130
134
  // The app opened the extensions manager — reply with the current installed list
131
135
  // (a sendExtensions frame). Optional so pre-extensions callbacks keep compiling.
132
136
  onExtensionsList?: () => void;
@@ -147,10 +151,10 @@ export interface RelayCallbacks {
147
151
  // The app toggled a user skill's model-invocation availability.
148
152
  onSkillSetEnabled?: (name: string, enabled: boolean, sig?: string, ts?: number) => void;
149
153
  // The app opened the routines manager — reply with the current routines list
150
- // (a sendRoutines frame). Owned by the daemon, so these only fire on its relay.
154
+ // (a sendRoutines frame). Owned by the harbor, so these only fire on its relay.
151
155
  onRoutinesList?: () => void;
152
156
  // The app asked to create (no id) or edit (id) a routine. The raw draft object is
153
- // handed through untyped; the daemon's routinesControl validates it. Signed (H2) —
157
+ // handed through untyped; the harbor's routinesControl validates it. Signed (H2) —
154
158
  // a forged routine runs a headless bypass-mode session (RCE), so it MUST be verified.
155
159
  onRoutinesSave?: (draft: Record<string, unknown>, sig?: string, ts?: number) => void;
156
160
  // The app asked to delete a routine by id or name.
@@ -163,14 +167,14 @@ export interface RelayCallbacks {
163
167
  // right now — a fresh restricted-tool bypass session whose result is sealed to the
164
168
  // account outbox. Signed (H2) — a forged task_submit runs an arbitrary headless
165
169
  // session (RCE), identical in blast radius to a forged routines_run, so it MUST be
166
- // verified via controlAuth before it runs. Daemon-owned (fires only on its relay).
170
+ // verified via controlAuth before it runs. Harbor-owned (fires only on its relay).
167
171
  onTaskSubmit?: (spec: TaskSpec, sig?: string, ts?: number) => void;
168
172
  // The app asked to SPAWN a fresh interactive session it can drive live (mode:"live").
169
- // The daemon stands up a new RemoteBridge terminal and replies (sendTaskSpawned) with
173
+ // The harbor stands up a new RemoteBridge terminal and replies (sendTaskSpawned) with
170
174
  // its termId so the app can attach. Same signed-RCE gate as task_submit.
171
175
  onTaskSpawn?: (spec: TaskSpec, sig?: string, ts?: number) => void;
172
176
  // The app opened the channels manager — reply with the current channel config
173
- // (a sendChannels frame). Owned by the daemon, so these only fire on its relay.
177
+ // (a sendChannels frame). Owned by the harbor, so these only fire on its relay.
174
178
  onChannelsList?: () => void;
175
179
  // The app asked to create/edit a platform's channel config. `draft` carries only
176
180
  // NON-secret fields (roles/posture/tools/model). `sealedSecrets`, when present, is
@@ -183,7 +187,7 @@ export interface RelayCallbacks {
183
187
  // (H2) — a forged removal is a DoS (the bot stops until re-added).
184
188
  onChannelsRemove?: (platform: string, sig?: string, ts?: number) => void;
185
189
  // The app opened the MCP connectors manager — reply with the current MCP config
186
- // (a sendMcp frame). Owned by the daemon (the host that runs the adapter), so these
190
+ // (a sendMcp frame). Owned by the harbor (the host that runs the adapter), so these
187
191
  // only fire on its relay. Read-only, so unsigned.
188
192
  onMcpList?: () => void;
189
193
  // The app asked to create/edit an MCP connector. `draft` carries only NON-secret
@@ -199,12 +203,12 @@ export interface RelayCallbacks {
199
203
  // The app asked to delete a connector by name. Signed (H2) — a forged removal is a DoS.
200
204
  onMcpRemove?: (name: string, sig?: string, ts?: number) => void;
201
205
  // The app opened the workflows manager — reply with the current workflow summaries
202
- // (a sendWorkflows frame). Owned by the daemon, so these only fire on its relay.
206
+ // (a sendWorkflows frame). Owned by the harbor, so these only fire on its relay.
203
207
  onWorkflowsList?: () => void;
204
208
  // The app opened one workflow in its editor — reply with the full graph (sendWorkflow).
205
209
  onWorkflowsGet?: (idOrName: string) => void;
206
210
  // The app asked to create (no workflow.id) or edit (id) a workflow. The raw graph is
207
- // handed through untyped; the daemon's workflowsControl strict-validates it. Signed
211
+ // handed through untyped; the harbor's workflowsControl strict-validates it. Signed
208
212
  // (H2) — a forged save plants a `script` step that BYPASSES the permission gate (RCE),
209
213
  // exactly like a forged routine, so it MUST be verified before it persists.
210
214
  onWorkflowsSave?: (draft: Record<string, unknown>, sig?: string, ts?: number) => void;
@@ -286,7 +290,7 @@ export class RelayClient {
286
290
  private buf = "";
287
291
  private flushTimer: ReturnType<typeof setTimeout> | undefined;
288
292
  // Stable for this process so reconnects keep the same terminal identity. Callers
289
- // may pass a persisted id/label (e.g. the routines daemon, so it shows up as one
293
+ // may pass a persisted id/label (e.g. the routines harbor, so it shows up as one
290
294
  // recognizable "Privateer Routines" terminal across restarts instead of a fresh
291
295
  // random one each time).
292
296
  private readonly termId: string;
@@ -424,6 +428,7 @@ export class RelayClient {
424
428
  title?: string;
425
429
  tools?: unknown;
426
430
  mode?: string;
431
+ query?: string;
427
432
  sig?: string;
428
433
  ts?: number;
429
434
  };
@@ -467,6 +472,9 @@ export class RelayClient {
467
472
  case "input_response":
468
473
  if (frame.id) this.cb.onInputResponse?.(frame.id, typeof frame.value === "string" ? frame.value : null);
469
474
  break;
475
+ case "files_search":
476
+ if (typeof frame.id === "string") this.cb.onFilesSearch?.(frame.id, typeof frame.query === "string" ? frame.query : "");
477
+ break;
470
478
  case "extensions_list":
471
479
  this.cb.onExtensionsList?.();
472
480
  break;
@@ -540,7 +548,7 @@ export class RelayClient {
540
548
  break;
541
549
  case "mcp_save":
542
550
  // The connector rides in `draft` (same slot as channels_save), untyped — the
543
- // daemon strict-validates it via mcpControl.save after the signature check.
551
+ // harbor strict-validates it via mcpControl.save after the signature check.
544
552
  if (frame.draft && typeof frame.draft === "object") {
545
553
  this.cb.onMcpSave?.(
546
554
  frame.draft,
@@ -563,7 +571,7 @@ export class RelayClient {
563
571
  if (typeof frame.idOrName === "string") this.cb.onWorkflowsGet?.(frame.idOrName);
564
572
  break;
565
573
  case "workflows_save":
566
- // The graph rides in `draft` (same slot as channels_save), untyped — the daemon
574
+ // The graph rides in `draft` (same slot as channels_save), untyped — the harbor
567
575
  // strict-validates it via workflowsControl.save after the signature check.
568
576
  if (frame.draft && typeof frame.draft === "object") this.cb.onWorkflowsSave?.(frame.draft, sig(frame), tsOf(frame));
569
577
  break;
@@ -667,7 +675,7 @@ export class RelayClient {
667
675
  }
668
676
 
669
677
  // Push a workflow's live step text / final result to any attached controller as a text
670
- // event, so it renders in the daemon terminal's feed. Same durable-copy caveat as
678
+ // event, so it renders in the harbor terminal's feed. Same durable-copy caveat as
671
679
  // sendRoutineResult (the outbox is the source of truth).
672
680
  sendWorkflowResult(name: string, content: string): boolean {
673
681
  if (!this.isConnected()) return false;
@@ -678,7 +686,7 @@ export class RelayClient {
678
686
 
679
687
  // Tell the app that a live task session was stood up on `termId` (label for display),
680
688
  // so it can open a controller connection to that terminal and drive it. Fire-and-forget
681
- // over the daemon's management relay.
689
+ // over the harbor's management relay.
682
690
  sendTaskSpawned(termId: string, label: string): void {
683
691
  this.rawSend({ type: "task_spawned", termId, label });
684
692
  }
@@ -771,6 +779,18 @@ export class RelayClient {
771
779
  });
772
780
  }
773
781
 
782
+ // Reply to an `@file` autocomplete query with the matching cwd files/dirs. Keyed by
783
+ // the query's id so the app can match it to the in-flight request. Bounded; paths are
784
+ // cwd-relative (the caller — searchFiles — never emits a path outside the cwd subtree,
785
+ // so no machine location leaks beyond the project filenames the driver is browsing).
786
+ sendFileMatches(id: string, matches: { path: string; isDir: boolean }[]): void {
787
+ this.rawSend({
788
+ type: "file_matches",
789
+ id,
790
+ matches: matches.slice(0, 50).map((m) => ({ path: safe(m.path, 300), isDir: !!m.isDir })),
791
+ });
792
+ }
793
+
774
794
  // Push the terminal's installed Pi extensions (the user's own packages; the moat
775
795
  // is excluded upstream) to the app's extensions manager. Sent on request and after
776
796
  // each add/remove. `busy` drives a progress indicator; `needsRestart` tells the app
@@ -823,7 +843,7 @@ export class RelayClient {
823
843
  });
824
844
  }
825
845
 
826
- // Push the daemon's saved routines to the app's routines manager. Sent on request
846
+ // Push the harbor's saved routines to the app's routines manager. Sent on request
827
847
  // and after each save/delete/pause/run. `busy` drives a progress indicator;
828
848
  // `message` carries a one-line result/error. Unlike the feed/webhook paths this is
829
849
  // the user's OWN config echoed back to their OWN app, so fields are size-clipped
@@ -873,7 +893,7 @@ export class RelayClient {
873
893
  });
874
894
  }
875
895
 
876
- // Push the daemon's channel config to the app's channels manager. Sent on request
896
+ // Push the harbor's channel config to the app's channels manager. Sent on request
877
897
  // and after each save/remove. Like sendRoutines this is the user's OWN config
878
898
  // echoed to their OWN app — but a bot token NEVER crosses this wire: only
879
899
  // `secretsSet` (which secret fields are present, by NAME) is sent, so a relay /
@@ -953,7 +973,7 @@ export class RelayClient {
953
973
  });
954
974
  }
955
975
 
956
- // Push the daemon's saved workflows to the app's workflows manager as SUMMARIES (not
976
+ // Push the harbor's saved workflows to the app's workflows manager as SUMMARIES (not
957
977
  // the full graphs — the editor fetches one at a time via sendWorkflow). Sent on request
958
978
  // and after each save/remove/run. The user's OWN config echoed to their OWN app, so
959
979
  // fields are clipped (not redacted). List bounded like the other managers.
@@ -27,6 +27,7 @@ export interface RelayLike {
27
27
  sendCommands(commands: { name: string; description?: string }[]): void;
28
28
  requestSelect(id: string, req: SelectRequest): void;
29
29
  requestInput(id: string, req: InputRequest): void;
30
+ sendFileMatches(id: string, matches: { path: string; isDir: boolean }[]): void;
30
31
  sendExtensions(payload: ExtensionsPayload): void;
31
32
  sendSkills(payload: SkillsPayload): void;
32
33
  }
@@ -93,6 +94,9 @@ export interface RemoteBridgeConfig {
93
94
  onSkillCreate?: (skill: { name: string; description: string; instructions: string }, sig?: string, ts?: number) => void;
94
95
  onSkillDelete?: (name: string, sig?: string, ts?: number) => void;
95
96
  onSkillSetEnabled?: (name: string, enabled: boolean, sig?: string, ts?: number) => void;
97
+ // The app is autocompleting an `@file` mention in its composer — the owner should
98
+ // list the cwd files matching `query` and reply via sendFileMatches(id, …).
99
+ onFilesSearch?: (id: string, query: string) => void;
96
100
  // A controller (re)attached — the owner should push a transcript snapshot.
97
101
  onControllerAttached?: () => void;
98
102
  onStatus?: (text: string) => void;
@@ -139,7 +143,7 @@ export class RemoteBridge {
139
143
  onSkillCreate: (skill, sig, ts) => this.cfg.onSkillCreate?.(skill, sig, ts),
140
144
  onSkillDelete: (name, sig, ts) => this.cfg.onSkillDelete?.(name, sig, ts),
141
145
  onSkillSetEnabled: (name, enabled, sig, ts) => this.cfg.onSkillSetEnabled?.(name, enabled, sig, ts),
142
- // Routines are owned by the daemon, not an interactive session, so its own relay
146
+ // Routines are owned by the harbor, not an interactive session, so its own relay
143
147
  // (not this bridge) handles routines_*. These no-ops just satisfy Required — an
144
148
  // interactive terminal never surfaces the routines manager in the app.
145
149
  onRoutinesList: () => {},
@@ -147,25 +151,25 @@ export class RemoteBridge {
147
151
  onRoutinesDelete: () => {},
148
152
  onRoutinesSetEnabled: () => {},
149
153
  onRoutinesRun: () => {},
150
- // Ad-hoc task spawns are daemon-owned too (they run on / are stood up by the daemon,
154
+ // Ad-hoc task spawns are harbor-owned too (they run on / are stood up by the harbor,
151
155
  // not an interactive session), so its own relay handles task_submit/task_spawn. These
152
156
  // no-ops just satisfy Required — an interactive terminal never receives them.
153
157
  onTaskSubmit: () => {},
154
158
  onTaskSpawn: () => {},
155
- // Channels, like routines, are owned by the daemon (its channels/run.ts config),
156
- // not an interactive session — the daemon's own relay handles channels_*. These
159
+ // Channels, like routines, are owned by the harbor (its channels/run.ts config),
160
+ // not an interactive session — the harbor's own relay handles channels_*. These
157
161
  // no-ops just satisfy Required; an interactive terminal never surfaces channels.
158
162
  onChannelsList: () => {},
159
163
  onChannelsSave: () => {},
160
164
  onChannelsRemove: () => {},
161
- // MCP connectors, like channels, are managed on the daemon (the host that runs the
162
- // adapter) — the daemon's own relay handles mcp_*. These no-ops just satisfy Required;
165
+ // MCP connectors, like channels, are managed on the harbor (the host that runs the
166
+ // adapter) — the harbor's own relay handles mcp_*. These no-ops just satisfy Required;
163
167
  // an interactive terminal manages MCP over IPC (desktop), never over this relay.
164
168
  onMcpList: () => {},
165
169
  onMcpSave: () => {},
166
170
  onMcpSetEnabled: () => {},
167
171
  onMcpRemove: () => {},
168
- // Workflows, like routines/channels, are daemon-owned — the daemon's own relay handles
172
+ // Workflows, like routines/channels, are harbor-owned — the harbor's own relay handles
169
173
  // workflows_*. These no-ops just satisfy Required; an interactive terminal never
170
174
  // surfaces workflows.
171
175
  onWorkflowsList: () => {},
@@ -185,6 +189,7 @@ export class RemoteBridge {
185
189
  const resolve = this.pendingInputs.get(id);
186
190
  if (resolve) resolve(value);
187
191
  },
192
+ onFilesSearch: (id, query) => this.cfg.onFilesSearch?.(id, query),
188
193
  onNoQuarter: (on) => {
189
194
  this.noQuarter = on;
190
195
  this.relay?.sendNoQuarter(on); // echo the ack back so the app's toggle syncs
@@ -239,6 +244,11 @@ export class RemoteBridge {
239
244
  this.relay?.sendCommands(commands);
240
245
  }
241
246
 
247
+ // Reply to an app `@file` autocomplete query with the matching cwd entries.
248
+ sendFileMatches(id: string, matches: { path: string; isDir: boolean }[]): void {
249
+ this.relay?.sendFileMatches(id, matches);
250
+ }
251
+
242
252
  // Push the installed-extensions snapshot to the app's extensions manager.
243
253
  sendExtensions(payload: ExtensionsPayload): void {
244
254
  this.relay?.sendExtensions(payload);
@@ -2,15 +2,15 @@
2
2
  * Routine management for the app.
3
3
  *
4
4
  * A UI-agnostic wrapper over the routines store so the app (over the relay) can
5
- * see the daemon's saved routines and create / edit / delete / pause / run them —
5
+ * see the harbor's saved routines and create / edit / delete / pause / run them —
6
6
  * the sibling of extensionsControl.ts and skillsControl.ts, but for scheduled
7
7
  * tasks rather than Pi packages/skills.
8
8
  *
9
- * Unlike those two, routines are owned by the DAEMON (not an interactive Pi
9
+ * Unlike those two, routines are owned by the HARBOR (not an interactive Pi
10
10
  * session): they live in routines.json (see routines/store.ts) and fire from the
11
- * resident scheduler. So this control is wired into the daemon's own relay
11
+ * resident scheduler. So this control is wired into the harbor's own relay
12
12
  * connection (the "Privateer Routines" terminal), not the REPL/TUI. Running a
13
- * routine now is the one action that needs the daemon itself, so it's injected as
13
+ * routine now is the one action that needs the harbor itself, so it's injected as
14
14
  * `runNow` rather than reaching back into the store.
15
15
  *
16
16
  * Framework-agnostic: nothing here imports React or the relay. The caller owns the
@@ -75,7 +75,7 @@ export interface RoutinesControl {
75
75
  remove(idOrName: string): { ok: boolean; message?: string };
76
76
  // Pause/resume a routine. Resuming reschedules nextRun; pausing clears it.
77
77
  setEnabled(idOrName: string, enabled: boolean): { ok: boolean; message?: string };
78
- // Run a routine now (fire-and-forget on the daemon). ok:false when not found.
78
+ // Run a routine now (fire-and-forget on the harbor). ok:false when not found.
79
79
  run(idOrName: string): { ok: boolean; message?: string };
80
80
  }
81
81
 
@@ -122,11 +122,11 @@ function toRemote(r: Routine): RemoteRoutine {
122
122
  }
123
123
 
124
124
  export function makeRoutinesControl(opts: {
125
- // Working directory for a new routine when the draft omits `cwd` (the daemon's).
125
+ // Working directory for a new routine when the draft omits `cwd` (the harbor's).
126
126
  defaultCwd: () => string;
127
127
  // Is a webhook name declared in config? Guards "webhook:<name>" delivery entries.
128
128
  webhookExists?: (name: string) => boolean;
129
- // Fire a routine now — injected by the daemon (its runRoutine). Absent → run is
129
+ // Fire a routine now — injected by the harbor (its runRoutine). Absent → run is
130
130
  // reported unavailable rather than silently dropped.
131
131
  runNow?: (routine: Routine) => void;
132
132
  }): RoutinesControl {
@@ -3,12 +3,12 @@
3
3
  *
4
4
  * The sibling of routinesControl.ts / channelsControl.ts, for declarative workflow
5
5
  * graphs. UI-agnostic: nothing here imports React or the relay — the caller (the
6
- * daemon) owns the frame plumbing, the signed-frame gate (authorizeControl), and the
7
- * run seam. Like routines, workflows are owned by the DAEMON (they run on its resident
8
- * scheduler / on-demand), so this control is wired into the daemon's own relay.
6
+ * harbor) owns the frame plumbing, the signed-frame gate (authorizeControl), and the
7
+ * run seam. Like routines, workflows are owned by the HARBOR (they run on its resident
8
+ * scheduler / on-demand), so this control is wired into the harbor's own relay.
9
9
  *
10
10
  * A workflow file is an EXECUTABLE artifact (it can carry `script` steps), so the
11
- * daemon MUST verify the account signature on every mutating frame (workflows_save /
11
+ * harbor MUST verify the account signature on every mutating frame (workflows_save /
12
12
  * remove / run) via guardControl BEFORE calling save/remove/run here — a forged save
13
13
  * would plant a script step, a forged run would execute one. `list`/`get` are read-only.
14
14
  *
@@ -48,7 +48,7 @@ export interface WorkflowsControl {
48
48
  save(draft: unknown): { ok: boolean; message?: string; id?: string };
49
49
  // Remove a workflow by id or name. ok:false when nothing matched.
50
50
  remove(idOrName: string): { ok: boolean; message?: string };
51
- // Run a workflow now (fire-and-forget on the daemon). ok:false when not found or the
51
+ // Run a workflow now (fire-and-forget on the harbor). ok:false when not found or the
52
52
  // runner isn't wired.
53
53
  run(idOrName: string): { ok: boolean; message?: string };
54
54
  }
@@ -72,7 +72,7 @@ function draftId(draft: unknown): string | undefined {
72
72
  }
73
73
 
74
74
  export function makeWorkflowsControl(opts: {
75
- // Fire a workflow now — injected by the daemon (it owns the runner + its seams).
75
+ // Fire a workflow now — injected by the harbor (it owns the runner + its seams).
76
76
  // Absent → run is reported unavailable rather than silently dropped (mirrors routines).
77
77
  runNow?: (wf: Workflow) => void;
78
78
  }): WorkflowsControl {
@@ -2,13 +2,13 @@ import type { Routine } from "./schema.ts";
2
2
  import { webhookName } from "./schema.ts";
3
3
  import { writeRoutineOutput, addNotice } from "./store.ts";
4
4
 
5
- // A relay pusher, injected by the daemon. Given the finished result it either
5
+ // A relay pusher, injected by the harbor. Given the finished result it either
6
6
  // forwards it to an attached controller immediately ("live") or persists it to the
7
7
  // pending-relay queue to flush when the app next attaches ("queued"). Either way the
8
8
  // result is durably accounted for, so delivery doesn't add a notice backstop for it.
9
9
  export type RelayPusher = (routine: Routine, content: string) => "live" | "queued";
10
10
 
11
- // A cloud-outbox pusher, injected by the daemon. Seals the result to the account's
11
+ // A cloud-outbox pusher, injected by the harbor. Seals the result to the account's
12
12
  // outbox public key and POSTs the ciphertext to the server (E2EE store-and-forward),
13
13
  // or buffers it durably on failure. Returns "sent" when the server accepted it,
14
14
  // "queued" when it was persisted for a later flush — either way the result is
@@ -31,7 +31,7 @@ export interface DeliveryContext {
31
31
  // Named endpoints for "webhook:<name>" delivery entries; results POST here.
32
32
  webhooks?: Record<string, WebhookTarget>;
33
33
  // Scrubs secrets from anything leaving the machine. Webhook bodies always pass
34
- // through this when provided (the daemon wires in redactText).
34
+ // through this when provided (the harbor wires in redactText).
35
35
  redact?: (text: string) => string;
36
36
  // Injectable for tests; defaults to global fetch.
37
37
  fetchImpl?: typeof fetch;
@@ -39,7 +39,7 @@ export interface DeliveryContext {
39
39
 
40
40
  export interface DeliveryReport {
41
41
  // Channels that actually delivered (email is handled inside the agent run, so it
42
- // never appears here — see the daemon). Failed webhooks show as
42
+ // never appears here — see the harbor). Failed webhooks show as
43
43
  // "webhook:<name>(failed)" and leave a notice so the result isn't silently lost.
44
44
  delivered: string[];
45
45
  // Absolute path to latest.md when file delivery ran.
@@ -101,7 +101,7 @@ async function postWebhook(
101
101
  // result is never silently lost. `webhook:<name>` POSTs the (redacted) result to a
102
102
  // named endpoint from config; a failed or unconfigured webhook leaves a notice.
103
103
  // `email` is intentionally not handled here: it is fulfilled inside the agent turn
104
- // (the daemon adds the Gmail tool + an instruction to the prompt) so plaintext
104
+ // (the harbor adds the Gmail tool + an instruction to the prompt) so plaintext
105
105
  // egress stays an explicit, gated action.
106
106
  export async function deliver(
107
107
  routine: Routine,
@@ -127,7 +127,7 @@ export async function deliver(
127
127
  }
128
128
 
129
129
  // Relay: pushed live to an attached controller, or queued to flush when the app
130
- // next attaches (the daemon persists that queue, so it's durable either way). Only
130
+ // next attaches (the harbor persists that queue, so it's durable either way). Only
131
131
  // when no pusher is wired at all do we fall back to a notice so it isn't lost.
132
132
  if (wants.has("relay")) {
133
133
  const pushed = ctx.pushRelay?.(routine, content);
@@ -27,7 +27,7 @@ export function webhookName(entry: string): string | null {
27
27
  }
28
28
 
29
29
  // A saved, unattended agent task. Persisted in routines.json and executed by the
30
- // daemon when its trigger comes due. A routine's trigger is EITHER recurring (a
30
+ // harbor when its trigger comes due. A routine's trigger is EITHER recurring (a
31
31
  // cron expression) or one-off (`at`, a specific datetime) — exactly one is set.
32
32
  export const Routine = z
33
33
  .object({
@@ -48,14 +48,14 @@ export const Routine = z
48
48
  model: z.string().optional(),
49
49
  // Where to deliver the result. Defaults to on-box file output.
50
50
  delivery: z.array(DeliveryEntry).default(["file"]),
51
- // Optional tool allow-subset. Unset → the safe read/web set (see daemon). Entries
51
+ // Optional tool allow-subset. Unset → the safe read/web set (see harbor). Entries
52
52
  // may be builtin names ("read") or MCP selectors — "<server>__<tool>" exact or
53
53
  // "<server>__*" for a whole server (see routines/toolSelect.ts). Selected MCP
54
54
  // tools run unattended under the auto-approve gate, so grant the minimum needed.
55
55
  tools: z.array(z.string()).optional(),
56
56
  // Paused routines stay in the file but never fire.
57
57
  enabled: z.boolean().default(true),
58
- // Bookkeeping, updated by the daemon after each run.
58
+ // Bookkeeping, updated by the harbor after each run.
59
59
  lastRun: z.string().optional(),
60
60
  lastStatus: z.enum(["ok", "error"]).optional(),
61
61
  lastError: z.string().optional(),
@@ -20,7 +20,7 @@ function slug(name: string): string {
20
20
  return name.trim().toLowerCase().replace(/[^a-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "") || "routine";
21
21
  }
22
22
 
23
- // A stable relay terminal id for the daemon, persisted so it reappears as the same
23
+ // A stable relay terminal id for the harbor, persisted so it reappears as the same
24
24
  // "Privateer Routines" terminal in the app across restarts (rather than a fresh
25
25
  // random terminal each boot). Random on first use so it stays unique per install —
26
26
  // the relay routes on this id with no user namespacing, so a shared constant could
@@ -53,7 +53,7 @@ export function loadRoutines(): Routine[] {
53
53
  try {
54
54
  return RoutineFile.parse(JSON.parse(readFileSync(path, "utf8"))).routines;
55
55
  } catch {
56
- // A corrupt or hand-edited file shouldn't crash the daemon; treat as empty.
56
+ // A corrupt or hand-edited file shouldn't crash the harbor; treat as empty.
57
57
  return [];
58
58
  }
59
59
  }
@@ -160,7 +160,7 @@ export function drainNotices(): RoutineNotice[] {
160
160
  }
161
161
 
162
162
  // A relay result produced while no controller was attached, held until the app
163
- // next connects. Persisted (not just in-memory) so it survives a daemon restart.
163
+ // next connects. Persisted (not just in-memory) so it survives a harbor restart.
164
164
  export interface PendingRelay {
165
165
  routine: string;
166
166
  at: string; // ISO timestamp
@@ -16,7 +16,7 @@ export function triggerError(t: Trigger): string | null {
16
16
 
17
17
  // The fire time to store as `nextRun`. For cron: the next match strictly after
18
18
  // `from`. For a one-off: the fixed `at` time as-is (even if already past, so a
19
- // missed one-off still fires once when the daemon comes back). Null if unparseable.
19
+ // missed one-off still fires once when the harbor comes back). Null if unparseable.
20
20
  export function computeNextRun(t: Trigger, from: Date = new Date()): Date | null {
21
21
  if (t.cron) return cronNext(t.cron, from);
22
22
  if (t.at) {
@@ -1,6 +1,6 @@
1
1
  // The `create_routine` tool — lets the agent turn "summarize the news every morning"
2
- // or "remind me at 3pm tomorrow" into a saved routine the scheduler daemon runs
3
- // unattended (see src/daemon). Ported from tree-cli/src/tools/routine.ts, adapted to
2
+ // or "remind me at 3pm tomorrow" into a saved routine the scheduler harbor runs
3
+ // unattended (see src/harbor). Ported from tree-cli/src/tools/routine.ts, adapted to
4
4
  // Pi's registerTool (TypeBox schema) with the in-tool ctx.gate.request removed: in
5
5
  // the Pi model our permission-gate extension gates the tool_call itself (classify.ts
6
6
  // gives it a "Create routine" prompt + flags email/webhook egress).
@@ -11,7 +11,7 @@ import { configPath } from "../config/paths.ts";
11
11
  import { newRoutineId, webhookName, type Routine } from "../routines/schema.ts";
12
12
  import { triggerError, computeNextRun, describeTrigger } from "../routines/trigger.ts";
13
13
  import { upsertRoutine } from "../routines/store.ts";
14
- import { sendToDaemon, DaemonNotRunningError } from "../daemon/ipc.ts";
14
+ import { sendToHarbor, HarborNotRunningError } from "../harbor/ipc.ts";
15
15
 
16
16
  const KNOWN_CHANNELS = new Set(["file", "relay", "notice", "cloud", "email"]);
17
17
 
@@ -106,20 +106,20 @@ export const routineToolDefinition = {
106
106
  nextRun: next?.toISOString(),
107
107
  };
108
108
 
109
- // Hand to the running daemon (it validates + schedules); fall back to writing the
110
- // file so the routine persists until the daemon starts.
109
+ // Hand to the running harbor (it validates + schedules); fall back to writing the
110
+ // file so the routine persists until the harbor starts.
111
111
  try {
112
- const res = await sendToDaemon({ cmd: "add", routine });
112
+ const res = await sendToHarbor({ cmd: "add", routine });
113
113
  if (!res.ok) return text(`Error saving routine: ${res.message ?? "unknown"}`);
114
114
  return text(
115
115
  `Created routine "${name}" (${describeTrigger({ cron, at })}). ` +
116
116
  `Next run ${next ? next.toLocaleString() : "unknown"}, delivery: ${chans.join(", ")}.`,
117
117
  );
118
118
  } catch (e) {
119
- if (e instanceof DaemonNotRunningError) {
119
+ if (e instanceof HarborNotRunningError) {
120
120
  upsertRoutine(routine);
121
121
  return text(
122
- `Saved routine "${name}" (${describeTrigger({ cron, at })}), but the scheduler daemon isn't ` +
122
+ `Saved routine "${name}" (${describeTrigger({ cron, at })}), but the scheduler harbor isn't ` +
123
123
  `running yet, so it won't fire until you start it.`,
124
124
  );
125
125
  }