@cmodjs/core 0.0.0-stage → 0.1.2

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 (155) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +11 -2
  3. package/connect.d.ts +4 -0
  4. package/connect.d.ts.map +1 -0
  5. package/connect.js +98 -0
  6. package/jobs/check.d.ts +8 -0
  7. package/jobs/check.d.ts.map +1 -0
  8. package/jobs/check.js +60 -0
  9. package/jobs/part-context.d.ts +14 -0
  10. package/jobs/part-context.d.ts.map +1 -0
  11. package/jobs/part-context.js +106 -0
  12. package/jobs/permissions/decide-permission.d.ts +21 -0
  13. package/jobs/permissions/decide-permission.d.ts.map +1 -0
  14. package/jobs/permissions/decide-permission.js +71 -0
  15. package/jobs/permissions/find-project-scope.d.ts +13 -0
  16. package/jobs/permissions/find-project-scope.d.ts.map +1 -0
  17. package/jobs/permissions/find-project-scope.js +51 -0
  18. package/jobs/permissions/match-target.d.ts +27 -0
  19. package/jobs/permissions/match-target.d.ts.map +1 -0
  20. package/jobs/permissions/match-target.js +113 -0
  21. package/jobs/permissions.d.ts +8 -0
  22. package/jobs/permissions.d.ts.map +1 -0
  23. package/jobs/permissions.js +30 -0
  24. package/jobs/program.d.ts +13 -0
  25. package/jobs/program.d.ts.map +1 -0
  26. package/jobs/program.js +91 -0
  27. package/jobs/prompt.d.ts +16 -0
  28. package/jobs/prompt.d.ts.map +1 -0
  29. package/jobs/prompt.js +85 -0
  30. package/jobs/slash-command.d.ts +16 -0
  31. package/jobs/slash-command.d.ts.map +1 -0
  32. package/jobs/slash-command.js +54 -0
  33. package/jobs/status-line.d.ts +17 -0
  34. package/jobs/status-line.d.ts.map +1 -0
  35. package/jobs/status-line.js +43 -0
  36. package/jobs/tool.d.ts +13 -0
  37. package/jobs/tool.d.ts.map +1 -0
  38. package/jobs/tool.js +50 -0
  39. package/mod.d.ts +131 -0
  40. package/mod.d.ts.map +1 -0
  41. package/mod.js +8 -0
  42. package/package.json +47 -4
  43. package/records.d.ts +62 -0
  44. package/records.d.ts.map +1 -0
  45. package/records.js +193 -0
  46. package/runtime/claude.d.ts +77 -0
  47. package/runtime/claude.d.ts.map +1 -0
  48. package/runtime/claude.js +1 -0
  49. package/runtime/deadline.d.ts +10 -0
  50. package/runtime/deadline.d.ts.map +1 -0
  51. package/runtime/deadline.js +15 -0
  52. package/runtime/dependencies.d.ts +8 -0
  53. package/runtime/dependencies.d.ts.map +1 -0
  54. package/runtime/dependencies.js +19 -0
  55. package/runtime/hooks.d.ts +9 -0
  56. package/runtime/hooks.d.ts.map +1 -0
  57. package/runtime/hooks.js +167 -0
  58. package/runtime/lifecycle.d.ts +24 -0
  59. package/runtime/lifecycle.d.ts.map +1 -0
  60. package/runtime/lifecycle.js +469 -0
  61. package/runtime/router.d.ts +10 -0
  62. package/runtime/router.d.ts.map +1 -0
  63. package/runtime/router.js +20 -0
  64. package/runtime/state.d.ts +19 -0
  65. package/runtime/state.d.ts.map +1 -0
  66. package/runtime/state.js +187 -0
  67. package/runtime/tool-calls.d.ts +13 -0
  68. package/runtime/tool-calls.d.ts.map +1 -0
  69. package/runtime/tool-calls.js +60 -0
  70. package/runtime/ui.d.ts +31 -0
  71. package/runtime/ui.d.ts.map +1 -0
  72. package/runtime/ui.js +357 -0
  73. package/src/connect.ts +107 -0
  74. package/src/jobs/check.ts +57 -0
  75. package/src/jobs/part-context.ts +118 -0
  76. package/src/jobs/permissions/decide-permission.ts +80 -0
  77. package/src/jobs/permissions/find-project-scope.ts +50 -0
  78. package/src/jobs/permissions/match-target.ts +114 -0
  79. package/src/jobs/permissions.ts +41 -0
  80. package/src/jobs/program.ts +94 -0
  81. package/src/jobs/prompt.ts +104 -0
  82. package/src/jobs/slash-command.ts +67 -0
  83. package/src/jobs/status-line.ts +58 -0
  84. package/src/jobs/tool.ts +58 -0
  85. package/src/mod.ts +173 -0
  86. package/src/records.ts +204 -0
  87. package/src/runtime/claude.ts +89 -0
  88. package/src/runtime/deadline.ts +24 -0
  89. package/src/runtime/dependencies.ts +26 -0
  90. package/src/runtime/hooks.ts +175 -0
  91. package/src/runtime/lifecycle.ts +485 -0
  92. package/src/runtime/router.ts +34 -0
  93. package/src/runtime/state.ts +211 -0
  94. package/src/runtime/tool-calls.ts +69 -0
  95. package/src/runtime/ui.ts +409 -0
  96. package/src/testing/fake-claude.ts +178 -0
  97. package/src/testing/fake-elements.ts +163 -0
  98. package/src/testing/fake-files.ts +53 -0
  99. package/src/testing.ts +211 -0
  100. package/src/ui/define-pane.ts +23 -0
  101. package/src/ui/elements.ts +38 -0
  102. package/src/ui/markdown.ts +173 -0
  103. package/src/ui/slots.ts +36 -0
  104. package/src/utils/call-effects.ts +104 -0
  105. package/src/utils/matches-glob.ts +5 -0
  106. package/src/utils/parse-shell.ts +832 -0
  107. package/src/utils/paths.ts +40 -0
  108. package/src/utils/text.ts +14 -0
  109. package/src/vendor-markdown.ts +4 -0
  110. package/src/vendor.ts +3 -0
  111. package/testing/fake-claude.d.ts +60 -0
  112. package/testing/fake-claude.d.ts.map +1 -0
  113. package/testing/fake-claude.js +132 -0
  114. package/testing/fake-elements.d.ts +15 -0
  115. package/testing/fake-elements.d.ts.map +1 -0
  116. package/testing/fake-elements.js +163 -0
  117. package/testing/fake-files.d.ts +3 -0
  118. package/testing/fake-files.d.ts.map +1 -0
  119. package/testing/fake-files.js +58 -0
  120. package/testing.d.ts +41 -0
  121. package/testing.d.ts.map +1 -0
  122. package/testing.js +173 -0
  123. package/ui/define-pane.d.ts +11 -0
  124. package/ui/define-pane.d.ts.map +1 -0
  125. package/ui/define-pane.js +12 -0
  126. package/ui/elements.d.ts +12 -0
  127. package/ui/elements.d.ts.map +1 -0
  128. package/ui/elements.js +30 -0
  129. package/ui/markdown.d.ts +49 -0
  130. package/ui/markdown.d.ts.map +1 -0
  131. package/ui/markdown.js +128 -0
  132. package/ui/slots.d.ts +208 -0
  133. package/ui/slots.d.ts.map +1 -0
  134. package/ui/slots.js +19 -0
  135. package/utils/call-effects.d.ts +52 -0
  136. package/utils/call-effects.d.ts.map +1 -0
  137. package/utils/call-effects.js +89 -0
  138. package/utils/matches-glob.d.ts +2 -0
  139. package/utils/matches-glob.d.ts.map +1 -0
  140. package/utils/matches-glob.js +4 -0
  141. package/utils/parse-shell.d.ts +18 -0
  142. package/utils/parse-shell.d.ts.map +1 -0
  143. package/utils/parse-shell.js +864 -0
  144. package/utils/paths.d.ts +15 -0
  145. package/utils/paths.d.ts.map +1 -0
  146. package/utils/paths.js +33 -0
  147. package/utils/text.d.ts +4 -0
  148. package/utils/text.d.ts.map +1 -0
  149. package/utils/text.js +13 -0
  150. package/vendor-markdown.d.ts +5 -0
  151. package/vendor-markdown.d.ts.map +1 -0
  152. package/vendor-markdown.js +8692 -0
  153. package/vendor.d.ts +4 -0
  154. package/vendor.d.ts.map +1 -0
  155. package/vendor.js +3174 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Jordan Parker
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,12 @@
1
- # Temporary Holding Version
1
+ # @cmodjs/core
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ The library Claude Code mods import. CMod is the Claude Mod Manager: it installs, tries, builds, checks, and publishes Claude Code mods, which are plugins with hooks, panes, commands, tools, permissions, and an install step that runs with your consent.
4
+
5
+ ```sh
6
+ npm i -g @cmodjs/cli
7
+ cmod new my-mod
8
+ ```
9
+
10
+ `cmod new` writes a mod that depends on this package. A mod imports it by path, such as `../node_modules/@cmodjs/core/mod.js`, because Claude Code loads a plugin's own files only.
11
+
12
+ The guide and the CMod plugin live at [github.com/heyJordanParker/cmod](https://github.com/heyJordanParker/cmod).
package/connect.d.ts ADDED
@@ -0,0 +1,4 @@
1
+ import type { On } from 'claude-code';
2
+ import type { ModDefinition } from './mod.js';
3
+ export declare function connect<State extends object>(on: On, definition: ModDefinition<State>): void;
4
+ //# sourceMappingURL=connect.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"connect.d.ts","sourceRoot":"","sources":["src/connect.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAoD,EAAE,EAAE,MAAM,aAAa,CAAA;AACvF,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,UAAU,CAAA;AA2E7C,wBAAgB,OAAO,CAAC,KAAK,SAAS,MAAM,EAAE,EAAE,EAAE,EAAE,EAAE,UAAU,EAAE,aAAa,CAAC,KAAK,CAAC,GAAG,IAAI,CA8B5F"}
package/connect.js ADDED
@@ -0,0 +1,98 @@
1
+ import { createLifecycle, readPlugin } from './runtime/lifecycle.js';
2
+ let lifecycle;
3
+ async function start($, e, next) {
4
+ await lifecycle.start(claudeOf($), readPlugin);
5
+ return next(e);
6
+ }
7
+ function route(_$, e, next) {
8
+ return lifecycle.route(next.event, e, (passed) => next(passed));
9
+ }
10
+ function claudeOf($) {
11
+ return {
12
+ plugin: { name: $.plugin.name, root: $.plugin.root },
13
+ ui: {
14
+ toast: (text, options) => $.ui.toast(text, options),
15
+ status: (text) => $.ui.status(text),
16
+ log: (text, options) => $.ui.log(text, options),
17
+ notice: (toolUseId, text) => $.ui.notice(toolUseId, text),
18
+ invalidate: (event) => $.ui.invalidate(event),
19
+ ask: (question, options) => $.ui.ask(question, options),
20
+ open: (pane) => $.ui.open(pane),
21
+ close: (pane) => $.ui.close(pane),
22
+ panes: () => $.ui.panes(),
23
+ resolve: (render) => $.ui.resolve(render),
24
+ },
25
+ process: {
26
+ run: (argv, init) => $.process.run(argv, init),
27
+ spawn: (request) => $.process.spawn(request),
28
+ },
29
+ fs: {
30
+ read: (path) => $.fs.read(path),
31
+ write: (path, text) => $.fs.write(path, text),
32
+ list: (path) => $.fs.list(path),
33
+ exists: (path) => $.fs.exists(path),
34
+ stat: (path, options) => $.fs.stat(path, options),
35
+ },
36
+ http: { fetch: (url, init) => $.http.fetch(url, init) },
37
+ settings: { read: (args) => $.settings.read(args) },
38
+ store: {
39
+ get: (key) => $.store.get(key),
40
+ set: (key, value) => $.store.set(key, value),
41
+ delete: (key) => $.store.delete(key),
42
+ keys: () => $.store.keys(),
43
+ },
44
+ clock: {
45
+ now: () => $.clock.now(),
46
+ after: (ms, fn) => $.clock.after(ms, fn),
47
+ every: (ms, fn) => $.clock.every(ms, fn),
48
+ },
49
+ session: {
50
+ id: () => $.session.id(),
51
+ root: () => $.session.root(),
52
+ cwd: () => $.session.cwd(),
53
+ model: () => $.session.model(),
54
+ usage: () => $.session.usage(),
55
+ surfaces: () => $.session.surfaces(),
56
+ },
57
+ command: { register: (command) => $.command.register(command) },
58
+ tool: { register: (tool) => $.tool.register(tool) },
59
+ agent: { list: () => $.agent.list() },
60
+ env: {
61
+ home: () => $.env.get('HOME'),
62
+ dataHome: () => $.env.get('XDG_DATA_HOME'),
63
+ configHome: () => $.env.get('CLAUDE_CONFIG_DIR'),
64
+ },
65
+ cmod: { call: (input) => $.cmod.call(input) },
66
+ };
67
+ }
68
+ export function connect(on, definition) {
69
+ lifecycle = createLifecycle(definition);
70
+ on('session.start', start);
71
+ on('classic.SessionStart', route);
72
+ on('classic.SessionEnd', route);
73
+ on('classic.UserPromptSubmit', route);
74
+ on('classic.InstructionsLoaded', route);
75
+ on('classic.PreToolUse', route);
76
+ on('classic.PermissionRequest', route);
77
+ on('classic.PermissionDenied', route);
78
+ on('classic.PostToolUse', route);
79
+ on('classic.PostToolUseFailure', route);
80
+ on('classic.PostToolBatch', route);
81
+ on('classic.SubagentStart', route);
82
+ on('classic.SubagentStop', route);
83
+ on('classic.Notification', route);
84
+ on('classic.PreCompact', route);
85
+ on('classic.Stop', route);
86
+ on('classic.StopFailure', route);
87
+ on('tool.check', route);
88
+ on('tool.call', route);
89
+ on('prompt.submit', route);
90
+ on('prompt.context', route);
91
+ on('command.run', route);
92
+ on('session.measure', route);
93
+ on('skill.prompt', route);
94
+ on('ui.render', route);
95
+ on('ui.press', route);
96
+ on('ui.close', route);
97
+ on('cmod.call', route);
98
+ }
@@ -0,0 +1,8 @@
1
+ import type { Part } from '../mod.js';
2
+ import { type Target } from './permissions/match-target.js';
3
+ export declare function check<State extends object = Record<never, never>>(options: {
4
+ readonly after: Target | Target[];
5
+ readonly run: readonly string[];
6
+ readonly timeoutMs?: number;
7
+ }): Part<void, State>;
8
+ //# sourceMappingURL=check.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"check.d.ts","sourceRoot":"","sources":["../src/jobs/check.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,WAAW,CAAA;AAOrC,OAAO,EAAyB,KAAK,MAAM,EAAE,MAAM,+BAA+B,CAAA;AAIlF,wBAAgB,KAAK,CAAC,KAAK,SAAS,MAAM,GAAG,MAAM,CAAC,KAAK,EAAE,KAAK,CAAC,EAAE,OAAO,EAAE;IAAE,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;IAAC,QAAQ,CAAC,GAAG,EAAE,SAAS,MAAM,EAAE,CAAC;IAAC,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAAC,IAAI,EAAE,KAAK,CAAC,CA4BlM"}
package/jobs/check.js ADDED
@@ -0,0 +1,60 @@
1
+ import { longestMs } from '../runtime/deadline.js';
2
+ import { callEffects } from '../utils/call-effects.js';
3
+ import { parentOf } from '../utils/paths.js';
4
+ import { listed, messageOf } from '../utils/text.js';
5
+ import { afterCall, modOf, targetWords } from './part-context.js';
6
+ import { matchTarget, targetOf } from './permissions/match-target.js';
7
+ const defaultMs = 60_000;
8
+ export function check(options) {
9
+ const { after, run, timeoutMs = defaultMs } = options;
10
+ const targets = Array.isArray(after) ? after : [after];
11
+ if (targets.length === 0)
12
+ throw new Error("check: give after a target, such as { write: '**/*.ts' }.");
13
+ for (const target of targets)
14
+ targetOf(target);
15
+ const named = `check after ${listed(targets.map(targetWords))}`;
16
+ if (run.length === 0)
17
+ throw new Error(`check: the ${named} has no command. Give run a command, such as ['bun', 'test'].`);
18
+ if (!(timeoutMs > 0 && timeoutMs <= longestMs))
19
+ throw new Error(`check: the ${named} has timeoutMs ${timeoutMs}. Set it above 0 and at most ${longestMs} (${longestMs / 60_000} minutes).`);
20
+ const deadline = { ms: timeoutMs, job: 'check' };
21
+ return (context) => {
22
+ afterCall(context, async (use, workspace) => {
23
+ const triggered = await triggeredRun(targets, run, use, workspace);
24
+ if (triggered === undefined)
25
+ return [];
26
+ try {
27
+ const { command, folder } = triggered;
28
+ const { exitCode, stdout, stderr } = await modOf(context, { folder }, workspace, deadline).process.run(command);
29
+ return exitCode === 0 ? [] : [`${command.join(' ')} exited with ${exitCode}:\n${`${stdout}${stderr}`.trim()}`];
30
+ }
31
+ catch (error) {
32
+ return [`The ${named} failed: ${messageOf(error)}`];
33
+ }
34
+ }, (error) => [`The check job failed, so the ${named} did not run: ${messageOf(error)}`]);
35
+ context.announce(`a ${named}`);
36
+ };
37
+ }
38
+ async function triggeredRun(after, run, use, workspace) {
39
+ const effects = callEffects(use, workspace);
40
+ const paths = new Set();
41
+ let commandFolder;
42
+ for (const target of after) {
43
+ const matched = await matchTarget(target, use, effects, workspace);
44
+ const { key } = targetOf(target);
45
+ if (key !== 'read' && key !== 'write')
46
+ commandFolder ??= matched[0]?.folder;
47
+ else
48
+ for (const { call } of matched)
49
+ if ('path' in call)
50
+ paths.add(call.path);
51
+ }
52
+ const existing = [];
53
+ for (const path of paths)
54
+ if (await workspace.fs.exists(path))
55
+ existing.push(path);
56
+ const last = existing.at(-1);
57
+ if (last !== undefined)
58
+ return { command: [...run, ...existing], folder: parentOf(last) };
59
+ return commandFolder === undefined ? undefined : { command: [...run], folder: commandFolder };
60
+ }
@@ -0,0 +1,14 @@
1
+ import type { Mod, PartContext } from '../mod.js';
2
+ import { type Deadline } from '../runtime/deadline.js';
3
+ import type { ToolCall, ToolUse } from '../utils/call-effects.js';
4
+ import { type Workspace } from './permissions/find-project-scope.js';
5
+ import { type Target } from './permissions/match-target.js';
6
+ export declare function workspaceReader({ claude }: PartContext): () => Promise<Workspace>;
7
+ export declare function afterCall<State extends object>(part: PartContext<State>, contextAfter: (use: ToolUse, workspace: Workspace) => Promise<readonly string[]>, failed: (error: unknown) => readonly string[]): void;
8
+ export declare function modOf<State extends object>(context: PartContext<State>, { call, folder }: {
9
+ readonly call?: ToolCall;
10
+ readonly folder: string;
11
+ }, workspace: Workspace, deadline: Deadline): Mod<State>;
12
+ export declare function modWithin<State extends object>({ mod, claude }: PartContext<State>, deadline: Deadline, workTree?: () => Promise<string | undefined>): Mod<State>;
13
+ export declare function targetWords(target: Target): string;
14
+ //# sourceMappingURL=part-context.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"part-context.d.ts","sourceRoot":"","sources":["../src/jobs/part-context.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,GAAG,EAAE,WAAW,EAAE,MAAM,WAAW,CAAA;AAEjD,OAAO,EAAkB,KAAK,QAAQ,EAAE,MAAM,wBAAwB,CAAA;AAItE,OAAO,KAAK,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,0BAA0B,CAAA;AACjE,OAAO,EAAoB,KAAK,SAAS,EAAE,MAAM,qCAAqC,CAAA;AACtF,OAAO,EAAY,KAAK,MAAM,EAAE,MAAM,+BAA+B,CAAA;AAOrE,wBAAgB,eAAe,CAAC,EAAE,MAAM,EAAE,EAAE,WAAW,GAAG,MAAM,OAAO,CAAC,SAAS,CAAC,CAOjF;AAaD,wBAAgB,SAAS,CAAC,KAAK,SAAS,MAAM,EAC5C,IAAI,EAAE,WAAW,CAAC,KAAK,CAAC,EACxB,YAAY,EAAE,CAAC,GAAG,EAAE,OAAO,EAAE,SAAS,EAAE,SAAS,KAAK,OAAO,CAAC,SAAS,MAAM,EAAE,CAAC,EAChF,MAAM,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,SAAS,MAAM,EAAE,GAC5C,IAAI,CAUN;AAMD,wBAAgB,KAAK,CAAC,KAAK,SAAS,MAAM,EAAE,OAAO,EAAE,WAAW,CAAC,KAAK,CAAC,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE;IAAE,QAAQ,CAAC,IAAI,CAAC,EAAE,QAAQ,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;CAAE,EAAE,SAAS,EAAE,SAAS,EAAE,QAAQ,EAAE,QAAQ,GAAG,GAAG,CAAC,KAAK,CAAC,CAEtM;AAED,wBAAgB,SAAS,CAAC,KAAK,SAAS,MAAM,EAAE,EAAE,GAAG,EAAE,MAAM,EAAE,EAAE,WAAW,CAAC,KAAK,CAAC,EAAE,QAAQ,EAAE,QAAQ,EAAE,QAAQ,GAAE,MAAM,OAAO,CAAC,MAAM,GAAG,SAAS,CAAyB,GAAG,GAAG,CAAC,KAAK,CAAC,CAwBxL;AAwBD,wBAAgB,WAAW,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CASlD"}
@@ -0,0 +1,106 @@
1
+ import { beforeDeadline } from '../runtime/deadline.js';
2
+ import { dependencyCalls } from '../runtime/dependencies.js';
3
+ import { toolInputOf } from '../runtime/tool-calls.js';
4
+ import { listed } from '../utils/text.js';
5
+ import { findProjectScope } from './permissions/find-project-scope.js';
6
+ import { targetOf } from './permissions/match-target.js';
7
+ export function workspaceReader({ claude }) {
8
+ let session;
9
+ return async () => {
10
+ session ??= readSession(claude);
11
+ const [{ home, scope, fs }, projectRoot, cwd] = await Promise.all([session, claude.session.root(), claude.session.cwd()]);
12
+ return { projectRoot, cwd, home, fs, scope };
13
+ };
14
+ }
15
+ async function readSession(claude) {
16
+ const fs = {
17
+ read: (path) => claude.fs.read(path),
18
+ stat: (path) => claude.fs.stat(path, { resolve: true }),
19
+ exists: (path) => claude.fs.exists(path),
20
+ };
21
+ const home = await claude.env.home();
22
+ if (home === undefined)
23
+ throw new Error('HOME is not set, so ~ in a path has no meaning. Start Claude Code with HOME set.');
24
+ return { home, fs, scope: await findProjectScope(claude.plugin.root, home, fs) };
25
+ }
26
+ export function afterCall(part, contextAfter, failed) {
27
+ const readWorkspace = workspaceReader(part);
28
+ part.on('tool.call', async (e, next) => {
29
+ const before = Promise.all([useOf(part, e), readWorkspace()]);
30
+ await before.catch(() => undefined);
31
+ const result = await next(e);
32
+ if (result.deny !== undefined || result.isError === true)
33
+ return result;
34
+ const added = await before.then(([use, workspace]) => contextAfter(use, workspace)).catch(failed);
35
+ return added.length === 0 ? result : { ...result, context: [...(result.context ?? []), ...added] };
36
+ });
37
+ }
38
+ async function useOf({ toolCalls }, e) {
39
+ return { tool: e.tool, input: toolInputOf(e), ...(await toolCalls.agentOf(e.tool_use_id)) };
40
+ }
41
+ export function modOf(context, { call, folder }, workspace, deadline) {
42
+ return modWithin(context, deadline, async () => workspace.scope?.workTreeOf(call !== undefined && 'path' in call ? call.path : folder));
43
+ }
44
+ export function modWithin({ mod, claude }, deadline, workTree = async () => undefined) {
45
+ const within = (call, task) => beforeDeadline(claude, deadline, call, task);
46
+ const folderOf = async (cwd) => {
47
+ const folder = cwd ?? (await workTree());
48
+ return folder === undefined ? {} : { cwd: folder };
49
+ };
50
+ const overrides = {
51
+ process: {
52
+ async run(argv, init) {
53
+ const folder = await folderOf(init?.cwd);
54
+ const timeoutMs = Math.min(init?.timeoutMs ?? deadline.ms, deadline.longestMs ?? deadline.ms);
55
+ return beforeDeadline(claude, { ...deadline, ms: timeoutMs }, 'mod.process.run', mod.process.run(argv, { ...init, ...folder, timeoutMs }));
56
+ },
57
+ spawn: (argv, init) => spawnWithin(claude, deadline, folderOf(init?.cwd).then((folder) => mod.process.spawn(argv, { ...init, ...folder }))),
58
+ },
59
+ fs: {
60
+ read: (path) => within('mod.fs.read', mod.fs.read(path)),
61
+ write: (path, text) => within('mod.fs.write', mod.fs.write(path, text)),
62
+ list: (path) => within('mod.fs.list', mod.fs.list(path)),
63
+ },
64
+ http: { fetch: (url, init) => within('mod.http.fetch', mod.http.fetch(url, init)) },
65
+ dependencies: dependencyCalls(claude, within),
66
+ };
67
+ return Object.assign(Object.create(mod), overrides);
68
+ }
69
+ function spawnWithin(claude, deadline, started) {
70
+ async function* pieces() {
71
+ const stream = await started;
72
+ let finish = () => undefined;
73
+ const running = beforeDeadline(claude, deadline, 'mod.process.spawn', new Promise((resolve) => (finish = resolve)));
74
+ const passed = running.then(() => new Promise(() => undefined));
75
+ try {
76
+ for (;;) {
77
+ const piece = await Promise.race([stream.next(), passed]);
78
+ if (piece.done === true)
79
+ return piece.value;
80
+ yield piece.value;
81
+ }
82
+ }
83
+ finally {
84
+ finish();
85
+ void stream.return?.();
86
+ }
87
+ }
88
+ const result = started.then((stream) => stream.result);
89
+ result.catch(() => undefined);
90
+ return Object.assign(pieces(), { result });
91
+ }
92
+ export function targetWords(target) {
93
+ const { key, patterns } = targetOf(target);
94
+ const named = listed(patterns);
95
+ if (key === 'command')
96
+ return patterns.includes('*') ? 'every shell command' : named;
97
+ if (key === 'write')
98
+ return `edits to ${named}`;
99
+ if (key === 'read')
100
+ return `reads of ${named}`;
101
+ if (key === 'fetch')
102
+ return `fetches of ${named}`;
103
+ if (key === 'subagent')
104
+ return `the ${named} subagent`;
105
+ return `the ${named} tool`;
106
+ }
@@ -0,0 +1,21 @@
1
+ import { type ToolCall, type ToolUse } from '../../utils/call-effects.js';
2
+ import type { Workspace } from './find-project-scope.js';
3
+ import { type TargetCalls, type TargetKey, type TargetOf } from './match-target.js';
4
+ export type Rule<Mod> = {
5
+ [Key in TargetKey]: TargetOf<Key> & {
6
+ when?: (call: TargetCalls[Key], mod: Mod) => boolean | Promise<boolean>;
7
+ reason?: string;
8
+ };
9
+ }[TargetKey];
10
+ export type PermissionRules<Mod> = {
11
+ deny?: Rule<Mod>[];
12
+ ask?: Rule<Mod>[];
13
+ };
14
+ export type Decision = 'allow' | 'ask' | 'deny';
15
+ export type Verdict = {
16
+ decision: Decision;
17
+ reason?: string;
18
+ };
19
+ export declare function stricterVerdict<Result extends Verdict>(first: Result, second: Result): Result;
20
+ export declare function decidePermission<Mod>(rules: PermissionRules<Mod>, use: ToolUse, workspace: Workspace, modOf: (call: ToolCall, folder: string) => Mod): Promise<Verdict | undefined>;
21
+ //# sourceMappingURL=decide-permission.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"decide-permission.d.ts","sourceRoot":"","sources":["../../src/jobs/permissions/decide-permission.ts"],"names":[],"mappings":"AAEA,OAAO,EAA6C,KAAK,QAAQ,EAAE,KAAK,OAAO,EAAE,MAAM,6BAA6B,CAAA;AACpH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yBAAyB,CAAA;AACxD,OAAO,EAAuE,KAAK,WAAW,EAAE,KAAK,SAAS,EAAE,KAAK,QAAQ,EAAE,MAAM,mBAAmB,CAAA;AAGxJ,MAAM,MAAM,IAAI,CAAC,GAAG,IAAI;KACrB,GAAG,IAAI,SAAS,GAAG,QAAQ,CAAC,GAAG,CAAC,GAAG;QAClC,IAAI,CAAC,EAAE,CAAC,IAAI,EAAE,WAAW,CAAC,GAAG,CAAC,EAAE,GAAG,EAAE,GAAG,KAAK,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAA;QACvE,MAAM,CAAC,EAAE,MAAM,CAAA;KAChB;CACF,CAAC,SAAS,CAAC,CAAA;AAEZ,MAAM,MAAM,eAAe,CAAC,GAAG,IAAI;IAAE,IAAI,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC;IAAC,GAAG,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,EAAE,CAAA;CAAE,CAAA;AAC5E,MAAM,MAAM,QAAQ,GAAG,OAAO,GAAG,KAAK,GAAG,MAAM,CAAA;AAC/C,MAAM,MAAM,OAAO,GAAG;IAAE,QAAQ,EAAE,QAAQ,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,CAAA;AAI7D,wBAAgB,eAAe,CAAC,MAAM,SAAS,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAE7F;AAED,wBAAsB,gBAAgB,CAAC,GAAG,EAAE,KAAK,EAAE,eAAe,CAAC,GAAG,CAAC,EAAE,GAAG,EAAE,OAAO,EAAE,SAAS,EAAE,SAAS,EAAE,KAAK,EAAE,CAAC,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,KAAK,GAAG,GAAG,OAAO,CAAC,OAAO,GAAG,SAAS,CAAC,CAezL"}
@@ -0,0 +1,71 @@
1
+ import { strictness } from '../../runtime/hooks.js';
2
+ import { messageOf } from '../../utils/text.js';
3
+ import { callBaseOf, callEffects } from '../../utils/call-effects.js';
4
+ import { folderInScope, matchTarget, targetOf } from './match-target.js';
5
+ import { resolvePath } from '../../utils/paths.js';
6
+ const wordBoundary = '\\s;&|()<>\'"`=';
7
+ export function stricterVerdict(first, second) {
8
+ return strictness.indexOf(second.decision) > strictness.indexOf(first.decision) ? second : first;
9
+ }
10
+ export async function decidePermission(rules, use, workspace, modOf) {
11
+ try {
12
+ const effects = callEffects(use, workspace);
13
+ for (const decision of ['deny', 'ask']) {
14
+ for (const rule of rules[decision] ?? []) {
15
+ const outcome = await ruleOutcome(rule, use, effects, workspace, modOf);
16
+ if (outcome === undefined)
17
+ continue;
18
+ const reason = [rule.reason, outcome.failure === undefined ? undefined : `Its when check failed: ${outcome.failure}`].filter((part) => part !== undefined).join(' ');
19
+ return reason === '' ? { decision } : { decision, reason };
20
+ }
21
+ }
22
+ return undefined;
23
+ }
24
+ catch (error) {
25
+ return { decision: 'deny', reason: `The permissions job failed, so it denies the call: ${messageOf(error)}` };
26
+ }
27
+ }
28
+ async function ruleOutcome(rule, use, effects, workspace, modOf) {
29
+ const matched = await matchTarget(rule, use, effects, workspace);
30
+ const calls = matched.length > 0 ? matched : await unparsedMatches(rule, use, effects, workspace);
31
+ if (calls.length === 0)
32
+ return undefined;
33
+ const { when } = rule;
34
+ if (when === undefined)
35
+ return { failure: undefined };
36
+ for (const { call, access, folder } of calls) {
37
+ const fullCall = access?.contents === undefined ? call : { ...call, ...(await access.contents()) };
38
+ try {
39
+ if (await when(fullCall, modOf(fullCall, folder)))
40
+ return { failure: undefined };
41
+ }
42
+ catch (error) {
43
+ return { failure: messageOf(error) };
44
+ }
45
+ }
46
+ return undefined;
47
+ }
48
+ async function unparsedMatches(target, use, effects, workspace) {
49
+ const shell = effects.shell;
50
+ if (shell === undefined || shell.isFullyParsed)
51
+ return [];
52
+ const { key, patterns } = targetOf(target);
53
+ const base = callBaseOf(use);
54
+ if (key === 'command') {
55
+ const isMatched = patterns.some((pattern) => pattern.split(/\s+/).filter((word) => word !== '').every((word) => containsWord(shell.line, word)));
56
+ const folder = isMatched ? await folderInScope(shell, workspace) : undefined;
57
+ return folder === undefined ? [] : [{ call: { ...base, commands: shell.commands.map((command) => command.argv), isFullyParsed: false }, access: undefined, folder }];
58
+ }
59
+ if (key !== 'read' && key !== 'write')
60
+ return [];
61
+ const line = shell.line.toLowerCase();
62
+ const named = patterns.map((pattern) => pattern.slice(pattern.lastIndexOf('/') + 1)).filter((segment) => segment !== '' && !/[*?[\]{}]/.test(segment) && line.includes(segment.toLowerCase()));
63
+ const folder = named.length === 0 ? undefined : await folderInScope(shell, workspace);
64
+ if (folder === undefined)
65
+ return [];
66
+ return named.map((segment) => ({ call: { ...base, path: resolvePath(segment, folder) }, access: undefined, folder }));
67
+ }
68
+ function containsWord(line, word) {
69
+ const escaped = word.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
70
+ return new RegExp(`(^|[${wordBoundary}/])${escaped}(?=$|[${wordBoundary}])`).test(line);
71
+ }
@@ -0,0 +1,13 @@
1
+ import { type FileSystem } from '../../utils/paths.js';
2
+ export type ProjectScope = {
3
+ workTreeOf: (path: string) => Promise<string | undefined>;
4
+ };
5
+ export type Workspace = {
6
+ projectRoot: string;
7
+ cwd: string;
8
+ home: string;
9
+ fs: FileSystem;
10
+ scope: ProjectScope | undefined;
11
+ };
12
+ export declare function findProjectScope(pluginRoot: string, home: string, fs: FileSystem): Promise<ProjectScope | undefined>;
13
+ //# sourceMappingURL=find-project-scope.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"find-project-scope.d.ts","sourceRoot":"","sources":["../../src/jobs/permissions/find-project-scope.ts"],"names":[],"mappings":"AAAA,OAAO,EAAiC,KAAK,UAAU,EAAE,MAAM,sBAAsB,CAAA;AAErF,MAAM,MAAM,YAAY,GAAG;IAAE,UAAU,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAAA;CAAE,CAAA;AAExF,MAAM,MAAM,SAAS,GAAG;IAAE,WAAW,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,UAAU,CAAC;IAAC,KAAK,EAAE,YAAY,GAAG,SAAS,CAAA;CAAE,CAAA;AAE3H,wBAAsB,gBAAgB,CAAC,UAAU,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,UAAU,GAAG,OAAO,CAAC,YAAY,GAAG,SAAS,CAAC,CAwB1H"}
@@ -0,0 +1,51 @@
1
+ import { nameOf, parentOf, resolvePath } from '../../utils/paths.js';
2
+ export async function findProjectScope(pluginRoot, home, fs) {
3
+ const skills = parentOf(pluginRoot);
4
+ const claude = parentOf(skills);
5
+ const root = parentOf(claude);
6
+ if (nameOf(skills) !== 'skills' || nameOf(claude) !== '.claude' || root === resolvePath(home, '/'))
7
+ return undefined;
8
+ const repositories = new Map();
9
+ const repositoryOf = async (workTree) => {
10
+ const known = repositories.get(workTree);
11
+ if (known !== undefined)
12
+ return known;
13
+ const repository = await readRepository(workTree, fs);
14
+ if (repository !== undefined)
15
+ repositories.set(workTree, repository);
16
+ return repository;
17
+ };
18
+ const project = await repositoryOf(root);
19
+ if (project === undefined)
20
+ return undefined;
21
+ return {
22
+ async workTreeOf(path) {
23
+ for (let folder = path;; folder = parentOf(folder)) {
24
+ const repository = await repositoryOf(folder);
25
+ if (repository !== undefined)
26
+ return repository === project ? folder : undefined;
27
+ if (folder === '/')
28
+ return undefined;
29
+ }
30
+ },
31
+ };
32
+ }
33
+ async function readRepository(workTree, fs) {
34
+ const gitPath = resolvePath('.git', workTree);
35
+ const git = await fs.stat(gitPath).catch(() => undefined);
36
+ if (git === undefined)
37
+ return undefined;
38
+ if (git.kind === 'dir')
39
+ return git.realPath ?? gitPath;
40
+ try {
41
+ const gitDirectory = /^gitdir:\s*(.+)$/m.exec(await fs.read(gitPath))?.[1]?.trim();
42
+ if (gitDirectory === undefined)
43
+ throw new Error(`${gitPath} has no gitdir line.`);
44
+ const linkedDirectory = resolvePath(gitDirectory, workTree);
45
+ const commonDirectory = await fs.read(resolvePath('commondir', linkedDirectory)).then((text) => resolvePath(text.trim(), linkedDirectory), () => linkedDirectory);
46
+ return (await fs.stat(commonDirectory)).realPath ?? commonDirectory;
47
+ }
48
+ catch {
49
+ return workTree;
50
+ }
51
+ }
@@ -0,0 +1,27 @@
1
+ import { type CallBase, type CallEffects, type CommandCall, type FetchCall, type FileAccess, type FileCall, type ToolCall, type ToolUse } from '../../utils/call-effects.js';
2
+ import type { Workspace } from './find-project-scope.js';
3
+ export type TargetCalls = {
4
+ command: CommandCall;
5
+ read: FileCall;
6
+ write: FileCall;
7
+ fetch: FetchCall;
8
+ subagent: CallBase;
9
+ tool: CallBase;
10
+ };
11
+ export type TargetKey = keyof TargetCalls;
12
+ export type TargetOf<Key extends TargetKey> = Record<Key, string | string[]> & Partial<Record<Exclude<TargetKey, Key>, never>>;
13
+ export type Target = {
14
+ [Key in TargetKey]: TargetOf<Key>;
15
+ }[TargetKey];
16
+ export type MatchedCall = {
17
+ call: ToolCall;
18
+ access: FileAccess | undefined;
19
+ folder: string;
20
+ };
21
+ export declare function targetOf(target: Target): {
22
+ key: TargetKey;
23
+ patterns: string[];
24
+ };
25
+ export declare function matchTarget(target: Target, use: ToolUse, effects: CallEffects, workspace: Workspace): Promise<MatchedCall[]>;
26
+ export declare function folderInScope(shell: NonNullable<CallEffects['shell']>, workspace: Workspace): Promise<string | undefined>;
27
+ //# sourceMappingURL=match-target.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"match-target.d.ts","sourceRoot":"","sources":["../../src/jobs/permissions/match-target.ts"],"names":[],"mappings":"AAEA,OAAO,EAAc,KAAK,QAAQ,EAAE,KAAK,WAAW,EAAE,KAAK,WAAW,EAAE,KAAK,SAAS,EAAE,KAAK,UAAU,EAAE,KAAK,QAAQ,EAAE,KAAK,QAAQ,EAAE,KAAK,OAAO,EAAE,MAAM,6BAA6B,CAAA;AACxL,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yBAAyB,CAAA;AAGxD,MAAM,MAAM,WAAW,GAAG;IAAE,OAAO,EAAE,WAAW,CAAC;IAAC,IAAI,EAAE,QAAQ,CAAC;IAAC,KAAK,EAAE,QAAQ,CAAC;IAAC,KAAK,EAAE,SAAS,CAAC;IAAC,QAAQ,EAAE,QAAQ,CAAC;IAAC,IAAI,EAAE,QAAQ,CAAA;CAAE,CAAA;AACzI,MAAM,MAAM,SAAS,GAAG,MAAM,WAAW,CAAA;AACzC,MAAM,MAAM,QAAQ,CAAC,GAAG,SAAS,SAAS,IAAI,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC,GAAG,OAAO,CAAC,MAAM,CAAC,OAAO,CAAC,SAAS,EAAE,GAAG,CAAC,EAAE,KAAK,CAAC,CAAC,CAAA;AAC9H,MAAM,MAAM,MAAM,GAAG;KAAG,GAAG,IAAI,SAAS,GAAG,QAAQ,CAAC,GAAG,CAAC;CAAE,CAAC,SAAS,CAAC,CAAA;AACrE,MAAM,MAAM,WAAW,GAAG;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,MAAM,EAAE,UAAU,GAAG,SAAS,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAA;AAI5F,wBAAgB,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG;IAAE,GAAG,EAAE,SAAS,CAAC;IAAC,QAAQ,EAAE,MAAM,EAAE,CAAA;CAAE,CAQ/E;AAED,wBAAsB,WAAW,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,OAAO,EAAE,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,SAAS,GAAG,OAAO,CAAC,WAAW,EAAE,CAAC,CA6BlI;AA4BD,wBAAsB,aAAa,CAAC,KAAK,EAAE,WAAW,CAAC,WAAW,CAAC,OAAO,CAAC,CAAC,EAAE,SAAS,EAAE,SAAS,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAI/H"}