gemiterm 2.0.0 → 2.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (155) hide show
  1. package/README.md +32 -9
  2. package/package.json +13 -4
  3. package/src/cli/command-registry.ts +66 -62
  4. package/src/cli/commands/continue-command.ts +75 -1
  5. package/src/cli/commands/install-skills-command.ts +35 -0
  6. package/src/cli/commands/new-command.ts +62 -0
  7. package/src/cli/index.ts +19 -44
  8. package/src/cli/utils/long-arg-guard.ts +28 -0
  9. package/src/cli/utils/prompt-file.ts +26 -0
  10. package/src/infrastructure/cli-parser.ts +77 -0
  11. package/src/infrastructure/io.ts +9 -0
  12. package/src/services/skills-cli-driver.ts +58 -0
  13. package/src/services/skills-service.ts +52 -0
  14. package/.env +0 -2
  15. package/.github/workflows/manual-release.yml +0 -34
  16. package/.github/workflows/release.yml +0 -198
  17. package/.github/workflows/test.yml +0 -29
  18. package/.opencode/commands/opsx-apply.md +0 -152
  19. package/.opencode/commands/opsx-archive.md +0 -157
  20. package/.opencode/commands/opsx-explore.md +0 -169
  21. package/.opencode/commands/opsx-propose.md +0 -104
  22. package/.opencode/commands/opsx-sync.md +0 -140
  23. package/.opencode/commands/search-intelligently.md +0 -9
  24. package/.opencode/opencode.json +0 -20
  25. package/.opencode/skills/intellisearch/.version +0 -1
  26. package/.opencode/skills/intellisearch/SKILL.md +0 -84
  27. package/.opencode/skills/intellisearch/references/brave-search.md +0 -67
  28. package/.opencode/skills/intellisearch/references/ddg-search.md +0 -79
  29. package/.opencode/skills/intellisearch/references/deepwiki-tools.md +0 -246
  30. package/.opencode/skills/intellisearch/references/examples.md +0 -97
  31. package/.opencode/skills/intellisearch/references/gh-cli.md +0 -143
  32. package/.opencode/skills/intellisearch/references/google-search.md +0 -40
  33. package/.opencode/skills/intellisearch/references/search-workflow.md +0 -143
  34. package/.opencode/skills/intellisearch/references/workflow.md +0 -115
  35. package/.opencode/skills/openspec-apply-change/SKILL.md +0 -159
  36. package/.opencode/skills/openspec-archive-change/SKILL.md +0 -117
  37. package/.opencode/skills/openspec-explore/SKILL.md +0 -287
  38. package/.opencode/skills/openspec-propose/SKILL.md +0 -111
  39. package/.opencode/skills/openspec-sync-specs/SKILL.md +0 -147
  40. package/.serena/project.yml +0 -133
  41. package/AGENTS.md +0 -136
  42. package/CHANGELOG.md +0 -3
  43. package/bun.lock +0 -105
  44. package/docs/INSTALL.md +0 -185
  45. package/docs/PLAYWRIGHT_CLI_API.md +0 -306
  46. package/docs/python-release-for-reference.md +0 -175
  47. package/examples/sample_json_export.json +0 -32
  48. package/examples/sample_markdown_export.md +0 -221
  49. package/examples/usage_examples.sh +0 -222
  50. package/install.ps1 +0 -264
  51. package/install.sh +0 -275
  52. package/openspec/MAESTRO_MIGRATION.md +0 -180
  53. package/openspec/changes/archive/2026-06-08-command-spec-conformance/.openspec.yaml +0 -2
  54. package/openspec/changes/archive/2026-06-08-command-spec-conformance/design.md +0 -194
  55. package/openspec/changes/archive/2026-06-08-command-spec-conformance/proposal.md +0 -52
  56. package/openspec/changes/archive/2026-06-08-command-spec-conformance/specs/multi-profile-conversations/spec.md +0 -133
  57. package/openspec/changes/archive/2026-06-08-command-spec-conformance/tasks.md +0 -95
  58. package/openspec/changes/archive/2026-06-09-refactor-interactive-prompt-loop/.openspec.yaml +0 -2
  59. package/openspec/changes/archive/2026-06-09-refactor-interactive-prompt-loop/design.md +0 -28
  60. package/openspec/changes/archive/2026-06-09-refactor-interactive-prompt-loop/proposal.md +0 -22
  61. package/openspec/changes/archive/2026-06-09-refactor-interactive-prompt-loop/specs/interactive-prompt-loop/spec.md +0 -35
  62. package/openspec/changes/archive/2026-06-09-refactor-interactive-prompt-loop/tasks.md +0 -28
  63. package/openspec/changes/archive/2026-06-09-replace-gemini-api-placeholder-with-gemini-reverse/.openspec.yaml +0 -2
  64. package/openspec/changes/archive/2026-06-09-replace-gemini-api-placeholder-with-gemini-reverse/design.md +0 -323
  65. package/openspec/changes/archive/2026-06-09-replace-gemini-api-placeholder-with-gemini-reverse/proposal.md +0 -122
  66. package/openspec/changes/archive/2026-06-09-replace-gemini-api-placeholder-with-gemini-reverse/specs/no-capability-changes/spec.md +0 -29
  67. package/openspec/changes/archive/2026-06-09-replace-gemini-api-placeholder-with-gemini-reverse/tasks.md +0 -172
  68. package/openspec/changes/archive/2026-06-09-v2-install-migration/.openspec.yaml +0 -2
  69. package/openspec/changes/archive/2026-06-09-v2-install-migration/design.md +0 -241
  70. package/openspec/changes/archive/2026-06-09-v2-install-migration/proposal.md +0 -41
  71. package/openspec/changes/archive/2026-06-09-v2-install-migration/specs/v2-installer/spec.md +0 -205
  72. package/openspec/changes/archive/2026-06-09-v2-install-migration/tasks.md +0 -97
  73. package/openspec/changes/commander-cli-parser/.openspec.yaml +0 -2
  74. package/openspec/changes/commander-cli-parser/design.md +0 -107
  75. package/openspec/changes/commander-cli-parser/proposal.md +0 -31
  76. package/openspec/changes/commander-cli-parser/specs/cli/spec.md +0 -48
  77. package/openspec/changes/commander-cli-parser/tasks.md +0 -33
  78. package/openspec/changes/cross-platform-build-and-ci/.openspec.yaml +0 -2
  79. package/openspec/changes/cross-platform-build-and-ci/design.md +0 -213
  80. package/openspec/changes/cross-platform-build-and-ci/proposal.md +0 -50
  81. package/openspec/changes/cross-platform-build-and-ci/specs/path-and-file-mediation/spec.md +0 -335
  82. package/openspec/changes/cross-platform-build-and-ci/specs/release-pipeline/spec.md +0 -95
  83. package/openspec/changes/cross-platform-build-and-ci/tasks.md +0 -164
  84. package/openspec/config.yaml +0 -20
  85. package/openspec/specs/auth/spec.md +0 -394
  86. package/openspec/specs/cli/spec.md +0 -108
  87. package/openspec/specs/commands/spec.md +0 -409
  88. package/openspec/specs/configuration/spec.md +0 -130
  89. package/openspec/specs/conversations/spec.md +0 -139
  90. package/openspec/specs/domain-model/spec.md +0 -128
  91. package/openspec/specs/formatters/spec.md +0 -158
  92. package/openspec/specs/install-browser/spec.md +0 -146
  93. package/openspec/specs/interactive-prompt-loop/spec.md +0 -41
  94. package/openspec/specs/logger/spec.md +0 -71
  95. package/openspec/specs/mediator/spec.md +0 -72
  96. package/openspec/specs/multi-profile-conversations/spec.md +0 -145
  97. package/openspec/specs/path-utils/spec.md +0 -101
  98. package/openspec/specs/profiles/spec.md +0 -94
  99. package/openspec/specs/storage/spec.md +0 -198
  100. package/openspec/specs/testing/spec.md +0 -237
  101. package/openspec/specs/v2-installer/spec.md +0 -205
  102. package/openspec/specs/validators/spec.md +0 -65
  103. package/scripts/build.ts +0 -88
  104. package/scripts/clean-build.ps1 +0 -4
  105. package/scripts/clean-build.sh +0 -5
  106. package/scripts/install-browser.ps1 +0 -5
  107. package/scripts/install-browser.sh +0 -6
  108. package/scripts/lint-path-mediation.ps1 +0 -65
  109. package/scripts/lint-path-mediation.sh +0 -40
  110. package/tests/cli/auth-command.test.ts +0 -220
  111. package/tests/cli/command-registry.test.ts +0 -67
  112. package/tests/cli/continue-command.test.ts +0 -100
  113. package/tests/cli/delete-command.test.ts +0 -128
  114. package/tests/cli/export-all-command.test.ts +0 -375
  115. package/tests/cli/install-browser-command.test.ts +0 -48
  116. package/tests/cli/list-command.test.ts +0 -179
  117. package/tests/cli/new-command.test.ts +0 -139
  118. package/tests/cli/status-command.test.ts +0 -106
  119. package/tests/core/query-handlers.test.ts +0 -187
  120. package/tests/fixtures/auth-fixtures.ts +0 -98
  121. package/tests/fixtures/chat-fixtures.ts +0 -87
  122. package/tests/infrastructure/cli-table.test.ts +0 -107
  123. package/tests/infrastructure/formatters.test.ts +0 -202
  124. package/tests/infrastructure/io.test.ts +0 -197
  125. package/tests/infrastructure/logger.test.ts +0 -74
  126. package/tests/infrastructure/storage.test.ts +0 -335
  127. package/tests/infrastructure/validators.test.ts +0 -67
  128. package/tests/integration/commands/auth.test.ts +0 -186
  129. package/tests/integration/commands/continue.test.ts +0 -130
  130. package/tests/integration/commands/delete.test.ts +0 -140
  131. package/tests/integration/commands/export.test.ts +0 -266
  132. package/tests/integration/commands/fetch.test.ts +0 -238
  133. package/tests/integration/commands/list.test.ts +0 -281
  134. package/tests/integration/commands/profile.test.ts +0 -258
  135. package/tests/integration/commands/status.test.ts +0 -223
  136. package/tests/integration/installer-script-shape.test.ts +0 -67
  137. package/tests/parity/compare-outputs.ts +0 -359
  138. package/tests/parity/test-commands-parity.ps1 +0 -82
  139. package/tests/parity/test-commands-parity.sh +0 -61
  140. package/tests/services/auth-service.test.ts +0 -315
  141. package/tests/services/cookie-monitor.test.ts +0 -229
  142. package/tests/services/cookie-storage-service.test.ts +0 -230
  143. package/tests/services/gemini-client-wrapper.test.ts +0 -688
  144. package/tests/services/install-browser-service.test.ts +0 -83
  145. package/tests/services/playwright-cli-driver.test.ts +0 -468
  146. package/tests/services/profile-auth-manager.test.ts +0 -334
  147. package/tests/services/profile-service.test.ts +0 -224
  148. package/tests/setup.ts +0 -95
  149. package/tests/smoke/smoke.test.ts +0 -52
  150. package/tests/unit/config.test.ts +0 -159
  151. package/tests/unit/errors.test.ts +0 -119
  152. package/tests/unit/formatters.test.ts +0 -316
  153. package/tests/unit/mediator.test.ts +0 -152
  154. package/tests/unit/path-utils.test.ts +0 -289
  155. package/tsconfig.json +0 -16
