@thegitai/cli 1.0.0-preview.4 → 1.0.0-preview.41
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/README.md +49 -4
- package/dist/bin/ai.js +119 -111
- package/dist/bin/browser-host.js +265 -0
- package/dist/src/agent-mode.js +11 -6
- package/dist/src/api/auth.js +6 -4
- package/dist/src/api/browser-login.js +152 -37
- package/dist/src/api/chat.js +286 -44
- package/dist/src/api/contracts.js +55 -1
- package/dist/src/api/default-host.js +1 -0
- package/dist/src/api/http.js +127 -7
- package/dist/src/api/models.js +29 -10
- package/dist/src/background-jobs.js +4 -2
- package/dist/src/browser/bridge.js +232 -0
- package/dist/src/browser/framing.js +42 -0
- package/dist/src/browser/native-host.js +209 -0
- package/dist/src/browser/protocol.js +46 -0
- package/dist/src/browser/session-bridge.js +104 -0
- package/dist/src/cli-args.js +19 -5
- package/dist/src/client-environment.js +2 -0
- package/dist/src/core/clipboard.js +7 -13
- package/dist/src/core/image-limits.js +56 -0
- package/dist/src/core/image-path-extractor.js +70 -3
- package/dist/src/core/session-image-store.js +246 -0
- package/dist/src/edit-journal.js +33 -2
- package/dist/src/executor.js +25 -3
- package/dist/src/help-text.js +58 -17
- package/dist/src/patcher.js +43 -4
- package/dist/src/permissions.js +288 -0
- package/dist/src/session-safety.js +0 -12
- package/dist/src/session-store.js +121 -20
- package/dist/src/session.js +19 -4
- package/dist/src/signin.js +58 -0
- package/dist/src/tool-executor.js +12 -46
- package/dist/src/tools/browser.js +628 -0
- package/dist/src/tools/delete-file.js +20 -4
- package/dist/src/tools/index.js +32 -10
- package/dist/src/tools/patch-file.js +12 -26
- package/dist/src/tools/read-image-file.js +85 -0
- package/dist/src/tools/replace-document-text.js +28 -18
- package/dist/src/tools/restore-checkpoint.js +0 -1
- package/dist/src/tools/run-command.js +14 -71
- package/dist/src/tools/run-node-script.js +12 -81
- package/dist/src/tools/save-generated-image.js +120 -0
- package/dist/src/tools/str-replace.js +12 -26
- package/dist/src/tools/undo-edit.js +1 -6
- package/dist/src/tools/write-file.js +67 -11
- package/dist/src/ui/prompt-history-store.js +1 -1
- package/dist/src/ui/repl.js +802 -195
- package/dist/src/ui/tui/bridge.js +10 -0
- package/dist/src/ui/tui/build-frame.js +517 -165
- package/dist/src/ui/tui/composer-layout.js +76 -0
- package/dist/src/ui/tui/markdown-render.js +81 -73
- package/dist/src/ui/tui/shell-input.js +227 -67
- package/dist/src/ui/tui/terminal-theme.js +28 -0
- package/dist/src/ui/tui/terminal-title.js +3 -0
- package/dist/src/ui/tui/terminal-writes.js +48 -0
- package/dist/src/ui/tui/text.js +158 -4
- package/dist/src/ui/tui/user-input.js +568 -0
- package/dist/src/utils.js +9 -0
- package/package.json +20 -8
- package/dist/src/markdown-renderer.js +0 -112
- package/dist/src/project-index.js +0 -221
- package/dist/src/tools/code-intel.js +0 -472
- package/dist/src/tools/find-symbol.js +0 -70
- package/dist/src/tools/hover-symbol.js +0 -95
- package/dist/src/tools/list-symbols.js +0 -55
- package/dist/src/tools/search-code.js +0 -37
- package/dist/src/tools/signature-help.js +0 -118
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
import { getUnquotedShellText } from './agent-mode.js';
|
|
2
|
+
export const PERMISSION_BUCKETS = ['create', 'edit', 'delete', 'run', 'browse'];
|
|
3
|
+
export function createSessionGrants() {
|
|
4
|
+
return { buckets: [], commandPrefixes: [], origins: [] };
|
|
5
|
+
}
|
|
6
|
+
export function bucketActionLabel(bucket) {
|
|
7
|
+
if (bucket === 'create')
|
|
8
|
+
return 'create files';
|
|
9
|
+
if (bucket === 'edit')
|
|
10
|
+
return 'edit files';
|
|
11
|
+
if (bucket === 'delete')
|
|
12
|
+
return 'delete files';
|
|
13
|
+
if (bucket === 'browse')
|
|
14
|
+
return 'act on any website';
|
|
15
|
+
return 'run commands';
|
|
16
|
+
}
|
|
17
|
+
export function originForUrl(url) {
|
|
18
|
+
const raw = String(url ?? '').trim();
|
|
19
|
+
if (!raw)
|
|
20
|
+
return null;
|
|
21
|
+
try {
|
|
22
|
+
const parsed = new URL(/^[a-z][a-z0-9+.-]*:/i.test(raw) ? raw : `https://${raw}`);
|
|
23
|
+
if (parsed.protocol === 'about:' || parsed.protocol === 'chrome:')
|
|
24
|
+
return null;
|
|
25
|
+
return parsed.origin;
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
export function isOriginGranted(grants, origin) {
|
|
32
|
+
if (!grants || !origin)
|
|
33
|
+
return false;
|
|
34
|
+
return grants.origins?.includes(origin) === true;
|
|
35
|
+
}
|
|
36
|
+
export function grantOrigin(grants, origin) {
|
|
37
|
+
const normalized = String(origin ?? '').trim();
|
|
38
|
+
if (!normalized)
|
|
39
|
+
return;
|
|
40
|
+
const origins = grants.origins ??= [];
|
|
41
|
+
if (!origins.includes(normalized))
|
|
42
|
+
origins.push(normalized);
|
|
43
|
+
}
|
|
44
|
+
const NO_PREFIX_GRANT_BINARIES = new Set([
|
|
45
|
+
'rm',
|
|
46
|
+
'rmdir',
|
|
47
|
+
'mv',
|
|
48
|
+
'dd',
|
|
49
|
+
'mkfs',
|
|
50
|
+
'shred',
|
|
51
|
+
'sudo',
|
|
52
|
+
'doas',
|
|
53
|
+
'su',
|
|
54
|
+
'chmod',
|
|
55
|
+
'chown',
|
|
56
|
+
'chgrp',
|
|
57
|
+
'kill',
|
|
58
|
+
'pkill',
|
|
59
|
+
'killall',
|
|
60
|
+
'shutdown',
|
|
61
|
+
'reboot',
|
|
62
|
+
'curl',
|
|
63
|
+
'wget',
|
|
64
|
+
'ssh',
|
|
65
|
+
'scp',
|
|
66
|
+
'nc',
|
|
67
|
+
'eval',
|
|
68
|
+
'exec',
|
|
69
|
+
'source',
|
|
70
|
+
]);
|
|
71
|
+
const INTERPRETER_OR_WRAPPER_BINARIES = new Set([
|
|
72
|
+
'bash',
|
|
73
|
+
'sh',
|
|
74
|
+
'zsh',
|
|
75
|
+
'fish',
|
|
76
|
+
'dash',
|
|
77
|
+
'ksh',
|
|
78
|
+
'csh',
|
|
79
|
+
'tcsh',
|
|
80
|
+
'python',
|
|
81
|
+
'python2',
|
|
82
|
+
'python3',
|
|
83
|
+
'node',
|
|
84
|
+
'deno',
|
|
85
|
+
'bun',
|
|
86
|
+
'ruby',
|
|
87
|
+
'perl',
|
|
88
|
+
'php',
|
|
89
|
+
'lua',
|
|
90
|
+
'osascript',
|
|
91
|
+
'awk',
|
|
92
|
+
'gawk',
|
|
93
|
+
'sed',
|
|
94
|
+
'env',
|
|
95
|
+
'timeout',
|
|
96
|
+
'xargs',
|
|
97
|
+
'nohup',
|
|
98
|
+
'setsid',
|
|
99
|
+
'watch',
|
|
100
|
+
'script',
|
|
101
|
+
'nice',
|
|
102
|
+
'ionice',
|
|
103
|
+
'stdbuf',
|
|
104
|
+
'time',
|
|
105
|
+
'sudo',
|
|
106
|
+
'doas',
|
|
107
|
+
'su',
|
|
108
|
+
'find',
|
|
109
|
+
'make',
|
|
110
|
+
]);
|
|
111
|
+
const SCRIPT_RUNNER_VERBS = new Set(['run', 'exec', 'run-script', 'x', 'dlx']);
|
|
112
|
+
const VERB_PATTERN = /^[A-Za-z][A-Za-z0-9_:-]*$/;
|
|
113
|
+
const BINARY_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._+-]*$/;
|
|
114
|
+
const SHELL_CONTROL_OPERATOR_PATTERN = /[&|;<>\n]/;
|
|
115
|
+
const COMMAND_SUBSTITUTION_PATTERN = /`|\$\(/;
|
|
116
|
+
function normalizeCommand(command) {
|
|
117
|
+
return String(command ?? '').trim().replace(/\s+/g, ' ');
|
|
118
|
+
}
|
|
119
|
+
function parseCommand(command) {
|
|
120
|
+
const raw = String(command ?? '');
|
|
121
|
+
if (!raw.trim())
|
|
122
|
+
return null;
|
|
123
|
+
if (COMMAND_SUBSTITUTION_PATTERN.test(raw))
|
|
124
|
+
return null;
|
|
125
|
+
if (SHELL_CONTROL_OPERATOR_PATTERN.test(getUnquotedShellText(raw)))
|
|
126
|
+
return null;
|
|
127
|
+
const all = normalizeCommand(raw).match(/\S+/g) ?? [];
|
|
128
|
+
const tokens = [];
|
|
129
|
+
for (const token of all) {
|
|
130
|
+
if (!tokens.length && /^[A-Za-z_][A-Za-z0-9_]*=/.test(token))
|
|
131
|
+
continue;
|
|
132
|
+
if (!tokens.length && token === 'command')
|
|
133
|
+
continue;
|
|
134
|
+
tokens.push(token);
|
|
135
|
+
}
|
|
136
|
+
if (!tokens.length)
|
|
137
|
+
return null;
|
|
138
|
+
const binary = (tokens[0].split('/').pop() ?? '').trim();
|
|
139
|
+
if (!binary)
|
|
140
|
+
return null;
|
|
141
|
+
tokens[0] = binary;
|
|
142
|
+
return { canonical: tokens.join(' '), tokens };
|
|
143
|
+
}
|
|
144
|
+
function grantPrefixFromTokens(tokens) {
|
|
145
|
+
const binary = tokens[0];
|
|
146
|
+
if (!BINARY_PATTERN.test(binary))
|
|
147
|
+
return null;
|
|
148
|
+
if (NO_PREFIX_GRANT_BINARIES.has(binary))
|
|
149
|
+
return null;
|
|
150
|
+
if (INTERPRETER_OR_WRAPPER_BINARIES.has(binary))
|
|
151
|
+
return null;
|
|
152
|
+
const verb = tokens[1];
|
|
153
|
+
if (!verb || !VERB_PATTERN.test(verb))
|
|
154
|
+
return null;
|
|
155
|
+
if (SCRIPT_RUNNER_VERBS.has(verb)) {
|
|
156
|
+
const script = tokens[2];
|
|
157
|
+
if (!script || !VERB_PATTERN.test(script))
|
|
158
|
+
return null;
|
|
159
|
+
return `${binary} ${verb} ${script}`;
|
|
160
|
+
}
|
|
161
|
+
return `${binary} ${verb}`;
|
|
162
|
+
}
|
|
163
|
+
export function commandGrantPrefix(command) {
|
|
164
|
+
const parsed = parseCommand(command);
|
|
165
|
+
return parsed ? grantPrefixFromTokens(parsed.tokens) : null;
|
|
166
|
+
}
|
|
167
|
+
export function isBucketGranted(grants, bucket) {
|
|
168
|
+
return Boolean(grants?.buckets.includes(bucket));
|
|
169
|
+
}
|
|
170
|
+
export function isCommandGranted(grants, command) {
|
|
171
|
+
if (!grants)
|
|
172
|
+
return false;
|
|
173
|
+
if (grants.buckets.includes('run'))
|
|
174
|
+
return true;
|
|
175
|
+
if (!grants.commandPrefixes.length)
|
|
176
|
+
return false;
|
|
177
|
+
const parsed = parseCommand(command);
|
|
178
|
+
if (!parsed)
|
|
179
|
+
return false;
|
|
180
|
+
if (grantPrefixFromTokens(parsed.tokens) === null)
|
|
181
|
+
return false;
|
|
182
|
+
return grants.commandPrefixes.some((prefix) => parsed.canonical === prefix || parsed.canonical.startsWith(`${prefix} `));
|
|
183
|
+
}
|
|
184
|
+
export function grantBucket(grants, bucket) {
|
|
185
|
+
if (!grants.buckets.includes(bucket))
|
|
186
|
+
grants.buckets.push(bucket);
|
|
187
|
+
}
|
|
188
|
+
export function grantCommandPrefix(grants, prefix) {
|
|
189
|
+
const normalized = normalizeCommand(prefix);
|
|
190
|
+
if (!normalized)
|
|
191
|
+
return;
|
|
192
|
+
if (!grants.commandPrefixes.includes(normalized)) {
|
|
193
|
+
grants.commandPrefixes.push(normalized);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
export function buildPermissionOptions(bucket, command, origin) {
|
|
197
|
+
const options = [
|
|
198
|
+
{ label: 'Approve once', decision: { kind: 'once' } },
|
|
199
|
+
];
|
|
200
|
+
const prefix = bucket === 'run' && command ? commandGrantPrefix(command) : null;
|
|
201
|
+
if (prefix) {
|
|
202
|
+
options.push({
|
|
203
|
+
label: `Always allow: ${prefix}`,
|
|
204
|
+
decision: { kind: 'always-prefix', prefix },
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
if (bucket === 'browse' && origin) {
|
|
208
|
+
options.push({
|
|
209
|
+
label: `Allow this site for the session: ${origin}`,
|
|
210
|
+
decision: { kind: 'always-origin', origin },
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
options.push({
|
|
214
|
+
label: `Always allow: ${bucketActionLabel(bucket)}`,
|
|
215
|
+
decision: { kind: 'always-bucket' },
|
|
216
|
+
});
|
|
217
|
+
options.push({ label: 'Deny', decision: { kind: 'deny' } });
|
|
218
|
+
return options;
|
|
219
|
+
}
|
|
220
|
+
function declinedSubject(bucket) {
|
|
221
|
+
if (bucket === 'run') {
|
|
222
|
+
return { noun: 'command', effect: 'Nothing was executed' };
|
|
223
|
+
}
|
|
224
|
+
if (bucket === 'create') {
|
|
225
|
+
return { noun: 'file creation', effect: 'Nothing was created' };
|
|
226
|
+
}
|
|
227
|
+
if (bucket === 'delete') {
|
|
228
|
+
return { noun: 'file deletion', effect: 'Nothing was deleted' };
|
|
229
|
+
}
|
|
230
|
+
if (bucket === 'browse') {
|
|
231
|
+
return { noun: 'browser action', effect: 'The page was not touched' };
|
|
232
|
+
}
|
|
233
|
+
return { noun: 'edit', effect: 'Nothing was changed' };
|
|
234
|
+
}
|
|
235
|
+
function declinedResponse(bucket, toolName, extra) {
|
|
236
|
+
const { noun, effect } = declinedSubject(bucket);
|
|
237
|
+
return {
|
|
238
|
+
ok: false,
|
|
239
|
+
skipped: true,
|
|
240
|
+
...extra,
|
|
241
|
+
failureCategory: 'user_declined',
|
|
242
|
+
failureDetails: {
|
|
243
|
+
category: 'user_declined',
|
|
244
|
+
tool: toolName,
|
|
245
|
+
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.',
|
|
246
|
+
},
|
|
247
|
+
error: `The real user rejected this proposed ${noun}. ${effect}; this was not a tool failure or an automated system skip.`,
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
export async function ensurePermission(context, request, toolName, extra = {}) {
|
|
251
|
+
if (context.autoYes)
|
|
252
|
+
return null;
|
|
253
|
+
if (isBucketGranted(context.grants, request.bucket))
|
|
254
|
+
return null;
|
|
255
|
+
if (request.bucket === 'run' &&
|
|
256
|
+
request.command &&
|
|
257
|
+
isCommandGranted(context.grants, request.command)) {
|
|
258
|
+
return null;
|
|
259
|
+
}
|
|
260
|
+
if (request.bucket === 'browse' &&
|
|
261
|
+
isOriginGranted(context.grants, request.origin ?? null)) {
|
|
262
|
+
return null;
|
|
263
|
+
}
|
|
264
|
+
if (!context.requestPermission) {
|
|
265
|
+
return {
|
|
266
|
+
ok: false,
|
|
267
|
+
...extra,
|
|
268
|
+
error: `requestPermission is required when autoYes is false (${toolName})`,
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
const options = buildPermissionOptions(request.bucket, request.command, request.origin);
|
|
272
|
+
const decision = await context.requestPermission({ ...request, options });
|
|
273
|
+
if (decision.kind === 'deny') {
|
|
274
|
+
return declinedResponse(request.bucket, toolName, extra);
|
|
275
|
+
}
|
|
276
|
+
if (context.grants) {
|
|
277
|
+
if (decision.kind === 'always-bucket') {
|
|
278
|
+
grantBucket(context.grants, request.bucket);
|
|
279
|
+
}
|
|
280
|
+
else if (decision.kind === 'always-prefix') {
|
|
281
|
+
grantCommandPrefix(context.grants, decision.prefix);
|
|
282
|
+
}
|
|
283
|
+
else if (decision.kind === 'always-origin') {
|
|
284
|
+
grantOrigin(context.grants, decision.origin);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
return null;
|
|
288
|
+
}
|
|
@@ -4,7 +4,6 @@ import path from 'node:path';
|
|
|
4
4
|
import { ARTIFACT_IGNORE_DIRS, normalizeProjectRelativePath, shouldIgnoreArtifactPath, } from './artifact-policy.js';
|
|
5
5
|
import { hashBytes, readFileEditSnapshot, storedContentBuffer, } from './edit-journal.js';
|
|
6
6
|
import { deleteProjectFile, resolveProjectPath, writeProjectFile, writeProjectFileBuffer, } from './patcher.js';
|
|
7
|
-
import { removeIndexFile, upsertIndexFile } from './project-index.js';
|
|
8
7
|
const MAX_CHECKPOINTS = 20;
|
|
9
8
|
const MAX_SESSION_EDITS = 500;
|
|
10
9
|
const MAX_READ_RECORDS = 200;
|
|
@@ -672,7 +671,6 @@ export async function restoreCheckpointFiles(args) {
|
|
|
672
671
|
const restored = [];
|
|
673
672
|
const applied = [];
|
|
674
673
|
let changed = false;
|
|
675
|
-
const syncPolicy = args.projectIndex.initialized;
|
|
676
674
|
for (const snapshot of targets) {
|
|
677
675
|
const before = readFileEditSnapshot(args.rootDir, snapshot.filePath);
|
|
678
676
|
try {
|
|
@@ -680,15 +678,11 @@ export async function restoreCheckpointFiles(args) {
|
|
|
680
678
|
const result = writeStoredProjectFile(args.rootDir, snapshot.filePath, snapshot.content ?? '', snapshot.contentEncoding);
|
|
681
679
|
if (result.changed)
|
|
682
680
|
changed = true;
|
|
683
|
-
if (syncPolicy)
|
|
684
|
-
await upsertIndexFile(args.projectIndex, snapshot.filePath);
|
|
685
681
|
}
|
|
686
682
|
else {
|
|
687
683
|
const result = deleteProjectFile(args.rootDir, snapshot.filePath);
|
|
688
684
|
if (result.deleted)
|
|
689
685
|
changed = true;
|
|
690
|
-
if (syncPolicy)
|
|
691
|
-
await removeIndexFile(args.projectIndex, snapshot.filePath);
|
|
692
686
|
}
|
|
693
687
|
const after = readFileEditSnapshot(args.rootDir, snapshot.filePath);
|
|
694
688
|
applied.push({ snapshot, before, after });
|
|
@@ -710,15 +704,9 @@ export async function restoreCheckpointFiles(args) {
|
|
|
710
704
|
throw new Error('previous file content is unavailable');
|
|
711
705
|
}
|
|
712
706
|
writeStoredProjectFile(args.rootDir, item.snapshot.filePath, item.before.content, item.before.contentEncoding);
|
|
713
|
-
if (syncPolicy) {
|
|
714
|
-
await upsertIndexFile(args.projectIndex, item.snapshot.filePath);
|
|
715
|
-
}
|
|
716
707
|
}
|
|
717
708
|
else {
|
|
718
709
|
deleteProjectFile(args.rootDir, item.snapshot.filePath);
|
|
719
|
-
if (syncPolicy) {
|
|
720
|
-
await removeIndexFile(args.projectIndex, item.snapshot.filePath);
|
|
721
|
-
}
|
|
722
710
|
}
|
|
723
711
|
rolledBack.push({ filePath: item.snapshot.filePath });
|
|
724
712
|
}
|
|
@@ -1,12 +1,16 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
1
2
|
import { createHash } from 'node:crypto';
|
|
2
3
|
import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, writeFileSync, } from 'node:fs';
|
|
3
4
|
import path from 'node:path';
|
|
4
5
|
import { getClientStateDir } from './client-state.js';
|
|
6
|
+
import { findStoredImageByContent, pruneSessionImages, readSessionImage, sweepOrphanSessionImages, } from './core/session-image-store.js';
|
|
7
|
+
import { sweepGeneratedImages } from './tools/save-generated-image.js';
|
|
5
8
|
import { normalizeAssistantEditJournal } from './edit-journal.js';
|
|
9
|
+
import { createSessionGrants } from './permissions.js';
|
|
6
10
|
import { cloneSessionSafetyState, createSessionSafetyState, mergeLocalSessionSafetyState, normalizeSessionSafetyState, } from './session-safety.js';
|
|
7
|
-
import {
|
|
11
|
+
import { singleLinePreview } from './utils.js';
|
|
8
12
|
const SESSION_STORE_VERSION = 1;
|
|
9
|
-
const MAX_RECENT_SESSIONS =
|
|
13
|
+
export const MAX_RECENT_SESSIONS = 10;
|
|
10
14
|
function cloneJson(value) {
|
|
11
15
|
return JSON.parse(JSON.stringify(value ?? null));
|
|
12
16
|
}
|
|
@@ -77,6 +81,45 @@ function listSessionFiles(rootDir, env = process.env) {
|
|
|
77
81
|
.filter((name) => name.endsWith('.json'))
|
|
78
82
|
.map((name) => path.join(dir, name));
|
|
79
83
|
}
|
|
84
|
+
function normalizeBranch(value) {
|
|
85
|
+
const text = String(value ?? '')
|
|
86
|
+
.replace(/[\r\n\t]/g, ' ')
|
|
87
|
+
.trim();
|
|
88
|
+
return text ? text.slice(0, 120) : null;
|
|
89
|
+
}
|
|
90
|
+
function dehydrateHistoryImages(history) {
|
|
91
|
+
return history.map((entry) => ({
|
|
92
|
+
...entry,
|
|
93
|
+
parts: (entry.parts ?? []).map((part) => {
|
|
94
|
+
if (!part?.inlineData?.data)
|
|
95
|
+
return part;
|
|
96
|
+
const cachePath = part.imageCachePath ?? findStoredImageByContent(part.inlineData.data);
|
|
97
|
+
if (!cachePath)
|
|
98
|
+
return part;
|
|
99
|
+
return {
|
|
100
|
+
imageRef: { cachePath, mimeType: part.inlineData.mimeType },
|
|
101
|
+
text: '[image stored in this session]',
|
|
102
|
+
};
|
|
103
|
+
}),
|
|
104
|
+
}));
|
|
105
|
+
}
|
|
106
|
+
function rehydrateHistoryImages(history) {
|
|
107
|
+
return history.map((entry) => ({
|
|
108
|
+
...entry,
|
|
109
|
+
parts: (entry.parts ?? []).map((part) => {
|
|
110
|
+
const ref = part?.imageRef;
|
|
111
|
+
if (!ref?.cachePath)
|
|
112
|
+
return part;
|
|
113
|
+
const stored = readSessionImage(String(ref.cachePath));
|
|
114
|
+
if (!stored)
|
|
115
|
+
return { text: part.text ?? '[image no longer available]' };
|
|
116
|
+
return {
|
|
117
|
+
inlineData: { mimeType: stored.mimeType, data: stored.base64Data },
|
|
118
|
+
imageCachePath: ref.cachePath,
|
|
119
|
+
};
|
|
120
|
+
}),
|
|
121
|
+
}));
|
|
122
|
+
}
|
|
80
123
|
function normalizeHistory(value) {
|
|
81
124
|
if (!Array.isArray(value))
|
|
82
125
|
return [];
|
|
@@ -107,6 +150,7 @@ function normalizeSnapshot(raw, rootDir) {
|
|
|
107
150
|
createdAt: normalizeIsoDate(raw.createdAt),
|
|
108
151
|
updatedAt: normalizeIsoDate(raw.updatedAt),
|
|
109
152
|
modelId,
|
|
153
|
+
branch: normalizeBranch(raw.branch),
|
|
110
154
|
history: cloneJson(normalizeHistory(raw.history)),
|
|
111
155
|
clientState: sanitizeClientState(raw.clientState),
|
|
112
156
|
serverState: sanitizeOpaqueState(raw.serverState),
|
|
@@ -145,14 +189,58 @@ function assertSessionNameAvailable(rootDir, name, sessionId, env = process.env)
|
|
|
145
189
|
throw new Error(`Session name "${name}" is already used by ${duplicate.id}. Use --session "${name}" to resume it or choose a different name.`);
|
|
146
190
|
}
|
|
147
191
|
}
|
|
192
|
+
function listAllSessionIds(env = process.env) {
|
|
193
|
+
const ids = new Set();
|
|
194
|
+
const projectsDir = path.join(getSessionBaseDir(env), 'sessions', 'projects');
|
|
195
|
+
if (!existsSync(projectsDir))
|
|
196
|
+
return ids;
|
|
197
|
+
try {
|
|
198
|
+
for (const project of readdirSync(projectsDir)) {
|
|
199
|
+
const dir = path.join(projectsDir, project);
|
|
200
|
+
try {
|
|
201
|
+
for (const file of readdirSync(dir)) {
|
|
202
|
+
if (file.endsWith('.json'))
|
|
203
|
+
ids.add(path.basename(file, '.json'));
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
catch {
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
catch {
|
|
211
|
+
return ids;
|
|
212
|
+
}
|
|
213
|
+
return ids;
|
|
214
|
+
}
|
|
148
215
|
export function pruneSavedSessions(rootDir, env = process.env) {
|
|
149
216
|
const snapshots = loadAllSnapshots(rootDir, env);
|
|
150
217
|
const keep = new Set(snapshots.slice(0, MAX_RECENT_SESSIONS).map((snapshot) => snapshot.id));
|
|
151
218
|
for (const snapshot of snapshots.slice(MAX_RECENT_SESSIONS)) {
|
|
152
219
|
if (!keep.has(snapshot.id)) {
|
|
153
220
|
rmSync(getSessionPath(rootDir, snapshot.id, env), { force: true });
|
|
221
|
+
pruneSessionImages(snapshot.id, env);
|
|
154
222
|
}
|
|
155
223
|
}
|
|
224
|
+
sweepOrphanSessionImages({
|
|
225
|
+
activeSessionIds: listAllSessionIds(env),
|
|
226
|
+
env,
|
|
227
|
+
});
|
|
228
|
+
sweepGeneratedImages({ env });
|
|
229
|
+
}
|
|
230
|
+
export function readGitBranch(rootDir) {
|
|
231
|
+
try {
|
|
232
|
+
const result = spawnSync('git', ['rev-parse', '--abbrev-ref', 'HEAD'], {
|
|
233
|
+
cwd: rootDir,
|
|
234
|
+
encoding: 'utf8',
|
|
235
|
+
timeout: 1500,
|
|
236
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
237
|
+
});
|
|
238
|
+
const branch = result.status === 0 ? String(result.stdout ?? '').trim() : '';
|
|
239
|
+
return branch && branch !== 'HEAD' ? branch : null;
|
|
240
|
+
}
|
|
241
|
+
catch {
|
|
242
|
+
return null;
|
|
243
|
+
}
|
|
156
244
|
}
|
|
157
245
|
export function snapshotFromSession(session) {
|
|
158
246
|
const now = new Date().toISOString();
|
|
@@ -168,6 +256,7 @@ export function snapshotFromSession(session) {
|
|
|
168
256
|
createdAt,
|
|
169
257
|
updatedAt: now,
|
|
170
258
|
modelId: session.modelId,
|
|
259
|
+
branch: readGitBranch(session.rootDir),
|
|
171
260
|
history: cloneJson(session.history),
|
|
172
261
|
clientState: {
|
|
173
262
|
editCounter: session.clientState.editCounter,
|
|
@@ -187,6 +276,7 @@ export function saveSessionState(session, env = process.env) {
|
|
|
187
276
|
}
|
|
188
277
|
export function applySessionSnapshot(session, snapshot, options = {}) {
|
|
189
278
|
const currentAgentMode = session.agentMode;
|
|
279
|
+
const previousSessionId = session.sessionId;
|
|
190
280
|
session.sessionId = snapshot.id;
|
|
191
281
|
session.sessionName = snapshot.name;
|
|
192
282
|
session.sessionCreatedAt = snapshot.createdAt;
|
|
@@ -196,6 +286,9 @@ export function applySessionSnapshot(session, snapshot, options = {}) {
|
|
|
196
286
|
session.serverState = sanitizeOpaqueState(snapshot.serverState);
|
|
197
287
|
session.agentMode = options.preserveAgentMode ? currentAgentMode : 'default';
|
|
198
288
|
session.autoYes = session.agentMode === 'auto-accept';
|
|
289
|
+
if (previousSessionId !== session.sessionId) {
|
|
290
|
+
session.grants = createSessionGrants();
|
|
291
|
+
}
|
|
199
292
|
session.turnState = {
|
|
200
293
|
id: null,
|
|
201
294
|
historyStartIndex: session.history.length,
|
|
@@ -210,6 +303,9 @@ export function applySessionSnapshot(session, snapshot, options = {}) {
|
|
|
210
303
|
safety: mergeLocalSessionSafetyState(session.clientState.safety, snapshot.clientState.safety),
|
|
211
304
|
};
|
|
212
305
|
}
|
|
306
|
+
export function sessionHasUserMessage(session) {
|
|
307
|
+
return session.history.some((entry) => Boolean(userPromptText(entry)));
|
|
308
|
+
}
|
|
213
309
|
function extractMarkedSection(text, marker) {
|
|
214
310
|
const index = text.indexOf(marker);
|
|
215
311
|
if (index === -1)
|
|
@@ -218,23 +314,27 @@ function extractMarkedSection(text, marker) {
|
|
|
218
314
|
const end = after.indexOf('\n\n');
|
|
219
315
|
return (end === -1 ? after : after.slice(0, end)).trim();
|
|
220
316
|
}
|
|
221
|
-
function
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
317
|
+
export function userPromptText(entry) {
|
|
318
|
+
if (!entry || entry.role !== 'user' || entry.kind !== 'turnStart')
|
|
319
|
+
return '';
|
|
320
|
+
const text = (entry.parts ?? [])
|
|
321
|
+
.map((part) => (typeof part?.text === 'string' ? part.text : ''))
|
|
322
|
+
.filter(Boolean)
|
|
323
|
+
.join('\n')
|
|
324
|
+
.trim();
|
|
325
|
+
if (!text)
|
|
326
|
+
return '';
|
|
327
|
+
const request = extractMarkedSection(text, 'Current user request:') ||
|
|
328
|
+
extractMarkedSection(text, 'User request:');
|
|
329
|
+
const message = extractMarkedSection(text, 'Current user message:') ||
|
|
330
|
+
extractMarkedSection(text, 'User message:');
|
|
331
|
+
return (request || message || text).trim();
|
|
332
|
+
}
|
|
333
|
+
function extractFirstUserPrompt(history) {
|
|
334
|
+
for (const entry of history) {
|
|
335
|
+
const text = userPromptText(entry);
|
|
336
|
+
if (text)
|
|
337
|
+
return singleLinePreview(text, 120);
|
|
238
338
|
}
|
|
239
339
|
return '';
|
|
240
340
|
}
|
|
@@ -247,8 +347,9 @@ function metadataFromSnapshot(snapshot) {
|
|
|
247
347
|
updatedAt: snapshot.updatedAt,
|
|
248
348
|
modelId: snapshot.modelId,
|
|
249
349
|
messageCount: snapshot.history.length,
|
|
250
|
-
lastUserMessage:
|
|
350
|
+
lastUserMessage: extractFirstUserPrompt(snapshot.history),
|
|
251
351
|
summaryPreview: '',
|
|
352
|
+
branch: snapshot.branch ?? null,
|
|
252
353
|
};
|
|
253
354
|
}
|
|
254
355
|
export function listSessionMetadata(rootDir, env = process.env) {
|
package/dist/src/session.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import { normalizeAgentMode, } from './agent-mode.js';
|
|
3
|
+
import { createSessionGrants, } from './permissions.js';
|
|
3
4
|
import { createSessionSafetyState, } from './session-safety.js';
|
|
5
|
+
import { closeBrowserBridge } from './browser/session-bridge.js';
|
|
4
6
|
import { clampInteger } from './utils.js';
|
|
5
7
|
const DEFAULT_MAX_TOOL_STEPS = 32;
|
|
6
8
|
function defaultStatus(message) {
|
|
@@ -31,7 +33,7 @@ function preserveProviderSelection(serverState) {
|
|
|
31
33
|
: null;
|
|
32
34
|
return providerSelection ? { providerSelection } : {};
|
|
33
35
|
}
|
|
34
|
-
export function createSession({ rootDir, autoYes = false, agentMode, modelId, maxToolSteps = DEFAULT_MAX_TOOL_STEPS,
|
|
36
|
+
export function createSession({ rootDir, autoYes = false, agentMode, modelId, maxToolSteps = DEFAULT_MAX_TOOL_STEPS, maxToolStepsWasExplicit = false, requestPermission = null, requestSudoPassword = null, requestUserInput = null, onStatus = null, onContextLog = null, onToolEvent = null, env = process.env, sessionId = createSessionId(), sessionName = null, history = [], serverState = null, editJournal = [], stickyFilePaths = [], editCounter = 0, safety = createSessionSafetyState(), }) {
|
|
35
37
|
const createdAt = new Date().toISOString();
|
|
36
38
|
const initialAgentMode = normalizeAgentMode(agentMode ?? (autoYes ? 'auto-accept' : 'default'));
|
|
37
39
|
return {
|
|
@@ -40,12 +42,14 @@ export function createSession({ rootDir, autoYes = false, agentMode, modelId, ma
|
|
|
40
42
|
autoYes: initialAgentMode === 'auto-accept',
|
|
41
43
|
agentMode: initialAgentMode,
|
|
42
44
|
maxToolSteps: clampInteger(maxToolSteps, DEFAULT_MAX_TOOL_STEPS, 128),
|
|
45
|
+
maxToolStepsWasExplicit: maxToolStepsWasExplicit === true,
|
|
43
46
|
onStatus: onStatus ?? defaultStatus,
|
|
44
47
|
onContextLog: onContextLog ?? defaultContextLog,
|
|
45
48
|
onToolEvent,
|
|
46
|
-
|
|
47
|
-
|
|
49
|
+
grants: createSessionGrants(),
|
|
50
|
+
requestPermission,
|
|
48
51
|
requestSudoPassword,
|
|
52
|
+
requestUserInput,
|
|
49
53
|
history: JSON.parse(JSON.stringify(history)),
|
|
50
54
|
initialized: true,
|
|
51
55
|
sessionId,
|
|
@@ -69,8 +73,17 @@ export function createSession({ rootDir, autoYes = false, agentMode, modelId, ma
|
|
|
69
73
|
serverState: cloneOpaqueState(serverState),
|
|
70
74
|
};
|
|
71
75
|
}
|
|
76
|
+
export function startNewConversation(session) {
|
|
77
|
+
clearConversation(session);
|
|
78
|
+
const createdAt = new Date().toISOString();
|
|
79
|
+
session.sessionId = createSessionId();
|
|
80
|
+
session.sessionName = null;
|
|
81
|
+
session.sessionCreatedAt = createdAt;
|
|
82
|
+
session.sessionUpdatedAt = createdAt;
|
|
83
|
+
}
|
|
72
84
|
export function clearConversation(session) {
|
|
73
85
|
session.history = [];
|
|
86
|
+
session.grants = createSessionGrants();
|
|
74
87
|
session.serverState = preserveProviderSelection(session.serverState);
|
|
75
88
|
session.turnState = {
|
|
76
89
|
id: null,
|
|
@@ -86,7 +99,9 @@ export function clearConversation(session) {
|
|
|
86
99
|
safety: createSessionSafetyState(),
|
|
87
100
|
};
|
|
88
101
|
}
|
|
89
|
-
export async function disposeSession(_session) {
|
|
102
|
+
export async function disposeSession(_session) {
|
|
103
|
+
closeBrowserBridge();
|
|
104
|
+
}
|
|
90
105
|
export function switchModel(session, modelId) {
|
|
91
106
|
session.modelId = Number(modelId);
|
|
92
107
|
return { id: session.modelId };
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { stdin as input, stdout as output } from 'node:process';
|
|
2
|
+
import readline from 'node:readline/promises';
|
|
3
|
+
import chalk from './colors.js';
|
|
4
|
+
import { auth } from './api/index.js';
|
|
5
|
+
import { DEFAULT_THEGITAI_HOST } from './api/default-host.js';
|
|
6
|
+
import { loginViaBrowser } from './api/browser-login.js';
|
|
7
|
+
function hyperlink(url, label) {
|
|
8
|
+
if (process.stdout.isTTY !== true)
|
|
9
|
+
return label;
|
|
10
|
+
return `\x1b]8;;${url}\x07${label}\x1b]8;;\x07`;
|
|
11
|
+
}
|
|
12
|
+
export function formatSignInScreen({ url }) {
|
|
13
|
+
return [
|
|
14
|
+
'',
|
|
15
|
+
` ${chalk.bold.cyan('TheGitAI')}`,
|
|
16
|
+
'',
|
|
17
|
+
` ${chalk.bold('Sign in to continue.')}`,
|
|
18
|
+
' Your browser should open automatically. If not, copy this URL:',
|
|
19
|
+
'',
|
|
20
|
+
` ${chalk.cyan(url)}`,
|
|
21
|
+
'',
|
|
22
|
+
` ${hyperlink(url, chalk.cyan('→ Click here to authenticate'))}`,
|
|
23
|
+
'',
|
|
24
|
+
' If this terminal is not on the computer where that page opened, choose',
|
|
25
|
+
` ${chalk.bold('"On another computer"')} there and paste the code it gives you below.`,
|
|
26
|
+
'',
|
|
27
|
+
].join('\n');
|
|
28
|
+
}
|
|
29
|
+
async function promptForCode(signal) {
|
|
30
|
+
const rl = readline.createInterface({ input, output });
|
|
31
|
+
try {
|
|
32
|
+
return await rl.question(` ${chalk.bold('Authorization code:')} `, { signal });
|
|
33
|
+
}
|
|
34
|
+
finally {
|
|
35
|
+
rl.close();
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
export async function runSignIn({ env = process.env, login = loginViaBrowser, write = auth.writeCliAuthConfig, log = (line) => console.log(line), } = {}) {
|
|
39
|
+
const result = await login({
|
|
40
|
+
serverUrl: DEFAULT_THEGITAI_HOST,
|
|
41
|
+
deviceName: env.THEGITAI_DEVICE_NAME?.trim() || undefined,
|
|
42
|
+
onUrl: (url) => log(formatSignInScreen({ url })),
|
|
43
|
+
onBrowserOpen: (opened) => {
|
|
44
|
+
if (!opened) {
|
|
45
|
+
log(chalk.dim(' (no browser opened — use the URL or the code box above)'));
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
promptCode: promptForCode,
|
|
49
|
+
onPasteRejected: (message) => log(` ${chalk.red('✖')} ${message}\n` +
|
|
50
|
+
' Codes last 10 minutes. If that page has been open a while, run `ai`\n' +
|
|
51
|
+
' again for a fresh one.'),
|
|
52
|
+
});
|
|
53
|
+
write(result, env);
|
|
54
|
+
log('');
|
|
55
|
+
log(chalk.green(` ✓ Signed in as ${result.customer.email}`));
|
|
56
|
+
log('');
|
|
57
|
+
return result;
|
|
58
|
+
}
|