@aiwg/cli 2026.7.21 → 2026.7.24
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -3
- package/dist/src/api/index.d.ts +1 -0
- package/dist/src/api/index.js +1 -0
- package/dist/src/artifacts/browser-export.js +2 -0
- package/dist/src/artifacts/index-builder.js +44 -8
- package/dist/src/artifacts/query-engine.js +1 -1
- package/dist/src/artifacts/types.js +1 -0
- package/dist/src/cli/handlers/index.js +5 -1
- package/dist/src/cli/handlers/sessions.js +339 -40
- package/dist/src/cli/handlers/setup-manifest.js +800 -0
- package/dist/src/cli/handlers/use.js +127 -17
- package/dist/src/config/aiwg-config.js +18 -2
- package/dist/src/config/cli.js +16 -3
- package/dist/src/extensions/commands/definitions.js +99 -0
- package/dist/src/security/threat-assessment-config.js +296 -0
- package/dist/src/serve/sandbox-registry.js +34 -0
- package/dist/src/sessions/adapters/claude.js +37 -9
- package/dist/src/sessions/adapters/codex.js +38 -11
- package/dist/src/sessions/adapters/cursor.js +166 -10
- package/dist/src/sessions/adapters/factory.js +50 -9
- package/dist/src/sessions/batch-contracts.js +121 -0
- package/dist/src/sessions/batch-import.js +265 -0
- package/dist/src/sessions/contracts.js +32 -5
- package/dist/src/sessions/import-lease.js +152 -0
- package/dist/src/sessions/importer.js +163 -14
- package/dist/src/sessions/index.js +6 -0
- package/dist/src/sessions/origin.js +117 -0
- package/dist/src/sessions/readers.js +1 -1
- package/dist/src/sessions/repository.js +354 -13
- package/dist/src/sessions/timeline.js +148 -0
- package/dist/src/sessions/workspace-discovery.js +319 -0
- package/package.json +1 -1
|
@@ -1,16 +1,20 @@
|
|
|
1
|
-
import { mkdirSync } from 'node:fs';
|
|
2
|
-
import { dirname, resolve } from 'node:path';
|
|
3
|
-
import { CLAUDE_ADAPTER_VERSION, ClaudeSessionAdapter, CODEX_ADAPTER_VERSION, CodexSessionAdapter, COPILOT_ADAPTER_VERSION, CopilotSessionAdapter, CURSOR_ADAPTER_VERSION, CursorSessionAdapter, FACTORY_ADAPTER_VERSION, FactorySessionAdapter, HERMES_ADAPTER_VERSION, HermesSessionAdapter, OPENCODE_ADAPTER_VERSION, OpenCodeSessionAdapter, OPENCLAW_ADAPTER_VERSION, OpenClawSessionAdapter, OPENHUMAN_ADAPTER_VERSION, OpenHumanSessionAdapter, WARP_ADAPTER_VERSION, WarpSessionAdapter, DEVIN_DESKTOP_ADAPTER_VERSION, DevinDesktopSessionAdapter, CandidateExtractionService, GENERIC_ADAPTER_VERSION, GenericSessionInterchangeAdapter, IncrementalSessionImporter, FilesystemMemoryDestination, FilesystemPromotionDispositionCoordinator, MemoryPromotionGateway, SESSION_CONTRACT_VERSION, SESSION_PROVIDER_IDS, SessionContractError, SessionRepository, SessionSourceSchema, StructuralCandidateExtractor, resolveMemoryConsumerManifest, assertSessionProviderId, redactSourceLocator, } from '../../sessions/index.js';
|
|
1
|
+
import { existsSync, mkdirSync, realpathSync, statSync, } from 'node:fs';
|
|
2
|
+
import { dirname, isAbsolute, resolve, } from 'node:path';
|
|
3
|
+
import { CLAUDE_ADAPTER_VERSION, ClaudeSessionAdapter, CODEX_ADAPTER_VERSION, CodexSessionAdapter, COPILOT_ADAPTER_VERSION, CopilotSessionAdapter, CURSOR_ADAPTER_VERSION, CursorSessionAdapter, FACTORY_ADAPTER_VERSION, FactorySessionAdapter, HERMES_ADAPTER_VERSION, HermesSessionAdapter, OPENCODE_ADAPTER_VERSION, OpenCodeSessionAdapter, OPENCLAW_ADAPTER_VERSION, OpenClawSessionAdapter, OPENHUMAN_ADAPTER_VERSION, OpenHumanSessionAdapter, WARP_ADAPTER_VERSION, WarpSessionAdapter, DEVIN_DESKTOP_ADAPTER_VERSION, DevinDesktopSessionAdapter, CandidateExtractionService, GENERIC_ADAPTER_VERSION, GenericSessionInterchangeAdapter, IncrementalSessionImporter, ImportLeaseContentionError, FilesystemMemoryDestination, FilesystemPromotionDispositionCoordinator, MemoryPromotionGateway, SESSION_CONTRACT_VERSION, SESSION_PROVIDER_IDS, SessionContractError, SessionRepository, SessionSourceSchema, StructuralCandidateExtractor, resolveMemoryConsumerManifest, assertSessionProviderId, acquireImportLease, defaultDiscoveryManifestPath, discoverWorkspaceHistories, deriveSessionTimeline, importDiscoveryManifest, previewDiscoveryImport, publicDiscoveryManifest, readDiscoveryManifest, redactSourceLocator, sha256, parseTimelineGap, writeDiscoveryManifest, } from '../../sessions/index.js';
|
|
4
4
|
const JSON_CONTRACT_VERSION = '1.0.0';
|
|
5
5
|
const EXIT = {
|
|
6
6
|
ok: 0, usage: 2, unsupported: 3, unavailable: 4, contract: 5, storage: 6,
|
|
7
|
+
locked: 7, coverage: 8,
|
|
7
8
|
};
|
|
8
9
|
const HELP = `Usage: aiwg sessions <command> [options]
|
|
9
10
|
|
|
10
11
|
Commands:
|
|
11
12
|
sources Show all canonical provider dispositions
|
|
13
|
+
discover --workspace <path> Inventory authorized workspace histories
|
|
14
|
+
import-discovered --workspace <path> Import the exact saved manifest
|
|
12
15
|
import <file> --source-id <id> Import a supported provider JSONL source
|
|
13
16
|
list [--limit N] [--cursor N] List normalized sessions
|
|
17
|
+
timeline [--gap 30m] Report chronological activity segments
|
|
14
18
|
show <session-id> Show a session with events and tags
|
|
15
19
|
search <query> --workspace <id> Search authorized normalized content
|
|
16
20
|
extract [session-id] --workspace <id> Extract structural candidates
|
|
@@ -30,6 +34,13 @@ Options:
|
|
|
30
34
|
--json Emit the versioned JSON contract
|
|
31
35
|
--dry-run Preview a mutation without changing state
|
|
32
36
|
--db <path> Override .aiwg/sessions/catalog.sqlite
|
|
37
|
+
--manifest <path> Override the discovery manifest path
|
|
38
|
+
--provider-home <path> Override the provider home root (testing/portable homes)
|
|
39
|
+
--codex-root <path> Explicitly authorize a shared Codex sessions/export root
|
|
40
|
+
--confirm, --yes Confirm a persistent discovered batch import
|
|
41
|
+
--lock-wait-ms <n> Maximum import-lease wait (default 5000)
|
|
42
|
+
--inactivity-threshold <duration> Historical inactivity threshold (default 24h)
|
|
43
|
+
--min-coverage <0..1> Fail list/report commands below a coverage ratio
|
|
33
44
|
--provider <id> Filter list or select an import adapter
|
|
34
45
|
--consumer <id> Select a named memory consumer for promotion
|
|
35
46
|
--workspace <id>, --tag <tag>, --limit <n>, --cursor <n>
|
|
@@ -90,11 +101,15 @@ async function executeCommand(ctx, args) {
|
|
|
90
101
|
}
|
|
91
102
|
if (command === 'import')
|
|
92
103
|
return importSource(ctx, args);
|
|
104
|
+
if (command === 'discover')
|
|
105
|
+
return discoverWorkspace(ctx, args);
|
|
106
|
+
if (command === 'import-discovered')
|
|
107
|
+
return importDiscovered(ctx, args);
|
|
93
108
|
const repository = openRepository(ctx, args);
|
|
94
109
|
try {
|
|
95
110
|
switch (command) {
|
|
96
111
|
case 'list': {
|
|
97
|
-
const { workspaceId } =
|
|
112
|
+
const { workspaceId } = readAuthorizationContext(ctx, args, command, repository);
|
|
98
113
|
const limit = boundedInteger(args.values.get('--limit'), 50, 1, 500, '--limit');
|
|
99
114
|
const cursor = args.values.get('--cursor');
|
|
100
115
|
const providerInput = args.values.get('--provider');
|
|
@@ -106,7 +121,8 @@ async function executeCommand(ctx, args) {
|
|
|
106
121
|
limit,
|
|
107
122
|
cursor,
|
|
108
123
|
});
|
|
109
|
-
|
|
124
|
+
const coverage = repository.getCoverage(workspaceId);
|
|
125
|
+
const response = {
|
|
110
126
|
items: result.items,
|
|
111
127
|
page: {
|
|
112
128
|
limit,
|
|
@@ -115,11 +131,56 @@ async function executeCommand(ctx, args) {
|
|
|
115
131
|
snapshotRowid: result.snapshotRowid,
|
|
116
132
|
total: result.total,
|
|
117
133
|
},
|
|
118
|
-
|
|
134
|
+
coverage,
|
|
135
|
+
};
|
|
136
|
+
const minimum = args.values.has('--min-coverage')
|
|
137
|
+
? boundedNumber(args.values.get('--min-coverage'), 0, 0, 1, '--min-coverage')
|
|
138
|
+
: null;
|
|
139
|
+
if (minimum !== null
|
|
140
|
+
&& (coverage.coverageRatio === null || coverage.coverageRatio < minimum)) {
|
|
141
|
+
return {
|
|
142
|
+
envelope: envelope(`sessions.${command}`, 'error', response, {
|
|
143
|
+
code: 'COVERAGE_BELOW_THRESHOLD',
|
|
144
|
+
message: `session coverage is below the requested threshold ${minimum}`,
|
|
145
|
+
details: coverage,
|
|
146
|
+
}),
|
|
147
|
+
exitCode: EXIT.coverage,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
return ok(command, response);
|
|
151
|
+
}
|
|
152
|
+
case 'timeline': {
|
|
153
|
+
const { workspaceId } = readAuthorizationContext(ctx, args, command, repository);
|
|
154
|
+
const gapMs = timelineGap(args.values.get('--gap'));
|
|
155
|
+
const items = deriveSessionTimeline(repository.listTimelineInputs(workspaceId), gapMs);
|
|
156
|
+
const coverage = repository.getCoverage(workspaceId);
|
|
157
|
+
const response = {
|
|
158
|
+
schemaVersion: '1.0.0',
|
|
159
|
+
workspaceId,
|
|
160
|
+
gapMs,
|
|
161
|
+
items,
|
|
162
|
+
count: items.length,
|
|
163
|
+
coverage,
|
|
164
|
+
};
|
|
165
|
+
const minimum = args.values.has('--min-coverage')
|
|
166
|
+
? boundedNumber(args.values.get('--min-coverage'), 0, 0, 1, '--min-coverage')
|
|
167
|
+
: null;
|
|
168
|
+
if (minimum !== null
|
|
169
|
+
&& (coverage.coverageRatio === null || coverage.coverageRatio < minimum)) {
|
|
170
|
+
return {
|
|
171
|
+
envelope: envelope(`sessions.${command}`, 'error', response, {
|
|
172
|
+
code: 'COVERAGE_BELOW_THRESHOLD',
|
|
173
|
+
message: `session coverage is below the requested threshold ${minimum}`,
|
|
174
|
+
details: coverage,
|
|
175
|
+
}),
|
|
176
|
+
exitCode: EXIT.coverage,
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
return ok(command, response);
|
|
119
180
|
}
|
|
120
181
|
case 'show': {
|
|
121
182
|
const id = requiredPositional(args, 0, 'session-id');
|
|
122
|
-
const { workspaceId } =
|
|
183
|
+
const { workspaceId } = readAuthorizationContext(ctx, args, command, repository);
|
|
123
184
|
const session = repository.getSession(id, workspaceId);
|
|
124
185
|
if (!session)
|
|
125
186
|
throw new CliError('SESSION_NOT_FOUND', `session not found: ${id}`, EXIT.unavailable);
|
|
@@ -131,7 +192,7 @@ async function executeCommand(ctx, args) {
|
|
|
131
192
|
}
|
|
132
193
|
case 'search': {
|
|
133
194
|
const query = requiredPositional(args, 0, 'query');
|
|
134
|
-
const { workspaceId } =
|
|
195
|
+
const { workspaceId } = readAuthorizationContext(ctx, args, command, repository);
|
|
135
196
|
const providerInput = args.values.get('--provider');
|
|
136
197
|
const provider = providerInput ? assertSessionProviderId(providerInput) : undefined;
|
|
137
198
|
const result = repository.search({
|
|
@@ -148,6 +209,7 @@ async function executeCommand(ctx, args) {
|
|
|
148
209
|
entity: args.values.get('--entity'),
|
|
149
210
|
sensitivity: args.values.get('--sensitivity'),
|
|
150
211
|
extractionState: args.values.get('--extraction-state'),
|
|
212
|
+
controlEvents: controlEventMode(args.values.get('--control-events')),
|
|
151
213
|
limit: boundedInteger(args.values.get('--limit'), 50, 1, 500, '--limit'),
|
|
152
214
|
cursor: args.values.get('--cursor'),
|
|
153
215
|
});
|
|
@@ -158,7 +220,7 @@ async function executeCommand(ctx, args) {
|
|
|
158
220
|
});
|
|
159
221
|
}
|
|
160
222
|
case 'extract': {
|
|
161
|
-
const { workspaceId } = authorizationContext(args, command);
|
|
223
|
+
const { workspaceId } = authorizationContext(ctx, args, command);
|
|
162
224
|
const sessionId = args.positionals[0];
|
|
163
225
|
const pageSize = boundedInteger(args.values.get('--page-size'), 250, 1, 500, '--page-size');
|
|
164
226
|
const maxDocuments = args.values.has('--max-documents')
|
|
@@ -217,14 +279,14 @@ async function executeCommand(ctx, args) {
|
|
|
217
279
|
: ok(command, { items, count: items.length, durableMemoryWrites: 0, scan });
|
|
218
280
|
}
|
|
219
281
|
case 'candidates': {
|
|
220
|
-
const { workspaceId } =
|
|
282
|
+
const { workspaceId } = readAuthorizationContext(ctx, args, command, repository);
|
|
221
283
|
const state = candidateState(args.values.get('--state'));
|
|
222
284
|
return ok(command, {
|
|
223
285
|
items: repository.listCandidates(state, workspaceId),
|
|
224
286
|
});
|
|
225
287
|
}
|
|
226
288
|
case 'review': {
|
|
227
|
-
const { workspaceId } = authorizationContext(args, command);
|
|
289
|
+
const { workspaceId } = authorizationContext(ctx, args, command);
|
|
228
290
|
const candidateId = requiredPositional(args, 0, 'candidate-id');
|
|
229
291
|
const version = boundedInteger(requiredPositional(args, 1, 'version'), 1, 1, Number.MAX_SAFE_INTEGER, 'version');
|
|
230
292
|
const toState = candidateState(requiredPositional(args, 2, 'state'));
|
|
@@ -249,7 +311,7 @@ async function executeCommand(ctx, args) {
|
|
|
249
311
|
}));
|
|
250
312
|
}
|
|
251
313
|
case 'promote': {
|
|
252
|
-
const { workspaceId } = authorizationContext(args, command);
|
|
314
|
+
const { workspaceId } = authorizationContext(ctx, args, command);
|
|
253
315
|
const candidateId = requiredPositional(args, 0, 'candidate-id');
|
|
254
316
|
const version = boundedInteger(requiredPositional(args, 1, 'version'), 1, 1, Number.MAX_SAFE_INTEGER, 'version');
|
|
255
317
|
const consumer = requiredValue(args, '--consumer');
|
|
@@ -286,7 +348,7 @@ async function executeCommand(ctx, args) {
|
|
|
286
348
|
}
|
|
287
349
|
case 'tag': {
|
|
288
350
|
const id = requiredPositional(args, 0, 'session-id');
|
|
289
|
-
const { workspaceId } = authorizationContext(args, command);
|
|
351
|
+
const { workspaceId } = authorizationContext(ctx, args, command);
|
|
290
352
|
const tag = requiredPositional(args, 1, 'tag');
|
|
291
353
|
if (!/^[A-Za-z0-9][A-Za-z0-9._:-]{0,63}$/.test(tag)) {
|
|
292
354
|
throw new CliError('INVALID_TAG', 'tag must be 1-64 safe identifier characters', EXIT.usage);
|
|
@@ -302,7 +364,7 @@ async function executeCommand(ctx, args) {
|
|
|
302
364
|
}
|
|
303
365
|
case 'relocate': {
|
|
304
366
|
const sourceId = requiredPositional(args, 0, 'source-id');
|
|
305
|
-
const { workspaceId } = authorizationContext(args, command);
|
|
367
|
+
const { workspaceId } = authorizationContext(ctx, args, command);
|
|
306
368
|
const locator = requiredPositional(args, 1, 'file');
|
|
307
369
|
if (!repository.getSource(sourceId, workspaceId)) {
|
|
308
370
|
throw new CliError('SOURCE_NOT_FOUND', `source not found: ${sourceId}`, EXIT.unavailable);
|
|
@@ -314,14 +376,14 @@ async function executeCommand(ctx, args) {
|
|
|
314
376
|
return ok(command, { sourceId, redactedLocator });
|
|
315
377
|
}
|
|
316
378
|
case 'reindex': {
|
|
317
|
-
const { workspaceId } = authorizationContext(args, command);
|
|
379
|
+
const { workspaceId } = authorizationContext(ctx, args, command);
|
|
318
380
|
if (isDryRun(ctx, args))
|
|
319
381
|
return preview(command, { operation: 'reindex' });
|
|
320
382
|
repository.reindex(workspaceId);
|
|
321
383
|
return ok(command, { operation: 'reindex' });
|
|
322
384
|
}
|
|
323
385
|
case 'audit': {
|
|
324
|
-
const { workspaceId } =
|
|
386
|
+
const { workspaceId } = readAuthorizationContext(ctx, args, command, repository);
|
|
325
387
|
const limit = boundedInteger(args.values.get('--limit'), 100, 1, 500, '--limit');
|
|
326
388
|
const cursor = args.values.get('--cursor');
|
|
327
389
|
if (args.flags.has('--otel')) {
|
|
@@ -339,7 +401,7 @@ async function executeCommand(ctx, args) {
|
|
|
339
401
|
}
|
|
340
402
|
case 'delete': {
|
|
341
403
|
const id = requiredPositional(args, 0, 'session-id');
|
|
342
|
-
const { workspaceId } = authorizationContext(args, command);
|
|
404
|
+
const { workspaceId } = authorizationContext(ctx, args, command);
|
|
343
405
|
const counts = repository.deletionPreview(id, workspaceId);
|
|
344
406
|
if (counts.sessions === 0) {
|
|
345
407
|
throw new CliError('SESSION_NOT_FOUND', `session not found: ${id}`, EXIT.unavailable);
|
|
@@ -357,7 +419,7 @@ async function executeCommand(ctx, args) {
|
|
|
357
419
|
}
|
|
358
420
|
case 'restore': {
|
|
359
421
|
const id = requiredPositional(args, 0, 'session-id');
|
|
360
|
-
const { workspaceId } = authorizationContext(args, command);
|
|
422
|
+
const { workspaceId } = authorizationContext(ctx, args, command);
|
|
361
423
|
if (isDryRun(ctx, args))
|
|
362
424
|
return preview(command, { sessionId: id, wouldRestore: true });
|
|
363
425
|
if (!repository.restoreSession(id, workspaceId)) {
|
|
@@ -367,7 +429,7 @@ async function executeCommand(ctx, args) {
|
|
|
367
429
|
}
|
|
368
430
|
case 'purge': {
|
|
369
431
|
const id = requiredPositional(args, 0, 'session-id');
|
|
370
|
-
const { workspaceId } = authorizationContext(args, command);
|
|
432
|
+
const { workspaceId } = authorizationContext(ctx, args, command);
|
|
371
433
|
const completed = repository.getCompletedPurge(id, workspaceId);
|
|
372
434
|
if (completed) {
|
|
373
435
|
return ok(command, {
|
|
@@ -455,7 +517,9 @@ async function importSource(ctx, args) {
|
|
|
455
517
|
throw new CliError('UNSUPPORTED_OPERATION', `session import is not implemented for ${provider}`, EXIT.unsupported);
|
|
456
518
|
}
|
|
457
519
|
const sourceId = requiredValue(args, '--source-id');
|
|
458
|
-
const workspaceId = args.values.
|
|
520
|
+
const workspaceId = args.values.has('--workspace')
|
|
521
|
+
? normalizeWorkspaceId(ctx.cwd, args.values.get('--workspace'))
|
|
522
|
+
: 'default';
|
|
459
523
|
const isClaude = provider === 'claude';
|
|
460
524
|
const isCodex = provider === 'codex';
|
|
461
525
|
const isCopilot = provider === 'copilot';
|
|
@@ -605,22 +669,106 @@ async function importSource(ctx, args) {
|
|
|
605
669
|
if (isDryRun(ctx, args)) {
|
|
606
670
|
return preview('import', { source, wouldInspect: true, wouldPersist: false });
|
|
607
671
|
}
|
|
608
|
-
const
|
|
672
|
+
const lease = await acquireImportLease(databasePath(ctx, args), sha256(['single-source-import', sourceId, workspaceId].join('\0')), {
|
|
673
|
+
waitMs: boundedInteger(args.values.get('--lock-wait-ms'), 5_000, 0, 300_000, '--lock-wait-ms'),
|
|
674
|
+
});
|
|
609
675
|
try {
|
|
610
|
-
const
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
676
|
+
const repository = openRepository(ctx, args);
|
|
677
|
+
try {
|
|
678
|
+
const receipts = await new IncrementalSessionImporter(repository).import({
|
|
679
|
+
source, selectedSource, adapter, workspaceId, policyVersion: '1.0.0',
|
|
680
|
+
inactivityThresholdMs: inactivityThreshold(args.values.get('--inactivity-threshold')),
|
|
681
|
+
});
|
|
682
|
+
return ok('import', {
|
|
683
|
+
sourceId,
|
|
684
|
+
receipts,
|
|
685
|
+
totals: {
|
|
686
|
+
sessionsInserted: receipts.reduce((sum, item) => sum + item.sessionsInserted, 0),
|
|
687
|
+
eventsInserted: receipts.reduce((sum, item) => sum + item.eventsInserted, 0),
|
|
688
|
+
},
|
|
689
|
+
});
|
|
690
|
+
}
|
|
691
|
+
finally {
|
|
692
|
+
repository.close();
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
finally {
|
|
696
|
+
await lease.release();
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
async function discoverWorkspace(ctx, args) {
|
|
700
|
+
const workspace = resolve(ctx.cwd, requiredValue(args, '--workspace'));
|
|
701
|
+
const manifest = await discoverWorkspaceHistories({
|
|
702
|
+
workspace,
|
|
703
|
+
providerHome: args.values.has('--provider-home')
|
|
704
|
+
? resolve(ctx.cwd, args.values.get('--provider-home'))
|
|
705
|
+
: undefined,
|
|
706
|
+
codexRoot: args.values.has('--codex-root')
|
|
707
|
+
? resolve(ctx.cwd, args.values.get('--codex-root'))
|
|
708
|
+
: undefined,
|
|
709
|
+
});
|
|
710
|
+
const manifestPath = resolve(ctx.cwd, args.values.get('--manifest') ?? defaultDiscoveryManifestPath(manifest.workspacePath));
|
|
711
|
+
const data = {
|
|
712
|
+
manifest: publicDiscoveryManifest(manifest),
|
|
713
|
+
manifestPath: redactSourceLocator(manifestPath),
|
|
714
|
+
exactManifestRequiredForImport: true,
|
|
715
|
+
};
|
|
716
|
+
if (isDryRun(ctx, args)) {
|
|
717
|
+
return preview('discover', { ...data, wouldPersistManifest: false });
|
|
718
|
+
}
|
|
719
|
+
await writeDiscoveryManifest(manifestPath, manifest);
|
|
720
|
+
return ok('discover', { ...data, manifestPersisted: true });
|
|
721
|
+
}
|
|
722
|
+
async function importDiscovered(ctx, args) {
|
|
723
|
+
const workspace = resolve(ctx.cwd, requiredValue(args, '--workspace'));
|
|
724
|
+
const canonicalWorkspace = realpathSync(workspace);
|
|
725
|
+
const manifestPath = resolve(ctx.cwd, args.values.get('--manifest') ?? defaultDiscoveryManifestPath(canonicalWorkspace));
|
|
726
|
+
const manifest = await readDiscoveryManifest(manifestPath);
|
|
727
|
+
if (manifest.workspaceId !== canonicalWorkspace) {
|
|
728
|
+
throw new CliError('MANIFEST_WORKSPACE_MISMATCH', 'discovery manifest does not belong to the explicitly authorized workspace', EXIT.contract);
|
|
729
|
+
}
|
|
730
|
+
const confirmed = args.flags.has('--confirm') || args.flags.has('--yes');
|
|
731
|
+
if (isDryRun(ctx, args) || !confirmed) {
|
|
732
|
+
let existing = null;
|
|
733
|
+
try {
|
|
734
|
+
const repository = openRepository(ctx, args);
|
|
735
|
+
existing = repository.getBatchImportRunForManifest(manifest.manifestId, manifest.workspaceId);
|
|
736
|
+
repository.close();
|
|
737
|
+
}
|
|
738
|
+
catch {
|
|
739
|
+
// A preview can still describe the exact manifest before a catalog exists.
|
|
740
|
+
}
|
|
741
|
+
return preview('import-discovered', {
|
|
742
|
+
manifest: publicDiscoveryManifest(manifest),
|
|
743
|
+
receipt: previewDiscoveryImport(manifest, existing),
|
|
744
|
+
confirmationRequired: !isDryRun(ctx, args),
|
|
745
|
+
wouldPersist: false,
|
|
620
746
|
});
|
|
621
747
|
}
|
|
748
|
+
const lease = await acquireImportLease(databasePath(ctx, args), sha256(['discovered-import', manifest.manifestId, manifest.workspaceId].join('\0')), {
|
|
749
|
+
waitMs: boundedInteger(args.values.get('--lock-wait-ms'), 5_000, 0, 300_000, '--lock-wait-ms'),
|
|
750
|
+
});
|
|
751
|
+
try {
|
|
752
|
+
const repository = openRepository(ctx, args);
|
|
753
|
+
try {
|
|
754
|
+
const receipt = await importDiscoveryManifest({
|
|
755
|
+
manifest,
|
|
756
|
+
repository,
|
|
757
|
+
signal: ctx.signal,
|
|
758
|
+
inactivityThresholdMs: inactivityThreshold(args.values.get('--inactivity-threshold')),
|
|
759
|
+
});
|
|
760
|
+
return ok('import-discovered', {
|
|
761
|
+
manifestId: manifest.manifestId,
|
|
762
|
+
receipt,
|
|
763
|
+
resumable: receipt.run.status !== 'complete',
|
|
764
|
+
});
|
|
765
|
+
}
|
|
766
|
+
finally {
|
|
767
|
+
repository.close();
|
|
768
|
+
}
|
|
769
|
+
}
|
|
622
770
|
finally {
|
|
623
|
-
|
|
771
|
+
await lease.release();
|
|
624
772
|
}
|
|
625
773
|
}
|
|
626
774
|
function providerDisposition(provider) {
|
|
@@ -669,10 +817,10 @@ function providerDisposition(provider) {
|
|
|
669
817
|
if (provider === 'cursor') {
|
|
670
818
|
return {
|
|
671
819
|
provider, disposition: 'implemented', operationalState: 'available',
|
|
672
|
-
supportedOperations: ['inspect', 'stream'],
|
|
820
|
+
supportedOperations: ['discover', 'inspect', 'stream'],
|
|
673
821
|
acquisitionModes: ['api', 'jsonl', 'manual-export'],
|
|
674
822
|
reasonCode: null,
|
|
675
|
-
remediation: '
|
|
823
|
+
remediation: 'Authorize a workspace agent-transcripts root, or import Cursor CLI/Cloud Agent exports explicitly.',
|
|
676
824
|
evidence: {
|
|
677
825
|
adapterVersion: CURSOR_ADAPTER_VERSION,
|
|
678
826
|
verifiedAt: '2026-07-27',
|
|
@@ -804,6 +952,8 @@ function cursorLocatorClass(input) {
|
|
|
804
952
|
return 'cursor-editor-markdown';
|
|
805
953
|
if (input.endsWith('.cloud.jsonl'))
|
|
806
954
|
return 'cursor-cloud-events-jsonl';
|
|
955
|
+
if (input.split(/[\\/]+/).includes('agent-transcripts'))
|
|
956
|
+
return 'cursor-agent-transcript-jsonl';
|
|
807
957
|
return 'cursor-cli-stream-json';
|
|
808
958
|
}
|
|
809
959
|
function cursorProviderProfile(locatorClass) {
|
|
@@ -811,6 +961,8 @@ function cursorProviderProfile(locatorClass) {
|
|
|
811
961
|
return 'editor-markdown-lossy';
|
|
812
962
|
if (locatorClass === 'cursor-cloud-events-jsonl')
|
|
813
963
|
return 'cloud-agents-api-v1';
|
|
964
|
+
if (locatorClass === 'cursor-agent-transcript-jsonl')
|
|
965
|
+
return 'agent-transcript-jsonl';
|
|
814
966
|
return 'cli-stream-json';
|
|
815
967
|
}
|
|
816
968
|
function openRepository(ctx, args) {
|
|
@@ -820,11 +972,17 @@ function openRepository(ctx, args) {
|
|
|
820
972
|
return new SessionRepository(path);
|
|
821
973
|
}
|
|
822
974
|
catch (error) {
|
|
823
|
-
|
|
975
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
976
|
+
if (/SQLITE_BUSY|database is locked/i.test(message)) {
|
|
977
|
+
throw new CliError('IMPORT_LOCKED', 'the session catalog is busy; retry after the active writer finishes', EXIT.locked, { owner: null, waitedMs: 0 });
|
|
978
|
+
}
|
|
979
|
+
throw new CliError('CATALOG_UNAVAILABLE', message, EXIT.storage);
|
|
824
980
|
}
|
|
825
981
|
}
|
|
826
982
|
function databasePath(ctx, args) {
|
|
827
|
-
|
|
983
|
+
if (args.values.has('--db'))
|
|
984
|
+
return resolve(ctx.cwd, args.values.get('--db'));
|
|
985
|
+
return resolve(projectRootCandidate(ctx.cwd) ?? ctx.cwd, '.aiwg/sessions/catalog.sqlite');
|
|
828
986
|
}
|
|
829
987
|
function parseArgs(argv) {
|
|
830
988
|
const flags = new Set();
|
|
@@ -836,6 +994,9 @@ function parseArgs(argv) {
|
|
|
836
994
|
'--entity', '--sensitivity', '--extraction-state', '--page-size', '--max-documents',
|
|
837
995
|
'--state', '--reviewer', '--reason', '--policy-version', '--min-confidence',
|
|
838
996
|
'--consumer', '--actor-class', '--reason-code', '--dependent-action', '--basis',
|
|
997
|
+
'--manifest', '--provider-home', '--codex-root', '--lock-wait-ms', '--min-coverage', '--gap',
|
|
998
|
+
'--inactivity-threshold',
|
|
999
|
+
'--control-events',
|
|
839
1000
|
]);
|
|
840
1001
|
let command;
|
|
841
1002
|
for (let index = 0; index < argv.length; index += 1) {
|
|
@@ -872,15 +1033,94 @@ function requiredValue(args, flag) {
|
|
|
872
1033
|
throw new CliError('MISSING_ARGUMENT', `missing required ${flag}`, EXIT.usage);
|
|
873
1034
|
return value;
|
|
874
1035
|
}
|
|
875
|
-
function authorizationContext(args, operation) {
|
|
1036
|
+
function authorizationContext(ctx, args, operation) {
|
|
876
1037
|
return {
|
|
877
1038
|
actorId: 'local-catalog-owner',
|
|
878
|
-
workspaceId: requiredValue(args, '--workspace'),
|
|
1039
|
+
workspaceId: normalizeWorkspaceId(ctx.cwd, requiredValue(args, '--workspace')),
|
|
879
1040
|
operation,
|
|
880
1041
|
catalogScope: 'workspace',
|
|
881
1042
|
mode: 'local-owner',
|
|
882
1043
|
};
|
|
883
1044
|
}
|
|
1045
|
+
function readAuthorizationContext(ctx, args, operation, repository) {
|
|
1046
|
+
const explicit = args.values.get('--workspace');
|
|
1047
|
+
if (explicit) {
|
|
1048
|
+
return {
|
|
1049
|
+
actorId: 'local-catalog-owner',
|
|
1050
|
+
workspaceId: normalizeWorkspaceId(ctx.cwd, explicit),
|
|
1051
|
+
operation,
|
|
1052
|
+
catalogScope: 'workspace',
|
|
1053
|
+
mode: 'local-owner',
|
|
1054
|
+
};
|
|
1055
|
+
}
|
|
1056
|
+
const project = projectRootCandidate(ctx.cwd);
|
|
1057
|
+
if (project) {
|
|
1058
|
+
return {
|
|
1059
|
+
actorId: 'local-catalog-owner',
|
|
1060
|
+
workspaceId: project,
|
|
1061
|
+
operation,
|
|
1062
|
+
catalogScope: 'workspace',
|
|
1063
|
+
mode: 'local-owner',
|
|
1064
|
+
};
|
|
1065
|
+
}
|
|
1066
|
+
const candidates = repository.listWorkspaceIds();
|
|
1067
|
+
if (candidates.length === 1) {
|
|
1068
|
+
return {
|
|
1069
|
+
actorId: 'local-catalog-owner',
|
|
1070
|
+
workspaceId: candidates[0],
|
|
1071
|
+
operation,
|
|
1072
|
+
catalogScope: 'workspace',
|
|
1073
|
+
mode: 'local-owner',
|
|
1074
|
+
};
|
|
1075
|
+
}
|
|
1076
|
+
if (candidates.length > 1) {
|
|
1077
|
+
throw new CliError('WORKSPACE_AMBIGUOUS', 'current workspace cannot be inferred safely; pass --workspace explicitly', EXIT.usage, {
|
|
1078
|
+
candidates,
|
|
1079
|
+
example: `aiwg sessions ${operation} --workspace ${JSON.stringify(candidates[0])}`,
|
|
1080
|
+
});
|
|
1081
|
+
}
|
|
1082
|
+
throw new CliError('WORKSPACE_REQUIRED', 'current workspace cannot be inferred; pass --workspace explicitly', EXIT.usage, {
|
|
1083
|
+
candidates: [],
|
|
1084
|
+
example: `aiwg sessions ${operation} --workspace <path-or-id>`,
|
|
1085
|
+
});
|
|
1086
|
+
}
|
|
1087
|
+
function projectRootCandidate(start) {
|
|
1088
|
+
let current;
|
|
1089
|
+
try {
|
|
1090
|
+
const resolved = realpathSync(resolve(start));
|
|
1091
|
+
current = statSync(resolved).isDirectory() ? resolved : dirname(resolved);
|
|
1092
|
+
}
|
|
1093
|
+
catch {
|
|
1094
|
+
current = resolve(start);
|
|
1095
|
+
}
|
|
1096
|
+
let gitRoot = null;
|
|
1097
|
+
while (true) {
|
|
1098
|
+
if (existsSync(resolve(current, '.aiwg', 'aiwg.config')))
|
|
1099
|
+
return current;
|
|
1100
|
+
if (!gitRoot && existsSync(resolve(current, '.git')))
|
|
1101
|
+
gitRoot = current;
|
|
1102
|
+
const parent = dirname(current);
|
|
1103
|
+
if (parent === current)
|
|
1104
|
+
return gitRoot;
|
|
1105
|
+
current = parent;
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
function normalizeWorkspaceId(cwd, value) {
|
|
1109
|
+
const pathLike = isAbsolute(value)
|
|
1110
|
+
|| value === '.'
|
|
1111
|
+
|| value === '..'
|
|
1112
|
+
|| value.startsWith('./')
|
|
1113
|
+
|| value.startsWith('../');
|
|
1114
|
+
if (!pathLike)
|
|
1115
|
+
return value;
|
|
1116
|
+
const candidate = resolve(cwd, value);
|
|
1117
|
+
try {
|
|
1118
|
+
return realpathSync(candidate);
|
|
1119
|
+
}
|
|
1120
|
+
catch {
|
|
1121
|
+
return candidate;
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
884
1124
|
function boundedInteger(value, fallback, min, max, name) {
|
|
885
1125
|
if (value === undefined)
|
|
886
1126
|
return fallback;
|
|
@@ -901,6 +1141,29 @@ function boundedNumber(value, fallback, min, max, name) {
|
|
|
901
1141
|
}
|
|
902
1142
|
return parsed;
|
|
903
1143
|
}
|
|
1144
|
+
function timelineGap(value) {
|
|
1145
|
+
try {
|
|
1146
|
+
return parseTimelineGap(value);
|
|
1147
|
+
}
|
|
1148
|
+
catch (error) {
|
|
1149
|
+
throw new CliError('INVALID_ARGUMENT', error instanceof Error ? error.message : 'invalid timeline gap', EXIT.usage);
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1152
|
+
function inactivityThreshold(value) {
|
|
1153
|
+
try {
|
|
1154
|
+
return parseTimelineGap(value ?? '24h');
|
|
1155
|
+
}
|
|
1156
|
+
catch (error) {
|
|
1157
|
+
throw new CliError('INVALID_ARGUMENT', error instanceof Error ? error.message : 'invalid inactivity threshold', EXIT.usage);
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
function controlEventMode(value) {
|
|
1161
|
+
if (value === undefined)
|
|
1162
|
+
return 'exclude';
|
|
1163
|
+
if (value === 'exclude' || value === 'include' || value === 'only')
|
|
1164
|
+
return value;
|
|
1165
|
+
throw new CliError('INVALID_ARGUMENT', '--control-events must be exclude, include, or only', EXIT.usage);
|
|
1166
|
+
}
|
|
904
1167
|
function candidateState(value) {
|
|
905
1168
|
if (value === undefined)
|
|
906
1169
|
return undefined;
|
|
@@ -939,25 +1202,61 @@ function emit(value) {
|
|
|
939
1202
|
function printHuman(value) {
|
|
940
1203
|
if (value.status === 'preview')
|
|
941
1204
|
console.log('Preview (no changes applied)');
|
|
1205
|
+
if (value.command === 'sessions.timeline' && value.data
|
|
1206
|
+
&& typeof value.data === 'object' && 'items' in value.data) {
|
|
1207
|
+
const data = value.data;
|
|
1208
|
+
for (const item of data.items) {
|
|
1209
|
+
console.log([
|
|
1210
|
+
item.startAt ?? '<unknown-time>',
|
|
1211
|
+
item.endAt ?? '<unknown-time>',
|
|
1212
|
+
item.provider,
|
|
1213
|
+
item.sessionId,
|
|
1214
|
+
`segment=${item.segmentIndex}`,
|
|
1215
|
+
`events=${item.eventCount}`,
|
|
1216
|
+
`boundary=${item.boundaryBasis}`,
|
|
1217
|
+
`confidence=${item.confidence}`,
|
|
1218
|
+
].join(' '));
|
|
1219
|
+
}
|
|
1220
|
+
console.log(`Coverage: ${data.coverage?.status ?? 'unknown'}`);
|
|
1221
|
+
return;
|
|
1222
|
+
}
|
|
942
1223
|
console.log(JSON.stringify(value.data, null, 2));
|
|
943
1224
|
}
|
|
944
1225
|
class CliError extends Error {
|
|
945
1226
|
code;
|
|
946
1227
|
exitCode;
|
|
947
|
-
|
|
1228
|
+
details;
|
|
1229
|
+
constructor(code, message, exitCode, details) {
|
|
948
1230
|
super(message);
|
|
949
1231
|
this.code = code;
|
|
950
1232
|
this.exitCode = exitCode;
|
|
1233
|
+
this.details = details;
|
|
951
1234
|
}
|
|
952
1235
|
}
|
|
953
1236
|
function normalizeError(error) {
|
|
954
1237
|
if (error instanceof CliError) {
|
|
955
|
-
return {
|
|
1238
|
+
return {
|
|
1239
|
+
error: { code: error.code, message: error.message, details: error.details },
|
|
1240
|
+
exitCode: error.exitCode,
|
|
1241
|
+
};
|
|
956
1242
|
}
|
|
957
1243
|
if (error instanceof SessionContractError) {
|
|
958
1244
|
const exitCode = error.code === 'UNSUPPORTED_OPERATION' ? EXIT.unsupported : EXIT.contract;
|
|
959
1245
|
return { error: { code: error.code, message: error.message }, exitCode };
|
|
960
1246
|
}
|
|
1247
|
+
if (error instanceof ImportLeaseContentionError) {
|
|
1248
|
+
return {
|
|
1249
|
+
error: {
|
|
1250
|
+
code: error.code,
|
|
1251
|
+
message: error.message,
|
|
1252
|
+
details: {
|
|
1253
|
+
owner: error.owner,
|
|
1254
|
+
waitedMs: error.waitMs,
|
|
1255
|
+
},
|
|
1256
|
+
},
|
|
1257
|
+
exitCode: EXIT.locked,
|
|
1258
|
+
};
|
|
1259
|
+
}
|
|
961
1260
|
return {
|
|
962
1261
|
error: { code: 'INTERNAL_ERROR', message: error instanceof Error ? error.message : String(error) },
|
|
963
1262
|
exitCode: EXIT.storage,
|