@aiwg/cli 2026.7.20 → 2026.7.23

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 (62) hide show
  1. package/README.md +18 -7
  2. package/dist/src/api/index.d.ts +2 -0
  3. package/dist/src/api/index.js +2 -0
  4. package/dist/src/artifacts/browser-export.js +7 -0
  5. package/dist/src/artifacts/citation-parser.js +96 -35
  6. package/dist/src/artifacts/index-builder.js +54 -17
  7. package/dist/src/artifacts/state-transfer.js +27 -0
  8. package/dist/src/artifacts/stats.js +8 -0
  9. package/dist/src/cli/cli-extension-loader.js +73 -0
  10. package/dist/src/cli/handlers/index.js +3 -1
  11. package/dist/src/cli/handlers/sessions.js +1265 -0
  12. package/dist/src/cli/handlers/skill-lint.js +49 -45
  13. package/dist/src/cli/handlers/use.js +143 -60
  14. package/dist/src/cli/handlers/utilities.js +22 -8
  15. package/dist/src/cli/skill-usage.js +146 -24
  16. package/dist/src/config/aiwg-config.js +12 -0
  17. package/dist/src/config/cli.js +16 -3
  18. package/dist/src/extensions/commands/definitions.js +29 -0
  19. package/dist/src/extensions/manifest.js +29 -0
  20. package/dist/src/security/threat-assessment-config.js +296 -0
  21. package/dist/src/sessions/adapters/claude.js +385 -0
  22. package/dist/src/sessions/adapters/codex.js +548 -0
  23. package/dist/src/sessions/adapters/copilot.js +226 -0
  24. package/dist/src/sessions/adapters/cursor.js +528 -0
  25. package/dist/src/sessions/adapters/factory.js +386 -0
  26. package/dist/src/sessions/adapters/generic.js +225 -0
  27. package/dist/src/sessions/adapters/hermes.js +341 -0
  28. package/dist/src/sessions/adapters/openclaw.js +381 -0
  29. package/dist/src/sessions/adapters/opencode.js +454 -0
  30. package/dist/src/sessions/adapters/openhuman.js +315 -0
  31. package/dist/src/sessions/adapters/warp.js +160 -0
  32. package/dist/src/sessions/adapters/windsurf.js +212 -0
  33. package/dist/src/sessions/batch-contracts.js +121 -0
  34. package/dist/src/sessions/batch-import.js +265 -0
  35. package/dist/src/sessions/candidates.js +210 -0
  36. package/dist/src/sessions/contracts.js +337 -0
  37. package/dist/src/sessions/discovery.js +51 -0
  38. package/dist/src/sessions/fixtures.js +12 -0
  39. package/dist/src/sessions/import-lease.js +152 -0
  40. package/dist/src/sessions/importer.js +464 -0
  41. package/dist/src/sessions/index.js +31 -0
  42. package/dist/src/sessions/knowledge-shard.js +61 -0
  43. package/dist/src/sessions/optional-backends.js +238 -0
  44. package/dist/src/sessions/origin.js +117 -0
  45. package/dist/src/sessions/policy.js +192 -0
  46. package/dist/src/sessions/ports.js +2 -0
  47. package/dist/src/sessions/promotion.js +367 -0
  48. package/dist/src/sessions/readers.js +176 -0
  49. package/dist/src/sessions/repository.js +1892 -0
  50. package/dist/src/sessions/timeline.js +148 -0
  51. package/dist/src/sessions/workspace-discovery.js +319 -0
  52. package/dist/src/skills/adapters/agent-skills.js +59 -0
  53. package/dist/src/skills/adapters/local.js +19 -1
  54. package/dist/src/skills/agent-skills.js +249 -0
  55. package/dist/src/skills/cli.js +463 -7
  56. package/dist/src/skills/deployer.js +554 -0
  57. package/dist/src/skills/doctor.js +105 -0
  58. package/dist/src/skills/exporter.js +382 -0
  59. package/dist/src/skills/importer.js +921 -0
  60. package/dist/src/skills/registry.js +19 -0
  61. package/dist/src/skills/validator.js +323 -0
  62. package/package.json +2 -2
