@haystackeditor/cli 0.8.1 → 0.10.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.
Files changed (63) hide show
  1. package/README.md +93 -87
  2. package/dist/assets/hooks/llm-rules-template.md +21 -0
  3. package/dist/assets/hooks/package.json +2 -2
  4. package/dist/assets/hooks/scripts/pre-push.sh +20 -0
  5. package/dist/assets/skills/prepare-haystack.md +323 -0
  6. package/dist/assets/skills/secrets.md +164 -0
  7. package/dist/assets/skills/setup-external-sandbox.md +243 -0
  8. package/dist/assets/skills/setup-haystack.md +639 -0
  9. package/dist/assets/skills/submit.md +154 -0
  10. package/dist/assets/templates/CLAUDE.md.snippet +42 -0
  11. package/dist/assets/templates/haystack.yml +193 -0
  12. package/dist/commands/check-pending.d.ts +19 -0
  13. package/dist/commands/check-pending.js +217 -0
  14. package/dist/commands/config.d.ts +13 -21
  15. package/dist/commands/config.js +278 -92
  16. package/dist/commands/dismiss.d.ts +17 -0
  17. package/dist/commands/dismiss.js +201 -0
  18. package/dist/commands/init.js +25 -28
  19. package/dist/commands/install-session-hooks.d.ts +16 -0
  20. package/dist/commands/install-session-hooks.js +302 -0
  21. package/dist/commands/login.js +1 -1
  22. package/dist/commands/policy.d.ts +31 -0
  23. package/dist/commands/policy.js +365 -0
  24. package/dist/commands/pr-status.d.ts +16 -0
  25. package/dist/commands/pr-status.js +188 -0
  26. package/dist/commands/setup.d.ts +13 -0
  27. package/dist/commands/setup.js +496 -0
  28. package/dist/commands/skills.d.ts +2 -2
  29. package/dist/commands/skills.js +51 -186
  30. package/dist/commands/submit.d.ts +23 -0
  31. package/dist/commands/submit.js +456 -0
  32. package/dist/commands/triage.d.ts +16 -0
  33. package/dist/commands/triage.js +354 -0
  34. package/dist/index.d.ts +7 -0
  35. package/dist/index.js +344 -4
  36. package/dist/tools/detect.d.ts +50 -0
  37. package/dist/tools/detect.js +853 -0
  38. package/dist/tools/fixtures.d.ts +38 -0
  39. package/dist/tools/fixtures.js +199 -0
  40. package/dist/tools/setup.d.ts +43 -0
  41. package/dist/tools/setup.js +597 -0
  42. package/dist/triage/prompts.d.ts +31 -0
  43. package/dist/triage/prompts.js +296 -0
  44. package/dist/triage/runner.d.ts +21 -0
  45. package/dist/triage/runner.js +339 -0
  46. package/dist/triage/traces.d.ts +20 -0
  47. package/dist/triage/traces.js +305 -0
  48. package/dist/triage/types.d.ts +47 -0
  49. package/dist/triage/types.js +7 -0
  50. package/dist/types.d.ts +1387 -191
  51. package/dist/types.js +254 -2
  52. package/dist/utils/analysis-api.d.ts +108 -0
  53. package/dist/utils/analysis-api.js +194 -0
  54. package/dist/utils/config.js +1 -1
  55. package/dist/utils/git.d.ts +80 -0
  56. package/dist/utils/git.js +302 -0
  57. package/dist/utils/github-api.d.ts +83 -0
  58. package/dist/utils/github-api.js +266 -0
  59. package/dist/utils/pending-state.d.ts +40 -0
  60. package/dist/utils/pending-state.js +86 -0
  61. package/dist/utils/secrets.js +3 -3
  62. package/dist/utils/skill.js +257 -0
  63. package/package.json +11 -9
@@ -7,7 +7,8 @@ import chalk from 'chalk';
7
7
  import * as path from 'path';
8
8
  import { detectProject } from '../utils/detect.js';
9
9
  import { saveConfig, configExists } from '../utils/config.js';
10
- import { createSkillFile, createClaudeCommand } from '../utils/skill.js';
10
+ // HIDDEN: Verification flow — uncomment to re-enable
11
+ // import { createSkillFile, createClaudeCommand } from '../utils/skill.js';
11
12
  import { validateConfigSecurity, formatSecurityReport } from '../utils/secrets.js';
