@hazeljs/cli 1.0.6 → 2.0.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/@template-ai-native/README.md +19 -0
- package/@template-ai-native/src/examples/skillgate.example.ts +29 -0
- package/cli-manifest.json +1 -1
- package/dist/commands/agent.d.ts +3 -1
- package/dist/commands/agent.js +356 -2
- package/dist/commands/agent.test.d.ts +1 -0
- package/dist/commands/agent.test.js +14 -0
- package/dist/commands/skillgate.d.ts +6 -0
- package/dist/commands/skillgate.js +147 -0
- package/dist/index.js +2 -0
- package/dist/utils/packages-registry.js +19 -0
- package/package.json +12 -7
|
@@ -36,6 +36,25 @@ A complete AI-native backend application with HazelJS, featuring:
|
|
|
36
36
|
- Inspector: http://localhost:3000/\_\_hazel
|
|
37
37
|
- Health: http://localhost:3000/health
|
|
38
38
|
|
|
39
|
+
## Skillgate (optional)
|
|
40
|
+
|
|
41
|
+
Turn selected REST controllers into governed agent skills:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
npm install @hazeljs/skillgate @hazeljs/swagger
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
See `src/examples/skillgate.example.ts` and https://hazeljs.ai/docs/guides/skillgate
|
|
48
|
+
|
|
49
|
+
Tag controllers with `@ApiTags('agent')` or `@AgentSkill`, then:
|
|
50
|
+
|
|
51
|
+
```ts
|
|
52
|
+
Skillgate.fromModule(AppModule, { invoke: { baseUrl: 'http://127.0.0.1:3000' } }).register(
|
|
53
|
+
registry,
|
|
54
|
+
'api-concierge'
|
|
55
|
+
);
|
|
56
|
+
```
|
|
57
|
+
|
|
39
58
|
## Available Endpoints
|
|
40
59
|
|
|
41
60
|
### AI Chat
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Optional Skillgate example (not imported by AppModule).
|
|
3
|
+
*
|
|
4
|
+
* Install: npm install @hazeljs/skillgate @hazeljs/swagger
|
|
5
|
+
* Then wire `registerApiSkills()` from your bootstrap after the HTTP server listens.
|
|
6
|
+
*
|
|
7
|
+
* Docs: https://hazeljs.ai/docs/guides/skillgate
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/*
|
|
11
|
+
import { Skillgate } from '@hazeljs/skillgate';
|
|
12
|
+
import { ToolRegistry } from '@hazeljs/agent';
|
|
13
|
+
import { AppModule } from '../app.module';
|
|
14
|
+
|
|
15
|
+
export function registerApiSkills(registry: ToolRegistry = new ToolRegistry()) {
|
|
16
|
+
const gate = Skillgate.fromModule(AppModule, {
|
|
17
|
+
include: { tags: ['agent'] },
|
|
18
|
+
swagger: {
|
|
19
|
+
title: 'AI-native API',
|
|
20
|
+
servers: [{ url: process.env.API_BASE_URL || 'http://127.0.0.1:3000' }],
|
|
21
|
+
},
|
|
22
|
+
invoke: { baseUrl: process.env.API_BASE_URL || 'http://127.0.0.1:3000' },
|
|
23
|
+
});
|
|
24
|
+
gate.register(registry, 'api-concierge');
|
|
25
|
+
return { gate, registry };
|
|
26
|
+
}
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
export {};
|
package/cli-manifest.json
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
"description": "Machine-readable manifest of all CLI commands and options for LLM agent tool-use",
|
|
5
5
|
"cli": {
|
|
6
6
|
"name": "hazel",
|
|
7
|
-
"version": "
|
|
7
|
+
"version": "2.0.0",
|
|
8
8
|
"description": "CLI for generating HazelJS components and applications"
|
|
9
9
|
},
|
|
10
10
|
"commands": [
|
package/dist/commands/agent.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
2
|
/**
|
|
3
3
|
* `hazel agent install <file.dna.json>` — validate / print marketplace install plan.
|
|
4
|
-
*
|
|
4
|
+
* `hazel agent run` — live execute from DNA (AOS-011).
|
|
5
|
+
* `hazel agent logs` / `doctor` — timeline + environment checks.
|
|
6
|
+
* `hazel agent runs list|inspect|cancel|resume|approve` — durable store ops.
|
|
5
7
|
*/
|
|
6
8
|
export declare function registerAgentCommand(program: Command): void;
|
package/dist/commands/agent.js
CHANGED
|
@@ -35,13 +35,21 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.registerAgentCommand = registerAgentCommand;
|
|
37
37
|
const fs = __importStar(require("fs"));
|
|
38
|
+
const os = __importStar(require("os"));
|
|
38
39
|
const path = __importStar(require("path"));
|
|
40
|
+
const DEFAULT_RUN_STORE = path.join('.hazel', 'agent-runs.json');
|
|
41
|
+
const DEFAULT_DURABLE_DIR = path.join('.hazel', 'runs');
|
|
42
|
+
const DEFAULT_TIMELINE = path.join('.hazel', 'runs', 'timeline.jsonl');
|
|
39
43
|
/**
|
|
40
44
|
* `hazel agent install <file.dna.json>` — validate / print marketplace install plan.
|
|
41
|
-
*
|
|
45
|
+
* `hazel agent run` — live execute from DNA (AOS-011).
|
|
46
|
+
* `hazel agent logs` / `doctor` — timeline + environment checks.
|
|
47
|
+
* `hazel agent runs list|inspect|cancel|resume|approve` — durable store ops.
|
|
42
48
|
*/
|
|
43
49
|
function registerAgentCommand(program) {
|
|
44
|
-
const agent = program
|
|
50
|
+
const agent = program
|
|
51
|
+
.command('agent')
|
|
52
|
+
.description('Agent OS DNA / runtime / marketplace helpers');
|
|
45
53
|
agent
|
|
46
54
|
.command('install')
|
|
47
55
|
.description('Validate a .dna / marketplace JSON package and print install plan')
|
|
@@ -91,4 +99,350 @@ function registerAgentCommand(program) {
|
|
|
91
99
|
process.exitCode = 1;
|
|
92
100
|
}
|
|
93
101
|
});
|
|
102
|
+
agent
|
|
103
|
+
.command('run')
|
|
104
|
+
.description('Execute an agent from DNA / marketplace package (live CLI run, AOS-011)')
|
|
105
|
+
.argument('<file>', 'Path to .dna.json or marketplace package JSON')
|
|
106
|
+
.argument('[input...]', 'User input (default: hello)')
|
|
107
|
+
.option('--dir <path>', 'Durable store directory', DEFAULT_DURABLE_DIR)
|
|
108
|
+
.option('--mock', 'Use offline mock LLM (no API key)')
|
|
109
|
+
.option('--model <model>', 'Model id for HTTP LLM', process.env.HAZEL_AGENT_MODEL ?? 'gpt-4o-mini')
|
|
110
|
+
.option('--base-url <url>', 'OpenAI-compatible base URL', process.env.OPENAI_BASE_URL)
|
|
111
|
+
.option('--api-key <key>', 'API key (default: OPENAI_API_KEY)')
|
|
112
|
+
.option('--worker-id <id>', 'Worker id for run leases', `cli-${os.hostname()}`)
|
|
113
|
+
.option('--max-steps <n>', 'Max agent steps', '8')
|
|
114
|
+
.option('--json', 'Print full execution result JSON')
|
|
115
|
+
.action(async (file, inputParts, opts) => {
|
|
116
|
+
try {
|
|
117
|
+
const { bootstrapRuntimeFromDna, createHttpLlmProvider, createMockLlmProvider } = await Promise.resolve().then(() => __importStar(require('@hazeljs/agent')));
|
|
118
|
+
const dnaPath = path.resolve(process.cwd(), file);
|
|
119
|
+
if (!fs.existsSync(dnaPath)) {
|
|
120
|
+
throw new Error(`DNA file not found: ${dnaPath}`);
|
|
121
|
+
}
|
|
122
|
+
const input = inputParts.length ? inputParts.join(' ') : 'hello';
|
|
123
|
+
const storeDir = path.resolve(process.cwd(), opts.dir);
|
|
124
|
+
const apiKey = opts.apiKey ?? process.env.OPENAI_API_KEY;
|
|
125
|
+
const llm = opts.mock || !apiKey
|
|
126
|
+
? createMockLlmProvider(opts.mock
|
|
127
|
+
? 'Mock reply from hazel agent run.'
|
|
128
|
+
: 'No OPENAI_API_KEY — mock reply. Pass --mock to silence this, or set a key.')
|
|
129
|
+
: createHttpLlmProvider({
|
|
130
|
+
apiKey,
|
|
131
|
+
baseUrl: opts.baseUrl,
|
|
132
|
+
model: opts.model,
|
|
133
|
+
});
|
|
134
|
+
const { runtime, dna, store, timelinePath } = bootstrapRuntimeFromDna(dnaPath, {
|
|
135
|
+
llmProvider: llm,
|
|
136
|
+
storeDir,
|
|
137
|
+
durableSuspend: true,
|
|
138
|
+
workerId: opts.workerId,
|
|
139
|
+
stubTools: true,
|
|
140
|
+
});
|
|
141
|
+
const result = await runtime.execute(dna.name, input, {
|
|
142
|
+
maxSteps: Number(opts.maxSteps) || 8,
|
|
143
|
+
});
|
|
144
|
+
const run = store ? await store.runRepository.get(result.executionId) : undefined;
|
|
145
|
+
const summary = {
|
|
146
|
+
agent: dna.name,
|
|
147
|
+
executionId: result.executionId,
|
|
148
|
+
state: result.state,
|
|
149
|
+
response: result.response,
|
|
150
|
+
runStatus: run?.status,
|
|
151
|
+
storeDir,
|
|
152
|
+
timelinePath,
|
|
153
|
+
llm: opts.mock || !apiKey ? 'mock' : 'http',
|
|
154
|
+
};
|
|
155
|
+
// eslint-disable-next-line no-console
|
|
156
|
+
console.log(JSON.stringify(opts.json ? { ...summary, result, run } : summary, null, 2));
|
|
157
|
+
}
|
|
158
|
+
catch (e) {
|
|
159
|
+
// eslint-disable-next-line no-console
|
|
160
|
+
console.error(e);
|
|
161
|
+
process.exitCode = 1;
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
agent
|
|
165
|
+
.command('logs')
|
|
166
|
+
.description('Show AgentRun timeline JSONL (optionally filter by run id)')
|
|
167
|
+
.option('--timeline <path>', 'Timeline JSONL path', DEFAULT_TIMELINE)
|
|
168
|
+
.option('--run <runId>', 'Filter by execution / run id')
|
|
169
|
+
.option('--agent <name>', 'Filter by agent name')
|
|
170
|
+
.option('--follow', 'Tail new lines (poll)')
|
|
171
|
+
.option('--interval <ms>', 'Follow poll interval', '1000')
|
|
172
|
+
.action(async (opts) => {
|
|
173
|
+
try {
|
|
174
|
+
const { FileTimelineStore } = await Promise.resolve().then(() => __importStar(require('@hazeljs/agent')));
|
|
175
|
+
const timelinePath = path.resolve(process.cwd(), opts.timeline);
|
|
176
|
+
const store = new FileTimelineStore(timelinePath);
|
|
177
|
+
const print = () => {
|
|
178
|
+
const steps = store.load({
|
|
179
|
+
executionId: opts.run,
|
|
180
|
+
agentName: opts.agent,
|
|
181
|
+
});
|
|
182
|
+
// eslint-disable-next-line no-console
|
|
183
|
+
console.log(JSON.stringify(steps, null, 2));
|
|
184
|
+
};
|
|
185
|
+
if (!opts.follow) {
|
|
186
|
+
print();
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
let lastSize = fs.existsSync(timelinePath) ? fs.statSync(timelinePath).size : 0;
|
|
190
|
+
print();
|
|
191
|
+
const ms = Math.max(200, Number(opts.interval) || 1000);
|
|
192
|
+
// eslint-disable-next-line no-console
|
|
193
|
+
console.error(`Following ${timelinePath} every ${ms}ms (Ctrl+C to stop)…`);
|
|
194
|
+
setInterval(() => {
|
|
195
|
+
if (!fs.existsSync(timelinePath))
|
|
196
|
+
return;
|
|
197
|
+
const size = fs.statSync(timelinePath).size;
|
|
198
|
+
if (size !== lastSize) {
|
|
199
|
+
lastSize = size;
|
|
200
|
+
print();
|
|
201
|
+
}
|
|
202
|
+
}, ms);
|
|
203
|
+
}
|
|
204
|
+
catch (e) {
|
|
205
|
+
// eslint-disable-next-line no-console
|
|
206
|
+
console.error(e);
|
|
207
|
+
process.exitCode = 1;
|
|
208
|
+
}
|
|
209
|
+
});
|
|
210
|
+
agent
|
|
211
|
+
.command('doctor')
|
|
212
|
+
.description('Check Agent OS CLI environment (peers, store paths, LLM key)')
|
|
213
|
+
.option('--dir <path>', 'Expected durable store directory', DEFAULT_DURABLE_DIR)
|
|
214
|
+
.action(async (opts) => {
|
|
215
|
+
try {
|
|
216
|
+
const cwd = process.cwd();
|
|
217
|
+
const storeDir = path.resolve(cwd, opts.dir);
|
|
218
|
+
const checks = [];
|
|
219
|
+
checks.push({
|
|
220
|
+
ok: true,
|
|
221
|
+
name: 'node',
|
|
222
|
+
detail: process.version,
|
|
223
|
+
});
|
|
224
|
+
try {
|
|
225
|
+
await Promise.resolve().then(() => __importStar(require('@hazeljs/agent')));
|
|
226
|
+
checks.push({ ok: true, name: '@hazeljs/agent', detail: 'resolvable' });
|
|
227
|
+
}
|
|
228
|
+
catch (e) {
|
|
229
|
+
checks.push({
|
|
230
|
+
ok: false,
|
|
231
|
+
name: '@hazeljs/agent',
|
|
232
|
+
detail: e instanceof Error ? e.message : String(e),
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
const pkgPath = path.join(cwd, 'package.json');
|
|
236
|
+
checks.push({
|
|
237
|
+
ok: fs.existsSync(pkgPath),
|
|
238
|
+
name: 'package.json',
|
|
239
|
+
detail: fs.existsSync(pkgPath) ? pkgPath : 'missing in cwd',
|
|
240
|
+
});
|
|
241
|
+
checks.push({
|
|
242
|
+
ok: true,
|
|
243
|
+
name: 'durableStore',
|
|
244
|
+
detail: fs.existsSync(storeDir)
|
|
245
|
+
? `exists: ${storeDir}`
|
|
246
|
+
: `will be created on run: ${storeDir}`,
|
|
247
|
+
});
|
|
248
|
+
const key = process.env.OPENAI_API_KEY;
|
|
249
|
+
checks.push({
|
|
250
|
+
ok: Boolean(key),
|
|
251
|
+
name: 'OPENAI_API_KEY',
|
|
252
|
+
detail: key ? 'set (http LLM available)' : 'unset — use --mock for offline run',
|
|
253
|
+
});
|
|
254
|
+
const failed = checks.filter((c) => !c.ok);
|
|
255
|
+
// eslint-disable-next-line no-console
|
|
256
|
+
console.log(JSON.stringify({
|
|
257
|
+
ok: failed.length === 0 || (failed.length === 1 && failed[0].name === 'OPENAI_API_KEY'),
|
|
258
|
+
checks,
|
|
259
|
+
hints: [
|
|
260
|
+
'hazel agent run ./agent.dna.json "hello" --mock',
|
|
261
|
+
'hazel agent runs list --dir .hazel/runs',
|
|
262
|
+
'hazel agent logs --timeline .hazel/runs/timeline.jsonl',
|
|
263
|
+
],
|
|
264
|
+
}, null, 2));
|
|
265
|
+
if (failed.some((c) => c.name === '@hazeljs/agent'))
|
|
266
|
+
process.exitCode = 1;
|
|
267
|
+
}
|
|
268
|
+
catch (e) {
|
|
269
|
+
// eslint-disable-next-line no-console
|
|
270
|
+
console.error(e);
|
|
271
|
+
process.exitCode = 1;
|
|
272
|
+
}
|
|
273
|
+
});
|
|
274
|
+
const runs = agent.command('runs').description('Inspect durable AgentRun records (file store)');
|
|
275
|
+
runs
|
|
276
|
+
.command('list')
|
|
277
|
+
.description('List AgentRun records from a FileAgentRunRepository JSON store')
|
|
278
|
+
.option('--store <path>', 'Path to runs JSON', DEFAULT_RUN_STORE)
|
|
279
|
+
.option('--dir <path>', 'Durable store directory (uses runs.json inside)')
|
|
280
|
+
.option('--agent <name>', 'Filter by agent name')
|
|
281
|
+
.option('--status <status>', 'Filter by status')
|
|
282
|
+
.action(async (opts) => {
|
|
283
|
+
try {
|
|
284
|
+
const { FileAgentRunRepository, AgentRunStatus, createDurableRunStore } = await Promise.resolve().then(() => __importStar(require('@hazeljs/agent')));
|
|
285
|
+
const repo = opts.dir
|
|
286
|
+
? createDurableRunStore(path.resolve(process.cwd(), opts.dir))
|
|
287
|
+
.runRepository
|
|
288
|
+
: new FileAgentRunRepository(path.resolve(process.cwd(), opts.store));
|
|
289
|
+
const filter = {};
|
|
290
|
+
if (opts.agent)
|
|
291
|
+
filter.agentName = opts.agent;
|
|
292
|
+
if (opts.status) {
|
|
293
|
+
const values = Object.values(AgentRunStatus);
|
|
294
|
+
if (!values.includes(opts.status)) {
|
|
295
|
+
throw new Error(`Unknown status "${opts.status}". Expected one of: ${values.join(', ')}`);
|
|
296
|
+
}
|
|
297
|
+
filter.status = opts.status;
|
|
298
|
+
}
|
|
299
|
+
const list = await repo.list(filter);
|
|
300
|
+
// eslint-disable-next-line no-console
|
|
301
|
+
console.log(JSON.stringify(list.map((r) => ({
|
|
302
|
+
id: r.id,
|
|
303
|
+
agentName: r.agentName,
|
|
304
|
+
status: r.status,
|
|
305
|
+
leaseOwner: r.leaseOwner,
|
|
306
|
+
updatedAt: r.updatedAt,
|
|
307
|
+
})), null, 2));
|
|
308
|
+
}
|
|
309
|
+
catch (e) {
|
|
310
|
+
// eslint-disable-next-line no-console
|
|
311
|
+
console.error(e);
|
|
312
|
+
process.exitCode = 1;
|
|
313
|
+
}
|
|
314
|
+
});
|
|
315
|
+
runs
|
|
316
|
+
.command('inspect')
|
|
317
|
+
.description('Show one AgentRun by id')
|
|
318
|
+
.argument('<runId>', 'AgentRun / execution id')
|
|
319
|
+
.option('--store <path>', 'Path to runs JSON', DEFAULT_RUN_STORE)
|
|
320
|
+
.option('--dir <path>', 'Durable store directory')
|
|
321
|
+
.action(async (runId, opts) => {
|
|
322
|
+
try {
|
|
323
|
+
const { FileAgentRunRepository, createDurableRunStore } = await Promise.resolve().then(() => __importStar(require('@hazeljs/agent')));
|
|
324
|
+
const repo = opts.dir
|
|
325
|
+
? createDurableRunStore(path.resolve(process.cwd(), opts.dir)).runRepository
|
|
326
|
+
: new FileAgentRunRepository(path.resolve(process.cwd(), opts.store));
|
|
327
|
+
const run = await repo.get(runId);
|
|
328
|
+
if (!run) {
|
|
329
|
+
// eslint-disable-next-line no-console
|
|
330
|
+
console.error(`Run not found: ${runId}`);
|
|
331
|
+
process.exitCode = 1;
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
// eslint-disable-next-line no-console
|
|
335
|
+
console.log(JSON.stringify(run, null, 2));
|
|
336
|
+
}
|
|
337
|
+
catch (e) {
|
|
338
|
+
// eslint-disable-next-line no-console
|
|
339
|
+
console.error(e);
|
|
340
|
+
process.exitCode = 1;
|
|
341
|
+
}
|
|
342
|
+
});
|
|
343
|
+
runs
|
|
344
|
+
.command('cancel')
|
|
345
|
+
.description('Mark an AgentRun CANCELLED in the file store (does not abort a live worker)')
|
|
346
|
+
.argument('<runId>', 'AgentRun / execution id')
|
|
347
|
+
.option('--store <path>', 'Path to runs JSON', DEFAULT_RUN_STORE)
|
|
348
|
+
.option('--dir <path>', 'Durable store directory')
|
|
349
|
+
.action(async (runId, opts) => {
|
|
350
|
+
try {
|
|
351
|
+
const { FileAgentRunRepository, AgentRunStatus, createDurableRunStore } = await Promise.resolve().then(() => __importStar(require('@hazeljs/agent')));
|
|
352
|
+
const repo = opts.dir
|
|
353
|
+
? createDurableRunStore(path.resolve(process.cwd(), opts.dir)).runRepository
|
|
354
|
+
: new FileAgentRunRepository(path.resolve(process.cwd(), opts.store));
|
|
355
|
+
const run = await repo.updateStatus(runId, AgentRunStatus.CANCELLED, {
|
|
356
|
+
error: { message: 'Cancelled via hazel agent runs cancel' },
|
|
357
|
+
});
|
|
358
|
+
// eslint-disable-next-line no-console
|
|
359
|
+
console.log(JSON.stringify({ id: run.id, status: run.status }, null, 2));
|
|
360
|
+
}
|
|
361
|
+
catch (e) {
|
|
362
|
+
// eslint-disable-next-line no-console
|
|
363
|
+
console.error(e);
|
|
364
|
+
process.exitCode = 1;
|
|
365
|
+
}
|
|
366
|
+
});
|
|
367
|
+
const resumeAction = async (runId, opts) => {
|
|
368
|
+
try {
|
|
369
|
+
const approved = opts.approve !== false && !opts.reject;
|
|
370
|
+
const { createDurableRunStore, FileAgentRunRepository, FileHumanTaskService, FileCheckpointService, } = await Promise.resolve().then(() => __importStar(require('@hazeljs/agent')));
|
|
371
|
+
const storePath = path.resolve(process.cwd(), opts.store);
|
|
372
|
+
const isDir = opts.dir || storePath.endsWith('.hazel') || !storePath.endsWith('.json');
|
|
373
|
+
let humanTasks;
|
|
374
|
+
let runsRepo;
|
|
375
|
+
if (opts.dir) {
|
|
376
|
+
const store = createDurableRunStore(path.resolve(process.cwd(), opts.dir));
|
|
377
|
+
humanTasks = store.humanTaskService;
|
|
378
|
+
runsRepo = store.runRepository;
|
|
379
|
+
}
|
|
380
|
+
else if (isDir && !storePath.endsWith('.json')) {
|
|
381
|
+
const store = createDurableRunStore(storePath);
|
|
382
|
+
humanTasks = store.humanTaskService;
|
|
383
|
+
runsRepo = store.runRepository;
|
|
384
|
+
}
|
|
385
|
+
else {
|
|
386
|
+
const dir = path.dirname(storePath);
|
|
387
|
+
runsRepo = new FileAgentRunRepository(storePath);
|
|
388
|
+
humanTasks = new FileHumanTaskService(path.join(dir, 'human-tasks.json'));
|
|
389
|
+
void new FileCheckpointService(path.join(dir, 'checkpoints.json'));
|
|
390
|
+
}
|
|
391
|
+
const run = await runsRepo.get(runId);
|
|
392
|
+
if (!run) {
|
|
393
|
+
// eslint-disable-next-line no-console
|
|
394
|
+
console.error(`Run not found: ${runId}`);
|
|
395
|
+
process.exitCode = 1;
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
398
|
+
const tasks = await humanTasks.listByRun(runId);
|
|
399
|
+
const pending = tasks.find((t) => t.status === 'pending');
|
|
400
|
+
if (pending) {
|
|
401
|
+
await humanTasks.resolve(pending.id, approved ? 'approved' : 'rejected', opts.by);
|
|
402
|
+
}
|
|
403
|
+
await runsRepo.updateStatus(runId, run.status, {
|
|
404
|
+
metadata: {
|
|
405
|
+
...run.metadata,
|
|
406
|
+
cliDecision: {
|
|
407
|
+
approved,
|
|
408
|
+
by: opts.by,
|
|
409
|
+
at: new Date().toISOString(),
|
|
410
|
+
},
|
|
411
|
+
},
|
|
412
|
+
});
|
|
413
|
+
// eslint-disable-next-line no-console
|
|
414
|
+
console.log(JSON.stringify({
|
|
415
|
+
runId,
|
|
416
|
+
decision: approved ? 'approved' : 'rejected',
|
|
417
|
+
by: opts.by,
|
|
418
|
+
humanTaskId: pending?.id,
|
|
419
|
+
note: 'Human task updated. Call runtime.approveAndResume(runId, { approved, approvedBy }) in your app to continue the agent.',
|
|
420
|
+
}, null, 2));
|
|
421
|
+
}
|
|
422
|
+
catch (e) {
|
|
423
|
+
// eslint-disable-next-line no-console
|
|
424
|
+
console.error(e);
|
|
425
|
+
process.exitCode = 1;
|
|
426
|
+
}
|
|
427
|
+
};
|
|
428
|
+
runs
|
|
429
|
+
.command('resume')
|
|
430
|
+
.description('Record HITL approve/reject on file store (in-app approveAndResume still required to continue)')
|
|
431
|
+
.argument('<runId>', 'AgentRun / execution id')
|
|
432
|
+
.option('--store <path>', 'Path to runs.json or durable store directory', DEFAULT_RUN_STORE)
|
|
433
|
+
.option('--dir <path>', 'Durable store directory (runs + human-tasks + checkpoints)')
|
|
434
|
+
.option('--approve', 'Approve pending human task (default)')
|
|
435
|
+
.option('--reject', 'Reject pending human task')
|
|
436
|
+
.option('--by <who>', 'Approver identity', 'cli')
|
|
437
|
+
.action(resumeAction);
|
|
438
|
+
runs
|
|
439
|
+
.command('approve')
|
|
440
|
+
.description('Alias for runs resume --approve')
|
|
441
|
+
.argument('<runId>', 'AgentRun / execution id')
|
|
442
|
+
.option('--store <path>', 'Path to runs.json or durable store directory', DEFAULT_RUN_STORE)
|
|
443
|
+
.option('--dir <path>', 'Durable store directory')
|
|
444
|
+
.option('--by <who>', 'Approver identity', 'cli')
|
|
445
|
+
.action(async (runId, opts) => {
|
|
446
|
+
await resumeAction(runId, { ...opts, approve: true });
|
|
447
|
+
});
|
|
94
448
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const commander_1 = require("commander");
|
|
4
|
+
const agent_1 = require("./agent");
|
|
5
|
+
describe('registerAgentCommand (AOS-011)', () => {
|
|
6
|
+
it('registers run, logs, and doctor subcommands', () => {
|
|
7
|
+
const program = new commander_1.Command();
|
|
8
|
+
(0, agent_1.registerAgentCommand)(program);
|
|
9
|
+
const agent = program.commands.find((c) => c.name() === 'agent');
|
|
10
|
+
expect(agent).toBeDefined();
|
|
11
|
+
const names = agent.commands.map((c) => c.name());
|
|
12
|
+
expect(names).toEqual(expect.arrayContaining(['install', 'dna', 'run', 'logs', 'doctor', 'runs']));
|
|
13
|
+
});
|
|
14
|
+
});
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
/**
|
|
3
|
+
* `hazel skillgate from-openapi <file>` — preview governed skills from an OpenAPI doc.
|
|
4
|
+
* `hazel skillgate init` — write a starter skillgate.config.json.
|
|
5
|
+
*/
|
|
6
|
+
export declare function registerSkillgateCommand(program: Command): void;
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.registerSkillgateCommand = registerSkillgateCommand;
|
|
37
|
+
const fs = __importStar(require("fs"));
|
|
38
|
+
const path = __importStar(require("path"));
|
|
39
|
+
/**
|
|
40
|
+
* `hazel skillgate from-openapi <file>` — preview governed skills from an OpenAPI doc.
|
|
41
|
+
* `hazel skillgate init` — write a starter skillgate.config.json.
|
|
42
|
+
*/
|
|
43
|
+
function registerSkillgateCommand(program) {
|
|
44
|
+
const skillgate = program
|
|
45
|
+
.command('skillgate')
|
|
46
|
+
.description('Skillgate — OpenAPI → curated, governed agent skills');
|
|
47
|
+
skillgate
|
|
48
|
+
.command('from-openapi')
|
|
49
|
+
.description('Parse an OpenAPI JSON file and print the Skillgate report')
|
|
50
|
+
.argument('<file>', 'Path to OpenAPI JSON')
|
|
51
|
+
.option('--mode <mode>', 'Include mode: opt-in | all', 'opt-in')
|
|
52
|
+
.option('--tags <tags>', 'Comma-separated tag allowlist')
|
|
53
|
+
.option('--allow-destructive', 'Allow DELETE / destructive methods')
|
|
54
|
+
.option('--allow-admin', 'Allow admin / internal paths')
|
|
55
|
+
.option('--base-url <url>', 'Override servers[0].url for invokers')
|
|
56
|
+
.option('--json', 'Print raw JSON report')
|
|
57
|
+
.action(async (file, opts) => {
|
|
58
|
+
try {
|
|
59
|
+
const { Skillgate } = await Promise.resolve().then(() => __importStar(require('@hazeljs/skillgate')));
|
|
60
|
+
const abs = path.resolve(process.cwd(), file);
|
|
61
|
+
const raw = fs.readFileSync(abs, 'utf8');
|
|
62
|
+
const spec = JSON.parse(raw);
|
|
63
|
+
const gate = Skillgate.fromOpenApi(spec, {
|
|
64
|
+
include: {
|
|
65
|
+
mode: opts.mode === 'all' ? 'all' : 'opt-in',
|
|
66
|
+
tags: opts.tags
|
|
67
|
+
? opts.tags
|
|
68
|
+
.split(',')
|
|
69
|
+
.map((t) => t.trim())
|
|
70
|
+
.filter(Boolean)
|
|
71
|
+
: undefined,
|
|
72
|
+
},
|
|
73
|
+
classify: {
|
|
74
|
+
allowDestructive: Boolean(opts.allowDestructive),
|
|
75
|
+
allowAdmin: Boolean(opts.allowAdmin),
|
|
76
|
+
},
|
|
77
|
+
invoke: opts.baseUrl ? { baseUrl: opts.baseUrl } : undefined,
|
|
78
|
+
force: true,
|
|
79
|
+
});
|
|
80
|
+
const report = gate.report();
|
|
81
|
+
if (opts.json) {
|
|
82
|
+
// eslint-disable-next-line no-console
|
|
83
|
+
console.log(JSON.stringify(report, null, 2));
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
// eslint-disable-next-line no-console
|
|
87
|
+
console.log(JSON.stringify({
|
|
88
|
+
included: report.included.map((s) => ({
|
|
89
|
+
name: s.name,
|
|
90
|
+
class: s.class,
|
|
91
|
+
method: s.method,
|
|
92
|
+
path: s.path,
|
|
93
|
+
readOnly: s.readOnly,
|
|
94
|
+
requiresApproval: s.requiresApproval,
|
|
95
|
+
})),
|
|
96
|
+
denied: report.denied.map((s) => ({
|
|
97
|
+
name: s.name,
|
|
98
|
+
reason: s.denyReason,
|
|
99
|
+
})),
|
|
100
|
+
warnings: report.warnings,
|
|
101
|
+
next: 'gate.register(toolRegistry, "your-agent")',
|
|
102
|
+
}, null, 2));
|
|
103
|
+
}
|
|
104
|
+
catch (e) {
|
|
105
|
+
// eslint-disable-next-line no-console
|
|
106
|
+
console.error(e);
|
|
107
|
+
process.exitCode = 1;
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
skillgate
|
|
111
|
+
.command('init')
|
|
112
|
+
.description('Write a starter skillgate.config.json in the current directory')
|
|
113
|
+
.option('--force', 'Overwrite existing file')
|
|
114
|
+
.action((opts) => {
|
|
115
|
+
const out = path.resolve(process.cwd(), 'skillgate.config.json');
|
|
116
|
+
if (fs.existsSync(out) && !opts.force) {
|
|
117
|
+
// eslint-disable-next-line no-console
|
|
118
|
+
console.error(`Refusing to overwrite ${out} (pass --force)`);
|
|
119
|
+
process.exitCode = 1;
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
const starter = {
|
|
123
|
+
$schema: 'https://hazeljs.ai/schemas/skillgate.config.json',
|
|
124
|
+
include: {
|
|
125
|
+
mode: 'opt-in',
|
|
126
|
+
tags: ['agent', 'skillgate'],
|
|
127
|
+
},
|
|
128
|
+
classify: {
|
|
129
|
+
writeRequiresApproval: true,
|
|
130
|
+
allowDestructive: false,
|
|
131
|
+
allowAdmin: false,
|
|
132
|
+
},
|
|
133
|
+
invoke: {
|
|
134
|
+
baseUrl: 'http://127.0.0.1:3000',
|
|
135
|
+
headers: {
|
|
136
|
+
Authorization: 'Bearer ${API_TOKEN}',
|
|
137
|
+
},
|
|
138
|
+
},
|
|
139
|
+
warnAbove: 12,
|
|
140
|
+
maxTools: 24,
|
|
141
|
+
agentName: 'api-concierge',
|
|
142
|
+
};
|
|
143
|
+
fs.writeFileSync(out, JSON.stringify(starter, null, 2) + '\n');
|
|
144
|
+
// eslint-disable-next-line no-console
|
|
145
|
+
console.log(`Wrote ${out}`);
|
|
146
|
+
});
|
|
147
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -56,6 +56,7 @@ const add_1 = require("./commands/add");
|
|
|
56
56
|
const eval_1 = require("./commands/eval");
|
|
57
57
|
const benchmark_1 = require("./commands/benchmark");
|
|
58
58
|
const agent_1 = require("./commands/agent");
|
|
59
|
+
const skillgate_1 = require("./commands/skillgate");
|
|
59
60
|
// Read version from package.json to ensure consistency
|
|
60
61
|
const packageJson = JSON.parse((0, fs_1.readFileSync)((0, path_1.join)(__dirname, '../package.json'), 'utf8'));
|
|
61
62
|
const program = new commander_1.Command();
|
|
@@ -71,6 +72,7 @@ program
|
|
|
71
72
|
(0, eval_1.registerEvalCommand)(program);
|
|
72
73
|
(0, benchmark_1.registerBenchmarkCommand)(program);
|
|
73
74
|
(0, agent_1.registerAgentCommand)(program);
|
|
75
|
+
(0, skillgate_1.registerSkillgateCommand)(program);
|
|
74
76
|
// Generate command group (unified: hazel g <type> <name> [--path] [--dry-run] [--json], or hazel g --list)
|
|
75
77
|
const generateCommand = program
|
|
76
78
|
.command('generate')
|
|
@@ -234,6 +234,25 @@ export const mlImports = [MLModule.forRoot()];
|
|
|
234
234
|
// Minimal MCP server example:
|
|
235
235
|
const server = createMcpServer({ name: 'hazel-mcp', version: '0.1.0' });
|
|
236
236
|
server.start();
|
|
237
|
+
`,
|
|
238
|
+
},
|
|
239
|
+
{
|
|
240
|
+
shortName: 'skillgate',
|
|
241
|
+
npm: '@hazeljs/skillgate',
|
|
242
|
+
label: 'Skillgate - OpenAPI → governed agent skills (@hazeljs/skillgate)',
|
|
243
|
+
hint: 'import { Skillgate } from "@hazeljs/skillgate";\n // Skillgate.fromOpenApi(spec).register(toolRegistry, "api-concierge");',
|
|
244
|
+
moduleImport: null,
|
|
245
|
+
moduleExpression: null,
|
|
246
|
+
setupTemplate: `import { Skillgate } from '@hazeljs/skillgate';
|
|
247
|
+
import { ToolRegistry } from '@hazeljs/agent';
|
|
248
|
+
|
|
249
|
+
// Opt-in: tags agent|skillgate, x-hazel-skill, or explicit allowlists.
|
|
250
|
+
const gate = Skillgate.fromOpenApi(openApiSpec, {
|
|
251
|
+
include: { tags: ['agent'] },
|
|
252
|
+
invoke: { baseUrl: process.env.API_BASE_URL || 'http://127.0.0.1:3000' },
|
|
253
|
+
});
|
|
254
|
+
const registry = new ToolRegistry();
|
|
255
|
+
gate.register(registry, 'api-concierge');
|
|
237
256
|
`,
|
|
238
257
|
},
|
|
239
258
|
{
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hazeljs/cli",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"description": "Command-line interface for scaffolding and generating HazelJS applications and components",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -31,9 +31,10 @@
|
|
|
31
31
|
"mustache": "^4.2.0"
|
|
32
32
|
},
|
|
33
33
|
"peerDependencies": {
|
|
34
|
-
"@hazeljs/eval": "^
|
|
35
|
-
"@hazeljs/benchmark": "^
|
|
36
|
-
"@hazeljs/agent": "^
|
|
34
|
+
"@hazeljs/eval": "^2.0.0",
|
|
35
|
+
"@hazeljs/benchmark": "^2.0.0",
|
|
36
|
+
"@hazeljs/agent": "^2.0.0",
|
|
37
|
+
"@hazeljs/skillgate": "^2.0.0"
|
|
37
38
|
},
|
|
38
39
|
"peerDependenciesMeta": {
|
|
39
40
|
"@hazeljs/eval": {
|
|
@@ -44,12 +45,16 @@
|
|
|
44
45
|
},
|
|
45
46
|
"@hazeljs/agent": {
|
|
46
47
|
"optional": true
|
|
48
|
+
},
|
|
49
|
+
"@hazeljs/skillgate": {
|
|
50
|
+
"optional": true
|
|
47
51
|
}
|
|
48
52
|
},
|
|
49
53
|
"devDependencies": {
|
|
50
|
-
"@hazeljs/eval": "^
|
|
51
|
-
"@hazeljs/benchmark": "^
|
|
52
|
-
"@hazeljs/agent": "^
|
|
54
|
+
"@hazeljs/eval": "^2.0.0",
|
|
55
|
+
"@hazeljs/benchmark": "^2.0.0",
|
|
56
|
+
"@hazeljs/agent": "^2.0.0",
|
|
57
|
+
"@hazeljs/skillgate": "^2.0.0",
|
|
53
58
|
"@types/inquirer": "^8.2.12",
|
|
54
59
|
"@types/jest": "^29.5.14",
|
|
55
60
|
"@types/mustache": "^4.2.6",
|