harness-bujang 0.5.10 → 0.6.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.
|
@@ -45,6 +45,16 @@ async function runChat(args) {
|
|
|
45
45
|
`INSERT INTO harness_messages (id, "from", "to", type, message, severity)
|
|
46
46
|
VALUES (?, ?, ?, ?, ?, ?)`
|
|
47
47
|
);
|
|
48
|
+
const readStateRowsStmt = db.prepare(
|
|
49
|
+
`SELECT room, last_seen_at FROM harness_read_state`
|
|
50
|
+
);
|
|
51
|
+
const readStateUpsertStmt = db.prepare(
|
|
52
|
+
`INSERT INTO harness_read_state (room, last_seen_at, updated_at)
|
|
53
|
+
VALUES (?, ?, strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
|
|
54
|
+
ON CONFLICT(room) DO UPDATE SET
|
|
55
|
+
last_seen_at = excluded.last_seen_at,
|
|
56
|
+
updated_at = excluded.updated_at`
|
|
57
|
+
);
|
|
48
58
|
const port = await findOpenPort(opts.port);
|
|
49
59
|
const server = http.createServer(async (req, res) => {
|
|
50
60
|
const url2 = new URL(req.url ?? "/", `http://localhost:${port}`);
|
|
@@ -89,6 +99,39 @@ async function runChat(args) {
|
|
|
89
99
|
}
|
|
90
100
|
return;
|
|
91
101
|
}
|
|
102
|
+
if (req.method === "GET" && url2.pathname === "/api/read-state") {
|
|
103
|
+
try {
|
|
104
|
+
const rows = readStateRowsStmt.all();
|
|
105
|
+
const map = {};
|
|
106
|
+
for (const r of rows) map[r.room] = r.last_seen_at;
|
|
107
|
+
res.writeHead(200, { "content-type": "application/json" });
|
|
108
|
+
res.end(JSON.stringify({ data: map }));
|
|
109
|
+
} catch (err) {
|
|
110
|
+
res.writeHead(500, { "content-type": "application/json" });
|
|
111
|
+
res.end(JSON.stringify({ data: {}, error: String(err) }));
|
|
112
|
+
}
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
if (req.method === "POST" && url2.pathname === "/api/read-state") {
|
|
116
|
+
try {
|
|
117
|
+
const body = await readBody(req);
|
|
118
|
+
const parsed = JSON.parse(body);
|
|
119
|
+
const room = (parsed.room ?? "").trim();
|
|
120
|
+
const lastSeenAt = (parsed.last_seen_at ?? "").trim();
|
|
121
|
+
if (!room || !lastSeenAt) {
|
|
122
|
+
res.writeHead(400, { "content-type": "application/json" });
|
|
123
|
+
res.end(JSON.stringify({ error: "room and last_seen_at are required" }));
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
readStateUpsertStmt.run(room, lastSeenAt);
|
|
127
|
+
res.writeHead(200, { "content-type": "application/json" });
|
|
128
|
+
res.end(JSON.stringify({ data: { room, last_seen_at: lastSeenAt } }));
|
|
129
|
+
} catch (err) {
|
|
130
|
+
res.writeHead(500, { "content-type": "application/json" });
|
|
131
|
+
res.end(JSON.stringify({ error: String(err) }));
|
|
132
|
+
}
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
92
135
|
res.writeHead(404);
|
|
93
136
|
res.end("not found");
|
|
94
137
|
});
|
|
@@ -203,6 +246,14 @@ var SCHEMA_SQL = `
|
|
|
203
246
|
);
|
|
204
247
|
CREATE INDEX IF NOT EXISTS harness_messages_timestamp_idx ON harness_messages(timestamp DESC);
|
|
205
248
|
CREATE INDEX IF NOT EXISTS harness_messages_from_to_idx ON harness_messages("from", "to");
|
|
249
|
+
|
|
250
|
+
-- 0.6.1: per-room read marker (chat.db is the single source of truth, so
|
|
251
|
+
-- read state survives port changes / server restarts / browsers).
|
|
252
|
+
CREATE TABLE IF NOT EXISTS harness_read_state (
|
|
253
|
+
room TEXT PRIMARY KEY,
|
|
254
|
+
last_seen_at TEXT NOT NULL,
|
|
255
|
+
updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
|
|
256
|
+
);
|
|
206
257
|
`;
|
|
207
258
|
function renderHtml() {
|
|
208
259
|
return (
|
|
@@ -290,12 +341,16 @@ const ROOMS = [
|
|
|
290
341
|
{ id: '\uC678\uBD80\uD300\uC6D0', name: '\uC678\uBD80\uD300\uC6D0', icon: '\u{1F310}', members: ['\uBD80\uC7A5', '\uC678\uBD80\uD300\uC6D0', '\uACF5\uB3D9\uB300\uD45C'] },
|
|
291
342
|
];
|
|
292
343
|
|
|
293
|
-
const STORAGE_KEY = 'harness-bujang-read';
|
|
294
344
|
const FILTER_KEY = 'harness-bujang-filter';
|
|
345
|
+
// 0.6.1: Read state moved server-side (chat.db harness_read_state table) so
|
|
346
|
+
// it survives port changes / server restarts / different browsers. The
|
|
347
|
+
// localStorage 'harness-bujang-read' key from 0.5.x\u20130.6.0 is now ignored
|
|
348
|
+
// (no migration needed \u2014 server first-run auto-marks current state as read).
|
|
295
349
|
const state = {
|
|
296
350
|
messages: [],
|
|
297
351
|
selectedRoom: null,
|
|
298
|
-
|
|
352
|
+
/** room id \u2192 last_seen_at ISO timestamp. Populated by GET /api/read-state. */
|
|
353
|
+
readByRoom: {},
|
|
299
354
|
filter: localStorage.getItem(FILTER_KEY) || 'all', // 'all' | 'unread'
|
|
300
355
|
loading: true,
|
|
301
356
|
};
|
|
@@ -374,11 +429,16 @@ function render() {
|
|
|
374
429
|
const infos = state.messages.filter((m) => m.severity === 'info').length;
|
|
375
430
|
|
|
376
431
|
// Pre-compute unread per room (for the filter button + badges).
|
|
432
|
+
// 0.6.1: count messages newer than the per-room last_seen_at marker
|
|
433
|
+
// returned by the server. Survives port changes / server restarts.
|
|
377
434
|
const unreadByRoom = {};
|
|
378
435
|
let totalUnread = 0;
|
|
379
436
|
for (const room of ROOMS) {
|
|
380
|
-
const
|
|
381
|
-
const
|
|
437
|
+
const roomMsgs = filterMessages(state.messages, room.id);
|
|
438
|
+
const lastSeen = state.readByRoom[room.id];
|
|
439
|
+
const unread = lastSeen
|
|
440
|
+
? roomMsgs.filter((m) => m.timestamp > lastSeen).length
|
|
441
|
+
: roomMsgs.length;
|
|
382
442
|
unreadByRoom[room.id] = unread;
|
|
383
443
|
totalUnread += unread;
|
|
384
444
|
}
|
|
@@ -529,12 +589,24 @@ function render() {
|
|
|
529
589
|
});
|
|
530
590
|
|
|
531
591
|
// Re-bind room-click handlers (room list).
|
|
592
|
+
// 0.6.1: persist read marker via POST /api/read-state (server is SoT) and
|
|
593
|
+
// also update the in-memory map so the badge clears immediately without
|
|
594
|
+
// waiting for the next 2s poll.
|
|
532
595
|
document.querySelectorAll('[data-room-id]').forEach((el) => {
|
|
533
596
|
el.addEventListener('click', () => {
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
state.
|
|
537
|
-
|
|
597
|
+
const roomId = el.getAttribute('data-room-id');
|
|
598
|
+
state.selectedRoom = roomId;
|
|
599
|
+
const last = getLastMessage(state.messages, roomId);
|
|
600
|
+
if (last) {
|
|
601
|
+
state.readByRoom[roomId] = last.timestamp;
|
|
602
|
+
// Fire-and-forget; if it fails the next 2s poll re-syncs from the
|
|
603
|
+
// server. We don't block the UI on the round-trip.
|
|
604
|
+
fetch('/api/read-state', {
|
|
605
|
+
method: 'POST',
|
|
606
|
+
headers: { 'content-type': 'application/json' },
|
|
607
|
+
body: JSON.stringify({ room: roomId, last_seen_at: last.timestamp }),
|
|
608
|
+
}).catch(() => {});
|
|
609
|
+
}
|
|
538
610
|
render();
|
|
539
611
|
const conv = document.getElementById('conversation');
|
|
540
612
|
if (conv) conv.scrollTop = conv.scrollHeight;
|
|
@@ -560,9 +632,15 @@ function render() {
|
|
|
560
632
|
|
|
561
633
|
async function refresh() {
|
|
562
634
|
try {
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
635
|
+
// 0.6.1: fetch messages + read-state in parallel. chat.db is SoT for
|
|
636
|
+
// read state, so port/server/browser changes don't reset it.
|
|
637
|
+
const [msgRes, readRes] = await Promise.all([
|
|
638
|
+
fetch('/api/messages?days=14'),
|
|
639
|
+
fetch('/api/read-state'),
|
|
640
|
+
]);
|
|
641
|
+
const msgJson = await msgRes.json();
|
|
642
|
+
const readJson = await readRes.json();
|
|
643
|
+
state.messages = (msgJson.data || []).map((m) => ({
|
|
566
644
|
id: m.id,
|
|
567
645
|
timestamp: m.timestamp,
|
|
568
646
|
from: m.from,
|
|
@@ -571,6 +649,32 @@ async function refresh() {
|
|
|
571
649
|
message: m.message,
|
|
572
650
|
severity: m.severity || undefined,
|
|
573
651
|
}));
|
|
652
|
+
state.readByRoom = readJson.data || {};
|
|
653
|
+
|
|
654
|
+
// First-run auto-mark: if the server returned an empty read-state but we
|
|
655
|
+
// have messages, this is a fresh upgrade from 0.6.0. Mark every room's
|
|
656
|
+
// current last message as read so the user only sees genuinely-new
|
|
657
|
+
// messages flagged unread from here on.
|
|
658
|
+
if (
|
|
659
|
+
Object.keys(state.readByRoom).length === 0 &&
|
|
660
|
+
state.messages.length > 0
|
|
661
|
+
) {
|
|
662
|
+
const initial = {};
|
|
663
|
+
for (const room of ROOMS) {
|
|
664
|
+
const last = getLastMessage(state.messages, room.id);
|
|
665
|
+
if (last) initial[room.id] = last.timestamp;
|
|
666
|
+
}
|
|
667
|
+
state.readByRoom = initial;
|
|
668
|
+
// Persist to server (fire-and-forget per room \u2014 small N, ~18 calls).
|
|
669
|
+
for (const [room, ts] of Object.entries(initial)) {
|
|
670
|
+
fetch('/api/read-state', {
|
|
671
|
+
method: 'POST',
|
|
672
|
+
headers: { 'content-type': 'application/json' },
|
|
673
|
+
body: JSON.stringify({ room, last_seen_at: ts }),
|
|
674
|
+
}).catch(() => {});
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
|
|
574
678
|
state.loading = false;
|
|
575
679
|
render();
|
|
576
680
|
} catch (e) {
|
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
import * as fs2 from "fs/promises";
|
|
7
7
|
import * as path2 from "path";
|
|
8
8
|
import { fileURLToPath } from "url";
|
|
9
|
-
import { select, confirm } from "@inquirer/prompts";
|
|
9
|
+
import { select, confirm, checkbox } from "@inquirer/prompts";
|
|
10
10
|
|
|
11
11
|
// src/scan.ts
|
|
12
12
|
import * as fs from "fs/promises";
|
|
@@ -169,6 +169,27 @@ async function resolveAssetPaths() {
|
|
|
169
169
|
If installed via npm, try reinstalling. If running from source, run "npm run build" in packages/cli first.`
|
|
170
170
|
);
|
|
171
171
|
}
|
|
172
|
+
var ALL_ADAPTERS = ["cursor", "cline", "aider", "codex", "gemini"];
|
|
173
|
+
var BALANCED_MAPPING = {
|
|
174
|
+
director: "opus",
|
|
175
|
+
cofounder: "opus",
|
|
176
|
+
"architect-team": "opus",
|
|
177
|
+
consultant: "opus",
|
|
178
|
+
"security-team": "opus",
|
|
179
|
+
"db-guard-team": "opus",
|
|
180
|
+
"dev-team": "sonnet",
|
|
181
|
+
"code-review-team": "sonnet",
|
|
182
|
+
"qa-team": "sonnet",
|
|
183
|
+
"doc-sync-team": "sonnet",
|
|
184
|
+
"research-team": "sonnet",
|
|
185
|
+
"analysis-team": "sonnet",
|
|
186
|
+
"script-team": "sonnet",
|
|
187
|
+
"verifier-team": "haiku",
|
|
188
|
+
"image-team": "haiku",
|
|
189
|
+
"voice-team": "haiku",
|
|
190
|
+
"edit-team": "haiku",
|
|
191
|
+
"content-qa-team": "haiku"
|
|
192
|
+
};
|
|
172
193
|
async function runInit(args) {
|
|
173
194
|
let opts = parseArgs(args);
|
|
174
195
|
const assets = await resolveAssetPaths();
|
|
@@ -211,6 +232,8 @@ async function runInit(args) {
|
|
|
211
232
|
console.log(c.bold("\u{1F4CB} Configuration"));
|
|
212
233
|
console.log(c.dim(` Language: ${opts.lang}`));
|
|
213
234
|
console.log(c.dim(` Chat backend: ${opts.chatBackend}${opts.chatBackend === "sqlite" ? " (local file)" : " (cloud Postgres)"}`));
|
|
235
|
+
console.log(c.dim(` Tools: claude${opts.adapters.length > 0 ? ` + ${opts.adapters.join(", ")}` : " (only)"}`));
|
|
236
|
+
console.log(c.dim(` Models: ${describeModelMap(opts.modelMap)}`));
|
|
214
237
|
if (scan.framework.startsWith("Next.js")) {
|
|
215
238
|
console.log(c.dim(` Chat-room UI: ${opts.installTemplate ? "install" : "skip"}`));
|
|
216
239
|
}
|
|
@@ -287,8 +310,12 @@ async function runInit(args) {
|
|
|
287
310
|
continue;
|
|
288
311
|
}
|
|
289
312
|
const raw = await fs2.readFile(path2.join(agentsSrc, f), "utf8");
|
|
290
|
-
|
|
291
|
-
|
|
313
|
+
const slug = f.replace(/\.md$/, "");
|
|
314
|
+
const override = opts.modelMap[slug];
|
|
315
|
+
const rendered = renderTemplate(raw, context);
|
|
316
|
+
const final = override ? overrideModelFrontmatter(rendered, override) : rendered;
|
|
317
|
+
await fs2.writeFile(dst, final);
|
|
318
|
+
console.log(` ${c.green("\u2713")} ${f}${override ? c.dim(` \u2192 model: ${override}`) : ""}`);
|
|
292
319
|
}
|
|
293
320
|
console.log();
|
|
294
321
|
if (opts.editClaudeMd) {
|
|
@@ -380,6 +407,16 @@ async function runInit(args) {
|
|
|
380
407
|
console.log();
|
|
381
408
|
}
|
|
382
409
|
}
|
|
410
|
+
if (opts.adapters.length > 0) {
|
|
411
|
+
console.log(c.bold("\u{1F501} Fanning out to extra tool adapters"));
|
|
412
|
+
console.log(c.dim(` Targets: ${opts.adapters.join(", ")}`));
|
|
413
|
+
const { runAdapt } = await import("./adapt-VPWOYF6W.js");
|
|
414
|
+
await runAdapt([
|
|
415
|
+
`--to=${opts.adapters.join(",")}`,
|
|
416
|
+
`--target=${opts.target}`,
|
|
417
|
+
"--yes"
|
|
418
|
+
]);
|
|
419
|
+
}
|
|
383
420
|
console.log(c.bold(c.green("\u2705 Done.")));
|
|
384
421
|
console.log();
|
|
385
422
|
console.log("Next steps:");
|
|
@@ -431,6 +468,35 @@ async function promptInteractive(opts, scan) {
|
|
|
431
468
|
],
|
|
432
469
|
default: opts.chatBackend
|
|
433
470
|
});
|
|
471
|
+
const isPreset = (t) => opts.adapters.includes(t);
|
|
472
|
+
const adapters = await checkbox({
|
|
473
|
+
message: "Extra tool adapters? (Claude Code is always installed at .claude/agents/ \u2014 these add files for OTHER tools)",
|
|
474
|
+
choices: [
|
|
475
|
+
{ name: "Cursor (.cursor/rules/bujang-*.mdc)", value: "cursor", checked: isPreset("cursor") },
|
|
476
|
+
{ name: "Codex / Copilot (AGENTS.md)", value: "codex", checked: isPreset("codex") },
|
|
477
|
+
{ name: "Cline (.clinerules/bujang-*.md)", value: "cline", checked: isPreset("cline") },
|
|
478
|
+
{ name: "Aider (CONVENTIONS.md + .aider.conf.yml)", value: "aider", checked: isPreset("aider") },
|
|
479
|
+
{ name: "Gemini / Antigravity (GEMINI.md + .gemini/styleguide.md)", value: "gemini", checked: isPreset("gemini") }
|
|
480
|
+
],
|
|
481
|
+
required: false
|
|
482
|
+
});
|
|
483
|
+
const preset = await select({
|
|
484
|
+
message: "Per-agent Claude model? (only affects .claude/agents/ \u2014 other tools manage models themselves)",
|
|
485
|
+
choices: [
|
|
486
|
+
{ name: "balanced \u2014 opus / sonnet / haiku mix (recommended, ~60% cost cut)", value: "balanced" },
|
|
487
|
+
{ name: "keep \u2014 leave each agent's default model untouched", value: "keep" },
|
|
488
|
+
{ name: "cost \u2014 all haiku (cheapest, fastest)", value: "cost" },
|
|
489
|
+
{ name: "quality \u2014 all opus (most expensive, highest quality)", value: "quality" },
|
|
490
|
+
{ name: "custom \u2014 pick model per agent (18 prompts)", value: "custom" }
|
|
491
|
+
],
|
|
492
|
+
default: "balanced"
|
|
493
|
+
});
|
|
494
|
+
let modelMap = {};
|
|
495
|
+
if (preset === "custom") {
|
|
496
|
+
modelMap = await promptCustomModelMap();
|
|
497
|
+
} else {
|
|
498
|
+
modelMap = resolvePreset(preset);
|
|
499
|
+
}
|
|
434
500
|
let installTemplate = opts.installTemplate;
|
|
435
501
|
if (scan.framework.startsWith("Next.js") && opts.installTemplate) {
|
|
436
502
|
installTemplate = await confirm({
|
|
@@ -438,7 +504,26 @@ async function promptInteractive(opts, scan) {
|
|
|
438
504
|
default: true
|
|
439
505
|
});
|
|
440
506
|
}
|
|
441
|
-
return { ...opts, lang, chatBackend, installTemplate };
|
|
507
|
+
return { ...opts, lang, chatBackend, installTemplate, adapters, modelMap };
|
|
508
|
+
}
|
|
509
|
+
async function promptCustomModelMap() {
|
|
510
|
+
const out = {};
|
|
511
|
+
const slugs = Object.keys(BALANCED_MAPPING);
|
|
512
|
+
console.log();
|
|
513
|
+
console.log(c.dim(` Custom mapping \u2014 pick a model for each of ${slugs.length} agents.`));
|
|
514
|
+
for (const slug of slugs) {
|
|
515
|
+
const tier = await select({
|
|
516
|
+
message: `${slug.padEnd(20)}`,
|
|
517
|
+
choices: [
|
|
518
|
+
{ name: "opus (heaviest, smartest)", value: "opus" },
|
|
519
|
+
{ name: "sonnet (balanced)", value: "sonnet" },
|
|
520
|
+
{ name: "haiku (lightest, fastest, cheap)", value: "haiku" }
|
|
521
|
+
],
|
|
522
|
+
default: BALANCED_MAPPING[slug] ?? "sonnet"
|
|
523
|
+
});
|
|
524
|
+
out[slug] = tier;
|
|
525
|
+
}
|
|
526
|
+
return out;
|
|
442
527
|
}
|
|
443
528
|
function parseArgs(args) {
|
|
444
529
|
const lang = getFlag(args, "--lang") ?? "ko";
|
|
@@ -450,6 +535,30 @@ function parseArgs(args) {
|
|
|
450
535
|
throw new Error(`--chat must be "sqlite" or "supabase", got "${chatBackend}"`);
|
|
451
536
|
}
|
|
452
537
|
const targetRaw = getFlag(args, "--target") ?? ".";
|
|
538
|
+
const toolsRaw = getFlag(args, "--tools");
|
|
539
|
+
let adapters = [];
|
|
540
|
+
if (toolsRaw) {
|
|
541
|
+
const parts = toolsRaw === "all" ? ALL_ADAPTERS.slice() : toolsRaw.split(",").map((t) => t.trim()).filter(Boolean);
|
|
542
|
+
for (const t of parts) {
|
|
543
|
+
if (t === "claude" || t === "claude-code") continue;
|
|
544
|
+
if (!ALL_ADAPTERS.includes(t)) {
|
|
545
|
+
throw new Error(
|
|
546
|
+
`Unknown tool "${t}" in --tools. Allowed: claude, ${ALL_ADAPTERS.join(", ")}, all`
|
|
547
|
+
);
|
|
548
|
+
}
|
|
549
|
+
if (!adapters.includes(t)) adapters.push(t);
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
const modelsRaw = getFlag(args, "--models");
|
|
553
|
+
let modelMap = {};
|
|
554
|
+
if (modelsRaw) {
|
|
555
|
+
if (!["balanced", "cost", "quality", "keep"].includes(modelsRaw)) {
|
|
556
|
+
throw new Error(
|
|
557
|
+
`--models must be one of: balanced, cost, quality, keep (got "${modelsRaw}")`
|
|
558
|
+
);
|
|
559
|
+
}
|
|
560
|
+
modelMap = resolvePreset(modelsRaw);
|
|
561
|
+
}
|
|
453
562
|
return {
|
|
454
563
|
lang,
|
|
455
564
|
target: path2.resolve(targetRaw),
|
|
@@ -460,9 +569,19 @@ function parseArgs(args) {
|
|
|
460
569
|
installTemplate: !args.includes("--no-template"),
|
|
461
570
|
editClaudeMd: !args.includes("--no-claude-md"),
|
|
462
571
|
seedLearningLog: !args.includes("--no-learning-log"),
|
|
463
|
-
yes: args.includes("--yes") || args.includes("-y")
|
|
572
|
+
yes: args.includes("--yes") || args.includes("-y"),
|
|
573
|
+
adapters,
|
|
574
|
+
modelMap
|
|
464
575
|
};
|
|
465
576
|
}
|
|
577
|
+
function resolvePreset(preset) {
|
|
578
|
+
if (preset === "keep") return {};
|
|
579
|
+
if (preset === "balanced") return { ...BALANCED_MAPPING };
|
|
580
|
+
const tier = preset === "cost" ? "haiku" : "opus";
|
|
581
|
+
const out = {};
|
|
582
|
+
for (const k of Object.keys(BALANCED_MAPPING)) out[k] = tier;
|
|
583
|
+
return out;
|
|
584
|
+
}
|
|
466
585
|
function getFlag(args, name) {
|
|
467
586
|
for (const a of args) {
|
|
468
587
|
if (a.startsWith(`${name}=`)) return a.slice(name.length + 1);
|
|
@@ -571,6 +690,26 @@ function printBackendInstructions(backend, commitChat) {
|
|
|
571
690
|
}
|
|
572
691
|
console.log();
|
|
573
692
|
}
|
|
693
|
+
function overrideModelFrontmatter(content, model) {
|
|
694
|
+
if (!content.startsWith("---\n")) return content;
|
|
695
|
+
const end = content.indexOf("\n---\n", 4);
|
|
696
|
+
if (end < 0) return content;
|
|
697
|
+
const fmRaw = content.slice(0, end);
|
|
698
|
+
const rest = content.slice(end);
|
|
699
|
+
const newFm = /^model:\s*\S+/m.test(fmRaw) ? fmRaw.replace(/^model:\s*\S+/m, `model: ${model}`) : fmRaw + `
|
|
700
|
+
model: ${model}`;
|
|
701
|
+
return newFm + rest;
|
|
702
|
+
}
|
|
703
|
+
function describeModelMap(map) {
|
|
704
|
+
if (Object.keys(map).length === 0) return "keep (use each agent's default)";
|
|
705
|
+
const counts = { opus: 0, sonnet: 0, haiku: 0 };
|
|
706
|
+
for (const v of Object.values(map)) counts[v]++;
|
|
707
|
+
const parts = [];
|
|
708
|
+
for (const tier of ["opus", "sonnet", "haiku"]) {
|
|
709
|
+
if (counts[tier] > 0) parts.push(`${counts[tier]} ${tier}`);
|
|
710
|
+
}
|
|
711
|
+
return parts.join(" \xB7 ");
|
|
712
|
+
}
|
|
574
713
|
function stackReviewRules(framework) {
|
|
575
714
|
if (framework.startsWith("Next.js")) {
|
|
576
715
|
return `Next.js App Router rules:
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,19 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
|
+
import * as fs from "fs/promises";
|
|
5
|
+
import * as path from "path";
|
|
6
|
+
import { fileURLToPath } from "url";
|
|
7
|
+
var __filename = fileURLToPath(import.meta.url);
|
|
8
|
+
var __dirname = path.dirname(__filename);
|
|
9
|
+
async function readVersion() {
|
|
10
|
+
try {
|
|
11
|
+
const pkgRaw = await fs.readFile(path.resolve(__dirname, "..", "package.json"), "utf8");
|
|
12
|
+
return JSON.parse(pkgRaw).version ?? "unknown";
|
|
13
|
+
} catch {
|
|
14
|
+
return "unknown";
|
|
15
|
+
}
|
|
16
|
+
}
|
|
4
17
|
var c = {
|
|
5
18
|
bold: (s) => `\x1B[1m${s}\x1B[22m`,
|
|
6
19
|
dim: (s) => `\x1B[2m${s}\x1B[22m`,
|
|
@@ -25,6 +38,10 @@ ${c.bold("Options for init:")}
|
|
|
25
38
|
--lang=<ko|en> Agent language (default: ko \u2014 full \uBD80\uC7A5 persona)
|
|
26
39
|
--chat=<sqlite|supabase> Chat-room backend (default: sqlite \u2014 local file, no setup)
|
|
27
40
|
--commit-chat Don't gitignore .harness/ (for solo cross-machine sync via git)
|
|
41
|
+
--tools=<list> Extra tool adapters: cursor,cline,aider,codex,gemini,all
|
|
42
|
+
(Claude Code is always installed at .claude/agents/)
|
|
43
|
+
--models=<preset> Per-agent Claude model preset: balanced (recommended),
|
|
44
|
+
keep (default), cost (all haiku), quality (all opus)
|
|
28
45
|
--target=<path> Project root (default: cwd)
|
|
29
46
|
--framework=<name> Override detected framework
|
|
30
47
|
--db=<name> Override detected project DB (separate from --chat)
|
|
@@ -90,26 +107,26 @@ async function main() {
|
|
|
90
107
|
const command = args[0];
|
|
91
108
|
switch (command) {
|
|
92
109
|
case "init":
|
|
93
|
-
await (await import("./init-
|
|
110
|
+
await (await import("./init-4G7R63DX.js")).runInit(args.slice(1));
|
|
94
111
|
break;
|
|
95
112
|
case "status":
|
|
96
113
|
await (await import("./status-UE2TQQPU.js")).runStatus(args.slice(1));
|
|
97
114
|
break;
|
|
98
115
|
case "chat":
|
|
99
|
-
await (await import("./chat-
|
|
116
|
+
await (await import("./chat-XVTJ3XM7.js")).runChat(args.slice(1));
|
|
100
117
|
break;
|
|
101
118
|
case "adapt":
|
|
102
119
|
await (await import("./adapt-VPWOYF6W.js")).runAdapt(args.slice(1));
|
|
103
120
|
break;
|
|
104
121
|
case "update":
|
|
105
|
-
await (await import("./update-
|
|
122
|
+
await (await import("./update-KHDRQZQE.js")).runUpdate(args.slice(1));
|
|
106
123
|
break;
|
|
107
124
|
case "migrate":
|
|
108
125
|
await (await import("./migrate-PISZFX6C.js")).runMigrate(args.slice(1));
|
|
109
126
|
break;
|
|
110
127
|
case "--version":
|
|
111
128
|
case "-v":
|
|
112
|
-
console.log(
|
|
129
|
+
console.log(await readVersion());
|
|
113
130
|
break;
|
|
114
131
|
case "--help":
|
|
115
132
|
case "-h":
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "harness-bujang",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.1",
|
|
4
4
|
"description": "Install the Harness-Bujang multi-agent harness into any project — Director, 7 specialist teams, real-time chat-room UI. Korean and English personas. Works with Claude Code, Cursor, Cline, Aider, or any tool that reads .claude/agents/.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"claude-code",
|