shellmates 0.1.0 → 0.1.3

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
@@ -1,131 +1,115 @@
1
1
  # shellmates
2
2
 
3
- Your terminal. Multiple AI models. All talking to each other.
3
+ <div align="center">
4
+ <img src="docs/logo.png" alt="shellmates" width="720" />
5
+ </div>
4
6
 
5
- ```
6
- ┌──────────────────────────┬──────────────────────────┬──────────────────────────┐
7
- │ claude │ gemini │ codex │
8
- │ │ │ │
9
- │ Planning phase 3... │ Reading the plan... │ Reading the plan... │
10
- │ Delegating to agents... │ Writing auth.py... │ Writing tests... │
11
- │ Waiting for signals... │ Tests passing ✓ │ All passing ✓ │
12
- │ Nice. On to phase 4. │ PHASE_COMPLETE: done │ PHASE_COMPLETE: done │
13
- └──────────────────────────┴──────────────────────────┴──────────────────────────┘
14
- ```
15
-
16
- Claude plans. Gemini builds. Codex verifies. They coordinate through your terminal using nothing but tmux — no APIs between them, no glue code, just agents passing messages like coworkers at adjacent desks.
17
-
18
- ---
7
+ <br />
19
8
 
20
- ## What
9
+ Your terminal. Multiple AI agents. All talking to each other.
21
10
 
22
- shellmates is a lightweight orchestration layer for your terminal. It connects Claude Code, Gemini CLI, and Codex in side-by-side tmux panes — Claude holds the plan and delegates, the others execute in parallel and signal back when done.
11
+ ```
12
+ npm install -g shellmates
13
+ ```
23
14
 
24
- No new framework. No shared memory. No API calls between models. Just text in a terminal.
15
+ Claude plans. Gemini builds. Codex verifies. They coordinate through your terminal using tmux — no APIs between them, no glue code, just agents passing tasks like coworkers at adjacent desks.
25
16
 
26
17
  ---
27
18
 
28
- ## Why
29
-
30
- Every AI coding tool runs one model in one context. You hit a wall when the task gets big — context fills up, the model loses the thread, architectural decisions get buried in implementation noise.
31
-
32
- shellmates splits the work the way a good team does:
33
-
34
- - **One agent thinks.** Claude holds the plan, reviews the work, decides what's next. Uses [GSD](https://github.com/gsd-build/get-shit-done) to produce structured plans that sub-agents can execute without needing your entire conversation history.
35
- - **Other agents build.** Gemini and Codex get a fresh context, a clear plan, and a specific job. They commit, signal done, and wait.
36
- - **The terminal is the meeting room.** tmux `send-keys` delivers tasks. `capture-pane` reads the replies. That's the whole protocol.
19
+ ![shellmates demo](docs/demo.gif)
37
20
 
38
21
  ---
39
22
 
40
23
  ## How it works
41
24
 
42
25
  ```
43
- You describe what to build
44
-
45
- Claude plans it with /gsd:plan-phase → PLAN.md on disk
46
-
47
- Claude sends the plan to Gemini → tmux send-keys
48
- Claude sends the plan to Codex → tmux send-keys (in parallel)
26
+ shellmates spawn --task "Add dark mode" --agent gemini
49
27
 
50
- Gemini implements, tests, commits
51
- Codex implements, tests, commits (simultaneously)
28
+ shellmates opens a tmux pane, launches Gemini, hands it the task
52
29
 
53
- Both signal: PHASE_COMPLETE → Claude reads with capture-pane
30
+ Gemini implements, tests, writes results to ~/.shellmates/inbox/
54
31
 
55
- Claude reviews, decides next step
32
+ Claude gets a native notification (no polling) → reads the result → decides what's next
56
33
 
57
34
  repeat
58
35
  ```
59
36
 
60
- The plan lives on disk. Sub-agents read it fresh every time. No conversation history required, no context bleed between agents, no awkward handoffs.
37
+ The agent runs in its own isolated tmux session. You stay in your pane. When it's done, Claude wakes up automatically via a PostToolUse hook not because it kept checking.
61
38
 
62
39
  ---
63
40
 
64
- ![shellmates demo](docs/demo.gif)
41
+ ## Get started
42
+
43
+ ```bash
44
+ shellmates init # create config and directories
45
+ shellmates install-hook # wire up native Claude Code notifications (do this once)
46
+ shellmates config # set your default agent and permission mode
47
+ ```
48
+
49
+ Then dispatch a task:
50
+
51
+ ```bash
52
+ shellmates spawn --task "Add dark mode to the settings page"
53
+ shellmates spawn --task-file plan.md --agent codex --watch
54
+ ```
65
55
 
66
56
  ---
67
57
 
68
- ## Get started
58
+ ## Commands
69
59
 
70
- **Point your AI agent at this:**
60
+ | Command | What it does |
61
+ |---|---|
62
+ | `shellmates init` | First-time setup — create `~/.shellmates/` and default config |
63
+ | `shellmates config` | Change default agent, orchestrator, and permission mode |
64
+ | `shellmates spawn` | Dispatch a task to a worker agent in a new tmux session |
65
+ | `shellmates status` | Show active sessions, config, and inbox results |
66
+ | `shellmates install-hook` | Install the Claude Code PostToolUse hook for native notifications |
67
+ | `shellmates teardown` | Kill shellmates tmux sessions |
68
+ | `shellmates update` | Update to the latest version |
69
+
70
+ **Spawn options:**
71
71
 
