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.
Files changed (57) hide show
  1. package/README.md +15 -0
  2. package/package.json +33 -0
  3. package/src/agent-transcript.js +80 -0
  4. package/src/agent.js +170 -0
  5. package/src/agents.js +213 -0
  6. package/src/attachments.js +141 -0
  7. package/src/boot.js +28 -0
  8. package/src/catalog-snapshot.json +1 -0
  9. package/src/catalog.js +86 -0
  10. package/src/codex-models.js +56 -0
  11. package/src/commands.js +61 -0
  12. package/src/compaction.js +82 -0
  13. package/src/completion.js +21 -0
  14. package/src/config.js +32 -0
  15. package/src/context.js +82 -0
  16. package/src/controller.js +1263 -0
  17. package/src/conversation-search.js +101 -0
  18. package/src/deliberation-history.js +65 -0
  19. package/src/deliberation.js +61 -0
  20. package/src/derive.js +307 -0
  21. package/src/events.js +54 -0
  22. package/src/export.js +16 -0
  23. package/src/files.js +39 -0
  24. package/src/format.js +6 -0
  25. package/src/fuzzy.js +41 -0
  26. package/src/git.js +156 -0
  27. package/src/history.js +51 -0
  28. package/src/init.js +25 -0
  29. package/src/keys.js +26 -0
  30. package/src/mcp.js +280 -0
  31. package/src/memory.js +120 -0
  32. package/src/models.js +14 -0
  33. package/src/openai-auth.js +204 -0
  34. package/src/paths.js +67 -0
  35. package/src/reversible-edit.js +79 -0
  36. package/src/rewind.js +84 -0
  37. package/src/session-index.js +264 -0
  38. package/src/session-lock.js +27 -0
  39. package/src/session.js +160 -0
  40. package/src/shells.js +166 -0
  41. package/src/skills.js +164 -0
  42. package/src/steer.js +129 -0
  43. package/src/system-prompt.js +49 -0
  44. package/src/terminal-theme.js +49 -0
  45. package/src/tools/bash.js +184 -0
  46. package/src/tools/diff.js +18 -0
  47. package/src/tools/edit.js +95 -0
  48. package/src/tools/glob.js +43 -0
  49. package/src/tools/grep.js +59 -0
  50. package/src/tools/index.js +296 -0
  51. package/src/tools/read.js +49 -0
  52. package/src/tools/recorder.js +74 -0
  53. package/src/tools/web.js +84 -0
  54. package/src/tools/write.js +48 -0
  55. package/src/update.js +82 -0
  56. package/src/user-tools.js +59 -0
  57. 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
+ }