@haystackeditor/cli 0.8.0 → 0.9.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 (76) hide show
  1. package/README.md +105 -12
  2. package/dist/assets/hooks/agent-context/detect.ts +136 -0
  3. package/dist/assets/hooks/agent-context/format.ts +99 -0
  4. package/dist/assets/hooks/agent-context/index.ts +39 -0
  5. package/dist/assets/hooks/agent-context/parsers/claude.ts +253 -0
  6. package/dist/assets/hooks/agent-context/parsers/gemini.ts +155 -0
  7. package/dist/assets/hooks/agent-context/parsers/opencode.ts +174 -0
  8. package/dist/assets/hooks/agent-context/tsconfig.json +13 -0
  9. package/dist/assets/hooks/agent-context/types.ts +58 -0
  10. package/dist/assets/hooks/llm-rules-template.md +56 -0
  11. package/dist/assets/hooks/package.json +11 -0
  12. package/dist/assets/hooks/scripts/commit-msg.sh +4 -0
  13. package/dist/assets/hooks/scripts/post-commit.sh +4 -0
  14. package/dist/assets/hooks/scripts/pre-commit.sh +92 -0
  15. package/dist/assets/hooks/scripts/pre-push.sh +25 -0
  16. package/dist/assets/hooks/scripts/prepare-commit-msg.sh +3 -0
  17. package/dist/assets/hooks/truncation-checker/ast-analyzer.ts +528 -0
  18. package/dist/assets/hooks/truncation-checker/index.ts +595 -0
  19. package/dist/assets/hooks/truncation-checker/tsconfig.json +13 -0
  20. package/dist/assets/skills/prepare-haystack.md +323 -0
  21. package/dist/assets/skills/secrets.md +164 -0
  22. package/dist/assets/skills/setup-external-sandbox.md +243 -0
  23. package/dist/assets/skills/setup-haystack.md +639 -0
  24. package/dist/assets/skills/submit.md +154 -0
  25. package/dist/assets/templates/CLAUDE.md.snippet +42 -0
  26. package/dist/assets/templates/haystack.yml +193 -0
  27. package/dist/commands/check-pending.d.ts +19 -0
  28. package/dist/commands/check-pending.js +217 -0
  29. package/dist/commands/config.d.ts +18 -12
  30. package/dist/commands/config.js +327 -52
  31. package/dist/commands/hooks.d.ts +17 -0
  32. package/dist/commands/hooks.js +269 -0
  33. package/dist/commands/install-session-hooks.d.ts +16 -0
  34. package/dist/commands/install-session-hooks.js +302 -0
  35. package/dist/commands/login.js +1 -1
  36. package/dist/commands/policy.d.ts +31 -0
  37. package/dist/commands/policy.js +365 -0
  38. package/dist/commands/skills.d.ts +8 -0
  39. package/dist/commands/skills.js +80 -0
  40. package/dist/commands/submit.d.ts +22 -0
  41. package/dist/commands/submit.js +428 -0
  42. package/dist/commands/triage.d.ts +16 -0
  43. package/dist/commands/triage.js +354 -0
  44. package/dist/index.d.ts +3 -0
  45. package/dist/index.js +317 -2
  46. package/dist/tools/detect.d.ts +50 -0
  47. package/dist/tools/detect.js +853 -0
  48. package/dist/tools/fixtures.d.ts +38 -0
  49. package/dist/tools/fixtures.js +199 -0
  50. package/dist/tools/setup.d.ts +43 -0
  51. package/dist/tools/setup.js +597 -0
  52. package/dist/triage/prompts.d.ts +31 -0
  53. package/dist/triage/prompts.js +266 -0
  54. package/dist/triage/runner.d.ts +21 -0
  55. package/dist/triage/runner.js +325 -0
  56. package/dist/triage/traces.d.ts +20 -0
  57. package/dist/triage/traces.js +305 -0
  58. package/dist/triage/types.d.ts +47 -0
  59. package/dist/triage/types.js +7 -0
  60. package/dist/types.d.ts +1387 -191
  61. package/dist/types.js +254 -2
  62. package/dist/utils/analysis-api.d.ts +108 -0
  63. package/dist/utils/analysis-api.js +194 -0
  64. package/dist/utils/config.js +1 -1
  65. package/dist/utils/git.d.ts +80 -0
  66. package/dist/utils/git.js +302 -0
  67. package/dist/utils/github-api.d.ts +83 -0
  68. package/dist/utils/github-api.js +266 -0
  69. package/dist/utils/hooks.d.ts +26 -0
  70. package/dist/utils/hooks.js +226 -0
  71. package/dist/utils/pending-state.d.ts +38 -0
  72. package/dist/utils/pending-state.js +86 -0
  73. package/dist/utils/secrets.js +3 -3
  74. package/dist/utils/skill.d.ts +1 -1
  75. package/dist/utils/skill.js +658 -1
  76. package/package.json +5 -3
