@cmodjs/core 1.0.0 → 1.5.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 (122) hide show
  1. package/docs/commands.md +22 -11
  2. package/docs/dependencies.md +6 -4
  3. package/docs/hooks.md +26 -3
  4. package/docs/index.md +1 -1
  5. package/docs/install-steps.md +5 -3
  6. package/docs/mod.md +29 -3
  7. package/docs/permissions.md +4 -4
  8. package/docs/session.md +126 -63
  9. package/docs/testing.md +7 -5
  10. package/docs/ui.md +9 -1
  11. package/jobs/program.d.ts.map +1 -1
  12. package/jobs/program.js +7 -2
  13. package/mod.d.ts +30 -6
  14. package/mod.d.ts.map +1 -1
  15. package/mod.js +34 -1
  16. package/package.json +1 -1
  17. package/records.d.ts +4 -1
  18. package/records.d.ts.map +1 -1
  19. package/records.js +6 -1
  20. package/register.d.ts.map +1 -1
  21. package/register.js +17 -0
  22. package/runtime/announce.d.ts.map +1 -1
  23. package/runtime/announce.js +2 -0
  24. package/runtime/before.d.ts +32 -0
  25. package/runtime/before.d.ts.map +1 -0
  26. package/runtime/before.js +214 -0
  27. package/runtime/claude.d.ts +2 -1
  28. package/runtime/claude.d.ts.map +1 -1
  29. package/runtime/conversations.d.ts +1 -0
  30. package/runtime/conversations.d.ts.map +1 -1
  31. package/runtime/conversations.js +333 -61
  32. package/runtime/dependencies.js +1 -1
  33. package/runtime/engine.d.ts +2 -6
  34. package/runtime/engine.d.ts.map +1 -1
  35. package/runtime/grants.d.ts.map +1 -1
  36. package/runtime/grants.js +8 -8
  37. package/runtime/hooks.d.ts +1 -1
  38. package/runtime/hooks.d.ts.map +1 -1
  39. package/runtime/hooks.js +19 -4
  40. package/runtime/installer.d.ts +1 -1
  41. package/runtime/installer.d.ts.map +1 -1
  42. package/runtime/installer.js +6 -5
  43. package/runtime/lifecycle.d.ts.map +1 -1
  44. package/runtime/lifecycle.js +130 -59
  45. package/runtime/metadata.d.ts.map +1 -1
  46. package/runtime/metadata.js +5 -4
  47. package/runtime/sessions.d.ts +26 -4
  48. package/runtime/sessions.d.ts.map +1 -1
  49. package/runtime/sessions.js +315 -117
  50. package/runtime/toaster.d.ts +5 -12
  51. package/runtime/toaster.d.ts.map +1 -1
  52. package/runtime/toaster.js +158 -25
  53. package/runtime/ui.d.ts.map +1 -1
  54. package/runtime/ui.js +26 -6
  55. package/session/context/context.d.ts +3 -1
  56. package/session/context/context.d.ts.map +1 -1
  57. package/session/context/turn/message/message.d.ts +1 -0
  58. package/session/context/turn/message/message.d.ts.map +1 -1
  59. package/session/context/turn/message/message.js +42 -1
  60. package/session/context/turn/turn.d.ts +2 -1
  61. package/session/context/turn/turn.d.ts.map +1 -1
  62. package/session/conversation.d.ts +20 -11
  63. package/session/conversation.d.ts.map +1 -1
  64. package/session/conversation.js +50 -27
  65. package/session/goal/goal.d.ts +3 -2
  66. package/session/goal/goal.d.ts.map +1 -1
  67. package/session/goal/task.d.ts +4 -4
  68. package/session/goal/task.d.ts.map +1 -1
  69. package/session/session.d.ts +80 -21
  70. package/session/session.d.ts.map +1 -1
  71. package/session/session.js +1 -0
  72. package/src/jobs/program.ts +7 -2
  73. package/src/mod.ts +48 -5
  74. package/src/records.ts +6 -1
  75. package/src/register.ts +17 -0
  76. package/src/runtime/announce.ts +2 -0
  77. package/src/runtime/before.ts +200 -0
  78. package/src/runtime/claude.ts +3 -0
  79. package/src/runtime/conversations.ts +337 -60
  80. package/src/runtime/dependencies.ts +1 -1
  81. package/src/runtime/engine.ts +2 -1
  82. package/src/runtime/grants.ts +8 -10
  83. package/src/runtime/hooks.ts +20 -4
  84. package/src/runtime/installer.ts +6 -5
  85. package/src/runtime/lifecycle.ts +136 -56
  86. package/src/runtime/metadata.ts +5 -4
  87. package/src/runtime/sessions.ts +316 -111
  88. package/src/runtime/toaster.ts +183 -45
  89. package/src/runtime/ui.ts +23 -4
  90. package/src/session/context/context.ts +3 -1
  91. package/src/session/context/turn/message/message.ts +39 -1
  92. package/src/session/context/turn/turn.ts +2 -1
  93. package/src/session/conversation.ts +56 -31
  94. package/src/session/goal/goal.ts +3 -2
  95. package/src/session/goal/task.ts +4 -4
  96. package/src/session/session.ts +62 -15
  97. package/src/testing/fake-claude.ts +4 -2
  98. package/src/testing/fake-sessions.ts +94 -20
  99. package/src/testing.ts +27 -3
  100. package/src/utils/call-effects.ts +2 -2
  101. package/src/utils/disposable.ts +3 -0
  102. package/src/utils/matcher.ts +1 -1
  103. package/src/utils/paths.ts +6 -3
  104. package/testing/fake-claude.d.ts +1 -0
  105. package/testing/fake-claude.d.ts.map +1 -1
  106. package/testing/fake-claude.js +3 -1
  107. package/testing/fake-sessions.d.ts +8 -1
  108. package/testing/fake-sessions.d.ts.map +1 -1
  109. package/testing/fake-sessions.js +97 -18
  110. package/testing.d.ts +1 -0
  111. package/testing.d.ts.map +1 -1
  112. package/testing.js +23 -2
  113. package/utils/call-effects.d.ts +1 -0
  114. package/utils/call-effects.d.ts.map +1 -1
  115. package/utils/call-effects.js +1 -1
  116. package/utils/disposable.d.ts +4 -0
  117. package/utils/disposable.d.ts.map +1 -0
  118. package/utils/disposable.js +1 -0
  119. package/utils/matcher.js +1 -1
  120. package/utils/paths.d.ts +2 -1
  121. package/utils/paths.d.ts.map +1 -1
  122. package/utils/paths.js +6 -3
package/runtime/grants.js CHANGED
@@ -1,7 +1,7 @@
1
- import { permissionWords } from '../records.js';
1
+ import { isObject, permissionWords } from '../records.js';
2
2
  import { relativePath } from '../utils/paths.js';
