@ai-devkit/agent-manager 0.41.0 → 0.42.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 (101) hide show
  1. package/dist/AgentManager.d.ts.map +1 -1
  2. package/dist/AgentManager.js +25 -9
  3. package/dist/AgentManager.js.map +1 -1
  4. package/dist/__tests__/fixtures.test.js.map +1 -1
  5. package/dist/__tests__/harnesses/devin/DevinAdapter.test.js.map +1 -1
  6. package/dist/__tests__/harnesses/devin/DevinSessionParser.test.js.map +1 -1
  7. package/dist/__tests__/harnesses/muse/MuseAdapter.test.js +247 -0
  8. package/dist/__tests__/harnesses/muse/MuseAdapter.test.js.map +1 -0
  9. package/dist/__tests__/harnesses/muse/MuseAgentMapper.test.js +81 -0
  10. package/dist/__tests__/harnesses/muse/MuseAgentMapper.test.js.map +1 -0
  11. package/dist/__tests__/harnesses/muse/MuseSessionLocator.test.js +185 -0
  12. package/dist/__tests__/harnesses/muse/MuseSessionLocator.test.js.map +1 -0
  13. package/dist/__tests__/harnesses/muse/MuseSessionParser.test.js +328 -0
  14. package/dist/__tests__/harnesses/muse/MuseSessionParser.test.js.map +1 -0
  15. package/dist/__tests__/harnesses/opencode/OpenCodeSessionParser.test.js.map +1 -1
  16. package/dist/__tests__/harnesses/readiness/AgentReadiness.test.js +42 -1
  17. package/dist/__tests__/harnesses/readiness/AgentReadiness.test.js.map +1 -1
  18. package/dist/__tests__/harnesses/runtimeProfiles.test.js +12 -0
  19. package/dist/__tests__/harnesses/runtimeProfiles.test.js.map +1 -1
  20. package/dist/__tests__/utils/process.test.js +71 -1
  21. package/dist/__tests__/utils/process.test.js.map +1 -1
  22. package/dist/adapters/AgentAdapter.d.ts +8 -1
  23. package/dist/adapters/AgentAdapter.d.ts.map +1 -1
  24. package/dist/adapters/AgentAdapter.js +2 -1
  25. package/dist/adapters/AgentAdapter.js.map +1 -1
  26. package/dist/fixtures/capture.d.ts.map +1 -1
  27. package/dist/fixtures/capture.js +5 -4
  28. package/dist/fixtures/capture.js.map +1 -1
  29. package/dist/harnesses/devin/DevinAdapter.d.ts.map +1 -1
  30. package/dist/harnesses/devin/DevinAdapter.js.map +1 -1
  31. package/dist/harnesses/devin/DevinAgentMapper.d.ts.map +1 -1
  32. package/dist/harnesses/devin/DevinAgentMapper.js.map +1 -1
  33. package/dist/harnesses/devin/DevinSessionParser.d.ts.map +1 -1
  34. package/dist/harnesses/devin/DevinSessionParser.js.map +1 -1
  35. package/dist/harnesses/index.d.ts.map +1 -1
  36. package/dist/harnesses/index.js +3 -1
  37. package/dist/harnesses/index.js.map +1 -1
  38. package/dist/harnesses/muse/MuseAdapter.d.ts +33 -0
  39. package/dist/harnesses/muse/MuseAdapter.d.ts.map +1 -0
  40. package/dist/harnesses/muse/MuseAdapter.js +122 -0
  41. package/dist/harnesses/muse/MuseAdapter.js.map +1 -0
  42. package/dist/harnesses/muse/MuseAgentMapper.d.ts +15 -0
  43. package/dist/harnesses/muse/MuseAgentMapper.d.ts.map +1 -0
  44. package/dist/harnesses/muse/MuseAgentMapper.js +29 -0
  45. package/dist/harnesses/muse/MuseAgentMapper.js.map +1 -0
  46. package/dist/harnesses/muse/MuseSessionLocator.d.ts +61 -0
  47. package/dist/harnesses/muse/MuseSessionLocator.d.ts.map +1 -0
  48. package/dist/harnesses/muse/MuseSessionLocator.js +233 -0
  49. package/dist/harnesses/muse/MuseSessionLocator.js.map +1 -0
  50. package/dist/harnesses/muse/MuseSessionParser.d.ts +77 -0
  51. package/dist/harnesses/muse/MuseSessionParser.d.ts.map +1 -0
  52. package/dist/harnesses/muse/MuseSessionParser.js +350 -0
  53. package/dist/harnesses/muse/MuseSessionParser.js.map +1 -0
  54. package/dist/harnesses/muse/readiness.d.ts +3 -0
  55. package/dist/harnesses/muse/readiness.d.ts.map +1 -0
  56. package/dist/harnesses/muse/readiness.js +37 -0
  57. package/dist/harnesses/muse/readiness.js.map +1 -0
  58. package/dist/harnesses/readiness/AgentReadiness.d.ts.map +1 -1
  59. package/dist/harnesses/readiness/AgentReadiness.js +3 -1
  60. package/dist/harnesses/readiness/AgentReadiness.js.map +1 -1
  61. package/dist/harnesses/readiness/types.d.ts +1 -1
  62. package/dist/harnesses/readiness/types.d.ts.map +1 -1
  63. package/dist/harnesses/runtimeProfiles.d.ts.map +1 -1
  64. package/dist/harnesses/runtimeProfiles.js +2 -1
  65. package/dist/harnesses/runtimeProfiles.js.map +1 -1
  66. package/dist/harnesses/shared.d.ts +1 -1
  67. package/dist/harnesses/shared.d.ts.map +1 -1
  68. package/dist/harnesses/shared.js +6 -5
  69. package/dist/harnesses/shared.js.map +1 -1
  70. package/dist/utils/process.d.ts +23 -1
  71. package/dist/utils/process.d.ts.map +1 -1
  72. package/dist/utils/process.js +44 -8
  73. package/dist/utils/process.js.map +1 -1
  74. package/package.json +1 -1
  75. package/src/AgentManager.ts +34 -8
  76. package/src/__tests__/fixtures.test.ts +15 -17
  77. package/src/__tests__/harnesses/devin/DevinAdapter.test.ts +1 -3
  78. package/src/__tests__/harnesses/devin/DevinSessionParser.test.ts +3 -9
  79. package/src/__tests__/harnesses/muse/MuseAdapter.test.ts +197 -0
  80. package/src/__tests__/harnesses/muse/MuseAgentMapper.test.ts +83 -0
  81. package/src/__tests__/harnesses/muse/MuseSessionLocator.test.ts +177 -0
  82. package/src/__tests__/harnesses/muse/MuseSessionParser.test.ts +279 -0
  83. package/src/__tests__/harnesses/opencode/OpenCodeSessionParser.test.ts +3 -2
  84. package/src/__tests__/harnesses/readiness/AgentReadiness.test.ts +36 -0
  85. package/src/__tests__/harnesses/runtimeProfiles.test.ts +13 -0
  86. package/src/__tests__/utils/process.test.ts +30 -0
  87. package/src/adapters/AgentAdapter.ts +9 -0
  88. package/src/fixtures/capture.ts +32 -17
  89. package/src/harnesses/devin/DevinAdapter.ts +3 -15
  90. package/src/harnesses/devin/DevinAgentMapper.ts +1 -5
  91. package/src/harnesses/devin/DevinSessionParser.ts +3 -12
  92. package/src/harnesses/index.ts +2 -0
  93. package/src/harnesses/muse/MuseAdapter.ts +157 -0
  94. package/src/harnesses/muse/MuseAgentMapper.ts +33 -0
  95. package/src/harnesses/muse/MuseSessionLocator.ts +286 -0
  96. package/src/harnesses/muse/MuseSessionParser.ts +435 -0
  97. package/src/harnesses/muse/readiness.ts +38 -0
  98. package/src/harnesses/readiness/AgentReadiness.ts +2 -0
  99. package/src/harnesses/runtimeProfiles.ts +1 -0
  100. package/src/harnesses/shared.ts +10 -5
  101. package/src/utils/process.ts +61 -9