72
72
  ```
73
- Read https://raw.githubusercontent.com/rs07-git/shellmates/main/INIT.md and set up shellmates for this project.
73
+ -t, --task <text> Inline task text
74
+ -f, --task-file <path> Path to a task file
75
+ -a, --agent <name> gemini | codex (overrides default)
76
+ -s, --session <name> tmux session name
77
+ -p, --project <path> Working directory for the agent (default: cwd)
78
+ -w, --watch Wait and print result when agent finishes
74
79
  ```
75
80
 
76
- That's it. Your agent will install the tools, update your project files, fill in the config, and drop a personalized tutorial in your terminal. No manual steps required.
77
-
78
- > Works with Claude Code, Gemini CLI, Codex, or any AI that can read a URL and run shell commands.
79
-
80
81
  ---
81
82
 
82
- ## Want to understand what's happening first?
83
+ ## The notification hook
83
84
 
84
- **→ [QUICKSTART.md](QUICKSTART.md)**step-by-step walkthrough you can follow yourself
85
+ Without the hook, Claude has to poll for results checking the inbox every few seconds, burning tokens doing nothing useful.
85
86
 
86
- ---
87
+ With `shellmates install-hook`, a PostToolUse hook script watches for inbox files in the background and uses Claude Code's `asyncRewake` mechanism to deliver a native notification when the agent finishes. Claude wakes up exactly once, reads the result, and moves on.
87
88
 
88
- ## Mix and match
89
+ ```bash
90
+ shellmates install-hook
91
+ ```
89
92
 
90
- | Orchestrator | Executor(s) | Good for |
91
- |---|---|---|
92
- | Claude | Gemini | Large context tasks, Google Search grounding |
93
- | Claude | Codex | Sandboxed execution, internal multi-agent roles |
94
- | Claude | Gemini + Codex | Parallel tracks — implement and verify simultaneously |
95
- | Claude | Multiple Gemini panes | Fan-out across many files at once |
93
+ Run it once. It installs `~/.claude/hooks/shellmates-notify.sh` and adds the hook entry to `~/.claude/settings.json` automatically.
96
94
 
97
95
  ---
98
96
 
99
- ## What's in the box
97
+ ## Mix and match
100
98
 
101
- ```
102
- shellmates/
103
- ├── INIT.md ← agent-executable setup (point your AI here)
104
- ├── QUICKSTART.md ← human-readable setup guide
105
- ├── ORCHESTRATOR.md ← Claude's operating instructions (copied to your project)
106
- ├── templates/
107
- │ ├── CLAUDE.md ← snippet added to your project's CLAUDE.md
108
- │ ├── GEMINI.md ← filled in and added to your project root
109
- │ ├── AGENTS.md ← for Codex
110
- │ └── .codex/ ← Codex multi-agent role configs
111
- ├── scripts/
112
- │ ├── launch.sh ← spin up a 2-pane session
113
- │ ├── launch-full-team.sh ← spin up a 4-pane session
114
- │ └── monitor.sh ← watch for signals in the background
115
- └── docs/
116
- ├── WORKFLOW.md ← the plan/execute split explained
117
- ├── PROTOCOL.md ← full tmux IPC reference
118
- ├── ROLES.md ← patterns and when to use each
119
- └── TROUBLESHOOTING.md
120
- ```
99
+ | Orchestrator | Worker(s) | Good for |
100
+ |---|---|---|
101
+ | Claude Code | Gemini CLI | Large context tasks, long-running implementations |
102
+ | Claude Code | Codex CLI | Sandboxed execution, isolated environments |
103
+ | Claude Code | Gemini + Codex | Parallel tracks — build and verify simultaneously |
104
+ | Claude Code | Multiple Gemini panes | Fan-out across many files or components |
121
105
 
122
106
  ---
123
107
 
124
- ## The protocol in one paragraph
125
-
126
- Claude sends a task by running `tmux send-keys -t pane "do X" Enter`. The sub-agent does the work and prints `PHASE_COMPLETE: Phase N — summary` when done. Claude reads the output with `tmux capture-pane -t pane -p | tail -20`. No framework, no SDK, no shared state. Just text in a terminal.
108
+ ## Requirements
127
109
 
