gsd-pi 2.7.1 → 2.8.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 (133) hide show
  1. package/README.md +12 -5
  2. package/dist/loader.js +0 -0
  3. package/dist/modes/interactive/theme/dark.json +85 -0
  4. package/dist/modes/interactive/theme/light.json +84 -0
  5. package/dist/modes/interactive/theme/theme-schema.json +335 -0
  6. package/dist/modes/interactive/theme/theme.d.ts +78 -0
  7. package/dist/modes/interactive/theme/theme.d.ts.map +1 -0
  8. package/dist/modes/interactive/theme/theme.js +949 -0
  9. package/dist/modes/interactive/theme/theme.js.map +1 -0
  10. package/node_modules/@gsd/pi-coding-agent/dist/config.d.ts +2 -0
  11. package/node_modules/@gsd/pi-coding-agent/dist/config.d.ts.map +1 -1
  12. package/node_modules/@gsd/pi-coding-agent/dist/config.js +4 -0
  13. package/node_modules/@gsd/pi-coding-agent/dist/config.js.map +1 -1
  14. package/node_modules/@gsd/pi-coding-agent/dist/core/artifact-manager.d.ts +52 -0
  15. package/node_modules/@gsd/pi-coding-agent/dist/core/artifact-manager.d.ts.map +1 -0
  16. package/node_modules/@gsd/pi-coding-agent/dist/core/artifact-manager.js +117 -0
  17. package/node_modules/@gsd/pi-coding-agent/dist/core/artifact-manager.js.map +1 -0
  18. package/node_modules/@gsd/pi-coding-agent/dist/core/blob-store.d.ts +31 -0
  19. package/node_modules/@gsd/pi-coding-agent/dist/core/blob-store.d.ts.map +1 -0
  20. package/node_modules/@gsd/pi-coding-agent/dist/core/blob-store.js +97 -0
  21. package/node_modules/@gsd/pi-coding-agent/dist/core/blob-store.js.map +1 -0
  22. package/node_modules/@gsd/pi-coding-agent/dist/core/session-manager.d.ts +1 -0
  23. package/node_modules/@gsd/pi-coding-agent/dist/core/session-manager.d.ts.map +1 -1
  24. package/node_modules/@gsd/pi-coding-agent/dist/core/session-manager.js +112 -3
  25. package/node_modules/@gsd/pi-coding-agent/dist/core/session-manager.js.map +1 -1
  26. package/node_modules/@gsd/pi-coding-agent/dist/core/tools/bash.d.ts +4 -0
  27. package/node_modules/@gsd/pi-coding-agent/dist/core/tools/bash.d.ts.map +1 -1
  28. package/node_modules/@gsd/pi-coding-agent/dist/core/tools/bash.js +30 -20
  29. package/node_modules/@gsd/pi-coding-agent/dist/core/tools/bash.js.map +1 -1
  30. package/node_modules/@gsd/pi-coding-agent/dist/index.d.ts +2 -0
  31. package/node_modules/@gsd/pi-coding-agent/dist/index.d.ts.map +1 -1
  32. package/node_modules/@gsd/pi-coding-agent/dist/index.js +3 -0
  33. package/node_modules/@gsd/pi-coding-agent/dist/index.js.map +1 -1
  34. package/node_modules/@gsd/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  35. package/node_modules/@gsd/pi-coding-agent/dist/modes/interactive/interactive-mode.js +1 -1
  36. package/node_modules/@gsd/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
  37. package/node_modules/@gsd/pi-coding-agent/src/config.ts +5 -0
  38. package/node_modules/@gsd/pi-coding-agent/src/core/artifact-manager.ts +125 -0
  39. package/node_modules/@gsd/pi-coding-agent/src/core/blob-store.ts +106 -0
  40. package/node_modules/@gsd/pi-coding-agent/src/core/session-manager.ts +119 -3
  41. package/node_modules/@gsd/pi-coding-agent/src/core/tools/bash.ts +33 -20
  42. package/node_modules/@gsd/pi-coding-agent/src/index.ts +3 -0
  43. package/node_modules/@gsd/pi-coding-agent/src/modes/interactive/interactive-mode.ts +1 -1
  44. package/node_modules/cliui/CHANGELOG.md +121 -0
  45. package/node_modules/color-convert/CHANGELOG.md +54 -0
  46. package/node_modules/esprima/ChangeLog +235 -0
  47. package/node_modules/mz/HISTORY.md +66 -0
  48. package/node_modules/proper-lockfile/CHANGELOG.md +108 -0
  49. package/node_modules/source-map/CHANGELOG.md +301 -0
  50. package/node_modules/thenify/History.md +11 -0
  51. package/node_modules/thenify-all/History.md +11 -0
  52. package/node_modules/y18n/CHANGELOG.md +100 -0
  53. package/node_modules/yargs/CHANGELOG.md +88 -0
  54. package/node_modules/yargs-parser/CHANGELOG.md +263 -0
  55. package/package.json +10 -2
  56. package/packages/pi-coding-agent/dist/config.d.ts +2 -0
  57. package/packages/pi-coding-agent/dist/config.d.ts.map +1 -1
  58. package/packages/pi-coding-agent/dist/config.js +4 -0
  59. package/packages/pi-coding-agent/dist/config.js.map +1 -1
  60. package/packages/pi-coding-agent/dist/core/artifact-manager.d.ts +52 -0
  61. package/packages/pi-coding-agent/dist/core/artifact-manager.d.ts.map +1 -0
  62. package/packages/pi-coding-agent/dist/core/artifact-manager.js +117 -0
  63. package/packages/pi-coding-agent/dist/core/artifact-manager.js.map +1 -0
  64. package/packages/pi-coding-agent/dist/core/blob-store.d.ts +31 -0
  65. package/packages/pi-coding-agent/dist/core/blob-store.d.ts.map +1 -0
  66. package/packages/pi-coding-agent/dist/core/blob-store.js +97 -0
  67. package/packages/pi-coding-agent/dist/core/blob-store.js.map +1 -0
  68. package/packages/pi-coding-agent/dist/core/session-manager.d.ts +1 -0
  69. package/packages/pi-coding-agent/dist/core/session-manager.d.ts.map +1 -1
  70. package/packages/pi-coding-agent/dist/core/session-manager.js +112 -3
  71. package/packages/pi-coding-agent/dist/core/session-manager.js.map +1 -1
  72. package/packages/pi-coding-agent/dist/core/tools/bash.d.ts +4 -0
  73. package/packages/pi-coding-agent/dist/core/tools/bash.d.ts.map +1 -1
  74. package/packages/pi-coding-agent/dist/core/tools/bash.js +30 -20
  75. package/packages/pi-coding-agent/dist/core/tools/bash.js.map +1 -1
  76. package/packages/pi-coding-agent/dist/index.d.ts +2 -0
  77. package/packages/pi-coding-agent/dist/index.d.ts.map +1 -1
  78. package/packages/pi-coding-agent/dist/index.js +3 -0
  79. package/packages/pi-coding-agent/dist/index.js.map +1 -1
  80. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  81. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +1 -1
  82. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
  83. package/packages/pi-coding-agent/src/config.ts +5 -0
  84. package/packages/pi-coding-agent/src/core/artifact-manager.ts +125 -0
  85. package/packages/pi-coding-agent/src/core/blob-store.ts +106 -0
  86. package/packages/pi-coding-agent/src/core/session-manager.ts +119 -3
  87. package/packages/pi-coding-agent/src/core/tools/bash.ts +33 -20
  88. package/packages/pi-coding-agent/src/index.ts +3 -0
  89. package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +1 -1
  90. package/src/resources/extensions/browser-tools/capture.ts +165 -0
  91. package/src/resources/extensions/browser-tools/evaluate-helpers.ts +184 -0
  92. package/src/resources/extensions/browser-tools/index.ts +47 -4985
  93. package/src/resources/extensions/browser-tools/lifecycle.ts +270 -0
  94. package/src/resources/extensions/browser-tools/package.json +5 -1
  95. package/src/resources/extensions/browser-tools/refs.ts +264 -0
  96. package/src/resources/extensions/browser-tools/settle.ts +197 -0
  97. package/src/resources/extensions/browser-tools/state.ts +408 -0
  98. package/src/resources/extensions/browser-tools/tests/browser-tools-integration.test.mjs +652 -0
  99. package/src/resources/extensions/browser-tools/tests/browser-tools-unit.test.cjs +614 -0
  100. package/src/resources/extensions/browser-tools/tools/assertions.ts +342 -0
  101. package/src/resources/extensions/browser-tools/tools/forms.ts +801 -0
  102. package/src/resources/extensions/browser-tools/tools/inspection.ts +492 -0
  103. package/src/resources/extensions/browser-tools/tools/intent.ts +614 -0
  104. package/src/resources/extensions/browser-tools/tools/interaction.ts +865 -0
  105. package/src/resources/extensions/browser-tools/tools/navigation.ts +232 -0
  106. package/src/resources/extensions/browser-tools/tools/pages.ts +303 -0
  107. package/src/resources/extensions/browser-tools/tools/refs.ts +541 -0
  108. package/src/resources/extensions/browser-tools/tools/screenshot.ts +83 -0
  109. package/src/resources/extensions/browser-tools/tools/session.ts +400 -0
  110. package/src/resources/extensions/browser-tools/tools/wait.ts +247 -0
  111. package/src/resources/extensions/browser-tools/utils.ts +660 -0
  112. package/src/resources/extensions/gsd/auto.ts +74 -13
  113. package/src/resources/extensions/gsd/dispatch-guard.ts +65 -0
  114. package/src/resources/extensions/gsd/exit-command.ts +18 -0
  115. package/src/resources/extensions/gsd/files.ts +9 -40
  116. package/src/resources/extensions/gsd/git-service.ts +65 -17
  117. package/src/resources/extensions/gsd/gitignore.ts +1 -0
  118. package/src/resources/extensions/gsd/guided-flow.ts +39 -2
  119. package/src/resources/extensions/gsd/index.ts +111 -16
  120. package/src/resources/extensions/gsd/preferences.ts +8 -0
  121. package/src/resources/extensions/gsd/prompts/complete-slice.md +2 -2
  122. package/src/resources/extensions/gsd/prompts/discuss.md +25 -0
  123. package/src/resources/extensions/gsd/roadmap-slices.ts +50 -0
  124. package/src/resources/extensions/gsd/tests/dispatch-guard.test.ts +102 -0
  125. package/src/resources/extensions/gsd/tests/exit-command.test.ts +50 -0
  126. package/src/resources/extensions/gsd/tests/git-service.test.ts +64 -39
  127. package/src/resources/extensions/gsd/tests/roadmap-slices.test.ts +59 -0
  128. package/src/resources/extensions/gsd/tests/write-gate.test.ts +122 -0
  129. package/src/resources/extensions/shared/interview-ui.ts +1 -1
  130. package/src/resources/extensions/ttsr/index.ts +163 -0
  131. package/src/resources/extensions/ttsr/rule-loader.ts +121 -0
  132. package/src/resources/extensions/ttsr/ttsr-interrupt.md +6 -0
  133. package/src/resources/extensions/ttsr/ttsr-manager.ts +344 -0
