@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.
- package/CHANGELOG.md +31 -0
- package/README.md +4 -1
- package/dist/cli-bin.d.ts +21 -1
- package/dist/cli-bin.js +141 -10
- package/dist/cli-bin.js.map +1 -1
- package/dist/cli-update-contract.d.ts +7 -0
- package/dist/cli-update-contract.js +10 -0
- package/dist/cli-update-contract.js.map +1 -0
- package/dist/client.d.ts +6 -1
- package/dist/client.js +1 -0
- package/dist/client.js.map +1 -1
- package/dist/codex-wake.d.ts +2 -0
- package/dist/codex-wake.js +2 -1
- package/dist/codex-wake.js.map +1 -1
- package/dist/commands-auth.d.ts +15 -1
- package/dist/commands-auth.js +32 -14
- package/dist/commands-auth.js.map +1 -1
- package/dist/commands-core.d.ts +10 -1
- package/dist/commands-core.js +13 -4
- package/dist/commands-core.js.map +1 -1
- package/dist/commands-devices.d.ts +10 -1
- package/dist/commands-devices.js +17 -13
- package/dist/commands-devices.js.map +1 -1
- package/dist/commands-doctor.js +158 -71
- package/dist/commands-doctor.js.map +1 -1
- package/dist/commands-harness-context.js +8 -0
- package/dist/commands-harness-context.js.map +1 -1
- package/dist/commands-hooks.js +169 -15
- package/dist/commands-hooks.js.map +1 -1
- package/dist/commands-init.d.ts +16 -0
- package/dist/commands-init.js +254 -91
- package/dist/commands-init.js.map +1 -1
- package/dist/commands-send.d.ts +1 -1
- package/dist/commands-send.js +121 -35
- package/dist/commands-send.js.map +1 -1
- package/dist/commands-update.d.ts +10 -0
- package/dist/commands-update.js +141 -0
- package/dist/commands-update.js.map +1 -0
- package/dist/commands.d.ts +2 -1
- package/dist/commands.js +2 -1
- package/dist/commands.js.map +1 -1
- package/dist/harnesses.d.ts +2 -2
- package/dist/harnesses.js +22 -2
- package/dist/harnesses.js.map +1 -1
- package/dist/hook-adapter.d.ts +2 -0
- package/dist/hook-adapter.js +12 -1
- package/dist/hook-adapter.js.map +1 -1
- package/dist/hooks.d.ts +43 -2
- package/dist/hooks.js +163 -21
- package/dist/hooks.js.map +1 -1
- package/dist/install-hooks.js +15 -9
- package/dist/install-hooks.js.map +1 -1
- package/dist/interactive.js +20 -23
- package/dist/interactive.js.map +1 -1
- package/dist/main-run.d.ts +1 -0
- package/dist/main-run.js +42 -0
- package/dist/main-run.js.map +1 -0
- package/dist/main.js +10 -31
- package/dist/main.js.map +1 -1
- package/dist/node-floor.d.ts +18 -0
- package/dist/node-floor.js +35 -0
- package/dist/node-floor.js.map +1 -0
- package/dist/openclaw-plugin.d.ts +49 -0
- package/dist/openclaw-plugin.js +350 -0
- package/dist/openclaw-plugin.js.map +1 -0
- package/dist/program.d.ts +2 -1
- package/dist/program.js +31 -13
- package/dist/program.js.map +1 -1
- package/dist/question-settlement-process.d.ts +15 -0
- package/dist/question-settlement-process.js +35 -0
- package/dist/question-settlement-process.js.map +1 -0
- package/dist/readiness.d.ts +25 -1
- package/dist/readiness.js +32 -2
- package/dist/readiness.js.map +1 -1
- package/dist/session-activation.js +1 -1
- package/dist/session-activation.js.map +1 -1
- package/dist/setup-destinations.d.ts +23 -0
- package/dist/setup-destinations.js +44 -0
- package/dist/setup-destinations.js.map +1 -0
- package/dist/ui/help.d.ts +4 -5
- package/dist/ui/help.js +5 -6
- package/dist/ui/help.js.map +1 -1
- package/package.json +2 -2
- package/src/cli-bin.test.ts +145 -7
- package/src/cli-bin.ts +176 -10
- package/src/cli-update-contract.ts +9 -0
- package/src/cli-update.test.ts +125 -0
- package/src/client.ts +7 -0
- package/src/codex-wake.ts +5 -1
- package/src/commands-auth.ts +47 -13
- package/src/commands-core.ts +21 -4
- package/src/commands-devices.ts +23 -16
- package/src/commands-doctor.ts +197 -75
- package/src/commands-harness-context.ts +8 -0
- package/src/commands-hooks.ts +210 -18
- package/src/commands-init.ts +275 -92
- package/src/commands-send.ts +145 -39
- package/src/commands-update.ts +180 -0
- package/src/commands.test.ts +701 -53
- package/src/commands.ts +2 -0
- package/src/fixtures/openclaw-2026.7.1-2-agent-local.json +57 -0
- package/src/harness-integration.test.ts +82 -0
- package/src/harnesses.test.ts +9 -1
- package/src/harnesses.ts +24 -2
- package/src/hook-adapter.ts +14 -0
- package/src/hooks.test.ts +275 -0
- package/src/hooks.ts +270 -18
- package/src/install-hooks.test.ts +208 -0
- package/src/install-hooks.ts +19 -9
- package/src/interactive.test.ts +31 -0
- package/src/interactive.ts +20 -26
- package/src/main-run.ts +45 -0
- package/src/main.ts +9 -33
- package/src/node-floor.test.ts +27 -0
- package/src/node-floor.ts +37 -0
- package/src/openclaw-plugin.ts +409 -0
- package/src/program.test.ts +35 -0
- package/src/program.ts +37 -12
- package/src/question-settlement-process.ts +47 -0
- package/src/readiness.ts +35 -2
- package/src/session-activation.ts +1 -1
- package/src/setup-destinations.test.ts +41 -0
- package/src/setup-destinations.ts +50 -0
- package/src/skill.test.ts +13 -2
- package/src/ui/help.test.ts +16 -0
- package/src/ui/help.ts +6 -7
- package/src/commands-devices.test.ts +0 -15
package/src/commands-hooks.ts
CHANGED
|
@@ -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 (
|
|
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(
|
|
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'
|
|
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')
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
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 {
|