@pellux/goodvibes-daemon 1.28.18 → 1.28.20

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 (108) hide show
  1. package/CHANGELOG.md +100 -63
  2. package/README.md +13 -13
  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 +4 -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/card-store.ts +486 -0
  40. package/src/daemon/handlers/payments/index.ts +32 -0
  41. package/src/daemon/handlers/payments/purchase-ledger.ts +108 -0
  42. package/src/daemon/handlers/payments/register.ts +391 -0
  43. package/src/daemon/handlers/register.ts +3 -3
  44. package/src/daemon/handlers/remote/backends/cloud-terminal.ts +9 -1
  45. package/src/daemon/handlers/remote/backends/process-runner.ts +1 -1
  46. package/src/daemon/handlers/remote/backends/ssh.ts +9 -1
  47. package/src/daemon/handlers/remote/backends/types.ts +2 -2
  48. package/src/daemon/handlers/remote/dispatcher.ts +3 -3
  49. package/src/daemon/handlers/remote/index.ts +1 -1
  50. package/src/daemon/handlers/remote/peer-registry.ts +62 -13
  51. package/src/daemon/handlers/routing/inbox-bridge.ts +5 -5
  52. package/src/daemon/handlers/routing/index.ts +1 -1
  53. package/src/daemon/handlers/routing/route-store.ts +1 -1
  54. package/src/daemon/handlers/routing/routing-resolver.ts +3 -3
  55. package/src/daemon/handlers/sqlite-store.ts +9 -9
  56. package/src/daemon/handlers/triage/index.ts +1 -1
  57. package/src/daemon/handlers/triage/integration.ts +3 -3
  58. package/src/daemon/handlers/triage/pipeline.ts +2 -2
  59. package/src/daemon/handlers/triage/scorer.ts +2 -2
  60. package/src/daemon/handlers/triage/tagger/discord.ts +3 -3
  61. package/src/daemon/handlers/triage/tagger/imap.ts +7 -7
  62. package/src/daemon/handlers/triage/tagger/index.ts +1 -1
  63. package/src/daemon/handlers/triage/tagger/shared.ts +3 -3
  64. package/src/daemon/handlers/triage/tagger/slack.ts +1 -1
  65. package/src/daemon/handlers/triage/types.ts +2 -2
  66. package/src/daemon/lifecycle.ts +5 -5
  67. package/src/daemon/local-daemon-state.ts +7 -7
  68. package/src/daemon/pair-command.ts +14 -14
  69. package/src/daemon/provision-wake-model.ts +5 -5
  70. package/src/daemon/send/channels.ts +7 -7
  71. package/src/daemon/send/command.ts +11 -11
  72. package/src/daemon/send/composition.ts +7 -7
  73. package/src/daemon/send/failure-text.ts +6 -6
  74. package/src/daemon/send/inert-text.ts +18 -18
  75. package/src/daemon/send/stdin.ts +3 -3
  76. package/src/daemon/service-commands.ts +32 -32
  77. package/src/daemon/sessions-command.ts +7 -7
  78. package/src/daemon/status-command.ts +22 -22
  79. package/src/daemon/webui-command.ts +14 -14
  80. package/src/runtime/boot-tasks.ts +1 -1
  81. package/src/runtime/cluster-composition.ts +9 -9
  82. package/src/runtime/cluster-group-composition.ts +7 -7
  83. package/src/runtime/conversation-rewind-port.ts +8 -8
  84. package/src/runtime/credential-composition.ts +2 -2
  85. package/src/runtime/daemon-handler-composition.ts +44 -4
  86. package/src/runtime/device-posture-composition.ts +10 -10
  87. package/src/runtime/disposal-wiring.ts +8 -8
  88. package/src/runtime/fleet-needs-input-push.ts +4 -4
  89. package/src/runtime/fleet-services.ts +1 -1
  90. package/src/runtime/hosted-session-composition.ts +13 -13
  91. package/src/runtime/index.ts +1 -1
  92. package/src/runtime/knowledge-services.ts +2 -2
  93. package/src/runtime/legacy-daemon-migration.ts +43 -43
  94. package/src/runtime/legacy-daemon-reconcile.ts +30 -30
  95. package/src/runtime/mail-composition.ts +6 -6
  96. package/src/runtime/notification-dispatch.ts +7 -7
  97. package/src/runtime/payments-composition.ts +143 -0
  98. package/src/runtime/plugin-composition.ts +7 -7
  99. package/src/runtime/runtime-services-types.ts +9 -9
  100. package/src/runtime/services.ts +31 -29
  101. package/src/runtime/trigger-services.ts +1 -1
  102. package/src/runtime/trust/checkpoint-eligibility.ts +5 -5
  103. package/src/runtime/trust/trust-gated-approvals.ts +9 -9
  104. package/src/runtime/update-check.ts +4 -4
  105. package/src/runtime/workspace-checkpointing.ts +6 -6
  106. package/src/testing/daemon-fixture.ts +11 -11
  107. package/src/testing/hosted-session-failures.ts +4 -4
  108. package/src/version.ts +2 -2
