@haystackeditor/cli 0.15.16 → 0.15.18
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 +45 -6
- package/dist/commands/ask.d.ts +14 -0
- package/dist/commands/ask.js +20 -0
- package/dist/commands/inbox.d.ts +65 -0
- package/dist/commands/inbox.js +137 -0
- package/dist/commands/mcp.js +88 -134
- package/dist/commands/pr.d.ts +37 -1
- package/dist/commands/pr.js +95 -77
- package/dist/commands/schema-cmd.js +1 -1
- package/dist/commands/traces.d.ts +48 -0
- package/dist/commands/traces.js +92 -0
- package/dist/commands/triage.d.ts +12 -0
- package/dist/commands/triage.js +90 -58
- package/dist/commands/webhooks.js +3 -6
- package/dist/index.js +50 -148
- package/dist/schema.d.ts +5 -2
- package/dist/schema.js +5 -2
- package/dist/triage/prompts.js +4 -0
- package/dist/utils/analysis-api.d.ts +7 -0
- package/dist/utils/haystack-api.d.ts +20 -0
- package/dist/utils/haystack-api.js +66 -0
- package/package.json +4 -4
- package/schemas/ask.v1.json +40 -0
- package/schemas/inbox.v1.json +27 -0
- package/schemas/pr.v2.json +54 -0
- package/schemas/traces.v1.json +53 -0
- package/schemas/triage.v2.json +64 -0
- package/dist/assets/skills/install-verification.md +0 -67
- package/dist/commands/verification.d.ts +0 -21
- package/dist/commands/verification.js +0 -291
- package/dist/verification/contract.d.ts +0 -240
- package/dist/verification/contract.js +0 -115
- package/dist/verification/init.d.ts +0 -10
- package/dist/verification/init.js +0 -243
- package/dist/verification/manifest.d.ts +0 -49
- package/dist/verification/manifest.js +0 -186
- package/dist/verification/runner.d.ts +0 -20
- package/dist/verification/runner.js +0 -102
- package/dist/verification/safety.d.ts +0 -10
- package/dist/verification/safety.js +0 -252
- package/dist/verification/validate.d.ts +0 -12
- package/dist/verification/validate.js +0 -219
package/README.md
CHANGED
|
@@ -69,7 +69,6 @@ Create a PR from current changes. Runs pre-PR triage (code review, rules validat
|
|
|
69
69
|
|
|
70
70
|
```bash
|
|
71
71
|
haystack submit # Triage -> create PR -> wait for analysis
|
|
72
|
-
haystack submit --auto-fix # Discouraged alpha auto-fix for straightforward mechanical issues
|
|
73
72
|
haystack submit --title "Fix auth" # Custom PR title
|
|
74
73
|
haystack submit --draft # Create as draft PR
|
|
75
74
|
haystack submit --force # Skip triage checks
|
|
@@ -85,8 +84,6 @@ haystack submit --review octocat # Request review from a specific teammate
|
|
|
85
84
|
|
|
86
85
|
When `--review` is used without a username, the PR is labeled `haystack:needs-review` and appears in your team's assignment queue. When a username is provided, that person is also requested as a reviewer on GitHub.
|
|
87
86
|
|
|
88
|
-
The `--auto-fix` flag is an alpha feature and is discouraged by default. It opts into a sandbox agent that attempts straightforward mechanical fixes before surfacing issues in the Feed. For most PRs, prefer plain `haystack submit`.
|
|
89
|
-
|
|
90
87
|
### `haystack triage`
|
|
91
88
|
|
|
92
89
|
View Haystack analysis results for any PR. Shows the same data as the Haystack web feed: rating, verdict, structured findings with details, verified bugs, human review reasons, and agent fix prompts.
|
|
@@ -102,9 +99,51 @@ haystack triage --hook # Minimal one-liner (for session hoo
|
|
|
102
99
|
haystack triage --clear # Clear pending submit state
|
|
103
100
|
```
|
|
104
101
|
|
|
105
|
-
When called without a PR identifier, checks the last PR submitted via `haystack submit`. The `--hook` flag produces a single-line summary with the Haystack rating
|
|
102
|
+
When called without a PR identifier, checks the last PR submitted via `haystack submit`. The `--hook` flag produces a single-line summary with the Haystack rating, designed for session-start hooks.
|
|
103
|
+
|
|
104
|
+
The `--json` output includes every finding and its customer-facing `suggested_fix` when available.
|
|
105
|
+
|
|
106
|
+
### `haystack inbox list`
|
|
107
|
+
|
|
108
|
+
List PRs in your Haystack inbox with the reason each PR is present and the next action:
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
haystack inbox list
|
|
112
|
+
haystack inbox list --json
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
### `haystack pr get`
|
|
116
|
+
|
|
117
|
+
Get the useful state of one PR: triage, minimal merge blockers, and customer-trace availability:
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
haystack pr get 42
|
|
121
|
+
haystack pr get owner/repo#42 --json
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### `haystack ask`
|
|
125
|
+
|
|
126
|
+
Ask Haystack Chat about a PR. Machine output contains the answer and every customer-facing source Chat consulted:
|
|
127
|
+
|
|
128
|
+
```bash
|
|
129
|
+
haystack ask 42 "Why is this finding legitimate?"
|
|
130
|
+
haystack ask owner/repo#42 "Show the implementation" --json
|
|
131
|
+
haystack ask owner/repo#42 "What about its callers?" --session <session-id> --json
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
Chat may search the complete authorized repository when the changed files are insufficient. Search results and fetched source are included in `evidence`.
|
|
135
|
+
|
|
136
|
+
### `haystack traces`
|
|
137
|
+
|
|
138
|
+
Inspect retained customer-owned Entire checkpoints and transcript chunks:
|
|
139
|
+
|
|
140
|
+
```bash
|
|
141
|
+
haystack traces list owner/repo#42 --json
|
|
142
|
+
haystack traces get owner/repo#42 <checkpoint-id> --json
|
|
143
|
+
haystack traces get owner/repo#42 <checkpoint-id> --cursor 20 --limit 20 --json
|
|
144
|
+
```
|
|
106
145
|
|
|
107
|
-
|
|
146
|
+
These commands expose customer coding-session data, not Haystack's internal analysis-agent execution.
|
|
108
147
|
|
|
109
148
|
### `haystack dismiss`
|
|
110
149
|
|
|
@@ -135,7 +174,7 @@ haystack mark-reviewed acme/widgets#99 # Mark for specific repo
|
|
|
135
174
|
|
|
136
175
|
### `haystack pr-status`
|
|
137
176
|
|
|
138
|
-
Show what bucket a PR is in within the Haystack pipeline (analyzing,
|
|
177
|
+
Show what bucket a PR is in within the Haystack pipeline (analyzing, good-to-merge, issues, needs-assignment, etc.):
|
|
139
178
|
|
|
140
179
|
```bash
|
|
141
180
|
haystack pr-status 42 # Current repo, PR #42
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export interface AskOptions {
|
|
2
|
+
json?: boolean;
|
|
3
|
+
session?: string;
|
|
4
|
+
}
|
|
5
|
+
export interface AskResponse {
|
|
6
|
+
schema_version: string;
|
|
7
|
+
ref: string;
|
|
8
|
+
head_sha: string;
|
|
9
|
+
answer: string;
|
|
10
|
+
evidence: Array<Record<string, unknown>>;
|
|
11
|
+
session_id: string;
|
|
12
|
+
}
|
|
13
|
+
export declare function fetchAskPayload(ref: string, question: string, options: Pick<AskOptions, 'session'>): Promise<AskResponse>;
|
|
14
|
+
export declare function askHaystackCommand(ref: string, question: string, options: AskOptions): Promise<void>;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { loadToken } from '../utils/auth.js';
|
|
3
|
+
import { postAskHaystack } from '../utils/haystack-api.js';
|
|
4
|
+
import { parsePrRef } from './pr.js';
|
|
5
|
+
export async function fetchAskPayload(ref, question, options) {
|
|
6
|
+
const pr = parsePrRef(ref);
|
|
7
|
+
const token = await loadToken({ owner: pr.owner, repo: pr.repo });
|
|
8
|
+
if (!token)
|
|
9
|
+
throw new Error('Not authenticated. Run `haystack login` first.');
|
|
10
|
+
return postAskHaystack(pr.owner, pr.repo, pr.prNumber, question, options.session, token);
|
|
11
|
+
}
|
|
12
|
+
export async function askHaystackCommand(ref, question, options) {
|
|
13
|
+
const response = await fetchAskPayload(ref, question, options);
|
|
14
|
+
if (options.json) {
|
|
15
|
+
console.log(JSON.stringify(response, null, 2));
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
console.log(`\n${response.answer}\n`);
|
|
19
|
+
console.log(chalk.dim(`${response.evidence.length} source${response.evidence.length === 1 ? '' : 's'} consulted · session ${response.session_id}\n`));
|
|
20
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
interface GitHubPull {
|
|
2
|
+
number: number;
|
|
3
|
+
title: string;
|
|
4
|
+
html_url: string;
|
|
5
|
+
draft?: boolean;
|
|
6
|
+
user: {
|
|
7
|
+
id: number;
|
|
8
|
+
login: string;
|
|
9
|
+
};
|
|
10
|
+
assignees?: Array<{
|
|
11
|
+
id: number;
|
|
12
|
+
login: string;
|
|
13
|
+
}>;
|
|
14
|
+
requested_reviewers?: Array<{
|
|
15
|
+
id: number;
|
|
16
|
+
login: string;
|
|
17
|
+
}>;
|
|
18
|
+
head: {
|
|
19
|
+
sha: string;
|
|
20
|
+
ref: string;
|
|
21
|
+
};
|
|
22
|
+
base: {
|
|
23
|
+
ref: string;
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
interface InboxCandidate extends GitHubPull {
|
|
27
|
+
owner: string;
|
|
28
|
+
repo: string;
|
|
29
|
+
}
|
|
30
|
+
interface AnalysisSummary {
|
|
31
|
+
analysisStatus?: 'pending' | 'in_queue' | 'ready' | 'error';
|
|
32
|
+
analysisVerdict?: 'clean' | 'has-issues' | 'needs-review';
|
|
33
|
+
haystackRating?: number;
|
|
34
|
+
needsHumanReview?: boolean;
|
|
35
|
+
findingsCount?: number;
|
|
36
|
+
merge?: {
|
|
37
|
+
status: string;
|
|
38
|
+
reason: string;
|
|
39
|
+
blocking_checks: string[];
|
|
40
|
+
action: string | null;
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
export interface InboxListOptions {
|
|
44
|
+
json?: boolean;
|
|
45
|
+
}
|
|
46
|
+
export declare function isRelevantToUser(pr: GitHubPull, userId: number): boolean;
|
|
47
|
+
export declare function classify(candidate: InboxCandidate, analysis: AnalysisSummary | undefined): {
|
|
48
|
+
bucket: string;
|
|
49
|
+
reason: string;
|
|
50
|
+
action: string;
|
|
51
|
+
};
|
|
52
|
+
export declare function fetchInboxPayload(token: string): Promise<{
|
|
53
|
+
pull_requests: {
|
|
54
|
+
bucket: string;
|
|
55
|
+
reason: string;
|
|
56
|
+
action: string;
|
|
57
|
+
ref: string;
|
|
58
|
+
title: string;
|
|
59
|
+
head_sha: string;
|
|
60
|
+
}[];
|
|
61
|
+
} & {
|
|
62
|
+
schema_version: string;
|
|
63
|
+
}>;
|
|
64
|
+
export declare function inboxListCommand(options: InboxListOptions): Promise<void>;
|
|
65
|
+
export {};
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { loadToken } from '../utils/auth.js';
|
|
3
|
+
import { fetchCliIdentity, fetchGitHubJson, fetchHaystackInstallations, haystackJson, } from '../utils/haystack-api.js';
|
|
4
|
+
import { withSchema } from '../schema.js';
|
|
5
|
+
async function fetchOpenPulls(owner, repo, token) {
|
|
6
|
+
const pulls = [];
|
|
7
|
+
for (let page = 1;; page++) {
|
|
8
|
+
const batch = await fetchGitHubJson(`/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/pulls?state=open&per_page=100&page=${page}`, token);
|
|
9
|
+
pulls.push(...batch);
|
|
10
|
+
if (batch.length < 100)
|
|
11
|
+
return pulls;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
export function isRelevantToUser(pr, userId) {
|
|
15
|
+
return pr.user.id === userId
|
|
16
|
+
|| (pr.assignees ?? []).some((user) => user.id === userId)
|
|
17
|
+
|| (pr.requested_reviewers ?? []).some((user) => user.id === userId);
|
|
18
|
+
}
|
|
19
|
+
async function mapConcurrent(values, concurrency, fn) {
|
|
20
|
+
const results = new Array(values.length);
|
|
21
|
+
let next = 0;
|
|
22
|
+
await Promise.all(Array.from({ length: Math.min(concurrency, values.length) }, async () => {
|
|
23
|
+
while (next < values.length) {
|
|
24
|
+
const index = next++;
|
|
25
|
+
results[index] = await fn(values[index]);
|
|
26
|
+
}
|
|
27
|
+
}));
|
|
28
|
+
return results;
|
|
29
|
+
}
|
|
30
|
+
export function classify(candidate, analysis) {
|
|
31
|
+
const findingCount = analysis?.findingsCount ?? 0;
|
|
32
|
+
const failedChecks = analysis?.merge?.blocking_checks ?? [];
|
|
33
|
+
if (analysis?.analysisStatus === 'error') {
|
|
34
|
+
return {
|
|
35
|
+
bucket: 'needs_shepherding',
|
|
36
|
+
reason: 'Haystack analysis failed.',
|
|
37
|
+
action: 'Retry the analysis or inspect the failure.',
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
if (!analysis || analysis.analysisStatus === 'pending' || analysis.analysisStatus === 'in_queue') {
|
|
41
|
+
return {
|
|
42
|
+
bucket: 'analyzing',
|
|
43
|
+
reason: 'Haystack is still analyzing this PR.',
|
|
44
|
+
action: 'Wait for analysis to complete.',
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
if (analysis.analysisVerdict === 'has-issues' || (analysis.haystackRating ?? 5) < 4) {
|
|
48
|
+
return {
|
|
49
|
+
bucket: 'needs_shepherding',
|
|
50
|
+
reason: findingCount > 0
|
|
51
|
+
? `Haystack found ${findingCount} author-actionable finding${findingCount === 1 ? '' : 's'}.`
|
|
52
|
+
: 'Haystack found author-actionable issues.',
|
|
53
|
+
action: 'Read the triage findings and resolve the issues.',
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
if (failedChecks.length > 0 || analysis.merge?.status === 'blocked') {
|
|
57
|
+
return {
|
|
58
|
+
bucket: 'needs_shepherding',
|
|
59
|
+
reason: failedChecks.length > 0
|
|
60
|
+
? `${failedChecks.length} required check${failedChecks.length === 1 ? ' is' : 's are'} failing: ${failedChecks.join(', ')}.`
|
|
61
|
+
: (analysis.merge?.reason ?? 'The PR is blocked from merging.'),
|
|
62
|
+
action: analysis.merge?.action ?? 'Resolve the merge blockers.',
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
if (analysis.analysisVerdict === 'needs-review' || analysis.needsHumanReview) {
|
|
66
|
+
return {
|
|
67
|
+
bucket: 'needs_review',
|
|
68
|
+
reason: 'Analysis is clean, but review policy requires a human reviewer.',
|
|
69
|
+
action: 'Complete or request the required review.',
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
return {
|
|
73
|
+
bucket: 'good_to_merge',
|
|
74
|
+
reason: 'Haystack found no blocking issues.',
|
|
75
|
+
action: 'No user action is required.',
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
export async function fetchInboxPayload(token) {
|
|
79
|
+
const [identity, installations] = await Promise.all([
|
|
80
|
+
fetchCliIdentity(token),
|
|
81
|
+
fetchHaystackInstallations(token),
|
|
82
|
+
]);
|
|
83
|
+
const repos = [...new Set(installations.flatMap((i) => i.repositories ?? []).map((r) => r.full_name))];
|
|
84
|
+
const batches = await mapConcurrent(repos, 8, async (fullName) => {
|
|
85
|
+
const [owner, repo] = fullName.split('/');
|
|
86
|
+
if (!owner || !repo)
|
|
87
|
+
throw new Error(`Invalid repository returned by Haystack: ${fullName}`);
|
|
88
|
+
const pulls = await fetchOpenPulls(owner, repo, token);
|
|
89
|
+
return pulls
|
|
90
|
+
.filter((pr) => isRelevantToUser(pr, identity.id))
|
|
91
|
+
.map((pr) => ({ ...pr, owner, repo }));
|
|
92
|
+
});
|
|
93
|
+
const candidates = batches.flat();
|
|
94
|
+
const refs = candidates.map((pr) => `${pr.owner}/${pr.repo}#${pr.number}`);
|
|
95
|
+
const analysis = refs.length === 0
|
|
96
|
+
? {}
|
|
97
|
+
: await haystackJson('/api/bulk-analysis-summaries', token, {
|
|
98
|
+
method: 'POST',
|
|
99
|
+
headers: { 'Content-Type': 'application/json' },
|
|
100
|
+
body: JSON.stringify({
|
|
101
|
+
prIdentifiers: refs,
|
|
102
|
+
prBaseBranches: Object.fromEntries(candidates.map((pr) => [`${pr.owner}/${pr.repo}#${pr.number}`, pr.base.ref])),
|
|
103
|
+
prHeadShas: Object.fromEntries(candidates.map((pr) => [`${pr.owner}/${pr.repo}#${pr.number}`, pr.head.sha])),
|
|
104
|
+
}),
|
|
105
|
+
});
|
|
106
|
+
return withSchema('inbox', {
|
|
107
|
+
pull_requests: candidates.map((pr) => {
|
|
108
|
+
const ref = `${pr.owner}/${pr.repo}#${pr.number}`;
|
|
109
|
+
return {
|
|
110
|
+
ref,
|
|
111
|
+
title: pr.title,
|
|
112
|
+
head_sha: pr.head.sha,
|
|
113
|
+
...classify(pr, analysis[ref]),
|
|
114
|
+
};
|
|
115
|
+
}),
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
export async function inboxListCommand(options) {
|
|
119
|
+
const token = await loadToken();
|
|
120
|
+
if (!token)
|
|
121
|
+
throw new Error('Not authenticated. Run `haystack login` first.');
|
|
122
|
+
const payload = await fetchInboxPayload(token);
|
|
123
|
+
if (options.json) {
|
|
124
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
if (payload.pull_requests.length === 0) {
|
|
128
|
+
console.log(chalk.dim('\nNo PRs need your attention.\n'));
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
console.log(chalk.bold('\nHaystack inbox\n'));
|
|
132
|
+
for (const pr of payload.pull_requests) {
|
|
133
|
+
console.log(`${chalk.cyan(pr.ref)} ${chalk.bold(pr.title)}`);
|
|
134
|
+
console.log(` ${pr.reason}`);
|
|
135
|
+
console.log(` ${chalk.dim('Next:')} ${pr.action}\n`);
|
|
136
|
+
}
|
|
137
|
+
}
|
package/dist/commands/mcp.js
CHANGED
|
@@ -1,71 +1,74 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `haystack mcp` — Model Context Protocol server.
|
|
3
|
-
*
|
|
4
|
-
* Exposes Haystack's read surface to Claude Code, Cursor, Codex, etc. as
|
|
5
|
-
* stdio MCP tools. Every tool wraps the same library function that powers
|
|
6
|
-
* the corresponding `--json` CLI command, so an MCP client and a webhook
|
|
7
|
-
* receiver see byte-identical payloads.
|
|
8
|
-
*
|
|
9
|
-
* Tools:
|
|
10
|
-
* triage_pr(ref) same shape as `haystack triage <ref> --json`
|
|
11
|
-
* pr_state(ref) PR state in the Haystack pipeline (schemas/pr.v1.json)
|
|
12
|
-
* schema(name) prints the JSON Schema for a noun (debugging)
|
|
13
|
-
*
|
|
14
|
-
* Auth: inherited from the local CLI (loadToken). MCP clients run as the
|
|
15
|
-
* operator's user, so they get whatever credentials are on disk — same model
|
|
16
|
-
* as `git`, `gh`, etc.
|
|
17
|
-
*/
|
|
18
1
|
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
19
2
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
20
|
-
import { CallToolRequestSchema, ListToolsRequestSchema
|
|
3
|
+
import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
21
4
|
import { readFileSync } from 'node:fs';
|
|
22
5
|
import { fileURLToPath } from 'node:url';
|
|
23
6
|
import { dirname, join } from 'node:path';
|
|
24
|
-
import {
|
|
25
|
-
import {
|
|
7
|
+
import { fetchInboxPayload } from './inbox.js';
|
|
8
|
+
import { fetchPrStatePayload } from './pr.js';
|
|
9
|
+
import { fetchTriagePayload } from './triage.js';
|
|
10
|
+
import { fetchAskPayload } from './ask.js';
|
|
11
|
+
import { fetchTracesGetPayload, fetchTracesListPayload } from './traces.js';
|
|
26
12
|
import { loadToken } from '../utils/auth.js';
|
|
27
|
-
import { SCHEMA_VERSIONS
|
|
13
|
+
import { SCHEMA_VERSIONS } from '../schema.js';
|
|
14
|
+
const refInput = {
|
|
15
|
+
type: 'string',
|
|
16
|
+
description: 'PR reference: bare number, owner/repo#number, or GitHub URL.',
|
|
17
|
+
};
|
|
28
18
|
const TOOLS = [
|
|
29
19
|
{
|
|
30
|
-
name: '
|
|
31
|
-
description: '
|
|
32
|
-
|
|
33
|
-
|
|
20
|
+
name: 'inbox_list',
|
|
21
|
+
description: 'List PRs in the user\'s Haystack inbox with the reason and next action.',
|
|
22
|
+
inputSchema: { type: 'object', properties: {} },
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
name: 'pr_get',
|
|
26
|
+
description: 'Get useful state for one PR: triage, minimal merge blockers, trace availability, and conditional fixer outcome.',
|
|
27
|
+
inputSchema: { type: 'object', properties: { ref: refInput }, required: ['ref'] },
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
name: 'triage_get',
|
|
31
|
+
description: 'Get the complete Haystack verdict and every customer-facing finding for one PR.',
|
|
32
|
+
inputSchema: { type: 'object', properties: { ref: refInput }, required: ['ref'] },
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
name: 'ask_haystack',
|
|
36
|
+
description: 'Ask Haystack about a PR. Returns the answer and every customer-facing source consulted.',
|
|
34
37
|
inputSchema: {
|
|
35
38
|
type: 'object',
|
|
36
39
|
properties: {
|
|
37
|
-
ref:
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
},
|
|
40
|
+
ref: refInput,
|
|
41
|
+
question: { type: 'string', description: 'Question about the pull request.' },
|
|
42
|
+
session_id: { type: 'string', description: 'Optional prior Ask session to continue.' },
|
|
41
43
|
},
|
|
42
|
-
required: ['ref'],
|
|
44
|
+
required: ['ref', 'question'],
|
|
43
45
|
},
|
|
44
46
|
},
|
|
45
47
|
{
|
|
46
|
-
name: '
|
|
47
|
-
description: '
|
|
48
|
+
name: 'traces_list',
|
|
49
|
+
description: 'List retained customer-owned Entire checkpoints for one PR.',
|
|
50
|
+
inputSchema: { type: 'object', properties: { ref: refInput }, required: ['ref'] },
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
name: 'traces_get',
|
|
54
|
+
description: 'Get a page of retained customer-owned transcript chunks for one checkpoint.',
|
|
48
55
|
inputSchema: {
|
|
49
56
|
type: 'object',
|
|
50
57
|
properties: {
|
|
51
|
-
ref:
|
|
58
|
+
ref: refInput,
|
|
59
|
+
checkpoint_id: { type: 'string' },
|
|
60
|
+
cursor: { type: 'string' },
|
|
61
|
+
limit: { type: 'integer', minimum: 1, maximum: 100 },
|
|
52
62
|
},
|
|
53
|
-
required: ['ref'],
|
|
63
|
+
required: ['ref', 'checkpoint_id'],
|
|
54
64
|
},
|
|
55
65
|
},
|
|
56
66
|
{
|
|
57
67
|
name: 'schema',
|
|
58
|
-
description: 'Return the JSON Schema for a Haystack
|
|
59
|
-
'Use this to discover the exact shape of triage / setup / pr envelopes.',
|
|
68
|
+
description: 'Return the committed JSON Schema for a Haystack machine response.',
|
|
60
69
|
inputSchema: {
|
|
61
70
|
type: 'object',
|
|
62
|
-
properties: {
|
|
63
|
-
name: {
|
|
64
|
-
type: 'string',
|
|
65
|
-
enum: Object.keys(SCHEMA_VERSIONS),
|
|
66
|
-
description: 'Schema noun.',
|
|
67
|
-
},
|
|
68
|
-
},
|
|
71
|
+
properties: { name: { type: 'string', enum: Object.keys(SCHEMA_VERSIONS) } },
|
|
69
72
|
required: ['name'],
|
|
70
73
|
},
|
|
71
74
|
},
|
|
@@ -73,113 +76,64 @@ const TOOLS = [
|
|
|
73
76
|
async function readSchema(name) {
|
|
74
77
|
const major = SCHEMA_VERSIONS[name].split('.')[0];
|
|
75
78
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
76
|
-
|
|
77
|
-
const p = join(here, '..', '..', 'schemas', `${name}.v${major}.json`);
|
|
78
|
-
return JSON.parse(readFileSync(p, 'utf8'));
|
|
79
|
-
}
|
|
80
|
-
/** Capture process.stdout written by CLI commands and return it. MCP tool
|
|
81
|
-
* callbacks must return their result, not write it. The simplest way to
|
|
82
|
-
* reuse the existing --json printers without refactoring them is to
|
|
83
|
-
* redirect stdout for the duration of the call. */
|
|
84
|
-
async function captureStdout(fn) {
|
|
85
|
-
const origWrite = process.stdout.write.bind(process.stdout);
|
|
86
|
-
let captured = '';
|
|
87
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
88
|
-
process.stdout.write = (chunk, encoding, cb) => {
|
|
89
|
-
captured += typeof chunk === 'string' ? chunk : new TextDecoder().decode(chunk);
|
|
90
|
-
if (typeof encoding === 'function')
|
|
91
|
-
encoding();
|
|
92
|
-
else if (cb)
|
|
93
|
-
cb();
|
|
94
|
-
return true;
|
|
95
|
-
};
|
|
96
|
-
try {
|
|
97
|
-
await fn();
|
|
98
|
-
}
|
|
99
|
-
finally {
|
|
100
|
-
process.stdout.write = origWrite;
|
|
101
|
-
}
|
|
102
|
-
return captured;
|
|
103
|
-
}
|
|
104
|
-
function jsonFromCapture(captured) {
|
|
105
|
-
try {
|
|
106
|
-
return JSON.parse(captured);
|
|
107
|
-
}
|
|
108
|
-
catch {
|
|
109
|
-
return { error: 'tool produced non-JSON output', raw: captured.slice(0, 1000) };
|
|
110
|
-
}
|
|
79
|
+
return JSON.parse(readFileSync(join(here, '..', '..', 'schemas', `${name}.v${major}.json`), 'utf8'));
|
|
111
80
|
}
|
|
112
81
|
export async function runMcpServer() {
|
|
113
|
-
const server = new Server({
|
|
114
|
-
name: 'haystack-cli',
|
|
115
|
-
version: '1.0.0',
|
|
116
|
-
}, {
|
|
117
|
-
capabilities: { tools: {} },
|
|
118
|
-
});
|
|
82
|
+
const server = new Server({ name: 'haystack-cli', version: '1.0.0' }, { capabilities: { tools: {} } });
|
|
119
83
|
server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOLS }));
|
|
120
84
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
121
85
|
const { name, arguments: args } = request.params;
|
|
122
|
-
const
|
|
123
|
-
|
|
124
|
-
});
|
|
125
|
-
const fail = (msg) => ({
|
|
126
|
-
isError: true,
|
|
127
|
-
content: [{ type: 'text', text: msg }],
|
|
128
|
-
});
|
|
86
|
+
const input = (args ?? {});
|
|
87
|
+
const ok = (data) => ({ content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] });
|
|
88
|
+
const fail = (message) => ({ isError: true, content: [{ type: 'text', text: message }] });
|
|
129
89
|
try {
|
|
130
|
-
if (name === '
|
|
131
|
-
const
|
|
132
|
-
if (!
|
|
90
|
+
if (name === 'inbox_list') {
|
|
91
|
+
const token = await loadToken();
|
|
92
|
+
if (!token)
|
|
93
|
+
return fail('Not authenticated. Run `haystack login` first.');
|
|
94
|
+
return ok(await fetchInboxPayload(token));
|
|
95
|
+
}
|
|
96
|
+
if (name === 'pr_get') {
|
|
97
|
+
if (typeof input.ref !== 'string')
|
|
133
98
|
return fail('ref required');
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
prNumber: pr.prNumber,
|
|
147
|
-
rating: synthesis.haystackRating,
|
|
148
|
-
autoFixerHandling: manifest?.fixedItems ?? [],
|
|
149
|
-
autoFixerSkipped: manifest?.leftForReview ?? [],
|
|
150
|
-
findings: (synthesis.synthesisDisplay ?? []).map((f) => ({
|
|
151
|
-
category: f.category,
|
|
152
|
-
summary: f.summary,
|
|
153
|
-
detail: f.detail,
|
|
154
|
-
agentFixPrompt: f.agentFixPrompt ?? null,
|
|
155
|
-
source: f.source ?? null,
|
|
156
|
-
})),
|
|
99
|
+
return ok(await fetchPrStatePayload(input.ref));
|
|
100
|
+
}
|
|
101
|
+
if (name === 'triage_get') {
|
|
102
|
+
if (typeof input.ref !== 'string')
|
|
103
|
+
return fail('ref required');
|
|
104
|
+
return ok(await fetchTriagePayload(input.ref));
|
|
105
|
+
}
|
|
106
|
+
if (name === 'ask_haystack') {
|
|
107
|
+
if (typeof input.ref !== 'string' || typeof input.question !== 'string')
|
|
108
|
+
return fail('ref and question required');
|
|
109
|
+
return ok(await fetchAskPayload(input.ref, input.question, {
|
|
110
|
+
session: typeof input.session_id === 'string' ? input.session_id : undefined,
|
|
157
111
|
}));
|
|
158
112
|
}
|
|
159
|
-
if (name === '
|
|
160
|
-
|
|
161
|
-
if (!ref)
|
|
113
|
+
if (name === 'traces_list') {
|
|
114
|
+
if (typeof input.ref !== 'string')
|
|
162
115
|
return fail('ref required');
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
116
|
+
return ok(await fetchTracesListPayload(input.ref));
|
|
117
|
+
}
|
|
118
|
+
if (name === 'traces_get') {
|
|
119
|
+
if (typeof input.ref !== 'string' || typeof input.checkpoint_id !== 'string')
|
|
120
|
+
return fail('ref and checkpoint_id required');
|
|
121
|
+
return ok(await fetchTracesGetPayload(input.ref, input.checkpoint_id, {
|
|
122
|
+
cursor: typeof input.cursor === 'string' ? input.cursor : undefined,
|
|
123
|
+
limit: typeof input.limit === 'number' ? String(input.limit) : undefined,
|
|
124
|
+
}));
|
|
167
125
|
}
|
|
168
126
|
if (name === 'schema') {
|
|
169
|
-
const
|
|
170
|
-
if (!
|
|
127
|
+
const schema = input.name;
|
|
128
|
+
if (!(schema in SCHEMA_VERSIONS))
|
|
171
129
|
return fail(`unknown schema; choose one of ${Object.keys(SCHEMA_VERSIONS).join(', ')}`);
|
|
172
|
-
|
|
173
|
-
return ok(await readSchema(n));
|
|
130
|
+
return ok(await readSchema(schema));
|
|
174
131
|
}
|
|
175
132
|
return fail(`unknown tool: ${name}`);
|
|
176
133
|
}
|
|
177
|
-
catch (
|
|
178
|
-
return fail(
|
|
134
|
+
catch (error) {
|
|
135
|
+
return fail(error instanceof Error ? error.message : String(error));
|
|
179
136
|
}
|
|
180
137
|
});
|
|
181
|
-
|
|
182
|
-
await server.connect(transport);
|
|
183
|
-
// The server runs until stdin closes. Stdio MCP gives no exit signal beyond
|
|
184
|
-
// that; we don't return.
|
|
138
|
+
await server.connect(new StdioServerTransport());
|
|
185
139
|
}
|
package/dist/commands/pr.d.ts
CHANGED
|
@@ -4,7 +4,43 @@ export interface ParsedPR {
|
|
|
4
4
|
prNumber: number;
|
|
5
5
|
}
|
|
6
6
|
export declare function parsePrRef(ref: string): ParsedPR;
|
|
7
|
+
interface GitHubPr {
|
|
8
|
+
number: number;
|
|
9
|
+
title: string;
|
|
10
|
+
state: 'open' | 'closed';
|
|
11
|
+
merged: boolean;
|
|
12
|
+
mergeable_state?: string;
|
|
13
|
+
head: {
|
|
14
|
+
sha: string;
|
|
15
|
+
ref: string;
|
|
16
|
+
};
|
|
17
|
+
base: {
|
|
18
|
+
ref: string;
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
interface MergeQueueEntry {
|
|
22
|
+
status?: string;
|
|
23
|
+
detail?: string;
|
|
24
|
+
failedChecks?: string[];
|
|
25
|
+
actionNeeded?: string;
|
|
26
|
+
}
|
|
27
|
+
export declare function mergePublicState(meta: GitHubPr, entry: MergeQueueEntry | undefined, queueAvailable?: boolean): {
|
|
28
|
+
status: string;
|
|
29
|
+
reason: string;
|
|
30
|
+
blocking_checks: string[];
|
|
31
|
+
action: string | null;
|
|
32
|
+
};
|
|
33
|
+
export declare function fetchMergePublicState(meta: GitHubPr, prNumber: number, owner: string, repo: string, token: string): Promise<{
|
|
34
|
+
status: string;
|
|
35
|
+
reason: string;
|
|
36
|
+
blocking_checks: string[];
|
|
37
|
+
action: string | null;
|
|
38
|
+
}>;
|
|
7
39
|
export interface PrReadOptions {
|
|
8
40
|
json?: boolean;
|
|
9
41
|
}
|
|
10
|
-
export declare function
|
|
42
|
+
export declare function fetchPrStatePayload(ref: string): Promise<Record<string, unknown> & {
|
|
43
|
+
schema_version: string;
|
|
44
|
+
}>;
|
|
45
|
+
export declare function prReadCommand(ref: string, options: PrReadOptions): Promise<void>;
|
|
46
|
+
export {};
|