@ai-devkit/agent-manager 0.24.0 → 0.26.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 (84) hide show
  1. package/README.md +14 -0
  2. package/dist/__tests__/adapters/GrokCliAdapter.test.js +403 -0
  3. package/dist/__tests__/adapters/GrokCliAdapter.test.js.map +1 -0
  4. package/dist/__tests__/print/ClaudeCliProbe.test.js +53 -0
  5. package/dist/__tests__/print/ClaudeCliProbe.test.js.map +1 -0
  6. package/dist/__tests__/print/ClaudePrintAgent.integration.test.js +69 -0
  7. package/dist/__tests__/print/ClaudePrintAgent.integration.test.js.map +1 -0
  8. package/dist/__tests__/print/ClaudePrintAgentService.test.js +108 -0
  9. package/dist/__tests__/print/ClaudePrintAgentService.test.js.map +1 -0
  10. package/dist/__tests__/print/ClaudePrintRunner.test.js +187 -0
  11. package/dist/__tests__/print/ClaudePrintRunner.test.js.map +1 -0
  12. package/dist/__tests__/print/PrintAgent.test.js +17 -0
  13. package/dist/__tests__/print/PrintAgent.test.js.map +1 -0
  14. package/dist/__tests__/print/PrintAgentStore.test.js +307 -0
  15. package/dist/__tests__/print/PrintAgentStore.test.js.map +1 -0
  16. package/dist/__tests__/terminal/TmuxManager.test.js +9 -0
  17. package/dist/__tests__/terminal/TmuxManager.test.js.map +1 -1
  18. package/dist/__tests__/utils/agents.test.js +6 -0
  19. package/dist/__tests__/utils/agents.test.js.map +1 -1
  20. package/dist/adapters/AgentAdapter.d.ts +1 -1
  21. package/dist/adapters/AgentAdapter.d.ts.map +1 -1
  22. package/dist/adapters/AgentAdapter.js.map +1 -1
  23. package/dist/adapters/GrokCliAdapter.d.ts +79 -0
  24. package/dist/adapters/GrokCliAdapter.d.ts.map +1 -0
  25. package/dist/adapters/GrokCliAdapter.js +306 -0
  26. package/dist/adapters/GrokCliAdapter.js.map +1 -0
  27. package/dist/adapters/index.d.ts +1 -0
  28. package/dist/adapters/index.d.ts.map +1 -1
  29. package/dist/adapters/index.js +1 -0
  30. package/dist/adapters/index.js.map +1 -1
  31. package/dist/index.d.ts +12 -0
  32. package/dist/index.d.ts.map +1 -1
  33. package/dist/index.js +7 -0
  34. package/dist/index.js.map +1 -1
  35. package/dist/print/ClaudeCliProbe.d.ts +20 -0
  36. package/dist/print/ClaudeCliProbe.d.ts.map +1 -0
  37. package/dist/print/ClaudeCliProbe.js +57 -0
  38. package/dist/print/ClaudeCliProbe.js.map +1 -0
  39. package/dist/print/ClaudePrintAgentService.d.ts +44 -0
  40. package/dist/print/ClaudePrintAgentService.d.ts.map +1 -0
  41. package/dist/print/ClaudePrintAgentService.js +66 -0
  42. package/dist/print/ClaudePrintAgentService.js.map +1 -0
  43. package/dist/print/ClaudePrintRunner.d.ts +32 -0
  44. package/dist/print/ClaudePrintRunner.d.ts.map +1 -0
  45. package/dist/print/ClaudePrintRunner.js +128 -0
  46. package/dist/print/ClaudePrintRunner.js.map +1 -0
  47. package/dist/print/PrintAgent.d.ts +57 -0
  48. package/dist/print/PrintAgent.d.ts.map +1 -0
  49. package/dist/print/PrintAgent.js +42 -0
  50. package/dist/print/PrintAgent.js.map +1 -0
  51. package/dist/print/PrintAgentStore.d.ts +69 -0
  52. package/dist/print/PrintAgentStore.d.ts.map +1 -0
  53. package/dist/print/PrintAgentStore.js +484 -0
  54. package/dist/print/PrintAgentStore.js.map +1 -0
  55. package/dist/terminal/TmuxManager.d.ts +2 -2
  56. package/dist/terminal/TmuxManager.d.ts.map +1 -1
  57. package/dist/terminal/TmuxManager.js +5 -7
  58. package/dist/terminal/TmuxManager.js.map +1 -1
  59. package/dist/utils/agents.d.ts +1 -1
  60. package/dist/utils/agents.d.ts.map +1 -1
  61. package/dist/utils/agents.js +4 -0
  62. package/dist/utils/agents.js.map +1 -1
  63. package/package.json +2 -2
  64. package/src/__tests__/adapters/GrokCliAdapter.test.ts +307 -0
  65. package/src/__tests__/fixtures/fake-claude.cjs +24 -0
  66. package/src/__tests__/print/ClaudeCliProbe.test.ts +32 -0
  67. package/src/__tests__/print/ClaudePrintAgent.integration.test.ts +56 -0
  68. package/src/__tests__/print/ClaudePrintAgentService.test.ts +46 -0
  69. package/src/__tests__/print/ClaudePrintRunner.test.ts +105 -0
  70. package/src/__tests__/print/PrintAgent.test.ts +21 -0
  71. package/src/__tests__/print/PrintAgentStore.test.ts +192 -0
  72. package/src/__tests__/terminal/TmuxManager.test.ts +10 -0
  73. package/src/__tests__/utils/agents.test.ts +7 -0
  74. package/src/adapters/AgentAdapter.ts +1 -1
  75. package/src/adapters/GrokCliAdapter.ts +394 -0
  76. package/src/adapters/index.ts +1 -0
  77. package/src/index.ts +40 -0
  78. package/src/print/ClaudeCliProbe.ts +58 -0
  79. package/src/print/ClaudePrintAgentService.ts +94 -0
  80. package/src/print/ClaudePrintRunner.ts +139 -0
  81. package/src/print/PrintAgent.ts +86 -0
  82. package/src/print/PrintAgentStore.ts +503 -0
  83. package/src/terminal/TmuxManager.ts +5 -7
  84. package/src/utils/agents.ts +2 -1
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/__tests__/print/PrintAgentStore.test.ts"],"sourcesContent":["import fs from 'fs';\nimport os from 'os';\nimport path from 'path';\nimport { afterEach, describe, expect, it } from 'vitest';\n\nconst tempDirs: string[] = [];\n\nafterEach(() => {\n for (const dir of tempDirs.splice(0)) fs.rmSync(dir, { recursive: true, force: true });\n});\n\nasync function loadStore(): Promise<any> {\n const api = await import('../../index.js') as Record<string, unknown>;\n expect(api).toHaveProperty('PrintAgentStore');\n return api.PrintAgentStore;\n}\n\nfunction fixture(): { root: string; cwd: string; filePath: string } {\n const root = fs.mkdtempSync(path.join(os.tmpdir(), 'print-agent-store-'));\n tempDirs.push(root);\n const cwd = path.join(root, 'project');\n fs.mkdirSync(cwd);\n return { root, cwd, filePath: path.join(root, 'state', 'print-agents.json') };\n}\n\ndescribe('PrintAgentStore create/list/resolve', () => {\n it('creates distinct durable identities with a canonical cwd and lists them', async () => {\n const PrintAgentStore = await loadStore();\n const { cwd, filePath } = fixture();\n const store = new PrintAgentStore({ filePath, now: () => new Date('2026-08-07T09:00:00Z') });\n\n const agent = await store.create({ name: 'reviewer', cwd });\n\n expect(agent).toMatchObject({\n name: 'reviewer',\n provider: 'claude',\n mode: 'print',\n cwd: fs.realpathSync(cwd),\n state: 'ready',\n sessionHealth: 'uninitialized',\n activeRun: null,\n });\n expect(agent.id).toMatch(/^[0-9a-f-]{36}$/);\n expect(agent.providerSessionId).toMatch(/^[0-9a-f-]{36}$/);\n expect(agent.id).not.toBe(agent.providerSessionId);\n expect(await store.list()).toEqual([agent]);\n expect(fs.statSync(filePath).mode & 0o777).toBe(0o600);\n });\n\n it('resolves exact ids and names and rejects duplicate names', async () => {\n const PrintAgentStore = await loadStore();\n const { cwd, filePath } = fixture();\n const store = new PrintAgentStore({ filePath });\n const agent = await store.create({ name: 'Reviewer', cwd });\n\n expect(await store.resolve(agent.id)).toMatchObject({ id: agent.id });\n expect(await store.resolve('reviewer')).toMatchObject({ id: agent.id });\n expect(await store.resolve('view')).toBeNull();\n await expect(store.create({ name: 'reviewer', cwd })).rejects.toMatchObject({\n code: 'PRINT_AGENT_NAME_CONFLICT',\n });\n });\n\n it('rejects missing cwd, malformed storage, and symlinked store targets', async () => {\n const PrintAgentStore = await loadStore();\n const { root, cwd, filePath } = fixture();\n const store = new PrintAgentStore({ filePath });\n\n await expect(store.create({ name: 'missing', cwd: path.join(root, 'missing') }))\n .rejects.toMatchObject({ code: 'PRINT_AGENT_STORE' });\n\n fs.mkdirSync(path.dirname(filePath), { recursive: true });\n fs.writeFileSync(filePath, '{bad json', { mode: 0o600 });\n await expect(store.list()).rejects.toMatchObject({ code: 'PRINT_AGENT_STORE' });\n\n fs.rmSync(filePath);\n const target = path.join(root, 'target.json');\n fs.writeFileSync(target, JSON.stringify({ version: 1, agents: [] }));\n fs.symlinkSync(target, filePath);\n await expect(store.create({ name: 'unsafe', cwd })).rejects.toMatchObject({\n code: 'PRINT_AGENT_STORE',\n });\n });\n\n it('recovers an abandoned old mutation lock after a crash', async () => {\n const PrintAgentStore = await loadStore();\n const { cwd, filePath } = fixture();\n fs.mkdirSync(path.dirname(filePath), { recursive: true });\n const lockPath = `${filePath}.lock`;\n fs.mkdirSync(lockPath);\n const old = new Date(Date.now() - 60_000);\n fs.utimesSync(lockPath, old, old);\n const store = new PrintAgentStore({ filePath, mutationLockStaleMs: 10 });\n\n await expect(store.create({ name: 'recovered', cwd })).resolves.toMatchObject({ name: 'recovered' });\n });\n});\n\ndescribe('PrintAgentStore run ownership', () => {\n it('fails fast when another exact owner is live and completes only for its token', async () => {\n const PrintAgentStore = await loadStore();\n const { cwd, filePath } = fixture();\n const live = new Map<number, string>([[process.pid, 'owner-start']]);\n const processInspector = { getIdentity: (pid: number) => {\n const startedAt = live.get(pid);\n return startedAt ? { pid, startedAt } : null;\n } };\n const store = new PrintAgentStore({ filePath, processInspector });\n const agent = await store.create({ name: 'runner', cwd });\n\n const acquired = await store.acquireRun(agent.id);\n await expect(store.acquireRun(agent.id)).rejects.toMatchObject({ code: 'PRINT_AGENT_BUSY' });\n await expect(store.completeRun(agent.id, 'wrong-token', {\n status: 'succeeded', exitCode: 0, summary: 'done', sessionHealth: 'healthy',\n })).rejects.toMatchObject({ code: 'PRINT_AGENT_STORE' });\n\n const completed = await store.completeRun(agent.id, acquired.token, {\n status: 'succeeded', exitCode: 0, summary: 'done', sessionHealth: 'healthy',\n });\n expect(completed).toMatchObject({ state: 'ready', sessionHealth: 'healthy', activeRun: null });\n });\n\n it('retains busy for a live provider then recovers a dead run without signaling it', async () => {\n const PrintAgentStore = await loadStore();\n const { cwd, filePath } = fixture();\n const live = new Map<number, string>([[process.pid, 'owner-start'], [4242, 'provider-start']]);\n const processInspector = { getIdentity: (pid: number) => {\n const startedAt = live.get(pid);\n return startedAt ? { pid, startedAt } : null;\n } };\n const first = new PrintAgentStore({ filePath, processInspector });\n const agent = await first.create({ name: 'recoverable', cwd });\n const run = await first.acquireRun(agent.id);\n await first.recordProviderProcess(agent.id, run.token, { pid: 4242, startedAt: 'provider-start' });\n\n live.delete(process.pid);\n await expect(first.acquireRun(agent.id)).rejects.toMatchObject({ code: 'PRINT_AGENT_BUSY' });\n\n live.delete(4242);\n live.set(process.pid, 'replacement-owner-start');\n const recovered = await first.acquireRun(agent.id);\n expect(recovered.agent).toMatchObject({\n state: 'running',\n lastResult: { status: 'interrupted' },\n });\n await first.completeRun(agent.id, recovered.token, {\n status: 'failed', exitCode: 1, summary: 'failed', sessionHealth: 'unknown',\n });\n });\n\n it('reconciles an old incomplete lock to degraded during list', async () => {\n const PrintAgentStore = await loadStore();\n const { root, cwd, filePath } = fixture();\n const live = new Map<number, string>([[process.pid, 'owner-start']]);\n const store = new PrintAgentStore({ filePath, incompleteLockGraceMs: 10, processInspector: {\n getIdentity: (pid: number) => {\n const startedAt = live.get(pid);\n return startedAt ? { pid, startedAt } : null;\n },\n } });\n const agent = await store.create({ name: 'crashed', cwd });\n await store.acquireRun(agent.id);\n const lockPath = path.join(root, 'state', 'print-agent-locks', `${agent.id}.lock`);\n fs.unlinkSync(path.join(lockPath, 'owner.json'));\n const old = new Date(Date.now() - 1000);\n fs.utimesSync(lockPath, old, old);\n live.clear();\n\n const listed = await store.list();\n\n expect(listed[0]).toMatchObject({\n state: 'degraded',\n sessionHealth: 'unknown',\n activeRun: null,\n lastResult: { status: 'interrupted' },\n });\n });\n\n it('rejects send acquisition when the bound cwd is replaced by a symlink', async () => {\n const PrintAgentStore = await loadStore();\n const { root, cwd, filePath } = fixture();\n const store = new PrintAgentStore({ filePath });\n const agent = await store.create({ name: 'bound', cwd });\n const moved = path.join(root, 'moved-project');\n const other = path.join(root, 'other-project');\n fs.renameSync(cwd, moved);\n fs.mkdirSync(other);\n fs.symlinkSync(other, cwd);\n\n await expect(store.acquireRun(agent.id)).rejects.toMatchObject({ code: 'PRINT_AGENT_STORE' });\n });\n});\n"],"names":["fs","os","path","afterEach","describe","expect","it","tempDirs","dir","splice","rmSync","recursive","force","loadStore","api","toHaveProperty","PrintAgentStore","fixture","root","mkdtempSync","join","tmpdir","push","cwd","mkdirSync","filePath","store","now","Date","agent","create","name","toMatchObject","provider","mode","realpathSync","state","sessionHealth","activeRun","id","toMatch","providerSessionId","not","toBe","list","toEqual","statSync","resolve","toBeNull","rejects","code","dirname","writeFileSync","target","JSON","stringify","version","agents","symlinkSync","lockPath","old","utimesSync","mutationLockStaleMs","resolves","live","Map","process","pid","processInspector","getIdentity","startedAt","get","acquired","acquireRun","completeRun","status","exitCode","summary","completed","token","first","run","recordProviderProcess","delete","set","recovered","lastResult","incompleteLockGraceMs","unlinkSync","clear","listed","moved","other","renameSync"],"mappings":"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"}
@@ -93,6 +93,15 @@ describe('TmuxManager', ()=>{
93
93
  });
