@raidiant/notifai 10.0.0 → 10.1.1

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 (127) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/README.md +4 -1
  3. package/dist/cli-bin.d.ts +21 -1
  4. package/dist/cli-bin.js +141 -10
  5. package/dist/cli-bin.js.map +1 -1
  6. package/dist/cli-update-contract.d.ts +7 -0
  7. package/dist/cli-update-contract.js +10 -0
  8. package/dist/cli-update-contract.js.map +1 -0
  9. package/dist/client.d.ts +6 -1
  10. package/dist/client.js +1 -0
  11. package/dist/client.js.map +1 -1
  12. package/dist/codex-wake.d.ts +2 -0
  13. package/dist/codex-wake.js +2 -1
  14. package/dist/codex-wake.js.map +1 -1
  15. package/dist/commands-auth.d.ts +15 -1
  16. package/dist/commands-auth.js +32 -14
  17. package/dist/commands-auth.js.map +1 -1
  18. package/dist/commands-core.d.ts +10 -1
  19. package/dist/commands-core.js +13 -4
  20. package/dist/commands-core.js.map +1 -1
  21. package/dist/commands-devices.d.ts +10 -1
  22. package/dist/commands-devices.js +17 -13
  23. package/dist/commands-devices.js.map +1 -1
  24. package/dist/commands-doctor.js +158 -71
  25. package/dist/commands-doctor.js.map +1 -1
  26. package/dist/commands-harness-context.js +8 -0
  27. package/dist/commands-harness-context.js.map +1 -1
  28. package/dist/commands-hooks.js +169 -15
  29. package/dist/commands-hooks.js.map +1 -1
  30. package/dist/commands-init.d.ts +16 -0
  31. package/dist/commands-init.js +254 -91
  32. package/dist/commands-init.js.map +1 -1
  33. package/dist/commands-send.d.ts +1 -1
  34. package/dist/commands-send.js +121 -35
  35. package/dist/commands-send.js.map +1 -1
  36. package/dist/commands-update.d.ts +10 -0
  37. package/dist/commands-update.js +141 -0
  38. package/dist/commands-update.js.map +1 -0
  39. package/dist/commands.d.ts +2 -1
  40. package/dist/commands.js +2 -1
  41. package/dist/commands.js.map +1 -1
  42. package/dist/harnesses.d.ts +2 -2
  43. package/dist/harnesses.js +22 -2
  44. package/dist/harnesses.js.map +1 -1
  45. package/dist/hook-adapter.d.ts +2 -0
  46. package/dist/hook-adapter.js +12 -1
  47. package/dist/hook-adapter.js.map +1 -1
  48. package/dist/hooks.d.ts +43 -2
  49. package/dist/hooks.js +163 -21
  50. package/dist/hooks.js.map +1 -1
  51. package/dist/install-hooks.js +15 -9
  52. package/dist/install-hooks.js.map +1 -1
  53. package/dist/interactive.js +20 -23
  54. package/dist/interactive.js.map +1 -1
  55. package/dist/main-run.d.ts +1 -0
  56. package/dist/main-run.js +42 -0
  57. package/dist/main-run.js.map +1 -0
  58. package/dist/main.js +10 -31
  59. package/dist/main.js.map +1 -1
  60. package/dist/node-floor.d.ts +18 -0
  61. package/dist/node-floor.js +35 -0
  62. package/dist/node-floor.js.map +1 -0
  63. package/dist/openclaw-plugin.d.ts +49 -0
  64. package/dist/openclaw-plugin.js +350 -0
  65. package/dist/openclaw-plugin.js.map +1 -0
  66. package/dist/program.d.ts +2 -1
  67. package/dist/program.js +31 -13
  68. package/dist/program.js.map +1 -1
  69. package/dist/question-settlement-process.d.ts +15 -0
  70. package/dist/question-settlement-process.js +35 -0
  71. package/dist/question-settlement-process.js.map +1 -0
  72. package/dist/readiness.d.ts +25 -1
  73. package/dist/readiness.js +32 -2
  74. package/dist/readiness.js.map +1 -1
  75. package/dist/session-activation.js +1 -1
  76. package/dist/session-activation.js.map +1 -1
  77. package/dist/setup-destinations.d.ts +23 -0
  78. package/dist/setup-destinations.js +44 -0
  79. package/dist/setup-destinations.js.map +1 -0
  80. package/dist/ui/help.d.ts +4 -5
  81. package/dist/ui/help.js +5 -6
  82. package/dist/ui/help.js.map +1 -1
  83. package/package.json +2 -2
  84. package/src/cli-bin.test.ts +145 -7
  85. package/src/cli-bin.ts +176 -10
  86. package/src/cli-update-contract.ts +9 -0
  87. package/src/cli-update.test.ts +125 -0
  88. package/src/client.ts +7 -0
  89. package/src/codex-wake.ts +5 -1
  90. package/src/commands-auth.ts +47 -13
  91. package/src/commands-core.ts +21 -4
  92. package/src/commands-devices.ts +23 -16
  93. package/src/commands-doctor.ts +197 -75
  94. package/src/commands-harness-context.ts +8 -0
  95. package/src/commands-hooks.ts +210 -18
  96. package/src/commands-init.ts +275 -92
  97. package/src/commands-send.ts +145 -39
  98. package/src/commands-update.ts +180 -0
  99. package/src/commands.test.ts +701 -53
  100. package/src/commands.ts +2 -0
  101. package/src/fixtures/openclaw-2026.7.1-2-agent-local.json +57 -0
  102. package/src/harness-integration.test.ts +82 -0
  103. package/src/harnesses.test.ts +9 -1
  104. package/src/harnesses.ts +24 -2
  105. package/src/hook-adapter.ts +14 -0
  106. package/src/hooks.test.ts +275 -0
  107. package/src/hooks.ts +270 -18
  108. package/src/install-hooks.test.ts +208 -0
  109. package/src/install-hooks.ts +19 -9
  110. package/src/interactive.test.ts +31 -0
  111. package/src/interactive.ts +20 -26
  112. package/src/main-run.ts +45 -0
  113. package/src/main.ts +9 -33
  114. package/src/node-floor.test.ts +27 -0
  115. package/src/node-floor.ts +37 -0
  116. package/src/openclaw-plugin.ts +409 -0
  117. package/src/program.test.ts +35 -0
  118. package/src/program.ts +37 -12
  119. package/src/question-settlement-process.ts +47 -0
  120. package/src/readiness.ts +35 -2
  121. package/src/session-activation.ts +1 -1
  122. package/src/setup-destinations.test.ts +41 -0
  123. package/src/setup-destinations.ts +50 -0
  124. package/src/skill.test.ts +13 -2
  125. package/src/ui/help.test.ts +16 -0
  126. package/src/ui/help.ts +6 -7
  127. package/src/commands-devices.test.ts +0 -15
