trajectories 0.0.0 → 0.1.1
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/CHANGELOG.md +23 -0
- package/LICENSE +201 -0
- package/README.md +274 -2
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +8542 -0
- package/dist/convert.d.ts +31 -0
- package/dist/convert.js +431 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +8402 -0
- package/dist/log-baseline.d.ts +26 -0
- package/dist/log-baseline.js +104 -0
- package/dist/log-content.d.ts +16 -0
- package/dist/log-content.js +80 -0
- package/dist/log-io.d.ts +10 -0
- package/dist/log-io.js +121 -0
- package/dist/log-read.d.ts +24 -0
- package/dist/log-read.js +117 -0
- package/dist/log-reconcile.d.ts +5 -0
- package/dist/log-reconcile.js +38 -0
- package/dist/log-records.d.ts +35 -0
- package/dist/log-records.js +85 -0
- package/dist/log-recovery.d.ts +18 -0
- package/dist/log-recovery.js +123 -0
- package/dist/log-stage.d.ts +14 -0
- package/dist/log-stage.js +138 -0
- package/dist/log-state.d.ts +89 -0
- package/dist/log-state.js +92 -0
- package/dist/log-switch.d.ts +3 -0
- package/dist/log-switch.js +98 -0
- package/dist/log-types.d.ts +129 -0
- package/dist/log-types.js +8 -0
- package/dist/log.d.ts +8 -0
- package/dist/log.js +62 -0
- package/dist/migrate.d.ts +29 -0
- package/dist/migrate.js +173 -0
- package/dist/normalization-runtime.d.ts +1 -0
- package/dist/normalization-runtime.js +3150 -0
- package/dist/portable.d.ts +9 -0
- package/dist/portable.js +26 -0
- package/dist/readers/canonical.d.ts +13 -0
- package/dist/readers/canonical.js +54 -0
- package/dist/readers/cline.d.ts +10 -0
- package/dist/readers/cline.js +107 -0
- package/dist/readers/cursor.d.ts +20 -0
- package/dist/readers/cursor.js +374 -0
- package/dist/readers/deepagents.d.ts +41 -0
- package/dist/readers/deepagents.js +163 -0
- package/dist/readers/fx.d.ts +11 -0
- package/dist/readers/fx.js +573 -0
- package/dist/readers/github-copilot.d.ts +8 -0
- package/dist/readers/github-copilot.js +216 -0
- package/dist/readers/grok-build.d.ts +12 -0
- package/dist/readers/grok-build.js +391 -0
- package/dist/readers/guards.d.ts +4 -0
- package/dist/readers/guards.js +121 -0
- package/dist/readers/pi-compaction.d.ts +7 -0
- package/dist/readers/pi-compaction.js +112 -0
- package/dist/readers/pi-context.d.ts +13 -0
- package/dist/readers/pi-context.js +64 -0
- package/dist/store.d.ts +25 -0
- package/dist/store.js +218 -0
- package/dist/types.d.ts +91 -0
- package/dist/types.js +1 -0
- package/dist/writers/claude-code.d.ts +5 -0
- package/dist/writers/claude-code.js +51 -0
- package/dist/writers/cline.d.ts +3 -0
- package/dist/writers/cline.js +77 -0
- package/dist/writers/codex.d.ts +3 -0
- package/dist/writers/codex.js +32 -0
- package/dist/writers/cursor.d.ts +3 -0
- package/dist/writers/cursor.js +197 -0
- package/dist/writers/deepagents.d.ts +3 -0
- package/dist/writers/deepagents.js +116 -0
- package/dist/writers/entropy.d.ts +5 -0
- package/dist/writers/entropy.js +33 -0
- package/dist/writers/fx.d.ts +3 -0
- package/dist/writers/fx.js +135 -0
- package/dist/writers/github-copilot.d.ts +3 -0
- package/dist/writers/github-copilot.js +71 -0
- package/dist/writers/grok-build.d.ts +3 -0
- package/dist/writers/grok-build.js +43 -0
- package/dist/writers/helpers.d.ts +17 -0
- package/dist/writers/helpers.js +75 -0
- package/dist/writers/omp.d.ts +3 -0
- package/dist/writers/omp.js +7 -0
- package/dist/writers/opencode.d.ts +3 -0
- package/dist/writers/opencode.js +65 -0
- package/dist/writers/pi.d.ts +3 -0
- package/dist/writers/pi.js +45 -0
- package/docs/cline-native-format.md +43 -0
- package/docs/copilot-native-format.md +78 -0
- package/docs/cursor-native-format.md +103 -0
- package/docs/deepagents-native-format.md +98 -0
- package/docs/evidence-api.md +170 -0
- package/docs/fx-native-format.md +87 -0
- package/docs/harness-parity-bootstrap.md +78 -0
- package/docs/harness-support-parity.md +107 -0
- package/docs/inspirations.md +103 -0
- package/docs/ledger-migration.md +141 -0
- package/docs/pi-compaction-context.md +75 -0
- package/docs/pi-provider-context.md +76 -0
- package/docs/research/2026-10-06-huihua-and-oar.md +176 -0
- package/docs/research/ledger/README.md +96 -0
- package/docs/research/ledger/confer/2026-09-13-round-10/README.md +61 -0
- package/docs/research/ledger/confer/2026-09-13-round-2/cass.md +72 -0
- package/docs/research/ledger/confer/2026-09-13-round-3/README.md +70 -0
- package/docs/research/ledger/confer/2026-09-13-round-5/README.md +61 -0
- package/docs/research/ledger/confer/2026-09-13-round-6/README.md +68 -0
- package/docs/research/ledger/confer/2026-09-13-round-7/README.md +64 -0
- package/docs/research/ledger/confer/2026-09-13-round-9/README.md +59 -0
- package/docs/research/ledger/confer/2026-09-14-wake/wake.md +146 -0
- package/docs/research/ledger/docs/inspirations.md +320 -0
- package/docs/research/ledger/docs/pr2-pr6-integration.md +53 -0
- package/docs/research/ledger/docs/research/2026-09-07-confer-inspirations.md +163 -0
- package/docs/research/ledger/docs/research/2026-09-07-letta-continuous-service.md +104 -0
- package/docs/research/ledger/docs/research/2026-09-08-claude-history-retrieval.md +117 -0
- package/docs/research/ledger/docs/research/2026-09-09-agentsview-native-discovery.md +87 -0
- package/docs/research/ledger/docs/research/2026-09-09-daemon-observation.md +231 -0
- package/docs/research/ledger/docs/research/2026-09-09-discovery-pagination.md +114 -0
- package/docs/research/ledger/docs/research/2026-09-09-harness-transcript-contract.md +145 -0
- package/docs/research/ledger/docs/research/2026-09-09-native-history-discovery.md +180 -0
- package/docs/research/ledger/docs/research/2026-09-09-native-history-implementation.md +137 -0
- package/docs/research/ledger/docs/research/2026-09-09-other-inspirations.md +279 -0
- package/docs/research/ledger/docs/research/2026-09-09-trajectory-and-letta-code.md +128 -0
- package/docs/research/ledger/docs/research/2026-09-13-diagnostics-coverage.md +116 -0
- package/docs/research/ledger/docs/research/2026-09-13-fts-rebuild.md +83 -0
- package/docs/research/ledger/docs/research/2026-09-13-read-path-split.md +74 -0
- package/docs/research/ledger/docs/research/2026-09-13-runtime-rollout.md +102 -0
- package/docs/research/ledger/docs/research/2026-09-13-scan-outcomes.md +80 -0
- package/docs/research/ledger/docs/research/2026-09-13-search-quality.md +105 -0
- package/docs/research/ledger/docs/research/2026-09-13-source-coverage.md +64 -0
- package/docs/research/ledger/docs/research/2026-09-13-sync-wall-budget.md +82 -0
- package/docs/research/ledger/docs/research/2026-09-13-typed-read-gate.md +81 -0
- package/docs/research/ledger/docs/research/2026-09-13-writer-lease.md +84 -0
- package/docs/research/ledger/docs/research/2026-09-16-c1-copy-fallback.md +227 -0
- package/docs/research/ledger/docs/research/2026-09-16-c10-kimi.md +414 -0
- package/docs/research/ledger/docs/research/2026-09-16-c2-derive-counter.md +216 -0
- package/docs/research/ledger/docs/research/2026-09-16-c3-echo-filter.md +305 -0
- package/docs/research/ledger/docs/research/2026-09-16-c4-title-index.md +349 -0
- package/docs/research/ledger/docs/research/2026-09-16-c5-trigram.md +268 -0
- package/docs/research/ledger/docs/research/2026-09-16-c6-recency-prior.md +438 -0
- package/docs/research/ledger/docs/research/2026-09-16-c7-tombstones.md +467 -0
- package/docs/research/ledger/docs/research/2026-09-16-c8-remote-truth.md +390 -0
- package/docs/research/ledger/docs/research/2026-09-16-c9-grok.md +380 -0
- package/docs/research/ledger/docs/research/2026-09-16-wake-port-program.md +38 -0
- package/docs/research/ledger/docs/research/2026-09-17-cursor-blobstore.md +157 -0
- package/docs/research/ledger/manifest.json +540 -0
- package/legacy/NOTICE.md +50 -0
- package/legacy/bin/ledger-core.ts +7 -0
- package/legacy/bin/ledger.ts +7 -0
- package/legacy/bin/traj-bridge.ts +7 -0
- package/legacy/scripts/import-claude-history.ts +130 -0
- package/legacy/scripts/native-history-smoke.ts +52 -0
- package/legacy/scripts/observe-daemon.ts +570 -0
- package/legacy/scripts/retitle-sessions.md +19 -0
- package/legacy/scripts/verify-runtime-rollout.ts +151 -0
- package/legacy/src/canonical.ts +108 -0
- package/legacy/src/cli.ts +1293 -0
- package/legacy/src/db.ts +2254 -0
- package/legacy/src/diagnostics.ts +87 -0
- package/legacy/src/discovery.ts +56 -0
- package/legacy/src/embeddings.ts +382 -0
- package/legacy/src/filter.ts +602 -0
- package/legacy/src/gemini_embeddings.ts +103 -0
- package/legacy/src/identity.ts +9 -0
- package/legacy/src/index.ts +10 -0
- package/legacy/src/ingest.ts +791 -0
- package/legacy/src/managed.ts +178 -0
- package/legacy/src/native/common.ts +68 -0
- package/legacy/src/native/cursor.ts +647 -0
- package/legacy/src/native/grok.ts +540 -0
- package/legacy/src/native/kimi.ts +706 -0
- package/legacy/src/native/opencode.ts +470 -0
- package/legacy/src/native/sqlite-ro.ts +281 -0
- package/legacy/src/native.ts +97 -0
- package/legacy/src/normalizer.ts +457 -0
- package/legacy/src/parentage.ts +562 -0
- package/legacy/src/pressure.ts +162 -0
- package/legacy/src/private-storage.ts +74 -0
- package/legacy/src/reader.ts +1255 -0
- package/legacy/src/scanner.ts +591 -0
- package/legacy/src/schema-gate.ts +263 -0
- package/legacy/src/schema.ts +829 -0
- package/legacy/src/semantic-top-k.ts +71 -0
- package/legacy/src/session-hash.ts +49 -0
- package/legacy/src/store-status.ts +218 -0
- package/legacy/src/sync-preparation.ts +45 -0
- package/legacy/src/sync-truth.ts +843 -0
- package/legacy/src/sync.ts +1219 -0
- package/legacy/src/types.ts +436 -0
- package/legacy/src/vocab.txt +30522 -0
- package/legacy/src/watch.ts +673 -0
- package/legacy/src/writer-lease.ts +310 -0
- package/ops/README.md +120 -0
- package/ops/install-cli.sh +63 -0
- package/ops/install-release.sh +49 -0
- package/ops/linux/install-systemd-user.sh +103 -0
- package/ops/macos/install-launchd.sh +72 -0
- package/ops/sync-settings.sh +37 -0
- package/package.json +19 -5
- package/skills/trajectories/SKILL.md +138 -0
- package/skills/trajectories/scripts/trajectories_recent.sh +26 -0
- package/skills/trajectories/scripts/trajectories_search.sh +26 -0
- package/index.js +0 -1
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { NormalizedRecord } from '@letta-ai/trajectory';
|
|
2
|
+
export interface PortableConversationToken {
|
|
3
|
+
readonly semantic: string;
|
|
4
|
+
readonly recordIndex: number;
|
|
5
|
+
/** A tool-call ordinal within the record; all other records use zero. */
|
|
6
|
+
readonly componentIndex: number;
|
|
7
|
+
}
|
|
8
|
+
/** Compare portable conversation positions without native record IDs or timestamps. */
|
|
9
|
+
export declare function portableConversationTokens(records: readonly NormalizedRecord[]): PortableConversationToken[];
|
package/dist/portable.js
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
function canonicalJson(value) {
|
|
2
|
+
if (Array.isArray(value))
|
|
3
|
+
return `[${value.map(canonicalJson).join(',')}]`;
|
|
4
|
+
if (value && typeof value === 'object')
|
|
5
|
+
return `{${Object.keys(value).sort().map(key => `${JSON.stringify(key)}:${canonicalJson(value[key])}`).join(',')}}`;
|
|
6
|
+
return JSON.stringify(value);
|
|
7
|
+
}
|
|
8
|
+
/** Compare portable conversation positions without native record IDs or timestamps. */
|
|
9
|
+
export function portableConversationTokens(records) {
|
|
10
|
+
const result = [];
|
|
11
|
+
for (const [recordIndex, record] of records.entries()) {
|
|
12
|
+
if (record.role === 'user' || record.role === 'system' || record.role === 'reasoning' || record.role === 'observation'
|
|
13
|
+
|| (record.role === 'assistant' && typeof record.content === 'string')) {
|
|
14
|
+
result.push({ semantic: canonicalJson([record.role, record.content]), recordIndex, componentIndex: 0 });
|
|
15
|
+
}
|
|
16
|
+
else if (record.role === 'assistant' && 'tool_calls' in record && Array.isArray(record.tool_calls)) {
|
|
17
|
+
for (const [componentIndex, call] of record.tool_calls.entries()) {
|
|
18
|
+
result.push({ semantic: canonicalJson(['call', call.id, call.name, JSON.parse(call.args)]), recordIndex, componentIndex });
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
else if (record.role === 'tool') {
|
|
22
|
+
result.push({ semantic: canonicalJson(['tool', record.tool_call_id, record.content, record.ok ?? null]), recordIndex, componentIndex: 0 });
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return result;
|
|
26
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { type CanonicalResult, type Diagnostic, type NormalizedRecord } from '@letta-ai/trajectory';
|
|
2
|
+
export interface DecodedRecord {
|
|
3
|
+
record: NormalizedRecord;
|
|
4
|
+
nativeId?: string;
|
|
5
|
+
sourceTimestamp?: string;
|
|
6
|
+
/** Position in the native store, never transport arrival order. */
|
|
7
|
+
sourceSequence?: number;
|
|
8
|
+
}
|
|
9
|
+
/** Canonical schema shared with trajectory; custom codecs retain their own provenance.
|
|
10
|
+
* Semantic hashes/component keys follow @letta-ai/trajectory 0.4.3's canonical.ts
|
|
11
|
+
* (Apache-2.0). Native identities belong to the decoder, never a surrogate source.
|
|
12
|
+
*/
|
|
13
|
+
export declare function canonicalFromRecords(source: string, sessionId: string, entries: readonly DecodedRecord[], diagnostics?: Diagnostic[]): CanonicalResult;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { CANONICAL_SCHEMA_VERSION, validateTranscript } from '@letta-ai/trajectory';
|
|
3
|
+
const hash = (value) => createHash('sha256').update(value).digest('hex');
|
|
4
|
+
function json(value) {
|
|
5
|
+
if (Array.isArray(value))
|
|
6
|
+
return `[${value.map(json).join(',')}]`;
|
|
7
|
+
if (value && typeof value === 'object')
|
|
8
|
+
return `{${Object.entries(value).filter(([, item]) => item !== undefined).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0).map(([key, item]) => `${JSON.stringify(key)}:${json(item)}`).join(',')}}`;
|
|
9
|
+
return JSON.stringify(value);
|
|
10
|
+
}
|
|
11
|
+
/** Canonical schema shared with trajectory; custom codecs retain their own provenance.
|
|
12
|
+
* Semantic hashes/component keys follow @letta-ai/trajectory 0.4.3's canonical.ts
|
|
13
|
+
* (Apache-2.0). Native identities belong to the decoder, never a surrogate source.
|
|
14
|
+
*/
|
|
15
|
+
export function canonicalFromRecords(source, sessionId, entries, diagnostics = []) {
|
|
16
|
+
if (typeof source !== 'string' || !source.trim() || typeof sessionId !== 'string' || !sessionId.trim())
|
|
17
|
+
throw new Error('Decoded history requires a source and session identity.');
|
|
18
|
+
if (entries[0]?.record.role !== 'meta' || entries.filter(entry => entry.record.role === 'meta').length !== 1)
|
|
19
|
+
throw new Error('Decoded history requires one leading metadata record.');
|
|
20
|
+
validateTranscript(entries.map(entry => entry.record));
|
|
21
|
+
const records = [];
|
|
22
|
+
for (const [index, entry] of entries.entries()) {
|
|
23
|
+
const sequence = entry.sourceSequence ?? index;
|
|
24
|
+
if (!Number.isSafeInteger(sequence) || sequence < 0)
|
|
25
|
+
throw new Error('Invalid native record sequence.');
|
|
26
|
+
const original = entry.record;
|
|
27
|
+
const components = original.role === 'assistant' && 'tool_calls' in original
|
|
28
|
+
? original.tool_calls.map(call => ({ ...original, tool_calls: [call] })) : [original];
|
|
29
|
+
for (const [component, record] of components.entries()) {
|
|
30
|
+
const type = record.role === 'assistant' && 'tool_calls' in record ? 'assistant-tool-call' : record.role;
|
|
31
|
+
const call = record.role === 'assistant' && 'tool_calls' in record ? record.tool_calls[0] : undefined;
|
|
32
|
+
const meta = type === 'meta';
|
|
33
|
+
const stableId = meta ? 'meta' : entry.nativeId ?? hash(`${sessionId}|ordinal|${sequence}|decoded|${index}`);
|
|
34
|
+
const componentKey = call ? `tool-call:${call.id}` : record.role === 'tool' ? `tool-result:${record.tool_call_id}` : meta ? 'meta' : `${type === 'reasoning' || type === 'observation' ? type : 'message'}:0`;
|
|
35
|
+
const semantic = record.role === 'meta' ? { source: record.source, ...(record.cwd !== undefined ? { cwd: record.cwd } : {}), ...(record.git_branch !== undefined ? { git_branch: record.git_branch } : {}), ...(record.model !== undefined ? { model: record.model } : {}) } : call ? { name: call.name, args: call.args } : record.role === 'tool' ? { content: record.content, ...(record.ok !== undefined ? { ok: record.ok } : {}) } : { content: 'content' in record ? record.content : null };
|
|
36
|
+
const recordJson = json(record);
|
|
37
|
+
const sourceTime = entry.sourceTimestamp ?? null;
|
|
38
|
+
records.push({
|
|
39
|
+
source_type: source, source_group_id: sessionId, stable_source_record_id: stableId,
|
|
40
|
+
source_identity_kind: meta ? 'synthetic' : entry.nativeId ? 'native' : 'location',
|
|
41
|
+
source_order_id: `${meta ? '0' : '1'}|${meta ? '0000-00-00T00:00:00.000Z' : sourceTime ?? '0000-00-00T00:00:00.001Z'}|${String(sequence).padStart(20, '0')}|${stableId}`,
|
|
42
|
+
component_index: component, record_type: type, record_id: hash(json([sessionId, stableId, componentKey])),
|
|
43
|
+
record_hash: hash(recordJson), content_hash: hash(json({ type, content: semantic })),
|
|
44
|
+
source_timestamp: sourceTime, record_timestamp: 'timestamp' in record ? record.timestamp : null,
|
|
45
|
+
content: ['user', 'assistant', 'system', 'observation', 'reasoning'].includes(type) && 'content' in record ? record.content : null,
|
|
46
|
+
tool_call_id: call?.id ?? (record.role === 'tool' ? record.tool_call_id : null), tool_name: call?.name ?? null,
|
|
47
|
+
tool_arguments_json: call?.args ?? null, tool_result_json: record.role === 'tool' ? record.content : null,
|
|
48
|
+
tool_result_ok: record.role === 'tool' ? record.ok ?? null : null, record_json: recordJson,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return { records, diagnostics, normalizer_version: 'trajectories/0.1.0', canonical_schema_version: CANONICAL_SCHEMA_VERSION,
|
|
53
|
+
config: { bounds: { toolArguments: { maxCharacters: null }, toolResults: { maxCharacters: null, strategy: 'head' } }, filters: { systemMessages: 'include', toolResults: 'include' } } };
|
|
54
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { CanonicalResult } from '@letta-ai/trajectory';
|
|
2
|
+
/** Cline Core messages.json and @cline/agents AgentMessage[] (0.0.79).
|
|
3
|
+
* Both are native formats; the Vercel adapter persists the latter as history.json.
|
|
4
|
+
*/
|
|
5
|
+
export declare function readCline(raw: string, metadata?: {
|
|
6
|
+
sessionId?: string;
|
|
7
|
+
cwd?: string;
|
|
8
|
+
model?: string;
|
|
9
|
+
}): CanonicalResult;
|
|
10
|
+
export declare function assertClinePortable(raw: string): void;
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { canonicalFromRecords } from './canonical.js';
|
|
3
|
+
import { toolNames } from '../writers/helpers.js';
|
|
4
|
+
const object = (value) => value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
5
|
+
const epoch = '1970-01-01T00:00:00.000Z';
|
|
6
|
+
/** Cline Core messages.json and @cline/agents AgentMessage[] (0.0.79).
|
|
7
|
+
* Both are native formats; the Vercel adapter persists the latter as history.json.
|
|
8
|
+
*/
|
|
9
|
+
export function readCline(raw, metadata = {}) {
|
|
10
|
+
const data = JSON.parse(raw);
|
|
11
|
+
const messages = Array.isArray(data) ? data : data?.messages;
|
|
12
|
+
if (!Array.isArray(messages) || !messages.length)
|
|
13
|
+
throw new Error('Cline history requires native messages.');
|
|
14
|
+
if (!Array.isArray(data) && data.version !== 1)
|
|
15
|
+
throw new Error('Unsupported Cline messages file version.');
|
|
16
|
+
for (const value of [metadata.sessionId, data.sessionId]) {
|
|
17
|
+
if (value !== undefined && (typeof value !== 'string' || !value.trim()))
|
|
18
|
+
throw new Error('Invalid Cline session identity.');
|
|
19
|
+
}
|
|
20
|
+
const id = metadata.sessionId ?? data.sessionId ?? `cline-${createHash('sha256').update(raw).digest('hex')}`;
|
|
21
|
+
if (metadata.sessionId && data.sessionId && metadata.sessionId !== data.sessionId)
|
|
22
|
+
throw new Error('Cline session identities disagree.');
|
|
23
|
+
const diagnostics = [];
|
|
24
|
+
const entries = [{ record: { role: 'meta', source: 'cline', ...(metadata.cwd ? { cwd: metadata.cwd } : {}), ...(metadata.model ? { model: metadata.model } : {}) } }];
|
|
25
|
+
if (data.origin?.sessionId !== undefined && data.origin.sessionId !== id)
|
|
26
|
+
throw new Error('Cline origin session identity disagrees with its history.');
|
|
27
|
+
if (typeof data.system_prompt === 'string' && data.system_prompt)
|
|
28
|
+
entries.push({ record: { role: 'system', content: data.system_prompt, timestamp: epoch } });
|
|
29
|
+
const add = (record, message, index, part, sourceTimestamp) => entries.push({ record, ...(message.id ? { nativeId: `${message.id}:${part}` } : {}), sourceSequence: entries.length, ...(sourceTimestamp ? { sourceTimestamp } : {}) });
|
|
30
|
+
for (const [index, unknown] of messages.entries()) {
|
|
31
|
+
if (!object(unknown) || !['user', 'assistant', 'tool'].includes(unknown.role))
|
|
32
|
+
throw new Error(`Invalid Cline message ${index}.`);
|
|
33
|
+
const message = unknown;
|
|
34
|
+
if (message.id !== undefined && (typeof message.id !== 'string' || !message.id.trim()))
|
|
35
|
+
throw new Error('Invalid Cline message identity.');
|
|
36
|
+
if (message.sessionId !== undefined && message.sessionId !== id)
|
|
37
|
+
throw new Error('Cline message session identity disagrees with its history.');
|
|
38
|
+
const at = message.createdAt ?? message.ts;
|
|
39
|
+
const sourceTimestamp = typeof at === 'number' && Number.isFinite(at) ? new Date(at).toISOString() : undefined;
|
|
40
|
+
const timestamp = sourceTimestamp ?? epoch;
|
|
41
|
+
if (!sourceTimestamp)
|
|
42
|
+
diagnostics.push({ code: 'timestamps_synthesized', message: `Cline message ${index} has no native timestamp.`, inputLine: index + 1 });
|
|
43
|
+
const parts = typeof message.content === 'string' ? [{ type: 'text', text: message.content }] : message.content;
|
|
44
|
+
if (!Array.isArray(parts))
|
|
45
|
+
throw new Error(`Invalid Cline content at message ${index}.`);
|
|
46
|
+
for (const [partIndex, part] of parts.entries()) {
|
|
47
|
+
if (!object(part))
|
|
48
|
+
throw new Error('Invalid Cline content block.');
|
|
49
|
+
if (part.type === 'text') {
|
|
50
|
+
if (typeof part.text !== 'string' || message.role === 'tool')
|
|
51
|
+
throw new Error('Invalid Cline text block.');
|
|
52
|
+
add({ role: message.role, content: part.text, timestamp }, message, index, partIndex, sourceTimestamp);
|
|
53
|
+
}
|
|
54
|
+
else if (part.type === 'thinking' || part.type === 'reasoning') {
|
|
55
|
+
const content = part.thinking ?? part.text;
|
|
56
|
+
if (typeof content !== 'string')
|
|
57
|
+
throw new Error('Invalid Cline reasoning block.');
|
|
58
|
+
add({ role: 'reasoning', content, timestamp }, message, index, partIndex, sourceTimestamp);
|
|
59
|
+
}
|
|
60
|
+
else if (part.type === 'tool_use' || part.type === 'tool-call') {
|
|
61
|
+
const callId = part.tool_use_id ?? part.toolCallId ?? part.id;
|
|
62
|
+
const name = part.toolName ?? part.name;
|
|
63
|
+
if (message.role !== 'assistant' || typeof callId !== 'string' || !callId || typeof name !== 'string' || !name || !object(part.input))
|
|
64
|
+
throw new Error('Invalid Cline tool call.');
|
|
65
|
+
add({ role: 'assistant', content: null, tool_calls: [{ id: callId, name, args: JSON.stringify(part.input) }], timestamp }, message, index, partIndex, sourceTimestamp);
|
|
66
|
+
}
|
|
67
|
+
else if (part.type === 'tool_result' || part.type === 'tool-result') {
|
|
68
|
+
const callId = part.toolCallId ?? part.tool_use_id;
|
|
69
|
+
const output = part.type === 'tool-result' ? part.output : part.content;
|
|
70
|
+
const isError = part.isError ?? part.is_error;
|
|
71
|
+
if (!['user', 'tool'].includes(message.role) || typeof callId !== 'string' || !callId || output === undefined)
|
|
72
|
+
throw new Error('Invalid Cline tool result.');
|
|
73
|
+
const content = typeof output === 'string' ? output : Array.isArray(output) && output.every(item => object(item) && item.type === 'text' && typeof item.text === 'string') ? output.map(item => item.text).join('\n') : JSON.stringify(output);
|
|
74
|
+
// Canonical tool content is text; raw retains the original structured output.
|
|
75
|
+
add({ role: 'tool', tool_call_id: callId, content, ...(typeof isError === 'boolean' ? { ok: !isError } : {}), timestamp }, message, index, partIndex, sourceTimestamp);
|
|
76
|
+
}
|
|
77
|
+
else
|
|
78
|
+
throw new Error(`Unsupported Cline content block: ${String(part.type)}. Raw input remains available to the caller.`);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return canonicalFromRecords('cline', id, entries, diagnostics);
|
|
82
|
+
}
|
|
83
|
+
export function assertClinePortable(raw) {
|
|
84
|
+
const data = JSON.parse(raw);
|
|
85
|
+
for (const message of Array.isArray(data) ? data : data.messages ?? []) {
|
|
86
|
+
if (message.metadata?.displayOnly || message.metadata?.compaction || message.metadata?.compactionSummary)
|
|
87
|
+
throw new Error('Unsupported Cline compaction/display-only history requires native context reconstruction.');
|
|
88
|
+
for (const part of Array.isArray(message.content) ? message.content : []) {
|
|
89
|
+
if (['signature', 'call_id', 'details', 'summary'].some(key => part[key] !== undefined))
|
|
90
|
+
throw new Error('Cline provider replay state requires native context reconstruction.');
|
|
91
|
+
if (!['tool_result', 'tool-result'].includes(part.type))
|
|
92
|
+
continue;
|
|
93
|
+
const output = part.type === 'tool-result' ? part.output : part.content;
|
|
94
|
+
const textBlocks = Array.isArray(output) && output.every(block => object(block) && block.type === 'text' && typeof block.text === 'string');
|
|
95
|
+
// Pinned Core read_files emits ToolOperationResult[], including plain text reads.
|
|
96
|
+
const operationResults = Array.isArray(output) && output.every(block => object(block) &&
|
|
97
|
+
typeof block.query === 'string' && typeof block.result === 'string' && typeof block.success === 'boolean' &&
|
|
98
|
+
(block.error === undefined || typeof block.error === 'string') &&
|
|
99
|
+
(block.duration === undefined || typeof block.duration === 'number' && Number.isFinite(block.duration) && block.duration >= 0) &&
|
|
100
|
+
Object.keys(block).every(key => ['query', 'result', 'success', 'error', 'duration'].includes(key)));
|
|
101
|
+
if ((Array.isArray(output) && !textBlocks && !operationResults) || (object(output) && ['image', 'file', 'media', 'audio', 'video'].includes(output.type)))
|
|
102
|
+
throw new Error('Cline media or unsupported structured tool results require native context reconstruction.');
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
const canonical = readCline(raw);
|
|
106
|
+
toolNames(canonical.records.map(record => JSON.parse(record.record_json)));
|
|
107
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { type CanonicalResult } from '@letta-ai/trajectory';
|
|
2
|
+
export interface CursorNativeReadResult {
|
|
3
|
+
raw: string;
|
|
4
|
+
canonical: CanonicalResult;
|
|
5
|
+
nativeSessionId: string;
|
|
6
|
+
attachments: Array<{
|
|
7
|
+
name: string;
|
|
8
|
+
content: string;
|
|
9
|
+
sha256: string;
|
|
10
|
+
}>;
|
|
11
|
+
}
|
|
12
|
+
export declare function readCursor(raw: string): CanonicalResult;
|
|
13
|
+
/** Guard used before treating a Cursor readout as resumable active context. */
|
|
14
|
+
export declare function assertCursorPortable(raw: string): void;
|
|
15
|
+
/**
|
|
16
|
+
* Read one Cursor CLI/ACP SQLite session without mutating it. `raw` is a
|
|
17
|
+
* deterministic, lossless row/blob readout rather than the SQLite container's
|
|
18
|
+
* page bytes; the original store remains untouched.
|
|
19
|
+
*/
|
|
20
|
+
export declare function readCursorNative(path: string, nativeSessionId?: string): Promise<CursorNativeReadResult>;
|
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { readFile, stat } from 'node:fs/promises';
|
|
3
|
+
import { basename, dirname, join, resolve } from 'node:path';
|
|
4
|
+
import { normalizeToCanonical } from '@letta-ai/trajectory';
|
|
5
|
+
import { canonicalFromRecords } from './canonical.js';
|
|
6
|
+
const FORMAT = 'cursor-agent-sqlite-readout-v1';
|
|
7
|
+
export function readCursor(raw) {
|
|
8
|
+
let readout;
|
|
9
|
+
try {
|
|
10
|
+
readout = JSON.parse(raw);
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
return fail('readout JSON is invalid');
|
|
14
|
+
}
|
|
15
|
+
if (readout?.format !== FORMAT || readout.source?.runtime !== 'cursor-agent' || readout.source?.schema !== 1) {
|
|
16
|
+
fail('unsupported readout format');
|
|
17
|
+
}
|
|
18
|
+
if (typeof readout.sessionId !== 'string' || !readout.sessionId || typeof readout.metadataHex !== 'string' ||
|
|
19
|
+
!Array.isArray(readout.metaRows) || !Array.isArray(readout.blobs))
|
|
20
|
+
fail('readout shape is invalid');
|
|
21
|
+
const metadataRow = readout.metaRows.find(row => row.key === '0');
|
|
22
|
+
if (!metadataRow || metadataRow.value.toLowerCase() !== readout.metadataHex.toLowerCase())
|
|
23
|
+
fail('readout metadata rows conflict');
|
|
24
|
+
let metadata;
|
|
25
|
+
try {
|
|
26
|
+
metadata = JSON.parse(Buffer.from(readout.metadataHex, 'hex').toString('utf8'));
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return fail('readout metadata JSON is invalid');
|
|
30
|
+
}
|
|
31
|
+
if (metadata.agentId !== readout.sessionId || typeof metadata.latestRootBlobId !== 'string' ||
|
|
32
|
+
metadata.latestRootBlobId.toLowerCase() !== readout.rootBlobId)
|
|
33
|
+
fail('readout identity conflicts with metadata');
|
|
34
|
+
const seen = new Set();
|
|
35
|
+
for (const blob of readout.blobs) {
|
|
36
|
+
if (seen.has(blob.id) || !/^[0-9a-f]{64}$/.test(blob.id) || !/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test(blob.dataBase64) ||
|
|
37
|
+
createHash('sha256').update(Buffer.from(blob.dataBase64, 'base64')).digest('hex') !== blob.id) {
|
|
38
|
+
fail(`blob ${blob.id} is corrupt`);
|
|
39
|
+
}
|
|
40
|
+
seen.add(blob.id);
|
|
41
|
+
}
|
|
42
|
+
const transcript = decodeStore(readout);
|
|
43
|
+
const normalized = normalizeToCanonical({ source: 'cursor', transcript, sourceContext: { groupId: readout.sessionId },
|
|
44
|
+
bounds: { toolArguments: { maxCharacters: null }, toolResults: { maxCharacters: null } },
|
|
45
|
+
filters: { systemMessages: 'include', toolResults: 'include' } });
|
|
46
|
+
const decoded = normalized.records.map(record => JSON.parse(record.record_json)).filter(record => record.role !== 'meta');
|
|
47
|
+
return canonicalFromRecords('cursor', readout.sessionId, [
|
|
48
|
+
{ record: { role: 'meta', source: 'cursor', ...(readout.provenance?.cwd ? { cwd: readout.provenance.cwd } : {}),
|
|
49
|
+
...(typeof metadata.lastUsedModel === 'string' ? { model: metadata.lastUsedModel } : {}) }, sourceSequence: 0 },
|
|
50
|
+
...decoded.map((record, index) => ({ record, sourceSequence: index + 1 })),
|
|
51
|
+
], normalized.diagnostics);
|
|
52
|
+
}
|
|
53
|
+
/** Guard used before treating a Cursor readout as resumable active context. */
|
|
54
|
+
export function assertCursorPortable(raw) { readCursor(raw); }
|
|
55
|
+
function fail(message) { throw new Error(`Cursor native store: ${message}`); }
|
|
56
|
+
function hex(bytes) { return Buffer.from(bytes).toString('hex'); }
|
|
57
|
+
function fields(bytes) {
|
|
58
|
+
const output = [];
|
|
59
|
+
let offset = 0;
|
|
60
|
+
const varint = () => {
|
|
61
|
+
let value = 0n;
|
|
62
|
+
let shift = 0n;
|
|
63
|
+
while (offset < bytes.length && shift <= 63n) {
|
|
64
|
+
const byte = bytes[offset++];
|
|
65
|
+
value |= BigInt(byte & 0x7f) << shift;
|
|
66
|
+
if ((byte & 0x80) === 0)
|
|
67
|
+
return value;
|
|
68
|
+
shift += 7n;
|
|
69
|
+
}
|
|
70
|
+
return fail('invalid protobuf varint');
|
|
71
|
+
};
|
|
72
|
+
while (offset < bytes.length) {
|
|
73
|
+
const tag = varint();
|
|
74
|
+
const number = Number(tag >> 3n);
|
|
75
|
+
const wire = Number(tag & 7n);
|
|
76
|
+
if (!number)
|
|
77
|
+
fail('invalid protobuf field number');
|
|
78
|
+
if (wire === 0)
|
|
79
|
+
output.push({ number, wire, value: varint() });
|
|
80
|
+
else if (wire === 1) {
|
|
81
|
+
if (offset + 8 > bytes.length)
|
|
82
|
+
fail('truncated fixed64 field');
|
|
83
|
+
output.push({ number, wire, value: bytes.slice(offset, offset += 8) });
|
|
84
|
+
}
|
|
85
|
+
else if (wire === 2) {
|
|
86
|
+
const length = Number(varint());
|
|
87
|
+
if (!Number.isSafeInteger(length) || length < 0 || offset + length > bytes.length)
|
|
88
|
+
fail('truncated length-delimited field');
|
|
89
|
+
output.push({ number, wire, value: bytes.slice(offset, offset += length) });
|
|
90
|
+
}
|
|
91
|
+
else if (wire === 5) {
|
|
92
|
+
if (offset + 4 > bytes.length)
|
|
93
|
+
fail('truncated fixed32 field');
|
|
94
|
+
output.push({ number, wire, value: bytes.slice(offset, offset += 4) });
|
|
95
|
+
}
|
|
96
|
+
else
|
|
97
|
+
fail(`unsupported protobuf wire type ${wire}`);
|
|
98
|
+
}
|
|
99
|
+
return output;
|
|
100
|
+
}
|
|
101
|
+
function bytesOf(field, label) {
|
|
102
|
+
if (field.wire !== 2 || !(field.value instanceof Uint8Array))
|
|
103
|
+
fail(`${label} has the wrong protobuf type`);
|
|
104
|
+
return field.value;
|
|
105
|
+
}
|
|
106
|
+
function fixedBytesOf(field, wire, label) {
|
|
107
|
+
if (field.wire !== wire || !(field.value instanceof Uint8Array))
|
|
108
|
+
fail(`${label} has the wrong protobuf type`);
|
|
109
|
+
return field.value;
|
|
110
|
+
}
|
|
111
|
+
function stringOf(field, label) {
|
|
112
|
+
return new TextDecoder('utf-8', { fatal: true }).decode(bytesOf(field, label));
|
|
113
|
+
}
|
|
114
|
+
function boolOf(field, label) {
|
|
115
|
+
if (field.wire !== 0 || typeof field.value !== 'bigint')
|
|
116
|
+
fail(`${label} has the wrong protobuf type`);
|
|
117
|
+
return field.value !== 0n;
|
|
118
|
+
}
|
|
119
|
+
function one(values, number, label, required = false) {
|
|
120
|
+
const matches = values.filter(field => field.number === number);
|
|
121
|
+
if (matches.length > 1)
|
|
122
|
+
fail(`${label} is duplicated`);
|
|
123
|
+
if (required && matches.length !== 1)
|
|
124
|
+
fail(`${label} is missing`);
|
|
125
|
+
return matches[0];
|
|
126
|
+
}
|
|
127
|
+
function only(values, allowed, label) {
|
|
128
|
+
const unsupported = [...new Set(values.filter(field => !allowed.has(field.number)).map(field => field.number))];
|
|
129
|
+
if (unsupported.length)
|
|
130
|
+
fail(`${label} contains unsupported field(s) ${unsupported.join(', ')}`);
|
|
131
|
+
}
|
|
132
|
+
function decodeJsonValue(value) {
|
|
133
|
+
const valueFields = fields(value);
|
|
134
|
+
if (valueFields.length !== 1)
|
|
135
|
+
fail('MCP argument has an invalid protobuf Value');
|
|
136
|
+
const field = valueFields[0];
|
|
137
|
+
if (field.number === 1 && field.wire === 0)
|
|
138
|
+
return null;
|
|
139
|
+
if (field.number === 2 && field.wire === 1)
|
|
140
|
+
return Buffer.from(fixedBytesOf(field, 1, 'number')).readDoubleLE(0);
|
|
141
|
+
if (field.number === 3)
|
|
142
|
+
return stringOf(field, 'string');
|
|
143
|
+
if (field.number === 4)
|
|
144
|
+
return boolOf(field, 'boolean');
|
|
145
|
+
if (field.number === 5) {
|
|
146
|
+
const object = {};
|
|
147
|
+
for (const entry of fields(bytesOf(field, 'struct')).filter(item => item.number === 1)) {
|
|
148
|
+
const pair = fields(bytesOf(entry, 'struct entry'));
|
|
149
|
+
const key = stringOf(one(pair, 1, 'struct key', true), 'struct key');
|
|
150
|
+
object[key] = decodeJsonValue(bytesOf(one(pair, 2, 'struct value', true), 'struct value'));
|
|
151
|
+
}
|
|
152
|
+
return object;
|
|
153
|
+
}
|
|
154
|
+
if (field.number === 6)
|
|
155
|
+
return fields(bytesOf(field, 'list')).filter(item => item.number === 1)
|
|
156
|
+
.map(item => decodeJsonValue(bytesOf(item, 'list value')));
|
|
157
|
+
return fail(`unsupported protobuf Value field ${field.number}`);
|
|
158
|
+
}
|
|
159
|
+
function decodeMcpContent(item) {
|
|
160
|
+
const content = fields(item);
|
|
161
|
+
only(content, new Set([1]), 'MCP result content');
|
|
162
|
+
const text = one(content, 1, 'MCP text content', true);
|
|
163
|
+
const textFields = fields(bytesOf(text, 'MCP text content'));
|
|
164
|
+
only(textFields, new Set([1, 2]), 'MCP text');
|
|
165
|
+
return stringOf(one(textFields, 1, 'MCP text', true), 'MCP text');
|
|
166
|
+
}
|
|
167
|
+
function decodeMcpTool(value, outerId) {
|
|
168
|
+
const call = fields(value);
|
|
169
|
+
only(call, new Set([1, 2, 3]), 'MCP tool call');
|
|
170
|
+
const args = fields(bytesOf(one(call, 1, 'MCP args', true), 'MCP args'));
|
|
171
|
+
only(args, new Set([1, 2, 3, 4, 5]), 'MCP args');
|
|
172
|
+
const id = one(args, 3, 'MCP tool call ID') ? stringOf(one(args, 3, 'MCP tool call ID'), 'MCP tool call ID') : outerId;
|
|
173
|
+
if (!id || (outerId && id !== outerId))
|
|
174
|
+
fail('MCP tool call IDs conflict');
|
|
175
|
+
const nameField = one(args, 5, 'MCP tool name') ?? one(args, 1, 'MCP name');
|
|
176
|
+
if (!nameField)
|
|
177
|
+
fail('MCP tool name is missing');
|
|
178
|
+
const object = {};
|
|
179
|
+
for (const entry of args.filter(field => field.number === 2)) {
|
|
180
|
+
const pair = fields(bytesOf(entry, 'MCP argument'));
|
|
181
|
+
const key = stringOf(one(pair, 1, 'MCP argument key', true), 'MCP argument key');
|
|
182
|
+
object[key] = decodeJsonValue(bytesOf(one(pair, 2, 'MCP argument value', true), 'MCP argument value'));
|
|
183
|
+
}
|
|
184
|
+
const result = fields(bytesOf(one(call, 2, 'MCP result', true), 'MCP result'));
|
|
185
|
+
if (result.length !== 1)
|
|
186
|
+
fail('MCP result has no single terminal outcome');
|
|
187
|
+
const terminal = result[0];
|
|
188
|
+
if (terminal.number === 1) {
|
|
189
|
+
const success = fields(bytesOf(terminal, 'MCP success'));
|
|
190
|
+
only(success, new Set([1, 2, 3, 4]), 'MCP success');
|
|
191
|
+
const content = success.filter(field => field.number === 1).map(field => decodeMcpContent(bytesOf(field, 'MCP content'))).join('\n');
|
|
192
|
+
return { id, name: stringOf(nameField, 'MCP tool name'), args: JSON.stringify(object), result: content,
|
|
193
|
+
ok: !(one(success, 2, 'MCP is_error') && boolOf(one(success, 2, 'MCP is_error'), 'MCP is_error')) };
|
|
194
|
+
}
|
|
195
|
+
if ([2, 3, 4, 5, 6].includes(terminal.number)) {
|
|
196
|
+
const failure = fields(bytesOf(terminal, 'MCP failure'));
|
|
197
|
+
const reason = one(failure, 1, 'MCP failure reason');
|
|
198
|
+
return { id, name: stringOf(nameField, 'MCP tool name'), args: JSON.stringify(object),
|
|
199
|
+
result: reason ? stringOf(reason, 'MCP failure reason') : `Cursor MCP outcome ${terminal.number}`, ok: false };
|
|
200
|
+
}
|
|
201
|
+
return fail(`unsupported MCP result outcome ${terminal.number}`);
|
|
202
|
+
}
|
|
203
|
+
function decodeShellTool(encoded, outerId) {
|
|
204
|
+
const call = fields(encoded);
|
|
205
|
+
only(call, new Set([1, 2, 3]), 'shell tool call');
|
|
206
|
+
const args = fields(bytesOf(one(call, 1, 'shell args', true), 'shell args'));
|
|
207
|
+
only(args, new Set([1, 2, 3, 4, 5, 6, 7, 11, 12, 14, 15, 17, 21, 22, 23, 24]), 'shell args');
|
|
208
|
+
const nestedId = one(args, 4, 'shell tool call ID');
|
|
209
|
+
const id = nestedId ? stringOf(nestedId, 'shell tool call ID') : outerId;
|
|
210
|
+
if (!id || (outerId && id !== outerId))
|
|
211
|
+
fail('shell tool call IDs conflict');
|
|
212
|
+
const command = stringOf(one(args, 1, 'shell command', true), 'shell command');
|
|
213
|
+
const cwd = one(args, 2, 'shell cwd');
|
|
214
|
+
const result = fields(bytesOf(one(call, 2, 'shell result', true), 'shell result'));
|
|
215
|
+
if (result.length !== 1)
|
|
216
|
+
fail('shell result has no single terminal outcome');
|
|
217
|
+
const terminal = result[0];
|
|
218
|
+
const payload = fields(bytesOf(terminal, 'shell terminal result'));
|
|
219
|
+
const value = (number, label) => one(payload, number, label) ? stringOf(one(payload, number, label), label) : '';
|
|
220
|
+
const output = terminal.number === 1 || terminal.number === 2
|
|
221
|
+
? JSON.stringify({ command: value(1, 'shell result command'), workingDirectory: value(2, 'shell result cwd'),
|
|
222
|
+
exitCode: one(payload, 3, 'shell exit code')?.value ? Number(one(payload, 3, 'shell exit code').value) : 0,
|
|
223
|
+
signal: value(4, 'shell signal'), stdout: value(5, 'shell stdout'), stderr: value(6, 'shell stderr') })
|
|
224
|
+
: value(3, 'shell failure reason') || value(1, 'shell failure command') || `Cursor shell outcome ${terminal.number}`;
|
|
225
|
+
return { id, name: 'shell', args: JSON.stringify({ command, ...(cwd ? { workingDirectory: stringOf(cwd, 'shell cwd') } : {}) }),
|
|
226
|
+
result: output, ok: terminal.number === 1 };
|
|
227
|
+
}
|
|
228
|
+
function decodeTool(value) {
|
|
229
|
+
const call = fields(value);
|
|
230
|
+
const outerId = one(call, 57, 'tool call ID');
|
|
231
|
+
const id = outerId ? stringOf(outerId, 'tool call ID') : undefined;
|
|
232
|
+
const tools = call.filter(field => field.number !== 54 && field.number !== 57 && field.number !== 59 && field.number !== 60);
|
|
233
|
+
if (tools.length !== 1)
|
|
234
|
+
fail('tool call has no single supported tool payload');
|
|
235
|
+
const tool = tools[0];
|
|
236
|
+
if (tool.number === 15)
|
|
237
|
+
return decodeMcpTool(bytesOf(tool, 'MCP tool'), id);
|
|
238
|
+
if (tool.number === 1)
|
|
239
|
+
return decodeShellTool(bytesOf(tool, 'shell tool'), id);
|
|
240
|
+
return fail(`unsupported Cursor tool kind field ${tool.number}`);
|
|
241
|
+
}
|
|
242
|
+
function decodeStore(readout) {
|
|
243
|
+
const blobs = new Map(readout.blobs.map(blob => [blob.id, Buffer.from(blob.dataBase64, 'base64')]));
|
|
244
|
+
const load = (id, label) => blobs.get(hex(id)) ?? fail(`missing ${label} blob ${hex(id)}`);
|
|
245
|
+
const root = fields(blobs.get(readout.rootBlobId) ?? fail('missing root blob'));
|
|
246
|
+
const compacted = root.some(field => [6, 11, 13, 37].includes(field.number));
|
|
247
|
+
const branched = root.some(field => [16, 19, 21, 24, 29, 30, 31].includes(field.number));
|
|
248
|
+
if (compacted)
|
|
249
|
+
fail('compacted conversations are not importable without loss');
|
|
250
|
+
if (branched)
|
|
251
|
+
fail('branched or subagent conversations require an explicit branch-aware reader');
|
|
252
|
+
only(root, new Set([8, 10, 26, 27, 33]), 'conversation root');
|
|
253
|
+
const rows = [];
|
|
254
|
+
let sequence = 0;
|
|
255
|
+
for (const turnRef of root.filter(field => field.number === 8)) {
|
|
256
|
+
const turn = fields(load(bytesOf(turnRef, 'turn reference'), 'turn'));
|
|
257
|
+
only(turn, new Set([1]), 'conversation turn');
|
|
258
|
+
const agent = fields(bytesOf(one(turn, 1, 'agent turn', true), 'agent turn'));
|
|
259
|
+
only(agent, new Set([1, 2, 3]), 'agent turn');
|
|
260
|
+
const user = fields(load(bytesOf(one(agent, 1, 'user reference', true), 'user reference'), 'user'));
|
|
261
|
+
only(user, new Set([1, 2, 10, 25, 26]), 'user message');
|
|
262
|
+
const userText = stringOf(one(user, 1, 'user text', true), 'user text');
|
|
263
|
+
rows.push({ id: `cursor-import-user-${++sequence}`, role: 'user', message: { content: [{ type: 'text', text: userText }] } });
|
|
264
|
+
for (const stepRef of agent.filter(field => field.number === 2)) {
|
|
265
|
+
const step = fields(load(bytesOf(stepRef, 'step reference'), 'conversation step'));
|
|
266
|
+
if (step.length !== 1)
|
|
267
|
+
fail('conversation step has no single message');
|
|
268
|
+
const message = step[0];
|
|
269
|
+
if (message.number === 1) {
|
|
270
|
+
const assistant = fields(bytesOf(message, 'assistant message'));
|
|
271
|
+
only(assistant, new Set([1, 2, 3]), 'assistant message');
|
|
272
|
+
rows.push({ id: `cursor-import-assistant-${++sequence}`, role: 'assistant',
|
|
273
|
+
message: { content: [{ type: 'text', text: stringOf(one(assistant, 1, 'assistant text', true), 'assistant text') }] } });
|
|
274
|
+
}
|
|
275
|
+
else if (message.number === 3) {
|
|
276
|
+
const thought = fields(bytesOf(message, 'thinking message'));
|
|
277
|
+
only(thought, new Set([1, 2, 3, 4]), 'thinking message');
|
|
278
|
+
rows.push({ id: `cursor-import-thinking-${++sequence}`, role: 'assistant',
|
|
279
|
+
message: { content: [{ type: 'thinking', thinking: stringOf(one(thought, 1, 'thinking text', true), 'thinking text') }] } });
|
|
280
|
+
}
|
|
281
|
+
else if (message.number === 2) {
|
|
282
|
+
const tool = decodeTool(bytesOf(message, 'tool call'));
|
|
283
|
+
rows.push({ id: `cursor-import-tool-${++sequence}`, role: 'assistant', message: { content: [
|
|
284
|
+
{ type: 'tool_use', id: tool.id, name: tool.name, input: JSON.parse(tool.args) },
|
|
285
|
+
{ type: 'tool_result', tool_use_id: tool.id, content: tool.result, is_error: !tool.ok },
|
|
286
|
+
] } });
|
|
287
|
+
}
|
|
288
|
+
else
|
|
289
|
+
fail(`unsupported conversation step field ${message.number}`);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
return rows.map(row => JSON.stringify(row)).join('\n') + '\n';
|
|
293
|
+
}
|
|
294
|
+
async function storePath(path) {
|
|
295
|
+
const resolved = resolve(path);
|
|
296
|
+
const info = await stat(resolved);
|
|
297
|
+
if (info.isFile()) {
|
|
298
|
+
if (basename(resolved) !== 'store.db')
|
|
299
|
+
fail('file input must be a Cursor store.db');
|
|
300
|
+
return resolved;
|
|
301
|
+
}
|
|
302
|
+
if (!info.isDirectory())
|
|
303
|
+
fail('input is neither a store.db nor its session directory');
|
|
304
|
+
const candidate = join(resolved, 'store.db');
|
|
305
|
+
if (!(await stat(candidate).catch(() => null))?.isFile())
|
|
306
|
+
fail('directory has no direct store.db; select one native session directory');
|
|
307
|
+
return candidate;
|
|
308
|
+
}
|
|
309
|
+
/**
|
|
310
|
+
* Read one Cursor CLI/ACP SQLite session without mutating it. `raw` is a
|
|
311
|
+
* deterministic, lossless row/blob readout rather than the SQLite container's
|
|
312
|
+
* page bytes; the original store remains untouched.
|
|
313
|
+
*/
|
|
314
|
+
export async function readCursorNative(path, nativeSessionId) {
|
|
315
|
+
const { DatabaseSync } = await import('node:sqlite');
|
|
316
|
+
const dbPath = await storePath(path);
|
|
317
|
+
const db = new DatabaseSync(dbPath, { readOnly: true });
|
|
318
|
+
let metadataHex;
|
|
319
|
+
let metaRows;
|
|
320
|
+
let blobs;
|
|
321
|
+
try {
|
|
322
|
+
db.exec('PRAGMA query_only = ON');
|
|
323
|
+
const tables = db.prepare("SELECT name FROM sqlite_schema WHERE type='table' AND name IN ('blobs','meta') ORDER BY name").all();
|
|
324
|
+
if (tables.map(row => row.name).join(',') !== 'blobs,meta')
|
|
325
|
+
fail('unsupported SQLite schema');
|
|
326
|
+
const version = Number(db.prepare('PRAGMA user_version').get().user_version);
|
|
327
|
+
if (version !== 1)
|
|
328
|
+
fail(`unsupported SQLite user_version ${version}`);
|
|
329
|
+
metaRows = db.prepare('SELECT key, value FROM meta ORDER BY key').all().map(row => {
|
|
330
|
+
if (typeof row.key !== 'string' || typeof row.value !== 'string')
|
|
331
|
+
fail('invalid metadata row');
|
|
332
|
+
return { key: row.key, value: row.value };
|
|
333
|
+
});
|
|
334
|
+
const meta = metaRows.find(row => row.key === '0');
|
|
335
|
+
if (!meta || !/^(?:[0-9a-f]{2})+$/i.test(meta.value))
|
|
336
|
+
fail('metadata row is missing or invalid');
|
|
337
|
+
metadataHex = meta.value.toLowerCase();
|
|
338
|
+
blobs = db.prepare('SELECT id, data FROM blobs ORDER BY id').all().map(row => {
|
|
339
|
+
if (typeof row.id !== 'string' || !/^[0-9a-f]{64}$/i.test(row.id) || !(row.data instanceof Uint8Array))
|
|
340
|
+
fail('invalid blob row');
|
|
341
|
+
return { id: row.id.toLowerCase(), dataBase64: Buffer.from(row.data).toString('base64') };
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
finally {
|
|
345
|
+
db.close();
|
|
346
|
+
}
|
|
347
|
+
let metadata;
|
|
348
|
+
try {
|
|
349
|
+
metadata = JSON.parse(Buffer.from(metadataHex, 'hex').toString('utf8'));
|
|
350
|
+
}
|
|
351
|
+
catch {
|
|
352
|
+
return fail('metadata JSON is invalid');
|
|
353
|
+
}
|
|
354
|
+
if (typeof metadata.agentId !== 'string' || !metadata.agentId)
|
|
355
|
+
fail('metadata has no agentId');
|
|
356
|
+
if (nativeSessionId !== undefined && metadata.agentId !== nativeSessionId)
|
|
357
|
+
fail('selected session ID does not match store metadata');
|
|
358
|
+
if (typeof metadata.latestRootBlobId !== 'string' || !/^[0-9a-f]{64}$/i.test(metadata.latestRootBlobId))
|
|
359
|
+
fail('metadata has no valid root blob ID');
|
|
360
|
+
const readout = { format: FORMAT,
|
|
361
|
+
source: { runtime: 'cursor-agent', schema: 1, store: basename(dbPath) }, sessionId: metadata.agentId,
|
|
362
|
+
metadataHex, metaRows, rootBlobId: metadata.latestRootBlobId.toLowerCase(), blobs };
|
|
363
|
+
const sidecar = await readFile(join(dirname(dbPath), 'meta.json'), 'utf8').then(value => JSON.parse(value)).catch(() => null);
|
|
364
|
+
if (typeof sidecar?.cwd === 'string' && sidecar.cwd)
|
|
365
|
+
readout.provenance = { cwd: sidecar.cwd };
|
|
366
|
+
const raw = JSON.stringify(readout, null, 2) + '\n';
|
|
367
|
+
const transcript = decodeStore(readout);
|
|
368
|
+
const canonical = readCursor(raw);
|
|
369
|
+
if (canonical.diagnostics.some(diagnostic => diagnostic.code === 'invalid_json_line' || diagnostic.code === 'non_object_json_line')) {
|
|
370
|
+
fail('generated capture did not normalize cleanly');
|
|
371
|
+
}
|
|
372
|
+
return { raw, canonical, nativeSessionId: metadata.agentId, attachments: [{ name: 'cursor-capture.jsonl', content: transcript,
|
|
373
|
+
sha256: createHash('sha256').update(transcript).digest('hex') }] };
|
|
374
|
+
}
|