picocode-core 0.9.119
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 +15 -0
- package/package.json +33 -0
- package/src/agent-transcript.js +80 -0
- package/src/agent.js +170 -0
- package/src/agents.js +213 -0
- package/src/attachments.js +141 -0
- package/src/boot.js +28 -0
- package/src/catalog-snapshot.json +1 -0
- package/src/catalog.js +86 -0
- package/src/codex-models.js +56 -0
- package/src/commands.js +61 -0
- package/src/compaction.js +82 -0
- package/src/completion.js +21 -0
- package/src/config.js +32 -0
- package/src/context.js +82 -0
- package/src/controller.js +1263 -0
- package/src/conversation-search.js +101 -0
- package/src/deliberation-history.js +65 -0
- package/src/deliberation.js +61 -0
- package/src/derive.js +307 -0
- package/src/events.js +54 -0
- package/src/export.js +16 -0
- package/src/files.js +39 -0
- package/src/format.js +6 -0
- package/src/fuzzy.js +41 -0
- package/src/git.js +156 -0
- package/src/history.js +51 -0
- package/src/init.js +25 -0
- package/src/keys.js +26 -0
- package/src/mcp.js +280 -0
- package/src/memory.js +120 -0
- package/src/models.js +14 -0
- package/src/openai-auth.js +204 -0
- package/src/paths.js +67 -0
- package/src/reversible-edit.js +79 -0
- package/src/rewind.js +84 -0
- package/src/session-index.js +264 -0
- package/src/session-lock.js +27 -0
- package/src/session.js +160 -0
- package/src/shells.js +166 -0
- package/src/skills.js +164 -0
- package/src/steer.js +129 -0
- package/src/system-prompt.js +49 -0
- package/src/terminal-theme.js +49 -0
- package/src/tools/bash.js +184 -0
- package/src/tools/diff.js +18 -0
- package/src/tools/edit.js +95 -0
- package/src/tools/glob.js +43 -0
- package/src/tools/grep.js +59 -0
- package/src/tools/index.js +296 -0
- package/src/tools/read.js +49 -0
- package/src/tools/recorder.js +74 -0
- package/src/tools/web.js +84 -0
- package/src/tools/write.js +48 -0
- package/src/update.js +82 -0
- package/src/user-tools.js +59 -0
- package/src/wakeups.js +40 -0
package/src/models.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export function findModel(models, name) {
|
|
2
|
+
return models.find((m) => m.name === name) || null
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export function defaultModel(models) {
|
|
6
|
+
return models.find((m) => m.available !== false) || null
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function estimateCost(model, usage) {
|
|
10
|
+
if (!model?.price || !usage) return 0
|
|
11
|
+
const inCost = (usage.promptTokens || 0) * model.price.in
|
|
12
|
+
const outCost = (usage.completionTokens || 0) * model.price.out
|
|
13
|
+
return (inCost + outCost) / 1e6
|
|
14
|
+
}
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import { createServer } from 'node:http'
|
|
2
|
+
import { randomBytes, createHash } from 'node:crypto'
|
|
3
|
+
import { execFile } from 'node:child_process'
|
|
4
|
+
import { readFile, writeFile, chmod } from 'node:fs/promises'
|
|
5
|
+
import { join } from 'node:path'
|
|
6
|
+
import { picoHome, ensureDir } from './paths.js'
|
|
7
|
+
|
|
8
|
+
const CLIENT_ID = 'app_EMoamEEZ73f0CkXaXp7hrann'
|
|
9
|
+
const AUTH_URL = 'https://auth.openai.com/oauth/authorize'
|
|
10
|
+
const TOKEN_URL = 'https://auth.openai.com/oauth/token'
|
|
11
|
+
const REDIRECT_PORT = 1455
|
|
12
|
+
const REDIRECT_URI = `http://localhost:${REDIRECT_PORT}/auth/callback`
|
|
13
|
+
const SCOPE = 'openid profile email offline_access'
|
|
14
|
+
const REFRESH_SKEW_MS = 5 * 60 * 1000
|
|
15
|
+
|
|
16
|
+
function authFile() {
|
|
17
|
+
return join(picoHome(), 'auth.json')
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function openInBrowser(url) {
|
|
21
|
+
const [command, args] = process.platform === 'darwin'
|
|
22
|
+
? ['open', [url]]
|
|
23
|
+
: process.platform === 'win32'
|
|
24
|
+
? ['explorer.exe', [url]]
|
|
25
|
+
: ['xdg-open', [url]]
|
|
26
|
+
execFile(command, args, () => {})
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function base64url(buffer) {
|
|
30
|
+
return buffer.toString('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '')
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function decodeJwtClaims(token) {
|
|
34
|
+
try {
|
|
35
|
+
const payload = token.split('.')[1]
|
|
36
|
+
return JSON.parse(Buffer.from(payload, 'base64url').toString('utf-8'))
|
|
37
|
+
} catch {
|
|
38
|
+
return {}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function accountIdFromClaims(claims) {
|
|
43
|
+
return claims['https://api.openai.com/auth']?.chatgpt_account_id || claims.chatgpt_account_id || null
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function readAuth() {
|
|
47
|
+
try {
|
|
48
|
+
return JSON.parse(await readFile(authFile(), 'utf-8'))
|
|
49
|
+
} catch {
|
|
50
|
+
return {}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async function writeAuth(auth) {
|
|
55
|
+
ensureDir(picoHome())
|
|
56
|
+
await writeFile(authFile(), JSON.stringify(auth, null, 2) + '\n')
|
|
57
|
+
await chmod(authFile(), 0o600)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async function tokenRequest(params) {
|
|
61
|
+
const response = await fetch(TOKEN_URL, {
|
|
62
|
+
method: 'POST',
|
|
63
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
64
|
+
body: new URLSearchParams(params).toString(),
|
|
65
|
+
})
|
|
66
|
+
if (!response.ok) {
|
|
67
|
+
throw new Error(`token request failed: ${response.status} ${await response.text()}`)
|
|
68
|
+
}
|
|
69
|
+
return response.json()
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function storeTokens(auth, tokens) {
|
|
73
|
+
const claims = tokens.id_token ? decodeJwtClaims(tokens.id_token) : {}
|
|
74
|
+
auth.openai = {
|
|
75
|
+
access_token: tokens.access_token,
|
|
76
|
+
refresh_token: tokens.refresh_token || auth.openai?.refresh_token,
|
|
77
|
+
id_token: tokens.id_token || auth.openai?.id_token,
|
|
78
|
+
account_id: accountIdFromClaims(claims) || auth.openai?.account_id || null,
|
|
79
|
+
email: claims.email || auth.openai?.email || null,
|
|
80
|
+
expires_at: Date.now() + (tokens.expires_in || 3600) * 1000,
|
|
81
|
+
}
|
|
82
|
+
return auth
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export async function connectOpenAI({ openBrowser = true, timeoutMs = 5 * 60 * 1000, onUrl = () => {} } = {}) {
|
|
86
|
+
const verifier = base64url(randomBytes(64))
|
|
87
|
+
const challenge = base64url(createHash('sha256').update(verifier).digest())
|
|
88
|
+
const state = base64url(randomBytes(24))
|
|
89
|
+
|
|
90
|
+
const url = `${AUTH_URL}?${new URLSearchParams({
|
|
91
|
+
response_type: 'code',
|
|
92
|
+
client_id: CLIENT_ID,
|
|
93
|
+
redirect_uri: REDIRECT_URI,
|
|
94
|
+
scope: SCOPE,
|
|
95
|
+
code_challenge: challenge,
|
|
96
|
+
code_challenge_method: 'S256',
|
|
97
|
+
state,
|
|
98
|
+
prompt: 'login',
|
|
99
|
+
id_token_add_organizations: 'true',
|
|
100
|
+
codex_cli_simplified_flow: 'true',
|
|
101
|
+
originator: 'codex_cli_rs',
|
|
102
|
+
}).toString()}`
|
|
103
|
+
|
|
104
|
+
const code = await new Promise((resolve, reject) => {
|
|
105
|
+
const server = createServer((req, res) => {
|
|
106
|
+
const requestUrl = new URL(req.url, `http://localhost:${REDIRECT_PORT}`)
|
|
107
|
+
if (requestUrl.pathname !== '/auth/callback') {
|
|
108
|
+
res.writeHead(404).end()
|
|
109
|
+
return
|
|
110
|
+
}
|
|
111
|
+
const gotState = requestUrl.searchParams.get('state')
|
|
112
|
+
const gotCode = requestUrl.searchParams.get('code')
|
|
113
|
+
const gotError = requestUrl.searchParams.get('error')
|
|
114
|
+
res.writeHead(200, { 'Content-Type': 'text/html' })
|
|
115
|
+
res.end(
|
|
116
|
+
gotCode && gotState === state
|
|
117
|
+
? '<html><body style="font-family: sans-serif; padding: 2rem">signed in - you can return to pico</body></html>'
|
|
118
|
+
: '<html><body style="font-family: sans-serif; padding: 2rem">sign-in failed - return to pico and try again</body></html>',
|
|
119
|
+
)
|
|
120
|
+
cleanup()
|
|
121
|
+
if (gotError) reject(new Error(`authorization failed: ${gotError}`))
|
|
122
|
+
else if (!gotCode || gotState !== state) reject(new Error('authorization failed: missing code or state mismatch'))
|
|
123
|
+
else resolve(gotCode)
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
const timer = setTimeout(() => {
|
|
127
|
+
cleanup()
|
|
128
|
+
reject(new Error('sign-in timed out'))
|
|
129
|
+
}, timeoutMs)
|
|
130
|
+
|
|
131
|
+
function cleanup() {
|
|
132
|
+
clearTimeout(timer)
|
|
133
|
+
server.close()
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
server.on('error', (err) => {
|
|
137
|
+
cleanup()
|
|
138
|
+
reject(err.code === 'EADDRINUSE' ? new Error(`port ${REDIRECT_PORT} is in use (is another sign-in or codex running?)`) : err)
|
|
139
|
+
})
|
|
140
|
+
server.listen(REDIRECT_PORT, '127.0.0.1', () => {
|
|
141
|
+
onUrl(url)
|
|
142
|
+
if (openBrowser) openInBrowser(url)
|
|
143
|
+
})
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
const tokens = await tokenRequest({
|
|
147
|
+
grant_type: 'authorization_code',
|
|
148
|
+
code,
|
|
149
|
+
redirect_uri: REDIRECT_URI,
|
|
150
|
+
client_id: CLIENT_ID,
|
|
151
|
+
code_verifier: verifier,
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
const auth = storeTokens(await readAuth(), tokens)
|
|
155
|
+
await writeAuth(auth)
|
|
156
|
+
return { email: auth.openai.email, accountId: auth.openai.account_id }
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export async function openaiCredentials() {
|
|
160
|
+
const auth = await readAuth()
|
|
161
|
+
const stored = auth.openai
|
|
162
|
+
if (!stored?.access_token) return null
|
|
163
|
+
|
|
164
|
+
if (Date.now() > (stored.expires_at || 0) - REFRESH_SKEW_MS) {
|
|
165
|
+
if (!stored.refresh_token) return null
|
|
166
|
+
const tokens = await tokenRequest({
|
|
167
|
+
grant_type: 'refresh_token',
|
|
168
|
+
refresh_token: stored.refresh_token,
|
|
169
|
+
client_id: CLIENT_ID,
|
|
170
|
+
scope: 'openid profile email',
|
|
171
|
+
})
|
|
172
|
+
const refreshed = storeTokens(auth, tokens)
|
|
173
|
+
await writeAuth(refreshed)
|
|
174
|
+
return credentialsFrom(refreshed.openai)
|
|
175
|
+
}
|
|
176
|
+
return credentialsFrom(stored)
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function credentialsFrom(stored) {
|
|
180
|
+
return {
|
|
181
|
+
apiKey: stored.access_token,
|
|
182
|
+
headers: stored.account_id ? { 'chatgpt-account-id': stored.account_id } : {},
|
|
183
|
+
email: stored.email,
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export async function openaiConnected() {
|
|
188
|
+
const auth = await readAuth()
|
|
189
|
+
return !!(auth.openai?.access_token && auth.openai?.refresh_token)
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export async function openaiStatus() {
|
|
193
|
+
const auth = await readAuth()
|
|
194
|
+
return {
|
|
195
|
+
connected: !!(auth.openai?.access_token && auth.openai?.refresh_token),
|
|
196
|
+
email: auth.openai?.email || null,
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export async function disconnectOpenAI() {
|
|
201
|
+
const auth = await readAuth()
|
|
202
|
+
delete auth.openai
|
|
203
|
+
await writeAuth(auth)
|
|
204
|
+
}
|
package/src/paths.js
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { existsSync, mkdirSync } from 'node:fs'
|
|
2
|
+
import { homedir } from 'node:os'
|
|
3
|
+
import { dirname, join, resolve } from 'node:path'
|
|
4
|
+
|
|
5
|
+
export function picoHome() {
|
|
6
|
+
return process.env.PICO_HOME || join(homedir(), '.pico')
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function projectKey(root) {
|
|
10
|
+
return resolve(root).replace(/[/\\:]/g, '-')
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function findProjectRoot(cwd) {
|
|
14
|
+
let dir = resolve(cwd)
|
|
15
|
+
const home = homedir()
|
|
16
|
+
while (true) {
|
|
17
|
+
if (existsSync(join(dir, '.git'))) return dir
|
|
18
|
+
const parent = dirname(dir)
|
|
19
|
+
if (parent === dir || dir === home) return resolve(cwd)
|
|
20
|
+
dir = parent
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function projectDir(root) {
|
|
25
|
+
return join(picoHome(), 'projects', projectKey(root))
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function sessionsDir(root) {
|
|
29
|
+
return join(projectDir(root), 'sessions')
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function sessionScratchDir(root, sessionId) {
|
|
33
|
+
return join(projectDir(root), 'scratchpads', sessionId)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function agentScratchDir(root, sessionId, agentId) {
|
|
37
|
+
return join(sessionScratchDir(root, sessionId), `agent-${agentId}`)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function projectHistoryFile(root) {
|
|
41
|
+
return join(projectDir(root), 'history.jsonl')
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function projectMcpFile(root) {
|
|
45
|
+
return join(projectDir(root), 'mcp.json')
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function globalMcpFile() {
|
|
49
|
+
return join(picoHome(), 'mcp.json')
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function globalSkillsDir() {
|
|
53
|
+
return join(picoHome(), 'skills')
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function projectSkillsDir(root) {
|
|
57
|
+
return join(root, '.pico', 'skills')
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function globalAgentsFile() {
|
|
61
|
+
return join(picoHome(), 'AGENTS.md')
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function ensureDir(dir) {
|
|
65
|
+
mkdirSync(dir, { recursive: true })
|
|
66
|
+
return dir
|
|
67
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto'
|
|
2
|
+
|
|
3
|
+
export const REVERSIBLE_EDIT_VERSION = 2
|
|
4
|
+
|
|
5
|
+
export function contentHash(text) {
|
|
6
|
+
return createHash('sha256').update(text, 'utf-8').digest('hex')
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function state(exists, text) {
|
|
10
|
+
return { exists, hash: exists ? contentHash(text) : null }
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function makeReversibleEdit(path, before, after, splices, { beforeExists = true, afterExists = true } = {}) {
|
|
14
|
+
return {
|
|
15
|
+
version: REVERSIBLE_EDIT_VERSION,
|
|
16
|
+
path,
|
|
17
|
+
before: state(beforeExists, before),
|
|
18
|
+
after: state(afterExists, after),
|
|
19
|
+
splices,
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function makeWriteEdit(path, before, after, beforeExists) {
|
|
24
|
+
let start = 0
|
|
25
|
+
const shared = Math.min(before.length, after.length)
|
|
26
|
+
while (start < shared && before.charCodeAt(start) === after.charCodeAt(start)) start++
|
|
27
|
+
|
|
28
|
+
let beforeEnd = before.length
|
|
29
|
+
let afterEnd = after.length
|
|
30
|
+
while (beforeEnd > start && afterEnd > start && before.charCodeAt(beforeEnd - 1) === after.charCodeAt(afterEnd - 1)) {
|
|
31
|
+
beforeEnd--
|
|
32
|
+
afterEnd--
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return makeReversibleEdit(path, before, after, [{
|
|
36
|
+
start,
|
|
37
|
+
oldText: before.slice(start, beforeEnd),
|
|
38
|
+
newText: after.slice(start, afterEnd),
|
|
39
|
+
}], { beforeExists })
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function reversibleEditVersion(revert) {
|
|
43
|
+
if (revert?.version === REVERSIBLE_EDIT_VERSION && Array.isArray(revert.splices)) return 2
|
|
44
|
+
if (typeof revert?.before === 'string' && typeof revert?.after === 'string') return 1
|
|
45
|
+
return null
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function validatedSplices(revert) {
|
|
49
|
+
let end = 0
|
|
50
|
+
return revert.splices.map((splice) => {
|
|
51
|
+
if (!Number.isSafeInteger(splice?.start) || splice.start < end || typeof splice.oldText !== 'string' || typeof splice.newText !== 'string') {
|
|
52
|
+
throw new Error('invalid reversible edit splices')
|
|
53
|
+
}
|
|
54
|
+
end = splice.start + splice.oldText.length
|
|
55
|
+
return splice
|
|
56
|
+
})
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function applySplices(source, revert, direction) {
|
|
60
|
+
const splices = validatedSplices(revert)
|
|
61
|
+
const parts = []
|
|
62
|
+
let sourceAt = 0
|
|
63
|
+
let delta = 0
|
|
64
|
+
|
|
65
|
+
for (const splice of splices) {
|
|
66
|
+
const start = direction === 'forward' ? splice.start : splice.start + delta
|
|
67
|
+
const oldText = direction === 'forward' ? splice.oldText : splice.newText
|
|
68
|
+
const newText = direction === 'forward' ? splice.newText : splice.oldText
|
|
69
|
+
if (start < sourceAt || start + oldText.length > source.length || source.slice(start, start + oldText.length) !== oldText) {
|
|
70
|
+
throw new Error('reversible edit does not match file content')
|
|
71
|
+
}
|
|
72
|
+
parts.push(source.slice(sourceAt, start), newText)
|
|
73
|
+
sourceAt = start + oldText.length
|
|
74
|
+
delta += splice.newText.length - splice.oldText.length
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
parts.push(source.slice(sourceAt))
|
|
78
|
+
return parts.join('')
|
|
79
|
+
}
|
package/src/rewind.js
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { mkdir, readFile, rm, writeFile } from 'node:fs/promises'
|
|
2
|
+
import { dirname } from 'node:path'
|
|
3
|
+
import { applySplices, contentHash, reversibleEditVersion } from './reversible-edit.js'
|
|
4
|
+
|
|
5
|
+
export function implicitRewindTarget(state, input) {
|
|
6
|
+
if (input !== '') return null
|
|
7
|
+
const last = state.transcript.at(-1)
|
|
8
|
+
if (last?.kind !== 'user') return null
|
|
9
|
+
return { text: last.text, content: last.content, index: state.transcript.length - 1, eventId: last.eventId }
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
async function readCurrent(path) {
|
|
13
|
+
try {
|
|
14
|
+
return { exists: true, text: await readFile(path, 'utf-8') }
|
|
15
|
+
} catch (err) {
|
|
16
|
+
if (err.code === 'ENOENT') return { exists: false, text: '' }
|
|
17
|
+
throw err
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function matches(current, expected) {
|
|
22
|
+
return current.exists === expected.exists && (!current.exists || contentHash(current.text) === expected.hash)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async function writeState(path, state) {
|
|
26
|
+
if (!state.exists) {
|
|
27
|
+
await rm(path, { force: true })
|
|
28
|
+
return
|
|
29
|
+
}
|
|
30
|
+
await mkdir(dirname(path), { recursive: true })
|
|
31
|
+
await writeFile(path, state.text, 'utf-8')
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async function transform(edit, direction) {
|
|
35
|
+
const revert = edit.revert
|
|
36
|
+
const version = reversibleEditVersion(revert)
|
|
37
|
+
if (!version) return { reason: 'invalid reversible edit data' }
|
|
38
|
+
const current = await readCurrent(revert.path)
|
|
39
|
+
|
|
40
|
+
if (version === 1) {
|
|
41
|
+
const expected = direction === 'undo' ? revert.after : revert.before
|
|
42
|
+
if (!current.exists || current.text !== expected) return { reason: direction === 'undo' ? 'file changed since edit' : 'file changed since revert' }
|
|
43
|
+
await writeState(revert.path, { exists: true, text: direction === 'undo' ? revert.before : revert.after })
|
|
44
|
+
return { ok: true }
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const expected = direction === 'undo' ? revert.after : revert.before
|
|
48
|
+
const target = direction === 'undo' ? revert.before : revert.after
|
|
49
|
+
if (!matches(current, expected)) return { reason: direction === 'undo' ? 'file changed since edit' : 'file changed since revert' }
|
|
50
|
+
|
|
51
|
+
try {
|
|
52
|
+
const text = applySplices(current.text, revert, direction === 'undo' ? 'reverse' : 'forward')
|
|
53
|
+
if (target.exists && contentHash(text) !== target.hash) return { reason: 'reversible edit data is corrupt' }
|
|
54
|
+
await writeState(revert.path, { exists: target.exists, text })
|
|
55
|
+
return { ok: true }
|
|
56
|
+
} catch {
|
|
57
|
+
return { reason: 'reversible edit data is corrupt' }
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async function applyEdits(edits, direction) {
|
|
62
|
+
const applied = []
|
|
63
|
+
const skipped = []
|
|
64
|
+
for (const edit of direction === 'undo' ? [...edits].reverse() : edits) {
|
|
65
|
+
try {
|
|
66
|
+
const result = await transform(edit, direction)
|
|
67
|
+
if (result.ok) applied.push(edit.callId)
|
|
68
|
+
else skipped.push({ path: edit.revert?.path, callId: edit.callId, reason: result.reason })
|
|
69
|
+
} catch (err) {
|
|
70
|
+
skipped.push({ path: edit.revert?.path, callId: edit.callId, reason: err.message || String(err) })
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return { applied, skipped }
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export async function revertEdits(edits) {
|
|
77
|
+
const { applied, skipped } = await applyEdits(edits, 'undo')
|
|
78
|
+
return { reverted: applied, skipped }
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export async function reapplyEdits(edits) {
|
|
82
|
+
const { applied, skipped } = await applyEdits(edits, 'redo')
|
|
83
|
+
return { reapplied: applied, skipped }
|
|
84
|
+
}
|