@@ -12,6 +12,8 @@ import {
12
12
  lstatSync,
13
13
  mkdirSync,
14
14
  readFileSync,
15
+ readdirSync,
16
+ rmdirSync,
15
17
  rmSync,
16
18
  } from 'node:fs'
17
19
  import path from 'node:path'
@@ -53,6 +55,7 @@ import {
53
55
  withdrawUnpushedQuestions,
54
56
  type EscalationDeliveryRoute,
55
57
  type HookContext,
58
+ type HookOutcome,
56
59
  type HookHarness,
57
60
  type PendingQuestion,
58
61
  } from './hooks.js'
@@ -87,7 +90,20 @@ import { QUESTION_WAITER_CEILING_SECONDS } from './question-timing.js'
87
90
  import { inferInvocationContext } from './invocation-context.js'
88
91
  import { logConfigResolved, logSettingsFrom } from './logging.js'
89
92
  import { isOurOpencodePlugin, opencodePluginSource } from './opencode-plugin.js'
93
+ import {
94
+ OPENCLAW_PLUGIN_MANIFEST,
95
+ OPENCLAW_PLUGIN_PACKAGE,
96
+ isOurOpenclawPlugin,
97
+ mergeOpenclawNotifaiEntry,
98
+ openclawConfigPath,
99
+ openclawPluginManifest,
100
+ openclawPluginPackage,
101
+ openclawPluginSource,
102
+ parseOpenclawConfig,
103
+ removeOpenclawNotifaiEntry,
104
+ } from './openclaw-plugin.js'
90
105
  import { packageVersion } from './release.js'
106
+ import { spawnQuestionSettlement } from './question-settlement-process.js'
91
107
  import { enableProject, projectBinding, projectEnabled } from './project-enablement.js'
