@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.
Files changed (409) hide show
  1. package/CHANGELOG.md +53 -0
  2. package/NOTICE +16 -0
  3. package/THIRD_PARTY_NOTICES +725 -0
  4. package/bin/notifai.mjs +11420 -0
  5. package/data/install.ps1 +318 -0
  6. package/data/release.json +1 -0
  7. package/inventory.json +1 -0
  8. package/npm-adapter-files.json +52 -0
  9. package/package.json +14 -44
  10. package/README.md +0 -168
  11. package/dist/agent-update-notice.d.ts +0 -7
  12. package/dist/agent-update-notice.js +0 -77
  13. package/dist/agent-update-notice.js.map +0 -1
  14. package/dist/atomic-file.d.ts +0 -30
  15. package/dist/atomic-file.js +0 -148
  16. package/dist/atomic-file.js.map +0 -1
  17. package/dist/attendant-update.d.ts +0 -15
  18. package/dist/attendant-update.js +0 -96
  19. package/dist/attendant-update.js.map +0 -1
  20. package/dist/claude-wake.d.ts +0 -108
  21. package/dist/claude-wake.js +0 -369
  22. package/dist/claude-wake.js.map +0 -1
  23. package/dist/cli-bin.d.ts +0 -26
  24. package/dist/cli-bin.js +0 -220
  25. package/dist/cli-bin.js.map +0 -1
  26. package/dist/cli-contract.d.ts +0 -16
  27. package/dist/cli-contract.js +0 -27
  28. package/dist/cli-contract.js.map +0 -1
  29. package/dist/cli-release.d.ts +0 -36
  30. package/dist/cli-release.js +0 -85
  31. package/dist/cli-release.js.map +0 -1
  32. package/dist/client.d.ts +0 -109
  33. package/dist/client.js +0 -217
  34. package/dist/client.js.map +0 -1
  35. package/dist/codex-answer-control.d.ts +0 -14
  36. package/dist/codex-answer-control.js +0 -41
  37. package/dist/codex-answer-control.js.map +0 -1
  38. package/dist/codex-answer-presentation.d.ts +0 -18
  39. package/dist/codex-answer-presentation.js +0 -113
  40. package/dist/codex-answer-presentation.js.map +0 -1
  41. package/dist/codex-input-lifecycle.d.ts +0 -10
  42. package/dist/codex-input-lifecycle.js +0 -63
  43. package/dist/codex-input-lifecycle.js.map +0 -1
  44. package/dist/codex-native-control.d.ts +0 -13
  45. package/dist/codex-native-control.js +0 -120
  46. package/dist/codex-native-control.js.map +0 -1
  47. package/dist/codex-native-turn.d.ts +0 -37
  48. package/dist/codex-native-turn.js +0 -155
  49. package/dist/codex-native-turn.js.map +0 -1
  50. package/dist/codex-question-bindings.d.ts +0 -76
  51. package/dist/codex-question-bindings.js +0 -151
  52. package/dist/codex-question-bindings.js.map +0 -1
  53. package/dist/codex-queue-control.d.ts +0 -21
  54. package/dist/codex-queue-control.js +0 -59
  55. package/dist/codex-queue-control.js.map +0 -1
  56. package/dist/codex-tool-messages.d.ts +0 -7
  57. package/dist/codex-tool-messages.js +0 -126
  58. package/dist/codex-tool-messages.js.map +0 -1
  59. package/dist/codex-wake.d.ts +0 -58
  60. package/dist/codex-wake.js +0 -135
  61. package/dist/codex-wake.js.map +0 -1
  62. package/dist/command-session.d.ts +0 -14
  63. package/dist/command-session.js +0 -31
  64. package/dist/command-session.js.map +0 -1
  65. package/dist/commands-acknowledge.d.ts +0 -13
  66. package/dist/commands-acknowledge.js +0 -116
  67. package/dist/commands-acknowledge.js.map +0 -1
  68. package/dist/commands-agent-sessions.d.ts +0 -6
  69. package/dist/commands-agent-sessions.js +0 -51
  70. package/dist/commands-agent-sessions.js.map +0 -1
  71. package/dist/commands-ask.d.ts +0 -62
  72. package/dist/commands-ask.js +0 -503
  73. package/dist/commands-ask.js.map +0 -1
  74. package/dist/commands-auth.d.ts +0 -76
  75. package/dist/commands-auth.js +0 -421
  76. package/dist/commands-auth.js.map +0 -1
  77. package/dist/commands-close.d.ts +0 -6
  78. package/dist/commands-close.js +0 -273
  79. package/dist/commands-close.js.map +0 -1
  80. package/dist/commands-config.d.ts +0 -31
  81. package/dist/commands-config.js +0 -272
  82. package/dist/commands-config.js.map +0 -1
  83. package/dist/commands-core.d.ts +0 -145
  84. package/dist/commands-core.js +0 -190
  85. package/dist/commands-core.js.map +0 -1
  86. package/dist/commands-devices.d.ts +0 -41
  87. package/dist/commands-devices.js +0 -144
  88. package/dist/commands-devices.js.map +0 -1
  89. package/dist/commands-doctor.d.ts +0 -25
  90. package/dist/commands-doctor.js +0 -1553
  91. package/dist/commands-doctor.js.map +0 -1
  92. package/dist/commands-guidance.d.ts +0 -27
  93. package/dist/commands-guidance.js +0 -150
  94. package/dist/commands-guidance.js.map +0 -1
  95. package/dist/commands-harness-context.d.ts +0 -57
  96. package/dist/commands-harness-context.js +0 -102
  97. package/dist/commands-harness-context.js.map +0 -1
  98. package/dist/commands-hook-attend.d.ts +0 -70
  99. package/dist/commands-hook-attend.js +0 -655
  100. package/dist/commands-hook-attend.js.map +0 -1
  101. package/dist/commands-hook-diagnostics.d.ts +0 -37
  102. package/dist/commands-hook-diagnostics.js +0 -136
  103. package/dist/commands-hook-diagnostics.js.map +0 -1
  104. package/dist/commands-hook-install.d.ts +0 -26
  105. package/dist/commands-hook-install.js +0 -784
  106. package/dist/commands-hook-install.js.map +0 -1
  107. package/dist/commands-hook-run.d.ts +0 -29
  108. package/dist/commands-hook-run.js +0 -933
  109. package/dist/commands-hook-run.js.map +0 -1
  110. package/dist/commands-hook-shape.d.ts +0 -19
  111. package/dist/commands-hook-shape.js +0 -48
  112. package/dist/commands-hook-shape.js.map +0 -1
  113. package/dist/commands-init.d.ts +0 -42
  114. package/dist/commands-init.js +0 -901
  115. package/dist/commands-init.js.map +0 -1
  116. package/dist/commands-io.d.ts +0 -2
  117. package/dist/commands-io.js +0 -117
  118. package/dist/commands-io.js.map +0 -1
  119. package/dist/commands-logs.d.ts +0 -30
  120. package/dist/commands-logs.js +0 -201
  121. package/dist/commands-logs.js.map +0 -1
  122. package/dist/commands-native-acknowledge.d.ts +0 -8
  123. package/dist/commands-native-acknowledge.js +0 -114
  124. package/dist/commands-native-acknowledge.js.map +0 -1
  125. package/dist/commands-project.d.ts +0 -4
  126. package/dist/commands-project.js +0 -41
  127. package/dist/commands-project.js.map +0 -1
  128. package/dist/commands-receive.d.ts +0 -3
  129. package/dist/commands-receive.js +0 -59
  130. package/dist/commands-receive.js.map +0 -1
  131. package/dist/commands-send-support.d.ts +0 -59
  132. package/dist/commands-send-support.js +0 -258
  133. package/dist/commands-send-support.js.map +0 -1
  134. package/dist/commands-send.d.ts +0 -34
  135. package/dist/commands-send.js +0 -900
  136. package/dist/commands-send.js.map +0 -1
  137. package/dist/commands-setup-proof.d.ts +0 -43
  138. package/dist/commands-setup-proof.js +0 -134
  139. package/dist/commands-setup-proof.js.map +0 -1
  140. package/dist/commands-skill.d.ts +0 -21
  141. package/dist/commands-skill.js +0 -199
  142. package/dist/commands-skill.js.map +0 -1
  143. package/dist/commands-sounds.d.ts +0 -12
  144. package/dist/commands-sounds.js +0 -35
  145. package/dist/commands-sounds.js.map +0 -1
  146. package/dist/commands-update-check.d.ts +0 -6
  147. package/dist/commands-update-check.js +0 -102
  148. package/dist/commands-update-check.js.map +0 -1
  149. package/dist/commands-update-resume.d.ts +0 -7
  150. package/dist/commands-update-resume.js +0 -143
  151. package/dist/commands-update-resume.js.map +0 -1
  152. package/dist/commands-update-skill.d.ts +0 -5
  153. package/dist/commands-update-skill.js +0 -36
  154. package/dist/commands-update-skill.js.map +0 -1
  155. package/dist/commands-update.d.ts +0 -11
  156. package/dist/commands-update.js +0 -286
  157. package/dist/commands-update.js.map +0 -1
  158. package/dist/commands.d.ts +0 -26
  159. package/dist/commands.js +0 -27
  160. package/dist/commands.js.map +0 -1
  161. package/dist/config-schema.d.ts +0 -62
  162. package/dist/config-schema.js +0 -302
  163. package/dist/config-schema.js.map +0 -1
  164. package/dist/config.d.ts +0 -181
  165. package/dist/config.js +0 -357
  166. package/dist/config.js.map +0 -1
  167. package/dist/credentials.d.ts +0 -96
  168. package/dist/credentials.js +0 -342
  169. package/dist/credentials.js.map +0 -1
  170. package/dist/file-lock.d.ts +0 -34
  171. package/dist/file-lock.js +0 -367
  172. package/dist/file-lock.js.map +0 -1
  173. package/dist/generated-session-label.d.ts +0 -2
  174. package/dist/generated-session-label.js +0 -152
  175. package/dist/generated-session-label.js.map +0 -1
  176. package/dist/guidance-content.d.ts +0 -53
  177. package/dist/guidance-content.js +0 -248
  178. package/dist/guidance-content.js.map +0 -1
  179. package/dist/guidance-render.d.ts +0 -25
  180. package/dist/guidance-render.js +0 -47
  181. package/dist/guidance-render.js.map +0 -1
  182. package/dist/guidance.d.ts +0 -69
  183. package/dist/guidance.js +0 -173
  184. package/dist/guidance.js.map +0 -1
  185. package/dist/harness-session-title.d.ts +0 -22
  186. package/dist/harness-session-title.js +0 -74
  187. package/dist/harness-session-title.js.map +0 -1
  188. package/dist/harnesses.d.ts +0 -65
  189. package/dist/harnesses.js +0 -159
  190. package/dist/harnesses.js.map +0 -1
  191. package/dist/hermes-attendant.d.ts +0 -38
  192. package/dist/hermes-attendant.js +0 -337
  193. package/dist/hermes-attendant.js.map +0 -1
  194. package/dist/hermes-plugin.d.ts +0 -11
  195. package/dist/hermes-plugin.js +0 -299
  196. package/dist/hermes-plugin.js.map +0 -1
  197. package/dist/hook-acknowledgements.d.ts +0 -70
  198. package/dist/hook-acknowledgements.js +0 -490
  199. package/dist/hook-acknowledgements.js.map +0 -1
  200. package/dist/hook-adapter.d.ts +0 -48
  201. package/dist/hook-adapter.js +0 -419
  202. package/dist/hook-adapter.js.map +0 -1
  203. package/dist/hook-events.d.ts +0 -105
  204. package/dist/hook-events.js +0 -130
  205. package/dist/hook-events.js.map +0 -1
  206. package/dist/hook-gates.d.ts +0 -7
  207. package/dist/hook-gates.js +0 -20
  208. package/dist/hook-gates.js.map +0 -1
  209. package/dist/hook-input.d.ts +0 -10
  210. package/dist/hook-input.js +0 -64
  211. package/dist/hook-input.js.map +0 -1
  212. package/dist/hook-lifecycle.d.ts +0 -112
  213. package/dist/hook-lifecycle.js +0 -2031
  214. package/dist/hook-lifecycle.js.map +0 -1
  215. package/dist/hook-project-sessions.d.ts +0 -28
  216. package/dist/hook-project-sessions.js +0 -141
  217. package/dist/hook-project-sessions.js.map +0 -1
  218. package/dist/hook-question-lock.d.ts +0 -29
  219. package/dist/hook-question-lock.js +0 -217
  220. package/dist/hook-question-lock.js.map +0 -1
  221. package/dist/hook-question-retirement.d.ts +0 -74
  222. package/dist/hook-question-retirement.js +0 -314
  223. package/dist/hook-question-retirement.js.map +0 -1
  224. package/dist/hook-question-state.d.ts +0 -58
  225. package/dist/hook-question-state.js +0 -239
  226. package/dist/hook-question-state.js.map +0 -1
  227. package/dist/hook-session-state.d.ts +0 -127
  228. package/dist/hook-session-state.js +0 -488
  229. package/dist/hook-session-state.js.map +0 -1
  230. package/dist/hook-types.d.ts +0 -476
  231. package/dist/hook-types.js +0 -4
  232. package/dist/hook-types.js.map +0 -1
  233. package/dist/injection-render.d.ts +0 -36
  234. package/dist/injection-render.js +0 -61
  235. package/dist/injection-render.js.map +0 -1
  236. package/dist/install-hooks.d.ts +0 -470
  237. package/dist/install-hooks.js +0 -1622
  238. package/dist/install-hooks.js.map +0 -1
  239. package/dist/integration-health.d.ts +0 -26
  240. package/dist/integration-health.js +0 -133
  241. package/dist/integration-health.js.map +0 -1
  242. package/dist/interactive.d.ts +0 -22
  243. package/dist/interactive.js +0 -644
  244. package/dist/interactive.js.map +0 -1
  245. package/dist/invocation-context.d.ts +0 -47
  246. package/dist/invocation-context.js +0 -164
  247. package/dist/invocation-context.js.map +0 -1
  248. package/dist/local-path.d.ts +0 -7
  249. package/dist/local-path.js +0 -27
  250. package/dist/local-path.js.map +0 -1
  251. package/dist/logging.d.ts +0 -157
  252. package/dist/logging.js +0 -599
  253. package/dist/logging.js.map +0 -1
  254. package/dist/main-run.d.ts +0 -1
  255. package/dist/main-run.js +0 -42
  256. package/dist/main-run.js.map +0 -1
  257. package/dist/main.d.ts +0 -2
  258. package/dist/main.js +0 -12
  259. package/dist/main.js.map +0 -1
  260. package/dist/native-answer-operation.d.ts +0 -70
  261. package/dist/native-answer-operation.js +0 -247
  262. package/dist/native-answer-operation.js.map +0 -1
  263. package/dist/native-skills.d.ts +0 -64
  264. package/dist/native-skills.js +0 -167
  265. package/dist/native-skills.js.map +0 -1
  266. package/dist/node-floor.d.ts +0 -19
  267. package/dist/node-floor.js +0 -32
  268. package/dist/node-floor.js.map +0 -1
  269. package/dist/npm-invocation.d.ts +0 -34
  270. package/dist/npm-invocation.js +0 -53
  271. package/dist/npm-invocation.js.map +0 -1
  272. package/dist/openclaw-continuation-bridge.d.ts +0 -7
  273. package/dist/openclaw-continuation-bridge.js +0 -64
  274. package/dist/openclaw-continuation-bridge.js.map +0 -1
  275. package/dist/openclaw-gateway-readiness.d.ts +0 -1
  276. package/dist/openclaw-gateway-readiness.js +0 -24
  277. package/dist/openclaw-gateway-readiness.js.map +0 -1
  278. package/dist/openclaw-generation.d.ts +0 -21
  279. package/dist/openclaw-generation.js +0 -115
  280. package/dist/openclaw-generation.js.map +0 -1
  281. package/dist/openclaw-message-bridge.d.ts +0 -8
  282. package/dist/openclaw-message-bridge.js +0 -93
  283. package/dist/openclaw-message-bridge.js.map +0 -1
  284. package/dist/openclaw-pending.d.ts +0 -8
  285. package/dist/openclaw-pending.js +0 -47
  286. package/dist/openclaw-pending.js.map +0 -1
  287. package/dist/openclaw-plugin.d.ts +0 -75
  288. package/dist/openclaw-plugin.js +0 -1353
  289. package/dist/openclaw-plugin.js.map +0 -1
  290. package/dist/openclaw-session-access.d.ts +0 -12
  291. package/dist/openclaw-session-access.js +0 -79
  292. package/dist/openclaw-session-access.js.map +0 -1
  293. package/dist/opencode-plugin.d.ts +0 -88
  294. package/dist/opencode-plugin.js +0 -286
  295. package/dist/opencode-plugin.js.map +0 -1
  296. package/dist/orca-session-title.d.ts +0 -11
  297. package/dist/orca-session-title.js +0 -111
  298. package/dist/orca-session-title.js.map +0 -1
  299. package/dist/pack-feedback-slice.d.ts +0 -35
  300. package/dist/pack-feedback-slice.js +0 -126
  301. package/dist/pack-feedback-slice.js.map +0 -1
  302. package/dist/pairing-qr.d.ts +0 -5
  303. package/dist/pairing-qr.js +0 -24
  304. package/dist/pairing-qr.js.map +0 -1
  305. package/dist/pending-pairing.d.ts +0 -38
  306. package/dist/pending-pairing.js +0 -70
  307. package/dist/pending-pairing.js.map +0 -1
  308. package/dist/platform.d.ts +0 -67
  309. package/dist/platform.js +0 -140
  310. package/dist/platform.js.map +0 -1
  311. package/dist/process-identity.d.ts +0 -26
  312. package/dist/process-identity.js +0 -82
  313. package/dist/process-identity.js.map +0 -1
  314. package/dist/program.d.ts +0 -59
  315. package/dist/program.js +0 -691
  316. package/dist/program.js.map +0 -1
  317. package/dist/project-enablement.d.ts +0 -21
  318. package/dist/project-enablement.js +0 -71
  319. package/dist/project-enablement.js.map +0 -1
  320. package/dist/question-settlement-process.d.ts +0 -17
  321. package/dist/question-settlement-process.js +0 -35
  322. package/dist/question-settlement-process.js.map +0 -1
  323. package/dist/question-timing.d.ts +0 -46
  324. package/dist/question-timing.js +0 -48
  325. package/dist/question-timing.js.map +0 -1
  326. package/dist/readiness.d.ts +0 -153
  327. package/dist/readiness.js +0 -149
  328. package/dist/readiness.js.map +0 -1
  329. package/dist/release.d.ts +0 -31
  330. package/dist/release.js +0 -52
  331. package/dist/release.js.map +0 -1
  332. package/dist/send-attempts.d.ts +0 -26
  333. package/dist/send-attempts.js +0 -139
  334. package/dist/send-attempts.js.map +0 -1
  335. package/dist/send.d.ts +0 -115
  336. package/dist/send.js +0 -421
  337. package/dist/send.js.map +0 -1
  338. package/dist/session-activation.d.ts +0 -13
  339. package/dist/session-activation.js +0 -64
  340. package/dist/session-activation.js.map +0 -1
  341. package/dist/session-attendant-probe.d.ts +0 -85
  342. package/dist/session-attendant-probe.js +0 -173
  343. package/dist/session-attendant-probe.js.map +0 -1
  344. package/dist/session-attendant-state.d.ts +0 -70
  345. package/dist/session-attendant-state.js +0 -256
  346. package/dist/session-attendant-state.js.map +0 -1
  347. package/dist/session-attendant.d.ts +0 -143
  348. package/dist/session-attendant.js +0 -520
  349. package/dist/session-attendant.js.map +0 -1
  350. package/dist/session-delivery.d.ts +0 -249
  351. package/dist/session-delivery.js +0 -576
  352. package/dist/session-delivery.js.map +0 -1
  353. package/dist/session-handoff.d.ts +0 -98
  354. package/dist/session-handoff.js +0 -118
  355. package/dist/session-handoff.js.map +0 -1
  356. package/dist/session-input-wakes.d.ts +0 -78
  357. package/dist/session-input-wakes.js +0 -239
  358. package/dist/session-input-wakes.js.map +0 -1
  359. package/dist/session-inputs.d.ts +0 -57
  360. package/dist/session-inputs.js +0 -434
  361. package/dist/session-inputs.js.map +0 -1
  362. package/dist/session-labels.d.ts +0 -56
  363. package/dist/session-labels.js +0 -396
  364. package/dist/session-labels.js.map +0 -1
  365. package/dist/session-message-handoff.d.ts +0 -39
  366. package/dist/session-message-handoff.js +0 -107
  367. package/dist/session-message-handoff.js.map +0 -1
  368. package/dist/setup-destinations.d.ts +0 -23
  369. package/dist/setup-destinations.js +0 -44
  370. package/dist/setup-destinations.js.map +0 -1
  371. package/dist/skill-integrity.d.ts +0 -54
  372. package/dist/skill-integrity.js +0 -164
  373. package/dist/skill-integrity.js.map +0 -1
  374. package/dist/skill-source/manifest.json +0 -40
  375. package/dist/skill-source/notifai/SKILL.md +0 -350
  376. package/dist/skill-source/notifai/references/diagnostics.md +0 -36
  377. package/dist/skill-source/notifai/references/harness-setup.md +0 -384
  378. package/dist/skill-source/notifai/references/native-questions.md +0 -51
  379. package/dist/skill-source/notifai/references/notes-and-edits.md +0 -54
  380. package/dist/skill-source/notifai/references/send-details.md +0 -14
  381. package/dist/skill-source/notifai/references/updates.md +0 -116
  382. package/dist/skill-source/notifai/references/writing-examples.md +0 -56
  383. package/dist/sound-ref.d.ts +0 -23
  384. package/dist/sound-ref.js +0 -28
  385. package/dist/sound-ref.js.map +0 -1
  386. package/dist/ui/banner.d.ts +0 -22
  387. package/dist/ui/banner.js +0 -87
  388. package/dist/ui/banner.js.map +0 -1
  389. package/dist/ui/config-view.d.ts +0 -30
  390. package/dist/ui/config-view.js +0 -108
  391. package/dist/ui/config-view.js.map +0 -1
  392. package/dist/ui/help.d.ts +0 -44
  393. package/dist/ui/help.js +0 -107
  394. package/dist/ui/help.js.map +0 -1
  395. package/dist/ui/theme.d.ts +0 -90
  396. package/dist/ui/theme.js +0 -244
  397. package/dist/ui/theme.js.map +0 -1
  398. package/dist/update-handoff.d.ts +0 -29
  399. package/dist/update-handoff.js +0 -69
  400. package/dist/update-handoff.js.map +0 -1
  401. package/dist/url-policy.d.ts +0 -85
  402. package/dist/url-policy.js +0 -369
  403. package/dist/url-policy.js.map +0 -1
  404. package/dist/version.d.ts +0 -40
  405. package/dist/version.js +0 -132
  406. package/dist/version.js.map +0 -1
  407. package/dist/wake-support.d.ts +0 -40
  408. package/dist/wake-support.js +0 -95
  409. package/dist/wake-support.js.map +0 -1
@@ -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