@ai-devkit/agent-manager 0.34.0 → 0.35.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.
- package/dist/AgentManager.d.ts +2 -0
- package/dist/AgentManager.d.ts.map +1 -1
- package/dist/AgentManager.js +25 -0
- package/dist/AgentManager.js.map +1 -1
- package/dist/__tests__/AgentManager.test.js +74 -0
- package/dist/__tests__/AgentManager.test.js.map +1 -1
- package/dist/__tests__/adapters/ClaudeCodeAdapter.test.js +28 -0
- package/dist/__tests__/adapters/ClaudeCodeAdapter.test.js.map +1 -1
- package/dist/__tests__/adapters/CodexAdapter.test.js +7 -0
- package/dist/__tests__/adapters/CodexAdapter.test.js.map +1 -1
- package/dist/__tests__/providers/copilot/CopilotAdapter.test.js +18 -0
- package/dist/__tests__/providers/copilot/CopilotAdapter.test.js.map +1 -1
- package/dist/__tests__/providers/gemini/GeminiCliAdapter.test.js +36 -0
- package/dist/__tests__/providers/gemini/GeminiCliAdapter.test.js.map +1 -1
- package/dist/__tests__/providers/grok/GrokAgentMapper.test.js +56 -0
- package/dist/__tests__/providers/grok/GrokAgentMapper.test.js.map +1 -0
- package/dist/__tests__/{adapters → providers/grok}/GrokCliAdapter.test.js +19 -6
- package/dist/__tests__/providers/grok/GrokCliAdapter.test.js.map +1 -0
- package/dist/__tests__/providers/grok/GrokSessionLocator.test.js +133 -0
- package/dist/__tests__/providers/grok/GrokSessionLocator.test.js.map +1 -0
- package/dist/__tests__/providers/opencode/OpenCodeAdapter.test.js +24 -0
- package/dist/__tests__/providers/opencode/OpenCodeAdapter.test.js.map +1 -1
- package/dist/__tests__/providers/pi/PiAdapter.test.js +30 -0
- package/dist/__tests__/providers/pi/PiAdapter.test.js.map +1 -1
- package/dist/__tests__/utils/session.test.js +18 -1
- package/dist/__tests__/utils/session.test.js.map +1 -1
- package/dist/adapters/AgentAdapter.d.ts +8 -0
- package/dist/adapters/AgentAdapter.d.ts.map +1 -1
- package/dist/adapters/AgentAdapter.js.map +1 -1
- package/dist/adapters/index.d.ts +1 -1
- package/dist/adapters/index.d.ts.map +1 -1
- package/dist/adapters/index.js +1 -1
- package/dist/adapters/index.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/providers/claude/ClaudeCodeAdapter.d.ts +2 -0
- package/dist/providers/claude/ClaudeCodeAdapter.d.ts.map +1 -1
- package/dist/providers/claude/ClaudeCodeAdapter.js +22 -19
- package/dist/providers/claude/ClaudeCodeAdapter.js.map +1 -1
- package/dist/providers/claude/ClaudeSessionLocator.d.ts +4 -0
- package/dist/providers/claude/ClaudeSessionLocator.d.ts.map +1 -1
- package/dist/providers/claude/ClaudeSessionLocator.js +18 -1
- package/dist/providers/claude/ClaudeSessionLocator.js.map +1 -1
- package/dist/providers/codex/CodexAdapter.d.ts +1 -0
- package/dist/providers/codex/CodexAdapter.d.ts.map +1 -1
- package/dist/providers/codex/CodexAdapter.js +8 -0
- package/dist/providers/codex/CodexAdapter.js.map +1 -1
- package/dist/providers/copilot/CopilotAdapter.d.ts +2 -0
- package/dist/providers/copilot/CopilotAdapter.d.ts.map +1 -1
- package/dist/providers/copilot/CopilotAdapter.js +24 -9
- package/dist/providers/copilot/CopilotAdapter.js.map +1 -1
- package/dist/providers/copilot/CopilotSessionLocator.d.ts +1 -0
- package/dist/providers/copilot/CopilotSessionLocator.d.ts.map +1 -1
- package/dist/providers/copilot/CopilotSessionLocator.js +9 -1
- package/dist/providers/copilot/CopilotSessionLocator.js.map +1 -1
- package/dist/providers/gemini/GeminiCliAdapter.d.ts +1 -0
- package/dist/providers/gemini/GeminiCliAdapter.d.ts.map +1 -1
- package/dist/providers/gemini/GeminiCliAdapter.js +9 -0
- package/dist/providers/gemini/GeminiCliAdapter.js.map +1 -1
- package/dist/providers/grok/GrokAgentMapper.d.ts +14 -0
- package/dist/providers/grok/GrokAgentMapper.d.ts.map +1 -0
- package/dist/providers/grok/GrokAgentMapper.js +36 -0
- package/dist/providers/grok/GrokAgentMapper.js.map +1 -0
- package/dist/providers/grok/GrokCliAdapter.d.ts +36 -0
- package/dist/providers/grok/GrokCliAdapter.d.ts.map +1 -0
- package/dist/providers/grok/GrokCliAdapter.js +98 -0
- package/dist/providers/grok/GrokCliAdapter.js.map +1 -0
- package/dist/providers/grok/GrokSessionLocator.d.ts +62 -0
- package/dist/providers/grok/GrokSessionLocator.d.ts.map +1 -0
- package/dist/providers/grok/GrokSessionLocator.js +140 -0
- package/dist/providers/grok/GrokSessionLocator.js.map +1 -0
- package/dist/providers/grok/GrokSessionParser.d.ts +54 -0
- package/dist/providers/grok/GrokSessionParser.d.ts.map +1 -0
- package/dist/providers/grok/GrokSessionParser.js +122 -0
- package/dist/providers/grok/GrokSessionParser.js.map +1 -0
- package/dist/providers/opencode/OpenCodeAdapter.d.ts +1 -0
- package/dist/providers/opencode/OpenCodeAdapter.d.ts.map +1 -1
- package/dist/providers/opencode/OpenCodeAdapter.js +3 -0
- package/dist/providers/opencode/OpenCodeAdapter.js.map +1 -1
- package/dist/providers/opencode/OpenCodeSessionLocator.d.ts +2 -0
- package/dist/providers/opencode/OpenCodeSessionLocator.d.ts.map +1 -1
- package/dist/providers/opencode/OpenCodeSessionLocator.js +31 -12
- package/dist/providers/opencode/OpenCodeSessionLocator.js.map +1 -1
- package/dist/providers/pi/PiAdapter.d.ts +1 -0
- package/dist/providers/pi/PiAdapter.d.ts.map +1 -1
- package/dist/providers/pi/PiAdapter.js +10 -0
- package/dist/providers/pi/PiAdapter.js.map +1 -1
- package/dist/providers/pi/PiSessionLocator.d.ts +1 -0
- package/dist/providers/pi/PiSessionLocator.d.ts.map +1 -1
- package/dist/providers/pi/PiSessionLocator.js +8 -1
- package/dist/providers/pi/PiSessionLocator.js.map +1 -1
- package/dist/utils/session.d.ts +2 -0
- package/dist/utils/session.d.ts.map +1 -1
- package/dist/utils/session.js +3 -0
- package/dist/utils/session.js.map +1 -1
- package/package.json +1 -1
- package/src/AgentManager.ts +34 -0
- package/src/__tests__/AgentManager.test.ts +60 -0
- package/src/__tests__/adapters/ClaudeCodeAdapter.test.ts +23 -0
- package/src/__tests__/adapters/CodexAdapter.test.ts +7 -0
- package/src/__tests__/providers/copilot/CopilotAdapter.test.ts +12 -0
- package/src/__tests__/providers/gemini/GeminiCliAdapter.test.ts +21 -0
- package/src/__tests__/providers/grok/GrokAgentMapper.test.ts +57 -0
- package/src/__tests__/{adapters → providers/grok}/GrokCliAdapter.test.ts +18 -9
- package/src/__tests__/providers/grok/GrokSessionLocator.test.ts +106 -0
- package/src/__tests__/providers/opencode/OpenCodeAdapter.test.ts +18 -0
- package/src/__tests__/providers/pi/PiAdapter.test.ts +15 -0
- package/src/__tests__/utils/session.test.ts +11 -1
- package/src/adapters/AgentAdapter.ts +9 -0
- package/src/adapters/index.ts +1 -1
- package/src/index.ts +1 -1
- package/src/providers/claude/ClaudeCodeAdapter.ts +28 -23
- package/src/providers/claude/ClaudeSessionLocator.ts +20 -0
- package/src/providers/codex/CodexAdapter.ts +8 -0
- package/src/providers/copilot/CopilotAdapter.ts +26 -10
- package/src/providers/copilot/CopilotSessionLocator.ts +7 -1
- package/src/providers/gemini/GeminiCliAdapter.ts +8 -0
- package/src/providers/grok/GrokAgentMapper.ts +41 -0
- package/src/providers/grok/GrokCliAdapter.ts +125 -0
- package/src/providers/grok/GrokSessionLocator.ts +179 -0
- package/src/providers/grok/GrokSessionParser.ts +166 -0
- package/src/providers/opencode/OpenCodeAdapter.ts +4 -0
- package/src/providers/opencode/OpenCodeSessionLocator.ts +43 -15
- package/src/providers/pi/PiAdapter.ts +12 -0
- package/src/providers/pi/PiSessionLocator.ts +16 -1
- package/src/utils/session.ts +11 -0
- package/dist/__tests__/adapters/GrokCliAdapter.test.js.map +0 -1
- package/dist/adapters/GrokCliAdapter.d.ts +0 -80
- package/dist/adapters/GrokCliAdapter.d.ts.map +0 -1
- package/dist/adapters/GrokCliAdapter.js +0 -310
- package/dist/adapters/GrokCliAdapter.js.map +0 -1
- package/src/adapters/GrokCliAdapter.ts +0 -408
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/utils/session.ts"],"sourcesContent":["/**\n * Session File Utilities\n *\n * Utilities for discovering session files and their birth times.\n * Uses Node.js fs APIs to get birth timestamps without reading file contents.\n */\n\nimport * as fs from \"fs\";\nimport * as path from \"path\";\n\n/**\n * Represents a session file with its birth time metadata.\n */\nexport interface SessionFile {\n /** Session identifier (filename without .jsonl extension) */\n sessionId: string;\n\n /** Full path to the session file */\n filePath: string;\n\n /** Parent directory of the session file */\n projectDir: string;\n\n /** File creation time in milliseconds since epoch */\n birthtimeMs: number;\n\n /** CWD this session maps to — set by the adapter after calling batchGetSessionFileBirthtimes() */\n resolvedCwd: string;\n}\n\n/**\n * Check whether a path exists and is a directory.\n * Returns false on any error (missing path, permission denied, broken symlink, etc.).\n */\nexport function isDirectory(p: string): boolean {\n return safeStat(p)?.isDirectory() ?? false;\n}\n\n/**\n * `fs.statSync` that swallows errors and returns `undefined` on failure.\n * Callers can pull whichever fields they need (mtime, birthtime, ...).\n */\nexport function safeStat(filePath: string): fs.Stats | undefined {\n try {\n return fs.statSync(filePath);\n } catch {\n return undefined;\n }\n}\n\n/**\n * `fs.readFileSync` (utf-8) that swallows errors and returns `undefined`\n * on failure. Use when an unreadable file should be skipped rather than\n * raised.\n */\nexport function safeReadFile(filePath: string): string | undefined {\n try {\n return fs.readFileSync(filePath, \"utf-8\");\n } catch {\n return undefined;\n }\n}\n\n/**\n * `fs.readdirSync` that swallows errors and returns `[]` on failure.\n * Useful when walking optional/transient directories where missing or\n * unreadable entries should be skipped silently.\n */\nexport function safeReaddir(dir: string): string[] {\n try {\n return fs.readdirSync(dir);\n } catch {\n return [];\n }\n}\n\n/**\n * List entries in a directory that end with `.jsonl`. Returns `[]` on\n * read errors. The result preserves directory order (no sorting).\n */\nexport function listJsonl(dir: string): string[] {\n return safeReaddir(dir).filter((name) => name.endsWith(\".jsonl\"));\n}\n\n/**\n * Get birth times for .jsonl session files across multiple directories.\n *\n * Enumerates each directory with readdirSync and stats each .jsonl file\n * to get its birth time. No shell commands are used.\n * Returns empty array if no directories have .jsonl files or reads fail.\n * resolvedCwd is left empty — the adapter must set it.\n */\nexport function batchGetSessionFileBirthtimes(dirs: string[]): SessionFile[] {\n if (dirs.length === 0) return [];\n\n const results: SessionFile[] = [];\n\n for (const dir of dirs) {\n let entries: string[];\n try {\n entries = fs.readdirSync(dir);\n } catch {\n continue;\n }\n\n for (const entry of entries) {\n if (!entry.endsWith(\".jsonl\")) continue;\n\n const filePath = path.join(dir, entry);\n\n let birthtimeMs: number;\n try {\n birthtimeMs = fs.statSync(filePath).birthtimeMs;\n } catch {\n continue;\n }\n\n if (!Number.isFinite(birthtimeMs) || birthtimeMs <= 0) continue;\n\n const sessionId = entry.replace(/\\.jsonl$/, \"\");\n\n results.push({\n sessionId,\n filePath,\n projectDir: dir,\n birthtimeMs,\n resolvedCwd: \"\",\n });\n }\n }\n\n return results;\n}\n"],"names":["fs","path","isDirectory","p","safeStat","filePath","statSync","undefined","safeReadFile","readFileSync","safeReaddir","dir","readdirSync","listJsonl","filter","name","endsWith","batchGetSessionFileBirthtimes","dirs","
|
|
1
|
+
{"version":3,"sources":["../../src/utils/session.ts"],"sourcesContent":["/**\n * Session File Utilities\n *\n * Utilities for discovering session files and their birth times.\n * Uses Node.js fs APIs to get birth timestamps without reading file contents.\n */\n\nimport * as fs from \"fs\";\nimport * as path from \"path\";\n\n/**\n * Represents a session file with its birth time metadata.\n */\nexport interface SessionFile {\n /** Session identifier (filename without .jsonl extension) */\n sessionId: string;\n\n /** Full path to the session file */\n filePath: string;\n\n /** Parent directory of the session file */\n projectDir: string;\n\n /** File creation time in milliseconds since epoch */\n birthtimeMs: number;\n\n /** CWD this session maps to — set by the adapter after calling batchGetSessionFileBirthtimes() */\n resolvedCwd: string;\n}\n\n/**\n * Check whether a path exists and is a directory.\n * Returns false on any error (missing path, permission denied, broken symlink, etc.).\n */\nexport function isDirectory(p: string): boolean {\n return safeStat(p)?.isDirectory() ?? false;\n}\n\n/** Return whether a value can be safely appended as one filesystem path segment. */\nexport function isSafePathSegment(value: string): boolean {\n return (\n value.length > 0 &&\n value !== \".\" &&\n value !== \"..\" &&\n !value.includes(\"\\0\") &&\n path.basename(value) === value\n );\n}\n\n/**\n * `fs.statSync` that swallows errors and returns `undefined` on failure.\n * Callers can pull whichever fields they need (mtime, birthtime, ...).\n */\nexport function safeStat(filePath: string): fs.Stats | undefined {\n try {\n return fs.statSync(filePath);\n } catch {\n return undefined;\n }\n}\n\n/**\n * `fs.readFileSync` (utf-8) that swallows errors and returns `undefined`\n * on failure. Use when an unreadable file should be skipped rather than\n * raised.\n */\nexport function safeReadFile(filePath: string): string | undefined {\n try {\n return fs.readFileSync(filePath, \"utf-8\");\n } catch {\n return undefined;\n }\n}\n\n/**\n * `fs.readdirSync` that swallows errors and returns `[]` on failure.\n * Useful when walking optional/transient directories where missing or\n * unreadable entries should be skipped silently.\n */\nexport function safeReaddir(dir: string): string[] {\n try {\n return fs.readdirSync(dir);\n } catch {\n return [];\n }\n}\n\n/**\n * List entries in a directory that end with `.jsonl`. Returns `[]` on\n * read errors. The result preserves directory order (no sorting).\n */\nexport function listJsonl(dir: string): string[] {\n return safeReaddir(dir).filter((name) => name.endsWith(\".jsonl\"));\n}\n\n/**\n * Get birth times for .jsonl session files across multiple directories.\n *\n * Enumerates each directory with readdirSync and stats each .jsonl file\n * to get its birth time. No shell commands are used.\n * Returns empty array if no directories have .jsonl files or reads fail.\n * resolvedCwd is left empty — the adapter must set it.\n */\nexport function batchGetSessionFileBirthtimes(dirs: string[]): SessionFile[] {\n if (dirs.length === 0) return [];\n\n const results: SessionFile[] = [];\n\n for (const dir of dirs) {\n let entries: string[];\n try {\n entries = fs.readdirSync(dir);\n } catch {\n continue;\n }\n\n for (const entry of entries) {\n if (!entry.endsWith(\".jsonl\")) continue;\n\n const filePath = path.join(dir, entry);\n\n let birthtimeMs: number;\n try {\n birthtimeMs = fs.statSync(filePath).birthtimeMs;\n } catch {\n continue;\n }\n\n if (!Number.isFinite(birthtimeMs) || birthtimeMs <= 0) continue;\n\n const sessionId = entry.replace(/\\.jsonl$/, \"\");\n\n results.push({\n sessionId,\n filePath,\n projectDir: dir,\n birthtimeMs,\n resolvedCwd: \"\",\n });\n }\n }\n\n return results;\n}\n"],"names":["fs","path","isDirectory","p","safeStat","isSafePathSegment","value","length","includes","basename","filePath","statSync","undefined","safeReadFile","readFileSync","safeReaddir","dir","readdirSync","listJsonl","filter","name","endsWith","batchGetSessionFileBirthtimes","dirs","results","entries","entry","join","birthtimeMs","Number","isFinite","sessionId","replace","push","projectDir","resolvedCwd"],"mappings":"AAAA;;;;;CAKC,GAED,YAAYA,QAAQ,KAAK;AACzB,YAAYC,UAAU,OAAO;AAsB7B;;;CAGC,GACD,OAAO,SAASC,YAAYC,CAAS;IACnC,OAAOC,SAASD,IAAID,iBAAiB;AACvC;AAEA,kFAAkF,GAClF,OAAO,SAASG,kBAAkBC,KAAa;IAC7C,OACEA,MAAMC,MAAM,GAAG,KACfD,UAAU,OACVA,UAAU,QACV,CAACA,MAAME,QAAQ,CAAC,SAChBP,KAAKQ,QAAQ,CAACH,WAAWA;AAE7B;AAEA;;;CAGC,GACD,OAAO,SAASF,SAASM,QAAgB;IACvC,IAAI;QACF,OAAOV,GAAGW,QAAQ,CAACD;IACrB,EAAE,OAAM;QACN,OAAOE;IACT;AACF;AAEA;;;;CAIC,GACD,OAAO,SAASC,aAAaH,QAAgB;IAC3C,IAAI;QACF,OAAOV,GAAGc,YAAY,CAACJ,UAAU;IACnC,EAAE,OAAM;QACN,OAAOE;IACT;AACF;AAEA;;;;CAIC,GACD,OAAO,SAASG,YAAYC,GAAW;IACrC,IAAI;QACF,OAAOhB,GAAGiB,WAAW,CAACD;IACxB,EAAE,OAAM;QACN,OAAO,EAAE;IACX;AACF;AAEA;;;CAGC,GACD,OAAO,SAASE,UAAUF,GAAW;IACnC,OAAOD,YAAYC,KAAKG,MAAM,CAAC,CAACC,OAASA,KAAKC,QAAQ,CAAC;AACzD;AAEA;;;;;;;CAOC,GACD,OAAO,SAASC,8BAA8BC,IAAc;IAC1D,IAAIA,KAAKhB,MAAM,KAAK,GAAG,OAAO,EAAE;IAEhC,MAAMiB,UAAyB,EAAE;IAEjC,KAAK,MAAMR,OAAOO,KAAM;QACtB,IAAIE;QACJ,IAAI;YACFA,UAAUzB,GAAGiB,WAAW,CAACD;QAC3B,EAAE,OAAM;YACN;QACF;QAEA,KAAK,MAAMU,SAASD,QAAS;YAC3B,IAAI,CAACC,MAAML,QAAQ,CAAC,WAAW;YAE/B,MAAMX,WAAWT,KAAK0B,IAAI,CAACX,KAAKU;YAEhC,IAAIE;YACJ,IAAI;gBACFA,cAAc5B,GAAGW,QAAQ,CAACD,UAAUkB,WAAW;YACjD,EAAE,OAAM;gBACN;YACF;YAEA,IAAI,CAACC,OAAOC,QAAQ,CAACF,gBAAgBA,eAAe,GAAG;YAEvD,MAAMG,YAAYL,MAAMM,OAAO,CAAC,YAAY;YAE5CR,QAAQS,IAAI,CAAC;gBACXF;gBACArB;gBACAwB,YAAYlB;gBACZY;gBACAO,aAAa;YACf;QACF;IACF;IAEA,OAAOX;AACT"}
|
package/package.json
CHANGED
package/src/AgentManager.ts
CHANGED
|
@@ -347,6 +347,40 @@ export class AgentManager {
|
|
|
347
347
|
return merged;
|
|
348
348
|
}
|
|
349
349
|
|
|
350
|
+
/** Resolve an exact historical session ID across the selected providers. */
|
|
351
|
+
async findSessionsById(
|
|
352
|
+
sessionId: string,
|
|
353
|
+
opts?: Pick<ListSessionsOptions, "type">,
|
|
354
|
+
): Promise<SessionSummary[]> {
|
|
355
|
+
const targetAdapters = Array.from(this.adapters.values()).filter(
|
|
356
|
+
(adapter) => opts?.type === undefined || adapter.type === opts.type,
|
|
357
|
+
);
|
|
358
|
+
const errors: Array<{ type: string; error: Error }> = [];
|
|
359
|
+
|
|
360
|
+
const results = await Promise.all(
|
|
361
|
+
targetAdapters.map(async (adapter) => {
|
|
362
|
+
try {
|
|
363
|
+
if (adapter.findSessionsById) return await adapter.findSessionsById(sessionId);
|
|
364
|
+
const sessions = await adapter.listSessions(opts);
|
|
365
|
+
return sessions.filter((session) => session.sessionId === sessionId);
|
|
366
|
+
} catch (error) {
|
|
367
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
368
|
+
errors.push({ type: adapter.type, error: err });
|
|
369
|
+
return [];
|
|
370
|
+
}
|
|
371
|
+
}),
|
|
372
|
+
);
|
|
373
|
+
|
|
374
|
+
if (errors.length > 0) {
|
|
375
|
+
console.error(`Warning: ${errors.length} adapter(s) failed to find session by ID:`);
|
|
376
|
+
for (const { type, error } of errors) {
|
|
377
|
+
console.error(` - ${type}: ${error.message}`);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
return results.flat();
|
|
382
|
+
}
|
|
383
|
+
|
|
350
384
|
/**
|
|
351
385
|
* Get count of registered adapters
|
|
352
386
|
*
|
|
@@ -21,6 +21,7 @@ import type { HerdrAgentPane } from "../runtime/herdr/HerdrAgentDiscovery.js";
|
|
|
21
21
|
// Mock adapter for testing
|
|
22
22
|
class MockAdapter implements AgentAdapter {
|
|
23
23
|
public lastListSessionsOpts: unknown = undefined;
|
|
24
|
+
public findSessionsByIdCalls: string[] = [];
|
|
24
25
|
|
|
25
26
|
constructor(
|
|
26
27
|
public readonly type: AgentType,
|
|
@@ -53,6 +54,11 @@ class MockAdapter implements AgentAdapter {
|
|
|
53
54
|
return this.mockSessions;
|
|
54
55
|
}
|
|
55
56
|
|
|
57
|
+
async findSessionsById(sessionId: string): Promise<SessionSummary[]> {
|
|
58
|
+
this.findSessionsByIdCalls.push(sessionId);
|
|
59
|
+
return this.mockSessions.filter((session) => session.sessionId === sessionId);
|
|
60
|
+
}
|
|
61
|
+
|
|
56
62
|
setAgents(agents: AgentInfo[]): void {
|
|
57
63
|
this.mockAgents = agents;
|
|
58
64
|
}
|
|
@@ -1119,6 +1125,60 @@ describe("AgentManager", () => {
|
|
|
1119
1125
|
});
|
|
1120
1126
|
});
|
|
1121
1127
|
|
|
1128
|
+
describe("findSessionsById", () => {
|
|
1129
|
+
const session = (type: AgentType, sessionId: string): SessionSummary => ({
|
|
1130
|
+
type,
|
|
1131
|
+
sessionId,
|
|
1132
|
+
cwd: "/repo",
|
|
1133
|
+
firstUserMessage: "hello",
|
|
1134
|
+
lastActive: new Date("2025-01-01T00:00:00Z"),
|
|
1135
|
+
startedAt: new Date("2025-01-01T00:00:00Z"),
|
|
1136
|
+
sessionFilePath: `/tmp/${sessionId}`,
|
|
1137
|
+
});
|
|
1138
|
+
|
|
1139
|
+
it("uses each built-in adapter direct lookup and preserves cross-provider ambiguity", async () => {
|
|
1140
|
+
const claude = new MockAdapter("claude", [], false, [session("claude", "shared")]);
|
|
1141
|
+
const codex = new MockAdapter("codex", [], false, [session("codex", "shared")]);
|
|
1142
|
+
manager.registerAdapter(claude);
|
|
1143
|
+
manager.registerAdapter(codex);
|
|
1144
|
+
|
|
1145
|
+
const result = await manager.findSessionsById("shared");
|
|
1146
|
+
|
|
1147
|
+
expect(result).toHaveLength(2);
|
|
1148
|
+
expect(claude.findSessionsByIdCalls).toEqual(["shared"]);
|
|
1149
|
+
expect(codex.findSessionsByIdCalls).toEqual(["shared"]);
|
|
1150
|
+
expect(claude.lastListSessionsOpts).toBeUndefined();
|
|
1151
|
+
expect(codex.lastListSessionsOpts).toBeUndefined();
|
|
1152
|
+
});
|
|
1153
|
+
|
|
1154
|
+
it("skips non-matching providers when type is supplied", async () => {
|
|
1155
|
+
const claude = new MockAdapter("claude", [], false, [session("claude", "target")]);
|
|
1156
|
+
const codex = new MockAdapter("codex", [], false, [session("codex", "target")]);
|
|
1157
|
+
manager.registerAdapter(claude);
|
|
1158
|
+
manager.registerAdapter(codex);
|
|
1159
|
+
|
|
1160
|
+
const result = await manager.findSessionsById("target", { type: "codex" });
|
|
1161
|
+
|
|
1162
|
+
expect(result).toEqual([expect.objectContaining({ type: "codex", sessionId: "target" })]);
|
|
1163
|
+
expect(claude.findSessionsByIdCalls).toEqual([]);
|
|
1164
|
+
expect(codex.findSessionsByIdCalls).toEqual(["target"]);
|
|
1165
|
+
});
|
|
1166
|
+
|
|
1167
|
+
it("falls back to filtered listing for external adapters without direct lookup", async () => {
|
|
1168
|
+
const external = new MockAdapter("other", [], false, [
|
|
1169
|
+
session("other", "target"),
|
|
1170
|
+
session("other", "different"),
|
|
1171
|
+
]);
|
|
1172
|
+
(external as Partial<AgentAdapter>).findSessionsById = undefined;
|
|
1173
|
+
manager.registerAdapter(external);
|
|
1174
|
+
|
|
1175
|
+
const result = await manager.findSessionsById("target");
|
|
1176
|
+
|
|
1177
|
+
expect(result).toEqual([expect.objectContaining({ sessionId: "target" })]);
|
|
1178
|
+
expect(external.lastListSessionsOpts).toEqual(undefined);
|
|
1179
|
+
});
|
|
1180
|
+
});
|
|
1181
|
+
|
|
1122
1182
|
describe("resolveAgent", () => {
|
|
1123
1183
|
it("should return null for empty input or empty agents list", () => {
|
|
1124
1184
|
const agent = createMockAgent({ name: "test-agent" });
|
|
@@ -1624,6 +1624,29 @@ describe("ClaudeCodeAdapter", () => {
|
|
|
1624
1624
|
expect(cwds).toEqual([cwdA, cwdB]);
|
|
1625
1625
|
});
|
|
1626
1626
|
|
|
1627
|
+
it("finds an exact session ID without returning other project sessions", async () => {
|
|
1628
|
+
const wantedPath = writeSession(path.join(projectsDir, "-repo-a"), "wanted", [
|
|
1629
|
+
{
|
|
1630
|
+
type: "user",
|
|
1631
|
+
timestamp: "2025-01-01T00:00:00Z",
|
|
1632
|
+
cwd: "/repo-a",
|
|
1633
|
+
message: { content: "wanted prompt" },
|
|
1634
|
+
},
|
|
1635
|
+
]);
|
|
1636
|
+
writeSession(path.join(projectsDir, "-repo-b"), "other", [
|
|
1637
|
+
{
|
|
1638
|
+
type: "user",
|
|
1639
|
+
timestamp: "2025-01-01T00:00:00Z",
|
|
1640
|
+
cwd: "/repo-b",
|
|
1641
|
+
message: { content: "other prompt" },
|
|
1642
|
+
},
|
|
1643
|
+
]);
|
|
1644
|
+
|
|
1645
|
+
await expect(adapter.findSessionsById("wanted")).resolves.toEqual([
|
|
1646
|
+
expect.objectContaining({ sessionId: "wanted", sessionFilePath: wantedPath }),
|
|
1647
|
+
]);
|
|
1648
|
+
});
|
|
1649
|
+
|
|
1627
1650
|
it("drops sessions whose recorded cwd does not match opts.cwd (strict equality)", async () => {
|
|
1628
1651
|
const cwdReal = "/Users/test/foo";
|
|
1629
1652
|
const cwdRequested = "/Users/test/foo/sub";
|
|
@@ -339,6 +339,13 @@ describe("CodexAdapter", () => {
|
|
|
339
339
|
sessionFilePath: sessionFile,
|
|
340
340
|
},
|
|
341
341
|
]);
|
|
342
|
+
await expect(adapter.findSessionsById("listed")).resolves.toMatchObject([
|
|
343
|
+
{
|
|
344
|
+
type: "codex",
|
|
345
|
+
sessionId: "listed",
|
|
346
|
+
sessionFilePath: sessionFile,
|
|
347
|
+
},
|
|
348
|
+
]);
|
|
342
349
|
});
|
|
343
350
|
|
|
344
351
|
function writeSession(
|
|
@@ -552,6 +552,18 @@ describe("CopilotAdapter", () => {
|
|
|
552
552
|
});
|
|
553
553
|
});
|
|
554
554
|
|
|
555
|
+
it("finds an exact session ID from its session directory", async () => {
|
|
556
|
+
writeSession("wanted", { events: [sessionStart("wanted", "/repo")] });
|
|
557
|
+
writeSession("other", { events: [sessionStart("other", "/other")] });
|
|
558
|
+
|
|
559
|
+
await expect(adapter.findSessionsById("wanted")).resolves.toEqual([
|
|
560
|
+
expect.objectContaining({
|
|
561
|
+
sessionId: "wanted",
|
|
562
|
+
sessionFilePath: path.join(sessionStateDir, "wanted", "events.jsonl"),
|
|
563
|
+
}),
|
|
564
|
+
]);
|
|
565
|
+
});
|
|
566
|
+
|
|
555
567
|
it("applies strict cwd filter", async () => {
|
|
556
568
|
writeSession("keep", { events: [sessionStart("keep", "/repo")] });
|
|
557
569
|
writeSession("drop", { events: [sessionStart("drop", "/other")] });
|
|
@@ -1185,6 +1185,27 @@ describe("GeminiCliAdapter", () => {
|
|
|
1185
1185
|
});
|
|
1186
1186
|
});
|
|
1187
1187
|
|
|
1188
|
+
it("finds an exact session ID from Gemini metadata", async () => {
|
|
1189
|
+
const wantedPath = writeSession(tmpHome, "aaa", "session-one", {
|
|
1190
|
+
sessionId: "wanted",
|
|
1191
|
+
projectHash: hashProjectRoot("/repo"),
|
|
1192
|
+
startTime: "2025-01-01T00:00:00Z",
|
|
1193
|
+
directories: ["/repo"],
|
|
1194
|
+
messages: [{ type: "user", content: "wanted prompt" }],
|
|
1195
|
+
});
|
|
1196
|
+
writeSession(tmpHome, "bbb", "session-two", {
|
|
1197
|
+
sessionId: "other",
|
|
1198
|
+
projectHash: hashProjectRoot("/other"),
|
|
1199
|
+
startTime: "2025-01-01T00:00:00Z",
|
|
1200
|
+
directories: ["/other"],
|
|
1201
|
+
messages: [{ type: "user", content: "other prompt" }],
|
|
1202
|
+
});
|
|
1203
|
+
|
|
1204
|
+
await expect(adapter.findSessionsById("wanted")).resolves.toEqual([
|
|
1205
|
+
expect.objectContaining({ sessionId: "wanted", sessionFilePath: wantedPath }),
|
|
1206
|
+
]);
|
|
1207
|
+
});
|
|
1208
|
+
|
|
1188
1209
|
it("applies strict-equality cwd filter against directories[0]", async () => {
|
|
1189
1210
|
writeSession(tmpHome, "aaa", "session-keep", {
|
|
1190
1211
|
sessionId: "keep",
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
|
|
3
|
+
import { AgentStatus, type ProcessInfo } from "../../../adapters/AgentAdapter.js";
|
|
4
|
+
import { GrokAgentMapper } from "../../../providers/grok/GrokAgentMapper.js";
|
|
5
|
+
import type { GrokSession } from "../../../providers/grok/GrokSessionParser.js";
|
|
6
|
+
|
|
7
|
+
function makeProcess(overrides: Partial<ProcessInfo> = {}): ProcessInfo {
|
|
8
|
+
return { pid: 123, command: "grok", cwd: "/repo/my-app", tty: "ttys001", ...overrides };
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function makeSession(overrides: Partial<GrokSession> = {}): GrokSession {
|
|
12
|
+
return {
|
|
13
|
+
sessionId: "session-1",
|
|
14
|
+
projectPath: "/repo/my-app",
|
|
15
|
+
sessionFilePath: "/home/.grok/sessions/%2Frepo%2Fmy-app/session-1/chat_history.jsonl",
|
|
16
|
+
sessionStart: new Date(),
|
|
17
|
+
lastActive: new Date(),
|
|
18
|
+
lastUserMessage: "Review this change",
|
|
19
|
+
lastRole: "assistant",
|
|
20
|
+
...overrides,
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
describe("GrokAgentMapper", () => {
|
|
25
|
+
const mapper = new GrokAgentMapper();
|
|
26
|
+
|
|
27
|
+
it("maps a session to a grok_cli agent pointing at chat_history.jsonl", () => {
|
|
28
|
+
const agent = mapper.mapSessionToAgent({
|
|
29
|
+
session: makeSession(),
|
|
30
|
+
processInfo: makeProcess(),
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
expect(agent).toMatchObject({
|
|
34
|
+
name: "my-app-123",
|
|
35
|
+
type: "grok_cli",
|
|
36
|
+
status: AgentStatus.WAITING,
|
|
37
|
+
summary: "Review this change",
|
|
38
|
+
pid: 123,
|
|
39
|
+
projectPath: "/repo/my-app",
|
|
40
|
+
sessionId: "session-1",
|
|
41
|
+
sessionFilePath: "/home/.grok/sessions/%2Frepo%2Fmy-app/session-1/chat_history.jsonl",
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("maps a process-only agent as RUNNING with a pid session id", () => {
|
|
46
|
+
const agent = mapper.mapProcessOnlyAgent(makeProcess(), "/repo/resolved");
|
|
47
|
+
|
|
48
|
+
expect(agent).toMatchObject({
|
|
49
|
+
type: "grok_cli",
|
|
50
|
+
status: AgentStatus.RUNNING,
|
|
51
|
+
summary: "Grok CLI process running",
|
|
52
|
+
projectPath: "/repo/resolved",
|
|
53
|
+
sessionId: "pid-123",
|
|
54
|
+
});
|
|
55
|
+
expect(agent.sessionFilePath).toBeUndefined();
|
|
56
|
+
});
|
|
57
|
+
});
|
|
@@ -10,18 +10,18 @@ import * as fs from "fs";
|
|
|
10
10
|
import * as os from "os";
|
|
11
11
|
import * as path from "path";
|
|
12
12
|
|
|
13
|
-
import { GrokCliAdapter } from "
|
|
14
|
-
import type { ProcessInfo } from "
|
|
15
|
-
import { AgentStatus } from "
|
|
13
|
+
import { GrokCliAdapter } from "../../../providers/grok/GrokCliAdapter.js";
|
|
14
|
+
import type { ProcessInfo } from "../../../adapters/AgentAdapter.js";
|
|
15
|
+
import { AgentStatus } from "../../../adapters/AgentAdapter.js";
|
|
16
16
|
import {
|
|
17
17
|
listAgentProcesses,
|
|
18
18
|
enrichProcesses,
|
|
19
19
|
captureProcessSnapshot,
|
|
20
|
-
} from "
|
|
21
|
-
import { generateAgentName } from "
|
|
20
|
+
} from "../../../utils/process.js";
|
|
21
|
+
import { generateAgentName } from "../../../utils/matching.js";
|
|
22
22
|
|
|
23
|
-
vi.mock("
|
|
24
|
-
const actual = (await importOriginal()) as typeof import("
|
|
23
|
+
vi.mock("../../../utils/process.js", async (importOriginal) => {
|
|
24
|
+
const actual = (await importOriginal()) as typeof import("../../../utils/process.js");
|
|
25
25
|
return {
|
|
26
26
|
...actual,
|
|
27
27
|
listAgentProcesses: vi.fn(),
|
|
@@ -30,8 +30,8 @@ vi.mock("../../utils/process.js", async (importOriginal) => {
|
|
|
30
30
|
};
|
|
31
31
|
});
|
|
32
32
|
|
|
33
|
-
vi.mock("
|
|
34
|
-
const actual = (await importOriginal()) as typeof import("
|
|
33
|
+
vi.mock("../../../utils/matching.js", async (importOriginal) => {
|
|
34
|
+
const actual = (await importOriginal()) as typeof import("../../../utils/matching.js");
|
|
35
35
|
return {
|
|
36
36
|
...actual,
|
|
37
37
|
generateAgentName: vi.fn(),
|
|
@@ -335,6 +335,15 @@ describe("GrokCliAdapter", () => {
|
|
|
335
335
|
);
|
|
336
336
|
});
|
|
337
337
|
|
|
338
|
+
it("finds an exact session ID across project groups", async () => {
|
|
339
|
+
writeSession({});
|
|
340
|
+
writeSession({ sessionCwd: "/Users/dev/other", id: "other-session" });
|
|
341
|
+
|
|
342
|
+
await expect(adapter.findSessionsById(SESSION_ID)).resolves.toEqual([
|
|
343
|
+
expect.objectContaining({ sessionId: SESSION_ID, cwd }),
|
|
344
|
+
]);
|
|
345
|
+
});
|
|
346
|
+
|
|
338
347
|
it("applies the cwd filter against the decoded cwd", async () => {
|
|
339
348
|
writeSession({
|
|
340
349
|
sessionCwd: "/Users/dev/project-a",
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
import * as os from "os";
|
|
3
|
+
import * as path from "path";
|
|
4
|
+
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
5
|
+
|
|
6
|
+
import type { ProcessInfo } from "../../../adapters/AgentAdapter.js";
|
|
7
|
+
import { GrokSessionLocator } from "../../../providers/grok/GrokSessionLocator.js";
|
|
8
|
+
|
|
9
|
+
describe("GrokSessionLocator", () => {
|
|
10
|
+
let baseDir: string;
|
|
11
|
+
let locator: GrokSessionLocator;
|
|
12
|
+
|
|
13
|
+
beforeEach(() => {
|
|
14
|
+
baseDir = fs.mkdtempSync(path.join(os.tmpdir(), "grok-locator-test-"));
|
|
15
|
+
locator = new GrokSessionLocator({ baseDir });
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
afterEach(() => {
|
|
19
|
+
fs.rmSync(baseDir, { recursive: true, force: true });
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
function writeSession(cwd: string, id: string, mtime?: Date): string {
|
|
23
|
+
const sessionDir = path.join(baseDir, "sessions", encodeURIComponent(cwd), id);
|
|
24
|
+
fs.mkdirSync(sessionDir, { recursive: true });
|
|
25
|
+
const chatPath = path.join(sessionDir, "chat_history.jsonl");
|
|
26
|
+
fs.writeFileSync(chatPath, "");
|
|
27
|
+
if (mtime) fs.utimesSync(chatPath, mtime, mtime);
|
|
28
|
+
return sessionDir;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function proc(overrides: Partial<ProcessInfo> = {}): ProcessInfo {
|
|
32
|
+
return { pid: 4242, command: "grok", cwd: "/repo/app", tty: "ttys001", ...overrides };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
describe("matchRunningProcesses", () => {
|
|
36
|
+
it("prefers the active_sessions.json cwd over the process cwd", () => {
|
|
37
|
+
const sessionDir = writeSession("/repo/real", "s1");
|
|
38
|
+
fs.writeFileSync(
|
|
39
|
+
path.join(baseDir, "active_sessions.json"),
|
|
40
|
+
JSON.stringify([{ pid: 4242, cwd: "/repo/real" }]),
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
expect(locator.matchRunningProcesses([proc({ cwd: "/wrong" })])).toEqual([
|
|
44
|
+
{ process: expect.objectContaining({ pid: 4242 }), cwd: "/repo/real", sessionDir },
|
|
45
|
+
]);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("ignores a malformed active_sessions.json and falls back to the process cwd", () => {
|
|
49
|
+
const sessionDir = writeSession("/repo/app", "s1");
|
|
50
|
+
fs.writeFileSync(path.join(baseDir, "active_sessions.json"), "{not json");
|
|
51
|
+
|
|
52
|
+
expect(locator.matchRunningProcesses([proc()])[0]).toMatchObject({
|
|
53
|
+
cwd: "/repo/app",
|
|
54
|
+
sessionDir,
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("picks the session with the newest chat_history.jsonl", () => {
|
|
59
|
+
writeSession("/repo/app", "old", new Date(Date.now() - 60_000));
|
|
60
|
+
const newest = writeSession("/repo/app", "new");
|
|
61
|
+
|
|
62
|
+
expect(locator.matchRunningProcesses([proc()])[0].sessionDir).toBe(newest);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("returns a null sessionDir when no cwd is known", () => {
|
|
66
|
+
expect(locator.matchRunningProcesses([proc({ cwd: "" })])[0]).toMatchObject({
|
|
67
|
+
cwd: "",
|
|
68
|
+
sessionDir: null,
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
describe("historical sessions", () => {
|
|
74
|
+
it("discovers session dirs with the decoded group cwd", () => {
|
|
75
|
+
const a = writeSession("/repo/a", "s1");
|
|
76
|
+
const b = writeSession("/repo/b", "s2");
|
|
77
|
+
|
|
78
|
+
expect(locator.discoverHistoricalSessionDirs()).toEqual(
|
|
79
|
+
expect.arrayContaining([
|
|
80
|
+
{ sessionDir: a, defaultCwd: "/repo/a" },
|
|
81
|
+
{ sessionDir: b, defaultCwd: "/repo/b" },
|
|
82
|
+
]),
|
|
83
|
+
);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("prefers the .cwd file over the encoded group name", () => {
|
|
87
|
+
const groupDir = path.join(baseDir, "sessions", "slug-abc123");
|
|
88
|
+
fs.mkdirSync(path.join(groupDir, "s1"), { recursive: true });
|
|
89
|
+
fs.writeFileSync(path.join(groupDir, ".cwd"), "/very/long/path\n");
|
|
90
|
+
|
|
91
|
+
expect(locator.discoverHistoricalSessionDirs()).toEqual([
|
|
92
|
+
{ sessionDir: path.join(groupDir, "s1"), defaultCwd: "/very/long/path" },
|
|
93
|
+
]);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("finds session dirs by id and rejects unsafe ids", () => {
|
|
97
|
+
const dir = writeSession("/repo/a", "s1");
|
|
98
|
+
writeSession("/repo/b", "s2");
|
|
99
|
+
|
|
100
|
+
expect(locator.findHistoricalSessionDirsById("s1")).toEqual([
|
|
101
|
+
{ sessionDir: dir, defaultCwd: "/repo/a" },
|
|
102
|
+
]);
|
|
103
|
+
expect(locator.findHistoricalSessionDirsById("../s1")).toEqual([]);
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
});
|
|
@@ -121,6 +121,24 @@ describe("OpenCodeAdapter", () => {
|
|
|
121
121
|
]);
|
|
122
122
|
expect(adapter.getConversation("/not-an-opencode-ref")).toEqual([]);
|
|
123
123
|
});
|
|
124
|
+
|
|
125
|
+
it("finds an exact session ID with a targeted database query", async () => {
|
|
126
|
+
const now = Date.now();
|
|
127
|
+
writeDatabase(dbPath, {
|
|
128
|
+
sessions: [
|
|
129
|
+
{ id: "wanted", directory: "/repo", timeCreated: now },
|
|
130
|
+
{ id: "other", directory: "/other", timeCreated: now - 1_000 },
|
|
131
|
+
],
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
await expect(adapter.findSessionsById("wanted")).resolves.toEqual([
|
|
135
|
+
expect.objectContaining({
|
|
136
|
+
type: "opencode",
|
|
137
|
+
sessionId: "wanted",
|
|
138
|
+
sessionFilePath: `${dbPath}::wanted`,
|
|
139
|
+
}),
|
|
140
|
+
]);
|
|
141
|
+
});
|
|
124
142
|
});
|
|
125
143
|
|
|
126
144
|
function writeDatabase(
|
|
@@ -260,6 +260,21 @@ describe("PiAdapter", () => {
|
|
|
260
260
|
]);
|
|
261
261
|
});
|
|
262
262
|
|
|
263
|
+
it("finds an exact session ID from the filename suffix", async () => {
|
|
264
|
+
const wanted = writePiSession("/repo/wanted", [
|
|
265
|
+
{ timestamp: "2026-06-10T08:58:20.754Z", sessionId: "sess-wanted", cwd: "/repo/wanted" },
|
|
266
|
+
{ role: "user", content: "wanted" },
|
|
267
|
+
]);
|
|
268
|
+
writePiSession("/repo/other", [
|
|
269
|
+
{ timestamp: "2026-06-10T08:58:20.754Z", sessionId: "sess-other", cwd: "/repo/other" },
|
|
270
|
+
{ role: "user", content: "other" },
|
|
271
|
+
]);
|
|
272
|
+
|
|
273
|
+
await expect(adapter.findSessionsById("sess-wanted")).resolves.toEqual([
|
|
274
|
+
expect.objectContaining({ sessionId: "sess-wanted", sessionFilePath: wanted }),
|
|
275
|
+
]);
|
|
276
|
+
});
|
|
277
|
+
|
|
263
278
|
function makeProcess(overrides: Partial<ProcessInfo>): ProcessInfo {
|
|
264
279
|
return {
|
|
265
280
|
pid: 1,
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
import type { MockedFunction } from "vitest";
|
|
6
6
|
|
|
7
7
|
import * as fs from "fs";
|
|
8
|
-
import { batchGetSessionFileBirthtimes } from "../../utils/session.js";
|
|
8
|
+
import { batchGetSessionFileBirthtimes, isSafePathSegment } from "../../utils/session.js";
|
|
9
9
|
|
|
10
10
|
vi.mock("fs", () => ({
|
|
11
11
|
readdirSync: vi.fn(),
|
|
@@ -15,6 +15,16 @@ vi.mock("fs", () => ({
|
|
|
15
15
|
const mockedReaddirSync = fs.readdirSync as MockedFunction<typeof fs.readdirSync>;
|
|
16
16
|
const mockedStatSync = fs.statSync as MockedFunction<typeof fs.statSync>;
|
|
17
17
|
|
|
18
|
+
describe("isSafePathSegment", () => {
|
|
19
|
+
it.each(["", ".", "..", "nested/session", "bad\0id"])("rejects unsafe segment %j", (value) => {
|
|
20
|
+
expect(isSafePathSegment(value)).toBe(false);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it.each(["session-id", ".hidden-session"])("accepts safe segment %j", (value) => {
|
|
24
|
+
expect(isSafePathSegment(value)).toBe(true);
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
|
|
18
28
|
describe("batchGetSessionFileBirthtimes", () => {
|
|
19
29
|
beforeEach(() => {
|
|
20
30
|
mockedReaddirSync.mockReset();
|
|
@@ -208,4 +208,13 @@ export interface AgentAdapter {
|
|
|
208
208
|
* @returns Array of sessions discovered on disk
|
|
209
209
|
*/
|
|
210
210
|
listSessions(opts?: ListSessionsOptions): Promise<SessionSummary[]>;
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Resolve exact historical session matches without enumerating every summary.
|
|
214
|
+
*
|
|
215
|
+
* Built-in adapters implement this using their provider-native storage shape.
|
|
216
|
+
* The method remains optional so external adapters can fall back to
|
|
217
|
+
* {@link listSessions} until they adopt direct lookup.
|
|
218
|
+
*/
|
|
219
|
+
findSessionsById?(sessionId: string): Promise<SessionSummary[]>;
|
|
211
220
|
}
|
package/src/adapters/index.ts
CHANGED
|
@@ -2,7 +2,7 @@ export { ClaudeCodeAdapter } from "../providers/claude/ClaudeCodeAdapter.js";
|
|
|
2
2
|
export { CodexAdapter } from "../providers/codex/CodexAdapter.js";
|
|
3
3
|
export { CopilotAdapter } from "../providers/copilot/CopilotAdapter.js";
|
|
4
4
|
export { GeminiCliAdapter } from "../providers/gemini/GeminiCliAdapter.js";
|
|
5
|
-
export { GrokCliAdapter } from "
|
|
5
|
+
export { GrokCliAdapter } from "../providers/grok/GrokCliAdapter.js";
|
|
6
6
|
export { OpenCodeAdapter } from "../providers/opencode/OpenCodeAdapter.js";
|
|
7
7
|
export { PiAdapter } from "../providers/pi/PiAdapter.js";
|
|
8
8
|
export { AgentStatus } from "./AgentAdapter.js";
|
package/src/index.ts
CHANGED
|
@@ -29,7 +29,7 @@ export { ClaudeCodeAdapter } from "./providers/claude/ClaudeCodeAdapter.js";
|
|
|
29
29
|
export { CodexAdapter } from "./providers/codex/CodexAdapter.js";
|
|
30
30
|
export { CopilotAdapter } from "./providers/copilot/CopilotAdapter.js";
|
|
31
31
|
export { GeminiCliAdapter } from "./providers/gemini/GeminiCliAdapter.js";
|
|
32
|
-
export { GrokCliAdapter } from "./
|
|
32
|
+
export { GrokCliAdapter } from "./providers/grok/GrokCliAdapter.js";
|
|
33
33
|
export { OpenCodeAdapter } from "./providers/opencode/OpenCodeAdapter.js";
|
|
34
34
|
export { PiAdapter } from "./providers/pi/PiAdapter.js";
|
|
35
35
|
export { AgentStatus } from "./adapters/AgentAdapter.js";
|
|
@@ -138,31 +138,36 @@ export class ClaudeCodeAdapter implements AgentAdapter {
|
|
|
138
138
|
const summaries: SessionSummary[] = [];
|
|
139
139
|
|
|
140
140
|
for (const { filePath, defaultCwd } of candidates) {
|
|
141
|
-
const
|
|
142
|
-
if (!
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
// readSession is permissive (returns a shell record even when every
|
|
146
|
-
// line fails to parse); listSessions needs at least one real entry
|
|
147
|
-
// so we don't surface garbage files.
|
|
148
|
-
if (!session.lastEntryType) continue;
|
|
149
|
-
|
|
150
|
-
const recordedCwd = session.lastCwd || defaultCwd;
|
|
151
|
-
if (filterCwd !== undefined && recordedCwd !== filterCwd) continue;
|
|
152
|
-
|
|
153
|
-
const stat = safeStat(filePath);
|
|
154
|
-
|
|
155
|
-
summaries.push({
|
|
156
|
-
type: "claude",
|
|
157
|
-
sessionId: session.sessionId,
|
|
158
|
-
cwd: recordedCwd,
|
|
159
|
-
firstUserMessage: session.firstUserMessage || "",
|
|
160
|
-
lastActive: session.lastActive ?? stat?.mtime ?? new Date(),
|
|
161
|
-
startedAt: session.sessionStart ?? stat?.birthtime ?? stat?.mtime ?? new Date(),
|
|
162
|
-
sessionFilePath: filePath,
|
|
163
|
-
});
|
|
141
|
+
const summary = this.toSessionSummary(filePath, defaultCwd);
|
|
142
|
+
if (!summary) continue;
|
|
143
|
+
if (filterCwd !== undefined && summary.cwd !== filterCwd) continue;
|
|
144
|
+
summaries.push(summary);
|
|
164
145
|
}
|
|
165
146
|
|
|
166
147
|
return summaries;
|
|
167
148
|
}
|
|
149
|
+
|
|
150
|
+
async findSessionsById(sessionId: string): Promise<SessionSummary[]> {
|
|
151
|
+
return this.createLocator()
|
|
152
|
+
.findHistoricalSessionFilesById(sessionId)
|
|
153
|
+
.map(({ filePath, defaultCwd }) => this.toSessionSummary(filePath, defaultCwd))
|
|
154
|
+
.filter((summary): summary is SessionSummary => summary?.sessionId === sessionId);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
private toSessionSummary(filePath: string, defaultCwd: string): SessionSummary | null {
|
|
158
|
+
const session = this.parser.readSession(filePath, defaultCwd);
|
|
159
|
+
if (!session?.lastEntryType) return null;
|
|
160
|
+
|
|
161
|
+
const recordedCwd = session.lastCwd || defaultCwd;
|
|
162
|
+
const stat = safeStat(filePath);
|
|
163
|
+
return {
|
|
164
|
+
type: "claude",
|
|
165
|
+
sessionId: session.sessionId,
|
|
166
|
+
cwd: recordedCwd,
|
|
167
|
+
firstUserMessage: session.firstUserMessage || "",
|
|
168
|
+
lastActive: session.lastActive ?? stat?.mtime ?? new Date(),
|
|
169
|
+
startedAt: session.sessionStart ?? stat?.birthtime ?? stat?.mtime ?? new Date(),
|
|
170
|
+
sessionFilePath: filePath,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
168
173
|
}
|