@remnic/import-gemini 9.3.610 → 9.3.612
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/index.js +6 -13
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
import { defaultWriteMemoriesToOrchestrator } from "@remnic/core";
|
|
5
5
|
|
|
6
6
|
// src/parser.ts
|
|
7
|
-
|
|
7
|
+
import { parseIsoOffsetTimestamp } from "@remnic/core";
|
|
8
|
+
var UTC_ISO_INSTANT_RE = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]00:00)$/;
|
|
8
9
|
function parseGeminiExport(input, options = {}) {
|
|
9
10
|
if (input === void 0 || input === null) {
|
|
10
11
|
throw new Error(
|
|
@@ -37,9 +38,7 @@ function parseGeminiExport(input, options = {}) {
|
|
|
37
38
|
}
|
|
38
39
|
return result;
|
|
39
40
|
}
|
|
40
|
-
throw new Error(
|
|
41
|
-
`Gemini export must be a JSON array or object; received ${describeType(raw)}`
|
|
42
|
-
);
|
|
41
|
+
throw new Error(`Gemini export must be a JSON array or object; received ${describeType(raw)}`);
|
|
43
42
|
}
|
|
44
43
|
function describeType(value) {
|
|
45
44
|
if (value === null) return "null";
|
|
@@ -64,14 +63,10 @@ function validateGeminiTimestamp(value) {
|
|
|
64
63
|
if (typeof value !== "string" || value.trim().length === 0) {
|
|
65
64
|
throw new Error("Gemini activity time must be an ISO-8601 UTC timestamp");
|
|
66
65
|
}
|
|
67
|
-
|
|
68
|
-
if (!match) {
|
|
66
|
+
if (!UTC_ISO_INSTANT_RE.test(value)) {
|
|
69
67
|
throw new Error("Gemini activity time must be an ISO-8601 UTC timestamp");
|
|
70
68
|
}
|
|
71
|
-
|
|
72
|
-
const canonical = `${match[1]}.${milliseconds}Z`;
|
|
73
|
-
const parsedMs = Date.parse(canonical);
|
|
74
|
-
if (!Number.isFinite(parsedMs) || new Date(parsedMs).toISOString() !== canonical) {
|
|
69
|
+
if (parseIsoOffsetTimestamp(value) === null) {
|
|
75
70
|
throw new Error("Gemini activity time must be a valid ISO-8601 UTC timestamp");
|
|
76
71
|
}
|
|
77
72
|
}
|
|
@@ -88,9 +83,7 @@ function coerceJson(input) {
|
|
|
88
83
|
try {
|
|
89
84
|
return JSON.parse(input);
|
|
90
85
|
} catch (err) {
|
|
91
|
-
throw new Error(
|
|
92
|
-
`Gemini export is not valid JSON: ${err instanceof Error ? err.message : String(err)}`
|
|
93
|
-
);
|
|
86
|
+
throw new Error(`Gemini export is not valid JSON: ${err instanceof Error ? err.message : String(err)}`);
|
|
94
87
|
}
|
|
95
88
|
}
|
|
96
89
|
return input;
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/adapter.ts","../src/parser.ts","../src/transform.ts"],"sourcesContent":["// ---------------------------------------------------------------------------\n// Gemini importer adapter (issue #568 slice 4)\n// ---------------------------------------------------------------------------\n\nimport type {\n ImportedMemory,\n ImporterAdapter,\n ImporterParseOptions,\n ImporterTransformOptions,\n ImporterWriteResult,\n ImporterWriteTarget,\n} from \"@remnic/core\";\nimport { defaultWriteMemoriesToOrchestrator } from \"@remnic/core\";\n\nimport { parseGeminiExport, type ParsedGeminiExport } from \"./parser.js\";\nimport { GEMINI_SOURCE_LABEL, transformGeminiExport } from \"./transform.js\";\n\n/**\n * Canonical `ImporterAdapter` exposed by `@remnic/import-gemini`.\n *\n * Loaded by `remnic-cli/optional-importer.ts` via a computed-specifier dynamic\n * import. The CLI drives `adapter.parse` → `adapter.transform` →\n * `adapter.writeTo` through the shared `runImporter` helper in `@remnic/core`.\n */\nexport const adapter: ImporterAdapter<ParsedGeminiExport> = {\n name: \"gemini\",\n sourceLabel: GEMINI_SOURCE_LABEL,\n\n parse(input: unknown, options?: ImporterParseOptions): ParsedGeminiExport {\n return parseGeminiExport(input, {\n ...(options?.strict !== undefined ? { strict: options.strict } : {}),\n ...(options?.filePath !== undefined ? { filePath: options.filePath } : {}),\n });\n },\n\n transform(\n parsed: ParsedGeminiExport,\n options?: ImporterTransformOptions,\n ): ImportedMemory[] {\n return transformGeminiExport(parsed, {\n ...(options?.maxMemories !== undefined\n ? { maxMemories: options.maxMemories }\n : {}),\n ...(options?.minPromptLength !== undefined\n ? { minPromptLength: options.minPromptLength }\n : {}),\n });\n },\n\n async writeTo(\n target: ImporterWriteTarget,\n memories: ImportedMemory[],\n ): Promise<ImporterWriteResult> {\n return defaultWriteMemoriesToOrchestrator(target, memories);\n },\n};\n\n/** Alias kept for symmetry with other @remnic/import-* packages. */\nexport const geminiAdapter = adapter;\n","// ---------------------------------------------------------------------------\n// Gemini (Google Takeout \"Gemini Apps Activity\") parser (issue #568 slice 4)\n// ---------------------------------------------------------------------------\n//\n// Google Takeout bundles Gemini Apps activity into `My Activity.json` (and a\n// legacy `MyActivity.json` spelling). Each record represents one prompt the\n// user sent. The schema is stable across exports we have observed:\n//\n// {\n// \"header\": \"Gemini Apps\",\n// \"title\": \"Asked: <user prompt>\", // older exports\n// \"text\": \"<user prompt>\", // newer exports\n// \"titleUrl\": \"https://gemini.google.com/...\",\n// \"time\": \"2026-02-14T09:30:00.000Z\",\n// \"products\": [\"Gemini Apps\"],\n// \"subtitles\": [{ \"name\": \"Model: Gemini X\" }]\n// }\n//\n// Prompts are pre-extracted by Google (no DOM scraping). The user prompt text\n// lives in `text`, or inside `title` as \"Asked: <prompt>\" for legacy records.\n// Assistant responses are NOT exported by Takeout (Google omits them), so\n// we only import the user's prompts.\n//\n// The parser accepts either the raw `My Activity.json` contents (JSON string\n// or already-parsed array) or a combined bundle object\n// `{ activities: [...] }` for future bundle-auto-detect (PR 7).\n\n// ---------------------------------------------------------------------------\n// Raw export shapes\n// ---------------------------------------------------------------------------\n\n/**\n * A single Gemini Apps activity record. Other `header` values (Search, Maps,\n * YouTube) co-exist in the same file when the user exports multiple products;\n * we filter to Gemini Apps only.\n */\nexport interface GeminiActivityRecord {\n header?: string;\n title?: string;\n titleUrl?: string;\n /** Newer exports put the prompt text here. */\n text?: string;\n /** ISO 8601 timestamp of the prompt. */\n time?: string;\n products?: string[];\n subtitles?: Array<{ name?: string; url?: string }>;\n /** Some exports embed the model response as a follow-up subtitle. */\n details?: Array<{ name?: string }>;\n}\n\nconst UTC_ISO_INSTANT_RE =\n /^(\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2})(?:\\.(\\d{1,3}))?(Z|[+-]00:00)$/;\n\nexport interface ParsedGeminiExport {\n /** Prompts filtered to `header === \"Gemini Apps\"`. */\n activities: GeminiActivityRecord[];\n filePath?: string;\n}\n\n// ---------------------------------------------------------------------------\n// Public parse API\n// ---------------------------------------------------------------------------\n\nexport interface GeminiParseOptions {\n strict?: boolean;\n filePath?: string;\n /**\n * When true, keep records even if their header is not \"Gemini Apps\".\n * Default false — we only import Gemini activity.\n */\n keepNonGemini?: boolean;\n}\n\n/**\n * Parse a Takeout activity payload. Accepts a JSON string, parsed object, or\n * parsed array. Non-Gemini records are filtered out by default.\n */\nexport function parseGeminiExport(\n input: unknown,\n options: GeminiParseOptions = {},\n): ParsedGeminiExport {\n // File-backed adapter contract: `runImportCommand` passes `undefined` when\n // `--file` is omitted. Gemini is a file-only importer (Takeout doesn't\n // expose an API), so a missing payload MUST surface as a user-facing\n // error rather than silently succeeding with 0 memories. Cursor review\n // on PR #600 flagged the silent-success path.\n if (input === undefined || input === null) {\n throw new Error(\n \"The 'gemini' importer requires a file. Pass `--file <path>` pointing at \" +\n \"your Google Takeout `My Activity.json` (Gemini Apps section).\",\n );\n }\n const raw = coerceJson(input);\n const result: ParsedGeminiExport = {\n activities: [],\n ...(options.filePath !== undefined ? { filePath: options.filePath } : {}),\n };\n\n if (Array.isArray(raw)) {\n appendActivities(result.activities, raw, options);\n return result;\n }\n\n if (raw && typeof raw === \"object\") {\n const obj = raw as Record<string, unknown>;\n // Common wrapper shapes: { activities: [...] } or { MyActivity: [...] }.\n let sawKnownKey = false;\n for (const key of [\"activities\", \"MyActivity\", \"activity\"] as const) {\n const v = obj[key];\n if (Array.isArray(v)) {\n sawKnownKey = true;\n appendActivities(result.activities, v, options);\n }\n }\n // Codex review on PR #600: pointing --file at a random JSON object\n // (e.g. a config file) used to report a successful 0-memory import.\n // We now throw a user-facing error when no known wrapper key was\n // present. This differs from strict mode because the \"array or\n // object\" shape check already passed — we just didn't find anything\n // that looked like a Gemini export inside the object.\n if (!sawKnownKey) {\n throw new Error(\n \"Gemini export object has no recognized activity key. Expected one of \" +\n \"'activities', 'MyActivity', or 'activity'. Point --file at your \" +\n \"Google Takeout `My Activity.json` (Gemini Apps section).\",\n );\n }\n return result;\n }\n\n // Codex review on PR #600 — primitive payloads (numbers, booleans,\n // strings, etc.) must always throw, regardless of strict mode. Silently\n // returning a 0-memory success on a JSON primitive (e.g. `true`,\n // `\"text\"`, `123`) hides operator input mistakes and makes automation\n // treat a broken import as healthy. Report the actual received value —\n // `typeof null === \"object\"` is the JS trap CLAUDE.md rule 18 calls\n // out. Using describeType() sidesteps the \"received object\" message\n // for null inputs.\n throw new Error(\n `Gemini export must be a JSON array or object; received ${describeType(raw)}`,\n );\n}\n\nfunction describeType(value: unknown): string {\n if (value === null) return \"null\";\n return typeof value;\n}\n\nfunction appendActivities(\n dest: GeminiActivityRecord[],\n src: unknown[],\n options: GeminiParseOptions,\n): void {\n for (const entry of src) {\n if (!entry || typeof entry !== \"object\") {\n if (options.strict) {\n throw new Error(\"Gemini activity entry must be an object\");\n }\n continue;\n }\n const record = entry as GeminiActivityRecord;\n if (!options.keepNonGemini && !isGeminiRecord(record)) continue;\n validateGeminiTimestamp(record.time);\n dest.push(record);\n }\n}\n\nfunction validateGeminiTimestamp(value: unknown): void {\n if (value === undefined) return;\n if (typeof value !== \"string\" || value.trim().length === 0) {\n throw new Error(\"Gemini activity time must be an ISO-8601 UTC timestamp\");\n }\n const match = UTC_ISO_INSTANT_RE.exec(value);\n if (!match) {\n throw new Error(\"Gemini activity time must be an ISO-8601 UTC timestamp\");\n }\n const milliseconds = (match[2] ?? \"\").padEnd(3, \"0\");\n const canonical = `${match[1]}.${milliseconds}Z`;\n const parsedMs = Date.parse(canonical);\n if (!Number.isFinite(parsedMs) || new Date(parsedMs).toISOString() !== canonical) {\n throw new Error(\"Gemini activity time must be a valid ISO-8601 UTC timestamp\");\n }\n}\n\nfunction isGeminiRecord(record: GeminiActivityRecord): boolean {\n if (record.header === \"Gemini Apps\") return true;\n if (record.header === \"Bard\") return true; // pre-rebrand exports\n if (\n Array.isArray(record.products) &&\n record.products.some((p) => p === \"Gemini Apps\" || p === \"Bard\")\n ) {\n return true;\n }\n return false;\n}\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\nfunction coerceJson(input: unknown): unknown {\n if (typeof input === \"string\") {\n try {\n return JSON.parse(input);\n } catch (err) {\n throw new Error(\n `Gemini export is not valid JSON: ${\n err instanceof Error ? err.message : String(err)\n }`,\n );\n }\n }\n return input;\n}\n\n/**\n * Extract the user prompt text from an activity record. Newer exports put it\n * in `text`; older ones nest it inside `title` as \"Asked: <prompt>\". Returns\n * `undefined` when no usable prompt is present.\n */\nexport function extractUserPrompt(record: GeminiActivityRecord): string | undefined {\n if (typeof record.text === \"string\") {\n const t = record.text.trim();\n if (t.length > 0) return t;\n }\n if (typeof record.title === \"string\") {\n const t = record.title.trim();\n if (t.length === 0) return undefined;\n // Strip legacy prefixes that Takeout added in older exports.\n const stripped = t.replace(/^(Asked|Prompted|Searched|Typed):\\s*/i, \"\");\n return stripped.length > 0 ? stripped : undefined;\n }\n return undefined;\n}\n","// ---------------------------------------------------------------------------\n// Gemini parsed → ImportedMemory transform (issue #568 slice 4)\n// ---------------------------------------------------------------------------\n//\n// Google Takeout only exports the user's prompts — assistant responses are\n// omitted by Google. We therefore import every Gemini Apps activity record\n// as one memory containing the prompt text. Each prompt is first-person\n// intent (what the user asked), which the downstream extraction pipeline\n// can score / cluster just like any other memory source.\n//\n// Unlike the ChatGPT and Claude importers, there is no \"conversation\" layer\n// to opt into — Takeout doesn't preserve conversation boundaries for\n// Gemini Apps. The `includeConversations` flag is ignored.\n\nimport type { ImportedMemory } from \"@remnic/core\";\n\nimport type { GeminiActivityRecord, ParsedGeminiExport } from \"./parser.js\";\nimport { extractUserPrompt } from \"./parser.js\";\n\nexport const GEMINI_SOURCE_LABEL = \"gemini\";\n\nexport interface GeminiTransformOptions {\n /** Optional cap on total memories emitted — primarily for tests. */\n maxMemories?: number;\n /**\n * Minimum prompt length (in characters) to import. Very short prompts\n * (\"yes\", \"ok\", \"tell me more\") provide little durable signal. Default 10.\n */\n minPromptLength?: number;\n}\n\nconst DEFAULT_MIN_PROMPT_LENGTH = 10;\n\n/**\n * Transform a parsed Gemini export into `ImportedMemory[]`. One memory per\n * Gemini activity record containing a non-trivial user prompt.\n */\nexport function transformGeminiExport(\n parsed: ParsedGeminiExport,\n options: GeminiTransformOptions = {},\n): ImportedMemory[] {\n const out: ImportedMemory[] = [];\n const cap = options.maxMemories;\n const minLen = options.minPromptLength ?? DEFAULT_MIN_PROMPT_LENGTH;\n\n for (const record of parsed.activities) {\n if (cap !== undefined && out.length >= cap) return out;\n const memory = activityToImported(record, parsed.filePath, minLen);\n if (memory) out.push(memory);\n }\n return out;\n}\n\nfunction activityToImported(\n record: GeminiActivityRecord,\n filePath: string | undefined,\n minLen: number,\n): ImportedMemory | undefined {\n const prompt = extractUserPrompt(record);\n if (!prompt || prompt.length < minLen) return undefined;\n\n const metadata: Record<string, unknown> = { kind: \"prompt\" };\n if (typeof record.titleUrl === \"string\" && record.titleUrl.length > 0) {\n metadata.activityUrl = record.titleUrl;\n }\n if (Array.isArray(record.subtitles) && record.subtitles.length > 0) {\n const modelSubtitle = record.subtitles.find(\n (s) => typeof s.name === \"string\" && /model/i.test(s.name),\n );\n if (modelSubtitle?.name) metadata.modelTag = modelSubtitle.name;\n }\n\n return {\n content: prompt,\n sourceLabel: GEMINI_SOURCE_LABEL,\n ...(record.time !== undefined ? { sourceTimestamp: record.time } : {}),\n ...(filePath !== undefined ? { importedFromPath: filePath } : {}),\n metadata,\n };\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/adapter.ts","../src/parser.ts","../src/transform.ts"],"sourcesContent":["// ---------------------------------------------------------------------------\n// Gemini importer adapter (issue #568 slice 4)\n// ---------------------------------------------------------------------------\n\nimport type {\n ImportedMemory,\n ImporterAdapter,\n ImporterParseOptions,\n ImporterTransformOptions,\n ImporterWriteResult,\n ImporterWriteTarget,\n} from \"@remnic/core\";\nimport { defaultWriteMemoriesToOrchestrator } from \"@remnic/core\";\n\nimport { parseGeminiExport, type ParsedGeminiExport } from \"./parser.js\";\nimport { GEMINI_SOURCE_LABEL, transformGeminiExport } from \"./transform.js\";\n\n/**\n * Canonical `ImporterAdapter` exposed by `@remnic/import-gemini`.\n *\n * Loaded by `remnic-cli/optional-importer.ts` via a computed-specifier dynamic\n * import. The CLI drives `adapter.parse` → `adapter.transform` →\n * `adapter.writeTo` through the shared `runImporter` helper in `@remnic/core`.\n */\nexport const adapter: ImporterAdapter<ParsedGeminiExport> = {\n name: \"gemini\",\n sourceLabel: GEMINI_SOURCE_LABEL,\n\n parse(input: unknown, options?: ImporterParseOptions): ParsedGeminiExport {\n return parseGeminiExport(input, {\n ...(options?.strict !== undefined ? { strict: options.strict } : {}),\n ...(options?.filePath !== undefined ? { filePath: options.filePath } : {}),\n });\n },\n\n transform(\n parsed: ParsedGeminiExport,\n options?: ImporterTransformOptions,\n ): ImportedMemory[] {\n return transformGeminiExport(parsed, {\n ...(options?.maxMemories !== undefined\n ? { maxMemories: options.maxMemories }\n : {}),\n ...(options?.minPromptLength !== undefined\n ? { minPromptLength: options.minPromptLength }\n : {}),\n });\n },\n\n async writeTo(\n target: ImporterWriteTarget,\n memories: ImportedMemory[],\n ): Promise<ImporterWriteResult> {\n return defaultWriteMemoriesToOrchestrator(target, memories);\n },\n};\n\n/** Alias kept for symmetry with other @remnic/import-* packages. */\nexport const geminiAdapter = adapter;\n","// ---------------------------------------------------------------------------\n// Gemini (Google Takeout \"Gemini Apps Activity\") parser (issue #568 slice 4)\n// ---------------------------------------------------------------------------\n\nimport { parseIsoOffsetTimestamp } from \"@remnic/core\";\n//\n// Google Takeout bundles Gemini Apps activity into `My Activity.json` (and a\n// legacy `MyActivity.json` spelling). Each record represents one prompt the\n// user sent. The schema is stable across exports we have observed:\n//\n// {\n// \"header\": \"Gemini Apps\",\n// \"title\": \"Asked: <user prompt>\", // older exports\n// \"text\": \"<user prompt>\", // newer exports\n// \"titleUrl\": \"https://gemini.google.com/...\",\n// \"time\": \"2026-02-14T09:30:00.000Z\",\n// \"products\": [\"Gemini Apps\"],\n// \"subtitles\": [{ \"name\": \"Model: Gemini X\" }]\n// }\n//\n// Prompts are pre-extracted by Google (no DOM scraping). The user prompt text\n// lives in `text`, or inside `title` as \"Asked: <prompt>\" for legacy records.\n// Assistant responses are NOT exported by Takeout (Google omits them), so\n// we only import the user's prompts.\n//\n// The parser accepts either the raw `My Activity.json` contents (JSON string\n// or already-parsed array) or a combined bundle object\n// `{ activities: [...] }` for future bundle-auto-detect (PR 7).\n\n// ---------------------------------------------------------------------------\n// Raw export shapes\n// ---------------------------------------------------------------------------\n\n/**\n * A single Gemini Apps activity record. Other `header` values (Search, Maps,\n * YouTube) co-exist in the same file when the user exports multiple products;\n * we filter to Gemini Apps only.\n */\nexport interface GeminiActivityRecord {\n header?: string;\n title?: string;\n titleUrl?: string;\n /** Newer exports put the prompt text here. */\n text?: string;\n /** ISO 8601 timestamp of the prompt. */\n time?: string;\n products?: string[];\n subtitles?: Array<{ name?: string; url?: string }>;\n /** Some exports embed the model response as a follow-up subtitle. */\n details?: Array<{ name?: string }>;\n}\n\nconst UTC_ISO_INSTANT_RE = /^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(?:\\.\\d+)?(?:Z|[+-]00:00)$/;\n\nexport interface ParsedGeminiExport {\n /** Prompts filtered to `header === \"Gemini Apps\"`. */\n activities: GeminiActivityRecord[];\n filePath?: string;\n}\n\n// ---------------------------------------------------------------------------\n// Public parse API\n// ---------------------------------------------------------------------------\n\nexport interface GeminiParseOptions {\n strict?: boolean;\n filePath?: string;\n /**\n * When true, keep records even if their header is not \"Gemini Apps\".\n * Default false — we only import Gemini activity.\n */\n keepNonGemini?: boolean;\n}\n\n/**\n * Parse a Takeout activity payload. Accepts a JSON string, parsed object, or\n * parsed array. Non-Gemini records are filtered out by default.\n */\nexport function parseGeminiExport(input: unknown, options: GeminiParseOptions = {}): ParsedGeminiExport {\n // File-backed adapter contract: `runImportCommand` passes `undefined` when\n // `--file` is omitted. Gemini is a file-only importer (Takeout doesn't\n // expose an API), so a missing payload MUST surface as a user-facing\n // error rather than silently succeeding with 0 memories. Cursor review\n // on PR #600 flagged the silent-success path.\n if (input === undefined || input === null) {\n throw new Error(\n \"The 'gemini' importer requires a file. Pass `--file <path>` pointing at \" +\n \"your Google Takeout `My Activity.json` (Gemini Apps section).\"\n );\n }\n const raw = coerceJson(input);\n const result: ParsedGeminiExport = {\n activities: [],\n ...(options.filePath !== undefined ? { filePath: options.filePath } : {}),\n };\n\n if (Array.isArray(raw)) {\n appendActivities(result.activities, raw, options);\n return result;\n }\n\n if (raw && typeof raw === \"object\") {\n const obj = raw as Record<string, unknown>;\n // Common wrapper shapes: { activities: [...] } or { MyActivity: [...] }.\n let sawKnownKey = false;\n for (const key of [\"activities\", \"MyActivity\", \"activity\"] as const) {\n const v = obj[key];\n if (Array.isArray(v)) {\n sawKnownKey = true;\n appendActivities(result.activities, v, options);\n }\n }\n // Codex review on PR #600: pointing --file at a random JSON object\n // (e.g. a config file) used to report a successful 0-memory import.\n // We now throw a user-facing error when no known wrapper key was\n // present. This differs from strict mode because the \"array or\n // object\" shape check already passed — we just didn't find anything\n // that looked like a Gemini export inside the object.\n if (!sawKnownKey) {\n throw new Error(\n \"Gemini export object has no recognized activity key. Expected one of \" +\n \"'activities', 'MyActivity', or 'activity'. Point --file at your \" +\n \"Google Takeout `My Activity.json` (Gemini Apps section).\"\n );\n }\n return result;\n }\n\n // Codex review on PR #600 — primitive payloads (numbers, booleans,\n // strings, etc.) must always throw, regardless of strict mode. Silently\n // returning a 0-memory success on a JSON primitive (e.g. `true`,\n // `\"text\"`, `123`) hides operator input mistakes and makes automation\n // treat a broken import as healthy. Report the actual received value —\n // `typeof null === \"object\"` is the JS trap CLAUDE.md rule 18 calls\n // out. Using describeType() sidesteps the \"received object\" message\n // for null inputs.\n throw new Error(`Gemini export must be a JSON array or object; received ${describeType(raw)}`);\n}\n\nfunction describeType(value: unknown): string {\n if (value === null) return \"null\";\n return typeof value;\n}\n\nfunction appendActivities(dest: GeminiActivityRecord[], src: unknown[], options: GeminiParseOptions): void {\n for (const entry of src) {\n if (!entry || typeof entry !== \"object\") {\n if (options.strict) {\n throw new Error(\"Gemini activity entry must be an object\");\n }\n continue;\n }\n const record = entry as GeminiActivityRecord;\n if (!options.keepNonGemini && !isGeminiRecord(record)) continue;\n validateGeminiTimestamp(record.time);\n dest.push(record);\n }\n}\n\nfunction validateGeminiTimestamp(value: unknown): void {\n if (value === undefined) return;\n if (typeof value !== \"string\" || value.trim().length === 0) {\n throw new Error(\"Gemini activity time must be an ISO-8601 UTC timestamp\");\n }\n if (!UTC_ISO_INSTANT_RE.test(value)) {\n throw new Error(\"Gemini activity time must be an ISO-8601 UTC timestamp\");\n }\n if (parseIsoOffsetTimestamp(value) === null) {\n throw new Error(\"Gemini activity time must be a valid ISO-8601 UTC timestamp\");\n }\n}\n\nfunction isGeminiRecord(record: GeminiActivityRecord): boolean {\n if (record.header === \"Gemini Apps\") return true;\n if (record.header === \"Bard\") return true; // pre-rebrand exports\n if (Array.isArray(record.products) && record.products.some((p) => p === \"Gemini Apps\" || p === \"Bard\")) {\n return true;\n }\n return false;\n}\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\nfunction coerceJson(input: unknown): unknown {\n if (typeof input === \"string\") {\n try {\n return JSON.parse(input);\n } catch (err) {\n throw new Error(`Gemini export is not valid JSON: ${err instanceof Error ? err.message : String(err)}`);\n }\n }\n return input;\n}\n\n/**\n * Extract the user prompt text from an activity record. Newer exports put it\n * in `text`; older ones nest it inside `title` as \"Asked: <prompt>\". Returns\n * `undefined` when no usable prompt is present.\n */\nexport function extractUserPrompt(record: GeminiActivityRecord): string | undefined {\n if (typeof record.text === \"string\") {\n const t = record.text.trim();\n if (t.length > 0) return t;\n }\n if (typeof record.title === \"string\") {\n const t = record.title.trim();\n if (t.length === 0) return undefined;\n // Strip legacy prefixes that Takeout added in older exports.\n const stripped = t.replace(/^(Asked|Prompted|Searched|Typed):\\s*/i, \"\");\n return stripped.length > 0 ? stripped : undefined;\n }\n return undefined;\n}\n","// ---------------------------------------------------------------------------\n// Gemini parsed → ImportedMemory transform (issue #568 slice 4)\n// ---------------------------------------------------------------------------\n//\n// Google Takeout only exports the user's prompts — assistant responses are\n// omitted by Google. We therefore import every Gemini Apps activity record\n// as one memory containing the prompt text. Each prompt is first-person\n// intent (what the user asked), which the downstream extraction pipeline\n// can score / cluster just like any other memory source.\n//\n// Unlike the ChatGPT and Claude importers, there is no \"conversation\" layer\n// to opt into — Takeout doesn't preserve conversation boundaries for\n// Gemini Apps. The `includeConversations` flag is ignored.\n\nimport type { ImportedMemory } from \"@remnic/core\";\n\nimport type { GeminiActivityRecord, ParsedGeminiExport } from \"./parser.js\";\nimport { extractUserPrompt } from \"./parser.js\";\n\nexport const GEMINI_SOURCE_LABEL = \"gemini\";\n\nexport interface GeminiTransformOptions {\n /** Optional cap on total memories emitted — primarily for tests. */\n maxMemories?: number;\n /**\n * Minimum prompt length (in characters) to import. Very short prompts\n * (\"yes\", \"ok\", \"tell me more\") provide little durable signal. Default 10.\n */\n minPromptLength?: number;\n}\n\nconst DEFAULT_MIN_PROMPT_LENGTH = 10;\n\n/**\n * Transform a parsed Gemini export into `ImportedMemory[]`. One memory per\n * Gemini activity record containing a non-trivial user prompt.\n */\nexport function transformGeminiExport(\n parsed: ParsedGeminiExport,\n options: GeminiTransformOptions = {},\n): ImportedMemory[] {\n const out: ImportedMemory[] = [];\n const cap = options.maxMemories;\n const minLen = options.minPromptLength ?? DEFAULT_MIN_PROMPT_LENGTH;\n\n for (const record of parsed.activities) {\n if (cap !== undefined && out.length >= cap) return out;\n const memory = activityToImported(record, parsed.filePath, minLen);\n if (memory) out.push(memory);\n }\n return out;\n}\n\nfunction activityToImported(\n record: GeminiActivityRecord,\n filePath: string | undefined,\n minLen: number,\n): ImportedMemory | undefined {\n const prompt = extractUserPrompt(record);\n if (!prompt || prompt.length < minLen) return undefined;\n\n const metadata: Record<string, unknown> = { kind: \"prompt\" };\n if (typeof record.titleUrl === \"string\" && record.titleUrl.length > 0) {\n metadata.activityUrl = record.titleUrl;\n }\n if (Array.isArray(record.subtitles) && record.subtitles.length > 0) {\n const modelSubtitle = record.subtitles.find(\n (s) => typeof s.name === \"string\" && /model/i.test(s.name),\n );\n if (modelSubtitle?.name) metadata.modelTag = modelSubtitle.name;\n }\n\n return {\n content: prompt,\n sourceLabel: GEMINI_SOURCE_LABEL,\n ...(record.time !== undefined ? { sourceTimestamp: record.time } : {}),\n ...(filePath !== undefined ? { importedFromPath: filePath } : {}),\n metadata,\n };\n}\n"],"mappings":";;;AAYA,SAAS,0CAA0C;;;ACRnD,SAAS,+BAA+B;AAgDxC,IAAM,qBAAqB;AA0BpB,SAAS,kBAAkB,OAAgB,UAA8B,CAAC,GAAuB;AAMtG,MAAI,UAAU,UAAa,UAAU,MAAM;AACzC,UAAM,IAAI;AAAA,MACR;AAAA,IAEF;AAAA,EACF;AACA,QAAM,MAAM,WAAW,KAAK;AAC5B,QAAM,SAA6B;AAAA,IACjC,YAAY,CAAC;AAAA,IACb,GAAI,QAAQ,aAAa,SAAY,EAAE,UAAU,QAAQ,SAAS,IAAI,CAAC;AAAA,EACzE;AAEA,MAAI,MAAM,QAAQ,GAAG,GAAG;AACtB,qBAAiB,OAAO,YAAY,KAAK,OAAO;AAChD,WAAO;AAAA,EACT;AAEA,MAAI,OAAO,OAAO,QAAQ,UAAU;AAClC,UAAM,MAAM;AAEZ,QAAI,cAAc;AAClB,eAAW,OAAO,CAAC,cAAc,cAAc,UAAU,GAAY;AACnE,YAAM,IAAI,IAAI,GAAG;AACjB,UAAI,MAAM,QAAQ,CAAC,GAAG;AACpB,sBAAc;AACd,yBAAiB,OAAO,YAAY,GAAG,OAAO;AAAA,MAChD;AAAA,IACF;AAOA,QAAI,CAAC,aAAa;AAChB,YAAM,IAAI;AAAA,QACR;AAAA,MAGF;AAAA,IACF;AACA,WAAO;AAAA,EACT;AAUA,QAAM,IAAI,MAAM,0DAA0D,aAAa,GAAG,CAAC,EAAE;AAC/F;AAEA,SAAS,aAAa,OAAwB;AAC5C,MAAI,UAAU,KAAM,QAAO;AAC3B,SAAO,OAAO;AAChB;AAEA,SAAS,iBAAiB,MAA8B,KAAgB,SAAmC;AACzG,aAAW,SAAS,KAAK;AACvB,QAAI,CAAC,SAAS,OAAO,UAAU,UAAU;AACvC,UAAI,QAAQ,QAAQ;AAClB,cAAM,IAAI,MAAM,yCAAyC;AAAA,MAC3D;AACA;AAAA,IACF;AACA,UAAM,SAAS;AACf,QAAI,CAAC,QAAQ,iBAAiB,CAAC,eAAe,MAAM,EAAG;AACvD,4BAAwB,OAAO,IAAI;AACnC,SAAK,KAAK,MAAM;AAAA,EAClB;AACF;AAEA,SAAS,wBAAwB,OAAsB;AACrD,MAAI,UAAU,OAAW;AACzB,MAAI,OAAO,UAAU,YAAY,MAAM,KAAK,EAAE,WAAW,GAAG;AAC1D,UAAM,IAAI,MAAM,wDAAwD;AAAA,EAC1E;AACA,MAAI,CAAC,mBAAmB,KAAK,KAAK,GAAG;AACnC,UAAM,IAAI,MAAM,wDAAwD;AAAA,EAC1E;AACA,MAAI,wBAAwB,KAAK,MAAM,MAAM;AAC3C,UAAM,IAAI,MAAM,6DAA6D;AAAA,EAC/E;AACF;AAEA,SAAS,eAAe,QAAuC;AAC7D,MAAI,OAAO,WAAW,cAAe,QAAO;AAC5C,MAAI,OAAO,WAAW,OAAQ,QAAO;AACrC,MAAI,MAAM,QAAQ,OAAO,QAAQ,KAAK,OAAO,SAAS,KAAK,CAAC,MAAM,MAAM,iBAAiB,MAAM,MAAM,GAAG;AACtG,WAAO;AAAA,EACT;AACA,SAAO;AACT;AAMA,SAAS,WAAW,OAAyB;AAC3C,MAAI,OAAO,UAAU,UAAU;AAC7B,QAAI;AACF,aAAO,KAAK,MAAM,KAAK;AAAA,IACzB,SAAS,KAAK;AACZ,YAAM,IAAI,MAAM,oCAAoC,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG,CAAC,EAAE;AAAA,IACxG;AAAA,EACF;AACA,SAAO;AACT;AAOO,SAAS,kBAAkB,QAAkD;AAClF,MAAI,OAAO,OAAO,SAAS,UAAU;AACnC,UAAM,IAAI,OAAO,KAAK,KAAK;AAC3B,QAAI,EAAE,SAAS,EAAG,QAAO;AAAA,EAC3B;AACA,MAAI,OAAO,OAAO,UAAU,UAAU;AACpC,UAAM,IAAI,OAAO,MAAM,KAAK;AAC5B,QAAI,EAAE,WAAW,EAAG,QAAO;AAE3B,UAAM,WAAW,EAAE,QAAQ,yCAAyC,EAAE;AACtE,WAAO,SAAS,SAAS,IAAI,WAAW;AAAA,EAC1C;AACA,SAAO;AACT;;;ACnMO,IAAM,sBAAsB;AAYnC,IAAM,4BAA4B;AAM3B,SAAS,sBACd,QACA,UAAkC,CAAC,GACjB;AAClB,QAAM,MAAwB,CAAC;AAC/B,QAAM,MAAM,QAAQ;AACpB,QAAM,SAAS,QAAQ,mBAAmB;AAE1C,aAAW,UAAU,OAAO,YAAY;AACtC,QAAI,QAAQ,UAAa,IAAI,UAAU,IAAK,QAAO;AACnD,UAAM,SAAS,mBAAmB,QAAQ,OAAO,UAAU,MAAM;AACjE,QAAI,OAAQ,KAAI,KAAK,MAAM;AAAA,EAC7B;AACA,SAAO;AACT;AAEA,SAAS,mBACP,QACA,UACA,QAC4B;AAC5B,QAAM,SAAS,kBAAkB,MAAM;AACvC,MAAI,CAAC,UAAU,OAAO,SAAS,OAAQ,QAAO;AAE9C,QAAM,WAAoC,EAAE,MAAM,SAAS;AAC3D,MAAI,OAAO,OAAO,aAAa,YAAY,OAAO,SAAS,SAAS,GAAG;AACrE,aAAS,cAAc,OAAO;AAAA,EAChC;AACA,MAAI,MAAM,QAAQ,OAAO,SAAS,KAAK,OAAO,UAAU,SAAS,GAAG;AAClE,UAAM,gBAAgB,OAAO,UAAU;AAAA,MACrC,CAAC,MAAM,OAAO,EAAE,SAAS,YAAY,SAAS,KAAK,EAAE,IAAI;AAAA,IAC3D;AACA,QAAI,eAAe,KAAM,UAAS,WAAW,cAAc;AAAA,EAC7D;AAEA,SAAO;AAAA,IACL,SAAS;AAAA,IACT,aAAa;AAAA,IACb,GAAI,OAAO,SAAS,SAAY,EAAE,iBAAiB,OAAO,KAAK,IAAI,CAAC;AAAA,IACpE,GAAI,aAAa,SAAY,EAAE,kBAAkB,SAAS,IAAI,CAAC;AAAA,IAC/D;AAAA,EACF;AACF;;;AFvDO,IAAM,UAA+C;AAAA,EAC1D,MAAM;AAAA,EACN,aAAa;AAAA,EAEb,MAAM,OAAgB,SAAoD;AACxE,WAAO,kBAAkB,OAAO;AAAA,MAC9B,GAAI,SAAS,WAAW,SAAY,EAAE,QAAQ,QAAQ,OAAO,IAAI,CAAC;AAAA,MAClE,GAAI,SAAS,aAAa,SAAY,EAAE,UAAU,QAAQ,SAAS,IAAI,CAAC;AAAA,IAC1E,CAAC;AAAA,EACH;AAAA,EAEA,UACE,QACA,SACkB;AAClB,WAAO,sBAAsB,QAAQ;AAAA,MACnC,GAAI,SAAS,gBAAgB,SACzB,EAAE,aAAa,QAAQ,YAAY,IACnC,CAAC;AAAA,MACL,GAAI,SAAS,oBAAoB,SAC7B,EAAE,iBAAiB,QAAQ,gBAAgB,IAC3C,CAAC;AAAA,IACP,CAAC;AAAA,EACH;AAAA,EAEA,MAAM,QACJ,QACA,UAC8B;AAC9B,WAAO,mCAAmC,QAAQ,QAAQ;AAAA,EAC5D;AACF;AAGO,IAAM,gBAAgB;","names":[]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@remnic/import-gemini",
|
|
3
|
-
"version": "9.3.
|
|
3
|
+
"version": "9.3.612",
|
|
4
4
|
"description": "Import activity history from Google Takeout (Gemini Apps) exports into Remnic (issue #568)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -19,13 +19,13 @@
|
|
|
19
19
|
"provenance": true
|
|
20
20
|
},
|
|
21
21
|
"peerDependencies": {
|
|
22
|
-
"@remnic/core": "^9.3.
|
|
22
|
+
"@remnic/core": "^9.3.612"
|
|
23
23
|
},
|
|
24
24
|
"devDependencies": {
|
|
25
25
|
"tsup": "^8.0.0",
|
|
26
26
|
"tsx": "^4.0.0",
|
|
27
27
|
"typescript": "^5.7.0",
|
|
28
|
-
"@remnic/core": "9.3.
|
|
28
|
+
"@remnic/core": "9.3.612"
|
|
29
29
|
},
|
|
30
30
|
"license": "MIT",
|
|
31
31
|
"repository": {
|