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.
- package/README.md +32 -9
- package/package.json +13 -4
- package/src/cli/command-registry.ts +66 -62
- package/src/cli/commands/continue-command.ts +75 -1
- package/src/cli/commands/install-skills-command.ts +35 -0
- package/src/cli/commands/new-command.ts +62 -0
- package/src/cli/index.ts +19 -44
- package/src/cli/utils/long-arg-guard.ts +28 -0
- package/src/cli/utils/prompt-file.ts +26 -0
- package/src/infrastructure/cli-parser.ts +77 -0
- package/src/infrastructure/io.ts +9 -0
- package/src/services/skills-cli-driver.ts +58 -0
- package/src/services/skills-service.ts +52 -0
- package/.env +0 -2
- package/.github/workflows/manual-release.yml +0 -34
- package/.github/workflows/release.yml +0 -198
- package/.github/workflows/test.yml +0 -29
- package/.opencode/commands/opsx-apply.md +0 -152
- package/.opencode/commands/opsx-archive.md +0 -157
- package/.opencode/commands/opsx-explore.md +0 -169
- package/.opencode/commands/opsx-propose.md +0 -104
- package/.opencode/commands/opsx-sync.md +0 -140
- package/.opencode/commands/search-intelligently.md +0 -9
- package/.opencode/opencode.json +0 -20
- package/.opencode/skills/intellisearch/.version +0 -1
- package/.opencode/skills/intellisearch/SKILL.md +0 -84
- package/.opencode/skills/intellisearch/references/brave-search.md +0 -67
- package/.opencode/skills/intellisearch/references/ddg-search.md +0 -79
- package/.opencode/skills/intellisearch/references/deepwiki-tools.md +0 -246
- package/.opencode/skills/intellisearch/references/examples.md +0 -97
- package/.opencode/skills/intellisearch/references/gh-cli.md +0 -143
- package/.opencode/skills/intellisearch/references/google-search.md +0 -40
- package/.opencode/skills/intellisearch/references/search-workflow.md +0 -143
- package/.opencode/skills/intellisearch/references/workflow.md +0 -115
- package/.opencode/skills/openspec-apply-change/SKILL.md +0 -159
- package/.opencode/skills/openspec-archive-change/SKILL.md +0 -117
- package/.opencode/skills/openspec-explore/SKILL.md +0 -287
- package/.opencode/skills/openspec-propose/SKILL.md +0 -111
- package/.opencode/skills/openspec-sync-specs/SKILL.md +0 -147
- package/.serena/project.yml +0 -133
- package/AGENTS.md +0 -136
- package/CHANGELOG.md +0 -3
- package/bun.lock +0 -105
- package/docs/INSTALL.md +0 -185
- package/docs/PLAYWRIGHT_CLI_API.md +0 -306
- package/docs/python-release-for-reference.md +0 -175
- package/examples/sample_json_export.json +0 -32
- package/examples/sample_markdown_export.md +0 -221
- package/examples/usage_examples.sh +0 -222
- package/install.ps1 +0 -264
- package/install.sh +0 -275
- package/openspec/MAESTRO_MIGRATION.md +0 -180
- package/openspec/changes/archive/2026-06-08-command-spec-conformance/.openspec.yaml +0 -2
- package/openspec/changes/archive/2026-06-08-command-spec-conformance/design.md +0 -194
- package/openspec/changes/archive/2026-06-08-command-spec-conformance/proposal.md +0 -52
- package/openspec/changes/archive/2026-06-08-command-spec-conformance/specs/multi-profile-conversations/spec.md +0 -133
- package/openspec/changes/archive/2026-06-08-command-spec-conformance/tasks.md +0 -95
- package/openspec/changes/archive/2026-06-09-refactor-interactive-prompt-loop/.openspec.yaml +0 -2
- package/openspec/changes/archive/2026-06-09-refactor-interactive-prompt-loop/design.md +0 -28
- package/openspec/changes/archive/2026-06-09-refactor-interactive-prompt-loop/proposal.md +0 -22
- package/openspec/changes/archive/2026-06-09-refactor-interactive-prompt-loop/specs/interactive-prompt-loop/spec.md +0 -35
- package/openspec/changes/archive/2026-06-09-refactor-interactive-prompt-loop/tasks.md +0 -28
- package/openspec/changes/archive/2026-06-09-replace-gemini-api-placeholder-with-gemini-reverse/.openspec.yaml +0 -2
- package/openspec/changes/archive/2026-06-09-replace-gemini-api-placeholder-with-gemini-reverse/design.md +0 -323
- package/openspec/changes/archive/2026-06-09-replace-gemini-api-placeholder-with-gemini-reverse/proposal.md +0 -122
- package/openspec/changes/archive/2026-06-09-replace-gemini-api-placeholder-with-gemini-reverse/specs/no-capability-changes/spec.md +0 -29
- package/openspec/changes/archive/2026-06-09-replace-gemini-api-placeholder-with-gemini-reverse/tasks.md +0 -172
- package/openspec/changes/archive/2026-06-09-v2-install-migration/.openspec.yaml +0 -2
- package/openspec/changes/archive/2026-06-09-v2-install-migration/design.md +0 -241
- package/openspec/changes/archive/2026-06-09-v2-install-migration/proposal.md +0 -41
- package/openspec/changes/archive/2026-06-09-v2-install-migration/specs/v2-installer/spec.md +0 -205
- package/openspec/changes/archive/2026-06-09-v2-install-migration/tasks.md +0 -97
- package/openspec/changes/commander-cli-parser/.openspec.yaml +0 -2
- package/openspec/changes/commander-cli-parser/design.md +0 -107
- package/openspec/changes/commander-cli-parser/proposal.md +0 -31
- package/openspec/changes/commander-cli-parser/specs/cli/spec.md +0 -48
- package/openspec/changes/commander-cli-parser/tasks.md +0 -33
- package/openspec/changes/cross-platform-build-and-ci/.openspec.yaml +0 -2
- package/openspec/changes/cross-platform-build-and-ci/design.md +0 -213
- package/openspec/changes/cross-platform-build-and-ci/proposal.md +0 -50
- package/openspec/changes/cross-platform-build-and-ci/specs/path-and-file-mediation/spec.md +0 -335
- package/openspec/changes/cross-platform-build-and-ci/specs/release-pipeline/spec.md +0 -95
- package/openspec/changes/cross-platform-build-and-ci/tasks.md +0 -164
- package/openspec/config.yaml +0 -20
- package/openspec/specs/auth/spec.md +0 -394
- package/openspec/specs/cli/spec.md +0 -108
- package/openspec/specs/commands/spec.md +0 -409
- package/openspec/specs/configuration/spec.md +0 -130
- package/openspec/specs/conversations/spec.md +0 -139
- package/openspec/specs/domain-model/spec.md +0 -128
- package/openspec/specs/formatters/spec.md +0 -158
- package/openspec/specs/install-browser/spec.md +0 -146
- package/openspec/specs/interactive-prompt-loop/spec.md +0 -41
- package/openspec/specs/logger/spec.md +0 -71
- package/openspec/specs/mediator/spec.md +0 -72
- package/openspec/specs/multi-profile-conversations/spec.md +0 -145
- package/openspec/specs/path-utils/spec.md +0 -101
- package/openspec/specs/profiles/spec.md +0 -94
- package/openspec/specs/storage/spec.md +0 -198
- package/openspec/specs/testing/spec.md +0 -237
- package/openspec/specs/v2-installer/spec.md +0 -205
- package/openspec/specs/validators/spec.md +0 -65
- package/scripts/build.ts +0 -88
- package/scripts/clean-build.ps1 +0 -4
- package/scripts/clean-build.sh +0 -5
- package/scripts/install-browser.ps1 +0 -5
- package/scripts/install-browser.sh +0 -6
- package/scripts/lint-path-mediation.ps1 +0 -65
- package/scripts/lint-path-mediation.sh +0 -40
- package/tests/cli/auth-command.test.ts +0 -220
- package/tests/cli/command-registry.test.ts +0 -67
- package/tests/cli/continue-command.test.ts +0 -100
- package/tests/cli/delete-command.test.ts +0 -128
- package/tests/cli/export-all-command.test.ts +0 -375
- package/tests/cli/install-browser-command.test.ts +0 -48
- package/tests/cli/list-command.test.ts +0 -179
- package/tests/cli/new-command.test.ts +0 -139
- package/tests/cli/status-command.test.ts +0 -106
- package/tests/core/query-handlers.test.ts +0 -187
- package/tests/fixtures/auth-fixtures.ts +0 -98
- package/tests/fixtures/chat-fixtures.ts +0 -87
- package/tests/infrastructure/cli-table.test.ts +0 -107
- package/tests/infrastructure/formatters.test.ts +0 -202
- package/tests/infrastructure/io.test.ts +0 -197
- package/tests/infrastructure/logger.test.ts +0 -74
- package/tests/infrastructure/storage.test.ts +0 -335
- package/tests/infrastructure/validators.test.ts +0 -67
- package/tests/integration/commands/auth.test.ts +0 -186
- package/tests/integration/commands/continue.test.ts +0 -130
- package/tests/integration/commands/delete.test.ts +0 -140
- package/tests/integration/commands/export.test.ts +0 -266
- package/tests/integration/commands/fetch.test.ts +0 -238
- package/tests/integration/commands/list.test.ts +0 -281
- package/tests/integration/commands/profile.test.ts +0 -258
- package/tests/integration/commands/status.test.ts +0 -223
- package/tests/integration/installer-script-shape.test.ts +0 -67
- package/tests/parity/compare-outputs.ts +0 -359
- package/tests/parity/test-commands-parity.ps1 +0 -82
- package/tests/parity/test-commands-parity.sh +0 -61
- package/tests/services/auth-service.test.ts +0 -315
- package/tests/services/cookie-monitor.test.ts +0 -229
- package/tests/services/cookie-storage-service.test.ts +0 -230
- package/tests/services/gemini-client-wrapper.test.ts +0 -688
- package/tests/services/install-browser-service.test.ts +0 -83
- package/tests/services/playwright-cli-driver.test.ts +0 -468
- package/tests/services/profile-auth-manager.test.ts +0 -334
- package/tests/services/profile-service.test.ts +0 -224
- package/tests/setup.ts +0 -95
- package/tests/smoke/smoke.test.ts +0 -52
- package/tests/unit/config.test.ts +0 -159
- package/tests/unit/errors.test.ts +0 -119
- package/tests/unit/formatters.test.ts +0 -316
- package/tests/unit/mediator.test.ts +0 -152
- package/tests/unit/path-utils.test.ts +0 -289
- package/tsconfig.json +0 -16
|
@@ -1,194 +0,0 @@
|
|
|
1
|
-
## Context
|
|
2
|
-
|
|
3
|
-
The GemiTerm Bun TypeScript CLI completed its Maestro migration in the recent `v2-install-migration` and `cross-platform-build-and-ci` changes. Three small but real spec-conformance gaps remain in the `continue`, `delete`, and `list` commands that were called out in the Maestro Phase 4 plan (`Phase-04-Commands.md:11, 31, 47`):
|
|
4
|
-
|
|
5
|
-
1. `continue-command.ts:50-69` and `:75-130` never call `ProfileAuthManager.findProfileForConversation`; the `SendMessageCommand` is sent to the default profile regardless of where the conversation actually lives. In a multi-profile setup (e.g. a user with a `work` and a `personal` profile) the message goes to the wrong account.
|
|
6
|
-
2. `delete-command.ts:51-77` has the same defect for `DeleteConversationCommand`.
|
|
7
|
-
3. `list-command.ts:129-136` calls `formatChatList(chats)` without the new Profile column that the spec requires when `--all-profiles` is set. Multi-profile users can list chats from every profile but cannot tell which profile owns which conversation in the text output.
|
|
8
|
-
|
|
9
|
-
Compounding problem #1: `ProfileAuthManager.findProfileForConversation` in `src/services/profile-auth-manager.ts:46-60` is itself broken. The method body iterates over all profiles, checks `profileManager.getStatus(name).isActive` for each, and returns the **first** active profile. The `conversationId` argument is never used. The method signature is correct (`findProfileForConversation(conversationId: string): string | null`) but the implementation is the first-active-wins fallback, which is a silent no-op for single-profile users and an outright wrong-profile return for multi-profile users. The 8 unit tests in `tests/services/profile-auth-manager.test.ts:88-223` currently encode the BUGGY behavior; updating them is part of the fix.
|
|
10
|
-
|
|
11
|
-
Constraints:
|
|
12
|
-
- **SENSITIVE AREA** (per the maintainer's note): `src/services/playwright-cli-driver.ts`, `src/services/cookie-monitor.ts`, and `src/services/auth-service.ts` are off-limits. This change touches only `profile-auth-manager.ts`, `gemini-client-wrapper.ts`, the three CLI command files, `core/types.ts`, `core/command-handlers.ts` (interface only), `infrastructure/formatters.ts`, and the test files. No cookie-capture or browser-lifecycle code is modified.
|
|
13
|
-
- **Baseline gate:** 432/432 tests must continue to pass; the change adds new tests on top.
|
|
14
|
-
- **No CLI-breaking changes**: the `profile` field added to `ChatInfo` is optional, and the Profile column only renders when the user passes `--all-profiles`. Existing serialized JSON output is byte-compatible.
|
|
15
|
-
- **Public method signatures MUST NOT change** for `findProfileForConversation(conversationId: string): string | null` — it is wired into the registry and the test fixtures assert this exact shape.
|
|
16
|
-
|
|
17
|
-
Stakeholders: multi-profile end users (the primary beneficiaries of the fix), single-profile users (whose behavior must not change), maintainers reviewing the test-file changes (need clear documentation that the 8 changed tests document a bug, not a regression), and the auth/playwright maintainers (who do not need to touch their code).
|
|
18
|
-
|
|
19
|
-
## Goals / Non-Goals
|
|
20
|
-
|
|
21
|
-
**Goals:**
|
|
22
|
-
- `findProfileForConversation(conversationId)` MUST actually inspect each profile's chat list to find the one that owns the conversation; it MUST return `null` when no profile owns it.
|
|
23
|
-
- `continue` and `delete` commands MUST look up the owning profile before sending the `SendMessageCommand` / `DeleteConversationCommand`, MUST route the request to that profile's GeminiClient, and MUST surface a clear `AuthenticationError` with remediation guidance when no profile owns the conversation.
|
|
24
|
-
- `list --all-profiles` MUST render a Profile column in the text table and a `profile` field on each `ChatInfo` in the JSON output; `list` without `--all-profiles` MUST behave exactly as today (4 columns, no `profile` field on chats).
|
|
25
|
-
- The 8 existing `profile-auth-manager.test.ts` tests MUST be updated to encode the new correct behavior. The test count for that file increases (8 → 11+) after the change.
|
|
26
|
-
- The 432/432 baseline test count MUST continue to pass; the new tests are added on top.
|
|
27
|
-
|
|
28
|
-
**Non-Goals:**
|
|
29
|
-
- Modifying `src/services/playwright-cli-driver.ts`, `src/services/cookie-monitor.ts`, or `src/services/auth-service.ts`. The cookie-capture and browser-lifecycle paths are not touched.
|
|
30
|
-
- Auto-migrating the per-profile conversation cache across logins. The cache is built fresh on every `findProfileForConversation` call (or per-session, if the simpler in-memory cache is chosen).
|
|
31
|
-
- Adding a new `--profile <name>` flag to `continue` or `delete` for explicit override. The error message in the help text mentions it as a possible follow-up, but it is not implemented in this change.
|
|
32
|
-
- Changing the public surface of `IGeminiClientService` in a breaking way. The new per-profile routing is additive: either the interface gains a `profileName` field on the existing payload, or a profile-scoped client constructor is added; either way, all existing callers keep working.
|
|
33
|
-
- Multi-profile interactive flows (a session that switches profiles mid-conversation). Out of scope; the change is for one-shot `continue` and `delete` invocations.
|
|
34
|
-
|
|
35
|
-
## Decisions
|
|
36
|
-
|
|
37
|
-
### D1. `findProfileForConversation` uses a per-profile chat-list lookup, not first-active-wins
|
|
38
|
-
|
|
39
|
-
The current implementation at `src/services/profile-auth-manager.ts:46-60` returns the first active profile in the loop, ignoring `conversationId`. The new implementation iterates over all profiles, calls a new `profileHasConversation(profileName, conversationId): Promise<boolean>` helper on `GeminiClientService` for each, and returns the first profile that returns `true`. If no profile returns `true`, the method returns `null` (the caller's cue to throw `AuthenticationError`).
|
|
40
|
-
|
|
41
|
-
This is the only correct semantic: a profile's `isActive` flag (cookies are fresh) tells you nothing about whether the conversation lives in that profile. The current code confuses "active" (has valid cookies) with "owns the conversation" (the conversation ID is in that profile's server-side chat list). These are independent properties.
|
|
42
|
-
|
|
43
|
-
**Why this approach over a fingerprint in the cookies themselves?** The conversation ID is generated server-side by Gemini; the local cookies do not contain it. There is no client-side signal that says "this conversation belongs to this account". The only authoritative answer is the server, and the only way to ask the server is to use that profile's cookies to call `listChats()` and look for the ID. The cookie-based cache path (D2) optimizes this for the common case.
|
|
44
|
-
|
|
45
|
-
**Alternative considered:** Read the profile's `storage_state.json` and look for the conversation ID in the cookies' metadata. Rejected: the conversation ID is server-side state, not stored in the cookies.
|
|
46
|
-
|
|
47
|
-
**Alternative considered:** Embed a profile-aware lookup in the GeminiClientService constructor and route every command through it. Rejected: a step too far for this change; see D3 for the routing decision.
|
|
48
|
-
|
|
49
|
-
### D2. Conversation→profile cache is built on first lookup, scoped to the call
|
|
50
|
-
|
|
51
|
-
The naive implementation of D1 calls `listChats()` once per profile, which is N network round-trips (where N is the number of active profiles). For a user with 3 active profiles, `continue` would issue 3 GETs to `gemini.google.com/app/api/chat/history` before sending the message. This is unacceptable latency on every `continue` and `delete` call.
|
|
52
|
-
|
|
53
|
-
Mitigation: an in-memory `Map<string, string>` (conversationId → profileName) is built once per `findProfileForConversation` call by iterating over all profiles, calling `listChats()` for each, and recording each conversation ID's owning profile. The cache is then queried in a single `Map.get` per profile. Total work: still N `listChats` calls, but the iteration is `O(N + C)` where `C` is the total number of chats across all profiles, and the per-iteration cost is a `Map.has` check rather than a full loop. The cache is also reusable: a subsequent `findProfileForConversation` call within the same CLI invocation can reuse the cache (a follow-up task; this change scopes the cache to a single call for simplicity and to avoid stale-cache bugs after a delete).
|
|
54
|
-
|
|
55
|
-
The cache is not persisted to disk. The Gemini conversation list is fetched fresh on every CLI invocation; the cache is purely a per-call optimization.
|
|
56
|
-
|
|
57
|
-
**Alternative considered:** Persist the cache to disk under `%APPDATA%\gemiterm\conversation-cache.json` and reuse it across invocations. Rejected: stale-cache risk. A user who deletes a conversation from `work` would have the cache report the conversation as still being in `work` on the next `gemiterm delete`. Refreshing the cache is the same cost as the fresh call; persistence is not worth the staleness surface.
|
|
58
|
-
|
|
59
|
-
**Alternative considered:** Build the cache on first `list-chats` query (which already iterates per profile in `--all-profiles` mode) and stash it in the mediator. Rejected: couples `list` and `continue`/`delete` lifecycle. The per-call cache is self-contained and easy to reason about.
|
|
60
|
-
|
|
61
|
-
### D3. Per-profile routing is exposed via an additive `profileName` field on the command payloads, not a profile-scoped GeminiClient constructor
|
|
62
|
-
|
|
63
|
-
The `IGeminiClientService` interface in `src/core/command-handlers.ts:90-94` exposes `deleteChat`, `sendMessage`, and `startNewChat` — none of which takes a profile. The `IGeminiClientQueryService` in `src/core/query-handlers.ts:56-60` exposes `listChats`, `fetchChat`, `listModels` — also none takes a profile. To route `continue` to a non-default profile, one of two shapes is needed:
|
|
64
|
-
|
|
65
|
-
(a) **Additive `profileName` field** on `SendMessageCommandPayload` and `DeleteConversationCommandPayload`. The `SendMessageCommandHandler` and `DeleteConversationCommandHandler` read `payload.profileName` and forward it to a per-profile-aware method on the service (or to a factory that builds a per-profile `GeminiClientService`).
|
|
66
|
-
|
|
67
|
-
(b) **Profile-scoped client constructor** on the command. The command builds a `GeminiClientService` with the resolved profile's cookies (`cookieStorageService.loadCookiesForProfile(profileName)`) and passes it explicitly to the mediator or to a new `IGeminiClientService.forProfile(name)` method.
|
|
68
|
-
|
|
69
|
-
The chosen approach is **(a) with a thin factory helper**: add `profileName?: string` to `SendMessageCommandPayload` and `DeleteConversationCommandPayload`; add a `forProfile(name: string)` method to `GeminiClientService` that returns a new instance configured with that profile's cookies (does not mutate `this`); the `SendMessageCommandHandler` and `DeleteConversationCommandHandler` resolve the profile name from the payload, build the per-profile client via `forProfile`, and call its `sendMessage` / `deleteChat`. The mediator continues to own the **default-profile** client for backwards compatibility, and the command payload carries the **explicit** profile for multi-profile routing.
|
|
70
|
-
|
|
71
|
-
Why (a) over (b):
|
|
72
|
-
- (a) keeps the mediator wiring unchanged for the default profile. The existing `SendMessageCommandHandler` is constructed with a single `IGeminiClientService` in `src/cli/index.ts` (or wherever the handlers are registered) and the existing flow keeps working.
|
|
73
|
-
- (a) localizes the per-profile routing inside the handler. The handler is the only place that needs to know "this is a multi-profile call", which is exactly where the knowledge belongs.
|
|
74
|
-
- (b) would require either passing a per-profile client into the command's `execute(args, context)` (a new `CliCommandContext` field — invasive) or building a per-profile client inside the command and calling the mediator with a custom command handler. Both are more wiring than (a).
|
|
75
|
-
- The `forProfile` factory method on `GeminiClientService` is a small, additive change to the class (it does not change the existing constructor signature).
|
|
76
|
-
|
|
77
|
-
**Alternative considered:** Construct the per-profile client inside the command and call the method directly (bypass the mediator for the actual API call). Rejected: breaks the mediator pattern that the rest of the CLI uses; the `IGeminiClientService` injection at the handler boundary is the right place to inject the per-profile instance.
|
|
78
|
-
|
|
79
|
-
**Alternative considered:** Make `IGeminiClientService` carry a `forProfile` method, and have the handler call it on its injected instance. The injected instance is the default-profile client; `forProfile` returns a new instance. This is the same as (a) — the payload's `profileName` field is the trigger. The "expose via interface" detail is implementation, not design.
|
|
80
|
-
|
|
81
|
-
### D4. `ChatInfo.profile` is optional; `formatChatList` only renders the column when the flag is set
|
|
82
|
-
|
|
83
|
-
The `ChatInfo` type at `src/core/types.ts:9-14` gains `profile?: string`. The `ListChatsQueryResult.chats` array is unchanged in shape (each chat may or may not have a `profile` field). The `ListChatsQueryHandler` propagates the profile name to each chat when `payload.allProfiles === true`; otherwise, the chats are returned without a `profile` field (matches today's behavior exactly).
|
|
84
|
-
|
|
85
|
-
`formatChatList(chats)` keeps its 4-column layout (ID / TITLE / DATE / PIN) when called with no second argument — every existing caller's output is byte-identical. A new optional second argument `options?: { includeProfileColumn?: boolean }` flips on a 5th `PROFILE` column (width 14 chars, matching the style of the other columns).
|
|
86
|
-
|
|
87
|
-
`list-command.ts:130` changes from `formatChatList(chats)` to `formatChatList(chats, { includeProfileColumn: options.allProfiles })`. The flag is plumbed through cleanly.
|
|
88
|
-
|
|
89
|
-
**Why the column is gated on `--all-profiles`:** In single-profile mode the Profile column would always show the same value for every row — a column of identical strings is noise. The column only earns its space in multi-profile mode where the rows differ.
|
|
90
|
-
|
|
91
|
-
**Why the JSON `profile` field is also gated:** JSON consumers (scripts, future web tools) that only deal with single-profile flows would see a `profile: "default"` field appear on every chat after this change, even though they don't need it. Gating on `allProfiles` keeps the JSON shape byte-compatible for the single-profile case.
|
|
92
|
-
|
|
93
|
-
**Alternative considered:** Always include the `profile` field on every chat in JSON output. Rejected: changes the JSON contract for single-profile users. Backward compat wins.
|
|
94
|
-
|
|
95
|
-
**Alternative considered:** Always include the `profile` field, default to the active profile name. Rejected: same reasoning — single-profile users don't need the field, and the field is undefined for any chat fetched without `--all-profiles`.
|
|
96
|
-
|
|
97
|
-
### D5. The 8 existing `profile-auth-manager.test.ts` tests are updated; the change IS the fix
|
|
98
|
-
|
|
99
|
-
The 8 unit tests in `tests/services/profile-auth-manager.test.ts:88-223` (in the `describe("findProfileForConversation")` block) currently assert:
|
|
100
|
-
- "returns first active profile" — the BUG.
|
|
101
|
-
- "returns null when no active profiles" — still correct under the new implementation.
|
|
102
|
-
- "returns null when no profiles exist" — still correct under the new implementation.
|
|
103
|
-
|
|
104
|
-
The first test encodes the bug. Updating it to assert "returns the profile whose chat list contains the conversation ID" is the fix, not a regression. The new test ("returns first active profile" → "returns the profile that owns the conversation") is part of the 3-4 new tests added in the same `describe` block, and the test count goes from 8 → 11+.
|
|
105
|
-
|
|
106
|
-
The PR body MUST explicitly call this out: "the 8 existing tests document the bug; updating them to encode the correct per-profile lookup is the fix." The task list (group 3) includes a "document the test change in the test file's leading comment" sub-task to make this discoverable to reviewers.
|
|
107
|
-
|
|
108
|
-
**Why we don't keep the old test as a separate `@deprecated` block:** it asserts wrong behavior. Leaving it in place would be a perpetual foot-gun for any future maintainer who runs `bun test` and sees a green light while the bug is present in production. The right move is to delete the wrong assertion and replace it with the correct one.
|
|
109
|
-
|
|
110
|
-
### D6. When no profile owns the conversation, the command throws `AuthenticationError` with a remediation message
|
|
111
|
-
|
|
112
|
-
If `findProfileForConversation(conversationId)` returns `null`, the command (`continue` or `delete`) throws `AuthenticationError` with this message:
|
|
113
|
-
|
|
114
|
-
> "Could not find a profile that owns conversation '<id>'. Run 'gemiterm list --all-profiles' to see which profile it belongs to, then 'gemiterm continue <id> <msg> --profile <name>' to specify the profile explicitly."
|
|
115
|
-
|
|
116
|
-
The error is thrown at the command layer (after the lookup, before the mediator send), so the user sees the message in the standard error-handling path that all CLI commands use (the `try/catch` in `continue-command.ts:112-116` and `delete-command.ts:77-81`). The exit code is non-zero (the existing error path sets `process.exitCode = 1` or `process.exit(1)`).
|
|
117
|
-
|
|
118
|
-
**Why not silently fall back to the default profile:** that hides the bug. A user with two profiles, a typo'd conversation ID, and a silent fallback to the default profile would conclude "I sent a message to a non-existent chat and the server accepted it" — worse than a hard error. The hard error tells the user exactly what went wrong and how to fix it.
|
|
119
|
-
|
|
120
|
-
**Why `AuthenticationError` specifically (not `ValidationError` or a new error class):** the conversation-ownership check is logically a "is this profile authenticated to act on this conversation" check; the existing `AuthenticationError` class in `src/core/errors.ts` is the right semantic. The error code stays the same so existing error-handling middleware doesn't need to change.
|
|
121
|
-
|
|
122
|
-
**Alternative considered:** Print a warning and fall back to the default profile. Rejected: see above — silently doing the wrong thing is worse than failing loud.
|
|
123
|
-
|
|
124
|
-
**Alternative considered:** Introduce a new `ConversationNotFoundError` class. Rejected: the message is the same shape as `AuthenticationError`, the user's remediation is the same, and adding a new error class for one call site is over-engineering. The follow-up could add `--profile <name>` to override, and the error class can be split later if needed.
|
|
125
|
-
|
|
126
|
-
### D7. No separate test file for `profileHasConversation`; coverage is via the integration tests for `continue` and `delete`
|
|
127
|
-
|
|
128
|
-
`GeminiClientService` (`src/services/gemini-client-wrapper.ts`) has no direct unit tests today; the existing coverage for its methods is the integration test suites for `list`, `fetch`, and the other commands that go through the mediator. Adding a unit test file `tests/services/gemini-client-wrapper.test.ts` is a separate change (the maintainer can add one when direct service-level unit tests are needed across the board). For this change, the new `profileHasConversation` helper is exercised by:
|
|
129
|
-
|
|
130
|
-
- `tests/integration/commands/continue.test.ts` (new tests in group 10 of `tasks.md`): 2 tests covering "resolves the profile that owns the conversation" and "errors when no profile owns the conversation".
|
|
131
|
-
- `tests/integration/commands/delete.test.ts` (new tests in group 10): 2 tests covering the analogous delete paths.
|
|
132
|
-
- `tests/services/profile-auth-manager.test.ts` (new tests in group 3): 3-4 tests that mock `GeminiClientService.profileHasConversation` to return `true`/`false` and assert the `findProfileForConversation` behavior.
|
|
133
|
-
|
|
134
|
-
The mock-based unit tests for `findProfileForConversation` are the primary gate for the helper's behavior, and the integration tests for `continue` and `delete` are the gate for the end-to-end flow.
|
|
135
|
-
|
|
136
|
-
**Why no separate test file for `profileHasConversation`:** the helper is one method on `GeminiClientService`; unit-testing it would require mocking the entire HTTP layer (the existing service uses `fetch` directly, not a `HttpClient` interface). The mock surface is larger than the code being tested, which is a smell. The integration coverage is the right gate for this change.
|
|
137
|
-
|
|
138
|
-
**Alternative considered:** Mock `fetch` in a `tests/services/gemini-client-wrapper.test.ts` test. Rejected: too much mock surface for a 1-line helper; the integration tests are sufficient and more representative of real usage.
|
|
139
|
-
|
|
140
|
-
## Risks / Trade-offs
|
|
141
|
-
|
|
142
|
-
- **Risk:** The cookie-based conversation→profile cache is built fresh on every `findProfileForConversation` call, which is N `listChats` calls. → **Mitigation:** the cache is built once per call, so the cost is bounded; subsequent calls within the same CLI invocation can short-circuit on a hit. For the typical `gemiterm continue <id> <msg>` invocation (one call, two profiles), the latency is 2 GETs + 1 POST. The optimization is to keep the cache in the `Mediator` instance for the lifetime of the CLI process (a follow-up; not in this change because it changes the mediator contract).
|
|
143
|
-
|
|
144
|
-
- **Risk:** The 8 existing `profile-auth-manager` tests changing behavior could be flagged as a "test regression" by code reviewers who skim the diff. → **Mitigation:** the test file's leading comment (added in group 3, sub-task 3.1 of `tasks.md`) explicitly states: "The 8 tests in `describe('findProfileForConversation')` previously asserted the BUGGY 'first active profile' behavior; they have been updated to assert the CORRECT per-profile-lookup behavior. See `openspec/changes/command-spec-conformance/proposal.md` for context." The PR body will also call this out.
|
|
145
|
-
|
|
146
|
-
- **Risk:** Adding `profile?: string` to `ChatInfo` is a type change that could ripple to consumers that do `exactOptionalPropertyTypes` checks. → **Mitigation:** the field is optional and unset when `allProfiles` is false; consumers that ignore unknown fields (the standard JSON behavior) keep working. The 432/432 baseline test count includes consumers that iterate `chats` and assert specific fields — those tests are the regression gate for the type change.
|
|
147
|
-
|
|
148
|
-
- **Risk:** Multi-profile users on WSL (where Chromium is WSL-local, per `src/services/install-browser-service.ts:124-141`) might see inconsistent results if one profile's cookies are for a host-Windows Chromium and another is for a WSL-Linux Chromium. → **Mitigation:** out of scope. The existing single-profile path already handles WSL cookie storage (the per-profile lookup is the same code path, just iterating N profiles instead of 1).
|
|
149
|
-
|
|
150
|
-
- **Risk:** The `SendMessageCommandPayload.profileName` and `DeleteConversationCommandPayload.profileName` fields are optional, but a future change could add a third command that needs the field and forget to thread it. → **Mitigation:** the spec (`specs/multi-profile-conversations/spec.md`) explicitly states the routing contract for the three commands in this change. The `IGeminiClientService` interface does not grow a profile parameter (D3), so future commands follow the same pattern by adding `profileName` to their payload and using `forProfile` on the injected client.
|
|
151
|
-
|
|
152
|
-
- **Risk:** The `formatChatList` signature change (adding an optional second arg) could be a TypeScript error in any consumer that calls `formatChatList(chats)` with a different second-arg shape. → **Mitigation:** the new parameter is a single object `{ includeProfileColumn?: boolean }`; TypeScript allows passing no argument or passing an object literal; existing calls (`formatChatList(chats)`) compile without change. The `bun test` baseline run is the regression gate.
|
|
153
|
-
|
|
154
|
-
- **Trade-off:** The per-profile cache is per-call, not per-session. → **Accepted:** a per-session cache requires the `Mediator` to know about the cache, which couples concerns. The per-call cost is bounded (N `listChats` calls per `findProfileForConversation` call, where N is the number of active profiles — usually 1 or 2). The follow-up to make the cache per-session is a small change to the mediator's constructor; it can be done in a separate change without breaking the API.
|
|
155
|
-
|
|
156
|
-
- **Trade-off:** The `forProfile` method on `GeminiClientService` creates a new instance per call. Each instance is a thin wrapper around the profile's cookies; the underlying `fetch` is created per request, not per instance. → **Accepted:** the alternative (caching a single `fetch` and swapping cookies per request) is more complex for no measurable gain; the per-call cost is one object allocation and one method call.
|
|
157
|
-
|
|
158
|
-
## Migration Plan
|
|
159
|
-
|
|
160
|
-
The change is **purely additive at the public surface** and **behavior-changing at the fix boundary**. Deploy steps:
|
|
161
|
-
|
|
162
|
-
1. Land the change behind a feature branch.
|
|
163
|
-
2. Run `bun test` and confirm the 432/432 baseline still passes; the new tests raise the count to 432+12 = 444+ (8 updated profile-auth-manager tests + 3-4 new = 11-12 in that file; 2+2 new continue/delete integration tests; 2 new list integration tests).
|
|
164
|
-
3. Land on `main` after CI passes. No data migration is required: the profile storage layout, cookie storage layout, and config dir layout are unchanged. The `ChatInfo` JSON output for single-profile users is byte-identical (the `profile` field is absent).
|
|
165
|
-
4. Update the user-facing `gemiterm --help` and the README to mention the new Profile column in `list --all-profiles` output (a one-line addition to the existing `--all-profiles` flag description in `list-command.ts:215`).
|
|
166
|
-
|
|
167
|
-
**Rollback strategy:**
|
|
168
|
-
- Revert the commit. The `profile?: string` field on `ChatInfo` is the only additive type change; reverting removes it, which is a non-breaking deletion for consumers that ignore unknown fields. The `findProfileForConversation` fix is the load-bearing piece; reverting restores the buggy behavior, but the 432 baseline is preserved (the existing tests that encoded the bug still pass).
|
|
169
|
-
- No data is at risk. The change does not modify any on-disk state.
|
|
170
|
-
|
|
171
|
-
**Deploy verification:**
|
|
172
|
-
- A multi-profile user (`gemiterm auth --profile work && gemiterm auth --profile personal`, then `gemiterm list --all-profiles`) sees a Profile column. The JSON output includes `profile: "work"` / `profile: "personal"` on each chat.
|
|
173
|
-
- A `gemiterm continue <id> <msg>` against a conversation owned by `work` actually sends to `work` (verified by reading the response and confirming it references the work account's data).
|
|
174
|
-
- A `gemiterm continue <unknown-id> <msg>` exits non-zero with the remediation message.
|
|
175
|
-
- A `gemiterm delete <id> --force` against a conversation owned by `work` deletes from `work`, not from `personal`.
|
|
176
|
-
- A single-profile user (the 99% case) sees no change in any output shape.
|
|
177
|
-
|
|
178
|
-
## Open Questions
|
|
179
|
-
|
|
180
|
-
- **Q1:** Should the per-profile conversation cache be lifted into the `Mediator` (or a shared `ConversationOwnershipCache` service) so that a `gemiterm list --all-profiles` call followed by a `gemiterm continue <id>` reuses the cache? **Decision:** not in this change. The follow-up is a small change to `Mediator`'s constructor and the `ListChatsQueryHandler`; the cache can be invalidated by writing to it (delete a conversation → remove from the map). Out of scope for a spec-conformance change; tracked in a follow-up.
|
|
181
|
-
|
|
182
|
-
- **Q2:** Should `findProfileForConversation` short-circuit on the active profile if `conversationId` is `undefined` (a degenerate case where the caller forgot to pass the argument)? **Decision:** no. The method signature requires a `string`; passing `undefined` is a TypeScript error. The current 8 tests already cover the cases the change needs.
|
|
183
|
-
|
|
184
|
-
- **Q3:** Should the `forProfile` factory on `GeminiClientService` be exposed on the `IGeminiClientService` interface, or kept as a concrete-class-only method (the handler downcasts to `GeminiClientService`)? **Decision:** expose on the interface. The interface is a contract; the concrete class is the implementation. Adding `forProfile(name: string): IGeminiClientService` to the interface keeps the handler type-safe and lets test mocks implement the method.
|
|
185
|
-
|
|
186
|
-
- **Q4:** Should the error message include the list of active profiles as a hint? E.g. "Could not find a profile that owns conversation '<id>'. Active profiles: ['work', 'personal']. Run 'gemiterm list --all-profiles'…" **Decision:** no, in this change. Listing the active profiles leaks information about the user's setup in a multi-user log scenario. The remediation message stays generic; the user can run `gemiterm list --all-profiles` to see the active profiles themselves.
|
|
187
|
-
|
|
188
|
-
- **Q5:** Should the Profile column in `formatChatList` show the default profile with a `*` marker (matching the profile table convention in `formatProfileTable` at `src/infrastructure/formatters.ts:92`)? **Decision:** not in this change. The Profile column in the chat list is for disambiguation, not for marking the default; mixing the two would confuse the column's purpose. A follow-up could add a separate "DEFAULT" column or a `*` suffix if the maintainer wants the marker.
|
|
189
|
-
|
|
190
|
-
- **Q6:** Should the integration tests for `continue` and `delete` mock the `ProfileAuthManager` and `GeminiClientService` separately, or use a single integration-level mock at the mediator boundary? **Decision:** the existing integration tests in `tests/integration/commands/continue.test.ts` and `delete.test.ts` (when they exist) mock at the mediator boundary (`spyOn(context.mediator, "send")`). The new profile-lookup tests follow the same pattern: mock the mediator to simulate a "profile found" or "profile not found" response, and assert the command's behavior. This keeps the test surface consistent with the existing 432-test baseline.
|
|
191
|
-
|
|
192
|
-
- **Q7:** Does the per-profile routing break the `--all-profiles` flag in `list`? Today `list --all-profiles` already routes through the mediator with `allProfiles: true` in the payload; the new behavior propagates the active profile name into each `ChatInfo.profile` field. The `formatChatList` call gets the flag and renders the column. **Decision:** no breakage; the change is purely additive at the rendering layer.
|
|
193
|
-
|
|
194
|
-
- **Q8:** Should the `profileHasConversation` helper be `async` (the current `listChats` is `async`)? **Decision:** yes. The helper is a thin wrapper around `listChats`, which awaits `fetch`. Marking the helper `async` keeps the call-site clean (`await geminiClient.profileHasConversation(name, id)`). The alternative (a sync helper that returns a `Promise<boolean>` from a non-`async` function) is just a wrapper around `Promise.resolve(...)` and adds no value.
|
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
## Why
|
|
2
|
-
|
|
3
|
-
Three small but real spec-conformance gaps remain in the CLI commands after the Maestro migration:
|
|
4
|
-
|
|
5
|
-
1. **`continue-command.ts` and `delete-command.ts` do not call `ProfileAuthManager.findProfileForConversation`.** Per Maestro Phase 4 (`Phase-04-Commands.md:31, 47`), both commands should look up the profile that owns the conversation before sending the mediator command, so that a user with multiple profiles (e.g. work + personal) sends the message to / deletes from the right profile. Currently both commands go to the default profile regardless of where the conversation lives.
|
|
6
|
-
2. **`ProfileAuthManager.findProfileForConversation` ignores its `conversationId` argument** (`src/services/profile-auth-manager.ts:46-60`). The method body iterates over all profiles and returns the first active one, never inspecting the cookies or storage of each profile for ownership of the requested conversation. In a single-profile setup this is a no-op; in a multi-profile setup it returns a wrong profile. The method is wired up to the registry but is functionally broken, which means even if commands 1 were implemented they would still be wrong without this fix.
|
|
7
|
-
3. **`list-command.ts` does not render a `Profile` column when `--all-profiles` is set.** Per Maestro Phase 4 (`Phase-04-Commands.md:11`), the table should include a `Profile` column in that mode. The mediator payload already carries `allProfiles: boolean` (`list-command.ts:62`) and the integration tests assert the JSON contains profile info, but the text output uses `formatChatList(chats)` (`formatters.ts:115-145`) which renders only `ID / TITLE / DATE / PIN`. Users with multiple profiles cannot tell which profile a chat belongs to in the text output.
|
|
8
|
-
|
|
9
|
-
## What Changes
|
|
10
|
-
|
|
11
|
-
- **Fix `ProfileAuthManager.findProfileForConversation(conversationId)` in `src/services/profile-auth-manager.ts:46-60`** so that the `conversationId` argument is actually used. The implementation should iterate over all profiles; for each, call `geminiClient.listChats({ profile: name })` (or a new lightweight `profileHasConversation(name, conversationId)` helper on the Gemini client) to check whether the conversation appears in that profile's chat list; return the first matching profile name, or `null` if no profile owns it. **The 8 existing unit tests in `tests/services/profile-auth-manager.test.ts:88-223` are the regression gate** — they must be updated to assert the new per-profile-lookup behavior, not the broken first-active behavior. **Do NOT change the public method signature** — it must remain `findProfileForConversation(conversationId: string): string | null`.
|
|
12
|
-
- **Add `profileHasConversation(profileName, conversationId): Promise<boolean>` (or a non-async equivalent that reuses the same chat-listing path)** to `GeminiClientService` in `src/services/gemini-client-wrapper.ts` so `ProfileAuthManager` has a fast per-profile lookup. This is a small wrapper around the existing `listChats()` call scoped to a single profile. The existing `tests/integration/commands/list.test.ts` and the `IGeminiClientService` interface in `src/core/command-handlers.ts` already define the per-profile listing path.
|
|
13
|
-
- **Update `src/cli/commands/continue-command.ts:50-69` and `:75-130`** to call `profileAuthManager.findProfileForConversation(conversationId)` before sending the `SendMessageCommand`. If the method returns `null`, throw `AuthenticationError` with a clear message ("Could not find a profile that owns conversation '<id>'. Run 'gemiterm list --all-profiles' to see which profile it belongs to, then 'gemiterm continue <id> <msg> --profile <name>' to specify the profile explicitly."). If it returns a profile name, send the command to that profile's mediator wiring (this may require extending the `IGeminiClientService` injection so commands can target a specific profile, or routing through a profile-scoped client constructor). Document the chosen approach in `design.md`.
|
|
14
|
-
- **Update `src/cli/commands/delete-command.ts:51-77`** analogously — call `findProfileForConversation` before sending `DeleteConversationCommand`. Same error-handling pattern.
|
|
15
|
-
- **Add `profile?: string` to `ChatInfo` in `src/core/types.ts:9-14`** (optional, backward-compatible).
|
|
16
|
-
- **Update `formatChatList` in `src/infrastructure/formatters.ts:115-145`** to accept an optional `includeProfileColumn: boolean` parameter. When true, render a 5th `PROFILE` column (width 14 chars); when false, keep the current 4-column layout.
|
|
17
|
-
- **Update `src/cli/commands/list-command.ts:129-136`** to pass `includeProfileColumn: options.allProfiles` to `formatChatList`.
|
|
18
|
-
- **Add a new optional `profile` field to the `ListChatsQueryResult.chats` payload** by propagating the active profile name in `GeminiClientService.listChats(options)` when `options.allProfiles` is true.
|
|
19
|
-
- **Tests:**
|
|
20
|
-
- Update `tests/services/profile-auth-manager.test.ts` so the 8 existing cases test the new per-profile-lookup behavior (mocking `profileHasConversation`).
|
|
21
|
-
- Add 3-4 new unit tests in `tests/services/profile-auth-manager.test.ts` covering: "conversation found in second profile", "conversation not in any profile", "throws when no active profiles".
|
|
22
|
-
- Add 2 new integration tests in `tests/integration/commands/continue.test.ts` and `tests/integration/commands/delete.test.ts` covering the profile-lookup code path.
|
|
23
|
-
- Add 2 new integration tests in `tests/integration/commands/list.test.ts` covering the new Profile column when `--all-profiles` is set.
|
|
24
|
-
|
|
25
|
-
**No breaking changes** to the CLI user surface. The `profile` field in `ChatInfo` is optional; existing JSON output is unchanged. The Profile column only appears when the user explicitly passes `--all-profiles`.
|
|
26
|
-
|
|
27
|
-
## Capabilities
|
|
28
|
-
|
|
29
|
-
### New Capabilities
|
|
30
|
-
- `multi-profile-conversations`: The ability for the `continue` and `delete` commands to locate the profile that owns a given conversation, and for `list --all-profiles` to display which profile each conversation belongs to. Creates `openspec/changes/command-spec-conformance/specs/multi-profile-conversations/spec.md`.
|
|
31
|
-
|
|
32
|
-
### Modified Capabilities
|
|
33
|
-
- (none) — there is no existing main `commands` spec yet, so this is a new capability rather than a modification.
|
|
34
|
-
|
|
35
|
-
## Impact
|
|
36
|
-
|
|
37
|
-
- **Code (8 files):**
|
|
38
|
-
- `src/services/profile-auth-manager.ts` — fix `findProfileForConversation`
|
|
39
|
-
- `src/services/gemini-client-wrapper.ts` — add `profileHasConversation` helper
|
|
40
|
-
- `src/cli/commands/continue-command.ts` — call `findProfileForConversation`
|
|
41
|
-
- `src/cli/commands/delete-command.ts` — call `findProfileForConversation`
|
|
42
|
-
- `src/cli/commands/list-command.ts` — pass `includeProfileColumn` flag
|
|
43
|
-
- `src/core/types.ts` — add optional `profile` to `ChatInfo`
|
|
44
|
-
- `src/infrastructure/formatters.ts` — add `includeProfileColumn` to `formatChatList`
|
|
45
|
-
- `src/core/command-handlers.ts` (or the `IGeminiClientService` interface) — ensure per-profile routing is exposed
|
|
46
|
-
- **Tests (3 files):**
|
|
47
|
-
- `tests/services/profile-auth-manager.test.ts` — update 8 existing + add 3-4 new
|
|
48
|
-
- `tests/integration/commands/continue.test.ts` — add 2
|
|
49
|
-
- `tests/integration/commands/delete.test.ts` — add 2
|
|
50
|
-
- `tests/integration/commands/list.test.ts` — add 2
|
|
51
|
-
- **Baseline gate:** 432/432 tests must continue to pass; the new tests bring the count higher.
|
|
52
|
-
- **SENSITIVE AREA:** the auth/playwright-cli/cookie-monitor code paths are NOT touched. This change is purely about the `continue`, `delete`, and `list` commands and the `ProfileAuthManager` + `GeminiClientService` services. No cookie capture or browser lifecycle code is modified.
|
|
@@ -1,133 +0,0 @@
|
|
|
1
|
-
## ADDED Requirements
|
|
2
|
-
|
|
3
|
-
### Requirement: findProfileForConversation returns the profile that owns a conversation
|
|
4
|
-
The system MUST return the name of the profile whose server-side chat list contains the given conversation ID. The `conversationId` argument MUST be passed to the per-profile lookup helper; it MUST NOT be ignored. When no profile owns the conversation, the system MUST return `null`. When multiple profiles report ownership of the same conversation ID (an inconsistent state the user is responsible for), the system MUST return the first profile in `profileManager.list()` order that reports ownership.
|
|
5
|
-
|
|
6
|
-
#### Scenario: Conversation exists in one profile
|
|
7
|
-
- **WHEN** `findProfileForConversation("abc-123")` is called and conversation `abc-123` exists in profile `work` but not in profile `personal`
|
|
8
|
-
- **THEN** the method returns the string `"work"`
|
|
9
|
-
|
|
10
|
-
#### Scenario: Conversation exists in no profile
|
|
11
|
-
- **WHEN** `findProfileForConversation("abc-123")` is called and conversation `abc-123` does not exist in any active profile's chat list
|
|
12
|
-
- **THEN** the method returns `null`
|
|
13
|
-
|
|
14
|
-
#### Scenario: Conversation exists in multiple profiles (inconsistent state)
|
|
15
|
-
- **WHEN** `findProfileForConversation("abc-123")` is called and conversation `abc-123` exists in both profile `work` and profile `personal`, and `profileManager.list()` returns `["work", "personal"]` in that order
|
|
16
|
-
- **THEN** the method returns the string `"work"` (the first profile in list order that reports ownership)
|
|
17
|
-
|
|
18
|
-
#### Scenario: The conversationId argument is actually used
|
|
19
|
-
- **WHEN** a unit test calls `findProfileForConversation("abc-123")` and the `profileHasConversation` helper is mocked
|
|
20
|
-
- **THEN** the mocked helper MUST have been invoked with the exact string `"abc-123"` (verified via `expect(mock).toHaveBeenCalledWith("work", "abc-123")` or an equivalent assertion); the method MUST NOT return early with a first-active-wins fallback
|
|
21
|
-
|
|
22
|
-
### Requirement: continue-command targets the profile that owns the conversation
|
|
23
|
-
The system MUST look up the profile that owns the conversation via `ProfileAuthManager.findProfileForConversation` before sending the `SendMessageCommand`. The system MUST route the message to that profile's `GeminiClientService` instance. When no profile owns the conversation, the system MUST throw `AuthenticationError` with a remediation message and exit non-zero. In a single-profile setup (only the default profile is active), the behavior MUST be unchanged: the default profile is used.
|
|
24
|
-
|
|
25
|
-
#### Scenario: Multi-profile continue routes to the owning profile
|
|
26
|
-
- **WHEN** a user with active profiles `work` and `personal` runs `gemiterm continue abc-123 "hello"` and conversation `abc-123` is owned by `work`
|
|
27
|
-
- **THEN** the `SendMessageCommand` is sent via the `work` profile's `GeminiClientService` (verified by the response content being scoped to the `work` account); the `personal` profile's `GeminiClientService` is NOT invoked for this send
|
|
28
|
-
|
|
29
|
-
#### Scenario: Continue on an unknown conversation ID exits non-zero
|
|
30
|
-
- **WHEN** a user runs `gemiterm continue unknown-id "hello"` and no profile owns conversation `unknown-id`
|
|
31
|
-
- **THEN** the command throws `AuthenticationError` whose message contains `"Could not find a profile that owns conversation 'unknown-id'"` and the suggested remediation `"gemiterm list --all-profiles"`, and the process exits with a non-zero exit code
|
|
32
|
-
|
|
33
|
-
#### Scenario: Single-profile continue is unchanged
|
|
34
|
-
- **WHEN** a user with only the default profile active runs `gemiterm continue abc-123 "hello"`
|
|
35
|
-
- **THEN** the behavior is identical to before the change: the default profile is used, no `findProfileForConversation` call is required, and the response is returned normally
|
|
36
|
-
|
|
37
|
-
### Requirement: delete-command targets the profile that owns the conversation
|
|
38
|
-
The system MUST look up the profile that owns the conversation via `ProfileAuthManager.findProfileForConversation` before sending the `DeleteConversationCommand`. The system MUST route the delete to that profile's `GeminiClientService` instance. When no profile owns the conversation, the system MUST throw `AuthenticationError` with the same remediation message used by `continue` and exit non-zero. In a single-profile setup, the behavior MUST be unchanged.
|
|
39
|
-
|
|
40
|
-
#### Scenario: Multi-profile delete routes to the owning profile
|
|
41
|
-
- **WHEN** a user with active profiles `work` and `personal` runs `gemiterm delete abc-123 --force` and conversation `abc-123` is owned by `work`
|
|
42
|
-
- **THEN** the `DeleteConversationCommand` is sent via the `work` profile's `GeminiClientService`; the `personal` profile's `GeminiClientService` is NOT invoked for this delete
|
|
43
|
-
|
|
44
|
-
#### Scenario: Delete on an unknown conversation ID exits non-zero
|
|
45
|
-
- **WHEN** a user runs `gemiterm delete unknown-id --force` and no profile owns conversation `unknown-id`
|
|
46
|
-
- **THEN** the command throws `AuthenticationError` whose message contains `"Could not find a profile that owns conversation 'unknown-id'"` and the process exits with a non-zero exit code
|
|
47
|
-
|
|
48
|
-
#### Scenario: Single-profile delete is unchanged
|
|
49
|
-
- **WHEN** a user with only the default profile active runs `gemiterm delete abc-123 --force`
|
|
50
|
-
- **THEN** the behavior is identical to before the change: the default profile is used, no `findProfileForConversation` call is required, and the conversation is deleted
|
|
51
|
-
|
|
52
|
-
### Requirement: list --all-profiles renders a Profile column in the text table
|
|
53
|
-
The system MUST render a `PROFILE` column in the text table output of `gemiterm list --all-profiles` and MUST NOT render it in any other mode. Each row MUST show the name of the profile that owns the conversation in that row. The column width MUST be 14 characters, matching the visual style of the other columns.
|
|
54
|
-
|
|
55
|
-
#### Scenario: list --all-profiles shows 5 columns
|
|
56
|
-
- **WHEN** a user with active profiles `work` and `personal` runs `gemiterm list --all-profiles`
|
|
57
|
-
- **THEN** the text table header contains the columns `ID, TITLE, DATE, PIN, PROFILE` in that order, and each row's last cell contains the owning profile name (e.g. `work` or `personal`)
|
|
58
|
-
|
|
59
|
-
#### Scenario: list without --all-profiles shows 4 columns
|
|
60
|
-
- **WHEN** a user runs `gemiterm list` (no `--all-profiles` flag)
|
|
61
|
-
- **THEN** the text table header contains the columns `ID, TITLE, DATE, PIN` only; the `PROFILE` column is absent and the chat output matches the pre-change format byte-for-byte
|
|
62
|
-
|
|
63
|
-
#### Scenario: list --all-profiles --format json includes profile field
|
|
64
|
-
- **WHEN** a user runs `gemiterm list --all-profiles --format json`
|
|
65
|
-
- **THEN** the JSON output's `chats` array contains a `profile` field on every chat; each `profile` value is the name of the profile that owns that chat
|
|
66
|
-
|
|
67
|
-
#### Scenario: list without --all-profiles --format json omits profile field
|
|
68
|
-
- **WHEN** a user runs `gemiterm list --format json` (no `--all-profiles` flag)
|
|
69
|
-
- **THEN** the JSON output's `chats` array does NOT contain a `profile` field on any chat; the JSON shape is byte-compatible with the pre-change output
|
|
70
|
-
|
|
71
|
-
### Requirement: ChatInfo carries an optional profile field
|
|
72
|
-
The `ChatInfo` type in `src/core/types.ts` MUST carry an optional `profile?: string` field. The field MUST be unset (or `undefined`) for chats returned by `list` without `--all-profiles` and MUST be the owning profile name for chats returned by `list --all-profiles`. Existing serialized JSON output for the non-`--all-profiles` path MUST be byte-compatible with the pre-change shape (the field is omitted, not set to `null` or `""`).
|
|
73
|
-
|
|
74
|
-
#### Scenario: ChatInfo type allows optional profile field
|
|
75
|
-
- **WHEN** a `ChatInfo` object is constructed with `{ id: "abc-123", title: "T", isPinned: false, timestamp: 0, profile: "work" }`
|
|
76
|
-
- **THEN** the object is type-valid and the `profile` field is the string `"work"`
|
|
77
|
-
|
|
78
|
-
#### Scenario: ChatInfo type allows absence of profile field
|
|
79
|
-
- **WHEN** a `ChatInfo` object is constructed with `{ id: "abc-123", title: "T", isPinned: false, timestamp: 0 }` (no `profile` key)
|
|
80
|
-
- **THEN** the object is type-valid and the `profile` field is `undefined`
|
|
81
|
-
|
|
82
|
-
#### Scenario: ChatInfo serialized to JSON omits profile when unset
|
|
83
|
-
- **WHEN** a `ChatInfo` object with no `profile` field is serialized via `JSON.stringify`
|
|
84
|
-
- **THEN** the resulting JSON does NOT contain a `profile` key (matches the pre-change byte layout)
|
|
85
|
-
|
|
86
|
-
### Requirement: formatChatList accepts an includeProfileColumn flag
|
|
87
|
-
The `formatChatList` function in `src/infrastructure/formatters.ts` MUST accept an optional second argument `options?: { includeProfileColumn?: boolean }`. When `options.includeProfileColumn` is `true`, the rendered table MUST have 5 columns including `PROFILE`. When the flag is `false` or omitted, the rendered table MUST have the original 4 columns (`ID, TITLE, DATE, PIN`) and the output MUST be byte-compatible with the pre-change format.
|
|
88
|
-
|
|
89
|
-
#### Scenario: formatChatList with includeProfileColumn: true renders 5 columns
|
|
90
|
-
- **WHEN** `formatChatList(chats, { includeProfileColumn: true })` is called with chats that each have a `profile` field
|
|
91
|
-
- **THEN** the rendered output contains the header `ID TITLE DATE PIN PROFILE` (with the column-padding and divider convention used elsewhere) and each row's last cell shows the chat's `profile` value
|
|
92
|
-
|
|
93
|
-
#### Scenario: formatChatList without flag renders 4 columns (backward compat)
|
|
94
|
-
- **WHEN** `formatChatList(chats)` is called with no second argument
|
|
95
|
-
- **THEN** the rendered output contains the header `ID TITLE DATE PIN` and the output is byte-compatible with the pre-change format for the same input (no `PROFILE` column)
|
|
96
|
-
|
|
97
|
-
#### Scenario: Existing formatChatList callers continue to pass without modification
|
|
98
|
-
- **WHEN** an existing test calls `formatChatList(chats)` (the pre-change call shape) after the change lands
|
|
99
|
-
- **THEN** the test continues to pass without modification (regression gate for the formatter; the optional second argument is non-breaking)
|
|
100
|
-
|
|
101
|
-
### Requirement: GeminiClientService exposes a profileHasConversation helper
|
|
102
|
-
The `GeminiClientService` class in `src/services/gemini-client-wrapper.ts` MUST expose a `profileHasConversation(profileName: string, conversationId: string): Promise<boolean>` method. The method MUST return `true` if the given conversation ID appears in the named profile's chat list, and `false` otherwise. The `IGeminiClientService` interface in `src/core/command-handlers.ts` MUST declare the method so handlers and tests can use it without downcasting. The method MUST NOT mutate the calling instance's cookies or session state.
|
|
103
|
-
|
|
104
|
-
#### Scenario: profileHasConversation returns true for owning profile
|
|
105
|
-
- **WHEN** `await geminiClient.profileHasConversation("work", "abc-123")` is called and conversation `abc-123` exists in `work`'s chat list
|
|
106
|
-
- **THEN** the method returns `true`
|
|
107
|
-
|
|
108
|
-
#### Scenario: profileHasConversation returns false for non-owning profile
|
|
109
|
-
- **WHEN** `await geminiClient.profileHasConversation("personal", "abc-123")` is called and conversation `abc-123` does NOT exist in `personal`'s chat list
|
|
110
|
-
- **THEN** the method returns `false`
|
|
111
|
-
|
|
112
|
-
#### Scenario: profileHasConversation is declared on the IGeminiClientService interface
|
|
113
|
-
- **WHEN** a handler is typed against `IGeminiClientService` and calls `geminiClient.profileHasConversation(name, id)`
|
|
114
|
-
- **THEN** the call type-checks (the method is part of the interface, not a concrete-class-only method)
|
|
115
|
-
|
|
116
|
-
#### Scenario: profileHasConversation does not mutate the calling instance
|
|
117
|
-
- **WHEN** `geminiClient.profileHasConversation("work", "abc-123")` is called on an instance configured for the default profile
|
|
118
|
-
- **THEN** the calling instance's cookie config and `authenticated` flag are unchanged after the call returns (verified by reading the instance fields in a test)
|
|
119
|
-
|
|
120
|
-
### Requirement: Regression test update for profile-auth-manager documents the bug fix
|
|
121
|
-
The 8 existing unit tests in the `describe("findProfileForConversation")` block of `tests/services/profile-auth-manager.test.ts` MUST be updated to assert the correct per-profile-lookup behavior (not the previous first-active-wins behavior). The test count for that describe block MUST increase (8 → 11+) after the change. A leading comment in the test file MUST explain that the 8 changed tests previously documented the bug and the change is the fix.
|
|
122
|
-
|
|
123
|
-
#### Scenario: The 8 existing tests are updated to mock profileHasConversation
|
|
124
|
-
- **WHEN** `tests/services/profile-auth-manager.test.ts` is read after the change
|
|
125
|
-
- **THEN** the 3 existing tests in the `findProfileForConversation` block (the original 8 minus the 5 that don't exercise the per-profile lookup) are rewritten to mock `GeminiClientService.profileHasConversation` and assert the new behavior; the new behavior is the first profile in list order whose `profileHasConversation` returns `true`
|
|
126
|
-
|
|
127
|
-
#### Scenario: The test count for profile-auth-manager increases
|
|
128
|
-
- **WHEN** `bun test tests/services/profile-auth-manager.test.ts` is run after the change
|
|
129
|
-
- **THEN** the test count is at least 11 (8 original tests minus the deleted first-active-wins test plus 3-4 new tests covering: "conversation found in second profile", "conversation not in any profile", "throws when no active profiles")
|
|
130
|
-
|
|
131
|
-
#### Scenario: The test file's leading comment documents the test changes
|
|
132
|
-
- **WHEN** `tests/services/profile-auth-manager.test.ts` is read after the change
|
|
133
|
-
- **THEN** the file contains a leading comment block stating: "The 8 tests in `describe('findProfileForConversation')` previously asserted the BUGGY 'first active profile' behavior; they have been updated to assert the CORRECT per-profile-lookup behavior. See `openspec/changes/command-spec-conformance/proposal.md` for context."
|
|
@@ -1,95 +0,0 @@
|
|
|
1
|
-
## 1. Fix `findProfileForConversation` core logic in `ProfileAuthManager`
|
|
2
|
-
|
|
3
|
-
- [x] 1.1 Read `src/services/profile-auth-manager.ts:46-60` to confirm the current implementation iterates `profileManager.list()` and returns the first profile with `getStatus(name).isActive === true`, never using the `conversationId` argument.
|
|
4
|
-
- [x] 1.2 Read `src/services/profile-auth-manager.ts:10-39` to confirm the constructor signature and the `getActiveProfiles` pattern; the new method should follow the same shape (use `profileManager.list()`, then filter for the owning profile).
|
|
5
|
-
- [x] 1.3 Refactor `findProfileForConversation(conversationId)` so it calls a `profileHasConversation` helper on the injected service for each active profile and returns the first profile whose helper returns `true`. If no profile returns `true`, the method MUST return `null`. The method signature MUST remain `findProfileForConversation(conversationId: string): string | null` (the public contract is unchanged).
|
|
6
|
-
- [x] 1.4 Do NOT touch any of the SENSITIVE files (`src/services/playwright-cli-driver.ts`, `src/services/cookie-monitor.ts`, `src/services/auth-service.ts`); the change is scoped to `profile-auth-manager.ts` and its dependencies.
|
|
7
|
-
- [x] 1.5 Do NOT change the public method signature; the only acceptable change is the method body.
|
|
8
|
-
|
|
9
|
-
## 2. Add `profileHasConversation` helper to `GeminiClientService`
|
|
10
|
-
|
|
11
|
-
- [x] 2.1 Read `src/services/gemini-client-wrapper.ts:91-141` to confirm the existing `listChats(options)` method; the new helper is a thin wrapper that scopes a `listChats` call to a single profile's cookies and returns a boolean.
|
|
12
|
-
- [x] 2.2 Add `async profileHasConversation(profileName: string, conversationId: string): Promise<boolean>` to `GeminiClientService` in `src/services/gemini-client-wrapper.ts`. The method MUST construct a per-profile client (via the `forProfile` factory in task 2.3), call `listChats()` on it, and return `true` if the conversation ID is in the result.
|
|
13
|
-
- [x] 2.3 Add a `forProfile(profileName: string): GeminiClientService` factory method on `GeminiClientService` that returns a NEW instance configured with the named profile's cookies (via `CookieStorageService.loadCookiesForProfile`). The method MUST NOT mutate the calling instance's `config` or `authenticated` fields.
|
|
14
|
-
- [x] 2.4 Expose `profileHasConversation` on the `IGeminiClientService` interface in `src/core/command-handlers.ts:90-94` so handlers and tests can use it without downcasting. The interface method signature is `profileHasConversation(profileName: string, conversationId: string): Promise<boolean>`.
|
|
15
|
-
- [x] 2.5 The helper MUST NOT make any changes to the on-disk cookie storage; it only reads via the injected `CookieStorageService`.
|
|
16
|
-
|
|
17
|
-
## 3. Update `profile-auth-manager.test.ts` to encode the new behavior
|
|
18
|
-
|
|
19
|
-
- [x] 3.1 Add a leading comment block at the top of `tests/services/profile-auth-manager.test.ts` (above the first `describe`) stating: "The 8 tests in `describe('findProfileForConversation')` previously asserted the BUGGY 'first active profile' behavior; they have been updated to assert the CORRECT per-profile-lookup behavior. See `openspec/changes/command-spec-conformance/proposal.md` for context." This is required so reviewers do not flag the test changes as a regression.
|
|
20
|
-
- [x] 3.2 Rewrite the existing "returns first active profile" test (currently at `tests/services/profile-auth-manager.test.ts:188-200`) to: create two profiles, mock `GeminiClientService.profileHasConversation` to return `true` for `work` and `false` for `personal`, and assert the method returns `"work"`. The test name MUST be changed to "returns the profile that owns the conversation" (or similar) to reflect the new behavior.
|
|
21
|
-
- [x] 3.3 Keep the "returns null when no active profiles" test (currently at `tests/services/profile-auth-manager.test.ts:202-212`) but update it to mock `profileHasConversation` to return `false` for the only active profile; the assertion (`toBeNull()`) is unchanged.
|
|
22
|
-
- [x] 3.4 Keep the "returns null when no profiles exist" test (currently at `tests/services/profile-auth-manager.test.ts:214-222`) unchanged: the `profileManager.list()` returns an empty array, so the loop never runs and the method returns `null`.
|
|
23
|
-
- [x] 3.5 Add a new test "returns null when conversation is not in any profile" that creates two profiles with valid cookies, mocks `profileHasConversation` to return `false` for both, and asserts `findProfileForConversation` returns `null`.
|
|
24
|
-
- [x] 3.6 Add a new test "returns first profile in list order when multiple profiles report ownership" that creates three profiles, mocks `profileHasConversation` to return `true` for profiles 1 and 3 (and `false` for profile 2), and asserts the method returns profile 1 (the first in `list()` order). This guards the iteration-order contract.
|
|
25
|
-
- [x] 3.7 Add a new test "passes the conversationId argument to the lookup helper" that mocks `profileHasConversation` with a spy and asserts the spy was called with the exact `conversationId` string the test passed in. This is the regression gate that catches the "first-active-wins" bug recurring.
|
|
26
|
-
- [x] 3.8 The test count for `profile-auth-manager.test.ts` MUST be 11 or higher after the change (the original 8, minus 0 since we keep all but rename one, plus 3 new tests = 11; the new tests can be expanded to 4 if helpful for coverage).
|
|
27
|
-
|
|
28
|
-
## 4. Add `profile?: string` to `ChatInfo`
|
|
29
|
-
|
|
30
|
-
- [x] 4.1 Read `src/core/types.ts:9-14` to confirm the current `ChatInfo` shape: `{ id, title, isPinned, timestamp }`.
|
|
31
|
-
- [x] 4.2 Add an optional `profile?: string` field to the `ChatInfo` interface. The field MUST be optional (declared with `?`) to preserve backward compatibility for single-profile consumers and for JSON serialization that does not include the field.
|
|
32
|
-
- [x] 4.3 Do NOT change any other field in the interface. The type is purely additive.
|
|
33
|
-
- [x] 4.4 Verify the change compiles via `bun run build` (or the project's TypeScript check command); the optional field MUST NOT break any existing `ChatInfo` construction sites.
|
|
34
|
-
|
|
35
|
-
## 5. Add `includeProfileColumn` flag to `formatChatList`
|
|
36
|
-
|
|
37
|
-
- [x] 5.1 Read `src/infrastructure/formatters.ts:115-145` to confirm the current `formatChatList(chats)` signature and 4-column layout (ID, TITLE, DATE, PIN).
|
|
38
|
-
- [x] 5.2 Update the signature to `formatChatList(chats: ChatInfo[], options?: { includeProfileColumn?: boolean }): string`. The second argument is an optional options object.
|
|
39
|
-
- [x] 5.3 When `options?.includeProfileColumn === true`, render a 5th `PROFILE` column with width 14 characters. The column header MUST be `PROFILE` and the column value MUST be `chat.profile ?? ""` (defensive: in case the chat is missing the field for any reason). The 5-column layout MUST match the visual style of the existing columns (header + dashed underline + per-row data).
|
|
40
|
-
- [x] 5.4 When `options?.includeProfileColumn` is `false` or `undefined`, the output MUST be byte-compatible with the pre-change format. The 4-column layout is unchanged.
|
|
41
|
-
- [x] 5.5 Verify all existing callers of `formatChatList` continue to compile: the new optional second argument is non-breaking.
|
|
42
|
-
|
|
43
|
-
## 6. Wire `list-command.ts` to pass `includeProfileColumn` to `formatChatList`
|
|
44
|
-
|
|
45
|
-
- [x] 6.1 Read `src/cli/commands/list-command.ts:129-136` to confirm the current `outputText` method calls `formatChatList(chats)` without a second argument.
|
|
46
|
-
- [x] 6.2 Update the call to `formatChatList(chats, { includeProfileColumn: options.allProfiles })`. The flag is plumbed from the `ListCommandOptions.allProfiles` field that already exists at `list-command.ts:21`.
|
|
47
|
-
- [x] 6.3 Read `src/cli/commands/list-command.ts:58-63` to confirm the `ListChatsQueryPayload` already carries `allProfiles: options.allProfiles`; this is the upstream signal that the mediator will populate `ChatInfo.profile` on the result.
|
|
48
|
-
- [x] 6.4 Update the help text in `list-command.ts:215` to mention that the Profile column appears in `--all-profiles` mode. The existing `--all-profiles` description is "Show conversations from all profiles"; expand it to "Show conversations from all profiles (with Profile column in text output)" or similar.
|
|
49
|
-
|
|
50
|
-
## 7. Update `continue-command.ts` to call `findProfileForConversation` and route per-profile
|
|
51
|
-
|
|
52
|
-
- [x] 7.1 Read `src/cli/commands/continue-command.ts:50-69` and `:75-130` to confirm the current `sendNonInteractive` and `startInteractive` methods both send the `SendMessageCommand` to the default profile only.
|
|
53
|
-
- [x] 7.2 Add a call to `findProfileForConversation(conversationId)` in `sendNonInteractive` before the mediator send. The lookup MUST use the `ProfileAuthManager` (added to the `CliCommandContext` — see task 9.2).
|
|
54
|
-
- [x] 7.3 If the lookup returns `null`, throw `AuthenticationError` with the message: `"Could not find a profile that owns conversation '<id>'. Run 'gemiterm list --all-profiles' to see which profile it belongs to, then 'gemiterm continue <id> <msg> --profile <name>' to specify the profile explicitly."` (the `--profile` override is a follow-up; mentioned in the message as guidance, not as a current flag).
|
|
55
|
-
- [x] 7.4 If the lookup returns a profile name, build a per-profile `GeminiClientService` via `forProfile(name)` and route the `SendMessageCommand` through that instance. The default profile is still used as a fallback when only one profile is active.
|
|
56
|
-
- [x] 7.5 Apply the same pattern to `startInteractive` at `continue-command.ts:75-130` so that the interactive loop sends to the correct profile.
|
|
57
|
-
- [x] 7.6 The error must propagate to the existing `try/catch` at `continue-command.ts:112-116`; the user sees the error in red and the process exits non-zero.
|
|
58
|
-
|
|
59
|
-
## 8. Update `delete-command.ts` to call `findProfileForConversation` and route per-profile
|
|
60
|
-
|
|
61
|
-
- [x] 8.1 Read `src/cli/commands/delete-command.ts:51-77` to confirm the current flow sends the `DeleteConversationCommand` to the default profile only.
|
|
62
|
-
- [x] 8.2 Add a call to `findProfileForConversation(conversationId)` after the `validateConversationId` check at `delete-command.ts:44-49` and before the mediator send at `delete-command.ts:64-69`.
|
|
63
|
-
- [x] 8.3 If the lookup returns `null`, throw `AuthenticationError` with the same remediation message as `continue` (task 7.3), adapted for delete: `"… then 'gemiterm delete <id> --profile <name>' to specify the profile explicitly."`.
|
|
64
|
-
- [x] 8.4 If the lookup returns a profile name, route the `DeleteConversationCommand` through that profile's `GeminiClientService` (via `forProfile`).
|
|
65
|
-
- [x] 8.5 The error must propagate to the existing `try/catch` at `delete-command.ts:77-81`; the process exits non-zero with the error message.
|
|
66
|
-
|
|
67
|
-
## 9. Add per-profile routing via `IGeminiClientService` extension
|
|
68
|
-
|
|
69
|
-
- [x] 9.1 Add `profileName?: string` to `SendMessageCommandPayload` and `DeleteConversationCommandPayload` in `src/core/command-handlers.ts:44-55`. The field is optional: when absent, the handler uses its injected (default-profile) client; when present, the handler builds a per-profile client via `forProfile` and uses that.
|
|
70
|
-
- [x] 9.2 Add the `ProfileAuthManager` to `CliCommandContext` in `src/cli/command-registry.ts:14-17`. The `CliCommandContext` interface gains a new `profileAuthManager: ProfileAuthManager` field. Update the wiring at `src/cli/index.ts` (or wherever the context is constructed) to inject the `ProfileAuthManager` instance.
|
|
71
|
-
- [x] 9.3 Update `SendMessageCommandHandler.handle` at `src/core/command-handlers.ts:198-213` so it reads `payload.profileName`; if set, it builds the per-profile client via `geminiClient.forProfile(profileName)` and calls `sendMessage` on the per-profile client. The injected `geminiClient` is treated as the default-profile client.
|
|
72
|
-
- [x] 9.4 Update `DeleteConversationCommandHandler.handle` at `src/core/command-handlers.ts:178-196` with the same pattern: read `payload.profileName`, build the per-profile client, route `deleteChat` through it.
|
|
73
|
-
- [x] 9.5 The per-profile client is created per `handle` call; the injection of `GeminiClientService` at the handler level continues to use the default-profile instance. The factory pattern (D3 in `design.md`) means no per-profile state is held in the handler.
|
|
74
|
-
|
|
75
|
-
## 10. Add integration tests for continue/delete profile lookup
|
|
76
|
-
|
|
77
|
-
- [x] 10.1 Read `tests/integration/commands/continue.test.ts` if it exists; if it does not, create it following the pattern in `tests/integration/commands/list.test.ts` (mock the mediator, assert command behavior). The new test file MUST follow the existing test conventions (Bun's `describe`/`test`/`expect`).
|
|
78
|
-
- [x] 10.2 Add a new test in the continue integration suite: "resolves the profile that owns the conversation" — mock the `ProfileAuthManager.findProfileForConversation` to return `"work"`, mock the mediator to capture the `SendMessageCommandPayload`, and assert the payload's `profileName` is `"work"`.
|
|
79
|
-
- [x] 10.3 Add a new test in the continue integration suite: "throws AuthenticationError when no profile owns the conversation" — mock `findProfileForConversation` to return `null`, run the command, and assert the error message contains the remediation text and the process exits non-zero.
|
|
80
|
-
- [x] 10.4 Add a new test in the delete integration suite: "resolves the profile that owns the conversation" — analog of 10.2 for delete.
|
|
81
|
-
- [x] 10.5 Add a new test in the delete integration suite: "throws AuthenticationError when no profile owns the conversation" — analog of 10.3 for delete.
|
|
82
|
-
|
|
83
|
-
## 11. Add integration test for list Profile column
|
|
84
|
-
|
|
85
|
-
- [x] 11.1 Add a new test in `tests/integration/commands/list.test.ts`: "renders Profile column when --all-profiles is set" — mock the mediator to return chats with a `profile` field, run `command.execute(["--all-profiles"], context)`, and assert the output contains the `PROFILE` header and the profile names from the mocks.
|
|
86
|
-
- [x] 11.2 Add a new test in `tests/integration/commands/list.test.ts`: "omits Profile column when --all-profiles is not set" — mock the mediator to return chats (with or without a `profile` field), run `command.execute([], context)`, and assert the output does NOT contain the `PROFILE` header.
|
|
87
|
-
- [x] 11.3 Add a new test in `tests/integration/commands/list.test.ts`: "JSON output includes profile field only when --all-profiles is set" — run the command with and without `--all-profiles --format json`, parse the JSON output, and assert the `profile` key is present in the `--all-profiles` case and absent in the other.
|
|
88
|
-
|
|
89
|
-
## 12. Final verification
|
|
90
|
-
|
|
91
|
-
- [x] 12.1 Run `bun test` from the repo root and confirm all tests pass. The baseline is 432/432; the new tests (4-5 in profile-auth-manager + 2 in continue integration + 2 in delete integration + 3 in list integration) bring the count to 442+ (exact count depends on how the new tests are split).
|
|
92
|
-
- [x] 12.2 Run `bun test tests/services/profile-auth-manager.test.ts` and confirm the test count is at least 11 (8 original + 3-4 new) and the leading comment is in place.
|
|
93
|
-
- [x] 12.3 Run `bun run build` (or the project's TypeScript check command) to confirm the `ChatInfo.profile` field and the `formatChatList` options object do not break any consumer.
|
|
94
|
-
- [x] 12.4 Manually run `gemiterm list` (single-profile mode) and confirm the output is byte-compatible with the pre-change format. Manually run `gemiterm list --all-profiles` (multi-profile mode) and confirm the Profile column appears. Manually run `gemiterm continue <id> <msg>` against a known conversation and confirm it routes to the right profile.
|
|
95
|
-
- [x] 12.5 Confirm no file under `src/services/playwright-cli-driver.ts`, `src/services/cookie-monitor.ts`, or `src/services/auth-service.ts` was modified. The git diff for those files MUST be empty.
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
## Context
|
|
2
|
-
|
|
3
|
-
The `new` and `continue` commands both implement interactive chat loops using `node:readline`'s `createInterface`. Both have essentially identical `startInteractive` implementations with only minor differences in:
|
|
4
|
-
- How messages are processed (new chat vs. send to existing)
|
|
5
|
-
- Profile resolution logic
|
|
6
|
-
|
|
7
|
-
## Goals / Non-Goals
|
|
8
|
-
|
|
9
|
-
**Goals:**
|
|
10
|
-
- Extract shared interactive prompt logic into a reusable utility
|
|
11
|
-
- Fix the bug where `new` command's interactive mode exits after one round
|
|
12
|
-
- Reduce code duplication
|
|
13
|
-
|
|
14
|
-
**Non-Goals:**
|
|
15
|
-
- No new external dependencies
|
|
16
|
-
- No changes to command-line argument parsing
|
|
17
|
-
- No changes to the CLI command interface
|
|
18
|
-
|
|
19
|
-
## Decisions
|
|
20
|
-
|
|
21
|
-
1. **Create `src/cli/utils/interactive-prompt.ts`** as a shared utility module
|
|
22
|
-
2. **Export a `runInteractiveLoop` function** that accepts a callback for message handling
|
|
23
|
-
3. **Keep command-specific logic in the commands** - the shared utility handles only the prompt loop mechanics
|
|
24
|
-
|
|
25
|
-
## Risks / Trade-offs
|
|
26
|
-
|
|
27
|
-
- Minimal risk - this is a pure refactoring with no behavioral changes beyond fixing the bug
|
|
28
|
-
- Trade-off: adds a new file, but reduces overall complexity
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
## Why
|
|
2
|
-
|
|
3
|
-
The `new` and `continue` commands both implement interactive chat loops using `createInterface` from `node:readline`, but the `startInteractive` logic is duplicated in each command with minor variations. Additionally, the `new` command's interactive mode has a bug where it exits after processing just one message instead of looping until the user quits.
|
|
4
|
-
|
|
5
|
-
## What Changes
|
|
6
|
-
|
|
7
|
-
- Extract shared `startInteractive` logic into a reusable utility function/class
|
|
8
|
-
- Fix the bug where `new` command's interactive mode exits after one round
|
|
9
|
-
- Reduce code duplication between `new-command.ts` and `continue-command.ts`
|
|
10
|
-
|
|
11
|
-
## Capabilities
|
|
12
|
-
|
|
13
|
-
### New Capabilities
|
|
14
|
-
- `interactive-prompt-loop`: Shared interactive prompt loop utility for chat commands
|
|
15
|
-
|
|
16
|
-
### Modified Capabilities
|
|
17
|
-
<!-- No spec-level behavior changes - this is a pure refactoring with a bug fix -->
|
|
18
|
-
|
|
19
|
-
## Impact
|
|
20
|
-
|
|
21
|
-
- Files affected: `src/cli/commands/new-command.ts`, `src/cli/commands/continue-command.ts`
|
|
22
|
-
- New file: `src/cli/utils/interactive-prompt.ts` (or similar shared location)
|