@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
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
import { mkdir, readFile, rename, stat, writeFile, appendFile, readdir } from 'fs/promises';
|
|
2
|
-
import { existsSync } from 'fs';
|
|
2
|
+
import { createReadStream, existsSync } from 'fs';
|
|
3
3
|
import { createHash } from 'crypto';
|
|
4
|
+
import { createInterface } from 'readline';
|
|
4
5
|
import path from 'path';
|
|
5
6
|
import os from 'os';
|
|
6
7
|
import { readAiwgConfig } from '../config/aiwg-config.js';
|
|
7
8
|
import { PROJECT_AIWG_LOCATION_FILE, projectAiwgPath } from '../config/project-artifacts.js';
|
|
8
9
|
const DEFAULT_MAX_BYTES = 1_048_576;
|
|
9
10
|
const DEFAULT_REPORT_LIMIT = 20;
|
|
11
|
+
const MAX_TRANSCRIPT_LINE_BYTES = 1_048_576;
|
|
12
|
+
const MAX_TRANSCRIPT_BYTES = 256 * 1_048_576;
|
|
13
|
+
const MAX_TRANSCRIPT_RECORDS = 1_000_000;
|
|
10
14
|
export async function maybeAppendSkillUsage(input) {
|
|
11
15
|
const env = input.env ?? process.env;
|
|
12
16
|
const resolved = await resolveSkillUsageSettings(input.cwd, env);
|
|
@@ -43,15 +47,38 @@ export async function ingestSkillUsageTranscript(options) {
|
|
|
43
47
|
const contextCwd = options.projectRoot ?? options.cwd;
|
|
44
48
|
const resolved = await resolveSkillUsageSettings(contextCwd, env);
|
|
45
49
|
if (!resolved.enabled || resolved.scopes.length === 0) {
|
|
46
|
-
return {
|
|
50
|
+
return {
|
|
51
|
+
events: [], appended: 0, skipped: 0, paths: [],
|
|
52
|
+
receipt: {
|
|
53
|
+
source_generation: '', provider: normalizeProvider(options.provider),
|
|
54
|
+
records_read: 0, events_extracted: 0, events_appended: 0,
|
|
55
|
+
duplicates_skipped: 0, malformed_skipped: 0,
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
const provider = normalizeProvider(options.provider);
|
|
60
|
+
if (provider !== 'claude-code') {
|
|
61
|
+
throw new Error(`unsupported skill-usage transcript provider: ${provider}`);
|
|
47
62
|
}
|
|
48
|
-
const
|
|
49
|
-
const
|
|
63
|
+
const sourceStat = await stat(options.transcriptPath);
|
|
64
|
+
const sourceGeneration = digest([
|
|
65
|
+
provider, String(sourceStat.size), String(sourceStat.mtimeMs),
|
|
66
|
+
].join('\0'));
|
|
50
67
|
const version = await readAiwgVersion(options.frameworkRoot);
|
|
51
68
|
const context = await resolvePathContext(contextCwd);
|
|
52
69
|
const extracted = [];
|
|
53
70
|
let skipped = 0;
|
|
54
|
-
|
|
71
|
+
let recordsRead = 0;
|
|
72
|
+
let bytesRead = 0;
|
|
73
|
+
for await (const line of streamTranscriptLines(options.transcriptPath)) {
|
|
74
|
+
recordsRead += 1;
|
|
75
|
+
bytesRead += Buffer.byteLength(line) + 1;
|
|
76
|
+
if (recordsRead > MAX_TRANSCRIPT_RECORDS || bytesRead > MAX_TRANSCRIPT_BYTES) {
|
|
77
|
+
throw new Error('skill-usage transcript exceeds bounded ingestion limits');
|
|
78
|
+
}
|
|
79
|
+
if (Buffer.byteLength(line) > MAX_TRANSCRIPT_LINE_BYTES) {
|
|
80
|
+
throw new Error('skill-usage transcript record exceeds bounded line limit');
|
|
81
|
+
}
|
|
55
82
|
let parsed;
|
|
56
83
|
try {
|
|
57
84
|
parsed = JSON.parse(line);
|
|
@@ -65,22 +92,50 @@ export async function ingestSkillUsageTranscript(options) {
|
|
|
65
92
|
skipped += 1;
|
|
66
93
|
}
|
|
67
94
|
else {
|
|
68
|
-
extracted.push(...events
|
|
95
|
+
extracted.push(...events.map(event => ({
|
|
96
|
+
...event,
|
|
97
|
+
occurredAt: sourceOccurrenceTime(parsed),
|
|
98
|
+
nativeEventId: sourceNativeEventId(parsed),
|
|
99
|
+
position: recordsRead,
|
|
100
|
+
contentDigest: digest(line),
|
|
101
|
+
})));
|
|
69
102
|
}
|
|
70
103
|
}
|
|
71
104
|
const persisted = [];
|
|
72
105
|
const paths = resolved.scopes
|
|
73
106
|
.map(scope => resolveUsagePath(scope, context.projectRoot, env))
|
|
74
107
|
.filter((p) => Boolean(p));
|
|
108
|
+
const knownEventIdsByPath = new Map();
|
|
109
|
+
for (const filePath of paths) {
|
|
110
|
+
const knownEventIds = new Set();
|
|
111
|
+
for (const event of await readJsonlEvents(filePath)) {
|
|
112
|
+
if (event.event_id)
|
|
113
|
+
knownEventIds.add(event.event_id);
|
|
114
|
+
}
|
|
115
|
+
knownEventIdsByPath.set(filePath, knownEventIds);
|
|
116
|
+
}
|
|
117
|
+
let duplicatesSkipped = 0;
|
|
75
118
|
for (const scope of resolved.scopes) {
|
|
76
119
|
const filePath = resolveUsagePath(scope, context.projectRoot, env);
|
|
77
120
|
if (!filePath)
|
|
78
121
|
continue;
|
|
122
|
+
const knownEventIds = knownEventIdsByPath.get(filePath) ?? new Set();
|
|
79
123
|
for (const item of extracted) {
|
|
124
|
+
const eventId = digest([
|
|
125
|
+
sourceGeneration,
|
|
126
|
+
item.nativeEventId ?? String(item.position),
|
|
127
|
+
item.contentDigest,
|
|
128
|
+
item.artifact.kind,
|
|
129
|
+
item.artifact.id,
|
|
130
|
+
].join('\0'));
|
|
131
|
+
if (knownEventIds.has(eventId)) {
|
|
132
|
+
duplicatesSkipped += 1;
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
80
135
|
const event = buildUsageEvent({
|
|
81
136
|
env,
|
|
82
137
|
source: 'transcript',
|
|
83
|
-
provider
|
|
138
|
+
provider,
|
|
84
139
|
artifact: item.artifact,
|
|
85
140
|
action: item.action,
|
|
86
141
|
outcome: 'unknown',
|
|
@@ -88,8 +143,13 @@ export async function ingestSkillUsageTranscript(options) {
|
|
|
88
143
|
cwd: context.projectRoot ?? contextCwd,
|
|
89
144
|
context,
|
|
90
145
|
scope,
|
|
146
|
+
occurredAt: item.occurredAt,
|
|
147
|
+
eventId,
|
|
148
|
+
sourceGeneration,
|
|
149
|
+
nativeEventId: item.nativeEventId,
|
|
91
150
|
});
|
|
92
151
|
persisted.push(event);
|
|
152
|
+
knownEventIds.add(eventId);
|
|
93
153
|
if (!options.dryRun) {
|
|
94
154
|
await appendBoundedJsonl(filePath, event, resolved.maxBytes);
|
|
95
155
|
}
|
|
@@ -100,6 +160,15 @@ export async function ingestSkillUsageTranscript(options) {
|
|
|
100
160
|
appended: options.dryRun ? 0 : persisted.length,
|
|
101
161
|
skipped,
|
|
102
162
|
paths,
|
|
163
|
+
receipt: {
|
|
164
|
+
source_generation: sourceGeneration,
|
|
165
|
+
provider,
|
|
166
|
+
records_read: recordsRead,
|
|
167
|
+
events_extracted: extracted.length,
|
|
168
|
+
events_appended: options.dryRun ? 0 : persisted.length,
|
|
169
|
+
duplicates_skipped: duplicatesSkipped,
|
|
170
|
+
malformed_skipped: skipped,
|
|
171
|
+
},
|
|
103
172
|
};
|
|
104
173
|
}
|
|
105
174
|
export async function readSkillUsageReport(options) {
|
|
@@ -131,6 +200,10 @@ export async function readSkillUsageReport(options) {
|
|
|
131
200
|
limit: 5,
|
|
132
201
|
}),
|
|
133
202
|
paths,
|
|
203
|
+
window: {
|
|
204
|
+
retained_segments: paths.reduce((count, filePath) => count + retainedUsagePaths(filePath).filter(existsSync).length, 0),
|
|
205
|
+
truncated_before: paths.some(filePath => existsSync(`${filePath}.1`)),
|
|
206
|
+
},
|
|
134
207
|
};
|
|
135
208
|
}
|
|
136
209
|
export async function printSkillUsageReport(options) {
|
|
@@ -205,10 +278,17 @@ function classifyCliUsage(command, args) {
|
|
|
205
278
|
return { kind: 'command', id: command, action: 'invoke' };
|
|
206
279
|
}
|
|
207
280
|
function buildUsageEvent(input) {
|
|
281
|
+
const observedTimestamp = new Date().toISOString();
|
|
208
282
|
const event = {
|
|
209
|
-
schema_version: 1,
|
|
283
|
+
schema_version: input.eventId ? 2 : 1,
|
|
210
284
|
event_type: 'aiwg.skill_usage',
|
|
211
|
-
timestamp:
|
|
285
|
+
timestamp: input.occurredAt ?? observedTimestamp,
|
|
286
|
+
...(input.eventId ? {
|
|
287
|
+
observed_timestamp: observedTimestamp,
|
|
288
|
+
event_id: input.eventId,
|
|
289
|
+
source_generation: input.sourceGeneration,
|
|
290
|
+
native_event_id: input.nativeEventId,
|
|
291
|
+
} : {}),
|
|
212
292
|
invocation_id: input.env.AIWG_INVOCATION_ID,
|
|
213
293
|
source: input.source,
|
|
214
294
|
provider: input.provider,
|
|
@@ -294,6 +374,25 @@ function normalizeProvider(value) {
|
|
|
294
374
|
return 'claude-code';
|
|
295
375
|
return normalized || 'unknown';
|
|
296
376
|
}
|
|
377
|
+
function sourceOccurrenceTime(value) {
|
|
378
|
+
if (!isRecord(value))
|
|
379
|
+
return undefined;
|
|
380
|
+
for (const key of ['timestamp', 'created_at', 'createdAt', 'time']) {
|
|
381
|
+
const candidate = value[key];
|
|
382
|
+
if (typeof candidate === 'string' && !Number.isNaN(Date.parse(candidate))) {
|
|
383
|
+
return new Date(candidate).toISOString();
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
return undefined;
|
|
387
|
+
}
|
|
388
|
+
function sourceNativeEventId(value) {
|
|
389
|
+
if (!isRecord(value))
|
|
390
|
+
return undefined;
|
|
391
|
+
return firstString(value, ['uuid', 'event_id', 'eventId', 'id', 'message_id']);
|
|
392
|
+
}
|
|
393
|
+
function digest(value) {
|
|
394
|
+
return createHash('sha256').update(value).digest('hex');
|
|
395
|
+
}
|
|
297
396
|
function isArtifactKind(value) {
|
|
298
397
|
return value === 'skill' ||
|
|
299
398
|
value === 'agent' ||
|
|
@@ -393,15 +492,20 @@ function resolveUsagePath(scope, projectRoot, env) {
|
|
|
393
492
|
}
|
|
394
493
|
async function appendBoundedJsonl(filePath, event, maxBytes) {
|
|
395
494
|
await mkdir(path.dirname(filePath), { recursive: true });
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
495
|
+
const line = JSON.stringify(event) + '\n';
|
|
496
|
+
const lineBytes = Buffer.byteLength(line);
|
|
497
|
+
if (maxBytes > 0 && lineBytes > maxBytes)
|
|
498
|
+
event.oversized_record = true;
|
|
499
|
+
const finalLine = JSON.stringify(event) + '\n';
|
|
500
|
+
await rotateIfNeeded(filePath, maxBytes, Buffer.byteLength(finalLine));
|
|
501
|
+
await appendFile(filePath, finalLine, 'utf8');
|
|
502
|
+
}
|
|
503
|
+
async function rotateIfNeeded(filePath, maxBytes, appendBytes) {
|
|
400
504
|
if (maxBytes <= 0)
|
|
401
505
|
return;
|
|
402
506
|
try {
|
|
403
507
|
const current = await stat(filePath);
|
|
404
|
-
if (current.size <= maxBytes)
|
|
508
|
+
if (current.size === 0 || current.size + appendBytes <= maxBytes)
|
|
405
509
|
return;
|
|
406
510
|
const rotated = `${filePath}.1`;
|
|
407
511
|
if (existsSync(rotated)) {
|
|
@@ -415,18 +519,36 @@ async function rotateIfNeeded(filePath, maxBytes) {
|
|
|
415
519
|
}
|
|
416
520
|
}
|
|
417
521
|
async function readJsonlEvents(filePath) {
|
|
522
|
+
const events = [];
|
|
523
|
+
for (const retainedPath of retainedUsagePaths(filePath)) {
|
|
524
|
+
try {
|
|
525
|
+
for await (const line of streamTranscriptLines(retainedPath)) {
|
|
526
|
+
if (line.trim())
|
|
527
|
+
events.push(JSON.parse(line));
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
catch (error) {
|
|
531
|
+
if (error.code !== 'ENOENT')
|
|
532
|
+
throw error;
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
return events;
|
|
536
|
+
}
|
|
537
|
+
function retainedUsagePaths(filePath) {
|
|
538
|
+
return [`${filePath}.1`, filePath];
|
|
539
|
+
}
|
|
540
|
+
async function* streamTranscriptLines(filePath) {
|
|
541
|
+
const input = createReadStream(filePath, { encoding: 'utf8' });
|
|
542
|
+
const lines = createInterface({ input, crlfDelay: Infinity });
|
|
418
543
|
try {
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
.filter(Boolean)
|
|
424
|
-
.map(line => JSON.parse(line));
|
|
544
|
+
for await (const line of lines) {
|
|
545
|
+
if (line.trim())
|
|
546
|
+
yield line;
|
|
547
|
+
}
|
|
425
548
|
}
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
throw error;
|
|
549
|
+
finally {
|
|
550
|
+
lines.close();
|
|
551
|
+
input.destroy();
|
|
430
552
|
}
|
|
431
553
|
}
|
|
432
554
|
function summarizeEvents(events) {
|
|
@@ -15,6 +15,7 @@ import { normalizeNamedCaptures } from '../artifacts/index-builder.js';
|
|
|
15
15
|
import { getProviderDefinition, PROVIDER_IDS, resolveProviderPathValue, } from '../providers/provider-definitions.js';
|
|
16
16
|
import { validateAuthorization, } from '../policy/authorization.js';
|
|
17
17
|
import { projectAiwgPath, resolveProjectAiwgDir } from './project-artifacts.js';
|
|
18
|
+
import { defaultThreatAssessmentConfig, validateThreatAssessmentConfig, } from '../security/threat-assessment-config.js';
|
|
18
19
|
const CONFIG_FILENAME = 'aiwg.config';
|
|
19
20
|
/**
|
|
20
21
|
* Operations that a workspace may authorize for one member repository.
|
|
@@ -633,6 +634,9 @@ export function emptyConfig(providers = ['claude']) {
|
|
|
633
634
|
providers,
|
|
634
635
|
installed: {},
|
|
635
636
|
scripts: {},
|
|
637
|
+
security: {
|
|
638
|
+
threatAssessment: defaultThreatAssessmentConfig(),
|
|
639
|
+
},
|
|
636
640
|
delivery: {
|
|
637
641
|
mode: 'pr-required',
|
|
638
642
|
default_branch: 'main',
|
|
@@ -711,12 +715,20 @@ export async function readAiwgConfig(projectDir) {
|
|
|
711
715
|
if (authorizationErrors.length > 0) {
|
|
712
716
|
throw new Error(`Invalid .aiwg/aiwg.config:\n${authorizationErrors.map(item => item.message).join('\n')}`);
|
|
713
717
|
}
|
|
718
|
+
const threatAssessmentErrors = validateThreatAssessmentConfig(parsed.security?.threatAssessment);
|
|
719
|
+
if (threatAssessmentErrors.length > 0) {
|
|
720
|
+
throw new Error(`Invalid .aiwg/aiwg.config:\n${threatAssessmentErrors.join('\n')}`);
|
|
721
|
+
}
|
|
714
722
|
return parsed;
|
|
715
723
|
}
|
|
716
724
|
/**
|
|
717
725
|
* Write aiwg.config, creating the resolved AIWG artifact directory if needed.
|
|
718
726
|
*/
|
|
719
727
|
export async function writeAiwgConfig(projectDir, config) {
|
|
728
|
+
const threatAssessmentErrors = validateThreatAssessmentConfig(config.security?.threatAssessment);
|
|
729
|
+
if (threatAssessmentErrors.length > 0) {
|
|
730
|
+
throw new Error(`Invalid .aiwg/aiwg.config:\n${threatAssessmentErrors.join('\n')}`);
|
|
731
|
+
}
|
|
720
732
|
const dir = resolveProjectAiwgDir(projectDir);
|
|
721
733
|
await mkdir(dir, { recursive: true });
|
|
722
734
|
const filePath = join(dir, CONFIG_FILENAME);
|
package/dist/src/config/cli.js
CHANGED
|
@@ -152,6 +152,7 @@ const ENUM_RULES = {
|
|
|
152
152
|
'remotes.tracker_actor.via': ['tea', 'gh', 'mcp', 'api'],
|
|
153
153
|
'remotes.transport.protocol': ['ssh', 'https'],
|
|
154
154
|
'repo_maintainer.tiers.local': ['collaborator', 'maintainer', 'admin'],
|
|
155
|
+
'security.threatAssessment.mode': ['off', 'audit', 'enforce'],
|
|
155
156
|
};
|
|
156
157
|
const BOOLEAN_FIELDS = new Set([
|
|
157
158
|
'delivery.delete_branch_on_merge',
|
|
@@ -214,15 +215,17 @@ async function projectConfigSet(key, raw, args) {
|
|
|
214
215
|
}
|
|
215
216
|
// Coerce booleans for known boolean fields
|
|
216
217
|
let value = raw;
|
|
217
|
-
if (/^externalLinks\.[^.]+$/.test(key)) {
|
|
218
|
+
if (/^externalLinks\.[^.]+$/.test(key) || key === 'security.threatAssessment') {
|
|
218
219
|
try {
|
|
219
220
|
value = JSON.parse(raw);
|
|
220
221
|
}
|
|
221
222
|
catch {
|
|
222
223
|
throw new AiwgError({
|
|
223
224
|
code: 'ERR_INVALID_VALUE',
|
|
224
|
-
message: `${key} must be a JSON object
|
|
225
|
-
hint:
|
|
225
|
+
message: `${key} must be a valid JSON object`,
|
|
226
|
+
hint: key === 'security.threatAssessment'
|
|
227
|
+
? `Try: aiwg config set --project ${key} '{"schemaVersion":"1","mode":"audit","defaultProfile":"balanced"}'`
|
|
228
|
+
: `Try: aiwg config set --project ${key} '{"label":"Project docs","url":"https://example.com/docs"}'`,
|
|
226
229
|
exitCode: EXIT_CODES.USAGE,
|
|
227
230
|
});
|
|
228
231
|
}
|
|
@@ -285,6 +288,16 @@ async function projectConfigSet(key, raw, args) {
|
|
|
285
288
|
exitCode: EXIT_CODES.USAGE,
|
|
286
289
|
});
|
|
287
290
|
}
|
|
291
|
+
const { validateThreatAssessmentConfig } = await import('../security/threat-assessment-config.js');
|
|
292
|
+
const threatErrors = validateThreatAssessmentConfig(cfg.security?.threatAssessment);
|
|
293
|
+
if (threatErrors.length > 0) {
|
|
294
|
+
throw new AiwgError({
|
|
295
|
+
code: 'ERR_INVALID_VALUE',
|
|
296
|
+
message: `Invalid threat-assessment configuration: ${threatErrors.join('; ')}`,
|
|
297
|
+
hint: 'Use a built-in profile or correct the referenced profile, rule pack, threshold, or regex.',
|
|
298
|
+
exitCode: EXIT_CODES.USAGE,
|
|
299
|
+
});
|
|
300
|
+
}
|
|
288
301
|
await writeAiwgConfig(projectDir, cfg);
|
|
289
302
|
console.log(`Set --project ${key} = ${raw}`);
|
|
290
303
|
}
|
|
@@ -907,6 +907,34 @@ export const sessionCommand = {
|
|
|
907
907
|
},
|
|
908
908
|
},
|
|
909
909
|
};
|
|
910
|
+
// Session Catalog Command (#1903)
|
|
911
|
+
export const sessionsCommand = {
|
|
912
|
+
id: 'sessions',
|
|
913
|
+
type: 'skill',
|
|
914
|
+
name: 'Sessions',
|
|
915
|
+
description: 'Manage the normalized session catalog with versioned JSON, deterministic pagination, previews, and health checks',
|
|
916
|
+
version: '1.0.0',
|
|
917
|
+
capabilities: ['cli', 'session-catalog', 'session-import', 'session-lifecycle', 'doctor'],
|
|
918
|
+
keywords: ['sessions', 'catalog', 'import', 'source', 'tag', 'reindex', 'delete'],
|
|
919
|
+
category: 'project',
|
|
920
|
+
platforms: {
|
|
921
|
+
claude: 'full',
|
|
922
|
+
generic: 'full',
|
|
923
|
+
},
|
|
924
|
+
deployment: {
|
|
925
|
+
pathTemplate: '.{platform}/commands/{id}.md',
|
|
926
|
+
core: true,
|
|
927
|
+
},
|
|
928
|
+
metadata: {
|
|
929
|
+
type: 'skill',
|
|
930
|
+
triggerPhrases: ['list sessions', 'import sessions', 'session catalog', 'sessions doctor'],
|
|
931
|
+
commandHint: {
|
|
932
|
+
template: 'utility',
|
|
933
|
+
argumentHint: '<sources|import|list|show|tag|relocate|reindex|delete|doctor> [--json] [--dry-run]',
|
|
934
|
+
allowedTools: ['Bash'],
|
|
935
|
+
},
|
|
936
|
+
},
|
|
937
|
+
};
|
|
910
938
|
// Sandbox Management Commands (#917)
|
|
911
939
|
export const sandboxCommand = {
|
|
912
940
|
id: 'sandbox',
|
|
@@ -3458,6 +3486,7 @@ export const commandDefinitions = [
|
|
|
3458
3486
|
feedbackCommand,
|
|
3459
3487
|
// Session (#884)
|
|
3460
3488
|
sessionCommand,
|
|
3489
|
+
sessionsCommand,
|
|
3461
3490
|
];
|
|
3462
3491
|
// ============================================
|
|
3463
3492
|
// Helper Functions
|
|
@@ -39,6 +39,7 @@ export const ProjectLocalTypeSchema = z.enum([
|
|
|
39
39
|
* underscore + hyphen, optional trailing slash.
|
|
40
40
|
*/
|
|
41
41
|
const safeRelativePath = z.string().regex(/^[a-zA-Z0-9_-]+(?:\/[a-zA-Z0-9_-]+)*\/?$/, 'must be a relative path (alphanumeric + _-, no leading slash, no ..)');
|
|
42
|
+
const safeModuleFile = z.string().regex(/^[a-zA-Z0-9_-]+(?:\/[a-zA-Z0-9_-]+)*\.mjs$/, 'must be a relative .mjs path (alphanumeric + _-, no leading slash, no ..)');
|
|
42
43
|
// Single-char ids are allowed; multi-char ids must end with alphanumeric
|
|
43
44
|
// (no trailing hyphen). This pattern: `[a-z0-9]([a-z0-9-]*[a-z0-9])?`
|
|
44
45
|
const bundleNamePattern = /^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/;
|
|
@@ -129,6 +130,31 @@ export const ProviderConfigSchema = z.object({
|
|
|
129
130
|
aliases: z.array(z.string().min(1).max(64)).max(20).optional(),
|
|
130
131
|
capabilities: ProviderCapabilityOverridesSchema.optional(),
|
|
131
132
|
}).strict();
|
|
133
|
+
export const CliCommandsSchema = z.object({
|
|
134
|
+
namespace: z.string()
|
|
135
|
+
.min(1)
|
|
136
|
+
.max(64)
|
|
137
|
+
.regex(bundleNamePattern, 'kebab-case alphanumeric, no leading/trailing hyphen'),
|
|
138
|
+
description: z.string().min(1).max(512),
|
|
139
|
+
entry: safeRelativePath.optional(),
|
|
140
|
+
subcommands: z.record(z.string()
|
|
141
|
+
.min(1)
|
|
142
|
+
.max(64)
|
|
143
|
+
.regex(bundleNamePattern, 'kebab-case alphanumeric, no leading/trailing hyphen'), z.object({
|
|
144
|
+
file: safeModuleFile,
|
|
145
|
+
description: z.string().min(1).max(512),
|
|
146
|
+
hook_event: z.enum([
|
|
147
|
+
'Stop',
|
|
148
|
+
'SessionStart',
|
|
149
|
+
'SessionEnd',
|
|
150
|
+
'PreToolUse',
|
|
151
|
+
'PostToolUse',
|
|
152
|
+
'FeatureComplete',
|
|
153
|
+
]).optional(),
|
|
154
|
+
}).strict()).refine((commands) => Object.keys(commands).length > 0, {
|
|
155
|
+
message: 'at least one CLI subcommand is required',
|
|
156
|
+
}),
|
|
157
|
+
}).strict();
|
|
132
158
|
// ============================================
|
|
133
159
|
// Top-level BundleManifestSchema
|
|
134
160
|
// ============================================
|
|
@@ -162,6 +188,9 @@ export const BundleManifestSchema = z.object({
|
|
|
162
188
|
// Optional patterns shared with existing extension validation
|
|
163
189
|
deprecation: DeprecationSchema.optional(),
|
|
164
190
|
memory: MemoryFootprintSchema.optional(),
|
|
191
|
+
// Expandable CLI namespace contributed by an addon-shaped bundle.
|
|
192
|
+
// The same block is accepted by bundled addon manifests.
|
|
193
|
+
cli_commands: CliCommandsSchema.optional(),
|
|
165
194
|
})
|
|
166
195
|
.strict()
|
|
167
196
|
.refine((m) => {
|