gemiterm 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (184) hide show
  1. package/.env +2 -0
  2. package/.github/workflows/manual-release.yml +34 -0
  3. package/.github/workflows/release.yml +198 -0
  4. package/.github/workflows/test.yml +29 -0
  5. package/.opencode/commands/opsx-apply.md +152 -0
  6. package/.opencode/commands/opsx-archive.md +157 -0
  7. package/.opencode/commands/opsx-explore.md +169 -0
  8. package/.opencode/commands/opsx-propose.md +104 -0
  9. package/.opencode/commands/opsx-sync.md +140 -0
  10. package/.opencode/commands/search-intelligently.md +9 -0
  11. package/.opencode/opencode.json +20 -0
  12. package/.opencode/skills/intellisearch/.version +1 -0
  13. package/.opencode/skills/intellisearch/SKILL.md +84 -0
  14. package/.opencode/skills/intellisearch/references/brave-search.md +67 -0
  15. package/.opencode/skills/intellisearch/references/ddg-search.md +79 -0
  16. package/.opencode/skills/intellisearch/references/deepwiki-tools.md +246 -0
  17. package/.opencode/skills/intellisearch/references/examples.md +97 -0
  18. package/.opencode/skills/intellisearch/references/gh-cli.md +143 -0
  19. package/.opencode/skills/intellisearch/references/google-search.md +40 -0
  20. package/.opencode/skills/intellisearch/references/search-workflow.md +143 -0
  21. package/.opencode/skills/intellisearch/references/workflow.md +115 -0
  22. package/.opencode/skills/openspec-apply-change/SKILL.md +159 -0
  23. package/.opencode/skills/openspec-archive-change/SKILL.md +117 -0
  24. package/.opencode/skills/openspec-explore/SKILL.md +287 -0
  25. package/.opencode/skills/openspec-propose/SKILL.md +111 -0
  26. package/.opencode/skills/openspec-sync-specs/SKILL.md +147 -0
  27. package/.serena/project.yml +133 -0
  28. package/AGENTS.md +136 -0
  29. package/CHANGELOG.md +3 -0
  30. package/README.md +245 -0
  31. package/bun.lock +105 -0
  32. package/docs/INSTALL.md +185 -0
  33. package/docs/PLAYWRIGHT_CLI_API.md +306 -0
  34. package/docs/python-release-for-reference.md +175 -0
  35. package/examples/sample_json_export.json +32 -0
  36. package/examples/sample_markdown_export.md +221 -0
  37. package/examples/usage_examples.sh +222 -0
  38. package/install.ps1 +264 -0
  39. package/install.sh +275 -0
  40. package/openspec/MAESTRO_MIGRATION.md +180 -0
  41. package/openspec/changes/archive/2026-06-08-command-spec-conformance/.openspec.yaml +2 -0
  42. package/openspec/changes/archive/2026-06-08-command-spec-conformance/design.md +194 -0
  43. package/openspec/changes/archive/2026-06-08-command-spec-conformance/proposal.md +52 -0
  44. package/openspec/changes/archive/2026-06-08-command-spec-conformance/specs/multi-profile-conversations/spec.md +133 -0
  45. package/openspec/changes/archive/2026-06-08-command-spec-conformance/tasks.md +95 -0
  46. package/openspec/changes/archive/2026-06-09-refactor-interactive-prompt-loop/.openspec.yaml +2 -0
  47. package/openspec/changes/archive/2026-06-09-refactor-interactive-prompt-loop/design.md +28 -0
  48. package/openspec/changes/archive/2026-06-09-refactor-interactive-prompt-loop/proposal.md +22 -0
  49. package/openspec/changes/archive/2026-06-09-refactor-interactive-prompt-loop/specs/interactive-prompt-loop/spec.md +35 -0
  50. package/openspec/changes/archive/2026-06-09-refactor-interactive-prompt-loop/tasks.md +28 -0
  51. package/openspec/changes/archive/2026-06-09-replace-gemini-api-placeholder-with-gemini-reverse/.openspec.yaml +2 -0
  52. package/openspec/changes/archive/2026-06-09-replace-gemini-api-placeholder-with-gemini-reverse/design.md +323 -0
  53. package/openspec/changes/archive/2026-06-09-replace-gemini-api-placeholder-with-gemini-reverse/proposal.md +122 -0
  54. package/openspec/changes/archive/2026-06-09-replace-gemini-api-placeholder-with-gemini-reverse/specs/no-capability-changes/spec.md +29 -0
  55. package/openspec/changes/archive/2026-06-09-replace-gemini-api-placeholder-with-gemini-reverse/tasks.md +172 -0
  56. package/openspec/changes/archive/2026-06-09-v2-install-migration/.openspec.yaml +2 -0
  57. package/openspec/changes/archive/2026-06-09-v2-install-migration/design.md +241 -0
  58. package/openspec/changes/archive/2026-06-09-v2-install-migration/proposal.md +41 -0
  59. package/openspec/changes/archive/2026-06-09-v2-install-migration/specs/v2-installer/spec.md +205 -0
  60. package/openspec/changes/archive/2026-06-09-v2-install-migration/tasks.md +97 -0
  61. package/openspec/changes/commander-cli-parser/.openspec.yaml +2 -0
  62. package/openspec/changes/commander-cli-parser/design.md +107 -0
  63. package/openspec/changes/commander-cli-parser/proposal.md +31 -0
  64. package/openspec/changes/commander-cli-parser/specs/cli/spec.md +48 -0
  65. package/openspec/changes/commander-cli-parser/tasks.md +33 -0
  66. package/openspec/changes/cross-platform-build-and-ci/.openspec.yaml +2 -0
  67. package/openspec/changes/cross-platform-build-and-ci/design.md +213 -0
  68. package/openspec/changes/cross-platform-build-and-ci/proposal.md +50 -0
  69. package/openspec/changes/cross-platform-build-and-ci/specs/path-and-file-mediation/spec.md +335 -0
  70. package/openspec/changes/cross-platform-build-and-ci/specs/release-pipeline/spec.md +95 -0
  71. package/openspec/changes/cross-platform-build-and-ci/tasks.md +164 -0
  72. package/openspec/config.yaml +20 -0
  73. package/openspec/specs/auth/spec.md +394 -0
  74. package/openspec/specs/cli/spec.md +108 -0
  75. package/openspec/specs/commands/spec.md +409 -0
  76. package/openspec/specs/configuration/spec.md +130 -0
  77. package/openspec/specs/conversations/spec.md +139 -0
  78. package/openspec/specs/domain-model/spec.md +128 -0
  79. package/openspec/specs/formatters/spec.md +158 -0
  80. package/openspec/specs/install-browser/spec.md +146 -0
  81. package/openspec/specs/interactive-prompt-loop/spec.md +41 -0
  82. package/openspec/specs/logger/spec.md +71 -0
  83. package/openspec/specs/mediator/spec.md +72 -0
  84. package/openspec/specs/multi-profile-conversations/spec.md +145 -0
  85. package/openspec/specs/path-utils/spec.md +101 -0
  86. package/openspec/specs/profiles/spec.md +94 -0
  87. package/openspec/specs/storage/spec.md +198 -0
  88. package/openspec/specs/testing/spec.md +237 -0
  89. package/openspec/specs/v2-installer/spec.md +205 -0
  90. package/openspec/specs/validators/spec.md +65 -0
  91. package/package.json +45 -0
  92. package/scripts/build.ts +88 -0
  93. package/scripts/clean-build.ps1 +4 -0
  94. package/scripts/clean-build.sh +5 -0
  95. package/scripts/install-browser.ps1 +5 -0
  96. package/scripts/install-browser.sh +6 -0
  97. package/scripts/lint-path-mediation.ps1 +65 -0
  98. package/scripts/lint-path-mediation.sh +40 -0
  99. package/src/cli/.gitkeep +0 -0
  100. package/src/cli/command-registry.ts +62 -0
  101. package/src/cli/commands/auth-command.ts +179 -0
  102. package/src/cli/commands/continue-command.ts +158 -0
  103. package/src/cli/commands/delete-command.ts +163 -0
  104. package/src/cli/commands/export-all-command.ts +243 -0
  105. package/src/cli/commands/export-command.ts +156 -0
  106. package/src/cli/commands/fetch-command.ts +176 -0
  107. package/src/cli/commands/help.ts +45 -0
  108. package/src/cli/commands/install-browser-command.ts +33 -0
  109. package/src/cli/commands/list-command.ts +226 -0
  110. package/src/cli/commands/new-command.ts +147 -0
  111. package/src/cli/commands/profile-command.ts +226 -0
  112. package/src/cli/commands/status-command.ts +59 -0
  113. package/src/cli/index.ts +213 -0
  114. package/src/cli/utils/interactive-prompt.ts +64 -0
  115. package/src/core/.gitkeep +0 -0
  116. package/src/core/command-handlers.ts +240 -0
  117. package/src/core/errors.ts +41 -0
  118. package/src/core/mediator.ts +58 -0
  119. package/src/core/query-handlers.ts +163 -0
  120. package/src/core/types.ts +43 -0
  121. package/src/infrastructure/.gitkeep +0 -0
  122. package/src/infrastructure/cli-table.ts +45 -0
  123. package/src/infrastructure/config.ts +52 -0
  124. package/src/infrastructure/formatters.ts +130 -0
  125. package/src/infrastructure/io.ts +161 -0
  126. package/src/infrastructure/logger.ts +41 -0
  127. package/src/infrastructure/path-utils.ts +157 -0
  128. package/src/infrastructure/storage.ts +209 -0
  129. package/src/infrastructure/validators.ts +28 -0
  130. package/src/services/.gitkeep +0 -0
  131. package/src/services/auth-service.ts +135 -0
  132. package/src/services/cookie-monitor.ts +159 -0
  133. package/src/services/cookie-storage-service.ts +74 -0
  134. package/src/services/gemini-client-wrapper.ts +239 -0
  135. package/src/services/install-browser-service.ts +205 -0
  136. package/src/services/playwright-cli-driver.ts +269 -0
  137. package/src/services/profile-auth-manager.ts +65 -0
  138. package/src/services/profile-service.ts +121 -0
  139. package/tests/cli/auth-command.test.ts +220 -0
  140. package/tests/cli/command-registry.test.ts +67 -0
  141. package/tests/cli/continue-command.test.ts +100 -0
  142. package/tests/cli/delete-command.test.ts +128 -0
  143. package/tests/cli/export-all-command.test.ts +375 -0
  144. package/tests/cli/install-browser-command.test.ts +48 -0
  145. package/tests/cli/list-command.test.ts +179 -0
  146. package/tests/cli/new-command.test.ts +139 -0
  147. package/tests/cli/status-command.test.ts +106 -0
  148. package/tests/core/query-handlers.test.ts +187 -0
  149. package/tests/fixtures/auth-fixtures.ts +98 -0
  150. package/tests/fixtures/chat-fixtures.ts +87 -0
  151. package/tests/infrastructure/cli-table.test.ts +107 -0
  152. package/tests/infrastructure/formatters.test.ts +202 -0
  153. package/tests/infrastructure/io.test.ts +197 -0
  154. package/tests/infrastructure/logger.test.ts +74 -0
  155. package/tests/infrastructure/storage.test.ts +335 -0
  156. package/tests/infrastructure/validators.test.ts +67 -0
  157. package/tests/integration/commands/auth.test.ts +186 -0
  158. package/tests/integration/commands/continue.test.ts +130 -0
  159. package/tests/integration/commands/delete.test.ts +140 -0
  160. package/tests/integration/commands/export.test.ts +266 -0
  161. package/tests/integration/commands/fetch.test.ts +238 -0
  162. package/tests/integration/commands/list.test.ts +281 -0
  163. package/tests/integration/commands/profile.test.ts +258 -0
  164. package/tests/integration/commands/status.test.ts +223 -0
  165. package/tests/integration/installer-script-shape.test.ts +67 -0
  166. package/tests/parity/compare-outputs.ts +359 -0
  167. package/tests/parity/test-commands-parity.ps1 +82 -0
  168. package/tests/parity/test-commands-parity.sh +61 -0
  169. package/tests/services/auth-service.test.ts +315 -0
  170. package/tests/services/cookie-monitor.test.ts +229 -0
  171. package/tests/services/cookie-storage-service.test.ts +230 -0
  172. package/tests/services/gemini-client-wrapper.test.ts +688 -0
  173. package/tests/services/install-browser-service.test.ts +83 -0
  174. package/tests/services/playwright-cli-driver.test.ts +468 -0
  175. package/tests/services/profile-auth-manager.test.ts +334 -0
  176. package/tests/services/profile-service.test.ts +224 -0
  177. package/tests/setup.ts +95 -0
  178. package/tests/smoke/smoke.test.ts +52 -0
  179. package/tests/unit/config.test.ts +159 -0
  180. package/tests/unit/errors.test.ts +119 -0
  181. package/tests/unit/formatters.test.ts +316 -0
  182. package/tests/unit/mediator.test.ts +152 -0
  183. package/tests/unit/path-utils.test.ts +289 -0
  184. package/tsconfig.json +16 -0
