@ai-devkit/agent-manager 0.17.0 → 0.19.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.
Files changed (47) hide show
  1. package/dist/__tests__/adapters/CodexAdapter.test.js +80 -15
  2. package/dist/__tests__/adapters/CodexAdapter.test.js.map +1 -1
  3. package/dist/__tests__/adapters/PiAdapter.test.js +590 -0
  4. package/dist/__tests__/adapters/PiAdapter.test.js.map +1 -0
  5. package/dist/__tests__/terminal/TerminalFocusManager.test.js +73 -0
  6. package/dist/__tests__/terminal/TerminalFocusManager.test.js.map +1 -0
  7. package/dist/__tests__/utils/agents.test.js +17 -0
  8. package/dist/__tests__/utils/agents.test.js.map +1 -0
  9. package/dist/adapters/AgentAdapter.d.ts +1 -1
  10. package/dist/adapters/AgentAdapter.d.ts.map +1 -1
  11. package/dist/adapters/AgentAdapter.js.map +1 -1
  12. package/dist/adapters/CodexAdapter.d.ts +4 -0
  13. package/dist/adapters/CodexAdapter.d.ts.map +1 -1
  14. package/dist/adapters/CodexAdapter.js +38 -1
  15. package/dist/adapters/CodexAdapter.js.map +1 -1
  16. package/dist/adapters/PiAdapter.d.ts +62 -0
  17. package/dist/adapters/PiAdapter.d.ts.map +1 -0
  18. package/dist/adapters/PiAdapter.js +450 -0
  19. package/dist/adapters/PiAdapter.js.map +1 -0
  20. package/dist/adapters/index.d.ts +1 -0
  21. package/dist/adapters/index.d.ts.map +1 -1
  22. package/dist/adapters/index.js +1 -0
  23. package/dist/adapters/index.js.map +1 -1
  24. package/dist/index.d.ts +1 -0
  25. package/dist/index.d.ts.map +1 -1
  26. package/dist/index.js +1 -0
  27. package/dist/index.js.map +1 -1
  28. package/dist/terminal/TerminalFocusManager.d.ts +1 -0
  29. package/dist/terminal/TerminalFocusManager.d.ts.map +1 -1
  30. package/dist/terminal/TerminalFocusManager.js +10 -9
  31. package/dist/terminal/TerminalFocusManager.js.map +1 -1
  32. package/dist/utils/agents.d.ts +1 -1
  33. package/dist/utils/agents.d.ts.map +1 -1
  34. package/dist/utils/agents.js +29 -3
  35. package/dist/utils/agents.js.map +1 -1
  36. package/package.json +6 -1
  37. package/src/__tests__/adapters/CodexAdapter.test.ts +64 -13
  38. package/src/__tests__/adapters/PiAdapter.test.ts +435 -0
  39. package/src/__tests__/terminal/TerminalFocusManager.test.ts +92 -0
  40. package/src/__tests__/utils/agents.test.ts +17 -0
  41. package/src/adapters/AgentAdapter.ts +1 -1
  42. package/src/adapters/CodexAdapter.ts +51 -1
  43. package/src/adapters/PiAdapter.ts +597 -0
  44. package/src/adapters/index.ts +1 -0
  45. package/src/index.ts +1 -0
  46. package/src/terminal/TerminalFocusManager.ts +11 -3
  47. package/src/utils/agents.ts +22 -2
@@ -81,11 +81,8 @@ export class TerminalFocusManager {
81
81
  }
