@aiwg/cli 2026.7.20 → 2026.7.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/README.md +4 -4
  2. package/dist/src/api/index.d.ts +1 -0
  3. package/dist/src/api/index.js +1 -0
  4. package/dist/src/artifacts/browser-export.js +7 -0
  5. package/dist/src/artifacts/citation-parser.js +96 -35
  6. package/dist/src/artifacts/index-builder.js +54 -17
  7. package/dist/src/artifacts/state-transfer.js +27 -0
  8. package/dist/src/artifacts/stats.js +8 -0
  9. package/dist/src/cli/cli-extension-loader.js +73 -0
  10. package/dist/src/cli/handlers/index.js +3 -1
  11. package/dist/src/cli/handlers/sessions.js +966 -0
  12. package/dist/src/cli/handlers/skill-lint.js +49 -45
  13. package/dist/src/cli/handlers/use.js +143 -60
  14. package/dist/src/cli/handlers/utilities.js +22 -8
  15. package/dist/src/cli/skill-usage.js +146 -24
  16. package/dist/src/extensions/commands/definitions.js +29 -0
  17. package/dist/src/extensions/manifest.js +29 -0
  18. package/dist/src/sessions/adapters/claude.js +357 -0
  19. package/dist/src/sessions/adapters/codex.js +521 -0
  20. package/dist/src/sessions/adapters/copilot.js +226 -0
  21. package/dist/src/sessions/adapters/cursor.js +372 -0
  22. package/dist/src/sessions/adapters/factory.js +345 -0
  23. package/dist/src/sessions/adapters/generic.js +225 -0
  24. package/dist/src/sessions/adapters/hermes.js +341 -0
  25. package/dist/src/sessions/adapters/openclaw.js +381 -0
  26. package/dist/src/sessions/adapters/opencode.js +454 -0
  27. package/dist/src/sessions/adapters/openhuman.js +315 -0
  28. package/dist/src/sessions/adapters/warp.js +160 -0
  29. package/dist/src/sessions/adapters/windsurf.js +212 -0
  30. package/dist/src/sessions/candidates.js +210 -0
  31. package/dist/src/sessions/contracts.js +310 -0
  32. package/dist/src/sessions/discovery.js +51 -0
  33. package/dist/src/sessions/fixtures.js +12 -0
  34. package/dist/src/sessions/importer.js +315 -0
  35. package/dist/src/sessions/index.js +25 -0
  36. package/dist/src/sessions/knowledge-shard.js +61 -0
  37. package/dist/src/sessions/optional-backends.js +238 -0
  38. package/dist/src/sessions/policy.js +192 -0
  39. package/dist/src/sessions/ports.js +2 -0
  40. package/dist/src/sessions/promotion.js +367 -0
  41. package/dist/src/sessions/readers.js +176 -0
  42. package/dist/src/sessions/repository.js +1551 -0
  43. package/dist/src/skills/adapters/agent-skills.js +59 -0
  44. package/dist/src/skills/adapters/local.js +19 -1
  45. package/dist/src/skills/agent-skills.js +249 -0
  46. package/dist/src/skills/cli.js +463 -7
  47. package/dist/src/skills/deployer.js +554 -0
  48. package/dist/src/skills/doctor.js +105 -0
  49. package/dist/src/skills/exporter.js +382 -0
  50. package/dist/src/skills/importer.js +921 -0
  51. package/dist/src/skills/registry.js +19 -0
  52. package/dist/src/skills/validator.js +323 -0
  53. 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 { events: [], appended: 0, skipped: 0, paths: [] };
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 raw = await readFile(options.transcriptPath, 'utf8');
49
- const lines = raw.split('\n').map(line => line.trim()).filter(Boolean);
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
- for (const line of lines) {
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: normalizeProvider(options.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: new Date().toISOString(),
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
- await rotateIfNeeded(filePath, maxBytes);
397
- await appendFile(filePath, JSON.stringify(event) + '\n', 'utf8');
398
- }
399
- async function rotateIfNeeded(filePath, maxBytes) {
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
- const raw = await readFile(filePath, 'utf8');
420
- return raw
421
- .split('\n')
422
- .map(line => line.trim())
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
- catch (error) {
427
- if (error.code === 'ENOENT')
428
- return [];
429
- throw error;
549
+ finally {
550
+ lines.close();
551
+ input.destroy();
430
552
  }
431
553
  }
432
554
  function summarizeEvents(events) {
@@ -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) => {