92
108
  import { rejectAccidentalEscapedNewlines } from './send.js'
93
109
  import {
@@ -104,6 +120,7 @@ import {
104
120
  } from './send.js'
105
121
  import {
106
122
  EXIT,
123
+ SETUP_COMMAND,
107
124
  authedClient,
108
125
  diagnoseIgnoredOriginOverride,
109
126
  loadLoggedConfig,
@@ -141,10 +158,11 @@ export const HOOK_EVENTS = [
141
158
  'session-end',
142
159
  ] as const
143
160
  export type HookEvent = (typeof HOOK_EVENTS)[number]
161
+ const INTERNAL_HOOK_EVENTS = ['question-settlement'] as const
144
162
 
145
163
  /** Lifecycle handlers one installed harness must carry in this CLI build. */
146
164
  export function requiredHookEvents(harness: HookInstallableHarness): readonly HookEvent[] {
147
- if (harness === 'opencode') return []
165
+ if (harness === 'opencode' || harness === 'openclaw') return []
148
166
  return harness === 'cursor'
149
167
  ? ['session-start', 'activation-stop', 'user-prompt-submit', 'stop', 'session-end']
150
168
  : ['session-start', 'subagent-start', 'user-prompt-submit', 'stop', 'session-end']
@@ -172,7 +190,10 @@ export async function hookRunCommand(
172
190
  readStdin: () => Promise<string>,
173
191
  harness?: HookHarness,
174
192
  ): Promise<number> {
175
- if (!(HOOK_EVENTS as readonly string[]).includes(event)) {
193
+ if (
194
+ !(HOOK_EVENTS as readonly string[]).includes(event) &&
195
+ !(INTERNAL_HOOK_EVENTS as readonly string[]).includes(event)
196
+ ) {
176
197
  deps.io.err(`Unknown hook event "${event}". Valid: ${HOOK_EVENTS.join(', ')}`)
177
198
  return EXIT.usage
178
199
  }
@@ -469,7 +490,7 @@ export async function hookRunCommand(
469
490
  const credential = deps.store.load()
470
491
  if (!credential) {
471
492
  logger.error('hook.end', { hook: event, outcome: 'not-paired' })
472
- deps.io.err('notifai: hook skipped: this machine is not paired; run `notifai login`')
493
+ deps.io.err(`notifai: hook skipped: this machine is not paired; run \`${SETUP_COMMAND}\``)
473
494
  return EXIT.ok
474
495
  }
475
496
  // Pin authenticated traffic to the origin the credential was issued for. A
@@ -486,7 +507,9 @@ export async function hookRunCommand(
486
507
  `Bearer nfm_${credential.machineId}.${credential.secret}`,
487
508
  {
488
509
  timeoutMs: event === 'user-prompt-submit' ? 4_000 : 20_000,
489
- ...(event === 'stop' ? { deadlineAt: processDeadlineAt, now } : {}),
510
+ ...(event === 'stop' || event === 'question-settlement'
511
+ ? { deadlineAt: processDeadlineAt, now }
512
+ : {}),
490
513
  },
491
514
  )
492
515
  const ctx: HookContext = {
@@ -518,17 +541,49 @@ export async function hookRunCommand(
518
541
  // Daily state pruning is housekeeping, not part of the Stop delivery
519
542
  // contract. Its directory scan has no useful bound, so keep it on the
520
543
  // short prompt path and never spend the answer owner's finite budget on it.
521
- if (event !== 'stop') pruneAbandonedSessions(deps.env)
522
-
523
- const outcome =
524
- event === 'user-prompt-submit'
525
- ? await handleUserPromptSubmit(ctx, envelope)
526
- : await handleStop(
527
- ctx,
528
- envelope,
529
- processDeadlineAt,
530
- stopWakeRoute(deps, harness, envelope.session_id, cwd),
531
- )
544
+ if (event !== 'stop' && event !== 'question-settlement') {
545
+ pruneAbandonedSessions(deps.env)
546
+ }
547
+
548
+ let outcome: HookOutcome
549
+ if (event === 'user-prompt-submit') {
550
+ outcome = await handleUserPromptSubmit(ctx, envelope)
551
+ if (
552
+ outcome.settlementRequired === true &&
553
+ envelope.session_id !== undefined &&
554
+ harness !== undefined &&
555
+ questionRoutingCapability(harness).stopContinuation !== 'unsupported'
556
+ ) {
557
+ try {
558
+ const launchSettlement = deps.spawnQuestionSettlement ?? spawnQuestionSettlement
559
+ launchSettlement({
560
+ envelope: { session_id: envelope.session_id, cwd },
561
+ harness,
562
+ })
563
+ outcome.log = { ...outcome.log, settlement: 'launched' }
564
+ } catch (err) {
565
+ outcome.log = {
566
+ ...outcome.log,
567
+ settlement: 'launch-failed',
568
+ settlement_error: failureData(err),
569
+ }
570
+ }
571
+ }
572
+ } else {
573
+ outcome = await handleStop(
574
+ ctx,
575
+ envelope,
576
+ processDeadlineAt,
577
+ stopWakeRoute(
578
+ deps,
579
+ harness,
580
+ envelope.session_id,
581
+ cwd,
582
+ event === 'stop',
583
+ ),
584
+ event === 'stop',
585
+ )
586
+ }
532
587
  // Answer diagnostics are already persisted once as hook.answer. Keep every
533
588
  // other note in the lifecycle record without duplicating the user's text.
534
589
  const notes = outcome.notes.filter((note) => !/^(?:late )?answer from /.test(note))
@@ -580,6 +635,7 @@ function stopWakeRoute(
580
635
  harness: HookInstallableHarness | undefined,
581
636
  sessionId: string | undefined,
582
637
  cwd: string,
638
+ continuationActive = true,
583
639
  ): EscalationDeliveryRoute | undefined {
584
640
  if (sessionId === undefined) return undefined
585
641
  const declaredSourcePid = declaredHookSourcePid(deps)
@@ -599,6 +655,7 @@ function stopWakeRoute(
599
655
  sourcePid: deps.codexSourcePid ?? declaredSourcePid ?? process.ppid,
600
656
  env: deps.env,
601
657
  ...(deps.codexWake === undefined ? {} : { adapters: deps.codexWake }),
658
+ continuationActive,
602
659
  })
603
660
  }
604
661
  return undefined
@@ -927,12 +984,17 @@ function recordRegisteredQuestion(
927
984
  {
928
985
  registered: true,
929
986
  question_id: questionId,
987
+ state: 'local',
988
+ submitted: false,
989
+ request_id: null,
990
+ provider_acceptance: 'not_available',
930
991
  questions: built.questions.map((entry) => ({
931
992
  id: entry.id,
932
993
  text: entry.text,
933
994
  ...(entry.choices === undefined ? {} : { choices: entry.choices }),
934
995
  ...(entry.multi === true ? { multi: true } : {}),
935
996
  })),
997
+ status: `notifai status ${questionId}`,
936
998
  close: `notifai close ${questionId}`,
937
999
  next: {
938
1000
  end_turn: true,
@@ -962,8 +1024,11 @@ function recordRegisteredQuestion(
962
1024
  }
963
1025
  deps.io.out(
964
1026
  built.questions.length > 1
965
- ? `${built.questions.length} questions registered as one form (${questionId}). Ask them in the conversation, state the concrete work you will resume for their answers, then end your turn.`
966
- : `Question registered (${questionId}). Ask it in the conversation, state the concrete work you will resume when the answer arrives, then end your turn.`,
1027
+ ? `${built.questions.length} questions registered locally as one form (${questionId}); they have not been submitted as a Notification Request and have no Provider Acceptance yet. Ask them in the conversation, state the concrete work you will resume for their answers, then end your turn.`
1028
+ : `Question registered locally (${questionId}); it has not been submitted as a Notification Request and has no Provider Acceptance yet. Ask it in the conversation, state the concrete work you will resume when the answer arrives, then end your turn.`,
1029
+ )
1030
+ deps.io.out(
1031
+ `Question settlement runs after registration. Inspect the original identity with \`notifai status ${questionId}\`; never register a replacement to check whether this one was sent.`,
967
1032
  )
968
1033
  deps.io.out('Before ending this turn, pre-commit in your own words to the work you will resume:')
969
1034
  for (const [index, entry] of built.questions.entries()) {
@@ -1348,6 +1413,11 @@ export function hookActivationAdvice(installations: Installation[]): string {
1348
1413
  'OpenCode: restart it, then send one prompt; plugins load at startup, but non-blocking question continuation is intentionally unsupported',
1349
1414
  )
1350
1415
  }
1416
+ if (harnesses.has('openclaw')) {
1417
+ advice.push(
1418
+ 'OpenClaw: restart the Gateway, then send one prompt; plugins load at startup, but non-blocking question continuation is intentionally unsupported',
1419
+ )
1420
+ }
1351
1421
  return `${advice.join('. ')}.`
1352
1422
  }
1353
1423
 
@@ -1573,7 +1643,7 @@ async function closeLocalQuestion(
1573
1643
  if (entry === undefined) return null
1574
1644
 
1575
1645
  if (entry.request_id === undefined) {
1576
- dropPendingQuestion(sessionId, deps.env, entry)
1646
+ dropPendingQuestion(sessionId, deps.env, entry, 'withdrawn')
1577
1647
  const output = {
1578
1648
  session_id: sessionId,
1579
1649
  withdrawn: [
@@ -1710,6 +1780,8 @@ function printHooksInstallClose(deps: CommandDeps, harness: HookInstallableHarne
1710
1780
  ? 'Start one fresh Cursor conversation, send one prompt, finish its first turn, then run `notifai doctor`.'
1711
1781
  : harness === 'opencode'
1712
1782
  ? 'Restart OpenCode, start one fresh session, send one prompt, then run `notifai doctor`.'
1783
+ : harness === 'openclaw'
1784
+ ? 'Restart the OpenClaw Gateway, start one fresh Agent Session, send one prompt, then run `notifai doctor`.'
1713
1785
  : `Start one fresh ${label} session, send one prompt, then run \`notifai doctor\`.`
1714
1786
  if (deps.io.interactive === true && deps.io.note) {
1715
1787
  void deps.io.note(`${file}\n${activation}`, `${label} hooks installed`)
@@ -1802,6 +1874,16 @@ export function hooksInstallCommand(deps: CommandDeps, flags: HooksInstallFlags)
1802
1874
  ...(flags.narrate === undefined ? {} : { narrate: flags.narrate }),
1803
1875
  })
1804
1876
  }
1877
+ if (harness === 'openclaw') {
1878
+ return installOpenclawPlugin(deps, settingsTarget, {
1879
+ adapterPath,
1880
+ timeoutSeconds: NON_ROUTING_BLOCKING_STOP_TIMEOUT_SECONDS,
1881
+ platform: hookPlatform,
1882
+ nodePath,
1883
+ global: wantGlobal,
1884
+ ...(flags.narrate === undefined ? {} : { narrate: flags.narrate }),
1885
+ })
1886
+ }
1805
1887
 
1806
1888
  if (harness === 'cursor') {
1807
1889
  try {
@@ -1948,6 +2030,113 @@ function installOpencodePlugin(
1948
2030
  return EXIT.ok
1949
2031
  }
1950
2032
 
2033
+ function installOpenclawPlugin(
2034
+ deps: CommandDeps,
2035
+ file: string,
2036
+ options: {
2037
+ adapterPath: string
2038
+ timeoutSeconds: number
2039
+ platform?: NodeJS.Platform
2040
+ nodePath?: string
2041
+ global: boolean
2042
+ narrate?: boolean
2043
+ },
2044
+ ): number {
2045
+ const pluginDir = path.dirname(file)
2046
+ try {
2047
+ withTargetFileLock(file, () => {
2048
+ if (existsSync(file)) {
2049
+ assertOwnedRegularFile(file)
2050
+ const existing = readFileSync(file, 'utf8')
2051
+ if (!isOurOpenclawPlugin(existing)) {
2052
+ throw new Error(`${file} exists and was not written by Notifai; move it aside first.`)
2053
+ }
2054
+ }
2055
+ atomicWriteFileSync(file, openclawPluginSource(options), {
2056
+ mode: 0o600,
2057
+ preserveMode: false,
2058
+ requireCurrentUserOwner: true,
2059
+ })
2060
+ atomicWriteFileSync(path.join(pluginDir, OPENCLAW_PLUGIN_MANIFEST), openclawPluginManifest(), {
2061
+ mode: 0o600,
2062
+ preserveMode: false,
2063
+ requireCurrentUserOwner: true,
2064
+ })
2065
+ atomicWriteFileSync(path.join(pluginDir, OPENCLAW_PLUGIN_PACKAGE), openclawPluginPackage(), {
2066
+ mode: 0o600,
2067
+ preserveMode: false,
2068
+ requireCurrentUserOwner: true,
2069
+ })
2070
+ })
2071
+ writeOpenclawEnablement(deps, pluginDir, options.global)
2072
+ } catch (err) {
2073
+ deps.io.err(String(err))
2074
+ return EXIT.failed
2075
+ }
2076
+ if (options.narrate !== false) printHooksInstallClose(deps, 'openclaw', file)
2077
+ return EXIT.ok
2078
+ }
2079
+
2080
+ function writeOpenclawEnablement(deps: CommandDeps, pluginDir: string, global: boolean): void {
2081
+ const configFile = openclawConfigPath(deps.env, deps.hookPlatform)
2082
+ withTargetFileLock(configFile, () => {
2083
+ let config: Record<string, unknown> = {}
2084
+ if (existsSync(configFile)) {
2085
+ assertOwnedRegularFile(configFile)
2086
+ config = parseOpenclawConfig(readFileSync(configFile, 'utf8'))
2087
+ }
2088
+ const merged = mergeOpenclawNotifaiEntry(config, pluginDir, global)
2089
+ atomicWriteFileSync(configFile, `${JSON.stringify(merged, null, 2)}\n`, {
2090
+ mode: 0o600,
2091
+ preserveMode: true,
2092
+ requireCurrentUserOwner: true,
2093
+ })
2094
+ })
2095
+ }
2096
+
2097
+ function uninstallOpenclawPlugin(deps: CommandDeps, file: string): number {
2098
+ const pluginDir = path.dirname(file)
2099
+ try {
2100
+ const removed = withTargetFileLock(file, () => {
2101
+ if (!existsSync(file)) return false
2102
+ assertOwnedRegularFile(file)
2103
+ if (!isOurOpenclawPlugin(readFileSync(file, 'utf8'))) {
2104
+ deps.io.out(`Left ${file} alone: Notifai did not write it.`)
2105
+ return false
2106
+ }
2107
+ rmSync(file, { force: true })
2108
+ rmSync(path.join(pluginDir, OPENCLAW_PLUGIN_MANIFEST), { force: true })
2109
+ rmSync(path.join(pluginDir, OPENCLAW_PLUGIN_PACKAGE), { force: true })
2110
+ if (existsSync(pluginDir) && readdirSync(pluginDir).length === 0) {
2111
+ rmdirSync(pluginDir)
2112
+ }
2113
+ return true
2114
+ })
2115
+ if (removed) {
2116
+ const configFile = openclawConfigPath(deps.env, deps.hookPlatform)
2117
+ if (existsSync(configFile)) {
2118
+ withTargetFileLock(configFile, () => {
2119
+ assertOwnedRegularFile(configFile)
2120
+ const config = parseOpenclawConfig(readFileSync(configFile, 'utf8'))
2121
+ const next = removeOpenclawNotifaiEntry(config, pluginDir)
2122
+ atomicWriteFileSync(configFile, `${JSON.stringify(next, null, 2)}\n`, {
2123
+ mode: 0o600,
2124
+ preserveMode: true,
2125
+ requireCurrentUserOwner: true,
2126
+ })
2127
+ })
2128
+ }
2129
+ deps.io.out(`Removed the Notifai OpenClaw plugin at ${file}`)
2130
+ } else if (!existsSync(file)) {
2131
+ deps.io.out(`Nothing to remove: ${file} does not exist.`)
2132
+ }
2133
+ return EXIT.ok
2134
+ } catch (err) {
2135
+ deps.io.err(String(err))
2136
+ return EXIT.failed
2137
+ }
2138
+ }
2139
+
1951
2140
  export function hooksUninstallCommand(deps: CommandDeps, flags: HooksInstallFlags): number {
1952
2141
  const harness = resolveHarness(deps, flags.harness)
1953
2142
  if (!harness) return EXIT.usage
@@ -1981,6 +2170,9 @@ export function hooksUninstallCommand(deps: CommandDeps, flags: HooksInstallFlag
1981
2170
  return EXIT.failed
1982
2171
  }
1983
2172
  }
2173
+ if (harness === 'openclaw') {
2174
+ return uninstallOpenclawPlugin(deps, file)
2175
+ }
1984
2176
  if (harness === 'cursor') {
1985
2177
  let stripped: ReturnType<typeof removeCursorHooks> | null
1986
2178
  try {