npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

pi-idle-time

v0.4.1

Published

Pi extension that injects per-message timing context (idle time, turn duration, local time) and integrates with the statusline.

Downloads

253

Readme

pi-idle-time

Pi extension that makes the AI aware of wall-clock time, idle duration, and previous turn execution time. Includes an opt-in idle heartbeat that sends a keepalive message to refresh the Anthropic prompt cache.

Features

  • Per-prompt timing context — every user message gets a hidden [timing] block with the current time, idle duration since the last response, and the previous turn's execution duration.
  • Statusline — shows the elapsed time since the last response in the pi statusline (turn duration while the agent is active, idle timer when stopped). Displays --- when the model changes mid-session.
  • Idle heartbeat — opt-in tool that sends a keepalive user message after a configurable idle period. Triggers a real LLM turn, which refreshes the Anthropic prompt cache (default 5 min, extended 1 hour).
  • Compact TUI rendering — the heartbeat tool result and the keepalive message both render as one-liners in the transcript (♥ cache keepalive · 14:32:15 · 4.5m). Press Ctrl+E to expand.
  • Steer-aware — steering an active agent does not reset idle state.
  • Persistent toggle — the heartbeat enabled state survives /reload via a global state file.
  • Idle goal reminders/idle-goal <description> sets a goal the model is reminded of after the heartbeat interval. Goal reminders take precedence over the keepalive while a goal is active.

Installation

pi install /path/to/pi-idle-time
# or from npm (when published):
# pi install npm:pi-idle-time

What the model sees

On the first prompt, the extension injects a hidden timing block:

[timing]
local_time=2026-04-17T16:04:19+10:00
[/timing]

On subsequent prompts, the block includes idle and execution time:

[timing]
2026-04-17T16:05:19+10:00
idle_for=57.0s
last_turn_dur=88.2s
[/timing]

This is a display: false custom message — it is sent to the LLM as a user-role message but does not appear in the TUI transcript.

When the user has been idle for more than idleMessageThresholdSeconds (default 10s), a visible system message appears in the TUI:

[after 5m 2s]

Commands

| Command | Description | |---------|-------------| | /idle-time-reset | Reset state for the current session | | /idle-time-reset --all --yes | Wipe all sessions and logs | | /idle-time-status | Show plugin status (data dir, state, config) | | /idle-time-config | Show current configuration | | /idle-time-heartbeat on | Enable the idle heartbeat (persists across /reload) | | /idle-time-heartbeat off | Disable the idle heartbeat | | /idle-time-heartbeat / toggle | Flip the current state | | /idle-time-heartbeat status | Show whether the heartbeat is on or off | | /idle-time-heartbeat on 10 | Enable with a 10-minute override | | /idle-goal <description> | Set an idle goal reminder | | /idle-goal / /idle-goal --status | Show the active goal | | /idle-goal --complete | Mark the active goal complete |

When toggled, the command:

  • Updates in-memory heartbeatEnabled
  • Persists to global state (survives /reload)
  • Shows a UI-only notification via ctx.ui.notify (NOT sent to LLM)

The notification is a plain-text toast:

on: ♥ idle heartbeat on · 4.5m off: ♥ idle heartbeat off

The notification is plain text rather than the custom compact one-liner the keepalive uses, because the runtime's display: true path also adds the message to LLM context. There is no built-in way to show a custom message in chat without it being sent to the model. Toggling is a pure UI state change, so we use ctx.ui.notify to keep it out of the LLM's context.

Tool: idle_time_heartbeat_control

LLM-callable tool that controls the idle heartbeat and idle goal for the current session.

idle_time_heartbeat_control(enabled: true, minutes: 4.5)
idle_time_heartbeat_control(enabled: false)
idle_time_heartbeat_control(goal: "draft release notes for v0.4.1")
idle_time_heartbeat_control(completeGoal: true)
  • enabled (boolean, optional) — whether the heartbeat should be active. Omit when only changing the goal.
  • minutes (number, optional) — override the interval. Must be positive. Falls back to config.idleHeartbeatMinutes, then 4.5. Remembered per session per mode (heartbeat vs. goal).
  • goal (string, optional) — set the idle goal description. Pass an empty string to clear without completing.
  • completeGoal (boolean, optional) — mark the active goal complete and resume the heartbeat if enabled. Ignored when goal is also set.

The enabled state persists across /reload via ~/.pi/idle-time/global.json. The active goal persists per session in ~/.pi/idle-time/sessions/<id>.json. Users can also toggle directly with the /idle-time-heartbeat and /idle-goal slash commands (see Commands above).

Idle goal reminders

/idle-goal <description> sets a per-session goal. After the configured interval of inactivity the extension sends the LLM:

[goal reminder] HH:MM:SS
<description>

<system-reminder>Use idle_time_heartbeat_control with completeGoal=true to mark the goal complete.</system-reminder>

The user sees a compact TUI render (🎯 idle goal · <preview> · <time> · <interval>). Goal reminders take precedence over the keepalive heartbeat while a goal is active, and fire regardless of the heartbeat's enabled flag.

Statusline

The extension publishes a statusline via ctx.ui.setStatus("idle-time", text):

  • Agent active: live turn duration counting up (12s, 2m15s, 1h12m, 1d4h)
  • Just stopped, idle < 1s: turn duration with idle indicator (40s|💤)
  • Idle ≥ 1s: turn duration with idle timer (40s|💤2m15s)
  • Model changed since last stop: ---
  • Format options: drops seconds after 15 min (configurable via dropSecondsAfterSeconds); format days+hours at 1 day (configurable via formatHoursAsDays)

