gemiterm 2.1.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gemiterm",
3
- "version": "2.1.0",
3
+ "version": "2.1.1",
4
4
  "module": "index.ts",
5
5
  "type": "module",
6
6
  "repository": {
@@ -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."));
@@ -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."));
@@ -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
+ }
@@ -114,6 +114,14 @@ function removeDir(path: string): void {
114
114
  }
115
115
  }
116
116
 
117
+ function removeFile(path: string): void {
118
+ try {
119
+ rmSync(path, { force: true });
120
+ } catch (err) {
121
+ throw wrap("removeFile", path, err instanceof Error ? err : undefined);
122
+ }
123
+ }
124
+
117
125
  function renameDir(src: string, dest: string): void {
118
126
  try {
119
127
  renameSync(src, dest);
@@ -155,6 +163,7 @@ export {
155
163
  readJsonFile,
156
164
  writeJsonFile,
157
165
  removeDir,
166
+ removeFile,
158
167
  renameDir,
159
168
  isDirectory,
160
169
  listSubdirectories,