Skip to content
Merged
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: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@

Each version's section is shown in the in-app update window and on the GitHub release.

## 1.101.0

### New
- **Pi.** The AI agents section, the notch and the MCP server now show Pi's usage (tokens, the cost Pi records, models and projects) and its sessions while it runs. Menew finds `~/.pi` and also agents built on Pi that keep the same sessions under another folder, in Pi's files or in a `sessions.db`. Like OpenCode, Pi has no shell hooks, so it shows as working when its log grew in the last few minutes.

## 1.100.1

### Fixed
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ Settings › AI lists every AI feature with how to use it and a switch to turn i
</picture>
</p>

The **AI agents** section of the Menew window shows what your coding agents are doing. **Now**: every Claude Code, Codex, Gemini CLI, Cursor, Copilot CLI, Kiro, OpenCode, Droid, Qwen Code, Amp, OpenClaw, Kimi and Goose session, with its project and whether it is working, done or **waiting for you** (a permission or a question); the notch shows “2 working” or “1 waiting for you” with an **Open** button that brings its terminal to the front; rest the pointer on the notch to see what each agent is doing right now (“Edit · session.ts”, “Bash · Run the tests”), how long it has been working, and today’s tokens, cost and limits (always there, even with no session open; a window that has already reset shows 0% and its next reset); and you get a notification when a request that took more than 20 seconds finishes. Turn it on per tool under Set up: Menew adds one command to that tool’s hooks (`~/.claude/settings.json`, `~/.codex/hooks.json`, `~/.gemini/settings.json`), keeps a `.menew-backup` copy and touches nothing else. **Plan limits**: the 5-hour and weekly windows with a bar, when each resets and, at the current pace, when you would run out, with alerts at 75% and 90%; Codex limits come from its own logs, and Claude’s are optional (see Privacy). **Usage**: tokens for today, 7, 30 or 90 days (a bar per day), 12 months (a bar per week) or all time (a bar per month), stacked by tool or combined, with the estimated cost at Anthropic API prices, what the prompt cache saved, and the top models, projects and apps (Claude Code in the terminal, the Claude desktop app or an IDE; Codex CLI or the Codex app), read from the logs each tool already keeps on your Mac. Claude Code deletes its logs after 30 days, so Menew keeps a small daily summary (tokens and cost per day, tool, app, model and project) in `~/Library/Application Support/Menew/agent-usage.json`; on first launch it reads everything the logs still have. Models loaded in **Ollama** or **LM Studio** show up with the memory they use. Tools appear only when they are on your Mac. Cursor, Copilot CLI, Kiro, Droid and Qwen Code report their state through hooks (Copilot and Kiro get a separate `menew.json` that Menew creates and removes); OpenCode, Amp, OpenClaw, Kimi and Goose have no shell hooks, so Menew shows them from the running process and the usage in their logs. Kiro counts credits instead of tokens.
The **AI agents** section of the Menew window shows what your coding agents are doing. **Now**: every Claude Code, Codex, Gemini CLI, Cursor, Copilot CLI, Kiro, OpenCode, Droid, Qwen Code, Amp, OpenClaw, Kimi, Goose and Pi session, with its project and whether it is working, done or **waiting for you** (a permission or a question); the notch shows “2 working” or “1 waiting for you” with an **Open** button that brings its terminal to the front; rest the pointer on the notch to see what each agent is doing right now (“Edit · session.ts”, “Bash · Run the tests”), how long it has been working, and today’s tokens, cost and limits (always there, even with no session open; a window that has already reset shows 0% and its next reset); and you get a notification when a request that took more than 20 seconds finishes. Turn it on per tool under Set up: Menew adds one command to that tool’s hooks (`~/.claude/settings.json`, `~/.codex/hooks.json`, `~/.gemini/settings.json`), keeps a `.menew-backup` copy and touches nothing else. **Plan limits**: the 5-hour and weekly windows with a bar, when each resets and, at the current pace, when you would run out, with alerts at 75% and 90%; Codex limits come from its own logs, and Claude’s are optional (see Privacy). **Usage**: tokens for today, 7, 30 or 90 days (a bar per day), 12 months (a bar per week) or all time (a bar per month), stacked by tool or combined, with the estimated cost at Anthropic API prices, what the prompt cache saved, and the top models, projects and apps (Claude Code in the terminal, the Claude desktop app or an IDE; Codex CLI or the Codex app), read from the logs each tool already keeps on your Mac. Claude Code deletes its logs after 30 days, so Menew keeps a small daily summary (tokens and cost per day, tool, app, model and project) in `~/Library/Application Support/Menew/agent-usage.json`; on first launch it reads everything the logs still have. Models loaded in **Ollama** or **LM Studio** show up with the memory they use. Tools appear only when they are on your Mac. Cursor, Copilot CLI, Kiro, Droid and Qwen Code report their state through hooks (Copilot and Kiro get a separate `menew.json` that Menew creates and removes); OpenCode, Amp, OpenClaw, Kimi, Goose and Pi have no shell hooks, so Menew shows them from the running process and the usage in their logs. Kiro counts credits instead of tokens. Pi is found in `~/.pi` and in any agent built on it that keeps the same session format under another folder.

