slackhive 0.1.32 → 0.1.34

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 (2) hide show
  1. package/dist/commands/init.js +173 -166
  2. package/package.json +1 -1
@@ -12,16 +12,11 @@ exports.init = init;
12
12
  const child_process_1 = require("child_process");
13
13
  const fs_1 = require("fs");
14
14
  const path_1 = require("path");
15
- const os_1 = require("os");
16
15
  const chalk_1 = __importDefault(require("chalk"));
17
16
  const ora_1 = __importDefault(require("ora"));
18
17
  const prompts_1 = __importDefault(require("prompts"));
19
18
  const REPO_URL = 'https://github.com/pelago-labs/slackhive.git';
20
- /**
21
- * Simple Claude installation detection for Linux.
22
- * Uses the binary directly without module path detection.
23
- */
24
- function detectClaudeInstallationLinux() {
19
+ function detectClaudeBin() {
25
20
  let claudeBin;
26
21
  try {
27
22
  claudeBin = (0, child_process_1.execSync)('which claude', { encoding: 'utf-8' }).trim();
@@ -29,82 +24,53 @@ function detectClaudeInstallationLinux() {
29
24
  catch {
30
25
  throw new Error('Claude Code not found. Please install Claude Code first.');
31
26
  }
32
- if (!claudeBin) {
27
+ if (!claudeBin)
33
28
  throw new Error('Claude Code not found. Please install Claude Code first.');
34
- }
35
- // For Linux, we don't need to detect the module path - just use the binary directly
36
- // The Docker container will use the mounted binary
37
- return { claudeBin, claudeModulePath: '' };
29
+ return claudeBin;
38
30
  }
39
31
  /**
40
- * Advanced Claude installation detection for macOS.
41
- * Finds both the binary and module directory automatically with symlink resolution.
32
+ * Parses a JSON credential blob and extracts OAuth tokens.
42
33
  */
43
- function detectClaudeInstallationMacOS() {
44
- // Find Claude binary
45
- let claudeBin;
34
+ function parseOAuthFromJson(json) {
46
35
  try {
47
- claudeBin = (0, child_process_1.execSync)('which claude', { encoding: 'utf-8' }).trim();
48
- }
49
- catch {
50
- throw new Error('Claude Code not found. Please install Claude Code first.');
51
- }
52
- if (!claudeBin) {
53
- throw new Error('Claude Code not found. Please install Claude Code first.');
54
- }
55
- let claudeModulePath;
56
- // Check if it's a symlink and resolve it
57
- if ((0, fs_1.existsSync)(claudeBin)) {
58
- try {
59
- const target = (0, fs_1.readlinkSync)(claudeBin);
60
- // Handle relative paths
61
- const resolvedTarget = target.startsWith('/') ? target : (0, path_1.join)((0, path_1.dirname)(claudeBin), target);
62
- // Extract module directory from cli.js path
63
- claudeModulePath = (0, path_1.dirname)(resolvedTarget);
36
+ const parsed = JSON.parse(json);
37
+ const oauth = parsed?.claudeAiOauth;
38
+ if (oauth?.accessToken && oauth?.refreshToken) {
39
+ return { accessToken: oauth.accessToken, refreshToken: oauth.refreshToken };
64
40
  }
65
- catch {
66
- // Not a symlink, try common installation paths
67
- const possiblePaths = [
68
- '/usr/local/lib/node_modules/@anthropic-ai/claude-code',
69
- '/opt/homebrew/lib/node_modules/@anthropic-ai/claude-code',
70
- (0, path_1.join)(process.env.HOME || '~', '.local/lib/node_modules/@anthropic-ai/claude-code'),
71
- '/usr/lib/node_modules/@anthropic-ai/claude-code',
72
- ];
73
- claudeModulePath = '';
74
- for (const path of possiblePaths) {
75
- if ((0, fs_1.existsSync)(path) && (0, fs_1.existsSync)((0, path_1.join)(path, 'cli.js'))) {
76
- claudeModulePath = path;
77
- break;
78
- }
79
- }
80
- }
81
- }
82
- else {
83
- throw new Error(`Claude binary not found at: ${claudeBin}`);
84
- }
85
- if (!claudeModulePath || !(0, fs_1.existsSync)((0, path_1.join)(claudeModulePath, 'cli.js'))) {
86
- throw new Error('Could not find Claude Code module directory.');
87
- }
88
- // Verify required files exist
89
- if (!(0, fs_1.existsSync)((0, path_1.join)(claudeModulePath, 'yoga.wasm'))) {
90
- throw new Error(`Warning: yoga.wasm not found in ${claudeModulePath}`);
91
41
  }
92
- return { claudeBin, claudeModulePath };
42
+ catch { /* invalid json */ }
43
+ return null;
93
44
  }
94
45
  /**
95
- * Cross-platform Claude installation detection.
96
- * Uses different strategies based on the operating system.
46
+ * Extracts the OAuth credentials from the OS credential store.
47
+ * Tries macOS Keychain, then Linux secret-tool (GNOME Keyring).
48
+ * Returns access + refresh tokens, or null if not found.
97
49
  */
98
- function detectClaudeInstallation() {
99
- const os = (0, os_1.platform)();
100
- if (os === 'darwin') {
101
- // macOS - use advanced detection with symlink resolution
102
- return detectClaudeInstallationMacOS();
50
+ function extractOAuthCredentials() {
51
+ // macOS: read from Keychain
52
+ try {
53
+ const creds = (0, child_process_1.execSync)('security find-generic-password -s "Claude Code-credentials" -w', {
54
+ encoding: 'utf-8',
55
+ stdio: ['pipe', 'pipe', 'ignore'],
56
+ }).trim();
57
+ const result = parseOAuthFromJson(creds);
58
+ if (result)
59
+ return result;
103
60
  }
104
- else {
105
- // Linux and other Unix-like systems - use simple detection
106
- return detectClaudeInstallationLinux();
61
+ catch { /* not macOS or not found */ }
62
+ // Linux: try secret-tool (GNOME Keyring)
63
+ try {
64
+ const creds = (0, child_process_1.execSync)('secret-tool lookup service "Claude Code-credentials"', {
65
+ encoding: 'utf-8',
66
+ stdio: ['pipe', 'pipe', 'ignore'],
67
+ }).trim();
68
+ const result = parseOAuthFromJson(creds);
69
+ if (result)
70
+ return result;
107
71
  }
72
+ catch { /* not available */ }
73
+ return null;
108
74
  }
109
75
  /**
110
76
  * Runs `slackhive init` — interactive setup wizard.
@@ -116,10 +82,10 @@ async function init(opts) {
116
82
  const O = chalk_1.default.hex('#D97757').bold;
117
83
  const W = chalk_1.default.hex('#EBE6E0').bold;
118
84
  console.log('');
119
- console.log(' ' + W(' │ │ '));
120
- console.log(' ' + W('──┼───┼──'));
121
- console.log(' ' + O('>') + W(' │──') + O('█') + W(''));
122
- console.log(' ' + W(' │ │ '));
85
+ console.log(' ' + W('│ │'));
86
+ console.log(' ' + W('───┼───┼───'));
87
+ console.log(' ' + O('>') + W(' ──┼──') + O('█') + W('┼──'));
88
+ console.log(' ' + W('│ │'));
123
89
  console.log('');
124
90
  console.log(chalk_1.default.bold(' SlackHive') + chalk_1.default.gray(' — AI agent teams on Slack'));
125
91
  console.log('');
@@ -180,6 +146,7 @@ async function init(opts) {
180
146
  process.exit(1);
181
147
  }
182
148
  const questions = [];
149
+ let oauthCreds = null;
183
150
  if (authMode.mode === 'apikey') {
184
151
  questions.push({
185
152
  type: 'text',
@@ -189,61 +156,33 @@ async function init(opts) {
189
156
  });
190
157
  }
191
158
  else {
192
- // Claude subscription mode — detect installation automatically
159
+ // Claude subscription mode — extract OAuth token
193
160
  const claudeDir = (0, path_1.join)(process.env.HOME || '~', '.claude');
194
161
  if (!(0, fs_1.existsSync)(claudeDir)) {
195
162
  console.log(chalk_1.default.yellow('\n warning: ~/.claude not found. Run `claude login` first, then re-run `slackhive init`.'));
196
163
  process.exit(1);
197
164
  }
198
165
  console.log(chalk_1.default.green(' ✓') + ' Found ~/.claude credentials');
199
- // Auto-detect Claude installation paths
200
- const spinner = (0, ora_1.default)(' Detecting Claude installation...').start();
201
- try {
202
- const { claudeBin, claudeModulePath } = detectClaudeInstallation();
203
- spinner.succeed(`Found Claude at ${claudeBin}`);
204
- // Set these for the .env file
205
- questions.push({
206
- type: 'text',
207
- name: 'claudeBin',
208
- message: 'Path to claude binary',
209
- initial: claudeBin,
210
- });
211
- // Only ask for module path on macOS (Linux uses binary directly)
212
- if (claudeModulePath) {
213
- questions.push({
214
- type: 'text',
215
- name: 'claudeModulePath',
216
- message: 'Path to claude module',
217
- initial: claudeModulePath,
218
- });
219
- }
166
+ const spinner = (0, ora_1.default)(' Extracting OAuth credentials...').start();
167
+ oauthCreds = extractOAuthCredentials();
168
+ if (oauthCreds) {
169
+ spinner.succeed('OAuth credentials extracted');
220
170
  }
221
- catch (error) {
222
- spinner.fail('Could not detect Claude installation');
223
- console.log(chalk_1.default.yellow(` ${error}`));
224
- console.log(chalk_1.default.gray(' Please provide paths manually:'));
225
- let claudeBinDefault = '/usr/local/bin/claude';
226
- try {
227
- const found = (0, child_process_1.execSync)('which claude', { encoding: 'utf-8' }).trim();
228
- if (found)
229
- claudeBinDefault = found;
230
- }
231
- catch { /* use default */ }
232
- questions.push({
233
- type: 'text',
234
- name: 'claudeBin',
235
- message: 'Path to claude binary',
236
- initial: claudeBinDefault,
237
- });
238
- // Only ask for module path on macOS
239
- if ((0, os_1.platform)() === 'darwin') {
240
- questions.push({
241
- type: 'text',
242
- name: 'claudeModulePath',
243
- message: 'Path to claude module directory',
244
- initial: '/usr/local/lib/node_modules/@anthropic-ai/claude-code',
245
- });
171
+ else {
172
+ spinner.warn('Could not auto-extract credentials from keychain');
173
+ console.log(chalk_1.default.gray(' On Linux/headless servers, paste your OAuth token manually.'));
174
+ console.log(chalk_1.default.gray(' Get it from a machine where you ran `claude login`:'));
175
+ console.log(chalk_1.default.gray(' security find-generic-password -s "Claude Code-credentials" -w'));
176
+ console.log('');
177
+ const tokenResponse = await (0, prompts_1.default)([
178
+ { type: 'password', name: 'accessToken', message: 'OAuth access token (sk-ant-oat01-...)', validate: (v) => v.startsWith('sk-ant-oat') ? true : 'Must start with sk-ant-oat' },
179
+ { type: 'password', name: 'refreshToken', message: 'OAuth refresh token (sk-ant-ort01-...)', validate: (v) => v.startsWith('sk-ant-ort') ? true : 'Must start with sk-ant-ort' },
180
+ ]);
181
+ if (!tokenResponse.accessToken || !tokenResponse.refreshToken) {
182
+ console.log(chalk_1.default.red('\n Setup cancelled. Use API Key mode instead on headless servers.'));
183
+ process.exit(1);
246
184
  }
185
+ oauthCreds = { accessToken: tokenResponse.accessToken, refreshToken: tokenResponse.refreshToken };
247
186
  }
248
187
  }
249
188
  questions.push({ type: 'text', name: 'adminUsername', message: 'Admin username', initial: 'admin' }, { type: 'password', name: 'adminPassword', message: 'Admin password', validate: (v) => v.length >= 6 ? true : 'At least 6 characters' }, { type: 'text', name: 'postgresPassword', message: 'Postgres password', initial: randomSecret().slice(0, 16) }, { type: 'text', name: 'redisPassword', message: 'Redis password', initial: randomSecret().slice(0, 16) });
@@ -257,11 +196,9 @@ async function init(opts) {
257
196
  envContent += `ANTHROPIC_API_KEY=${response.anthropicKey}\n`;
258
197
  }
259
198
  else {
260
- envContent += `# Claude Code subscription — credentials from ~/.claude\n`;
261
- envContent += `CLAUDE_BIN=${response.claudeBin}\n`;
262
- if (response.claudeModulePath) {
263
- envContent += `CLAUDE_MODULE_PATH=${response.claudeModulePath}\n`;
264
- }
199
+ envContent += `# Claude Code subscription — OAuth credentials from keychain\n`;
200
+ envContent += `CLAUDE_CODE_OAUTH_TOKEN=${oauthCreds.accessToken}\n`;
201
+ envContent += `CLAUDE_CODE_OAUTH_REFRESH_TOKEN=${oauthCreds.refreshToken}\n`;
265
202
  }
266
203
  envContent += `\nPOSTGRES_DB=slackhive\n`;
267
204
  envContent += `POSTGRES_USER=slackhive\n`;
@@ -330,31 +267,42 @@ async function init(opts) {
330
267
  }
331
268
  }
332
269
  catch { /* non-fatal */ }
333
- await runDockerBuild(dir, opts.dir);
334
- // If containers didn't come up during build, retry once silently
270
+ // Remove stale Postgres volume if it exists — prevents password mismatch
271
+ // when re-running init with new credentials
335
272
  try {
336
- (0, child_process_1.execSync)('docker compose up -d', { cwd: dir, stdio: 'ignore' });
273
+ (0, child_process_1.execSync)('docker compose down -v', { cwd: dir, stdio: 'ignore' });
337
274
  }
338
- catch { /* non-fatal */ }
339
- // Wait for web UI — up to 3 minutes
340
- const webSpinner = (0, ora_1.default)(' Waiting for web UI to be ready...').start();
341
- let ready = false;
342
- for (let i = 0; i < 60; i++) {
275
+ catch { /* non-fatal — may not exist yet */ }
276
+ const buildOk = await runDockerBuild(dir, opts.dir);
277
+ if (buildOk) {
278
+ // If containers didn't come up during build, retry once silently
343
279
  try {
344
- (0, child_process_1.execSync)('curl -sf http://localhost:3001/login', { stdio: 'ignore' });
345
- ready = true;
346
- break;
280
+ (0, child_process_1.execSync)('docker compose up -d', { cwd: dir, stdio: 'ignore' });
347
281
  }
348
- catch {
349
- await sleep(3000);
282
+ catch { /* non-fatal */ }
283
+ // Wait for web UI — up to 3 minutes
284
+ const webSpinner = (0, ora_1.default)(' Waiting for web UI to be ready...').start();
285
+ let ready = false;
286
+ for (let i = 0; i < 60; i++) {
287
+ try {
288
+ (0, child_process_1.execSync)('curl -sf http://localhost:3001/login', { stdio: 'ignore' });
289
+ ready = true;
290
+ break;
291
+ }
292
+ catch {
293
+ await sleep(3000);
294
+ }
295
+ }
296
+ if (ready) {
297
+ webSpinner.succeed('Web UI is ready');
298
+ }
299
+ else {
300
+ webReady = false;
301
+ webSpinner.stopAndPersist({ symbol: ' ' });
350
302
  }
351
- }
352
- if (ready) {
353
- webSpinner.succeed('Web UI is ready');
354
303
  }
355
304
  else {
356
305
  webReady = false;
357
- webSpinner.stopAndPersist({ symbol: ' ' });
358
306
  }
359
307
  }
360
308
  // ── Done ──────────────────────────────────────────────────────────────────
@@ -388,35 +336,78 @@ async function init(opts) {
388
336
  */
389
337
  function runDockerBuild(cwd, displayDir) {
390
338
  return new Promise((resolve) => {
391
- const proc = (0, child_process_1.spawn)('docker', ['compose', 'up', '-d', '--build', '--progress', 'plain'], {
339
+ const proc = (0, child_process_1.spawn)('docker', ['compose', '--progress', 'plain', 'up', '-d', '--build'], {
392
340
  cwd,
393
341
  env: { ...process.env },
394
342
  });
395
343
  const startTime = Date.now();
396
344
  const frames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
397
345
  let frameIdx = 0;
398
- let currentStep = 'Building images';
399
- const spinnerInterval = setInterval(() => {
346
+ // Phased progress tracking
347
+ const phases = [
348
+ { name: 'Installing system packages', weight: 10, pattern: /apk add|fetch.*APKINDEX/i },
349
+ { name: 'Installing npm dependencies', weight: 30, pattern: /npm ci|npm install|added \d+ packages/i },
350
+ { name: 'Compiling TypeScript', weight: 10, pattern: /tsc|--skipLibCheck/i },
351
+ { name: 'Building web app', weight: 30, pattern: /next build|next\.config/i },
352
+ { name: 'Creating containers', weight: 10, pattern: /exporting to image|naming to|exporting layers/i },
353
+ { name: 'Starting services', weight: 10, pattern: /Container .*(Starting|Started|Healthy|Created)/i },
354
+ ];
355
+ let currentPhase = 0;
356
+ let phaseStartTime = Date.now();
357
+ function getProgress() {
358
+ let pct = 0;
359
+ for (let i = 0; i < currentPhase; i++)
360
+ pct += phases[i].weight;
361
+ // Add partial progress within current phase
362
+ if (currentPhase < phases.length) {
363
+ const elapsed = (Date.now() - phaseStartTime) / 1000;
364
+ const estimatedDuration = currentPhase === 1 ? 90 : currentPhase === 3 ? 100 : 30;
365
+ const partial = Math.min(0.9, elapsed / estimatedDuration);
366
+ pct += phases[currentPhase].weight * partial;
367
+ }
368
+ return Math.min(99, Math.round(pct));
369
+ }
370
+ function renderBar() {
371
+ const pct = getProgress();
372
+ const cols = process.stdout.columns || 80;
373
+ const barWidth = Math.min(20, Math.max(10, cols - 55));
374
+ const filled = Math.round((pct / 100) * barWidth);
375
+ const empty = barWidth - filled;
376
+ const bar = chalk_1.default.hex('#D97757')('█'.repeat(filled)) + chalk_1.default.gray('░'.repeat(empty));
400
377
  const elapsed = Math.floor((Date.now() - startTime) / 1000);
378
+ const phaseName = currentPhase < phases.length ? phases[currentPhase].name : 'Finishing';
401
379
  const frame = frames[frameIdx++ % frames.length];
402
- const cols = process.stdout.columns || 80;
403
- const line = ` ${chalk_1.default.hex('#D97757')(frame)} ${currentStep} ${chalk_1.default.gray(elapsed + 's')}`;
404
- process.stdout.write(`\r\x1b[K${line.slice(0, cols - 1)}`);
380
+ const pctStr = String(pct).padStart(2);
381
+ return ` ${chalk_1.default.hex('#D97757')(frame)} ${bar} ${chalk_1.default.bold(pctStr + '%')} ${phaseName} ${chalk_1.default.gray('(' + elapsed + 's)')}`;
382
+ }
383
+ const spinnerInterval = setInterval(() => {
384
+ process.stdout.write(`\r\x1b[K${renderBar()}`);
405
385
  }, 80);
406
386
  let buf = '';
407
387
  const errorLines = [];
408
388
  const onData = (chunk) => {
409
- buf += chunk.toString().replace(/\x1b\[[0-9;]*[a-zA-Z]/g, ''); // strip ANSI
389
+ buf += chunk.toString().replace(/\x1b\[[0-9;]*[a-zA-Z]/g, '');
410
390
  const lines = buf.split('\n');
411
391
  buf = lines.pop() ?? '';
412
392
  for (const raw of lines) {
413
393
  const line = raw.trim();
414
394
  if (!line)
415
395
  continue;
416
- // Extract meaningful step: "#5 [runner 3/6] RUN npm ci"
417
- const stepMatch = /^#\d+\s+\[([^\]]+)\]\s+(.+)/.exec(line);
418
- if (stepMatch) {
419
- currentStep = `${chalk_1.default.dim(stepMatch[1])} ${stepMatch[2].slice(0, 45)}`;
396
+ // Check if we've entered a new phase
397
+ for (let i = currentPhase + 1; i < phases.length; i++) {
398
+ if (phases[i].pattern.test(line)) {
399
+ // Print completed phases
400
+ const elapsed = Math.floor((Date.now() - phaseStartTime) / 1000);
401
+ process.stdout.write('\r\x1b[K');
402
+ console.log(' ' + chalk_1.default.green('✓') + ' ' + phases[currentPhase].name + chalk_1.default.gray(` (${elapsed}s)`));
403
+ // Skip intermediate phases
404
+ for (let j = currentPhase + 1; j < i; j++) {
405
+ console.log(' ' + chalk_1.default.green('✓') + ' ' + phases[j].name + chalk_1.default.gray(' (cached)'));
406
+ }
407
+ currentPhase = i;
408
+ phaseStartTime = Date.now();
409
+ break;
410
+ }
420
411
  }
421
412
  if (/error/i.test(line))
422
413
  errorLines.push(line);
@@ -428,33 +419,49 @@ function runDockerBuild(cwd, displayDir) {
428
419
  clearInterval(spinnerInterval);
429
420
  process.stdout.write('\r\x1b[K');
430
421
  if (code === 0) {
431
- console.log(' ' + chalk_1.default.green('✓') + ' All services started');
432
- resolve();
422
+ // Print any remaining phases as done
423
+ const elapsed = Math.floor((Date.now() - phaseStartTime) / 1000);
424
+ if (currentPhase < phases.length) {
425
+ console.log(' ' + chalk_1.default.green('✓') + ' ' + phases[currentPhase].name + chalk_1.default.gray(` (${elapsed}s)`));
426
+ for (let j = currentPhase + 1; j < phases.length; j++) {
427
+ console.log(' ' + chalk_1.default.green('✓') + ' ' + phases[j].name);
428
+ }
429
+ }
430
+ console.log('');
431
+ console.log(' ' + chalk_1.default.green('✓') + chalk_1.default.bold(' All services started'));
432
+ resolve(true);
433
433
  return;
434
434
  }
435
+ console.log(' ' + chalk_1.default.red('✗') + ' Failed to start services');
436
+ console.log('');
435
437
  const allErrors = errorLines.join('\n').toLowerCase();
436
438
  if (allErrors.includes('no space left') || allErrors.includes('disk full')) {
437
- console.log(chalk_1.default.yellow(' Docker is out of disk space.'));
438
- console.log(chalk_1.default.gray(' Fix: docker system prune -a'));
439
+ console.log(chalk_1.default.yellow(' Cause: Docker is out of disk space.'));
440
+ console.log(chalk_1.default.gray(' Fix: docker system prune -a'));
439
441
  }
440
442
  else if (allErrors.includes('port is already allocated') || allErrors.includes('address already in use')) {
441
443
  const portMatch = /bind for .+:(\d+)/.exec(allErrors);
442
444
  const port = portMatch ? portMatch[1] : 'a required port';
443
- console.log(chalk_1.default.yellow(` Port ${port} is already in use.`));
444
- console.log(chalk_1.default.gray(` Fix: stop the process on port ${port} and retry`));
445
+ console.log(chalk_1.default.yellow(` Cause: Port ${port} is already in use.`));
446
+ console.log(chalk_1.default.gray(` Fix: stop the process on port ${port} and retry`));
445
447
  }
446
448
  else if (allErrors.includes('permission denied') || allErrors.includes('unauthorized')) {
447
- console.log(chalk_1.default.yellow(' Docker permission denied — is Docker Desktop running?'));
449
+ console.log(chalk_1.default.yellow(' Cause: Docker permission denied — is Docker Desktop running?'));
448
450
  }
449
451
  else if (allErrors.includes('memory') || allErrors.includes('oom')) {
450
- console.log(chalk_1.default.yellow(' Docker ran out of memory.'));
451
- console.log(chalk_1.default.gray(' Fix: increase Docker Desktop memory to 4GB+ in Settings → Resources'));
452
+ console.log(chalk_1.default.yellow(' Cause: Docker ran out of memory.'));
453
+ console.log(chalk_1.default.gray(' Fix: increase Docker Desktop memory to 4GB+ in Settings → Resources'));
454
+ }
455
+ else if (allErrors.includes('network') || allErrors.includes('timeout') || allErrors.includes('pull') || allErrors.includes('tls') || allErrors.includes('certificate')) {
456
+ console.log(chalk_1.default.yellow(' Cause: Network/TLS error — try restarting Docker Desktop.'));
452
457
  }
453
- else if (allErrors.includes('network') || allErrors.includes('timeout') || allErrors.includes('pull')) {
454
- console.log(chalk_1.default.yellow(' Network error while pulling images — check your connection and retry.'));
458
+ else if (errorLines.length > 0) {
459
+ console.log(chalk_1.default.gray(' Error details:'));
460
+ errorLines.slice(-5).forEach(l => console.log(chalk_1.default.red(' ' + l)));
455
461
  }
462
+ console.log('');
456
463
  console.log(chalk_1.default.gray(` To retry: cd ${displayDir} && docker compose up -d --build`));
457
- resolve();
464
+ resolve(false);
458
465
  });
459
466
  });
460
467
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "slackhive",
3
- "version": "0.1.32",
3
+ "version": "0.1.34",
4
4
  "description": "CLI to install and manage SlackHive — AI agent teams on Slack",
5
5
  "bin": {
6
6
  "slackhive": "./dist/index.js"