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
@@ -0,0 +1,101 @@
1
+ function escapeRegex(value) {
2
+ return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
3
+ }
4
+
5
+ function visibleLines(text, limit) {
6
+ return String(text).split('\n').slice(0, limit).join('\n')
7
+ }
8
+
9
+ export function matchOffsets(text, query) {
10
+ if (!query) return []
11
+ const matches = []
12
+ const pattern = new RegExp(escapeRegex(query), 'gi')
13
+ let match
14
+ while ((match = pattern.exec(String(text))) !== null) matches.push(match.index)
15
+ return matches
16
+ }
17
+
18
+ export function highlightMatches(text, query, currentIndex = -1, startIndex = 0) {
19
+ if (!query) return text
20
+ const pattern = new RegExp(escapeRegex(query), 'gi')
21
+ const parts = String(text).split(/(\x1b\[[0-9;]*m)/)
22
+ let index = startIndex
23
+ return parts.map((part) => {
24
+ if (part.startsWith('\x1b[')) return part
25
+ return part.replace(pattern, (match) => {
26
+ const current = index++ === currentIndex
27
+ return current
28
+ ? `\x1b[7;33m${match}\x1b[27;39m`
29
+ : `\x1b[7m${match}\x1b[27m`
30
+ })
31
+ }).join('')
32
+ }
33
+
34
+ function searchableFields(item, verbose) {
35
+ if (item.kind === 'tool-group') return item.tools.flatMap((tool) => searchableFields(tool, verbose))
36
+ if (item.kind === 'agent-notice-group') {
37
+ return verbose ? item.notices.map((notice, index) => ({ field: 'text', text: notice.text, lineOffset: 3 + index })) : []
38
+ }
39
+ if (item.kind === 'thoughts') {
40
+ return verbose && item.text ? [{ field: 'text', text: visibleLines(item.text, 300), lineOffset: 3 }] : []
41
+ }
42
+ if (item.kind === 'summary') {
43
+ const fields = [{ field: 'text', text: visibleLines(item.text, verbose ? 500 : 0), lineOffset: 3 }]
44
+ return verbose ? fields : []
45
+ }
46
+ if (item.kind === 'tool') {
47
+ const fields = [{ field: 'title', text: item.title || item.name || '', lineOffset: 1 }]
48
+ if (verbose && item.fullOutput) fields.push({ field: 'fullOutput', text: visibleLines(item.fullOutput, 200), lineOffset: 4 })
49
+ return fields
50
+ }
51
+ return item.text ? [{ field: 'text', text: item.text, lineOffset: 1 }] : []
52
+ }
53
+
54
+ export function conversationMatches(items, query, verbose = false) {
55
+ const matches = []
56
+ items.forEach((item, itemIndex) => {
57
+ searchableFields(item, verbose).forEach(({ field, text, lineOffset }, fieldIndex) => {
58
+ matchOffsets(text, query).forEach((offset, occurrenceIndex) => {
59
+ const line = lineOffset + String(text).slice(0, offset).split('\n').length - 1
60
+ matches.push({ itemIndex, field, fieldIndex, offset, line, occurrenceIndex })
61
+ })
62
+ })
63
+ })
64
+ return matches
65
+ }
66
+
67
+ export function highlightConversation(items, query, currentIndex, verbose = false) {
68
+ let startIndex = 0
69
+ function decorate(item) {
70
+ if (item.kind === 'tool-group') return { ...item, tools: item.tools.map(decorate) }
71
+ if (item.kind === 'agent-notice-group') {
72
+ if (!verbose) return item
73
+ return { ...item, notices: item.notices.map(decorate) }
74
+ }
75
+ if (item.kind === 'thoughts' && !verbose) return item
76
+ if (item.kind === 'summary' && !verbose) return item
77
+ if (item.kind === 'tool') {
78
+ const decorated = { ...item }
79
+ const title = item.title || item.name || ''
80
+ const titleCount = matchOffsets(title, query).length
81
+ decorated.title = highlightMatches(title, query, currentIndex, startIndex)
82
+ startIndex += titleCount
83
+ if (verbose && item.fullOutput) {
84
+ const visibleOutput = visibleLines(item.fullOutput, 200)
85
+ const outputCount = matchOffsets(visibleOutput, query).length
86
+ decorated.fullOutput = highlightMatches(item.fullOutput, query, currentIndex, startIndex)
87
+ startIndex += outputCount
88
+ }
89
+ return decorated
90
+ }
91
+ if (!item.text) return item
92
+ const visibleText = item.kind === 'thoughts' ? visibleLines(item.text, 300)
93
+ : item.kind === 'summary' ? visibleLines(item.text, 500)
94
+ : item.text
95
+ const count = matchOffsets(visibleText, query).length
96
+ const decorated = { ...item, text: highlightMatches(item.text, query, currentIndex, startIndex) }
97
+ startIndex += count
98
+ return decorated
99
+ }
100
+ return items.map(decorate)
101
+ }
@@ -0,0 +1,65 @@
1
+ export function deliberationsFromEvents(events) {
2
+ const byId = new Map()
3
+
4
+ for (const event of events) {
5
+ const data = event.data || {}
6
+ const id = data.deliberationId
7
+ if (!id) continue
8
+
9
+ if (event.type === 'deliberation_dismiss') {
10
+ byId.delete(id)
11
+ continue
12
+ }
13
+
14
+ if (event.type === 'deliberation_start') {
15
+ byId.set(id, {
16
+ id: `deliberation-${id}`,
17
+ deliberationId: id,
18
+ role: 'deliberation',
19
+ description: data.brief,
20
+ prompt: data.brief,
21
+ model: data.model,
22
+ rounds: data.rounds,
23
+ status: 'running',
24
+ startedAt: event.at,
25
+ updatedAt: event.at,
26
+ events: [],
27
+ turns: [],
28
+ timeline: [],
29
+ })
30
+ continue
31
+ }
32
+
33
+ const item = byId.get(id)
34
+ if (!item) continue
35
+ item.updatedAt = event.at
36
+
37
+ if (event.type === 'deliberation_event') {
38
+ const recorded = { ...data.event, role: data.role, round: data.round, at: event.at }
39
+ item.events.push(recorded)
40
+ item.timeline.push({ kind: 'event', value: recorded })
41
+ } else if (event.type === 'deliberation_turn') {
42
+ const turn = { role: data.role, round: data.round, text: data.text }
43
+ item.turns.push(turn)
44
+ item.timeline.push({ kind: 'turn', value: turn })
45
+ item.usage = mergeUsage(item.usage, data.usage)
46
+ } else if (event.type === 'deliberation_result') {
47
+ item.result = data.result
48
+ item.error = data.error
49
+ item.usage = mergeUsage(item.usage, data.usage)
50
+ item.status = data.error ? 'failed' : data.interrupted ? 'cancelled' : 'completed'
51
+ item.endedAt = event.at
52
+ }
53
+ }
54
+
55
+ return [...byId.values()].sort((a, b) => Number(b.deliberationId) - Number(a.deliberationId))
56
+ }
57
+
58
+ function mergeUsage(current = {}, next = {}) {
59
+ if (!next) return current
60
+ return {
61
+ promptTokens: (current.promptTokens || 0) + (next.promptTokens || 0),
62
+ completionTokens: (current.completionTokens || 0) + (next.completionTokens || 0),
63
+ totalTokens: (current.totalTokens || 0) + (next.totalTokens || 0),
64
+ }
65
+ }
@@ -0,0 +1,61 @@
1
+ import { resultText } from './agents.js'
2
+
3
+ export const DEFAULT_DELIBERATION_ROUNDS = 3
4
+ export const MAX_DELIBERATION_ROUNDS = 5
5
+
6
+ export function validateDeliberation({ brief, rounds = DEFAULT_DELIBERATION_ROUNDS } = {}) {
7
+ if (!brief?.trim()) throw new Error('deliberation brief is required')
8
+ if (!Number.isInteger(rounds) || rounds < 1 || rounds > MAX_DELIBERATION_ROUNDS) {
9
+ throw new Error(`deliberation rounds must be between 1 and ${MAX_DELIBERATION_ROUNDS}`)
10
+ }
11
+ return { brief: brief.trim(), rounds }
12
+ }
13
+
14
+ function participantPrompt(brief, role, rounds) {
15
+ return `You are the ${role} in a ${rounds}-round deliberation about the following decision:\n\n${brief}\n\nResearch before making claims. Use the available project and web tools whenever they can replace assumption with evidence. Cite URLs and project paths in your response. Challenge weak premises and address the peer's strongest points. Do not seek agreement for its own sake or defend a fixed position. Change your position only when evidence warrants it, and preserve material disagreement and uncertainty. Do not modify project files. Return a concise message addressed to the other participant.`
16
+ }
17
+
18
+ function peerMessage(role, text, round, rounds) {
19
+ return `Round ${round} of ${rounds}. The ${role} replied:\n\n${text}\n\nResearch and respond to the substance of this message.`
20
+ }
21
+
22
+ function synthesisPrompt(brief, turns) {
23
+ const transcript = turns.map((turn) => `Round ${turn.round}, ${turn.role}:\n${turn.text}`).join('\n\n')
24
+ return `Synthesize this deliberation into a decision for the main agent. State the recommendation, decisive evidence, unresolved uncertainty, material disagreement, and implementation constraints. Do not manufacture consensus: preserve disagreements that the evidence did not resolve. Preserve useful URLs and project paths. Do not mention the deliberation process unless disagreement remains.\n\nDecision brief:\n${brief}\n\nTranscript:\n${transcript}`
25
+ }
26
+
27
+ export async function runDeliberation({ brief, rounds, runParticipant, runSynthesis, onEvent = () => {}, signal }) {
28
+ const valid = validateDeliberation({ brief, rounds })
29
+ const participants = {
30
+ proposer: [{ role: 'user', content: participantPrompt(valid.brief, 'proposer', valid.rounds) }],
31
+ reviewer: [{ role: 'user', content: participantPrompt(valid.brief, 'reviewer', valid.rounds) }],
32
+ }
33
+ const turns = []
34
+ let peer = null
35
+
36
+ for (let round = 1; round <= valid.rounds; round++) {
37
+ for (const role of ['proposer', 'reviewer']) {
38
+ if (signal?.aborted) return { turns, interrupted: true }
39
+ const history = participants[role]
40
+ if (peer) history.push({ role: 'user', content: peerMessage(peer.role, peer.text, round, valid.rounds) })
41
+ const result = await runParticipant({ role, round, rounds: valid.rounds, history: [...history], signal })
42
+ history.push(...(result.messages || []))
43
+ const text = resultText(result.messages)
44
+ if (result.error) return { turns, interrupted: true, error: result.error }
45
+ if (result.interrupted || !text) return { turns, interrupted: true, error: text ? null : 'deliberation participant returned no response' }
46
+ const turn = { role, round, text, messages: result.messages || [], usage: result.usage || null }
47
+ turns.push(turn)
48
+ peer = turn
49
+ onEvent({ type: 'deliberation_turn', ...turn })
50
+ }
51
+ }
52
+
53
+ const synthesis = await runSynthesis({ history: [{ role: 'user', content: synthesisPrompt(valid.brief, turns) }], signal })
54
+ return {
55
+ turns,
56
+ result: resultText(synthesis.messages),
57
+ usage: synthesis.usage || null,
58
+ interrupted: !!synthesis.interrupted,
59
+ error: synthesis.error || null,
60
+ }
61
+ }
package/src/derive.js ADDED
@@ -0,0 +1,307 @@
1
+ import { continuationMessage } from './compaction.js'
2
+ import { applySteering } from './steer.js'
3
+
4
+ const DROPPING_MODES = ['both', 'chat', 'summary']
5
+
6
+ // context editing: a large tool result keeps getting re-sent with every
7
+ // request long after the model has used it. once the conversation has moved
8
+ // on (two user turns past it), the body is replaced with a short note in the
9
+ // model's view only; the transcript and the jsonl keep the real thing, and
10
+ // the model can re-run the tool if it truly needs the data again
11
+ const ELIDE_MIN_CHARS = 4000
12
+ const ELIDE_AFTER_USER_TURNS = 2
13
+
14
+ function elideStaleToolResults(history) {
15
+ const userIndexes = []
16
+ history.forEach((message, i) => {
17
+ if (message.role === 'user') userIndexes.push(i)
18
+ })
19
+ const cutoff = userIndexes.at(-ELIDE_AFTER_USER_TURNS)
20
+ if (cutoff === undefined) return history
21
+ return history.map((message, i) => {
22
+ if (i >= cutoff || message.role !== 'tool') return message
23
+ const content = String(message.content ?? '')
24
+ if (content.length < ELIDE_MIN_CHARS) return message
25
+ return {
26
+ ...message,
27
+ content: `[tool result elided to save context: ${content.length.toLocaleString()} chars. re-run the tool if this is needed again]`,
28
+ }
29
+ })
30
+ }
31
+
32
+ function activeRewinds(events) {
33
+ const canceled = new Set()
34
+ for (const e of events) {
35
+ if (e.type === 'rewind_undo') canceled.add(e.data.rewindId)
36
+ }
37
+ return events.filter((e) => e.type === 'rewind' && !canceled.has(e.id))
38
+ }
39
+
40
+ function droppedIds(events) {
41
+ const dropped = new Set()
42
+ const index = new Map(events.map((e, i) => [e.id, i]))
43
+ for (const rewind of activeRewinds(events)) {
44
+ if (!DROPPING_MODES.includes(rewind.data.mode)) continue
45
+ const from = index.get(rewind.data.target)
46
+ const to = index.get(rewind.id)
47
+ if (from === undefined || to === undefined) continue
48
+ for (let i = from; i < to; i++) dropped.add(events[i].id)
49
+ }
50
+ return dropped
51
+ }
52
+
53
+ function addUsageInto(total, usage) {
54
+ total.promptTokens += usage.promptTokens || 0
55
+ total.completionTokens += usage.completionTokens || 0
56
+ total.totalTokens += usage.totalTokens || 0
57
+ total.cachedTokens += usage.cachedTokens || 0
58
+ total.thoughtTokens += usage.thoughtTokens || 0
59
+ }
60
+
61
+ function emptyUsage() {
62
+ return { promptTokens: 0, completionTokens: 0, totalTokens: 0, cachedTokens: 0, thoughtTokens: 0 }
63
+ }
64
+
65
+ function parseArgs(raw) {
66
+ try {
67
+ return JSON.parse(raw)
68
+ } catch {
69
+ return { _raw: raw }
70
+ }
71
+ }
72
+
73
+ function pushHistory(state, message, eventId) {
74
+ state.providerHistory.push(message)
75
+ state.historyEventIds.push(eventId)
76
+ }
77
+
78
+ function foldMessage(state, event) {
79
+ const message = event.data.message
80
+ if (!message) return
81
+ pushHistory(state, message, event.id)
82
+ if (event.data.hideFromTranscript) {
83
+ for (const call of message.tool_calls || []) {
84
+ state.toolItems.set(call.id, { kind: 'tool', callId: call.id, status: 'done', hidden: true })
85
+ }
86
+ return
87
+ }
88
+ const base = {
89
+ messageId: event.id,
90
+ eventId: event.id,
91
+ role: message.role,
92
+ locked: event._sourceIndex < state.latestCompactIndex,
93
+ steered: !!event._steered,
94
+ }
95
+
96
+ if (message.role === 'system' || message.role === 'developer') {
97
+ state.transcript.push({ ...base, kind: message.role, text: String(message.content ?? '') })
98
+ return
99
+ }
100
+ if (message.role === 'user') {
101
+ const text = Array.isArray(message.content)
102
+ ? message.content
103
+ .map((p) => (p.type === 'text' ? p.text : `[image: ${String(p.source?.path || '').split('/').pop() || 'attached'}]`))
104
+ .join('')
105
+ : String(message.content)
106
+ state.transcript.push({ ...base, kind: 'user', text, content: message.content })
107
+ return
108
+ }
109
+ if (message.role === 'assistant') {
110
+ if (message.content) {
111
+ state.transcript.push({ ...base, kind: 'assistant', text: message.content, model: state.model })
112
+ }
113
+ for (const call of message.tool_calls || []) {
114
+ const item = {
115
+ kind: 'tool',
116
+ callId: call.id,
117
+ name: call.function.name,
118
+ args: parseArgs(call.function.arguments),
119
+ title: call.function.name,
120
+ status: 'done',
121
+ eventId: event.id,
122
+ }
123
+ state.transcript.push(item)
124
+ state.toolItems.set(call.id, item)
125
+ }
126
+ return
127
+ }
128
+ if (message.role === 'tool') {
129
+ const item = state.toolItems.get(message.tool_call_id)
130
+ if (item) item.resultText = String(message.content)
131
+ }
132
+ }
133
+
134
+ function foldRewind(state, event) {
135
+ const { mode, summaryText, reverted = [] } = event.data
136
+ for (const callId of reverted) {
137
+ const item = state.toolItems.get(callId)
138
+ if (item) item.status = 'reverted'
139
+ }
140
+ if (mode === 'summary' && summaryText) {
141
+ state.transcript.push({ kind: 'summary', source: 'rewind', text: summaryText })
142
+ pushHistory(state, {
143
+ role: 'assistant',
144
+ content: `[summary of the rewound conversation]\n${summaryText}`,
145
+ }, event.id)
146
+ }
147
+ }
148
+
149
+ export function deriveState(events) {
150
+ const effectiveEvents = applySteering(events)
151
+ const dropped = droppedIds(effectiveEvents)
152
+ const latestCompactIndex = effectiveEvents.reduce((latest, event) => event.type === 'clear' ? -1 : event.type === 'compact' ? events.indexOf(event) : latest, -1)
153
+ const canceledUndoTargets = new Set(
154
+ events.filter((e) => e.type === 'rewind_undo').map((e) => e.data.rewindId),
155
+ )
156
+
157
+ const spentUsage = emptyUsage()
158
+ const spentUsageByModel = {}
159
+ for (const event of events) {
160
+ if (event.type !== 'usage') continue
161
+ addUsageInto(spentUsage, event.data.usage)
162
+ addUsageInto((spentUsageByModel[event.data.model] ||= emptyUsage()), event.data.usage)
163
+ }
164
+
165
+ const state = {
166
+ transcript: [],
167
+ providerHistory: [],
168
+ historyEventIds: [],
169
+ model: null,
170
+ effort: undefined,
171
+ usage: spentUsage,
172
+ usageByModel: spentUsageByModel,
173
+ usageActive: emptyUsage(),
174
+ usageActiveByModel: {},
175
+ lastPromptTokens: 0,
176
+ lastPromptModel: null,
177
+ loadedContext: new Set(),
178
+ toolItems: new Map(),
179
+ latestCompactIndex,
180
+ }
181
+
182
+ for (const event of effectiveEvents) {
183
+ if (event.type === 'usage') {
184
+ if (!dropped.has(event.id)) {
185
+ addUsageInto(state.usageActive, event.data.usage)
186
+ const activeByModel = (state.usageActiveByModel[event.data.model] ||= emptyUsage())
187
+ addUsageInto(activeByModel, event.data.usage)
188
+ // only events that recorded the final request's size can drive the
189
+ // context meter; older cumulative-only events would overstate it
190
+ if (event.data.lastPrompt !== undefined) {
191
+ state.lastPromptTokens = event.data.lastPrompt
192
+ state.lastPromptModel = event.data.model
193
+ }
194
+ }
195
+ continue
196
+ }
197
+ if (dropped.has(event.id)) continue
198
+
199
+ switch (event.type) {
200
+ case 'message':
201
+ foldMessage(state, event)
202
+ break
203
+ case 'turn_transcript':
204
+ for (const item of event.data.items || []) {
205
+ const restored = { ...item, eventId: event.id }
206
+ state.transcript.push(restored)
207
+ if (restored.kind === 'tool' && restored.callId) state.toolItems.set(restored.callId, restored)
208
+ }
209
+ break
210
+ case 'tool_meta': {
211
+ const item = state.toolItems.get(event.data.callId)
212
+ if (item) Object.assign(item, event.data, { kind: 'tool', callId: item.callId })
213
+ break
214
+ }
215
+ case 'interrupt': {
216
+ const last = state.transcript.at(-1)
217
+ if (last?.kind === 'assistant') last.interrupted = true
218
+ if (last?.kind === 'tool' && last.status === 'running') last.status = 'interrupted'
219
+ break
220
+ }
221
+ case 'model_switch':
222
+ state.model = event.data.to
223
+ break
224
+ case 'effort':
225
+ state.effort = event.data.to
226
+ break
227
+ case 'title':
228
+ state.title = event.data.text
229
+ break
230
+ case 'color':
231
+ state.color = event.data.value
232
+ break
233
+ case 'rewind':
234
+ if (!canceledUndoTargets.has(event.id)) foldRewind(state, event)
235
+ break
236
+ case 'compact': {
237
+ const { summary, keepFrom, sessionFile } = event.data
238
+ if (keepFrom !== undefined || sessionFile) {
239
+ const idx = keepFrom ? state.historyEventIds.indexOf(keepFrom) : -1
240
+ const kept = idx >= 0 ? state.providerHistory.slice(idx) : []
241
+ const keptIds = idx >= 0 ? state.historyEventIds.slice(idx) : []
242
+ state.providerHistory = [
243
+ { role: 'user', content: continuationMessage(summary, { sessionFile, recentKept: kept.length > 0 }) },
244
+ ...kept,
245
+ ]
246
+ state.historyEventIds = [null, ...keptIds]
247
+ } else {
248
+ state.providerHistory = [
249
+ { role: 'user', content: `[conversation summary]\n${summary}` },
250
+ { role: 'assistant', content: 'Got it. Continuing from that summary.' },
251
+ ]
252
+ state.historyEventIds = [null, null]
253
+ }
254
+ state.transcript.push({ kind: 'summary', source: 'compact', text: summary })
255
+ state.lastPromptTokens = 0
256
+ break
257
+ }
258
+ case 'clear':
259
+ state.transcript = []
260
+ state.providerHistory = []
261
+ state.historyEventIds = []
262
+ state.toolItems = new Map()
263
+ state.lastPromptTokens = 0
264
+ break
265
+ case 'context_file':
266
+ state.loadedContext.add(event.data.path)
267
+ break
268
+ case 'skill':
269
+ state.transcript.push({ kind: 'skill', name: event.data.name })
270
+ break
271
+ case 'thoughts':
272
+ state.transcript.push({ kind: 'thoughts', text: event.data.text })
273
+ break
274
+ case 'shell_note':
275
+ case 'system_note': {
276
+ pushHistory(state, { role: 'user', content: event.data.text }, event.id)
277
+ const lines = event.data.text.split('\n').filter(Boolean)
278
+ const agentCompletions = lines.filter((line) => /^Agent \d+ \(.+\) finished with status /.test(line))
279
+ if (agentCompletions.length) {
280
+ for (const text of agentCompletions) state.transcript.push({ kind: 'notice', text, agentCompletion: true })
281
+ } else {
282
+ state.transcript.push({ kind: 'notice', text: lines[0] || '' })
283
+ }
284
+ break
285
+ }
286
+ }
287
+ }
288
+
289
+ state.providerHistory = elideStaleToolResults(state.providerHistory)
290
+ return state
291
+ }
292
+
293
+ export function userEntries(state) {
294
+ const entries = []
295
+ state.transcript.forEach((item, index) => {
296
+ if (item.kind === 'user') entries.push({ text: item.text, content: item.content, index, eventId: item.eventId })
297
+ })
298
+ return entries
299
+ }
300
+
301
+ export function rewindStats(state, index) {
302
+ const tail = state.transcript.slice(index)
303
+ return {
304
+ msgs: tail.length,
305
+ edits: tail.filter((m) => m.kind === 'tool' && m.revert && m.status !== 'reverted'),
306
+ }
307
+ }
package/src/events.js ADDED
@@ -0,0 +1,54 @@
1
+ import { randomUUID } from 'node:crypto'
2
+ import { createReadStream } from 'node:fs'
3
+ import { createInterface } from 'node:readline'
4
+
5
+ export const SESSION_VERSION = 1
6
+
7
+ export function makeEvent(type, data = {}) {
8
+ return { id: randomUUID(), at: Date.now(), type, data }
9
+ }
10
+
11
+ export function makeHeader({ cwd, root, forkedFrom }) {
12
+ return {
13
+ type: 'session',
14
+ version: SESSION_VERSION,
15
+ id: randomUUID(),
16
+ cwd,
17
+ root,
18
+ createdAt: Date.now(),
19
+ ...(forkedFrom ? { forkedFrom } : {}),
20
+ }
21
+ }
22
+
23
+ export function serializeLine(event) {
24
+ return JSON.stringify(event) + '\n'
25
+ }
26
+
27
+ export function parseLine(line) {
28
+ const trimmed = line.trim()
29
+ if (!trimmed) return null
30
+ try {
31
+ const parsed = JSON.parse(trimmed)
32
+ return parsed && typeof parsed.type === 'string' ? parsed : null
33
+ } catch {
34
+ return null
35
+ }
36
+ }
37
+
38
+ export function parseLines(text) {
39
+ return text.split('\n').map(parseLine).filter(Boolean)
40
+ }
41
+
42
+ export async function* streamEvents(file) {
43
+ const lines = createInterface({ input: createReadStream(file), crlfDelay: Infinity })
44
+ for await (const line of lines) {
45
+ const event = parseLine(line)
46
+ if (event) yield event
47
+ }
48
+ }
49
+
50
+ export async function readEvents(file) {
51
+ const events = []
52
+ for await (const event of streamEvents(file)) events.push(event)
53
+ return events
54
+ }
package/src/export.js ADDED
@@ -0,0 +1,16 @@
1
+ export function transcriptToMarkdown(transcript, { title = 'pico session' } = {}) {
2
+ const parts = [`# ${title}`, '']
3
+ for (const item of transcript) {
4
+ if (item.kind === 'user') {
5
+ parts.push('## You', '', item.text, '')
6
+ } else if (item.kind === 'assistant') {
7
+ parts.push(item.interrupted ? `${item.text}\n\n*(interrupted)*` : item.text, '')
8
+ } else if (item.kind === 'tool') {
9
+ const label = item.diff ? ` (+${item.diff.additions} -${item.diff.deletions})` : ''
10
+ parts.push(`> ${item.status === 'reverted' ? '↩' : '✓'} \`${item.name}\` ${item.title}${label}`, '')
11
+ } else if (item.kind === 'summary') {
12
+ parts.push(`> summary: ${item.text}`, '')
13
+ }
14
+ }
15
+ return parts.join('\n')
16
+ }
package/src/files.js ADDED
@@ -0,0 +1,39 @@
1
+ import { execFile } from 'node:child_process'
2
+ import fg from 'fast-glob'
3
+
4
+ const MAX_FILES = 5000
5
+ const TTL = 5000
6
+
7
+ let cached = { at: 0, cwd: null, files: [] }
8
+ const pending = new Map()
9
+
10
+ function ripgrepFiles(cwd) {
11
+ return new Promise((resolve) => {
12
+ const args = ['--files', '--hidden', '--glob', '!**/.git/**', '--sortr=modified']
13
+ execFile('rg', args, { cwd, maxBuffer: 50 * 1024 * 1024 }, (err, stdout) => {
14
+ resolve(err && !stdout ? null : stdout.split('\n').filter(Boolean))
15
+ })
16
+ })
17
+ }
18
+
19
+ export function listFiles(cwd) {
20
+ if (cached.cwd === cwd && Date.now() - cached.at < TTL) return Promise.resolve(cached.files)
21
+ if (pending.has(cwd)) return pending.get(cwd)
22
+
23
+ const request = (async () => {
24
+ let files = await ripgrepFiles(cwd)
25
+ if (!files) {
26
+ files = await fg('**/*', {
27
+ cwd,
28
+ onlyFiles: true,
29
+ dot: true,
30
+ ignore: ['**/node_modules/**', '**/.git/**'],
31
+ }).catch(() => [])
32
+ }
33
+ cached = { at: Date.now(), cwd, files: files.slice(0, MAX_FILES) }
34
+ return cached.files
35
+ })().finally(() => pending.delete(cwd))
36
+
37
+ pending.set(cwd, request)
38
+ return request
39
+ }
package/src/format.js ADDED
@@ -0,0 +1,6 @@
1
+ export function compactNumber(value) {
2
+ const n = Math.round(value)
3
+ if (Math.abs(n) >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}m`
4
+ if (Math.abs(n) >= 1_000) return `${(n / 1_000).toFixed(1)}k`
5
+ return String(n)
6
+ }