@@ -0,0 +1,179 @@
1
+ import chalk from "chalk";
2
+ import { createInterface } from "node:readline";
3
+ import type { CliCommand, CliCommandContext } from "../command-registry.ts";
4
+ import { Logger } from "../../infrastructure/logger.ts";
5
+ import { CookieStorage, ProfileManager } from "../../infrastructure/storage.ts";
6
+ import { PlaywrightCliDriver } from "../../services/playwright-cli-driver.ts";
7
+ import { CookieMonitor } from "../../services/cookie-monitor.ts";
8
+ import { AuthService } from "../../services/auth-service.ts";
9
+ import { getDefaultProfileName, listProfiles, setDefaultProfileName } from "../../infrastructure/config.ts";
10
+ import { GemitermError } from "../../core/errors.ts";
11
+ import { validateProfileName } from "../../infrastructure/validators.ts";
12
+ import { formatProfileTable } from "../../infrastructure/formatters.ts";
13
+
14
+ export class AuthCommand implements CliCommand {
15
+ readonly name = "auth";
16
+ readonly description = "Authenticate with Google Gemini";
17
+
18
+ async execute(args: string[], context: CliCommandContext): Promise<void> {
19
+ if (args.includes("--help") || args.includes("-h")) {
20
+ this.showUsage();
21
+ return;
22
+ }
23
+
24
+ const logger = new Logger("auth-command");
25
+ logger.debug("Executing auth command", args);
26
+
27
+ const cookieStorage = new CookieStorage();
28
+ const profileManager = new ProfileManager(cookieStorage);
29
+ const driver = new PlaywrightCliDriver();
30
+ const cookieMonitor = new CookieMonitor({ driver, logger });
31
+ const authService = new AuthService({
32
+ driver,
33
+ cookieMonitor,
34
+ cookieStorage,
35
+ logger,
36
+ });
37
+
38
+ const profiles = listProfiles();
39
+ logger.debug("Found profiles", profiles);
40
+
41
+ if (profiles.length === 0) {
42
+ logger.debug("No profiles exist, creating first profile and authenticating");
43
+ await this.authenticateWithProfile(authService, getDefaultProfileName(), profileManager, true);
44
+ return;
45
+ }
46
+
47
+ if (profiles.length === 1) {
48
+ logger.debug("Single profile found, authenticating with", profiles[0]);
49
+ await this.authenticateWithProfile(authService, profiles[0], profileManager, false);
50
+ return;
51
+ }
52
+
53
+ const selected = await this.showProfileMenu(profiles, profileManager);
54
+ if (selected === null) {
55
+ console.log(chalk.dim("Continuing with current default profile."));
56
+ return;
57
+ }
58
+
59
+ if (selected.type === "auth") {
60
+ logger.debug("Authenticating with profile", selected.profileName);
61
+ await this.authenticateWithProfile(authService, selected.profileName, profileManager, false);
62
+ }
63
+ }
64
+
65
+ private async showProfileMenu(
66
+ profiles: string[],
67
+ profileManager: ProfileManager,
68
+ ): Promise<{ type: "auth"; profileName: string } | null> {
69
+ console.log(chalk.bold("\nProfile Management"));
70
+ console.log("");
71
+
72
+ const statuses = profiles.map((name) => profileManager.getStatus(name));
73
+ console.log(formatProfileTable(statuses));
74
+
75
+ const options = [
76
+ { key: "A", label: "Add new profile" },
77
+ { key: "D", label: "Delete profile" },
78
+ { key: "S", label: "Set default" },
79
+ { key: "R", label: "Rename profile" },
80
+ { key: "X", label: "Exit and continue with current default" },
81
+ ];
82
+
83
+ for (const opt of options) {
84
+ console.log(` ${chalk.cyan(`[${opt.key}]`)} ${opt.label}`);
85
+ }
86
+ console.log("");
87
+
88
+ const answer = await this.promptInput("Select an option");
89
+ const choice = answer.toUpperCase().trim();
90
+
91
+ switch (choice) {
92
+ case "A": {
93
+ const name = await this.promptInput("Enter profile name");
94
+ validateProfileName(name.trim());
95
+ if (listProfiles().includes(name.trim())) {
96
+ throw new GemitermError(`Profile '${name.trim()}' already exists.`);
97
+ }
98
+ profileManager.create(name.trim());
99
+ console.log(chalk.green(`Profile '${name.trim()}' created.`));
100
+ return { type: "auth", profileName: name.trim() };
101
+ }
102
+ case "D": {
103
+ const name = await this.promptInput("Enter profile name to delete");
104
+ const trimmed = name.trim();
105
+ if (!profiles.includes(trimmed)) {
106
+ throw new GemitermError(`Profile '${trimmed}' does not exist.`);
107
+ }
108
+ const confirm = await this.promptInput(`Delete profile '${trimmed}'? [y/N]`);
109
+ if (confirm.toLowerCase().startsWith("y")) {
110
+ profileManager.delete(trimmed);
111
+ console.log(chalk.green(`Profile '${trimmed}' deleted.`));
112
+ } else {
113
+ console.log(chalk.dim("Cancelled."));
114
+ }
115
+ return null;
116
+ }
117
+ case "S": {
118
+ const name = await this.promptInput("Enter profile name to set as default");
119
+ const trimmed = name.trim();
120
+ if (!profiles.includes(trimmed)) {
121
+ throw new GemitermError(`Profile '${trimmed}' does not exist.`);
122
+ }
123
+ profileManager.setDefault(trimmed);
124
+ setDefaultProfileName(trimmed);
125
+ console.log(chalk.green(`Default profile set to '${trimmed}'.`));
126
+ return null;
127
+ }
128
+ case "R": {
129
+ const oldName = await this.promptInput("Enter current profile name");
130
+ const oldTrimmed = oldName.trim();
131
+ if (!profiles.includes(oldTrimmed)) {
132
+ throw new GemitermError(`Profile '${oldTrimmed}' does not exist.`);
133
+ }
134
+ const newName = await this.promptInput("Enter new profile name");
135
+ const newTrimmed = newName.trim();
136
+ validateProfileName(newTrimmed);
137
+ profileManager.rename(oldTrimmed, newTrimmed);
138
+ console.log(chalk.green(`Profile renamed: ${oldTrimmed} → ${newTrimmed}`));
139
+ return { type: "auth", profileName: newTrimmed };
140
+ }
141
+ case "X":
142
+ default:
143
+ return null;
144
+ }
145
+ }
146
+
147
+ private async authenticateWithProfile(
148
+ authService: AuthService,
149
+ profileName: string,
150
+ profileManager: ProfileManager,
151
+ createFirst: boolean,
152
+ ): Promise<void> {
153
+ if (createFirst) {
154
+ profileManager.create(profileName);
155
+ console.log(chalk.dim(`Created profile: ${profileName}`));
156
+ }
157
+
158
+ await authService.authenticate(profileName);
159
+ }
160
+
161
+ private showUsage(): void {
162
+ console.log(chalk.bold("Usage: gemiterm auth"));
163
+ console.log("");
164
+ console.log("Authenticate with Google Gemini.");
165
+ console.log("");
166
+ console.log("Options:");
167
+ console.log(" -h, --help Show this help message");
168
+ }
169
+
170
+ private promptInput(prompt: string): Promise<string> {
171
+ return new Promise<string>((resolve) => {
172
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
173
+ rl.question(`${chalk.cyan(prompt + ": ")} `, (answer) => {
174
+ rl.close();
175
+ resolve(answer);
176
+ });
177
+ });
178
+ }
179
+ }
@@ -0,0 +1,158 @@
1
+ import chalk from "chalk";
2
+ import type { CliCommand, CliCommandContext } from "../command-registry.ts";
3
+ import type { Mediator, Command } from "../../core/mediator.ts";
4
+ import { Logger } from "../../infrastructure/logger.ts";
5
+ import { AuthenticationError } from "../../core/errors.ts";
6
+ import {
7
+ COMMAND_TYPES,
8
+ type SendMessageCommandPayload,
9
+ type SendMessageCommandResult,
10
+ } from "../../core/command-handlers.ts";
11
+ import { runInteractiveLoop, type MessageHandlerResult } from "../utils/interactive-prompt.ts";
12
+
13
+ interface ContinueCommandOptions {
14
+ help: boolean;
15
+ }
16
+
17
+ const DEFAULT_OPTIONS: ContinueCommandOptions = {
18
+ help: false,
19
+ };
20
+
21
+ export class ContinueCommand implements CliCommand {
22
+ readonly name = "continue";
23
+ readonly description = "Continue a conversation";
24
+
25
+ async execute(args: string[], context: CliCommandContext): Promise<void> {
26
+ const logger = new Logger("continue-command");
27
+ const options = this.parseArgs(args);
28
+
29
+ if (options.help) {
30
+ this.showUsage();
31
+ return;
32
+ }
33
+
34
+ let conversationId: string | null = null;
35
+ let message: string | null = null;
36
+
37
+ for (const arg of args) {
38
+ if (arg.startsWith("--") || arg.startsWith("-")) continue;
39
+ if (!conversationId) {
40
+ conversationId = arg;
41
+ } else if (!message) {
42
+ message = arg;
43
+ }
44
+ }
45
+
46
+ if (!conversationId) {
47
+ await this.invokeListCommand(context);
48
+ return;
49
+ }
50
+
51
+ const mediator: Mediator = context.mediator;
52
+
53
+ if (message) {
54
+ await this.sendNonInteractive(mediator, conversationId, message, logger, context);
55
+ } else {
56
+ await this.startInteractive(mediator, conversationId, logger, context);
57
+ }
58
+ }
59
+
60
+ private async sendNonInteractive(
61
+ mediator: Mediator,
62
+ conversationId: string,
63
+ message: string,
64
+ logger: Logger,
65
+ context: CliCommandContext,
66
+ ): Promise<void> {
67
+ const profileName = await this.resolveProfile(context, conversationId);
68
+ logger.debug(`Sending message to ${conversationId}`);
69
+ const result = await mediator.send<SendMessageCommandResult>({
70
+ type: COMMAND_TYPES.SEND_MESSAGE,
71
+ payload: { conversationId, message, profileName: profileName ?? undefined },
72
+ } as Command<SendMessageCommandPayload>);
73
+
74
+ console.log(chalk.blue.bold("Model:"));
75
+ console.log(result.response);
76
+ }
77
+
78
+ private async resolveProfile(context: CliCommandContext, conversationId: string): Promise<string | null> {
79
+ const profiles = context.profileAuthManager.getActiveProfiles();
80
+ if (profiles.length <= 1) {
81
+ return null;
82
+ }
83
+ const profileName = await context.profileAuthManager.findProfileForConversation(conversationId);
84
+ if (profileName === null) {
85
+ throw new AuthenticationError(
86
+ `Could not find a profile that owns conversation '${conversationId}'. Run 'gemiterm list --all-profiles' to see which profile it belongs to, then 'gemiterm continue ${conversationId} <msg> --profile <name>' to specify the profile explicitly.`,
87
+ );
88
+ }
89
+ return profileName;
90
+ }
91
+
92
+ private async startInteractive(
93
+ mediator: Mediator,
94
+ conversationId: string,
95
+ logger: Logger,
96
+ context: CliCommandContext,
97
+ ): Promise<void> {
98
+ const profileName = await this.resolveProfile(context, conversationId);
99
+
100
+ const messageHandler = async (message: string): Promise<MessageHandlerResult> => {
101
+ logger.debug(`Sending message to ${conversationId}`);
102
+ const result = await mediator.send<SendMessageCommandResult>({
103
+ type: COMMAND_TYPES.SEND_MESSAGE,
104
+ payload: { conversationId, message, profileName: profileName ?? undefined },
105
+ } as Command<SendMessageCommandPayload>);
106
+
107
+ return { response: result.response };
108
+ };
109
+
110
+ await runInteractiveLoop(messageHandler, { profileName });
111
+ }
112
+
113
+ private async invokeListCommand(context: CliCommandContext): Promise<void> {
114
+ const { CommandRegistry } = await import("../command-registry.ts");
115
+ const registry = new CommandRegistry();
116
+ registry.registerAllCommands();
117
+
118
+ const listHandler = registry.getHandler("list");
119
+ if (listHandler) {
120
+ console.log(chalk.dim("No conversation ID specified. Listing conversations:\n"));
121
+ await listHandler.execute([], context);
122
+ } else {
123
+ console.error("Could not invoke list command.");
124
+ process.exit(1);
125
+ }
126
+ }
127
+
128
+ private parseArgs(args: string[]): ContinueCommandOptions {
129
+ const options = { ...DEFAULT_OPTIONS };
130
+
131
+ for (const arg of args) {
132
+ if (arg === "--help" || arg === "-h") {
133
+ options.help = true;
134
+ }
135
+ }
136
+
137
+ return options;
138
+ }
139
+
140
+ private showUsage(): void {
141
+ console.log(chalk.bold("Usage: gemiterm continue [conversation_id] [message] [options]"));
142
+ console.log("");
143
+ console.log(chalk.bold("Arguments:"));
144
+ console.log(
145
+ ` ${chalk.cyan("conversation_id".padEnd(20))}${chalk.dim("ID of the conversation to continue (optional)")}`,
146
+ );
147
+ console.log(
148
+ ` ${chalk.cyan("message".padEnd(20))}${chalk.dim("Message to send (optional, starts interactive mode if omitted)")}`,
149
+ );
150
+ console.log("");
151
+ console.log(chalk.bold("Options:"));
152
+ console.log(` ${chalk.cyan("--help, -h".padEnd(22))}${chalk.dim("Show this help message")}`);
153
+ console.log("");
154
+ console.log(chalk.dim("If no conversation_id is provided, the list command will be invoked."));
155
+ console.log(chalk.dim("If no message is provided, an interactive chat session will start."));
156
+ console.log(chalk.dim("In interactive mode, type /exit or /quit to exit."));
157
+ }
158
+ }
@@ -0,0 +1,163 @@
1
+ import chalk from "chalk";
2
+ import { createInterface } from "node:readline";
3
+ import type { CliCommand, CliCommandContext } from "../command-registry.ts";
4
+ import type { Mediator, Command } from "../../core/mediator.ts";
5
+ import { Logger } from "../../infrastructure/logger.ts";
6
+ import { AuthenticationError } from "../../core/errors.ts";
7
+ import {
8
+ COMMAND_TYPES,
9
+ type DeleteConversationCommandPayload,
10
+ type DeleteConversationCommandResult,
11
+ } from "../../core/command-handlers.ts";
12
+ import { validateConversationId } from "../../infrastructure/validators.ts";
13
+
14
+ interface DeleteCommandOptions {
15
+ help: boolean;
16
+ force: boolean;
17
+ }
18
+
19
+ const DEFAULT_OPTIONS: DeleteCommandOptions = {
20
+ help: false,
21
+ force: false,
22
+ };
23
+
24
+ export class DeleteCommand implements CliCommand {
25
+ readonly name = "delete";
26
+ readonly description = "Delete a conversation";
27
+
28
+ async execute(args: string[], context: CliCommandContext): Promise<void> {
29
+ const logger = new Logger("delete-command");
30
+ const options = this.parseArgs(args);
31
+
32
+ if (options.help) {
33
+ this.showUsage();
34
+ return;
35
+ }
36
+
37
+ const conversationId = this.extractConversationId(args);
38
+
39
+ if (!conversationId) {
40
+ console.error(chalk.red("Error: conversation ID is required."));
41
+ this.showUsage();
42
+ process.exit(1);
43
+ }
44
+
45
+ try {
46
+ validateConversationId(conversationId);
47
+ } catch (error) {
48
+ console.error(chalk.red(`Error: ${(error as Error).message}`));
49
+ process.exit(1);
50
+ }
51
+
52
+ const profileName = await this.resolveProfile(context, conversationId);
53
+
54
+ if (!options.force) {
55
+ const confirmed = await this.promptConfirmation(conversationId);
56
+ if (!confirmed) {
57
+ console.log(chalk.dim("Cancelled."));
58
+ return;
59
+ }
60
+ }
61
+
62
+ const mediator: Mediator = context.mediator;
63
+ const payload: DeleteConversationCommandPayload = { conversationId, profileName: profileName ?? undefined };
64
+
65
+ logger.debug(`Sending delete-conversation command: ${JSON.stringify(payload)}`);
66
+
67
+ try {
68
+ const result = await mediator.send<DeleteConversationCommandResult>({
69
+ type: COMMAND_TYPES.DELETE_CONVERSATION,
70
+ payload,
71
+ } as Command<DeleteConversationCommandPayload>);
72
+
73
+ if (result.success) {
74
+ console.log(chalk.green(`Conversation '${chalk.cyan(conversationId)}' deleted.`));
75
+ logger.info(`Deleted conversation: ${conversationId}`);
76
+ } else {
77
+ console.error(chalk.red("Failed to delete conversation."));
78
+ process.exit(1);
79
+ }
80
+ } catch (error) {
81
+ const message = error instanceof Error ? error.message : String(error);
82
+ console.error(chalk.red(`Error: ${message}`));
83
+ process.exit(1);
84
+ }
85
+ }
86
+
87
+ private async resolveProfile(context: CliCommandContext, conversationId: string): Promise<string | null> {
88
+ const profiles = context.profileAuthManager.getActiveProfiles();
89
+ if (profiles.length <= 1) {
90
+ return null;
91
+ }
92
+ const profileName = await context.profileAuthManager.findProfileForConversation(conversationId);
93
+ if (profileName === null) {
94
+ throw new AuthenticationError(
95
+ `Could not find a profile that owns conversation '${conversationId}'. Run 'gemiterm list --all-profiles' to see which profile it belongs to, then 'gemiterm delete ${conversationId} --profile <name>' to specify the profile explicitly.`,
96
+ );
97
+ }
98
+ return profileName;
99
+ }
100
+
101
+ private extractConversationId(args: string[]): string | null {
102
+ for (const arg of args) {
103
+ if (arg.startsWith("--") || arg.startsWith("-")) continue;
104
+ return arg;
105
+ }
106
+ return null;
107
+ }
108
+
109
+ private parseArgs(args: string[]): DeleteCommandOptions {
110
+ const options = { ...DEFAULT_OPTIONS };
111
+
112
+ for (let i = 0; i < args.length; i++) {
113
+ const arg = args[i];
114
+ switch (arg) {
115
+ case "--help":
116
+ case "-h":
117
+ options.help = true;
118
+ break;
119
+ case "--force":
120
+ case "-f":
121
+ options.force = true;
122
+ break;
123
+ }
124
+ }
125
+
126
+ return options;
127
+ }
128
+
129
+ private promptConfirmation(conversationId: string): Promise<boolean> {
130
+ return new Promise<boolean>((resolve) => {
131
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
132
+ rl.question(
133
+ `${chalk.cyan(`Delete conversation '${conversationId}'?`)} (yes/no): `,
134
+ (answer) => {
135
+ rl.close();
136
+ resolve(answer.toLowerCase().startsWith("y"));
137
+ },
138
+ );
139
+ });
140
+ }
141
+
142
+ private showUsage(): void {
143
+ console.log(chalk.bold("Usage: gemiterm delete <conversation_id> [options]"));
144
+ console.log("");
145
+ console.log(chalk.bold("Arguments:"));
146
+ console.log(
147
+ ` ${chalk.cyan("conversation_id".padEnd(20))}${chalk.dim("ID of the conversation to delete")}`,
148
+ );
149
+ console.log("");
150
+ console.log(chalk.bold("Options:"));
151
+
152
+ const flags = [
153
+ { flag: "--force, -f", desc: "Skip confirmation prompt" },
154
+ { flag: "--help, -h", desc: "Show this help message" },
155
+ ];
156
+
157
+ const maxLen = Math.max(...flags.map((f) => f.flag.length));
158
+ for (const f of flags) {
159
+ const padded = f.flag.padEnd(maxLen + 2);
160
+ console.log(` ${chalk.cyan(padded)}${chalk.dim(f.desc)}`);
161
+ }
162
+ }
163
+ }