@raidiant/notifai 11.4.1 → 11.5.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/CHANGELOG.md +16 -0
- package/README.md +7 -3
- package/dist/codex-tool-messages.d.ts +17 -0
- package/dist/codex-tool-messages.js +142 -0
- package/dist/codex-tool-messages.js.map +1 -0
- package/dist/commands-acknowledge.js +8 -0
- package/dist/commands-acknowledge.js.map +1 -1
- package/dist/commands-ask.js +14 -2
- package/dist/commands-ask.js.map +1 -1
- package/dist/commands-doctor.js +140 -36
- package/dist/commands-doctor.js.map +1 -1
- package/dist/commands-harness-context.js +3 -0
- package/dist/commands-harness-context.js.map +1 -1
- package/dist/commands-hook-attend.d.ts +2 -0
- package/dist/commands-hook-attend.js +70 -22
- package/dist/commands-hook-attend.js.map +1 -1
- package/dist/commands-hook-diagnostics.d.ts +11 -1
- package/dist/commands-hook-diagnostics.js +40 -1
- package/dist/commands-hook-diagnostics.js.map +1 -1
- package/dist/commands-hook-install.js +162 -21
- package/dist/commands-hook-install.js.map +1 -1
- package/dist/commands-hook-run.js +216 -8
- package/dist/commands-hook-run.js.map +1 -1
- package/dist/commands-hook-shape.d.ts +4 -3
- package/dist/commands-hook-shape.js +5 -4
- package/dist/commands-hook-shape.js.map +1 -1
- package/dist/commands-send.js +29 -0
- package/dist/commands-send.js.map +1 -1
- package/dist/harnesses.d.ts +8 -9
- package/dist/harnesses.js +25 -7
- package/dist/harnesses.js.map +1 -1
- package/dist/hermes-plugin.d.ts +11 -0
- package/dist/hermes-plugin.js +161 -0
- package/dist/hermes-plugin.js.map +1 -0
- package/dist/hook-events.d.ts +9 -2
- package/dist/hook-events.js +16 -6
- package/dist/hook-events.js.map +1 -1
- package/dist/hook-lifecycle.js +6 -0
- package/dist/hook-lifecycle.js.map +1 -1
- package/dist/hook-session-state.d.ts +4 -1
- package/dist/hook-session-state.js +16 -4
- package/dist/hook-session-state.js.map +1 -1
- package/dist/hook-types.d.ts +20 -0
- package/dist/install-hooks.js +51 -4
- package/dist/install-hooks.js.map +1 -1
- package/dist/openclaw-continuation-bridge.d.ts +7 -0
- package/dist/openclaw-continuation-bridge.js +64 -0
- package/dist/openclaw-continuation-bridge.js.map +1 -0
- package/dist/openclaw-gateway-readiness.d.ts +1 -0
- package/dist/openclaw-gateway-readiness.js +24 -0
- package/dist/openclaw-gateway-readiness.js.map +1 -0
- package/dist/openclaw-generation.d.ts +17 -0
- package/dist/openclaw-generation.js +93 -0
- package/dist/openclaw-generation.js.map +1 -0
- package/dist/openclaw-pending.d.ts +8 -0
- package/dist/openclaw-pending.js +52 -0
- package/dist/openclaw-pending.js.map +1 -0
- package/dist/openclaw-plugin.d.ts +22 -4
- package/dist/openclaw-plugin.js +701 -87
- package/dist/openclaw-plugin.js.map +1 -1
- package/dist/openclaw-session-access.d.ts +10 -0
- package/dist/openclaw-session-access.js +69 -0
- package/dist/openclaw-session-access.js.map +1 -0
- package/dist/program.js +6 -6
- package/dist/program.js.map +1 -1
- package/dist/session-activation.js +4 -4
- package/dist/session-activation.js.map +1 -1
- package/dist/session-attendant-probe.d.ts +10 -0
- package/dist/session-attendant-probe.js +31 -0
- package/dist/session-attendant-probe.js.map +1 -1
- package/dist/session-attendant-state.d.ts +2 -0
- package/dist/session-attendant-state.js +6 -0
- package/dist/session-attendant-state.js.map +1 -1
- package/dist/session-delivery.d.ts +2 -0
- package/dist/session-delivery.js +2 -0
- package/dist/session-delivery.js.map +1 -1
- package/dist/session-handoff.d.ts +1 -1
- package/dist/session-message-handoff.d.ts +2 -0
- package/dist/session-message-handoff.js +1 -0
- package/dist/session-message-handoff.js.map +1 -1
- package/dist/skill-source/manifest.json +6 -6
- package/dist/skill-source/notifai/SKILL.md +2 -2
- package/dist/skill-source/notifai/references/harness-setup.md +51 -18
- package/dist/skill-source/notifai/references/notes-and-edits.md +6 -1
- package/dist/skill-source/notifai/references/updates.md +6 -6
- package/dist/update-handoff.d.ts +1 -1
- package/dist/update-handoff.js +1 -0
- package/dist/update-handoff.js.map +1 -1
- package/package.json +3 -2
package/dist/openclaw-plugin.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
|
+
import { applyEdits, createScanner, findNodeAtLocation, modify, parse, parseTree, printParseErrorCode, } from 'jsonc-parser';
|
|
2
3
|
import { hookHostPlatform } from './hook-adapter.js';
|
|
3
4
|
import { harnessAccountHome } from './install-hooks.js';
|
|
4
5
|
import { MISSING_LIFECYCLE_GUIDANCE_CONTEXT, WORKER_ACTIVATION_CONTEXT, } from './session-activation.js';
|
|
@@ -10,21 +11,23 @@ import { MISSING_LIFECYCLE_GUIDANCE_CONTEXT, WORKER_ACTIVATION_CONTEXT, } from '
|
|
|
10
11
|
* commands every other harness invokes, so presence, activation, and
|
|
11
12
|
* retirement stay in the CLI.
|
|
12
13
|
*
|
|
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)
|
|
14
17
|
* message_received -> user-prompt-submit (User is present)
|
|
15
18
|
* agent_end -> stop (the turn ended)
|
|
16
19
|
* session_end -> session-end (retire local state)
|
|
17
20
|
* resolve_exec_env -> NOTIFAI_ACTIVE_* markers (exact Source Context)
|
|
18
21
|
*
|
|
19
|
-
* `command:stop` is a User abort and is deliberately not wired.
|
|
20
|
-
*
|
|
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.
|
|
21
24
|
*/
|
|
22
25
|
export const OPENCLAW_PLUGIN_MARKER = '// notifai managed openclaw plugin';
|
|
23
26
|
export const OPENCLAW_PLUGIN_ID = 'notifai';
|
|
24
27
|
export const OPENCLAW_PLUGIN_FILENAME = 'index.js';
|
|
25
28
|
export const OPENCLAW_PLUGIN_MANIFEST = 'openclaw.plugin.json';
|
|
26
29
|
export const OPENCLAW_PLUGIN_PACKAGE = 'package.json';
|
|
27
|
-
const OPENCLAW_ADAPTER_VERSION =
|
|
30
|
+
const OPENCLAW_ADAPTER_VERSION = 4;
|
|
28
31
|
export function openclawStateDir(env = process.env, platform = process.platform) {
|
|
29
32
|
const override = env['OPENCLAW_STATE_DIR'];
|
|
30
33
|
if (override !== undefined && override !== '')
|
|
@@ -54,6 +57,403 @@ export function legacyOpenclawProjectPluginDir(cwd) {
|
|
|
54
57
|
export function legacyOpenclawProjectPluginPath(cwd) {
|
|
55
58
|
return path.join(legacyOpenclawProjectPluginDir(cwd), OPENCLAW_PLUGIN_FILENAME);
|
|
56
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
|
+
|
|
66
|
+
function readinessPath() {
|
|
67
|
+
return path.join(path.dirname(JOURNAL_DIR), "continuation-ready.json")
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function writeReadiness(target) {
|
|
71
|
+
const start = execFileSync("ps", ["-o", "lstart=", "-p", String(process.pid)], {
|
|
72
|
+
encoding: "utf8", timeout: 2_000,
|
|
73
|
+
env: { PATH: process.env.PATH ?? "/bin:/usr/bin", TZ: "UTC", LC_ALL: "C" },
|
|
74
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
75
|
+
}).trim().replace(/\\s+/g, " ")
|
|
76
|
+
if (start === "") return false
|
|
77
|
+
const file = readinessPath()
|
|
78
|
+
mkdirSync(path.dirname(file), { recursive: true, mode: 0o700 })
|
|
79
|
+
const temp = file + "." + randomUUID() + ".tmp"
|
|
80
|
+
writeFileSync(temp, JSON.stringify({ pid: process.pid, start,
|
|
81
|
+
script: target.script, script_mtime: statSync(target.script).mtimeMs }) + "\\n",
|
|
82
|
+
{ mode: 0o600, flag: "wx" })
|
|
83
|
+
renameSync(temp, file)
|
|
84
|
+
return true
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function clearReadiness() {
|
|
88
|
+
try { unlinkSync(readinessPath()) } catch { /* Already absent. */ }
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function journalPath(deliveryId) {
|
|
92
|
+
return path.join(JOURNAL_DIR, deliveryId + ".json")
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function saveJournal(record) {
|
|
96
|
+
mkdirSync(JOURNAL_DIR, { recursive: true, mode: 0o700 })
|
|
97
|
+
const file = journalPath(record.delivery_id)
|
|
98
|
+
const temp = file + "." + randomUUID() + ".tmp"
|
|
99
|
+
writeFileSync(temp, JSON.stringify(record) + "\\n", { mode: 0o600, flag: "wx" })
|
|
100
|
+
const fd = openSync(temp, "r")
|
|
101
|
+
try { fsyncSync(fd) } finally { closeSync(fd) }
|
|
102
|
+
renameSync(temp, file)
|
|
103
|
+
try {
|
|
104
|
+
const directory = openSync(JOURNAL_DIR, "r")
|
|
105
|
+
try { fsyncSync(directory) } finally { closeSync(directory) }
|
|
106
|
+
} catch { /* Directory fsync is unavailable on some hosts. */ }
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function readJournals() {
|
|
110
|
+
try {
|
|
111
|
+
return readdirSync(JOURNAL_DIR).filter((name) => name.endsWith(".json"))
|
|
112
|
+
.flatMap((name) => {
|
|
113
|
+
try {
|
|
114
|
+
const record = JSON.parse(readFileSync(path.join(JOURNAL_DIR, name), "utf8"))
|
|
115
|
+
return record && typeof record.delivery_id === "string" ? [record] : []
|
|
116
|
+
} catch { return [] }
|
|
117
|
+
})
|
|
118
|
+
} catch { return [] }
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function existingJournal(deliveryId) {
|
|
122
|
+
try {
|
|
123
|
+
return JSON.parse(readFileSync(journalPath(deliveryId), "utf8"))
|
|
124
|
+
} catch { return null }
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function openclawCliTarget(config) {
|
|
128
|
+
if (process.env.OPENCLAW_SHELL === "exec") return null
|
|
129
|
+
const gateway = config?.gateway ?? {}
|
|
130
|
+
if (gateway.mode === "remote" || (gateway.bind !== undefined && gateway.bind !== "loopback")) return null
|
|
131
|
+
let script
|
|
132
|
+
try { script = realpathSync(process.argv[1]) } catch { return null }
|
|
133
|
+
let folder = path.dirname(script)
|
|
134
|
+
let packageVersion = null
|
|
135
|
+
for (;;) {
|
|
136
|
+
try {
|
|
137
|
+
const manifest = JSON.parse(readFileSync(path.join(folder, "package.json"), "utf8"))
|
|
138
|
+
if (manifest.name === "openclaw" && typeof manifest.version === "string") {
|
|
139
|
+
packageVersion = manifest.version
|
|
140
|
+
break
|
|
141
|
+
}
|
|
142
|
+
} catch { /* Keep walking within this executable's ancestors. */ }
|
|
143
|
+
const parent = path.dirname(folder)
|
|
144
|
+
if (parent === folder) return null
|
|
145
|
+
folder = parent
|
|
146
|
+
}
|
|
147
|
+
const explicit = []
|
|
148
|
+
for (let i = 0; i < process.argv.length - 1; i += 1) {
|
|
149
|
+
if (process.argv[i] === "--port") explicit.push(Number(process.argv[i + 1]))
|
|
150
|
+
}
|
|
151
|
+
if (explicit.length > 1) return null
|
|
152
|
+
const configured = Number(gateway.port ?? process.env.OPENCLAW_GATEWAY_PORT ?? 18789)
|
|
153
|
+
const port = explicit[0] ?? configured
|
|
154
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535) return null
|
|
155
|
+
return { script, port, packageVersion }
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function openclawExec(target, args) {
|
|
159
|
+
return new Promise((resolve, reject) => {
|
|
160
|
+
execFile(process.execPath, [target.script, ...args], {
|
|
161
|
+
shell: false, windowsHide: true, timeout: 20_000, maxBuffer: 2_000_000,
|
|
162
|
+
env: process.env,
|
|
163
|
+
}, (error, stdout) => {
|
|
164
|
+
if (error) reject(new Error("OpenClaw CLI call failed"))
|
|
165
|
+
else resolve(stdout.trim())
|
|
166
|
+
})
|
|
167
|
+
})
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
async function gatewayCall(target, method, params) {
|
|
171
|
+
const output = await openclawExec(target, ["gateway", "call", method,
|
|
172
|
+
"--port", String(target.port), "--params", JSON.stringify(params), "--json"])
|
|
173
|
+
return JSON.parse(output)
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
async function verifiedGatewayTarget(config, logger) {
|
|
177
|
+
const target = openclawCliTarget(config)
|
|
178
|
+
if (target === null) {
|
|
179
|
+
logger?.warn?.("notifai continuation target: local CLI path or Gateway bind unavailable")
|
|
180
|
+
return null
|
|
181
|
+
}
|
|
182
|
+
try {
|
|
183
|
+
const banner = (await openclawExec(target, ["--version"])).trim()
|
|
184
|
+
const cliVersion = /^OpenClaw ([0-9]+\\.[0-9]+\\.[0-9]+)(?: \\([0-9a-f]+\\))?$/.exec(banner)?.[1]
|
|
185
|
+
const status = await gatewayCall(target, "status", {})
|
|
186
|
+
if (cliVersion !== target.packageVersion || status?.runtimeVersion !== cliVersion) {
|
|
187
|
+
logger?.warn?.("notifai continuation target: CLI and Gateway versions differ")
|
|
188
|
+
return null
|
|
189
|
+
}
|
|
190
|
+
if (status?.pid !== process.pid) {
|
|
191
|
+
logger?.warn?.("notifai continuation target: Gateway process identity differs")
|
|
192
|
+
return null
|
|
193
|
+
}
|
|
194
|
+
return target
|
|
195
|
+
} catch {
|
|
196
|
+
logger?.warn?.("notifai continuation target: local CLI status unavailable")
|
|
197
|
+
return null
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function submissionKey(record) {
|
|
202
|
+
return "notifai-" + record.delivery_id + "-a" + record.attempt
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function submissionIdentity(message) {
|
|
206
|
+
const meta = message?.metadata ?? message?.__openclaw ?? {}
|
|
207
|
+
return message?.idempotencyKey ?? meta.idempotencyKey ?? null
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
async function inspectSubmission(target, record) {
|
|
211
|
+
let offset = 0
|
|
212
|
+
let before
|
|
213
|
+
let pendingFinished = false
|
|
214
|
+
let interrupted = false
|
|
215
|
+
for (let page = 0; page < 1000; page += 1) {
|
|
216
|
+
const params = { sessionKey: record.session_key, limit: 200, offset,
|
|
217
|
+
...(before === undefined ? {} : { pendingBefore: before }) }
|
|
218
|
+
const history = await gatewayCall(target, "chat.history", params)
|
|
219
|
+
if (history?.kind === "reset" || history?.truncationReason ||
|
|
220
|
+
(record.openclaw_session_id && history?.sessionInfo?.sessionId &&
|
|
221
|
+
history.sessionInfo.sessionId !== record.openclaw_session_id)) return "unconfirmed"
|
|
222
|
+
const messages = history?.messages
|
|
223
|
+
if (!Array.isArray(messages)) return "unconfirmed"
|
|
224
|
+
if (messages.some((entry) => entry?.role === "user" &&
|
|
225
|
+
submissionIdentity(entry) === submissionKey(record) + ":user")) return "transcript"
|
|
226
|
+
const pending = history?.pendingInputs
|
|
227
|
+
if (!pending || !Array.isArray(pending.items) ||
|
|
228
|
+
!Number.isInteger(pending.total) || pending.total < pending.items.length ||
|
|
229
|
+
typeof history.hasMore !== "boolean") return "unconfirmed"
|
|
230
|
+
for (const item of pending.items) {
|
|
231
|
+
if (item?.runId !== submissionKey(record) &&
|
|
232
|
+
item?.idempotencyKey !== submissionKey(record)) continue
|
|
233
|
+
if (item.state === "queued" || item.state === "running") return "pending"
|
|
234
|
+
if (item.state === "interrupted" || item.state === "cancelled") interrupted = true
|
|
235
|
+
else return "unconfirmed"
|
|
236
|
+
}
|
|
237
|
+
const nextOffset = history.nextOffset
|
|
238
|
+
const hasMore = history.hasMore === true
|
|
239
|
+
const nextBefore = pending.nextBefore
|
|
240
|
+
if (!pendingFinished && typeof nextBefore === "string" && nextBefore !== before) {
|
|
241
|
+
before = nextBefore
|
|
242
|
+
continue
|
|
243
|
+
}
|
|
244
|
+
if (!pendingFinished && pending.total > pending.items.length) return "unconfirmed"
|
|
245
|
+
pendingFinished = true
|
|
246
|
+
if (hasMore && Number.isInteger(nextOffset) && nextOffset > offset) {
|
|
247
|
+
offset = nextOffset
|
|
248
|
+
before = undefined
|
|
249
|
+
continue
|
|
250
|
+
}
|
|
251
|
+
if (hasMore) return "unconfirmed"
|
|
252
|
+
return interrupted ? "interrupted" : "absent"
|
|
253
|
+
}
|
|
254
|
+
return "unconfirmed"
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function pointerMessage(record) {
|
|
258
|
+
return "A Notifai reply is waiting for this session. " +
|
|
259
|
+
record.request_ids.map((id) => "Run notifai replies " + id +
|
|
260
|
+
" --json, then notifai acknowledge " + id +
|
|
261
|
+
" with the concrete work you will do.").join(" ")
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function spawnOwnedHook(event, envelope, extraFds = false) {
|
|
265
|
+
const child = spawn(HOOK_COMMAND, [...HOOK_PREFIX, "hook", event,
|
|
266
|
+
"--owner", "notifai", "--harness", "openclaw"], {
|
|
267
|
+
cwd: envelope.cwd,
|
|
268
|
+
env: { ...process.env, NOTIFAI_HOOK_SOURCE_PID: String(process.pid) },
|
|
269
|
+
shell: false, windowsHide: true,
|
|
270
|
+
stdio: extraFds ? ["pipe", "ignore", "ignore", "pipe", "pipe"] : ["pipe", "ignore", "ignore"],
|
|
271
|
+
})
|
|
272
|
+
child.stdin.end(JSON.stringify(envelope))
|
|
273
|
+
return child
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function makeContinuationService(config, logger) {
|
|
277
|
+
const active = new Map()
|
|
278
|
+
const delivering = new Set()
|
|
279
|
+
const reported = new Map()
|
|
280
|
+
let stopped = false
|
|
281
|
+
let timer = null
|
|
282
|
+
let scanning = false
|
|
283
|
+
let ready = false
|
|
284
|
+
|
|
285
|
+
function report(record, reason) {
|
|
286
|
+
if (reported.get(record.delivery_id) === reason) return
|
|
287
|
+
reported.set(record.delivery_id, reason)
|
|
288
|
+
logger?.warn?.("notifai continuation " + record.delivery_id + ": " + reason)
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
async function deliver(record) {
|
|
292
|
+
if (delivering.has(record.delivery_id) || record.phase === "transcript") return
|
|
293
|
+
delivering.add(record.delivery_id)
|
|
294
|
+
try {
|
|
295
|
+
const envelope = { session_id: record.session_key, cwd: record.cwd,
|
|
296
|
+
openclaw_session_id: record.openclaw_session_id }
|
|
297
|
+
const generation = (await runHook("openclaw-generation", envelope))?.trim()
|
|
298
|
+
if (generation !== record.generation) { report(record, "generation-fenced"); return }
|
|
299
|
+
if (record.phase === "prepared") {
|
|
300
|
+
const proved = await runHook("openclaw-verify-prepared", {
|
|
301
|
+
...envelope, openclaw_request_ids: record.request_ids })
|
|
302
|
+
if (proved?.trim() !== "committed") return
|
|
303
|
+
record = { ...record, phase: "committed" }
|
|
304
|
+
saveJournal(record)
|
|
305
|
+
}
|
|
306
|
+
const target = await verifiedGatewayTarget(config, logger)
|
|
307
|
+
if (target === null) { report(record, "gateway-target-unverified"); return }
|
|
308
|
+
const state = await inspectSubmission(target, record)
|
|
309
|
+
if (state === "unconfirmed") { report(record, "history-unconfirmed"); return }
|
|
310
|
+
if (state === "transcript") {
|
|
311
|
+
saveJournal({ ...record, phase: "transcript" })
|
|
312
|
+
return
|
|
313
|
+
}
|
|
314
|
+
if (state === "pending") {
|
|
315
|
+
if (record.phase !== "admitted") saveJournal({ ...record, phase: "admitted" })
|
|
316
|
+
return
|
|
317
|
+
}
|
|
318
|
+
if (state === "interrupted" || (state === "absent" && record.phase === "admitted")) {
|
|
319
|
+
record = { ...record, attempt: record.attempt + 1, phase: "committed" }
|
|
320
|
+
saveJournal(record)
|
|
321
|
+
}
|
|
322
|
+
if (state !== "absent" && state !== "interrupted") return
|
|
323
|
+
saveJournal({ ...record, phase: "submitting" })
|
|
324
|
+
report(record, "submitting-pointer")
|
|
325
|
+
await gatewayCall(target, "chat.send", { sessionKey: record.session_key,
|
|
326
|
+
message: pointerMessage(record), queueMode: "followup",
|
|
327
|
+
idempotencyKey: submissionKey(record) })
|
|
328
|
+
saveJournal({ ...record, phase: "admitted" })
|
|
329
|
+
report(record, "pointer-admitted")
|
|
330
|
+
} catch {
|
|
331
|
+
// An uncertain RPC is reconciled by identity before any retry.
|
|
332
|
+
report(record, "rpc-or-journal-unconfirmed")
|
|
333
|
+
} finally {
|
|
334
|
+
delivering.delete(record.delivery_id)
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function startSettlement(session, entry) {
|
|
339
|
+
const envelope = { session_id: session.session_key, cwd: session.cwd,
|
|
340
|
+
openclaw_session_id: session.session_id, hook_event_name: "SessionStart" }
|
|
341
|
+
const child = spawnOwnedHook("openclaw-settlement", envelope, true)
|
|
342
|
+
entry.child = child
|
|
343
|
+
let buffer = ""
|
|
344
|
+
let prepared = null
|
|
345
|
+
child.stdio[3].setEncoding("utf8")
|
|
346
|
+
child.stdio[3].on("data", (chunk) => {
|
|
347
|
+
buffer += chunk
|
|
348
|
+
for (;;) {
|
|
349
|
+
const end = buffer.indexOf("\\n")
|
|
350
|
+
if (end < 0) break
|
|
351
|
+
const line = buffer.slice(0, end)
|
|
352
|
+
buffer = buffer.slice(end + 1)
|
|
353
|
+
let payload
|
|
354
|
+
try { payload = JSON.parse(line) } catch { continue }
|
|
355
|
+
if (payload.type === "prepare") {
|
|
356
|
+
const ids = payload.request_ids
|
|
357
|
+
if (payload.session_key !== session.session_key ||
|
|
358
|
+
payload.generation !== session.generation || !Array.isArray(ids) ||
|
|
359
|
+
ids.length < 1 || ids.some((id) => typeof id !== "string" ||
|
|
360
|
+
!/^req_[A-Za-z0-9_-]+$/.test(id))) continue
|
|
361
|
+
const deliveryId = createHash("sha256")
|
|
362
|
+
.update(session.generation + "\\0" + ids.join("\\0"))
|
|
363
|
+
.digest("hex").slice(0, 32)
|
|
364
|
+
const existing = existingJournal(deliveryId)
|
|
365
|
+
if (existing === null && existsSync(journalPath(deliveryId))) continue
|
|
366
|
+
if (existing !== null && (existing.session_key !== session.session_key ||
|
|
367
|
+
existing.generation !== session.generation ||
|
|
368
|
+
JSON.stringify(existing.request_ids) !== JSON.stringify(ids))) continue
|
|
369
|
+
prepared = existing ?? { delivery_id: deliveryId, session_key: session.session_key,
|
|
370
|
+
cwd: session.cwd, generation: session.generation,
|
|
371
|
+
openclaw_session_id: session.session_id, request_ids: ids,
|
|
372
|
+
attempt: 1, phase: "prepared" }
|
|
373
|
+
if (existing === null) saveJournal(prepared)
|
|
374
|
+
child.stdio[4].write('{"type":"prepared"}\\n')
|
|
375
|
+
} else if (payload.type === "committed" && prepared !== null) {
|
|
376
|
+
if (prepared.phase === "prepared") {
|
|
377
|
+
prepared = { ...prepared, phase: "committed" }
|
|
378
|
+
saveJournal(prepared)
|
|
379
|
+
}
|
|
380
|
+
void deliver(prepared)
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
})
|
|
384
|
+
child.on("close", () => {
|
|
385
|
+
const current = active.get(session.session_key)
|
|
386
|
+
if (current?.child === child) {
|
|
387
|
+
current.child = null
|
|
388
|
+
}
|
|
389
|
+
})
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
function startSession(session) {
|
|
393
|
+
const envelope = { session_id: session.session_key, cwd: session.cwd,
|
|
394
|
+
openclaw_session_id: session.session_id, hook_event_name: "SessionStart" }
|
|
395
|
+
const entry = { generation: session.generation,
|
|
396
|
+
attendant: spawnOwnedHook("attend", envelope), child: null }
|
|
397
|
+
active.set(session.session_key, entry)
|
|
398
|
+
startSettlement(session, entry)
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
async function tick() {
|
|
402
|
+
if (stopped || scanning) return
|
|
403
|
+
scanning = true
|
|
404
|
+
try {
|
|
405
|
+
if (!ready) {
|
|
406
|
+
const target = await verifiedGatewayTarget(config, logger)
|
|
407
|
+
if (target === null || !writeReadiness(target)) return
|
|
408
|
+
ready = true
|
|
409
|
+
}
|
|
410
|
+
const raw = await runHook("openclaw-list-pending", { cwd: process.cwd() })
|
|
411
|
+
const pending = raw === null ? [] : JSON.parse(raw)
|
|
412
|
+
if (Array.isArray(pending)) for (const session of pending) {
|
|
413
|
+
if (typeof session?.session_key !== "string" ||
|
|
414
|
+
typeof session?.cwd !== "string" ||
|
|
415
|
+
typeof session?.generation !== "string") continue
|
|
416
|
+
const current = active.get(session.session_key)
|
|
417
|
+
if (current?.generation === session.generation) {
|
|
418
|
+
if (current.attendant.exitCode !== null) {
|
|
419
|
+
active.delete(session.session_key)
|
|
420
|
+
if (current.child !== null) current.child.kill("SIGTERM")
|
|
421
|
+
startSession(session)
|
|
422
|
+
} else if (current.child === null) {
|
|
423
|
+
const ready = await runHook("openclaw-attendance-ready", {
|
|
424
|
+
session_id: session.session_key, cwd: session.cwd,
|
|
425
|
+
openclaw_session_id: session.session_id })
|
|
426
|
+
if (ready?.trim() === "ready") startSettlement(session, current)
|
|
427
|
+
}
|
|
428
|
+
continue
|
|
429
|
+
}
|
|
430
|
+
if (current) {
|
|
431
|
+
if (current.child !== null) current.child.kill("SIGTERM")
|
|
432
|
+
current.attendant.kill("SIGTERM")
|
|
433
|
+
}
|
|
434
|
+
startSession(session)
|
|
435
|
+
}
|
|
436
|
+
for (const record of readJournals()) void deliver(record)
|
|
437
|
+
} catch { /* The next Gateway tick retries discovery. */ }
|
|
438
|
+
finally { scanning = false }
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
return {
|
|
442
|
+
start() { timer = setInterval(() => { void tick() }, 2_000); void tick() },
|
|
443
|
+
stop() {
|
|
444
|
+
stopped = true
|
|
445
|
+
clearReadiness()
|
|
446
|
+
if (timer !== null) clearInterval(timer)
|
|
447
|
+
for (const entry of active.values()) {
|
|
448
|
+
if (entry.child !== null) entry.child.kill("SIGTERM")
|
|
449
|
+
entry.attendant.kill("SIGTERM")
|
|
450
|
+
}
|
|
451
|
+
active.clear()
|
|
452
|
+
},
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
`;
|
|
456
|
+
}
|
|
57
457
|
export function openclawPluginManifest() {
|
|
58
458
|
return `${JSON.stringify({
|
|
59
459
|
id: OPENCLAW_PLUGIN_ID,
|
|
@@ -84,9 +484,14 @@ export function openclawPluginSource(options) {
|
|
|
84
484
|
return `${OPENCLAW_PLUGIN_MARKER}
|
|
85
485
|
// Generated by \`notifai hooks install --harness openclaw\`. Edits are lost on
|
|
86
486
|
// the next install; change the CLI instead, which is where the logic lives.
|
|
87
|
-
import { spawn } from "node:child_process"
|
|
487
|
+
import { spawn, execFile, execFileSync } from "node:child_process"
|
|
488
|
+
import { createHash, randomUUID } from "node:crypto"
|
|
489
|
+
import { closeSync, existsSync, fsyncSync, mkdirSync, openSync, readFileSync, readdirSync, realpathSync, renameSync, statSync, unlinkSync, writeFileSync } from "node:fs"
|
|
490
|
+
import path from "node:path"
|
|
88
491
|
|
|
89
492
|
${nodeConstant}const ADAPTER = ${JSON.stringify(adapterPath)}
|
|
493
|
+
const HOOK_COMMAND = ${JSON.stringify(win32 ? options.nodePath ?? process.execPath : adapterPath)}
|
|
494
|
+
const HOOK_PREFIX = ${JSON.stringify(win32 ? [adapterPath] : [])}
|
|
90
495
|
const TIMEOUT_MS = ${timeoutSeconds * 1000}
|
|
91
496
|
const ADAPTER_VERSION = ${OPENCLAW_ADAPTER_VERSION}
|
|
92
497
|
const MISSING_LIFECYCLE_GUIDANCE_CONTEXT = ${JSON.stringify(MISSING_LIFECYCLE_GUIDANCE_CONTEXT)}
|
|
@@ -140,6 +545,21 @@ function sessionKeyOf(event, ctx) {
|
|
|
140
545
|
return typeof key === "string" ? key.trim() : ""
|
|
141
546
|
}
|
|
142
547
|
|
|
548
|
+
function sessionIdOf(event, ctx) {
|
|
549
|
+
const id = event?.sessionId ?? ctx?.sessionId
|
|
550
|
+
return typeof id === "string" && id.length > 0 ? id : undefined
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
function resumedFromOf(event) {
|
|
554
|
+
return typeof event?.resumedFrom === "string" && event.resumedFrom.length > 0
|
|
555
|
+
? event.resumedFrom : undefined
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
function reasonOf(event) {
|
|
559
|
+
return typeof event?.reason === "string" && event.reason.length > 0
|
|
560
|
+
? event.reason : undefined
|
|
561
|
+
}
|
|
562
|
+
|
|
143
563
|
function workspaceDirOf(event, ctx) {
|
|
144
564
|
const dir = ctx?.workspaceDir ?? event?.workspaceDir ?? event?.context?.workspaceDir
|
|
145
565
|
return typeof dir === "string" && dir.length > 0 ? dir : process.cwd()
|
|
@@ -162,18 +582,65 @@ function isUserMessage(event, ctx) {
|
|
|
162
582
|
return true
|
|
163
583
|
}
|
|
164
584
|
|
|
585
|
+
function envelopeFor(sessionKey, event, ctx, hookEventName) {
|
|
586
|
+
return {
|
|
587
|
+
session_id: sessionKey,
|
|
588
|
+
cwd: workspaceDirOf(event, ctx),
|
|
589
|
+
hook_event_name: hookEventName,
|
|
590
|
+
openclaw_session_id: sessionIdOf(event, ctx),
|
|
591
|
+
openclaw_reason: reasonOf(event),
|
|
592
|
+
openclaw_resumed_from: resumedFromOf(event),
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
function onIfSupported(api, name, handler) {
|
|
597
|
+
try {
|
|
598
|
+
api.on(name, handler)
|
|
599
|
+
} catch {
|
|
600
|
+
// Older OpenClaw builds may reject a typed hook. The prompt fallback
|
|
601
|
+
// remains available, while unobserved rotations cannot be guessed.
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
${openclawContinuationServiceSource()}
|
|
606
|
+
|
|
165
607
|
function register(api) {
|
|
608
|
+
api.registerService?.({
|
|
609
|
+
id: "notifai-continuation",
|
|
610
|
+
start(ctx) {
|
|
611
|
+
if (typeof ctx.stateDir !== "string" || !path.isAbsolute(ctx.stateDir)) return
|
|
612
|
+
JOURNAL_DIR = path.join(ctx.stateDir, "notifai", "continuation-journal")
|
|
613
|
+
continuationService = makeContinuationService(ctx.config, ctx.logger)
|
|
614
|
+
continuationService.start()
|
|
615
|
+
},
|
|
616
|
+
stop() {
|
|
617
|
+
continuationService?.stop()
|
|
618
|
+
continuationService = null
|
|
619
|
+
},
|
|
620
|
+
})
|
|
621
|
+
onIfSupported(api, "session_start", async (event, ctx) => {
|
|
622
|
+
const sessionKey = sessionKeyOf(event, ctx)
|
|
623
|
+
if (sessionKey === "") return
|
|
624
|
+
await runHook("openclaw-lifecycle", envelopeFor(sessionKey, event, ctx, "SessionStart"))
|
|
625
|
+
})
|
|
626
|
+
|
|
627
|
+
onIfSupported(api, "before_reset", async (event, ctx) => {
|
|
628
|
+
const sessionKey = sessionKeyOf(event, ctx)
|
|
629
|
+
if (sessionKey === "") return
|
|
630
|
+
await runHook("openclaw-lifecycle", envelopeFor(sessionKey, event, ctx, "BeforeReset"))
|
|
631
|
+
})
|
|
632
|
+
|
|
166
633
|
api.on("before_prompt_build", async (event, ctx) => {
|
|
167
634
|
const sessionKey = sessionKeyOf(event, ctx)
|
|
168
635
|
if (sessionKey === "") return { prependContext: WORKER_ACTIVATION_CONTEXT }
|
|
169
636
|
const worker = isWorkerSession(sessionKey, ctx)
|
|
170
|
-
const resolved = await runHook(worker ? "subagent-start" : "session-start",
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
// Successful empty output means this Project is disabled
|
|
176
|
-
//
|
|
637
|
+
const resolved = await runHook(worker ? "subagent-start" : "session-start",
|
|
638
|
+
envelopeFor(sessionKey, event, ctx, worker ? "SubagentStart" : "SessionStart"))
|
|
639
|
+
// chat.send does not emit message_received on this supported Gateway path.
|
|
640
|
+
// The prompt build is the exact session's authoritative turn-start seam.
|
|
641
|
+
await runHook("openclaw-turn-start", envelopeFor(sessionKey, event, ctx, "BeforePromptBuild"))
|
|
642
|
+
// Successful empty output means this Project is disabled or this
|
|
643
|
+
// generation was already activated. A later prompt rechecks enablement.
|
|
177
644
|
if (resolved !== null && resolved.trim().length === 0) return
|
|
178
645
|
const context = typeof resolved === "string" && resolved.trim().length > 0
|
|
179
646
|
? resolved.trim()
|
|
@@ -195,29 +662,25 @@ function register(api) {
|
|
|
195
662
|
api.on("agent_end", async (event, ctx) => {
|
|
196
663
|
const sessionKey = sessionKeyOf(event, ctx)
|
|
197
664
|
if (sessionKey === "") return
|
|
198
|
-
await runHook("
|
|
199
|
-
session_id: sessionKey,
|
|
200
|
-
cwd: workspaceDirOf(event, ctx),
|
|
201
|
-
hook_event_name: "Stop",
|
|
202
|
-
})
|
|
665
|
+
await runHook("openclaw-turn-end", envelopeFor(sessionKey, event, ctx, "Stop"))
|
|
203
666
|
})
|
|
204
667
|
|
|
205
668
|
api.on("session_end", async (event, ctx) => {
|
|
206
669
|
const sessionKey = sessionKeyOf(event, ctx)
|
|
207
670
|
if (sessionKey === "") return
|
|
208
|
-
await runHook("session-end",
|
|
209
|
-
session_id: sessionKey,
|
|
210
|
-
cwd: workspaceDirOf(event, ctx),
|
|
211
|
-
hook_event_name: "SessionEnd",
|
|
212
|
-
})
|
|
671
|
+
await runHook("session-end", envelopeFor(sessionKey, event, ctx, "SessionEnd"))
|
|
213
672
|
})
|
|
214
673
|
|
|
215
|
-
api.on("resolve_exec_env", (event, ctx) => {
|
|
674
|
+
api.on("resolve_exec_env", async (event, ctx) => {
|
|
216
675
|
const sessionKey = sessionKeyOf(event, ctx)
|
|
217
676
|
if (sessionKey === "") return
|
|
677
|
+
const generation = await runHook("openclaw-generation",
|
|
678
|
+
envelopeFor(sessionKey, event, ctx, "ResolveExecEnv"))
|
|
679
|
+
if (generation === null || !/^[0-9a-f-]{36}$/i.test(generation.trim())) return
|
|
218
680
|
return {
|
|
219
681
|
NOTIFAI_ACTIVE_HARNESS: "openclaw",
|
|
220
682
|
NOTIFAI_ACTIVE_SESSION_ID: sessionKey,
|
|
683
|
+
NOTIFAI_ACTIVE_OPENCLAW_GENERATION: generation.trim(),
|
|
221
684
|
}
|
|
222
685
|
})
|
|
223
686
|
}
|
|
@@ -253,24 +716,193 @@ export function openclawPluginTarget(contents) {
|
|
|
253
716
|
return null;
|
|
254
717
|
}
|
|
255
718
|
}
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
719
|
+
function isJsonObject(value) {
|
|
720
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
721
|
+
}
|
|
722
|
+
export function parseOpenclawConfig(source, configFile = 'OpenClaw config') {
|
|
723
|
+
const errors = [];
|
|
724
|
+
const parsed = parse(source, errors, { allowTrailingComma: true });
|
|
725
|
+
if (errors.length > 0) {
|
|
726
|
+
const problems = errors
|
|
727
|
+
.map((error) => `${printParseErrorCode(error.error)} at offset ${error.offset}`)
|
|
728
|
+
.join(', ');
|
|
729
|
+
throw new Error(`${configFile} contains invalid JSONC (${problems}); refusing to write`);
|
|
730
|
+
}
|
|
731
|
+
if (isJsonObject(parsed))
|
|
732
|
+
return parsed;
|
|
733
|
+
throw new Error(`${configFile} is not a JSON object; refusing to write`);
|
|
734
|
+
}
|
|
735
|
+
function openclawFormattingOptions(source) {
|
|
736
|
+
const indentation = /(?:^|\r?\n)([\t ]+)(?=")/.exec(source)?.[1] ?? ' ';
|
|
737
|
+
return {
|
|
738
|
+
insertSpaces: !indentation.includes('\t'),
|
|
739
|
+
tabSize: indentation.length,
|
|
740
|
+
eol: source.includes('\r\n') ? '\r\n' : '\n',
|
|
741
|
+
};
|
|
742
|
+
}
|
|
743
|
+
/** Apply one JSONC edit while leaving unrelated source ranges intact. */
|
|
744
|
+
export function editOpenclawConfigText(source, configFile, location, value) {
|
|
745
|
+
parseOpenclawConfig(source, configFile);
|
|
746
|
+
return applyEdits(source, modify(source, location, value, { formattingOptions: openclawFormattingOptions(source) }));
|
|
747
|
+
}
|
|
748
|
+
function objectAt(value) {
|
|
749
|
+
return isJsonObject(value) ? value : null;
|
|
750
|
+
}
|
|
751
|
+
/** Install or refresh Notifai's entry with a JSONC parser edit. */
|
|
752
|
+
export function enableOpenclawNotifaiConfigText(source, configFile) {
|
|
753
|
+
const config = parseOpenclawConfig(source, configFile);
|
|
754
|
+
const merged = mergeOpenclawNotifaiEntry(config);
|
|
755
|
+
const mergedPlugins = objectAt(merged.plugins);
|
|
756
|
+
const mergedEntries = objectAt(mergedPlugins['entries']);
|
|
757
|
+
const plugins = objectAt(config.plugins);
|
|
758
|
+
if (plugins === null) {
|
|
759
|
+
return editOpenclawConfigText(source, configFile, ['plugins'], merged.plugins);
|
|
760
|
+
}
|
|
761
|
+
if (Object.hasOwn(plugins, 'entries') && !isJsonObject(plugins['entries'])) {
|
|
762
|
+
return editOpenclawConfigText(source, configFile, ['plugins', 'entries'], mergedEntries);
|
|
763
|
+
}
|
|
764
|
+
return editOpenclawConfigText(source, configFile, ['plugins', 'entries', OPENCLAW_PLUGIN_ID], mergedEntries[OPENCLAW_PLUGIN_ID]);
|
|
765
|
+
}
|
|
766
|
+
// jsonc-parser's SyntaxKind is an ambient const enum, unavailable with
|
|
767
|
+
// verbatimModuleSyntax. Keep the scanner token values local to this helper.
|
|
768
|
+
const COMMA_TOKEN = 5;
|
|
769
|
+
const LINE_COMMENT_TOKEN = 12;
|
|
770
|
+
const BLOCK_COMMENT_TOKEN = 13;
|
|
771
|
+
const EOF_TOKEN = 17;
|
|
772
|
+
function jsoncGap(source, from, to) {
|
|
773
|
+
const scanner = createScanner(source);
|
|
774
|
+
scanner.setPosition(from);
|
|
775
|
+
let comma = null;
|
|
776
|
+
let hasComment = false;
|
|
777
|
+
while (scanner.getPosition() < to) {
|
|
778
|
+
const token = scanner.scan();
|
|
779
|
+
const offset = scanner.getTokenOffset();
|
|
780
|
+
if (offset >= to || token === EOF_TOKEN)
|
|
781
|
+
break;
|
|
782
|
+
if (token === COMMA_TOKEN)
|
|
783
|
+
comma = offset;
|
|
784
|
+
if (token === LINE_COMMENT_TOKEN || token === BLOCK_COMMENT_TOKEN) {
|
|
785
|
+
hasComment = true;
|
|
261
786
|
}
|
|
262
787
|
}
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
788
|
+
return { comma, hasComment };
|
|
789
|
+
}
|
|
790
|
+
function removeJsoncItem(source, path) {
|
|
791
|
+
const root = parseTree(source);
|
|
792
|
+
if (root === undefined)
|
|
793
|
+
return source;
|
|
794
|
+
const value = findNodeAtLocation(root, path);
|
|
795
|
+
if (value === undefined)
|
|
796
|
+
return source;
|
|
797
|
+
const item = value.parent?.type === 'property' ? value.parent : value;
|
|
798
|
+
const container = item.parent;
|
|
799
|
+
const siblings = container?.children;
|
|
800
|
+
if (container === undefined || siblings === undefined)
|
|
801
|
+
return source;
|
|
802
|
+
const index = siblings.indexOf(item);
|
|
803
|
+
if (index < 0)
|
|
804
|
+
return source;
|
|
805
|
+
const end = item.offset + item.length;
|
|
806
|
+
const ranges = [];
|
|
807
|
+
if (index < siblings.length - 1) {
|
|
808
|
+
const gap = jsoncGap(source, end, siblings[index + 1].offset);
|
|
809
|
+
if (gap.comma === null)
|
|
810
|
+
return source;
|
|
811
|
+
if (gap.hasComment) {
|
|
812
|
+
ranges.push([item.offset, end], [gap.comma, gap.comma + 1]);
|
|
813
|
+
}
|
|
814
|
+
else {
|
|
815
|
+
ranges.push([item.offset, gap.comma + 1]);
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
else {
|
|
819
|
+
const trailing = jsoncGap(source, end, container.offset + container.length - 1);
|
|
820
|
+
if (trailing.comma !== null)
|
|
821
|
+
ranges.push([trailing.comma, trailing.comma + 1]);
|
|
822
|
+
if (index > 0) {
|
|
823
|
+
const previous = siblings[index - 1];
|
|
824
|
+
const gap = jsoncGap(source, previous.offset + previous.length, item.offset);
|
|
825
|
+
if (gap.comma === null)
|
|
826
|
+
return source;
|
|
827
|
+
if (gap.hasComment) {
|
|
828
|
+
ranges.push([gap.comma, gap.comma + 1], [item.offset, end]);
|
|
829
|
+
}
|
|
830
|
+
else {
|
|
831
|
+
ranges.push([gap.comma, end]);
|
|
832
|
+
}
|
|
271
833
|
}
|
|
834
|
+
else {
|
|
835
|
+
// An inserted sole property occupies a line of its own. Recover the
|
|
836
|
+
// original commented-empty object by removing that added line.
|
|
837
|
+
const lineStart = source.lastIndexOf('\n', item.offset - 1) + 1;
|
|
838
|
+
const lineEndOffset = source.indexOf('\n', end);
|
|
839
|
+
const lineEnd = lineEndOffset < 0 ? source.length : lineEndOffset;
|
|
840
|
+
const onOwnLine = source.slice(lineStart, item.offset).trim() === '' &&
|
|
841
|
+
source.slice(end, lineEnd).trim() === (trailing.comma === null ? '' : ',');
|
|
842
|
+
const lineBreakStart = source[lineStart - 2] === '\r' ? lineStart - 2 : lineStart - 1;
|
|
843
|
+
ranges.push([onOwnLine && lineStart > 0 ? lineBreakStart : item.offset, end]);
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
let text = source;
|
|
847
|
+
for (const [start, stop] of ranges.sort((a, b) => b[0] - a[0])) {
|
|
848
|
+
text = text.slice(0, start) + text.slice(stop);
|
|
849
|
+
}
|
|
850
|
+
return text;
|
|
851
|
+
}
|
|
852
|
+
function removeOpenclawLoadPathText(source, pluginDir) {
|
|
853
|
+
let text = source;
|
|
854
|
+
for (;;) {
|
|
855
|
+
const config = parseOpenclawConfig(text);
|
|
856
|
+
const paths = objectAt(objectAt(config.plugins)?.['load'])?.['paths'];
|
|
857
|
+
if (!Array.isArray(paths))
|
|
858
|
+
return text;
|
|
859
|
+
const index = paths.findIndex((value) => value === pluginDir);
|
|
860
|
+
if (index < 0)
|
|
861
|
+
return text;
|
|
862
|
+
const next = removeJsoncItem(text, ['plugins', 'load', 'paths', index]);
|
|
863
|
+
if (next === text)
|
|
864
|
+
throw new Error('Could not remove the OpenClaw load path');
|
|
865
|
+
text = next;
|
|
272
866
|
}
|
|
273
|
-
|
|
867
|
+
}
|
|
868
|
+
/** Remove only the entry and explicit load path owned by this installation. */
|
|
869
|
+
export function removeOpenclawNotifaiConfigText(source, configFile, pluginDir) {
|
|
870
|
+
const config = parseOpenclawConfig(source, configFile);
|
|
871
|
+
const entries = objectAt(objectAt(config.plugins)?.['entries']);
|
|
872
|
+
const withoutEntry = entries !== null && Object.hasOwn(entries, OPENCLAW_PLUGIN_ID)
|
|
873
|
+
? removeJsoncItem(source, ['plugins', 'entries', OPENCLAW_PLUGIN_ID])
|
|
874
|
+
: source;
|
|
875
|
+
return removeOpenclawLoadPathText(withoutEntry, pluginDir);
|
|
876
|
+
}
|
|
877
|
+
/** Remove one older Project load path while leaving the shared entry alone. */
|
|
878
|
+
export function removeOpenclawLoadPathConfigText(source, configFile, pluginDir) {
|
|
879
|
+
parseOpenclawConfig(source, configFile);
|
|
880
|
+
return removeOpenclawLoadPathText(source, pluginDir);
|
|
881
|
+
}
|
|
882
|
+
export function openclawPluginLoadBlockers(config) {
|
|
883
|
+
const plugins = objectAt(config.plugins);
|
|
884
|
+
if (plugins === null)
|
|
885
|
+
return [];
|
|
886
|
+
const blockers = [];
|
|
887
|
+
const allow = plugins['allow'];
|
|
888
|
+
if (Array.isArray(allow) && allow.length > 0 && !allow.includes(OPENCLAW_PLUGIN_ID)) {
|
|
889
|
+
blockers.push({ key: 'plugins.allow', action: 'add' });
|
|
890
|
+
}
|
|
891
|
+
const deny = plugins['deny'];
|
|
892
|
+
if (Array.isArray(deny) && deny.includes(OPENCLAW_PLUGIN_ID)) {
|
|
893
|
+
blockers.push({ key: 'plugins.deny', action: 'remove' });
|
|
894
|
+
}
|
|
895
|
+
return blockers;
|
|
896
|
+
}
|
|
897
|
+
export function openclawPluginLoadRemedy(blocker) {
|
|
898
|
+
return blocker.action === 'add'
|
|
899
|
+
? `add \"${OPENCLAW_PLUGIN_ID}\" to ${blocker.key}`
|
|
900
|
+
: `remove \"${OPENCLAW_PLUGIN_ID}\" from ${blocker.key}`;
|
|
901
|
+
}
|
|
902
|
+
export function openclawPluginLoadWarning(blockers) {
|
|
903
|
+
if (blockers.length === 0)
|
|
904
|
+
return null;
|
|
905
|
+
return `OpenClaw will not load Notifai: ${blockers.map(openclawPluginLoadRemedy).join('; ')}.`;
|
|
274
906
|
}
|
|
275
907
|
export function mergeOpenclawNotifaiEntry(config) {
|
|
276
908
|
const plugins = config.plugins !== null && typeof config.plugins === 'object' && !Array.isArray(config.plugins)
|
|
@@ -301,62 +933,44 @@ export function mergeOpenclawNotifaiEntry(config) {
|
|
|
301
933
|
* exactly as it was.
|
|
302
934
|
*/
|
|
303
935
|
export function removeOpenclawLoadPath(config, pluginDir) {
|
|
304
|
-
const
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
const load = plugins.load !== null && typeof plugins.load === 'object' && !Array.isArray(plugins.load)
|
|
308
|
-
? { ...plugins.load }
|
|
309
|
-
: {};
|
|
310
|
-
if (!Array.isArray(load.paths))
|
|
936
|
+
const currentPlugins = objectAt(config.plugins);
|
|
937
|
+
const currentLoad = objectAt(currentPlugins?.['load']);
|
|
938
|
+
if (currentPlugins === null || currentLoad === null || !Array.isArray(currentLoad['paths'])) {
|
|
311
939
|
return config;
|
|
312
|
-
|
|
313
|
-
|
|
940
|
+
}
|
|
941
|
+
const load = { ...currentLoad };
|
|
942
|
+
const originalPaths = currentLoad['paths'];
|
|
943
|
+
const paths = originalPaths.filter((value) => value !== pluginDir);
|
|
944
|
+
if (paths.length === originalPaths.length)
|
|
314
945
|
return config;
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
else
|
|
318
|
-
delete load.paths;
|
|
319
|
-
if (Object.keys(load).length > 0)
|
|
320
|
-
plugins.load = load;
|
|
321
|
-
else
|
|
322
|
-
delete plugins.load;
|
|
323
|
-
if (Object.keys(plugins).length > 0)
|
|
324
|
-
return { ...config, plugins };
|
|
325
|
-
const next = { ...config };
|
|
326
|
-
delete next.plugins;
|
|
327
|
-
return next;
|
|
946
|
+
load.paths = paths;
|
|
947
|
+
return { ...config, plugins: { ...currentPlugins, load } };
|
|
328
948
|
}
|
|
329
949
|
export function removeOpenclawNotifaiEntry(config, pluginDir) {
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
const
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
950
|
+
if (!isJsonObject(config.plugins))
|
|
951
|
+
return config;
|
|
952
|
+
const currentPlugins = config.plugins;
|
|
953
|
+
const currentEntries = objectAt(currentPlugins['entries']);
|
|
954
|
+
const currentLoad = objectAt(currentPlugins['load']);
|
|
955
|
+
const removesEntry = currentEntries !== null && Object.hasOwn(currentEntries, OPENCLAW_PLUGIN_ID);
|
|
956
|
+
const removesLoadPath = currentLoad !== null &&
|
|
957
|
+
Array.isArray(currentLoad['paths']) &&
|
|
958
|
+
currentLoad['paths'].includes(pluginDir);
|
|
959
|
+
if (!removesEntry && !removesLoadPath)
|
|
960
|
+
return config;
|
|
961
|
+
const plugins = { ...currentPlugins };
|
|
962
|
+
if (removesEntry) {
|
|
963
|
+
const entries = { ...currentEntries };
|
|
964
|
+
delete entries[OPENCLAW_PLUGIN_ID];
|
|
338
965
|
plugins.entries = entries;
|
|
339
|
-
else
|
|
340
|
-
delete plugins.entries;
|
|
341
|
-
const load = plugins.load !== null && typeof plugins.load === 'object' && !Array.isArray(plugins.load)
|
|
342
|
-
? { ...plugins.load }
|
|
343
|
-
: {};
|
|
344
|
-
if (Array.isArray(load.paths)) {
|
|
345
|
-
const paths = load.paths.filter((value) => value !== pluginDir);
|
|
346
|
-
if (paths.length > 0)
|
|
347
|
-
load.paths = paths;
|
|
348
|
-
else
|
|
349
|
-
delete load.paths;
|
|
350
966
|
}
|
|
351
|
-
if (
|
|
352
|
-
plugins.load =
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
delete next.plugins;
|
|
359
|
-
return next;
|
|
967
|
+
if (removesLoadPath) {
|
|
968
|
+
plugins.load = {
|
|
969
|
+
...currentLoad,
|
|
970
|
+
paths: currentLoad['paths'].filter((value) => value !== pluginDir),
|
|
971
|
+
};
|
|
972
|
+
}
|
|
973
|
+
return { ...config, plugins };
|
|
360
974
|
}
|
|
361
975
|
/** Config-directory presence is not ownership. These files mean OpenClaw is actually configured. */
|
|
362
976
|
export function openclawHasGlobalEvidence(exists, env = process.env, platform = process.platform) {
|