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,163 @@
|
|
|
1
|
+
import type { Query, QueryHandler } from "./mediator.ts";
|
|
2
|
+
import type { ChatInfo, Message, ProfileStatus } from "./types.ts";
|
|
3
|
+
import type { IGeminiClientService } from "./command-handlers.ts";
|
|
4
|
+
|
|
5
|
+
function extractPayload<T>(query: Query<T>): T {
|
|
6
|
+
return query.payload;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface ListChatsQueryPayload {
|
|
10
|
+
limit?: number;
|
|
11
|
+
offset?: number;
|
|
12
|
+
search?: string;
|
|
13
|
+
allProfiles?: boolean;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface ListChatsQueryResult {
|
|
17
|
+
chats: ChatInfo[];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface FetchChatQueryPayload {
|
|
21
|
+
conversationId: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface FetchChatQueryResult {
|
|
25
|
+
messages: Message[];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface GetProfileStatusesQueryPayload {}
|
|
29
|
+
|
|
30
|
+
export interface GetProfileStatusesQueryResult {
|
|
31
|
+
statuses: ProfileStatus[];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface GetAuthStatusQueryPayload {}
|
|
35
|
+
|
|
36
|
+
export interface GetAuthStatusQueryResult {
|
|
37
|
+
authenticated: boolean;
|
|
38
|
+
profileName: string | null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface ListModelsQueryPayload {}
|
|
42
|
+
|
|
43
|
+
export interface ListModelsQueryResult {
|
|
44
|
+
models: string[];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export const QUERY_TYPES = {
|
|
48
|
+
LIST_CHATS: "list-chats",
|
|
49
|
+
FETCH_CHAT: "fetch-chat",
|
|
50
|
+
GET_PROFILE_STATUSES: "get-profile-statuses",
|
|
51
|
+
GET_AUTH_STATUS: "get-auth-status",
|
|
52
|
+
LIST_MODELS: "list-models",
|
|
53
|
+
} as const;
|
|
54
|
+
|
|
55
|
+
export type QueryType = (typeof QUERY_TYPES)[keyof typeof QUERY_TYPES];
|
|
56
|
+
|
|
57
|
+
export interface IGeminiClientQueryService {
|
|
58
|
+
listChats(options?: { limit?: number; offset?: number; search?: string }): Promise<ChatInfo[]>;
|
|
59
|
+
fetchChat(conversationId: string): Promise<Message[]>;
|
|
60
|
+
listModels(): Promise<string[]>;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface IProfileQueryService {
|
|
64
|
+
getProfileStatuses(): Promise<ProfileStatus[]>;
|
|
65
|
+
getAuthStatus(): Promise<{ authenticated: boolean; profileName: string | null }>;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export class ListChatsQueryHandler
|
|
69
|
+
implements QueryHandler<ListChatsQueryPayload, ListChatsQueryResult>
|
|
70
|
+
{
|
|
71
|
+
readonly queryType = QUERY_TYPES.LIST_CHATS;
|
|
72
|
+
private readonly getGeminiClient: () => IGeminiClientService;
|
|
73
|
+
private readonly listProfiles: () => string[];
|
|
74
|
+
|
|
75
|
+
constructor(getGeminiClient: () => IGeminiClientService, listProfiles: () => string[]) {
|
|
76
|
+
this.getGeminiClient = getGeminiClient;
|
|
77
|
+
this.listProfiles = listProfiles;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async handle(query: Query<ListChatsQueryPayload>): Promise<ListChatsQueryResult> {
|
|
81
|
+
const { limit, offset, search, allProfiles } = extractPayload(query);
|
|
82
|
+
const options = { limit, offset, search };
|
|
83
|
+
const client = this.getGeminiClient();
|
|
84
|
+
|
|
85
|
+
let chats: ChatInfo[];
|
|
86
|
+
if (allProfiles) {
|
|
87
|
+
const profileNames = this.listProfiles();
|
|
88
|
+
const results = await Promise.all(
|
|
89
|
+
profileNames.map((name) => client.forProfile(name).listChats(options)),
|
|
90
|
+
);
|
|
91
|
+
chats = results.flat();
|
|
92
|
+
chats.sort((a, b) => b.timestamp - a.timestamp);
|
|
93
|
+
} else {
|
|
94
|
+
chats = await client.listChats(options);
|
|
95
|
+
}
|
|
96
|
+
return { chats };
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export class FetchChatQueryHandler
|
|
101
|
+
implements QueryHandler<FetchChatQueryPayload, FetchChatQueryResult>
|
|
102
|
+
{
|
|
103
|
+
readonly queryType = QUERY_TYPES.FETCH_CHAT;
|
|
104
|
+
private readonly geminiClient: IGeminiClientQueryService;
|
|
105
|
+
|
|
106
|
+
constructor(geminiClient: IGeminiClientQueryService) {
|
|
107
|
+
this.geminiClient = geminiClient;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async handle(query: Query<FetchChatQueryPayload>): Promise<FetchChatQueryResult> {
|
|
111
|
+
const { conversationId } = extractPayload(query);
|
|
112
|
+
const messages = await this.geminiClient.fetchChat(conversationId);
|
|
113
|
+
return { messages };
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export class GetProfileStatusesQueryHandler
|
|
118
|
+
implements QueryHandler<GetProfileStatusesQueryPayload, GetProfileStatusesQueryResult>
|
|
119
|
+
{
|
|
120
|
+
readonly queryType = QUERY_TYPES.GET_PROFILE_STATUSES;
|
|
121
|
+
private readonly profileService: IProfileQueryService;
|
|
122
|
+
|
|
123
|
+
constructor(profileService: IProfileQueryService) {
|
|
124
|
+
this.profileService = profileService;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
async handle(_query: Query<GetProfileStatusesQueryPayload>): Promise<GetProfileStatusesQueryResult> {
|
|
128
|
+
const statuses = await this.profileService.getProfileStatuses();
|
|
129
|
+
return { statuses };
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export class GetAuthStatusQueryHandler
|
|
134
|
+
implements QueryHandler<GetAuthStatusQueryPayload, GetAuthStatusQueryResult>
|
|
135
|
+
{
|
|
136
|
+
readonly queryType = QUERY_TYPES.GET_AUTH_STATUS;
|
|
137
|
+
private readonly profileService: IProfileQueryService;
|
|
138
|
+
|
|
139
|
+
constructor(profileService: IProfileQueryService) {
|
|
140
|
+
this.profileService = profileService;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
async handle(_query: Query<GetAuthStatusQueryPayload>): Promise<GetAuthStatusQueryResult> {
|
|
144
|
+
const status = await this.profileService.getAuthStatus();
|
|
145
|
+
return { authenticated: status.authenticated, profileName: status.profileName };
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export class ListModelsQueryHandler
|
|
150
|
+
implements QueryHandler<ListModelsQueryPayload, ListModelsQueryResult>
|
|
151
|
+
{
|
|
152
|
+
readonly queryType = QUERY_TYPES.LIST_MODELS;
|
|
153
|
+
private readonly geminiClient: IGeminiClientQueryService;
|
|
154
|
+
|
|
155
|
+
constructor(geminiClient: IGeminiClientQueryService) {
|
|
156
|
+
this.geminiClient = geminiClient;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
async handle(_query: Query<ListModelsQueryPayload>): Promise<ListModelsQueryResult> {
|
|
160
|
+
const models = await this.geminiClient.listModels();
|
|
161
|
+
return { models };
|
|
162
|
+
}
|
|
163
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
export interface ProfileStatus {
|
|
2
|
+
name: string;
|
|
3
|
+
exists: boolean;
|
|
4
|
+
isActive: boolean;
|
|
5
|
+
expiresAt: string | null;
|
|
6
|
+
isDefault: boolean;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface ChatInfo {
|
|
10
|
+
id: string;
|
|
11
|
+
title: string;
|
|
12
|
+
isPinned: boolean;
|
|
13
|
+
timestamp: number;
|
|
14
|
+
profile?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface Message {
|
|
18
|
+
role: "user" | "model";
|
|
19
|
+
content: string;
|
|
20
|
+
conversationId?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface Conversation {
|
|
24
|
+
id: string;
|
|
25
|
+
title: string;
|
|
26
|
+
messages: Message[];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface AuthResult {
|
|
30
|
+
cookies: Cookie[];
|
|
31
|
+
expiresAt: Date | null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface Cookie {
|
|
35
|
+
name: string;
|
|
36
|
+
value: string;
|
|
37
|
+
domain: string;
|
|
38
|
+
path: string;
|
|
39
|
+
expires: number;
|
|
40
|
+
httpOnly: boolean;
|
|
41
|
+
secure: boolean;
|
|
42
|
+
sameSite: "Strict" | "Lax" | "None";
|
|
43
|
+
}
|
|
File without changes
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import Table from "cli-table3";
|
|
2
|
+
import chalk from "chalk";
|
|
3
|
+
|
|
4
|
+
export interface ColumnDef<T> {
|
|
5
|
+
header: string;
|
|
6
|
+
width: number;
|
|
7
|
+
cell: (row: T) => string;
|
|
8
|
+
align?: "left" | "center" | "right";
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface RenderTableOptions<T> {
|
|
12
|
+
columns: ColumnDef<T>[];
|
|
13
|
+
rows: T[];
|
|
14
|
+
emptyMessage?: string;
|
|
15
|
+
footer?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function renderTable<T>(opts: RenderTableOptions<T>): string {
|
|
19
|
+
if (opts.rows.length === 0 && opts.emptyMessage) {
|
|
20
|
+
return chalk.dim(opts.emptyMessage);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const head = opts.columns.map((c) => c.header);
|
|
24
|
+
const colWidths = opts.columns.map((c) => c.width);
|
|
25
|
+
const colAligns = opts.columns.map((c) => c.align ?? "left");
|
|
26
|
+
|
|
27
|
+
const table = new Table({
|
|
28
|
+
head,
|
|
29
|
+
colWidths,
|
|
30
|
+
colAligns,
|
|
31
|
+
wordWrap: false,
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
for (const row of opts.rows) {
|
|
35
|
+
const cells = opts.columns.map((c) => c.cell(row));
|
|
36
|
+
table.push(cells);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const lines = [table.toString()];
|
|
40
|
+
if (opts.footer) {
|
|
41
|
+
lines.push("");
|
|
42
|
+
lines.push(chalk.dim(opts.footer));
|
|
43
|
+
}
|
|
44
|
+
return lines.join("\n");
|
|
45
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ensureDir,
|
|
3
|
+
existsFile,
|
|
4
|
+
isDirectory,
|
|
5
|
+
listSubdirectories,
|
|
6
|
+
readTextFile,
|
|
7
|
+
writeTextFile,
|
|
8
|
+
} from "./io.ts";
|
|
9
|
+
import {
|
|
10
|
+
getConfigDir,
|
|
11
|
+
getDefaultProfileMarkerPath,
|
|
12
|
+
getProfilesDir,
|
|
13
|
+
dirnamePath,
|
|
14
|
+
DEFAULT_PROFILE_MARKER,
|
|
15
|
+
} from "./path-utils.ts";
|
|
16
|
+
|
|
17
|
+
function getDefaultProfileName(): string {
|
|
18
|
+
const marker = getDefaultProfileMarkerPath();
|
|
19
|
+
if (existsFile(marker)) {
|
|
20
|
+
return readTextFile(marker).trim() || "default";
|
|
21
|
+
}
|
|
22
|
+
return "default";
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function setDefaultProfileName(name: string): void {
|
|
26
|
+
const marker = getDefaultProfileMarkerPath();
|
|
27
|
+
ensureDir(dirnamePath(marker));
|
|
28
|
+
writeTextFile(marker, name);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function listProfiles(): string[] {
|
|
32
|
+
const profilesPath = getProfilesDir();
|
|
33
|
+
if (!isDirectory(profilesPath)) return [];
|
|
34
|
+
return listSubdirectories(profilesPath)
|
|
35
|
+
.filter((entry) => entry !== DEFAULT_PROFILE_MARKER)
|
|
36
|
+
.sort();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function ensureConfigDir(): string {
|
|
40
|
+
const configDir = getConfigDir();
|
|
41
|
+
ensureDir(configDir);
|
|
42
|
+
ensureDir(getProfilesDir());
|
|
43
|
+
return configDir;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export {
|
|
47
|
+
getConfigDir,
|
|
48
|
+
getDefaultProfileName,
|
|
49
|
+
setDefaultProfileName,
|
|
50
|
+
listProfiles,
|
|
51
|
+
ensureConfigDir,
|
|
52
|
+
};
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import type { Message, ChatInfo, ProfileStatus } from "../core/types.ts";
|
|
3
|
+
import { renderTable, type ColumnDef } from "./cli-table.ts";
|
|
4
|
+
|
|
5
|
+
function formatDate(date: Date | string | null): string {
|
|
6
|
+
if (!date) return "N/A";
|
|
7
|
+
const d = typeof date === "string" ? new Date(date) : date;
|
|
8
|
+
return d.toLocaleDateString("en-US", {
|
|
9
|
+
month: "short",
|
|
10
|
+
day: "numeric",
|
|
11
|
+
year: "numeric",
|
|
12
|
+
hour: "2-digit",
|
|
13
|
+
minute: "2-digit",
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function formatTimestamp(ts: number): string {
|
|
18
|
+
return new Date(ts).toLocaleDateString("en-US", {
|
|
19
|
+
month: "short",
|
|
20
|
+
day: "numeric",
|
|
21
|
+
year: "numeric",
|
|
22
|
+
hour: "2-digit",
|
|
23
|
+
minute: "2-digit",
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function formatChatAsMarkdown(
|
|
28
|
+
messages: Message[],
|
|
29
|
+
title: string,
|
|
30
|
+
conversationId?: string,
|
|
31
|
+
includeMetadata?: boolean,
|
|
32
|
+
): string {
|
|
33
|
+
const lines: string[] = [];
|
|
34
|
+
|
|
35
|
+
lines.push(`# ${title}`);
|
|
36
|
+
lines.push("");
|
|
37
|
+
|
|
38
|
+
if (includeMetadata && conversationId) {
|
|
39
|
+
lines.push(`> Conversation ID: ${conversationId}`);
|
|
40
|
+
lines.push(`> Messages: ${messages.length}`);
|
|
41
|
+
lines.push(`> Exported: ${new Date().toISOString()}`);
|
|
42
|
+
lines.push("");
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
for (const msg of messages) {
|
|
46
|
+
const label = msg.role === "user" ? "**You:**" : "**Gemini:**";
|
|
47
|
+
lines.push(`${label}`);
|
|
48
|
+
lines.push("");
|
|
49
|
+
lines.push(msg.content);
|
|
50
|
+
lines.push("");
|
|
51
|
+
lines.push("---");
|
|
52
|
+
lines.push("");
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return lines.join("\n");
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function formatChatAsJson(messages: Message[], conversationId: string): string {
|
|
59
|
+
return JSON.stringify({ conversationId, messages }, null, 2);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function formatProfileTable(statuses: ProfileStatus[]): string {
|
|
63
|
+
const columns: ColumnDef<ProfileStatus>[] = [
|
|
64
|
+
{
|
|
65
|
+
header: "NAME",
|
|
66
|
+
width: 18,
|
|
67
|
+
cell: (s) => (s.isDefault ? chalk.green(s.name) + chalk.dim(" *") : s.name),
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
header: "ACTIVE",
|
|
71
|
+
width: 10,
|
|
72
|
+
cell: (s) =>
|
|
73
|
+
s.isActive
|
|
74
|
+
? chalk.green("\u2713 Yes")
|
|
75
|
+
: s.exists
|
|
76
|
+
? chalk.red("\u2717 No")
|
|
77
|
+
: chalk.dim("\u2014"),
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
header: "EXPIRES",
|
|
81
|
+
width: 22,
|
|
82
|
+
cell: (s) => {
|
|
83
|
+
if (s.expiresAt) return formatTimestamp(new Date(s.expiresAt).getTime());
|
|
84
|
+
if (s.isActive) return chalk.dim("Session");
|
|
85
|
+
return chalk.dim("N/A");
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
header: "DEFAULT",
|
|
90
|
+
width: 10,
|
|
91
|
+
cell: (s) => (s.isDefault ? chalk.green("Yes") : ""),
|
|
92
|
+
},
|
|
93
|
+
];
|
|
94
|
+
|
|
95
|
+
return renderTable<ProfileStatus>({
|
|
96
|
+
columns,
|
|
97
|
+
rows: statuses,
|
|
98
|
+
emptyMessage: "No profiles found. Run 'gemiterm login' to create one.",
|
|
99
|
+
footer: "* = default profile",
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function formatChatList(chats: ChatInfo[], options?: { includeProfileColumn?: boolean }): string {
|
|
104
|
+
const includeProfile = options?.includeProfileColumn === true;
|
|
105
|
+
|
|
106
|
+
const columns: ColumnDef<ChatInfo>[] = [
|
|
107
|
+
{ header: "ID", width: 24, cell: (c) => chalk.dim(c.id) },
|
|
108
|
+
{ header: "TITLE", width: 40, cell: (c) => c.title },
|
|
109
|
+
{ header: "DATE", width: 22, cell: (c) => formatTimestamp(c.timestamp) },
|
|
110
|
+
{
|
|
111
|
+
header: "PIN",
|
|
112
|
+
width: 6,
|
|
113
|
+
cell: (c) => (c.isPinned ? chalk.yellow("\uD83D\uDCCC") : ""),
|
|
114
|
+
},
|
|
115
|
+
];
|
|
116
|
+
if (includeProfile) {
|
|
117
|
+
columns.push({
|
|
118
|
+
header: "PROFILE",
|
|
119
|
+
width: 14,
|
|
120
|
+
cell: (c) => c.profile ?? "",
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return renderTable<ChatInfo>({
|
|
125
|
+
columns,
|
|
126
|
+
rows: chats,
|
|
127
|
+
emptyMessage: "No conversations found.",
|
|
128
|
+
footer: `Total: ${chats.length} conversation${chats.length !== 1 ? "s" : ""}`,
|
|
129
|
+
});
|
|
130
|
+
}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical home for file-system access in `src/`.
|
|
3
|
+
*
|
|
4
|
+
* No other source file in `src/` may import from `node:fs` or `node:path`
|
|
5
|
+
* directly. The only allowed exception is `src/services/install-browser-service.ts`,
|
|
6
|
+
* which keeps a `node:path` import for the WSL `9p`/`drvfs` mount parser.
|
|
7
|
+
*
|
|
8
|
+
* This module is intentionally small: every function is a thin wrapper around
|
|
9
|
+
* the corresponding `node:fs` call with consistent semantics (always-recursive
|
|
10
|
+
* `mkdir`, safe returns, structured errors). New functions should be added
|
|
11
|
+
* only when at least 2 call sites need them; ad-hoc single-use helpers should
|
|
12
|
+
* stay in the call site.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import {
|
|
16
|
+
existsSync,
|
|
17
|
+
mkdirSync,
|
|
18
|
+
readdirSync,
|
|
19
|
+
readFileSync,
|
|
20
|
+
renameSync,
|
|
21
|
+
rmSync,
|
|
22
|
+
statSync,
|
|
23
|
+
writeFileSync,
|
|
24
|
+
} from "node:fs";
|
|
25
|
+
import { dirname, join, resolve } from "node:path";
|
|
26
|
+
|
|
27
|
+
export class IOError extends Error {
|
|
28
|
+
override readonly name = "IOError";
|
|
29
|
+
constructor(message: string, public readonly cause?: Error) {
|
|
30
|
+
super(message);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function wrap(op: string, path: string, cause?: Error): IOError {
|
|
35
|
+
return new IOError(`${op} failed for ${path}`, cause);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function ensureDir(path: string): void {
|
|
39
|
+
try {
|
|
40
|
+
mkdirSync(path, { recursive: true });
|
|
41
|
+
} catch (err) {
|
|
42
|
+
throw wrap("ensureDir", path, err instanceof Error ? err : undefined);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function existsFile(path: string): boolean {
|
|
47
|
+
return existsSync(path);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function readTextFile(path: string): string {
|
|
51
|
+
try {
|
|
52
|
+
return readFileSync(path, "utf-8");
|
|
53
|
+
} catch (err) {
|
|
54
|
+
throw wrap("readTextFile", path, err instanceof Error ? err : undefined);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Reads the file at `path` and returns its UTF-8 content. On any error
|
|
60
|
+
* (ENOENT, EACCES, EISDIR), returns the empty string `""` instead of
|
|
61
|
+
* throwing.
|
|
62
|
+
*
|
|
63
|
+
* Note: the `""` return conflates "file does not exist" with "file exists
|
|
64
|
+
* but is empty". This is appropriate for callers that only need a string
|
|
65
|
+
* for `.includes()` / `.trim()` checks (for example, the WSL `/proc/version`
|
|
66
|
+
* probe in `install-browser-service.ts`). Callers that need to distinguish
|
|
67
|
+
* the two cases should use `readTextFile` and handle the `IOError`.
|
|
68
|
+
*/
|
|
69
|
+
function safeReadTextFile(path: string): string {
|
|
70
|
+
try {
|
|
71
|
+
return readFileSync(path, "utf-8");
|
|
72
|
+
} catch {
|
|
73
|
+
return "";
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function writeTextFile(path: string, content: string): void {
|
|
78
|
+
const absolute = resolve(path);
|
|
79
|
+
const parent = dirname(absolute);
|
|
80
|
+
ensureDir(parent);
|
|
81
|
+
try {
|
|
82
|
+
writeFileSync(absolute, content, "utf-8");
|
|
83
|
+
} catch (err) {
|
|
84
|
+
throw wrap("writeTextFile", absolute, err instanceof Error ? err : undefined);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function readJsonFile<T = unknown>(path: string): T {
|
|
89
|
+
let raw: string;
|
|
90
|
+
try {
|
|
91
|
+
raw = readFileSync(path, "utf-8");
|
|
92
|
+
} catch (err) {
|
|
93
|
+
throw wrap("readJsonFile", path, err instanceof Error ? err : undefined);
|
|
94
|
+
}
|
|
95
|
+
try {
|
|
96
|
+
return JSON.parse(raw) as T;
|
|
97
|
+
} catch (err) {
|
|
98
|
+
throw new IOError(
|
|
99
|
+
`readJsonFile: invalid JSON in ${path}: ${err instanceof Error ? err.message : String(err)}`,
|
|
100
|
+
err instanceof Error ? err : undefined,
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function writeJsonFile(path: string, data: unknown): void {
|
|
106
|
+
writeTextFile(path, JSON.stringify(data, null, 2));
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function removeDir(path: string): void {
|
|
110
|
+
try {
|
|
111
|
+
rmSync(path, { recursive: true, force: true });
|
|
112
|
+
} catch (err) {
|
|
113
|
+
throw wrap("removeDir", path, err instanceof Error ? err : undefined);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function renameDir(src: string, dest: string): void {
|
|
118
|
+
try {
|
|
119
|
+
renameSync(src, dest);
|
|
120
|
+
} catch (err) {
|
|
121
|
+
throw new IOError(
|
|
122
|
+
`renameDir: ${src} -> ${dest}: ${err instanceof Error ? err.message : String(err)}`,
|
|
123
|
+
err instanceof Error ? err : undefined,
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function isDirectory(path: string): boolean {
|
|
129
|
+
try {
|
|
130
|
+
return statSync(path).isDirectory();
|
|
131
|
+
} catch {
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function listSubdirectories(path: string): string[] {
|
|
137
|
+
if (!existsSync(path)) {
|
|
138
|
+
return [];
|
|
139
|
+
}
|
|
140
|
+
return readdirSync(path).filter((entry) => {
|
|
141
|
+
try {
|
|
142
|
+
return statSync(join(path, entry)).isDirectory();
|
|
143
|
+
} catch {
|
|
144
|
+
return false;
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export {
|
|
150
|
+
ensureDir,
|
|
151
|
+
existsFile,
|
|
152
|
+
readTextFile,
|
|
153
|
+
safeReadTextFile,
|
|
154
|
+
writeTextFile,
|
|
155
|
+
readJsonFile,
|
|
156
|
+
writeJsonFile,
|
|
157
|
+
removeDir,
|
|
158
|
+
renameDir,
|
|
159
|
+
isDirectory,
|
|
160
|
+
listSubdirectories,
|
|
161
|
+
};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
type LogLevel = "DEBUG" | "INFO" | "WARN" | "ERROR";
|
|
2
|
+
|
|
3
|
+
class Logger {
|
|
4
|
+
private static verbose = false;
|
|
5
|
+
|
|
6
|
+
static setVerbose(enabled: boolean): void {
|
|
7
|
+
Logger.verbose = enabled;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
static isVerbose(): boolean {
|
|
11
|
+
return Logger.verbose;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
constructor(private readonly module: string) {}
|
|
15
|
+
|
|
16
|
+
debug(message: string, ...args: unknown[]): void {
|
|
17
|
+
if (Logger.verbose) {
|
|
18
|
+
Logger.output("DEBUG", this.module, message, args);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
info(message: string, ...args: unknown[]): void {
|
|
23
|
+
Logger.output("INFO", this.module, message, args);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
warn(message: string, ...args: unknown[]): void {
|
|
27
|
+
Logger.output("WARN", this.module, message, args);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
error(message: string, ...args: unknown[]): void {
|
|
31
|
+
Logger.output("ERROR", this.module, message, args);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
private static output(level: LogLevel, module: string, message: string, args: unknown[]): void {
|
|
35
|
+
const timestamp = new Date().toISOString();
|
|
36
|
+
const formatted = args.length > 0 ? `${message} ${args.map(String).join(" ")}` : message;
|
|
37
|
+
process.stderr.write(`[${timestamp}] [${level}] [${module}] ${formatted}\n`);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export { Logger };
|