128
- Full spec in [docs/PROTOCOL.md](docs/PROTOCOL.md).
110
+ - Node 18+
111
+ - tmux
112
+ - At least one of: [Gemini CLI](https://github.com/google-gemini/gemini-cli), [Codex CLI](https://github.com/openai/codex)
129
113
 
130
114
  ---
131
115
 
package/bin/shellmates.js CHANGED
@@ -7,10 +7,62 @@ import { fileURLToPath } from 'url'
7
7
  const __dirname = dirname(fileURLToPath(import.meta.url))
8
8
  const pkg = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf8'))
9
9
 
10
+ // ── Bare call or top-level --help/-h → custom welcome screen ─────────────────
11
+ const args = process.argv.slice(2)
12
+ const isWelcome = args.length === 0 || args[0] === '--help' || args[0] === '-h' || args[0] === 'help'
13
+
14
+ if (isWelcome) {
15
+ const chalk = (await import('chalk')).default
16
+ const { printLogo } = await import('../lib/utils/logo.js')
17
+ const { checkUpdate } = await import('../lib/utils/update-check.js')
18
+ const { existsSync } = await import('fs')
19
+ const { join: pathJoin } = await import('path')
20
+ const { homedir } = await import('os')
21
+
22
+ printLogo(pkg.version)
23
+
24
+ const cmd = (name, desc) =>
25
+ ` ${chalk.bold(name.padEnd(16))}${chalk.dim(desc)}`
26
+
27
+ console.log(chalk.dim(' COMMANDS'))
28
+ console.log(cmd('init', 'First-time setup — create config and directories'))
29
+ console.log(cmd('config', 'Configure agents, orchestrator, and permission mode'))
30
+ console.log(cmd('spawn', 'Dispatch a task to a worker agent in a new tmux session'))
31
+ console.log(cmd('status', 'Show active sessions and inbox results'))
32
+ console.log(cmd('install-hook', 'Wire up native Claude Code AGENT_PING notifications'))
33
+ console.log(cmd('teardown', 'Kill shellmates tmux sessions'))
34
+ console.log(cmd('update', 'Update shellmates to the latest version'))
35
+ console.log('')
36
+ console.log(chalk.dim(' EXAMPLES'))
37
+ console.log(` ${chalk.dim('shellmates spawn --task "Add dark mode" --agent gemini')}`)
38
+ console.log(` ${chalk.dim('shellmates spawn --task-file plan.md --watch')}`)
39
+ console.log(` ${chalk.dim('shellmates status')}`)
40
+ console.log('')
41
+ console.log(' ' + chalk.dim('shellmates <command> --help') + chalk.dim(' for command details.'))
42
+ console.log('')
43
+
44
+ // First-run nudge
45
+ const configPath = pathJoin(homedir(), '.shellmates', 'config.json')
46
+ if (!existsSync(configPath)) {
47
+ console.log(chalk.yellow(' ~ Not set up yet.') + ' Run ' + chalk.bold('shellmates init') + ' to get started.')
48
+ console.log('')
49
+ }
50
+
51
+ // Update notice (non-blocking, cached — won't slow you down)
52
+ const update = await checkUpdate(pkg.version)
53
+ if (update) {
54
+ console.log(chalk.cyan(` ✨ Update available: ${chalk.dim(update.current)} → ${chalk.bold(update.latest)}`))
55
+ console.log(chalk.dim(' shellmates update'))
56
+ console.log('')
57
+ }
58
+
59
+ process.exit(0)
60
+ }
61
+
10
62
  program
11
63
  .name('shellmates')
12
64
  .description('Seamless tmux multi-agent orchestration')
13
- .version(pkg.version)
65
+ .version(pkg.version, '-v, --version', 'Print version number')
14
66
 
15
67
  // ── shellmates init ──────────────────────────────────────────────────────────
16
68
  program
@@ -74,6 +126,15 @@ program
74
126
  await installHook(opts)
75
127
  })
76
128
 
129
+ // ── shellmates update ────────────────────────────────────────────────────────
130
+ program
131
+ .command('update')
132
+ .description('Update shellmates to the latest version')
133
+ .action(async () => {
134
+ const { update } = await import('../lib/commands/update.js')
135
+ await update()
136
+ })
137
+
77
138
  // ── shellmates teardown ──────────────────────────────────────────────────────
78
139
  program
79
140
  .command('teardown [session]')
@@ -1,36 +1,40 @@
1
1
  import chalk from 'chalk'
2
2
  import inquirer from 'inquirer'
3
3
  import { readConfig, writeConfig, ensureDirs } from '../utils/config.js'
4
+ import { AGENTS } from '../utils/agents.js'
4
5
 
5
- const AGENT_CHOICES = [
6
- { name: 'Gemini CLI (google/gemini-cli)', value: 'gemini' },
7
- { name: 'Codex CLI (openai/codex)', value: 'codex' },
8
- { name: 'Ask me each time', value: 'ask' },
9
- ]
6
+ const AGENT_CHOICES = Object.entries(AGENTS).map(([value, a]) => ({
7
+ name: `${a.label.padEnd(14)} ${chalk.dim(a.hint)}`,
8
+ value,
9
+ }))
10
10
 
11
11
  const ORCHESTRATOR_CHOICES = [
12
- { name: 'Claude Code (this session)', value: 'claude' },
13
- { name: 'Gemini CLI', value: 'gemini' },
14
- { name: 'Codex CLI', value: 'codex' },
12
+ { name: `Claude Code ${chalk.dim('(this session)')}`, value: 'claude' },
13
+ { name: `Gemini CLI ${chalk.dim('gemini')}`, value: 'gemini' },
14
+ { name: `Codex CLI ${chalk.dim('codex')}`, value: 'codex' },
15
15
  ]
16
16
 