@@ -1,35 +0,0 @@
1
- ## ADDED Requirements
2
-
3
- ### Requirement: Interactive prompt loop SHALL support continuous chat sessions
4
- The interactive prompt loop utility SHALL provide a reusable mechanism for chat commands to engage in continuous back-and-forth conversations with the user until explicitly terminated.
5
-
6
- #### Scenario: User sends multiple messages in a session
7
- - **WHEN** user starts an interactive session and enters a message
8
- - **THEN** the system processes the message and displays the response
9
- - **AND** the system prompts for the next message
10
-
11
- #### Scenario: User can exit the session
12
- - **WHEN** user types "/exit" or "/quit"
13
- - **THEN** the system displays "Goodbye." and terminates the session
14
-
15
- #### Scenario: Empty input is handled gracefully
16
- - **WHEN** user presses Enter without typing anything
17
- - **THEN** the system re-prompts without sending an empty message
18
-
19
- #### Scenario: Errors during message processing do not terminate the session
20
- - **WHEN** user enters a message but processing throws an error
21
- - **THEN** the system displays the error message
22
- - **AND** the system re-prompts for the next message
23
-
24
- ### Requirement: Interactive prompt loop SHALL support command-specific message handling
25
- The utility SHALL accept a callback function that handles each message, allowing different commands to process messages differently.
26
-
27
- #### Scenario: New chat command creates new conversation
28
- - **WHEN** the new command uses the interactive loop with a message handler
29
- - **AND** user enters a message
30
- - **THEN** the system starts a new conversation and displays the response
31
-
32
- #### Scenario: Continue command sends to existing conversation
33
- - **WHEN** the continue command uses the interactive loop with a message handler
34
- - **AND** user enters a message
35
- - **THEN** the system sends the message to the existing conversation ID
@@ -1,28 +0,0 @@
1
- ## 1. Create shared interactive prompt utility
2
-
3
- - [x] 1.1 Create `src/cli/utils/interactive-prompt.ts` module
4
- - [x] 1.2 Define `MessageHandler` callback interface for command-specific message processing
5
- - [x] 1.3 Implement `runInteractiveLoop` function that handles prompt loop mechanics
6
- - [x] 1.4 Ensure loop continues until /exit or /quit command
7
- - [x] 1.5 Handle empty input gracefully without sending
8
-
9
- ## 2. Refactor new-command.ts
10
-
11
- - [x] 2.1 Import the new `runInteractiveLoop` utility
12
- - [x] 2.2 Replace `startInteractive` method with call to `runInteractiveLoop`
13
- - [x] 2.3 Pass message handler callback that creates new chat conversations
14
- - [x] 2.4 Remove duplicated `startInteractive` method
15
- - [x] 2.5 Verify interactive mode loops correctly (not just one message)
16
-
17
- ## 3. Refactor continue-command.ts
18
-
19
- - [x] 3.1 Import the new `runInteractiveLoop` utility
20
- - [x] 3.2 Replace `startInteractive` method with call to `runInteractiveLoop`
21
- - [x] 3.3 Pass message handler callback that sends to existing conversation
22
- - [x] 3.4 Remove duplicated `startInteractive` method
23
-
24
- ## 4. Verify
25
-
26
- - [x] 4.1 Run `bun run test` to ensure all tests pass
27
- - [x] 4.2 Manually test `new` command interactive mode loops correctly
28
- - [x] 4.3 Manually test `continue` command interactive mode works correctly
@@ -1,323 +0,0 @@
1
- ## Context
2
-
3
- `src/services/gemini-client-wrapper.ts` is a 304-line placeholder that talks
4
- to `gemini.google.com` over `fetch()` with hand-typed `URLSearchParams` bodies
5
- and guessed endpoint paths. The class implements two interfaces:
6
-
7
- - `IGeminiClientService` (`src/core/command-handlers.ts:92`) — `deleteChat`,
8
- `sendMessage`, `startNewChat`, `profileHasConversation`, `forProfile`.
9
- - `IGeminiClientQueryService` (`src/core/query-handlers.ts:56`) — `listChats`,
10
- `fetchChat`, `listModels`.
11
-
12
- `GeminiClientService` is currently constructed in one place
13
- (`src/cli/index.ts:98,110`) and consumed in three (`src/cli/index.ts:116-153`,
14
- `src/services/profile-auth-manager.ts:22-54`, plus all command/query handlers
15
- that depend on the interfaces — never on the class directly).
16
-
17
- The proposed replacement is the npm package
18
- [`gemini-reverse`](https://www.npmjs.com/package/gemini-reverse) by
19
- [rynn-k](https://github.com/rynn-k/Gemini-Reverse). It exposes:
20
-
21
- - `class GeminiClient` with constructor
22
- `({ secure_1psid, secure_1psidts?, proxy?, cookies? })` and async
23
- `init({ timeout?, autoClose?, closeDelay?, autoRefresh?, refreshInterval?,
24
- verbose?, watchdogTimeout? })`.
25
- - `client.listChats(): ChatInfo[]` (fields: `cid`, `title`, `is_pinned`,
26
- `timestamp`).
27
- - `client.readChat(cid: string, limit?: number): Promise<ChatHistory | null>` —
28
- `ChatHistory.turns: { role, text, parts? }[]`.
29
- - `client.startChat(opts?): ChatSession` — session with
30
- `sendMessage({ prompt, … }): Promise<ModelOutput>`,
31
- `readHistory(limit?): Promise<ChatHistory | null>`, `cid`, `rcid`.
32
- - `client.generateContent({ prompt, … }): Promise<ModelOutput>` for
33
- single-shot.
34
- - `client.deleteChat(cid: string): Promise<void>`.
35
- - `client.listModels(): AvailableModel[]` (fields: `model_id`, `model_name`,
36
- `display_name`, `description`, `capacity`, `is_available`, …).
37
- - Error classes: `GeminiError` (base), `AuthError`, `APIError`, `TimeoutError`,
38
- `UsageLimitExceeded`, `TemporarilyBlocked`, `ModelInvalid`.
39
-
40
- `src/core/errors.ts` defines `GemitermError` (base), `AuthenticationError`,
41
- `CookieExpiredError`, `GeminiAPIError`, `ConversationNotFoundError`,
42
- `ConversationPendingError`. Callers expect these types in `try/catch` blocks
43
- (e.g. `command-handlers.ts:120`, `query-handlers.ts:138`).
44
-
45
- ## Goals / Non-Goals
46
-
47
- **Goals:**
48
-
49
- - Replace the placeholder `fetch()`-based HTTP client with a real Gemini web
50
- protocol client that talks to the live service.
51
- - Keep the public class signature of `GeminiClientService` and the two
52
- interfaces it implements bit-identical, so all 11 CLI commands, the
53
- `ProfileAuthManager`, and the command/query handlers need zero edits.
54
- - Translate `gemini-reverse` exceptions into our `GemitermError` subclasses so
55
- the existing error-handling in command handlers keeps working.
56
- - Preserve the per-profile session model: each profile gets its own
57
- `GeminiClient` (and therefore its own auto-refresh watchdog) bound to the
58
- cookies loaded by `CookieStorageService`.
59
- - Add a unit test file that mocks `gemini-reverse` and verifies the mapping
60
- and error translation in isolation, so we don't need a real Google account
61
- in CI.
62
- - Stay at the 432/432 test baseline (and add new tests on top of it).
63
-
64
- **Non-Goals:**
65
-
66
- - Surfacing `gemini-reverse` features the wrapper doesn't currently expose
67
- (Gems, file uploads, deep research, streaming, image/video output, candidate
68
- selection). These are deferred to a follow-up change.
69
- - Changing the cookie storage file format or the v1.4.1 → v2.0.0 migration
70
- story.
71
- - Touching any of the four regression-gate files flagged in `AGENTS.md`
72
- (`playwright-cli-driver.ts`, `cookie-monitor.ts`, `auth-service.ts`,
73
- `cookie-storage-service.ts`).
74
- - Resolving the existing `bun run build` breakage (that's the
75
- `cross-platform-build-and-ci` change).
76
- - Re-shaping `IGeminiClientService` or `IGeminiClientQueryService` — those
77
- are part of the `conversations` / `multi-profile-conversations` /
78
- `commands` capability contracts and are explicitly preserved.
79
-
80
- ## Decisions
81
-
82
- ### Decision 1: `gemini-reverse` over hand-rolled or other libraries
83
-
84
- We pick `gemini-reverse@^0.x` (pin the exact range in `tasks.md` based on
85
- current npm at implementation time) because:
86
-
87
- - It is the only widely-used library that wraps the live Gemini web protocol
88
- (same upstream as our `playwright-cli`-driven auth).
89
- - It handles cookie auto-refresh in the background (the upstream `1PSIDTS`
90
- cookie rotates on a timer; hand-rolling this is a known footgun).
91
- - It is pure JavaScript with no native bindings, so Bun resolves it cleanly.
92
-
93
- **Alternatives considered:**
94
-
95
- - *Port Python `gemiterm` v1.4.1's `gemini_webapi` usage.* Rejected: that
96
- project is the Python equivalent, not a Node package.
97
- - *Re-implement the HTTP client in TypeScript.* Rejected: the upstream
98
- protocol changes and the auth flow is non-trivial; this is the path that
99
- produced the placeholder we're replacing.
100
- - *Use Google's official `generativelanguage` SDK.* Rejected: that's a
101
- different product (AI Studio API key model) — `gemiterm` is explicitly a
102
- CLI for the consumer web app, not the AI Studio API.
103
-
104
- ### Decision 2: Compose `GeminiClient` per-instance, not per-CLI
105
-
106
- The current placeholder holds cookies on a `GeminiClientConfig` and uses
107
- `fetch()` directly. The new wrapper holds a `GeminiClient` instance from
108
- `gemini-reverse` as a private field, and calls `listChats()` / `readChat()` /
109
- `startChat()` / `deleteChat()` / `listModels()` on it.
110
-
111
- `forProfile(name)` constructs a brand-new `GeminiClientService` with a
112
- brand-new `GeminiClient` bound to the profile's cookies. This is what
113
- `gemini-reverse` is designed for (one client = one logged-in account) and
114
- naturally scopes the upstream auto-refresh watchdog to that profile.
115
-
116
- **Why not a single shared `GeminiClient` and pass cookies per call?** The
117
- upstream API bakes cookies into the client at construction time and runs the
118
- refresh watchdog against that single account. A shared client would mean the
119
- "current" profile is implicit state, and `forProfile()` could race the
120
- watchdog. Per-instance is the upstream-idiomatic and race-free model.
121
-
122
- ### Decision 3: Translation layer is a private helper, not a public module
123
-
124
- Mapping `gemini-reverse` types into our domain types (`ChatInfo`, `Message`,
125
- model display names) and translating exceptions into `GemitermError`
126
- subclasses lives as **private methods** on `GeminiClientService`, not as
127
- exported functions in a new module. Reasons:
128
-
129
- - Only `GeminiClientService` needs them.
130
- - Keeping the translation local makes the wrapper's contract surface minimal
131
- — `core/types.ts` stays the only public domain-types module, and
132
- `core/errors.ts` stays the only public error module.
133
- - It avoids leaking the `gemini-reverse` import path into more than one file
134
- (which simplifies future swaps if we ever need to replace this library
135
- again).
136
-
137
- The private helpers we will introduce:
138
-
139
- - `toDomainChatInfo(raw: geminiReverse.ChatInfo, profileName?: string):
140
- ChatInfo` — maps `cid → id`, `is_pinned → isPinned`, preserves `timestamp`
141
- and `title`, adds `profile` when `profileName` is set (matches existing
142
- placeholder behavior on `gemini-client-wrapper.ts:144`).
143
- - `toDomainMessages(history: geminiReverse.ChatHistory, conversationId:
144
- string): Message[]` — flattens `turns[].text ?? turns[].parts[].text` and
145
- normalizes `role` to `"user" | "model"`.
146
- - `toDomainModelName(model: geminiReverse.AvailableModel): string` — returns
147
- `display_name || model_name || model_id`.
148
- - `translateError(e: unknown): GemitermError` — see Decision 5.
149
-
150
- ### Decision 4: `init()` is exposed on the wrapper, not lazy
151
-
152
- The placeholder's constructor sets `this.authenticated = !!config.secure1psid`
153
- and never makes a network call. `gemini-reverse`'s `GeminiClient.init()` is
154
- async and does perform I/O (probe models, set up refresh watchdog). To keep
155
- the wrapper's construction synchronous (the factory in `src/cli/index.ts:98,110`
156
- calls `new GeminiClientService(…)` without `await`), we add an explicit async
157
- `init(): Promise<void>` on the wrapper that:
158
-
159
- 1. Awaits `this.client.init({ timeout: 300_000, autoClose: false,
160
- autoRefresh: true, refreshInterval: 540_000 })`.
161
- 2. Is called once at CLI startup (in `src/cli/index.ts` after the factory
162
- is constructed and before the first user command) — the call site edit
163
- is a 2-line addition.
164
- 3. Is idempotent (`this.initialized` guard) so re-entry from test setup is
165
- safe.
166
- 4. `forProfile()` returns a wrapper whose `init()` is also lazily awaited
167
- before its first call (or eagerly awaited in the same place; decided in
168
- the implementation based on call-site symmetry).
169
-
170
- The factory in `src/cli/index.ts` constructs the wrapper with
171
- `secure1psid: ""` (a placeholder until a profile is selected). For that
172
- factory instance, `init()` is called with empty cookies and we treat that as
173
- "deferred" — `init()` is still awaited, but the underlying `GeminiClient` is
174
- not used for I/O until a profile is selected (the per-profile wrappers
175
- constructed via `forProfile()` do the real I/O).
176
-
177
- **Why not lazy `init()` on first call?** Two reasons: (a) the existing
178
- `ProfileAuthManager` and command handlers can call methods concurrently
179
- during a multi-profile operation, and lazy init is racy without a mutex; (b)
180
- explicit init at startup makes the `await` visible in `src/cli/index.ts` and
181
- keeps the lifecycle linear.
182
-
183
- ### Decision 5: Error translation mapping
184
-
185
- Add a private `translateError(e: unknown): GemitermError` to the wrapper
186
- (per Decision 3). Mapping:
187
-
188
- | `gemini-reverse` class | Translated to | Message |
189
- |-------------------------|--------------------------------------|------------------------------------------------------|
190
- | `AuthError` | `AuthenticationError` | `"Session expired or invalid. Please run 'gemiterm login' again."` (matches the existing message at `gemini-client-wrapper.ts:83`) |
191
- | `TimeoutError` | `GeminiAPIError` | `"Request to Gemini timed out"` |
192
- | `UsageLimitExceeded` | `GeminiAPIError` | `"Gemini usage limit reached; try again later or switch model"` |
193
- | `TemporarilyBlocked` | `GeminiAPIError` | `"Temporarily blocked by Gemini; try a proxy or wait"` |
194
- | `ModelInvalid` | `GeminiAPIError` | `"Model is invalid or unavailable"` |
195
- | `APIError` | `GeminiAPIError` | `e.message` (with `cause = e`) |
196
- | any other `GeminiError` | `GeminiAPIError` | `e.message` (with `cause = e`) |
197
- | anything else | `GeminiAPIError` | `"Unexpected error: " + String(e)` |
198
-
199
- The `cause` field on `GeminiAPIError` is supported by `Error` since ES2022
200
- and is the right channel for the underlying error so the existing
201
- `logger.debug(\`${operation} failed: ${error}\`)` calls in
202
- command/query handlers still see the original message via
203
- `error.cause?.message`.
204
-
205
- We do **not** re-export `gemini-reverse` error classes from `core/errors.ts`.
206
- That would leak the library's type surface into our public module, which
207
- violates the "translate, don't re-export" principle and increases the blast
208
- radius of any future library swap.
209
-
210
- ### Decision 6: `sendMessage` and `startNewChat` return types stay text-only
211
-
212
- `IGeminiClientService.sendMessage(cid, msg): Promise<string>` and
213
- `startNewChat(msg): Promise<{ response: string; conversationId: string }>`
214
- return plain text today. `gemini-reverse` returns a rich `ModelOutput` with
215
- candidates, images, videos, thoughts, deep-research plans, and an `rcid` we
216
- can use as the conversation ID.
217
-
218
- We keep the wrappers text-only for this change and pull
219
- `output.text.toString()`. Surfacing candidates / media / `rcid` is a
220
- follow-up change (and a new capability spec) — it requires deciding how
221
- `gemiterm chat --json` should serialize them, which is out of scope here.
222
-
223
- For `startNewChat`, the `conversationId` we return comes from the
224
- `ChatSession.cid` after `client.startChat().sendMessage(...)` returns
225
- (equivalently `output.rcid` is also a valid candidate — we pick `cid` to
226
- match the existing `cid` field on `ChatInfo` from `listChats`).
227
-
228
- ### Decision 7: Test strategy — module mock, not real network
229
-
230
- Add `tests/services/gemini-client-wrapper.test.ts` that:
231
-
232
- - Uses `bun:test`'s `mock.module("gemini-reverse", …)` to install a fake
233
- `GeminiClient` class whose constructor records its arguments and whose
234
- methods return fixtures.
235
- - Spies on the fake's `listChats` / `readChat` / `startChat` / `deleteChat` /
236
- `listModels` / `init` methods to assert that the wrapper calls them with
237
- the right arguments and translates the return values into our domain
238
- types.
239
- - Asserts the `AuthError → AuthenticationError` and the other error
240
- translations by injecting the relevant `gemini-reverse` error class
241
- instances and catching them.
242
- - Verifies `forProfile(name)` constructs a *new* `GeminiClient` bound to
243
- that profile's cookies (asserted via a counter on the mock constructor).
244
-
245
- This mirrors the test style of
246
- `tests/services/cookie-storage-service.test.ts` and keeps the test
247
- self-contained (no network, no Playwright, no `playwright-cli`).
248
-
249
- The factory instance in `src/cli/index.ts:110` uses an empty
250
- `secure1psid: ""` and is used only as a `forProfile` factory; it is not
251
- exercised directly. We add a separate, small test for "empty-cookies client
252
- constructs but does not throw" to lock in the invariant.
253
-
254
- ### Decision 8: Dependency version
255
-
256
- Pin `gemini-reverse` to `^0.5.0` (or whatever the latest stable is at
257
- implementation time — task 1 of `tasks.md` re-validates this against
258
- `npm view gemini-reverse version` and updates the `package.json` entry
259
- accordingly). The `^0.5.0` placeholder is to convey "use the latest 0.x" in
260
- the `tasks.md` pin — implementers should re-check the registry.
261
-
262
- ## Risks / Trade-offs
263
-
264
- - **[Upstream library is unofficial / can break]** → Mitigation: we wrap it
265
- in `GeminiClientService` and translate errors. If `gemini-reverse`
266
- changes its public API, the blast radius is the wrapper file only. We
267
- also pin to `^0.x` (not `^1.0.0` yet) to acknowledge pre-1.0 churn.
268
- - **[Cookie auto-refresh race with multi-profile use]** → Mitigation:
269
- per-instance `GeminiClient` per profile (Decision 2). Each profile's
270
- watchdog runs against its own session, no shared state.
271
- - **[Bun resolution of npm package]** → Mitigation: `gemini-reverse` is
272
- pure JS (no native bindings, no `node-gyp`). Task 1 in `tasks.md` adds
273
- a `bun test` smoke step that imports the package from a placeholder
274
- `src/services/__smoke__/import-gemini-reverse.test.ts` to catch any
275
- resolution issue early. The smoke test is removed after the real tests
276
- are in place.
277
- - **[Loss of placeholder-specific `search` / `limit` / `offset` filtering
278
- in `listChats`]** → Mitigation: `gemini-reverse`'s `listChats()` returns
279
- a fully populated array. The wrapper applies the same post-filter (case-
280
- insensitive `search` on `title`, then `offset` slice, then `limit` slice)
281
- after mapping, preserving the existing behavior at
282
- `gemini-client-wrapper.ts:148-160`.
283
- - **[Empty-cookies factory instance crashes `init()`]** → Mitigation:
284
- `gemini-reverse`'s `init()` tolerates null/empty `secure_1psid` (it
285
- defers actual I/O until a method is called). We add a smoke test
286
- asserting the factory instance constructs and `init()` resolves without
287
- throwing.
288
- - **[Per-call `GeminiClient` construction is heavier than per-call `fetch`]**
289
- → Mitigation: this is a CLI invoked once per user command, not a
290
- long-running daemon. Construction cost is negligible.
291
- - **[Test count churn]** → The new test file adds tests but should not
292
- remove any of the 432 existing tests. `bun run test` after
293
- implementation must show `≥ 432` tests passing (target: 432 + ~25 new
294
- cases from the new file).
295
-
296
- ## Migration Plan
297
-
298
- - **Deploy**: this is a `v2.0.0` change; deploy with the normal release
299
- process (handled by the `cross-platform-build-and-ci` change once
300
- that's implemented — out of scope here).
301
- - **Rollback**: revert the single commit that swaps the wrapper; the
302
- previous `fetch()`-based implementation lives in `git` history. Cookie
303
- files in `%APPDATA%\gemiterm\` / `~/.config/gemiterm/` are unchanged.
304
- - **User action**: none. No re-auth, no config migration, no new flags.
305
- - **Operational note**: the first call after upgrade will hit
306
- `gemini-reverse`'s model probe and chat list, which performs a couple
307
- of HTTPS requests at startup. Latency is comparable to the placeholder.
308
-
309
- ## Open Questions
310
-
311
- - **Q1**: At implementation time, confirm the latest published
312
- `gemini-reverse` version on npm and pin accordingly (placeholder:
313
- `^0.5.0`).
314
- - **Q2**: Confirm Bun resolves `import { GeminiClient } from "gemini-reverse"`
315
- cleanly (no `bun install` quirks). This is the first non-Bun-first-party
316
- runtime dep we're pulling in.
317
- - **Q3**: Decide whether `init()` failure should be fatal at CLI startup
318
- or deferred until first use. Default plan (per Decision 4): fatal
319
- at startup, with the error mapped to `GeminiAPIError` and surfaced as
320
- the standard CLI error.
321
- - **Q4**: Should we expose a `--no-auto-refresh` flag to disable
322
- `gemini-reverse`'s cookie auto-refresh for debugging? Deferred to a
323
- follow-up; not in this change.
@@ -1,122 +0,0 @@
1
- ## Why
2
-
3
- `src/services/gemini-client-wrapper.ts` is a hand-rolled placeholder that talks
4
- to `gemini.google.com` over `fetch()` with hard-coded URL paths
5
- (`/app/api/chat/history`, `/app/api/chat/<id>/send`, `/app/api/models`, …) and
6
- URL-encoded form bodies. Those endpoints and shapes are educated guesses — the
7
- real Gemini web app uses a different RPC and streaming protocol, so today the
8
- CLI is structurally a stub against a real (changing) Google surface. We need
9
- to swap the in-tree HTTP client for the community-maintained
10
- [`gemini-reverse`](https://www.npmjs.com/package/gemini-reverse) library
11
- (rynn-k/Gemini-Reverse), which already implements the live Gemini web protocol
12
- (including cookie auto-refresh, streaming, model discovery, and per-account
13
- sessions). This is a prerequisite for actually shipping v2.0.0 against the
14
- real Gemini web app.
15
-
16
- ## What Changes
17
-
18
- - **Add `gemini-reverse` as a runtime dependency** in `package.json` and
19
- document the minimum supported version pinned in `tasks.md`.
20
- - **Replace the body of `src/services/gemini-client-wrapper.ts`** so that
21
- `GeminiClientService` composes a `GeminiClient` from `gemini-reverse`
22
- instead of issuing `fetch()` calls. The public class signature and the two
23
- interfaces it implements (`IGeminiClientService` in
24
- `src/core/command-handlers.ts`, `IGeminiClientQueryService` in
25
- `src/core/query-handlers.ts`) **do not change** — all call sites in
26
- `src/cli/index.ts`, `src/services/profile-auth-manager.ts`, and the command
27
- / query handlers keep working without edits.
28
- - **Map `gemini-reverse` types into our domain types** in
29
- `src/services/gemini-client-wrapper.ts`:
30
- - `ChatInfo` (ours) ← `ChatInfo` from `gemini-reverse` (`cid` → `id`,
31
- `is_pinned` → `isPinned`, `timestamp` preserved, optional `profile` added
32
- for the multi-profile case).
33
- - `Message[]` (ours) ← `ChatHistory.turns` from `readChat(cid, limit)` (role
34
- `"user"` vs `"model"`, content flattened from `text` / `parts[].text`,
35
- `conversationId` injected).
36
- - `string[]` of model display names (ours) ←
37
- `AvailableModel[]` from `listModels()` (use `display_name` with `model_id`
38
- as fallback).
39
- - **Map `gemini-reverse` errors into our error hierarchy** in
40
- `src/core/errors.ts` (re-export shim or constructor translation in the
41
- wrapper):
42
- - `AuthError` → `AuthenticationError` (with the same "run `gemiterm login`"
43
- message).
44
- - `UsageLimitExceeded` / `TemporarilyBlocked` / `ModelInvalid` →
45
- `GeminiAPIError` with a domain-specific message prefix.
46
- - `TimeoutError` → `GeminiAPIError` ("Request to Gemini timed out").
47
- - `APIError` / other `GeminiError` subclasses → `GeminiAPIError` with the
48
- original `message` and `cause` set to the underlying error.
49
- - **Preserve multi-profile behavior**: `forProfile(name)` still returns a new
50
- `GeminiClientService` instance bound to that profile's cookies loaded from
51
- `CookieStorageService`. The underlying `GeminiClient` is constructed
52
- per-profile, so the upstream cookie auto-refresh watchdog is scoped to that
53
- profile's session.
54
- - **Preserve initialization sequencing**: the wrapper's `init()` is called
55
- once after the `GeminiClient` is constructed and before any I/O. `init()`
56
- configures `timeout`, `autoRefresh: true`, `refreshInterval: 540000` (9
57
- minutes, matching upstream defaults), and `autoClose: false` so the process
58
- stays warm during a CLI session.
59
- - **Add a unit test file `tests/services/gemini-client-wrapper.test.ts`** that
60
- mocks the `gemini-reverse` `GeminiClient` class via a module mock and
61
- verifies the mapping/translation for `listChats`, `fetchChat`, `sendMessage`,
62
- `startNewChat`, `deleteChat`, `listModels`, `forProfile`, and the
63
- `AuthError → AuthenticationError` translation. Reuse the existing
64
- test-factory style from `tests/services/cookie-storage-service.test.ts`.
65
-
66
- No public CLI surface, command output, config file format, or persisted cookie
67
- file format changes. The v1.4.1 → v2.0.0 upgrade path stays intact.
68
-
69
- ## Capabilities
70
-
71
- ### New Capabilities
72
-
73
- None. This change is a drop-in implementation swap; no new user-visible
74
- capability is introduced. If we ever need to expose `gemini-reverse` features
75
- the wrapper doesn't currently surface (Gems, file uploads, deep research,
76
- streaming `--json`), those would be a separate change with their own
77
- `specs/<name>/spec.md`.
78
-
79
- ### Modified Capabilities
80
-
81
- None. The requirements in `openspec/specs/conversations/spec.md`,
82
- `openspec/specs/multi-profile-conversations/spec.md`, `openspec/specs/commands/`
83
- (every command that talks to Gemini), and `openspec/specs/auth/spec.md` are
84
- unaffected — the `IGeminiClientService` / `IGeminiClientQueryService` contract
85
- and observable behavior of every command are preserved. This is an
86
- implementation-detail refactor only.
87
-
88
- ## Impact
89
-
90
- - **Dependencies**: adds `gemini-reverse` to `dependencies` in
91
- `package.json`. Confirms Bun runtime can resolve and load the package (it's
92
- pure Node.js, no native bindings).
93
- - **Source files**:
94
- - **Modified**: `src/services/gemini-client-wrapper.ts` (full rewrite of
95
- internal implementation; public class signature unchanged).
96
- - **Modified**: `src/core/errors.ts` — add `translateGeminiError(e: unknown):
97
- GemitermError` helper (or extend the wrapper to perform the mapping
98
- inline; decided in `design.md`).
99
- - **New**: `tests/services/gemini-client-wrapper.test.ts`.
100
- - **Call sites (no edits expected, verified during implementation)**:
101
- - `src/cli/index.ts` (3× `IGeminiClientService` / `IGeminiClientQueryService`
102
- consumers and the factory `new GeminiClientService({ secure1psid: "" }, …)`).
103
- - `src/services/profile-auth-manager.ts` (consumes `IGeminiClientService`).
104
- - `src/core/command-handlers.ts`, `src/core/query-handlers.ts` (consume the
105
- interfaces, not the class).
106
- - **Sensitive-area check**: `AGENTS.md` flags four files as regression-gate
107
- files for auth flow; this change touches `src/services/gemini-client-wrapper.ts`
108
- only, which is **not** one of those four. The four flagged files
109
- (`playwright-cli-driver.ts`, `cookie-monitor.ts`, `auth-service.ts`,
110
- `cookie-storage-service.ts`) are not modified by this change. The wrapper
111
- consumes `CookieStorageService` via its existing constructor parameter, so
112
- the auth pipeline keeps working.
113
- - **Tests**: target stays at the 432/432 baseline. The new test file is
114
- expected to add coverage (post-implementation count > 432).
115
- - **Build**: `bun run build` (which currently fails for unrelated reasons
116
- captured in the `cross-platform-build-and-ci` change) is unaffected by
117
- this change.
118
- - **No new environment variables.** `gemini-reverse` honors
119
- `GEMINI_COOKIE_PATH`; we do not set it, so the library uses its default
120
- temp directory.
121
- - **No config-file or cookie-file format change.** v1.4.1 users upgrading
122
- to v2.0.0 will not need to re-authenticate.
@@ -1,29 +0,0 @@
1
- ## ADDED Requirements
2
-
3
- ### Requirement: This change introduces no new requirements
4
- This change is a pure implementation swap of the in-tree Gemini HTTP client
5
- (`src/services/gemini-client-wrapper.ts`) for the npm `gemini-reverse`
6
- library. The public contracts — `IGeminiClientService`,
7
- `IGeminiClientQueryService`, and the `GeminiClientService` class signature —
8
- are preserved bit-identical. No new user-visible capability is added, and no
9
- existing requirement in `openspec/specs/conversations/spec.md`,
10
- `openspec/specs/multi-profile-conversations/spec.md`, `openspec/specs/commands/`,
11
- or `openspec/specs/auth/spec.md` is modified. All other capabilities in
12
- `openspec/specs/` are likewise unaffected.
13
-
14
- #### Scenario: Public contract preserved
15
- - **WHEN** a caller imports `GeminiClientService`,
16
- `IGeminiClientService`, or `IGeminiClientQueryService` and uses the
17
- existing methods (`listChats`, `fetchChat`, `sendMessage`, `startNewChat`,
18
- `deleteChat`, `listModels`, `forProfile`, `profileHasConversation`)
19
- - **THEN** signatures, return types, and thrown error types are identical to
20
- the pre-change implementation; the 11 CLI commands, the
21
- `ProfileAuthManager`, and all command/query handlers compile and run
22
- without source edits
23
-
24
- #### Scenario: Per-profile sessions preserved
25
- - **WHEN** a caller invokes `geminiClient.forProfile("work")` and then
26
- `listChats()` on the returned instance
27
- - **THEN** the `ChatInfo[]` results are scoped to the "work" profile's
28
- Google account and carry `profile: "work"` on each entry, matching the
29
- existing behavior of the placeholder