instar 0.1.0 → 0.1.2

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.
package/README.md CHANGED
@@ -187,7 +187,11 @@ The goal: make it possible for anyone to give their Claude Code project the kind
187
187
  - Node.js 18+
188
188
  - [Claude Code CLI](https://docs.anthropic.com/en/docs/claude-code)
189
189
  - tmux (`brew install tmux` on macOS, `apt install tmux` on Linux)
190
- - A Claude subscription (Max or Pro) -- Instar uses your existing subscription, not API keys
190
+ - Claude authentication -- either:
191
+ - An [Anthropic API key](https://console.anthropic.com/) (recommended for production/commercial use)
192
+ - A Claude subscription (Max or Pro) with Claude Code logged in (for personal use)
193
+
194
+ Instar spawns the official Claude Code CLI and respects whatever authentication you have configured. It never extracts or proxies your credentials.
191
195
 
192
196
  ## License
193
197
 
package/dist/cli.js CHANGED
@@ -25,7 +25,7 @@ const program = new Command();
25
25
  program
26
26
  .name('instar')
27
27
  .description('Persistent autonomy infrastructure for AI agents')
28
- .version('0.1.0')
28
+ .version('0.1.2')
29
29
  .option('--classic', 'Use the classic inquirer-based setup wizard instead of Claude')
30
30
  .action((opts) => runSetup(opts)); // Default: run interactive setup when no subcommand given
31
31
  // ── Setup (explicit alias) ────────────────────────────────────────
@@ -31,7 +31,7 @@ import path from 'node:path';
31
31
  import pc from 'picocolors';
32
32
  import { randomUUID } from 'node:crypto';
33
33
  import { ensureStateDir } from '../core/Config.js';
34
- import { checkPrerequisites, printPrerequisiteCheck } from '../core/Prerequisites.js';
34
+ import { ensurePrerequisites } from '../core/Prerequisites.js';
35
35
  import { defaultIdentity } from '../scaffold/bootstrap.js';
36
36
  import { generateAgentMd, generateUserMd, generateMemoryMd, generateClaudeMd, } from '../scaffold/templates.js';
37
37
  /**
@@ -57,9 +57,9 @@ async function initFreshProject(projectName, options) {
57
57
  console.log(pc.bold(` Creating new agent project: ${pc.cyan(projectName)}`));
58
58
  console.log(pc.dim(` Directory: ${projectDir}`));
59
59
  console.log();
60
- // Check prerequisites
61
- const prereqs = checkPrerequisites();
62
- if (!printPrerequisiteCheck(prereqs)) {
60
+ // Check and install prerequisites
61
+ const prereqs = await ensurePrerequisites();
62
+ if (!prereqs.allMet) {
63
63
  process.exit(1);
64
64
  }
65
65
  // Check if directory already exists
@@ -198,9 +198,9 @@ async function initExistingProject(options) {
198
198
  const port = options.port || 4040;
199
199
  console.log(pc.bold(`\nInitializing instar in: ${pc.cyan(projectDir)}`));
200
200
  console.log();
201
- // Check prerequisites
202
- const prereqs = checkPrerequisites();
203
- if (!printPrerequisiteCheck(prereqs)) {
201
+ // Check and install prerequisites
202
+ const prereqs = await ensurePrerequisites();
203
+ if (!prereqs.allMet) {
204
204
  process.exit(1);
205
205
  }
206
206
  const tmuxPath = prereqs.results.find(r => r.name === 'tmux').path;
@@ -14,13 +14,14 @@
14
14
  *
15
15
  * No flags needed. No manual config editing. Just answers.
16
16
  */
17
- import { spawn } from 'node:child_process';
17
+ import { execSync, spawn } from 'node:child_process';
18
18
  import fs from 'node:fs';
19
19
  import path from 'node:path';
20
20
  import pc from 'picocolors';
21
21
  import { input, confirm, select, number } from '@inquirer/prompts';
22
22
  import { Cron } from 'croner';
23
- import { detectTmuxPath, detectClaudePath, ensureStateDir } from '../core/Config.js';
23
+ import { detectClaudePath, ensureStateDir } from '../core/Config.js';
24
+ import { ensurePrerequisites } from '../core/Prerequisites.js';
24
25
  import { UserManager } from '../users/UserManager.js';
25
26
  /**
26
27
  * Launch the conversational setup wizard via Claude Code.
@@ -31,13 +32,21 @@ export async function runSetup(opts) {
31
32
  if (opts?.classic) {
32
33
  return runClassicSetup();
33
34
  }
34
- // Check for Claude CLI
35
+ // Check and install prerequisites
36
+ console.log();
37
+ const prereqs = await ensurePrerequisites();
38
+ // Check for Claude CLI (may have been just installed)
35
39
  const claudePath = detectClaudePath();
36
40
  if (!claudePath) {
37
41
  console.log();
38
42
  console.log(pc.yellow(' Claude CLI not found — falling back to classic setup wizard.'));
39
43
  console.log(pc.dim(' Install Claude Code for the conversational experience:'));
40
- console.log(pc.dim(' https://docs.anthropic.com/en/docs/claude-code'));
44
+ console.log(pc.dim(' npm install -g @anthropic-ai/claude-code'));
45
+ console.log();
46
+ return runClassicSetup();
47
+ }
48
+ if (!prereqs.allMet) {
49
+ console.log(pc.yellow(' Some prerequisites are still missing. Falling back to classic setup.'));
41
50
  console.log();
42
51
  return runClassicSetup();
43
52
  }
@@ -114,22 +123,14 @@ async function runClassicSetup() {
114
123
  console.log(pc.bold(' Welcome to Instar'));
115
124
  console.log(pc.dim(' Persistent agent infrastructure for any Claude Code project'));
116
125
  console.log();
117
- // ── Step 0: Check prerequisites ──────────────────────────────────
118
- const tmuxPath = detectTmuxPath();
119
- const claudePath = detectClaudePath();
120
- if (!tmuxPath) {
121
- console.log(pc.red(' tmux is required but not installed.'));
122
- console.log(' Install: brew install tmux (macOS) or apt install tmux (Linux)');
126
+ // ── Step 0: Check and install prerequisites ─────────────────────
127
+ const prereqs = await ensurePrerequisites();
128
+ if (!prereqs.allMet) {
123
129
  process.exit(1);
124
130
  }
125
- console.log(` ${pc.green('')} tmux found: ${pc.dim(tmuxPath)}`);
126
- if (!claudePath) {
127
- console.log(pc.red(' Claude CLI is required but not installed.'));
128
- console.log(' Install: https://docs.anthropic.com/en/docs/claude-code');
129
- process.exit(1);
130
- }
131
- console.log(` ${pc.green('✓')} Claude CLI found: ${pc.dim(claudePath)}`);
132
- console.log();
131
+ const tmuxPath = prereqs.results.find(r => r.name === 'tmux').path;
132
+ // Use a scoped name to avoid shadowing the outer runSetup's claudePath
133
+ const claudePath = prereqs.results.find(r => r.name === 'Claude CLI').path;
133
134
  // ── Step 1: Project ──────────────────────────────────────────────
134
135
  const detectedDir = process.cwd();
135
136
  const detectedName = path.basename(detectedDir);
@@ -302,6 +303,29 @@ async function runClassicSetup() {
302
303
  console.log(` ${pc.cyan('.instar/jobs.json')} — job definitions`);
303
304
  console.log(` ${pc.cyan('.instar/users.json')} — user profiles`);
304
305
  console.log();
306
+ // Check if instar is globally installed (needed for server commands)
307
+ const isGloballyInstalled = isInstarGlobal();
308
+ if (!isGloballyInstalled) {
309
+ console.log(pc.dim(' Tip: instar is not installed globally. For persistent server'));
310
+ console.log(pc.dim(' commands (start, stop, status), install it globally:'));
311
+ console.log();
312
+ const installGlobal = await confirm({
313
+ message: 'Install instar globally? (npm install -g instar)',
314
+ default: true,
315
+ });
316
+ if (installGlobal) {
317
+ try {
318
+ console.log(pc.dim(' Running: npm install -g instar'));
319
+ execSync('npm install -g instar', { encoding: 'utf-8', stdio: 'inherit' });
320
+ console.log(` ${pc.green('✓')} instar installed globally`);
321
+ }
322
+ catch {
323
+ console.log(pc.yellow(' Could not install globally. You can run it later:'));
324
+ console.log(` ${pc.cyan('npm install -g instar')}`);
325
+ }
326
+ }
327
+ console.log();
328
+ }
305
329
  // Offer to start server
306
330
  const startNow = await confirm({
307
331
  message: 'Start the agent server now?',
@@ -324,6 +348,22 @@ async function runClassicSetup() {
324
348
  }
325
349
  console.log();
326
350
  }
351
+ /**
352
+ * Check if instar is installed globally (vs running via npx).
353
+ */
354
+ function isInstarGlobal() {
355
+ try {
356
+ const result = execSync('which instar 2>/dev/null || where instar 2>/dev/null', {
357
+ encoding: 'utf-8',
358
+ stdio: 'pipe',
359
+ }).trim();
360
+ // npx creates a temp binary — check if it's a real global install
361
+ return !!result && !result.includes('.npm/_npx');
362
+ }
363
+ catch {
364
+ return false;
365
+ }
366
+ }
327
367
  // ── Prompt Helpers ───────────────────────────────────────────────
328
368
  /**
329
369
  * Full Telegram walkthrough. Returns config or null if skipped.
@@ -1,8 +1,8 @@
1
1
  /**
2
- * Prerequisite detection and installation guidance.
2
+ * Prerequisite detection and auto-installation.
3
3
  *
4
4
  * Checks for required software (tmux, Claude CLI, Node.js)
5
- * and provides clear installation instructions when something is missing.
5
+ * and offers to install missing dependencies automatically.
6
6
  */
7
7
  export interface PrerequisiteResult {
8
8
  name: string;
@@ -10,6 +10,10 @@ export interface PrerequisiteResult {
10
10
  path?: string;
11
11
  version?: string;
12
12
  installHint: string;
13
+ /** Whether this prerequisite can be auto-installed. */
14
+ canAutoInstall: boolean;
15
+ /** The command to run to auto-install this prerequisite. */
16
+ installCommand?: string;
13
17
  }
14
18
  export interface PrerequisiteCheck {
15
19
  allMet: boolean;
@@ -25,4 +29,9 @@ export declare function checkPrerequisites(): PrerequisiteCheck;
25
29
  * Returns true if all prerequisites are met.
26
30
  */
27
31
  export declare function printPrerequisiteCheck(check: PrerequisiteCheck): boolean;
32
+ /**
33
+ * Interactive prerequisite check that offers to install missing dependencies.
34
+ * Returns a fresh PrerequisiteCheck after any installations.
35
+ */
36
+ export declare function ensurePrerequisites(): Promise<PrerequisiteCheck>;
28
37
  //# sourceMappingURL=Prerequisites.d.ts.map
@@ -1,11 +1,12 @@
1
1
  /**
2
- * Prerequisite detection and installation guidance.
2
+ * Prerequisite detection and auto-installation.
3
3
  *
4
4
  * Checks for required software (tmux, Claude CLI, Node.js)
5
- * and provides clear installation instructions when something is missing.
5
+ * and offers to install missing dependencies automatically.
6
6
  */
7
7
  import { execSync } from 'node:child_process';
8
8
  import pc from 'picocolors';
9
+ import { confirm } from '@inquirer/prompts';
9
10
  import { detectTmuxPath, detectClaudePath } from './Config.js';
10
11
  /**
11
12
  * Detect the current platform for install guidance.
@@ -69,27 +70,46 @@ function getNodeVersion() {
69
70
  return { version, major };
70
71
  }
71
72
  /**
72
- * Build install hint for tmux based on platform.
73
+ * Build install hint and command for tmux based on platform.
73
74
  */
74
- function tmuxInstallHint() {
75
+ function tmuxInstallInfo() {
75
76
  const platform = detectPlatform();
76
77
  switch (platform) {
77
78
  case 'macos-arm':
78
79
  case 'macos-intel':
79
- return hasHomebrew()
80
- ? 'Install with: brew install tmux'
81
- : 'Install Homebrew first (https://brew.sh), then: brew install tmux';
80
+ if (hasHomebrew()) {
81
+ return {
82
+ hint: 'Install with: brew install tmux',
83
+ canAutoInstall: true,
84
+ command: 'brew install tmux',
85
+ };
86
+ }
87
+ return {
88
+ hint: 'Install Homebrew first (https://brew.sh), then: brew install tmux',
89
+ canAutoInstall: false,
90
+ };
82
91
  case 'linux':
83
- return 'Install with: sudo apt install tmux (Debian/Ubuntu) or sudo yum install tmux (RHEL/CentOS)';
92
+ return {
93
+ hint: 'Install with: sudo apt install tmux (Debian/Ubuntu) or sudo yum install tmux (RHEL/CentOS)',
94
+ canAutoInstall: true,
95
+ command: 'sudo apt install -y tmux',
96
+ };
84
97
  default:
85
- return 'Install tmux: https://github.com/tmux/tmux/wiki/Installing';
98
+ return {
99
+ hint: 'Install tmux: https://github.com/tmux/tmux/wiki/Installing',
100
+ canAutoInstall: false,
101
+ };
86
102
  }
87
103
  }
88
104
  /**
89
- * Build install hint for Claude CLI based on platform.
105
+ * Build install hint and command for Claude CLI.
90
106
  */
91
- function claudeInstallHint() {
92
- return 'Install Claude Code: npm install -g @anthropic-ai/claude-code\n Docs: https://docs.anthropic.com/en/docs/claude-code';
107
+ function claudeInstallInfo() {
108
+ return {
109
+ hint: 'Install Claude Code: npm install -g @anthropic-ai/claude-code',
110
+ canAutoInstall: true,
111
+ command: 'npm install -g @anthropic-ai/claude-code',
112
+ };
93
113
  }
94
114
  /**
95
115
  * Check all prerequisites and return a structured result.
@@ -105,24 +125,31 @@ export function checkPrerequisites() {
105
125
  installHint: node.major < 18
106
126
  ? `Node.js 18+ required (found ${node.version}). Update: https://nodejs.org`
107
127
  : '',
128
+ canAutoInstall: false,
108
129
  });
109
130
  // 2. tmux
110
131
  const tmuxPath = detectTmuxPath();
132
+ const tmuxInfo = tmuxInstallInfo();
111
133
  results.push({
112
134
  name: 'tmux',
113
135
  found: !!tmuxPath,
114
136
  path: tmuxPath || undefined,
115
137
  version: tmuxPath ? getTmuxVersion(tmuxPath) : undefined,
116
- installHint: tmuxInstallHint(),
138
+ installHint: tmuxInfo.hint,
139
+ canAutoInstall: tmuxInfo.canAutoInstall,
140
+ installCommand: tmuxInfo.command,
117
141
  });
118
142
  // 3. Claude CLI
119
143
  const claudePath = detectClaudePath();
144
+ const claudeInfo = claudeInstallInfo();
120
145
  results.push({
121
146
  name: 'Claude CLI',
122
147
  found: !!claudePath,
123
148
  path: claudePath || undefined,
124
149
  version: claudePath ? getClaudeVersion(claudePath) : undefined,
125
- installHint: claudeInstallHint(),
150
+ installHint: claudeInfo.hint,
151
+ canAutoInstall: claudeInfo.canAutoInstall,
152
+ installCommand: claudeInfo.command,
126
153
  });
127
154
  const missing = results.filter(r => !r.found);
128
155
  return {
@@ -131,6 +158,26 @@ export function checkPrerequisites() {
131
158
  missing,
132
159
  };
133
160
  }
161
+ /**
162
+ * Attempt to install a missing prerequisite.
163
+ * Returns true if installation succeeded.
164
+ */
165
+ function installPrerequisite(result) {
166
+ if (!result.installCommand)
167
+ return false;
168
+ try {
169
+ console.log(pc.dim(` Running: ${result.installCommand}`));
170
+ execSync(result.installCommand, {
171
+ encoding: 'utf-8',
172
+ stdio: 'inherit',
173
+ timeout: 120000, // 2 min timeout
174
+ });
175
+ return true;
176
+ }
177
+ catch {
178
+ return false;
179
+ }
180
+ }
134
181
  /**
135
182
  * Print prerequisite check results to console.
136
183
  * Returns true if all prerequisites are met.
@@ -156,4 +203,61 @@ export function printPrerequisiteCheck(check) {
156
203
  }
157
204
  return check.allMet;
158
205
  }
206
+ /**
207
+ * Interactive prerequisite check that offers to install missing dependencies.
208
+ * Returns a fresh PrerequisiteCheck after any installations.
209
+ */
210
+ export async function ensurePrerequisites() {
211
+ let check = checkPrerequisites();
212
+ console.log(pc.bold(' Checking prerequisites...'));
213
+ console.log();
214
+ for (const result of check.results) {
215
+ if (result.found) {
216
+ const versionStr = result.version ? ` (${result.version})` : '';
217
+ const pathStr = result.path ? pc.dim(` ${result.path}`) : '';
218
+ console.log(` ${pc.green('✓')} ${result.name}${versionStr}${pathStr}`);
219
+ }
220
+ }
221
+ if (check.allMet) {
222
+ console.log();
223
+ return check;
224
+ }
225
+ // Handle missing prerequisites
226
+ for (const missing of check.missing) {
227
+ console.log();
228
+ console.log(` ${pc.red('✗')} ${missing.name} — not found`);
229
+ if (missing.canAutoInstall && missing.installCommand) {
230
+ const install = await confirm({
231
+ message: `Install ${missing.name}? (${pc.dim(missing.installCommand)})`,
232
+ default: true,
233
+ });
234
+ if (install) {
235
+ const success = installPrerequisite(missing);
236
+ if (success) {
237
+ console.log(` ${pc.green('✓')} ${missing.name} installed successfully`);
238
+ }
239
+ else {
240
+ console.log(` ${pc.red('✗')} Failed to install ${missing.name}`);
241
+ console.log(` Try manually: ${missing.installHint}`);
242
+ }
243
+ }
244
+ else {
245
+ console.log(` ${missing.installHint}`);
246
+ }
247
+ }
248
+ else {
249
+ console.log(` ${missing.installHint}`);
250
+ }
251
+ }
252
+ // Re-check after installations
253
+ check = checkPrerequisites();
254
+ console.log();
255
+ if (!check.allMet) {
256
+ const stillMissing = check.missing.map(r => r.name).join(', ');
257
+ console.log(pc.red(` Still missing: ${stillMissing}`));
258
+ console.log(pc.dim(' Install the missing prerequisites and run instar again.'));
259
+ console.log();
260
+ }
261
+ return check;
262
+ }
159
263
  //# sourceMappingURL=Prerequisites.js.map
@@ -68,9 +68,8 @@ export class SessionManager extends EventEmitter {
68
68
  }
69
69
  claudeArgs.push('-p', options.prompt);
70
70
  // Create tmux session and run claude
71
- // Unset ANTHROPIC_* env vars so Claude uses OAuth (subscription) not API key
72
- const cleanEnv = 'unset ANTHROPIC_API_KEY ANTHROPIC_ADMIN_KEY CLAUDECODE;';
73
- const claudeCmd = `${cleanEnv} ${this.config.claudePath} ${claudeArgs.map(a => `'${a.replace(/'/g, "'\\''")}'`).join(' ')}`;
71
+ // Respect the user's configured auth method (API key or OAuth subscription)
72
+ const claudeCmd = `${this.config.claudePath} ${claudeArgs.map(a => `'${a.replace(/'/g, "'\\''")}'`).join(' ')}`;
74
73
  const tmuxCmd = [
75
74
  this.config.tmuxPath,
76
75
  'new-session',
@@ -223,9 +222,9 @@ export class SessionManager extends EventEmitter {
223
222
  }
224
223
  return tmuxSession;
225
224
  }
226
- // Unset ANTHROPIC_* env vars so Claude uses OAuth (subscription) not API key
225
+ // Respect the user's configured auth method (API key or OAuth subscription)
227
226
  const claudeCmd = `${this.config.claudePath} --dangerously-skip-permissions`;
228
- const shellCmd = `cd '${this.config.projectDir}' && unset ANTHROPIC_API_KEY ANTHROPIC_ADMIN_KEY CLAUDECODE && ${claudeCmd}`;
227
+ const shellCmd = `cd '${this.config.projectDir}' && ${claudeCmd}`;
229
228
  const tmuxCmd = `${this.config.tmuxPath} new-session -d -s '${tmuxSession}' -x 200 -y 50 'bash -c "${shellCmd.replace(/"/g, '\\"')}"'`;
230
229
  try {
231
230
  execSync(tmuxCmd, { encoding: 'utf-8' });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "instar",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "Persistent autonomy infrastructure for AI agents",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
package/src/cli.ts CHANGED
@@ -29,7 +29,7 @@ const program = new Command();
29
29
  program
30
30
  .name('instar')
31
31
  .description('Persistent autonomy infrastructure for AI agents')
32
- .version('0.1.0')
32
+ .version('0.1.2')
33
33
  .option('--classic', 'Use the classic inquirer-based setup wizard instead of Claude')
34
34
  .action((opts) => runSetup(opts)); // Default: run interactive setup when no subcommand given
35
35
 
@@ -32,7 +32,7 @@ import path from 'node:path';
32
32
  import pc from 'picocolors';
33
33
  import { randomUUID } from 'node:crypto';
34
34
  import { detectTmuxPath, detectClaudePath, ensureStateDir } from '../core/Config.js';
35
- import { checkPrerequisites, printPrerequisiteCheck } from '../core/Prerequisites.js';
35
+ import { ensurePrerequisites } from '../core/Prerequisites.js';
36
36
  import { defaultIdentity } from '../scaffold/bootstrap.js';
37
37
  import {
38
38
  generateAgentMd,
@@ -75,9 +75,9 @@ async function initFreshProject(projectName: string, options: InitOptions): Prom
75
75
  console.log(pc.dim(` Directory: ${projectDir}`));
76
76
  console.log();
77
77
 
78
- // Check prerequisites
79
- const prereqs = checkPrerequisites();
80
- if (!printPrerequisiteCheck(prereqs)) {
78
+ // Check and install prerequisites
79
+ const prereqs = await ensurePrerequisites();
80
+ if (!prereqs.allMet) {
81
81
  process.exit(1);
82
82
  }
83
83
 
@@ -246,9 +246,9 @@ async function initExistingProject(options: InitOptions): Promise<void> {
246
246
  console.log(pc.bold(`\nInitializing instar in: ${pc.cyan(projectDir)}`));
247
247
  console.log();
248
248
 
249
- // Check prerequisites
250
- const prereqs = checkPrerequisites();
251
- if (!printPrerequisiteCheck(prereqs)) {
249
+ // Check and install prerequisites
250
+ const prereqs = await ensurePrerequisites();
251
+ if (!prereqs.allMet) {
252
252
  process.exit(1);
253
253
  }
254
254
 
@@ -22,6 +22,7 @@ import pc from 'picocolors';
22
22
  import { input, confirm, select, number } from '@inquirer/prompts';
23
23
  import { Cron } from 'croner';
24
24
  import { detectTmuxPath, detectClaudePath, ensureStateDir } from '../core/Config.js';
25
+ import { ensurePrerequisites } from '../core/Prerequisites.js';
25
26
  import { UserManager } from '../users/UserManager.js';
26
27
  import { validateJob } from '../scheduler/JobLoader.js';
27
28
  import type { AgentKitConfig, JobDefinition, UserProfile, UserChannel } from '../core/types.js';
@@ -36,13 +37,23 @@ export async function runSetup(opts?: { classic?: boolean }): Promise<void> {
36
37
  return runClassicSetup();
37
38
  }
38
39
 
39
- // Check for Claude CLI
40
+ // Check and install prerequisites
41
+ console.log();
42
+ const prereqs = await ensurePrerequisites();
43
+
44
+ // Check for Claude CLI (may have been just installed)
40
45
  const claudePath = detectClaudePath();
41
46
  if (!claudePath) {
42
47
  console.log();
43
48
  console.log(pc.yellow(' Claude CLI not found — falling back to classic setup wizard.'));
44
49
  console.log(pc.dim(' Install Claude Code for the conversational experience:'));
45
- console.log(pc.dim(' https://docs.anthropic.com/en/docs/claude-code'));
50
+ console.log(pc.dim(' npm install -g @anthropic-ai/claude-code'));
51
+ console.log();
52
+ return runClassicSetup();
53
+ }
54
+
55
+ if (!prereqs.allMet) {
56
+ console.log(pc.yellow(' Some prerequisites are still missing. Falling back to classic setup.'));
46
57
  console.log();
47
58
  return runClassicSetup();
48
59
  }
@@ -123,25 +134,16 @@ async function runClassicSetup(): Promise<void> {
123
134
  console.log(pc.dim(' Persistent agent infrastructure for any Claude Code project'));
124
135
  console.log();
125
136
 
126
- // ── Step 0: Check prerequisites ──────────────────────────────────
127
-
128
- const tmuxPath = detectTmuxPath();
129
- const claudePath = detectClaudePath();
137
+ // ── Step 0: Check and install prerequisites ─────────────────────
130
138
 
131
- if (!tmuxPath) {
132
- console.log(pc.red(' tmux is required but not installed.'));
133
- console.log(' Install: brew install tmux (macOS) or apt install tmux (Linux)');
139
+ const prereqs = await ensurePrerequisites();
140
+ if (!prereqs.allMet) {
134
141
  process.exit(1);
135
142
  }
136
- console.log(` ${pc.green('✓')} tmux found: ${pc.dim(tmuxPath)}`);
137
143
 
138
- if (!claudePath) {
139
- console.log(pc.red(' Claude CLI is required but not installed.'));
140
- console.log(' Install: https://docs.anthropic.com/en/docs/claude-code');
141
- process.exit(1);
142
- }
143
- console.log(` ${pc.green('✓')} Claude CLI found: ${pc.dim(claudePath)}`);
144
- console.log();
144
+ const tmuxPath = prereqs.results.find(r => r.name === 'tmux')!.path!;
145
+ // Use a scoped name to avoid shadowing the outer runSetup's claudePath
146
+ const claudePath = prereqs.results.find(r => r.name === 'Claude CLI')!.path!;
145
147
 
146
148
  // ── Step 1: Project ──────────────────────────────────────────────
147
149
 
@@ -349,6 +351,31 @@ async function runClassicSetup(): Promise<void> {
349
351
  console.log(` ${pc.cyan('.instar/users.json')} — user profiles`);
350
352
  console.log();
351
353
 
354
+ // Check if instar is globally installed (needed for server commands)
355
+ const isGloballyInstalled = isInstarGlobal();
356
+ if (!isGloballyInstalled) {
357
+ console.log(pc.dim(' Tip: instar is not installed globally. For persistent server'));
358
+ console.log(pc.dim(' commands (start, stop, status), install it globally:'));
359
+ console.log();
360
+
361
+ const installGlobal = await confirm({
362
+ message: 'Install instar globally? (npm install -g instar)',
363
+ default: true,
364
+ });
365
+
366
+ if (installGlobal) {
367
+ try {
368
+ console.log(pc.dim(' Running: npm install -g instar'));
369
+ execSync('npm install -g instar', { encoding: 'utf-8', stdio: 'inherit' });
370
+ console.log(` ${pc.green('✓')} instar installed globally`);
371
+ } catch {
372
+ console.log(pc.yellow(' Could not install globally. You can run it later:'));
373
+ console.log(` ${pc.cyan('npm install -g instar')}`);
374
+ }
375
+ }
376
+ console.log();
377
+ }
378
+
352
379
  // Offer to start server
353
380
  const startNow = await confirm({
354
381
  message: 'Start the agent server now?',
@@ -373,6 +400,22 @@ async function runClassicSetup(): Promise<void> {
373
400
  console.log();
374
401
  }
375
402
 
403
+ /**
404
+ * Check if instar is installed globally (vs running via npx).
405
+ */
406
+ function isInstarGlobal(): boolean {
407
+ try {
408
+ const result = execSync('which instar 2>/dev/null || where instar 2>/dev/null', {
409
+ encoding: 'utf-8',
410
+ stdio: 'pipe',
411
+ }).trim();
412
+ // npx creates a temp binary — check if it's a real global install
413
+ return !!result && !result.includes('.npm/_npx');
414
+ } catch {
415
+ return false;
416
+ }
417
+ }
418
+
376
419
  // ── Prompt Helpers ───────────────────────────────────────────────
377
420
 
378
421
  /**
@@ -1,13 +1,14 @@
1
1
  /**
2
- * Prerequisite detection and installation guidance.
2
+ * Prerequisite detection and auto-installation.
3
3
  *
4
4
  * Checks for required software (tmux, Claude CLI, Node.js)
5
- * and provides clear installation instructions when something is missing.
5
+ * and offers to install missing dependencies automatically.
6
6
  */
7
7
 
8
8
  import { execSync } from 'node:child_process';
9
9
  import fs from 'node:fs';
10
10
  import pc from 'picocolors';
11
+ import { confirm } from '@inquirer/prompts';
11
12
  import { detectTmuxPath, detectClaudePath } from './Config.js';
12
13
 
13
14
  export interface PrerequisiteResult {
@@ -16,6 +17,10 @@ export interface PrerequisiteResult {
16
17
  path?: string;
17
18
  version?: string;
18
19
  installHint: string;
20
+ /** Whether this prerequisite can be auto-installed. */
21
+ canAutoInstall: boolean;
22
+ /** The command to run to auto-install this prerequisite. */
23
+ installCommand?: string;
19
24
  }
20
25
 
21
26
  export interface PrerequisiteCheck {
@@ -87,28 +92,47 @@ function getNodeVersion(): { version: string; major: number } {
87
92
  }
88
93
 
89
94
  /**
90
- * Build install hint for tmux based on platform.
95
+ * Build install hint and command for tmux based on platform.
91
96
  */
92
- function tmuxInstallHint(): string {
97
+ function tmuxInstallInfo(): { hint: string; canAutoInstall: boolean; command?: string } {
93
98
  const platform = detectPlatform();
94
99
  switch (platform) {
95
100
  case 'macos-arm':
96
101
  case 'macos-intel':
97
- return hasHomebrew()
98
- ? 'Install with: brew install tmux'
99
- : 'Install Homebrew first (https://brew.sh), then: brew install tmux';
102
+ if (hasHomebrew()) {
103
+ return {
104
+ hint: 'Install with: brew install tmux',
105
+ canAutoInstall: true,
106
+ command: 'brew install tmux',
107
+ };
108
+ }
109
+ return {
110
+ hint: 'Install Homebrew first (https://brew.sh), then: brew install tmux',
111
+ canAutoInstall: false,
112
+ };
100
113
  case 'linux':
101
- return 'Install with: sudo apt install tmux (Debian/Ubuntu) or sudo yum install tmux (RHEL/CentOS)';
114
+ return {
115
+ hint: 'Install with: sudo apt install tmux (Debian/Ubuntu) or sudo yum install tmux (RHEL/CentOS)',
116
+ canAutoInstall: true,
117
+ command: 'sudo apt install -y tmux',
118
+ };
102
119
  default:
103
- return 'Install tmux: https://github.com/tmux/tmux/wiki/Installing';
120
+ return {
121
+ hint: 'Install tmux: https://github.com/tmux/tmux/wiki/Installing',
122
+ canAutoInstall: false,
123
+ };
104
124
  }
105
125
  }
106
126
 
107
127
  /**
108
- * Build install hint for Claude CLI based on platform.
128
+ * Build install hint and command for Claude CLI.
109
129
  */
110
- function claudeInstallHint(): string {
111
- return 'Install Claude Code: npm install -g @anthropic-ai/claude-code\n Docs: https://docs.anthropic.com/en/docs/claude-code';
130
+ function claudeInstallInfo(): { hint: string; canAutoInstall: boolean; command: string } {
131
+ return {
132
+ hint: 'Install Claude Code: npm install -g @anthropic-ai/claude-code',
133
+ canAutoInstall: true,
134
+ command: 'npm install -g @anthropic-ai/claude-code',
135
+ };
112
136
  }
113
137
 
114
138
  /**
@@ -126,26 +150,33 @@ export function checkPrerequisites(): PrerequisiteCheck {
126
150
  installHint: node.major < 18
127
151
  ? `Node.js 18+ required (found ${node.version}). Update: https://nodejs.org`
128
152
  : '',
153
+ canAutoInstall: false,
129
154
  });
130
155
 
131
156
  // 2. tmux
132
157
  const tmuxPath = detectTmuxPath();
158
+ const tmuxInfo = tmuxInstallInfo();
133
159
  results.push({
134
160
  name: 'tmux',
135
161
  found: !!tmuxPath,
136
162
  path: tmuxPath || undefined,
137
163
  version: tmuxPath ? getTmuxVersion(tmuxPath) : undefined,
138
- installHint: tmuxInstallHint(),
164
+ installHint: tmuxInfo.hint,
165
+ canAutoInstall: tmuxInfo.canAutoInstall,
166
+ installCommand: tmuxInfo.command,
139
167
  });
140
168
 
141
169
  // 3. Claude CLI
142
170
  const claudePath = detectClaudePath();
171
+ const claudeInfo = claudeInstallInfo();
143
172
  results.push({
144
173
  name: 'Claude CLI',
145
174
  found: !!claudePath,
146
175
  path: claudePath || undefined,
147
176
  version: claudePath ? getClaudeVersion(claudePath) : undefined,
148
- installHint: claudeInstallHint(),
177
+ installHint: claudeInfo.hint,
178
+ canAutoInstall: claudeInfo.canAutoInstall,
179
+ installCommand: claudeInfo.command,
149
180
  });
150
181
 
151
182
  const missing = results.filter(r => !r.found);
@@ -157,6 +188,26 @@ export function checkPrerequisites(): PrerequisiteCheck {
157
188
  };
158
189
  }
159
190
 
191
+ /**
192
+ * Attempt to install a missing prerequisite.
193
+ * Returns true if installation succeeded.
194
+ */
195
+ function installPrerequisite(result: PrerequisiteResult): boolean {
196
+ if (!result.installCommand) return false;
197
+
198
+ try {
199
+ console.log(pc.dim(` Running: ${result.installCommand}`));
200
+ execSync(result.installCommand, {
201
+ encoding: 'utf-8',
202
+ stdio: 'inherit',
203
+ timeout: 120000, // 2 min timeout
204
+ });
205
+ return true;
206
+ } catch {
207
+ return false;
208
+ }
209
+ }
210
+
160
211
  /**
161
212
  * Print prerequisite check results to console.
162
213
  * Returns true if all prerequisites are met.
@@ -185,3 +236,67 @@ export function printPrerequisiteCheck(check: PrerequisiteCheck): boolean {
185
236
 
186
237
  return check.allMet;
187
238
  }
239
+
240
+ /**
241
+ * Interactive prerequisite check that offers to install missing dependencies.
242
+ * Returns a fresh PrerequisiteCheck after any installations.
243
+ */
244
+ export async function ensurePrerequisites(): Promise<PrerequisiteCheck> {
245
+ let check = checkPrerequisites();
246
+
247
+ console.log(pc.bold(' Checking prerequisites...'));
248
+ console.log();
249
+
250
+ for (const result of check.results) {
251
+ if (result.found) {
252
+ const versionStr = result.version ? ` (${result.version})` : '';
253
+ const pathStr = result.path ? pc.dim(` ${result.path}`) : '';
254
+ console.log(` ${pc.green('✓')} ${result.name}${versionStr}${pathStr}`);
255
+ }
256
+ }
257
+
258
+ if (check.allMet) {
259
+ console.log();
260
+ return check;
261
+ }
262
+
263
+ // Handle missing prerequisites
264
+ for (const missing of check.missing) {
265
+ console.log();
266
+ console.log(` ${pc.red('✗')} ${missing.name} — not found`);
267
+
268
+ if (missing.canAutoInstall && missing.installCommand) {
269
+ const install = await confirm({
270
+ message: `Install ${missing.name}? (${pc.dim(missing.installCommand)})`,
271
+ default: true,
272
+ });
273
+
274
+ if (install) {
275
+ const success = installPrerequisite(missing);
276
+ if (success) {
277
+ console.log(` ${pc.green('✓')} ${missing.name} installed successfully`);
278
+ } else {
279
+ console.log(` ${pc.red('✗')} Failed to install ${missing.name}`);
280
+ console.log(` Try manually: ${missing.installHint}`);
281
+ }
282
+ } else {
283
+ console.log(` ${missing.installHint}`);
284
+ }
285
+ } else {
286
+ console.log(` ${missing.installHint}`);
287
+ }
288
+ }
289
+
290
+ // Re-check after installations
291
+ check = checkPrerequisites();
292
+ console.log();
293
+
294
+ if (!check.allMet) {
295
+ const stillMissing = check.missing.map(r => r.name).join(', ');
296
+ console.log(pc.red(` Still missing: ${stillMissing}`));
297
+ console.log(pc.dim(' Install the missing prerequisites and run instar again.'));
298
+ console.log();
299
+ }
300
+
301
+ return check;
302
+ }
@@ -92,9 +92,8 @@ export class SessionManager extends EventEmitter {
92
92
  claudeArgs.push('-p', options.prompt);
93
93
 
94
94
  // Create tmux session and run claude
95
- // Unset ANTHROPIC_* env vars so Claude uses OAuth (subscription) not API key
96
- const cleanEnv = 'unset ANTHROPIC_API_KEY ANTHROPIC_ADMIN_KEY CLAUDECODE;';
97
- const claudeCmd = `${cleanEnv} ${this.config.claudePath} ${claudeArgs.map(a => `'${a.replace(/'/g, "'\\''")}'`).join(' ')}`;
95
+ // Respect the user's configured auth method (API key or OAuth subscription)
96
+ const claudeCmd = `${this.config.claudePath} ${claudeArgs.map(a => `'${a.replace(/'/g, "'\\''")}'`).join(' ')}`;
98
97
  const tmuxCmd = [
99
98
  this.config.tmuxPath,
100
99
  'new-session',
@@ -269,9 +268,9 @@ export class SessionManager extends EventEmitter {
269
268
  return tmuxSession;
270
269
  }
271
270
 
272
- // Unset ANTHROPIC_* env vars so Claude uses OAuth (subscription) not API key
271
+ // Respect the user's configured auth method (API key or OAuth subscription)
273
272
  const claudeCmd = `${this.config.claudePath} --dangerously-skip-permissions`;
274
- const shellCmd = `cd '${this.config.projectDir}' && unset ANTHROPIC_API_KEY ANTHROPIC_ADMIN_KEY CLAUDECODE && ${claudeCmd}`;
273
+ const shellCmd = `cd '${this.config.projectDir}' && ${claudeCmd}`;
275
274
  const tmuxCmd = `${this.config.tmuxPath} new-session -d -s '${tmuxSession}' -x 200 -y 50 'bash -c "${shellCmd.replace(/"/g, '\\"')}"'`;
276
275
 
277
276
  try {