@ai-devkit/agent-manager 0.38.0 → 0.40.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 (126) hide show
  1. package/dist/__tests__/capacity/devin.test.js +239 -0
  2. package/dist/__tests__/capacity/devin.test.js.map +1 -0
  3. package/dist/__tests__/harnesses/devin/DevinAdapter.test.js +597 -0
  4. package/dist/__tests__/harnesses/devin/DevinAdapter.test.js.map +1 -0
  5. package/dist/__tests__/harnesses/devin/DevinAgentMapper.test.js +91 -0
  6. package/dist/__tests__/harnesses/devin/DevinAgentMapper.test.js.map +1 -0
  7. package/dist/__tests__/harnesses/devin/DevinSessionLocator.test.js +192 -0
  8. package/dist/__tests__/harnesses/devin/DevinSessionLocator.test.js.map +1 -0
  9. package/dist/__tests__/harnesses/devin/DevinSessionParser.test.js +407 -0
  10. package/dist/__tests__/harnesses/devin/DevinSessionParser.test.js.map +1 -0
  11. package/dist/__tests__/harnesses/devin/fixtures.js +95 -0
  12. package/dist/__tests__/harnesses/devin/fixtures.js.map +1 -0
  13. package/dist/__tests__/harnesses/readiness/AgentReadiness.test.js +64 -1
  14. package/dist/__tests__/harnesses/readiness/AgentReadiness.test.js.map +1 -1
  15. package/dist/__tests__/terminal/GhosttyBackend.test.js +334 -0
  16. package/dist/__tests__/terminal/GhosttyBackend.test.js.map +1 -0
  17. package/dist/adapters/AgentAdapter.d.ts +1 -1
  18. package/dist/adapters/AgentAdapter.d.ts.map +1 -1
  19. package/dist/adapters/AgentAdapter.js +2 -1
  20. package/dist/adapters/AgentAdapter.js.map +1 -1
  21. package/dist/capacity/index.d.ts +3 -0
  22. package/dist/capacity/index.d.ts.map +1 -1
  23. package/dist/capacity/index.js +8 -0
  24. package/dist/capacity/index.js.map +1 -1
  25. package/dist/capacity/providers/devin.d.ts +13 -0
  26. package/dist/capacity/providers/devin.d.ts.map +1 -0
  27. package/dist/capacity/providers/devin.js +92 -0
  28. package/dist/capacity/providers/devin.js.map +1 -0
  29. package/dist/capacity/sources/devin.d.ts +12 -0
  30. package/dist/capacity/sources/devin.d.ts.map +1 -0
  31. package/dist/capacity/sources/devin.js +29 -0
  32. package/dist/capacity/sources/devin.js.map +1 -0
  33. package/dist/harnesses/devin/DevinAdapter.d.ts +39 -0
  34. package/dist/harnesses/devin/DevinAdapter.d.ts.map +1 -0
  35. package/dist/harnesses/devin/DevinAdapter.js +177 -0
  36. package/dist/harnesses/devin/DevinAdapter.js.map +1 -0
  37. package/dist/harnesses/devin/DevinAgentMapper.d.ts +11 -0
  38. package/dist/harnesses/devin/DevinAgentMapper.d.ts.map +1 -0
  39. package/dist/harnesses/devin/DevinAgentMapper.js +35 -0
  40. package/dist/harnesses/devin/DevinAgentMapper.js.map +1 -0
  41. package/dist/harnesses/devin/DevinSessionLocator.d.ts +46 -0
  42. package/dist/harnesses/devin/DevinSessionLocator.d.ts.map +1 -0
  43. package/dist/harnesses/devin/DevinSessionLocator.js +176 -0
  44. package/dist/harnesses/devin/DevinSessionLocator.js.map +1 -0
  45. package/dist/harnesses/devin/DevinSessionParser.d.ts +29 -0
  46. package/dist/harnesses/devin/DevinSessionParser.d.ts.map +1 -0
  47. package/dist/harnesses/devin/DevinSessionParser.js +166 -0
  48. package/dist/harnesses/devin/DevinSessionParser.js.map +1 -0
  49. package/dist/harnesses/devin/credentials.d.ts +10 -0
  50. package/dist/harnesses/devin/credentials.d.ts.map +1 -0
  51. package/dist/harnesses/devin/credentials.js +66 -0
  52. package/dist/harnesses/devin/credentials.js.map +1 -0
  53. package/dist/harnesses/devin/readiness.d.ts +3 -0
  54. package/dist/harnesses/devin/readiness.d.ts.map +1 -0
  55. package/dist/harnesses/devin/readiness.js +39 -0
  56. package/dist/harnesses/devin/readiness.js.map +1 -0
  57. package/dist/harnesses/index.d.ts.map +1 -1
  58. package/dist/harnesses/index.js +3 -1
  59. package/dist/harnesses/index.js.map +1 -1
  60. package/dist/harnesses/readiness/AgentReadiness.d.ts.map +1 -1
  61. package/dist/harnesses/readiness/AgentReadiness.js +3 -1
  62. package/dist/harnesses/readiness/AgentReadiness.js.map +1 -1
  63. package/dist/harnesses/readiness/types.d.ts +1 -1
  64. package/dist/harnesses/readiness/types.d.ts.map +1 -1
  65. package/dist/harnesses/runtimeProfiles.d.ts.map +1 -1
  66. package/dist/harnesses/runtimeProfiles.js +2 -1
  67. package/dist/harnesses/runtimeProfiles.js.map +1 -1
  68. package/dist/index.d.ts +1 -1
  69. package/dist/index.d.ts.map +1 -1
  70. package/dist/index.js +1 -1
  71. package/dist/index.js.map +1 -1
  72. package/dist/terminal/TtyWriter.d.ts.map +1 -1
  73. package/dist/terminal/TtyWriter.js +4 -3
  74. package/dist/terminal/TtyWriter.js.map +1 -1
  75. package/dist/terminal/backends/ghostty.d.ts +3 -0
  76. package/dist/terminal/backends/ghostty.d.ts.map +1 -0
  77. package/dist/terminal/backends/ghostty.js +161 -0
  78. package/dist/terminal/backends/ghostty.js.map +1 -0
  79. package/dist/terminal/backends/index.d.ts.map +1 -1
  80. package/dist/terminal/backends/index.js +2 -0
  81. package/dist/terminal/backends/index.js.map +1 -1
  82. package/dist/terminal/backends/shared.d.ts +2 -0
  83. package/dist/terminal/backends/shared.d.ts.map +1 -1
  84. package/dist/terminal/backends/shared.js +4 -1
  85. package/dist/terminal/backends/shared.js.map +1 -1
  86. package/dist/terminal/types.d.ts +1 -0
  87. package/dist/terminal/types.d.ts.map +1 -1
  88. package/dist/terminal/types.js +1 -0
  89. package/dist/terminal/types.js.map +1 -1
  90. package/dist/utils/process.d.ts +2 -0
  91. package/dist/utils/process.d.ts.map +1 -1
  92. package/dist/utils/process.js +3 -0
  93. package/dist/utils/process.js.map +1 -1
  94. package/package.json +1 -1
  95. package/src/__tests__/capacity/devin.test.ts +210 -0
  96. package/src/__tests__/capacity/fixtures/devin-config.json +6 -0
  97. package/src/__tests__/capacity/fixtures/devin-credentials.toml +4 -0
  98. package/src/__tests__/capacity/fixtures/devin-quota-nonplan.json +5 -0
  99. package/src/__tests__/capacity/fixtures/devin-quota.json +10 -0
  100. package/src/__tests__/harnesses/devin/DevinAdapter.test.ts +292 -0
  101. package/src/__tests__/harnesses/devin/DevinAgentMapper.test.ts +88 -0
  102. package/src/__tests__/harnesses/devin/DevinSessionLocator.test.ts +139 -0
  103. package/src/__tests__/harnesses/devin/DevinSessionParser.test.ts +218 -0
  104. package/src/__tests__/harnesses/devin/fixtures.ts +144 -0
  105. package/src/__tests__/harnesses/readiness/AgentReadiness.test.ts +41 -0
  106. package/src/__tests__/terminal/GhosttyBackend.test.ts +271 -0
  107. package/src/adapters/AgentAdapter.ts +1 -0
  108. package/src/capacity/index.ts +12 -0
  109. package/src/capacity/providers/devin.ts +121 -0
  110. package/src/capacity/sources/devin.ts +49 -0
  111. package/src/harnesses/devin/DevinAdapter.ts +224 -0
  112. package/src/harnesses/devin/DevinAgentMapper.ts +43 -0
  113. package/src/harnesses/devin/DevinSessionLocator.ts +230 -0
  114. package/src/harnesses/devin/DevinSessionParser.ts +236 -0
  115. package/src/harnesses/devin/credentials.ts +88 -0
  116. package/src/harnesses/devin/readiness.ts +36 -0
  117. package/src/harnesses/index.ts +2 -0
  118. package/src/harnesses/readiness/AgentReadiness.ts +2 -0
  119. package/src/harnesses/runtimeProfiles.ts +1 -0
  120. package/src/index.ts +1 -0
  121. package/src/terminal/TtyWriter.ts +5 -5
  122. package/src/terminal/backends/ghostty.ts +180 -0
  123. package/src/terminal/backends/index.ts +2 -0
  124. package/src/terminal/backends/shared.ts +6 -1
  125. package/src/terminal/types.ts +1 -0
  126. package/src/utils/process.ts +5 -0
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/utils/process.ts"],"sourcesContent":["/**\n * Process Detection Utilities\n *\n * Shared shell command wrappers for detecting and inspecting running processes.\n * Built-in refresh discovery uses captureProcessSnapshot(); synchronous helpers\n * remain exported for compatibility with existing consumers.\n */\n\nimport * as fs from \"fs\";\nimport * as path from \"path\";\nimport { execFile, execFileSync } from \"child_process\";\nimport type { ProcessInfo } from \"../adapters/AgentAdapter.js\";\n\nconst PROCESS_EXEC_MAX_BUFFER = 10 * 1024 * 1024;\nconst VALID_EXECUTABLE_NAME = /^[a-zA-Z0-9_-]+$/;\nconst ABSOLUTE_PATH = /^(?:\\/|[a-zA-Z]:[\\\\/])/;\nconst PATH_SEPARATOR = /[\\\\/]/;\n\nfunction pathBasename(executable: string): string {\n return path.basename(executable.replace(/\\\\/g, \"/\")).toLowerCase();\n}\n\nfunction isFile(filePath: string): boolean {\n try {\n return fs.statSync(filePath, { throwIfNoEntry: false })?.isFile() ?? false;\n } catch {\n return false;\n }\n}\n\n/**\n * Resolve argv[0] from a `ps` command line.\n *\n * `ps` joins argv with spaces, so an executable installed under a directory\n * containing spaces (`/Applications/Some App.app/Contents/Resources/codex`)\n * spans several whitespace tokens. When the first token of an absolute path\n * is not itself a file, longer space-joined prefixes ending in a token with a\n * path separator are tried, and the first one that exists on disk wins.\n * Otherwise the first token is returned, so arguments are never folded into\n * the executable path unless that combined path really is a file.\n */\nexport function executablePath(command: string): string {\n const parts = command.trim().split(/(\\s+)/);\n const first = parts[0] ?? \"\";\n if (parts.length === 1 || !ABSOLUTE_PATH.test(first) || isFile(first)) return first;\n\n let prefix = first;\n for (let i = 2; i < parts.length; i += 2) {\n prefix += parts[i - 1] + parts[i];\n if (PATH_SEPARATOR.test(parts[i]) && isFile(prefix)) return prefix;\n }\n return first;\n}\n\nexport function executableBasename(command: string): string {\n return pathBasename(executablePath(command));\n}\n\nfunction normalizeExecutableName(name: string): string {\n const lower = name.toLowerCase();\n return lower.endsWith(\".exe\") ? lower.slice(0, -4) : lower;\n}\n\nfunction normalizedProcessNames(namePatterns: readonly string[]): Set<string> {\n return new Set(namePatterns.filter(Boolean).map(normalizeExecutableName));\n}\n\n/**\n * Whether argv[0] of `command` is one of `names`.\n *\n * The first token is checked with string operations only, as before. The\n * filesystem is consulted only for absolute commands where a later\n * path-like token ends in one of the names, i.e. a possible executable under\n * a directory containing spaces.\n */\nfunction matchesExecutableName(command: string, names: ReadonlySet<string>): boolean {\n const tokens = command.trim().split(/\\s+/);\n const first = tokens[0] ?? \"\";\n if (names.has(normalizeExecutableName(pathBasename(first)))) return true;\n if (!ABSOLUTE_PATH.test(first)) return false;\n\n const mayContinuePath = tokens\n .slice(1)\n .some(\n (token) =>\n PATH_SEPARATOR.test(token) && names.has(normalizeExecutableName(pathBasename(token))),\n );\n return mayContinuePath && names.has(normalizeExecutableName(executableBasename(command)));\n}\n\nexport function filterByProcessNames(\n processes: readonly ProcessInfo[],\n namePatterns: readonly string[],\n): ProcessInfo[] {\n const names = normalizedProcessNames(namePatterns);\n if (names.size === 0) return [];\n return processes.filter((process) => matchesExecutableName(process.command, names));\n}\n\n/**\n * List running processes matching an agent executable name.\n *\n * Uses `ps -axo` then filters in JS for exact executable basename match.\n * This avoids shell pipelines and string interpolation.\n *\n * Returned ProcessInfo has pid, ppid, command, tty populated.\n * cwd and startTime are NOT populated — call enrichProcesses() to fill them.\n */\nexport function listAgentProcesses(namePattern: string): ProcessInfo[] {\n // Validate pattern contains only safe characters (alphanumeric, dash, underscore)\n if (!namePattern || !VALID_EXECUTABLE_NAME.test(namePattern)) {\n return [];\n }\n\n try {\n const output = execFileSync(\"ps\", [\"-axo\", \"pid=,ppid=,tty=,command=\"], { encoding: \"utf-8\" });\n\n const names = normalizedProcessNames([namePattern]);\n const processes: ProcessInfo[] = [];\n\n for (const line of output.trim().split(\"\\n\")) {\n if (!line.trim()) continue;\n\n const match = line.match(/^\\s*(\\d+)\\s+(\\d+)\\s+(\\S+)\\s+(.+)$/);\n if (!match) continue;\n\n const pid = parseInt(match[1], 10);\n const ppid = parseInt(match[2], 10);\n if (Number.isNaN(pid) || Number.isNaN(ppid)) continue;\n\n const tty = match[3];\n const command = match[4];\n\n if (!matchesExecutableName(command, names)) continue;\n\n const ttyShort = tty.startsWith(\"/dev/\") ? tty.slice(5) : tty;\n\n processes.push({\n pid,\n ppid,\n command,\n cwd: \"\",\n tty: ttyShort,\n });\n }\n\n return processes;\n } catch {\n return [];\n }\n}\n\n/** Runs an executable and resolves with its stdout. Injectable for tests. */\nexport type ProcessExec = (file: string, args: readonly string[]) => Promise<string>;\n\nexport interface ProcessSnapshotOptions {\n /**\n * Decides which name-matched processes are worth enriching with cwd and\n * start time. Non-candidates are still returned (pid, ppid, tty, command\n * only) so adapters can walk parent chains. Omitted = enrich everything.\n */\n isCandidate?: (process: ProcessInfo) => boolean;\n}\n\nexport type ProcessSnapshotCapture = (\n namePatterns: readonly string[],\n options?: ProcessSnapshotOptions,\n) => Promise<ProcessInfo[]>;\n\nconst execFileText: ProcessExec = (file, args) =>\n new Promise((resolve, reject) => {\n execFile(\n file,\n args,\n { encoding: \"utf-8\", maxBuffer: PROCESS_EXEC_MAX_BUFFER },\n (error, stdout) => {\n if (error) {\n reject(error);\n return;\n }\n resolve(stdout);\n },\n );\n });\n\ninterface ParsedProcessList {\n /** Every PID in the listing, used to evict cache entries for exited processes. */\n allPids: Set<number>;\n /** Processes whose executable matches one of the requested names. */\n matched: ProcessInfo[];\n}\n\nfunction parseProcessList(output: string, namePatterns: ReadonlySet<string>): ParsedProcessList {\n const allPids = new Set<number>();\n const matched: ProcessInfo[] = [];\n\n for (const line of output.trim().split(\"\\n\")) {\n if (!line.trim()) continue;\n\n const match = line.match(/^\\s*(\\d+)\\s+(\\d+)\\s+(\\S+)\\s+(.+)$/);\n if (!match) continue;\n\n const pid = parseInt(match[1], 10);\n const ppid = parseInt(match[2], 10);\n if (Number.isNaN(pid) || Number.isNaN(ppid)) continue;\n allPids.add(pid);\n\n const tty = match[3];\n const command = match[4];\n if (!matchesExecutableName(command, namePatterns)) continue;\n\n matched.push({\n pid,\n ppid,\n command,\n cwd: \"\",\n tty: tty.startsWith(\"/dev/\") ? tty.slice(5) : tty,\n });\n }\n\n return { allPids, matched };\n}\n\nasync function batchGetProcessCwdsAsync(\n exec: ProcessExec,\n pids: number[],\n): Promise<Map<number, string>> {\n const result = new Map<number, string>();\n if (pids.length === 0) return result;\n\n try {\n const output = await exec(\"lsof\", [\"-a\", \"-d\", \"cwd\", \"-Fn\", \"-p\", pids.join(\",\")]);\n let currentPid: number | null = null;\n for (const line of output.trim().split(\"\\n\")) {\n if (line.startsWith(\"p\")) {\n currentPid = parseInt(line.slice(1), 10);\n } else if (line.startsWith(\"n\") && currentPid !== null) {\n result.set(currentPid, line.slice(1));\n currentPid = null;\n }\n }\n return result;\n } catch {\n const entries = await Promise.all(\n pids.map(async (pid) => {\n try {\n const output = await exec(\"pwdx\", [String(pid)]);\n const match = output.match(/^\\d+:\\s*(.+)$/);\n return match ? ([pid, match[1].trim()] as const) : null;\n } catch {\n return null;\n }\n }),\n );\n for (const entry of entries) {\n if (entry) result.set(entry[0], entry[1]);\n }\n return result;\n }\n}\n\nasync function batchGetProcessStartTimesAsync(\n exec: ProcessExec,\n pids: number[],\n): Promise<Map<number, Date>> {\n const result = new Map<number, Date>();\n if (pids.length === 0) return result;\n\n try {\n const output = await exec(\"ps\", [\"-o\", \"pid=,lstart=\", \"-p\", pids.join(\",\")]);\n for (const rawLine of output.split(\"\\n\")) {\n const match = rawLine.trim().match(/^(\\d+)\\s+(.+)$/);\n if (!match) continue;\n const pid = parseInt(match[1], 10);\n const date = new Date(match[2].trim());\n if (Number.isFinite(pid) && !Number.isNaN(date.getTime())) result.set(pid, date);\n }\n } catch {\n // Return partial/empty data, matching the synchronous helper.\n }\n return result;\n}\n\ninterface CachedStartTime {\n /** ppid + command seen when the start time was read; a mismatch means PID reuse. */\n identity: string;\n startTime: Date;\n}\n\nfunction processIdentity(process: ProcessInfo): string {\n return `${process.ppid ?? \"\"}\\0${process.command}`;\n}\n\n/**\n * Create a snapshot capture function with its own start-time cache.\n *\n * Start times never change for a live process, so each PID is queried with\n * `ps lstart` once and reused on later refreshes. A cache entry is dropped\n * when its PID disappears from the base listing, and ignored when the PID's\n * ppid/command no longer match (PID reuse). cwd is re-read every refresh\n * because a process can change directory.\n */\nexport function createProcessSnapshotCapture(\n deps: { exec?: ProcessExec } = {},\n): ProcessSnapshotCapture {\n const exec = deps.exec ?? execFileText;\n const startTimeCache = new Map<number, CachedStartTime>();\n\n const cachedStartTime = (process: ProcessInfo): Date | undefined => {\n const cached = startTimeCache.get(process.pid);\n return cached?.identity === processIdentity(process) ? cached.startTime : undefined;\n };\n\n return async (namePatterns, options = {}) => {\n const names = normalizedProcessNames(\n namePatterns.filter((name) => Boolean(name) && VALID_EXECUTABLE_NAME.test(name)),\n );\n if (names.size === 0) return [];\n\n try {\n const output = await exec(\"ps\", [\"-axo\", \"pid=,ppid=,tty=,command=\"]);\n const { allPids, matched } = parseProcessList(output, names);\n\n for (const pid of startTimeCache.keys()) {\n if (!allPids.has(pid)) startTimeCache.delete(pid);\n }\n\n const { isCandidate } = options;\n const candidates = isCandidate ? matched.filter((process) => isCandidate(process)) : matched;\n const candidatePids = candidates.map((process) => process.pid);\n const uncachedPids = candidates\n .filter((process) => cachedStartTime(process) === undefined)\n .map((process) => process.pid);\n\n const [cwdMap, startTimeMap] = await Promise.all([\n batchGetProcessCwdsAsync(exec, candidatePids),\n batchGetProcessStartTimesAsync(exec, uncachedPids),\n ]);\n\n for (const process of candidates) {\n const startTime = startTimeMap.get(process.pid);\n if (startTime) {\n startTimeCache.set(process.pid, { identity: processIdentity(process), startTime });\n }\n }\n\n const candidateSet = new Set(candidatePids);\n return matched.map((process) =>\n candidateSet.has(process.pid)\n ? {\n ...process,\n cwd: cwdMap.get(process.pid) || \"\",\n startTime: cachedStartTime(process),\n }\n : process,\n );\n } catch {\n return [];\n }\n };\n}\n\n/**\n * Capture and enrich relevant processes without blocking the event loop.\n * One base process listing is shared by every requested executable name;\n * only processes accepted by `options.isCandidate` are enriched.\n */\nexport const captureProcessSnapshot: ProcessSnapshotCapture = createProcessSnapshotCapture();\n\n/**\n * Batch-get current working directories for multiple PIDs.\n *\n * Single `lsof -a -d cwd -Fn -p PID1,PID2,...` call.\n * Returns partial results — if lsof fails for one PID, others still return.\n */\nexport function batchGetProcessCwds(pids: number[]): Map<number, string> {\n const result = new Map<number, string>();\n if (pids.length === 0) return result;\n\n try {\n const output = execFileSync(\"lsof\", [\"-a\", \"-d\", \"cwd\", \"-Fn\", \"-p\", pids.join(\",\")], {\n encoding: \"utf-8\",\n stdio: [\"pipe\", \"pipe\", \"ignore\"],\n });\n\n // lsof output format: p{PID}\\nn{path}\\np{PID}\\nn{path}...\n let currentPid: number | null = null;\n for (const line of output.trim().split(\"\\n\")) {\n if (line.startsWith(\"p\")) {\n currentPid = parseInt(line.slice(1), 10);\n } else if (line.startsWith(\"n\") && currentPid !== null) {\n result.set(currentPid, line.slice(1));\n currentPid = null;\n }\n }\n } catch {\n // Try per-PID fallback with pwdx (Linux)\n for (const pid of pids) {\n try {\n const output = execFileSync(\"pwdx\", [String(pid)], {\n encoding: \"utf-8\",\n stdio: [\"pipe\", \"pipe\", \"ignore\"],\n });\n const match = output.match(/^\\d+:\\s*(.+)$/);\n if (match) {\n result.set(pid, match[1].trim());\n }\n } catch {\n // Skip this PID\n }\n }\n }\n\n return result;\n}\n\n/**\n * Batch-get process start times for multiple PIDs.\n *\n * Single `ps -o pid=,lstart= -p PID1,PID2,...` call.\n * Uses lstart format which gives full timestamp (e.g., \"Thu Feb 5 16:00:57 2026\").\n * Returns partial results.\n */\nexport function batchGetProcessStartTimes(pids: number[]): Map<number, Date> {\n const result = new Map<number, Date>();\n if (pids.length === 0) return result;\n\n try {\n const output = execFileSync(\"ps\", [\"-o\", \"pid=,lstart=\", \"-p\", pids.join(\",\")], {\n encoding: \"utf-8\",\n });\n\n for (const rawLine of output.split(\"\\n\")) {\n const line = rawLine.trim();\n if (!line) continue;\n\n // Format: \" PID DAY MON DD HH:MM:SS YYYY\"\n // e.g., \" 78070 Wed Mar 18 23:18:01 2026\"\n const match = line.match(/^\\s*(\\d+)\\s+(.+)$/);\n if (!match) continue;\n\n const pid = parseInt(match[1], 10);\n const dateStr = match[2].trim();\n\n if (!Number.isFinite(pid)) continue;\n\n const date = new Date(dateStr);\n if (!Number.isNaN(date.getTime())) {\n result.set(pid, date);\n }\n }\n } catch {\n // Return whatever we have\n }\n\n return result;\n}\n\n/**\n * Enrich ProcessInfo array with cwd and startTime.\n *\n * Calls batchGetProcessCwds and batchGetProcessStartTimes in batched shell calls,\n * then populates each ProcessInfo in-place. Returns partial results —\n * if a PID fails, that process keeps empty cwd / undefined startTime.\n */\nexport function enrichProcesses(processes: ProcessInfo[]): ProcessInfo[] {\n if (processes.length === 0) return processes;\n\n const pids = processes.map((p) => p.pid);\n const cwdMap = batchGetProcessCwds(pids);\n const startTimeMap = batchGetProcessStartTimes(pids);\n\n for (const proc of processes) {\n proc.cwd = cwdMap.get(proc.pid) || \"\";\n proc.startTime = startTimeMap.get(proc.pid);\n }\n\n return processes;\n}\n\nfunction isSameTerminalProcess(proc: ProcessInfo, matched: ProcessInfo): boolean {\n const sameTty = proc.tty !== \"\" && proc.tty !== \"?\" && proc.tty === matched.tty;\n const sameCwd = proc.cwd !== \"\" && proc.cwd === matched.cwd;\n\n return sameTty && (sameCwd || proc.cwd === \"\" || matched.cwd === \"\");\n}\n\nfunction matchesProcessIdentity(proc: ProcessInfo, matched: ProcessInfo): boolean {\n return proc.pid === matched.pid || isSameTerminalProcess(proc, matched);\n}\n\nexport function findWrapperProcess(\n processes: ProcessInfo[],\n child: ProcessInfo,\n): ProcessInfo | undefined {\n return processes.find(\n (proc) =>\n proc.pid !== child.pid && child.ppid === proc.pid && matchesProcessIdentity(proc, child),\n );\n}\n\n/**\n * Find parent wrapper processes that should not be reported as separate agents.\n *\n * A process is considered a wrapper when it is the parent of another candidate\n * agent process in the same terminal/worktree, or when it points at the same\n * terminal/worktree as an already session-matched process.\n */\nexport function findWrapperProcessPids(\n processes: ProcessInfo[],\n matchedProcesses: ProcessInfo[] = [],\n): Set<number> {\n const wrappers = new Set<number>();\n\n for (const child of processes) {\n const wrapper = findWrapperProcess(processes, child);\n if (wrapper) {\n wrappers.add(wrapper.pid);\n }\n }\n\n for (const proc of processes) {\n if (\n matchedProcesses.some(\n (matched) => proc.pid !== matched.pid && isSameTerminalProcess(proc, matched),\n )\n ) {\n wrappers.add(proc.pid);\n }\n }\n\n return wrappers;\n}\n\n/**\n * Get the TTY device for a specific process\n */\nexport function getProcessTty(pid: number): string {\n try {\n const output = execFileSync(\"ps\", [\"-p\", String(pid), \"-o\", \"tty=\"], { encoding: \"utf-8\" });\n\n const tty = output.trim();\n return tty.startsWith(\"/dev/\") ? tty.slice(5) : tty;\n } catch {\n return \"?\";\n }\n}\n"],"names":["fs","path","execFile","execFileSync","PROCESS_EXEC_MAX_BUFFER","VALID_EXECUTABLE_NAME","ABSOLUTE_PATH","PATH_SEPARATOR","pathBasename","executable","basename","replace","toLowerCase","isFile","filePath","statSync","throwIfNoEntry","executablePath","command","parts","trim","split","first","length","test","prefix","i","executableBasename","normalizeExecutableName","name","lower","endsWith","slice","normalizedProcessNames","namePatterns","Set","filter","Boolean","map","matchesExecutableName","names","tokens","has","mayContinuePath","some","token","filterByProcessNames","processes","size","process","listAgentProcesses","namePattern","output","encoding","line","match","pid","parseInt","ppid","Number","isNaN","tty","ttyShort","startsWith","push","cwd","execFileText","file","args","Promise","resolve","reject","maxBuffer","error","stdout","parseProcessList","allPids","matched","add","batchGetProcessCwdsAsync","exec","pids","result","Map","join","currentPid","set","entries","all","String","entry","batchGetProcessStartTimesAsync","rawLine","date","Date","isFinite","getTime","processIdentity","createProcessSnapshotCapture","deps","startTimeCache","cachedStartTime","cached","get","identity","startTime","undefined","options","keys","delete","isCandidate","candidates","candidatePids","uncachedPids","cwdMap","startTimeMap","candidateSet","captureProcessSnapshot","batchGetProcessCwds","stdio","batchGetProcessStartTimes","dateStr","enrichProcesses","p","proc","isSameTerminalProcess","sameTty","sameCwd","matchesProcessIdentity","findWrapperProcess","child","find","findWrapperProcessPids","matchedProcesses","wrappers","wrapper","getProcessTty"],"mappings":"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"}
1
+ {"version":3,"sources":["../../src/utils/process.ts"],"sourcesContent":["/**\n * Process Detection Utilities\n *\n * Shared shell command wrappers for detecting and inspecting running processes.\n * Built-in refresh discovery uses captureProcessSnapshot(); synchronous helpers\n * remain exported for compatibility with existing consumers.\n */\n\nimport * as fs from \"fs\";\nimport * as path from \"path\";\nimport { execFile, execFileSync } from \"child_process\";\nimport type { ProcessInfo } from \"../adapters/AgentAdapter.js\";\n\nconst PROCESS_EXEC_MAX_BUFFER = 10 * 1024 * 1024;\nconst VALID_EXECUTABLE_NAME = /^[a-zA-Z0-9_-]+$/;\nconst ABSOLUTE_PATH = /^(?:\\/|[a-zA-Z]:[\\\\/])/;\nconst PATH_SEPARATOR = /[\\\\/]/;\n\nfunction pathBasename(executable: string): string {\n return path.basename(executable.replace(/\\\\/g, \"/\")).toLowerCase();\n}\n\nfunction isFile(filePath: string): boolean {\n try {\n return fs.statSync(filePath, { throwIfNoEntry: false })?.isFile() ?? false;\n } catch {\n return false;\n }\n}\n\n/**\n * Resolve argv[0] from a `ps` command line.\n *\n * `ps` joins argv with spaces, so an executable installed under a directory\n * containing spaces (`/Applications/Some App.app/Contents/Resources/codex`)\n * spans several whitespace tokens. When the first token of an absolute path\n * is not itself a file, longer space-joined prefixes ending in a token with a\n * path separator are tried, and the first one that exists on disk wins.\n * Otherwise the first token is returned, so arguments are never folded into\n * the executable path unless that combined path really is a file.\n */\nexport function executablePath(command: string): string {\n const parts = command.trim().split(/(\\s+)/);\n const first = parts[0] ?? \"\";\n if (parts.length === 1 || !ABSOLUTE_PATH.test(first) || isFile(first)) return first;\n\n let prefix = first;\n for (let i = 2; i < parts.length; i += 2) {\n prefix += parts[i - 1] + parts[i];\n if (PATH_SEPARATOR.test(parts[i]) && isFile(prefix)) return prefix;\n }\n return first;\n}\n\nexport function executableBasename(command: string): string {\n return pathBasename(executablePath(command));\n}\n\nfunction normalizeExecutableName(name: string): string {\n const lower = name.toLowerCase();\n return lower.endsWith(\".exe\") ? lower.slice(0, -4) : lower;\n}\n\nfunction normalizedProcessNames(namePatterns: readonly string[]): Set<string> {\n return new Set(namePatterns.filter(Boolean).map(normalizeExecutableName));\n}\n\n/**\n * Whether argv[0] of `command` is one of `names`.\n *\n * The first token is checked with string operations only, as before. The\n * filesystem is consulted only for absolute commands where a later\n * path-like token ends in one of the names, i.e. a possible executable under\n * a directory containing spaces.\n */\nfunction matchesExecutableName(command: string, names: ReadonlySet<string>): boolean {\n const tokens = command.trim().split(/\\s+/);\n const first = tokens[0] ?? \"\";\n if (names.has(normalizeExecutableName(pathBasename(first)))) return true;\n if (!ABSOLUTE_PATH.test(first)) return false;\n\n const mayContinuePath = tokens\n .slice(1)\n .some(\n (token) =>\n PATH_SEPARATOR.test(token) && names.has(normalizeExecutableName(pathBasename(token))),\n );\n return mayContinuePath && names.has(normalizeExecutableName(executableBasename(command)));\n}\n\nexport function filterByProcessNames(\n processes: readonly ProcessInfo[],\n namePatterns: readonly string[],\n): ProcessInfo[] {\n const names = normalizedProcessNames(namePatterns);\n if (names.size === 0) return [];\n return processes.filter((process) => matchesExecutableName(process.command, names));\n}\n\n/**\n * List running processes matching an agent executable name.\n *\n * Uses `ps -axo` then filters in JS for exact executable basename match.\n * This avoids shell pipelines and string interpolation.\n *\n * Returned ProcessInfo has pid, ppid, command, tty populated.\n * cwd and startTime are NOT populated — call enrichProcesses() to fill them.\n */\nexport function listAgentProcesses(namePattern: string): ProcessInfo[] {\n // Validate pattern contains only safe characters (alphanumeric, dash, underscore)\n if (!namePattern || !VALID_EXECUTABLE_NAME.test(namePattern)) {\n return [];\n }\n\n try {\n const output = execFileSync(\"ps\", [\"-axo\", \"pid=,ppid=,tty=,command=\"], { encoding: \"utf-8\" });\n\n const names = normalizedProcessNames([namePattern]);\n const processes: ProcessInfo[] = [];\n\n for (const line of output.trim().split(\"\\n\")) {\n if (!line.trim()) continue;\n\n const match = line.match(/^\\s*(\\d+)\\s+(\\d+)\\s+(\\S+)\\s+(.+)$/);\n if (!match) continue;\n\n const pid = parseInt(match[1], 10);\n const ppid = parseInt(match[2], 10);\n if (Number.isNaN(pid) || Number.isNaN(ppid)) continue;\n\n const tty = match[3];\n const command = match[4];\n\n if (!matchesExecutableName(command, names)) continue;\n\n const ttyShort = tty.startsWith(\"/dev/\") ? tty.slice(5) : tty;\n\n processes.push({\n pid,\n ppid,\n command,\n cwd: \"\",\n tty: ttyShort,\n });\n }\n\n return processes;\n } catch {\n return [];\n }\n}\n\n/** Runs an executable and resolves with its stdout. Injectable for tests. */\nexport type ProcessExec = (file: string, args: readonly string[]) => Promise<string>;\n\nexport interface ProcessSnapshotOptions {\n /**\n * Decides which name-matched processes are worth enriching with cwd and\n * start time. Non-candidates are still returned (pid, ppid, tty, command\n * only) so adapters can walk parent chains. Omitted = enrich everything.\n */\n isCandidate?: (process: ProcessInfo) => boolean;\n}\n\nexport type ProcessSnapshotCapture = (\n namePatterns: readonly string[],\n options?: ProcessSnapshotOptions,\n) => Promise<ProcessInfo[]>;\n\nconst execFileText: ProcessExec = (file, args) =>\n new Promise((resolve, reject) => {\n execFile(\n file,\n args,\n { encoding: \"utf-8\", maxBuffer: PROCESS_EXEC_MAX_BUFFER },\n (error, stdout) => {\n if (error) {\n reject(error);\n return;\n }\n resolve(stdout);\n },\n );\n });\n\ninterface ParsedProcessList {\n /** Every PID in the listing, used to evict cache entries for exited processes. */\n allPids: Set<number>;\n /** Processes whose executable matches one of the requested names. */\n matched: ProcessInfo[];\n}\n\nfunction parseProcessList(output: string, namePatterns: ReadonlySet<string>): ParsedProcessList {\n const allPids = new Set<number>();\n const matched: ProcessInfo[] = [];\n\n for (const line of output.trim().split(\"\\n\")) {\n if (!line.trim()) continue;\n\n const match = line.match(/^\\s*(\\d+)\\s+(\\d+)\\s+(\\S+)\\s+(.+)$/);\n if (!match) continue;\n\n const pid = parseInt(match[1], 10);\n const ppid = parseInt(match[2], 10);\n if (Number.isNaN(pid) || Number.isNaN(ppid)) continue;\n allPids.add(pid);\n\n const tty = match[3];\n const command = match[4];\n if (!matchesExecutableName(command, namePatterns)) continue;\n\n matched.push({\n pid,\n ppid,\n command,\n cwd: \"\",\n tty: tty.startsWith(\"/dev/\") ? tty.slice(5) : tty,\n });\n }\n\n return { allPids, matched };\n}\n\n/** Working directories of `pids` (async `lsof`); PIDs it cannot read are omitted. */\nexport function getProcessCwdsAsync(pids: number[]): Promise<Map<number, string>> {\n return batchGetProcessCwdsAsync(execFileText, pids);\n}\n\nasync function batchGetProcessCwdsAsync(\n exec: ProcessExec,\n pids: number[],\n): Promise<Map<number, string>> {\n const result = new Map<number, string>();\n if (pids.length === 0) return result;\n\n try {\n const output = await exec(\"lsof\", [\"-a\", \"-d\", \"cwd\", \"-Fn\", \"-p\", pids.join(\",\")]);\n let currentPid: number | null = null;\n for (const line of output.trim().split(\"\\n\")) {\n if (line.startsWith(\"p\")) {\n currentPid = parseInt(line.slice(1), 10);\n } else if (line.startsWith(\"n\") && currentPid !== null) {\n result.set(currentPid, line.slice(1));\n currentPid = null;\n }\n }\n return result;\n } catch {\n const entries = await Promise.all(\n pids.map(async (pid) => {\n try {\n const output = await exec(\"pwdx\", [String(pid)]);\n const match = output.match(/^\\d+:\\s*(.+)$/);\n return match ? ([pid, match[1].trim()] as const) : null;\n } catch {\n return null;\n }\n }),\n );\n for (const entry of entries) {\n if (entry) result.set(entry[0], entry[1]);\n }\n return result;\n }\n}\n\nasync function batchGetProcessStartTimesAsync(\n exec: ProcessExec,\n pids: number[],\n): Promise<Map<number, Date>> {\n const result = new Map<number, Date>();\n if (pids.length === 0) return result;\n\n try {\n const output = await exec(\"ps\", [\"-o\", \"pid=,lstart=\", \"-p\", pids.join(\",\")]);\n for (const rawLine of output.split(\"\\n\")) {\n const match = rawLine.trim().match(/^(\\d+)\\s+(.+)$/);\n if (!match) continue;\n const pid = parseInt(match[1], 10);\n const date = new Date(match[2].trim());\n if (Number.isFinite(pid) && !Number.isNaN(date.getTime())) result.set(pid, date);\n }\n } catch {\n // Return partial/empty data, matching the synchronous helper.\n }\n return result;\n}\n\ninterface CachedStartTime {\n /** ppid + command seen when the start time was read; a mismatch means PID reuse. */\n identity: string;\n startTime: Date;\n}\n\nfunction processIdentity(process: ProcessInfo): string {\n return `${process.ppid ?? \"\"}\\0${process.command}`;\n}\n\n/**\n * Create a snapshot capture function with its own start-time cache.\n *\n * Start times never change for a live process, so each PID is queried with\n * `ps lstart` once and reused on later refreshes. A cache entry is dropped\n * when its PID disappears from the base listing, and ignored when the PID's\n * ppid/command no longer match (PID reuse). cwd is re-read every refresh\n * because a process can change directory.\n */\nexport function createProcessSnapshotCapture(\n deps: { exec?: ProcessExec } = {},\n): ProcessSnapshotCapture {\n const exec = deps.exec ?? execFileText;\n const startTimeCache = new Map<number, CachedStartTime>();\n\n const cachedStartTime = (process: ProcessInfo): Date | undefined => {\n const cached = startTimeCache.get(process.pid);\n return cached?.identity === processIdentity(process) ? cached.startTime : undefined;\n };\n\n return async (namePatterns, options = {}) => {\n const names = normalizedProcessNames(\n namePatterns.filter((name) => Boolean(name) && VALID_EXECUTABLE_NAME.test(name)),\n );\n if (names.size === 0) return [];\n\n try {\n const output = await exec(\"ps\", [\"-axo\", \"pid=,ppid=,tty=,command=\"]);\n const { allPids, matched } = parseProcessList(output, names);\n\n for (const pid of startTimeCache.keys()) {\n if (!allPids.has(pid)) startTimeCache.delete(pid);\n }\n\n const { isCandidate } = options;\n const candidates = isCandidate ? matched.filter((process) => isCandidate(process)) : matched;\n const candidatePids = candidates.map((process) => process.pid);\n const uncachedPids = candidates\n .filter((process) => cachedStartTime(process) === undefined)\n .map((process) => process.pid);\n\n const [cwdMap, startTimeMap] = await Promise.all([\n batchGetProcessCwdsAsync(exec, candidatePids),\n batchGetProcessStartTimesAsync(exec, uncachedPids),\n ]);\n\n for (const process of candidates) {\n const startTime = startTimeMap.get(process.pid);\n if (startTime) {\n startTimeCache.set(process.pid, { identity: processIdentity(process), startTime });\n }\n }\n\n const candidateSet = new Set(candidatePids);\n return matched.map((process) =>\n candidateSet.has(process.pid)\n ? {\n ...process,\n cwd: cwdMap.get(process.pid) || \"\",\n startTime: cachedStartTime(process),\n }\n : process,\n );\n } catch {\n return [];\n }\n };\n}\n\n/**\n * Capture and enrich relevant processes without blocking the event loop.\n * One base process listing is shared by every requested executable name;\n * only processes accepted by `options.isCandidate` are enriched.\n */\nexport const captureProcessSnapshot: ProcessSnapshotCapture = createProcessSnapshotCapture();\n\n/**\n * Batch-get current working directories for multiple PIDs.\n *\n * Single `lsof -a -d cwd -Fn -p PID1,PID2,...` call.\n * Returns partial results — if lsof fails for one PID, others still return.\n */\nexport function batchGetProcessCwds(pids: number[]): Map<number, string> {\n const result = new Map<number, string>();\n if (pids.length === 0) return result;\n\n try {\n const output = execFileSync(\"lsof\", [\"-a\", \"-d\", \"cwd\", \"-Fn\", \"-p\", pids.join(\",\")], {\n encoding: \"utf-8\",\n stdio: [\"pipe\", \"pipe\", \"ignore\"],\n });\n\n // lsof output format: p{PID}\\nn{path}\\np{PID}\\nn{path}...\n let currentPid: number | null = null;\n for (const line of output.trim().split(\"\\n\")) {\n if (line.startsWith(\"p\")) {\n currentPid = parseInt(line.slice(1), 10);\n } else if (line.startsWith(\"n\") && currentPid !== null) {\n result.set(currentPid, line.slice(1));\n currentPid = null;\n }\n }\n } catch {\n // Try per-PID fallback with pwdx (Linux)\n for (const pid of pids) {\n try {\n const output = execFileSync(\"pwdx\", [String(pid)], {\n encoding: \"utf-8\",\n stdio: [\"pipe\", \"pipe\", \"ignore\"],\n });\n const match = output.match(/^\\d+:\\s*(.+)$/);\n if (match) {\n result.set(pid, match[1].trim());\n }\n } catch {\n // Skip this PID\n }\n }\n }\n\n return result;\n}\n\n/**\n * Batch-get process start times for multiple PIDs.\n *\n * Single `ps -o pid=,lstart= -p PID1,PID2,...` call.\n * Uses lstart format which gives full timestamp (e.g., \"Thu Feb 5 16:00:57 2026\").\n * Returns partial results.\n */\nexport function batchGetProcessStartTimes(pids: number[]): Map<number, Date> {\n const result = new Map<number, Date>();\n if (pids.length === 0) return result;\n\n try {\n const output = execFileSync(\"ps\", [\"-o\", \"pid=,lstart=\", \"-p\", pids.join(\",\")], {\n encoding: \"utf-8\",\n });\n\n for (const rawLine of output.split(\"\\n\")) {\n const line = rawLine.trim();\n if (!line) continue;\n\n // Format: \" PID DAY MON DD HH:MM:SS YYYY\"\n // e.g., \" 78070 Wed Mar 18 23:18:01 2026\"\n const match = line.match(/^\\s*(\\d+)\\s+(.+)$/);\n if (!match) continue;\n\n const pid = parseInt(match[1], 10);\n const dateStr = match[2].trim();\n\n if (!Number.isFinite(pid)) continue;\n\n const date = new Date(dateStr);\n if (!Number.isNaN(date.getTime())) {\n result.set(pid, date);\n }\n }\n } catch {\n // Return whatever we have\n }\n\n return result;\n}\n\n/**\n * Enrich ProcessInfo array with cwd and startTime.\n *\n * Calls batchGetProcessCwds and batchGetProcessStartTimes in batched shell calls,\n * then populates each ProcessInfo in-place. Returns partial results —\n * if a PID fails, that process keeps empty cwd / undefined startTime.\n */\nexport function enrichProcesses(processes: ProcessInfo[]): ProcessInfo[] {\n if (processes.length === 0) return processes;\n\n const pids = processes.map((p) => p.pid);\n const cwdMap = batchGetProcessCwds(pids);\n const startTimeMap = batchGetProcessStartTimes(pids);\n\n for (const proc of processes) {\n proc.cwd = cwdMap.get(proc.pid) || \"\";\n proc.startTime = startTimeMap.get(proc.pid);\n }\n\n return processes;\n}\n\nfunction isSameTerminalProcess(proc: ProcessInfo, matched: ProcessInfo): boolean {\n const sameTty = proc.tty !== \"\" && proc.tty !== \"?\" && proc.tty === matched.tty;\n const sameCwd = proc.cwd !== \"\" && proc.cwd === matched.cwd;\n\n return sameTty && (sameCwd || proc.cwd === \"\" || matched.cwd === \"\");\n}\n\nfunction matchesProcessIdentity(proc: ProcessInfo, matched: ProcessInfo): boolean {\n return proc.pid === matched.pid || isSameTerminalProcess(proc, matched);\n}\n\nexport function findWrapperProcess(\n processes: ProcessInfo[],\n child: ProcessInfo,\n): ProcessInfo | undefined {\n return processes.find(\n (proc) =>\n proc.pid !== child.pid && child.ppid === proc.pid && matchesProcessIdentity(proc, child),\n );\n}\n\n/**\n * Find parent wrapper processes that should not be reported as separate agents.\n *\n * A process is considered a wrapper when it is the parent of another candidate\n * agent process in the same terminal/worktree, or when it points at the same\n * terminal/worktree as an already session-matched process.\n */\nexport function findWrapperProcessPids(\n processes: ProcessInfo[],\n matchedProcesses: ProcessInfo[] = [],\n): Set<number> {\n const wrappers = new Set<number>();\n\n for (const child of processes) {\n const wrapper = findWrapperProcess(processes, child);\n if (wrapper) {\n wrappers.add(wrapper.pid);\n }\n }\n\n for (const proc of processes) {\n if (\n matchedProcesses.some(\n (matched) => proc.pid !== matched.pid && isSameTerminalProcess(proc, matched),\n )\n ) {\n wrappers.add(proc.pid);\n }\n }\n\n return wrappers;\n}\n\n/**\n * Get the TTY device for a specific process\n */\nexport function getProcessTty(pid: number): string {\n try {\n const output = execFileSync(\"ps\", [\"-p\", String(pid), \"-o\", \"tty=\"], { encoding: \"utf-8\" });\n\n const tty = output.trim();\n return tty.startsWith(\"/dev/\") ? tty.slice(5) : tty;\n } catch {\n return \"?\";\n }\n}\n"],"names":["fs","path","execFile","execFileSync","PROCESS_EXEC_MAX_BUFFER","VALID_EXECUTABLE_NAME","ABSOLUTE_PATH","PATH_SEPARATOR","pathBasename","executable","basename","replace","toLowerCase","isFile","filePath","statSync","throwIfNoEntry","executablePath","command","parts","trim","split","first","length","test","prefix","i","executableBasename","normalizeExecutableName","name","lower","endsWith","slice","normalizedProcessNames","namePatterns","Set","filter","Boolean","map","matchesExecutableName","names","tokens","has","mayContinuePath","some","token","filterByProcessNames","processes","size","process","listAgentProcesses","namePattern","output","encoding","line","match","pid","parseInt","ppid","Number","isNaN","tty","ttyShort","startsWith","push","cwd","execFileText","file","args","Promise","resolve","reject","maxBuffer","error","stdout","parseProcessList","allPids","matched","add","getProcessCwdsAsync","pids","batchGetProcessCwdsAsync","exec","result","Map","join","currentPid","set","entries","all","String","entry","batchGetProcessStartTimesAsync","rawLine","date","Date","isFinite","getTime","processIdentity","createProcessSnapshotCapture","deps","startTimeCache","cachedStartTime","cached","get","identity","startTime","undefined","options","keys","delete","isCandidate","candidates","candidatePids","uncachedPids","cwdMap","startTimeMap","candidateSet","captureProcessSnapshot","batchGetProcessCwds","stdio","batchGetProcessStartTimes","dateStr","enrichProcesses","p","proc","isSameTerminalProcess","sameTty","sameCwd","matchesProcessIdentity","findWrapperProcess","child","find","findWrapperProcessPids","matchedProcesses","wrappers","wrapper","getProcessTty"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ai-devkit/agent-manager",
3
- "version": "0.38.0",
3
+ "version": "0.40.0",
4
4
  "description": "Standalone agent detection and management utilities for AI DevKit",
5
5
  "keywords": [
6
6
  "agent",
@@ -0,0 +1,210 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { fileURLToPath } from "node:url";
3
+ import { describe, expect, it, vi } from "vitest";
4
+ import { parseDevinCapacity } from "../../capacity/providers/devin.js";
5
+ import { probeDevinCapacity } from "../../capacity/sources/devin.js";
6
+ import { resolveDevinCredential } from "../../harnesses/devin/credentials.js";
7
+
8
+ const checkedAt = "2026-10-05T10:00:00.000Z";
9
+ const fixture = (name: string) =>
10
+ readFile(fileURLToPath(new URL(`./fixtures/${name}`, import.meta.url)), "utf8");
11
+
12
+ const credentialFiles = async (path: string): Promise<string> => {
13
+ if (path === "/users/test/.local/share/devin/credentials.toml") {
14
+ return fixture("devin-credentials.toml");
15
+ }
16
+ if (path === "/users/test/.config/devin/config.json") {
17
+ return fixture("devin-config.json");
18
+ }
19
+ throw Object.assign(new Error("missing"), { code: "ENOENT" });
20
+ };
21
+
22
+ describe("devin credential resolution", () => {
23
+ it("prefers DEVIN_API_KEY and DEVIN_ORG_ID without reading files", async () => {
24
+ const reader = vi.fn(credentialFiles);
25
+ await expect(
26
+ resolveDevinCredential({
27
+ env: { DEVIN_API_KEY: "env-key", DEVIN_ORG_ID: "org-env" },
28
+ readFile: reader,
29
+ }),
30
+ ).resolves.toEqual({ key: "env-key", orgId: "org-env" });
31
+ expect(reader).not.toHaveBeenCalled();
32
+ });
33
+
34
+ it("reads the key from credentials.toml and org from config.json", async () => {
35
+ const reader = vi.fn(credentialFiles);
36
+ await expect(
37
+ resolveDevinCredential({ env: { HOME: "/users/test" }, readFile: reader }),
38
+ ).resolves.toEqual({ key: "devin-test-key", orgId: "org-test1234" });
39
+ expect(reader).toHaveBeenCalledWith("/users/test/.local/share/devin/credentials.toml", "utf8");
40
+ expect(reader).toHaveBeenCalledWith("/users/test/.config/devin/config.json", "utf8");
41
+ });
42
+
43
+ it.each([
44
+ [
45
+ "missing credentials file",
46
+ async (path: string) => {
47
+ if (path.endsWith("config.json")) return fixture("devin-config.json");
48
+ throw Object.assign(new Error("missing"), { code: "ENOENT" });
49
+ },
50
+ /Devin API key not found/,
51
+ ],
52
+ [
53
+ "malformed credentials file",
54
+ async (path: string) => {
55
+ if (path.endsWith("credentials.toml")) return "not = [valid";
56
+ return fixture("devin-config.json");
57
+ },
58
+ /credentials file is malformed/,
59
+ ],
60
+ [
61
+ "credentials without key",
62
+ async (path: string) => {
63
+ if (path.endsWith("credentials.toml")) return 'api_server_url = "https://x"';
64
+ return fixture("devin-config.json");
65
+ },
66
+ /Devin API key not found/,
67
+ ],
68
+ [
69
+ "missing config file",
70
+ async (path: string) => {
71
+ if (path.endsWith("credentials.toml")) return fixture("devin-credentials.toml");
72
+ throw Object.assign(new Error("missing"), { code: "ENOENT" });
73
+ },
74
+ /Devin organization id not found/,
75
+ ],
76
+ [
77
+ "config without org_id",
78
+ async (path: string) => {
79
+ if (path.endsWith("credentials.toml")) return fixture("devin-credentials.toml");
80
+ return JSON.stringify({ devin: {} });
81
+ },
82
+ /Devin organization id not found/,
83
+ ],
84
+ ])("fails clearly for %s without exposing file content", async (_name, readFileImpl, pattern) => {
85
+ await expect(
86
+ resolveDevinCredential({ env: { HOME: "/users/test" }, readFile: readFileImpl }),
87
+ ).rejects.toThrow(pattern);
88
+ });
89
+ });
90
+
91
+ describe("devin quota mapping", () => {
92
+ it("maps daily and weekly windows with credits", async () => {
93
+ const report = parseDevinCapacity(JSON.parse(await fixture("devin-quota.json")));
94
+ expect(report).toMatchObject({
95
+ authenticated: true,
96
+ available: "yes",
97
+ creditsRemaining: 12.5,
98
+ });
99
+ expect(report.windows).toEqual([
100
+ {
101
+ id: "devin:daily",
102
+ label: "Daily",
103
+ durationMinutes: 1440,
104
+ usedPercent: 25,
105
+ resetsAt: "2026-10-06T08:00:00.000Z",
106
+ },
107
+ {
108
+ id: "devin:weekly",
109
+ label: "Weekly",
110
+ durationMinutes: 10080,
111
+ usedPercent: 60,
112
+ resetsAt: "2026-10-11T08:00:00.000Z",
113
+ },
114
+ ]);
115
+ });
116
+
117
+ it("omits the daily window when hide_daily_quota is true", () => {
118
+ const report = parseDevinCapacity({
119
+ is_quota_plan: true,
120
+ has_quota_allocation: true,
121
+ daily_percentage: 90,
122
+ weekly_percentage: 10,
123
+ hide_daily_quota: true,
124
+ });
125
+ expect(report.windows.map((window) => window.id)).toEqual(["devin:weekly"]);
126
+ });
127
+
128
+ it("reports authenticated unknown availability for non-quota plans", async () => {
129
+ const report = parseDevinCapacity(JSON.parse(await fixture("devin-quota-nonplan.json")));
130
+ expect(report).toEqual({
131
+ authenticated: true,
132
+ available: "unknown",
133
+ windows: [],
134
+ creditsRemaining: null,
135
+ });
136
+ });
137
+
138
+ it("clamps percentages and degrades missing fields honestly", () => {
139
+ const report = parseDevinCapacity({
140
+ is_quota_plan: true,
141
+ has_quota_allocation: true,
142
+ daily_percentage: 140,
143
+ weekly_reset_at: "not-a-date",
144
+ overage_balance: "unknown",
145
+ });
146
+ const [daily, weekly] = report.windows;
147
+ expect(daily.usedPercent).toBe(100);
148
+ expect(weekly.usedPercent).toBeNull();
149
+ expect(weekly.resetsAt).toBeNull();
150
+ expect(report.creditsRemaining).toBeNull();
151
+ expect(report.available).toBe("no");
152
+ });
153
+
154
+ it("rejects non-object responses", () => {
155
+ expect(() => parseDevinCapacity("nope")).toThrow(/Devin quota/);
156
+ expect(() => parseDevinCapacity(null)).toThrow(/Devin quota/);
157
+ });
158
+ });
159
+
160
+ describe("devin request", () => {
161
+ it("GETs the org quota endpoint with only Bearer authentication", async () => {
162
+ const body = await fixture("devin-quota.json");
163
+ const fetch = vi.fn(async () => new Response(body, { status: 200 }));
164
+ const report = await probeDevinCapacity({
165
+ checkedAt,
166
+ env: { DEVIN_API_KEY: "request-test-key", DEVIN_ORG_ID: "org-abc" },
167
+ fetch,
168
+ });
169
+ expect(fetch).toHaveBeenCalledWith(
170
+ "https://app.devin.ai/api/org-abc/billing/quota/usage",
171
+ expect.objectContaining({
172
+ method: "GET",
173
+ headers: { Authorization: "Bearer request-test-key" },
174
+ }),
175
+ );
176
+ expect(report.harness).toBe("devin");
177
+ expect(report.provider).toBe("devin");
178
+ expect(JSON.stringify(report)).not.toContain("request-test-key");
179
+ });
180
+
181
+ it.each([401, 403])("reports HTTP %i as unauthenticated", async (status) => {
182
+ const report = await probeDevinCapacity({
183
+ checkedAt,
184
+ env: { DEVIN_API_KEY: "request-test-key", DEVIN_ORG_ID: "org-abc" },
185
+ fetch: vi.fn(async () => new Response("denied", { status })),
186
+ });
187
+ expect(report).toMatchObject({
188
+ authenticated: false,
189
+ available: "unknown",
190
+ windows: [],
191
+ });
192
+ });
193
+
194
+ it("rejects other non-200 and malformed JSON responses with sanitized errors", async () => {
195
+ await expect(
196
+ probeDevinCapacity({
197
+ checkedAt,
198
+ env: { DEVIN_API_KEY: "never-expose-this-key", DEVIN_ORG_ID: "org-abc" },
199
+ fetch: vi.fn(async () => new Response("private body", { status: 503 })),
200
+ }),
201
+ ).rejects.toThrow("Devin quota request failed: HTTP 503");
202
+ await expect(
203
+ probeDevinCapacity({
204
+ checkedAt,
205
+ env: { DEVIN_API_KEY: "never-expose-this-key", DEVIN_ORG_ID: "org-abc" },
206
+ fetch: vi.fn(async () => new Response("not-json", { status: 200 })),
207
+ }),
208
+ ).rejects.toThrow("Devin quota response is not valid JSON");
209
+ });
210
+ });
@@ -0,0 +1,6 @@
1
+ {
2
+ "version": 1,
3
+ "devin": {
4
+ "org_id": "org-test1234"
5
+ }
6
+ }
@@ -0,0 +1,4 @@
1
+ windsurf_api_key = "devin-test-key"
2
+ api_server_url = "https://server.codeium.com"
3
+ devin_webapp_host = "app.devin.ai"
4
+ devin_api_url = "https://api.devin.ai"
@@ -0,0 +1,5 @@
1
+ {
2
+ "is_quota_plan": false,
3
+ "has_quota_allocation": false,
4
+ "overage_balance": 0.0
5
+ }
@@ -0,0 +1,10 @@
1
+ {
2
+ "is_quota_plan": true,
3
+ "has_quota_allocation": true,
4
+ "daily_percentage": 25,
5
+ "weekly_percentage": 60,
6
+ "daily_reset_at": "2026-10-06T00:00:00-08:00",
7
+ "weekly_reset_at": "2026-10-11T00:00:00-08:00",
8
+ "overage_balance": 12.5,
9
+ "hide_daily_quota": false
10
+ }