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
package/src/cli/index.ts
ADDED
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
import { CommandRegistry } from "./command-registry.ts";
|
|
4
|
+
import { Logger } from "../infrastructure/logger.ts";
|
|
5
|
+
import { Mediator } from "../core/mediator.ts";
|
|
6
|
+
import { showHelp } from "./commands/help.ts";
|
|
7
|
+
import { GeminiClientService } from "../services/gemini-client-wrapper.ts";
|
|
8
|
+
import { CookieStorageService } from "../services/cookie-storage-service.ts";
|
|
9
|
+
import { ProfileAuthManager } from "../services/profile-auth-manager.ts";
|
|
10
|
+
import { CookieStorage, ProfileManager } from "../infrastructure/storage.ts";
|
|
11
|
+
import { getDefaultProfileName, listProfiles, ensureConfigDir } from "../infrastructure/config.ts";
|
|
12
|
+
import { getPackageJson } from "../infrastructure/path-utils.ts";
|
|
13
|
+
import { AuthenticationError } from "../core/errors.ts";
|
|
14
|
+
import {
|
|
15
|
+
AuthenticateCommandHandler,
|
|
16
|
+
DeleteProfileCommandHandler,
|
|
17
|
+
RenameProfileCommandHandler,
|
|
18
|
+
SetDefaultProfileCommandHandler,
|
|
19
|
+
DeleteConversationCommandHandler,
|
|
20
|
+
SendMessageCommandHandler,
|
|
21
|
+
StartNewChatCommandHandler,
|
|
22
|
+
} from "../core/command-handlers.ts";
|
|
23
|
+
import {
|
|
24
|
+
ListChatsQueryHandler,
|
|
25
|
+
FetchChatQueryHandler,
|
|
26
|
+
GetProfileStatusesQueryHandler,
|
|
27
|
+
GetAuthStatusQueryHandler,
|
|
28
|
+
ListModelsQueryHandler,
|
|
29
|
+
} from "../core/query-handlers.ts";
|
|
30
|
+
|
|
31
|
+
const pkg = getPackageJson(import.meta.url);
|
|
32
|
+
|
|
33
|
+
interface GlobalFlags {
|
|
34
|
+
verbose: boolean;
|
|
35
|
+
version: boolean;
|
|
36
|
+
help: boolean;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function parseGlobalFlags(args: string[]): { flags: GlobalFlags; remaining: string[] } {
|
|
40
|
+
const flags: GlobalFlags = {
|
|
41
|
+
verbose: process.env.GEMITERM_VERBOSE === "true",
|
|
42
|
+
version: false,
|
|
43
|
+
help: false,
|
|
44
|
+
};
|
|
45
|
+
const remaining: string[] = [];
|
|
46
|
+
|
|
47
|
+
for (const arg of args) {
|
|
48
|
+
if (arg === "--verbose" || arg === "-v") {
|
|
49
|
+
flags.verbose = true;
|
|
50
|
+
} else if (arg === "--version") {
|
|
51
|
+
flags.version = true;
|
|
52
|
+
} else if (arg === "--help" || arg === "-h") {
|
|
53
|
+
if (remaining.length === 0) {
|
|
54
|
+
flags.help = true;
|
|
55
|
+
} else {
|
|
56
|
+
remaining.push(arg);
|
|
57
|
+
}
|
|
58
|
+
} else {
|
|
59
|
+
remaining.push(arg);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return { flags, remaining };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async function setupMediator(mediator: Mediator): Promise<ProfileAuthManager> {
|
|
67
|
+
const logger = new Logger("mediator");
|
|
68
|
+
const cookieStorage = new CookieStorage();
|
|
69
|
+
const profileManager = new ProfileManager(cookieStorage);
|
|
70
|
+
|
|
71
|
+
const profileQueryService = {
|
|
72
|
+
async getProfileStatuses() {
|
|
73
|
+
return profileManager.getAllStatuses();
|
|
74
|
+
},
|
|
75
|
+
async getAuthStatus() {
|
|
76
|
+
const profiles = listProfiles();
|
|
77
|
+
if (profiles.length === 0) {
|
|
78
|
+
return { authenticated: false, profileName: null };
|
|
79
|
+
}
|
|
80
|
+
const defaultName = getDefaultProfileName();
|
|
81
|
+
const isValid = profileManager.hasValidCookies(defaultName);
|
|
82
|
+
return { authenticated: isValid, profileName: defaultName };
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const cookieStorageService = new CookieStorageService({ cookieStorage, logger });
|
|
87
|
+
let geminiClient: GeminiClientService | null = null;
|
|
88
|
+
|
|
89
|
+
function getGeminiClient(): GeminiClientService {
|
|
90
|
+
if (geminiClient) return geminiClient;
|
|
91
|
+
const profiles = listProfiles();
|
|
92
|
+
if (profiles.length === 0) {
|
|
93
|
+
throw new AuthenticationError();
|
|
94
|
+
}
|
|
95
|
+
const profileName = getDefaultProfileName();
|
|
96
|
+
try {
|
|
97
|
+
const cookieData = profileManager.loadCookiesForApi(profileName);
|
|
98
|
+
geminiClient = new GeminiClientService(
|
|
99
|
+
{ secure1psid: cookieData.secure1psid, secure1psidts: cookieData.secure1psidts },
|
|
100
|
+
logger,
|
|
101
|
+
cookieStorageService,
|
|
102
|
+
profileName,
|
|
103
|
+
);
|
|
104
|
+
return geminiClient;
|
|
105
|
+
} catch {
|
|
106
|
+
throw new AuthenticationError();
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const factoryClient = new GeminiClientService({ secure1psid: "" }, logger, cookieStorageService);
|
|
111
|
+
try { await factoryClient.init(); } catch { /* factory: init deferred until first real profile call */ }
|
|
112
|
+
const profileAuthManager = new ProfileAuthManager({ profileManager, cookieStorageService, logger, geminiClient: factoryClient });
|
|
113
|
+
|
|
114
|
+
mediator.registerQueryHandler(new GetAuthStatusQueryHandler(profileQueryService));
|
|
115
|
+
mediator.registerQueryHandler(new GetProfileStatusesQueryHandler(profileQueryService));
|
|
116
|
+
mediator.registerQueryHandler(new ListChatsQueryHandler(getGeminiClient, listProfiles));
|
|
117
|
+
mediator.registerQueryHandler(new FetchChatQueryHandler({
|
|
118
|
+
async listChats(options) { return getGeminiClient().listChats(options); },
|
|
119
|
+
async fetchChat(id) { return getGeminiClient().fetchChat(id); },
|
|
120
|
+
async listModels() { return getGeminiClient().listModels(); },
|
|
121
|
+
}));
|
|
122
|
+
mediator.registerQueryHandler(new ListModelsQueryHandler({
|
|
123
|
+
async listChats(options) { return getGeminiClient().listChats(options); },
|
|
124
|
+
async fetchChat(id) { return getGeminiClient().fetchChat(id); },
|
|
125
|
+
async listModels() { return getGeminiClient().listModels(); },
|
|
126
|
+
}));
|
|
127
|
+
mediator.registerCommandHandler(new AuthenticateCommandHandler(null as any));
|
|
128
|
+
mediator.registerCommandHandler(new DeleteProfileCommandHandler(null as any));
|
|
129
|
+
mediator.registerCommandHandler(new RenameProfileCommandHandler(null as any));
|
|
130
|
+
mediator.registerCommandHandler(new SetDefaultProfileCommandHandler(null as any));
|
|
131
|
+
mediator.registerCommandHandler(new DeleteConversationCommandHandler({
|
|
132
|
+
async deleteChat(id) { return getGeminiClient().deleteChat(id); },
|
|
133
|
+
async sendMessage(id, msg) { return getGeminiClient().sendMessage(id, msg); },
|
|
134
|
+
async startNewChat(msg) { return getGeminiClient().startNewChat(msg); },
|
|
135
|
+
async profileHasConversation(name, id) { return getGeminiClient().profileHasConversation(name, id); },
|
|
136
|
+
forProfile(name) { return getGeminiClient().forProfile(name); },
|
|
137
|
+
async listChats(options) { return getGeminiClient().listChats(options); },
|
|
138
|
+
}));
|
|
139
|
+
mediator.registerCommandHandler(new SendMessageCommandHandler({
|
|
140
|
+
async deleteChat(id) { return getGeminiClient().deleteChat(id); },
|
|
141
|
+
async sendMessage(id, msg) { return getGeminiClient().sendMessage(id, msg); },
|
|
142
|
+
async startNewChat(msg) { return getGeminiClient().startNewChat(msg); },
|
|
143
|
+
async profileHasConversation(name, id) { return getGeminiClient().profileHasConversation(name, id); },
|
|
144
|
+
forProfile(name) { return getGeminiClient().forProfile(name); },
|
|
145
|
+
async listChats(options) { return getGeminiClient().listChats(options); },
|
|
146
|
+
}));
|
|
147
|
+
mediator.registerCommandHandler(new StartNewChatCommandHandler({
|
|
148
|
+
async deleteChat(id) { return getGeminiClient().deleteChat(id); },
|
|
149
|
+
async sendMessage(id, msg) { return getGeminiClient().sendMessage(id, msg); },
|
|
150
|
+
async startNewChat(msg) { return getGeminiClient().startNewChat(msg); },
|
|
151
|
+
async profileHasConversation(name, id) { return getGeminiClient().profileHasConversation(name, id); },
|
|
152
|
+
forProfile(name) { return getGeminiClient().forProfile(name); },
|
|
153
|
+
async listChats(options) { return getGeminiClient().listChats(options); },
|
|
154
|
+
}));
|
|
155
|
+
|
|
156
|
+
return profileAuthManager;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
async function main(): Promise<void> {
|
|
160
|
+
const args = process.argv.slice(2);
|
|
161
|
+
const logger = new Logger("cli");
|
|
162
|
+
const { flags, remaining } = parseGlobalFlags(args);
|
|
163
|
+
|
|
164
|
+
if (flags.verbose) {
|
|
165
|
+
Logger.setVerbose(true);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (flags.version) {
|
|
169
|
+
console.log(`gemiterm v${pkg.version}`);
|
|
170
|
+
process.exit(0);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const registry = new CommandRegistry();
|
|
174
|
+
registry.registerAllCommands();
|
|
175
|
+
|
|
176
|
+
if (remaining.length === 0) {
|
|
177
|
+
showHelp(registry);
|
|
178
|
+
process.exit(0);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const mediator = new Mediator();
|
|
182
|
+
const profileAuthManager = await setupMediator(mediator);
|
|
183
|
+
|
|
184
|
+
const subcommand = remaining[0];
|
|
185
|
+
const subcommandArgs = remaining.slice(1);
|
|
186
|
+
|
|
187
|
+
const handler = registry.getHandler(subcommand);
|
|
188
|
+
if (!handler) {
|
|
189
|
+
const known = registry.getRegisteredNames();
|
|
190
|
+
if (known.length > 0) {
|
|
191
|
+
console.error(`Unknown command: '${subcommand}'`);
|
|
192
|
+
console.error(`Did you mean one of: ${known.join(", ")}?`);
|
|
193
|
+
} else {
|
|
194
|
+
console.error(`Unknown command: '${subcommand}'`);
|
|
195
|
+
console.error("Run 'gemiterm --help' for available commands.");
|
|
196
|
+
}
|
|
197
|
+
process.exit(1);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
try {
|
|
201
|
+
await handler.execute(subcommandArgs, { verbose: flags.verbose, mediator, profileAuthManager });
|
|
202
|
+
} catch (error) {
|
|
203
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
204
|
+
logger.error(`Command '${subcommand}' failed: ${message}`);
|
|
205
|
+
process.exit(1);
|
|
206
|
+
}
|
|
207
|
+
process.exit(0);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
main().catch((error) => {
|
|
211
|
+
console.error(`Fatal error: ${error}`);
|
|
212
|
+
process.exit(1);
|
|
213
|
+
});
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import { createInterface } from "node:readline";
|
|
3
|
+
|
|
4
|
+
export interface MessageHandlerResult {
|
|
5
|
+
response: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export type MessageHandler = (message: string) => Promise<MessageHandlerResult>;
|
|
9
|
+
|
|
10
|
+
export interface InteractiveLoopOptions {
|
|
11
|
+
profileName?: string | null;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export async function runInteractiveLoop(
|
|
15
|
+
messageHandler: MessageHandler,
|
|
16
|
+
options: InteractiveLoopOptions = {},
|
|
17
|
+
): Promise<void> {
|
|
18
|
+
const profileLabel = options.profileName ?? "default";
|
|
19
|
+
console.log(chalk.dim(`Starting chat session (profile: ${chalk.cyan(profileLabel)})`));
|
|
20
|
+
console.log(chalk.dim("Type your message and press Enter. Type /exit to quit.\n"));
|
|
21
|
+
|
|
22
|
+
const rl = createInterface({
|
|
23
|
+
input: process.stdin,
|
|
24
|
+
output: process.stdout,
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
const prompt = (): void => {
|
|
28
|
+
rl.question(chalk.green.bold("You: "), async (input) => {
|
|
29
|
+
const trimmed = input.trim();
|
|
30
|
+
|
|
31
|
+
if (trimmed === "/exit" || trimmed === "/quit") {
|
|
32
|
+
rl.close();
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (!trimmed) {
|
|
37
|
+
prompt();
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
try {
|
|
42
|
+
const result = await messageHandler(trimmed);
|
|
43
|
+
console.log(chalk.blue.bold("Model:"));
|
|
44
|
+
console.log(result.response);
|
|
45
|
+
console.log("");
|
|
46
|
+
} catch (error) {
|
|
47
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
48
|
+
console.error(chalk.red(`Error: ${message}`));
|
|
49
|
+
console.log("");
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
prompt();
|
|
53
|
+
});
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
prompt();
|
|
57
|
+
|
|
58
|
+
await new Promise<void>((resolve) => {
|
|
59
|
+
rl.on("close", () => {
|
|
60
|
+
console.log(chalk.dim("\nGoodbye."));
|
|
61
|
+
resolve();
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
}
|
|
File without changes
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import type { Command, CommandHandler } from "./mediator.ts";
|
|
2
|
+
import type { AuthResult, ChatInfo } from "./types.ts";
|
|
3
|
+
import { AuthenticationError } from "./errors.ts";
|
|
4
|
+
|
|
5
|
+
function extractPayload<T>(command: Command<T>): T {
|
|
6
|
+
return command.payload;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface AuthenticateCommandPayload {
|
|
10
|
+
profileName?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface AuthenticateCommandResult {
|
|
14
|
+
success: boolean;
|
|
15
|
+
cookieCount: number;
|
|
16
|
+
expiresAt: string | null;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface DeleteProfileCommandPayload {
|
|
20
|
+
profileName: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface DeleteProfileCommandResult {
|
|
24
|
+
success: boolean;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface RenameProfileCommandPayload {
|
|
28
|
+
oldName: string;
|
|
29
|
+
newName: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface RenameProfileCommandResult {
|
|
33
|
+
success: boolean;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface SetDefaultProfileCommandPayload {
|
|
37
|
+
profileName: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface SetDefaultProfileCommandResult {
|
|
41
|
+
success: boolean;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface DeleteConversationCommandPayload {
|
|
45
|
+
conversationId: string;
|
|
46
|
+
profileName?: string;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface DeleteConversationCommandResult {
|
|
50
|
+
success: boolean;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface SendMessageCommandPayload {
|
|
54
|
+
conversationId: string;
|
|
55
|
+
message: string;
|
|
56
|
+
profileName?: string;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface SendMessageCommandResult {
|
|
60
|
+
response: string;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface StartNewChatCommandPayload {
|
|
64
|
+
message: string;
|
|
65
|
+
profileName?: string;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export interface StartNewChatCommandResult {
|
|
69
|
+
response: string;
|
|
70
|
+
conversationId: string;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export const COMMAND_TYPES = {
|
|
74
|
+
AUTHENTICATE: "authenticate",
|
|
75
|
+
DELETE_PROFILE: "delete-profile",
|
|
76
|
+
RENAME_PROFILE: "rename-profile",
|
|
77
|
+
SET_DEFAULT_PROFILE: "set-default-profile",
|
|
78
|
+
DELETE_CONVERSATION: "delete-conversation",
|
|
79
|
+
SEND_MESSAGE: "send-message",
|
|
80
|
+
START_NEW_CHAT: "start-new-chat",
|
|
81
|
+
} as const;
|
|
82
|
+
|
|
83
|
+
export type CommandType = (typeof COMMAND_TYPES)[keyof typeof COMMAND_TYPES];
|
|
84
|
+
|
|
85
|
+
export interface IProfileService {
|
|
86
|
+
authenticate(profileName?: string): Promise<AuthResult>;
|
|
87
|
+
deleteProfile(name: string): Promise<void>;
|
|
88
|
+
renameProfile(oldName: string, newName: string): Promise<void>;
|
|
89
|
+
setDefaultProfile(name: string): Promise<void>;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export interface IGeminiClientService {
|
|
93
|
+
deleteChat(conversationId: string): Promise<void>;
|
|
94
|
+
sendMessage(conversationId: string, message: string): Promise<string>;
|
|
95
|
+
startNewChat(message: string): Promise<{ response: string; conversationId: string }>;
|
|
96
|
+
profileHasConversation(profileName: string, conversationId: string): Promise<boolean>;
|
|
97
|
+
forProfile(profileName: string): IGeminiClientService;
|
|
98
|
+
listChats(options?: { limit?: number; offset?: number; search?: string }): Promise<ChatInfo[]>;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export class AuthenticateCommandHandler
|
|
102
|
+
implements CommandHandler<AuthenticateCommandPayload, AuthenticateCommandResult>
|
|
103
|
+
{
|
|
104
|
+
readonly commandType = COMMAND_TYPES.AUTHENTICATE;
|
|
105
|
+
private readonly profileService: IProfileService;
|
|
106
|
+
|
|
107
|
+
constructor(profileService: IProfileService) {
|
|
108
|
+
this.profileService = profileService;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async handle(command: Command<AuthenticateCommandPayload>): Promise<AuthenticateCommandResult> {
|
|
112
|
+
const { profileName } = extractPayload(command);
|
|
113
|
+
try {
|
|
114
|
+
const result = await this.profileService.authenticate(profileName);
|
|
115
|
+
return {
|
|
116
|
+
success: true,
|
|
117
|
+
cookieCount: result.cookies.length,
|
|
118
|
+
expiresAt: result.expiresAt?.toISOString() ?? null,
|
|
119
|
+
};
|
|
120
|
+
} catch (error) {
|
|
121
|
+
if (error instanceof AuthenticationError) {
|
|
122
|
+
return { success: false, cookieCount: 0, expiresAt: null };
|
|
123
|
+
}
|
|
124
|
+
throw error;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export class DeleteProfileCommandHandler
|
|
130
|
+
implements CommandHandler<DeleteProfileCommandPayload, DeleteProfileCommandResult>
|
|
131
|
+
{
|
|
132
|
+
readonly commandType = COMMAND_TYPES.DELETE_PROFILE;
|
|
133
|
+
private readonly profileService: IProfileService;
|
|
134
|
+
|
|
135
|
+
constructor(profileService: IProfileService) {
|
|
136
|
+
this.profileService = profileService;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async handle(command: Command<DeleteProfileCommandPayload>): Promise<DeleteProfileCommandResult> {
|
|
140
|
+
const { profileName } = extractPayload(command);
|
|
141
|
+
await this.profileService.deleteProfile(profileName);
|
|
142
|
+
return { success: true };
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export class RenameProfileCommandHandler
|
|
147
|
+
implements CommandHandler<RenameProfileCommandPayload, RenameProfileCommandResult>
|
|
148
|
+
{
|
|
149
|
+
readonly commandType = COMMAND_TYPES.RENAME_PROFILE;
|
|
150
|
+
private readonly profileService: IProfileService;
|
|
151
|
+
|
|
152
|
+
constructor(profileService: IProfileService) {
|
|
153
|
+
this.profileService = profileService;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
async handle(command: Command<RenameProfileCommandPayload>): Promise<RenameProfileCommandResult> {
|
|
157
|
+
const { oldName, newName } = extractPayload(command);
|
|
158
|
+
await this.profileService.renameProfile(oldName, newName);
|
|
159
|
+
return { success: true };
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export class SetDefaultProfileCommandHandler
|
|
164
|
+
implements
|
|
165
|
+
CommandHandler<SetDefaultProfileCommandPayload, SetDefaultProfileCommandResult>
|
|
166
|
+
{
|
|
167
|
+
readonly commandType = COMMAND_TYPES.SET_DEFAULT_PROFILE;
|
|
168
|
+
private readonly profileService: IProfileService;
|
|
169
|
+
|
|
170
|
+
constructor(profileService: IProfileService) {
|
|
171
|
+
this.profileService = profileService;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
async handle(
|
|
175
|
+
command: Command<SetDefaultProfileCommandPayload>,
|
|
176
|
+
): Promise<SetDefaultProfileCommandResult> {
|
|
177
|
+
const { profileName } = extractPayload(command);
|
|
178
|
+
await this.profileService.setDefaultProfile(profileName);
|
|
179
|
+
return { success: true };
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export class DeleteConversationCommandHandler
|
|
184
|
+
implements
|
|
185
|
+
CommandHandler<DeleteConversationCommandPayload, DeleteConversationCommandResult>
|
|
186
|
+
{
|
|
187
|
+
readonly commandType = COMMAND_TYPES.DELETE_CONVERSATION;
|
|
188
|
+
private readonly geminiClient: IGeminiClientService;
|
|
189
|
+
|
|
190
|
+
constructor(geminiClient: IGeminiClientService) {
|
|
191
|
+
this.geminiClient = geminiClient;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
async handle(
|
|
195
|
+
command: Command<DeleteConversationCommandPayload>,
|
|
196
|
+
): Promise<DeleteConversationCommandResult> {
|
|
197
|
+
const { conversationId, profileName } = extractPayload(command);
|
|
198
|
+
const client = profileName ? this.geminiClient.forProfile(profileName) : this.geminiClient;
|
|
199
|
+
await client.deleteChat(conversationId);
|
|
200
|
+
return { success: true };
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export class SendMessageCommandHandler
|
|
205
|
+
implements CommandHandler<SendMessageCommandPayload, SendMessageCommandResult>
|
|
206
|
+
{
|
|
207
|
+
readonly commandType = COMMAND_TYPES.SEND_MESSAGE;
|
|
208
|
+
private readonly geminiClient: IGeminiClientService;
|
|
209
|
+
|
|
210
|
+
constructor(geminiClient: IGeminiClientService) {
|
|
211
|
+
this.geminiClient = geminiClient;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
async handle(command: Command<SendMessageCommandPayload>): Promise<SendMessageCommandResult> {
|
|
215
|
+
const { conversationId, message, profileName } = extractPayload(command);
|
|
216
|
+
const client = profileName ? this.geminiClient.forProfile(profileName) : this.geminiClient;
|
|
217
|
+
const response = await client.sendMessage(conversationId, message);
|
|
218
|
+
return { response };
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
export class StartNewChatCommandHandler
|
|
223
|
+
implements CommandHandler<StartNewChatCommandPayload, StartNewChatCommandResult>
|
|
224
|
+
{
|
|
225
|
+
readonly commandType = COMMAND_TYPES.START_NEW_CHAT;
|
|
226
|
+
private readonly geminiClient: IGeminiClientService;
|
|
227
|
+
|
|
228
|
+
constructor(geminiClient: IGeminiClientService) {
|
|
229
|
+
this.geminiClient = geminiClient;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
async handle(
|
|
233
|
+
command: Command<StartNewChatCommandPayload>,
|
|
234
|
+
): Promise<StartNewChatCommandResult> {
|
|
235
|
+
const { message, profileName } = extractPayload(command);
|
|
236
|
+
const client = profileName ? this.geminiClient.forProfile(profileName) : this.geminiClient;
|
|
237
|
+
const result = await client.startNewChat(message);
|
|
238
|
+
return { response: result.response, conversationId: result.conversationId };
|
|
239
|
+
}
|
|
240
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
export class GemitermError extends Error {
|
|
2
|
+
constructor(message: string) {
|
|
3
|
+
super(message);
|
|
4
|
+
this.name = "GemitermError";
|
|
5
|
+
}
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export class AuthenticationError extends GemitermError {
|
|
9
|
+
constructor(message = "Not authenticated. Please run 'gemiterm login' first.") {
|
|
10
|
+
super(message);
|
|
11
|
+
this.name = "AuthenticationError";
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export class CookieExpiredError extends GemitermError {
|
|
16
|
+
constructor(message = "Session has expired. Please run 'gemiterm login' again.") {
|
|
17
|
+
super(message);
|
|
18
|
+
this.name = "CookieExpiredError";
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export class GeminiAPIError extends GemitermError {
|
|
23
|
+
constructor(message: string) {
|
|
24
|
+
super(message);
|
|
25
|
+
this.name = "GeminiAPIError";
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export class ConversationNotFoundError extends GemitermError {
|
|
30
|
+
constructor(conversationId: string) {
|
|
31
|
+
super(`Conversation '${conversationId}' not found.`);
|
|
32
|
+
this.name = "ConversationNotFoundError";
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export class ConversationPendingError extends GemitermError {
|
|
37
|
+
constructor(message = "Conversation operation is still pending.") {
|
|
38
|
+
super(message);
|
|
39
|
+
this.name = "ConversationPendingError";
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
export interface Query<TPayload = unknown> {
|
|
2
|
+
readonly type: string;
|
|
3
|
+
readonly payload: TPayload;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export interface Command<TPayload = unknown> {
|
|
7
|
+
readonly type: string;
|
|
8
|
+
readonly payload: TPayload;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface QueryHandler<TPayload, TResult> {
|
|
12
|
+
readonly queryType: string;
|
|
13
|
+
handle(query: Query<TPayload>): Promise<TResult>;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface CommandHandler<TPayload, TResult> {
|
|
17
|
+
readonly commandType: string;
|
|
18
|
+
handle(command: Command<TPayload>): Promise<TResult>;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
type HandlerLike = {
|
|
22
|
+
type: string;
|
|
23
|
+
handle(message: Query | Command): Promise<unknown>;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export class Mediator {
|
|
27
|
+
private queryHandlers = new Map<string, HandlerLike>();
|
|
28
|
+
private commandHandlers = new Map<string, HandlerLike>();
|
|
29
|
+
|
|
30
|
+
registerQueryHandler<TPayload, TResult>(
|
|
31
|
+
handler: QueryHandler<TPayload, TResult>,
|
|
32
|
+
): void {
|
|
33
|
+
if (this.queryHandlers.has(handler.queryType)) {
|
|
34
|
+
throw new Error(`Query handler already registered for type: ${handler.queryType}`);
|
|
35
|
+
}
|
|
36
|
+
this.queryHandlers.set(handler.queryType, handler as unknown as HandlerLike);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
registerCommandHandler<TPayload, TResult>(
|
|
40
|
+
handler: CommandHandler<TPayload, TResult>,
|
|
41
|
+
): void {
|
|
42
|
+
if (this.commandHandlers.has(handler.commandType)) {
|
|
43
|
+
throw new Error(`Command handler already registered for type: ${handler.commandType}`);
|
|
44
|
+
}
|
|
45
|
+
this.commandHandlers.set(
|
|
46
|
+
handler.commandType,
|
|
47
|
+
handler as unknown as HandlerLike,
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async send<TResult>(message: Query | Command): Promise<TResult> {
|
|
52
|
+
const handler = this.queryHandlers.get(message.type) ?? this.commandHandlers.get(message.type);
|
|
53
|
+
if (!handler) {
|
|
54
|
+
throw new Error(`No handler registered for message type: ${message.type}`);
|
|
55
|
+
}
|
|
56
|
+
return handler.handle(message) as Promise<TResult>;
|
|
57
|
+
}
|
|
58
|
+
}
|