@catdaemon/pi-cmux 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,5 +1,46 @@
1
- # @catdaemon/pi-cmux
1
+ # pi-cmux
2
2
 
3
3
  cmux status and notification integration for Pi.
4
4
 
5
- The extension updates cmux status when the main Pi agent starts/finishes work and sends a cmux notification when a turn completes. It also exports helper functions for other Pi extensions that need cmux notifications.
5
+ ## Why
6
+
7
+ When Pi runs inside cmux, it is useful for the surrounding workspace to know whether the agent is busy, idle, or waiting for attention. Without an integration, the user has to keep checking the Pi surface manually to see whether a turn has finished or whether an approval prompt needs feedback.
8
+
9
+ `pi-cmux` bridges Pi and cmux with lightweight status updates and notifications. It keeps the cmux surface informed while avoiding noise from subagent sessions by default.
10
+
11
+ This is especially useful for:
12
+
13
+ - Seeing at a glance whether Pi is currently working
14
+ - Getting notified when an agent turn finishes
15
+ - Surfacing approval prompts from guard extensions
16
+ - Sharing cmux notification helpers with other Pi extensions
17
+
18
+ ## What it does
19
+
20
+ This Pi package adds cmux integration to Pi:
21
+
22
+ - **Agent status updates**: sets a cmux status while the main agent is running and returns it to idle when the turn ends.
23
+ - **Done notifications**: sends a cmux notification when a main agent turn completes.
24
+ - **Feedback notifications**: exports helpers that other extensions can use to notify cmux when user feedback is needed.
25
+ - **Subagent filtering**: suppresses subagent turn noise by default.
26
+ - **Runtime status command**: `/cmux-status` shows whether cmux environment variables are detected and how the integration is configured.
27
+ - **Environment configuration**: supports `PI_CMUX_STATUS_KEY`, `PI_CMUX_NOTIFY_DONE`, `PI_CMUX_STATUS_PREVIEW`, and `PI_CMUX_INCLUDE_SUBAGENTS`.
28
+
29
+ ## Install from npm
30
+
31
+ ```bash
32
+ pi install npm:@catdaemon/pi-cmux
33
+ ```
34
+
35
+ Then reload Pi:
36
+
37
+ ```text
38
+ /reload
39
+ /cmux-status
40
+ ```
41
+
42
+ ## Runtime dependencies
43
+
44
+ This package expects the `cmux` CLI to be available when Pi is running inside cmux. Outside cmux, it safely no-ops.
45
+
46
+ Other Pi extensions can import its notification helpers when they need to signal cmux feedback or completion events.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@catdaemon/pi-cmux",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "cmux status and notification integration for Pi.",
5
5
  "type": "module",
6
6
  "keywords": ["pi-package", "pi", "cmux", "notifications"],
