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.
Files changed (155) hide show
  1. package/README.md +32 -9
  2. package/package.json +13 -4
  3. package/src/cli/command-registry.ts +66 -62
  4. package/src/cli/commands/continue-command.ts +75 -1
  5. package/src/cli/commands/install-skills-command.ts +35 -0
  6. package/src/cli/commands/new-command.ts +62 -0
  7. package/src/cli/index.ts +19 -44
  8. package/src/cli/utils/long-arg-guard.ts +28 -0
  9. package/src/cli/utils/prompt-file.ts +26 -0
  10. package/src/infrastructure/cli-parser.ts +77 -0
  11. package/src/infrastructure/io.ts +9 -0
  12. package/src/services/skills-cli-driver.ts +58 -0
  13. package/src/services/skills-service.ts +52 -0
  14. package/.env +0 -2
  15. package/.github/workflows/manual-release.yml +0 -34
  16. package/.github/workflows/release.yml +0 -198
  17. package/.github/workflows/test.yml +0 -29
  18. package/.opencode/commands/opsx-apply.md +0 -152
  19. package/.opencode/commands/opsx-archive.md +0 -157
  20. package/.opencode/commands/opsx-explore.md +0 -169
  21. package/.opencode/commands/opsx-propose.md +0 -104
  22. package/.opencode/commands/opsx-sync.md +0 -140
  23. package/.opencode/commands/search-intelligently.md +0 -9
  24. package/.opencode/opencode.json +0 -20
  25. package/.opencode/skills/intellisearch/.version +0 -1
  26. package/.opencode/skills/intellisearch/SKILL.md +0 -84
  27. package/.opencode/skills/intellisearch/references/brave-search.md +0 -67
  28. package/.opencode/skills/intellisearch/references/ddg-search.md +0 -79
  29. package/.opencode/skills/intellisearch/references/deepwiki-tools.md +0 -246
  30. package/.opencode/skills/intellisearch/references/examples.md +0 -97
  31. package/.opencode/skills/intellisearch/references/gh-cli.md +0 -143
  32. package/.opencode/skills/intellisearch/references/google-search.md +0 -40
  33. package/.opencode/skills/intellisearch/references/search-workflow.md +0 -143
  34. package/.opencode/skills/intellisearch/references/workflow.md +0 -115
  35. package/.opencode/skills/openspec-apply-change/SKILL.md +0 -159
  36. package/.opencode/skills/openspec-archive-change/SKILL.md +0 -117
  37. package/.opencode/skills/openspec-explore/SKILL.md +0 -287
  38. package/.opencode/skills/openspec-propose/SKILL.md +0 -111
  39. package/.opencode/skills/openspec-sync-specs/SKILL.md +0 -147
  40. package/.serena/project.yml +0 -133
  41. package/AGENTS.md +0 -136
  42. package/CHANGELOG.md +0 -3
  43. package/bun.lock +0 -105
  44. package/docs/INSTALL.md +0 -185
  45. package/docs/PLAYWRIGHT_CLI_API.md +0 -306
  46. package/docs/python-release-for-reference.md +0 -175
  47. package/examples/sample_json_export.json +0 -32
  48. package/examples/sample_markdown_export.md +0 -221
  49. package/examples/usage_examples.sh +0 -222
  50. package/install.ps1 +0 -264
  51. package/install.sh +0 -275
  52. package/openspec/MAESTRO_MIGRATION.md +0 -180
  53. package/openspec/changes/archive/2026-06-08-command-spec-conformance/.openspec.yaml +0 -2
  54. package/openspec/changes/archive/2026-06-08-command-spec-conformance/design.md +0 -194
  55. package/openspec/changes/archive/2026-06-08-command-spec-conformance/proposal.md +0 -52
  56. package/openspec/changes/archive/2026-06-08-command-spec-conformance/specs/multi-profile-conversations/spec.md +0 -133
  57. package/openspec/changes/archive/2026-06-08-command-spec-conformance/tasks.md +0 -95
  58. package/openspec/changes/archive/2026-06-09-refactor-interactive-prompt-loop/.openspec.yaml +0 -2
  59. package/openspec/changes/archive/2026-06-09-refactor-interactive-prompt-loop/design.md +0 -28
  60. package/openspec/changes/archive/2026-06-09-refactor-interactive-prompt-loop/proposal.md +0 -22
  61. package/openspec/changes/archive/2026-06-09-refactor-interactive-prompt-loop/specs/interactive-prompt-loop/spec.md +0 -35
  62. package/openspec/changes/archive/2026-06-09-refactor-interactive-prompt-loop/tasks.md +0 -28
  63. package/openspec/changes/archive/2026-06-09-replace-gemini-api-placeholder-with-gemini-reverse/.openspec.yaml +0 -2
  64. package/openspec/changes/archive/2026-06-09-replace-gemini-api-placeholder-with-gemini-reverse/design.md +0 -323
  65. package/openspec/changes/archive/2026-06-09-replace-gemini-api-placeholder-with-gemini-reverse/proposal.md +0 -122
  66. package/openspec/changes/archive/2026-06-09-replace-gemini-api-placeholder-with-gemini-reverse/specs/no-capability-changes/spec.md +0 -29
  67. package/openspec/changes/archive/2026-06-09-replace-gemini-api-placeholder-with-gemini-reverse/tasks.md +0 -172
  68. package/openspec/changes/archive/2026-06-09-v2-install-migration/.openspec.yaml +0 -2
  69. package/openspec/changes/archive/2026-06-09-v2-install-migration/design.md +0 -241
  70. package/openspec/changes/archive/2026-06-09-v2-install-migration/proposal.md +0 -41
  71. package/openspec/changes/archive/2026-06-09-v2-install-migration/specs/v2-installer/spec.md +0 -205
  72. package/openspec/changes/archive/2026-06-09-v2-install-migration/tasks.md +0 -97
  73. package/openspec/changes/commander-cli-parser/.openspec.yaml +0 -2
  74. package/openspec/changes/commander-cli-parser/design.md +0 -107
  75. package/openspec/changes/commander-cli-parser/proposal.md +0 -31
  76. package/openspec/changes/commander-cli-parser/specs/cli/spec.md +0 -48
  77. package/openspec/changes/commander-cli-parser/tasks.md +0 -33
  78. package/openspec/changes/cross-platform-build-and-ci/.openspec.yaml +0 -2
  79. package/openspec/changes/cross-platform-build-and-ci/design.md +0 -213
  80. package/openspec/changes/cross-platform-build-and-ci/proposal.md +0 -50
  81. package/openspec/changes/cross-platform-build-and-ci/specs/path-and-file-mediation/spec.md +0 -335
  82. package/openspec/changes/cross-platform-build-and-ci/specs/release-pipeline/spec.md +0 -95
  83. package/openspec/changes/cross-platform-build-and-ci/tasks.md +0 -164
  84. package/openspec/config.yaml +0 -20
  85. package/openspec/specs/auth/spec.md +0 -394
  86. package/openspec/specs/cli/spec.md +0 -108
  87. package/openspec/specs/commands/spec.md +0 -409
  88. package/openspec/specs/configuration/spec.md +0 -130
  89. package/openspec/specs/conversations/spec.md +0 -139
  90. package/openspec/specs/domain-model/spec.md +0 -128
  91. package/openspec/specs/formatters/spec.md +0 -158
  92. package/openspec/specs/install-browser/spec.md +0 -146
  93. package/openspec/specs/interactive-prompt-loop/spec.md +0 -41
  94. package/openspec/specs/logger/spec.md +0 -71
  95. package/openspec/specs/mediator/spec.md +0 -72
  96. package/openspec/specs/multi-profile-conversations/spec.md +0 -145
  97. package/openspec/specs/path-utils/spec.md +0 -101
  98. package/openspec/specs/profiles/spec.md +0 -94
  99. package/openspec/specs/storage/spec.md +0 -198
  100. package/openspec/specs/testing/spec.md +0 -237
  101. package/openspec/specs/v2-installer/spec.md +0 -205
  102. package/openspec/specs/validators/spec.md +0 -65
  103. package/scripts/build.ts +0 -88
  104. package/scripts/clean-build.ps1 +0 -4
  105. package/scripts/clean-build.sh +0 -5
  106. package/scripts/install-browser.ps1 +0 -5
  107. package/scripts/install-browser.sh +0 -6
  108. package/scripts/lint-path-mediation.ps1 +0 -65
  109. package/scripts/lint-path-mediation.sh +0 -40
  110. package/tests/cli/auth-command.test.ts +0 -220
  111. package/tests/cli/command-registry.test.ts +0 -67
  112. package/tests/cli/continue-command.test.ts +0 -100
  113. package/tests/cli/delete-command.test.ts +0 -128
  114. package/tests/cli/export-all-command.test.ts +0 -375
  115. package/tests/cli/install-browser-command.test.ts +0 -48
  116. package/tests/cli/list-command.test.ts +0 -179
  117. package/tests/cli/new-command.test.ts +0 -139
  118. package/tests/cli/status-command.test.ts +0 -106
  119. package/tests/core/query-handlers.test.ts +0 -187
  120. package/tests/fixtures/auth-fixtures.ts +0 -98
  121. package/tests/fixtures/chat-fixtures.ts +0 -87
  122. package/tests/infrastructure/cli-table.test.ts +0 -107
  123. package/tests/infrastructure/formatters.test.ts +0 -202
  124. package/tests/infrastructure/io.test.ts +0 -197
  125. package/tests/infrastructure/logger.test.ts +0 -74
  126. package/tests/infrastructure/storage.test.ts +0 -335
  127. package/tests/infrastructure/validators.test.ts +0 -67
  128. package/tests/integration/commands/auth.test.ts +0 -186
  129. package/tests/integration/commands/continue.test.ts +0 -130
  130. package/tests/integration/commands/delete.test.ts +0 -140
  131. package/tests/integration/commands/export.test.ts +0 -266
  132. package/tests/integration/commands/fetch.test.ts +0 -238
  133. package/tests/integration/commands/list.test.ts +0 -281
  134. package/tests/integration/commands/profile.test.ts +0 -258
  135. package/tests/integration/commands/status.test.ts +0 -223
  136. package/tests/integration/installer-script-shape.test.ts +0 -67
  137. package/tests/parity/compare-outputs.ts +0 -359
  138. package/tests/parity/test-commands-parity.ps1 +0 -82
  139. package/tests/parity/test-commands-parity.sh +0 -61
  140. package/tests/services/auth-service.test.ts +0 -315
  141. package/tests/services/cookie-monitor.test.ts +0 -229
  142. package/tests/services/cookie-storage-service.test.ts +0 -230
  143. package/tests/services/gemini-client-wrapper.test.ts +0 -688
  144. package/tests/services/install-browser-service.test.ts +0 -83
  145. package/tests/services/playwright-cli-driver.test.ts +0 -468
  146. package/tests/services/profile-auth-manager.test.ts +0 -334
  147. package/tests/services/profile-service.test.ts +0 -224
  148. package/tests/setup.ts +0 -95
  149. package/tests/smoke/smoke.test.ts +0 -52
  150. package/tests/unit/config.test.ts +0 -159
  151. package/tests/unit/errors.test.ts +0 -119
  152. package/tests/unit/formatters.test.ts +0 -316
  153. package/tests/unit/mediator.test.ts +0 -152
  154. package/tests/unit/path-utils.test.ts +0 -289
  155. 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
