Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -224,7 +224,7 @@ Upgrade with `brew upgrade --cask mm7894215/tokentracker/tokentracker`. The tap
| **GitHub Copilot App / CLI** | ✅ Auto | Unified per-request SQLite usage (`~/.copilot/session-store.db`); App DB legacy baseline |
| **GitHub Copilot Chat extension / legacy CLI** | ✅ Auto | OpenTelemetry file exporter (`COPILOT_OTEL_FILE_EXPORTER_PATH`) |
| **Kimi Code** | ✅ Auto | Passive `wire.jsonl` reader (`~/.kimi/sessions/**/wire.jsonl`) |
| **oh-my-pi (Pi Coding Agent)** | ✅ Auto | Passive reader (`~/.omp/agent/sessions/**/*.jsonl`) |
| **oh-my-pi (Pi Coding Agent)** | ✅ Auto | Passive reader (`~/.omp/agent/sessions/**/*.jsonl`) + managed notify extension written by `tokentracker init` to `~/.omp/agent/extensions/tokentracker-notify.ts` for near-real-time sync (skipped if a same-named unmanaged file already exists; removed by `tokentracker uninstall` when still managed) |
| **CodeBuddy** (Tencent) | ✅ Auto | SessionEnd hook in `~/.codebuddy/settings.json` (Claude-Code fork) |
| **WorkBuddy** (Tencent) | ✅ Auto | SessionEnd hook in `~/.workbuddy/settings.json` (Claude-Code fork) + passive `projects/**/*.jsonl` scan |
| **Grok Build** (xAI) | ✅ Auto | SessionEnd hook + passive `updates.jsonl` / `signals.json` scan (`~/.grok/sessions/**/`) |
Expand All @@ -246,7 +246,8 @@ Upgrade with `brew upgrade --cask mm7894215/tokentracker/tokentracker`. The tap
> **Do I need to install any plugin or hook manually?** No. `tokentracker` (or `tokentracker init`) handles everything on first run:
> - **Hook-based** tools (Claude Code, Codex, Gemini, Every Code, **CodeBuddy**, **WorkBuddy**, **Grok Build**) — we write a SessionEnd hook or TOML notify entry into the tool's own config.
> - **Plugin-based** tools (OpenCode, **OpenClaw**) — plugins ship inside the npm package. OpenClaw's session plugin lives at `~/.tokentracker/tracker/openclaw-plugin/openclaw-session-sync/`; we link and enable it via OpenClaw's own CLI, then set `hooks.allowConversationAccess=true` so OpenClaw permits the session-finished event that triggers sync. No download, no drag-and-drop.
> - **Passive readers** (Cursor, Kiro, Hermes, Kimi Code, Copilot, **Grok Build**, **oh-my-pi**, **pi**, **Craft Agents**, **Kilo CLI**, **Kilo Code**, **Roo Code**, **Antigravity**, **Zed Agent**, **Goose**, **Droid**, **Mimo Code**, **ZCode**, **Qoder**, **AnythingLLM Desktop**, **Claude Science**) — nothing is installed into those tools. We only read files they already produce (SQLite DB, JSONL, OTEL export, session logs). Copilot App / CLI usage is read per request from `~/.copilot/session-store.db`; `data.db` provides the one-time legacy adoption baseline and stays observe-only after the store becomes canonical, while the Chat extension and legacy CLI continue using OTEL. TokenTracker coordinates the sources so overlapping requests are counted once. Mixed App/CLI usage that predates adoption is retained as a `github-copilot-legacy` aggregate rather than assigned to a guessed request model.
> - **oh-my-pi** — passive session scan is always the billing/token source of truth (`~/.omp/agent/sessions/**/*.jsonl`). When OMP is detected, `tokentracker init` also writes a managed notify extension to `~/.omp/agent/extensions/tokentracker-notify.ts` so turns can trigger near-real-time sync. That file is owned only when it carries TokenTracker's managed marker: a same-named user-authored extension is never overwritten or deleted. `tokentracker uninstall` removes the extension only while it is still managed.
> - **Passive readers** (Cursor, Kiro, Hermes, Kimi Code, Copilot, **Grok Build**, **pi**, **Craft Agents**, **Kilo CLI**, **Kilo Code**, **Roo Code**, **Antigravity**, **Zed Agent**, **Goose**, **Droid**, **Mimo Code**, **ZCode**, **Qoder**, **AnythingLLM Desktop**, **Claude Science**) — nothing is installed into those tools. We only read files they already produce (SQLite DB, JSONL, OTEL export, session logs). Copilot App / CLI usage is read per request from `~/.copilot/session-store.db`; `data.db` provides the one-time legacy adoption baseline and stays observe-only after the store becomes canonical, while the Chat extension and legacy CLI continue using OTEL. TokenTracker coordinates the sources so overlapping requests are counted once. Mixed App/CLI usage that predates adoption is retained as a `github-copilot-legacy` aggregate rather than assigned to a guessed request model.
> - **Grok Build estimate** — current local telemetry exposes cumulative `updates.jsonl` `totalTokens`, but not a stable prompt/output/cache split; `signals.json` remains a fallback with `contextTokensUsed` snapshots. TokenTracker estimates Grok cost until per-call usage details are available.
>
> Run `tokentracker status` anytime to verify every integration's state. If something shows `skipped`, the `detail` column explains why (e.g. tool CLI not on `PATH`, config unreadable).
Expand Down
5 changes: 3 additions & 2 deletions README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,7 @@ brew install mm7894215/tokentracker/tokentracker
| **GitHub Copilot App / CLI** | ✅ 自动 | 统一逐请求 SQLite 用量(`~/.copilot/session-store.db`);App DB 作为旧数据基线 |
| **GitHub Copilot Chat 扩展 / 旧版 CLI** | ✅ 自动 | OpenTelemetry 文件导出(`COPILOT_OTEL_FILE_EXPORTER_PATH`) |
| **Kimi Code** | ✅ 自动 | 被动读取 `wire.jsonl`(`~/.kimi/sessions/**/wire.jsonl`) |
| **oh-my-pi (Pi Coding Agent)** | ✅ 自动 | 被动读取(`~/.omp/agent/sessions/**/*.jsonl`) |
| **oh-my-pi (Pi Coding Agent)** | ✅ 自动 | 被动读取(`~/.omp/agent/sessions/**/*.jsonl`)+ `tokentracker init` 会写入托管的 notify 扩展(`~/.omp/agent/extensions/tokentracker-notify.ts`)用于近实时同步(若同名非托管文件已存在则跳过;`tokentracker uninstall` 仅在仍为托管时删除) |
| **CodeBuddy** (腾讯) | ✅ 自动 | 写入 `~/.codebuddy/settings.json` 的 SessionEnd hook(Claude-Code fork) |
| **WorkBuddy** (腾讯) | ✅ 自动 | 写入 `~/.workbuddy/settings.json` 的 SessionEnd hook(Claude-Code fork)+ 被动扫描 `projects/**/*.jsonl` |
| **Grok Build** (xAI) | ✅ 自动 | SessionEnd hook + 被动扫描 `updates.jsonl` / `signals.json`(`~/.grok/sessions/**/`) |
Expand All @@ -200,7 +200,8 @@ brew install mm7894215/tokentracker/tokentracker
> **需要手动装什么插件 / hook 吗?** 不需要。`tokentracker`(或 `tokentracker init`)第一次跑的时候会全部搞定:
> - **基于 hook 的工具**(Claude Code、Codex、Gemini、Every Code、**CodeBuddy**、**WorkBuddy**、**Grok Build**)—— 我们把 SessionEnd hook 或 TOML notify 条目写入它们自己的配置文件
> - **基于插件的工具**(OpenCode、**OpenClaw**)—— 插件随 npm 包一起分发。OpenClaw 的 session plugin 位于 `~/.tokentracker/tracker/openclaw-plugin/openclaw-session-sync/`;TokenTracker 会通过 OpenClaw 自己的 CLI 挂接并启用它,然后写入 `hooks.allowConversationAccess=true`,让 OpenClaw 放行触发同步的会话结束事件。无需下载、无需拖拽
> - **被动读取类**(Cursor、Kiro、Hermes、Kimi Code、Copilot、**Grok Build**、**oh-my-pi**、**pi**、**Craft Agents**、**Kilo CLI**、**Kilo Code**、**Roo Code**、**Antigravity**、**Zed Agent**、**Goose**、**Droid**、**Mimo Code**、**ZCode**、**AnythingLLM Desktop**、**Claude Science**)—— 完全不往它们里面塞东西,只读取它们自己产生的文件(SQLite DB、JSONL、OTEL 导出、会话轨迹日志)。Copilot App / CLI 的用量按请求读取 `~/.copilot/session-store.db`;`data.db` 只提供一次旧数据迁移基线,并在 store 成为主源后保持仅观察,Chat 扩展和旧版 CLI 继续使用 OTEL。TokenTracker 会协调这些数据源,使重叠请求只统计一次。迁移前无法无损拆分模型的 App/CLI 混合历史会保留为 `github-copilot-legacy` 聚合量,而不会猜测请求模型
> - **oh-my-pi** —— 被动会话扫描始终是计费/token 的事实来源(`~/.omp/agent/sessions/**/*.jsonl`)。检测到 OMP 时,`tokentracker init` 还会写入托管的 notify 扩展到 `~/.omp/agent/extensions/tokentracker-notify.ts`,以便回合结束后近实时触发同步。仅当文件带有 TokenTracker 托管标记时才视为我们拥有:同名用户自写扩展绝不会被覆盖或删除。`tokentracker uninstall` 也仅在文件仍为托管状态时删除它。
> - **被动读取类**(Cursor、Kiro、Hermes、Kimi Code、Copilot、**Grok Build**、**pi**、**Craft Agents**、**Kilo CLI**、**Kilo Code**、**Roo Code**、**Antigravity**、**Zed Agent**、**Goose**、**Droid**、**Mimo Code**、**ZCode**、**AnythingLLM Desktop**、**Claude Science**)—— 完全不往它们里面塞东西,只读取它们自己产生的文件(SQLite DB、JSONL、OTEL 导出、会话轨迹日志)。Copilot App / CLI 的用量按请求读取 `~/.copilot/session-store.db`;`data.db` 只提供一次旧数据迁移基线,并在 store 成为主源后保持仅观察,Chat 扩展和旧版 CLI 继续使用 OTEL。TokenTracker 会协调这些数据源,使重叠请求只统计一次。迁移前无法无损拆分模型的 App/CLI 混合历史会保留为 `github-copilot-legacy` 聚合量,而不会猜测请求模型
> - **Grok Build 估算说明** —— Grok 当前本地遥测提供 `updates.jsonl` 里的累计 `totalTokens`,但还没有稳定的输入/输出/cache 拆分;`signals.json` 仍作为 `contextTokensUsed` 快照兜底。所以在 Grok 提供按调用粒度的用量明细之前,TokenTracker 对 Grok 成本仍是估算值
>
> 任何时候都可以用 `tokentracker status` 查看每个集成的状态。如果显示 `skipped`,`detail` 列会解释原因(例如某工具 CLI 不在 `PATH` 上、config 不可读等)。
Expand Down
33 changes: 33 additions & 0 deletions dashboard/public/brand-logos/omp.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
16 changes: 16 additions & 0 deletions dashboard/public/brand-logos/pi.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions dashboard/src/lib/provider-display.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
const SPECIAL_PROVIDER_NAMES = {
anythingllm: "AnythingLLM",
claudescience: "Claude Science",
omp: "oh-my-pi",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Move the new display label into the dashboard copy source.

"oh-my-pi" is user-facing text and is hardcoded in dashboard/src/lib/provider-display.js. Add it to dashboard/src/content/copy.csv and use the existing content lookup instead.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@dashboard/src/lib/provider-display.js` at line 4, Move the user-facing
"oh-my-pi" label from the provider mapping in provider-display.js into copy.csv,
then replace the hardcoded value with the existing content lookup mechanism.
Preserve the provider mapping behavior while ensuring the label is sourced
through dashboard copy content.

Source: Path instructions

pianthropic: "Pi · Anthropic",
pigithubcopilot: "Pi · GitHub Copilot",
picopilot: "Pi · Copilot",
Expand Down
5 changes: 5 additions & 0 deletions dashboard/src/lib/provider-display.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,9 @@ describe("formatProviderDisplayName", () => {
expect(formatProviderDisplayName("pi-anthropic")).toBe("Pi · Anthropic");
expect(formatProviderDisplayName("PI-GITHUB-COPILOT")).toBe("Pi · GitHub Copilot");
});

it("formats omp as oh-my-pi", () => {
expect(formatProviderDisplayName("omp")).toBe("oh-my-pi");
expect(formatProviderDisplayName("OMP")).toBe("oh-my-pi");
});
});
6 changes: 6 additions & 0 deletions dashboard/src/ui/dashboard/components/ProviderIcon.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -320,8 +320,14 @@ const PROVIDER_LOGO_MAP = {
"KILO-CLI": "/brand-logos/kilo.svg",
"KILO-CODE": "/brand-logos/kilo.svg",
MIMO: "/brand-logos/mimo.svg",
// oh-my-pi / pi: multi-color brand marks (pi letterform + plugin connector).
OMP: "/brand-logos/omp.svg",
OPENCLAW: "/brand-logos/openclaw.svg",
OPENCODE: "/brand-logos/opencode.svg",
PI: "/brand-logos/pi.svg",
"PI-ANTHROPIC": "/brand-logos/pi.svg",
"PI-GITHUB-COPILOT": "/brand-logos/pi.svg",
"PI-COPILOT": "/brand-logos/pi.svg",
QODER: "/brand-logos/qoder.svg",
};

Expand Down
15 changes: 15 additions & 0 deletions dashboard/src/ui/dashboard/components/ProviderIcon.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,4 +40,19 @@ describe("ProviderIcon", () => {
expect(icon?.querySelector("path")).not.toBeNull();
expect(icon?.querySelector("circle")).toBeNull();
});

it("renders the multi-color oh-my-pi brand logo", () => {
const { container } = render(<ProviderIcon provider="omp" size={20} />);
const icon = container.querySelector('img[src="/brand-logos/omp.svg"]');
expect(icon).not.toBeNull();
expect(icon).toHaveAttribute("width", "20");
expect(icon).toHaveAttribute("height", "20");
});

it("renders the multi-color Pi brand logo", () => {
const { container } = render(<ProviderIcon provider="pi" size={18} />);
const icon = container.querySelector('img[src="/brand-logos/pi.svg"]');
expect(icon).not.toBeNull();
expect(icon).toHaveAttribute("width", "18");
});
});
57 changes: 49 additions & 8 deletions src/commands/init.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,10 @@ const {
removeGrokHook,
GROK_HOOK_FILENAME
} = require("../lib/grok-hook");
const {
upsertOmpHook,
probeOmpHookState,
} = require("../lib/omp-hook");
const { resolveTrackerPaths } = require("../lib/tracker-paths");
const {
resolveOmpAgentDir,
Expand Down Expand Up @@ -361,6 +365,7 @@ async function runSetup({
trackerDir,
notifyPath,
notifyOriginalPath,
dryRun: Boolean(opts.dryRun),
});

return {
Expand Down Expand Up @@ -551,7 +556,13 @@ function buildIntegrationTargets({ home, trackerDir, notifyPath }) {
};
}

async function applyIntegrationSetup({ home, trackerDir, notifyPath, notifyOriginalPath }) {
async function applyIntegrationSetup({
home,
trackerDir,
notifyPath,
notifyOriginalPath,
dryRun = false,
}) {
const context = buildIntegrationTargets({ home, trackerDir, notifyPath });
context.notifyOriginalPath = notifyOriginalPath;

Expand Down Expand Up @@ -642,15 +653,44 @@ async function applyIntegrationSetup({ home, trackerDir, notifyPath, notifyOrigi
}
}

// oh-my-pi: passive reader — no hook installation needed.
// TokenTracker reads ~/.omp/agent/sessions/**/*.jsonl directly.
// oh-my-pi: passive session scan always works; also install an optional
// notify extension so turn_end triggers sync --source omp near-real-time.
{
// resolveOmpAgentDir returns null on Windows when ~/.omp doesn't exist (the
// win32 path resolver only yields a dir it can see) — null means "not
// installed", so skip rather than path.join(null, …) and crash.
const fssyncLocal = require("node:fs");
const ompAgentDir = resolveOmpAgentDir(process.env);
if (ompAgentDir && fssync.existsSync(path.join(ompAgentDir, "sessions"))) {
summary.push({ label: "oh-my-pi", status: "detected", detail: "Passive reader (no hook needed)" });
const ompSessions = ompAgentDir && fssyncLocal.existsSync(path.join(ompAgentDir, "sessions"));
if (ompAgentDir && (ompSessions || fssyncLocal.existsSync(ompAgentDir))) {
if (dryRun) {
const probe = await probeOmpHookState({ home, trackerDir, env: process.env });
summary.push({
label: "oh-my-pi",
status: "detected",
detail: probe.configured
? "Notify extension already installed (passive scan still runs)"
: "Will install notify extension + keep passive scan",
});
} else {
const result = await upsertOmpHook({ home, trackerDir, env: process.env });
Comment thread
coderabbitai[bot] marked this conversation as resolved.
if (result.written) {
summary.push({
label: "oh-my-pi",
status: "installed",
detail: `Notify extension → ${result.extensionPath} (passive scan still runs)`,
});
} else if (result.skippedReason === "unmanaged-extension-present") {
summary.push({
label: "oh-my-pi",
status: "skipped",
detail: "Existing unmanaged tokentracker-notify extension left untouched",
});
} else {
summary.push({
label: "oh-my-pi",
status: "detected",
detail: "Passive reader (notify extension not written)",
});
}
}
}
}

Expand Down Expand Up @@ -1904,6 +1944,7 @@ module.exports = {
installLocalTrackerApp,
repairCodexNotifyIntegration,
repairRuntimeIntegrations,
applyIntegrationSetup,
};

async function probeFile(p) {
Expand Down
Loading
Loading