@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.
@@ -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": "1.0.6",
7
+ "version": "2.0.0",
8
8
  "description": "CLI for generating HazelJS components and applications"
9
9
  },
10
10
  "commands": [
@@ -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
- * Live hot-reload happens in-process via AgentRuntime.installAgentPackage().
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;
@@ -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
- * Live hot-reload happens in-process via AgentRuntime.installAgentPackage().
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.command('agent').description('Agent OS DNA / marketplace helpers');
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": "1.0.6",
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": "^1.0.6",
35
- "@hazeljs/benchmark": "^1.0.6",
36
- "@hazeljs/agent": "^1.0.6"
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": "^1.0.6",
51
- "@hazeljs/benchmark": "^1.0.6",
52
- "@hazeljs/agent": "^1.0.6",
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",