- See [docs/INSTALL.md](docs/INSTALL.md) for the full guide, uninstall instructions, troubleshooting, and build-from-source steps.
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
- > **Upgrading from v1.4.1?** Your profiles, cookies, and default profile marker are preserved. The installer replaces the binary in place; it does NOT touch your config dir. See the "Upgrade from v1.4.1" section in [docs/INSTALL.md](docs/INSTALL.md) for details.
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
- ```bash
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.0.0",
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
- "devDependencies": {
18
- "@types/bun": "^1.3.14"
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
- "peerDependencies": {
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
- export interface CliCommandContext {
16
- verbose: boolean;
17
- mediator: Mediator;
18
- profileAuthManager: ProfileAuthManager;
19
- }
20
-
21
- export interface CliCommand {
22
- readonly name: string;
23
- readonly description: string;
24
- execute(args: string[], context: CliCommandContext): Promise<void>;
25
- }
26
-
27
- export class CommandRegistry {
28
- private handlers = new Map<string, CliCommand>();
29
-
30
- register(commandName: string, handler: CliCommand): void {
31
- if (this.handlers.has(commandName)) {
32
- throw new Error(`Command already registered: ${commandName}`);
33
- }
34
- this.handlers.set(commandName, handler);
35
- }
36
-
37
- getHandler(commandName: string): CliCommand | undefined {
38
- return this.handlers.get(commandName);
39
- }
40
-
41
- has(commandName: string): boolean {
42
- return this.handlers.has(commandName);
43
- }
44
-
45
- getRegisteredNames(): string[] {
46
- return [...this.handlers.keys()];
47
- }
48
-
49
- registerAllCommands(): void {
50
- this.register("auth", new AuthCommand());
51
- this.register("profile", new ProfileCommand());
52
- this.register("status", new StatusCommand());
53
- this.register("list", new ListCommand());
54
- this.register("fetch", new FetchCommand());
55
- this.register("continue", new ContinueCommand());
56
- this.register("new", new NewCommand());
57
- this.register("delete", new DeleteCommand());
58
- this.register("export", new ExportCommand());
59
- this.register("export-all", new ExportAllCommand());
60
- this.register("install-browser", new InstallBrowserCommand());
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 (const arg of args) {
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, ensureConfigDir } from "../infrastructure/config.ts";
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
- if (flags.verbose) {
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
- console.log(`gemiterm v${pkg.version}`);
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 (remaining.length === 0) {
177
- showHelp(registry);
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: flags.verbose, mediator, profileAuthManager });
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
+ }