@groundtrack/hooks 0.1.9 → 0.1.10

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 (39) hide show
  1. package/README.md +28 -10
  2. package/dist/agents.d.ts +55 -0
  3. package/dist/agents.js +77 -3
  4. package/dist/groundtrack-hook.js +0 -0
  5. package/dist/groundtrack.js +0 -0
  6. package/dist/session-import/canonical.d.ts +33 -0
  7. package/dist/session-import/canonical.js +10 -0
  8. package/dist/session-import/claude.d.ts +20 -0
  9. package/dist/session-import/claude.js +237 -0
  10. package/dist/session-import/cli.d.ts +3 -2
  11. package/dist/session-import/cli.js +161 -131
  12. package/dist/session-import/client.d.ts +4 -3
  13. package/dist/session-import/client.js +4 -3
  14. package/dist/session-import/codex.d.ts +6 -33
  15. package/dist/session-import/codex.js +62 -9
  16. package/dist/session-import/cursor.d.ts +13 -0
  17. package/dist/session-import/cursor.js +268 -0
  18. package/dist/session-import/discovery.d.ts +16 -7
  19. package/dist/session-import/discovery.js +120 -67
  20. package/dist/session-import/format.d.ts +51 -0
  21. package/dist/session-import/format.js +11 -0
  22. package/dist/session-import/formats.d.ts +4 -0
  23. package/dist/session-import/formats.js +13 -0
  24. package/dist/session-import/normalize.d.ts +21 -0
  25. package/dist/session-import/normalize.js +96 -0
  26. package/dist/session-import/opencode-cli.d.ts +14 -0
  27. package/dist/session-import/opencode-cli.js +178 -0
  28. package/dist/session-import/opencode-normalizer.d.ts +8 -0
  29. package/dist/session-import/opencode-normalizer.js +230 -0
  30. package/dist/session-import/opencode.d.ts +8 -0
  31. package/dist/session-import/opencode.js +230 -0
  32. package/dist/session-import/prune.d.ts +13 -4
  33. package/dist/session-import/prune.js +45 -79
  34. package/dist/session-import/receipts.d.ts +2 -1
  35. package/dist/session-import/receipts.js +3 -3
  36. package/dist/session-import/types.d.ts +21 -7
  37. package/dist/setup/agents.d.ts +32 -0
  38. package/package.json +29 -1
  39. package/src/agents.ts +104 -3
package/README.md CHANGED
@@ -87,16 +87,28 @@ The lower-level `runDoctor` API remains for existing consumers. New setup uses
87
87
  client-specific plans for MCP, hooks/plugins, and instructions, so it can preview
88
88
  and repair several selected agents safely.
89
89
 
90
- ## Import Codex sessions
91
-
92
- Import completed Codex sessions from `CODEX_HOME` (default `~/.codex`) with an API key
93
- created in Groundtrack Settings. The default scope is the current Git repository;
94
- subagents and files modified in the last five minutes are excluded.
90
+ ## Import coding-agent sessions
91
+
92
+ Import completed Codex, Claude Code, Cursor, or OpenCode sessions with an API key created in Groundtrack
93
+ Settings. The default scope is the current Git repository; subagents and files modified
94
+ in the last five minutes are excluded. Groundtrack reads Codex sessions from `CODEX_HOME`
95
+ (default `~/.codex`), Claude sessions from `CLAUDE_CONFIG_DIR` (default `~/.claude`),
96
+ and Cursor transcripts from `CURSOR_HOME` (default `~/.cursor`). Cursor metadata comes from
97
+ its local `state.vscdb`; set `CURSOR_STATE_DB` to override that database path. Cursor import
98
+ is CLI-only and requires unflagged `node:sqlite` support (Node.js 22.13+, 23.4+, or 24+).
99
+ OpenCode import invokes the installed `opencode` CLI (override it with `OPENCODE_BIN`) and uses
100
+ private temporary files for its session inventory and exports. This avoids OpenCode's large piped
101
+ export truncation while ensuring the unsanitized source export is removed after local pruning.
95
102
 
