@cmodjs/core 0.1.14 → 0.2.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 (96) hide show
  1. package/docs/commands.md +32 -21
  2. package/docs/dependencies.md +8 -0
  3. package/docs/index.md +12 -4
  4. package/docs/install-steps.md +36 -4
  5. package/docs/jobs.md +25 -9
  6. package/docs/mod.md +103 -37
  7. package/docs/options.md +93 -0
  8. package/docs/permissions.md +88 -0
  9. package/docs/state.md +4 -31
  10. package/docs/testing.md +13 -2
  11. package/installer.d.ts +10 -0
  12. package/installer.d.ts.map +1 -0
  13. package/installer.js +8 -0
  14. package/jobs/context.d.ts +2 -2
  15. package/jobs/context.d.ts.map +1 -1
  16. package/jobs/context.js +5 -3
  17. package/jobs/permissions.js +1 -1
  18. package/jobs/program.d.ts.map +1 -1
  19. package/jobs/program.js +5 -1
  20. package/jobs/prompt.js +5 -5
  21. package/jobs/schedule.d.ts.map +1 -1
  22. package/jobs/schedule.js +2 -1
  23. package/jobs/slash-command.d.ts.map +1 -1
  24. package/jobs/slash-command.js +3 -2
  25. package/jobs/status-line.d.ts.map +1 -1
  26. package/jobs/status-line.js +3 -2
  27. package/jobs/tool.d.ts.map +1 -1
  28. package/jobs/tool.js +3 -2
  29. package/mod.d.ts +48 -10
  30. package/mod.d.ts.map +1 -1
  31. package/mod.js +3 -0
  32. package/options.d.ts +58 -0
  33. package/options.d.ts.map +1 -0
  34. package/options.js +66 -0
  35. package/package.json +1 -1
  36. package/records.d.ts +16 -1
  37. package/records.d.ts.map +1 -1
  38. package/records.js +119 -7
  39. package/register.d.ts +4 -2
  40. package/register.d.ts.map +1 -1
  41. package/register.js +8 -2
  42. package/runtime/claude.d.ts +5 -1
  43. package/runtime/claude.d.ts.map +1 -1
  44. package/runtime/grants.d.ts +28 -0
  45. package/runtime/grants.d.ts.map +1 -0
  46. package/runtime/grants.js +286 -0
  47. package/runtime/installer.d.ts +24 -0
  48. package/runtime/installer.d.ts.map +1 -0
  49. package/runtime/installer.js +118 -0
  50. package/runtime/lifecycle.d.ts +6 -3
  51. package/runtime/lifecycle.d.ts.map +1 -1
  52. package/runtime/lifecycle.js +226 -36
  53. package/runtime/metadata.d.ts +20 -0
  54. package/runtime/metadata.d.ts.map +1 -0
  55. package/runtime/metadata.js +143 -0
  56. package/runtime/options.d.ts +21 -0
  57. package/runtime/options.d.ts.map +1 -0
  58. package/runtime/options.js +100 -0
  59. package/runtime/state.d.ts +1 -0
  60. package/runtime/state.d.ts.map +1 -1
  61. package/runtime/state.js +5 -75
  62. package/src/installer.ts +17 -0
  63. package/src/jobs/context.ts +5 -3
  64. package/src/jobs/permissions.ts +1 -1
  65. package/src/jobs/program.ts +4 -1
  66. package/src/jobs/prompt.ts +5 -5
  67. package/src/jobs/schedule.ts +2 -1
  68. package/src/jobs/slash-command.ts +3 -2
  69. package/src/jobs/status-line.ts +3 -2
  70. package/src/jobs/tool.ts +3 -2
  71. package/src/mod.ts +46 -11
  72. package/src/options.ts +91 -0
  73. package/src/records.ts +131 -8
  74. package/src/register.ts +12 -3
  75. package/src/runtime/claude.ts +2 -0
  76. package/src/runtime/grants.ts +306 -0
  77. package/src/runtime/installer.ts +169 -0
  78. package/src/runtime/lifecycle.ts +227 -45
  79. package/src/runtime/metadata.ts +153 -0
  80. package/src/runtime/options.ts +102 -0
  81. package/src/runtime/state.ts +7 -70
  82. package/src/testing/fake-claude.ts +6 -1
  83. package/src/testing/fake-files.ts +9 -3
  84. package/src/testing.ts +40 -15
  85. package/src/utils/metadata.ts +242 -0
  86. package/testing/fake-claude.d.ts +4 -0
  87. package/testing/fake-claude.d.ts.map +1 -1
  88. package/testing/fake-claude.js +5 -1
  89. package/testing/fake-files.d.ts.map +1 -1
  90. package/testing/fake-files.js +9 -3
  91. package/testing.d.ts +6 -2
  92. package/testing.d.ts.map +1 -1
  93. package/testing.js +29 -13
  94. package/utils/metadata.d.ts +21 -0
  95. package/utils/metadata.d.ts.map +1 -0
  96. package/utils/metadata.js +250 -0
