@phnx-labs/agents-cli 1.22.104 → 1.22.106
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 +18 -0
- package/README.md +19 -1
- package/dist/browser.js +0 -0
- package/dist/commands/exec.js +135 -233
- package/dist/commands/resume.d.ts +6 -21
- package/dist/commands/resume.js +18 -55
- package/dist/commands/run-device-picker.d.ts +58 -0
- package/dist/commands/run-device-picker.js +222 -0
- package/dist/commands/sessions-resume.d.ts +4 -0
- package/dist/commands/sessions-resume.js +132 -49
- package/dist/commands/sessions.js +32 -5
- package/dist/commands/setup-computer.js +2 -2
- package/dist/index.js +0 -0
- package/dist/lib/accounting/account-launch.d.ts +54 -0
- package/dist/lib/accounting/account-launch.js +117 -0
- package/dist/lib/accounting/account-pool-collect.js +2 -1
- package/dist/lib/accounting/account-pool.d.ts +2 -0
- package/dist/lib/accounting/account-pool.js +1 -0
- package/dist/lib/accounting/rotate.d.ts +32 -6
- package/dist/lib/accounting/rotate.js +70 -42
- package/dist/lib/accounting/usage.d.ts +71 -0
- package/dist/lib/accounting/usage.js +160 -11
- package/dist/lib/exec-account-home.d.ts +3 -1
- package/dist/lib/exec-account-home.js +2 -2
- package/dist/lib/exec.d.ts +27 -1
- package/dist/lib/exec.js +150 -27
- package/dist/lib/models.d.ts +1 -1
- package/dist/lib/models.js +4 -4
- package/dist/lib/session/actor-sidecar.d.ts +3 -11
- package/dist/lib/session/actor-sidecar.js +3 -0
- package/dist/lib/session/claude-accounts.d.ts +12 -73
- package/dist/lib/session/claude-accounts.js +32 -70
- package/dist/lib/session/db.d.ts +1 -1
- package/dist/lib/session/db.js +18 -5
- package/dist/lib/session/discover.d.ts +4 -0
- package/dist/lib/session/discover.js +116 -15
- package/dist/lib/session/recovery.d.ts +30 -34
- package/dist/lib/session/recovery.js +212 -76
- package/dist/lib/session/types.d.ts +2 -0
- package/dist/lib/teams/placement-probe.js +1 -1
- package/dist/session-tracker/dist/adapters/claude.d.ts +10 -0
- package/dist/session-tracker/dist/adapters/claude.js +45 -0
- package/dist/session-tracker/dist/hook.sh +191 -0
- package/dist/session-tracker/dist/index.d.ts +19 -0
- package/dist/session-tracker/dist/index.js +67 -0
- package/dist/session-tracker/dist/install-hook.d.ts +19 -0
- package/dist/session-tracker/dist/install-hook.js +245 -0
- package/dist/session-tracker/dist/prune-state.d.ts +2 -0
- package/dist/session-tracker/dist/prune-state.js +7 -0
- package/dist/session-tracker/dist/reader.d.ts +7 -0
- package/dist/session-tracker/dist/reader.js +151 -0
- package/dist/session-tracker/dist/state-file.d.ts +10 -0
- package/dist/session-tracker/dist/state-file.js +119 -0
- package/dist/session-tracker/dist/types.d.ts +32 -0
- package/dist/session-tracker/dist/types.js +1 -0
- package/dist/session-tracker/dist/writer.d.ts +12 -0
- package/dist/session-tracker/dist/writer.js +27 -0
- package/package.json +1 -1
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { exec } from 'child_process';
|
|
2
|
+
import * as fs from 'fs';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
import { promisify } from 'util';
|
|
5
|
+
import { STATE_DIR, parseState, stateFilePath } from './state-file.js';
|
|
6
|
+
const execAsync = promisify(exec);
|
|
7
|
+
const PID_TREE_MAX_DEPTH = 5;
|
|
8
|
+
const PID_TREE_MAX_NODES = 100;
|
|
9
|
+
// macOS `pgrep -P` silently misses children for some pids; `ps -eo` is reliable.
|
|
10
|
+
async function buildChildIndex() {
|
|
11
|
+
const index = new Map();
|
|
12
|
+
try {
|
|
13
|
+
const { stdout } = await execAsync('ps -eo pid,ppid', { timeout: 2000 });
|
|
14
|
+
for (const line of stdout.split('\n').slice(1)) {
|
|
15
|
+
const [pidStr, ppidStr] = line.trim().split(/\s+/);
|
|
16
|
+
const pid = Number(pidStr);
|
|
17
|
+
const ppid = Number(ppidStr);
|
|
18
|
+
if (!Number.isFinite(pid) || !Number.isFinite(ppid))
|
|
19
|
+
continue;
|
|
20
|
+
const kids = index.get(ppid);
|
|
21
|
+
if (kids)
|
|
22
|
+
kids.push(pid);
|
|
23
|
+
else
|
|
24
|
+
index.set(ppid, [pid]);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
/* empty index — caller treats as no descendants */
|
|
29
|
+
}
|
|
30
|
+
return index;
|
|
31
|
+
}
|
|
32
|
+
export async function descendantPids(rootPid) {
|
|
33
|
+
const index = await buildChildIndex();
|
|
34
|
+
const visited = new Set([rootPid]);
|
|
35
|
+
const out = [];
|
|
36
|
+
const queue = [{ pid: rootPid, depth: 0 }];
|
|
37
|
+
while (queue.length > 0 && visited.size < PID_TREE_MAX_NODES) {
|
|
38
|
+
const { pid, depth } = queue.shift();
|
|
39
|
+
if (depth >= PID_TREE_MAX_DEPTH)
|
|
40
|
+
continue;
|
|
41
|
+
for (const c of index.get(pid) ?? []) {
|
|
42
|
+
if (visited.has(c))
|
|
43
|
+
continue;
|
|
44
|
+
visited.add(c);
|
|
45
|
+
out.push(c);
|
|
46
|
+
queue.push({ pid: c, depth: depth + 1 });
|
|
47
|
+
if (visited.size >= PID_TREE_MAX_NODES)
|
|
48
|
+
break;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return out;
|
|
52
|
+
}
|
|
53
|
+
export async function findStateByPid(pid) {
|
|
54
|
+
try {
|
|
55
|
+
const raw = await fs.promises.readFile(stateFilePath(pid), 'utf8');
|
|
56
|
+
return parseState(raw);
|
|
57
|
+
}
|
|
58
|
+
catch (err) {
|
|
59
|
+
if (err.code === 'ENOENT')
|
|
60
|
+
return null;
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
export async function findStateInTree(shellPid) {
|
|
65
|
+
const pids = [shellPid, ...(await descendantPids(shellPid))];
|
|
66
|
+
let best = null;
|
|
67
|
+
for (const pid of pids) {
|
|
68
|
+
const s = await findStateByPid(pid);
|
|
69
|
+
if (s && (!best || s.ts > best.ts))
|
|
70
|
+
best = s;
|
|
71
|
+
}
|
|
72
|
+
return best;
|
|
73
|
+
}
|
|
74
|
+
async function scanAllStates() {
|
|
75
|
+
let names;
|
|
76
|
+
try {
|
|
77
|
+
names = await fs.promises.readdir(STATE_DIR);
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
return [];
|
|
81
|
+
}
|
|
82
|
+
const out = [];
|
|
83
|
+
for (const name of names) {
|
|
84
|
+
if (!name.endsWith('.json'))
|
|
85
|
+
continue;
|
|
86
|
+
try {
|
|
87
|
+
const raw = await fs.promises.readFile(path.join(STATE_DIR, name), 'utf8');
|
|
88
|
+
const s = parseState(raw);
|
|
89
|
+
if (s)
|
|
90
|
+
out.push(s);
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
/* skip */
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return out;
|
|
97
|
+
}
|
|
98
|
+
export async function findStateByTerminalId(terminalId) {
|
|
99
|
+
const all = await scanAllStates();
|
|
100
|
+
let best = null;
|
|
101
|
+
for (const s of all) {
|
|
102
|
+
if (s.terminal_id !== terminalId)
|
|
103
|
+
continue;
|
|
104
|
+
if (!best || s.ts > best.ts)
|
|
105
|
+
best = s;
|
|
106
|
+
}
|
|
107
|
+
return best;
|
|
108
|
+
}
|
|
109
|
+
export async function findStateByLaunchId(launchId) {
|
|
110
|
+
const all = await scanAllStates();
|
|
111
|
+
let best = null;
|
|
112
|
+
for (const s of all) {
|
|
113
|
+
if (s.launch_id !== launchId)
|
|
114
|
+
continue;
|
|
115
|
+
if (!best || s.ts > best.ts)
|
|
116
|
+
best = s;
|
|
117
|
+
}
|
|
118
|
+
return best;
|
|
119
|
+
}
|
|
120
|
+
export async function pruneStaleSessionState() {
|
|
121
|
+
let names;
|
|
122
|
+
try {
|
|
123
|
+
names = await fs.promises.readdir(STATE_DIR);
|
|
124
|
+
}
|
|
125
|
+
catch {
|
|
126
|
+
return 0;
|
|
127
|
+
}
|
|
128
|
+
let removed = 0;
|
|
129
|
+
for (const name of names) {
|
|
130
|
+
if (!name.endsWith('.json'))
|
|
131
|
+
continue;
|
|
132
|
+
const pid = Number(name.slice(0, -5));
|
|
133
|
+
if (!Number.isFinite(pid) || pid <= 0)
|
|
134
|
+
continue;
|
|
135
|
+
try {
|
|
136
|
+
process.kill(pid, 0);
|
|
137
|
+
}
|
|
138
|
+
catch (err) {
|
|
139
|
+
if (err.code === 'ESRCH') {
|
|
140
|
+
try {
|
|
141
|
+
await fs.promises.unlink(path.join(STATE_DIR, name));
|
|
142
|
+
removed++;
|
|
143
|
+
}
|
|
144
|
+
catch {
|
|
145
|
+
/* best-effort */
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return removed;
|
|
151
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { SessionState } from './types.js';
|
|
2
|
+
export declare const STATE_DIR: string;
|
|
3
|
+
export declare function stateFilePath(pid: number, stateDir?: string): string;
|
|
4
|
+
export declare function serializeState(s: SessionState): string;
|
|
5
|
+
export declare function parseState(raw: string): SessionState | null;
|
|
6
|
+
export declare function writeStateAtomic(state: SessionState, stateDir?: string): Promise<void>;
|
|
7
|
+
/** Remove stale state files: dead-pid records, zero-byte JSON files, and orphaned
|
|
8
|
+
* temp files left behind by failed atomic writes. Returns the number of files
|
|
9
|
+
* removed. Best-effort: individual failures are ignored and do not stop the sweep. */
|
|
10
|
+
export declare function cleanupOrphanedStateFiles(stateDir?: string): Promise<number>;
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as os from 'os';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
export const STATE_DIR = path.join(os.homedir(), '.agents', '.cache', 'terminals', 'sessions');
|
|
5
|
+
export function stateFilePath(pid, stateDir = STATE_DIR) {
|
|
6
|
+
return path.join(stateDir, `${pid}.json`);
|
|
7
|
+
}
|
|
8
|
+
const KEY_ORDER = [
|
|
9
|
+
'session_id',
|
|
10
|
+
'agent',
|
|
11
|
+
'cwd',
|
|
12
|
+
'pid',
|
|
13
|
+
'terminal_id',
|
|
14
|
+
'launch_id',
|
|
15
|
+
'ts',
|
|
16
|
+
'method',
|
|
17
|
+
];
|
|
18
|
+
export function serializeState(s) {
|
|
19
|
+
const ordered = {};
|
|
20
|
+
for (const k of KEY_ORDER) {
|
|
21
|
+
const v = s[k];
|
|
22
|
+
if (v !== undefined)
|
|
23
|
+
ordered[k] = v;
|
|
24
|
+
}
|
|
25
|
+
return JSON.stringify(ordered);
|
|
26
|
+
}
|
|
27
|
+
export function parseState(raw) {
|
|
28
|
+
let obj;
|
|
29
|
+
try {
|
|
30
|
+
obj = JSON.parse(raw);
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
if (!obj || typeof obj !== 'object')
|
|
36
|
+
return null;
|
|
37
|
+
const o = obj;
|
|
38
|
+
if (typeof o.session_id !== 'string' ||
|
|
39
|
+
typeof o.cwd !== 'string' ||
|
|
40
|
+
typeof o.pid !== 'number' ||
|
|
41
|
+
typeof o.ts !== 'number') {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
// Legacy 04-capture hook omits agent + method. Default rather than reject.
|
|
45
|
+
if (typeof o.agent !== 'string')
|
|
46
|
+
o.agent = 'unknown';
|
|
47
|
+
if (typeof o.method !== 'string')
|
|
48
|
+
o.method = 'hook-stdin';
|
|
49
|
+
return o;
|
|
50
|
+
}
|
|
51
|
+
export async function writeStateAtomic(state, stateDir = STATE_DIR) {
|
|
52
|
+
await fs.promises.mkdir(stateDir, { recursive: true });
|
|
53
|
+
const finalPath = stateFilePath(state.pid, stateDir);
|
|
54
|
+
const tmpPath = `${finalPath}.${process.pid}.${Date.now()}.tmp`;
|
|
55
|
+
try {
|
|
56
|
+
await fs.promises.writeFile(tmpPath, serializeState(state), 'utf8');
|
|
57
|
+
await fs.promises.rename(tmpPath, finalPath);
|
|
58
|
+
}
|
|
59
|
+
catch (err) {
|
|
60
|
+
try {
|
|
61
|
+
await fs.promises.unlink(tmpPath);
|
|
62
|
+
}
|
|
63
|
+
catch { /* best-effort */ }
|
|
64
|
+
throw err;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
const PID_JSON_RE = /^(\d+)\.json$/;
|
|
68
|
+
const PID_TEMP_RE = /^\.(\d+)\.[^.]+$/;
|
|
69
|
+
/** Remove stale state files: dead-pid records, zero-byte JSON files, and orphaned
|
|
70
|
+
* temp files left behind by failed atomic writes. Returns the number of files
|
|
71
|
+
* removed. Best-effort: individual failures are ignored and do not stop the sweep. */
|
|
72
|
+
export async function cleanupOrphanedStateFiles(stateDir = STATE_DIR) {
|
|
73
|
+
let names;
|
|
74
|
+
try {
|
|
75
|
+
names = await fs.promises.readdir(stateDir);
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
return 0;
|
|
79
|
+
}
|
|
80
|
+
let removed = 0;
|
|
81
|
+
for (const name of names) {
|
|
82
|
+
const full = path.join(stateDir, name);
|
|
83
|
+
let stat;
|
|
84
|
+
try {
|
|
85
|
+
stat = await fs.promises.stat(full);
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
// Zero-byte JSON files are never valid state records.
|
|
91
|
+
if (stat.size === 0 && name.endsWith('.json')) {
|
|
92
|
+
try {
|
|
93
|
+
await fs.promises.unlink(full);
|
|
94
|
+
removed++;
|
|
95
|
+
}
|
|
96
|
+
catch { /* ignore race */ }
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
const pidMatch = PID_JSON_RE.exec(name) ?? PID_TEMP_RE.exec(name);
|
|
100
|
+
if (!pidMatch)
|
|
101
|
+
continue;
|
|
102
|
+
const pid = Number(pidMatch[1]);
|
|
103
|
+
if (!Number.isFinite(pid) || pid <= 0)
|
|
104
|
+
continue;
|
|
105
|
+
try {
|
|
106
|
+
process.kill(pid, 0);
|
|
107
|
+
}
|
|
108
|
+
catch (err) {
|
|
109
|
+
if (err.code === 'ESRCH') {
|
|
110
|
+
try {
|
|
111
|
+
await fs.promises.unlink(full);
|
|
112
|
+
removed++;
|
|
113
|
+
}
|
|
114
|
+
catch { /* ignore race */ }
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return removed;
|
|
119
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export type AgentId = 'claude' | 'codex' | 'gemini' | 'cursor' | 'grok' | 'kimi' | 'droid' | 'antigravity' | 'opencode' | 'hermes';
|
|
2
|
+
export interface TrackSpawnInput {
|
|
3
|
+
agent: AgentId;
|
|
4
|
+
agentPid: number;
|
|
5
|
+
shellPid?: number;
|
|
6
|
+
cwd: string;
|
|
7
|
+
terminalId?: string;
|
|
8
|
+
launchId: string;
|
|
9
|
+
}
|
|
10
|
+
export type DetectionMethod = 'hook-stdin' | 'hook-env' | 'fs-watch' | 'stdout-banner';
|
|
11
|
+
export type DetectionConfidence = 'high' | 'medium' | 'low';
|
|
12
|
+
export interface DetectionResult {
|
|
13
|
+
sessionId: string | null;
|
|
14
|
+
method: DetectionMethod | null;
|
|
15
|
+
latencyMs: number;
|
|
16
|
+
confidence: DetectionConfidence;
|
|
17
|
+
}
|
|
18
|
+
export interface SessionState {
|
|
19
|
+
session_id: string;
|
|
20
|
+
agent: AgentId;
|
|
21
|
+
cwd: string;
|
|
22
|
+
pid: number;
|
|
23
|
+
terminal_id?: string;
|
|
24
|
+
launch_id?: string;
|
|
25
|
+
ts: number;
|
|
26
|
+
method: DetectionMethod;
|
|
27
|
+
}
|
|
28
|
+
export interface LookupInput {
|
|
29
|
+
shellPid?: number;
|
|
30
|
+
terminalId?: string;
|
|
31
|
+
launchId?: string;
|
|
32
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { AgentId, DetectionMethod } from './types.js';
|
|
2
|
+
export interface RecordSessionArgs {
|
|
3
|
+
sessionId: string;
|
|
4
|
+
agent: AgentId;
|
|
5
|
+
pid: number;
|
|
6
|
+
cwd: string;
|
|
7
|
+
terminalId?: string;
|
|
8
|
+
launchId?: string;
|
|
9
|
+
method: DetectionMethod;
|
|
10
|
+
}
|
|
11
|
+
export declare function recordSession(args: RecordSessionArgs): Promise<void>;
|
|
12
|
+
export declare function clearSession(pid: number): Promise<void>;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import { stateFilePath, writeStateAtomic } from './state-file.js';
|
|
3
|
+
export async function recordSession(args) {
|
|
4
|
+
const state = {
|
|
5
|
+
session_id: args.sessionId,
|
|
6
|
+
agent: args.agent,
|
|
7
|
+
cwd: args.cwd,
|
|
8
|
+
pid: args.pid,
|
|
9
|
+
ts: Date.now(),
|
|
10
|
+
method: args.method,
|
|
11
|
+
};
|
|
12
|
+
if (args.terminalId)
|
|
13
|
+
state.terminal_id = args.terminalId;
|
|
14
|
+
if (args.launchId)
|
|
15
|
+
state.launch_id = args.launchId;
|
|
16
|
+
await writeStateAtomic(state);
|
|
17
|
+
}
|
|
18
|
+
export async function clearSession(pid) {
|
|
19
|
+
try {
|
|
20
|
+
await fs.promises.unlink(stateFilePath(pid));
|
|
21
|
+
}
|
|
22
|
+
catch (err) {
|
|
23
|
+
if (err.code !== 'ENOENT') {
|
|
24
|
+
throw err;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@phnx-labs/agents-cli",
|
|
3
|
-
"version": "1.22.
|
|
3
|
+
"version": "1.22.106",
|
|
4
4
|
"description": "One CLI for all your AI coding agents - versions, config, cloud dispatch, sessions, and teams (now with first-class Grok Build CLI support)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|