96
103
  ```sh
97
104
  GROUNDTRACK_API_KEY=... groundtrack import --dry-run
98
105
  GROUNDTRACK_API_KEY=... groundtrack import --yes
99
- GROUNDTRACK_API_KEY=... groundtrack import --all-repos --include-unscoped --yes
106
+ GROUNDTRACK_API_KEY=... groundtrack import --agent claude --dry-run
107
+ GROUNDTRACK_API_KEY=... groundtrack import --agent claude --all-repos --yes
108
+ GROUNDTRACK_API_KEY=... groundtrack import --agent cursor --dry-run
109
+ GROUNDTRACK_API_KEY=... groundtrack import --agent cursor --all-repos --yes
110
+ GROUNDTRACK_API_KEY=... groundtrack import --agent opencode --dry-run
111
+ GROUNDTRACK_API_KEY=... groundtrack import --agent opencode --all-repos --yes
100
112
  ```
101
113
 
102
114
  The command streams source files through a deterministic privacy filter before upload.
@@ -107,16 +119,22 @@ transient failures. Successful imports are recorded atomically under
107
119
  visibility, source hash, and canonicalization version. Use `--json` for one machine-readable
108
120
  result object and `--url` for a preview or self-hosted deployment.
109
121
 
122
+ Imported transcripts are private by default. Groundtrack analyzes completed imports and may add
123
+ high-confidence extracted lessons—not transcript content—to the organization's shared knowledge.
124
+ Changing transcript visibility does not change this learning behavior; deleting an imported
125
+ session retracts its derived knowledge.
126
+
110
127
  `templates/AGENTS.md` is the source of truth for the managed repository policy.
111
128
  After changing it, run `pnpm sync:agent-instructions`; tests reject drift between
112
129
  the setup guide, this repository's managed `AGENTS.md` block, and the packaged template.
113
130
 
114
131
  ## Development
115
132
 
116
- Agent IDs, names, aliases, event sources, and enrichment kinds are defined once in
117
- `src/agents.ts`, exported as `@groundtrack/hooks/agents`. The CLI, web UI, hooks,
118
- and server consume that catalog and its derived types. Keep it dependency-free
119
- so browser bundles can load it without the Node-only hook runtime.
133
+ Agent IDs, names, aliases, event sources, enrichment kinds, and session-import
134
+ capabilities are defined once in `src/agents.ts`, exported as
135
+ `@groundtrack/hooks/agents`. The CLI, web UI, hooks, and server consume that
136
+ catalog and its derived types. Keep it dependency-free so browser bundles can
137
+ load it without the Node-only hook runtime.
120
138
 
121
139
  Build and test the CLI from the repository:
122
140
 
package/dist/agents.d.ts CHANGED
@@ -5,24 +5,56 @@ export declare const AGENTS: readonly [{
5
5
  readonly aliases: readonly ["claude-code"];
6
6
  readonly source: "claude_code";
7
7
  readonly enrichment: "command";
8
+ readonly sessionImport: {
9
+ readonly cli: true;
10
+ readonly web: {
11
+ readonly extension: ".jsonl";
12
+ readonly accept: ".jsonl";
13
+ readonly fileLabel: "Claude Code session JSONL";
14
+ readonly location: "~/.claude/projects/<project>/*.jsonl";
15
+ };
16
+ };
8
17
  }, {
9
18
  readonly id: "codex";
10
19
  readonly name: "Codex";
11
20
  readonly aliases: readonly [];
12
21
  readonly source: "codex";
13
22
  readonly enrichment: "command";
23
+ readonly sessionImport: {
24
+ readonly cli: true;
25
+ readonly web: {
26
+ readonly extension: ".jsonl";
27
+ readonly accept: ".jsonl";
28
+ readonly fileLabel: "Codex rollout JSONL";
29
+ readonly location: "~/.codex/sessions/YYYY/MM/DD/rollout-*.jsonl";
30
+ };
31
+ };
14
32
  }, {
15
33
  readonly id: "cursor";
16
34
  readonly name: "Cursor";
17
35
  readonly aliases: readonly [];
18
36
  readonly source: "cursor";
19
37
  readonly enrichment: "command";
38
+ readonly sessionImport: {
39
+ readonly cli: true;
40
+ readonly web: null;
41
+ };
20
42
  }, {
21
43
  readonly id: "opencode";
22
44
  readonly name: "OpenCode";
23
45
  readonly aliases: readonly [];
24
46
  readonly source: "opencode";
25
47
  readonly enrichment: "plugin";
48
+ readonly sessionImport: {
49
+ readonly cli: true;
50
+ readonly web: {
51
+ readonly extension: ".json";
52
+ readonly accept: ".json,application/json";
53
+ readonly fileLabel: "OpenCode session export JSON";
54
+ readonly location: "opencode export <session-id> --pure > opencode-session.json";
55
+ readonly note: "Use direct file redirection as shown. Do not add --sanitize; Groundtrack removes sensitive records during import.";
56
+ };
57
+ };
26
58
  }];