3
3
  import { cmodPluginName } from './dependencies.js';
4
- import { conversationMethods } from './sessions.js';
4
+ import { actPermission, conversationMethods, isAct, toolPermission } from './sessions.js';
5
5
  export class PermissionRefused extends Error {
6
6
  }
7
7
  const addressesOfPrograms = new WeakMap();
@@ -11,8 +11,6 @@ export function programAddresses(mod) {
11
11
  return known;
12
12
  }
13
13
  const projectSettingsFiles = ['.claude/settings.json', '.claude/settings.local.json', '.mcp.json'];
14
- const startsTurns = ['CronCreate'];
15
- const ownSchedule = ['CronDelete', 'CronList'];
16
14
  const conversationCalls = {
17
15
  [conversationMethods.sessions]: 'sessions.list',
18
16
  [conversationMethods.search]: 'sessions.search',
@@ -23,7 +21,8 @@ const conversationCalls = {
23
21
  [conversationMethods.goal]: 'goal',
24
22
  [conversationMethods.updateGoal]: 'goal.update',
25
23
  [conversationMethods.subscribe]: 'on',
26
- [conversationMethods.send]: 'send',
24
+ [conversationMethods.watchBefore]: 'before',
25
+ [conversationMethods.act]: 'act',
27
26
  };
28
27
  export async function checkGrant(grants, call, item) {
29
28
  if (item === undefined)
@@ -95,8 +94,10 @@ export function checkingGrants(claude, grants) {
95
94
  cmod: {
96
95
  async call(args) {
97
96
  const named = args.to === cmodPluginName ? conversationCalls[args.method] : undefined;
97
+ const given = args.method === conversationMethods.act && isObject(args.input) ? String(args.input['action']) : undefined;
98
+ const action = given !== undefined && isAct(given) ? given : undefined;
98
99
  if (named !== undefined)
99
- await gate(named, args.method === conversationMethods.send ? 'prompt' : 'conversation');
100
+ await gate(action ?? named, action === undefined ? 'conversation' : actPermission(action, isObject(args.input) ? args.input['input'] : undefined));
100
101
  return claude.cmod.call(args);
101
102
  },
102
103
  },
@@ -116,8 +117,7 @@ export function checkingGrants(claude, grants) {
116
117
  tool: {
117
118
  ...claude.tool,
118
119
  call: async (input) => {
119
- const item = startsTurns.includes(input.tool) ? 'prompt' : ownSchedule.includes(input.tool) ? undefined : 'tools';
120
- await gate(`tool.call(${input.tool})`, item);
120
+ await gate(`tool.call(${input.tool})`, toolPermission(input.tool));
121
121
  return claude.tool.call(input);
122
122
  },
123
123
  },
@@ -3,7 +3,7 @@ import type { Claude } from './claude.js';
3
3
  import type { RoutedHook } from './router.js';
4
4
  import type { Failure } from './toaster.js';
5
5
  import { type ToolCalls } from './tool-calls.js';
6
- export type RoutedEvent = `classic.${Exclude<Hook, 'PreToolUse'>}` | 'tool.check' | 'tool.call' | 'prompt.submit' | 'prompt.context' | 'command.run' | 'session.measure' | 'skill.prompt' | 'ui.render' | 'ui.press' | 'ui.scroll' | 'ui.close' | 'cmod.call' | 'session.append' | 'session.compact' | 'session.receive' | 'agent.spawn';
6
+ export type RoutedEvent = `classic.${Exclude<Hook, 'PreToolUse'>}` | 'tool.check' | 'tool.call' | 'prompt.submit' | 'prompt.context' | 'prompt.attachment' | 'command.run' | 'session.measure' | 'skill.prompt' | 'ui.render' | 'ui.press' | 'ui.scroll' | 'ui.close' | 'cmod.call' | 'session.append' | 'session.compact' | 'session.receive' | 'agent.spawn';
7
7
  export declare const permissionEvents: readonly RoutedEvent[];
8
8
  export declare function classicHook<E extends Exclude<Hook, 'PreToolUse'>>(name: string, event: E, hook: HookListener<E>, claude: Claude, calls: ToolCalls, report: (failure: Failure) => void): RoutedHook<RoutedEvent>;
9
9
  type HeldDecision = {
@@ -1 +1 @@
1
- {"version":3,"file":"hooks.d.ts","sourceRoot":"","sources":["../src/runtime/hooks.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,IAAI,EAAyB,YAAY,EAAE,MAAM,WAAW,CAAA;AAE1E,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AACzC,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAA;AAC7C,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,cAAc,CAAA;AAC3C,OAAO,EAA6B,KAAK,SAAS,EAAE,MAAM,iBAAiB,CAAA;AAM3E,MAAM,MAAM,WAAW,GACnB,WAAW,OAAO,CAAC,IAAI,EAAE,YAAY,CAAC,EAAE,GACxC,YAAY,GACZ,WAAW,GACX,eAAe,GACf,gBAAgB,GAChB,aAAa,GACb,iBAAiB,GACjB,cAAc,GACd,WAAW,GACX,UAAU,GACV,WAAW,GACX,UAAU,GACV,WAAW,GACX,gBAAgB,GAChB,iBAAiB,GACjB,iBAAiB,GACjB,aAAa,CAAA;AAEjB,eAAO,MAAM,gBAAgB,EAAE,SAAS,WAAW,EAAgD,CAAA;AA6BnG,wBAAgB,WAAW,CAAC,CAAC,SAAS,OAAO,CAAC,IAAI,EAAE,YAAY,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,EAAE,IAAI,EAAE,YAAY,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,SAAS,EAAE,MAAM,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,IAAI,GAAG,UAAU,CAAC,WAAW,CAAC,CAuB/M;AAED,KAAK,YAAY,GAAG;IAAE,QAAQ,CAAC,QAAQ,EAAE,OAAO,GAAG,KAAK,CAAC;IAAC,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,CAAA;AAEpF,MAAM,MAAM,aAAa,GAAG,GAAG,CAAC,MAAM,EAAE,YAAY,CAAC,CAAA;AAErD,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,YAAY,CAAC,YAAY,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,SAAS,EAAE,IAAI,EAAE,aAAa,GAAG,SAAS,GAAG,UAAU,CAAC,WAAW,CAAC,CA6BzK;AAED,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,aAAa,GAAG,UAAU,CAAC,YAAY,CAAC,CAM9E;AAYD,wBAAgB,aAAa,CAAC,MAAM,EAAE,MAAM,GAAG,UAAU,CAAC,cAAc,CAAC,CAexE;AAgED,eAAO,MAAM,UAAU,YAAI,OAAO,EAAE,KAAK,EAAE,MAAM,CAAU,CAAA"}
1
+ {"version":3,"file":"hooks.d.ts","sourceRoot":"","sources":["../src/runtime/hooks.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,IAAI,EAAyB,YAAY,EAAE,MAAM,WAAW,CAAA;AAE1E,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AACzC,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAA;AAC7C,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,cAAc,CAAA;AAC3C,OAAO,EAA6B,KAAK,SAAS,EAAE,MAAM,iBAAiB,CAAA;AAM3E,MAAM,MAAM,WAAW,GACnB,WAAW,OAAO,CAAC,IAAI,EAAE,YAAY,CAAC,EAAE,GACxC,YAAY,GACZ,WAAW,GACX,eAAe,GACf,gBAAgB,GAChB,mBAAmB,GACnB,aAAa,GACb,iBAAiB,GACjB,cAAc,GACd,WAAW,GACX,UAAU,GACV,WAAW,GACX,UAAU,GACV,WAAW,GACX,gBAAgB,GAChB,iBAAiB,GACjB,iBAAiB,GACjB,aAAa,CAAA;AAEjB,eAAO,MAAM,gBAAgB,EAAE,SAAS,WAAW,EAAgD,CAAA;AA4CnG,wBAAgB,WAAW,CAAC,CAAC,SAAS,OAAO,CAAC,IAAI,EAAE,YAAY,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,EAAE,IAAI,EAAE,YAAY,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,SAAS,EAAE,MAAM,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,IAAI,GAAG,UAAU,CAAC,WAAW,CAAC,CAuB/M;AAED,KAAK,YAAY,GAAG;IAAE,QAAQ,CAAC,QAAQ,EAAE,OAAO,GAAG,KAAK,CAAC;IAAC,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,CAAA;AAEpF,MAAM,MAAM,aAAa,GAAG,GAAG,CAAC,MAAM,EAAE,YAAY,CAAC,CAAA;AAErD,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,YAAY,CAAC,YAAY,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,SAAS,EAAE,IAAI,EAAE,aAAa,GAAG,SAAS,GAAG,UAAU,CAAC,WAAW,CAAC,CA6BzK;AAED,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,aAAa,GAAG,UAAU,CAAC,YAAY,CAAC,CAM9E;AAYD,wBAAgB,aAAa,CAAC,MAAM,EAAE,MAAM,GAAG,UAAU,CAAC,cAAc,CAAC,CAexE;AAgED,eAAO,MAAM,UAAU,YAAI,OAAO,EAAE,KAAK,EAAE,MAAM,CAAU,CAAA"}
package/runtime/hooks.js CHANGED
@@ -7,24 +7,39 @@ import { messageOf } from '../utils/text.js';
7
7
  export const permissionEvents = ['tool.check', 'classic.PermissionRequest'];
8
8
  const flags = ['suppressOriginalPrompt', 'reloadSkills', 'retry'];
9
9
  const readFields = {
10
+ Setup: ['additionalContext'],
10
11
  SessionStart: ['additionalContext', 'initialUserMessage', 'sessionTitle', 'watchPaths', 'reloadSkills'],
11
12
  SessionEnd: [],
12
13
  UserPromptSubmit: ['additionalContext', 'sessionTitle', 'suppressOriginalPrompt'],
14
+ UserPromptExpansion: ['additionalContext', 'suppressOriginalPrompt'],
13
15
  InstructionsLoaded: [],
16
+ ConfigChange: [],
17
+ DirectoryAdded: [],
18
+ PreModelSwitch: ['permissionDecision', 'permissionDecisionReason'],
19
+ PostModelSwitch: ['additionalContext'],
14
20
  PreToolUse: ['additionalContext', 'permissionDecision', 'permissionDecisionReason', 'updatedInput'],
15
21
  PermissionRequest: ['decision'],
16
22
  PermissionDenied: ['retry'],
17
23
  PostToolUse: ['additionalContext', 'updatedToolOutput', 'updatedMCPToolOutput'],
18
24
  PostToolUseFailure: ['additionalContext'],
19
25
  PostToolBatch: ['additionalContext'],
26
+ MessageDisplay: ['displayContent'],
20
27
  SubagentStart: ['additionalContext'],
21
28
  SubagentStop: ['additionalContext'],
29
+ TaskCreated: [],
30
+ TaskCompleted: [],
31
+ TeammateIdle: [],
32
+ Elicitation: [],
33
+ ElicitationResult: [],
22
34
  Notification: [],
23
35
  PreCompact: [],
36
+ PostCompact: [],
24
37
  Stop: ['additionalContext'],
25
38
  StopFailure: [],
26
39
  CwdChanged: [],
27
40
  FileChanged: [],
41
+ WorktreeCreate: ['worktreePath'],
42
+ WorktreeRemove: [],
28
43
  };
29
44
  export function classicHook(name, event, hook, claude, calls, report) {
30
45
  const hookInputOf = async (e) => {
@@ -146,10 +161,10 @@ async function preToolUseInput(name, envelope, claude, agentOf) {
146
161
  }
147
162
  async function callFiles(name, claude, use, cwd) {
148
163
  try {
149
- const home = await claude.env.home();
164
+ const [home, configRoot] = await Promise.all([claude.env.home(), claude.env.configHome()]);
150
165
  if (home === undefined)
151
166
  throw new Error('HOME is not set, so ~ in a path has no meaning.');
152
- const { shell, reads, writes } = callEffects(use, { cwd, home, fs: claude.fs });
167
+ const { shell, reads, writes } = callEffects(use, { cwd, home, ...(configRoot === undefined ? {} : { configRoot }), fs: claude.fs });
153
168
  const known = (accesses) => accesses.map(({ path }) => path).filter((path) => shell === undefined || !dynamicPattern.test(path));
154
169
  const unchecked = (path) => (error) => {
155
170
  if (!isMissing(error))
@@ -192,11 +207,11 @@ function classicResult(event, answer) {
192
207
  throw unread(`hookSpecificOutput.${field}`);
193
208
  if (field === 'additionalContext')
194
209
  result['additionalContext'] = [value];
195
- else if (field === 'permissionDecision') {
210
+ else if (field === 'permissionDecision' && event === 'PreToolUse') {
196
211
  const reason = specific.permissionDecisionReason ?? '';
197
212
  result[value] = value === 'allow' ? true : reason;
198
213
  }
199
- else if (field !== 'permissionDecisionReason')
214
+ else if (field !== 'permissionDecisionReason' || event !== 'PreToolUse')
200
215
  result[field] = value;
201
216
  }
202
217
  return result;
@@ -3,7 +3,7 @@ import type { Step } from '../installer.js';
3
3
  import type { Mod } from '../mod.js';
4
4
  import type { Option } from '../options.js';
5
5
  import type { Claude } from './claude.js';
6
- export declare const installerPaneId = "cmod-installer";
6
+ export declare const installerPaneOf: (name: string) => string;
7
7
  export type Consent = {
8
8
  readonly install: string;
9
9
  readonly uninstall: string;
@@ -1 +1 @@
1
- {"version":3,"file":"installer.d.ts","sourceRoot":"","sources":["../src/runtime/installer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,EAAE,aAAa,EAAiB,MAAM,aAAa,CAAA;AAC7E,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,iBAAiB,CAAA;AAC3C,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,WAAW,CAAA;AACpC,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,eAAe,CAAA;AAI3C,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AAEzC,eAAO,MAAM,eAAe,mBAAmB,CAAA;AAE/C,MAAM,MAAM,OAAO,GAAG;IACpB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;IACxB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;IAC1B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,WAAW,EAAE,SAAS,MAAM,EAAE,CAAA;IACvC,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;CACtC,CAAA;AAOD,MAAM,MAAM,SAAS,GAAG;IACtB,KAAK,CAAC,CAAC,EAAE,MAAM,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC,GAAG,OAAO,CAAA;IAC5C,IAAI,CAAC,CAAC,EAAE,MAAM,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC,GAAG,aAAa,CAAA;IACjD,MAAM,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,OAAO,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAA;IAC3C,OAAO,CAAC,OAAO,EAAE,SAAS,CAAC,SAAS,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,EAAE,EAAE,IAAI,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,GAAG,OAAO,CAAC,OAAO,CAAC,CAAA;IAC7I,IAAI,CAAC,KAAK,SAAS,MAAM,EAAE,GAAG,EAAE,GAAG,CAAC,KAAK,CAAC,EAAE,IAAI,EAAE,IAAI,CAAC,KAAK,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAA;IAC9G,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAA;CACvB,CAAA;AAED,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,MAAM,GAAG,SAAS,CAuI7E"}
1
+ {"version":3,"file":"installer.d.ts","sourceRoot":"","sources":["../src/runtime/installer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,EAAE,aAAa,EAAiB,MAAM,aAAa,CAAA;AAC7E,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,iBAAiB,CAAA;AAC3C,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,WAAW,CAAA;AACpC,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,eAAe,CAAA;AAI3C,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AAEzC,eAAO,MAAM,eAAe,SAAU,MAAM,KAAG,MAA0E,CAAA;AAEzH,MAAM,MAAM,OAAO,GAAG;IACpB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;IACxB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;IAC1B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,WAAW,EAAE,SAAS,MAAM,EAAE,CAAA;IACvC,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;CACtC,CAAA;AAOD,MAAM,MAAM,SAAS,GAAG;IACtB,KAAK,CAAC,CAAC,EAAE,MAAM,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC,GAAG,OAAO,CAAA;IAC5C,IAAI,CAAC,CAAC,EAAE,MAAM,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC,GAAG,aAAa,CAAA;IACjD,MAAM,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,OAAO,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAA;IAC3C,OAAO,CAAC,OAAO,EAAE,SAAS,CAAC,SAAS,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,EAAE,EAAE,IAAI,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,GAAG,OAAO,CAAC,OAAO,CAAC,CAAA;IAC7I,IAAI,CAAC,KAAK,SAAS,MAAM,EAAE,GAAG,EAAE,GAAG,CAAC,KAAK,CAAC,EAAE,IAAI,EAAE,IAAI,CAAC,KAAK,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAA;IAC9G,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAA;CACvB,CAAA;AAED,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,MAAM,GAAG,SAAS,CAwI7E"}
@@ -1,8 +1,9 @@
1
1
  import { permissionWords } from '../records.js';
2
2
  import { Box, Button, drawWith, Input, Text } from '../ui/elements.js';
3
3
  import { messageOf } from '../utils/text.js';
4
- export const installerPaneId = 'cmod-installer';
4
+ export const installerPaneOf = (name) => `installer-${name.replace(/[^A-Za-z0-9_-]/g, '_')}`.slice(0, 64);
5
5
  export function createInstaller(name, claude) {
6
+ const paneId = installerPaneOf(name);
6
7
  let page;
7
8
  let isOpen = false;
8
9
  const show = async (build, cancelled) => {
@@ -18,7 +19,7 @@ export function createInstaller(name, claude) {
18
19
  });
19
20
  if (!isOpen) {
20
21
  isOpen = true;
21
- await claude().ui.open({ id: installerPaneId, title: `Install ${name}`, focus: true, holdToasts: true, closeOnEscape: true });
22
+ await claude().ui.open({ id: paneId, title: `Install ${name}`, focus: true, holdToasts: true, closeOnEscape: true });
22
23
  }
23
24
  claude().ui.invalidate('ui.render');
24
25
  return answer;
@@ -26,12 +27,12 @@ export function createInstaller(name, claude) {
26
27
  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
28
  const bullet = (text) => Text({ children: [Text({ color: 'claude', children: '◆ ' }), text] });
28
29
  return {
29
- draws: (e) => e.component === 'Pane' && e.requestId === installerPaneId,
30
+ draws: (e) => e.component === 'Pane' && e.requestId === paneId,
30
31
  draw(e) {
31
32
  return drawWith(claude().ui.resolve(e), () => page?.draw(e.props) ?? Text({ dimColor: true, children: `${name} is installing.` }));
32
33
  },
33
34
  closed(id) {
34
- if (id !== installerPaneId)
35
+ if (id !== paneId)
35
36
  return;
36
37
  isOpen = false;
37
38
  page?.cancel();
@@ -114,7 +115,7 @@ export function createInstaller(name, claude) {
114
115
  if (!isOpen)
115
116
  return;
116
117
  isOpen = false;
117
- await claude().ui.close({ id: installerPaneId });
118
+ await claude().ui.close({ id: paneId });
118
119
  },
119
120
  };
120
121
  }
@@ -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,EAAc,aAAa,EAAiB,MAAM,aAAa,CAAA;AAEtG,OAAO,EAAkI,KAAK,GAAG,EAAE,KAAK,aAAa,EAAkD,MAAM,WAAW,CAAA;AACxO,OAAO,KAAK,EAAE,OAAO,EAAgB,MAAM,eAAe,CAAA;AAC1D,OAAO,EAA2T,KAAK,KAAK,EAAE,MAAM,eAAe,CAAA;AAGnW,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AAOzC,OAAO,EAAsG,KAAK,WAAW,EAAE,MAAM,YAAY,CAAA;AAOjJ,OAAO,EAA6B,KAAK,UAAU,EAAE,MAAM,aAAa,CAAA;AAKxE,OAAO,EAA4C,KAAK,UAAU,EAAgB,MAAM,cAAc,CAAA;AAMtG,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,QAAQ,CAAC,MAAM,EAAE,SAAS,UAAU,EAAE,CAAA;IACtC,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;AAsCD,wBAAsB,UAAU,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAwBhE;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,CA6YlB"}
1
+ {"version":3,"file":"lifecycle.d.ts","sourceRoot":"","sources":["../src/runtime/lifecycle.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,WAAW,EAAE,MAAM,EAAc,aAAa,EAAwB,MAAM,aAAa,CAAA;AAE7G,OAAO,EAAkI,KAAK,GAAG,EAAE,KAAK,aAAa,EAAkD,MAAM,WAAW,CAAA;AACxO,OAAO,KAAK,EAAE,OAAO,EAAgB,MAAM,eAAe,CAAA;AAC1D,OAAO,EAA2T,KAAK,KAAK,EAAE,MAAM,eAAe,CAAA;AAGnW,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AAOzC,OAAO,EAAsG,KAAK,WAAW,EAAE,MAAM,YAAY,CAAA;AAOjJ,OAAO,EAA6B,KAAK,UAAU,EAAE,MAAM,aAAa,CAAA;AAMxE,OAAO,EAA+B,KAAK,UAAU,EAAgB,MAAM,cAAc,CAAA;AAMzF,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,QAAQ,CAAC,MAAM,EAAE,SAAS,UAAU,EAAE,CAAA;IACtC,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;AAwCD,wBAAsB,UAAU,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAwBhE;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,CAkclB"}
@@ -1,12 +1,12 @@
1
1
  import { hooks, PreToolUse } from '../mod.js';
2
2
  import { changelogPage, consentPath, dataFolder, finishStepsMethod, isAtLeast, marketplaceOf, oldestCmodFor, openPageMethod, parseConsent, pendingStepsMethod, parseEvent, permissionWords, readRecord, readSteps, recordPath, scriptsSha256, settingsPagesMethod, storeFolder, updatesToTurnOn } from '../records.js';
3
- import { ifMissing, relativePath } from '../utils/paths.js';
3
+ import { homePrefix, ifMissing, relativePath } from '../utils/paths.js';
4
4
  import { firstLine, formatExit, listed, messageOf } from '../utils/text.js';
5
5
  import { beforeDeadline } from './deadline.js';
6
6
  import { announcedOf, announcementMs, announcementOf } from './announce.js';
7
7
  import { bind } from './bound.js';
8
8
  import { serveConversations } from './conversations.js';
9
- import { answerCall, cmodPluginName, dependencyCalls, notInstalled, withBareRefusals } from './dependencies.js';
9
+ import { answerCall, cmodPluginName, dependencyCalls, withBareRefusals } from './dependencies.js';
10
10
  import { keepEngine } from './engine.js';
11
11
  import { classicHook, heldDecisionHook, permissionEvents, preToolUseHook, userSkillHook } from './hooks.js';
12
12
  import { checkedAnswer, checkingGrants, checkSettings, checksAnswers, checkWrite, isCovered, itemOf, passedDown, programAddresses } from './grants.js';
@@ -19,13 +19,14 @@ import { createRouter } from './router.js';
19
19
  import { conversationMethods, createSessionsClient } from './sessions.js';
20
20
  import { matches } from '../utils/matcher.js';
21
21
  import { createState } from './state.js';
22
- import { createToaster, showFailure } from './toaster.js';
22
+ import { createToaster } from './toaster.js';
23
23
  import { toolCalls } from './tool-calls.js';
24
24
  import { createUi } from './ui.js';
25
25
  import { Progress } from '../ui/components.js';
26
26
  import { Box, Text } from '../ui/elements.js';
27
27
  const announcedKey = 'cmod:announced';
28
28
  const installId = 'cmod:install';
29
+ const installRetrySeconds = [2, 4, 8, 16, 32, 64, 128];
29
30
  const cmodCheckMs = 1000;
30
31
  const cmodWaitMs = 60_000;
31
32
  const startHoldMs = 10_000 - 1_000;
@@ -77,7 +78,7 @@ async function readJson(read, path) {
77
78
  return JSON.parse(text);
78
79
  }
79
80
  catch (error) {
80
- throw new Error(`${path} is not JSON (${messageOf(error)}). Fix the file, then run /reload-plugins.`);
81
+ throw new Error(`${path} is not JSON (${messageOf(error)}). Fix it, then press Retry.`);
81
82
  }
82
83
  }
83
84
  async function cmodVersion(claude) {
@@ -116,15 +117,52 @@ export function createLifecycle(definition, checksPermissions = () => true, opti
116
117
  return runtime.claude;
117
118
  };
118
119
  const installing = (description) => runtime?.toaster.toast.loading('Installing', { id: installId, ...(description === undefined ? {} : { description }) });
119
- const toastFailure = (shown) => {
120
- if (runtime !== undefined)
121
- showFailure({ name: definition.name, ui: runtime.toaster, claude: runtime.claude }, shown);
122
- };
120
+ const toastFailure = (shown) => runtime?.toaster.fail(shown);
123
121
  const fail = (reason, fix, retry) => {
124
122
  phase = 'failed';
125
- failure ??= new Error(`${definition.name}: ${reason}${/[.!?]$/.test(reason) ? '' : '.'} ${fix}`);
126
- const action = retry === undefined ? {} : { action: { label: 'Retry', onClick: () => ((failure = undefined), retry().catch(report)) } };
127
- runtime?.toaster.toast.error(reason, { id: installId, description: fix, duration: Infinity, ...action });
123
+ failure ??= new Error(`${definition.name}: ${reason}${/[.!?]$/.test(reason) ? '' : '.'}${fix === undefined ? '' : ` ${fix}`}`);
124
+ const again = retry === undefined ? {} : { retry: () => ((failure = undefined), stopRetrying(), retry().catch(report)) };
125
+ toastFailure({ id: installId, message: reason, ...(fix === undefined ? {} : { description: fix }), ...again });
126
+ };
127
+ let consented;
128
+ const installAgain = () => install(consented);
129
+ let retrying;
130
+ const retryLater = (next) => {
131
+ const seconds = installRetrySeconds[Math.min(next.tries, installRetrySeconds.length - 1)];
132
+ retrying?.timer.cancel();
133
+ retrying = { ...next, tries: next.tries + 1, timer: claude().clock.after(seconds * 1000, () => void retryInBackground().catch(report)) };
134
+ fail(next.reason, `Trying again in ${seconds} s.`, installAgain);
135
+ };
136
+ const stopRetrying = () => {
137
+ retrying?.timer.cancel();
138
+ retrying = undefined;
139
+ };
140
+ const checkoutState = async () => {
141
+ if (plugin === undefined)
142
+ return undefined;
143
+ const lines = [];
144
+ const { code } = await readLines(claude().process.spawn({ argv: ['sh', '-c', 'git rev-parse HEAD && git status --porcelain=v1 -uall && git diff HEAD'], cwd: plugin.root }), (text) => void lines.push(text));
145
+ return code === 0 ? lines.join('\n') : undefined;
146
+ };
147
+ const failInstall = async (reason, when) => {
148
+ if (when === undefined) {
149
+ stopRetrying();
150
+ return fail(reason, undefined, installAgain);
151
+ }
152
+ retryLater({ when, tries: retrying?.when === when ? retrying.tries : 0, checkout: when === 'changed' ? await checkoutState() : undefined, reason });
153
+ };
154
+ const retryInBackground = async () => {
155
+ const waiting = retrying;
156
+ if (phase !== 'failed' || waiting === undefined)
157
+ return;
158
+ if (await recover()) {
159
+ stopRetrying();
160
+ return whenActive();
161
+ }
162
+ if (waiting.when === 'changed' && waiting.checkout !== undefined && (await checkoutState()) === waiting.checkout)
163
+ return retryLater(waiting);
164
+ await install(consented, true);
165
+ await whenActive();
128
166
  };
129
167
  const finish = async () => {
130
168
  await programs?.refresh();
@@ -215,7 +253,7 @@ export function createLifecycle(definition, checksPermissions = () => true, opti
215
253
  const active = await createMod(definition, { ...runtime, router: activeRouter, dataFolder: dataFolder(plugin.store, plugin.name), store: plugin.store, steps: plugin.steps, granted, refreshGrants, checksPermissions, options: pluginOptions }).catch(async (error) => {
216
254
  if (error instanceof MissingOptions && (await askOptions(error)))
217
255
  return undefined;
218
- fail(messageOf(error), error instanceof MissingOptions ? `Set ${error.keys.length === 1 ? 'it' : 'them'} in /config.` : 'Fix it, then run /reload-plugins.');
256
+ fail(messageOf(error), error instanceof MissingOptions ? `Set ${error.keys.length === 1 ? 'it' : 'them'} in /config.` : undefined, begin);
219
257
  throw error;
220
258
  });
221
259
  if (active === undefined)
@@ -263,18 +301,21 @@ export function createLifecycle(definition, checksPermissions = () => true, opti
263
301
  claude().ui.log(`${name} waits for consent to run ${event.install}. Run cmod install ${name} in a terminal.`);
264
302
  return;
265
303
  }
266
- installing('Waiting for your answer');
304
+ runtime?.toaster.toast.info('Installing', { id: installId, description: 'Waiting for your answer', duration: Infinity });
267
305
  const updates = await updatesToShow().catch((error) => {
268
306
  toastFailure({ id: 'cmod:updates', message: 'Could not read whether its marketplace updates automatically', error });
269
307
  return undefined;
270
308
  });
271
- if (await installer.consent({ ...event, updates }))
272
- return install(event.sha256);
309
+ const isAccepted = await installer.consent({ ...event, updates });
273
310
  await installer.close();
311
+ if (isAccepted) {
312
+ consented = event.sha256;
313
+ return install(consented);
314
+ }
274
315
  phase = 'declined';
275
- runtime?.toaster.toast('Not installed', { id: installId, description: `Run cmod install ${name} to install it.` });
316
+ runtime?.toaster.toast('Not installed', { id: installId, description: 'Install it whenever you like.', duration: Infinity, action: { label: 'Install', onClick: () => install().catch(report) } });
276
317
  };
277
- const install = async (consent) => {
318
+ const install = async (consent, isQuiet = false) => {
278
319
  if (plugin === undefined)
279
320
  return;
280
321
  if (plugin.name === cmodPluginName)
@@ -283,25 +324,29 @@ export function createLifecycle(definition, checksPermissions = () => true, opti
283
324
  const found = await cmodVersion(claude());
284
325
  if (!fits(found))
285
326
  return waitForcmod(found);
286
- installing();
327
+ if (!isQuiet)
328
+ installing();
287
329
  let outcome;
330
+ let retries;
288
331
  const argv = ['cmod', 'setup', plugin.root, '--events', ...(consent === undefined ? [] : ['--consent', consent])];
289
332
  const { code, lastError } = await readLines(claude().process.spawn({ argv }), (text) => {
290
333
  const event = parseEvent(text);
291
- if (event.type === 'progress')
334
+ if (event.type === 'progress' && !isQuiet)
292
335
  installing(progressOf(event));
336
+ if (event.type === 'retries')
337
+ retries = event.when;
293
338
  if (event.type === 'done' || event.type === 'failed' || event.type === 'needs-consent')
294
339
  outcome = event;
295
340
  });
296
341
  if (outcome?.type === 'done') {
342
+ stopRetrying();
297
343
  for (const item of plugin.steps.permissions ?? [])
298
344
  granted.add(item);
299
345
  return finish();
300
346
  }
301
347
  if (outcome?.type === 'needs-consent')
302
348
  return askConsent(outcome);
303
- const reason = outcome?.type === 'failed' ? outcome.message : `cmod setup ${formatExit(code, lastError)}`;
304
- fail(reason, `Fix the cause, then retry, or run cmod install ${definition.name} in a terminal.`, install);
349
+ return failInstall(outcome?.type === 'failed' ? outcome.message : `cmod setup ${formatExit(code, lastError)}`, retries);
305
350
  };
306
351
  const fits = (found) => found !== undefined && isAtLeast(found, oldestCmodFor(plugin?.steps ?? {}));
307
352
  const waitForcmod = (found) => {
@@ -342,12 +387,32 @@ export function createLifecycle(definition, checksPermissions = () => true, opti
342
387
  });
343
388
  if (code === 0)
344
389
  return finish();
345
- fail(`bootstrap ${formatExit(code, lastError)}`, `Run ./setup/bootstrap.sh in ${root} to see the whole log.`, () => bootstrap(root));
390
+ fail(`bootstrap ${formatExit(code, lastError)}`, undefined, () => bootstrap(root));
391
+ };
392
+ const begin = async () => {
393
+ if (readStarted === undefined)
394
+ return;
395
+ const started = await readStarted(claude());
396
+ plugin = started;
397
+ for (const item of started.granted)
398
+ granted.add(item);
399
+ consentMs = await stampOf(consentPath(started.store));
400
+ recordMs = await stampOf(recordPath(started.store, started.name));
401
+ if (started.isInstalled) {
402
+ shouldRecord = started.shouldRecord;
403
+ if (shouldRecord)
404
+ record();
405
+ activation = undefined;
406
+ return activate();
407
+ }
408
+ if (started.version === undefined)
409
+ return fail('no version to install', 'Its author adds "version" to .claude-plugin/plugin.json.');
410
+ void install().catch(report);
346
411
  };
347
412
  const report = (error) => {
348
413
  failure = error;
349
414
  if (phase !== 'failed')
350
- fail(messageOf(error), `Run cmod install ${definition.name} in a terminal to see the whole log.`);
415
+ fail(messageOf(error), undefined, begin);
351
416
  };
352
417
  let consentMs = 0;
353
418
  const stampOf = async (path) => {
@@ -358,19 +423,20 @@ export function createLifecycle(definition, checksPermissions = () => true, opti
358
423
  let readStarted;
359
424
  const recover = async () => {
360
425
  if (plugin === undefined || readStarted === undefined || (phase !== 'failed' && phase !== 'declined'))
361
- return;
426
+ return false;
362
427
  const modifiedMs = await stampOf(recordPath(plugin.store, plugin.name));
363
428
  if (modifiedMs === recordMs)
364
- return;
429
+ return false;
365
430
  recordMs = modifiedMs;
366
431
  const current = await readStarted(claude());
367
432
  if (!current.isInstalled)
368
- return;
433
+ return false;
369
434
  plugin = current;
370
435
  failure = undefined;
371
436
  activation = undefined;
372
437
  await refreshGrants();
373
438
  await finish();
439
+ return true;
374
440
  };
375
441
  const refreshGrants = async () => {
376
442
  if (plugin === undefined || plugin.name === cmodPluginName)
@@ -440,30 +506,11 @@ export function createLifecycle(definition, checksPermissions = () => true, opti
440
506
  if (runtime !== undefined)
441
507
  return;
442
508
  programs = locatingPrograms(claudeCalls);
443
- runtime = { claude: programs.claude, toaster: createToaster(claudeCalls, definition.name) };
509
+ runtime = { claude: programs.claude, toaster: createToaster(claudeCalls, definition.name, () => plugin?.version) };
510
+ readStarted = read;
444
511
  try {
445
512
  await programs.refresh();
446
- const started = await read(claudeCalls).catch((error) => {
447
- report(error);
448
- return undefined;
449
- });
450
- if (started === undefined)
451
- return;
452
- plugin = started;
453
- readStarted = read;
454
- for (const item of started.granted)
455
- granted.add(item);
456
- consentMs = await stampOf(consentPath(started.store));
457
- recordMs = await stampOf(recordPath(started.store, started.name));
458
- if (started.isInstalled) {
459
- shouldRecord = started.shouldRecord;
460
- if (shouldRecord)
461
- record();
462
- return await activate().catch(report);
463
- }
464
- if (started.version === undefined)
465
- return fail('no version to install', 'Add "version" to .claude-plugin/plugin.json, then run /reload-plugins.');
466
- void install().catch(report);
513
+ await begin().catch(report);
467
514
  }
468
515
  finally {
469
516
  settleStart();
@@ -487,12 +534,14 @@ export function createLifecycle(definition, checksPermissions = () => true, opti
487
534
  if (event === 'classic.UserPromptSubmit' || event === 'classic.SessionStart')
488
535
  await recover().catch((error) => claude().ui.log(`${definition.name} stays stopped: ${messageOf(error)}`, { to: 'debug' }));
489
536
  const isCallToThisMod = event === 'cmod.call' && e.to === definition.name;
537
+ if (event === 'cmod.call' && !isCallToThisMod)
538
+ return next(e);
490
539
  await (event === 'classic.SessionStart' || isCallToThisMod ? holdUntilStarted() : whenActive());
491
540
  if (event === 'classic.SessionStart' && phase !== 'active')
492
541
  missedSessionStart = e;
493
542
  if (isCallToThisMod && phase !== 'active') {
494
543
  const isStopped = phase === 'declined' || phase === 'failed';
495
- return { deny: isStopped ? notInstalled(definition.name) : `${definition.name} is installing. Try again when it's ready.` };
544
+ return { deny: isStopped ? `${definition.name} is not set up. Its toast says why.` : `${definition.name} is installing. Try again when it's ready.` };
496
545
  }
497
546
  const call = e;
498
547
  if (event === 'cmod.call' && call.to === definition.name && mod !== undefined) {
@@ -503,19 +552,19 @@ export function createLifecycle(definition, checksPermissions = () => true, opti
503
552
  }
504
553
  if (event === 'classic.UserPromptSubmit' || event === 'classic.SessionStart')
505
554
  await refreshGrants().catch((error) => claude().ui.log(`${definition.name} keeps its grants from before: ${messageOf(error)}`, { to: 'debug' }));
506
- if (checksAnswers(event) && phase === 'active')
555
+ if (checksAnswers(event) && phase === 'active' && definition.name !== cmodPluginName)
507
556
  return checkedRoute(event, e, next);
508
557
  const answer = router.dispatch(event, e, next);
509
558
  const render = e;
510
559
  if (event !== 'ui.render' || render.component !== 'AbovePrompt' || runtime === undefined)
511
560
  return answer;
512
- return runtime.toaster.draw((await answer), runtime.claude.ui.resolve(render), render.props);
561
+ return runtime.toaster.draw((await answer), runtime.claude.ui.resolve(render), render.props, render.requestId);
513
562
  },
514
563
  };
515
564
  }
516
565
  async function createMod(definition, runtime) {
517
566
  const { router, toaster } = runtime;
518
- const report = (failure) => void showFailure({ name: definition.name, ui: toaster, claude: runtime.claude }, failure);
567
+ const report = (failure) => void toaster.fail(failure);
519
568
  const claude = withBareRefusals(checkingKeys(runtime.claude, toaster.toast, runtime.steps.keys ?? {}));
520
569
  const added = [];
521
570
  const hooksAdded = [];
@@ -571,6 +620,7 @@ async function createMod(definition, runtime) {
571
620
  let forkedFrom;
572
621
  let parentAsked;
573
622
  let hasEarlierPrompts = true;
623
+ let agentType;
574
624
  const area = createUi({ name: definition.name, claude, router, toaster, report, announce, mod: () => mod });
575
625
  const askParent = async () => {
576
626
  if (definition.name === cmodPluginName)
@@ -591,7 +641,7 @@ async function createMod(definition, runtime) {
591
641
  const modState = createState({ name: definition.name, initial: definition.state ?? {}, session, root, claude, report, changed: area.changed, forkParent });
592
642
  const modOptions = createOptions({ name: definition.name, declared: definition.options ?? {}, fromClaude: runtime.options, claude, report, changed: modState.changed });
593
643
  const held = runtime.checksPermissions() ? new Map() : undefined;
594
- const expand = (path) => (path === '~' || path.startsWith('~/') ? resolvedPath(path, places) : path);
644
+ const expand = (path) => (homePrefix.test(path) ? resolvedPath(path, places) : path);
595
645
  const settingsFile = (source) => ({
596
646
  read: () => checked.settings.read({ source }),
597
647
  async update(values) {
@@ -599,7 +649,7 @@ async function createMod(definition, runtime) {
599
649
  await claude.cmod.call({ to: cmodPluginName, method: 'updateSettings', input: { mod: definition.name, source, project: modState.root, values } });
600
650
  },
601
651
  });
602
- const client = createSessionsClient({ name: definition.name, claude: checked, report: (id, message, error) => report({ id, message, error }) });
652
+ const client = createSessionsClient({ name: definition.name, claude: checked, report: (id, message, error) => report({ id, message, error }), isGranted: (permission) => flag(permission) });
603
653
  let followed;
604
654
  const followFolders = (project, folder) => {
605
655
  if (followed === undefined)
@@ -609,7 +659,21 @@ async function createMod(definition, runtime) {
609
659
  };
610
660
  const imports = {
611
661
  sessions: client.sessions,
612
- settings: { user: settingsFile('user'), project: settingsFile('project'), local: settingsFile('local') },
662
+ settings: {
663
+ list: async () => (await checked.config.list()).map(({ key, label, description, kind, value, options, provider, isLocked }) => ({
664
+ key,
665
+ label,
666
+ kind,
667
+ value,
668
+ isLocked,
669
+ ...(description === undefined ? {} : { description }),
670
+ ...(options === undefined ? {} : { options }),
671
+ ...(provider.plugin === 'engine' ? {} : { plugin: provider.plugin }),
672
+ })),
673
+ user: settingsFile('user'),
674
+ project: settingsFile('project'),
675
+ local: settingsFile('local'),
676
+ },
613
677
  model: { complete: (completion, options) => checked.model.complete(completion, options) },
614
678
  fs: {
615
679
  read: (path) => checked.fs.read(expand(path)),
@@ -745,10 +809,11 @@ async function createMod(definition, runtime) {
745
809
  };
746
810
  const followCurrent = async (title) => {
747
811
  const [id, model, usage] = await Promise.all([claude.session.id(), claude.session.model(), claude.session.usage()]);
748
- followed = { id, title: title ?? '', project: modState.root, cwd, model, startedAt: new Date(usage.startedAt).toISOString(), ...(forkedFrom === undefined ? {} : { forkedFrom }) };
812
+ followed = { id, title: title ?? '', project: modState.root, cwd, model, startedAt: new Date(usage.startedAt).toISOString(), ...(forkedFrom === undefined ? {} : { forkedFrom }), ...(agentType === undefined ? {} : { agentType }) };
749
813
  client.follow(followed);
750
814
  };
751
815
  router.add('classic.SessionStart', async (e, next) => {
816
+ agentType = e.agent_id === undefined ? e.agent_type : agentType;
752
817
  const previous = followed?.id;
753
818
  if (e.source === 'fork' && previous !== undefined && previous !== e.session_id)
754
819
  forkedFrom = previous;
@@ -779,8 +844,10 @@ async function createMod(definition, runtime) {
779
844
  }
780
845
  router.add('command.run', async (e, next) => {
781
846
  const answer = await next(e);
782
- if (e.command === 'cd')
847
+ if (e.command === 'cd') {
783
848
  await followSession(true);
849
+ conversations?.moved();
850
+ }
784
851
  return answer;
785
852
  });
786
853
  router.add('skill.prompt', userSkillHook(claude));
@@ -793,10 +860,14 @@ async function createMod(definition, runtime) {
793
860
  if (e.method === conversationMethods.changed && call.caller === cmodPluginName) {
794
861
  if (!flag('conversation'))
795
862
  return { value: null };
796
- const { session: changedSession, change } = e.input;
797
- client.changed(changedSession, change);
863
+ const { session: changedSession, change, isInProject } = e.input;
864
+ client.changed(changedSession, change, isInProject !== false);
798
865
  return { value: null };
799
866
  }
867
+ if (e.method === conversationMethods.before && call.caller === cmodPluginName) {
868
+ const { session: asking, event, change, isInProject } = e.input;
869
+ return { value: (await client.answerBefore(asking, event, change, isInProject !== false)) ?? null };
870
+ }
800
871
  if (e.method === settingsPagesMethod)
801
872
  return { value: [...pages].map(([id, { title }]) => ({ id, title })) };
802
873
  if (e.method === openPageMethod) {
@@ -1 +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;AAKpD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AACzC,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,cAAc,CAAA;AAE3C,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,EAC7B,IAAI,EACJ,MAAM,EACN,MAAM,EACN,UAAU,EACV,MAAM,GACP,EAAE;IACD,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,UAAU,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;IACpD,QAAQ,CAAC,MAAM,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,IAAI,CAAA;CAC5C,GAAG,QAAQ,CA4HX"}
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;AAKpD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AACzC,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,cAAc,CAAA;AAE3C,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,CASjE;AAED,wBAAgB,cAAc,CAAC,EAC7B,IAAI,EACJ,MAAM,EACN,MAAM,EACN,UAAU,EACV,MAAM,GACP,EAAE;IACD,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,UAAU,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;IACpD,QAAQ,CAAC,MAAM,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,IAAI,CAAA;CAC5C,GAAG,QAAQ,CA4HX"}
@@ -1,5 +1,5 @@
1
1
  import { formatOf, MetadataError, readMetadata, sidecarSuffix, writeMetadata } from '../utils/metadata.js';
2
- import { ifMissing, isMissing } from '../utils/paths.js';
2
+ import { expandHome, homePrefix, ifMissing, isMissing } from '../utils/paths.js';
3
3
  import { messageOf } from '../utils/text.js';
4
4
  import { picomatch, resolve } from '../vendor.js';
5
5
  const skippedFolders = ['.git', 'node_modules'];
@@ -8,10 +8,11 @@ export function resolvedPath(path, places) {
8
8
  const configured = /^~\/\.claude(?=\/|$)/.exec(path);
9
9
  if (configured !== null && places.configRoot !== undefined)
10
10
  return resolve(`${places.configRoot}${path.slice(configured[0].length)}`);
11
- if (path === '~' || path.startsWith('~/')) {
11
+ const prefix = homePrefix.exec(path)?.[0];
12
+ if (prefix !== undefined) {
12
13
  if (places.home === undefined)
13
- throw new Error(`${path} starts with ~, and HOME is not set. Start Claude Code with HOME set.`);
14
- return resolve(`${places.home}${path.slice(1)}`);
14
+ throw new Error(`${path} starts with ${prefix}, and HOME is not set. Start Claude Code with HOME set.`);
15
+ return resolve(expandHome(path, places.home, places.configRoot));
15
16
  }
16
17
  return path.startsWith('/') ? resolve(path) : resolve(places.projectRoot(), path);
17
18
  }