@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
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
import { createReadStream } from 'node:fs';
|
|
2
|
+
import { access, mkdir, readFile, realpath, rename, stat, writeFile, } from 'node:fs/promises';
|
|
3
|
+
import { homedir, userInfo } from 'node:os';
|
|
4
|
+
import { basename, dirname, join, resolve } from 'node:path';
|
|
5
|
+
import { createInterface } from 'node:readline';
|
|
6
|
+
import { ClaudeSessionAdapter } from './adapters/claude.js';
|
|
7
|
+
import { CodexSessionAdapter } from './adapters/codex.js';
|
|
8
|
+
import { CursorSessionAdapter } from './adapters/cursor.js';
|
|
9
|
+
import { FactorySessionAdapter } from './adapters/factory.js';
|
|
10
|
+
import { SESSION_PROVIDER_IDS, sha256, } from './contracts.js';
|
|
11
|
+
import { redactSourceLocator } from './discovery.js';
|
|
12
|
+
import { fingerprintSourceFile } from './readers.js';
|
|
13
|
+
export const DISCOVERY_MANIFEST_VERSION = '1.0.0';
|
|
14
|
+
const MANUAL_EXPORT_PROVIDERS = new Set([
|
|
15
|
+
'copilot', 'hermes', 'opencode', 'openclaw', 'openhuman',
|
|
16
|
+
'warp', 'devin-desktop', 'generic',
|
|
17
|
+
]);
|
|
18
|
+
export async function discoverWorkspaceHistories(options) {
|
|
19
|
+
const workspacePath = await canonicalPath(options.workspace);
|
|
20
|
+
const workspaceId = workspacePath;
|
|
21
|
+
const providerHomes = providerHomeCandidates(options.providerHome, options.operatorHome);
|
|
22
|
+
const keyWithLeadingDash = workspaceKey(workspacePath, true);
|
|
23
|
+
const keyWithoutLeadingDash = workspaceKey(workspacePath, false);
|
|
24
|
+
const discoverable = [
|
|
25
|
+
{
|
|
26
|
+
provider: 'claude',
|
|
27
|
+
adapter: new ClaudeSessionAdapter(),
|
|
28
|
+
roots: providerHomes.map((providerHome) => join(providerHome, '.claude', 'projects', keyWithLeadingDash)),
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
provider: 'codex',
|
|
32
|
+
adapter: new CodexSessionAdapter(),
|
|
33
|
+
roots: options.codexRoot
|
|
34
|
+
? [resolve(options.codexRoot)]
|
|
35
|
+
: options.providerHome
|
|
36
|
+
? [join(resolve(options.providerHome), '.codex', 'sessions')]
|
|
37
|
+
: [],
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
provider: 'cursor',
|
|
41
|
+
adapter: new CursorSessionAdapter(),
|
|
42
|
+
roots: providerHomes.map((providerHome) => join(providerHome, '.cursor', 'projects', keyWithoutLeadingDash, 'agent-transcripts')),
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
provider: 'factory',
|
|
46
|
+
adapter: new FactorySessionAdapter(),
|
|
47
|
+
roots: providerHomes.flatMap((providerHome) => [
|
|
48
|
+
join(providerHome, '.factory', 'projects', keyWithLeadingDash),
|
|
49
|
+
join(providerHome, '.factory', 'sessions', keyWithLeadingDash),
|
|
50
|
+
]),
|
|
51
|
+
},
|
|
52
|
+
];
|
|
53
|
+
const reports = new Map();
|
|
54
|
+
const candidates = [];
|
|
55
|
+
const seen = new Set();
|
|
56
|
+
for (const entry of discoverable) {
|
|
57
|
+
const availableRoots = [];
|
|
58
|
+
for (const root of entry.roots) {
|
|
59
|
+
if (await pathExists(root))
|
|
60
|
+
availableRoots.push(await canonicalPath(root));
|
|
61
|
+
}
|
|
62
|
+
if (availableRoots.length === 0) {
|
|
63
|
+
const codexNeedsAuthorization = entry.provider === 'codex'
|
|
64
|
+
&& entry.roots.length === 0;
|
|
65
|
+
reports.set(entry.provider, providerReport(entry.provider, codexNeedsAuthorization ? 'export-required' : 'unavailable', codexNeedsAuthorization ? 'manual-export' : 'discoverable', [], codexNeedsAuthorization
|
|
66
|
+
? 'SHARED_ROOT_AUTHORIZATION_REQUIRED'
|
|
67
|
+
: 'PROVIDER_ROOT_UNAVAILABLE', codexNeedsAuthorization
|
|
68
|
+
? 'Pass --codex-root with an explicitly authorized Codex sessions or App Server export root.'
|
|
69
|
+
: `No authorized ${entry.provider} workspace history root was found.`));
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
const scope = {
|
|
73
|
+
workspaceId,
|
|
74
|
+
allowedRoots: availableRoots,
|
|
75
|
+
};
|
|
76
|
+
const providerSources = [];
|
|
77
|
+
for await (const descriptor of entry.adapter.discover(scope)) {
|
|
78
|
+
const locator = await canonicalPath(descriptor.locator);
|
|
79
|
+
if (entry.provider === 'codex'
|
|
80
|
+
&& !await codexSourceMatchesWorkspace(locator, workspacePath))
|
|
81
|
+
continue;
|
|
82
|
+
const details = await stat(locator);
|
|
83
|
+
const authorizedRoot = scope.allowedRoots.find((root) => locator === root || locator.startsWith(`${root}/`));
|
|
84
|
+
if (!authorizedRoot)
|
|
85
|
+
continue;
|
|
86
|
+
const fingerprint = await fingerprintSourceFile({
|
|
87
|
+
selectedPath: locator,
|
|
88
|
+
allowedRoots: [authorizedRoot],
|
|
89
|
+
});
|
|
90
|
+
const dedupeKey = `${entry.provider}\0${fingerprint.digest}\0${fingerprint.size}`;
|
|
91
|
+
if (seen.has(dedupeKey))
|
|
92
|
+
continue;
|
|
93
|
+
seen.add(dedupeKey);
|
|
94
|
+
const source = sourceFromDescriptor(descriptor, locator, authorizedRoot, details, fingerprint.digest);
|
|
95
|
+
providerSources.push(source);
|
|
96
|
+
candidates.push(source);
|
|
97
|
+
}
|
|
98
|
+
reports.set(entry.provider, providerReport(entry.provider, 'checked', 'discoverable', providerSources, providerSources.length === 0 ? 'NO_WORKSPACE_SOURCES' : null, providerSources.length === 0
|
|
99
|
+
? `The ${entry.provider} root was checked, but no source matched the authorized workspace.`
|
|
100
|
+
: null));
|
|
101
|
+
}
|
|
102
|
+
for (const provider of SESSION_PROVIDER_IDS) {
|
|
103
|
+
if (reports.has(provider))
|
|
104
|
+
continue;
|
|
105
|
+
const manual = MANUAL_EXPORT_PROVIDERS.has(provider);
|
|
106
|
+
reports.set(provider, providerReport(provider, manual ? 'export-required' : 'not-checked', manual ? 'manual-export' : 'unsupported', [], manual ? 'EXPLICIT_EXPORT_REQUIRED' : 'PROVIDER_NOT_CHECKED', manual
|
|
107
|
+
? `Select and authorize a supported ${provider} export before importing it.`
|
|
108
|
+
: `No discovery strategy is registered for ${provider}.`));
|
|
109
|
+
}
|
|
110
|
+
const sources = candidates.sort(compareSources);
|
|
111
|
+
const providers = [...reports.values()].sort((a, b) => a.provider.localeCompare(b.provider));
|
|
112
|
+
const identity = {
|
|
113
|
+
schemaVersion: DISCOVERY_MANIFEST_VERSION,
|
|
114
|
+
workspaceId,
|
|
115
|
+
sources: sources.map((source) => ({
|
|
116
|
+
sourceId: source.sourceId,
|
|
117
|
+
provider: source.provider,
|
|
118
|
+
locatorClass: source.locatorClass,
|
|
119
|
+
locator: source.locator,
|
|
120
|
+
modifiedAt: source.modifiedAt,
|
|
121
|
+
sizeBytes: source.sizeBytes,
|
|
122
|
+
digest: source.digest,
|
|
123
|
+
})),
|
|
124
|
+
providers: providers.map((provider) => ({
|
|
125
|
+
provider: provider.provider,
|
|
126
|
+
status: provider.status,
|
|
127
|
+
reasonCode: provider.reasonCode,
|
|
128
|
+
})),
|
|
129
|
+
};
|
|
130
|
+
return {
|
|
131
|
+
schemaVersion: DISCOVERY_MANIFEST_VERSION,
|
|
132
|
+
manifestId: sha256(JSON.stringify(identity)),
|
|
133
|
+
createdAt: options.createdAt ?? new Date().toISOString(),
|
|
134
|
+
workspaceId,
|
|
135
|
+
workspacePath,
|
|
136
|
+
providers,
|
|
137
|
+
sources,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
export function defaultDiscoveryManifestPath(workspace) {
|
|
141
|
+
return resolve(workspace, '.aiwg', 'sessions', 'discovery-manifest.json');
|
|
142
|
+
}
|
|
143
|
+
export async function writeDiscoveryManifest(path, manifest) {
|
|
144
|
+
await mkdir(dirname(path), { recursive: true, mode: 0o700 });
|
|
145
|
+
const temporary = `${path}.tmp-${process.pid}`;
|
|
146
|
+
await writeFile(temporary, `${JSON.stringify(manifest, null, 2)}\n`, { mode: 0o600 });
|
|
147
|
+
await rename(temporary, path);
|
|
148
|
+
}
|
|
149
|
+
export async function readDiscoveryManifest(path) {
|
|
150
|
+
const value = JSON.parse(await readFile(path, 'utf8'));
|
|
151
|
+
if (value.schemaVersion !== DISCOVERY_MANIFEST_VERSION
|
|
152
|
+
|| typeof value.manifestId !== 'string'
|
|
153
|
+
|| typeof value.workspaceId !== 'string'
|
|
154
|
+
|| !Array.isArray(value.providers)
|
|
155
|
+
|| !Array.isArray(value.sources)) {
|
|
156
|
+
throw new Error('session discovery manifest is malformed or unsupported');
|
|
157
|
+
}
|
|
158
|
+
const expected = await rediscoverManifestIdentity(value);
|
|
159
|
+
if (expected !== value.manifestId) {
|
|
160
|
+
throw new Error('session discovery manifest identity does not match its contents');
|
|
161
|
+
}
|
|
162
|
+
return value;
|
|
163
|
+
}
|
|
164
|
+
export function publicDiscoveryManifest(manifest) {
|
|
165
|
+
return {
|
|
166
|
+
schemaVersion: manifest.schemaVersion,
|
|
167
|
+
manifestId: manifest.manifestId,
|
|
168
|
+
createdAt: manifest.createdAt,
|
|
169
|
+
workspaceId: manifest.workspaceId,
|
|
170
|
+
workspacePath: manifest.workspacePath,
|
|
171
|
+
providers: manifest.providers,
|
|
172
|
+
sources: manifest.sources.map(({ locator: _locator, authorizedRoot: _root, ...source }) => source),
|
|
173
|
+
totals: {
|
|
174
|
+
providers: manifest.providers.length,
|
|
175
|
+
sources: manifest.sources.length,
|
|
176
|
+
},
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
function sourceFromDescriptor(descriptor, locator, authorizedRoot, details, digest) {
|
|
180
|
+
return {
|
|
181
|
+
sourceId: sha256([
|
|
182
|
+
'workspace-source-v1', descriptor.provider, descriptor.locatorClass, digest,
|
|
183
|
+
].join('\0')),
|
|
184
|
+
provider: descriptor.provider,
|
|
185
|
+
locator,
|
|
186
|
+
redactedLocator: redactSourceLocator(locator),
|
|
187
|
+
locatorClass: descriptor.locatorClass,
|
|
188
|
+
authorizedRoot,
|
|
189
|
+
modifiedAt: details.mtime.toISOString(),
|
|
190
|
+
sizeBytes: Number(details.size),
|
|
191
|
+
digest,
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
function providerReport(provider, status, disposition, sources, reasonCode, remediation) {
|
|
195
|
+
const timestamps = sources.map((source) => source.modifiedAt).sort();
|
|
196
|
+
return {
|
|
197
|
+
provider,
|
|
198
|
+
status,
|
|
199
|
+
disposition,
|
|
200
|
+
sourceCount: sources.length,
|
|
201
|
+
dateRange: {
|
|
202
|
+
earliest: timestamps.at(0) ?? null,
|
|
203
|
+
latest: timestamps.at(-1) ?? null,
|
|
204
|
+
},
|
|
205
|
+
dateRangeBasis: 'source-mtime',
|
|
206
|
+
reasonCode,
|
|
207
|
+
remediation,
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
async function rediscoverManifestIdentity(manifest) {
|
|
211
|
+
const identity = {
|
|
212
|
+
schemaVersion: manifest.schemaVersion,
|
|
213
|
+
workspaceId: manifest.workspaceId,
|
|
214
|
+
sources: manifest.sources.map((source) => ({
|
|
215
|
+
sourceId: source.sourceId,
|
|
216
|
+
provider: source.provider,
|
|
217
|
+
locatorClass: source.locatorClass,
|
|
218
|
+
locator: source.locator,
|
|
219
|
+
modifiedAt: source.modifiedAt,
|
|
220
|
+
sizeBytes: source.sizeBytes,
|
|
221
|
+
digest: source.digest,
|
|
222
|
+
})),
|
|
223
|
+
providers: manifest.providers.map((provider) => ({
|
|
224
|
+
provider: provider.provider,
|
|
225
|
+
status: provider.status,
|
|
226
|
+
reasonCode: provider.reasonCode,
|
|
227
|
+
})),
|
|
228
|
+
};
|
|
229
|
+
return sha256(JSON.stringify(identity));
|
|
230
|
+
}
|
|
231
|
+
async function codexSourceMatchesWorkspace(locator, workspacePath) {
|
|
232
|
+
const input = createReadStream(locator, { encoding: 'utf8' });
|
|
233
|
+
const lines = createInterface({ input, crlfDelay: Infinity });
|
|
234
|
+
let count = 0;
|
|
235
|
+
try {
|
|
236
|
+
for await (const line of lines) {
|
|
237
|
+
if (++count > 50)
|
|
238
|
+
break;
|
|
239
|
+
let value;
|
|
240
|
+
try {
|
|
241
|
+
value = JSON.parse(line);
|
|
242
|
+
}
|
|
243
|
+
catch {
|
|
244
|
+
continue;
|
|
245
|
+
}
|
|
246
|
+
const cwd = codexWorkspaceField(value);
|
|
247
|
+
if (!cwd)
|
|
248
|
+
continue;
|
|
249
|
+
try {
|
|
250
|
+
return await canonicalPath(cwd) === workspacePath;
|
|
251
|
+
}
|
|
252
|
+
catch {
|
|
253
|
+
return resolve(cwd) === workspacePath;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
return false;
|
|
257
|
+
}
|
|
258
|
+
finally {
|
|
259
|
+
lines.close();
|
|
260
|
+
input.destroy();
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
function codexWorkspaceField(value) {
|
|
264
|
+
const root = asObject(value);
|
|
265
|
+
const payload = asObject(root.payload);
|
|
266
|
+
const result = asObject(root.result);
|
|
267
|
+
const resultThread = asObject(result.thread);
|
|
268
|
+
const params = asObject(root.params);
|
|
269
|
+
const paramsThread = asObject(params.thread);
|
|
270
|
+
for (const candidate of [
|
|
271
|
+
payload.cwd, result.cwd, resultThread.cwd, params.cwd, paramsThread.cwd,
|
|
272
|
+
]) {
|
|
273
|
+
if (typeof candidate === 'string' && candidate.length > 0)
|
|
274
|
+
return candidate;
|
|
275
|
+
}
|
|
276
|
+
return null;
|
|
277
|
+
}
|
|
278
|
+
function asObject(value) {
|
|
279
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value)
|
|
280
|
+
? value
|
|
281
|
+
: {};
|
|
282
|
+
}
|
|
283
|
+
async function canonicalPath(path) {
|
|
284
|
+
return realpath(resolve(path));
|
|
285
|
+
}
|
|
286
|
+
function workspaceKey(path, retainLeadingSeparator) {
|
|
287
|
+
const normalized = path.replace(/\\/g, '/');
|
|
288
|
+
const input = retainLeadingSeparator ? normalized : normalized.replace(/^\/+/, '');
|
|
289
|
+
return input.replace(/[/:]+/g, '-');
|
|
290
|
+
}
|
|
291
|
+
function providerHomeCandidates(explicit, operatorHome) {
|
|
292
|
+
if (explicit)
|
|
293
|
+
return [resolve(explicit)];
|
|
294
|
+
let accountHome = operatorHome;
|
|
295
|
+
if (!accountHome) {
|
|
296
|
+
try {
|
|
297
|
+
accountHome = userInfo().homedir;
|
|
298
|
+
}
|
|
299
|
+
catch {
|
|
300
|
+
accountHome = undefined;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
return [...new Set([homedir(), accountHome].filter((candidate) => Boolean(candidate)).map((candidate) => resolve(candidate)))];
|
|
304
|
+
}
|
|
305
|
+
async function pathExists(path) {
|
|
306
|
+
try {
|
|
307
|
+
await access(path);
|
|
308
|
+
return true;
|
|
309
|
+
}
|
|
310
|
+
catch {
|
|
311
|
+
return false;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
function compareSources(left, right) {
|
|
315
|
+
return left.provider.localeCompare(right.provider)
|
|
316
|
+
|| left.locator.localeCompare(right.locator)
|
|
317
|
+
|| basename(left.locator).localeCompare(basename(right.locator));
|
|
318
|
+
}
|
|
319
|
+
//# sourceMappingURL=workspace-discovery.js.map
|