@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/model/anthropic.ts
DELETED
|
@@ -1,116 +0,0 @@
|
|
|
1
|
-
// packages/client/src/model/anthropic.ts
|
|
2
|
-
import Anthropic from '@anthropic-ai/sdk';
|
|
3
|
-
import type { DraftRequest, ReviewRequest, ReviewResult, ModelClient } from '@skillit/core';
|
|
4
|
-
import { DRAFTER, REVIEWER, MAX_TOKENS } from './models.js';
|
|
5
|
-
|
|
6
|
-
export function parseReviewVerdict(text: string): ReviewResult {
|
|
7
|
-
// Prefer {"verdict" anchor to skip stray {braces} in prose.
|
|
8
|
-
// Fall back to the first { if the model omits the verdict key.
|
|
9
|
-
// Depth-scan for the matching }, skipping { and } inside JSON string values.
|
|
10
|
-
const verdictAnchor = text.indexOf('{"verdict"');
|
|
11
|
-
const start = verdictAnchor !== -1 ? verdictAnchor : text.indexOf('{');
|
|
12
|
-
if (start === -1) return { verdict: 'accepted', feedback: '' };
|
|
13
|
-
let depth = 0;
|
|
14
|
-
let end = -1;
|
|
15
|
-
let inString = false;
|
|
16
|
-
let escaped = false;
|
|
17
|
-
for (let i = start; i < text.length; i++) {
|
|
18
|
-
const ch = text[i]!;
|
|
19
|
-
if (escaped) {
|
|
20
|
-
escaped = false;
|
|
21
|
-
continue;
|
|
22
|
-
}
|
|
23
|
-
if (ch === '\\' && inString) {
|
|
24
|
-
escaped = true;
|
|
25
|
-
continue;
|
|
26
|
-
}
|
|
27
|
-
if (ch === '"') {
|
|
28
|
-
inString = !inString;
|
|
29
|
-
continue;
|
|
30
|
-
}
|
|
31
|
-
if (inString) continue;
|
|
32
|
-
if (ch === '{') depth++;
|
|
33
|
-
else if (ch === '}') {
|
|
34
|
-
if (--depth === 0) {
|
|
35
|
-
end = i;
|
|
36
|
-
break;
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
if (end === -1) return { verdict: 'accepted', feedback: '' };
|
|
41
|
-
try {
|
|
42
|
-
const parsed = JSON.parse(text.slice(start, end + 1)) as Partial<ReviewResult>;
|
|
43
|
-
return {
|
|
44
|
-
verdict: parsed.verdict === 'revise' ? 'revise' : 'accepted',
|
|
45
|
-
feedback: parsed.feedback ?? ''
|
|
46
|
-
};
|
|
47
|
-
} catch {
|
|
48
|
-
return { verdict: 'accepted', feedback: '' };
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
export function buildDraftPrompt(req: DraftRequest): string {
|
|
53
|
-
const parts = [
|
|
54
|
-
`You are improving skill annotations for "${req.skill.name}".`,
|
|
55
|
-
`Tool: ${req.toolName}`,
|
|
56
|
-
`Tag to fill: @${req.tag}`,
|
|
57
|
-
`Guidance: ${req.suggestion}`,
|
|
58
|
-
req.currentValue ? `Current value:\n${req.currentValue}` : 'No current value.',
|
|
59
|
-
'Write only the annotation content — no code fences, no extra commentary.'
|
|
60
|
-
];
|
|
61
|
-
if (req.guidance) {
|
|
62
|
-
parts.push(`Conventions (follow these):\n${req.guidance}`);
|
|
63
|
-
}
|
|
64
|
-
return parts.join('\n\n');
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
export function buildReviewPrompt(req: ReviewRequest): string {
|
|
68
|
-
const parts = [
|
|
69
|
-
`You are reviewing a skill annotation draft for "${req.skill.name}".`,
|
|
70
|
-
`Tool: ${req.toolName}, Tag: @${req.tag}`,
|
|
71
|
-
`Guidance the drafter was given: ${req.suggestion}`,
|
|
72
|
-
`Draft:\n${req.draft}`
|
|
73
|
-
];
|
|
74
|
-
if (req.guidance) {
|
|
75
|
-
parts.push(`Conventions (follow these):\n${req.guidance}`);
|
|
76
|
-
}
|
|
77
|
-
parts.push(
|
|
78
|
-
'Respond with JSON only: {"verdict":"accepted"|"revise","feedback":"..."}.',
|
|
79
|
-
'Accept if the draft meaningfully addresses the guidance. Revise if it is vague or incorrect.'
|
|
80
|
-
);
|
|
81
|
-
return parts.join('\n\n');
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
export class AnthropicModelClient implements ModelClient {
|
|
85
|
-
private client = new Anthropic();
|
|
86
|
-
|
|
87
|
-
async draft(req: DraftRequest): Promise<string> {
|
|
88
|
-
const prompt = buildDraftPrompt(req);
|
|
89
|
-
|
|
90
|
-
const msg = await this.client.messages.create({
|
|
91
|
-
model: DRAFTER,
|
|
92
|
-
max_tokens: MAX_TOKENS,
|
|
93
|
-
messages: [{ role: 'user', content: prompt }]
|
|
94
|
-
});
|
|
95
|
-
const block = msg.content[0];
|
|
96
|
-
if (!block || block.type !== 'text') {
|
|
97
|
-
throw new Error(`Unexpected response from ${DRAFTER}: no text block`);
|
|
98
|
-
}
|
|
99
|
-
return block.text.trim();
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
async review(req: ReviewRequest): Promise<ReviewResult> {
|
|
103
|
-
const prompt = buildReviewPrompt(req);
|
|
104
|
-
|
|
105
|
-
const msg = await this.client.messages.create({
|
|
106
|
-
model: REVIEWER,
|
|
107
|
-
max_tokens: MAX_TOKENS,
|
|
108
|
-
messages: [{ role: 'user', content: prompt }]
|
|
109
|
-
});
|
|
110
|
-
const block = msg.content[0];
|
|
111
|
-
if (!block || block.type !== 'text') {
|
|
112
|
-
throw new Error(`Unexpected response from ${REVIEWER}: no text block`);
|
|
113
|
-
}
|
|
114
|
-
return parseReviewVerdict(block.text);
|
|
115
|
-
}
|
|
116
|
-
}
|
|
@@ -1,178 +0,0 @@
|
|
|
1
|
-
// packages/client/src/model/cli/adapters.ts
|
|
2
|
-
import { DRAFTER, REVIEWER } from '../models.js';
|
|
3
|
-
|
|
4
|
-
export type CliModelClientKind = 'claude' | 'codex' | 'copilot';
|
|
5
|
-
export type ModelRole = 'draft' | 'review';
|
|
6
|
-
|
|
7
|
-
export interface CliInvocation {
|
|
8
|
-
/** Executable name. */
|
|
9
|
-
cmd: string;
|
|
10
|
-
/** Arguments (no shell). */
|
|
11
|
-
args: string[];
|
|
12
|
-
/** Prompt to pipe to stdin, if this CLI takes the prompt that way. */
|
|
13
|
-
input?: string;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export interface CliAdapter {
|
|
17
|
-
readonly name: CliModelClientKind;
|
|
18
|
-
/** How to invoke the CLI for a given role with a given prompt. */
|
|
19
|
-
invocation(role: ModelRole, prompt: string): CliInvocation;
|
|
20
|
-
/**
|
|
21
|
-
* Extract the model's answer text from the CLI's stdout. An empty-string
|
|
22
|
-
* answer is valid and returned as-is; throws only when no answer message is
|
|
23
|
-
* present or the CLI reported an in-band error.
|
|
24
|
-
*/
|
|
25
|
-
extractResult(stdout: string): string;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/** Minimal shape a JSONL line is narrowed to before an adapter inspects it. */
|
|
29
|
-
interface JsonlEvent {
|
|
30
|
-
type?: string;
|
|
31
|
-
item?: { type?: string; text?: string };
|
|
32
|
-
data?: { content?: string };
|
|
33
|
-
exitCode?: number;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* Walk a JSONL stream (one JSON object per line), skip non-`{` log/noise lines
|
|
38
|
-
* and unparseable lines, and return the LAST value `extract` accepts. Returning
|
|
39
|
-
* `''` from `extract` is a valid match (the empty answer wins last) — only
|
|
40
|
-
* `undefined` means "not a match", so the adapter never editorializes empty
|
|
41
|
-
* model output.
|
|
42
|
-
*/
|
|
43
|
-
function lastJsonlMatch<T>(
|
|
44
|
-
stdout: string,
|
|
45
|
-
extract: (evt: JsonlEvent) => T | undefined
|
|
46
|
-
): T | undefined {
|
|
47
|
-
let last: T | undefined;
|
|
48
|
-
for (const line of stdout.split('\n')) {
|
|
49
|
-
const trimmed = line.trim();
|
|
50
|
-
if (!trimmed.startsWith('{')) continue;
|
|
51
|
-
let evt: JsonlEvent;
|
|
52
|
-
try {
|
|
53
|
-
evt = JSON.parse(trimmed) as JsonlEvent;
|
|
54
|
-
} catch {
|
|
55
|
-
continue;
|
|
56
|
-
}
|
|
57
|
-
const got = extract(evt);
|
|
58
|
-
if (got !== undefined) last = got;
|
|
59
|
-
}
|
|
60
|
-
return last;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
/** Best-effort scan for a JSONL failure signal, used to enrich no-match errors. */
|
|
64
|
-
function jsonlFailureSignal(
|
|
65
|
-
stdout: string,
|
|
66
|
-
detect: (evt: JsonlEvent) => string | undefined
|
|
67
|
-
): string | undefined {
|
|
68
|
-
let signal: string | undefined;
|
|
69
|
-
for (const line of stdout.split('\n')) {
|
|
70
|
-
const trimmed = line.trim();
|
|
71
|
-
if (!trimmed.startsWith('{')) continue;
|
|
72
|
-
let evt: JsonlEvent;
|
|
73
|
-
try {
|
|
74
|
-
evt = JSON.parse(trimmed) as JsonlEvent;
|
|
75
|
-
} catch {
|
|
76
|
-
continue;
|
|
77
|
-
}
|
|
78
|
-
const got = detect(evt);
|
|
79
|
-
if (got !== undefined) signal = got;
|
|
80
|
-
}
|
|
81
|
-
return signal;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
export const claudeAdapter: CliAdapter = {
|
|
85
|
-
name: 'claude',
|
|
86
|
-
invocation(role, prompt) {
|
|
87
|
-
const model = role === 'review' ? REVIEWER : DRAFTER;
|
|
88
|
-
return {
|
|
89
|
-
cmd: 'claude',
|
|
90
|
-
args: ['-p', '--output-format', 'json', '--model', model],
|
|
91
|
-
input: prompt
|
|
92
|
-
};
|
|
93
|
-
},
|
|
94
|
-
extractResult(stdout) {
|
|
95
|
-
let parsed: { is_error?: boolean; result?: string };
|
|
96
|
-
try {
|
|
97
|
-
parsed = JSON.parse(stdout) as typeof parsed;
|
|
98
|
-
} catch {
|
|
99
|
-
throw new Error(`claude: could not parse JSON output: ${stdout.slice(0, 200)}`);
|
|
100
|
-
}
|
|
101
|
-
if (parsed.is_error) {
|
|
102
|
-
throw new Error(`claude reported an error: ${parsed.result ?? '(no result)'}`);
|
|
103
|
-
}
|
|
104
|
-
if (typeof parsed.result !== 'string') {
|
|
105
|
-
throw new Error(`claude: no result field in output: ${stdout.slice(0, 200)}`);
|
|
106
|
-
}
|
|
107
|
-
return parsed.result;
|
|
108
|
-
}
|
|
109
|
-
};
|
|
110
|
-
|
|
111
|
-
export const codexAdapter: CliAdapter = {
|
|
112
|
-
name: 'codex',
|
|
113
|
-
invocation(_role, prompt) {
|
|
114
|
-
// codex uses its default model (no per-role mapping); prompt on stdin.
|
|
115
|
-
return { cmd: 'codex', args: ['exec', '--json'], input: prompt };
|
|
116
|
-
},
|
|
117
|
-
extractResult(stdout) {
|
|
118
|
-
const last = lastJsonlMatch(stdout, (evt) =>
|
|
119
|
-
evt.type === 'item.completed' &&
|
|
120
|
-
evt.item?.type === 'agent_message' &&
|
|
121
|
-
typeof evt.item.text === 'string'
|
|
122
|
-
? evt.item.text
|
|
123
|
-
: undefined
|
|
124
|
-
);
|
|
125
|
-
if (last === undefined) {
|
|
126
|
-
const failure = jsonlFailureSignal(stdout, (evt) =>
|
|
127
|
-
evt.type === 'turn.failed' || (evt.type === 'item.completed' && evt.item?.type === 'error')
|
|
128
|
-
? evt.type === 'turn.failed'
|
|
129
|
-
? 'turn.failed'
|
|
130
|
-
: 'item.error'
|
|
131
|
-
: undefined
|
|
132
|
-
);
|
|
133
|
-
const detail = failure !== undefined ? ` (failure signal: ${failure})` : '';
|
|
134
|
-
throw new Error(`codex: no agent_message in output${detail}: ${stdout.slice(0, 200)}`);
|
|
135
|
-
}
|
|
136
|
-
return last;
|
|
137
|
-
}
|
|
138
|
-
};
|
|
139
|
-
|
|
140
|
-
export const copilotAdapter: CliAdapter = {
|
|
141
|
-
name: 'copilot',
|
|
142
|
-
invocation(_role, prompt) {
|
|
143
|
-
// copilot uses its default model; prompt piped via stdin (not `-p <text>`)
|
|
144
|
-
// so untrusted prompt content never reaches argv — keeps the Windows
|
|
145
|
-
// shell-launch path injection-safe (only static flags are args).
|
|
146
|
-
return { cmd: 'copilot', args: ['--output-format', 'json', '--no-color'], input: prompt };
|
|
147
|
-
},
|
|
148
|
-
extractResult(stdout) {
|
|
149
|
-
// copilot emits JSONL; the answer is the last `assistant.message` event's
|
|
150
|
-
// data.content (skip `assistant.message_delta` streaming events + the final
|
|
151
|
-
// `result` event). Verified live 2026-06-03.
|
|
152
|
-
const last = lastJsonlMatch(stdout, (evt) =>
|
|
153
|
-
evt.type === 'assistant.message' && typeof evt.data?.content === 'string'
|
|
154
|
-
? evt.data.content
|
|
155
|
-
: undefined
|
|
156
|
-
);
|
|
157
|
-
if (last === undefined) {
|
|
158
|
-
const failure = jsonlFailureSignal(stdout, (evt) =>
|
|
159
|
-
evt.type === 'result' && typeof evt.exitCode === 'number' && evt.exitCode !== 0
|
|
160
|
-
? `exitCode ${evt.exitCode}`
|
|
161
|
-
: undefined
|
|
162
|
-
);
|
|
163
|
-
const detail = failure !== undefined ? ` (failure signal: ${failure})` : '';
|
|
164
|
-
throw new Error(`copilot: no assistant.message in output${detail}: ${stdout.slice(0, 200)}`);
|
|
165
|
-
}
|
|
166
|
-
return last;
|
|
167
|
-
}
|
|
168
|
-
};
|
|
169
|
-
|
|
170
|
-
const ADAPTERS: Record<CliModelClientKind, CliAdapter> = {
|
|
171
|
-
claude: claudeAdapter,
|
|
172
|
-
codex: codexAdapter,
|
|
173
|
-
copilot: copilotAdapter
|
|
174
|
-
};
|
|
175
|
-
|
|
176
|
-
export function adapterFor(kind: CliModelClientKind): CliAdapter {
|
|
177
|
-
return ADAPTERS[kind];
|
|
178
|
-
}
|
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
// packages/client/src/model/cli/cli-client.ts
|
|
2
|
-
import type { DraftRequest, ReviewRequest, ReviewResult, ModelClient } from '@skillit/core';
|
|
3
|
-
import { buildDraftPrompt, buildReviewPrompt, parseReviewVerdict } from '../anthropic.js';
|
|
4
|
-
import { runCli, type RunCliOptions } from './run.js';
|
|
5
|
-
import type { CliAdapter } from './adapters.js';
|
|
6
|
-
|
|
7
|
-
/** Injectable process runner (defaults to the real `runCli`). */
|
|
8
|
-
export type CliRunner = (opts: RunCliOptions) => Promise<string>;
|
|
9
|
-
|
|
10
|
-
export interface CliModelClientOptions {
|
|
11
|
-
runner?: CliRunner;
|
|
12
|
-
timeoutMs?: number;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* A {@link ModelClient} that drives an agent CLI (claude/codex/copilot) instead
|
|
17
|
-
* of the Anthropic API. Reuses the shared prompt builders and verdict parser;
|
|
18
|
-
* only the transport differs.
|
|
19
|
-
*/
|
|
20
|
-
export class CliModelClient implements ModelClient {
|
|
21
|
-
private readonly runner: CliRunner;
|
|
22
|
-
private readonly timeoutMs?: number;
|
|
23
|
-
|
|
24
|
-
constructor(
|
|
25
|
-
private readonly adapter: CliAdapter,
|
|
26
|
-
options: CliModelClientOptions = {}
|
|
27
|
-
) {
|
|
28
|
-
this.runner = options.runner ?? runCli;
|
|
29
|
-
this.timeoutMs = options.timeoutMs;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
private async run(role: 'draft' | 'review', prompt: string): Promise<string> {
|
|
33
|
-
const inv = this.adapter.invocation(role, prompt);
|
|
34
|
-
const stdout = await this.runner({
|
|
35
|
-
cmd: inv.cmd,
|
|
36
|
-
args: inv.args,
|
|
37
|
-
...(inv.input !== undefined ? { input: inv.input } : {}),
|
|
38
|
-
...(this.timeoutMs !== undefined ? { timeoutMs: this.timeoutMs } : {})
|
|
39
|
-
});
|
|
40
|
-
return this.adapter.extractResult(stdout);
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
async draft(req: DraftRequest): Promise<string> {
|
|
44
|
-
const result = await this.run('draft', buildDraftPrompt(req));
|
|
45
|
-
return result.trim();
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
async review(req: ReviewRequest): Promise<ReviewResult> {
|
|
49
|
-
const result = await this.run('review', buildReviewPrompt(req));
|
|
50
|
-
return parseReviewVerdict(result);
|
|
51
|
-
}
|
|
52
|
-
}
|
package/src/model/cli/run.ts
DELETED
|
@@ -1,83 +0,0 @@
|
|
|
1
|
-
// packages/client/src/model/cli/run.ts
|
|
2
|
-
import { spawn } from 'node:child_process';
|
|
3
|
-
|
|
4
|
-
export interface RunCliOptions {
|
|
5
|
-
/** Executable name (resolved on PATH) — never a shell string. */
|
|
6
|
-
cmd: string;
|
|
7
|
-
/** Arguments as an array — no shell, so no injection/escaping concerns. */
|
|
8
|
-
args: string[];
|
|
9
|
-
/** Optional text written to the child's stdin, then closed. */
|
|
10
|
-
input?: string;
|
|
11
|
-
/** Per-call timeout in milliseconds (default 120000). */
|
|
12
|
-
timeoutMs?: number;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
const DEFAULT_TIMEOUT_MS = 120_000;
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* Spawn `cmd` with `args` (no shell), optionally pipe `input` to stdin, and
|
|
19
|
-
* resolve the captured stdout. Throws on non-zero exit (message includes the
|
|
20
|
-
* command, exit code, and a stderr tail) or on timeout.
|
|
21
|
-
*/
|
|
22
|
-
export function runCli(opts: RunCliOptions): Promise<string> {
|
|
23
|
-
const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
24
|
-
return new Promise<string>((resolve, reject) => {
|
|
25
|
-
// On Windows, npm-installed CLIs are `.cmd`/`.bat` shims that Node cannot
|
|
26
|
-
// exec directly — they must go through the shell. This is injection-safe
|
|
27
|
-
// because every adapter delivers the (untrusted) prompt via stdin, so
|
|
28
|
-
// `args` only ever holds static flags and hardcoded model ids — no
|
|
29
|
-
// untrusted content reaches argv on any platform. On POSIX we keep the
|
|
30
|
-
// no-shell path regardless.
|
|
31
|
-
const child = spawn(opts.cmd, opts.args, {
|
|
32
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
33
|
-
shell: process.platform === 'win32'
|
|
34
|
-
});
|
|
35
|
-
let stdout = '';
|
|
36
|
-
let stderr = '';
|
|
37
|
-
let settled = false;
|
|
38
|
-
|
|
39
|
-
const timer = setTimeout(() => {
|
|
40
|
-
if (settled) return;
|
|
41
|
-
settled = true;
|
|
42
|
-
child.kill('SIGKILL');
|
|
43
|
-
reject(new Error(`${opts.cmd} timed out after ${timeoutMs}ms`));
|
|
44
|
-
}, timeoutMs);
|
|
45
|
-
|
|
46
|
-
child.stdout.on('data', (d: Buffer) => {
|
|
47
|
-
stdout += d.toString();
|
|
48
|
-
});
|
|
49
|
-
child.stderr.on('data', (d: Buffer) => {
|
|
50
|
-
stderr += d.toString();
|
|
51
|
-
});
|
|
52
|
-
child.on('error', (err) => {
|
|
53
|
-
if (settled) return;
|
|
54
|
-
settled = true;
|
|
55
|
-
clearTimeout(timer);
|
|
56
|
-
reject(new Error(`${opts.cmd} failed to start: ${err.message}`));
|
|
57
|
-
});
|
|
58
|
-
child.on('close', (code) => {
|
|
59
|
-
if (settled) return;
|
|
60
|
-
settled = true;
|
|
61
|
-
clearTimeout(timer);
|
|
62
|
-
if (code === 0) {
|
|
63
|
-
resolve(stdout);
|
|
64
|
-
} else {
|
|
65
|
-
const tail = stderr.trim().slice(-500);
|
|
66
|
-
// Intentionally omit args from the message: an adapter may pass the
|
|
67
|
-
// prompt as an argument (copilot), and the prompt can be large/sensitive.
|
|
68
|
-
reject(new Error(`${opts.cmd} exited with code ${code}: ${tail}`));
|
|
69
|
-
}
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
// The child may exit before consuming stdin (e.g. an auth/usage failure),
|
|
73
|
-
// in which case writing a large prompt emits EPIPE. Swallow stdin errors so
|
|
74
|
-
// they don't crash the process as an unhandled stream error — the `close`
|
|
75
|
-
// handler still rejects with the real exit code + stderr. Also omit args
|
|
76
|
-
// from that message: the prompt is delivered here, not via argv.
|
|
77
|
-
child.stdin.on('error', () => {});
|
|
78
|
-
if (opts.input !== undefined) {
|
|
79
|
-
child.stdin.write(opts.input);
|
|
80
|
-
}
|
|
81
|
-
child.stdin.end();
|
|
82
|
-
});
|
|
83
|
-
}
|
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
// packages/client/src/model/model-client-factory.ts
|
|
2
|
-
import { execFileSync } from 'node:child_process';
|
|
3
|
-
import type { ModelClient } from '@skillit/core';
|
|
4
|
-
import { AnthropicModelClient } from './anthropic.js';
|
|
5
|
-
import { CliModelClient } from './cli/cli-client.js';
|
|
6
|
-
import { adapterFor, type CliModelClientKind } from './cli/adapters.js';
|
|
7
|
-
|
|
8
|
-
export type ModelClientKind = 'api' | CliModelClientKind;
|
|
9
|
-
|
|
10
|
-
const CLI_KINDS: readonly CliModelClientKind[] = ['claude', 'codex', 'copilot'];
|
|
11
|
-
const ALL_KINDS = ['api', ...CLI_KINDS] as const;
|
|
12
|
-
|
|
13
|
-
export interface CreateModelClientOptions {
|
|
14
|
-
/** Per-call timeout for CLI invocations. */
|
|
15
|
-
timeoutMs?: number;
|
|
16
|
-
/** Injectable PATH check (defaults to a real `command -v` probe). */
|
|
17
|
-
hasBinary?: (cmd: string) => boolean;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
function defaultHasBinary(cmd: string): boolean {
|
|
21
|
-
try {
|
|
22
|
-
if (process.platform === 'win32') {
|
|
23
|
-
// `where` has no `-v` flag; `where <cmd>` exits non-zero when not found.
|
|
24
|
-
execFileSync('where', [cmd], { stdio: 'ignore' });
|
|
25
|
-
} else {
|
|
26
|
-
// POSIX: `command -v <cmd>` is a shell builtin, so run it through a shell.
|
|
27
|
-
execFileSync('command', ['-v', cmd], { stdio: 'ignore', shell: true });
|
|
28
|
-
}
|
|
29
|
-
return true;
|
|
30
|
-
} catch {
|
|
31
|
-
return false;
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function isCliKind(kind: string): kind is CliModelClientKind {
|
|
36
|
-
return (CLI_KINDS as readonly string[]).includes(kind);
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* Build the {@link ModelClient} for the requested backend. `'api'` → the
|
|
41
|
-
* Anthropic API client; a CLI kind → a {@link CliModelClient} after a PATH
|
|
42
|
-
* pre-flight. Throws an actionable error for an unknown kind or a missing CLI.
|
|
43
|
-
*/
|
|
44
|
-
export function createModelClient(
|
|
45
|
-
kind: string,
|
|
46
|
-
options: CreateModelClientOptions = {}
|
|
47
|
-
): ModelClient {
|
|
48
|
-
if (kind === 'api') return new AnthropicModelClient();
|
|
49
|
-
if (!isCliKind(kind)) {
|
|
50
|
-
throw new Error(`invalid --model-client '${kind}'. Use one of: ${ALL_KINDS.join('|')}.`);
|
|
51
|
-
}
|
|
52
|
-
const hasBinary = options.hasBinary ?? defaultHasBinary;
|
|
53
|
-
if (!hasBinary(kind)) {
|
|
54
|
-
throw new Error(
|
|
55
|
-
`${kind} CLI not found on PATH — install it, or use --model-client api (requires ANTHROPIC_API_KEY).`
|
|
56
|
-
);
|
|
57
|
-
}
|
|
58
|
-
return new CliModelClient(
|
|
59
|
-
adapterFor(kind),
|
|
60
|
-
options.timeoutMs !== undefined ? { timeoutMs: options.timeoutMs } : {}
|
|
61
|
-
);
|
|
62
|
-
}
|
package/src/model/models.ts
DELETED
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
// packages/client/src/model/models.ts
|
|
2
|
-
// Shared model identifiers for the refine drafter/reviewer roles. Imported by
|
|
3
|
-
// both the Anthropic API client and the claude CLI adapter so the role→model
|
|
4
|
-
// mapping has one source of truth.
|
|
5
|
-
export const DRAFTER = 'claude-sonnet-4-6';
|
|
6
|
-
export const REVIEWER = 'claude-opus-4-7';
|
|
7
|
-
export const MAX_TOKENS = 1024;
|
package/tsconfig.build.json
DELETED
package/tsconfig.json
DELETED