@groundtrack/hooks 0.1.8 → 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.
- package/README.md +42 -4
- package/dist/agents.d.ts +55 -0
- package/dist/agents.js +77 -3
- package/dist/groundtrack.js +5 -2
- package/dist/session-import/canonical.d.ts +33 -0
- package/dist/session-import/canonical.js +10 -0
- package/dist/session-import/claude.d.ts +20 -0
- package/dist/session-import/claude.js +237 -0
- package/dist/session-import/cli.d.ts +22 -0
- package/dist/session-import/cli.js +365 -0
- package/dist/session-import/client.d.ts +14 -0
- package/dist/session-import/client.js +74 -0
- package/dist/session-import/codex.d.ts +10 -0
- package/dist/session-import/codex.js +187 -0
- package/dist/session-import/cursor.d.ts +13 -0
- package/dist/session-import/cursor.js +268 -0
- package/dist/session-import/discovery.d.ts +31 -0
- package/dist/session-import/discovery.js +241 -0
- package/dist/session-import/format.d.ts +51 -0
- package/dist/session-import/format.js +11 -0
- package/dist/session-import/formats.d.ts +4 -0
- package/dist/session-import/formats.js +13 -0
- package/dist/session-import/normalize.d.ts +21 -0
- package/dist/session-import/normalize.js +96 -0
- package/dist/session-import/opencode-cli.d.ts +14 -0
- package/dist/session-import/opencode-cli.js +178 -0
- package/dist/session-import/opencode-normalizer.d.ts +8 -0
- package/dist/session-import/opencode-normalizer.js +230 -0
- package/dist/session-import/opencode.d.ts +8 -0
- package/dist/session-import/opencode.js +230 -0
- package/dist/session-import/prune.d.ts +14 -0
- package/dist/session-import/prune.js +86 -0
- package/dist/session-import/receipts.d.ts +21 -0
- package/dist/session-import/receipts.js +44 -0
- package/dist/session-import/redaction.d.ts +6 -0
- package/dist/session-import/redaction.js +61 -0
- package/dist/session-import/types.d.ts +42 -0
- package/dist/session-import/types.js +1 -0
- package/dist/setup/agents.d.ts +32 -0
- package/dist/setup/cli.js +1 -0
- package/package.json +43 -1
- package/src/agents.ts +104 -3
package/README.md
CHANGED
|
@@ -87,16 +87,54 @@ 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 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.
|
|
102
|
+
|
|
103
|
+
```sh
|
|
104
|
+
GROUNDTRACK_API_KEY=... groundtrack import --dry-run
|
|
105
|
+
GROUNDTRACK_API_KEY=... groundtrack import --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
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
The command streams source files through a deterministic privacy filter before upload.
|
|
115
|
+
Reasoning, system/developer instructions, turn context other than the model name, and
|
|
116
|
+
recognized credentials are removed locally. Uploads are sequential and retry only
|
|
117
|
+
transient failures. Successful imports are recorded atomically under
|
|
118
|
+
`$XDG_CONFIG_HOME/groundtrack`; receipts are scoped to the endpoint, API-key fingerprint,
|
|
119
|
+
visibility, source hash, and canonicalization version. Use `--json` for one machine-readable
|
|
120
|
+
result object and `--url` for a preview or self-hosted deployment.
|
|
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
|
+
|
|
90
127
|
`templates/AGENTS.md` is the source of truth for the managed repository policy.
|
|
91
128
|
After changing it, run `pnpm sync:agent-instructions`; tests reject drift between
|
|
92
129
|
the setup guide, this repository's managed `AGENTS.md` block, and the packaged template.
|
|
93
130
|
|
|
94
131
|
## Development
|
|
95
132
|
|
|
96
|
-
Agent IDs, names, aliases, event sources,
|
|
97
|
-
`src/agents.ts`, exported as
|
|
98
|
-
|
|
99
|
-
|
|
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.
|
|
100
138
|
|
|
101
139
|
Build and test the CLI from the repository:
|
|
102
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));
|
package/dist/groundtrack.js
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { runCli } from "./setup/cli.js";
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
import { runImportCli } from "./session-import/cli.js";
|
|
4
|
+
const argv = process.argv.slice(2);
|
|
5
|
+
const operation = argv[0] === "import" ? runImportCli(argv.slice(1)) : runCli(argv);
|
|
6
|
+
operation.catch((error) => {
|
|
7
|
+
const message = error instanceof Error ? error.message : "Groundtrack command failed.";
|
|
5
8
|
if (process.argv.includes("--json"))
|
|
6
9
|
process.stdout.write(`${JSON.stringify({ error: message, applied: false })}\n`);
|
|
7
10
|
else
|
|
@@ -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
|
+
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { ImportAgent, ImportVisibility } from "./types.js";
|
|
2
|
+
export declare const IMPORT_USAGE: string;
|
|
3
|
+
type ImportArguments = Readonly<{
|
|
4
|
+
agent: ImportAgent;
|
|
5
|
+
repoDir?: string;
|
|
6
|
+
homeDir?: string;
|
|
7
|
+
allRepositories: boolean;
|
|
8
|
+
includeUnscoped: boolean;
|
|
9
|
+
includeSubagents: boolean;
|
|
10
|
+
visibility: ImportVisibility;
|
|
11
|
+
since: Date | null;
|
|
12
|
+
dryRun: boolean;
|
|
13
|
+
yes: boolean;
|
|
14
|
+
json: boolean;
|
|
15
|
+
apiKey?: string;
|
|
16
|
+
apiBaseUrl: string;
|
|
17
|
+
help: boolean;
|
|
18
|
+
}>;
|
|
19
|
+
export declare function normalizeApiBaseUrl(value: string): string;
|
|
20
|
+
export declare function parseImportArguments(argv: readonly string[]): ImportArguments;
|
|
21
|
+
export declare function runImportCli(argv: readonly string[]): Promise<void>;
|
|
22
|
+
export {};
|