82
82
  async findITerm2Session(tty) {
83
83
  try {
84
- // Check if iTerm2 is running first to avoid launching it
85
- await execFileAsync('pgrep', [
86
- '-x',
87
- 'iTerm2'
88
- ]);
84
+ // Check if iTerm2 is running first to avoid launching it.
85
+ if (!await this.isProcessRunning('iTerm2')) return null;
89
86
  } catch {
90
87
  return null;
91
88
  }
@@ -123,10 +120,7 @@ export class TerminalFocusManager {
123
120
  async findTerminalAppWindow(tty) {
124
121
  try {
125
122
  // Check if Terminal.app is running
126
- await execFileAsync('pgrep', [
127
- '-x',
128
- 'Terminal'
129
- ]);
123
+ if (!await this.isProcessRunning('Terminal')) return null;
130
124
  } catch {
131
125
  return null;
132
126
  }
@@ -159,6 +153,13 @@ export class TerminalFocusManager {
159
153
  }
160
154
  return null;
161
155
  }
156
+ async isProcessRunning(name) {
157
+ const { stdout } = await execFileAsync('ps', [
158
+ '-Axo',
159
+ 'comm'
160
+ ]);
161
+ return stdout.split('\n').map((line)=>line.trim()).some((command)=>command === name || command.endsWith(`/${name}`));
162
+ }
162
163
  async focusTmuxPane(identifier) {
163
164
  try {
164
165
  await execFileAsync('tmux', [
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/terminal/TerminalFocusManager.ts"],"sourcesContent":["import { execFile } from 'child_process';\nimport { promisify } from 'util';\nimport { getProcessTty } from '../utils/process.js';\nimport { escapeAppleScript } from '../utils/applescript.js';\n\nconst execFileAsync = promisify(execFile);\n\nexport enum TerminalType {\n TMUX = 'tmux',\n ITERM2 = 'iterm2',\n TERMINAL_APP = 'terminal-app',\n UNKNOWN = 'unknown',\n}\n\nexport interface TerminalLocation {\n type: TerminalType;\n identifier: string; // e.g., \"session:window.pane\" for tmux, or TTY for others\n tty: string; // e.g., \"/dev/ttys030\"\n}\n\nexport class TerminalFocusManager {\n /**\n * Find the terminal location (emulator info) for a given process ID\n */\n async findTerminal(pid: number): Promise<TerminalLocation | null> {\n const ttyShort = getProcessTty(pid);\n\n // If no TTY or invalid, we can't find the terminal\n if (!ttyShort || ttyShort === '?') {\n return null;\n }\n\n const fullTty = `/dev/${ttyShort}`;\n\n // 1. Check tmux (most specific if running inside it)\n const tmuxLocation = await this.findTmuxPane(fullTty);\n if (tmuxLocation) return tmuxLocation;\n\n // 2. Check iTerm2\n const itermLocation = await this.findITerm2Session(fullTty);\n if (itermLocation) return itermLocation;\n\n // 3. Check Terminal.app\n const terminalAppLocation = await this.findTerminalAppWindow(fullTty);\n if (terminalAppLocation) return terminalAppLocation;\n\n // 4. Fallback: we know the TTY but not the emulator wrapper\n return {\n type: TerminalType.UNKNOWN,\n identifier: '',\n tty: fullTty\n };\n }\n\n /**\n * Focus the terminal identified by the location\n */\n async focusTerminal(location: TerminalLocation): Promise<boolean> {\n try {\n switch (location.type) {\n case TerminalType.TMUX:\n return await this.focusTmuxPane(location.identifier);\n case TerminalType.ITERM2:\n return await this.focusITerm2Session(location.tty);\n case TerminalType.TERMINAL_APP:\n return await this.focusTerminalAppWindow(location.tty);\n default:\n return false;\n }\n } catch {\n return false;\n }\n }\n\n private async findTmuxPane(tty: string): Promise<TerminalLocation | null> {\n try {\n const { stdout } = await execFileAsync('tmux', [\n 'list-panes', '-a', '-F', '#{pane_tty}|#{session_name}:#{window_index}.#{pane_index}'\n ]);\n\n const lines = stdout.trim().split('\\n');\n for (const line of lines) {\n if (!line.trim()) continue;\n const [paneTty, identifier] = line.split('|');\n if (paneTty === tty && identifier) {\n return {\n type: TerminalType.TMUX,\n identifier,\n tty\n };\n }\n }\n } catch {\n // tmux might not be installed or running\n }\n return null;\n }\n\n private async findITerm2Session(tty: string): Promise<TerminalLocation | null> {\n try {\n // Check if iTerm2 is running first to avoid launching it\n await execFileAsync('pgrep', ['-x', 'iTerm2']);\n } catch {\n return null;\n }\n\n try {\n const escapedTty = escapeAppleScript(tty);\n const script = `\n tell application \"iTerm\"\n repeat with w in windows\n repeat with t in tabs of w\n repeat with s in sessions of t\n if tty of s is \"${escapedTty}\" then\n return \"found\"\n end if\n end repeat\n end repeat\n end repeat\n end tell\n `;\n\n const { stdout } = await execFileAsync('osascript', ['-e', script]);\n if (stdout.trim() === \"found\") {\n return {\n type: TerminalType.ITERM2,\n identifier: tty,\n tty\n };\n }\n } catch {\n // iTerm2 script failed\n }\n return null;\n }\n\n private async findTerminalAppWindow(tty: string): Promise<TerminalLocation | null> {\n try {\n // Check if Terminal.app is running\n await execFileAsync('pgrep', ['-x', 'Terminal']);\n } catch {\n return null;\n }\n\n try {\n const escapedTty = escapeAppleScript(tty);\n const script = `\n tell application \"Terminal\"\n repeat with w in windows\n repeat with t in tabs of w\n if tty of t is \"${escapedTty}\" then\n return \"found\"\n end if\n end repeat\n end repeat\n end tell\n `;\n\n const { stdout } = await execFileAsync('osascript', ['-e', script]);\n if (stdout.trim() === \"found\") {\n return {\n type: TerminalType.TERMINAL_APP,\n identifier: tty,\n tty\n };\n }\n } catch {\n // Terminal.app script failed\n }\n return null;\n }\n\n private async focusTmuxPane(identifier: string): Promise<boolean> {\n try {\n await execFileAsync('tmux', ['switch-client', '-t', identifier]);\n return true;\n } catch {\n return false;\n }\n }\n\n private async focusITerm2Session(tty: string): Promise<boolean> {\n const escapedTty = escapeAppleScript(tty);\n const script = `\n tell application \"iTerm\"\n activate\n repeat with w in windows\n repeat with t in tabs of w\n repeat with s in sessions of t\n if tty of s is \"${escapedTty}\" then\n select s\n return \"true\"\n end if\n end repeat\n end repeat\n end repeat\n end tell\n `;\n const { stdout } = await execFileAsync('osascript', ['-e', script]);\n return stdout.trim() === \"true\";\n }\n\n private async focusTerminalAppWindow(tty: string): Promise<boolean> {\n const escapedTty = escapeAppleScript(tty);\n const script = `\n tell application \"Terminal\"\n activate\n repeat with w in windows\n repeat with t in tabs of w\n if tty of t is \"${escapedTty}\" then\n set index of w to 1\n set selected tab of w to t\n return \"true\"\n end if\n end repeat\n end repeat\n end tell\n `;\n const { stdout } = await execFileAsync('osascript', ['-e', script]);\n return stdout.trim() === \"true\";\n }\n}\n"],"names":["execFile","promisify","getProcessTty","escapeAppleScript","execFileAsync","TerminalType","TerminalFocusManager","findTerminal","pid","ttyShort","fullTty","tmuxLocation","findTmuxPane","itermLocation","findITerm2Session","terminalAppLocation","findTerminalAppWindow","type","identifier","tty","focusTerminal","location","focusTmuxPane","focusITerm2Session","focusTerminalAppWindow","stdout","lines","trim","split","line","paneTty","escapedTty","script"],"mappings":"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"}
1
+ {"version":3,"sources":["../../src/terminal/TerminalFocusManager.ts"],"sourcesContent":["import { execFile } from 'child_process';\nimport { promisify } from 'util';\nimport { getProcessTty } from '../utils/process.js';\nimport { escapeAppleScript } from '../utils/applescript.js';\n\nconst execFileAsync = promisify(execFile);\n\nexport enum TerminalType {\n TMUX = 'tmux',\n ITERM2 = 'iterm2',\n TERMINAL_APP = 'terminal-app',\n UNKNOWN = 'unknown',\n}\n\nexport interface TerminalLocation {\n type: TerminalType;\n identifier: string; // e.g., \"session:window.pane\" for tmux, or TTY for others\n tty: string; // e.g., \"/dev/ttys030\"\n}\n\nexport class TerminalFocusManager {\n /**\n * Find the terminal location (emulator info) for a given process ID\n */\n async findTerminal(pid: number): Promise<TerminalLocation | null> {\n const ttyShort = getProcessTty(pid);\n\n // If no TTY or invalid, we can't find the terminal\n if (!ttyShort || ttyShort === '?') {\n return null;\n }\n\n const fullTty = `/dev/${ttyShort}`;\n\n // 1. Check tmux (most specific if running inside it)\n const tmuxLocation = await this.findTmuxPane(fullTty);\n if (tmuxLocation) return tmuxLocation;\n\n // 2. Check iTerm2\n const itermLocation = await this.findITerm2Session(fullTty);\n if (itermLocation) return itermLocation;\n\n // 3. Check Terminal.app\n const terminalAppLocation = await this.findTerminalAppWindow(fullTty);\n if (terminalAppLocation) return terminalAppLocation;\n\n // 4. Fallback: we know the TTY but not the emulator wrapper\n return {\n type: TerminalType.UNKNOWN,\n identifier: '',\n tty: fullTty\n };\n }\n\n /**\n * Focus the terminal identified by the location\n */\n async focusTerminal(location: TerminalLocation): Promise<boolean> {\n try {\n switch (location.type) {\n case TerminalType.TMUX:\n return await this.focusTmuxPane(location.identifier);\n case TerminalType.ITERM2:\n return await this.focusITerm2Session(location.tty);\n case TerminalType.TERMINAL_APP:\n return await this.focusTerminalAppWindow(location.tty);\n default:\n return false;\n }\n } catch {\n return false;\n }\n }\n\n private async findTmuxPane(tty: string): Promise<TerminalLocation | null> {\n try {\n const { stdout } = await execFileAsync('tmux', [\n 'list-panes', '-a', '-F', '#{pane_tty}|#{session_name}:#{window_index}.#{pane_index}'\n ]);\n\n const lines = stdout.trim().split('\\n');\n for (const line of lines) {\n if (!line.trim()) continue;\n const [paneTty, identifier] = line.split('|');\n if (paneTty === tty && identifier) {\n return {\n type: TerminalType.TMUX,\n identifier,\n tty\n };\n }\n }\n } catch {\n // tmux might not be installed or running\n }\n return null;\n }\n\n private async findITerm2Session(tty: string): Promise<TerminalLocation | null> {\n try {\n // Check if iTerm2 is running first to avoid launching it.\n if (!await this.isProcessRunning('iTerm2')) return null;\n } catch {\n return null;\n }\n\n try {\n const escapedTty = escapeAppleScript(tty);\n const script = `\n tell application \"iTerm\"\n repeat with w in windows\n repeat with t in tabs of w\n repeat with s in sessions of t\n if tty of s is \"${escapedTty}\" then\n return \"found\"\n end if\n end repeat\n end repeat\n end repeat\n end tell\n `;\n\n const { stdout } = await execFileAsync('osascript', ['-e', script]);\n if (stdout.trim() === \"found\") {\n return {\n type: TerminalType.ITERM2,\n identifier: tty,\n tty\n };\n }\n } catch {\n // iTerm2 script failed\n }\n return null;\n }\n\n private async findTerminalAppWindow(tty: string): Promise<TerminalLocation | null> {\n try {\n // Check if Terminal.app is running\n if (!await this.isProcessRunning('Terminal')) return null;\n } catch {\n return null;\n }\n\n try {\n const escapedTty = escapeAppleScript(tty);\n const script = `\n tell application \"Terminal\"\n repeat with w in windows\n repeat with t in tabs of w\n if tty of t is \"${escapedTty}\" then\n return \"found\"\n end if\n end repeat\n end repeat\n end tell\n `;\n\n const { stdout } = await execFileAsync('osascript', ['-e', script]);\n if (stdout.trim() === \"found\") {\n return {\n type: TerminalType.TERMINAL_APP,\n identifier: tty,\n tty\n };\n }\n } catch {\n // Terminal.app script failed\n }\n return null;\n }\n\n private async isProcessRunning(name: string): Promise<boolean> {\n const { stdout } = await execFileAsync('ps', ['-Axo', 'comm']);\n return stdout\n .split('\\n')\n .map((line) => line.trim())\n .some((command) => command === name || command.endsWith(`/${name}`));\n }\n\n private async focusTmuxPane(identifier: string): Promise<boolean> {\n try {\n await execFileAsync('tmux', ['switch-client', '-t', identifier]);\n return true;\n } catch {\n return false;\n }\n }\n\n private async focusITerm2Session(tty: string): Promise<boolean> {\n const escapedTty = escapeAppleScript(tty);\n const script = `\n tell application \"iTerm\"\n activate\n repeat with w in windows\n repeat with t in tabs of w\n repeat with s in sessions of t\n if tty of s is \"${escapedTty}\" then\n select s\n return \"true\"\n end if\n end repeat\n end repeat\n end repeat\n end tell\n `;\n const { stdout } = await execFileAsync('osascript', ['-e', script]);\n return stdout.trim() === \"true\";\n }\n\n private async focusTerminalAppWindow(tty: string): Promise<boolean> {\n const escapedTty = escapeAppleScript(tty);\n const script = `\n tell application \"Terminal\"\n activate\n repeat with w in windows\n repeat with t in tabs of w\n if tty of t is \"${escapedTty}\" then\n set index of w to 1\n set selected tab of w to t\n return \"true\"\n end if\n end repeat\n end repeat\n end tell\n `;\n const { stdout } = await execFileAsync('osascript', ['-e', script]);\n return stdout.trim() === \"true\";\n }\n}\n"],"names":["execFile","promisify","getProcessTty","escapeAppleScript","execFileAsync","TerminalType","TerminalFocusManager","findTerminal","pid","ttyShort","fullTty","tmuxLocation","findTmuxPane","itermLocation","findITerm2Session","terminalAppLocation","findTerminalAppWindow","type","identifier","tty","focusTerminal","location","focusTmuxPane","focusITerm2Session","focusTerminalAppWindow","stdout","lines","trim","split","line","paneTty","isProcessRunning","escapedTty","script","name","map","some","command","endsWith"],"mappings":"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"}
@@ -1,5 +1,5 @@
1
1
  import type { AgentType } from '../adapters/AgentAdapter.js';
2
- export type StartableAgentType = Extract<AgentType, 'claude' | 'codex' | 'gemini_cli' | 'opencode'>;
2
+ export type StartableAgentType = Extract<AgentType, 'claude' | 'codex' | 'copilot' | 'gemini_cli' | 'opencode' | 'pi'>;
3
3
  export interface AgentConfig {
4
4
  /** Shell command to launch the agent (sent to tmux via `send-keys`). */
5
5
  command: string;
@@ -1 +1 @@
1
- {"version":3,"file":"agents.d.ts","sourceRoot":"","sources":["../../src/utils/agents.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,6BAA6B,CAAC;AAE7D,MAAM,MAAM,kBAAkB,GAAG,OAAO,CAAC,SAAS,EAAE,QAAQ,GAAG,OAAO,GAAG,YAAY,GAAG,UAAU,CAAC,CAAC;AAEpG,MAAM,WAAW,WAAW;IACxB,wEAAwE;IACxE,OAAO,EAAE,MAAM,CAAC;IAChB,yEAAyE;IACzE,OAAO,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,OAAO,CAAC;CAC3C;AAED;;;;GAIG;AACH,eAAO,MAAM,MAAM,EAAE,MAAM,CAAC,kBAAkB,EAAE,WAAW,CAK1D,CAAC"}
1
+ {"version":3,"file":"agents.d.ts","sourceRoot":"","sources":["../../src/utils/agents.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,6BAA6B,CAAC;AAE7D,MAAM,MAAM,kBAAkB,GAAG,OAAO,CAAC,SAAS,EAAE,QAAQ,GAAG,OAAO,GAAG,SAAS,GAAG,YAAY,GAAG,UAAU,GAAG,IAAI,CAAC,CAAC;AAEvH,MAAM,WAAW,WAAW;IACxB,wEAAwE;IACxE,OAAO,EAAE,MAAM,CAAC;IAChB,yEAAyE;IACzE,OAAO,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,OAAO,CAAC;CAC3C;AAED;;;;GAIG;AACH,eAAO,MAAM,MAAM,EAAE,MAAM,CAAC,kBAAkB,EAAE,WAAW,CAO1D,CAAC"}
@@ -12,13 +12,23 @@ import path from 'path';
12
12
  command: 'codex',
13
13
  matches: matchArgv0('codex')
14
14
  },
15
- opencode: {
16
- command: 'opencode',
17
- matches: matchArgv0('opencode')
15
+ copilot: {
16
+ command: 'copilot',
17
+ matches: matchArgv0Name('copilot-cli')
18
18
  },
19
19
  gemini_cli: {
20
20
  command: 'gemini',
21
21
  matches: matchAnyToken('gemini')
22
+ },
23
+ opencode: {
24
+ command: 'opencode',
25
+ matches: matchArgv0('opencode')
26
+ },
27
+ pi: {
28
+ command: 'pi',
29
+ matches: matchAnyBasename([
30
+ 'pi'
31
+ ])
22
32
  }
23
33
  };
24
34
  function matchArgv0(name) {
@@ -28,6 +38,13 @@ function matchArgv0(name) {
28
38
  return token ? path.basename(token).toLowerCase() === lower : false;
29
39
  };
30
40
  }
41
+ function matchArgv0Name(name) {
42
+ const lower = name.toLowerCase();
43
+ return (psCommand)=>{
44
+ const token = psCommand.trim().split(/\s+/)[0];
45
+ return token ? token.toLowerCase().includes(lower) : false;
46
+ };
47
+ }
31
48
  function matchAnyToken(name) {
32
49
  const lower = name.toLowerCase();
33
50
  return (psCommand)=>{
@@ -37,5 +54,14 @@ function matchAnyToken(name) {
37
54
  return false;
38
55
  };
39
56
  }
57
+ function matchAnyBasename(names) {
58
+ const lowers = new Set(names.map((name)=>name.toLowerCase()));
59
+ return (psCommand)=>{
60
+ for (const token of psCommand.trim().split(/\s+/)){
61
+ if (lowers.has(path.basename(token).toLowerCase())) return true;
62
+ }
63
+ return false;
64
+ };
65
+ }
40
66
 
41
67
  //# sourceMappingURL=agents.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/utils/agents.ts"],"sourcesContent":["import path from 'path';\nimport type { AgentType } from '../adapters/AgentAdapter.js';\n\nexport type StartableAgentType = Extract<AgentType, 'claude' | 'codex' | 'gemini_cli' | 'opencode'>;\n\nexport interface AgentConfig {\n /** Shell command to launch the agent (sent to tmux via `send-keys`). */\n command: string;\n /** Returns true if the given `ps` command line belongs to this agent. */\n matches: (psCommand: string) => boolean;\n}\n\n/**\n * Per-agent configuration: launch command plus a matcher that recognizes the\n * agent's process in `ps` output. Each matcher knows that agent's distribution\n * quirks (e.g. gemini ships as a Node script so its real binary is in argv[1..]).\n */\nexport const AGENTS: Record<StartableAgentType, AgentConfig> = {\n claude: { command: 'claude', matches: matchArgv0('claude') },\n codex: { command: 'codex', matches: matchArgv0('codex') },\n opencode: { command: 'opencode', matches: matchArgv0('opencode') },\n gemini_cli: { command: 'gemini', matches: matchAnyToken('gemini') },\n};\n\nfunction matchArgv0(name: string): (psCommand: string) => boolean {\n const lower = name.toLowerCase();\n return (psCommand) => {\n const token = psCommand.trim().split(/\\s+/)[0];\n return token ? path.basename(token).toLowerCase() === lower : false;\n };\n}\n\nfunction matchAnyToken(name: string): (psCommand: string) => boolean {\n const lower = name.toLowerCase();\n return (psCommand) => {\n for (const token of psCommand.trim().split(/\\s+/)) {\n if (path.basename(token).toLowerCase() === lower) return true;\n }\n return false;\n };\n}\n"],"names":["path","AGENTS","claude","command","matches","matchArgv0","codex","opencode","gemini_cli","matchAnyToken","name","lower","toLowerCase","psCommand","token","trim","split","basename"],"mappings":"AAAA,OAAOA,UAAU,OAAO;AAYxB;;;;CAIC,GACD,OAAO,MAAMC,SAAkD;IAC3DC,QAAY;QAAEC,SAAS;QAAYC,SAASC,WAAW;IAAU;IACjEC,OAAY;QAAEH,SAAS;QAAYC,SAASC,WAAW;IAAS;IAChEE,UAAY;QAAEJ,SAAS;QAAYC,SAASC,WAAW;IAAY;IACnEG,YAAY;QAAEL,SAAS;QAAYC,SAASK,cAAc;IAAU;AACxE,EAAE;AAEF,SAASJ,WAAWK,IAAY;IAC5B,MAAMC,QAAQD,KAAKE,WAAW;IAC9B,OAAO,CAACC;QACJ,MAAMC,QAAQD,UAAUE,IAAI,GAAGC,KAAK,CAAC,MAAM,CAAC,EAAE;QAC9C,OAAOF,QAAQd,KAAKiB,QAAQ,CAACH,OAAOF,WAAW,OAAOD,QAAQ;IAClE;AACJ;AAEA,SAASF,cAAcC,IAAY;IAC/B,MAAMC,QAAQD,KAAKE,WAAW;IAC9B,OAAO,CAACC;QACJ,KAAK,MAAMC,SAASD,UAAUE,IAAI,GAAGC,KAAK,CAAC,OAAQ;YAC/C,IAAIhB,KAAKiB,QAAQ,CAACH,OAAOF,WAAW,OAAOD,OAAO,OAAO;QAC7D;QACA,OAAO;IACX;AACJ"}
1
+ {"version":3,"sources":["../../src/utils/agents.ts"],"sourcesContent":["import path from 'path';\nimport type { AgentType } from '../adapters/AgentAdapter.js';\n\nexport type StartableAgentType = Extract<AgentType, 'claude' | 'codex' | 'copilot' | 'gemini_cli' | 'opencode' | 'pi'>;\n\nexport interface AgentConfig {\n /** Shell command to launch the agent (sent to tmux via `send-keys`). */\n command: string;\n /** Returns true if the given `ps` command line belongs to this agent. */\n matches: (psCommand: string) => boolean;\n}\n\n/**\n * Per-agent configuration: launch command plus a matcher that recognizes the\n * agent's process in `ps` output. Each matcher knows that agent's distribution\n * quirks (e.g. gemini ships as a Node script so its real binary is in argv[1..]).\n */\nexport const AGENTS: Record<StartableAgentType, AgentConfig> = {\n claude: { command: 'claude', matches: matchArgv0('claude') },\n codex: { command: 'codex', matches: matchArgv0('codex') },\n copilot: { command: 'copilot', matches: matchArgv0Name('copilot-cli') },\n gemini_cli: { command: 'gemini', matches: matchAnyToken('gemini') },\n opencode: { command: 'opencode', matches: matchArgv0('opencode') },\n pi: { command: 'pi', matches: matchAnyBasename(['pi']) },\n};\n\nfunction matchArgv0(name: string): (psCommand: string) => boolean {\n const lower = name.toLowerCase();\n return (psCommand) => {\n const token = psCommand.trim().split(/\\s+/)[0];\n return token ? path.basename(token).toLowerCase() === lower : false;\n };\n}\n\nfunction matchArgv0Name(name: string): (psCommand: string) => boolean {\n const lower = name.toLowerCase();\n return (psCommand) => {\n const token = psCommand.trim().split(/\\s+/)[0];\n return token ? token.toLowerCase().includes(lower) : false;\n };\n}\n\nfunction matchAnyToken(name: string): (psCommand: string) => boolean {\n const lower = name.toLowerCase();\n return (psCommand) => {\n for (const token of psCommand.trim().split(/\\s+/)) {\n if (path.basename(token).toLowerCase() === lower) return true;\n }\n return false;\n };\n}\n\nfunction matchAnyBasename(names: string[]): (psCommand: string) => boolean {\n const lowers = new Set(names.map((name) => name.toLowerCase()));\n return (psCommand) => {\n for (const token of psCommand.trim().split(/\\s+/)) {\n if (lowers.has(path.basename(token).toLowerCase())) return true;\n }\n return false;\n };\n}\n"],"names":["path","AGENTS","claude","command","matches","matchArgv0","codex","copilot","matchArgv0Name","gemini_cli","matchAnyToken","opencode","pi","matchAnyBasename","name","lower","toLowerCase","psCommand","token","trim","split","basename","includes","names","lowers","Set","map","has"],"mappings":"AAAA,OAAOA,UAAU,OAAO;AAYxB;;;;CAIC,GACD,OAAO,MAAMC,SAAkD;IAC3DC,QAAY;QAAEC,SAAS;QAAYC,SAASC,WAAW;IAAU;IACjEC,OAAY;QAAEH,SAAS;QAAYC,SAASC,WAAW;IAAS;IAChEE,SAAY;QAAEJ,SAAS;QAAYC,SAASI,eAAe;IAAe;IAC1EC,YAAY;QAAEN,SAAS;QAAYC,SAASM,cAAc;IAAU;IACpEC,UAAY;QAAER,SAAS;QAAYC,SAASC,WAAW;IAAY;IACnEO,IAAY;QAAET,SAAS;QAAYC,SAASS,iBAAiB;YAAC;SAAK;IAAE;AACzE,EAAE;AAEF,SAASR,WAAWS,IAAY;IAC5B,MAAMC,QAAQD,KAAKE,WAAW;IAC9B,OAAO,CAACC;QACJ,MAAMC,QAAQD,UAAUE,IAAI,GAAGC,KAAK,CAAC,MAAM,CAAC,EAAE;QAC9C,OAAOF,QAAQlB,KAAKqB,QAAQ,CAACH,OAAOF,WAAW,OAAOD,QAAQ;IAClE;AACJ;AAEA,SAASP,eAAeM,IAAY;IAChC,MAAMC,QAAQD,KAAKE,WAAW;IAC9B,OAAO,CAACC;QACJ,MAAMC,QAAQD,UAAUE,IAAI,GAAGC,KAAK,CAAC,MAAM,CAAC,EAAE;QAC9C,OAAOF,QAAQA,MAAMF,WAAW,GAAGM,QAAQ,CAACP,SAAS;IACzD;AACJ;AAEA,SAASL,cAAcI,IAAY;IAC/B,MAAMC,QAAQD,KAAKE,WAAW;IAC9B,OAAO,CAACC;QACJ,KAAK,MAAMC,SAASD,UAAUE,IAAI,GAAGC,KAAK,CAAC,OAAQ;YAC/C,IAAIpB,KAAKqB,QAAQ,CAACH,OAAOF,WAAW,OAAOD,OAAO,OAAO;QAC7D;QACA,OAAO;IACX;AACJ;AAEA,SAASF,iBAAiBU,KAAe;IACrC,MAAMC,SAAS,IAAIC,IAAIF,MAAMG,GAAG,CAAC,CAACZ,OAASA,KAAKE,WAAW;IAC3D,OAAO,CAACC;QACJ,KAAK,MAAMC,SAASD,UAAUE,IAAI,GAAGC,KAAK,CAAC,OAAQ;YAC/C,IAAII,OAAOG,GAAG,CAAC3B,KAAKqB,QAAQ,CAACH,OAAOF,WAAW,KAAK,OAAO;QAC/D;QACA,OAAO;IACX;AACJ"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ai-devkit/agent-manager",
3
- "version": "0.17.0",
3
+ "version": "0.19.0",
4
4
  "type": "module",
5
5
  "description": "Standalone agent detection and management utilities for AI DevKit",
6
6
  "main": "dist/index.js",
@@ -29,6 +29,11 @@
29
29
  ],
30
30
  "author": "",
31
31
  "license": "MIT",
32
+ "repository": {
33
+ "type": "git",
34
+ "url": "git+https://github.com/codeaholicguy/ai-devkit.git",
35
+ "directory": "packages/agent-manager"
36
+ },
32
37
  "dependencies": {
33
38
  "better-sqlite3": "^12.6.2",
34
39
  "uuid": "14.0.0"
@@ -277,22 +277,73 @@ describe('CodexAdapter', () => {
277
277
 
278
278
  (adapter as any).codexSessionsDir = sessionsDir;
279
279
  mockedBatchGetSessionFileBirthtimes.mockReturnValue([]);
280
+ const collectAllSpy = vi.spyOn(adapter as any, 'collectAllSessionFiles');
280
281
 
281
- const agents = await adapter.detectAgents();
282
+ try {
283
+ const agents = await adapter.detectAgents();
282
284
 
283
- expect(mockedBatchGetSessionFileBirthtimes).not.toHaveBeenCalled();
284
- expect(mockedMatchProcessesToSessions).not.toHaveBeenCalled();
285
- expect(agents).toHaveLength(1);
286
- expect(agents[0]).toMatchObject({
287
- type: 'codex',
288
- pid: 88018,
289
- sessionId,
290
- projectPath: '/repo-a',
291
- sessionFilePath: sessionFile,
292
- });
293
- expect(agents[0].summary).toBe('resumed codex conversation');
285
+ expect(mockedBatchGetSessionFileBirthtimes).not.toHaveBeenCalled();
286
+ expect(mockedMatchProcessesToSessions).not.toHaveBeenCalled();
287
+ expect(collectAllSpy).not.toHaveBeenCalled();
288
+ expect(agents).toHaveLength(1);
289
+ expect(agents[0]).toMatchObject({
290
+ type: 'codex',
291
+ pid: 88018,
292
+ sessionId,
293
+ projectPath: '/repo-a',
294
+ sessionFilePath: sessionFile,
295
+ });
296
+ expect(agents[0].summary).toBe('resumed codex conversation');
297
+ } finally {
298
+ collectAllSpy.mockRestore();
299
+ fs.rmSync(tmpDir, { recursive: true, force: true });
300
+ }
301
+ });
294
302
 
295
- fs.rmSync(tmpDir, { recursive: true, force: true });
303
+ it('should fall back to all session files for non-time-sortable resume ids', async () => {
304
+ const sessionId = 'aaaaaaaa-bbbb-4ccc-dddd-eeeeeeeeeeee';
305
+ const processes: ProcessInfo[] = [
306
+ {
307
+ pid: 88020,
308
+ command: `codex resume ${sessionId}`,
309
+ cwd: '/repo-a',
310
+ tty: 'ttys001',
311
+ startTime: new Date('2026-06-10T12:00:00.000Z'),
312
+ },
313
+ ];
314
+ mockedListAgentProcesses.mockReturnValue(processes);
315
+ mockedEnrichProcesses.mockReturnValue(processes);
316
+
317
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'codex-resume-v4-'));
318
+ const sessionsDir = path.join(tmpDir, 'sessions');
319
+ const dateDir = path.join(sessionsDir, '2026', '01', '02');
320
+ fs.mkdirSync(dateDir, { recursive: true });
321
+
322
+ const recentTs = new Date().toISOString();
323
+ const sessionFile = path.join(dateDir, `${sessionId}.jsonl`);
324
+ fs.writeFileSync(sessionFile, [
325
+ JSON.stringify({ type: 'session_meta', payload: { id: sessionId, timestamp: recentTs, cwd: '/repo-a' } }),
326
+ JSON.stringify({ type: 'event', timestamp: recentTs, payload: { type: 'agent_message', message: 'legacy id conversation' } }),
327
+ ].join('\n'));
328
+
329
+ (adapter as any).codexSessionsDir = sessionsDir;
330
+ mockedBatchGetSessionFileBirthtimes.mockReturnValue([]);
331
+ const collectAllSpy = vi.spyOn(adapter as any, 'collectAllSessionFiles');
332
+
333
+ try {
334
+ const agents = await adapter.detectAgents();
335
+
336
+ expect(collectAllSpy).toHaveBeenCalledOnce();
337
+ expect(agents).toHaveLength(1);
338
+ expect(agents[0]).toMatchObject({
339
+ pid: 88020,
340
+ sessionId,
341
+ sessionFilePath: sessionFile,
342
+ });
343
+ } finally {
344
+ collectAllSpy.mockRestore();
345
+ fs.rmSync(tmpDir, { recursive: true, force: true });
346
+ }
296
347
  });
297
348
 
298
349
  it('should fall back to process-only when a resumed session becomes unreadable after direct matching', async () => {