@kaynooo/cli-kit 0.11.0 → 0.12.0
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/package.json +1 -1
- package/src/index.ts +1 -0
- package/src/sessions.ts +107 -0
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kaynooo/cli-kit",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.12.0",
|
|
5
5
|
"description": "What every @kaynooo CLI shares: argument parsing with --help, a command dispatcher with typo hints and shell completion, and output formatting.",
|
|
6
6
|
"author": "Kaynooo <contact@kaynooo.fr>",
|
|
7
7
|
"license": "MIT",
|
package/src/index.ts
CHANGED
package/src/sessions.ts
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { homedir } from 'node:os'
|
|
2
|
+
import { join } from 'node:path'
|
|
3
|
+
import process from 'node:process'
|
|
4
|
+
import { Glob } from 'bun'
|
|
5
|
+
|
|
6
|
+
// A running Claude Code session, as it registers itself in <config dir>/sessions/<pid>.json.
|
|
7
|
+
export interface Session { name: string, pid: number, status: string, cwd: string }
|
|
8
|
+
|
|
9
|
+
interface Entry { name?: string, pid?: number, procStart?: string, status?: string, cwd?: string }
|
|
10
|
+
|
|
11
|
+
const SESSION_FILES = new Glob('*.json')
|
|
12
|
+
const ACCOUNT_DIRS = new Glob('*/sessions')
|
|
13
|
+
|
|
14
|
+
async function sessionDirs(): Promise<string[]> {
|
|
15
|
+
const home = homedir()
|
|
16
|
+
const dirs = new Set([join(home, '.claude', 'sessions')])
|
|
17
|
+
|
|
18
|
+
if (process.env.CLAUDE_CONFIG_DIR !== undefined)
|
|
19
|
+
dirs.add(join(process.env.CLAUDE_CONFIG_DIR, 'sessions'))
|
|
20
|
+
|
|
21
|
+
try {
|
|
22
|
+
for await (const dir of ACCOUNT_DIRS.scan({ cwd: join(home, '.claude-accounts'), onlyFiles: false, absolute: true }))
|
|
23
|
+
dirs.add(dir)
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
// No ~/.claude-accounts: the default config dir is the only one.
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return [...dirs]
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// The fields of /proc/<pid>/stat after the command name, which may itself hold spaces and parentheses.
|
|
33
|
+
async function procStat(pid: number): Promise<string[] | undefined> {
|
|
34
|
+
try {
|
|
35
|
+
const stat = await Bun.file(`/proc/${pid}/stat`).text()
|
|
36
|
+
|
|
37
|
+
return stat.slice(stat.lastIndexOf(')') + 2).split(' ')
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
return undefined
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Alive and the same process the session registered, not a later one given its pid; starttime is field 22, the 20th after the name.
|
|
45
|
+
async function isAlive(pid: number, procStart: string | undefined): Promise<boolean> {
|
|
46
|
+
const stat = await procStat(pid)
|
|
47
|
+
|
|
48
|
+
if (stat === undefined)
|
|
49
|
+
return false
|
|
50
|
+
|
|
51
|
+
return procStart === undefined || stat[19] === procStart
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export async function liveSessions(): Promise<Session[]> {
|
|
55
|
+
const sessions = new Map<number, Session>()
|
|
56
|
+
|
|
57
|
+
for (const dir of await sessionDirs()) {
|
|
58
|
+
let files: string[]
|
|
59
|
+
|
|
60
|
+
try {
|
|
61
|
+
files = await Array.fromAsync(SESSION_FILES.scan({ cwd: dir, absolute: true }))
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
continue
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
for (const file of files) {
|
|
68
|
+
let entry: Entry
|
|
69
|
+
|
|
70
|
+
try {
|
|
71
|
+
entry = await Bun.file(file).json() as Entry
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
continue
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (entry.name === undefined || entry.pid === undefined || sessions.has(entry.pid) || !await isAlive(entry.pid, entry.procStart))
|
|
78
|
+
continue
|
|
79
|
+
|
|
80
|
+
sessions.set(entry.pid, { name: entry.name, pid: entry.pid, status: entry.status ?? 'unknown', cwd: entry.cwd ?? '' })
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return [...sessions.values()]
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// The session this process runs under, found by walking up its parent pids.
|
|
88
|
+
export async function currentSession(sessions: Session[]): Promise<Session | undefined> {
|
|
89
|
+
const byPid = new Map(sessions.map(session => [session.pid, session]))
|
|
90
|
+
let pid = process.ppid
|
|
91
|
+
|
|
92
|
+
for (let depth = 0; depth < 32 && pid > 1; depth++) {
|
|
93
|
+
const session = byPid.get(pid)
|
|
94
|
+
|
|
95
|
+
if (session !== undefined)
|
|
96
|
+
return session
|
|
97
|
+
|
|
98
|
+
const parent = Number((await procStat(pid))?.[1])
|
|
99
|
+
|
|
100
|
+
if (!Number.isInteger(parent))
|
|
101
|
+
return undefined
|
|
102
|
+
|
|
103
|
+
pid = parent
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return undefined
|
|
107
|
+
}
|