prjct-cli 0.10.14 → 0.11.1
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/CHANGELOG.md +19 -0
- package/bin/dev.js +217 -0
- package/bin/prjct +10 -0
- package/bin/serve.js +78 -0
- package/core/bus/index.js +322 -0
- package/core/command-registry.js +65 -0
- package/core/domain/snapshot-manager.js +375 -0
- package/core/plugin/hooks.js +313 -0
- package/core/plugin/index.js +52 -0
- package/core/plugin/loader.js +331 -0
- package/core/plugin/registry.js +325 -0
- package/core/plugins/webhook.js +143 -0
- package/core/session/index.js +449 -0
- package/core/session/metrics.js +293 -0
- package/package.json +28 -4
- package/packages/shared/dist/index.d.ts +615 -0
- package/packages/shared/dist/index.js +204 -0
- package/packages/shared/package.json +29 -0
- package/packages/shared/src/index.ts +9 -0
- package/packages/shared/src/schemas.ts +124 -0
- package/packages/shared/src/types.ts +187 -0
- package/packages/shared/src/utils.ts +148 -0
- package/packages/shared/tsconfig.json +18 -0
- package/packages/web/README.md +36 -0
- package/packages/web/app/api/claude/sessions/route.ts +44 -0
- package/packages/web/app/api/claude/status/route.ts +34 -0
- package/packages/web/app/api/projects/[id]/delete/route.ts +21 -0
- package/packages/web/app/api/projects/[id]/icon/route.ts +33 -0
- package/packages/web/app/api/projects/[id]/route.ts +29 -0
- package/packages/web/app/api/projects/[id]/stats/route.ts +36 -0
- package/packages/web/app/api/projects/[id]/status/route.ts +21 -0
- package/packages/web/app/api/projects/route.ts +16 -0
- package/packages/web/app/api/sessions/history/route.ts +122 -0
- package/packages/web/app/api/stats/route.ts +38 -0
- package/packages/web/app/error.tsx +34 -0
- package/packages/web/app/favicon.ico +0 -0
- package/packages/web/app/globals.css +155 -0
- package/packages/web/app/layout.tsx +43 -0
- package/packages/web/app/loading.tsx +7 -0
- package/packages/web/app/not-found.tsx +25 -0
- package/packages/web/app/page.tsx +227 -0
- package/packages/web/app/project/[id]/error.tsx +41 -0
- package/packages/web/app/project/[id]/loading.tsx +9 -0
- package/packages/web/app/project/[id]/not-found.tsx +27 -0
- package/packages/web/app/project/[id]/page.tsx +253 -0
- package/packages/web/app/project/[id]/stats/page.tsx +447 -0
- package/packages/web/app/sessions/page.tsx +165 -0
- package/packages/web/app/settings/page.tsx +150 -0
- package/packages/web/components/AppSidebar.tsx +113 -0
- package/packages/web/components/CommandButton.tsx +39 -0
- package/packages/web/components/ConnectionStatus.tsx +29 -0
- package/packages/web/components/Logo.tsx +65 -0
- package/packages/web/components/MarkdownContent.tsx +123 -0
- package/packages/web/components/ProjectAvatar.tsx +54 -0
- package/packages/web/components/TechStackBadges.tsx +20 -0
- package/packages/web/components/TerminalTab.tsx +84 -0
- package/packages/web/components/TerminalTabs.tsx +210 -0
- package/packages/web/components/charts/SessionsChart.tsx +172 -0
- package/packages/web/components/providers.tsx +45 -0
- package/packages/web/components/ui/alert-dialog.tsx +157 -0
- package/packages/web/components/ui/badge.tsx +46 -0
- package/packages/web/components/ui/button.tsx +60 -0
- package/packages/web/components/ui/card.tsx +92 -0
- package/packages/web/components/ui/chart.tsx +385 -0
- package/packages/web/components/ui/dropdown-menu.tsx +257 -0
- package/packages/web/components/ui/scroll-area.tsx +58 -0
- package/packages/web/components/ui/sheet.tsx +139 -0
- package/packages/web/components/ui/tabs.tsx +66 -0
- package/packages/web/components/ui/tooltip.tsx +61 -0
- package/packages/web/components.json +22 -0
- package/packages/web/context/TerminalContext.tsx +45 -0
- package/packages/web/context/TerminalTabsContext.tsx +136 -0
- package/packages/web/eslint.config.mjs +18 -0
- package/packages/web/hooks/useClaudeTerminal.ts +375 -0
- package/packages/web/hooks/useProjectStats.ts +38 -0
- package/packages/web/hooks/useProjects.ts +73 -0
- package/packages/web/hooks/useStats.ts +28 -0
- package/packages/web/lib/format.ts +23 -0
- package/packages/web/lib/parse-prjct-files.ts +1122 -0
- package/packages/web/lib/projects.ts +452 -0
- package/packages/web/lib/pty.ts +101 -0
- package/packages/web/lib/query-config.ts +44 -0
- package/packages/web/lib/utils.ts +6 -0
- package/packages/web/next-env.d.ts +6 -0
- package/packages/web/next.config.ts +7 -0
- package/packages/web/package.json +53 -0
- package/packages/web/postcss.config.mjs +7 -0
- package/packages/web/public/file.svg +1 -0
- package/packages/web/public/globe.svg +1 -0
- package/packages/web/public/next.svg +1 -0
- package/packages/web/public/vercel.svg +1 -0
- package/packages/web/public/window.svg +1 -0
- package/packages/web/server.ts +262 -0
- package/packages/web/tsconfig.json +34 -0
- package/templates/commands/done.md +176 -54
- package/templates/commands/history.md +176 -0
- package/templates/commands/init.md +28 -1
- package/templates/commands/now.md +191 -9
- package/templates/commands/pause.md +176 -12
- package/templates/commands/redo.md +142 -0
- package/templates/commands/resume.md +166 -62
- package/templates/commands/serve.md +121 -0
- package/templates/commands/ship.md +45 -1
- package/templates/commands/sync.md +34 -1
- package/templates/commands/undo.md +152 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1155 1000"><path d="m577.3 0 577.4 1000H0z" fill="#fff"/></svg>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill-rule="evenodd" clip-rule="evenodd" d="M1.5 2.5h13v10a1 1 0 0 1-1 1h-11a1 1 0 0 1-1-1zM0 1h16v11.5a2.5 2.5 0 0 1-2.5 2.5h-11A2.5 2.5 0 0 1 0 12.5zm3.75 4.5a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5M7 4.75a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0m1.75.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5" fill="#666"/></svg>
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Custom Next.js server with WebSocket support for PTY
|
|
3
|
+
*
|
|
4
|
+
* PTY sessions are managed here (not in API routes) to share memory context
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { createServer } from 'http'
|
|
8
|
+
import next from 'next'
|
|
9
|
+
import { WebSocketServer, WebSocket } from 'ws'
|
|
10
|
+
import * as pty from 'node-pty'
|
|
11
|
+
import type { IPty } from 'node-pty'
|
|
12
|
+
|
|
13
|
+
const dev = process.env.NODE_ENV !== 'production'
|
|
14
|
+
const hostname = 'localhost'
|
|
15
|
+
const port = parseInt(process.env.PORT || '9472', 10)
|
|
16
|
+
|
|
17
|
+
// PTY Sessions stored in server memory
|
|
18
|
+
interface Session {
|
|
19
|
+
pty: IPty
|
|
20
|
+
projectDir: string
|
|
21
|
+
createdAt: Date
|
|
22
|
+
hasStartedClaude: boolean // Track if claude command was sent
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const sessions = new Map<string, Session>()
|
|
26
|
+
|
|
27
|
+
function createSession(sessionId: string, projectDir: string): { pty: IPty; isNew: boolean } {
|
|
28
|
+
const existing = sessions.get(sessionId)
|
|
29
|
+
|
|
30
|
+
// If session exists for this project, reuse it (allows multiple tabs)
|
|
31
|
+
if (existing) {
|
|
32
|
+
console.log(`[PTY] Reusing existing session: ${sessionId}`)
|
|
33
|
+
return { pty: existing.pty, isNew: false }
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const shell = process.platform === 'win32' ? 'cmd.exe' : 'bash'
|
|
37
|
+
const args = process.platform === 'win32' ? [] : ['-l']
|
|
38
|
+
|
|
39
|
+
const ptyProcess = pty.spawn(shell, args, {
|
|
40
|
+
name: 'xterm-256color',
|
|
41
|
+
cols: 120,
|
|
42
|
+
rows: 30,
|
|
43
|
+
cwd: projectDir,
|
|
44
|
+
env: {
|
|
45
|
+
...process.env,
|
|
46
|
+
TERM: 'xterm-256color',
|
|
47
|
+
COLORTERM: 'truecolor'
|
|
48
|
+
}
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
sessions.set(sessionId, {
|
|
52
|
+
pty: ptyProcess,
|
|
53
|
+
projectDir,
|
|
54
|
+
createdAt: new Date(),
|
|
55
|
+
hasStartedClaude: false
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
// NOTE: Don't auto-start claude here - let the WebSocket handler do it
|
|
59
|
+
// once the client is connected and ready to receive output
|
|
60
|
+
|
|
61
|
+
return { pty: ptyProcess, isNew: true }
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function getSession(sessionId: string): IPty | null {
|
|
65
|
+
return sessions.get(sessionId)?.pty || null
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function killSession(sessionId: string): void {
|
|
69
|
+
const session = sessions.get(sessionId)
|
|
70
|
+
if (session) {
|
|
71
|
+
try { session.pty.kill() } catch {}
|
|
72
|
+
sessions.delete(sessionId)
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function resizeSession(sessionId: string, cols: number, rows: number): void {
|
|
77
|
+
const session = sessions.get(sessionId)
|
|
78
|
+
if (session) {
|
|
79
|
+
try { session.pty.resize(cols, rows) } catch {}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const app = next({ dev, hostname, port })
|
|
84
|
+
const handle = app.getRequestHandler()
|
|
85
|
+
|
|
86
|
+
app.prepare().then(() => {
|
|
87
|
+
const server = createServer(async (req, res) => {
|
|
88
|
+
const url = new URL(req.url || '', `http://${req.headers.host}`)
|
|
89
|
+
|
|
90
|
+
// Handle session creation directly in server (bypasses API route isolation)
|
|
91
|
+
if (url.pathname === '/api/claude/sessions' && req.method === 'POST') {
|
|
92
|
+
let body = ''
|
|
93
|
+
req.on('data', chunk => { body += chunk })
|
|
94
|
+
req.on('end', () => {
|
|
95
|
+
try {
|
|
96
|
+
const { sessionId, projectDir } = JSON.parse(body)
|
|
97
|
+
if (!sessionId || !projectDir) {
|
|
98
|
+
res.statusCode = 400
|
|
99
|
+
res.setHeader('Content-Type', 'application/json')
|
|
100
|
+
res.end(JSON.stringify({ success: false, error: 'sessionId and projectDir required' }))
|
|
101
|
+
return
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const { isNew } = createSession(sessionId, projectDir)
|
|
105
|
+
console.log(`[PTY] ${isNew ? 'Created' : 'Reusing'} session: ${sessionId} for ${projectDir}`)
|
|
106
|
+
|
|
107
|
+
res.statusCode = 200
|
|
108
|
+
res.setHeader('Content-Type', 'application/json')
|
|
109
|
+
res.end(JSON.stringify({ success: true, data: { sessionId, projectDir, isNew } }))
|
|
110
|
+
} catch (err) {
|
|
111
|
+
console.error('[PTY] Error creating session:', err)
|
|
112
|
+
res.statusCode = 500
|
|
113
|
+
res.setHeader('Content-Type', 'application/json')
|
|
114
|
+
res.end(JSON.stringify({ success: false, error: 'Failed to create session' }))
|
|
115
|
+
}
|
|
116
|
+
})
|
|
117
|
+
return
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Handle session list
|
|
121
|
+
if (url.pathname === '/api/claude/sessions' && req.method === 'GET') {
|
|
122
|
+
const list = Array.from(sessions.entries()).map(([id, s]) => ({
|
|
123
|
+
sessionId: id,
|
|
124
|
+
projectDir: s.projectDir,
|
|
125
|
+
createdAt: s.createdAt
|
|
126
|
+
}))
|
|
127
|
+
res.statusCode = 200
|
|
128
|
+
res.setHeader('Content-Type', 'application/json')
|
|
129
|
+
res.end(JSON.stringify({ success: true, data: list }))
|
|
130
|
+
return
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// All other requests go to Next.js
|
|
134
|
+
try {
|
|
135
|
+
await handle(req, res)
|
|
136
|
+
} catch (err) {
|
|
137
|
+
console.error('Error handling request:', err)
|
|
138
|
+
res.statusCode = 500
|
|
139
|
+
res.end('Internal Server Error')
|
|
140
|
+
}
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
// WebSocket server for PTY communication
|
|
144
|
+
const wss = new WebSocketServer({ noServer: true })
|
|
145
|
+
|
|
146
|
+
// Heartbeat interval to detect dead connections (30 seconds)
|
|
147
|
+
const HEARTBEAT_INTERVAL = 30000
|
|
148
|
+
|
|
149
|
+
const heartbeatInterval = setInterval(() => {
|
|
150
|
+
wss.clients.forEach((ws) => {
|
|
151
|
+
const extWs = ws as WebSocket & { isAlive?: boolean; sessionId?: string }
|
|
152
|
+
if (extWs.isAlive === false) {
|
|
153
|
+
console.log(`[WS] Terminating dead connection: ${extWs.sessionId}`)
|
|
154
|
+
if (extWs.sessionId) {
|
|
155
|
+
killSession(extWs.sessionId)
|
|
156
|
+
}
|
|
157
|
+
return ws.terminate()
|
|
158
|
+
}
|
|
159
|
+
extWs.isAlive = false
|
|
160
|
+
ws.ping()
|
|
161
|
+
})
|
|
162
|
+
}, HEARTBEAT_INTERVAL)
|
|
163
|
+
|
|
164
|
+
// Cleanup on server close
|
|
165
|
+
wss.on('close', () => {
|
|
166
|
+
clearInterval(heartbeatInterval)
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
server.on('upgrade', (request, socket, head) => {
|
|
170
|
+
const url = new URL(request.url || '', `http://${request.headers.host}`)
|
|
171
|
+
|
|
172
|
+
if (url.pathname.startsWith('/ws/claude/')) {
|
|
173
|
+
wss.handleUpgrade(request, socket, head, (ws) => {
|
|
174
|
+
wss.emit('connection', ws, request)
|
|
175
|
+
})
|
|
176
|
+
}
|
|
177
|
+
// Other upgrades (HMR) pass through to Next.js
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
wss.on('connection', (ws: WebSocket, request) => {
|
|
181
|
+
const url = new URL(request.url || '', `http://${request.headers.host}`)
|
|
182
|
+
const sessionId = url.pathname.replace('/ws/claude/', '')
|
|
183
|
+
|
|
184
|
+
console.log(`[WS] New PTY connection for session: ${sessionId}`)
|
|
185
|
+
|
|
186
|
+
// Mark connection as alive and store sessionId for heartbeat
|
|
187
|
+
const extWs = ws as WebSocket & { isAlive?: boolean; sessionId?: string }
|
|
188
|
+
extWs.isAlive = true
|
|
189
|
+
extWs.sessionId = sessionId
|
|
190
|
+
|
|
191
|
+
// Handle pong response
|
|
192
|
+
ws.on('pong', () => {
|
|
193
|
+
extWs.isAlive = true
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
const session = sessions.get(sessionId)
|
|
197
|
+
|
|
198
|
+
if (!session) {
|
|
199
|
+
console.log(`[WS] Session not found: ${sessionId}`)
|
|
200
|
+
ws.send(JSON.stringify({ type: 'error', message: 'Session not found' }))
|
|
201
|
+
ws.close()
|
|
202
|
+
return
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const ptyProcess = session.pty
|
|
206
|
+
|
|
207
|
+
// Register data handler FIRST before sending any commands
|
|
208
|
+
const dataHandler = ptyProcess.onData((data: string) => {
|
|
209
|
+
if (ws.readyState === WebSocket.OPEN) {
|
|
210
|
+
ws.send(JSON.stringify({ type: 'output', data }))
|
|
211
|
+
}
|
|
212
|
+
})
|
|
213
|
+
|
|
214
|
+
ws.send(JSON.stringify({ type: 'connected', sessionId }))
|
|
215
|
+
|
|
216
|
+
// Auto-start Claude CLI only once, when first client connects
|
|
217
|
+
if (!session.hasStartedClaude) {
|
|
218
|
+
session.hasStartedClaude = true
|
|
219
|
+
console.log(`[WS] Starting Claude CLI for session: ${sessionId}`)
|
|
220
|
+
setTimeout(() => {
|
|
221
|
+
ptyProcess.write('claude\r')
|
|
222
|
+
}, 200) // Small delay to ensure client is ready
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const exitHandler = ptyProcess.onExit(({ exitCode }) => {
|
|
226
|
+
if (ws.readyState === WebSocket.OPEN) {
|
|
227
|
+
ws.send(JSON.stringify({ type: 'exit', code: exitCode }))
|
|
228
|
+
}
|
|
229
|
+
killSession(sessionId)
|
|
230
|
+
})
|
|
231
|
+
|
|
232
|
+
ws.on('message', (message: Buffer) => {
|
|
233
|
+
try {
|
|
234
|
+
const { type, data, cols, rows } = JSON.parse(message.toString())
|
|
235
|
+
switch (type) {
|
|
236
|
+
case 'input':
|
|
237
|
+
ptyProcess?.write(data)
|
|
238
|
+
break
|
|
239
|
+
case 'resize':
|
|
240
|
+
if (cols && rows) resizeSession(sessionId, cols, rows)
|
|
241
|
+
break
|
|
242
|
+
}
|
|
243
|
+
} catch (err) {
|
|
244
|
+
console.error('[WS] Error:', err)
|
|
245
|
+
}
|
|
246
|
+
})
|
|
247
|
+
|
|
248
|
+
ws.on('close', () => {
|
|
249
|
+
console.log(`[WS] PTY connection closed: ${sessionId}`)
|
|
250
|
+
dataHandler.dispose()
|
|
251
|
+
exitHandler.dispose()
|
|
252
|
+
})
|
|
253
|
+
|
|
254
|
+
ws.on('error', (error) => {
|
|
255
|
+
console.error(`[WS] Error for ${sessionId}:`, error)
|
|
256
|
+
})
|
|
257
|
+
})
|
|
258
|
+
|
|
259
|
+
server.listen(port, () => {
|
|
260
|
+
console.log(`> prjct ready on http://${hostname}:${port}`)
|
|
261
|
+
})
|
|
262
|
+
})
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2017",
|
|
4
|
+
"lib": ["dom", "dom.iterable", "esnext"],
|
|
5
|
+
"allowJs": true,
|
|
6
|
+
"skipLibCheck": true,
|
|
7
|
+
"strict": true,
|
|
8
|
+
"noEmit": true,
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"module": "esnext",
|
|
11
|
+
"moduleResolution": "bundler",
|
|
12
|
+
"resolveJsonModule": true,
|
|
13
|
+
"isolatedModules": true,
|
|
14
|
+
"jsx": "react-jsx",
|
|
15
|
+
"incremental": true,
|
|
16
|
+
"plugins": [
|
|
17
|
+
{
|
|
18
|
+
"name": "next"
|
|
19
|
+
}
|
|
20
|
+
],
|
|
21
|
+
"paths": {
|
|
22
|
+
"@/*": ["./*"]
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
"include": [
|
|
26
|
+
"next-env.d.ts",
|
|
27
|
+
"**/*.ts",
|
|
28
|
+
"**/*.tsx",
|
|
29
|
+
".next/types/**/*.ts",
|
|
30
|
+
".next/dev/types/**/*.ts",
|
|
31
|
+
"**/*.mts"
|
|
32
|
+
],
|
|
33
|
+
"exclude": ["node_modules"]
|
|
34
|
+
}
|
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
---
|
|
2
|
-
allowed-tools: [Read, Write]
|
|
3
|
-
description: 'Complete current task'
|
|
2
|
+
allowed-tools: [Read, Write, Bash]
|
|
3
|
+
description: 'Complete current task with session metrics'
|
|
4
4
|
timestamp-rule: 'GetTimestamp() for all timestamps'
|
|
5
5
|
---
|
|
6
6
|
|
|
7
|
-
# /p:done - Complete Current Task
|
|
7
|
+
# /p:done - Complete Current Task with Session Metrics
|
|
8
8
|
|
|
9
9
|
## Context Variables
|
|
10
10
|
- `{projectId}`: From `.prjct/prjct.config.json`
|
|
11
11
|
- `{globalPath}`: `~/.prjct-cli/projects/{projectId}`
|
|
12
12
|
- `{nowPath}`: `{globalPath}/core/now.md`
|
|
13
|
+
- `{sessionPath}`: `{globalPath}/sessions/current.json`
|
|
14
|
+
- `{archiveDir}`: `{globalPath}/sessions/archive`
|
|
13
15
|
- `{memoryPath}`: `{globalPath}/memory/context.jsonl`
|
|
14
16
|
- `{metricsPath}`: `{globalPath}/progress/metrics.md`
|
|
15
17
|
|
|
@@ -22,116 +24,236 @@ IF file not found:
|
|
|
22
24
|
OUTPUT: "No prjct project. Run /p:init first."
|
|
23
25
|
STOP
|
|
24
26
|
|
|
25
|
-
## Step 2:
|
|
27
|
+
## Step 2: Check Session State
|
|
26
28
|
|
|
29
|
+
### Try structured session first
|
|
30
|
+
READ: `{sessionPath}`
|
|
31
|
+
|
|
32
|
+
IF file exists:
|
|
33
|
+
PARSE as JSON
|
|
34
|
+
EXTRACT: {session} object
|
|
35
|
+
GOTO Step 3 (Session Completion)
|
|
36
|
+
|
|
37
|
+
### Fallback to legacy now.md
|
|
27
38
|
READ: `{nowPath}`
|
|
28
39
|
|
|
29
40
|
IF empty OR contains "No current task":
|
|
30
41
|
OUTPUT: "⚠️ No active task to complete. Use /p:now to start one."
|
|
31
42
|
STOP
|
|
32
43
|
|
|
33
|
-
## Step 3:
|
|
44
|
+
## Step 3: Session Completion
|
|
34
45
|
|
|
35
|
-
|
|
46
|
+
### Calculate Final Duration
|
|
47
|
+
SET: {now} = GetTimestamp()
|
|
36
48
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
49
|
+
For each event in {session.timeline}:
|
|
50
|
+
Track start/resume/pause/complete times
|
|
51
|
+
Calculate total active time
|
|
40
52
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
- Example: `Started: 11/28/2025, 2:30:00 PM`
|
|
53
|
+
SET: {duration} = total active seconds
|
|
54
|
+
SET: {durationFormatted} = format as "Xh Ym" or "Xm"
|
|
44
55
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
- Format: "Xh Ym" (e.g., "2h 15m")
|
|
49
|
-
- If < 1 hour: "Xm"
|
|
50
|
-
- If < 1 minute: "< 1m"
|
|
56
|
+
### Calculate Git Metrics
|
|
57
|
+
BASH: `git rev-list --count --since="{session.startedAt}" HEAD 2>/dev/null || echo "0"`
|
|
58
|
+
CAPTURE as {commits}
|
|
51
59
|
|
|
52
|
-
|
|
60
|
+
BASH: `git diff --stat HEAD~{commits} 2>/dev/null || git diff --stat`
|
|
61
|
+
PARSE output for:
|
|
62
|
+
- {filesChanged}: number of files
|
|
63
|
+
- {linesAdded}: insertions
|
|
64
|
+
- {linesRemoved}: deletions
|
|
53
65
|
|
|
54
|
-
|
|
66
|
+
### Update Session Object
|
|
67
|
+
```json
|
|
68
|
+
{
|
|
69
|
+
"id": "{session.id}",
|
|
70
|
+
"projectId": "{projectId}",
|
|
71
|
+
"task": "{session.task}",
|
|
72
|
+
"status": "completed",
|
|
73
|
+
"startedAt": "{session.startedAt}",
|
|
74
|
+
"pausedAt": null,
|
|
75
|
+
"completedAt": "{now}",
|
|
76
|
+
"duration": {duration},
|
|
77
|
+
"metrics": {
|
|
78
|
+
"filesChanged": {filesChanged},
|
|
79
|
+
"linesAdded": {linesAdded},
|
|
80
|
+
"linesRemoved": {linesRemoved},
|
|
81
|
+
"commits": {commits},
|
|
82
|
+
"snapshots": {session.metrics.snapshots}
|
|
83
|
+
},
|
|
84
|
+
"timeline": [
|
|
85
|
+
...{session.timeline},
|
|
86
|
+
{"type": "complete", "at": "{now}"}
|
|
87
|
+
]
|
|
88
|
+
}
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## Step 4: Archive Session
|
|
92
|
+
|
|
93
|
+
### Create Archive Directory
|
|
94
|
+
GET: {yearMonth} = YYYY-MM from {now}
|
|
95
|
+
ENSURE: `{archiveDir}/{yearMonth}` exists
|
|
96
|
+
|
|
97
|
+
BASH: `mkdir -p {archiveDir}/{yearMonth}`
|
|
98
|
+
|
|
99
|
+
### Write Archived Session
|
|
100
|
+
WRITE: `{archiveDir}/{yearMonth}/{session.id}.json`
|
|
101
|
+
Content: Updated session object from Step 3
|
|
102
|
+
|
|
103
|
+
## Step 5: Clear Current Session
|
|
104
|
+
|
|
105
|
+
DELETE: `{sessionPath}`
|
|
55
106
|
|
|
56
|
-
|
|
107
|
+
OR WRITE empty state:
|
|
108
|
+
WRITE: `{sessionPath}`
|
|
109
|
+
Content:
|
|
110
|
+
```json
|
|
111
|
+
{}
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
## Step 6: Update Legacy now.md
|
|
115
|
+
|
|
116
|
+
WRITE: `{nowPath}`
|
|
117
|
+
Content:
|
|
57
118
|
```markdown
|
|
58
119
|
# NOW
|
|
59
120
|
|
|
60
121
|
No current task. Use `/p:now` to set focus.
|
|
61
122
|
```
|
|
62
123
|
|
|
63
|
-
## Step
|
|
124
|
+
## Step 7: Log to Memory
|
|
64
125
|
|
|
65
126
|
APPEND to: `{memoryPath}`
|
|
66
127
|
|
|
67
128
|
Single line (JSONL format):
|
|
68
129
|
```json
|
|
69
|
-
{"timestamp":"{
|
|
130
|
+
{"timestamp":"{now}","action":"session_completed","sessionId":"{session.id}","task":"{session.task}","duration":{duration},"metrics":{"files":{filesChanged},"added":{linesAdded},"removed":{linesRemoved},"commits":{commits}}}
|
|
70
131
|
```
|
|
71
132
|
|
|
72
|
-
## Step
|
|
133
|
+
## Step 8: Update Metrics Summary
|
|
134
|
+
|
|
135
|
+
READ: `{metricsPath}` (create if not exists)
|
|
73
136
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
137
|
+
### Append Daily Entry
|
|
138
|
+
GET: {date} = YYYY-MM-DD from {now}
|
|
139
|
+
|
|
140
|
+
INSERT or UPDATE entry for {date}:
|
|
141
|
+
```markdown
|
|
142
|
+
### {date}
|
|
143
|
+
- **{session.task}** ({durationFormatted})
|
|
144
|
+
- Files: {filesChanged} | +{linesAdded}/-{linesRemoved} | Commits: {commits}
|
|
145
|
+
```
|
|
77
146
|
|
|
78
147
|
## Output
|
|
79
148
|
|
|
80
149
|
SUCCESS:
|
|
81
150
|
```
|
|
82
|
-
✅ {task} ({
|
|
151
|
+
✅ {session.task} ({durationFormatted})
|
|
152
|
+
|
|
153
|
+
Session: {session.id}
|
|
154
|
+
Files: {filesChanged} | +{linesAdded}/-{linesRemoved}
|
|
155
|
+
Commits: {commits}
|
|
83
156
|
|
|
84
157
|
Next:
|
|
85
158
|
• /p:now - Start next task
|
|
86
159
|
• /p:ship - Ship completed work
|
|
87
|
-
• /p:
|
|
160
|
+
• /p:progress - View metrics
|
|
88
161
|
```
|
|
89
162
|
|
|
90
163
|
## Error Handling
|
|
91
164
|
|
|
92
|
-
| Error | Response |
|
|
93
|
-
|
|
94
|
-
| Config not found | "No prjct project
|
|
95
|
-
|
|
|
96
|
-
|
|
|
97
|
-
|
|
|
165
|
+
| Error | Response | Action |
|
|
166
|
+
|-------|----------|--------|
|
|
167
|
+
| Config not found | "No prjct project" | STOP |
|
|
168
|
+
| No session/task | "No active task" | STOP |
|
|
169
|
+
| Git fails | Use zeros for metrics | CONTINUE |
|
|
170
|
+
| Archive fails | Log warning | CONTINUE |
|
|
171
|
+
| Write fails | Log warning | CONTINUE |
|
|
98
172
|
|
|
99
173
|
## Examples
|
|
100
174
|
|
|
101
|
-
### Example 1:
|
|
102
|
-
**
|
|
175
|
+
### Example 1: Full Session Completion
|
|
176
|
+
**Session:**
|
|
177
|
+
```json
|
|
178
|
+
{
|
|
179
|
+
"id": "sess_abc12345",
|
|
180
|
+
"task": "implement authentication",
|
|
181
|
+
"status": "active",
|
|
182
|
+
"startedAt": "2025-12-07T10:00:00.000Z",
|
|
183
|
+
"timeline": [
|
|
184
|
+
{"type": "start", "at": "2025-12-07T10:00:00.000Z"}
|
|
185
|
+
]
|
|
186
|
+
}
|
|
103
187
|
```
|
|
104
|
-
# NOW
|
|
105
188
|
|
|
106
|
-
**
|
|
189
|
+
**Git activity:**
|
|
190
|
+
- 3 commits
|
|
191
|
+
- 5 files changed
|
|
192
|
+
- +120/-30 lines
|
|
107
193
|
|
|
108
|
-
|
|
194
|
+
**Output:**
|
|
109
195
|
```
|
|
196
|
+
✅ implement authentication (2h 15m)
|
|
110
197
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
198
|
+
Session: sess_abc12345
|
|
199
|
+
Files: 5 | +120/-30
|
|
200
|
+
Commits: 3
|
|
114
201
|
|
|
115
|
-
|
|
116
|
-
|
|
202
|
+
Next:
|
|
203
|
+
• /p:now - Start next task
|
|
204
|
+
• /p:ship - Ship completed work
|
|
205
|
+
• /p:progress - View metrics
|
|
117
206
|
```
|
|
118
|
-
# NOW
|
|
119
207
|
|
|
120
|
-
|
|
208
|
+
### Example 2: Session with Pauses
|
|
209
|
+
**Session with multiple pause/resume:**
|
|
210
|
+
```json
|
|
211
|
+
{
|
|
212
|
+
"id": "sess_xyz98765",
|
|
213
|
+
"task": "fix login bug",
|
|
214
|
+
"timeline": [
|
|
215
|
+
{"type": "start", "at": "2025-12-07T09:00:00.000Z"},
|
|
216
|
+
{"type": "pause", "at": "2025-12-07T10:00:00.000Z"},
|
|
217
|
+
{"type": "resume", "at": "2025-12-07T14:00:00.000Z"},
|
|
218
|
+
{"type": "pause", "at": "2025-12-07T15:30:00.000Z"},
|
|
219
|
+
{"type": "resume", "at": "2025-12-07T16:00:00.000Z"}
|
|
220
|
+
]
|
|
221
|
+
}
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
**Completion at 17:00:**
|
|
225
|
+
- Active time: 1h + 1.5h + 1h = 3.5h
|
|
226
|
+
- Duration: 3h 30m
|
|
227
|
+
|
|
228
|
+
**Output:**
|
|
121
229
|
```
|
|
230
|
+
✅ fix login bug (3h 30m)
|
|
122
231
|
|
|
123
|
-
|
|
232
|
+
Session: sess_xyz98765
|
|
233
|
+
Files: 2 | +45/-12
|
|
234
|
+
Commits: 1
|
|
124
235
|
|
|
125
|
-
|
|
236
|
+
Next:
|
|
237
|
+
• /p:now - Start next task
|
|
238
|
+
• /p:ship - Ship completed work
|
|
239
|
+
• /p:progress - View metrics
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
### Example 3: Legacy Fallback (No Session)
|
|
126
243
|
**now.md content:**
|
|
127
244
|
```
|
|
128
245
|
# NOW
|
|
129
246
|
|
|
130
|
-
**fix
|
|
247
|
+
**quick fix**
|
|
131
248
|
|
|
132
|
-
Started:
|
|
249
|
+
Started: 2025-12-07T16:45:00.000Z
|
|
133
250
|
```
|
|
134
251
|
|
|
135
|
-
**
|
|
136
|
-
|
|
137
|
-
|
|
252
|
+
**Output:**
|
|
253
|
+
```
|
|
254
|
+
✅ quick fix (15m)
|
|
255
|
+
|
|
256
|
+
Next:
|
|
257
|
+
• /p:now - Start next task
|
|
258
|
+
• /p:ship - Ship completed work
|
|
259
|
+
```
|