@remnic/import-gemini 0.1.0 → 9.3.517

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Joshua Warren
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/dist/index.js CHANGED
@@ -4,6 +4,7 @@
4
4
  import { defaultWriteMemoriesToOrchestrator } from "@remnic/core";
5
5
 
6
6
  // src/parser.ts
7
+ var UTC_ISO_INSTANT_RE = /^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2})(?:\.(\d{1,3}))?(Z|[+-]00:00)$/;
7
8
  function parseGeminiExport(input, options = {}) {
8
9
  if (input === void 0 || input === null) {
9
10
  throw new Error(
@@ -54,9 +55,26 @@ function appendActivities(dest, src, options) {
54
55
  }
55
56
  const record = entry;
56
57
  if (!options.keepNonGemini && !isGeminiRecord(record)) continue;
58
+ validateGeminiTimestamp(record.time);
57
59
  dest.push(record);
58
60
  }
59
61
  }
62
+ function validateGeminiTimestamp(value) {
63
+ if (value === void 0) return;
64
+ if (typeof value !== "string" || value.trim().length === 0) {
65
+ throw new Error("Gemini activity time must be an ISO-8601 UTC timestamp");
66
+ }
67
+ const match = UTC_ISO_INSTANT_RE.exec(value);
68
+ if (!match) {
69
+ throw new Error("Gemini activity time must be an ISO-8601 UTC timestamp");
70
+ }
71
+ const milliseconds = (match[2] ?? "").padEnd(3, "0");
72
+ const canonical = `${match[1]}.${milliseconds}Z`;
73
+ const parsedMs = Date.parse(canonical);
74
+ if (!Number.isFinite(parsedMs) || new Date(parsedMs).toISOString() !== canonical) {
75
+ throw new Error("Gemini activity time must be a valid ISO-8601 UTC timestamp");
76
+ }
77
+ }
60
78
  function isGeminiRecord(record) {
61
79
  if (record.header === "Gemini Apps") return true;
62
80
  if (record.header === "Bard") return true;
@@ -139,7 +157,8 @@ var adapter = {
139
157
  },
140
158
  transform(parsed, options) {
141
159
  return transformGeminiExport(parsed, {
142
- ...options?.maxMemories !== void 0 ? { maxMemories: options.maxMemories } : {}
160
+ ...options?.maxMemories !== void 0 ? { maxMemories: options.maxMemories } : {},
161
+ ...options?.minPromptLength !== void 0 ? { minPromptLength: options.minPromptLength } : {}
143
162
  });
144
163
  },
145
164
  async writeTo(target, memories) {
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 });\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\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 dest.push(record);\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//\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":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remnic/import-gemini",
3
- "version": "0.1.0",
3
+ "version": "9.3.517",
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",
@@ -11,24 +11,21 @@
11
11
  "import": "./dist/index.js"
12
12
  }
13
13
  },
14
- "files": ["dist"],
15
- "scripts": {
16
- "build": "tsup src/index.ts --format esm --dts",
17
- "check-types": "tsc --noEmit",
18
- "test": "tsx --test src/adapter.test.ts src/parser.test.ts src/transform.test.ts",
19
- "prepublishOnly": "npm run build"
20
- },
14
+ "files": [
15
+ "dist"
16
+ ],
21
17
  "publishConfig": {
22
18
  "access": "public",
23
19
  "provenance": true
24
20
  },
25
- "dependencies": {
26
- "@remnic/core": "workspace:^"
21
+ "peerDependencies": {
22
+ "@remnic/core": "^9.3.517"
27
23
  },
28
24
  "devDependencies": {
29
25
  "tsup": "^8.0.0",
30
26
  "tsx": "^4.0.0",
31
- "typescript": "^5.7.0"
27
+ "typescript": "^5.7.0",
28
+ "@remnic/core": "9.3.517"
32
29
  },
33
30
  "license": "MIT",
34
31
  "repository": {
@@ -36,5 +33,18 @@
36
33
  "url": "https://github.com/joshuaswarren/remnic.git",
37
34
  "directory": "packages/import-gemini"
38
35
  },
39
- "keywords": ["remnic", "memory", "gemini", "google", "takeout", "import"]
40
- }
36
+ "keywords": [
37
+ "remnic",
38
+ "memory",
39
+ "gemini",
40
+ "google",
41
+ "takeout",
42
+ "import"
43
+ ],
44
+ "scripts": {
45
+ "build": "tsup src/index.ts --format esm --dts",
46
+ "precheck-types": "node ../../scripts/ensure-bench-build-deps.mjs",
47
+ "check-types": "tsc --noEmit",
48
+ "test": "tsx --test src/adapter.test.ts src/parser.test.ts src/transform.test.ts"
49
+ }
50
+ }