@@ -0,0 +1,118 @@
1
+ import { permissionWords } from '../records.js';
2
+ import { Box, Button, drawWith, Input, Text } from '../ui/elements.js';
3
+ import { messageOf } from '../utils/text.js';
4
+ export const installerPaneId = 'cmod-installer';
5
+ export function createInstaller(name, claude) {
6
+ let page;
7
+ let isOpen = false;
8
+ const show = async (build, cancelled) => {
9
+ page?.cancel();
10
+ const answer = new Promise((resolve) => {
11
+ const done = (value) => {
12
+ if (page === shown)
13
+ page = undefined;
14
+ resolve(value);
15
+ };
16
+ const shown = { ...build(done), cancel: () => done(cancelled) };
17
+ page = shown;
18
+ });
19
+ if (!isOpen) {
20
+ isOpen = true;
21
+ await claude().ui.open({ id: installerPaneId, title: `Install ${name}`, focus: true, holdToasts: true, closeOnEscape: true });
22
+ }
23
+ claude().ui.invalidate('ui.render');
24
+ return answer;
25
+ };
26
+ const frame = (title, body, actions) => Box({ flexDirection: 'column', gap: 1, paddingX: 1, children: [Text({ bold: true, children: title }), ...body, Box({ gap: 3, children: actions })] });
27
+ const bullet = (text) => Text({ children: [Text({ color: 'claude', children: '◆ ' }), text] });
28
+ return {
29
+ draws: (e) => e.component === 'Pane' && e.requestId === installerPaneId,
30
+ draw(e) {
31
+ return drawWith(claude().ui.resolve(e), () => page?.draw(e.props) ?? Text({ dimColor: true, children: `${name} is installing.` }));
32
+ },
33
+ closed(id) {
34
+ if (id !== installerPaneId)
35
+ return;
36
+ isOpen = false;
37
+ page?.cancel();
38
+ },
39
+ consent({ install, uninstall, keys, permissions, updates }) {
40
+ const changes = [
41
+ ...(install === '' ? [] : [`Run ${install} now${uninstall === '' ? '' : `, and ${uninstall} when you remove it`}`]),
42
+ ...(install === '' && uninstall !== '' ? [`Run ${uninstall} when you remove it`] : []),
43
+ ...(keys === '' ? [] : [`Bind ${keys}`]),
44
+ ...(updates === undefined ? [] : [`Update ${name} automatically from the ${updates} marketplace`]),
45
+ ];
46
+ return show((answer) => ({
47
+ draw: () => frame(`${name} wants to:`, [
48
+ ...(permissions.length === 0 ? [] : [Box({ flexDirection: 'column', children: permissions.map((item) => bullet(permissionWords(item))) })]),
49
+ ...(changes.length === 0 ? [] : [Text({ children: permissions.length === 0 ? 'Change your computer:' : 'and change your computer:' }), Box({ flexDirection: 'column', children: changes.map(bullet) })]),
50
+ Text({ dimColor: true, children: 'You can turn each permission off later in /mods.' }),
51
+ ], [Button({ key: 'not-now', hotkey: 'n', label: 'Not now', onPress: () => answer(false) }), Button({ key: 'accept', hotkey: 'a', label: 'Accept', onPress: () => answer(true) })]),
52
+ }), false);
53
+ },
54
+ options(missing, save) {
55
+ const saved = new Map();
56
+ const failures = new Map();
57
+ return show((answer) => ({
58
+ draw: () => frame(`${name} needs ${missing.length === 1 ? 'one setting' : `${missing.length} settings`}:`, missing.map(([key, option]) => {
59
+ const label = Text({ children: [Text({ bold: true, children: option.title }), option.description === '' ? '' : Text({ dimColor: true, children: ` ${option.description}` })] });
60
+ if (option.kind === 'secret')
61
+ return Box({ flexDirection: 'column', children: [label, Text({ dimColor: true, children: `Set it in /config, where Claude Code keeps it in your keychain, then press Next.` })] });
62
+ const failure = failures.get(key);
63
+ const field = Input({
64
+ key: `option:${key}`,
65
+ label: '› ',
66
+ placeholder: option.kind === 'choice' ? (option.choices ?? []).join(', ') : option.kind === 'toggle' ? 'true or false' : option.kind === 'list' ? 'comma-separated' : '',
67
+ value: saved.get(key) ?? '',
68
+ submitLabel: 'save',
69
+ onSubmit: (value) => void save(key, value)
70
+ .then((refused) => {
71
+ if (refused === undefined) {
72
+ saved.set(key, value);
73
+ failures.delete(key);
74
+ }
75
+ else
76
+ failures.set(key, refused);
77
+ })
78
+ .catch((error) => failures.set(key, messageOf(error)))
79
+ .finally(() => claude().ui.invalidate('ui.render')),
80
+ });
81
+ const status = failure !== undefined ? Text({ color: 'error', children: failure }) : saved.has(key) ? Text({ color: 'success', children: '✔ saved' }) : Text({ children: '' });
82
+ return Box({ flexDirection: 'column', children: [label, field, status] });
83
+ }), [Button({ key: 'not-now', hotkey: 'n', label: 'Not now', onPress: () => answer(false) }), Button({ key: 'next', hotkey: 'x', label: 'Next', onPress: () => answer(true) })]),
84
+ }), false);
85
+ },
86
+ step(mod, step, index, total) {
87
+ let notice;
88
+ return show((answer) => ({
89
+ draw: (props) => {
90
+ let body;
91
+ try {
92
+ body = step.render(mod, props);
93
+ }
94
+ catch (error) {
95
+ body = Text({ color: 'error', children: `The ${step.title} step could not draw: ${messageOf(error)}` });
96
+ }
97
+ const next = async () => {
98
+ const isDone = await Promise.resolve(step.isDone(mod)).catch(() => false);
99
+ if (isDone)
100
+ answer(true);
101
+ else {
102
+ notice = `Finish ${step.title} first.`;
103
+ claude().ui.invalidate('ui.render');
104
+ }
105
+ };
106
+ return frame(`${step.title} (${index} of ${total})`, [body, ...(notice === undefined ? [] : [Text({ color: 'warning', children: notice })])], [Button({ key: 'not-now', hotkey: 'n', label: 'Not now', onPress: () => answer(false) }), Button({ key: 'next', hotkey: 'x', label: index === total ? 'Finish' : 'Next', onPress: () => void next() })]);
107
+ },
108
+ }), false);
109
+ },
110
+ async close() {
111
+ page = undefined;
112
+ if (!isOpen)
113
+ return;
114
+ isOpen = false;
115
+ await claude().ui.close({ id: installerPaneId });
116
+ },
117
+ };
118
+ }
@@ -1,5 +1,7 @@
1
- import type { Args, EventResult, Frozen } from 'claude-code';
1
+ import type { Args, EventResult, Frozen, PluginOptions } from 'claude-code';
2
2
  import type { Mod, ModDefinition } from '../mod.js';
