coban 0.0.0-stage → 0.2.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 cenkerarin
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,194 @@
1
- # Temporary Holding Version
1
+ # coban
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ [![ci](https://github.com/cenkerarin/coban/actions/workflows/ci.yml/badge.svg)](https://github.com/cenkerarin/coban/actions/workflows/ci.yml)
4
+ [![license: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
5
+
6
+ A terminal app (think [herdr](https://www.bitdoze.com/herdr-agent-multiplexer/)) where **one orchestrator agent
7
+ dispatches work to many AI coding agents** — several Claude Code accounts, Codex, OpenCode, Cursor — and those
8
+ agents **talk to each other** over direct messages and a shared board.
9
+
10
+ *Çoban* is Turkish for shepherd. The UI labels and the prompts coban gives agents are in Turkish for now.
11
+
12
+ ![coban: conversation feed, agent graph and chat](docs/screenshot.svg)
13
+
14
+ <sub>Rendered by `npm run snapshot` with scripted demo agents. Left: who said what to whom. Middle: the orchestrator
15
+ and its workers — the dots on the edges are messages in flight. Right: the focused agent's real terminal.</sub>
16
+
17
+ > **Status: early (v0.2).** Its author runs it with three Claude accounts; Codex, OpenCode and Cursor
18
+ > profiles are wired up but barely tested. Issues and PRs welcome.
19
+
20
+ ## How it works
21
+
22
+ - **One screen, three columns.** A conversation feed (messages, board notes, lifecycle events), the agent graph,
23
+ and the focused agent's terminal — your chat. Under 140 columns the feed hides; under 100 the graph stacks above
24
+ the chat.
25
+ - **Real agents, real terminals.** Every agent runs in its own PTY; coban emulates the screen (xterm) and reads a
26
+ status from it: `working`, `blocked` (permission prompt), `limited` (usage limit), `idle`, `exited`.
27
+ - **The orchestrator drives a CLI, not an MCP server.** Pick its profile with the `+` bubble; it starts with a
28
+ system prompt that teaches it the `coban` command, which talks to the app over a unix socket. Anything that can
29
+ run a shell command can take part.
30
+ - **Alive, not busy.** Messages travel along the edges as packets (board notes fan out to everyone), working agents
31
+ breathe, blocked ones blink, new ones fade in. Frames are drawn only while something moves.
32
+
33
+ ```
34
+ coban agents agents, model, status
35
+ coban map [subdir] repo map: files, line counts, symbol:line
36
+ coban spawn <profile> "<task>" [--name x] [--cwd dir] [--ctx where]...
37
+ --ctx "file#sym1,sym2" | "file:10-80" | "file"
38
+ inlines that code (line-numbered) into the task
39
+ coban post <id|user|all> "<message>" direct message (queued until idle) or a board note (all)
40
+ coban inbox your direct messages + board notes you haven't seen
41
+ coban board [--limit n] recent board notes
42
+ coban wait [id...] [--timeout s] returns when an agent settles or mail arrives
43
+ coban read <id> [--lines n] [--raw] an agent's screen, UI chrome stripped
44
+ coban approvals agents waiting on a permission question, parsed
45
+ coban approve <id> [yes|always|no] answer it (always = the "Yes, and …" option)
46
+ coban key <id> <keys...> enter esc up down tab y 1 2 ...
47
+ coban usage per-account usage, least spent first
48
+ coban kill <id> · coban log every command takes --json
49
+ ```
50
+
51
+ ## Install
52
+
53
+ Requires Node ≥ 24 on macOS or Linux.
54
+
55
+ ```sh
56
+ npm install -g coban
57
+ cd ~/your-project && coban
58
+ ```
59
+
60
+ ### First run
61
+
62
+ On first start coban writes `~/.config/coban/config.json` and only offers what it finds: `claude`, `codex`,
63
+ `opencode` and `cursor-agent` that are on your `PATH`. A plain `claude` login (`~/.claude`) becomes profile `h1`.
64
+ More Claude accounts appear as `h2`/`h3` when `~/.claude-hesap2` / `~/.claude-hesap3` exist (log in with
65
+ `CLAUDE_CONFIG_DIR=~/.claude-hesap2 claude`). Edit the file any time, see Config below.
66
+
67
+ Note: the prompts coban gives the orchestrator and workers, and `coban --help`, are currently in Turkish.
68
+
69
+ ### Troubleshooting
70
+
71
+ | symptom | fix |
72
+ |---|---|
73
+ | `SyntaxError` / `ERR_UNKNOWN_FILE_EXTENSION` on start | Node is too old, check `node -v` (needs ≥ 24) |
74
+ | agent panel opens but asks to log in | log in once with that CLI (`claude`, `codex`...) outside coban, using the same `CLAUDE_CONFIG_DIR` as the profile |
75
+ | a profile is missing | its CLI is not on `PATH` when coban first ran: install it and delete the config file, or add the profile by hand |
76
+ | `coban çalışmıyor` from a `coban ...` command | open the TUI first with plain `coban`: those commands talk to the running app |
77
+ | stale socket after a crash | `rm ~/.coban/coban.sock` |
78
+ | Codex cannot talk to coban | its sandbox may block the unix socket, see Known gaps |
79
+
80
+ ### From source (development)
81
+
82
+ In a clone, TypeScript runs directly with no build step:
83
+
84
+ ```sh
85
+ git clone https://github.com/cenkerarin/coban && cd coban
86
+ npm install
87
+ ln -s "$PWD/bin/coban" ~/.local/bin/coban
88
+ cd ~/your-project && coban
89
+ ```
90
+
91
+ `npm run build` compiles to `dist/`, which `bin/coban` prefers when present.
92
+
93
+ ## Config
94
+
95
+ `~/.config/coban/config.json` is created on first run. A profile is a command plus env vars, so several accounts
96
+ are just different `CLAUDE_CONFIG_DIR`s:
97
+
98
+ ```json
99
+ {
100
+ "orchestrator": "h1",
101
+ "orchestratorModel": "claude-sonnet-5-5",
102
+ "profiles": [
103
+ { "id": "h1", "title": "Claude H1", "cmd": "claude --allowedTools 'Bash(coban:*)'", "model": "claude-opus-5-5",
104
+ "promptArg": "-- {prompt}", "env": { "CLAUDE_CONFIG_DIR": "~/.claude-hesap1" }, "color": "#F6D5C8" },
105
+ { "id": "h2", "title": "Claude H2", "cmd": "claude --allowedTools 'Bash(coban:*)'", "model": "claude-sonnet-5-5",
106
+ "promptArg": "-- {prompt}", "env": { "CLAUDE_CONFIG_DIR": "~/.claude-hesap2" }, "limit5h": 4000000 },
107
+ { "id": "codex", "title": "Codex", "cmd": "codex", "promptArg": "{prompt}" }
108
+ ]
109
+ }
110
+ ```
111
+
112
+ `model` is appended as `--model <model>`; `orchestratorModel` overrides it for the profile running as orchestrator —
113
+ coordination is mostly reading and routing, so a cheaper model there saves the most.
114
+
115
+ ## Accounts and limits
116
+
117
+ `coban usage` (and the bars in the status bar) read each Claude profile's session logs
118
+ (`$CLAUDE_CONFIG_DIR/projects/**/*.jsonl`) and sum input + output + cache-creation tokens over 5 hours and 7 days.
119
+ The percentage is against the profile's `limit5h`; plans don't publish a token number, so the 5M default is a guess —
120
+ tune it per account. The orchestrator is told to give new work to the least-spent account.
121
+
122
+ When a worker hits a usage limit, coban **hands its task off** to the least-spent account of the same kind: the new
123
+ worker gets the original task plus the old worker's last screen, undelivered mail follows it, and the board and the
124
+ orchestrator are told.
125
+
126
+ ## Approvals and the input line
127
+
128
+ - **One approval inbox.** When agents stop at a permission question, coban reads it off their screen — title,
129
+ command or file, and the exact options — and lists them in a panel under the graph. `ctrl+b y` approves the
130
+ selected one, `ctrl+b a` picks its "Yes, and …" option (shown verbatim, so you see what it grants), `ctrl+b r`
131
+ rejects, `ctrl+b i` moves to the next; or click the buttons. The orchestrator can do the same with
132
+ `coban approvals` / `coban approve`. Answers are given by moving the menu selection with arrow keys and pressing
133
+ Enter (never by digit: Claude Code's folder-trust menu has no numbers and defaults to "No, exit"), and an answer
134
+ is refused if the question changed since it was shown.
135
+ - **coban's own input line** sits under the chat: `ctrl+b c` (or a click) focuses it. `@h2 also add a refresh
136
+ endpoint` goes to h2 without switching focus, `@all …` to the board, plain text to the focused agent; Tab
137
+ completes agent names, Esc returns the keyboard to the agent.
138
+
139
+ The parser is built against real Claude Code screens (`test/fixtures/prompts`). Driving agents through the Agent SDK
140
+ would give exact events instead of screen reading, but its docs ask third-party tools not to offer claude.ai login,
141
+ so coban keeps running the unmodified Claude Code with your own login.
142
+
143
+ ## Agents talking
144
+
145
+ - **Direct messages** (`coban post <id>`) are typed into the recipient's terminal once it is idle.
146
+ - **The board** (`coban post all`) is for things everyone should know — changed interfaces, who is on which file,
147
+ decisions. Notes are pulled, not pushed: `coban inbox` returns each note once, and workers are told to check it
148
+ after every major step and before editing shared files, so idle agents aren't woken (and billed) for every note.
149
+ It is mirrored to `.coban/board.md`; `ctrl+b m` opens it as a panel.
150
+ - Agents opened by hand show as `manuel`; the orchestrator is told to use idle ones before spawning new workers, and
151
+ coban prepends the worker brief to the first message they get.
152
+
153
+ ## Keeping context small
154
+
155
+ Each request an agent makes re-reads its whole context (Claude Code alone is ~45k tokens before any work), so coban
156
+ keeps what lands there lean:
157
+
158
+ - one-line CLI output; `coban read` returns 20 lines with agent UI chrome stripped;
159
+ - workers send ≤5-line summaries and put long results in `.coban/reports/<id>.md` (git-ignored automatically);
160
+ - the orchestrator learns the repo from `coban map` (≈400 tokens for this repo) and hands workers the relevant code
161
+ with `--ctx` — coban extracts symbols/line ranges itself (no model involved) so workers don't spend requests
162
+ searching;
163
+ - tasks follow a template: **HEDEF / DOSYALAR / KISITLAR / DOĞRULAMA / BİTTİ SAYILIR**
164
+ (goal, files, constraints, verification, done when).
165
+
166
+ ## Keys
167
+
168
+ Mouse: click a bubble to focus it, click `+` to add an agent, scroll the feed with the wheel. Mouse events in the
169
+ chat are forwarded when the agent asks for them.
170
+
171
+ `ctrl+b` then: `c` input line · `y`/`a`/`r` approve / always / reject · `i` next approval · `n` new agent · `1-9` select · `j`/`k` next/prev · `o` orchestrator · `m` board panel ·
172
+ `l` toggle feed · `s` select mode (release the mouse to copy text) · `u` next unread · `x` close agent · `q` quit ·
173
+ `ctrl+b` send a literal ctrl+b.
174
+
175
+ ## Development
176
+
177
+ ```sh
178
+ npm test # unit tests + an end-to-end run of the real TUI in a pty with fake agents
179
+ npm run typecheck
180
+ npm run snapshot # regenerate docs/screenshot.svg
181
+ ```
182
+
183
+ See [CONTRIBUTING.md](CONTRIBUTING.md) for how the code is laid out.
184
+
185
+ ## Known gaps
186
+
187
+ No scrollback in the chat, no split view, no kitty keyboard protocol (shift+enter), no session restore yet.
188
+ Codex's sandbox may block the unix socket. Status detection is screen heuristics and may need tuning per CLI
189
+ version. coban captures the mouse: to copy text press `ctrl+b s`, or use your terminal's override (shift-drag in
190
+ Ghostty/cmux, ⌥-drag in iTerm2).
191
+
192
+ ## License
193
+
194
+ MIT
package/bin/coban ADDED
@@ -0,0 +1,10 @@
1
+ #!/bin/sh
2
+ # Resolve the real script location so symlinks (e.g. ~/.local/bin/coban) work.
3
+ SELF=$(readlink -f "$0" 2>/dev/null || echo "$0")
4
+ ROOT="$(dirname "$SELF")/.."
5
+ if [ -f "$ROOT/dist/cli.js" ]; then
6
+ ENTRY="$ROOT/dist/cli.js"
7
+ else
8
+ ENTRY="$ROOT/src/cli.ts"
9
+ fi
10
+ exec node --disable-warning=ExperimentalWarning "$ENTRY" "$@"
package/dist/agent.js ADDED
@@ -0,0 +1,161 @@
1
+ import pty from 'node-pty';
2
+ import xterm from '@xterm/headless';
3
+ import { EventEmitter } from 'node:events';
4
+ import { parsePrompt } from './prompt.js';
5
+ const { Terminal } = xterm;
6
+ // Screen heuristics. Checked against the bottom of the visible screen.
7
+ const BLOCKED = /Do you want to (proceed|make this edit|create|allow|run)|Would you like to (run|make|proceed|allow)|Allow (this|command|once)|Approve\b.*\?|Press enter to confirm|trust (this|the files in this) folder|Do you trust/i;
8
+ // A plain y/n question only counts while it is still the line being answered; tools rarely clear it afterwards.
9
+ const YN = /\(y\/n\)|\[y\/N\]|\[Y\/n\]/;
10
+ // A CLI's own limit banner: a short line of its own, optionally behind one bullet/box glyph, sitting in the last few
11
+ // non-chrome lines of the screen (just above the idle prompt). Line-anchored on purpose: the same words inside a task
12
+ // text, a prompt echo ('> …'), a quote or mid-sentence tool output must never trigger a handoff.
13
+ const LIMIT_BANNER = /^\s{0,4}(?:[⎿●•✻*·]\s{0,4})?(?:(?:Claude(?: AI)? )?usage limit reached|(?:5-hour|weekly|session|daily|opus|sonnet) limit reached|You(?:'|’)?ve (?:hit|reached) your (?:\w+[ -])?(?:usage )?limit|You have (?:hit|reached) your (?:\w+[ -])?(?:usage )?limit|You(?:'|’)re out of (?:credits|extra usage))(?:\s*$|[\s.!,:;|∙·–—(-])/i;
14
+ const LIMIT_CHROME = /^\s*([─━═╌┄\-]{8,}|[╭╰│┃].*|❯.*|>.*|[⏵►▶]{2}.*|\? for shortcuts.*)$/;
15
+ const LIMIT_TAIL_LINES = 4;
16
+ // Broad form, only for masking quoted screen text (handoff tasks) so it can't re-trigger a limit in the new worker.
17
+ const LIMIT_WORDS = /(?:(?:usage|rate) limit|limit reached|(?:hit|reached) your[^\n]{0,24}?limit|out of (?:credits|extra usage))/gi;
18
+ // A question is live only while nothing has been printed after it: either it sits above an open selection menu
19
+ // (❯ …), or it is one of the last two non-empty lines. Answered questions that linger in the scrollback don't count.
20
+ export function waitingForAnswer(screen) {
21
+ const p = parsePrompt(screen);
22
+ if (p?.kind === 'yn')
23
+ return true;
24
+ if (p?.kind === 'menu' && BLOCKED.test(screen))
25
+ return true;
26
+ const lastTwo = screen.split('\n').filter((l) => l.trim()).slice(-2).join('\n');
27
+ return BLOCKED.test(lastTwo) || YN.test(lastTwo);
28
+ }
29
+ export function limitBanner(screen) {
30
+ const lines = screen.split('\n').filter((l) => l.trim() && !LIMIT_CHROME.test(l)).slice(-LIMIT_TAIL_LINES);
31
+ return lines.some((l) => l.length <= 200 && LIMIT_BANNER.test(l));
32
+ }
33
+ export function maskLimitText(text) {
34
+ return text.replace(LIMIT_WORDS, (m) => `[${m.replace(/limit/gi, 'l*mit')}]`);
35
+ }
36
+ const BUSY = /esc to interrupt|esc to cancel|ctrl\+c to (interrupt|stop)|Working \(|Thinking\.\.\./i;
37
+ const QUIET_MS = 1500;
38
+ export class Agent extends EventEmitter {
39
+ id;
40
+ profile;
41
+ role;
42
+ task;
43
+ cwd;
44
+ term;
45
+ proc;
46
+ status = 'starting';
47
+ settledSeq = 0; // bumps every time the agent settles (idle/blocked/limited/exited)
48
+ unread = false; // settled while not focused
49
+ lastOutput = Date.now();
50
+ cursorVisible = true;
51
+ exitCode = null;
52
+ inbox = [];
53
+ model = '';
54
+ briefed = false; // has received the coban worker brief
55
+ handedOff = false; // hit a limit and its task was already handed to another account (hub.handoff)
56
+ constructor(o) {
57
+ super();
58
+ this.id = o.id;
59
+ this.profile = o.profile;
60
+ this.role = o.role;
61
+ this.task = o.task;
62
+ this.cwd = o.cwd;
63
+ this.term = new Terminal({ cols: o.cols, rows: o.rows, scrollback: 2000, allowProposedApi: true });
64
+ // Track DECTCEM (cursor show/hide); headless xterm doesn't expose it.
65
+ this.term.parser.registerCsiHandler({ prefix: '?', final: 'h' }, (p) => { if (p.includes(25))
66
+ this.cursorVisible = true; return false; });
67
+ this.term.parser.registerCsiHandler({ prefix: '?', final: 'l' }, (p) => { if (p.includes(25))
68
+ this.cursorVisible = false; return false; });
69
+ // /bin/sh, not the user's shell: profiles are plain commands and sh exists everywhere (zsh is missing on most Linux).
70
+ // `exec` keeps the agent as the pty's direct child, but `exec VAR=x cmd` would try to run "VAR=x": skip it then.
71
+ this.proc = pty.spawn('/bin/sh', ['-c', /^\s*\w+=/.test(o.cmd) ? o.cmd : `exec ${o.cmd}`], {
72
+ name: 'xterm-256color', cols: o.cols, rows: o.rows, cwd: o.cwd, env: o.env,
73
+ });
74
+ this.proc.onData((d) => {
75
+ this.lastOutput = Date.now();
76
+ this.term.write(d, () => this.emit('render'));
77
+ });
78
+ // Replies to terminal queries (DA, DSR, ...) go back to the program.
79
+ this.term.onData((d) => { if (this.exitCode === null)
80
+ this.proc.write(d); });
81
+ this.proc.onExit(({ exitCode }) => {
82
+ this.exitCode = exitCode;
83
+ this.setStatus('exited');
84
+ this.emit('render');
85
+ });
86
+ }
87
+ screenText(lines = 40, scrollback = false) {
88
+ const buf = this.term.buffer.active;
89
+ // Drop blank rows below the content first, then take the last `lines` rows.
90
+ let end = buf.length;
91
+ while (end > 0 && !buf.getLine(end - 1)?.translateToString(true).trim())
92
+ end--;
93
+ const start = Math.max(scrollback ? 0 : buf.baseY, end - lines);
94
+ const out = [];
95
+ for (let y = start; y < end; y++)
96
+ out.push(buf.getLine(y)?.translateToString(true) ?? '');
97
+ return out.join('\n');
98
+ }
99
+ tick() {
100
+ if (this.status === 'exited')
101
+ return;
102
+ const tail = this.screenText(18);
103
+ let next;
104
+ if (limitBanner(tail) && !BUSY.test(tail))
105
+ next = 'limited';
106
+ else if (waitingForAnswer(tail))
107
+ next = 'blocked';
108
+ // A visible "esc to interrupt" means real work, even right after launch (workers get their task as an argv
109
+ // prompt and never pass through idle). Bare output without it is still startup noise.
110
+ else if (BUSY.test(tail))
111
+ next = 'working';
112
+ else if (Date.now() - this.lastOutput < QUIET_MS)
113
+ next = this.status === 'starting' ? 'starting' : 'working';
114
+ else
115
+ next = 'idle';
116
+ this.setStatus(next);
117
+ }
118
+ setStatus(s) {
119
+ if (s === this.status)
120
+ return;
121
+ const prev = this.status;
122
+ this.status = s;
123
+ if (s !== 'working' && s !== 'starting')
124
+ this.settledSeq++;
125
+ this.emit('status', s, prev);
126
+ }
127
+ // Type a prompt into the agent and submit it.
128
+ deliver(text) {
129
+ if (this.exitCode !== null)
130
+ return;
131
+ const body = this.term.modes.bracketedPasteMode ? `\x1b[200~${text}\x1b[201~` : text.replace(/\s*\n\s*/g, ' ');
132
+ this.proc.write(body);
133
+ setTimeout(() => this.proc.write('\r'), 250);
134
+ this.lastOutput = Date.now();
135
+ this.setStatus('working');
136
+ }
137
+ resize(cols, rows) {
138
+ if (cols === this.term.cols && rows === this.term.rows)
139
+ return;
140
+ this.term.resize(cols, rows);
141
+ if (this.exitCode === null)
142
+ this.proc.resize(cols, rows);
143
+ }
144
+ kill() {
145
+ if (this.exitCode === null)
146
+ this.proc.kill();
147
+ this.term.dispose();
148
+ }
149
+ }
150
+ const KEYS = {
151
+ enter: '\r', esc: '\x1b', escape: '\x1b', tab: '\t', 'shift-tab': '\x1b[Z', space: ' ',
152
+ backspace: '\x7f', 'ctrl-c': '\x03', 'ctrl-d': '\x04',
153
+ up: '\x1b[A', down: '\x1b[B', right: '\x1b[C', left: '\x1b[D',
154
+ };
155
+ export function keyBytes(agent, key) {
156
+ const k = key.toLowerCase();
157
+ if (agent.term.modes.applicationCursorKeysMode && /^(up|down|left|right)$/.test(k)) {
158
+ return '\x1bO' + { up: 'A', down: 'B', right: 'C', left: 'D' }[k];
159
+ }
160
+ return KEYS[k] ?? key;
161
+ }
package/dist/cli.js ADDED
@@ -0,0 +1,201 @@
1
+ import net from 'node:net';
2
+ import { loadConfig, SOCK } from './config.js';
3
+ import { repoMap, snippets } from './context.js';
4
+ import { allUsage } from './usage.js';
5
+ const HELP = `coban — çok hesaplı agent orkestratörü
6
+
7
+ coban [--cwd dizin] arayüzü aç (orkestratörü graf alanındaki + ile seç)
8
+
9
+ Agent'ların içinden (veya başka bir terminalden):
10
+ coban agents agent listesi ve durumları
11
+ coban spawn <profil> "<görev>" [--name ad] [--cwd dizin] [--ctx yer]...
12
+ --ctx: "dosya:10-80" | "dosya#sembol1,sembol2" | "dosya"
13
+ ilgili kodu satır numaralı olarak görevin içine ekler (send/post'ta da var)
14
+ coban map [alt-dizin] repo haritası: dosyalar, satır sayıları, semboller:satır
15
+ coban send <id> "<mesaj>" (post ile aynı)
16
+ coban post <id|user|all> "<mesaj>" all = ortak kutu (herkes görür)
17
+ coban wait [id...] [--timeout sn] biri durunca / mesaj gelince döner
18
+ coban read <id> [--lines n] [--scrollback] [--raw]
19
+ coban approvals izin bekleyen agent'lar ve soruları
20
+ coban approve <id> [yes|always|no] izni yanıtla (varsayılan yes)
21
+ coban key <id> <tuş...> enter esc up down tab 1 2 y ...
22
+ coban kill <id>
23
+ coban inbox özel mesajlar + ortak kutudaki yeniler
24
+ coban board [--limit n] ortak kutunun son notları
25
+ coban usage hesap kullanımı (5s %, 7g token, reset), en az harcayan başta
26
+ coban log [--limit n]
27
+
28
+ Her komut --json ile ham çıktı verir.`;
29
+ function call(method, params) {
30
+ return new Promise((resolve, reject) => {
31
+ const s = net.connect(SOCK);
32
+ let buf = '';
33
+ s.on('connect', () => s.write(JSON.stringify({ caller: process.env.COBAN_AGENT || 'user', method, params }) + '\n'));
34
+ s.on('data', (d) => {
35
+ buf += d;
36
+ const i = buf.indexOf('\n');
37
+ if (i < 0)
38
+ return;
39
+ const res = JSON.parse(buf.slice(0, i));
40
+ s.end();
41
+ res.ok ? resolve(res.result) : reject(new Error(res.error));
42
+ });
43
+ s.on('error', (e) => reject(e.code === 'ENOENT' || e.code === 'ECONNREFUSED' ? new Error('coban çalışmıyor (önce `coban` ile arayüzü aç)') : e));
44
+ });
45
+ }
46
+ const BOOL_FLAGS = ['scrollback', 'raw', 'json'];
47
+ function parse(argv) {
48
+ const pos = [];
49
+ const flags = {};
50
+ const ctx = []; // --ctx may repeat
51
+ for (let i = 0; i < argv.length; i++) {
52
+ const a = argv[i];
53
+ if (a.startsWith('--')) {
54
+ const [k, v] = a.slice(2).split('=', 2);
55
+ if (k === 'ctx') {
56
+ const val = v ?? argv[++i];
57
+ if (val)
58
+ ctx.push(val);
59
+ continue;
60
+ }
61
+ if (v !== undefined)
62
+ flags[k] = v;
63
+ else if (argv[i + 1] && !argv[i + 1].startsWith('--') && !BOOL_FLAGS.includes(k))
64
+ flags[k] = argv[++i];
65
+ else
66
+ flags[k] = true;
67
+ }
68
+ else
69
+ pos.push(a);
70
+ }
71
+ return { pos, flags, ctx };
72
+ }
73
+ async function main() {
74
+ const [cmd, ...rest] = process.argv.slice(2);
75
+ const { pos, flags, ctx } = parse(rest);
76
+ const num = (v) => (v === undefined || v === true ? undefined : Number(v));
77
+ if (!cmd || cmd === '--cwd') {
78
+ if (cmd === '--cwd')
79
+ process.chdir(rest[0]);
80
+ const { Hub } = await import('./hub.js');
81
+ const { Tui } = await import('./tui.js');
82
+ try {
83
+ await call('agents', {});
84
+ console.error('coban zaten açık (soket: ' + SOCK + ')');
85
+ process.exit(1);
86
+ }
87
+ catch { }
88
+ const hub = new Hub(loadConfig());
89
+ const tui = new Tui(hub);
90
+ hub.size = tui.paneSize;
91
+ hub.listen();
92
+ tui.start();
93
+ const quit = () => { tui.stop(); hub.shutdown(); process.exit(0); };
94
+ process.on('SIGTERM', quit);
95
+ process.on('SIGHUP', quit);
96
+ process.on('uncaughtException', (e) => { tui.stop(); hub.shutdown(); console.error(e); process.exit(1); });
97
+ return;
98
+ }
99
+ // Output is plain one-liners on purpose: it lands in the calling agent's context. --json gives the raw result.
100
+ const json = !!flags.json;
101
+ const print = (r, text) => console.log(json ? JSON.stringify(r) : text());
102
+ const short = (m) => (m ? m.replace(/^claude-/, '') : '');
103
+ const one = (s) => s.replace(/\s+/g, ' ').trim();
104
+ const agentLine = (a) => [a.id, `${a.profile}${a.model ? '/' + short(a.model) : ''}`, a.role === 'orchestrator' ? '★' : a.role === 'manual' ? 'manuel' : '', a.status,
105
+ a.unreadMessages ? `✉${a.unreadMessages}` : '', a.boardUnread ? `▤${a.boardUnread}` : '', a.cwd ? `cwd=${a.cwd}` : '', a.task ? `"${a.task}"` : '']
106
+ .filter(Boolean).join(' ');
107
+ switch (cmd) {
108
+ case 'agents': {
109
+ const r = await call('agents', {});
110
+ return print(r, () => [...r.agents.map(agentLine), 'profiller: ' + r.profiles.map((p) => `${p.id}${p.model ? '/' + short(p.model) : ''}`).join(' ')].join('\n'));
111
+ }
112
+ case 'spawn': {
113
+ const cwd = typeof flags.cwd === 'string' ? flags.cwd : process.cwd();
114
+ const r = await call('spawn', { profile: pos[0], task: pos.slice(1).join(' ') + snippets(ctx, cwd), name: flags.name, cwd: flags.cwd });
115
+ return print(r, () => `açıldı: ${r.id}`);
116
+ }
117
+ case 'send':
118
+ case 'post': {
119
+ const r = await call('post', { to: pos[0], text: pos.slice(1).join(' ') + snippets(ctx, process.cwd()) });
120
+ return print(r, () => `gönderildi → ${pos[0]}`);
121
+ }
122
+ case 'wait': {
123
+ const r = await call('wait', { ids: pos, timeout: num(flags.timeout) });
124
+ return print(r, () => {
125
+ if (r.timeout)
126
+ return 'zaman aşımı · ' + r.agents.map((a) => `${a.id}:${a.status}`).join(' ');
127
+ const out = [];
128
+ if (r.settled.length)
129
+ out.push('durdu: ' + r.settled.map((a) => `${a.id} ${a.status}`).join(', '));
130
+ if (r.removed.length)
131
+ out.push('artık yok: ' + r.removed.join(', '));
132
+ for (const m of r.messages)
133
+ out.push(`✉ ${m.from}: ${m.text}`);
134
+ for (const m of r.board ?? [])
135
+ out.push(`[ortak] ${m.from}: ${m.text}`);
136
+ return out.join('\n');
137
+ });
138
+ }
139
+ case 'read': {
140
+ const r = await call('read', { id: pos[0], lines: num(flags.lines), scrollback: !!flags.scrollback, raw: !!flags.raw });
141
+ return print(r, () => `[${r.id} · ${r.status}]\n${r.screen}`);
142
+ }
143
+ case 'key': {
144
+ const r = await call('key', { id: pos[0], keys: pos.slice(1) });
145
+ return print(r, () => 'tamam');
146
+ }
147
+ case 'approvals': {
148
+ const r = await call('approvals', {});
149
+ return print(r, () => r.approvals.length ? r.approvals.map((x) => {
150
+ const p = x.prompt;
151
+ if (!p)
152
+ return `${x.id} ${x.profile} soru okunamadı — coban read ${x.id}`;
153
+ const opts = p.options.map((o, i) => `${i + 1}) ${o.label}`).join(' ');
154
+ return [`${x.id} ${x.profile} ${p.title}${p.body.length ? ': ' + p.body.join(' · ') : ''}`, ` ? ${p.question}${opts ? ' ' + opts : ''}`].join('\n');
155
+ }).join('\n') : '(izin bekleyen yok)');
156
+ }
157
+ case 'approve': {
158
+ const r = await call('approve', { id: pos[0], choice: pos[1] ?? 'yes' });
159
+ return print(r, () => `${pos[0]}: "${r.picked}" seçildi`);
160
+ }
161
+ case 'kill': {
162
+ const r = await call('kill', { id: pos[0] });
163
+ return print(r, () => `kapatıldı: ${pos[0]}`);
164
+ }
165
+ case 'inbox': {
166
+ const r = await call('inbox', {});
167
+ const lines = [...r.messages.map((m) => `✉ ${m.from}: ${m.text}`), ...r.board.map((m) => `[ortak] ${m.from}: ${m.text}`)];
168
+ return print(r, () => (lines.length ? lines.join('\n') : '(yeni mesaj yok)'));
169
+ }
170
+ case 'board': {
171
+ const r = await call('board', { limit: num(flags.limit) });
172
+ return print(r, () => (r.board.length ? r.board.map((m) => `${new Date(m.ts).toTimeString().slice(0, 5)} ${m.from}: ${m.text}`).join('\n') : '(ortak kutu boş)'));
173
+ }
174
+ case 'log': {
175
+ const r = await call('log', { limit: num(flags.limit) });
176
+ return print(r, () => r.messages.map((m) => `${new Date(m.ts).toTimeString().slice(0, 5)} ${m.from}→${m.to}: ${one(m.text)}`).join('\n'));
177
+ }
178
+ case 'usage': {
179
+ // Works without a running coban too (then 'limited' is unknown → false).
180
+ const r = await call('usage', {}).catch(() => ({ usage: allUsage(loadConfig()) }));
181
+ const tok = (n) => (n >= 1e6 ? (n / 1e6).toFixed(1) + 'M' : n >= 1e3 ? Math.round(n / 1e3) + 'k' : String(n));
182
+ const line = (u) => !u.limit5h
183
+ ? [u.profileId, 'bilinmiyor', u.limited ? 'LİMİTLİ' : ''].filter(Boolean).join(' ')
184
+ : [u.profileId, `5s ${Math.round(u.pct5h * 100)}% (${tok(u.used5h)}/${tok(u.limit5h)})`, `7g ${tok(u.used7d)}`,
185
+ u.resetAt ? `reset ${new Date(u.resetAt).toTimeString().slice(0, 5)}` : 'reset -', u.limited ? 'LİMİTLİ' : ''].filter(Boolean).join(' ');
186
+ return print(r, () => r.usage.map(line).join('\n'));
187
+ }
188
+ case 'map':
189
+ console.log(repoMap(process.cwd(), pos[0] ?? ''));
190
+ return;
191
+ case 'help':
192
+ case '-h':
193
+ case '--help':
194
+ console.log(HELP);
195
+ return;
196
+ default:
197
+ console.error(HELP);
198
+ process.exit(2);
199
+ }
200
+ }
201
+ main().catch((e) => { console.error('coban: ' + e.message); process.exit(1); });