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.
- package/.env +2 -0
- package/.github/workflows/manual-release.yml +34 -0
- package/.github/workflows/release.yml +198 -0
- package/.github/workflows/test.yml +29 -0
- package/.opencode/commands/opsx-apply.md +152 -0
- package/.opencode/commands/opsx-archive.md +157 -0
- package/.opencode/commands/opsx-explore.md +169 -0
- package/.opencode/commands/opsx-propose.md +104 -0
- package/.opencode/commands/opsx-sync.md +140 -0
- package/.opencode/commands/search-intelligently.md +9 -0
- package/.opencode/opencode.json +20 -0
- package/.opencode/skills/intellisearch/.version +1 -0
- package/.opencode/skills/intellisearch/SKILL.md +84 -0
- package/.opencode/skills/intellisearch/references/brave-search.md +67 -0
- package/.opencode/skills/intellisearch/references/ddg-search.md +79 -0
- package/.opencode/skills/intellisearch/references/deepwiki-tools.md +246 -0
- package/.opencode/skills/intellisearch/references/examples.md +97 -0
- package/.opencode/skills/intellisearch/references/gh-cli.md +143 -0
- package/.opencode/skills/intellisearch/references/google-search.md +40 -0
- package/.opencode/skills/intellisearch/references/search-workflow.md +143 -0
- package/.opencode/skills/intellisearch/references/workflow.md +115 -0
- package/.opencode/skills/openspec-apply-change/SKILL.md +159 -0
- package/.opencode/skills/openspec-archive-change/SKILL.md +117 -0
- package/.opencode/skills/openspec-explore/SKILL.md +287 -0
- package/.opencode/skills/openspec-propose/SKILL.md +111 -0
- package/.opencode/skills/openspec-sync-specs/SKILL.md +147 -0
- package/.serena/project.yml +133 -0
- package/AGENTS.md +136 -0
- package/CHANGELOG.md +3 -0
- package/README.md +245 -0
- package/bun.lock +105 -0
- package/docs/INSTALL.md +185 -0
- package/docs/PLAYWRIGHT_CLI_API.md +306 -0
- package/docs/python-release-for-reference.md +175 -0
- package/examples/sample_json_export.json +32 -0
- package/examples/sample_markdown_export.md +221 -0
- package/examples/usage_examples.sh +222 -0
- package/install.ps1 +264 -0
- package/install.sh +275 -0
- package/openspec/MAESTRO_MIGRATION.md +180 -0
- package/openspec/changes/archive/2026-06-08-command-spec-conformance/.openspec.yaml +2 -0
- package/openspec/changes/archive/2026-06-08-command-spec-conformance/design.md +194 -0
- package/openspec/changes/archive/2026-06-08-command-spec-conformance/proposal.md +52 -0
- package/openspec/changes/archive/2026-06-08-command-spec-conformance/specs/multi-profile-conversations/spec.md +133 -0
- package/openspec/changes/archive/2026-06-08-command-spec-conformance/tasks.md +95 -0
- package/openspec/changes/archive/2026-06-09-refactor-interactive-prompt-loop/.openspec.yaml +2 -0
- package/openspec/changes/archive/2026-06-09-refactor-interactive-prompt-loop/design.md +28 -0
- package/openspec/changes/archive/2026-06-09-refactor-interactive-prompt-loop/proposal.md +22 -0
- package/openspec/changes/archive/2026-06-09-refactor-interactive-prompt-loop/specs/interactive-prompt-loop/spec.md +35 -0
- package/openspec/changes/archive/2026-06-09-refactor-interactive-prompt-loop/tasks.md +28 -0
- package/openspec/changes/archive/2026-06-09-replace-gemini-api-placeholder-with-gemini-reverse/.openspec.yaml +2 -0
- package/openspec/changes/archive/2026-06-09-replace-gemini-api-placeholder-with-gemini-reverse/design.md +323 -0
- package/openspec/changes/archive/2026-06-09-replace-gemini-api-placeholder-with-gemini-reverse/proposal.md +122 -0
- package/openspec/changes/archive/2026-06-09-replace-gemini-api-placeholder-with-gemini-reverse/specs/no-capability-changes/spec.md +29 -0
- package/openspec/changes/archive/2026-06-09-replace-gemini-api-placeholder-with-gemini-reverse/tasks.md +172 -0
- package/openspec/changes/archive/2026-06-09-v2-install-migration/.openspec.yaml +2 -0
- package/openspec/changes/archive/2026-06-09-v2-install-migration/design.md +241 -0
- package/openspec/changes/archive/2026-06-09-v2-install-migration/proposal.md +41 -0
- package/openspec/changes/archive/2026-06-09-v2-install-migration/specs/v2-installer/spec.md +205 -0
- package/openspec/changes/archive/2026-06-09-v2-install-migration/tasks.md +97 -0
- package/openspec/changes/commander-cli-parser/.openspec.yaml +2 -0
- package/openspec/changes/commander-cli-parser/design.md +107 -0
- package/openspec/changes/commander-cli-parser/proposal.md +31 -0
- package/openspec/changes/commander-cli-parser/specs/cli/spec.md +48 -0
- package/openspec/changes/commander-cli-parser/tasks.md +33 -0
- package/openspec/changes/cross-platform-build-and-ci/.openspec.yaml +2 -0
- package/openspec/changes/cross-platform-build-and-ci/design.md +213 -0
- package/openspec/changes/cross-platform-build-and-ci/proposal.md +50 -0
- package/openspec/changes/cross-platform-build-and-ci/specs/path-and-file-mediation/spec.md +335 -0
- package/openspec/changes/cross-platform-build-and-ci/specs/release-pipeline/spec.md +95 -0
- package/openspec/changes/cross-platform-build-and-ci/tasks.md +164 -0
- package/openspec/config.yaml +20 -0
- package/openspec/specs/auth/spec.md +394 -0
- package/openspec/specs/cli/spec.md +108 -0
- package/openspec/specs/commands/spec.md +409 -0
- package/openspec/specs/configuration/spec.md +130 -0
- package/openspec/specs/conversations/spec.md +139 -0
- package/openspec/specs/domain-model/spec.md +128 -0
- package/openspec/specs/formatters/spec.md +158 -0
- package/openspec/specs/install-browser/spec.md +146 -0
- package/openspec/specs/interactive-prompt-loop/spec.md +41 -0
- package/openspec/specs/logger/spec.md +71 -0
- package/openspec/specs/mediator/spec.md +72 -0
- package/openspec/specs/multi-profile-conversations/spec.md +145 -0
- package/openspec/specs/path-utils/spec.md +101 -0
- package/openspec/specs/profiles/spec.md +94 -0
- package/openspec/specs/storage/spec.md +198 -0
- package/openspec/specs/testing/spec.md +237 -0
- package/openspec/specs/v2-installer/spec.md +205 -0
- package/openspec/specs/validators/spec.md +65 -0
- package/package.json +45 -0
- package/scripts/build.ts +88 -0
- package/scripts/clean-build.ps1 +4 -0
- package/scripts/clean-build.sh +5 -0
- package/scripts/install-browser.ps1 +5 -0
- package/scripts/install-browser.sh +6 -0
- package/scripts/lint-path-mediation.ps1 +65 -0
- package/scripts/lint-path-mediation.sh +40 -0
- package/src/cli/.gitkeep +0 -0
- package/src/cli/command-registry.ts +62 -0
- package/src/cli/commands/auth-command.ts +179 -0
- package/src/cli/commands/continue-command.ts +158 -0
- package/src/cli/commands/delete-command.ts +163 -0
- package/src/cli/commands/export-all-command.ts +243 -0
- package/src/cli/commands/export-command.ts +156 -0
- package/src/cli/commands/fetch-command.ts +176 -0
- package/src/cli/commands/help.ts +45 -0
- package/src/cli/commands/install-browser-command.ts +33 -0
- package/src/cli/commands/list-command.ts +226 -0
- package/src/cli/commands/new-command.ts +147 -0
- package/src/cli/commands/profile-command.ts +226 -0
- package/src/cli/commands/status-command.ts +59 -0
- package/src/cli/index.ts +213 -0
- package/src/cli/utils/interactive-prompt.ts +64 -0
- package/src/core/.gitkeep +0 -0
- package/src/core/command-handlers.ts +240 -0
- package/src/core/errors.ts +41 -0
- package/src/core/mediator.ts +58 -0
- package/src/core/query-handlers.ts +163 -0
- package/src/core/types.ts +43 -0
- package/src/infrastructure/.gitkeep +0 -0
- package/src/infrastructure/cli-table.ts +45 -0
- package/src/infrastructure/config.ts +52 -0
- package/src/infrastructure/formatters.ts +130 -0
- package/src/infrastructure/io.ts +161 -0
- package/src/infrastructure/logger.ts +41 -0
- package/src/infrastructure/path-utils.ts +157 -0
- package/src/infrastructure/storage.ts +209 -0
- package/src/infrastructure/validators.ts +28 -0
- package/src/services/.gitkeep +0 -0
- package/src/services/auth-service.ts +135 -0
- package/src/services/cookie-monitor.ts +159 -0
- package/src/services/cookie-storage-service.ts +74 -0
- package/src/services/gemini-client-wrapper.ts +239 -0
- package/src/services/install-browser-service.ts +205 -0
- package/src/services/playwright-cli-driver.ts +269 -0
- package/src/services/profile-auth-manager.ts +65 -0
- package/src/services/profile-service.ts +121 -0
- package/tests/cli/auth-command.test.ts +220 -0
- package/tests/cli/command-registry.test.ts +67 -0
- package/tests/cli/continue-command.test.ts +100 -0
- package/tests/cli/delete-command.test.ts +128 -0
- package/tests/cli/export-all-command.test.ts +375 -0
- package/tests/cli/install-browser-command.test.ts +48 -0
- package/tests/cli/list-command.test.ts +179 -0
- package/tests/cli/new-command.test.ts +139 -0
- package/tests/cli/status-command.test.ts +106 -0
- package/tests/core/query-handlers.test.ts +187 -0
- package/tests/fixtures/auth-fixtures.ts +98 -0
- package/tests/fixtures/chat-fixtures.ts +87 -0
- package/tests/infrastructure/cli-table.test.ts +107 -0
- package/tests/infrastructure/formatters.test.ts +202 -0
- package/tests/infrastructure/io.test.ts +197 -0
- package/tests/infrastructure/logger.test.ts +74 -0
- package/tests/infrastructure/storage.test.ts +335 -0
- package/tests/infrastructure/validators.test.ts +67 -0
- package/tests/integration/commands/auth.test.ts +186 -0
- package/tests/integration/commands/continue.test.ts +130 -0
- package/tests/integration/commands/delete.test.ts +140 -0
- package/tests/integration/commands/export.test.ts +266 -0
- package/tests/integration/commands/fetch.test.ts +238 -0
- package/tests/integration/commands/list.test.ts +281 -0
- package/tests/integration/commands/profile.test.ts +258 -0
- package/tests/integration/commands/status.test.ts +223 -0
- package/tests/integration/installer-script-shape.test.ts +67 -0
- package/tests/parity/compare-outputs.ts +359 -0
- package/tests/parity/test-commands-parity.ps1 +82 -0
- package/tests/parity/test-commands-parity.sh +61 -0
- package/tests/services/auth-service.test.ts +315 -0
- package/tests/services/cookie-monitor.test.ts +229 -0
- package/tests/services/cookie-storage-service.test.ts +230 -0
- package/tests/services/gemini-client-wrapper.test.ts +688 -0
- package/tests/services/install-browser-service.test.ts +83 -0
- package/tests/services/playwright-cli-driver.test.ts +468 -0
- package/tests/services/profile-auth-manager.test.ts +334 -0
- package/tests/services/profile-service.test.ts +224 -0
- package/tests/setup.ts +95 -0
- package/tests/smoke/smoke.test.ts +52 -0
- package/tests/unit/config.test.ts +159 -0
- package/tests/unit/errors.test.ts +119 -0
- package/tests/unit/formatters.test.ts +316 -0
- package/tests/unit/mediator.test.ts +152 -0
- package/tests/unit/path-utils.test.ts +289 -0
- package/tsconfig.json +16 -0
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import type { CliCommand, CliCommandContext } from "../command-registry.ts";
|
|
3
|
+
import type { Mediator, Query } from "../../core/mediator.ts";
|
|
4
|
+
import { Logger } from "../../infrastructure/logger.ts";
|
|
5
|
+
import type { ChatInfo } from "../../core/types.ts";
|
|
6
|
+
import {
|
|
7
|
+
QUERY_TYPES,
|
|
8
|
+
type ListChatsQueryPayload,
|
|
9
|
+
type ListChatsQueryResult,
|
|
10
|
+
type FetchChatQueryPayload,
|
|
11
|
+
type FetchChatQueryResult,
|
|
12
|
+
} from "../../core/query-handlers.ts";
|
|
13
|
+
import { formatChatAsMarkdown } from "../../infrastructure/formatters.ts";
|
|
14
|
+
import { ensureDir, writeTextFile } from "../../infrastructure/io.ts";
|
|
15
|
+
import { joinPath, resolvePath } from "../../infrastructure/path-utils.ts";
|
|
16
|
+
|
|
17
|
+
interface ExportAllCommandOptions {
|
|
18
|
+
help: boolean;
|
|
19
|
+
outputDir: string;
|
|
20
|
+
since: string;
|
|
21
|
+
includeMetadata: boolean;
|
|
22
|
+
allProfiles: boolean;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const DEFAULT_OPTIONS: ExportAllCommandOptions = {
|
|
26
|
+
help: false,
|
|
27
|
+
outputDir: "./exports",
|
|
28
|
+
since: "",
|
|
29
|
+
includeMetadata: false,
|
|
30
|
+
allProfiles: false,
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
interface ExportResult {
|
|
34
|
+
id: string;
|
|
35
|
+
title: string;
|
|
36
|
+
filePath: string;
|
|
37
|
+
success: boolean;
|
|
38
|
+
error?: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export class ExportAllCommand implements CliCommand {
|
|
42
|
+
readonly name = "export-all";
|
|
43
|
+
readonly description = "Export all conversations to files";
|
|
44
|
+
|
|
45
|
+
async execute(args: string[], context: CliCommandContext): Promise<void> {
|
|
46
|
+
const logger = new Logger("export-all-command");
|
|
47
|
+
const options = this.parseArgs(args);
|
|
48
|
+
|
|
49
|
+
if (options.help) {
|
|
50
|
+
this.showUsage();
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const mediator: Mediator = context.mediator;
|
|
55
|
+
|
|
56
|
+
logger.debug("Sending list-chats query for export-all");
|
|
57
|
+
|
|
58
|
+
try {
|
|
59
|
+
const listResult = await mediator.send<ListChatsQueryResult>({
|
|
60
|
+
type: QUERY_TYPES.LIST_CHATS,
|
|
61
|
+
payload: {
|
|
62
|
+
allProfiles: options.allProfiles,
|
|
63
|
+
} as ListChatsQueryPayload,
|
|
64
|
+
} as Query<ListChatsQueryPayload>);
|
|
65
|
+
|
|
66
|
+
let chats = listResult.chats;
|
|
67
|
+
|
|
68
|
+
chats = this.applyDateFilter(chats, options.since);
|
|
69
|
+
|
|
70
|
+
if (chats.length === 0) {
|
|
71
|
+
console.log(chalk.dim("No conversations found to export."));
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
console.log(chalk.bold(`Found ${chats.length} conversation${chats.length !== 1 ? "s" : ""} to export.`));
|
|
76
|
+
console.log("");
|
|
77
|
+
|
|
78
|
+
const outputDir = resolvePath(options.outputDir);
|
|
79
|
+
ensureDir(outputDir);
|
|
80
|
+
|
|
81
|
+
const results: ExportResult[] = [];
|
|
82
|
+
|
|
83
|
+
for (let i = 0; i < chats.length; i++) {
|
|
84
|
+
const chat = chats[i];
|
|
85
|
+
const progress = `[${i + 1}/${chats.length}]`;
|
|
86
|
+
process.stdout.write(` ${chalk.dim(progress)} Exporting ${chalk.cyan(chat.id)}...`);
|
|
87
|
+
|
|
88
|
+
try {
|
|
89
|
+
const fetchResult = await mediator.send<FetchChatQueryResult>({
|
|
90
|
+
type: QUERY_TYPES.FETCH_CHAT,
|
|
91
|
+
payload: { conversationId: chat.id } as FetchChatQueryPayload,
|
|
92
|
+
} as Query<FetchChatQueryPayload>);
|
|
93
|
+
|
|
94
|
+
const filename = this.sanitizeFilename(chat.title || chat.id);
|
|
95
|
+
const filePath = joinPath(outputDir, `${filename}.md`);
|
|
96
|
+
const content = formatChatAsMarkdown(
|
|
97
|
+
fetchResult.messages,
|
|
98
|
+
chat.title,
|
|
99
|
+
chat.id,
|
|
100
|
+
options.includeMetadata,
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
writeTextFile(filePath, content);
|
|
104
|
+
|
|
105
|
+
results.push({ id: chat.id, title: chat.title, filePath, success: true });
|
|
106
|
+
process.stdout.write(chalk.green(" OK\n"));
|
|
107
|
+
} catch (error) {
|
|
108
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
109
|
+
results.push({ id: chat.id, title: chat.title, filePath: "", success: false, error: message });
|
|
110
|
+
process.stdout.write(chalk.red(" FAILED\n"));
|
|
111
|
+
logger.warn(`Failed to export ${chat.id}: ${message}`);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
this.writeIndex(outputDir, results, options.includeMetadata);
|
|
116
|
+
this.printSummary(results, outputDir);
|
|
117
|
+
} catch (error) {
|
|
118
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
119
|
+
console.error(chalk.red(`Error: ${message}`));
|
|
120
|
+
process.exit(1);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
private applyDateFilter(chats: ChatInfo[], since: string): ChatInfo[] {
|
|
125
|
+
if (!since) return chats;
|
|
126
|
+
const sinceDate = new Date(since);
|
|
127
|
+
if (isNaN(sinceDate.getTime())) return chats;
|
|
128
|
+
return chats.filter((chat) => new Date(chat.timestamp) >= sinceDate);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
private sanitizeFilename(title: string): string {
|
|
132
|
+
const date = new Date().toISOString().slice(0, 10);
|
|
133
|
+
const safe = title
|
|
134
|
+
.replace(/[/\\?%*:|"<>]/g, "-")
|
|
135
|
+
.replace(/\s+/g, "-")
|
|
136
|
+
.replace(/-+/g, "-")
|
|
137
|
+
.slice(0, 60);
|
|
138
|
+
return `gemini-chat-${safe}-${date}`.replace(/-+$/, "");
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
private writeIndex(outputDir: string, results: ExportResult[], includeMetadata: boolean): void {
|
|
142
|
+
const lines: string[] = [];
|
|
143
|
+
|
|
144
|
+
lines.push("# Exported Conversations");
|
|
145
|
+
lines.push("");
|
|
146
|
+
lines.push(`> Total: ${results.length}`);
|
|
147
|
+
lines.push(`> Exported: ${new Date().toISOString()}`);
|
|
148
|
+
lines.push("");
|
|
149
|
+
|
|
150
|
+
if (includeMetadata) {
|
|
151
|
+
const succeeded = results.filter((r) => r.success).length;
|
|
152
|
+
const failed = results.filter((r) => !r.success).length;
|
|
153
|
+
lines.push(`> Successful: ${succeeded} | Failed: ${failed}`);
|
|
154
|
+
lines.push("");
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
lines.push("## Conversations");
|
|
158
|
+
lines.push("");
|
|
159
|
+
|
|
160
|
+
const succeeded = results.filter((r) => r.success);
|
|
161
|
+
for (const result of succeeded) {
|
|
162
|
+
const filename = result.filePath.split(/[\\/]/).pop() || result.filePath;
|
|
163
|
+
lines.push(`- [${result.title || result.id}](${filename})`);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const failed = results.filter((r) => !r.success);
|
|
167
|
+
if (failed.length > 0) {
|
|
168
|
+
lines.push("");
|
|
169
|
+
lines.push("## Failed Exports");
|
|
170
|
+
lines.push("");
|
|
171
|
+
for (const result of failed) {
|
|
172
|
+
lines.push(`- **${result.title || result.id}**: ${result.error || "Unknown error"}`);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const indexPath = joinPath(outputDir, "index.md");
|
|
177
|
+
writeTextFile(indexPath, lines.join("\n"));
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
private printSummary(results: ExportResult[], outputDir: string): void {
|
|
181
|
+
const succeeded = results.filter((r) => r.success);
|
|
182
|
+
const failed = results.filter((r) => !r.success);
|
|
183
|
+
|
|
184
|
+
console.log("");
|
|
185
|
+
console.log(chalk.bold("Export Summary:"));
|
|
186
|
+
console.log(` Exported: ${chalk.green(succeeded.length)}`);
|
|
187
|
+
if (failed.length > 0) {
|
|
188
|
+
console.log(` Failed: ${chalk.red(failed.length)}`);
|
|
189
|
+
}
|
|
190
|
+
console.log(` Output: ${outputDir}`);
|
|
191
|
+
console.log(` Index: ${chalk.cyan(joinPath(outputDir, "index.md"))}`);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
private parseArgs(args: string[]): ExportAllCommandOptions {
|
|
195
|
+
const options = { ...DEFAULT_OPTIONS };
|
|
196
|
+
|
|
197
|
+
for (let i = 0; i < args.length; i++) {
|
|
198
|
+
const arg = args[i];
|
|
199
|
+
switch (arg) {
|
|
200
|
+
case "--help":
|
|
201
|
+
case "-h":
|
|
202
|
+
options.help = true;
|
|
203
|
+
break;
|
|
204
|
+
case "--output-dir":
|
|
205
|
+
case "-o":
|
|
206
|
+
options.outputDir = args[++i] ?? DEFAULT_OPTIONS.outputDir;
|
|
207
|
+
break;
|
|
208
|
+
case "--since":
|
|
209
|
+
options.since = args[++i] ?? "";
|
|
210
|
+
break;
|
|
211
|
+
case "--include-metadata":
|
|
212
|
+
options.includeMetadata = true;
|
|
213
|
+
break;
|
|
214
|
+
case "--all-profiles":
|
|
215
|
+
case "-a":
|
|
216
|
+
options.allProfiles = true;
|
|
217
|
+
break;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return options;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
private showUsage(): void {
|
|
225
|
+
console.log(chalk.bold("Usage: gemiterm export-all [options]"));
|
|
226
|
+
console.log("");
|
|
227
|
+
console.log(chalk.bold("Options:"));
|
|
228
|
+
|
|
229
|
+
const flags = [
|
|
230
|
+
{ flag: "--output-dir, -o <dir>", desc: "Output directory (default: ./exports)" },
|
|
231
|
+
{ flag: "--since <date>", desc: "Only export chats from this date onwards" },
|
|
232
|
+
{ flag: "--include-metadata", desc: "Include metadata headers in exports" },
|
|
233
|
+
{ flag: "--all-profiles, -a", desc: "Export conversations from all profiles" },
|
|
234
|
+
{ flag: "--help, -h", desc: "Show this help message" },
|
|
235
|
+
];
|
|
236
|
+
|
|
237
|
+
const maxLen = Math.max(...flags.map((f) => f.flag.length));
|
|
238
|
+
for (const f of flags) {
|
|
239
|
+
const padded = f.flag.padEnd(maxLen + 2);
|
|
240
|
+
console.log(` ${chalk.cyan(padded)}${chalk.dim(f.desc)}`);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import type { CliCommand, CliCommandContext } from "../command-registry.ts";
|
|
3
|
+
import type { Mediator, Query } from "../../core/mediator.ts";
|
|
4
|
+
import { Logger } from "../../infrastructure/logger.ts";
|
|
5
|
+
import {
|
|
6
|
+
QUERY_TYPES,
|
|
7
|
+
type FetchChatQueryPayload,
|
|
8
|
+
type FetchChatQueryResult,
|
|
9
|
+
} from "../../core/query-handlers.ts";
|
|
10
|
+
import {
|
|
11
|
+
formatChatAsMarkdown,
|
|
12
|
+
formatChatAsJson,
|
|
13
|
+
} from "../../infrastructure/formatters.ts";
|
|
14
|
+
import { validateConversationId } from "../../infrastructure/validators.ts";
|
|
15
|
+
import { writeTextFile } from "../../infrastructure/io.ts";
|
|
16
|
+
|
|
17
|
+
interface ExportCommandOptions {
|
|
18
|
+
help: boolean;
|
|
19
|
+
output: string;
|
|
20
|
+
format: "markdown" | "json";
|
|
21
|
+
includeMetadata: boolean;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const DEFAULT_OPTIONS: ExportCommandOptions = {
|
|
25
|
+
help: false,
|
|
26
|
+
output: "",
|
|
27
|
+
format: "markdown",
|
|
28
|
+
includeMetadata: false,
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export class ExportCommand implements CliCommand {
|
|
32
|
+
readonly name = "export";
|
|
33
|
+
readonly description = "Export a conversation to a file";
|
|
34
|
+
|
|
35
|
+
async execute(args: string[], context: CliCommandContext): Promise<void> {
|
|
36
|
+
const logger = new Logger("export-command");
|
|
37
|
+
const options = this.parseArgs(args);
|
|
38
|
+
|
|
39
|
+
if (options.help) {
|
|
40
|
+
this.showUsage();
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const conversationId = this.extractConversationId(args);
|
|
45
|
+
|
|
46
|
+
if (!conversationId) {
|
|
47
|
+
console.error(chalk.red("Error: conversation ID is required."));
|
|
48
|
+
this.showUsage();
|
|
49
|
+
process.exit(1);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
try {
|
|
53
|
+
validateConversationId(conversationId);
|
|
54
|
+
} catch (error) {
|
|
55
|
+
console.error(chalk.red(`Error: ${(error as Error).message}`));
|
|
56
|
+
process.exit(1);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const mediator: Mediator = context.mediator;
|
|
60
|
+
const query: FetchChatQueryPayload = { conversationId };
|
|
61
|
+
|
|
62
|
+
logger.debug(`Sending fetch-chat query for export: ${JSON.stringify(query)}`);
|
|
63
|
+
|
|
64
|
+
try {
|
|
65
|
+
const result = await mediator.send<FetchChatQueryResult>({
|
|
66
|
+
type: QUERY_TYPES.FETCH_CHAT,
|
|
67
|
+
payload: query,
|
|
68
|
+
} as Query<FetchChatQueryPayload>);
|
|
69
|
+
|
|
70
|
+
const outputPath = options.output || this.defaultFilename(conversationId, options.format);
|
|
71
|
+
|
|
72
|
+
const content =
|
|
73
|
+
options.format === "json"
|
|
74
|
+
? formatChatAsJson(result.messages, conversationId)
|
|
75
|
+
: formatChatAsMarkdown(result.messages, conversationId, conversationId, options.includeMetadata);
|
|
76
|
+
|
|
77
|
+
writeTextFile(outputPath, content);
|
|
78
|
+
console.log(chalk.green(`Exported conversation '${chalk.cyan(conversationId)}' to: ${outputPath}`));
|
|
79
|
+
logger.info(`Exported conversation ${conversationId} to ${outputPath}`);
|
|
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 extractConversationId(args: string[]): string | null {
|
|
88
|
+
for (const arg of args) {
|
|
89
|
+
if (arg.startsWith("--") || arg.startsWith("-")) continue;
|
|
90
|
+
return arg;
|
|
91
|
+
}
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
private defaultFilename(conversationId: string, format: "markdown" | "json"): string {
|
|
96
|
+
const date = new Date().toISOString().slice(0, 10);
|
|
97
|
+
const ext = format === "json" ? "json" : "md";
|
|
98
|
+
return `gemini-chat-${conversationId}-${date}.${ext}`;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
private parseArgs(args: string[]): ExportCommandOptions {
|
|
102
|
+
const options = { ...DEFAULT_OPTIONS };
|
|
103
|
+
|
|
104
|
+
for (let i = 0; i < args.length; i++) {
|
|
105
|
+
const arg = args[i];
|
|
106
|
+
switch (arg) {
|
|
107
|
+
case "--help":
|
|
108
|
+
case "-h":
|
|
109
|
+
options.help = true;
|
|
110
|
+
break;
|
|
111
|
+
case "--output":
|
|
112
|
+
case "-o":
|
|
113
|
+
options.output = args[++i] ?? "";
|
|
114
|
+
break;
|
|
115
|
+
case "--format":
|
|
116
|
+
case "-f":
|
|
117
|
+
options.format = this.parseFormat(args[++i]);
|
|
118
|
+
break;
|
|
119
|
+
case "--include-metadata":
|
|
120
|
+
options.includeMetadata = true;
|
|
121
|
+
break;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return options;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
private parseFormat(value: string | undefined): "markdown" | "json" {
|
|
129
|
+
if (value === "markdown" || value === "json") return value;
|
|
130
|
+
return DEFAULT_OPTIONS.format;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
private showUsage(): void {
|
|
134
|
+
console.log(chalk.bold("Usage: gemiterm export <conversation_id> [options]"));
|
|
135
|
+
console.log("");
|
|
136
|
+
console.log(chalk.bold("Arguments:"));
|
|
137
|
+
console.log(
|
|
138
|
+
` ${chalk.cyan("conversation_id".padEnd(20))}${chalk.dim("ID of the conversation to export")}`,
|
|
139
|
+
);
|
|
140
|
+
console.log("");
|
|
141
|
+
console.log(chalk.bold("Options:"));
|
|
142
|
+
|
|
143
|
+
const flags = [
|
|
144
|
+
{ flag: "--output, -o <path>", desc: "Output file path (default: gemini-chat-<id>-<date>.md)" },
|
|
145
|
+
{ flag: "--format, -f <fmt>", desc: "Output format: markdown, json (default: markdown)" },
|
|
146
|
+
{ flag: "--include-metadata", desc: "Include metadata header (ID, count, date)" },
|
|
147
|
+
{ flag: "--help, -h", desc: "Show this help message" },
|
|
148
|
+
];
|
|
149
|
+
|
|
150
|
+
const maxLen = Math.max(...flags.map((f) => f.flag.length));
|
|
151
|
+
for (const f of flags) {
|
|
152
|
+
const padded = f.flag.padEnd(maxLen + 2);
|
|
153
|
+
console.log(` ${chalk.cyan(padded)}${chalk.dim(f.desc)}`);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import type { CliCommand, CliCommandContext } from "../command-registry.ts";
|
|
3
|
+
import type { Mediator, Query } from "../../core/mediator.ts";
|
|
4
|
+
import { Logger } from "../../infrastructure/logger.ts";
|
|
5
|
+
import {
|
|
6
|
+
QUERY_TYPES,
|
|
7
|
+
type FetchChatQueryPayload,
|
|
8
|
+
type FetchChatQueryResult,
|
|
9
|
+
} from "../../core/query-handlers.ts";
|
|
10
|
+
import type { Message } from "../../core/types.ts";
|
|
11
|
+
import { writeTextFile } from "../../infrastructure/io.ts";
|
|
12
|
+
|
|
13
|
+
interface FetchCommandOptions {
|
|
14
|
+
help: boolean;
|
|
15
|
+
format: "text" | "json";
|
|
16
|
+
path: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const DEFAULT_OPTIONS: FetchCommandOptions = {
|
|
20
|
+
help: false,
|
|
21
|
+
format: "text",
|
|
22
|
+
path: "",
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export class FetchCommand implements CliCommand {
|
|
26
|
+
readonly name = "fetch";
|
|
27
|
+
readonly description = "Fetch and display a conversation";
|
|
28
|
+
|
|
29
|
+
async execute(args: string[], context: CliCommandContext): Promise<void> {
|
|
30
|
+
const logger = new Logger("fetch-command");
|
|
31
|
+
const options = this.parseArgs(args);
|
|
32
|
+
|
|
33
|
+
if (options.help) {
|
|
34
|
+
this.showUsage();
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
let conversationId = this.extractConversationId(args, options);
|
|
39
|
+
|
|
40
|
+
if (!conversationId) {
|
|
41
|
+
await this.invokeListCommand(context);
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const mediator: Mediator = context.mediator;
|
|
46
|
+
const query: FetchChatQueryPayload = { conversationId };
|
|
47
|
+
|
|
48
|
+
logger.debug(`Sending fetch-chat query: ${JSON.stringify(query)}`);
|
|
49
|
+
const result = await mediator.send<FetchChatQueryResult>({
|
|
50
|
+
type: QUERY_TYPES.FETCH_CHAT,
|
|
51
|
+
payload: query,
|
|
52
|
+
} as Query<FetchChatQueryPayload>);
|
|
53
|
+
|
|
54
|
+
if (options.format === "json") {
|
|
55
|
+
this.outputJson(result.messages, conversationId, options.path);
|
|
56
|
+
} else {
|
|
57
|
+
this.outputText(result.messages, conversationId, options.path);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
private extractConversationId(args: string[], options: FetchCommandOptions): string | null {
|
|
62
|
+
for (const arg of args) {
|
|
63
|
+
if (arg.startsWith("--") || arg.startsWith("-")) continue;
|
|
64
|
+
return arg;
|
|
65
|
+
}
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
private async invokeListCommand(context: CliCommandContext): Promise<void> {
|
|
70
|
+
const { CommandRegistry } = await import("../command-registry.ts");
|
|
71
|
+
const registry = new CommandRegistry();
|
|
72
|
+
registry.registerAllCommands();
|
|
73
|
+
|
|
74
|
+
const listHandler = registry.getHandler("list");
|
|
75
|
+
if (listHandler) {
|
|
76
|
+
console.log(chalk.dim("No conversation ID specified. Listing conversations:\n"));
|
|
77
|
+
await listHandler.execute([], context);
|
|
78
|
+
} else {
|
|
79
|
+
console.error("Could not invoke list command.");
|
|
80
|
+
process.exit(1);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
private outputJson(messages: Message[], conversationId: string, path: string): void {
|
|
85
|
+
const output = JSON.stringify({ conversationId, messages }, null, 2);
|
|
86
|
+
if (path) {
|
|
87
|
+
this.writeOutput(path, output);
|
|
88
|
+
} else {
|
|
89
|
+
console.log(output);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
private outputText(messages: Message[], conversationId: string, path: string): void {
|
|
94
|
+
const lines: string[] = [];
|
|
95
|
+
|
|
96
|
+
lines.push(chalk.bold(`Conversation: ${chalk.cyan(conversationId)}`));
|
|
97
|
+
lines.push("");
|
|
98
|
+
|
|
99
|
+
if (messages.length === 0) {
|
|
100
|
+
lines.push(chalk.dim("No messages found."));
|
|
101
|
+
} else {
|
|
102
|
+
for (const msg of messages) {
|
|
103
|
+
const label =
|
|
104
|
+
msg.role === "user" ? chalk.green.bold("User:") : chalk.blue.bold("Model:");
|
|
105
|
+
lines.push(label);
|
|
106
|
+
lines.push(msg.content);
|
|
107
|
+
lines.push("");
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const output = lines.join("\n");
|
|
112
|
+
if (path) {
|
|
113
|
+
this.writeOutput(path, output);
|
|
114
|
+
} else {
|
|
115
|
+
console.log(output);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
private writeOutput(path: string, content: string): void {
|
|
120
|
+
writeTextFile(path, content);
|
|
121
|
+
console.log(chalk.dim(`Output written to: ${path}`));
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
private parseArgs(args: string[]): FetchCommandOptions {
|
|
125
|
+
const options = { ...DEFAULT_OPTIONS };
|
|
126
|
+
|
|
127
|
+
for (let i = 0; i < args.length; i++) {
|
|
128
|
+
const arg = args[i];
|
|
129
|
+
switch (arg) {
|
|
130
|
+
case "--help":
|
|
131
|
+
case "-h":
|
|
132
|
+
options.help = true;
|
|
133
|
+
break;
|
|
134
|
+
case "--format":
|
|
135
|
+
case "-f":
|
|
136
|
+
options.format = this.parseFormat(args[++i]);
|
|
137
|
+
break;
|
|
138
|
+
case "--path":
|
|
139
|
+
case "-p":
|
|
140
|
+
options.path = args[++i] ?? "";
|
|
141
|
+
break;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return options;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
private parseFormat(value: string | undefined): "text" | "json" {
|
|
149
|
+
if (value === "text" || value === "json") return value;
|
|
150
|
+
return DEFAULT_OPTIONS.format;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
private showUsage(): void {
|
|
154
|
+
console.log(chalk.bold("Usage: gemiterm fetch [conversation_id] [options]"));
|
|
155
|
+
console.log("");
|
|
156
|
+
console.log(chalk.bold("Arguments:"));
|
|
157
|
+
console.log(` ${chalk.cyan("conversation_id".padEnd(20))}${chalk.dim("ID of the conversation to fetch (optional)")}`);
|
|
158
|
+
console.log("");
|
|
159
|
+
console.log(chalk.bold("Options:"));
|
|
160
|
+
|
|
161
|
+
const flags = [
|
|
162
|
+
{ flag: "--format, -f <fmt>", desc: "Output format: text, json (default: text)" },
|
|
163
|
+
{ flag: "--path, -p <path>", desc: "Write output to file" },
|
|
164
|
+
{ flag: "--help, -h", desc: "Show this help message" },
|
|
165
|
+
];
|
|
166
|
+
|
|
167
|
+
const maxLen = Math.max(...flags.map((f) => f.flag.length));
|
|
168
|
+
for (const f of flags) {
|
|
169
|
+
const padded = f.flag.padEnd(maxLen + 2);
|
|
170
|
+
console.log(` ${chalk.cyan(padded)}${chalk.dim(f.desc)}`);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
console.log("");
|
|
174
|
+
console.log(chalk.dim("If no conversation_id is provided, the list command will be invoked."));
|
|
175
|
+
}
|
|
176
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import type { CommandRegistry } from "../command-registry.ts";
|
|
3
|
+
|
|
4
|
+
const GLOBAL_OPTIONS = [
|
|
5
|
+
{ flag: "--version", description: "Show version number" },
|
|
6
|
+
{ flag: "--help, -h", description: "Show this help message" },
|
|
7
|
+
{ flag: "--verbose, -v", description: "Enable verbose output (also: GEMITERM_VERBOSE=true)" },
|
|
8
|
+
];
|
|
9
|
+
|
|
10
|
+
export function showHelp(registry?: CommandRegistry): void {
|
|
11
|
+
const lines: string[] = [];
|
|
12
|
+
|
|
13
|
+
lines.push(chalk.bold.white("GemiTerm") + chalk.dim(" - Google Gemini Terminal Client"));
|
|
14
|
+
lines.push("");
|
|
15
|
+
lines.push(chalk.bold("Usage:"));
|
|
16
|
+
lines.push(" gemiterm [command] [options]");
|
|
17
|
+
lines.push("");
|
|
18
|
+
lines.push(chalk.bold("Commands:"));
|
|
19
|
+
|
|
20
|
+
if (registry) {
|
|
21
|
+
const names = registry.getRegisteredNames();
|
|
22
|
+
const maxCmdLen = Math.max(...names.map((n) => n.length));
|
|
23
|
+
for (const name of names) {
|
|
24
|
+
const handler = registry.getHandler(name);
|
|
25
|
+
if (handler) {
|
|
26
|
+
const padded = name.padEnd(maxCmdLen + 2);
|
|
27
|
+
lines.push(` ${chalk.green(padded)}${chalk.dim(handler.description)}`);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
lines.push("");
|
|
33
|
+
lines.push(chalk.bold("Global Options:"));
|
|
34
|
+
|
|
35
|
+
const maxOptLen = Math.max(...GLOBAL_OPTIONS.map((o) => o.flag.length));
|
|
36
|
+
for (const opt of GLOBAL_OPTIONS) {
|
|
37
|
+
const padded = opt.flag.padEnd(maxOptLen + 2);
|
|
38
|
+
lines.push(` ${chalk.cyan(padded)}${chalk.dim(opt.description)}`);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
lines.push("");
|
|
42
|
+
lines.push(chalk.dim("Run 'gemiterm <command> --help' for more information about a command."));
|
|
43
|
+
|
|
44
|
+
console.log(lines.join("\n"));
|
|
45
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import type { CliCommand, CliCommandContext } from "../command-registry.ts";
|
|
3
|
+
import { Logger } from "../../infrastructure/logger.ts";
|
|
4
|
+
import { InstallBrowserService, InstallBrowserError } from "../../services/install-browser-service.ts";
|
|
5
|
+
|
|
6
|
+
export class InstallBrowserCommand implements CliCommand {
|
|
7
|
+
readonly name = "install-browser";
|
|
8
|
+
readonly description = "Install Chromium browser for Playwright (hidden command)";
|
|
9
|
+
|
|
10
|
+
async execute(_args: string[], _context: CliCommandContext): Promise<void> {
|
|
11
|
+
const logger = new Logger("install-browser-command");
|
|
12
|
+
logger.debug("Executing install-browser command");
|
|
13
|
+
const service = new InstallBrowserService(logger);
|
|
14
|
+
|
|
15
|
+
console.log(chalk.dim("Checking browser installation..."));
|
|
16
|
+
|
|
17
|
+
try {
|
|
18
|
+
await service.install();
|
|
19
|
+
console.log(chalk.green("Browser ready."));
|
|
20
|
+
} catch (error) {
|
|
21
|
+
if (error instanceof InstallBrowserError) {
|
|
22
|
+
logger.error(error.message);
|
|
23
|
+
if (error.cause) {
|
|
24
|
+
logger.error(`Cause: ${error.cause.message}`);
|
|
25
|
+
}
|
|
26
|
+
console.error(chalk.red("Failed to install browser."));
|
|
27
|
+
console.error(chalk.dim("You may need to run: bunx @playwright/cli install chromium"));
|
|
28
|
+
process.exit(1);
|
|
29
|
+
}
|
|
30
|
+
throw error;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|