3
+ import type { Options } from '../options.js';
4
+ import { type Steps } from '../records.js';
3
5
  import type { Claude } from './claude.js';
4
6
  import { type RoutedEvent } from './hooks.js';
5
7
  import { type RouterNext } from './router.js';
@@ -10,7 +12,8 @@ export type Plugin = {
10
12
  readonly store: string;
11
13
  readonly isInstalled: boolean;
12
14
  readonly shouldRecord: boolean;
13
- readonly keys: Readonly<Record<string, string>>;
15
+ readonly steps: Steps;
16
+ readonly granted: readonly string[];
14
17
  };
15
18
  export type Phase = 'starting' | 'installing' | 'waiting' | 'declined' | 'failed' | 'ready' | 'active';
16
19
  export type Lifecycle<State extends object> = {
@@ -21,5 +24,5 @@ export type Lifecycle<State extends object> = {
21
24
  route<N extends RoutedEvent>(event: N, e: Frozen<Args<N>>, next: RouterNext<N>): Promise<EventResult<N>>;
22
25
  };
23
26
  export declare function readPlugin(claude: Claude): Promise<Plugin>;
24
- export declare function createLifecycle<State extends object>(definition: ModDefinition<State>, checksPermissions?: () => boolean): Lifecycle<State>;
27
+ export declare function createLifecycle<State extends object, Declared extends Options = Options>(definition: ModDefinition<State, string, Declared>, checksPermissions?: () => boolean, options?: PluginOptions, asksPerson?: boolean): Lifecycle<State>;
25
28
  //# sourceMappingURL=lifecycle.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"lifecycle.d.ts","sourceRoot":"","sources":["../src/runtime/lifecycle.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,WAAW,EAAE,MAAM,EAAiE,MAAM,aAAa,CAAA;AAC3H,OAAO,KAAK,EAAa,GAAG,EAAE,aAAa,EAAqB,MAAM,WAAW,CAAA;AAIjF,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AAGzC,OAAO,EAAsG,KAAK,WAAW,EAAE,MAAM,YAAY,CAAA;AACjJ,OAAO,EAA6B,KAAK,UAAU,EAAE,MAAM,aAAa,CAAA;AAKxE,MAAM,MAAM,MAAM,GAAG;IACnB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,OAAO,EAAE,MAAM,GAAG,SAAS,CAAA;IACpC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,QAAQ,CAAC,WAAW,EAAE,OAAO,CAAA;IAC7B,QAAQ,CAAC,YAAY,EAAE,OAAO,CAAA;IAC9B,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAA;CAChD,CAAA;AAED,MAAM,MAAM,KAAK,GAAG,UAAU,GAAG,YAAY,GAAG,SAAS,GAAG,UAAU,GAAG,QAAQ,GAAG,OAAO,GAAG,QAAQ,CAAA;AAEtG,MAAM,MAAM,SAAS,CAAC,KAAK,SAAS,MAAM,IAAI;IAC5C,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAA;IACrB,QAAQ,CAAC,GAAG,EAAE,GAAG,CAAC,KAAK,CAAC,GAAG,SAAS,CAAA;IACpC,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAA;IACzB,KAAK,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAC/E,KAAK,CAAC,CAAC,SAAS,WAAW,EAAE,KAAK,EAAE,CAAC,EAAE,CAAC,EAAE,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,UAAU,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,CAAA;CACzG,CAAA;AAkCD,wBAAsB,UAAU,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAyBhE;AAkCD,wBAAgB,eAAe,CAAC,KAAK,SAAS,MAAM,EAAE,UAAU,EAAE,aAAa,CAAC,KAAK,CAAC,EAAE,iBAAiB,GAAE,MAAM,OAAoB,GAAG,SAAS,CAAC,KAAK,CAAC,CAiPvJ"}
1
+ {"version":3,"file":"lifecycle.d.ts","sourceRoot":"","sources":["../src/runtime/lifecycle.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,WAAW,EAAE,MAAM,EAA0B,aAAa,EAAyC,MAAM,aAAa,CAAA;AAC1I,OAAO,KAAK,EAAa,GAAG,EAAE,aAAa,EAAiC,MAAM,WAAW,CAAA;AAC7F,OAAO,KAAK,EAAE,OAAO,EAAgB,MAAM,eAAe,CAAA;AAC1D,OAAO,EAAgS,KAAK,KAAK,EAAE,MAAM,eAAe,CAAA;AAGxU,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AAGzC,OAAO,EAAsG,KAAK,WAAW,EAAE,MAAM,YAAY,CAAA;AAMjJ,OAAO,EAA6B,KAAK,UAAU,EAAE,MAAM,aAAa,CAAA;AAKxE,MAAM,MAAM,MAAM,GAAG;IACnB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,OAAO,EAAE,MAAM,GAAG,SAAS,CAAA;IACpC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,QAAQ,CAAC,WAAW,EAAE,OAAO,CAAA;IAC7B,QAAQ,CAAC,YAAY,EAAE,OAAO,CAAA;IAC9B,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAA;IACrB,QAAQ,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,CAAA;CACpC,CAAA;AAED,MAAM,MAAM,KAAK,GAAG,UAAU,GAAG,YAAY,GAAG,SAAS,GAAG,UAAU,GAAG,QAAQ,GAAG,OAAO,GAAG,QAAQ,CAAA;AAEtG,MAAM,MAAM,SAAS,CAAC,KAAK,SAAS,MAAM,IAAI;IAC5C,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAA;IACrB,QAAQ,CAAC,GAAG,EAAE,GAAG,CAAC,KAAK,CAAC,GAAG,SAAS,CAAA;IACpC,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAA;IACzB,KAAK,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAC/E,KAAK,CAAC,CAAC,SAAS,WAAW,EAAE,KAAK,EAAE,CAAC,EAAE,CAAC,EAAE,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,UAAU,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,CAAA;CACzG,CAAA;AAqCD,wBAAsB,UAAU,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CA2BhE;AAkCD,wBAAgB,eAAe,CAAC,KAAK,SAAS,MAAM,EAAE,QAAQ,SAAS,OAAO,GAAG,OAAO,EACtF,UAAU,EAAE,aAAa,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,CAAC,EAClD,iBAAiB,GAAE,MAAM,OAAoB,EAC7C,OAAO,GAAE,aAAkB,EAC3B,UAAU,UAAO,GAChB,SAAS,CAAC,KAAK,CAAC,CA0VlB"}
@@ -1,9 +1,14 @@
1
- import { dataFolder, isAtLeast, oldestCmodFor, parseEvent, readRecord, readSteps, scriptsSha256, storeFolder } from '../records.js';
1
+ import { consentPath, dataFolder, finishStepsMethod, isAtLeast, marketplaceOf, oldestCmodFor, openPageMethod, parseConsent, pendingStepsMethod, parseEvent, permissionWords, readRecord, readSteps, scriptsSha256, settingsPagesMethod, storeFolder, updatesToTurnOn } from '../records.js';
2
2
  import { relativePath } from '../utils/paths.js';
3
3
  import { formatExit, listed, messageOf } from '../utils/text.js';
4
4
  import { beforeDeadline } from './deadline.js';
5
5
  import { answerCall, dependencyCalls, notInstalled } from './dependencies.js';
6
6
  import { classicHook, heldDecisionHook, permissionEvents, preToolUseHook, userSkillHook } from './hooks.js';
7
+ import { checkedAnswer, checkingGrants, checksAnswers, checkWrite, isCovered, itemOf, passedDown } from './grants.js';
8
+ import { createModFiles } from './metadata.js';
9
+ import { fitsOption } from '../options.js';
10
+ import { createInstaller } from './installer.js';
11
+ import { createOptions, MissingOptions } from './options.js';
7
12
  import { createRouter } from './router.js';
8
13
  import { createState } from './state.js';
9
14
  import { toolCalls } from './tool-calls.js';
@@ -30,12 +35,14 @@ export async function readPlugin(claude) {
30
35
  const manifest = (await readJson(read, `${root}/.claude-plugin/plugin.json`));
31
36
  const version = typeof manifest?.version === 'string' ? manifest.version : undefined;
32
37
  const store = storeFolder({ HOME: await claude.env.home(), XDG_DATA_HOME: await claude.env.dataHome() });
38
+ const steps = readSteps(await readJson(read, `${root}/package.json`)) ?? {};
39
+ const declared = steps.permissions ?? [];
33
40
  if (name === cmodPluginName) {
34
41
  const installed = await cmodVersion(claude);
35
- return { name, root, version, store, keys: {}, isInstalled: version !== undefined && installed !== undefined && isAtLeast(installed, version), shouldRecord: false };
42
+ return { name, root, version, store, steps, granted: declared, isInstalled: version !== undefined && installed !== undefined && isAtLeast(installed, version), shouldRecord: false };
36
43
  }
37
- const steps = readSteps(await readJson(read, `${root}/package.json`)) ?? {};
38
- const plugin = { name, root, version, store, keys: steps.keys ?? {} };
44
+ const approved = parseConsent(await readJson(read, consentPath(store)), consentPath(store))[name] ?? [];
45
+ const plugin = { name, root, version, store, steps, granted: declared.filter((item) => approved.includes(item)) };
39
46
  const record = await readRecord(read, store, name);
40
47
  if (Object.keys(steps).length === 0)
41
48
  return { ...plugin, isInstalled: true, shouldRecord: record?.version !== version };
@@ -79,15 +86,18 @@ async function cmodVersion(claude) {
79
86
  throw new Error(`cmod --version ${formatExit(result.exitCode, lastLineOf(result.stderr) ?? '')}`);
80
87
  return result.stdout.trim().split(/\s+/).at(-1);
81
88
  }
82
- export function createLifecycle(definition, checksPermissions = () => true) {
89
+ export function createLifecycle(definition, checksPermissions = () => true, options = {}, asksPerson = true) {
83
90
  let router = createRouter();
84
91
  let phase = 'starting';
85
92
  let runtime;
86
93
  let plugin;
94
+ const granted = new Set();
87
95
  let line;
88
96
  let activation;
89
97
  let mod;
90
98
  let failure;
99
+ let pluginOptions = options;
100
+ const installer = createInstaller(definition.name, () => claude());
91
101
  let shouldRecord = false;
92
102
  let recording;
93
103
  let missedSessionStart;
@@ -95,7 +105,7 @@ export function createLifecycle(definition, checksPermissions = () => true) {
95
105
  const startSettled = new Promise((resolve) => (settleStart = resolve));
96
106
  const claude = () => {
97
107
  if (runtime === undefined)
98
- throw new Error(`${definition.name}: the lifecycle has not started. registerMod(addHook, mod) starts it at session.start.`);
108
+ throw new Error(`${definition.name}: the lifecycle has not started. registerMod(addHook, mod, options) starts it at session.start.`);
99
109
  return runtime.claude;
100
110
  };
101
111
  const showLine = () => {
@@ -132,20 +142,71 @@ export function createLifecycle(definition, checksPermissions = () => true) {
132
142
  recording = undefined;
133
143
  });
134
144
  };
145
+ const saveOption = async (key, text) => {
146
+ const option = definition.options?.[key];
147
+ if (option === undefined || plugin === undefined)
148
+ return `${definition.name} has no option ${key}.`;
149
+ const value = option.kind === 'number' ? Number(text) : option.kind === 'toggle' ? text.trim() === 'true' : option.kind === 'list' ? text.split(',').map((item) => item.trim()).filter((item) => item !== '') : text;
150
+ const misfit = option.kind === 'toggle' && !['true', 'false'].includes(text.trim()) ? 'takes true or false' : fitsOption(option, value);
151
+ if (misfit !== undefined)
152
+ return `${option.title} ${misfit}.`;
153
+ const saved = await claude().config.set({ key: `${plugin.name}.${key}`, value });
154
+ return saved.deny;
155
+ };
156
+ const askOptions = async (missing) => {
157
+ if (!asksPerson || plugin === undefined || (await claude().session.surfaces()).length === 0)
158
+ return false;
159
+ const declared = definition.options ?? {};
160
+ const asked = missing.keys.flatMap((key) => {
161
+ const option = declared[key];
162
+ return option === undefined ? [] : [[key, option]];
163
+ });
164
+ if (!(await installer.options(asked, saveOption)))
165
+ return false;
166
+ const prefix = `${plugin.name}.`;
167
+ const rows = await claude().config.list();
168
+ pluginOptions = { ...pluginOptions, ...Object.fromEntries(rows.filter((row) => row.key.startsWith(prefix)).map((row) => [row.key.slice(prefix.length), row.value])) };
169
+ return true;
170
+ };
171
+ const pendingSteps = async (active) => {
172
+ const pending = [];
173
+ for (const step of definition.installer ?? [])
174
+ if (!(await Promise.resolve(step.isDone(active)).catch(() => false)))
175
+ pending.push(step.title);
176
+ return pending;
177
+ };
178
+ const runSteps = async (active) => {
179
+ const steps = asksPerson ? (definition.installer ?? []) : [];
180
+ for (const [index, step] of steps.entries()) {
181
+ if (await Promise.resolve(step.isDone(active)).catch(() => false))
182
+ continue;
183
+ const isAnswered = (await claude().session.surfaces()).length > 0 && (await installer.step(active, step, index + 1, steps.length));
184
+ if (!isAnswered) {
185
+ claude().ui.log(`${definition.name} needs ${steps.length - index === 1 ? 'one more step' : `${steps.length - index} more steps`}, starting with ${step.title}. Run /mods ${definition.name} to finish.`);
186
+ break;
187
+ }
188
+ }
189
+ await installer.close();
190
+ };
135
191
  const activate = async () => {
136
192
  if (runtime === undefined || plugin === undefined)
137
193
  return;
138
194
  const activeRouter = createRouter();
139
- const active = await createMod(definition, { ...runtime, router: activeRouter, dataFolder: dataFolder(plugin.store, plugin.name), keys: plugin.keys, checksPermissions }).catch((error) => {
140
- fail(messageOf(error), 'Fix it, then run /reload-plugins.');
195
+ const active = await createMod(definition, { ...runtime, router: activeRouter, dataFolder: dataFolder(plugin.store, plugin.name), steps: plugin.steps, granted, refreshGrants, checksPermissions, options: pluginOptions }).catch(async (error) => {
196
+ if (error instanceof MissingOptions && (await askOptions(error)))
197
+ return undefined;
198
+ fail(messageOf(error), error instanceof MissingOptions ? `Set ${error.keys.length === 1 ? 'it' : 'them'} in /config.` : 'Fix it, then run /reload-plugins.');
141
199
  throw error;
142
200
  });
201
+ if (active === undefined)
202
+ return activate();
143
203
  const { added } = active;
144
204
  mod = active.mod;
145
205
  router = activeRouter;
146
206
  phase = 'active';
147
207
  runtime.claude.ui.invalidate('ui.render');
148
208
  endLine();
209
+ void runSteps(active.mod).catch((error) => claude().ui.log(`${definition.name}: its installer stopped: ${messageOf(error)}`));
149
210
  const missed = missedSessionStart;
150
211
  missedSessionStart = undefined;
151
212
  const lateAnswer = missed === undefined ? {} : await activeRouter.dispatch('classic.SessionStart', missed, async () => ({})).catch((error) => {
@@ -163,6 +224,12 @@ export function createLifecycle(definition, checksPermissions = () => true) {
163
224
  if (added.length > 0)
164
225
  runtime.claude.ui.log(`${definition.name} added ${listed(added)}.`);
165
226
  };
227
+ const updatesToShow = async () => {
228
+ const [home, configHome] = await Promise.all([claude().env.home(), claude().env.configHome()]);
229
+ const configRoot = configHome ?? `${home ?? ''}/.claude`;
230
+ const known = await claude().fs.read(`${configRoot}/plugins/known_marketplaces.json`).then((text) => JSON.parse(text), () => undefined);
231
+ return updatesToTurnOn(marketplaceOf(claude().plugin.root, claude().plugin.name), await claude().settings.read({ source: 'user' }), known);
232
+ };
166
233
  const askConsent = async (event) => {
167
234
  const name = definition.name;
168
235
  if ((await claude().session.surfaces()).length === 0) {
@@ -171,14 +238,9 @@ export function createLifecycle(definition, checksPermissions = () => true) {
171
238
  return;
172
239
  }
173
240
  showLine().wait('Waiting for your answer');
174
- const actions = [
175
- ...(event.install === '' ? [] : [`runs ${event.install} to install`]),
176
- ...(event.uninstall === '' ? [] : [`runs ${event.uninstall} when you remove it`]),
177
- ...(event.keys === '' ? [] : [`binds ${event.keys}`]),
178
- ];
179
- const answer = await claude().ui.ask(`${name} ${listed(actions)}. Install it now?`, { options: ['Install', 'Not now'], header: 'Install' });
180
- if (answer === 'Install')
241
+ if (await installer.consent({ ...event, updates: await updatesToShow().catch(() => undefined) }))
181
242
  return install(event.sha256);
243
+ await installer.close();
182
244
  phase = 'declined';
183
245
  endLine();
184
246
  claude().ui.log(`${name} is not installed. Run cmod install ${name} to install it.`);
@@ -203,14 +265,17 @@ export function createLifecycle(definition, checksPermissions = () => true) {
203
265
  if (event.kind === 'done' || event.kind === 'failed' || event.kind === 'needs-consent')
204
266
  outcome = event;
205
267
  });
206
- if (outcome?.kind === 'done')
268
+ if (outcome?.kind === 'done') {
269
+ for (const item of plugin.steps.permissions ?? [])
270
+ granted.add(item);
207
271
  return finish();
272
+ }
208
273
  if (outcome?.kind === 'needs-consent')
209
274
  return askConsent(outcome);
210
275
  const reason = outcome?.kind === 'failed' ? outcome.message : `cmod setup ${formatExit(code, lastError)}`;
211
276
  fail(reason, `Fix the cause, then run: cmod install ${definition.name}`);
212
277
  };
213
- const fits = (found) => found !== undefined && isAtLeast(found, oldestCmodFor(plugin?.keys ?? {}));
278
+ const fits = (found) => found !== undefined && isAtLeast(found, oldestCmodFor(plugin?.steps ?? {}));
214
279
  const waitForcmod = (found) => {
215
280
  phase = 'waiting';
216
281
  showLine().wait('Waiting for cmod');
@@ -237,7 +302,7 @@ export function createLifecycle(definition, checksPermissions = () => true) {
237
302
  });
238
303
  const longWait = claude().clock.after(cmodWaitMs, () => showLine().wait(latest === undefined
239
304
  ? "Still waiting for cmod to download cmod. See cmod's own line."
240
- : `PATH finds cmod ${latest}, and ${definition.name} needs cmod ${oldestCmodFor(plugin?.keys ?? {})} or later. Run npm i -g @cmodjs/cli, or put ~/.local/bin ahead of the old cmod on PATH.`));
305
+ : `PATH finds cmod ${latest}, and ${definition.name} needs cmod ${oldestCmodFor(plugin?.steps ?? {})} or later. Run npm i -g @cmodjs/cli, or put ~/.local/bin ahead of the old cmod on PATH.`));
241
306
  };
242
307
  const bootstrap = async (root) => {
243
308
  const progress = showLine();
@@ -256,6 +321,47 @@ export function createLifecycle(definition, checksPermissions = () => true) {
256
321
  if (phase !== 'failed')
257
322
  fail(messageOf(error), `Run cmod install ${definition.name} in a terminal to see the whole log.`);
258
323
  };
324
+ let consentMs = 0;
325
+ const consentStamp = async (path) => {
326
+ const stat = await claude().fs.stat(path).catch(() => undefined);
327
+ return stat?.kind === 'file' ? stat.mtimeMs : 0;
328
+ };
329
+ const refreshGrants = async () => {
330
+ if (plugin === undefined || plugin.name === cmodPluginName)
331
+ return;
332
+ const path = consentPath(plugin.store);
333
+ const modifiedMs = await consentStamp(path);
334
+ if (modifiedMs === consentMs)
335
+ return;
336
+ consentMs = modifiedMs;
337
+ const approved = modifiedMs === 0 ? [] : (parseConsent(JSON.parse(await claude().fs.read(path)), path)[plugin.name] ?? []);
338
+ granted.clear();
339
+ for (const item of plugin.steps.permissions ?? []) {
340
+ if (approved.includes(item))
341
+ granted.add(item);
342
+ }
343
+ };
344
+ const droppedParts = new Set();
345
+ const answerCheck = {
346
+ get plugin() {
347
+ return plugin?.name ?? definition.name;
348
+ },
349
+ isGranted: (item) => isCovered(granted, item, undefined),
350
+ dropped(item, part) {
351
+ if (droppedParts.has(item))
352
+ return;
353
+ droppedParts.add(item);
354
+ const declared = plugin?.steps.permissions ?? [];
355
+ const fix = declared.includes(item) ? `Turn it on in /mods ${definition.name}.` : `It needs "permissions": { "${item}": true } in package.json "cmod".`;
356
+ claude().ui.log(`${definition.name} answered ${part} without your grant to "${permissionWords(item)}", so cmod dropped it. ${fix}`);
357
+ },
358
+ };
359
+ const checkedRoute = async (event, e, next) => {
360
+ let below;
361
+ const passOn = (passed) => (below ??= next(passedDown(event, e, passed, answerCheck)));
362
+ const answer = await router.dispatch(event, e, passOn);
363
+ return (await checkedAnswer(event, e, answer, () => passOn(e), answerCheck));
364
+ };
259
365
  const whenActive = async () => {
260
366
  if (phase === 'ready')
261
367
  activation ??= activate().catch(report);
@@ -291,6 +397,9 @@ export function createLifecycle(definition, checksPermissions = () => true) {
291
397
  if (started === undefined)
292
398
  return;
293
399
  plugin = started;
400
+ for (const item of started.granted)
401
+ granted.add(item);
402
+ consentMs = await consentStamp(consentPath(started.store));
294
403
  if (started.isInstalled) {
295
404
  shouldRecord = started.shouldRecord;
296
405
  if (shouldRecord)
@@ -306,6 +415,10 @@ export function createLifecycle(definition, checksPermissions = () => true) {
306
415
  }
307
416
  },
308
417
  async route(event, e, next) {
418
+ if (event === 'ui.render' && installer.draws(e))
419
+ return installer.draw(e);
420
+ if (event === 'ui.close')
421
+ installer.closed(e.id);
309
422
  if (event === 'classic.SessionStart')
310
423
  missedSessionStart = undefined;
311
424
  if (event === 'classic.SessionStart' && runtime === undefined) {
@@ -321,6 +434,17 @@ export function createLifecycle(definition, checksPermissions = () => true) {
321
434
  const isStopped = phase === 'declined' || phase === 'failed';
322
435
  return { deny: isStopped ? notInstalled(definition.name) : `${definition.name} is installing. Try again when it's ready.` };
323
436
  }
437
+ const call = e;
438
+ if (event === 'cmod.call' && call.to === definition.name && mod !== undefined) {
439
+ if (call.method === pendingStepsMethod)
440
+ return { value: await pendingSteps(mod) };
441
+ if (call.method === finishStepsMethod)
442
+ return (await runSteps(mod), { value: null });
443
+ }
444
+ if (event === 'classic.UserPromptSubmit' || event === 'classic.SessionStart')
445
+ await refreshGrants().catch((error) => claude().ui.log(`${definition.name} keeps its grants from before: ${messageOf(error)}`, { to: 'debug' }));
446
+ if (checksAnswers(event) && phase === 'active')
447
+ return checkedRoute(event, e, next);
324
448
  const answer = router.dispatch(event, e, next);
325
449
  const render = e;
326
450
  if (event !== 'ui.render' || render.component !== 'AbovePrompt' || runtime === undefined || !runtime.progress.isShown)
@@ -331,22 +455,63 @@ export function createLifecycle(definition, checksPermissions = () => true) {
331
455
  }
332
456
  async function createMod(definition, runtime) {
333
457
  const { router, progress } = runtime;
334
- const claude = checkingKeys(runtime.claude, definition.name, runtime.keys);
458
+ const claude = checkingKeys(runtime.claude, definition.name, runtime.steps.keys ?? {});
335
459
  const added = [];
336
460
  const hookEvents = [];
337
461
  const announce = (feature) => {
338
462
  added.push(feature);
339
463
  };
340
- const [session, root, startCwd] = await Promise.all([claude.session.id(), claude.session.root(), claude.session.cwd()]);
464
+ const [session, root, startCwd, home, configHome] = await Promise.all([claude.session.id(), claude.session.root(), claude.session.cwd(), claude.env.home(), claude.env.configHome()]);
465
+ const { granted } = runtime;
466
+ const configRoot = configHome ?? (home === undefined ? undefined : `${home}/.claude`);
467
+ const grants = {
468
+ name: definition.name,
469
+ declared: runtime.steps.permissions ?? [],
470
+ granted: () => granted,
471
+ refresh: runtime.refreshGrants,
472
+ home,
473
+ configRoot,
474
+ projectRoot: () => modState.root,
475
+ freeFolders: () => [modState.root, runtime.dataFolder],
476
+ };
477
+ const checked = checkingGrants(claude, grants);
478
+ const files = createModFiles({
479
+ name: definition.name,
480
+ claude,
481
+ places: { home, configRoot, projectRoot: () => modState.root },
482
+ checkWrite: (path) => checkWrite(grants, `metadata.update(${path})`, path),
483
+ });
484
+ const pages = new Map();
485
+ const settingsPages = {
486
+ page(page) {
487
+ if (pages.has(page.id))
488
+ throw new Error(`${definition.name}: the settings page "${page.id}" is already added. Give each page its own id.`);
489
+ pages.set(page.id, { title: page.title, handle: area.ui.pane(page) });
490
+ },
491
+ async open(pageId) {
492
+ if (pageId === undefined) {
493
+ await claude.cmod.call({ to: cmodPluginName, method: 'openSettings', input: { mod: definition.name } });
494
+ return;
495
+ }
496
+ const page = pages.get(pageId);
497
+ if (page === undefined)
498
+ throw new Error(`${definition.name} has no settings page "${pageId}". It has: ${[...pages.keys()].join(', ') || 'none'}.`);
499
+ await page.handle.open({ focus: true });
500
+ },
501
+ };
341
502
  let cwd = startCwd;
342
503
  let loadedCwd = startCwd;
343
504
  let staleCwd;
344
505
  const area = createUi({ name: definition.name, claude, router, progress, announce, mod: () => mod });
345
506
  const modState = createState({ name: definition.name, initial: definition.state ?? {}, session, root, claude, changed: area.changed });
507
+ const modOptions = createOptions({ name: definition.name, declared: definition.options ?? {}, fromClaude: runtime.options, claude, changed: modState.changed });
346
508
  const held = runtime.checksPermissions() ? new Map() : undefined;
347
509
  const mod = {
348
510
  name: definition.name,
349
511
  state: modState.state,
512
+ get options() {
513
+ return modOptions.values;
514
+ },
350
515
  dataFolder: runtime.dataFolder,
351
516
  on(event, hook, options) {
352
517
  const bounded = options?.timeoutMs === undefined ? hook : timedHook(definition.name, event, hook, checkedMs(definition.name, `the ${event} hook's timeoutMs`, options.timeoutMs), claude);
@@ -357,7 +522,7 @@ async function createMod(definition, runtime) {
357
522
  else
358
523
  router.add(`classic.${event}`, classicHook(definition.name, event, bounded, claude, agents));
359
524
  },
360
- use: (job) => job({ mod, claude, on: (event, hook) => router.add(event, hook), announce, reserveName, toolCalls: agents }),
525
+ use: (job) => job({ mod, announce, reserveName, toolCalls: agents }),
361
526
  every(ms, hook) {
362
527
  let isRunning = false;
363
528
  return claude.clock.every(checkedMs(definition.name, 'mod.every', ms), () => {
@@ -374,33 +539,42 @@ async function createMod(definition, runtime) {
374
539
  },
375
540
  ui: area.ui,
376
541
  process: {
377
- run: (argv, init) => claude.process.run(argv, init),
378
- spawn: (argv, init) => claude.process.spawn({ ...init, argv }),
542
+ run: (argv, init) => checked.process.run(argv, init),
543
+ spawn: (argv, init) => checked.process.spawn({ ...init, argv }),
379
544
  },
380
545
  fs: {
381
- read: (path) => claude.fs.read(path),
382
- write: (path, text) => claude.fs.write(path, text),
383
- list: (path) => claude.fs.list(path),
384
- exists: (path) => claude.fs.exists(path),
385
- stat: (path, options) => claude.fs.stat(path, options),
546
+ read: (path) => checked.fs.read(path),
547
+ write: (path, text) => checked.fs.write(path, text),
548
+ list: (path) => checked.fs.list(path),
549
+ exists: (path) => checked.fs.exists(path),
550
+ stat: (path, options) => checked.fs.stat(path, options),
551
+ find: (glob) => files.find(glob),
552
+ },
553
+ metadata: {
554
+ read: (path) => files.read(path),
555
+ update: (path, change) => files.update(path, change),
386
556
  },
387
- http: { fetch: (url, init) => claude.http.fetch(url, init) },
388
- settings: { read: (args) => claude.settings.read(args) },
557
+ http: { fetch: (url, init) => checked.http.fetch(url, init) },
558
+ settings: settingsPages,
389
559
  session: {
390
- messages: claude.session.messages,
560
+ messages: ((args) => (args === undefined ? checked.session.messages() : checked.session.messages({ agentId: args.agentId }))),
391
561
  async append(text) {
392
- const added = await claude.session.append({ message: { type: 'user', content: [{ type: 'text', text }] } });
562
+ const added = await checked.session.append({ message: { type: 'user', content: [{ type: 'text', text }] } });
393
563
  if (added.deny !== undefined)
394
564
  throw new Error(`${definition.name}: Claude Code refused the note: ${added.deny}`);
395
565
  },
396
566
  async submit(text) {
397
- const submitted = await claude.prompt.submit({ text });
567
+ const submitted = await checked.prompt.submit({ text });
398
568
  if ('drop' in submitted && typeof submitted.drop === 'string')
399
569
  throw new Error(`${definition.name}: Claude Code dropped the prompt: ${submitted.drop}`);
400
570
  },
401
571
  },
402
- agent: { spawn: (args) => claude.agent.spawn(args) },
403
- model: { complete: (request, options) => claude.model.complete(request, options) },
572
+ agent: { spawn: (args) => checked.agent.spawn(args) },
573
+ model: { complete: (completion, options) => checked.model.complete(completion, options) },
574
+ permissions: {
575
+ has: (name, value) => isCovered(granted, itemOf(name, value), home),
576
+ },
577
+ claude: { ...checked, on: (event, hook) => router.add(event, hook) },
404
578
  get projectRoot() {
405
579
  return modState.root;
406
580
  },
@@ -427,6 +601,8 @@ async function createMod(definition, runtime) {
427
601
  cwd = loadedCwd;
428
602
  throw error;
429
603
  });
604
+ if (hasMovedRoot)
605
+ await modOptions.load(nextRoot);
430
606
  loadedCwd = nextCwd;
431
607
  if (!hasMovedRoot)
432
608
  area.changed();
@@ -465,7 +641,17 @@ async function createMod(definition, runtime) {
465
641
  });
466
642
  router.add('skill.prompt', userSkillHook(claude));
467
643
  const api = Object.fromEntries(Object.entries(definition.api ?? {}).map(([method, run]) => [method, (input) => run(input, mod)]));
468
- router.add('cmod.call', (e, next) => (e.to === definition.name ? answerCall(definition.name, api, e) : next(e)));
644
+ router.add('cmod.call', async (e, next) => {
645
+ if (e.to !== definition.name)
646
+ return next(e);
647
+ if (e.method === settingsPagesMethod)
648
+ return { value: [...pages].map(([id, { title }]) => ({ id, title })) };
649
+ if (e.method === openPageMethod) {
650
+ await settingsPages.open(String(e.input));
651
+ return { value: null };
652
+ }
653
+ return answerCall(definition.name, api, e);
654
+ });
469
655
  const agents = toolCalls(claude, router);
470
656
  const names = new Set();
471
657
  const reserveName = (kind, name, taken) => {
@@ -475,6 +661,10 @@ async function createMod(definition, runtime) {
475
661
  names.add(key);
476
662
  };
477
663
  await failsAs('its state did not load', () => modState.load());
664
+ await failsAs('its options did not load', () => modOptions.load(root));
665
+ const missing = modOptions.missing;
666
+ if (missing.length > 0)
667
+ throw new MissingOptions(missing, missing.map((key) => definition.options?.[key]?.title ?? key));
478
668
  await failsAs('its setup function threw', () => definition.setup(mod));
479
669
  const permissionHooks = permissionEvents.filter((event) => router.has(event));
480
670
  if (permissionHooks.length > 0 && !runtime.checksPermissions()) {
@@ -0,0 +1,20 @@
1
+ import type { FileMatch, Metadata } from '../mod.js';
2
+ import type { Claude } from './claude.js';
3
+ export type Places = {
4
+ readonly home: string | undefined;
5
+ readonly configRoot: string | undefined;
6
+ readonly projectRoot: () => string;
7
+ };
8
+ export type ModFiles = {
9
+ find(glob: string | readonly string[]): Promise<readonly FileMatch[]>;
10
+ read(path: string): Promise<Metadata>;
11
+ update(path: string, change: (metadata: Record<string, string>) => void): Promise<void>;
12
+ };
13
+ export declare function resolvedPath(path: string, places: Places): string;
14
+ export declare function createModFiles({ name, claude, places, checkWrite }: {
15
+ readonly name: string;
16
+ readonly claude: Claude;
17
+ readonly places: Places;
18
+ readonly checkWrite: (path: string) => Promise<void>;
19
+ }): ModFiles;
20
+ //# sourceMappingURL=metadata.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"metadata.d.ts","sourceRoot":"","sources":["../src/runtime/metadata.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAA;AAIpD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AAEzC,MAAM,MAAM,MAAM,GAAG;IACnB,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,CAAA;IACjC,QAAQ,CAAC,UAAU,EAAE,MAAM,GAAG,SAAS,CAAA;IACvC,QAAQ,CAAC,WAAW,EAAE,MAAM,MAAM,CAAA;CACnC,CAAA;AAED,MAAM,MAAM,QAAQ,GAAG;IACrB,IAAI,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,MAAM,EAAE,GAAG,OAAO,CAAC,SAAS,SAAS,EAAE,CAAC,CAAA;IACrE,IAAI,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAA;IACrC,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;CACxF,CAAA;AAUD,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAQjE;AAED,wBAAgB,cAAc,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,UAAU,EAAE,EAAE;IAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,UAAU,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;CAAE,GAAG,QAAQ,CAoHhN"}