**MCP server**: let your agents ask Menew. One switch adds Menew to Claude Code, Codex, Gemini CLI, Claude Desktop or Cursor (or copy the config for any other app); it runs on your Mac as `Menew --mcp`, with no network. Agents can search your Memory and notes (never protected or password notes), list recordings and read the minutes with tasks, check how the Mac is doing, see agent usage, and, with Menew open, save something to Memory, create a note or send you a notice in the notch. Each source has its own switch; the clipboard history is off unless you turn it on.

Expand Down Expand Up @@ -452,7 +452,7 @@ Recently shipped:
- [x] AI agent usage history: 90 days, 12 months and all time, per tool and per app
- [x] Automatic Memory: every screenshot, recording minutes, notes and a summary of each Claude Code, Codex and Gemini CLI session
- [x] AI agents: Claude Code, Codex and Gemini CLI usage, limits, notch alerts and an MCP server
- [x] More AI agents: Cursor, Copilot CLI, Kiro, OpenCode, Droid, Qwen Code, Amp, OpenClaw, Kimi and Goose (usage, sessions and hooks where the tool has them)
- [x] More AI agents: Cursor, Copilot CLI, Kiro, OpenCode, Droid, Qwen Code, Amp, OpenClaw, Kimi, Goose and Pi (usage, sessions and hooks where the tool has them)
- [x] Knowledge graph of memory and notes
- [x] Recordings with transcript, AI minutes, tasks to Reminders and About you
- [x] Actions on selected text: explain, document, tests, reminder, event, note, memory
Expand Down
3 changes: 3 additions & 0 deletions Sources/Menew/AgentHub.swift
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,7 @@ enum AgentProcesses {
case "cursor": return n == "cursor-agent" || n == "node" || n.hasPrefix("cursor")
case "kiro": return n.hasPrefix("kiro")
case "opencode", "droid", "kimi", "goose": return n == agent || n.hasPrefix(agent + "-")
case "pi": return n == "pi" || n == "node" || n == "bun"
default: return false
}
}
Expand Down Expand Up @@ -172,6 +173,7 @@ enum AgentProcesses {
case "openclaw": return (.openclaw, pid)
case "kimi": return (.kimi, pid)
case "goose": return (.goose, pid)
case "pi": return (.pi, pid)
case "node", "bun":
// node /opt/homebrew/bin/gemini …, node …/@github/copilot/index.js …
guard let script = rest.first?.lowercased() else { return nil }
Expand All @@ -183,6 +185,7 @@ enum AgentProcesses {
(.cursor, ["/cursor-agent", "cursor-agent/"]),
(.openclaw, ["/openclaw", "openclaw/"]),
(.opencode, ["/opencode", "opencode-ai/"]),
(.pi, ["/pi", "pi-coding-agent/"]),
]
return scripts.first { _, marks in marks.contains { script.hasSuffix($0) || ($0.hasSuffix("/") && script.contains($0)) } }.map { ($0.0, pid) }
default: return nil
Expand Down
68 changes: 67 additions & 1 deletion Sources/Menew/AgentSources.swift
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import Foundation
import SQLite3

/// O uso das outras ferramentas de IA (OpenCode, Kiro, Copilot CLI, Droid, Qwen Code, Amp, OpenClaw, Kimi, Goose),
/// O uso das outras ferramentas de IA (OpenCode, Kiro, Copilot CLI, Droid, Qwen Code, Amp, OpenClaw, Kimi, Goose, Pi),
/// lido dos arquivos que elas mesmas gravam no Mac. Cada fonte diz quais arquivos olhar e como ler um arquivo inteiro;
/// o scanner relê um arquivo só quando ele muda. Nada sai do Mac. O Cursor não guarda o uso no Mac (só no site dele).
struct AgentSource {
Expand All @@ -20,6 +20,7 @@ struct AgentSource {
AgentSource(agent: .openclaw, files: OpenClawLog.files, parse: OpenClawLog.parse),
AgentSource(agent: .kimi, files: KimiLog.files, parse: KimiLog.parse),
AgentSource(agent: .goose, files: GooseLog.files, parse: GooseLog.parse),
AgentSource(agent: .pi, files: PiLog.files, parse: PiLog.parse),
]

/// Quando o arquivo mudou; num banco SQLite, o `-wal` do lado muda antes do arquivo principal
Expand Down Expand Up @@ -479,3 +480,68 @@ enum GooseLog {
}
}
}