27
59
  export type AgentDefinition = (typeof AGENTS)[number];
28
60
  export type Agent = AgentDefinition["id"];
@@ -30,7 +62,30 @@ export type CommandHookAgent = Extract<AgentDefinition, {
30
62
  enrichment: "command";
31
63
  }>["id"];
32
64
  export declare const AGENT_IDS: ("claude" | "codex" | "cursor" | "opencode")[];
65
+ export type SessionImportAgentDefinition = Extract<AgentDefinition, {
66
+ sessionImport: {
67
+ cli: true;
68
+ };
69
+ }>;
70
+ export type SessionImportAgent = SessionImportAgentDefinition["id"];
71
+ export type AgentSessionSource = SessionImportAgentDefinition["source"];
72
+ export type WebSessionImportAgentDefinition = Extract<SessionImportAgentDefinition, {
73
+ sessionImport: {
74
+ web: object;
75
+ };
76
+ }>;
77
+ export type WebSessionImportSource = WebSessionImportAgentDefinition["source"];
78
+ export declare const SESSION_IMPORT_AGENTS: SessionImportAgentDefinition[];
79
+ export declare const WEB_SESSION_IMPORT_AGENTS: WebSessionImportAgentDefinition[];
80
+ export declare const WEB_SESSION_IMPORT_SOURCES: ("claude_code" | "codex" | "opencode")[];
81
+ export declare const DEFAULT_SESSION_IMPORT_AGENT: SessionImportAgent;
82
+ export declare const DEFAULT_WEB_SESSION_IMPORT_SOURCE: WebSessionImportSource;
33
83
  export declare function isAgent(value: unknown): value is Agent;
34
84
  export declare function resolveAgent(value: string): AgentDefinition | undefined;
85
+ export declare function isSessionImportAgent(value: string): value is SessionImportAgent;
86
+ export declare function normalizeAgentSessionSource(value: string): AgentSessionSource | undefined;
87
+ export declare function isWebSessionImportSource(value: string): value is WebSessionImportSource;
88
+ export declare function resolveSessionImportAgentFromSource(source: string | undefined): SessionImportAgentDefinition | undefined;
89
+ export declare function webSessionImportAgentFromSource(source: WebSessionImportSource): WebSessionImportAgentDefinition;
35
90
  export declare function agentFromClientName(value: string | null | undefined): AgentDefinition | undefined;
36
91
  export declare function requiresHookRuntime(id: Agent): boolean;
package/dist/agents.js CHANGED
@@ -6,18 +6,92 @@ export const AGENTS = [
6
6
  aliases: ["claude-code"],
7
7
  source: "claude_code",
8
8
  enrichment: "command",
9
+ sessionImport: {
10
+ cli: true,
11
+ web: {
12
+ extension: ".jsonl",
13
+ accept: ".jsonl",
14
+ fileLabel: "Claude Code session JSONL",
15
+ location: "~/.claude/projects/<project>/*.jsonl",
16
+ },
17
+ },
18
+ },
19
+ {
20
+ id: "codex",
21
+ name: "Codex",
22
+ aliases: [],
23
+ source: "codex",
24
+ enrichment: "command",
25
+ sessionImport: {
26
+ cli: true,
27
+ web: {
28
+ extension: ".jsonl",
29
+ accept: ".jsonl",
30
+ fileLabel: "Codex rollout JSONL",
31
+ location: "~/.codex/sessions/YYYY/MM/DD/rollout-*.jsonl",
32
+ },
33
+ },
34
+ },
35
+ {
36
+ id: "cursor",
37
+ name: "Cursor",
38
+ aliases: [],
39
+ source: "cursor",
40
+ enrichment: "command",
41
+ sessionImport: { cli: true, web: null },
42
+ },
43
+ {
44
+ id: "opencode",
45
+ name: "OpenCode",
46
+ aliases: [],
47
+ source: "opencode",
48
+ enrichment: "plugin",
49
+ sessionImport: {
50
+ cli: true,
51
+ web: {
52
+ extension: ".json",
53
+ accept: ".json,application/json",
54
+ fileLabel: "OpenCode session export JSON",
55
+ location: "opencode export <session-id> --pure > opencode-session.json",
56
+ note: "Use direct file redirection as shown. Do not add --sanitize; Groundtrack removes sensitive records during import.",
57
+ },
58
+ },
9
59
  },
