@raidiant/notifai 11.7.1 → 12.0.0-beta.11
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 +53 -0
- package/NOTICE +16 -0
- package/THIRD_PARTY_NOTICES +725 -0
- package/bin/notifai.mjs +11420 -0
- package/data/install.ps1 +318 -0
- package/data/release.json +1 -0
- package/inventory.json +1 -0
- package/npm-adapter-files.json +52 -0
- package/package.json +14 -44
- package/README.md +0 -168
- package/dist/agent-update-notice.d.ts +0 -7
- package/dist/agent-update-notice.js +0 -77
- package/dist/agent-update-notice.js.map +0 -1
- package/dist/atomic-file.d.ts +0 -30
- package/dist/atomic-file.js +0 -148
- package/dist/atomic-file.js.map +0 -1
- package/dist/attendant-update.d.ts +0 -15
- package/dist/attendant-update.js +0 -96
- package/dist/attendant-update.js.map +0 -1
- package/dist/claude-wake.d.ts +0 -108
- package/dist/claude-wake.js +0 -369
- package/dist/claude-wake.js.map +0 -1
- package/dist/cli-bin.d.ts +0 -26
- package/dist/cli-bin.js +0 -220
- package/dist/cli-bin.js.map +0 -1
- package/dist/cli-contract.d.ts +0 -16
- package/dist/cli-contract.js +0 -27
- package/dist/cli-contract.js.map +0 -1
- package/dist/cli-release.d.ts +0 -36
- package/dist/cli-release.js +0 -85
- package/dist/cli-release.js.map +0 -1
- package/dist/client.d.ts +0 -109
- package/dist/client.js +0 -217
- package/dist/client.js.map +0 -1
- package/dist/codex-answer-control.d.ts +0 -14
- package/dist/codex-answer-control.js +0 -41
- package/dist/codex-answer-control.js.map +0 -1
- package/dist/codex-answer-presentation.d.ts +0 -18
- package/dist/codex-answer-presentation.js +0 -113
- package/dist/codex-answer-presentation.js.map +0 -1
- package/dist/codex-input-lifecycle.d.ts +0 -10
- package/dist/codex-input-lifecycle.js +0 -63
- package/dist/codex-input-lifecycle.js.map +0 -1
- package/dist/codex-native-control.d.ts +0 -13
- package/dist/codex-native-control.js +0 -120
- package/dist/codex-native-control.js.map +0 -1
- package/dist/codex-native-turn.d.ts +0 -37
- package/dist/codex-native-turn.js +0 -155
- package/dist/codex-native-turn.js.map +0 -1
- package/dist/codex-question-bindings.d.ts +0 -76
- package/dist/codex-question-bindings.js +0 -151
- package/dist/codex-question-bindings.js.map +0 -1
- package/dist/codex-queue-control.d.ts +0 -21
- package/dist/codex-queue-control.js +0 -59
- package/dist/codex-queue-control.js.map +0 -1
- package/dist/codex-tool-messages.d.ts +0 -7
- package/dist/codex-tool-messages.js +0 -126
- package/dist/codex-tool-messages.js.map +0 -1
- package/dist/codex-wake.d.ts +0 -58
- package/dist/codex-wake.js +0 -135
- package/dist/codex-wake.js.map +0 -1
- package/dist/command-session.d.ts +0 -14
- package/dist/command-session.js +0 -31
- package/dist/command-session.js.map +0 -1
- package/dist/commands-acknowledge.d.ts +0 -13
- package/dist/commands-acknowledge.js +0 -116
- package/dist/commands-acknowledge.js.map +0 -1
- package/dist/commands-agent-sessions.d.ts +0 -6
- package/dist/commands-agent-sessions.js +0 -51
- package/dist/commands-agent-sessions.js.map +0 -1
- package/dist/commands-ask.d.ts +0 -62
- package/dist/commands-ask.js +0 -503
- package/dist/commands-ask.js.map +0 -1
- package/dist/commands-auth.d.ts +0 -76
- package/dist/commands-auth.js +0 -421
- package/dist/commands-auth.js.map +0 -1
- package/dist/commands-close.d.ts +0 -6
- package/dist/commands-close.js +0 -273
- package/dist/commands-close.js.map +0 -1
- package/dist/commands-config.d.ts +0 -31
- package/dist/commands-config.js +0 -272
- package/dist/commands-config.js.map +0 -1
- package/dist/commands-core.d.ts +0 -145
- package/dist/commands-core.js +0 -190
- package/dist/commands-core.js.map +0 -1
- package/dist/commands-devices.d.ts +0 -41
- package/dist/commands-devices.js +0 -144
- package/dist/commands-devices.js.map +0 -1
- package/dist/commands-doctor.d.ts +0 -25
- package/dist/commands-doctor.js +0 -1553
- package/dist/commands-doctor.js.map +0 -1
- package/dist/commands-guidance.d.ts +0 -27
- package/dist/commands-guidance.js +0 -150
- package/dist/commands-guidance.js.map +0 -1
- package/dist/commands-harness-context.d.ts +0 -57
- package/dist/commands-harness-context.js +0 -102
- package/dist/commands-harness-context.js.map +0 -1
- package/dist/commands-hook-attend.d.ts +0 -70
- package/dist/commands-hook-attend.js +0 -655
- package/dist/commands-hook-attend.js.map +0 -1
- package/dist/commands-hook-diagnostics.d.ts +0 -37
- package/dist/commands-hook-diagnostics.js +0 -136
- package/dist/commands-hook-diagnostics.js.map +0 -1
- package/dist/commands-hook-install.d.ts +0 -26
- package/dist/commands-hook-install.js +0 -784
- package/dist/commands-hook-install.js.map +0 -1
- package/dist/commands-hook-run.d.ts +0 -29
- package/dist/commands-hook-run.js +0 -933
- package/dist/commands-hook-run.js.map +0 -1
- package/dist/commands-hook-shape.d.ts +0 -19
- package/dist/commands-hook-shape.js +0 -48
- package/dist/commands-hook-shape.js.map +0 -1
- package/dist/commands-init.d.ts +0 -42
- package/dist/commands-init.js +0 -901
- package/dist/commands-init.js.map +0 -1
- package/dist/commands-io.d.ts +0 -2
- package/dist/commands-io.js +0 -117
- package/dist/commands-io.js.map +0 -1
- package/dist/commands-logs.d.ts +0 -30
- package/dist/commands-logs.js +0 -201
- package/dist/commands-logs.js.map +0 -1
- package/dist/commands-native-acknowledge.d.ts +0 -8
- package/dist/commands-native-acknowledge.js +0 -114
- package/dist/commands-native-acknowledge.js.map +0 -1
- package/dist/commands-project.d.ts +0 -4
- package/dist/commands-project.js +0 -41
- package/dist/commands-project.js.map +0 -1
- package/dist/commands-receive.d.ts +0 -3
- package/dist/commands-receive.js +0 -59
- package/dist/commands-receive.js.map +0 -1
- package/dist/commands-send-support.d.ts +0 -59
- package/dist/commands-send-support.js +0 -258
- package/dist/commands-send-support.js.map +0 -1
- package/dist/commands-send.d.ts +0 -34
- package/dist/commands-send.js +0 -900
- package/dist/commands-send.js.map +0 -1
- package/dist/commands-setup-proof.d.ts +0 -43
- package/dist/commands-setup-proof.js +0 -134
- package/dist/commands-setup-proof.js.map +0 -1
- package/dist/commands-skill.d.ts +0 -21
- package/dist/commands-skill.js +0 -199
- package/dist/commands-skill.js.map +0 -1
- package/dist/commands-sounds.d.ts +0 -12
- package/dist/commands-sounds.js +0 -35
- package/dist/commands-sounds.js.map +0 -1
- package/dist/commands-update-check.d.ts +0 -6
- package/dist/commands-update-check.js +0 -102
- package/dist/commands-update-check.js.map +0 -1
- package/dist/commands-update-resume.d.ts +0 -7
- package/dist/commands-update-resume.js +0 -143
- package/dist/commands-update-resume.js.map +0 -1
- package/dist/commands-update-skill.d.ts +0 -5
- package/dist/commands-update-skill.js +0 -36
- package/dist/commands-update-skill.js.map +0 -1
- package/dist/commands-update.d.ts +0 -11
- package/dist/commands-update.js +0 -286
- package/dist/commands-update.js.map +0 -1
- package/dist/commands.d.ts +0 -26
- package/dist/commands.js +0 -27
- package/dist/commands.js.map +0 -1
- package/dist/config-schema.d.ts +0 -62
- package/dist/config-schema.js +0 -302
- package/dist/config-schema.js.map +0 -1
- package/dist/config.d.ts +0 -181
- package/dist/config.js +0 -357
- package/dist/config.js.map +0 -1
- package/dist/credentials.d.ts +0 -96
- package/dist/credentials.js +0 -342
- package/dist/credentials.js.map +0 -1
- package/dist/file-lock.d.ts +0 -34
- package/dist/file-lock.js +0 -367
- package/dist/file-lock.js.map +0 -1
- package/dist/generated-session-label.d.ts +0 -2
- package/dist/generated-session-label.js +0 -152
- package/dist/generated-session-label.js.map +0 -1
- package/dist/guidance-content.d.ts +0 -53
- package/dist/guidance-content.js +0 -248
- package/dist/guidance-content.js.map +0 -1
- package/dist/guidance-render.d.ts +0 -25
- package/dist/guidance-render.js +0 -47
- package/dist/guidance-render.js.map +0 -1
- package/dist/guidance.d.ts +0 -69
- package/dist/guidance.js +0 -173
- package/dist/guidance.js.map +0 -1
- package/dist/harness-session-title.d.ts +0 -22
- package/dist/harness-session-title.js +0 -74
- package/dist/harness-session-title.js.map +0 -1
- package/dist/harnesses.d.ts +0 -65
- package/dist/harnesses.js +0 -159
- package/dist/harnesses.js.map +0 -1
- package/dist/hermes-attendant.d.ts +0 -38
- package/dist/hermes-attendant.js +0 -337
- package/dist/hermes-attendant.js.map +0 -1
- package/dist/hermes-plugin.d.ts +0 -11
- package/dist/hermes-plugin.js +0 -299
- package/dist/hermes-plugin.js.map +0 -1
- package/dist/hook-acknowledgements.d.ts +0 -70
- package/dist/hook-acknowledgements.js +0 -490
- package/dist/hook-acknowledgements.js.map +0 -1
- package/dist/hook-adapter.d.ts +0 -48
- package/dist/hook-adapter.js +0 -419
- package/dist/hook-adapter.js.map +0 -1
- package/dist/hook-events.d.ts +0 -105
- package/dist/hook-events.js +0 -130
- package/dist/hook-events.js.map +0 -1
- package/dist/hook-gates.d.ts +0 -7
- package/dist/hook-gates.js +0 -20
- package/dist/hook-gates.js.map +0 -1
- package/dist/hook-input.d.ts +0 -10
- package/dist/hook-input.js +0 -64
- package/dist/hook-input.js.map +0 -1
- package/dist/hook-lifecycle.d.ts +0 -112
- package/dist/hook-lifecycle.js +0 -2031
- package/dist/hook-lifecycle.js.map +0 -1
- package/dist/hook-project-sessions.d.ts +0 -28
- package/dist/hook-project-sessions.js +0 -141
- package/dist/hook-project-sessions.js.map +0 -1
- package/dist/hook-question-lock.d.ts +0 -29
- package/dist/hook-question-lock.js +0 -217
- package/dist/hook-question-lock.js.map +0 -1
- package/dist/hook-question-retirement.d.ts +0 -74
- package/dist/hook-question-retirement.js +0 -314
- package/dist/hook-question-retirement.js.map +0 -1
- package/dist/hook-question-state.d.ts +0 -58
- package/dist/hook-question-state.js +0 -239
- package/dist/hook-question-state.js.map +0 -1
- package/dist/hook-session-state.d.ts +0 -127
- package/dist/hook-session-state.js +0 -488
- package/dist/hook-session-state.js.map +0 -1
- package/dist/hook-types.d.ts +0 -476
- package/dist/hook-types.js +0 -4
- package/dist/hook-types.js.map +0 -1
- package/dist/injection-render.d.ts +0 -36
- package/dist/injection-render.js +0 -61
- package/dist/injection-render.js.map +0 -1
- package/dist/install-hooks.d.ts +0 -470
- package/dist/install-hooks.js +0 -1622
- package/dist/install-hooks.js.map +0 -1
- package/dist/integration-health.d.ts +0 -26
- package/dist/integration-health.js +0 -133
- package/dist/integration-health.js.map +0 -1
- package/dist/interactive.d.ts +0 -22
- package/dist/interactive.js +0 -644
- package/dist/interactive.js.map +0 -1
- package/dist/invocation-context.d.ts +0 -47
- package/dist/invocation-context.js +0 -164
- package/dist/invocation-context.js.map +0 -1
- package/dist/local-path.d.ts +0 -7
- package/dist/local-path.js +0 -27
- package/dist/local-path.js.map +0 -1
- package/dist/logging.d.ts +0 -157
- package/dist/logging.js +0 -599
- package/dist/logging.js.map +0 -1
- package/dist/main-run.d.ts +0 -1
- package/dist/main-run.js +0 -42
- package/dist/main-run.js.map +0 -1
- package/dist/main.d.ts +0 -2
- package/dist/main.js +0 -12
- package/dist/main.js.map +0 -1
- package/dist/native-answer-operation.d.ts +0 -70
- package/dist/native-answer-operation.js +0 -247
- package/dist/native-answer-operation.js.map +0 -1
- package/dist/native-skills.d.ts +0 -64
- package/dist/native-skills.js +0 -167
- package/dist/native-skills.js.map +0 -1
- package/dist/node-floor.d.ts +0 -19
- package/dist/node-floor.js +0 -32
- package/dist/node-floor.js.map +0 -1
- package/dist/npm-invocation.d.ts +0 -34
- package/dist/npm-invocation.js +0 -53
- package/dist/npm-invocation.js.map +0 -1
- package/dist/openclaw-continuation-bridge.d.ts +0 -7
- package/dist/openclaw-continuation-bridge.js +0 -64
- package/dist/openclaw-continuation-bridge.js.map +0 -1
- package/dist/openclaw-gateway-readiness.d.ts +0 -1
- package/dist/openclaw-gateway-readiness.js +0 -24
- package/dist/openclaw-gateway-readiness.js.map +0 -1
- package/dist/openclaw-generation.d.ts +0 -21
- package/dist/openclaw-generation.js +0 -115
- package/dist/openclaw-generation.js.map +0 -1
- package/dist/openclaw-message-bridge.d.ts +0 -8
- package/dist/openclaw-message-bridge.js +0 -93
- package/dist/openclaw-message-bridge.js.map +0 -1
- package/dist/openclaw-pending.d.ts +0 -8
- package/dist/openclaw-pending.js +0 -47
- package/dist/openclaw-pending.js.map +0 -1
- package/dist/openclaw-plugin.d.ts +0 -75
- package/dist/openclaw-plugin.js +0 -1353
- package/dist/openclaw-plugin.js.map +0 -1
- package/dist/openclaw-session-access.d.ts +0 -12
- package/dist/openclaw-session-access.js +0 -79
- package/dist/openclaw-session-access.js.map +0 -1
- package/dist/opencode-plugin.d.ts +0 -88
- package/dist/opencode-plugin.js +0 -286
- package/dist/opencode-plugin.js.map +0 -1
- package/dist/orca-session-title.d.ts +0 -11
- package/dist/orca-session-title.js +0 -111
- package/dist/orca-session-title.js.map +0 -1
- package/dist/pack-feedback-slice.d.ts +0 -35
- package/dist/pack-feedback-slice.js +0 -126
- package/dist/pack-feedback-slice.js.map +0 -1
- package/dist/pairing-qr.d.ts +0 -5
- package/dist/pairing-qr.js +0 -24
- package/dist/pairing-qr.js.map +0 -1
- package/dist/pending-pairing.d.ts +0 -38
- package/dist/pending-pairing.js +0 -70
- package/dist/pending-pairing.js.map +0 -1
- package/dist/platform.d.ts +0 -67
- package/dist/platform.js +0 -140
- package/dist/platform.js.map +0 -1
- package/dist/process-identity.d.ts +0 -26
- package/dist/process-identity.js +0 -82
- package/dist/process-identity.js.map +0 -1
- package/dist/program.d.ts +0 -59
- package/dist/program.js +0 -691
- package/dist/program.js.map +0 -1
- package/dist/project-enablement.d.ts +0 -21
- package/dist/project-enablement.js +0 -71
- package/dist/project-enablement.js.map +0 -1
- package/dist/question-settlement-process.d.ts +0 -17
- package/dist/question-settlement-process.js +0 -35
- package/dist/question-settlement-process.js.map +0 -1
- package/dist/question-timing.d.ts +0 -46
- package/dist/question-timing.js +0 -48
- package/dist/question-timing.js.map +0 -1
- package/dist/readiness.d.ts +0 -153
- package/dist/readiness.js +0 -149
- package/dist/readiness.js.map +0 -1
- package/dist/release.d.ts +0 -31
- package/dist/release.js +0 -52
- package/dist/release.js.map +0 -1
- package/dist/send-attempts.d.ts +0 -26
- package/dist/send-attempts.js +0 -139
- package/dist/send-attempts.js.map +0 -1
- package/dist/send.d.ts +0 -115
- package/dist/send.js +0 -421
- package/dist/send.js.map +0 -1
- package/dist/session-activation.d.ts +0 -13
- package/dist/session-activation.js +0 -64
- package/dist/session-activation.js.map +0 -1
- package/dist/session-attendant-probe.d.ts +0 -85
- package/dist/session-attendant-probe.js +0 -173
- package/dist/session-attendant-probe.js.map +0 -1
- package/dist/session-attendant-state.d.ts +0 -70
- package/dist/session-attendant-state.js +0 -256
- package/dist/session-attendant-state.js.map +0 -1
- package/dist/session-attendant.d.ts +0 -143
- package/dist/session-attendant.js +0 -520
- package/dist/session-attendant.js.map +0 -1
- package/dist/session-delivery.d.ts +0 -249
- package/dist/session-delivery.js +0 -576
- package/dist/session-delivery.js.map +0 -1
- package/dist/session-handoff.d.ts +0 -98
- package/dist/session-handoff.js +0 -118
- package/dist/session-handoff.js.map +0 -1
- package/dist/session-input-wakes.d.ts +0 -78
- package/dist/session-input-wakes.js +0 -239
- package/dist/session-input-wakes.js.map +0 -1
- package/dist/session-inputs.d.ts +0 -57
- package/dist/session-inputs.js +0 -434
- package/dist/session-inputs.js.map +0 -1
- package/dist/session-labels.d.ts +0 -56
- package/dist/session-labels.js +0 -396
- package/dist/session-labels.js.map +0 -1
- package/dist/session-message-handoff.d.ts +0 -39
- package/dist/session-message-handoff.js +0 -107
- package/dist/session-message-handoff.js.map +0 -1
- package/dist/setup-destinations.d.ts +0 -23
- package/dist/setup-destinations.js +0 -44
- package/dist/setup-destinations.js.map +0 -1
- package/dist/skill-integrity.d.ts +0 -54
- package/dist/skill-integrity.js +0 -164
- package/dist/skill-integrity.js.map +0 -1
- package/dist/skill-source/manifest.json +0 -40
- package/dist/skill-source/notifai/SKILL.md +0 -350
- package/dist/skill-source/notifai/references/diagnostics.md +0 -36
- package/dist/skill-source/notifai/references/harness-setup.md +0 -384
- package/dist/skill-source/notifai/references/native-questions.md +0 -51
- package/dist/skill-source/notifai/references/notes-and-edits.md +0 -54
- package/dist/skill-source/notifai/references/send-details.md +0 -14
- package/dist/skill-source/notifai/references/updates.md +0 -116
- package/dist/skill-source/notifai/references/writing-examples.md +0 -56
- package/dist/sound-ref.d.ts +0 -23
- package/dist/sound-ref.js +0 -28
- package/dist/sound-ref.js.map +0 -1
- package/dist/ui/banner.d.ts +0 -22
- package/dist/ui/banner.js +0 -87
- package/dist/ui/banner.js.map +0 -1
- package/dist/ui/config-view.d.ts +0 -30
- package/dist/ui/config-view.js +0 -108
- package/dist/ui/config-view.js.map +0 -1
- package/dist/ui/help.d.ts +0 -44
- package/dist/ui/help.js +0 -107
- package/dist/ui/help.js.map +0 -1
- package/dist/ui/theme.d.ts +0 -90
- package/dist/ui/theme.js +0 -244
- package/dist/ui/theme.js.map +0 -1
- package/dist/update-handoff.d.ts +0 -29
- package/dist/update-handoff.js +0 -69
- package/dist/update-handoff.js.map +0 -1
- package/dist/url-policy.d.ts +0 -85
- package/dist/url-policy.js +0 -369
- package/dist/url-policy.js.map +0 -1
- package/dist/version.d.ts +0 -40
- package/dist/version.js +0 -132
- package/dist/version.js.map +0 -1
- package/dist/wake-support.d.ts +0 -40
- package/dist/wake-support.js +0 -95
- package/dist/wake-support.js.map +0 -1
package/dist/openclaw-plugin.js
DELETED
|
@@ -1,1353 +0,0 @@
|
|
|
1
|
-
import path from 'node:path';
|
|
2
|
-
import { applyEdits, createScanner, findNodeAtLocation, modify, parse, parseTree, printParseErrorCode, } from 'jsonc-parser';
|
|
3
|
-
import { hookHostPlatform } from './hook-adapter.js';
|
|
4
|
-
import { harnessAccountHome } from './install-hooks.js';
|
|
5
|
-
import { MISSING_LIFECYCLE_GUIDANCE_CONTEXT, WORKER_ACTIVATION_CONTEXT, } from './session-activation.js';
|
|
6
|
-
/**
|
|
7
|
-
* The OpenClaw adapter.
|
|
8
|
-
*
|
|
9
|
-
* OpenClaw's extension point is an in-process Gateway plugin, not a command
|
|
10
|
-
* hook. The generated module shells out to the same `notifai hook <event>`
|
|
11
|
-
* commands every other harness invokes, so presence, activation, and
|
|
12
|
-
* retirement stay in the CLI.
|
|
13
|
-
*
|
|
14
|
-
* session_start -> openclaw-lifecycle (generation observation)
|
|
15
|
-
* before_reset -> openclaw-lifecycle (retire the old generation)
|
|
16
|
-
* before_prompt_build -> session-start / subagent-start (context once per generation)
|
|
17
|
-
* message_received -> user-prompt-submit (User is present)
|
|
18
|
-
* agent_end -> stop (the turn ended)
|
|
19
|
-
* session_end -> session-end (retire local state)
|
|
20
|
-
* resolve_exec_env -> NOTIFAI_ACTIVE_* markers (exact Source Context)
|
|
21
|
-
*
|
|
22
|
-
* `command:stop` is a User abort and is deliberately not wired. The Gateway
|
|
23
|
-
* service owns answer continuation after agent_end records its turn boundary.
|
|
24
|
-
*/
|
|
25
|
-
export const OPENCLAW_PLUGIN_MARKER = '// notifai managed openclaw plugin';
|
|
26
|
-
export const OPENCLAW_PLUGIN_ID = 'notifai';
|
|
27
|
-
export const OPENCLAW_PLUGIN_FILENAME = 'index.js';
|
|
28
|
-
export const OPENCLAW_PLUGIN_MANIFEST = 'openclaw.plugin.json';
|
|
29
|
-
export const OPENCLAW_PLUGIN_PACKAGE = 'package.json';
|
|
30
|
-
const OPENCLAW_ADAPTER_VERSION = 5;
|
|
31
|
-
export function openclawStateDir(env = process.env, platform = process.platform) {
|
|
32
|
-
const override = env['OPENCLAW_STATE_DIR'];
|
|
33
|
-
if (override !== undefined && override !== '')
|
|
34
|
-
return override;
|
|
35
|
-
const homeOverride = env['OPENCLAW_HOME'];
|
|
36
|
-
if (homeOverride !== undefined && homeOverride !== '') {
|
|
37
|
-
return path.join(homeOverride, '.openclaw');
|
|
38
|
-
}
|
|
39
|
-
return path.join(harnessAccountHome(env, platform), '.openclaw');
|
|
40
|
-
}
|
|
41
|
-
export function openclawConfigPath(env = process.env, platform = process.platform) {
|
|
42
|
-
const override = env['OPENCLAW_CONFIG_PATH'];
|
|
43
|
-
if (override !== undefined && override !== '')
|
|
44
|
-
return override;
|
|
45
|
-
return path.join(openclawStateDir(env, platform), 'openclaw.json');
|
|
46
|
-
}
|
|
47
|
-
export function openclawPluginDir(env = process.env, platform = process.platform) {
|
|
48
|
-
return path.join(openclawStateDir(env, platform), 'extensions', OPENCLAW_PLUGIN_ID);
|
|
49
|
-
}
|
|
50
|
-
export function openclawPluginPath(env = process.env, platform = process.platform) {
|
|
51
|
-
return path.join(openclawPluginDir(env, platform), OPENCLAW_PLUGIN_FILENAME);
|
|
52
|
-
}
|
|
53
|
-
/** A Project-scoped plugin directory an older build wrote; removal only. */
|
|
54
|
-
export function legacyOpenclawProjectPluginDir(cwd) {
|
|
55
|
-
return path.join(cwd, '.openclaw', 'extensions', OPENCLAW_PLUGIN_ID);
|
|
56
|
-
}
|
|
57
|
-
export function legacyOpenclawProjectPluginPath(cwd) {
|
|
58
|
-
return path.join(legacyOpenclawProjectPluginDir(cwd), OPENCLAW_PLUGIN_FILENAME);
|
|
59
|
-
}
|
|
60
|
-
/** Gateway-owned answer pointer delivery. The generated module stays standalone. */
|
|
61
|
-
function openclawContinuationServiceSource() {
|
|
62
|
-
return `
|
|
63
|
-
let continuationService = null
|
|
64
|
-
let JOURNAL_DIR = null
|
|
65
|
-
let MESSAGE_JOURNAL_DIR = null
|
|
66
|
-
let pendingMessageJournals = null
|
|
67
|
-
let awaitingMessageContexts = new Map()
|
|
68
|
-
const GATEWAY_BOOT_ID = randomUUID()
|
|
69
|
-
|
|
70
|
-
function readinessPath() {
|
|
71
|
-
return path.join(path.dirname(JOURNAL_DIR), "continuation-ready.json")
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
function writeReadiness(target) {
|
|
75
|
-
const start = execFileSync("ps", ["-o", "lstart=", "-p", String(process.pid)], {
|
|
76
|
-
encoding: "utf8", timeout: 2_000,
|
|
77
|
-
env: { PATH: process.env.PATH ?? "/bin:/usr/bin", TZ: "UTC", LC_ALL: "C" },
|
|
78
|
-
stdio: ["ignore", "pipe", "ignore"],
|
|
79
|
-
}).trim().replace(/\\s+/g, " ")
|
|
80
|
-
if (start === "") return false
|
|
81
|
-
const file = readinessPath()
|
|
82
|
-
mkdirSync(path.dirname(file), { recursive: true, mode: 0o700 })
|
|
83
|
-
const temp = file + "." + randomUUID() + ".tmp"
|
|
84
|
-
writeFileSync(temp, JSON.stringify({ pid: process.pid, start, boot_id: GATEWAY_BOOT_ID,
|
|
85
|
-
script: target.script, script_mtime: statSync(target.script).mtimeMs }) + "\\n",
|
|
86
|
-
{ mode: 0o600, flag: "wx" })
|
|
87
|
-
renameSync(temp, file)
|
|
88
|
-
return true
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
function clearReadiness() {
|
|
92
|
-
try { unlinkSync(readinessPath()) } catch { /* Already absent. */ }
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
function journalPath(deliveryId) {
|
|
96
|
-
return path.join(JOURNAL_DIR, deliveryId + ".json")
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
function saveJournal(record) {
|
|
100
|
-
mkdirSync(JOURNAL_DIR, { recursive: true, mode: 0o700 })
|
|
101
|
-
const file = journalPath(record.delivery_id)
|
|
102
|
-
const temp = file + "." + randomUUID() + ".tmp"
|
|
103
|
-
writeFileSync(temp, JSON.stringify(record) + "\\n", { mode: 0o600, flag: "wx" })
|
|
104
|
-
const fd = openSync(temp, "r")
|
|
105
|
-
try { fsyncSync(fd) } finally { closeSync(fd) }
|
|
106
|
-
renameSync(temp, file)
|
|
107
|
-
try {
|
|
108
|
-
const directory = openSync(JOURNAL_DIR, "r")
|
|
109
|
-
try { fsyncSync(directory) } finally { closeSync(directory) }
|
|
110
|
-
} catch { /* Directory fsync is unavailable on some hosts. */ }
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
function readJournals() {
|
|
114
|
-
try {
|
|
115
|
-
return readdirSync(JOURNAL_DIR).filter((name) => name.endsWith(".json"))
|
|
116
|
-
.flatMap((name) => {
|
|
117
|
-
try {
|
|
118
|
-
const record = JSON.parse(readFileSync(path.join(JOURNAL_DIR, name), "utf8"))
|
|
119
|
-
return record && typeof record.delivery_id === "string" ? [record] : []
|
|
120
|
-
} catch { return [] }
|
|
121
|
-
})
|
|
122
|
-
} catch { return [] }
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
function existingJournal(deliveryId) {
|
|
126
|
-
try {
|
|
127
|
-
return JSON.parse(readFileSync(journalPath(deliveryId), "utf8"))
|
|
128
|
-
} catch { return null }
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
function messageJournalPath(deliveryId) {
|
|
132
|
-
return path.join(MESSAGE_JOURNAL_DIR, deliveryId + ".json")
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
function messageContextMarkerPath(deliveryId) {
|
|
136
|
-
return path.join(MESSAGE_JOURNAL_DIR, deliveryId + ".context-used")
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
function saveMessageJournal(record) {
|
|
140
|
-
mkdirSync(MESSAGE_JOURNAL_DIR, { recursive: true, mode: 0o700 })
|
|
141
|
-
const file = messageJournalPath(record.delivery_id)
|
|
142
|
-
const temp = file + "." + randomUUID() + ".tmp"
|
|
143
|
-
const { text: _text, ...withoutText } = record
|
|
144
|
-
const stored = existsSync(messageContextMarkerPath(record.delivery_id)) ? withoutText : record
|
|
145
|
-
writeFileSync(temp, JSON.stringify(stored) + "\\n", { mode: 0o600, flag: "wx" })
|
|
146
|
-
const fd = openSync(temp, "r")
|
|
147
|
-
try { fsyncSync(fd) } finally { closeSync(fd) }
|
|
148
|
-
renameSync(temp, file)
|
|
149
|
-
if (pendingMessageJournals !== null) {
|
|
150
|
-
if (stored.phase === "transcript" || stored.phase === "unconfirmed") pendingMessageJournals.delete(stored.delivery_id)
|
|
151
|
-
else pendingMessageJournals.set(stored.delivery_id, stored)
|
|
152
|
-
}
|
|
153
|
-
if (stored.phase === "transcript" && stored.text !== undefined) {
|
|
154
|
-
awaitingMessageContexts.set(stored.delivery_id, stored)
|
|
155
|
-
} else {
|
|
156
|
-
awaitingMessageContexts.delete(stored.delivery_id)
|
|
157
|
-
}
|
|
158
|
-
try {
|
|
159
|
-
const directory = openSync(MESSAGE_JOURNAL_DIR, "r")
|
|
160
|
-
try { fsyncSync(directory) } finally { closeSync(directory) }
|
|
161
|
-
} catch { /* Directory fsync is unavailable on some hosts. */ }
|
|
162
|
-
// The prompt hook runs in another plugin process. If it consumed the context
|
|
163
|
-
// during this synchronous save, its marker wins and text is redacted again.
|
|
164
|
-
if (stored.text !== undefined && existsSync(messageContextMarkerPath(record.delivery_id))) {
|
|
165
|
-
saveMessageJournal(withoutText)
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
function readMessageJournal(deliveryId) {
|
|
170
|
-
try {
|
|
171
|
-
const record = JSON.parse(readFileSync(messageJournalPath(deliveryId), "utf8"))
|
|
172
|
-
if (!record || record.delivery_id !== deliveryId) throw new Error("Invalid message journal")
|
|
173
|
-
return record
|
|
174
|
-
} catch (error) {
|
|
175
|
-
if (error?.code === "ENOENT") return null
|
|
176
|
-
throw error
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
function retryMessageJournals() {
|
|
181
|
-
if (pendingMessageJournals === null) {
|
|
182
|
-
let names
|
|
183
|
-
try { names = readdirSync(MESSAGE_JOURNAL_DIR) }
|
|
184
|
-
catch (error) { if (error?.code !== "ENOENT") throw error; names = [] }
|
|
185
|
-
const pending = new Map()
|
|
186
|
-
awaitingMessageContexts = new Map()
|
|
187
|
-
for (const name of names.filter((value) => value.endsWith(".json"))) {
|
|
188
|
-
const record = readMessageJournal(name.slice(0, -5))
|
|
189
|
-
if (record !== null && record.boot_id !== GATEWAY_BOOT_ID) {
|
|
190
|
-
if (record.text !== undefined) {
|
|
191
|
-
settleMessageJournal(record, record.phase === "transcript" ? "transcript" : "unconfirmed")
|
|
192
|
-
}
|
|
193
|
-
continue
|
|
194
|
-
}
|
|
195
|
-
if (record !== null && record.phase !== "transcript" && record.phase !== "unconfirmed") {
|
|
196
|
-
pending.set(record.delivery_id, record)
|
|
197
|
-
} else if (record !== null && record.phase === "transcript" &&
|
|
198
|
-
record.text !== undefined) {
|
|
199
|
-
awaitingMessageContexts.set(record.delivery_id, record)
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
pendingMessageJournals = pending
|
|
203
|
-
}
|
|
204
|
-
return [...pendingMessageJournals.values()]
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
function settleMessageJournal(record, phase) {
|
|
208
|
-
const { text: _text, ...rest } = record
|
|
209
|
-
// A committed pointer can begin its model turn after chat.history sees the
|
|
210
|
-
// user transcript. Keep context until that exact turn claims it once.
|
|
211
|
-
saveMessageJournal(phase === "transcript" && record.boot_id === GATEWAY_BOOT_ID &&
|
|
212
|
-
!existsSync(messageContextMarkerPath(record.delivery_id))
|
|
213
|
-
? { ...record, phase } : { ...rest, phase })
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
function nativeSessionRevision(api, sessionKey, sessionId) {
|
|
217
|
-
if (typeof sessionId !== "string" || sessionId === "") return null
|
|
218
|
-
try {
|
|
219
|
-
const entry = api.runtime?.agent?.session?.getSessionEntry?.({
|
|
220
|
-
sessionKey, readConsistency: "latest",
|
|
221
|
-
})
|
|
222
|
-
return entry?.sessionId === sessionId &&
|
|
223
|
-
typeof entry.lifecycleRevision === "string" && entry.lifecycleRevision !== ""
|
|
224
|
-
? entry.lifecycleRevision : null
|
|
225
|
-
} catch { return null }
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
function claimTimeAvailable(record) {
|
|
229
|
-
return typeof record.deadline_ns === "string" &&
|
|
230
|
-
record.deadline_ns.length <= 32 && /^[0-9]+$/.test(record.deadline_ns) &&
|
|
231
|
-
BigInt(record.deadline_ns) > process.hrtime.bigint()
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
function openclawCliTarget(config) {
|
|
235
|
-
if (process.env.OPENCLAW_SHELL === "exec") return null
|
|
236
|
-
const gateway = config?.gateway ?? {}
|
|
237
|
-
if (gateway.mode === "remote" || (gateway.bind !== undefined && gateway.bind !== "loopback")) return null
|
|
238
|
-
let script
|
|
239
|
-
try { script = realpathSync(process.argv[1]) } catch { return null }
|
|
240
|
-
let folder = path.dirname(script)
|
|
241
|
-
let packageVersion = null
|
|
242
|
-
for (;;) {
|
|
243
|
-
try {
|
|
244
|
-
const manifest = JSON.parse(readFileSync(path.join(folder, "package.json"), "utf8"))
|
|
245
|
-
if (manifest.name === "openclaw" && typeof manifest.version === "string") {
|
|
246
|
-
packageVersion = manifest.version
|
|
247
|
-
break
|
|
248
|
-
}
|
|
249
|
-
} catch { /* Keep walking within this executable's ancestors. */ }
|
|
250
|
-
const parent = path.dirname(folder)
|
|
251
|
-
if (parent === folder) return null
|
|
252
|
-
folder = parent
|
|
253
|
-
}
|
|
254
|
-
const explicit = []
|
|
255
|
-
for (let i = 0; i < process.argv.length - 1; i += 1) {
|
|
256
|
-
if (process.argv[i] === "--port") explicit.push(Number(process.argv[i + 1]))
|
|
257
|
-
}
|
|
258
|
-
if (explicit.length > 1) return null
|
|
259
|
-
const configured = Number(gateway.port ?? process.env.OPENCLAW_GATEWAY_PORT ?? 18789)
|
|
260
|
-
const port = explicit[0] ?? configured
|
|
261
|
-
if (!Number.isInteger(port) || port < 1 || port > 65535) return null
|
|
262
|
-
return { script, port, packageVersion }
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
function openclawExec(target, args) {
|
|
266
|
-
return new Promise((resolve, reject) => {
|
|
267
|
-
execFile(process.execPath, [target.script, ...args], {
|
|
268
|
-
shell: false, windowsHide: true, timeout: 20_000, maxBuffer: 2_000_000,
|
|
269
|
-
env: process.env,
|
|
270
|
-
}, (error, stdout) => {
|
|
271
|
-
if (error) reject(new Error("OpenClaw CLI call failed"))
|
|
272
|
-
else resolve(stdout.trim())
|
|
273
|
-
})
|
|
274
|
-
})
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
async function gatewayCall(target, method, params) {
|
|
278
|
-
const output = await openclawExec(target, ["gateway", "call", method,
|
|
279
|
-
"--port", String(target.port), "--params", JSON.stringify(params), "--json"])
|
|
280
|
-
return JSON.parse(output)
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
async function verifiedGatewayTarget(config, logger) {
|
|
284
|
-
const target = openclawCliTarget(config)
|
|
285
|
-
if (target === null) {
|
|
286
|
-
logger?.warn?.("notifai continuation target: local CLI path or Gateway bind unavailable")
|
|
287
|
-
return null
|
|
288
|
-
}
|
|
289
|
-
try {
|
|
290
|
-
const banner = (await openclawExec(target, ["--version"])).trim()
|
|
291
|
-
const cliVersion = /^OpenClaw ([0-9]+\\.[0-9]+\\.[0-9]+)(?: \\([0-9a-f]+\\))?$/.exec(banner)?.[1]
|
|
292
|
-
const status = await gatewayCall(target, "status", {})
|
|
293
|
-
if (cliVersion !== target.packageVersion || status?.runtimeVersion !== cliVersion) {
|
|
294
|
-
logger?.warn?.("notifai continuation target: CLI and Gateway versions differ")
|
|
295
|
-
return null
|
|
296
|
-
}
|
|
297
|
-
if (status?.pid !== process.pid) {
|
|
298
|
-
logger?.warn?.("notifai continuation target: Gateway process identity differs")
|
|
299
|
-
return null
|
|
300
|
-
}
|
|
301
|
-
return target
|
|
302
|
-
} catch {
|
|
303
|
-
logger?.warn?.("notifai continuation target: local CLI status unavailable")
|
|
304
|
-
return null
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
function submissionKey(record) {
|
|
309
|
-
return "notifai-" + record.delivery_id + "-a" + record.attempt
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
function submissionIdentity(message) {
|
|
313
|
-
const meta = message?.metadata ?? message?.__openclaw ?? {}
|
|
314
|
-
return message?.idempotencyKey ?? meta.idempotencyKey ?? null
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
async function inspectSubmission(target, record) {
|
|
318
|
-
let offset = 0
|
|
319
|
-
let before
|
|
320
|
-
let pendingFinished = false
|
|
321
|
-
let interrupted = false
|
|
322
|
-
for (let page = 0; page < 1000; page += 1) {
|
|
323
|
-
const params = { sessionKey: record.session_key, limit: 200, offset,
|
|
324
|
-
...(before === undefined ? {} : { pendingBefore: before }) }
|
|
325
|
-
const history = await gatewayCall(target, "chat.history", params)
|
|
326
|
-
if (history?.kind === "reset" || history?.truncationReason ||
|
|
327
|
-
(record.openclaw_session_id && history?.sessionInfo?.sessionId &&
|
|
328
|
-
history.sessionInfo.sessionId !== record.openclaw_session_id)) return "unconfirmed"
|
|
329
|
-
const messages = history?.messages
|
|
330
|
-
if (!Array.isArray(messages)) return "unconfirmed"
|
|
331
|
-
if (messages.some((entry) => entry?.role === "user" &&
|
|
332
|
-
submissionIdentity(entry) === submissionKey(record) + ":user")) return "transcript"
|
|
333
|
-
const pending = history?.pendingInputs
|
|
334
|
-
if (!pending || !Array.isArray(pending.items) ||
|
|
335
|
-
!Number.isInteger(pending.total) || pending.total < pending.items.length ||
|
|
336
|
-
typeof history.hasMore !== "boolean") return "unconfirmed"
|
|
337
|
-
for (const item of pending.items) {
|
|
338
|
-
if (item?.runId !== submissionKey(record) &&
|
|
339
|
-
item?.idempotencyKey !== submissionKey(record)) continue
|
|
340
|
-
if (item.state === "queued" || item.state === "running") return "pending"
|
|
341
|
-
if (item.state === "interrupted" || item.state === "cancelled") interrupted = true
|
|
342
|
-
else return "unconfirmed"
|
|
343
|
-
}
|
|
344
|
-
const nextOffset = history.nextOffset
|
|
345
|
-
const hasMore = history.hasMore === true
|
|
346
|
-
const nextBefore = pending.nextBefore
|
|
347
|
-
if (!pendingFinished && typeof nextBefore === "string" && nextBefore !== before) {
|
|
348
|
-
before = nextBefore
|
|
349
|
-
continue
|
|
350
|
-
}
|
|
351
|
-
if (!pendingFinished && pending.total > pending.items.length) return "unconfirmed"
|
|
352
|
-
pendingFinished = true
|
|
353
|
-
if (hasMore && Number.isInteger(nextOffset) && nextOffset > offset) {
|
|
354
|
-
offset = nextOffset
|
|
355
|
-
before = undefined
|
|
356
|
-
continue
|
|
357
|
-
}
|
|
358
|
-
if (hasMore) return "unconfirmed"
|
|
359
|
-
return interrupted ? "interrupted" : "absent"
|
|
360
|
-
}
|
|
361
|
-
return "unconfirmed"
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
function pointerMessage(record) {
|
|
365
|
-
return "A Notifai reply is waiting for this session. " +
|
|
366
|
-
record.request_ids.map((id) => "Run notifai replies " + id +
|
|
367
|
-
" --json, then notifai acknowledge " + id +
|
|
368
|
-
" with the concrete work you will do.").join(" ")
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
function messagePointer(record) {
|
|
372
|
-
return "Notifai Session Message " + record.message_id +
|
|
373
|
-
" should appear in this turn's context. If its full context is absent, " +
|
|
374
|
-
"say that this Session Message is missing and do not acknowledge it. " +
|
|
375
|
-
"If present, read it and run notifai acknowledge " + record.message_id +
|
|
376
|
-
" with the concrete work you will do when text is required."
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
/** Attach private text only to the admitted pointer turn of its native generation. */
|
|
380
|
-
async function messageContextForPointer(api, event, ctx, sessionKey, nativeRevision) {
|
|
381
|
-
if (MESSAGE_JOURNAL_DIR === null || JOURNAL_DIR === null || nativeRevision === null) return null
|
|
382
|
-
const prompt = typeof event?.currentUserMessage === "string"
|
|
383
|
-
? event.currentUserMessage : event?.prompt
|
|
384
|
-
if (typeof prompt !== "string" || !prompt.startsWith("Notifai Session Message ")) return null
|
|
385
|
-
const messageId = prompt.slice("Notifai Session Message ".length).split(" ", 1)[0]
|
|
386
|
-
if (!/^sm_[A-Za-z0-9_-]+$/.test(messageId)) return null
|
|
387
|
-
const envelope = { ...envelopeFor(sessionKey, event, ctx, "BeforePromptBuild"),
|
|
388
|
-
openclaw_lifecycle_revision: nativeRevision }
|
|
389
|
-
const generation = (await runHook("openclaw-generation", envelope))?.trim()
|
|
390
|
-
if (typeof generation !== "string" || !/^[0-9a-f-]{36}$/i.test(generation)) return null
|
|
391
|
-
const deliveryId = createHash("sha256").update(generation + "\\0" + messageId)
|
|
392
|
-
.digest("hex").slice(0, 32)
|
|
393
|
-
try {
|
|
394
|
-
const record = readMessageJournal(deliveryId)
|
|
395
|
-
const readiness = JSON.parse(readFileSync(readinessPath(), "utf8"))
|
|
396
|
-
if (record === null || record.message_id !== messageId ||
|
|
397
|
-
record.session_key !== sessionKey || record.generation !== generation ||
|
|
398
|
-
record.native_revision !== nativeRevision ||
|
|
399
|
-
record.openclaw_session_id !== sessionIdOf(event, ctx) ||
|
|
400
|
-
record.boot_id !== readiness?.boot_id ||
|
|
401
|
-
typeof record.text !== "string" ||
|
|
402
|
-
(record.phase !== "submitting" && record.phase !== "admitted" &&
|
|
403
|
-
record.phase !== "transcript") ||
|
|
404
|
-
prompt !== messagePointer(record) ||
|
|
405
|
-
existsSync(messageContextMarkerPath(deliveryId)) ||
|
|
406
|
-
nativeSessionRevision(api, sessionKey, sessionIdOf(event, ctx)) !== nativeRevision) return null
|
|
407
|
-
ctx?.hookInvocation?.assertActive?.()
|
|
408
|
-
writeFileSync(messageContextMarkerPath(deliveryId), "used\\n", { mode: 0o600, flag: "wx" })
|
|
409
|
-
saveMessageJournal(record)
|
|
410
|
-
return record.text
|
|
411
|
-
} catch { return null }
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
async function gatewaySessions(target) {
|
|
415
|
-
let offset = 0
|
|
416
|
-
const sessions = new Map()
|
|
417
|
-
for (let page = 0; page < 1000; page += 1) {
|
|
418
|
-
const result = await gatewayCall(target, "sessions.list", { limit: 200, offset })
|
|
419
|
-
if (!Array.isArray(result?.sessions) || typeof result?.hasMore !== "boolean") return null
|
|
420
|
-
for (const session of result.sessions) {
|
|
421
|
-
if (typeof session?.key === "string") sessions.set(session.key, session)
|
|
422
|
-
}
|
|
423
|
-
if (!result.hasMore) return sessions
|
|
424
|
-
if (!Number.isInteger(result.nextOffset) || result.nextOffset <= offset) return null
|
|
425
|
-
offset = result.nextOffset
|
|
426
|
-
}
|
|
427
|
-
return null
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
async function currentSession(target, record) {
|
|
431
|
-
const sessions = await gatewaySessions(target)
|
|
432
|
-
const session = sessions?.get(record.session_key)
|
|
433
|
-
return typeof record.openclaw_session_id === "string" &&
|
|
434
|
-
session?.sessionId === record.openclaw_session_id &&
|
|
435
|
-
session.archived !== true ? session : null
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
function spawnOwnedHook(event, envelope, extraFds = false) {
|
|
439
|
-
const child = spawn(HOOK_COMMAND, [...HOOK_PREFIX, "hook", event,
|
|
440
|
-
"--owner", "notifai", "--harness", "openclaw"], {
|
|
441
|
-
cwd: envelope.cwd,
|
|
442
|
-
env: { ...process.env, NOTIFAI_HOOK_SOURCE_PID: String(process.pid),
|
|
443
|
-
...(event === "attend" && extraFds ? { NOTIFAI_OPENCLAW_MESSAGE_BRIDGE: "1" } : {}) },
|
|
444
|
-
shell: false, windowsHide: true,
|
|
445
|
-
stdio: extraFds ? ["pipe", "ignore", "ignore", "pipe", "pipe"] : ["pipe", "ignore", "ignore"],
|
|
446
|
-
})
|
|
447
|
-
child.stdin.end(JSON.stringify(envelope))
|
|
448
|
-
return child
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
function makeContinuationService(config, logger, api) {
|
|
452
|
-
const active = new Map()
|
|
453
|
-
const delivering = new Set()
|
|
454
|
-
const reported = new Map()
|
|
455
|
-
const deliveringMessages = new Map()
|
|
456
|
-
let stopped = false
|
|
457
|
-
let timer = null
|
|
458
|
-
let scanning = false
|
|
459
|
-
let ready = false
|
|
460
|
-
let gatewayTarget = null
|
|
461
|
-
|
|
462
|
-
function report(record, reason) {
|
|
463
|
-
if (reported.get(record.delivery_id) === reason) return
|
|
464
|
-
reported.set(record.delivery_id, reason)
|
|
465
|
-
logger?.warn?.("notifai continuation " + record.delivery_id + ": " + reason)
|
|
466
|
-
}
|
|
467
|
-
|
|
468
|
-
async function deliver(record) {
|
|
469
|
-
if (delivering.has(record.delivery_id) || record.phase === "transcript") return
|
|
470
|
-
delivering.add(record.delivery_id)
|
|
471
|
-
try {
|
|
472
|
-
const envelope = { session_id: record.session_key, cwd: record.cwd,
|
|
473
|
-
openclaw_session_id: record.openclaw_session_id }
|
|
474
|
-
const generation = (await runHook("openclaw-generation", envelope))?.trim()
|
|
475
|
-
if (generation !== record.generation) { report(record, "generation-fenced"); return }
|
|
476
|
-
if (record.phase === "prepared") {
|
|
477
|
-
const proved = await runHook("openclaw-verify-prepared", {
|
|
478
|
-
...envelope, openclaw_request_ids: record.request_ids })
|
|
479
|
-
if (proved?.trim() !== "committed") return
|
|
480
|
-
record = { ...record, phase: "committed" }
|
|
481
|
-
saveJournal(record)
|
|
482
|
-
}
|
|
483
|
-
const target = await verifiedGatewayTarget(config, logger)
|
|
484
|
-
if (target === null) { report(record, "gateway-target-unverified"); return }
|
|
485
|
-
const state = await inspectSubmission(target, record)
|
|
486
|
-
if (state === "unconfirmed") { report(record, "history-unconfirmed"); return }
|
|
487
|
-
if (state === "transcript") {
|
|
488
|
-
saveJournal({ ...record, phase: "transcript" })
|
|
489
|
-
return
|
|
490
|
-
}
|
|
491
|
-
if (state === "pending") {
|
|
492
|
-
if (record.phase !== "admitted") saveJournal({ ...record, phase: "admitted" })
|
|
493
|
-
return
|
|
494
|
-
}
|
|
495
|
-
if (state === "interrupted" || (state === "absent" && record.phase === "admitted")) {
|
|
496
|
-
record = { ...record, attempt: record.attempt + 1, phase: "committed" }
|
|
497
|
-
saveJournal(record)
|
|
498
|
-
}
|
|
499
|
-
if (state !== "absent" && state !== "interrupted") return
|
|
500
|
-
saveJournal({ ...record, phase: "submitting" })
|
|
501
|
-
report(record, "submitting-pointer")
|
|
502
|
-
await gatewayCall(target, "chat.send", { sessionKey: record.session_key,
|
|
503
|
-
message: pointerMessage(record), queueMode: "followup",
|
|
504
|
-
idempotencyKey: submissionKey(record) })
|
|
505
|
-
saveJournal({ ...record, phase: "admitted" })
|
|
506
|
-
report(record, "pointer-admitted")
|
|
507
|
-
} catch {
|
|
508
|
-
// An uncertain RPC is reconciled by identity before any retry.
|
|
509
|
-
report(record, "rpc-or-journal-unconfirmed")
|
|
510
|
-
} finally {
|
|
511
|
-
delivering.delete(record.delivery_id)
|
|
512
|
-
}
|
|
513
|
-
}
|
|
514
|
-
|
|
515
|
-
async function deliverMessageOnce(record) {
|
|
516
|
-
try {
|
|
517
|
-
if (record.boot_id !== GATEWAY_BOOT_ID) {
|
|
518
|
-
settleMessageJournal(record, "unconfirmed")
|
|
519
|
-
return false
|
|
520
|
-
}
|
|
521
|
-
if (!claimTimeAvailable(record) && record.phase !== "admitted" &&
|
|
522
|
-
record.phase !== "transcript") {
|
|
523
|
-
settleMessageJournal(record, "unconfirmed")
|
|
524
|
-
return false
|
|
525
|
-
}
|
|
526
|
-
const envelope = { session_id: record.session_key, cwd: record.cwd,
|
|
527
|
-
openclaw_session_id: record.openclaw_session_id,
|
|
528
|
-
openclaw_lifecycle_revision: record.native_revision }
|
|
529
|
-
if (nativeSessionRevision(api, record.session_key, record.openclaw_session_id) !==
|
|
530
|
-
record.native_revision) {
|
|
531
|
-
settleMessageJournal(record, "unconfirmed")
|
|
532
|
-
return false
|
|
533
|
-
}
|
|
534
|
-
const generation = (await runHook("openclaw-generation", envelope))?.trim()
|
|
535
|
-
if (generation !== record.generation) {
|
|
536
|
-
settleMessageJournal(record, "unconfirmed")
|
|
537
|
-
return false
|
|
538
|
-
}
|
|
539
|
-
const attendance = (await runHook("openclaw-attendance-ready", envelope))?.trim()
|
|
540
|
-
if (attendance !== "ready") return false
|
|
541
|
-
const target = await verifiedGatewayTarget(config, logger)
|
|
542
|
-
if (target === null) return false
|
|
543
|
-
const session = await currentSession(target, record)
|
|
544
|
-
if (session === null) return false
|
|
545
|
-
const state = await inspectSubmission(target, record)
|
|
546
|
-
if (state === "transcript") {
|
|
547
|
-
settleMessageJournal(record, "transcript")
|
|
548
|
-
return true
|
|
549
|
-
}
|
|
550
|
-
if (state === "pending") return true
|
|
551
|
-
if (state !== "absent" || record.phase === "admitted" ||
|
|
552
|
-
record.phase === "submitting") {
|
|
553
|
-
settleMessageJournal(record, "unconfirmed")
|
|
554
|
-
return false
|
|
555
|
-
}
|
|
556
|
-
if (!claimTimeAvailable(record)) {
|
|
557
|
-
settleMessageJournal(record, "unconfirmed")
|
|
558
|
-
return false
|
|
559
|
-
}
|
|
560
|
-
if ((await runHook("openclaw-generation", envelope))?.trim() !== record.generation) {
|
|
561
|
-
settleMessageJournal(record, "unconfirmed")
|
|
562
|
-
return false
|
|
563
|
-
}
|
|
564
|
-
if (nativeSessionRevision(api, record.session_key, record.openclaw_session_id) !==
|
|
565
|
-
record.native_revision) {
|
|
566
|
-
settleMessageJournal(record, "unconfirmed")
|
|
567
|
-
return false
|
|
568
|
-
}
|
|
569
|
-
if (await currentSession(target, record) === null) return false
|
|
570
|
-
saveMessageJournal({ ...record, phase: "submitting" })
|
|
571
|
-
await gatewayCall(target, "chat.send", {
|
|
572
|
-
sessionKey: record.session_key, message: messagePointer(record),
|
|
573
|
-
queueMode: "followup",
|
|
574
|
-
idempotencyKey: submissionKey(record),
|
|
575
|
-
})
|
|
576
|
-
saveMessageJournal({ ...record, phase: "admitted" })
|
|
577
|
-
return true
|
|
578
|
-
} catch {
|
|
579
|
-
// An uncertain admission is reconciled by identity, never blindly replayed.
|
|
580
|
-
return false
|
|
581
|
-
} finally { /* The wrapper removes the in-flight entry. */ }
|
|
582
|
-
}
|
|
583
|
-
|
|
584
|
-
function deliverMessage(record) {
|
|
585
|
-
if (record.phase === "transcript" || record.phase === "unconfirmed") return Promise.resolve(false)
|
|
586
|
-
const existing = deliveringMessages.get(record.delivery_id)
|
|
587
|
-
if (existing) return existing
|
|
588
|
-
const run = deliverMessageOnce(record).finally(() => deliveringMessages.delete(record.delivery_id))
|
|
589
|
-
deliveringMessages.set(record.delivery_id, run)
|
|
590
|
-
return run
|
|
591
|
-
}
|
|
592
|
-
|
|
593
|
-
function startMessageBridge(session, child) {
|
|
594
|
-
let buffer = ""
|
|
595
|
-
let queue = Promise.resolve()
|
|
596
|
-
child.stdio[4].on("error", () => { /* A closed attendant cannot accept a result. */ })
|
|
597
|
-
child.stdio[3].setEncoding("utf8")
|
|
598
|
-
child.stdio[3].on("data", (chunk) => {
|
|
599
|
-
buffer += chunk
|
|
600
|
-
if (buffer.length > 100_000) { child.kill("SIGTERM"); return }
|
|
601
|
-
for (;;) {
|
|
602
|
-
const end = buffer.indexOf("\\n")
|
|
603
|
-
if (end < 0) break
|
|
604
|
-
const line = buffer.slice(0, end)
|
|
605
|
-
buffer = buffer.slice(end + 1)
|
|
606
|
-
let payload
|
|
607
|
-
try { payload = JSON.parse(line) } catch { continue }
|
|
608
|
-
if (payload?.type !== "message" ||
|
|
609
|
-
!/^sm_[A-Za-z0-9_-]+$/.test(payload.message_id) ||
|
|
610
|
-
payload.session_key !== session.session_key ||
|
|
611
|
-
payload.generation !== session.generation ||
|
|
612
|
-
typeof payload.text !== "string" || payload.text.length > 32768 ||
|
|
613
|
-
typeof payload.deadline_ns !== "string" ||
|
|
614
|
-
payload.deadline_ns.length > 32 ||
|
|
615
|
-
!/^[0-9]+$/.test(payload.deadline_ns)) continue
|
|
616
|
-
queue = queue.then(async () => {
|
|
617
|
-
const deliveryId = createHash("sha256")
|
|
618
|
-
.update(session.generation + "\\0" + payload.message_id)
|
|
619
|
-
.digest("hex").slice(0, 32)
|
|
620
|
-
const record = { delivery_id: deliveryId, message_id: payload.message_id,
|
|
621
|
-
session_key: session.session_key, cwd: session.cwd,
|
|
622
|
-
openclaw_session_id: session.session_id, generation: session.generation,
|
|
623
|
-
native_revision: nativeSessionRevision(api, session.session_key, session.session_id),
|
|
624
|
-
boot_id: GATEWAY_BOOT_ID, text: payload.text, deadline_ns: payload.deadline_ns,
|
|
625
|
-
attempt: 1, phase: "prepared" }
|
|
626
|
-
const existing = readMessageJournal(deliveryId)
|
|
627
|
-
const current = active.get(session.session_key)
|
|
628
|
-
let written = false
|
|
629
|
-
if (current?.attendant === child && record.native_revision !== null &&
|
|
630
|
-
(existing === null ||
|
|
631
|
-
(existing.message_id === record.message_id &&
|
|
632
|
-
existing.session_key === record.session_key &&
|
|
633
|
-
existing.generation === record.generation))) {
|
|
634
|
-
if (existing === null) saveMessageJournal(record)
|
|
635
|
-
written = await deliverMessage(existing ?? record)
|
|
636
|
-
}
|
|
637
|
-
child.stdio[4].write(JSON.stringify({ message_id: payload.message_id,
|
|
638
|
-
status: written ? "written" : "unconfirmed" }) + "\\n")
|
|
639
|
-
}).catch(() => {
|
|
640
|
-
child.stdio[4].write(JSON.stringify({ message_id: payload.message_id,
|
|
641
|
-
status: "unconfirmed" }) + "\\n")
|
|
642
|
-
})
|
|
643
|
-
}
|
|
644
|
-
})
|
|
645
|
-
}
|
|
646
|
-
|
|
647
|
-
function startSettlement(session, entry) {
|
|
648
|
-
const envelope = { session_id: session.session_key, cwd: session.cwd,
|
|
649
|
-
openclaw_session_id: session.session_id, hook_event_name: "SessionStart" }
|
|
650
|
-
const child = spawnOwnedHook("openclaw-settlement", envelope, true)
|
|
651
|
-
entry.child = child
|
|
652
|
-
let buffer = ""
|
|
653
|
-
let prepared = null
|
|
654
|
-
child.stdio[3].setEncoding("utf8")
|
|
655
|
-
child.stdio[3].on("data", (chunk) => {
|
|
656
|
-
buffer += chunk
|
|
657
|
-
for (;;) {
|
|
658
|
-
const end = buffer.indexOf("\\n")
|
|
659
|
-
if (end < 0) break
|
|
660
|
-
const line = buffer.slice(0, end)
|
|
661
|
-
buffer = buffer.slice(end + 1)
|
|
662
|
-
let payload
|
|
663
|
-
try { payload = JSON.parse(line) } catch { continue }
|
|
664
|
-
if (payload.type === "prepare") {
|
|
665
|
-
const ids = payload.request_ids
|
|
666
|
-
if (payload.session_key !== session.session_key ||
|
|
667
|
-
payload.generation !== session.generation || !Array.isArray(ids) ||
|
|
668
|
-
ids.length < 1 || ids.some((id) => typeof id !== "string" ||
|
|
669
|
-
!/^req_[A-Za-z0-9_-]+$/.test(id))) continue
|
|
670
|
-
const deliveryId = createHash("sha256")
|
|
671
|
-
.update(session.generation + "\\0" + ids.join("\\0"))
|
|
672
|
-
.digest("hex").slice(0, 32)
|
|
673
|
-
const existing = existingJournal(deliveryId)
|
|
674
|
-
if (existing === null && existsSync(journalPath(deliveryId))) continue
|
|
675
|
-
if (existing !== null && (existing.session_key !== session.session_key ||
|
|
676
|
-
existing.generation !== session.generation ||
|
|
677
|
-
JSON.stringify(existing.request_ids) !== JSON.stringify(ids))) continue
|
|
678
|
-
prepared = existing ?? { delivery_id: deliveryId, session_key: session.session_key,
|
|
679
|
-
cwd: session.cwd, generation: session.generation,
|
|
680
|
-
openclaw_session_id: session.session_id, request_ids: ids,
|
|
681
|
-
attempt: 1, phase: "prepared" }
|
|
682
|
-
if (existing === null) saveJournal(prepared)
|
|
683
|
-
child.stdio[4].write('{"type":"prepared"}\\n')
|
|
684
|
-
} else if (payload.type === "committed" && prepared !== null) {
|
|
685
|
-
if (prepared.phase === "prepared") {
|
|
686
|
-
prepared = { ...prepared, phase: "committed" }
|
|
687
|
-
saveJournal(prepared)
|
|
688
|
-
}
|
|
689
|
-
void deliver(prepared)
|
|
690
|
-
}
|
|
691
|
-
}
|
|
692
|
-
})
|
|
693
|
-
child.on("close", () => {
|
|
694
|
-
const current = active.get(session.session_key)
|
|
695
|
-
if (current?.child === child) {
|
|
696
|
-
current.child = null
|
|
697
|
-
}
|
|
698
|
-
})
|
|
699
|
-
}
|
|
700
|
-
|
|
701
|
-
function startSession(session) {
|
|
702
|
-
const envelope = { session_id: session.session_key, cwd: session.cwd,
|
|
703
|
-
openclaw_session_id: session.session_id, hook_event_name: "SessionStart" }
|
|
704
|
-
const entry = { generation: session.generation,
|
|
705
|
-
attendant: spawnOwnedHook("attend", envelope, true), child: null }
|
|
706
|
-
active.set(session.session_key, entry)
|
|
707
|
-
startMessageBridge(session, entry.attendant)
|
|
708
|
-
startSettlement(session, entry)
|
|
709
|
-
}
|
|
710
|
-
|
|
711
|
-
function sendActivity(entry, session) {
|
|
712
|
-
if (entry.attendant.exitCode === null && entry.attendant.stdio[4].writable) {
|
|
713
|
-
entry.attendant.stdio[4].write(JSON.stringify({ type: "activity",
|
|
714
|
-
activity: session.hasActiveRun === true ? "working" : "idle" }) + "\\n")
|
|
715
|
-
}
|
|
716
|
-
}
|
|
717
|
-
|
|
718
|
-
async function tick() {
|
|
719
|
-
if (stopped || scanning) return
|
|
720
|
-
scanning = true
|
|
721
|
-
try {
|
|
722
|
-
if (!ready) {
|
|
723
|
-
const target = await verifiedGatewayTarget(config, logger)
|
|
724
|
-
if (target === null || !writeReadiness(target)) return
|
|
725
|
-
gatewayTarget = target
|
|
726
|
-
ready = true
|
|
727
|
-
}
|
|
728
|
-
const raw = await runHook("openclaw-list-pending", { cwd: process.cwd() })
|
|
729
|
-
if (raw === null) return
|
|
730
|
-
const pending = JSON.parse(raw)
|
|
731
|
-
const sessions = await gatewaySessions(gatewayTarget)
|
|
732
|
-
if (sessions === null) return
|
|
733
|
-
const seen = new Set()
|
|
734
|
-
if (Array.isArray(pending)) for (const session of pending) {
|
|
735
|
-
if (typeof session?.session_key !== "string" ||
|
|
736
|
-
typeof session?.cwd !== "string" ||
|
|
737
|
-
typeof session?.generation !== "string") continue
|
|
738
|
-
const gatewaySession = sessions.get(session.session_key)
|
|
739
|
-
if (typeof session.session_id !== "string" ||
|
|
740
|
-
gatewaySession?.sessionId !== session.session_id ||
|
|
741
|
-
gatewaySession.archived === true) continue
|
|
742
|
-
seen.add(session.session_key)
|
|
743
|
-
const current = active.get(session.session_key)
|
|
744
|
-
if (current?.generation === session.generation) {
|
|
745
|
-
if (current.attendant.exitCode !== null) {
|
|
746
|
-
active.delete(session.session_key)
|
|
747
|
-
if (current.child !== null) current.child.kill("SIGTERM")
|
|
748
|
-
startSession(session)
|
|
749
|
-
sendActivity(active.get(session.session_key), gatewaySession)
|
|
750
|
-
} else if (current.child === null) {
|
|
751
|
-
const ready = await runHook("openclaw-attendance-ready", {
|
|
752
|
-
session_id: session.session_key, cwd: session.cwd,
|
|
753
|
-
openclaw_session_id: session.session_id })
|
|
754
|
-
if (ready?.trim() === "ready") startSettlement(session, current)
|
|
755
|
-
}
|
|
756
|
-
if (active.get(session.session_key) === current) sendActivity(current, gatewaySession)
|
|
757
|
-
continue
|
|
758
|
-
}
|
|
759
|
-
if (current) {
|
|
760
|
-
if (current.child !== null) current.child.kill("SIGTERM")
|
|
761
|
-
current.attendant.kill("SIGTERM")
|
|
762
|
-
}
|
|
763
|
-
startSession(session)
|
|
764
|
-
sendActivity(active.get(session.session_key), gatewaySession)
|
|
765
|
-
}
|
|
766
|
-
for (const [key, entry] of active) {
|
|
767
|
-
if (seen.has(key)) continue
|
|
768
|
-
if (entry.child !== null) entry.child.kill("SIGTERM")
|
|
769
|
-
entry.attendant.kill("SIGTERM")
|
|
770
|
-
active.delete(key)
|
|
771
|
-
}
|
|
772
|
-
for (const record of readJournals()) void deliver(record)
|
|
773
|
-
for (const [deliveryId] of awaitingMessageContexts) {
|
|
774
|
-
const record = readMessageJournal(deliveryId)
|
|
775
|
-
if (record === null || record.text === undefined) {
|
|
776
|
-
awaitingMessageContexts.delete(deliveryId)
|
|
777
|
-
} else if (record.boot_id !== GATEWAY_BOOT_ID ||
|
|
778
|
-
nativeSessionRevision(api, record.session_key, record.openclaw_session_id) !==
|
|
779
|
-
record.native_revision) {
|
|
780
|
-
const { text: _text, ...withoutText } = record
|
|
781
|
-
saveMessageJournal(withoutText)
|
|
782
|
-
}
|
|
783
|
-
}
|
|
784
|
-
for (const pending of retryMessageJournals()) {
|
|
785
|
-
const record = readMessageJournal(pending.delivery_id)
|
|
786
|
-
if (record === null) continue
|
|
787
|
-
if (record.phase === "transcript" || record.phase === "unconfirmed") {
|
|
788
|
-
pendingMessageJournals?.delete(record.delivery_id)
|
|
789
|
-
continue
|
|
790
|
-
}
|
|
791
|
-
void deliverMessage(record)
|
|
792
|
-
}
|
|
793
|
-
} catch { /* The next Gateway tick retries discovery. */ }
|
|
794
|
-
finally { scanning = false }
|
|
795
|
-
}
|
|
796
|
-
|
|
797
|
-
return {
|
|
798
|
-
start() { pendingMessageJournals = null; timer = setInterval(() => { void tick() }, 2_000); void tick() },
|
|
799
|
-
stop() {
|
|
800
|
-
stopped = true
|
|
801
|
-
clearReadiness()
|
|
802
|
-
if (timer !== null) clearInterval(timer)
|
|
803
|
-
for (const entry of active.values()) {
|
|
804
|
-
if (entry.child !== null) entry.child.kill("SIGTERM")
|
|
805
|
-
entry.attendant.kill("SIGTERM")
|
|
806
|
-
}
|
|
807
|
-
active.clear()
|
|
808
|
-
},
|
|
809
|
-
}
|
|
810
|
-
}
|
|
811
|
-
`;
|
|
812
|
-
}
|
|
813
|
-
export function openclawPluginManifest() {
|
|
814
|
-
return `${JSON.stringify({
|
|
815
|
-
id: OPENCLAW_PLUGIN_ID,
|
|
816
|
-
name: 'Notifai',
|
|
817
|
-
description: 'Notifai lifecycle activation, Source Context, and session observation.',
|
|
818
|
-
activation: { onStartup: true },
|
|
819
|
-
configSchema: { type: 'object', additionalProperties: false },
|
|
820
|
-
}, null, 2)}\n`;
|
|
821
|
-
}
|
|
822
|
-
export function openclawPluginPackage() {
|
|
823
|
-
return `${JSON.stringify({
|
|
824
|
-
name: 'notifai-openclaw',
|
|
825
|
-
version: '1.0.0',
|
|
826
|
-
type: 'module',
|
|
827
|
-
openclaw: { extensions: [`./${OPENCLAW_PLUGIN_FILENAME}`] },
|
|
828
|
-
}, null, 2)}\n`;
|
|
829
|
-
}
|
|
830
|
-
export function openclawPluginSource(options) {
|
|
831
|
-
const { adapterPath, timeoutSeconds } = options;
|
|
832
|
-
const win32 = hookHostPlatform(options.platform) === 'win32';
|
|
833
|
-
const nodeConstant = win32
|
|
834
|
-
? `const NODE = ${JSON.stringify(options.nodePath ?? process.execPath)}\n`
|
|
835
|
-
: '';
|
|
836
|
-
const spawnArguments = win32
|
|
837
|
-
? 'NODE, [ADAPTER, "hook", event, "--owner", "notifai", "--harness", "openclaw"]'
|
|
838
|
-
: 'ADAPTER, ["hook", event, "--owner", "notifai", "--harness", "openclaw"]';
|
|
839
|
-
const windowsHide = win32 ? '\n windowsHide: true,' : '';
|
|
840
|
-
return `${OPENCLAW_PLUGIN_MARKER}
|
|
841
|
-
// Generated by \`notifai hooks install --harness openclaw\`. Edits are lost on
|
|
842
|
-
// the next install; change the CLI instead, which is where the logic lives.
|
|
843
|
-
import { spawn, execFile, execFileSync } from "node:child_process"
|
|
844
|
-
import { createHash, randomUUID } from "node:crypto"
|
|
845
|
-
import { closeSync, existsSync, fsyncSync, mkdirSync, openSync, readFileSync, readdirSync, realpathSync, renameSync, statSync, unlinkSync, writeFileSync } from "node:fs"
|
|
846
|
-
import path from "node:path"
|
|
847
|
-
|
|
848
|
-
${nodeConstant}const ADAPTER = ${JSON.stringify(adapterPath)}
|
|
849
|
-
const HOOK_COMMAND = ${JSON.stringify(win32 ? options.nodePath ?? process.execPath : adapterPath)}
|
|
850
|
-
const HOOK_PREFIX = ${JSON.stringify(win32 ? [adapterPath] : [])}
|
|
851
|
-
const TIMEOUT_MS = ${timeoutSeconds * 1000}
|
|
852
|
-
const ADAPTER_VERSION = ${OPENCLAW_ADAPTER_VERSION}
|
|
853
|
-
const MISSING_LIFECYCLE_GUIDANCE_CONTEXT = ${JSON.stringify(MISSING_LIFECYCLE_GUIDANCE_CONTEXT)}
|
|
854
|
-
const WORKER_ACTIVATION_CONTEXT = ${JSON.stringify(WORKER_ACTIVATION_CONTEXT)}
|
|
855
|
-
|
|
856
|
-
function runHook(event, envelope) {
|
|
857
|
-
return new Promise((resolve) => {
|
|
858
|
-
let child
|
|
859
|
-
try {
|
|
860
|
-
child = spawn(${spawnArguments}, {
|
|
861
|
-
env: process.env,
|
|
862
|
-
shell: false,${windowsHide}
|
|
863
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
864
|
-
})
|
|
865
|
-
} catch {
|
|
866
|
-
resolve(null)
|
|
867
|
-
return
|
|
868
|
-
}
|
|
869
|
-
|
|
870
|
-
let stdout = ""
|
|
871
|
-
let settled = false
|
|
872
|
-
const finish = (value) => {
|
|
873
|
-
if (settled) return
|
|
874
|
-
settled = true
|
|
875
|
-
clearTimeout(timer)
|
|
876
|
-
resolve(value)
|
|
877
|
-
}
|
|
878
|
-
const timer = setTimeout(() => {
|
|
879
|
-
child.kill("SIGTERM")
|
|
880
|
-
finish(null)
|
|
881
|
-
}, TIMEOUT_MS)
|
|
882
|
-
|
|
883
|
-
child.stdout?.setEncoding("utf-8")
|
|
884
|
-
child.stdout?.on("data", (chunk) => {
|
|
885
|
-
stdout += chunk
|
|
886
|
-
})
|
|
887
|
-
child.stderr?.resume()
|
|
888
|
-
child.on("error", () => finish(null))
|
|
889
|
-
child.on("close", (code) => finish(code === 0 ? stdout : null))
|
|
890
|
-
|
|
891
|
-
try {
|
|
892
|
-
child.stdin?.end(JSON.stringify(envelope))
|
|
893
|
-
} catch {
|
|
894
|
-
finish(null)
|
|
895
|
-
}
|
|
896
|
-
})
|
|
897
|
-
}
|
|
898
|
-
|
|
899
|
-
function sessionKeyOf(event, ctx) {
|
|
900
|
-
const key = ctx?.sessionKey ?? event?.sessionKey ?? event?.context?.sessionKey
|
|
901
|
-
return typeof key === "string" ? key.trim() : ""
|
|
902
|
-
}
|
|
903
|
-
|
|
904
|
-
function sessionIdOf(event, ctx) {
|
|
905
|
-
const id = event?.sessionId ?? ctx?.sessionId
|
|
906
|
-
return typeof id === "string" && id.length > 0 ? id : undefined
|
|
907
|
-
}
|
|
908
|
-
|
|
909
|
-
function resumedFromOf(event) {
|
|
910
|
-
return typeof event?.resumedFrom === "string" && event.resumedFrom.length > 0
|
|
911
|
-
? event.resumedFrom : undefined
|
|
912
|
-
}
|
|
913
|
-
|
|
914
|
-
function reasonOf(event) {
|
|
915
|
-
return typeof event?.reason === "string" && event.reason.length > 0
|
|
916
|
-
? event.reason : undefined
|
|
917
|
-
}
|
|
918
|
-
|
|
919
|
-
function workspaceDirOf(event, ctx) {
|
|
920
|
-
const dir = ctx?.workspaceDir ?? event?.workspaceDir ?? event?.context?.workspaceDir
|
|
921
|
-
return typeof dir === "string" && dir.length > 0 ? dir : process.cwd()
|
|
922
|
-
}
|
|
923
|
-
|
|
924
|
-
function isWorkerSession(sessionKey, ctx) {
|
|
925
|
-
if (sessionKey === "") return true
|
|
926
|
-
if (sessionKey.includes(":subagent:")) return true
|
|
927
|
-
if (sessionKey.includes(":acp:")) return true
|
|
928
|
-
if (ctx?.targetKind === "acp" || ctx?.dispatchKind === "acp") return true
|
|
929
|
-
if (typeof ctx?.parentSessionKey === "string" && ctx.parentSessionKey.length > 0) return true
|
|
930
|
-
return false
|
|
931
|
-
}
|
|
932
|
-
|
|
933
|
-
function isUserMessage(event, ctx) {
|
|
934
|
-
const from = event?.from ?? event?.context?.from ?? ctx?.senderId
|
|
935
|
-
if (typeof from !== "string" || from.trim().length === 0) return false
|
|
936
|
-
const trigger = ctx?.trigger ?? event?.trigger
|
|
937
|
-
if (trigger === "cron" || trigger === "heartbeat") return false
|
|
938
|
-
return true
|
|
939
|
-
}
|
|
940
|
-
|
|
941
|
-
function envelopeFor(sessionKey, event, ctx, hookEventName) {
|
|
942
|
-
return {
|
|
943
|
-
session_id: sessionKey,
|
|
944
|
-
cwd: workspaceDirOf(event, ctx),
|
|
945
|
-
hook_event_name: hookEventName,
|
|
946
|
-
openclaw_session_id: sessionIdOf(event, ctx),
|
|
947
|
-
openclaw_reason: reasonOf(event),
|
|
948
|
-
openclaw_resumed_from: resumedFromOf(event),
|
|
949
|
-
}
|
|
950
|
-
}
|
|
951
|
-
|
|
952
|
-
function onIfSupported(api, name, handler) {
|
|
953
|
-
try {
|
|
954
|
-
api.on(name, handler)
|
|
955
|
-
} catch {
|
|
956
|
-
// Older OpenClaw builds may reject a typed hook. The prompt fallback
|
|
957
|
-
// remains available, while unobserved rotations cannot be guessed.
|
|
958
|
-
}
|
|
959
|
-
}
|
|
960
|
-
|
|
961
|
-
${openclawContinuationServiceSource()}
|
|
962
|
-
|
|
963
|
-
function register(api) {
|
|
964
|
-
try {
|
|
965
|
-
const stateDir = api.runtime?.state?.resolveStateDir?.()
|
|
966
|
-
if (typeof stateDir === "string" && path.isAbsolute(stateDir)) {
|
|
967
|
-
JOURNAL_DIR = path.join(stateDir, "notifai", "continuation-journal")
|
|
968
|
-
MESSAGE_JOURNAL_DIR = path.join(stateDir, "notifai", "message-journal")
|
|
969
|
-
}
|
|
970
|
-
} catch { /* No private journal is available in this plugin instance. */ }
|
|
971
|
-
api.registerService?.({
|
|
972
|
-
id: "notifai-continuation",
|
|
973
|
-
start(ctx) {
|
|
974
|
-
if (typeof ctx.stateDir !== "string" || !path.isAbsolute(ctx.stateDir)) return
|
|
975
|
-
JOURNAL_DIR = path.join(ctx.stateDir, "notifai", "continuation-journal")
|
|
976
|
-
MESSAGE_JOURNAL_DIR = path.join(ctx.stateDir, "notifai", "message-journal")
|
|
977
|
-
continuationService = makeContinuationService(ctx.config, ctx.logger, api)
|
|
978
|
-
continuationService.start()
|
|
979
|
-
},
|
|
980
|
-
stop() {
|
|
981
|
-
continuationService?.stop()
|
|
982
|
-
continuationService = null
|
|
983
|
-
},
|
|
984
|
-
})
|
|
985
|
-
onIfSupported(api, "session_start", async (event, ctx) => {
|
|
986
|
-
const sessionKey = sessionKeyOf(event, ctx)
|
|
987
|
-
if (sessionKey === "") return
|
|
988
|
-
await runHook("openclaw-lifecycle", envelopeFor(sessionKey, event, ctx, "SessionStart"))
|
|
989
|
-
})
|
|
990
|
-
|
|
991
|
-
onIfSupported(api, "before_reset", async (event, ctx) => {
|
|
992
|
-
const sessionKey = sessionKeyOf(event, ctx)
|
|
993
|
-
if (sessionKey === "") return
|
|
994
|
-
const nativeRevision = nativeSessionRevision(api, sessionKey, sessionIdOf(event, ctx))
|
|
995
|
-
await runHook("openclaw-lifecycle", { ...envelopeFor(sessionKey, event, ctx, "BeforeReset"),
|
|
996
|
-
...(nativeRevision === null ? {} : { openclaw_lifecycle_revision: nativeRevision }) })
|
|
997
|
-
})
|
|
998
|
-
|
|
999
|
-
api.on("before_prompt_build", async (event, ctx) => {
|
|
1000
|
-
const sessionKey = sessionKeyOf(event, ctx)
|
|
1001
|
-
if (sessionKey === "") return { prependContext: WORKER_ACTIVATION_CONTEXT }
|
|
1002
|
-
const worker = isWorkerSession(sessionKey, ctx)
|
|
1003
|
-
const nativeRevision = nativeSessionRevision(api, sessionKey, sessionIdOf(event, ctx))
|
|
1004
|
-
const envelope = { ...envelopeFor(sessionKey, event, ctx, "BeforePromptBuild"),
|
|
1005
|
-
...(nativeRevision === null ? {} : { openclaw_lifecycle_revision: nativeRevision }) }
|
|
1006
|
-
const resolved = await runHook(worker ? "subagent-start" : "session-start",
|
|
1007
|
-
{ ...envelope, hook_event_name: worker ? "SubagentStart" : "SessionStart" })
|
|
1008
|
-
// chat.send does not emit message_received on this supported Gateway path.
|
|
1009
|
-
// The prompt build is the exact session's authoritative turn-start seam.
|
|
1010
|
-
await runHook("openclaw-turn-start", envelope)
|
|
1011
|
-
const messageContext = worker ? null :
|
|
1012
|
-
await messageContextForPointer(api, event, ctx, sessionKey, nativeRevision)
|
|
1013
|
-
// Successful empty output means this Project is disabled or this
|
|
1014
|
-
// generation was already activated. A later prompt rechecks enablement.
|
|
1015
|
-
if (resolved !== null && resolved.trim().length === 0 && messageContext === null) return
|
|
1016
|
-
const context = typeof resolved === "string" && resolved.trim().length > 0
|
|
1017
|
-
? resolved.trim()
|
|
1018
|
-
: resolved === null ? worker ? WORKER_ACTIVATION_CONTEXT : MISSING_LIFECYCLE_GUIDANCE_CONTEXT : ""
|
|
1019
|
-
return { prependContext: [context, messageContext].filter(Boolean).join("\\n\\n") }
|
|
1020
|
-
})
|
|
1021
|
-
|
|
1022
|
-
api.on("message_received", async (event, ctx) => {
|
|
1023
|
-
if (!isUserMessage(event, ctx)) return
|
|
1024
|
-
const sessionKey = sessionKeyOf(event, ctx)
|
|
1025
|
-
if (sessionKey === "") return
|
|
1026
|
-
await runHook("user-prompt-submit", {
|
|
1027
|
-
session_id: sessionKey,
|
|
1028
|
-
cwd: workspaceDirOf(event, ctx),
|
|
1029
|
-
hook_event_name: "UserPromptSubmit",
|
|
1030
|
-
})
|
|
1031
|
-
})
|
|
1032
|
-
|
|
1033
|
-
api.on("agent_end", async (event, ctx) => {
|
|
1034
|
-
const sessionKey = sessionKeyOf(event, ctx)
|
|
1035
|
-
if (sessionKey === "") return
|
|
1036
|
-
await runHook("openclaw-turn-end", envelopeFor(sessionKey, event, ctx, "Stop"))
|
|
1037
|
-
})
|
|
1038
|
-
|
|
1039
|
-
api.on("session_end", async (event, ctx) => {
|
|
1040
|
-
const sessionKey = sessionKeyOf(event, ctx)
|
|
1041
|
-
if (sessionKey === "") return
|
|
1042
|
-
await runHook("session-end", envelopeFor(sessionKey, event, ctx, "SessionEnd"))
|
|
1043
|
-
})
|
|
1044
|
-
|
|
1045
|
-
api.on("resolve_exec_env", async (event, ctx) => {
|
|
1046
|
-
const sessionKey = sessionKeyOf(event, ctx)
|
|
1047
|
-
if (sessionKey === "") return
|
|
1048
|
-
const generation = await runHook("openclaw-generation",
|
|
1049
|
-
envelopeFor(sessionKey, event, ctx, "ResolveExecEnv"))
|
|
1050
|
-
if (generation === null || !/^[0-9a-f-]{36}$/i.test(generation.trim())) return
|
|
1051
|
-
return {
|
|
1052
|
-
NOTIFAI_ACTIVE_HARNESS: "openclaw",
|
|
1053
|
-
NOTIFAI_ACTIVE_SESSION_ID: sessionKey,
|
|
1054
|
-
NOTIFAI_ACTIVE_OPENCLAW_GENERATION: generation.trim(),
|
|
1055
|
-
}
|
|
1056
|
-
})
|
|
1057
|
-
}
|
|
1058
|
-
|
|
1059
|
-
export default {
|
|
1060
|
-
id: ${JSON.stringify(OPENCLAW_PLUGIN_ID)},
|
|
1061
|
-
name: "Notifai",
|
|
1062
|
-
register,
|
|
1063
|
-
}
|
|
1064
|
-
`;
|
|
1065
|
-
}
|
|
1066
|
-
export function isOurOpenclawPlugin(contents) {
|
|
1067
|
-
return contents.includes(OPENCLAW_PLUGIN_MARKER);
|
|
1068
|
-
}
|
|
1069
|
-
export function openclawPluginTarget(contents) {
|
|
1070
|
-
if (!isOurOpenclawPlugin(contents))
|
|
1071
|
-
return null;
|
|
1072
|
-
const adapter = /^const ADAPTER = (".*")$/m.exec(contents)?.[1];
|
|
1073
|
-
const node = /^const NODE = (".*")$/m.exec(contents)?.[1];
|
|
1074
|
-
const version = Number(/^const ADAPTER_VERSION = (\d+)$/m.exec(contents)?.[1] ?? 0);
|
|
1075
|
-
const timeoutMs = Number(/^const TIMEOUT_MS = (\d+)$/m.exec(contents)?.[1] ?? Number.NaN);
|
|
1076
|
-
if (adapter === undefined)
|
|
1077
|
-
return null;
|
|
1078
|
-
try {
|
|
1079
|
-
return {
|
|
1080
|
-
adapter: JSON.parse(adapter),
|
|
1081
|
-
current: version === OPENCLAW_ADAPTER_VERSION,
|
|
1082
|
-
...(Number.isFinite(timeoutMs) ? { timeoutSeconds: timeoutMs / 1000 } : {}),
|
|
1083
|
-
...(node === undefined ? {} : { nodePath: JSON.parse(node) }),
|
|
1084
|
-
};
|
|
1085
|
-
}
|
|
1086
|
-
catch {
|
|
1087
|
-
return null;
|
|
1088
|
-
}
|
|
1089
|
-
}
|
|
1090
|
-
function isJsonObject(value) {
|
|
1091
|
-
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
1092
|
-
}
|
|
1093
|
-
export function parseOpenclawConfig(source, configFile = 'OpenClaw config') {
|
|
1094
|
-
const errors = [];
|
|
1095
|
-
const parsed = parse(source, errors, { allowTrailingComma: true });
|
|
1096
|
-
if (errors.length > 0) {
|
|
1097
|
-
const problems = errors
|
|
1098
|
-
.map((error) => `${printParseErrorCode(error.error)} at offset ${error.offset}`)
|
|
1099
|
-
.join(', ');
|
|
1100
|
-
throw new Error(`${configFile} contains invalid JSONC (${problems}); refusing to write`);
|
|
1101
|
-
}
|
|
1102
|
-
if (isJsonObject(parsed))
|
|
1103
|
-
return parsed;
|
|
1104
|
-
throw new Error(`${configFile} is not a JSON object; refusing to write`);
|
|
1105
|
-
}
|
|
1106
|
-
function openclawFormattingOptions(source) {
|
|
1107
|
-
const indentation = /(?:^|\r?\n)([\t ]+)(?=")/.exec(source)?.[1] ?? ' ';
|
|
1108
|
-
return {
|
|
1109
|
-
insertSpaces: !indentation.includes('\t'),
|
|
1110
|
-
tabSize: indentation.length,
|
|
1111
|
-
eol: source.includes('\r\n') ? '\r\n' : '\n',
|
|
1112
|
-
};
|
|
1113
|
-
}
|
|
1114
|
-
/** Apply one JSONC edit while leaving unrelated source ranges intact. */
|
|
1115
|
-
export function editOpenclawConfigText(source, configFile, location, value) {
|
|
1116
|
-
parseOpenclawConfig(source, configFile);
|
|
1117
|
-
return applyEdits(source, modify(source, location, value, { formattingOptions: openclawFormattingOptions(source) }));
|
|
1118
|
-
}
|
|
1119
|
-
function objectAt(value) {
|
|
1120
|
-
return isJsonObject(value) ? value : null;
|
|
1121
|
-
}
|
|
1122
|
-
/** Install or refresh Notifai's entry with a JSONC parser edit. */
|
|
1123
|
-
export function enableOpenclawNotifaiConfigText(source, configFile) {
|
|
1124
|
-
const config = parseOpenclawConfig(source, configFile);
|
|
1125
|
-
const merged = mergeOpenclawNotifaiEntry(config);
|
|
1126
|
-
const mergedPlugins = objectAt(merged.plugins);
|
|
1127
|
-
const mergedEntries = objectAt(mergedPlugins['entries']);
|
|
1128
|
-
const plugins = objectAt(config.plugins);
|
|
1129
|
-
if (plugins === null) {
|
|
1130
|
-
return editOpenclawConfigText(source, configFile, ['plugins'], merged.plugins);
|
|
1131
|
-
}
|
|
1132
|
-
if (Object.hasOwn(plugins, 'entries') && !isJsonObject(plugins['entries'])) {
|
|
1133
|
-
return editOpenclawConfigText(source, configFile, ['plugins', 'entries'], mergedEntries);
|
|
1134
|
-
}
|
|
1135
|
-
return editOpenclawConfigText(source, configFile, ['plugins', 'entries', OPENCLAW_PLUGIN_ID], mergedEntries[OPENCLAW_PLUGIN_ID]);
|
|
1136
|
-
}
|
|
1137
|
-
// jsonc-parser's SyntaxKind is an ambient const enum, unavailable with
|
|
1138
|
-
// verbatimModuleSyntax. Keep the scanner token values local to this helper.
|
|
1139
|
-
const COMMA_TOKEN = 5;
|
|
1140
|
-
const LINE_COMMENT_TOKEN = 12;
|
|
1141
|
-
const BLOCK_COMMENT_TOKEN = 13;
|
|
1142
|
-
const EOF_TOKEN = 17;
|
|
1143
|
-
function jsoncGap(source, from, to) {
|
|
1144
|
-
const scanner = createScanner(source);
|
|
1145
|
-
scanner.setPosition(from);
|
|
1146
|
-
let comma = null;
|
|
1147
|
-
let hasComment = false;
|
|
1148
|
-
while (scanner.getPosition() < to) {
|
|
1149
|
-
const token = scanner.scan();
|
|
1150
|
-
const offset = scanner.getTokenOffset();
|
|
1151
|
-
if (offset >= to || token === EOF_TOKEN)
|
|
1152
|
-
break;
|
|
1153
|
-
if (token === COMMA_TOKEN)
|
|
1154
|
-
comma = offset;
|
|
1155
|
-
if (token === LINE_COMMENT_TOKEN || token === BLOCK_COMMENT_TOKEN) {
|
|
1156
|
-
hasComment = true;
|
|
1157
|
-
}
|
|
1158
|
-
}
|
|
1159
|
-
return { comma, hasComment };
|
|
1160
|
-
}
|
|
1161
|
-
function removeJsoncItem(source, path) {
|
|
1162
|
-
const root = parseTree(source);
|
|
1163
|
-
if (root === undefined)
|
|
1164
|
-
return source;
|
|
1165
|
-
const value = findNodeAtLocation(root, path);
|
|
1166
|
-
if (value === undefined)
|
|
1167
|
-
return source;
|
|
1168
|
-
const item = value.parent?.type === 'property' ? value.parent : value;
|
|
1169
|
-
const container = item.parent;
|
|
1170
|
-
const siblings = container?.children;
|
|
1171
|
-
if (container === undefined || siblings === undefined)
|
|
1172
|
-
return source;
|
|
1173
|
-
const index = siblings.indexOf(item);
|
|
1174
|
-
if (index < 0)
|
|
1175
|
-
return source;
|
|
1176
|
-
const end = item.offset + item.length;
|
|
1177
|
-
const ranges = [];
|
|
1178
|
-
if (index < siblings.length - 1) {
|
|
1179
|
-
const gap = jsoncGap(source, end, siblings[index + 1].offset);
|
|
1180
|
-
if (gap.comma === null)
|
|
1181
|
-
return source;
|
|
1182
|
-
if (gap.hasComment) {
|
|
1183
|
-
ranges.push([item.offset, end], [gap.comma, gap.comma + 1]);
|
|
1184
|
-
}
|
|
1185
|
-
else {
|
|
1186
|
-
ranges.push([item.offset, gap.comma + 1]);
|
|
1187
|
-
}
|
|
1188
|
-
}
|
|
1189
|
-
else {
|
|
1190
|
-
const trailing = jsoncGap(source, end, container.offset + container.length - 1);
|
|
1191
|
-
if (trailing.comma !== null)
|
|
1192
|
-
ranges.push([trailing.comma, trailing.comma + 1]);
|
|
1193
|
-
if (index > 0) {
|
|
1194
|
-
const previous = siblings[index - 1];
|
|
1195
|
-
const gap = jsoncGap(source, previous.offset + previous.length, item.offset);
|
|
1196
|
-
if (gap.comma === null)
|
|
1197
|
-
return source;
|
|
1198
|
-
if (gap.hasComment) {
|
|
1199
|
-
ranges.push([gap.comma, gap.comma + 1], [item.offset, end]);
|
|
1200
|
-
}
|
|
1201
|
-
else {
|
|
1202
|
-
ranges.push([gap.comma, end]);
|
|
1203
|
-
}
|
|
1204
|
-
}
|
|
1205
|
-
else {
|
|
1206
|
-
// An inserted sole property occupies a line of its own. Recover the
|
|
1207
|
-
// original commented-empty object by removing that added line.
|
|
1208
|
-
const lineStart = source.lastIndexOf('\n', item.offset - 1) + 1;
|
|
1209
|
-
const lineEndOffset = source.indexOf('\n', end);
|
|
1210
|
-
const lineEnd = lineEndOffset < 0 ? source.length : lineEndOffset;
|
|
1211
|
-
const onOwnLine = source.slice(lineStart, item.offset).trim() === '' &&
|
|
1212
|
-
source.slice(end, lineEnd).trim() === (trailing.comma === null ? '' : ',');
|
|
1213
|
-
const lineBreakStart = source[lineStart - 2] === '\r' ? lineStart - 2 : lineStart - 1;
|
|
1214
|
-
ranges.push([onOwnLine && lineStart > 0 ? lineBreakStart : item.offset, end]);
|
|
1215
|
-
}
|
|
1216
|
-
}
|
|
1217
|
-
let text = source;
|
|
1218
|
-
for (const [start, stop] of ranges.sort((a, b) => b[0] - a[0])) {
|
|
1219
|
-
text = text.slice(0, start) + text.slice(stop);
|
|
1220
|
-
}
|
|
1221
|
-
return text;
|
|
1222
|
-
}
|
|
1223
|
-
function removeOpenclawLoadPathText(source, pluginDir) {
|
|
1224
|
-
let text = source;
|
|
1225
|
-
for (;;) {
|
|
1226
|
-
const config = parseOpenclawConfig(text);
|
|
1227
|
-
const paths = objectAt(objectAt(config.plugins)?.['load'])?.['paths'];
|
|
1228
|
-
if (!Array.isArray(paths))
|
|
1229
|
-
return text;
|
|
1230
|
-
const index = paths.findIndex((value) => value === pluginDir);
|
|
1231
|
-
if (index < 0)
|
|
1232
|
-
return text;
|
|
1233
|
-
const next = removeJsoncItem(text, ['plugins', 'load', 'paths', index]);
|
|
1234
|
-
if (next === text)
|
|
1235
|
-
throw new Error('Could not remove the OpenClaw load path');
|
|
1236
|
-
text = next;
|
|
1237
|
-
}
|
|
1238
|
-
}
|
|
1239
|
-
/** Remove only the entry and explicit load path owned by this installation. */
|
|
1240
|
-
export function removeOpenclawNotifaiConfigText(source, configFile, pluginDir) {
|
|
1241
|
-
const config = parseOpenclawConfig(source, configFile);
|
|
1242
|
-
const entries = objectAt(objectAt(config.plugins)?.['entries']);
|
|
1243
|
-
const withoutEntry = entries !== null && Object.hasOwn(entries, OPENCLAW_PLUGIN_ID)
|
|
1244
|
-
? removeJsoncItem(source, ['plugins', 'entries', OPENCLAW_PLUGIN_ID])
|
|
1245
|
-
: source;
|
|
1246
|
-
return removeOpenclawLoadPathText(withoutEntry, pluginDir);
|
|
1247
|
-
}
|
|
1248
|
-
/** Remove one older Project load path while leaving the shared entry alone. */
|
|
1249
|
-
export function removeOpenclawLoadPathConfigText(source, configFile, pluginDir) {
|
|
1250
|
-
parseOpenclawConfig(source, configFile);
|
|
1251
|
-
return removeOpenclawLoadPathText(source, pluginDir);
|
|
1252
|
-
}
|
|
1253
|
-
export function openclawPluginLoadBlockers(config) {
|
|
1254
|
-
const plugins = objectAt(config.plugins);
|
|
1255
|
-
if (plugins === null)
|
|
1256
|
-
return [];
|
|
1257
|
-
const blockers = [];
|
|
1258
|
-
const allow = plugins['allow'];
|
|
1259
|
-
if (Array.isArray(allow) && allow.length > 0 && !allow.includes(OPENCLAW_PLUGIN_ID)) {
|
|
1260
|
-
blockers.push({ key: 'plugins.allow', action: 'add' });
|
|
1261
|
-
}
|
|
1262
|
-
const deny = plugins['deny'];
|
|
1263
|
-
if (Array.isArray(deny) && deny.includes(OPENCLAW_PLUGIN_ID)) {
|
|
1264
|
-
blockers.push({ key: 'plugins.deny', action: 'remove' });
|
|
1265
|
-
}
|
|
1266
|
-
return blockers;
|
|
1267
|
-
}
|
|
1268
|
-
export function openclawPluginLoadRemedy(blocker) {
|
|
1269
|
-
return blocker.action === 'add'
|
|
1270
|
-
? `add \"${OPENCLAW_PLUGIN_ID}\" to ${blocker.key}`
|
|
1271
|
-
: `remove \"${OPENCLAW_PLUGIN_ID}\" from ${blocker.key}`;
|
|
1272
|
-
}
|
|
1273
|
-
export function openclawPluginLoadWarning(blockers) {
|
|
1274
|
-
if (blockers.length === 0)
|
|
1275
|
-
return null;
|
|
1276
|
-
return `OpenClaw will not load Notifai: ${blockers.map(openclawPluginLoadRemedy).join('; ')}.`;
|
|
1277
|
-
}
|
|
1278
|
-
export function mergeOpenclawNotifaiEntry(config) {
|
|
1279
|
-
const plugins = config.plugins !== null && typeof config.plugins === 'object' && !Array.isArray(config.plugins)
|
|
1280
|
-
? { ...config.plugins }
|
|
1281
|
-
: {};
|
|
1282
|
-
const entries = plugins.entries !== null && typeof plugins.entries === 'object' && !Array.isArray(plugins.entries)
|
|
1283
|
-
? { ...plugins.entries }
|
|
1284
|
-
: {};
|
|
1285
|
-
const existing = entries[OPENCLAW_PLUGIN_ID] !== null &&
|
|
1286
|
-
typeof entries[OPENCLAW_PLUGIN_ID] === 'object' &&
|
|
1287
|
-
!Array.isArray(entries[OPENCLAW_PLUGIN_ID])
|
|
1288
|
-
? { ...entries[OPENCLAW_PLUGIN_ID] }
|
|
1289
|
-
: {};
|
|
1290
|
-
const hooks = existing.hooks !== null && typeof existing.hooks === 'object' && !Array.isArray(existing.hooks)
|
|
1291
|
-
? { ...existing.hooks }
|
|
1292
|
-
: {};
|
|
1293
|
-
entries[OPENCLAW_PLUGIN_ID] = {
|
|
1294
|
-
...existing,
|
|
1295
|
-
enabled: true,
|
|
1296
|
-
hooks: { ...hooks, allowConversationAccess: true },
|
|
1297
|
-
};
|
|
1298
|
-
plugins.entries = entries;
|
|
1299
|
-
return { ...config, plugins };
|
|
1300
|
-
}
|
|
1301
|
-
/**
|
|
1302
|
-
* Drop a Project-scoped plugin directory from OpenClaw's explicit load paths,
|
|
1303
|
-
* leaving the shared `entries` record — and therefore the Machine plugin —
|
|
1304
|
-
* exactly as it was.
|
|
1305
|
-
*/
|
|
1306
|
-
export function removeOpenclawLoadPath(config, pluginDir) {
|
|
1307
|
-
const currentPlugins = objectAt(config.plugins);
|
|
1308
|
-
const currentLoad = objectAt(currentPlugins?.['load']);
|
|
1309
|
-
if (currentPlugins === null || currentLoad === null || !Array.isArray(currentLoad['paths'])) {
|
|
1310
|
-
return config;
|
|
1311
|
-
}
|
|
1312
|
-
const load = { ...currentLoad };
|
|
1313
|
-
const originalPaths = currentLoad['paths'];
|
|
1314
|
-
const paths = originalPaths.filter((value) => value !== pluginDir);
|
|
1315
|
-
if (paths.length === originalPaths.length)
|
|
1316
|
-
return config;
|
|
1317
|
-
load.paths = paths;
|
|
1318
|
-
return { ...config, plugins: { ...currentPlugins, load } };
|
|
1319
|
-
}
|
|
1320
|
-
export function removeOpenclawNotifaiEntry(config, pluginDir) {
|
|
1321
|
-
if (!isJsonObject(config.plugins))
|
|
1322
|
-
return config;
|
|
1323
|
-
const currentPlugins = config.plugins;
|
|
1324
|
-
const currentEntries = objectAt(currentPlugins['entries']);
|
|
1325
|
-
const currentLoad = objectAt(currentPlugins['load']);
|
|
1326
|
-
const removesEntry = currentEntries !== null && Object.hasOwn(currentEntries, OPENCLAW_PLUGIN_ID);
|
|
1327
|
-
const removesLoadPath = currentLoad !== null &&
|
|
1328
|
-
Array.isArray(currentLoad['paths']) &&
|
|
1329
|
-
currentLoad['paths'].includes(pluginDir);
|
|
1330
|
-
if (!removesEntry && !removesLoadPath)
|
|
1331
|
-
return config;
|
|
1332
|
-
const plugins = { ...currentPlugins };
|
|
1333
|
-
if (removesEntry) {
|
|
1334
|
-
const entries = { ...currentEntries };
|
|
1335
|
-
delete entries[OPENCLAW_PLUGIN_ID];
|
|
1336
|
-
plugins.entries = entries;
|
|
1337
|
-
}
|
|
1338
|
-
if (removesLoadPath) {
|
|
1339
|
-
plugins.load = {
|
|
1340
|
-
...currentLoad,
|
|
1341
|
-
paths: currentLoad['paths'].filter((value) => value !== pluginDir),
|
|
1342
|
-
};
|
|
1343
|
-
}
|
|
1344
|
-
return { ...config, plugins };
|
|
1345
|
-
}
|
|
1346
|
-
/** Config-directory presence is not ownership. These files mean OpenClaw is actually configured. */
|
|
1347
|
-
export function openclawHasGlobalEvidence(exists, env = process.env, platform = process.platform) {
|
|
1348
|
-
const state = openclawStateDir(env, platform);
|
|
1349
|
-
return (exists(openclawConfigPath(env, platform)) ||
|
|
1350
|
-
exists(path.join(state, 'agents')) ||
|
|
1351
|
-
exists(path.join(state, 'workspace')));
|
|
1352
|
-
}
|
|
1353
|
-
//# sourceMappingURL=openclaw-plugin.js.map
|