@skillit/client 0.2.0 → 0.4.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/README.md +40 -0
- package/dist/bin.d.ts +2 -1
- package/dist/bin.d.ts.map +1 -1
- package/dist/bin.js +21 -13
- package/dist/bin.js.map +1 -1
- package/dist/commands/audit.d.ts +34 -0
- package/dist/commands/audit.d.ts.map +1 -0
- package/dist/commands/audit.js +139 -0
- package/dist/commands/audit.js.map +1 -0
- package/dist/commands/gen.d.ts +21 -0
- package/dist/commands/gen.d.ts.map +1 -0
- package/dist/commands/gen.js +112 -0
- package/dist/commands/gen.js.map +1 -0
- package/dist/commands/init.d.ts +8 -22
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +39 -81
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/refine.d.ts +34 -2
- package/dist/commands/refine.d.ts.map +1 -1
- package/dist/commands/refine.js +61 -29
- package/dist/commands/refine.js.map +1 -1
- package/dist/detect-source.d.ts.map +1 -1
- package/dist/detect-source.js +12 -5
- package/dist/detect-source.js.map +1 -1
- package/dist/generate.d.ts +74 -0
- package/dist/generate.d.ts.map +1 -0
- package/dist/generate.js +68 -0
- package/dist/generate.js.map +1 -0
- package/dist/mcp-mode.d.ts +22 -0
- package/dist/mcp-mode.d.ts.map +1 -0
- package/dist/mcp-mode.js +27 -0
- package/dist/mcp-mode.js.map +1 -0
- package/dist/model/anthropic.d.ts +11 -0
- package/dist/model/anthropic.d.ts.map +1 -1
- package/dist/model/anthropic.js +16 -2
- package/dist/model/anthropic.js.map +1 -1
- package/dist/model/cli/adapters.d.ts.map +1 -1
- package/dist/model/cli/adapters.js +35 -2
- package/dist/model/cli/adapters.js.map +1 -1
- package/dist/model/cli/cli-client.d.ts.map +1 -1
- package/dist/model/cli/cli-client.js +4 -2
- package/dist/model/cli/cli-client.js.map +1 -1
- package/dist/postinstall-template.d.ts +2 -0
- package/dist/postinstall-template.d.ts.map +1 -0
- package/dist/postinstall-template.js +61 -0
- package/dist/postinstall-template.js.map +1 -0
- package/dist/program.d.ts +12 -0
- package/dist/program.d.ts.map +1 -0
- package/dist/program.js +42 -0
- package/dist/program.js.map +1 -0
- package/dist/typedoc-entry.d.ts +6 -0
- package/dist/typedoc-entry.d.ts.map +1 -0
- package/dist/typedoc-entry.js +14 -0
- package/dist/typedoc-entry.js.map +1 -0
- package/package.json +18 -7
- package/skills/skillit-bootstrap/SKILL.md +115 -0
- package/skills/skillit-bootstrap/references/surface-routing.md +108 -0
- package/CHANGELOG.md +0 -32
- package/scripts/gen-refine-cli-skill.mjs +0 -48
- package/src/__tests__/anthropic-model.test.ts +0 -45
- package/src/__tests__/anthropic-prompt.test.ts +0 -102
- package/src/__tests__/cli-adapters.test.ts +0 -119
- package/src/__tests__/cli-client.test.ts +0 -58
- package/src/__tests__/cli-run.test.ts +0 -52
- package/src/__tests__/detect-mode.test.ts +0 -114
- package/src/__tests__/detect-source.test.ts +0 -158
- package/src/__tests__/fixtures/bin-with-program.mjs +0 -6
- package/src/__tests__/init.test.ts +0 -218
- package/src/__tests__/model-client-factory.test.ts +0 -28
- package/src/__tests__/refine-resolve.test.ts +0 -72
- package/src/bin.ts +0 -18
- package/src/commands/init.ts +0 -207
- package/src/commands/refine.ts +0 -261
- package/src/detect-mode.ts +0 -94
- package/src/detect-source.ts +0 -123
- package/src/index.ts +0 -3
- package/src/model/anthropic.ts +0 -116
- package/src/model/cli/adapters.ts +0 -178
- package/src/model/cli/cli-client.ts +0 -52
- package/src/model/cli/run.ts +0 -83
- package/src/model/model-client-factory.ts +0 -62
- package/src/model/models.ts +0 -7
- package/tsconfig.build.json +0 -9
- package/tsconfig.json +0 -4
package/src/commands/init.ts
DELETED
|
@@ -1,207 +0,0 @@
|
|
|
1
|
-
// packages/client/src/commands/init.ts
|
|
2
|
-
import { spawn } from 'node:child_process';
|
|
3
|
-
import { readFile } from 'node:fs/promises';
|
|
4
|
-
import { join } from 'node:path';
|
|
5
|
-
import { Command } from 'commander';
|
|
6
|
-
import { extractCliSkill, loadProgram, writeCliSkill } from '@skillit/cli';
|
|
7
|
-
import {
|
|
8
|
-
detectPackageManager,
|
|
9
|
-
detectProjectNature,
|
|
10
|
-
type RefineSourceKind
|
|
11
|
-
} from '../detect-source.js';
|
|
12
|
-
import { runRefineCommand, type RefineCommandOpts } from './refine.js';
|
|
13
|
-
|
|
14
|
-
type PackageManager = 'pnpm' | 'yarn' | 'npm';
|
|
15
|
-
|
|
16
|
-
/** Options passed to the (CLI-path) skill generator. */
|
|
17
|
-
export interface GenerateSkillOpts {
|
|
18
|
-
/** Project root being initialized. */
|
|
19
|
-
cwd: string;
|
|
20
|
-
/** Resolved project nature. */
|
|
21
|
-
nature: RefineSourceKind;
|
|
22
|
-
/** Skill name (consumer package name, scope stripped). */
|
|
23
|
-
name: string;
|
|
24
|
-
/** Absolute output directory (`<cwd>/<out>`). */
|
|
25
|
-
outDir: string;
|
|
26
|
-
/** `--program <file#export>` entry, if provided. */
|
|
27
|
-
program?: string;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* Injectable side-effecting steps for {@link buildInitCommand}. All optional;
|
|
32
|
-
* real defaults run install / generate / refine. Tests inject stubs so no real
|
|
33
|
-
* install, network, or model call runs.
|
|
34
|
-
*/
|
|
35
|
-
export interface InitDeps {
|
|
36
|
-
/** Install `pkg` as a dev dependency in `cwd` using package manager `pm`. */
|
|
37
|
-
runInstall?(pkg: string, pm: PackageManager, cwd: string): Promise<void>;
|
|
38
|
-
/** Generate the initial skill (CLI path only). */
|
|
39
|
-
generateSkill?(opts: GenerateSkillOpts): Promise<void>;
|
|
40
|
-
/** Dispatch the refine loop for the resolved source. */
|
|
41
|
-
runRefine?(opts: RefineCommandOpts): Promise<void>;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
interface InitOpts {
|
|
45
|
-
source?: string;
|
|
46
|
-
program?: string;
|
|
47
|
-
out: string;
|
|
48
|
-
modelClient?: string;
|
|
49
|
-
modelCliTimeout?: string;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
const VALID_SOURCES: readonly RefineSourceKind[] = ['cli', 'mcp', 'typedoc'];
|
|
53
|
-
|
|
54
|
-
/** Map a project nature to the `@skillit/*` package that handles it. */
|
|
55
|
-
function natureToPackage(nature: RefineSourceKind): string {
|
|
56
|
-
if (nature === 'cli') return '@skillit/cli';
|
|
57
|
-
if (nature === 'mcp') return '@skillit/mcp';
|
|
58
|
-
return 'typedoc-plugin-skillit';
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/** Build the package manager's add-dev command line (for messaging + spawn). */
|
|
62
|
-
function addDevCommand(pm: PackageManager, pkg: string): string {
|
|
63
|
-
if (pm === 'pnpm') return `pnpm add -D ${pkg}`;
|
|
64
|
-
if (pm === 'yarn') return `yarn add -D ${pkg}`;
|
|
65
|
-
return `npm install -D ${pkg}`;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
/** Strip a leading `@scope/` from a package name for use as a skill dir name. */
|
|
69
|
-
function skillNameFrom(packageName: string): string {
|
|
70
|
-
const slash = packageName.indexOf('/');
|
|
71
|
-
if (packageName.startsWith('@') && slash !== -1) {
|
|
72
|
-
return packageName.slice(slash + 1);
|
|
73
|
-
}
|
|
74
|
-
return packageName;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
async function readPackageName(cwd: string): Promise<string> {
|
|
78
|
-
try {
|
|
79
|
-
const raw = await readFile(join(cwd, 'package.json'), 'utf8');
|
|
80
|
-
const pkg = JSON.parse(raw) as { name?: string };
|
|
81
|
-
return pkg.name ?? 'skill';
|
|
82
|
-
} catch {
|
|
83
|
-
return 'skill';
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
/** Default install: spawn the package manager's add-dev command, cwd-scoped. */
|
|
88
|
-
function defaultRunInstall(pkg: string, pm: PackageManager, cwd: string): Promise<void> {
|
|
89
|
-
const command = addDevCommand(pm, pkg);
|
|
90
|
-
const [bin, ...args] = command.split(' ');
|
|
91
|
-
return new Promise((resolve, reject) => {
|
|
92
|
-
const child = spawn(bin!, args, { cwd, stdio: 'inherit' });
|
|
93
|
-
child.on('error', reject);
|
|
94
|
-
child.on('close', (code) => {
|
|
95
|
-
if (code === 0) resolve();
|
|
96
|
-
else reject(new Error(`exited with code ${code}`));
|
|
97
|
-
});
|
|
98
|
-
});
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
/** Default CLI-path skill generation: loadProgram → extractCliSkill → writeCliSkill. */
|
|
102
|
-
async function defaultGenerateSkill(opts: GenerateSkillOpts): Promise<void> {
|
|
103
|
-
const program = await loadProgram({ program: opts.program, cwd: opts.cwd });
|
|
104
|
-
const skill = await extractCliSkill({ program, metadata: { name: opts.name } });
|
|
105
|
-
writeCliSkill(skill, { outDir: opts.outDir });
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
/** Default refine dispatch: reuse the shared refine command body. */
|
|
109
|
-
function defaultRunRefine(opts: RefineCommandOpts): Promise<void> {
|
|
110
|
-
return runRefineCommand(opts);
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
export function buildInitCommand(deps: InitDeps = {}): Command {
|
|
114
|
-
const runInstall = deps.runInstall ?? defaultRunInstall;
|
|
115
|
-
const generateSkill = deps.generateSkill ?? defaultGenerateSkill;
|
|
116
|
-
const runRefine = deps.runRefine ?? defaultRunRefine;
|
|
117
|
-
|
|
118
|
-
return new Command('init')
|
|
119
|
-
.description(
|
|
120
|
-
'Detect the project, install the right @skillit package, generate + refine a skill'
|
|
121
|
-
)
|
|
122
|
-
.option('--source <kind>', 'cli | mcp | typedoc (auto-detected if omitted)')
|
|
123
|
-
.option('--program <file#export>', 'commander program entry (cli source)')
|
|
124
|
-
.option('--out <dir>', 'output directory for the generated skill', 'skills')
|
|
125
|
-
.option(
|
|
126
|
-
'--model-client <kind>',
|
|
127
|
-
'model backend for refine: api | claude | codex | copilot',
|
|
128
|
-
'api'
|
|
129
|
-
)
|
|
130
|
-
.option('--model-cli-timeout <ms>', 'per-call timeout for cli model backends (ms)')
|
|
131
|
-
.action(async (opts: InitOpts) => {
|
|
132
|
-
const cwd = process.cwd();
|
|
133
|
-
|
|
134
|
-
// 1. Resolve nature: explicit --source wins, else detect.
|
|
135
|
-
let nature: RefineSourceKind;
|
|
136
|
-
if (opts.source !== undefined) {
|
|
137
|
-
if (!VALID_SOURCES.includes(opts.source as RefineSourceKind)) {
|
|
138
|
-
throw new Error(
|
|
139
|
-
`Invalid --source value: ${opts.source}. Use --source <cli|mcp|typedoc>.`
|
|
140
|
-
);
|
|
141
|
-
}
|
|
142
|
-
nature = opts.source as RefineSourceKind;
|
|
143
|
-
} else {
|
|
144
|
-
nature = await detectProjectNature(cwd);
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
// 2 + 3. Map nature → package, install via the detected package manager.
|
|
148
|
-
const pkg = natureToPackage(nature);
|
|
149
|
-
const pm = detectPackageManager(cwd);
|
|
150
|
-
const command = addDevCommand(pm, pkg);
|
|
151
|
-
try {
|
|
152
|
-
await runInstall(pkg, pm, cwd);
|
|
153
|
-
} catch (error) {
|
|
154
|
-
const reason = error instanceof Error ? error.message : String(error);
|
|
155
|
-
// Surface the exact command and STOP — do not generate or refine.
|
|
156
|
-
throw new Error(`Install failed (${reason}). Run it manually:\n ${command}`);
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
// 4 + 5. Generate + refine — CLI-first: only the cli source is fully
|
|
160
|
-
// automated this pass. For mcp/typedoc, skip both and print actionable
|
|
161
|
-
// next-step guidance rather than silently no-opping (refine needs
|
|
162
|
-
// source-specific flags like --mcp that init can't supply yet).
|
|
163
|
-
if (nature === 'cli') {
|
|
164
|
-
const outDir = join(cwd, opts.out);
|
|
165
|
-
const name = skillNameFrom(await readPackageName(cwd));
|
|
166
|
-
const generateOpts: GenerateSkillOpts = {
|
|
167
|
-
cwd,
|
|
168
|
-
nature,
|
|
169
|
-
name,
|
|
170
|
-
outDir,
|
|
171
|
-
...(opts.program !== undefined ? { program: opts.program } : {})
|
|
172
|
-
};
|
|
173
|
-
try {
|
|
174
|
-
await generateSkill(generateOpts);
|
|
175
|
-
} catch (error) {
|
|
176
|
-
// The cli source only auto-loads a commander program. A yargs/other
|
|
177
|
-
// CLI (still classified as 'cli') can't be generated yet — degrade
|
|
178
|
-
// gracefully: surface the reason and how to proceed, do NOT crash,
|
|
179
|
-
// and do NOT run refine/regenerate on a failed generate.
|
|
180
|
-
const reason = error instanceof Error ? error.message : String(error);
|
|
181
|
-
console.log(
|
|
182
|
-
`Installed ${pkg}, but couldn't auto-load a commander program (${reason}). ` +
|
|
183
|
-
`If this is a commander CLI, run: skillit refine --source cli --program <file#export>. ` +
|
|
184
|
-
`(yargs/other CLIs aren't auto-generated yet.)`
|
|
185
|
-
);
|
|
186
|
-
return;
|
|
187
|
-
}
|
|
188
|
-
await runRefine({
|
|
189
|
-
source: nature,
|
|
190
|
-
...(opts.program !== undefined ? { program: opts.program } : {}),
|
|
191
|
-
...(opts.modelClient !== undefined ? { modelClient: opts.modelClient } : {}),
|
|
192
|
-
...(opts.modelCliTimeout !== undefined ? { modelCliTimeout: opts.modelCliTimeout } : {}),
|
|
193
|
-
maxIterations: '5',
|
|
194
|
-
items: '5'
|
|
195
|
-
});
|
|
196
|
-
// refine only writes JSDoc back into source files; regenerate so the
|
|
197
|
-
// on-disk skill reflects the freshly-written annotations. Runs only
|
|
198
|
-
// after a successful refine (a refine throw propagates above).
|
|
199
|
-
await generateSkill(generateOpts);
|
|
200
|
-
} else {
|
|
201
|
-
const extra = nature === 'mcp' ? ' [--mcp <path>]' : '';
|
|
202
|
-
console.log(
|
|
203
|
-
`Installed ${pkg}. Skill generation + refine for the ${nature} source isn't automated yet — run: skillit refine --source ${nature}${extra}`
|
|
204
|
-
);
|
|
205
|
-
}
|
|
206
|
-
});
|
|
207
|
-
}
|
package/src/commands/refine.ts
DELETED
|
@@ -1,261 +0,0 @@
|
|
|
1
|
-
// packages/client/src/commands/refine.ts
|
|
2
|
-
import { Command } from 'commander';
|
|
3
|
-
import {
|
|
4
|
-
McpRefineSource,
|
|
5
|
-
TypeScriptMcpRefineSource,
|
|
6
|
-
extractMcpSkill,
|
|
7
|
-
readMcpConfigFile
|
|
8
|
-
} from '@skillit/mcp';
|
|
9
|
-
import { CliRefineSource, loadProgram } from '@skillit/cli';
|
|
10
|
-
import { refineSkill, type ModelClient, type RefineSource } from '@skillit/core';
|
|
11
|
-
import { createModelClient } from '../model/model-client-factory.js';
|
|
12
|
-
import { detectRefineMode } from '../detect-mode.js';
|
|
13
|
-
import {
|
|
14
|
-
classifyRefineSources,
|
|
15
|
-
detectInstalledSources,
|
|
16
|
-
type DetectedRefineSource,
|
|
17
|
-
type RefineSourceKind
|
|
18
|
-
} from '../detect-source.js';
|
|
19
|
-
import { join } from 'node:path';
|
|
20
|
-
|
|
21
|
-
function parsePositiveInt(raw: string, flag: string): number {
|
|
22
|
-
const n = parseInt(raw, 10);
|
|
23
|
-
if (!Number.isFinite(n) || n < 1) {
|
|
24
|
-
throw new Error(`${flag} must be a positive integer, got: ${raw}`);
|
|
25
|
-
}
|
|
26
|
-
return n;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
/** Options consulted by {@link resolveRefineSource}. */
|
|
30
|
-
export interface RefineSourceResolveOpts {
|
|
31
|
-
source?: string;
|
|
32
|
-
mcp?: string;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/** Result of resolving the refine source: a concrete kind or an actionable error. */
|
|
36
|
-
export type RefineSourceResolution = { kind: 'cli' | 'mcp' } | { error: string };
|
|
37
|
-
|
|
38
|
-
const VALID_SOURCES = ['cli', 'mcp', 'typedoc'] as const;
|
|
39
|
-
const SOURCE_FORM = '--source <cli|mcp|typedoc>';
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* Resolve the refine source from explicit `--source` (wins) or detection,
|
|
43
|
-
* then validate per-source flag requirements. Pure and unit-testable: it does
|
|
44
|
-
* not read the filesystem or invoke the model.
|
|
45
|
-
*
|
|
46
|
-
* @param opts - parsed `--source` / `--mcp` flags
|
|
47
|
-
* @param detected - result of {@link detectRefineSource} for the cwd
|
|
48
|
-
* @param candidates - the raw installed source kinds (from
|
|
49
|
-
* {@link detectInstalledSources}); named in the ambiguous error
|
|
50
|
-
*/
|
|
51
|
-
export function resolveRefineSource(
|
|
52
|
-
opts: RefineSourceResolveOpts,
|
|
53
|
-
detected: DetectedRefineSource,
|
|
54
|
-
candidates: readonly RefineSourceKind[] = []
|
|
55
|
-
): RefineSourceResolution {
|
|
56
|
-
let kind: RefineSourceKind;
|
|
57
|
-
if (opts.source !== undefined) {
|
|
58
|
-
if (!VALID_SOURCES.includes(opts.source as RefineSourceKind)) {
|
|
59
|
-
return { error: `Invalid --source value: ${opts.source}. Use ${SOURCE_FORM}.` };
|
|
60
|
-
}
|
|
61
|
-
kind = opts.source as RefineSourceKind;
|
|
62
|
-
} else if (detected === 'ambiguous') {
|
|
63
|
-
return {
|
|
64
|
-
error: `Cannot determine refine source: multiple @skillit sources installed (found: ${candidates.join(', ')}).
|
|
65
|
-
Pass ${SOURCE_FORM} to choose one.`
|
|
66
|
-
};
|
|
67
|
-
} else if (detected === 'none') {
|
|
68
|
-
return {
|
|
69
|
-
error: `Cannot determine refine source: no @skillit source package detected.
|
|
70
|
-
Pass ${SOURCE_FORM} to choose one.`
|
|
71
|
-
};
|
|
72
|
-
} else {
|
|
73
|
-
kind = detected;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
if (kind === 'typedoc') {
|
|
77
|
-
return { error: 'typedoc refine not yet supported; use --source cli|mcp.' };
|
|
78
|
-
}
|
|
79
|
-
if (kind === 'mcp' && opts.mcp === undefined) {
|
|
80
|
-
return { error: 'The mcp source requires --mcp <path> (path to mcp.json or MCP config file).' };
|
|
81
|
-
}
|
|
82
|
-
return { kind };
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
/** Parsed options for the `refine` action / {@link runRefineCommand}. */
|
|
86
|
-
export interface RefineCommandOpts {
|
|
87
|
-
source?: string;
|
|
88
|
-
program?: string;
|
|
89
|
-
mcp?: string;
|
|
90
|
-
server?: string;
|
|
91
|
-
overlay?: string;
|
|
92
|
-
mode?: string;
|
|
93
|
-
sourceGlob?: string;
|
|
94
|
-
maxIterations: string;
|
|
95
|
-
items: string;
|
|
96
|
-
modelClient?: string;
|
|
97
|
-
modelCliTimeout?: string;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
/** The model backend to use; defaults to the API client. */
|
|
101
|
-
export function resolveModelClientKind(raw: string | undefined): string {
|
|
102
|
-
return raw ?? 'api';
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
/**
|
|
106
|
-
* The body of the `refine` command action, extracted so it can be reused
|
|
107
|
-
* programmatically (e.g. by `skillit init`) without argv gymnastics. Sets
|
|
108
|
-
* `process.exitCode` and writes progress/errors to the console exactly as the
|
|
109
|
-
* CLI does.
|
|
110
|
-
*/
|
|
111
|
-
export async function runRefineCommand(opts: RefineCommandOpts): Promise<void> {
|
|
112
|
-
const cwd = process.cwd();
|
|
113
|
-
const maxIterations = parsePositiveInt(opts.maxIterations, '--max-iterations');
|
|
114
|
-
const itemsPerIteration = parsePositiveInt(opts.items, '--items');
|
|
115
|
-
|
|
116
|
-
const timeoutMs =
|
|
117
|
-
opts.modelCliTimeout !== undefined
|
|
118
|
-
? parsePositiveInt(opts.modelCliTimeout, '--model-cli-timeout')
|
|
119
|
-
: undefined;
|
|
120
|
-
let model: ModelClient;
|
|
121
|
-
try {
|
|
122
|
-
model = createModelClient(
|
|
123
|
-
resolveModelClientKind(opts.modelClient),
|
|
124
|
-
timeoutMs !== undefined ? { timeoutMs } : {}
|
|
125
|
-
);
|
|
126
|
-
} catch (error) {
|
|
127
|
-
console.error(error instanceof Error ? error.message : String(error));
|
|
128
|
-
process.exitCode = 1;
|
|
129
|
-
return;
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
const candidates = await detectInstalledSources(cwd);
|
|
133
|
-
const detected = classifyRefineSources(candidates);
|
|
134
|
-
const resolution = resolveRefineSource(opts, detected, candidates);
|
|
135
|
-
if ('error' in resolution) {
|
|
136
|
-
console.error(resolution.error);
|
|
137
|
-
process.exitCode = 1;
|
|
138
|
-
return;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
let source: RefineSource;
|
|
142
|
-
let reportInPlace = false;
|
|
143
|
-
|
|
144
|
-
if (resolution.kind === 'cli') {
|
|
145
|
-
const program = await loadProgram({ program: opts.program, cwd });
|
|
146
|
-
const sourceGlob = opts.sourceGlob ?? join(cwd, '**', '*.ts');
|
|
147
|
-
source = new CliRefineSource({ program, sourceGlob, cwd });
|
|
148
|
-
reportInPlace = true;
|
|
149
|
-
} else {
|
|
150
|
-
// mcp source: --mcp guaranteed present by resolveRefineSource.
|
|
151
|
-
const mcpPath = opts.mcp!;
|
|
152
|
-
const overlayPath = opts.overlay ?? join(cwd, '.skillit-overlay.json');
|
|
153
|
-
|
|
154
|
-
let mode: 'build' | 'runtime';
|
|
155
|
-
if (opts.mode === 'build' || opts.mode === 'runtime') {
|
|
156
|
-
mode = opts.mode;
|
|
157
|
-
} else if (opts.mode !== undefined) {
|
|
158
|
-
console.error(`Invalid --mode value: ${opts.mode}. Use 'build' or 'runtime'.`);
|
|
159
|
-
process.exitCode = 1;
|
|
160
|
-
return;
|
|
161
|
-
} else {
|
|
162
|
-
const detectedMode = await detectRefineMode(cwd, mcpPath);
|
|
163
|
-
if (detectedMode === 'ambiguous') {
|
|
164
|
-
console.error(`Cannot determine refine mode.
|
|
165
|
-
Use --mode build (TypeScript MCP server you own)
|
|
166
|
-
--mode runtime (consuming project, any MCP server)`);
|
|
167
|
-
process.exitCode = 1;
|
|
168
|
-
return;
|
|
169
|
-
}
|
|
170
|
-
mode = detectedMode;
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
if (mode === 'build') {
|
|
174
|
-
console.log('Refining in build mode (TypeScript MCP)');
|
|
175
|
-
} else {
|
|
176
|
-
console.log('Refining in runtime mode (overlay)');
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
const entries = await readMcpConfigFile(mcpPath);
|
|
180
|
-
const entry = opts.server
|
|
181
|
-
? entries.find((e) => e.name === opts.server)
|
|
182
|
-
: entries.find((e) => !e.disabled);
|
|
183
|
-
if (!entry) {
|
|
184
|
-
const name = opts.server ? `"${opts.server}"` : 'any enabled server';
|
|
185
|
-
throw new Error(`Could not find ${name} in ${mcpPath}`);
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
if (mode === 'build') {
|
|
189
|
-
const sourceGlob = opts.sourceGlob ?? join(cwd, '**', '*.ts');
|
|
190
|
-
source = new TypeScriptMcpRefineSource({
|
|
191
|
-
transport: entry.transport,
|
|
192
|
-
sourceGlob
|
|
193
|
-
});
|
|
194
|
-
reportInPlace = true;
|
|
195
|
-
} else {
|
|
196
|
-
source = new McpRefineSource({
|
|
197
|
-
overlayPath,
|
|
198
|
-
extract: () => extractMcpSkill({ transport: entry.transport })
|
|
199
|
-
});
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
const result = await runRefine(source, model, maxIterations, itemsPerIteration);
|
|
203
|
-
reportResult(result, { reportInPlace, overlayPath });
|
|
204
|
-
process.exitCode = result.passed ? 0 : 1;
|
|
205
|
-
return;
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
const result = await runRefine(source, model, maxIterations, itemsPerIteration);
|
|
209
|
-
reportResult(result, { reportInPlace });
|
|
210
|
-
process.exitCode = result.passed ? 0 : 1;
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
export function buildRefineCommand(): Command {
|
|
214
|
-
return new Command('refine')
|
|
215
|
-
.description('Autonomously improve a skill via the audit→draft→review loop')
|
|
216
|
-
.option('--source <kind>', 'cli | mcp | typedoc (auto-detected if omitted)')
|
|
217
|
-
.option('--program <file#export>', 'commander program entry (cli source)')
|
|
218
|
-
.option('--mcp <path>', 'path to mcp.json or MCP config file')
|
|
219
|
-
.option('--server <name>', 'server name within the config (defaults to first enabled)')
|
|
220
|
-
.option('--overlay <path>', 'path to overlay JSON file (runtime mode only)')
|
|
221
|
-
.option('--mode <mode>', 'refine mode: build or runtime (auto-detected if omitted)')
|
|
222
|
-
.option('--source-glob <glob>', 'glob pattern for TypeScript source files')
|
|
223
|
-
.option('--max-iterations <n>', 'iteration cap (default 5)', '5')
|
|
224
|
-
.option('--items <n>', 'work items per iteration (default 5)', '5')
|
|
225
|
-
.option('--model-client <kind>', 'model backend: api | claude | codex | copilot', 'api')
|
|
226
|
-
.option('--model-cli-timeout <ms>', 'per-call timeout for cli model backends (ms)')
|
|
227
|
-
.action((opts: RefineCommandOpts) => runRefineCommand(opts));
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
function runRefine(
|
|
231
|
-
source: RefineSource,
|
|
232
|
-
model: ModelClient,
|
|
233
|
-
maxIterations: number,
|
|
234
|
-
itemsPerIteration: number
|
|
235
|
-
): ReturnType<typeof refineSkill> {
|
|
236
|
-
return refineSkill({
|
|
237
|
-
source,
|
|
238
|
-
model,
|
|
239
|
-
maxIterations,
|
|
240
|
-
itemsPerIteration,
|
|
241
|
-
onIteration: (iter) => {
|
|
242
|
-
const { grade, total } = iter.estimate;
|
|
243
|
-
console.log(
|
|
244
|
-
` Iteration ${iter.iteration}: grade ${grade} (${total}/120), ${iter.fixes.length} fix(es) applied`
|
|
245
|
-
);
|
|
246
|
-
}
|
|
247
|
-
});
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
function reportResult(
|
|
251
|
-
result: Awaited<ReturnType<typeof refineSkill>>,
|
|
252
|
-
opts: { reportInPlace: boolean; overlayPath?: string }
|
|
253
|
-
): void {
|
|
254
|
-
console.log(`\nDone. Reason: ${result.stoppedReason}`);
|
|
255
|
-
console.log(`Final grade: ${result.finalEstimate.grade} (${result.finalEstimate.total}/120)`);
|
|
256
|
-
if (opts.reportInPlace) {
|
|
257
|
-
console.log(`Source files updated in place.`);
|
|
258
|
-
} else if (opts.overlayPath && result.iterations.length > 0) {
|
|
259
|
-
console.log(`Overlay: ${opts.overlayPath}`);
|
|
260
|
-
}
|
|
261
|
-
}
|
package/src/detect-mode.ts
DELETED
|
@@ -1,94 +0,0 @@
|
|
|
1
|
-
import { access, readFile } from 'node:fs/promises';
|
|
2
|
-
import { homedir } from 'node:os';
|
|
3
|
-
import { basename, dirname, join } from 'node:path';
|
|
4
|
-
|
|
5
|
-
function isMcpServerDep(dep: string): boolean {
|
|
6
|
-
// Match server-implementation packages only. Excludes purely consumer-side
|
|
7
|
-
// packages such as @modelcontextprotocol/inspector or @modelcontextprotocol/client-*
|
|
8
|
-
// which do not imply the project has editable server source files.
|
|
9
|
-
return (
|
|
10
|
-
dep === '@modelcontextprotocol/sdk' ||
|
|
11
|
-
dep.startsWith('@modelcontextprotocol/server-') ||
|
|
12
|
-
dep === 'fastmcp'
|
|
13
|
-
);
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
async function hasMcpSdkDep(cwd: string): Promise<boolean> {
|
|
17
|
-
const home = homedir();
|
|
18
|
-
let dir = cwd;
|
|
19
|
-
while (dir !== home && dir !== dirname(dir)) {
|
|
20
|
-
try {
|
|
21
|
-
const raw = await readFile(join(dir, 'package.json'), 'utf8');
|
|
22
|
-
const pkg = JSON.parse(raw) as Record<string, unknown>;
|
|
23
|
-
const deps = {
|
|
24
|
-
...(pkg['dependencies'] as Record<string, string> | undefined),
|
|
25
|
-
...(pkg['devDependencies'] as Record<string, string> | undefined)
|
|
26
|
-
};
|
|
27
|
-
if (Object.keys(deps).some(isMcpServerDep)) return true;
|
|
28
|
-
} catch {
|
|
29
|
-
// no package.json or parse error in this dir; keep walking
|
|
30
|
-
}
|
|
31
|
-
dir = dirname(dir);
|
|
32
|
-
}
|
|
33
|
-
return false;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
async function fileExists(path: string): Promise<boolean> {
|
|
37
|
-
try {
|
|
38
|
-
await access(path);
|
|
39
|
-
return true;
|
|
40
|
-
} catch {
|
|
41
|
-
return false;
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
async function hasMcpConfig(cwd: string): Promise<boolean> {
|
|
46
|
-
const home = homedir();
|
|
47
|
-
let dir = cwd;
|
|
48
|
-
while (dir !== home && dir !== dirname(dir)) {
|
|
49
|
-
if (
|
|
50
|
-
(await fileExists(join(dir, 'mcp.json'))) ||
|
|
51
|
-
(await fileExists(join(dir, 'claude_desktop_config.json')))
|
|
52
|
-
) {
|
|
53
|
-
return true;
|
|
54
|
-
}
|
|
55
|
-
dir = dirname(dir);
|
|
56
|
-
}
|
|
57
|
-
return false;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
const KNOWN_RUNTIME_BASENAMES = new Set(['mcp.json', 'claude_desktop_config.json']);
|
|
61
|
-
|
|
62
|
-
async function isMcpRuntimeConfigFile(path: string): Promise<boolean> {
|
|
63
|
-
if (KNOWN_RUNTIME_BASENAMES.has(basename(path))) return true;
|
|
64
|
-
try {
|
|
65
|
-
const raw = await readFile(path, 'utf8');
|
|
66
|
-
const parsed = JSON.parse(raw) as Record<string, unknown>;
|
|
67
|
-
return 'mcpServers' in parsed;
|
|
68
|
-
} catch {
|
|
69
|
-
return false;
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
/**
|
|
74
|
-
* Detect refine mode from project context.
|
|
75
|
-
*
|
|
76
|
-
* @param cwd - directory to inspect for build/runtime signals
|
|
77
|
-
* @param mcpConfigPath - optional path to the --mcp config file; if its
|
|
78
|
-
* basename is a known runtime config filename, or its contents contain a
|
|
79
|
-
* top-level `mcpServers` key, it is treated as an additional runtime signal.
|
|
80
|
-
*/
|
|
81
|
-
export async function detectRefineMode(
|
|
82
|
-
cwd: string,
|
|
83
|
-
mcpConfigPath?: string
|
|
84
|
-
): Promise<'build' | 'runtime' | 'ambiguous'> {
|
|
85
|
-
const [hasBuild, hasRuntimeFromCwd, mcpFileIsRuntime] = await Promise.all([
|
|
86
|
-
hasMcpSdkDep(cwd),
|
|
87
|
-
hasMcpConfig(cwd),
|
|
88
|
-
mcpConfigPath !== undefined ? isMcpRuntimeConfigFile(mcpConfigPath) : Promise.resolve(false)
|
|
89
|
-
]);
|
|
90
|
-
const hasRuntime = hasRuntimeFromCwd || mcpFileIsRuntime;
|
|
91
|
-
if (hasBuild && !hasRuntime) return 'build';
|
|
92
|
-
if (hasRuntime && !hasBuild) return 'runtime';
|
|
93
|
-
return 'ambiguous';
|
|
94
|
-
}
|
package/src/detect-source.ts
DELETED
|
@@ -1,123 +0,0 @@
|
|
|
1
|
-
import { existsSync } from 'node:fs';
|
|
2
|
-
import { readFile } from 'node:fs/promises';
|
|
3
|
-
import { join } from 'node:path';
|
|
4
|
-
import { loadProgram } from '@skillit/cli';
|
|
5
|
-
|
|
6
|
-
export type RefineSourceKind = 'cli' | 'mcp' | 'typedoc';
|
|
7
|
-
export type DetectedRefineSource = RefineSourceKind | 'ambiguous' | 'none';
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Read the union of `dependencies` + `devDependencies` from `<cwd>/package.json`.
|
|
11
|
-
* Missing or unreadable `package.json` → `{}` (never throws). Single source of
|
|
12
|
-
* truth for dependency reads across detection helpers.
|
|
13
|
-
*/
|
|
14
|
-
async function readDeps(cwd: string): Promise<Record<string, string>> {
|
|
15
|
-
try {
|
|
16
|
-
const raw = await readFile(join(cwd, 'package.json'), 'utf8');
|
|
17
|
-
const pkg = JSON.parse(raw) as Record<string, unknown>;
|
|
18
|
-
return {
|
|
19
|
-
...(pkg['dependencies'] as Record<string, string> | undefined),
|
|
20
|
-
...(pkg['devDependencies'] as Record<string, string> | undefined)
|
|
21
|
-
};
|
|
22
|
-
} catch {
|
|
23
|
-
return {};
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/** Map an installed package name to its refine source kind, if any. */
|
|
28
|
-
function packageToSource(dep: string): RefineSourceKind | undefined {
|
|
29
|
-
if (dep === '@skillit/cli') return 'cli';
|
|
30
|
-
if (dep === '@skillit/mcp') return 'mcp';
|
|
31
|
-
if (dep === 'typedoc-plugin-skillit' || dep === '@skillit/typedoc') return 'typedoc';
|
|
32
|
-
return undefined;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/** Stable, canonical ordering for reported candidate lists. */
|
|
36
|
-
const SOURCE_ORDER: readonly RefineSourceKind[] = ['cli', 'mcp', 'typedoc'];
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* Detect the raw, deduped list of refine source kinds installed in `cwd`'s
|
|
40
|
-
* `package.json` (union of `dependencies` + `devDependencies`), in stable
|
|
41
|
-
* order (`cli`, `mcp`, `typedoc`).
|
|
42
|
-
*
|
|
43
|
-
* Missing or unreadable `package.json` → `[]` (never throws). This is the
|
|
44
|
-
* single place the package→source mapping lives; {@link detectRefineSource}
|
|
45
|
-
* is derived from it.
|
|
46
|
-
*/
|
|
47
|
-
export async function detectInstalledSources(cwd: string): Promise<RefineSourceKind[]> {
|
|
48
|
-
const deps = await readDeps(cwd);
|
|
49
|
-
|
|
50
|
-
const sources = new Set<RefineSourceKind>();
|
|
51
|
-
for (const dep of Object.keys(deps)) {
|
|
52
|
-
const source = packageToSource(dep);
|
|
53
|
-
if (source) sources.add(source);
|
|
54
|
-
}
|
|
55
|
-
return SOURCE_ORDER.filter((s) => sources.has(s));
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* Collapse an installed-source list into the {@link DetectedRefineSource}
|
|
60
|
-
* verdict: empty → `'none'`, single → that source, many → `'ambiguous'`.
|
|
61
|
-
*/
|
|
62
|
-
export function classifyRefineSources(sources: readonly RefineSourceKind[]): DetectedRefineSource {
|
|
63
|
-
if (sources.length === 0) return 'none';
|
|
64
|
-
if (sources.length === 1) return sources[0]!;
|
|
65
|
-
return 'ambiguous';
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
/**
|
|
69
|
-
* Detect the refine source from `@skillit/*` packages installed in `cwd`'s
|
|
70
|
-
* `package.json` (union of `dependencies` + `devDependencies`).
|
|
71
|
-
*
|
|
72
|
-
* - 0 matching packages → `'none'`
|
|
73
|
-
* - exactly 1 distinct source → that source
|
|
74
|
-
* - more than 1 distinct source → `'ambiguous'`
|
|
75
|
-
* - missing or unreadable `package.json` → `'none'` (never throws)
|
|
76
|
-
*/
|
|
77
|
-
export async function detectRefineSource(cwd: string): Promise<DetectedRefineSource> {
|
|
78
|
-
return classifyRefineSources(await detectInstalledSources(cwd));
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
/**
|
|
82
|
-
* Detect the nature of the project at `cwd` from its `package.json`:
|
|
83
|
-
* - has `commander` or `yargs` dep, OR a `bin` that loads to a commander
|
|
84
|
-
* {@link Command} → `'cli'`
|
|
85
|
-
* - else has `@modelcontextprotocol/sdk` → `'mcp'`
|
|
86
|
-
* - else → `'typedoc'` (safe default for a plain TS library)
|
|
87
|
-
*
|
|
88
|
-
* Missing or unreadable `package.json` → `'typedoc'` (never throws). The `cli`
|
|
89
|
-
* check is evaluated first (deps as a fast path, no import; the loadable-bin
|
|
90
|
-
* probe runs only when those deps are absent), then `mcp`, then the `typedoc`
|
|
91
|
-
* default.
|
|
92
|
-
*/
|
|
93
|
-
export async function detectProjectNature(cwd: string): Promise<RefineSourceKind> {
|
|
94
|
-
const deps = await readDeps(cwd);
|
|
95
|
-
if ('commander' in deps || 'yargs' in deps) return 'cli';
|
|
96
|
-
if (await hasLoadableProgram(cwd)) return 'cli';
|
|
97
|
-
if ('@modelcontextprotocol/sdk' in deps) return 'mcp';
|
|
98
|
-
return 'typedoc';
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
/**
|
|
102
|
-
* Attempt to load the consumer's `package.json` `bin` as a commander program
|
|
103
|
-
* (via {@link loadProgram}). Returns `true` only if it resolves to a `Command`;
|
|
104
|
-
* any failure (no bin, import error, non-Command export) → `false`.
|
|
105
|
-
*/
|
|
106
|
-
async function hasLoadableProgram(cwd: string): Promise<boolean> {
|
|
107
|
-
try {
|
|
108
|
-
await loadProgram({ cwd });
|
|
109
|
-
return true;
|
|
110
|
-
} catch {
|
|
111
|
-
return false;
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
/**
|
|
116
|
-
* Detect the package manager for `cwd` by lockfile presence:
|
|
117
|
-
* `pnpm-lock.yaml` → `'pnpm'`, `yarn.lock` → `'yarn'`, else `'npm'`.
|
|
118
|
-
*/
|
|
119
|
-
export function detectPackageManager(cwd: string): 'pnpm' | 'yarn' | 'npm' {
|
|
120
|
-
if (existsSync(join(cwd, 'pnpm-lock.yaml'))) return 'pnpm';
|
|
121
|
-
if (existsSync(join(cwd, 'yarn.lock'))) return 'yarn';
|
|
122
|
-
return 'npm';
|
|
123
|
-
}
|
package/src/index.ts
DELETED