@oh-my-pi/pi-coding-agent 16.1.5 → 16.1.7
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/CHANGELOG.md +13 -0
- package/dist/cli.js +2457 -2438
- package/dist/types/config/models-config-schema.d.ts +10 -0
- package/dist/types/config/models-config.d.ts +6 -0
- package/dist/types/modes/interactive-mode.d.ts +1 -1
- package/dist/types/modes/loop-limit.d.ts +14 -1
- package/dist/types/modes/types.d.ts +1 -1
- package/package.json +12 -12
- package/src/config/models-config-schema.ts +1 -0
- package/src/modes/interactive-mode.ts +15 -9
- package/src/modes/loop-limit.ts +80 -28
- package/src/modes/types.ts +1 -1
- package/src/prompts/system/system-prompt.md +166 -147
- package/src/prompts/tools/task.md +31 -31
- package/src/slash-commands/builtin-registry.ts +5 -2
|
@@ -44,6 +44,7 @@ export declare const OpenAICompatSchema: import("arktype/internal/variants/objec
|
|
|
44
44
|
supportsReasoningParams?: boolean | undefined;
|
|
45
45
|
alwaysSendMaxTokens?: boolean | undefined;
|
|
46
46
|
strictResponsesPairing?: boolean | undefined;
|
|
47
|
+
supportsImageDetailOriginal?: boolean | undefined;
|
|
47
48
|
requiresToolResultId?: boolean | undefined;
|
|
48
49
|
replayUnsignedThinking?: boolean | undefined;
|
|
49
50
|
whenThinking?: {
|
|
@@ -92,6 +93,7 @@ export declare const OpenAICompatSchema: import("arktype/internal/variants/objec
|
|
|
92
93
|
supportsReasoningParams?: boolean | undefined;
|
|
93
94
|
alwaysSendMaxTokens?: boolean | undefined;
|
|
94
95
|
strictResponsesPairing?: boolean | undefined;
|
|
96
|
+
supportsImageDetailOriginal?: boolean | undefined;
|
|
95
97
|
requiresToolResultId?: boolean | undefined;
|
|
96
98
|
replayUnsignedThinking?: boolean | undefined;
|
|
97
99
|
} | undefined;
|
|
@@ -182,6 +184,7 @@ export declare const ModelOverrideSchema: import("arktype/internal/variants/obje
|
|
|
182
184
|
supportsReasoningParams?: boolean | undefined;
|
|
183
185
|
alwaysSendMaxTokens?: boolean | undefined;
|
|
184
186
|
strictResponsesPairing?: boolean | undefined;
|
|
187
|
+
supportsImageDetailOriginal?: boolean | undefined;
|
|
185
188
|
requiresToolResultId?: boolean | undefined;
|
|
186
189
|
replayUnsignedThinking?: boolean | undefined;
|
|
187
190
|
whenThinking?: {
|
|
@@ -230,6 +233,7 @@ export declare const ModelOverrideSchema: import("arktype/internal/variants/obje
|
|
|
230
233
|
supportsReasoningParams?: boolean | undefined;
|
|
231
234
|
alwaysSendMaxTokens?: boolean | undefined;
|
|
232
235
|
strictResponsesPairing?: boolean | undefined;
|
|
236
|
+
supportsImageDetailOriginal?: boolean | undefined;
|
|
233
237
|
requiresToolResultId?: boolean | undefined;
|
|
234
238
|
replayUnsignedThinking?: boolean | undefined;
|
|
235
239
|
} | undefined;
|
|
@@ -298,6 +302,7 @@ export declare const ModelsConfigSchema: import("arktype/internal/variants/objec
|
|
|
298
302
|
supportsReasoningParams?: boolean | undefined;
|
|
299
303
|
alwaysSendMaxTokens?: boolean | undefined;
|
|
300
304
|
strictResponsesPairing?: boolean | undefined;
|
|
305
|
+
supportsImageDetailOriginal?: boolean | undefined;
|
|
301
306
|
requiresToolResultId?: boolean | undefined;
|
|
302
307
|
replayUnsignedThinking?: boolean | undefined;
|
|
303
308
|
whenThinking?: {
|
|
@@ -346,6 +351,7 @@ export declare const ModelsConfigSchema: import("arktype/internal/variants/objec
|
|
|
346
351
|
supportsReasoningParams?: boolean | undefined;
|
|
347
352
|
alwaysSendMaxTokens?: boolean | undefined;
|
|
348
353
|
strictResponsesPairing?: boolean | undefined;
|
|
354
|
+
supportsImageDetailOriginal?: boolean | undefined;
|
|
349
355
|
requiresToolResultId?: boolean | undefined;
|
|
350
356
|
replayUnsignedThinking?: boolean | undefined;
|
|
351
357
|
} | undefined;
|
|
@@ -444,6 +450,7 @@ export declare const ModelsConfigSchema: import("arktype/internal/variants/objec
|
|
|
444
450
|
supportsReasoningParams?: boolean | undefined;
|
|
445
451
|
alwaysSendMaxTokens?: boolean | undefined;
|
|
446
452
|
strictResponsesPairing?: boolean | undefined;
|
|
453
|
+
supportsImageDetailOriginal?: boolean | undefined;
|
|
447
454
|
requiresToolResultId?: boolean | undefined;
|
|
448
455
|
replayUnsignedThinking?: boolean | undefined;
|
|
449
456
|
whenThinking?: {
|
|
@@ -492,6 +499,7 @@ export declare const ModelsConfigSchema: import("arktype/internal/variants/objec
|
|
|
492
499
|
supportsReasoningParams?: boolean | undefined;
|
|
493
500
|
alwaysSendMaxTokens?: boolean | undefined;
|
|
494
501
|
strictResponsesPairing?: boolean | undefined;
|
|
502
|
+
supportsImageDetailOriginal?: boolean | undefined;
|
|
495
503
|
requiresToolResultId?: boolean | undefined;
|
|
496
504
|
replayUnsignedThinking?: boolean | undefined;
|
|
497
505
|
} | undefined;
|
|
@@ -585,6 +593,7 @@ export declare const ModelsConfigSchema: import("arktype/internal/variants/objec
|
|
|
585
593
|
supportsReasoningParams?: boolean | undefined;
|
|
586
594
|
alwaysSendMaxTokens?: boolean | undefined;
|
|
587
595
|
strictResponsesPairing?: boolean | undefined;
|
|
596
|
+
supportsImageDetailOriginal?: boolean | undefined;
|
|
588
597
|
requiresToolResultId?: boolean | undefined;
|
|
589
598
|
replayUnsignedThinking?: boolean | undefined;
|
|
590
599
|
whenThinking?: {
|
|
@@ -633,6 +642,7 @@ export declare const ModelsConfigSchema: import("arktype/internal/variants/objec
|
|
|
633
642
|
supportsReasoningParams?: boolean | undefined;
|
|
634
643
|
alwaysSendMaxTokens?: boolean | undefined;
|
|
635
644
|
strictResponsesPairing?: boolean | undefined;
|
|
645
|
+
supportsImageDetailOriginal?: boolean | undefined;
|
|
636
646
|
requiresToolResultId?: boolean | undefined;
|
|
637
647
|
replayUnsignedThinking?: boolean | undefined;
|
|
638
648
|
} | undefined;
|
|
@@ -81,6 +81,7 @@ export declare const ModelsConfigFile: ConfigFile<{
|
|
|
81
81
|
supportsReasoningParams?: boolean | undefined;
|
|
82
82
|
alwaysSendMaxTokens?: boolean | undefined;
|
|
83
83
|
strictResponsesPairing?: boolean | undefined;
|
|
84
|
+
supportsImageDetailOriginal?: boolean | undefined;
|
|
84
85
|
requiresToolResultId?: boolean | undefined;
|
|
85
86
|
replayUnsignedThinking?: boolean | undefined;
|
|
86
87
|
whenThinking?: {
|
|
@@ -129,6 +130,7 @@ export declare const ModelsConfigFile: ConfigFile<{
|
|
|
129
130
|
supportsReasoningParams?: boolean | undefined;
|
|
130
131
|
alwaysSendMaxTokens?: boolean | undefined;
|
|
131
132
|
strictResponsesPairing?: boolean | undefined;
|
|
133
|
+
supportsImageDetailOriginal?: boolean | undefined;
|
|
132
134
|
requiresToolResultId?: boolean | undefined;
|
|
133
135
|
replayUnsignedThinking?: boolean | undefined;
|
|
134
136
|
} | undefined;
|
|
@@ -212,6 +214,7 @@ export declare const ModelsConfigFile: ConfigFile<{
|
|
|
212
214
|
supportsReasoningParams?: boolean | undefined;
|
|
213
215
|
alwaysSendMaxTokens?: boolean | undefined;
|
|
214
216
|
strictResponsesPairing?: boolean | undefined;
|
|
217
|
+
supportsImageDetailOriginal?: boolean | undefined;
|
|
215
218
|
requiresToolResultId?: boolean | undefined;
|
|
216
219
|
replayUnsignedThinking?: boolean | undefined;
|
|
217
220
|
whenThinking?: {
|
|
@@ -260,6 +263,7 @@ export declare const ModelsConfigFile: ConfigFile<{
|
|
|
260
263
|
supportsReasoningParams?: boolean | undefined;
|
|
261
264
|
alwaysSendMaxTokens?: boolean | undefined;
|
|
262
265
|
strictResponsesPairing?: boolean | undefined;
|
|
266
|
+
supportsImageDetailOriginal?: boolean | undefined;
|
|
263
267
|
requiresToolResultId?: boolean | undefined;
|
|
264
268
|
replayUnsignedThinking?: boolean | undefined;
|
|
265
269
|
} | undefined;
|
|
@@ -338,6 +342,7 @@ export declare const ModelsConfigFile: ConfigFile<{
|
|
|
338
342
|
supportsReasoningParams?: boolean | undefined;
|
|
339
343
|
alwaysSendMaxTokens?: boolean | undefined;
|
|
340
344
|
strictResponsesPairing?: boolean | undefined;
|
|
345
|
+
supportsImageDetailOriginal?: boolean | undefined;
|
|
341
346
|
requiresToolResultId?: boolean | undefined;
|
|
342
347
|
replayUnsignedThinking?: boolean | undefined;
|
|
343
348
|
whenThinking?: {
|
|
@@ -386,6 +391,7 @@ export declare const ModelsConfigFile: ConfigFile<{
|
|
|
386
391
|
supportsReasoningParams?: boolean | undefined;
|
|
387
392
|
alwaysSendMaxTokens?: boolean | undefined;
|
|
388
393
|
strictResponsesPairing?: boolean | undefined;
|
|
394
|
+
supportsImageDetailOriginal?: boolean | undefined;
|
|
389
395
|
requiresToolResultId?: boolean | undefined;
|
|
390
396
|
replayUnsignedThinking?: boolean | undefined;
|
|
391
397
|
} | undefined;
|
|
@@ -177,7 +177,7 @@ export declare class InteractiveMode implements InteractiveModeContext {
|
|
|
177
177
|
* user submits becomes the new loop prompt and resumes iteration.
|
|
178
178
|
*/
|
|
179
179
|
pauseLoop(): void;
|
|
180
|
-
handleLoopCommand(args?: string): Promise<
|
|
180
|
+
handleLoopCommand(args?: string): Promise<string | undefined>;
|
|
181
181
|
recordLocalSubmission(text: string, imageCount?: number): () => void;
|
|
182
182
|
withLocalSubmission<T>(text: string, fn: () => Promise<T>, options?: {
|
|
183
183
|
imageCount?: number;
|
|
@@ -14,7 +14,20 @@ export type LoopLimitRuntime = {
|
|
|
14
14
|
durationMs: number;
|
|
15
15
|
deadlineMs: number;
|
|
16
16
|
};
|
|
17
|
-
export
|
|
17
|
+
export interface ParsedLoopArgs {
|
|
18
|
+
/** Iteration/duration budget, when the user supplied a leading limit token. */
|
|
19
|
+
limit?: LoopLimitConfig;
|
|
20
|
+
/** Inline loop prompt: text after the limit, or the whole argument when no limit was given. */
|
|
21
|
+
prompt?: string;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Parse `/loop` arguments into an optional leading limit plus an optional inline
|
|
25
|
+
* prompt. A token that *looks* like a limit (starts with a digit or sign) but
|
|
26
|
+
* fails to parse is a hard error; anything else is treated as prompt text, so
|
|
27
|
+
* plain prose after `/loop` keeps starting an unbounded loop instead of erroring
|
|
28
|
+
* (the pre-arg-parsing behavior). Returns the error message string on failure.
|
|
29
|
+
*/
|
|
30
|
+
export declare function parseLoopLimitArgs(args: string): ParsedLoopArgs | string;
|
|
18
31
|
export declare function createLoopLimitRuntime(config: LoopLimitConfig | undefined, nowMs?: number): LoopLimitRuntime | undefined;
|
|
19
32
|
export declare function consumeLoopLimitIteration(limit: LoopLimitRuntime | undefined, nowMs?: number): boolean;
|
|
20
33
|
export declare function isLoopDurationExpired(limit: LoopLimitRuntime | undefined, nowMs?: number): boolean;
|
|
@@ -339,7 +339,7 @@ export interface InteractiveModeContext {
|
|
|
339
339
|
handlePlanModeCommand(initialPrompt?: string): Promise<void>;
|
|
340
340
|
handleGoalModeCommand(rest?: string): Promise<void>;
|
|
341
341
|
handleGuidedGoalCommand(rest?: string): Promise<void>;
|
|
342
|
-
handleLoopCommand(args?: string): Promise<
|
|
342
|
+
handleLoopCommand(args?: string): Promise<string | undefined>;
|
|
343
343
|
disableLoopMode(): void;
|
|
344
344
|
pauseLoop(): void;
|
|
345
345
|
handlePlanApproval(details: PlanApprovalDetails): Promise<void>;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@oh-my-pi/pi-coding-agent",
|
|
4
|
-
"version": "16.1.
|
|
4
|
+
"version": "16.1.7",
|
|
5
5
|
"description": "Coding agent CLI with read, bash, edit, write tools and session management",
|
|
6
6
|
"homepage": "https://omp.sh",
|
|
7
7
|
"author": "Can Boluk",
|
|
@@ -48,17 +48,17 @@
|
|
|
48
48
|
"@agentclientprotocol/sdk": "0.25.0",
|
|
49
49
|
"@babel/parser": "^7.29.7",
|
|
50
50
|
"@mozilla/readability": "^0.6.0",
|
|
51
|
-
"@oh-my-pi/hashline": "16.1.
|
|
52
|
-
"@oh-my-pi/omp-stats": "16.1.
|
|
53
|
-
"@oh-my-pi/pi-agent-core": "16.1.
|
|
54
|
-
"@oh-my-pi/pi-ai": "16.1.
|
|
55
|
-
"@oh-my-pi/pi-catalog": "16.1.
|
|
56
|
-
"@oh-my-pi/pi-mnemopi": "16.1.
|
|
57
|
-
"@oh-my-pi/pi-natives": "16.1.
|
|
58
|
-
"@oh-my-pi/pi-tui": "16.1.
|
|
59
|
-
"@oh-my-pi/pi-utils": "16.1.
|
|
60
|
-
"@oh-my-pi/pi-wire": "16.1.
|
|
61
|
-
"@oh-my-pi/snapcompact": "16.1.
|
|
51
|
+
"@oh-my-pi/hashline": "16.1.7",
|
|
52
|
+
"@oh-my-pi/omp-stats": "16.1.7",
|
|
53
|
+
"@oh-my-pi/pi-agent-core": "16.1.7",
|
|
54
|
+
"@oh-my-pi/pi-ai": "16.1.7",
|
|
55
|
+
"@oh-my-pi/pi-catalog": "16.1.7",
|
|
56
|
+
"@oh-my-pi/pi-mnemopi": "16.1.7",
|
|
57
|
+
"@oh-my-pi/pi-natives": "16.1.7",
|
|
58
|
+
"@oh-my-pi/pi-tui": "16.1.7",
|
|
59
|
+
"@oh-my-pi/pi-utils": "16.1.7",
|
|
60
|
+
"@oh-my-pi/pi-wire": "16.1.7",
|
|
61
|
+
"@oh-my-pi/snapcompact": "16.1.7",
|
|
62
62
|
"@opentelemetry/api": "^1.9.1",
|
|
63
63
|
"@opentelemetry/context-async-hooks": "^2.7.1",
|
|
64
64
|
"@opentelemetry/exporter-trace-otlp-proto": "^0.218.0",
|
|
@@ -57,6 +57,7 @@ const OpenAICompatFields = {
|
|
|
57
57
|
"supportsReasoningParams?": "boolean",
|
|
58
58
|
"alwaysSendMaxTokens?": "boolean",
|
|
59
59
|
"strictResponsesPairing?": "boolean",
|
|
60
|
+
"supportsImageDetailOriginal?": "boolean",
|
|
60
61
|
// anthropic-messages compat flags (same `compat` slot, per-api interpretation)
|
|
61
62
|
"requiresToolResultId?": "boolean",
|
|
62
63
|
"replayUnsignedThinking?": "boolean",
|
|
@@ -1128,27 +1128,33 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
1128
1128
|
this.#cancelLoopAutoSubmit();
|
|
1129
1129
|
}
|
|
1130
1130
|
|
|
1131
|
-
async handleLoopCommand(args = ""): Promise<
|
|
1131
|
+
async handleLoopCommand(args = ""): Promise<string | undefined> {
|
|
1132
1132
|
if (this.loopModeEnabled) {
|
|
1133
1133
|
this.disableLoopMode();
|
|
1134
|
-
return;
|
|
1134
|
+
return undefined;
|
|
1135
1135
|
}
|
|
1136
|
-
const
|
|
1137
|
-
if (typeof
|
|
1138
|
-
this.showError(
|
|
1139
|
-
return;
|
|
1136
|
+
const parsed = parseLoopLimitArgs(args);
|
|
1137
|
+
if (typeof parsed === "string") {
|
|
1138
|
+
this.showError(parsed);
|
|
1139
|
+
return undefined;
|
|
1140
1140
|
}
|
|
1141
1141
|
this.loopModeEnabled = true;
|
|
1142
1142
|
this.loopPrompt = undefined;
|
|
1143
|
-
this.loopLimit = createLoopLimitRuntime(
|
|
1143
|
+
this.loopLimit = createLoopLimitRuntime(parsed.limit);
|
|
1144
1144
|
this.statusLine.setLoopModeStatus({ enabled: true });
|
|
1145
1145
|
this.updateEditorTopBorder();
|
|
1146
1146
|
this.ui.requestRender();
|
|
1147
|
-
const limitSuffix =
|
|
1147
|
+
const limitSuffix = parsed.limit ? ` Limited to ${describeLoopLimit(parsed.limit)}.` : "";
|
|
1148
1148
|
const remainingSuffix = this.loopLimit ? ` ${describeLoopLimitRuntime(this.loopLimit)}.` : "";
|
|
1149
|
+
const tail = parsed.prompt ? "Repeating it after each turn." : "Your next prompt will repeat after each turn.";
|
|
1149
1150
|
this.showStatus(
|
|
1150
|
-
`Loop mode enabled.${limitSuffix}${remainingSuffix}
|
|
1151
|
+
`Loop mode enabled.${limitSuffix}${remainingSuffix} ${tail} Esc cancels the current iteration; /loop again to disable.`,
|
|
1151
1152
|
);
|
|
1153
|
+
// Hand any inline prompt back to the dispatcher so the normal submit flow
|
|
1154
|
+
// runs the first iteration — it records the text as the loop prompt and
|
|
1155
|
+
// auto-resubmits it after each yield, identical to typing the prompt right
|
|
1156
|
+
// after enabling loop mode.
|
|
1157
|
+
return parsed.prompt;
|
|
1152
1158
|
}
|
|
1153
1159
|
|
|
1154
1160
|
recordLocalSubmission(text: string, imageCount = 0): () => void {
|
package/src/modes/loop-limit.ts
CHANGED
|
@@ -38,53 +38,105 @@ const TIME_UNITS_MS = new Map<string, number>([
|
|
|
38
38
|
["hours", 3_600_000],
|
|
39
39
|
]);
|
|
40
40
|
|
|
41
|
-
|
|
42
|
-
const trimmed = args.trim().toLowerCase();
|
|
43
|
-
if (!trimmed) return undefined;
|
|
41
|
+
const LOOP_USAGE = "Usage: /loop [count|duration]. Examples: /loop 10, /loop 10m, /loop 10min.";
|
|
44
42
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
43
|
+
export interface ParsedLoopArgs {
|
|
44
|
+
/** Iteration/duration budget, when the user supplied a leading limit token. */
|
|
45
|
+
limit?: LoopLimitConfig;
|
|
46
|
+
/** Inline loop prompt: text after the limit, or the whole argument when no limit was given. */
|
|
47
|
+
prompt?: string;
|
|
48
|
+
}
|
|
49
49
|
|
|
50
|
-
|
|
51
|
-
|
|
50
|
+
/**
|
|
51
|
+
* Parse `/loop` arguments into an optional leading limit plus an optional inline
|
|
52
|
+
* prompt. A token that *looks* like a limit (starts with a digit or sign) but
|
|
53
|
+
* fails to parse is a hard error; anything else is treated as prompt text, so
|
|
54
|
+
* plain prose after `/loop` keeps starting an unbounded loop instead of erroring
|
|
55
|
+
* (the pre-arg-parsing behavior). Returns the error message string on failure.
|
|
56
|
+
*/
|
|
57
|
+
export function parseLoopLimitArgs(args: string): ParsedLoopArgs | string {
|
|
58
|
+
const trimmed = args.trim();
|
|
59
|
+
if (!trimmed) return {};
|
|
60
|
+
|
|
61
|
+
const firstSpace = trimmed.search(/\s/);
|
|
62
|
+
const firstToken = firstSpace === -1 ? trimmed : trimmed.slice(0, firstSpace);
|
|
63
|
+
const rest = firstSpace === -1 ? "" : trimmed.slice(firstSpace + 1).trim();
|
|
64
|
+
const token = firstToken.toLowerCase();
|
|
65
|
+
|
|
66
|
+
// Not a limit attempt (prose like "keep going") → unbounded loop, prompt = full args.
|
|
67
|
+
if (!/^[+-]?\d/.test(token)) {
|
|
68
|
+
return { prompt: trimmed };
|
|
52
69
|
}
|
|
53
70
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
71
|
+
// Bare integer: iteration count, unless the next token is a time unit ("10 minutes").
|
|
72
|
+
if (/^\d+$/.test(token)) {
|
|
73
|
+
if (rest) {
|
|
74
|
+
const restTokens = rest.split(/\s+/);
|
|
75
|
+
const unitMs = TIME_UNITS_MS.get(restTokens[0].toLowerCase());
|
|
76
|
+
if (unitMs !== undefined) {
|
|
77
|
+
const limit = makeDuration(token, unitMs);
|
|
78
|
+
if (typeof limit === "string") return limit;
|
|
79
|
+
return { limit, prompt: restTokens.slice(1).join(" ").trim() || undefined };
|
|
80
|
+
}
|
|
60
81
|
}
|
|
61
|
-
|
|
82
|
+
const limit = makeIterations(token);
|
|
83
|
+
if (typeof limit === "string") return limit;
|
|
84
|
+
return { limit, prompt: rest || undefined };
|
|
62
85
|
}
|
|
63
86
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
87
|
+
// Compact / compound duration: "10m", "90s", "1h30m".
|
|
88
|
+
const duration = parseCompoundDuration(token);
|
|
89
|
+
if (duration !== undefined) {
|
|
90
|
+
if (typeof duration === "string") return duration;
|
|
91
|
+
return { limit: duration, prompt: rest || undefined };
|
|
67
92
|
}
|
|
68
93
|
|
|
69
|
-
|
|
94
|
+
// Limit-shaped but unparseable ("-1", "1.5h", "10x10").
|
|
95
|
+
return LOOP_USAGE;
|
|
70
96
|
}
|
|
71
97
|
|
|
72
|
-
function
|
|
73
|
-
|
|
74
|
-
|
|
98
|
+
function makeIterations(amountText: string): LoopLimitConfig | string {
|
|
99
|
+
const amount = Number(amountText);
|
|
100
|
+
if (!Number.isSafeInteger(amount) || amount <= 0) {
|
|
101
|
+
return "Loop count must be a positive integer.";
|
|
75
102
|
}
|
|
103
|
+
return { kind: "iterations", iterations: amount };
|
|
104
|
+
}
|
|
76
105
|
|
|
106
|
+
function makeDuration(amountText: string, unitMs: number): LoopLimitConfig | string {
|
|
77
107
|
const amount = Number(amountText);
|
|
78
108
|
if (!Number.isSafeInteger(amount) || amount <= 0) {
|
|
79
109
|
return "Loop duration must be positive.";
|
|
80
110
|
}
|
|
111
|
+
return { kind: "duration", durationMs: amount * unitMs };
|
|
112
|
+
}
|
|
81
113
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
114
|
+
/**
|
|
115
|
+
* Parse a compact duration token such as `10m`, or a compound one like `1h30m`.
|
|
116
|
+
* Returns `undefined` when the token is not duration-shaped, or an error string
|
|
117
|
+
* when it is shaped like a duration but uses an unknown unit / non-positive
|
|
118
|
+
* amount.
|
|
119
|
+
*/
|
|
120
|
+
function parseCompoundDuration(token: string): LoopLimitConfig | string | undefined {
|
|
121
|
+
if (!/^(?:\d+[a-z]+)+$/.test(token)) return undefined;
|
|
122
|
+
const segments = token.match(/\d+[a-z]+/g);
|
|
123
|
+
if (!segments) return undefined;
|
|
124
|
+
let totalMs = 0;
|
|
125
|
+
for (const segment of segments) {
|
|
126
|
+
const match = /^(\d+)([a-z]+)$/.exec(segment);
|
|
127
|
+
if (!match) return LOOP_USAGE;
|
|
128
|
+
const unitMs = TIME_UNITS_MS.get(match[2]);
|
|
129
|
+
if (unitMs === undefined) {
|
|
130
|
+
return "Loop duration unit must be seconds, minutes, or hours.";
|
|
131
|
+
}
|
|
132
|
+
const amount = Number(match[1]);
|
|
133
|
+
if (!Number.isSafeInteger(amount) || amount <= 0) {
|
|
134
|
+
return "Loop duration must be positive.";
|
|
135
|
+
}
|
|
136
|
+
totalMs += amount * unitMs;
|
|
85
137
|
}
|
|
86
|
-
|
|
87
|
-
return { kind: "duration", durationMs:
|
|
138
|
+
if (totalMs <= 0) return "Loop duration must be positive.";
|
|
139
|
+
return { kind: "duration", durationMs: totalMs };
|
|
88
140
|
}
|
|
89
141
|
|
|
90
142
|
export function createLoopLimitRuntime(
|
package/src/modes/types.ts
CHANGED
|
@@ -362,7 +362,7 @@ export interface InteractiveModeContext {
|
|
|
362
362
|
handlePlanModeCommand(initialPrompt?: string): Promise<void>;
|
|
363
363
|
handleGoalModeCommand(rest?: string): Promise<void>;
|
|
364
364
|
handleGuidedGoalCommand(rest?: string): Promise<void>;
|
|
365
|
-
handleLoopCommand(args?: string): Promise<
|
|
365
|
+
handleLoopCommand(args?: string): Promise<string | undefined>;
|
|
366
366
|
disableLoopMode(): void;
|
|
367
367
|
pauseLoop(): void;
|
|
368
368
|
handlePlanApproval(details: PlanApprovalDetails): Promise<void>;
|