@assistkick/create 1.18.0 → 1.19.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/package.json +1 -1
- package/templates/assistkick-product-system/packages/shared/lib/openapi.ts +146 -0
- package/templates/assistkick-product-system/packages/shared/tools/openapi_describe.ts +59 -0
- package/templates/assistkick-product-system/packages/shared/tools/openapi_list.ts +69 -0
- package/templates/assistkick-product-system/packages/shared/tools/openapi_schema.ts +67 -0
- package/templates/skills/assistkick-openapi-explorer/SKILL.md +78 -0
- package/templates/skills/assistkick-openapi-explorer/cache/.gitignore +2 -0
package/package.json
CHANGED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared helper to load an OpenAPI spec from a local path or URL.
|
|
3
|
+
*
|
|
4
|
+
* If the source is a URL, the spec is downloaded into a local cache
|
|
5
|
+
* directory and reused for the rest of the current day.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
9
|
+
import { dirname, join } from 'node:path';
|
|
10
|
+
import { fileURLToPath } from 'node:url';
|
|
11
|
+
|
|
12
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
13
|
+
const CACHE_DIR = join(__dirname, '..', '..', '..', '..', '.claude', 'skills', 'assistkick-openapi-explorer', 'cache');
|
|
14
|
+
|
|
15
|
+
function isUrl(source: string): boolean {
|
|
16
|
+
return source.startsWith('http://') || source.startsWith('https://');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function urlToCachePath(url: string): string {
|
|
20
|
+
const parsed = new URL(url);
|
|
21
|
+
const safeName = (parsed.host + parsed.pathname).replace(/[^a-zA-Z0-9._-]/g, '_');
|
|
22
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
23
|
+
return join(CACHE_DIR, `${safeName}_${today}.json`);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async function download(url: string, dest: string): Promise<void> {
|
|
27
|
+
const res = await fetch(url, {
|
|
28
|
+
headers: { 'User-Agent': 'openapi-explorer/1.0' },
|
|
29
|
+
signal: AbortSignal.timeout(30_000),
|
|
30
|
+
});
|
|
31
|
+
if (!res.ok) {
|
|
32
|
+
throw new Error(`HTTP ${res.status} ${res.statusText}`);
|
|
33
|
+
}
|
|
34
|
+
const body = await res.text();
|
|
35
|
+
const dir = dirname(dest);
|
|
36
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
37
|
+
writeFileSync(dest, body, 'utf-8');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Load an OpenAPI spec from a local file path or URL.
|
|
42
|
+
* URLs are cached locally per day — a fresh download happens only once per day.
|
|
43
|
+
*/
|
|
44
|
+
export async function loadSpec(source: string): Promise<Record<string, any>> {
|
|
45
|
+
let path: string;
|
|
46
|
+
|
|
47
|
+
if (isUrl(source)) {
|
|
48
|
+
const cached = urlToCachePath(source);
|
|
49
|
+
if (!existsSync(cached)) {
|
|
50
|
+
await download(source, cached);
|
|
51
|
+
}
|
|
52
|
+
path = cached;
|
|
53
|
+
} else {
|
|
54
|
+
path = source;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (!existsSync(path)) {
|
|
58
|
+
throw new Error(`Spec file not found: ${path}`);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const raw = readFileSync(path, 'utf-8');
|
|
62
|
+
return JSON.parse(raw);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Resolve a JSON Pointer $ref (e.g. "#/components/schemas/User") to its value in the spec.
|
|
67
|
+
*/
|
|
68
|
+
export function resolveRef(spec: Record<string, any>, ref: string): any {
|
|
69
|
+
const parts = ref.replace(/^#\//, '').split('/');
|
|
70
|
+
let obj: any = spec;
|
|
71
|
+
for (const part of parts) {
|
|
72
|
+
obj = obj[part];
|
|
73
|
+
if (obj === undefined) {
|
|
74
|
+
throw new Error(`Cannot resolve $ref: ${ref}`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return obj;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Recursively inline $ref references up to a depth limit.
|
|
82
|
+
* Adds a __schema__ key to indicate which schema each inlined object represents.
|
|
83
|
+
*/
|
|
84
|
+
export function inlineRefs(spec: Record<string, any>, obj: any, depth = 0): any {
|
|
85
|
+
if (depth > 5) return obj;
|
|
86
|
+
|
|
87
|
+
if (Array.isArray(obj)) {
|
|
88
|
+
return obj.map(item => inlineRefs(spec, item, depth + 1));
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (obj !== null && typeof obj === 'object') {
|
|
92
|
+
if ('$ref' in obj) {
|
|
93
|
+
const refName = (obj['$ref'] as string).split('/').pop()!;
|
|
94
|
+
const resolved = resolveRef(spec, obj['$ref']);
|
|
95
|
+
return { __schema__: refName, ...inlineRefs(spec, resolved, depth + 1) };
|
|
96
|
+
}
|
|
97
|
+
const result: Record<string, any> = {};
|
|
98
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
99
|
+
result[k] = inlineRefs(spec, v, depth + 1);
|
|
100
|
+
}
|
|
101
|
+
return result;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return obj;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Recursively collect all $ref strings from an object.
|
|
109
|
+
*/
|
|
110
|
+
export function collectRefs(obj: any, refs: Set<string> = new Set()): Set<string> {
|
|
111
|
+
if (Array.isArray(obj)) {
|
|
112
|
+
for (const item of obj) collectRefs(item, refs);
|
|
113
|
+
} else if (obj !== null && typeof obj === 'object') {
|
|
114
|
+
if ('$ref' in obj) refs.add(obj['$ref'] as string);
|
|
115
|
+
for (const v of Object.values(obj)) collectRefs(v, refs);
|
|
116
|
+
}
|
|
117
|
+
return refs;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Resolve a named schema and all its nested $ref dependencies.
|
|
122
|
+
* Returns a map of schema name → schema definition.
|
|
123
|
+
*/
|
|
124
|
+
export function resolveSchemaTree(
|
|
125
|
+
spec: Record<string, any>,
|
|
126
|
+
schemaName: string,
|
|
127
|
+
visited: Record<string, any> = {},
|
|
128
|
+
): Record<string, any> {
|
|
129
|
+
if (schemaName in visited) return visited;
|
|
130
|
+
|
|
131
|
+
const schemas = spec?.components?.schemas ?? {};
|
|
132
|
+
if (!(schemaName in schemas)) return visited;
|
|
133
|
+
|
|
134
|
+
const schema = schemas[schemaName];
|
|
135
|
+
visited[schemaName] = schema;
|
|
136
|
+
|
|
137
|
+
const refs = collectRefs(schema);
|
|
138
|
+
for (const ref of refs) {
|
|
139
|
+
const childName = ref.split('/').pop()!;
|
|
140
|
+
if (!(childName in visited)) {
|
|
141
|
+
resolveSchemaTree(spec, childName, visited);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return visited;
|
|
146
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* openapi_describe — Show full details of a specific API endpoint with all $ref references inlined.
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* pnpm tsx packages/shared/tools/openapi_describe.ts <SPEC_PATH_OR_URL> <METHOD> <PATH>
|
|
8
|
+
*
|
|
9
|
+
* Example:
|
|
10
|
+
* pnpm tsx packages/shared/tools/openapi_describe.ts docs/api-spec.json GET /auth/verify
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { program } from 'commander';
|
|
14
|
+
import chalk from 'chalk';
|
|
15
|
+
import { loadSpec, inlineRefs } from '../lib/openapi.js';
|
|
16
|
+
|
|
17
|
+
program
|
|
18
|
+
.argument('<spec>', 'Path or URL to an OpenAPI spec (JSON)')
|
|
19
|
+
.argument('<method>', 'HTTP method (GET, POST, PUT, DELETE, etc.)')
|
|
20
|
+
.argument('<path>', 'API path (e.g. /auth/verify)')
|
|
21
|
+
.parse();
|
|
22
|
+
|
|
23
|
+
const [specSource, methodArg, pathArg] = program.args;
|
|
24
|
+
|
|
25
|
+
(async () => {
|
|
26
|
+
try {
|
|
27
|
+
const spec = await loadSpec(specSource);
|
|
28
|
+
const paths = spec.paths ?? {};
|
|
29
|
+
const method = methodArg.toLowerCase();
|
|
30
|
+
|
|
31
|
+
if (!(pathArg in paths)) {
|
|
32
|
+
console.error(chalk.red(`Path '${pathArg}' not found. Available paths:`));
|
|
33
|
+
for (const p of Object.keys(paths).sort()) {
|
|
34
|
+
console.log(` ${p}`);
|
|
35
|
+
}
|
|
36
|
+
process.exit(1);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (!(method in paths[pathArg])) {
|
|
40
|
+
const available = Object.keys(paths[pathArg]).map(m => m.toUpperCase());
|
|
41
|
+
console.error(chalk.red(`Method '${methodArg.toUpperCase()}' not found for '${pathArg}'. Available: ${available.join(', ')}`));
|
|
42
|
+
process.exit(1);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const endpoint = paths[pathArg][method];
|
|
46
|
+
const inlined = inlineRefs(spec, endpoint);
|
|
47
|
+
|
|
48
|
+
console.log(chalk.cyan.bold(`## ${methodArg.toUpperCase()} ${pathArg}`));
|
|
49
|
+
console.log();
|
|
50
|
+
if (endpoint.tags) console.log(`Tags: ${endpoint.tags.join(', ')}`);
|
|
51
|
+
if (endpoint.summary) console.log(`Summary: ${endpoint.summary}`);
|
|
52
|
+
if (endpoint.description) console.log(`Description: ${endpoint.description}`);
|
|
53
|
+
console.log();
|
|
54
|
+
console.log(JSON.stringify(inlined, null, 2));
|
|
55
|
+
} catch (err) {
|
|
56
|
+
console.error(chalk.red(`Error: ${(err as Error).message}`));
|
|
57
|
+
process.exit(1);
|
|
58
|
+
}
|
|
59
|
+
})();
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* openapi_list — List all API endpoints from an OpenAPI spec grouped by tag.
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* pnpm tsx packages/shared/tools/openapi_list.ts <SPEC_PATH_OR_URL>
|
|
8
|
+
*
|
|
9
|
+
* Example (local): pnpm tsx packages/shared/tools/openapi_list.ts docs/api-spec.json
|
|
10
|
+
* Example (URL): pnpm tsx packages/shared/tools/openapi_list.ts https://example.com/openapi.json
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { program } from 'commander';
|
|
14
|
+
import chalk from 'chalk';
|
|
15
|
+
import { loadSpec } from '../lib/openapi.js';
|
|
16
|
+
|
|
17
|
+
program
|
|
18
|
+
.argument('<spec>', 'Path or URL to an OpenAPI spec (JSON)')
|
|
19
|
+
.parse();
|
|
20
|
+
|
|
21
|
+
const [specSource] = program.args;
|
|
22
|
+
|
|
23
|
+
(async () => {
|
|
24
|
+
try {
|
|
25
|
+
const spec = await loadSpec(specSource);
|
|
26
|
+
const paths = spec.paths ?? {};
|
|
27
|
+
|
|
28
|
+
const title = spec.info?.title ?? 'N/A';
|
|
29
|
+
const version = spec.info?.version ?? 'N/A';
|
|
30
|
+
const totalEndpoints = Object.values(paths).reduce(
|
|
31
|
+
(sum: number, methods: any) => sum + Object.keys(methods).length,
|
|
32
|
+
0,
|
|
33
|
+
);
|
|
34
|
+
const totalSchemas = Object.keys(spec.components?.schemas ?? {}).length;
|
|
35
|
+
|
|
36
|
+
console.log(`API: ${chalk.bold(title)}`);
|
|
37
|
+
console.log(`Version: ${version}`);
|
|
38
|
+
console.log(`Total endpoints: ${chalk.green(String(totalEndpoints))}`);
|
|
39
|
+
console.log(`Total schemas: ${chalk.green(String(totalSchemas))}`);
|
|
40
|
+
console.log();
|
|
41
|
+
|
|
42
|
+
// Group by tag
|
|
43
|
+
const tagged: Record<string, Array<[string, string, string]>> = {};
|
|
44
|
+
|
|
45
|
+
for (const [path, methods] of Object.entries(paths).sort()) {
|
|
46
|
+
for (const [method, ep] of Object.entries(methods as Record<string, any>).sort()) {
|
|
47
|
+
const tags: string[] = ep.tags ?? ['Untagged'];
|
|
48
|
+
const summary = (ep.summary ?? ep.description ?? '').split('\n')[0].slice(0, 80);
|
|
49
|
+
for (const tag of tags) {
|
|
50
|
+
if (!tagged[tag]) tagged[tag] = [];
|
|
51
|
+
tagged[tag].push([method.toUpperCase(), path, summary]);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
for (const tag of Object.keys(tagged).sort()) {
|
|
57
|
+
console.log(chalk.cyan.bold(`## ${tag}`));
|
|
58
|
+
for (const [method, path, summary] of tagged[tag]) {
|
|
59
|
+
let line = ` ${method.padEnd(7)} ${path}`;
|
|
60
|
+
if (summary) line += ` — ${summary}`;
|
|
61
|
+
console.log(line);
|
|
62
|
+
}
|
|
63
|
+
console.log();
|
|
64
|
+
}
|
|
65
|
+
} catch (err) {
|
|
66
|
+
console.error(chalk.red(`Error: ${(err as Error).message}`));
|
|
67
|
+
process.exit(1);
|
|
68
|
+
}
|
|
69
|
+
})();
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* openapi_schema — Get a named schema and all its referenced child schemas from an OpenAPI spec.
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* pnpm tsx packages/shared/tools/openapi_schema.ts <SPEC_PATH_OR_URL> <SCHEMA_NAME>
|
|
8
|
+
* pnpm tsx packages/shared/tools/openapi_schema.ts <SPEC_PATH_OR_URL> --list
|
|
9
|
+
*
|
|
10
|
+
* Example:
|
|
11
|
+
* pnpm tsx packages/shared/tools/openapi_schema.ts docs/api-spec.json User
|
|
12
|
+
* pnpm tsx packages/shared/tools/openapi_schema.ts docs/api-spec.json --list
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { program } from 'commander';
|
|
16
|
+
import chalk from 'chalk';
|
|
17
|
+
import { loadSpec, resolveSchemaTree } from '../lib/openapi.js';
|
|
18
|
+
|
|
19
|
+
program
|
|
20
|
+
.argument('<spec>', 'Path or URL to an OpenAPI spec (JSON)')
|
|
21
|
+
.argument('[schema]', 'Schema name (or use --list to see all)')
|
|
22
|
+
.option('--list', 'List all available schema names')
|
|
23
|
+
.parse();
|
|
24
|
+
|
|
25
|
+
const [specSource, schemaArg] = program.args;
|
|
26
|
+
const opts = program.opts();
|
|
27
|
+
|
|
28
|
+
(async () => {
|
|
29
|
+
try {
|
|
30
|
+
const spec = await loadSpec(specSource);
|
|
31
|
+
const schemas = spec.components?.schemas ?? {};
|
|
32
|
+
|
|
33
|
+
if (opts.list || schemaArg === '--list') {
|
|
34
|
+
console.log(chalk.cyan.bold(`Available schemas (${Object.keys(schemas).length}):`));
|
|
35
|
+
for (const name of Object.keys(schemas).sort()) {
|
|
36
|
+
const props = schemas[name].properties ?? {};
|
|
37
|
+
const propNames = Object.keys(props).slice(0, 5);
|
|
38
|
+
let suffix = propNames.length > 0 ? ` — fields: ${propNames.join(', ')}` : '';
|
|
39
|
+
if (Object.keys(props).length > 5) {
|
|
40
|
+
suffix += `, ... (+${Object.keys(props).length - 5} more)`;
|
|
41
|
+
}
|
|
42
|
+
console.log(` ${chalk.bold(name)}${suffix}`);
|
|
43
|
+
}
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (!schemaArg) {
|
|
48
|
+
console.error(chalk.red('Provide a schema name or use --list to see available schemas.'));
|
|
49
|
+
process.exit(1);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (!(schemaArg in schemas)) {
|
|
53
|
+
console.error(chalk.red(`Schema '${schemaArg}' not found. Use --list to see available schemas.`));
|
|
54
|
+
process.exit(1);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const tree = resolveSchemaTree(spec, schemaArg);
|
|
58
|
+
|
|
59
|
+
console.log(chalk.cyan.bold(`## Schema: ${schemaArg}`));
|
|
60
|
+
console.log(`Includes ${Object.keys(tree).length} schema(s): ${Object.keys(tree).join(', ')}`);
|
|
61
|
+
console.log();
|
|
62
|
+
console.log(JSON.stringify(tree, null, 2));
|
|
63
|
+
} catch (err) {
|
|
64
|
+
console.error(chalk.red(`Error: ${(err as Error).message}`));
|
|
65
|
+
process.exit(1);
|
|
66
|
+
}
|
|
67
|
+
})();
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: assistkick-openapi-explorer
|
|
3
|
+
description: >
|
|
4
|
+
Explore any OpenAPI spec without loading the entire file into context.
|
|
5
|
+
Use when you need to understand API endpoints, request/response schemas, or build
|
|
6
|
+
features that interact with an API. Triggers: working with API routes,
|
|
7
|
+
implementing API clients, understanding endpoint contracts, checking request/response
|
|
8
|
+
formats, or when the user asks about available APIs.
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# OpenAPI Explorer
|
|
12
|
+
|
|
13
|
+
Explore any OpenAPI spec incrementally using lightweight TypeScript tools.
|
|
14
|
+
The spec can be a **local file path** or a **URL** (downloaded once per day and cached locally).
|
|
15
|
+
|
|
16
|
+
All commands are run from the `assistkick-product-system/` directory.
|
|
17
|
+
|
|
18
|
+
## Workflow
|
|
19
|
+
|
|
20
|
+
1. **List endpoints** to find the API you need
|
|
21
|
+
2. **Describe an endpoint** to see its full request/response contract with inlined schemas
|
|
22
|
+
3. **Get a schema** to inspect a specific component schema and all its dependencies
|
|
23
|
+
|
|
24
|
+
## Tool Reference
|
|
25
|
+
|
|
26
|
+
### List all endpoints
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
pnpm tsx packages/shared/tools/openapi_list.ts <SPEC_PATH_OR_URL>
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Example (local): `pnpm tsx packages/shared/tools/openapi_list.ts docs/api-spec.json`
|
|
33
|
+
Example (URL): `pnpm tsx packages/shared/tools/openapi_list.ts https://example.com/openapi.json`
|
|
34
|
+
|
|
35
|
+
Shows all endpoints grouped by tag with method, path, and summary.
|
|
36
|
+
Also shows API title, version, total endpoint count, and total schema count.
|
|
37
|
+
|
|
38
|
+
### Describe a specific endpoint
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
pnpm tsx packages/shared/tools/openapi_describe.ts <SPEC_PATH_OR_URL> <METHOD> <PATH>
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Example: `pnpm tsx packages/shared/tools/openapi_describe.ts docs/api-spec.json GET /auth/verify`
|
|
45
|
+
|
|
46
|
+
Shows the full endpoint definition with all `$ref` references inlined up to 5 levels deep.
|
|
47
|
+
If the path or method is not found, lists all available paths or methods.
|
|
48
|
+
|
|
49
|
+
### Get a schema and its dependencies
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
pnpm tsx packages/shared/tools/openapi_schema.ts <SPEC_PATH_OR_URL> <SCHEMA_NAME>
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Example: `pnpm tsx packages/shared/tools/openapi_schema.ts docs/api-spec.json User`
|
|
56
|
+
|
|
57
|
+
Resolves the named schema and all schemas it references recursively.
|
|
58
|
+
|
|
59
|
+
Use `--list` to see all available schema names with their top fields:
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
pnpm tsx packages/shared/tools/openapi_schema.ts <SPEC_PATH_OR_URL> --list
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## URL Caching
|
|
66
|
+
|
|
67
|
+
When the spec source is a URL, the file is downloaded and cached in
|
|
68
|
+
`.claude/skills/assistkick-openapi-explorer/cache/`. The cache is keyed by
|
|
69
|
+
URL + date, so a fresh download happens only once per day.
|
|
70
|
+
|
|
71
|
+
## Rules
|
|
72
|
+
|
|
73
|
+
1. Always start with `openapi_list` to get an overview before diving into specific endpoints
|
|
74
|
+
2. Use `openapi_describe` to see the full contract of a specific endpoint — it inlines all `$ref` references
|
|
75
|
+
3. Use `openapi_schema` to inspect data models and their dependency trees
|
|
76
|
+
4. The spec argument is always the first positional argument and is mandatory
|
|
77
|
+
5. Only JSON specs are supported — YAML specs must be converted to JSON first
|
|
78
|
+
6. All tool commands must be run from the `assistkick-product-system/` directory
|