@ascenda-one/history-import 0.1.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +127 -0
- package/dist/cli.js +3889 -0
- package/dist/cli.js.map +1 -0
- package/dist/daySlice.js +98 -0
- package/dist/daySlice.js.map +1 -0
- package/dist/extractors/claudeCode.js +779 -0
- package/dist/extractors/claudeCode.js.map +1 -0
- package/dist/extractors/cursor.js +601 -0
- package/dist/extractors/cursor.js.map +1 -0
- package/dist/extractors/git.js +9 -0
- package/dist/extractors/git.js.map +1 -0
- package/dist/extractors/vscode.js +615 -0
- package/dist/extractors/vscode.js.map +1 -0
- package/dist/localHandoff.js +238 -0
- package/dist/localHandoff.js.map +1 -0
- package/dist/retention.js +68 -0
- package/dist/retention.js.map +1 -0
- package/dist/scan.js +220 -0
- package/dist/scan.js.map +1 -0
- package/dist/ship.js +208 -0
- package/dist/ship.js.map +1 -0
- package/dist/staging.js +121 -0
- package/dist/staging.js.map +1 -0
- package/dist/stores.js +25 -0
- package/dist/stores.js.map +1 -0
- package/dist/types.js +74 -0
- package/dist/types.js.map +1 -0
- package/package.json +32 -0
package/dist/ship.js
ADDED
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ships normalized historical events over the existing batch wire
|
|
3
|
+
* (`POST /v1/tool-events/batch`), authenticated with the same event write
|
|
4
|
+
* token the live hooks use — the import is just another paired tool speaking
|
|
5
|
+
* the same protocol, backdated.
|
|
6
|
+
*
|
|
7
|
+
* Three deliberate choices:
|
|
8
|
+
*
|
|
9
|
+
* - **Own fetch, not tool-kit's `postToolEventsBatch`.** The hook path
|
|
10
|
+
* collapses the response to accepted/failed because a hook can't do
|
|
11
|
+
* anything with detail. An importer can: the batch response carries
|
|
12
|
+
* per-item accepted/rejected with reasons, and surfacing "9,741 accepted,
|
|
13
|
+
* 3 rejected (validation_failed)" is the difference between a verifiable
|
|
14
|
+
* import and a shrug.
|
|
15
|
+
* - **Stable `importKey` per event.** sha256 over
|
|
16
|
+
* (store|sessionRef|eventKind|occurredAt|ordinal) — identical on every
|
|
17
|
+
* re-run of the same store. Since 19 Aug 2026 this is what the backend
|
|
18
|
+
* dedups on: `(pairedUser, toolInstallation, importKey)` is unique, a
|
|
19
|
+
* replayed event is reported as `duplicate` and writes nothing, and the
|
|
20
|
+
* key is *required* on anything carrying a historical provenance. Note it
|
|
21
|
+
* dedups on this key alone, NOT on `(extractionId, importKey)` — an
|
|
22
|
+
* extraction id is fresh per run, so including it would dedup nothing.
|
|
23
|
+
*
|
|
24
|
+
* `ordinal` used to be the event's index in the whole shipped array, which
|
|
25
|
+
* made the key stable only while the store was. A later run extracting a
|
|
26
|
+
* different *set* — Claude Code's 30-day purge having eaten the oldest
|
|
27
|
+
* days — shifted every subsequent ordinal, so unchanged records re-keyed
|
|
28
|
+
* and dedup silently stopped working on exactly the re-run it exists for.
|
|
29
|
+
* That is no longer a position: `importOrdinals` numbers an event only
|
|
30
|
+
* among events sharing its whole identity (store, session, kind, instant),
|
|
31
|
+
* so it disambiguates genuine duplicates and nothing else. Deleting a day,
|
|
32
|
+
* a session, or an entire store leaves every surviving key untouched.
|
|
33
|
+
* - **Raw local refs are hashed at the wire, not before.** The normalized
|
|
34
|
+
* file in staging keeps the real cwd (local, never leaves the machine);
|
|
35
|
+
* `workspaceHash`/`projectHash` go out as the same machine-salted 16-hex
|
|
36
|
+
* hashes the live hooks send, so historical and live events for the same
|
|
37
|
+
* repo correlate server-side without the server ever learning the path.
|
|
38
|
+
*/
|
|
39
|
+
import { createHash } from "node:crypto";
|
|
40
|
+
import { hashWithMachineSalt, readTokenFile, defaultTokenFilePath, utcOffsetMinutesAt } from "@ascenda-one/tool-kit";
|
|
41
|
+
import { EXTRACTION_EPOCH_KIND, HISTORICAL_CONSENT_SCOPE, STORE_SOURCE } from "./types.js";
|
|
42
|
+
export const DEFAULT_API_BASE_URL = "https://api.ascenda.one";
|
|
43
|
+
/** Mirrors tool-kit's loadCliAgentConfig, minus the hook-only pieces. */
|
|
44
|
+
export function loadShipConfig() {
|
|
45
|
+
const apiBaseUrl = (process.env.ASCENDA_API_BASE_URL ?? DEFAULT_API_BASE_URL).replace(/\/$/, "");
|
|
46
|
+
const idRaw = process.env.ASCENDA_TOOL_INSTALLATION_ID;
|
|
47
|
+
if (!idRaw) {
|
|
48
|
+
throw new Error("Missing ASCENDA_TOOL_INSTALLATION_ID — the importer ships as the machine's paired tool; pair it first");
|
|
49
|
+
}
|
|
50
|
+
const toolInstallationId = idRaw.trim().includes(":") ? idRaw.trim() : `claude_code:${idRaw.trim()}`;
|
|
51
|
+
const tokenFilePath = process.env.ASCENDA_EVENT_WRITE_TOKEN_FILE ?? defaultTokenFilePath(toolInstallationId);
|
|
52
|
+
const eventWriteToken = readTokenFile(tokenFilePath) ?? process.env.ASCENDA_EVENT_WRITE_TOKEN;
|
|
53
|
+
if (!eventWriteToken) {
|
|
54
|
+
throw new Error(`No event write token at ${tokenFilePath} (or ASCENDA_EVENT_WRITE_TOKEN)`);
|
|
55
|
+
}
|
|
56
|
+
return { apiBaseUrl, toolInstallationId, eventWriteToken };
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Per-event ordinals for a batch, positionally aligned with `events`.
|
|
60
|
+
*
|
|
61
|
+
* The ordinal exists only to separate events that are otherwise identical on
|
|
62
|
+
* the wire — same store, same session, same kind, same instant. Claude Code
|
|
63
|
+
* really does emit these: one prompt can produce several events sharing a
|
|
64
|
+
* millisecond, and without a tiebreak they would collapse to one key and the
|
|
65
|
+
* backend would dedup away real records.
|
|
66
|
+
*
|
|
67
|
+
* Counting within that identity rather than across the array is what makes
|
|
68
|
+
* the key survive a changing store. Two runs over stores that differ by
|
|
69
|
+
* whole days still agree on every record they share, because nothing outside
|
|
70
|
+
* an event's own identity group can move its number. Within a group the
|
|
71
|
+
* assignment order is irrelevant: the members are indistinguishable by
|
|
72
|
+
* definition, so any consistent numbering pairs them up.
|
|
73
|
+
*/
|
|
74
|
+
export function importOrdinals(events) {
|
|
75
|
+
const seen = new Map();
|
|
76
|
+
return events.map((event) => {
|
|
77
|
+
const identity = [event.store, event.sessionRef ?? "", event.eventKind, event.occurredAt].join("|");
|
|
78
|
+
const ordinal = seen.get(identity) ?? 0;
|
|
79
|
+
seen.set(identity, ordinal + 1);
|
|
80
|
+
return ordinal;
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
export function importKeyOf(event, ordinal) {
|
|
84
|
+
return createHash("sha256")
|
|
85
|
+
.update([event.store, event.sessionRef ?? "", event.eventKind, event.occurredAt, String(ordinal)].join("|"))
|
|
86
|
+
.digest("hex")
|
|
87
|
+
.slice(0, 16);
|
|
88
|
+
}
|
|
89
|
+
export function toWirePayload(event, ordinal, toolInstallationId) {
|
|
90
|
+
const metadata = {
|
|
91
|
+
importKey: importKeyOf(event, ordinal),
|
|
92
|
+
extractionId: event.extractionId,
|
|
93
|
+
importSchema: 1
|
|
94
|
+
};
|
|
95
|
+
if (event.sourceVersion)
|
|
96
|
+
metadata.sourceVersion = event.sourceVersion;
|
|
97
|
+
for (const [key, value] of Object.entries(event.metrics)) {
|
|
98
|
+
metadata[key] = value;
|
|
99
|
+
}
|
|
100
|
+
// gitBranch is a metric locally but a name on the wire — hash it like the
|
|
101
|
+
// repo path. Branch names leak project vocabulary ("feature/acme-migration").
|
|
102
|
+
if (typeof metadata.gitBranch === "string") {
|
|
103
|
+
metadata.gitBranchHash = hashWithMachineSalt(metadata.gitBranch) ?? "";
|
|
104
|
+
delete metadata.gitBranch;
|
|
105
|
+
}
|
|
106
|
+
const workspaceHash = event.repoRef ? hashWithMachineSalt(event.repoRef) : null;
|
|
107
|
+
return {
|
|
108
|
+
toolInstallationId,
|
|
109
|
+
source: STORE_SOURCE[event.store],
|
|
110
|
+
// Always a canonical catalog type. `eventKind` is typed against the
|
|
111
|
+
// contract union and the epoch marker is filtered out upstream in
|
|
112
|
+
// `shippableEvents`, so no cast is needed here — and an off-catalog name
|
|
113
|
+
// cannot reach the wire to be silently bucketed as `unclassified`.
|
|
114
|
+
eventType: event.eventKind,
|
|
115
|
+
occurredAt: event.occurredAt,
|
|
116
|
+
// The offset in force WHEN THE EVENT HAPPENED, not when the import ran.
|
|
117
|
+
// A nine-month backfill crosses DST boundaries; stamping today's
|
|
118
|
+
// offset on all of it would shift a whole season by an hour.
|
|
119
|
+
utcOffsetMinutes: utcOffsetMinutesAt(new Date(event.occurredAt)),
|
|
120
|
+
severity: "low",
|
|
121
|
+
sessionId: event.sessionRef,
|
|
122
|
+
workspaceHash,
|
|
123
|
+
projectHash: workspaceHash,
|
|
124
|
+
// A real contract ToolConsentScope now, and a real gate: the backend
|
|
125
|
+
// requires an active historical-import consent lease for anything carrying
|
|
126
|
+
// one of the provenance classes below, and decides that on the provenance
|
|
127
|
+
// rather than on this string — sending `ide_telemetry` here would not buy
|
|
128
|
+
// the event a way in.
|
|
129
|
+
consentScope: HISTORICAL_CONSENT_SCOPE,
|
|
130
|
+
provenance: event.provenance,
|
|
131
|
+
privacyMode: "metadata_only",
|
|
132
|
+
metadata
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
const BATCH_SIZE = 200;
|
|
136
|
+
/**
|
|
137
|
+
* The events that belong on the wire: everything except the extraction epoch
|
|
138
|
+
* marker, which is local bookkeeping about the read itself rather than an
|
|
139
|
+
* observation of anyone's work (see `EXTRACTION_EPOCH_KIND`). Exported so the
|
|
140
|
+
* CLI can report the same count it is about to send.
|
|
141
|
+
*/
|
|
142
|
+
export function shippableEvents(events) {
|
|
143
|
+
return events.filter((e) => e.eventKind !== EXTRACTION_EPOCH_KIND);
|
|
144
|
+
}
|
|
145
|
+
export async function shipEvents(events, config, onProgress) {
|
|
146
|
+
const result = {
|
|
147
|
+
sent: 0,
|
|
148
|
+
accepted: 0,
|
|
149
|
+
duplicate: 0,
|
|
150
|
+
rejected: 0,
|
|
151
|
+
rejectionReasons: {},
|
|
152
|
+
httpFailures: 0
|
|
153
|
+
};
|
|
154
|
+
const wireEvents = shippableEvents(events);
|
|
155
|
+
// Ordinals are assigned across the whole shipment before it is cut into
|
|
156
|
+
// batches: an identity group split by a chunk boundary must still number
|
|
157
|
+
// continuously, or the same record would key differently depending on
|
|
158
|
+
// where the batching happened to fall.
|
|
159
|
+
const ordinals = importOrdinals(wireEvents);
|
|
160
|
+
for (let offset = 0; offset < wireEvents.length; offset += BATCH_SIZE) {
|
|
161
|
+
const chunk = wireEvents.slice(offset, offset + BATCH_SIZE);
|
|
162
|
+
const payloads = chunk.map((event, i) => toWirePayload(event, ordinals[offset + i], config.toolInstallationId));
|
|
163
|
+
let response;
|
|
164
|
+
try {
|
|
165
|
+
response = await fetch(`${config.apiBaseUrl}/v1/tool-events/batch`, {
|
|
166
|
+
method: "POST",
|
|
167
|
+
headers: {
|
|
168
|
+
"content-type": "application/json",
|
|
169
|
+
authorization: `Bearer ${config.eventWriteToken}`
|
|
170
|
+
},
|
|
171
|
+
body: JSON.stringify({ events: payloads })
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
catch {
|
|
175
|
+
result.httpFailures += 1;
|
|
176
|
+
continue; // Transport failure: skip this chunk, keep going — the stable
|
|
177
|
+
// importKey makes a later re-run of just the gaps safe to reconcile.
|
|
178
|
+
}
|
|
179
|
+
result.sent += chunk.length;
|
|
180
|
+
if (!response.ok) {
|
|
181
|
+
result.httpFailures += 1;
|
|
182
|
+
if (response.status === 401 || response.status === 403) {
|
|
183
|
+
throw new Error(`Ingest refused (${response.status}) — token invalid/revoked or consent missing; aborting rather than burning ${wireEvents.length - offset} more events`);
|
|
184
|
+
}
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
try {
|
|
188
|
+
const body = (await response.json());
|
|
189
|
+
result.accepted += body.accepted ?? 0;
|
|
190
|
+
result.duplicate += body.duplicate ?? 0;
|
|
191
|
+
result.rejected += body.rejected ?? 0;
|
|
192
|
+
for (const item of body.results ?? []) {
|
|
193
|
+
// Duplicates have their own counter; bucketing them as rejection
|
|
194
|
+
// reasons too would report the same events twice under two headings.
|
|
195
|
+
if (item.status !== "accepted" && item.status !== "duplicate") {
|
|
196
|
+
const reason = item.reason ?? "unknown";
|
|
197
|
+
result.rejectionReasons[reason] = (result.rejectionReasons[reason] ?? 0) + 1;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
catch {
|
|
202
|
+
result.httpFailures += 1;
|
|
203
|
+
}
|
|
204
|
+
onProgress?.(Math.min(offset + BATCH_SIZE, wireEvents.length), wireEvents.length);
|
|
205
|
+
}
|
|
206
|
+
return result;
|
|
207
|
+
}
|
|
208
|
+
//# sourceMappingURL=ship.js.map
|
package/dist/ship.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ship.js","sourceRoot":"","sources":["../src/ship.ts"],"names":[],"mappings":"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"}
|
package/dist/staging.js
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copy-then-parse.
|
|
3
|
+
*
|
|
4
|
+
* Every store is a live file another process is writing: Claude Code appends
|
|
5
|
+
* to the transcript mid-session, Cursor's db is WAL-mode with a hot journal.
|
|
6
|
+
* Parsing in place risks torn reads AND ties extraction time to purge time —
|
|
7
|
+
* a Claude cleanup running mid-extract would delete lines under the parser.
|
|
8
|
+
* So extraction always runs against a snapshot, stamped with an extraction id
|
|
9
|
+
* that every emitted event carries (it is also the natural idempotency key
|
|
10
|
+
* for backend dedup).
|
|
11
|
+
*/
|
|
12
|
+
import * as fs from "node:fs/promises";
|
|
13
|
+
import { constants } from "node:fs";
|
|
14
|
+
import * as path from "node:path";
|
|
15
|
+
import { randomUUID } from "node:crypto";
|
|
16
|
+
/** Clone-on-write when the filesystem supports it (APFS does): a 2.3 GB
|
|
17
|
+
* store snapshots in milliseconds and costs no disk until the live files
|
|
18
|
+
* diverge. NOT `FICLONE_FORCE` — on filesystems without reflinks this must
|
|
19
|
+
* degrade to a real copy, not an error. */
|
|
20
|
+
const CLONE = constants.COPYFILE_FICLONE;
|
|
21
|
+
/** Default staging root. Local, never synced, never shipped — raw records
|
|
22
|
+
* (including UNPARSED ones) stay here on the machine. */
|
|
23
|
+
export function defaultStagingRoot(home) {
|
|
24
|
+
return path.join(home, ".ascenda", "history-import", "staging");
|
|
25
|
+
}
|
|
26
|
+
export async function createStagingArea(stagingRoot) {
|
|
27
|
+
const extractionId = `${new Date().toISOString().replace(/[:.]/g, "-")}-${randomUUID().slice(0, 8)}`;
|
|
28
|
+
const root = path.join(stagingRoot, extractionId);
|
|
29
|
+
await fs.mkdir(root, { recursive: true });
|
|
30
|
+
return { extractionId, root };
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Snapshot a file or directory into the staging area. For SQLite stores pass
|
|
34
|
+
* the db path — the `-wal` and `-shm` siblings are picked up automatically,
|
|
35
|
+
* because a WAL db copied without its journal silently loses the newest
|
|
36
|
+
* transactions (the most recent sessions: exactly the ones a user would
|
|
37
|
+
* notice missing).
|
|
38
|
+
*/
|
|
39
|
+
export async function snapshotPath(area, sourcePath, label) {
|
|
40
|
+
const dest = path.join(area.root, label);
|
|
41
|
+
await fs.mkdir(path.dirname(dest), { recursive: true });
|
|
42
|
+
const st = await fs.stat(sourcePath);
|
|
43
|
+
if (st.isDirectory()) {
|
|
44
|
+
await fs.cp(sourcePath, dest, {
|
|
45
|
+
recursive: true,
|
|
46
|
+
errorOnExist: false,
|
|
47
|
+
force: true,
|
|
48
|
+
mode: CLONE
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
await fs.copyFile(sourcePath, dest, CLONE);
|
|
53
|
+
for (const suffix of ["-wal", "-shm"]) {
|
|
54
|
+
try {
|
|
55
|
+
await fs.copyFile(sourcePath + suffix, dest + suffix, CLONE);
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
// No journal sibling — a checkpointed or non-WAL db. Fine.
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return dest;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Surgical snapshot of `Code/User/workspaceStorage` for the VS Code
|
|
66
|
+
* extractor: only `workspace.json` (workspace identity) and
|
|
67
|
+
* `chatSessions/*.json` and `chatSessions/*.jsonl` (Copilot sessions) per
|
|
68
|
+
* hash directory, never the
|
|
69
|
+
* rest — `state.vscdb`, `state.vscdb.backup`, extension caches and other
|
|
70
|
+
* per-workspace artifacts live alongside those two and can run to gigabytes
|
|
71
|
+
* across dozens of workspaces, none of it needed by this extractor. A plain
|
|
72
|
+
* `snapshotPath` recursive copy would drag all of it into staging for
|
|
73
|
+
* nothing; this walks the hash directories itself and copies only what the
|
|
74
|
+
* extractor reads.
|
|
75
|
+
*/
|
|
76
|
+
export async function snapshotVsCodeWorkspaceStorage(area, sourceRoot, label) {
|
|
77
|
+
const dest = path.join(area.root, label);
|
|
78
|
+
await fs.mkdir(dest, { recursive: true });
|
|
79
|
+
let hashDirs = [];
|
|
80
|
+
try {
|
|
81
|
+
hashDirs = (await fs.readdir(sourceRoot, { withFileTypes: true }))
|
|
82
|
+
.filter((e) => e.isDirectory())
|
|
83
|
+
.map((e) => e.name);
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
return dest; // No workspaceStorage on this machine — an empty snapshot, not an error.
|
|
87
|
+
}
|
|
88
|
+
for (const hash of hashDirs) {
|
|
89
|
+
const destHashDir = path.join(dest, hash);
|
|
90
|
+
const workspaceJson = path.join(sourceRoot, hash, "workspace.json");
|
|
91
|
+
try {
|
|
92
|
+
await fs.mkdir(destHashDir, { recursive: true });
|
|
93
|
+
await fs.copyFile(workspaceJson, path.join(destHashDir, "workspace.json"), CLONE);
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
// No workspace.json for this hash — common, and chatSessions below may
|
|
97
|
+
// still exist and be worth copying.
|
|
98
|
+
}
|
|
99
|
+
const chatSessionsDir = path.join(sourceRoot, hash, "chatSessions");
|
|
100
|
+
let sessionFiles = [];
|
|
101
|
+
try {
|
|
102
|
+
// Both on-disk shapes: `.json` (pre-Feb-2026) and `.jsonl` (the delta-log
|
|
103
|
+
// format VS Code migrated to). Filtering to `.json` here is what kept
|
|
104
|
+
// seven months of sessions out of staging entirely — the extractor never
|
|
105
|
+
// saw them, so it could not even report them as unread.
|
|
106
|
+
sessionFiles = (await fs.readdir(chatSessionsDir)).filter((f) => f.endsWith(".json") || f.endsWith(".jsonl"));
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
continue; // Most workspaces have no chatSessions dir — the norm.
|
|
110
|
+
}
|
|
111
|
+
if (sessionFiles.length === 0)
|
|
112
|
+
continue;
|
|
113
|
+
const destSessionsDir = path.join(destHashDir, "chatSessions");
|
|
114
|
+
await fs.mkdir(destSessionsDir, { recursive: true });
|
|
115
|
+
for (const file of sessionFiles) {
|
|
116
|
+
await fs.copyFile(path.join(chatSessionsDir, file), path.join(destSessionsDir, file), CLONE);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return dest;
|
|
120
|
+
}
|
|
121
|
+
//# sourceMappingURL=staging.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"staging.js","sourceRoot":"","sources":["../src/staging.ts"],"names":[],"mappings":"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"}
|
package/dist/stores.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where each store lives on disk. Resolution is centralised here because the
|
|
3
|
+
* paths are the reverse-engineered part of the whole design: every one of
|
|
4
|
+
* them (except git) is an undocumented internal of someone else's product,
|
|
5
|
+
* and when an upstream release moves a store, this file is where the fix
|
|
6
|
+
* lands.
|
|
7
|
+
*/
|
|
8
|
+
import * as os from "node:os";
|
|
9
|
+
import * as path from "node:path";
|
|
10
|
+
export function resolveStorePaths(home = os.homedir()) {
|
|
11
|
+
const appSupport = path.join(home, "Library", "Application Support");
|
|
12
|
+
return {
|
|
13
|
+
home,
|
|
14
|
+
claudeRoot: path.join(home, ".claude"),
|
|
15
|
+
claudeProjects: path.join(home, ".claude", "projects"),
|
|
16
|
+
claudeSettings: path.join(home, ".claude", "settings.json"),
|
|
17
|
+
cursorStateDb: path.join(appSupport, "Cursor", "User", "globalStorage", "state.vscdb"),
|
|
18
|
+
cursorSearchDb: path.join(appSupport, "Cursor", "User", "globalStorage", "conversation-search.db"),
|
|
19
|
+
vscodeHistory: path.join(appSupport, "Code", "User", "History"),
|
|
20
|
+
vscodeWorkspaceStorage: path.join(appSupport, "Code", "User", "workspaceStorage"),
|
|
21
|
+
devRoot: path.join(home, "Dev"),
|
|
22
|
+
zshHistory: path.join(home, ".zsh_history")
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
//# sourceMappingURL=stores.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stores.js","sourceRoot":"","sources":["../src/stores.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,KAAK,EAAE,MAAM,SAAS,CAAC;AAC9B,OAAO,KAAK,IAAI,MAAM,WAAW,CAAC;AAsBlC,MAAM,UAAU,iBAAiB,CAAC,OAAe,EAAE,CAAC,OAAO,EAAE;IAC3D,MAAM,UAAU,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,SAAS,EAAE,qBAAqB,CAAC,CAAC;IACrE,OAAO;QACL,IAAI;QACJ,UAAU,EAAE,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,SAAS,CAAC;QACtC,cAAc,EAAE,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,SAAS,EAAE,UAAU,CAAC;QACtD,cAAc,EAAE,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,SAAS,EAAE,eAAe,CAAC;QAC3D,aAAa,EAAE,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,QAAQ,EAAE,MAAM,EAAE,eAAe,EAAE,aAAa,CAAC;QACtF,cAAc,EAAE,IAAI,CAAC,IAAI,CACvB,UAAU,EACV,QAAQ,EACR,MAAM,EACN,eAAe,EACf,wBAAwB,CACzB;QACD,aAAa,EAAE,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,CAAC;QAC/D,sBAAsB,EAAE,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,MAAM,EAAE,MAAM,EAAE,kBAAkB,CAAC;QACjF,OAAO,EAAE,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,KAAK,CAAC;QAC/B,UAAU,EAAE,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,cAAc,CAAC;KAC5C,CAAC;AACJ,CAAC"}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared shapes for the retrospective import.
|
|
3
|
+
*
|
|
4
|
+
* The design rules these encode come from the research note in the Flow
|
|
5
|
+
* workspace (`docs/HISTORICAL_TELEMETRY_IMPORT.md`): copy-then-parse,
|
|
6
|
+
* per-record schema sniffing, UNPARSED over guessing, and provenance class
|
|
7
|
+
* carried as data on every event so downstream charts can render HISTORICAL
|
|
8
|
+
* bars distinctly from LIVE ones.
|
|
9
|
+
*/
|
|
10
|
+
import { ASCENDA_HISTORICAL_CONSENT_SCOPE } from "@ascenda-one/tool-contract";
|
|
11
|
+
/** The stores this importer knows how to read. Ordered by evaporation risk:
|
|
12
|
+
* Claude Code's 30-day rolling purge deletes a day of baseline every day the
|
|
13
|
+
* importer hasn't run, so it always extracts first. */
|
|
14
|
+
export const HISTORY_STORES = ["claude_code", "cursor", "vscode", "git"];
|
|
15
|
+
/** How each store's events identify on the existing telemetry wire. */
|
|
16
|
+
export const STORE_SOURCE = {
|
|
17
|
+
claude_code: "claude_code",
|
|
18
|
+
cursor: "cursor_mcp",
|
|
19
|
+
vscode: "vscode_extension",
|
|
20
|
+
git: "code_forge"
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* Provenance classes for imported events. Live events carry
|
|
24
|
+
* `ASCENDA_PROVENANCE` ("ai_work_telemetry"); everything this package emits
|
|
25
|
+
* carries one of these instead, so no historical number can masquerade as a
|
|
26
|
+
* live observation.
|
|
27
|
+
*
|
|
28
|
+
* - `direct`: read verbatim from a store record (a timestamp, a token count).
|
|
29
|
+
* - `derived`: computed across records (session length, gap analysis).
|
|
30
|
+
* - `unparsed`: the record's self-labelled schema version was unknown; the
|
|
31
|
+
* raw record is retained in staging and nothing was inferred from it.
|
|
32
|
+
*/
|
|
33
|
+
export const HISTORICAL_PROVENANCE = {
|
|
34
|
+
direct: "historical_direct",
|
|
35
|
+
derived: "historical_derived",
|
|
36
|
+
unparsed: "historical_unparsed"
|
|
37
|
+
};
|
|
38
|
+
/**
|
|
39
|
+
* The consent scope imported events must carry. A real `ToolConsentScope`
|
|
40
|
+
* (re-exported from tool-contract rather than restated here, so the two
|
|
41
|
+
* cannot drift): consenting to prospective `ide_telemetry` is not consenting
|
|
42
|
+
* to a read of nine months of history.
|
|
43
|
+
*
|
|
44
|
+
* **The backend does not enforce this yet, and this package must not ship as
|
|
45
|
+
* if it did.** `ConsentType.HistoricalImport` (507), the `historical_import`
|
|
46
|
+
* scope mapping and the provenance override are implemented on the asc-core-be
|
|
47
|
+
* branch `claude/historical-import-dedup-and-consent`, and are absent from
|
|
48
|
+
* `origin/main` — verified 19 Aug 2026, zero occurrences of `HistoricalImport`
|
|
49
|
+
* in `Services/` or `Models/` there.
|
|
50
|
+
*
|
|
51
|
+
* Until that branch merges, `ResolveConsentType` does not recognise
|
|
52
|
+
* `historical_import` and takes its default arm, `ConsentType.AiDataProcessing`
|
|
53
|
+
* — the lease a user already granted for ordinary live IDE telemetry. So
|
|
54
|
+
* against main today, nine months of retrospective history would ingest under
|
|
55
|
+
* a consent the user gave for something else, which is the precise outcome the
|
|
56
|
+
* separate scope exists to prevent.
|
|
57
|
+
*
|
|
58
|
+
* Sending this scope is still correct: it is what the enforcing backend keys
|
|
59
|
+
* on, and it makes the intent explicit in the audit record either way. What is
|
|
60
|
+
* not correct is publishing this package while a reader of this file would
|
|
61
|
+
* conclude the protection is live.
|
|
62
|
+
*/
|
|
63
|
+
export const HISTORICAL_CONSENT_SCOPE = ASCENDA_HISTORICAL_CONSENT_SCOPE;
|
|
64
|
+
/**
|
|
65
|
+
* The extraction-window marker every extractor emits once per store. It is
|
|
66
|
+
* **not** a work event: it carries no session, no repo, and its metrics are
|
|
67
|
+
* statistics about the extraction run itself (observed window, unparsed file
|
|
68
|
+
* counts). It exists so `scan`/`import` can report the window and so the
|
|
69
|
+
* handoff can bound its own data, and the shipper drops it before the wire —
|
|
70
|
+
* there is no canonical telemetry type for "here is what I read", and
|
|
71
|
+
* inventing one would put extraction bookkeeping into the work catalog.
|
|
72
|
+
*/
|
|
73
|
+
export const EXTRACTION_EPOCH_KIND = "extraction_epoch";
|
|
74
|
+
//# sourceMappingURL=types.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAEA;;;;;;;;GAQG;AACH,OAAO,EAAE,gCAAgC,EAAE,MAAM,4BAA4B,CAAC;AAG9E;;uDAEuD;AACvD,MAAM,CAAC,MAAM,cAAc,GAAG,CAAC,aAAa,EAAE,QAAQ,EAAE,QAAQ,EAAE,KAAK,CAAU,CAAC;AAGlF,uEAAuE;AACvE,MAAM,CAAC,MAAM,YAAY,GAAiD;IACxE,WAAW,EAAE,aAAa;IAC1B,MAAM,EAAE,YAAY;IACpB,MAAM,EAAE,kBAAkB;IAC1B,GAAG,EAAE,YAAY;CAClB,CAAC;AAEF;;;;;;;;;;GAUG;AACH,MAAM,CAAC,MAAM,qBAAqB,GAAG;IACnC,MAAM,EAAE,mBAAmB;IAC3B,OAAO,EAAE,oBAAoB;IAC7B,QAAQ,EAAE,qBAAqB;CACvB,CAAC;AAIX;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,MAAM,CAAC,MAAM,wBAAwB,GAAG,gCAAgC,CAAC;AAoBzE;;;;;;;;GAQG;AACH,MAAM,CAAC,MAAM,qBAAqB,GAAG,kBAA2B,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"publishConfig": { "access": "public" },
|
|
3
|
+
"name": "@ascenda-one/history-import",
|
|
4
|
+
"version": "0.1.12",
|
|
5
|
+
"description": "Retrospective AI-usage import for Ascenda work telemetry — extracts the baseline already sitting in Claude Code, Cursor and VS Code stores.",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "https://github.com/ascendaone-com/ai-engineer-tools.git",
|
|
9
|
+
"directory": "ascenda-history-import"
|
|
10
|
+
},
|
|
11
|
+
"type": "module",
|
|
12
|
+
"files": ["dist"],
|
|
13
|
+
"bin": {
|
|
14
|
+
"ascenda-history-import": "./dist/cli.js"
|
|
15
|
+
},
|
|
16
|
+
"scripts": {
|
|
17
|
+
"build": "tsc -p ./ && npm run bundle",
|
|
18
|
+
"watch": "tsc -w -p ./",
|
|
19
|
+
"bundle": "esbuild src/cli.ts --bundle --platform=node --format=esm --banner:js=\"import { createRequire as __cr } from 'node:module'; const require = __cr(import.meta.url);\" --outfile=dist/cli.js",
|
|
20
|
+
"test": "node --test"
|
|
21
|
+
},
|
|
22
|
+
"license": "Apache-2.0",
|
|
23
|
+
"engines": {
|
|
24
|
+
"node": ">=20"
|
|
25
|
+
},
|
|
26
|
+
"devDependencies": {
|
|
27
|
+
"@types/node": "^20.14.10",
|
|
28
|
+
"typescript": "^5.5.0",
|
|
29
|
+
"@ascenda-one/tool-contract": "*",
|
|
30
|
+
"@ascenda-one/tool-kit": "*"
|
|
31
|
+
}
|
|
32
|
+
}
|