@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
@@ -4,6 +4,11 @@
4
4
  import chalk from 'chalk';
5
5
  import { loadToken } from './login.js';
6
6
  const API_BASE = 'https://haystackeditor.com/api/preferences';
7
+ const AGENTIC_TOOL_LABELS = {
8
+ 'opencode': 'OpenCode (Haystack billing)',
9
+ 'claude-code': 'Claude Code (your Claude Max subscription)',
10
+ 'codex': 'Codex CLI (your ChatGPT subscription)',
11
+ };
7
12
  async function requireAuth() {
8
13
  const token = await loadToken();
9
14
  if (!token) {
@@ -131,3 +136,87 @@ export async function handleSandbox(action) {
131
136
  process.exit(1);
132
137
  }
133
138
  }
139
+ /**
140
+ * Get current agentic tool setting
141
+ */
142
+ export async function getAgenticToolStatus() {
143
+ const token = await requireAuth();
144
+ console.log(chalk.dim('\nFetching preferences...\n'));
145
+ try {
146
+ const response = await apiRequest('GET', token);
147
+ if (!response.ok) {
148
+ if (response.status === 401) {
149
+ console.error(chalk.red('Session expired. Run `haystack login` again.\n'));
150
+ process.exit(1);
151
+ }
152
+ throw new Error(`Failed to get preferences: ${response.status}`);
153
+ }
154
+ const data = await response.json();
155
+ const currentTool = data.agentic_tool || 'opencode';
156
+ console.log(chalk.bold('Agentic tool:'), chalk.cyan(AGENTIC_TOOL_LABELS[currentTool]));
157
+ console.log();
158
+ console.log(chalk.dim('Available options:'));
159
+ for (const [key, label] of Object.entries(AGENTIC_TOOL_LABELS)) {
160
+ const marker = key === currentTool ? chalk.green(' ✓ ') : ' ';
161
+ console.log(`${marker}${chalk.dim(key)} - ${label}`);
162
+ }
163
+ console.log();
164
+ console.log(chalk.dim('Change with: haystack config agentic-tool <tool>'));
165
+ console.log();
166
+ }
167
+ catch (error) {
168
+ console.error(chalk.red(`\nError: ${error.message}\n`));
169
+ process.exit(1);
170
+ }
171
+ }
172
+ /**
173
+ * Set agentic tool preference
174
+ */
175
+ export async function setAgenticTool(tool) {
176
+ const token = await requireAuth();
177
+ console.log(chalk.dim(`\nSetting agentic tool to ${tool}...`));
178
+ try {
179
+ const response = await apiRequest('PUT', token, { agentic_tool: tool });
180
+ if (!response.ok) {
181
+ if (response.status === 401) {
182
+ console.error(chalk.red('Session expired. Run `haystack login` again.\n'));
183
+ process.exit(1);
184
+ }
185
+ const error = await response.json().catch(() => ({}));
186
+ throw new Error(error.error || `Failed to update preferences: ${response.status}`);
187
+ }
188
+ console.log(chalk.green(`\nAgentic tool set to: ${AGENTIC_TOOL_LABELS[tool]}\n`));
189
+ if (tool === 'claude-code') {
190
+ console.log(chalk.dim('Note: Requires Claude Max subscription.'));
191
+ console.log(chalk.dim('Connect your account in the dashboard settings.\n'));
192
+ }
193
+ else if (tool === 'codex') {
194
+ console.log(chalk.dim('Note: Requires ChatGPT Plus, Pro, or Team subscription.'));
195
+ console.log(chalk.dim('Connect your account in the dashboard settings.\n'));
196
+ }
197
+ }
198
+ catch (error) {
199
+ console.error(chalk.red(`\nError: ${error.message}\n`));
200
+ process.exit(1);
201
+ }
202
+ }
203
+ /**
204
+ * Handle agentic-tool subcommand
205
+ */
206
+ export async function handleAgenticTool(tool) {
207
+ const validTools = ['opencode', 'claude-code', 'codex'];
208
+ if (!tool || tool === 'status') {
209
+ return getAgenticToolStatus();
210
+ }
211
+ const normalizedTool = tool.toLowerCase();
212
+ if (!validTools.includes(normalizedTool)) {
213
+ console.error(chalk.red(`\nUnknown tool: ${tool}`));
214
+ console.log('\nValid options:');
215
+ for (const [key, label] of Object.entries(AGENTIC_TOOL_LABELS)) {
216
+ console.log(` ${chalk.cyan(key)} - ${label}`);
217
+ }
218
+ console.log('\nUsage: haystack config agentic-tool [opencode|claude-code|codex|status]\n');
219
+ process.exit(1);
220
+ }
221
+ return setAgenticTool(normalizedTool);
222
+ }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * haystack hooks - Install and manage git hooks for AI agent quality checks
3
+ *
4
+ * Installs:
5
+ * - Entire CLI binary (session tracking, powered by https://entire.dev)
6
+ * - Pre-commit hook (agent detection, truncation checking, LLM rules review)
7
+ * - Git hooks for session logging (commit-msg, post-commit, pre-push, prepare-commit-msg)
8
+ */
9
+ interface HooksInstallOptions {
10
+ version?: string;
11
+ force?: boolean;
12
+ skipEntire?: boolean;
13
+ }
14
+ export declare function hooksInstall(options: HooksInstallOptions): Promise<void>;
15
+ export declare function hooksStatus(): Promise<void>;
16
+ export declare function hooksUpdate(): Promise<void>;
17
+ export {};
@@ -0,0 +1,269 @@
1
+ /**
2
+ * haystack hooks - Install and manage git hooks for AI agent quality checks
3
+ *
4
+ * Installs:
5
+ * - Entire CLI binary (session tracking, powered by https://entire.dev)
6
+ * - Pre-commit hook (agent detection, truncation checking, LLM rules review)
7
+ * - Git hooks for session logging (commit-msg, post-commit, pre-push, prepare-commit-msg)
8
+ */
9
+ import chalk from 'chalk';
10
+ import { existsSync, accessSync, constants } from 'fs';
11
+ import * as path from 'path';
12
+ import { execSync } from 'child_process';
13
+ import { findGitRoot, ENTIRE_DEFAULT_VERSION, ENTIRE_BIN_PATH, getInstalledEntireVersion, getLatestEntireVersion, downloadEntireBinary, copyHookFiles, installHookDeps, copyLlmRulesTemplate, createEntireConfig, updateGitignore, } from '../utils/hooks.js';
14
+ export async function hooksInstall(options) {
15
+ console.log(chalk.cyan('\nHaystack Hooks Setup\n'));
16
+ // Step 1: Validate git repo
17
+ const gitRoot = findGitRoot();
18
+ if (!gitRoot) {
19
+ console.error(chalk.red('Not a git repository. Run this from inside a git repo.\n'));
20
+ process.exit(1);
21
+ }
22
+ const hooksDir = path.join(gitRoot, 'hooks');
23
+ // Step 2: Check for existing installation
24
+ if (existsSync(hooksDir) && !options.force) {
25
+ console.error(chalk.yellow('hooks/ directory already exists. Use --force to overwrite.\n'));
26
+ process.exit(1);
27
+ }
28
+ if (existsSync(hooksDir) && options.force) {
29
+ console.log(chalk.yellow('Overwriting existing hooks...'));
30
+ }
31
+ // Step 3: Download Entire binary
32
+ if (!options.skipEntire) {
33
+ const version = options.version || ENTIRE_DEFAULT_VERSION;
34
+ const installedVersion = await getInstalledEntireVersion();
35
+ if (installedVersion === version) {
36
+ console.log(chalk.green(`✓ Entire CLI v${version} already installed`));
37
+ }
38
+ else {
39
+ console.log(chalk.dim(`Downloading Entire CLI v${version}...`));
40
+ try {
41
+ await downloadEntireBinary(version);
42
+ console.log(chalk.green(`✓ Entire CLI v${version} installed to ~/.haystack/bin/entire`));
43
+ }
44
+ catch (err) {
45
+ const message = err instanceof Error ? err.message : String(err);
46
+ console.error(chalk.red(`Failed to download Entire CLI: ${message}\n`));
47
+ process.exit(1);
48
+ }
49
+ }
50
+ }
51
+ else {
52
+ console.log(chalk.dim('Skipping Entire CLI download (--skip-entire)'));
53
+ }
54
+ // Step 4: Copy hook files
55
+ try {
56
+ await copyHookFiles(hooksDir);
57
+ console.log(chalk.green('✓ Hook scripts installed to hooks/'));
58
+ }
59
+ catch (err) {
60
+ const message = err instanceof Error ? err.message : String(err);
61
+ console.error(chalk.red(`Failed to copy hook files: ${message}\n`));
62
+ process.exit(1);
63
+ }
64
+ // Step 5: Install hook dependencies
65
+ console.log(chalk.dim('Installing hook dependencies (tree-sitter)...'));
66
+ try {
67
+ await installHookDeps(hooksDir);
68
+ console.log(chalk.green('✓ Hook dependencies installed'));
69
+ }
70
+ catch {
71
+ console.log(chalk.yellow('⚠ Failed to install hook dependencies. Run `npm install` in hooks/ manually.'));
72
+ }
73
+ // Step 6: Configure git hooks path
74
+ try {
75
+ execSync('git config core.hooksPath hooks', { cwd: gitRoot, stdio: 'pipe' });
76
+ console.log(chalk.green('✓ Git configured to use hooks/ directory'));
77
+ }
78
+ catch (err) {
79
+ const message = err instanceof Error ? err.message : String(err);
80
+ console.error(chalk.red(`Failed to set git hooks path: ${message}\n`));
81
+ process.exit(1);
82
+ }
83
+ // Step 7: Create .entire/ configuration
84
+ try {
85
+ await createEntireConfig(gitRoot);
86
+ console.log(chalk.green('✓ Created .entire/settings.json'));
87
+ }
88
+ catch {
89
+ console.log(chalk.yellow('⚠ Failed to create .entire/ configuration'));
90
+ }
91
+ // Step 8: Create LLM_RULES.md if needed
92
+ try {
93
+ const created = await copyLlmRulesTemplate(gitRoot);
94
+ if (created) {
95
+ console.log(chalk.green('✓ Created LLM_RULES.md (customize this for your project)'));
96
+ }
97
+ else {
98
+ console.log(chalk.dim(' LLM_RULES.md already exists'));
99
+ }
100
+ }
101
+ catch {
102
+ console.log(chalk.yellow('⚠ Failed to create LLM_RULES.md'));
103
+ }
104
+ // Step 9: Update .gitignore
105
+ try {
106
+ const updated = await updateGitignore(gitRoot);
107
+ if (updated) {
108
+ console.log(chalk.green('✓ Updated .gitignore'));
109
+ }
110
+ }
111
+ catch {
112
+ console.log(chalk.yellow('⚠ Failed to update .gitignore'));
113
+ }
114
+ // Summary
115
+ console.log(chalk.green('\n✓ Haystack hooks installed!\n'));
116
+ console.log(chalk.dim('Hooks installed:'));
117
+ console.log(chalk.dim(' pre-commit Agent detection, truncation check, LLM rules review'));
118
+ console.log(chalk.dim(' commit-msg Session tracking (powered by Entire)'));
119
+ console.log(chalk.dim(' post-commit Session condensing (powered by Entire)'));
120
+ console.log(chalk.dim(' pre-push Session log push (powered by Entire)'));
121
+ console.log(chalk.dim(' prepare-commit-msg Commit preparation (powered by Entire)'));
122
+ console.log('');
123
+ console.log(chalk.dim('Files to commit:'));
124
+ console.log(chalk.dim(' hooks/ Hook scripts and TypeScript modules'));
125
+ console.log(chalk.dim(' .entire/settings.json'));
126
+ console.log(chalk.dim(' .entire/.gitignore'));
127
+ console.log(chalk.dim(' LLM_RULES.md Customize this for your project'));
128
+ console.log('');
129
+ }
130
+ // ============================================================================
131
+ // haystack hooks status
132
+ // ============================================================================
133
+ export async function hooksStatus() {
134
+ const gitRoot = findGitRoot();
135
+ if (!gitRoot) {
136
+ console.error(chalk.red('Not a git repository.\n'));
137
+ process.exit(1);
138
+ }
139
+ console.log(chalk.cyan('\nHaystack Hooks Status\n'));
140
+ const hooksDir = path.join(gitRoot, 'hooks');
141
+ // Check hooks directory
142
+ if (!existsSync(hooksDir)) {
143
+ console.log(chalk.red('✗ hooks/ directory not found'));
144
+ console.log(chalk.dim(' Run: haystack hooks install\n'));
145
+ return;
146
+ }
147
+ console.log(chalk.green('✓ hooks/ directory exists'));
148
+ // Check each hook script
149
+ const requiredHooks = [
150
+ 'pre-commit',
151
+ 'commit-msg',
152
+ 'post-commit',
153
+ 'pre-push',
154
+ 'prepare-commit-msg',
155
+ ];
156
+ for (const hook of requiredHooks) {
157
+ const hookPath = path.join(hooksDir, hook);
158
+ if (existsSync(hookPath)) {
159
+ try {
160
+ accessSync(hookPath, constants.X_OK);
161
+ console.log(chalk.green(`✓ ${hook}`));
162
+ }
163
+ catch {
164
+ console.log(chalk.yellow(`⚠ ${hook} (not executable)`));
165
+ }
166
+ }
167
+ else {
168
+ console.log(chalk.red(`✗ ${hook} missing`));
169
+ }
170
+ }
171
+ // Check TypeScript modules
172
+ const agentContextDir = path.join(hooksDir, 'agent-context');
173
+ const truncationDir = path.join(hooksDir, 'truncation-checker');
174
+ console.log(existsSync(agentContextDir)
175
+ ? chalk.green('✓ agent-context/ module')
176
+ : chalk.red('✗ agent-context/ missing'));
177
+ console.log(existsSync(truncationDir)
178
+ ? chalk.green('✓ truncation-checker/ module')
179
+ : chalk.red('✗ truncation-checker/ missing'));
180
+ // Check hook dependencies
181
+ const nodeModules = path.join(hooksDir, 'node_modules');
182
+ console.log(existsSync(nodeModules)
183
+ ? chalk.green('✓ Hook dependencies installed')
184
+ : chalk.yellow('⚠ Hook dependencies not installed (run npm install in hooks/)'));
185
+ // Check git core.hooksPath
186
+ try {
187
+ const hooksPath = execSync('git config core.hooksPath', {
188
+ cwd: gitRoot,
189
+ encoding: 'utf-8',
190
+ stdio: ['pipe', 'pipe', 'pipe'],
191
+ }).trim();
192
+ if (hooksPath === 'hooks') {
193
+ console.log(chalk.green('✓ Git core.hooksPath = hooks'));
194
+ }
195
+ else {
196
+ console.log(chalk.yellow(`⚠ Git core.hooksPath = ${hooksPath} (expected: hooks)`));
197
+ }
198
+ }
199
+ catch {
200
+ console.log(chalk.red('✗ Git core.hooksPath not set'));
201
+ }
202
+ // Check Entire binary
203
+ const installedVersion = await getInstalledEntireVersion();
204
+ if (installedVersion) {
205
+ console.log(chalk.green(`✓ Entire CLI v${installedVersion}`));
206
+ }
207
+ else if (existsSync(ENTIRE_BIN_PATH)) {
208
+ console.log(chalk.yellow('⚠ Entire CLI binary exists but failed to run'));
209
+ }
210
+ else {
211
+ console.log(chalk.yellow('⚠ Entire CLI not installed (~/.haystack/bin/entire)'));
212
+ }
213
+ // Check .entire/settings.json
214
+ const settingsPath = path.join(gitRoot, '.entire', 'settings.json');
215
+ if (existsSync(settingsPath)) {
216
+ console.log(chalk.green('✓ .entire/settings.json'));
217
+ }
218
+ else {
219
+ console.log(chalk.yellow('⚠ .entire/settings.json not found'));
220
+ }
221
+ // Check LLM_RULES.md
222
+ const rulesPath = path.join(gitRoot, 'LLM_RULES.md');
223
+ if (existsSync(rulesPath)) {
224
+ console.log(chalk.green('✓ LLM_RULES.md'));
225
+ }
226
+ else {
227
+ console.log(chalk.yellow('⚠ LLM_RULES.md not found'));
228
+ }
229
+ console.log('');
230
+ }
231
+ // ============================================================================
232
+ // haystack hooks update
233
+ // ============================================================================
234
+ export async function hooksUpdate() {
235
+ console.log(chalk.cyan('\nUpdating Entire CLI...\n'));
236
+ // Check current version
237
+ const currentVersion = await getInstalledEntireVersion();
238
+ if (currentVersion) {
239
+ console.log(chalk.dim(`Current version: v${currentVersion}`));
240
+ }
241
+ else {
242
+ console.log(chalk.dim('Entire CLI not currently installed.'));
243
+ }
244
+ // Fetch latest version
245
+ let latestVersion;
246
+ try {
247
+ latestVersion = await getLatestEntireVersion();
248
+ }
249
+ catch (err) {
250
+ const message = err instanceof Error ? err.message : String(err);
251
+ console.error(chalk.red(`Failed to check for updates: ${message}\n`));
252
+ process.exit(1);
253
+ }
254
+ console.log(chalk.dim(`Latest version: v${latestVersion}`));
255
+ if (currentVersion === latestVersion) {
256
+ console.log(chalk.green('\n✓ Already up to date.\n'));
257
+ return;
258
+ }
259
+ // Download and install
260
+ try {
261
+ await downloadEntireBinary(latestVersion);
262
+ console.log(chalk.green(`\n✓ Updated Entire CLI to v${latestVersion}\n`));
263
+ }
264
+ catch (err) {
265
+ const message = err instanceof Error ? err.message : String(err);
266
+ console.error(chalk.red(`Failed to update: ${message}\n`));
267
+ process.exit(1);
268
+ }
269
+ }
@@ -4,7 +4,7 @@
4
4
  * Creates .haystack.json with auto-detected settings.
5
5
  */
6
6
  interface InitOptions {
7
- yes?: boolean;
7
+ force?: boolean;
8
8
  }
9
9
  export declare function initCommand(options: InitOptions): Promise<void>;
10
10
  export {};