sidekick-shared 0.28.3 → 0.29.0

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 (97) hide show
  1. package/CHANGELOG.md +64 -0
  2. package/README.md +149 -19
  3. package/dist/authStatus.d.ts +54 -0
  4. package/dist/authStatus.js +91 -0
  5. package/dist/browser.d.ts +12 -6
  6. package/dist/browser.js +17 -1
  7. package/dist/codex.d.ts +23 -0
  8. package/dist/codex.js +44 -0
  9. package/dist/codexProfiles.d.ts +1 -0
  10. package/dist/codexProfiles.js +8 -11
  11. package/dist/esm/authStatus.js +87 -0
  12. package/dist/esm/browser.js +4 -2
  13. package/dist/esm/codex.js +18 -0
  14. package/dist/esm/codexProfiles.js +8 -11
  15. package/dist/esm/formatting.js +10 -1
  16. package/dist/esm/index.js +9 -6
  17. package/dist/esm/modelCatalog.js +2 -1
  18. package/dist/esm/modelInfo.js +422 -45
  19. package/dist/esm/node.js +9 -3
  20. package/dist/esm/parsers/codexParser.js +175 -54
  21. package/dist/esm/parsers/codexRolloutOrigin.js +107 -0
  22. package/dist/esm/parsers/openCodeParser.js +83 -21
  23. package/dist/esm/pricing.js +18 -0
  24. package/dist/esm/pricingCatalog.js +14 -7
  25. package/dist/esm/promptHistory.js +66 -4
  26. package/dist/esm/providerFailure.js +124 -3
  27. package/dist/esm/providers/codex.js +2 -24
  28. package/dist/esm/providers/codexDatabase.js +3 -19
  29. package/dist/esm/providers/codexRollouts.js +24 -1
  30. package/dist/esm/providers/openCode.js +9 -17
  31. package/dist/esm/report/htmlReportGenerator.js +42 -5
  32. package/dist/esm/report/transcriptParser.js +13 -11
  33. package/dist/esm/schemas/providerFailure.js +3 -1
  34. package/dist/esm/schemas/transcript.js +4 -0
  35. package/dist/esm/sessionPromptHistory.js +2 -2
  36. package/dist/esm/statusline/payload.js +9 -0
  37. package/dist/esm/testing/credentialGuard.js +87 -5
  38. package/dist/esm/toolCallOrigins.js +310 -0
  39. package/dist/esm/transcript.js +42 -3
  40. package/dist/esm/usage/usageEvents.js +146 -17
  41. package/dist/esm/usage/usageReports.js +39 -6
  42. package/dist/esm/watchers/jsonlTail.js +378 -59
  43. package/dist/formatting.d.ts +26 -1
  44. package/dist/formatting.js +11 -1
  45. package/dist/index.d.ts +16 -10
  46. package/dist/index.js +35 -12
  47. package/dist/modelCatalog.js +2 -1
  48. package/dist/modelInfo.d.ts +185 -16
  49. package/dist/modelInfo.js +431 -47
  50. package/dist/node.d.ts +16 -5
  51. package/dist/node.js +31 -2
  52. package/dist/parsers/codexParser.d.ts +55 -0
  53. package/dist/parsers/codexParser.js +175 -54
  54. package/dist/parsers/codexRolloutOrigin.d.ts +49 -0
  55. package/dist/parsers/codexRolloutOrigin.js +111 -0
  56. package/dist/parsers/openCodeParser.d.ts +38 -0
  57. package/dist/parsers/openCodeParser.js +86 -21
  58. package/dist/pricing.d.ts +22 -0
  59. package/dist/pricing.js +50 -0
  60. package/dist/pricingCatalog.d.ts +10 -0
  61. package/dist/pricingCatalog.js +13 -6
  62. package/dist/promptHistory.d.ts +65 -4
  63. package/dist/promptHistory.js +68 -5
  64. package/dist/providerFailure.d.ts +44 -0
  65. package/dist/providerFailure.js +126 -4
  66. package/dist/providers/codex.d.ts +3 -22
  67. package/dist/providers/codex.js +3 -26
  68. package/dist/providers/codexDatabase.d.ts +3 -2
  69. package/dist/providers/codexDatabase.js +4 -22
  70. package/dist/providers/codexRollouts.d.ts +22 -0
  71. package/dist/providers/codexRollouts.js +24 -0
  72. package/dist/providers/openCode.js +8 -16
  73. package/dist/report/htmlReportGenerator.js +42 -5
  74. package/dist/report/transcriptParser.d.ts +10 -1
  75. package/dist/report/transcriptParser.js +13 -11
  76. package/dist/schemas/providerFailure.d.ts +39 -0
  77. package/dist/schemas/providerFailure.js +2 -0
  78. package/dist/schemas/transcript.d.ts +2 -0
  79. package/dist/schemas/transcript.js +4 -0
  80. package/dist/sessionPromptHistory.d.ts +7 -1
  81. package/dist/sessionPromptHistory.js +1 -1
  82. package/dist/statusline/payload.d.ts +10 -0
  83. package/dist/statusline/payload.js +14 -0
  84. package/dist/testing/credentialGuard.d.ts +14 -0
  85. package/dist/testing/credentialGuard.js +87 -5
  86. package/dist/toolCallOrigins.d.ts +70 -0
  87. package/dist/toolCallOrigins.js +346 -0
  88. package/dist/transcript.d.ts +12 -0
  89. package/dist/transcript.js +42 -3
  90. package/dist/types/opencode.d.ts +2 -0
  91. package/dist/usage/usageEvents.d.ts +54 -6
  92. package/dist/usage/usageEvents.js +146 -17
  93. package/dist/usage/usageReports.d.ts +35 -4
  94. package/dist/usage/usageReports.js +39 -6
  95. package/dist/watchers/jsonlTail.d.ts +101 -4
  96. package/dist/watchers/jsonlTail.js +380 -60
  97. package/package.json +41 -1