Idle heartbeat (cache keepalive)

The heartbeat is opt-in and disabled by default. When enabled, it sends a short keepalive user message after a configurable idle period (default 4.5 minutes). This triggers a real LLM turn, which keeps the Anthropic prompt cache warm.

What the model sees

[cache keepalive] 14:32:15 — disable via idle_time_heartbeat_control tool.

The [cache keepalive] prefix tags the message; the trailing hint points at the tool to disable. The message is deliberately informational — the model is not instructed to reply or take action.

Compact TUI rendering

The keepalive is delivered via pi.sendMessage with customType: "idle-time-heartbeat" and a custom message renderer (see src/heartbeat-message-renderer.ts, modeled on the pi compact TUI recipe). The keepalive collapses to a single line in the transcript:

♥ cache keepalive · 14:32:15 · 4.5m

Press Ctrl+E to expand and see the full body.

Configuration

{time} is replaced with the current local HH:MM:SS. The message template is configurable per session.

Configuration

Create ~/.pi/idle-time/config.json to override defaults:

{
  "idleMessageThresholdSeconds": 10,
  "idleMessageDropSecondsAfterSeconds": 3600,
  "dropSecondsAfterSeconds": 900,
  "formatHoursAsDays": true,
  "idleHeartbeatMinutes": null,
  "idleHeartbeatMessage": "[cache keepalive] {time} — disable via idle_time_heartbeat_control tool."
}

| Key | Default | Description | |-----|---------|-------------| | idleMessageThresholdSeconds | 10 | Min idle gap (s) before the visible [after Xs] system message appears in the TUI | | idleMessageDropSecondsAfterSeconds | 3600 | Drop trailing seconds in the system message after this many seconds (1 hour) | | dropSecondsAfterSeconds | 900 | Statusline drops seconds after this many seconds (15 min) | | formatHoursAsDays | true | Format [after 1d 4h] instead of [after 28h 0m] | | idleHeartbeatMinutes | null | Default heartbeat interval in minutes; null disables it | | idleHeartbeatMessage | [cache keepalive] {time} — disable via idle_time_heartbeat_control tool. | Keepalive message template; {time} is replaced with current local HH:MM:SS |

Data directory

State is stored in ~/.pi/idle-time/:

~/.pi/idle-time/
  config.json                  # optional user overrides
  global.json                  # global state (survives /reload)
  sessions/
    <session-id>.json          # per-session timing state
    <session-id>.lastresponse  # flat timestamp for fast reads
  logs/
    <session-id>.log           # per-session NDJSON error log

global.json schema

{
  "heartbeatEnabled": false
}

heartbeatEnabled is written by the idle_time_heartbeat_control tool and read on every session_start. This is why the heartbeat toggle survives /reload — it is not tied to any session.

sessions/<id>.json schema

{
  "sessionId": "019ecfd3-30e5-79d5-889a-bb22a34f01d4",
  "lastUserPromptAt": "2026-06-17T08:09:00.000+10:00",
  "lastStopAt": "2026-06-17T08:09:43.000+10:00",
  "lastAssistantMessageAt": "2026-06-17T08:09:43.000+10:00",
  "lastTurnExecMs": 42137,
  "modelAtLastStop": "claude-opus-4-5",
  "modelAtLastStopAt": "2026-06-17T08:09:43.000+10:00"
}

Behavior notes

  • Steering an active agent is not a new turn. When the user types while the agent is processing, the input handler does NOT reset idle state or inject a new timing block. Steer events are ignored for idle-tracking purposes.
  • Statusline idle threshold is 1 second (not 10). The statusline indicator 💤 appears after just 1s of idle; the duration counter starts at the same point.
  • Heartbeat only fires when the agent is idle. The timer is stopped on agent_start and input. When the timer fires, the message is sent with deliverAs: "followUp" so it queues properly if the agent is busy.
  • Timing block uses display: false. It is sent to the LLM as a user-role message but does not appear in the TUI transcript. The agent_end event also fires a display: false idle-time message with the same content if needed.

Development

pnpm install
pnpm test    # 175 tests
pnpm check   # typecheck

Run tests with a timeout to be safe:

timeout 30 node --import tsx --test tests/*.test.ts

Module layout

src/
  index.ts                       — Pi extension entry point (lifecycle hooks, statusline, commands, heartbeat)
  heartbeat.ts                   — Idle heartbeat timer for cache keepalive
  heartbeat-tool-renderer.ts     — Compact renderer for the heartbeat control tool
  heartbeat-message-renderer.ts  — Compact renderer for [cache keepalive] deliverable
  goal.ts                        — Idle goal reminder message formatting
  goal-message-renderer.ts       — Compact renderer for [goal reminder] deliverable
  global-state.ts                — Global state file (heartbeatEnabled, survives /reload)
  time.ts                        — ISO timestamp utilities
  duration.ts                    — Elapsed time formatting for statusline
  format.ts                      — Timing block and idle system message formatting
  sanitize.ts                    — Session ID sanitization
  config.ts                      — Config loading with validation and defaults
  log.ts                         — Per-session NDJSON error logger
  last-response.ts               — Flat .lastresponse file for fast statusline reads
  state.ts                       — Per-session state persistence with atomic writes
  statusline.ts                  — Statusline text formatting