slackhive 0.1.33 → 0.1.35

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 +183 -165
  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,64 @@ 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);
64
- }
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
- }
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 };
80
40
  }
81
41
  }
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
- }
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
+ // Fallback: read credentials file directly (headless Linux / no keyring)
74
+ try {
75
+ const credPath = (0, path_1.join)(process.env.HOME || '~', '.claude', '.credentials.json');
76
+ if ((0, fs_1.existsSync)(credPath)) {
77
+ const creds = (0, fs_1.readFileSync)(credPath, 'utf-8').trim();
78
+ const result = parseOAuthFromJson(creds);
79
+ if (result)
80
+ return result;
81
+ }
82
+ }
83
+ catch { /* file not readable or invalid */ }
84
+ return null;
108
85
  }
109
86
  /**
110
87
  * Runs `slackhive init` — interactive setup wizard.
@@ -116,10 +93,10 @@ async function init(opts) {
116
93
  const O = chalk_1.default.hex('#D97757').bold;
117
94
  const W = chalk_1.default.hex('#EBE6E0').bold;
118
95
  console.log('');
119
- console.log(' ' + W(' │ │ '));
120
- console.log(' ' + W('──┼───┼──'));
121
- console.log(' ' + O('>') + W(' │──') + O('█') + W(''));
122
- console.log(' ' + W(' │ │ '));
96
+ console.log(' ' + W('│ │'));
97
+ console.log(' ' + W('───┼───┼───'));
98
+ console.log(' ' + O('>') + W(' ──┼──') + O('█') + W('┼──'));
99
+ console.log(' ' + W('│ │'));
123
100
  console.log('');
124
101
  console.log(chalk_1.default.bold(' SlackHive') + chalk_1.default.gray(' — AI agent teams on Slack'));
125
102
  console.log('');
@@ -180,6 +157,7 @@ async function init(opts) {
180
157
  process.exit(1);
181
158
  }
182
159
  const questions = [];
160
+ let oauthCreds = null;
183
161
  if (authMode.mode === 'apikey') {
184
162
  questions.push({
185
163
  type: 'text',
@@ -189,61 +167,33 @@ async function init(opts) {
189
167
  });
190
168
  }
191
169
  else {
192
- // Claude subscription mode — detect installation automatically
170
+ // Claude subscription mode — extract OAuth token
193
171
  const claudeDir = (0, path_1.join)(process.env.HOME || '~', '.claude');
194
172
  if (!(0, fs_1.existsSync)(claudeDir)) {
195
173
  console.log(chalk_1.default.yellow('\n warning: ~/.claude not found. Run `claude login` first, then re-run `slackhive init`.'));
196
174
  process.exit(1);
197
175
  }
198
176
  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
- }
177
+ const spinner = (0, ora_1.default)(' Extracting OAuth credentials...').start();
178
+ oauthCreds = extractOAuthCredentials();
179
+ if (oauthCreds) {
180
+ spinner.succeed('OAuth credentials extracted');
220
181
  }
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
- });
182
+ else {
183
+ spinner.warn('Could not auto-extract credentials from keychain');
184
+ console.log(chalk_1.default.gray(' On Linux/headless servers, paste your OAuth token manually.'));
185
+ console.log(chalk_1.default.gray(' Get it from a machine where you ran `claude login`:'));
186
+ console.log(chalk_1.default.gray(' security find-generic-password -s "Claude Code-credentials" -w'));
187
+ console.log('');
188
+ const tokenResponse = await (0, prompts_1.default)([
189
+ { 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' },
190
+ { 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' },
191
+ ]);
192
+ if (!tokenResponse.accessToken || !tokenResponse.refreshToken) {
193
+ console.log(chalk_1.default.red('\n Setup cancelled. Use API Key mode instead on headless servers.'));
194
+ process.exit(1);
246
195
  }
196
+ oauthCreds = { accessToken: tokenResponse.accessToken, refreshToken: tokenResponse.refreshToken };
247
197
  }
248
198
  }
249
199
  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 +207,9 @@ async function init(opts) {
257
207
  envContent += `ANTHROPIC_API_KEY=${response.anthropicKey}\n`;
258
208
  }
259
209
  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
- }
210
+ envContent += `# Claude Code subscription — OAuth credentials from keychain\n`;
211
+ envContent += `CLAUDE_CODE_OAUTH_TOKEN=${oauthCreds.accessToken}\n`;
212
+ envContent += `CLAUDE_CODE_OAUTH_REFRESH_TOKEN=${oauthCreds.refreshToken}\n`;
265
213
  }
266
214
  envContent += `\nPOSTGRES_DB=slackhive\n`;
267
215
  envContent += `POSTGRES_USER=slackhive\n`;
@@ -330,31 +278,42 @@ async function init(opts) {
330
278
  }
331
279
  }
332
280
  catch { /* non-fatal */ }
