@pellux/goodvibes-daemon 1.28.19 → 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.
- package/CHANGELOG.md +85 -63
- package/README.md +13 -13
- package/bin/launcher-support.js +5 -5
- package/package.json +6 -4
- package/scripts/postinstall.js +8 -8
- package/src/cli/command-catalog.ts +22 -22
- package/src/cli/completion.ts +4 -4
- package/src/cli/help.ts +5 -5
- package/src/cli/index.ts +3 -3
- package/src/cli/parser.ts +2 -2
- package/src/cli/surface-catalog.ts +1 -1
- package/src/cli/types.ts +2 -2
- package/src/cluster/daemon-ws-call.ts +5 -5
- package/src/cluster/raw-reply-route.ts +5 -5
- package/src/config/checkpoint-settings.ts +7 -7
- package/src/config/config-key-guard.ts +22 -0
- package/src/config/run-daemon-config-migration.ts +3 -3
- package/src/config/secret-config.ts +7 -7
- package/src/config/surface.ts +3 -3
- package/src/core/pairing-banner.ts +5 -5
- package/src/daemon/cli.ts +45 -43
- package/src/daemon/config-command.ts +15 -15
- package/src/daemon/handlers/context.ts +1 -1
- package/src/daemon/handlers/contracts.ts +4 -4
- package/src/daemon/handlers/credentials.ts +1 -1
- package/src/daemon/handlers/drafts/draft-store.ts +3 -3
- package/src/daemon/handlers/drafts/register.ts +4 -4
- package/src/daemon/handlers/inbox/aggregator.ts +8 -8
- package/src/daemon/handlers/inbox/cursor-store.ts +10 -10
- package/src/daemon/handlers/inbox/index.ts +7 -7
- package/src/daemon/handlers/inbox/mapping.ts +2 -2
- package/src/daemon/handlers/inbox/poller.ts +5 -5
- package/src/daemon/handlers/inbox/provider-adapter.ts +8 -8
- package/src/daemon/handlers/inbox/providers/discord.ts +6 -6
- package/src/daemon/handlers/inbox/providers/email.ts +3 -3
- package/src/daemon/handlers/inbox/providers/imap-client.ts +1 -1
- package/src/daemon/handlers/inbox/providers/slack.ts +4 -4
- package/src/daemon/handlers/index.ts +18 -8
- package/src/daemon/handlers/payments/card-store.ts +486 -0
- package/src/daemon/handlers/payments/index.ts +32 -0
- package/src/daemon/handlers/payments/purchase-ledger.ts +108 -0
- package/src/daemon/handlers/payments/register.ts +391 -0
- package/src/daemon/handlers/register.ts +3 -3
- package/src/daemon/handlers/remote/backends/cloud-terminal.ts +9 -1
- package/src/daemon/handlers/remote/backends/process-runner.ts +1 -1
- package/src/daemon/handlers/remote/backends/ssh.ts +9 -1
- package/src/daemon/handlers/remote/backends/types.ts +2 -2
- package/src/daemon/handlers/remote/dispatcher.ts +3 -3
- package/src/daemon/handlers/remote/index.ts +1 -1
- package/src/daemon/handlers/remote/peer-registry.ts +62 -13
- package/src/daemon/handlers/routing/inbox-bridge.ts +5 -5
- package/src/daemon/handlers/routing/index.ts +1 -1
- package/src/daemon/handlers/routing/route-store.ts +1 -1
- package/src/daemon/handlers/routing/routing-resolver.ts +3 -3
- package/src/daemon/handlers/sqlite-store.ts +9 -9
- package/src/daemon/handlers/triage/index.ts +1 -1
- package/src/daemon/handlers/triage/integration.ts +3 -3
- package/src/daemon/handlers/triage/pipeline.ts +2 -2
- package/src/daemon/handlers/triage/scorer.ts +2 -2
- package/src/daemon/handlers/triage/tagger/discord.ts +3 -3
- package/src/daemon/handlers/triage/tagger/imap.ts +7 -7
- package/src/daemon/handlers/triage/tagger/index.ts +1 -1
- package/src/daemon/handlers/triage/tagger/shared.ts +3 -3
- package/src/daemon/handlers/triage/tagger/slack.ts +1 -1
- package/src/daemon/handlers/triage/types.ts +2 -2
- package/src/daemon/lifecycle.ts +5 -5
- package/src/daemon/local-daemon-state.ts +7 -7
- package/src/daemon/pair-command.ts +14 -14
- package/src/daemon/provision-wake-model.ts +5 -5
- package/src/daemon/send/channels.ts +7 -7
- package/src/daemon/send/command.ts +11 -11
- package/src/daemon/send/composition.ts +5 -5
- package/src/daemon/send/failure-text.ts +6 -6
- package/src/daemon/send/inert-text.ts +18 -18
- package/src/daemon/send/stdin.ts +3 -3
- package/src/daemon/service-commands.ts +32 -32
- package/src/daemon/sessions-command.ts +7 -7
- package/src/daemon/status-command.ts +22 -22
- package/src/daemon/webui-command.ts +14 -14
- package/src/runtime/boot-tasks.ts +1 -1
- package/src/runtime/cluster-composition.ts +9 -9
- package/src/runtime/cluster-group-composition.ts +7 -7
- package/src/runtime/conversation-rewind-port.ts +8 -8
- package/src/runtime/credential-composition.ts +2 -2
- package/src/runtime/daemon-handler-composition.ts +44 -4
- package/src/runtime/device-posture-composition.ts +10 -10
- package/src/runtime/disposal-wiring.ts +8 -8
- package/src/runtime/fleet-needs-input-push.ts +4 -4
- package/src/runtime/fleet-services.ts +1 -1
- package/src/runtime/hosted-session-composition.ts +13 -13
- package/src/runtime/index.ts +1 -1
- package/src/runtime/knowledge-services.ts +2 -2
- package/src/runtime/legacy-daemon-migration.ts +43 -43
- package/src/runtime/legacy-daemon-reconcile.ts +30 -30
- package/src/runtime/mail-composition.ts +6 -6
- package/src/runtime/notification-dispatch.ts +7 -7
- package/src/runtime/payments-composition.ts +143 -0
- package/src/runtime/plugin-composition.ts +7 -7
- package/src/runtime/runtime-services-types.ts +9 -9
- package/src/runtime/services.ts +29 -27
- package/src/runtime/trigger-services.ts +1 -1
- package/src/runtime/trust/checkpoint-eligibility.ts +5 -5
- package/src/runtime/trust/trust-gated-approvals.ts +9 -9
- package/src/runtime/update-check.ts +4 -4
- package/src/runtime/workspace-checkpointing.ts +6 -6
- package/src/testing/daemon-fixture.ts +11 -11
- package/src/testing/hosted-session-failures.ts +4 -4
- package/src/version.ts +2 -2
|
@@ -9,22 +9,22 @@
|
|
|
9
9
|
// `getProfileForChannel(surfaceKind, routeId?)` applies the canonical
|
|
10
10
|
// resolution order:
|
|
11
11
|
//
|
|
12
|
-
// 1. exact
|
|
13
|
-
// 2. surface
|
|
14
|
-
// 3. wildcard
|
|
12
|
+
// 1. exact , surfaceKind AND routeId both match
|
|
13
|
+
// 2. surface , surfaceKind matches, route has no routeId
|
|
14
|
+
// 3. wildcard , a route with surfaceKind === 'any' (no routeId)
|
|
15
15
|
//
|
|
16
16
|
// This module bridges the two shapes. The bridge maps an inbound item's
|
|
17
17
|
// `provider` to the routing `surfaceKind` and forwards an optional `routeId`
|
|
18
18
|
// refinement; `fromDigest`/`kind` are NOT used as routing keys (the routing
|
|
19
19
|
// store is keyed by surfaceKind + optional routeId only, never by sender digest
|
|
20
|
-
// or message kind
|
|
20
|
+
// or message kind, see RoutingChannelRoute). When no binding matches the
|
|
21
21
|
// resolver returns null and the bridge yields `undefined`, so the adapter falls
|
|
22
22
|
// back to leaving the item unrouted (offline/default behaviour intact). The
|
|
23
23
|
// bridge NEVER throws: the underlying resolver is a pure in-memory lookup.
|
|
24
24
|
//
|
|
25
25
|
// The inbox surface is a separate follow-up module, so this bridge declares the
|
|
26
26
|
// minimal `RouteResolver` seam locally rather than importing a sibling that the
|
|
27
|
-
// routing slice does not own
|
|
27
|
+
// routing slice does not own, keeping the routing handler layer free of any
|
|
28
28
|
// cross-surface import edge.
|
|
29
29
|
// ---------------------------------------------------------------------------
|
|
30
30
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// ---------------------------------------------------------------------------
|
|
2
|
-
// Channel routing surface
|
|
2
|
+
// Channel routing surface, HANDLERS for the SDK-defined methods
|
|
3
3
|
// - channels.routing.list (read-only)
|
|
4
4
|
// - channels.routing.assign (admin, confirmation-gated)
|
|
5
5
|
// - channels.routing.delete (admin, dangerous, confirmation-gated)
|
|
@@ -96,7 +96,7 @@ const SCHEMA: string[] = [
|
|
|
96
96
|
updatedAt TEXT NOT NULL
|
|
97
97
|
)`,
|
|
98
98
|
`CREATE INDEX IF NOT EXISTS idx_routes_surface_route ON routes (surfaceKind, routeId)`,
|
|
99
|
-
// Enforce one assignment per channel
|
|
99
|
+
// Enforce one assignment per channel, the upsert path relies on this.
|
|
100
100
|
`CREATE UNIQUE INDEX IF NOT EXISTS idx_routes_channel ON routes (channelId)`,
|
|
101
101
|
];
|
|
102
102
|
|
|
@@ -9,9 +9,9 @@ export const WILDCARD_SURFACE = 'any';
|
|
|
9
9
|
* order as the agent-side `getProfileForChannel()` so offline and online
|
|
10
10
|
* routing produce identical results:
|
|
11
11
|
*
|
|
12
|
-
* 1. exact match
|
|
13
|
-
* 2. surface-only
|
|
14
|
-
* 3. wildcard
|
|
12
|
+
* 1. exact match , surfaceKind AND routeId both match
|
|
13
|
+
* 2. surface-only , surfaceKind matches, route has no routeId
|
|
14
|
+
* 3. wildcard , a route with surfaceKind === 'any' (no routeId)
|
|
15
15
|
*
|
|
16
16
|
* Returns the matching profileId, or `null` when nothing matches.
|
|
17
17
|
*/
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/// <reference types="@pellux/goodvibes-sdk/sql-js" />
|
|
2
2
|
// `sql.js` ships no types. The declaration is SDK-owned and reaches here
|
|
3
|
-
// through that reference
|
|
3
|
+
// through that reference, there is no local copy to keep in step.
|
|
4
4
|
|
|
5
5
|
import { mkdir, rename, writeFile } from 'node:fs/promises';
|
|
6
6
|
import { readFileSync, readdirSync, renameSync, rmSync, statSync } from 'node:fs';
|
|
@@ -23,7 +23,7 @@ export interface SqliteStoreOptions {
|
|
|
23
23
|
|
|
24
24
|
/**
|
|
25
25
|
* The 16 bytes every SQLite file begins with ("SQLite format 3" + NUL). A file
|
|
26
|
-
* that does not start with these is not a database, whatever its extension
|
|
26
|
+
* that does not start with these is not a database, whatever its extension,
|
|
27
27
|
* which is exactly what a crash mid-create, a zero-fill, or a partially
|
|
28
28
|
* restored backup leaves behind.
|
|
29
29
|
*/
|
|
@@ -93,7 +93,7 @@ export class HandlerSqliteStore {
|
|
|
93
93
|
/**
|
|
94
94
|
* Move a database file that cannot be opened aside instead of deleting it, so
|
|
95
95
|
* an operator still has something to salvage, and return the quarantine path.
|
|
96
|
-
* Returns null when the move itself failed
|
|
96
|
+
* Returns null when the move itself failed, in which case the caller starts
|
|
97
97
|
* fresh in memory and the bad file is left exactly where it was rather than
|
|
98
98
|
* being overwritten on the next save.
|
|
99
99
|
*/
|
|
@@ -110,8 +110,8 @@ export class HandlerSqliteStore {
|
|
|
110
110
|
/**
|
|
111
111
|
* Bound the quarantine directory: drop `.corrupt-*` copies of THIS store that
|
|
112
112
|
* are past the TTL or beyond the keep-newest count. Best-effort and
|
|
113
|
-
* idempotent
|
|
114
|
-
* error
|
|
113
|
+
* idempotent, a file another process already removed is success, not an
|
|
114
|
+
* error, so two daemons opening the same store at once cannot fight.
|
|
115
115
|
* Returns how many were reclaimed.
|
|
116
116
|
*/
|
|
117
117
|
private reapCorruptQuarantines(nowMs: number): number {
|
|
@@ -158,7 +158,7 @@ export class HandlerSqliteStore {
|
|
|
158
158
|
|
|
159
159
|
// Validate the file by its CONTENT, not by existsSync. `save()` writes
|
|
160
160
|
// through a pid-and-timestamp temp file and an atomic rename, so it cannot
|
|
161
|
-
// itself leave a half-written database
|
|
161
|
+
// itself leave a half-written database, but a zero-filled file recovered
|
|
162
162
|
// by a filesystem, a truncated restore, or a copy interrupted by something
|
|
163
163
|
// outside this process all produce a path that exists and holds no usable
|
|
164
164
|
// database. Handing those bytes to `new SQL.Database(...)` throws out of
|
|
@@ -200,7 +200,7 @@ export class HandlerSqliteStore {
|
|
|
200
200
|
}
|
|
201
201
|
} catch (error) {
|
|
202
202
|
// Header present but the body does not hold up: a truncated or damaged
|
|
203
|
-
// database. Same treatment
|
|
203
|
+
// database. Same treatment, set aside, disclose, start clean.
|
|
204
204
|
quarantineReason = `database would not open: ${error instanceof Error ? error.message : String(error)}`;
|
|
205
205
|
this.db = null;
|
|
206
206
|
quarantined = this.quarantineUnreadable();
|
|
@@ -215,7 +215,7 @@ export class HandlerSqliteStore {
|
|
|
215
215
|
// Disclosure: starting a store from scratch is data loss from the user's
|
|
216
216
|
// point of view, so it is never allowed to happen quietly.
|
|
217
217
|
if (quarantineReason) {
|
|
218
|
-
logger.warn('daemon store could not be opened
|
|
218
|
+
logger.warn('daemon store could not be opened; starting a fresh one', {
|
|
219
219
|
store: this.options.fileName,
|
|
220
220
|
path: this.resolvedPath,
|
|
221
221
|
reason: quarantineReason,
|
|
@@ -266,7 +266,7 @@ export class HandlerSqliteStore {
|
|
|
266
266
|
* saves of the same store in the SAME millisecond produced the same temp path
|
|
267
267
|
* before it did: both wrote it, the first rename moved it away, and the
|
|
268
268
|
* second failed with ENOENT on a file it had just written. That is not a
|
|
269
|
-
* hypothetical race
|
|
269
|
+
* hypothetical race, the inbox poller flushes once per provider and polls
|
|
270
270
|
* every provider concurrently, so it hit on an ordinary two-provider startup,
|
|
271
271
|
* and the failure surfaced as a provider reporting a filesystem error for its
|
|
272
272
|
* feed. Every store on this base shares the hazard, so the counter lives here.
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
// runtime composition root calls. It DECORATES the inbox surface's
|
|
6
6
|
// `channels.inbox.list` handler (overlaying persisted triage metadata) and
|
|
7
7
|
// returns the poller-facing pipeline + tagger. inbox.triage.* are NOT published
|
|
8
|
-
// catalog methods
|
|
8
|
+
// catalog methods, this surface registers no triage method id.
|
|
9
9
|
// ---------------------------------------------------------------------------
|
|
10
10
|
|
|
11
11
|
export {
|
|
@@ -12,12 +12,12 @@
|
|
|
12
12
|
// overlaid with the persisted triageScore/triageTags via
|
|
13
13
|
// enrichItemsWithTriage(). Every other registration passes straight
|
|
14
14
|
// through to the real catalog. The inbox descriptor/schema/id is never
|
|
15
|
-
// re-authored
|
|
15
|
+
// re-authored, only its handler is wrapped.
|
|
16
16
|
// 2. It exposes the triage pipeline + tagger (`runInboxTriage`, `tagger`) for
|
|
17
17
|
// the daemon-internal poller, which scores items and persists them to the
|
|
18
18
|
// co-located inbox-triage.sqlite store the decorator reads from.
|
|
19
19
|
// 3. inbox.triage.* are intentionally NOT registered on the catalog (they are
|
|
20
|
-
// a daemon-internal pipeline, not published methods)
|
|
20
|
+
// a daemon-internal pipeline, not published methods), so this module
|
|
21
21
|
// makes ZERO catalog.register call for any triage id.
|
|
22
22
|
//
|
|
23
23
|
// Reads in the decorator are best-effort and degrade to the raw item when no
|
|
@@ -51,7 +51,7 @@ import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
|
|
|
51
51
|
|
|
52
52
|
/**
|
|
53
53
|
* Canonical id of the inbox list method whose handler we decorate. This is the
|
|
54
|
-
* SDK's published id
|
|
54
|
+
* SDK's published id, referenced as a plain string for matching during
|
|
55
55
|
* registration; no descriptor or schema is authored here.
|
|
56
56
|
*/
|
|
57
57
|
export const INBOX_LIST_METHOD_ID = 'channels.inbox.list';
|
|
@@ -57,7 +57,7 @@ export interface RunInboxTriageOptions {
|
|
|
57
57
|
scorer?: TriageScorerOptions;
|
|
58
58
|
/** Inject a store (tests). When omitted, a triage store is opened/closed. */
|
|
59
59
|
store?: HandlerSqliteStore;
|
|
60
|
-
/** When true, do not persist
|
|
60
|
+
/** When true, do not persist, only compute (used by inbox.triage.list). */
|
|
61
61
|
dryRun?: boolean;
|
|
62
62
|
/** Clock injection for deterministic updatedAt in tests. */
|
|
63
63
|
now?: () => Date;
|
|
@@ -254,7 +254,7 @@ export function enrichItemsWithTriage<T extends { id: string }>(
|
|
|
254
254
|
items: readonly T[],
|
|
255
255
|
): Array<T & TriageOverlay> {
|
|
256
256
|
if (items.length === 0) return [];
|
|
257
|
-
// Single batched read (`WHERE id IN (...)`) instead of one SELECT per item
|
|
257
|
+
// Single batched read (`WHERE id IN (...)`) instead of one SELECT per item,
|
|
258
258
|
// this is a hot read path (every channels.inbox.list call), so the N+1 is
|
|
259
259
|
// collapsed to one query keyed by id.
|
|
260
260
|
const byId = readTriageMetadataBatch(
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
//
|
|
4
4
|
// Pure, dependency-free heuristic + naive-Bayes-style spam/priority scoring
|
|
5
5
|
// over an InboundChannelItem's textual surface (subject + snippet). No I/O,
|
|
6
|
-
// no credentials, no network
|
|
6
|
+
// no credentials, no network, safe to run on every polled item.
|
|
7
7
|
//
|
|
8
8
|
// The score is a normalized 0..1 confidence in the assigned label. The label
|
|
9
9
|
// is one of 'spam' | 'priority' | 'normal'. Scoring is deterministic so the
|
|
@@ -247,7 +247,7 @@ export function scoreInboundItem(
|
|
|
247
247
|
// A trailing question mark suggests an awaited answer.
|
|
248
248
|
if (/\?\s*$/.test(raw)) priorityLL += 0.5;
|
|
249
249
|
|
|
250
|
-
// Spam strongly suppresses priority
|
|
250
|
+
// Spam strongly suppresses priority, promotional text is rarely urgent.
|
|
251
251
|
priorityLL -= spam * 2.5;
|
|
252
252
|
|
|
253
253
|
const priority = clamp01(sigmoid(priorityLL));
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
// ---------------------------------------------------------------------------
|
|
2
|
-
// Triage tagger
|
|
2
|
+
// Triage tagger, Discord provider.
|
|
3
3
|
//
|
|
4
4
|
// Two paths, in priority order:
|
|
5
5
|
// 1. REAL thread tags: when the item targets a forum/media-channel thread and
|
|
6
6
|
// a forum-tag mapping resolves >=1 tag id, PATCH the thread's applied_tags
|
|
7
|
-
// (read-then-merge
|
|
7
|
+
// (read-then-merge, never blind overwrite). Exact fidelity to the
|
|
8
8
|
// contract's "Discord thread tags".
|
|
9
9
|
// 2. Reaction analog: Discord has no arbitrary per-message tags, so otherwise
|
|
10
10
|
// add a unicode reaction (PUT .../reactions/{emoji}/@me).
|
|
@@ -114,7 +114,7 @@ async function applyDiscordThreadTags(
|
|
|
114
114
|
// DATA-LOSS GUARD: we PATCH the thread's full applied_tags array, so we must
|
|
115
115
|
// first read the EXISTING tags and merge. If that read fails (network error
|
|
116
116
|
// OR a non-ok HTTP status), we have no idea what tags are currently on the
|
|
117
|
-
// thread
|
|
117
|
+
// thread, PATCHing with only our new ids would silently destroy whatever
|
|
118
118
|
// forum tags were already applied. Abort instead of overwriting.
|
|
119
119
|
const existing = await fetchDiscordAppliedTags(url, token, fetchImpl, ctx);
|
|
120
120
|
if (existing === null) {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// ---------------------------------------------------------------------------
|
|
2
|
-
// Triage tagger
|
|
2
|
+
// Triage tagger, IMAP provider.
|
|
3
3
|
//
|
|
4
4
|
// Applies a triage label as an IMAP keyword flag (STORE +FLAGS) over a minimal
|
|
5
5
|
// IMAP4rev1-over-TLS client. Credentials are resolved per-apply from the daemon
|
|
@@ -63,7 +63,7 @@ export type ImapConnect = (opts: {
|
|
|
63
63
|
servername: string;
|
|
64
64
|
}) => ImapSocketLike;
|
|
65
65
|
|
|
66
|
-
/** Production connector
|
|
66
|
+
/** Production connector, a real IMAP4rev1-over-TLS socket via node:tls. */
|
|
67
67
|
export const tlsImapConnect: ImapConnect = (opts) =>
|
|
68
68
|
tlsConnect(opts, () => {
|
|
69
69
|
/* greeting handled in the data pump */
|
|
@@ -128,7 +128,7 @@ export class ImapStoreError extends Error {
|
|
|
128
128
|
* in a value destined for an IMAP command line. CR/LF are the dangerous ones:
|
|
129
129
|
* an unescaped CRLF would terminate the current command and let an attacker
|
|
130
130
|
* inject a second IMAP command (CRLF injection). IMAP's quoted-string syntax
|
|
131
|
-
* has no escape for these control chars
|
|
131
|
+
* has no escape for these control chars, backslash only escapes `\` and `"`,
|
|
132
132
|
* so the only safe handling is to refuse the value outright.
|
|
133
133
|
*/
|
|
134
134
|
function assertImapSafe(value: string, field: string): void {
|
|
@@ -155,8 +155,8 @@ function quoteImap(value: string): string {
|
|
|
155
155
|
// A concrete UID sequence-set: one or more comma-separated single UIDs or
|
|
156
156
|
// numeric ranges (`12`, `3:9`, `1,4,7:9`). RFC 3501 also permits `*` (the
|
|
157
157
|
// largest UID in the mailbox) and ranges to `*`, but a wildcard would let a
|
|
158
|
-
// single value like `1:*` apply the flag to the ENTIRE mailbox
|
|
159
|
-
// intent when tagging one triaged item
|
|
158
|
+
// single value like `1:*` apply the flag to the ENTIRE mailbox, never the
|
|
159
|
+
// intent when tagging one triaged item, so `*` is rejected outright.
|
|
160
160
|
const IMAP_UID_SET = /^[0-9]+(:[0-9]+)?(,[0-9]+(:[0-9]+)?)*$/;
|
|
161
161
|
|
|
162
162
|
// An IMAP flag: an optional leading `\` (system flag, e.g. `\Seen`) followed by
|
|
@@ -187,7 +187,7 @@ function assertImapUid(value: string): void {
|
|
|
187
187
|
* UNQUOTED into the `+FLAGS (...)` list. assertImapSafe only blocks control
|
|
188
188
|
* chars; a flag containing a space (e.g. `\Seen Junk`) would still pass that
|
|
189
189
|
* guard and inject a second flag atom. This is the boundary check that
|
|
190
|
-
* prevents it
|
|
190
|
+
* prevents it, independent of any upstream normalizer.
|
|
191
191
|
*/
|
|
192
192
|
function assertImapFlag(value: string): void {
|
|
193
193
|
if (!IMAP_FLAG.test(value)) {
|
|
@@ -313,7 +313,7 @@ export function imapStoreFlagOverTls(
|
|
|
313
313
|
finish();
|
|
314
314
|
}
|
|
315
315
|
} else {
|
|
316
|
-
// NO/BAD: deterministic protocol rejection
|
|
316
|
+
// NO/BAD: deterministic protocol rejection, do not retry.
|
|
317
317
|
current.reject(new ImapStoreError(`IMAP command failed: ${status}`, false));
|
|
318
318
|
}
|
|
319
319
|
};
|
|
@@ -159,7 +159,7 @@ export function createTriageTagger(
|
|
|
159
159
|
return { ...base, reason: 'autotag-disabled' };
|
|
160
160
|
}
|
|
161
161
|
|
|
162
|
-
// Provider-side mutation is effectful
|
|
162
|
+
// Provider-side mutation is effectful, require explicit confirmation.
|
|
163
163
|
if (request.confirm !== true || request.explicitUserRequest !== true) {
|
|
164
164
|
throw new HandlerError(
|
|
165
165
|
'Provider-side triage tagging requires explicit user confirmation.',
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// ---------------------------------------------------------------------------
|
|
2
|
-
// Triage tagger
|
|
2
|
+
// Triage tagger, shared types and provider-agnostic helpers.
|
|
3
3
|
//
|
|
4
4
|
// Provider config shapes, the apply request/result contract, and the tag
|
|
5
5
|
// normalization helpers used by the IMAP/Slack/Discord modules. No I/O here.
|
|
@@ -17,7 +17,7 @@ export interface TaggerProviderConfig {
|
|
|
17
17
|
* optional forum-tag mapping. When `forumTagIds` maps a GoodVibes triage tag
|
|
18
18
|
* (e.g. 'GoodVibes/Spam') to a forum tag SNOWFLAKE id, items that target a
|
|
19
19
|
* forum/media-channel thread get that REAL thread tag applied (PATCH
|
|
20
|
-
* applied_tags)
|
|
20
|
+
* applied_tags), exact fidelity to the contract's "Discord thread tags".
|
|
21
21
|
* Without a mapping (or for non-thread messages) tagging degrades to a
|
|
22
22
|
* unicode reaction analog.
|
|
23
23
|
*/
|
|
@@ -29,7 +29,7 @@ export interface ApplyTagsRequest {
|
|
|
29
29
|
/** Provider-side tags to apply. Defaults to [labelToTag(label)] when omitted. */
|
|
30
30
|
tags?: readonly string[];
|
|
31
31
|
label?: TriageLabel;
|
|
32
|
-
/** Must be true
|
|
32
|
+
/** Must be true, provider-side mutation requires explicit confirmation. */
|
|
33
33
|
confirm?: boolean;
|
|
34
34
|
/** Mirror of the operator invocation context flag. */
|
|
35
35
|
explicitUserRequest?: boolean;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// ---------------------------------------------------------------------------
|
|
2
|
-
// Triage tagger
|
|
2
|
+
// Triage tagger, Slack provider.
|
|
3
3
|
//
|
|
4
4
|
// Applies a triage label as a Slack message reaction (reactions.add). The bot
|
|
5
5
|
// token is resolved per-apply from the daemon credential store and is never
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
// and the triage pipeline scores. It is NOT an SDK catalog contract: the
|
|
6
6
|
// published `channels.inbox.list` output schema (CHANNEL_INBOX_ITEM_SCHEMA) is
|
|
7
7
|
// owned by the SDK and never re-declared here. This is the internal poller
|
|
8
|
-
// item per the handoff doc
|
|
8
|
+
// item per the handoff doc, it carries `fromDigest` (never a raw sender id),
|
|
9
9
|
// `subjectPreview`/`bodyPreview` (PII-stripped, length-bounded) and an opaque
|
|
10
10
|
// `metadata` bag the tagger reads provider targeting from (imapUid, channelId,
|
|
11
11
|
// Slack ts, Discord messageId/threadId).
|
|
@@ -31,7 +31,7 @@ export interface InboundChannelItem {
|
|
|
31
31
|
/** Handoff-facing provider id ("slack" | "discord" | "email" | ...). */
|
|
32
32
|
readonly provider?: string;
|
|
33
33
|
readonly kind?: 'dm' | 'thread' | 'mention' | 'reaction';
|
|
34
|
-
/** SHA-256 first-N of sender external id
|
|
34
|
+
/** SHA-256 first-N of sender external id, NEVER a raw identifier. */
|
|
35
35
|
readonly fromDigest?: string;
|
|
36
36
|
/** Conversation id (Slack/Discord channel, IMAP mailbox-scoped). */
|
|
37
37
|
readonly conversationId?: string;
|
package/src/daemon/lifecycle.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* lifecycle.ts
|
|
2
|
+
* lifecycle.ts, resolve THIS binary's update-artifact identity for the SDK
|
|
3
3
|
* DaemonServer facade's auto-update lifecycle.
|
|
4
4
|
*
|
|
5
5
|
* The SDK facade now runs the entire self-update loop itself when handed a
|
|
@@ -7,12 +7,12 @@
|
|
|
7
7
|
* version against the release tags (no longer the version-blind sdk-package
|
|
8
8
|
* comparison that used to restart-loop), swaps only at an idle moment, keeps
|
|
9
9
|
* the outgoing file at `<path>.previous`, and leaves a receipt in the store it
|
|
10
|
-
* serves on /status. When the artifact is ABSENT, updates are host-managed
|
|
10
|
+
* serves on /status. When the artifact is ABSENT, updates are host-managed,
|
|
11
11
|
* the facade runs no loop (the safe embedded default).
|
|
12
12
|
*
|
|
13
13
|
* The one guard the facade does NOT apply is install-kind: it must never swap a
|
|
14
14
|
* dev `bun run daemon` interpreter or a bun-global package install. This helper
|
|
15
|
-
* is that guard
|
|
15
|
+
* is that guard, it hands the facade an artifact ONLY for a compiled binary
|
|
16
16
|
* install, so a dev run resolves to `undefined` (host-managed, no loop) and
|
|
17
17
|
* only a real self-contained binary self-updates.
|
|
18
18
|
*/
|
|
@@ -23,13 +23,13 @@ import { detectInstallKind } from '../runtime/update-check.ts';
|
|
|
23
23
|
export interface ResolveDaemonUpdateArtifactOptions {
|
|
24
24
|
/** The executable to identify; defaults to process.execPath. */
|
|
25
25
|
readonly execPath?: string;
|
|
26
|
-
/** Injectable so tests pin a fixture version
|
|
26
|
+
/** Injectable so tests pin a fixture version, never the live build VERSION. */
|
|
27
27
|
readonly version?: string;
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
/**
|
|
31
31
|
* The update-artifact identity to hand the DaemonServer facade, or `undefined`
|
|
32
|
-
* for a non-binary install (dev/source or bun-global package)
|
|
32
|
+
* for a non-binary install (dev/source or bun-global package), in which case
|
|
33
33
|
* the facade keeps updates host-managed and runs no swap loop.
|
|
34
34
|
*/
|
|
35
35
|
export function resolveDaemonUpdateArtifact(
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* local-daemon-state.ts
|
|
2
|
+
* local-daemon-state.ts, the two files a daemon writes about ITSELF.
|
|
3
3
|
*
|
|
4
4
|
* `status` and `update` want to report things no control-plane verb answers:
|
|
5
5
|
* how long the daemon has been up, whether the last start followed a crash,
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* <control-plane config dir>/control-plane/daemon-receipts.json
|
|
12
12
|
*
|
|
13
13
|
* They are READ here and never written, and the receipts are never marked
|
|
14
|
-
* delivered
|
|
14
|
+
* delivered, `/status?receipts=consume` hands each receipt to the first
|
|
15
15
|
* consuming reader exactly once, and a status command that quietly consumed
|
|
16
16
|
* them would take them away from the surface they were written for.
|
|
17
17
|
*
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
* The SDK owns the writers (`platform/daemon/lifecycle-marker.ts`,
|
|
23
23
|
* `platform/daemon/receipts.ts`) and its readers are not exported from the
|
|
24
24
|
* published package this repository pins, so the readers below are this
|
|
25
|
-
* repository's own
|
|
25
|
+
* repository's own, bounded and content-validated the same way, and no more
|
|
26
26
|
* trusting of the file than the SDK is. The shared-piece lane can re-point them
|
|
27
27
|
* at the SDK's own readers once those are exported.
|
|
28
28
|
*/
|
|
@@ -42,7 +42,7 @@ export interface DaemonLifecycleMarker {
|
|
|
42
42
|
readonly pid: number | undefined;
|
|
43
43
|
readonly failedStarts: number;
|
|
44
44
|
readonly version: string | undefined;
|
|
45
|
-
/** The version an automatic rollback moved AWAY from
|
|
45
|
+
/** The version an automatic rollback moved AWAY from, the build that crash looped. */
|
|
46
46
|
readonly rejectedVersion: string | undefined;
|
|
47
47
|
/** When an automatic rollback last restored the kept previous binary. */
|
|
48
48
|
readonly autoRollbackAt: number | undefined;
|
|
@@ -164,7 +164,7 @@ export function readDaemonReceipts(
|
|
|
164
164
|
}
|
|
165
165
|
|
|
166
166
|
export interface LocalDaemonState {
|
|
167
|
-
/** False when the caller asked about another machine
|
|
167
|
+
/** False when the caller asked about another machine, nothing below was read. */
|
|
168
168
|
readonly available: boolean;
|
|
169
169
|
/** Why it is unavailable, when it is. */
|
|
170
170
|
readonly unavailableReason: string | undefined;
|
|
@@ -195,7 +195,7 @@ export function describeLocalDaemonState(input: DescribeLocalDaemonStateInput):
|
|
|
195
195
|
return {
|
|
196
196
|
available: false,
|
|
197
197
|
unavailableReason:
|
|
198
|
-
'the uptime, update receipts and rollback state are read from files on the daemon\'s own host
|
|
198
|
+
'the uptime, update receipts and rollback state are read from files on the daemon\'s own host; '
|
|
199
199
|
+ 'run this command on that machine to see them',
|
|
200
200
|
marker: null,
|
|
201
201
|
receipts: [],
|
|
@@ -219,7 +219,7 @@ export function describeLocalDaemonState(input: DescribeLocalDaemonStateInput):
|
|
|
219
219
|
};
|
|
220
220
|
}
|
|
221
221
|
|
|
222
|
-
/** `3d 4h`, `4h 12m`, `12m 3s`, `9s
|
|
222
|
+
/** `3d 4h`, `4h 12m`, `12m 3s`, `9s`, two units, never more. */
|
|
223
223
|
export function formatDuration(milliseconds: number): string {
|
|
224
224
|
const totalSeconds = Math.max(0, Math.floor(milliseconds / 1000));
|
|
225
225
|
const days = Math.floor(totalSeconds / 86_400);
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* pair-command.ts
|
|
2
|
+
* pair-command.ts, `goodvibes-daemon pair`.
|
|
3
3
|
*
|
|
4
4
|
* Two forms.
|
|
5
5
|
*
|
|
6
|
-
* LOCAL FORM
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
116
|
+
/** `#pair=<token>&offers=...`, present even when no web origin is configured. */
|
|
117
117
|
readonly fragment: string;
|
|
118
|
-
/** `<webOrigin>/#pair
|
|
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
|
|
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
|
|
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
|
|
212
|
-
// usable
|
|
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
|
|
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
|
|
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
|
|
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
|
|
18
|
-
// line naming what happened and how to retry
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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`)
|
|
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
|
|
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 {
|