@tduyng/pi-cursor 0.0.0-stage → 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/models.ts ADDED
@@ -0,0 +1,198 @@
1
+ import type { ModelListItem, ModelParameterDefinition, ModelParameterValue } from '@cursor/sdk'
2
+ import type { ModelThinkingLevel, ThinkingLevelMap } from '@earendil-works/pi-ai'
3
+
4
+ export type PiModelEntry = {
5
+ id: string
6
+ name: string
7
+ reasoning: boolean
8
+ thinkingLevelMap?: ThinkingLevelMap
9
+ input: ('text' | 'image')[]
10
+ cost: { input: number; output: number; cacheRead: number; cacheWrite: number }
11
+ contextWindow: number
12
+ maxTokens: number
13
+ }
14
+
15
+ export type ModelSelection = { id: string; params?: ModelParameterValue[] }
16
+
17
+ type ThinkingParams = { boolean?: ModelParameterDefinition; levels?: ModelParameterDefinition }
18
+
19
+ const LEVELS: ModelThinkingLevel[] = ['off', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max']
20
+ export type ModelInfo = Pick<ModelListItem, 'id' | 'displayName' | 'parameters' | 'variants'>
21
+
22
+ const catalog = new Map<string, ModelInfo>()
23
+
24
+ const FALLBACK_MODELS: PiModelEntry[] = [
25
+ entry('auto-smart', 'Auto'),
26
+ entry('claude-opus-4-8', 'Claude Opus 4.8', 300_000),
27
+ entry('claude-sonnet-5-5', 'Claude Sonnet 5.5'),
28
+ entry('gpt-5.5', 'GPT-5.5', 272_000),
29
+ entry('grok-4.6', 'Grok 4.6', 256_000),
30
+ entry('composer-2.5', 'Composer 2.5'),
31
+ entry('gemini-3.7-flash', 'Gemini 3.7 Flash')
32
+ ]
33
+
34
+ function entry(id: string, name: string, contextWindow = 200_000): PiModelEntry {
35
+ return {
36
+ id,
37
+ name,
38
+ reasoning: false,
39
+ input: ['text', 'image'],
40
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
41
+ contextWindow,
42
+ maxTokens: 32_768
43
+ }
44
+ }
45
+
46
+ export function rememberModels(items: ModelInfo[]): void {
47
+ catalog.clear()
48
+ for (const item of items) catalog.set(item.id, item)
49
+ }
50
+
51
+ export function fallbackModels(): PiModelEntry[] {
52
+ return FALLBACK_MODELS.map((model) => ({ ...model }))
53
+ }
54
+
55
+ export function toPiModels(items: ModelInfo[]): PiModelEntry[] {
56
+ const entries: PiModelEntry[] = []
57
+ for (const item of items) {
58
+ const contexts = contextValues(item)
59
+ entries.push(toPiModel(item, undefined, contexts[0]))
60
+ for (const context of contexts.slice(1)) entries.push(toPiModel(item, context, context))
61
+ }
62
+ return entries
63
+ }
64
+
65
+ export type ModelIdParts = { base: string; context?: string }
66
+
67
+ export function parseModelId(id: string): ModelIdParts {
68
+ const match = /^(.+)@(\d+[km])$/.exec(id)
69
+ if (!match?.[1] || !match[2]) return { base: id }
70
+ return { base: match[1], context: match[2] }
71
+ }
72
+
73
+ function contextValues(item: ModelInfo): string[] {
74
+ const values = item.parameters?.find((candidate) => candidate.id === 'context')?.values.map((value) => value.value)
75
+ if (!values) return []
76
+ return values.toSorted((left, right) => contextSize(left) - contextSize(right))
77
+ }
78
+
79
+ function contextSize(context: string): number {
80
+ const match = /^(\d+)([km])$/.exec(context)
81
+ if (!match?.[1] || !match[2]) return Number.MAX_SAFE_INTEGER
82
+ const size = Number(match[1])
83
+ return match[2] === 'm' ? size * 1_000_000 : size * 1_000
84
+ }
85
+
86
+ export function contextWindowOf(id: string): number {
87
+ const { base, context } = parseModelId(id)
88
+ if (context) return contextSize(context)
89
+ return contextWindowFor(base)
90
+ }
91
+
92
+ function toPiModel(item: ModelInfo, suffix: string | undefined, window: string | undefined): PiModelEntry {
93
+ const params = thinkingParams(item)
94
+ const id = suffix ? `${item.id}@${suffix}` : item.id
95
+ const displayName = item.displayName || item.id
96
+ const name = suffix ? `${displayName} ${suffix.toUpperCase()}` : displayName
97
+ const model = entry(id, name, window ? contextSize(window) : contextWindowFor(item.id))
98
+ if (params) {
99
+ model.reasoning = true
100
+ model.thinkingLevelMap = thinkingLevelMap(params)
101
+ }
102
+ return model
103
+ }
104
+
105
+ function thinkingParams(item: ModelInfo): ThinkingParams | undefined {
106
+ const boolean = item.parameters?.find(
107
+ (candidate) =>
108
+ candidate.id === 'thinking' &&
109
+ candidate.values.length > 0 &&
110
+ candidate.values.every((value) => value.value === 'true' || value.value === 'false')
111
+ )
112
+ const levels =
113
+ item.parameters?.find((candidate) => candidate.id === 'effort' && candidate.values.length > 0) ??
114
+ item.parameters?.find((candidate) => candidate.id === 'reasoning' && candidate.values.length > 0)
115
+ if (!boolean && !levels) return undefined
116
+ return { boolean, levels }
117
+ }
118
+
119
+ function thinkingLevelMap(params: ThinkingParams): ThinkingLevelMap | undefined {
120
+ const map: ThinkingLevelMap = {}
121
+ let any = false
122
+ for (const level of LEVELS) {
123
+ let value: string | undefined
124
+ if (level === 'off') value = params.boolean ? 'false' : mapLevel(params.levels, level)
125
+ else if (params.levels) value = mapLevel(params.levels, level)
126
+ else value = params.boolean ? 'true' : undefined
127
+ map[level] = value ?? null
128
+ if (value) any = true
129
+ }
130
+ return any ? map : undefined
131
+ }
132
+
133
+ function mapLevel(param: ModelParameterDefinition | undefined, level: ModelThinkingLevel): string | undefined {
134
+ const values = param?.values.map((value) => value.value) ?? []
135
+ if (level === 'off') return values.find((value) => value === 'none' || value === 'false' || value === 'off')
136
+ if (level === 'minimal') {
137
+ return values.find((value) => value === 'minimal') ?? values.find((value) => value === 'low')
138
+ }
139
+ if (level === 'low') return values.find((value) => value === 'low')
140
+ if (level === 'medium') return values.find((value) => value === 'medium')
141
+ if (level === 'high') return values.find((value) => value === 'high')
142
+ if (level === 'xhigh') {
143
+ return values.find((value) => value === 'xhigh') ?? values.find((value) => value === 'extra-high')
144
+ }
145
+ return (
146
+ values.find((value) => value === 'max') ??
147
+ values.find((value) => value === 'xhigh') ??
148
+ values.find((value) => value === 'extra-high')
149
+ )
150
+ }
151
+
152
+ export function selectionFor(id: string, level: ModelThinkingLevel | undefined, fast = false): ModelSelection {
153
+ const { base, context } = parseModelId(id)
154
+ const item = catalog.get(base)
155
+ if (!item) return { id: base }
156
+ const variant = item.variants?.find((candidate) => candidate.isDefault) ?? item.variants?.[0]
157
+ const ordered = variant?.params.map((param) => param.id) ?? []
158
+ const values = new Map<string, string>()
159
+ for (const param of variant?.params ?? []) values.set(param.id, param.value)
160
+ const chosenContext = context ?? contextValues(item)[0]
161
+ if (chosenContext) {
162
+ values.set('context', chosenContext)
163
+ if (!ordered.includes('context')) ordered.push('context')
164
+ }
165
+ if (fast && item.parameters?.some((candidate) => candidate.id === 'fast')) {
166
+ values.set('fast', 'true')
167
+ if (!ordered.includes('fast')) ordered.push('fast')
168
+ }
169
+ const params = thinkingParams(item)
170
+ if (params && level) {
171
+ if (params.boolean) {
172
+ values.set(params.boolean.id, level === 'off' ? 'false' : 'true')
173
+ if (!ordered.includes(params.boolean.id)) ordered.push(params.boolean.id)
174
+ }
175
+ if (params.levels) {
176
+ const value = mapLevel(params.levels, level)
177
+ if (value) values.set(params.levels.id, value)
178
+ if (!ordered.includes(params.levels.id)) ordered.push(params.levels.id)
179
+ }
180
+ }
181
+ const result: ModelParameterValue[] = []
182
+ for (const key of ordered) {
183
+ const value = values.get(key)
184
+ if (value !== undefined) result.push({ id: key, value })
185
+ }
186
+ return result.length > 0 ? { id: base, params: result } : { id: base }
187
+ }
188
+
189
+ export function contextWindowFor(id: string): number {
190
+ if (id.startsWith('gpt-5')) return 272_000
191
+ if (id.startsWith('grok')) return 256_000
192
+ return 200_000
193
+ }
194
+
195
+ export function modelName(id: string): string {
196
+ const { base } = parseModelId(id)
197
+ return catalog.get(base)?.displayName ?? FALLBACK_MODELS.find((model) => model.id === base)?.name ?? id
198
+ }
package/src/resume.ts ADDED
@@ -0,0 +1,81 @@
1
+ import { readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs'
2
+ import { join } from 'node:path'
3
+
4
+ import { getAgentDir } from '@earendil-works/pi-coding-agent'
5
+ import { Validator, type Infer } from 'ata-validator'
6
+ import { t } from 'ata-validator/t'
7
+
8
+ import type { SessionKey } from './session.js'
9
+ import type { Transcript, TurnMessage } from './transcript.js'
10
+
11
+ const ResumeRecord = t.object({
12
+ sessionId: t.string(),
13
+ agentId: t.string(),
14
+ cwd: t.string(),
15
+ toolsHash: t.string(),
16
+ count: t.number(),
17
+ chain: t.string(),
18
+ at: t.number()
19
+ })
20
+
21
+ const ResumeLedger = t.object({ records: t.array(ResumeRecord) })
22
+
23
+ const isLedger = new Validator(ResumeLedger).isValidObject
24
+
25
+ export type AgentRecord = Infer<typeof ResumeRecord>
26
+
27
+ const KEEP = 20
28
+
29
+ function ledgerPath(): string {
30
+ return join(getAgentDir(), 'pi-cursor-agents.json')
31
+ }
32
+
33
+ function readLedger(): AgentRecord[] {
34
+ try {
35
+ const parsed: unknown = JSON.parse(readFileSync(ledgerPath(), 'utf8'))
36
+ return isLedger(parsed) ? parsed.records : []
37
+ } catch {
38
+ return []
39
+ }
40
+ }
41
+
42
+ export function dropAllRecords(): void {
43
+ rmSync(ledgerPath(), { force: true })
44
+ }
45
+
46
+ export function readRecord(sessionId: string): AgentRecord | undefined {
47
+ return readLedger().findLast((record) => record.sessionId === sessionId)
48
+ }
49
+
50
+ export function writeRecord(record: AgentRecord): void {
51
+ const records = readLedger().filter((candidate) => candidate.sessionId !== record.sessionId)
52
+ records.push(record)
53
+ const path = ledgerPath()
54
+ const temp = `${path}.${process.pid}.tmp`
55
+ const kept = records.toSorted((left, right) => left.at - right.at).slice(-KEEP)
56
+ writeFileSync(temp, JSON.stringify({ records: kept }), { mode: 0o600 })
57
+ renameSync(temp, path)
58
+ }
59
+
60
+ export function dropRecord(sessionId: string): void {
61
+ const records = readLedger().filter((candidate) => candidate.sessionId !== sessionId)
62
+ const path = ledgerPath()
63
+ const temp = `${path}.${process.pid}.tmp`
64
+ writeFileSync(temp, JSON.stringify({ records }), { mode: 0o600 })
65
+ renameSync(temp, path)
66
+ }
67
+
68
+ export function resumeTail(
69
+ record: AgentRecord,
70
+ transcript: Transcript,
71
+ key: SessionKey
72
+ ): TurnMessage[] | undefined {
73
+ if (record.cwd !== key.cwd) return undefined
74
+ if (record.toolsHash !== key.toolsHash) return undefined
75
+ if (record.count >= transcript.chain.length) return undefined
76
+ if (transcript.chain[record.count] !== record.chain) return undefined
77
+ const tail = transcript.messages.slice(record.count)
78
+ if (tail.some((message) => message.role === 'assistant' || message.role === 'toolResult')) return undefined
79
+ if (!tail.some((message) => message.role === 'user')) return undefined
80
+ return tail
81
+ }
package/src/sdk.ts ADDED
@@ -0,0 +1,14 @@
1
+ type CursorSdk = typeof import('@cursor/sdk')
2
+
3
+ let loading: Promise<CursorSdk> | undefined
4
+
5
+ export function cursorSdk(): Promise<CursorSdk> {
6
+ loading ??= import('@cursor/sdk')
7
+ return loading
8
+ }
9
+
10
+ export function cursorApiKey(): string | undefined {
11
+ const value = process.env['CURSOR_API_KEY']
12
+ const trimmed = value?.trim()
13
+ return trimmed && trimmed.length > 0 ? trimmed : undefined
14
+ }