From a0b7b570984852d2ef58fa9075ebe1dc5d32db30 Mon Sep 17 00:00:00 2001 From: Hideaki Terai Date: Sat, 23 May 2026 21:52:11 +0900 Subject: [PATCH 1/2] =?UTF-8?q?--status=20=E3=82=AA=E3=83=97=E3=82=B7?= =?UTF-8?q?=E3=83=A7=E3=83=B3=E3=82=92=E8=BF=BD=E5=8A=A0=E3=81=97=E3=81=A6?= =?UTF-8?q?=E7=9B=B4=E8=BF=911=E9=80=B1=E9=96=93=E3=81=AESlack=E5=88=A9?= =?UTF-8?q?=E7=94=A8=E7=8A=B6=E6=B3=81=E3=82=92=E8=A1=A8=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --log-sqlite と組み合わせて使うことで、RTM接続なしにSQLiteから メッセージ数の日別推移・チャンネル別・ユーザー別のランキングを バーチャートで表示する。 Co-Authored-By: Claude Sonnet 4.6 --- bin/slack-cli-stream | 13 ++++ lib/status.js | 139 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 152 insertions(+) create mode 100644 lib/status.js diff --git a/bin/slack-cli-stream b/bin/slack-cli-stream index 20c40bd..2222e02 100644 --- a/bin/slack-cli-stream +++ b/bin/slack-cli-stream @@ -3,6 +3,7 @@ let program = require("commander"); let core = require("../lib/core"); let chalk = require("chalk"); +let { showStatus } = require("../lib/status"); let showLogo = () => { console.log(chalk.cyan(" ____ _ _ ____ _ ___ _")); @@ -24,6 +25,7 @@ program .option("--log-sqlite ", "Specify the SQLite database file path for logging") .option("--refresh-interval ", "Refresh Slack metadata interval in minutes (default: 15)") .option("--mcp-port ", "Enable MCP server on the given port (e.g. 3737)") + .option("--status", "Show Slack usage status for the past week and exit") .usage("[options] ") .parse(process.argv); @@ -33,4 +35,15 @@ if (program.rawArgs.length == 2) { process.exit(0); } +const opts = program.opts(); + +if (opts.status) { + if (!opts.logSqlite) { + console.error(chalk.red("Error: --status requires --log-sqlite ")); + process.exit(1); + } + showStatus(opts.logSqlite); + process.exit(0); +} + core.start(program); diff --git a/lib/status.js b/lib/status.js new file mode 100644 index 0000000..a3ef0ca --- /dev/null +++ b/lib/status.js @@ -0,0 +1,139 @@ +const Database = require("better-sqlite3"); +const fs = require("fs"); +const chalk = require("chalk"); + +const BAR_MAX = 20; + +function makeBar(value, max) { + if (max === 0) return ""; + return "█".repeat(Math.round((value / max) * BAR_MAX)); +} + +function formatRelativeTime(seconds) { + if (seconds < 60) return `${Math.round(seconds)} seconds ago`; + if (seconds < 3600) return `${Math.round(seconds / 60)} minutes ago`; + if (seconds < 86400) return `${Math.round(seconds / 3600)} hours ago`; + return `${Math.round(seconds / 86400)} days ago`; +} + +function formatBytes(bytes) { + if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`; + return `${(bytes / (1024 * 1024)).toFixed(1)} MB`; +} + +const showStatus = (dbPath) => { + let db; + try { + db = new Database(dbPath, { readonly: true }); + } catch (e) { + console.error(chalk.red(`Cannot open database: ${e.message}`)); + process.exit(1); + } + + let dbSize = ""; + try { + dbSize = formatBytes(fs.statSync(dbPath).size); + } catch (_) {} + + const heartbeatRow = db.prepare("SELECT last_seen_at FROM app_heartbeat WHERE id = 1").get(); + let heartbeatStr = chalk.dim("N/A"); + if (heartbeatRow) { + const diff = Date.now() / 1000 - heartbeatRow.last_seen_at; + heartbeatStr = diff < 180 + ? chalk.green(formatRelativeTime(diff)) + : chalk.yellow(formatRelativeTime(diff)); + } + + const total7d = db.prepare(` + SELECT COUNT(*) as count FROM messages + WHERE logged_at >= datetime('now', '-7 days', 'localtime') + `).get().count; + + const todayCount = db.prepare(` + SELECT COUNT(*) as count FROM messages + WHERE date(logged_at) = date('now', 'localtime') + `).get().count; + + const dayRows = db.prepare(` + SELECT date(logged_at) as day, COUNT(*) as count + FROM messages + WHERE logged_at >= datetime('now', '-7 days', 'localtime') + GROUP BY day + ORDER BY day + `).all(); + + const channelRows = db.prepare(` + SELECT channel, COUNT(*) as count + FROM messages + WHERE logged_at >= datetime('now', '-7 days', 'localtime') + GROUP BY channel + ORDER BY count DESC + LIMIT 7 + `).all(); + + const userRows = db.prepare(` + SELECT user, COUNT(*) as count + FROM messages + WHERE logged_at >= datetime('now', '-7 days', 'localtime') + GROUP BY user + ORDER BY count DESC + LIMIT 7 + `).all(); + + db.close(); + + const avg7d = total7d > 0 ? Math.round(total7d / 7) : 0; + + console.log(); + console.log(chalk.bold.green("● Slack Stream Status")); + console.log(); + + console.log(chalk.bold(" App Health")); + console.log(` ├─ Last heartbeat : ${heartbeatStr}`); + console.log(` └─ Database : ${chalk.cyan(dbPath)} ${chalk.dim(`(${dbSize})`)}`); + console.log(); + + console.log(chalk.bold(" Messages — Last 7 Days")); + console.log(` ├─ Total : ${chalk.cyan(total7d.toLocaleString())} messages`); + console.log(` ├─ Daily average : ${chalk.cyan(avg7d.toLocaleString())} messages/day`); + console.log(` └─ Today : ${chalk.cyan(todayCount.toLocaleString())} messages`); + console.log(); + + if (dayRows.length > 0) { + console.log(chalk.bold(" Activity by Day")); + const maxDay = Math.max(...dayRows.map(r => r.count)); + for (const row of dayRows) { + const label = row.day.slice(5).replace("-", "/"); + const bar = makeBar(row.count, maxDay).padEnd(BAR_MAX); + const count = String(row.count).padStart(5); + console.log(` ${label} ${chalk.green(bar)} ${chalk.dim(count)}`); + } + console.log(); + } + + if (channelRows.length > 0) { + console.log(chalk.bold(" Top Active Channels (Last 7 Days)")); + const maxCh = channelRows[0].count; + for (const row of channelRows) { + const name = (row.channel || "(unknown)").padEnd(22); + const bar = makeBar(row.count, maxCh).padEnd(BAR_MAX); + const count = String(row.count).padStart(5); + console.log(` ${name} ${chalk.blue(bar)} ${chalk.dim(count)}`); + } + console.log(); + } + + if (userRows.length > 0) { + console.log(chalk.bold(" Top Active Users (Last 7 Days)")); + const maxU = userRows[0].count; + for (const row of userRows) { + const name = (row.user || "(unknown)").padEnd(22); + const bar = makeBar(row.count, maxU).padEnd(BAR_MAX); + const count = String(row.count).padStart(5); + console.log(` ${name} ${chalk.yellow(bar)} ${chalk.dim(count)}`); + } + console.log(); + } +}; + +module.exports = { showStatus }; From 8ab0a99bd597c92fe9d45a929e3cf4a73eac5340 Mon Sep 17 00:00:00 2001 From: Hideaki Terai Date: Sat, 23 May 2026 21:54:06 +0900 Subject: [PATCH 2/2] =?UTF-8?q?lint=E3=82=A8=E3=83=A9=E3=83=BC=E4=BF=AE?= =?UTF-8?q?=E6=AD=A3:=20=E7=A9=BA=E3=81=AEcatch=E3=83=96=E3=83=AD=E3=83=83?= =?UTF-8?q?=E3=82=AF=E3=81=AB=E3=82=B3=E3=83=A1=E3=83=B3=E3=83=88=E3=82=92?= =?UTF-8?q?=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- lib/status.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/status.js b/lib/status.js index a3ef0ca..eebff45 100644 --- a/lib/status.js +++ b/lib/status.js @@ -33,7 +33,7 @@ const showStatus = (dbPath) => { let dbSize = ""; try { dbSize = formatBytes(fs.statSync(dbPath).size); - } catch (_) {} + } catch (_) { /* ignore stat errors */ } const heartbeatRow = db.prepare("SELECT last_seen_at FROM app_heartbeat WHERE id = 1").get(); let heartbeatStr = chalk.dim("N/A");