// MARK: Pi (e agentes feitos a partir dele)

/// O Pi grava cada sessão em `~/.pi/agent/sessions/--<pasta>--/<data>_<id>.jsonl`: a primeira linha (`session`) tem o
/// `cwd`; as linhas `message` do assistente têm `usage` {input, output, cacheRead, cacheWrite, cost.total} e `timestamp`
/// em ms. Agentes feitos a partir do Pi trocam o nome da pasta (`~/.<nome>/agent`) e alguns guardam as mesmas linhas num
/// `sessions.db` (tabela `session_entries`): o Menew procura os dois formatos nas pastas ocultas da sua pasta pessoal.
enum PiLog {
/// As pastas `agent` com sessões no formato do Pi
static func roots() -> [URL] {
let fm = FileManager.default
let hidden = ((try? fm.contentsOfDirectory(atPath: AgentFiles.home.path)) ?? []).filter { $0.hasPrefix(".") && $0.count > 1 }
return hidden.map { AgentFiles.home.appendingPathComponent($0).appendingPathComponent("agent") }.filter {
fm.fileExists(atPath: $0.appendingPathComponent("sessions.db").path)
|| fm.fileExists(atPath: $0.appendingPathComponent("sessions").path)
}
}

static func files(_ cutoff: Date) -> [URL] {
roots().flatMap { root -> [URL] in
let db = root.appendingPathComponent("sessions.db")
let dbs = FileManager.default.fileExists(atPath: db.path) && AgentSource.mtime(db) >= cutoff ? [db] : []
return dbs + AgentFiles.find(root.appendingPathComponent("sessions"), ext: ["jsonl"], cutoff: cutoff, depth: 2)
}
}

static func parse(_ url: URL) -> [UsageRecord] {
if url.pathExtension == "db" {
guard SQLiteReader.columns(url, "session_entries").contains("payload_json") else { return [] }
let join = SQLiteReader.columns(url, "sessions").contains("cwd") ? " LEFT JOIN sessions s ON s.id = e.session_id" : ""
let sql = "SELECT e.session_id, e.payload_json, \(join.isEmpty ? "NULL" : "s.cwd") FROM session_entries e\(join) WHERE e.kind = 'message' AND e.payload_json LIKE '%\"usage\"%'"
return SQLiteReader.query(url, sql).compactMap { row in
guard row.count == 3 else { return nil }
return record(AgentFiles.dict(row[1]), session: row[0] as? String ?? "", cwd: row[2] as? String)
}
}
var cwd: String?, session = url.deletingPathExtension().lastPathComponent
var out: [UsageRecord] = []
for line in AgentFiles.lines(url) {
if line["type"] as? String == "session" {
cwd = line["cwd"] as? String
if let id = line["id"] as? String { session = id }
} else if let r = record(line, session: session, cwd: cwd) {
out.append(r)
}
}
return out
}

static func record(_ e: [String: Any], session: String, cwd: String?) -> UsageRecord? {
guard e["type"] as? String ?? "message" == "message", let m = e["message"] as? [String: Any], m["role"] as? String == "assistant",
let u = m["usage"] as? [String: Any], let date = AgentFiles.date(m["timestamp"]) ?? AgentFiles.date(e["timestamp"]) else { return nil }
let model = m["responseModel"] as? String ?? m["model"] as? String ?? "pi"
var r = UsageRecord(date: date, agent: .pi, model: model, project: AgentDates.project(cwd))
r.input = AgentFiles.int(u["input"])
r.output = AgentFiles.int(u["output"])
r.cacheRead = AgentFiles.int(u["cacheRead"])
let write1h = AgentFiles.int(u["cacheWrite1h"])
r.cacheWrite5m = max(0, AgentFiles.int(u["cacheWrite"]) - write1h)
r.cacheWrite1h = write1h
if let c = AgentFiles.double((u["cost"] as? [String: Any])?["total"]), c > 0 { r.reportedCost = c }
r.key = (e["id"] as? String).map { "pi:\(session):\($0)" }
return r.total > 0 ? r : nil
}
}
11 changes: 8 additions & 3 deletions Sources/Menew/AgentUsage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import SwiftUI
/// (a % do plano do Claude, que só a Anthropic sabe e vem desligada).
enum AgentKind: String, CaseIterable, Identifiable, Codable {
case claude, codex, gemini
case opencode, cursor, copilot, kiro, droid, qwen, amp, openclaw, kimi, goose
case opencode, cursor, copilot, kiro, droid, qwen, amp, openclaw, kimi, goose, pi
var id: String { rawValue }

var name: String {
Expand All @@ -26,6 +26,7 @@ enum AgentKind: String, CaseIterable, Identifiable, Codable {
case .openclaw: "OpenClaw"
case .kimi: "Kimi"
case .goose: "Goose"
case .pi: "Pi"
}
}

Expand Down Expand Up @@ -56,6 +57,7 @@ enum AgentKind: String, CaseIterable, Identifiable, Codable {
case .openclaw: "pawprint.fill"
case .kimi: "moon.stars.fill"
case .goose: "bird.fill"
case .pi: "p.circle.fill"
}
}

