@indigoai-us/hq-cli 5.345.178 → 5.345.180
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 +39 -0
- package/dist/command-catalog.generated.d.ts +197 -1
- package/dist/command-catalog.generated.js +241 -1
- package/dist/commands/bot-jobs.d.ts +11 -9
- package/dist/commands/bot-jobs.js +56 -21
- package/dist/commands/bot.d.ts +37 -1
- package/dist/commands/bot.js +289 -13
- package/dist/commands/channels.d.ts +18 -1
- package/dist/commands/channels.js +185 -1
- package/dist/commands/daemon.d.ts +2 -0
- package/dist/commands/daemon.js +15 -4
- package/dist/commands/integrations-core.d.ts +2 -0
- package/dist/commands/integrations-core.js +1 -0
- package/dist/commands/lanes.d.ts +1 -1
- package/dist/commands/lanes.js +6 -2
- package/dist/commands/mesh.js +19 -0
- package/dist/commands/sources.d.ts +89 -0
- package/dist/commands/sources.js +363 -1
- package/dist/hq-cloud-sync-runner/bin/sync-runner.js +2285 -1743
- package/dist/hq-cloud-sync-runner/package.json +1 -1
- package/dist/lib/bot/config.d.ts +8 -0
- package/dist/lib/bot/fleet-local.d.ts +1 -1
- package/dist/lib/bot/fleet-local.js +82 -30
- package/dist/lib/bot/jobs/hot-signal-sync.d.ts +109 -0
- package/dist/lib/bot/jobs/hot-signal-sync.js +177 -0
- package/dist/lib/bot/local-jobs.d.ts +40 -4
- package/dist/lib/bot/local-jobs.js +72 -5
- package/dist/lib/bot/run.d.ts +15 -0
- package/dist/lib/bot/run.js +99 -1
- package/dist/lib/bot/runtime-auth.d.ts +119 -0
- package/dist/lib/bot/runtime-auth.js +251 -0
- package/dist/lib/daemon/catalog.d.ts +1 -0
- package/dist/lib/daemon/catalog.js +1 -1
- package/dist/lib/daemon/handoff.d.ts +4 -0
- package/dist/lib/daemon/handoff.js +6 -2
- package/dist/lib/daemon/install.d.ts +70 -2
- package/dist/lib/daemon/install.js +185 -27
- package/dist/lib/daemon/legacy.d.ts +9 -3
- package/dist/lib/daemon/legacy.js +11 -4
- package/dist/lib/daemon/migrate.js +1 -1
- package/dist/lib/daemon/run.d.ts +5 -2
- package/dist/lib/daemon/run.js +74 -22
- package/dist/lib/doctor/checks/anywhere.d.ts +6 -0
- package/dist/lib/doctor/checks/anywhere.js +35 -6
- package/dist/lib/flag-registry-worker-source.generated.d.ts +1 -1
- package/dist/lib/flag-registry-worker-source.generated.js +1 -1
- package/dist/lib/hot-channels/adapters/__fixtures__/dummy.d.ts +33 -0
- package/dist/lib/hot-channels/adapters/__fixtures__/dummy.js +53 -0
- package/dist/lib/hot-channels/adapters/__fixtures__/meeting-providers.d.ts +38 -0
- package/dist/lib/hot-channels/adapters/__fixtures__/meeting-providers.js +64 -0
- package/dist/lib/hot-channels/adapters/attio.d.ts +67 -0
- package/dist/lib/hot-channels/adapters/attio.js +374 -0
- package/dist/lib/hot-channels/adapters/gmail.d.ts +111 -0
- package/dist/lib/hot-channels/adapters/gmail.js +413 -0
- package/dist/lib/hot-channels/adapters/index.d.ts +23 -0
- package/dist/lib/hot-channels/adapters/index.js +39 -0
- package/dist/lib/hot-channels/adapters/linear.d.ts +79 -0
- package/dist/lib/hot-channels/adapters/linear.js +321 -0
- package/dist/lib/hot-channels/adapters/meeting-providers.d.ts +86 -0
- package/dist/lib/hot-channels/adapters/meeting-providers.js +253 -0
- package/dist/lib/hot-channels/adapters/meetings.d.ts +81 -0
- package/dist/lib/hot-channels/adapters/meetings.js +525 -0
- package/dist/lib/hot-channels/adapters/slack.d.ts +70 -0
- package/dist/lib/hot-channels/adapters/slack.js +277 -0
- package/dist/lib/hot-channels/alert.d.ts +78 -0
- package/dist/lib/hot-channels/alert.js +179 -0
- package/dist/lib/hot-channels/audience.d.ts +15 -0
- package/dist/lib/hot-channels/audience.js +25 -0
- package/dist/lib/hot-channels/contract.d.ts +146 -0
- package/dist/lib/hot-channels/contract.js +20 -0
- package/dist/lib/hot-channels/cursors.d.ts +41 -0
- package/dist/lib/hot-channels/cursors.js +84 -0
- package/dist/lib/hot-channels/index.d.ts +10 -0
- package/dist/lib/hot-channels/index.js +10 -0
- package/dist/lib/hot-channels/landing.d.ts +83 -0
- package/dist/lib/hot-channels/landing.js +175 -0
- package/dist/lib/hot-channels/ledger.d.ts +101 -0
- package/dist/lib/hot-channels/ledger.js +203 -0
- package/dist/lib/hot-channels/loop.d.ts +74 -0
- package/dist/lib/hot-channels/loop.js +221 -0
- package/dist/lib/hot-channels/prompts/signals-pass.md +22 -0
- package/dist/lib/hot-channels/registry.d.ts +28 -0
- package/dist/lib/hot-channels/registry.js +121 -0
- package/dist/lib/hot-channels/run.d.ts +137 -0
- package/dist/lib/hot-channels/run.js +324 -0
- package/dist/lib/hot-channels/signals-pass.d.ts +150 -0
- package/dist/lib/hot-channels/signals-pass.js +581 -0
- package/dist/lib/hot-channels/sync-lock.d.ts +51 -0
- package/dist/lib/hot-channels/sync-lock.js +224 -0
- package/dist/lib/install/hook-target.d.ts +6 -2
- package/dist/lib/install/hook-target.js +9 -4
- package/dist/lib/lanes/accounts.d.ts +1 -17
- package/dist/lib/lanes/accounts.js +5 -45
- package/dist/lib/lanes/loop.d.ts +10 -0
- package/dist/lib/lanes/loop.js +18 -1
- package/dist/lib/lanes/provider-home.d.ts +24 -0
- package/dist/lib/lanes/provider-home.js +52 -0
- package/dist/lib/lanes/spawn.d.ts +12 -0
- package/dist/lib/lanes/spawn.js +27 -0
- package/dist/lib/mesh/live/daemon/doctor.d.ts +3 -0
- package/dist/lib/mesh/live/daemon/doctor.js +13 -0
- package/dist/lib/mesh/live/daemon/owner-inbox-push.d.ts +94 -0
- package/dist/lib/mesh/live/daemon/owner-inbox-push.js +273 -0
- package/dist/lib/mesh/live/daemon/run.js +31 -0
- package/dist/lib/mesh/live/daemon/work-subscribe.d.ts +14 -0
- package/dist/lib/mesh/live/daemon/work-subscribe.js +210 -3
- package/dist/lib/mesh/live/paths.d.ts +2 -0
- package/dist/lib/mesh/live/paths.js +2 -0
- package/dist/lib/sync-core/flag.d.ts +1 -1
- package/dist/lib/sync-core/flag.js +1 -1
- package/dist/lib/sync-core/reconcile.d.ts +1 -1
- package/dist/lib/sync-core/reconcile.js +9 -7
- package/dist/lib/sync-core/release.d.ts +3 -3
- package/dist/lib/sync-core/release.js +4 -4
- package/dist/lib/sync-core/rollout-state.d.ts +1 -0
- package/dist/lib/sync-core/rollout-state.js +1 -0
- package/package.json +8 -8
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Declarative hot-channel adapter contract.
|
|
3
|
+
*
|
|
4
|
+
* Owner requirement (hot-signal-sync US-001), verbatim: "we want to be able to
|
|
5
|
+
* add more hot channels to the list without some massive engineering effort."
|
|
6
|
+
*
|
|
7
|
+
* Every channel-specific fact lives on the adapter object as data. The shared
|
|
8
|
+
* loop (loop.ts) is written once against this contract and only branches on
|
|
9
|
+
* `transport`. Adding a channel is one adapter registered in the registry plus
|
|
10
|
+
* one catalog entry; the loop, landing format, and readers do not change.
|
|
11
|
+
*/
|
|
12
|
+
/** How events reach HQ: pushed into the person's buffer, or polled by cursor. */
|
|
13
|
+
export type HotTransport = "push" | "poll";
|
|
14
|
+
export declare const HOT_TRANSPORTS: readonly HotTransport[];
|
|
15
|
+
/** Upper bound on any adapter's catch-up window (30 days). */
|
|
16
|
+
export declare const MAX_CATCH_UP_WINDOW_MS: number;
|
|
17
|
+
/**
|
|
18
|
+
* Bounded look-back. The loop never lands an event older than
|
|
19
|
+
* `now - maxLookbackMs`, and poll adapters receive it as `since`.
|
|
20
|
+
*/
|
|
21
|
+
export interface CatchUpWindow {
|
|
22
|
+
maxLookbackMs: number;
|
|
23
|
+
}
|
|
24
|
+
/** The connection a channel needs: provider id plus the scopes it requires. */
|
|
25
|
+
export interface TokenScope {
|
|
26
|
+
provider: string;
|
|
27
|
+
scopes: readonly string[];
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Rule-based suggestion declaration. Pure data, evaluated without a model
|
|
31
|
+
* call. US-001 declares and validates it; evaluation belongs to US-014.
|
|
32
|
+
*/
|
|
33
|
+
export type SuggestionRule = {
|
|
34
|
+
kind: "recent-activity";
|
|
35
|
+
withinMs: number;
|
|
36
|
+
minEvents: number;
|
|
37
|
+
} | {
|
|
38
|
+
kind: "assigned-to-me";
|
|
39
|
+
} | {
|
|
40
|
+
kind: "owned-by-me";
|
|
41
|
+
};
|
|
42
|
+
export declare const SUGGESTION_RULE_KINDS: readonly SuggestionRule["kind"][];
|
|
43
|
+
/** The normalized event an adapter emits, independent of the provider. */
|
|
44
|
+
export interface HotEvent {
|
|
45
|
+
/** Provider-side id of the object the event is about. */
|
|
46
|
+
providerId: string;
|
|
47
|
+
/** Provider-side version (revision, updated-at, sequence). Part of the dedupe key. */
|
|
48
|
+
version: string;
|
|
49
|
+
who: string;
|
|
50
|
+
what: string;
|
|
51
|
+
where: string;
|
|
52
|
+
/** ISO 8601 time the event happened. */
|
|
53
|
+
when: string;
|
|
54
|
+
audience: string[];
|
|
55
|
+
title?: string;
|
|
56
|
+
body?: string;
|
|
57
|
+
/** Provider payload, written verbatim to the `.raw.json` sidecar. */
|
|
58
|
+
raw?: unknown;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* The adapter's handle to one connection, routed through the HQ integration
|
|
62
|
+
* gateway. The gateway holds the provider token; an adapter never sees it.
|
|
63
|
+
*/
|
|
64
|
+
export interface HotChannelGateway {
|
|
65
|
+
connectionId: string;
|
|
66
|
+
provider: string;
|
|
67
|
+
/** Call one provider tool on this connection and return its unwrapped result. */
|
|
68
|
+
call(tool: string, args: Record<string, unknown>): Promise<unknown>;
|
|
69
|
+
}
|
|
70
|
+
export interface PollContext {
|
|
71
|
+
/** ISO time: now minus the adapter's catch-up window. */
|
|
72
|
+
since: string;
|
|
73
|
+
personUid: string;
|
|
74
|
+
/** Present when the run is bound to a connection (on-demand and scheduled syncs). */
|
|
75
|
+
gateway?: HotChannelGateway;
|
|
76
|
+
/**
|
|
77
|
+
* Gateways for the adapter's `companionProviders`, one per usable
|
|
78
|
+
* connection. Set only for adapters that declare companions (meetings reads
|
|
79
|
+
* the vault and also pulls Fireflies or Granola notes).
|
|
80
|
+
*/
|
|
81
|
+
gateways?: HotChannelGateway[];
|
|
82
|
+
/**
|
|
83
|
+
* Called before an adapter writes to the vault itself (meetings writes
|
|
84
|
+
* provider notes during fetch). Throws to stop the run when the sync lock
|
|
85
|
+
* was lost.
|
|
86
|
+
*/
|
|
87
|
+
beforeWrite?: () => Promise<void>;
|
|
88
|
+
}
|
|
89
|
+
export interface PollPage {
|
|
90
|
+
events: HotEvent[];
|
|
91
|
+
/** Cursor to resume from. Stored only after every event on the page lands. */
|
|
92
|
+
cursor: string | null;
|
|
93
|
+
/** True when the provider has another page ready now. */
|
|
94
|
+
hasMore?: boolean;
|
|
95
|
+
}
|
|
96
|
+
/** Fields every adapter declares, whatever its transport. */
|
|
97
|
+
export interface HotChannelDeclaration {
|
|
98
|
+
/** Adapter id. Used in `event_key` and for push-buffer / cursor paths. */
|
|
99
|
+
id: string;
|
|
100
|
+
/** Source channel segment: landed docs go to `sources/{channel}/`. */
|
|
101
|
+
channel: string;
|
|
102
|
+
transport: HotTransport;
|
|
103
|
+
hotEventTypes: readonly string[];
|
|
104
|
+
catchUpWindow: CatchUpWindow;
|
|
105
|
+
tokenScope: TokenScope;
|
|
106
|
+
suggestionRule: SuggestionRule;
|
|
107
|
+
/**
|
|
108
|
+
* Optional feature-flag gate. When set, the adapter runs only for companies
|
|
109
|
+
* whose catalog lists it, which hq-pro does only while `flag` is on. With the
|
|
110
|
+
* flag off, `hq sources sync` reports the adapter as `gated` and never calls it.
|
|
111
|
+
*/
|
|
112
|
+
gate?: HotChannelGate;
|
|
113
|
+
/**
|
|
114
|
+
* Optional extra providers whose usable connections this one channel also
|
|
115
|
+
* reads, e.g. meetings pulls Fireflies and Granola beside the HQ bot. The
|
|
116
|
+
* run hands every matching connection to `fetch` as `PollContext.gateways`.
|
|
117
|
+
*/
|
|
118
|
+
companionProviders?: readonly string[];
|
|
119
|
+
}
|
|
120
|
+
/** A feature flag the adapter needs on before it runs. */
|
|
121
|
+
export interface HotChannelGate {
|
|
122
|
+
/** Flag key as registered in hq-pro, e.g. `hq.hot-channel-gmail`. */
|
|
123
|
+
flag: string;
|
|
124
|
+
}
|
|
125
|
+
export interface PushHotChannelAdapter extends HotChannelDeclaration {
|
|
126
|
+
transport: "push";
|
|
127
|
+
}
|
|
128
|
+
export interface PollHotChannelAdapter extends HotChannelDeclaration {
|
|
129
|
+
transport: "poll";
|
|
130
|
+
fetch(cursor: string | null, ctx: PollContext): Promise<PollPage>;
|
|
131
|
+
}
|
|
132
|
+
export type HotChannelAdapter = PushHotChannelAdapter | PollHotChannelAdapter;
|
|
133
|
+
/** Minimal catalog entry shape; the hq-pro catalog (US-002) extends it. */
|
|
134
|
+
export interface CatalogEntry {
|
|
135
|
+
provider: string;
|
|
136
|
+
enabled?: boolean;
|
|
137
|
+
/**
|
|
138
|
+
* The connection this entry runs against. A channel runs once per usable
|
|
139
|
+
* connection, and each connection keeps its own cursor. Unset for channels
|
|
140
|
+
* that need no connection (meetings reads the vault).
|
|
141
|
+
*/
|
|
142
|
+
connectionId?: string;
|
|
143
|
+
/** Companion connections (see `companionProviders`) handed to this entry's run. */
|
|
144
|
+
companionConnectionIds?: readonly string[];
|
|
145
|
+
}
|
|
146
|
+
//# sourceMappingURL=contract.d.ts.map
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Declarative hot-channel adapter contract.
|
|
3
|
+
*
|
|
4
|
+
* Owner requirement (hot-signal-sync US-001), verbatim: "we want to be able to
|
|
5
|
+
* add more hot channels to the list without some massive engineering effort."
|
|
6
|
+
*
|
|
7
|
+
* Every channel-specific fact lives on the adapter object as data. The shared
|
|
8
|
+
* loop (loop.ts) is written once against this contract and only branches on
|
|
9
|
+
* `transport`. Adding a channel is one adapter registered in the registry plus
|
|
10
|
+
* one catalog entry; the loop, landing format, and readers do not change.
|
|
11
|
+
*/
|
|
12
|
+
export const HOT_TRANSPORTS = ["push", "poll"];
|
|
13
|
+
/** Upper bound on any adapter's catch-up window (30 days). */
|
|
14
|
+
export const MAX_CATCH_UP_WINDOW_MS = 30 * 24 * 60 * 60 * 1000;
|
|
15
|
+
export const SUGGESTION_RULE_KINDS = [
|
|
16
|
+
"recent-activity",
|
|
17
|
+
"assigned-to-me",
|
|
18
|
+
"owned-by-me",
|
|
19
|
+
];
|
|
20
|
+
//# sourceMappingURL=contract.js.map
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/** What one cursor file holds. */
|
|
2
|
+
export interface CursorRecord {
|
|
3
|
+
cursor: string | null;
|
|
4
|
+
updatedAt: string;
|
|
5
|
+
/** `{adapter}:{provider_id}:{version}` of the last event processed before this cursor. */
|
|
6
|
+
lastEventKey: string | null;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Where poll cursors live, keyed by the person's audience key (see
|
|
10
|
+
* `audienceKey`). `set` keeps the stored `lastEventKey` when none is passed.
|
|
11
|
+
*/
|
|
12
|
+
export interface CursorStore {
|
|
13
|
+
get(personKey: string, adapterId: string): Promise<string | null>;
|
|
14
|
+
set(personKey: string, adapterId: string, cursor: string | null, lastEventKey?: string | null): Promise<void>;
|
|
15
|
+
}
|
|
16
|
+
/** `@{personKey}`, the per-person directory name under `_cursors/` and `_runs/`. */
|
|
17
|
+
export declare function personDirName(personKey: string): string;
|
|
18
|
+
/**
|
|
19
|
+
* The poller only writes under `sources/`. This refuses any path that resolves
|
|
20
|
+
* into `secrets/integrations/`, so a misconfigured vault root cannot make a run
|
|
21
|
+
* touch integration credentials.
|
|
22
|
+
*/
|
|
23
|
+
export declare function assertNotIntegrationSecretsPath(file: string): void;
|
|
24
|
+
/**
|
|
25
|
+
* The cursor-store key for one adapter on one connection:
|
|
26
|
+
* `{adapterId}.{connectionId}`, or `{adapterId}` when the channel has no
|
|
27
|
+
* connection. A connection id that cannot be a file-name segment as-is
|
|
28
|
+
* (uppercase, other characters, too long) is replaced by a hash of it, so two
|
|
29
|
+
* connections never share a cursor file.
|
|
30
|
+
*/
|
|
31
|
+
export declare function cursorKey(adapterId: string, connectionId?: string | null): string;
|
|
32
|
+
/** `{vaultRoot}/sources/_cursors/@{personKey}/{adapterId}.json` (adapterId may be a `cursorKey`). */
|
|
33
|
+
export declare function cursorFilePath(vaultRoot: string, personKey: string, adapterId: string): string;
|
|
34
|
+
export interface VaultCursorStore extends CursorStore {
|
|
35
|
+
read(personKey: string, adapterId: string): Promise<CursorRecord | null>;
|
|
36
|
+
}
|
|
37
|
+
/** One JSON file per person and adapter in the company vault. */
|
|
38
|
+
export declare function createVaultCursorStore(vaultRoot: string, options?: {
|
|
39
|
+
now?: () => Date;
|
|
40
|
+
}): VaultCursorStore;
|
|
41
|
+
//# sourceMappingURL=cursors.d.ts.map
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import * as fs from "node:fs/promises";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
import { assertSafeSegment, writeFileAtomic } from "./landing.js";
|
|
5
|
+
/** `@{personKey}`, the per-person directory name under `_cursors/` and `_runs/`. */
|
|
6
|
+
export function personDirName(personKey) {
|
|
7
|
+
assertSafeSegment(personKey, "person key");
|
|
8
|
+
return `@${personKey}`;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* The poller only writes under `sources/`. This refuses any path that resolves
|
|
12
|
+
* into `secrets/integrations/`, so a misconfigured vault root cannot make a run
|
|
13
|
+
* touch integration credentials.
|
|
14
|
+
*/
|
|
15
|
+
export function assertNotIntegrationSecretsPath(file) {
|
|
16
|
+
const parts = path.resolve(file).split(path.sep);
|
|
17
|
+
for (let i = 0; i < parts.length - 1; i += 1) {
|
|
18
|
+
if (parts[i] === "secrets" && parts[i + 1] === "integrations") {
|
|
19
|
+
throw new Error("hot-channel runs never write under secrets/integrations/");
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
const SAFE_KEY = /^[a-z0-9][a-z0-9._-]{0,63}$/;
|
|
24
|
+
/**
|
|
25
|
+
* The cursor-store key for one adapter on one connection:
|
|
26
|
+
* `{adapterId}.{connectionId}`, or `{adapterId}` when the channel has no
|
|
27
|
+
* connection. A connection id that cannot be a file-name segment as-is
|
|
28
|
+
* (uppercase, other characters, too long) is replaced by a hash of it, so two
|
|
29
|
+
* connections never share a cursor file.
|
|
30
|
+
*/
|
|
31
|
+
export function cursorKey(adapterId, connectionId) {
|
|
32
|
+
assertSafeSegment(adapterId, "adapter id");
|
|
33
|
+
if (connectionId === undefined || connectionId === null)
|
|
34
|
+
return adapterId;
|
|
35
|
+
if (typeof connectionId !== "string" || connectionId.length === 0) {
|
|
36
|
+
throw new Error("connection id must be a non-empty string");
|
|
37
|
+
}
|
|
38
|
+
const plain = `${adapterId}.${connectionId}`;
|
|
39
|
+
if (SAFE_KEY.test(plain) && !plain.includes(".."))
|
|
40
|
+
return plain;
|
|
41
|
+
const hash = createHash("sha256").update(connectionId).digest("hex").slice(0, 16);
|
|
42
|
+
return `${adapterId}.c-${hash}`;
|
|
43
|
+
}
|
|
44
|
+
/** `{vaultRoot}/sources/_cursors/@{personKey}/{adapterId}.json` (adapterId may be a `cursorKey`). */
|
|
45
|
+
export function cursorFilePath(vaultRoot, personKey, adapterId) {
|
|
46
|
+
assertSafeSegment(adapterId, "adapter id");
|
|
47
|
+
const file = path.join(vaultRoot, "sources", "_cursors", personDirName(personKey), `${adapterId}.json`);
|
|
48
|
+
assertNotIntegrationSecretsPath(file);
|
|
49
|
+
return file;
|
|
50
|
+
}
|
|
51
|
+
/** One JSON file per person and adapter in the company vault. */
|
|
52
|
+
export function createVaultCursorStore(vaultRoot, options = {}) {
|
|
53
|
+
const now = options.now ?? (() => new Date());
|
|
54
|
+
const read = async (personKey, adapterId) => {
|
|
55
|
+
let text;
|
|
56
|
+
try {
|
|
57
|
+
text = await fs.readFile(cursorFilePath(vaultRoot, personKey, adapterId), "utf8");
|
|
58
|
+
}
|
|
59
|
+
catch (err) {
|
|
60
|
+
if (err.code === "ENOENT")
|
|
61
|
+
return null;
|
|
62
|
+
throw err;
|
|
63
|
+
}
|
|
64
|
+
const parsed = JSON.parse(text);
|
|
65
|
+
return {
|
|
66
|
+
cursor: typeof parsed.cursor === "string" ? parsed.cursor : null,
|
|
67
|
+
updatedAt: typeof parsed.updatedAt === "string" ? parsed.updatedAt : "",
|
|
68
|
+
lastEventKey: typeof parsed.lastEventKey === "string" ? parsed.lastEventKey : null,
|
|
69
|
+
};
|
|
70
|
+
};
|
|
71
|
+
return {
|
|
72
|
+
read,
|
|
73
|
+
async get(personKey, adapterId) {
|
|
74
|
+
return (await read(personKey, adapterId))?.cursor ?? null;
|
|
75
|
+
},
|
|
76
|
+
async set(personKey, adapterId, cursor, lastEventKey) {
|
|
77
|
+
const file = cursorFilePath(vaultRoot, personKey, adapterId);
|
|
78
|
+
const kept = lastEventKey === undefined ? ((await read(personKey, adapterId))?.lastEventKey ?? null) : lastEventKey;
|
|
79
|
+
const record = { cursor, updatedAt: now().toISOString(), lastEventKey: kept };
|
|
80
|
+
await writeFileAtomic(file, `${JSON.stringify(record)}\n`);
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
//# sourceMappingURL=cursors.js.map
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export * from "./contract.js";
|
|
2
|
+
export * from "./registry.js";
|
|
3
|
+
export * from "./landing.js";
|
|
4
|
+
export * from "./audience.js";
|
|
5
|
+
export * from "./cursors.js";
|
|
6
|
+
export * from "./loop.js";
|
|
7
|
+
export * from "./ledger.js";
|
|
8
|
+
export * from "./run.js";
|
|
9
|
+
export * from "./adapters/index.js";
|
|
10
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export * from "./contract.js";
|
|
2
|
+
export * from "./registry.js";
|
|
3
|
+
export * from "./landing.js";
|
|
4
|
+
export * from "./audience.js";
|
|
5
|
+
export * from "./cursors.js";
|
|
6
|
+
export * from "./loop.js";
|
|
7
|
+
export * from "./ledger.js";
|
|
8
|
+
export * from "./run.js";
|
|
9
|
+
export * from "./adapters/index.js";
|
|
10
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import type { HotChannelDeclaration, HotEvent } from "./contract.js";
|
|
2
|
+
/** `origin` written on every hot-signal landed document. */
|
|
3
|
+
export declare const HOT_SIGNAL_ORIGIN = "hot-signal-sync";
|
|
4
|
+
export type LandingOutcome = "landed" | "duplicate";
|
|
5
|
+
/**
|
|
6
|
+
* Optional landing hints an adapter may set on an event it emits. They change
|
|
7
|
+
* only how the event is written, never its dedupe identity.
|
|
8
|
+
*/
|
|
9
|
+
export interface HotEventLandingHints {
|
|
10
|
+
/**
|
|
11
|
+
* Vault-relative key of an existing source document the event points at,
|
|
12
|
+
* under `sources/`. When set, the landed document's `payload_ref` names it
|
|
13
|
+
* and `raw_ref` names the `.raw.json` sidecar.
|
|
14
|
+
*/
|
|
15
|
+
payloadRef?: string;
|
|
16
|
+
/** Extra scalar frontmatter fields. They may not replace a field landing writes. */
|
|
17
|
+
frontmatter?: Record<string, string>;
|
|
18
|
+
}
|
|
19
|
+
export interface LandHotEventInput {
|
|
20
|
+
/** Root of the person's vault scope; `sources/` lives directly under it. */
|
|
21
|
+
vaultRoot: string;
|
|
22
|
+
adapter: Pick<HotChannelDeclaration, "id" | "channel">;
|
|
23
|
+
personUid: string;
|
|
24
|
+
event: HotEvent & HotEventLandingHints;
|
|
25
|
+
now?: Date;
|
|
26
|
+
}
|
|
27
|
+
export interface LandHotEventResult {
|
|
28
|
+
outcome: LandingOutcome;
|
|
29
|
+
eventKey: string;
|
|
30
|
+
sourceId: string;
|
|
31
|
+
/** Vault-relative key of the landed markdown document. */
|
|
32
|
+
markdownKey: string;
|
|
33
|
+
/** Vault-relative key of the raw payload sidecar (`payload_ref`). */
|
|
34
|
+
rawKey: string;
|
|
35
|
+
}
|
|
36
|
+
/** Index row; same shape as hq-pro's `SourceIndexEntry`. */
|
|
37
|
+
export interface HotSourceIndexEntry {
|
|
38
|
+
source_ref: {
|
|
39
|
+
channel: string;
|
|
40
|
+
source_id: string;
|
|
41
|
+
key: string;
|
|
42
|
+
};
|
|
43
|
+
channel: string;
|
|
44
|
+
source_id: string;
|
|
45
|
+
keys: {
|
|
46
|
+
markdown: string;
|
|
47
|
+
raw: string;
|
|
48
|
+
};
|
|
49
|
+
written_at: string;
|
|
50
|
+
title?: string | null;
|
|
51
|
+
ingested_at?: string;
|
|
52
|
+
event_key: string;
|
|
53
|
+
}
|
|
54
|
+
/** `{adapter}:{provider_id}:{version}`, the dedupe identity of an event. */
|
|
55
|
+
export declare function eventKey(adapterId: string, providerId: string, version: string): string;
|
|
56
|
+
/**
|
|
57
|
+
* Deterministic, path-safe source id for an event key. The same event always
|
|
58
|
+
* maps to the same file, which is what makes landing idempotent.
|
|
59
|
+
*/
|
|
60
|
+
export declare function hotSourceId(adapterId: string, key: string): string;
|
|
61
|
+
export declare function assertSafeSegment(value: string, label: string): void;
|
|
62
|
+
/** A single event is malformed. The loop quarantines it and keeps going. */
|
|
63
|
+
export declare class HotEventValidationError extends Error {
|
|
64
|
+
constructor(message: string);
|
|
65
|
+
}
|
|
66
|
+
/** Throws `HotEventValidationError` unless the event has every required field. */
|
|
67
|
+
export declare function assertHotEvent(event: HotEvent): void;
|
|
68
|
+
/**
|
|
69
|
+
* A payload ref is a vault-relative key under `sources/`: at least three
|
|
70
|
+
* segments, none empty, `.` or `..`, and no backslash or NUL. The signals pass
|
|
71
|
+
* checks a landed `payload_ref` with the same rule before reading it.
|
|
72
|
+
*/
|
|
73
|
+
export declare function assertPayloadRef(ref: string): void;
|
|
74
|
+
/** Write via a sibling temp file and rename, so readers never see a partial file. */
|
|
75
|
+
export declare function writeFileAtomic(file: string, contents: string): Promise<void>;
|
|
76
|
+
/**
|
|
77
|
+
* Land one normalized event as a source document under
|
|
78
|
+
* `sources/{channel}/{source_id}.md`, with its raw payload alongside and a row
|
|
79
|
+
* in the day's source index. Returns `duplicate` without writing anything when
|
|
80
|
+
* a document for the same `event_key` already exists.
|
|
81
|
+
*/
|
|
82
|
+
export declare function landHotEvent(input: LandHotEventInput): Promise<LandHotEventResult>;
|
|
83
|
+
//# sourceMappingURL=landing.d.ts.map
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
2
|
+
import * as fs from "node:fs/promises";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
import * as yaml from "js-yaml";
|
|
5
|
+
/** `origin` written on every hot-signal landed document. */
|
|
6
|
+
export const HOT_SIGNAL_ORIGIN = "hot-signal-sync";
|
|
7
|
+
const SAFE_SEGMENT = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
|
|
8
|
+
/** `{adapter}:{provider_id}:{version}`, the dedupe identity of an event. */
|
|
9
|
+
export function eventKey(adapterId, providerId, version) {
|
|
10
|
+
return `${adapterId}:${providerId}:${version}`;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Deterministic, path-safe source id for an event key. The same event always
|
|
14
|
+
* maps to the same file, which is what makes landing idempotent.
|
|
15
|
+
*/
|
|
16
|
+
export function hotSourceId(adapterId, key) {
|
|
17
|
+
const digest = createHash("sha256").update(key).digest("hex").slice(0, 32);
|
|
18
|
+
return `${adapterId}-${digest}`;
|
|
19
|
+
}
|
|
20
|
+
export function assertSafeSegment(value, label) {
|
|
21
|
+
if (typeof value !== "string" ||
|
|
22
|
+
value.includes("..") ||
|
|
23
|
+
value.includes("/") ||
|
|
24
|
+
value.includes("\\") ||
|
|
25
|
+
value.includes("\0") ||
|
|
26
|
+
!SAFE_SEGMENT.test(value)) {
|
|
27
|
+
throw new Error(`${label} must be a safe path segment`);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
/** A single event is malformed. The loop quarantines it and keeps going. */
|
|
31
|
+
export class HotEventValidationError extends Error {
|
|
32
|
+
constructor(message) {
|
|
33
|
+
super(message);
|
|
34
|
+
this.name = "HotEventValidationError";
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
/** Throws `HotEventValidationError` unless the event has every required field. */
|
|
38
|
+
export function assertHotEvent(event) {
|
|
39
|
+
if (!event || typeof event !== "object")
|
|
40
|
+
throw new HotEventValidationError("hot event is not an object");
|
|
41
|
+
for (const field of ["providerId", "version", "who", "what", "where", "when"]) {
|
|
42
|
+
if (typeof event[field] !== "string" || event[field].length === 0) {
|
|
43
|
+
throw new HotEventValidationError(`hot event is missing ${field}`);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
if (Number.isNaN(Date.parse(event.when))) {
|
|
47
|
+
throw new HotEventValidationError("hot event `when` must be an ISO date-time");
|
|
48
|
+
}
|
|
49
|
+
if (!Array.isArray(event.audience) || !event.audience.every((a) => typeof a === "string")) {
|
|
50
|
+
throw new HotEventValidationError("hot event audience must be a list of strings");
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* A payload ref is a vault-relative key under `sources/`: at least three
|
|
55
|
+
* segments, none empty, `.` or `..`, and no backslash or NUL. The signals pass
|
|
56
|
+
* checks a landed `payload_ref` with the same rule before reading it.
|
|
57
|
+
*/
|
|
58
|
+
export function assertPayloadRef(ref) {
|
|
59
|
+
const parts = typeof ref === "string" ? ref.split("/") : [];
|
|
60
|
+
if (ref.includes("\\") ||
|
|
61
|
+
ref.includes("\0") ||
|
|
62
|
+
parts.length < 3 ||
|
|
63
|
+
parts[0] !== "sources" ||
|
|
64
|
+
parts.some((part) => part === "" || part === "." || part === "..")) {
|
|
65
|
+
throw new HotEventValidationError("hot event payloadRef must be a vault-relative key under sources/");
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
async function exists(file) {
|
|
69
|
+
try {
|
|
70
|
+
await fs.access(file);
|
|
71
|
+
return true;
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
/** Write via a sibling temp file and rename, so readers never see a partial file. */
|
|
78
|
+
export async function writeFileAtomic(file, contents) {
|
|
79
|
+
await fs.mkdir(path.dirname(file), { recursive: true });
|
|
80
|
+
const tmp = `${file}.${process.pid}.${randomBytes(4).toString("hex")}.tmp`;
|
|
81
|
+
try {
|
|
82
|
+
await fs.writeFile(tmp, contents, "utf8");
|
|
83
|
+
await fs.rename(tmp, file);
|
|
84
|
+
}
|
|
85
|
+
catch (err) {
|
|
86
|
+
await fs.rm(tmp, { force: true });
|
|
87
|
+
throw err;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
async function appendIndexEntry(vaultRoot, day, entry) {
|
|
91
|
+
const file = path.join(vaultRoot, "sources", "_index", `${day}.json`);
|
|
92
|
+
let rows = [];
|
|
93
|
+
try {
|
|
94
|
+
const parsed = JSON.parse(await fs.readFile(file, "utf8"));
|
|
95
|
+
if (Array.isArray(parsed))
|
|
96
|
+
rows = parsed;
|
|
97
|
+
}
|
|
98
|
+
catch (err) {
|
|
99
|
+
if (err.code !== "ENOENT")
|
|
100
|
+
throw err;
|
|
101
|
+
}
|
|
102
|
+
if (rows.some((row) => row.channel === entry.channel && row.source_id === entry.source_id))
|
|
103
|
+
return;
|
|
104
|
+
rows.push(entry);
|
|
105
|
+
await writeFileAtomic(file, `${JSON.stringify(rows, null, 2)}\n`);
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Land one normalized event as a source document under
|
|
109
|
+
* `sources/{channel}/{source_id}.md`, with its raw payload alongside and a row
|
|
110
|
+
* in the day's source index. Returns `duplicate` without writing anything when
|
|
111
|
+
* a document for the same `event_key` already exists.
|
|
112
|
+
*/
|
|
113
|
+
export async function landHotEvent(input) {
|
|
114
|
+
const { vaultRoot, adapter, personUid, event } = input;
|
|
115
|
+
const now = (input.now ?? new Date()).toISOString();
|
|
116
|
+
assertSafeSegment(adapter.id, "adapter id");
|
|
117
|
+
assertSafeSegment(adapter.channel, "channel");
|
|
118
|
+
assertSafeSegment(personUid, "person uid");
|
|
119
|
+
assertHotEvent(event);
|
|
120
|
+
const key = eventKey(adapter.id, event.providerId, event.version);
|
|
121
|
+
const sourceId = hotSourceId(adapter.id, key);
|
|
122
|
+
const markdownKey = `sources/${adapter.channel}/${sourceId}.md`;
|
|
123
|
+
const rawKey = `sources/${adapter.channel}/${sourceId}.raw.json`;
|
|
124
|
+
const markdownFile = path.join(vaultRoot, markdownKey);
|
|
125
|
+
const result = { eventKey: key, sourceId, markdownKey, rawKey };
|
|
126
|
+
if (await exists(markdownFile))
|
|
127
|
+
return { outcome: "duplicate", ...result };
|
|
128
|
+
if (event.payloadRef !== undefined)
|
|
129
|
+
assertPayloadRef(event.payloadRef);
|
|
130
|
+
const title = event.title ?? event.what;
|
|
131
|
+
const frontmatter = {
|
|
132
|
+
id: sourceId,
|
|
133
|
+
channel: adapter.channel,
|
|
134
|
+
source_id: sourceId,
|
|
135
|
+
source_ref: { channel: adapter.channel, source_id: sourceId, key: markdownKey },
|
|
136
|
+
title,
|
|
137
|
+
origin: HOT_SIGNAL_ORIGIN,
|
|
138
|
+
person_uid: personUid,
|
|
139
|
+
created_at: event.when,
|
|
140
|
+
updated_at: now,
|
|
141
|
+
ingested_at: now,
|
|
142
|
+
event_key: key,
|
|
143
|
+
who: event.who,
|
|
144
|
+
what: event.what,
|
|
145
|
+
where: event.where,
|
|
146
|
+
when: event.when,
|
|
147
|
+
audience: [...event.audience],
|
|
148
|
+
payload_ref: event.payloadRef ?? rawKey,
|
|
149
|
+
};
|
|
150
|
+
if (event.payloadRef !== undefined)
|
|
151
|
+
frontmatter.raw_ref = rawKey;
|
|
152
|
+
for (const [field, value] of Object.entries(event.frontmatter ?? {})) {
|
|
153
|
+
if (field in frontmatter || typeof value !== "string") {
|
|
154
|
+
throw new HotEventValidationError(`hot event frontmatter field ${field} is reserved or not a string`);
|
|
155
|
+
}
|
|
156
|
+
frontmatter[field] = value;
|
|
157
|
+
}
|
|
158
|
+
const markdown = `---\n${yaml.dump(frontmatter, { lineWidth: -1, noRefs: true }).trimEnd()}\n---\n${event.body ?? ""}`;
|
|
159
|
+
// Raw first, markdown second: the markdown is the dedupe marker, so it must
|
|
160
|
+
// only exist once its payload_ref target does.
|
|
161
|
+
await writeFileAtomic(path.join(vaultRoot, rawKey), `${JSON.stringify(event.raw ?? event, null, 2)}\n`);
|
|
162
|
+
await writeFileAtomic(markdownFile, markdown);
|
|
163
|
+
await appendIndexEntry(vaultRoot, now.slice(0, 10), {
|
|
164
|
+
source_ref: frontmatter.source_ref,
|
|
165
|
+
channel: adapter.channel,
|
|
166
|
+
source_id: sourceId,
|
|
167
|
+
keys: { markdown: markdownKey, raw: rawKey },
|
|
168
|
+
written_at: now,
|
|
169
|
+
title,
|
|
170
|
+
ingested_at: now,
|
|
171
|
+
event_key: key,
|
|
172
|
+
});
|
|
173
|
+
return { outcome: "landed", ...result };
|
|
174
|
+
}
|
|
175
|
+
//# sourceMappingURL=landing.js.map
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The sources/signals run ledger. Every hot-channel run appends one JSON line
|
|
3
|
+
* per adapter to `sources/_runs/@{personKey}/{YYYY-MM-DD}.jsonl`, so a person
|
|
4
|
+
* can see what each run landed and what it produced.
|
|
5
|
+
*
|
|
6
|
+
* Owner, verbatim: "sync and build feels wrong. we should be logging sources and signals generated."
|
|
7
|
+
*/
|
|
8
|
+
import { type HotChannelRunResult, type RunHotChannelLoopInput, type WriteGuard } from "./loop.js";
|
|
9
|
+
/** The signals pass over one adapter's landed sources; checks `guard` before each write. */
|
|
10
|
+
export type SignalsGeneratedFn = (result: HotChannelRunResult, guard?: WriteGuard) => number | Promise<number>;
|
|
11
|
+
export type RunTrigger = "scheduled" | "on-demand";
|
|
12
|
+
export type RunStatus = "ok" | "failed" | "skipped";
|
|
13
|
+
export declare const RUN_TRIGGERS: readonly RunTrigger[];
|
|
14
|
+
/** One line of the run ledger: one adapter in one run. */
|
|
15
|
+
export interface RunLedgerEntry {
|
|
16
|
+
runId: string;
|
|
17
|
+
trigger: RunTrigger;
|
|
18
|
+
adapter: string;
|
|
19
|
+
/**
|
|
20
|
+
* The connection the adapter ran against; one line per adapter and
|
|
21
|
+
* connection. Null for a channel with no connection. Lines written before
|
|
22
|
+
* this field existed omit it.
|
|
23
|
+
*/
|
|
24
|
+
connectionId?: string | null;
|
|
25
|
+
sourcesLanded: number;
|
|
26
|
+
signalsGenerated: number;
|
|
27
|
+
startedAt: string;
|
|
28
|
+
finishedAt: string;
|
|
29
|
+
status: RunStatus;
|
|
30
|
+
error: string | null;
|
|
31
|
+
/** Malformed events this adapter quarantined and skipped. Omitted when 0. */
|
|
32
|
+
rejected?: number;
|
|
33
|
+
/** Why each rejected event was skipped. Omitted when nothing was rejected. */
|
|
34
|
+
rejectedReasons?: string[];
|
|
35
|
+
/** Why a `skipped` run did not run (for example, another sync is running). */
|
|
36
|
+
reason?: string;
|
|
37
|
+
/** The person the key in the path belongs to, so a human can map it back. */
|
|
38
|
+
personUid: string;
|
|
39
|
+
/**
|
|
40
|
+
* True when a scheduled run fired late, after a missed window (the computer
|
|
41
|
+
* was asleep or the bot was stopped). Omitted for runs that were not marked.
|
|
42
|
+
*/
|
|
43
|
+
catchUp?: boolean;
|
|
44
|
+
}
|
|
45
|
+
/** The UTC calendar day of an instant, `YYYY-MM-DD`. Ledger files are per UTC day. */
|
|
46
|
+
export declare function ledgerDay(at: Date): string;
|
|
47
|
+
/** `{vaultRoot}/sources/_runs/@{personKey}/{day}.jsonl` */
|
|
48
|
+
export declare function runLedgerPath(vaultRoot: string, personKey: string, day: string): string;
|
|
49
|
+
/**
|
|
50
|
+
* Append one entry to the ledger for the day it started. The file is opened
|
|
51
|
+
* with O_APPEND and the line is written in one call, so concurrent runs do not
|
|
52
|
+
* interleave within a line.
|
|
53
|
+
*/
|
|
54
|
+
export declare function appendRunLedgerEntry(vaultRoot: string, personKey: string, entry: RunLedgerEntry): Promise<string>;
|
|
55
|
+
export interface RunLedgerRead {
|
|
56
|
+
file: string;
|
|
57
|
+
entries: RunLedgerEntry[];
|
|
58
|
+
/** Lines that were not valid ledger entries; skipped and counted. */
|
|
59
|
+
malformed: number;
|
|
60
|
+
}
|
|
61
|
+
/** Read one day's ledger. A missing file is an empty ledger. */
|
|
62
|
+
export declare function readRunLedger(vaultRoot: string, personKey: string, day: string): Promise<RunLedgerRead>;
|
|
63
|
+
/** Per-adapter totals for one day's ledger. */
|
|
64
|
+
export interface RunLedgerAdapterSummary {
|
|
65
|
+
adapter: string;
|
|
66
|
+
runs: number;
|
|
67
|
+
sourcesLanded: number;
|
|
68
|
+
signalsGenerated: number;
|
|
69
|
+
failed: number;
|
|
70
|
+
firstStartedAt: string;
|
|
71
|
+
lastFinishedAt: string;
|
|
72
|
+
lastError: string | null;
|
|
73
|
+
}
|
|
74
|
+
export declare function summarizeRunLedger(entries: readonly RunLedgerEntry[]): RunLedgerAdapterSummary[];
|
|
75
|
+
export interface RunHotChannelSyncInput extends RunHotChannelLoopInput {
|
|
76
|
+
trigger: RunTrigger;
|
|
77
|
+
runId?: string;
|
|
78
|
+
/** Mark every ledger line of this run as a catch-up of a missed scheduled window. */
|
|
79
|
+
catchUp?: boolean;
|
|
80
|
+
/**
|
|
81
|
+
* The signals pass for this adapter's landed sources; returns the number of
|
|
82
|
+
* signals it wrote. Any throw makes this adapter's line `failed` with a count
|
|
83
|
+
* of 0 and the run goes on to the next adapter: a `SignalsPassError` (claude
|
|
84
|
+
* logged out, timed out, or returned bad output) records its code, any other
|
|
85
|
+
* error its message. With no callback the count is 0.
|
|
86
|
+
*/
|
|
87
|
+
signalsGenerated?: SignalsGeneratedFn;
|
|
88
|
+
}
|
|
89
|
+
export interface RunHotChannelSyncResult {
|
|
90
|
+
runId: string;
|
|
91
|
+
results: HotChannelRunResult[];
|
|
92
|
+
entries: RunLedgerEntry[];
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Run the hot-channel loop one adapter at a time and write a ledger line for
|
|
96
|
+
* each. The line is written in a `finally`, so an adapter that fails, or a run
|
|
97
|
+
* that throws partway, still leaves a `failed` line carrying the error text.
|
|
98
|
+
* A thrown error is re-raised after its line is written.
|
|
99
|
+
*/
|
|
100
|
+
export declare function runHotChannelSync(input: RunHotChannelSyncInput): Promise<RunHotChannelSyncResult>;
|
|
101
|
+
//# sourceMappingURL=ledger.d.ts.map
|