@@ -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
@@ -34,7 +34,7 @@
34
34
 
35
35
  import { ChannelDeliveryRouter } from '@pellux/goodvibes-sdk/platform/channels';
36
36
  import { ArtifactStore } from '@pellux/goodvibes-sdk/platform/artifacts';
37
- import { ConfigManager, ServiceRegistry, SubscriptionManager } from '@pellux/goodvibes-sdk/platform/config';
37
+ import { ConfigManager, ServiceRegistry, SubscriptionManager, sharedSubscriptionsPath } from '@pellux/goodvibes-sdk/platform/config';
38
38
  import { createShellPathService } from '@/runtime/index.ts';
39
39
  import { SecretsManager } from '../../config/secrets.ts';
40
40
  import type { SendDeliver } from './command.ts';
@@ -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 {
@@ -85,7 +85,7 @@ export function createSendStack(roots: SendStackRoots): SendStack {
85
85
  });
86
86
  const serviceRegistry = new ServiceRegistry(shellPaths.resolveProjectPath(GOODVIBES_DAEMON_SURFACE_ROOT, 'services.json'), {
87
87
  secretsManager,
88
- subscriptionManager: new SubscriptionManager(shellPaths.resolveUserPath(GOODVIBES_DAEMON_SURFACE_ROOT, 'subscriptions.json')),
88
+ subscriptionManager: new SubscriptionManager(sharedSubscriptionsPath(shellPaths), { legacyPath: shellPaths.resolveUserPath(GOODVIBES_DAEMON_SURFACE_ROOT, 'subscriptions.json') }),
89
89
  });
