@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.
- package/README.md +18 -7
- package/dist/src/api/index.d.ts +2 -0
- package/dist/src/api/index.js +2 -0
- package/dist/src/artifacts/browser-export.js +7 -0
- package/dist/src/artifacts/citation-parser.js +96 -35
- package/dist/src/artifacts/index-builder.js +54 -17
- package/dist/src/artifacts/state-transfer.js +27 -0
- package/dist/src/artifacts/stats.js +8 -0
- package/dist/src/cli/cli-extension-loader.js +73 -0
- package/dist/src/cli/handlers/index.js +3 -1
- package/dist/src/cli/handlers/sessions.js +1265 -0
- package/dist/src/cli/handlers/skill-lint.js +49 -45
- package/dist/src/cli/handlers/use.js +143 -60
- package/dist/src/cli/handlers/utilities.js +22 -8
- package/dist/src/cli/skill-usage.js +146 -24
- package/dist/src/config/aiwg-config.js +12 -0
- package/dist/src/config/cli.js +16 -3
- package/dist/src/extensions/commands/definitions.js +29 -0
- package/dist/src/extensions/manifest.js +29 -0
- package/dist/src/security/threat-assessment-config.js +296 -0
- package/dist/src/sessions/adapters/claude.js +385 -0
- package/dist/src/sessions/adapters/codex.js +548 -0
- package/dist/src/sessions/adapters/copilot.js +226 -0
- package/dist/src/sessions/adapters/cursor.js +528 -0
- package/dist/src/sessions/adapters/factory.js +386 -0
- package/dist/src/sessions/adapters/generic.js +225 -0
- package/dist/src/sessions/adapters/hermes.js +341 -0
- package/dist/src/sessions/adapters/openclaw.js +381 -0
- package/dist/src/sessions/adapters/opencode.js +454 -0
- package/dist/src/sessions/adapters/openhuman.js +315 -0
- package/dist/src/sessions/adapters/warp.js +160 -0
- package/dist/src/sessions/adapters/windsurf.js +212 -0
- package/dist/src/sessions/batch-contracts.js +121 -0
- package/dist/src/sessions/batch-import.js +265 -0
- package/dist/src/sessions/candidates.js +210 -0
- package/dist/src/sessions/contracts.js +337 -0
- package/dist/src/sessions/discovery.js +51 -0
- package/dist/src/sessions/fixtures.js +12 -0
- package/dist/src/sessions/import-lease.js +152 -0
- package/dist/src/sessions/importer.js +464 -0
- package/dist/src/sessions/index.js +31 -0
- package/dist/src/sessions/knowledge-shard.js +61 -0
- package/dist/src/sessions/optional-backends.js +238 -0
- package/dist/src/sessions/origin.js +117 -0
- package/dist/src/sessions/policy.js +192 -0
- package/dist/src/sessions/ports.js +2 -0
- package/dist/src/sessions/promotion.js +367 -0
- package/dist/src/sessions/readers.js +176 -0
- package/dist/src/sessions/repository.js +1892 -0
- package/dist/src/sessions/timeline.js +148 -0
- package/dist/src/sessions/workspace-discovery.js +319 -0
- package/dist/src/skills/adapters/agent-skills.js +59 -0
- package/dist/src/skills/adapters/local.js +19 -1
- package/dist/src/skills/agent-skills.js +249 -0
- package/dist/src/skills/cli.js +463 -7
- package/dist/src/skills/deployer.js +554 -0
- package/dist/src/skills/doctor.js +105 -0
- package/dist/src/skills/exporter.js +382 -0
- package/dist/src/skills/importer.js +921 -0
- package/dist/src/skills/registry.js +19 -0
- package/dist/src/skills/validator.js +323 -0
- package/package.json +2 -2
|
@@ -0,0 +1,1265 @@
|
|
|
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
|
+
const JSON_CONTRACT_VERSION = '1.0.0';
|
|
5
|
+
const EXIT = {
|
|
6
|
+
ok: 0, usage: 2, unsupported: 3, unavailable: 4, contract: 5, storage: 6,
|
|
7
|
+
locked: 7, coverage: 8,
|
|
8
|
+
};
|
|
9
|
+
const HELP = `Usage: aiwg sessions <command> [options]
|
|
10
|
+
|
|
11
|
+
Commands:
|
|
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
|
|
15
|
+
import <file> --source-id <id> Import a supported provider JSONL source
|
|
16
|
+
list [--limit N] [--cursor N] List normalized sessions
|
|
17
|
+
timeline [--gap 30m] Report chronological activity segments
|
|
18
|
+
show <session-id> Show a session with events and tags
|
|
19
|
+
search <query> --workspace <id> Search authorized normalized content
|
|
20
|
+
extract [session-id] --workspace <id> Extract structural candidates
|
|
21
|
+
candidates [--state <state>] List the candidate review queue
|
|
22
|
+
review <id> <version> <state> Record an explicit review transition
|
|
23
|
+
promote <id> <version> Preview promotion; use --confirm to write
|
|
24
|
+
tag <session-id> <tag> Add a catalog tag
|
|
25
|
+
relocate <source-id> <file> Update AIWG source-location metadata
|
|
26
|
+
reindex Rebuild catalog indexes
|
|
27
|
+
delete <session-id> Preview deletion; use --confirm to tombstone
|
|
28
|
+
restore <session-id> Restore a reversible catalog tombstone
|
|
29
|
+
purge <session-id> Preview terminal AIWG-copy purge
|
|
30
|
+
audit --workspace <id> Read content-free mutation events
|
|
31
|
+
doctor Check catalog availability and integrity
|
|
32
|
+
|
|
33
|
+
Options:
|
|
34
|
+
--json Emit the versioned JSON contract
|
|
35
|
+
--dry-run Preview a mutation without changing state
|
|
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
|
|
44
|
+
--provider <id> Filter list or select an import adapter
|
|
45
|
+
--consumer <id> Select a named memory consumer for promotion
|
|
46
|
+
--workspace <id>, --tag <tag>, --limit <n>, --cursor <n>
|
|
47
|
+
--page-size <n> Extraction scan page size (default 250, maximum 500)
|
|
48
|
+
--max-documents <n> Explicit extraction safety limit; returns a partial receipt`;
|
|
49
|
+
export const sessionsHandler = {
|
|
50
|
+
id: 'sessions',
|
|
51
|
+
name: 'Sessions',
|
|
52
|
+
description: 'Manage the normalized session catalog (the singular `session` command remains the launcher)',
|
|
53
|
+
category: 'project',
|
|
54
|
+
aliases: [],
|
|
55
|
+
async execute(ctx) {
|
|
56
|
+
const json = ctx.args.includes('--json');
|
|
57
|
+
let parsed;
|
|
58
|
+
try {
|
|
59
|
+
parsed = parseArgs(ctx.args);
|
|
60
|
+
}
|
|
61
|
+
catch (error) {
|
|
62
|
+
const normalized = normalizeError(error);
|
|
63
|
+
const output = envelope('sessions', 'error', null, normalized.error);
|
|
64
|
+
if (json)
|
|
65
|
+
emit(output);
|
|
66
|
+
else
|
|
67
|
+
console.error(`${normalized.error.code}: ${normalized.error.message}`);
|
|
68
|
+
return { exitCode: normalized.exitCode, message: normalized.error.message };
|
|
69
|
+
}
|
|
70
|
+
if (!parsed.command || parsed.flags.has('--help') || parsed.flags.has('-h')) {
|
|
71
|
+
if (json)
|
|
72
|
+
emit(envelope('sessions.help', 'ok', { usage: HELP }, null));
|
|
73
|
+
else
|
|
74
|
+
console.log(HELP);
|
|
75
|
+
return { exitCode: parsed.command ? EXIT.ok : EXIT.usage };
|
|
76
|
+
}
|
|
77
|
+
try {
|
|
78
|
+
const result = await executeCommand(ctx, parsed);
|
|
79
|
+
if (json)
|
|
80
|
+
emit(result.envelope);
|
|
81
|
+
else
|
|
82
|
+
printHuman(result.envelope);
|
|
83
|
+
return { exitCode: result.exitCode };
|
|
84
|
+
}
|
|
85
|
+
catch (error) {
|
|
86
|
+
const normalized = normalizeError(error);
|
|
87
|
+
const output = envelope(`sessions.${parsed.command}`, 'error', null, normalized.error);
|
|
88
|
+
if (json)
|
|
89
|
+
emit(output);
|
|
90
|
+
else
|
|
91
|
+
console.error(`${normalized.error.code}: ${normalized.error.message}`);
|
|
92
|
+
return { exitCode: normalized.exitCode, message: normalized.error.message };
|
|
93
|
+
}
|
|
94
|
+
},
|
|
95
|
+
};
|
|
96
|
+
async function executeCommand(ctx, args) {
|
|
97
|
+
const command = args.command;
|
|
98
|
+
if (command === 'sources') {
|
|
99
|
+
const reports = [...SESSION_PROVIDER_IDS].sort().map(providerDisposition);
|
|
100
|
+
return ok(command, { providers: reports, count: reports.length });
|
|
101
|
+
}
|
|
102
|
+
if (command === 'import')
|
|
103
|
+
return importSource(ctx, args);
|
|
104
|
+
if (command === 'discover')
|
|
105
|
+
return discoverWorkspace(ctx, args);
|
|
106
|
+
if (command === 'import-discovered')
|
|
107
|
+
return importDiscovered(ctx, args);
|
|
108
|
+
const repository = openRepository(ctx, args);
|
|
109
|
+
try {
|
|
110
|
+
switch (command) {
|
|
111
|
+
case 'list': {
|
|
112
|
+
const { workspaceId } = readAuthorizationContext(ctx, args, command, repository);
|
|
113
|
+
const limit = boundedInteger(args.values.get('--limit'), 50, 1, 500, '--limit');
|
|
114
|
+
const cursor = args.values.get('--cursor');
|
|
115
|
+
const providerInput = args.values.get('--provider');
|
|
116
|
+
const provider = providerInput ? assertSessionProviderId(providerInput) : undefined;
|
|
117
|
+
const result = repository.listSessions({
|
|
118
|
+
provider,
|
|
119
|
+
workspaceId,
|
|
120
|
+
tag: args.values.get('--tag'),
|
|
121
|
+
limit,
|
|
122
|
+
cursor,
|
|
123
|
+
});
|
|
124
|
+
const coverage = repository.getCoverage(workspaceId);
|
|
125
|
+
const response = {
|
|
126
|
+
items: result.items,
|
|
127
|
+
page: {
|
|
128
|
+
limit,
|
|
129
|
+
cursor: cursor ?? null,
|
|
130
|
+
nextCursor: result.nextCursor,
|
|
131
|
+
snapshotRowid: result.snapshotRowid,
|
|
132
|
+
total: result.total,
|
|
133
|
+
},
|
|
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);
|
|
180
|
+
}
|
|
181
|
+
case 'show': {
|
|
182
|
+
const id = requiredPositional(args, 0, 'session-id');
|
|
183
|
+
const { workspaceId } = readAuthorizationContext(ctx, args, command, repository);
|
|
184
|
+
const session = repository.getSession(id, workspaceId);
|
|
185
|
+
if (!session)
|
|
186
|
+
throw new CliError('SESSION_NOT_FOUND', `session not found: ${id}`, EXIT.unavailable);
|
|
187
|
+
return ok(command, {
|
|
188
|
+
session,
|
|
189
|
+
tags: repository.listTags(id, workspaceId),
|
|
190
|
+
events: repository.listEvents(id, workspaceId),
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
case 'search': {
|
|
194
|
+
const query = requiredPositional(args, 0, 'query');
|
|
195
|
+
const { workspaceId } = readAuthorizationContext(ctx, args, command, repository);
|
|
196
|
+
const providerInput = args.values.get('--provider');
|
|
197
|
+
const provider = providerInput ? assertSessionProviderId(providerInput) : undefined;
|
|
198
|
+
const result = repository.search({
|
|
199
|
+
query,
|
|
200
|
+
workspaceId,
|
|
201
|
+
providers: provider ? [provider] : undefined,
|
|
202
|
+
dateFrom: args.values.get('--date-from'),
|
|
203
|
+
dateTo: args.values.get('--date-to'),
|
|
204
|
+
participant: args.values.get('--participant'),
|
|
205
|
+
model: args.values.get('--model'),
|
|
206
|
+
role: args.values.get('--role'),
|
|
207
|
+
tool: args.values.get('--tool'),
|
|
208
|
+
tag: args.values.get('--tag'),
|
|
209
|
+
entity: args.values.get('--entity'),
|
|
210
|
+
sensitivity: args.values.get('--sensitivity'),
|
|
211
|
+
extractionState: args.values.get('--extraction-state'),
|
|
212
|
+
controlEvents: controlEventMode(args.values.get('--control-events')),
|
|
213
|
+
limit: boundedInteger(args.values.get('--limit'), 50, 1, 500, '--limit'),
|
|
214
|
+
cursor: args.values.get('--cursor'),
|
|
215
|
+
});
|
|
216
|
+
return ok(command, {
|
|
217
|
+
items: result.items,
|
|
218
|
+
page: { limit: boundedInteger(args.values.get('--limit'), 50, 1, 500, '--limit'),
|
|
219
|
+
nextCursor: result.nextCursor },
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
case 'extract': {
|
|
223
|
+
const { workspaceId } = authorizationContext(ctx, args, command);
|
|
224
|
+
const sessionId = args.positionals[0];
|
|
225
|
+
const pageSize = boundedInteger(args.values.get('--page-size'), 250, 1, 500, '--page-size');
|
|
226
|
+
const maxDocuments = args.values.has('--max-documents')
|
|
227
|
+
? boundedInteger(args.values.get('--max-documents'), pageSize, 1, 1_000_000, '--max-documents')
|
|
228
|
+
: null;
|
|
229
|
+
const documents = [];
|
|
230
|
+
let cursor;
|
|
231
|
+
let truncated = false;
|
|
232
|
+
do {
|
|
233
|
+
const remaining = maxDocuments === null
|
|
234
|
+
? pageSize
|
|
235
|
+
: Math.min(pageSize, maxDocuments - documents.length);
|
|
236
|
+
if (remaining <= 0) {
|
|
237
|
+
truncated = true;
|
|
238
|
+
break;
|
|
239
|
+
}
|
|
240
|
+
const page = repository.authorizedSearchDocumentPage({
|
|
241
|
+
workspaceId,
|
|
242
|
+
sessionIds: sessionId ? [sessionId] : undefined,
|
|
243
|
+
limit: remaining,
|
|
244
|
+
cursor,
|
|
245
|
+
});
|
|
246
|
+
documents.push(...page.items);
|
|
247
|
+
cursor = page.nextCursor ?? undefined;
|
|
248
|
+
if (maxDocuments !== null && documents.length >= maxDocuments && cursor) {
|
|
249
|
+
truncated = true;
|
|
250
|
+
break;
|
|
251
|
+
}
|
|
252
|
+
} while (cursor);
|
|
253
|
+
if (sessionId && documents.length === 0) {
|
|
254
|
+
throw new CliError('SESSION_NOT_FOUND', `no authorized evidence found for session: ${sessionId}`, EXIT.unavailable);
|
|
255
|
+
}
|
|
256
|
+
const dryRun = isDryRun(ctx, args);
|
|
257
|
+
const service = new CandidateExtractionService(dryRun
|
|
258
|
+
? { saveCandidates: (candidates) => [...candidates] }
|
|
259
|
+
: repository);
|
|
260
|
+
const items = await service.extract({
|
|
261
|
+
documents,
|
|
262
|
+
extractor: new StructuralCandidateExtractor(),
|
|
263
|
+
policy: {
|
|
264
|
+
version: args.values.get('--policy-version') ?? '1.0.0',
|
|
265
|
+
projectScope: workspaceId,
|
|
266
|
+
temporalScope: 'source-event',
|
|
267
|
+
minimumConfidence: boundedNumber(args.values.get('--min-confidence'), 0.5, 0, 1, '--min-confidence'),
|
|
268
|
+
},
|
|
269
|
+
});
|
|
270
|
+
const scan = {
|
|
271
|
+
complete: !truncated,
|
|
272
|
+
documentsScanned: documents.length,
|
|
273
|
+
pageSize,
|
|
274
|
+
nextCursor: truncated ? cursor ?? null : null,
|
|
275
|
+
limit: maxDocuments,
|
|
276
|
+
};
|
|
277
|
+
return dryRun
|
|
278
|
+
? preview(command, { items, count: items.length, durableMemoryWrites: 0, scan })
|
|
279
|
+
: ok(command, { items, count: items.length, durableMemoryWrites: 0, scan });
|
|
280
|
+
}
|
|
281
|
+
case 'candidates': {
|
|
282
|
+
const { workspaceId } = readAuthorizationContext(ctx, args, command, repository);
|
|
283
|
+
const state = candidateState(args.values.get('--state'));
|
|
284
|
+
return ok(command, {
|
|
285
|
+
items: repository.listCandidates(state, workspaceId),
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
case 'review': {
|
|
289
|
+
const { workspaceId } = authorizationContext(ctx, args, command);
|
|
290
|
+
const candidateId = requiredPositional(args, 0, 'candidate-id');
|
|
291
|
+
const version = boundedInteger(requiredPositional(args, 1, 'version'), 1, 1, Number.MAX_SAFE_INTEGER, 'version');
|
|
292
|
+
const toState = candidateState(requiredPositional(args, 2, 'state'));
|
|
293
|
+
if (!toState)
|
|
294
|
+
throw new CliError('INVALID_ARGUMENT', 'review state is required', EXIT.usage);
|
|
295
|
+
if (isDryRun(ctx, args)) {
|
|
296
|
+
return preview(command, {
|
|
297
|
+
candidateId, version, toState,
|
|
298
|
+
reviewer: requiredValue(args, '--reviewer'),
|
|
299
|
+
reason: requiredValue(args, '--reason'),
|
|
300
|
+
securityAcknowledged: args.flags.has('--acknowledge-security-risk'),
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
return ok(command, repository.reviewCandidate({
|
|
304
|
+
candidateId,
|
|
305
|
+
version,
|
|
306
|
+
toState,
|
|
307
|
+
reviewer: requiredValue(args, '--reviewer'),
|
|
308
|
+
reason: requiredValue(args, '--reason'),
|
|
309
|
+
securityAcknowledged: args.flags.has('--acknowledge-security-risk'),
|
|
310
|
+
workspaceId,
|
|
311
|
+
}));
|
|
312
|
+
}
|
|
313
|
+
case 'promote': {
|
|
314
|
+
const { workspaceId } = authorizationContext(ctx, args, command);
|
|
315
|
+
const candidateId = requiredPositional(args, 0, 'candidate-id');
|
|
316
|
+
const version = boundedInteger(requiredPositional(args, 1, 'version'), 1, 1, Number.MAX_SAFE_INTEGER, 'version');
|
|
317
|
+
const consumer = requiredValue(args, '--consumer');
|
|
318
|
+
const destination = new FilesystemMemoryDestination({
|
|
319
|
+
projectRoot: ctx.cwd,
|
|
320
|
+
consumer,
|
|
321
|
+
manifestPath: resolveMemoryConsumerManifest(ctx.cwd, consumer),
|
|
322
|
+
});
|
|
323
|
+
const scopedPromotionStore = {
|
|
324
|
+
getCandidate: (id, candidateVersion) => repository.getCandidate(id, candidateVersion, workspaceId),
|
|
325
|
+
getPromotionReceipt: (id, candidateVersion, namedConsumer) => repository.getCandidate(id, candidateVersion, workspaceId)
|
|
326
|
+
? repository.getPromotionReceipt(id, candidateVersion, namedConsumer)
|
|
327
|
+
: null,
|
|
328
|
+
recordPromotion: (receipt) => {
|
|
329
|
+
if (!repository.getCandidate(receipt.candidateId, receipt.candidateVersion, workspaceId)) {
|
|
330
|
+
throw new SessionContractError('OPERATION_NOT_AUTHORIZED', 'candidate version is not available in the authorized workspace');
|
|
331
|
+
}
|
|
332
|
+
return repository.recordPromotion(receipt);
|
|
333
|
+
},
|
|
334
|
+
};
|
|
335
|
+
const gateway = new MemoryPromotionGateway(scopedPromotionStore);
|
|
336
|
+
const promotionPreview = gateway.preview({ candidateId, version, destination });
|
|
337
|
+
if (!args.flags.has('--confirm') || isDryRun(ctx, args)) {
|
|
338
|
+
return preview(command, promotionPreview);
|
|
339
|
+
}
|
|
340
|
+
const receipt = await gateway.promote({
|
|
341
|
+
candidateId,
|
|
342
|
+
version,
|
|
343
|
+
destination,
|
|
344
|
+
reviewer: requiredValue(args, '--reviewer'),
|
|
345
|
+
operationId: promotionPreview.operationId,
|
|
346
|
+
});
|
|
347
|
+
return ok(command, { preview: promotionPreview, receipt });
|
|
348
|
+
}
|
|
349
|
+
case 'tag': {
|
|
350
|
+
const id = requiredPositional(args, 0, 'session-id');
|
|
351
|
+
const { workspaceId } = authorizationContext(ctx, args, command);
|
|
352
|
+
const tag = requiredPositional(args, 1, 'tag');
|
|
353
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9._:-]{0,63}$/.test(tag)) {
|
|
354
|
+
throw new CliError('INVALID_TAG', 'tag must be 1-64 safe identifier characters', EXIT.usage);
|
|
355
|
+
}
|
|
356
|
+
if (isDryRun(ctx, args))
|
|
357
|
+
return preview(command, { sessionId: id, tag, wouldAdd: true });
|
|
358
|
+
if (!repository.tagSession(id, tag, workspaceId)) {
|
|
359
|
+
if (!repository.getSession(id, workspaceId)) {
|
|
360
|
+
throw new CliError('SESSION_NOT_FOUND', `session not found: ${id}`, EXIT.unavailable);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
return ok(command, { sessionId: id, tag, tags: repository.listTags(id, workspaceId) });
|
|
364
|
+
}
|
|
365
|
+
case 'relocate': {
|
|
366
|
+
const sourceId = requiredPositional(args, 0, 'source-id');
|
|
367
|
+
const { workspaceId } = authorizationContext(ctx, args, command);
|
|
368
|
+
const locator = requiredPositional(args, 1, 'file');
|
|
369
|
+
if (!repository.getSource(sourceId, workspaceId)) {
|
|
370
|
+
throw new CliError('SOURCE_NOT_FOUND', `source not found: ${sourceId}`, EXIT.unavailable);
|
|
371
|
+
}
|
|
372
|
+
const redactedLocator = redactSourceLocator(locator);
|
|
373
|
+
if (isDryRun(ctx, args))
|
|
374
|
+
return preview(command, { sourceId, redactedLocator });
|
|
375
|
+
repository.relocateSource(sourceId, redactedLocator, workspaceId);
|
|
376
|
+
return ok(command, { sourceId, redactedLocator });
|
|
377
|
+
}
|
|
378
|
+
case 'reindex': {
|
|
379
|
+
const { workspaceId } = authorizationContext(ctx, args, command);
|
|
380
|
+
if (isDryRun(ctx, args))
|
|
381
|
+
return preview(command, { operation: 'reindex' });
|
|
382
|
+
repository.reindex(workspaceId);
|
|
383
|
+
return ok(command, { operation: 'reindex' });
|
|
384
|
+
}
|
|
385
|
+
case 'audit': {
|
|
386
|
+
const { workspaceId } = readAuthorizationContext(ctx, args, command, repository);
|
|
387
|
+
const limit = boundedInteger(args.values.get('--limit'), 100, 1, 500, '--limit');
|
|
388
|
+
const cursor = args.values.get('--cursor');
|
|
389
|
+
if (args.flags.has('--otel')) {
|
|
390
|
+
return ok(command, repository.exportMutationEventsOtel({
|
|
391
|
+
workspaceId,
|
|
392
|
+
limit,
|
|
393
|
+
cursor,
|
|
394
|
+
}));
|
|
395
|
+
}
|
|
396
|
+
const page = repository.listMutationEvents({ workspaceId, limit, cursor });
|
|
397
|
+
return ok(command, {
|
|
398
|
+
items: page.items,
|
|
399
|
+
page: { limit, nextCursor: page.nextCursor },
|
|
400
|
+
});
|
|
401
|
+
}
|
|
402
|
+
case 'delete': {
|
|
403
|
+
const id = requiredPositional(args, 0, 'session-id');
|
|
404
|
+
const { workspaceId } = authorizationContext(ctx, args, command);
|
|
405
|
+
const counts = repository.deletionPreview(id, workspaceId);
|
|
406
|
+
if (counts.sessions === 0) {
|
|
407
|
+
throw new CliError('SESSION_NOT_FOUND', `session not found: ${id}`, EXIT.unavailable);
|
|
408
|
+
}
|
|
409
|
+
if (!args.flags.has('--confirm') || isDryRun(ctx, args)) {
|
|
410
|
+
return preview(command, {
|
|
411
|
+
sessionId: id, counts, providerLogsModified: false,
|
|
412
|
+
confirmationRequired: true,
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
repository.tombstoneSession(id, workspaceId);
|
|
416
|
+
return ok(command, {
|
|
417
|
+
sessionId: id, counts, providerLogsModified: false, outcome: 'tombstoned',
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
case 'restore': {
|
|
421
|
+
const id = requiredPositional(args, 0, 'session-id');
|
|
422
|
+
const { workspaceId } = authorizationContext(ctx, args, command);
|
|
423
|
+
if (isDryRun(ctx, args))
|
|
424
|
+
return preview(command, { sessionId: id, wouldRestore: true });
|
|
425
|
+
if (!repository.restoreSession(id, workspaceId)) {
|
|
426
|
+
throw new CliError('SESSION_NOT_FOUND', `tombstoned session not found: ${id}`, EXIT.unavailable);
|
|
427
|
+
}
|
|
428
|
+
return ok(command, { sessionId: id, outcome: 'restored', providerLogsModified: false });
|
|
429
|
+
}
|
|
430
|
+
case 'purge': {
|
|
431
|
+
const id = requiredPositional(args, 0, 'session-id');
|
|
432
|
+
const { workspaceId } = authorizationContext(ctx, args, command);
|
|
433
|
+
const completed = repository.getCompletedPurge(id, workspaceId);
|
|
434
|
+
if (completed) {
|
|
435
|
+
return ok(command, {
|
|
436
|
+
receipt: completed,
|
|
437
|
+
dependentDecisions: repository.listPromotionDependencyDecisions(completed.operationId),
|
|
438
|
+
duplicate: true,
|
|
439
|
+
providerLogsModified: false,
|
|
440
|
+
});
|
|
441
|
+
}
|
|
442
|
+
const purgePreview = repository.previewPurge(id, workspaceId);
|
|
443
|
+
const requestedAction = args.values.get('--dependent-action');
|
|
444
|
+
const action = purgePreview.promotedDependents.length > 0 && requestedAction
|
|
445
|
+
? dependentAction(requestedAction)
|
|
446
|
+
: 'origin_unavailable';
|
|
447
|
+
const basis = purgePreview.promotedDependents.length > 0
|
|
448
|
+
? (args.values.get('--basis') ?? 'not-yet-confirmed')
|
|
449
|
+
: (args.values.get('--basis') ?? 'no-promoted-dependents');
|
|
450
|
+
const decisions = purgePreview.promotedDependents.map((item) => ({
|
|
451
|
+
dependentId: item.dependentId,
|
|
452
|
+
action,
|
|
453
|
+
basis,
|
|
454
|
+
}));
|
|
455
|
+
const dispositionCoordinator = new FilesystemPromotionDispositionCoordinator({
|
|
456
|
+
projectRoot: ctx.cwd,
|
|
457
|
+
allowedRoots: ['.aiwg'],
|
|
458
|
+
});
|
|
459
|
+
if (!args.flags.has('--confirm') || isDryRun(ctx, args)) {
|
|
460
|
+
return preview(command, {
|
|
461
|
+
...purgePreview,
|
|
462
|
+
artifactEffects: requestedAction
|
|
463
|
+
? dispositionCoordinator.preview(purgePreview, decisions)
|
|
464
|
+
: purgePreview.promotedDependents.map((item) => ({
|
|
465
|
+
dependentId: item.dependentId,
|
|
466
|
+
destinationRef: item.destinationRef,
|
|
467
|
+
allowedActions: [
|
|
468
|
+
'origin_unavailable', 'retain', 'revoke',
|
|
469
|
+
'supersede', 'delete', 'abort',
|
|
470
|
+
],
|
|
471
|
+
confirmationRequired: true,
|
|
472
|
+
})),
|
|
473
|
+
providerLogsModified: false,
|
|
474
|
+
});
|
|
475
|
+
}
|
|
476
|
+
if (purgePreview.promotedDependents.length > 0) {
|
|
477
|
+
dependentAction(requiredValue(args, '--dependent-action'));
|
|
478
|
+
requiredValue(args, '--basis');
|
|
479
|
+
}
|
|
480
|
+
const disposition = dispositionCoordinator.apply(purgePreview, decisions);
|
|
481
|
+
const receipt = repository.purgeSession({
|
|
482
|
+
preview: purgePreview,
|
|
483
|
+
actorClass: requiredValue(args, '--actor-class'),
|
|
484
|
+
reasonCode: requiredValue(args, '--reason-code'),
|
|
485
|
+
decisions,
|
|
486
|
+
});
|
|
487
|
+
dispositionCoordinator.catalogCommitted(receipt.operationId);
|
|
488
|
+
return ok(command, {
|
|
489
|
+
receipt,
|
|
490
|
+
artifactDisposition: disposition,
|
|
491
|
+
dependentDecisions: repository.listPromotionDependencyDecisions(receipt.operationId),
|
|
492
|
+
providerLogsModified: false,
|
|
493
|
+
});
|
|
494
|
+
}
|
|
495
|
+
case 'doctor':
|
|
496
|
+
return ok(command, {
|
|
497
|
+
database: redactSourceLocator(databasePath(ctx, args)),
|
|
498
|
+
health: repository.doctor(),
|
|
499
|
+
jsonContractVersion: JSON_CONTRACT_VERSION,
|
|
500
|
+
});
|
|
501
|
+
default:
|
|
502
|
+
throw new CliError('UNKNOWN_COMMAND', `unknown sessions command: ${command}`, EXIT.usage);
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
finally {
|
|
506
|
+
repository.close();
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
async function importSource(ctx, args) {
|
|
510
|
+
const input = resolve(ctx.cwd, requiredPositional(args, 0, 'file'));
|
|
511
|
+
const providerInput = args.values.get('--provider') ?? 'generic';
|
|
512
|
+
const provider = assertSessionProviderId(providerInput);
|
|
513
|
+
if (provider !== 'generic' && provider !== 'claude' && provider !== 'codex'
|
|
514
|
+
&& provider !== 'copilot' && provider !== 'cursor' && provider !== 'factory'
|
|
515
|
+
&& provider !== 'hermes' && provider !== 'opencode' && provider !== 'openclaw'
|
|
516
|
+
&& provider !== 'openhuman' && provider !== 'warp' && provider !== 'devin-desktop') {
|
|
517
|
+
throw new CliError('UNSUPPORTED_OPERATION', `session import is not implemented for ${provider}`, EXIT.unsupported);
|
|
518
|
+
}
|
|
519
|
+
const sourceId = requiredValue(args, '--source-id');
|
|
520
|
+
const workspaceId = args.values.has('--workspace')
|
|
521
|
+
? normalizeWorkspaceId(ctx.cwd, args.values.get('--workspace'))
|
|
522
|
+
: 'default';
|
|
523
|
+
const isClaude = provider === 'claude';
|
|
524
|
+
const isCodex = provider === 'codex';
|
|
525
|
+
const isCopilot = provider === 'copilot';
|
|
526
|
+
const isCursor = provider === 'cursor';
|
|
527
|
+
const isFactory = provider === 'factory';
|
|
528
|
+
const isHermes = provider === 'hermes';
|
|
529
|
+
const isOpenCode = provider === 'opencode';
|
|
530
|
+
const isOpenClaw = provider === 'openclaw';
|
|
531
|
+
const isOpenHuman = provider === 'openhuman';
|
|
532
|
+
const isWarp = provider === 'warp';
|
|
533
|
+
const isDevinDesktop = provider === 'devin-desktop';
|
|
534
|
+
const adapter = isClaude
|
|
535
|
+
? new ClaudeSessionAdapter()
|
|
536
|
+
: isCodex
|
|
537
|
+
? new CodexSessionAdapter()
|
|
538
|
+
: isCopilot
|
|
539
|
+
? new CopilotSessionAdapter()
|
|
540
|
+
: isCursor
|
|
541
|
+
? new CursorSessionAdapter()
|
|
542
|
+
: isFactory
|
|
543
|
+
? new FactorySessionAdapter()
|
|
544
|
+
: isHermes
|
|
545
|
+
? new HermesSessionAdapter()
|
|
546
|
+
: isOpenCode
|
|
547
|
+
? new OpenCodeSessionAdapter()
|
|
548
|
+
: isOpenClaw
|
|
549
|
+
? new OpenClawSessionAdapter()
|
|
550
|
+
: isOpenHuman
|
|
551
|
+
? new OpenHumanSessionAdapter()
|
|
552
|
+
: isWarp
|
|
553
|
+
? new WarpSessionAdapter()
|
|
554
|
+
: isDevinDesktop
|
|
555
|
+
? new DevinDesktopSessionAdapter()
|
|
556
|
+
: new GenericSessionInterchangeAdapter();
|
|
557
|
+
const locatorClass = isClaude
|
|
558
|
+
? (input.endsWith('.hooks.jsonl') ? 'claude-hook-jsonl' : 'claude-transcript-jsonl')
|
|
559
|
+
: isCodex
|
|
560
|
+
? (input.endsWith('.app-server.jsonl') ? 'codex-app-server-jsonl' : 'codex-rollout-jsonl')
|
|
561
|
+
: isCopilot
|
|
562
|
+
? 'copilot-chat-json-export'
|
|
563
|
+
: isCursor
|
|
564
|
+
? cursorLocatorClass(input)
|
|
565
|
+
: isFactory
|
|
566
|
+
? 'factory-droid-jsonl'
|
|
567
|
+
: isHermes
|
|
568
|
+
? 'hermes-export-jsonl'
|
|
569
|
+
: isOpenCode
|
|
570
|
+
? 'opencode-export-json'
|
|
571
|
+
: isOpenClaw
|
|
572
|
+
? 'openclaw-consistent-snapshot-jsonl'
|
|
573
|
+
: isOpenHuman
|
|
574
|
+
? 'openhuman-enriched-jsonl'
|
|
575
|
+
: isWarp
|
|
576
|
+
? 'warp-markdown-export'
|
|
577
|
+
: isDevinDesktop
|
|
578
|
+
? 'devin-desktop-cascade-hook-jsonl'
|
|
579
|
+
: 'manual-export';
|
|
580
|
+
const selectedSource = {
|
|
581
|
+
provider, locator: input, locatorClass, sourceId,
|
|
582
|
+
authorizedScope: { workspaceId, allowedRoots: [dirname(input)] },
|
|
583
|
+
};
|
|
584
|
+
const probe = await adapter.inspect(selectedSource);
|
|
585
|
+
const source = SessionSourceSchema.parse({
|
|
586
|
+
contractVersion: SESSION_CONTRACT_VERSION, sourceId, provider,
|
|
587
|
+
providerProfile: isClaude
|
|
588
|
+
? 'documented-local-jsonl'
|
|
589
|
+
: isCodex
|
|
590
|
+
? 'app-server-v2-rollout-fallback'
|
|
591
|
+
: isCopilot
|
|
592
|
+
? 'vscode-chat-json-export'
|
|
593
|
+
: isCursor
|
|
594
|
+
? cursorProviderProfile(locatorClass)
|
|
595
|
+
: isFactory
|
|
596
|
+
? 'documented-project-jsonl'
|
|
597
|
+
: isHermes
|
|
598
|
+
? 'native-schema-23-export'
|
|
599
|
+
: isOpenCode
|
|
600
|
+
? 'sanitized-json-export'
|
|
601
|
+
: isOpenClaw
|
|
602
|
+
? 'schema-16-event-v3-consistent-snapshot'
|
|
603
|
+
: isOpenHuman
|
|
604
|
+
? 'schema-1-session-raw-enriched'
|
|
605
|
+
: isWarp
|
|
606
|
+
? 'manual-lossy-markdown-export'
|
|
607
|
+
: isDevinDesktop
|
|
608
|
+
? 'opt-in-cascade-transcript-hook'
|
|
609
|
+
: 'manual-interchange',
|
|
610
|
+
locatorClass, redactedLocator: redactSourceLocator(input),
|
|
611
|
+
adapterVersion: isClaude
|
|
612
|
+
? CLAUDE_ADAPTER_VERSION
|
|
613
|
+
: isCodex
|
|
614
|
+
? CODEX_ADAPTER_VERSION
|
|
615
|
+
: isCopilot
|
|
616
|
+
? COPILOT_ADAPTER_VERSION
|
|
617
|
+
: isCursor
|
|
618
|
+
? CURSOR_ADAPTER_VERSION
|
|
619
|
+
: isFactory
|
|
620
|
+
? FACTORY_ADAPTER_VERSION
|
|
621
|
+
: isHermes
|
|
622
|
+
? HERMES_ADAPTER_VERSION
|
|
623
|
+
: isOpenCode
|
|
624
|
+
? OPENCODE_ADAPTER_VERSION
|
|
625
|
+
: isOpenClaw
|
|
626
|
+
? OPENCLAW_ADAPTER_VERSION
|
|
627
|
+
: isOpenHuman
|
|
628
|
+
? OPENHUMAN_ADAPTER_VERSION
|
|
629
|
+
: isWarp
|
|
630
|
+
? WARP_ADAPTER_VERSION
|
|
631
|
+
: isDevinDesktop
|
|
632
|
+
? DEVIN_DESKTOP_ADAPTER_VERSION
|
|
633
|
+
: GENERIC_ADAPTER_VERSION,
|
|
634
|
+
sourceSchemaVersion: probe.sourceSchemaVersion,
|
|
635
|
+
disposition: isWarp
|
|
636
|
+
? 'manual-only'
|
|
637
|
+
: isClaude || isCodex || isCopilot || isCursor || isFactory || isHermes
|
|
638
|
+
|| isOpenCode || isOpenClaw || isOpenHuman || isDevinDesktop
|
|
639
|
+
? 'implemented' : 'manual-only',
|
|
640
|
+
operationalState: probe.operationalState,
|
|
641
|
+
consistency: probe.consistency, authorizedAt: new Date().toISOString(),
|
|
642
|
+
extensions: isClaude
|
|
643
|
+
? { 'native.claude': {} }
|
|
644
|
+
: isCodex
|
|
645
|
+
? { 'native.codex': {} }
|
|
646
|
+
: isCopilot
|
|
647
|
+
? { 'native.copilot': {} }
|
|
648
|
+
: isCursor
|
|
649
|
+
? { 'native.cursor': {} }
|
|
650
|
+
: isFactory
|
|
651
|
+
? { 'native.factory': {} }
|
|
652
|
+
: isHermes
|
|
653
|
+
? { 'native.hermes': {} }
|
|
654
|
+
: isOpenCode
|
|
655
|
+
? { 'native.opencode': {} }
|
|
656
|
+
: isOpenClaw
|
|
657
|
+
? { 'native.openclaw': {} }
|
|
658
|
+
: isOpenHuman
|
|
659
|
+
? { 'native.openhuman': {} }
|
|
660
|
+
: isWarp
|
|
661
|
+
? { 'native.warp': {} }
|
|
662
|
+
: isDevinDesktop
|
|
663
|
+
? { 'native.devin-desktop': {
|
|
664
|
+
product: 'Devin Desktop',
|
|
665
|
+
compatibilityProviderId: 'windsurf',
|
|
666
|
+
} }
|
|
667
|
+
: { 'native.generic': {} },
|
|
668
|
+
});
|
|
669
|
+
if (isDryRun(ctx, args)) {
|
|
670
|
+
return preview('import', { source, wouldInspect: true, wouldPersist: false });
|
|
671
|
+
}
|
|
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
|
+
});
|
|
675
|
+
try {
|
|
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,
|
|
746
|
+
});
|
|
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
|
+
}
|
|
770
|
+
finally {
|
|
771
|
+
await lease.release();
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
function providerDisposition(provider) {
|
|
775
|
+
if (provider === 'codex') {
|
|
776
|
+
return {
|
|
777
|
+
provider, disposition: 'implemented', operationalState: 'available',
|
|
778
|
+
supportedOperations: ['discover', 'inspect', 'stream'],
|
|
779
|
+
acquisitionModes: ['api', 'jsonl'],
|
|
780
|
+
reasonCode: null,
|
|
781
|
+
remediation: 'Authorize an App Server export or Codex sessions root, then import an explicit JSONL source.',
|
|
782
|
+
evidence: {
|
|
783
|
+
adapterVersion: CODEX_ADAPTER_VERSION,
|
|
784
|
+
verifiedAt: '2026-07-27',
|
|
785
|
+
documentation: 'https://github.com/openai/codex/blob/main/codex-rs/app-server/README.md',
|
|
786
|
+
},
|
|
787
|
+
};
|
|
788
|
+
}
|
|
789
|
+
if (provider === 'claude') {
|
|
790
|
+
return {
|
|
791
|
+
provider, disposition: 'implemented', operationalState: 'available',
|
|
792
|
+
supportedOperations: ['discover', 'inspect', 'stream'],
|
|
793
|
+
acquisitionModes: ['jsonl', 'hook'],
|
|
794
|
+
reasonCode: null,
|
|
795
|
+
remediation: 'Authorize a Claude projects or hook root, then import an explicit JSONL file.',
|
|
796
|
+
evidence: {
|
|
797
|
+
adapterVersion: CLAUDE_ADAPTER_VERSION,
|
|
798
|
+
verifiedAt: '2026-07-27',
|
|
799
|
+
documentation: 'https://code.claude.com/docs/en/sessions',
|
|
800
|
+
},
|
|
801
|
+
};
|
|
802
|
+
}
|
|
803
|
+
if (provider === 'copilot') {
|
|
804
|
+
return {
|
|
805
|
+
provider, disposition: 'implemented', operationalState: 'available',
|
|
806
|
+
supportedOperations: ['inspect', 'stream'],
|
|
807
|
+
acquisitionModes: ['manual-export'],
|
|
808
|
+
reasonCode: null,
|
|
809
|
+
remediation: 'Use Chat: Export Chat in VS Code, then import the explicitly selected JSON file.',
|
|
810
|
+
evidence: {
|
|
811
|
+
adapterVersion: COPILOT_ADAPTER_VERSION,
|
|
812
|
+
verifiedAt: '2026-07-27',
|
|
813
|
+
documentation: 'https://code.visualstudio.com/docs/chat/chat-sessions#_export-a-chat-session-as-a-json-file',
|
|
814
|
+
},
|
|
815
|
+
};
|
|
816
|
+
}
|
|
817
|
+
if (provider === 'cursor') {
|
|
818
|
+
return {
|
|
819
|
+
provider, disposition: 'implemented', operationalState: 'available',
|
|
820
|
+
supportedOperations: ['discover', 'inspect', 'stream'],
|
|
821
|
+
acquisitionModes: ['api', 'jsonl', 'manual-export'],
|
|
822
|
+
reasonCode: null,
|
|
823
|
+
remediation: 'Authorize a workspace agent-transcripts root, or import Cursor CLI/Cloud Agent exports explicitly.',
|
|
824
|
+
evidence: {
|
|
825
|
+
adapterVersion: CURSOR_ADAPTER_VERSION,
|
|
826
|
+
verifiedAt: '2026-07-27',
|
|
827
|
+
documentation: 'https://docs.cursor.com/en/cli/reference/output-format',
|
|
828
|
+
},
|
|
829
|
+
};
|
|
830
|
+
}
|
|
831
|
+
if (provider === 'factory') {
|
|
832
|
+
return {
|
|
833
|
+
provider, disposition: 'implemented', operationalState: 'available',
|
|
834
|
+
supportedOperations: ['discover', 'inspect', 'stream'],
|
|
835
|
+
acquisitionModes: ['jsonl', 'api'],
|
|
836
|
+
reasonCode: null,
|
|
837
|
+
remediation: 'Authorize a Factory projects root and import a Droid JSONL transcript; API and Exec require explicit negotiated transports.',
|
|
838
|
+
evidence: {
|
|
839
|
+
adapterVersion: FACTORY_ADAPTER_VERSION,
|
|
840
|
+
verifiedAt: '2026-07-27',
|
|
841
|
+
documentation: 'https://docs.factory.ai/reference/hooks-reference',
|
|
842
|
+
},
|
|
843
|
+
};
|
|
844
|
+
}
|
|
845
|
+
if (provider === 'hermes') {
|
|
846
|
+
return {
|
|
847
|
+
provider, disposition: 'implemented', operationalState: 'available',
|
|
848
|
+
supportedOperations: ['inspect', 'stream'],
|
|
849
|
+
acquisitionModes: ['jsonl', 'api', 'sqlite-snapshot'],
|
|
850
|
+
reasonCode: null,
|
|
851
|
+
remediation: 'Use `hermes sessions export` or an explicitly verified sqlite3.backup() snapshot export.',
|
|
852
|
+
evidence: {
|
|
853
|
+
adapterVersion: HERMES_ADAPTER_VERSION,
|
|
854
|
+
verifiedAt: '2026-07-27',
|
|
855
|
+
documentation: 'https://hermes-agent.nousresearch.com/docs/user-guide/sessions/',
|
|
856
|
+
},
|
|
857
|
+
};
|
|
858
|
+
}
|
|
859
|
+
if (provider === 'opencode') {
|
|
860
|
+
return {
|
|
861
|
+
provider, disposition: 'implemented', operationalState: 'available',
|
|
862
|
+
supportedOperations: ['inspect', 'stream'],
|
|
863
|
+
acquisitionModes: ['manual-export', 'api', 'jsonl'],
|
|
864
|
+
reasonCode: null,
|
|
865
|
+
remediation: 'Use `opencode export <sessionID>` or an explicitly authorized negotiated local API/SSE transport.',
|
|
866
|
+
evidence: {
|
|
867
|
+
adapterVersion: OPENCODE_ADAPTER_VERSION,
|
|
868
|
+
verifiedAt: '2026-07-27',
|
|
869
|
+
documentation: 'https://opencode.ai/docs/cli/',
|
|
870
|
+
},
|
|
871
|
+
};
|
|
872
|
+
}
|
|
873
|
+
if (provider === 'openclaw') {
|
|
874
|
+
return {
|
|
875
|
+
provider, disposition: 'implemented', operationalState: 'available',
|
|
876
|
+
supportedOperations: ['inspect', 'stream'],
|
|
877
|
+
acquisitionModes: ['api', 'sqlite-snapshot', 'jsonl'],
|
|
878
|
+
reasonCode: null,
|
|
879
|
+
remediation: 'Use an authorized read-only Gateway transport or a schema-16/event-v3 sqlite3.backup() projection.',
|
|
880
|
+
evidence: {
|
|
881
|
+
adapterVersion: OPENCLAW_ADAPTER_VERSION,
|
|
882
|
+
verifiedAt: '2026-07-27',
|
|
883
|
+
documentation: 'https://docs.openclaw.ai/reference/session-management-compaction',
|
|
884
|
+
},
|
|
885
|
+
};
|
|
886
|
+
}
|
|
887
|
+
if (provider === 'openhuman') {
|
|
888
|
+
return {
|
|
889
|
+
provider, disposition: 'implemented', operationalState: 'available',
|
|
890
|
+
supportedOperations: ['inspect', 'stream'],
|
|
891
|
+
acquisitionModes: ['jsonl'],
|
|
892
|
+
reasonCode: null,
|
|
893
|
+
remediation: 'Import an explicitly selected schema-1 session_raw JSONL file, optionally bundled with thread/turn enrichment.',
|
|
894
|
+
evidence: {
|
|
895
|
+
adapterVersion: OPENHUMAN_ADAPTER_VERSION,
|
|
896
|
+
verifiedAt: '2026-07-27',
|
|
897
|
+
documentation: 'https://github.com/tinyhumansai/openhuman/releases',
|
|
898
|
+
},
|
|
899
|
+
};
|
|
900
|
+
}
|
|
901
|
+
if (provider === 'warp') {
|
|
902
|
+
return {
|
|
903
|
+
provider, disposition: 'manual-only', operationalState: 'available',
|
|
904
|
+
supportedOperations: ['inspect', 'stream'],
|
|
905
|
+
acquisitionModes: ['manual-export'],
|
|
906
|
+
reasonCode: 'LOSSY_MARKDOWN_ONLY',
|
|
907
|
+
remediation: 'Run `/export-to-file` in Warp and import the explicitly selected Markdown file.',
|
|
908
|
+
evidence: {
|
|
909
|
+
adapterVersion: WARP_ADAPTER_VERSION,
|
|
910
|
+
verifiedAt: '2026-07-27',
|
|
911
|
+
documentation: 'https://docs.warp.dev/agent-platform/capabilities/slash-commands',
|
|
912
|
+
},
|
|
913
|
+
};
|
|
914
|
+
}
|
|
915
|
+
if (provider === 'devin-desktop') {
|
|
916
|
+
return {
|
|
917
|
+
provider,
|
|
918
|
+
product: 'Devin Desktop',
|
|
919
|
+
compatibilityAliases: ['windsurf'],
|
|
920
|
+
aliasDeprecatedAfter: '2027-07-27',
|
|
921
|
+
disposition: 'implemented', operationalState: 'available',
|
|
922
|
+
supportedOperations: ['inspect', 'stream'],
|
|
923
|
+
acquisitionModes: ['hook', 'jsonl'],
|
|
924
|
+
reasonCode: null,
|
|
925
|
+
remediation: 'Enable Devin Desktop post_cascade_response_with_transcript, then explicitly select its JSONL output.',
|
|
926
|
+
evidence: {
|
|
927
|
+
adapterVersion: DEVIN_DESKTOP_ADAPTER_VERSION,
|
|
928
|
+
verifiedAt: '2026-07-27',
|
|
929
|
+
documentation: 'https://docs.devin.ai/desktop/cascade/hooks',
|
|
930
|
+
},
|
|
931
|
+
};
|
|
932
|
+
}
|
|
933
|
+
if (provider === 'generic') {
|
|
934
|
+
return {
|
|
935
|
+
provider, disposition: 'manual-only', operationalState: 'available',
|
|
936
|
+
supportedOperations: ['inspect', 'stream'], acquisitionModes: ['manual-export'],
|
|
937
|
+
reasonCode: 'MANUAL_SOURCE_SELECTION_REQUIRED',
|
|
938
|
+
remediation: 'Pass a declared interchange file to `aiwg sessions import`.',
|
|
939
|
+
evidence: { adapterVersion: GENERIC_ADAPTER_VERSION, verifiedAt: '2026-07-26' },
|
|
940
|
+
};
|
|
941
|
+
}
|
|
942
|
+
return {
|
|
943
|
+
provider, disposition: 'unsupported', operationalState: 'unavailable',
|
|
944
|
+
supportedOperations: [], acquisitionModes: [],
|
|
945
|
+
reasonCode: 'ADAPTER_NOT_IMPLEMENTED',
|
|
946
|
+
remediation: 'Use the generic interchange until the provider adapter milestone is delivered.',
|
|
947
|
+
evidence: { adapterVersion: null, verifiedAt: '2026-07-26' },
|
|
948
|
+
};
|
|
949
|
+
}
|
|
950
|
+
function cursorLocatorClass(input) {
|
|
951
|
+
if (input.endsWith('.md') || input.endsWith('.markdown'))
|
|
952
|
+
return 'cursor-editor-markdown';
|
|
953
|
+
if (input.endsWith('.cloud.jsonl'))
|
|
954
|
+
return 'cursor-cloud-events-jsonl';
|
|
955
|
+
if (input.split(/[\\/]+/).includes('agent-transcripts'))
|
|
956
|
+
return 'cursor-agent-transcript-jsonl';
|
|
957
|
+
return 'cursor-cli-stream-json';
|
|
958
|
+
}
|
|
959
|
+
function cursorProviderProfile(locatorClass) {
|
|
960
|
+
if (locatorClass === 'cursor-editor-markdown')
|
|
961
|
+
return 'editor-markdown-lossy';
|
|
962
|
+
if (locatorClass === 'cursor-cloud-events-jsonl')
|
|
963
|
+
return 'cloud-agents-api-v1';
|
|
964
|
+
if (locatorClass === 'cursor-agent-transcript-jsonl')
|
|
965
|
+
return 'agent-transcript-jsonl';
|
|
966
|
+
return 'cli-stream-json';
|
|
967
|
+
}
|
|
968
|
+
function openRepository(ctx, args) {
|
|
969
|
+
const path = databasePath(ctx, args);
|
|
970
|
+
mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
|
|
971
|
+
try {
|
|
972
|
+
return new SessionRepository(path);
|
|
973
|
+
}
|
|
974
|
+
catch (error) {
|
|
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);
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
function databasePath(ctx, args) {
|
|
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');
|
|
986
|
+
}
|
|
987
|
+
function parseArgs(argv) {
|
|
988
|
+
const flags = new Set();
|
|
989
|
+
const values = new Map();
|
|
990
|
+
const positionals = [];
|
|
991
|
+
const valueFlags = new Set([
|
|
992
|
+
'--db', '--provider', '--workspace', '--tag', '--limit', '--cursor', '--source-id',
|
|
993
|
+
'--date-from', '--date-to', '--participant', '--model', '--role', '--tool',
|
|
994
|
+
'--entity', '--sensitivity', '--extraction-state', '--page-size', '--max-documents',
|
|
995
|
+
'--state', '--reviewer', '--reason', '--policy-version', '--min-confidence',
|
|
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',
|
|
1000
|
+
]);
|
|
1001
|
+
let command;
|
|
1002
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
1003
|
+
const arg = argv[index];
|
|
1004
|
+
if (valueFlags.has(arg)) {
|
|
1005
|
+
const value = argv[index + 1];
|
|
1006
|
+
if (!value || value.startsWith('--')) {
|
|
1007
|
+
throw new CliError('MISSING_OPTION_VALUE', `missing value for ${arg}`, EXIT.usage);
|
|
1008
|
+
}
|
|
1009
|
+
values.set(arg, value);
|
|
1010
|
+
index += 1;
|
|
1011
|
+
}
|
|
1012
|
+
else if (arg.startsWith('-')) {
|
|
1013
|
+
flags.add(arg);
|
|
1014
|
+
}
|
|
1015
|
+
else if (!command) {
|
|
1016
|
+
command = arg;
|
|
1017
|
+
}
|
|
1018
|
+
else {
|
|
1019
|
+
positionals.push(arg);
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
return { command, positionals, flags, values };
|
|
1023
|
+
}
|
|
1024
|
+
function requiredPositional(args, index, name) {
|
|
1025
|
+
const value = args.positionals[index];
|
|
1026
|
+
if (!value)
|
|
1027
|
+
throw new CliError('MISSING_ARGUMENT', `missing required ${name}`, EXIT.usage);
|
|
1028
|
+
return value;
|
|
1029
|
+
}
|
|
1030
|
+
function requiredValue(args, flag) {
|
|
1031
|
+
const value = args.values.get(flag);
|
|
1032
|
+
if (!value)
|
|
1033
|
+
throw new CliError('MISSING_ARGUMENT', `missing required ${flag}`, EXIT.usage);
|
|
1034
|
+
return value;
|
|
1035
|
+
}
|
|
1036
|
+
function authorizationContext(ctx, args, operation) {
|
|
1037
|
+
return {
|
|
1038
|
+
actorId: 'local-catalog-owner',
|
|
1039
|
+
workspaceId: normalizeWorkspaceId(ctx.cwd, requiredValue(args, '--workspace')),
|
|
1040
|
+
operation,
|
|
1041
|
+
catalogScope: 'workspace',
|
|
1042
|
+
mode: 'local-owner',
|
|
1043
|
+
};
|
|
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
|
+
}
|
|
1124
|
+
function boundedInteger(value, fallback, min, max, name) {
|
|
1125
|
+
if (value === undefined)
|
|
1126
|
+
return fallback;
|
|
1127
|
+
if (!/^\d+$/.test(value))
|
|
1128
|
+
throw new CliError('INVALID_ARGUMENT', `${name} must be an integer`, EXIT.usage);
|
|
1129
|
+
const parsed = Number(value);
|
|
1130
|
+
if (parsed < min || parsed > max) {
|
|
1131
|
+
throw new CliError('INVALID_ARGUMENT', `${name} must be between ${min} and ${max}`, EXIT.usage);
|
|
1132
|
+
}
|
|
1133
|
+
return parsed;
|
|
1134
|
+
}
|
|
1135
|
+
function boundedNumber(value, fallback, min, max, name) {
|
|
1136
|
+
if (value === undefined)
|
|
1137
|
+
return fallback;
|
|
1138
|
+
const parsed = Number(value);
|
|
1139
|
+
if (!Number.isFinite(parsed) || parsed < min || parsed > max) {
|
|
1140
|
+
throw new CliError('INVALID_ARGUMENT', `${name} must be between ${min} and ${max}`, EXIT.usage);
|
|
1141
|
+
}
|
|
1142
|
+
return parsed;
|
|
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
|
+
}
|
|
1167
|
+
function candidateState(value) {
|
|
1168
|
+
if (value === undefined)
|
|
1169
|
+
return undefined;
|
|
1170
|
+
const states = new Set([
|
|
1171
|
+
'pending', 'accepted', 'rejected', 'deferred', 'promoted', 'superseded',
|
|
1172
|
+
]);
|
|
1173
|
+
if (!states.has(value)) {
|
|
1174
|
+
throw new CliError('INVALID_ARGUMENT', `invalid candidate state: ${value}`, EXIT.usage);
|
|
1175
|
+
}
|
|
1176
|
+
return value;
|
|
1177
|
+
}
|
|
1178
|
+
function dependentAction(value) {
|
|
1179
|
+
const actions = new Set([
|
|
1180
|
+
'revoke', 'supersede', 'retain', 'origin_unavailable', 'delete', 'abort',
|
|
1181
|
+
]);
|
|
1182
|
+
if (!actions.has(value)) {
|
|
1183
|
+
throw new CliError('INVALID_ARGUMENT', `invalid dependent action: ${value}`, EXIT.usage);
|
|
1184
|
+
}
|
|
1185
|
+
return value;
|
|
1186
|
+
}
|
|
1187
|
+
function isDryRun(ctx, args) {
|
|
1188
|
+
return Boolean(ctx.dryRun || args.flags.has('--dry-run'));
|
|
1189
|
+
}
|
|
1190
|
+
function ok(command, data) {
|
|
1191
|
+
return { envelope: envelope(`sessions.${command}`, 'ok', data, null), exitCode: EXIT.ok };
|
|
1192
|
+
}
|
|
1193
|
+
function preview(command, data) {
|
|
1194
|
+
return { envelope: envelope(`sessions.${command}`, 'preview', data, null), exitCode: EXIT.ok };
|
|
1195
|
+
}
|
|
1196
|
+
function envelope(command, status, data, error) {
|
|
1197
|
+
return { contractVersion: JSON_CONTRACT_VERSION, command, status, data, error };
|
|
1198
|
+
}
|
|
1199
|
+
function emit(value) {
|
|
1200
|
+
console.log(JSON.stringify(value, null, 2));
|
|
1201
|
+
}
|
|
1202
|
+
function printHuman(value) {
|
|
1203
|
+
if (value.status === 'preview')
|
|
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
|
+
}
|
|
1223
|
+
console.log(JSON.stringify(value.data, null, 2));
|
|
1224
|
+
}
|
|
1225
|
+
class CliError extends Error {
|
|
1226
|
+
code;
|
|
1227
|
+
exitCode;
|
|
1228
|
+
details;
|
|
1229
|
+
constructor(code, message, exitCode, details) {
|
|
1230
|
+
super(message);
|
|
1231
|
+
this.code = code;
|
|
1232
|
+
this.exitCode = exitCode;
|
|
1233
|
+
this.details = details;
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
1236
|
+
function normalizeError(error) {
|
|
1237
|
+
if (error instanceof CliError) {
|
|
1238
|
+
return {
|
|
1239
|
+
error: { code: error.code, message: error.message, details: error.details },
|
|
1240
|
+
exitCode: error.exitCode,
|
|
1241
|
+
};
|
|
1242
|
+
}
|
|
1243
|
+
if (error instanceof SessionContractError) {
|
|
1244
|
+
const exitCode = error.code === 'UNSUPPORTED_OPERATION' ? EXIT.unsupported : EXIT.contract;
|
|
1245
|
+
return { error: { code: error.code, message: error.message }, exitCode };
|
|
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
|
+
}
|
|
1260
|
+
return {
|
|
1261
|
+
error: { code: 'INTERNAL_ERROR', message: error instanceof Error ? error.message : String(error) },
|
|
1262
|
+
exitCode: EXIT.storage,
|
|
1263
|
+
};
|
|
1264
|
+
}
|
|
1265
|
+
//# sourceMappingURL=sessions.js.map
|