@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/print/PrintAgentStore.ts"],"sourcesContent":["import fs from 'fs';\nimport os from 'os';\nimport path from 'path';\nimport { randomUUID } from 'crypto';\nimport { execFileSync } from 'child_process';\nimport type { PrintAgent, ProcessIdentity, PrintRunStatus, PrintSessionHealth } from './PrintAgent.js';\nimport {\n PrintAgentBusyError,\n PrintAgentNameConflictError,\n PrintAgentNotFoundError,\n PrintAgentStoreError,\n} from './PrintAgent.js';\n\ninterface PrintAgentStoreFile {\n version: 1;\n agents: PrintAgent[];\n}\n\nexport interface CreatePrintAgentInput {\n name: string;\n cwd: string;\n}\n\nexport interface PrintAgentStoreOptions {\n filePath?: string;\n lockTimeoutMs?: number;\n now?: () => Date;\n processInspector?: ProcessInspector;\n incompleteLockGraceMs?: number;\n mutationLockStaleMs?: number;\n}\n\nexport interface ProcessInspector {\n getIdentity(pid: number): ProcessIdentity | null;\n}\n\nexport interface PrintRunCompletion {\n status: PrintRunStatus;\n exitCode: number | null;\n summary: string;\n sessionHealth: PrintSessionHealth;\n}\n\nconst DEFAULT_FILE = path.join(os.homedir(), '.ai-devkit', 'print-agents.json');\n\nexport class PrintAgentStore {\n readonly filePath: string;\n private readonly lockPath: string;\n private readonly lockTimeoutMs: number;\n private readonly now: () => Date;\n private readonly processInspector: ProcessInspector;\n private readonly runLocksRoot: string;\n private readonly incompleteLockGraceMs: number;\n private readonly mutationLockStaleMs: number;\n\n constructor(options: PrintAgentStoreOptions = {}) {\n this.filePath = options.filePath ?? DEFAULT_FILE;\n this.lockPath = `${this.filePath}.lock`;\n this.lockTimeoutMs = options.lockTimeoutMs ?? 2000;\n this.now = options.now ?? (() => new Date());\n this.processInspector = options.processInspector ?? new LocalProcessInspector();\n this.runLocksRoot = path.join(path.dirname(this.filePath), 'print-agent-locks');\n this.incompleteLockGraceMs = options.incompleteLockGraceMs ?? 30_000;\n this.mutationLockStaleMs = options.mutationLockStaleMs ?? 30_000;\n }\n\n async create(input: CreatePrintAgentInput): Promise<PrintAgent> {\n const cwd = this.canonicalDirectory(input.cwd);\n return this.withMutationLock(async () => {\n const data = this.readFile();\n if (data.agents.some((agent) => agent.name.toLowerCase() === input.name.toLowerCase())) {\n throw new PrintAgentNameConflictError(input.name);\n }\n const timestamp = this.now().toISOString();\n let id = randomUUID();\n let providerSessionId = randomUUID();\n while (providerSessionId === id) providerSessionId = randomUUID();\n while (data.agents.some((agent) => agent.id === id)) id = randomUUID();\n const agent: PrintAgent = {\n id,\n name: input.name,\n provider: 'claude',\n mode: 'print',\n cwd,\n providerSessionId,\n state: 'ready',\n sessionHealth: 'uninitialized',\n createdAt: timestamp,\n updatedAt: timestamp,\n lastActiveAt: null,\n lastResult: null,\n activeRun: null,\n };\n data.agents.push(agent);\n this.writeFile(data);\n return structuredClone(agent);\n });\n }\n\n async list(): Promise<PrintAgent[]> {\n await this.reconcile();\n return this.listRaw();\n }\n\n async getById(id: string): Promise<PrintAgent | null> {\n return (await this.list()).find((agent) => agent.id === id) ?? null;\n }\n\n async reconcile(): Promise<void> {\n const running = this.listRaw().filter((agent) => agent.state === 'running' && agent.activeRun);\n for (const snapshot of running) {\n const lockPath = this.runLockPath(snapshot.id);\n const metadata = this.readRunLock(snapshot.id);\n if (metadata && this.isActive(metadata)) continue;\n if (!metadata && this.isYoungLock(lockPath)) continue;\n\n if (fs.existsSync(lockPath)) {\n const quarantine = `${lockPath}.stale-${randomUUID()}`;\n try {\n fs.renameSync(lockPath, quarantine);\n this.removeLockDirectory(quarantine);\n } catch {\n continue;\n }\n }\n const completedAt = this.now().toISOString();\n await this.updateAgent(snapshot.id, (current) => {\n if (current.state !== 'running' || current.activeRun?.token !== snapshot.activeRun?.token) return current;\n return {\n ...current,\n state: 'degraded',\n sessionHealth: 'unknown',\n activeRun: null,\n updatedAt: completedAt,\n lastActiveAt: completedAt,\n lastResult: {\n status: 'interrupted',\n completedAt,\n exitCode: null,\n summary: 'Previous print run was interrupted.',\n },\n };\n });\n }\n }\n\n async resolve(reference: string): Promise<PrintAgent | PrintAgent[] | null> {\n const agents = await this.list();\n const byId = agents.find((agent) => agent.id === reference);\n if (byId) return byId;\n const matches = agents.filter((agent) => agent.name.toLowerCase() === reference.toLowerCase());\n if (matches.length === 0) return null;\n return matches.length === 1 ? matches[0]! : matches;\n }\n\n async acquireRun(id: string): Promise<{ agent: PrintAgent; token: string }> {\n const existing = await this.getById(id);\n if (!existing) throw new PrintAgentNotFoundError(id);\n this.validateBoundCwd(existing.cwd);\n const runLock = this.runLockPath(id);\n let recoveredStale = false;\n\n for (;;) {\n this.ensureRunLocksRoot();\n this.assertNotSymlink(runLock);\n try {\n fs.mkdirSync(runLock, { mode: 0o700 });\n break;\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code !== 'EEXIST') {\n throw new PrintAgentStoreError(`Cannot acquire print-agent run lock: ${(error as Error).message}`);\n }\n const metadata = this.readRunLock(id);\n if (!metadata || this.isActive(metadata)) {\n throw new PrintAgentBusyError(id, existing.name);\n }\n const quarantine = `${runLock}.stale-${randomUUID()}`;\n try {\n fs.renameSync(runLock, quarantine);\n this.removeLockDirectory(quarantine);\n recoveredStale = true;\n } catch {\n // Another contender changed the lock. Retry and inspect the winner.\n }\n }\n }\n\n const owner = this.processInspector.getIdentity(process.pid);\n if (!owner) {\n this.removeLockDirectory(runLock);\n throw new PrintAgentStoreError('Cannot determine the current process identity.');\n }\n const token = randomUUID();\n const startedAt = this.now().toISOString();\n const activeRun = { token, owner, provider: null, startedAt };\n this.writeRunLock(id, activeRun);\n\n try {\n const agent = await this.updateAgent(id, (current) => ({\n ...current,\n state: 'running',\n activeRun,\n updatedAt: startedAt,\n ...(recoveredStale ? {\n sessionHealth: 'unknown' as const,\n lastResult: {\n status: 'interrupted' as const,\n completedAt: startedAt,\n exitCode: null,\n summary: 'Previous print run was interrupted.',\n },\n } : {}),\n }));\n return { agent, token };\n } catch (error) {\n this.removeOwnedRunLock(id, token);\n throw error;\n }\n }\n\n async recordProviderProcess(id: string, token: string, identity: ProcessIdentity): Promise<void> {\n const metadata = this.requireOwnedRun(id, token);\n const next = { ...metadata, provider: identity };\n this.writeRunLock(id, next);\n await this.updateAgent(id, (agent) => {\n if (agent.activeRun?.token !== token) throw new PrintAgentStoreError('Print run ownership changed.');\n return { ...agent, activeRun: next, updatedAt: this.now().toISOString() };\n });\n }\n\n async completeRun(id: string, token: string, result: PrintRunCompletion): Promise<PrintAgent> {\n this.requireOwnedRun(id, token);\n const completedAt = this.now().toISOString();\n const agent = await this.updateAgent(id, (current) => {\n if (current.activeRun?.token !== token) throw new PrintAgentStoreError('Print run ownership changed.');\n return {\n ...current,\n state: result.status === 'succeeded' ? 'ready' : 'degraded',\n sessionHealth: result.sessionHealth,\n activeRun: null,\n lastActiveAt: completedAt,\n updatedAt: completedAt,\n lastResult: {\n status: result.status,\n completedAt,\n exitCode: result.exitCode,\n summary: result.summary.slice(0, 4096),\n },\n };\n });\n this.removeOwnedRunLock(id, token);\n return agent;\n }\n\n private canonicalDirectory(input: string): string {\n try {\n const resolved = fs.realpathSync(input);\n if (!fs.statSync(resolved).isDirectory()) throw new Error('not a directory');\n return resolved;\n } catch {\n throw new PrintAgentStoreError(`Print agent cwd is not an existing directory: ${input}`);\n }\n }\n\n private validateBoundCwd(bound: string): void {\n try {\n const stat = fs.lstatSync(bound);\n if (!stat.isDirectory() || stat.isSymbolicLink() || fs.realpathSync(bound) !== bound) {\n throw new Error('binding changed');\n }\n } catch {\n throw new PrintAgentStoreError(`Print agent cwd binding is no longer safe: ${bound}`);\n }\n }\n\n private ensureSafeParent(): string {\n const parent = path.dirname(this.filePath);\n fs.mkdirSync(parent, { recursive: true, mode: 0o700 });\n const stat = fs.lstatSync(parent);\n if (!stat.isDirectory() || stat.isSymbolicLink()) {\n throw new PrintAgentStoreError(`Unsafe print-agent store directory: ${parent}`);\n }\n return parent;\n }\n\n private ensureRunLocksRoot(): void {\n this.ensureSafeParent();\n fs.mkdirSync(this.runLocksRoot, { recursive: true, mode: 0o700 });\n const stat = fs.lstatSync(this.runLocksRoot);\n if (!stat.isDirectory() || stat.isSymbolicLink()) {\n throw new PrintAgentStoreError(`Unsafe print-agent lock directory: ${this.runLocksRoot}`);\n }\n }\n\n private assertNotSymlink(target: string): void {\n try {\n if (fs.lstatSync(target).isSymbolicLink()) {\n throw new PrintAgentStoreError(`Unsafe symbolic link in print-agent storage: ${target}`);\n }\n } catch (error) {\n if (error instanceof PrintAgentStoreError) throw error;\n if ((error as NodeJS.ErrnoException).code !== 'ENOENT') {\n throw new PrintAgentStoreError(`Cannot inspect print-agent storage: ${target}`);\n }\n }\n }\n\n private readFile(): PrintAgentStoreFile {\n this.ensureSafeParent();\n this.assertNotSymlink(this.filePath);\n if (!fs.existsSync(this.filePath)) return { version: 1, agents: [] };\n try {\n const parsed = JSON.parse(fs.readFileSync(this.filePath, 'utf8')) as unknown;\n if (!this.isStoreFile(parsed)) throw new Error('invalid schema');\n return parsed;\n } catch {\n throw new PrintAgentStoreError(`Invalid print-agent store: ${this.filePath}`);\n }\n }\n\n private listRaw(): PrintAgent[] {\n return this.readFile().agents.map((agent) => structuredClone(agent));\n }\n\n private isStoreFile(value: unknown): value is PrintAgentStoreFile {\n if (!value || typeof value !== 'object') return false;\n const record = value as Record<string, unknown>;\n return record.version === 1 && Array.isArray(record.agents);\n }\n\n private writeFile(data: PrintAgentStoreFile): void {\n const parent = this.ensureSafeParent();\n this.assertNotSymlink(this.filePath);\n const temp = path.join(parent, `.print-agents-${process.pid}-${randomUUID()}.tmp`);\n this.assertNotSymlink(temp);\n let fd: number | undefined;\n try {\n fd = fs.openSync(temp, 'wx', 0o600);\n fs.writeFileSync(fd, JSON.stringify(data, null, 2), 'utf8');\n fs.fsyncSync(fd);\n fs.closeSync(fd);\n fd = undefined;\n fs.renameSync(temp, this.filePath);\n fs.chmodSync(this.filePath, 0o600);\n } catch (error) {\n if (fd !== undefined) fs.closeSync(fd);\n try { fs.unlinkSync(temp); } catch { /* best effort */ }\n if (error instanceof PrintAgentStoreError) throw error;\n throw new PrintAgentStoreError(`Failed to update print-agent store: ${(error as Error).message}`);\n }\n }\n\n private async withMutationLock<T>(operation: () => Promise<T>): Promise<T> {\n this.ensureSafeParent();\n const started = Date.now();\n for (;;) {\n this.assertNotSymlink(this.lockPath);\n try {\n fs.mkdirSync(this.lockPath, { mode: 0o700 });\n break;\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code !== 'EEXIST') {\n throw new PrintAgentStoreError(`Cannot acquire print-agent store lock: ${(error as Error).message}`);\n }\n if (!this.isYoungMutationLock()) {\n const quarantine = `${this.lockPath}.stale-${randomUUID()}`;\n try {\n fs.renameSync(this.lockPath, quarantine);\n fs.rmdirSync(quarantine);\n continue;\n } catch {\n // Another contender changed the lock. Retry until the bounded timeout.\n }\n }\n if (Date.now() - started >= this.lockTimeoutMs) {\n throw new PrintAgentStoreError('Timed out acquiring print-agent store lock.');\n }\n await new Promise((resolve) => setTimeout(resolve, 10));\n }\n }\n try {\n return await operation();\n } finally {\n try { fs.rmdirSync(this.lockPath); } catch { /* surfaced by later contention */ }\n }\n }\n\n private isYoungMutationLock(): boolean {\n try {\n this.assertNotSymlink(this.lockPath);\n const stat = fs.statSync(this.lockPath);\n return stat.isDirectory() && Date.now() - stat.mtimeMs < this.mutationLockStaleMs;\n } catch {\n return false;\n }\n }\n\n private async updateAgent(id: string, update: (agent: PrintAgent) => PrintAgent): Promise<PrintAgent> {\n return this.withMutationLock(async () => {\n const data = this.readFile();\n const index = data.agents.findIndex((agent) => agent.id === id);\n if (index < 0) throw new PrintAgentNotFoundError(id);\n const next = update(data.agents[index]!);\n data.agents[index] = next;\n this.writeFile(data);\n return structuredClone(next);\n });\n }\n\n private runLockPath(id: string): string {\n if (!/^[0-9a-f-]{36}$/i.test(id)) throw new PrintAgentStoreError('Invalid print-agent id.');\n return path.join(this.runLocksRoot, `${id}.lock`);\n }\n\n private readRunLock(id: string): import('./PrintAgent.js').PrintActiveRun | null {\n const ownerPath = path.join(this.runLockPath(id), 'owner.json');\n try {\n this.assertNotSymlink(ownerPath);\n const value = JSON.parse(fs.readFileSync(ownerPath, 'utf8')) as import('./PrintAgent.js').PrintActiveRun;\n if (!value || typeof value.token !== 'string' || !value.owner || typeof value.owner.pid !== 'number') return null;\n return value;\n } catch {\n return null;\n }\n }\n\n private writeRunLock(id: string, metadata: import('./PrintAgent.js').PrintActiveRun): void {\n const lockPath = this.runLockPath(id);\n const ownerPath = path.join(lockPath, 'owner.json');\n const tempPath = path.join(lockPath, `.owner-${randomUUID()}.tmp`);\n this.assertNotSymlink(lockPath);\n this.assertNotSymlink(ownerPath);\n fs.writeFileSync(tempPath, JSON.stringify(metadata), { encoding: 'utf8', mode: 0o600, flag: 'wx' });\n fs.renameSync(tempPath, ownerPath);\n }\n\n private requireOwnedRun(id: string, token: string): import('./PrintAgent.js').PrintActiveRun {\n const metadata = this.readRunLock(id);\n if (!metadata || metadata.token !== token) throw new PrintAgentStoreError('Print run ownership changed.');\n return metadata;\n }\n\n private isActive(metadata: import('./PrintAgent.js').PrintActiveRun): boolean {\n return this.sameProcess(metadata.owner) || (metadata.provider !== null && this.sameProcess(metadata.provider));\n }\n\n private sameProcess(expected: ProcessIdentity): boolean {\n const actual = this.processInspector.getIdentity(expected.pid);\n return actual !== null && actual.startedAt === expected.startedAt;\n }\n\n private isYoungLock(lockPath: string): boolean {\n try {\n this.assertNotSymlink(lockPath);\n return Date.now() - fs.statSync(lockPath).mtimeMs < this.incompleteLockGraceMs;\n } catch {\n return false;\n }\n }\n\n private removeOwnedRunLock(id: string, token: string): void {\n const metadata = this.readRunLock(id);\n if (!metadata || metadata.token !== token) return;\n this.removeLockDirectory(this.runLockPath(id));\n }\n\n private removeLockDirectory(lockPath: string): void {\n this.assertNotSymlink(lockPath);\n try {\n for (const name of fs.readdirSync(lockPath)) {\n const entry = path.join(lockPath, name);\n this.assertNotSymlink(entry);\n if (!fs.lstatSync(entry).isFile()) throw new PrintAgentStoreError(`Unsafe entry in print-agent lock: ${entry}`);\n fs.unlinkSync(entry);\n }\n fs.rmdirSync(lockPath);\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error;\n }\n }\n}\n\nexport class LocalProcessInspector implements ProcessInspector {\n getIdentity(pid: number): ProcessIdentity | null {\n if (!Number.isInteger(pid) || pid <= 0) return null;\n try {\n if (process.platform === 'linux') {\n const stat = fs.readFileSync(`/proc/${pid}/stat`, 'utf8');\n const close = stat.lastIndexOf(')');\n const fields = stat.slice(close + 2).split(' ');\n const startTicks = fields[19];\n if (!startTicks) return null;\n return { pid, startedAt: `linux:${startTicks}` };\n }\n const startedAt = execFileSync('ps', ['-o', 'lstart=', '-p', String(pid)], {\n encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'],\n }).trim();\n return startedAt ? { pid, startedAt } : null;\n } catch {\n return null;\n }\n }\n}\n"],"names":["fs","os","path","randomUUID","execFileSync","PrintAgentBusyError","PrintAgentNameConflictError","PrintAgentNotFoundError","PrintAgentStoreError","DEFAULT_FILE","join","homedir","PrintAgentStore","filePath","lockPath","lockTimeoutMs","now","processInspector","runLocksRoot","incompleteLockGraceMs","mutationLockStaleMs","options","Date","LocalProcessInspector","dirname","create","input","cwd","canonicalDirectory","withMutationLock","data","readFile","agents","some","agent","name","toLowerCase","timestamp","toISOString","id","providerSessionId","provider","mode","state","sessionHealth","createdAt","updatedAt","lastActiveAt","lastResult","activeRun","push","writeFile","structuredClone","list","reconcile","listRaw","getById","find","running","filter","snapshot","runLockPath","metadata","readRunLock","isActive","isYoungLock","existsSync","quarantine","renameSync","removeLockDirectory","completedAt","updateAgent","current","token","status","exitCode","summary","resolve","reference","byId","matches","length","acquireRun","existing","validateBoundCwd","runLock","recoveredStale","ensureRunLocksRoot","assertNotSymlink","mkdirSync","error","code","message","owner","getIdentity","process","pid","startedAt","writeRunLock","removeOwnedRunLock","recordProviderProcess","identity","requireOwnedRun","next","completeRun","result","slice","resolved","realpathSync","statSync","isDirectory","Error","bound","stat","lstatSync","isSymbolicLink","ensureSafeParent","parent","recursive","target","version","parsed","JSON","parse","readFileSync","isStoreFile","map","value","record","Array","isArray","temp","fd","openSync","writeFileSync","stringify","fsyncSync","closeSync","undefined","chmodSync","unlinkSync","operation","started","isYoungMutationLock","rmdirSync","Promise","setTimeout","mtimeMs","update","index","findIndex","test","ownerPath","tempPath","encoding","flag","sameProcess","expected","actual","readdirSync","entry","isFile","Number","isInteger","platform","close","lastIndexOf","fields","split","startTicks","String","stdio","trim"],"mappings":"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"}
@@ -7,7 +7,7 @@ export declare class TmuxManager {
7
7
  /**
8
8
  * Find the actual agent process PID inside a tmux pane.
9
9
  *
10
- * Strategy: BFS the process tree, return the deepest descendant whose
10
+ * Strategy: BFS the process tree, return the deepest process whose
11
11
  * `ps` command line is accepted by `matches`. The caller supplies the
12
12
  * matcher so this method has no agent-type knowledge.
13
13
  *
@@ -17,7 +17,7 @@ export declare class TmuxManager {
17
17
  * - Subprocess case: shell → claude (matches) → MCP server child (doesn't match)
18
18
  * → returns claude, not the subprocess
19
19
  *
20
- * Returns null when no descendant matches yet (agent still starting); the
20
+ * Returns null when no process matches yet (agent still starting); the
21
21
  * caller's poll loop retries.
22
22
  */
23
23
  findAgentPid(session: string, matches: (psCommand: string) => boolean): Promise<number | null>;
@@ -1 +1 @@
1
- {"version":3,"file":"TmuxManager.d.ts","sourceRoot":"","sources":["../../src/terminal/TmuxManager.ts"],"names":[],"mappings":"AAKA,qBAAa,WAAW;IACd,WAAW,IAAI,OAAO,CAAC,OAAO,CAAC;IAS/B,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAS7C,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAIvD,QAAQ,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAItD,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAQ9C;;;;;;;;;;;;;;;OAeG;IACG,YAAY,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,OAAO,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;YA2BtF,UAAU;YAYV,aAAa;YAab,iBAAiB;CASlC"}
1
+ {"version":3,"file":"TmuxManager.d.ts","sourceRoot":"","sources":["../../src/terminal/TmuxManager.ts"],"names":[],"mappings":"AAKA,qBAAa,WAAW;IACd,WAAW,IAAI,OAAO,CAAC,OAAO,CAAC;IAS/B,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAS7C,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAIvD,QAAQ,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAItD,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAQ9C;;;;;;;;;;;;;;;OAeG;IACG,YAAY,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,OAAO,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;YAyBtF,UAAU;YAYV,aAAa;YAab,iBAAiB;CASlC"}
@@ -57,7 +57,7 @@ export class TmuxManager {
57
57
  /**
58
58
  * Find the actual agent process PID inside a tmux pane.
59
59
  *
60
- * Strategy: BFS the process tree, return the deepest descendant whose
60
+ * Strategy: BFS the process tree, return the deepest process whose
61
61
  * `ps` command line is accepted by `matches`. The caller supplies the
62
62
  * matcher so this method has no agent-type knowledge.
63
63
  *
@@ -67,7 +67,7 @@ export class TmuxManager {
67
67
  * - Subprocess case: shell → claude (matches) → MCP server child (doesn't match)
68
68
  * → returns claude, not the subprocess
69
69
  *
70
- * Returns null when no descendant matches yet (agent still starting); the
70
+ * Returns null when no process matches yet (agent still starting); the
71
71
  * caller's poll loop retries.
72
72
  */ async findAgentPid(session, matches) {
73
73
  const panePid = await this.getPanePid(session);
@@ -81,11 +81,9 @@ export class TmuxManager {
81
81
  const pid = queue.shift();
82
82
  if (visited.has(pid)) continue;
83
83
  visited.add(pid);
84
- if (pid !== panePid) {
85
- const command = await this.getProcessCommand(pid);
86
- if (command && matches(command)) {
87
- deepestMatch = pid;
88
- }
84
+ const command = await this.getProcessCommand(pid);
85
+ if (command && matches(command)) {
86
+ deepestMatch = pid;
89
87
  }
90
88
  const children = await this.pgrepChildren(pid);
91
89
  queue.push(...children);
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/terminal/TmuxManager.ts"],"sourcesContent":["import { execFile } from 'child_process';\nimport { promisify } from 'util';\n\nconst execFileAsync = promisify(execFile);\n\nexport class TmuxManager {\n async isAvailable(): Promise<boolean> {\n try {\n await execFileAsync('tmux', ['-V']);\n return true;\n } catch {\n return false;\n }\n }\n\n async sessionExists(name: string): Promise<boolean> {\n try {\n await execFileAsync('tmux', ['has-session', '-t', name]);\n return true;\n } catch {\n return false;\n }\n }\n\n async createSession(name: string, cwd: string): Promise<void> {\n await execFileAsync('tmux', ['new-session', '-d', '-s', name, '-c', cwd]);\n }\n\n async sendKeys(session: string, keys: string): Promise<void> {\n await execFileAsync('tmux', ['send-keys', '-t', session, keys, 'Enter']);\n }\n\n async killSession(name: string): Promise<void> {\n try {\n await execFileAsync('tmux', ['kill-session', '-t', name]);\n } catch {\n // Already gone — ignore\n }\n }\n\n /**\n * Find the actual agent process PID inside a tmux pane.\n *\n * Strategy: BFS the process tree, return the deepest descendant whose\n * `ps` command line is accepted by `matches`. The caller supplies the\n * matcher so this method has no agent-type knowledge.\n *\n * This handles two real-world process shapes:\n * - Wrapper case: shell → claude-wrapper (matches) → claude (matches, deeper)\n * → returns the deeper one\n * - Subprocess case: shell → claude (matches) → MCP server child (doesn't match)\n * → returns claude, not the subprocess\n *\n * Returns null when no descendant matches yet (agent still starting); the\n * caller's poll loop retries.\n */\n async findAgentPid(session: string, matches: (psCommand: string) => boolean): Promise<number | null> {\n const panePid = await this.getPanePid(session);\n if (panePid === null) return null;\n\n const visited = new Set<number>();\n const queue: number[] = [panePid];\n let deepestMatch: number | null = null;\n\n while (queue.length > 0) {\n const pid = queue.shift()!;\n if (visited.has(pid)) continue;\n visited.add(pid);\n\n if (pid !== panePid) {\n const command = await this.getProcessCommand(pid);\n if (command && matches(command)) {\n deepestMatch = pid;\n }\n }\n\n const children = await this.pgrepChildren(pid);\n queue.push(...children);\n }\n\n return deepestMatch;\n }\n\n private async getPanePid(session: string): Promise<number | null> {\n try {\n const { stdout } = await execFileAsync('tmux', [\n 'list-panes', '-t', session, '-F', '#{pane_pid}',\n ]);\n const panePid = parseInt(stdout.trim().split('\\n')[0], 10);\n return isNaN(panePid) ? null : panePid;\n } catch {\n return null;\n }\n }\n\n private async pgrepChildren(pid: number): Promise<number[]> {\n try {\n const { stdout } = await execFileAsync('pgrep', ['-P', String(pid)]);\n return stdout\n .trim()\n .split('\\n')\n .map((s) => parseInt(s, 10))\n .filter((n) => !isNaN(n));\n } catch {\n return [];\n }\n }\n\n private async getProcessCommand(pid: number): Promise<string | null> {\n try {\n const { stdout } = await execFileAsync('ps', ['-p', String(pid), '-o', 'command=']);\n const trimmed = stdout.trim();\n return trimmed || null;\n } catch {\n return null;\n }\n }\n}\n"],"names":["execFile","promisify","execFileAsync","TmuxManager","isAvailable","sessionExists","name","createSession","cwd","sendKeys","session","keys","killSession","findAgentPid","matches","panePid","getPanePid","visited","Set","queue","deepestMatch","length","pid","shift","has","add","command","getProcessCommand","children","pgrepChildren","push","stdout","parseInt","trim","split","isNaN","String","map","s","filter","n","trimmed"],"mappings":"AAAA,SAASA,QAAQ,QAAQ,gBAAgB;AACzC,SAASC,SAAS,QAAQ,OAAO;AAEjC,MAAMC,gBAAgBD,UAAUD;AAEhC,OAAO,MAAMG;IACT,MAAMC,cAAgC;QAClC,IAAI;YACA,MAAMF,cAAc,QAAQ;gBAAC;aAAK;YAClC,OAAO;QACX,EAAE,OAAM;YACJ,OAAO;QACX;IACJ;IAEA,MAAMG,cAAcC,IAAY,EAAoB;QAChD,IAAI;YACA,MAAMJ,cAAc,QAAQ;gBAAC;gBAAe;gBAAMI;aAAK;YACvD,OAAO;QACX,EAAE,OAAM;YACJ,OAAO;QACX;IACJ;IAEA,MAAMC,cAAcD,IAAY,EAAEE,GAAW,EAAiB;QAC1D,MAAMN,cAAc,QAAQ;YAAC;YAAe;YAAM;YAAMI;YAAM;YAAME;SAAI;IAC5E;IAEA,MAAMC,SAASC,OAAe,EAAEC,IAAY,EAAiB;QACzD,MAAMT,cAAc,QAAQ;YAAC;YAAa;YAAMQ;YAASC;YAAM;SAAQ;IAC3E;IAEA,MAAMC,YAAYN,IAAY,EAAiB;QAC3C,IAAI;YACA,MAAMJ,cAAc,QAAQ;gBAAC;gBAAgB;gBAAMI;aAAK;QAC5D,EAAE,OAAM;QACJ,wBAAwB;QAC5B;IACJ;IAEA;;;;;;;;;;;;;;;KAeC,GACD,MAAMO,aAAaH,OAAe,EAAEI,OAAuC,EAA0B;QACjG,MAAMC,UAAU,MAAM,IAAI,CAACC,UAAU,CAACN;QACtC,IAAIK,YAAY,MAAM,OAAO;QAE7B,MAAME,UAAU,IAAIC;QACpB,MAAMC,QAAkB;YAACJ;SAAQ;QACjC,IAAIK,eAA8B;QAElC,MAAOD,MAAME,MAAM,GAAG,EAAG;YACrB,MAAMC,MAAMH,MAAMI,KAAK;YACvB,IAAIN,QAAQO,GAAG,CAACF,MAAM;YACtBL,QAAQQ,GAAG,CAACH;YAEZ,IAAIA,QAAQP,SAAS;gBACjB,MAAMW,UAAU,MAAM,IAAI,CAACC,iBAAiB,CAACL;gBAC7C,IAAII,WAAWZ,QAAQY,UAAU;oBAC7BN,eAAeE;gBACnB;YACJ;YAEA,MAAMM,WAAW,MAAM,IAAI,CAACC,aAAa,CAACP;YAC1CH,MAAMW,IAAI,IAAIF;QAClB;QAEA,OAAOR;IACX;IAEA,MAAcJ,WAAWN,OAAe,EAA0B;QAC9D,IAAI;YACA,MAAM,EAAEqB,MAAM,EAAE,GAAG,MAAM7B,cAAc,QAAQ;gBAC3C;gBAAc;gBAAMQ;gBAAS;gBAAM;aACtC;YACD,MAAMK,UAAUiB,SAASD,OAAOE,IAAI,GAAGC,KAAK,CAAC,KAAK,CAAC,EAAE,EAAE;YACvD,OAAOC,MAAMpB,WAAW,OAAOA;QACnC,EAAE,OAAM;YACJ,OAAO;QACX;IACJ;IAEA,MAAcc,cAAcP,GAAW,EAAqB;QACxD,IAAI;YACA,MAAM,EAAES,MAAM,EAAE,GAAG,MAAM7B,cAAc,SAAS;gBAAC;gBAAMkC,OAAOd;aAAK;YACnE,OAAOS,OACFE,IAAI,GACJC,KAAK,CAAC,MACNG,GAAG,CAAC,CAACC,IAAMN,SAASM,GAAG,KACvBC,MAAM,CAAC,CAACC,IAAM,CAACL,MAAMK;QAC9B,EAAE,OAAM;YACJ,OAAO,EAAE;QACb;IACJ;IAEA,MAAcb,kBAAkBL,GAAW,EAA0B;QACjE,IAAI;YACA,MAAM,EAAES,MAAM,EAAE,GAAG,MAAM7B,cAAc,MAAM;gBAAC;gBAAMkC,OAAOd;gBAAM;gBAAM;aAAW;YAClF,MAAMmB,UAAUV,OAAOE,IAAI;YAC3B,OAAOQ,WAAW;QACtB,EAAE,OAAM;YACJ,OAAO;QACX;IACJ;AACJ"}
1
+ {"version":3,"sources":["../../src/terminal/TmuxManager.ts"],"sourcesContent":["import { execFile } from 'child_process';\nimport { promisify } from 'util';\n\nconst execFileAsync = promisify(execFile);\n\nexport class TmuxManager {\n async isAvailable(): Promise<boolean> {\n try {\n await execFileAsync('tmux', ['-V']);\n return true;\n } catch {\n return false;\n }\n }\n\n async sessionExists(name: string): Promise<boolean> {\n try {\n await execFileAsync('tmux', ['has-session', '-t', name]);\n return true;\n } catch {\n return false;\n }\n }\n\n async createSession(name: string, cwd: string): Promise<void> {\n await execFileAsync('tmux', ['new-session', '-d', '-s', name, '-c', cwd]);\n }\n\n async sendKeys(session: string, keys: string): Promise<void> {\n await execFileAsync('tmux', ['send-keys', '-t', session, keys, 'Enter']);\n }\n\n async killSession(name: string): Promise<void> {\n try {\n await execFileAsync('tmux', ['kill-session', '-t', name]);\n } catch {\n // Already gone — ignore\n }\n }\n\n /**\n * Find the actual agent process PID inside a tmux pane.\n *\n * Strategy: BFS the process tree, return the deepest process whose\n * `ps` command line is accepted by `matches`. The caller supplies the\n * matcher so this method has no agent-type knowledge.\n *\n * This handles two real-world process shapes:\n * - Wrapper case: shell → claude-wrapper (matches) → claude (matches, deeper)\n * → returns the deeper one\n * - Subprocess case: shell → claude (matches) → MCP server child (doesn't match)\n * → returns claude, not the subprocess\n *\n * Returns null when no process matches yet (agent still starting); the\n * caller's poll loop retries.\n */\n async findAgentPid(session: string, matches: (psCommand: string) => boolean): Promise<number | null> {\n const panePid = await this.getPanePid(session);\n if (panePid === null) return null;\n\n const visited = new Set<number>();\n const queue: number[] = [panePid];\n let deepestMatch: number | null = null;\n\n while (queue.length > 0) {\n const pid = queue.shift()!;\n if (visited.has(pid)) continue;\n visited.add(pid);\n\n const command = await this.getProcessCommand(pid);\n if (command && matches(command)) {\n deepestMatch = pid;\n }\n\n const children = await this.pgrepChildren(pid);\n queue.push(...children);\n }\n\n return deepestMatch;\n }\n\n private async getPanePid(session: string): Promise<number | null> {\n try {\n const { stdout } = await execFileAsync('tmux', [\n 'list-panes', '-t', session, '-F', '#{pane_pid}',\n ]);\n const panePid = parseInt(stdout.trim().split('\\n')[0], 10);\n return isNaN(panePid) ? null : panePid;\n } catch {\n return null;\n }\n }\n\n private async pgrepChildren(pid: number): Promise<number[]> {\n try {\n const { stdout } = await execFileAsync('pgrep', ['-P', String(pid)]);\n return stdout\n .trim()\n .split('\\n')\n .map((s) => parseInt(s, 10))\n .filter((n) => !isNaN(n));\n } catch {\n return [];\n }\n }\n\n private async getProcessCommand(pid: number): Promise<string | null> {\n try {\n const { stdout } = await execFileAsync('ps', ['-p', String(pid), '-o', 'command=']);\n const trimmed = stdout.trim();\n return trimmed || null;\n } catch {\n return null;\n }\n }\n}\n"],"names":["execFile","promisify","execFileAsync","TmuxManager","isAvailable","sessionExists","name","createSession","cwd","sendKeys","session","keys","killSession","findAgentPid","matches","panePid","getPanePid","visited","Set","queue","deepestMatch","length","pid","shift","has","add","command","getProcessCommand","children","pgrepChildren","push","stdout","parseInt","trim","split","isNaN","String","map","s","filter","n","trimmed"],"mappings":"AAAA,SAASA,QAAQ,QAAQ,gBAAgB;AACzC,SAASC,SAAS,QAAQ,OAAO;AAEjC,MAAMC,gBAAgBD,UAAUD;AAEhC,OAAO,MAAMG;IACT,MAAMC,cAAgC;QAClC,IAAI;YACA,MAAMF,cAAc,QAAQ;gBAAC;aAAK;YAClC,OAAO;QACX,EAAE,OAAM;YACJ,OAAO;QACX;IACJ;IAEA,MAAMG,cAAcC,IAAY,EAAoB;QAChD,IAAI;YACA,MAAMJ,cAAc,QAAQ;gBAAC;gBAAe;gBAAMI;aAAK;YACvD,OAAO;QACX,EAAE,OAAM;YACJ,OAAO;QACX;IACJ;IAEA,MAAMC,cAAcD,IAAY,EAAEE,GAAW,EAAiB;QAC1D,MAAMN,cAAc,QAAQ;YAAC;YAAe;YAAM;YAAMI;YAAM;YAAME;SAAI;IAC5E;IAEA,MAAMC,SAASC,OAAe,EAAEC,IAAY,EAAiB;QACzD,MAAMT,cAAc,QAAQ;YAAC;YAAa;YAAMQ;YAASC;YAAM;SAAQ;IAC3E;IAEA,MAAMC,YAAYN,IAAY,EAAiB;QAC3C,IAAI;YACA,MAAMJ,cAAc,QAAQ;gBAAC;gBAAgB;gBAAMI;aAAK;QAC5D,EAAE,OAAM;QACJ,wBAAwB;QAC5B;IACJ;IAEA;;;;;;;;;;;;;;;KAeC,GACD,MAAMO,aAAaH,OAAe,EAAEI,OAAuC,EAA0B;QACjG,MAAMC,UAAU,MAAM,IAAI,CAACC,UAAU,CAACN;QACtC,IAAIK,YAAY,MAAM,OAAO;QAE7B,MAAME,UAAU,IAAIC;QACpB,MAAMC,QAAkB;YAACJ;SAAQ;QACjC,IAAIK,eAA8B;QAElC,MAAOD,MAAME,MAAM,GAAG,EAAG;YACrB,MAAMC,MAAMH,MAAMI,KAAK;YACvB,IAAIN,QAAQO,GAAG,CAACF,MAAM;YACtBL,QAAQQ,GAAG,CAACH;YAEZ,MAAMI,UAAU,MAAM,IAAI,CAACC,iBAAiB,CAACL;YAC7C,IAAII,WAAWZ,QAAQY,UAAU;gBAC7BN,eAAeE;YACnB;YAEA,MAAMM,WAAW,MAAM,IAAI,CAACC,aAAa,CAACP;YAC1CH,MAAMW,IAAI,IAAIF;QAClB;QAEA,OAAOR;IACX;IAEA,MAAcJ,WAAWN,OAAe,EAA0B;QAC9D,IAAI;YACA,MAAM,EAAEqB,MAAM,EAAE,GAAG,MAAM7B,cAAc,QAAQ;gBAC3C;gBAAc;gBAAMQ;gBAAS;gBAAM;aACtC;YACD,MAAMK,UAAUiB,SAASD,OAAOE,IAAI,GAAGC,KAAK,CAAC,KAAK,CAAC,EAAE,EAAE;YACvD,OAAOC,MAAMpB,WAAW,OAAOA;QACnC,EAAE,OAAM;YACJ,OAAO;QACX;IACJ;IAEA,MAAcc,cAAcP,GAAW,EAAqB;QACxD,IAAI;YACA,MAAM,EAAES,MAAM,EAAE,GAAG,MAAM7B,cAAc,SAAS;gBAAC;gBAAMkC,OAAOd;aAAK;YACnE,OAAOS,OACFE,IAAI,GACJC,KAAK,CAAC,MACNG,GAAG,CAAC,CAACC,IAAMN,SAASM,GAAG,KACvBC,MAAM,CAAC,CAACC,IAAM,CAACL,MAAMK;QAC9B,EAAE,OAAM;YACJ,OAAO,EAAE;QACb;IACJ;IAEA,MAAcb,kBAAkBL,GAAW,EAA0B;QACjE,IAAI;YACA,MAAM,EAAES,MAAM,EAAE,GAAG,MAAM7B,cAAc,MAAM;gBAAC;gBAAMkC,OAAOd;gBAAM;gBAAM;aAAW;YAClF,MAAMmB,UAAUV,OAAOE,IAAI;YAC3B,OAAOQ,WAAW;QACtB,EAAE,OAAM;YACJ,OAAO;QACX;IACJ;AACJ"}
@@ -1,5 +1,5 @@
1
1
  import type { AgentType } from '../adapters/AgentAdapter.js';
2
- export type StartableAgentType = Extract<AgentType, 'claude' | 'codex' | 'copilot' | 'gemini_cli' | 'opencode' | 'pi'>;
2
+ export type StartableAgentType = Extract<AgentType, 'claude' | 'codex' | 'copilot' | 'gemini_cli' | 'grok_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,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"}
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,UAAU,GAAG,IAAI,CAAC,CAAC;AAEpI,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,CAQ1D,CAAC"}
@@ -20,6 +20,10 @@ import path from 'path';
20
20
  command: 'gemini',
21
21
  matches: matchAnyToken('gemini')
22
22
  },
23
+ grok_cli: {
24
+ command: 'grok',
25
+ matches: matchArgv0('grok')
26
+ },
23
27
  opencode: {
24
28
  command: 'opencode',
25
29
  matches: matchArgv0('opencode')
@@ -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' | '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"}
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' | 'grok_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 grok_cli: { command: 'grok', matches: matchArgv0('grok') },\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","grok_cli","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;IAAQ;IAC/DO,UAAY;QAAET,SAAS;QAAYC,SAASC,WAAW;IAAY;IACnEQ,IAAY;QAAEV,SAAS;QAAYC,SAASU,iBAAiB;YAAC;SAAK;IAAE;AACzE,EAAE;AAEF,SAAST,WAAWU,IAAY;IAC5B,MAAMC,QAAQD,KAAKE,WAAW;IAC9B,OAAO,CAACC;QACJ,MAAMC,QAAQD,UAAUE,IAAI,GAAGC,KAAK,CAAC,MAAM,CAAC,EAAE;QAC9C,OAAOF,QAAQnB,KAAKsB,QAAQ,CAACH,OAAOF,WAAW,OAAOD,QAAQ;IAClE;AACJ;AAEA,SAASR,eAAeO,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,SAASN,cAAcK,IAAY;IAC/B,MAAMC,QAAQD,KAAKE,WAAW;IAC9B,OAAO,CAACC;QACJ,KAAK,MAAMC,SAASD,UAAUE,IAAI,GAAGC,KAAK,CAAC,OAAQ;YAC/C,IAAIrB,KAAKsB,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,CAAC5B,KAAKsB,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.24.0",
3
+ "version": "0.26.0",
4
4
  "type": "module",
5
5
  "description": "Standalone agent detection and management utilities for AI DevKit",
6
6
  "main": "dist/index.js",
@@ -35,7 +35,7 @@
35
35
  "directory": "packages/agent-manager"
36
36
  },
37
37
  "dependencies": {
38
- "better-sqlite3": "^12.6.2",
38
+ "better-sqlite3": "12.11.1",
39
39
  "uuid": "14.0.0"
40
40
  },
41
41
  "devDependencies": {
@@ -0,0 +1,307 @@
1
+ /**
2
+ * Tests for GrokCliAdapter
3
+ *
4
+ * The adapter resolves a live process to its cwd via ~/.grok/active_sessions.json
5
+ * and reads session details from chat_history.jsonl (not summary.json/updates.jsonl).
6
+ */
7
+
8
+ import type { MockedFunction } from 'vitest';
9
+ import * as fs from 'fs';
10
+ import * as os from 'os';
11
+ import * as path from 'path';
12
+
13
+ import { GrokCliAdapter } from '../../adapters/GrokCliAdapter.js';
14
+ import type { ProcessInfo } from '../../adapters/AgentAdapter.js';
15
+ import { AgentStatus } from '../../adapters/AgentAdapter.js';
16
+ import { listAgentProcesses, enrichProcesses } from '../../utils/process.js';
17
+ import { generateAgentName } from '../../utils/matching.js';
18
+
19
+ vi.mock('../../utils/process.js', async (importOriginal) => {
20
+ const actual = (await importOriginal()) as typeof import('../../utils/process.js');
21
+ return {
22
+ ...actual,
23
+ listAgentProcesses: vi.fn(),
24
+ enrichProcesses: vi.fn(),
25
+ };
26
+ });
27
+
28
+ vi.mock('../../utils/matching.js', async (importOriginal) => {
29
+ const actual = (await importOriginal()) as typeof import('../../utils/matching.js');
30
+ return {
31
+ ...actual,
32
+ generateAgentName: vi.fn(),
33
+ };
34
+ });
35
+
36
+ const mockedListAgentProcesses = listAgentProcesses as MockedFunction<typeof listAgentProcesses>;
37
+ const mockedEnrichProcesses = enrichProcesses as MockedFunction<typeof enrichProcesses>;
38
+ const mockedGenerateAgentName = generateAgentName as MockedFunction<typeof generateAgentName>;
39
+
40
+ const SESSION_ID = '019f16c3-5d5d-7dc3-85d1-bc629416ca2d';
41
+
42
+ /** A user transcript record: the real prompt wrapped in <user_query> like Grok writes it. */
43
+ const userRecord = (text: string) => ({
44
+ type: 'user',
45
+ content: [{ type: 'text', text: `<user_query>\n${text}\n</user_query>` }],
46
+ });
47
+ /** A user context-injection record (no <user_query>) — should be ignored as a prompt. */
48
+ const contextRecord = (text: string) => ({ type: 'user', content: [{ type: 'text', text }] });
49
+ const assistantRecord = (text: string) => ({ type: 'assistant', content: [{ type: 'text', text }] });
50
+ const systemRecord = (text: string) => ({ type: 'system', content: text });
51
+
52
+ describe('GrokCliAdapter', () => {
53
+ let adapter: GrokCliAdapter;
54
+ let tmpHome: string;
55
+ let cwd: string;
56
+
57
+ beforeEach(() => {
58
+ tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), 'grok-adapter-test-'));
59
+ process.env.HOME = tmpHome;
60
+ delete process.env.GROK_HOME;
61
+ cwd = '/Users/dev/my-project';
62
+
63
+ adapter = new GrokCliAdapter();
64
+
65
+ mockedListAgentProcesses.mockReset();
66
+ mockedEnrichProcesses.mockReset();
67
+ mockedGenerateAgentName.mockReset();
68
+
69
+ mockedEnrichProcesses.mockImplementation((procs) => procs);
70
+ mockedGenerateAgentName.mockImplementation((c: string, pid: number) => `${path.basename(c) || 'unknown'}-${pid}`);
71
+ });
72
+
73
+ afterEach(() => {
74
+ fs.rmSync(tmpHome, { recursive: true, force: true });
75
+ });
76
+
77
+ /** Write a session dir under ~/.grok/sessions/<enc(cwd)>/<id>/chat_history.jsonl. */
78
+ function writeSession(opts: {
79
+ sessionCwd?: string;
80
+ id?: string;
81
+ records?: object[];
82
+ chat?: boolean;
83
+ mtime?: Date;
84
+ }): string {
85
+ const sessionCwd = opts.sessionCwd ?? cwd;
86
+ const id = opts.id ?? SESSION_ID;
87
+ const sessionDir = path.join(tmpHome, '.grok', 'sessions', encodeURIComponent(sessionCwd), id);
88
+ fs.mkdirSync(sessionDir, { recursive: true });
89
+
90
+ if (opts.chat !== false) {
91
+ const records = opts.records ?? [userRecord('fix the bug')];
92
+ const chatPath = path.join(sessionDir, 'chat_history.jsonl');
93
+ fs.writeFileSync(chatPath, records.map((r) => JSON.stringify(r)).join('\n'));
94
+ if (opts.mtime) fs.utimesSync(chatPath, opts.mtime, opts.mtime);
95
+ }
96
+
97
+ return sessionDir;
98
+ }
99
+
100
+ /** Write ~/.grok/active_sessions.json (the live pid -> cwd registry). */
101
+ function writeActiveSessions(entries: Array<{ pid: number; cwd: string; opened_at?: number }>): void {
102
+ fs.mkdirSync(path.join(tmpHome, '.grok'), { recursive: true });
103
+ fs.writeFileSync(path.join(tmpHome, '.grok', 'active_sessions.json'), JSON.stringify(entries));
104
+ }
105
+
106
+ function proc(overrides: Partial<ProcessInfo> = {}): ProcessInfo {
107
+ return { pid: 4242, ppid: 1, command: 'grok', cwd, tty: 'ttys010', startTime: new Date(), ...overrides };
108
+ }
109
+
110
+ describe('initialization', () => {
111
+ it('exposes the grok_cli type', () => {
112
+ expect(adapter.type).toBe('grok_cli');
113
+ });
114
+ });
115
+
116
+ describe('canHandle', () => {
117
+ it('returns true for a plain grok command', () => {
118
+ expect(adapter.canHandle(proc({ command: 'grok' }))).toBe(true);
119
+ });
120
+
121
+ it('returns true for grok with a full path and args', () => {
122
+ expect(adapter.canHandle(proc({ command: '/Users/dev/.grok/bin/grok --always-approve' }))).toBe(true);
123
+ });
124
+
125
+ it('returns false for non-grok processes', () => {
126
+ expect(adapter.canHandle(proc({ command: 'node app.js' }))).toBe(false);
127
+ });
128
+
129
+ it('returns false when "grok" appears only in an argument path', () => {
130
+ expect(adapter.canHandle(proc({ command: 'node /path/to/grok-thing.js' }))).toBe(false);
131
+ });
132
+ });
133
+
134
+ describe('detectAgents', () => {
135
+ it('returns [] when there are no grok processes', async () => {
136
+ mockedListAgentProcesses.mockReturnValue([]);
137
+ expect(await adapter.detectAgents()).toEqual([]);
138
+ });
139
+
140
+ it('resolves the cwd via active_sessions.json (authoritative over the process cwd)', async () => {
141
+ const realCwd = '/Users/dev/real-project';
142
+ writeSession({ sessionCwd: realCwd });
143
+ // The process cwd is stale/wrong; active_sessions.json has the truth.
144
+ writeActiveSessions([{ pid: 4242, cwd: realCwd, opened_at: 1 }]);
145
+ mockedListAgentProcesses.mockReturnValue([proc({ cwd: '/wrong/path' })]);
146
+
147
+ const agents = await adapter.detectAgents();
148
+
149
+ expect(agents).toHaveLength(1);
150
+ expect(agents[0]).toMatchObject({
151
+ type: 'grok_cli',
152
+ pid: 4242,
153
+ projectPath: realCwd,
154
+ sessionId: SESSION_ID,
155
+ summary: 'fix the bug',
156
+ });
157
+ expect(agents[0].sessionFilePath).toBe(
158
+ path.join(tmpHome, '.grok', 'sessions', encodeURIComponent(realCwd), SESSION_ID, 'chat_history.jsonl'),
159
+ );
160
+ });
161
+
162
+ it('falls back to the process cwd when the pid is not in active_sessions.json', async () => {
163
+ writeSession({});
164
+ writeActiveSessions([{ pid: 9999, cwd: '/somewhere/else' }]);
165
+ mockedListAgentProcesses.mockReturnValue([proc()]);
166
+
167
+ const agents = await adapter.detectAgents();
168
+
169
+ expect(agents[0]).toMatchObject({ projectPath: cwd, sessionId: SESSION_ID });
170
+ });
171
+
172
+ it('picks the most recently active session dir when a cwd has several', async () => {
173
+ const older = new Date(Date.now() - 60 * 60 * 1000);
174
+ writeSession({ id: '019f0000-0000-7000-8000-00000000000a', records: [userRecord('old one')], mtime: older });
175
+ writeSession({ id: '019f0000-0000-7000-8000-00000000000b', records: [userRecord('newest one')] });
176
+ mockedListAgentProcesses.mockReturnValue([proc()]);
177
+
178
+ const agents = await adapter.detectAgents();
179
+
180
+ expect(agents[0].sessionId).toBe('019f0000-0000-7000-8000-00000000000b');
181
+ expect(agents[0].summary).toBe('newest one');
182
+ });
183
+
184
+ it('falls back to a process-only RUNNING agent when no session matches', async () => {
185
+ mockedListAgentProcesses.mockReturnValue([proc()]);
186
+
187
+ const agents = await adapter.detectAgents();
188
+
189
+ expect(agents).toHaveLength(1);
190
+ expect(agents[0].status).toBe(AgentStatus.RUNNING);
191
+ expect(agents[0].sessionId).toBe('pid-4242');
192
+ expect(agents[0].sessionFilePath).toBeUndefined();
193
+ });
194
+
195
+ it('treats a session dir without chat_history.jsonl as no match (process-only)', async () => {
196
+ writeSession({ chat: false });
197
+ mockedListAgentProcesses.mockReturnValue([proc()]);
198
+
199
+ const agents = await adapter.detectAgents();
200
+
201
+ expect(agents).toHaveLength(1);
202
+ expect(agents[0].sessionId).toBe('pid-4242');
203
+ });
204
+ });
205
+
206
+ describe('getConversation', () => {
207
+ it('maps user (<user_query>) and assistant records to roles', () => {
208
+ const dir = writeSession({ records: [userRecord('hi'), assistantRecord('hello')] });
209
+ expect(adapter.getConversation(path.join(dir, 'chat_history.jsonl'))).toEqual([
210
+ { role: 'user', content: 'hi' },
211
+ { role: 'assistant', content: 'hello' },
212
+ ]);
213
+ });
214
+
215
+ it('accepts a session dir path and skips context-injection user records', () => {
216
+ const dir = writeSession({
217
+ records: [contextRecord('<user_info>OS: macos</user_info>'), userRecord('do the thing')],
218
+ });
219
+ expect(adapter.getConversation(dir)).toEqual([{ role: 'user', content: 'do the thing' }]);
220
+ });
221
+
222
+ it('skips malformed lines', () => {
223
+ const dir = writeSession({ records: [userRecord('hi')] });
224
+ fs.appendFileSync(path.join(dir, 'chat_history.jsonl'), '\n{bad json');
225
+ expect(adapter.getConversation(dir)).toEqual([{ role: 'user', content: 'hi' }]);
226
+ });
227
+
228
+ it('excludes system records unless verbose', () => {
229
+ const dir = writeSession({ records: [systemRecord('You are Grok'), userRecord('go')] });
230
+ expect(adapter.getConversation(dir)).toEqual([{ role: 'user', content: 'go' }]);
231
+ expect(adapter.getConversation(dir, { verbose: true }).map((m) => m.role)).toEqual(['system', 'user']);
232
+ });
233
+ });
234
+
235
+ describe('detectAgents status + summary mapping', () => {
236
+ const detectFirst = async () => (await adapter.detectAgents())[0];
237
+
238
+ it('marks WAITING when the last transcript turn is an assistant message', async () => {
239
+ writeSession({ records: [userRecord('go'), assistantRecord('done')] });
240
+ mockedListAgentProcesses.mockReturnValue([proc()]);
241
+ expect((await detectFirst()).status).toBe(AgentStatus.WAITING);
242
+ });
243
+
244
+ it('marks RUNNING when the last transcript turn is a user message', async () => {
245
+ writeSession({ records: [userRecord('still there?')] });
246
+ mockedListAgentProcesses.mockReturnValue([proc()]);
247
+ expect((await detectFirst()).status).toBe(AgentStatus.RUNNING);
248
+ });
249
+
250
+ it('marks IDLE when chat_history.jsonl is older than the threshold', async () => {
251
+ const old = new Date(Date.now() - 10 * 60 * 1000);
252
+ writeSession({ records: [userRecord('go')], mtime: old });
253
+ mockedListAgentProcesses.mockReturnValue([proc()]);
254
+ expect((await detectFirst()).status).toBe(AgentStatus.IDLE);
255
+ });
256
+
257
+ it('uses the last user prompt as the agent summary', async () => {
258
+ writeSession({ records: [userRecord('refactor the parser'), assistantRecord('on it')] });
259
+ mockedListAgentProcesses.mockReturnValue([proc()]);
260
+ expect((await detectFirst()).summary).toBe('refactor the parser');
261
+ });
262
+ });
263
+
264
+ describe('listSessions', () => {
265
+ it('returns [] when the sessions dir does not exist', async () => {
266
+ expect(await adapter.listSessions()).toEqual([]);
267
+ });
268
+
269
+ it('lists historical sessions with cwd decoded from the group dir', async () => {
270
+ writeSession({});
271
+ const summaries = await adapter.listSessions();
272
+ expect(summaries).toHaveLength(1);
273
+ expect(summaries[0]).toMatchObject({
274
+ type: 'grok_cli',
275
+ sessionId: SESSION_ID,
276
+ cwd,
277
+ firstUserMessage: 'fix the bug',
278
+ });
279
+ expect(summaries[0].sessionFilePath).toBe(
280
+ path.join(tmpHome, '.grok', 'sessions', encodeURIComponent(cwd), SESSION_ID, 'chat_history.jsonl'),
281
+ );
282
+ });
283
+
284
+ it('applies the cwd filter against the decoded cwd', async () => {
285
+ writeSession({ sessionCwd: '/Users/dev/project-a', id: '019f0000-0000-7000-8000-00000000000a' });
286
+ writeSession({ sessionCwd: '/Users/dev/project-b', id: '019f0000-0000-7000-8000-00000000000b' });
287
+
288
+ const all = await adapter.listSessions();
289
+ expect(all).toHaveLength(2);
290
+
291
+ const filtered = await adapter.listSessions({ cwd: '/Users/dev/project-a' });
292
+ expect(filtered).toHaveLength(1);
293
+ expect(filtered[0].cwd).toBe('/Users/dev/project-a');
294
+ });
295
+
296
+ it('skips non-session entries (e.g. prompt_history.jsonl) in a group dir', async () => {
297
+ writeSession({});
298
+ // Grok writes a group-level prompt_history.jsonl alongside session dirs.
299
+ fs.writeFileSync(
300
+ path.join(tmpHome, '.grok', 'sessions', encodeURIComponent(cwd), 'prompt_history.jsonl'),
301
+ '{"text":"noise"}',
302
+ );
303
+ const summaries = await adapter.listSessions();
304
+ expect(summaries).toHaveLength(1);
305
+ });
306
+ });
307
+ });
@@ -0,0 +1,24 @@
1
+ #!/usr/bin/env node
2
+ const fs = require('node:fs');
3
+
4
+ const args = process.argv.slice(2);
5
+ if (args[0] === '--version') {
6
+ process.stdout.write('fake-claude 2.1.220\n');
7
+ process.exit(0);
8
+ }
9
+ if (args[0] === '--help') {
10
+ process.stdout.write('--print -p --session-id --resume --output-format stream-json --verbose\n');
11
+ process.exit(0);
12
+ }
13
+
14
+ let prompt = '';
15
+ process.stdin.setEncoding('utf8');
16
+ process.stdin.on('data', (chunk) => { prompt += chunk; });
17
+ process.stdin.on('end', () => {
18
+ const flag = args.includes('--session-id') ? '--session-id' : '--resume';
19
+ const sessionId = args[args.indexOf(flag) + 1];
20
+ const capture = process.env.AI_DEVKIT_FAKE_CLAUDE_CAPTURE;
21
+ if (capture) fs.appendFileSync(capture, `${JSON.stringify({ args, prompt, cwd: process.cwd() })}\n`);
22
+ process.stdout.write(`${JSON.stringify({ type: 'system', subtype: 'init', session_id: sessionId })}\n`);
23
+ process.stdout.write(`${JSON.stringify({ type: 'result', session_id: sessionId, result: `answer:${prompt}` })}\n`);
24
+ });
@@ -0,0 +1,32 @@
1
+ import { describe, expect, it, vi } from 'vitest';
2
+
3
+ describe('ClaudeCliProbe', () => {
4
+ it('validates only version/help and requires the print session flags', async () => {
5
+ const api = await import('../../index.js') as Record<string, unknown>;
6
+ expect(api).toHaveProperty('ClaudeCliProbe');
7
+ const exec = vi.fn()
8
+ .mockResolvedValueOnce({ stdout: '2.1.220\n', stderr: '' })
9
+ .mockResolvedValueOnce({
10
+ stdout: '--print --session-id --resume --output-format stream-json', stderr: '',
11
+ });
12
+ const Probe = api.ClaudeCliProbe as new (options: unknown) => { validate(): Promise<unknown> };
13
+
14
+ await expect(new Probe({ exec }).validate()).resolves.toEqual({
15
+ executable: 'claude', version: '2.1.220',
16
+ });
17
+ expect(exec.mock.calls).toEqual([
18
+ ['claude', ['--version']],
19
+ ['claude', ['--help']],
20
+ ]);
21
+ });
22
+
23
+ it('rejects a CLI missing a required capability', async () => {
24
+ const api = await import('../../index.js') as Record<string, unknown>;
25
+ const exec = vi.fn()
26
+ .mockResolvedValueOnce({ stdout: 'old', stderr: '' })
27
+ .mockResolvedValueOnce({ stdout: '--print only', stderr: '' });
28
+ const Probe = api.ClaudeCliProbe as new (options: unknown) => { validate(): Promise<unknown> };
29
+
30
+ await expect(new Probe({ exec }).validate()).rejects.toMatchObject({ code: 'CLAUDE_CLI_UNSUPPORTED' });
31
+ });
32
+ });