12
13
  export async function initCommand(options) {
13
14
  console.log(chalk.cyan('\n🌾 Haystack Setup\n'));
@@ -36,23 +37,17 @@ export async function initCommand(options) {
36
37
  const config = buildConfigFromDetection(detected);
37
38
  const configPath = await saveConfig(config);
38
39
  console.log(chalk.green(`āœ“ Created ${configPath}`));
39
- // Create skill file for agent discovery
40
- const skillPath = await createSkillFile();
41
- console.log(chalk.green(`āœ“ Created ${skillPath}`));
42
- // Create Claude Code slash command
43
- const commandPath = await createClaudeCommand();
44
- console.log(chalk.green(`āœ“ Created ${commandPath}`));
40
+ // HIDDEN: Verification flow — uncomment to re-enable
41
+ // // Create skill file for agent discovery
42
+ // const skillPath = await createSkillFile();
43
+ // console.log(chalk.green(`āœ“ Created ${skillPath}`));
44
+ //
45
+ // // Create Claude Code slash command
46
+ // const commandPath = await createClaudeCommand();
47
+ // console.log(chalk.green(`āœ“ Created ${commandPath}`));
45
48
  // Security validation
46
49
  await runSecurityCheck(configPath);
47
- // Print next steps - direct to skill
48
- console.log(chalk.cyan('\n━'.repeat(60)));
49
- console.log(chalk.cyan.bold('\nšŸ“‹ NEXT: Add verification flows\n'));
50
- console.log(chalk.white('Base config created. Now an AI agent needs to add flows.\n'));
51
- console.log(chalk.bold('Option 1: Claude Code'));
52
- console.log(chalk.dim(' Run /setup-haystack in Claude Code\n'));
53
- console.log(chalk.bold('Option 2: Other AI agents'));
54
- console.log(chalk.dim(' Give your agent: "Read .agents/skills/setup-haystack.md and follow it"\n'));
55
- console.log(chalk.cyan('━'.repeat(60) + '\n'));
50
+ console.log(chalk.green('\nDone! Run `haystack setup` to configure rules, signals, and policies.\n'));
56
51
  }
57
52
  /**
58
53
  * Run security check on config file
@@ -91,12 +86,13 @@ function buildConfigFromDetection(detected) {
91
86
  version: '1',
92
87
  name: path.basename(process.cwd()),
93
88
  services,
94
- verification: {
95
- commands: [
96
- { name: 'build', run: `${detected.packageManager} build` },
97
- { name: 'lint', run: `${detected.packageManager} lint` },
98
- ],
99
- },
89
+ // HIDDEN: Verification flow — uncomment to re-enable
90
+ // verification: {
91
+ // commands: [
92
+ // { name: 'build', run: `${detected.packageManager} build` },
93
+ // { name: 'lint', run: `${detected.packageManager} lint` },
94
+ // ],
95
+ // },
100
96
  };
101
97
  }
102
98
  return {
@@ -108,11 +104,12 @@ function buildConfigFromDetection(detected) {
108
104
  ready_pattern: detected.suggestedReadyPattern || 'Local:',
109
105
  ...(Object.keys(env).length > 0 ? { env } : {}),
110
106
  },
111
- verification: {
112
- commands: [
113
- { name: 'build', run: `${detected.packageManager} build` },
114
- { name: 'lint', run: `${detected.packageManager} lint` },
115
- ],
116
- },
107
+ // HIDDEN: Verification flow — uncomment to re-enable
108
+ // verification: {
109
+ // commands: [
110
+ // { name: 'build', run: `${detected.packageManager} build` },
111
+ // { name: 'lint', run: `${detected.packageManager} lint` },
112
+ // ],
113
+ // },
117
114
  };
118
115
  }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * install-session-hooks — Wire up session-start hooks for coding CLIs.
3
+ *
4
+ * When a CLI session starts, `haystack check-pending --hook` runs automatically
5
+ * and shows the user whether their pending PRs are "Good to merge" or "Need input".
6
+ *
7
+ * Supported CLIs:
8
+ * - Claude Code: Native SessionStart hook in .claude/settings.json
9
+ * - Codex CLI: AGENTS.md instructions (no native hook support)
10
+ * - Gemini CLI: GEMINI.md instructions (no native hook support)
11
+ */
12
+ export interface InstallSessionOptions {
13
+ cli?: string;
14
+ }
15
+ export declare function installSessionHooks(options: InstallSessionOptions): Promise<void>;
16
+ export declare function sessionHooksStatus(): Promise<void>;
@@ -0,0 +1,302 @@
1
+ /**
2
+ * install-session-hooks — Wire up session-start hooks for coding CLIs.
3
+ *
4
+ * When a CLI session starts, `haystack check-pending --hook` runs automatically
5
+ * and shows the user whether their pending PRs are "Good to merge" or "Need input".
6
+ *
7
+ * Supported CLIs:
8
+ * - Claude Code: Native SessionStart hook in .claude/settings.json
9
+ * - Codex CLI: AGENTS.md instructions (no native hook support)
10
+ * - Gemini CLI: GEMINI.md instructions (no native hook support)
11
+ */
12
+ import { execSync } from 'child_process';
13
+ import chalk from 'chalk';
14
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
15
+ import { join } from 'path';
16
+ import { findGitRoot } from '../utils/git.js';
17
+ // ============================================================================
18
+ // CLI detection (reused from triage runner pattern)
19
+ // ============================================================================
20
+ function isCLIInstalled(name) {
21
+ try {
22
+ execSync(process.platform === 'win32' ? `where ${name}` : `which ${name}`, {
23
+ stdio: 'ignore',
24
+ });
25
+ return true;
26
+ }
27
+ catch {
28
+ return false;
29
+ }
30
+ }
31
+ function detectInstalledCLIs() {
32
+ const installed = [];
33
+ if (isCLIInstalled('claude'))
34
+ installed.push('claude');
35
+ if (isCLIInstalled('codex'))
36
+ installed.push('codex');
37
+ if (isCLIInstalled('gemini'))
38
+ installed.push('gemini');
39
+ return installed;
40
+ }
41
+ // ============================================================================
42
+ // Claude Code: Native SessionStart hook
43
+ // ============================================================================
44
+ const HAYSTACK_HOOK_COMMAND = 'haystack check-pending --hook';
45
+ function installClaudeHook(gitRoot) {
46
+ const settingsDir = join(gitRoot, '.claude');
47
+ const settingsPath = join(settingsDir, 'settings.json');
48
+ try {
49
+ // Ensure .claude directory exists
50
+ if (!existsSync(settingsDir)) {
51
+ mkdirSync(settingsDir, { recursive: true });
52
+ }
53
+ // Read or create settings
54
+ let settings = {};
55
+ if (existsSync(settingsPath)) {
56
+ settings = JSON.parse(readFileSync(settingsPath, 'utf-8'));
57
+ }
58
+ // Ensure hooks object exists
59
+ const hooks = (settings.hooks || {});
60
+ settings.hooks = hooks;
61
+ // Ensure SessionStart array exists
62
+ const sessionStart = (hooks.SessionStart || []);
63
+ hooks.SessionStart = sessionStart;
64
+ // Check if haystack hook already installed
65
+ const alreadyInstalled = sessionStart.some(entry => entry.hooks?.some(h => h.command?.includes('haystack check-pending')));
66
+ if (alreadyInstalled) {
67
+ console.log(chalk.dim(' Claude Code hook already installed.'));
68
+ return true;
69
+ }
70
+ // Add the hook
71
+ sessionStart.push({
72
+ matcher: '',
73
+ hooks: [
74
+ {
75
+ type: 'command',
76
+ command: HAYSTACK_HOOK_COMMAND,
77
+ },
78
+ ],
79
+ });
80
+ writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
81
+ return true;
82
+ }
83
+ catch (err) {
84
+ console.error(chalk.red(` Failed to update .claude/settings.json: ${err}`));
85
+ return false;
86
+ }
87
+ }
88
+ function checkClaudeHook(gitRoot) {
89
+ const settingsPath = join(gitRoot, '.claude', 'settings.json');
90
+ if (!existsSync(settingsPath))
91
+ return false;
92
+ try {
93
+ const settings = JSON.parse(readFileSync(settingsPath, 'utf-8'));
94
+ const sessionStart = settings?.hooks?.SessionStart || [];
95
+ return sessionStart.some((entry) => entry.hooks?.some((h) => h.command?.includes('haystack check-pending')));
96
+ }
97
+ catch {
98
+ return false;
99
+ }
100
+ }
101
+ // ============================================================================
102
+ // Codex CLI: AGENTS.md instructions
103
+ // ============================================================================
104
+ const CODEX_HOOK_MARKER = '<!-- haystack-session-hook -->';
105
+ const CODEX_HOOK_BLOCK = `
106
+ ${CODEX_HOOK_MARKER}
107
+ ## Haystack Session Check
108
+
109
+ When starting a new session, run this command first to check for pending PR analysis results:
110
+
111
+ \`\`\`bash
112
+ haystack check-pending --hook
113
+ \`\`\`
114
+
115
+ This shows whether your submitted PRs are "Good to merge" or "Need your input".
116
+ `;
117
+ function installCodexHook(gitRoot) {
118
+ const agentsPath = join(gitRoot, 'AGENTS.md');
119
+ try {
120
+ let content = '';
121
+ if (existsSync(agentsPath)) {
122
+ content = readFileSync(agentsPath, 'utf-8');
123
+ if (content.includes(CODEX_HOOK_MARKER)) {
124
+ console.log(chalk.dim(' Codex AGENTS.md already has session hook.'));
125
+ return true;
126
+ }
127
+ }
128
+ content += CODEX_HOOK_BLOCK;
129
+ writeFileSync(agentsPath, content);
130
+ return true;
131
+ }
132
+ catch (err) {
133
+ console.error(chalk.red(` Failed to update AGENTS.md: ${err}`));
134
+ return false;
135
+ }
136
+ }
137
+ function checkCodexHook(gitRoot) {
138
+ const agentsPath = join(gitRoot, 'AGENTS.md');
139
+ if (!existsSync(agentsPath))
140
+ return false;
141
+ try {
142
+ const content = readFileSync(agentsPath, 'utf-8');
143
+ return content.includes(CODEX_HOOK_MARKER);
144
+ }
145
+ catch {
146
+ return false;
147
+ }
148
+ }
149
+ // ============================================================================
150
+ // Gemini CLI: GEMINI.md instructions
151
+ // ============================================================================
152
+ const GEMINI_HOOK_MARKER = '<!-- haystack-session-hook -->';
153
+ const GEMINI_HOOK_BLOCK = `
154
+ ${GEMINI_HOOK_MARKER}
155
+ ## Haystack Session Check
156
+
157
+ When starting a new session, run this command first to check for pending PR analysis results:
158
+
159
+ \`\`\`bash
160
+ haystack check-pending --hook
161
+ \`\`\`
162
+
163
+ This shows whether your submitted PRs are "Good to merge" or "Need your input".
164
+ `;
165
+ function installGeminiHook(gitRoot) {
166
+ const geminiPath = join(gitRoot, 'GEMINI.md');
167
+ try {
168
+ let content = '';
169
+ if (existsSync(geminiPath)) {
170
+ content = readFileSync(geminiPath, 'utf-8');
171
+ if (content.includes(GEMINI_HOOK_MARKER)) {
172
+ console.log(chalk.dim(' Gemini GEMINI.md already has session hook.'));
173
+ return true;
174
+ }
175
+ }
176
+ content += GEMINI_HOOK_BLOCK;
177
+ writeFileSync(geminiPath, content);
178
+ return true;
179
+ }
180
+ catch (err) {
181
+ console.error(chalk.red(` Failed to update GEMINI.md: ${err}`));
182
+ return false;
183
+ }
184
+ }
185
+ function checkGeminiHook(gitRoot) {
186
+ const geminiPath = join(gitRoot, 'GEMINI.md');
187
+ if (!existsSync(geminiPath))
188
+ return false;
189
+ try {
190
+ const content = readFileSync(geminiPath, 'utf-8');
191
+ return content.includes(GEMINI_HOOK_MARKER);
192
+ }
193
+ catch {
194
+ return false;
195
+ }
196
+ }
197
+ // ============================================================================
198
+ // CLI config registry
199
+ // ============================================================================
200
+ const CLI_HOOKS = {
201
+ claude: {
202
+ name: 'claude',
203
+ displayName: 'Claude Code',
204
+ hasNativeHook: true,
205
+ install: installClaudeHook,
206
+ check: checkClaudeHook,
207
+ },
208
+ codex: {
209
+ name: 'codex',
210
+ displayName: 'Codex CLI',
211
+ hasNativeHook: false,
212
+ install: installCodexHook,
213
+ check: checkCodexHook,
214
+ },
215
+ gemini: {
216
+ name: 'gemini',
217
+ displayName: 'Gemini CLI',
218
+ hasNativeHook: false,
219
+ install: installGeminiHook,
220
+ check: checkGeminiHook,
221
+ },
222
+ };
223
+ // ============================================================================
224
+ // Commands
225
+ // ============================================================================
226
+ export async function installSessionHooks(options) {
227
+ console.log(chalk.cyan('\nHaystack Session Hooks\n'));
228
+ const gitRoot = findGitRoot();
229
+ if (!gitRoot) {
230
+ console.error(chalk.red('Not a git repository.\n'));
231
+ process.exit(1);
232
+ }
233
+ // Determine which CLIs to install for
234
+ let targets;
235
+ if (options.cli) {
236
+ const cli = options.cli.toLowerCase();
237
+ if (cli === 'all') {
238
+ targets = detectInstalledCLIs();
239
+ }
240
+ else if (cli in CLI_HOOKS) {
241
+ targets = [cli];
242
+ }
243
+ else {
244
+ console.error(chalk.red(`Unknown CLI: ${options.cli}`));
245
+ console.log(chalk.dim('Supported: claude, codex, gemini, all'));
246
+ process.exit(1);
247
+ }
248
+ }
249
+ else {
250
+ // Auto-detect
251
+ targets = detectInstalledCLIs();
252
+ }
253
+ if (targets.length === 0) {
254
+ console.log(chalk.yellow('No supported coding CLI detected.\n'));
255
+ console.log(chalk.dim('Supported CLIs: Claude Code, Codex CLI, Gemini CLI'));
256
+ console.log(chalk.dim('Install one and try again, or specify --cli <name>.\n'));
257
+ return;
258
+ }
259
+ console.log(chalk.dim(`Installing for: ${targets.map(t => CLI_HOOKS[t].displayName).join(', ')}\n`));
260
+ for (const target of targets) {
261
+ const config = CLI_HOOKS[target];
262
+ const hookType = config.hasNativeHook ? 'native hook' : 'markdown instructions';
263
+ console.log(` Installing ${config.displayName} (${hookType})...`);
264
+ const success = config.install(gitRoot);
265
+ if (success) {
266
+ console.log(chalk.green(` āœ“ ${config.displayName} configured`));
267
+ }
268
+ else {
269
+ console.log(chalk.red(` āœ— ${config.displayName} failed`));
270
+ }
271
+ }
272
+ console.log(chalk.green('\nāœ“ Session hooks installed!\n'));
273
+ console.log(chalk.dim('When you start a new session, you\'ll see:'));
274
+ console.log(chalk.green(' [Haystack] āœ“ PR #42 "Fix auth": Good to merge'));
275
+ console.log(chalk.yellow(' [Haystack] ⚠ PR #37 "Refactor API": Needs your input (2 bugs)'));
276
+ console.log('');
277
+ }
278
+ export async function sessionHooksStatus() {
279
+ console.log(chalk.cyan('\nHaystack Session Hooks Status\n'));
280
+ const gitRoot = findGitRoot();
281
+ if (!gitRoot) {
282
+ console.error(chalk.red('Not a git repository.\n'));
283
+ process.exit(1);
284
+ }
285
+ const installed = detectInstalledCLIs();
286
+ for (const cli of ['claude', 'codex', 'gemini']) {
287
+ const config = CLI_HOOKS[cli];
288
+ const isInstalled = installed.includes(cli);
289
+ const hookConfigured = config.check(gitRoot);
290
+ const cliIcon = isInstalled ? chalk.green('āœ“') : chalk.dim('-');
291
+ const hookIcon = hookConfigured ? chalk.green('āœ“') : chalk.red('āœ—');
292
+ console.log(` ${cliIcon} ${config.displayName} ${isInstalled ? '(installed)' : '(not found)'}`);
293
+ if (isInstalled) {
294
+ console.log(` ${hookIcon} Session hook ${hookConfigured ? 'configured' : 'not configured'}`);
295
+ }
296
+ }
297
+ const anyMissing = installed.some(cli => !CLI_HOOKS[cli].check(gitRoot));
298
+ if (anyMissing) {
299
+ console.log(chalk.dim('\nRun: haystack hooks install-session'));
300
+ }
301
+ console.log('');
302
+ }
@@ -32,7 +32,7 @@ async function startDeviceFlow() {
32
32
  * Poll for access token
33
33
  */
34
34
  async function pollForToken(deviceCode, interval) {
35
- while (true) {
35
+ for (;;) {
36
36
  await new Promise(resolve => setTimeout(resolve, interval * 1000));
37
37
  const response = await fetch('https://github.com/login/oauth/access_token', {
38
38
  method: 'POST',
@@ -0,0 +1,31 @@
1
+ /**
2
+ * haystack policy - Manage review policies
3
+ *
4
+ * Commands:
5
+ * list - List all review policies
6
+ * add - Add a new policy interactively
7
+ * remove - Remove a policy by name
8
+ * init - Create initial review-policy.md
9
+ */
10
+ /**
11
+ * List all review policies
12
+ */
13
+ export declare function listPolicies(): Promise<void>;
14
+ /**
15
+ * Add a new review policy interactively
16
+ */
17
+ export declare function addPolicy(nameArg?: string): Promise<void>;
18
+ /**
19
+ * Remove a policy by name
20
+ */
21
+ export declare function removePolicy(name: string): Promise<void>;
22
+ /**
23
+ * Add a review instruction
24
+ */
25
+ export declare function addInstruction(text?: string): Promise<void>;
26
+ /**
27
+ * Create initial review-policy.md with example policies
28
+ */
29
+ export declare function initPolicies(options: {
30
+ force?: boolean;
31
+ }): Promise<void>;