harness-bujang 0.6.0 → 0.6.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.
@@ -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
- readCounts: JSON.parse(localStorage.getItem(STORAGE_KEY) || '{}'),
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 count = filterMessages(state.messages, room.id).length;
381
- const unread = Math.max(0, count - (state.readCounts[room.id] || 0));
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
- state.selectedRoom = el.getAttribute('data-room-id');
535
- const count = filterMessages(state.messages, state.selectedRoom).length;
536
- state.readCounts[state.selectedRoom] = count;
537
- localStorage.setItem(STORAGE_KEY, JSON.stringify(state.readCounts));
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
- const res = await fetch('/api/messages?days=14');
564
- const json = await res.json();
565
- state.messages = (json.data || []).map((m) => ({
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) {
package/dist/index.js CHANGED
@@ -22,7 +22,89 @@ var c = {
22
22
  yellow: (s) => `\x1B[33m${s}\x1B[39m`,
23
23
  cyan: (s) => `\x1B[36m${s}\x1B[39m`
24
24
  };
25
- var HELP = `
25
+ var HELP_KO = `
26
+ ${c.bold("harness-bujang")} \u2014 \uD55C\uAD6D\uC5B4 \uBD80\uC7A5 \uD398\uB974\uC18C\uB098 \uAE30\uBC18 \uBA40\uD2F0 \uC5D0\uC774\uC804\uD2B8 \uD558\uB124\uC2A4
27
+ ${c.dim("https://github.com/bjcho4141/harness-bujang")}
28
+
29
+ ${c.bold("\uC0AC\uC6A9\uBC95:")}
30
+ npx harness-bujang ${c.cyan("init")} [\uC635\uC158] \uD504\uB85C\uC81D\uD2B8\uC5D0 \uD558\uB124\uC2A4 \uC124\uCE58
31
+ npx harness-bujang ${c.cyan("update")} [\uC635\uC158] \uC2E0\uADDC \uC5D0\uC774\uC804\uD2B8\uB9CC \uCD94\uAC00 \u2014 \uAE30\uC874 \uD30C\uC77C \uC548 \uAC74\uB4DC\uB9BC
32
+ npx harness-bujang ${c.cyan("status")} [\uC635\uC158] \uD558\uB124\uC2A4 \uC124\uCE58 \uC0C1\uD0DC \uD655\uC778
33
+ npx harness-bujang ${c.cyan("chat")} [\uC635\uC158] \uD1A1\uBC29 viewer \uC2E4\uD589 (\uC5B4\uB5A4 \uC2A4\uD0DD\uC774\uB4E0)
34
+ npx harness-bujang ${c.cyan("adapt")} --to=<cursor|cline|aider|codex|gemini|all> \uB2E4\uB978 \uB3C4\uAD6C\uC6A9\uC73C\uB85C \uBCC0\uD658
35
+ npx harness-bujang ${c.cyan("migrate")} --to=<sqlite|supabase> \uD1A1\uBC29 \uB370\uC774\uD130 \uC774\uC804
36
+
37
+ ${c.bold("init \uC635\uC158:")}
38
+ --lang=<ko|en> \uC5D0\uC774\uC804\uD2B8 \uC5B8\uC5B4 (\uAE30\uBCF8\uAC12: ko \u2014 \uC804\uCCB4 \uBD80\uC7A5 \uD398\uB974\uC18C\uB098)
39
+ --chat=<sqlite|supabase> \uD1A1\uBC29 \uBC31\uC5D4\uB4DC (\uAE30\uBCF8\uAC12: sqlite \u2014 \uB85C\uCEEC \uD30C\uC77C, \uC14B\uC5C5 \uBD88\uD544\uC694)
40
+ --commit-chat .harness/ \uB97C gitignore \uC548 \uD568 (\uD63C\uC790 \uC5EC\uB7EC PC \uC5D0\uC11C git \uB3D9\uAE30\uD654 \uC2DC)
41
+ --tools=<list> \uCD94\uAC00 \uC5B4\uB311\uD130: cursor,cline,aider,codex,gemini,all
42
+ (Claude Code \uB294 \uD56D\uC0C1 .claude/agents/ \uC5D0 \uC790\uB3D9 \uC124\uCE58\uB428)
43
+ --models=<preset> \uC5D0\uC774\uC804\uD2B8\uBCC4 Claude \uBAA8\uB378 \uD504\uB9AC\uC14B: balanced (\uCD94\uCC9C),
44
+ keep (\uAE30\uBCF8), cost (\uC804\uBD80 haiku), quality (\uC804\uBD80 opus)
45
+ --target=<path> \uD504\uB85C\uC81D\uD2B8 \uB8E8\uD2B8 (\uAE30\uBCF8\uAC12: \uD604\uC7AC \uB514\uB809\uD1A0\uB9AC)
46
+ --framework=<name> \uAC10\uC9C0\uB41C \uD504\uB808\uC784\uC6CC\uD06C \uB36E\uC5B4\uC4F0\uAE30
47
+ --db=<name> \uAC10\uC9C0\uB41C \uD504\uB85C\uC81D\uD2B8 DB \uB36E\uC5B4\uC4F0\uAE30 (--chat \uC640 \uBCC4\uAC1C)
48
+ --no-template \uD1A1\uBC29 UI \uC124\uCE58 \uAC74\uB108\uB6F0\uAE30
49
+ --no-claude-md CLAUDE.md \uC218\uC815 \uAC74\uB108\uB6F0\uAE30
50
+ --no-learning-log \uD559\uC2B5 \uB85C\uADF8 \uC2DC\uB4DC \uAC74\uB108\uB6F0\uAE30
51
+ --yes, -y \uD504\uB86C\uD504\uD2B8 \uAC74\uB108\uB6F0\uACE0 \uB36E\uC5B4\uC4F0\uAE30 (CI / \uC2A4\uD06C\uB9BD\uD2B8\uC6A9)
52
+
53
+ ${c.dim("--yes \uC548 \uBD99\uC774\uBA74 \uC778\uD130\uB799\uD2F0\uBE0C \uC14B\uC5C5 (\uC5B8\uC5B4 / \uBC31\uC5D4\uB4DC / \uB3C4\uAD6C / \uBAA8\uB378 \uD504\uB9AC\uC14B prompt).")}
54
+
55
+ ${c.bold("chat \uC635\uC158:")}
56
+ --target=<path> \uD504\uB85C\uC81D\uD2B8 \uB8E8\uD2B8 (\uAE30\uBCF8\uAC12: \uD604\uC7AC \uB514\uB809\uD1A0\uB9AC)
57
+ --port=<number> \uD3EC\uD2B8 (\uAE30\uBCF8\uAC12: 7777, \uC0AC\uC6A9 \uC911\uC774\uBA74 \uB2E4\uC74C \uD3EC\uD2B8\uB85C)
58
+ --no-open \uBE0C\uB77C\uC6B0\uC800 \uC790\uB3D9 \uC624\uD508 \uC548 \uD568
59
+ --create \uD1A1\uBC29 DB \uAC00 \uC5C6\uC73C\uBA74 \uBE48 DB + \uC2A4\uD0A4\uB9C8 \uC0DD\uC131
60
+
61
+ ${c.bold("adapt \uC635\uC158:")}
62
+ --to=<cursor|cline|aider|codex|gemini|all> \uD544\uC218 \u2014 \uCF64\uB9C8 \uAD6C\uBD84\uC73C\uB85C \uC5EC\uB7EC \uAC1C OK
63
+ --target=<path> \uD504\uB85C\uC81D\uD2B8 \uB8E8\uD2B8 (\uAE30\uBCF8\uAC12: \uD604\uC7AC \uB514\uB809\uD1A0\uB9AC)
64
+ --yes, -y \uAE30\uC874 \uC5B4\uB311\uD130 \uD30C\uC77C \uB36E\uC5B4\uC4F0\uAE30
65
+
66
+ ${c.bold("update \uC635\uC158:")}
67
+ --target=<path> \uD504\uB85C\uC81D\uD2B8 \uB8E8\uD2B8 (\uAE30\uBCF8\uAC12: \uD604\uC7AC \uB514\uB809\uD1A0\uB9AC)
68
+ --lang=<ko|en> \uC0C8\uB85C \uCD94\uAC00\uB420 \uC5D0\uC774\uC804\uD2B8 \uC5B8\uC5B4 (\uAE30\uBCF8\uAC12: ko)
69
+
70
+ ${c.dim(" update \uB294 \uC2E0\uADDC \uC5D0\uC774\uC804\uD2B8 \uD30C\uC77C\uB9CC \uCD94\uAC00. \uAE30\uC874 \uD30C\uC77C\uC740 \uC808\uB300 \uC548 \uAC74\uB4DC\uB9BC.")}
71
+ ${c.dim(" \uC644\uC804 \uB36E\uC5B4\uC4F0\uAE30 (\uBAA8\uB4E0 \uC5D0\uC774\uC804\uD2B8 \uB9AC\uC14B) \uAC00 \uD544\uC694\uD558\uBA74: bujang init --yes")}
72
+
73
+ ${c.dim("\uC5B4\uB311\uD130 \uD0C0\uAE43:")}
74
+ ${c.dim(" cursor \u2192 .cursor/rules/bujang-*.mdc (Cursor IDE)")}
75
+ ${c.dim(" cline \u2192 .clinerules/bujang-*.md (Cline)")}
76
+ ${c.dim(" aider \u2192 CONVENTIONS.md + .aider.conf.yml (Aider)")}
77
+ ${c.dim(" codex \u2192 AGENTS.md (Codex CLI / Copilot Coding Agent / Cody)")}
78
+ ${c.dim(" gemini \u2192 GEMINI.md + .gemini/styleguide.md (Antigravity / Gemini CLI / Code Assist)")}
79
+
80
+ ${c.bold("migrate \uC635\uC158:")}
81
+ --to=<sqlite|supabase> \uD544\uC218 \u2014 \uC774\uC804\uD560 \uBC31\uC5D4\uB4DC
82
+ --target=<path> \uD504\uB85C\uC81D\uD2B8 \uB8E8\uD2B8 (\uAE30\uBCF8\uAC12: \uD604\uC7AC \uB514\uB809\uD1A0\uB9AC)
83
+ --yes, -y \uD655\uC778 \uD504\uB86C\uD504\uD2B8 \uAC74\uB108\uB6F0\uAE30
84
+
85
+ ${c.bold("\uC608\uC2DC:")}
86
+ ${c.dim("# \uD55C\uAD6D\uC5B4 \uBD80\uC7A5 \uD398\uB974\uC18C\uB098 + SQLite \uD1A1\uBC29 (\uAE30\uBCF8\uAC12 \u2014 \uC14B\uC5C5 \uBD88\uD544\uC694)")}
87
+ npx harness-bujang init --lang=ko
88
+
89
+ ${c.dim("# Standalone \uD1A1\uBC29 viewer \u2014 \uC5B4\uB5A4 \uC2A4\uD0DD\uC5D0\uC11C\uB3C4 \uB3D9\uC791 (Next.js, Rails, Django, \u2026)")}
90
+ npx harness-bujang chat
91
+ ${c.dim("# \u2192 http://localhost:7777 \uC790\uB3D9 \uC624\uD508")}
92
+
93
+ ${c.dim("# \uD63C\uC790 \uC5EC\uB7EC PC \uC5D0\uC11C \u2014 git \uC73C\uB85C \uD1A1\uBC29 \uD788\uC2A4\uD1A0\uB9AC \uB3D9\uAE30\uD654")}
94
+ npx harness-bujang init --commit-chat
95
+
96
+ ${c.dim("# \uD300 \uC6B4\uC601 \u2014 Supabase \uBC31\uC5D4\uB4DC")}
97
+ npx harness-bujang init --chat=supabase
98
+
99
+ ${c.dim("# \uC194\uB85C\uB85C \uC2DC\uC791\uD588\uB2E4\uAC00 \uD300 \uB2E8\uC704\uB85C \uD655\uC7A5 \u2014 \uD074\uB77C\uC6B0\uB4DC\uB85C \uC2B9\uACA9")}
100
+ bujang migrate --to=supabase
101
+
102
+ ${c.dim("# \uB2E4\uC2DC \uC194\uB85C / \uC544\uCE74\uC774\uBE59 \u2014 \uD074\uB77C\uC6B0\uB4DC \uB370\uC774\uD130\uB97C \uB85C\uCEEC SQLite \uB85C")}
103
+ bujang migrate --to=sqlite
104
+
105
+ ${c.dim("English help: ")}${c.bold("npx harness-bujang --help-en")}
106
+ `;
107
+ var HELP_EN = `
26
108
  ${c.bold("harness-bujang")} \u2014 Korean-style multi-agent harness director for Claude Code
27
109
  ${c.dim("https://github.com/bjcho4141/harness-bujang")}
28
110
 
@@ -113,7 +195,7 @@ async function main() {
113
195
  await (await import("./status-UE2TQQPU.js")).runStatus(args.slice(1));
114
196
  break;
115
197
  case "chat":
116
- await (await import("./chat-6VQQYMBV.js")).runChat(args.slice(1));
198
+ await (await import("./chat-XVTJ3XM7.js")).runChat(args.slice(1));
117
199
  break;
118
200
  case "adapt":
119
201
  await (await import("./adapt-VPWOYF6W.js")).runAdapt(args.slice(1));
@@ -128,14 +210,18 @@ async function main() {
128
210
  case "-v":
129
211
  console.log(await readVersion());
130
212
  break;
213
+ case "--help-en":
214
+ case "-h-en":
215
+ console.log(HELP_EN);
216
+ break;
131
217
  case "--help":
132
218
  case "-h":
133
219
  case void 0:
134
- console.log(HELP);
220
+ console.log(HELP_KO);
135
221
  break;
136
222
  default:
137
- console.error(c.red(`Unknown command: ${command}`));
138
- console.log(HELP);
223
+ console.error(c.red(`\uC54C \uC218 \uC5C6\uB294 \uBA85\uB839\uC5B4: ${command}`));
224
+ console.log(HELP_KO);
139
225
  process.exit(1);
140
226
  }
141
227
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "harness-bujang",
3
- "version": "0.6.0",
3
+ "version": "0.6.2",
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",