10
- { id: "codex", name: "Codex", aliases: [], source: "codex", enrichment: "command" },
11
- { id: "cursor", name: "Cursor", aliases: [], source: "cursor", enrichment: "command" },
12
- { id: "opencode", name: "OpenCode", aliases: [], source: "opencode", enrichment: "plugin" },
13
60
  ];
14
61
  export const AGENT_IDS = AGENTS.map((agent) => agent.id);
62
+ export const SESSION_IMPORT_AGENTS = AGENTS.filter((agent) => agent.sessionImport.cli);
63
+ export const WEB_SESSION_IMPORT_AGENTS = SESSION_IMPORT_AGENTS.filter((agent) => agent.sessionImport.web !== null);
64
+ export const WEB_SESSION_IMPORT_SOURCES = WEB_SESSION_IMPORT_AGENTS.map((agent) => agent.source);
65
+ export const DEFAULT_SESSION_IMPORT_AGENT = "codex";
66
+ export const DEFAULT_WEB_SESSION_IMPORT_SOURCE = "codex";
15
67
  export function isAgent(value) {
16
68
  return AGENTS.some((agent) => agent.id === value);
17
69
  }
18
70
  export function resolveAgent(value) {
19
71
  return AGENTS.find((agent) => agent.id === value || agent.aliases.some((alias) => alias === value));
20
72
  }
