@stilero/bankan 1.0.13 → 1.0.17

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.
@@ -7,7 +7,7 @@
7
7
  <link rel="preconnect" href="https://fonts.googleapis.com" />
8
8
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
9
9
  <link href="https://fonts.googleapis.com/css2?family=DM+Mono:wght@400;500&family=Syne:wght@600;700;800&display=swap" rel="stylesheet" />
10
- <script type="module" crossorigin src="/assets/index-pUZAEGtO.js"></script>
10
+ <script type="module" crossorigin src="/assets/index-CHxyLFN_.js"></script>
11
11
  <link rel="stylesheet" crossorigin href="/assets/index-BZkAflU1.css">
12
12
  </head>
13
13
  <body>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stilero/bankan",
3
- "version": "1.0.13",
3
+ "version": "1.0.17",
4
4
  "type": "module",
5
5
  "description": "Run AI coding agents like a Kanban board. Plan, implement, review and ship code using parallel AI agents across your local repositories.",
6
6
  "license": "MIT",
@@ -45,6 +45,13 @@
45
45
  "scripts": {
46
46
  "setup": "node scripts/setup.js",
47
47
  "build": "npm run build --prefix client",
48
+ "lint": "eslint .",
49
+ "lint:fix": "eslint . --fix",
50
+ "test": "npm run test --prefix server && npm run test --prefix client",
51
+ "test:server": "npm run test --prefix server",
52
+ "test:client": "npm run test --prefix client",
53
+ "coverage": "npm run coverage --prefix server && npm run coverage --prefix client && node scripts/check-coverage.js",
54
+ "coverage:check": "node scripts/check-coverage.js",
48
55
  "dev": "concurrently -n server,client -c cyan,magenta \"npm run dev --prefix server\" \"npm run dev --prefix client\"",
49
56
  "start": "npm run dev",
50
57
  "install:all": "npm install && npm install --prefix server && npm install --prefix client",
@@ -60,6 +67,11 @@
60
67
  "ws": "^8.16.0"
61
68
  },
62
69
  "devDependencies": {
63
- "concurrently": "^8.2.2"
70
+ "@eslint/js": "^9.39.4",
71
+ "concurrently": "^8.2.2",
72
+ "eslint": "^9.39.4",
73
+ "eslint-plugin-react": "^7.37.5",
74
+ "eslint-plugin-react-hooks": "^5.2.0",
75
+ "globals": "^16.5.0"
64
76
  }
65
77
  }
package/scripts/setup.js CHANGED
@@ -15,10 +15,6 @@ const ENV_FILE = runtimePaths.envFile;
15
15
 
16
16
  const rl = createInterface({ input: process.stdin, output: process.stdout });
17
17
 
18
- function ask(question) {
19
- return new Promise((resolve) => rl.question(question, resolve));
20
- }
21
-
22
18
  function dim(text) { return `\x1b[2m${text}\x1b[0m`; }
23
19
  function green(text) { return `\x1b[32m${text}\x1b[0m`; }
24
20
  function yellow(text) { return `\x1b[33m${text}\x1b[0m`; }
@@ -172,7 +168,7 @@ async function main() {
172
168
  try {
173
169
  execSync(step.cmd, { cwd: ROOT, stdio: 'inherit' });
174
170
  console.log(` ${green('✓')} ${step.label}\n`);
175
- } catch (err) {
171
+ } catch {
176
172
  console.log(` ${red('✗')} ${step.label} install failed. Try running manually: ${step.cmd}\n`);
177
173
  }
178
174
  }
@@ -36,6 +36,38 @@ const TOKEN_PATTERNS = [
36
36
  /(\d[\d, ]*)\s+(?:input\s+)?tokens\b/i,
37
37
  ];
38
38
 
