@thegitai/cli 1.0.0-preview.1 → 1.0.0-preview.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.
package/dist/bin/ai.js CHANGED
@@ -18,6 +18,7 @@ import { formatVersionLine } from '../src/version.js';
18
18
  import { parseArgs } from '../src/cli-args.js';
19
19
  import { getJobBufferedOutput, killBackgroundJob, killAllBackgroundJobs, listBackgroundJobs, setBackgroundJobSession, } from '../src/background-jobs.js';
20
20
  import { collectBackgroundJobUiKillMutations, collectBackgroundJobUiOutputMutations, } from '../src/tool-executor.js';
21
+ import { setScratchSession } from '../src/scratch-dir.js';
21
22
  const DEFAULT_SERVER_URL = 'https://thegit.ai';
22
23
  const { auth, chat, models, sessions } = ServerApi;
23
24
  function printUsage() {
@@ -318,6 +319,7 @@ async function mainInteractive({ authConfig, projectIndex, serverModels, serverS
318
319
  }
319
320
  applySessionSnapshot(session, snapshot);
320
321
  setBackgroundJobSession(session.sessionId);
322
+ setScratchSession(session.sessionId);
321
323
  await saveSessionBoth({ session, serverSessionClient });
322
324
  console.log(chalk.dim(`Resumed session${session.sessionName ? ` "${session.sessionName}"` : ''} (${session.sessionId})\n`));
323
325
  continue;
@@ -454,6 +456,7 @@ export async function main() {
454
456
  applySessionSnapshot(session, sourceSnapshot);
455
457
  await saveSessionBoth({ session, serverSessionClient });
456
458
  }
459
+ setScratchSession(session.sessionId);
457
460
  const projectIndex = createIndex({
458
461
  rootDir,
459
462
  onStatus: (message) => {
@@ -503,6 +506,7 @@ export async function main() {
503
506
  finally {
504
507
  killAllBackgroundJobs({ sessionId: session.sessionId, remove: true });
505
508
  setBackgroundJobSession(null);
509
+ setScratchSession(null);
506
510
  }
507
511
  }
508
512
  main().catch((error) => {
@@ -4,6 +4,7 @@ import { applySessionSnapshot, snapshotFromSession, } from '../session-store.js'
4
4
  import { executeLocalToolCall } from '../tool-executor.js';
5
5
  import { createTraceContext, normalizeServerUrl, readErrorResponse, } from './http.js';
6
6
  import { collectClientEnvironment } from '../client-environment.js';
7
+ import { collectProjectOrientation } from '../project-orientation.js';
7
8
  import { autoAttachImages } from '../core/image-path-extractor.js';
8
9
  export class TurnCancelledError extends Error {
9
10
  name = 'TurnCancelledError';
@@ -339,6 +340,7 @@ export async function sendServerUserMessage({ config, projectIndex, session, inp
339
340
  input: requestInputBase,
340
341
  backgroundJobUpdate: backgroundJobUpdate || undefined,
341
342
  clientEnvironment: collectClientEnvironment({ env: session.env }),
343
+ projectOrientation: collectProjectOrientation(session.rootDir) ?? undefined,
342
344
  imageAttachments: imageAttachmentsForServer(requestImageAttachments),
343
345
  maxToolSteps: session.maxToolSteps,
344
346
  autoYes: session.autoYes,
@@ -1,8 +1,9 @@
1
1
  import chalk from './colors.js';
2
- import { existsSync, lstatSync, mkdirSync, readFileSync, unlinkSync, writeFileSync, } from 'fs';
2
+ import { chmodSync, closeSync, constants, existsSync, fchmodSync, fstatSync, ftruncateSync, lstatSync, mkdirSync, openSync, readFileSync, realpathSync, unlinkSync, writeFileSync, } from 'fs';
3
3
  import path from 'path';
4
4
  import { createInterface } from 'readline';
5
5
  import { runCommand } from './executor.js';
6
+ import { ensureSessionScratchDir, isInsideTheGitAiScratch, isWithinSessionScratchDir, } from './scratch-dir.js';
6
7
  import { isTuiMode } from './runtime-mode.js';
7
8
  import { truncate } from './utils.js';
8
9
  function parseUnifiedDiff(patchText) {
@@ -143,17 +144,92 @@ export function renderDiffPreview(filePath, patchText) {
143
144
  function normalizeRoot(rootDir) {
144
145
  return path.resolve(rootDir);
145
146
  }
146
- export function resolveProjectPath(rootDir, filePath) {
147
+ function expandScratchPath(filePath) {
148
+ const match = filePath.match(/^(?:\$THEGITAI_SCRATCH_DIR|\$\{THEGITAI_SCRATCH_DIR\})(?:[\\/](.*))?$/);
149
+ if (!match)
150
+ return filePath;
151
+ const root = ensureSessionScratchDir();
152
+ return match[1] ? path.join(root, match[1]) : root;
153
+ }
154
+ export function classifyProjectPath(rootDir, filePath) {
147
155
  const absRoot = normalizeRoot(rootDir);
148
- const absPath = path.resolve(absRoot, filePath);
156
+ const absPath = path.resolve(absRoot, expandScratchPath(filePath));
157
+ if (isWithinSessionScratchDir(absPath)) {
158
+ return absPath !== path.resolve(ensureSessionScratchDir()) &&
159
+ isInsideTheGitAiScratch(absPath)
160
+ ? 'scratch'
161
+ : 'outside';
162
+ }
149
163
  const relative = path.relative(absRoot, absPath);
150
- if (relative.startsWith('..') || path.isAbsolute(relative)) {
151
- throw new Error(`Refusing to access path outside the project root: ${filePath}`);
164
+ if (!relative.startsWith('..') && !path.isAbsolute(relative)) {
165
+ return 'project';
166
+ }
167
+ return 'outside';
168
+ }
169
+ export function resolveProjectPath(rootDir, filePath) {
170
+ const absRoot = normalizeRoot(rootDir);
171
+ const absPath = path.resolve(absRoot, expandScratchPath(filePath));
172
+ if (classifyProjectPath(rootDir, filePath) === 'outside') {
173
+ throw new Error(`Refusing to access path outside the project root: ${filePath}. Allowed locations are the project root and the session scratch directory ($THEGITAI_SCRATCH_DIR).`);
152
174
  }
153
175
  return absPath;
154
176
  }
177
+ function mkdirForWrite(absPath, scratchPath) {
178
+ const parent = path.dirname(absPath);
179
+ if (!scratchPath) {
180
+ mkdirSync(parent, { recursive: true });
181
+ return;
182
+ }
183
+ const scratchRoot = path.resolve(ensureSessionScratchDir());
184
+ const relative = path.relative(scratchRoot, parent);
185
+ if (relative.startsWith('..') || path.isAbsolute(relative)) {
186
+ throw new Error(`Refusing to create a directory outside the session scratch root: ${parent}`);
187
+ }
188
+ let current = scratchRoot;
189
+ for (const segment of relative.split(path.sep).filter(Boolean)) {
190
+ current = path.join(current, segment);
191
+ try {
192
+ mkdirSync(current, { mode: 0o700 });
193
+ }
194
+ catch (error) {
195
+ if (error?.code !== 'EEXIST')
196
+ throw error;
197
+ }
198
+ const stat = lstatSync(current);
199
+ if (stat.isSymbolicLink() || !stat.isDirectory()) {
200
+ throw new Error(`Refusing to traverse unsafe scratch directory: ${current}`);
201
+ }
202
+ if (process.platform !== 'win32')
203
+ chmodSync(current, 0o700);
204
+ }
205
+ }
206
+ function writeScratchFile(absPath, content) {
207
+ const scratchRoot = realpathSync(ensureSessionScratchDir());
208
+ const parent = realpathSync(path.dirname(absPath));
209
+ const relativeParent = path.relative(scratchRoot, parent);
210
+ if (relativeParent.startsWith('..') || path.isAbsolute(relativeParent)) {
211
+ throw new Error(`Refusing to write through an unsafe scratch directory: ${absPath}`);
212
+ }
213
+ const verifiedPath = path.join(parent, path.basename(absPath));
214
+ const noFollow = typeof constants.O_NOFOLLOW === 'number' ? constants.O_NOFOLLOW : 0;
215
+ const fd = openSync(verifiedPath, constants.O_WRONLY | constants.O_CREAT | noFollow, 0o600);
216
+ try {
217
+ const stat = fstatSync(fd);
218
+ if (!stat.isFile() || stat.nlink > 1) {
219
+ throw new Error(`Refusing to write an unsafe scratch file: ${absPath}`);
220
+ }
221
+ ftruncateSync(fd, 0);
222
+ if (process.platform !== 'win32')
223
+ fchmodSync(fd, 0o600);
224
+ writeFileSync(fd, content);
225
+ }
226
+ finally {
227
+ closeSync(fd);
228
+ }
229
+ }
155
230
  export function writeProjectFile(rootDir, filePath, content) {
156
231
  const absPath = resolveProjectPath(rootDir, filePath);
232
+ const scratchPath = classifyProjectPath(rootDir, filePath) === 'scratch';
157
233
  if (existsSync(absPath)) {
158
234
  try {
159
235
  const existingContent = readFileSync(absPath, 'utf-8');
@@ -164,12 +240,18 @@ export function writeProjectFile(rootDir, filePath, content) {
164
240
  catch {
165
241
  }
166
242
  }
167
- mkdirSync(path.dirname(absPath), { recursive: true });
168
- writeFileSync(absPath, content, 'utf-8');
243
+ mkdirForWrite(absPath, scratchPath);
244
+ if (scratchPath) {
245
+ writeScratchFile(absPath, content);
246
+ }
247
+ else {
248
+ writeFileSync(absPath, content, 'utf-8');
249
+ }
169
250
  return { absPath, changed: true };
170
251
  }
171
252
  export function writeProjectFileBuffer(rootDir, filePath, content) {
172
253
  const absPath = resolveProjectPath(rootDir, filePath);
254
+ const scratchPath = classifyProjectPath(rootDir, filePath) === 'scratch';
173
255
  if (existsSync(absPath)) {
174
256
  try {
175
257
  const existingContent = readFileSync(absPath);
@@ -180,8 +262,13 @@ export function writeProjectFileBuffer(rootDir, filePath, content) {
180
262
  catch {
181
263
  }
182
264
  }
183
- mkdirSync(path.dirname(absPath), { recursive: true });
184
- writeFileSync(absPath, content);
265
+ mkdirForWrite(absPath, scratchPath);
266
+ if (scratchPath) {
267
+ writeScratchFile(absPath, content);
268
+ }
269
+ else {
270
+ writeFileSync(absPath, content);
271
+ }
185
272
  return { absPath, changed: true };
186
273
  }
187
274
  export function deleteProjectFile(rootDir, filePath) {
@@ -0,0 +1,99 @@
1
+ import { closeSync, constants, fstatSync, lstatSync, openSync, readdirSync, readFileSync, } from 'node:fs';
2
+ import path from 'node:path';
3
+ import { shouldIgnoreArtifactPath } from './artifact-policy.js';
4
+ const MAX_TOP_LEVEL_ENTRIES = 80;
5
+ const MAX_DECLARED_TASKS = 40;
6
+ const MAX_NAME_CHARS = 120;
7
+ const MAX_PACKAGE_JSON_BYTES = 1024 * 1024;
8
+ function normalizeName(value) {
9
+ if (typeof value !== 'string')
10
+ return null;
11
+ const normalized = value
12
+ .replace(/[\u0000-\u001f\u007f]/g, ' ')
13
+ .replace(/\s+/g, ' ')
14
+ .trim()
15
+ .slice(0, MAX_NAME_CHARS);
16
+ return normalized || null;
17
+ }
18
+ function normalizeNames(value, limit) {
19
+ if (!Array.isArray(value))
20
+ return [];
21
+ const names = [];
22
+ const seen = new Set();
23
+ for (const item of value) {
24
+ const name = normalizeName(item);
25
+ if (!name || seen.has(name))
26
+ continue;
27
+ seen.add(name);
28
+ names.push(name);
29
+ if (names.length >= limit)
30
+ break;
31
+ }
32
+ return names;
33
+ }
34
+ function readPackageTasks(rootDir) {
35
+ const packagePath = path.join(rootDir, 'package.json');
36
+ let fd = null;
37
+ try {
38
+ const pathStat = lstatSync(packagePath);
39
+ if (pathStat.isSymbolicLink() ||
40
+ !pathStat.isFile() ||
41
+ pathStat.nlink > 1 ||
42
+ pathStat.size > MAX_PACKAGE_JSON_BYTES) {
43
+ return [];
44
+ }
45
+ const noFollow = typeof constants.O_NOFOLLOW === 'number' ? constants.O_NOFOLLOW : 0;
46
+ fd = openSync(packagePath, constants.O_RDONLY | noFollow);
47
+ const fileStat = fstatSync(fd);
48
+ if (!fileStat.isFile() ||
49
+ fileStat.nlink > 1 ||
50
+ fileStat.size > MAX_PACKAGE_JSON_BYTES ||
51
+ fileStat.dev !== pathStat.dev ||
52
+ fileStat.ino !== pathStat.ino) {
53
+ return [];
54
+ }
55
+ const parsed = JSON.parse(readFileSync(fd, 'utf8'));
56
+ if (!parsed?.scripts || typeof parsed.scripts !== 'object')
57
+ return [];
58
+ return normalizeNames(Object.keys(parsed.scripts), MAX_DECLARED_TASKS);
59
+ }
60
+ catch {
61
+ return [];
62
+ }
63
+ finally {
64
+ if (fd !== null)
65
+ closeSync(fd);
66
+ }
67
+ }
68
+ export function normalizeProjectOrientation(value) {
69
+ if (!value || typeof value !== 'object')
70
+ return null;
71
+ const record = value;
72
+ const topLevelEntries = normalizeNames(record.topLevelEntries, MAX_TOP_LEVEL_ENTRIES);
73
+ const packageScripts = normalizeNames(record.packageScripts, MAX_DECLARED_TASKS);
74
+ if (topLevelEntries.length === 0 && packageScripts.length === 0)
75
+ return null;
76
+ return {
77
+ topLevelEntries,
78
+ packageScripts,
79
+ truncated: record.truncated === true,
80
+ };
81
+ }
82
+ export function collectProjectOrientation(rootDir) {
83
+ try {
84
+ const entries = readdirSync(rootDir, { withFileTypes: true })
85
+ .filter((entry) => !shouldIgnoreArtifactPath(entry.name))
86
+ .sort((a, b) => a.name.localeCompare(b.name));
87
+ const topLevelEntries = entries
88
+ .slice(0, MAX_TOP_LEVEL_ENTRIES)
89
+ .map((entry) => `${entry.name}${entry.isDirectory() ? '/' : ''}`);
90
+ return normalizeProjectOrientation({
91
+ topLevelEntries,
92
+ packageScripts: readPackageTasks(rootDir),
93
+ truncated: entries.length > MAX_TOP_LEVEL_ENTRIES,
94
+ });
95
+ }
96
+ catch {
97
+ return null;
98
+ }
99
+ }
@@ -1,57 +1,75 @@
1
- import { chmodSync, lstatSync, mkdirSync, mkdtempSync } from 'node:fs';
1
+ import { chmodSync, lstatSync, mkdtempSync } from 'node:fs';
2
2
  import os from 'node:os';
3
3
  import path from 'node:path';
4
- let cachedScratchDir = null;
4
+ const scratchDirs = new Map();
5
+ let activeSessionId = 'default';
6
+ export function setScratchSession(sessionId) {
7
+ activeSessionId = String(sessionId ?? '').trim() || 'default';
8
+ }
9
+ function allocateSessionScratchDir() {
10
+ const dir = mkdtempSync(path.join(os.tmpdir(), 'thegitai-'));
11
+ scratchDirs.set(activeSessionId, dir);
12
+ return dir;
13
+ }
5
14
  export function sessionScratchDir() {
6
- if (!cachedScratchDir) {
7
- cachedScratchDir = path.join(os.tmpdir(), `thegitai-${process.pid}`);
8
- }
9
- return cachedScratchDir;
15
+ return scratchDirs.get(activeSessionId) ?? allocateSessionScratchDir();
10
16
  }
11
- function isSquattedScratchRoot(dir) {
17
+ function isOwnedDirectory(dir) {
12
18
  try {
13
19
  const st = lstatSync(dir);
14
20
  if (st.isSymbolicLink() || !st.isDirectory())
15
- return true;
21
+ return false;
16
22
  if (process.platform !== 'win32' &&
17
23
  typeof process.getuid === 'function' &&
18
24
  st.uid !== process.getuid()) {
19
- return true;
25
+ return false;
20
26
  }
21
- return false;
27
+ return true;
22
28
  }
23
29
  catch {
24
30
  return false;
25
31
  }
26
32
  }
27
33
  export function ensureSessionScratchDir() {
28
- const dir = sessionScratchDir();
29
- try {
30
- if (isSquattedScratchRoot(dir)) {
31
- cachedScratchDir = mkdtempSync(path.join(os.tmpdir(), 'thegitai-'));
32
- return cachedScratchDir;
34
+ let dir = sessionScratchDir();
35
+ if (!isOwnedDirectory(dir)) {
36
+ dir = allocateSessionScratchDir();
37
+ }
38
+ if (process.platform !== 'win32') {
39
+ chmodSync(dir, 0o700);
40
+ }
41
+ return dir;
42
+ }
43
+ function hasUnsafeScratchComponent(root, relativePath) {
44
+ let current = root;
45
+ for (const segment of relativePath.split(path.sep).filter(Boolean)) {
46
+ current = path.join(current, segment);
47
+ try {
48
+ const stat = lstatSync(current);
49
+ if (stat.isSymbolicLink())
50
+ return true;
51
+ if (stat.isDirectory())
52
+ continue;
53
+ if (!stat.isFile() || stat.nlink > 1)
54
+ return true;
33
55
  }
34
- mkdirSync(dir, { recursive: true, mode: 0o700 });
35
- if (process.platform !== 'win32') {
36
- chmodSync(dir, 0o700);
56
+ catch (error) {
57
+ return error?.code !== 'ENOENT';
37
58
  }
38
59
  }
39
- catch {
40
- }
41
- return sessionScratchDir();
60
+ return false;
61
+ }
62
+ export function isWithinSessionScratchDir(absPath) {
63
+ const root = path.resolve(ensureSessionScratchDir());
64
+ const resolved = path.resolve(absPath);
65
+ const relative = path.relative(root, resolved);
66
+ return !relative.startsWith('..') && !path.isAbsolute(relative);
42
67
  }
43
68
  export function isInsideTheGitAiScratch(absPath) {
44
- const tempRoot = path.resolve(os.tmpdir());
45
- const relPath = path.relative(tempRoot, path.resolve(absPath));
46
- if (!relPath || relPath.startsWith('..') || path.isAbsolute(relPath)) {
47
- return false;
48
- }
49
- const first = relPath.split(/[\\/]/, 1)[0] ?? '';
50
- if (!/^thegitai(?:$|[-.])/i.test(first)) {
51
- return false;
52
- }
53
- if (/[*?[\]{}]/.test(first)) {
69
+ const root = path.resolve(ensureSessionScratchDir());
70
+ const resolved = path.resolve(absPath);
71
+ const relative = path.relative(root, resolved);
72
+ if (!isWithinSessionScratchDir(resolved))
54
73
  return false;
55
- }
56
- return !isSquattedScratchRoot(path.join(tempRoot, first));
74
+ return !hasUnsafeScratchComponent(root, relative);
57
75
  }
@@ -2,6 +2,7 @@ import { drainBackgroundJobNotifications, getBackgroundJob, } from './background
2
2
  import { canStoreEditSnapshot, isEditToolName, isGitWorkTree, MAX_EDIT_JOURNAL_RECORDS, operationFromSnapshots, readFileEditSnapshot, } from './edit-journal.js';
3
3
  import { clearEditFailure, collectCommandMutations, captureMutationBaseline, ensureActiveCheckpoint, recordEditFailure, recordSessionEdit, rememberCheckpointFiles, } from './session-safety.js';
4
4
  import { buildAgentModeToolBlockedResult, } from './agent-mode.js';
5
+ import { classifyProjectPath } from './patcher.js';
5
6
  import { dispatchTool } from './tools/index.js';
6
7
  import { syncIndexFromDisk } from './project-index.js';
7
8
  import { PATH_REPAIRING_EDIT_TOOLS, repairFilePath } from './tools/path-suggest.js';
@@ -84,9 +85,38 @@ async function collectTrackedCommandMutations({ session, projectIndex, result, t
84
85
  invalidateShellDiagnosticsCache(session.rootDir);
85
86
  rememberCheckpointFiles(session.clientState.safety, session.rootDir, records.map((record) => record.filePath), turnId);
86
87
  const priorSync = result.repoSync;
87
- if (!(priorSync &&
88
- (priorSync.added || priorSync.modified || priorSync.removed))) {
89
- result.repoSync = await syncIndexFromDisk(projectIndex);
88
+ if (!(priorSync && (priorSync.added || priorSync.modified || priorSync.removed))) {
89
+ const mutationCounts = {
90
+ added: records.filter((record) => record.operation === 'create').length,
91
+ modified: records.filter((record) => record.operation === 'update').length,
92
+ removed: records.filter((record) => record.operation === 'delete').length,
93
+ indexedChunks: 0,
94
+ retrievalTokensUsed: 0,
95
+ };
96
+ if (priorSync?.error) {
97
+ result.repoSync = {
98
+ ...mutationCounts,
99
+ indexSyncError: priorSync.error,
100
+ skipped: true,
101
+ reason: 'local index sync failed after command execution',
102
+ };
103
+ }
104
+ else {
105
+ try {
106
+ const repoSync = await syncIndexFromDisk(projectIndex);
107
+ result.repoSync = {
108
+ ...repoSync,
109
+ added: Math.max(repoSync.added, mutationCounts.added),
110
+ modified: Math.max(repoSync.modified, mutationCounts.modified),
111
+ removed: Math.max(repoSync.removed, mutationCounts.removed),
112
+ };
113
+ }
114
+ catch (error) {
115
+ const indexSyncError = error instanceof Error ? error.message : String(error);
116
+ result.repoSync = { ...mutationCounts, indexSyncError };
117
+ session.onStatus(`Repository changes were recorded, but local index sync failed: ${indexSyncError}`);
118
+ }
119
+ }
90
120
  }
91
121
  result.sessionEdits = records.map((record) => ({
92
122
  id: record.id,
@@ -150,7 +180,7 @@ export async function collectBackgroundJobUiOutputMutations({ session, projectIn
150
180
  }
151
181
  }
152
182
  function recordAssistantEdit(session, call, result, before) {
153
- if (!before || !isEditToolName(call.name))
183
+ if (!before || !isEditToolName(call.name) || result?.scratch === true)
154
184
  return;
155
185
  if (!result || typeof result !== 'object' || result.ok !== true) {
156
186
  const filePath = getEditToolFilePath(call);
@@ -231,10 +261,12 @@ export async function executeLocalToolCall(toolContext, session, call) {
231
261
  !editToolWritesSeparateOutput(call)
232
262
  ? repairFilePath(session.rootDir, rawEditFilePath)
233
263
  : rawEditFilePath;
234
- if (filePathBeforeEdit) {
264
+ const tracksRepositoryEdit = filePathBeforeEdit &&
265
+ classifyProjectPath(session.rootDir, filePathBeforeEdit) === 'project';
266
+ if (tracksRepositoryEdit) {
235
267
  rememberCheckpointFiles(session.clientState.safety, session.rootDir, [filePathBeforeEdit], session.turnState.id);
236
268
  }
237
- const beforeEditSnapshot = filePathBeforeEdit
269
+ const beforeEditSnapshot = tracksRepositoryEdit
238
270
  ? readFileEditSnapshot(session.rootDir, filePathBeforeEdit)
239
271
  : null;
240
272
  const commandTracker = call.name === 'run_command' || call.name === 'run_node_script'
@@ -1,5 +1,5 @@
1
1
  import chalk from '../colors.js';
2
- import { deleteProjectFile } from '../patcher.js';
2
+ import { classifyProjectPath, deleteProjectFile } from '../patcher.js';
3
3
  import { isTuiMode } from '../runtime-mode.js';
4
4
  import { removeIndexFile } from '../project-index.js';
5
5
  import { invalidateShellDiagnosticsCache, runShellDiagnostics, } from './shell-diagnostics.js';
@@ -9,10 +9,27 @@ export async function deleteFile(context, args) {
9
9
  if (!filePath) {
10
10
  return { ok: false, error: 'filePath is required' };
11
11
  }
12
+ const pathKind = classifyProjectPath(rootDir, filePath);
13
+ if (pathKind === 'outside') {
14
+ return {
15
+ ok: false,
16
+ filePath,
17
+ error: `Refusing to delete outside the project root: ${filePath}. Deletable locations are the project root and the session scratch directory ($THEGITAI_SCRATCH_DIR).`,
18
+ failureCategory: 'invalid_argument',
19
+ failureDetails: {
20
+ category: 'invalid_argument',
21
+ tool: 'delete_file',
22
+ action: 'Delete files inside the project root, or use an absolute path under the session scratch directory ($THEGITAI_SCRATCH_DIR).',
23
+ },
24
+ };
25
+ }
26
+ const scratchPath = pathKind === 'scratch';
12
27
  const result = deleteProjectFile(rootDir, filePath);
13
28
  if (result.deleted) {
14
- await removeIndexFile(projectIndex, filePath);
15
- invalidateShellDiagnosticsCache(rootDir, filePath);
29
+ if (!scratchPath) {
30
+ await removeIndexFile(projectIndex, filePath);
31
+ invalidateShellDiagnosticsCache(rootDir, filePath);
32
+ }
16
33
  if (!isTuiMode())
17
34
  console.log(chalk.red(` 🗑️ Deleted: ${filePath}`));
18
35
  }
@@ -21,7 +38,8 @@ export async function deleteFile(context, args) {
21
38
  filePath,
22
39
  changed: result.deleted,
23
40
  deleted: result.deleted,
41
+ ...(scratchPath ? { scratch: true } : {}),
24
42
  content: result.content,
25
- diagnostics: result.deleted ? runShellDiagnostics(rootDir) : undefined,
43
+ diagnostics: result.deleted && !scratchPath ? runShellDiagnostics(rootDir) : undefined,
26
44
  };
27
45
  }
@@ -1,7 +1,7 @@
1
1
  import chalk from '../colors.js';
2
2
  import path from 'node:path';
3
3
  import { normalizeProjectRelativePath } from '../artifact-policy.js';
4
- import { applyUnifiedPatch, readProjectFile, renderDiffPreview, writeProjectFile, } from '../patcher.js';
4
+ import { applyUnifiedPatch, classifyProjectPath, readProjectFile, renderDiffPreview, writeProjectFile, } from '../patcher.js';
5
5
  import { upsertIndexFile } from '../project-index.js';
6
6
  import { repairFilePath } from './path-suggest.js';
7
7
  import { isTuiMode } from '../runtime-mode.js';
@@ -29,6 +29,21 @@ export async function patchFile(context, args) {
29
29
  failureCategory: 'invalid_argument',
30
30
  };
31
31
  }
32
+ const pathKind = classifyProjectPath(rootDir, filePath);
33
+ if (pathKind === 'outside') {
34
+ return {
35
+ ok: false,
36
+ filePath,
37
+ error: `Refusing to edit outside the project root: ${filePath}. Editable locations are the project root and the session scratch directory ($THEGITAI_SCRATCH_DIR).`,
38
+ failureCategory: 'invalid_argument',
39
+ failureDetails: {
40
+ category: 'invalid_argument',
41
+ tool: 'patch_file',
42
+ action: 'Edit files inside the project root, or use an absolute path under the session scratch directory ($THEGITAI_SCRATCH_DIR) for temporary files.',
43
+ },
44
+ };
45
+ }
46
+ const scratchPath = pathKind === 'scratch';
32
47
  let originalContent;
33
48
  try {
34
49
  originalContent = readProjectFile(rootDir, filePath);
@@ -68,20 +83,29 @@ export async function patchFile(context, args) {
68
83
  ok: false,
69
84
  skipped: true,
70
85
  filePath,
71
- error: 'User declined patch',
86
+ failureCategory: 'user_declined',
87
+ failureDetails: {
88
+ category: 'user_declined',
89
+ tool: 'patch_file',
90
+ action: 'Respect the real user’s decision. Do not retry the same or an equivalent edit; reconsider the approach or ask one specific question if needed.',
91
+ },
92
+ error: 'The real user rejected this proposed patch. Nothing was changed; this was not a tool failure or an automated system skip.',
72
93
  };
73
94
  }
74
95
  }
75
96
  const { changed } = writeProjectFile(rootDir, filePath, patchedContent);
76
97
  let indexedChunks = 0;
77
98
  let retrievalTokensUsed = 0;
78
- if (changed) {
99
+ if (changed && !scratchPath) {
79
100
  const indexResult = await upsertIndexFile(projectIndex, filePath);
80
101
  indexedChunks = indexResult.indexedChunks;
81
102
  retrievalTokensUsed = indexResult.retrievalTokensUsed ?? 0;
82
103
  }
83
- invalidateShellDiagnosticsCache(rootDir, filePath);
84
- const diagnostics = runShellDiagnostics(rootDir, filePath);
104
+ let diagnostics;
105
+ if (!scratchPath) {
106
+ invalidateShellDiagnosticsCache(rootDir, filePath);
107
+ diagnostics = runShellDiagnostics(rootDir, filePath);
108
+ }
85
109
  const originalLines = originalContent.split('\n').length;
86
110
  const patchedLines = patchedContent.split('\n').length;
87
111
  if (!isTuiMode()) {
@@ -94,6 +118,7 @@ export async function patchFile(context, args) {
94
118
  filePath,
95
119
  changed,
96
120
  operation: 'patch',
121
+ ...(scratchPath ? { scratch: true } : {}),
97
122
  indexedChunks,
98
123
  retrievalTokensUsed,
99
124
  bytesWritten: Buffer.byteLength(patchedContent, 'utf-8'),
@@ -3,6 +3,7 @@ import path from 'path';
3
3
  import { normalizeProjectRelativePath, shouldIgnoreArtifactPath, } from '../artifact-policy.js';
4
4
  import { buildSecretFilePreview, isDotenvLikePath, looksLikeEditableDotenv, shouldUseSecretFilePreview, } from '../secret-preview.js';
5
5
  import { readProjectFile } from '../patcher.js';
6
+ import { isWithinSessionScratchDir } from '../scratch-dir.js';
6
7
  import { repairFilePath } from './path-suggest.js';
7
8
  import { dotenvFitsRedactionBudget, getCurrentFileHash, recordReadCoverage, redactContentWithStableTokens, redactDotenvWithStableTokens, } from '../session-safety.js';
8
9
  import { readFileRange, truncate } from '../utils.js';
@@ -16,6 +17,7 @@ export async function readFile(context, args) {
16
17
  return { ok: false, error: 'filePath is required' };
17
18
  }
18
19
  const projectPath = normalizeProjectRelativePath(rootDir, filePath);
20
+ const scratchPath = path.isAbsolute(filePath) && isWithinSessionScratchDir(filePath);
19
21
  if (!projectPath && !path.isAbsolute(filePath)) {
20
22
  return {
21
23
  ok: false,
@@ -37,7 +39,7 @@ export async function readFile(context, args) {
37
39
  };
38
40
  }
39
41
  let content;
40
- if (projectPath) {
42
+ if (projectPath || scratchPath) {
41
43
  try {
42
44
  content = readProjectFile(rootDir, filePath);
43
45
  }
@@ -188,7 +188,13 @@ export async function replaceDocumentText(context, args) {
188
188
  ok: false,
189
189
  skipped: true,
190
190
  filePath: targetPath,
191
- error: 'User declined replace_document_text',
191
+ failureCategory: 'user_declined',
192
+ failureDetails: {
193
+ category: 'user_declined',
194
+ tool: 'replace_document_text',
195
+ action: 'Respect the real user’s decision. Do not retry the same or an equivalent edit; reconsider the approach or ask one specific question if needed.',
196
+ },
197
+ error: 'The real user rejected this proposed document edit. Nothing was changed; this was not a tool failure or an automated system skip.',
192
198
  };
193
199
  }
194
200
  }
@@ -49,7 +49,13 @@ export async function runShellCommand(context, args) {
49
49
  ok: false,
50
50
  skipped: true,
51
51
  command,
52
- error: 'User declined command execution. Do not rerun this command or try a broader variant of it; either continue without it or ask the user one specific question about how to proceed.',
52
+ failureCategory: 'user_declined',
53
+ failureDetails: {
54
+ category: 'user_declined',
55
+ tool: 'run_command',
56
+ action: 'Respect the real user’s decision. Do not retry the same or an equivalent action; reconsider the approach or ask one specific question if needed.',
57
+ },
58
+ error: 'The real user rejected this proposed command. Nothing was executed; this was not a tool failure or an automated system skip.',
53
59
  };
54
60
  }
55
61
  }
@@ -60,15 +66,7 @@ export async function runShellCommand(context, args) {
60
66
  requestSudoPassword,
61
67
  timeout: typeof args.timeout_ms === 'number' && args.timeout_ms > 0 ? args.timeout_ms : undefined,
62
68
  });
63
- const repoSync = projectIndex.initialized
64
- ? await syncIndexFromDisk(projectIndex)
65
- : {
66
- added: 0,
67
- modified: 0,
68
- removed: 0,
69
- indexedChunks: 0,
70
- retrievalTokensUsed: 0,
71
- };
69
+ const repoSync = await syncRepoIndex(projectIndex, onStatus);
72
70
  invalidateShellDiagnosticsCache(rootDir);
73
71
  const diagnostics = buildDeferredShellDiagnostics('run_command');
74
72
  if (repoSync.added || repoSync.modified || repoSync.removed) {
@@ -98,6 +96,34 @@ export function boundCommandOutput(output) {
98
96
  `\n\n... (${output.length - headSize - tailSize} chars truncated) ...\n\n` +
99
97
  output.slice(-tailSize));
100
98
  }
99
+ async function syncRepoIndex(projectIndex, onStatus) {
100
+ if (!projectIndex.initialized) {
101
+ return {
102
+ added: 0,
103
+ modified: 0,
104
+ removed: 0,
105
+ indexedChunks: 0,
106
+ retrievalTokensUsed: 0,
107
+ };
108
+ }
109
+ try {
110
+ return await syncIndexFromDisk(projectIndex);
111
+ }
112
+ catch (error) {
113
+ const message = error instanceof Error ? error.message : String(error);
114
+ onStatus(`Command completed, but local index sync failed: ${message}`);
115
+ return {
116
+ added: 0,
117
+ modified: 0,
118
+ removed: 0,
119
+ indexedChunks: 0,
120
+ retrievalTokensUsed: 0,
121
+ skipped: true,
122
+ reason: 'local index sync failed after command execution',
123
+ error: message,
124
+ };
125
+ }
126
+ }
101
127
  async function runBackgroundCommand(context, command, timeoutMs, repoHint) {
102
128
  const { rootDir, projectIndex, onStatus } = context;
103
129
  const started = await startBackgroundJob(command, rootDir, {
@@ -116,15 +142,7 @@ async function runBackgroundCommand(context, command, timeoutMs, repoHint) {
116
142
  };
117
143
  }
118
144
  const snapshot = started.snapshot;
119
- const repoSync = projectIndex.initialized
120
- ? await syncIndexFromDisk(projectIndex)
121
- : {
122
- added: 0,
123
- modified: 0,
124
- removed: 0,
125
- indexedChunks: 0,
126
- retrievalTokensUsed: 0,
127
- };
145
+ const repoSync = await syncRepoIndex(projectIndex, onStatus);
128
146
  invalidateShellDiagnosticsCache(rootDir);
129
147
  if (repoSync.added || repoSync.modified || repoSync.removed) {
130
148
  onStatus(`Synced repo state after command (${repoSync.added} added, ${repoSync.modified} modified, ${repoSync.removed} removed).`);
@@ -175,7 +175,13 @@ export async function runNodeScript(context, args) {
175
175
  ok: false,
176
176
  skipped: true,
177
177
  command: COMMAND_LABEL,
178
- error: 'User declined command execution',
178
+ failureCategory: 'user_declined',
179
+ failureDetails: {
180
+ category: 'user_declined',
181
+ tool: 'run_node_script',
182
+ action: 'Respect the real user’s decision. Do not retry the same or an equivalent action; reconsider the approach or ask one specific question if needed.',
183
+ },
184
+ error: 'The real user rejected this proposed script. Nothing was executed; this was not a tool failure or an automated system skip.',
179
185
  };
180
186
  }
181
187
  }
@@ -186,13 +192,27 @@ export async function runNodeScript(context, args) {
186
192
  const afterGitStatus = readGitStatusSignature(rootDir);
187
193
  const gitStatusCleanBeforeAndAfter = beforeGitStatus === '' && afterGitStatus === '';
188
194
  const shouldSync = context.projectIndex.initialized && !gitStatusCleanBeforeAndAfter;
189
- const repoSync = shouldSync
190
- ? await syncIndexFromDisk(context.projectIndex)
191
- : emptyRepoSync(!context.projectIndex.initialized
195
+ let repoSync;
196
+ if (shouldSync) {
197
+ try {
198
+ repoSync = await syncIndexFromDisk(context.projectIndex);
199
+ }
200
+ catch (error) {
201
+ const message = error instanceof Error ? error.message : String(error);
202
+ context.onStatus(`Node script completed, but local index sync failed: ${message}`);
203
+ repoSync = {
204
+ ...emptyRepoSync('local index sync failed after Node script execution'),
205
+ error: message,
206
+ };
207
+ }
208
+ }
209
+ else {
210
+ repoSync = emptyRepoSync(!context.projectIndex.initialized
192
211
  ? 'project index not initialized'
193
212
  : gitStatusCleanBeforeAndAfter
194
213
  ? 'git status clean before and after'
195
214
  : 'sync not needed');
215
+ }
196
216
  invalidateShellDiagnosticsCache(rootDir);
197
217
  if (repoSync.added || repoSync.modified || repoSync.removed) {
198
218
  context.onStatus(`Synced repo state after Node script (${repoSync.added} added, ${repoSync.modified} modified, ${repoSync.removed} removed).`);
@@ -1,7 +1,7 @@
1
1
  import chalk from '../colors.js';
2
2
  import path from 'node:path';
3
3
  import { normalizeProjectRelativePath } from '../artifact-policy.js';
4
- import { readProjectFile, writeProjectFile } from '../patcher.js';
4
+ import { classifyProjectPath, readProjectFile, writeProjectFile, } from '../patcher.js';
5
5
  import { repairFilePath } from './path-suggest.js';
6
6
  import { upsertIndexFile } from '../project-index.js';
7
7
  import { isTuiMode } from '../runtime-mode.js';
@@ -106,6 +106,21 @@ export async function strReplace(context, args) {
106
106
  failureCategory: 'invalid_argument',
107
107
  };
108
108
  }
109
+ const pathKind = classifyProjectPath(rootDir, filePath);
110
+ if (pathKind === 'outside') {
111
+ return {
112
+ ok: false,
113
+ filePath,
114
+ error: `Refusing to edit outside the project root: ${filePath}. Editable locations are the project root and the session scratch directory ($THEGITAI_SCRATCH_DIR).`,
115
+ failureCategory: 'invalid_argument',
116
+ failureDetails: {
117
+ category: 'invalid_argument',
118
+ tool: 'str_replace',
119
+ action: 'Edit files inside the project root, or use an absolute path under the session scratch directory ($THEGITAI_SCRATCH_DIR) for temporary files.',
120
+ },
121
+ };
122
+ }
123
+ const scratchPath = pathKind === 'scratch';
109
124
  let originalContent;
110
125
  try {
111
126
  originalContent = readProjectFile(rootDir, filePath);
@@ -169,7 +184,13 @@ export async function strReplace(context, args) {
169
184
  ok: false,
170
185
  skipped: true,
171
186
  filePath,
172
- error: 'User declined str_replace',
187
+ failureCategory: 'user_declined',
188
+ failureDetails: {
189
+ category: 'user_declined',
190
+ tool: 'str_replace',
191
+ action: 'Respect the real user’s decision. Do not retry the same or an equivalent edit; reconsider the approach or ask one specific question if needed.',
192
+ },
193
+ error: 'The real user rejected this proposed edit. Nothing was changed; this was not a tool failure or an automated system skip.',
173
194
  };
174
195
  }
175
196
  }
@@ -178,13 +199,16 @@ export async function strReplace(context, args) {
178
199
  const replacements = changed ? (replaceAll ? n : 1) : 0;
179
200
  let indexedChunks = 0;
180
201
  let retrievalTokensUsed = 0;
181
- if (changed) {
202
+ if (changed && !scratchPath) {
182
203
  const indexResult = await upsertIndexFile(projectIndex, filePath);
183
204
  indexedChunks = indexResult.indexedChunks;
184
205
  retrievalTokensUsed = indexResult.retrievalTokensUsed ?? 0;
185
206
  }
186
- invalidateShellDiagnosticsCache(rootDir, filePath);
187
- const diagnostics = runShellDiagnostics(rootDir, filePath);
207
+ let diagnostics;
208
+ if (!scratchPath) {
209
+ invalidateShellDiagnosticsCache(rootDir, filePath);
210
+ diagnostics = runShellDiagnostics(rootDir, filePath);
211
+ }
188
212
  const originalLines = originalContent.split('\n').length;
189
213
  const nextLines = nextContent.split('\n').length;
190
214
  if (!isTuiMode()) {
@@ -197,6 +221,7 @@ export async function strReplace(context, args) {
197
221
  filePath,
198
222
  changed,
199
223
  operation: 'str_replace',
224
+ ...(scratchPath ? { scratch: true } : {}),
200
225
  replacements,
201
226
  indexedChunks,
202
227
  retrievalTokensUsed,
@@ -1,7 +1,7 @@
1
1
  import chalk from '../colors.js';
2
2
  import path from 'node:path';
3
3
  import { normalizeProjectRelativePath } from '../artifact-policy.js';
4
- import { writeProjectFile } from '../patcher.js';
4
+ import { classifyProjectPath, writeProjectFile } from '../patcher.js';
5
5
  import { upsertIndexFile } from '../project-index.js';
6
6
  import { isTuiMode } from '../runtime-mode.js';
7
7
  import { getCurrentFileHash, hasFreshFullReadCoverage, resolveRedactionTokens, } from '../session-safety.js';
@@ -25,9 +25,25 @@ export async function writeFile(context, args) {
25
25
  failureCategory: 'invalid_argument',
26
26
  };
27
27
  }
28
+ const pathKind = classifyProjectPath(rootDir, filePath);
29
+ if (pathKind === 'outside') {
30
+ return {
31
+ ok: false,
32
+ filePath,
33
+ error: `Refusing to write outside the project root: ${filePath}. Writable locations are the project root and the session scratch directory ($THEGITAI_SCRATCH_DIR).`,
34
+ failureCategory: 'invalid_argument',
35
+ failureDetails: {
36
+ category: 'invalid_argument',
37
+ tool: 'write_file',
38
+ action: 'Write inside the project root, or use an absolute path under the session scratch directory ($THEGITAI_SCRATCH_DIR) for temporary files.',
39
+ },
40
+ };
41
+ }
42
+ const scratchPath = pathKind === 'scratch';
28
43
  const coveragePath = normalizeProjectRelativePath(rootDir, filePath) ?? filePath;
29
44
  const currentHash = getCurrentFileHash(rootDir, filePath);
30
- if (currentHash !== null &&
45
+ if (!scratchPath &&
46
+ currentHash !== null &&
31
47
  context.safety &&
32
48
  !hasFreshFullReadCoverage(context.safety, coveragePath, currentHash)) {
33
49
  return {
@@ -47,13 +63,16 @@ export async function writeFile(context, args) {
47
63
  const { changed } = writeProjectFile(rootDir, filePath, content);
48
64
  let indexedChunks = 0;
49
65
  let retrievalTokensUsed = 0;
50
- if (changed) {
66
+ if (changed && !scratchPath) {
51
67
  const indexResult = await upsertIndexFile(projectIndex, filePath);
52
68
  indexedChunks = indexResult.indexedChunks;
53
69
  retrievalTokensUsed = indexResult.retrievalTokensUsed ?? 0;
54
70
  }
55
- invalidateShellDiagnosticsCache(rootDir, filePath);
56
- const diagnostics = runShellDiagnostics(rootDir, filePath);
71
+ let diagnostics;
72
+ if (!scratchPath) {
73
+ invalidateShellDiagnosticsCache(rootDir, filePath);
74
+ diagnostics = runShellDiagnostics(rootDir, filePath);
75
+ }
57
76
  if (!isTuiMode()) {
58
77
  const icon = changed ? '✨' : '📝';
59
78
  const label = changed ? 'Created/Updated' : 'Created/Updated (no change)';
@@ -64,6 +83,7 @@ export async function writeFile(context, args) {
64
83
  filePath,
65
84
  changed,
66
85
  operation: 'write',
86
+ ...(scratchPath ? { scratch: true } : {}),
67
87
  indexedChunks,
68
88
  retrievalTokensUsed,
69
89
  bytesWritten: Buffer.byteLength(content, 'utf-8'),
@@ -7,6 +7,7 @@ import { chat, models } from '../api/index.js';
7
7
  import { isTurnCancelledError } from '../api/chat.js';
8
8
  import { getJobBufferedOutput, getJobOutputPreview, hasRunningBackgroundJobs, killAllBackgroundJobs, killBackgroundJob, listBackgroundJobs, setBackgroundJobSession, setBackgroundJobUpdateHook, } from '../background-jobs.js';
9
9
  import { clearTodos, listTodos, setTodoSession } from '../todo-list.js';
10
+ import { setScratchSession } from '../scratch-dir.js';
10
11
  import { cancelActiveCommand } from '../executor.js';
11
12
  import { setCommandOutputHook, withTuiMode } from '../runtime-mode.js';
12
13
  import { collectBackgroundJobUiKillMutations, collectBackgroundJobUiOutputMutations, } from '../tool-executor.js';
@@ -558,6 +559,31 @@ function buildFileChangeEntry(event) {
558
559
  title: skipped ? `${verb} skipped: ${filePath}` : `${verb} failed: ${filePath}`,
559
560
  };
560
561
  }
562
+ if (result?.scratch === true) {
563
+ const scratchName = truncate(filePath.split(/[\\/]/).filter(Boolean).at(-1) ?? filePath, 72);
564
+ if (call.name === 'delete_file') {
565
+ return {
566
+ body: '',
567
+ filePath,
568
+ kind: 'tool',
569
+ title: result?.deleted === true
570
+ ? `Removed scratch file: ${scratchName}`
571
+ : `Scratch delete skipped: ${scratchName}`,
572
+ };
573
+ }
574
+ const content = typeof call.args?.content === 'string' ? call.args.content : '';
575
+ const lineSummary = call.name === 'write_file' && content
576
+ ? ` (${splitDiffLines(content).length} lines)`
577
+ : '';
578
+ return {
579
+ body: '',
580
+ filePath,
581
+ kind: 'tool',
582
+ title: call.name === 'write_file'
583
+ ? `Wrote scratch file: ${scratchName}${lineSummary}`
584
+ : `Edited scratch file: ${scratchName}`,
585
+ };
586
+ }
561
587
  if (call.name === 'undo_edit') {
562
588
  const dryRun = result?.dryRun === true ||
563
589
  result?.dry_run === true ||
@@ -1257,6 +1283,7 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
1257
1283
  }
1258
1284
  await withTuiMode(async () => {
1259
1285
  setBackgroundJobSession(session.sessionId);
1286
+ setScratchSession(session.sessionId);
1260
1287
  setTodoSession(session.sessionId);
1261
1288
  const store = createShellStore(createInitialShellState(session, serverModels, debugUi));
1262
1289
  store.replaceTranscript(createSessionTranscript(session));
@@ -1933,6 +1960,7 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
1933
1960
  const snapshot = await loadInteractiveSession(selected.id);
1934
1961
  applySessionSnapshot(session, snapshot);
1935
1962
  setBackgroundJobSession(session.sessionId);
1963
+ setScratchSession(session.sessionId);
1936
1964
  syncBackgroundJobsState();
1937
1965
  setTodoSession(session.sessionId);
1938
1966
  syncTodosState();
@@ -2531,6 +2559,7 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
2531
2559
  terminalTitle.dispose();
2532
2560
  killAllBackgroundJobs();
2533
2561
  setBackgroundJobSession(null);
2562
+ setScratchSession(null);
2534
2563
  setTodoSession(null);
2535
2564
  setBackgroundJobUpdateHook(null);
2536
2565
  await bridge.close();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@thegitai/cli",
3
- "version": "1.0.0-preview.1",
3
+ "version": "1.0.0-preview.2",
4
4
  "description": "TheGitAI CLI client (source-visible, proprietary)",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "homepage": "https://thegit.ai",
@@ -25,10 +25,10 @@
25
25
  "@lydell/node-pty-linux-x64": "1.1.0",
26
26
  "@lydell/node-pty-win32-arm64": "1.1.0",
27
27
  "@lydell/node-pty-win32-x64": "1.1.0",
28
- "@thegitai/tui-darwin-arm64": "1.0.0-preview.1",
29
- "@thegitai/tui-darwin-x64": "1.0.0-preview.1",
30
- "@thegitai/tui-linux-x64": "1.0.0-preview.1",
31
- "@thegitai/tui-win32-x64": "1.0.0-preview.1",
28
+ "@thegitai/tui-darwin-arm64": "1.0.0-preview.2",
29
+ "@thegitai/tui-darwin-x64": "1.0.0-preview.2",
30
+ "@thegitai/tui-linux-x64": "1.0.0-preview.2",
31
+ "@thegitai/tui-win32-x64": "1.0.0-preview.2",
32
32
  "@vscode/ripgrep": "1.18.0"
33
33
  },
34
34
  "publishConfig": {