@cmodjs/core 1.6.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (128) hide show
  1. package/docs/commands.md +4 -4
  2. package/docs/index.md +12 -11
  3. package/docs/jobs.md +12 -16
  4. package/docs/mod.md +111 -36
  5. package/docs/permissions.md +14 -18
  6. package/docs/session.md +171 -82
  7. package/docs/state.md +5 -5
  8. package/docs/testing.md +57 -41
  9. package/docs/ui.md +9 -9
  10. package/jobs/permissions/decide-permission.d.ts.map +1 -1
  11. package/jobs/permissions/decide-permission.js +1 -1
  12. package/jobs/prompt.d.ts +1 -3
  13. package/jobs/prompt.d.ts.map +1 -1
  14. package/jobs/prompt.js +2 -21
  15. package/jobs/slash-command.d.ts +1 -1
  16. package/jobs/slash-command.d.ts.map +1 -1
  17. package/jobs/slash-command.js +1 -1
  18. package/jobs/status-line.d.ts +0 -1
  19. package/jobs/status-line.d.ts.map +1 -1
  20. package/jobs/status-line.js +3 -4
  21. package/mod.d.ts +53 -105
  22. package/mod.d.ts.map +1 -1
  23. package/mod.js +18 -80
  24. package/package.json +1 -1
  25. package/records.d.ts +1 -1
  26. package/records.js +1 -1
  27. package/register.d.ts.map +1 -1
  28. package/register.js +3 -1
  29. package/runtime/before.d.ts +5 -1
  30. package/runtime/before.d.ts.map +1 -1
  31. package/runtime/before.js +81 -39
  32. package/runtime/claude.d.ts +6 -1
  33. package/runtime/claude.d.ts.map +1 -1
  34. package/runtime/conversations.d.ts.map +1 -1
  35. package/runtime/conversations.js +45 -26
  36. package/runtime/deadline.d.ts +17 -0
  37. package/runtime/deadline.d.ts.map +1 -1
  38. package/runtime/deadline.js +34 -0
  39. package/runtime/dependencies.d.ts +2 -0
  40. package/runtime/dependencies.d.ts.map +1 -1
  41. package/runtime/dependencies.js +12 -0
  42. package/runtime/grants.d.ts.map +1 -1
  43. package/runtime/grants.js +13 -8
  44. package/runtime/hooks.d.ts +28 -11
  45. package/runtime/hooks.d.ts.map +1 -1
  46. package/runtime/hooks.js +20 -123
  47. package/runtime/lifecycle.d.ts +2 -1
  48. package/runtime/lifecycle.d.ts.map +1 -1
  49. package/runtime/lifecycle.js +66 -49
  50. package/runtime/sessions.d.ts +31 -5
  51. package/runtime/sessions.d.ts.map +1 -1
  52. package/runtime/sessions.js +342 -81
  53. package/runtime/tool-calls.d.ts +0 -1
  54. package/runtime/tool-calls.d.ts.map +1 -1
  55. package/runtime/tool-calls.js +0 -19
  56. package/session/context/turn/message/instruction-file.d.ts +9 -0
  57. package/session/context/turn/message/instruction-file.d.ts.map +1 -0
  58. package/session/context/turn/message/instruction-file.js +4 -0
  59. package/session/context/turn/message/message.d.ts +5 -4
  60. package/session/context/turn/message/message.d.ts.map +1 -1
  61. package/session/context/turn/message/message.js +7 -6
  62. package/session/context/turn/message/sender.d.ts +2 -1
  63. package/session/context/turn/message/sender.d.ts.map +1 -1
  64. package/session/context/turn/message/sender.js +4 -1
  65. package/session/context/turn/message/tool-call.d.ts +5 -0
  66. package/session/context/turn/message/tool-call.d.ts.map +1 -1
  67. package/session/context/turn/message/tool-result.d.ts +6 -1
  68. package/session/context/turn/message/tool-result.d.ts.map +1 -1
  69. package/session/context/turn/turn.d.ts +7 -0
  70. package/session/context/turn/turn.d.ts.map +1 -1
  71. package/session/conversation.d.ts +10 -3
  72. package/session/conversation.d.ts.map +1 -1
  73. package/session/conversation.js +14 -5
  74. package/session/goal/goal.d.ts +1 -1
  75. package/session/goal/goal.d.ts.map +1 -1
  76. package/session/goal/task.d.ts +2 -2
  77. package/session/goal/task.d.ts.map +1 -1
  78. package/session/session.d.ts +361 -26
  79. package/session/session.d.ts.map +1 -1
  80. package/session/session.js +40 -0
  81. package/src/jobs/permissions/decide-permission.ts +2 -1
  82. package/src/jobs/prompt.ts +3 -20
  83. package/src/jobs/slash-command.ts +2 -2
  84. package/src/jobs/status-line.ts +4 -7
  85. package/src/mod.ts +52 -147
  86. package/src/records.ts +1 -1
  87. package/src/register.ts +3 -1
  88. package/src/runtime/before.ts +90 -37
  89. package/src/runtime/claude.ts +5 -1
  90. package/src/runtime/conversations.ts +41 -25
  91. package/src/runtime/deadline.ts +38 -0
  92. package/src/runtime/dependencies.ts +12 -0
  93. package/src/runtime/grants.ts +11 -7
  94. package/src/runtime/hooks.ts +48 -116
  95. package/src/runtime/lifecycle.ts +65 -50
  96. package/src/runtime/sessions.ts +366 -90
  97. package/src/runtime/tool-calls.ts +0 -19
  98. package/src/session/context/turn/message/instruction-file.ts +13 -0
  99. package/src/session/context/turn/message/message.ts +11 -9
  100. package/src/session/context/turn/message/sender.ts +5 -2
  101. package/src/session/context/turn/message/tool-call.ts +3 -0
  102. package/src/session/context/turn/message/tool-result.ts +6 -1
  103. package/src/session/context/turn/turn.ts +4 -0
  104. package/src/session/conversation.ts +21 -8
  105. package/src/session/goal/goal.ts +1 -1
  106. package/src/session/goal/task.ts +2 -2
  107. package/src/session/session.ts +227 -24
  108. package/src/testing/claude-code.ts +23 -0
  109. package/src/testing/fake-claude.ts +5 -1
  110. package/src/testing/fake-sessions.ts +38 -24
  111. package/src/testing.ts +84 -16
  112. package/testing/claude-code.d.ts +12 -0
  113. package/testing/claude-code.d.ts.map +1 -0
  114. package/testing/claude-code.js +10 -0
  115. package/testing/fake-claude.d.ts +4 -0
  116. package/testing/fake-claude.d.ts.map +1 -1
  117. package/testing/fake-claude.js +4 -1
  118. package/testing/fake-sessions.d.ts +8 -6
  119. package/testing/fake-sessions.d.ts.map +1 -1
  120. package/testing/fake-sessions.js +35 -20
  121. package/testing.d.ts +38 -9
  122. package/testing.d.ts.map +1 -1
  123. package/testing.js +54 -5
  124. package/docs/hooks.md +0 -259
  125. package/session/context/turn/message/document.d.ts +0 -7
  126. package/session/context/turn/message/document.d.ts.map +0 -1
  127. package/session/context/turn/message/document.js +0 -1
  128. package/src/session/context/turn/message/document.ts +0 -8
