@cmodjs/core 0.1.14 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/docs/commands.md +32 -21
- package/docs/dependencies.md +8 -0
- package/docs/index.md +12 -4
- package/docs/install-steps.md +36 -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 +16 -1
- package/records.d.ts.map +1 -1
- package/records.js +119 -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 +24 -0
- package/runtime/installer.d.ts.map +1 -0
- package/runtime/installer.js +118 -0
- package/runtime/lifecycle.d.ts +6 -3
- package/runtime/lifecycle.d.ts.map +1 -1
- package/runtime/lifecycle.js +226 -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 +131 -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 +169 -0
- package/src/runtime/lifecycle.ts +227 -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,143 @@
|
|
|
1
|
+
import { formatOf, MetadataError, readMetadata, sidecarSuffix, writeMetadata } from '../utils/metadata.js';
|
|
2
|
+
import { messageOf } from '../utils/text.js';
|
|
3
|
+
import { picomatch, resolve } from '../vendor.js';
|
|
4
|
+
const skippedFolders = ['.git', 'node_modules'];
|
|
5
|
+
const ownKey = /^[A-Za-z0-9_][A-Za-z0-9_.-]*$/;
|
|
6
|
+
export function resolvedPath(path, places) {
|
|
7
|
+
const configured = /^~\/\.claude(?=\/|$)/.exec(path);
|
|
8
|
+
if (configured !== null && places.configRoot !== undefined)
|
|
9
|
+
return resolve(`${places.configRoot}${path.slice(configured[0].length)}`);
|
|
10
|
+
if (path === '~' || path.startsWith('~/')) {
|
|
11
|
+
if (places.home === undefined)
|
|
12
|
+
throw new Error(`${path} starts with ~, and HOME is not set. Start Claude Code with HOME set.`);
|
|
13
|
+
return resolve(`${places.home}${path.slice(1)}`);
|
|
14
|
+
}
|
|
15
|
+
return path.startsWith('/') ? resolve(path) : resolve(places.projectRoot(), path);
|
|
16
|
+
}
|
|
17
|
+
export function createModFiles({ name, claude, places, checkWrite }) {
|
|
18
|
+
const prefix = `${name}.`;
|
|
19
|
+
const cache = new Map();
|
|
20
|
+
const logged = new Set();
|
|
21
|
+
const own = (all) => Object.freeze(Object.fromEntries(Object.entries(all).flatMap(([key, value]) => (key.startsWith(prefix) ? [[key.slice(prefix.length), value]] : []))));
|
|
22
|
+
const locate = async (path) => {
|
|
23
|
+
const stat = await claude.fs.stat(path, { resolve: true });
|
|
24
|
+
if (stat.kind !== 'file')
|
|
25
|
+
throw new Error(`${path} is not a file, so it holds no metadata.`);
|
|
26
|
+
const realPath = stat.realPath ?? path;
|
|
27
|
+
const extensionless = !realPath.slice(realPath.lastIndexOf('/') + 1).includes('.');
|
|
28
|
+
const firstLine = extensionless ? (await claude.fs.read(realPath)).split('\n', 1)[0] : undefined;
|
|
29
|
+
const format = formatOf(realPath, firstLine);
|
|
30
|
+
const holder = format.kind === 'sidecar' ? `${realPath}${sidecarSuffix}` : realPath;
|
|
31
|
+
const sidecar = format.kind === 'sidecar' ? await claude.fs.stat(holder).catch(() => undefined) : undefined;
|
|
32
|
+
return { realPath, format, holder, signature: `${stat.mtimeMs}:${stat.size}:${sidecar?.kind === 'file' ? `${sidecar.mtimeMs}:${sidecar.size}` : ''}` };
|
|
33
|
+
};
|
|
34
|
+
const textOf = async (holder) => ((await claude.fs.stat(holder).catch(() => undefined))?.kind === 'file' ? claude.fs.read(holder) : '');
|
|
35
|
+
const readEntry = async (path) => {
|
|
36
|
+
const located = await locate(path);
|
|
37
|
+
const cached = cache.get(path);
|
|
38
|
+
if (cached?.signature === located.signature)
|
|
39
|
+
return cached.read;
|
|
40
|
+
let read;
|
|
41
|
+
try {
|
|
42
|
+
const { metadata, name: declared } = readMetadata(await textOf(located.holder), located.format);
|
|
43
|
+
read = { metadata, ...(declared === undefined ? {} : { name: declared }) };
|
|
44
|
+
}
|
|
45
|
+
catch (error) {
|
|
46
|
+
if (!(error instanceof MetadataError))
|
|
47
|
+
throw error;
|
|
48
|
+
const failure = `${located.holder} ${error.message}`;
|
|
49
|
+
read = { metadata: {}, error: failure };
|
|
50
|
+
if (!logged.has(failure))
|
|
51
|
+
claude.ui.log(`${name}: ${failure}`);
|
|
52
|
+
logged.add(failure);
|
|
53
|
+
}
|
|
54
|
+
cache.set(path, { signature: located.signature, read });
|
|
55
|
+
return read;
|
|
56
|
+
};
|
|
57
|
+
const matchesOf = async (pattern) => {
|
|
58
|
+
const absolute = resolvedPath(pattern, places);
|
|
59
|
+
const scanned = picomatch.scan(absolute);
|
|
60
|
+
if (!scanned.isGlob)
|
|
61
|
+
return (await claude.fs.stat(absolute).catch(() => undefined))?.kind === 'file' ? [absolute] : [];
|
|
62
|
+
const isMatch = picomatch(absolute, { dot: true });
|
|
63
|
+
const entersSkipped = skippedFolders.filter((folder) => scanned.glob.split('/').includes(folder));
|
|
64
|
+
const maxDepth = scanned.glob.includes('**') ? Number.POSITIVE_INFINITY : scanned.glob.split('/').length;
|
|
65
|
+
const found = [];
|
|
66
|
+
const visited = new Set();
|
|
67
|
+
const walk = async (folder, depth) => {
|
|
68
|
+
const real = (await claude.fs.stat(folder, { resolve: true }).catch(() => undefined))?.realPath ?? folder;
|
|
69
|
+
if (visited.has(real))
|
|
70
|
+
return;
|
|
71
|
+
visited.add(real);
|
|
72
|
+
const entries = await claude.fs.list(folder).catch(() => []);
|
|
73
|
+
await Promise.all(entries.map(async (entry) => {
|
|
74
|
+
const path = `${folder}/${entry.name}`;
|
|
75
|
+
const kind = entry.isLink === true ? (await claude.fs.stat(path).catch(() => undefined))?.kind : entry.kind;
|
|
76
|
+
if (kind === 'file' && isMatch(path))
|
|
77
|
+
found.push(path);
|
|
78
|
+
if (kind !== 'dir' || depth + 1 >= maxDepth)
|
|
79
|
+
return;
|
|
80
|
+
if (skippedFolders.includes(entry.name) && !entersSkipped.includes(entry.name))
|
|
81
|
+
return;
|
|
82
|
+
await walk(path, depth + 1);
|
|
83
|
+
}));
|
|
84
|
+
};
|
|
85
|
+
const base = scanned.base === '' ? '/' : scanned.base;
|
|
86
|
+
if ((await claude.fs.stat(base).catch(() => undefined))?.kind === 'dir')
|
|
87
|
+
await walk(base, 0);
|
|
88
|
+
return found;
|
|
89
|
+
};
|
|
90
|
+
return {
|
|
91
|
+
async find(glob) {
|
|
92
|
+
const patterns = typeof glob === 'string' ? [glob] : glob;
|
|
93
|
+
const paths = [...new Set((await Promise.all(patterns.map(matchesOf))).flat())].sort();
|
|
94
|
+
const kept = paths.filter((path) => !(path.endsWith(sidecarSuffix) && paths.includes(path.slice(0, -sidecarSuffix.length))));
|
|
95
|
+
return Promise.all(kept.map(async (path) => {
|
|
96
|
+
const read = await readEntry(path);
|
|
97
|
+
return { path, ...(read.name === undefined ? {} : { name: read.name }), metadata: own(read.metadata), ...(read.error === undefined ? {} : { error: read.error }) };
|
|
98
|
+
}));
|
|
99
|
+
},
|
|
100
|
+
async read(path) {
|
|
101
|
+
const read = await readEntry(resolvedPath(path, places));
|
|
102
|
+
if (read.error !== undefined)
|
|
103
|
+
throw new Error(`${name}: ${read.error}`);
|
|
104
|
+
return own(read.metadata);
|
|
105
|
+
},
|
|
106
|
+
async update(path, change) {
|
|
107
|
+
const given = resolvedPath(path, places);
|
|
108
|
+
await checkWrite(given);
|
|
109
|
+
const located = await locate(given);
|
|
110
|
+
const text = await textOf(located.holder);
|
|
111
|
+
let all;
|
|
112
|
+
try {
|
|
113
|
+
all = readMetadata(text, located.format).metadata;
|
|
114
|
+
}
|
|
115
|
+
catch (error) {
|
|
116
|
+
throw new Error(`${name}: ${located.holder} ${messageOf(error)}`, { cause: error });
|
|
117
|
+
}
|
|
118
|
+
const edited = { ...own(all) };
|
|
119
|
+
change(edited);
|
|
120
|
+
for (const [key, value] of Object.entries(edited)) {
|
|
121
|
+
if (!ownKey.test(key))
|
|
122
|
+
throw new Error(`${name}: the metadata key "${key}" is not a name. Use letters, digits, "_", ".", and "-".`);
|
|
123
|
+
if (typeof value !== 'string')
|
|
124
|
+
throw new Error(`${name}: the metadata key "${key}" holds ${JSON.stringify(value)}. Each value is text, and a list is one space-separated text.`);
|
|
125
|
+
if (value.includes('\n'))
|
|
126
|
+
throw new Error(`${name}: the metadata key "${key}" holds a line break. Each value is one line of text.`);
|
|
127
|
+
}
|
|
128
|
+
const next = {};
|
|
129
|
+
for (const [key, value] of Object.entries(all)) {
|
|
130
|
+
if (!key.startsWith(prefix))
|
|
131
|
+
next[key] = value;
|
|
132
|
+
else if (Object.hasOwn(edited, key.slice(prefix.length)))
|
|
133
|
+
next[key] = edited[key.slice(prefix.length)];
|
|
134
|
+
}
|
|
135
|
+
for (const [key, value] of Object.entries(edited))
|
|
136
|
+
next[`${prefix}${key}`] = value;
|
|
137
|
+
if (JSON.stringify(next) === JSON.stringify(all))
|
|
138
|
+
return;
|
|
139
|
+
await claude.fs.write(located.holder, writeMetadata(text, located.format, next));
|
|
140
|
+
cache.delete(given);
|
|
141
|
+
},
|
|
142
|
+
};
|
|
143
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { PluginOptions } from 'claude-code';
|
|
2
|
+
import { type Options } from '../options.js';
|
|
3
|
+
import type { Claude } from './claude.js';
|
|
4
|
+
export declare class MissingOptions extends Error {
|
|
5
|
+
readonly keys: readonly string[];
|
|
6
|
+
readonly titles: readonly string[];
|
|
7
|
+
constructor(keys: readonly string[], titles: readonly string[]);
|
|
8
|
+
}
|
|
9
|
+
export type ModOptions = {
|
|
10
|
+
readonly values: Readonly<Record<string, unknown>>;
|
|
11
|
+
readonly missing: readonly string[];
|
|
12
|
+
load(root: string): Promise<void>;
|
|
13
|
+
};
|
|
14
|
+
export declare function createOptions({ name, declared, fromClaude, claude, changed }: {
|
|
15
|
+
readonly name: string;
|
|
16
|
+
readonly declared: Options;
|
|
17
|
+
readonly fromClaude: PluginOptions;
|
|
18
|
+
readonly claude: Claude;
|
|
19
|
+
readonly changed: () => void;
|
|
20
|
+
}): ModOptions;
|
|
21
|
+
//# sourceMappingURL=options.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"options.d.ts","sourceRoot":"","sources":["../src/runtime/options.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAA;AAChD,OAAO,EAAoC,KAAK,OAAO,EAAE,MAAM,eAAe,CAAA;AAG9E,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AAEzC,qBAAa,cAAe,SAAQ,KAAK;IACvC,QAAQ,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,CAAA;IAChC,QAAQ,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAA;IAElC,YAAY,IAAI,EAAE,SAAS,MAAM,EAAE,EAAE,MAAM,EAAE,SAAS,MAAM,EAAE,EAI7D;CACF;AAED,MAAM,MAAM,UAAU,GAAG;IACvB,QAAQ,CAAC,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAA;IAClD,QAAQ,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,CAAA;IACnC,IAAI,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;CAClC,CAAA;AAED,wBAAgB,aAAa,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,UAAU,EAAE,MAAM,EAAE,OAAO,EAAE,EAAE;IAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;IAAC,QAAQ,CAAC,UAAU,EAAE,aAAa,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,OAAO,EAAE,MAAM,IAAI,CAAA;CAAE,GAAG,UAAU,CAmE3O"}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { fitsOption, isUnset } from '../options.js';
|
|
2
|
+
import { configFolders } from '../records.js';
|
|
3
|
+
import { listed, messageOf } from '../utils/text.js';
|
|
4
|
+
export class MissingOptions extends Error {
|
|
5
|
+
keys;
|
|
6
|
+
titles;
|
|
7
|
+
constructor(keys, titles) {
|
|
8
|
+
super(`it needs ${listed(titles)}`);
|
|
9
|
+
this.keys = keys;
|
|
10
|
+
this.titles = titles;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
export function createOptions({ name, declared, fromClaude, claude, changed }) {
|
|
14
|
+
let values = merged(declared, fromClaude, {}, new Set());
|
|
15
|
+
let locked;
|
|
16
|
+
let loadedRoot;
|
|
17
|
+
const readLocked = async () => {
|
|
18
|
+
if (Object.keys(declared).length === 0)
|
|
19
|
+
return new Set();
|
|
20
|
+
const rows = await claude.config.list().catch((error) => {
|
|
21
|
+
claude.ui.log(`${name} could not read /config, so a project's options.json may override a value your organization set: ${messageOf(error)}`, { to: 'debug' });
|
|
22
|
+
return [];
|
|
23
|
+
});
|
|
24
|
+
return new Set(rows.filter((row) => row.isLocked && row.key.startsWith(`${name}.`)).map((row) => row.key.slice(name.length + 1)));
|
|
25
|
+
};
|
|
26
|
+
const readProject = async (root) => {
|
|
27
|
+
const env = { HOME: await claude.env.home(), CLAUDE_CONFIG_DIR: await claude.env.configHome() };
|
|
28
|
+
const project = configFolders(env, name, root).find(({ tier }) => tier === 'project');
|
|
29
|
+
if (project === undefined)
|
|
30
|
+
return {};
|
|
31
|
+
const path = `${project.folder}/options.json`;
|
|
32
|
+
if (!(await claude.fs.exists(path)))
|
|
33
|
+
return {};
|
|
34
|
+
const ignore = (reason) => claude.ui.log(`${path} ${reason}`);
|
|
35
|
+
let file;
|
|
36
|
+
try {
|
|
37
|
+
file = JSON.parse(await claude.fs.read(path));
|
|
38
|
+
}
|
|
39
|
+
catch (error) {
|
|
40
|
+
ignore(`is not JSON (${messageOf(error)}). Fix the file; ${name} uses its other options until then.`);
|
|
41
|
+
return {};
|
|
42
|
+
}
|
|
43
|
+
if (typeof file !== 'object' || file === null || Array.isArray(file)) {
|
|
44
|
+
ignore('is not a JSON object. Write one, such as { "branch": "main" }.');
|
|
45
|
+
return {};
|
|
46
|
+
}
|
|
47
|
+
const kept = {};
|
|
48
|
+
for (const [key, value] of Object.entries(file)) {
|
|
49
|
+
const option = declared[key];
|
|
50
|
+
if (option === undefined)
|
|
51
|
+
ignore(`sets ${key}, which ${name} does not declare. ${Object.keys(declared).length === 0 ? 'Remove it.' : `Remove it, or use one of: ${Object.keys(declared).join(', ')}.`}`);
|
|
52
|
+
else if (option.kind === 'secret')
|
|
53
|
+
ignore(`sets ${key}, a secret, and a project file is committed for everyone to read. Remove it, and set it in /config.`);
|
|
54
|
+
else {
|
|
55
|
+
const misfit = fitsOption(option, value);
|
|
56
|
+
if (misfit === undefined)
|
|
57
|
+
kept[key] = value;
|
|
58
|
+
else
|
|
59
|
+
ignore(`sets ${key} to ${JSON.stringify(value)}, and ${option.title} ${misfit}.`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return kept;
|
|
63
|
+
};
|
|
64
|
+
return {
|
|
65
|
+
get values() {
|
|
66
|
+
return values;
|
|
67
|
+
},
|
|
68
|
+
get missing() {
|
|
69
|
+
return Object.entries(declared)
|
|
70
|
+
.filter(([key, option]) => option.default === undefined && isUnset(values[key]))
|
|
71
|
+
.map(([key]) => key);
|
|
72
|
+
},
|
|
73
|
+
async load(root) {
|
|
74
|
+
if (Object.keys(declared).length === 0)
|
|
75
|
+
return;
|
|
76
|
+
locked ??= await readLocked();
|
|
77
|
+
const project = await readProject(root);
|
|
78
|
+
const isFirst = loadedRoot === undefined;
|
|
79
|
+
loadedRoot = root;
|
|
80
|
+
const next = merged(declared, fromClaude, project, locked);
|
|
81
|
+
if (!isFirst && JSON.stringify(next) === JSON.stringify(values))
|
|
82
|
+
return;
|
|
83
|
+
values = next;
|
|
84
|
+
if (!isFirst)
|
|
85
|
+
changed();
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
function merged(declared, fromClaude, project, locked) {
|
|
90
|
+
return Object.freeze(Object.fromEntries(Object.entries(declared).map(([key, option]) => [key, valueOf(key, option, fromClaude[key], project, locked)])));
|
|
91
|
+
}
|
|
92
|
+
function valueOf(key, option, own, project, locked) {
|
|
93
|
+
if (locked.has(key))
|
|
94
|
+
return own;
|
|
95
|
+
if (Object.hasOwn(project, key))
|
|
96
|
+
return project[key];
|
|
97
|
+
if (!isUnset(own) && fitsOption(option, own) === undefined)
|
|
98
|
+
return Array.isArray(own) ? Object.freeze([...own]) : own;
|
|
99
|
+
return option.default ?? (option.kind === 'list' ? Object.freeze([]) : option.kind === 'number' ? undefined : '');
|
|
100
|
+
}
|
package/runtime/state.d.ts
CHANGED
|
@@ -11,6 +11,7 @@ export type StateOptions = {
|
|
|
11
11
|
export type ModState<State extends object> = {
|
|
12
12
|
readonly state: State;
|
|
13
13
|
readonly root: string;
|
|
14
|
+
changed(): void;
|
|
14
15
|
load(): Promise<void>;
|
|
15
16
|
moveTo(root: string): Promise<void>;
|
|
16
17
|
switchSession(session: string, source: ClassicHookInputs['SessionStart']['source']): Promise<void>;
|
package/runtime/state.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"state.d.ts","sourceRoot":"","sources":["../src/runtime/state.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAA;
|
|
1
|
+
{"version":3,"file":"state.d.ts","sourceRoot":"","sources":["../src/runtime/state.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAA;AAEpD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AAQzC,MAAM,MAAM,YAAY,GAAG;IACzB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;IACxB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;IACxB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,OAAO,EAAE,MAAM,IAAI,CAAA;CAC7B,CAAA;AAED,MAAM,MAAM,QAAQ,CAAC,KAAK,SAAS,MAAM,IAAI;IAC3C,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAA;IACrB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,OAAO,IAAI,IAAI,CAAA;IACf,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAAA;IACrB,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IACnC,aAAa,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,iBAAiB,CAAC,cAAc,CAAC,CAAC,QAAQ,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;CACnG,CAAA;AAQD,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,IAAI,GAAG,IAAI,CAIvE;AAED,wBAAgB,WAAW,CAAC,KAAK,SAAS,MAAM,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,EAAE,YAAY,GAAG,QAAQ,CAAC,KAAK,CAAC,CAoF1I"}
|
package/runtime/state.js
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
|
-
import { configFolder, configFolders } from '../records.js';
|
|
2
1
|
import { messageOf } from '../utils/text.js';
|
|
3
2
|
const lifetimes = ['memory', 'session', 'project', 'global'];
|
|
4
|
-
const savedLifetimes = ['session', 'project', 'global'];
|
|
5
3
|
const keptPerValue = 20;
|
|
6
4
|
const listeners = new WeakMap();
|
|
7
5
|
export function onStateChange(state, listener) {
|
|
@@ -17,7 +15,6 @@ export function createState({ name, initial, session, root, claude, changed: red
|
|
|
17
15
|
};
|
|
18
16
|
const declared = declaredGroups(name, initial);
|
|
19
17
|
const current = copied(declared);
|
|
20
|
-
let defaults = declared;
|
|
21
18
|
let projectRoot = root;
|
|
22
19
|
let sessionId = session;
|
|
23
20
|
let turn = Promise.resolve();
|
|
@@ -38,62 +35,6 @@ export function createState({ name, initial, session, root, claude, changed: red
|
|
|
38
35
|
for (const oldest of owners.slice(0, -keptPerValue))
|
|
39
36
|
await claude.store.delete(oldest);
|
|
40
37
|
};
|
|
41
|
-
const fileValues = (path, tier, text, systemPath) => {
|
|
42
|
-
const values = [];
|
|
43
|
-
const ignore = (reason) => claude.ui.log(`${path} ${reason}`);
|
|
44
|
-
let file;
|
|
45
|
-
try {
|
|
46
|
-
file = JSON.parse(text);
|
|
47
|
-
}
|
|
48
|
-
catch (error) {
|
|
49
|
-
ignore(`is not JSON (${messageOf(error)}). Fix the file, then run /reload-plugins.`);
|
|
50
|
-
return values;
|
|
51
|
-
}
|
|
52
|
-
if (!isPlainObject(file)) {
|
|
53
|
-
ignore('is not a JSON object. Write one, such as { "global": { "key": "value" } }.');
|
|
54
|
-
return values;
|
|
55
|
-
}
|
|
56
|
-
for (const [group, entries] of Object.entries(file)) {
|
|
57
|
-
if (!isLifetime(group)) {
|
|
58
|
-
ignore(`sets ${group}, which ${name} does not declare. Remove it, or use one of: ${savedLifetimes.join(', ')}.`);
|
|
59
|
-
continue;
|
|
60
|
-
}
|
|
61
|
-
if (group === 'memory') {
|
|
62
|
-
ignore('sets memory, and memory values are never saved, so a file cannot set them. Remove it.');
|
|
63
|
-
continue;
|
|
64
|
-
}
|
|
65
|
-
if (!isPlainObject(entries)) {
|
|
66
|
-
ignore(`sets ${group} to ${typeOf(entries)}. Write an object of values, such as { "${group}": { "key": "value" } }.`);
|
|
67
|
-
continue;
|
|
68
|
-
}
|
|
69
|
-
for (const [key, value] of Object.entries(entries)) {
|
|
70
|
-
const choices = Object.keys(declared[group]);
|
|
71
|
-
const wanted = typeOf(declared[group][key]);
|
|
72
|
-
if (!Object.hasOwn(declared[group], key))
|
|
73
|
-
ignore(`sets ${group}.${key}, which ${name} does not declare. ${choices.length === 0 ? 'Remove it.' : `Remove it, or use one of: ${choices.join(', ')}.`}`);
|
|
74
|
-
else if (tier === 'project' && group === 'global')
|
|
75
|
-
ignore(`sets ${group}.${key}, and one repository cannot change a value for every project. Move it to ${systemPath}.`);
|
|
76
|
-
else if (typeOf(value) !== wanted)
|
|
77
|
-
ignore(`sets ${group}.${key} to ${typeOf(value)}, and ${name} keeps ${wanted} there. Write ${wanted}, or remove it.`);
|
|
78
|
-
else
|
|
79
|
-
values.push([group, key, frozen(name, `${group}.${key}`, value)]);
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
return values;
|
|
83
|
-
};
|
|
84
|
-
const readDefaults = async (nextRoot) => {
|
|
85
|
-
const env = { HOME: await claude.env.home(), CLAUDE_CONFIG_DIR: await claude.env.configHome() };
|
|
86
|
-
const systemPath = `${configFolder(env, name)}/state.json`;
|
|
87
|
-
const next = copied(declared);
|
|
88
|
-
for (const { tier, folder } of configFolders(env, name, nextRoot)) {
|
|
89
|
-
const path = `${folder}/state.json`;
|
|
90
|
-
if (!(await claude.fs.exists(path)))
|
|
91
|
-
continue;
|
|
92
|
-
for (const [lifetime, key, value] of fileValues(path, tier, await claude.fs.read(path), systemPath))
|
|
93
|
-
next[lifetime][key] = value;
|
|
94
|
-
}
|
|
95
|
-
return next;
|
|
96
|
-
};
|
|
97
38
|
const loadGroups = async (only, next) => {
|
|
98
39
|
const loaded = [];
|
|
99
40
|
for (const lifetime of only) {
|
|
@@ -102,9 +43,8 @@ export function createState({ name, initial, session, root, claude, changed: red
|
|
|
102
43
|
}
|
|
103
44
|
projectRoot = next.root;
|
|
104
45
|
sessionId = next.session;
|
|
105
|
-
defaults = next.defaults;
|
|
106
46
|
for (const [lifetime, key, saved] of loaded)
|
|
107
|
-
current[lifetime][key] = saved === undefined ?
|
|
47
|
+
current[lifetime][key] = saved === undefined ? declared[lifetime][key] : frozen(name, `${lifetime}.${key}`, saved);
|
|
108
48
|
changed();
|
|
109
49
|
};
|
|
110
50
|
const copySession = async (nextSession) => {
|
|
@@ -139,14 +79,15 @@ export function createState({ name, initial, session, root, claude, changed: red
|
|
|
139
79
|
get root() {
|
|
140
80
|
return projectRoot;
|
|
141
81
|
},
|
|
142
|
-
|
|
143
|
-
|
|
82
|
+
changed,
|
|
83
|
+
load: () => inTurn(() => loadGroups(lifetimes, { root: projectRoot, session: sessionId })),
|
|
84
|
+
moveTo: (nextRoot) => inTurn(async () => (nextRoot === projectRoot ? undefined : loadGroups(['project'], { root: nextRoot, session: sessionId }))),
|
|
144
85
|
switchSession: (nextSession, source) => inTurn(async () => {
|
|
145
86
|
if (nextSession === sessionId)
|
|
146
87
|
return;
|
|
147
88
|
if (source === 'fork')
|
|
148
89
|
await copySession(nextSession);
|
|
149
|
-
return loadGroups(['memory', 'session'], { root: projectRoot, session: nextSession
|
|
90
|
+
return loadGroups(['memory', 'session'], { root: projectRoot, session: nextSession });
|
|
150
91
|
}),
|
|
151
92
|
};
|
|
152
93
|
}
|
|
@@ -168,17 +109,6 @@ function copied(groups) {
|
|
|
168
109
|
function isLifetime(group) {
|
|
169
110
|
return lifetimes.includes(group);
|
|
170
111
|
}
|
|
171
|
-
function typeOf(value) {
|
|
172
|
-
if (value === null)
|
|
173
|
-
return 'null';
|
|
174
|
-
if (Array.isArray(value))
|
|
175
|
-
return 'an array';
|
|
176
|
-
if (typeof value === 'object')
|
|
177
|
-
return 'an object';
|
|
178
|
-
if (value === undefined)
|
|
179
|
-
return 'nothing';
|
|
180
|
-
return `a ${typeof value}`;
|
|
181
|
-
}
|
|
182
112
|
function frozen(name, path, value) {
|
|
183
113
|
const isArray = Array.isArray(value);
|
|
184
114
|
if (!isArray && !isPlainObject(value))
|
package/src/installer.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { Frozen, RenderElement, RenderPropsOf } from 'claude-code'
|
|
2
|
+
import type { Mod } from './mod.js'
|
|
3
|
+
|
|
4
|
+
export type Step<State extends object = Record<never, never>> = {
|
|
5
|
+
readonly id: string
|
|
6
|
+
readonly title: string
|
|
7
|
+
render(mod: Mod<State>, props: Frozen<RenderPropsOf['Pane']>): RenderElement
|
|
8
|
+
isDone(mod: Mod<State>): boolean | Promise<boolean>
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const stepId = /^[A-Za-z0-9_-]{1,64}$/
|
|
12
|
+
|
|
13
|
+
export function defineStep<State extends object = Record<never, never>>(step: Step<State>): Step<State> {
|
|
14
|
+
if (!stepId.test(step.id)) throw new Error(`defineStep: "${step.id}" is not a step id. Use 1 to 64 letters, digits, "_", or "-".`)
|
|
15
|
+
if (step.title.trim() === '') throw new Error(`defineStep: the step "${step.id}" needs a title, the line the installer shows above it.`)
|
|
16
|
+
return step
|
|
17
|
+
}
|
package/src/jobs/context.ts
CHANGED
|
@@ -14,7 +14,7 @@ type Session = Omit<Workspace, 'projectRoot' | 'cwd'>
|
|
|
14
14
|
|
|
15
15
|
type Stream = HookStream<ProcessSpawnChunk, ProcessSpawnResult>
|
|
16
16
|
|
|
17
|
-
export function workspaceReader({ claude }: JobContext): () => Promise<Workspace> {
|
|
17
|
+
export function workspaceReader({ mod: { claude } }: JobContext): () => Promise<Workspace> {
|
|
18
18
|
let session: Promise<Session> | undefined
|
|
19
19
|
return async () => {
|
|
20
20
|
session ??= readSession(claude)
|
|
@@ -40,7 +40,7 @@ export function afterCall<State extends object>(
|
|
|
40
40
|
failed: (error: unknown) => readonly string[],
|
|
41
41
|
): void {
|
|
42
42
|
const readWorkspace = workspaceReader(job)
|
|
43
|
-
job.on('tool.call', async (e, next) => {
|
|
43
|
+
job.mod.claude.on('tool.call', async (e, next) => {
|
|
44
44
|
const before = Promise.all([useOf(job, e), readWorkspace()])
|
|
45
45
|
await before.catch(() => undefined)
|
|
46
46
|
const result = await next(e)
|
|
@@ -58,7 +58,8 @@ export function modOf<State extends object>(job: JobContext<State>, { call, fold
|
|
|
58
58
|
return modWithin(job, deadline, async () => workspace.scope?.workTreeOf(call !== undefined && 'path' in call ? call.path : folder))
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
-
export function modWithin<State extends object>({ mod
|
|
61
|
+
export function modWithin<State extends object>({ mod }: JobContext<State>, deadline: Deadline, workTree: () => Promise<string | undefined> = async () => undefined): Mod<State> {
|
|
62
|
+
const { claude } = mod
|
|
62
63
|
const within = <Value>(call: string, task: Promise<Value>) => beforeDeadline(claude, deadline, call, task)
|
|
63
64
|
const folderOf = async (cwd: string | undefined) => {
|
|
64
65
|
const folder = cwd ?? (await workTree())
|
|
@@ -79,6 +80,7 @@ export function modWithin<State extends object>({ mod, claude }: JobContext<Stat
|
|
|
79
80
|
list: (path) => within('mod.fs.list', mod.fs.list(path)),
|
|
80
81
|
exists: (path) => within('mod.fs.exists', mod.fs.exists(path)),
|
|
81
82
|
stat: (path, options) => within('mod.fs.stat', mod.fs.stat(path, options)),
|
|
83
|
+
find: (glob) => within('mod.fs.find', mod.fs.find(glob)),
|
|
82
84
|
},
|
|
83
85
|
http: { fetch: (url, init) => within('mod.http.fetch', mod.http.fetch(url, init)) },
|
|
84
86
|
dependencies: dependencyCalls(claude, within),
|
package/src/jobs/permissions.ts
CHANGED
|
@@ -37,7 +37,7 @@ export function permissions<State extends object = Record<never, never>>(rules:
|
|
|
37
37
|
}
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
-
job.on('tool.check', async (e, next) => {
|
|
40
|
+
job.mod.claude.on('tool.check', async (e, next) => {
|
|
41
41
|
const [below, ours] = await Promise.all([next(e), verdictOf(e)])
|
|
42
42
|
return ours === undefined ? below : stricterVerdict<Verdict>(below, ours)
|
|
43
43
|
})
|
package/src/jobs/program.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Job } from '../mod.js'
|
|
2
|
+
import { PermissionRefused } from '../runtime/grants.js'
|
|
2
3
|
import { messageOf } from '../utils/text.js'
|
|
3
4
|
|
|
4
5
|
export type Program = {
|
|
@@ -12,7 +13,8 @@ const backoffSeconds = [1, 2, 4, 8, 16] as const
|
|
|
12
13
|
|
|
13
14
|
export function program(options: { readonly command: readonly string[]; readonly environment?: Record<string, string> }): Job<Program> {
|
|
14
15
|
const { command, environment } = options
|
|
15
|
-
return ({ mod,
|
|
16
|
+
return ({ mod, announce }) => {
|
|
17
|
+
const { claude } = mod
|
|
16
18
|
const [executable] = command
|
|
17
19
|
if (executable === undefined) throw new Error(`${mod.name}: the program has no command. Name the program on PATH and its arguments, such as ['preview-server', '--port', '0'].`)
|
|
18
20
|
let state: Program['state'] = 'stopped'
|
|
@@ -53,6 +55,7 @@ export function program(options: { readonly command: readonly string[]; readonly
|
|
|
53
55
|
for (const waiter of waiting.splice(0)) waiter.resolve(address)
|
|
54
56
|
}
|
|
55
57
|
} catch (error) {
|
|
58
|
+
if (error instanceof PermissionRefused) return stop(`${messageOf(error)} Add it, then run /reload-plugins.`)
|
|
56
59
|
lastError = messageOf(error)
|
|
57
60
|
}
|
|
58
61
|
address = undefined
|
package/src/jobs/prompt.ts
CHANGED
|
@@ -26,7 +26,7 @@ export function prompt<State extends object = Record<never, never>>({ name, prom
|
|
|
26
26
|
return (job) => {
|
|
27
27
|
job.reserveName('prompt', name, `prompt: ${job.mod.name} adds the name "${name}" two times. Give each prompt its own name.`)
|
|
28
28
|
|
|
29
|
-
const log = (reason: string) => job.claude.ui.log(`prompt "${name}" added nothing: ${reason}`, { to: 'debug' })
|
|
29
|
+
const log = (reason: string) => job.mod.claude.ui.log(`prompt "${name}" added nothing: ${reason}`, { to: 'debug' })
|
|
30
30
|
const textFor = async (input: PromptInput, mod: Mod<State>) => {
|
|
31
31
|
try {
|
|
32
32
|
const value = typeof text === 'string' ? text : await text(input, mod)
|
|
@@ -61,12 +61,12 @@ function addKeptCurrent<State extends object>(job: JobContext<State>, name: stri
|
|
|
61
61
|
let isQueued = false
|
|
62
62
|
let updates = Promise.resolve()
|
|
63
63
|
|
|
64
|
-
job.on('classic.SessionStart', (e, next) => {
|
|
64
|
+
job.mod.claude.on('classic.SessionStart', (e, next) => {
|
|
65
65
|
hasStarted = e.source === 'resume' || e.source === 'compact'
|
|
66
66
|
if (!hasStarted) given = undefined
|
|
67
67
|
return next(e)
|
|
68
68
|
})
|
|
69
|
-
job.on('prompt.context', async (e, next) => {
|
|
69
|
+
job.mod.claude.on('prompt.context', async (e, next) => {
|
|
70
70
|
const below = await next(e)
|
|
71
71
|
hasStarted = true
|
|
72
72
|
if (below.blocks.some((block) => block.name === name)) {
|
|
@@ -83,7 +83,7 @@ function addKeptCurrent<State extends object>(job: JobContext<State>, name: stri
|
|
|
83
83
|
const value = await textFor()
|
|
84
84
|
if (value === undefined || value === given) return
|
|
85
85
|
given = value
|
|
86
|
-
await job.claude.session.append({ message: { type: 'user', content: [{ type: 'text', text: `# ${name}\n${value}` }] } }).then((added) => {
|
|
86
|
+
await job.mod.claude.session.append({ message: { type: 'user', content: [{ type: 'text', text: `# ${name}\n${value}` }] } }).then((added) => {
|
|
87
87
|
if (added.deny !== undefined) log(`Claude Code refused the new text: ${added.deny}`)
|
|
88
88
|
})
|
|
89
89
|
}
|
|
@@ -95,7 +95,7 @@ function addKeptCurrent<State extends object>(job: JobContext<State>, name: stri
|
|
|
95
95
|
}
|
|
96
96
|
|
|
97
97
|
function addAfterUserPrompts(job: JobContext, when: RegExp | ((userPrompt: string) => boolean), textFor: (userPrompt: string) => Promise<string | undefined>, log: (reason: string) => void): void {
|
|
98
|
-
job.on('prompt.submit', async (e, next) => {
|
|
98
|
+
job.mod.claude.on('prompt.submit', async (e, next) => {
|
|
99
99
|
let value: string | undefined
|
|
100
100
|
try {
|
|
101
101
|
if (typeof when === 'function' ? when(e.text) : e.text.search(when) >= 0) value = await textFor(e.text)
|
package/src/jobs/schedule.ts
CHANGED
|
@@ -10,7 +10,8 @@ export function schedule<State extends object = Record<never, never>>(entry: Sch
|
|
|
10
10
|
if (typeof entry !== 'function') checked(entry)
|
|
11
11
|
|
|
12
12
|
return (job) => {
|
|
13
|
-
const { mod
|
|
13
|
+
const { mod } = job
|
|
14
|
+
const { claude } = mod
|
|
14
15
|
const order = scheduleCounts.get(mod) ?? 0
|
|
15
16
|
scheduleCounts.set(mod, order + 1)
|
|
16
17
|
const storeKey = `cmod:schedule:${order}`
|
|
@@ -20,7 +20,8 @@ export function slashCommand<State extends object = Record<never, never>>(option
|
|
|
20
20
|
}): Job<void, State> {
|
|
21
21
|
const { name, description, argumentHint, immediate, reply } = options
|
|
22
22
|
return (job) => {
|
|
23
|
-
const { mod,
|
|
23
|
+
const { mod, announce, reserveName } = job
|
|
24
|
+
const { claude } = mod
|
|
24
25
|
if (!commandName.test(name)) throw new Error(`${mod.name}: "${name}" is not a slash command name. Use 1 to 64 letters, digits, "_", or "-", without the slash.`)
|
|
25
26
|
reserveName('slashCommand', name, `${mod.name}: the slash command /${name} is already added. Give each slashCommand its own name.`)
|
|
26
27
|
|
|
@@ -36,7 +37,7 @@ export function slashCommand<State extends object = Record<never, never>>(option
|
|
|
36
37
|
announce(`/${name}`)
|
|
37
38
|
|
|
38
39
|
const replyMod = modWithin(job, deadline)
|
|
39
|
-
|
|
40
|
+
claude.on('command.run', async (e, next) => {
|
|
40
41
|
if (e.command !== registered) return next(e)
|
|
41
42
|
try {
|
|
42
43
|
return answerOf(await reply({ args: e.args, positionals: splitWords(e.args) }, replyMod))
|
package/src/jobs/status-line.ts
CHANGED
|
@@ -17,7 +17,8 @@ export function statusLine<State extends object = Record<never, never>>(options:
|
|
|
17
17
|
}): Job<void, State> {
|
|
18
18
|
const { text, interval = 10000 } = options
|
|
19
19
|
return (job) => {
|
|
20
|
-
const { mod,
|
|
20
|
+
const { mod, announce, reserveName } = job
|
|
21
|
+
const { claude } = mod
|
|
21
22
|
if (!(interval >= 1)) throw new Error(`${mod.name}: the status line interval is ${interval}. Give it in milliseconds, 1 or more.`)
|
|
22
23
|
reserveName('statusLine', '', `${mod.name}: a status line is already added. A mod has one status line: join the texts in one statusLine.`)
|
|
23
24
|
|
|
@@ -48,7 +49,7 @@ export function statusLine<State extends object = Record<never, never>>(options:
|
|
|
48
49
|
}
|
|
49
50
|
|
|
50
51
|
announce('a status line')
|
|
51
|
-
|
|
52
|
+
claude.on('session.measure', (e, next) => {
|
|
52
53
|
refresh()
|
|
53
54
|
return next(e)
|
|
54
55
|
})
|
package/src/jobs/tool.ts
CHANGED
|
@@ -16,7 +16,8 @@ export function tool<const Schema extends object = { type: 'object' }, State ext
|
|
|
16
16
|
}): Job<{ readonly name: string }, State> {
|
|
17
17
|
const { name, description, inputSchema, execute } = options
|
|
18
18
|
return (job) => {
|
|
19
|
-
const { mod,
|
|
19
|
+
const { mod, announce, reserveName } = job
|
|
20
|
+
const { claude } = mod
|
|
20
21
|
if (!toolName.test(name)) throw new Error(`${mod.name}: "${name}" is not a tool name. Use 1 to 64 letters, digits, "_", or "-".`)
|
|
21
22
|
const properties = (inputSchema as { properties?: object } | undefined)?.properties ?? {}
|
|
22
23
|
const reserved = reservedKeys.filter((key) => Object.hasOwn(properties, key))
|
|
@@ -36,7 +37,7 @@ export function tool<const Schema extends object = { type: 'object' }, State ext
|
|
|
36
37
|
announce(`the ${name} tool`)
|
|
37
38
|
|
|
38
39
|
const executeMod = modWithin(job, toolDeadline)
|
|
39
|
-
|
|
40
|
+
claude.on('tool.call', async (e, next) => {
|
|
40
41
|
if (e.tool !== registered) return next(e)
|
|
41
42
|
const input = toolInputOf(e)
|
|
42
43
|
const { valid, errors } = validator.validate(input)
|