333
- await runDockerBuild(dir, opts.dir);
334
- // If containers didn't come up during build, retry once silently
281
+ // Remove stale Postgres volume if it exists — prevents password mismatch
282
+ // when re-running init with new credentials
335
283
  try {
336
- (0, child_process_1.execSync)('docker compose up -d', { cwd: dir, stdio: 'ignore' });
284
+ (0, child_process_1.execSync)('docker compose down -v', { cwd: dir, stdio: 'ignore' });
337
285
  }
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++) {
286
+ catch { /* non-fatal — may not exist yet */ }
287
+ const buildOk = await runDockerBuild(dir, opts.dir);
288
+ if (buildOk) {
289
+ // If containers didn't come up during build, retry once silently
343
290
  try {
344
- (0, child_process_1.execSync)('curl -sf http://localhost:3001/login', { stdio: 'ignore' });
345
- ready = true;
346
- break;
291
+ (0, child_process_1.execSync)('docker compose up -d', { cwd: dir, stdio: 'ignore' });
347
292
  }
348
- catch {
349
- await sleep(3000);
293
+ catch { /* non-fatal */ }
294
+ // Wait for web UI — up to 3 minutes
295
+ const webSpinner = (0, ora_1.default)(' Waiting for web UI to be ready...').start();
296
+ let ready = false;
297
+ for (let i = 0; i < 60; i++) {
298
+ try {
299
+ (0, child_process_1.execSync)('curl -sf http://localhost:3001/login', { stdio: 'ignore' });
300
+ ready = true;
301
+ break;
302
+ }
303
+ catch {
304
+ await sleep(3000);
305
+ }
306
+ }
307
+ if (ready) {
308
+ webSpinner.succeed('Web UI is ready');
309
+ }
310
+ else {
311
+ webReady = false;
312
+ webSpinner.stopAndPersist({ symbol: ' ' });
350
313
  }
351
- }
352
- if (ready) {
353
- webSpinner.succeed('Web UI is ready');
354
314
  }
355
315
  else {
356
316
  webReady = false;
357
- webSpinner.stopAndPersist({ symbol: ' ' });
358
317
  }
359
318
  }
360
319
  // ── Done ──────────────────────────────────────────────────────────────────
@@ -395,28 +354,71 @@ function runDockerBuild(cwd, displayDir) {
395
354
  const startTime = Date.now();
396
355
  const frames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
397
356
  let frameIdx = 0;
398
- let currentStep = 'Building images';
399
- const spinnerInterval = setInterval(() => {
357
+ // Phased progress tracking
358
+ const phases = [
359
+ { name: 'Installing system packages', weight: 10, pattern: /apk add|fetch.*APKINDEX/i },
360
+ { name: 'Installing npm dependencies', weight: 30, pattern: /npm ci|npm install|added \d+ packages/i },
361
+ { name: 'Compiling TypeScript', weight: 10, pattern: /tsc|--skipLibCheck/i },
362
+ { name: 'Building web app', weight: 30, pattern: /next build|next\.config/i },
363
+ { name: 'Creating containers', weight: 10, pattern: /exporting to image|naming to|exporting layers/i },
364
+ { name: 'Starting services', weight: 10, pattern: /Container .*(Starting|Started|Healthy|Created)/i },
365
+ ];
366
+ let currentPhase = 0;
367
+ let phaseStartTime = Date.now();
368
+ function getProgress() {
369
+ let pct = 0;
370
+ for (let i = 0; i < currentPhase; i++)
371
+ pct += phases[i].weight;
372
+ // Add partial progress within current phase
373
+ if (currentPhase < phases.length) {
374
+ const elapsed = (Date.now() - phaseStartTime) / 1000;
375
+ const estimatedDuration = currentPhase === 1 ? 90 : currentPhase === 3 ? 100 : 30;
376
+ const partial = Math.min(0.9, elapsed / estimatedDuration);
377
+ pct += phases[currentPhase].weight * partial;
378
+ }
379
+ return Math.min(99, Math.round(pct));
380
+ }
381
+ function renderBar() {
382
+ const pct = getProgress();
383
+ const cols = process.stdout.columns || 80;
384
+ const barWidth = Math.min(20, Math.max(10, cols - 55));
385
+ const filled = Math.round((pct / 100) * barWidth);
386
+ const empty = barWidth - filled;
387
+ const bar = chalk_1.default.hex('#D97757')('█'.repeat(filled)) + chalk_1.default.gray('░'.repeat(empty));
400
388
  const elapsed = Math.floor((Date.now() - startTime) / 1000);
389
+ const phaseName = currentPhase < phases.length ? phases[currentPhase].name : 'Finishing';
401
390
  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)}`);
391
+ const pctStr = String(pct).padStart(2);
392
+ return ` ${chalk_1.default.hex('#D97757')(frame)} ${bar} ${chalk_1.default.bold(pctStr + '%')} ${phaseName} ${chalk_1.default.gray('(' + elapsed + 's)')}`;
393
+ }
394
+ const spinnerInterval = setInterval(() => {
395
+ process.stdout.write(`\r\x1b[K${renderBar()}`);
405
396
  }, 80);
406
397
  let buf = '';
407
398
  const errorLines = [];
408
399
  const onData = (chunk) => {
409
- buf += chunk.toString().replace(/\x1b\[[0-9;]*[a-zA-Z]/g, ''); // strip ANSI
400
+ buf += chunk.toString().replace(/\x1b\[[0-9;]*[a-zA-Z]/g, '');
410
401
  const lines = buf.split('\n');
411
402
  buf = lines.pop() ?? '';
412
403
  for (const raw of lines) {
413
404
  const line = raw.trim();
414
405
  if (!line)
415
406
  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)}`;
407
+ // Check if we've entered a new phase
408
+ for (let i = currentPhase + 1; i < phases.length; i++) {
409
+ if (phases[i].pattern.test(line)) {
410
+ // Print completed phases
411
+ const elapsed = Math.floor((Date.now() - phaseStartTime) / 1000);
412
+ process.stdout.write('\r\x1b[K');
413
+ console.log(' ' + chalk_1.default.green('✓') + ' ' + phases[currentPhase].name + chalk_1.default.gray(` (${elapsed}s)`));
414
+ // Skip intermediate phases
415
+ for (let j = currentPhase + 1; j < i; j++) {
416
+ console.log(' ' + chalk_1.default.green('✓') + ' ' + phases[j].name + chalk_1.default.gray(' (cached)'));
417
+ }
418
+ currentPhase = i;
419
+ phaseStartTime = Date.now();
420
+ break;
421
+ }
420
422
  }
421
423
  if (/error/i.test(line))
422
424
  errorLines.push(line);
@@ -428,33 +430,49 @@ function runDockerBuild(cwd, displayDir) {
428
430
  clearInterval(spinnerInterval);
429
431
  process.stdout.write('\r\x1b[K');
430
432
  if (code === 0) {
431
- console.log(' ' + chalk_1.default.green('✓') + ' All services started');
432
- resolve();
433
+ // Print any remaining phases as done
434
+ const elapsed = Math.floor((Date.now() - phaseStartTime) / 1000);
435
+ if (currentPhase < phases.length) {
436
+ console.log(' ' + chalk_1.default.green('✓') + ' ' + phases[currentPhase].name + chalk_1.default.gray(` (${elapsed}s)`));
437
+ for (let j = currentPhase + 1; j < phases.length; j++) {
438
+ console.log(' ' + chalk_1.default.green('✓') + ' ' + phases[j].name);
439
+ }
440
+ }
441
+ console.log('');
442
+ console.log(' ' + chalk_1.default.green('✓') + chalk_1.default.bold(' All services started'));
443
+ resolve(true);
433
444
  return;
434
445
  }
446
+ console.log(' ' + chalk_1.default.red('✗') + ' Failed to start services');
447
+ console.log('');
435
448
  const allErrors = errorLines.join('\n').toLowerCase();
436
449
  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'));
450
+ console.log(chalk_1.default.yellow(' Cause: Docker is out of disk space.'));
451
+ console.log(chalk_1.default.gray(' Fix: docker system prune -a'));
439
452
  }
440
453
  else if (allErrors.includes('port is already allocated') || allErrors.includes('address already in use')) {
441
454
  const portMatch = /bind for .+:(\d+)/.exec(allErrors);
442
455
  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`));
456
+ console.log(chalk_1.default.yellow(` Cause: Port ${port} is already in use.`));
457
+ console.log(chalk_1.default.gray(` Fix: stop the process on port ${port} and retry`));
445
458
  }
446
459
  else if (allErrors.includes('permission denied') || allErrors.includes('unauthorized')) {
447
- console.log(chalk_1.default.yellow(' Docker permission denied — is Docker Desktop running?'));
460
+ console.log(chalk_1.default.yellow(' Cause: Docker permission denied — is Docker Desktop running?'));
448
461
  }
449
462
  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'));
463
+ console.log(chalk_1.default.yellow(' Cause: Docker ran out of memory.'));
464
+ console.log(chalk_1.default.gray(' Fix: increase Docker Desktop memory to 4GB+ in Settings → Resources'));
465
+ }
466
+ else if (allErrors.includes('network') || allErrors.includes('timeout') || allErrors.includes('pull') || allErrors.includes('tls') || allErrors.includes('certificate')) {
467
+ console.log(chalk_1.default.yellow(' Cause: Network/TLS error — try restarting Docker Desktop.'));
452
468
  }
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.'));
469
+ else if (errorLines.length > 0) {
470
+ console.log(chalk_1.default.gray(' Error details:'));
471
+ errorLines.slice(-5).forEach(l => console.log(chalk_1.default.red(' ' + l)));
455
472
  }
473
+ console.log('');
456
474
  console.log(chalk_1.default.gray(` To retry: cd ${displayDir} && docker compose up -d --build`));
457
- resolve();
475
+ resolve(false);
458
476
  });
459
477
  });
460
478
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "slackhive",
3
- "version": "0.1.33",
3
+ "version": "0.1.35",
4
4
  "description": "CLI to install and manage SlackHive — AI agent teams on Slack",
5
5
  "bin": {
6
6
  "slackhive": "./dist/index.js"