73
+ export function isSessionImportAgent(value) {
74
+ return SESSION_IMPORT_AGENTS.some((agent) => agent.id === value);
75
+ }
76
+ function isAgentSessionSource(value) {
77
+ return SESSION_IMPORT_AGENTS.some((agent) => agent.source === value);
78
+ }
79
+ export function normalizeAgentSessionSource(value) {
80
+ const source = value === "codex_cli" ? "codex" : value;
81
+ return isAgentSessionSource(source) ? source : undefined;
82
+ }
83
+ export function isWebSessionImportSource(value) {
84
+ return WEB_SESSION_IMPORT_AGENTS.some((agent) => agent.source === value);
85
+ }
86
+ export function resolveSessionImportAgentFromSource(source) {
87
+ return SESSION_IMPORT_AGENTS.find((agent) => agent.source === source);
88
+ }
89
+ export function webSessionImportAgentFromSource(source) {
90
+ const agent = WEB_SESSION_IMPORT_AGENTS.find((candidate) => candidate.source === source);
91
+ if (!agent)
92
+ throw new Error(`Unsupported web session import source: ${source}`);
93
+ return agent;
94
+ }
21
95
  export function agentFromClientName(value) {
22
96
  const normalized = value?.trim().toLowerCase() ?? "";
23
97
  return AGENTS.find((agent) => normalized.includes(agent.id));
File without changes
File without changes
@@ -0,0 +1,33 @@
1
+ export declare const CANONICALIZATION_VERSION = 1;
2
+ export declare const CANONICAL_IMPORT_MAX_BYTES: number;
3
+ export declare const CANONICAL_IMPORT_MAX_RECORD_BYTES: number;
4
+ export declare const CANONICAL_IMPORT_MAX_ENTRIES = 25000;
5
+ export type SessionRecord = Record<string, unknown>;
6
+ export type CanonicalSessionEntry = Readonly<{
7
+ kind: "user_message" | "assistant_message" | "tool_call" | "tool_result";
8
+ occurredAt: string | null;
9
+ payload: Record<string, unknown>;
10
+ }>;
11
+ export type CanonicalSessionMetadata = Readonly<{
12
+ type: "session";
13
+ version: 1;
14
+ externalSessionId: string;
15
+ repositoryUrl: string | null;
16
+ repositoryBranch: string | null;
17
+ repositoryCommitSha: string | null;
18
+ workingDirectory: string | null;
19
+ originator: string | null;
20
+ model: string | null;
21
+ startedAt: string | null;
22
+ endedAt: string | null;
23
+ statistics: {
24
+ totalRecordCount: number;
25
+ importedEntryCount: number;
26
+ skippedRecordCount: number;
27
+ unsupportedRecordCount: number;
28
+ malformedRecordCount: number;
29
+ sensitiveRecordCount: number;
30
+ };
31
+ }>;
32
+ export declare function isSessionRecord(value: unknown): value is SessionRecord;
33
+ export declare function sessionString(value: unknown): string | null;
@@ -0,0 +1,10 @@
1
+ export const CANONICALIZATION_VERSION = 1;
2
+ export const CANONICAL_IMPORT_MAX_BYTES = 50 * 1024 * 1024;
3
+ export const CANONICAL_IMPORT_MAX_RECORD_BYTES = 4 * 1024 * 1024;
4
+ export const CANONICAL_IMPORT_MAX_ENTRIES = 25_000;
5
+ export function isSessionRecord(value) {
6
+ return value !== null && typeof value === "object" && !Array.isArray(value);
7
+ }
8
+ export function sessionString(value) {
9
+ return typeof value === "string" && value.length > 0 ? value : null;
10
+ }
@@ -0,0 +1,20 @@
1
+ import { type CanonicalSessionMetadata, type SessionRecord } from "./canonical.js";
2
+ import { type SessionImportFormat, type SessionRecordNormalization } from "./format.js";
3
+ import type { AgentSessionMetadata } from "./types.js";
4
+ type ClaudeMetadata = Readonly<{
5
+ externalSessionId: string | null;
6
+ timestamp: Date | null;
7
+ cwd: string | null;
8
+ repositoryBranch: string | null;
9
+ model: string | null;
10
+ isSubagent: boolean;
11
+ }>;
12
+ export declare function parseClaudeDiscoveryMetadata(records: readonly SessionRecord[]): AgentSessionMetadata | null;
13
+ export type ClaudeSessionNormalizer = Readonly<{
14
+ normalizeRecord(record: SessionRecord, entryBudget?: number): SessionRecordNormalization;
15
+ canonicalMetadata(): Omit<CanonicalSessionMetadata, "startedAt" | "endedAt" | "statistics"> | null;
16
+ metadata(): ClaudeMetadata;
17
+ }>;
18
+ export declare function createClaudeSessionNormalizer(): ClaudeSessionNormalizer;
19
+ export declare const claudeSessionFormat: SessionImportFormat;
20
+ export {};
@@ -0,0 +1,237 @@
1
+ import { isSessionRecord, sessionString, } from "./canonical.js";
2
+ import { sessionImportFormatIdentity, } from "./format.js";
3
+ import { containsInternalInstructionText, redactCredentialText, sanitizeSessionValue, sanitizeWorkingDirectory, } from "./redaction.js";
4
+ const knownSkippedRecordTypes = new Set([
5
+ "agent-name",
6
+ "ai-title",
7
+ "atis-latch",
8
+ "attachment",
9
+ "cost-state",
10
+ "file-history-delta",
11
+ "file-history-snapshot",
12
+ "last-prompt",
13
+ "mode",
14
+ "permission-mode",
15
+ "pr-link",
16
+ "progress",
17
+ "queue-operation",
18
+ "summary",
19
+ ]);
20
+ function validDate(value) {
21
+ const text = sessionString(value);
22
+ if (!text)
23
+ return null;
24
+ const date = new Date(text);
25
+ return Number.isNaN(date.getTime()) ? null : date;
26
+ }
27
+ function structuredValue(value) {
28
+ if (typeof value !== "string")
29
+ return sanitizeSessionValue(value);
30
+ try {
31
+ return sanitizeSessionValue(JSON.parse(value));
32
+ }
33
+ catch {
34
+ return redactCredentialText(value);
35
+ }
36
+ }
37
+ function toolOutput(value) {
38
+ if (!Array.isArray(value))
39
+ return { output: structuredValue(value), unsupported: false };
40
+ const output = [];
41
+ let unsupported = false;
42
+ for (const part of value) {
43
+ if (!isSessionRecord(part)) {
44
+ unsupported = true;
45
+ continue;
46
+ }
47
+ const text = sessionString(part.text);
48
+ if (!text) {
49
+ unsupported = true;
50
+ continue;
51
+ }
52
+ output.push({
53
+ type: sessionString(part.type) ?? "text",
54
+ text: redactCredentialText(text),
55
+ });
56
+ }
57
+ return { output, unsupported };
58
+ }
59
+ function normalizeMessage(record, entryBudget = Number.POSITIVE_INFINITY) {
60
+ const type = sessionString(record.type);
61
+ if (type !== "user" && type !== "assistant")
62
+ return null;
63
+ const message = isSessionRecord(record.message) ? record.message : null;
64
+ if (!message || sessionString(message.role) !== type)
65
+ return null;
66
+ const occurredAt = validDate(record.timestamp)?.toISOString() ?? null;
67
+ const content = Array.isArray(message.content) ? message.content : [message.content];
68
+ const entries = [];
69
+ let sensitive = false;
70
+ let unsupported = false;
71
+ for (const block of content) {
72
+ if (entries.length > entryBudget)
73
+ break;
74
+ if (typeof block === "string") {
75
+ if (block && containsInternalInstructionText(block))
76
+ sensitive = true;
77
+ else if (block)
78
+ entries.push({
79
+ kind: type === "user" ? "user_message" : "assistant_message",
80
+ occurredAt,
81
+ payload: { text: redactCredentialText(block) },
82
+ });
83
+ continue;
84
+ }
85
+ if (!isSessionRecord(block)) {
86
+ if (block != null)
87
+ unsupported = true;
88
+ continue;
89
+ }
90
+ const blockType = sessionString(block.type);
91
+ if (blockType === "thinking" || blockType === "redacted_thinking") {
92
+ sensitive = true;
93
+ continue;
94
+ }
95
+ if (blockType === "text") {
96
+ const text = sessionString(block.text);
97
+ if (text && containsInternalInstructionText(text))
98
+ sensitive = true;
99
+ else if (text)
100
+ entries.push({
101
+ kind: type === "user" ? "user_message" : "assistant_message",
102
+ occurredAt,
103
+ payload: { text: redactCredentialText(text) },
104
+ });
105
+ continue;
106
+ }
107
+ if (type === "assistant" && blockType === "tool_use") {
108
+ const toolName = sessionString(block.name);
109
+ if (toolName)
110
+ entries.push({
111
+ kind: "tool_call",
112
+ occurredAt,
113
+ payload: {
114
+ callId: sessionString(block.id),
115
+ toolName,
116
+ input: structuredValue(block.input),
117
+ },
118
+ });
119
+ else
120
+ unsupported = true;
121
+ continue;
122
+ }
123
+ if (type === "user" && blockType === "tool_result") {
124
+ const result = toolOutput(block.content);
125
+ unsupported ||= result.unsupported;
126
+ entries.push({
127
+ kind: "tool_result",
128
+ occurredAt,
129
+ payload: {
130
+ callId: sessionString(block.tool_use_id),
131
+ output: result.output,
132
+ ...(typeof block.is_error === "boolean" ? { isError: block.is_error } : {}),
133
+ },
134
+ });
135
+ }
136
+ else
137
+ unsupported = true;
138
+ }
139
+ const overflowed = entries.length > entryBudget;
140
+ return {
141
+ entries: overflowed ? entries.slice(0, entryBudget) : entries,
142
+ sensitive,
143
+ unsupported,
144
+ overflowed,
145
+ };
146
+ }
147
+ function emptyMetadata() {
148
+ return {
149
+ externalSessionId: null,
150
+ timestamp: null,
151
+ cwd: null,
152
+ repositoryBranch: null,
153
+ model: null,
154
+ isSubagent: false,
155
+ };
156
+ }
157
+ function mergeMetadata(metadata, record) {
158
+ metadata.externalSessionId ??=
159
+ sessionString(record.sessionId) ?? sessionString(record.session_id);
160
+ metadata.timestamp ??= validDate(record.timestamp);
161
+ metadata.cwd ??= sessionString(record.cwd);
162
+ metadata.repositoryBranch ??= sessionString(record.gitBranch);
163
+ metadata.isSubagent ||= record.isSidechain === true || sessionString(record.agentId) !== null;
164
+ if (record.type === "assistant" && isSessionRecord(record.message))
165
+ metadata.model ??= sessionString(record.message.model);
166
+ }
167
+ function collectMetadata(records) {
168
+ const metadata = emptyMetadata();
169
+ for (const record of records)
170
+ mergeMetadata(metadata, record);
171
+ return metadata;
172
+ }
173
+ export function parseClaudeDiscoveryMetadata(records) {
174
+ const metadata = collectMetadata(records);
175
+ if (!metadata.externalSessionId)
176
+ return null;
177
+ return {
178
+ externalSessionId: metadata.externalSessionId,
179
+ timestamp: metadata.timestamp,
180
+ cwd: metadata.cwd,
181
+ repositoryUrl: null,
182
+ repositoryBranch: metadata.repositoryBranch,
183
+ repositoryCommitSha: null,
184
+ originator: null,
185
+ isSubagent: metadata.isSubagent,
186
+ };
187
+ }
188
+ export function createClaudeSessionNormalizer() {
189
+ const metadata = emptyMetadata();
190
+ return {
191
+ normalizeRecord: (record, entryBudget) => {
192
+ mergeMetadata(metadata, record);
193
+ if (record.type === "system" || record.isMeta === true)
194
+ return { entries: [], sensitive: true, unsupported: false };
195
+ const type = sessionString(record.type);
196
+ if (type !== "user" && type !== "assistant")
197
+ return {
198
+ entries: [],
199
+ sensitive: false,
200
+ unsupported: !type || !knownSkippedRecordTypes.has(type),
201
+ };
202
+ return (normalizeMessage(record, entryBudget) ?? {
203
+ entries: [],
204
+ sensitive: false,
205
+ unsupported: true,
206
+ });
207
+ },
208
+ canonicalMetadata: () => {
209
+ if (!metadata.externalSessionId)
210
+ return null;
211
+ return {
212
+ type: "session",
213
+ version: 1,
214
+ externalSessionId: metadata.externalSessionId,
215
+ repositoryUrl: null,
216
+ repositoryBranch: metadata.repositoryBranch
217
+ ? redactCredentialText(metadata.repositoryBranch)
218
+ : null,
219
+ repositoryCommitSha: null,
220
+ workingDirectory: sanitizeWorkingDirectory(metadata.cwd),
221
+ originator: null,
222
+ model: metadata.model ? redactCredentialText(metadata.model) : null,
223
+ };
224
+ },
225
+ metadata: () => ({ ...metadata }),
226
+ };
227
+ }
228
+ export const claudeSessionFormat = {
229
+ ...sessionImportFormatIdentity("claude"),
230
+ discovery: "files",
231
+ homeEnvironmentVariable: "CLAUDE_CONFIG_DIR",
232
+ defaultHomeDirectory: ".claude",
233
+ discoveryRecordLimit: 16,
234
+ sessionRoots: (home) => [`${home}/projects`],
235
+ parseDiscoveryMetadata: parseClaudeDiscoveryMetadata,
236
+ createNormalizer: createClaudeSessionNormalizer,
237
+ };
@@ -1,6 +1,7 @@
1
- import type { ImportVisibility } from "./types.js";
2
- export declare const IMPORT_USAGE = "Usage:\n groundtrack import [--agent codex] [--repo PATH | --all-repos] [--since DATE]\n [--include-subagents] [--include-unscoped]\n [--visibility private|organization] [--dry-run] [--yes] [--json]\n\nImport options:\n --agent codex Session format to discover (default: codex)\n --repo PATH Import sessions for this Git repository (default: current repository)\n --all-repos Import sessions carrying repository metadata\n --include-unscoped With --all-repos, also import sessions without repository metadata\n --since DATE Import sessions at or after an ISO timestamp or local YYYY-MM-DD\n --include-subagents Include subagent, forked-agent, and guardian sessions\n --visibility VALUE private (default) or organization\n --api-key KEY API key; prefer the GROUNDTRACK_API_KEY environment variable\n --url URL Groundtrack app or API URL (default: https://app.groundtrack.dev)\n --dry-run Discover and prune without authentication or network calls\n --yes, -y Upload without another confirmation\n --json Emit exactly one machine-readable result object\n";
1
+ import type { ImportAgent, ImportVisibility } from "./types.js";
2
+ export declare const IMPORT_USAGE: string;
3
3
  type ImportArguments = Readonly<{
4
+ agent: ImportAgent;
4
5
  repoDir?: string;
5
6
  homeDir?: string;
6
7
  allRepositories: boolean;