@skillit/client 0.2.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/CHANGELOG.md +32 -0
- package/LICENSE +21 -0
- package/dist/bin.d.ts +3 -0
- package/dist/bin.d.ts.map +1 -0
- package/dist/bin.js +16 -0
- package/dist/bin.js.map +1 -0
- package/dist/commands/init.d.ts +33 -0
- package/dist/commands/init.d.ts.map +1 -0
- package/dist/commands/init.js +152 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/commands/refine.d.ts +49 -0
- package/dist/commands/refine.d.ts.map +1 -0
- package/dist/commands/refine.js +201 -0
- package/dist/commands/refine.js.map +1 -0
- package/dist/detect-mode.d.ts +10 -0
- package/dist/detect-mode.d.ts.map +1 -0
- package/dist/detect-mode.js +88 -0
- package/dist/detect-mode.js.map +1 -0
- package/dist/detect-source.d.ts +46 -0
- package/dist/detect-source.d.ts.map +1 -0
- package/dist/detect-source.js +124 -0
- package/dist/detect-source.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -0
- package/dist/model/anthropic.d.ts +10 -0
- package/dist/model/anthropic.d.ts.map +1 -0
- package/dist/model/anthropic.js +110 -0
- package/dist/model/anthropic.js.map +1 -0
- package/dist/model/cli/adapters.d.ts +26 -0
- package/dist/model/cli/adapters.d.ts.map +1 -0
- package/dist/model/cli/adapters.js +133 -0
- package/dist/model/cli/adapters.js.map +1 -0
- package/dist/model/cli/cli-client.d.ts +24 -0
- package/dist/model/cli/cli-client.d.ts.map +1 -0
- package/dist/model/cli/cli-client.js +36 -0
- package/dist/model/cli/cli-client.js.map +1 -0
- package/dist/model/cli/run.d.ts +17 -0
- package/dist/model/cli/run.d.ts.map +1 -0
- package/dist/model/cli/run.js +72 -0
- package/dist/model/cli/run.js.map +1 -0
- package/dist/model/model-client-factory.d.ts +16 -0
- package/dist/model/model-client-factory.d.ts.map +1 -0
- package/dist/model/model-client-factory.js +44 -0
- package/dist/model/model-client-factory.js.map +1 -0
- package/dist/model/models.d.ts +4 -0
- package/dist/model/models.d.ts.map +1 -0
- package/dist/model/models.js +8 -0
- package/dist/model/models.js.map +1 -0
- package/package.json +31 -0
- package/scripts/gen-refine-cli-skill.mjs +48 -0
- package/src/__tests__/anthropic-model.test.ts +45 -0
- package/src/__tests__/anthropic-prompt.test.ts +102 -0
- package/src/__tests__/cli-adapters.test.ts +119 -0
- package/src/__tests__/cli-client.test.ts +58 -0
- package/src/__tests__/cli-run.test.ts +52 -0
- package/src/__tests__/detect-mode.test.ts +114 -0
- package/src/__tests__/detect-source.test.ts +158 -0
- package/src/__tests__/fixtures/bin-with-program.mjs +6 -0
- package/src/__tests__/init.test.ts +218 -0
- package/src/__tests__/model-client-factory.test.ts +28 -0
- package/src/__tests__/refine-resolve.test.ts +72 -0
- package/src/bin.ts +18 -0
- package/src/commands/init.ts +207 -0
- package/src/commands/refine.ts +261 -0
- package/src/detect-mode.ts +94 -0
- package/src/detect-source.ts +123 -0
- package/src/index.ts +3 -0
- package/src/model/anthropic.ts +116 -0
- package/src/model/cli/adapters.ts +178 -0
- package/src/model/cli/cli-client.ts +52 -0
- package/src/model/cli/run.ts +83 -0
- package/src/model/model-client-factory.ts +62 -0
- package/src/model/models.ts +7 -0
- package/tsconfig.build.json +9 -0
- package/tsconfig.json +4 -0
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { buildDraftPrompt, buildReviewPrompt, parseReviewVerdict } from '../anthropic.js';
|
|
2
|
+
import { runCli } from './run.js';
|
|
3
|
+
/**
|
|
4
|
+
* A {@link ModelClient} that drives an agent CLI (claude/codex/copilot) instead
|
|
5
|
+
* of the Anthropic API. Reuses the shared prompt builders and verdict parser;
|
|
6
|
+
* only the transport differs.
|
|
7
|
+
*/
|
|
8
|
+
export class CliModelClient {
|
|
9
|
+
adapter;
|
|
10
|
+
runner;
|
|
11
|
+
timeoutMs;
|
|
12
|
+
constructor(adapter, options = {}) {
|
|
13
|
+
this.adapter = adapter;
|
|
14
|
+
this.runner = options.runner ?? runCli;
|
|
15
|
+
this.timeoutMs = options.timeoutMs;
|
|
16
|
+
}
|
|
17
|
+
async run(role, prompt) {
|
|
18
|
+
const inv = this.adapter.invocation(role, prompt);
|
|
19
|
+
const stdout = await this.runner({
|
|
20
|
+
cmd: inv.cmd,
|
|
21
|
+
args: inv.args,
|
|
22
|
+
...(inv.input !== undefined ? { input: inv.input } : {}),
|
|
23
|
+
...(this.timeoutMs !== undefined ? { timeoutMs: this.timeoutMs } : {})
|
|
24
|
+
});
|
|
25
|
+
return this.adapter.extractResult(stdout);
|
|
26
|
+
}
|
|
27
|
+
async draft(req) {
|
|
28
|
+
const result = await this.run('draft', buildDraftPrompt(req));
|
|
29
|
+
return result.trim();
|
|
30
|
+
}
|
|
31
|
+
async review(req) {
|
|
32
|
+
const result = await this.run('review', buildReviewPrompt(req));
|
|
33
|
+
return parseReviewVerdict(result);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
//# sourceMappingURL=cli-client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli-client.js","sourceRoot":"","sources":["../../../src/model/cli/cli-client.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAC;AAC1F,OAAO,EAAE,MAAM,EAAsB,MAAM,UAAU,CAAC;AAWtD;;;;GAIG;AACH,MAAM,OAAO,cAAc;IAKN,OAAO;IAJT,MAAM,CAAY;IAClB,SAAS,CAAU;IAEpC,YACmB,OAAmB,EACpC,OAAO,GAA0B,EAAE;uBADlB,OAAO;QAGxB,IAAI,CAAC,MAAM,GAAG,OAAO,CAAC,MAAM,IAAI,MAAM,CAAC;QACvC,IAAI,CAAC,SAAS,GAAG,OAAO,CAAC,SAAS,CAAC;IACrC,CAAC;IAEO,KAAK,CAAC,GAAG,CAAC,IAAwB,EAAE,MAAc;QACxD,MAAM,GAAG,GAAG,IAAI,CAAC,OAAO,CAAC,UAAU,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;QAClD,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC;YAC/B,GAAG,EAAE,GAAG,CAAC,GAAG;YACZ,IAAI,EAAE,GAAG,CAAC,IAAI;YACd,GAAG,CAAC,GAAG,CAAC,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,GAAG,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;YACxD,GAAG,CAAC,IAAI,CAAC,SAAS,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,SAAS,EAAE,IAAI,CAAC,SAAS,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;SACvE,CAAC,CAAC;QACH,OAAO,IAAI,CAAC,OAAO,CAAC,aAAa,CAAC,MAAM,CAAC,CAAC;IAC5C,CAAC;IAED,KAAK,CAAC,KAAK,CAAC,GAAiB;QAC3B,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,GAAG,CAAC,OAAO,EAAE,gBAAgB,CAAC,GAAG,CAAC,CAAC,CAAC;QAC9D,OAAO,MAAM,CAAC,IAAI,EAAE,CAAC;IACvB,CAAC;IAED,KAAK,CAAC,MAAM,CAAC,GAAkB;QAC7B,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,GAAG,CAAC,QAAQ,EAAE,iBAAiB,CAAC,GAAG,CAAC,CAAC,CAAC;QAChE,OAAO,kBAAkB,CAAC,MAAM,CAAC,CAAC;IACpC,CAAC;CACF"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export interface RunCliOptions {
|
|
2
|
+
/** Executable name (resolved on PATH) — never a shell string. */
|
|
3
|
+
cmd: string;
|
|
4
|
+
/** Arguments as an array — no shell, so no injection/escaping concerns. */
|
|
5
|
+
args: string[];
|
|
6
|
+
/** Optional text written to the child's stdin, then closed. */
|
|
7
|
+
input?: string;
|
|
8
|
+
/** Per-call timeout in milliseconds (default 120000). */
|
|
9
|
+
timeoutMs?: number;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Spawn `cmd` with `args` (no shell), optionally pipe `input` to stdin, and
|
|
13
|
+
* resolve the captured stdout. Throws on non-zero exit (message includes the
|
|
14
|
+
* command, exit code, and a stderr tail) or on timeout.
|
|
15
|
+
*/
|
|
16
|
+
export declare function runCli(opts: RunCliOptions): Promise<string>;
|
|
17
|
+
//# sourceMappingURL=run.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"run.d.ts","sourceRoot":"","sources":["../../../src/model/cli/run.ts"],"names":[],"mappings":"AAGA,MAAM,WAAW,aAAa;IAC5B,iEAAiE;IACjE,GAAG,EAAE,MAAM,CAAC;IACZ,2EAA2E;IAC3E,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,+DAA+D;IAC/D,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,yDAAyD;IACzD,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAID;;;;GAIG;AACH,wBAAgB,MAAM,CAAC,IAAI,EAAE,aAAa,GAAG,OAAO,CAAC,MAAM,CAAC,CA6D3D"}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// packages/client/src/model/cli/run.ts
|
|
2
|
+
import { spawn } from 'node:child_process';
|
|
3
|
+
const DEFAULT_TIMEOUT_MS = 120_000;
|
|
4
|
+
/**
|
|
5
|
+
* Spawn `cmd` with `args` (no shell), optionally pipe `input` to stdin, and
|
|
6
|
+
* resolve the captured stdout. Throws on non-zero exit (message includes the
|
|
7
|
+
* command, exit code, and a stderr tail) or on timeout.
|
|
8
|
+
*/
|
|
9
|
+
export function runCli(opts) {
|
|
10
|
+
const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
11
|
+
return new Promise((resolve, reject) => {
|
|
12
|
+
// On Windows, npm-installed CLIs are `.cmd`/`.bat` shims that Node cannot
|
|
13
|
+
// exec directly — they must go through the shell. This is injection-safe
|
|
14
|
+
// because every adapter delivers the (untrusted) prompt via stdin, so
|
|
15
|
+
// `args` only ever holds static flags and hardcoded model ids — no
|
|
16
|
+
// untrusted content reaches argv on any platform. On POSIX we keep the
|
|
17
|
+
// no-shell path regardless.
|
|
18
|
+
const child = spawn(opts.cmd, opts.args, {
|
|
19
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
20
|
+
shell: process.platform === 'win32'
|
|
21
|
+
});
|
|
22
|
+
let stdout = '';
|
|
23
|
+
let stderr = '';
|
|
24
|
+
let settled = false;
|
|
25
|
+
const timer = setTimeout(() => {
|
|
26
|
+
if (settled)
|
|
27
|
+
return;
|
|
28
|
+
settled = true;
|
|
29
|
+
child.kill('SIGKILL');
|
|
30
|
+
reject(new Error(`${opts.cmd} timed out after ${timeoutMs}ms`));
|
|
31
|
+
}, timeoutMs);
|
|
32
|
+
child.stdout.on('data', (d) => {
|
|
33
|
+
stdout += d.toString();
|
|
34
|
+
});
|
|
35
|
+
child.stderr.on('data', (d) => {
|
|
36
|
+
stderr += d.toString();
|
|
37
|
+
});
|
|
38
|
+
child.on('error', (err) => {
|
|
39
|
+
if (settled)
|
|
40
|
+
return;
|
|
41
|
+
settled = true;
|
|
42
|
+
clearTimeout(timer);
|
|
43
|
+
reject(new Error(`${opts.cmd} failed to start: ${err.message}`));
|
|
44
|
+
});
|
|
45
|
+
child.on('close', (code) => {
|
|
46
|
+
if (settled)
|
|
47
|
+
return;
|
|
48
|
+
settled = true;
|
|
49
|
+
clearTimeout(timer);
|
|
50
|
+
if (code === 0) {
|
|
51
|
+
resolve(stdout);
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
const tail = stderr.trim().slice(-500);
|
|
55
|
+
// Intentionally omit args from the message: an adapter may pass the
|
|
56
|
+
// prompt as an argument (copilot), and the prompt can be large/sensitive.
|
|
57
|
+
reject(new Error(`${opts.cmd} exited with code ${code}: ${tail}`));
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
// The child may exit before consuming stdin (e.g. an auth/usage failure),
|
|
61
|
+
// in which case writing a large prompt emits EPIPE. Swallow stdin errors so
|
|
62
|
+
// they don't crash the process as an unhandled stream error — the `close`
|
|
63
|
+
// handler still rejects with the real exit code + stderr. Also omit args
|
|
64
|
+
// from that message: the prompt is delivered here, not via argv.
|
|
65
|
+
child.stdin.on('error', () => { });
|
|
66
|
+
if (opts.input !== undefined) {
|
|
67
|
+
child.stdin.write(opts.input);
|
|
68
|
+
}
|
|
69
|
+
child.stdin.end();
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
//# sourceMappingURL=run.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"run.js","sourceRoot":"","sources":["../../../src/model/cli/run.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { ModelClient } from '@skillit/core';
|
|
2
|
+
import { type CliModelClientKind } from './cli/adapters.js';
|
|
3
|
+
export type ModelClientKind = 'api' | CliModelClientKind;
|
|
4
|
+
export interface CreateModelClientOptions {
|
|
5
|
+
/** Per-call timeout for CLI invocations. */
|
|
6
|
+
timeoutMs?: number;
|
|
7
|
+
/** Injectable PATH check (defaults to a real `command -v` probe). */
|
|
8
|
+
hasBinary?: (cmd: string) => boolean;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Build the {@link ModelClient} for the requested backend. `'api'` → the
|
|
12
|
+
* Anthropic API client; a CLI kind → a {@link CliModelClient} after a PATH
|
|
13
|
+
* pre-flight. Throws an actionable error for an unknown kind or a missing CLI.
|
|
14
|
+
*/
|
|
15
|
+
export declare function createModelClient(kind: string, options?: CreateModelClientOptions): ModelClient;
|
|
16
|
+
//# sourceMappingURL=model-client-factory.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"model-client-factory.d.ts","sourceRoot":"","sources":["../../src/model/model-client-factory.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAGjD,OAAO,EAAc,KAAK,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AAExE,MAAM,MAAM,eAAe,GAAG,KAAK,GAAG,kBAAkB,CAAC;AAKzD,MAAM,WAAW,wBAAwB;IACvC,4CAA4C;IAC5C,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,qEAAqE;IACrE,SAAS,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC;CACtC;AAqBD;;;;GAIG;AACH,wBAAgB,iBAAiB,CAC/B,IAAI,EAAE,MAAM,EACZ,OAAO,GAAE,wBAA6B,GACrC,WAAW,CAeb"}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// packages/client/src/model/model-client-factory.ts
|
|
2
|
+
import { execFileSync } from 'node:child_process';
|
|
3
|
+
import { AnthropicModelClient } from './anthropic.js';
|
|
4
|
+
import { CliModelClient } from './cli/cli-client.js';
|
|
5
|
+
import { adapterFor } from './cli/adapters.js';
|
|
6
|
+
const CLI_KINDS = ['claude', 'codex', 'copilot'];
|
|
7
|
+
const ALL_KINDS = ['api', ...CLI_KINDS];
|
|
8
|
+
function defaultHasBinary(cmd) {
|
|
9
|
+
try {
|
|
10
|
+
if (process.platform === 'win32') {
|
|
11
|
+
// `where` has no `-v` flag; `where <cmd>` exits non-zero when not found.
|
|
12
|
+
execFileSync('where', [cmd], { stdio: 'ignore' });
|
|
13
|
+
}
|
|
14
|
+
else {
|
|
15
|
+
// POSIX: `command -v <cmd>` is a shell builtin, so run it through a shell.
|
|
16
|
+
execFileSync('command', ['-v', cmd], { stdio: 'ignore', shell: true });
|
|
17
|
+
}
|
|
18
|
+
return true;
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
function isCliKind(kind) {
|
|
25
|
+
return CLI_KINDS.includes(kind);
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Build the {@link ModelClient} for the requested backend. `'api'` → the
|
|
29
|
+
* Anthropic API client; a CLI kind → a {@link CliModelClient} after a PATH
|
|
30
|
+
* pre-flight. Throws an actionable error for an unknown kind or a missing CLI.
|
|
31
|
+
*/
|
|
32
|
+
export function createModelClient(kind, options = {}) {
|
|
33
|
+
if (kind === 'api')
|
|
34
|
+
return new AnthropicModelClient();
|
|
35
|
+
if (!isCliKind(kind)) {
|
|
36
|
+
throw new Error(`invalid --model-client '${kind}'. Use one of: ${ALL_KINDS.join('|')}.`);
|
|
37
|
+
}
|
|
38
|
+
const hasBinary = options.hasBinary ?? defaultHasBinary;
|
|
39
|
+
if (!hasBinary(kind)) {
|
|
40
|
+
throw new Error(`${kind} CLI not found on PATH — install it, or use --model-client api (requires ANTHROPIC_API_KEY).`);
|
|
41
|
+
}
|
|
42
|
+
return new CliModelClient(adapterFor(kind), options.timeoutMs !== undefined ? { timeoutMs: options.timeoutMs } : {});
|
|
43
|
+
}
|
|
44
|
+
//# sourceMappingURL=model-client-factory.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"model-client-factory.js","sourceRoot":"","sources":["../../src/model/model-client-factory.ts"],"names":[],"mappings":"AAAA,oDAAoD;AACpD,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAElD,OAAO,EAAE,oBAAoB,EAAE,MAAM,gBAAgB,CAAC;AACtD,OAAO,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AACrD,OAAO,EAAE,UAAU,EAA2B,MAAM,mBAAmB,CAAC;AAIxE,MAAM,SAAS,GAAkC,CAAC,QAAQ,EAAE,OAAO,EAAE,SAAS,CAAC,CAAC;AAChF,MAAM,SAAS,GAAG,CAAC,KAAK,EAAE,GAAG,SAAS,CAAU,CAAC;AASjD,SAAS,gBAAgB,CAAC,GAAW;IACnC,IAAI,CAAC;QACH,IAAI,OAAO,CAAC,QAAQ,KAAK,OAAO,EAAE,CAAC;YACjC,yEAAyE;YACzE,YAAY,CAAC,OAAO,EAAE,CAAC,GAAG,CAAC,EAAE,EAAE,KAAK,EAAE,QAAQ,EAAE,CAAC,CAAC;QACpD,CAAC;aAAM,CAAC;YACN,2EAA2E;YAC3E,YAAY,CAAC,SAAS,EAAE,CAAC,IAAI,EAAE,GAAG,CAAC,EAAE,EAAE,KAAK,EAAE,QAAQ,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;QACzE,CAAC;QACD,OAAO,IAAI,CAAC;IACd,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC;AAED,SAAS,SAAS,CAAC,IAAY;IAC7B,OAAQ,SAA+B,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;AACzD,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,iBAAiB,CAC/B,IAAY,EACZ,OAAO,GAA6B,EAAE;IAEtC,IAAI,IAAI,KAAK,KAAK;QAAE,OAAO,IAAI,oBAAoB,EAAE,CAAC;IACtD,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,EAAE,CAAC;QACrB,MAAM,IAAI,KAAK,CAAC,2BAA2B,IAAI,kBAAkB,SAAS,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;IAC3F,CAAC;IACD,MAAM,SAAS,GAAG,OAAO,CAAC,SAAS,IAAI,gBAAgB,CAAC;IACxD,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,EAAE,CAAC;QACrB,MAAM,IAAI,KAAK,CACb,GAAG,IAAI,8FAA8F,CACtG,CAAC;IACJ,CAAC;IACD,OAAO,IAAI,cAAc,CACvB,UAAU,CAAC,IAAI,CAAC,EAChB,OAAO,CAAC,SAAS,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,SAAS,EAAE,OAAO,CAAC,SAAS,EAAE,CAAC,CAAC,CAAC,EAAE,CACxE,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"models.d.ts","sourceRoot":"","sources":["../../src/model/models.ts"],"names":[],"mappings":"AAIA,eAAO,MAAM,OAAO,sBAAsB,CAAC;AAC3C,eAAO,MAAM,QAAQ,oBAAoB,CAAC;AAC1C,eAAO,MAAM,UAAU,OAAO,CAAC"}
|
|
@@ -0,0 +1,8 @@
|
|
|
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;
|
|
8
|
+
//# sourceMappingURL=models.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"models.js","sourceRoot":"","sources":["../../src/model/models.ts"],"names":[],"mappings":"AAAA,sCAAsC;AACtC,8EAA8E;AAC9E,6EAA6E;AAC7E,mCAAmC;AACnC,MAAM,CAAC,MAAM,OAAO,GAAG,mBAAmB,CAAC;AAC3C,MAAM,CAAC,MAAM,QAAQ,GAAG,iBAAiB,CAAC;AAC1C,MAAM,CAAC,MAAM,UAAU,GAAG,IAAI,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@skillit/client",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Anthropic model client + skillit CLI (refine command)",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "Pradeep Mouli",
|
|
7
|
+
"bin": {
|
|
8
|
+
"skillit": "./dist/bin.js"
|
|
9
|
+
},
|
|
10
|
+
"type": "module",
|
|
11
|
+
"main": "./dist/index.js",
|
|
12
|
+
"types": "./dist/index.d.ts",
|
|
13
|
+
"exports": {
|
|
14
|
+
".": {
|
|
15
|
+
"types": "./dist/index.d.ts",
|
|
16
|
+
"import": "./dist/index.js"
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
"dependencies": {
|
|
20
|
+
"@anthropic-ai/sdk": "^0.100.0",
|
|
21
|
+
"commander": "^14.0.3",
|
|
22
|
+
"@skillit/mcp": "0.3.0",
|
|
23
|
+
"@skillit/core": "1.5.0",
|
|
24
|
+
"@skillit/cli": "0.4.0"
|
|
25
|
+
},
|
|
26
|
+
"scripts": {
|
|
27
|
+
"build": "tsgo -p tsconfig.build.json",
|
|
28
|
+
"gen-cli-skill": "pnpm build && node scripts/gen-refine-cli-skill.mjs",
|
|
29
|
+
"type-check": "tsgo --noEmit"
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// Dogfood: generate a skill for the `skillit` CLI binary (the `refine` command)
|
|
2
|
+
// using @skillit/cli's own commander introspection.
|
|
3
|
+
//
|
|
4
|
+
// Run via the package script (builds first): pnpm --filter @skillit/client gen-cli-skill
|
|
5
|
+
// Or directly, after building @skillit/client: node packages/client/scripts/gen-refine-cli-skill.mjs
|
|
6
|
+
import { existsSync } from 'node:fs';
|
|
7
|
+
import { fileURLToPath } from 'node:url';
|
|
8
|
+
import { dirname, resolve } from 'node:path';
|
|
9
|
+
import { Command } from 'commander';
|
|
10
|
+
import { extractCliSkill, writeCliSkill } from '@skillit/cli';
|
|
11
|
+
|
|
12
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
13
|
+
const repoRoot = resolve(here, '../../..');
|
|
14
|
+
|
|
15
|
+
// This script reads the refine command from build output. `dist/` is not in the
|
|
16
|
+
// repo by default, so fail with an actionable message rather than a raw
|
|
17
|
+
// ERR_MODULE_NOT_FOUND when it is missing or stale.
|
|
18
|
+
const refineDist = resolve(here, '../dist/commands/refine.js');
|
|
19
|
+
if (!existsSync(refineDist)) {
|
|
20
|
+
console.error(
|
|
21
|
+
`Cannot find ${refineDist}.\nBuild @skillit/client first: pnpm --filter @skillit/client build`
|
|
22
|
+
);
|
|
23
|
+
process.exit(1);
|
|
24
|
+
}
|
|
25
|
+
const { buildRefineCommand } = await import(refineDist);
|
|
26
|
+
|
|
27
|
+
// Reconstruct the same program shape that packages/client/src/bin.ts ships.
|
|
28
|
+
const program = new Command('skillit').description('skillit CLI').version('0.1.0');
|
|
29
|
+
program.addCommand(buildRefineCommand());
|
|
30
|
+
|
|
31
|
+
const skill = await extractCliSkill({
|
|
32
|
+
program,
|
|
33
|
+
metadata: {
|
|
34
|
+
name: 'skillit-refine',
|
|
35
|
+
description:
|
|
36
|
+
'Autonomously improve an MCP skill via the skillit audit→draft→review loop (build or runtime mode)',
|
|
37
|
+
keywords: ['skillit', 'refine', 'mcp', 'skill-generation', 'cli', 'audit', 'overlay'],
|
|
38
|
+
repository: 'https://github.com/pradeepmouli/skillit'
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
writeCliSkill(skill, { outDir: resolve(repoRoot, 'skills') });
|
|
43
|
+
|
|
44
|
+
const issues = skill.audit?.issues ?? [];
|
|
45
|
+
console.log(`Generated skills/skillit-refine/ — ${issues.length} audit finding(s).`);
|
|
46
|
+
for (const i of issues) {
|
|
47
|
+
console.log(` [${i.severity ?? '?'}] ${i.code ?? ''} ${i.message ?? ''}`.trimEnd());
|
|
48
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// packages/client/src/__tests__/anthropic-model.test.ts
|
|
2
|
+
import { describe, it, expect } from 'vitest';
|
|
3
|
+
import { parseReviewVerdict } from '../model/anthropic.js';
|
|
4
|
+
|
|
5
|
+
describe('parseReviewVerdict', () => {
|
|
6
|
+
it('parses accepted verdict', () => {
|
|
7
|
+
const text = 'Looks good. {"verdict":"accepted","feedback":""}';
|
|
8
|
+
expect(parseReviewVerdict(text)).toEqual({ verdict: 'accepted', feedback: '' });
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it('parses revise verdict', () => {
|
|
12
|
+
const text = '{"verdict":"revise","feedback":"Be more specific about edge cases"}';
|
|
13
|
+
expect(parseReviewVerdict(text)).toEqual({
|
|
14
|
+
verdict: 'revise',
|
|
15
|
+
feedback: 'Be more specific about edge cases'
|
|
16
|
+
});
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('fails open on malformed JSON — returns accepted', () => {
|
|
20
|
+
expect(parseReviewVerdict('not json at all')).toEqual({ verdict: 'accepted', feedback: '' });
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('fails open on missing verdict field', () => {
|
|
24
|
+
expect(parseReviewVerdict('{"feedback":"ok"}')).toEqual({
|
|
25
|
+
verdict: 'accepted',
|
|
26
|
+
feedback: 'ok'
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('parses revise verdict when feedback contains balanced braces', () => {
|
|
31
|
+
const text = '{"verdict":"revise","feedback":"handle {edge-case} and {null} inputs"}';
|
|
32
|
+
expect(parseReviewVerdict(text)).toEqual({
|
|
33
|
+
verdict: 'revise',
|
|
34
|
+
feedback: 'handle {edge-case} and {null} inputs'
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('parses revise verdict when feedback contains an unbalanced open brace', () => {
|
|
39
|
+
const text = '{"verdict":"revise","feedback":"use {var in templates"}';
|
|
40
|
+
expect(parseReviewVerdict(text)).toEqual({
|
|
41
|
+
verdict: 'revise',
|
|
42
|
+
feedback: 'use {var in templates'
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
});
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
// packages/client/src/__tests__/anthropic-prompt.test.ts
|
|
2
|
+
import { describe, it, expect } from 'vitest';
|
|
3
|
+
import { buildDraftPrompt, buildReviewPrompt } from '../model/anthropic.js';
|
|
4
|
+
import type { DraftRequest, ReviewRequest, ExtractedSkill } from '@skillit/core';
|
|
5
|
+
|
|
6
|
+
const baseSkill = (): ExtractedSkill =>
|
|
7
|
+
({ name: 'my-tool', functions: [] }) as unknown as ExtractedSkill;
|
|
8
|
+
|
|
9
|
+
const baseDraftReq = (overrides: Partial<DraftRequest> = {}): DraftRequest => ({
|
|
10
|
+
toolName: 'tool_a',
|
|
11
|
+
tag: 'useWhen',
|
|
12
|
+
suggestion: 'Add @useWhen annotation',
|
|
13
|
+
currentValue: undefined,
|
|
14
|
+
skill: baseSkill(),
|
|
15
|
+
...overrides
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
const baseReviewReq = (overrides: Partial<ReviewRequest> = {}): ReviewRequest => ({
|
|
19
|
+
toolName: 'tool_a',
|
|
20
|
+
tag: 'useWhen',
|
|
21
|
+
draft: 'Use this tool when listing files',
|
|
22
|
+
suggestion: 'Add @useWhen annotation',
|
|
23
|
+
skill: baseSkill(),
|
|
24
|
+
...overrides
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
describe('buildDraftPrompt', () => {
|
|
28
|
+
it('includes a Conventions section with guidance text when guidance is provided', () => {
|
|
29
|
+
const req = baseDraftReq({ guidance: 'Always use active voice.' });
|
|
30
|
+
const prompt = buildDraftPrompt(req);
|
|
31
|
+
expect(prompt).toContain('Conventions');
|
|
32
|
+
expect(prompt).toContain('Always use active voice.');
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('does NOT include a Conventions section when guidance is undefined', () => {
|
|
36
|
+
const req = baseDraftReq({ guidance: undefined });
|
|
37
|
+
const prompt = buildDraftPrompt(req);
|
|
38
|
+
expect(prompt).not.toContain('Conventions');
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('does NOT include a Conventions section when guidance is absent', () => {
|
|
42
|
+
const req = baseDraftReq();
|
|
43
|
+
const prompt = buildDraftPrompt(req);
|
|
44
|
+
expect(prompt).not.toContain('Conventions');
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('still includes core prompt content regardless of guidance', () => {
|
|
48
|
+
const req = baseDraftReq({ guidance: 'Some guidance.' });
|
|
49
|
+
const prompt = buildDraftPrompt(req);
|
|
50
|
+
expect(prompt).toContain('tool_a');
|
|
51
|
+
expect(prompt).toContain('@useWhen');
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('uses source-neutral framing (no hardcoded MCP) and keeps guidance', () => {
|
|
55
|
+
const req = baseDraftReq({ guidance: 'CLI conventions: use --flag syntax.' });
|
|
56
|
+
const prompt = buildDraftPrompt(req);
|
|
57
|
+
expect(prompt).not.toContain('MCP');
|
|
58
|
+
expect(prompt).toContain('skill annotations for "my-tool"');
|
|
59
|
+
expect(prompt).toContain('Conventions');
|
|
60
|
+
expect(prompt).toContain('CLI conventions: use --flag syntax.');
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
describe('buildReviewPrompt', () => {
|
|
65
|
+
it('includes a Conventions section with guidance text when guidance is provided', () => {
|
|
66
|
+
const req = baseReviewReq({ guidance: 'Always use active voice.' });
|
|
67
|
+
const prompt = buildReviewPrompt(req);
|
|
68
|
+
expect(prompt).toContain('Conventions');
|
|
69
|
+
expect(prompt).toContain('Always use active voice.');
|
|
70
|
+
expect(prompt.indexOf('Conventions')).toBeLessThan(prompt.indexOf('Respond with JSON only'));
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('does NOT include a Conventions section when guidance is undefined', () => {
|
|
74
|
+
const req = baseReviewReq({ guidance: undefined });
|
|
75
|
+
const prompt = buildReviewPrompt(req);
|
|
76
|
+
expect(prompt).not.toContain('Conventions');
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('does NOT include a Conventions section when guidance is absent', () => {
|
|
80
|
+
const req = baseReviewReq();
|
|
81
|
+
const prompt = buildReviewPrompt(req);
|
|
82
|
+
expect(prompt).not.toContain('Conventions');
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('still includes core prompt content regardless of guidance', () => {
|
|
86
|
+
const req = baseReviewReq({ guidance: 'Some guidance.' });
|
|
87
|
+
const prompt = buildReviewPrompt(req);
|
|
88
|
+
expect(prompt).toContain('tool_a');
|
|
89
|
+
expect(prompt).toContain('@useWhen');
|
|
90
|
+
expect(prompt).toContain('Use this tool when listing files');
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('uses source-neutral framing (no hardcoded MCP) and keeps guidance + JSON instruction', () => {
|
|
94
|
+
const req = baseReviewReq({ guidance: 'CLI conventions: use --flag syntax.' });
|
|
95
|
+
const prompt = buildReviewPrompt(req);
|
|
96
|
+
expect(prompt).not.toContain('MCP');
|
|
97
|
+
expect(prompt).toContain('skill annotation draft for "my-tool"');
|
|
98
|
+
expect(prompt).toContain('Conventions');
|
|
99
|
+
expect(prompt).toContain('CLI conventions: use --flag syntax.');
|
|
100
|
+
expect(prompt).toContain('Respond with JSON only');
|
|
101
|
+
});
|
|
102
|
+
});
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
// packages/client/src/__tests__/cli-adapters.test.ts
|
|
2
|
+
import { describe, it, expect } from 'vitest';
|
|
3
|
+
import { claudeAdapter, codexAdapter, copilotAdapter, adapterFor } from '../model/cli/adapters.js';
|
|
4
|
+
import { DRAFTER, REVIEWER } from '../model/models.js';
|
|
5
|
+
|
|
6
|
+
describe('claudeAdapter', () => {
|
|
7
|
+
it('maps draft role to the drafter model and review role to the reviewer model', () => {
|
|
8
|
+
const draft = claudeAdapter.invocation('draft', 'PROMPT');
|
|
9
|
+
expect(draft.cmd).toBe('claude');
|
|
10
|
+
expect(draft.args).toEqual(['-p', '--output-format', 'json', '--model', DRAFTER]);
|
|
11
|
+
expect(draft.input).toBe('PROMPT');
|
|
12
|
+
const review = claudeAdapter.invocation('review', 'PROMPT');
|
|
13
|
+
expect(review.args).toContain(REVIEWER);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('extracts result from the claude json envelope', () => {
|
|
17
|
+
const stdout = JSON.stringify({ type: 'result', is_error: false, result: 'the answer' });
|
|
18
|
+
expect(claudeAdapter.extractResult(stdout)).toBe('the answer');
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('throws when claude reports is_error', () => {
|
|
22
|
+
const stdout = JSON.stringify({ type: 'result', is_error: true, result: 'nope' });
|
|
23
|
+
expect(() => claudeAdapter.extractResult(stdout)).toThrow(/claude/i);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('returns an empty result string (does not editorialize)', () => {
|
|
27
|
+
const stdout = JSON.stringify({ type: 'result', is_error: false, result: '' });
|
|
28
|
+
expect(claudeAdapter.extractResult(stdout)).toBe('');
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
describe('codexAdapter', () => {
|
|
33
|
+
it('invokes codex exec --json with the prompt on stdin and no per-role model', () => {
|
|
34
|
+
const inv = codexAdapter.invocation('draft', 'PROMPT');
|
|
35
|
+
expect(inv.cmd).toBe('codex');
|
|
36
|
+
expect(inv.args).toEqual(['exec', '--json']);
|
|
37
|
+
expect(inv.input).toBe('PROMPT');
|
|
38
|
+
// role-agnostic: review uses the same invocation
|
|
39
|
+
expect(codexAdapter.invocation('review', 'PROMPT').args).toEqual(['exec', '--json']);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('extracts the last agent_message from the jsonl stream, skipping log/noise lines', () => {
|
|
43
|
+
const stdout = [
|
|
44
|
+
'some non-json log line',
|
|
45
|
+
JSON.stringify({ type: 'thread.started', thread_id: 'x' }),
|
|
46
|
+
JSON.stringify({ type: 'item.completed', item: { type: 'agent_message', text: 'first' } }),
|
|
47
|
+
JSON.stringify({ type: 'item.completed', item: { type: 'agent_message', text: 'final' } }),
|
|
48
|
+
JSON.stringify({ type: 'turn.completed' })
|
|
49
|
+
].join('\n');
|
|
50
|
+
expect(codexAdapter.extractResult(stdout)).toBe('final');
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('returns an empty final agent_message, overriding an earlier non-empty one (last-wins on empty)', () => {
|
|
54
|
+
const stdout = [
|
|
55
|
+
JSON.stringify({ type: 'item.completed', item: { type: 'agent_message', text: 'first' } }),
|
|
56
|
+
JSON.stringify({ type: 'item.completed', item: { type: 'agent_message', text: '' } })
|
|
57
|
+
].join('\n');
|
|
58
|
+
expect(codexAdapter.extractResult(stdout)).toBe('');
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('throws when no agent_message is present', () => {
|
|
62
|
+
expect(() => codexAdapter.extractResult('{"type":"turn.completed"}')).toThrow(/codex/i);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('surfaces a turn.failed signal in the no-message error', () => {
|
|
66
|
+
const stdout = JSON.stringify({ type: 'turn.failed' });
|
|
67
|
+
expect(() => codexAdapter.extractResult(stdout)).toThrow(/turn\.failed/);
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
describe('copilotAdapter', () => {
|
|
72
|
+
it('pipes the prompt via stdin (not argv) so untrusted content stays out of argv', () => {
|
|
73
|
+
const inv = copilotAdapter.invocation('draft', 'PROMPT');
|
|
74
|
+
expect(inv.cmd).toBe('copilot');
|
|
75
|
+
expect(inv.args).toEqual(['--output-format', 'json', '--no-color']);
|
|
76
|
+
expect(inv.args).not.toContain('PROMPT');
|
|
77
|
+
expect(inv.input).toBe('PROMPT');
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('extracts the last assistant.message content from the jsonl stream, skipping deltas/result', () => {
|
|
81
|
+
const stdout = [
|
|
82
|
+
JSON.stringify({ type: 'assistant.message_delta', data: { deltaContent: 'the ' } }),
|
|
83
|
+
JSON.stringify({ type: 'assistant.message_delta', data: { deltaContent: 'answer' } }),
|
|
84
|
+
JSON.stringify({
|
|
85
|
+
type: 'assistant.message',
|
|
86
|
+
data: { content: 'the answer', toolRequests: [] }
|
|
87
|
+
}),
|
|
88
|
+
JSON.stringify({ type: 'result', exitCode: 0 })
|
|
89
|
+
].join('\n');
|
|
90
|
+
expect(copilotAdapter.extractResult(stdout)).toBe('the answer');
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('returns an empty assistant.message content (does not editorialize)', () => {
|
|
94
|
+
const stdout = [
|
|
95
|
+
JSON.stringify({ type: 'assistant.message', data: { content: 'first', toolRequests: [] } }),
|
|
96
|
+
JSON.stringify({ type: 'assistant.message', data: { content: '', toolRequests: [] } })
|
|
97
|
+
].join('\n');
|
|
98
|
+
expect(copilotAdapter.extractResult(stdout)).toBe('');
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('throws when no assistant.message is present', () => {
|
|
102
|
+
expect(() => copilotAdapter.extractResult('{"type":"result","exitCode":0}')).toThrow(
|
|
103
|
+
/copilot/i
|
|
104
|
+
);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('surfaces a nonzero result exitCode in the no-message error', () => {
|
|
108
|
+
const stdout = JSON.stringify({ type: 'result', exitCode: 1 });
|
|
109
|
+
expect(() => copilotAdapter.extractResult(stdout)).toThrow(/exitCode 1/);
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
describe('adapterFor', () => {
|
|
114
|
+
it('returns the matching adapter', () => {
|
|
115
|
+
expect(adapterFor('claude')).toBe(claudeAdapter);
|
|
116
|
+
expect(adapterFor('codex')).toBe(codexAdapter);
|
|
117
|
+
expect(adapterFor('copilot')).toBe(copilotAdapter);
|
|
118
|
+
});
|
|
119
|
+
});
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// packages/client/src/__tests__/cli-client.test.ts
|
|
2
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
3
|
+
import { CliModelClient } from '../model/cli/cli-client.js';
|
|
4
|
+
import { claudeAdapter } from '../model/cli/adapters.js';
|
|
5
|
+
import type { DraftRequest, ReviewRequest, ExtractedSkill } from '@skillit/core';
|
|
6
|
+
|
|
7
|
+
const skill = { name: 'demo' } as unknown as ExtractedSkill;
|
|
8
|
+
const draftReq: DraftRequest = {
|
|
9
|
+
toolName: 'gen',
|
|
10
|
+
tag: 'useWhen',
|
|
11
|
+
suggestion: 'say when',
|
|
12
|
+
currentValue: undefined,
|
|
13
|
+
skill
|
|
14
|
+
};
|
|
15
|
+
const reviewReq: ReviewRequest = {
|
|
16
|
+
toolName: 'gen',
|
|
17
|
+
tag: 'useWhen',
|
|
18
|
+
draft: 'When generating',
|
|
19
|
+
suggestion: 'say when',
|
|
20
|
+
skill
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
describe('CliModelClient', () => {
|
|
24
|
+
it('draft() returns the trimmed result extracted from the adapter envelope', async () => {
|
|
25
|
+
const runner = vi.fn(async () =>
|
|
26
|
+
JSON.stringify({ type: 'result', is_error: false, result: ' When generating output ' })
|
|
27
|
+
);
|
|
28
|
+
const client = new CliModelClient(claudeAdapter, { runner });
|
|
29
|
+
const out = await client.draft(draftReq);
|
|
30
|
+
expect(out).toBe('When generating output');
|
|
31
|
+
// the adapter's invocation was forwarded to the runner
|
|
32
|
+
expect(runner).toHaveBeenCalledWith(
|
|
33
|
+
expect.objectContaining({ cmd: 'claude', args: expect.arrayContaining(['-p']) })
|
|
34
|
+
);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('review() parses the verdict JSON out of the adapter result', async () => {
|
|
38
|
+
const runner = vi.fn(async () =>
|
|
39
|
+
JSON.stringify({
|
|
40
|
+
type: 'result',
|
|
41
|
+
is_error: false,
|
|
42
|
+
result: 'Sure: {"verdict":"revise","feedback":"too vague"}'
|
|
43
|
+
})
|
|
44
|
+
);
|
|
45
|
+
const client = new CliModelClient(claudeAdapter, { runner });
|
|
46
|
+
const res = await client.review(reviewReq);
|
|
47
|
+
expect(res).toEqual({ verdict: 'revise', feedback: 'too vague' });
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('passes the configured timeout through to the runner', async () => {
|
|
51
|
+
const runner = vi.fn(async () =>
|
|
52
|
+
JSON.stringify({ type: 'result', is_error: false, result: 'x' })
|
|
53
|
+
);
|
|
54
|
+
const client = new CliModelClient(claudeAdapter, { runner, timeoutMs: 5000 });
|
|
55
|
+
await client.draft(draftReq);
|
|
56
|
+
expect(runner).toHaveBeenCalledWith(expect.objectContaining({ timeoutMs: 5000 }));
|
|
57
|
+
});
|
|
58
|
+
});
|