@@ -1,6 +1,6 @@
1
1
  import type { Args, EventResult, Frozen, HookBudget, PluginOptions, RenderElement, Timer } from 'claude-code'
2
2
  import type { Step } from '../installer.js'
3
- import { hooks, PreToolUse, type Allowed, type Disposable, type Hook, type HookInput, type HookListener, type HookOptions, type Imports, type Mod, type ModDefinition, type PaneHandle, type SettingsFile, type Toast } from '../mod.js'
3
+ import type { Allowed, FileChange, Disposable, Imports, Mod, ModDefinition, PaneHandle, SettingsFile, SettingsFileChange, SettingsFileChangeListener, Toast } from '../mod.js'
4
4
  import type { Options, OptionValues } from '../options.js'
5
5
  import { changelogPage, consentPath, dataFolder, finishStepsMethod, isAtLeast, marketplaceOf, oldestCmodFor, openPageMethod, parseConsent, pendingStepsMethod, parseEvent, permissionWords, readRecord, readSteps, recordPath, scriptsSha256, settingsPagesMethod, storeFolder, updatesToTurnOn, type ReadFile, type RunnerEvent, type Steps } from '../records.js'
6
6
  import { homePrefix, ifMissing, relativePath } from '../utils/paths.js'
@@ -10,9 +10,9 @@ import { beforeDeadline } from './deadline.js'
10
10
  import { announcedOf, announcementMs, announcementOf } from './announce.js'
