coban 0.0.0-stage → 0.2.1
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 +21 -0
- package/README.md +173 -2
- package/bin/coban +10 -0
- package/dist/agent.js +161 -0
- package/dist/cli.js +201 -0
- package/dist/config.js +55 -0
- package/dist/context.js +174 -0
- package/dist/hub.js +543 -0
- package/dist/prompt.js +119 -0
- package/dist/tui.js +1382 -0
- package/dist/usage.js +100 -0
- package/package.json +58 -4
package/dist/context.js
ADDED
|
@@ -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
|
+
}
|