@pellux/goodvibes-daemon 1.28.19 → 1.28.21

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 (114) hide show
  1. package/CHANGELOG.md +119 -63
  2. package/README.md +24 -19
  3. package/bin/launcher-support.js +5 -5
  4. package/package.json +6 -4
  5. package/scripts/postinstall.js +8 -8
  6. package/src/cli/command-catalog.ts +22 -22
  7. package/src/cli/completion.ts +4 -4
  8. package/src/cli/help.ts +5 -5
  9. package/src/cli/index.ts +3 -3
  10. package/src/cli/parser.ts +2 -2
  11. package/src/cli/surface-catalog.ts +1 -1
  12. package/src/cli/types.ts +2 -2
  13. package/src/cluster/daemon-ws-call.ts +5 -5
  14. package/src/cluster/raw-reply-route.ts +5 -5
  15. package/src/config/checkpoint-settings.ts +7 -7
  16. package/src/config/config-key-guard.ts +22 -0
  17. package/src/config/run-daemon-config-migration.ts +3 -3
  18. package/src/config/secret-config.ts +7 -7
  19. package/src/config/surface.ts +3 -3
  20. package/src/core/pairing-banner.ts +5 -5
  21. package/src/daemon/cli.ts +45 -43
  22. package/src/daemon/config-command.ts +15 -15
  23. package/src/daemon/handlers/context.ts +1 -1
  24. package/src/daemon/handlers/contracts.ts +19 -4
  25. package/src/daemon/handlers/credentials.ts +1 -1
  26. package/src/daemon/handlers/drafts/draft-store.ts +3 -3
  27. package/src/daemon/handlers/drafts/register.ts +4 -4
  28. package/src/daemon/handlers/inbox/aggregator.ts +8 -8
  29. package/src/daemon/handlers/inbox/cursor-store.ts +10 -10
  30. package/src/daemon/handlers/inbox/index.ts +7 -7
  31. package/src/daemon/handlers/inbox/mapping.ts +2 -2
  32. package/src/daemon/handlers/inbox/poller.ts +5 -5
  33. package/src/daemon/handlers/inbox/provider-adapter.ts +8 -8
  34. package/src/daemon/handlers/inbox/providers/discord.ts +6 -6
  35. package/src/daemon/handlers/inbox/providers/email.ts +3 -3
  36. package/src/daemon/handlers/inbox/providers/imap-client.ts +1 -1
  37. package/src/daemon/handlers/inbox/providers/slack.ts +4 -4
  38. package/src/daemon/handlers/index.ts +18 -8
  39. package/src/daemon/handlers/payments/address-store.ts +54 -0
  40. package/src/daemon/handlers/payments/budget-store.ts +356 -0
  41. package/src/daemon/handlers/payments/card-store.ts +486 -0
  42. package/src/daemon/handlers/payments/checkout-handlers.ts +526 -0
  43. package/src/daemon/handlers/payments/index.ts +38 -0
  44. package/src/daemon/handlers/payments/merchant-judge.ts +57 -0
  45. package/src/daemon/handlers/payments/notifier.ts +112 -0
  46. package/src/daemon/handlers/payments/purchase-ledger.ts +108 -0
  47. package/src/daemon/handlers/payments/register.ts +518 -0
  48. package/src/daemon/handlers/register.ts +3 -3
  49. package/src/daemon/handlers/remote/backends/cloud-terminal.ts +9 -1
  50. package/src/daemon/handlers/remote/backends/process-runner.ts +1 -1
  51. package/src/daemon/handlers/remote/backends/ssh.ts +9 -1
  52. package/src/daemon/handlers/remote/backends/types.ts +2 -2
  53. package/src/daemon/handlers/remote/dispatcher.ts +3 -3
  54. package/src/daemon/handlers/remote/index.ts +1 -1
  55. package/src/daemon/handlers/remote/peer-registry.ts +62 -13
  56. package/src/daemon/handlers/routing/inbox-bridge.ts +5 -5
  57. package/src/daemon/handlers/routing/index.ts +1 -1
  58. package/src/daemon/handlers/routing/route-store.ts +1 -1
  59. package/src/daemon/handlers/routing/routing-resolver.ts +3 -3
  60. package/src/daemon/handlers/sqlite-store.ts +9 -9
  61. package/src/daemon/handlers/triage/index.ts +1 -1
  62. package/src/daemon/handlers/triage/integration.ts +3 -3
  63. package/src/daemon/handlers/triage/pipeline.ts +2 -2
  64. package/src/daemon/handlers/triage/scorer.ts +2 -2
  65. package/src/daemon/handlers/triage/tagger/discord.ts +3 -3
  66. package/src/daemon/handlers/triage/tagger/imap.ts +7 -7
  67. package/src/daemon/handlers/triage/tagger/index.ts +1 -1
  68. package/src/daemon/handlers/triage/tagger/shared.ts +3 -3
  69. package/src/daemon/handlers/triage/tagger/slack.ts +1 -1
  70. package/src/daemon/handlers/triage/types.ts +2 -2
  71. package/src/daemon/lifecycle.ts +5 -5
  72. package/src/daemon/local-daemon-state.ts +7 -7
  73. package/src/daemon/pair-command.ts +14 -14
  74. package/src/daemon/provision-wake-model.ts +5 -5
  75. package/src/daemon/send/channels.ts +7 -7
  76. package/src/daemon/send/command.ts +11 -11
  77. package/src/daemon/send/composition.ts +5 -5
  78. package/src/daemon/send/failure-text.ts +6 -6
  79. package/src/daemon/send/inert-text.ts +18 -18
  80. package/src/daemon/send/stdin.ts +3 -3
  81. package/src/daemon/service-commands.ts +32 -32
  82. package/src/daemon/sessions-command.ts +7 -7
  83. package/src/daemon/status-command.ts +22 -22
  84. package/src/daemon/webui-command.ts +14 -14
  85. package/src/runtime/boot-tasks.ts +1 -1
  86. package/src/runtime/browser-checkout-seam-holder.ts +55 -0
  87. package/src/runtime/cluster-composition.ts +9 -9
  88. package/src/runtime/cluster-group-composition.ts +7 -7
  89. package/src/runtime/conversation-rewind-port.ts +8 -8
  90. package/src/runtime/credential-composition.ts +2 -2
  91. package/src/runtime/daemon-handler-composition.ts +61 -4
  92. package/src/runtime/device-posture-composition.ts +10 -10
  93. package/src/runtime/disposal-wiring.ts +8 -8
  94. package/src/runtime/fleet-needs-input-push.ts +4 -4
  95. package/src/runtime/fleet-services.ts +1 -1
  96. package/src/runtime/hosted-session-composition.ts +13 -13
  97. package/src/runtime/index.ts +1 -1
  98. package/src/runtime/knowledge-services.ts +2 -2
  99. package/src/runtime/legacy-daemon-migration.ts +43 -43
  100. package/src/runtime/legacy-daemon-reconcile.ts +30 -30
  101. package/src/runtime/mail-composition.ts +6 -6
  102. package/src/runtime/notification-dispatch.ts +7 -7
  103. package/src/runtime/payments-composition.ts +187 -0
  104. package/src/runtime/plugin-composition.ts +7 -7
  105. package/src/runtime/runtime-services-types.ts +9 -9
  106. package/src/runtime/services.ts +41 -32
  107. package/src/runtime/trigger-services.ts +1 -1
  108. package/src/runtime/trust/checkpoint-eligibility.ts +5 -5
  109. package/src/runtime/trust/trust-gated-approvals.ts +9 -9
  110. package/src/runtime/update-check.ts +4 -4
  111. package/src/runtime/workspace-checkpointing.ts +6 -6
  112. package/src/testing/daemon-fixture.ts +11 -11
  113. package/src/testing/hosted-session-failures.ts +4 -4
  114. package/src/version.ts +2 -2