@@ -599,14 +599,51 @@ function renderContentBlock(block, includeThinking, includeToolDetail) {
599
599
  return '';
600
600
  }
601
601
  }
602
- function renderToolUse(block, includeDetail) {
602
+ /** Prefix of the bookkeeping keys session parsers stash inside tool inputs. */
603
+ const SIDEKICK_INPUT_KEY_PREFIX = '_sidekick';
604
+ /**
605
+ * The tool's display identity and user-facing input. Entries built outside
606
+ * the canonical projection may still carry the parsers' `_sidekick*` keys in
607
+ * `toolInput`; those are hidden here and read as the raw identity.
608
+ */
609
+ function toolDisplay(block) {
603
610
  const name = block.toolName || 'unknown';
611
+ const raw = block.toolInput;
612
+ let input = raw;
613
+ let rawToolName = block.rawToolName;
614
+ let mcpServerName = block.mcpServerName;
615
+ if (raw && Object.keys(raw).some((key) => key.startsWith(SIDEKICK_INPUT_KEY_PREFIX))) {
616
+ const clean = {};
617
+ for (const [key, value] of Object.entries(raw)) {
618
+ if (!key.startsWith(SIDEKICK_INPUT_KEY_PREFIX))
619
+ clean[key] = value;
620
+ }
621
+ input = clean;
622
+ if (!rawToolName && typeof raw._sidekickRawToolName === 'string') {
623
+ rawToolName = raw._sidekickRawToolName;
624
+ }
625
+ if (!mcpServerName && typeof raw._sidekickMcpServerName === 'string') {
626
+ mcpServerName = raw._sidekickMcpServerName;
627
+ }
628
+ }
629
+ const parts = [];
630
+ if (rawToolName && rawToolName.trim() && rawToolName !== name)
631
+ parts.push(rawToolName);
632
+ if (mcpServerName && mcpServerName.trim())
633
+ parts.push(`MCP: ${mcpServerName}`);
634
+ const hint = parts.length
635
+ ? ` <span class="tool-raw-name" style="color:var(--text-muted);font-size:11px">${(0, htmlHelpers_1.escapeHtml)(parts.join(' · '))}</span>`
636
+ : '';
637
+ return { name, hint, input };
638
+ }
639
+ function renderToolUse(block, includeDetail) {
640
+ const { name, hint, input } = toolDisplay(block);
604
641
  if (!includeDetail) {
605
- return `<div class="tool-block" data-block-type="tool"><div style="padding:8px 12px;background:rgba(245,166,35,0.08);color:var(--accent-orange);font-family:monospace;font-size:13px;">${(0, htmlHelpers_1.escapeHtml)(name)}</div></div>`;
642
+ return `<div class="tool-block" data-block-type="tool"><div style="padding:8px 12px;background:rgba(245,166,35,0.08);color:var(--accent-orange);font-family:monospace;font-size:13px;">${(0, htmlHelpers_1.escapeHtml)(name)}${hint}</div></div>`;
606
643
  }
607
644
  let inputHtml = '';
608
- if (block.toolInput && Object.keys(block.toolInput).length > 0) {
609
- const inputJson = JSON.stringify(block.toolInput, null, 2);
645
+ if (input && Object.keys(input).length > 0) {
646
+ const inputJson = JSON.stringify(input, null, 2);
610
647
  const highlighted = (0, htmlHelpers_1.highlightCodeBlock)(inputJson, 'json');
611
648
  inputHtml = `<div class="tool-io">
612
649
  <div class="tool-label">Input</div>
@@ -614,7 +651,7 @@ function renderToolUse(block, includeDetail) {
614
651
  </div>`;
615
652
  }
616
653
  return `<details class="tool-block" data-block-type="tool">
617
- <summary>${(0, htmlHelpers_1.escapeHtml)(name)}${block.toolUseId ? ` <span style="color:var(--text-muted);font-size:11px">${(0, htmlHelpers_1.escapeHtml)(block.toolUseId)}</span>` : ''}</summary>
654
+ <summary>${(0, htmlHelpers_1.escapeHtml)(name)}${hint}${block.toolUseId ? ` <span style="color:var(--text-muted);font-size:11px">${(0, htmlHelpers_1.escapeHtml)(block.toolUseId)}</span>` : ''}</summary>
618
655
  <div class="tool-content">
619
656
  ${inputHtml}
620
657
  </div>
@@ -1,6 +1,15 @@
1
1
  /** Report compatibility adapters over the canonical transcript projection. */
2
2
  import type { SessionEvent } from '../types/sessionEvent';
3
- import type { TranscriptEntry } from './types';
3
+ import type { TranscriptEntry, TranscriptContentBlock } from './types';
4
+ /**
5
+ * A report tool block that also names the provider's raw tool and MCP
6
+ * server. Internal: the extra fields are optional and read only by the HTML
7
+ * renderer.
8
+ */
9
+ export type ReportToolUseBlock = TranscriptContentBlock & {
10
+ rawToolName?: string;
11
+ mcpServerName?: string;
12
+ };
4
13
  /** Parse a Claude Code JSONL session through the canonical provider reader. */
5
14
  export declare function parseTranscript(sessionPath: string): TranscriptEntry[];
6
15
  /** Project canonical provider events into the legacy report transcript shape. */
@@ -79,17 +79,19 @@ function toReportBlock(block) {
79
79
  return block.text ? [{ type: 'text', text: block.text }] : [];
80
80
  case 'thinking':
81
81
  return block.text ? [{ type: 'thinking', text: block.text }] : [];
82
- case 'tool_use':
83
- return [
84
- {
85
- type: 'tool_use',
86
- toolName: block.toolName ?? 'unknown',
87
- toolInput: block.input && typeof block.input === 'object'
88
- ? block.input
89
- : {},
90
- toolUseId: block.toolUseId,
91
- },
92
- ];
82
+ case 'tool_use': {
83
+ const toolBlock = {
84
+ type: 'tool_use',
85
+ toolName: block.toolName ?? 'unknown',
86
+ toolInput: block.input && typeof block.input === 'object'
87
+ ? block.input
88
+ : {},
89
+ toolUseId: block.toolUseId,
90
+ ...(block.rawToolName ? { rawToolName: block.rawToolName } : {}),
91
+ ...(block.mcpServerName ? { mcpServerName: block.mcpServerName } : {}),
92
+ };
93
+ return [toolBlock];
94
+ }
93
95
  case 'tool_result':
94
96
  return [
95
97
  {
@@ -52,6 +52,25 @@ export declare const providerFailureEvidenceSchema: z.ZodObject<{
52
52
  context_overflow: "context_overflow";
53
53
  }>;
54
54
  checkedAt: z.ZodOptional<z.ZodString>;
55
+ codexErrorInfo: z.ZodOptional<z.ZodEnum<{
56
+ other: "other";
57
+ contextWindowExceeded: "contextWindowExceeded";
58
+ sessionBudgetExceeded: "sessionBudgetExceeded";
59
+ usageLimitExceeded: "usageLimitExceeded";
60
+ rateLimitExceeded: "rateLimitExceeded";
61
+ serverOverloaded: "serverOverloaded";
62
+ cyberPolicy: "cyberPolicy";
63
+ httpConnectionFailed: "httpConnectionFailed";
64
+ responseStreamConnectionFailed: "responseStreamConnectionFailed";
65
+ internalServerError: "internalServerError";
66
+ unauthorized: "unauthorized";
67
+ badRequest: "badRequest";
68
+ threadRollbackFailed: "threadRollbackFailed";
69
+ sandboxError: "sandboxError";
70
+ responseStreamDisconnected: "responseStreamDisconnected";
71
+ responseTooManyFailedAttempts: "responseTooManyFailedAttempts";
72
+ activeTurnNotSteerable: "activeTurnNotSteerable";
73
+ }>>;
55
74
  }, z.core.$strip>;
56
75
  export declare const providerRetryAfterSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
57
76
  kind: z.ZodLiteral<"delay">;
@@ -98,6 +117,7 @@ export declare const providerFailureDiagnosisSchema: z.ZodObject<{
98
117
  reduce_context: "reduce_context";
99
118
  inspect_error: "inspect_error";
100
119
  }>;
120
+ retryable: z.ZodOptional<z.ZodBoolean>;
101
121
  evidence: z.ZodArray<z.ZodObject<{
102
122
  source: z.ZodEnum<{
103
123
  message: "message";
@@ -122,6 +142,25 @@ export declare const providerFailureDiagnosisSchema: z.ZodObject<{
122
142
  context_overflow: "context_overflow";
123
143
  }>;
124
144
  checkedAt: z.ZodOptional<z.ZodString>;
145
+ codexErrorInfo: z.ZodOptional<z.ZodEnum<{
146
+ other: "other";
147
+ contextWindowExceeded: "contextWindowExceeded";
148
+ sessionBudgetExceeded: "sessionBudgetExceeded";
149
+ usageLimitExceeded: "usageLimitExceeded";
150
+ rateLimitExceeded: "rateLimitExceeded";
151
+ serverOverloaded: "serverOverloaded";
152
+ cyberPolicy: "cyberPolicy";
153
+ httpConnectionFailed: "httpConnectionFailed";
154
+ responseStreamConnectionFailed: "responseStreamConnectionFailed";
155
+ internalServerError: "internalServerError";
156
+ unauthorized: "unauthorized";
157
+ badRequest: "badRequest";
158
+ threadRollbackFailed: "threadRollbackFailed";
159
+ sandboxError: "sandboxError";
160
+ responseStreamDisconnected: "responseStreamDisconnected";
161
+ responseTooManyFailedAttempts: "responseTooManyFailedAttempts";
162
+ activeTurnNotSteerable: "activeTurnNotSteerable";
163
+ }>>;
125
164
  }, z.core.$strip>>;
126
165
  authentication: z.ZodOptional<z.ZodObject<{
127
166
  credentialKind: z.ZodEnum<{
@@ -23,6 +23,7 @@ exports.providerFailureEvidenceSchema = zod_1.z.object({
23
23
  source: zod_1.z.enum(providerFailure_1.PROVIDER_FAILURE_EVIDENCE_SOURCES),
24
24
  rule: zod_1.z.enum(providerFailure_1.PROVIDER_FAILURE_CODES),
25
25
  checkedAt: zod_1.z.string().optional(),
26
+ codexErrorInfo: zod_1.z.enum(providerFailure_1.CODEX_ERROR_INFO_KINDS).optional(),
26
27
  });
27
28
  exports.providerRetryAfterSchema = zod_1.z.discriminatedUnion('kind', [
28
29
  zod_1.z.object({ kind: zod_1.z.literal('delay'), delayMs: zod_1.z.number() }),
@@ -33,6 +34,7 @@ exports.providerFailureDiagnosisSchema = zod_1.z.object({
33
34
  credentialKind: zod_1.z.enum(providerFailure_1.PROVIDER_CREDENTIAL_KINDS),
34
35
  diagnosis: zod_1.z.enum(providerFailure_1.PROVIDER_FAILURE_CODES),
35
36
  recovery: zod_1.z.enum(providerFailure_1.PROVIDER_RECOVERY_ACTIONS),
37
+ retryable: zod_1.z.boolean().optional(),
36
38
  evidence: zod_1.z.array(exports.providerFailureEvidenceSchema),
37
39
  authentication: exports.providerAuthenticationEvidenceSchema.optional(),
38
40
  httpStatus: zod_1.z.number().optional(),
@@ -12,6 +12,8 @@ export declare const canonicalTranscriptBlockSchema: z.ZodObject<{
12
12
  }>;
13
13
  text: z.ZodOptional<z.ZodString>;
14
14
  toolName: z.ZodOptional<z.ZodString>;
15
+ rawToolName: z.ZodOptional<z.ZodString>;
16
+ mcpServerName: z.ZodOptional<z.ZodString>;
15
17
  toolUseId: z.ZodOptional<z.ZodString>;
16
18
  input: z.ZodOptional<z.ZodUnknown>;
17
19
  output: z.ZodOptional<z.ZodUnknown>;
@@ -20,6 +20,8 @@ exports.canonicalTranscriptBlockSchema = zod_1.z.object({
20
20
  type: zod_1.z.enum(['text', 'thinking', 'tool_use', 'tool_result', 'image', 'unknown']),
21
21
  text: zod_1.z.string().optional(),
22
22
  toolName: zod_1.z.string().optional(),
23
+ rawToolName: zod_1.z.string().optional(),
24
+ mcpServerName: zod_1.z.string().optional(),
23
25
  toolUseId: zod_1.z.string().optional(),
24
26
  input: zod_1.z.unknown().optional(),
25
27
  output: zod_1.z.unknown().optional(),
@@ -31,6 +33,8 @@ exports.canonicalTranscriptBlockSchema = zod_1.z.object({
31
33
  exports.canonicalToolCallSchema = zod_1.z.object({
32
34
  id: zod_1.z.string().optional(),
33
35
  name: zod_1.z.string(),
36
+ rawToolName: zod_1.z.string().optional(),
37
+ mcpServerName: zod_1.z.string().optional(),
34
38
  timestamp: zod_1.z.string(),
35
39
  input: zod_1.z.unknown().optional(),
36
40
  output: zod_1.z.unknown().optional(),
@@ -28,7 +28,13 @@ export interface SessionPromptHistoryBounds {
28
28
  maxSessionBytes?: number;
29
29
  /** Bytes read across all sessions in one call. Default 256 MiB. */
30
30
  maxTotalBytes?: number;
31
- /** Largest single log record that is parsed. Default 16 MiB. */
31
+ /**
32
+ * Largest single log record that is parsed. Default 16 MiB
33
+ * (`PROMPT_HISTORY_ORDINAL_MAX_RECORD_BYTES`). A prompt inside a longer
34
+ * record is never seen, so later ordinals do not count it: ordinals match
35
+ * `collectPromptHistory()` (and usage events' `promptOrdinal`) only when
36
+ * both use the default.
37
+ */
32
38
  maxRecordBytes?: number;
33
39
  /** Wall-clock budget for the call. Default 60 000 ms. */
34
40
  deadlineMs?: number;
@@ -56,7 +56,7 @@ const DEFAULT_SESSION_BOUNDS = {
56
56
  maxSessions: 1000,
57
57
  maxSessionBytes: 256 * 1024 * 1024,
58
58
  maxTotalBytes: 256 * 1024 * 1024,
59
- maxRecordBytes: promptHistory_1.DEFAULT_BOUNDS.maxRecordBytes,
59
+ maxRecordBytes: promptHistory_1.PROMPT_HISTORY_ORDINAL_MAX_RECORD_BYTES,
60
60
  deadlineMs: 60_000,
61
61
  };
62
62
  const MAX_EXCLUSIONS = 1000;
@@ -0,0 +1,10 @@
1
+ /**
2
+ * `sidekick-shared/statusline/payload` — the pure Claude Code status-line
3
+ * stdin parser on its own, for hosts with a tight bundle budget that render
4
+ * their own line. Loads no account, credential, quota-snapshot, or network
5
+ * code (the full `sidekick-shared/statusline` entry does).
6
+ *
7
+ * @module statusline/payload
8
+ */
9
+ export { parseClaudeStatuslinePayload, quotaFromStatuslinePayload, } from './claudeStatuslinePayload';
10
+ export type { ClaudeStatuslinePayload, ClaudeStatuslineRateLimitWindow, QuotaFromStatuslineOptions, } from './claudeStatuslinePayload';
@@ -0,0 +1,14 @@
1
+ "use strict";
2
+ /**
3
+ * `sidekick-shared/statusline/payload` — the pure Claude Code status-line
4
+ * stdin parser on its own, for hosts with a tight bundle budget that render
5
+ * their own line. Loads no account, credential, quota-snapshot, or network
6
+ * code (the full `sidekick-shared/statusline` entry does).
7
+ *
8
+ * @module statusline/payload
9
+ */
10
+ Object.defineProperty(exports, "__esModule", { value: true });
11
+ exports.quotaFromStatuslinePayload = exports.parseClaudeStatuslinePayload = void 0;
12
+ var claudeStatuslinePayload_1 = require("./claudeStatuslinePayload");
13
+ Object.defineProperty(exports, "parseClaudeStatuslinePayload", { enumerable: true, get: function () { return claudeStatuslinePayload_1.parseClaudeStatuslinePayload; } });
14
+ Object.defineProperty(exports, "quotaFromStatuslinePayload", { enumerable: true, get: function () { return claudeStatuslinePayload_1.quotaFromStatuslinePayload; } });
@@ -15,6 +15,20 @@ export interface CredentialGuardOptions {
15
15
  blockedBinaries?: string[];
16
16
  /** Extra real paths to protect, besides the Claude, Codex, and Sidekick homes. */
17
17
  protectedPaths?: string[];
18
+ /**
19
+ * Home overrides to keep instead of unsetting, for a suite that points one
20
+ * at its own fixture before the guard installs. Accepts only the variables
21
+ * the guard unsets: `CLAUDE_CONFIG_DIR`, `CLAUDE_SECURESTORAGE_CONFIG_DIR`,
22
+ * `CODEX_HOME`, and `SIDEKICK_CONFIG_DIR`.
23
+ *
24
+ * Install throws, before changing anything, for any other name, or when a
25
+ * preserved value is, contains, or lies inside a real protected location:
26
+ * the real home's `.claude`, `.claude.json`, and `.codex`, the default
27
+ * Sidekick config dir, or a `protectedPaths` entry. Symlinks are resolved
28
+ * first. The guard cannot tell a real store kept elsewhere from a fixture,
29
+ * so preserve only paths the suite created. Blank values are kept unchecked.
30
+ */
31
+ preserveEnv?: readonly string[];
18
32
  }
19
33
  /**
20
34
  * Installs the guard once per process and returns it; later calls return the
@@ -46,7 +46,8 @@ exports.installCredentialGuard = installCredentialGuard;
46
46
  * - `HOME` (and the Windows equivalents) point at a per-process temp dir,
47
47
  * removed on exit, and the home overrides (`CLAUDE_CONFIG_DIR`,
48
48
  * `CLAUDE_SECURESTORAGE_CONFIG_DIR`, `CODEX_HOME`, `SIDEKICK_CONFIG_DIR`)
49
- * are unset, so every `os.homedir()`-derived path is isolated structurally;
49
+ * are unset (unless named in `preserveEnv` and pointed at a fixture), so
50
+ * every `os.homedir()`-derived path is isolated structurally;
50
51
  * - the real `child_process` module object is patched, which reaches both ESM
51
52
  * named imports (after `syncBuiltinESMExports`) and CommonJS code loaded
52
53
  * through native `require`; per-file `vi.mock('child_process')` factories
@@ -64,6 +65,13 @@ const os = __importStar(require("node:os"));
64
65
  const path = __importStar(require("node:path"));
65
66
  const GUARD_KEY = '__sidekickCredentialGuard';
66
67
  const DEFAULT_BLOCKED_BINARIES = ['security', 'claude', 'codex'];
68
+ /** Home overrides the guard unsets; only these can be preserved. */
69
+ const SCRUBBED_ENV = [
70
+ 'CLAUDE_CONFIG_DIR',
71
+ 'CLAUDE_SECURESTORAGE_CONFIG_DIR',
72
+ 'CODEX_HOME',
73
+ 'SIDEKICK_CONFIG_DIR',
74
+ ];
67
75
  const CHILD_PROCESS_FUNCTIONS = [
68
76
  'spawn',
69
77
  'spawnSync',
@@ -111,6 +119,8 @@ function installCredentialGuard(options = {}) {
111
119
  // os.userInfo() reads the passwd entry, so it still names the real home
112
120
  // after HOME is redirected below.
113
121
  const realHome = os.userInfo().homedir;
122
+ // Validated before any side effect, so a refused install changes nothing.
123
+ const preserved = checkPreservedEnv(options, realHome, realFs);
114
124
  const testHome = realFs.mkdtempSync(path.join(os.tmpdir(), 'sidekick-test-home-'));
115
125
  const removeTestHome = realFs.rmSync.bind(realFs);
116
126
  process.once('exit', () => {
@@ -139,10 +149,9 @@ function installCredentialGuard(options = {}) {
139
149
  process.env.USERPROFILE = testHome;
140
150
  process.env.APPDATA = path.join(testHome, 'AppData', 'Roaming');
141
151
  process.env.LOCALAPPDATA = path.join(testHome, 'AppData', 'Local');
142
- delete process.env.CLAUDE_CONFIG_DIR;
143
- delete process.env.CLAUDE_SECURESTORAGE_CONFIG_DIR;
144
- delete process.env.CODEX_HOME;
145
- delete process.env.SIDEKICK_CONFIG_DIR;
152
+ for (const name of SCRUBBED_ENV)
153
+ if (!preserved.has(name))
154
+ delete process.env[name];
146
155
  const protectedRoots = path.resolve(realHome) === path.resolve(testHome)
147
156
  ? []
148
157
  : [
@@ -244,6 +253,79 @@ function installCredentialGuard(options = {}) {
244
253
  globalThis[GUARD_KEY] = state;
245
254
  return state;
246
255
  }
256
+ /** The `preserveEnv` names, after refusing unknown names and unsafe values. */
257
+ function checkPreservedEnv(options, realHome, realFs) {
258
+ const preserved = new Set();
259
+ for (const name of options.preserveEnv ?? []) {
260
+ if (!SCRUBBED_ENV.includes(name)) {
261
+ throw new Error(`installCredentialGuard: preserveEnv accepts only ${SCRUBBED_ENV.join(', ')}; got ${JSON.stringify(name)}.`);
262
+ }
263
+ preserved.add(name);
264
+ }
265
+ if (preserved.size === 0)
266
+ return preserved;
267
+ const roots = [
268
+ path.join(realHome, '.claude'),
269
+ path.join(realHome, '.claude.json'),
270
+ path.join(realHome, '.codex'),
271
+ path.join(realHome, '.config', 'sidekick'),
272
+ // The Windows default config dir, read before HOME and APPDATA move.
273
+ ...(process.platform === 'win32' && process.env.APPDATA
274
+ ? [path.join(process.env.APPDATA, 'sidekick')]
275
+ : []),
276
+ ...(options.protectedPaths ?? []),
277
+ ];
278
+ for (const name of preserved) {
279
+ const value = process.env[name]?.trim();
280
+ if (!value)
281
+ continue;
282
+ const hit = protectedLocation(value, roots, realFs);
283
+ if (hit) {
284
+ throw new Error(`installCredentialGuard: preserveEnv ${name}=${value} reaches the real protected location ${hit}. Point it at a temporary fixture instead.`);
285
+ }
286
+ }
287
+ return preserved;
288
+ }
289
+ /** The root `target` equals, contains, or lies inside, comparing plain and symlink-resolved paths. */
290
+ function protectedLocation(target, roots, realFs) {
291
+ // Case-insensitive by default on macOS and Windows; folding can only over-refuse.
292
+ const fold = process.platform === 'darwin' || process.platform === 'win32';
293
+ const normal = (value) => {
294
+ const resolved = path.resolve(value);
295
+ return fold ? resolved.toLowerCase() : resolved;
296
+ };
297
+ const within = (parent, child) => child === parent || child.startsWith(parent.endsWith(path.sep) ? parent : parent + path.sep);
298
+ const related = (a, b) => within(a, b) || within(b, a);
299
+ // Plain paths first, so an obviously unsafe value never reaches the filesystem.
300
+ const plain = normal(target);
301
+ for (const root of roots)
302
+ if (related(normal(root), plain))
303
+ return root;
304
+ const real = normal(realpathOfNearest(target, realFs));
305
+ for (const root of roots) {
306
+ const realRoot = normal(realpathOfNearest(root, realFs));
307
+ if (related(realRoot, real) || related(normal(root), real) || related(realRoot, plain))
308
+ return root;
309
+ }
310
+ return null;
311
+ }
312
+ /** `realpath` of the nearest existing ancestor, with the missing tail re-appended. */
313
+ function realpathOfNearest(target, realFs) {
314
+ let current = path.resolve(target);
315
+ let tail = [];
316
+ for (;;) {
317
+ try {
318
+ return path.join(realFs.realpathSync.native(current), ...tail);
319
+ }
320
+ catch {
321
+ const parent = path.dirname(current);
322
+ if (parent === current)
323
+ return path.resolve(target);
324
+ tail = [path.basename(current), ...tail];
325
+ current = parent;
326
+ }
327
+ }
328
+ }
247
329
  /**
248
330
  * The real module object of a Node built-in: `process.getBuiltinModule`
249
331
  * (Node 20.16+), or `require` in CommonJS on older Node.
@@ -0,0 +1,70 @@
1
+ /**
2
+ * Locates the session record that issued each of a set of tool calls, by id.
3
+ *
4
+ * Given Claude Code `tool_use` ids (`toolu_…`) and/or Codex `call_id`s, finds
5
+ * the session file, session id and time of each call, plus the ordinal of the
6
+ * human prompt the call answered, so a downstream record (an edit, a commit)
7
+ * can be tied back to the prompt that caused it. Scope, listing, session keys
8
+ * and ordinals are the ones `collectPromptHistory()` uses, so results join
9
+ * directly with prompt-history entries.
10
+ *
11
+ * Cost: one listing of the workspace-scoped session files (`since` prunes by
12
+ * modification time), then a streaming pass over each candidate file in which
13
+ * a line is only parsed when it contains one of the wanted ids. Claude Code
14
+ * subagent transcripts of each session are searched too. Stops as soon as
15
+ * every id is found. Prompt ordinals are read only for files that matched.
16
+ *
17
+ * Node-only: reads session files.
18
+ *
19
+ * @module toolCallOrigins
20
+ */
21
+ import { type PromptHistoryProvider } from './promptHistory';
22
+ export interface FindToolCallOriginsOptions {
23
+ /** Tool-call ids to locate: Claude `tool_use` ids and/or Codex `call_id`s. */
24
+ ids: Iterable<string>;
25
+ /** Workspace roots that scope the search (as for `collectPromptHistory`). */
26
+ workspacePaths: string[];
27
+ /** Also search the roots' git worktree siblings. Default true. */
28
+ includeWorktrees?: boolean;
29
+ /** Only session files modified at or after this instant. */
30
+ since?: Date;
31
+ /** Default both. */
32
+ providers?: PromptHistoryProvider[];
33
+ /** Stops between files and lines; the promise rejects with the signal's reason. */
34
+ signal?: AbortSignal;
35
+ }
36
+ export interface ToolCallOrigin {
37
+ provider: PromptHistoryProvider;
38
+ /**
39
+ * Session whose prompt history `afterOrdinal` counts in: the session itself,
40
+ * or its parent for a subagent or fork (Claude subagent transcripts live
41
+ * under the parent; a Codex subagent or fork has its own rollout, named in
42
+ * `subagent.agentId` / `filePath`, and no prompts of its own).
43
+ */
44
+ sessionId: string;
45
+ /** Same key `collectPromptHistory()` reports for that session's file. */
46
+ sessionKey: string;
47
+ /** File the call was recorded in (a subagent transcript for subagent calls). */
48
+ filePath: string;
49
+ /** ISO timestamp of the record that issued the call. */
50
+ timestamp: string;
51
+ /**
52
+ * Ordinal of the last human prompt at or before the call, in the session's
53
+ * prompt history; null when the call precedes every prompt or the session
54
+ * has none (e.g. a Codex subagent whose parent was not found in scope).
55
+ */
56
+ afterOrdinal: number | null;
57
+ toolName?: string;
58
+ /** Set when a subagent issued the call. */
59
+ subagent?: {
60
+ agentId: string;
61
+ parentSessionId: string;
62
+ };
63
+ }
64
+ /**
65
+ * Finds where each tool call was issued. Ids that are not found are absent
66
+ * from the result. When an id appears more than once (a forked or resumed
67
+ * session can replay history), the earliest record wins, and a session's own
68
+ * record wins over a fork's copy at the same instant.
69
+ */
70
+ export declare function findToolCallOrigins(options: FindToolCallOriginsOptions): Promise<Map<string, ToolCallOrigin>>;