@@ -235,6 +235,11 @@ export function getSessionsDir(): string {
235
235
  return join(getAgentDir(), "sessions");
236
236
  }
237
237
 
238
+ /** Get path to content-addressed blob store directory */
239
+ export function getBlobsDir(): string {
240
+ return join(getAgentDir(), "blobs");
241
+ }
242
+
238
243
  /** Get path to debug log file */
239
244
  export function getDebugLogPath(): string {
240
245
  return join(getAgentDir(), `${APP_NAME}-debug.log`);
@@ -0,0 +1,125 @@
1
+ /**
2
+ * Session-scoped artifact storage for truncated tool outputs.
3
+ *
4
+ * Artifacts are stored in a directory alongside the session file,
5
+ * accessible via artifact:// URLs.
6
+ */
7
+ import { mkdirSync, readdirSync, writeFileSync, existsSync } from "node:fs";
8
+ import { join } from "node:path";
9
+
10
+ /**
11
+ * Manages artifact storage for a session.
12
+ *
13
+ * Artifacts are stored with sequential IDs in the session's artifact directory.
14
+ * The directory is created lazily on first write.
15
+ */
16
+ export class ArtifactManager {
17
+ #nextId = 0;
18
+ readonly #dir: string;
19
+ #dirCreated = false;
20
+ #initialized = false;
21
+
22
+ /**
23
+ * @param sessionFile Path to the session .jsonl file
24
+ */
25
+ constructor(sessionFile: string) {
26
+ // Artifact directory is session file path without .jsonl extension
27
+ this.#dir = sessionFile.slice(0, -6);
28
+ }
29
+
30
+ /**
31
+ * Artifact directory path.
32
+ * Directory may not exist until first artifact is saved.
33
+ */
34
+ get dir(): string {
35
+ return this.#dir;
36
+ }
37
+
38
+ #ensureDir(): void {
39
+ if (!this.#dirCreated) {
40
+ mkdirSync(this.#dir, { recursive: true });
41
+ this.#dirCreated = true;
42
+ }
43
+ if (!this.#initialized) {
44
+ this.#scanExistingIds();
45
+ this.#initialized = true;
46
+ }
47
+ }
48
+
49
+ /**
50
+ * Scan existing artifact files to find the next available ID.
51
+ * Ensures we don't overwrite artifacts when resuming a session.
52
+ */
53
+ #scanExistingIds(): void {
54
+ const files = this.listFiles();
55
+ let maxId = -1;
56
+ for (const file of files) {
57
+ const match = file.match(/^(\d+)\..*\.log$/);
58
+ if (match) {
59
+ const id = parseInt(match[1], 10);
60
+ if (id > maxId) maxId = id;
61
+ }
62
+ }
63
+ this.#nextId = maxId + 1;
64
+ }
65
+
66
+ /** Atomically allocate next artifact ID. */
67
+ allocateId(): number {
68
+ return this.#nextId++;
69
+ }
70
+
71
+ /**
72
+ * Allocate a new artifact path and ID without writing content.
73
+ * @param toolType Tool name for file extension (e.g., "bash", "fetch")
74
+ */
75
+ allocatePath(toolType: string): { id: string; path: string } {
76
+ this.#ensureDir();
77
+ const id = String(this.allocateId());
78
+ const filename = `${id}.${toolType}.log`;
79
+ return { id, path: join(this.#dir, filename) };
80
+ }
81
+
82
+ /**
83
+ * Save content as an artifact and return the artifact ID.
84
+ * @param content Full content to save
85
+ * @param toolType Tool name for file extension (e.g., "bash", "fetch")
86
+ * @returns Artifact ID (numeric string)
87
+ */
88
+ save(content: string, toolType: string): string {
89
+ const { id, path } = this.allocatePath(toolType);
90
+ writeFileSync(path, content);
91
+ return id;
92
+ }
93
+
94
+ /**
95
+ * Check if an artifact exists.
96
+ * @param id Artifact ID (numeric string)
97
+ */
98
+ exists(id: string): boolean {
99
+ const files = this.listFiles();
100
+ return files.some((f) => f.startsWith(`${id}.`));
101
+ }
102
+
103
+ /**
104
+ * List all artifact files in the directory.
105
+ * Returns empty array if directory doesn't exist.
106
+ */
107
+ listFiles(): string[] {
108
+ try {
109
+ return readdirSync(this.#dir);
110
+ } catch {
111
+ return [];
112
+ }
113
+ }
114
+
115
+ /**
116
+ * Get the full path to an artifact file.
117
+ * Returns null if artifact doesn't exist.
118
+ * @param id Artifact ID (numeric string)
119
+ */
120
+ getPath(id: string): string | null {
121
+ const files = this.listFiles();
122
+ const match = files.find((f) => f.startsWith(`${id}.`));
123
+ return match ? join(this.#dir, match) : null;
124
+ }
125
+ }
@@ -0,0 +1,106 @@
1
+ /**
2
+ * Content-addressed blob store for externalizing large binary data (images) from session JSONL files.
3
+ *
4
+ * Files are stored at `<dir>/<sha256-hex>` with no extension. The SHA-256 hash is computed
5
+ * over the raw binary data (not base64). Content-addressing makes writes idempotent and
6
+ * provides automatic deduplication across sessions.
7
+ */
8
+ import { createHash } from "node:crypto";
9
+ import { mkdirSync, readFileSync, writeFileSync, existsSync, accessSync } from "node:fs";
10
+ import { join } from "node:path";
11
+
12
+ const BLOB_PREFIX = "blob:sha256:";
13
+ const SHA256_HEX_RE = /^[a-f0-9]{64}$/;
14
+
15
+ export interface BlobPutResult {
16
+ hash: string;
17
+ path: string;
18
+ get ref(): string;
19
+ }
20
+
21
+ export class BlobStore {
22
+ readonly dir: string;
23
+ constructor(dir: string) {
24
+ this.dir = dir;
25
+ mkdirSync(dir, { recursive: true });
26
+ }
27
+
28
+ /** Write binary data to the blob store. Idempotent — same content → same hash. */
29
+ put(data: Buffer): BlobPutResult {
30
+ const hash = createHash("sha256").update(data).digest("hex");
31
+ const blobPath = join(this.dir, hash);
32
+ const result: BlobPutResult = {
33
+ hash,
34
+ path: blobPath,
35
+ get ref() {
36
+ return `${BLOB_PREFIX}${hash}`;
37
+ },
38
+ };
39
+
40
+ if (!existsSync(blobPath)) {
41
+ writeFileSync(blobPath, data);
42
+ }
43
+ return result;
44
+ }
45
+
46
+ /** Read blob by hash, returns Buffer or null if not found. */
47
+ get(hash: string): Buffer | null {
48
+ if (!SHA256_HEX_RE.test(hash)) return null;
49
+ const blobPath = join(this.dir, hash);
50
+ try {
51
+ return readFileSync(blobPath);
52
+ } catch {
53
+ return null;
54
+ }
55
+ }
56
+
57
+ /** Check if a blob exists. */
58
+ has(hash: string): boolean {
59
+ if (!SHA256_HEX_RE.test(hash)) return false;
60
+ try {
61
+ accessSync(join(this.dir, hash));
62
+ return true;
63
+ } catch {
64
+ return false;
65
+ }
66
+ }
67
+ }
68
+
69
+ /** Check if a data string is a blob reference. */
70
+ export function isBlobRef(data: string): boolean {
71
+ return data.startsWith(BLOB_PREFIX);
72
+ }
73
+
74
+ /** Extract the SHA-256 hash from a blob reference string. Returns null if format is invalid. */
75
+ export function parseBlobRef(data: string): string | null {
76
+ if (!data.startsWith(BLOB_PREFIX)) return null;
77
+ const hash = data.slice(BLOB_PREFIX.length);
78
+ if (!SHA256_HEX_RE.test(hash)) return null;
79
+ return hash;
80
+ }
81
+
82
+ /**
83
+ * Externalize an image's base64 data to the blob store, returning a blob reference.
84
+ * If the data is already a blob reference, returns it unchanged.
85
+ */
86
+ export function externalizeImageData(blobStore: BlobStore, base64Data: string): string {
87
+ if (isBlobRef(base64Data)) return base64Data;
88
+ const buffer = Buffer.from(base64Data, "base64");
89
+ const { ref } = blobStore.put(buffer);
90
+ return ref;
91
+ }
92
+
93
+ /**
94
+ * Resolve a blob reference back to base64 data.
95
+ * If the data is not a blob reference, returns it unchanged.
96
+ * If the blob is missing, returns the ref unchanged.
97
+ */
98
+ export function resolveImageData(blobStore: BlobStore, data: string): string {
99
+ const hash = parseBlobRef(data);
100
+ if (!hash) return data;
101
+
102
+ const buffer = blobStore.get(hash);
103
+ if (!buffer) return data; // Missing blob — return ref as-is
104
+
105
+ return buffer.toString("base64");
106
+ }
@@ -15,7 +15,7 @@ import {
15
15
  } from "fs";
16
16
  import { readdir, readFile, stat } from "fs/promises";
17
17
  import { join, resolve } from "path";
18
- import { getAgentDir as getDefaultAgentDir, getSessionsDir } from "../config.js";
18
+ import { getAgentDir as getDefaultAgentDir, getBlobsDir, getSessionsDir } from "../config.js";
19
19
  import {
20
20
  type BashExecutionMessage,
21
21
  type CustomMessage,
@@ -23,6 +23,11 @@ import {
23
23
  createCompactionSummaryMessage,
24
24
  createCustomMessage,
25
25
  } from "./messages.js";
26
+ import { BlobStore, externalizeImageData, isBlobRef, resolveImageData } from "./blob-store.js";
27
+
28
+ const BLOB_EXTERNALIZE_THRESHOLD = 1024; // 1KB minimum to externalize
29
+ const MAX_PERSIST_CHARS = 500_000;
30
+ const TRUNCATION_NOTICE = "\n\n[Session persistence truncated large content]";
26
31
 
27
32
  export const CURRENT_SESSION_VERSION = 3;
28
33
 
@@ -426,6 +431,112 @@ function getDefaultSessionDir(cwd: string): string {
426
431
  return sessionDir;
427
432
  }
428
433
 
434
+ function isImageBlock(value: unknown): value is { type: "image"; data: string; mimeType?: string } {
435
+ return (
436
+ typeof value === "object" &&
437
+ value !== null &&
438
+ "type" in value &&
439
+ (value as { type?: string }).type === "image" &&
440
+ "data" in value &&
441
+ typeof (value as { data?: string }).data === "string"
442
+ );
443
+ }
444
+
445
+ function truncateString(s: string, maxLength: number): string {
446
+ if (s.length <= maxLength) return s;
447
+ // Avoid splitting surrogate pairs
448
+ if (maxLength > 0 && s.charCodeAt(maxLength - 1) >= 0xd800 && s.charCodeAt(maxLength - 1) <= 0xdbff) {
449
+ return s.slice(0, maxLength - 1);
450
+ }
451
+ return s.slice(0, maxLength);
452
+ }
453
+
454
+ /**
455
+ * Prepare an entry for JSONL persistence: externalize large images to blob store,
456
+ * truncate oversized strings, strip transient fields.
457
+ */
458
+ function prepareForPersistence(obj: unknown, blobStore: BlobStore, key?: string): unknown {
459
+ if (obj === null || obj === undefined) return obj;
460
+
461
+ if (typeof obj === "string") {
462
+ if (obj.length > MAX_PERSIST_CHARS) {
463
+ // Cryptographic signatures must be preserved exactly or cleared entirely
464
+ if (key === "thinkingSignature" || key === "thoughtSignature" || key === "textSignature") {
465
+ return "";
466
+ }
467
+ const limit = Math.max(0, MAX_PERSIST_CHARS - TRUNCATION_NOTICE.length);
468
+ return `${truncateString(obj, limit)}${TRUNCATION_NOTICE}`;
469
+ }
470
+ return obj;
471
+ }
472
+
473
+ if (Array.isArray(obj)) {
474
+ let changed = false;
475
+ const result = obj.map((item) => {
476
+ // Externalize oversized images to blob store
477
+ if (key === "content" && isImageBlock(item)) {
478
+ if (!isBlobRef(item.data) && item.data.length >= BLOB_EXTERNALIZE_THRESHOLD) {
479
+ changed = true;
480
+ const blobRef = externalizeImageData(blobStore, item.data);
481
+ return { ...item, data: blobRef };
482
+ }
483
+ }
484
+ const newItem = prepareForPersistence(item, blobStore, key);
485
+ if (newItem !== item) changed = true;
486
+ return newItem;
487
+ });
488
+ return changed ? result : obj;
489
+ }
490
+
491
+ if (typeof obj === "object") {
492
+ let changed = false;
493
+ const result: Record<string, unknown> = {};
494
+ for (const [k, v] of Object.entries(obj as Record<string, unknown>)) {
495
+ // Strip transient properties
496
+ if (k === "partialJson" || k === "jsonlEvents") {
497
+ changed = true;
498
+ continue;
499
+ }
500
+ const newV = prepareForPersistence(v, blobStore, k);
501
+ result[k] = newV;
502
+ if (newV !== v) changed = true;
503
+ }
504
+ // Update lineCount if content was truncated (for FileMentionFile)
505
+ if (changed && "lineCount" in result && "content" in result && typeof result.content === "string") {
506
+ result.lineCount = (result.content as string).split("\n").length;
507
+ }
508
+ return changed ? result : obj;
509
+ }
510
+
511
+ return obj;
512
+ }
513
+
514
+ /**
515
+ * Resolve blob references in loaded entries, replacing `blob:sha256:<hash>` data
516
+ * fields with actual base64 content. Mutates entries in place.
517
+ */
518
+ function resolveBlobRefsInEntries(entries: FileEntry[], blobStore: BlobStore): void {
519
+ for (const entry of entries) {
520
+ if (entry.type === "session") continue;
521
+
522
+ let contentArray: unknown[] | undefined;
523
+ if (entry.type === "message") {
524
+ const content = ((entry as SessionMessageEntry).message as { content?: unknown }).content;
525
+ if (Array.isArray(content)) contentArray = content;
526
+ } else if (entry.type === "custom_message" && Array.isArray((entry as any).content)) {
527
+ contentArray = (entry as any).content;
528
+ }
529
+
530
+ if (!contentArray) continue;
531
+
532
+ for (const block of contentArray) {
533
+ if (isImageBlock(block) && isBlobRef(block.data)) {
534
+ (block as { data: string }).data = resolveImageData(blobStore, block.data);
535
+ }
536
+ }
537
+ }
538
+ }
539
+
429
540
  /** Exported for testing */
430
541
  export function loadEntriesFromFile(filePath: string): FileEntry[] {
431
542
  if (!existsSync(filePath)) return [];
@@ -669,6 +780,7 @@ export class SessionManager {
669
780
  private flushed: boolean = false;
670
781
  private fileEntries: FileEntry[] = [];
671
782
  private byId: Map<string, SessionEntry> = new Map();
783
+ private blobStore: BlobStore;
672
784
  private labelsById: Map<string, string> = new Map();
673
785
  private leafId: string | null = null;
674
786
 
@@ -676,6 +788,7 @@ export class SessionManager {
676
788
  this.cwd = cwd;
677
789
  this.sessionDir = sessionDir;
678
790
  this.persist = persist;
791
+ this.blobStore = new BlobStore(getBlobsDir());
679
792
  if (persist && sessionDir && !existsSync(sessionDir)) {
680
793
  mkdirSync(sessionDir, { recursive: true });
681
794
  }
@@ -712,6 +825,7 @@ export class SessionManager {
712
825
  }
713
826
 
714
827
  this._buildIndex();
828
+ resolveBlobRefsInEntries(this.fileEntries, this.blobStore);
715
829
  this.flushed = true;
716
830
  } else {
717
831
  const explicitPath = this.sessionFile;
@@ -800,11 +914,13 @@ export class SessionManager {
800
914
 
801
915
  if (!this.flushed) {
802
916
  for (const e of this.fileEntries) {
803
- appendFileSync(this.sessionFile, `${JSON.stringify(e)}\n`);
917
+ const prepared = prepareForPersistence(e, this.blobStore) as FileEntry;
918
+ appendFileSync(this.sessionFile, `${JSON.stringify(prepared)}\n`);
804
919
  }
805
920
  this.flushed = true;
806
921
  } else {
807
- appendFileSync(this.sessionFile, `${JSON.stringify(entry)}\n`);
922
+ const prepared = prepareForPersistence(entry, this.blobStore) as FileEntry;
923
+ appendFileSync(this.sessionFile, `${JSON.stringify(prepared)}\n`);
808
924
  }
809
925
  }
810
926
 
@@ -8,6 +8,7 @@ import { type Static, Type } from "@sinclair/typebox";
8
8
  import { spawn } from "child_process";
9
9
  import { getShellConfig, getShellEnv, killProcessTree } from "../../utils/shell.js";
10
10
  import { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, formatSize, type TruncationResult, truncateTail } from "./truncate.js";
11
+ import type { ArtifactManager } from "../artifact-manager.js";
11
12
 
12
13
  // Cached Win32 FFI handles for restoring VT input after child processes
13
14
  let _vtHandles: { GetConsoleMode: any; SetConsoleMode: any; handle: any } | null = null;
@@ -51,6 +52,7 @@ export type BashToolInput = Static<typeof bashSchema>;
51
52
  export interface BashToolDetails {
52
53
  truncation?: TruncationResult;
53
54
  fullOutputPath?: string;
55
+ artifactId?: string;
54
56
  }
55
57
 
56
58
  /**
@@ -187,12 +189,15 @@ export interface BashToolOptions {
187
189
  commandPrefix?: string;
188
190
  /** Hook to adjust command, cwd, or env before execution */
189
191
  spawnHook?: BashSpawnHook;
192
+ /** Session-scoped artifact storage. When provided, spills to artifact files instead of temp files. */
193
+ artifactManager?: ArtifactManager;
190
194
  }
191
195
 
192
196
  export function createBashTool(cwd: string, options?: BashToolOptions): AgentTool<typeof bashSchema> {
193
197
  const ops = options?.operations ?? defaultBashOperations;
194
198
  const commandPrefix = options?.commandPrefix;
195
199
  const spawnHook = options?.spawnHook;
200
+ const artifactManager = options?.artifactManager;
196
201
 
197
202
  return {
198
203
  name: "bash",
@@ -210,9 +215,10 @@ export function createBashTool(cwd: string, options?: BashToolOptions): AgentToo
210
215
  const spawnContext = resolveSpawnContext(resolvedCommand, cwd, spawnHook);
211
216
 
212
217
  return new Promise((resolve, reject) => {
213
- // We'll stream to a temp file if output gets large
214
- let tempFilePath: string | undefined;
215
- let tempFileStream: ReturnType<typeof createWriteStream> | undefined;
218
+ // We'll stream to a file if output gets large
219
+ let spillFilePath: string | undefined;
220
+ let spillArtifactId: string | undefined;
221
+ let spillFileStream: ReturnType<typeof createWriteStream> | undefined;
216
222
  let totalBytes = 0;
217
223
 
218
224
  // Keep a rolling buffer of the last chunk for tail truncation
@@ -224,19 +230,25 @@ export function createBashTool(cwd: string, options?: BashToolOptions): AgentToo
224
230
  const handleData = (data: Buffer) => {
225
231
  totalBytes += data.length;
226
232
 
227
- // Start writing to temp file once we exceed the threshold
228
- if (totalBytes > DEFAULT_MAX_BYTES && !tempFilePath) {
229
- tempFilePath = getTempFilePath();
230
- tempFileStream = createWriteStream(tempFilePath);
233
+ // Start writing to file once we exceed the threshold
234
+ if (totalBytes > DEFAULT_MAX_BYTES && !spillFilePath) {
235
+ if (artifactManager) {
236
+ const allocated = artifactManager.allocatePath("bash");
237
+ spillFilePath = allocated.path;
238
+ spillArtifactId = allocated.id;
239
+ } else {
240
+ spillFilePath = getTempFilePath();
241
+ }
242
+ spillFileStream = createWriteStream(spillFilePath);
231
243
  // Write all buffered chunks to the file
232
244
  for (const chunk of chunks) {
233
- tempFileStream.write(chunk);
245
+ spillFileStream.write(chunk);
234
246
  }
235
247
  }
236
248
 
237
249
  // Write to temp file if we have one
238
- if (tempFileStream) {
239
- tempFileStream.write(data);
250
+ if (spillFileStream) {
251
+ spillFileStream.write(data);
240
252
  }
241
253
 
242
254
  // Keep rolling buffer of recent data
@@ -258,7 +270,7 @@ export function createBashTool(cwd: string, options?: BashToolOptions): AgentToo
258
270
  content: [{ type: "text", text: truncation.content || "" }],
259
271
  details: {
260
272
  truncation: truncation.truncated ? truncation : undefined,
261
- fullOutputPath: tempFilePath,
273
+ fullOutputPath: spillFilePath,
262
274
  },
263
275
  });
264
276
  }
@@ -272,8 +284,8 @@ export function createBashTool(cwd: string, options?: BashToolOptions): AgentToo
272
284
  })
273
285
  .then(({ exitCode }) => {
274
286
  // Close temp file stream
275
- if (tempFileStream) {
276
- tempFileStream.end();
287
+ if (spillFileStream) {
288
+ spillFileStream.end();
277
289
  }
278
290
 
279
291
  // Combine all buffered chunks
@@ -290,21 +302,22 @@ export function createBashTool(cwd: string, options?: BashToolOptions): AgentToo
290
302
  if (truncation.truncated) {
291
303
  details = {
292
304
  truncation,
293
- fullOutputPath: tempFilePath,
305
+ fullOutputPath: spillFilePath,
306
+ ...(spillArtifactId ? { artifactId: spillArtifactId } : {}),
294
307
  };
295
308
 
296
309
  // Build actionable notice
297
310
  const startLine = truncation.totalLines - truncation.outputLines + 1;
298
311
  const endLine = truncation.totalLines;
312
+ const outputRef = spillArtifactId ? `artifact://${spillArtifactId}` : spillFilePath;
299
313
 
300
314
  if (truncation.lastLinePartial) {
301
- // Edge case: last line alone > 30KB
302
315
  const lastLineSize = formatSize(Buffer.byteLength(fullOutput.split("\n").pop() || "", "utf-8"));
303
- outputText += `\n\n[Showing last ${formatSize(truncation.outputBytes)} of line ${endLine} (line is ${lastLineSize}). Full output: ${tempFilePath}]`;
316
+ outputText += `\n\n[Showing last ${formatSize(truncation.outputBytes)} of line ${endLine} (line is ${lastLineSize}). Full output: ${outputRef}]`;
304
317
  } else if (truncation.truncatedBy === "lines") {
305
- outputText += `\n\n[Showing lines ${startLine}-${endLine} of ${truncation.totalLines}. Full output: ${tempFilePath}]`;
318
+ outputText += `\n\n[Showing lines ${startLine}-${endLine} of ${truncation.totalLines}. Full output: ${outputRef}]`;
306
319
  } else {
307
- outputText += `\n\n[Showing lines ${startLine}-${endLine} of ${truncation.totalLines} (${formatSize(DEFAULT_MAX_BYTES)} limit). Full output: ${tempFilePath}]`;
320
+ outputText += `\n\n[Showing lines ${startLine}-${endLine} of ${truncation.totalLines} (${formatSize(DEFAULT_MAX_BYTES)} limit). Full output: ${outputRef}]`;
308
321
  }
309
322
  }
310
323
 
@@ -317,8 +330,8 @@ export function createBashTool(cwd: string, options?: BashToolOptions): AgentToo
317
330
  })
318
331
  .catch((err: Error) => {
319
332
  // Close temp file stream
320
- if (tempFileStream) {
321
- tempFileStream.end();
333
+ if (spillFileStream) {
334
+ spillFileStream.end();
322
335
  }
323
336
 
324
337
  // Combine all buffered chunks for error output
@@ -198,6 +198,9 @@ export {
198
198
  type SessionMessageEntry,
199
199
  type ThinkingLevelChangeEntry,
200
200
  } from "./core/session-manager.js";
201
+ // Blob and artifact storage
202
+ export { BlobStore, isBlobRef, parseBlobRef, externalizeImageData, resolveImageData } from "./core/blob-store.js";
203
+ export { ArtifactManager } from "./core/artifact-manager.js";
201
204
  export {
202
205
  type CompactionSettings,
203
206
  type ImageSettings,
@@ -176,7 +176,7 @@ export class InteractiveMode {
176
176
  private pendingTools = new Map<string, ToolExecutionComponent>();
177
177
 
178
178
  // Tool output expansion state
179
- private toolOutputExpanded = false;
179
+ private toolOutputExpanded = true;
180
180
 
181
181
  // Thinking block visibility state
182
182
  private hideThinkingBlock = false;
@@ -0,0 +1,121 @@
1
+ # Change Log
2
+
3
+ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
4
+
5
+ ### [7.0.4](https://www.github.com/yargs/cliui/compare/v7.0.3...v7.0.4) (2020-11-08)
6
+
7
+
8
+ ### Bug Fixes
9
+
10
+ * **deno:** import UIOptions from definitions ([#97](https://www.github.com/yargs/cliui/issues/97)) ([f04f343](https://www.github.com/yargs/cliui/commit/f04f3439bc78114c7e90f82ff56f5acf16268ea8))
11
+
12
+ ### [7.0.3](https://www.github.com/yargs/cliui/compare/v7.0.2...v7.0.3) (2020-10-16)
13
+
14
+
15
+ ### Bug Fixes
16
+
17
+ * **exports:** node 13.0 and 13.1 require the dotted object form _with_ a string fallback ([#93](https://www.github.com/yargs/cliui/issues/93)) ([eca16fc](https://www.github.com/yargs/cliui/commit/eca16fc05d26255df3280906c36d7f0e5b05c6e9))
18
+
19
+ ### [7.0.2](https://www.github.com/yargs/cliui/compare/v7.0.1...v7.0.2) (2020-10-14)
20
+
21
+
22
+ ### Bug Fixes
23
+
24
+ * **exports:** node 13.0-13.6 require a string fallback ([#91](https://www.github.com/yargs/cliui/issues/91)) ([b529d7e](https://www.github.com/yargs/cliui/commit/b529d7e432901af1af7848b23ed6cf634497d961))
25
+
26
+ ### [7.0.1](https://www.github.com/yargs/cliui/compare/v7.0.0...v7.0.1) (2020-08-16)
27
+
28
+
29
+ ### Bug Fixes
30
+
31
+ * **build:** main should be build/index.cjs ([dc29a3c](https://www.github.com/yargs/cliui/commit/dc29a3cc617a410aa850e06337b5954b04f2cb4d))
32
+
33
+ ## [7.0.0](https://www.github.com/yargs/cliui/compare/v6.0.0...v7.0.0) (2020-08-16)
34
+
35
+
36
+ ### ⚠ BREAKING CHANGES
37
+
38
+ * tsc/ESM/Deno support (#82)
39
+ * modernize deps and build (#80)
40
+
41
+ ### Build System
42
+
43
+ * modernize deps and build ([#80](https://www.github.com/yargs/cliui/issues/80)) ([339d08d](https://www.github.com/yargs/cliui/commit/339d08dc71b15a3928aeab09042af94db2f43743))
44
+
45
+
46
+ ### Code Refactoring
47
+
48
+ * tsc/ESM/Deno support ([#82](https://www.github.com/yargs/cliui/issues/82)) ([4b777a5](https://www.github.com/yargs/cliui/commit/4b777a5fe01c5d8958c6708695d6aab7dbe5706c))
49
+
50
+ ## [6.0.0](https://www.github.com/yargs/cliui/compare/v5.0.0...v6.0.0) (2019-11-10)
51
+
52
+
53
+ ### ⚠ BREAKING CHANGES
54
+
55
+ * update deps, drop Node 6
56
+
57
+ ### Code Refactoring
58
+
59
+ * update deps, drop Node 6 ([62056df](https://www.github.com/yargs/cliui/commit/62056df))
60
+
61
+ ## [5.0.0](https://github.com/yargs/cliui/compare/v4.1.0...v5.0.0) (2019-04-10)
62
+
63
+
64
+ ### Bug Fixes
65
+
66
+ * Update wrap-ansi to fix compatibility with latest versions of chalk. ([#60](https://github.com/yargs/cliui/issues/60)) ([7bf79ae](https://github.com/yargs/cliui/commit/7bf79ae))
67
+
68
+
69
+ ### BREAKING CHANGES
70
+
71
+ * Drop support for node < 6.
72
+
73
+
74
+
75
+ <a name="4.1.0"></a>
76
+ ## [4.1.0](https://github.com/yargs/cliui/compare/v4.0.0...v4.1.0) (2018-04-23)
77
+
78
+
79
+ ### Features
80
+
81
+ * add resetOutput method ([#57](https://github.com/yargs/cliui/issues/57)) ([7246902](https://github.com/yargs/cliui/commit/7246902))
82
+
83
+
84
+
85
+ <a name="4.0.0"></a>
86
+ ## [4.0.0](https://github.com/yargs/cliui/compare/v3.2.0...v4.0.0) (2017-12-18)
87
+
88
+
89
+ ### Bug Fixes
90
+
91
+ * downgrades strip-ansi to version 3.0.1 ([#54](https://github.com/yargs/cliui/issues/54)) ([5764c46](https://github.com/yargs/cliui/commit/5764c46))
92
+ * set env variable FORCE_COLOR. ([#56](https://github.com/yargs/cliui/issues/56)) ([7350e36](https://github.com/yargs/cliui/commit/7350e36))
93
+
94
+
95
+ ### Chores
96
+
97
+ * drop support for node < 4 ([#53](https://github.com/yargs/cliui/issues/53)) ([b105376](https://github.com/yargs/cliui/commit/b105376))
98
+
99
+
100
+ ### Features
101
+
102
+ * add fallback for window width ([#45](https://github.com/yargs/cliui/issues/45)) ([d064922](https://github.com/yargs/cliui/commit/d064922))
103
+
104
+
105
+ ### BREAKING CHANGES
106
+
107
+ * officially drop support for Node < 4
108
+
109
+
110
+
111
+ <a name="3.2.0"></a>
112
+ ## [3.2.0](https://github.com/yargs/cliui/compare/v3.1.2...v3.2.0) (2016-04-11)
113
+
114
+
115
+ ### Bug Fixes
116
+
117
+ * reduces tarball size ([acc6c33](https://github.com/yargs/cliui/commit/acc6c33))
118
+
119
+ ### Features
120
+
121
+ * adds standard-version for release management ([ff84e32](https://github.com/yargs/cliui/commit/ff84e32))