@groundtrack/hooks 0.1.14 → 0.1.16

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.
@@ -13,6 +13,7 @@ type ImportArguments = Readonly<{
13
13
  yes: boolean;
14
14
  json: boolean;
15
15
  apiKey?: string;
16
+ receiptScope?: string;
16
17
  apiBaseUrl: string;
17
18
  help: boolean;
18
19
  }>;
@@ -24,7 +24,8 @@ Import options:
24
24
  --since DATE Import sessions at or after an ISO timestamp or local YYYY-MM-DD
25
25
  --include-subagents Include subagent, forked-agent, and guardian sessions
26
26
  --visibility VALUE private (default) or organization
27
- --api-key KEY API key; prefer the GROUNDTRACK_API_KEY environment variable
27
+ --api-key KEY API key; prefer GROUNDTRACK_IMPORT_KEY or GROUNDTRACK_API_KEY
28
+ --receipt-scope VALUE Stable receipt scope from create_session_import_key for rotating keys
28
29
  --url URL Groundtrack app or API URL (default: https://app.groundtrack.dev)
29
30
  --dry-run Discover and prune without authentication or network calls
30
31
  --yes, -y Upload without another confirmation
@@ -68,6 +69,7 @@ export function parseImportArguments(argv) {
68
69
  let yes = false;
69
70
  let json = false;
70
71
  let apiKey;
72
+ let receiptScope;
71
73
  let apiBaseUrl = normalizeApiBaseUrl("https://app.groundtrack.dev");
72
74
  let help = false;
73
75
  for (let index = 0; index < args.length; index += 1) {
@@ -98,6 +100,8 @@ export function parseImportArguments(argv) {
98
100
  since = parseDate(valueAfter(index++, arg));
99
101
  else if (arg === "--api-key")
100
102
  apiKey = valueAfter(index++, arg);
103
+ else if (arg === "--receipt-scope")
104
+ receiptScope = valueAfter(index++, arg);
101
105
  else if (arg === "--url")
102
106
  apiBaseUrl = normalizeApiBaseUrl(valueAfter(index++, arg));
103
107
  else if (arg === "--dry-run")
@@ -128,6 +132,7 @@ export function parseImportArguments(argv) {
128
132
  yes,
129
133
  json,
130
134
  apiKey,
135
+ receiptScope,
131
136
  apiBaseUrl,
132
137
  help,
133
138
  };
@@ -216,7 +221,12 @@ async function runAgentImport(args, agent, context, interactive) {
216
221
  workingDirectory: context.repoDir,
217
222
  });
218
223
  try {
219
- const apiKey = args.apiKey ?? context.env.GROUNDTRACK_API_KEY;
224
+ const apiKey = args.apiKey ?? context.env.GROUNDTRACK_IMPORT_KEY ?? context.env.GROUNDTRACK_API_KEY;
225
+ const providedReceiptScope = args.receiptScope ?? context.env.GROUNDTRACK_IMPORT_RECEIPT_SCOPE;
226
+ if (providedReceiptScope && !/^[a-f0-9]{64}$/.test(providedReceiptScope))
227
+ throw new Error("The import receipt scope must be a 64-character lowercase hex value.");
228
+ // A leftover scope from another import must not skip uploads with a legacy key.
229
+ const stableReceiptScope = apiKey?.startsWith("gti_") ? providedReceiptScope : undefined;
220
230
  const receipts = apiKey ? readReceipts(context) : { version: 1, receipts: {} };
221
231
  const prepared = [];
222
232
  const preparationErrors = [...discovery.failed];
@@ -232,6 +242,7 @@ async function runAgentImport(args, agent, context, interactive) {
232
242
  ? receiptKey({
233
243
  apiBaseUrl: args.apiBaseUrl,
234
244
  apiKey,
245
+ receiptScope: stableReceiptScope,
235
246
  source: format.source,
236
247
  sourceContentHash: pruned.sourceContentHash,
237
248
  visibility: args.visibility,
@@ -290,7 +301,7 @@ async function runAgentImport(args, agent, context, interactive) {
290
301
  failed: preparationErrors.length,
291
302
  });
292
303
  if (!apiKey)
293
- throw new Error("Set GROUNDTRACK_API_KEY or pass --api-key before uploading.");
304
+ throw new Error("Set GROUNDTRACK_IMPORT_KEY or GROUNDTRACK_API_KEY before uploading.");
294
305
  if (!args.yes && !interactive)
295
306
  throw new Error("Import needs confirmation. Use --yes, or run it in an interactive terminal.");
296
307
  if (!args.yes) {
@@ -178,6 +178,7 @@ export function discoverSessions(options) {
178
178
  return null;
179
179
  return identity.remotes.values().next().value ?? null;
180
180
  };
181
+ const targetRemote = soleRemote(options.repository);
181
182
  for (const candidate of discovered.included) {
182
183
  const { path, metadata } = candidate;
183
184
  const modifiedAtMs = candidate.modifiedAt?.getTime() ??
@@ -202,11 +203,16 @@ export function discoverSessions(options) {
202
203
  const normalizedRemote = metadata.repositoryUrl
203
204
  ? normalizeRepositoryUrl(metadata.repositoryUrl)
204
205
  : null;
205
- const cwdMatchesCurrentRepository = Boolean(options.repository && metadata.cwd && isInside(metadata.cwd, options.repository.root));
206
- const currentRepositoryRemote = cwdMatchesCurrentRepository
207
- ? soleRemote(options.repository)
208
- : null;
206
+ const pathMatchesCurrentRepository = Boolean(options.repository && metadata.cwd && isInside(metadata.cwd, options.repository.root));
209
207
  const remoteMatchesCurrentRepository = Boolean(options.repository && normalizedRemote && options.repository.remotes.has(normalizedRemote));
208
+ // A session may retain an old remote after an owner rename. Its recorded checkout
209
+ // can still identify the target repository even when importing from another worktree.
210
+ const checkoutMatchesCurrentRepository = Boolean(targetRemote &&
211
+ !pathMatchesCurrentRepository &&
212
+ !remoteMatchesCurrentRepository &&
213
+ soleRemote(repositoryIdentityForCwd(metadata.cwd)) === targetRemote);
214
+ const cwdMatchesCurrentRepository = pathMatchesCurrentRepository || checkoutMatchesCurrentRepository;
215
+ const currentRepositoryRemote = cwdMatchesCurrentRepository ? targetRemote : null;
210
216
  const matchesCurrentRepository = options.repository
211
217
  ? remoteMatchesCurrentRepository ||
212
218
  (!normalizedRemote && cwdMatchesCurrentRepository) ||
@@ -13,6 +13,7 @@ export declare function readReceipts(context: SetupEnvironment): ReceiptFile;
13
13
  export declare function receiptKey(input: {
14
14
  apiBaseUrl: string;
15
15
  apiKey: string;
16
+ receiptScope?: string;
16
17
  source: AgentSessionSource;
17
18
  sourceContentHash: string;
18
19
  visibility: ImportVisibility;
@@ -30,7 +30,9 @@ export function receiptKey(input) {
30
30
  return createHash("sha256")
31
31
  .update([
32
32
  input.apiBaseUrl,
33
- createHash("sha256").update(input.apiKey).digest("hex"),
33
+ input.receiptScope
34
+ ? `scoped:${input.receiptScope}`
35
+ : createHash("sha256").update(input.apiKey).digest("hex"),
34
36
  input.source,
35
37
  CANONICALIZATION_VERSION,
36
38
  input.visibility,
@@ -16,12 +16,13 @@ function isSensitiveKey(value) {
16
16
  return sensitiveKeyPattern.test(value.replace(/([a-z0-9])([A-Z])/g, "$1_$2"));
17
17
  }
18
18
  export function redactCredentialText(value) {
19
+ // Server persistence and CLI normalization both use this redactor.
19
20
  return value
20
21
  .replace(/-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z ]*PRIVATE KEY-----/g, "[REDACTED PRIVATE KEY]")
21
22
  .replace(/\b(authorization\s*:\s*(?:bearer|basic)\s+)[^\s,;]+/gi, "$1[REDACTED]")
22
23
  .replace(/\b((?:[a-z0-9_-]*(?:api[_-]?key|access[_-]?key(?:[_-]?id)?|token|password|passwd|secret|private[_-]?key|credentials?|database[_-]?url))\s*[=:]\s*)[^\s,;]+/gi, "$1[REDACTED]")
23
24
  .replace(/(?:^|\s)((?:--)?(?:api-key|token|password|secret)\s+)[^\s,;]+/gi, "$1$2[REDACTED]")
24
- .replace(/\b(?:sk-[A-Za-z0-9_-]{16,}|ghp_[A-Za-z0-9]{8,}|github_pat_[A-Za-z0-9_]{8,}|glpat-[A-Za-z0-9_-]{12,}|npm_[A-Za-z0-9]{20,}|xox[baprs]-[A-Za-z0-9-]{12,}|(?:AKIA|ASIA)[A-Z0-9]{16}|AIza[A-Za-z0-9_-]{20,})\b/g, "[REDACTED]")
25
+ .replace(/\b(?:gti_[A-Za-z]{32,}|sk-[A-Za-z0-9_-]{16,}|ghp_[A-Za-z0-9]{8,}|github_pat_[A-Za-z0-9_]{8,}|glpat-[A-Za-z0-9_-]{12,}|npm_[A-Za-z0-9]{20,}|xox[baprs]-[A-Za-z0-9-]{12,}|(?:AKIA|ASIA)[A-Z0-9]{16}|AIza[A-Za-z0-9_-]{20,})\b/g, "[REDACTED]")
25
26
  .replace(/\beyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\b/g, "[REDACTED]")
26
27
  .replace(/\b([a-z][a-z0-9+.-]*:\/\/)[^\s/:@]+:[^\s/@]+@/gi, "$1[REDACTED]@");
27
28
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@groundtrack/hooks",
3
- "version": "0.1.14",
3
+ "version": "0.1.16",
4
4
  "description": "Lifecycle hook utilities for Groundtrack MCP clients.",
5
5
  "keywords": [
6
6
  "claude-code",