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 +65 -81
- package/bin/shellmates.js +62 -1
- package/lib/commands/config.js +22 -19
- package/lib/commands/init.js +14 -17
- package/lib/commands/pond.js +105 -0
- package/lib/commands/spawn.js +81 -40
- package/lib/commands/update.js +28 -0
- package/lib/utils/agents.js +88 -0
- package/lib/utils/update-check.js +47 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,131 +1,115 @@
|
|
|
1
1
|
# shellmates
|
|
2
2
|
|
|
3
|
-
|
|
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
|
-
|
|
9
|
+
Your terminal. Multiple AI agents. All talking to each other.
|
|
21
10
|
|
|
22
|
-
|
|
11
|
+
```
|
|
12
|
+
npm install -g shellmates
|
|
13
|
+
```
|
|
23
14
|
|
|
24
|
-
|
|
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
|
-
|
|
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
|
+

|
|
37
20
|
|
|
38
21
|
---
|
|
39
22
|
|
|
40
23
|
## How it works
|
|
41
24
|
|
|
42
25
|
```
|
|
43
|
-
|
|
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
|
-
|
|
51
|
-
Codex implements, tests, commits (simultaneously)
|
|
28
|
+
shellmates opens a tmux pane, launches Gemini, hands it the task
|
|
52
29
|
↓
|
|
53
|
-
|
|
30
|
+
Gemini implements, tests, writes results to ~/.shellmates/inbox/
|
|
54
31
|
↓
|
|
55
|
-
Claude
|
|
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
|
|
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
|
-
|
|
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
|
-
##
|
|
58
|
+
## Commands
|
|
69
59
|
|
|
70
|
-
|
|
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
|
-
|
|
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
|
-
##
|
|
83
|
+
## The notification hook
|
|
83
84
|
|
|
84
|
-
|
|
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
|
-
|
|
89
|
+
```bash
|
|
90
|
+
shellmates install-hook
|
|
91
|
+
```
|
|
89
92
|
|
|
90
|
-
|
|
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
|
-
##
|
|
97
|
+
## Mix and match
|
|
100
98
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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
|
-
##
|
|
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
|
-
|
|
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]')
|
package/lib/commands/config.js
CHANGED
|
@@ -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
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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:
|
|
13
|
-
{ name:
|
|
14
|
-
{ name:
|
|
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: '
|
|
30
|
-
name: '
|
|
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:
|
|
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:
|
|
54
|
+
name: `default ${chalk.dim('— agents ask before modifying files or running commands')}`,
|
|
51
55
|
value: 'default',
|
|
52
56
|
},
|
|
53
57
|
{
|
|
54
|
-
name:
|
|
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('
|
|
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
|
}
|
package/lib/commands/init.js
CHANGED
|
@@ -1,21 +1,12 @@
|
|
|
1
1
|
import chalk from 'chalk'
|
|
2
2
|
import { existsSync } from 'fs'
|
|
3
|
-
import { CONFIG_PATH,
|
|
3
|
+
import { CONFIG_PATH, writeConfig, ensureDirs } from '../utils/config.js'
|
|
4
4
|
import { tmuxAvailable } from '../utils/tmux.js'
|
|
5
|
-
import {
|
|
5
|
+
import { checkAndInstallAgents } from '../utils/agents.js'
|
|
6
6
|
|
|
7
7
|
export async function init({ force = false } = {}) {
|
|
8
|
-
|
|
9
|
-
|
|
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
|
|
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
|
-
|
|
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') + '
|
|
54
|
-
console.log(chalk.dim(' shellmates
|
|
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
|
+
}
|
package/lib/commands/spawn.js
CHANGED
|
@@ -1,28 +1,42 @@
|
|
|
1
1
|
import chalk from 'chalk'
|
|
2
2
|
import { spawnSync } from 'child_process'
|
|
3
|
-
import { existsSync, readFileSync, writeFileSync
|
|
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
|
-
|
|
17
|
-
|
|
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
|
|
36
|
+
// Resolve task file
|
|
22
37
|
let taskFile = options.taskFile
|
|
23
38
|
if (!taskFile && options.task) {
|
|
24
|
-
|
|
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
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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(
|
|
98
|
+
await watchInbox(sessions)
|
|
62
99
|
}
|
|
63
100
|
}
|
|
64
101
|
|
|
65
|
-
async function watchInbox(
|
|
102
|
+
async function watchInbox(sessions) {
|
|
66
103
|
const { readdir } = await import('fs/promises')
|
|
67
104
|
console.log('')
|
|
68
|
-
console.log(chalk.dim(' Watching for
|
|
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
|
-
|
|
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
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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
|
-
|
|
131
|
+
if (matchingSession) remaining.delete(matchingSession)
|
|
132
|
+
before.add(f)
|
|
94
133
|
}
|
|
95
134
|
}
|
|
96
135
|
|
|
97
|
-
|
|
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.
|
|
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",
|