11
11
  import { bind } from './bound.js'
12
12
  import { serveConversations } from './conversations.js'
13
- import { answerCall, cmodPluginName, dependencyCalls, withBareRefusals } from './dependencies.js'
13
+ import { answerCall, answeringOwnCalls, cmodPluginName, dependencyCalls, withBareRefusals } from './dependencies.js'
14
14
  import { keepEngine } from './engine.js'
15
- import { classicHook, heldDecisionHook, permissionEvents, preToolUseHook, userSkillHook, type HeldDecisions, type RoutedEvent } from './hooks.js'
15
+ import { classicHook, permissionEvents, userSkillHook, type Hook, type HookListener, type RoutedEvent } from './hooks.js'
16
16
  import { checkedAnswer, checkingGrants, checkSettings, checksAnswers, checkWrite, isCovered, itemOf, passedDown, programAddresses, type AnswerCheck, type Grants } from './grants.js'
17
17
  import { cmodError, lastLineOf, locatingPrograms, readLines, type Programs } from './programs.js'
18
18
  import { createModFiles, resolvedPath, type Places } from './metadata.js'
@@ -51,6 +51,7 @@ export type Lifecycle<State extends object> = {
51
51
  readonly toasts: readonly ShownToast[]
52
52
  start(claude: Claude, read: (claude: Claude) => Promise<Plugin>): Promise<void>
53
53
  route<N extends RoutedEvent>(event: N, e: Frozen<Args<N>>, next: RouterNext<N>): Promise<EventResult<N>>
54
+ raise(method: string, payload: Readonly<Record<string, unknown>>): Promise<unknown>
54
55
  }
55
56
 