@@ -13,13 +13,59 @@ import {
13
13
  const DEFAULT_STATUS_KEY = 'pi-agent'
14
14
  const SUBAGENT_SESSION_DIR = join(getAgentDir(), 'subagents', 'sessions')
15
15
 
16
- type CmuxIntegrationConfig = {
16
+ export type CmuxIntegrationConfig = {
17
17
  statusKey: string
18
18
  notifyDone: boolean
19
19
  includePromptPreviewInStatus: boolean
20
20
  includeSubagents: boolean
21
21
  }
22
22
 
23
+ type ActiveTurn = {
24
+ prompt: string
25
+ startedAt: number
26
+ }
27
+
28
+ type FinishedTurn = ActiveTurn & {
29
+ remainingActiveTurns: number
30
+ }
31
+
32
+ export type CmuxTurnTracker = {
33
+ start(input: { key: string; prompt: string; ignored: boolean; now: number }): void
34
+ finish(key: string): FinishedTurn | undefined
35
+ activeCount(): number
36
+ }
37
+
38
+ function createTurnTracker(): CmuxTurnTracker {
39
+ const activeTurns = new Map<string, ActiveTurn>()
40
+ const ignoredTurns = new Set<string>()
41
+
42
+ return {
43
+ start(input) {
44
+ activeTurns.delete(input.key)
45
+ ignoredTurns.delete(input.key)
46
+ if (input.ignored) {
47
+ ignoredTurns.add(input.key)
48
+ return
49
+ }
50
+ activeTurns.set(input.key, { prompt: input.prompt, startedAt: input.now })
51
+ },
52
+ finish(key) {
53
+ if (ignoredTurns.delete(key)) return undefined
54
+ const turn = activeTurns.get(key)
55
+ activeTurns.delete(key)
56
+ if (!turn) return undefined
57
+ return { ...turn, remainingActiveTurns: activeTurns.size }
58
+ },
59
+ activeCount() {
60
+ return activeTurns.size
61
+ },
62
+ }
63
+ }
64
+
65
+ export function createCmuxTurnTrackerForTest(): CmuxTurnTracker {
66
+ return createTurnTracker()
67
+ }
68
+
23
69
  function readConfig(env: NodeJS.ProcessEnv = process.env): CmuxIntegrationConfig {
24
70
  return {
25
71
  statusKey: env.PI_CMUX_STATUS_KEY || DEFAULT_STATUS_KEY,
@@ -53,21 +99,20 @@ function sessionKey(ctx: ExtensionContext) {
53
99
  }
54
100
 
55
101
  export default function cmuxIntegration(pi: ExtensionAPI) {
56
- const activePrompts = new Map<string, string>()
57
- const activeStarts = new Map<string, number>()
102
+ const turnTracker = createTurnTracker()
58
103
  const config = readConfig()
59
104
 
60
105
  pi.on('before_agent_start', async (event, ctx) => {
61
106
  const key = sessionKey(ctx)
62
- if (!isCmuxEnvironment() || shouldIgnoreTurn(ctx, event.prompt, config)) {
63
- activePrompts.delete(key)
64
- activeStarts.delete(key)
107
+ if (!isCmuxEnvironment()) {
108
+ turnTracker.finish(key)
65
109
  return undefined
66
110
  }
67
111
 
112
+ const ignored = shouldIgnoreTurn(ctx, event.prompt, config)
68
113
  const preview = truncatePreview(event.prompt)
69
- activePrompts.set(key, preview)
70
- activeStarts.set(key, Date.now())
114
+ turnTracker.start({ key, prompt: preview, ignored, now: Date.now() })
115
+ if (ignored) return undefined
71
116
  const status = config.includePromptPreviewInStatus ? `Running: ${truncatePreview(event.prompt, 48)}` : 'Running'
72
117
  void setCmuxStatus(pi, config.statusKey, status, { icon: 'sparkles', color: '#3b82f6', signal: ctx.signal })
73
118
  return undefined
@@ -75,23 +120,16 @@ export default function cmuxIntegration(pi: ExtensionAPI) {
75
120
 
76
121
  pi.on('agent_end', async (_event, ctx) => {
77
122
  const key = sessionKey(ctx)
78
- if (!isCmuxEnvironment() || (!config.includeSubagents && isSubagentSession(ctx))) {
79
- activePrompts.delete(key)
80
- activeStarts.delete(key)
81
- return
82
- }
123
+ const finished = turnTracker.finish(key)
124
+ if (!isCmuxEnvironment() || !finished) return
83
125
 
84
- const prompt = activePrompts.get(key)
85
- const startedAt = activeStarts.get(key)
86
- activePrompts.delete(key)
87
- activeStarts.delete(key)
88
-
89
- const elapsed = startedAt ? Math.max(1, Math.round((Date.now() - startedAt) / 1000)) : undefined
90
- const idleText = elapsed ? `Idle (${elapsed}s)` : 'Idle'
91
- void setCmuxStatus(pi, config.statusKey, idleText, { icon: 'check', color: '#34c759', signal: ctx.signal })
126
+ const elapsed = Math.max(1, Math.round((Date.now() - finished.startedAt) / 1000))
127
+ if (finished.remainingActiveTurns === 0) {
128
+ void setCmuxStatus(pi, config.statusKey, `Idle (${elapsed}s)`, { icon: 'check', color: '#34c759', signal: ctx.signal })
129
+ }
92
130
 
93
131
  if (config.notifyDone) {
94
- const body = prompt ? `Finished: ${prompt}` : 'Agent finished and is ready for your next message.'
132
+ const body = finished.prompt ? `Finished: ${finished.prompt}` : 'Agent finished and is ready for your next message.'
95
133
  void notifyCmuxDone(pi, elapsed ? `${body}\nElapsed: ${elapsed}s` : body, {
96
134
  title: 'Pi',
97
135
  subtitle: 'Done',
@@ -0,0 +1,27 @@
1
+ import assert from 'node:assert/strict'
2
+ import { describe, it } from 'node:test'
3
+ import { createCmuxTurnTrackerForTest } from '../cmuxIntegration.ts'
4
+
5
+ describe('cmux turn tracker', () => {
6
+ it('does not finish ignored subagent turns', () => {
7
+ const tracker = createCmuxTurnTrackerForTest()
8
+ tracker.start({ key: 'subagent', prompt: 'subagent work', ignored: true, now: 1000 })
9
+
10
+ assert.equal(tracker.activeCount(), 0)
11
+ assert.equal(tracker.finish('subagent'), undefined)
12
+ })
13
+
14
+ it('keeps workspace running until all tracked turns finish', () => {
15
+ const tracker = createCmuxTurnTrackerForTest()
16
+ tracker.start({ key: 'main', prompt: 'main work', ignored: false, now: 1000 })
17
+ tracker.start({ key: 'worker', prompt: 'worker work', ignored: false, now: 2000 })
18
+
19
+ const first = tracker.finish('worker')
20
+ assert.equal(first?.remainingActiveTurns, 1)
21
+ assert.equal(tracker.activeCount(), 1)
22
+
23
+ const second = tracker.finish('main')
24
+ assert.equal(second?.remainingActiveTurns, 0)
25
+ assert.equal(tracker.activeCount(), 0)
26
+ })
27
+ })