@@ -0,0 +1,226 @@
1
+ /**
2
+ * Utilities for haystack hooks install/status/update commands.
3
+ *
4
+ * Handles Entire CLI binary download, hook file copying, and git configuration.
5
+ */
6
+ import * as fs from 'fs/promises';
7
+ import { existsSync } from 'fs';
8
+ import * as path from 'path';
9
+ import * as os from 'os';
10
+ import { execSync } from 'child_process';
11
+ import { fileURLToPath } from 'url';
12
+ // ============================================================================
13
+ // Constants
14
+ // ============================================================================
15
+ export const ENTIRE_DEFAULT_VERSION = '0.4.8';
16
+ export const ENTIRE_GITHUB_REPO = 'entireio/cli';
17
+ export const HAYSTACK_BIN_DIR = path.join(os.homedir(), '.haystack', 'bin');
18
+ export const ENTIRE_BIN_PATH = path.join(HAYSTACK_BIN_DIR, 'entire');
19
+ const HOOK_SCRIPTS = [
20
+ 'pre-commit',
21
+ 'commit-msg',
22
+ 'post-commit',
23
+ 'pre-push',
24
+ 'prepare-commit-msg',
25
+ ];
26
+ // ============================================================================
27
+ // Platform detection
28
+ // ============================================================================
29
+ const OS_MAP = {
30
+ darwin: 'darwin',
31
+ linux: 'linux',
32
+ };
33
+ const ARCH_MAP = {
34
+ arm64: 'arm64',
35
+ x64: 'amd64',
36
+ };
37
+ export function getPlatformAssetName() {
38
+ const osName = OS_MAP[process.platform];
39
+ const archName = ARCH_MAP[process.arch];
40
+ if (!osName || !archName) {
41
+ throw new Error(`Unsupported platform: ${process.platform}-${process.arch}. Entire CLI supports macOS and Linux (amd64/arm64).`);
42
+ }
43
+ return `entire_${osName}_${archName}.tar.gz`;
44
+ }
45
+ // ============================================================================
46
+ // Git repo detection
47
+ // ============================================================================
48
+ export function findGitRoot() {
49
+ try {
50
+ return execSync('git rev-parse --show-toplevel', {
51
+ encoding: 'utf-8',
52
+ stdio: ['pipe', 'pipe', 'pipe'],
53
+ }).trim();
54
+ }
55
+ catch {
56
+ return null;
57
+ }
58
+ }
59
+ // ============================================================================
60
+ // Entire binary management
61
+ // ============================================================================
62
+ export async function getInstalledEntireVersion() {
63
+ if (!existsSync(ENTIRE_BIN_PATH))
64
+ return null;
65
+ try {
66
+ const output = execSync(`"${ENTIRE_BIN_PATH}" --version`, {
67
+ encoding: 'utf-8',
68
+ stdio: ['pipe', 'pipe', 'pipe'],
69
+ }).trim();
70
+ // Output is like "entire version 0.4.8" or just "0.4.8"
71
+ const match = output.match(/(\d+\.\d+\.\d+)/);
72
+ return match ? match[1] : output;
73
+ }
74
+ catch {
75
+ return null;
76
+ }
77
+ }
78
+ export async function getLatestEntireVersion() {
79
+ const response = await fetch(`https://api.github.com/repos/${ENTIRE_GITHUB_REPO}/releases/latest`, {
80
+ headers: {
81
+ 'User-Agent': 'Haystack-CLI',
82
+ Accept: 'application/vnd.github.v3+json',
83
+ },
84
+ });
85
+ if (!response.ok) {
86
+ throw new Error(`Failed to fetch latest Entire release: ${response.status} ${response.statusText}`);
87
+ }
88
+ const data = (await response.json());
89
+ // tag_name is like "v0.4.8"
90
+ return data.tag_name.replace(/^v/, '');
91
+ }
92
+ export async function downloadEntireBinary(version) {
93
+ const assetName = getPlatformAssetName();
94
+ const tag = version.startsWith('v') ? version : `v${version}`;
95
+ // Fetch release to get asset download URL
96
+ const releaseResponse = await fetch(`https://api.github.com/repos/${ENTIRE_GITHUB_REPO}/releases/tags/${tag}`, {
97
+ headers: {
98
+ 'User-Agent': 'Haystack-CLI',
99
+ Accept: 'application/vnd.github.v3+json',
100
+ },
101
+ });
102
+ if (!releaseResponse.ok) {
103
+ throw new Error(`Failed to fetch Entire release ${tag}: ${releaseResponse.status}`);
104
+ }
105
+ const release = (await releaseResponse.json());
106
+ const asset = release.assets.find((a) => a.name === assetName);
107
+ if (!asset) {
108
+ throw new Error(`No binary found for ${assetName} in Entire release ${tag}. Available: ${release.assets.map((a) => a.name).join(', ')}`);
109
+ }
110
+ // Download the tar.gz
111
+ const downloadResponse = await fetch(asset.browser_download_url, {
112
+ headers: { 'User-Agent': 'Haystack-CLI' },
113
+ });
114
+ if (!downloadResponse.ok) {
115
+ throw new Error(`Failed to download ${assetName}: ${downloadResponse.status}`);
116
+ }
117
+ const buffer = Buffer.from(await downloadResponse.arrayBuffer());
118
+ // Create temp dir, extract, and move binary
119
+ const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'haystack-entire-'));
120
+ const tarPath = path.join(tmpDir, assetName);
121
+ try {
122
+ await fs.writeFile(tarPath, buffer);
123
+ execSync(`tar xzf "${tarPath}" -C "${tmpDir}"`, { stdio: 'pipe' });
124
+ // Ensure bin dir exists
125
+ await fs.mkdir(HAYSTACK_BIN_DIR, { recursive: true });
126
+ // Move the binary
127
+ const extractedBinary = path.join(tmpDir, 'entire');
128
+ if (!existsSync(extractedBinary)) {
129
+ throw new Error('Entire binary not found in extracted archive');
130
+ }
131
+ await fs.copyFile(extractedBinary, ENTIRE_BIN_PATH);
132
+ await fs.chmod(ENTIRE_BIN_PATH, 0o755);
133
+ }
134
+ finally {
135
+ // Clean up temp dir
136
+ await fs.rm(tmpDir, { recursive: true, force: true });
137
+ }
138
+ }
139
+ // ============================================================================
140
+ // Asset directory resolution
141
+ // ============================================================================
142
+ const __filename = fileURLToPath(import.meta.url);
143
+ const __dirname = path.dirname(__filename);
144
+ /** Resolves the path to dist/assets/hooks/ (or src/assets/hooks/ in dev) */
145
+ export function getAssetsDir() {
146
+ // From dist/utils/hooks.js → dist/assets/hooks/
147
+ // From src/utils/hooks.ts → src/assets/hooks/
148
+ return path.resolve(__dirname, '..', 'assets', 'hooks');
149
+ }
150
+ // ============================================================================
151
+ // Hook file operations
152
+ // ============================================================================
153
+ /** Copy hook scripts and TypeScript modules into the target hooks directory */
154
+ export async function copyHookFiles(hooksDir) {
155
+ const assetsDir = getAssetsDir();
156
+ const scriptsDir = path.join(assetsDir, 'scripts');
157
+ // Ensure hooks directory exists
158
+ await fs.mkdir(hooksDir, { recursive: true });
159
+ // Copy shell scripts (strip .sh extension, set executable)
160
+ for (const hook of HOOK_SCRIPTS) {
161
+ const src = path.join(scriptsDir, `${hook}.sh`);
162
+ const dest = path.join(hooksDir, hook);
163
+ await fs.copyFile(src, dest);
164
+ await fs.chmod(dest, 0o755);
165
+ }
166
+ // Copy agent-context module
167
+ await copyDirRecursive(path.join(assetsDir, 'agent-context'), path.join(hooksDir, 'agent-context'));
168
+ // Copy truncation-checker module
169
+ await copyDirRecursive(path.join(assetsDir, 'truncation-checker'), path.join(hooksDir, 'truncation-checker'));
170
+ // Copy hooks package.json
171
+ await fs.copyFile(path.join(assetsDir, 'package.json'), path.join(hooksDir, 'package.json'));
172
+ }
173
+ /** Install npm dependencies in the hooks directory */
174
+ export async function installHookDeps(hooksDir) {
175
+ execSync('npm install --no-fund --no-audit', {
176
+ cwd: hooksDir,
177
+ stdio: 'pipe',
178
+ timeout: 60_000,
179
+ });
180
+ }
181
+ /** Copy LLM rules template if none exists */
182
+ export async function copyLlmRulesTemplate(repoRoot) {
183
+ const dest = path.join(repoRoot, 'LLM_RULES.md');
184
+ if (existsSync(dest))
185
+ return false;
186
+ const src = path.join(getAssetsDir(), 'llm-rules-template.md');
187
+ await fs.copyFile(src, dest);
188
+ return true;
189
+ }
190
+ /** Create .entire/settings.json and .entire/.gitignore */
191
+ export async function createEntireConfig(repoRoot) {
192
+ const entireDir = path.join(repoRoot, '.entire');
193
+ await fs.mkdir(entireDir, { recursive: true });
194
+ await fs.writeFile(path.join(entireDir, 'settings.json'), JSON.stringify({ enabled: true, telemetry: false }, null, 2) + '\n', 'utf-8');
195
+ await fs.writeFile(path.join(entireDir, '.gitignore'), 'tmp/\nsettings.local.json\nmetadata/\nlogs/\n', 'utf-8');
196
+ }
197
+ /** Add .entire/metadata/ to repo .gitignore if not already present */
198
+ export async function updateGitignore(repoRoot) {
199
+ const gitignorePath = path.join(repoRoot, '.gitignore');
200
+ let content = '';
201
+ if (existsSync(gitignorePath)) {
202
+ content = await fs.readFile(gitignorePath, 'utf-8');
203
+ }
204
+ if (content.includes('.entire/metadata'))
205
+ return false;
206
+ const addition = '\n# Entire CLI local session data\n.entire/metadata/\n';
207
+ await fs.writeFile(gitignorePath, content + addition, 'utf-8');
208
+ return true;
209
+ }
210
+ // ============================================================================
211
+ // Helpers
212
+ // ============================================================================
213
+ async function copyDirRecursive(src, dest) {
214
+ await fs.mkdir(dest, { recursive: true });
215
+ const entries = await fs.readdir(src, { withFileTypes: true });
216
+ for (const entry of entries) {
217
+ const srcPath = path.join(src, entry.name);
218
+ const destPath = path.join(dest, entry.name);
219
+ if (entry.isDirectory()) {
220
+ await copyDirRecursive(srcPath, destPath);
221
+ }
222
+ else {
223
+ await fs.copyFile(srcPath, destPath);
224
+ }
225
+ }
226
+ }
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Pending submit state persistence.
3
+ *
4
+ * Manages `.haystack/pending-submit.json` in the git root directory.
5
+ * This file persists across CLI sessions so that analysis results
6
+ * can be retrieved after disconnection.
7
+ */
8
+ import type { AnalysisVerdict } from './analysis-api.js';
9
+ export interface PendingSubmit {
10
+ version: 1;
11
+ prNumber: number;
12
+ prUrl: string;
13
+ owner: string;
14
+ repo: string;
15
+ branch: string;
16
+ baseBranch: string;
17
+ submittedAt: string;
18
+ status: 'polling' | 'completed' | 'failed' | 'stale';
19
+ analysisResult?: AnalysisVerdict;
20
+ resolvedAt?: string;
21
+ }
22
+ /**
23
+ * Save pending submit state to disk.
24
+ */
25
+ export declare function savePendingSubmit(state: PendingSubmit, gitRoot?: string): void;
26
+ /**
27
+ * Load pending submit state from disk.
28
+ * Returns null if no pending state exists or if it's stale.
29
+ */
30
+ export declare function loadPendingSubmit(gitRoot?: string): PendingSubmit | null;
31
+ /**
32
+ * Clear pending submit state (delete the file).
33
+ */
34
+ export declare function clearPendingSubmit(gitRoot?: string): void;
35
+ /**
36
+ * Update the status and optionally the analysis result of a pending submit.
37
+ */
38
+ export declare function updatePendingSubmit(updates: Partial<Pick<PendingSubmit, 'status' | 'analysisResult' | 'resolvedAt'>>, gitRoot?: string): void;
@@ -0,0 +1,86 @@
1
+ /**
2
+ * Pending submit state persistence.
3
+ *
4
+ * Manages `.haystack/pending-submit.json` in the git root directory.
5
+ * This file persists across CLI sessions so that analysis results
6
+ * can be retrieved after disconnection.
7
+ */
8
+ import { existsSync, mkdirSync, readFileSync, writeFileSync, unlinkSync } from 'fs';
9
+ import { join } from 'path';
10
+ import { findGitRoot } from './git.js';
11
+ // ============================================================================
12
+ // Constants
13
+ // ============================================================================
14
+ const PENDING_FILE = 'pending-submit.json';
15
+ const HAYSTACK_DIR = '.haystack';
16
+ const STALE_THRESHOLD_MS = 2 * 60 * 60 * 1000; // 2 hours
17
+ // ============================================================================
18
+ // Functions
19
+ // ============================================================================
20
+ function getPendingPath(gitRoot) {
21
+ const root = gitRoot || findGitRoot();
22
+ if (!root)
23
+ return null;
24
+ return join(root, HAYSTACK_DIR, PENDING_FILE);
25
+ }
26
+ /**
27
+ * Save pending submit state to disk.
28
+ */
29
+ export function savePendingSubmit(state, gitRoot) {
30
+ const root = gitRoot || findGitRoot();
31
+ if (!root)
32
+ throw new Error('Not a git repository');
33
+ const dir = join(root, HAYSTACK_DIR);
34
+ if (!existsSync(dir)) {
35
+ mkdirSync(dir, { recursive: true });
36
+ }
37
+ const filePath = join(dir, PENDING_FILE);
38
+ writeFileSync(filePath, JSON.stringify(state, null, 2));
39
+ }
40
+ /**
41
+ * Load pending submit state from disk.
42
+ * Returns null if no pending state exists or if it's stale.
43
+ */
44
+ export function loadPendingSubmit(gitRoot) {
45
+ const filePath = getPendingPath(gitRoot);
46
+ if (!filePath || !existsSync(filePath))
47
+ return null;
48
+ try {
49
+ const content = readFileSync(filePath, 'utf-8');
50
+ const state = JSON.parse(content);
51
+ // Check version
52
+ if (state.version !== 1)
53
+ return null;
54
+ // Auto-mark stale entries
55
+ if (state.status === 'polling') {
56
+ const age = Date.now() - new Date(state.submittedAt).getTime();
57
+ if (age > STALE_THRESHOLD_MS) {
58
+ state.status = 'stale';
59
+ savePendingSubmit(state, gitRoot);
60
+ }
61
+ }
62
+ return state;
63
+ }
64
+ catch {
65
+ return null;
66
+ }
67
+ }
68
+ /**
69
+ * Clear pending submit state (delete the file).
70
+ */
71
+ export function clearPendingSubmit(gitRoot) {
72
+ const filePath = getPendingPath(gitRoot);
73
+ if (filePath && existsSync(filePath)) {
74
+ unlinkSync(filePath);
75
+ }
76
+ }
77
+ /**
78
+ * Update the status and optionally the analysis result of a pending submit.
79
+ */
80
+ export function updatePendingSubmit(updates, gitRoot) {
81
+ const state = loadPendingSubmit(gitRoot);
82
+ if (!state)
83
+ return;
84
+ Object.assign(state, updates);
85
+ savePendingSubmit(state, gitRoot);
86
+ }
@@ -18,7 +18,7 @@ const SECRET_PATTERNS = [
18
18
  // API Keys
19
19
  {
20
20
  name: 'generic-api-key',
21
- pattern: /(?:api[_-]?key|apikey)\s*[:=]\s*['"]?([a-zA-Z0-9_\-]{20,})['"]?/gi,
21
+ pattern: /(?:api[_-]?key|apikey)\s*[:=]\s*['"]?([a-zA-Z0-9_-]{20,})['"]?/gi,
22
22
  description: 'Potential API key detected',
23
23
  severity: 'high',
24
24
  },
@@ -43,7 +43,7 @@ const SECRET_PATTERNS = [
43
43
  },
44
44
  {
45
45
  name: 'cloudflare-api-token',
46
- pattern: /(?:cloudflare|cf)[_-]?(?:api)?[_-]?token\s*[:=]\s*['"]?([a-zA-Z0-9_\-]{40,})['"]?/gi,
46
+ pattern: /(?:cloudflare|cf)[_-]?(?:api)?[_-]?token\s*[:=]\s*['"]?([a-zA-Z0-9_-]{40,})['"]?/gi,
47
47
  description: 'Cloudflare API Token detected',
48
48
  severity: 'high',
49
49
  },
@@ -122,7 +122,7 @@ const SECRET_PATTERNS = [
122
122
  // Generic high-entropy strings in config values
123
123
  {
124
124
  name: 'high-entropy-string',
125
- pattern: /(?:token|key|secret|password|credential|auth)\s*[:=]\s*['"]([a-zA-Z0-9+/=_\-]{32,})['"]?/gi,
125
+ pattern: /(?:token|key|secret|password|credential|auth)\s*[:=]\s*['"]([a-zA-Z0-9+/=_-]{32,})['"]?/gi,
126
126
  description: 'High-entropy string in sensitive field',
127
127
  severity: 'medium',
128
128
  },
@@ -5,6 +5,6 @@
5
5
  export declare function createSkillFile(): Promise<string>;
6
6
  /**
7
7
  * Create the .claude/commands/ files for Claude Code slash commands
8
- * Users can invoke with /setup-haystack or /prepare-haystack
8
+ * Users can invoke with /setup-haystack, /prepare-haystack, or /setup-haystack-secrets
9
9
  */
10
10
  export declare function createClaudeCommand(): Promise<string>;