@cmodjs/core 0.1.14 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/docs/commands.md +31 -19
- package/docs/index.md +12 -4
- package/docs/install-steps.md +30 -4
- package/docs/jobs.md +25 -9
- package/docs/mod.md +103 -37
- package/docs/options.md +93 -0
- package/docs/permissions.md +88 -0
- package/docs/state.md +4 -31
- package/docs/testing.md +13 -2
- package/installer.d.ts +10 -0
- package/installer.d.ts.map +1 -0
- package/installer.js +8 -0
- package/jobs/context.d.ts +2 -2
- package/jobs/context.d.ts.map +1 -1
- package/jobs/context.js +5 -3
- package/jobs/permissions.js +1 -1
- package/jobs/program.d.ts.map +1 -1
- package/jobs/program.js +5 -1
- package/jobs/prompt.js +5 -5
- package/jobs/schedule.d.ts.map +1 -1
- package/jobs/schedule.js +2 -1
- package/jobs/slash-command.d.ts.map +1 -1
- package/jobs/slash-command.js +3 -2
- package/jobs/status-line.d.ts.map +1 -1
- package/jobs/status-line.js +3 -2
- package/jobs/tool.d.ts.map +1 -1
- package/jobs/tool.js +3 -2
- package/mod.d.ts +48 -10
- package/mod.d.ts.map +1 -1
- package/mod.js +3 -0
- package/options.d.ts +58 -0
- package/options.d.ts.map +1 -0
- package/options.js +66 -0
- package/package.json +1 -1
- package/records.d.ts +14 -1
- package/records.d.ts.map +1 -1
- package/records.js +106 -7
- package/register.d.ts +4 -2
- package/register.d.ts.map +1 -1
- package/register.js +8 -2
- package/runtime/claude.d.ts +5 -1
- package/runtime/claude.d.ts.map +1 -1
- package/runtime/grants.d.ts +28 -0
- package/runtime/grants.d.ts.map +1 -0
- package/runtime/grants.js +286 -0
- package/runtime/installer.d.ts +23 -0
- package/runtime/installer.d.ts.map +1 -0
- package/runtime/installer.js +113 -0
- package/runtime/lifecycle.d.ts +6 -3
- package/runtime/lifecycle.d.ts.map +1 -1
- package/runtime/lifecycle.js +220 -36
- package/runtime/metadata.d.ts +20 -0
- package/runtime/metadata.d.ts.map +1 -0
- package/runtime/metadata.js +143 -0
- package/runtime/options.d.ts +21 -0
- package/runtime/options.d.ts.map +1 -0
- package/runtime/options.js +100 -0
- package/runtime/state.d.ts +1 -0
- package/runtime/state.d.ts.map +1 -1
- package/runtime/state.js +5 -75
- package/src/installer.ts +17 -0
- package/src/jobs/context.ts +5 -3
- package/src/jobs/permissions.ts +1 -1
- package/src/jobs/program.ts +4 -1
- package/src/jobs/prompt.ts +5 -5
- package/src/jobs/schedule.ts +2 -1
- package/src/jobs/slash-command.ts +3 -2
- package/src/jobs/status-line.ts +3 -2
- package/src/jobs/tool.ts +3 -2
- package/src/mod.ts +46 -11
- package/src/options.ts +91 -0
- package/src/records.ts +117 -8
- package/src/register.ts +12 -3
- package/src/runtime/claude.ts +2 -0
- package/src/runtime/grants.ts +306 -0
- package/src/runtime/installer.ts +163 -0
- package/src/runtime/lifecycle.ts +220 -45
- package/src/runtime/metadata.ts +153 -0
- package/src/runtime/options.ts +102 -0
- package/src/runtime/state.ts +7 -70
- package/src/testing/fake-claude.ts +6 -1
- package/src/testing/fake-files.ts +9 -3
- package/src/testing.ts +40 -15
- package/src/utils/metadata.ts +242 -0
- package/testing/fake-claude.d.ts +4 -0
- package/testing/fake-claude.d.ts.map +1 -1
- package/testing/fake-claude.js +5 -1
- package/testing/fake-files.d.ts.map +1 -1
- package/testing/fake-files.js +9 -3
- package/testing.d.ts +6 -2
- package/testing.d.ts.map +1 -1
- package/testing.js +29 -13
- package/utils/metadata.d.ts +21 -0
- package/utils/metadata.d.ts.map +1 -0
- package/utils/metadata.js +250 -0
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { Claude } from './claude.js';
|
|
2
|
+
import type { RoutedEvent } from './hooks.js';
|
|
3
|
+
export type Grants = {
|
|
4
|
+
readonly name: string;
|
|
5
|
+
readonly declared: readonly string[];
|
|
6
|
+
readonly granted: () => ReadonlySet<string>;
|
|
7
|
+
readonly refresh: () => Promise<void>;
|
|
8
|
+
readonly home: string | undefined;
|
|
9
|
+
readonly configRoot: string | undefined;
|
|
10
|
+
readonly projectRoot: () => string;
|
|
11
|
+
readonly freeFolders: () => readonly string[];
|
|
12
|
+
};
|
|
13
|
+
export declare class PermissionRefused extends Error {
|
|
14
|
+
}
|
|
15
|
+
export declare function checkWrite(grants: Grants, call: string, path: string): Promise<void>;
|
|
16
|
+
export declare function checkingGrants(claude: Claude, grants: Grants): Claude;
|
|
17
|
+
export declare function itemOf(name: string, value?: string): string;
|
|
18
|
+
export declare function isCovered(granted: ReadonlySet<string>, item: string, home: string | undefined): boolean;
|
|
19
|
+
export declare function refusal(grants: Pick<Grants, 'name' | 'declared' | 'home'>, call: string, item: string): string;
|
|
20
|
+
export type AnswerCheck = {
|
|
21
|
+
readonly plugin: string;
|
|
22
|
+
isGranted(item: string): boolean;
|
|
23
|
+
dropped(item: string, part: string): void;
|
|
24
|
+
};
|
|
25
|
+
export declare function checksAnswers(event: RoutedEvent): boolean;
|
|
26
|
+
export declare function passedDown(event: RoutedEvent, original: unknown, passed: unknown, check: AnswerCheck): unknown;
|
|
27
|
+
export declare function checkedAnswer(event: RoutedEvent, e: unknown, answer: unknown, below: () => Promise<unknown>, check: AnswerCheck): Promise<unknown>;
|
|
28
|
+
//# sourceMappingURL=grants.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"grants.d.ts","sourceRoot":"","sources":["../src/runtime/grants.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AACzC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAA;AAE7C,MAAM,MAAM,MAAM,GAAG;IACnB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,QAAQ,EAAE,SAAS,MAAM,EAAE,CAAA;IACpC,QAAQ,CAAC,OAAO,EAAE,MAAM,WAAW,CAAC,MAAM,CAAC,CAAA;IAC3C,QAAQ,CAAC,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;IACrC,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;IAClC,QAAQ,CAAC,WAAW,EAAE,MAAM,SAAS,MAAM,EAAE,CAAA;CAC9C,CAAA;AAED,qBAAa,iBAAkB,SAAQ,KAAK;CAAG;AAc/C,wBAAgB,UAAU,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAEpF;AAED,wBAAgB,cAAc,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CA6ErE;AAmBD,wBAAgB,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,CAE3D;AAED,wBAAgB,SAAS,CAAC,OAAO,EAAE,WAAW,CAAC,MAAM,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,CAUvG;AAcD,wBAAgB,OAAO,CAAC,MAAM,EAAE,IAAI,CAAC,MAAM,EAAE,MAAM,GAAG,UAAU,GAAG,MAAM,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAI9G;AA4BD,MAAM,MAAM,WAAW,GAAG;IACxB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAA;IAChC,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI,CAAA;CAC1C,CAAA;AAED,wBAAgB,aAAa,CAAC,KAAK,EAAE,WAAW,GAAG,OAAO,CAEzD;AAED,wBAAgB,UAAU,CAAC,KAAK,EAAE,WAAW,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,KAAK,EAAE,WAAW,GAAG,OAAO,CAa9G;AAED,wBAAsB,aAAa,CAAC,KAAK,EAAE,WAAW,EAAE,CAAC,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,OAAO,CAAC,OAAO,CAAC,EAAE,KAAK,EAAE,WAAW,GAAG,OAAO,CAAC,OAAO,CAAC,CA+BxJ"}
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
import { permissionWords } from '../records.js';
|
|
2
|
+
import { relativePath } from '../utils/paths.js';
|
|
3
|
+
export class PermissionRefused extends Error {
|
|
4
|
+
}
|
|
5
|
+
const configFiles = ['.claude/settings.json', '.claude/settings.local.json', '.mcp.json'];
|
|
6
|
+
const startsTurns = ['CronCreate'];
|
|
7
|
+
const ownSchedule = ['CronDelete', 'CronList'];
|
|
8
|
+
async function checkGrant(grants, call, item) {
|
|
9
|
+
if (item === undefined)
|
|
10
|
+
return;
|
|
11
|
+
await grants.refresh();
|
|
12
|
+
if (!isCovered(grants.granted(), item, grants.home))
|
|
13
|
+
throw new PermissionRefused(refusal(grants, call, item));
|
|
14
|
+
}
|
|
15
|
+
export function checkWrite(grants, call, path) {
|
|
16
|
+
return checkGrant(grants, call, writeItem(grants, path));
|
|
17
|
+
}
|
|
18
|
+
export function checkingGrants(claude, grants) {
|
|
19
|
+
const gate = (call, item) => checkGrant(grants, call, item);
|
|
20
|
+
return {
|
|
21
|
+
...claude,
|
|
22
|
+
process: {
|
|
23
|
+
async run(argv, init) {
|
|
24
|
+
await gate(`process.run(${argv.join(' ')})`, `run:${programOf(argv)}`);
|
|
25
|
+
return claude.process.run(argv, init);
|
|
26
|
+
},
|
|
27
|
+
spawn(request) {
|
|
28
|
+
const started = gate(`process.spawn(${request.argv.join(' ')})`, `run:${programOf(request.argv)}`).then(() => claude.process.spawn(request));
|
|
29
|
+
return spawned(started);
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
fs: {
|
|
33
|
+
...claude.fs,
|
|
34
|
+
async read(path) {
|
|
35
|
+
await gate(`fs.read(${path})`, readItem(grants, path));
|
|
36
|
+
return claude.fs.read(path);
|
|
37
|
+
},
|
|
38
|
+
async write(path, text) {
|
|
39
|
+
await gate(`fs.write(${path})`, writeItem(grants, path));
|
|
40
|
+
return claude.fs.write(path, text);
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
http: {
|
|
44
|
+
async fetch(url, init) {
|
|
45
|
+
await gate(`http.fetch(${url})`, `network:${init?.socketPath ?? hostOf(url)}`);
|
|
46
|
+
return claude.http.fetch(url, init);
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
config: {
|
|
50
|
+
...claude.config,
|
|
51
|
+
set: (async (args) => {
|
|
52
|
+
await gate('config.set', 'config');
|
|
53
|
+
return claude.config.set(args);
|
|
54
|
+
}),
|
|
55
|
+
},
|
|
56
|
+
session: {
|
|
57
|
+
...claude.session,
|
|
58
|
+
messages: (async (...args) => {
|
|
59
|
+
await gate('session.messages', 'conversation');
|
|
60
|
+
return claude.session.messages(...args);
|
|
61
|
+
}),
|
|
62
|
+
append: (async (args) => {
|
|
63
|
+
await gate('session.append', 'prompt');
|
|
64
|
+
return claude.session.append(args);
|
|
65
|
+
}),
|
|
66
|
+
},
|
|
67
|
+
prompt: {
|
|
68
|
+
submit: (async (args) => {
|
|
69
|
+
await gate('session.submit', 'prompt');
|
|
70
|
+
return claude.prompt.submit(args);
|
|
71
|
+
}),
|
|
72
|
+
},
|
|
73
|
+
model: {
|
|
74
|
+
complete: (async (...args) => {
|
|
75
|
+
await gate('model.complete', 'model');
|
|
76
|
+
return claude.model.complete(...args);
|
|
77
|
+
}),
|
|
78
|
+
},
|
|
79
|
+
agent: {
|
|
80
|
+
...claude.agent,
|
|
81
|
+
async spawn(args) {
|
|
82
|
+
await gate('agent.spawn', 'agents');
|
|
83
|
+
return claude.agent.spawn(args);
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
tool: {
|
|
87
|
+
...claude.tool,
|
|
88
|
+
call: (async (input) => {
|
|
89
|
+
const item = startsTurns.includes(input.tool) ? 'prompt' : ownSchedule.includes(input.tool) ? undefined : 'tools';
|
|
90
|
+
await gate(`tool.call(${input.tool})`, item);
|
|
91
|
+
return claude.tool.call(input);
|
|
92
|
+
}),
|
|
93
|
+
},
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
function spawned(started) {
|
|
97
|
+
const result = started.then((stream) => stream.result);
|
|
98
|
+
result.catch(() => undefined);
|
|
99
|
+
const stream = {
|
|
100
|
+
next: async () => (await started).next(),
|
|
101
|
+
async return(value) {
|
|
102
|
+
const running = await started.catch(() => undefined);
|
|
103
|
+
return (await running?.return?.(value)) ?? { done: true, value };
|
|
104
|
+
},
|
|
105
|
+
[Symbol.asyncIterator]: () => stream,
|
|
106
|
+
result,
|
|
107
|
+
};
|
|
108
|
+
return stream;
|
|
109
|
+
}
|
|
110
|
+
export function itemOf(name, value) {
|
|
111
|
+
return value === undefined ? name : `${name}:${value}`;
|
|
112
|
+
}
|
|
113
|
+
export function isCovered(granted, item, home) {
|
|
114
|
+
if (granted.has(item))
|
|
115
|
+
return true;
|
|
116
|
+
const split = item.indexOf(':');
|
|
117
|
+
if (split === -1)
|
|
118
|
+
return false;
|
|
119
|
+
const name = item.slice(0, split);
|
|
120
|
+
const target = expanded(item.slice(split + 1), home);
|
|
121
|
+
const values = [...granted].filter((each) => each.startsWith(`${name}:`)).map((each) => expanded(each.slice(name.length + 1), home));
|
|
122
|
+
if (name === 'run')
|
|
123
|
+
return values.includes('*') || values.includes(target);
|
|
124
|
+
if (name === 'files')
|
|
125
|
+
return values.some((each) => relativePath(each, target) !== undefined);
|
|
126
|
+
return values.includes(target);
|
|
127
|
+
}
|
|
128
|
+
function readItem(grants, path) {
|
|
129
|
+
if (grants.configRoot === undefined)
|
|
130
|
+
return undefined;
|
|
131
|
+
return relativePath(`${grants.configRoot}/projects`, path) === undefined ? undefined : 'conversation';
|
|
132
|
+
}
|
|
133
|
+
function writeItem(grants, path) {
|
|
134
|
+
const root = grants.projectRoot();
|
|
135
|
+
if (configFiles.some((file) => `${root}/${file}` === path))
|
|
136
|
+
return 'config';
|
|
137
|
+
if (grants.freeFolders().some((folder) => relativePath(folder, path) !== undefined))
|
|
138
|
+
return undefined;
|
|
139
|
+
return `files:${path}`;
|
|
140
|
+
}
|
|
141
|
+
export function refusal(grants, call, item) {
|
|
142
|
+
const { name } = grants;
|
|
143
|
+
if (isCovered(new Set(grants.declared), item, grants.home))
|
|
144
|
+
return `${name} calls ${call} without your grant to "${permissionWords(item)}". Turn it on in /mods ${name}.`;
|
|
145
|
+
return `${name} calls ${call}, which needs ${declaration(item, grants.home)} in package.json "cmod".`;
|
|
146
|
+
}
|
|
147
|
+
function declaration(item, home) {
|
|
148
|
+
const split = item.indexOf(':');
|
|
149
|
+
if (split === -1)
|
|
150
|
+
return `"permissions": { "${item}": true }`;
|
|
151
|
+
const name = item.slice(0, split);
|
|
152
|
+
const target = item.slice(split + 1);
|
|
153
|
+
const fromHome = home === undefined ? undefined : relativePath(home, target);
|
|
154
|
+
const shown = name !== 'run' && fromHome !== undefined ? `~/${fromHome}` : target;
|
|
155
|
+
return `"permissions": { "${name}": ["${shown}"] }`;
|
|
156
|
+
}
|
|
157
|
+
const classicParts = {
|
|
158
|
+
additionalContext: 'prompt',
|
|
159
|
+
initialUserMessage: 'prompt',
|
|
160
|
+
suppressOriginalPrompt: 'prompt',
|
|
161
|
+
updatedToolOutput: 'prompt',
|
|
162
|
+
updatedMCPToolOutput: 'prompt',
|
|
163
|
+
updatedInput: 'tools',
|
|
164
|
+
retry: 'approve',
|
|
165
|
+
};
|
|
166
|
+
const blocksKeepWorking = ['classic.Stop', 'classic.PostToolUse'];
|
|
167
|
+
const reservedToolKeys = ['tool', 'tool_use_id'];
|
|
168
|
+
export function checksAnswers(event) {
|
|
169
|
+
return event.startsWith('classic.') || event === 'tool.check' || event === 'tool.call' || event === 'prompt.context' || event === 'prompt.submit';
|
|
170
|
+
}
|
|
171
|
+
export function passedDown(event, original, passed, check) {
|
|
172
|
+
if (passed === original)
|
|
173
|
+
return passed;
|
|
174
|
+
if (event === 'tool.call') {
|
|
175
|
+
if (sameExcept(original, passed, reservedToolKeys) || check.isGranted('tools'))
|
|
176
|
+
return passed;
|
|
177
|
+
check.dropped('tools', 'a changed tool input');
|
|
178
|
+
return original;
|
|
179
|
+
}
|
|
180
|
+
if (event === 'prompt.context' || event === 'prompt.submit') {
|
|
181
|
+
if (same(original, passed) || check.isGranted('prompt'))
|
|
182
|
+
return passed;
|
|
183
|
+
check.dropped('prompt', event === 'prompt.submit' ? 'a rewritten prompt' : 'changed context');
|
|
184
|
+
return original;
|
|
185
|
+
}
|
|
186
|
+
return passed;
|
|
187
|
+
}
|
|
188
|
+
export async function checkedAnswer(event, e, answer, below, check) {
|
|
189
|
+
if (!isFields(answer))
|
|
190
|
+
return answer;
|
|
191
|
+
if (event === 'tool.check')
|
|
192
|
+
return decided(answer['decision'], answer, below, check);
|
|
193
|
+
if (event === 'classic.PermissionRequest') {
|
|
194
|
+
const decision = isFields(answer['decision']) ? answer['decision']['behavior'] : undefined;
|
|
195
|
+
return decided(decision, answer, below, check);
|
|
196
|
+
}
|
|
197
|
+
if (event.startsWith('classic.'))
|
|
198
|
+
return classicAnswer(event, answer, below, check);
|
|
199
|
+
if (event === 'tool.call') {
|
|
200
|
+
if (answer['deny'] !== undefined || check.isGranted('prompt'))
|
|
201
|
+
return answer;
|
|
202
|
+
const tool = isFields(e) ? e['tool'] : undefined;
|
|
203
|
+
if (typeof tool === 'string' && tool.startsWith(`mcp__${check.plugin}__`)) {
|
|
204
|
+
if (answer['context'] === undefined)
|
|
205
|
+
return answer;
|
|
206
|
+
check.dropped('prompt', 'context after a tool call');
|
|
207
|
+
const { context: _context, ...rest } = answer;
|
|
208
|
+
return rest;
|
|
209
|
+
}
|
|
210
|
+
const theirs = await below();
|
|
211
|
+
if (same(answer, theirs))
|
|
212
|
+
return answer;
|
|
213
|
+
check.dropped('prompt', same(contextOf(answer), contextOf(theirs)) ? 'a changed tool result' : 'context after a tool call');
|
|
214
|
+
return theirs;
|
|
215
|
+
}
|
|
216
|
+
if (event === 'prompt.submit' && answer['drop'] !== undefined)
|
|
217
|
+
return answer;
|
|
218
|
+
if (event === 'prompt.context' || event === 'prompt.submit') {
|
|
219
|
+
if (check.isGranted('prompt'))
|
|
220
|
+
return answer;
|
|
221
|
+
const theirs = await below();
|
|
222
|
+
if (same(answer, theirs))
|
|
223
|
+
return answer;
|
|
224
|
+
check.dropped('prompt', event === 'prompt.submit' ? 'a rewritten prompt' : 'changed context');
|
|
225
|
+
return theirs;
|
|
226
|
+
}
|
|
227
|
+
return answer;
|
|
228
|
+
}
|
|
229
|
+
async function decided(decision, answer, below, check) {
|
|
230
|
+
if ((decision !== 'allow' && decision !== 'ask') || check.isGranted('approve'))
|
|
231
|
+
return answer;
|
|
232
|
+
const theirs = await below();
|
|
233
|
+
if (same(answer, theirs))
|
|
234
|
+
return answer;
|
|
235
|
+
check.dropped('approve', `${decision} on a tool call`);
|
|
236
|
+
return theirs;
|
|
237
|
+
}
|
|
238
|
+
async function classicAnswer(event, answer, below, check) {
|
|
239
|
+
const blocks = blocksKeepWorking.includes(event) ? [['block', 'prompt']] : [];
|
|
240
|
+
const parts = [...Object.entries(classicParts), ...blocks].filter(([part, item]) => answer[part] !== undefined && !check.isGranted(item));
|
|
241
|
+
if (parts.length === 0)
|
|
242
|
+
return answer;
|
|
243
|
+
const theirs = await below();
|
|
244
|
+
const kept = { ...answer };
|
|
245
|
+
const theirFields = isFields(theirs) ? theirs : {};
|
|
246
|
+
for (const [part, item] of parts) {
|
|
247
|
+
if (same(answer[part], theirFields[part]))
|
|
248
|
+
continue;
|
|
249
|
+
check.dropped(item, part);
|
|
250
|
+
if (theirFields[part] === undefined)
|
|
251
|
+
delete kept[part];
|
|
252
|
+
else
|
|
253
|
+
kept[part] = theirFields[part];
|
|
254
|
+
}
|
|
255
|
+
return kept;
|
|
256
|
+
}
|
|
257
|
+
function contextOf(answer) {
|
|
258
|
+
return isFields(answer) ? answer['context'] : undefined;
|
|
259
|
+
}
|
|
260
|
+
function same(left, right) {
|
|
261
|
+
return left === right || JSON.stringify(left) === JSON.stringify(right);
|
|
262
|
+
}
|
|
263
|
+
function sameExcept(left, right, keys) {
|
|
264
|
+
if (!isFields(left) || !isFields(right))
|
|
265
|
+
return same(left, right);
|
|
266
|
+
const without = (fields) => Object.fromEntries(Object.entries(fields).filter(([key]) => !keys.includes(key)));
|
|
267
|
+
return same(without(left), without(right));
|
|
268
|
+
}
|
|
269
|
+
function isFields(value) {
|
|
270
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
271
|
+
}
|
|
272
|
+
function programOf(argv) {
|
|
273
|
+
const command = argv[0] ?? '';
|
|
274
|
+
return command.slice(command.lastIndexOf('/') + 1);
|
|
275
|
+
}
|
|
276
|
+
function hostOf(url) {
|
|
277
|
+
try {
|
|
278
|
+
return new URL(url).hostname;
|
|
279
|
+
}
|
|
280
|
+
catch {
|
|
281
|
+
return url;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
function expanded(path, home) {
|
|
285
|
+
return path.startsWith('~/') && home !== undefined ? `${home}${path.slice(1)}` : path;
|
|
286
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { Args, Frozen, RenderElement } from 'claude-code';
|
|
2
|
+
import type { Step } from '../installer.js';
|
|
3
|
+
import type { Mod } from '../mod.js';
|
|
4
|
+
import type { Option } from '../options.js';
|
|
5
|
+
import type { Claude } from './claude.js';
|
|
6
|
+
export declare const installerPaneId = "cmod-installer";
|
|
7
|
+
export type Consent = {
|
|
8
|
+
readonly install: string;
|
|
9
|
+
readonly uninstall: string;
|
|
10
|
+
readonly keys: string;
|
|
11
|
+
readonly permissions: readonly string[];
|
|
12
|
+
};
|
|
13
|
+
export type Installer = {
|
|
14
|
+
draws(e: Frozen<Args<'ui.render'>>): boolean;
|
|
15
|
+
draw(e: Frozen<Args<'ui.render'>>): RenderElement;
|
|
16
|
+
closed(id: string): void;
|
|
17
|
+
consent(consent: Consent): Promise<boolean>;
|
|
18
|
+
options(missing: readonly (readonly [string, Option])[], save: (key: string, value: string) => Promise<string | undefined>): Promise<boolean>;
|
|
19
|
+
step<State extends object>(mod: Mod<State>, step: Step<State>, index: number, total: number): Promise<boolean>;
|
|
20
|
+
close(): Promise<void>;
|
|
21
|
+
};
|
|
22
|
+
export declare function createInstaller(name: string, claude: () => Claude): Installer;
|
|
23
|
+
//# sourceMappingURL=installer.d.ts.map
|
|
@@ -0,0 +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;CACxC,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,CAiI7E"}
|
|
@@ -0,0 +1,113 @@
|
|
|
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 }) {
|
|
40
|
+
const changes = [...(install === '' ? [] : [`Run ${install} now${uninstall === '' ? '' : `, and ${uninstall} when you remove it`}`]), ...(install === '' && uninstall !== '' ? [`Run ${uninstall} when you remove it`] : []), ...(keys === '' ? [] : [`Bind ${keys}`])];
|
|
41
|
+
return show((answer) => ({
|
|
42
|
+
draw: () => frame(`${name} wants to:`, [
|
|
43
|
+
...(permissions.length === 0 ? [] : [Box({ flexDirection: 'column', children: permissions.map((item) => bullet(permissionWords(item))) })]),
|
|
44
|
+
...(changes.length === 0 ? [] : [Text({ children: permissions.length === 0 ? 'Change your computer:' : 'and change your computer:' }), Box({ flexDirection: 'column', children: changes.map(bullet) })]),
|
|
45
|
+
Text({ dimColor: true, children: 'You can turn each permission off later in /mods.' }),
|
|
46
|
+
], [Button({ key: 'not-now', hotkey: 'n', label: 'Not now', onPress: () => answer(false) }), Button({ key: 'accept', hotkey: 'a', label: 'Accept', onPress: () => answer(true) })]),
|
|
47
|
+
}), false);
|
|
48
|
+
},
|
|
49
|
+
options(missing, save) {
|
|
50
|
+
const saved = new Map();
|
|
51
|
+
const failures = new Map();
|
|
52
|
+
return show((answer) => ({
|
|
53
|
+
draw: () => frame(`${name} needs ${missing.length === 1 ? 'one setting' : `${missing.length} settings`}:`, missing.map(([key, option]) => {
|
|
54
|
+
const label = Text({ children: [Text({ bold: true, children: option.title }), option.description === '' ? '' : Text({ dimColor: true, children: ` ${option.description}` })] });
|
|
55
|
+
if (option.kind === 'secret')
|
|
56
|
+
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.` })] });
|
|
57
|
+
const failure = failures.get(key);
|
|
58
|
+
const field = Input({
|
|
59
|
+
key: `option:${key}`,
|
|
60
|
+
label: '› ',
|
|
61
|
+
placeholder: option.kind === 'choice' ? (option.choices ?? []).join(', ') : option.kind === 'toggle' ? 'true or false' : option.kind === 'list' ? 'comma-separated' : '',
|
|
62
|
+
value: saved.get(key) ?? '',
|
|
63
|
+
submitLabel: 'save',
|
|
64
|
+
onSubmit: (value) => void save(key, value)
|
|
65
|
+
.then((refused) => {
|
|
66
|
+
if (refused === undefined) {
|
|
67
|
+
saved.set(key, value);
|
|
68
|
+
failures.delete(key);
|
|
69
|
+
}
|
|
70
|
+
else
|
|
71
|
+
failures.set(key, refused);
|
|
72
|
+
})
|
|
73
|
+
.catch((error) => failures.set(key, messageOf(error)))
|
|
74
|
+
.finally(() => claude().ui.invalidate('ui.render')),
|
|
75
|
+
});
|
|
76
|
+
const status = failure !== undefined ? Text({ color: 'error', children: failure }) : saved.has(key) ? Text({ color: 'success', children: '✔ saved' }) : Text({ children: '' });
|
|
77
|
+
return Box({ flexDirection: 'column', children: [label, field, status] });
|
|
78
|
+
}), [Button({ key: 'not-now', hotkey: 'n', label: 'Not now', onPress: () => answer(false) }), Button({ key: 'next', hotkey: 'x', label: 'Next', onPress: () => answer(true) })]),
|
|
79
|
+
}), false);
|
|
80
|
+
},
|
|
81
|
+
step(mod, step, index, total) {
|
|
82
|
+
let notice;
|
|
83
|
+
return show((answer) => ({
|
|
84
|
+
draw: (props) => {
|
|
85
|
+
let body;
|
|
86
|
+
try {
|
|
87
|
+
body = step.render(mod, props);
|
|
88
|
+
}
|
|
89
|
+
catch (error) {
|
|
90
|
+
body = Text({ color: 'error', children: `The ${step.title} step could not draw: ${messageOf(error)}` });
|
|
91
|
+
}
|
|
92
|
+
const next = async () => {
|
|
93
|
+
const isDone = await Promise.resolve(step.isDone(mod)).catch(() => false);
|
|
94
|
+
if (isDone)
|
|
95
|
+
answer(true);
|
|
96
|
+
else {
|
|
97
|
+
notice = `Finish ${step.title} first.`;
|
|
98
|
+
claude().ui.invalidate('ui.render');
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
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() })]);
|
|
102
|
+
},
|
|
103
|
+
}), false);
|
|
104
|
+
},
|
|
105
|
+
async close() {
|
|
106
|
+
page = undefined;
|
|
107
|
+
if (!isOpen)
|
|
108
|
+
return;
|
|
109
|
+
isOpen = false;
|
|
110
|
+
await claude().ui.close({ id: installerPaneId });
|
|
111
|
+
},
|
|
112
|
+
};
|
|
113
|
+
}
|
package/runtime/lifecycle.d.ts
CHANGED
|
@@ -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
|
|
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,
|
|
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,EAAgQ,KAAK,KAAK,EAAE,MAAM,eAAe,CAAA;AAGxS,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,CAmVlB"}
|