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,226 @@
|
|
|
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 ListChatsQueryPayload,
|
|
8
|
+
type ListChatsQueryResult,
|
|
9
|
+
} from "../../core/query-handlers.ts";
|
|
10
|
+
import { formatChatList } from "../../infrastructure/formatters.ts";
|
|
11
|
+
import type { ChatInfo } from "../../core/types.ts";
|
|
12
|
+
import { writeTextFile } from "../../infrastructure/io.ts";
|
|
13
|
+
|
|
14
|
+
interface ListCommandOptions {
|
|
15
|
+
help: boolean;
|
|
16
|
+
limit: number;
|
|
17
|
+
offset: number;
|
|
18
|
+
all: boolean;
|
|
19
|
+
allProfiles: boolean;
|
|
20
|
+
sort: "recent" | "oldest" | "alpha";
|
|
21
|
+
search: string;
|
|
22
|
+
after: string;
|
|
23
|
+
before: string;
|
|
24
|
+
format: "text" | "json";
|
|
25
|
+
path: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const DEFAULT_OPTIONS: ListCommandOptions = {
|
|
29
|
+
help: false,
|
|
30
|
+
limit: 10,
|
|
31
|
+
offset: 0,
|
|
32
|
+
all: false,
|
|
33
|
+
allProfiles: false,
|
|
34
|
+
sort: "recent",
|
|
35
|
+
search: "",
|
|
36
|
+
after: "",
|
|
37
|
+
before: "",
|
|
38
|
+
format: "text",
|
|
39
|
+
path: "",
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export class ListCommand implements CliCommand {
|
|
43
|
+
readonly name = "list";
|
|
44
|
+
readonly description = "List conversations";
|
|
45
|
+
|
|
46
|
+
async execute(args: string[], context: CliCommandContext): Promise<void> {
|
|
47
|
+
const logger = new Logger("list-command");
|
|
48
|
+
const options = this.parseArgs(args);
|
|
49
|
+
|
|
50
|
+
if (options.help) {
|
|
51
|
+
this.showUsage();
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const mediator: Mediator = context.mediator;
|
|
56
|
+
const query: ListChatsQueryPayload = {
|
|
57
|
+
limit: options.all ? undefined : options.limit,
|
|
58
|
+
offset: options.offset || undefined,
|
|
59
|
+
search: options.search || undefined,
|
|
60
|
+
allProfiles: options.allProfiles,
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
logger.debug(`Sending list-chats query: ${JSON.stringify(query)}`);
|
|
64
|
+
const result = await mediator.send<ListChatsQueryResult>({
|
|
65
|
+
type: QUERY_TYPES.LIST_CHATS,
|
|
66
|
+
payload: query,
|
|
67
|
+
} as Query<ListChatsQueryPayload>);
|
|
68
|
+
|
|
69
|
+
let chats = result.chats;
|
|
70
|
+
|
|
71
|
+
chats = this.applySort(chats, options.sort);
|
|
72
|
+
chats = this.applyDateFilter(chats, options.after, options.before);
|
|
73
|
+
|
|
74
|
+
if (!options.all) {
|
|
75
|
+
chats = chats.slice(options.offset, options.offset + options.limit);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (options.format === "json") {
|
|
79
|
+
this.outputJson(chats, options.path);
|
|
80
|
+
} else {
|
|
81
|
+
this.outputText(chats, options.path, options.allProfiles);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
private applySort(chats: ChatInfo[], sort: "recent" | "oldest" | "alpha"): ChatInfo[] {
|
|
86
|
+
const sorted = [...chats];
|
|
87
|
+
switch (sort) {
|
|
88
|
+
case "recent":
|
|
89
|
+
sorted.sort((a, b) => b.timestamp - a.timestamp);
|
|
90
|
+
break;
|
|
91
|
+
case "oldest":
|
|
92
|
+
sorted.sort((a, b) => a.timestamp - b.timestamp);
|
|
93
|
+
break;
|
|
94
|
+
case "alpha":
|
|
95
|
+
sorted.sort((a, b) => a.title.localeCompare(b.title));
|
|
96
|
+
break;
|
|
97
|
+
}
|
|
98
|
+
return sorted;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
private applyDateFilter(chats: ChatInfo[], after: string, before: string): ChatInfo[] {
|
|
102
|
+
return chats.filter((chat) => {
|
|
103
|
+
const chatDate = new Date(chat.timestamp);
|
|
104
|
+
if (after) {
|
|
105
|
+
const afterDate = new Date(after);
|
|
106
|
+
if (isNaN(afterDate.getTime())) return true;
|
|
107
|
+
if (chatDate < afterDate) return false;
|
|
108
|
+
}
|
|
109
|
+
if (before) {
|
|
110
|
+
const beforeDate = new Date(before);
|
|
111
|
+
if (isNaN(beforeDate.getTime())) return true;
|
|
112
|
+
if (chatDate > beforeDate) return false;
|
|
113
|
+
}
|
|
114
|
+
return true;
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
private outputJson(chats: ChatInfo[], path: string): void {
|
|
119
|
+
const output = JSON.stringify({ chats }, null, 2);
|
|
120
|
+
if (path) {
|
|
121
|
+
this.writeOutput(path, output);
|
|
122
|
+
} else {
|
|
123
|
+
console.log(output);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
private outputText(chats: ChatInfo[], path: string, allProfiles: boolean): void {
|
|
128
|
+
const output = formatChatList(chats, { includeProfileColumn: allProfiles });
|
|
129
|
+
if (path) {
|
|
130
|
+
this.writeOutput(path, output);
|
|
131
|
+
} else {
|
|
132
|
+
console.log(output);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
private writeOutput(path: string, content: string): void {
|
|
137
|
+
writeTextFile(path, content);
|
|
138
|
+
console.log(chalk.dim(`Output written to: ${path}`));
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
private parseArgs(args: string[]): ListCommandOptions {
|
|
142
|
+
const options = { ...DEFAULT_OPTIONS };
|
|
143
|
+
|
|
144
|
+
for (let i = 0; i < args.length; i++) {
|
|
145
|
+
const arg = args[i];
|
|
146
|
+
switch (arg) {
|
|
147
|
+
case "--help":
|
|
148
|
+
case "-h":
|
|
149
|
+
options.help = true;
|
|
150
|
+
break;
|
|
151
|
+
case "--limit":
|
|
152
|
+
case "-n":
|
|
153
|
+
options.limit = parseInt(args[++i], 10) || DEFAULT_OPTIONS.limit;
|
|
154
|
+
break;
|
|
155
|
+
case "--offset":
|
|
156
|
+
options.offset = parseInt(args[++i], 10) || 0;
|
|
157
|
+
break;
|
|
158
|
+
case "--all":
|
|
159
|
+
options.all = true;
|
|
160
|
+
break;
|
|
161
|
+
case "--all-profiles":
|
|
162
|
+
options.allProfiles = true;
|
|
163
|
+
break;
|
|
164
|
+
case "--sort":
|
|
165
|
+
options.sort = this.parseSort(args[++i]);
|
|
166
|
+
break;
|
|
167
|
+
case "--search":
|
|
168
|
+
case "-s":
|
|
169
|
+
options.search = args[++i] ?? "";
|
|
170
|
+
break;
|
|
171
|
+
case "--after":
|
|
172
|
+
options.after = args[++i] ?? "";
|
|
173
|
+
break;
|
|
174
|
+
case "--before":
|
|
175
|
+
options.before = args[++i] ?? "";
|
|
176
|
+
break;
|
|
177
|
+
case "--format":
|
|
178
|
+
case "-f":
|
|
179
|
+
options.format = this.parseFormat(args[++i]);
|
|
180
|
+
break;
|
|
181
|
+
case "--path":
|
|
182
|
+
case "-p":
|
|
183
|
+
options.path = args[++i] ?? "";
|
|
184
|
+
break;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return options;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
private parseSort(value: string | undefined): "recent" | "oldest" | "alpha" {
|
|
192
|
+
if (value === "recent" || value === "oldest" || value === "alpha") return value;
|
|
193
|
+
return DEFAULT_OPTIONS.sort;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
private parseFormat(value: string | undefined): "text" | "json" {
|
|
197
|
+
if (value === "text" || value === "json") return value;
|
|
198
|
+
return DEFAULT_OPTIONS.format;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
private showUsage(): void {
|
|
202
|
+
console.log(chalk.bold("Usage: gemiterm list [options]"));
|
|
203
|
+
console.log("");
|
|
204
|
+
console.log(chalk.bold("Options:"));
|
|
205
|
+
|
|
206
|
+
const flags = [
|
|
207
|
+
{ flag: "--limit, -n N", desc: "Number of results (default: 10)" },
|
|
208
|
+
{ flag: "--offset N", desc: "Skip N results (default: 0)" },
|
|
209
|
+
{ flag: "--all", desc: "Show all conversations (no limit)" },
|
|
210
|
+
{ flag: "--all-profiles", desc: "Show conversations from all profiles (with Profile column in text output)" },
|
|
211
|
+
{ flag: "--sort <mode>", desc: "Sort order: recent, oldest, alpha (default: recent)" },
|
|
212
|
+
{ flag: "--search, -s <query>", desc: "Filter by title search" },
|
|
213
|
+
{ flag: "--after <date>", desc: "Only show chats after this date" },
|
|
214
|
+
{ flag: "--before <date>", desc: "Only show chats before this date" },
|
|
215
|
+
{ flag: "--format, -f <fmt>", desc: "Output format: text, json (default: text)" },
|
|
216
|
+
{ flag: "--path, -p <path>", desc: "Write output to file" },
|
|
217
|
+
{ flag: "--help, -h", desc: "Show this help message" },
|
|
218
|
+
];
|
|
219
|
+
|
|
220
|
+
const maxLen = Math.max(...flags.map((f) => f.flag.length));
|
|
221
|
+
for (const f of flags) {
|
|
222
|
+
const padded = f.flag.padEnd(maxLen + 2);
|
|
223
|
+
console.log(` ${chalk.cyan(padded)}${chalk.dim(f.desc)}`);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
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 {
|
|
6
|
+
COMMAND_TYPES,
|
|
7
|
+
type StartNewChatCommandPayload,
|
|
8
|
+
type StartNewChatCommandResult,
|
|
9
|
+
} from "../../core/command-handlers.ts";
|
|
10
|
+
import { runInteractiveLoop, type MessageHandlerResult } from "../utils/interactive-prompt.ts";
|
|
11
|
+
|
|
12
|
+
interface NewCommandOptions {
|
|
13
|
+
help: boolean;
|
|
14
|
+
profile: string | null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const DEFAULT_OPTIONS: NewCommandOptions = {
|
|
18
|
+
help: false,
|
|
19
|
+
profile: null,
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export class NewCommand implements CliCommand {
|
|
23
|
+
readonly name = "new";
|
|
24
|
+
readonly description = "Start a new conversation";
|
|
25
|
+
|
|
26
|
+
async execute(args: string[], context: CliCommandContext): Promise<void> {
|
|
27
|
+
const logger = new Logger("new-command");
|
|
28
|
+
const options = this.parseArgs(args);
|
|
29
|
+
|
|
30
|
+
if (options.help) {
|
|
31
|
+
this.showUsage();
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
let message: string | null = null;
|
|
36
|
+
|
|
37
|
+
for (const arg of args) {
|
|
38
|
+
if (arg.startsWith("--") || arg.startsWith("-")) continue;
|
|
39
|
+
if (options.profile && arg === options.profile) continue;
|
|
40
|
+
if (!message) {
|
|
41
|
+
message = arg;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const mediator: Mediator = context.mediator;
|
|
46
|
+
|
|
47
|
+
if (message) {
|
|
48
|
+
await this.sendNonInteractive(mediator, message, options.profile, logger);
|
|
49
|
+
} else {
|
|
50
|
+
await this.startInteractive(mediator, options.profile, logger);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
private async sendNonInteractive(
|
|
55
|
+
mediator: Mediator,
|
|
56
|
+
message: string,
|
|
57
|
+
profileName: string | null,
|
|
58
|
+
logger: Logger,
|
|
59
|
+
): Promise<void> {
|
|
60
|
+
logger.debug("Starting new chat with message");
|
|
61
|
+
const payload: StartNewChatCommandPayload = { message };
|
|
62
|
+
if (profileName) {
|
|
63
|
+
payload.profileName = profileName;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const result = await mediator.send<StartNewChatCommandResult>({
|
|
67
|
+
type: COMMAND_TYPES.START_NEW_CHAT,
|
|
68
|
+
payload,
|
|
69
|
+
} as Command<StartNewChatCommandPayload>);
|
|
70
|
+
|
|
71
|
+
console.log(chalk.cyan(`Conversation ID: ${result.conversationId}`));
|
|
72
|
+
console.log(chalk.blue.bold("Model:"));
|
|
73
|
+
console.log(result.response);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
private async startInteractive(
|
|
77
|
+
mediator: Mediator,
|
|
78
|
+
profileName: string | null,
|
|
79
|
+
logger: Logger,
|
|
80
|
+
): Promise<void> {
|
|
81
|
+
let conversationId: string | null = null;
|
|
82
|
+
|
|
83
|
+
const messageHandler = async (message: string): Promise<MessageHandlerResult> => {
|
|
84
|
+
const payload: StartNewChatCommandPayload = { message };
|
|
85
|
+
if (profileName) {
|
|
86
|
+
payload.profileName = profileName;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const result = await mediator.send<StartNewChatCommandResult>({
|
|
90
|
+
type: COMMAND_TYPES.START_NEW_CHAT,
|
|
91
|
+
payload,
|
|
92
|
+
} as Command<StartNewChatCommandPayload>);
|
|
93
|
+
|
|
94
|
+
const isFirst = !conversationId;
|
|
95
|
+
if (isFirst) {
|
|
96
|
+
conversationId = result.conversationId;
|
|
97
|
+
console.log(chalk.dim(`Conversation started: ${chalk.cyan(conversationId)}`));
|
|
98
|
+
} else {
|
|
99
|
+
console.log(chalk.dim(`Response from: ${chalk.cyan(result.conversationId)}`));
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return { response: result.response };
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
await runInteractiveLoop(messageHandler, { profileName });
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
private parseArgs(args: string[]): NewCommandOptions {
|
|
109
|
+
const options = { ...DEFAULT_OPTIONS };
|
|
110
|
+
|
|
111
|
+
for (let i = 0; i < args.length; i++) {
|
|
112
|
+
const arg = args[i];
|
|
113
|
+
if (arg === "--help" || arg === "-h") {
|
|
114
|
+
options.help = true;
|
|
115
|
+
} else if (arg === "--profile" || arg === "-p") {
|
|
116
|
+
const next = args[i + 1];
|
|
117
|
+
if (next && !next.startsWith("-")) {
|
|
118
|
+
options.profile = next;
|
|
119
|
+
i++;
|
|
120
|
+
} else {
|
|
121
|
+
console.error(chalk.red(`Error: --profile requires a profile name`));
|
|
122
|
+
process.exit(1);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return options;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
private showUsage(): void {
|
|
131
|
+
console.log(chalk.bold("Usage: gemiterm new [message] [options]"));
|
|
132
|
+
console.log("");
|
|
133
|
+
console.log(chalk.bold("Arguments:"));
|
|
134
|
+
console.log(
|
|
135
|
+
` ${chalk.cyan("message".padEnd(20))}${chalk.dim("Message to send (optional, starts interactive mode if omitted)")}`,
|
|
136
|
+
);
|
|
137
|
+
console.log("");
|
|
138
|
+
console.log(chalk.bold("Options:"));
|
|
139
|
+
console.log(
|
|
140
|
+
` ${chalk.cyan("--profile, -p <name>".padEnd(22))}${chalk.dim("Use a specific profile (default profile used if omitted)")}`,
|
|
141
|
+
);
|
|
142
|
+
console.log(` ${chalk.cyan("--help, -h".padEnd(22))}${chalk.dim("Show this help message")}`);
|
|
143
|
+
console.log("");
|
|
144
|
+
console.log(chalk.dim("If no message is provided, an interactive chat session will start."));
|
|
145
|
+
console.log(chalk.dim("In interactive mode, type /exit or /quit to exit."));
|
|
146
|
+
}
|
|
147
|
+
}
|
|
@@ -0,0 +1,226 @@
|
|
|
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 {
|
|
10
|
+
listProfiles,
|
|
11
|
+
getDefaultProfileName,
|
|
12
|
+
setDefaultProfileName,
|
|
13
|
+
} from "../../infrastructure/config.ts";
|
|
14
|
+
import { GemitermError } from "../../core/errors.ts";
|
|
15
|
+
import { validateProfileName } from "../../infrastructure/validators.ts";
|
|
16
|
+
import { formatProfileTable } from "../../infrastructure/formatters.ts";
|
|
17
|
+
|
|
18
|
+
const ACTIONS = [
|
|
19
|
+
{ name: "add", usage: "profile add <name>", desc: "Create new profile and authenticate" },
|
|
20
|
+
{ name: "delete", usage: "profile delete <name>", desc: "Delete a profile" },
|
|
21
|
+
{ name: "rename", usage: "profile rename <name> <newName>", desc: "Rename a profile" },
|
|
22
|
+
{ name: "default", usage: "profile default <name>", desc: "Set default profile" },
|
|
23
|
+
{ name: "list", usage: "profile list", desc: "List all profiles with status" },
|
|
24
|
+
] as const;
|
|
25
|
+
|
|
26
|
+
type ProfileAction = (typeof ACTIONS)[number]["name"];
|
|
27
|
+
|
|
28
|
+
export class ProfileCommand implements CliCommand {
|
|
29
|
+
readonly name = "profile";
|
|
30
|
+
readonly description = "Manage authentication profiles";
|
|
31
|
+
|
|
32
|
+
async execute(args: string[], context: CliCommandContext): Promise<void> {
|
|
33
|
+
const logger = new Logger("profile-command");
|
|
34
|
+
logger.debug("Executing profile command", args);
|
|
35
|
+
|
|
36
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
37
|
+
this.showUsage();
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const action = args[0] as ProfileAction | undefined;
|
|
42
|
+
|
|
43
|
+
if (!action) {
|
|
44
|
+
this.showUsage();
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const validActions = ACTIONS.map((a) => a.name);
|
|
49
|
+
if (!validActions.includes(action)) {
|
|
50
|
+
throw new GemitermError(
|
|
51
|
+
`Unknown action '${action}'. Valid actions: ${validActions.join(", ")}`,
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
logger.debug("Profile action:", action);
|
|
56
|
+
|
|
57
|
+
switch (action) {
|
|
58
|
+
case "add":
|
|
59
|
+
await this.addProfile(args.slice(1), logger);
|
|
60
|
+
break;
|
|
61
|
+
case "delete":
|
|
62
|
+
await this.deleteProfile(args.slice(1), logger);
|
|
63
|
+
break;
|
|
64
|
+
case "rename":
|
|
65
|
+
await this.renameProfile(args.slice(1), logger);
|
|
66
|
+
break;
|
|
67
|
+
case "default":
|
|
68
|
+
await this.setDefaultProfile(args.slice(1), logger);
|
|
69
|
+
break;
|
|
70
|
+
case "list":
|
|
71
|
+
await this.listProfiles(logger);
|
|
72
|
+
break;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
private async addProfile(args: string[], logger: Logger): Promise<void> {
|
|
77
|
+
const profileName = args[0];
|
|
78
|
+
if (!profileName) {
|
|
79
|
+
throw new GemitermError("Usage: profile add <name>");
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
validateProfileName(profileName);
|
|
83
|
+
logger.debug("Adding profile:", profileName);
|
|
84
|
+
|
|
85
|
+
if (listProfiles().includes(profileName)) {
|
|
86
|
+
throw new GemitermError(`Profile '${profileName}' already exists.`);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const cookieStorage = new CookieStorage();
|
|
90
|
+
const profileManager = new ProfileManager(cookieStorage);
|
|
91
|
+
profileManager.create(profileName);
|
|
92
|
+
console.log(chalk.green(`Profile '${profileName}' created.`));
|
|
93
|
+
|
|
94
|
+
const driver = new PlaywrightCliDriver();
|
|
95
|
+
const cookieMonitor = new CookieMonitor({ driver, logger });
|
|
96
|
+
const authService = new AuthService({
|
|
97
|
+
driver,
|
|
98
|
+
cookieMonitor,
|
|
99
|
+
cookieStorage,
|
|
100
|
+
logger,
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
await authService.authenticate(profileName);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
private async deleteProfile(args: string[], logger: Logger): Promise<void> {
|
|
107
|
+
const profileName = args[0];
|
|
108
|
+
if (!profileName) {
|
|
109
|
+
throw new GemitermError("Usage: profile delete <name>");
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const profiles = listProfiles();
|
|
113
|
+
if (!profiles.includes(profileName)) {
|
|
114
|
+
throw new GemitermError(`Profile '${profileName}' does not exist.`);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
logger.debug("Deleting profile:", profileName);
|
|
118
|
+
|
|
119
|
+
const confirm = await this.promptInput(`Delete profile '${profileName}'? [y/N]`);
|
|
120
|
+
if (!confirm.toLowerCase().startsWith("y")) {
|
|
121
|
+
console.log(chalk.dim("Cancelled."));
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const cookieStorage = new CookieStorage();
|
|
126
|
+
const profileManager = new ProfileManager(cookieStorage);
|
|
127
|
+
profileManager.delete(profileName);
|
|
128
|
+
logger.info(`Deleted profile: ${profileName}`);
|
|
129
|
+
console.log(chalk.green(`Profile '${profileName}' deleted.`));
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
private async renameProfile(args: string[], logger: Logger): Promise<void> {
|
|
133
|
+
const profileName = args[0];
|
|
134
|
+
const newName = args[1];
|
|
135
|
+
if (!profileName || !newName) {
|
|
136
|
+
throw new GemitermError("Usage: profile rename <name> <newName>");
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const profiles = listProfiles();
|
|
140
|
+
if (!profiles.includes(profileName)) {
|
|
141
|
+
throw new GemitermError(`Profile '${profileName}' does not exist.`);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
validateProfileName(newName);
|
|
145
|
+
logger.debug("Renaming profile:", profileName, "→", newName);
|
|
146
|
+
|
|
147
|
+
if (profiles.includes(newName)) {
|
|
148
|
+
throw new GemitermError(`Profile '${newName}' already exists.`);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const cookieStorage = new CookieStorage();
|
|
152
|
+
const profileManager = new ProfileManager(cookieStorage);
|
|
153
|
+
profileManager.rename(profileName, newName);
|
|
154
|
+
logger.info(`Renamed profile: ${profileName} → ${newName}`);
|
|
155
|
+
console.log(chalk.green(`Profile renamed: ${profileName} → ${newName}`));
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
private async setDefaultProfile(args: string[], logger: Logger): Promise<void> {
|
|
159
|
+
const profileName = args[0];
|
|
160
|
+
if (!profileName) {
|
|
161
|
+
throw new GemitermError("Usage: profile default <name>");
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const profiles = listProfiles();
|
|
165
|
+
if (!profiles.includes(profileName)) {
|
|
166
|
+
throw new GemitermError(`Profile '${profileName}' does not exist.`);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
logger.debug("Setting default profile:", profileName);
|
|
170
|
+
|
|
171
|
+
const cookieStorage = new CookieStorage();
|
|
172
|
+
const profileManager = new ProfileManager(cookieStorage);
|
|
173
|
+
profileManager.setDefault(profileName);
|
|
174
|
+
setDefaultProfileName(profileName);
|
|
175
|
+
logger.info(`Set default profile: ${profileName}`);
|
|
176
|
+
console.log(chalk.green(`Default profile set to '${profileName}'.`));
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
private async listProfiles(logger: Logger): Promise<void> {
|
|
180
|
+
logger.debug("Listing all profiles");
|
|
181
|
+
const profileNames = listProfiles();
|
|
182
|
+
if (profileNames.length === 0) {
|
|
183
|
+
console.log(chalk.dim("No profiles found. Run 'gemiterm login' to create one."));
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const cookieStorage = new CookieStorage();
|
|
188
|
+
const profileManager = new ProfileManager(cookieStorage);
|
|
189
|
+
const defaultName = getDefaultProfileName();
|
|
190
|
+
const statuses = profileNames.map((name) => {
|
|
191
|
+
const status = profileManager.getStatus(name);
|
|
192
|
+
return { ...status, isDefault: name === defaultName };
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
console.log(chalk.bold("Profiles"));
|
|
196
|
+
console.log(formatProfileTable(statuses));
|
|
197
|
+
|
|
198
|
+
const active = statuses.filter((s) => s.isActive);
|
|
199
|
+
logger.info(`${active.length} of ${statuses.length} profile(s) active`);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
private showUsage(): void {
|
|
203
|
+
console.log(chalk.bold("Usage: gemiterm profile <action> [args]"));
|
|
204
|
+
console.log("");
|
|
205
|
+
console.log(chalk.bold("Actions:"));
|
|
206
|
+
|
|
207
|
+
const maxUsageLen = Math.max(...ACTIONS.map((a) => a.usage.length));
|
|
208
|
+
for (const action of ACTIONS) {
|
|
209
|
+
const padded = action.usage.padEnd(maxUsageLen + 2);
|
|
210
|
+
console.log(` ${chalk.cyan(padded)}${chalk.dim(action.desc)}`);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
console.log("");
|
|
214
|
+
console.log(chalk.dim("Run 'gemiterm profile <action> --help' for more information."));
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
private promptInput(prompt: string): Promise<string> {
|
|
218
|
+
return new Promise<string>((resolve) => {
|
|
219
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
220
|
+
rl.question(`${chalk.cyan(prompt + ": ")} `, (answer) => {
|
|
221
|
+
rl.close();
|
|
222
|
+
resolve(answer);
|
|
223
|
+
});
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import { CookieStorage, ProfileManager } from "../../infrastructure/storage.ts";
|
|
3
|
+
import type { CliCommand, CliCommandContext } from "../command-registry.ts";
|
|
4
|
+
import { Logger } from "../../infrastructure/logger.ts";
|
|
5
|
+
import { getConfigDir, ensureConfigDir, listProfiles, getDefaultProfileName } from "../../infrastructure/config.ts";
|
|
6
|
+
import { formatProfileTable } from "../../infrastructure/formatters.ts";
|
|
7
|
+
|
|
8
|
+
export class StatusCommand implements CliCommand {
|
|
9
|
+
readonly name = "status";
|
|
10
|
+
readonly description = "Show configuration and profile status";
|
|
11
|
+
|
|
12
|
+
async execute(args: string[], context: CliCommandContext): Promise<void> {
|
|
13
|
+
const logger = new Logger("status-command");
|
|
14
|
+
logger.debug("Executing status command", args);
|
|
15
|
+
|
|
16
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
17
|
+
console.log("Usage: gemiterm status");
|
|
18
|
+
console.log("");
|
|
19
|
+
console.log("Show configuration and profile status.");
|
|
20
|
+
console.log("");
|
|
21
|
+
console.log("Options:");
|
|
22
|
+
console.log(" -h, --help Show this help message");
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
ensureConfigDir();
|
|
27
|
+
|
|
28
|
+
const configDir = getConfigDir();
|
|
29
|
+
logger.debug("Config directory:", configDir);
|
|
30
|
+
console.log(chalk.bold("Configuration"));
|
|
31
|
+
console.log(` Directory: ${chalk.cyan(configDir)}`);
|
|
32
|
+
console.log("");
|
|
33
|
+
|
|
34
|
+
const profileNames = listProfiles();
|
|
35
|
+
logger.debug("Profile scan found:", profileNames);
|
|
36
|
+
if (profileNames.length === 0) {
|
|
37
|
+
console.log(chalk.dim("No profiles found. Run 'gemiterm login' to create one."));
|
|
38
|
+
process.exit(2);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const cookieStorage = new CookieStorage();
|
|
42
|
+
const profileManager = new ProfileManager(cookieStorage);
|
|
43
|
+
const statuses = profileNames.map((name) => {
|
|
44
|
+
const status = profileManager.getStatus(name);
|
|
45
|
+
return { ...status, isDefault: name === getDefaultProfileName() };
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
console.log(chalk.bold("Profiles"));
|
|
49
|
+
console.log(formatProfileTable(statuses));
|
|
50
|
+
|
|
51
|
+
const defaultName = getDefaultProfileName();
|
|
52
|
+
const active = statuses.filter((s) => s.isActive);
|
|
53
|
+
if (active.length > 0) {
|
|
54
|
+
logger.info(`${active.length} of ${statuses.length} profile(s) active`);
|
|
55
|
+
} else {
|
|
56
|
+
logger.info("No profiles have valid sessions. Run 'gemiterm login' to authenticate.");
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|