@ai-devkit/agent-manager 0.33.0 → 0.33.1

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 (112) hide show
  1. package/dist/AgentManager.d.ts +11 -2
  2. package/dist/AgentManager.d.ts.map +1 -1
  3. package/dist/AgentManager.js +37 -6
  4. package/dist/AgentManager.js.map +1 -1
  5. package/dist/__tests__/AgentManager.test.js +212 -5
  6. package/dist/__tests__/AgentManager.test.js.map +1 -1
  7. package/dist/__tests__/adapters/OpenCodeAdapter.test.js +1 -0
  8. package/dist/__tests__/adapters/OpenCodeAdapter.test.js.map +1 -1
  9. package/dist/__tests__/{adapters → providers/copilot}/CopilotAdapter.test.js +19 -14
  10. package/dist/__tests__/providers/copilot/CopilotAdapter.test.js.map +1 -0
  11. package/dist/__tests__/{adapters → providers/gemini}/GeminiCliAdapter.test.js +37 -29
  12. package/dist/__tests__/providers/gemini/GeminiCliAdapter.test.js.map +1 -0
  13. package/dist/__tests__/runtime/HerdrAgentDiscovery.test.js +164 -0
  14. package/dist/__tests__/runtime/HerdrAgentDiscovery.test.js.map +1 -0
  15. package/dist/__tests__/runtime/HerdrAgentRuntime.test.js +37 -1
  16. package/dist/__tests__/runtime/HerdrAgentRuntime.test.js.map +1 -1
  17. package/dist/__tests__/utils/AgentRegistry.test.js +20 -13
  18. package/dist/__tests__/utils/AgentRegistry.test.js.map +1 -1
  19. package/dist/adapters/OpenCodeAdapter.d.ts +1 -0
  20. package/dist/adapters/OpenCodeAdapter.d.ts.map +1 -1
  21. package/dist/adapters/OpenCodeAdapter.js +7 -4
  22. package/dist/adapters/OpenCodeAdapter.js.map +1 -1
  23. package/dist/adapters/index.d.ts +2 -2
  24. package/dist/adapters/index.d.ts.map +1 -1
  25. package/dist/adapters/index.js +2 -2
  26. package/dist/adapters/index.js.map +1 -1
  27. package/dist/index.d.ts +5 -3
  28. package/dist/index.d.ts.map +1 -1
  29. package/dist/index.js +3 -2
  30. package/dist/index.js.map +1 -1
  31. package/dist/{adapters → providers/copilot}/CopilotAdapter.d.ts +11 -25
  32. package/dist/providers/copilot/CopilotAdapter.d.ts.map +1 -0
  33. package/dist/providers/copilot/CopilotAdapter.js +108 -0
  34. package/dist/providers/copilot/CopilotAdapter.js.map +1 -0
  35. package/dist/providers/copilot/CopilotAgentMapper.d.ts +9 -0
  36. package/dist/providers/copilot/CopilotAgentMapper.d.ts.map +1 -0
  37. package/dist/providers/copilot/CopilotAgentMapper.js +37 -0
  38. package/dist/providers/copilot/CopilotAgentMapper.js.map +1 -0
  39. package/dist/providers/copilot/CopilotSessionLocator.d.ts +19 -0
  40. package/dist/providers/copilot/CopilotSessionLocator.d.ts.map +1 -0
  41. package/dist/providers/copilot/CopilotSessionLocator.js +41 -0
  42. package/dist/providers/copilot/CopilotSessionLocator.js.map +1 -0
  43. package/dist/providers/copilot/CopilotSessionParser.d.ts +30 -0
  44. package/dist/providers/copilot/CopilotSessionParser.d.ts.map +1 -0
  45. package/dist/providers/copilot/CopilotSessionParser.js +181 -0
  46. package/dist/providers/copilot/CopilotSessionParser.js.map +1 -0
  47. package/dist/providers/gemini/GeminiAgentMapper.d.ts +9 -0
  48. package/dist/providers/gemini/GeminiAgentMapper.d.ts.map +1 -0
  49. package/dist/providers/gemini/GeminiAgentMapper.js +37 -0
  50. package/dist/providers/gemini/GeminiAgentMapper.js.map +1 -0
  51. package/dist/providers/gemini/GeminiCliAdapter.d.ts +52 -0
  52. package/dist/providers/gemini/GeminiCliAdapter.d.ts.map +1 -0
  53. package/dist/providers/gemini/GeminiCliAdapter.js +188 -0
  54. package/dist/providers/gemini/GeminiCliAdapter.js.map +1 -0
  55. package/dist/providers/gemini/GeminiSessionLocator.d.ts +30 -0
  56. package/dist/providers/gemini/GeminiSessionLocator.d.ts.map +1 -0
  57. package/dist/providers/gemini/GeminiSessionLocator.js +142 -0
  58. package/dist/providers/gemini/GeminiSessionLocator.js.map +1 -0
  59. package/dist/providers/gemini/GeminiSessionParser.d.ts +94 -0
  60. package/dist/providers/gemini/GeminiSessionParser.d.ts.map +1 -0
  61. package/dist/providers/gemini/GeminiSessionParser.js +166 -0
  62. package/dist/providers/gemini/GeminiSessionParser.js.map +1 -0
  63. package/dist/runtime/herdr/HerdrAgentDiscovery.d.ts +18 -0
  64. package/dist/runtime/herdr/HerdrAgentDiscovery.d.ts.map +1 -0
  65. package/dist/runtime/herdr/HerdrAgentDiscovery.js +183 -0
  66. package/dist/runtime/herdr/HerdrAgentDiscovery.js.map +1 -0
  67. package/dist/runtime/herdr/HerdrAgentRuntime.d.ts +5 -0
  68. package/dist/runtime/herdr/HerdrAgentRuntime.d.ts.map +1 -1
  69. package/dist/runtime/herdr/HerdrAgentRuntime.js +26 -8
  70. package/dist/runtime/herdr/HerdrAgentRuntime.js.map +1 -1
  71. package/dist/runtime/herdr/HerdrResponseParsers.d.ts +2 -0
  72. package/dist/runtime/herdr/HerdrResponseParsers.d.ts.map +1 -1
  73. package/dist/runtime/herdr/HerdrResponseParsers.js +12 -1
  74. package/dist/runtime/herdr/HerdrResponseParsers.js.map +1 -1
  75. package/dist/utils/AgentRegistry.d.ts.map +1 -1
  76. package/dist/utils/AgentRegistry.js +5 -5
  77. package/dist/utils/AgentRegistry.js.map +1 -1
  78. package/package.json +1 -1
  79. package/src/AgentManager.ts +67 -5
  80. package/src/__tests__/AgentManager.test.ts +231 -5
  81. package/src/__tests__/adapters/OpenCodeAdapter.test.ts +1 -0
  82. package/src/__tests__/{adapters → providers/copilot}/CopilotAdapter.test.ts +13 -16
  83. package/src/__tests__/{adapters → providers/gemini}/GeminiCliAdapter.test.ts +39 -31
  84. package/src/__tests__/runtime/HerdrAgentDiscovery.test.ts +131 -0
  85. package/src/__tests__/runtime/HerdrAgentRuntime.test.ts +20 -1
  86. package/src/__tests__/utils/AgentRegistry.test.ts +20 -10
  87. package/src/adapters/OpenCodeAdapter.ts +8 -4
  88. package/src/adapters/index.ts +2 -2
  89. package/src/index.ts +10 -3
  90. package/src/providers/copilot/CopilotAdapter.ts +145 -0
  91. package/src/providers/copilot/CopilotAgentMapper.ts +36 -0
  92. package/src/providers/copilot/CopilotSessionLocator.ts +57 -0
  93. package/src/providers/copilot/CopilotSessionParser.ts +289 -0
  94. package/src/providers/gemini/GeminiAgentMapper.ts +36 -0
  95. package/src/providers/gemini/GeminiCliAdapter.ts +266 -0
  96. package/src/providers/gemini/GeminiSessionLocator.ts +183 -0
  97. package/src/providers/gemini/GeminiSessionParser.ts +271 -0
  98. package/src/runtime/herdr/HerdrAgentDiscovery.ts +178 -0
  99. package/src/runtime/herdr/HerdrAgentRuntime.ts +33 -7
  100. package/src/runtime/herdr/HerdrResponseParsers.ts +11 -1
  101. package/src/utils/AgentRegistry.ts +8 -6
  102. package/dist/__tests__/adapters/CopilotAdapter.test.js.map +0 -1
  103. package/dist/__tests__/adapters/GeminiCliAdapter.test.js.map +0 -1
  104. package/dist/adapters/CopilotAdapter.d.ts.map +0 -1
  105. package/dist/adapters/CopilotAdapter.js +0 -329
  106. package/dist/adapters/CopilotAdapter.js.map +0 -1
  107. package/dist/adapters/GeminiCliAdapter.d.ts +0 -98
  108. package/dist/adapters/GeminiCliAdapter.d.ts.map +0 -1
  109. package/dist/adapters/GeminiCliAdapter.js +0 -513
  110. package/dist/adapters/GeminiCliAdapter.js.map +0 -1
  111. package/src/adapters/CopilotAdapter.ts +0 -483
  112. package/src/adapters/GeminiCliAdapter.ts +0 -699