@@ -1,9 +1,9 @@
1
1
  /**
2
- * pair-command.ts `goodvibes-daemon pair`.
2
+ * pair-command.ts, `goodvibes-daemon pair`.
3
3
  *
4
4
  * Two forms.
5
5
  *
6
- * LOCAL FORM `pair` with no `--host`, or one naming this machine reprints
6
+ * LOCAL FORM, `pair` with no `--host`, or one naming this machine, reprints
7
7
  * the pairing block a daemon prints once as it starts: the web origin, the
8
8
  * offers a new device can accept, what it will be able to do, and a QR code
9
9
  * encoding the deep link that opens the web app already signed in. It reads
@@ -12,13 +12,13 @@
12
12
  * resolve to) and reprints the EXISTING token rather than minting a new one,
13
13
  * so a link printed here and the one printed at boot are the same link.
14
14
  *
15
- * REMOTE FORM `pair --host <name> [--port] [--token]` asks THAT daemon to
15
+ * REMOTE FORM, `pair --host <name> [--port] [--token]`, asks THAT daemon to
16
16
  * MINT A NEW per-device pairing token over `pairing.handoff.create` and prints
17
17
  * the pairing block for it. Minting is a different act than reprinting: it is
18
18
  * a fresh token, and every token that daemon already issued (its own shared
19
19
  * token included) is left exactly as it was. Because it changes state on a
20
20
  * daemon that may not be this process's own, it states the plan and asks for
21
- * confirmation before acting `-y`/`--yes` satisfies that non-interactively,
21
+ * confirmation before acting, `-y`/`--yes` satisfies that non-interactively,
22
22
  * the same convention `migrate-service` uses. An unreachable daemon, a
23
23
  * rejected token, and a daemon too old to serve the verb are each refused by
24
24
  * name (see `callDaemonWsVerb`), never a stack trace.
@@ -84,7 +84,7 @@ function failure(error: string, fix: string, json: boolean): DaemonCommandResult
84
84
  /**
85
85
  * The web origin a remote handoff's deep link opens, recovered from the link
86
86
  * itself. `buildPairingHandoffLink` in the SDK builds the link as exactly
87
- * `<webOrigin, trailing slashes stripped>/#<params>` so slicing at the
87
+ * `<webOrigin, trailing slashes stripped>/#<params>`, so slicing at the
88
88
  * first `/#` is the precise inverse, and feeding the result back into
89
89
  * `renderPairingBanner` (which rebuilds the SAME link from origin + token +
90
90
  * offers) reproduces byte-for-byte what the remote daemon already returned.
@@ -103,19 +103,19 @@ interface PairingHandoffOfferDetail {
103
103
  readonly available?: boolean;
104
104
  }
105
105
 
106
- /** The shape `pairing.handoff.create` returns see routes/pairing-handoff.ts. */
106
+ /** The shape `pairing.handoff.create` returns, see routes/pairing-handoff.ts. */
107
107
  interface PairingHandoffCreateResult {
108
108
  readonly token: {
109
109
  readonly id: string;
110
110
  readonly name: string;
111
- /** The plaintext secret returned exactly once. */
111
+ /** The plaintext secret, returned exactly once. */
112
112
  readonly token: string;
113
113
  readonly createdAt: number;
114
114
  };
115
115
  readonly offers: readonly PairingHandoffOfferDetail[];
116
- /** `#pair=<token>&offers=...` present even when no web origin is configured. */
116
+ /** `#pair=<token>&offers=...`, present even when no web origin is configured. */
117
117
  readonly fragment: string;
118
- /** `<webOrigin>/#pair=...` present only when that daemon has a web origin configured. */
118
+ /** `<webOrigin>/#pair=...`, present only when that daemon has a web origin configured. */
119
119
  readonly deepLink?: string;
120
120
  }