17
17
  export async function config() {
18
18
  ensureDirs()
19
19
  const current = readConfig()
20
20
 
21
+ // Migrate old single-string format → array
22
+ const currentAgents = current.default_agents
23
+ || (current.default_agent ? [current.default_agent] : ['gemini'])
24
+
21
25
  console.log('')
22
26
  console.log(chalk.bold(' Shellmates — Settings'))
23
- console.log(chalk.dim(' ────────────────────────────────────'))
24
- console.log(chalk.dim(` Current config: ~/.shellmates/config.json`))
27
+ console.log(chalk.dim(' ─────────────────────────────────────'))
25
28
  console.log('')
26
29
 
27
30
  const answers = await inquirer.prompt([
28
31
  {
29
- type: 'list',
30
- name: 'default_agent',
31
- message: 'Default worker agent:',
32
+ type: 'checkbox',
33
+ name: 'default_agents',
34
+ message: 'Default worker agent(s):',
32
35
  choices: AGENT_CHOICES,
33
- default: current.default_agent,
36
+ default: currentAgents,
37
+ validate: (ans) => ans.length > 0 || 'Select at least one agent.',
34
38
  prefix: ' ',
35
39
  },
36
40
  {
@@ -47,11 +51,11 @@ export async function config() {
47
51
  message: 'Permission mode:',
48
52
  choices: [
49
53
  {
50
- name: 'default — agents ask before modifying files or running commands',
54
+ name: `default ${chalk.dim('— agents ask before modifying files or running commands')}`,
51
55
  value: 'default',
52
56
  },
53
57
  {
54
- name: 'bypass — agents run fully autonomously (gemini --yolo, codex --full-auto)',
58
+ name: `bypass ${chalk.dim('— agents run fully autonomously (gemini --yolo, codex --full-auto)')}`,
55
59
  value: 'bypass',
56
60
  },
57
61
  ],
@@ -68,7 +72,6 @@ export async function config() {
68
72
  },
69
73
  ])
70
74
 
71
- // If user chose bypass but didn't confirm, revert to default
72
75
  if (answers.permission_mode === 'bypass' && answers.bypass_confirmed === false) {
73
76
  answers.permission_mode = 'default'
74
77
  console.log('')
@@ -81,8 +84,8 @@ export async function config() {
81
84
  console.log('')
82
85
  console.log(chalk.green(' ✓') + ' Settings saved.')
83
86
  console.log('')
84
- console.log(chalk.dim(' permission_mode: ') + chalk.bold(toSave.permission_mode))
85
- console.log(chalk.dim(' default_agent: ') + chalk.bold(toSave.default_agent))
87
+ console.log(chalk.dim(' default_agents: ') + chalk.bold(toSave.default_agents.join(', ')))
86
88
  console.log(chalk.dim(' orchestrator: ') + chalk.bold(toSave.orchestrator))
89
+ console.log(chalk.dim(' permission_mode: ') + chalk.bold(toSave.permission_mode))
87
90
  console.log('')
88
91
  }
@@ -1,21 +1,12 @@
1
1
  import chalk from 'chalk'
2
2
  import { existsSync } from 'fs'
3
- import { CONFIG_PATH, CONFIG_DIR, INBOX_DIR, readConfig, writeConfig, ensureDirs } from '../utils/config.js'
3
+ import { CONFIG_PATH, writeConfig, ensureDirs } from '../utils/config.js'
4
4
  import { tmuxAvailable } from '../utils/tmux.js'
5
- import { printLogo } from '../utils/logo.js'
5
+ import { checkAndInstallAgents } from '../utils/agents.js'
6
6
 
7
7
  export async function init({ force = false } = {}) {
8
- const { readFileSync } = await import('fs')
9
- const { join, dirname, fileURLToPath } = await import('path')
10
- const { fileURLToPath: fu } = await import('url')
11
- let version = '0.1.0'
12
- try {
13
- const __dirname = dirname(fu(import.meta.url))
14
- const pkg = JSON.parse(readFileSync(join(__dirname, '..', '..', 'package.json'), 'utf8'))
15
- version = pkg.version
16
- } catch {}
17
-
18
- printLogo(version)
8
+ console.log('')
9
+ console.log(chalk.bold(' Shellmates Setup'))
19
10
  console.log(chalk.dim(' ─────────────────────────────'))
20
11
  console.log('')
21
12
 
@@ -27,6 +18,11 @@ export async function init({ force = false } = {}) {
27
18
  process.exit(1)
28
19
  }
29
20
  console.log(chalk.green(' ✓') + ' tmux found')
21
+ console.log('')
22
+
23
+ // Agent detection + optional install
24
+ await checkAndInstallAgents()
25
+ console.log('')
30
26
 
31
27
  // Create dirs
32
28
  ensureDirs()
@@ -35,12 +31,12 @@ export async function init({ force = false } = {}) {
35
31
 
36
32
  // Config
37
33
  if (existsSync(CONFIG_PATH) && !force) {
38
- console.log(chalk.yellow(' ~') + ` Config already exists at ${CONFIG_PATH}`)
34
+ console.log(chalk.yellow(' ~') + ` Config already exists`)
39
35
  console.log(chalk.dim(' Run with --force to reset, or use: shellmates config'))
40
36
  } else {
41
37
  writeConfig({
42
38
  permission_mode: 'default',
43
- default_agent: 'gemini',
39
+ default_agents: ['gemini'],
44
40
  orchestrator: 'claude',
45
41
  })
46
42
  console.log(chalk.green(' ✓') + ` Config created at ${CONFIG_PATH}`)
@@ -50,7 +46,8 @@ export async function init({ force = false } = {}) {
50
46
  console.log(chalk.bold(' Ready.'))
51
47
  console.log('')
52
48
  console.log(' Next steps:')
53
- console.log(chalk.dim(' shellmates config') + ' — configure agents and permission mode')
54
- console.log(chalk.dim(' shellmates spawn') + ' dispatch a task to a worker agent')
49
+ console.log(chalk.dim(' shellmates config') + ' — configure agents and permission mode')
50
+ console.log(chalk.dim(' shellmates install-hook') + ' wire up native Claude notifications')
51
+ console.log(chalk.dim(' shellmates spawn') + ' — dispatch a task to a worker agent')
55
52
  console.log('')
56
53
  }
@@ -0,0 +1,105 @@
1
+ import chalk from 'chalk'
2
+ import { writeFileSync, chmodSync, existsSync } from 'fs'
3
+ import { join } from 'path'
4
+ import { tmpdir, homedir } from 'os'
5
+ import { execSync, spawnSync } from 'child_process'
6
+ import { readConfig, CONFIG_PATH } from '../utils/config.js'
7
+ import { tmuxAvailable } from '../utils/tmux.js'
8
+
9
+ const ORCHESTRATOR_CMDS = {
10
+ claude: (promptFile) => `claude "$(cat ${promptFile})"`,
11
+ gemini: (promptFile) => `gemini -p "$(cat ${promptFile})"`,
12
+ }
13
+
14
+ function buildPrompt(agents, project) {
15
+ const agentList = agents.join(', ')
16
+ const projectNote = project ? `The user's project is at: ${project}` : ''
17
+
18
+ return `You are the shellmates orchestrator — a coordinating AI that helps users accomplish software development goals by delegating work to specialized AI agent executors.
19
+
20
+ Your job is NOT to write code yourself. You plan, clarify, and dispatch.
21
+
22
+ Start with a single warm, brief greeting and ask the user what they want to work on today. Keep it to one or two sentences — don't explain your role, just start the conversation naturally.
23
+
24
+ When the user shares their goal:
25
+ 1. Ask clarifying questions if needed (scope, affected files, tech stack, constraints)
26
+ 2. Once you have enough context, break the work into discrete, self-contained tasks
27
+ 3. Dispatch each task using the Bash tool: shellmates spawn --task "precise task description" --agent <agent>
28
+ 4. When an executor finishes you'll receive an AGENT_PING in your terminal — review the result, then decide what comes next
29
+ 5. Repeat until the goal is complete
30
+
31
+ Dispatch rules:
32
+ - Only spawn when you have a specific, actionable task — not vague intentions
33
+ - gemini: best for large-context work, long implementations, reading many files
34
+ - codex: best for sandboxed execution, isolated environments, focused rewrites
35
+ - You can dispatch multiple agents in parallel if tasks are truly independent
36
+ - Never spawn the same task twice — verify before re-dispatching
37
+
38
+ Available agents: ${agentList}
39
+ Dispatch command: shellmates spawn --task "..." --agent gemini|codex
40
+ Check sessions: shellmates status
41
+ ${projectNote}`
42
+ }
43
+
44
+ export async function pond(options) {
45
+ if (!existsSync(CONFIG_PATH)) {
46
+ console.log(chalk.yellow('\n ~ Not initialized yet.'))
47
+ console.log(' Run ' + chalk.bold('shellmates init') + ' first.\n')
48
+ process.exit(1)
49
+ }
50
+
51
+ if (!tmuxAvailable()) {
52
+ console.log(chalk.red('\n ✗ tmux not found. Install: brew install tmux\n'))
53
+ process.exit(1)
54
+ }
55
+
56
+ const config = readConfig()
57
+ const orchestrator = config.orchestrator || 'claude'
58
+ const agents = config.default_agents || (config.default_agent ? [config.default_agent] : ['gemini'])
59
+ const project = options.project || process.cwd()
60
+
61
+ if (!ORCHESTRATOR_CMDS[orchestrator]) {
62
+ console.log(chalk.red(`\n ✗ Orchestrator "${orchestrator}" doesn't support pond mode yet.`))
63
+ console.log(chalk.dim(' Set orchestrator to "claude" in shellmates config.\n'))
64
+ process.exit(1)
65
+ }
66
+
67
+ const ts = Date.now()
68
+ const sessionName = options.session || `shellmates-pond-${ts}`
69
+
70
+ // Write prompt to temp file (avoids shell escaping issues)
71
+ const promptFile = join(tmpdir(), `shellmates-pond-${ts}.txt`)
72
+ writeFileSync(promptFile, buildPrompt(agents, project))
73
+
74
+ // Write launcher script
75
+ const launchFile = join(tmpdir(), `shellmates-pond-${ts}.sh`)
76
+ const orchCmd = ORCHESTRATOR_CMDS[orchestrator](promptFile)
77
+ writeFileSync(launchFile, `#!/bin/bash\ncd ${JSON.stringify(project)}\n${orchCmd}\n`)
78
+ chmodSync(launchFile, 0o755)
79
+
80
+ console.log('')
81
+ console.log(chalk.bold(' Shellmates — Pond'))
82
+ console.log(chalk.dim(' ─────────────────────────────────────'))
83
+ console.log(chalk.dim(' Orchestrator: ') + chalk.bold(orchestrator))
84
+ console.log(chalk.dim(' Agents: ') + chalk.bold(agents.join(', ')))
85
+ console.log(chalk.dim(' Project: ') + chalk.dim(project))
86
+ console.log(chalk.dim(' Session: ') + chalk.dim(sessionName))
87
+ console.log('')
88
+ console.log(chalk.dim(' Starting pond... attach with Ctrl+A (or configured prefix)'))
89
+ console.log('')
90
+
91
+ // Create detached tmux session
92
+ try {
93
+ execSync(`tmux new-session -d -s ${JSON.stringify(sessionName)} -x 220 -y 50`, { stdio: 'ignore' })
94
+ } catch {
95
+ console.log(chalk.red(` ✗ Could not create tmux session (already exists?)`))
96
+ console.log(chalk.dim(` Try: tmux attach -t ${sessionName}\n`))
97
+ process.exit(1)
98
+ }
99
+
100
+ // Launch orchestrator
101
+ execSync(`tmux send-keys -t ${JSON.stringify(sessionName)} ${JSON.stringify(launchFile)} Enter`)
102
+
103
+ // Attach — this takes over the terminal
104
+ spawnSync('tmux', ['attach-session', '-t', sessionName], { stdio: 'inherit' })
105
+ }
@@ -1,28 +1,42 @@
1
1
  import chalk from 'chalk'
2
2
  import { spawnSync } from 'child_process'
3
- import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs'
3
+ import { existsSync, readFileSync, writeFileSync } from 'fs'
4
4
  import { join, dirname } from 'path'
5
5
  import { fileURLToPath } from 'url'
6
6
  import { tmpdir, homedir } from 'os'
7
- import { readConfig } from '../utils/config.js'
7
+ import { readConfig, CONFIG_PATH } from '../utils/config.js'
8
8
 
9
9
  const __dirname = dirname(fileURLToPath(import.meta.url))
10
10
  const SCRIPTS_DIR = join(__dirname, '..', '..', 'scripts')
11
11
  const INBOX_DIR = join(homedir(), '.shellmates', 'inbox')
12
12
 
13
13
  export async function spawn(options) {
14
+ if (!existsSync(CONFIG_PATH)) {
15
+ console.log(chalk.yellow('\n ~ Not initialized yet.'))
16
+ console.log(' Run ' + chalk.bold('shellmates init') + ' first.\n')
17
+ process.exit(1)
18
+ }
19
+
14
20
  const config = readConfig()
15
21
 
16
- const agent = options.agent || config.default_agent
17
- const session = options.session || `shellmates-${Date.now()}`
22
+ // Resolve agents: --agent flag (single) overrides config; config may have array
23
+ let agents
24
+ if (options.agent) {
25
+ agents = [options.agent]
26
+ } else {
27
+ // Support both old default_agent and new default_agents
28
+ agents = config.default_agents
29
+ || (config.default_agent ? [config.default_agent] : ['gemini'])
30
+ }
31
+
32
+ const ts = Date.now()
18
33
  const project = options.project || process.cwd()
19
34
  const noPing = options.noPing || false
20
35
 
21
- // Resolve task content
36
+ // Resolve task file
22
37
  let taskFile = options.taskFile
23
38
  if (!taskFile && options.task) {
24
- // Inline task string → write to temp file
25
- const tmp = join(tmpdir(), `shellmates-task-${Date.now()}.txt`)
39
+ const tmp = join(tmpdir(), `shellmates-task-${ts}.txt`)
26
40
  writeFileSync(tmp, options.task)
27
41
  taskFile = tmp
28
42
  }
@@ -32,67 +46,94 @@ export async function spawn(options) {
32
46
  process.exit(1)
33
47
  }
34
48
 
49
+ const parallel = agents.length > 1
50
+
35
51
  console.log('')
36
52
  console.log(chalk.bold(' Shellmates — Spawning'))
37
53
  console.log(chalk.dim(' ─────────────────────────────────────'))
38
- console.log(chalk.dim(' Agent: ') + chalk.bold(agent))
39
- console.log(chalk.dim(' Session: ') + chalk.bold(session))
40
- console.log(chalk.dim(' Mode: ') + chalk.bold(config.permission_mode))
54
+ if (parallel) {
55
+ console.log(chalk.dim(' Agents: ') + chalk.bold(agents.join(' + ')))
56
+ console.log(chalk.dim(' Mode: parallel'))
57
+ } else {
58
+ console.log(chalk.dim(' Agent: ') + chalk.bold(agents[0]))
59
+ }
60
+ console.log(chalk.dim(' Perms: ') + chalk.bold(config.permission_mode))
41
61
  console.log('')
42
62
 
43
63
  const spawnScript = join(SCRIPTS_DIR, 'spawn-team.sh')
44
- const args = [
45
- '--task-file', taskFile,
46
- '--project', project,
47
- '--session', session,
48
- '--agent', agent,
49
- ]
50
- if (noPing) args.push('--no-ping')
51
-
52
- const result = spawnSync('bash', [spawnScript, ...args], { stdio: 'inherit' })
53
-
54
- if (result.status !== 0) {
55
- console.error(chalk.red('\n ✗ Spawn failed'))
56
- process.exit(result.status || 1)
64
+ const sessions = []
65
+
66
+ for (const agent of agents) {
67
+ const session = options.session
68
+ ? (agents.length > 1 ? `${options.session}-${agent}` : options.session)
69
+ : `shellmates-${agent}-${ts}`
70
+
71
+ const args = [
72
+ '--task-file', taskFile,
73
+ '--project', project,
74
+ '--session', session,
75
+ '--agent', agent,
76
+ ]
77
+ if (noPing) args.push('--no-ping')
78
+
79
+ const result = spawnSync('bash', [spawnScript, ...args], { stdio: 'inherit' })
80
+
81
+ if (result.status !== 0) {
82
+ console.error(chalk.red(`\n ✗ Spawn failed for ${agent}`))
83
+ process.exit(result.status || 1)
84
+ }
85
+
86
+ sessions.push(session)
87
+ }
88
+
89
+ if (parallel) {
90
+ console.log('')
91
+ console.log(chalk.dim(' Sessions:'))
92
+ for (const s of sessions) {
93
+ console.log(chalk.dim(' • ') + s)
94
+ }
57
95
  }
58
96
 
59
- // If --watch, tail the inbox until the job file appears
60
97
  if (options.watch) {
61
- await watchInbox(session)
98
+ await watchInbox(sessions)
62
99
  }
63
100
  }
64
101
 
65
- async function watchInbox(session) {
102
+ async function watchInbox(sessions) {
66
103
  const { readdir } = await import('fs/promises')
67
104
  console.log('')
68
- console.log(chalk.dim(' Watching for result... (Ctrl+C to stop)'))
105
+ console.log(chalk.dim(' Watching for results... (Ctrl+C to stop)'))
69
106
 
70
107
  const before = new Set(existsSync(INBOX_DIR)
71
108
  ? (await readdir(INBOX_DIR)).filter(f => f.endsWith('.txt'))
72
109
  : [])
73
110
 
111
+ const remaining = new Set(sessions)
74
112
  let elapsed = 0
75
113
  const timeout = 300
76
- while (elapsed < timeout) {
114
+
115
+ while (elapsed < timeout && remaining.size > 0) {
77
116
  await new Promise(r => setTimeout(r, 2000))
78
117
  elapsed += 2
79
118
  if (!existsSync(INBOX_DIR)) continue
80
119
  const after = (await readdir(INBOX_DIR)).filter(f => f.endsWith('.txt'))
81
120
  const newFiles = after.filter(f => !before.has(f))
82
- if (newFiles.length > 0) {
83
- for (const f of newFiles) {
84
- const content = readFileSync(join(INBOX_DIR, f), 'utf8')
85
- console.log('')
86
- console.log(chalk.green(' ✓ Result received') + chalk.dim(` (${f})`))
87
- console.log('')
88
- for (const line of content.trim().split('\n')) {
89
- console.log(' ' + chalk.dim(line.split(':')[0] + ':') + ' ' + line.split(':').slice(1).join(':').trim())
90
- }
91
- console.log('')
121
+ for (const f of newFiles) {
122
+ const content = readFileSync(join(INBOX_DIR, f), 'utf8')
123
+ const matchingSession = sessions.find(s => f.includes(s))
124
+ console.log('')
125
+ console.log(chalk.green(' ✓ Result received') + chalk.dim(` (${matchingSession || f})`))
126
+ console.log('')
127
+ for (const line of content.trim().split('\n')) {
128
+ const [key, ...rest] = line.split(':')
129
+ console.log(' ' + chalk.dim(key + ':') + ' ' + rest.join(':').trim())
92
130
  }
93
- return
131
+ if (matchingSession) remaining.delete(matchingSession)
132
+ before.add(f)
94
133
  }
95
134
  }
96
135
 
97
- console.log(chalk.yellow('\n ~ Timed out waiting for result'))
136
+ if (remaining.size > 0) {
137
+ console.log(chalk.yellow('\n ~ Timed out waiting for: ' + [...remaining].join(', ')))
138
+ }
98
139
  }
@@ -0,0 +1,28 @@
1
+ import chalk from 'chalk'
2
+ import { spawnSync } from 'child_process'
3
+
4
+ export async function update() {
5
+ console.log('')
6
+ console.log(chalk.bold(' Shellmates — Update'))
7
+ console.log(chalk.dim(' ─────────────────────────────────────'))
8
+ console.log('')
9
+ console.log(chalk.dim(' Running: npm install -g shellmates@latest'))
10
+ console.log('')
11
+
12
+ const result = spawnSync('npm', ['install', '-g', 'shellmates@latest'], {
13
+ stdio: 'inherit',
14
+ shell: true,
15
+ })
16
+
17
+ if (result.status === 0) {
18
+ console.log('')
19
+ console.log(chalk.green(' ✓') + ' Updated. Run ' + chalk.bold('shellmates') + ' to see the new version.')
20
+ console.log('')
21
+ } else {
22
+ console.log('')
23
+ console.log(chalk.red(' ✗') + ' Update failed.')
24
+ console.log(chalk.dim(' Try manually: npm install -g shellmates@latest'))
25
+ console.log('')
26
+ process.exit(1)
27
+ }
28
+ }
@@ -0,0 +1,88 @@
1
+ import { execSync, spawnSync } from 'child_process'
2
+ import chalk from 'chalk'
3
+
4
+ export const AGENTS = {
5
+ gemini: {
6
+ label: 'Gemini CLI',
7
+ bin: 'gemini',
8
+ pkg: '@google/gemini-cli',
9
+ hint: 'google/gemini-cli',
10
+ },
11
+ codex: {
12
+ label: 'Codex CLI',
13
+ bin: 'codex',
14
+ pkg: '@openai/codex',
15
+ hint: 'openai/codex',
16
+ },
17
+ claude: {
18
+ label: 'Claude Code',
19
+ bin: 'claude',
20
+ pkg: '@anthropic-ai/claude-code',
21
+ hint: 'anthropic-ai/claude-code',
22
+ },
23
+ }
24
+
25
+ /** Returns { gemini: true, codex: false, claude: true } */
26
+ export function detectAgents() {
27
+ const results = {}
28
+ for (const [key, agent] of Object.entries(AGENTS)) {
29
+ try {
30
+ execSync(`which ${agent.bin}`, { stdio: 'ignore' })
31
+ results[key] = true
32
+ } catch {
33
+ results[key] = false
34
+ }
35
+ }
36
+ return results
37
+ }
38
+
39
+ /** Print detection results and offer to install missing agents. */
40
+ export async function checkAndInstallAgents() {
41
+ const { default: inquirer } = await import('inquirer')
42
+ const detected = detectAgents()
43
+ const missing = Object.entries(detected).filter(([, ok]) => !ok).map(([k]) => k)
44
+
45
+ console.log(' Checking agents...')
46
+ for (const [key, ok] of Object.entries(detected)) {
47
+ const a = AGENTS[key]
48
+ if (ok) {
49
+ console.log(chalk.green(' ✓') + ` ${a.label.padEnd(14)} ${chalk.dim(a.hint)}`)
50
+ } else {
51
+ console.log(chalk.red(' ✗') + ` ${a.label.padEnd(14)} ${chalk.dim('not found')}`)
52
+ }
53
+ }
54
+
55
+ if (missing.length === 0) return
56
+
57
+ console.log('')
58
+ const { toInstall } = await inquirer.prompt([
59
+ {
60
+ type: 'checkbox',
61
+ name: 'toInstall',
62
+ message: 'Install missing agents?',
63
+ choices: missing.map(k => ({
64
+ name: `${AGENTS[k].label} ${chalk.dim(AGENTS[k].hint)}`,
65
+ value: k,
66
+ checked: true,
67
+ })),
68
+ prefix: ' ',
69
+ },
70
+ ])
71
+
72
+ if (toInstall.length === 0) return
73
+
74
+ console.log('')
75
+ for (const key of toInstall) {
76
+ const a = AGENTS[key]
77
+ console.log(chalk.dim(` Installing ${a.label}...`))
78
+ const result = spawnSync('npm', ['install', '-g', `${a.pkg}@latest`], {
79
+ stdio: 'inherit',
80
+ shell: true,
81
+ })
82
+ if (result.status === 0) {
83
+ console.log(chalk.green(' ✓') + ` ${a.label} installed`)
84
+ } else {
85
+ console.log(chalk.yellow(' ~') + ` ${a.label} install failed — try: ${chalk.dim(`npm install -g ${a.pkg}@latest`)}`)
86
+ }
87
+ }
88
+ }
@@ -0,0 +1,47 @@
1
+ import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'fs'
2
+ import { join } from 'path'
3
+ import { homedir } from 'os'
4
+
5
+ const CACHE_PATH = join(homedir(), '.shellmates', 'update-cache.json')
6
+ const TTL_MS = 24 * 60 * 60 * 1000 // 24 hours
7
+
8
+ export async function checkUpdate(currentVersion) {
9
+ // Try cache first
10
+ let latestVersion = null
11
+ if (existsSync(CACHE_PATH)) {
12
+ try {
13
+ const cache = JSON.parse(readFileSync(CACHE_PATH, 'utf8'))
14
+ if (Date.now() - cache.checkedAt < TTL_MS) {
15
+ latestVersion = cache.latest
16
+ }
17
+ } catch {}
18
+ }
19
+
20
+ if (!latestVersion) {
21
+ try {
22
+ const res = await fetch('https://registry.npmjs.org/shellmates/latest', {
23
+ signal: AbortSignal.timeout(3000),
24
+ })
25
+ const data = await res.json()
26
+ latestVersion = data.version
27
+ try {
28
+ mkdirSync(join(homedir(), '.shellmates'), { recursive: true })
29
+ writeFileSync(CACHE_PATH, JSON.stringify({ latest: latestVersion, checkedAt: Date.now() }))
30
+ } catch {}
31
+ } catch {
32
+ return null // network error or timeout — silent
33
+ }
34
+ }
35
+
36
+ if (!latestVersion || !isNewer(latestVersion, currentVersion)) return null
37
+ return { current: currentVersion, latest: latestVersion }
38
+ }
39
+
40
+ function isNewer(a, b) {
41
+ const p = v => v.replace(/[^0-9.]/g, '').split('.').map(Number)
42
+ const [aM, am, ap] = p(a)
43
+ const [bM, bm, bp] = p(b)
44
+ if (aM !== bM) return aM > bM
45
+ if (am !== bm) return am > bm
46
+ return ap > bp
47
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "shellmates",
3
- "version": "0.1.0",
3
+ "version": "0.1.3",
4
4
  "description": "Seamless tmux multi-agent orchestration for Claude, Gemini, and Codex",
5
5
  "keywords": [
6
6
  "tmux",
@@ -14,7 +14,7 @@
14
14
  "homepage": "https://github.com/rs07-git/shellmates",
15
15
  "repository": {
16
16
  "type": "git",
17
- "url": "https://github.com/rs07-git/shellmates.git"
17
+ "url": "git+https://github.com/rs07-git/shellmates.git"
18
18
  },
19
19
  "license": "MIT",
20
20
  "type": "module",