@@ -3,7 +3,7 @@
3
3
  * matching and parsing; these cover the parts that are the same everywhere.
4
4
  */ import { AgentStatus } from "../adapters/AgentAdapter.js";
5
5
  import { generateAgentName } from "../utils/matching.js";
6
- import { captureProcessSnapshot, executableBasename, filterByProcessNames } from "../utils/process.js";
6
+ import { captureProcessSnapshot, executableBasename, filterByProcessNames, snapshotOptions } from "../utils/process.js";
7
7
  /** A session untouched for longer than this is IDLE. */ export const IDLE_THRESHOLD_MINUTES = 5;
8
8
  /** Maximum length of a session summary derived from a user prompt. */ export const SUMMARY_MAX_LENGTH = 120;
9
9
  /**
@@ -11,10 +11,11 @@ import { captureProcessSnapshot, executableBasename, filterByProcessNames } from
11
11
  * capture when called standalone. `relevant` also keeps helper processes
12
12
  * (e.g. a harness's child runtime) that detection may need to walk through.
13
13
  */ export async function findHarnessProcesses(adapter, context) {
14
- const snapshot = context?.processes ?? await captureProcessSnapshot(adapter.processNames, {
15
- isCandidate: (process1)=>adapter.canHandle(process1)
16
- });
17
- const relevant = filterByProcessNames(snapshot, adapter.processNames);
14
+ const prefixes = adapter.processNamePrefixes ? [
15
+ ...adapter.processNamePrefixes
16
+ ] : [];
17
+ const snapshot = context?.processes ?? await captureProcessSnapshot(adapter.processNames, snapshotOptions(prefixes, (process1)=>adapter.canHandle(process1)));
18
+ const relevant = filterByProcessNames(snapshot, adapter.processNames, prefixes);
18
19
  const byPid = new Map();
19
20
  for (const process1 of relevant){
20
21
  if (!byPid.has(process1.pid) && adapter.canHandle(process1)) byPid.set(process1.pid, process1);
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/harnesses/shared.ts"],"sourcesContent":["/**\n * Helpers shared by the harness adapters. Each harness keeps its own session\n * matching and parsing; these cover the parts that are the same everywhere.\n */\n\nimport type {\n AgentAdapter,\n AgentDetectionContext,\n AgentInfo,\n AgentType,\n ProcessInfo,\n} from \"../adapters/AgentAdapter.js\";\nimport { AgentStatus } from \"../adapters/AgentAdapter.js\";\nimport { generateAgentName } from \"../utils/matching.js\";\nimport {\n captureProcessSnapshot,\n executableBasename,\n filterByProcessNames,\n} from \"../utils/process.js\";\n\n/** A session untouched for longer than this is IDLE. */\nexport const IDLE_THRESHOLD_MINUTES = 5;\n\n/** Maximum length of a session summary derived from a user prompt. */\nexport const SUMMARY_MAX_LENGTH = 120;\n\nexport interface HarnessProcesses {\n /** Every process whose executable is one of the adapter's `processNames`. */\n relevant: ProcessInfo[];\n /** The subset `canHandle` accepts (deduplicated by PID): the agents. */\n processes: ProcessInfo[];\n}\n\n/**\n * The adapter's processes from the manager's shared snapshot, or from its own\n * capture when called standalone. `relevant` also keeps helper processes\n * (e.g. a harness's child runtime) that detection may need to walk through.\n */\nexport async function findHarnessProcesses(\n adapter: Pick<AgentAdapter, \"canHandle\"> & { readonly processNames: readonly string[] },\n context?: AgentDetectionContext,\n): Promise<HarnessProcesses> {\n const snapshot =\n context?.processes ??\n (await captureProcessSnapshot(adapter.processNames, {\n isCandidate: (process) => adapter.canHandle(process),\n }));\n const relevant = filterByProcessNames(snapshot, adapter.processNames);\n\n const byPid = new Map<number, ProcessInfo>();\n for (const process of relevant) {\n if (!byPid.has(process.pid) && adapter.canHandle(process)) byPid.set(process.pid, process);\n }\n return { relevant, processes: Array.from(byPid.values()) };\n}\n\n/** Whether argv[0]'s basename is `name` or `name.exe`. */\nexport function matchesExecutable(command: string, name: string): boolean {\n const base = executableBasename(command);\n return base === name || base === `${name}.exe`;\n}\n\n/**\n * The placeholder agent for a running process with no matched session:\n * RUNNING, with a `pid-<pid>` session id.\n */\nexport function processOnlyAgent(\n type: AgentType,\n processInfo: ProcessInfo,\n { summary, cwd = processInfo.cwd ?? \"\" }: { summary: string; cwd?: string },\n): AgentInfo {\n return {\n name: generateAgentName(cwd, processInfo.pid),\n type,\n status: AgentStatus.RUNNING,\n summary,\n pid: processInfo.pid,\n projectPath: cwd,\n sessionId: `pid-${processInfo.pid}`,\n lastActive: new Date(),\n };\n}\n\nexport function isIdle(lastActive: Date, now = Date.now()): boolean {\n return (now - lastActive.getTime()) / 60000 > IDLE_THRESHOLD_MINUTES;\n}\n\nexport function homeDir(): string {\n return process.env.HOME || process.env.USERPROFILE || \"\";\n}\n\n/** ISO strings, or epoch numbers in seconds or milliseconds; null when invalid. */\nexport function parseTimestamp(value: unknown): Date | null {\n if (typeof value === \"number\") {\n const date = new Date(value < 1_000_000_000_000 ? value * 1000 : value);\n return Number.isNaN(date.getTime()) ? null : date;\n }\n if (typeof value !== \"string\" || !value) return null;\n const date = new Date(value);\n return Number.isNaN(date.getTime()) ? null : date;\n}\n\nexport function truncate(value: string, maxLength: number): string {\n if (value.length <= maxLength) return value;\n return `${value.slice(0, maxLength - 3)}...`;\n}\n\n/** Flatten a string, or an array of `{ text }` blocks, to plain text. */\nexport function flattenTextBlocks(content: unknown): string {\n if (typeof content === \"string\") return content;\n if (!Array.isArray(content)) return \"\";\n return content\n .map((block) =>\n block && typeof block === \"object\" && typeof (block as { text?: unknown }).text === \"string\"\n ? (block as { text: string }).text\n : \"\",\n )\n .join(\"\");\n}\n"],"names":["AgentStatus","generateAgentName","captureProcessSnapshot","executableBasename","filterByProcessNames","IDLE_THRESHOLD_MINUTES","SUMMARY_MAX_LENGTH","findHarnessProcesses","adapter","context","snapshot","processes","processNames","isCandidate","process","canHandle","relevant","byPid","Map","has","pid","set","Array","from","values","matchesExecutable","command","name","base","processOnlyAgent","type","processInfo","summary","cwd","status","RUNNING","projectPath","sessionId","lastActive","Date","isIdle","now","getTime","homeDir","env","HOME","USERPROFILE","parseTimestamp","value","date","Number","isNaN","truncate","maxLength","length","slice","flattenTextBlocks","content","isArray","map","block","text","join"],"mappings":"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"}
1
+ {"version":3,"sources":["../../src/harnesses/shared.ts"],"sourcesContent":["/**\n * Helpers shared by the harness adapters. Each harness keeps its own session\n * matching and parsing; these cover the parts that are the same everywhere.\n */\n\nimport type {\n AgentAdapter,\n AgentDetectionContext,\n AgentInfo,\n AgentType,\n ProcessInfo,\n} from \"../adapters/AgentAdapter.js\";\nimport { AgentStatus } from \"../adapters/AgentAdapter.js\";\nimport { generateAgentName } from \"../utils/matching.js\";\nimport {\n captureProcessSnapshot,\n executableBasename,\n filterByProcessNames,\n snapshotOptions,\n} from \"../utils/process.js\";\n\n/** A session untouched for longer than this is IDLE. */\nexport const IDLE_THRESHOLD_MINUTES = 5;\n\n/** Maximum length of a session summary derived from a user prompt. */\nexport const SUMMARY_MAX_LENGTH = 120;\n\nexport interface HarnessProcesses {\n /** Every process whose executable is one of the adapter's `processNames`. */\n relevant: ProcessInfo[];\n /** The subset `canHandle` accepts (deduplicated by PID): the agents. */\n processes: ProcessInfo[];\n}\n\n/**\n * The adapter's processes from the manager's shared snapshot, or from its own\n * capture when called standalone. `relevant` also keeps helper processes\n * (e.g. a harness's child runtime) that detection may need to walk through.\n */\nexport async function findHarnessProcesses(\n adapter: Pick<AgentAdapter, \"canHandle\" | \"processNamePrefixes\"> & {\n readonly processNames: readonly string[];\n },\n context?: AgentDetectionContext,\n): Promise<HarnessProcesses> {\n const prefixes = adapter.processNamePrefixes ? [...adapter.processNamePrefixes] : [];\n const snapshot =\n context?.processes ??\n (await captureProcessSnapshot(\n adapter.processNames,\n snapshotOptions(prefixes, (process) => adapter.canHandle(process)),\n ));\n const relevant = filterByProcessNames(snapshot, adapter.processNames, prefixes);\n\n const byPid = new Map<number, ProcessInfo>();\n for (const process of relevant) {\n if (!byPid.has(process.pid) && adapter.canHandle(process)) byPid.set(process.pid, process);\n }\n return { relevant, processes: Array.from(byPid.values()) };\n}\n\n/** Whether argv[0]'s basename is `name` or `name.exe`. */\nexport function matchesExecutable(command: string, name: string): boolean {\n const base = executableBasename(command);\n return base === name || base === `${name}.exe`;\n}\n\n/**\n * The placeholder agent for a running process with no matched session:\n * RUNNING, with a `pid-<pid>` session id.\n */\nexport function processOnlyAgent(\n type: AgentType,\n processInfo: ProcessInfo,\n { summary, cwd = processInfo.cwd ?? \"\" }: { summary: string; cwd?: string },\n): AgentInfo {\n return {\n name: generateAgentName(cwd, processInfo.pid),\n type,\n status: AgentStatus.RUNNING,\n summary,\n pid: processInfo.pid,\n projectPath: cwd,\n sessionId: `pid-${processInfo.pid}`,\n lastActive: new Date(),\n };\n}\n\nexport function isIdle(lastActive: Date, now = Date.now()): boolean {\n return (now - lastActive.getTime()) / 60000 > IDLE_THRESHOLD_MINUTES;\n}\n\nexport function homeDir(): string {\n return process.env.HOME || process.env.USERPROFILE || \"\";\n}\n\n/** ISO strings, or epoch numbers in seconds or milliseconds; null when invalid. */\nexport function parseTimestamp(value: unknown): Date | null {\n if (typeof value === \"number\") {\n const date = new Date(value < 1_000_000_000_000 ? value * 1000 : value);\n return Number.isNaN(date.getTime()) ? null : date;\n }\n if (typeof value !== \"string\" || !value) return null;\n const date = new Date(value);\n return Number.isNaN(date.getTime()) ? null : date;\n}\n\nexport function truncate(value: string, maxLength: number): string {\n if (value.length <= maxLength) return value;\n return `${value.slice(0, maxLength - 3)}...`;\n}\n\n/** Flatten a string, or an array of `{ text }` blocks, to plain text. */\nexport function flattenTextBlocks(content: unknown): string {\n if (typeof content === \"string\") return content;\n if (!Array.isArray(content)) return \"\";\n return content\n .map((block) =>\n block && typeof block === \"object\" && typeof (block as { text?: unknown }).text === \"string\"\n ? (block as { text: string }).text\n : \"\",\n )\n .join(\"\");\n}\n"],"names":["AgentStatus","generateAgentName","captureProcessSnapshot","executableBasename","filterByProcessNames","snapshotOptions","IDLE_THRESHOLD_MINUTES","SUMMARY_MAX_LENGTH","findHarnessProcesses","adapter","context","prefixes","processNamePrefixes","snapshot","processes","processNames","process","canHandle","relevant","byPid","Map","has","pid","set","Array","from","values","matchesExecutable","command","name","base","processOnlyAgent","type","processInfo","summary","cwd","status","RUNNING","projectPath","sessionId","lastActive","Date","isIdle","now","getTime","homeDir","env","HOME","USERPROFILE","parseTimestamp","value","date","Number","isNaN","truncate","maxLength","length","slice","flattenTextBlocks","content","isArray","map","block","text","join"],"mappings":"AAAA;;;CAGC,GASD,SAASA,WAAW,QAAQ,8BAA8B;AAC1D,SAASC,iBAAiB,QAAQ,uBAAuB;AACzD,SACEC,sBAAsB,EACtBC,kBAAkB,EAClBC,oBAAoB,EACpBC,eAAe,QACV,sBAAsB;AAE7B,sDAAsD,GACtD,OAAO,MAAMC,yBAAyB,EAAE;AAExC,oEAAoE,GACpE,OAAO,MAAMC,qBAAqB,IAAI;AAStC;;;;CAIC,GACD,OAAO,eAAeC,qBACpBC,OAEC,EACDC,OAA+B;IAE/B,MAAMC,WAAWF,QAAQG,mBAAmB,GAAG;WAAIH,QAAQG,mBAAmB;KAAC,GAAG,EAAE;IACpF,MAAMC,WACJH,SAASI,aACR,MAAMZ,uBACLO,QAAQM,YAAY,EACpBV,gBAAgBM,UAAU,CAACK,WAAYP,QAAQQ,SAAS,CAACD;IAE7D,MAAME,WAAWd,qBAAqBS,UAAUJ,QAAQM,YAAY,EAAEJ;IAEtE,MAAMQ,QAAQ,IAAIC;IAClB,KAAK,MAAMJ,YAAWE,SAAU;QAC9B,IAAI,CAACC,MAAME,GAAG,CAACL,SAAQM,GAAG,KAAKb,QAAQQ,SAAS,CAACD,WAAUG,MAAMI,GAAG,CAACP,SAAQM,GAAG,EAAEN;IACpF;IACA,OAAO;QAAEE;QAAUJ,WAAWU,MAAMC,IAAI,CAACN,MAAMO,MAAM;IAAI;AAC3D;AAEA,wDAAwD,GACxD,OAAO,SAASC,kBAAkBC,OAAe,EAAEC,IAAY;IAC7D,MAAMC,OAAO3B,mBAAmByB;IAChC,OAAOE,SAASD,QAAQC,SAAS,GAAGD,KAAK,IAAI,CAAC;AAChD;AAEA;;;CAGC,GACD,OAAO,SAASE,iBACdC,IAAe,EACfC,WAAwB,EACxB,EAAEC,OAAO,EAAEC,MAAMF,YAAYE,GAAG,IAAI,EAAE,EAAqC;IAE3E,OAAO;QACLN,MAAM5B,kBAAkBkC,KAAKF,YAAYX,GAAG;QAC5CU;QACAI,QAAQpC,YAAYqC,OAAO;QAC3BH;QACAZ,KAAKW,YAAYX,GAAG;QACpBgB,aAAaH;QACbI,WAAW,CAAC,IAAI,EAAEN,YAAYX,GAAG,EAAE;QACnCkB,YAAY,IAAIC;IAClB;AACF;AAEA,OAAO,SAASC,OAAOF,UAAgB,EAAEG,MAAMF,KAAKE,GAAG,EAAE;IACvD,OAAO,AAACA,CAAAA,MAAMH,WAAWI,OAAO,EAAC,IAAK,QAAQtC;AAChD;AAEA,OAAO,SAASuC;IACd,OAAO7B,QAAQ8B,GAAG,CAACC,IAAI,IAAI/B,QAAQ8B,GAAG,CAACE,WAAW,IAAI;AACxD;AAEA,iFAAiF,GACjF,OAAO,SAASC,eAAeC,KAAc;IAC3C,IAAI,OAAOA,UAAU,UAAU;QAC7B,MAAMC,OAAO,IAAIV,KAAKS,QAAQ,oBAAoBA,QAAQ,OAAOA;QACjE,OAAOE,OAAOC,KAAK,CAACF,KAAKP,OAAO,MAAM,OAAOO;IAC/C;IACA,IAAI,OAAOD,UAAU,YAAY,CAACA,OAAO,OAAO;IAChD,MAAMC,OAAO,IAAIV,KAAKS;IACtB,OAAOE,OAAOC,KAAK,CAACF,KAAKP,OAAO,MAAM,OAAOO;AAC/C;AAEA,OAAO,SAASG,SAASJ,KAAa,EAAEK,SAAiB;IACvD,IAAIL,MAAMM,MAAM,IAAID,WAAW,OAAOL;IACtC,OAAO,GAAGA,MAAMO,KAAK,CAAC,GAAGF,YAAY,GAAG,GAAG,CAAC;AAC9C;AAEA,uEAAuE,GACvE,OAAO,SAASG,kBAAkBC,OAAgB;IAChD,IAAI,OAAOA,YAAY,UAAU,OAAOA;IACxC,IAAI,CAACnC,MAAMoC,OAAO,CAACD,UAAU,OAAO;IACpC,OAAOA,QACJE,GAAG,CAAC,CAACC,QACJA,SAAS,OAAOA,UAAU,YAAY,OAAO,AAACA,MAA6BC,IAAI,KAAK,WAChF,AAACD,MAA2BC,IAAI,GAChC,IAELC,IAAI,CAAC;AACV"}
@@ -19,7 +19,18 @@ import type { ProcessInfo } from "../adapters/AgentAdapter.js";
19
19
  */