121
121
 
@@ -127,7 +127,7 @@ function runLocalReprint(input: RunPairCommandInput): DaemonCommandResult {
127
127
  if (token === undefined) {
128
128
  return failure(
129
129
  'no operator token was found for this machine, so there is no link to print',
130
- 'start the daemon once it creates the token as it starts: goodvibes-daemon serve',
130
+ 'start the daemon once; it creates the token as it starts: goodvibes-daemon serve',
131
131
  flags.json,
132
132
  );
133
133
  }
@@ -175,7 +175,7 @@ function mintPlanResult(target: RemoteDaemonTarget, json: boolean): DaemonComman
175
175
  `pair --host: this will MINT A NEW per-device pairing token on the daemon at ${target.baseUrl}`,
176
176
  'and print the pairing link/QR for that new token.',
177
177
  '',
178
- "That daemon's existing tokens its shared token and every other paired device are",
178
+ "That daemon's existing tokens (its shared token and every other paired device) are",
179
179
  'left exactly as they are: minting is a different act than reprinting, and this is a',
180
180
  'fresh token, not a link to one that already exists.',
181
181
  '',
@@ -208,15 +208,15 @@ async function runRemoteMint(input: RunPairCommandInput, target: RemoteDaemonTar
208
208
  const mintedLine = `minted a new per-device pairing token ("${data.token.name}") on the daemon at ${target.baseUrl}.`;
209
209
 
210
210
  if (data.deepLink === undefined) {
211
- // Honest degraded path: the mint itself succeeded the token is real and
212
- // usable but that daemon has no web origin configured, and this process
211
+ // Honest degraded path: the mint itself succeeded, the token is real and
212
+ // usable, but that daemon has no web origin configured, and this process
213
213
  // has no way to read or fabricate one for it. `resolvePairingWebOrigin` is
214
214
  // what the local form reads instead, and it is exactly the thing this
215
215
  // process cannot ask a REMOTE daemon for outside this verb's own reply.
216
216
  const lines = [
217
217
  mintedLine,
218
218
  '',
219
- 'that daemon has no web origin configured, so no deep link or QR could be built only',
219
+ 'that daemon has no web origin configured, so no deep link or QR could be built; only',
220
220
  'the raw token and pairing fragment:',
221
221
  '',
222
222
  ` token: ${data.token.token}`,
@@ -1,11 +1,11 @@
1
1
  // ---------------------------------------------------------------------------
2
- // provision-wake-model.ts `goodvibes-daemon provision-wake-model`.
2
+ // provision-wake-model.ts, `goodvibes-daemon provision-wake-model`.
3
3
  //
4
4
  // WHAT THIS IS FOR
5
5
  //
6
6
  // The curl installer needs to put the wake-word model on the machine, and it must
7
7
  // not hold the pinned URLs, byte counts or checksums to do it. Those live in ONE
8
- // place the SDK's wake-word manifest and a shell script that copied them
8
+ // place, the SDK's wake-word manifest, and a shell script that copied them
9
9
  // would be a second copy of a pin, drifting silently the first time the model is
10
10
  // retrained. So the installer runs the binary it just installed and lets the SDK
11
11
  // do what the SDK owns.
@@ -14,8 +14,8 @@
14
14
  //
15
15
  // The caller is an installer. A wake-word model is not a reason to fail installing
16
16
  // a coding tool, and an installer that aborts half-way through is worse than one
17
- // that finishes without a wake word. The outcome is printed either way one plain
18
- // line naming what happened and how to retry and a running daemon retries at
17
+ // that finishes without a wake word. The outcome is printed either way, one plain
18
+ // line naming what happened and how to retry, and a running daemon retries at
19
19
  // every boot. `--strict` is there for a caller that genuinely wants the exit code
20
20
  // to carry the result (a test, a provisioning script that is checking); the
21
21
  // installer does not pass it.
@@ -66,7 +66,7 @@ export async function runProvisionWakeModelCommand(
66
66
  const message = error instanceof Error ? error.message : String(error);
67
67
  return {
68
68
  exitCode: strict ? 1 : 0,
69
- lines: [`wake-word model: skipped ${message}`],
69
+ lines: [`wake-word model: skipped: ${message}`],
70
70
  };
71
71
  }
72
72
  const outcome = await provision({
@@ -1,5 +1,5 @@
1
1
  /**
2
- * channels.ts which channels `goodvibes-daemon send` can reach, and which one
2
+ * channels.ts, which channels `goodvibes-daemon send` can reach, and which one
3
3
  * it uses when the operator names none.
4
4
  *
5
5
  * The surface list, labels and required-setup keys are IMPORTED from
@@ -7,7 +7,7 @@
7
7
  * list` and `goodvibes-daemon send --list` can never disagree about what a
8
8
  * channel is called or what it needs. What this module adds on top is the two
9
9
  * facts that command needs and `SURFACE_CONFIGS` does not carry: the routable
10
- * `ChannelDeliverySurfaceKind` for each surface id (they differ in case
10
+ * `ChannelDeliverySurfaceKind` for each surface id (they differ in case,
11
11
  * `googleChat` vs `google-chat`), and the settings key holding each surface's
12
12
  * default destination.
13
13
  */
@@ -18,7 +18,7 @@ import { SURFACE_CONFIGS } from '../../cli/surface-catalog.ts';
18
18
  import { canRenderInert } from './inert-text.ts';
19
19
 
20
20
  /**
21
- * The settings key holding each supported surface's default destination the
21
+ * The settings key holding each supported surface's default destination, the
22
22
  * chat, channel, topic or URL a message goes to when `--to` is not given.
23
23
  *
24
24
  * These are the SAME keys the delivery strategies in the SDK's
@@ -58,7 +58,7 @@ const SURFACE_KIND_BY_ID: Readonly<Record<string, ChannelDeliverySurfaceKind>> =
58
58
  * This is naming, not a scheme: `--to` becomes `ChannelDeliveryTarget.address`,
59
59
  * which is the first thing every strategy in `strategies-core.ts`,
60
60
  * `strategies-bridge.ts` and `strategies-enterprise.ts` already checks before
61
- * falling back to its configured default. Nothing new is invented the label
61
+ * falling back to its configured default. Nothing new is invented, the label
62
62
  * exists so `--list` and the help can say "topic" where the channel says topic
63
63
  * and "chat id" where it says chat id, rather than making the operator work out
64
64
  * what an "address" is for ntfy.
@@ -120,7 +120,7 @@ export const SEND_CHANNELS: readonly SendChannel[] = SURFACE_CONFIGS
120
120
 
121
121
  /**
122
122
  * Look a channel up by the id an operator typed, accepting either the settings
123
- * id (`googleChat`) or the routable kind (`google-chat`) the two spellings
123
+ * id (`googleChat`) or the routable kind (`google-chat`), the two spellings
124
124
  * are both visible in this product's own output, and making the operator
125
125
  * remember which one this command wants would be a trap.
126
126
  */
@@ -172,7 +172,7 @@ export type DefaultChannelResolution =
172
172
  /**
173
173
  * Which channel a `send` with no `--channel` goes to.
174
174
  *
175
- * A channel qualifies when it is switched on AND has a destination configured
175
+ * A channel qualifies when it is switched on AND has a destination configured,
176
176
  * "enabled" alone is not enough, because an enabled surface with a blank
177
177
  * destination is a channel that would throw at the provider rather than deliver.
178
178
  *
@@ -180,7 +180,7 @@ export type DefaultChannelResolution =
180
180
  * no non-arbitrary winner, and this command has an outward effect: sending the
181
181
  * owner's message to the wrong one of his channels is worse than printing the
182
182
  * list and exiting non-zero. There is deliberately no priority order here to
183
- * silently break that tie a preference ordering baked into this file would be
183
+ * silently break that tie, a preference ordering baked into this file would be
184
184
  * an invisible decision about where his messages go.
185
185
  */
186
186
  export function resolveDefaultChannel(config: Pick<ConfigManager, 'get'>): DefaultChannelResolution {
@@ -1,5 +1,5 @@
1
1
  /**
2
- * command.ts `goodvibes-daemon send`, the shell's way to put a message on one
2
+ * command.ts, `goodvibes-daemon send`, the shell's way to put a message on one
3
3
  * of the owner's channels.
4
4
  *
5
5
  * ## Why this exists
@@ -7,7 +7,7 @@
7
7
  * Nothing on this machine could send the owner a message from a script. The
8
8
  * three binaries exposed no send verb, the daemon's HTTP API answers
9
9
  * `401 AUTH_REQUIRED` to the operator token as stored, and driving the agent to
10
- * do it failed with `Missing Telegram bot token` because the credential lives
10
+ * do it failed with `Missing Telegram bot token`, because the credential lives
11
11
  * in the DAEMON tier (`~/.goodvibes/daemon/settings.json`) and the agent was
12
12
  * reading its own surface silo. So the one process that could always send was
13
13
  * the daemon, and only while it was running.
@@ -15,7 +15,7 @@
15
15
  * ## Three properties this command is built around
16
16
  *
17
17
  * 1. **It uses the delivery path, it is not a second sender.** Everything below
18
- * ends in `ChannelDeliveryRouter.deliver()` the same call
18
+ * ends in `ChannelDeliveryRouter.deliver()`, the same call
19
19
  * `AutomationDeliveryManager.sendTarget` makes, reaching the same
20
20
  * per-surface strategies in `strategies-core.ts`. Nothing here talks to a
21
21
  * provider API directly.
@@ -26,7 +26,7 @@
26
26
  * was NOT used, deliberately: it returns an empty array when a feature gate
27
27
  * is off and returns failed attempts rather than throwing, so a caller that
28
28
  * did not inspect its result would report success for a message that never
29
- * left the machine the exact false-green this command exists to avoid. The
29
+ * left the machine, the exact false-green this command exists to avoid. The
30
30
  * gate check it would have done is done here instead, explicitly, and a gate
31
31
  * that is off produces a refusal naming the settings key rather than silence.
32
32
  *
@@ -34,8 +34,8 @@
34
34
  * through `inertBodyFor` for the target surface before it reaches the router,
35
35
  * and there is no flag, env var or code path that skips that. The message
36
36
  * normally comes from the operator's own shell, but the command must not
37
- * become the way something else's text a log line, a captured error, a
38
- * remote agent's output piped in arrives on the owner's phone rendered as
37
+ * become the way something else's text, a log line, a captured error, a
38
+ * remote agent's output piped in, arrives on the owner's phone rendered as
39
39
  * live markup with a clickable link in it. See inert-text.ts.
40
40
  */
41
41
 
@@ -51,7 +51,7 @@ import { inertBodyFor } from './inert-text.ts';
51
51
  export type SendDeliver = (request: ChannelDeliveryRequest) => Promise<string | undefined>;
52
52
 
53
53
  export interface SendCommandDeps {
54
- /** Built with a `homeDir` so the daemon tier overlays see channels.ts. */
54
+ /** Built with a `homeDir` so the daemon tier overlays, see channels.ts. */
55
55
  readonly configManager: Pick<ConfigManager, 'get'>;
56
56
  readonly deliver: SendDeliver;
57
57
  /** Reads the whole of stdin; only called when no message argument was given. */
@@ -93,12 +93,12 @@ const USAGE = [
93
93
  ' --list Show every channel, whether it is on, and where it sends.',
94
94
  ' -h, --help Print this help',
95
95
  '',
96
- 'A channel that is switched off is refused by name and NOTHING is sent the',
96
+ 'A channel that is switched off is refused by name and NOTHING is sent: the',
97
97
  'command never quietly falls back to the default, so a message meant for a',
98
98
  'quiet channel cannot end up on a noisy one.',
99
99
  '',
100
100
  'The message is always delivered as literal text: markup a channel would',
101
- 'otherwise render a Discord masked link, a Slack mention arrives inert.',
101
+ 'otherwise render (a Discord masked link, a Slack mention) arrives inert.',
102
102
  ].join('\n');
103
103
 
104
104
  function parseSendArgs(argv: readonly string[]): ParsedSendArgs {
@@ -153,7 +153,7 @@ function describeConfiguredChannels(config: Pick<ConfigManager, 'get'>): string
153
153
  .map((entry) => entry.channel.id);
154
154
  return usable.length > 0
155
155
  ? `Configured and ready: ${usable.join(', ')}.`
156
- : 'No channel is currently both switched on and given a destination run: goodvibes-daemon send --list';
156
+ : 'No channel is currently both switched on and given a destination; run: goodvibes-daemon send --list';
157
157
  }
158
158
 
159
159
  function renderChannelList(config: Pick<ConfigManager, 'get'>): string[] {
@@ -304,7 +304,7 @@ export async function runSendCommand(
304
304
  };
305
305
 
306
306
  const preamble = usedDefault
307
- ? [`No channel named using ${channel.id}: ${defaultReason}.`]
307
+ ? [`No channel named; using ${channel.id}: ${defaultReason}.`]
308
308
  : [];
309
309
  try {
310
310
  const responseId = await deps.deliver(request);
@@ -1,5 +1,5 @@
1
1
  /**
2
- * composition.ts the smallest set of services that can put a message on a
2
+ * composition.ts, the smallest set of services that can put a message on a
3
3
  * channel, built from the daemon's own tier.
4
4
  *
5
5
  * ## Why not `createRuntimeServices`
@@ -7,7 +7,7 @@
7
7
  * The daemon's full runtime graph starts a LAN scan, a cluster coordinator, an
8
8
  * inbox poller, a fleet tick, a memory governor and a config watch. Composing
9
9
  * it to send one message would build a second, competing set of that state on a
10
- * machine that is already running a daemon the same reason `cluster …` is
10
+ * machine that is already running a daemon, the same reason `cluster …` is
11
11
  * intercepted before any runtime is constructed (see src/daemon/cli.ts). This
12
12
  * builds only the five objects `ChannelDeliveryRouter` needs and starts no
13
13
  * timers, binds no sockets and joins no election, so it is safe to run beside a
@@ -18,7 +18,7 @@
18
18
  * That was tried before this command existed: the control-plane API answers
19
19
  * `401 AUTH_REQUIRED` to the operator token as stored on disk. More importantly
20
20
  * an HTTP-backed send would only work while a daemon is up, and the case this
21
- * command is for telling the owner that something has stopped is exactly
21
+ * command is for, telling the owner that something has stopped, is exactly
22
22
  * when it may not be.
23
23
  *
24
24
  * ## Where the credentials come from
@@ -42,7 +42,7 @@ import { GOODVIBES_DAEMON_SURFACE_ROOT } from '../../config/surface.ts';
42
42
 
43
43
  export interface SendStackRoots {
44
44
  readonly workingDirectory: string;
45
- /** The GoodVibes tree root the directory `.goodvibes/` sits under. */
45
+ /** The GoodVibes tree root, the directory `.goodvibes/` sits under. */
46
46
  readonly homeDirectory: string;
47
47
  /** The daemon's own state root, holding the daemon-scoped secret stores. */
48
48
  readonly daemonHomeDirectory: string;
@@ -62,7 +62,7 @@ export interface SendStack {
62
62
  * roots (goodvibes-tui and goodvibes-agent) omitted it, still type-checked,
63
63
  * still delivered on every surface whose credential happens to sit in config or
64
64
  * the environment, and failed only on the surfaces that use a secret reference
65
- * at send time, as `Missing Telegram bot token`. This composition root does
65
+ *, at send time, as `Missing Telegram bot token`. This composition root does
66
66
  * not repeat that.
67
67
  */
68
68
  export function createSendStack(roots: SendStackRoots): SendStack {
@@ -1,5 +1,5 @@
1
1
  /**
2
- * failure-text.ts what the operator is told when a send does not happen.
2
+ * failure-text.ts, what the operator is told when a send does not happen.
3
3
  *
4
4
  * ## Why this is not `summarizeError`
5
5
  *
@@ -10,10 +10,10 @@
10
10
  *
11
11
  * Telegram delivery failed HTTP 401: {"ok":false,"description":"Unauthorized"}
12
12
  *
13
- * it returns `Telegram delivery failed HTTP 401` its `stripJson` pass removes
13
+ * it returns `Telegram delivery failed HTTP 401`, its `stripJson` pass removes
14
14
  * the `{...}`, which is exactly the part naming what went wrong. Given
15
15
  * `connect ECONNREFUSED 149.154.167.220:443` it returns "Cannot connect to the
16
- * provider. Check whether the service is reachable." advice in place of the
16
+ * provider. Check whether the service is reachable.", advice in place of the
17
17
  * address that was refused.
18
18
  *
19
19
  * A person debugging a message that did not arrive needs the provider's own
@@ -39,7 +39,7 @@ import { redactSensitiveData } from '@pellux/goodvibes-sdk/platform/utils';
39
39
 
40
40
  /**
41
41
  * A generous cap. The point of this text is diagnosis, so it is far larger than
42
- * the 240 characters `summarizeError` allows but an HTML error page or a
42
+ * the 240 characters `summarizeError` allows, but an HTML error page or a
43
43
  * multi-megabyte body still must not flood the terminal.
44
44
  */
45
45
  const MAX_FAILURE_TEXT = 2_000;
@@ -47,10 +47,10 @@ const MAX_FAILURE_TEXT = 2_000;
47
47
  const URL_CREDENTIAL_PATTERNS: ReadonlyArray<{ readonly pattern: RegExp; readonly replacement: string }> = [
48
48
  // Telegram: the bot token IS the path segment.
49
49
  { pattern: /\/bot\d{5,}:[A-Za-z0-9_-]{10,}/g, replacement: '/bot[REDACTED_BOT_TOKEN]' },
50
- // Any URL carrying `user:password@host` BlueBubbles, Mattermost and Matrix
50
+ // Any URL carrying `user:password@host`, BlueBubbles, Mattermost and Matrix
51
51
  // base URLs are all operator-supplied and can be written this way.
52
52
  { pattern: /(\b[a-z][a-z0-9+.-]*:\/\/)[^/\s:@]+:[^/\s@]+@/gi, replacement: '$1[REDACTED_CREDENTIALS]@' },
53
- // A credential passed as a query parameter BlueBubbles sends `?password=`.
53
+ // A credential passed as a query parameter, BlueBubbles sends `?password=`.
54
54
  {
55
55
  pattern: /([?&](?:password|token|secret|access_token|api_?key|auth)=)[^&\s"']+/gi,
56
56
  replacement: '$1[REDACTED]',
@@ -1,5 +1,5 @@
1
1
  /**
2
- * inert-text.ts make a message arrive as LITERAL TEXT on whichever channel
2
+ * inert-text.ts, make a message arrive as LITERAL TEXT on whichever channel
3
3
  * `goodvibes-daemon send` delivers it to.
4
4
  *
5
5
  * ## What "inert" means here, and why it is per (surface × transport), not
@@ -12,7 +12,7 @@
12
12
  *
13
13
  * - Telegram's `sendMessage` CAN parse MarkdownV2, but the strategy sends with
14
14
  * NO `parse_mode`, which is Telegram's plain-text mode. Running a MarkdownV2
15
- * escaper over the body on this path would not protect anything it would
15
+ * escaper over the body on this path would not protect anything, it would
16
16
  * put visible backslashes in front of every `.` `-` `!` `(` in the owner's
17
17
  * message. The correct transform is identity, and the property that makes it
18
18
  * correct is asserted by a test against the real wire payload rather than
@@ -29,7 +29,7 @@
29
29
  *
30
30
  * `escapeDiscordMarkdown` and `escapeSlackMrkdwn` are the escapers written for
31
31
  * the inbound-mail structured notices, in the SDK at
32
- * `packages/sdk/src/platform/email/inbound-notice.ts` same character classes,
32
+ * `packages/sdk/src/platform/email/inbound-notice.ts`, same character classes,
33
33
  * same zero-width-space mention break, same rationale. They are reproduced here
34
34
  * rather than imported for one reason: that module lives on the unmerged
35
35
  * `inbound-email-*` branches and is absent from the published
@@ -72,8 +72,8 @@ function breakMentionForms(text: string): string {
72
72
  *
73
73
  * `[` `]` `(` `)` are escaped too, and this is REQUIRED rather than
74
74
  * precautionary: masked links (`[text](url)`) DO render as clickable in
75
- * bot-sent and webhook messages which is exactly how this product delivers to
76
- * Discord even though they do not render for text a human typed into the
75
+ * bot-sent and webhook messages, which is exactly how this product delivers to
76
+ * Discord, even though they do not render for text a human typed into the
77
77
  * client.
78
78
  * https://github.com/discord/discord-api-docs/issues/6096
79
79
  * https://gist.github.com/matthewzring/9f7bbfd102003963f9be7dbcf7d40e51
@@ -97,11 +97,11 @@ function escapeDiscordMarkdown(text: string): string {
97
97
  * unescaped `<`.
98
98
  *
99
99
  * Slack has no backslash escape for `* _ ~ \``, so the zero-width break is
100
- * applied to those as the best available mitigation stated as a mitigation,
100
+ * applied to those as the best available mitigation, stated as a mitigation,
101
101
  * not a guarantee, because Slack's whitespace-adjacency rule for what breaks a
102
102
  * delimiter pair is not publicly specified to that precision. The residual risk
103
103
  * is cosmetic (accidental bold/italic); the injection class this exists to
104
- * close a clickable link or a real mention is closed by the entity escaping
104
+ * close, a clickable link or a real mention, is closed by the entity escaping
105
105
  * above.
106
106
  */
107
107
  function escapeSlackMrkdwn(text: string): string {
@@ -122,8 +122,8 @@ function escapeGoogleChatMarkup(text: string): string {
122
122
 
123
123
  /**
124
124
  * WhatsApp Cloud API text messages render `*bold*`, `_italic_`, `~strike~` and
125
- * ```` ```mono``` ````. There is no masked-link syntax a URL in the body is
126
- * auto-linked showing its real address so the injection class Discord has
125
+ * ```` ```mono``` ````. There is no masked-link syntax, a URL in the body is
126
+ * auto-linked showing its real address, so the injection class Discord has
127
127
  * does not exist here and only the delimiters need neutralizing. WhatsApp
128
128
  * documents no backslash escape, so the zero-width break is used.
129
129
  */
@@ -136,24 +136,24 @@ function escapeWhatsAppMarkup(text: string): string {
136
136
  * markup. Every other routable surface is absent DELIBERATELY, and each absence
137
137
  * is a checked claim about the strategy, not an oversight:
138
138
  *
139
- * - `telegram` `sendMessage` without `parse_mode`; Telegram's plain-text
139
+ * - `telegram` , `sendMessage` without `parse_mode`; Telegram's plain-text
140
140
  * mode. Escaping here would corrupt, not protect.
141
- * - `ntfy` `publish` sends the body as `text/plain` and never sets the
141
+ * - `ntfy` , `publish` sends the body as `text/plain` and never sets the
142
142
  * `Markdown` header, so ntfy renders it literally. The title
143
143
  * is derived by `titleFromBody` (first non-empty LINE) and
144
144
  * passed through `toHeaderSafeTitle`, so no line break or
145
145
  * non-ASCII byte from the body can reach an HTTP header.
146
- * - `webhook` the body is a JSON string field; the receiver decides what
146
+ * - `webhook` , the body is a JSON string field; the receiver decides what
147
147
  * to do with it and there is no markup layer to neutralize.
148
148
  *
149
- * - `signal` the strategy posts `text` to a signal bridge. Signal renders
149
+ * - `signal` , the strategy posts `text` to a signal bridge. Signal renders
150
150
  * no markup in a plain message body; styling travels as
151
151
  * explicit range metadata the strategy never sends.
152
152
  * - `imessage`,
153
- * `bluebubbles` both end at iMessage, which renders no markup at all.
154
- * - `msteams` the strategy sends `textFormat: 'plain'` alongside the text,
153
+ * `bluebubbles`, both end at iMessage, which renders no markup at all.
154
+ * - `msteams` , the strategy sends `textFormat: 'plain'` alongside the text,
155
155
  * which is Teams' own instruction not to parse it.
156
- * - `matrix` `msgtype: 'm.text'` with no `format`/`formatted_body`. A
156
+ * - `matrix` , `msgtype: 'm.text'` with no `format`/`formatted_body`. A
157
157
  * Matrix event without the HTML format field is rendered
158
158
  * literally by clients, per the spec.
159
159
  *
@@ -161,7 +161,7 @@ function escapeWhatsAppMarkup(text: string): string {
161
161
  * `ControlPlaneGateway` in the same process, which a short-lived CLI does not
162
162
  * have, so the command does not offer it as a channel at all. `telephony` is
163
163
  * absent because a CLI `send` to it would place a phone call or an SMS through
164
- * a paid carrier a different act from messaging a channel and because the
164
+ * a paid carrier, a different act from messaging a channel, and because the
165
165
  * strategy already XML-escapes the voice path itself via `escapeTwiml`.
166
166
  */
167
167
  const INERT_TRANSFORMS: Partial<Record<ChannelDeliverySurfaceKind, (text: string) => string>> = {
@@ -173,7 +173,7 @@ const INERT_TRANSFORMS: Partial<Record<ChannelDeliverySurfaceKind, (text: string
173
173
  'google-chat': escapeGoogleChatMarkup,
174
174
  whatsapp: escapeWhatsAppMarkup,
175
175
  // Delivered as plain text by the strategy: transforming would corrupt, not
176
- // protect. Each of these is a checked claim about the strategy, not a guess
176
+ // protect. Each of these is a checked claim about the strategy, not a guess,
177
177
  // see the list above.
178
178
  telegram: (text) => text,
179
179
  ntfy: (text) => text,
@@ -1,5 +1,5 @@
1
1
  /**
2
- * stdin.ts read the whole of stdin as the message body.
2
+ * stdin.ts, read the whole of stdin as the message body.
3
3
  *
4
4
  * Kept out of command.ts so the command stays a pure function of its arguments
5
5
  * and its injected dependencies: every test drives it with a stub rather than
@@ -10,8 +10,8 @@
10
10
  * Read stdin to end, as UTF-8.
11
11
  *
12
12
  * There is deliberately no timeout. `send` reads stdin only when the operator
13
- * gave no message argument AND stdin is not a terminal meaning something is
14
- * piping into it so waiting for that producer to finish is the correct
13
+ * gave no message argument AND stdin is not a terminal, meaning something is
14
+ * piping into it, so waiting for that producer to finish is the correct
15
15
  * behaviour, and cutting it off at an arbitrary deadline would silently
16
16
  * truncate a long message.
17
17
  */