@minko-ai/minko 0.1.0 → 0.1.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/README.md +577 -2
- package/assets/mascot-preview.png +0 -0
- package/assets/mascot-preview.txt +46 -0
- package/assets/mascot-source.png +0 -0
- package/assets/skills/README.md +21 -0
- package/assets/skills/brainstorming/LICENSE +21 -0
- package/assets/skills/brainstorming/SKILL.md +316 -0
- package/assets/skills/buddhist-method/LICENSE +21 -0
- package/assets/skills/buddhist-method/SKILL.md +137 -0
- package/assets/skills/buddhist-method/references/ariyasacca-debug.md +57 -0
- package/assets/skills/buddhist-method/references/extended-principles.md +100 -0
- package/assets/skills/code-humanizer/LICENSE +21 -0
- package/assets/skills/code-humanizer/SKILL.md +174 -0
- package/assets/skills/code-review/LICENSE +21 -0
- package/assets/skills/code-review/SKILL.md +232 -0
- package/assets/skills/code-review/reference/angular.md +768 -0
- package/assets/skills/code-review/reference/architecture-review-guide.md +472 -0
- package/assets/skills/code-review/reference/c.md +890 -0
- package/assets/skills/code-review/reference/code-quality-universal.md +488 -0
- package/assets/skills/code-review/reference/code-review-best-practices.md +136 -0
- package/assets/skills/code-review/reference/common-bugs-checklist.md +302 -0
- package/assets/skills/code-review/reference/cpp.md +893 -0
- package/assets/skills/code-review/reference/cross-cutting/async-concurrency-patterns.md +515 -0
- package/assets/skills/code-review/reference/cross-cutting/error-handling-principles.md +492 -0
- package/assets/skills/code-review/reference/cross-cutting/n-plus-one-queries.md +309 -0
- package/assets/skills/code-review/reference/cross-cutting/sql-injection-prevention.md +308 -0
- package/assets/skills/code-review/reference/cross-cutting/xss-prevention.md +264 -0
- package/assets/skills/code-review/reference/csharp.md +525 -0
- package/assets/skills/code-review/reference/css-less-sass.md +661 -0
- package/assets/skills/code-review/reference/dart.md +670 -0
- package/assets/skills/code-review/reference/django.md +985 -0
- package/assets/skills/code-review/reference/fastapi.md +580 -0
- package/assets/skills/code-review/reference/go.md +993 -0
- package/assets/skills/code-review/reference/java.md +409 -0
- package/assets/skills/code-review/reference/java8.md +586 -0
- package/assets/skills/code-review/reference/kotlin.md +1018 -0
- package/assets/skills/code-review/reference/nestjs.md +593 -0
- package/assets/skills/code-review/reference/performance-review-guide.md +816 -0
- package/assets/skills/code-review/reference/php.md +684 -0
- package/assets/skills/code-review/reference/python.md +1073 -0
- package/assets/skills/code-review/reference/qt.md +757 -0
- package/assets/skills/code-review/reference/react.md +871 -0
- package/assets/skills/code-review/reference/ruby.md +964 -0
- package/assets/skills/code-review/reference/rust.md +846 -0
- package/assets/skills/code-review/reference/security-review-guide.md +494 -0
- package/assets/skills/code-review/reference/svelte.md +1064 -0
- package/assets/skills/code-review/reference/swift.md +936 -0
- package/assets/skills/code-review/reference/typescript.md +1016 -0
- package/assets/skills/code-review/reference/vue.md +924 -0
- package/assets/skills/code-review/reference/zig.md +440 -0
- package/assets/skills/cyclomatic-complexity/LICENSE +201 -0
- package/assets/skills/cyclomatic-complexity/SKILL.md +69 -0
- package/assets/skills/five-whys/LICENSE +21 -0
- package/assets/skills/five-whys/SKILL.md +247 -0
- package/assets/skills/five-whys/examples/performance-regression.md +161 -0
- package/assets/skills/five-whys/examples/production-incident.md +134 -0
- package/assets/skills/five-whys/references/software-patterns.md +284 -0
- package/assets/skills/five-whys/references/toyota-origins.md +257 -0
- package/assets/skills/systematic-debugging/LICENSE +21 -0
- package/assets/skills/systematic-debugging/SKILL.md +283 -0
- package/assets/skills/test-driven-development/LICENSE +21 -0
- package/assets/skills/test-driven-development/SKILL.md +330 -0
- package/assets/skills/verification-before-completion/LICENSE +21 -0
- package/assets/skills/verification-before-completion/SKILL.md +120 -0
- package/assets/skills/vibe-sec/LICENSE +201 -0
- package/assets/skills/vibe-sec/SKILL.md +758 -0
- package/assets/skills/writing-plans/LICENSE +21 -0
- package/assets/skills/writing-plans/SKILL.md +204 -0
- package/bin/minko.js +2 -2
- package/dist/agent/approvalTokens.js +224 -0
- package/dist/agent/branchLock.js +315 -0
- package/dist/agent/compact.js +182 -0
- package/dist/agent/context.js +183 -0
- package/dist/agent/cron.js +326 -0
- package/dist/agent/hooks.js +211 -0
- package/dist/agent/lanes.js +511 -0
- package/dist/agent/loop.js +337 -0
- package/dist/agent/memory.js +159 -0
- package/dist/agent/permissions.js +71 -0
- package/dist/agent/plan.js +118 -0
- package/dist/agent/prompt.js +120 -0
- package/dist/agent/subagent.js +67 -0
- package/dist/agent/tasks.js +449 -0
- package/dist/agent/transcript.js +62 -0
- package/dist/agent/workerStore.js +172 -0
- package/dist/agent/workerTypes.js +1 -0
- package/dist/agent/workers.js +503 -0
- package/dist/cli/app.js +261 -0
- package/dist/cli/argv.js +206 -0
- package/dist/cli/commandsCommand.js +12 -0
- package/dist/cli/configCommand.js +170 -0
- package/dist/cli/doctorCommand.js +112 -0
- package/dist/cli/exportCommand.js +85 -0
- package/dist/cli/forkCommand.js +24 -0
- package/dist/cli/linkCommand.js +272 -0
- package/dist/cli/loginCommand.js +69 -0
- package/dist/cli/mcpLoginCommand.js +68 -0
- package/dist/cli/pluginCommand.js +113 -0
- package/dist/cli/powerCommand.js +174 -0
- package/dist/cli/promptFooter.js +74 -0
- package/dist/cli/relayCommand.js +38 -0
- package/dist/cli/sandboxCommand.js +31 -0
- package/dist/cli/serveCommand.js +104 -0
- package/dist/cli/statusCommand.js +120 -0
- package/dist/cli/tui.js +1104 -0
- package/dist/commands/registry.js +99 -0
- package/dist/config/paths.js +81 -0
- package/dist/config/store.js +605 -0
- package/dist/index.js +199 -0
- package/dist/link/protocol.js +159 -0
- package/dist/link/relay.js +184 -0
- package/dist/link/server.js +566 -0
- package/dist/link/tunnel.js +90 -0
- package/dist/lsp/client.js +340 -0
- package/dist/lsp/tool.js +177 -0
- package/dist/mcp/client.js +469 -0
- package/dist/mcp/httpClient.js +191 -0
- package/dist/mcp/server.js +206 -0
- package/dist/mcp/tools.js +173 -0
- package/dist/oauth/device.js +111 -0
- package/dist/oauth/store.js +56 -0
- package/dist/plugins/loader.js +291 -0
- package/dist/plugins/market.js +98 -0
- package/dist/providers/anthropic.js +279 -0
- package/dist/providers/factory.js +109 -0
- package/dist/providers/openai.js +182 -0
- package/dist/providers/retry.js +95 -0
- package/dist/providers/sse.js +67 -0
- package/dist/providers/types.js +10 -0
- package/dist/rag/embeddings.js +171 -0
- package/dist/rag/index.js +373 -0
- package/dist/rag/server.js +167 -0
- package/dist/rag/tool.js +148 -0
- package/dist/sandbox/index.js +55 -0
- package/dist/sdk/server.js +150 -0
- package/dist/sdk/session.js +104 -0
- package/dist/session/liveness.js +148 -0
- package/dist/session/recorder.js +49 -0
- package/dist/session/store.js +171 -0
- package/dist/skills/index.js +428 -0
- package/dist/theme.js +30 -0
- package/dist/tools/fs.js +195 -0
- package/dist/tools/notebook.js +109 -0
- package/dist/tools/registry.js +32 -0
- package/dist/tools/search.js +130 -0
- package/dist/tools/shell.js +200 -0
- package/dist/tools/structured.js +83 -0
- package/dist/tools/types.js +1 -0
- package/dist/ui/ansi.js +135 -0
- package/dist/ui/banner.js +103 -0
- package/dist/ui/input.js +49 -0
- package/dist/ui/inputModel.js +113 -0
- package/dist/ui/keyboard.js +142 -0
- package/dist/ui/mascot.js +34 -0
- package/dist/ui/mascotData.js +96 -0
- package/dist/ui/md.js +183 -0
- package/dist/ui/screen.js +49 -0
- package/dist/ui/spinner.js +52 -0
- package/dist/ui/transcript.js +287 -0
- package/dist/util/clipboard.js +53 -0
- package/dist/util/errors.js +50 -0
- package/dist/util/version.js +12 -0
- package/dist/web/fetch.js +168 -0
- package/dist/web/search.js +97 -0
- package/package.json +45 -36
- package/src/agent/loop.ts +0 -159
- package/src/agent/prompt.ts +0 -45
- package/src/cli/app.ts +0 -396
- package/src/cli/argv.ts +0 -139
- package/src/cli/configCommand.ts +0 -175
- package/src/config/paths.ts +0 -20
- package/src/config/store.ts +0 -149
- package/src/index.ts +0 -120
- package/src/providers/anthropic.ts +0 -215
- package/src/providers/openai.ts +0 -158
- package/src/providers/sse.ts +0 -57
- package/src/providers/types.ts +0 -59
- package/src/session/store.ts +0 -125
- package/src/theme.ts +0 -41
- package/src/tools/fs.ts +0 -143
- package/src/tools/registry.ts +0 -19
- package/src/tools/shell.ts +0 -120
- package/src/tools/types.ts +0 -26
- package/src/ui/ansi.ts +0 -110
- package/src/ui/banner.ts +0 -39
- package/src/ui/input.ts +0 -54
- package/src/ui/md.ts +0 -160
- package/src/ui/spinner.ts +0 -58
- package/src/util/errors.ts +0 -48
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { minkoHome } from "../config/paths.js";
|
|
4
|
+
import { WorkerRegistry } from "./workers.js";
|
|
5
|
+
export class CronStore {
|
|
6
|
+
dir;
|
|
7
|
+
now;
|
|
8
|
+
constructor(dir = path.join(minkoHome(), 'cron'), now = Date.now) {
|
|
9
|
+
this.dir = dir;
|
|
10
|
+
this.now = now;
|
|
11
|
+
}
|
|
12
|
+
create(input) {
|
|
13
|
+
const name = input.name.trim();
|
|
14
|
+
const prompt = input.prompt.trim();
|
|
15
|
+
if (name.length === 0) {
|
|
16
|
+
throw new Error('缺少 name 参数');
|
|
17
|
+
}
|
|
18
|
+
if (prompt.length === 0) {
|
|
19
|
+
throw new Error('缺少 prompt 参数');
|
|
20
|
+
}
|
|
21
|
+
const everyMinutes = clampMinutes(input.everyMinutes);
|
|
22
|
+
const now = this.now();
|
|
23
|
+
const record = {
|
|
24
|
+
id: 'c-' + Date.now().toString(36) + '-' + Math.random().toString(36).slice(2, 6),
|
|
25
|
+
name,
|
|
26
|
+
prompt,
|
|
27
|
+
everyMinutes,
|
|
28
|
+
enabled: true,
|
|
29
|
+
allowWrite: input.allowWrite === true,
|
|
30
|
+
cwd: input.cwd?.trim() || undefined,
|
|
31
|
+
nextRunAt: new Date(now + everyMinutes * 60_000).toISOString(),
|
|
32
|
+
createdAt: new Date(now).toISOString(),
|
|
33
|
+
updatedAt: new Date(now).toISOString(),
|
|
34
|
+
};
|
|
35
|
+
this.save(record);
|
|
36
|
+
return record;
|
|
37
|
+
}
|
|
38
|
+
get(id) {
|
|
39
|
+
try {
|
|
40
|
+
const raw = JSON.parse(fs.readFileSync(this.fileFor(id), 'utf8'));
|
|
41
|
+
return parseRecord(id, raw);
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
list() {
|
|
48
|
+
let names;
|
|
49
|
+
try {
|
|
50
|
+
names = fs.readdirSync(this.dir);
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return [];
|
|
54
|
+
}
|
|
55
|
+
const records = [];
|
|
56
|
+
for (const name of names) {
|
|
57
|
+
if (!name.endsWith('.json')) {
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
const id = name.slice(0, -'.json'.length);
|
|
61
|
+
if (!/^[A-Za-z0-9._-]+$/.test(id)) {
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
const record = this.get(id);
|
|
65
|
+
if (record) {
|
|
66
|
+
records.push(record);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
records.sort((a, b) => a.nextRunAt.localeCompare(b.nextRunAt));
|
|
70
|
+
return records;
|
|
71
|
+
}
|
|
72
|
+
save(record) {
|
|
73
|
+
try {
|
|
74
|
+
fs.mkdirSync(this.dir, { recursive: true });
|
|
75
|
+
const file = this.fileFor(record.id);
|
|
76
|
+
const payload = JSON.stringify({
|
|
77
|
+
...record,
|
|
78
|
+
updatedAt: new Date(this.now()).toISOString(),
|
|
79
|
+
}, null, 2);
|
|
80
|
+
const temporary = file + '.tmp';
|
|
81
|
+
fs.writeFileSync(temporary, payload, 'utf8');
|
|
82
|
+
try {
|
|
83
|
+
fs.renameSync(temporary, file);
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
fs.writeFileSync(file, payload, 'utf8');
|
|
87
|
+
try {
|
|
88
|
+
fs.unlinkSync(temporary);
|
|
89
|
+
}
|
|
90
|
+
catch { /* best effort */ }
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
// Scheduling is advisory.
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
remove(id) {
|
|
98
|
+
try {
|
|
99
|
+
fs.unlinkSync(this.fileFor(id));
|
|
100
|
+
return true;
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
fileFor(id) {
|
|
107
|
+
if (!/^[A-Za-z0-9._-]+$/.test(id)) {
|
|
108
|
+
throw new Error('非法的 cron id');
|
|
109
|
+
}
|
|
110
|
+
return path.join(this.dir, id + '.json');
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
/** Runs due cron entries as background workers while the process is alive. */
|
|
114
|
+
export class CronScheduler {
|
|
115
|
+
workers;
|
|
116
|
+
store;
|
|
117
|
+
now;
|
|
118
|
+
intervalMs;
|
|
119
|
+
onLog;
|
|
120
|
+
timer = null;
|
|
121
|
+
constructor(options) {
|
|
122
|
+
this.workers = options.workers;
|
|
123
|
+
this.store = options.store ?? new CronStore(options.storeDir);
|
|
124
|
+
this.now = options.now ?? Date.now;
|
|
125
|
+
this.intervalMs = options.intervalMs ?? 60_000;
|
|
126
|
+
this.onLog = options.onLog ?? (() => { });
|
|
127
|
+
}
|
|
128
|
+
getStore() {
|
|
129
|
+
return this.store;
|
|
130
|
+
}
|
|
131
|
+
start() {
|
|
132
|
+
if (this.timer) {
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
this.timer = setInterval(() => this.tick(), this.intervalMs);
|
|
136
|
+
this.timer.unref?.();
|
|
137
|
+
}
|
|
138
|
+
stop() {
|
|
139
|
+
if (this.timer) {
|
|
140
|
+
clearInterval(this.timer);
|
|
141
|
+
this.timer = null;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
tick() {
|
|
145
|
+
const now = this.now();
|
|
146
|
+
const started = [];
|
|
147
|
+
for (const record of this.store.list()) {
|
|
148
|
+
if (!record.enabled) {
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
if (Date.parse(record.nextRunAt) > now) {
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
if (record.lastWorkerId) {
|
|
155
|
+
const worker = this.workers.get(record.lastWorkerId);
|
|
156
|
+
if (worker && (worker.state === 'running' || worker.state === 'queued')) {
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
const worker = this.workers.create({
|
|
161
|
+
prompt: record.prompt,
|
|
162
|
+
name: 'cron:' + record.name,
|
|
163
|
+
cwd: record.cwd,
|
|
164
|
+
allowWrite: record.allowWrite,
|
|
165
|
+
});
|
|
166
|
+
record.lastRunAt = new Date(now).toISOString();
|
|
167
|
+
record.lastWorkerId = worker.id;
|
|
168
|
+
record.nextRunAt = new Date(now + record.everyMinutes * 60_000).toISOString();
|
|
169
|
+
this.store.save(record);
|
|
170
|
+
this.onLog('cron ' + record.name + ' → worker ' + worker.id);
|
|
171
|
+
started.push(record);
|
|
172
|
+
}
|
|
173
|
+
return started;
|
|
174
|
+
}
|
|
175
|
+
runNow(id) {
|
|
176
|
+
const record = this.store.get(id);
|
|
177
|
+
if (!record) {
|
|
178
|
+
throw new Error('找不到 cron:' + id);
|
|
179
|
+
}
|
|
180
|
+
const now = this.now();
|
|
181
|
+
const worker = this.workers.create({
|
|
182
|
+
prompt: record.prompt,
|
|
183
|
+
name: 'cron:' + record.name,
|
|
184
|
+
cwd: record.cwd,
|
|
185
|
+
allowWrite: record.allowWrite,
|
|
186
|
+
});
|
|
187
|
+
record.lastRunAt = new Date(now).toISOString();
|
|
188
|
+
record.lastWorkerId = worker.id;
|
|
189
|
+
record.nextRunAt = new Date(now + record.everyMinutes * 60_000).toISOString();
|
|
190
|
+
this.store.save(record);
|
|
191
|
+
return record;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
export function createCronTools(scheduler) {
|
|
195
|
+
const create = {
|
|
196
|
+
readOnly: true,
|
|
197
|
+
spec: {
|
|
198
|
+
name: 'cron_create',
|
|
199
|
+
description: '创建一个定时后台任务(按分钟间隔),在交互模式或后台服务运行期间自动启动 worker。',
|
|
200
|
+
parameters: {
|
|
201
|
+
type: 'object',
|
|
202
|
+
properties: {
|
|
203
|
+
name: { type: 'string' },
|
|
204
|
+
prompt: { type: 'string' },
|
|
205
|
+
every_minutes: { type: 'number', description: '1-10080 分钟' },
|
|
206
|
+
allow_write: { type: 'boolean' },
|
|
207
|
+
},
|
|
208
|
+
required: ['name', 'prompt', 'every_minutes'],
|
|
209
|
+
additionalProperties: false,
|
|
210
|
+
},
|
|
211
|
+
},
|
|
212
|
+
async run(args, ctx) {
|
|
213
|
+
const allowWrite = args.allow_write === true;
|
|
214
|
+
if (allowWrite && ctx.planMode) {
|
|
215
|
+
throw new Error('计划模式不能创建写入 cron;先提交计划或退出计划模式');
|
|
216
|
+
}
|
|
217
|
+
const record = scheduler.getStore().create({
|
|
218
|
+
name: typeof args.name === 'string' ? args.name : '',
|
|
219
|
+
prompt: typeof args.prompt === 'string' ? args.prompt : '',
|
|
220
|
+
everyMinutes: typeof args.every_minutes === 'number' ? args.every_minutes : 0,
|
|
221
|
+
allowWrite,
|
|
222
|
+
cwd: ctx.cwd,
|
|
223
|
+
});
|
|
224
|
+
return '已创建 cron ' + record.id + '(' + record.name + ',每 '
|
|
225
|
+
+ record.everyMinutes + ' 分钟)';
|
|
226
|
+
},
|
|
227
|
+
};
|
|
228
|
+
const list = {
|
|
229
|
+
readOnly: true,
|
|
230
|
+
spec: {
|
|
231
|
+
name: 'cron_list',
|
|
232
|
+
description: '列出定时任务及下次运行时间。',
|
|
233
|
+
parameters: { type: 'object', properties: {}, additionalProperties: false },
|
|
234
|
+
},
|
|
235
|
+
async run() {
|
|
236
|
+
const records = scheduler.getStore().list();
|
|
237
|
+
if (records.length === 0) {
|
|
238
|
+
return '还没有 cron。';
|
|
239
|
+
}
|
|
240
|
+
return records.map((record) => record.id + ' [' + (record.enabled ? '启用' : '停用') + '] '
|
|
241
|
+
+ record.name + ' · 每 ' + record.everyMinutes + ' 分钟 · 下次 '
|
|
242
|
+
+ record.nextRunAt.slice(0, 16).replace('T', ' ')).join('\n');
|
|
243
|
+
},
|
|
244
|
+
};
|
|
245
|
+
const remove = {
|
|
246
|
+
readOnly: true,
|
|
247
|
+
spec: {
|
|
248
|
+
name: 'cron_delete',
|
|
249
|
+
description: '删除一个定时任务。',
|
|
250
|
+
parameters: {
|
|
251
|
+
type: 'object',
|
|
252
|
+
properties: { id: { type: 'string' } },
|
|
253
|
+
required: ['id'],
|
|
254
|
+
additionalProperties: false,
|
|
255
|
+
},
|
|
256
|
+
},
|
|
257
|
+
async run(args) {
|
|
258
|
+
const id = requiredId(args);
|
|
259
|
+
if (!scheduler.getStore().remove(id)) {
|
|
260
|
+
throw new Error('找不到 cron:' + id);
|
|
261
|
+
}
|
|
262
|
+
return '已删除 cron ' + id;
|
|
263
|
+
},
|
|
264
|
+
};
|
|
265
|
+
const run = {
|
|
266
|
+
readOnly: true,
|
|
267
|
+
spec: {
|
|
268
|
+
name: 'cron_run',
|
|
269
|
+
description: '立即运行一次定时任务。',
|
|
270
|
+
parameters: {
|
|
271
|
+
type: 'object',
|
|
272
|
+
properties: { id: { type: 'string' } },
|
|
273
|
+
required: ['id'],
|
|
274
|
+
additionalProperties: false,
|
|
275
|
+
},
|
|
276
|
+
},
|
|
277
|
+
async run(args, ctx) {
|
|
278
|
+
const id = requiredId(args);
|
|
279
|
+
const record = scheduler.getStore().get(id);
|
|
280
|
+
if (!record) {
|
|
281
|
+
throw new Error('找不到 cron:' + id);
|
|
282
|
+
}
|
|
283
|
+
if (record.allowWrite && ctx.planMode) {
|
|
284
|
+
throw new Error('计划模式不能运行写入 cron;先提交计划或退出计划模式');
|
|
285
|
+
}
|
|
286
|
+
scheduler.runNow(id);
|
|
287
|
+
return '已触发 cron ' + id;
|
|
288
|
+
},
|
|
289
|
+
};
|
|
290
|
+
return [create, list, remove, run];
|
|
291
|
+
}
|
|
292
|
+
function clampMinutes(value) {
|
|
293
|
+
if (!Number.isFinite(value)) {
|
|
294
|
+
throw new Error('every_minutes 必须是数字');
|
|
295
|
+
}
|
|
296
|
+
return Math.min(10_080, Math.max(1, Math.floor(value)));
|
|
297
|
+
}
|
|
298
|
+
function requiredId(args) {
|
|
299
|
+
const id = typeof args.id === 'string' ? args.id.trim() : '';
|
|
300
|
+
if (id.length === 0) {
|
|
301
|
+
throw new Error('缺少 id 参数');
|
|
302
|
+
}
|
|
303
|
+
return id;
|
|
304
|
+
}
|
|
305
|
+
function parseRecord(id, raw) {
|
|
306
|
+
if (typeof raw.name !== 'string' || typeof raw.prompt !== 'string'
|
|
307
|
+
|| typeof raw.everyMinutes !== 'number' || typeof raw.enabled !== 'boolean'
|
|
308
|
+
|| typeof raw.allowWrite !== 'boolean' || typeof raw.nextRunAt !== 'string'
|
|
309
|
+
|| typeof raw.createdAt !== 'string' || typeof raw.updatedAt !== 'string') {
|
|
310
|
+
return null;
|
|
311
|
+
}
|
|
312
|
+
return {
|
|
313
|
+
id,
|
|
314
|
+
name: raw.name,
|
|
315
|
+
prompt: raw.prompt,
|
|
316
|
+
everyMinutes: raw.everyMinutes,
|
|
317
|
+
enabled: raw.enabled,
|
|
318
|
+
allowWrite: raw.allowWrite,
|
|
319
|
+
cwd: typeof raw.cwd === 'string' ? raw.cwd : undefined,
|
|
320
|
+
lastRunAt: typeof raw.lastRunAt === 'string' ? raw.lastRunAt : undefined,
|
|
321
|
+
nextRunAt: raw.nextRunAt,
|
|
322
|
+
lastWorkerId: typeof raw.lastWorkerId === 'string' ? raw.lastWorkerId : undefined,
|
|
323
|
+
createdAt: raw.createdAt,
|
|
324
|
+
updatedAt: raw.updatedAt,
|
|
325
|
+
};
|
|
326
|
+
}
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
const DEFAULT_TIMEOUT_MS = 10_000;
|
|
3
|
+
/** Tool-name matcher: exact, `*` wildcard, or comma/pipe separated alternatives. */
|
|
4
|
+
export function hookMatches(matcher, toolName) {
|
|
5
|
+
if (matcher === undefined || matcher.trim().length === 0) {
|
|
6
|
+
return true;
|
|
7
|
+
}
|
|
8
|
+
return matcher
|
|
9
|
+
.split(/[,|]/)
|
|
10
|
+
.map((part) => part.trim())
|
|
11
|
+
.filter((part) => part.length > 0)
|
|
12
|
+
.some((part) => globToRegExp(part).test(toolName));
|
|
13
|
+
}
|
|
14
|
+
export async function runHookEvent(options) {
|
|
15
|
+
const outcome = {
|
|
16
|
+
denied: false,
|
|
17
|
+
failed: false,
|
|
18
|
+
cancelled: false,
|
|
19
|
+
messages: [],
|
|
20
|
+
};
|
|
21
|
+
const commands = commandsFor(options.config, options.event, options.toolName);
|
|
22
|
+
if (commands.length === 0) {
|
|
23
|
+
return outcome;
|
|
24
|
+
}
|
|
25
|
+
if (options.signal?.aborted) {
|
|
26
|
+
outcome.cancelled = true;
|
|
27
|
+
outcome.messages.push('hook 已取消');
|
|
28
|
+
return outcome;
|
|
29
|
+
}
|
|
30
|
+
const payload = JSON.stringify({
|
|
31
|
+
event: options.event,
|
|
32
|
+
tool_name: options.toolName,
|
|
33
|
+
tool_input: parseInput(options.toolInput),
|
|
34
|
+
...(options.toolOutput === undefined ? {} : { tool_output: options.toolOutput }),
|
|
35
|
+
is_error: Boolean(options.isError),
|
|
36
|
+
});
|
|
37
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
38
|
+
for (const command of commands) {
|
|
39
|
+
const result = await runCommand(command.command, payload, timeoutMs, options.signal, command.cwd);
|
|
40
|
+
if (result.cancelled) {
|
|
41
|
+
outcome.cancelled = true;
|
|
42
|
+
outcome.messages.push('hook 已取消');
|
|
43
|
+
break;
|
|
44
|
+
}
|
|
45
|
+
if (result.timedOut) {
|
|
46
|
+
outcome.failed = true;
|
|
47
|
+
outcome.messages.push('hook 超时(' + timeoutMs + 'ms):' + command.command);
|
|
48
|
+
break;
|
|
49
|
+
}
|
|
50
|
+
if (result.code !== 0) {
|
|
51
|
+
outcome.failed = true;
|
|
52
|
+
const detail = result.stderr.trim() || result.stdout.trim() || 'exit ' + String(result.code);
|
|
53
|
+
outcome.messages.push('hook 失败:' + detail);
|
|
54
|
+
break;
|
|
55
|
+
}
|
|
56
|
+
const parsed = parseHookOutput(result.stdout);
|
|
57
|
+
outcome.messages.push(...parsed.messages);
|
|
58
|
+
if (parsed.denied) {
|
|
59
|
+
outcome.denied = true;
|
|
60
|
+
}
|
|
61
|
+
if (parsed.permission) {
|
|
62
|
+
outcome.permission = parsed.permission;
|
|
63
|
+
}
|
|
64
|
+
if (parsed.permissionReason) {
|
|
65
|
+
outcome.permissionReason = parsed.permissionReason;
|
|
66
|
+
}
|
|
67
|
+
if (parsed.updatedInput !== undefined) {
|
|
68
|
+
outcome.updatedInput = parsed.updatedInput;
|
|
69
|
+
}
|
|
70
|
+
if (outcome.denied) {
|
|
71
|
+
break;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return outcome;
|
|
75
|
+
}
|
|
76
|
+
function commandsFor(config, event, toolName) {
|
|
77
|
+
const entries = config?.[event] ?? [];
|
|
78
|
+
const commands = [];
|
|
79
|
+
for (const entry of entries) {
|
|
80
|
+
if (typeof entry === 'string') {
|
|
81
|
+
commands.push({ type: 'command', command: entry });
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
if (!hookMatches(entry.matcher, toolName)) {
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
for (const hook of entry.hooks) {
|
|
88
|
+
if (hook.type === undefined || hook.type === 'command') {
|
|
89
|
+
commands.push(hook);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return commands;
|
|
94
|
+
}
|
|
95
|
+
function parseHookOutput(stdout) {
|
|
96
|
+
const trimmed = stdout.trim();
|
|
97
|
+
const parsed = { denied: false, messages: [] };
|
|
98
|
+
if (trimmed.length === 0) {
|
|
99
|
+
return parsed;
|
|
100
|
+
}
|
|
101
|
+
let root;
|
|
102
|
+
try {
|
|
103
|
+
root = JSON.parse(trimmed);
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
parsed.messages.push(trimmed);
|
|
107
|
+
return parsed;
|
|
108
|
+
}
|
|
109
|
+
if (root === null || typeof root !== 'object' || Array.isArray(root)) {
|
|
110
|
+
parsed.messages.push(trimmed);
|
|
111
|
+
return parsed;
|
|
112
|
+
}
|
|
113
|
+
const record = root;
|
|
114
|
+
if (record.continue === false || record.decision === 'block') {
|
|
115
|
+
parsed.denied = true;
|
|
116
|
+
}
|
|
117
|
+
if (typeof record.systemMessage === 'string') {
|
|
118
|
+
parsed.messages.push(record.systemMessage);
|
|
119
|
+
}
|
|
120
|
+
if (typeof record.reason === 'string') {
|
|
121
|
+
parsed.messages.push(record.reason);
|
|
122
|
+
}
|
|
123
|
+
const specific = record.hookSpecificOutput;
|
|
124
|
+
if (specific !== null && typeof specific === 'object') {
|
|
125
|
+
const output = specific;
|
|
126
|
+
if (typeof output.additionalContext === 'string') {
|
|
127
|
+
parsed.messages.push(output.additionalContext);
|
|
128
|
+
}
|
|
129
|
+
if (output.permissionDecision === 'allow'
|
|
130
|
+
|| output.permissionDecision === 'deny'
|
|
131
|
+
|| output.permissionDecision === 'ask') {
|
|
132
|
+
parsed.permission = output.permissionDecision;
|
|
133
|
+
}
|
|
134
|
+
if (typeof output.permissionDecisionReason === 'string') {
|
|
135
|
+
parsed.permissionReason = output.permissionDecisionReason;
|
|
136
|
+
}
|
|
137
|
+
if (output.updatedInput !== undefined) {
|
|
138
|
+
parsed.updatedInput = JSON.stringify(output.updatedInput);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
if (parsed.messages.length === 0 && !parsed.denied && !parsed.permission) {
|
|
142
|
+
parsed.messages.push(trimmed);
|
|
143
|
+
}
|
|
144
|
+
return parsed;
|
|
145
|
+
}
|
|
146
|
+
function parseInput(raw) {
|
|
147
|
+
try {
|
|
148
|
+
const value = JSON.parse(raw);
|
|
149
|
+
return value !== null && typeof value === 'object' ? value : raw;
|
|
150
|
+
}
|
|
151
|
+
catch {
|
|
152
|
+
return raw;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
function runCommand(command, payload, timeoutMs, signal, cwd) {
|
|
156
|
+
return new Promise((resolve) => {
|
|
157
|
+
const child = spawn(command, {
|
|
158
|
+
shell: true,
|
|
159
|
+
windowsHide: true,
|
|
160
|
+
cwd,
|
|
161
|
+
env: { ...process.env, MINKO: '1' },
|
|
162
|
+
});
|
|
163
|
+
let stdout = '';
|
|
164
|
+
let stderr = '';
|
|
165
|
+
let settled = false;
|
|
166
|
+
let timer;
|
|
167
|
+
const finish = (result) => {
|
|
168
|
+
if (settled) {
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
settled = true;
|
|
172
|
+
if (timer) {
|
|
173
|
+
clearTimeout(timer);
|
|
174
|
+
}
|
|
175
|
+
if (signal) {
|
|
176
|
+
signal.removeEventListener('abort', onAbort);
|
|
177
|
+
}
|
|
178
|
+
resolve(result);
|
|
179
|
+
};
|
|
180
|
+
const onAbort = () => {
|
|
181
|
+
child.kill();
|
|
182
|
+
finish({ code: null, stdout, stderr, timedOut: false, cancelled: true });
|
|
183
|
+
};
|
|
184
|
+
if (signal) {
|
|
185
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
186
|
+
}
|
|
187
|
+
timer = setTimeout(() => {
|
|
188
|
+
child.kill();
|
|
189
|
+
finish({ code: null, stdout, stderr, timedOut: true, cancelled: false });
|
|
190
|
+
}, timeoutMs);
|
|
191
|
+
child.stdout.on('data', (chunk) => { stdout += chunk.toString('utf8'); });
|
|
192
|
+
child.stderr.on('data', (chunk) => { stderr += chunk.toString('utf8'); });
|
|
193
|
+
// Hooks are free to ignore stdin; a broken pipe must not crash the agent.
|
|
194
|
+
child.stdin.on('error', () => { });
|
|
195
|
+
child.on('error', (error) => {
|
|
196
|
+
stderr += error.message;
|
|
197
|
+
finish({ code: null, stdout, stderr, timedOut: false, cancelled: false });
|
|
198
|
+
});
|
|
199
|
+
child.on('close', (code) => {
|
|
200
|
+
finish({ code, stdout, stderr, timedOut: false, cancelled: false });
|
|
201
|
+
});
|
|
202
|
+
child.stdin.end(payload);
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
function globToRegExp(pattern) {
|
|
206
|
+
const escaped = pattern
|
|
207
|
+
.replace(/[.+^${}()|[\]\\]/g, '\\$&')
|
|
208
|
+
.replace(/\*/g, '.*')
|
|
209
|
+
.replace(/\?/g, '.');
|
|
210
|
+
return new RegExp('^' + escaped + '$', 'i');
|
|
211
|
+
}
|