20
20
  export declare function executablePath(command: string): string;
21
21
  export declare function executableBasename(command: string): string;
22
- export declare function filterByProcessNames(processes: readonly ProcessInfo[], namePatterns: readonly string[]): ProcessInfo[];
22
+ /**
23
+ * Whether argv[0]'s basename is exactly `prefix` or starts with `prefix-`.
24
+ *
25
+ * Covers versioned launchers such as `muse-bin-1.4.4-R5419.1` for prefix
26
+ * `muse`. This is a recall-oriented prefilter: adapters still apply
27
+ * `canHandle` for precise shaping, so a coincidental `prefix-*` binary
28
+ * cannot leak into another harness.
29
+ */
30
+ export declare function matchesExecutablePrefix(command: string, prefixes: ReadonlySet<string>): boolean;
31
+ /** Whether argv[0] matches one of `names` exactly or one of `prefixes`. */
32
+ export declare function matchesProcessPatterns(command: string, names: ReadonlySet<string>, prefixes?: ReadonlySet<string>): boolean;
33
+ export declare function filterByProcessNames(processes: readonly ProcessInfo[], namePatterns: readonly string[], namePrefixes?: readonly string[]): ProcessInfo[];
23
34
  /**
24
35
  * List running processes matching an agent executable name.
25
36
  *
@@ -39,8 +50,19 @@ export interface ProcessSnapshotOptions {
39
50
  * only) so adapters can walk parent chains. Omitted = enrich everything.
40
51
  */
