gemiterm 2.0.0 → 2.1.1
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/README.md +32 -9
- package/package.json +13 -4
- package/src/cli/command-registry.ts +66 -62
- package/src/cli/commands/continue-command.ts +75 -1
- package/src/cli/commands/install-skills-command.ts +35 -0
- package/src/cli/commands/new-command.ts +62 -0
- package/src/cli/index.ts +19 -44
- package/src/cli/utils/long-arg-guard.ts +28 -0
- package/src/cli/utils/prompt-file.ts +26 -0
- package/src/infrastructure/cli-parser.ts +77 -0
- package/src/infrastructure/io.ts +9 -0
- package/src/services/skills-cli-driver.ts +58 -0
- package/src/services/skills-service.ts +52 -0
- package/.env +0 -2
- package/.github/workflows/manual-release.yml +0 -34
- package/.github/workflows/release.yml +0 -198
- package/.github/workflows/test.yml +0 -29
- package/.opencode/commands/opsx-apply.md +0 -152
- package/.opencode/commands/opsx-archive.md +0 -157
- package/.opencode/commands/opsx-explore.md +0 -169
- package/.opencode/commands/opsx-propose.md +0 -104
- package/.opencode/commands/opsx-sync.md +0 -140
- package/.opencode/commands/search-intelligently.md +0 -9
- package/.opencode/opencode.json +0 -20
- package/.opencode/skills/intellisearch/.version +0 -1
- package/.opencode/skills/intellisearch/SKILL.md +0 -84
- package/.opencode/skills/intellisearch/references/brave-search.md +0 -67
- package/.opencode/skills/intellisearch/references/ddg-search.md +0 -79
- package/.opencode/skills/intellisearch/references/deepwiki-tools.md +0 -246
- package/.opencode/skills/intellisearch/references/examples.md +0 -97
- package/.opencode/skills/intellisearch/references/gh-cli.md +0 -143
- package/.opencode/skills/intellisearch/references/google-search.md +0 -40
- package/.opencode/skills/intellisearch/references/search-workflow.md +0 -143
- package/.opencode/skills/intellisearch/references/workflow.md +0 -115
- package/.opencode/skills/openspec-apply-change/SKILL.md +0 -159
- package/.opencode/skills/openspec-archive-change/SKILL.md +0 -117
- package/.opencode/skills/openspec-explore/SKILL.md +0 -287
- package/.opencode/skills/openspec-propose/SKILL.md +0 -111
- package/.opencode/skills/openspec-sync-specs/SKILL.md +0 -147
- package/.serena/project.yml +0 -133
- package/AGENTS.md +0 -136
- package/CHANGELOG.md +0 -3
- package/bun.lock +0 -105
- package/docs/INSTALL.md +0 -185
- package/docs/PLAYWRIGHT_CLI_API.md +0 -306
- package/docs/python-release-for-reference.md +0 -175
- package/examples/sample_json_export.json +0 -32
- package/examples/sample_markdown_export.md +0 -221
- package/examples/usage_examples.sh +0 -222
- package/install.ps1 +0 -264
- package/install.sh +0 -275
- package/openspec/MAESTRO_MIGRATION.md +0 -180
- package/openspec/changes/archive/2026-06-08-command-spec-conformance/.openspec.yaml +0 -2
- package/openspec/changes/archive/2026-06-08-command-spec-conformance/design.md +0 -194
- package/openspec/changes/archive/2026-06-08-command-spec-conformance/proposal.md +0 -52
- package/openspec/changes/archive/2026-06-08-command-spec-conformance/specs/multi-profile-conversations/spec.md +0 -133
- package/openspec/changes/archive/2026-06-08-command-spec-conformance/tasks.md +0 -95
- package/openspec/changes/archive/2026-06-09-refactor-interactive-prompt-loop/.openspec.yaml +0 -2
- package/openspec/changes/archive/2026-06-09-refactor-interactive-prompt-loop/design.md +0 -28
- package/openspec/changes/archive/2026-06-09-refactor-interactive-prompt-loop/proposal.md +0 -22
- package/openspec/changes/archive/2026-06-09-refactor-interactive-prompt-loop/specs/interactive-prompt-loop/spec.md +0 -35
- package/openspec/changes/archive/2026-06-09-refactor-interactive-prompt-loop/tasks.md +0 -28
- package/openspec/changes/archive/2026-06-09-replace-gemini-api-placeholder-with-gemini-reverse/.openspec.yaml +0 -2
- package/openspec/changes/archive/2026-06-09-replace-gemini-api-placeholder-with-gemini-reverse/design.md +0 -323
- package/openspec/changes/archive/2026-06-09-replace-gemini-api-placeholder-with-gemini-reverse/proposal.md +0 -122
- package/openspec/changes/archive/2026-06-09-replace-gemini-api-placeholder-with-gemini-reverse/specs/no-capability-changes/spec.md +0 -29
- package/openspec/changes/archive/2026-06-09-replace-gemini-api-placeholder-with-gemini-reverse/tasks.md +0 -172
- package/openspec/changes/archive/2026-06-09-v2-install-migration/.openspec.yaml +0 -2
- package/openspec/changes/archive/2026-06-09-v2-install-migration/design.md +0 -241
- package/openspec/changes/archive/2026-06-09-v2-install-migration/proposal.md +0 -41
- package/openspec/changes/archive/2026-06-09-v2-install-migration/specs/v2-installer/spec.md +0 -205
- package/openspec/changes/archive/2026-06-09-v2-install-migration/tasks.md +0 -97
- package/openspec/changes/commander-cli-parser/.openspec.yaml +0 -2
- package/openspec/changes/commander-cli-parser/design.md +0 -107
- package/openspec/changes/commander-cli-parser/proposal.md +0 -31
- package/openspec/changes/commander-cli-parser/specs/cli/spec.md +0 -48
- package/openspec/changes/commander-cli-parser/tasks.md +0 -33
- package/openspec/changes/cross-platform-build-and-ci/.openspec.yaml +0 -2
- package/openspec/changes/cross-platform-build-and-ci/design.md +0 -213
- package/openspec/changes/cross-platform-build-and-ci/proposal.md +0 -50
- package/openspec/changes/cross-platform-build-and-ci/specs/path-and-file-mediation/spec.md +0 -335
- package/openspec/changes/cross-platform-build-and-ci/specs/release-pipeline/spec.md +0 -95
- package/openspec/changes/cross-platform-build-and-ci/tasks.md +0 -164
- package/openspec/config.yaml +0 -20
- package/openspec/specs/auth/spec.md +0 -394
- package/openspec/specs/cli/spec.md +0 -108
- package/openspec/specs/commands/spec.md +0 -409
- package/openspec/specs/configuration/spec.md +0 -130
- package/openspec/specs/conversations/spec.md +0 -139
- package/openspec/specs/domain-model/spec.md +0 -128
- package/openspec/specs/formatters/spec.md +0 -158
- package/openspec/specs/install-browser/spec.md +0 -146
- package/openspec/specs/interactive-prompt-loop/spec.md +0 -41
- package/openspec/specs/logger/spec.md +0 -71
- package/openspec/specs/mediator/spec.md +0 -72
- package/openspec/specs/multi-profile-conversations/spec.md +0 -145
- package/openspec/specs/path-utils/spec.md +0 -101
- package/openspec/specs/profiles/spec.md +0 -94
- package/openspec/specs/storage/spec.md +0 -198
- package/openspec/specs/testing/spec.md +0 -237
- package/openspec/specs/v2-installer/spec.md +0 -205
- package/openspec/specs/validators/spec.md +0 -65
- package/scripts/build.ts +0 -88
- package/scripts/clean-build.ps1 +0 -4
- package/scripts/clean-build.sh +0 -5
- package/scripts/install-browser.ps1 +0 -5
- package/scripts/install-browser.sh +0 -6
- package/scripts/lint-path-mediation.ps1 +0 -65
- package/scripts/lint-path-mediation.sh +0 -40
- package/tests/cli/auth-command.test.ts +0 -220
- package/tests/cli/command-registry.test.ts +0 -67
- package/tests/cli/continue-command.test.ts +0 -100
- package/tests/cli/delete-command.test.ts +0 -128
- package/tests/cli/export-all-command.test.ts +0 -375
- package/tests/cli/install-browser-command.test.ts +0 -48
- package/tests/cli/list-command.test.ts +0 -179
- package/tests/cli/new-command.test.ts +0 -139
- package/tests/cli/status-command.test.ts +0 -106
- package/tests/core/query-handlers.test.ts +0 -187
- package/tests/fixtures/auth-fixtures.ts +0 -98
- package/tests/fixtures/chat-fixtures.ts +0 -87
- package/tests/infrastructure/cli-table.test.ts +0 -107
- package/tests/infrastructure/formatters.test.ts +0 -202
- package/tests/infrastructure/io.test.ts +0 -197
- package/tests/infrastructure/logger.test.ts +0 -74
- package/tests/infrastructure/storage.test.ts +0 -335
- package/tests/infrastructure/validators.test.ts +0 -67
- package/tests/integration/commands/auth.test.ts +0 -186
- package/tests/integration/commands/continue.test.ts +0 -130
- package/tests/integration/commands/delete.test.ts +0 -140
- package/tests/integration/commands/export.test.ts +0 -266
- package/tests/integration/commands/fetch.test.ts +0 -238
- package/tests/integration/commands/list.test.ts +0 -281
- package/tests/integration/commands/profile.test.ts +0 -258
- package/tests/integration/commands/status.test.ts +0 -223
- package/tests/integration/installer-script-shape.test.ts +0 -67
- package/tests/parity/compare-outputs.ts +0 -359
- package/tests/parity/test-commands-parity.ps1 +0 -82
- package/tests/parity/test-commands-parity.sh +0 -61
- package/tests/services/auth-service.test.ts +0 -315
- package/tests/services/cookie-monitor.test.ts +0 -229
- package/tests/services/cookie-storage-service.test.ts +0 -230
- package/tests/services/gemini-client-wrapper.test.ts +0 -688
- package/tests/services/install-browser-service.test.ts +0 -83
- package/tests/services/playwright-cli-driver.test.ts +0 -468
- package/tests/services/profile-auth-manager.test.ts +0 -334
- package/tests/services/profile-service.test.ts +0 -224
- package/tests/setup.ts +0 -95
- package/tests/smoke/smoke.test.ts +0 -52
- package/tests/unit/config.test.ts +0 -159
- package/tests/unit/errors.test.ts +0 -119
- package/tests/unit/formatters.test.ts +0 -316
- package/tests/unit/mediator.test.ts +0 -152
- package/tests/unit/path-utils.test.ts +0 -289
- package/tsconfig.json +0 -16
package/README.md
CHANGED
|
@@ -4,13 +4,37 @@ Access and manage your Gemini web chats from the command line. GemiTerm bridges
|
|
|
4
4
|
|
|
5
5
|
## Prerequisites
|
|
6
6
|
|
|
7
|
-
- **[Bun](https://bun.sh)** runtime
|
|
7
|
+
- **[Bun](https://bun.sh)** runtime ≥ 1.0.0
|
|
8
8
|
- **Chromium Browser** — GemiTerm uses your system Chrome/Edge if available, otherwise installs Playwright's Chromium automatically
|
|
9
9
|
- **Google Account** with access to [Gemini](https://gemini.google.com)
|
|
10
10
|
|
|
11
|
+
## Quick Start (no install)
|
|
12
|
+
|
|
13
|
+
Use `bunx` to run GemiTerm directly — no install step needed:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
bunx gemiterm auth # authenticate with Google
|
|
17
|
+
bunx gemiterm list # list your chats
|
|
18
|
+
```
|
|
19
|
+
|
|
11
20
|
## Installation
|
|
12
21
|
|
|
13
|
-
|
|
22
|
+
### Global install (recommended for daily use)
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
bun install gemiterm -g
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Then use from anywhere:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
gemiterm auth
|
|
32
|
+
gemiterm list
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### Install scripts (binary drop)
|
|
36
|
+
|
|
37
|
+
For systems without Bun — the script auto-bootstraps Bun if needed.
|
|
14
38
|
|
|
15
39
|
**Windows** (PowerShell 7+):
|
|
16
40
|
|
|
@@ -24,14 +48,9 @@ irm https://github.com/expert-vision-software/GemiTerm/releases/latest/download/
|
|
|
24
48
|
curl -fsSL https://github.com/expert-vision-software/GemiTerm/releases/latest/download/install.sh | bash
|
|
25
49
|
```
|
|
26
50
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
## Quick Start
|
|
51
|
+
See [docs/INSTALL.md](docs/INSTALL.md) for the full guide, uninstall instructions, troubleshooting, and build-from-source steps.
|
|
30
52
|
|
|
31
|
-
|
|
32
|
-
gemiterm auth # authenticate with Google
|
|
33
|
-
gemiterm list # list your chats
|
|
34
|
-
```
|
|
53
|
+
> **Upgrading from v1.4.1?** Your profiles, cookies, and default profile marker are preserved. See the "Upgrade from v1.4.1" section in [docs/INSTALL.md](docs/INSTALL.md) for details.
|
|
35
54
|
|
|
36
55
|
## Development
|
|
37
56
|
|
|
@@ -197,6 +216,10 @@ Checks for system Chrome/Edge first, falls back to installing Playwright's Chrom
|
|
|
197
216
|
gemiterm -v <command>
|
|
198
217
|
```
|
|
199
218
|
|
|
219
|
+
## Skills
|
|
220
|
+
|
|
221
|
+
GemiTerm has an associated skills repository for AI coding agents (Copilot, Claude, etc.) that provides domain-specific instructions for working with **gemiterm** and automating common workflows. There's also a fun `debate-with-gemini` skill that created an interactive turn-based conversation between your agent and gemini. See [opencode-gemiterm-skills](https://www.npmjs.com/package/opencode-gemiterm-skills).
|
|
222
|
+
|
|
200
223
|
## Configuration
|
|
201
224
|
|
|
202
225
|
### Configuration Directory
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gemiterm",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.1.1",
|
|
4
4
|
"module": "index.ts",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"repository": {
|
|
@@ -14,9 +14,16 @@
|
|
|
14
14
|
"publishConfig": {
|
|
15
15
|
"access": "public"
|
|
16
16
|
},
|
|
17
|
-
"
|
|
18
|
-
"
|
|
17
|
+
"bin": {
|
|
18
|
+
"gemiterm": "src/cli/index.ts"
|
|
19
|
+
},
|
|
20
|
+
"engines": {
|
|
21
|
+
"bun": ">=1.0.0"
|
|
19
22
|
},
|
|
23
|
+
"files": [
|
|
24
|
+
"src/",
|
|
25
|
+
"README.md"
|
|
26
|
+
],
|
|
20
27
|
"scripts": {
|
|
21
28
|
"start": "bun run src/cli/index.ts",
|
|
22
29
|
"dev": "bun run src/cli/index.ts",
|
|
@@ -34,12 +41,14 @@
|
|
|
34
41
|
"build:windows": "bun run scripts/build.ts windows",
|
|
35
42
|
"build:release": "bun run scripts/build.ts release"
|
|
36
43
|
},
|
|
37
|
-
"
|
|
44
|
+
"devDependencies": {
|
|
45
|
+
"@types/bun": "^1.3.14",
|
|
38
46
|
"typescript": "^6.0.3"
|
|
39
47
|
},
|
|
40
48
|
"dependencies": {
|
|
41
49
|
"chalk": "^5.6.2",
|
|
42
50
|
"cli-table3": "^0.6.5",
|
|
51
|
+
"commander": "^15.0.0",
|
|
43
52
|
"gemini-reverse": "^1.0.12"
|
|
44
53
|
}
|
|
45
54
|
}
|
|
@@ -1,62 +1,66 @@
|
|
|
1
|
-
import type { Mediator } from "../core/mediator.ts";
|
|
2
|
-
import type { ProfileAuthManager } from "../services/profile-auth-manager.ts";
|
|
3
|
-
import { AuthCommand } from "./commands/auth-command.ts";
|
|
4
|
-
import { ProfileCommand } from "./commands/profile-command.ts";
|
|
5
|
-
import { StatusCommand } from "./commands/status-command.ts";
|
|
6
|
-
import { ListCommand } from "./commands/list-command.ts";
|
|
7
|
-
import { FetchCommand } from "./commands/fetch-command.ts";
|
|
8
|
-
import { ContinueCommand } from "./commands/continue-command.ts";
|
|
9
|
-
import { NewCommand } from "./commands/new-command.ts";
|
|
10
|
-
import { DeleteCommand } from "./commands/delete-command.ts";
|
|
11
|
-
import { ExportCommand } from "./commands/export-command.ts";
|
|
12
|
-
import { ExportAllCommand } from "./commands/export-all-command.ts";
|
|
13
|
-
import { InstallBrowserCommand } from "./commands/install-browser-command.ts";
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
readonly
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
this.register("
|
|
53
|
-
this.register("
|
|
54
|
-
this.register("
|
|
55
|
-
this.register("
|
|
56
|
-
this.register("
|
|
57
|
-
this.register("
|
|
58
|
-
this.register("
|
|
59
|
-
this.register("
|
|
60
|
-
this.register("
|
|
61
|
-
|
|
62
|
-
|
|
1
|
+
import type { Mediator } from "../core/mediator.ts";
|
|
2
|
+
import type { ProfileAuthManager } from "../services/profile-auth-manager.ts";
|
|
3
|
+
import { AuthCommand } from "./commands/auth-command.ts";
|
|
4
|
+
import { ProfileCommand } from "./commands/profile-command.ts";
|
|
5
|
+
import { StatusCommand } from "./commands/status-command.ts";
|
|
6
|
+
import { ListCommand } from "./commands/list-command.ts";
|
|
7
|
+
import { FetchCommand } from "./commands/fetch-command.ts";
|
|
8
|
+
import { ContinueCommand } from "./commands/continue-command.ts";
|
|
9
|
+
import { NewCommand } from "./commands/new-command.ts";
|
|
10
|
+
import { DeleteCommand } from "./commands/delete-command.ts";
|
|
11
|
+
import { ExportCommand } from "./commands/export-command.ts";
|
|
12
|
+
import { ExportAllCommand } from "./commands/export-all-command.ts";
|
|
13
|
+
import { InstallBrowserCommand } from "./commands/install-browser-command.ts";
|
|
14
|
+
import { InstallSkillsCommand } from "./commands/install-skills-command.ts";
|
|
15
|
+
|
|
16
|
+
export interface CliCommandContext {
|
|
17
|
+
verbose: boolean;
|
|
18
|
+
mediator: Mediator;
|
|
19
|
+
profileAuthManager: ProfileAuthManager;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface CliCommand {
|
|
23
|
+
readonly name: string;
|
|
24
|
+
readonly description: string;
|
|
25
|
+
execute(args: string[], context: CliCommandContext): Promise<void>;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export class CommandRegistry {
|
|
29
|
+
private handlers = new Map<string, CliCommand>();
|
|
30
|
+
|
|
31
|
+
register(commandName: string, handler: CliCommand): void {
|
|
32
|
+
if (this.handlers.has(commandName)) {
|
|
33
|
+
throw new Error(`Command already registered: ${commandName}`);
|
|
34
|
+
}
|
|
35
|
+
this.handlers.set(commandName, handler);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
getHandler(commandName: string): CliCommand | undefined {
|
|
39
|
+
return this.handlers.get(commandName);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
has(commandName: string): boolean {
|
|
43
|
+
return this.handlers.has(commandName);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
getRegisteredNames(): string[] {
|
|
47
|
+
return [...this.handlers.keys()];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
registerAllCommands(): void {
|
|
51
|
+
const authCommand = new AuthCommand();
|
|
52
|
+
this.register("auth", authCommand);
|
|
53
|
+
this.register("login", authCommand);
|
|
54
|
+
this.register("profile", new ProfileCommand());
|
|
55
|
+
this.register("status", new StatusCommand());
|
|
56
|
+
this.register("list", new ListCommand());
|
|
57
|
+
this.register("fetch", new FetchCommand());
|
|
58
|
+
this.register("continue", new ContinueCommand());
|
|
59
|
+
this.register("new", new NewCommand());
|
|
60
|
+
this.register("delete", new DeleteCommand());
|
|
61
|
+
this.register("export", new ExportCommand());
|
|
62
|
+
this.register("export-all", new ExportAllCommand());
|
|
63
|
+
this.register("install-browser", new InstallBrowserCommand());
|
|
64
|
+
this.register("install-skills", new InstallSkillsCommand());
|
|
65
|
+
}
|
|
66
|
+
}
|
|
@@ -9,13 +9,18 @@ import {
|
|
|
9
9
|
type SendMessageCommandResult,
|
|
10
10
|
} from "../../core/command-handlers.ts";
|
|
11
11
|
import { runInteractiveLoop, type MessageHandlerResult } from "../utils/interactive-prompt.ts";
|
|
12
|
+
import { checkArgLength } from "../utils/long-arg-guard.ts";
|
|
13
|
+
import { loadPromptFromFile, spillOverToTempFile } from "../utils/prompt-file.ts";
|
|
14
|
+
import { removeFile } from "../../infrastructure/io.ts";
|
|
12
15
|
|
|
13
16
|
interface ContinueCommandOptions {
|
|
14
17
|
help: boolean;
|
|
18
|
+
promptFile: string | null;
|
|
15
19
|
}
|
|
16
20
|
|
|
17
21
|
const DEFAULT_OPTIONS: ContinueCommandOptions = {
|
|
18
22
|
help: false,
|
|
23
|
+
promptFile: null,
|
|
19
24
|
};
|
|
20
25
|
|
|
21
26
|
export class ContinueCommand implements CliCommand {
|
|
@@ -36,6 +41,7 @@ export class ContinueCommand implements CliCommand {
|
|
|
36
41
|
|
|
37
42
|
for (const arg of args) {
|
|
38
43
|
if (arg.startsWith("--") || arg.startsWith("-")) continue;
|
|
44
|
+
if (options.promptFile && arg === options.promptFile) continue;
|
|
39
45
|
if (!conversationId) {
|
|
40
46
|
conversationId = arg;
|
|
41
47
|
} else if (!message) {
|
|
@@ -43,11 +49,66 @@ export class ContinueCommand implements CliCommand {
|
|
|
43
49
|
}
|
|
44
50
|
}
|
|
45
51
|
|
|
52
|
+
if (options.promptFile) {
|
|
53
|
+
if (!conversationId) {
|
|
54
|
+
console.error(
|
|
55
|
+
chalk.red(
|
|
56
|
+
`Error: --prompt-file requires a conversation_id. ` +
|
|
57
|
+
`Specify a conversation to continue, e.g. \`gemiterm continue <conversation_id> --prompt-file <path>\`.`,
|
|
58
|
+
),
|
|
59
|
+
);
|
|
60
|
+
process.exit(1);
|
|
61
|
+
}
|
|
62
|
+
if (message) {
|
|
63
|
+
console.error(
|
|
64
|
+
chalk.red(
|
|
65
|
+
`Error: cannot use --prompt-file together with a positional message argument. ` +
|
|
66
|
+
`Use one or the other, not both.`,
|
|
67
|
+
),
|
|
68
|
+
);
|
|
69
|
+
process.exit(1);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
46
73
|
if (!conversationId) {
|
|
47
74
|
await this.invokeListCommand(context);
|
|
48
75
|
return;
|
|
49
76
|
}
|
|
50
77
|
|
|
78
|
+
let effectivePromptFile: string | null = null;
|
|
79
|
+
let isSpillover = false;
|
|
80
|
+
if (options.promptFile) {
|
|
81
|
+
effectivePromptFile = options.promptFile;
|
|
82
|
+
} else if (message) {
|
|
83
|
+
const guard = checkArgLength(message);
|
|
84
|
+
if (!guard.safe) {
|
|
85
|
+
const spilled = await spillOverToTempFile(message);
|
|
86
|
+
effectivePromptFile = spilled;
|
|
87
|
+
isSpillover = true;
|
|
88
|
+
console.log(
|
|
89
|
+
chalk.dim(
|
|
90
|
+
`[gemiterm] Message is ${guard.length} UTF-16 code units, exceeding the ${guard.limit} limit. ` +
|
|
91
|
+
`Spilled to temp file '${spilled}' and loading from there.`,
|
|
92
|
+
),
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (effectivePromptFile) {
|
|
98
|
+
try {
|
|
99
|
+
message = await loadPromptFromFile(effectivePromptFile);
|
|
100
|
+
} catch (err) {
|
|
101
|
+
console.error(chalk.red(`Error: ${err instanceof Error ? err.message : String(err)}`));
|
|
102
|
+
process.exit(1);
|
|
103
|
+
}
|
|
104
|
+
if (isSpillover) {
|
|
105
|
+
try {
|
|
106
|
+
removeFile(effectivePromptFile);
|
|
107
|
+
} catch {
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
51
112
|
const mediator: Mediator = context.mediator;
|
|
52
113
|
|
|
53
114
|
if (message) {
|
|
@@ -128,9 +189,19 @@ export class ContinueCommand implements CliCommand {
|
|
|
128
189
|
private parseArgs(args: string[]): ContinueCommandOptions {
|
|
129
190
|
const options = { ...DEFAULT_OPTIONS };
|
|
130
191
|
|
|
131
|
-
for (
|
|
192
|
+
for (let i = 0; i < args.length; i++) {
|
|
193
|
+
const arg = args[i];
|
|
132
194
|
if (arg === "--help" || arg === "-h") {
|
|
133
195
|
options.help = true;
|
|
196
|
+
} else if (arg === "--prompt-file" || arg === "-f") {
|
|
197
|
+
const next = args[i + 1];
|
|
198
|
+
if (next && !next.startsWith("-")) {
|
|
199
|
+
options.promptFile = next;
|
|
200
|
+
i++;
|
|
201
|
+
} else {
|
|
202
|
+
console.error(chalk.red(`Error: --prompt-file requires a path`));
|
|
203
|
+
process.exit(1);
|
|
204
|
+
}
|
|
134
205
|
}
|
|
135
206
|
}
|
|
136
207
|
|
|
@@ -149,6 +220,9 @@ export class ContinueCommand implements CliCommand {
|
|
|
149
220
|
);
|
|
150
221
|
console.log("");
|
|
151
222
|
console.log(chalk.bold("Options:"));
|
|
223
|
+
console.log(
|
|
224
|
+
` ${chalk.cyan("--prompt-file, -f <path>".padEnd(22))}${chalk.dim("Read the message from a file (bypasses the 2048 code unit arg limit)")}`,
|
|
225
|
+
);
|
|
152
226
|
console.log(` ${chalk.cyan("--help, -h".padEnd(22))}${chalk.dim("Show this help message")}`);
|
|
153
227
|
console.log("");
|
|
154
228
|
console.log(chalk.dim("If no conversation_id is provided, the list command will be invoked."));
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import type { CliCommand, CliCommandContext } from "../command-registry.ts";
|
|
3
|
+
import { Logger } from "../../infrastructure/logger.ts";
|
|
4
|
+
import { InstallSkillsService, SkillsInstallError } from "../../services/skills-service.ts";
|
|
5
|
+
|
|
6
|
+
export class InstallSkillsCommand implements CliCommand {
|
|
7
|
+
readonly name = "install-skills";
|
|
8
|
+
readonly description = "Install GemiTerm opencode skills from expert-vision-software/opencode-gemiterm-skills";
|
|
9
|
+
|
|
10
|
+
async execute(_args: string[], _context: CliCommandContext): Promise<void> {
|
|
11
|
+
const logger = new Logger("install-skills-command");
|
|
12
|
+
logger.debug("Executing install-skills command");
|
|
13
|
+
const service = new InstallSkillsService({ logger });
|
|
14
|
+
|
|
15
|
+
console.log(chalk.dim("Installing GemiTerm opencode skills..."));
|
|
16
|
+
|
|
17
|
+
try {
|
|
18
|
+
const output = await service.install();
|
|
19
|
+
if (output) {
|
|
20
|
+
console.log(output);
|
|
21
|
+
}
|
|
22
|
+
console.log(chalk.green("Skills installed successfully."));
|
|
23
|
+
} catch (error) {
|
|
24
|
+
if (error instanceof SkillsInstallError) {
|
|
25
|
+
logger.error(error.message);
|
|
26
|
+
if (error.cause) {
|
|
27
|
+
logger.error(`Cause: ${error.cause.message}`);
|
|
28
|
+
}
|
|
29
|
+
console.error(chalk.red("Failed to install skills."));
|
|
30
|
+
process.exit(1);
|
|
31
|
+
}
|
|
32
|
+
throw error;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -8,15 +8,20 @@ import {
|
|
|
8
8
|
type StartNewChatCommandResult,
|
|
9
9
|
} from "../../core/command-handlers.ts";
|
|
10
10
|
import { runInteractiveLoop, type MessageHandlerResult } from "../utils/interactive-prompt.ts";
|
|
11
|
+
import { checkArgLength } from "../utils/long-arg-guard.ts";
|
|
12
|
+
import { loadPromptFromFile, spillOverToTempFile } from "../utils/prompt-file.ts";
|
|
13
|
+
import { removeFile } from "../../infrastructure/io.ts";
|
|
11
14
|
|
|
12
15
|
interface NewCommandOptions {
|
|
13
16
|
help: boolean;
|
|
14
17
|
profile: string | null;
|
|
18
|
+
promptFile: string | null;
|
|
15
19
|
}
|
|
16
20
|
|
|
17
21
|
const DEFAULT_OPTIONS: NewCommandOptions = {
|
|
18
22
|
help: false,
|
|
19
23
|
profile: null,
|
|
24
|
+
promptFile: null,
|
|
20
25
|
};
|
|
21
26
|
|
|
22
27
|
export class NewCommand implements CliCommand {
|
|
@@ -37,11 +42,56 @@ export class NewCommand implements CliCommand {
|
|
|
37
42
|
for (const arg of args) {
|
|
38
43
|
if (arg.startsWith("--") || arg.startsWith("-")) continue;
|
|
39
44
|
if (options.profile && arg === options.profile) continue;
|
|
45
|
+
if (options.promptFile && arg === options.promptFile) continue;
|
|
40
46
|
if (!message) {
|
|
41
47
|
message = arg;
|
|
42
48
|
}
|
|
43
49
|
}
|
|
44
50
|
|
|
51
|
+
if (options.promptFile && message) {
|
|
52
|
+
console.error(
|
|
53
|
+
chalk.red(
|
|
54
|
+
`Error: cannot use --prompt-file together with a positional message argument. ` +
|
|
55
|
+
`Use one or the other, not both.`,
|
|
56
|
+
),
|
|
57
|
+
);
|
|
58
|
+
process.exit(1);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
let effectivePromptFile: string | null = null;
|
|
62
|
+
let isSpillover = false;
|
|
63
|
+
if (options.promptFile) {
|
|
64
|
+
effectivePromptFile = options.promptFile;
|
|
65
|
+
} else if (message) {
|
|
66
|
+
const guard = checkArgLength(message);
|
|
67
|
+
if (!guard.safe) {
|
|
68
|
+
const spilled = await spillOverToTempFile(message);
|
|
69
|
+
effectivePromptFile = spilled;
|
|
70
|
+
isSpillover = true;
|
|
71
|
+
console.log(
|
|
72
|
+
chalk.dim(
|
|
73
|
+
`[gemiterm] Message is ${guard.length} UTF-16 code units, exceeding the ${guard.limit} limit. ` +
|
|
74
|
+
`Spilled to temp file '${spilled}' and loading from there.`,
|
|
75
|
+
),
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (effectivePromptFile) {
|
|
81
|
+
try {
|
|
82
|
+
message = await loadPromptFromFile(effectivePromptFile);
|
|
83
|
+
} catch (err) {
|
|
84
|
+
console.error(chalk.red(`Error: ${err instanceof Error ? err.message : String(err)}`));
|
|
85
|
+
process.exit(1);
|
|
86
|
+
}
|
|
87
|
+
if (isSpillover) {
|
|
88
|
+
try {
|
|
89
|
+
removeFile(effectivePromptFile);
|
|
90
|
+
} catch {
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
45
95
|
const mediator: Mediator = context.mediator;
|
|
46
96
|
|
|
47
97
|
if (message) {
|
|
@@ -121,6 +171,15 @@ export class NewCommand implements CliCommand {
|
|
|
121
171
|
console.error(chalk.red(`Error: --profile requires a profile name`));
|
|
122
172
|
process.exit(1);
|
|
123
173
|
}
|
|
174
|
+
} else if (arg === "--prompt-file" || arg === "-f") {
|
|
175
|
+
const next = args[i + 1];
|
|
176
|
+
if (next && !next.startsWith("-")) {
|
|
177
|
+
options.promptFile = next;
|
|
178
|
+
i++;
|
|
179
|
+
} else {
|
|
180
|
+
console.error(chalk.red(`Error: --prompt-file requires a path`));
|
|
181
|
+
process.exit(1);
|
|
182
|
+
}
|
|
124
183
|
}
|
|
125
184
|
}
|
|
126
185
|
|
|
@@ -139,6 +198,9 @@ export class NewCommand implements CliCommand {
|
|
|
139
198
|
console.log(
|
|
140
199
|
` ${chalk.cyan("--profile, -p <name>".padEnd(22))}${chalk.dim("Use a specific profile (default profile used if omitted)")}`,
|
|
141
200
|
);
|
|
201
|
+
console.log(
|
|
202
|
+
` ${chalk.cyan("--prompt-file, -f <path>".padEnd(22))}${chalk.dim("Read the message from a file (bypasses the 2048 code unit arg limit)")}`,
|
|
203
|
+
);
|
|
142
204
|
console.log(` ${chalk.cyan("--help, -h".padEnd(22))}${chalk.dim("Show this help message")}`);
|
|
143
205
|
console.log("");
|
|
144
206
|
console.log(chalk.dim("If no message is provided, an interactive chat session will start."));
|
package/src/cli/index.ts
CHANGED
|
@@ -3,13 +3,13 @@
|
|
|
3
3
|
import { CommandRegistry } from "./command-registry.ts";
|
|
4
4
|
import { Logger } from "../infrastructure/logger.ts";
|
|
5
5
|
import { Mediator } from "../core/mediator.ts";
|
|
6
|
-
import { showHelp } from "./commands/help.ts";
|
|
7
6
|
import { GeminiClientService } from "../services/gemini-client-wrapper.ts";
|
|
8
7
|
import { CookieStorageService } from "../services/cookie-storage-service.ts";
|
|
9
8
|
import { ProfileAuthManager } from "../services/profile-auth-manager.ts";
|
|
10
9
|
import { CookieStorage, ProfileManager } from "../infrastructure/storage.ts";
|
|
11
|
-
import { getDefaultProfileName, listProfiles
|
|
10
|
+
import { getDefaultProfileName, listProfiles } from "../infrastructure/config.ts";
|
|
12
11
|
import { getPackageJson } from "../infrastructure/path-utils.ts";
|
|
12
|
+
import { parseGlobalArgs, printVersion, printHelp } from "../infrastructure/cli-parser.ts";
|
|
13
13
|
import { AuthenticationError } from "../core/errors.ts";
|
|
14
14
|
import {
|
|
15
15
|
AuthenticateCommandHandler,
|
|
@@ -30,39 +30,6 @@ import {
|
|
|
30
30
|
|
|
31
31
|
const pkg = getPackageJson(import.meta.url);
|
|
32
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
33
|
async function setupMediator(mediator: Mediator): Promise<ProfileAuthManager> {
|
|
67
34
|
const logger = new Logger("mediator");
|
|
68
35
|
const cookieStorage = new CookieStorage();
|
|
@@ -159,31 +126,39 @@ async function setupMediator(mediator: Mediator): Promise<ProfileAuthManager> {
|
|
|
159
126
|
async function main(): Promise<void> {
|
|
160
127
|
const args = process.argv.slice(2);
|
|
161
128
|
const logger = new Logger("cli");
|
|
162
|
-
const { flags, remaining } = parseGlobalFlags(args);
|
|
163
129
|
|
|
164
|
-
|
|
130
|
+
let flags: ReturnType<typeof parseGlobalArgs>["flags"];
|
|
131
|
+
let subcommand: string | null;
|
|
132
|
+
let subcommandArgs: string[];
|
|
133
|
+
try {
|
|
134
|
+
({ flags, subcommand, subcommandArgs } = parseGlobalArgs(args));
|
|
135
|
+
} catch (error) {
|
|
136
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
137
|
+
console.error(message);
|
|
138
|
+
process.exit(1);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const verbose = flags.verbose || process.env.GEMITERM_VERBOSE === "true";
|
|
142
|
+
if (verbose) {
|
|
165
143
|
Logger.setVerbose(true);
|
|
166
144
|
}
|
|
167
145
|
|
|
168
146
|
if (flags.version) {
|
|
169
|
-
|
|
147
|
+
printVersion(pkg.version);
|
|
170
148
|
process.exit(0);
|
|
171
149
|
}
|
|
172
150
|
|
|
173
151
|
const registry = new CommandRegistry();
|
|
174
152
|
registry.registerAllCommands();
|
|
175
153
|
|
|
176
|
-
if (
|
|
177
|
-
|
|
154
|
+
if (!subcommand) {
|
|
155
|
+
printHelp(registry);
|
|
178
156
|
process.exit(0);
|
|
179
157
|
}
|
|
180
158
|
|
|
181
159
|
const mediator = new Mediator();
|
|
182
160
|
const profileAuthManager = await setupMediator(mediator);
|
|
183
161
|
|
|
184
|
-
const subcommand = remaining[0];
|
|
185
|
-
const subcommandArgs = remaining.slice(1);
|
|
186
|
-
|
|
187
162
|
const handler = registry.getHandler(subcommand);
|
|
188
163
|
if (!handler) {
|
|
189
164
|
const known = registry.getRegisteredNames();
|
|
@@ -198,7 +173,7 @@ async function main(): Promise<void> {
|
|
|
198
173
|
}
|
|
199
174
|
|
|
200
175
|
try {
|
|
201
|
-
await handler.execute(subcommandArgs, { verbose
|
|
176
|
+
await handler.execute(subcommandArgs, { verbose, mediator, profileAuthManager });
|
|
202
177
|
} catch (error) {
|
|
203
178
|
const message = error instanceof Error ? error.message : String(error);
|
|
204
179
|
logger.error(`Command '${subcommand}' failed: ${message}`);
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export const WINDOWS_COMMAND_LINE_ARG_LIMIT = 2048;
|
|
2
|
+
|
|
3
|
+
export type LongArgGuardResult =
|
|
4
|
+
| { safe: true; arg: string }
|
|
5
|
+
| { safe: false; arg: string; length: number; limit: number; suggestion: string };
|
|
6
|
+
|
|
7
|
+
export function checkArgLength(arg: string): LongArgGuardResult {
|
|
8
|
+
const length = utf16CodeUnitLength(arg);
|
|
9
|
+
if (length <= WINDOWS_COMMAND_LINE_ARG_LIMIT) {
|
|
10
|
+
return { safe: true, arg };
|
|
11
|
+
}
|
|
12
|
+
return {
|
|
13
|
+
safe: false,
|
|
14
|
+
arg,
|
|
15
|
+
length,
|
|
16
|
+
limit: WINDOWS_COMMAND_LINE_ARG_LIMIT,
|
|
17
|
+
suggestion:
|
|
18
|
+
`Argument is ${length} UTF-16 code units, which exceeds the ${WINDOWS_COMMAND_LINE_ARG_LIMIT} ` +
|
|
19
|
+
`code unit limit imposed by Bun's Windows process spawn path (appendWindowsArgument in ` +
|
|
20
|
+
`Bun's run_command.zig panics with "index out of bounds" when this limit is exceeded). ` +
|
|
21
|
+
`Pipe the message via stdin (e.g. \`echo "..." | gemiterm new\`) or save it to a file and ` +
|
|
22
|
+
`pass it with \`--prompt-file <path>\`.`,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function utf16CodeUnitLength(s: string): number {
|
|
27
|
+
return s.length;
|
|
28
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { IOError, readTextFile, writeTextFile } from "../../infrastructure/io.ts";
|
|
2
|
+
import { getTempFilePath } from "../../infrastructure/path-utils.ts";
|
|
3
|
+
|
|
4
|
+
export async function loadPromptFromFile(filePath: string): Promise<string> {
|
|
5
|
+
try {
|
|
6
|
+
return readTextFile(filePath);
|
|
7
|
+
} catch (err) {
|
|
8
|
+
if (err instanceof IOError) {
|
|
9
|
+
throw new Error(`Could not read prompt file '${filePath}': ${err.message}`);
|
|
10
|
+
}
|
|
11
|
+
throw err;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export async function spillOverToTempFile(content: string): Promise<string> {
|
|
16
|
+
const path = getTempFilePath("gemiterm-arg-spill", ".txt");
|
|
17
|
+
try {
|
|
18
|
+
writeTextFile(path, content);
|
|
19
|
+
} catch (err) {
|
|
20
|
+
if (err instanceof IOError) {
|
|
21
|
+
throw new Error(`Could not spill message to temp file: ${err.message}`);
|
|
22
|
+
}
|
|
23
|
+
throw err;
|
|
24
|
+
}
|
|
25
|
+
return path;
|
|
26
|
+
}
|