@haystackeditor/cli 0.8.0 → 0.8.1

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 (31) hide show
  1. package/README.md +59 -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 +35 -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 +5 -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/commands/config.d.ts +14 -0
  21. package/dist/commands/config.js +89 -0
  22. package/dist/commands/hooks.d.ts +17 -0
  23. package/dist/commands/hooks.js +269 -0
  24. package/dist/commands/skills.d.ts +8 -0
  25. package/dist/commands/skills.js +215 -0
  26. package/dist/index.js +86 -1
  27. package/dist/utils/hooks.d.ts +26 -0
  28. package/dist/utils/hooks.js +226 -0
  29. package/dist/utils/skill.d.ts +1 -1
  30. package/dist/utils/skill.js +401 -1
  31. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -16,7 +16,9 @@ import { statusCommand } from './commands/status.js';
16
16
  import { initCommand } from './commands/init.js';
17
17
  import { loginCommand, logoutCommand } from './commands/login.js';
18
18
  import { listSecrets, setSecret, getSecret, getSecretsForRepo, deleteSecret } from './commands/secrets.js';
19
- import { handleSandbox } from './commands/config.js';
19
+ import { handleSandbox, handleAgenticTool } from './commands/config.js';
20
+ import { installSkills, listSkills } from './commands/skills.js';
21
+ import { hooksInstall, hooksStatus, hooksUpdate } from './commands/hooks.js';
20
22
  const program = new Command();
21
23
  program
22
24
  .name('haystack')
@@ -101,6 +103,89 @@ Examples:
101
103
  haystack config sandbox off # Disable sandbox mode
102
104
  `)
103
105
  .action(handleSandbox);
106
+ config
107
+ .command('agentic-tool [tool]')
108
+ .description('Set agentic tool (opencode|claude-code|codex|status)')
109
+ .addHelpText('after', `
110
+ Tools:
111
+ opencode OpenCode (Haystack billing) - default
112
+ claude-code Claude Code (your Claude Max subscription)
113
+ codex Codex CLI (your ChatGPT subscription)
114
+ status Show current setting (default)
115
+
116
+ This sets your account-level default. Projects can override
117
+ this in .haystack.json under agentic.tool.
118
+
119
+ Examples:
120
+ haystack config agentic-tool # Show current setting
121
+ haystack config agentic-tool opencode # Use Haystack billing
122
+ haystack config agentic-tool claude-code # Use your Claude Max
123
+ haystack config agentic-tool codex # Use your ChatGPT
124
+ `)
125
+ .action(handleAgenticTool);
126
+ // Skills subcommands
127
+ const skills = program
128
+ .command('skills')
129
+ .description('Manage AI skills for Claude Code');
130
+ skills
131
+ .command('install')
132
+ .description('Install Haystack skills into your coding CLI')
133
+ .option('--cli <name>', 'Target CLI: claude, codex, cursor, or manual')
134
+ .addHelpText('after', `
135
+ This registers the Haystack MCP server with your coding CLI, enabling:
136
+ /setup-haystack - AI-assisted project setup
137
+ /prepare-haystack - Add accessibility attributes
138
+ /setup-haystack-secrets - Configure secrets
139
+
140
+ Supported CLIs:
141
+ claude Claude Code (auto-detected)
142
+ codex Codex CLI (auto-detected)
143
+ cursor Cursor IDE (auto-detected)
144
+ manual Show manual setup instructions
145
+
146
+ Examples:
147
+ haystack skills install # Auto-detect and install
148
+ haystack skills install --cli codex # Install for Codex only
149
+ haystack skills install --cli manual # Show manual instructions
150
+ `)
151
+ .action(installSkills);
152
+ skills
153
+ .command('list')
154
+ .description('List available Haystack skills')
155
+ .action(listSkills);
156
+ // Hooks subcommands
157
+ const hooks = program
158
+ .command('hooks')
159
+ .description('Manage git hooks for AI agent quality checks');
160
+ hooks
161
+ .command('install')
162
+ .description('Install Haystack git hooks and Entire CLI')
163
+ .option('--version <version>', 'Entire CLI version (default: pinned)')
164
+ .option('-f, --force', 'Overwrite existing hooks')
165
+ .option('--skip-entire', 'Skip Entire binary download')
166
+ .addHelpText('after', `
167
+ This installs:
168
+ • Git hooks for AI agent quality checks (pre-commit, commit-msg, etc.)
169
+ • Agent context detector (identifies AI agent sessions)
170
+ • Truncation checker (prevents code truncation by LLMs)
171
+ • Entire CLI binary for session tracking (powered by https://entire.dev)
172
+
173
+ Hooks are installed to <repo>/hooks/ and git is configured to use them.
174
+
175
+ Examples:
176
+ haystack hooks install # Install with pinned Entire version
177
+ haystack hooks install --force # Overwrite existing hooks
178
+ haystack hooks install --skip-entire # Only install Haystack hooks
179
+ `)
180
+ .action(hooksInstall);
181
+ hooks
182
+ .command('status')
183
+ .description('Check hooks installation status')
184
+ .action(hooksStatus);
185
+ hooks
186
+ .command('update')
187
+ .description('Update Entire CLI to the latest version')
188
+ .action(hooksUpdate);
104
189
  // Show help if no command provided
105
190
  if (process.argv.length === 2) {
106
191
  program.help();
@@ -0,0 +1,26 @@
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
+ export declare const ENTIRE_DEFAULT_VERSION = "0.4.8";
7
+ export declare const ENTIRE_GITHUB_REPO = "entireio/cli";
8
+ export declare const HAYSTACK_BIN_DIR: string;
9
+ export declare const ENTIRE_BIN_PATH: string;
10
+ export declare function getPlatformAssetName(): string;
11
+ export declare function findGitRoot(): string | null;
12
+ export declare function getInstalledEntireVersion(): Promise<string | null>;
13
+ export declare function getLatestEntireVersion(): Promise<string>;
14
+ export declare function downloadEntireBinary(version: string): Promise<void>;
15
+ /** Resolves the path to dist/assets/hooks/ (or src/assets/hooks/ in dev) */
16
+ export declare function getAssetsDir(): string;
17
+ /** Copy hook scripts and TypeScript modules into the target hooks directory */
18
+ export declare function copyHookFiles(hooksDir: string): Promise<void>;
19
+ /** Install npm dependencies in the hooks directory */
20
+ export declare function installHookDeps(hooksDir: string): Promise<void>;
21
+ /** Copy LLM rules template if none exists */
22
+ export declare function copyLlmRulesTemplate(repoRoot: string): Promise<boolean>;
23
+ /** Create .entire/settings.json and .entire/.gitignore */
24
+ export declare function createEntireConfig(repoRoot: string): Promise<void>;
25
+ /** Add .entire/metadata/ to repo .gitignore if not already present */
26
+ export declare function updateGitignore(repoRoot: string): Promise<boolean>;
@@ -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
+ }
@@ -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>;