@@ -0,0 +1,142 @@
1
+ import * as crypto from "crypto";
2
+ import * as fs from "fs";
3
+ import * as path from "path";
4
+ import { isDirectory, safeReaddir } from "../../utils/session.js";
5
+ const SESSION_FILE_PREFIX = "session-";
6
+ const CHATS_DIR_NAME = "chats";
7
+ export class GeminiSessionLocator {
8
+ geminiTmpDir;
9
+ constructor(options = {}){
10
+ const homeDir = process.env.HOME || process.env.USERPROFILE || "";
11
+ this.geminiTmpDir = options.geminiTmpDir ?? path.join(homeDir, ".gemini", "tmp");
12
+ }
13
+ /**
14
+ * Discover session files for the given processes.
15
+ *
16
+ * Gemini CLI writes sessions to ~/.gemini/tmp/<shortId>/chats/session-*.json
17
+ * where <shortId> is opaque (managed by a project registry). We scan every
18
+ * shortId directory and filter by matching session.projectHash against
19
+ * sha256(process.cwd) to bind each session to a candidate process CWD.
20
+ */ discoverSessions(processes) {
21
+ const empty = {
22
+ sessions: [],
23
+ contentCache: new Map()
24
+ };
25
+ if (!fs.existsSync(this.geminiTmpDir)) return empty;
26
+ const cwdHashMap = this.buildCwdHashMap(processes);
27
+ if (cwdHashMap.size === 0) return empty;
28
+ const contentCache = new Map();
29
+ const sessions = [];
30
+ let shortIdEntries;
31
+ try {
32
+ shortIdEntries = fs.readdirSync(this.geminiTmpDir);
33
+ } catch {
34
+ return empty;
35
+ }
36
+ for (const shortId of shortIdEntries){
37
+ const chatsDir = path.join(this.geminiTmpDir, shortId, CHATS_DIR_NAME);
38
+ if (!isDirectory(chatsDir)) continue;
39
+ for (const fileName of safeReaddir(chatsDir)){
40
+ if (!this.isSessionFile(fileName)) continue;
41
+ const session = this.readCandidateSession(chatsDir, fileName, cwdHashMap);
42
+ if (!session) continue;
43
+ contentCache.set(session.filePath, session.content);
44
+ sessions.push(session.sessionFile);
45
+ }
46
+ }
47
+ return {
48
+ sessions,
49
+ contentCache
50
+ };
51
+ }
52
+ discoverHistoricalSessionFiles() {
53
+ if (!isDirectory(this.geminiTmpDir)) return [];
54
+ const files = [];
55
+ for (const shortId of safeReaddir(this.geminiTmpDir)){
56
+ const chatsDir = path.join(this.geminiTmpDir, shortId, CHATS_DIR_NAME);
57
+ if (!isDirectory(chatsDir)) continue;
58
+ for (const fileName of safeReaddir(chatsDir)){
59
+ if (this.isSessionFile(fileName)) {
60
+ files.push(path.join(chatsDir, fileName));
61
+ }
62
+ }
63
+ }
64
+ return files;
65
+ }
66
+ readCandidateSession(chatsDir, fileName, cwdHashMap) {
67
+ const filePath = path.join(chatsDir, fileName);
68
+ let content;
69
+ try {
70
+ content = fs.readFileSync(filePath, "utf-8");
71
+ } catch {
72
+ return null;
73
+ }
74
+ const parsed = this.parseSessionMetadata(content);
75
+ if (!parsed?.projectHash) return null;
76
+ const resolvedCwd = cwdHashMap.get(parsed.projectHash);
77
+ if (!resolvedCwd) return null;
78
+ let birthtimeMs = 0;
79
+ try {
80
+ birthtimeMs = fs.statSync(filePath).birthtimeMs;
81
+ } catch {
82
+ return null;
83
+ }
84
+ return {
85
+ filePath,
86
+ content,
87
+ sessionFile: {
88
+ sessionId: parsed.sessionId || fileName.replace(/\.json$/, ""),
89
+ filePath,
90
+ projectDir: chatsDir,
91
+ birthtimeMs,
92
+ resolvedCwd
93
+ }
94
+ };
95
+ }
96
+ buildCwdHashMap(processes) {
97
+ const map = new Map();
98
+ for (const proc of processes){
99
+ if (!proc.cwd) continue;
100
+ // Gemini CLI resolves its project root by walking up from the
101
+ // startup directory looking for a `.git` boundary marker. A
102
+ // session's projectHash therefore tracks that ancestor rather
103
+ // than the process' actual CWD. Enumerate every ancestor as a
104
+ // candidate so subdirectory invocations still line up with the
105
+ // session the Gemini process wrote.
106
+ for (const candidate of this.candidateProjectRoots(proc.cwd)){
107
+ const hash = this.hashProjectRoot(candidate);
108
+ if (!map.has(hash)) {
109
+ map.set(hash, proc.cwd);
110
+ }
111
+ }
112
+ }
113
+ return map;
114
+ }
115
+ candidateProjectRoots(cwd) {
116
+ const roots = [];
117
+ let current = path.resolve(cwd);
118
+ let parent = path.dirname(current);
119
+ while(parent !== current){
120
+ roots.push(current);
121
+ current = parent;
122
+ parent = path.dirname(current);
123
+ }
124
+ roots.push(current);
125
+ return roots;
126
+ }
127
+ hashProjectRoot(projectRoot) {
128
+ return crypto.createHash("sha256").update(projectRoot).digest("hex");
129
+ }
130
+ isSessionFile(fileName) {
131
+ return fileName.startsWith(SESSION_FILE_PREFIX) && fileName.endsWith(".json");
132
+ }
133
+ parseSessionMetadata(content) {
134
+ try {
135
+ return JSON.parse(content);
136
+ } catch {
137
+ return null;
138
+ }
139
+ }
140
+ }
141
+
142
+ //# sourceMappingURL=GeminiSessionLocator.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/providers/gemini/GeminiSessionLocator.ts"],"sourcesContent":["import * as crypto from \"crypto\";\nimport * as fs from \"fs\";\nimport * as path from \"path\";\nimport type { ProcessInfo } from \"../../adapters/AgentAdapter.js\";\nimport type { SessionFile } from \"../../utils/session.js\";\nimport { isDirectory, safeReaddir } from \"../../utils/session.js\";\n\nexport interface GeminiSessionDiscovery {\n sessions: SessionFile[];\n contentCache: Map<string, string>;\n}\n\nexport interface GeminiSessionLocatorOptions {\n geminiTmpDir?: string;\n}\n\ninterface GeminiSessionMetadata {\n sessionId?: string;\n projectHash?: string;\n}\n\nconst SESSION_FILE_PREFIX = \"session-\";\nconst CHATS_DIR_NAME = \"chats\";\n\nexport class GeminiSessionLocator {\n private readonly geminiTmpDir: string;\n\n constructor(options: GeminiSessionLocatorOptions = {}) {\n const homeDir = process.env.HOME || process.env.USERPROFILE || \"\";\n this.geminiTmpDir = options.geminiTmpDir ?? path.join(homeDir, \".gemini\", \"tmp\");\n }\n\n /**\n * Discover session files for the given processes.\n *\n * Gemini CLI writes sessions to ~/.gemini/tmp/<shortId>/chats/session-*.json\n * where <shortId> is opaque (managed by a project registry). We scan every\n * shortId directory and filter by matching session.projectHash against\n * sha256(process.cwd) to bind each session to a candidate process CWD.\n */\n discoverSessions(processes: ProcessInfo[]): GeminiSessionDiscovery {\n const empty = { sessions: [] as SessionFile[], contentCache: new Map<string, string>() };\n if (!fs.existsSync(this.geminiTmpDir)) return empty;\n\n const cwdHashMap = this.buildCwdHashMap(processes);\n if (cwdHashMap.size === 0) return empty;\n\n const contentCache = new Map<string, string>();\n const sessions: SessionFile[] = [];\n\n let shortIdEntries: string[];\n try {\n shortIdEntries = fs.readdirSync(this.geminiTmpDir);\n } catch {\n return empty;\n }\n\n for (const shortId of shortIdEntries) {\n const chatsDir = path.join(this.geminiTmpDir, shortId, CHATS_DIR_NAME);\n if (!isDirectory(chatsDir)) continue;\n\n for (const fileName of safeReaddir(chatsDir)) {\n if (!this.isSessionFile(fileName)) continue;\n\n const session = this.readCandidateSession(chatsDir, fileName, cwdHashMap);\n if (!session) continue;\n\n contentCache.set(session.filePath, session.content);\n sessions.push(session.sessionFile);\n }\n }\n\n return { sessions, contentCache };\n }\n\n discoverHistoricalSessionFiles(): string[] {\n if (!isDirectory(this.geminiTmpDir)) return [];\n\n const files: string[] = [];\n for (const shortId of safeReaddir(this.geminiTmpDir)) {\n const chatsDir = path.join(this.geminiTmpDir, shortId, CHATS_DIR_NAME);\n if (!isDirectory(chatsDir)) continue;\n\n for (const fileName of safeReaddir(chatsDir)) {\n if (this.isSessionFile(fileName)) {\n files.push(path.join(chatsDir, fileName));\n }\n }\n }\n\n return files;\n }\n\n private readCandidateSession(\n chatsDir: string,\n fileName: string,\n cwdHashMap: Map<string, string>,\n ): { filePath: string; content: string; sessionFile: SessionFile } | null {\n const filePath = path.join(chatsDir, fileName);\n\n let content: string;\n try {\n content = fs.readFileSync(filePath, \"utf-8\");\n } catch {\n return null;\n }\n\n const parsed = this.parseSessionMetadata(content);\n if (!parsed?.projectHash) return null;\n\n const resolvedCwd = cwdHashMap.get(parsed.projectHash);\n if (!resolvedCwd) return null;\n\n let birthtimeMs = 0;\n try {\n birthtimeMs = fs.statSync(filePath).birthtimeMs;\n } catch {\n return null;\n }\n\n return {\n filePath,\n content,\n sessionFile: {\n sessionId: parsed.sessionId || fileName.replace(/\\.json$/, \"\"),\n filePath,\n projectDir: chatsDir,\n birthtimeMs,\n resolvedCwd,\n },\n };\n }\n\n private buildCwdHashMap(processes: ProcessInfo[]): Map<string, string> {\n const map = new Map<string, string>();\n for (const proc of processes) {\n if (!proc.cwd) continue;\n\n // Gemini CLI resolves its project root by walking up from the\n // startup directory looking for a `.git` boundary marker. A\n // session's projectHash therefore tracks that ancestor rather\n // than the process' actual CWD. Enumerate every ancestor as a\n // candidate so subdirectory invocations still line up with the\n // session the Gemini process wrote.\n for (const candidate of this.candidateProjectRoots(proc.cwd)) {\n const hash = this.hashProjectRoot(candidate);\n if (!map.has(hash)) {\n map.set(hash, proc.cwd);\n }\n }\n }\n return map;\n }\n\n private candidateProjectRoots(cwd: string): string[] {\n const roots: string[] = [];\n let current = path.resolve(cwd);\n let parent = path.dirname(current);\n while (parent !== current) {\n roots.push(current);\n current = parent;\n parent = path.dirname(current);\n }\n roots.push(current);\n return roots;\n }\n\n private hashProjectRoot(projectRoot: string): string {\n return crypto.createHash(\"sha256\").update(projectRoot).digest(\"hex\");\n }\n\n private isSessionFile(fileName: string): boolean {\n return fileName.startsWith(SESSION_FILE_PREFIX) && fileName.endsWith(\".json\");\n }\n\n private parseSessionMetadata(content: string): GeminiSessionMetadata | null {\n try {\n return JSON.parse(content) as GeminiSessionMetadata;\n } catch {\n return null;\n }\n }\n}\n"],"names":["crypto","fs","path","isDirectory","safeReaddir","SESSION_FILE_PREFIX","CHATS_DIR_NAME","GeminiSessionLocator","geminiTmpDir","options","homeDir","process","env","HOME","USERPROFILE","join","discoverSessions","processes","empty","sessions","contentCache","Map","existsSync","cwdHashMap","buildCwdHashMap","size","shortIdEntries","readdirSync","shortId","chatsDir","fileName","isSessionFile","session","readCandidateSession","set","filePath","content","push","sessionFile","discoverHistoricalSessionFiles","files","readFileSync","parsed","parseSessionMetadata","projectHash","resolvedCwd","get","birthtimeMs","statSync","sessionId","replace","projectDir","map","proc","cwd","candidate","candidateProjectRoots","hash","hashProjectRoot","has","roots","current","resolve","parent","dirname","projectRoot","createHash","update","digest","startsWith","endsWith","JSON","parse"],"mappings":"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"}
@@ -0,0 +1,94 @@
1
+ import type { ConversationMessage, SessionSummary } from "../../adapters/AgentAdapter.js";
2
+ import { AgentStatus } from "../../adapters/AgentAdapter.js";
3
+ /**
4
+ * A single Gemini CLI message content part. Mirrors the `{text?: string}`
5
+ * shape that Gemini writes for user message parts (derived from the
6
+ * Gemini API `Content.parts[]` schema). Non-text part variants (data,
7
+ * file, etc.) are preserved via the index signature but ignored by
8
+ * `resolveContent` since the adapter only surfaces human-readable text.
9
+ */
10
+ interface GeminiContentPart {
11
+ text?: string;
12
+ [key: string]: unknown;
13
+ }
14
+ type GeminiMessageContent = string | GeminiContentPart[];
15
+ export interface GeminiMessageEntry {
16
+ id?: string;
17
+ timestamp?: string;
18
+ type?: string;
19
+ /**
20
+ * Gemini CLI stores two different content shapes depending on the
21
+ * message origin:
22
+ * - `type: "user"` messages carry the raw Part[] from userContent.parts
23
+ * (e.g. `[{ text: "hello" }]`).
24
+ * - `type: "gemini"` (assistant) messages carry a pre-joined string
25
+ * built from `consolidatedParts.filter(p => p.text).join('').trim()`.
26
+ * Both forms must be normalized via resolveContent before any string
27
+ * operation is applied.
28
+ */
29
+ content?: GeminiMessageContent;
30
+ displayContent?: GeminiMessageContent;
31
+ }
32
+ export interface GeminiSessionFile {
33
+ sessionId?: string;
34
+ projectHash?: string;
35
+ startTime?: string;
36
+ lastUpdated?: string;
37
+ messages?: GeminiMessageEntry[];
38
+ directories?: string[];
39
+ kind?: string;
40
+ }
41
+ export interface GeminiSession {
42
+ sessionId: string;
43
+ projectPath: string;
44
+ summary: string;
45
+ sessionStart: Date;
46
+ lastActive: Date;
47
+ lastMessageType?: string;
48
+ }
49
+ export declare class GeminiSessionParser {
50
+ /**
51
+ * Parse session file content into GeminiSession.
52
+ * Uses cached content if available, otherwise reads from disk.
53
+ */
54
+ parseSession(cachedContent: string | undefined, filePath: string): GeminiSession | null;
55
+ determineStatus(session: GeminiSession): AgentStatus;
56
+ /**
57
+ * Read the full conversation from a Gemini CLI session JSON file.
58
+ *
59
+ * Gemini sessions store messages in an array with `type` field — typically
60
+ * 'user' or 'gemini' for visible turns, with tool and system entries mixed in.
61
+ */
62
+ getConversation(sessionFilePath: string, options?: {
63
+ verbose?: boolean;
64
+ }): ConversationMessage[];
65
+ /**
66
+ * Read a Gemini session JSON file and produce a {@link SessionSummary}.
67
+ * Returns null when the file is unreadable, the JSON doesn't parse,
68
+ * or the body lacks a sessionId.
69
+ */
70
+ fileToSessionSummary(filePath: string): SessionSummary | null;
71
+ private parseSessionJson;
72
+ private readSessionFile;
73
+ private roleForEntry;
74
+ private extractSummary;
75
+ /**
76
+ * Normalize an entry's content/displayContent into a plain string.
77
+ * Prefers displayContent when both are present (matches Gemini CLI's
78
+ * own rendering priority for the /chat UI).
79
+ */
80
+ private messageText;
81
+ /**
82
+ * Collapse a Gemini message content field into plain text.
83
+ * Accepts either a pre-joined string (assistant turns) or a Part[]
84
+ * list (user turns carrying `[{text: "..."}]`). Non-text part
85
+ * variants (data, file) are dropped since this helper is only used
86
+ * for summary/conversation rendering.
87
+ */
88
+ private resolveContent;
89
+ private extractFirstUserMessage;
90
+ private parseTimestamp;
91
+ private truncate;
92
+ }
93
+ export {};
94
+ //# sourceMappingURL=GeminiSessionParser.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"GeminiSessionParser.d.ts","sourceRoot":"","sources":["../../../src/providers/gemini/GeminiSessionParser.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,mBAAmB,EAAE,cAAc,EAAE,MAAM,gCAAgC,CAAC;AAC1F,OAAO,EAAE,WAAW,EAAE,MAAM,gCAAgC,CAAC;AAG7D;;;;;;GAMG;AACH,UAAU,iBAAiB;IACzB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AAED,KAAK,oBAAoB,GAAG,MAAM,GAAG,iBAAiB,EAAE,CAAC;AAEzD,MAAM,WAAW,kBAAkB;IACjC,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd;;;;;;;;;OASG;IACH,OAAO,CAAC,EAAE,oBAAoB,CAAC;IAC/B,cAAc,CAAC,EAAE,oBAAoB,CAAC;CACvC;AAED,MAAM,WAAW,iBAAiB;IAChC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,kBAAkB,EAAE,CAAC;IAChC,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;IACvB,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,aAAa;IAC5B,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,MAAM,CAAC;IAChB,YAAY,EAAE,IAAI,CAAC;IACnB,UAAU,EAAE,IAAI,CAAC;IACjB,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B;AAID,qBAAa,mBAAmB;IAC9B;;;OAGG;IACH,YAAY,CAAC,aAAa,EAAE,MAAM,GAAG,SAAS,EAAE,QAAQ,EAAE,MAAM,GAAG,aAAa,GAAG,IAAI;IAiCvF,eAAe,CAAC,OAAO,EAAE,aAAa,GAAG,WAAW;IAepD;;;;;OAKG;IACH,eAAe,CAAC,eAAe,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;QAAE,OAAO,CAAC,EAAE,OAAO,CAAA;KAAE,GAAG,mBAAmB,EAAE;IA0BhG;;;;OAIG;IACH,oBAAoB,CAAC,QAAQ,EAAE,MAAM,GAAG,cAAc,GAAG,IAAI;IAkC7D,OAAO,CAAC,gBAAgB;IAQxB,OAAO,CAAC,eAAe;IAQvB,OAAO,CAAC,YAAY;IAUpB,OAAO,CAAC,cAAc;IAWtB;;;;OAIG;IACH,OAAO,CAAC,WAAW;IAMnB;;;;;;OAMG;IACH,OAAO,CAAC,cAAc;IActB,OAAO,CAAC,uBAAuB;IAS/B,OAAO,CAAC,cAAc;IAMtB,OAAO,CAAC,QAAQ;CAIjB"}
@@ -0,0 +1,166 @@
1
+ import * as fs from "fs";
2
+ import { AgentStatus } from "../../adapters/AgentAdapter.js";
3
+ import { safeReadFile, safeStat } from "../../utils/session.js";
4
+ const IDLE_THRESHOLD_MINUTES = 5;
5
+ export class GeminiSessionParser {
6
+ /**
7
+ * Parse session file content into GeminiSession.
8
+ * Uses cached content if available, otherwise reads from disk.
9
+ */ parseSession(cachedContent, filePath) {
10
+ const content = cachedContent ?? this.readSessionFile(filePath);
11
+ if (content === null) return null;
12
+ const parsed = this.parseSessionJson(content);
13
+ if (!parsed?.sessionId) return null;
14
+ const messages = Array.isArray(parsed.messages) ? parsed.messages : [];
15
+ const lastEntry = messages.length > 0 ? messages[messages.length - 1] : undefined;
16
+ const fileStat = safeStat(filePath);
17
+ const lastActive = this.parseTimestamp(parsed.lastUpdated) || this.parseTimestamp(lastEntry?.timestamp) || fileStat?.mtime || new Date();
18
+ const sessionStart = this.parseTimestamp(parsed.startTime) || lastActive;
19
+ const projectPath = Array.isArray(parsed.directories) && parsed.directories.length > 0 ? parsed.directories[0] : "";
20
+ return {
21
+ sessionId: parsed.sessionId,
22
+ projectPath,
23
+ summary: this.extractSummary(messages),
24
+ sessionStart,
25
+ lastActive,
26
+ lastMessageType: lastEntry?.type
27
+ };
28
+ }
29
+ determineStatus(session) {
30
+ const diffMs = Date.now() - session.lastActive.getTime();
31
+ const diffMinutes = diffMs / 60000;
32
+ if (diffMinutes > IDLE_THRESHOLD_MINUTES) {
33
+ return AgentStatus.IDLE;
34
+ }
35
+ if (session.lastMessageType === "gemini" || session.lastMessageType === "assistant") {
36
+ return AgentStatus.WAITING;
37
+ }
38
+ return AgentStatus.RUNNING;
39
+ }
40
+ /**
41
+ * Read the full conversation from a Gemini CLI session JSON file.
42
+ *
43
+ * Gemini sessions store messages in an array with `type` field — typically
44
+ * 'user' or 'gemini' for visible turns, with tool and system entries mixed in.
45
+ */ getConversation(sessionFilePath, options) {
46
+ const verbose = options?.verbose ?? false;
47
+ const content = safeReadFile(sessionFilePath);
48
+ if (content === undefined) return [];
49
+ const parsed = this.parseSessionJson(content);
50
+ if (!Array.isArray(parsed?.messages)) return [];
51
+ const messages = [];
52
+ for (const entry of parsed.messages){
53
+ const role = this.roleForEntry(entry?.type, verbose);
54
+ if (!role) continue;
55
+ const text = this.messageText(entry).trim();
56
+ if (!text) continue;
57
+ messages.push({
58
+ role,
59
+ content: text,
60
+ timestamp: entry.timestamp
61
+ });
62
+ }
63
+ return messages;
64
+ }
65
+ /**
66
+ * Read a Gemini session JSON file and produce a {@link SessionSummary}.
67
+ * Returns null when the file is unreadable, the JSON doesn't parse,
68
+ * or the body lacks a sessionId.
69
+ */ fileToSessionSummary(filePath) {
70
+ const content = safeReadFile(filePath);
71
+ if (content === undefined) return null;
72
+ const parsed = this.parseSessionJson(content);
73
+ if (!parsed?.sessionId) return null;
74
+ const messages = Array.isArray(parsed.messages) ? parsed.messages : [];
75
+ const firstUserMessage = this.extractFirstUserMessage(messages);
76
+ const cwd = Array.isArray(parsed.directories) && parsed.directories.length > 0 ? parsed.directories[0] : "";
77
+ const stat = safeStat(filePath);
78
+ const lastEntryTimestamp = this.parseTimestamp(messages.length > 0 ? messages[messages.length - 1]?.timestamp : undefined);
79
+ const lastActive = this.parseTimestamp(parsed.lastUpdated) || lastEntryTimestamp || stat?.mtime || new Date();
80
+ const startedAt = this.parseTimestamp(parsed.startTime) || stat?.birthtime || stat?.mtime || lastActive;
81
+ return {
82
+ type: "gemini_cli",
83
+ sessionId: parsed.sessionId,
84
+ cwd,
85
+ firstUserMessage,
86
+ lastActive,
87
+ startedAt,
88
+ sessionFilePath: filePath
89
+ };
90
+ }
91
+ parseSessionJson(content) {
92
+ try {
93
+ return JSON.parse(content);
94
+ } catch {
95
+ return null;
96
+ }
97
+ }
98
+ readSessionFile(filePath) {
99
+ try {
100
+ return fs.readFileSync(filePath, "utf-8");
101
+ } catch {
102
+ return null;
103
+ }
104
+ }
105
+ roleForEntry(entryType, verbose) {
106
+ if (entryType === "user") return "user";
107
+ if (entryType === "gemini" || entryType === "assistant") return "assistant";
108
+ if (verbose && entryType) return "system";
109
+ return null;
110
+ }
111
+ extractSummary(messages) {
112
+ for(let i = messages.length - 1; i >= 0; i--){
113
+ const entry = messages[i];
114
+ if (entry?.type !== "user") continue;
115
+ const text = this.messageText(entry).trim();
116
+ if (text) return this.truncate(text, 120);
117
+ }
118
+ return "Gemini CLI session active";
119
+ }
120
+ /**
121
+ * Normalize an entry's content/displayContent into a plain string.
122
+ * Prefers displayContent when both are present (matches Gemini CLI's
123
+ * own rendering priority for the /chat UI).
124
+ */ messageText(entry) {
125
+ const displayText = this.resolveContent(entry.displayContent);
126
+ if (displayText) return displayText;
127
+ return this.resolveContent(entry.content);
128
+ }
129
+ /**
130
+ * Collapse a Gemini message content field into plain text.
131
+ * Accepts either a pre-joined string (assistant turns) or a Part[]
132
+ * list (user turns carrying `[{text: "..."}]`). Non-text part
133
+ * variants (data, file) are dropped since this helper is only used
134
+ * for summary/conversation rendering.
135
+ */ resolveContent(content) {
136
+ if (!content) return "";
137
+ if (typeof content === "string") return content;
138
+ if (!Array.isArray(content)) return "";
139
+ const parts = [];
140
+ for (const part of content){
141
+ if (part && typeof part.text === "string" && part.text) {
142
+ parts.push(part.text);
143
+ }
144
+ }
145
+ return parts.join("");
146
+ }
147
+ extractFirstUserMessage(messages) {
148
+ for (const entry of messages){
149
+ if (entry?.type !== "user") continue;
150
+ const text = this.messageText(entry).trim();
151
+ if (text) return text;
152
+ }
153
+ return "";
154
+ }
155
+ parseTimestamp(value) {
156
+ if (!value) return null;
157
+ const timestamp = new Date(value);
158
+ return Number.isNaN(timestamp.getTime()) ? null : timestamp;
159
+ }
160
+ truncate(value, maxLength) {
161
+ if (value.length <= maxLength) return value;
162
+ return `${value.slice(0, maxLength - 3)}...`;
163
+ }
164
+ }
165
+
166
+ //# sourceMappingURL=GeminiSessionParser.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/providers/gemini/GeminiSessionParser.ts"],"sourcesContent":["import * as fs from \"fs\";\nimport type { ConversationMessage, SessionSummary } from \"../../adapters/AgentAdapter.js\";\nimport { AgentStatus } from \"../../adapters/AgentAdapter.js\";\nimport { safeReadFile, safeStat } from \"../../utils/session.js\";\n\n/**\n * A single Gemini CLI message content part. Mirrors the `{text?: string}`\n * shape that Gemini writes for user message parts (derived from the\n * Gemini API `Content.parts[]` schema). Non-text part variants (data,\n * file, etc.) are preserved via the index signature but ignored by\n * `resolveContent` since the adapter only surfaces human-readable text.\n */\ninterface GeminiContentPart {\n text?: string;\n [key: string]: unknown;\n}\n\ntype GeminiMessageContent = string | GeminiContentPart[];\n\nexport interface GeminiMessageEntry {\n id?: string;\n timestamp?: string;\n type?: string;\n /**\n * Gemini CLI stores two different content shapes depending on the\n * message origin:\n * - `type: \"user\"` messages carry the raw Part[] from userContent.parts\n * (e.g. `[{ text: \"hello\" }]`).\n * - `type: \"gemini\"` (assistant) messages carry a pre-joined string\n * built from `consolidatedParts.filter(p => p.text).join('').trim()`.\n * Both forms must be normalized via resolveContent before any string\n * operation is applied.\n */\n content?: GeminiMessageContent;\n displayContent?: GeminiMessageContent;\n}\n\nexport interface GeminiSessionFile {\n sessionId?: string;\n projectHash?: string;\n startTime?: string;\n lastUpdated?: string;\n messages?: GeminiMessageEntry[];\n directories?: string[];\n kind?: string;\n}\n\nexport interface GeminiSession {\n sessionId: string;\n projectPath: string;\n summary: string;\n sessionStart: Date;\n lastActive: Date;\n lastMessageType?: string;\n}\n\nconst IDLE_THRESHOLD_MINUTES = 5;\n\nexport class GeminiSessionParser {\n /**\n * Parse session file content into GeminiSession.\n * Uses cached content if available, otherwise reads from disk.\n */\n parseSession(cachedContent: string | undefined, filePath: string): GeminiSession | null {\n const content = cachedContent ?? this.readSessionFile(filePath);\n if (content === null) return null;\n\n const parsed = this.parseSessionJson(content);\n if (!parsed?.sessionId) return null;\n\n const messages = Array.isArray(parsed.messages) ? parsed.messages : [];\n const lastEntry = messages.length > 0 ? messages[messages.length - 1] : undefined;\n const fileStat = safeStat(filePath);\n\n const lastActive =\n this.parseTimestamp(parsed.lastUpdated) ||\n this.parseTimestamp(lastEntry?.timestamp) ||\n fileStat?.mtime ||\n new Date();\n\n const sessionStart = this.parseTimestamp(parsed.startTime) || lastActive;\n const projectPath =\n Array.isArray(parsed.directories) && parsed.directories.length > 0\n ? parsed.directories[0]\n : \"\";\n\n return {\n sessionId: parsed.sessionId,\n projectPath,\n summary: this.extractSummary(messages),\n sessionStart,\n lastActive,\n lastMessageType: lastEntry?.type,\n };\n }\n\n determineStatus(session: GeminiSession): AgentStatus {\n const diffMs = Date.now() - session.lastActive.getTime();\n const diffMinutes = diffMs / 60000;\n\n if (diffMinutes > IDLE_THRESHOLD_MINUTES) {\n return AgentStatus.IDLE;\n }\n\n if (session.lastMessageType === \"gemini\" || session.lastMessageType === \"assistant\") {\n return AgentStatus.WAITING;\n }\n\n return AgentStatus.RUNNING;\n }\n\n /**\n * Read the full conversation from a Gemini CLI session JSON file.\n *\n * Gemini sessions store messages in an array with `type` field — typically\n * 'user' or 'gemini' for visible turns, with tool and system entries mixed in.\n */\n getConversation(sessionFilePath: string, options?: { verbose?: boolean }): ConversationMessage[] {\n const verbose = options?.verbose ?? false;\n const content = safeReadFile(sessionFilePath);\n if (content === undefined) return [];\n\n const parsed = this.parseSessionJson(content);\n if (!Array.isArray(parsed?.messages)) return [];\n\n const messages: ConversationMessage[] = [];\n for (const entry of parsed.messages) {\n const role = this.roleForEntry(entry?.type, verbose);\n if (!role) continue;\n\n const text = this.messageText(entry).trim();\n if (!text) continue;\n\n messages.push({\n role,\n content: text,\n timestamp: entry.timestamp,\n });\n }\n\n return messages;\n }\n\n /**\n * Read a Gemini session JSON file and produce a {@link SessionSummary}.\n * Returns null when the file is unreadable, the JSON doesn't parse,\n * or the body lacks a sessionId.\n */\n fileToSessionSummary(filePath: string): SessionSummary | null {\n const content = safeReadFile(filePath);\n if (content === undefined) return null;\n\n const parsed = this.parseSessionJson(content);\n if (!parsed?.sessionId) return null;\n\n const messages = Array.isArray(parsed.messages) ? parsed.messages : [];\n const firstUserMessage = this.extractFirstUserMessage(messages);\n const cwd =\n Array.isArray(parsed.directories) && parsed.directories.length > 0\n ? parsed.directories[0]\n : \"\";\n\n const stat = safeStat(filePath);\n const lastEntryTimestamp = this.parseTimestamp(\n messages.length > 0 ? messages[messages.length - 1]?.timestamp : undefined,\n );\n const lastActive =\n this.parseTimestamp(parsed.lastUpdated) || lastEntryTimestamp || stat?.mtime || new Date();\n const startedAt =\n this.parseTimestamp(parsed.startTime) || stat?.birthtime || stat?.mtime || lastActive;\n\n return {\n type: \"gemini_cli\",\n sessionId: parsed.sessionId,\n cwd,\n firstUserMessage,\n lastActive,\n startedAt,\n sessionFilePath: filePath,\n };\n }\n\n private parseSessionJson(content: string): GeminiSessionFile | null {\n try {\n return JSON.parse(content) as GeminiSessionFile;\n } catch {\n return null;\n }\n }\n\n private readSessionFile(filePath: string): string | null {\n try {\n return fs.readFileSync(filePath, \"utf-8\");\n } catch {\n return null;\n }\n }\n\n private roleForEntry(\n entryType: string | undefined,\n verbose: boolean,\n ): ConversationMessage[\"role\"] | null {\n if (entryType === \"user\") return \"user\";\n if (entryType === \"gemini\" || entryType === \"assistant\") return \"assistant\";\n if (verbose && entryType) return \"system\";\n return null;\n }\n\n private extractSummary(messages: GeminiMessageEntry[]): string {\n for (let i = messages.length - 1; i >= 0; i--) {\n const entry = messages[i];\n if (entry?.type !== \"user\") continue;\n const text = this.messageText(entry).trim();\n if (text) return this.truncate(text, 120);\n }\n\n return \"Gemini CLI session active\";\n }\n\n /**\n * Normalize an entry's content/displayContent into a plain string.\n * Prefers displayContent when both are present (matches Gemini CLI's\n * own rendering priority for the /chat UI).\n */\n private messageText(entry: GeminiMessageEntry): string {\n const displayText = this.resolveContent(entry.displayContent);\n if (displayText) return displayText;\n return this.resolveContent(entry.content);\n }\n\n /**\n * Collapse a Gemini message content field into plain text.\n * Accepts either a pre-joined string (assistant turns) or a Part[]\n * list (user turns carrying `[{text: \"...\"}]`). Non-text part\n * variants (data, file) are dropped since this helper is only used\n * for summary/conversation rendering.\n */\n private resolveContent(content: GeminiMessageContent | undefined): string {\n if (!content) return \"\";\n if (typeof content === \"string\") return content;\n if (!Array.isArray(content)) return \"\";\n\n const parts: string[] = [];\n for (const part of content) {\n if (part && typeof part.text === \"string\" && part.text) {\n parts.push(part.text);\n }\n }\n return parts.join(\"\");\n }\n\n private extractFirstUserMessage(messages: GeminiMessageEntry[]): string {\n for (const entry of messages) {\n if (entry?.type !== \"user\") continue;\n const text = this.messageText(entry).trim();\n if (text) return text;\n }\n return \"\";\n }\n\n private parseTimestamp(value?: string): Date | null {\n if (!value) return null;\n const timestamp = new Date(value);\n return Number.isNaN(timestamp.getTime()) ? null : timestamp;\n }\n\n private truncate(value: string, maxLength: number): string {\n if (value.length <= maxLength) return value;\n return `${value.slice(0, maxLength - 3)}...`;\n }\n}\n"],"names":["fs","AgentStatus","safeReadFile","safeStat","IDLE_THRESHOLD_MINUTES","GeminiSessionParser","parseSession","cachedContent","filePath","content","readSessionFile","parsed","parseSessionJson","sessionId","messages","Array","isArray","lastEntry","length","undefined","fileStat","lastActive","parseTimestamp","lastUpdated","timestamp","mtime","Date","sessionStart","startTime","projectPath","directories","summary","extractSummary","lastMessageType","type","determineStatus","session","diffMs","now","getTime","diffMinutes","IDLE","WAITING","RUNNING","getConversation","sessionFilePath","options","verbose","entry","role","roleForEntry","text","messageText","trim","push","fileToSessionSummary","firstUserMessage","extractFirstUserMessage","cwd","stat","lastEntryTimestamp","startedAt","birthtime","JSON","parse","readFileSync","entryType","i","truncate","displayText","resolveContent","displayContent","parts","part","join","value","Number","isNaN","maxLength","slice"],"mappings":"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"}
@@ -0,0 +1,18 @@
1
+ import type { AgentInfo } from "../../adapters/AgentAdapter.js";
2
+ import { type HerdrCommandRunner } from "./HerdrCliClient.js";
3
+ import type { HerdrRuntimeRef } from "./HerdrRuntimeRef.js";
4
+ export interface HerdrAgentPane {
5
+ agent: string;
6
+ agentSessionId?: string;
7
+ cwd?: string;
8
+ paneId: string;
9
+ workspaceId?: string;
10
+ tabId?: string;
11
+ foregroundPids?: number[];
12
+ shellPid?: number;
13
+ }
14
+ export declare function fetchHerdrAgentPanes(runner?: HerdrCommandRunner): Promise<HerdrAgentPane[]>;
15
+ export declare function extractHerdrAgentPanes(value: unknown): HerdrAgentPane[];
16
+ export declare function findMatchingHerdrPane(agent: Pick<AgentInfo, "type" | "projectPath" | "sessionId" | "pid">, panes: readonly HerdrAgentPane[]): HerdrAgentPane | null;
17
+ export declare function herdrPaneToRuntimeRef(pane: HerdrAgentPane, agentName: string): HerdrRuntimeRef;
18
+ //# sourceMappingURL=HerdrAgentDiscovery.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"HerdrAgentDiscovery.d.ts","sourceRoot":"","sources":["../../../src/runtime/herdr/HerdrAgentDiscovery.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAa,MAAM,gCAAgC,CAAC;AAC3E,OAAO,EAAkB,KAAK,kBAAkB,EAAE,MAAM,qBAAqB,CAAC;AAC9E,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AAE5D,MAAM,WAAW,cAAc;IAC7B,KAAK,EAAE,MAAM,CAAC;IACd,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;IAC1B,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,wBAAsB,oBAAoB,CAAC,MAAM,CAAC,EAAE,kBAAkB,GAAG,OAAO,CAAC,cAAc,EAAE,CAAC,CAIjG;AAED,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,OAAO,GAAG,cAAc,EAAE,CA8BvE;AAED,wBAAgB,qBAAqB,CACnC,KAAK,EAAE,IAAI,CAAC,SAAS,EAAE,MAAM,GAAG,aAAa,GAAG,WAAW,GAAG,KAAK,CAAC,EACpE,KAAK,EAAE,SAAS,cAAc,EAAE,GAC/B,cAAc,GAAG,IAAI,CAmBvB;AAED,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,cAAc,EAAE,SAAS,EAAE,MAAM,GAAG,eAAe,CAQ9F"}
@@ -0,0 +1,183 @@
1
+ import { HerdrCliClient } from "./HerdrCliClient.js";
2
+ export async function fetchHerdrAgentPanes(runner) {
3
+ const client = new HerdrCliClient(runner);
4
+ const response = await client.runJson([
5
+ "pane",
6
+ "list"
7
+ ]);
8
+ return enrichHerdrAgentPanes(client, extractHerdrAgentPanes(response));
9
+ }
10
+ export function extractHerdrAgentPanes(value) {
11
+ const snapshot = getObject(value, [
12
+ "result",
13
+ "snapshot"
14
+ ]);
15
+ const rawPanes = getArray(value, [
16
+ "result",
17
+ "panes"
18
+ ]) ?? snapshot?.agents;
19
+ if (!Array.isArray(rawPanes)) return [];
20
+ const panes = [];
21
+ for (const rawPane of rawPanes){
22
+ if (!rawPane || typeof rawPane !== "object" || Array.isArray(rawPane)) continue;
23
+ const pane = rawPane;
24
+ const agent = nonEmptyString(pane.agent);
25
+ const paneId = nonEmptyString(pane.pane_id);
26
+ if (!agent || !paneId) continue;
27
+ const session = getObject(pane, [
28
+ "agent_session"
29
+ ]);
30
+ const agentSessionId = nonEmptyString(session?.value);
31
+ const cwd = nonEmptyString(pane.cwd);
32
+ const workspaceId = nonEmptyString(pane.workspace_id);
33
+ const tabId = nonEmptyString(pane.tab_id);
34
+ panes.push({
35
+ agent,
36
+ paneId,
37
+ ...agentSessionId ? {
38
+ agentSessionId
39
+ } : {},
40
+ ...cwd ? {
41
+ cwd
42
+ } : {},
43
+ ...workspaceId ? {
44
+ workspaceId
45
+ } : {},
46
+ ...tabId ? {
47
+ tabId
48
+ } : {}
49
+ });
50
+ }
51
+ return panes;
52
+ }
53
+ export function findMatchingHerdrPane(agent, panes) {
54
+ if (agent.sessionId) {
55
+ const sessionMatches = panes.filter((pane)=>pane.agentSessionId === agent.sessionId);
56
+ if (sessionMatches.length === 1) return sessionMatches[0];
57
+ if (sessionMatches.length > 1) return null;
58
+ }
59
+ const foregroundPidMatches = panes.filter((pane)=>pane.foregroundPids?.includes(agent.pid));
60
+ if (foregroundPidMatches.length === 1) return foregroundPidMatches[0];
61
+ if (foregroundPidMatches.length > 1) return null;
62
+ const shellPidMatches = panes.filter((pane)=>pane.shellPid === agent.pid);
63
+ if (shellPidMatches.length === 1) return shellPidMatches[0];
64
+ if (shellPidMatches.length > 1) return null;
65
+ const cwdMatches = panes.filter((pane)=>pane.cwd === agent.projectPath && herdrAgentMatchesType(pane.agent, agent.type));
66
+ return cwdMatches.length === 1 ? cwdMatches[0] : null;
67
+ }
68
+ export function herdrPaneToRuntimeRef(pane, agentName) {
69
+ return {
70
+ session: "default",
71
+ paneId: pane.paneId,
72
+ agentName,
73
+ ...pane.workspaceId ? {
74
+ workspaceId: pane.workspaceId
75
+ } : {},
76
+ ...pane.tabId ? {
77
+ tabId: pane.tabId
78
+ } : {}
79
+ };
80
+ }
81
+ function herdrAgentMatchesType(herdrAgent, agentType) {
82
+ const normalized = herdrAgent.toLowerCase();
83
+ const candidates = {
84
+ claude: [
85
+ "claude"
86
+ ],
87
+ codex: [
88
+ "codex"
89
+ ],
90
+ copilot: [
91
+ "copilot"
92
+ ],
93
+ gemini_cli: [
94
+ "gemini",
95
+ "gemini_cli"
96
+ ],
97
+ grok_cli: [
98
+ "grok",
99
+ "grok_cli"
100
+ ],
101
+ opencode: [
102
+ "opencode"
103
+ ],
104
+ pi: [
105
+ "pi"
106
+ ],
107
+ other: [
108
+ "other"
109
+ ]
110
+ };
111
+ return candidates[agentType]?.includes(normalized) ?? false;
112
+ }
113
+ async function enrichHerdrAgentPanes(client, panes) {
114
+ return Promise.all(panes.map(async (pane)=>{
115
+ try {
116
+ const response = await client.runJson([
117
+ "pane",
118
+ "process-info",
119
+ "--pane",
120
+ pane.paneId
121
+ ]);
122
+ return {
123
+ ...pane,
124
+ ...extractPaneProcessIds(response)
125
+ };
126
+ } catch {
127
+ return pane;
128
+ }
129
+ }));
130
+ }
131
+ function extractPaneProcessIds(value) {
132
+ const processInfo = getObject(value, [
133
+ "result",
134
+ "process_info"
135
+ ]);
136
+ if (!processInfo) return {};
137
+ const foregroundProcesses = processInfo.foreground_processes;
138
+ const foregroundPids = [];
139
+ if (Array.isArray(foregroundProcesses)) {
140
+ for (const foregroundProcess of foregroundProcesses){
141
+ if (!foregroundProcess || typeof foregroundProcess !== "object" || Array.isArray(foregroundProcess)) continue;
142
+ const pid = positiveIntegerOrNull(foregroundProcess.pid);
143
+ if (pid !== null) foregroundPids.push(pid);
144
+ }
145
+ }
146
+ const shellPid = positiveIntegerOrNull(processInfo.shell_pid);
147
+ return {
148
+ ...foregroundPids.length > 0 ? {
149
+ foregroundPids
150
+ } : {},
151
+ ...shellPid !== null ? {
152
+ shellPid
153
+ } : {}
154
+ };
155
+ }
156
+ function getArray(value, path) {
157
+ let current = value;
158
+ for (const part of path){
159
+ if (!current || typeof current !== "object" || Array.isArray(current) || !(part in current)) {
160
+ return null;
161
+ }
162
+ current = current[part];
163
+ }
164
+ return Array.isArray(current) ? current : null;
165
+ }
166
+ function getObject(value, path) {
167
+ let current = value;
168
+ for (const part of path){
169
+ if (!current || typeof current !== "object" || Array.isArray(current) || !(part in current)) {
170
+ return null;
171
+ }
172
+ current = current[part];
173
+ }
174
+ return current && typeof current === "object" && !Array.isArray(current) ? current : null;
175
+ }
176
+ function nonEmptyString(value) {
177
+ return typeof value === "string" && value.trim() ? value : undefined;
178
+ }
179
+ function positiveIntegerOrNull(value) {
180
+ return typeof value === "number" && Number.isInteger(value) && value > 0 ? value : null;
181
+ }
182
+
183
+ //# sourceMappingURL=HerdrAgentDiscovery.js.map