Expand All @@ -74,6 +76,7 @@ enum AgentKind: String, CaseIterable, Identifiable, Codable {
case .openclaw: Color(red: 0.98, green: 0.45, blue: 0.55)
case .kimi: Color(red: 0.20, green: 0.70, blue: 0.92)
case .goose: Color(red: 0.30, green: 0.72, blue: 0.40)
case .pi: Color(red: 0.85, green: 0.35, blue: 0.65)
}
}

Expand All @@ -97,17 +100,19 @@ enum AgentKind: String, CaseIterable, Identifiable, Codable {
case .openclaw: [".openclaw"]
case .kimi: [".kimi", ".kimi-code"]
case .goose: [".local/share/goose", "Library/Application Support/goose", ".config/goose"]
case .pi: [".pi"]
}
return paths.map { h.appendingPathComponent($0, isDirectory: true) }
}

var isInstalled: Bool { homes.contains { FileManager.default.fileExists(atPath: $0.path) } }
/// O Pi também vale quando só um agente feito a partir dele está no Mac (com outra pasta)
var isInstalled: Bool { homes.contains { FileManager.default.fileExists(atPath: $0.path) } || (self == .pi && !PiLog.roots().isEmpty) }

/// As que avisam o Menew a cada evento por um comando (as outras só por plugin, que o Menew não instala:
/// delas o Menew vê o processo rodando e o uso nos registros)
var hasHooks: Bool {
switch self {
case .opencode, .amp, .openclaw, .kimi, .goose: false
case .opencode, .amp, .openclaw, .kimi, .goose, .pi: false
default: true
}
}
Expand Down
53 changes: 53 additions & 0 deletions Tests/MenewTests/AgentSourcesTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -271,3 +271,56 @@ final class ResumeOnceTests: XCTestCase {
XCTAssertEqual(text, "")
}
}