41
52
  isCandidate?: (process: ProcessInfo) => boolean;
53
+ /**
54
+ * Basename prefixes matched like `processNamePrefixes`: exact or
55
+ * `<prefix>-*`. Same validation as namePatterns.
56
+ */
57
+ namePrefixes?: readonly string[];
42
58
  }
43
59
  export type ProcessSnapshotCapture = (namePatterns: readonly string[], options?: ProcessSnapshotOptions) => Promise<ProcessInfo[]>;
60
+ /**
61
+ * One construction site for snapshot options: `namePrefixes` is sent only
62
+ * when non-empty, so adapters without prefixes keep the exact legacy call
63
+ * shape that existing mock assertions pin.
64
+ */
65
+ export declare function snapshotOptions(namePrefixes: readonly string[], isCandidate: (process: ProcessInfo) => boolean): ProcessSnapshotOptions;
44
66
  /** Working directories of `pids` (async `lsof`); PIDs it cannot read are omitted. */
45
67
  export declare function getProcessCwdsAsync(pids: number[]): Promise<Map<number, string>>;
46
68
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"process.d.ts","sourceRoot":"","sources":["../../src/utils/process.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAKH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,6BAA6B,CAAC;AAmB/D;;;;;;;;;;GAUG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAWtD;AAED,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAE1D;AAkCD,wBAAgB,oBAAoB,CAClC,SAAS,EAAE,SAAS,WAAW,EAAE,EACjC,YAAY,EAAE,SAAS,MAAM,EAAE,GAC9B,WAAW,EAAE,CAIf;AAED;;;;;;;;GAQG;AACH,wBAAgB,kBAAkB,CAAC,WAAW,EAAE,MAAM,GAAG,WAAW,EAAE,CA0CrE;AAED,6EAA6E;AAC7E,MAAM,MAAM,WAAW,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,SAAS,MAAM,EAAE,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;AAErF,MAAM,WAAW,sBAAsB;IACrC;;;;OAIG;IACH,WAAW,CAAC,EAAE,CAAC,OAAO,EAAE,WAAW,KAAK,OAAO,CAAC;CACjD;AAED,MAAM,MAAM,sBAAsB,GAAG,CACnC,YAAY,EAAE,SAAS,MAAM,EAAE,EAC/B,OAAO,CAAC,EAAE,sBAAsB,KAC7B,OAAO,CAAC,WAAW,EAAE,CAAC,CAAC;AAwD5B,qFAAqF;AACrF,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAEhF;AAwED;;;;;;;;GAQG;AACH,wBAAgB,4BAA4B,CAC1C,IAAI,GAAE;IAAE,IAAI,CAAC,EAAE,WAAW,CAAA;CAAO,GAChC,sBAAsB,CAwDxB;AAED;;;;GAIG;AACH,eAAO,MAAM,sBAAsB,EAAE,sBAAuD,CAAC;AAE7F;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAuCvE;AAED;;;;;;GAMG;AACH,wBAAgB,yBAAyB,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,GAAG,CAAC,MAAM,EAAE,IAAI,CAAC,CAiC3E;AAED;;;;;;GAMG;AACH,wBAAgB,eAAe,CAAC,SAAS,EAAE,WAAW,EAAE,GAAG,WAAW,EAAE,CAavE;AAaD,wBAAgB,kBAAkB,CAChC,SAAS,EAAE,WAAW,EAAE,EACxB,KAAK,EAAE,WAAW,GACjB,WAAW,GAAG,SAAS,CAKzB;AAED;;;;;;GAMG;AACH,wBAAgB,sBAAsB,CACpC,SAAS,EAAE,WAAW,EAAE,EACxB,gBAAgB,GAAE,WAAW,EAAO,GACnC,GAAG,CAAC,MAAM,CAAC,CAqBb;AAED;;GAEG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CASjD"}
1
+ {"version":3,"file":"process.d.ts","sourceRoot":"","sources":["../../src/utils/process.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAKH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,6BAA6B,CAAC;AAmB/D;;;;;;;;;;GAUG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAWtD;AAED,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAE1D;AAkCD;;;;;;;GAOG;AACH,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,WAAW,CAAC,MAAM,CAAC,GAAG,OAAO,CAQ/F;AAED,2EAA2E;AAC3E,wBAAgB,sBAAsB,CACpC,OAAO,EAAE,MAAM,EACf,KAAK,EAAE,WAAW,CAAC,MAAM,CAAC,EAC1B,QAAQ,GAAE,WAAW,CAAC,MAAM,CAAa,GACxC,OAAO,CAET;AAED,wBAAgB,oBAAoB,CAClC,SAAS,EAAE,SAAS,WAAW,EAAE,EACjC,YAAY,EAAE,SAAS,MAAM,EAAE,EAC/B,YAAY,GAAE,SAAS,MAAM,EAAO,GACnC,WAAW,EAAE,CAKf;AAED;;;;;;;;GAQG;AACH,wBAAgB,kBAAkB,CAAC,WAAW,EAAE,MAAM,GAAG,WAAW,EAAE,CA0CrE;AAED,6EAA6E;AAC7E,MAAM,MAAM,WAAW,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,SAAS,MAAM,EAAE,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;AAErF,MAAM,WAAW,sBAAsB;IACrC;;;;OAIG;IACH,WAAW,CAAC,EAAE,CAAC,OAAO,EAAE,WAAW,KAAK,OAAO,CAAC;IAChD;;;OAGG;IACH,YAAY,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CAClC;AAED,MAAM,MAAM,sBAAsB,GAAG,CACnC,YAAY,EAAE,SAAS,MAAM,EAAE,EAC/B,OAAO,CAAC,EAAE,sBAAsB,KAC7B,OAAO,CAAC,WAAW,EAAE,CAAC,CAAC;AAE5B;;;;GAIG;AACH,wBAAgB,eAAe,CAC7B,YAAY,EAAE,SAAS,MAAM,EAAE,EAC/B,WAAW,EAAE,CAAC,OAAO,EAAE,WAAW,KAAK,OAAO,GAC7C,sBAAsB,CAIxB;AA4DD,qFAAqF;AACrF,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAEhF;AAwED;;;;;;;;GAQG;AACH,wBAAgB,4BAA4B,CAC1C,IAAI,GAAE;IAAE,IAAI,CAAC,EAAE,WAAW,CAAA;CAAO,GAChC,sBAAsB,CAwDxB;AAED;;;;GAIG;AACH,eAAO,MAAM,sBAAsB,EAAE,sBAAuD,CAAC;AAE7F;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAuCvE;AAED;;;;;;GAMG;AACH,wBAAgB,yBAAyB,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,GAAG,CAAC,MAAM,EAAE,IAAI,CAAC,CAiC3E;AAED;;;;;;GAMG;AACH,wBAAgB,eAAe,CAAC,SAAS,EAAE,WAAW,EAAE,GAAG,WAAW,EAAE,CAavE;AAaD,wBAAgB,kBAAkB,CAChC,SAAS,EAAE,WAAW,EAAE,EACxB,KAAK,EAAE,WAAW,GACjB,WAAW,GAAG,SAAS,CAKzB;AAED;;;;;;GAMG;AACH,wBAAgB,sBAAsB,CACpC,SAAS,EAAE,WAAW,EAAE,EACxB,gBAAgB,GAAE,WAAW,EAAO,GACnC,GAAG,CAAC,MAAM,CAAC,CAqBb;AAED;;GAEG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CASjD"}
@@ -69,10 +69,30 @@ function normalizedProcessNames(namePatterns) {
69
69
  const mayContinuePath = tokens.slice(1).some((token)=>PATH_SEPARATOR.test(token) && names.has(normalizeExecutableName(pathBasename(token))));
70
70
  return mayContinuePath && names.has(normalizeExecutableName(executableBasename(command)));
71
71
  }
72
- export function filterByProcessNames(processes, namePatterns) {
72
+ /**
73
+ * Whether argv[0]'s basename is exactly `prefix` or starts with `prefix-`.
74
+ *
75
+ * Covers versioned launchers such as `muse-bin-1.4.4-R5419.1` for prefix
76
+ * `muse`. This is a recall-oriented prefilter: adapters still apply
77
+ * `canHandle` for precise shaping, so a coincidental `prefix-*` binary
78
+ * cannot leak into another harness.
79
+ */ export function matchesExecutablePrefix(command, prefixes) {
80
+ if (prefixes.size === 0) return false;
81
+ const base = normalizeExecutableName(pathBasename(command.trim().split(/\s+/)[0] ?? ""));
82
+ if (prefixes.has(base)) return true;
83
+ for (const prefix of prefixes){
84
+ if (base.startsWith(`${prefix}-`)) return true;
85
+ }
86
+ return false;
87
+ }
88
+ /** Whether argv[0] matches one of `names` exactly or one of `prefixes`. */ export function matchesProcessPatterns(command, names, prefixes = new Set()) {
89
+ return matchesExecutableName(command, names) || matchesExecutablePrefix(command, prefixes);
90
+ }
91
+ export function filterByProcessNames(processes, namePatterns, namePrefixes = []) {
73
92
  const names = normalizedProcessNames(namePatterns);
74
- if (names.size === 0) return [];
75
- return processes.filter((process)=>matchesExecutableName(process.command, names));
93
+ const prefixes = normalizedProcessNames(namePrefixes);
94
+ if (names.size === 0 && prefixes.size === 0) return [];
95
+ return processes.filter((process)=>matchesProcessPatterns(process.command, names, prefixes));
76
96
  }
77
97
  /**
78
98
  * List running processes matching an agent executable name.
@@ -122,6 +142,20 @@ export function filterByProcessNames(processes, namePatterns) {
122
142
  return [];
123
143
  }
124
144
  }
145
+ /**
146
+ * One construction site for snapshot options: `namePrefixes` is sent only
147
+ * when non-empty, so adapters without prefixes keep the exact legacy call
148
+ * shape that existing mock assertions pin.
149
+ */ export function snapshotOptions(namePrefixes, isCandidate) {
150
+ return namePrefixes.length > 0 ? {
151
+ namePrefixes: [
152
+ ...namePrefixes
153
+ ],
154
+ isCandidate
155
+ } : {
156
+ isCandidate
157
+ };
158
+ }
125
159
  const execFileText = (file, args)=>new Promise((resolve, reject)=>{
126
160
  execFile(file, args, {
127
161
  encoding: "utf-8",
@@ -134,7 +168,7 @@ const execFileText = (file, args)=>new Promise((resolve, reject)=>{
134
168
  resolve(stdout);
135
169
  });
136
170
  });
137
- function parseProcessList(output, namePatterns) {
171
+ function parseProcessList(output, namePatterns, namePrefixes = new Set()) {
138
172
  const allPids = new Set();
139
173
  const matched = [];
140
174
  for (const line of output.trim().split("\n")){
@@ -147,7 +181,7 @@ function parseProcessList(output, namePatterns) {
147
181
  allPids.add(pid);
148
182
  const tty = match[3];
149
183
  const command = match[4];
150
- if (!matchesExecutableName(command, namePatterns)) continue;
184
+ if (!matchesProcessPatterns(command, namePatterns, namePrefixes)) continue;
151
185
  matched.push({
152
186
  pid,
153
187
  ppid,
@@ -248,14 +282,16 @@ function processIdentity(process) {
248
282
  return cached?.identity === processIdentity(process) ? cached.startTime : undefined;
249
283
  };
250
284
  return async (namePatterns, options = {})=>{
251
- const names = normalizedProcessNames(namePatterns.filter((name)=>Boolean(name) && VALID_EXECUTABLE_NAME.test(name)));
252
- if (names.size === 0) return [];
285
+ const valid = (name)=>Boolean(name) && VALID_EXECUTABLE_NAME.test(name);
286
+ const names = normalizedProcessNames(namePatterns.filter(valid));
287
+ const prefixes = normalizedProcessNames((options.namePrefixes ?? []).filter(valid));
288
+ if (names.size === 0 && prefixes.size === 0) return [];
253
289
  try {
254
290
  const output = await exec("ps", [
255
291
  "-axo",
256
292
  "pid=,ppid=,tty=,command="
257
293
  ]);
258
- const { allPids, matched } = parseProcessList(output, names);
294
+ const { allPids, matched } = parseProcessList(output, names, prefixes);
259
295
  for (const pid of startTimeCache.keys()){
260
296
  if (!allPids.has(pid)) startTimeCache.delete(pid);
261
297
  }
@@ -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\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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"}
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\n/**\n * Whether argv[0]'s basename is exactly `prefix` or starts with `prefix-`.\n *\n * Covers versioned launchers such as `muse-bin-1.4.4-R5419.1` for prefix\n * `muse`. This is a recall-oriented prefilter: adapters still apply\n * `canHandle` for precise shaping, so a coincidental `prefix-*` binary\n * cannot leak into another harness.\n */\nexport function matchesExecutablePrefix(command: string, prefixes: ReadonlySet<string>): boolean {\n if (prefixes.size === 0) return false;\n const base = normalizeExecutableName(pathBasename(command.trim().split(/\\s+/)[0] ?? \"\"));\n if (prefixes.has(base)) return true;\n for (const prefix of prefixes) {\n if (base.startsWith(`${prefix}-`)) return true;\n }\n return false;\n}\n\n/** Whether argv[0] matches one of `names` exactly or one of `prefixes`. */\nexport function matchesProcessPatterns(\n command: string,\n names: ReadonlySet<string>,\n prefixes: ReadonlySet<string> = new Set(),\n): boolean {\n return matchesExecutableName(command, names) || matchesExecutablePrefix(command, prefixes);\n}\n\nexport function filterByProcessNames(\n processes: readonly ProcessInfo[],\n namePatterns: readonly string[],\n namePrefixes: readonly string[] = [],\n): ProcessInfo[] {\n const names = normalizedProcessNames(namePatterns);\n const prefixes = normalizedProcessNames(namePrefixes);\n if (names.size === 0 && prefixes.size === 0) return [];\n return processes.filter((process) => matchesProcessPatterns(process.command, names, prefixes));\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 * Basename prefixes matched like `processNamePrefixes`: exact or\n * `<prefix>-*`. Same validation as namePatterns.\n */\n namePrefixes?: readonly string[];\n}\n\nexport type ProcessSnapshotCapture = (\n namePatterns: readonly string[],\n options?: ProcessSnapshotOptions,\n) => Promise<ProcessInfo[]>;\n\n/**\n * One construction site for snapshot options: `namePrefixes` is sent only\n * when non-empty, so adapters without prefixes keep the exact legacy call\n * shape that existing mock assertions pin.\n */\nexport function snapshotOptions(\n namePrefixes: readonly string[],\n isCandidate: (process: ProcessInfo) => boolean,\n): ProcessSnapshotOptions {\n return namePrefixes.length > 0\n ? { namePrefixes: [...namePrefixes], isCandidate }\n : { isCandidate };\n}\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(\n output: string,\n namePatterns: ReadonlySet<string>,\n namePrefixes: ReadonlySet<string> = new Set(),\n): 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 (!matchesProcessPatterns(command, namePatterns, namePrefixes)) 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 valid = (name: string) => Boolean(name) && VALID_EXECUTABLE_NAME.test(name);\n const names = normalizedProcessNames(namePatterns.filter(valid));\n const prefixes = normalizedProcessNames((options.namePrefixes ?? []).filter(valid));\n if (names.size === 0 && prefixes.size === 0) return [];\n\n try {\n const output = await exec(\"ps\", [\"-axo\", \"pid=,ppid=,tty=,command=\"]);\n const { allPids, matched } = parseProcessList(output, names, prefixes);\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","matchesExecutablePrefix","prefixes","size","base","startsWith","matchesProcessPatterns","filterByProcessNames","processes","namePrefixes","process","listAgentProcesses","namePattern","output","encoding","line","match","pid","parseInt","ppid","Number","isNaN","tty","ttyShort","push","cwd","snapshotOptions","isCandidate","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","valid","keys","delete","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.41.0",
3
+ "version": "0.42.0",
4
4
  "description": "Standalone agent detection and management utilities for AI DevKit",
5
5
  "keywords": [
6
6
  "agent",
@@ -24,6 +24,7 @@ import {
24
24
  import {
25
25
  captureProcessSnapshot,
26
26
  filterByProcessNames,
27
+ snapshotOptions,
27
28
  type ProcessSnapshotCapture,
28
29
  } from "./utils/process.js";
29
30
  import {
@@ -33,6 +34,17 @@ import {
33
34
  type HerdrAgentPane,
34
35
  } from "./runtime/herdr/HerdrAgentDiscovery.js";
35
36
 
37
+ /** Discovery patterns of one adapter: exact names plus basename prefixes. */
38
+ function adapterPatterns(adapter: AgentAdapter): {
39
+ names: string[];
40
+ prefixes: string[];
41
+ } {
42
+ return {
43
+ names: adapter.processNames ? [...adapter.processNames] : [],
44
+ prefixes: adapter.processNamePrefixes ? [...adapter.processNamePrefixes] : [],
45
+ };
46
+ }
47
+
36
48
  export interface ListAgentsOptions {
37
49
  /**
38
50
  * Sort key for the returned list. Defaults to `status`, which orders
@@ -204,12 +216,20 @@ export class AgentManager {
204
216
  adapters.flatMap((adapter) => (adapter.processNames ? [...adapter.processNames] : [])),
205
217
  ),
206
218
  );
219
+ const namePrefixes = Array.from(
220
+ new Set(
221
+ adapters.flatMap((adapter) =>
222
+ adapter.processNamePrefixes ? [...adapter.processNamePrefixes] : [],
223
+ ),
224
+ ),
225
+ );
207
226
  let processes: readonly ProcessInfo[] = [];
208
- if (processNames.length > 0) {
227
+ if (processNames.length > 0 || namePrefixes.length > 0) {
209
228
  try {
210
- processes = await this.captureSnapshot(processNames, {
211
- isCandidate: (process) => this.isCandidateProcess(adapters, process),
212
- });
229
+ processes = await this.captureSnapshot(
230
+ processNames,
231
+ snapshotOptions(namePrefixes, (process) => this.isCandidateProcess(adapters, process)),
232
+ );
213
233
  } catch {
214
234
  processes = [];
215
235
  }
@@ -218,9 +238,12 @@ export class AgentManager {
218
238
  // Query all adapters in parallel using executable-scoped slices of the shared snapshot.
219
239
  const adapterPromises = adapters.map(async (adapter) => {
220
240
  try {
221
- const agents = adapter.processNames
241
+ const hasPatterns =
242
+ adapter.processNames !== undefined || adapter.processNamePrefixes !== undefined;
243
+ const { names, prefixes } = adapterPatterns(adapter);
244
+ const agents = hasPatterns
222
245
  ? await adapter.detectAgents({
223
- processes: filterByProcessNames(processes, adapter.processNames),
246
+ processes: filterByProcessNames(processes, names, prefixes),
224
247
  })
225
248
  : await adapter.detectAgents();
226
249
  return { type: adapter.type, agents, error: null };
@@ -284,8 +307,11 @@ export class AgentManager {
284
307
  */
285
308
  private isCandidateProcess(adapters: readonly AgentAdapter[], process: ProcessInfo): boolean {
286
309
  return adapters.some((adapter) => {
287
- if (!adapter.processNames) return false;
288
- if (filterByProcessNames([process], adapter.processNames).length === 0) return false;
310
+ if (adapter.processNames === undefined && adapter.processNamePrefixes === undefined) {
311
+ return false;
312
+ }
313
+ const { names, prefixes } = adapterPatterns(adapter);
314
+ if (filterByProcessNames([process], names, prefixes).length === 0) return false;
289
315
  try {
290
316
  return adapter.canHandle(process);
291
317
  } catch {
@@ -23,16 +23,14 @@ import type { FixtureBundle } from "../fixtures/bundle.js";
23
23
  import { captureLive, FIXTURES_ROOT } from "../fixtures/capture.js";
24
24
 
25
25
  const bundlePaths = fs.existsSync(FIXTURES_ROOT)
26
- ? fs
27
- .readdirSync(FIXTURES_ROOT, { withFileTypes: true })
28
- .flatMap((d) =>
29
- d.isDirectory()
30
- ? fs
31
- .readdirSync(path.join(FIXTURES_ROOT, d.name))
32
- .filter((f) => f.endsWith(".json"))
33
- .map((f) => path.join(FIXTURES_ROOT, d.name, f))
34
- : [],
35
- )
26
+ ? fs.readdirSync(FIXTURES_ROOT, { withFileTypes: true }).flatMap((d) =>
27
+ d.isDirectory()
28
+ ? fs
29
+ .readdirSync(path.join(FIXTURES_ROOT, d.name))
30
+ .filter((f) => f.endsWith(".json"))
31
+ .map((f) => path.join(FIXTURES_ROOT, d.name, f))
32
+ : [],
33
+ )
36
34
  : [];
37
35
 
38
36
  /** Adapters whose detect path consults AgentRegistry. */
@@ -51,7 +49,11 @@ function adapterFor(type: string, registry?: AgentRegistry) {
51
49
  }
52
50
 
53
51
  /** Seed an isolated registry at `<home>/.ai-devkit/agents.json`. */
54
- function seedRegistry(bundle: FixtureBundle, home: string, nowIso: string): AgentRegistry | undefined {
52
+ function seedRegistry(
53
+ bundle: FixtureBundle,
54
+ home: string,
55
+ nowIso: string,
56
+ ): AgentRegistry | undefined {
55
57
  if (!REGISTRY_AWARE.has(bundle.adapter)) return undefined;
56
58
  const registry = new AgentRegistry(path.join(home, ".ai-devkit", "agents.json"));
57
59
  const entries = (bundle.registry ?? []).map(
@@ -125,9 +127,7 @@ function materialize(bundle: FixtureBundle) {
125
127
  // Pids whose startTime resolves to the materialization instant — Codex
126
128
  // derives session day-dirs from start times, so only these paths contain
127
129
  // the replay-day key and normalize to $TODAY.
128
- const nowPids = new Set(
129
- bundle.processes.filter((p) => p.startTime === "$NOW").map((p) => p.pid),
130
- );
130
+ const nowPids = new Set(bundle.processes.filter((p) => p.startTime === "$NOW").map((p) => p.pid));
131
131
  return {
132
132
  home,
133
133
  processes,
@@ -177,9 +177,7 @@ describe("fixture replay (TS parity oracle)", () => {
177
177
  // process startTime was $NOW — static-start agents keep concrete
178
178
  // dates so live bundles stay replayable on any day.
179
179
  const actual = JSON.parse(JSON.stringify(normalizeAgents(agents, home))).map((a) =>
180
- nowPids.has(a.pid)
181
- ? JSON.parse(JSON.stringify(a).split(todayKey).join("$TODAY"))
182
- : a,
180
+ nowPids.has(a.pid) ? JSON.parse(JSON.stringify(a).split(todayKey).join("$TODAY")) : a,
183
181
  );
184
182
  expect(actual).toEqual(expected);
185
183
  } finally {
@@ -239,9 +239,7 @@ describe("DevinAdapter", () => {
239
239
  { id: "newer", directory: "/repo", timeCreated: now - 200, lastActivityAt: now - 60 },
240
240
  { id: "hidden-sess", directory: "/repo", timeCreated: now - 10, hidden: 1 },
241
241
  ],
242
- nodes: [
243
- { sessionId: "newer", nodeId: 1, role: "user", content: "hi", createdAt: now - 60 },
244
- ],
242
+ nodes: [{ sessionId: "newer", nodeId: 1, role: "user", content: "hi", createdAt: now - 60 }],
245
243
  });
246
244
 
247
245
  const agents = await adapter.detectAgents({
@@ -59,9 +59,7 @@ describe("DevinSessionParser", () => {
59
59
  });
60
60
  const db = openDb();
61
61
  try {
62
- expect(parser.getConversation(db, "s")).toEqual([
63
- { role: "assistant", content: "final" },
64
- ]);
62
+ expect(parser.getConversation(db, "s")).toEqual([{ role: "assistant", content: "final" }]);
65
63
  } finally {
66
64
  db.close();
67
65
  }
@@ -99,9 +97,7 @@ describe("DevinSessionParser", () => {
99
97
  });
100
98
  const db = openDb();
101
99
  try {
102
- expect(parser.getConversation(db, "s")).toEqual([
103
- { role: "assistant", content: "working" },
104
- ]);
100
+ expect(parser.getConversation(db, "s")).toEqual([{ role: "assistant", content: "working" }]);
105
101
  expect(parser.getConversation(db, "s", { verbose: true })).toEqual([
106
102
  { role: "assistant", content: "[thinking] hmm" },
107
103
  { role: "assistant", content: "working" },
@@ -145,9 +141,7 @@ describe("DevinSessionParser", () => {
145
141
  });
146
142
  const db = openDb();
147
143
  try {
148
- expect(parser.getConversation(db, "s")).toEqual([
149
- { role: "assistant", content: "ok" },
150
- ]);
144
+ expect(parser.getConversation(db, "s")).toEqual([{ role: "assistant", content: "ok" }]);
151
145
  expect(parser.getConversation(db, "missing")).toEqual([]);
152
146
  } finally {
153
147
  db.close();