39
+ const STRUCTURED_BLOCK_MARKERS = {
40
+ plan: {
41
+ start: '=== PLAN START ===',
42
+ end: '=== PLAN END ===',
43
+ },
44
+ review: {
45
+ start: '=== REVIEW START ===',
46
+ end: '=== REVIEW END ===',
47
+ },
48
+ };
49
+
50
+ function stripAnsi(text) {
51
+ if (typeof text !== 'string') return text;
52
+ // Replace cursor forward codes (\x1b[nC) with a space to preserve word boundaries.
53
+ // eslint-disable-next-line no-control-regex
54
+ let result = text.replace(/\x1b\[\d*C/g, ' ');
55
+ return result.replace(
56
+ // eslint-disable-next-line no-control-regex
57
+ /[\x1b\x9b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><~]|\x1b\].*?(?:\x07|\x1b\\)|\r/g,
58
+ ''
59
+ );
60
+ }
61
+
62
+ function getLastStructuredBlock(text, startMarker, endMarker) {
63
+ if (typeof text !== 'string' || !text) return null;
64
+ const endIdx = text.lastIndexOf(endMarker);
65
+ if (endIdx === -1) return null;
66
+ const startIdx = text.lastIndexOf(startMarker, endIdx);
67
+ if (startIdx === -1) return null;
68
+ return text.slice(startIdx, endIdx + endMarker.length);
69
+ }
70
+
39
71
  class Agent {
40
72
  constructor(def) {
41
73
  this.id = def.id;
@@ -44,6 +76,7 @@ class Agent {
44
76
  this.icon = def.icon;
45
77
  this.color = def.color;
46
78
  this.cli = def.cli || 'claude';
79
+ this.model = def.model || '';
47
80
  this.draining = false;
48
81
  this.status = 'idle';
49
82
  this.currentTask = null;
@@ -63,6 +96,23 @@ class Agent {
63
96
  openedAt: null,
64
97
  outputPath: null,
65
98
  };
99
+ this._lastTokenSync = null;
100
+ this.structuredOutput = this._createStructuredOutputState();
101
+ this.terminalSize = {
102
+ cols: 220,
103
+ rows: 50,
104
+ };
105
+ }
106
+
107
+ _createStructuredOutputState() {
108
+ return {
109
+ plan: { pending: '', completed: null, allCompleted: [] },
110
+ review: { pending: '', completed: null, allCompleted: [] },
111
+ };
112
+ }
113
+
114
+ _resetStructuredOutput() {
115
+ this.structuredOutput = this._createStructuredOutputState();
66
116
  }
67
117
 
68
118
  spawn(cwd, command) {
@@ -90,6 +140,7 @@ class Agent {
90
140
  this.tokens = 0;
91
141
  this.taskTokenBase = this.currentTask ? (store.getTask(this.currentTask)?.totalTokens || 0) : 0;
92
142
  this.lastOutputAt = Date.now();
143
+ this._lastTokenSync = null;
93
144
  this.bridge = {
94
145
  active: false,
95
146
  mode: null,
@@ -97,13 +148,14 @@ class Agent {
97
148
  openedAt: null,
98
149
  outputPath: null,
99
150
  };
151
+ this._resetStructuredOutput();
100
152
 
101
153
  const env = { ...process.env, TERM: 'xterm-256color' };
102
154
  delete env.CLAUDECODE;
103
155
  this.process = pty.spawn('bash', ['-l', '-c', command], {
104
156
  name: 'xterm-256color',
105
- cols: 220,
106
- rows: 50,
157
+ cols: this.terminalSize.cols,
158
+ rows: this.terminalSize.rows,
107
159
  cwd,
108
160
  env,
109
161
  });
@@ -115,6 +167,7 @@ class Agent {
115
167
  }
116
168
  this.lastOutputAt = Date.now();
117
169
  this._parseTokens(data);
170
+ this._captureStructuredOutput(data);
118
171
  this._syncTaskTokens();
119
172
  if (this.bridge.active && this.bridge.outputPath) {
120
173
  try { appendFileSync(this.bridge.outputPath, data); } catch { /* ignore */ }
@@ -161,8 +214,44 @@ class Agent {
161
214
  }
162
215
  }
163
216
 
217
+ _captureStructuredOutput(data) {
218
+ for (const [kind, markers] of Object.entries(STRUCTURED_BLOCK_MARKERS)) {
219
+ const state = this.structuredOutput[kind];
220
+ // Accumulate raw data so ANSI sequences split across chunks
221
+ // are stripped correctly when we process the combined text.
222
+ const rawCombined = `${state.pending}${data}`;
223
+ const combined = stripAnsi(rawCombined);
224
+ const completed = getLastStructuredBlock(combined, markers.start, markers.end);
225
+ if (completed) {
226
+ state.completed = completed;
227
+ state.allCompleted.push(completed);
228
+ }
229
+
230
+ const lastStartIdx = combined.lastIndexOf(markers.start);
231
+ const lastEndIdx = combined.lastIndexOf(markers.end);
232
+ if (lastStartIdx !== -1 && lastStartIdx > lastEndIdx) {
233
+ // Inside an open block — keep raw data for re-stripping next time
234
+ state.pending = rawCombined;
235
+ } else {
236
+ const tailLength = Math.max(markers.start.length, markers.end.length) * 4;
237
+ state.pending = rawCombined.slice(-tailLength);
238
+ }
239
+ }
240
+ }
241
+
242
+ getStructuredBlock(kind) {
243
+ return this.structuredOutput[kind]?.completed || null;
244
+ }
245
+
246
+ getAllCapturedBlocks(kind) {
247
+ return this.structuredOutput[kind]?.allCompleted || [];
248
+ }
249
+
164
250
  _syncTaskTokens() {
165
251
  if (!this.currentTask || this.tokens <= 0) return;
252
+ const now = Date.now();
253
+ if (this._lastTokenSync && now - this._lastTokenSync < 2000) return;
254
+ this._lastTokenSync = now;
166
255
  store.updateTaskTokens(this.currentTask, this.taskTokenBase + this.tokens);
167
256
  bus.emit('agent:updated', this.getStatus());
168
257
  }
@@ -192,6 +281,29 @@ class Agent {
192
281
  return false;
193
282
  }
194
283
 
284
+ resize(cols, rows) {
285
+ const nextCols = Math.max(20, Math.floor(Number(cols) || 0));
286
+ const nextRows = Math.max(5, Math.floor(Number(rows) || 0));
287
+
288
+ if (!Number.isFinite(nextCols) || !Number.isFinite(nextRows)) {
289
+ return false;
290
+ }
291
+
292
+ this.terminalSize = {
293
+ cols: nextCols,
294
+ rows: nextRows,
295
+ };
296
+
297
+ if (!this.process) return true;
298
+
299
+ try {
300
+ this.process.resize(nextCols, nextRows);
301
+ return true;
302
+ } catch {
303
+ return false;
304
+ }
305
+ }
306
+
195
307
  kill() {
196
308
  if (this.process) {
197
309
  try { this.process.kill(); } catch { /* ignore */ }
@@ -208,6 +320,7 @@ class Agent {
208
320
  openedAt: null,
209
321
  outputPath: null,
210
322
  };
323
+ this._resetStructuredOutput();
211
324
  bus.emit('agent:updated', this.getStatus());
212
325
  }
213
326
 
@@ -232,6 +345,7 @@ class Agent {
232
345
  bridgeMode: this.bridge.mode,
233
346
  bridgeOwner: this.bridge.owner,
234
347
  bridgeOpenedAt: this.bridge.openedAt,
348
+ terminalSize: this.terminalSize,
235
349
  aggregatedTokens: this.currentTask
236
350
  ? Math.max(store.getTask(this.currentTask)?.totalTokens || 0, this.taskTokenBase + this.tokens)
237
351
  : 0,
@@ -250,6 +364,7 @@ class AgentManager {
250
364
  this.agents = new Map();
251
365
  this._maxSettings = {}; // { planners: 4, implementors: 8, reviewers: 4 }
252
366
  this._cliSettings = {}; // { planners: 'claude', implementors: 'claude', reviewers: 'claude' }
367
+ this._modelSettings = {}; // { planners: '', implementors: 'opus', reviewers: 'haiku' }
253
368
  this._sessionCounters = { plan: 0, imp: 0, rev: 0 };
254
369
 
255
370
  // Orchestrator is always present
@@ -269,10 +384,11 @@ class AgentManager {
269
384
  }
270
385
 
271
386
  reconfigure(settings) {
272
- for (const [settingsKey, { meta, prefix }] of Object.entries(ROLE_MAP)) {
387
+ for (const [settingsKey, { prefix }] of Object.entries(ROLE_MAP)) {
273
388
  const cfg = settings.agents[settingsKey];
274
389
  this._maxSettings[settingsKey] = cfg.max;
275
390
  this._cliSettings[settingsKey] = cfg.cli;
391
+ this._modelSettings[settingsKey] = cfg.model || '';
276
392
 
277
393
  // Scale down if current count exceeds new max
278
394
  const currentAgents = this.getAgentsByRole(prefix);
@@ -288,10 +404,11 @@ class AgentManager {
288
404
  }
289
405
  }
290
406
 
291
- // Update CLI on all existing non-draining agents for this role
407
+ // Update CLI and model on all existing non-draining agents for this role
292
408
  for (const agent of this.getAgentsByRole(prefix)) {
293
409
  if (!agent.draining) {
294
410
  agent.cli = cfg.cli;
411
+ agent.model = cfg.model || '';
295
412
  }
296
413
  }
297
414
  }
@@ -302,6 +419,7 @@ class AgentManager {
302
419
  const { meta, prefix } = ROLE_MAP[settingsKey];
303
420
  const max = this._maxSettings[settingsKey] ?? 1;
304
421
  const cli = this._cliSettings[settingsKey] || 'claude';
422
+ const model = this._modelSettings[settingsKey] || '';
305
423
  const current = this.getAgentsByRole(prefix);
306
424
 
307
425
  if (current.length >= max) return null;
@@ -317,6 +435,7 @@ class AgentManager {
317
435
  icon: meta.icon,
318
436
  color,
319
437
  cli,
438
+ model,
320
439
  });
321
440
  this.agents.set(agent.id, agent);
322
441
  bus.emit('agent:updated', agent.getStatus());