@jingyi0605/codingns 0.1.4 → 0.2.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/public/assets/{TerminalPage-4ulgBhv9.js → TerminalPage-BlbQuWi1.js} +1 -1
- package/dist/public/assets/gemini-D4G1NbrE.png +0 -0
- package/dist/public/assets/index-1VIm8lVL.css +1 -0
- package/dist/public/assets/index-Dti93O2S.js +109 -0
- package/dist/public/assets/kimi-BWNNSh7e.png +0 -0
- package/dist/public/index.html +2 -2
- package/dist/server/config/env.d.ts +7 -0
- package/dist/server/config/env.js +150 -1
- package/dist/server/config/env.js.map +1 -1
- package/dist/server/config/opencode-system-probe-helper-process.d.ts +24 -0
- package/dist/server/config/opencode-system-probe-helper-process.js +70 -5
- package/dist/server/config/opencode-system-probe-helper-process.js.map +1 -1
- package/dist/server/modules/butler/butler-action-context-service.d.ts +30 -0
- package/dist/server/modules/butler/butler-action-context-service.js +108 -0
- package/dist/server/modules/butler/butler-action-context-service.js.map +1 -0
- package/dist/server/modules/butler/butler-auth-service.d.ts +17 -0
- package/dist/server/modules/butler/butler-auth-service.js +91 -0
- package/dist/server/modules/butler/butler-auth-service.js.map +1 -0
- package/dist/server/modules/butler/butler-control-action-service.d.ts +65 -0
- package/dist/server/modules/butler/butler-control-action-service.js +296 -0
- package/dist/server/modules/butler/butler-control-action-service.js.map +1 -0
- package/dist/server/modules/butler/butler-control-session-service.d.ts +55 -0
- package/dist/server/modules/butler/butler-control-session-service.js +367 -0
- package/dist/server/modules/butler/butler-control-session-service.js.map +1 -0
- package/dist/server/modules/butler/butler-controller.d.ts +367 -0
- package/dist/server/modules/butler/butler-controller.js +475 -0
- package/dist/server/modules/butler/butler-controller.js.map +1 -0
- package/dist/server/modules/butler/butler-follow-up-evaluation-instruction-adapter.d.ts +34 -0
- package/dist/server/modules/butler/butler-follow-up-evaluation-instruction-adapter.js +77 -0
- package/dist/server/modules/butler/butler-follow-up-evaluation-instruction-adapter.js.map +1 -0
- package/dist/server/modules/butler/butler-follow-up-scheduler.d.ts +23 -0
- package/dist/server/modules/butler/butler-follow-up-scheduler.js +57 -0
- package/dist/server/modules/butler/butler-follow-up-scheduler.js.map +1 -0
- package/dist/server/modules/butler/butler-follow-up-service.d.ts +86 -0
- package/dist/server/modules/butler/butler-follow-up-service.js +948 -0
- package/dist/server/modules/butler/butler-follow-up-service.js.map +1 -0
- package/dist/server/modules/butler/butler-inbox-service.d.ts +35 -0
- package/dist/server/modules/butler/butler-inbox-service.js +136 -0
- package/dist/server/modules/butler/butler-inbox-service.js.map +1 -0
- package/dist/server/modules/butler/butler-notification-service.d.ts +12 -0
- package/dist/server/modules/butler/butler-notification-service.js +45 -0
- package/dist/server/modules/butler/butler-notification-service.js.map +1 -0
- package/dist/server/modules/butler/butler-profile-service.d.ts +26 -0
- package/dist/server/modules/butler/butler-profile-service.js +529 -0
- package/dist/server/modules/butler/butler-profile-service.js.map +1 -0
- package/dist/server/modules/butler/butler-project-service.d.ts +48 -0
- package/dist/server/modules/butler/butler-project-service.js +253 -0
- package/dist/server/modules/butler/butler-project-service.js.map +1 -0
- package/dist/server/modules/butler/butler-session-service.d.ts +79 -0
- package/dist/server/modules/butler/butler-session-service.js +503 -0
- package/dist/server/modules/butler/butler-session-service.js.map +1 -0
- package/dist/server/modules/butler/butler-session-summary-service.d.ts +55 -0
- package/dist/server/modules/butler/butler-session-summary-service.js +382 -0
- package/dist/server/modules/butler/butler-session-summary-service.js.map +1 -0
- package/dist/server/modules/butler/context-aggregator.d.ts +187 -0
- package/dist/server/modules/butler/context-aggregator.js +807 -0
- package/dist/server/modules/butler/context-aggregator.js.map +1 -0
- package/dist/server/modules/butler/instruction-adapter.d.ts +28 -0
- package/dist/server/modules/butler/instruction-adapter.js +101 -0
- package/dist/server/modules/butler/instruction-adapter.js.map +1 -0
- package/dist/server/modules/butler/patrol-execution-service.d.ts +47 -0
- package/dist/server/modules/butler/patrol-execution-service.js +347 -0
- package/dist/server/modules/butler/patrol-execution-service.js.map +1 -0
- package/dist/server/modules/butler/patrol-plan-service.d.ts +54 -0
- package/dist/server/modules/butler/patrol-plan-service.js +272 -0
- package/dist/server/modules/butler/patrol-plan-service.js.map +1 -0
- package/dist/server/modules/butler/patrol-run-service.d.ts +60 -0
- package/dist/server/modules/butler/patrol-run-service.js +185 -0
- package/dist/server/modules/butler/patrol-run-service.js.map +1 -0
- package/dist/server/modules/butler/patrol-scheduler.d.ts +36 -0
- package/dist/server/modules/butler/patrol-scheduler.js +99 -0
- package/dist/server/modules/butler/patrol-scheduler.js.map +1 -0
- package/dist/server/modules/butler/project-memory-service.d.ts +30 -0
- package/dist/server/modules/butler/project-memory-service.js +103 -0
- package/dist/server/modules/butler/project-memory-service.js.map +1 -0
- package/dist/server/modules/butler/provider-adapter-registry.d.ts +61 -0
- package/dist/server/modules/butler/provider-adapter-registry.js +430 -0
- package/dist/server/modules/butler/provider-adapter-registry.js.map +1 -0
- package/dist/server/modules/butler/session-summary-instruction-adapter.d.ts +28 -0
- package/dist/server/modules/butler/session-summary-instruction-adapter.js +79 -0
- package/dist/server/modules/butler/session-summary-instruction-adapter.js.map +1 -0
- package/dist/server/modules/butler/session-summary-scheduler.d.ts +23 -0
- package/dist/server/modules/butler/session-summary-scheduler.js +57 -0
- package/dist/server/modules/butler/session-summary-scheduler.js.map +1 -0
- package/dist/server/modules/butler/verification-run-service.d.ts +73 -0
- package/dist/server/modules/butler/verification-run-service.js +633 -0
- package/dist/server/modules/butler/verification-run-service.js.map +1 -0
- package/dist/server/modules/file/file-controller.d.ts +12 -1
- package/dist/server/modules/file/file-controller.js +72 -1
- package/dist/server/modules/file/file-controller.js.map +1 -1
- package/dist/server/modules/file/file-preview-link-service.d.ts +22 -0
- package/dist/server/modules/file/file-preview-link-service.js +160 -0
- package/dist/server/modules/file/file-preview-link-service.js.map +1 -0
- package/dist/server/modules/preferences/profile-service.js +8 -2
- package/dist/server/modules/preferences/profile-service.js.map +1 -1
- package/dist/server/modules/sessions/claude-runtime-helper-process.js +1 -1
- package/dist/server/modules/sessions/claude-runtime-helper-process.js.map +1 -1
- package/dist/server/modules/sessions/codex-app-server-helper-client.d.ts +7 -2
- package/dist/server/modules/sessions/codex-app-server-helper-client.js +113 -2
- package/dist/server/modules/sessions/codex-app-server-helper-client.js.map +1 -1
- package/dist/server/modules/sessions/codex-app-server-helper-process.js +106 -1
- package/dist/server/modules/sessions/codex-app-server-helper-process.js.map +1 -1
- package/dist/server/modules/sessions/session-controller.d.ts +24 -1
- package/dist/server/modules/sessions/session-controller.js +34 -3
- package/dist/server/modules/sessions/session-controller.js.map +1 -1
- package/dist/server/modules/sessions/session-history-service.d.ts +47 -2
- package/dist/server/modules/sessions/session-history-service.js +881 -56
- package/dist/server/modules/sessions/session-history-service.js.map +1 -1
- package/dist/server/modules/sessions/session-live-runtime-service.d.ts +30 -2
- package/dist/server/modules/sessions/session-live-runtime-service.js +584 -159
- package/dist/server/modules/sessions/session-live-runtime-service.js.map +1 -1
- package/dist/server/modules/sessions/session-provider-error-mapper.js +94 -0
- package/dist/server/modules/sessions/session-provider-error-mapper.js.map +1 -1
- package/dist/server/modules/workbench/workbench-service.d.ts +7 -1
- package/dist/server/modules/workbench/workbench-service.js +31 -7
- package/dist/server/modules/workbench/workbench-service.js.map +1 -1
- package/dist/server/routes/butler.d.ts +3 -0
- package/dist/server/routes/butler.js +54 -0
- package/dist/server/routes/butler.js.map +1 -0
- package/dist/server/routes/files.js +2 -0
- package/dist/server/routes/files.js.map +1 -1
- package/dist/server/routes/sessions.js +1 -0
- package/dist/server/routes/sessions.js.map +1 -1
- package/dist/server/server/create-server.d.ts +65 -0
- package/dist/server/server/create-server.js +154 -5
- package/dist/server/server/create-server.js.map +1 -1
- package/dist/server/shared/utils/command-availability.d.ts +1 -0
- package/dist/server/shared/utils/command-availability.js +83 -0
- package/dist/server/shared/utils/command-availability.js.map +1 -0
- package/dist/server/storage/repositories/butler-control-event-repository.d.ts +8 -0
- package/dist/server/storage/repositories/butler-control-event-repository.js +78 -0
- package/dist/server/storage/repositories/butler-control-event-repository.js.map +1 -0
- package/dist/server/storage/repositories/butler-control-session-repository.d.ts +11 -0
- package/dist/server/storage/repositories/butler-control-session-repository.js +86 -0
- package/dist/server/storage/repositories/butler-control-session-repository.js.map +1 -0
- package/dist/server/storage/repositories/butler-follow-up-task-repository.d.ts +16 -0
- package/dist/server/storage/repositories/butler-follow-up-task-repository.js +252 -0
- package/dist/server/storage/repositories/butler-follow-up-task-repository.js.map +1 -0
- package/dist/server/storage/repositories/butler-inbox-item-repository.d.ts +15 -0
- package/dist/server/storage/repositories/butler-inbox-item-repository.js +111 -0
- package/dist/server/storage/repositories/butler-inbox-item-repository.js.map +1 -0
- package/dist/server/storage/repositories/butler-notification-archive-repository.d.ts +9 -0
- package/dist/server/storage/repositories/butler-notification-archive-repository.js +48 -0
- package/dist/server/storage/repositories/butler-notification-archive-repository.js.map +1 -0
- package/dist/server/storage/repositories/butler-profile-repository.d.ts +9 -0
- package/dist/server/storage/repositories/butler-profile-repository.js +86 -0
- package/dist/server/storage/repositories/butler-profile-repository.js.map +1 -0
- package/dist/server/storage/repositories/butler-project-repository.d.ts +14 -0
- package/dist/server/storage/repositories/butler-project-repository.js +140 -0
- package/dist/server/storage/repositories/butler-project-repository.js.map +1 -0
- package/dist/server/storage/repositories/butler-session-repository.d.ts +11 -0
- package/dist/server/storage/repositories/butler-session-repository.js +106 -0
- package/dist/server/storage/repositories/butler-session-repository.js.map +1 -0
- package/dist/server/storage/repositories/butler-session-summary-state-repository.d.ts +8 -0
- package/dist/server/storage/repositories/butler-session-summary-state-repository.js +62 -0
- package/dist/server/storage/repositories/butler-session-summary-state-repository.js.map +1 -0
- package/dist/server/storage/repositories/patrol-plan-repository.d.ts +27 -0
- package/dist/server/storage/repositories/patrol-plan-repository.js +119 -0
- package/dist/server/storage/repositories/patrol-plan-repository.js.map +1 -0
- package/dist/server/storage/repositories/patrol-run-repository.d.ts +28 -0
- package/dist/server/storage/repositories/patrol-run-repository.js +121 -0
- package/dist/server/storage/repositories/patrol-run-repository.js.map +1 -0
- package/dist/server/storage/repositories/project-memory-repository.d.ts +15 -0
- package/dist/server/storage/repositories/project-memory-repository.js +150 -0
- package/dist/server/storage/repositories/project-memory-repository.js.map +1 -0
- package/dist/server/storage/repositories/session-checkpoint-repository.d.ts +9 -0
- package/dist/server/storage/repositories/session-checkpoint-repository.js +72 -0
- package/dist/server/storage/repositories/session-checkpoint-repository.js.map +1 -0
- package/dist/server/storage/repositories/session-fork-repository.d.ts +8 -0
- package/dist/server/storage/repositories/session-fork-repository.js +69 -0
- package/dist/server/storage/repositories/session-fork-repository.js.map +1 -0
- package/dist/server/storage/repositories/session-index-repository.js +40 -2
- package/dist/server/storage/repositories/session-index-repository.js.map +1 -1
- package/dist/server/storage/repositories/session-message-origin-repository.d.ts +10 -0
- package/dist/server/storage/repositories/session-message-origin-repository.js +93 -0
- package/dist/server/storage/repositories/session-message-origin-repository.js.map +1 -0
- package/dist/server/storage/repositories/verification-run-repository.d.ts +29 -0
- package/dist/server/storage/repositories/verification-run-repository.js +125 -0
- package/dist/server/storage/repositories/verification-run-repository.js.map +1 -0
- package/dist/server/storage/sqlite/client.js +146 -0
- package/dist/server/storage/sqlite/client.js.map +1 -1
- package/dist/server/storage/sqlite/schema.sql +354 -0
- package/dist/server/types/domain.d.ts +286 -2
- package/dist/server/ws/ws-server.d.ts +2 -1
- package/dist/server/ws/ws-server.js +2 -1
- package/dist/server/ws/ws-server.js.map +1 -1
- package/node_modules/@codingns/session-sync-core/dist/index.d.ts +4 -0
- package/node_modules/@codingns/session-sync-core/dist/index.js +4 -0
- package/node_modules/@codingns/session-sync-core/dist/index.js.map +1 -1
- package/node_modules/@codingns/session-sync-core/dist/kimi-message-normalizer.d.ts +18 -0
- package/node_modules/@codingns/session-sync-core/dist/kimi-message-normalizer.js +659 -0
- package/node_modules/@codingns/session-sync-core/dist/kimi-message-normalizer.js.map +1 -0
- package/node_modules/@codingns/session-sync-core/dist/kimi-shared.d.ts +11 -0
- package/node_modules/@codingns/session-sync-core/dist/kimi-shared.js +72 -0
- package/node_modules/@codingns/session-sync-core/dist/kimi-shared.js.map +1 -0
- package/node_modules/@codingns/session-sync-core/dist/patch-builder.d.ts +8 -0
- package/node_modules/@codingns/session-sync-core/dist/patch-builder.js +89 -0
- package/node_modules/@codingns/session-sync-core/dist/patch-builder.js.map +1 -1
- package/node_modules/@codingns/session-sync-core/dist/providers/claude-code.d.ts +6 -1
- package/node_modules/@codingns/session-sync-core/dist/providers/claude-code.js +228 -7
- package/node_modules/@codingns/session-sync-core/dist/providers/claude-code.js.map +1 -1
- package/node_modules/@codingns/session-sync-core/dist/providers/codex.d.ts +26 -1
- package/node_modules/@codingns/session-sync-core/dist/providers/codex.js +499 -3
- package/node_modules/@codingns/session-sync-core/dist/providers/codex.js.map +1 -1
- package/node_modules/@codingns/session-sync-core/dist/providers/gemini.d.ts +41 -0
- package/node_modules/@codingns/session-sync-core/dist/providers/gemini.js +1175 -0
- package/node_modules/@codingns/session-sync-core/dist/providers/gemini.js.map +1 -0
- package/node_modules/@codingns/session-sync-core/dist/providers/kimi.d.ts +29 -0
- package/node_modules/@codingns/session-sync-core/dist/providers/kimi.js +578 -0
- package/node_modules/@codingns/session-sync-core/dist/providers/kimi.js.map +1 -0
- package/node_modules/@codingns/session-sync-core/dist/providers/opencode.d.ts +5 -1
- package/node_modules/@codingns/session-sync-core/dist/providers/opencode.js +271 -4
- package/node_modules/@codingns/session-sync-core/dist/providers/opencode.js.map +1 -1
- package/node_modules/@codingns/session-sync-core/dist/providers/utils.d.ts +1 -0
- package/node_modules/@codingns/session-sync-core/dist/providers/utils.js +147 -19
- package/node_modules/@codingns/session-sync-core/dist/providers/utils.js.map +1 -1
- package/node_modules/@codingns/session-sync-core/dist/runtime/active-run-registry.d.ts +2 -0
- package/node_modules/@codingns/session-sync-core/dist/runtime/active-run-registry.js +43 -5
- package/node_modules/@codingns/session-sync-core/dist/runtime/active-run-registry.js.map +1 -1
- package/node_modules/@codingns/session-sync-core/dist/runtime/codex-runtime.d.ts +12 -0
- package/node_modules/@codingns/session-sync-core/dist/runtime/codex-runtime.js +442 -71
- package/node_modules/@codingns/session-sync-core/dist/runtime/codex-runtime.js.map +1 -1
- package/node_modules/@codingns/session-sync-core/dist/runtime/gemini-runtime.d.ts +21 -0
- package/node_modules/@codingns/session-sync-core/dist/runtime/gemini-runtime.js +537 -0
- package/node_modules/@codingns/session-sync-core/dist/runtime/gemini-runtime.js.map +1 -0
- package/node_modules/@codingns/session-sync-core/dist/runtime/kimi-runtime.d.ts +38 -0
- package/node_modules/@codingns/session-sync-core/dist/runtime/kimi-runtime.js +911 -0
- package/node_modules/@codingns/session-sync-core/dist/runtime/kimi-runtime.js.map +1 -0
- package/node_modules/@codingns/session-sync-core/dist/services.d.ts +2 -1
- package/node_modules/@codingns/session-sync-core/dist/services.js +55 -8
- package/node_modules/@codingns/session-sync-core/dist/services.js.map +1 -1
- package/node_modules/@codingns/session-sync-core/dist/sqlite/node-sqlite.d.ts +6 -0
- package/node_modules/@codingns/session-sync-core/dist/sqlite/node-sqlite.js +9 -0
- package/node_modules/@codingns/session-sync-core/dist/sqlite/node-sqlite.js.map +1 -0
- package/node_modules/@codingns/session-sync-core/dist/types.d.ts +27 -0
- package/node_modules/@codingns/session-sync-core/package.json +8 -0
- package/package.json +1 -1
- package/dist/public/assets/index-C5lu52cQ.css +0 -1
- package/dist/public/assets/index-WpdUo_Vs.js +0 -108
|
@@ -0,0 +1,1175 @@
|
|
|
1
|
+
import { execFile as nodeExecFile } from "node:child_process";
|
|
2
|
+
import { accessSync, constants, existsSync, readdirSync, readFileSync, statSync } from "node:fs";
|
|
3
|
+
import { basename, delimiter, dirname, extname, join } from "node:path";
|
|
4
|
+
import { promisify } from "node:util";
|
|
5
|
+
import { buildApplyPatchFromStructuredFileTool } from "../patch-builder.js";
|
|
6
|
+
import { ensureText, extractTextBlocks, messageIdFromRawRef, nextTimestamp, normalizeWorkspacePath, sliceHistory, stringifyStructuredValue } from "./utils.js";
|
|
7
|
+
const execFile = promisify(nodeExecFile);
|
|
8
|
+
const GEMINI_RAW_STORE_PREFIX = "gemini://session/";
|
|
9
|
+
const DEFAULT_GEMINI_TITLE_LENGTH = 48;
|
|
10
|
+
export class GeminiAdapter {
|
|
11
|
+
options;
|
|
12
|
+
providerId = "gemini";
|
|
13
|
+
constructor(options) {
|
|
14
|
+
this.options = options;
|
|
15
|
+
}
|
|
16
|
+
async detectSessions(workspacePath, options) {
|
|
17
|
+
const discovery = await this.detectSessionsDetailed(workspacePath, options);
|
|
18
|
+
return discovery.sessions;
|
|
19
|
+
}
|
|
20
|
+
async detectSessionsDetailed(workspacePath, options) {
|
|
21
|
+
const targetPath = normalizeWorkspacePath(workspacePath);
|
|
22
|
+
const knownSessions = (options?.knownSessions ?? []).filter((session) => session.provider === this.providerId);
|
|
23
|
+
const knownByProviderSessionId = new Map(knownSessions.map((session) => [session.providerSessionId, session]));
|
|
24
|
+
const localSessions = this.readLocalSessions();
|
|
25
|
+
const cliResult = await this.readCliSessions(workspacePath);
|
|
26
|
+
const mergedByProviderSessionId = new Map();
|
|
27
|
+
for (const localSession of localSessions) {
|
|
28
|
+
const known = knownByProviderSessionId.get(localSession.providerSessionId);
|
|
29
|
+
const resolvedWorkspacePath = localSession.workspacePath || ensureNonEmptyText(known?.workspacePath);
|
|
30
|
+
if (!this.matchesWorkspace(resolvedWorkspacePath, targetPath)) {
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
mergedByProviderSessionId.set(localSession.providerSessionId, {
|
|
34
|
+
provider: this.providerId,
|
|
35
|
+
providerSessionId: localSession.providerSessionId,
|
|
36
|
+
title: localSession.title,
|
|
37
|
+
workspacePath: resolvedWorkspacePath || workspacePath,
|
|
38
|
+
rawStoreRef: buildGeminiRawStoreRef(localSession.providerSessionId),
|
|
39
|
+
lastMessageAt: localSession.lastMessageAt ?? known?.lastMessageAt ?? null,
|
|
40
|
+
messageCount: localSession.messageCount,
|
|
41
|
+
sourceMtimeMs: localSession.sourceMtimeMs,
|
|
42
|
+
sourceSizeBytes: localSession.sourceSizeBytes
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
for (const cliSession of cliResult.sessions) {
|
|
46
|
+
const known = knownByProviderSessionId.get(cliSession.providerSessionId);
|
|
47
|
+
const local = mergedByProviderSessionId.get(cliSession.providerSessionId);
|
|
48
|
+
const resolvedWorkspacePath = cliSession.workspacePath ||
|
|
49
|
+
local?.workspacePath ||
|
|
50
|
+
ensureNonEmptyText(known?.workspacePath);
|
|
51
|
+
if (!this.matchesWorkspace(resolvedWorkspacePath, targetPath)) {
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
mergedByProviderSessionId.set(cliSession.providerSessionId, {
|
|
55
|
+
provider: this.providerId,
|
|
56
|
+
providerSessionId: cliSession.providerSessionId,
|
|
57
|
+
title: local?.title ||
|
|
58
|
+
cliSession.title ||
|
|
59
|
+
known?.title ||
|
|
60
|
+
cliSession.providerSessionId,
|
|
61
|
+
workspacePath: resolvedWorkspacePath || workspacePath,
|
|
62
|
+
rawStoreRef: buildGeminiRawStoreRef(cliSession.providerSessionId),
|
|
63
|
+
lastMessageAt: local?.lastMessageAt ??
|
|
64
|
+
cliSession.lastMessageAt ??
|
|
65
|
+
known?.lastMessageAt ??
|
|
66
|
+
null,
|
|
67
|
+
messageCount: local?.messageCount ??
|
|
68
|
+
cliSession.messageCount ??
|
|
69
|
+
known?.messageCount ??
|
|
70
|
+
0,
|
|
71
|
+
sourceMtimeMs: local?.sourceMtimeMs ?? known?.sourceMtimeMs,
|
|
72
|
+
sourceSizeBytes: local?.sourceSizeBytes ?? known?.sourceSizeBytes
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
// 当 CLI 临时失败时,用 knownSessions 补回最近一次已发现的会话,避免列表突然丢失。
|
|
76
|
+
if (!cliResult.isComplete) {
|
|
77
|
+
for (const known of knownSessions) {
|
|
78
|
+
if (mergedByProviderSessionId.has(known.providerSessionId)) {
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
if (!this.matchesWorkspace(known.workspacePath, targetPath)) {
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
mergedByProviderSessionId.set(known.providerSessionId, known);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return {
|
|
88
|
+
sessions: [...mergedByProviderSessionId.values()].sort((left, right) => (right.lastMessageAt ?? "").localeCompare(left.lastMessageAt ?? "")),
|
|
89
|
+
isComplete: cliResult.isComplete
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
async readSessionHistory(providerSessionId, rawStoreRef, cursor, limit, direction = "forward") {
|
|
93
|
+
const resolvedProviderSessionId = this.resolveProviderSessionId(providerSessionId, rawStoreRef);
|
|
94
|
+
const parsedChat = this.readParsedChatBySessionId(resolvedProviderSessionId);
|
|
95
|
+
return sliceHistory(parsedChat.messages, cursor, limit, direction);
|
|
96
|
+
}
|
|
97
|
+
subscribeSession(providerSessionId, rawStoreRef, cursor, limit, onEvent) {
|
|
98
|
+
const sessionRef = this.resolveSessionRef(providerSessionId, rawStoreRef);
|
|
99
|
+
let currentCursor = cursor;
|
|
100
|
+
let lastSeenSignature = "";
|
|
101
|
+
let closed = false;
|
|
102
|
+
const timer = setInterval(() => {
|
|
103
|
+
if (closed || !sessionRef.providerSessionId) {
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
let page;
|
|
107
|
+
try {
|
|
108
|
+
page = this.readSessionHistorySync(sessionRef.providerSessionId, rawStoreRef, currentCursor, limit);
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
if (page.messages.length === 0) {
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
const signature = `${page.messages.at(-1)?.messageId ?? ""}:${page.cursor ?? ""}`;
|
|
117
|
+
if (signature === lastSeenSignature) {
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
lastSeenSignature = signature;
|
|
121
|
+
currentCursor = page.cursor;
|
|
122
|
+
void onEvent({
|
|
123
|
+
messages: page.messages,
|
|
124
|
+
cursor: page.cursor
|
|
125
|
+
});
|
|
126
|
+
}, 700);
|
|
127
|
+
return {
|
|
128
|
+
close() {
|
|
129
|
+
closed = true;
|
|
130
|
+
clearInterval(timer);
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
async resumeSession(providerSessionId, rawStoreRef) {
|
|
135
|
+
const resolvedProviderSessionId = this.resolveProviderSessionId(providerSessionId, rawStoreRef);
|
|
136
|
+
this.readParsedChatBySessionId(resolvedProviderSessionId);
|
|
137
|
+
return {
|
|
138
|
+
provider: this.providerId,
|
|
139
|
+
providerSessionId: resolvedProviderSessionId,
|
|
140
|
+
resumedAt: nextTimestamp(),
|
|
141
|
+
rawStoreRef: buildGeminiRawStoreRef(resolvedProviderSessionId)
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
async startSession(_workspacePath, _options) {
|
|
145
|
+
throw new Error("GEMINI_READ_ONLY_PROVIDER");
|
|
146
|
+
}
|
|
147
|
+
async sendMessage(_providerSessionId, _rawStoreRef, _content, _clientRequestId, _permissionMode) {
|
|
148
|
+
throw new Error("GEMINI_READ_ONLY_PROVIDER");
|
|
149
|
+
}
|
|
150
|
+
async readSessionTitle(providerSessionId, rawStoreRef) {
|
|
151
|
+
const resolvedProviderSessionId = this.resolveProviderSessionId(providerSessionId, rawStoreRef);
|
|
152
|
+
const parsedChat = this.readParsedChatBySessionId(resolvedProviderSessionId);
|
|
153
|
+
return parsedChat.title;
|
|
154
|
+
}
|
|
155
|
+
async renameSessionTitle(_providerSessionId, _rawStoreRef, _title) {
|
|
156
|
+
throw new Error("GEMINI_READ_ONLY_PROVIDER");
|
|
157
|
+
}
|
|
158
|
+
async updateSessionArchiveState(_providerSessionId, _rawStoreRef, _isArchived) {
|
|
159
|
+
throw new Error("GEMINI_ARCHIVE_NOT_SUPPORTED");
|
|
160
|
+
}
|
|
161
|
+
getProviderCapabilities() {
|
|
162
|
+
return {
|
|
163
|
+
provider: this.providerId,
|
|
164
|
+
canStartSession: true,
|
|
165
|
+
canResumeSession: true,
|
|
166
|
+
canSendMessage: true,
|
|
167
|
+
inRunInputMode: "none",
|
|
168
|
+
supportsSubagents: false,
|
|
169
|
+
supportsInterrupt: true,
|
|
170
|
+
supportsStructuredToolCalls: true,
|
|
171
|
+
supportsTokenUsage: false,
|
|
172
|
+
supportsAttachments: false,
|
|
173
|
+
supportsPermissionPrompt: false,
|
|
174
|
+
supportsCheckpoint: false,
|
|
175
|
+
limitations: [
|
|
176
|
+
"当前 Gemini 仅接入会话发现与历史只读能力,运行时链路尚未启用",
|
|
177
|
+
"本地 chats schema 属于非稳定公开协议,升级 CLI 后需要通过 fixture 回归"
|
|
178
|
+
]
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
async getSessionCapabilities(_providerSessionId) {
|
|
182
|
+
return this.getProviderCapabilities();
|
|
183
|
+
}
|
|
184
|
+
readSessionHistorySync(providerSessionId, rawStoreRef, cursor, limit, direction = "forward") {
|
|
185
|
+
const resolvedProviderSessionId = this.resolveProviderSessionId(providerSessionId, rawStoreRef);
|
|
186
|
+
const parsedChat = this.readParsedChatBySessionId(resolvedProviderSessionId);
|
|
187
|
+
return sliceHistory(parsedChat.messages, cursor, limit, direction);
|
|
188
|
+
}
|
|
189
|
+
resolveProviderSessionId(providerSessionId, rawStoreRef) {
|
|
190
|
+
const sessionRef = this.resolveSessionRef(providerSessionId, rawStoreRef);
|
|
191
|
+
if (!sessionRef.providerSessionId) {
|
|
192
|
+
throw new Error("PROVIDER_SESSION_ID_REQUIRED");
|
|
193
|
+
}
|
|
194
|
+
return sessionRef.providerSessionId;
|
|
195
|
+
}
|
|
196
|
+
resolveSessionRef(providerSessionId, rawStoreRef) {
|
|
197
|
+
const trimmedProviderSessionId = providerSessionId.trim();
|
|
198
|
+
if (trimmedProviderSessionId) {
|
|
199
|
+
return {
|
|
200
|
+
providerSessionId: trimmedProviderSessionId,
|
|
201
|
+
fromRawStoreRef: false
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
return {
|
|
205
|
+
providerSessionId: parseGeminiRawStoreRef(rawStoreRef),
|
|
206
|
+
fromRawStoreRef: true
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
async readCliSessions(workspacePath) {
|
|
210
|
+
if (this.options.listSessions) {
|
|
211
|
+
try {
|
|
212
|
+
return {
|
|
213
|
+
sessions: await this.options.listSessions(),
|
|
214
|
+
isComplete: true
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
catch {
|
|
218
|
+
return {
|
|
219
|
+
sessions: [],
|
|
220
|
+
isComplete: false
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
const commandPath = this.options.commandPath?.trim() || "gemini";
|
|
225
|
+
// CLI 没装是正常场景,直接回退到本地 chats 扫描,不要把整个工作区打成 partial。
|
|
226
|
+
if (!isCommandAvailable(commandPath)) {
|
|
227
|
+
return {
|
|
228
|
+
sessions: [],
|
|
229
|
+
isComplete: true
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
try {
|
|
233
|
+
const sessions = await this.readCliSessionsFromCommand(workspacePath);
|
|
234
|
+
return {
|
|
235
|
+
sessions,
|
|
236
|
+
isComplete: true
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
catch {
|
|
240
|
+
return {
|
|
241
|
+
sessions: [],
|
|
242
|
+
isComplete: false
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
async readCliSessionsFromCommand(workspacePath) {
|
|
247
|
+
const commandPath = this.options.commandPath?.trim() || "gemini";
|
|
248
|
+
const env = {
|
|
249
|
+
...process.env,
|
|
250
|
+
GEMINI_HOME: this.options.homeDir
|
|
251
|
+
};
|
|
252
|
+
const attempts = [
|
|
253
|
+
["--list-sessions", "--output-format", "json"],
|
|
254
|
+
["--list-sessions"]
|
|
255
|
+
];
|
|
256
|
+
let lastError = null;
|
|
257
|
+
for (const args of attempts) {
|
|
258
|
+
try {
|
|
259
|
+
const result = await execFile(commandPath, args, {
|
|
260
|
+
env,
|
|
261
|
+
cwd: workspacePath,
|
|
262
|
+
timeout: 8_000,
|
|
263
|
+
windowsHide: true,
|
|
264
|
+
shell: shouldUseShellForCommand(commandPath)
|
|
265
|
+
});
|
|
266
|
+
return parseGeminiCliSessionOutput(result.stdout, workspacePath);
|
|
267
|
+
}
|
|
268
|
+
catch (error) {
|
|
269
|
+
lastError = error;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
throw lastError ?? new Error("GEMINI_LIST_SESSIONS_FAILED");
|
|
273
|
+
}
|
|
274
|
+
readLocalSessions() {
|
|
275
|
+
const sessions = [];
|
|
276
|
+
const latestByProviderSessionId = new Map();
|
|
277
|
+
for (const filePath of listGeminiChatFiles(this.options.homeDir)) {
|
|
278
|
+
let parsedChat;
|
|
279
|
+
try {
|
|
280
|
+
parsedChat = this.parseLocalChatFile(filePath);
|
|
281
|
+
}
|
|
282
|
+
catch {
|
|
283
|
+
continue;
|
|
284
|
+
}
|
|
285
|
+
const existing = latestByProviderSessionId.get(parsedChat.providerSessionId);
|
|
286
|
+
if (existing &&
|
|
287
|
+
(existing.sourceMtimeMs > parsedChat.sourceMtimeMs ||
|
|
288
|
+
(existing.sourceMtimeMs === parsedChat.sourceMtimeMs &&
|
|
289
|
+
existing.sourceSizeBytes >= parsedChat.sourceSizeBytes))) {
|
|
290
|
+
continue;
|
|
291
|
+
}
|
|
292
|
+
latestByProviderSessionId.set(parsedChat.providerSessionId, {
|
|
293
|
+
providerSessionId: parsedChat.providerSessionId,
|
|
294
|
+
workspacePath: parsedChat.workspacePath,
|
|
295
|
+
title: parsedChat.title,
|
|
296
|
+
lastMessageAt: parsedChat.lastMessageAt,
|
|
297
|
+
messageCount: parsedChat.messages.length,
|
|
298
|
+
filePath,
|
|
299
|
+
sourceMtimeMs: parsedChat.sourceMtimeMs,
|
|
300
|
+
sourceSizeBytes: parsedChat.sourceSizeBytes
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
sessions.push(...latestByProviderSessionId.values());
|
|
304
|
+
return sessions;
|
|
305
|
+
}
|
|
306
|
+
readParsedChatBySessionId(providerSessionId) {
|
|
307
|
+
const sessionId = providerSessionId.trim();
|
|
308
|
+
if (!sessionId) {
|
|
309
|
+
throw new Error("PROVIDER_SESSION_ID_REQUIRED");
|
|
310
|
+
}
|
|
311
|
+
const chatFiles = listGeminiChatFiles(this.options.homeDir);
|
|
312
|
+
let matchedByName = null;
|
|
313
|
+
for (const filePath of chatFiles) {
|
|
314
|
+
if (basename(filePath, ".json") === sessionId) {
|
|
315
|
+
matchedByName = filePath;
|
|
316
|
+
}
|
|
317
|
+
let parsed;
|
|
318
|
+
try {
|
|
319
|
+
parsed = this.parseLocalChatFile(filePath);
|
|
320
|
+
}
|
|
321
|
+
catch (error) {
|
|
322
|
+
if (basename(filePath, ".json") === sessionId) {
|
|
323
|
+
throw wrapGeminiSchemaError(filePath, error);
|
|
324
|
+
}
|
|
325
|
+
continue;
|
|
326
|
+
}
|
|
327
|
+
if (parsed.providerSessionId === sessionId) {
|
|
328
|
+
return parsed;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
if (matchedByName) {
|
|
332
|
+
try {
|
|
333
|
+
return this.parseLocalChatFile(matchedByName);
|
|
334
|
+
}
|
|
335
|
+
catch (error) {
|
|
336
|
+
throw wrapGeminiSchemaError(matchedByName, error);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
throw new Error("GEMINI_CHAT_NOT_FOUND");
|
|
340
|
+
}
|
|
341
|
+
parseLocalChatFile(filePath) {
|
|
342
|
+
const stats = statSync(filePath);
|
|
343
|
+
const raw = readFileSync(filePath, "utf8").trim();
|
|
344
|
+
if (!raw) {
|
|
345
|
+
throw new Error("GEMINI_CHAT_SCHEMA_INVALID");
|
|
346
|
+
}
|
|
347
|
+
let parsedRaw;
|
|
348
|
+
try {
|
|
349
|
+
parsedRaw = JSON.parse(raw);
|
|
350
|
+
}
|
|
351
|
+
catch (error) {
|
|
352
|
+
throw wrapGeminiSchemaError(filePath, error);
|
|
353
|
+
}
|
|
354
|
+
const parsedRecord = toRecord(parsedRaw);
|
|
355
|
+
const providerSessionId = this.resolveLocalProviderSessionId(parsedRecord, filePath);
|
|
356
|
+
const messageNodes = readMessageNodes(parsedRecord);
|
|
357
|
+
const messages = normalizeMessageNodes({
|
|
358
|
+
sessionId: providerSessionId,
|
|
359
|
+
filePath,
|
|
360
|
+
messageNodes
|
|
361
|
+
});
|
|
362
|
+
const title = resolveStringField(parsedRecord, ["title", "name", "chatTitle"]) ||
|
|
363
|
+
messages.find((message) => message.role === "user")?.content.slice(0, DEFAULT_GEMINI_TITLE_LENGTH) ||
|
|
364
|
+
providerSessionId;
|
|
365
|
+
const workspacePath = resolveWorkspacePath(parsedRecord, messageNodes, filePath);
|
|
366
|
+
const lastMessageAt = messages.at(-1)?.timestamp ||
|
|
367
|
+
resolveStringField(parsedRecord, [
|
|
368
|
+
"updatedAt",
|
|
369
|
+
"updated_at",
|
|
370
|
+
"lastUpdated",
|
|
371
|
+
"last_updated",
|
|
372
|
+
"lastMessageAt",
|
|
373
|
+
"last_message_at",
|
|
374
|
+
"startTime",
|
|
375
|
+
"start_time",
|
|
376
|
+
"createdAt",
|
|
377
|
+
"created_at"
|
|
378
|
+
]) ||
|
|
379
|
+
null;
|
|
380
|
+
return {
|
|
381
|
+
providerSessionId,
|
|
382
|
+
workspacePath,
|
|
383
|
+
title,
|
|
384
|
+
lastMessageAt,
|
|
385
|
+
messages,
|
|
386
|
+
sourceMtimeMs: stats.mtimeMs,
|
|
387
|
+
sourceSizeBytes: stats.size
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
resolveLocalProviderSessionId(record, filePath) {
|
|
391
|
+
const sessionId = resolveStringField(record, [
|
|
392
|
+
"sessionId",
|
|
393
|
+
"session_id",
|
|
394
|
+
"id",
|
|
395
|
+
"chatId",
|
|
396
|
+
"conversationId",
|
|
397
|
+
"conversation_id"
|
|
398
|
+
]) || basename(filePath, ".json");
|
|
399
|
+
if (!sessionId.trim()) {
|
|
400
|
+
throw new Error("GEMINI_CHAT_SCHEMA_INVALID");
|
|
401
|
+
}
|
|
402
|
+
return sessionId.trim();
|
|
403
|
+
}
|
|
404
|
+
matchesWorkspace(workspacePath, targetPath) {
|
|
405
|
+
const normalizedWorkspacePath = normalizeWorkspacePath(workspacePath ?? "");
|
|
406
|
+
if (!normalizedWorkspacePath) {
|
|
407
|
+
return false;
|
|
408
|
+
}
|
|
409
|
+
return normalizedWorkspacePath === targetPath;
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
function parseGeminiRawStoreRef(rawStoreRef) {
|
|
413
|
+
const trimmed = rawStoreRef.trim();
|
|
414
|
+
if (!trimmed.startsWith(GEMINI_RAW_STORE_PREFIX)) {
|
|
415
|
+
return null;
|
|
416
|
+
}
|
|
417
|
+
const rawSessionId = trimmed.slice(GEMINI_RAW_STORE_PREFIX.length).split(/[?#]/, 1)[0];
|
|
418
|
+
return rawSessionId ? decodeURIComponent(rawSessionId) : null;
|
|
419
|
+
}
|
|
420
|
+
function buildGeminiRawStoreRef(providerSessionId) {
|
|
421
|
+
return `${GEMINI_RAW_STORE_PREFIX}${encodeURIComponent(providerSessionId)}`;
|
|
422
|
+
}
|
|
423
|
+
function listGeminiChatFiles(homeDir) {
|
|
424
|
+
const tmpRoot = join(homeDir, "tmp");
|
|
425
|
+
if (!existsSync(tmpRoot)) {
|
|
426
|
+
return [];
|
|
427
|
+
}
|
|
428
|
+
const queue = [tmpRoot];
|
|
429
|
+
const chatFiles = [];
|
|
430
|
+
while (queue.length > 0) {
|
|
431
|
+
const current = queue.shift();
|
|
432
|
+
if (!current) {
|
|
433
|
+
continue;
|
|
434
|
+
}
|
|
435
|
+
const entries = readdirSync(current, { withFileTypes: true });
|
|
436
|
+
for (const entry of entries) {
|
|
437
|
+
const entryPath = join(current, entry.name);
|
|
438
|
+
if (entry.isDirectory()) {
|
|
439
|
+
queue.push(entryPath);
|
|
440
|
+
continue;
|
|
441
|
+
}
|
|
442
|
+
if (!entry.isFile() || !entry.name.endsWith(".json")) {
|
|
443
|
+
continue;
|
|
444
|
+
}
|
|
445
|
+
if (!isGeminiChatFile(entryPath)) {
|
|
446
|
+
continue;
|
|
447
|
+
}
|
|
448
|
+
chatFiles.push(entryPath);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
return chatFiles;
|
|
452
|
+
}
|
|
453
|
+
function isGeminiChatFile(filePath) {
|
|
454
|
+
return filePath.replaceAll("\\", "/").includes("/chats/");
|
|
455
|
+
}
|
|
456
|
+
function parseGeminiCliSessionOutput(stdout, workspacePathFallback = null) {
|
|
457
|
+
const trimmed = stdout.trim();
|
|
458
|
+
if (!trimmed) {
|
|
459
|
+
return [];
|
|
460
|
+
}
|
|
461
|
+
const parsedAsWhole = parseJsonSafe(trimmed);
|
|
462
|
+
const normalizedWhole = normalizeCliSessionsPayload(parsedAsWhole, workspacePathFallback);
|
|
463
|
+
if (normalizedWhole.length > 0) {
|
|
464
|
+
return normalizedWhole;
|
|
465
|
+
}
|
|
466
|
+
const normalizedText = parseGeminiPlainTextSessions(trimmed, workspacePathFallback);
|
|
467
|
+
if (normalizedText.length > 0) {
|
|
468
|
+
return normalizedText;
|
|
469
|
+
}
|
|
470
|
+
const sessions = [];
|
|
471
|
+
for (const line of trimmed.split(/\r?\n/)) {
|
|
472
|
+
const parsedLine = parseJsonSafe(line.trim());
|
|
473
|
+
if (!parsedLine) {
|
|
474
|
+
continue;
|
|
475
|
+
}
|
|
476
|
+
sessions.push(...normalizeCliSessionsPayload(parsedLine, workspacePathFallback));
|
|
477
|
+
}
|
|
478
|
+
return dedupeCliSessions(sessions);
|
|
479
|
+
}
|
|
480
|
+
function normalizeCliSessionsPayload(payload, workspacePathFallback) {
|
|
481
|
+
if (!payload) {
|
|
482
|
+
return [];
|
|
483
|
+
}
|
|
484
|
+
if (Array.isArray(payload)) {
|
|
485
|
+
return dedupeCliSessions(payload
|
|
486
|
+
.map((item) => normalizeCliSessionRecord(item, workspacePathFallback))
|
|
487
|
+
.filter((item) => item !== null));
|
|
488
|
+
}
|
|
489
|
+
const record = toRecord(payload);
|
|
490
|
+
const wrappedArray = arrayFromUnknown(record.sessions) ||
|
|
491
|
+
arrayFromUnknown(record.items) ||
|
|
492
|
+
arrayFromUnknown(record.data);
|
|
493
|
+
if (wrappedArray) {
|
|
494
|
+
return dedupeCliSessions(wrappedArray
|
|
495
|
+
.map((item) => normalizeCliSessionRecord(item, workspacePathFallback))
|
|
496
|
+
.filter((item) => item !== null));
|
|
497
|
+
}
|
|
498
|
+
const single = normalizeCliSessionRecord(record, workspacePathFallback);
|
|
499
|
+
return single ? [single] : [];
|
|
500
|
+
}
|
|
501
|
+
function normalizeCliSessionRecord(payload, workspacePathFallback) {
|
|
502
|
+
const record = toRecord(payload);
|
|
503
|
+
const providerSessionId = resolveStringField(record, [
|
|
504
|
+
"sessionId",
|
|
505
|
+
"session_id",
|
|
506
|
+
"id",
|
|
507
|
+
"conversationId",
|
|
508
|
+
"conversation_id"
|
|
509
|
+
]);
|
|
510
|
+
if (!providerSessionId) {
|
|
511
|
+
return null;
|
|
512
|
+
}
|
|
513
|
+
const messageCountValue = resolveNumberField(record, ["messageCount", "message_count"]);
|
|
514
|
+
return {
|
|
515
|
+
providerSessionId,
|
|
516
|
+
workspacePath: resolveStringField(record, [
|
|
517
|
+
"workspacePath",
|
|
518
|
+
"workspace_path",
|
|
519
|
+
"cwd",
|
|
520
|
+
"directory",
|
|
521
|
+
"projectPath",
|
|
522
|
+
"project_path"
|
|
523
|
+
]) || workspacePathFallback,
|
|
524
|
+
title: resolveStringField(record, ["title", "name", "summary"]) || null,
|
|
525
|
+
lastMessageAt: resolveStringField(record, [
|
|
526
|
+
"updatedAt",
|
|
527
|
+
"updated_at",
|
|
528
|
+
"lastUpdated",
|
|
529
|
+
"last_updated",
|
|
530
|
+
"lastMessageAt",
|
|
531
|
+
"last_message_at",
|
|
532
|
+
"startTime",
|
|
533
|
+
"start_time",
|
|
534
|
+
"createdAt",
|
|
535
|
+
"created_at"
|
|
536
|
+
]) || null,
|
|
537
|
+
messageCount: messageCountValue === null ? null : messageCountValue
|
|
538
|
+
};
|
|
539
|
+
}
|
|
540
|
+
function dedupeCliSessions(sessions) {
|
|
541
|
+
const deduped = new Map();
|
|
542
|
+
for (const session of sessions) {
|
|
543
|
+
const existing = deduped.get(session.providerSessionId);
|
|
544
|
+
if (!existing ||
|
|
545
|
+
(existing.lastMessageAt ?? "").localeCompare(session.lastMessageAt ?? "") < 0) {
|
|
546
|
+
deduped.set(session.providerSessionId, session);
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
return [...deduped.values()];
|
|
550
|
+
}
|
|
551
|
+
function resolveWorkspacePath(record, messageNodes, filePath) {
|
|
552
|
+
const directWorkspace = resolveStringField(record, [
|
|
553
|
+
"workspacePath",
|
|
554
|
+
"workspace_path",
|
|
555
|
+
"cwd",
|
|
556
|
+
"projectPath",
|
|
557
|
+
"project_path"
|
|
558
|
+
]);
|
|
559
|
+
if (directWorkspace) {
|
|
560
|
+
return directWorkspace;
|
|
561
|
+
}
|
|
562
|
+
for (const node of messageNodes) {
|
|
563
|
+
const nodeRecord = toRecord(node);
|
|
564
|
+
const workspace = resolveStringField(nodeRecord, [
|
|
565
|
+
"workspacePath",
|
|
566
|
+
"workspace_path",
|
|
567
|
+
"cwd",
|
|
568
|
+
"projectPath",
|
|
569
|
+
"project_path"
|
|
570
|
+
]);
|
|
571
|
+
if (workspace) {
|
|
572
|
+
return workspace;
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
return resolveWorkspacePathFromChatFile(filePath);
|
|
576
|
+
}
|
|
577
|
+
function readMessageNodes(record) {
|
|
578
|
+
const candidates = [
|
|
579
|
+
record.messages,
|
|
580
|
+
record.history,
|
|
581
|
+
record.events,
|
|
582
|
+
record.contents,
|
|
583
|
+
record.turns,
|
|
584
|
+
toRecord(record.chat).messages,
|
|
585
|
+
toRecord(record.conversation).messages,
|
|
586
|
+
toRecord(record.transcript).messages
|
|
587
|
+
];
|
|
588
|
+
for (const candidate of candidates) {
|
|
589
|
+
const array = arrayFromUnknown(candidate);
|
|
590
|
+
if (array && array.length > 0) {
|
|
591
|
+
return array;
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
return [];
|
|
595
|
+
}
|
|
596
|
+
function normalizeMessageNodes(input) {
|
|
597
|
+
const messages = [];
|
|
598
|
+
let sequence = 0;
|
|
599
|
+
for (let index = 0; index < input.messageNodes.length; index += 1) {
|
|
600
|
+
const node = input.messageNodes[index];
|
|
601
|
+
const nodeRecord = toRecord(node);
|
|
602
|
+
const role = resolveGeminiRole(nodeRecord);
|
|
603
|
+
const timestamp = resolveMessageTimestamp(nodeRecord);
|
|
604
|
+
const descriptors = readMessageDescriptors(nodeRecord, role);
|
|
605
|
+
if (descriptors.length === 0) {
|
|
606
|
+
continue;
|
|
607
|
+
}
|
|
608
|
+
for (let descriptorIndex = 0; descriptorIndex < descriptors.length; descriptorIndex += 1) {
|
|
609
|
+
const descriptor = descriptors[descriptorIndex];
|
|
610
|
+
sequence += 1;
|
|
611
|
+
const rawRef = buildGeminiMessageRawRef(input.sessionId, input.filePath, index, descriptorIndex);
|
|
612
|
+
messages.push({
|
|
613
|
+
messageId: messageIdFromRawRef(rawRef),
|
|
614
|
+
provider: "gemini",
|
|
615
|
+
providerSessionId: input.sessionId,
|
|
616
|
+
role: descriptor.role,
|
|
617
|
+
kind: descriptor.kind,
|
|
618
|
+
content: descriptor.content,
|
|
619
|
+
toolCall: descriptor.toolCall,
|
|
620
|
+
timestamp: descriptor.timestamp ?? timestamp,
|
|
621
|
+
sequence,
|
|
622
|
+
rawRef
|
|
623
|
+
});
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
return messages;
|
|
627
|
+
}
|
|
628
|
+
function readMessageDescriptors(nodeRecord, fallbackRole) {
|
|
629
|
+
const descriptors = [];
|
|
630
|
+
descriptors.push(...readGeminiThoughtDescriptors(nodeRecord));
|
|
631
|
+
descriptors.push(...readGeminiToolCallDescriptors(nodeRecord));
|
|
632
|
+
const parts = arrayFromUnknown(nodeRecord.parts) ||
|
|
633
|
+
arrayFromUnknown(nodeRecord.content) ||
|
|
634
|
+
arrayFromUnknown(toRecord(nodeRecord.message).parts) ||
|
|
635
|
+
null;
|
|
636
|
+
const partDescriptors = [];
|
|
637
|
+
if (parts && parts.length > 0) {
|
|
638
|
+
for (const part of parts) {
|
|
639
|
+
const descriptor = normalizeMessagePart(part, fallbackRole);
|
|
640
|
+
if (descriptor) {
|
|
641
|
+
partDescriptors.push(descriptor);
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
if (partDescriptors.length > 0) {
|
|
646
|
+
descriptors.push(...partDescriptors);
|
|
647
|
+
return descriptors;
|
|
648
|
+
}
|
|
649
|
+
const fallbackDescriptor = normalizeMessagePart(nodeRecord, fallbackRole);
|
|
650
|
+
if (fallbackDescriptor) {
|
|
651
|
+
descriptors.push(fallbackDescriptor);
|
|
652
|
+
}
|
|
653
|
+
return descriptors;
|
|
654
|
+
}
|
|
655
|
+
function normalizeMessagePart(value, fallbackRole) {
|
|
656
|
+
const record = toRecord(value);
|
|
657
|
+
const toolCallPayload = maybeRecord(record.tool_use) ??
|
|
658
|
+
maybeRecord(record.toolUse) ??
|
|
659
|
+
maybeRecord(record.functionCall) ??
|
|
660
|
+
(record.type === "tool_use" || record.type === "function_call" ? record : null);
|
|
661
|
+
if (toolCallPayload) {
|
|
662
|
+
const timestamp = resolveOptionalMessageTimestamp(record);
|
|
663
|
+
const patchText = buildGeminiApplyPatchFromInput(toolCallPayload);
|
|
664
|
+
const callId = resolveStringField(toolCallPayload, ["id", "toolCallId", "call_id"]) || "gemini-call";
|
|
665
|
+
const name = patchText
|
|
666
|
+
? "apply_patch"
|
|
667
|
+
: resolveStringField(toolCallPayload, ["name", "toolName", "tool_name"]) || "unknown_tool";
|
|
668
|
+
const inputPayload = toolCallPayload.input ??
|
|
669
|
+
toolCallPayload.arguments ??
|
|
670
|
+
toolCallPayload.args ??
|
|
671
|
+
null;
|
|
672
|
+
const inputText = patchText || stringifyStructuredValue(inputPayload);
|
|
673
|
+
return {
|
|
674
|
+
role: patchText ? "tool" : "assistant",
|
|
675
|
+
kind: "tool_call",
|
|
676
|
+
content: inputText,
|
|
677
|
+
toolCall: {
|
|
678
|
+
callId,
|
|
679
|
+
name,
|
|
680
|
+
input: inputText,
|
|
681
|
+
output: null,
|
|
682
|
+
error: null,
|
|
683
|
+
status: "running"
|
|
684
|
+
},
|
|
685
|
+
timestamp
|
|
686
|
+
};
|
|
687
|
+
}
|
|
688
|
+
const toolResultPayload = maybeRecord(record.tool_result) ??
|
|
689
|
+
maybeRecord(record.toolResult) ??
|
|
690
|
+
maybeRecord(record.functionResponse) ??
|
|
691
|
+
(record.type === "tool_result" || record.type === "function_response" ? record : null);
|
|
692
|
+
if (toolResultPayload) {
|
|
693
|
+
const timestamp = resolveOptionalMessageTimestamp(record);
|
|
694
|
+
const errorDetail = resolveStringField(toolResultPayload, ["error", "error_message"]);
|
|
695
|
+
const outputPayload = toolResultPayload.output ??
|
|
696
|
+
toolResultPayload.result ??
|
|
697
|
+
toolResultPayload.content ??
|
|
698
|
+
null;
|
|
699
|
+
const normalizedName = resolveStringField(toolResultPayload, [
|
|
700
|
+
"name",
|
|
701
|
+
"tool_name",
|
|
702
|
+
"toolName"
|
|
703
|
+
]) || "unknown_tool";
|
|
704
|
+
const name = isGeminiEditableToolName(normalizedName) ? "apply_patch" : normalizedName;
|
|
705
|
+
return {
|
|
706
|
+
role: "tool",
|
|
707
|
+
kind: "tool_result",
|
|
708
|
+
content: extractTextBlocks(outputPayload).trim() || stringifyStructuredValue(outputPayload),
|
|
709
|
+
toolCall: {
|
|
710
|
+
callId: resolveStringField(toolResultPayload, [
|
|
711
|
+
"tool_use_id",
|
|
712
|
+
"toolUseId",
|
|
713
|
+
"call_id",
|
|
714
|
+
"callId"
|
|
715
|
+
]) || "gemini-tool-result",
|
|
716
|
+
name,
|
|
717
|
+
input: "",
|
|
718
|
+
output: stringifyStructuredValue(outputPayload),
|
|
719
|
+
error: errorDetail,
|
|
720
|
+
status: errorDetail ? "failed" : "completed"
|
|
721
|
+
},
|
|
722
|
+
timestamp
|
|
723
|
+
};
|
|
724
|
+
}
|
|
725
|
+
const text = extractTextBlocks(record.text ??
|
|
726
|
+
record.content ??
|
|
727
|
+
record.output ??
|
|
728
|
+
record.message ??
|
|
729
|
+
value).trim();
|
|
730
|
+
if (!text) {
|
|
731
|
+
return null;
|
|
732
|
+
}
|
|
733
|
+
const partType = ensureText(record.type).toLowerCase();
|
|
734
|
+
const kind = partType.includes("think") ? "thinking" : "text";
|
|
735
|
+
return {
|
|
736
|
+
role: fallbackRole,
|
|
737
|
+
kind,
|
|
738
|
+
content: text,
|
|
739
|
+
toolCall: null,
|
|
740
|
+
timestamp: resolveOptionalMessageTimestamp(record)
|
|
741
|
+
};
|
|
742
|
+
}
|
|
743
|
+
function readGeminiThoughtDescriptors(nodeRecord) {
|
|
744
|
+
const thoughts = arrayFromUnknown(nodeRecord.thoughts) ??
|
|
745
|
+
arrayFromUnknown(nodeRecord.reasoning) ??
|
|
746
|
+
null;
|
|
747
|
+
if (!thoughts || thoughts.length === 0) {
|
|
748
|
+
return [];
|
|
749
|
+
}
|
|
750
|
+
return thoughts
|
|
751
|
+
.map((thought) => normalizeGeminiThoughtDescriptor(thought))
|
|
752
|
+
.filter((descriptor) => descriptor !== null);
|
|
753
|
+
}
|
|
754
|
+
function normalizeGeminiThoughtDescriptor(value) {
|
|
755
|
+
const record = toRecord(value);
|
|
756
|
+
const subject = resolveStringField(record, ["subject", "title", "name"]);
|
|
757
|
+
const description = resolveStringField(record, [
|
|
758
|
+
"description",
|
|
759
|
+
"content",
|
|
760
|
+
"text",
|
|
761
|
+
"message"
|
|
762
|
+
]);
|
|
763
|
+
const content = [subject, description].filter((item) => Boolean(item)).join("\n\n").trim();
|
|
764
|
+
if (!content) {
|
|
765
|
+
return null;
|
|
766
|
+
}
|
|
767
|
+
return {
|
|
768
|
+
role: "assistant",
|
|
769
|
+
kind: "thinking",
|
|
770
|
+
content,
|
|
771
|
+
toolCall: null,
|
|
772
|
+
timestamp: resolveOptionalMessageTimestamp(record)
|
|
773
|
+
};
|
|
774
|
+
}
|
|
775
|
+
function readGeminiToolCallDescriptors(nodeRecord) {
|
|
776
|
+
const toolCalls = arrayFromUnknown(nodeRecord.toolCalls) ??
|
|
777
|
+
arrayFromUnknown(nodeRecord.tool_calls) ??
|
|
778
|
+
null;
|
|
779
|
+
if (!toolCalls || toolCalls.length === 0) {
|
|
780
|
+
return [];
|
|
781
|
+
}
|
|
782
|
+
const descriptors = [];
|
|
783
|
+
for (let index = 0; index < toolCalls.length; index += 1) {
|
|
784
|
+
const descriptorSet = normalizeGeminiToolCall(toolCalls[index], index);
|
|
785
|
+
if (descriptorSet.call) {
|
|
786
|
+
descriptors.push(descriptorSet.call);
|
|
787
|
+
}
|
|
788
|
+
if (descriptorSet.result) {
|
|
789
|
+
descriptors.push(descriptorSet.result);
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
return descriptors;
|
|
793
|
+
}
|
|
794
|
+
function normalizeGeminiToolCall(value, index) {
|
|
795
|
+
const record = toRecord(value);
|
|
796
|
+
const timestamp = resolveOptionalMessageTimestamp(record);
|
|
797
|
+
const patchText = buildGeminiApplyPatchFromInput(record);
|
|
798
|
+
const callId = resolveStringField(record, ["id", "toolCallId", "call_id"]) ||
|
|
799
|
+
`gemini-tool-call-${index + 1}`;
|
|
800
|
+
const rawName = resolveStringField(record, ["name", "toolName", "tool_name", "displayName"]) || "tool";
|
|
801
|
+
const name = patchText ? "apply_patch" : rawName;
|
|
802
|
+
const inputPayload = record.args ?? record.input ?? record.arguments ?? null;
|
|
803
|
+
const inputText = patchText || stringifyStructuredValue(inputPayload);
|
|
804
|
+
const callDescriptor = {
|
|
805
|
+
role: patchText ? "tool" : "assistant",
|
|
806
|
+
kind: "tool_call",
|
|
807
|
+
content: inputText || name,
|
|
808
|
+
toolCall: {
|
|
809
|
+
callId,
|
|
810
|
+
name,
|
|
811
|
+
input: inputText,
|
|
812
|
+
output: null,
|
|
813
|
+
error: null,
|
|
814
|
+
status: "running"
|
|
815
|
+
},
|
|
816
|
+
timestamp
|
|
817
|
+
};
|
|
818
|
+
return {
|
|
819
|
+
call: callDescriptor,
|
|
820
|
+
result: normalizeGeminiToolCallResult(record, callId, name, timestamp)
|
|
821
|
+
};
|
|
822
|
+
}
|
|
823
|
+
function normalizeGeminiToolCallResult(record, callId, name, fallbackTimestamp) {
|
|
824
|
+
const output = extractGeminiToolCallOutput(record);
|
|
825
|
+
const error = extractGeminiToolCallError(record);
|
|
826
|
+
const hasResultPayload = Array.isArray(record.result) ||
|
|
827
|
+
record.result !== undefined ||
|
|
828
|
+
record.output !== undefined ||
|
|
829
|
+
record.resultDisplay !== undefined ||
|
|
830
|
+
error !== null;
|
|
831
|
+
if (!hasResultPayload) {
|
|
832
|
+
return null;
|
|
833
|
+
}
|
|
834
|
+
const status = normalizeGeminiToolCallStatus(record, error);
|
|
835
|
+
const content = output || error || name;
|
|
836
|
+
if (!content.trim()) {
|
|
837
|
+
return null;
|
|
838
|
+
}
|
|
839
|
+
return {
|
|
840
|
+
role: "tool",
|
|
841
|
+
kind: "tool_result",
|
|
842
|
+
content,
|
|
843
|
+
toolCall: {
|
|
844
|
+
callId,
|
|
845
|
+
name,
|
|
846
|
+
input: "",
|
|
847
|
+
output: output || null,
|
|
848
|
+
error,
|
|
849
|
+
status
|
|
850
|
+
},
|
|
851
|
+
timestamp: resolveStringField(record, [
|
|
852
|
+
"timestamp",
|
|
853
|
+
"updatedAt",
|
|
854
|
+
"updated_at",
|
|
855
|
+
"lastUpdated",
|
|
856
|
+
"last_updated"
|
|
857
|
+
]) || fallbackTimestamp
|
|
858
|
+
};
|
|
859
|
+
}
|
|
860
|
+
function extractGeminiToolCallOutput(record) {
|
|
861
|
+
const resultItems = arrayFromUnknown(record.result) ?? [];
|
|
862
|
+
for (const item of resultItems) {
|
|
863
|
+
const itemRecord = toRecord(item);
|
|
864
|
+
const functionResponse = toRecord(itemRecord.functionResponse);
|
|
865
|
+
const response = toRecord(functionResponse.response);
|
|
866
|
+
const outputText = extractTextBlocks(response.output ??
|
|
867
|
+
itemRecord.output ??
|
|
868
|
+
itemRecord.result ??
|
|
869
|
+
itemRecord.content).trim();
|
|
870
|
+
if (outputText) {
|
|
871
|
+
return outputText;
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
const directOutput = extractTextBlocks(record.output ??
|
|
875
|
+
record.result ??
|
|
876
|
+
toRecord(record.resultDisplay).fileDiff ??
|
|
877
|
+
toRecord(record.resultDisplay).newContent).trim();
|
|
878
|
+
return directOutput;
|
|
879
|
+
}
|
|
880
|
+
function extractGeminiToolCallError(record) {
|
|
881
|
+
const directError = resolveStringField(record, [
|
|
882
|
+
"error",
|
|
883
|
+
"error_message",
|
|
884
|
+
"failure",
|
|
885
|
+
"description"
|
|
886
|
+
]);
|
|
887
|
+
if (directError && normalizeGeminiToolCallStatus(record, null) === "failed") {
|
|
888
|
+
return directError;
|
|
889
|
+
}
|
|
890
|
+
return null;
|
|
891
|
+
}
|
|
892
|
+
function normalizeGeminiToolCallStatus(record, error) {
|
|
893
|
+
if (error) {
|
|
894
|
+
return "failed";
|
|
895
|
+
}
|
|
896
|
+
const normalizedStatus = ensureText(record.status).trim().toLowerCase();
|
|
897
|
+
if (!normalizedStatus || normalizedStatus === "success" || normalizedStatus === "completed") {
|
|
898
|
+
return "completed";
|
|
899
|
+
}
|
|
900
|
+
if (["error", "failed", "failure", "cancelled", "canceled"].includes(normalizedStatus)) {
|
|
901
|
+
return "failed";
|
|
902
|
+
}
|
|
903
|
+
return "completed";
|
|
904
|
+
}
|
|
905
|
+
function buildGeminiApplyPatchFromInput(value) {
|
|
906
|
+
const input = value && typeof value === "object" && !Array.isArray(value)
|
|
907
|
+
? value
|
|
908
|
+
: null;
|
|
909
|
+
if (!input) {
|
|
910
|
+
return null;
|
|
911
|
+
}
|
|
912
|
+
const candidates = [toRecord(input.input), toRecord(input.arguments), toRecord(input.args), input];
|
|
913
|
+
for (const candidate of candidates) {
|
|
914
|
+
const patchText = buildApplyPatchFromStructuredFileTool(candidate);
|
|
915
|
+
if (patchText) {
|
|
916
|
+
return patchText;
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
return null;
|
|
920
|
+
}
|
|
921
|
+
function isGeminiEditableToolName(toolName) {
|
|
922
|
+
const normalized = toolName.trim().toLowerCase();
|
|
923
|
+
return [
|
|
924
|
+
"write_file",
|
|
925
|
+
"writefile",
|
|
926
|
+
"create_file",
|
|
927
|
+
"edit_file",
|
|
928
|
+
"replace",
|
|
929
|
+
"replace_file",
|
|
930
|
+
"update_file",
|
|
931
|
+
"multi_edit",
|
|
932
|
+
"multiedit"
|
|
933
|
+
].includes(normalized);
|
|
934
|
+
}
|
|
935
|
+
function resolveGeminiRole(record) {
|
|
936
|
+
const candidate = resolveStringField(record, [
|
|
937
|
+
"role",
|
|
938
|
+
"author",
|
|
939
|
+
"sender",
|
|
940
|
+
"source",
|
|
941
|
+
"participant",
|
|
942
|
+
"type"
|
|
943
|
+
])?.toLowerCase();
|
|
944
|
+
if (!candidate) {
|
|
945
|
+
return "assistant";
|
|
946
|
+
}
|
|
947
|
+
if (candidate.includes("user") || candidate.includes("human")) {
|
|
948
|
+
return "user";
|
|
949
|
+
}
|
|
950
|
+
if (candidate.includes("tool")) {
|
|
951
|
+
return "tool";
|
|
952
|
+
}
|
|
953
|
+
if (candidate.includes("system")) {
|
|
954
|
+
return "system";
|
|
955
|
+
}
|
|
956
|
+
if (candidate.includes("assistant")
|
|
957
|
+
|| candidate.includes("model")
|
|
958
|
+
|| candidate.includes("gemini")) {
|
|
959
|
+
return "assistant";
|
|
960
|
+
}
|
|
961
|
+
return "assistant";
|
|
962
|
+
}
|
|
963
|
+
function resolveMessageTimestamp(record) {
|
|
964
|
+
const direct = resolveStringField(record, [
|
|
965
|
+
"timestamp",
|
|
966
|
+
"time",
|
|
967
|
+
"createdAt",
|
|
968
|
+
"created_at",
|
|
969
|
+
"updatedAt",
|
|
970
|
+
"updated_at"
|
|
971
|
+
]);
|
|
972
|
+
if (direct) {
|
|
973
|
+
return direct;
|
|
974
|
+
}
|
|
975
|
+
return nextTimestamp();
|
|
976
|
+
}
|
|
977
|
+
function resolveOptionalMessageTimestamp(record) {
|
|
978
|
+
return resolveStringField(record, [
|
|
979
|
+
"timestamp",
|
|
980
|
+
"time",
|
|
981
|
+
"createdAt",
|
|
982
|
+
"created_at",
|
|
983
|
+
"updatedAt",
|
|
984
|
+
"updated_at",
|
|
985
|
+
"lastUpdated",
|
|
986
|
+
"last_updated"
|
|
987
|
+
]);
|
|
988
|
+
}
|
|
989
|
+
function buildGeminiMessageRawRef(sessionId, filePath, messageIndex, partIndex) {
|
|
990
|
+
return `${GEMINI_RAW_STORE_PREFIX}${encodeURIComponent(sessionId)}#file=${encodeURIComponent(filePath.replaceAll("\\", "/"))}&index=${messageIndex}&part=${partIndex}`;
|
|
991
|
+
}
|
|
992
|
+
function resolveStringField(record, fieldNames) {
|
|
993
|
+
for (const fieldName of fieldNames) {
|
|
994
|
+
const value = ensureNonEmptyText(record[fieldName]);
|
|
995
|
+
if (value) {
|
|
996
|
+
return value;
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
const metadata = toRecord(record.metadata);
|
|
1000
|
+
for (const fieldName of fieldNames) {
|
|
1001
|
+
const value = ensureNonEmptyText(metadata[fieldName]);
|
|
1002
|
+
if (value) {
|
|
1003
|
+
return value;
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
return null;
|
|
1007
|
+
}
|
|
1008
|
+
function resolveNumberField(record, fieldNames) {
|
|
1009
|
+
for (const fieldName of fieldNames) {
|
|
1010
|
+
const value = record[fieldName];
|
|
1011
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
1012
|
+
return Math.max(0, Math.trunc(value));
|
|
1013
|
+
}
|
|
1014
|
+
if (typeof value === "string") {
|
|
1015
|
+
const parsed = Number.parseInt(value, 10);
|
|
1016
|
+
if (Number.isFinite(parsed)) {
|
|
1017
|
+
return Math.max(0, parsed);
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
return null;
|
|
1022
|
+
}
|
|
1023
|
+
function parseGeminiPlainTextSessions(stdout, workspacePathFallback) {
|
|
1024
|
+
const sessions = stdout
|
|
1025
|
+
.split(/\r?\n/)
|
|
1026
|
+
.map((line) => normalizePlainTextCliSessionLine(line, workspacePathFallback))
|
|
1027
|
+
.filter((item) => item !== null);
|
|
1028
|
+
return dedupeCliSessions(sessions);
|
|
1029
|
+
}
|
|
1030
|
+
function normalizePlainTextCliSessionLine(line, workspacePathFallback) {
|
|
1031
|
+
const trimmed = line.trim();
|
|
1032
|
+
if (!trimmed) {
|
|
1033
|
+
return null;
|
|
1034
|
+
}
|
|
1035
|
+
const sessionIdMatch = trimmed.match(/\[([^\]]+)\]\s*$/);
|
|
1036
|
+
if (!sessionIdMatch?.[1]) {
|
|
1037
|
+
return null;
|
|
1038
|
+
}
|
|
1039
|
+
const providerSessionId = sessionIdMatch[1].trim();
|
|
1040
|
+
if (!providerSessionId) {
|
|
1041
|
+
return null;
|
|
1042
|
+
}
|
|
1043
|
+
let prefix = trimmed.slice(0, sessionIdMatch.index).trim();
|
|
1044
|
+
prefix = prefix.replace(/^\d+\.\s*/, "").trim();
|
|
1045
|
+
const timeSuffixMatch = prefix.match(/\(([^()]*)\)\s*$/);
|
|
1046
|
+
const title = (timeSuffixMatch && typeof timeSuffixMatch.index === "number"
|
|
1047
|
+
? prefix.slice(0, timeSuffixMatch.index)
|
|
1048
|
+
: prefix).trim() || providerSessionId;
|
|
1049
|
+
return {
|
|
1050
|
+
providerSessionId,
|
|
1051
|
+
workspacePath: workspacePathFallback,
|
|
1052
|
+
title,
|
|
1053
|
+
lastMessageAt: null,
|
|
1054
|
+
messageCount: null
|
|
1055
|
+
};
|
|
1056
|
+
}
|
|
1057
|
+
function ensureNonEmptyText(value) {
|
|
1058
|
+
const text = ensureText(value).trim();
|
|
1059
|
+
return text.length > 0 ? text : null;
|
|
1060
|
+
}
|
|
1061
|
+
function parseJsonSafe(value) {
|
|
1062
|
+
if (!value) {
|
|
1063
|
+
return null;
|
|
1064
|
+
}
|
|
1065
|
+
try {
|
|
1066
|
+
return JSON.parse(value);
|
|
1067
|
+
}
|
|
1068
|
+
catch {
|
|
1069
|
+
return null;
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
function arrayFromUnknown(value) {
|
|
1073
|
+
return Array.isArray(value) ? value : null;
|
|
1074
|
+
}
|
|
1075
|
+
function toRecord(value) {
|
|
1076
|
+
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
1077
|
+
return value;
|
|
1078
|
+
}
|
|
1079
|
+
return {};
|
|
1080
|
+
}
|
|
1081
|
+
function maybeRecord(value) {
|
|
1082
|
+
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
1083
|
+
return value;
|
|
1084
|
+
}
|
|
1085
|
+
return null;
|
|
1086
|
+
}
|
|
1087
|
+
function resolveWorkspacePathFromChatFile(filePath) {
|
|
1088
|
+
const projectRootFile = join(dirname(dirname(filePath)), ".project_root");
|
|
1089
|
+
if (!existsSync(projectRootFile)) {
|
|
1090
|
+
return null;
|
|
1091
|
+
}
|
|
1092
|
+
try {
|
|
1093
|
+
return ensureNonEmptyText(readFileSync(projectRootFile, "utf8"));
|
|
1094
|
+
}
|
|
1095
|
+
catch {
|
|
1096
|
+
return null;
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
function shouldUseShellForCommand(commandPath) {
|
|
1100
|
+
return process.platform === "win32" && [".cmd", ".bat"].includes(extname(commandPath).toLowerCase());
|
|
1101
|
+
}
|
|
1102
|
+
function isCommandAvailable(commandPath) {
|
|
1103
|
+
const normalizedCommandPath = stripWrappingQuotes(commandPath);
|
|
1104
|
+
if (!normalizedCommandPath) {
|
|
1105
|
+
return false;
|
|
1106
|
+
}
|
|
1107
|
+
if (normalizedCommandPath.includes("/")
|
|
1108
|
+
|| normalizedCommandPath.includes("\\")) {
|
|
1109
|
+
if (!existsSync(normalizedCommandPath)) {
|
|
1110
|
+
return false;
|
|
1111
|
+
}
|
|
1112
|
+
const extension = extname(normalizedCommandPath).toLowerCase();
|
|
1113
|
+
if ([".js", ".cjs", ".mjs"].includes(extension)) {
|
|
1114
|
+
return true;
|
|
1115
|
+
}
|
|
1116
|
+
if (process.platform === "win32") {
|
|
1117
|
+
return true;
|
|
1118
|
+
}
|
|
1119
|
+
try {
|
|
1120
|
+
accessSync(normalizedCommandPath, constants.X_OK);
|
|
1121
|
+
return true;
|
|
1122
|
+
}
|
|
1123
|
+
catch {
|
|
1124
|
+
return false;
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
const pathEntries = (process.env.PATH ?? "")
|
|
1128
|
+
.split(delimiter)
|
|
1129
|
+
.map((entry) => entry.trim())
|
|
1130
|
+
.filter(Boolean);
|
|
1131
|
+
if (process.platform === "win32") {
|
|
1132
|
+
const pathextEntries = (process.env.PATHEXT ?? ".EXE;.CMD;.BAT;.COM")
|
|
1133
|
+
.split(";")
|
|
1134
|
+
.map((entry) => entry.trim().toLowerCase())
|
|
1135
|
+
.filter(Boolean);
|
|
1136
|
+
const candidateNames = extname(normalizedCommandPath)
|
|
1137
|
+
? [normalizedCommandPath]
|
|
1138
|
+
: pathextEntries.map((entry) => `${normalizedCommandPath}${entry.toLowerCase()}`);
|
|
1139
|
+
for (const entry of pathEntries) {
|
|
1140
|
+
for (const candidateName of candidateNames) {
|
|
1141
|
+
if (existsSync(join(entry, candidateName))) {
|
|
1142
|
+
return true;
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
1145
|
+
}
|
|
1146
|
+
return false;
|
|
1147
|
+
}
|
|
1148
|
+
for (const entry of pathEntries) {
|
|
1149
|
+
const candidatePath = join(entry, normalizedCommandPath);
|
|
1150
|
+
if (!existsSync(candidatePath)) {
|
|
1151
|
+
continue;
|
|
1152
|
+
}
|
|
1153
|
+
try {
|
|
1154
|
+
accessSync(candidatePath, constants.X_OK);
|
|
1155
|
+
return true;
|
|
1156
|
+
}
|
|
1157
|
+
catch {
|
|
1158
|
+
continue;
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
return false;
|
|
1162
|
+
}
|
|
1163
|
+
function stripWrappingQuotes(value) {
|
|
1164
|
+
const trimmed = value.trim();
|
|
1165
|
+
if ((trimmed.startsWith("\"") && trimmed.endsWith("\""))
|
|
1166
|
+
|| (trimmed.startsWith("'") && trimmed.endsWith("'"))) {
|
|
1167
|
+
return trimmed.slice(1, -1).trim();
|
|
1168
|
+
}
|
|
1169
|
+
return trimmed;
|
|
1170
|
+
}
|
|
1171
|
+
function wrapGeminiSchemaError(filePath, error) {
|
|
1172
|
+
const detail = error instanceof Error ? error.message : ensureText(error);
|
|
1173
|
+
return new Error(`GEMINI_CHAT_SCHEMA_INVALID: file=${filePath.replaceAll("\\", "/")} detail=${detail}`);
|
|
1174
|
+
}
|
|
1175
|
+
//# sourceMappingURL=gemini.js.map
|