94
94
  expect(await tmux.findAgentPid('foo', matchesClaude)).toBeNull();
95
95
  });
96
+ it('returns the matching pane PID when the agent replaces the shell', async ()=>{
97
+ setExecFileHandler((cmd, args)=>{
98
+ if (cmd === 'tmux' && args[0] === 'list-panes') return '100\n';
99
+ if (cmd === 'pgrep') return new Error('no children');
100
+ if (cmd === 'ps' && args[1] === '100') return '/usr/local/bin/claude';
101
+ return '';
102
+ });
103
+ expect(await tmux.findAgentPid('foo', matchesClaude)).toBe(100);
104
+ });
96
105
  it('returns the matching descendant when found', async ()=>{
97
106
  // pane 100 → child 200 (claude) — no grandchildren
98
107
  setExecFileHandler((cmd, args)=>{
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/__tests__/terminal/TmuxManager.test.ts"],"sourcesContent":["import { execFile } from 'child_process';\nimport type { MockedFunction } from 'vitest';\nimport { TmuxManager } from '../../terminal/TmuxManager.js';\n\nvi.mock('child_process', () => ({\n execFile: vi.fn(),\n}));\n\ntype ExecFileCb = (err: Error | null, result?: { stdout: string; stderr: string }) => void;\nconst mockedExecFile = execFile as unknown as MockedFunction<\n (cmd: string, args: string[], cb: ExecFileCb) => void\n>;\n\n/** Drive the promisified execFile mock with a per-call handler. */\nfunction setExecFileHandler(handler: (cmd: string, args: string[]) => string | Error) {\n mockedExecFile.mockImplementation((_cmd, _args, cb) => {\n const result = handler(_cmd, _args);\n if (result instanceof Error) cb(result);\n else cb(null, { stdout: result, stderr: '' });\n });\n}\n\ndescribe('TmuxManager', () => {\n let tmux: TmuxManager;\n\n beforeEach(() => {\n tmux = new TmuxManager();\n mockedExecFile.mockReset();\n });\n\n describe('isAvailable', () => {\n it('returns true when `tmux -V` succeeds', async () => {\n setExecFileHandler(() => 'tmux 3.4');\n expect(await tmux.isAvailable()).toBe(true);\n });\n\n it('returns false when tmux is missing', async () => {\n setExecFileHandler(() => new Error('ENOENT'));\n expect(await tmux.isAvailable()).toBe(false);\n });\n });\n\n describe('sessionExists', () => {\n it('returns true when has-session succeeds', async () => {\n setExecFileHandler(() => '');\n expect(await tmux.sessionExists('foo')).toBe(true);\n });\n\n it('returns false when has-session fails', async () => {\n setExecFileHandler(() => new Error('no session'));\n expect(await tmux.sessionExists('foo')).toBe(false);\n });\n });\n\n describe('createSession', () => {\n it('issues `tmux new-session -d -s <name> -c <cwd>`', async () => {\n setExecFileHandler(() => '');\n await tmux.createSession('foo', '/work');\n expect(mockedExecFile).toHaveBeenCalledWith(\n 'tmux',\n ['new-session', '-d', '-s', 'foo', '-c', '/work'],\n expect.any(Function),\n );\n });\n });\n\n describe('sendKeys', () => {\n it('appends Enter so the command runs', async () => {\n setExecFileHandler(() => '');\n await tmux.sendKeys('foo', 'claude');\n expect(mockedExecFile).toHaveBeenCalledWith(\n 'tmux',\n ['send-keys', '-t', 'foo', 'claude', 'Enter'],\n expect.any(Function),\n );\n });\n });\n\n describe('killSession', () => {\n it('swallows errors when session is already gone', async () => {\n setExecFileHandler(() => new Error(\"can't find session\"));\n await expect(tmux.killSession('foo')).resolves.toBeUndefined();\n });\n });\n\n describe('findAgentPid', () => {\n const matchesClaude = (cmd: string) => cmd.split(/\\s+/)[0]?.endsWith('claude') ?? false;\n\n it('returns null when the session has no pane', async () => {\n setExecFileHandler((cmd, args) => {\n if (args[0] === 'list-panes') return new Error('no session');\n return '';\n });\n expect(await tmux.findAgentPid('foo', matchesClaude)).toBeNull();\n });\n\n it('returns null when no descendant matches', async () => {\n // pane PID 100; child 200 is \"node /unrelated\" — no match\n setExecFileHandler((cmd, args) => {\n if (cmd === 'tmux' && args[0] === 'list-panes') return '100\\n';\n if (cmd === 'pgrep' && args[1] === '100') return '200\\n';\n if (cmd === 'pgrep') return new Error('no children');\n if (cmd === 'ps' && args[1] === '200') return 'node /unrelated';\n return '';\n });\n expect(await tmux.findAgentPid('foo', matchesClaude)).toBeNull();\n });\n\n it('returns the matching descendant when found', async () => {\n // pane 100 → child 200 (claude) — no grandchildren\n setExecFileHandler((cmd, args) => {\n if (cmd === 'tmux') return '100\\n';\n if (cmd === 'pgrep' && args[1] === '100') return '200\\n';\n if (cmd === 'pgrep') return new Error('no children');\n if (cmd === 'ps' && args[1] === '200') return 'claude';\n return '';\n });\n expect(await tmux.findAgentPid('foo', matchesClaude)).toBe(200);\n });\n\n it('prefers the deepest match (wrapper case)', async () => {\n // pane 100 → 200 (claude wrapper, matches) → 300 (claude, matches, deeper)\n setExecFileHandler((cmd, args) => {\n if (cmd === 'tmux') return '100\\n';\n if (cmd === 'pgrep' && args[1] === '100') return '200\\n';\n if (cmd === 'pgrep' && args[1] === '200') return '300\\n';\n if (cmd === 'pgrep') return new Error('no children');\n if (cmd === 'ps' && args[1] === '200') return '/usr/local/bin/claude';\n if (cmd === 'ps' && args[1] === '300') return '/usr/local/lib/claude';\n return '';\n });\n expect(await tmux.findAgentPid('foo', matchesClaude)).toBe(300);\n });\n\n it('skips non-matching subprocesses (MCP child case)', async () => {\n // pane 100 → 200 (claude, matches) → 300 (mcp-server, no match)\n setExecFileHandler((cmd, args) => {\n if (cmd === 'tmux') return '100\\n';\n if (cmd === 'pgrep' && args[1] === '100') return '200\\n';\n if (cmd === 'pgrep' && args[1] === '200') return '300\\n';\n if (cmd === 'pgrep') return new Error('no children');\n if (cmd === 'ps' && args[1] === '200') return 'claude';\n if (cmd === 'ps' && args[1] === '300') return 'node mcp-server.js';\n return '';\n });\n expect(await tmux.findAgentPid('foo', matchesClaude)).toBe(200);\n });\n\n it('handles the gemini Node-script shape via a token-scan matcher', async () => {\n const matchesGemini = (cmd: string) =>\n cmd.split(/\\s+/).some((t) => t.endsWith('/gemini') || t === 'gemini');\n setExecFileHandler((cmd, args) => {\n if (cmd === 'tmux') return '100\\n';\n if (cmd === 'pgrep' && args[1] === '100') return '200\\n';\n if (cmd === 'pgrep') return new Error('no children');\n if (cmd === 'ps' && args[1] === '200') return 'node /opt/homebrew/bin/gemini --foo';\n return '';\n });\n expect(await tmux.findAgentPid('foo', matchesGemini)).toBe(200);\n });\n\n it('handles multiple children at the same level', async () => {\n // pane 100 → [200 (nope), 201 (claude)]\n setExecFileHandler((cmd, args) => {\n if (cmd === 'tmux') return '100\\n';\n if (cmd === 'pgrep' && args[1] === '100') return '200\\n201\\n';\n if (cmd === 'pgrep') return new Error('no children');\n if (cmd === 'ps' && args[1] === '200') return 'bash';\n if (cmd === 'ps' && args[1] === '201') return 'claude';\n return '';\n });\n expect(await tmux.findAgentPid('foo', matchesClaude)).toBe(201);\n });\n });\n});\n"],"names":["execFile","TmuxManager","vi","mock","fn","mockedExecFile","setExecFileHandler","handler","mockImplementation","_cmd","_args","cb","result","Error","stdout","stderr","describe","tmux","beforeEach","mockReset","it","expect","isAvailable","toBe","sessionExists","createSession","toHaveBeenCalledWith","any","Function","sendKeys","killSession","resolves","toBeUndefined","matchesClaude","cmd","split","endsWith","args","findAgentPid","toBeNull","matchesGemini","some","t"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../src/__tests__/terminal/TmuxManager.test.ts"],"sourcesContent":["import { execFile } from 'child_process';\nimport type { MockedFunction } from 'vitest';\nimport { TmuxManager } from '../../terminal/TmuxManager.js';\n\nvi.mock('child_process', () => ({\n execFile: vi.fn(),\n}));\n\ntype ExecFileCb = (err: Error | null, result?: { stdout: string; stderr: string }) => void;\nconst mockedExecFile = execFile as unknown as MockedFunction<\n (cmd: string, args: string[], cb: ExecFileCb) => void\n>;\n\n/** Drive the promisified execFile mock with a per-call handler. */\nfunction setExecFileHandler(handler: (cmd: string, args: string[]) => string | Error) {\n mockedExecFile.mockImplementation((_cmd, _args, cb) => {\n const result = handler(_cmd, _args);\n if (result instanceof Error) cb(result);\n else cb(null, { stdout: result, stderr: '' });\n });\n}\n\ndescribe('TmuxManager', () => {\n let tmux: TmuxManager;\n\n beforeEach(() => {\n tmux = new TmuxManager();\n mockedExecFile.mockReset();\n });\n\n describe('isAvailable', () => {\n it('returns true when `tmux -V` succeeds', async () => {\n setExecFileHandler(() => 'tmux 3.4');\n expect(await tmux.isAvailable()).toBe(true);\n });\n\n it('returns false when tmux is missing', async () => {\n setExecFileHandler(() => new Error('ENOENT'));\n expect(await tmux.isAvailable()).toBe(false);\n });\n });\n\n describe('sessionExists', () => {\n it('returns true when has-session succeeds', async () => {\n setExecFileHandler(() => '');\n expect(await tmux.sessionExists('foo')).toBe(true);\n });\n\n it('returns false when has-session fails', async () => {\n setExecFileHandler(() => new Error('no session'));\n expect(await tmux.sessionExists('foo')).toBe(false);\n });\n });\n\n describe('createSession', () => {\n it('issues `tmux new-session -d -s <name> -c <cwd>`', async () => {\n setExecFileHandler(() => '');\n await tmux.createSession('foo', '/work');\n expect(mockedExecFile).toHaveBeenCalledWith(\n 'tmux',\n ['new-session', '-d', '-s', 'foo', '-c', '/work'],\n expect.any(Function),\n );\n });\n });\n\n describe('sendKeys', () => {\n it('appends Enter so the command runs', async () => {\n setExecFileHandler(() => '');\n await tmux.sendKeys('foo', 'claude');\n expect(mockedExecFile).toHaveBeenCalledWith(\n 'tmux',\n ['send-keys', '-t', 'foo', 'claude', 'Enter'],\n expect.any(Function),\n );\n });\n });\n\n describe('killSession', () => {\n it('swallows errors when session is already gone', async () => {\n setExecFileHandler(() => new Error(\"can't find session\"));\n await expect(tmux.killSession('foo')).resolves.toBeUndefined();\n });\n });\n\n describe('findAgentPid', () => {\n const matchesClaude = (cmd: string) => cmd.split(/\\s+/)[0]?.endsWith('claude') ?? false;\n\n it('returns null when the session has no pane', async () => {\n setExecFileHandler((cmd, args) => {\n if (args[0] === 'list-panes') return new Error('no session');\n return '';\n });\n expect(await tmux.findAgentPid('foo', matchesClaude)).toBeNull();\n });\n\n it('returns null when no descendant matches', async () => {\n // pane PID 100; child 200 is \"node /unrelated\" — no match\n setExecFileHandler((cmd, args) => {\n if (cmd === 'tmux' && args[0] === 'list-panes') return '100\\n';\n if (cmd === 'pgrep' && args[1] === '100') return '200\\n';\n if (cmd === 'pgrep') return new Error('no children');\n if (cmd === 'ps' && args[1] === '200') return 'node /unrelated';\n return '';\n });\n expect(await tmux.findAgentPid('foo', matchesClaude)).toBeNull();\n });\n\n it('returns the matching pane PID when the agent replaces the shell', async () => {\n setExecFileHandler((cmd, args) => {\n if (cmd === 'tmux' && args[0] === 'list-panes') return '100\\n';\n if (cmd === 'pgrep') return new Error('no children');\n if (cmd === 'ps' && args[1] === '100') return '/usr/local/bin/claude';\n return '';\n });\n expect(await tmux.findAgentPid('foo', matchesClaude)).toBe(100);\n });\n\n it('returns the matching descendant when found', async () => {\n // pane 100 → child 200 (claude) — no grandchildren\n setExecFileHandler((cmd, args) => {\n if (cmd === 'tmux') return '100\\n';\n if (cmd === 'pgrep' && args[1] === '100') return '200\\n';\n if (cmd === 'pgrep') return new Error('no children');\n if (cmd === 'ps' && args[1] === '200') return 'claude';\n return '';\n });\n expect(await tmux.findAgentPid('foo', matchesClaude)).toBe(200);\n });\n\n it('prefers the deepest match (wrapper case)', async () => {\n // pane 100 → 200 (claude wrapper, matches) → 300 (claude, matches, deeper)\n setExecFileHandler((cmd, args) => {\n if (cmd === 'tmux') return '100\\n';\n if (cmd === 'pgrep' && args[1] === '100') return '200\\n';\n if (cmd === 'pgrep' && args[1] === '200') return '300\\n';\n if (cmd === 'pgrep') return new Error('no children');\n if (cmd === 'ps' && args[1] === '200') return '/usr/local/bin/claude';\n if (cmd === 'ps' && args[1] === '300') return '/usr/local/lib/claude';\n return '';\n });\n expect(await tmux.findAgentPid('foo', matchesClaude)).toBe(300);\n });\n\n it('skips non-matching subprocesses (MCP child case)', async () => {\n // pane 100 → 200 (claude, matches) → 300 (mcp-server, no match)\n setExecFileHandler((cmd, args) => {\n if (cmd === 'tmux') return '100\\n';\n if (cmd === 'pgrep' && args[1] === '100') return '200\\n';\n if (cmd === 'pgrep' && args[1] === '200') return '300\\n';\n if (cmd === 'pgrep') return new Error('no children');\n if (cmd === 'ps' && args[1] === '200') return 'claude';\n if (cmd === 'ps' && args[1] === '300') return 'node mcp-server.js';\n return '';\n });\n expect(await tmux.findAgentPid('foo', matchesClaude)).toBe(200);\n });\n\n it('handles the gemini Node-script shape via a token-scan matcher', async () => {\n const matchesGemini = (cmd: string) =>\n cmd.split(/\\s+/).some((t) => t.endsWith('/gemini') || t === 'gemini');\n setExecFileHandler((cmd, args) => {\n if (cmd === 'tmux') return '100\\n';\n if (cmd === 'pgrep' && args[1] === '100') return '200\\n';\n if (cmd === 'pgrep') return new Error('no children');\n if (cmd === 'ps' && args[1] === '200') return 'node /opt/homebrew/bin/gemini --foo';\n return '';\n });\n expect(await tmux.findAgentPid('foo', matchesGemini)).toBe(200);\n });\n\n it('handles multiple children at the same level', async () => {\n // pane 100 → [200 (nope), 201 (claude)]\n setExecFileHandler((cmd, args) => {\n if (cmd === 'tmux') return '100\\n';\n if (cmd === 'pgrep' && args[1] === '100') return '200\\n201\\n';\n if (cmd === 'pgrep') return new Error('no children');\n if (cmd === 'ps' && args[1] === '200') return 'bash';\n if (cmd === 'ps' && args[1] === '201') return 'claude';\n return '';\n });\n expect(await tmux.findAgentPid('foo', matchesClaude)).toBe(201);\n });\n });\n});\n"],"names":["execFile","TmuxManager","vi","mock","fn","mockedExecFile","setExecFileHandler","handler","mockImplementation","_cmd","_args","cb","result","Error","stdout","stderr","describe","tmux","beforeEach","mockReset","it","expect","isAvailable","toBe","sessionExists","createSession","toHaveBeenCalledWith","any","Function","sendKeys","killSession","resolves","toBeUndefined","matchesClaude","cmd","split","endsWith","args","findAgentPid","toBeNull","matchesGemini","some","t"],"mappings":"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"}
@@ -12,6 +12,12 @@ describe('AGENTS', ()=>{
12
12
  expect(AGENTS.pi.matches('/usr/local/bin/pi --model x')).toBe(true);
13
13
  expect(AGENTS.pi.matches('node /repo/feature-pi-adapter/script.js')).toBe(false);
14
14
  });
15
+ it('includes Grok as a startable agent', ()=>{
16
+ expect(AGENTS.grok_cli.command).toBe('grok');
17
+ expect(AGENTS.grok_cli.matches('grok')).toBe(true);
18
+ expect(AGENTS.grok_cli.matches('/Users/dev/.grok/bin/grok --always-approve')).toBe(true);
19
+ expect(AGENTS.grok_cli.matches('node /repo/feature-grok-cli/script.js')).toBe(false);
20
+ });
15
21
  });
16
22
 
17
23
  //# sourceMappingURL=agents.test.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/__tests__/utils/agents.test.ts"],"sourcesContent":["import { describe, expect, it } from 'vitest';\nimport { AGENTS } from '../../../src/utils/agents.js';\n\ndescribe('AGENTS', () => {\n it('includes Copilot as a startable agent', () => {\n expect(AGENTS.copilot.command).toBe('copilot');\n expect(AGENTS.copilot.matches('/opt/homebrew/Caskroom/copilot-cli/1.0.60/copilot')).toBe(true);\n expect(AGENTS.copilot.matches('node /repo/feature-cli-copilot-cli/script.js')).toBe(false);\n });\n\n it('includes Pi as a startable agent', () => {\n expect(AGENTS.pi.command).toBe('pi');\n expect(AGENTS.pi.matches('pi')).toBe(true);\n expect(AGENTS.pi.matches('/usr/local/bin/pi --model x')).toBe(true);\n expect(AGENTS.pi.matches('node /repo/feature-pi-adapter/script.js')).toBe(false);\n });\n});\n"],"names":["describe","expect","it","AGENTS","copilot","command","toBe","matches","pi"],"mappings":"AAAA,SAASA,QAAQ,EAAEC,MAAM,EAAEC,EAAE,QAAQ,SAAS;AAC9C,SAASC,MAAM,QAAQ,+BAA+B;AAEtDH,SAAS,UAAU;IACfE,GAAG,yCAAyC;QACxCD,OAAOE,OAAOC,OAAO,CAACC,OAAO,EAAEC,IAAI,CAAC;QACpCL,OAAOE,OAAOC,OAAO,CAACG,OAAO,CAAC,sDAAsDD,IAAI,CAAC;QACzFL,OAAOE,OAAOC,OAAO,CAACG,OAAO,CAAC,iDAAiDD,IAAI,CAAC;IACxF;IAEAJ,GAAG,oCAAoC;QACnCD,OAAOE,OAAOK,EAAE,CAACH,OAAO,EAAEC,IAAI,CAAC;QAC/BL,OAAOE,OAAOK,EAAE,CAACD,OAAO,CAAC,OAAOD,IAAI,CAAC;QACrCL,OAAOE,OAAOK,EAAE,CAACD,OAAO,CAAC,gCAAgCD,IAAI,CAAC;QAC9DL,OAAOE,OAAOK,EAAE,CAACD,OAAO,CAAC,4CAA4CD,IAAI,CAAC;IAC9E;AACJ"}
1
+ {"version":3,"sources":["../../../src/__tests__/utils/agents.test.ts"],"sourcesContent":["import { describe, expect, it } from 'vitest';\nimport { AGENTS } from '../../../src/utils/agents.js';\n\ndescribe('AGENTS', () => {\n it('includes Copilot as a startable agent', () => {\n expect(AGENTS.copilot.command).toBe('copilot');\n expect(AGENTS.copilot.matches('/opt/homebrew/Caskroom/copilot-cli/1.0.60/copilot')).toBe(true);\n expect(AGENTS.copilot.matches('node /repo/feature-cli-copilot-cli/script.js')).toBe(false);\n });\n\n it('includes Pi as a startable agent', () => {\n expect(AGENTS.pi.command).toBe('pi');\n expect(AGENTS.pi.matches('pi')).toBe(true);\n expect(AGENTS.pi.matches('/usr/local/bin/pi --model x')).toBe(true);\n expect(AGENTS.pi.matches('node /repo/feature-pi-adapter/script.js')).toBe(false);\n });\n\n it('includes Grok as a startable agent', () => {\n expect(AGENTS.grok_cli.command).toBe('grok');\n expect(AGENTS.grok_cli.matches('grok')).toBe(true);\n expect(AGENTS.grok_cli.matches('/Users/dev/.grok/bin/grok --always-approve')).toBe(true);\n expect(AGENTS.grok_cli.matches('node /repo/feature-grok-cli/script.js')).toBe(false);\n });\n});\n"],"names":["describe","expect","it","AGENTS","copilot","command","toBe","matches","pi","grok_cli"],"mappings":"AAAA,SAASA,QAAQ,EAAEC,MAAM,EAAEC,EAAE,QAAQ,SAAS;AAC9C,SAASC,MAAM,QAAQ,+BAA+B;AAEtDH,SAAS,UAAU;IACfE,GAAG,yCAAyC;QACxCD,OAAOE,OAAOC,OAAO,CAACC,OAAO,EAAEC,IAAI,CAAC;QACpCL,OAAOE,OAAOC,OAAO,CAACG,OAAO,CAAC,sDAAsDD,IAAI,CAAC;QACzFL,OAAOE,OAAOC,OAAO,CAACG,OAAO,CAAC,iDAAiDD,IAAI,CAAC;IACxF;IAEAJ,GAAG,oCAAoC;QACnCD,OAAOE,OAAOK,EAAE,CAACH,OAAO,EAAEC,IAAI,CAAC;QAC/BL,OAAOE,OAAOK,EAAE,CAACD,OAAO,CAAC,OAAOD,IAAI,CAAC;QACrCL,OAAOE,OAAOK,EAAE,CAACD,OAAO,CAAC,gCAAgCD,IAAI,CAAC;QAC9DL,OAAOE,OAAOK,EAAE,CAACD,OAAO,CAAC,4CAA4CD,IAAI,CAAC;IAC9E;IAEAJ,GAAG,sCAAsC;QACrCD,OAAOE,OAAOM,QAAQ,CAACJ,OAAO,EAAEC,IAAI,CAAC;QACrCL,OAAOE,OAAOM,QAAQ,CAACF,OAAO,CAAC,SAASD,IAAI,CAAC;QAC7CL,OAAOE,OAAOM,QAAQ,CAACF,OAAO,CAAC,+CAA+CD,IAAI,CAAC;QACnFL,OAAOE,OAAOM,QAAQ,CAACF,OAAO,CAAC,0CAA0CD,IAAI,CAAC;IAClF;AACJ"}
@@ -7,7 +7,7 @@
7
7
  /**
8
8
  * Type of AI agent
9
9
  */
10
- export type AgentType = 'claude' | 'gemini_cli' | 'codex' | 'opencode' | 'copilot' | 'pi' | 'other';
10
+ export type AgentType = 'claude' | 'gemini_cli' | 'grok_cli' | 'codex' | 'opencode' | 'copilot' | 'pi' | 'other';
11
11
  /**
12
12
  * Current status of an agent
13
13
  */
@@ -1 +1 @@
1
- {"version":3,"file":"AgentAdapter.d.ts","sourceRoot":"","sources":["../../src/adapters/AgentAdapter.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH;;GAEG;AACH,MAAM,MAAM,SAAS,GAAG,QAAQ,GAAG,YAAY,GAAG,OAAO,GAAG,UAAU,GAAG,SAAS,GAAG,IAAI,GAAG,OAAO,CAAC;AAEpG;;GAEG;AACH,oBAAY,WAAW;IACnB,OAAO,YAAY;IACnB,OAAO,YAAY;IACnB,IAAI,SAAS;IACb,OAAO,YAAY;CACtB;AAED;;GAEG;AACH,MAAM,WAAW,SAAS;IACtB,oEAAoE;IACpE,IAAI,EAAE,MAAM,CAAC;IAEb,oBAAoB;IACpB,IAAI,EAAE,SAAS,CAAC;IAEhB,qBAAqB;IACrB,MAAM,EAAE,WAAW,CAAC;IAEpB,oCAAoC;IACpC,OAAO,EAAE,MAAM,CAAC;IAEhB,iBAAiB;IACjB,GAAG,EAAE,MAAM,CAAC;IAEZ,qCAAqC;IACrC,WAAW,EAAE,MAAM,CAAC;IAEpB,mBAAmB;IACnB,SAAS,EAAE,MAAM,CAAC;IAElB,iCAAiC;IACjC,UAAU,EAAE,IAAI,CAAC;IAEjB,6CAA6C;IAC7C,eAAe,CAAC,EAAE,MAAM,CAAC;CAC5B;AAED;;GAEG;AACH,MAAM,WAAW,WAAW;IACxB,iBAAiB;IACjB,GAAG,EAAE,MAAM,CAAC;IAEZ,wEAAwE;IACxE,IAAI,CAAC,EAAE,MAAM,CAAC;IAEd,sBAAsB;IACtB,OAAO,EAAE,MAAM,CAAC;IAEhB,wBAAwB;IACxB,GAAG,EAAE,MAAM,CAAC;IAEZ,qCAAqC;IACrC,GAAG,EAAE,MAAM,CAAC;IAEZ,uDAAuD;IACvD,SAAS,CAAC,EAAE,IAAI,CAAC;CACpB;AAED;;GAEG;AACH,MAAM,WAAW,mBAAmB;IAChC,IAAI,EAAE,MAAM,GAAG,WAAW,GAAG,QAAQ,CAAC;IACtC,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,MAAM,CAAC;CACtB;AAED;;;;;GAKG;AACH,MAAM,WAAW,cAAc;IAC3B,sCAAsC;IACtC,IAAI,EAAE,SAAS,CAAC;IAEhB;;;;OAIG;IACH,SAAS,EAAE,MAAM,CAAC;IAElB,sEAAsE;IACtE,GAAG,EAAE,MAAM,CAAC;IAEZ;;;;;;OAMG;IACH,gBAAgB,EAAE,MAAM,CAAC;IAEzB,+EAA+E;IAC/E,UAAU,EAAE,IAAI,CAAC;IAEjB,oFAAoF;IACpF,SAAS,EAAE,IAAI,CAAC;IAEhB,oEAAoE;IACpE,eAAe,EAAE,MAAM,CAAC;CAC3B;AAED;;;;;;GAMG;AACH,MAAM,WAAW,mBAAmB;IAChC;;;;OAIG;IACH,GAAG,CAAC,EAAE,MAAM,CAAC;IAEb;;;;;;OAMG;IACH,IAAI,CAAC,EAAE,SAAS,CAAC;CACpB;AAED;;;;GAIG;AACH,MAAM,WAAW,YAAY;IACzB,yCAAyC;IACzC,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC;IAEzB;;;OAGG;IACH,YAAY,IAAI,OAAO,CAAC,SAAS,EAAE,CAAC,CAAC;IAErC;;;;OAIG;IACH,SAAS,CAAC,WAAW,EAAE,WAAW,GAAG,OAAO,CAAC;IAE7C;;;;;OAKG;IACH,eAAe,CAAC,eAAe,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;QAAE,OAAO,CAAC,EAAE,OAAO,CAAA;KAAE,GAAG,mBAAmB,EAAE,CAAC;IAEjG;;;;;;;;;OASG;IACH,YAAY,CAAC,IAAI,CAAC,EAAE,mBAAmB,GAAG,OAAO,CAAC,cAAc,EAAE,CAAC,CAAC;CACvE"}
1
+ {"version":3,"file":"AgentAdapter.d.ts","sourceRoot":"","sources":["../../src/adapters/AgentAdapter.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH;;GAEG;AACH,MAAM,MAAM,SAAS,GAAG,QAAQ,GAAG,YAAY,GAAG,UAAU,GAAG,OAAO,GAAG,UAAU,GAAG,SAAS,GAAG,IAAI,GAAG,OAAO,CAAC;AAEjH;;GAEG;AACH,oBAAY,WAAW;IACnB,OAAO,YAAY;IACnB,OAAO,YAAY;IACnB,IAAI,SAAS;IACb,OAAO,YAAY;CACtB;AAED;;GAEG;AACH,MAAM,WAAW,SAAS;IACtB,oEAAoE;IACpE,IAAI,EAAE,MAAM,CAAC;IAEb,oBAAoB;IACpB,IAAI,EAAE,SAAS,CAAC;IAEhB,qBAAqB;IACrB,MAAM,EAAE,WAAW,CAAC;IAEpB,oCAAoC;IACpC,OAAO,EAAE,MAAM,CAAC;IAEhB,iBAAiB;IACjB,GAAG,EAAE,MAAM,CAAC;IAEZ,qCAAqC;IACrC,WAAW,EAAE,MAAM,CAAC;IAEpB,mBAAmB;IACnB,SAAS,EAAE,MAAM,CAAC;IAElB,iCAAiC;IACjC,UAAU,EAAE,IAAI,CAAC;IAEjB,6CAA6C;IAC7C,eAAe,CAAC,EAAE,MAAM,CAAC;CAC5B;AAED;;GAEG;AACH,MAAM,WAAW,WAAW;IACxB,iBAAiB;IACjB,GAAG,EAAE,MAAM,CAAC;IAEZ,wEAAwE;IACxE,IAAI,CAAC,EAAE,MAAM,CAAC;IAEd,sBAAsB;IACtB,OAAO,EAAE,MAAM,CAAC;IAEhB,wBAAwB;IACxB,GAAG,EAAE,MAAM,CAAC;IAEZ,qCAAqC;IACrC,GAAG,EAAE,MAAM,CAAC;IAEZ,uDAAuD;IACvD,SAAS,CAAC,EAAE,IAAI,CAAC;CACpB;AAED;;GAEG;AACH,MAAM,WAAW,mBAAmB;IAChC,IAAI,EAAE,MAAM,GAAG,WAAW,GAAG,QAAQ,CAAC;IACtC,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,MAAM,CAAC;CACtB;AAED;;;;;GAKG;AACH,MAAM,WAAW,cAAc;IAC3B,sCAAsC;IACtC,IAAI,EAAE,SAAS,CAAC;IAEhB;;;;OAIG;IACH,SAAS,EAAE,MAAM,CAAC;IAElB,sEAAsE;IACtE,GAAG,EAAE,MAAM,CAAC;IAEZ;;;;;;OAMG;IACH,gBAAgB,EAAE,MAAM,CAAC;IAEzB,+EAA+E;IAC/E,UAAU,EAAE,IAAI,CAAC;IAEjB,oFAAoF;IACpF,SAAS,EAAE,IAAI,CAAC;IAEhB,oEAAoE;IACpE,eAAe,EAAE,MAAM,CAAC;CAC3B;AAED;;;;;;GAMG;AACH,MAAM,WAAW,mBAAmB;IAChC;;;;OAIG;IACH,GAAG,CAAC,EAAE,MAAM,CAAC;IAEb;;;;;;OAMG;IACH,IAAI,CAAC,EAAE,SAAS,CAAC;CACpB;AAED;;;;GAIG;AACH,MAAM,WAAW,YAAY;IACzB,yCAAyC;IACzC,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC;IAEzB;;;OAGG;IACH,YAAY,IAAI,OAAO,CAAC,SAAS,EAAE,CAAC,CAAC;IAErC;;;;OAIG;IACH,SAAS,CAAC,WAAW,EAAE,WAAW,GAAG,OAAO,CAAC;IAE7C;;;;;OAKG;IACH,eAAe,CAAC,eAAe,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;QAAE,OAAO,CAAC,EAAE,OAAO,CAAA;KAAE,GAAG,mBAAmB,EAAE,CAAC;IAEjG;;;;;;;;;OASG;IACH,YAAY,CAAC,IAAI,CAAC,EAAE,mBAAmB,GAAG,OAAO,CAAC,cAAc,EAAE,CAAC,CAAC;CACvE"}
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/adapters/AgentAdapter.ts"],"sourcesContent":["/**\n * Agent Adapter Interface\n * \n * Defines the contract for detecting and managing different types of AI agents.\n * Each adapter is responsible for detecting agents of a specific type (e.g., claude).\n */\n\n/**\n * Type of AI agent\n */\nexport type AgentType = 'claude' | 'gemini_cli' | 'codex' | 'opencode' | 'copilot' | 'pi' | 'other';\n\n/**\n * Current status of an agent\n */\nexport enum AgentStatus {\n RUNNING = 'running',\n WAITING = 'waiting',\n IDLE = 'idle',\n UNKNOWN = 'unknown'\n}\n\n/**\n * Information about a detected agent\n */\nexport interface AgentInfo {\n /** Project-based name (e.g., \"ai-devkit\" or \"ai-devkit (merry)\") */\n name: string;\n\n /** Type of agent */\n type: AgentType;\n\n /** Current status */\n status: AgentStatus;\n\n /** Last user prompt from history */\n summary: string;\n\n /** Process ID */\n pid: number;\n\n /** Working directory/project path */\n projectPath: string;\n\n /** Session UUID */\n sessionId: string;\n\n /** Timestamp of last activity */\n lastActive: Date;\n\n /** Path to the session JSONL file on disk */\n sessionFilePath?: string;\n}\n\n/**\n * Information about a running process\n */\nexport interface ProcessInfo {\n /** Process ID */\n pid: number;\n\n /** Parent process ID, populated by listAgentProcesses when available */\n ppid?: number;\n\n /** Process command */\n command: string;\n\n /** Working directory */\n cwd: string;\n\n /** Terminal TTY (e.g., \"ttys030\") */\n tty: string;\n\n /** Process start time, populated by enrichProcesses */\n startTime?: Date;\n}\n\n/**\n * A single message in a conversation\n */\nexport interface ConversationMessage {\n role: 'user' | 'assistant' | 'system';\n content: string;\n timestamp?: string;\n}\n\n/**\n * A historical session discovered on disk (running or not).\n *\n * Used by `listSessions` to surface enough context for a user to identify\n * a session and resume it via the originating tool's resume command.\n */\nexport interface SessionSummary {\n /** Tool that produced this session */\n type: AgentType;\n\n /**\n * ID accepted by the tool's resume command. Adapters MUST pass this\n * through verbatim — no normalization, no encoding/decoding — so it\n * round-trips into `claude --resume <id>` (and equivalents).\n */\n sessionId: string;\n\n /** Working directory the session was started in (best-known value) */\n cwd: string;\n\n /**\n * Trimmed first user message; empty string if none. Adapters apply\n * the same noise-filter their existing parsers use (skip tool_result\n * blocks, request-interruption notices, system-injected skill\n * content). The CLI table renderer substitutes a placeholder for\n * empty values; JSON output keeps the empty string raw.\n */\n firstUserMessage: string;\n\n /** Last activity timestamp (from session content; falls back to file mtime) */\n lastActive: Date;\n\n /** Session start time (from session content; falls back to file birthtime/mtime) */\n startedAt: Date;\n\n /** Absolute path to the session file on disk (debug/diagnostics) */\n sessionFilePath: string;\n}\n\n/**\n * Filters passed by the CLI to {@link AgentAdapter.listSessions}.\n *\n * The CLI is the source of truth for filter defaults and semantics\n * (e.g. cwd defaults to process.cwd(); --all clears it). Adapters apply\n * the values they receive — they don't invent defaults.\n */\nexport interface ListSessionsOptions {\n /**\n * Filter to sessions whose recorded cwd matches this path using strict\n * equality (no prefix/ancestor matching in v1). Undefined = no cwd\n * filter.\n */\n cwd?: string;\n\n /**\n * Filter to a single tool. Enforced by `AgentManager.listSessions`,\n * which skips adapters whose `type` doesn't match. Adapters MAY\n * ignore this field — by the time their `listSessions` runs, the\n * type filter is already satisfied. Undefined = include every\n * registered adapter.\n */\n type?: AgentType;\n}\n\n/**\n * Agent Adapter Interface\n *\n * Implementations must provide detection logic for a specific agent type.\n */\nexport interface AgentAdapter {\n /** Type of agent this adapter handles */\n readonly type: AgentType;\n\n /**\n * Detect running agents of this type\n * @returns List of detected agents\n */\n detectAgents(): Promise<AgentInfo[]>;\n\n /**\n * Check if this adapter can handle the given process\n * @param processInfo Process information\n * @returns True if this adapter can handle the process\n */\n canHandle(processInfo: ProcessInfo): boolean;\n\n /**\n * Read the full conversation from a session file\n * @param sessionFilePath Path to the session JSONL file\n * @param options.verbose Include tool call/result details\n * @returns Array of conversation messages\n */\n getConversation(sessionFilePath: string, options?: { verbose?: boolean }): ConversationMessage[];\n\n /**\n * Enumerate historical sessions for this tool from disk.\n *\n * Applies `opts.cwd` as a strict-equality filter when set. Returns\n * {@link SessionSummary} entries unsorted; sorting and global filters\n * are handled by `AgentManager` and the CLI.\n *\n * @param opts Filter options computed by the CLI\n * @returns Array of sessions discovered on disk\n */\n listSessions(opts?: ListSessionsOptions): Promise<SessionSummary[]>;\n}\n"],"names":["AgentStatus"],"mappings":"AAAA;;;;;CAKC,GAED;;CAEC,GAGD;;CAEC,GACD,OAAO,IAAA,AAAKA,qCAAAA;;;;;WAAAA;MAKX"}
1
+ {"version":3,"sources":["../../src/adapters/AgentAdapter.ts"],"sourcesContent":["/**\n * Agent Adapter Interface\n * \n * Defines the contract for detecting and managing different types of AI agents.\n * Each adapter is responsible for detecting agents of a specific type (e.g., claude).\n */\n\n/**\n * Type of AI agent\n */\nexport type AgentType = 'claude' | 'gemini_cli' | 'grok_cli' | 'codex' | 'opencode' | 'copilot' | 'pi' | 'other';\n\n/**\n * Current status of an agent\n */\nexport enum AgentStatus {\n RUNNING = 'running',\n WAITING = 'waiting',\n IDLE = 'idle',\n UNKNOWN = 'unknown'\n}\n\n/**\n * Information about a detected agent\n */\nexport interface AgentInfo {\n /** Project-based name (e.g., \"ai-devkit\" or \"ai-devkit (merry)\") */\n name: string;\n\n /** Type of agent */\n type: AgentType;\n\n /** Current status */\n status: AgentStatus;\n\n /** Last user prompt from history */\n summary: string;\n\n /** Process ID */\n pid: number;\n\n /** Working directory/project path */\n projectPath: string;\n\n /** Session UUID */\n sessionId: string;\n\n /** Timestamp of last activity */\n lastActive: Date;\n\n /** Path to the session JSONL file on disk */\n sessionFilePath?: string;\n}\n\n/**\n * Information about a running process\n */\nexport interface ProcessInfo {\n /** Process ID */\n pid: number;\n\n /** Parent process ID, populated by listAgentProcesses when available */\n ppid?: number;\n\n /** Process command */\n command: string;\n\n /** Working directory */\n cwd: string;\n\n /** Terminal TTY (e.g., \"ttys030\") */\n tty: string;\n\n /** Process start time, populated by enrichProcesses */\n startTime?: Date;\n}\n\n/**\n * A single message in a conversation\n */\nexport interface ConversationMessage {\n role: 'user' | 'assistant' | 'system';\n content: string;\n timestamp?: string;\n}\n\n/**\n * A historical session discovered on disk (running or not).\n *\n * Used by `listSessions` to surface enough context for a user to identify\n * a session and resume it via the originating tool's resume command.\n */\nexport interface SessionSummary {\n /** Tool that produced this session */\n type: AgentType;\n\n /**\n * ID accepted by the tool's resume command. Adapters MUST pass this\n * through verbatim — no normalization, no encoding/decoding — so it\n * round-trips into `claude --resume <id>` (and equivalents).\n */\n sessionId: string;\n\n /** Working directory the session was started in (best-known value) */\n cwd: string;\n\n /**\n * Trimmed first user message; empty string if none. Adapters apply\n * the same noise-filter their existing parsers use (skip tool_result\n * blocks, request-interruption notices, system-injected skill\n * content). The CLI table renderer substitutes a placeholder for\n * empty values; JSON output keeps the empty string raw.\n */\n firstUserMessage: string;\n\n /** Last activity timestamp (from session content; falls back to file mtime) */\n lastActive: Date;\n\n /** Session start time (from session content; falls back to file birthtime/mtime) */\n startedAt: Date;\n\n /** Absolute path to the session file on disk (debug/diagnostics) */\n sessionFilePath: string;\n}\n\n/**\n * Filters passed by the CLI to {@link AgentAdapter.listSessions}.\n *\n * The CLI is the source of truth for filter defaults and semantics\n * (e.g. cwd defaults to process.cwd(); --all clears it). Adapters apply\n * the values they receive — they don't invent defaults.\n */\nexport interface ListSessionsOptions {\n /**\n * Filter to sessions whose recorded cwd matches this path using strict\n * equality (no prefix/ancestor matching in v1). Undefined = no cwd\n * filter.\n */\n cwd?: string;\n\n /**\n * Filter to a single tool. Enforced by `AgentManager.listSessions`,\n * which skips adapters whose `type` doesn't match. Adapters MAY\n * ignore this field — by the time their `listSessions` runs, the\n * type filter is already satisfied. Undefined = include every\n * registered adapter.\n */\n type?: AgentType;\n}\n\n/**\n * Agent Adapter Interface\n *\n * Implementations must provide detection logic for a specific agent type.\n */\nexport interface AgentAdapter {\n /** Type of agent this adapter handles */\n readonly type: AgentType;\n\n /**\n * Detect running agents of this type\n * @returns List of detected agents\n */\n detectAgents(): Promise<AgentInfo[]>;\n\n /**\n * Check if this adapter can handle the given process\n * @param processInfo Process information\n * @returns True if this adapter can handle the process\n */\n canHandle(processInfo: ProcessInfo): boolean;\n\n /**\n * Read the full conversation from a session file\n * @param sessionFilePath Path to the session JSONL file\n * @param options.verbose Include tool call/result details\n * @returns Array of conversation messages\n */\n getConversation(sessionFilePath: string, options?: { verbose?: boolean }): ConversationMessage[];\n\n /**\n * Enumerate historical sessions for this tool from disk.\n *\n * Applies `opts.cwd` as a strict-equality filter when set. Returns\n * {@link SessionSummary} entries unsorted; sorting and global filters\n * are handled by `AgentManager` and the CLI.\n *\n * @param opts Filter options computed by the CLI\n * @returns Array of sessions discovered on disk\n */\n listSessions(opts?: ListSessionsOptions): Promise<SessionSummary[]>;\n}\n"],"names":["AgentStatus"],"mappings":"AAAA;;;;;CAKC,GAED;;CAEC,GAGD;;CAEC,GACD,OAAO,IAAA,AAAKA,qCAAAA;;;;;WAAAA;MAKX"}
@@ -0,0 +1,79 @@
1
+ import type { AgentAdapter, AgentInfo, ProcessInfo, ConversationMessage, SessionSummary, ListSessionsOptions } from './AgentAdapter.js';
2
+ export declare class GrokCliAdapter implements AgentAdapter {
3
+ readonly type: "grok_cli";
4
+ private base;
5
+ private sessionsDir;
6
+ constructor();
7
+ canHandle(processInfo: ProcessInfo): boolean;
8
+ private isGrokExecutable;
9
+ detectAgents(): Promise<AgentInfo[]>;
10
+ /**
11
+ * Read ~/.grok/active_sessions.json into a pid -> cwd map. Grok writes one
12
+ * { pid, cwd, opened_at } entry per running session and removes it on exit,
13
+ * so this is the reliable way to learn a live process's working directory.
14
+ */
15
+ private readActiveSessions;
16
+ /**
17
+ * Full paths of the session subdirectories directly under a group dir,
18
+ * skipping any non-directory entries. Shared by latestSessionDir() and
19
+ * listSessions() so both enumerate session dirs the same way.
20
+ */
21
+ private listSessionDirs;
22
+ /**
23
+ * Return the most recently active session subdirectory for a cwd, i.e. the
24
+ * ~/.grok/sessions/<encodeURIComponent(cwd)>/<id>/ whose chat_history.jsonl
25
+ * was written last. Returns null when the group dir or any transcript is
26
+ * missing.
27
+ */
28
+ private latestSessionDir;
29
+ private mapSessionToAgent;
30
+ private mapProcessOnlyAgent;
31
+ getConversation(sessionFilePath: string, options?: {
32
+ verbose?: boolean;
33
+ }): ConversationMessage[];
34
+ listSessions(opts?: ListSessionsOptions): Promise<SessionSummary[]>;
35
+ /**
36
+ * Parse a session directory into a {@link GrokSession} from its
37
+ * chat_history.jsonl transcript. Returns null when the transcript is
38
+ * missing — i.e. there is no real session to surface.
39
+ */
40
+ private readSession;
41
+ /**
42
+ * Determine agent status from parsed session state.
43
+ *
44
+ * - past the idle threshold → IDLE
45
+ * - last transcript turn is an assistant message → WAITING (awaiting user)
46
+ * - otherwise (last turn was a user message, or unknown) → RUNNING
47
+ */
48
+ private determineStatus;
49
+ /**
50
+ * Single pass over chat_history.jsonl. Each line is a
51
+ * { type: 'system' | 'user' | 'assistant', content } record where content is
52
+ * either a string or an array of { type: 'text', text } blocks.
53
+ *
54
+ * Grok wraps the real user prompt in <user_query>...</user_query>; the other
55
+ * user records are context injections (<user_info>, <system-reminder>, ...)
56
+ * and are skipped so the summary is the actual prompt, not boilerplate.
57
+ */
58
+ private parseChatHistory;
59
+ /** Flatten a chat record's content (string or text-block array) to text. */
60
+ private extractText;
61
+ /**
62
+ * Extract the prompt inside <user_query>...</user_query>. Returns null when
63
+ * the record has no such tag (a context injection rather than a prompt).
64
+ */
65
+ private extractUserQuery;
66
+ /** Resolve a session dir or an explicit chat_history.jsonl path to the file. */
67
+ private resolveChatPath;
68
+ private getProjectDir;
69
+ /**
70
+ * Resolve the working directory a session group dir was created for.
71
+ *
72
+ * The common case is `decodeURIComponent(<group-name>)`. For paths whose
73
+ * encoded form exceeds the filesystem limit Grok uses a slug+hash and records
74
+ * the original path in a `.cwd` file inside the group — prefer that when
75
+ * present.
76
+ */
77
+ private decodeGroupCwd;
78
+ }
79
+ //# sourceMappingURL=GrokCliAdapter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"GrokCliAdapter.d.ts","sourceRoot":"","sources":["../../src/adapters/GrokCliAdapter.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACR,YAAY,EACZ,SAAS,EACT,WAAW,EACX,mBAAmB,EACnB,cAAc,EACd,mBAAmB,EACtB,MAAM,mBAAmB,CAAC;AA6D3B,qBAAa,cAAe,YAAW,YAAY;IAC/C,QAAQ,CAAC,IAAI,EAAG,UAAU,CAAU;IAEpC,OAAO,CAAC,IAAI,CAAS;IACrB,OAAO,CAAC,WAAW,CAAS;;IAW5B,SAAS,CAAC,WAAW,EAAE,WAAW,GAAG,OAAO;IAI5C,OAAO,CAAC,gBAAgB;IAMlB,YAAY,IAAI,OAAO,CAAC,SAAS,EAAE,CAAC;IA0B1C;;;;OAIG;IACH,OAAO,CAAC,kBAAkB;IAqB1B;;;;OAIG;IACH,OAAO,CAAC,eAAe;IAMvB;;;;;OAKG;IACH,OAAO,CAAC,gBAAgB;IAexB,OAAO,CAAC,iBAAiB;IAezB,OAAO,CAAC,mBAAmB;IAc3B,eAAe,CAAC,eAAe,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;QAAE,OAAO,CAAC,EAAE,OAAO,CAAA;KAAE,GAAG,mBAAmB,EAAE;IAI1F,YAAY,CAAC,IAAI,CAAC,EAAE,mBAAmB,GAAG,OAAO,CAAC,cAAc,EAAE,CAAC;IAoCzE;;;;OAIG;IACH,OAAO,CAAC,WAAW;IAqBnB;;;;;;OAMG;IACH,OAAO,CAAC,eAAe;IAWvB;;;;;;;;OAQG;IACH,OAAO,CAAC,gBAAgB;IA2CxB,4EAA4E;IAC5E,OAAO,CAAC,WAAW;IAcnB;;;OAGG;IACH,OAAO,CAAC,gBAAgB;IAKxB,gFAAgF;IAChF,OAAO,CAAC,eAAe;IAIvB,OAAO,CAAC,aAAa;IAIrB;;;;;;;OAOG;IACH,OAAO,CAAC,cAAc;CASzB"}
@@ -0,0 +1,306 @@
1
+ import * as path from 'path';
2
+ import { AgentStatus } from './AgentAdapter.js';
3
+ import { listAgentProcesses, enrichProcesses } from '../utils/process.js';
4
+ import { isDirectory, safeReadFile, safeReaddir, safeStat } from '../utils/session.js';
5
+ import { generateAgentName } from '../utils/matching.js';
6
+ /**
7
+ * Grok Build CLI Adapter
8
+ *
9
+ * Detects running Grok Build CLI agents by:
10
+ * 1. Finding running `grok` processes via shared listAgentProcesses() — Grok is
11
+ * a native binary at ~/.grok/bin/grok, so argv[0] basename is `grok`.
12
+ * 2. Resolving each live process to its working directory via
13
+ * ~/.grok/active_sessions.json, which Grok maintains as a list of
14
+ * { pid, cwd, opened_at } for every running session. The cwd is then encoded
15
+ * into the session group dir ~/.grok/sessions/<encodeURIComponent(cwd)>/, and
16
+ * the most recently active session subdirectory is picked from it. The
17
+ * process cwd from lsof is only a fallback when the PID is not registered.
18
+ * 3. Reading the session transcript from chat_history.jsonl (the authoritative
19
+ * record of the conversation). The last user turn (the text inside
20
+ * <user_query>...</user_query>) is the summary; the file's mtime is the last
21
+ * activity time. summary.json / updates.jsonl are intentionally not used.
22
+ */ const CHAT_HISTORY_FILE = 'chat_history.jsonl';
23
+ const ACTIVE_SESSIONS_FILE = 'active_sessions.json';
24
+ const CWD_FILE = '.cwd';
25
+ const IDLE_THRESHOLD_MINUTES = 5;
26
+ export class GrokCliAdapter {
27
+ type = 'grok_cli';
28
+ base;
29
+ sessionsDir;
30
+ constructor(){
31
+ // GROK_HOME overrides the ~/.grok base directory; sessions live under
32
+ // <base>/sessions/ and the active-session registry at
33
+ // <base>/active_sessions.json.
34
+ const homeDir = process.env.HOME || process.env.USERPROFILE || '';
35
+ this.base = process.env.GROK_HOME || path.join(homeDir, '.grok');
36
+ this.sessionsDir = path.join(this.base, 'sessions');
37
+ }
38
+ canHandle(processInfo) {
39
+ return this.isGrokExecutable(processInfo.command);
40
+ }
41
+ isGrokExecutable(command) {
42
+ const executable = command.trim().split(/\s+/)[0] || '';
43
+ const base = path.basename(executable).toLowerCase();
44
+ return base === 'grok' || base === 'grok.exe';
45
+ }
46
+ async detectAgents() {
47
+ const processes = enrichProcesses(listAgentProcesses('grok'));
48
+ if (processes.length === 0) {
49
+ return [];
50
+ }
51
+ // active_sessions.json is the authoritative pid -> cwd map for live
52
+ // sessions; the lsof-derived process cwd is only a fallback.
53
+ const pidToCwd = this.readActiveSessions();
54
+ const agents = [];
55
+ for (const proc of processes){
56
+ const cwd = pidToCwd.get(proc.pid) || proc.cwd || '';
57
+ const sessionDir = cwd ? this.latestSessionDir(cwd) : null;
58
+ const session = sessionDir ? this.readSession(sessionDir, cwd) : null;
59
+ if (session && sessionDir) {
60
+ agents.push(this.mapSessionToAgent(session, proc, sessionDir));
61
+ } else {
62
+ agents.push(this.mapProcessOnlyAgent(proc, cwd));
63
+ }
64
+ }
65
+ return agents;
66
+ }
67
+ /**
68
+ * Read ~/.grok/active_sessions.json into a pid -> cwd map. Grok writes one
69
+ * { pid, cwd, opened_at } entry per running session and removes it on exit,
70
+ * so this is the reliable way to learn a live process's working directory.
71
+ */ readActiveSessions() {
72
+ const map = new Map();
73
+ const content = safeReadFile(path.join(this.base, ACTIVE_SESSIONS_FILE));
74
+ if (content === undefined) return map;
75
+ let entries;
76
+ try {
77
+ entries = JSON.parse(content);
78
+ } catch {
79
+ return map;
80
+ }
81
+ if (!Array.isArray(entries)) return map;
82
+ for (const entry of entries){
83
+ if (typeof entry?.pid === 'number' && typeof entry?.cwd === 'string' && entry.cwd) {
84
+ map.set(entry.pid, entry.cwd);
85
+ }
86
+ }
87
+ return map;
88
+ }
89
+ /**
90
+ * Full paths of the session subdirectories directly under a group dir,
91
+ * skipping any non-directory entries. Shared by latestSessionDir() and
92
+ * listSessions() so both enumerate session dirs the same way.
93
+ */ listSessionDirs(groupDir) {
94
+ return safeReaddir(groupDir).map((sessionId)=>path.join(groupDir, sessionId)).filter((sessionDir)=>isDirectory(sessionDir));
95
+ }
96
+ /**
97
+ * Return the most recently active session subdirectory for a cwd, i.e. the
98
+ * ~/.grok/sessions/<encodeURIComponent(cwd)>/<id>/ whose chat_history.jsonl
99
+ * was written last. Returns null when the group dir or any transcript is
100
+ * missing.
101
+ */ latestSessionDir(cwd) {
102
+ const groupDir = this.getProjectDir(cwd);
103
+ if (!isDirectory(groupDir)) return null;
104
+ let best = null;
105
+ for (const sessionDir of this.listSessionDirs(groupDir)){
106
+ const stat = safeStat(path.join(sessionDir, CHAT_HISTORY_FILE));
107
+ if (!stat) continue;
108
+ if (!best || stat.mtimeMs > best.mtimeMs) {
109
+ best = {
110
+ dir: sessionDir,
111
+ mtimeMs: stat.mtimeMs
112
+ };
113
+ }
114
+ }
115
+ return best?.dir ?? null;
116
+ }
117
+ mapSessionToAgent(session, processInfo, sessionDir) {
118
+ const projectPath = session.projectPath || processInfo.cwd || '';
119
+ return {
120
+ name: generateAgentName(projectPath, processInfo.pid),
121
+ type: this.type,
122
+ status: this.determineStatus(session),
123
+ summary: session.summary || 'Grok CLI session active',
124
+ pid: processInfo.pid,
125
+ projectPath,
126
+ sessionId: session.sessionId,
127
+ lastActive: session.lastActive,
128
+ sessionFilePath: path.join(sessionDir, CHAT_HISTORY_FILE)
129
+ };
130
+ }
131
+ mapProcessOnlyAgent(processInfo, cwd) {
132
+ const projectPath = cwd || processInfo.cwd || '';
133
+ return {
134
+ name: generateAgentName(projectPath, processInfo.pid),
135
+ type: this.type,
136
+ status: AgentStatus.RUNNING,
137
+ summary: 'Grok CLI process running',
138
+ pid: processInfo.pid,
139
+ projectPath,
140
+ sessionId: `pid-${processInfo.pid}`,
141
+ lastActive: new Date()
142
+ };
143
+ }
144
+ getConversation(sessionFilePath, options) {
145
+ return this.parseChatHistory(this.resolveChatPath(sessionFilePath), options?.verbose ?? false).messages;
146
+ }
147
+ async listSessions(opts) {
148
+ if (!isDirectory(this.sessionsDir)) return [];
149
+ const filterCwd = opts?.cwd;
150
+ const summaries = [];
151
+ for (const groupName of safeReaddir(this.sessionsDir)){
152
+ const groupDir = path.join(this.sessionsDir, groupName);
153
+ if (!isDirectory(groupDir)) continue;
154
+ const decodedCwd = this.decodeGroupCwd(groupName, groupDir);
155
+ for (const sessionDir of this.listSessionDirs(groupDir)){
156
+ const session = this.readSession(sessionDir, decodedCwd);
157
+ if (!session) continue;
158
+ const cwd = session.projectPath || decodedCwd;
159
+ if (filterCwd !== undefined && cwd !== filterCwd) continue;
160
+ summaries.push({
161
+ type: this.type,
162
+ sessionId: session.sessionId,
163
+ cwd,
164
+ firstUserMessage: session.firstUserMessage || '',
165
+ lastActive: session.lastActive,
166
+ startedAt: session.sessionStart,
167
+ sessionFilePath: path.join(sessionDir, CHAT_HISTORY_FILE)
168
+ });
169
+ }
170
+ }
171
+ return summaries;
172
+ }
173
+ // --- Session parsing (chat_history.jsonl) ---
174
+ /**
175
+ * Parse a session directory into a {@link GrokSession} from its
176
+ * chat_history.jsonl transcript. Returns null when the transcript is
177
+ * missing — i.e. there is no real session to surface.
178
+ */ readSession(sessionDir, defaultCwd) {
179
+ const chatPath = path.join(sessionDir, CHAT_HISTORY_FILE);
180
+ const chatStat = safeStat(chatPath);
181
+ if (!chatStat) return null;
182
+ const scan = this.parseChatHistory(chatPath, false);
183
+ const dirStat = safeStat(sessionDir);
184
+ const lastActive = chatStat.mtime;
185
+ return {
186
+ sessionId: path.basename(sessionDir),
187
+ projectPath: defaultCwd || '',
188
+ summary: scan.lastUserMessage || 'Grok CLI session active',
189
+ sessionStart: dirStat?.birthtime || lastActive,
190
+ lastActive,
191
+ firstUserMessage: scan.firstUserMessage,
192
+ lastUserMessage: scan.lastUserMessage,
193
+ lastRole: scan.lastRole
194
+ };
195
+ }
196
+ /**
197
+ * Determine agent status from parsed session state.
198
+ *
199
+ * - past the idle threshold → IDLE
200
+ * - last transcript turn is an assistant message → WAITING (awaiting user)
201
+ * - otherwise (last turn was a user message, or unknown) → RUNNING
202
+ */ determineStatus(session) {
203
+ const diffMinutes = (Date.now() - session.lastActive.getTime()) / 60000;
204
+ if (diffMinutes > IDLE_THRESHOLD_MINUTES) {
205
+ return AgentStatus.IDLE;
206
+ }
207
+ if (session.lastRole === 'assistant') {
208
+ return AgentStatus.WAITING;
209
+ }
210
+ return AgentStatus.RUNNING;
211
+ }
212
+ /**
213
+ * Single pass over chat_history.jsonl. Each line is a
214
+ * { type: 'system' | 'user' | 'assistant', content } record where content is
215
+ * either a string or an array of { type: 'text', text } blocks.
216
+ *
217
+ * Grok wraps the real user prompt in <user_query>...</user_query>; the other
218
+ * user records are context injections (<user_info>, <system-reminder>, ...)
219
+ * and are skipped so the summary is the actual prompt, not boilerplate.
220
+ */ parseChatHistory(chatPath, verbose) {
221
+ const empty = {
222
+ messages: []
223
+ };
224
+ const content = safeReadFile(chatPath);
225
+ if (content === undefined) return empty;
226
+ const messages = [];
227
+ let lastRole;
228
+ for (const line of content.trim().split('\n')){
229
+ if (!line.trim()) continue;
230
+ let record;
231
+ try {
232
+ record = JSON.parse(line);
233
+ } catch {
234
+ continue;
235
+ }
236
+ const text = this.extractText(record.content);
237
+ if (record.type === 'user') {
238
+ const query = this.extractUserQuery(text);
239
+ if (query === null) continue; // context injection, not a real prompt
240
+ messages.push({
241
+ role: 'user',
242
+ content: query
243
+ });
244
+ lastRole = 'user';
245
+ } else if (record.type === 'assistant') {
246
+ if (!text) continue;
247
+ messages.push({
248
+ role: 'assistant',
249
+ content: text
250
+ });
251
+ lastRole = 'assistant';
252
+ } else if (verbose && record.type === 'system') {
253
+ if (!text) continue;
254
+ messages.push({
255
+ role: 'system',
256
+ content: text
257
+ });
258
+ }
259
+ }
260
+ const userTurns = messages.filter((m)=>m.role === 'user');
261
+ return {
262
+ messages,
263
+ firstUserMessage: userTurns[0]?.content,
264
+ lastUserMessage: userTurns[userTurns.length - 1]?.content,
265
+ lastRole
266
+ };
267
+ }
268
+ /** Flatten a chat record's content (string or text-block array) to text. */ extractText(content) {
269
+ if (typeof content === 'string') return content;
270
+ if (Array.isArray(content)) {
271
+ return content.map((block)=>block && typeof block === 'object' && typeof block.text === 'string' ? block.text : '').join('');
272
+ }
273
+ return '';
274
+ }
275
+ /**
276
+ * Extract the prompt inside <user_query>...</user_query>. Returns null when
277
+ * the record has no such tag (a context injection rather than a prompt).
278
+ */ extractUserQuery(text) {
279
+ const match = text.match(/<user_query>\s*([\s\S]*?)\s*<\/user_query>/);
280
+ return match ? match[1].trim() : null;
281
+ }
282
+ /** Resolve a session dir or an explicit chat_history.jsonl path to the file. */ resolveChatPath(sessionPath) {
283
+ return sessionPath.endsWith('.jsonl') ? sessionPath : path.join(sessionPath, CHAT_HISTORY_FILE);
284
+ }
285
+ getProjectDir(cwd) {
286
+ return path.join(this.sessionsDir, encodeURIComponent(cwd));
287
+ }
288
+ /**
289
+ * Resolve the working directory a session group dir was created for.
290
+ *
291
+ * The common case is `decodeURIComponent(<group-name>)`. For paths whose
292
+ * encoded form exceeds the filesystem limit Grok uses a slug+hash and records
293
+ * the original path in a `.cwd` file inside the group — prefer that when
294
+ * present.
295
+ */ decodeGroupCwd(groupName, groupDir) {
296
+ const fromFile = safeReadFile(path.join(groupDir, CWD_FILE));
297
+ if (fromFile !== undefined && fromFile.trim()) return fromFile.trim();
298
+ try {
299
+ return decodeURIComponent(groupName);
300
+ } catch {
301
+ return '';
302
+ }
303
+ }
304
+ }
305
+
306
+ //# sourceMappingURL=GrokCliAdapter.js.map