@@ -0,0 +1,296 @@
1
+ export const THREAT_ASSESSMENT_MODES = ['off', 'audit', 'enforce'];
2
+ export const THREAT_ASSESSMENT_SEVERITIES = [
3
+ 'informational',
4
+ 'low',
5
+ 'moderate',
6
+ 'high',
7
+ 'critical',
8
+ ];
9
+ export const THREAT_ASSESSMENT_SURFACES = [
10
+ 'issue-title',
11
+ 'issue-body',
12
+ 'issue-comment',
13
+ 'pull-request-title',
14
+ 'pull-request-body',
15
+ 'pull-request-diff-summary',
16
+ 'review-comment',
17
+ 'release-note',
18
+ 'handoff',
19
+ 'outbound-maintainer-comment',
20
+ ];
21
+ const BUILTIN_PROFILES = new Set(['trusted', 'audit', 'balanced', 'strict', 'high-assurance']);
22
+ const BUILTIN_RULE_PACKS = new Set([
23
+ 'aiwg:all',
24
+ 'aiwg:prompt-injection',
25
+ 'aiwg:supply-chain',
26
+ 'aiwg:credential-protection',
27
+ ]);
28
+ const BUILTIN_RULE_IDS = new Set([
29
+ 'instruction-override',
30
+ 'sensitive-file-target',
31
+ 'third-party-execution',
32
+ 'floating-version',
33
+ 'credential-or-env-probing',
34
+ 'pressure-without-evidence',
35
+ 'unverifiable-authority-claim',
36
+ 'security-framing-conflict',
37
+ ]);
38
+ function objectValue(value) {
39
+ return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
40
+ }
41
+ function stringArray(value) {
42
+ return Array.isArray(value) && value.every(item => typeof item === 'string');
43
+ }
44
+ function validateStatement(statement, where, errors) {
45
+ if (!objectValue(statement)) {
46
+ errors.push(`${where}: must be an object`);
47
+ return;
48
+ }
49
+ if (typeof statement.id !== 'string' || !/^[a-z][a-z0-9-]*$/.test(statement.id)) {
50
+ errors.push(`${where}.id: must be a kebab-case identifier`);
51
+ }
52
+ if (!['suppress', 'set-severity'].includes(String(statement.effect))) {
53
+ errors.push(`${where}.effect: must be suppress or set-severity`);
54
+ }
55
+ if (typeof statement.reason !== 'string' || !statement.reason.trim()) {
56
+ errors.push(`${where}.reason: must be a non-empty string`);
57
+ }
58
+ if (statement.signals !== undefined && !stringArray(statement.signals)) {
59
+ errors.push(`${where}.signals: must be an array of strings`);
60
+ }
61
+ if (statement.severity !== undefined
62
+ && !THREAT_ASSESSMENT_SEVERITIES.includes(statement.severity)) {
63
+ errors.push(`${where}.severity: unknown severity`);
64
+ }
65
+ if (statement.when !== undefined && !objectValue(statement.when)) {
66
+ errors.push(`${where}.when: must be an object`);
67
+ }
68
+ if (statement.riskAcceptance !== undefined) {
69
+ if (!objectValue(statement.riskAcceptance)
70
+ || typeof statement.riskAcceptance.acceptedBy !== 'string'
71
+ || typeof statement.riskAcceptance.rationale !== 'string') {
72
+ errors.push(`${where}.riskAcceptance: requires acceptedBy and rationale`);
73
+ }
74
+ }
75
+ if (statement.effect === 'suppress') {
76
+ if (!stringArray(statement.signals) || statement.signals.length === 0) {
77
+ errors.push(`${where}.signals: suppress statements must name at least one signal`);
78
+ }
79
+ const when = objectValue(statement.when) ? statement.when : undefined;
80
+ if (!when || !Object.values(when).some(value => Array.isArray(value) && value.length > 0)) {
81
+ errors.push(`${where}.when: suppress statements require at least one narrow condition`);
82
+ }
83
+ if (!objectValue(statement.riskAcceptance)
84
+ || typeof statement.riskAcceptance.acceptedBy !== 'string'
85
+ || typeof statement.riskAcceptance.rationale !== 'string') {
86
+ errors.push(`${where}.riskAcceptance: suppress statements require acceptedBy and rationale`);
87
+ }
88
+ }
89
+ if (statement.effect === 'set-severity'
90
+ && !THREAT_ASSESSMENT_SEVERITIES.includes(statement.severity)) {
91
+ errors.push(`${where}.severity: set-severity requires a known severity`);
92
+ }
93
+ }
94
+ /**
95
+ * Fail-closed validation for project threat-assessment policy. The evaluator
96
+ * performs the same checks at runtime; config loading validates first so a
97
+ * malformed policy never degrades into the default or off mode.
98
+ */
99
+ export function validateThreatAssessmentConfig(value) {
100
+ const errors = [];
101
+ if (value === undefined || value === null)
102
+ return errors;
103
+ if (!objectValue(value))
104
+ return ['security.threatAssessment: must be an object'];
105
+ if (value.schemaVersion !== undefined && value.schemaVersion !== '1') {
106
+ errors.push("security.threatAssessment.schemaVersion: must be '1'");
107
+ }
108
+ if (value.mode !== undefined
109
+ && !THREAT_ASSESSMENT_MODES.includes(value.mode)) {
110
+ errors.push('security.threatAssessment.mode: must be off, audit, or enforce');
111
+ }
112
+ if (value.defaultProfile !== undefined && typeof value.defaultProfile !== 'string') {
113
+ errors.push('security.threatAssessment.defaultProfile: must be a string');
114
+ }
115
+ if (value.surfaces !== undefined) {
116
+ if (!objectValue(value.surfaces))
117
+ errors.push('security.threatAssessment.surfaces: must be an object');
118
+ else {
119
+ for (const [surface, entry] of Object.entries(value.surfaces)) {
120
+ const where = `security.threatAssessment.surfaces.${surface}`;
121
+ if (!THREAT_ASSESSMENT_SURFACES.includes(surface)) {
122
+ errors.push(`${where}: unknown surface`);
123
+ }
124
+ if (!objectValue(entry)) {
125
+ errors.push(`${where}: must be an object`);
126
+ continue;
127
+ }
128
+ if (entry.mode !== undefined
129
+ && !THREAT_ASSESSMENT_MODES.includes(entry.mode)) {
130
+ errors.push(`${where}.mode: must be off, audit, or enforce`);
131
+ }
132
+ if (entry.profile !== undefined && typeof entry.profile !== 'string') {
133
+ errors.push(`${where}.profile: must be a string`);
134
+ }
135
+ }
136
+ }
137
+ }
138
+ const rulePacks = objectValue(value.rulePacks) ? value.rulePacks : {};
139
+ if (value.rulePacks !== undefined && !objectValue(value.rulePacks)) {
140
+ errors.push('security.threatAssessment.rulePacks: must be an object');
141
+ }
142
+ for (const [name, pack] of Object.entries(rulePacks)) {
143
+ const where = `security.threatAssessment.rulePacks.${name}`;
144
+ if (name.startsWith('aiwg:'))
145
+ errors.push(`${where}: cannot shadow a built-in rule pack`);
146
+ if (!objectValue(pack) || !Array.isArray(pack.rules)) {
147
+ errors.push(`${where}.rules: must be an array`);
148
+ continue;
149
+ }
150
+ if (typeof pack.version !== 'string' || !pack.version.trim()) {
151
+ errors.push(`${where}.version: must be a non-empty string`);
152
+ }
153
+ const ids = new Set();
154
+ pack.rules.forEach((rule, index) => {
155
+ const ruleWhere = `${where}.rules[${index}]`;
156
+ if (!objectValue(rule)) {
157
+ errors.push(`${ruleWhere}: must be an object`);
158
+ return;
159
+ }
160
+ if (typeof rule.id !== 'string' || !/^[a-z][a-z0-9-]*$/.test(rule.id)) {
161
+ errors.push(`${ruleWhere}.id: must be a kebab-case identifier`);
162
+ }
163
+ else if (BUILTIN_RULE_IDS.has(rule.id)) {
164
+ errors.push(`${ruleWhere}.id: cannot shadow built-in rule '${rule.id}'`);
165
+ }
166
+ else if (ids.has(rule.id)) {
167
+ errors.push(`${ruleWhere}.id: duplicate '${rule.id}'`);
168
+ }
169
+ else
170
+ ids.add(rule.id);
171
+ if (!THREAT_ASSESSMENT_SEVERITIES.includes(rule.severity)) {
172
+ errors.push(`${ruleWhere}.severity: unknown severity`);
173
+ }
174
+ for (const dimension of ['likelihood', 'impact']) {
175
+ const dimensionValue = rule[dimension];
176
+ if (dimensionValue !== undefined
177
+ && (!Number.isInteger(dimensionValue) || Number(dimensionValue) < 1 || Number(dimensionValue) > 5)) {
178
+ errors.push(`${ruleWhere}.${dimension}: must be an integer from 1 to 5`);
179
+ }
180
+ }
181
+ if (!stringArray(rule.patterns) || rule.patterns.length === 0) {
182
+ errors.push(`${ruleWhere}.patterns: must be a non-empty array of strings`);
183
+ }
184
+ else {
185
+ rule.patterns.forEach((pattern, patternIndex) => {
186
+ const patternWhere = `${ruleWhere}.patterns[${patternIndex}]`;
187
+ if (!pattern || pattern.length > 500) {
188
+ errors.push(`${patternWhere}: must be 1-500 characters`);
189
+ return;
190
+ }
191
+ try {
192
+ new RegExp(pattern, 'imu');
193
+ }
194
+ catch (error) {
195
+ errors.push(`${patternWhere}: invalid regular expression (${error.message})`);
196
+ }
197
+ if (/\\[1-9]/.test(pattern) || /\(\?<([=!])/.test(pattern)
198
+ || /\([^)]*[+*][^)]*\)[+*{]/.test(pattern) || /(?:\.\*){2,}/.test(pattern)) {
199
+ errors.push(`${patternWhere}: unsafe regex construct (backreference, lookbehind, or nested unbounded quantifier)`);
200
+ }
201
+ });
202
+ }
203
+ });
204
+ }
205
+ const profiles = objectValue(value.profiles) ? value.profiles : {};
206
+ if (value.profiles !== undefined && !objectValue(value.profiles)) {
207
+ errors.push('security.threatAssessment.profiles: must be an object');
208
+ }
209
+ const visit = (name, stack) => {
210
+ if (stack.includes(name)) {
211
+ errors.push(`security.threatAssessment.profiles.${name}.extends: cyclic inheritance (${[...stack, name].join(' -> ')})`);
212
+ return;
213
+ }
214
+ const profile = profiles[name];
215
+ if (!objectValue(profile))
216
+ return;
217
+ const parents = profile.extends ?? [];
218
+ if (!stringArray(parents)) {
219
+ errors.push(`security.threatAssessment.profiles.${name}.extends: must be an array of strings`);
220
+ return;
221
+ }
222
+ for (const rawParent of parents) {
223
+ const parent = rawParent.replace(/^aiwg:/, '');
224
+ if (!BUILTIN_PROFILES.has(parent) && !(parent in profiles)) {
225
+ errors.push(`security.threatAssessment.profiles.${name}.extends: unknown profile '${rawParent}'`);
226
+ }
227
+ else if (parent in profiles)
228
+ visit(parent, [...stack, name]);
229
+ }
230
+ };
231
+ for (const [name, profile] of Object.entries(profiles)) {
232
+ const where = `security.threatAssessment.profiles.${name}`;
233
+ if (BUILTIN_PROFILES.has(name) || name.startsWith('aiwg:')) {
234
+ errors.push(`${where}: cannot shadow a built-in profile`);
235
+ }
236
+ if (!objectValue(profile)) {
237
+ errors.push(`${where}: must be an object`);
238
+ continue;
239
+ }
240
+ if (profile.mode !== undefined
241
+ && !THREAT_ASSESSMENT_MODES.includes(profile.mode)) {
242
+ errors.push(`${where}.mode: must be off, audit, or enforce`);
243
+ }
244
+ if (profile.version !== undefined && (typeof profile.version !== 'string' || !profile.version.trim())) {
245
+ errors.push(`${where}.version: must be a non-empty string`);
246
+ }
247
+ if (profile.ruleSets !== undefined && !stringArray(profile.ruleSets)) {
248
+ errors.push(`${where}.ruleSets: must be an array of strings`);
249
+ }
250
+ else {
251
+ for (const pack of (profile.ruleSets ?? [])) {
252
+ if (!BUILTIN_RULE_PACKS.has(pack) && !(pack in rulePacks)) {
253
+ errors.push(`${where}.ruleSets: unavailable rule pack '${pack}'`);
254
+ }
255
+ }
256
+ }
257
+ if (profile.thresholds !== undefined) {
258
+ if (!objectValue(profile.thresholds))
259
+ errors.push(`${where}.thresholds: must be an object`);
260
+ else {
261
+ for (const [action, severity] of Object.entries(profile.thresholds)) {
262
+ if (!['flag', 'requireAuthorization', 'reject'].includes(action)
263
+ || !THREAT_ASSESSMENT_SEVERITIES.includes(severity)) {
264
+ errors.push(`${where}.thresholds.${action}: unknown threshold or severity`);
265
+ }
266
+ }
267
+ }
268
+ }
269
+ if (profile.statements !== undefined) {
270
+ if (!Array.isArray(profile.statements))
271
+ errors.push(`${where}.statements: must be an array`);
272
+ else
273
+ profile.statements.forEach((statement, index) => validateStatement(statement, `${where}.statements[${index}]`, errors));
274
+ }
275
+ visit(name, []);
276
+ }
277
+ if (value.statements !== undefined) {
278
+ if (!Array.isArray(value.statements))
279
+ errors.push('security.threatAssessment.statements: must be an array');
280
+ else
281
+ value.statements.forEach((statement, index) => validateStatement(statement, `security.threatAssessment.statements[${index}]`, errors));
282
+ }
283
+ const defaultProfile = typeof value.defaultProfile === 'string' ? value.defaultProfile : 'balanced';
284
+ if (!BUILTIN_PROFILES.has(defaultProfile) && !(defaultProfile in profiles)) {
285
+ errors.push(`security.threatAssessment.defaultProfile: unknown profile '${defaultProfile}'`);
286
+ }
287
+ return Array.from(new Set(errors));
288
+ }
289
+ export function defaultThreatAssessmentConfig() {
290
+ return {
291
+ schemaVersion: '1',
292
+ mode: 'enforce',
293
+ defaultProfile: 'balanced',
294
+ };
295
+ }
296
+ //# sourceMappingURL=threat-assessment-config.js.map
@@ -0,0 +1,385 @@
1
+ import { opendir } from 'node:fs/promises';
2
+ import { basename, extname, resolve } from 'node:path';
3
+ import { z } from 'zod';
4
+ import { SessionContractError, assertSupportedSchemaMajor, sha256, } from '../contracts.js';
5
+ import { redactSourceLocator } from '../discovery.js';
6
+ import { readBoundedJsonLines, streamBoundedJsonLines, } from '../readers.js';
7
+ export const CLAUDE_ADAPTER_VERSION = '1.0.0';
8
+ export const CLAUDE_TRANSCRIPT_SCHEMA_VERSION = '1.0.0';
9
+ const ClaudeRecordSchema = z.object({
10
+ type: z.string().min(1),
11
+ subtype: z.string().optional(),
12
+ uuid: z.string().min(1).optional(),
13
+ parentUuid: z.string().min(1).nullable().optional(),
14
+ sessionId: z.string().min(1).optional(),
15
+ session_id: z.string().min(1).optional(),
16
+ timestamp: z.string().datetime({ offset: true }).optional(),
17
+ cwd: z.string().optional(),
18
+ gitBranch: z.string().optional(),
19
+ parentSessionId: z.string().min(1).optional(),
20
+ isSidechain: z.boolean().optional(),
21
+ agentId: z.string().min(1).optional(),
22
+ slug: z.string().min(1).optional(),
23
+ version: z.string().optional(),
24
+ schemaVersion: z.string().optional(),
25
+ message: z.object({
26
+ id: z.string().min(1).optional(),
27
+ role: z.string().min(1).optional(),
28
+ content: z.union([z.string(), z.array(z.unknown())]).optional(),
29
+ }).passthrough().optional(),
30
+ }).passthrough();
31
+ const ClaudeHookSchema = z.object({
32
+ session_id: z.string().min(1),
33
+ transcript_path: z.string().min(1),
34
+ cwd: z.string().min(1),
35
+ hook_event_name: z.string().min(1),
36
+ permission_mode: z.string().optional(),
37
+ source: z.enum(['startup', 'resume', 'clear', 'compact']).optional(),
38
+ model: z.string().optional(),
39
+ reason: z.string().optional(),
40
+ schemaVersion: z.string().optional(),
41
+ timestamp: z.string().datetime({ offset: true }).optional(),
42
+ }).passthrough();
43
+ export class ClaudeSessionAdapter {
44
+ limits;
45
+ discoveryLimits;
46
+ provider = 'claude';
47
+ adapterVersion = CLAUDE_ADAPTER_VERSION;
48
+ disposition = 'implemented';
49
+ supportedOperations = ['discover', 'inspect', 'stream'];
50
+ acquisitionModes = ['jsonl', 'hook'];
51
+ constructor(limits, discoveryLimits = { maxDepth: 8, maxFiles: 10_000 }) {
52
+ this.limits = limits;
53
+ this.discoveryLimits = discoveryLimits;
54
+ }
55
+ async *discover(scope) {
56
+ if (scope.allowedRoots.length === 0) {
57
+ throw new SessionContractError('SOURCE_NOT_AUTHORIZED', 'Claude discovery requires an explicitly authorized projects or hook root');
58
+ }
59
+ let emitted = 0;
60
+ for (const root of [...scope.allowedRoots].sort()) {
61
+ for await (const locator of discoverJsonl(resolve(root), this.discoveryLimits.maxDepth)) {
62
+ emitted += 1;
63
+ if (emitted > this.discoveryLimits.maxFiles) {
64
+ throw new SessionContractError('RESOURCE_LIMIT_EXCEEDED', 'Claude source discovery exceeded the authorized file limit');
65
+ }
66
+ yield {
67
+ provider: 'claude',
68
+ locator,
69
+ locatorClass: isHookLocator(locator) ? 'claude-hook-jsonl' : 'claude-transcript-jsonl',
70
+ };
71
+ }
72
+ }
73
+ }
74
+ async inspect(source) {
75
+ const parsed = await this.readSource(source);
76
+ return {
77
+ sourceSchemaVersion: parsed.schemaVersion,
78
+ consistency: parsed.consistency,
79
+ operationalState: 'available',
80
+ };
81
+ }
82
+ async *stream(source, cursor) {
83
+ const start = parseRecordCursor(cursor?.value);
84
+ const input = await streamBoundedJsonLines({
85
+ selectedPath: source.locator,
86
+ allowedRoots: source.authorizedScope.allowedRoots,
87
+ }, { consistency: 'provisional', limits: this.limits });
88
+ const hookSource = isHookLocator(source.locator);
89
+ let outputIndex = 0;
90
+ let sawRecord = false;
91
+ let schemaVersion = null;
92
+ for await (const line of input) {
93
+ sawRecord = true;
94
+ const value = line.value;
95
+ const currentSchema = typeof value.schemaVersion === 'string'
96
+ ? value.schemaVersion : CLAUDE_TRANSCRIPT_SCHEMA_VERSION;
97
+ if (schemaVersion && currentSchema !== schemaVersion) {
98
+ throw new SessionContractError('SCHEMA_DRIFT', 'Claude source declares mixed schema versions');
99
+ }
100
+ schemaVersion = currentSchema;
101
+ assertSupportedSchemaMajor(schemaVersion);
102
+ const normalized = hookSource || isHookRecord(line.value)
103
+ ? normalizeHooks([line]).records
104
+ : normalizeTranscript([line], source.locator).records;
105
+ for (const record of normalized) {
106
+ if (outputIndex++ >= start)
107
+ yield record;
108
+ }
109
+ }
110
+ if (!sawRecord && !input.incompleteTail) {
111
+ throw new SessionContractError('MALFORMED_SOURCE', 'Claude JSONL source is empty');
112
+ }
113
+ }
114
+ async readSource(source) {
115
+ const result = await readBoundedJsonLines({
116
+ selectedPath: source.locator,
117
+ allowedRoots: source.authorizedScope.allowedRoots,
118
+ }, { consistency: 'provisional', limits: this.limits });
119
+ if (result.records.length === 0 && !result.incompleteTail) {
120
+ throw new SessionContractError('MALFORMED_SOURCE', 'Claude JSONL source is empty');
121
+ }
122
+ const hookSource = isHookLocator(source.locator)
123
+ || result.records.some((record) => isHookRecord(record.value));
124
+ const normalized = hookSource
125
+ ? normalizeHooks(result.records)
126
+ : normalizeTranscript(result.records, source.locator);
127
+ const schemaVersion = declaredSchemaVersion(result.records);
128
+ assertSupportedSchemaMajor(schemaVersion);
129
+ return {
130
+ records: normalized.records,
131
+ schemaVersion,
132
+ consistency: normalized.complete ? 'complete' : 'provisional',
133
+ };
134
+ }
135
+ }
136
+ function normalizeTranscript(records, locator) {
137
+ const filenameSessionId = basename(locator, extname(locator));
138
+ const output = [];
139
+ for (const record of records) {
140
+ const parsed = ClaudeRecordSchema.safeParse(record.value);
141
+ if (!parsed.success) {
142
+ throw new SessionContractError('MALFORMED_SOURCE', 'Claude transcript record is malformed');
143
+ }
144
+ const value = parsed.data;
145
+ const embeddedSessionId = value.sessionId ?? value.session_id;
146
+ const nativeSessionId = embeddedSessionId ?? filenameSessionId;
147
+ const blocks = messageBlocks(value);
148
+ if (blocks.length === 0) {
149
+ output.push(providerRecord(value, record, nativeSessionId, filenameSessionId, {
150
+ kind: `claude.${value.type}`,
151
+ text: '',
152
+ opaque: true,
153
+ unknownFields: unknownFields(value, TRANSCRIPT_KEYS),
154
+ }));
155
+ continue;
156
+ }
157
+ for (const [blockIndex, block] of blocks.entries()) {
158
+ output.push(providerRecord(value, record, nativeSessionId, filenameSessionId, {
159
+ kind: block.kind,
160
+ text: block.text,
161
+ blockIndex,
162
+ blockNativeId: block.nativeId,
163
+ opaque: block.opaque,
164
+ unknownFields: block.unknownFields,
165
+ }));
166
+ }
167
+ }
168
+ return { records: output, complete: false };
169
+ }
170
+ function normalizeHooks(records) {
171
+ const output = [];
172
+ let complete = false;
173
+ for (const record of records) {
174
+ const parsed = ClaudeHookSchema.safeParse(record.value);
175
+ if (!parsed.success) {
176
+ throw new SessionContractError('MALFORMED_SOURCE', 'Claude lifecycle hook record is malformed');
177
+ }
178
+ const hook = parsed.data;
179
+ complete ||= hook.hook_event_name === 'SessionEnd';
180
+ output.push({
181
+ nativeSessionId: hook.session_id,
182
+ nativeEventId: hookNativeId(hook, record.sequence),
183
+ sequence: record.sequence,
184
+ kind: 'lifecycle-hook',
185
+ role: 'system',
186
+ participant: 'claude',
187
+ model: hook.model,
188
+ occurredAt: hook.timestamp,
189
+ activityBoundary: hook.hook_event_name === 'SessionEnd'
190
+ ? 'end'
191
+ : hook.source === 'resume'
192
+ ? 'resume'
193
+ : hook.source === 'compact'
194
+ ? 'continuation'
195
+ : undefined,
196
+ activityBoundaryBasis: hook.hook_event_name === 'SessionEnd' || hook.source
197
+ ? `claude-hook:${hook.hook_event_name}:${hook.source ?? 'none'}`
198
+ : undefined,
199
+ activityBoundaryConfidence: hook.hook_event_name === 'SessionEnd' || hook.source
200
+ ? 'high'
201
+ : undefined,
202
+ text: '',
203
+ rawReference: { locatorClass: 'claude-hook-jsonl', offset: record.byteOffset },
204
+ extensions: {
205
+ hookEventName: hook.hook_event_name,
206
+ ...(hook.hook_event_name === 'SessionEnd' ? { lifecycle: 'complete' } : {}),
207
+ startSource: hook.source,
208
+ model: hook.model,
209
+ reason: hook.reason,
210
+ permissionMode: hook.permission_mode,
211
+ workspace: { cwdClass: '<workspace>', transcript: redactSourceLocator(hook.transcript_path) },
212
+ provenance: { acquisition: 'claude-hook', schema: CLAUDE_TRANSCRIPT_SCHEMA_VERSION },
213
+ unknownFields: unknownFields(hook, HOOK_KEYS),
214
+ },
215
+ });
216
+ }
217
+ return { records: output, complete };
218
+ }
219
+ function providerRecord(value, record, nativeSessionId, sourceArtifactSessionId, block) {
220
+ const nativeBase = value.uuid ?? value.message?.id;
221
+ const nativeEventId = block.blockNativeId
222
+ ?? (nativeBase ? `${nativeBase}:${block.blockIndex ?? 0}` : undefined);
223
+ return {
224
+ nativeSessionId,
225
+ nativeEventId,
226
+ sequence: record.sequence * 1_000 + (block.blockIndex ?? 0),
227
+ kind: block.kind,
228
+ role: value.message?.role,
229
+ occurredAt: value.timestamp,
230
+ text: block.text,
231
+ rawReference: { locatorClass: 'claude-transcript-jsonl', offset: record.byteOffset },
232
+ extensions: {
233
+ transcriptType: value.type,
234
+ transcriptSubtype: value.subtype,
235
+ parentUuid: value.parentUuid,
236
+ productVersion: value.version,
237
+ workspace: { cwdClass: value.cwd ? '<workspace>' : undefined, gitBranch: value.gitBranch },
238
+ provenance: { acquisition: 'claude-transcript', schema: CLAUDE_TRANSCRIPT_SCHEMA_VERSION },
239
+ transcriptFamily: {
240
+ sourceArtifactSessionId,
241
+ nativeSessionId,
242
+ identityRelation: nativeSessionId === sourceArtifactSessionId ? 'self' : 'related',
243
+ parentUuid: value.parentUuid,
244
+ parentSessionId: value.parentSessionId,
245
+ gitBranch: value.gitBranch,
246
+ continuation: record.sequence > 0 && value.parentUuid !== null,
247
+ subagent: value.isSidechain === true || Boolean(value.agentId),
248
+ agentId: value.agentId,
249
+ agentSlug: value.slug,
250
+ },
251
+ opaque: block.opaque,
252
+ unknownFields: {
253
+ ...unknownFields(value, TRANSCRIPT_KEYS),
254
+ ...block.unknownFields,
255
+ },
256
+ },
257
+ };
258
+ }
259
+ function messageBlocks(value) {
260
+ const content = value.message?.content;
261
+ if (typeof content === 'string') {
262
+ return [{ kind: 'message', text: content, opaque: false, unknownFields: {} }];
263
+ }
264
+ if (!Array.isArray(content))
265
+ return [];
266
+ return content.map((input) => {
267
+ if (!input || typeof input !== 'object' || Array.isArray(input)) {
268
+ return { kind: 'claude.unknown-block', text: '', opaque: true, unknownFields: { value: input } };
269
+ }
270
+ const block = input;
271
+ const type = typeof block.type === 'string' ? block.type : 'unknown';
272
+ if (type === 'text' && typeof block.text === 'string') {
273
+ return {
274
+ kind: 'message', text: block.text, opaque: false,
275
+ unknownFields: unknownFields(block, new Set(['type', 'text'])),
276
+ };
277
+ }
278
+ if (type === 'tool_use') {
279
+ return {
280
+ kind: 'tool-call',
281
+ text: typeof block.name === 'string' ? block.name : '',
282
+ nativeId: typeof block.id === 'string' ? block.id : undefined,
283
+ opaque: false,
284
+ unknownFields: unknownFields(block, new Set(['type', 'id', 'name', 'input'])),
285
+ };
286
+ }
287
+ if (type === 'tool_result') {
288
+ return {
289
+ kind: 'tool-result',
290
+ text: extractToolResultText(block.content),
291
+ nativeId: typeof block.tool_use_id === 'string' ? block.tool_use_id : undefined,
292
+ opaque: false,
293
+ unknownFields: unknownFields(block, new Set(['type', 'tool_use_id', 'content', 'is_error'])),
294
+ };
295
+ }
296
+ return {
297
+ kind: `claude.${type}`,
298
+ text: '',
299
+ opaque: true,
300
+ unknownFields: { ...block },
301
+ };
302
+ });
303
+ }
304
+ function extractToolResultText(value) {
305
+ if (typeof value === 'string')
306
+ return value;
307
+ if (!Array.isArray(value))
308
+ return '';
309
+ return value
310
+ .filter((item) => Boolean(item) && typeof item === 'object' && !Array.isArray(item))
311
+ .map((item) => typeof item.text === 'string' ? item.text : '')
312
+ .filter(Boolean)
313
+ .join('\n');
314
+ }
315
+ async function* discoverJsonl(root, maxDepth) {
316
+ const pending = [{ path: root, depth: 0 }];
317
+ while (pending.length > 0) {
318
+ const current = pending.shift();
319
+ let directory;
320
+ try {
321
+ directory = await opendir(current.path);
322
+ }
323
+ catch {
324
+ throw new SessionContractError('SOURCE_NOT_AUTHORIZED', 'authorized Claude source root is inaccessible');
325
+ }
326
+ const childDirectories = [];
327
+ const files = [];
328
+ for await (const entry of directory) {
329
+ const path = resolve(current.path, entry.name);
330
+ if (entry.isSymbolicLink())
331
+ continue;
332
+ if (entry.isDirectory() && current.depth < maxDepth)
333
+ childDirectories.push(path);
334
+ else if (entry.isFile() && entry.name.endsWith('.jsonl'))
335
+ files.push(path);
336
+ }
337
+ for (const file of files.sort())
338
+ yield file;
339
+ for (const path of childDirectories.sort())
340
+ pending.push({ path, depth: current.depth + 1 });
341
+ }
342
+ }
343
+ function declaredSchemaVersion(records) {
344
+ const declared = records
345
+ .map((record) => {
346
+ if (!record.value || typeof record.value !== 'object' || Array.isArray(record.value))
347
+ return undefined;
348
+ return record.value.schemaVersion;
349
+ })
350
+ .find((value) => typeof value === 'string');
351
+ return declared ?? CLAUDE_TRANSCRIPT_SCHEMA_VERSION;
352
+ }
353
+ function parseRecordCursor(value) {
354
+ if (value === undefined || value === '')
355
+ return 0;
356
+ if (!/^\d+$/.test(value))
357
+ throw new SessionContractError('SCHEMA_DRIFT', 'Claude record cursor is invalid');
358
+ return Number(value);
359
+ }
360
+ function hookNativeId(hook, sequence) {
361
+ return sha256([
362
+ hook.session_id, hook.hook_event_name, hook.source ?? '',
363
+ hook.timestamp ?? '', hook.reason ?? '', sequence,
364
+ ].join('\0'));
365
+ }
366
+ function isHookLocator(locator) {
367
+ return /\.hooks?\.jsonl$/i.test(locator);
368
+ }
369
+ function isHookRecord(value) {
370
+ return Boolean(value) && typeof value === 'object' && !Array.isArray(value)
371
+ && typeof value.hook_event_name === 'string';
372
+ }
373
+ function unknownFields(value, known) {
374
+ return Object.fromEntries(Object.entries(value).filter(([key]) => !known.has(key)));
375
+ }
376
+ const TRANSCRIPT_KEYS = new Set([
377
+ 'type', 'subtype', 'uuid', 'parentUuid', 'sessionId', 'session_id',
378
+ 'timestamp', 'cwd', 'gitBranch', 'parentSessionId', 'isSidechain',
379
+ 'agentId', 'slug', 'version', 'schemaVersion', 'message',
380
+ ]);
381
+ const HOOK_KEYS = new Set([
382
+ 'session_id', 'transcript_path', 'cwd', 'hook_event_name', 'permission_mode',
383
+ 'source', 'model', 'reason', 'schemaVersion', 'timestamp',
384
+ ]);
385
+ //# sourceMappingURL=claude.js.map