pi-ultracode 0.1.0 → 0.1.2

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.
@@ -14,7 +14,7 @@
14
14
  import * as fs from "node:fs";
15
15
  import * as os from "node:os";
16
16
  import * as path from "node:path";
17
- import type { ThinkingLevel } from "./agent-runner.ts";
17
+ import { isThinkingLevel, type ThinkingLevel } from "../thinking.ts";
18
18
 
19
19
  export interface AgentTypeDef {
20
20
  name: string;
@@ -76,8 +76,6 @@ const BUILTIN_AGENT_TYPES: AgentTypeDef[] = [
76
76
  },
77
77
  ];
78
78
 
79
- const VALID_THINKING = new Set(["off", "minimal", "low", "medium", "high", "xhigh"]);
80
-
81
79
  export function discoverAgentTypes(cwd: string): Map<string, AgentTypeDef> {
82
80
  const map = new Map<string, AgentTypeDef>();
83
81
  for (const def of BUILTIN_AGENT_TYPES) map.set(def.name, def);
@@ -138,9 +136,8 @@ export function parseAgentTypeFile(
138
136
  .map((t) => t.trim())
139
137
  .filter(Boolean)
140
138
  : undefined;
141
- const thinking = frontmatter.thinking && VALID_THINKING.has(frontmatter.thinking.trim())
142
- ? (frontmatter.thinking.trim() as ThinkingLevel)
143
- : undefined;
139
+ const rawThinking = frontmatter.thinking?.trim();
140
+ const thinking = rawThinking && isThinkingLevel(rawThinking) ? rawThinking : undefined;
144
141
  const systemPromptMode = frontmatter.systemPromptMode === "replace" ? "replace" : "append";
145
142
  const systemPrompt = (frontmatter.systemPrompt ?? body ?? "").trim();
146
143
  return {
@@ -0,0 +1,461 @@
1
+ /** Terminal-safe, bounded text helpers for workflow progress UI. */
2
+
3
+ const ESC = 0x1b;
4
+ const BEL = 0x07;
5
+
6
+ /** Hard ceiling before any terminal scanning or redaction work. */
7
+ export const DISPLAY_INPUT_LIMIT = 16_384;
8
+ export const DISPLAY_OMITTED_TEXT = "[output omitted]";
9
+
10
+ function isInvisibleFormat(code: number): boolean {
11
+ return code === 0x00ad
12
+ || code === 0x034f
13
+ || (code >= 0x0600 && code <= 0x0605)
14
+ || code === 0x061c
15
+ || code === 0x06dd
16
+ || code === 0x070f
17
+ || (code >= 0x0890 && code <= 0x0891)
18
+ || code === 0x08e2
19
+ || (code >= 0x17b4 && code <= 0x17b5)
20
+ || (code >= 0x180b && code <= 0x180f)
21
+ || (code >= 0x200b && code <= 0x200f)
22
+ || (code >= 0x202a && code <= 0x202e)
23
+ || (code >= 0x2060 && code <= 0x206f)
24
+ || (code >= 0xfe00 && code <= 0xfe0f)
25
+ || code === 0xfeff
26
+ || (code >= 0xfff9 && code <= 0xfffb)
27
+ || code === 0x110bd
28
+ || code === 0x110cd
29
+ || (code >= 0x13430 && code <= 0x1345f)
30
+ || (code >= 0x1bca0 && code <= 0x1bca3)
31
+ || (code >= 0x1d173 && code <= 0x1d17a)
32
+ || code === 0xe0001
33
+ || (code >= 0xe0020 && code <= 0xe007f)
34
+ || (code >= 0xe0100 && code <= 0xe01ef);
35
+ }
36
+
37
+ const SAFE_SUBCOMMANDS = new Map<string, ReadonlySet<string>>([
38
+ ["npm", new Set(["test", "install", "ci", "run", "exec"])],
39
+ ["pnpm", new Set(["test", "install", "run", "exec"])],
40
+ ["yarn", new Set(["test", "install", "run"])],
41
+ ["bun", new Set(["test", "install", "run"])],
42
+ ["git", new Set(["status", "diff", "log", "show", "branch", "grep"])],
43
+ ["cargo", new Set(["test", "check", "build", "clippy"])],
44
+ ["go", new Set(["test", "build", "vet"])],
45
+ ]);
46
+ const SAFE_EXECUTABLES = new Set([
47
+ ...SAFE_SUBCOMMANDS.keys(),
48
+ "sleep", "curl", "wget", "node", "python", "python3", "pytest", "tsc", "make", "cmake",
49
+ "rg", "grep", "find", "ls", "pwd",
50
+ ]);
51
+
52
+ /**
53
+ * Remove terminal and invisible formatting controls in one pass while preserving
54
+ * ordinary newlines/tabs. Work is capped before scanning. Unterminated OSC/DCS
55
+ * strings consume the bounded remainder, avoiding injection and backtracking.
56
+ */
57
+ export function stripTerminalControls(value: string, inputLimit = DISPLAY_INPUT_LIMIT): string {
58
+ const limit = Math.max(0, Math.floor(inputLimit));
59
+ const truncated = value.length > limit;
60
+ const source = truncated ? value.slice(0, limit) : value;
61
+ const output: string[] = [];
62
+ let index = 0;
63
+ while (index < source.length) {
64
+ const code = source.charCodeAt(index);
65
+ if (code === ESC) {
66
+ index++;
67
+ if (index >= source.length) break;
68
+ const kind = source.charCodeAt(index);
69
+ if (kind === 0x5d) {
70
+ // OSC: ESC ] ... BEL or ST (ESC \).
71
+ index++;
72
+ while (index < source.length) {
73
+ const current = source.charCodeAt(index);
74
+ if (current === BEL) {
75
+ index++;
76
+ break;
77
+ }
78
+ if (current === ESC && source.charCodeAt(index + 1) === 0x5c) {
79
+ index += 2;
80
+ break;
81
+ }
82
+ index++;
83
+ }
84
+ continue;
85
+ }
86
+ if (kind === 0x50 || kind === 0x5e || kind === 0x5f || kind === 0x58) {
87
+ // DCS/SOS/PM/APC: ESC P/^/_/X ... ST (ESC \).
88
+ index++;
89
+ while (index < source.length) {
90
+ if (source.charCodeAt(index) === ESC && source.charCodeAt(index + 1) === 0x5c) {
91
+ index += 2;
92
+ break;
93
+ }
94
+ index++;
95
+ }
96
+ continue;
97
+ }
98
+ if (kind === 0x5b) {
99
+ // CSI: ESC [ parameters/intermediates final-byte.
100
+ index++;
101
+ while (index < source.length) {
102
+ const current = source.charCodeAt(index++);
103
+ if (current >= 0x40 && current <= 0x7e) break;
104
+ }
105
+ continue;
106
+ }
107
+ // Generic two-byte ESC sequence, with optional intermediates.
108
+ while (index < source.length) {
109
+ const current = source.charCodeAt(index);
110
+ if (current >= 0x20 && current <= 0x2f) {
111
+ index++;
112
+ continue;
113
+ }
114
+ if (current >= 0x30 && current <= 0x7e) index++;
115
+ break;
116
+ }
117
+ continue;
118
+ }
119
+
120
+ const codePoint = source.codePointAt(index) ?? code;
121
+ const width = codePoint > 0xffff ? 2 : 1;
122
+ const c0 = codePoint <= 0x1f && codePoint !== 0x09 && codePoint !== 0x0a && codePoint !== 0x0d;
123
+ const c1 = codePoint >= 0x7f && codePoint <= 0x9f;
124
+ if (!c0 && !c1 && !isInvisibleFormat(codePoint)) {
125
+ output.push(source.slice(index, index + width));
126
+ }
127
+ index += width;
128
+ }
129
+ if (truncated) output.push("…");
130
+ return output.join("");
131
+ }
132
+
133
+ export function displayOneLine(value: string): string {
134
+ return stripTerminalControls(value).replace(/\s+/g, " ").trim();
135
+ }
136
+
137
+ export function truncateDisplay(value: string, max: number): string {
138
+ const text = displayOneLine(value);
139
+ if (text.length <= max) return text;
140
+ if (max <= 1) return "…".slice(0, Math.max(0, max));
141
+ return `${text.slice(0, max - 1)}…`;
142
+ }
143
+
144
+ /** Keep the newest end only when the complete input fits the safety window. */
145
+ export function truncateDisplayTail(value: string, max: number): string {
146
+ if (value.length > DISPLAY_INPUT_LIMIT) return fitMarker(DISPLAY_OMITTED_TEXT, max);
147
+ const text = displayOneLine(value);
148
+ if (text.length <= max) return text;
149
+ if (max <= 1) return "…".slice(0, Math.max(0, max));
150
+ return `…${text.slice(-(max - 1))}`;
151
+ }
152
+
153
+ /** Best-effort redaction for provider errors and other bounded status text. */
154
+ export function redactCommand(value: string): string {
155
+ const text = displayOneLine(value);
156
+ if (value.length > DISPLAY_INPUT_LIMIT && hasPendingCredentialContextPrepared(text)) {
157
+ return DISPLAY_OMITTED_TEXT;
158
+ }
159
+ return redactPrepared(text);
160
+ }
161
+
162
+ /** Terminal-safe, redacted, and bounded status text. */
163
+ export function safeDisplayText(value: string, max: number): string {
164
+ return truncatePrepared(redactCommand(value), max, false);
165
+ }
166
+
167
+ /** Keep the newest terminal-safe text, or omit it when prior context was dropped. */
168
+ export function safeDisplayTail(value: string, max: number): string {
169
+ if (value.length > DISPLAY_INPUT_LIMIT) return fitMarker(DISPLAY_OMITTED_TEXT, max);
170
+ return truncatePrepared(redactCommand(value), max, true);
171
+ }
172
+
173
+ /**
174
+ * Preserve newlines and indentation while applying the same terminal-control
175
+ * stripping and credential redaction used by compact status text. The caller
176
+ * owns the explicit input bound; transcript buffers use this only after their
177
+ * own per-task/run limits have been enforced.
178
+ */
179
+ export function safeTranscriptText(value: string, maxInput = 1024 * 1024): string {
180
+ return redactPrepared(stripTerminalControls(value, maxInput));
181
+ }
182
+
183
+ /**
184
+ * Whether a bounded stream fragment ends inside a credential-shaped construct
185
+ * whose safe rendering depends on a later delimiter.
186
+ */
187
+ export function hasPendingCredentialContext(value: string): boolean {
188
+ if (value.length > DISPLAY_INPUT_LIMIT) return true;
189
+ if (hasPendingTerminalSequence(value)) return true;
190
+ return hasPendingCredentialContextPrepared(stripTerminalControls(value));
191
+ }
192
+
193
+ function redactPrepared(value: string): string {
194
+ const text = redactSensitiveHeaders(value);
195
+ return text
196
+ .replace(
197
+ /(\b[A-Z0-9_]*(?:API[_-]?KEY|TOKEN|SECRET|PASSWORD|PASSWD)[A-Z0-9_]*=)(?:"(?:\\.|[^"\\])*(?:"|$)|'(?:\\.|[^'\\])*(?:'|$)|[^\s]+)/gi,
198
+ "$1***",
199
+ )
200
+ .replace(
201
+ /(\B--?(?:api[-_]?key|token|access[-_]?token|refresh[-_]?token|id[-_]?token|password|passwd|secret|client-secret|user)(?:=|\s+))(?:"(?:\\.|[^"\\])*(?:"|$)|'(?:\\.|[^'\\])*(?:'|$)|[^\s]+)/gi,
202
+ "$1***",
203
+ )
204
+ .replace(/(\s-u)(?:\s+)?(?:"(?:\\.|[^"\\])*(?:"|$)|'(?:\\.|[^'\\])*(?:'|$)|[^\s]+)/gi, "$1***")
205
+ .replace(/(^|[^A-Za-z0-9+.-])([A-Za-z][A-Za-z0-9+.-]*:\/\/)[^/@\s]+@/g, "$1$2***@")
206
+ .replace(/(^|[^A-Za-z0-9+.-])([A-Za-z][A-Za-z0-9+.-]*:\/\/)[^/@\s]+:[^/@\s]*(?=$|[/?#\s])/g, "$1$2***")
207
+ .replace(/([?&](?:api[_-]?key|token|access[_-]?token|refresh[_-]?token|id[_-]?token|password|secret|client_secret)=)[^&\s'" ]+/gi, "$1***")
208
+ .replace(/\b(?:sk|ghp|github_pat|xox[baprs])[-_][A-Za-z0-9_-]{6,}\b/g, "***");
209
+ }
210
+
211
+ function truncatePrepared(value: string, max: number, tail: boolean): string {
212
+ if (value.length <= max) return value;
213
+ if (max <= 1) return "…".slice(0, Math.max(0, max));
214
+ return tail ? `…${value.slice(-(max - 1))}` : `${value.slice(0, max - 1)}…`;
215
+ }
216
+
217
+ /**
218
+ * Commands can carry secrets in arbitrary shell syntax, so status uses a
219
+ * whitelist synopsis instead of trying to display every argument safely.
220
+ */
221
+ export function safeCommandPreview(value: string): string {
222
+ const command = displayOneLine(value);
223
+ if (!command) return "command";
224
+ const tokens = command.match(/"[^"]*"|'[^']*'|[^\s]+/g) ?? [];
225
+ const index = 0;
226
+ if (/^[A-Za-z_][A-Za-z0-9_]*=/.test(tokens[0] ?? "")) return "environment-prefixed command";
227
+ const rawExecutable = tokens[index] ?? "command";
228
+ const basename = rawExecutable.replace(/^['"]|['"]$/g, "").split("/").pop() || "command";
229
+ const candidate = /^[A-Za-z0-9_.+-]+$/.test(basename) ? basename.toLowerCase() : "";
230
+ if (!SAFE_EXECUTABLES.has(candidate)) return "command";
231
+ const executable = truncateDisplay(basename, 32);
232
+ const next = (tokens[index + 1] ?? "").replace(/^['"]|['"]$/g, "");
233
+
234
+ const allowed = SAFE_SUBCOMMANDS.get(candidate);
235
+ if (allowed?.has(next.toLowerCase())) return `${executable} ${truncateDisplay(next, 24)}`;
236
+ if (executable.toLowerCase() === "sleep" && /^\d+(?:\.\d+)?[smhd]?$/.test(next)) {
237
+ return `${executable} ${next}`;
238
+ }
239
+ return tokens.length > index + 1 ? `${executable} …` : executable;
240
+ }
241
+
242
+ const SENSITIVE_HEADER_NAME = "(?:proxy-authorization|authorization|x-auth-token|x-api-key|api-key|api_key|client-secret|client_secret|access-token|access_token|refresh-token|refresh_token|id-token|id_token|set-cookie|cookie|token|password|passwd|secret)";
243
+ const SENSITIVE_HEADER = new RegExp(
244
+ `(^|[^A-Za-z0-9_-]|(?:^|\\s)-H\\s*["']?)((?:["']?${SENSITIVE_HEADER_NAME}["']?)\\s*:\\s*)`,
245
+ "gi",
246
+ );
247
+ const PENDING_URI_USERINFO = /(?:^|[^A-Za-z0-9+.-])[A-Za-z][A-Za-z0-9+.-]*:\/\/[^/\s@]*$/;
248
+
249
+ function hasPendingTerminalSequence(value: string): boolean {
250
+ let index = 0;
251
+ while (index < value.length) {
252
+ if (value.charCodeAt(index) !== ESC) {
253
+ index++;
254
+ continue;
255
+ }
256
+ index++;
257
+ if (index >= value.length) return true;
258
+ const kind = value.charCodeAt(index++);
259
+ if (kind === 0x5d) {
260
+ let terminated = false;
261
+ while (index < value.length) {
262
+ const current = value.charCodeAt(index);
263
+ if (current === BEL) {
264
+ index++;
265
+ terminated = true;
266
+ break;
267
+ }
268
+ if (current === ESC && value.charCodeAt(index + 1) === 0x5c) {
269
+ index += 2;
270
+ terminated = true;
271
+ break;
272
+ }
273
+ index++;
274
+ }
275
+ if (!terminated) return true;
276
+ continue;
277
+ }
278
+ if (kind === 0x50 || kind === 0x5e || kind === 0x5f || kind === 0x58) {
279
+ let terminated = false;
280
+ while (index < value.length) {
281
+ if (value.charCodeAt(index) === ESC && value.charCodeAt(index + 1) === 0x5c) {
282
+ index += 2;
283
+ terminated = true;
284
+ break;
285
+ }
286
+ index++;
287
+ }
288
+ if (!terminated) return true;
289
+ continue;
290
+ }
291
+ if (kind === 0x5b) {
292
+ let terminated = false;
293
+ while (index < value.length) {
294
+ const current = value.charCodeAt(index++);
295
+ if (current >= 0x40 && current <= 0x7e) {
296
+ terminated = true;
297
+ break;
298
+ }
299
+ }
300
+ if (!terminated) return true;
301
+ continue;
302
+ }
303
+ while (index < value.length && value.charCodeAt(index) >= 0x20 && value.charCodeAt(index) <= 0x2f) {
304
+ index++;
305
+ }
306
+ if (index >= value.length) return true;
307
+ const final = value.charCodeAt(index);
308
+ if (final >= 0x30 && final <= 0x7e) index++;
309
+ }
310
+ return false;
311
+ }
312
+
313
+ function hasPendingCredentialContextPrepared(value: string): boolean {
314
+ if (PENDING_URI_USERINFO.test(value)) return true;
315
+ SENSITIVE_HEADER.lastIndex = 0;
316
+ for (let match = SENSITIVE_HEADER.exec(value); match; match = SENSITIVE_HEADER.exec(value)) {
317
+ const start = SENSITIVE_HEADER.lastIndex;
318
+ if (!sensitiveValueIsComplete(value, start, /authorization|cookie/i.test(match[2]))) {
319
+ SENSITIVE_HEADER.lastIndex = 0;
320
+ return true;
321
+ }
322
+ }
323
+ SENSITIVE_HEADER.lastIndex = 0;
324
+ return false;
325
+ }
326
+
327
+ function sensitiveValueIsComplete(value: string, start: number, authorizationHeader: boolean): boolean {
328
+ if (start >= value.length) return false;
329
+ if (value.startsWith("***", start)) return true;
330
+ const opener = value[start];
331
+ if (opener === '"' || opener === "'") return quotedValueIsComplete(value, start, opener);
332
+ if (opener === "[" || opener === "{") return structuredValueIsComplete(value, start, opener);
333
+ const delimiters = authorizationHeader ? /[\r\n}\]]/ : /[,}\]\r\n]/;
334
+ return delimiters.test(value.slice(start));
335
+ }
336
+
337
+ function quotedValueIsComplete(value: string, start: number, quote: string): boolean {
338
+ for (let index = start + 1; index < value.length; index++) {
339
+ if (value[index] === "\\") {
340
+ index++;
341
+ continue;
342
+ }
343
+ if (value[index] === quote) return true;
344
+ }
345
+ return false;
346
+ }
347
+
348
+ function structuredValueIsComplete(value: string, start: number, opener: string): boolean {
349
+ const stack = [opener];
350
+ let quote: string | undefined;
351
+ for (let index = start + 1; index < value.length; index++) {
352
+ const character = value[index];
353
+ if (quote) {
354
+ if (character === "\\") {
355
+ index++;
356
+ } else if (character === quote) {
357
+ quote = undefined;
358
+ }
359
+ continue;
360
+ }
361
+ if (character === '"' || character === "'") {
362
+ quote = character;
363
+ } else if (character === "[" || character === "{") {
364
+ stack.push(character);
365
+ } else if (character === "]" || character === "}") {
366
+ const expected = character === "]" ? "[" : "{";
367
+ if (stack.pop() !== expected) return false;
368
+ if (stack.length === 0) return true;
369
+ }
370
+ }
371
+ return false;
372
+ }
373
+
374
+ function redactSensitiveHeaders(value: string): string {
375
+ let cursor = 0;
376
+ let output = "";
377
+ SENSITIVE_HEADER.lastIndex = 0;
378
+ for (let match = SENSITIVE_HEADER.exec(value); match; match = SENSITIVE_HEADER.exec(value)) {
379
+ const valueStart = SENSITIVE_HEADER.lastIndex;
380
+ const consumeUnquotedRemainder = /authorization|cookie/i.test(match[2]);
381
+ const valueEnd = consumeSensitiveValue(value, valueStart, consumeUnquotedRemainder);
382
+ output += value.slice(cursor, valueStart);
383
+ output += "***";
384
+ cursor = valueEnd;
385
+ SENSITIVE_HEADER.lastIndex = valueEnd;
386
+ }
387
+ return output + value.slice(cursor);
388
+ }
389
+
390
+ function consumeSensitiveValue(value: string, start: number, consumeUnquotedRemainder: boolean): number {
391
+ if (start >= value.length) return start;
392
+ if (value.startsWith("***", start)) return start + 3;
393
+ const opener = value[start];
394
+ if (opener === '"' || opener === "'") return consumeQuoted(value, start, opener);
395
+ if (opener === "[" || opener === "{") return consumeStructured(value, start, opener);
396
+ if (consumeUnquotedRemainder) return value.length;
397
+
398
+ let index = start;
399
+ while (index < value.length && value[index] !== "," && value[index] !== "}" && value[index] !== "]") {
400
+ index++;
401
+ }
402
+ return index;
403
+ }
404
+
405
+ function consumeQuoted(value: string, start: number, quote: string): number {
406
+ let index = start + 1;
407
+ while (index < value.length) {
408
+ if (value[index] === "\\") {
409
+ index = Math.min(value.length, index + 2);
410
+ continue;
411
+ }
412
+ if (value[index] === quote) return index + 1;
413
+ index++;
414
+ }
415
+ return value.length;
416
+ }
417
+
418
+ function consumeStructured(value: string, start: number, opener: string): number {
419
+ const stack = [opener];
420
+ let quote: string | undefined;
421
+ let index = start + 1;
422
+ while (index < value.length) {
423
+ const character = value[index];
424
+ if (quote) {
425
+ if (character === "\\") {
426
+ index = Math.min(value.length, index + 2);
427
+ continue;
428
+ }
429
+ if (character === quote) quote = undefined;
430
+ index++;
431
+ continue;
432
+ }
433
+ if (character === '"' || character === "'") {
434
+ quote = character;
435
+ index++;
436
+ continue;
437
+ }
438
+ if (character === "[" || character === "{") {
439
+ stack.push(character);
440
+ index++;
441
+ continue;
442
+ }
443
+ if (character === "]" || character === "}") {
444
+ const expected = character === "]" ? "[" : "{";
445
+ if (stack.at(-1) !== expected) return value.length;
446
+ stack.pop();
447
+ index++;
448
+ if (stack.length === 0) return index;
449
+ continue;
450
+ }
451
+ index++;
452
+ }
453
+ return value.length;
454
+ }
455
+
456
+ function fitMarker(marker: string, max: number): string {
457
+ if (max <= 0) return "";
458
+ if (marker.length <= max) return marker;
459
+ if (max === 1) return "…";
460
+ return `${marker.slice(0, max - 1)}…`;
461
+ }