@loggie-ai/openclaw-plugin 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +205 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +12 -0
- package/dist/setup-entry.d.ts +73 -0
- package/dist/setup-entry.js +3 -0
- package/dist/src/account.d.ts +70 -0
- package/dist/src/account.js +182 -0
- package/dist/src/channel.d.ts +72 -0
- package/dist/src/channel.js +105 -0
- package/dist/src/config-schema.d.ts +98 -0
- package/dist/src/config-schema.js +55 -0
- package/dist/src/cursor-store.d.ts +86 -0
- package/dist/src/cursor-store.js +141 -0
- package/dist/src/doctor.d.ts +11 -0
- package/dist/src/doctor.js +29 -0
- package/dist/src/event-types.d.ts +113 -0
- package/dist/src/event-types.js +86 -0
- package/dist/src/loggie-client.d.ts +33 -0
- package/dist/src/loggie-client.js +74 -0
- package/dist/src/monitor/connection.d.ts +30 -0
- package/dist/src/monitor/connection.js +289 -0
- package/dist/src/monitor/event-handler.d.ts +27 -0
- package/dist/src/monitor/event-handler.js +90 -0
- package/dist/src/monitor/transcript-dispatch.d.ts +16 -0
- package/dist/src/monitor/transcript-dispatch.js +124 -0
- package/dist/src/monitor/transcript-format.d.ts +2 -0
- package/dist/src/monitor/transcript-format.js +41 -0
- package/dist/src/object.d.ts +4 -0
- package/dist/src/object.js +12 -0
- package/dist/src/routing.d.ts +11 -0
- package/dist/src/routing.js +13 -0
- package/dist/src/runtime.d.ts +3 -0
- package/dist/src/runtime.js +6 -0
- package/dist/src/status.d.ts +45 -0
- package/dist/src/status.js +45 -0
- package/index.ts +13 -0
- package/openclaw.plugin.json +71 -0
- package/package.json +93 -0
- package/plugin-inspector.config.json +15 -0
- package/setup-entry.ts +4 -0
- package/src/account.ts +265 -0
- package/src/channel.ts +148 -0
- package/src/config-schema.ts +57 -0
- package/src/cursor-store.ts +233 -0
- package/src/doctor.ts +39 -0
- package/src/event-types.ts +105 -0
- package/src/loggie-client.ts +111 -0
- package/src/monitor/connection.ts +349 -0
- package/src/monitor/event-handler.ts +133 -0
- package/src/monitor/transcript-dispatch.ts +145 -0
- package/src/monitor/transcript-format.ts +49 -0
- package/src/object.ts +15 -0
- package/src/routing.ts +27 -0
- package/src/runtime.ts +13 -0
- package/src/status.ts +72 -0
package/README.md
ADDED
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
# Loggie OpenClaw Plugin
|
|
2
|
+
|
|
3
|
+
OpenClaw channel plugin for Loggie meeting transcript events.
|
|
4
|
+
|
|
5
|
+
The plugin opens an outbound authenticated WebSocket to Loggie, replays from the
|
|
6
|
+
last durable cursor, and turns each `meeting.transcript.ready` event into one
|
|
7
|
+
OpenClaw inbound channel turn for the configured agent.
|
|
8
|
+
|
|
9
|
+
## How It Works
|
|
10
|
+
|
|
11
|
+
1. OpenClaw loads the setup-safe entrypoint for discovery, config inspection,
|
|
12
|
+
status, doctor checks, and SecretRef target discovery.
|
|
13
|
+
2. When the configured channel account starts, the full runtime entrypoint lazy
|
|
14
|
+
loads the WebSocket monitor.
|
|
15
|
+
3. The monitor opens a WebSocket to `baseUrl + socketPath`, authenticating with
|
|
16
|
+
either `Authorization: Bearer <token>` or `x-api-key: <token>`.
|
|
17
|
+
4. Before opening the socket, the plugin loads the durable cursor from OpenClaw
|
|
18
|
+
runtime state and appends it to the URL as `?cursor=<lastCursor>`.
|
|
19
|
+
5. Loggie sends event envelopes. The plugin validates the envelope, skips
|
|
20
|
+
duplicates or cursor regressions, and processes `meeting.transcript.ready`.
|
|
21
|
+
6. If the event includes `payload.detailPath`, the plugin fetches that detail
|
|
22
|
+
from the same Loggie origin with the same auth header. Cross-origin detail
|
|
23
|
+
URLs are rejected before auth headers are attached.
|
|
24
|
+
7. The plugin builds a deterministic OpenClaw session key from the agent,
|
|
25
|
+
account, and meeting id, records the inbound route, and runs the configured
|
|
26
|
+
OpenClaw agent with the transcript prompt.
|
|
27
|
+
8. The cursor advances only after dispatch succeeds, or after an unsupported
|
|
28
|
+
event is intentionally skipped.
|
|
29
|
+
|
|
30
|
+
## Loggie Server Contract
|
|
31
|
+
|
|
32
|
+
Minimum server-side behavior expected by this plugin:
|
|
33
|
+
|
|
34
|
+
- Authenticated WebSocket endpoint at `/api/events/socket` by default.
|
|
35
|
+
- WebSocket auth using the configured agent profile token.
|
|
36
|
+
- Initial replay from the WebSocket URL query: `?cursor=<cursor>`.
|
|
37
|
+
- Durable event ordering using `eventId` and monotonic string `cursor`.
|
|
38
|
+
- Client messages `{ "type": "ping" }` and `{ "type": "resume", "cursor": "..." }`.
|
|
39
|
+
- Replay pagination control messages:
|
|
40
|
+
`{ "type": "loggie.events.more_available", "cursor": "..." }`.
|
|
41
|
+
- `meeting.transcript.ready` event envelopes.
|
|
42
|
+
- Optional HTTP detail endpoint referenced by `payload.detailPath`.
|
|
43
|
+
- Control messages with `type: "hello"`, `"ack"`, or `"pong"` are accepted and
|
|
44
|
+
treated as connection/control traffic.
|
|
45
|
+
|
|
46
|
+
Example event:
|
|
47
|
+
|
|
48
|
+
```json
|
|
49
|
+
{
|
|
50
|
+
"cursor": "123",
|
|
51
|
+
"type": "meeting.transcript.ready",
|
|
52
|
+
"payload": {
|
|
53
|
+
"eventId": "evt_1",
|
|
54
|
+
"cursor": "123",
|
|
55
|
+
"type": "meeting.transcript.ready",
|
|
56
|
+
"workspaceId": "ws_1",
|
|
57
|
+
"meetingId": "mtg_1",
|
|
58
|
+
"meetingScheduleId": "sched_1",
|
|
59
|
+
"title": "Weekly Standup",
|
|
60
|
+
"source": "recall_ai",
|
|
61
|
+
"externalId": "tr_1",
|
|
62
|
+
"meetingDate": "2026-06-26T11:30:00.000Z",
|
|
63
|
+
"hasTranscript": true,
|
|
64
|
+
"detailPath": "/api/proxy/meetings/sched_1"
|
|
65
|
+
},
|
|
66
|
+
"createdAt": "2026-06-26T12:00:00.000Z"
|
|
67
|
+
}
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
The plugin also accepts the older internal envelope shape and `{ "event": { ... } }`
|
|
71
|
+
wrappers for compatibility, but the server socket shape above is the primary
|
|
72
|
+
contract.
|
|
73
|
+
|
|
74
|
+
## Configuration
|
|
75
|
+
|
|
76
|
+
Configure the channel under `channels.loggie`.
|
|
77
|
+
|
|
78
|
+
```json
|
|
79
|
+
{
|
|
80
|
+
"channels": {
|
|
81
|
+
"loggie": {
|
|
82
|
+
"enabled": true,
|
|
83
|
+
"baseUrl": "https://loggie.example.com",
|
|
84
|
+
"agentProfileId": "ap_123",
|
|
85
|
+
"credentialRef": "$LOGGIE_AGENT_TOKEN",
|
|
86
|
+
"agentId": "main"
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Important fields:
|
|
93
|
+
|
|
94
|
+
- `baseUrl`: Loggie HTTP origin. Defaults to `LOGGIE_BASE_URL`, then
|
|
95
|
+
`http://127.0.0.1:8787`.
|
|
96
|
+
- `socketPath`: WebSocket path. Defaults to `/api/events/socket`.
|
|
97
|
+
- `agentProfileId`: Loggie agent profile used for socket auth and OpenClaw
|
|
98
|
+
sender/account identity. Server events are workspace-scoped and do not need to
|
|
99
|
+
include this field.
|
|
100
|
+
- `credentialRef`: Token input. Use `$ENV_VAR` for env-backed credentials or a
|
|
101
|
+
structured SecretRef. If OpenClaw materializes a SecretRef before runtime, the
|
|
102
|
+
resulting string is treated as token material.
|
|
103
|
+
- `token`: Literal token fallback for local development.
|
|
104
|
+
- `authHeader`: `authorization` by default, or `x-api-key`.
|
|
105
|
+
- `agentId`: OpenClaw agent id that should receive transcript turns. Defaults to
|
|
106
|
+
`main`.
|
|
107
|
+
- `reconnect.minMs` and `reconnect.maxMs`: exponential reconnect backoff bounds.
|
|
108
|
+
- `heartbeat.timeoutMs`: stale socket timeout. Defaults to `120000`.
|
|
109
|
+
|
|
110
|
+
Environment defaults:
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
export LOGGIE_BASE_URL="https://loggie.example.com"
|
|
114
|
+
export LOGGIE_AGENT_TOKEN="..."
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
For multiple accounts, put account-specific settings under
|
|
118
|
+
`channels.loggie.accounts`:
|
|
119
|
+
|
|
120
|
+
```json
|
|
121
|
+
{
|
|
122
|
+
"channels": {
|
|
123
|
+
"loggie": {
|
|
124
|
+
"accountId": "prod",
|
|
125
|
+
"accounts": {
|
|
126
|
+
"prod": {
|
|
127
|
+
"baseUrl": "https://loggie.example.com",
|
|
128
|
+
"agentProfileId": "ap_prod",
|
|
129
|
+
"credentialRef": "$LOGGIE_AGENT_TOKEN",
|
|
130
|
+
"agentId": "main"
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
## Sessions And Cursoring
|
|
139
|
+
|
|
140
|
+
Session identity is deterministic:
|
|
141
|
+
|
|
142
|
+
- Route session key:
|
|
143
|
+
`agent:<agentId>:loggie:<accountId>:meeting:<meetingScheduleId|meetingId|eventId>`
|
|
144
|
+
- Embedded run session id:
|
|
145
|
+
`loggie-<accountId>-<meetingScheduleId|meetingId|eventId>`
|
|
146
|
+
|
|
147
|
+
The cursor store is required to be durable. Runtime startup fails if OpenClaw
|
|
148
|
+
does not provide a keyed runtime state store. This prevents silent in-memory
|
|
149
|
+
cursor loss and duplicate transcript dispatch after restart.
|
|
150
|
+
|
|
151
|
+
The plugin keeps a bounded set of recently seen event ids and skips:
|
|
152
|
+
|
|
153
|
+
- duplicate `eventId`
|
|
154
|
+
- lower numeric `cursor` values than the stored cursor
|
|
155
|
+
|
|
156
|
+
If transcript dispatch fails for the same event, the plugin records the failure
|
|
157
|
+
in the durable cursor state and retries it on replay. After 3 failed dispatch
|
|
158
|
+
attempts, the event is recorded in `deadLetteredEvents`, the cursor advances,
|
|
159
|
+
and later events can continue processing. The latest dead-letter is also
|
|
160
|
+
reported in channel status as `lastDeadLetterAt`, `lastDeadLetteredEventId`,
|
|
161
|
+
`lastDeadLetteredCursor`, `lastDeadLetterReason`, and
|
|
162
|
+
`lastDeadLetterAttempts`, and the monitor emits a warning log.
|
|
163
|
+
|
|
164
|
+
## Reconnect And Heartbeat Behavior
|
|
165
|
+
|
|
166
|
+
Unexpected socket errors or closes reconnect with exponential backoff. The
|
|
167
|
+
next socket URL includes the last persisted cursor so Loggie can replay missed
|
|
168
|
+
events. If Loggie reports `loggie.events.more_available`, the plugin sends
|
|
169
|
+
`{ "type": "resume", "cursor": "..." }` to request the next replay page.
|
|
170
|
+
|
|
171
|
+
The plugin sends periodic `{ "type": "ping" }` messages. The heartbeat timeout is
|
|
172
|
+
based on any received socket message, including `pong` and other control
|
|
173
|
+
messages. When the heartbeat expires, the plugin closes the socket and waits for
|
|
174
|
+
in-flight event handling to finish before reconnecting. That avoids dispatching
|
|
175
|
+
the same transcript twice while an earlier turn is still advancing the cursor.
|
|
176
|
+
|
|
177
|
+
## Security Notes
|
|
178
|
+
|
|
179
|
+
- Tokens are used only for outbound Loggie auth headers.
|
|
180
|
+
- Status and inspect output report token availability/source, not token values.
|
|
181
|
+
- Detail fetches are restricted to the configured Loggie origin.
|
|
182
|
+
- `credentialRef` should be `$ENV_VAR` or a structured SecretRef for config.
|
|
183
|
+
Plain strings are treated as materialized token values.
|
|
184
|
+
|
|
185
|
+
## Development
|
|
186
|
+
|
|
187
|
+
```bash
|
|
188
|
+
npm install
|
|
189
|
+
npm run check
|
|
190
|
+
npm test
|
|
191
|
+
npm run build
|
|
192
|
+
npm run preflight
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
Useful targeted commands:
|
|
196
|
+
|
|
197
|
+
```bash
|
|
198
|
+
npm run plugin:inspect
|
|
199
|
+
npm run plugin:inspect:runtime
|
|
200
|
+
npx tsc -p tsconfig.json --noEmit --noUnusedLocals --noUnusedParameters
|
|
201
|
+
npm pack --dry-run
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
`npm run preflight` runs typecheck, tests, build, and both plugin-inspector
|
|
205
|
+
passes.
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { defineChannelPluginEntry } from "openclaw/plugin-sdk/channel-core";
|
|
2
|
+
import { loggiePlugin } from "./src/channel.js";
|
|
3
|
+
import { loggieEntryConfigSchema } from "./src/config-schema.js";
|
|
4
|
+
import { setLoggieRuntime } from "./src/runtime.js";
|
|
5
|
+
export default defineChannelPluginEntry({
|
|
6
|
+
id: "loggie",
|
|
7
|
+
name: "Loggie",
|
|
8
|
+
description: "Receives durable Loggie meeting transcript events over WebSocket.",
|
|
9
|
+
plugin: loggiePlugin,
|
|
10
|
+
configSchema: loggieEntryConfigSchema,
|
|
11
|
+
setRuntime: setLoggieRuntime,
|
|
12
|
+
});
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
declare const _default: {
|
|
2
|
+
plugin: {
|
|
3
|
+
status: {
|
|
4
|
+
defaultRuntime: {
|
|
5
|
+
accountId: string;
|
|
6
|
+
running: false;
|
|
7
|
+
lastStartAt: null;
|
|
8
|
+
lastStopAt: null;
|
|
9
|
+
lastError: null;
|
|
10
|
+
} & import("./src/status.js").LoggieRuntimeExtra;
|
|
11
|
+
buildAccountSnapshot: ({ account, runtime, }: {
|
|
12
|
+
account: import("./src/account.js").ResolvedLoggieAccount;
|
|
13
|
+
runtime?: Record<string, unknown> | null;
|
|
14
|
+
}) => Record<string, unknown> & {
|
|
15
|
+
mode: string;
|
|
16
|
+
baseUrl: string;
|
|
17
|
+
agentId: string;
|
|
18
|
+
agentProfileId: string;
|
|
19
|
+
tokenStatus: "available" | "configured_unavailable" | "missing";
|
|
20
|
+
tokenSource: string;
|
|
21
|
+
connected: {};
|
|
22
|
+
authenticated: {};
|
|
23
|
+
caughtUp: {};
|
|
24
|
+
lastEventId: {} | null;
|
|
25
|
+
lastCursor: {} | null;
|
|
26
|
+
lastSequence: {} | null;
|
|
27
|
+
lastDispatchAt: {} | null;
|
|
28
|
+
lastDeadLetterAt: {} | null;
|
|
29
|
+
lastDeadLetteredEventId: {} | null;
|
|
30
|
+
lastDeadLetteredCursor: {} | null;
|
|
31
|
+
lastDeadLetterReason: {} | null;
|
|
32
|
+
lastDeadLetterAttempts: {} | null;
|
|
33
|
+
};
|
|
34
|
+
};
|
|
35
|
+
doctor: {
|
|
36
|
+
collectPreviewWarnings: (params: {
|
|
37
|
+
cfg: import("openclaw/plugin-sdk/channel-core").OpenClawConfig;
|
|
38
|
+
env?: NodeJS.ProcessEnv;
|
|
39
|
+
}) => string[];
|
|
40
|
+
};
|
|
41
|
+
secrets: {
|
|
42
|
+
secretTargetRegistryEntries: {
|
|
43
|
+
id: string;
|
|
44
|
+
targetType: string;
|
|
45
|
+
configFile: string;
|
|
46
|
+
pathPattern: string;
|
|
47
|
+
secretShape: string;
|
|
48
|
+
expectedResolvedValue: string;
|
|
49
|
+
includeInPlan: boolean;
|
|
50
|
+
includeInConfigure: boolean;
|
|
51
|
+
includeInAudit: boolean;
|
|
52
|
+
}[];
|
|
53
|
+
};
|
|
54
|
+
gateway: {
|
|
55
|
+
startAccount: (ctx: {
|
|
56
|
+
account: import("./src/account.js").ResolvedLoggieAccount;
|
|
57
|
+
cfg: import("openclaw/plugin-sdk/channel-core").OpenClawConfig;
|
|
58
|
+
runtime: unknown;
|
|
59
|
+
channelRuntime?: unknown;
|
|
60
|
+
abortSignal?: AbortSignal;
|
|
61
|
+
setStatus: (next: unknown) => void;
|
|
62
|
+
getStatus: () => unknown;
|
|
63
|
+
log?: {
|
|
64
|
+
info?: (message: string) => void;
|
|
65
|
+
warn?: (message: string) => void;
|
|
66
|
+
};
|
|
67
|
+
}) => Promise<void>;
|
|
68
|
+
};
|
|
69
|
+
id: string;
|
|
70
|
+
config?: Record<string, unknown>;
|
|
71
|
+
};
|
|
72
|
+
};
|
|
73
|
+
export default _default;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import type { OpenClawConfig } from "openclaw/plugin-sdk/channel-core";
|
|
2
|
+
export type LoggieAuthHeader = "authorization" | "x-api-key";
|
|
3
|
+
export type LoggieSecretStatus = {
|
|
4
|
+
status: "available";
|
|
5
|
+
source: string;
|
|
6
|
+
value: string;
|
|
7
|
+
} | {
|
|
8
|
+
status: "configured_unavailable";
|
|
9
|
+
source: string;
|
|
10
|
+
value?: undefined;
|
|
11
|
+
} | {
|
|
12
|
+
status: "missing";
|
|
13
|
+
source: "missing";
|
|
14
|
+
value?: undefined;
|
|
15
|
+
};
|
|
16
|
+
export type ResolvedLoggieAccount = {
|
|
17
|
+
accountId: string;
|
|
18
|
+
name?: string;
|
|
19
|
+
enabled: boolean;
|
|
20
|
+
configured: boolean;
|
|
21
|
+
baseUrl: string;
|
|
22
|
+
socketPath: string;
|
|
23
|
+
agentId: string;
|
|
24
|
+
agentProfileId: string;
|
|
25
|
+
authHeader: LoggieAuthHeader;
|
|
26
|
+
tokenStatus: LoggieSecretStatus;
|
|
27
|
+
reconnect: {
|
|
28
|
+
minMs: number;
|
|
29
|
+
maxMs: number;
|
|
30
|
+
};
|
|
31
|
+
heartbeat: {
|
|
32
|
+
timeoutMs: number;
|
|
33
|
+
};
|
|
34
|
+
transcript: {
|
|
35
|
+
activation: "final-only";
|
|
36
|
+
debounceMs: number;
|
|
37
|
+
};
|
|
38
|
+
};
|
|
39
|
+
export declare function listLoggieAccountIds(cfg: OpenClawConfig): string[];
|
|
40
|
+
export declare function resolveDefaultLoggieAccountId(cfg: OpenClawConfig): string;
|
|
41
|
+
export declare function resolveLoggieAccount(cfg: OpenClawConfig, accountId?: string | null, env?: NodeJS.ProcessEnv): ResolvedLoggieAccount;
|
|
42
|
+
export declare function inspectLoggieAccount(cfg: OpenClawConfig, accountId?: string | null): {
|
|
43
|
+
accountId: string;
|
|
44
|
+
name: string | undefined;
|
|
45
|
+
enabled: boolean;
|
|
46
|
+
configured: boolean;
|
|
47
|
+
baseUrl: string;
|
|
48
|
+
socketPath: string;
|
|
49
|
+
agentId: string;
|
|
50
|
+
agentProfileId: string;
|
|
51
|
+
authHeader: LoggieAuthHeader;
|
|
52
|
+
tokenStatus: "available" | "configured_unavailable" | "missing";
|
|
53
|
+
tokenSource: string;
|
|
54
|
+
reconnect: {
|
|
55
|
+
minMs: number;
|
|
56
|
+
maxMs: number;
|
|
57
|
+
};
|
|
58
|
+
heartbeat: {
|
|
59
|
+
timeoutMs: number;
|
|
60
|
+
};
|
|
61
|
+
transcript: {
|
|
62
|
+
activation: "final-only";
|
|
63
|
+
debounceMs: number;
|
|
64
|
+
};
|
|
65
|
+
};
|
|
66
|
+
export declare function applyLoggieAccountConfig(params: {
|
|
67
|
+
cfg: OpenClawConfig;
|
|
68
|
+
accountId: string;
|
|
69
|
+
input: Record<string, unknown>;
|
|
70
|
+
}): OpenClawConfig;
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import { DEFAULT_LOGGIE_ACCOUNT_ID, DEFAULT_LOGGIE_AGENT_ID, DEFAULT_LOGGIE_BASE_URL_ENV, DEFAULT_LOGGIE_HEARTBEAT_TIMEOUT_MS, DEFAULT_LOGGIE_SOCKET_PATH, DEFAULT_LOGGIE_TOKEN_ENV, } from "./config-schema.js";
|
|
2
|
+
import { isRecord, readBoolean, readInteger, readString } from "./object.js";
|
|
3
|
+
function getLoggieSection(cfg) {
|
|
4
|
+
const channels = isRecord(cfg.channels)
|
|
5
|
+
? cfg.channels
|
|
6
|
+
: {};
|
|
7
|
+
return isRecord(channels.loggie) ? channels.loggie : {};
|
|
8
|
+
}
|
|
9
|
+
function getAccounts(section) {
|
|
10
|
+
if (!isRecord(section.accounts)) {
|
|
11
|
+
return {};
|
|
12
|
+
}
|
|
13
|
+
const entries = {};
|
|
14
|
+
for (const [id, raw] of Object.entries(section.accounts)) {
|
|
15
|
+
if (isRecord(raw)) {
|
|
16
|
+
entries[id] = raw;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return entries;
|
|
20
|
+
}
|
|
21
|
+
function mergeAccountConfig(section, accountId) {
|
|
22
|
+
const { accounts: _accounts, ...topLevel } = section;
|
|
23
|
+
return {
|
|
24
|
+
...topLevel,
|
|
25
|
+
...(getAccounts(section)[accountId] ?? {}),
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
function normalizeAuthHeader(value) {
|
|
29
|
+
return value === "x-api-key" ? "x-api-key" : "authorization";
|
|
30
|
+
}
|
|
31
|
+
function readEnvSecretRefName(value) {
|
|
32
|
+
if (typeof value === "string") {
|
|
33
|
+
const trimmed = value.trim();
|
|
34
|
+
if (trimmed.length === 0) {
|
|
35
|
+
return undefined;
|
|
36
|
+
}
|
|
37
|
+
const envName = trimmed.startsWith("$")
|
|
38
|
+
? trimmed.replace(/^\$\{?([^}]+)\}?$/, "$1")
|
|
39
|
+
: undefined;
|
|
40
|
+
return envName && /^[A-Z][A-Z0-9_]{0,127}$/.test(envName) ? envName : undefined;
|
|
41
|
+
}
|
|
42
|
+
if (isRecord(value) &&
|
|
43
|
+
value.source === "env" &&
|
|
44
|
+
typeof value.id === "string" &&
|
|
45
|
+
value.id.trim().length > 0) {
|
|
46
|
+
return value.id.trim();
|
|
47
|
+
}
|
|
48
|
+
return undefined;
|
|
49
|
+
}
|
|
50
|
+
function readSecretRefLabel(value) {
|
|
51
|
+
if (isRecord(value) &&
|
|
52
|
+
typeof value.source === "string" &&
|
|
53
|
+
typeof value.provider === "string" &&
|
|
54
|
+
typeof value.id === "string") {
|
|
55
|
+
return `${value.source}:${value.provider}:${value.id}`;
|
|
56
|
+
}
|
|
57
|
+
return undefined;
|
|
58
|
+
}
|
|
59
|
+
function resolveToken(params) {
|
|
60
|
+
const literalToken = readString(params.raw.token);
|
|
61
|
+
if (literalToken) {
|
|
62
|
+
return { status: "available", source: "literal", value: literalToken };
|
|
63
|
+
}
|
|
64
|
+
if (params.raw.credentialRef === undefined) {
|
|
65
|
+
const value = readString(params.env[DEFAULT_LOGGIE_TOKEN_ENV]);
|
|
66
|
+
return value
|
|
67
|
+
? { status: "available", source: `env:${DEFAULT_LOGGIE_TOKEN_ENV}`, value }
|
|
68
|
+
: { status: "configured_unavailable", source: `env:${DEFAULT_LOGGIE_TOKEN_ENV}` };
|
|
69
|
+
}
|
|
70
|
+
const credentialRef = params.raw.credentialRef;
|
|
71
|
+
const envName = readEnvSecretRefName(credentialRef);
|
|
72
|
+
if (envName) {
|
|
73
|
+
const value = readString(params.env[envName]);
|
|
74
|
+
return value
|
|
75
|
+
? { status: "available", source: `env:${envName}`, value }
|
|
76
|
+
: { status: "configured_unavailable", source: `env:${envName}` };
|
|
77
|
+
}
|
|
78
|
+
if (typeof credentialRef === "string") {
|
|
79
|
+
const resolvedSecretValue = readString(credentialRef);
|
|
80
|
+
return resolvedSecretValue
|
|
81
|
+
? { status: "available", source: "credentialRef", value: resolvedSecretValue }
|
|
82
|
+
: { status: "missing", source: "missing" };
|
|
83
|
+
}
|
|
84
|
+
const secretRefLabel = readSecretRefLabel(credentialRef);
|
|
85
|
+
if (!secretRefLabel) {
|
|
86
|
+
return { status: "missing", source: "missing" };
|
|
87
|
+
}
|
|
88
|
+
return { status: "configured_unavailable", source: secretRefLabel };
|
|
89
|
+
}
|
|
90
|
+
function normalizeReconnect(raw) {
|
|
91
|
+
const reconnect = isRecord(raw.reconnect) ? raw.reconnect : {};
|
|
92
|
+
const minMs = Math.max(100, readInteger(reconnect.minMs, 1000));
|
|
93
|
+
const maxMs = Math.max(minMs, readInteger(reconnect.maxMs, 60_000));
|
|
94
|
+
return { minMs, maxMs };
|
|
95
|
+
}
|
|
96
|
+
function normalizeHeartbeat(raw) {
|
|
97
|
+
const heartbeat = isRecord(raw.heartbeat) ? raw.heartbeat : {};
|
|
98
|
+
return {
|
|
99
|
+
timeoutMs: Math.max(1000, readInteger(heartbeat.timeoutMs, DEFAULT_LOGGIE_HEARTBEAT_TIMEOUT_MS)),
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
function normalizeTranscript(raw) {
|
|
103
|
+
const transcript = isRecord(raw.transcript) ? raw.transcript : {};
|
|
104
|
+
return {
|
|
105
|
+
activation: "final-only",
|
|
106
|
+
debounceMs: Math.max(0, readInteger(transcript.debounceMs, 5000)),
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
export function listLoggieAccountIds(cfg) {
|
|
110
|
+
const section = getLoggieSection(cfg);
|
|
111
|
+
const accounts = Object.keys(getAccounts(section));
|
|
112
|
+
if (accounts.length > 0) {
|
|
113
|
+
return accounts;
|
|
114
|
+
}
|
|
115
|
+
return [readString(section.accountId) ?? DEFAULT_LOGGIE_ACCOUNT_ID];
|
|
116
|
+
}
|
|
117
|
+
export function resolveDefaultLoggieAccountId(cfg) {
|
|
118
|
+
const section = getLoggieSection(cfg);
|
|
119
|
+
return readString(section.accountId) ?? DEFAULT_LOGGIE_ACCOUNT_ID;
|
|
120
|
+
}
|
|
121
|
+
export function resolveLoggieAccount(cfg, accountId, env = process.env) {
|
|
122
|
+
const section = getLoggieSection(cfg);
|
|
123
|
+
const resolvedAccountId = accountId?.trim() || resolveDefaultLoggieAccountId(cfg);
|
|
124
|
+
const raw = mergeAccountConfig(section, resolvedAccountId);
|
|
125
|
+
const baseUrl = readString(raw.baseUrl) ?? readString(env[DEFAULT_LOGGIE_BASE_URL_ENV]) ?? "http://127.0.0.1:8787";
|
|
126
|
+
const agentProfileId = readString(raw.agentProfileId) ?? "";
|
|
127
|
+
const tokenStatus = resolveToken({ raw, env });
|
|
128
|
+
const enabled = readBoolean(raw.enabled, true);
|
|
129
|
+
const configured = Boolean(baseUrl && agentProfileId && tokenStatus.status === "available");
|
|
130
|
+
return {
|
|
131
|
+
accountId: resolvedAccountId,
|
|
132
|
+
name: readString(raw.name),
|
|
133
|
+
enabled,
|
|
134
|
+
configured,
|
|
135
|
+
baseUrl,
|
|
136
|
+
socketPath: readString(raw.socketPath) ?? DEFAULT_LOGGIE_SOCKET_PATH,
|
|
137
|
+
agentId: readString(raw.agentId) ?? DEFAULT_LOGGIE_AGENT_ID,
|
|
138
|
+
agentProfileId,
|
|
139
|
+
authHeader: normalizeAuthHeader(raw.authHeader),
|
|
140
|
+
tokenStatus,
|
|
141
|
+
reconnect: normalizeReconnect(raw),
|
|
142
|
+
heartbeat: normalizeHeartbeat(raw),
|
|
143
|
+
transcript: normalizeTranscript(raw),
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
export function inspectLoggieAccount(cfg, accountId) {
|
|
147
|
+
const account = resolveLoggieAccount(cfg, accountId);
|
|
148
|
+
return {
|
|
149
|
+
accountId: account.accountId,
|
|
150
|
+
name: account.name,
|
|
151
|
+
enabled: account.enabled,
|
|
152
|
+
configured: account.configured,
|
|
153
|
+
baseUrl: account.baseUrl,
|
|
154
|
+
socketPath: account.socketPath,
|
|
155
|
+
agentId: account.agentId,
|
|
156
|
+
agentProfileId: account.agentProfileId,
|
|
157
|
+
authHeader: account.authHeader,
|
|
158
|
+
tokenStatus: account.tokenStatus.status,
|
|
159
|
+
tokenSource: account.tokenStatus.source,
|
|
160
|
+
reconnect: account.reconnect,
|
|
161
|
+
heartbeat: account.heartbeat,
|
|
162
|
+
transcript: account.transcript,
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
export function applyLoggieAccountConfig(params) {
|
|
166
|
+
const next = structuredClone(params.cfg);
|
|
167
|
+
const channels = isRecord(next.channels) ? next.channels : {};
|
|
168
|
+
const loggie = isRecord(channels.loggie) ? channels.loggie : {};
|
|
169
|
+
channels.loggie = {
|
|
170
|
+
...loggie,
|
|
171
|
+
accountId: params.accountId,
|
|
172
|
+
accounts: {
|
|
173
|
+
...(isRecord(loggie.accounts) ? loggie.accounts : {}),
|
|
174
|
+
[params.accountId]: {
|
|
175
|
+
enabled: true,
|
|
176
|
+
...params.input,
|
|
177
|
+
},
|
|
178
|
+
},
|
|
179
|
+
};
|
|
180
|
+
next.channels = channels;
|
|
181
|
+
return next;
|
|
182
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { type OpenClawConfig } from "openclaw/plugin-sdk/channel-core";
|
|
2
|
+
import { type ResolvedLoggieAccount } from "./account.js";
|
|
3
|
+
export declare const loggiePlugin: {
|
|
4
|
+
status: {
|
|
5
|
+
defaultRuntime: {
|
|
6
|
+
accountId: string;
|
|
7
|
+
running: false;
|
|
8
|
+
lastStartAt: null;
|
|
9
|
+
lastStopAt: null;
|
|
10
|
+
lastError: null;
|
|
11
|
+
} & import("./status.js").LoggieRuntimeExtra;
|
|
12
|
+
buildAccountSnapshot: ({ account, runtime, }: {
|
|
13
|
+
account: ResolvedLoggieAccount;
|
|
14
|
+
runtime?: Record<string, unknown> | null;
|
|
15
|
+
}) => Record<string, unknown> & {
|
|
16
|
+
mode: string;
|
|
17
|
+
baseUrl: string;
|
|
18
|
+
agentId: string;
|
|
19
|
+
agentProfileId: string;
|
|
20
|
+
tokenStatus: "available" | "configured_unavailable" | "missing";
|
|
21
|
+
tokenSource: string;
|
|
22
|
+
connected: {};
|
|
23
|
+
authenticated: {};
|
|
24
|
+
caughtUp: {};
|
|
25
|
+
lastEventId: {} | null;
|
|
26
|
+
lastCursor: {} | null;
|
|
27
|
+
lastSequence: {} | null;
|
|
28
|
+
lastDispatchAt: {} | null;
|
|
29
|
+
lastDeadLetterAt: {} | null;
|
|
30
|
+
lastDeadLetteredEventId: {} | null;
|
|
31
|
+
lastDeadLetteredCursor: {} | null;
|
|
32
|
+
lastDeadLetterReason: {} | null;
|
|
33
|
+
lastDeadLetterAttempts: {} | null;
|
|
34
|
+
};
|
|
35
|
+
};
|
|
36
|
+
doctor: {
|
|
37
|
+
collectPreviewWarnings: (params: {
|
|
38
|
+
cfg: OpenClawConfig;
|
|
39
|
+
env?: NodeJS.ProcessEnv;
|
|
40
|
+
}) => string[];
|
|
41
|
+
};
|
|
42
|
+
secrets: {
|
|
43
|
+
secretTargetRegistryEntries: {
|
|
44
|
+
id: string;
|
|
45
|
+
targetType: string;
|
|
46
|
+
configFile: string;
|
|
47
|
+
pathPattern: string;
|
|
48
|
+
secretShape: string;
|
|
49
|
+
expectedResolvedValue: string;
|
|
50
|
+
includeInPlan: boolean;
|
|
51
|
+
includeInConfigure: boolean;
|
|
52
|
+
includeInAudit: boolean;
|
|
53
|
+
}[];
|
|
54
|
+
};
|
|
55
|
+
gateway: {
|
|
56
|
+
startAccount: (ctx: {
|
|
57
|
+
account: ResolvedLoggieAccount;
|
|
58
|
+
cfg: OpenClawConfig;
|
|
59
|
+
runtime: unknown;
|
|
60
|
+
channelRuntime?: unknown;
|
|
61
|
+
abortSignal?: AbortSignal;
|
|
62
|
+
setStatus: (next: unknown) => void;
|
|
63
|
+
getStatus: () => unknown;
|
|
64
|
+
log?: {
|
|
65
|
+
info?: (message: string) => void;
|
|
66
|
+
warn?: (message: string) => void;
|
|
67
|
+
};
|
|
68
|
+
}) => Promise<void>;
|
|
69
|
+
};
|
|
70
|
+
id: string;
|
|
71
|
+
config?: Record<string, unknown>;
|
|
72
|
+
};
|