final class PiLogTests: XCTestCase {
func testPiSessionFile() throws {
let dir = FileManager.default.temporaryDirectory.appendingPathComponent("menew-pi-\(UUID().uuidString)")
try FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true)
defer { try? FileManager.default.removeItem(at: dir) }
let url = dir.appendingPathComponent("2026-11-11T12-00-00-000Z_s1.jsonl")
try """
{"type":"session","version":3,"id":"s1","timestamp":"2026-11-11T12:00:00.000Z","cwd":"/Users/ana/apps/menew"}
{"type":"message","id":"a1","parentId":null,"timestamp":"2026-11-11T12:00:01.000Z","message":{"role":"user","content":"oi","timestamp":1794400801000}}
{"type":"message","id":"a2","parentId":"a1","timestamp":"2026-11-11T12:00:05.000Z","message":{"role":"assistant","model":"claude-sonnet-5-5","provider":"anthropic","timestamp":1794400802000,"usage":{"input":100,"output":40,"cacheRead":900,"cacheWrite":300,"cacheWrite1h":100,"totalTokens":1340,"cost":{"input":0,"output":0,"cacheRead":0,"cacheWrite":0,"total":0.0042}}}}
""".write(to: url, atomically: true, encoding: .utf8)
let r = PiLog.parse(url)
XCTAssertEqual(r.count, 1)
XCTAssertEqual(r[0].agent, .pi)
XCTAssertEqual(r[0].project, "menew")
XCTAssertEqual(r[0].total, 1340)
XCTAssertEqual(r[0].cacheWrite5m, 200)
XCTAssertEqual(r[0].cacheWrite1h, 100)
XCTAssertEqual(r[0].reportedCost ?? 0, 0.0042, accuracy: 0.00001)
XCTAssertEqual(r[0].key, "pi:s1:a2")
}

func testPiEntriesInSQLite() throws {
// Agentes feitos a partir do Pi que guardam as mesmas linhas num banco
let dir = FileManager.default.temporaryDirectory.appendingPathComponent("menew-pidb-\(UUID().uuidString)")
try FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true)
defer { try? FileManager.default.removeItem(at: dir) }
let db = dir.appendingPathComponent("sessions.db")
let payload = #"{"type":"message","id":"m2","parentId":"m1","timestamp":"2026-11-11T12:52:18.254Z","message":{"role":"assistant","model":"x-01","provider":"x","timestamp":1794401875496,"usage":{"input":15790,"output":347,"cacheRead":0,"cacheWrite":0,"totalTokens":16137,"cost":{"total":0.002542}}}}"#
let sql = """
CREATE TABLE sessions (id TEXT, header_json TEXT, cwd TEXT, created TEXT, modified_ms INTEGER, leaf_id TEXT, revision INTEGER);
CREATE TABLE session_entries (session_id TEXT, id TEXT, sequence INTEGER, parent_id TEXT, kind TEXT, timestamp TEXT, payload_json TEXT);
INSERT INTO sessions VALUES ('s1', '{}', '/Users/ana/apps/api', '2026-11-11', 0, 'm2', 1);
INSERT INTO session_entries VALUES ('s1', 'm2', 2, 'm1', 'message', '2026-11-11T12:52:18.254Z', '\(payload.replacingOccurrences(of: "'", with: "''"))');
INSERT INTO session_entries VALUES ('s1', 'm0', 0, NULL, 'model_change', '2026-11-11T12:50:00.000Z', '{}');
"""
let p = Process()
p.executableURL = URL(fileURLWithPath: "/usr/bin/sqlite3")
p.arguments = [db.path, sql]
try p.run(); p.waitUntilExit()
let r = PiLog.parse(db)
XCTAssertEqual(r.count, 1)
XCTAssertEqual(r[0].project, "api")
XCTAssertEqual(r[0].total, 16137)
XCTAssertEqual(r[0].model, "x-01")
}

func testPiProcess() {
let found = AgentProcesses.parse(" 20 pi\n 21 node /opt/homebrew/lib/node_modules/@earendil-works/pi-coding-agent/dist/bundle/cli.js\n 22 node /usr/local/bin/pip")
XCTAssertEqual(found.filter { $0.0 == .pi }.map(\.1), [20, 21])
}
}
4 changes: 2 additions & 2 deletions docs/de/index.html

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions docs/es/index.html

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions docs/index.html

Large diffs are not rendered by default.

Loading
Loading