@haystackeditor/cli 0.7.2 → 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 (36) 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/init.d.ts +1 -1
  25. package/dist/commands/init.js +20 -239
  26. package/dist/commands/secrets.d.ts +15 -0
  27. package/dist/commands/secrets.js +83 -0
  28. package/dist/commands/skills.d.ts +8 -0
  29. package/dist/commands/skills.js +215 -0
  30. package/dist/index.js +107 -7
  31. package/dist/types.d.ts +32 -8
  32. package/dist/utils/hooks.d.ts +26 -0
  33. package/dist/utils/hooks.js +226 -0
  34. package/dist/utils/skill.d.ts +1 -1
  35. package/dist/utils/skill.js +481 -13
  36. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -15,8 +15,10 @@ import { Command } from 'commander';
15
15
  import { statusCommand } from './commands/status.js';
16
16
  import { initCommand } from './commands/init.js';
17
17
  import { loginCommand, logoutCommand } from './commands/login.js';
18
- import { listSecrets, setSecret, deleteSecret } from './commands/secrets.js';
19
- import { handleSandbox } from './commands/config.js';
18
+ import { listSecrets, setSecret, getSecret, getSecretsForRepo, deleteSecret } from './commands/secrets.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')
@@ -25,14 +27,15 @@ program
25
27
  program
26
28
  .command('init')
27
29
  .description('Create .haystack.json configuration')
28
- .option('-y, --yes', 'Use auto-detected defaults without prompting')
30
+ .option('-f, --force', 'Overwrite existing .haystack.json')
29
31
  .addHelpText('after', `
30
- This creates a .haystack.json file that configures:
31
- How to start your dev server
32
- Verification commands (build, lint, typecheck)
32
+ This creates a .haystack.json file with auto-detected settings:
33
+ Dev server command and port
34
+ Services (for monorepos)
33
35
  • Auth bypass for sandbox environments
34
36
 
35
- Once set up, AI agents can automatically spin up and test your app.
37
+ After running, use /setup-haystack in Claude Code (or give your AI agent
38
+ .agents/skills/setup-haystack.md) to add verification flows.
36
39
  `)
37
40
  .action(initCommand);
38
41
  program
@@ -63,6 +66,20 @@ secrets
63
66
  .action((key, value, options) => {
64
67
  setSecret(key, value, options);
65
68
  });
69
+ secrets
70
+ .command('get <key>')
71
+ .description('Get a secret value')
72
+ .option('-q, --quiet', 'Output only the value (for piping)')
73
+ .action((key, options) => {
74
+ getSecret(key, options);
75
+ });
76
+ secrets
77
+ .command('get-for-repo <keys...>')
78
+ .description('Get multiple secrets needed by a repo')
79
+ .option('--json', 'Output as JSON')
80
+ .action((keys, options) => {
81
+ getSecretsForRepo(keys, options);
82
+ });
66
83
  secrets
67
84
  .command('delete <key>')
68
85
  .description('Delete a secret')
@@ -86,6 +103,89 @@ Examples:
86
103
  haystack config sandbox off # Disable sandbox mode
87
104
  `)
88
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);
89
189
  // Show help if no command provided
90
190
  if (process.argv.length === 2) {
91
191
  program.help();
package/dist/types.d.ts CHANGED
@@ -34,19 +34,43 @@ export interface HaystackConfig {
34
34
  */
35
35
  network?: NetworkConfig;
36
36
  /**
37
- * Secrets for fixture fetching and other operations.
38
- * These are injected as environment variables in the sandbox.
39
- * Supports tokens for staging APIs, S3 credentials, etc.
37
+ * Secrets required by this project.
38
+ * Values are stored securely on the Haystack platform via `haystack secrets set`.
39
+ * At runtime, secrets are fetched and injected as environment variables.
40
40
  *
41
- * ⚠️ SECURITY: Never commit real secrets to git!
42
- * Use environment variable references: $ENV_VAR or ${ENV_VAR}
41
+ * This field declares WHAT secrets are needed, not their values.
42
+ * Use `haystack secrets set KEY VALUE` to store the actual values.
43
43
  *
44
44
  * @example
45
45
  * secrets:
46
- * STAGING_TOKEN: "$STAGING_API_TOKEN" # Read from local env
47
- * AWS_ACCESS_KEY_ID: "${AWS_ACCESS_KEY_ID}"
46
+ * OPENAI_API_KEY:
47
+ * description: "OpenAI API key for LLM calls"
48
+ * required: true
49
+ * STAGING_TOKEN:
50
+ * description: "Token for staging API access"
51
+ * required: false
48
52
  */
49
- secrets?: Record<string, string>;
53
+ secrets?: Record<string, SecretDeclaration>;
54
+ }
55
+ /**
56
+ * Secret declaration - describes what a secret is for, not its value
57
+ */
58
+ export interface SecretDeclaration {
59
+ /** Human-readable description of what this secret is used for */
60
+ description?: string;
61
+ /**
62
+ * Whether this secret is required for verification to run.
63
+ * If true and the secret is not set, verification will fail with an error.
64
+ * If false, verification will continue with a warning.
65
+ * @default false
66
+ */
67
+ required?: boolean;
68
+ /**
69
+ * Services that use this secret.
70
+ * If specified, the secret is only injected into these services' environments.
71
+ * If omitted, the secret is available to all services.
72
+ */
73
+ services?: string[];
50
74
  }
51
75
  /**
52
76
  * Dev server configuration (simple mode)
@@ -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>;