56
57
  type ModRuntime = {
@@ -70,6 +71,7 @@ type ActiveMod<State extends object, Declared extends Options> = {
70
71
  readonly mod: Mod<State, Declared>
71
72
  readonly added: readonly string[]
72
73
  readonly imports: Imports
74
+ readonly raise: (method: string, payload: Readonly<Record<string, unknown>>) => Promise<unknown>
73
75
  }
74
76
 
75
77
 
@@ -164,6 +166,7 @@ export function createLifecycle<State extends object, Declared extends Options =
164
166
  let activation: Promise<void> | undefined
165
167
  let mod: Mod<State> | undefined
166
168
  let imports: Imports | undefined
169
+ let raise: ActiveMod<State, Declared>['raise'] | undefined
167
170
  let failure: unknown
168
171
  let pluginOptions = options
169
172
  const installer = createInstaller(definition.name, () => claude())
@@ -325,6 +328,7 @@ export function createLifecycle<State extends object, Declared extends Options =
325
328
  const { added } = active
326
329
  mod = active.mod
327
330
  imports = active.imports
331
+ raise = active.raise
328
332
  router = activeRouter
329
333
  phase = 'active'
330
334
  runtime.claude.ui.invalidate('ui.render')
@@ -560,6 +564,11 @@ export function createLifecycle<State extends object, Declared extends Options =
560
564
  get toasts() {
561
565
  return runtime?.toaster.shown ?? []
562
566
  },
567
+ async raise(method, payload) {
568
+ if (raise === undefined || imports === undefined) throw new Error(`${definition.name} has not started, so ${method} has no listener yet.`)
569
+ bind(imports)
570
+ return raise(method, payload)
571
+ },
563
572
  async start(claudeCalls, read) {
564
573
  if (runtime !== undefined) return
565
574
  programs = locatingPrograms(claudeCalls)
@@ -609,9 +618,8 @@ export function createLifecycle<State extends object, Declared extends Options =
609
618
  async function createMod<State extends object, Declared extends Options>(definition: ModDefinition<State, string, Declared>, runtime: ModRuntime): Promise<ActiveMod<State, Declared>> {
610
619
  const { router, toaster } = runtime
611
620
  const report = (failure: Failure) => void toaster.fail(failure)
612
- const claude = withBareRefusals(checkingKeys(runtime.claude, toaster.toast, runtime.steps.keys ?? {}))
621
+ const claude = answeringOwnCalls(withBareRefusals(checkingKeys(runtime.claude, toaster.toast, runtime.steps.keys ?? {})), router)
613
622
  const added: string[] = []
614
- const hooksAdded: Hook[] = []
615
623
  const announce = (feature: string) => {
616
624
  added.push(feature)
617
625
  }
@@ -680,7 +688,7 @@ async function createMod<State extends object, Declared extends Options>(definit
680
688
  }))
681
689
  const modState = createState<State>({ name: definition.name, initial: definition.state ?? {}, session, root, claude, report, changed: area.changed, forkParent })
682
690
  const modOptions = createOptions({ name: definition.name, declared: definition.options ?? {}, fromClaude: runtime.options, claude, report, changed: modState.changed })
683
- const held: HeldDecisions | undefined = runtime.checksPermissions() ? new Map() : undefined
691
+ const hookOn = <H extends Hook>(hook: H, listener: HookListener<H>): Disposable => router.add(`classic.${hook}`, classicHook(definition.name, hook, listener, report))
684
692
  const expand = (path: string) => (homePrefix.test(path) ? resolvedPath(path, places) : path)
685
693
  const settingsFile = (source: 'user' | 'project' | 'local'): SettingsFile => ({
686
694
  read: () => checked.settings.read({ source }),
@@ -689,8 +697,17 @@ async function createMod<State extends object, Declared extends Options>(definit
689
697
  await claude.cmod.call({ to: cmodPluginName, method: 'updateSettings', input: { mod: definition.name, source, project: modState.root, values } })
690
698
  },
691
699
  })
692
- const client = createSessionsClient({ name: definition.name, claude: checked, report: (id, message, error) => report({ id, message, error }), isGranted: (permission) => flag(permission) })
700
+ const client = createSessionsClient({
701
+ name: definition.name,
702
+ claude: checked,
703
+ report: (id, message, error) => report({ id, message, error }),
704
+ isGranted: (permission) => flag(permission),
705
+ hookOn: (hook, listener) => hookOn(hook as Hook, listener as HookListener<Hook>),
706
+ routeOn: (event, hook) => router.add(event, hook as never),
707
+ })
693
708
  let followed: Parameters<typeof client.follow>[0] | undefined
709
+ let isSetUp = false
710
+ const watches = new Set<{ readonly paths: readonly string[]; readonly listener: (change: FileChange) => unknown }>()
694
711
  const followFolders = (project: string, folder: string) => {
695
712
  if (followed === undefined) return
696
713
  followed = { ...followed, project, cwd: folder }
@@ -698,7 +715,16 @@ async function createMod<State extends object, Declared extends Options>(definit
698
715
  }
699
716
  const imports: Imports = {
700
717
  sessions: client.sessions,
701
- settings: {
718
+ commands: {
719
+ list: async () => (await checked.command.list()).map(({ name, description, source, plugin }) => ({ name, description, source, ...(plugin === undefined ? {} : { plugin }) })),
720
+ run: async (command, args) => {
721
+ const { text, context } = await checked.command.run({ command, ...(args === undefined ? {} : { args }) })
722
+ return { ...(text === undefined ? {} : { text }), ...(context === undefined || context.length === 0 ? {} : { note: context.join('\n') }) }
723
+ },
724
+ beforeRun: (...args: unknown[]) => client.listen('beforeRun', args),
725
+ onRun: (...args: unknown[]) => client.listen('onRun', args),
726
+ },
727
+ config: {
702
728
  list: async () =>
703
729
  (await checked.config.list()).map(({ key, label, description, kind, value, options, provider, isLocked }) => ({
704
730
  key,
@@ -710,9 +736,21 @@ async function createMod<State extends object, Declared extends Options>(definit
710
736
  ...(options === undefined ? {} : { options }),
711
737
  ...(provider.plugin === 'engine' ? {} : { plugin: provider.plugin }),
712
738
  })),
739
+ },
740
+ settings: {
713
741
  user: settingsFile('user'),
714
742
  project: settingsFile('project'),
715
743
  local: settingsFile('local'),
744
+ beforeFileChange: (matcherOrListener: unknown, maybeListener?: SettingsFileChangeListener) => {
745
+ const listener = (maybeListener ?? matcherOrListener) as SettingsFileChangeListener
746
+ const matcher = maybeListener === undefined ? undefined : matcherOrListener
747
+ return hookOn('ConfigChange', async ({ source, file_path }) => {
748
+ const change: SettingsFileChange = { source: source === 'skills' ? 'skills' : (source.replace('_settings', '') as SettingsFileChange['source']), ...(file_path === undefined ? {} : { path: file_path }) }
749
+ if (matcher !== undefined && !matches(change, matcher)) return undefined
750
+ const answer = await listener(change)
751
+ return answer === undefined ? undefined : { decision: 'block', reason: answer.refuse }
752
+ })
753
+ },
716
754
  },
717
755
  model: { complete: (completion, options) => checked.model.complete(completion, options) },
718
756
  fs: {
@@ -725,6 +763,19 @@ async function createMod<State extends object, Declared extends Options>(definit
725
763
  const { kind, size, mtimeMs, isLink, realPath } = await checked.fs.stat(expand(path), options)
726
764
  return { isFile: kind === 'file', isFolder: kind === 'dir', isLink, size, modifiedAt: mtimeMs, ...(realPath === undefined ? {} : { realPath }) }
727
765
  },
766
+ onChange({ path }, listener) {
767
+ if (isSetUp) throw new Error(`${definition.name}: fs.onChange works only in setup, because Claude Code reads the paths it watches when a session starts.`)
768
+ const paths = [path].flat().map((each) => resolvedPath(each, places))
769
+ const watch = { paths, listener }
770
+ watches.add(watch)
771
+ if (watches.size === 1) {
772
+ hookOn('SessionStart', () => ({ hookSpecificOutput: { watchPaths: [...new Set([...watches].flatMap((each) => each.paths))] } }))
773
+ hookOn('FileChanged', ({ file_path, event }) => {
774
+ for (const each of watches) if (each.paths.some((watched) => relativePath(watched, file_path) !== undefined)) void Promise.resolve(each.listener({ path: file_path, type: event })).catch((error: unknown) => report({ id: 'fs:onChange', message: 'The fs.onChange listener failed', error }))
775
+ })
776
+ }
777
+ return { dispose: () => void watches.delete(watch) }
778
+ },
728
779
  },
729
780
  metadata: {
730
781
  read: (path) => files.read(path),
@@ -787,23 +838,10 @@ async function createMod<State extends object, Declared extends Options>(definit
787
838
  files: allowed('files'),
788
839
  config: allowed('config'),
789
840
  },
790
- on(hook: Hook, matcherOrListener: unknown, listenerOrOptions?: unknown, maybeOptions?: HookOptions): Disposable {
791
- const hasMatcher = typeof listenerOrOptions === 'function'
792
- const listener = (hasMatcher ? listenerOrOptions : matcherOrListener) as HookListener<Hook>
793
- const matcher = hasMatcher ? matcherOrListener : undefined
794
- const options = (hasMatcher ? maybeOptions : listenerOrOptions) as HookOptions | undefined
795
- if (!hooks.includes(hook)) throw new Error(`${definition.name}: mod.on takes one of Claude Code's hooks, such as PreToolUse, not ${String(hook)}.`)
796
- const matched: HookListener<Hook> = matcher === undefined ? listener : (input) => (matches(input, matcher) ? listener(input) : undefined)
797
- const bounded = options?.timeoutMs === undefined ? matched : timedHook(hook, matched, checkedMs(definition.name, `the ${hook} hook's timeoutMs`, options.timeoutMs), claude, toaster.toast)
798
- if (!hooksAdded.includes(hook)) hooksAdded.push(hook)
799
- if (hook === PreToolUse) return router.add('tool.call', preToolUseHook(definition.name, bounded as HookListener<typeof PreToolUse>, claude, agents, held))
800
- const classic: Exclude<Hook, typeof PreToolUse> = hook
801
- return router.add(`classic.${classic}`, classicHook(definition.name, classic, bounded as HookListener<typeof classic>, claude, agents, report))
802
- },
803
841
  every: (ms, listener) => timer(ms, 'every', claude.clock.every, listener),
804
842
  after: (ms, listener) => timer(ms, 'after', claude.clock.after, listener),
805
843
  use: (job) => job({ mod, announce, reserveName }),
806
- log: (text) => claude.ui.log(`${definition.name}: ${text}`, { to: 'debug' }),
844
+ log: (text, options) => claude.ui.log(`${definition.name}: ${text}`, { to: options?.to ?? 'debug' }),
807
845
  ui: { ...area.ui, notice: (callId, text) => claude.ui.notice(callId, text) },
808
846
  settings: settingsPages,
809
847
  dependencies: dependencyCalls(claude, (call, task) => beforeDeadline(claude, { ms: dependencyCallMs }, call, task)),
@@ -829,8 +867,8 @@ async function createMod<State extends object, Declared extends Options>(definit
829
867
  loadedCwd = nextCwd
830
868
  if (!hasMovedRoot) area.changed()
831
869
  if (nextCwd === oldCwd) return
832
- const moved: HookInput<'CwdChanged'> = { session_id: await claude.session.id(), cwd: nextCwd, hook_event_name: 'CwdChanged', old_cwd: oldCwd, new_cwd: nextCwd }
833
- await router.dispatch('classic.CwdChanged', moved as Frozen<Args<'classic.CwdChanged'>>, async () => ({}))
870
+ const moved = { session_id: await claude.session.id(), cwd: nextCwd, hook_event_name: 'CwdChanged', old_cwd: oldCwd, new_cwd: nextCwd }
871
+ await router.dispatch('classic.CwdChanged', moved as unknown as Frozen<Args<'classic.CwdChanged'>>, async () => ({}))
834
872
  } catch (error) {
835
873
  report({ id: 'cmod:follow', message: `Keeps the state of ${modState.root} until the next prompt or folder move`, error })
836
874
  }
@@ -883,8 +921,8 @@ async function createMod<State extends object, Declared extends Options>(definit
883
921
  if (e.to !== definition.name) return next(e)
884
922
  const call = e as typeof e & { readonly caller?: string }
885
923
  if (e.method === conversationMethods.changed && call.caller === cmodPluginName) {
886
- if (!flag('conversation')) return { value: null }
887
924
  const { session: changedSession, change, isInProject } = e.input as { readonly session: string; readonly change: SessionChange; readonly isInProject?: boolean }
925
+ if (changedSession !== client.sessions.current.id && !flag('conversation')) return { value: null }
888
926
  client.changed(changedSession, change, isInProject !== false)
889
927
  return { value: null }
890
928
  }
@@ -915,14 +953,13 @@ async function createMod<State extends object, Declared extends Options>(definit
915
953
  if (missing.length > 0) throw new MissingOptions(missing, missing.map((key) => definition.options?.[key]?.title ?? key))
916
954
  await failsAs('the details of its session did not load', () => followCurrent(undefined))
917
955
  await failsAs('its setup function threw', () => definition.setup(mod))
956
+ isSetUp = true
918
957
  const permissionHooks = permissionEvents.filter((event) => router.has(event))
919
958
  if (permissionHooks.length > 0 && !runtime.checksPermissions()) {
920
959
  throw new Error(`it decides permissions on ${listed(permissionHooks)}, so hooks/register.ts must call registerPermissionCheck(addHook) after registerMod`)
921
960
  }
922
- if (held !== undefined && hooksAdded.includes(PreToolUse)) router.add('tool.check', heldDecisionHook(held))
923
961
  await failsAs('its open panes did not load', () => area.restorePanes())
924
- if (hooksAdded.length > 0) added.push(`${hooksAdded.length === 1 ? 'a hook' : 'hooks'} on ${listed(hooksAdded)}`)
925
- return { mod, added, imports }
962
+ return { mod, added, imports, raise: client.raise }
926
963
  }
927
964
 
928
965
  function checkingKeys(claude: Claude, toast: Toast, keys: Readonly<Record<string, string>>): Claude {
@@ -946,28 +983,6 @@ function checkedMs(name: string, subject: string, ms: number): number {
946
983
  return ms
947
984
  }
948
985
 
949
- function timedHook<H extends Hook>(event: H, hook: HookListener<H>, ms: number, claude: Claude, toast: Toast): HookListener<H> {
950
- return (input) =>
951
- new Promise((resolve, reject) => {
952
- const timer = claude.clock.after(ms, () => {
953
- toast.warning(`The ${event} hook passed its ${ms / 1000} s timeout`, { id: `timeout:${event}` })
954
- resolve(undefined)
955
- })
956
- Promise.resolve()
957
- .then(() => hook(input))
958
- .then(
959
- (answer) => {
960
- timer.cancel()
961
- resolve(answer)
962
- },
963
- (error: unknown) => {
964
- timer.cancel()
965
- reject(error)
966
- },
967
- )
968
- })
969
- }
970
-
971
986
  async function failsAs(subject: string, step: () => unknown): Promise<void> {
972
987
  try {
973
988
  await step()