90
90
  const router = new ChannelDeliveryRouter({
91
91
  configManager,
@@ -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
  */
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Service-lifecycle subcommands `goodvibes-daemon install-service |
2
+ * Service-lifecycle subcommands, `goodvibes-daemon install-service |
3
3
  * uninstall-service | service-status`.
4
4
  *
5
5
  * The daemon is a SYSTEM SERVICE. These subcommands install it as a durable host
@@ -9,8 +9,8 @@
9
9
  *
10
10
  * Drift note: the SDK's older `systemd-user-service.ts` (a Linux-only,
11
11
  * bespoke systemd shim) was dead code and got deleted from the SDK. The SDK's
12
- * REAL wired service machinery reached in production by the daemon's own HTTP
13
- * `/api/service/*` routes via facade-composition.ts is
12
+ * REAL wired service machinery, reached in production by the daemon's own HTTP
13
+ * `/api/service/*` routes via facade-composition.ts, is
14
14
  * `PlatformServiceManager` (`@pellux/goodvibes-sdk/platform/daemon`): a single
15
15
  * systemd/launchd/windows-aware manager with install/uninstall/status/start/
16
16
  * stop/restart and a `suggestedCommands` hint list. This module now rewires the
@@ -38,16 +38,16 @@
38
38
  * honestly queries systemd via the injected actionRunner) BEFORE the
39
39
  * legacy unit is stopped, disabled, or removed. A failed or unhealthy new
40
40
  * unit rolls itself back (uninstalled) and never touches the legacy one
41
- * a botched takeover must never cost the user their working daemon.
41
+ * , a botched takeover must never cost the user their working daemon.
42
42
  * - ADOPT-OR-WARN, NEVER KILL. If the legacy unit file is simply absent but
43
43
  * something is already listening on the configured host:port (Mike's real
44
44
  * dev-host case: a manual `nohup`'d daemon with no unit at all), this is
45
- * an unidentified process, not a managed unit there is nothing to stop
45
+ * an unidentified process, not a managed unit, there is nothing to stop
46
46
  * or disable, and this module will not attempt to kill it. It warns and
47
47
  * leaves the decision to the operator.
48
48
  * - Every action (legacy stop/disable, unit-file removal, daemon-reload)
49
49
  * goes through the SAME injectable `actionRunner`/`legacyUnitFileRemove`
50
- * seams tests already use this module never has a code path that bypasses
50
+ * seams tests already use, this module never has a code path that bypasses
51
51
  * them, so the migration is exercised deterministically via fakes and never
52
52
  * touches a real running service in tests.
53
53
  */
@@ -67,7 +67,7 @@ import {
67
67
  type ManagedServiceActionRunner,
68
68
  } from '../runtime/legacy-daemon-migration.ts';
69
69
 
70
- // `resolveInstalledDaemonBinary` lives in the runtime module re-exported
70
+ // `resolveInstalledDaemonBinary` lives in the runtime module, re-exported
71
71
  // here so this module stays the CLI's stable public surface (and so existing
72
72
  // test imports keep working).
73
73
  export {
@@ -83,7 +83,7 @@ export type { ManagedServiceActionRunner } from '../runtime/legacy-daemon-migrat
83
83
  // literal name `goodvibes-daemon.service`. This module (rewired onto
84
84
  // PlatformServiceManager, see the file banner above) manages a DIFFERENT
85
85
  // unit name (`goodvibes`, SERVICE_NAME). A host that still has the legacy
86
- // unit installed this dev machine included would otherwise see
86
+ // unit installed, this dev machine included, would otherwise see
87
87
  // service-status honestly report installed:false/running:false for the
88
88
  // tracked name while the legacy unit keeps running untouched underneath it,
89
89
  // uninstall-service would silently leave the legacy unit orphaned with no
@@ -94,13 +94,13 @@ export type { ManagedServiceActionRunner } from '../runtime/legacy-daemon-migrat
94
94
  // migration engine (`runLegacyDaemonMigration`) both live in
95
95
  // `../runtime/legacy-daemon-migration.ts` rather than here, because this
96
96
  // daemon's own boot-time reconcile (`../runtime/legacy-daemon-reconcile.ts`)
97
- // needs them too the entrypoint-agnostic `runtime` layer is what both of
97
+ // needs them too, the entrypoint-agnostic `runtime` layer is what both of
98
98
  // this repository's consumers can import, so this CLI module is just one of
99
99
  // them. The terminal app's own onboarding guided UX solves the same problem
100
100
  // independently, under its own architecture constraints.
101
101
  //
102
102
  // This detection is entirely independent of PlatformServiceManager's own
103
- // status() it does not rely on (or get invalidated by) the parallel SDK
103
+ // status(), it does not rely on (or get invalidated by) the parallel SDK
104
104
  // fix that makes status().running itself query systemd honestly for the
105
105
  // TRACKED unit name.
106
106
  // ---------------------------------------------------------------------------
@@ -125,7 +125,7 @@ export function isDaemonServiceSubcommand(value: string | undefined): value is D
125
125
  * Exit codes `service-status` reports, so a script never parses prose.
126
126
  *
127
127
  * 0/3/4 are the three answers the question actually has; 1 stays what it has
128
- * always been the platform refused the query and the error is printed.
128
+ * always been, the platform refused the query and the error is printed.
129
129
  */
130
130
  export const SERVICE_STATUS_EXIT_RUNNING = 0;
131
131
  export const SERVICE_STATUS_EXIT_INSTALLED_NOT_RUNNING = 3;
@@ -134,10 +134,10 @@ export const SERVICE_STATUS_EXIT_NOT_INSTALLED = 4;
134
134
  export interface DaemonServiceCliInput {
135
135
  readonly subcommand: DaemonServiceSubcommand;
136
136
  readonly binaryPath: string;
137
- /** The GoodVibes tree home (GOODVIBES_HOME-overridable) config/state root only, never unit-path resolution. See `unitHomeDir`. */
137
+ /** The GoodVibes tree home (GOODVIBES_HOME-overridable), config/state root only, never unit-path resolution. See `unitHomeDir`. */
138
138
  readonly homeDir: string;
139
139
  /**
140
- * The LOGIN user's home where the systemd/launchd/Windows unit actually
140
+ * The LOGIN user's home, where the systemd/launchd/Windows unit actually
141
141
  * lives, regardless of any GOODVIBES_HOME/GOODVIBES_DAEMON_HOME override.
142
142
  * Required (no default to `homeDir`) so a unit-management subcommand can
143
143
  * never silently search for its unit under a relocated tree home instead of
@@ -148,14 +148,14 @@ export interface DaemonServiceCliInput {
148
148
  readonly port: number;
149
149
  /**
150
150
  * Whether the invoking CLI line carried an explicit `--hostname`/`--port`
151
- * flag (not merely whether `host`/`port` above are set those are always
151
+ * flag (not merely whether `host`/`port` above are set, those are always
152
152
  * set, resolved from config either way). `install-service`/`migrate-service`
153
- * refuse when either is true see `validateServiceEndpointFlags`. Defaults
153
+ * refuse when either is true, see `validateServiceEndpointFlags`. Defaults
154
154
  * to false (no flag), matching every subcommand these two don't apply to.
155
155
  */
156
156
  readonly hostnameFlagProvided?: boolean | undefined;
157
157
  readonly portFlagProvided?: boolean | undefined;
158
- /** Defaults to `homeDir` overridable so tests can scope both to one tempdir. */
158
+ /** Defaults to `homeDir`, overridable so tests can scope both to one tempdir. */
159
159
  readonly workingDirectory?: string | undefined;
160
160
  /** Injected in tests; a real `ConfigManager` rooted at `homeDir` otherwise. */
161
161
  readonly configManager?: ConfigManager | undefined;
@@ -166,7 +166,7 @@ export interface DaemonServiceCliInput {
166
166
  /**
167
167
  * `migrate-service` only: explicit consent to actually execute the
168
168
  * migration (wired from the CLI's `-y`/`--yes` flag). Without it, the
169
- * subcommand prints the exact plan and changes nothing never auto-migrate.
169
+ * subcommand prints the exact plan and changes nothing, never auto-migrate.
170
170
  */
171
171
  readonly confirmMigration?: boolean | undefined;
172
172
  /**
@@ -183,7 +183,7 @@ export interface DaemonServiceCliInput {
183
183
  readonly legacyUnitFileRemove?: ((path: string) => void) | undefined;
184
184
  /**
185
185
  * `service-status` only: report one JSON document instead of prose. The exit
186
- * code is the same either way the codes are the machine-readable answer,
186
+ * code is the same either way, the codes are the machine-readable answer,
187
187
  * and JSON is for when a script wants the fields as well.
188
188
  */
189
189
  readonly json?: boolean | undefined;
@@ -200,7 +200,7 @@ export interface DaemonServiceCliResult {
200
200
  /**
201
201
  * The manager's definition (`ExecStart` command/args, name, description) is
202
202
  * built once, in `../runtime/legacy-daemon-migration.ts`, and shared with the
203
- * onboarding guided UX see that module's doc comment for why.
203
+ * onboarding guided UX, see that module's doc comment for why.
204
204
  */
205
205
  function buildManager(input: DaemonServiceCliInput): PlatformServiceManager {
206
206
  return buildManagedDaemonServiceManager({
@@ -225,12 +225,12 @@ function statusLines(status: ManagedServiceStatus): string[] {
225
225
  if (status.pid !== undefined) lines.push(`pid: ${status.pid}`);
226
226
  if (status.platform !== 'manual' && status.installed && !status.running) {
227
227
  // W3 Finding 4: this used to assert "'running' here only reflects
228
- // processes this tool started directly" true for the pid-file-only
228
+ // processes this tool started directly", true for the pid-file-only
229
229
  // check the (currently linked) SDK still uses, but the parallel SDK
230
230
  // batch is making status().running query systemd honestly via
231
231
  // `is-active`, which would make that specific claim stale. Drop the
232
232
  // claim about HOW running was computed and just offer the escape
233
- // hatch true and useful under either SDK version.
233
+ // hatch, true and useful under either SDK version.
234
234
  lines.push(
235
235
  `note: if this looks wrong, verify directly: ${status.suggestedCommands[status.suggestedCommands.length - 1] ?? 'the platform service-status command'}`,
236
236
  );
@@ -264,7 +264,7 @@ export function buildInstallResultLines(status: ManagedServiceStatus): string[]
264
264
  * Every line names the RESOLVED unit (`status.serviceName`, `status.path`) and
265
265
  * says what the platform reported afterwards, rather than asserting the verb
266
266
  * worked because it was dispatched without throwing. A verb aimed at a service
267
- * that is not installed says exactly that installing it silently would be a
267
+ * that is not installed says exactly that, installing it silently would be a
268
268
  * different command than the one that was run.
269
269
  */
270
270
  export function buildLifecycleResultLines(
@@ -364,7 +364,7 @@ function ok(action: 'install' | 'uninstall' | 'status', status: ManagedServiceSt
364
364
  lines.push(`removed the ${status.platform} service at ${status.path}`);
365
365
  if (status.platform === 'systemd') {
366
366
  lines.push(
367
- "note: this removes the unit file but does not run `disable` run " +
367
+ "note: this removes the unit file but does not run `disable`; run " +
368
368
  "`systemctl --user daemon-reload` to clear any stale enablement symlink.",
369
369
  );
370
370
  }
@@ -388,12 +388,12 @@ function failed(action: 'install' | 'uninstall' | 'status', status: ManagedServi
388
388
  * `install-service` and `migrate-service` refuse an explicit
389
389
  * `--hostname`/`--port` flag rather than silently accepting it. Both flags are
390
390
  * RUNTIME-ONLY overrides applied to this one invocation's in-memory config
391
- * (`applyRuntimeEndpointFlagOverrides` in `src/daemon/cli.ts`) but the unit
391
+ * (`applyRuntimeEndpointFlagOverrides` in `src/daemon/cli.ts`), but the unit
392
392
  * this subcommand writes carries no endpoint flags at all (see
393
393
  * `buildManagedDaemonServiceManager`'s ExecStart doc): the daemon always
394
394
  * re-resolves `controlPlane.host`/`port`/`hostMode` from PERSISTED settings at
395
395
  * boot. A flag accepted here would print/probe a binding the installed unit
396
- * will never actually have an honest gap, not a cosmetic one, since the
396
+ * will never actually have, an honest gap, not a cosmetic one, since the
397
397
  * printed value is exactly what an operator would expect the running service
398
398
  * to bind to. Refusing with a pointer to the persistent config path is
399
399
  * strictly better than silently doing nothing with the flag.
@@ -408,7 +408,7 @@ export function validateServiceEndpointFlags(
408
408
  .filter((name): name is string => name !== null)
409
409
  .join('/');
410
410
  return [
411
- `${subcommand} refused: ${flagNames} only override this process's runtime config the installed unit carries ` +
411
+ `${subcommand} refused: ${flagNames} only override this process's runtime config; the installed unit carries ` +
412
412
  'no endpoint flags and re-resolves controlPlane.host/port/hostMode from persisted settings at boot, so the ' +
413
413
  'override would never take effect there.',
414
414
  `Set the persistent binding instead: goodvibes-daemon config set controlPlane.host / controlPlane.port (or ` +
@@ -419,7 +419,7 @@ export function validateServiceEndpointFlags(
419
419
  /**
420
420
  * `migrate-service`: the guided, consented takeover of the legacy
421
421
  * `goodvibes-daemon.service` unit. Thin wrapper over
422
- * `runLegacyDaemonMigration` (`../runtime/legacy-daemon-migration.ts`) see
422
+ * `runLegacyDaemonMigration` (`../runtime/legacy-daemon-migration.ts`), see
423
423
  * that module for the design constraints (never auto-migrate,
424
424
  * new-up-then-old-down, adopt-or-warn/never kill an unrecognized process,
425
425
  * every action through an injectable seam) and the onboarding UX consumer.
@@ -461,7 +461,7 @@ export async function runDaemonServiceCli(input: DaemonServiceCliInput): Promise
461
461
  case 'install-service': {
462
462
  // W3 Finding 4: refuse rather than risk starting a second daemon
463
463
  // alongside an already-installed legacy unit. Refuses whenever the
464
- // legacy unit is present at all (not just when currently active)
464
+ // legacy unit is present at all (not just when currently active),
465
465
  // an installed-but-inactive legacy unit can still be enabled and
466
466
  // start competing for the same port later, and "never silently
467
467
  // start a second daemon" is the bar here, not "never right now."
@@ -483,7 +483,7 @@ export async function runDaemonServiceCli(input: DaemonServiceCliInput): Promise
483
483
  if (installed.actionError) return failed('install', installed);
484
484
  const started = manager.start();
485
485
  // start()'s actionError (e.g. a platform this manager can't dispatch
486
- // actions for) doesn't undo the write report install as ok, but surface
486
+ // actions for) doesn't undo the write, report install as ok, but surface
487
487
  // the follow-up problem honestly instead of claiming it is running.
488
488
  return started.actionError
489
489
  ? ok('install', { ...started, running: false }, [`could not start it automatically: ${started.actionError}`])
@@ -496,7 +496,7 @@ export async function runDaemonServiceCli(input: DaemonServiceCliInput): Promise
496
496
  const extra: string[] = [];
497
497
  if (stopped.actionError) extra.push(`(it may not have been running: ${stopped.actionError})`);
498
498
  // This command only ever touches the TRACKED unit (whatever name/path
499
- // actually resolved not necessarily the SERVICE_NAME constant) above
499
+ // actually resolved, not necessarily the SERVICE_NAME constant) above,
500
500
  // say so explicitly when an install-script unit also exists, so its
501
501
  // continued presence is never a silent surprise.
502
502
  if (legacy.present) extra.push(legacyUnitNote(legacy, resolveManagedUnitName(uninstalled)));
@@ -520,8 +520,8 @@ export async function runDaemonServiceCli(input: DaemonServiceCliInput): Promise
520
520
  };
521
521
  }
522
522
  const result = ok('status', status, legacy.present ? [legacyUnitNote(legacy, resolveManagedUnitName(status))] : []);
523
- // The status verb reports; it does not fail. `ok` is still true the
524
- // question was answered while the exit code carries the answer itself
523
+ // The status verb reports; it does not fail. `ok` is still true, the
524
+ // question was answered, while the exit code carries the answer itself
525
525
  // (0 running / 3 installed-not-running / 4 not installed), so a script
526
526
  // never has to read the prose above.
527
527
  return { ...result, exitCode: serviceStatusExitCode(status) };
@@ -1,14 +1,14 @@
1
1
  /**
2
- * sessions-command.ts `goodvibes-daemon sessions list|kill <id>`.
2
+ * sessions-command.ts, `goodvibes-daemon sessions list|kill <id>`.
3
3
  *
4
4
  * The sessions this daemon HOSTS: conversation loops running inside it, which
5
5
  * outlive the client that started them. That is the whole reason they need a
6
- * command a terminal's own session dies with the terminal and never needs
6
+ * command, a terminal's own session dies with the terminal and never needs
7
7
  * listing from outside, while a hosted one can be running on a headless box
8
8
  * with nothing attached to it at all.
9
9
  *
10
10
  * The verbs are `sessions.hosted.list` and `sessions.hosted.kill`, and both are
11
- * declared ws-only in the method catalog they have no REST binding, so they
11
+ * declared ws-only in the method catalog, they have no REST binding, so they
12
12
  * go through `callDaemonWsVerb` rather than `callDaemonVerb`. Same target, same
13
13
  * operator token, same --host/--port/--token convention; only the transport is
14
14
  * different. A daemon built without hosted sessions answers "does not know the
@@ -62,7 +62,7 @@ export interface SessionsCommandFlags extends RemoteCommandFlags {
62
62
 
63
63
  export interface RunSessionsCommandInput extends RemoteCommandDeps {
64
64
  readonly flags: SessionsCommandFlags;
65
- /** Positional words after `sessions` the subcommand and its argument. */
65
+ /** Positional words after `sessions`, the subcommand and its argument. */
66
66
  readonly args: readonly string[];
67
67
  }
68
68
 
@@ -119,7 +119,7 @@ function renderSession(session: HostedSessionRecord, now: number): string[] {
119
119
  *
120
120
  * Exit 0 when the daemon answered, 1 when it refused or could not be reached,
121
121
  * 2 when the command line was wrong. A `kill` with no id is a usage refusal
122
- * rather than a "kill everything" there is no shape of this command that ends
122
+ * rather than a "kill everything", there is no shape of this command that ends
123
123
  * more than the one session named.
124
124
  */
125
125
  export async function runSessionsCommand(input: RunSessionsCommandInput): Promise<DaemonCommandResult> {
@@ -129,11 +129,11 @@ export async function runSessionsCommand(input: RunSessionsCommandInput): Promis
129
129
  return refusal('name what to do with the sessions.', flags.json);
130
130
  }
131
131
  if (!isSessionsSubcommand(subcommand)) {
132
- return refusal(`'${subcommand}' is not a sessions command try list or kill.`, flags.json);
132
+ return refusal(`'${subcommand}' is not a sessions command; try list or kill.`, flags.json);
133
133
  }
134
134
  const sessionId = args[1];
135
135
  if (subcommand === 'kill' && sessionId === undefined) {
136
- return refusal('kill needs the session to end run `sessions list` to see them.', flags.json);
136
+ return refusal('kill needs the session to end; run `sessions list` to see them.', flags.json);
137
137
  }
138
138
  if (args.length > (subcommand === 'kill' ? 2 : 1)) {
139
139
  return refusal(`'${args[subcommand === 'kill' ? 2 : 1]}' is one argument too many.`, flags.json);