From c7d7e44c336c3a8d8886eba03075e7f6e9dc7b21 Mon Sep 17 00:00:00 2001 From: Hideaki Terai Date: Sat, 20 Jun 2026 22:22:49 +0900 Subject: [PATCH 1/3] =?UTF-8?q?=E5=85=A5=E5=8A=9B=E5=8F=97=E4=BB=98?= =?UTF-8?q?=E3=81=AB=20#=20=E3=83=81=E3=83=A3=E3=83=B3=E3=83=8D=E3=83=AB?= =?UTF-8?q?=E5=90=8DTab=E8=A3=9C=E5=AE=8C=E3=81=A8=E7=9B=B4=E8=BF=91?= =?UTF-8?q?=E3=83=AD=E3=82=B0=E5=86=8D=E8=A1=A8=E7=A4=BA=E3=82=92=E8=BF=BD?= =?UTF-8?q?=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - cli.js: readline に completer を渡せるようにし、# 始まり入力を channelRecent ハンドラへルーティング - core.js: channelCompleter で # 入力時にチャンネル名候補を列挙 (util.channels に加え、MCP由来など util.buffer の # キーも対象) - core.js: showRecent で確定チャンネルの直近ログを再表示。SQLite優先 (channel_id 未解決時は labelKey で引き MCP由来 #claude 等も取得可)、 無効/失敗時はメモリバッファにフォールバック - #channel N で件数指定にも対応 Co-Authored-By: Claude Opus 4.8 --- lib/cli.js | 25 +++++++++++- lib/core.js | 112 +++++++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 134 insertions(+), 3 deletions(-) diff --git a/lib/cli.js b/lib/cli.js index e14aaf1..7ab95ff 100644 --- a/lib/cli.js +++ b/lib/cli.js @@ -1,15 +1,36 @@ var readline = require("readline"); -function Cli(handler) { +function Cli(handler, opts) { this.handler = handler; + this.opts = opts || {}; } Cli.prototype.run = function() { var self = this; - var rli = readline.createInterface(process.stdin, process.stdout); + var rliOpts = { input: process.stdin, output: process.stdout }; + + // # 入力時にチャンネル名候補を Tab 補完する + if (typeof self.opts.completer === "function") { + rliOpts.completer = self.opts.completer; + } + + var rli = readline.createInterface(rliOpts); rli.setPrompt("> "); rli.on("line", function(line) { + var trimmed = line.trim(); + + // "#channel" で始まる入力は該当チャンネルの直近ログ表示にルーティングする + if (trimmed.charAt(0) === "#" && typeof self.handler.channelRecent === "function") { + self.handler.channelRecent.call(rli, trimmed, function(err) { + if (err) { + console.error("エラー発生: " + err.message); + } + rli.prompt(); + }); + return; + } + var args = line.split(/\s+/), cmd = args.shift(); if (self.handler[cmd]) { diff --git a/lib/core.js b/lib/core.js index 721fe86..b1f2373 100644 --- a/lib/core.js +++ b/lib/core.js @@ -793,9 +793,119 @@ core.start = async (commander) => { rtm.start(); }; +// "#" 入力時のチャンネル名 Tab 補完 +let channelCompleter = (line) => { + // "#" で始まる入力のみ補完対象 + if (line.charAt(0) !== "#") { + return [[], line]; + } + + let prefix = line.slice(1).toLowerCase(); + let names = []; + + Object.keys(util.channels).forEach((id) => { + let ch = util.channels[id]; + if (ch.is_im) { + return; // DM は対象外 + } + let name = resolveChannelName(id); + if (name) { + names.push("#" + name); + } + }); + + // MCP(post_to_stream)由来など、Slack外から注入されたチャンネルラベルも候補に含める。 + // これらは util.channels には存在せず、バッファキー("#claude"等)としてのみ現れる。 + Object.keys(util.buffer || {}).forEach((key) => { + if (key.charAt(0) === "#") { + names.push(key); + } + }); + + names = Array.from(new Set(names)).sort(); + + let hits = names.filter((n) => n.toLowerCase().indexOf("#" + prefix) === 0); + + return [hits.length ? hits : names, line]; +}; + +// 指定チャンネルの直近ログを再表示する。SQLite を優先し、無効時はメモリバッファにフォールバック。 +core.showRecent = (labelKey, channelId, limit) => { + let label = { + lines: [`--- Show recent (${labelKey}) ---`], + time: moment() + }; + core.display(label); + + let shown = false; + + // Slackチャンネルは解決済みID、MCP等の注入チャンネルは labelKey("#claude") が + // そのまま channel_id として記録されているため、未解決時は labelKey で引く。 + let queryChannelId = channelId || labelKey; + + if (sqliteDb && queryChannelId) { + try { + let rows = sqliteDb.prepare(` + SELECT logged_at, channel, user, message + FROM messages + WHERE channel_id = ? + ORDER BY id DESC + LIMIT ? + `).all(queryChannelId, limit); + + rows.reverse().forEach((row) => { + core.display({ + lines: [row.message || ""], + time: moment(row.logged_at, "YYYY-MM-DD HH:mm:ss"), + channel: row.channel, + user: row.user + }); + }); + + shown = rows.length > 0; + } catch (e) { + // SQLite 取得に失敗した場合はメモリバッファへフォールバック + } + } + + if (!shown && util.buffer[labelKey]) { + util.buffer[labelKey].forEach((data) => { + core.display(data); + }); + shown = true; + } + + label.lines = shown ? ["--- finish ---"] : ["(ログがありません)"]; + core.display(label); +}; + // Declare cli-handler function handler() {} +handler.prototype.channelRecent = function(line, fn) { + // line 例: "#general" または "#general 50" (件数指定) + let parts = line.split(/\s+/); + let labelKey = parts[0]; // "#general" + let limit = parseInt(parts[1], 10); + if (isNaN(limit) || limit <= 0) { + limit = 20; + } + + let name = labelKey.slice(1); // "general" + let channelId = null; + + if (name.length > 0) { + Object.keys(util.channels).forEach((id) => { + if (channelId === null && resolveChannelName(id) === name) { + channelId = id; + } + }); + } + + core.showRecent(labelKey, channelId, limit); + fn(null, line); +}; + handler.prototype.recent = function(args, fn) { if (util.buffer[args[0]]) { let messageBuffer = util.buffer[args[0]]; @@ -828,6 +938,6 @@ handler.prototype.exit = function(args, fn) { this.emit("close"); }; -(new cli(new handler())).run(); +(new cli(new handler(), { completer: channelCompleter })).run(); module.exports = core; From 587861def229af93a8fe5f8e05cc540bcad61df7 Mon Sep 17 00:00:00 2001 From: Hideaki Terai Date: Sat, 20 Jun 2026 22:24:18 +0900 Subject: [PATCH 2/3] =?UTF-8?q?=E9=81=8E=E5=8E=BB=E3=82=BB=E3=83=83?= =?UTF-8?q?=E3=82=B7=E3=83=A7=E3=83=B3=E3=81=AE=E6=B3=A8=E5=85=A5=E3=83=81?= =?UTF-8?q?=E3=83=A3=E3=83=B3=E3=83=8D=E3=83=AB=E3=82=82=E8=B5=B7=E5=8B=95?= =?UTF-8?q?=E6=99=82=E3=81=ABSQLite=E3=81=8B=E3=82=89=E8=A3=9C=E5=AE=8C?= =?UTF-8?q?=E5=80=99=E8=A3=9C=E3=81=B8=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - sqlite-logger.js: getInjectedChannelLabels を追加。channel_id が "#" 始まり(MCP等の注入チャンネル)を DISTINCT で取得 - core.js: 起動時に SQLite から注入チャンネルラベルを読み込み、 channelCompleter の候補に追加。今セッション未受信の #claude 等も Tab 一覧に出るようになる - sqlite_logger_test.js: getInjectedChannelLabels のテストを追加 Co-Authored-By: Claude Opus 4.8 --- lib/core.js | 18 +++++++++++++++++- lib/sqlite-logger.js | 14 +++++++++++++- test/sqlite_logger_test.js | 31 ++++++++++++++++++++++++++++++- 3 files changed, 60 insertions(+), 3 deletions(-) diff --git a/lib/core.js b/lib/core.js index b1f2373..3a29b5a 100644 --- a/lib/core.js +++ b/lib/core.js @@ -9,10 +9,12 @@ let cli = require("./cli.js"); const path = require("path"); const fs = require("fs"); const exec = require("child_process").exec; -const { initSqliteDb, logMessageSqlite, getLastSlackTsPerChannel, updateAppHeartbeat, getLastAppHeartbeat } = require("./sqlite-logger"); +const { initSqliteDb, logMessageSqlite, getLastSlackTsPerChannel, updateAppHeartbeat, getLastAppHeartbeat, getInjectedChannelLabels } = require("./sqlite-logger"); const { startMcpServer } = require("./mcp-server"); let sqliteDb = null; +// 起動時に SQLite から読み込む、MCP等の注入チャンネルラベル一覧(Tab補完候補用) +let injectedChannelLabels = []; let getLogger = (filePath) => { return winston.createLogger({ @@ -236,6 +238,13 @@ core.start = async (commander) => { } updateAppHeartbeat(sqliteDb); setInterval(() => updateAppHeartbeat(sqliteDb), 60 * 1000); + + // 過去セッションで注入された MCP 等のチャンネルも Tab 補完候補に含める + try { + injectedChannelLabels = getInjectedChannelLabels(sqliteDb); + } catch (e) { + injectedChannelLabels = []; + } } // Slack 以外のソース(Claude等のAIエージェント)から任意のメッセージを @@ -822,6 +831,13 @@ let channelCompleter = (line) => { } }); + // 今セッションでまだ受信していない、過去の注入チャンネルも SQLite から候補に含める + injectedChannelLabels.forEach((label) => { + if (label && label.charAt(0) === "#") { + names.push(label); + } + }); + names = Array.from(new Set(names)).sort(); let hits = names.filter((n) => n.toLowerCase().indexOf("#" + prefix) === 0); diff --git a/lib/sqlite-logger.js b/lib/sqlite-logger.js index 66ce727..0509368 100644 --- a/lib/sqlite-logger.js +++ b/lib/sqlite-logger.js @@ -80,4 +80,16 @@ const getLastSlackTsPerChannel = (db) => { return map; }; -module.exports = { initSqliteDb, logMessageSqlite, getLastSlackTsPerChannel, updateAppHeartbeat, getLastAppHeartbeat }; +// MCP(post_to_stream)等、Slack外から注入されたチャンネルは channel_id が +// "#label" 形式で記録される。それらのラベル一覧を取得する(Tab補完候補用)。 +const getInjectedChannelLabels = (db) => { + const rows = db.prepare(` + SELECT DISTINCT channel_id + FROM messages + WHERE channel_id LIKE '#%' + ORDER BY channel_id + `).all(); + return rows.map((row) => row.channel_id); +}; + +module.exports = { initSqliteDb, logMessageSqlite, getLastSlackTsPerChannel, updateAppHeartbeat, getLastAppHeartbeat, getInjectedChannelLabels }; diff --git a/test/sqlite_logger_test.js b/test/sqlite_logger_test.js index bd4bae1..254b16a 100644 --- a/test/sqlite_logger_test.js +++ b/test/sqlite_logger_test.js @@ -2,7 +2,7 @@ let assert = require("chai").assert; let fs = require("fs"); let os = require("os"); let path = require("path"); -let { initSqliteDb, logMessageSqlite } = require("../lib/sqlite-logger"); +let { initSqliteDb, logMessageSqlite, getInjectedChannelLabels } = require("../lib/sqlite-logger"); describe("SQLiteロガーのテスト", () => { let db; @@ -182,4 +182,33 @@ describe("SQLiteロガーのテスト", () => { assert.equal(rows.length, 0, "0件ヒットする"); }); }); + + describe("getInjectedChannelLabels", () => { + it("注入チャンネル(channel_idが#始まり)のラベルが取得できること", () => { + logMessageSqlite(db, "2026-04-05 12:00:00", "#claude", "claude", "progress", "#claude", "claude", null, null); + logMessageSqlite(db, "2026-04-05 12:00:01", "#agent", "claude", "done", "#agent", "claude", null, null); + const labels = getInjectedChannelLabels(db); + assert.deepEqual(labels, ["#agent", "#claude"], "#始まりのchannel_idがソートされて返る"); + }); + + it("Slackチャンネル(channel_idがID形式)は含まれないこと", () => { + logMessageSqlite(db, "2026-04-05 12:00:00", "#general", "alice", "Hello", "C0123", "U0001", "100.000", null); + logMessageSqlite(db, "2026-04-05 12:00:01", "#claude", "claude", "progress", "#claude", "claude", null, null); + const labels = getInjectedChannelLabels(db); + assert.deepEqual(labels, ["#claude"], "Slackチャンネルは除外される"); + }); + + it("同一ラベルは重複排除されること", () => { + logMessageSqlite(db, "2026-04-05 12:00:00", "#claude", "claude", "line1", "#claude", "claude", null, null); + logMessageSqlite(db, "2026-04-05 12:00:01", "#claude", "claude", "line2", "#claude", "claude", null, null); + const labels = getInjectedChannelLabels(db); + assert.deepEqual(labels, ["#claude"], "DISTINCTで1件になる"); + }); + + it("注入チャンネルがない場合は空配列が返ること", () => { + logMessageSqlite(db, "2026-04-05 12:00:00", "#general", "alice", "Hello", "C0123", "U0001", "100.000", null); + const labels = getInjectedChannelLabels(db); + assert.deepEqual(labels, [], "空配列が返る"); + }); + }); }); From 5570ab2e26f1af13c2341924ee9be274eb1ece3d Mon Sep 17 00:00:00 2001 From: Hideaki Terai Date: Sat, 20 Jun 2026 22:35:27 +0900 Subject: [PATCH 3/3] =?UTF-8?q?MCP=E7=94=B1=E6=9D=A5=E3=83=A1=E3=83=83?= =?UTF-8?q?=E3=82=BB=E3=83=BC=E3=82=B8=E3=82=92=E3=82=B9=E3=83=88=E3=83=AA?= =?UTF-8?q?=E3=83=BC=E3=83=A0=E8=A1=A8=E7=A4=BA=E3=81=A7=20[MCP]=20?= =?UTF-8?q?=E3=83=9E=E3=83=BC=E3=82=AB=E3=83=BC=E4=BB=98=E4=B8=8E=E3=81=97?= =?UTF-8?q?=E3=81=A6=E5=8C=BA=E5=88=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - core.js: synthetic(post_to_stream注入)メッセージのチャンネル列に [MCP] プレフィックスを付与(既存cyanも併用)。removeEscapeSequencesで 色は落ちるがタグはログ/SQLite/TSVに残り grep 可能 Co-Authored-By: Claude Opus 4.8 --- lib/core.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/core.js b/lib/core.js index 3a29b5a..4c39e61 100644 --- a/lib/core.js +++ b/lib/core.js @@ -113,8 +113,10 @@ core.display = (data, options) => { // Claude(AI)など、Slack外のソースから注入されたメッセージ。 // data.channel / data.user は Slack ID ではなく任意のラベル文字列なので // util.users / resolveChannelLabelDisplay による ID 解決はバイパスする。 + // [MCP] マーカーを付けて Slack 由来のメッセージと区別する(ログにも残る)。 + let channelLabel = typeof data.channel == "string" ? data.channel : "-"; name = chalk.cyan(typeof data.user == "string" ? data.user : "-"); - channel = chalk.cyan(typeof data.channel == "string" ? data.channel : "-"); + channel = chalk.cyan("[MCP] " + channelLabel); } else { if (util.users[data.user]) { name = chalk[util.users[data.user].color](util.users[data.user].name);