sidekick-shared 0.28.3 → 0.29.1
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/CHANGELOG.md +86 -0
- package/README.md +188 -27
- package/dist/aggregation/EventAggregator.d.ts +19 -0
- package/dist/aggregation/EventAggregator.js +174 -35
- package/dist/aggregation/snapshot.d.ts +37 -8
- package/dist/aggregation/snapshot.js +31 -9
- package/dist/aggregation/types.d.ts +13 -0
- package/dist/authStatus.d.ts +54 -0
- package/dist/authStatus.js +91 -0
- package/dist/browser.d.ts +16 -6
- package/dist/browser.js +24 -2
- package/dist/codex.d.ts +23 -0
- package/dist/codex.js +44 -0
- package/dist/codexProfiles.d.ts +1 -0
- package/dist/codexProfiles.js +8 -11
- package/dist/context/sessionContext.js +4 -0
- package/dist/esm/aggregation/EventAggregator.js +174 -35
- package/dist/esm/aggregation/snapshot.js +31 -9
- package/dist/esm/authStatus.js +87 -0
- package/dist/esm/browser.js +5 -2
- package/dist/esm/codex.js +18 -0
- package/dist/esm/codexProfiles.js +8 -11
- package/dist/esm/context/sessionContext.js +4 -0
- package/dist/esm/formatters/toolSummary.js +4 -1
- package/dist/esm/formatting.js +10 -1
- package/dist/esm/index.js +11 -6
- package/dist/esm/modelCatalog.js +7 -5
- package/dist/esm/modelContext.js +4 -3
- package/dist/esm/modelInfo.js +425 -47
- package/dist/esm/node.js +9 -3
- package/dist/esm/observedContextWindows.js +2 -1
- package/dist/esm/ownKey.js +26 -0
- package/dist/esm/parsers/codexParser.js +351 -54
- package/dist/esm/parsers/codexRolloutOrigin.js +107 -0
- package/dist/esm/parsers/openCodeParser.js +85 -22
- package/dist/esm/parsers/subagentScanner.js +33 -12
- package/dist/esm/parsers/subagentTraceParser.js +97 -37
- package/dist/esm/pricing.js +18 -0
- package/dist/esm/pricingCatalog.js +17 -9
- package/dist/esm/promptHistory.js +66 -4
- package/dist/esm/providerFailure.js +124 -3
- package/dist/esm/providers/claudeCode.js +10 -2
- package/dist/esm/providers/codex.js +84 -62
- package/dist/esm/providers/codexDatabase.js +3 -19
- package/dist/esm/providers/codexRollouts.js +24 -1
- package/dist/esm/providers/openCode.js +9 -17
- package/dist/esm/report/htmlReportGenerator.js +42 -5
- package/dist/esm/report/transcriptParser.js +13 -11
- package/dist/esm/schemas/providerFailure.js +3 -1
- package/dist/esm/schemas/sessionEvent.js +35 -14
- package/dist/esm/schemas/transcript.js +4 -0
- package/dist/esm/sessionPromptHistory.js +2 -2
- package/dist/esm/statusline/payload.js +9 -0
- package/dist/esm/subagentSpawn.js +163 -0
- package/dist/esm/testing/credentialGuard.js +87 -5
- package/dist/esm/toolCallOrigins.js +310 -0
- package/dist/esm/transcript.js +46 -3
- package/dist/esm/turns/assistantTurn.js +3 -2
- package/dist/esm/usage/claudeUsageDedupe.js +68 -0
- package/dist/esm/usage/usageEvents.js +146 -17
- package/dist/esm/usage/usageReports.js +39 -6
- package/dist/esm/watchers/eventBridge.js +15 -0
- package/dist/esm/watchers/factory.js +5 -3
- package/dist/esm/watchers/jsonlTail.js +442 -58
- package/dist/esm/watchers/jsonlWatcher.js +51 -4
- package/dist/esm/watchers/providerReaderWatcher.js +13 -0
- package/dist/esm/zaiQuota.js +2 -1
- package/dist/formatters/toolSummary.js +4 -1
- package/dist/formatting.d.ts +26 -1
- package/dist/formatting.js +11 -1
- package/dist/index.d.ts +22 -11
- package/dist/index.js +44 -12
- package/dist/modelCatalog.js +7 -5
- package/dist/modelContext.js +4 -3
- package/dist/modelInfo.d.ts +185 -16
- package/dist/modelInfo.js +434 -49
- package/dist/node.d.ts +16 -5
- package/dist/node.js +33 -2
- package/dist/observedContextWindows.js +2 -1
- package/dist/ownKey.d.ts +16 -0
- package/dist/ownKey.js +30 -0
- package/dist/parsers/codexParser.d.ts +118 -0
- package/dist/parsers/codexParser.js +352 -54
- package/dist/parsers/codexRolloutOrigin.d.ts +49 -0
- package/dist/parsers/codexRolloutOrigin.js +111 -0
- package/dist/parsers/openCodeParser.d.ts +38 -0
- package/dist/parsers/openCodeParser.js +88 -22
- package/dist/parsers/subagentScanner.d.ts +19 -6
- package/dist/parsers/subagentScanner.js +33 -12
- package/dist/parsers/subagentTraceParser.d.ts +25 -5
- package/dist/parsers/subagentTraceParser.js +97 -37
- package/dist/pricing.d.ts +22 -0
- package/dist/pricing.js +50 -0
- package/dist/pricingCatalog.d.ts +10 -0
- package/dist/pricingCatalog.js +16 -8
- package/dist/promptHistory.d.ts +65 -4
- package/dist/promptHistory.js +68 -5
- package/dist/providerFailure.d.ts +44 -0
- package/dist/providerFailure.js +126 -4
- package/dist/providers/claudeCode.js +9 -1
- package/dist/providers/codex.d.ts +3 -22
- package/dist/providers/codex.js +85 -64
- package/dist/providers/codexDatabase.d.ts +3 -2
- package/dist/providers/codexDatabase.js +4 -22
- package/dist/providers/codexRollouts.d.ts +22 -0
- package/dist/providers/codexRollouts.js +24 -0
- package/dist/providers/openCode.js +8 -16
- package/dist/providers/types.d.ts +14 -0
- package/dist/report/htmlReportGenerator.js +42 -5
- package/dist/report/transcriptParser.d.ts +10 -1
- package/dist/report/transcriptParser.js +13 -11
- package/dist/schemas/index.d.ts +1 -0
- package/dist/schemas/providerFailure.d.ts +39 -0
- package/dist/schemas/providerFailure.js +2 -0
- package/dist/schemas/sessionEvent.d.ts +27 -10
- package/dist/schemas/sessionEvent.js +35 -14
- package/dist/schemas/transcript.d.ts +2 -0
- package/dist/schemas/transcript.js +4 -0
- package/dist/sessionPromptHistory.d.ts +7 -1
- package/dist/sessionPromptHistory.js +1 -1
- package/dist/statusline/payload.d.ts +10 -0
- package/dist/statusline/payload.js +14 -0
- package/dist/subagentSpawn.d.ts +80 -0
- package/dist/subagentSpawn.js +174 -0
- package/dist/testing/credentialGuard.d.ts +14 -0
- package/dist/testing/credentialGuard.js +87 -5
- package/dist/toolCallOrigins.d.ts +70 -0
- package/dist/toolCallOrigins.js +346 -0
- package/dist/transcript.d.ts +12 -0
- package/dist/transcript.js +46 -3
- package/dist/turns/assistantTurn.d.ts +1 -0
- package/dist/turns/assistantTurn.js +3 -2
- package/dist/types/opencode.d.ts +2 -0
- package/dist/usage/claudeUsageDedupe.d.ts +31 -0
- package/dist/usage/claudeUsageDedupe.js +68 -0
- package/dist/usage/usageEvents.d.ts +54 -6
- package/dist/usage/usageEvents.js +146 -17
- package/dist/usage/usageReports.d.ts +35 -4
- package/dist/usage/usageReports.js +39 -6
- package/dist/watchers/eventBridge.js +15 -0
- package/dist/watchers/factory.d.ts +6 -0
- package/dist/watchers/factory.js +5 -3
- package/dist/watchers/jsonlTail.d.ts +124 -4
- package/dist/watchers/jsonlTail.js +446 -59
- package/dist/watchers/jsonlWatcher.d.ts +18 -1
- package/dist/watchers/jsonlWatcher.js +50 -3
- package/dist/watchers/providerReaderWatcher.d.ts +7 -0
- package/dist/watchers/providerReaderWatcher.js +13 -0
- package/dist/watchers/types.d.ts +12 -0
- package/dist/zaiQuota.js +2 -1
- package/package.json +41 -1
|
@@ -596,14 +596,51 @@ function renderContentBlock(block, includeThinking, includeToolDetail) {
|
|
|
596
596
|
return '';
|
|
597
597
|
}
|
|
598
598
|
}
|
|
599
|
-
|
|
599
|
+
/** Prefix of the bookkeeping keys session parsers stash inside tool inputs. */
|
|
600
|
+
const SIDEKICK_INPUT_KEY_PREFIX = '_sidekick';
|
|
601
|
+
/**
|
|
602
|
+
* The tool's display identity and user-facing input. Entries built outside
|
|
603
|
+
* the canonical projection may still carry the parsers' `_sidekick*` keys in
|
|
604
|
+
* `toolInput`; those are hidden here and read as the raw identity.
|
|
605
|
+
*/
|
|
606
|
+
function toolDisplay(block) {
|
|
600
607
|
const name = block.toolName || 'unknown';
|
|
608
|
+
const raw = block.toolInput;
|
|
609
|
+
let input = raw;
|
|
610
|
+
let rawToolName = block.rawToolName;
|
|
611
|
+
let mcpServerName = block.mcpServerName;
|
|
612
|
+
if (raw && Object.keys(raw).some((key) => key.startsWith(SIDEKICK_INPUT_KEY_PREFIX))) {
|
|
613
|
+
const clean = {};
|
|
614
|
+
for (const [key, value] of Object.entries(raw)) {
|
|
615
|
+
if (!key.startsWith(SIDEKICK_INPUT_KEY_PREFIX))
|
|
616
|
+
clean[key] = value;
|
|
617
|
+
}
|
|
618
|
+
input = clean;
|
|
619
|
+
if (!rawToolName && typeof raw._sidekickRawToolName === 'string') {
|
|
620
|
+
rawToolName = raw._sidekickRawToolName;
|
|
621
|
+
}
|
|
622
|
+
if (!mcpServerName && typeof raw._sidekickMcpServerName === 'string') {
|
|
623
|
+
mcpServerName = raw._sidekickMcpServerName;
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
const parts = [];
|
|
627
|
+
if (rawToolName && rawToolName.trim() && rawToolName !== name)
|
|
628
|
+
parts.push(rawToolName);
|
|
629
|
+
if (mcpServerName && mcpServerName.trim())
|
|
630
|
+
parts.push(`MCP: ${mcpServerName}`);
|
|
631
|
+
const hint = parts.length
|
|
632
|
+
? ` <span class="tool-raw-name" style="color:var(--text-muted);font-size:11px">${escapeHtml(parts.join(' · '))}</span>`
|
|
633
|
+
: '';
|
|
634
|
+
return { name, hint, input };
|
|
635
|
+
}
|
|
636
|
+
function renderToolUse(block, includeDetail) {
|
|
637
|
+
const { name, hint, input } = toolDisplay(block);
|
|
601
638
|
if (!includeDetail) {
|
|
602
|
-
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;">${escapeHtml(name)}</div></div>`;
|
|
639
|
+
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;">${escapeHtml(name)}${hint}</div></div>`;
|
|
603
640
|
}
|
|
604
641
|
let inputHtml = '';
|
|
605
|
-
if (
|
|
606
|
-
const inputJson = JSON.stringify(
|
|
642
|
+
if (input && Object.keys(input).length > 0) {
|
|
643
|
+
const inputJson = JSON.stringify(input, null, 2);
|
|
607
644
|
const highlighted = highlightCodeBlock(inputJson, 'json');
|
|
608
645
|
inputHtml = `<div class="tool-io">
|
|
609
646
|
<div class="tool-label">Input</div>
|
|
@@ -611,7 +648,7 @@ function renderToolUse(block, includeDetail) {
|
|
|
611
648
|
</div>`;
|
|
612
649
|
}
|
|
613
650
|
return `<details class="tool-block" data-block-type="tool">
|
|
614
|
-
<summary>${escapeHtml(name)}${block.toolUseId ? ` <span style="color:var(--text-muted);font-size:11px">${escapeHtml(block.toolUseId)}</span>` : ''}</summary>
|
|
651
|
+
<summary>${escapeHtml(name)}${hint}${block.toolUseId ? ` <span style="color:var(--text-muted);font-size:11px">${escapeHtml(block.toolUseId)}</span>` : ''}</summary>
|
|
615
652
|
<div class="tool-content">
|
|
616
653
|
${inputHtml}
|
|
617
654
|
</div>
|
|
@@ -42,17 +42,19 @@ function toReportBlock(block) {
|
|
|
42
42
|
return block.text ? [{ type: 'text', text: block.text }] : [];
|
|
43
43
|
case 'thinking':
|
|
44
44
|
return block.text ? [{ type: 'thinking', text: block.text }] : [];
|
|
45
|
-
case 'tool_use':
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
},
|
|
55
|
-
|
|
45
|
+
case 'tool_use': {
|
|
46
|
+
const toolBlock = {
|
|
47
|
+
type: 'tool_use',
|
|
48
|
+
toolName: block.toolName ?? 'unknown',
|
|
49
|
+
toolInput: block.input && typeof block.input === 'object'
|
|
50
|
+
? block.input
|
|
51
|
+
: {},
|
|
52
|
+
toolUseId: block.toolUseId,
|
|
53
|
+
...(block.rawToolName ? { rawToolName: block.rawToolName } : {}),
|
|
54
|
+
...(block.mcpServerName ? { mcpServerName: block.mcpServerName } : {}),
|
|
55
|
+
};
|
|
56
|
+
return [toolBlock];
|
|
57
|
+
}
|
|
56
58
|
case 'tool_result':
|
|
57
59
|
return [
|
|
58
60
|
{
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import { z } from 'zod';
|
|
11
11
|
import { ACCOUNT_PROVIDER_IDS } from '../providerIds';
|
|
12
|
-
import { PROVIDER_AUTHENTICATION_SOURCES, PROVIDER_AUTHENTICATION_STATES, PROVIDER_CREDENTIAL_KINDS, PROVIDER_FAILURE_CODES, PROVIDER_FAILURE_EVIDENCE_SOURCES, PROVIDER_RECOVERY_ACTIONS, } from '../providerFailure';
|
|
12
|
+
import { CODEX_ERROR_INFO_KINDS, PROVIDER_AUTHENTICATION_SOURCES, PROVIDER_AUTHENTICATION_STATES, PROVIDER_CREDENTIAL_KINDS, PROVIDER_FAILURE_CODES, PROVIDER_FAILURE_EVIDENCE_SOURCES, PROVIDER_RECOVERY_ACTIONS, } from '../providerFailure';
|
|
13
13
|
export const providerAuthenticationEvidenceSchema = z.object({
|
|
14
14
|
credentialKind: z.enum(PROVIDER_CREDENTIAL_KINDS),
|
|
15
15
|
state: z.enum(PROVIDER_AUTHENTICATION_STATES),
|
|
@@ -20,6 +20,7 @@ export const providerFailureEvidenceSchema = z.object({
|
|
|
20
20
|
source: z.enum(PROVIDER_FAILURE_EVIDENCE_SOURCES),
|
|
21
21
|
rule: z.enum(PROVIDER_FAILURE_CODES),
|
|
22
22
|
checkedAt: z.string().optional(),
|
|
23
|
+
codexErrorInfo: z.enum(CODEX_ERROR_INFO_KINDS).optional(),
|
|
23
24
|
});
|
|
24
25
|
export const providerRetryAfterSchema = z.discriminatedUnion('kind', [
|
|
25
26
|
z.object({ kind: z.literal('delay'), delayMs: z.number() }),
|
|
@@ -30,6 +31,7 @@ export const providerFailureDiagnosisSchema = z.object({
|
|
|
30
31
|
credentialKind: z.enum(PROVIDER_CREDENTIAL_KINDS),
|
|
31
32
|
diagnosis: z.enum(PROVIDER_FAILURE_CODES),
|
|
32
33
|
recovery: z.enum(PROVIDER_RECOVERY_ACTIONS),
|
|
34
|
+
retryable: z.boolean().optional(),
|
|
33
35
|
evidence: z.array(providerFailureEvidenceSchema),
|
|
34
36
|
authentication: providerAuthenticationEvidenceSchema.optional(),
|
|
35
37
|
httpStatus: z.number().optional(),
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import { z } from 'zod';
|
|
10
10
|
import { normalizedUsageSchema } from './usageNormalization';
|
|
11
|
+
import { taskNotificationPromptFromAttachment } from '../subagentSpawn';
|
|
11
12
|
// ── MessageUsage ──
|
|
12
13
|
export const messageUsageSchema = z.object({
|
|
13
14
|
input_tokens: z.number(),
|
|
@@ -52,7 +53,9 @@ const eventBaseShape = {
|
|
|
52
53
|
promptSource: z.string().optional(),
|
|
53
54
|
cwd: z.string().optional(),
|
|
54
55
|
gitBranch: z.string().optional(),
|
|
55
|
-
|
|
56
|
+
// A mode this release doesn't know (Claude Code added `auto`) is dropped
|
|
57
|
+
// rather than rejecting the whole line with its prompt or tool results.
|
|
58
|
+
permissionMode: permissionModeSchema.optional().catch(undefined),
|
|
56
59
|
rateLimits: z
|
|
57
60
|
.object({
|
|
58
61
|
primary: z
|
|
@@ -169,22 +172,21 @@ export const sessionEventSchema = z.union([
|
|
|
169
172
|
// ── Progress unwrapping ──
|
|
170
173
|
/** Recursion bound for nested progress envelopes; real data nests 1 level. */
|
|
171
174
|
const MAX_PROGRESS_DEPTH = 8;
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
* `sessionEventSchema` alone rejects. This helper tries a direct parse,
|
|
178
|
-
* then unwraps progress envelopes (recursively, in case of nesting).
|
|
179
|
-
* Returns zero events for unrecognized input — never throws.
|
|
180
|
-
*/
|
|
181
|
-
export function extractSessionEvents(raw, depth = 0) {
|
|
182
|
-
return extractSessionEventsWithProvenance(raw, depth, {});
|
|
175
|
+
export function extractSessionEvents(raw, depthOrOptions = 0) {
|
|
176
|
+
if (typeof depthOrOptions === 'number') {
|
|
177
|
+
return extractSessionEventsWithProvenance(raw, depthOrOptions, {}, {});
|
|
178
|
+
}
|
|
179
|
+
return extractSessionEventsWithProvenance(raw, 0, {}, depthOrOptions ?? {});
|
|
183
180
|
}
|
|
184
|
-
function extractSessionEventsWithProvenance(raw, depth, inherited) {
|
|
181
|
+
function extractSessionEventsWithProvenance(raw, depth, inherited, options) {
|
|
185
182
|
const direct = sessionEventSchema.safeParse(raw);
|
|
186
183
|
if (direct.success)
|
|
187
184
|
return [{ ...inherited, ...direct.data }];
|
|
185
|
+
if (options.includeTaskNotifications) {
|
|
186
|
+
const notification = taskNotificationAttachmentEvent(raw);
|
|
187
|
+
if (notification)
|
|
188
|
+
return [{ ...inherited, ...notification }];
|
|
189
|
+
}
|
|
188
190
|
if (depth >= MAX_PROGRESS_DEPTH)
|
|
189
191
|
return [];
|
|
190
192
|
if (typeof raw === 'object' && raw !== null && raw.type === 'progress') {
|
|
@@ -193,12 +195,31 @@ function extractSessionEventsWithProvenance(raw, depth, inherited) {
|
|
|
193
195
|
if (typeof data === 'object' && data !== null) {
|
|
194
196
|
const message = data.message;
|
|
195
197
|
if (typeof message === 'object' && message !== null) {
|
|
196
|
-
return extractSessionEventsWithProvenance(message, depth + 1, provenance);
|
|
198
|
+
return extractSessionEventsWithProvenance(message, depth + 1, provenance, options);
|
|
197
199
|
}
|
|
198
200
|
}
|
|
199
201
|
}
|
|
200
202
|
return [];
|
|
201
203
|
}
|
|
204
|
+
/** The `system` event for a task-notification attachment line, or `null`. */
|
|
205
|
+
function taskNotificationAttachmentEvent(raw) {
|
|
206
|
+
const prompt = taskNotificationPromptFromAttachment(raw);
|
|
207
|
+
if (prompt === null)
|
|
208
|
+
return null;
|
|
209
|
+
const line = raw;
|
|
210
|
+
const candidate = {
|
|
211
|
+
type: 'system',
|
|
212
|
+
timestamp: line.timestamp,
|
|
213
|
+
origin: { kind: 'task-notification' },
|
|
214
|
+
message: { role: 'user', sourceLabel: 'task notification', content: prompt },
|
|
215
|
+
};
|
|
216
|
+
for (const key of ['entrypoint', 'isSidechain', 'cwd', 'gitBranch']) {
|
|
217
|
+
if (line[key] !== undefined)
|
|
218
|
+
candidate[key] = line[key];
|
|
219
|
+
}
|
|
220
|
+
const parsed = systemSessionEventSchema.safeParse(candidate);
|
|
221
|
+
return parsed.success ? parsed.data : null;
|
|
222
|
+
}
|
|
202
223
|
function progressProvenance(raw) {
|
|
203
224
|
const value = raw;
|
|
204
225
|
const provenance = {};
|
|
@@ -17,6 +17,8 @@ export const canonicalTranscriptBlockSchema = z.object({
|
|
|
17
17
|
type: z.enum(['text', 'thinking', 'tool_use', 'tool_result', 'image', 'unknown']),
|
|
18
18
|
text: z.string().optional(),
|
|
19
19
|
toolName: z.string().optional(),
|
|
20
|
+
rawToolName: z.string().optional(),
|
|
21
|
+
mcpServerName: z.string().optional(),
|
|
20
22
|
toolUseId: z.string().optional(),
|
|
21
23
|
input: z.unknown().optional(),
|
|
22
24
|
output: z.unknown().optional(),
|
|
@@ -28,6 +30,8 @@ export const canonicalTranscriptBlockSchema = z.object({
|
|
|
28
30
|
export const canonicalToolCallSchema = z.object({
|
|
29
31
|
id: z.string().optional(),
|
|
30
32
|
name: z.string(),
|
|
33
|
+
rawToolName: z.string().optional(),
|
|
34
|
+
mcpServerName: z.string().optional(),
|
|
31
35
|
timestamp: z.string(),
|
|
32
36
|
input: z.unknown().optional(),
|
|
33
37
|
output: z.unknown().optional(),
|
|
@@ -14,13 +14,13 @@
|
|
|
14
14
|
* and replies or signals that belong to an out-of-scope prompt are dropped.
|
|
15
15
|
*/
|
|
16
16
|
import * as fs from 'fs';
|
|
17
|
-
import { DEFAULT_BOUNDS, compareStrings, emptyStats, listCandidates, resolveRoots, scanFile, } from './promptHistory';
|
|
17
|
+
import { DEFAULT_BOUNDS, PROMPT_HISTORY_ORDINAL_MAX_RECORD_BYTES, compareStrings, emptyStats, listCandidates, resolveRoots, scanFile, } from './promptHistory';
|
|
18
18
|
import { createSessionRecordExtractor, } from './sessionPromptSignals';
|
|
19
19
|
const DEFAULT_SESSION_BOUNDS = {
|
|
20
20
|
maxSessions: 1000,
|
|
21
21
|
maxSessionBytes: 256 * 1024 * 1024,
|
|
22
22
|
maxTotalBytes: 256 * 1024 * 1024,
|
|
23
|
-
maxRecordBytes:
|
|
23
|
+
maxRecordBytes: PROMPT_HISTORY_ORDINAL_MAX_RECORD_BYTES,
|
|
24
24
|
deadlineMs: 60_000,
|
|
25
25
|
};
|
|
26
26
|
const MAX_EXCLUSIONS = 1000;
|
|
@@ -0,0 +1,9 @@
|
|
|
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';
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Recognizes Claude Code subagent spawns and background-task notifications.
|
|
3
|
+
*
|
|
4
|
+
* Claude Code's subagent tool is `Agent`; `Task` is its earlier name (and the
|
|
5
|
+
* name OpenCode's `task` tool is normalized to). A background agent's spawning
|
|
6
|
+
* tool call gets its result right away ("Async agent launched successfully.")
|
|
7
|
+
* and its completion arrives later as a `<task-notification>` block, delivered
|
|
8
|
+
* either as a user line or as a mid-turn `queued_command` attachment.
|
|
9
|
+
*
|
|
10
|
+
* Pure: no filesystem access, so it is safe for browser bundles.
|
|
11
|
+
*
|
|
12
|
+
* @module subagentSpawn
|
|
13
|
+
*/
|
|
14
|
+
/** Tool names that spawn a subagent: `Agent` (current) and `Task` (earlier name). */
|
|
15
|
+
export const SUBAGENT_SPAWN_TOOL_NAMES = Object.freeze(['Agent', 'Task']);
|
|
16
|
+
const SPAWN_TOOLS = new Set(SUBAGENT_SPAWN_TOOL_NAMES);
|
|
17
|
+
/** Whether a tool name spawns a subagent (`Agent` or `Task`). */
|
|
18
|
+
export function isSubagentSpawnTool(name) {
|
|
19
|
+
return typeof name === 'string' && SPAWN_TOOLS.has(name);
|
|
20
|
+
}
|
|
21
|
+
const ASYNC_LAUNCH_PREFIX = /^\s*Async agent launched successfully\b/;
|
|
22
|
+
const AGENT_ID_LINE = /(?:^|\n)[ \t]*agentId:[ \t]*([A-Za-z0-9][\w.@-]*)/;
|
|
23
|
+
/**
|
|
24
|
+
* Plain text of tool-result or message content: a string as-is, or the text
|
|
25
|
+
* blocks of a content array joined by newlines.
|
|
26
|
+
*/
|
|
27
|
+
export function contentText(content) {
|
|
28
|
+
if (typeof content === 'string')
|
|
29
|
+
return content;
|
|
30
|
+
if (!Array.isArray(content))
|
|
31
|
+
return '';
|
|
32
|
+
const parts = [];
|
|
33
|
+
for (const block of content) {
|
|
34
|
+
if (!block || typeof block !== 'object')
|
|
35
|
+
continue;
|
|
36
|
+
const typed = block;
|
|
37
|
+
if (typed.type === 'text' && typeof typed.text === 'string')
|
|
38
|
+
parts.push(typed.text);
|
|
39
|
+
}
|
|
40
|
+
return parts.join('\n');
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Recognizes the immediate result of a background spawn
|
|
44
|
+
* (`Async agent launched successfully.` … `agentId: <id> …`). Accepts the
|
|
45
|
+
* result text or the raw tool-result content. Returns `null` for anything
|
|
46
|
+
* else, including a finished foreground agent's result.
|
|
47
|
+
*/
|
|
48
|
+
export function parseAsyncAgentLaunch(result) {
|
|
49
|
+
const text = contentText(result);
|
|
50
|
+
if (!ASYNC_LAUNCH_PREFIX.test(text))
|
|
51
|
+
return null;
|
|
52
|
+
const match = AGENT_ID_LINE.exec(text);
|
|
53
|
+
return match ? { agentId: match[1] } : {};
|
|
54
|
+
}
|
|
55
|
+
const NOTIFICATION_OPEN = '<task-notification>';
|
|
56
|
+
const NOTIFICATION_CLOSE = '</task-notification>';
|
|
57
|
+
const NOTIFICATION_PREFIX = /^\s*<task-notification>/;
|
|
58
|
+
const TAG_PATTERNS = {
|
|
59
|
+
taskId: /<task-id>\s*([\s\S]*?)\s*<\/task-id>/,
|
|
60
|
+
toolUseId: /<tool-use-id>\s*([\s\S]*?)\s*<\/tool-use-id>/,
|
|
61
|
+
status: /<status>\s*([\s\S]*?)\s*<\/status>/,
|
|
62
|
+
summary: /<summary>\s*([\s\S]*?)\s*<\/summary>/,
|
|
63
|
+
note: /<note>\s*([\s\S]*?)\s*<\/note>/,
|
|
64
|
+
};
|
|
65
|
+
const INTERIM_TEXT = /waiting on its own background work|background work of its own still running/i;
|
|
66
|
+
function tagValue(body, pattern) {
|
|
67
|
+
const value = pattern.exec(body)?.[1]?.trim();
|
|
68
|
+
return value ? value : undefined;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Parses the first `<task-notification>` block in `text`. Whitespace and
|
|
72
|
+
* newlines between tags, missing fields, and unknown extra fields are
|
|
73
|
+
* tolerated; the block needs a `<task-id>`. Fields are read before the
|
|
74
|
+
* agent's own `<result>` output, which may quote anything.
|
|
75
|
+
*/
|
|
76
|
+
export function parseTaskNotification(text) {
|
|
77
|
+
if (typeof text !== 'string')
|
|
78
|
+
return null;
|
|
79
|
+
const start = text.indexOf(NOTIFICATION_OPEN);
|
|
80
|
+
if (start < 0)
|
|
81
|
+
return null;
|
|
82
|
+
let body = text.slice(start + NOTIFICATION_OPEN.length);
|
|
83
|
+
const resultAt = body.indexOf('<result>');
|
|
84
|
+
let header = resultAt >= 0 ? body.slice(0, resultAt) : body;
|
|
85
|
+
if (resultAt < 0) {
|
|
86
|
+
const end = body.indexOf(NOTIFICATION_CLOSE);
|
|
87
|
+
if (end >= 0)
|
|
88
|
+
header = body = body.slice(0, end);
|
|
89
|
+
}
|
|
90
|
+
if (!TAG_PATTERNS.taskId.test(header))
|
|
91
|
+
header = body;
|
|
92
|
+
const taskId = tagValue(header, TAG_PATTERNS.taskId);
|
|
93
|
+
if (!taskId)
|
|
94
|
+
return null;
|
|
95
|
+
const toolUseId = tagValue(header, TAG_PATTERNS.toolUseId);
|
|
96
|
+
const status = tagValue(header, TAG_PATTERNS.status);
|
|
97
|
+
const summary = tagValue(header, TAG_PATTERNS.summary);
|
|
98
|
+
const note = tagValue(header, TAG_PATTERNS.note);
|
|
99
|
+
return {
|
|
100
|
+
taskId,
|
|
101
|
+
...(toolUseId ? { toolUseId } : {}),
|
|
102
|
+
...(status ? { status } : {}),
|
|
103
|
+
...(summary ? { summary } : {}),
|
|
104
|
+
interim: INTERIM_TEXT.test(summary ?? '') || INTERIM_TEXT.test(note ?? ''),
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* The `<task-notification>` prompt of a raw Claude Code attachment line
|
|
109
|
+
* (`{ type: 'attachment', attachment: { type: 'queued_command', commandMode:
|
|
110
|
+
* 'task-notification', prompt } }`), which is how a notification that lands
|
|
111
|
+
* mid-turn is recorded. `null` for any other value.
|
|
112
|
+
*/
|
|
113
|
+
export function taskNotificationPromptFromAttachment(raw) {
|
|
114
|
+
if (!raw || typeof raw !== 'object')
|
|
115
|
+
return null;
|
|
116
|
+
const line = raw;
|
|
117
|
+
if (line.type !== 'attachment' || !line.attachment || typeof line.attachment !== 'object') {
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
const attachment = line.attachment;
|
|
121
|
+
return attachment.type === 'queued_command' &&
|
|
122
|
+
attachment.commandMode === 'task-notification' &&
|
|
123
|
+
typeof attachment.prompt === 'string'
|
|
124
|
+
? attachment.prompt
|
|
125
|
+
: null;
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* The synthetic `system` event `extractSessionEvents(raw, {
|
|
129
|
+
* includeTaskNotifications: true })` makes from a mid-turn task-notification
|
|
130
|
+
* attachment. It is bookkeeping, not a message or a prompt.
|
|
131
|
+
*/
|
|
132
|
+
export function isTaskNotificationSystemEvent(event) {
|
|
133
|
+
return event.type === 'system' && event.origin?.kind === 'task-notification';
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* The task notification an event carries: a user line with
|
|
137
|
+
* `origin.kind === 'task-notification'` or whose text starts with
|
|
138
|
+
* `<task-notification>`, or the opt-in synthetic system event. `null` otherwise.
|
|
139
|
+
*/
|
|
140
|
+
export function taskNotificationFromEvent(event) {
|
|
141
|
+
if (event.type !== 'user' && event.type !== 'system')
|
|
142
|
+
return null;
|
|
143
|
+
const flagged = event.origin?.kind === 'task-notification';
|
|
144
|
+
if (event.type === 'system' && !flagged)
|
|
145
|
+
return null;
|
|
146
|
+
const text = contentText(event.message?.content);
|
|
147
|
+
if (!flagged && !NOTIFICATION_PREFIX.test(text))
|
|
148
|
+
return null;
|
|
149
|
+
return parseTaskNotification(text);
|
|
150
|
+
}
|
|
151
|
+
/** Maps a notification `<status>` to an outcome; unknown or missing statuses read as `completed`. */
|
|
152
|
+
export function taskNotificationOutcome(status) {
|
|
153
|
+
switch (status?.toLowerCase()) {
|
|
154
|
+
case 'failed':
|
|
155
|
+
return 'failed';
|
|
156
|
+
case 'killed':
|
|
157
|
+
return 'killed';
|
|
158
|
+
case 'stopped':
|
|
159
|
+
return 'stopped';
|
|
160
|
+
default:
|
|
161
|
+
return 'completed';
|
|
162
|
+
}
|
|
163
|
+
}
|
|
@@ -10,7 +10,8 @@
|
|
|
10
10
|
* - `HOME` (and the Windows equivalents) point at a per-process temp dir,
|
|
11
11
|
* removed on exit, and the home overrides (`CLAUDE_CONFIG_DIR`,
|
|
12
12
|
* `CLAUDE_SECURESTORAGE_CONFIG_DIR`, `CODEX_HOME`, `SIDEKICK_CONFIG_DIR`)
|
|
13
|
-
* are unset
|
|
13
|
+
* are unset (unless named in `preserveEnv` and pointed at a fixture), so
|
|
14
|
+
* every `os.homedir()`-derived path is isolated structurally;
|
|
14
15
|
* - the real `child_process` module object is patched, which reaches both ESM
|
|
15
16
|
* named imports (after `syncBuiltinESMExports`) and CommonJS code loaded
|
|
16
17
|
* through native `require`; per-file `vi.mock('child_process')` factories
|
|
@@ -28,6 +29,13 @@ import * as os from 'node:os';
|
|
|
28
29
|
import * as path from 'node:path';
|
|
29
30
|
const GUARD_KEY = '__sidekickCredentialGuard';
|
|
30
31
|
const DEFAULT_BLOCKED_BINARIES = ['security', 'claude', 'codex'];
|
|
32
|
+
/** Home overrides the guard unsets; only these can be preserved. */
|
|
33
|
+
const SCRUBBED_ENV = [
|
|
34
|
+
'CLAUDE_CONFIG_DIR',
|
|
35
|
+
'CLAUDE_SECURESTORAGE_CONFIG_DIR',
|
|
36
|
+
'CODEX_HOME',
|
|
37
|
+
'SIDEKICK_CONFIG_DIR',
|
|
38
|
+
];
|
|
31
39
|
const CHILD_PROCESS_FUNCTIONS = [
|
|
32
40
|
'spawn',
|
|
33
41
|
'spawnSync',
|
|
@@ -75,6 +83,8 @@ export function installCredentialGuard(options = {}) {
|
|
|
75
83
|
// os.userInfo() reads the passwd entry, so it still names the real home
|
|
76
84
|
// after HOME is redirected below.
|
|
77
85
|
const realHome = os.userInfo().homedir;
|
|
86
|
+
// Validated before any side effect, so a refused install changes nothing.
|
|
87
|
+
const preserved = checkPreservedEnv(options, realHome, realFs);
|
|
78
88
|
const testHome = realFs.mkdtempSync(path.join(os.tmpdir(), 'sidekick-test-home-'));
|
|
79
89
|
const removeTestHome = realFs.rmSync.bind(realFs);
|
|
80
90
|
process.once('exit', () => {
|
|
@@ -103,10 +113,9 @@ export function installCredentialGuard(options = {}) {
|
|
|
103
113
|
process.env.USERPROFILE = testHome;
|
|
104
114
|
process.env.APPDATA = path.join(testHome, 'AppData', 'Roaming');
|
|
105
115
|
process.env.LOCALAPPDATA = path.join(testHome, 'AppData', 'Local');
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
delete process.env.SIDEKICK_CONFIG_DIR;
|
|
116
|
+
for (const name of SCRUBBED_ENV)
|
|
117
|
+
if (!preserved.has(name))
|
|
118
|
+
delete process.env[name];
|
|
110
119
|
const protectedRoots = path.resolve(realHome) === path.resolve(testHome)
|
|
111
120
|
? []
|
|
112
121
|
: [
|
|
@@ -208,6 +217,79 @@ export function installCredentialGuard(options = {}) {
|
|
|
208
217
|
globalThis[GUARD_KEY] = state;
|
|
209
218
|
return state;
|
|
210
219
|
}
|
|
220
|
+
/** The `preserveEnv` names, after refusing unknown names and unsafe values. */
|
|
221
|
+
function checkPreservedEnv(options, realHome, realFs) {
|
|
222
|
+
const preserved = new Set();
|
|
223
|
+
for (const name of options.preserveEnv ?? []) {
|
|
224
|
+
if (!SCRUBBED_ENV.includes(name)) {
|
|
225
|
+
throw new Error(`installCredentialGuard: preserveEnv accepts only ${SCRUBBED_ENV.join(', ')}; got ${JSON.stringify(name)}.`);
|
|
226
|
+
}
|
|
227
|
+
preserved.add(name);
|
|
228
|
+
}
|
|
229
|
+
if (preserved.size === 0)
|
|
230
|
+
return preserved;
|
|
231
|
+
const roots = [
|
|
232
|
+
path.join(realHome, '.claude'),
|
|
233
|
+
path.join(realHome, '.claude.json'),
|
|
234
|
+
path.join(realHome, '.codex'),
|
|
235
|
+
path.join(realHome, '.config', 'sidekick'),
|
|
236
|
+
// The Windows default config dir, read before HOME and APPDATA move.
|
|
237
|
+
...(process.platform === 'win32' && process.env.APPDATA
|
|
238
|
+
? [path.join(process.env.APPDATA, 'sidekick')]
|
|
239
|
+
: []),
|
|
240
|
+
...(options.protectedPaths ?? []),
|
|
241
|
+
];
|
|
242
|
+
for (const name of preserved) {
|
|
243
|
+
const value = process.env[name]?.trim();
|
|
244
|
+
if (!value)
|
|
245
|
+
continue;
|
|
246
|
+
const hit = protectedLocation(value, roots, realFs);
|
|
247
|
+
if (hit) {
|
|
248
|
+
throw new Error(`installCredentialGuard: preserveEnv ${name}=${value} reaches the real protected location ${hit}. Point it at a temporary fixture instead.`);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
return preserved;
|
|
252
|
+
}
|
|
253
|
+
/** The root `target` equals, contains, or lies inside, comparing plain and symlink-resolved paths. */
|
|
254
|
+
function protectedLocation(target, roots, realFs) {
|
|
255
|
+
// Case-insensitive by default on macOS and Windows; folding can only over-refuse.
|
|
256
|
+
const fold = process.platform === 'darwin' || process.platform === 'win32';
|
|
257
|
+
const normal = (value) => {
|
|
258
|
+
const resolved = path.resolve(value);
|
|
259
|
+
return fold ? resolved.toLowerCase() : resolved;
|
|
260
|
+
};
|
|
261
|
+
const within = (parent, child) => child === parent || child.startsWith(parent.endsWith(path.sep) ? parent : parent + path.sep);
|
|
262
|
+
const related = (a, b) => within(a, b) || within(b, a);
|
|
263
|
+
// Plain paths first, so an obviously unsafe value never reaches the filesystem.
|
|
264
|
+
const plain = normal(target);
|
|
265
|
+
for (const root of roots)
|
|
266
|
+
if (related(normal(root), plain))
|
|
267
|
+
return root;
|
|
268
|
+
const real = normal(realpathOfNearest(target, realFs));
|
|
269
|
+
for (const root of roots) {
|
|
270
|
+
const realRoot = normal(realpathOfNearest(root, realFs));
|
|
271
|
+
if (related(realRoot, real) || related(normal(root), real) || related(realRoot, plain))
|
|
272
|
+
return root;
|
|
273
|
+
}
|
|
274
|
+
return null;
|
|
275
|
+
}
|
|
276
|
+
/** `realpath` of the nearest existing ancestor, with the missing tail re-appended. */
|
|
277
|
+
function realpathOfNearest(target, realFs) {
|
|
278
|
+
let current = path.resolve(target);
|
|
279
|
+
let tail = [];
|
|
280
|
+
for (;;) {
|
|
281
|
+
try {
|
|
282
|
+
return path.join(realFs.realpathSync.native(current), ...tail);
|
|
283
|
+
}
|
|
284
|
+
catch {
|
|
285
|
+
const parent = path.dirname(current);
|
|
286
|
+
if (parent === current)
|
|
287
|
+
return path.resolve(target);
|
|
288
|
+
tail = [path.basename(current), ...tail];
|
|
289
|
+
current = parent;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
211
293
|
/**
|
|
212
294
|
* The real module object of a Node built-in: `process.getBuiltinModule`
|
|
213
295
|
* (Node 20.16+), or `require` in CommonJS on older Node.
|