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/dist/config.js ADDED
@@ -0,0 +1,82 @@
1
+ import fs from 'node:fs';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+ export const ROOT = path.resolve(path.dirname(new URL(import.meta.url).pathname), '..');
5
+ export const BIN_DIR = path.join(ROOT, 'bin');
6
+ export const STATE_DIR = path.join(os.homedir(), '.coban');
7
+ export const DEFAULT_SOCK = path.join(STATE_DIR, 'coban.sock');
8
+ export const SOCK = process.env.COBAN_SOCK || DEFAULT_SOCK;
9
+ export const CONFIG_PATH = process.env.COBAN_CONFIG || path.join(os.homedir(), '.config', 'coban', 'config.json');
10
+ export const OPUS = 'claude-opus-5-5';
11
+ export const SONNET = 'claude-sonnet-5-5';
12
+ // Claude may run `coban ...` without a permission prompt.
13
+ const CLAUDE_CMD = "claude --allowedTools 'Bash(coban:*)'";
14
+ const claude = (n, model, color) => ({
15
+ id: `h${n}`,
16
+ title: `Claude H${n}`,
17
+ cmd: CLAUDE_CMD,
18
+ model,
19
+ promptArg: '-- {prompt}',
20
+ env: { CLAUDE_CONFIG_DIR: `~/.claude-hesap${n}` },
21
+ color,
22
+ });
23
+ const onPath = (bin) => (process.env.PATH ?? '').split(path.delimiter).some((d) => d && fs.existsSync(path.join(d, bin)));
24
+ // First run on a fresh machine: only offer what is actually installed. A single `claude` login (~/.claude) becomes
25
+ // h1; extra accounts (h2, h3) appear only when their own config dir (~/.claude-hesapN) already exists.
26
+ export function detectConfig() {
27
+ const profiles = [];
28
+ if (onPath('claude')) {
29
+ const h1 = claude(1, OPUS, '#F6D5C8');
30
+ if (!fs.existsSync(path.join(os.homedir(), '.claude-hesap1')))
31
+ delete h1.env;
32
+ profiles.push(h1);
33
+ for (const n of [2, 3]) {
34
+ if (fs.existsSync(path.join(os.homedir(), `.claude-hesap${n}`)))
35
+ profiles.push(claude(n, SONNET, n === 2 ? '#FFD9A0' : '#C8E6C9'));
36
+ }
37
+ }
38
+ for (const p of DEFAULT_CONFIG.profiles) {
39
+ if (!p.id.match(/^h\d$/) && onPath(p.cmd.split(' ')[0]))
40
+ profiles.push(p);
41
+ }
42
+ if (!profiles.length)
43
+ return DEFAULT_CONFIG; // nothing found: keep the full template to edit
44
+ const orchestrator = profiles[0].id;
45
+ const cfg = { ...DEFAULT_CONFIG, orchestrator, profiles };
46
+ if (!/^h\d$/.test(orchestrator))
47
+ delete cfg.orchestratorModel; // the Sonnet override is a Claude model id
48
+ return cfg;
49
+ }
50
+ export const DEFAULT_CONFIG = {
51
+ orchestrator: 'h1',
52
+ // Coordination is mostly reading and routing, so the orchestrator runs on Sonnet;
53
+ // h1 keeps Opus for heavy coding work when it runs as a worker.
54
+ orchestratorModel: SONNET,
55
+ profiles: [
56
+ claude(1, OPUS, '#F6D5C8'),
57
+ claude(2, SONNET, '#FFD9A0'),
58
+ claude(3, SONNET, '#C8E6C9'),
59
+ { id: 'codex', title: 'Codex', cmd: 'codex', promptArg: '{prompt}', color: '#8FDBFF' },
60
+ { id: 'opencode', title: 'OpenCode', cmd: 'opencode', promptArg: '--prompt {prompt}', color: '#B5E48C' },
61
+ { id: 'cursor', title: 'Cursor', cmd: 'cursor-agent', promptArg: '{prompt}', color: '#D0B3FF' },
62
+ ],
63
+ };
64
+ const expand = (v) => v.replace(/^~(?=\/|$)/, os.homedir());
65
+ export function loadConfig() {
66
+ if (!fs.existsSync(CONFIG_PATH)) {
67
+ fs.mkdirSync(path.dirname(CONFIG_PATH), { recursive: true });
68
+ fs.writeFileSync(CONFIG_PATH, JSON.stringify(detectConfig(), null, 2) + '\n');
69
+ }
70
+ const cfg = JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf8'));
71
+ for (const p of cfg.profiles) {
72
+ // Older configs baked the model into cmd; lift it into `model` so overrides work.
73
+ const m = p.cmd.match(/\s--model[ =](\S+)/);
74
+ if (m) {
75
+ p.model ??= m[1];
76
+ p.cmd = p.cmd.replace(m[0], '');
77
+ }
78
+ for (const k of Object.keys(p.env ?? {}))
79
+ p.env[k] = expand(p.env[k]);
80
+ }
81
+ return cfg;
82
+ }
@@ -0,0 +1,174 @@
1
+ // Deterministic context packing: a cheap repo map for the orchestrator and code snippets inlined into worker tasks,
2
+ // so workers don't spend requests (each re-reading ~45k of baseline context) searching for code they were pointed at.
3
+ import fs from 'node:fs';
4
+ import path from 'node:path';
5
+ import { execFileSync } from 'node:child_process';
6
+ const CODE = /\.(ts|tsx|js|jsx|mjs|cjs|py|go|rs|swift|kt|java|rb|php|cs)$/;
7
+ const SKIP = /(^|\/)(node_modules|dist|build|\.git|\.next|vendor|Pods)\//;
8
+ const MAX_SNIPPET_LINES = 150;
9
+ const MAX_TOTAL_LINES = 400;
10
+ function listFiles(root) {
11
+ try {
12
+ return execFileSync('git', ['ls-files'], { cwd: root, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] })
13
+ .split('\n').filter(Boolean);
14
+ }
15
+ catch {
16
+ const out = [];
17
+ const walk = (d) => {
18
+ for (const e of fs.readdirSync(path.join(root, d), { withFileTypes: true })) {
19
+ const rel = d ? `${d}/${e.name}` : e.name;
20
+ if (e.name.startsWith('.') || SKIP.test(rel + '/'))
21
+ continue;
22
+ if (e.isDirectory())
23
+ walk(rel);
24
+ else
25
+ out.push(rel);
26
+ }
27
+ };
28
+ walk('');
29
+ return out;
30
+ }
31
+ }
32
+ // Top-level declarations and class members, with 1-based line numbers.
33
+ function symbols(src, file) {
34
+ const out = [];
35
+ const py = file.endsWith('.py');
36
+ src.split('\n').forEach((l, i) => {
37
+ let m = null;
38
+ if (py)
39
+ m = l.match(/^(?: {4})?(?:async\s+)?(?:def|class)\s+(\w+)/);
40
+ else {
41
+ m = l.match(/^(?:export\s+)?(?:default\s+)?(?:async\s+)?(?:function\*?|class|interface|type|enum|const|let)\s+(\w+)/) ??
42
+ l.match(/^ {2}(?:(?:public|private|protected|static|async|get|set)\s+)*(\w+)\s*(?:<[^>]*>)?\([^)]*\)?.*\{\s*$/) ??
43
+ l.match(/^(?:func|fn|pub fn|def)\s+(?:\([^)]*\)\s*)?(\w+)/);
44
+ }
45
+ if (m && !/^(if|for|while|switch|return|catch|constructor)$/.test(m[1]))
46
+ out.push(`${m[1]}:${i + 1}`);
47
+ });
48
+ return out;
49
+ }
50
+ export function repoMap(root, sub = '') {
51
+ const files = listFiles(root).filter((f) => (!sub || f.startsWith(sub.replace(/^\.\//, ''))) && !SKIP.test(f));
52
+ const lines = [];
53
+ let budget = 160;
54
+ for (const f of files) {
55
+ const abs = path.join(root, f);
56
+ let src = '';
57
+ try {
58
+ if (fs.statSync(abs).size > 400_000)
59
+ continue;
60
+ src = fs.readFileSync(abs, 'utf8');
61
+ }
62
+ catch {
63
+ continue;
64
+ }
65
+ const n = src.split('\n').length;
66
+ if (CODE.test(f) && budget > 0) {
67
+ const syms = symbols(src, f);
68
+ lines.push(`${f} (${n})${syms.length ? ' ' + syms.join(' ') : ''}`);
69
+ budget--;
70
+ }
71
+ else if (!/\.(lock|png|jpe?g|gif|ico|svg|woff2?)$|package-lock\.json$/.test(f)) {
72
+ lines.push(`${f} (${n})`);
73
+ }
74
+ }
75
+ return lines.join('\n');
76
+ }
77
+ // End line (0-based, inclusive) of the block starting at `start`.
78
+ function blockEnd(ls, start, py) {
79
+ if (py) {
80
+ const ind = ls[start].match(/^\s*/)[0].length;
81
+ let end = start;
82
+ for (let i = start + 1; i < ls.length; i++) {
83
+ if (!ls[i].trim())
84
+ continue;
85
+ if (ls[i].match(/^\s*/)[0].length <= ind)
86
+ break;
87
+ end = i;
88
+ }
89
+ return end;
90
+ }
91
+ let depth = 0, seen = false;
92
+ for (let i = start; i < ls.length; i++) {
93
+ for (const ch of ls[i].replace(/(["'`])(?:\\.|(?!\1).)*\1/g, '""')) {
94
+ if (ch === '{') {
95
+ depth++;
96
+ seen = true;
97
+ }
98
+ else if (ch === '}')
99
+ depth--;
100
+ }
101
+ if (seen && depth <= 0)
102
+ return i;
103
+ if (!seen && /;\s*$/.test(ls[i]))
104
+ return i; // one-line declaration
105
+ }
106
+ return ls.length - 1;
107
+ }
108
+ function findSymbol(ls, name, py) {
109
+ const n = name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
110
+ const pats = py
111
+ ? [new RegExp(`^\\s*(async\\s+)?(def|class)\\s+${n}\\b`)]
112
+ : [
113
+ new RegExp(`^\\s*(export\\s+)?(default\\s+)?(async\\s+)?(function\\*?|class|interface|type|enum|const|let|var)\\s+${n}\\b`),
114
+ new RegExp(`^\\s*((public|private|protected|static|async|get|set)\\s+)*${n}\\s*(<[^>]*>)?\\(`),
115
+ new RegExp(`^\\s*(func|fn|pub fn)\\s+(\\([^)]*\\)\\s*)?${n}\\b`),
116
+ ];
117
+ for (const re of pats) {
118
+ const i = ls.findIndex((l) => re.test(l));
119
+ if (i >= 0)
120
+ return i;
121
+ }
122
+ return -1;
123
+ }
124
+ const numbered = (ls, a, b) => ls.slice(a, b + 1).map((l, k) => `${String(a + k + 1).padStart(4)}│ ${l}`).join('\n');
125
+ // spec: "path:10-80" | "path:42" | "path#sym1,sym2" | "path" (whole file, capped)
126
+ export function snippets(specs, cwd) {
127
+ const parts = [];
128
+ let total = 0;
129
+ for (const spec of specs.flatMap((s) => s.split(';')).map((s) => s.trim()).filter(Boolean)) {
130
+ const m = spec.match(/^(.*?)(?:#(.+)|:(\d+)(?:-(\d+))?)?$/);
131
+ const file = m[1];
132
+ let ls;
133
+ try {
134
+ ls = fs.readFileSync(path.resolve(cwd, file), 'utf8').split('\n');
135
+ }
136
+ catch {
137
+ parts.push(`### ${spec}\n(dosya okunamadı)`);
138
+ continue;
139
+ }
140
+ const py = file.endsWith('.py');
141
+ const ranges = []; // a < 0 marks a missing symbol, kept in order
142
+ if (m[2]) {
143
+ for (const sym of m[2].split(',').map((s) => s.trim()).filter(Boolean)) {
144
+ const s = findSymbol(ls, sym, py);
145
+ if (s < 0) {
146
+ ranges.push([-1, -1, `#${sym}`]);
147
+ continue;
148
+ }
149
+ ranges.push([s, Math.min(blockEnd(ls, s, py), s + MAX_SNIPPET_LINES - 1), `#${sym}`]);
150
+ }
151
+ }
152
+ else if (m[3]) {
153
+ const a = Math.max(1, Number(m[3])), b = m[4] ? Number(m[4]) : a + 20;
154
+ ranges.push([a - 1, Math.min(ls.length - 1, b - 1, a - 1 + MAX_SNIPPET_LINES - 1), `:${a}-${b}`]);
155
+ }
156
+ else
157
+ ranges.push([0, Math.min(ls.length - 1, MAX_SNIPPET_LINES - 1), '']);
158
+ for (const [a, b, tag] of ranges) {
159
+ if (a < 0) {
160
+ parts.push(`### ${file}${tag}\n(sembol bulunamadı)`);
161
+ continue;
162
+ }
163
+ if (total >= MAX_TOTAL_LINES) {
164
+ parts.push(`### ${file}${tag}\n(atlandı: bağlam sınırı ${MAX_TOTAL_LINES} satır)`);
165
+ continue;
166
+ }
167
+ const end = Math.min(b, a + (MAX_TOTAL_LINES - total) - 1);
168
+ total += end - a + 1;
169
+ const cut = end < blockEnd(ls, a, py) && tag.startsWith('#') ? `\n … (kısaltıldı, devamı ${end + 2}. satırdan)` : '';
170
+ parts.push(`### ${file}${tag} (satır ${a + 1}-${end + 1})\n${numbered(ls, a, end)}${cut}`);
171
+ }
172
+ }
173
+ return parts.length ? `\n\nİLGİLİ KOD (coban'ın görevi verdiği an itibarıyla güncel; bu kısımları yeniden okuma/arama):\n${parts.join('\n\n')}` : '';
174
+ }