@ai-devkit/agent-manager 0.14.0 → 0.16.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/.eslintrc.json +1 -1
- package/dist/AgentManager.d.ts +14 -12
- package/dist/AgentManager.d.ts.map +1 -1
- package/dist/AgentManager.js +40 -33
- package/dist/AgentManager.js.map +1 -1
- package/dist/adapters/AgentAdapter.js +2 -5
- package/dist/adapters/AgentAdapter.js.map +1 -1
- package/dist/adapters/ClaudeCodeAdapter.d.ts +1 -1
- package/dist/adapters/ClaudeCodeAdapter.d.ts.map +1 -1
- package/dist/adapters/ClaudeCodeAdapter.js +29 -63
- package/dist/adapters/ClaudeCodeAdapter.js.map +1 -1
- package/dist/adapters/CodexAdapter.d.ts +5 -2
- package/dist/adapters/CodexAdapter.d.ts.map +1 -1
- package/dist/adapters/CodexAdapter.js +66 -73
- package/dist/adapters/CodexAdapter.js.map +1 -1
- package/dist/adapters/GeminiCliAdapter.d.ts +5 -2
- package/dist/adapters/GeminiCliAdapter.d.ts.map +1 -1
- package/dist/adapters/GeminiCliAdapter.js +63 -70
- package/dist/adapters/GeminiCliAdapter.js.map +1 -1
- package/dist/adapters/OpenCodeAdapter.d.ts +1 -1
- package/dist/adapters/OpenCodeAdapter.d.ts.map +1 -1
- package/dist/adapters/OpenCodeAdapter.js +20 -59
- package/dist/adapters/OpenCodeAdapter.js.map +1 -1
- package/dist/adapters/index.d.ts +6 -6
- package/dist/adapters/index.d.ts.map +1 -1
- package/dist/adapters/index.js +5 -13
- package/dist/adapters/index.js.map +1 -1
- package/dist/index.d.ts +18 -11
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +12 -22
- package/dist/index.js.map +1 -1
- package/dist/terminal/TerminalFocusManager.js +13 -17
- package/dist/terminal/TerminalFocusManager.js.map +1 -1
- package/dist/terminal/TmuxManager.d.ts +28 -0
- package/dist/terminal/TmuxManager.d.ts.map +1 -0
- package/dist/terminal/TmuxManager.js +112 -0
- package/dist/terminal/TmuxManager.js.map +1 -0
- package/dist/terminal/TtyWriter.d.ts +1 -1
- package/dist/terminal/TtyWriter.d.ts.map +1 -1
- package/dist/terminal/TtyWriter.js +11 -15
- package/dist/terminal/TtyWriter.js.map +1 -1
- package/dist/terminal/index.d.ts +4 -4
- package/dist/terminal/index.d.ts.map +1 -1
- package/dist/terminal/index.js +3 -9
- package/dist/terminal/index.js.map +1 -1
- package/dist/utils/AgentRegistry.d.ts +35 -0
- package/dist/utils/AgentRegistry.d.ts.map +1 -0
- package/dist/utils/AgentRegistry.js +114 -0
- package/dist/utils/AgentRegistry.js.map +1 -0
- package/dist/utils/ClaudeSessionParser.d.ts +2 -2
- package/dist/utils/ClaudeSessionParser.d.ts.map +1 -1
- package/dist/utils/ClaudeSessionParser.js +32 -59
- package/dist/utils/ClaudeSessionParser.js.map +1 -1
- package/dist/utils/agents.d.ts +15 -0
- package/dist/utils/agents.d.ts.map +1 -0
- package/dist/utils/agents.js +30 -0
- package/dist/utils/agents.js.map +1 -0
- package/dist/utils/applescript.js +1 -4
- package/dist/utils/applescript.js.map +1 -1
- package/dist/utils/index.d.ts +6 -6
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +4 -15
- package/dist/utils/index.js.map +1 -1
- package/dist/utils/matching.d.ts +2 -2
- package/dist/utils/matching.d.ts.map +1 -1
- package/dist/utils/matching.js +3 -40
- package/dist/utils/matching.js.map +1 -1
- package/dist/utils/process.d.ts +1 -1
- package/dist/utils/process.d.ts.map +1 -1
- package/dist/utils/process.js +12 -52
- package/dist/utils/process.js.map +1 -1
- package/dist/utils/session.js +8 -49
- package/dist/utils/session.js.map +1 -1
- package/dist/utils/sortAgents.d.ts +4 -0
- package/dist/utils/sortAgents.d.ts.map +1 -0
- package/dist/utils/sortAgents.js +48 -0
- package/dist/utils/sortAgents.js.map +1 -0
- package/package.json +9 -8
- package/src/AgentManager.ts +53 -28
- package/src/__tests__/AgentManager.test.ts +140 -5
- package/src/__tests__/adapters/ClaudeCodeAdapter.test.ts +45 -44
- package/src/__tests__/adapters/CodexAdapter.test.ts +137 -30
- package/src/__tests__/adapters/GeminiCliAdapter.test.ts +130 -18
- package/src/__tests__/adapters/OpenCodeAdapter.test.ts +23 -21
- package/src/__tests__/terminal/TmuxManager.test.ts +175 -0
- package/src/__tests__/terminal/TtyWriter.test.ts +10 -9
- package/src/__tests__/utils/AgentRegistry.test.ts +223 -0
- package/src/__tests__/utils/ClaudeSessionParser.test.ts +73 -2
- package/src/__tests__/utils/matching.test.ts +4 -4
- package/src/__tests__/utils/process.test.ts +6 -5
- package/src/__tests__/utils/session.test.ts +8 -7
- package/src/adapters/ClaudeCodeAdapter.ts +8 -8
- package/src/adapters/CodexAdapter.ts +51 -12
- package/src/adapters/GeminiCliAdapter.ts +51 -12
- package/src/adapters/OpenCodeAdapter.ts +4 -4
- package/src/adapters/index.ts +6 -6
- package/src/index.ts +19 -11
- package/src/terminal/TerminalFocusManager.ts +2 -2
- package/src/terminal/TmuxManager.ts +118 -0
- package/src/terminal/TtyWriter.ts +3 -3
- package/src/terminal/index.ts +4 -4
- package/src/utils/AgentRegistry.ts +139 -0
- package/src/utils/ClaudeSessionParser.ts +23 -12
- package/src/utils/agents.ts +41 -0
- package/src/utils/index.ts +6 -6
- package/src/utils/matching.ts +2 -2
- package/src/utils/process.ts +1 -1
- package/src/utils/sortAgents.ts +52 -0
- package/tsconfig.json +2 -2
- package/vitest.config.ts +20 -0
- package/jest.config.js +0 -21
package/src/index.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
export { AgentManager } from './AgentManager';
|
|
1
|
+
export { AgentManager } from './AgentManager.js';
|
|
2
2
|
|
|
3
|
-
export { ClaudeCodeAdapter } from './adapters/ClaudeCodeAdapter';
|
|
4
|
-
export { CodexAdapter } from './adapters/CodexAdapter';
|
|
5
|
-
export { GeminiCliAdapter } from './adapters/GeminiCliAdapter';
|
|
6
|
-
export { OpenCodeAdapter } from './adapters/OpenCodeAdapter';
|
|
7
|
-
export { AgentStatus } from './adapters/AgentAdapter';
|
|
3
|
+
export { ClaudeCodeAdapter } from './adapters/ClaudeCodeAdapter.js';
|
|
4
|
+
export { CodexAdapter } from './adapters/CodexAdapter.js';
|
|
5
|
+
export { GeminiCliAdapter } from './adapters/GeminiCliAdapter.js';
|
|
6
|
+
export { OpenCodeAdapter } from './adapters/OpenCodeAdapter.js';
|
|
7
|
+
export { AgentStatus } from './adapters/AgentAdapter.js';
|
|
8
8
|
export type {
|
|
9
9
|
AgentAdapter,
|
|
10
10
|
AgentType,
|
|
@@ -13,10 +13,18 @@ export type {
|
|
|
13
13
|
ConversationMessage,
|
|
14
14
|
SessionSummary,
|
|
15
15
|
ListSessionsOptions,
|
|
16
|
-
} from './adapters/AgentAdapter';
|
|
16
|
+
} from './adapters/AgentAdapter.js';
|
|
17
17
|
|
|
18
|
-
export { TerminalFocusManager, TerminalType } from './terminal/TerminalFocusManager';
|
|
19
|
-
export type { TerminalLocation } from './terminal/TerminalFocusManager';
|
|
20
|
-
export { TtyWriter } from './terminal/TtyWriter';
|
|
18
|
+
export { TerminalFocusManager, TerminalType } from './terminal/TerminalFocusManager.js';
|
|
19
|
+
export type { TerminalLocation } from './terminal/TerminalFocusManager.js';
|
|
20
|
+
export { TtyWriter } from './terminal/TtyWriter.js';
|
|
21
21
|
|
|
22
|
-
export { getProcessTty } from './utils/process';
|
|
22
|
+
export { getProcessTty } from './utils/process.js';
|
|
23
|
+
export type { AgentSortKey } from './utils/sortAgents.js';
|
|
24
|
+
export type { ListAgentsOptions } from './AgentManager.js';
|
|
25
|
+
|
|
26
|
+
export { AgentRegistry, RenameNotFoundError, RenameConflictError } from './utils/AgentRegistry.js';
|
|
27
|
+
export type { RegistryEntry } from './utils/AgentRegistry.js';
|
|
28
|
+
export { TmuxManager } from './terminal/TmuxManager.js';
|
|
29
|
+
export { AGENTS } from './utils/agents.js';
|
|
30
|
+
export type { AgentConfig, StartableAgentType } from './utils/agents.js';
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { execFile } from 'child_process';
|
|
2
2
|
import { promisify } from 'util';
|
|
3
|
-
import { getProcessTty } from '../utils/process';
|
|
4
|
-
import { escapeAppleScript } from '../utils/applescript';
|
|
3
|
+
import { getProcessTty } from '../utils/process.js';
|
|
4
|
+
import { escapeAppleScript } from '../utils/applescript.js';
|
|
5
5
|
|
|
6
6
|
const execFileAsync = promisify(execFile);
|
|
7
7
|
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { execFile } from 'child_process';
|
|
2
|
+
import { promisify } from 'util';
|
|
3
|
+
|
|
4
|
+
const execFileAsync = promisify(execFile);
|
|
5
|
+
|
|
6
|
+
export class TmuxManager {
|
|
7
|
+
async isAvailable(): Promise<boolean> {
|
|
8
|
+
try {
|
|
9
|
+
await execFileAsync('tmux', ['-V']);
|
|
10
|
+
return true;
|
|
11
|
+
} catch {
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async sessionExists(name: string): Promise<boolean> {
|
|
17
|
+
try {
|
|
18
|
+
await execFileAsync('tmux', ['has-session', '-t', name]);
|
|
19
|
+
return true;
|
|
20
|
+
} catch {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async createSession(name: string, cwd: string): Promise<void> {
|
|
26
|
+
await execFileAsync('tmux', ['new-session', '-d', '-s', name, '-c', cwd]);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async sendKeys(session: string, keys: string): Promise<void> {
|
|
30
|
+
await execFileAsync('tmux', ['send-keys', '-t', session, keys, 'Enter']);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async killSession(name: string): Promise<void> {
|
|
34
|
+
try {
|
|
35
|
+
await execFileAsync('tmux', ['kill-session', '-t', name]);
|
|
36
|
+
} catch {
|
|
37
|
+
// Already gone — ignore
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Find the actual agent process PID inside a tmux pane.
|
|
43
|
+
*
|
|
44
|
+
* Strategy: BFS the process tree, return the deepest descendant whose
|
|
45
|
+
* `ps` command line is accepted by `matches`. The caller supplies the
|
|
46
|
+
* matcher so this method has no agent-type knowledge.
|
|
47
|
+
*
|
|
48
|
+
* This handles two real-world process shapes:
|
|
49
|
+
* - Wrapper case: shell → claude-wrapper (matches) → claude (matches, deeper)
|
|
50
|
+
* → returns the deeper one
|
|
51
|
+
* - Subprocess case: shell → claude (matches) → MCP server child (doesn't match)
|
|
52
|
+
* → returns claude, not the subprocess
|
|
53
|
+
*
|
|
54
|
+
* Returns null when no descendant matches yet (agent still starting); the
|
|
55
|
+
* caller's poll loop retries.
|
|
56
|
+
*/
|
|
57
|
+
async findAgentPid(session: string, matches: (psCommand: string) => boolean): Promise<number | null> {
|
|
58
|
+
const panePid = await this.getPanePid(session);
|
|
59
|
+
if (panePid === null) return null;
|
|
60
|
+
|
|
61
|
+
const visited = new Set<number>();
|
|
62
|
+
const queue: number[] = [panePid];
|
|
63
|
+
let deepestMatch: number | null = null;
|
|
64
|
+
|
|
65
|
+
while (queue.length > 0) {
|
|
66
|
+
const pid = queue.shift()!;
|
|
67
|
+
if (visited.has(pid)) continue;
|
|
68
|
+
visited.add(pid);
|
|
69
|
+
|
|
70
|
+
if (pid !== panePid) {
|
|
71
|
+
const command = await this.getProcessCommand(pid);
|
|
72
|
+
if (command && matches(command)) {
|
|
73
|
+
deepestMatch = pid;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const children = await this.pgrepChildren(pid);
|
|
78
|
+
queue.push(...children);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return deepestMatch;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
private async getPanePid(session: string): Promise<number | null> {
|
|
85
|
+
try {
|
|
86
|
+
const { stdout } = await execFileAsync('tmux', [
|
|
87
|
+
'list-panes', '-t', session, '-F', '#{pane_pid}',
|
|
88
|
+
]);
|
|
89
|
+
const panePid = parseInt(stdout.trim().split('\n')[0], 10);
|
|
90
|
+
return isNaN(panePid) ? null : panePid;
|
|
91
|
+
} catch {
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
private async pgrepChildren(pid: number): Promise<number[]> {
|
|
97
|
+
try {
|
|
98
|
+
const { stdout } = await execFileAsync('pgrep', ['-P', String(pid)]);
|
|
99
|
+
return stdout
|
|
100
|
+
.trim()
|
|
101
|
+
.split('\n')
|
|
102
|
+
.map((s) => parseInt(s, 10))
|
|
103
|
+
.filter((n) => !isNaN(n));
|
|
104
|
+
} catch {
|
|
105
|
+
return [];
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
private async getProcessCommand(pid: number): Promise<string | null> {
|
|
110
|
+
try {
|
|
111
|
+
const { stdout } = await execFileAsync('ps', ['-p', String(pid), '-o', 'command=']);
|
|
112
|
+
const trimmed = stdout.trim();
|
|
113
|
+
return trimmed || null;
|
|
114
|
+
} catch {
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { execFile } from 'child_process';
|
|
2
2
|
import { promisify } from 'util';
|
|
3
|
-
import type { TerminalLocation } from './TerminalFocusManager';
|
|
4
|
-
import { TerminalType } from './TerminalFocusManager';
|
|
5
|
-
import { escapeAppleScript } from '../utils/applescript';
|
|
3
|
+
import type { TerminalLocation } from './TerminalFocusManager.js';
|
|
4
|
+
import { TerminalType } from './TerminalFocusManager.js';
|
|
5
|
+
import { escapeAppleScript } from '../utils/applescript.js';
|
|
6
6
|
|
|
7
7
|
const execFileAsync = promisify(execFile);
|
|
8
8
|
|
package/src/terminal/index.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { TerminalFocusManager } from './TerminalFocusManager';
|
|
2
|
-
export { TerminalType } from './TerminalFocusManager';
|
|
3
|
-
export type { TerminalLocation } from './TerminalFocusManager';
|
|
4
|
-
export { TtyWriter } from './TtyWriter';
|
|
1
|
+
export { TerminalFocusManager } from './TerminalFocusManager.js';
|
|
2
|
+
export { TerminalType } from './TerminalFocusManager.js';
|
|
3
|
+
export type { TerminalLocation } from './TerminalFocusManager.js';
|
|
4
|
+
export { TtyWriter } from './TtyWriter.js';
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import os from 'os';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import type { AgentType } from '../adapters/AgentAdapter.js';
|
|
5
|
+
|
|
6
|
+
export class RenameNotFoundError extends Error {
|
|
7
|
+
constructor(public agentName: string) {
|
|
8
|
+
super(`Agent "${agentName}" not found in registry.`);
|
|
9
|
+
this.name = 'RenameNotFoundError';
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export class RenameConflictError extends Error {
|
|
14
|
+
constructor(public agentName: string) {
|
|
15
|
+
super(`Agent "${agentName}" is already in use.`);
|
|
16
|
+
this.name = 'RenameConflictError';
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface RegistryEntry {
|
|
21
|
+
name: string;
|
|
22
|
+
type: AgentType;
|
|
23
|
+
pid: number;
|
|
24
|
+
tmuxSession: string;
|
|
25
|
+
cwd: string;
|
|
26
|
+
startedAt: string; // ISO 8601
|
|
27
|
+
sessionId: string;
|
|
28
|
+
sessionFilePath: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
interface RegistryFile {
|
|
32
|
+
entries: RegistryEntry[];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const DEFAULT_REGISTRY_PATH = path.join(os.homedir(), '.ai-devkit', 'agents.json');
|
|
36
|
+
|
|
37
|
+
let defaultInstance: AgentRegistry | null = null;
|
|
38
|
+
|
|
39
|
+
export class AgentRegistry {
|
|
40
|
+
private filePath: string;
|
|
41
|
+
|
|
42
|
+
constructor(filePath: string = DEFAULT_REGISTRY_PATH) {
|
|
43
|
+
this.filePath = filePath;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
static default(): AgentRegistry {
|
|
47
|
+
if (!defaultInstance) {
|
|
48
|
+
defaultInstance = new AgentRegistry();
|
|
49
|
+
}
|
|
50
|
+
return defaultInstance;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
private readFile(): RegistryFile {
|
|
54
|
+
try {
|
|
55
|
+
const raw = fs.readFileSync(this.filePath, 'utf8');
|
|
56
|
+
const parsed = JSON.parse(raw) as RegistryFile;
|
|
57
|
+
return { entries: Array.isArray(parsed.entries) ? parsed.entries : [] };
|
|
58
|
+
} catch {
|
|
59
|
+
return { entries: [] };
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
private writeFile(data: RegistryFile): void {
|
|
64
|
+
const dir = path.dirname(this.filePath);
|
|
65
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
66
|
+
const tmp = `${this.filePath}.tmp`;
|
|
67
|
+
fs.writeFileSync(tmp, JSON.stringify(data, null, 2), 'utf8');
|
|
68
|
+
fs.renameSync(tmp, this.filePath);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
private mergeEntry(incoming: RegistryEntry, existing: RegistryEntry | undefined): RegistryEntry {
|
|
72
|
+
if (!existing) return incoming;
|
|
73
|
+
return {
|
|
74
|
+
...incoming,
|
|
75
|
+
tmuxSession: incoming.tmuxSession || existing.tmuxSession,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
isAlive(entry: RegistryEntry): boolean {
|
|
80
|
+
try {
|
|
81
|
+
process.kill(entry.pid, 0);
|
|
82
|
+
return true;
|
|
83
|
+
} catch {
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
prune(): void {
|
|
89
|
+
const data = this.readFile();
|
|
90
|
+
const live = data.entries.filter((e) => this.isAlive(e));
|
|
91
|
+
if (live.length !== data.entries.length) {
|
|
92
|
+
this.writeFile({ entries: live });
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
register(entry: RegistryEntry): void {
|
|
97
|
+
this.registerBatch([entry]);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
registerBatch(entries: RegistryEntry[]): void {
|
|
101
|
+
if (entries.length === 0) return;
|
|
102
|
+
const data = this.readFile();
|
|
103
|
+
for (const incoming of entries) {
|
|
104
|
+
const idx = data.entries.findIndex((e) => e.name === incoming.name);
|
|
105
|
+
if (idx >= 0) {
|
|
106
|
+
data.entries[idx] = this.mergeEntry(incoming, data.entries[idx]);
|
|
107
|
+
} else {
|
|
108
|
+
data.entries.push(incoming);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
this.writeFile(data);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
rename(currentName: string, newName: string): void {
|
|
115
|
+
const data = this.readFile();
|
|
116
|
+
const idx = data.entries.findIndex((e) => e.name === currentName);
|
|
117
|
+
if (idx < 0) {
|
|
118
|
+
throw new RenameNotFoundError(currentName);
|
|
119
|
+
}
|
|
120
|
+
const liveEntries = data.entries.filter((e) => this.isAlive(e));
|
|
121
|
+
const conflict = liveEntries.find((e) => e.name === newName);
|
|
122
|
+
if (conflict) {
|
|
123
|
+
throw new RenameConflictError(newName);
|
|
124
|
+
}
|
|
125
|
+
const pruned = liveEntries.map((e) =>
|
|
126
|
+
e.name === currentName ? { ...e, name: newName } : e,
|
|
127
|
+
);
|
|
128
|
+
this.writeFile({ entries: pruned });
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
lookup(name: string): RegistryEntry | null {
|
|
132
|
+
const data = this.readFile();
|
|
133
|
+
return data.entries.find((e) => e.name === name) ?? null;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
list(): RegistryEntry[] {
|
|
137
|
+
return this.readFile().entries;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as fs from 'fs';
|
|
2
2
|
import * as path from 'path';
|
|
3
|
-
import type { ConversationMessage } from '../adapters/AgentAdapter';
|
|
4
|
-
import { AgentStatus } from '../adapters/AgentAdapter';
|
|
3
|
+
import type { ConversationMessage } from '../adapters/AgentAdapter.js';
|
|
4
|
+
import { AgentStatus } from '../adapters/AgentAdapter.js';
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* Content block within a Claude Code JSONL message entry.
|
|
@@ -51,8 +51,23 @@ export interface ClaudeSession {
|
|
|
51
51
|
firstUserMessage?: string;
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
-
/**
|
|
55
|
-
|
|
54
|
+
/**
|
|
55
|
+
* Top-level JSONL entry types that represent conversation/agent state.
|
|
56
|
+
*
|
|
57
|
+
* Only these types update `lastEntryType` for status determination. All
|
|
58
|
+
* other types Claude Code emits (`attachment`, `permission-mode`,
|
|
59
|
+
* `ai-title`, `queued_command`, `tools_changed`, `model_changed`,
|
|
60
|
+
* `hook_progress`, …) are UI-state events that must not overwrite the
|
|
61
|
+
* last conversation turn — otherwise polling between writes lands on a
|
|
62
|
+
* UI-state entry and `determineStatus` falls through to UNKNOWN.
|
|
63
|
+
*/
|
|
64
|
+
const CONVERSATION_ENTRY_TYPES = new Set([
|
|
65
|
+
'user',
|
|
66
|
+
'assistant',
|
|
67
|
+
'system',
|
|
68
|
+
'progress',
|
|
69
|
+
'thinking',
|
|
70
|
+
]);
|
|
56
71
|
|
|
57
72
|
/**
|
|
58
73
|
* Parses Claude Code session JSONL files into structured data.
|
|
@@ -110,7 +125,7 @@ export class ClaudeSessionParser {
|
|
|
110
125
|
lastCwd = entry.cwd;
|
|
111
126
|
}
|
|
112
127
|
|
|
113
|
-
if (entry.type &&
|
|
128
|
+
if (entry.type && CONVERSATION_ENTRY_TYPES.has(entry.type)) {
|
|
114
129
|
lastEntryType = entry.type;
|
|
115
130
|
|
|
116
131
|
if (entry.type === 'user') {
|
|
@@ -220,16 +235,12 @@ export class ClaudeSessionParser {
|
|
|
220
235
|
continue;
|
|
221
236
|
}
|
|
222
237
|
|
|
223
|
-
const entryType = entry.type;
|
|
224
|
-
if (!entryType || METADATA_ENTRY_TYPES.has(entryType)) continue;
|
|
225
|
-
if (entryType === 'progress' || entryType === 'thinking') continue;
|
|
226
|
-
|
|
227
238
|
let role: ConversationMessage['role'];
|
|
228
|
-
if (
|
|
239
|
+
if (entry.type === 'user') {
|
|
229
240
|
role = 'user';
|
|
230
|
-
} else if (
|
|
241
|
+
} else if (entry.type === 'assistant') {
|
|
231
242
|
role = 'assistant';
|
|
232
|
-
} else if (
|
|
243
|
+
} else if (entry.type === 'system') {
|
|
233
244
|
role = 'system';
|
|
234
245
|
} else {
|
|
235
246
|
continue;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import type { AgentType } from '../adapters/AgentAdapter.js';
|
|
3
|
+
|
|
4
|
+
export type StartableAgentType = Exclude<AgentType, 'other'>;
|
|
5
|
+
|
|
6
|
+
export interface AgentConfig {
|
|
7
|
+
/** Shell command to launch the agent (sent to tmux via `send-keys`). */
|
|
8
|
+
command: string;
|
|
9
|
+
/** Returns true if the given `ps` command line belongs to this agent. */
|
|
10
|
+
matches: (psCommand: string) => boolean;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Per-agent configuration: launch command plus a matcher that recognizes the
|
|
15
|
+
* agent's process in `ps` output. Each matcher knows that agent's distribution
|
|
16
|
+
* quirks (e.g. gemini ships as a Node script so its real binary is in argv[1..]).
|
|
17
|
+
*/
|
|
18
|
+
export const AGENTS: Record<StartableAgentType, AgentConfig> = {
|
|
19
|
+
claude: { command: 'claude', matches: matchArgv0('claude') },
|
|
20
|
+
codex: { command: 'codex', matches: matchArgv0('codex') },
|
|
21
|
+
opencode: { command: 'opencode', matches: matchArgv0('opencode') },
|
|
22
|
+
gemini_cli: { command: 'gemini', matches: matchAnyToken('gemini') },
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
function matchArgv0(name: string): (psCommand: string) => boolean {
|
|
26
|
+
const lower = name.toLowerCase();
|
|
27
|
+
return (psCommand) => {
|
|
28
|
+
const token = psCommand.trim().split(/\s+/)[0];
|
|
29
|
+
return token ? path.basename(token).toLowerCase() === lower : false;
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function matchAnyToken(name: string): (psCommand: string) => boolean {
|
|
34
|
+
const lower = name.toLowerCase();
|
|
35
|
+
return (psCommand) => {
|
|
36
|
+
for (const token of psCommand.trim().split(/\s+/)) {
|
|
37
|
+
if (path.basename(token).toLowerCase() === lower) return true;
|
|
38
|
+
}
|
|
39
|
+
return false;
|
|
40
|
+
};
|
|
41
|
+
}
|
package/src/utils/index.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
export { listAgentProcesses, batchGetProcessCwds, batchGetProcessStartTimes, enrichProcesses } from './process';
|
|
2
|
-
export { getProcessTty } from './process';
|
|
3
|
-
export { batchGetSessionFileBirthtimes } from './session';
|
|
4
|
-
export type { SessionFile } from './session';
|
|
5
|
-
export { matchProcessesToSessions, generateAgentName } from './matching';
|
|
6
|
-
export type { MatchResult } from './matching';
|
|
1
|
+
export { listAgentProcesses, batchGetProcessCwds, batchGetProcessStartTimes, enrichProcesses } from './process.js';
|
|
2
|
+
export { getProcessTty } from './process.js';
|
|
3
|
+
export { batchGetSessionFileBirthtimes } from './session.js';
|
|
4
|
+
export type { SessionFile } from './session.js';
|
|
5
|
+
export { matchProcessesToSessions, generateAgentName } from './matching.js';
|
|
6
|
+
export type { MatchResult } from './matching.js';
|
package/src/utils/matching.ts
CHANGED
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import * as path from 'path';
|
|
9
|
-
import type { ProcessInfo } from '../adapters/AgentAdapter';
|
|
10
|
-
import type { SessionFile } from './session';
|
|
9
|
+
import type { ProcessInfo } from '../adapters/AgentAdapter.js';
|
|
10
|
+
import type { SessionFile } from './session.js';
|
|
11
11
|
|
|
12
12
|
/** Maximum allowed delta between process start time and session file birth time. */
|
|
13
13
|
const TOLERANCE_MS = 3 * 60 * 1000; // 3 minutes
|
package/src/utils/process.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
import * as path from 'path';
|
|
9
9
|
import { execFileSync } from 'child_process';
|
|
10
|
-
import type { ProcessInfo } from '../adapters/AgentAdapter';
|
|
10
|
+
import type { ProcessInfo } from '../adapters/AgentAdapter.js';
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
13
|
* List running processes matching an agent executable name.
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { AgentStatus, type AgentInfo } from '../adapters/AgentAdapter.js';
|
|
2
|
+
|
|
3
|
+
export type AgentSortKey = 'name' | 'status' | 'pid' | 'lastActive';
|
|
4
|
+
|
|
5
|
+
const STATUS_PRIORITY: Record<AgentStatus, number> = {
|
|
6
|
+
[AgentStatus.WAITING]: 0,
|
|
7
|
+
[AgentStatus.RUNNING]: 1,
|
|
8
|
+
[AgentStatus.IDLE]: 2,
|
|
9
|
+
[AgentStatus.UNKNOWN]: 3,
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
function compareByName(a: AgentInfo, b: AgentInfo): number {
|
|
13
|
+
return a.name.localeCompare(b.name);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function compareByStatus(a: AgentInfo, b: AgentInfo): number {
|
|
17
|
+
const pa = STATUS_PRIORITY[a.status] ?? 999;
|
|
18
|
+
const pb = STATUS_PRIORITY[b.status] ?? 999;
|
|
19
|
+
if (pa !== pb) return pa - pb;
|
|
20
|
+
return compareByName(a, b);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function compareByPid(a: AgentInfo, b: AgentInfo): number {
|
|
24
|
+
if (a.pid !== b.pid) return a.pid - b.pid;
|
|
25
|
+
return compareByName(a, b);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function compareByLastActive(a: AgentInfo, b: AgentInfo): number {
|
|
29
|
+
const ta = a.lastActive instanceof Date ? a.lastActive.getTime() : new Date(a.lastActive).getTime();
|
|
30
|
+
const tb = b.lastActive instanceof Date ? b.lastActive.getTime() : new Date(b.lastActive).getTime();
|
|
31
|
+
if (tb !== ta) return tb - ta; // most recent first
|
|
32
|
+
return compareByName(a, b);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function sortAgents(agents: AgentInfo[], by: AgentSortKey): AgentInfo[] {
|
|
36
|
+
const copy = agents.slice();
|
|
37
|
+
switch (by) {
|
|
38
|
+
case 'name':
|
|
39
|
+
copy.sort(compareByName);
|
|
40
|
+
break;
|
|
41
|
+
case 'status':
|
|
42
|
+
copy.sort(compareByStatus);
|
|
43
|
+
break;
|
|
44
|
+
case 'pid':
|
|
45
|
+
copy.sort(compareByPid);
|
|
46
|
+
break;
|
|
47
|
+
case 'lastActive':
|
|
48
|
+
copy.sort(compareByLastActive);
|
|
49
|
+
break;
|
|
50
|
+
}
|
|
51
|
+
return copy;
|
|
52
|
+
}
|
package/tsconfig.json
CHANGED
package/vitest.config.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { defineConfig } from 'vitest/config';
|
|
2
|
+
|
|
3
|
+
export default defineConfig({
|
|
4
|
+
test: {
|
|
5
|
+
globals: true,
|
|
6
|
+
environment: 'node',
|
|
7
|
+
include: ['src/**/__tests__/**/*.test.ts'],
|
|
8
|
+
coverage: {
|
|
9
|
+
provider: 'v8',
|
|
10
|
+
include: ['src/**/*.ts'],
|
|
11
|
+
exclude: ['src/index.ts', 'src/**/*.d.ts'],
|
|
12
|
+
thresholds: {
|
|
13
|
+
branches: 70,
|
|
14
|
+
functions: 70,
|
|
15
|
+
lines: 70,
|
|
16
|
+
statements: 70,
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
});
|
package/jest.config.js
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
module.exports = {
|
|
2
|
-
preset: 'ts-jest',
|
|
3
|
-
testEnvironment: 'node',
|
|
4
|
-
roots: ['<rootDir>/src'],
|
|
5
|
-
testMatch: ['**/__tests__/**/*.test.ts', '**/?(*.)+(spec|test).ts'],
|
|
6
|
-
collectCoverageFrom: [
|
|
7
|
-
'src/**/*.{ts,js}',
|
|
8
|
-
'!src/**/*.d.ts',
|
|
9
|
-
'!src/index.ts'
|
|
10
|
-
],
|
|
11
|
-
coverageDirectory: 'coverage',
|
|
12
|
-
coverageReporters: ['text', 'lcov', 'html'],
|
|
13
|
-
coverageThreshold: {
|
|
14
|
-
global: {
|
|
15
|
-
branches: 80,
|
|
16
|
-
functions: 80,
|
|
17
|
-
lines: 80,
|
|
18
|
-
statements: 80
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
};
|