@aiwg/cli 2026.8.2 → 2026.8.4

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.
@@ -37,6 +37,58 @@
37
37
  * @issue #972
38
38
  */
39
39
  const DEFAULT_MCP_SERVER = 'fortemi';
40
+ const ENV_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
41
+ export function resolveMcpRequestHeaders(server, environment = process.env) {
42
+ const headers = { ...(server.headers ?? {}) };
43
+ for (const [header, envName] of Object.entries(server.headerEnv ?? {})) {
44
+ if (!ENV_NAME.test(envName)) {
45
+ throw new Error(`storage(fortemi): invalid environment variable reference "${envName}"`);
46
+ }
47
+ const value = environment[envName];
48
+ if (!value) {
49
+ throw new Error(`storage(fortemi): required credential environment variable "${envName}" is not set`);
50
+ }
51
+ headers[header] = header.toLowerCase() === 'authorization' ? `Bearer ${value}` : value;
52
+ }
53
+ return headers;
54
+ }
55
+ export function validateRemoteMcpUrl(raw) {
56
+ let url;
57
+ try {
58
+ url = new URL(raw);
59
+ }
60
+ catch {
61
+ throw new Error(`storage(fortemi): invalid MCP server URL "${raw}"`);
62
+ }
63
+ const loopback = ['localhost', '127.0.0.1', '[::1]'].includes(url.hostname);
64
+ if (url.protocol !== 'https:' && !(url.protocol === 'http:' && loopback)) {
65
+ throw new Error('storage(fortemi): remote MCP URLs must use HTTPS; HTTP is allowed only for loopback development');
66
+ }
67
+ return url;
68
+ }
69
+ export function unwrapMcpToolResult(result) {
70
+ if (!result || typeof result !== 'object')
71
+ return result;
72
+ const envelope = result;
73
+ if (envelope.isError) {
74
+ const detail = envelope.content
75
+ ?.filter((item) => item.type === 'text' && typeof item.text === 'string')
76
+ .map((item) => item.text)
77
+ .join('; ');
78
+ throw new Error(`storage(fortemi): MCP tool failed${detail ? `: ${detail}` : ''}`);
79
+ }
80
+ if (envelope.structuredContent !== undefined)
81
+ return envelope.structuredContent;
82
+ const text = envelope.content?.find((item) => item.type === 'text' && typeof item.text === 'string')?.text;
83
+ if (text === undefined)
84
+ return result;
85
+ try {
86
+ return JSON.parse(text);
87
+ }
88
+ catch {
89
+ return { content: text };
90
+ }
91
+ }
40
92
  export class FortemiAdapter {
41
93
  subsystem;
42
94
  mcpServer;
@@ -194,30 +246,60 @@ export class FortemiAdapter {
194
246
  * Implemented as a lazy import so tests that inject a stub never load
195
247
  * the SDK or touch the registry.
196
248
  */
197
- export const createDefaultMcpClient = async (serverName) => {
249
+ export const createDefaultMcpClient = async (serverName, registryOverride, environment = process.env) => {
198
250
  const { McpServerRegistry } = await import('../../mcp/registry.js');
199
- const registry = new McpServerRegistry();
251
+ const registry = registryOverride ?? new McpServerRegistry();
200
252
  const server = await registry.get(serverName);
201
253
  if (!server) {
202
254
  throw new Error(`storage(fortemi): MCP server "${serverName}" is not registered. ` +
203
255
  `Add it via "aiwg mcp add ${serverName} --command <cmd>" before using the fortemi backend.`);
204
256
  }
205
- if (server.type !== 'stdio') {
206
- throw new Error(`storage(fortemi): only stdio MCP servers are supported (got "${server.type}" for "${serverName}")`);
207
- }
208
- // Lazy import the SDK so tests that inject a stub don't pay the cost
257
+ // Lazy imports keep unit tests that inject a stub isolated from transports.
209
258
  const { Client } = await import('@modelcontextprotocol/sdk/client/index.js');
210
- const { StdioClientTransport } = await import('@modelcontextprotocol/sdk/client/stdio.js');
211
- const transport = new StdioClientTransport({
212
- command: server.command ?? '',
213
- args: server.args ?? [],
214
- env: server.env,
215
- });
259
+ let transport;
260
+ if (server.type === 'stdio') {
261
+ const { StdioClientTransport } = await import('@modelcontextprotocol/sdk/client/stdio.js');
262
+ transport = new StdioClientTransport({
263
+ command: server.command ?? '',
264
+ args: server.args ?? [],
265
+ env: server.env,
266
+ });
267
+ }
268
+ else {
269
+ if (!server.url) {
270
+ throw new Error(`storage(fortemi): MCP server "${serverName}" has no URL`);
271
+ }
272
+ const url = validateRemoteMcpUrl(server.url);
273
+ const headers = resolveMcpRequestHeaders(server, environment);
274
+ if (server.type === 'http') {
275
+ const { StreamableHTTPClientTransport } = await import('@modelcontextprotocol/sdk/client/streamableHttp.js');
276
+ transport = new StreamableHTTPClientTransport(url, {
277
+ requestInit: { headers },
278
+ });
279
+ }
280
+ else if (server.type === 'sse') {
281
+ const { SSEClientTransport } = await import('@modelcontextprotocol/sdk/client/sse.js');
282
+ transport = new SSEClientTransport(url, {
283
+ requestInit: { headers },
284
+ eventSourceInit: {
285
+ fetch: async (input, init) => {
286
+ const merged = new Headers(init?.headers);
287
+ for (const [name, value] of Object.entries(headers))
288
+ merged.set(name, value);
289
+ return fetch(input, { ...init, headers: merged });
290
+ },
291
+ },
292
+ });
293
+ }
294
+ else {
295
+ throw new Error(`storage(fortemi): unsupported MCP transport "${String(server.type)}"`);
296
+ }
297
+ }
216
298
  const client = new Client({ name: 'aiwg-storage-fortemi-adapter', version: '1.0.0' }, { capabilities: {} });
217
299
  await client.connect(transport);
218
300
  return {
219
301
  async callTool(name, args) {
220
- return client.callTool({ name, arguments: args });
302
+ return unwrapMcpToolResult(await client.callTool({ name, arguments: args }));
221
303
  },
222
304
  async close() {
223
305
  await client.close();
@@ -7,6 +7,7 @@
7
7
  * test <subsystem> — round-trip read/write/list/delete through the
8
8
  * configured backend
9
9
  * migrate <subsystem> — copy entries from one backend to another
10
+ * import-corpus — ingest the local research corpus through a storage backend
10
11
  *
11
12
  * @design @.aiwg/architecture/storage-design.md (§7)
12
13
  * @issue #934
@@ -16,13 +17,13 @@
16
17
  import { randomUUID } from 'crypto';
17
18
  import { existsSync } from 'fs';
18
19
  import { mkdir, readFile, writeFile, appendFile } from 'fs/promises';
19
- import { dirname, join, resolve as resolvePath } from 'path';
20
+ import { dirname, extname, join, resolve as resolvePath } from 'path';
21
+ import { parseFrontmatter } from '../artifacts/index-builder.js';
20
22
  import { BACKEND_TYPES, FilesystemAdapter, ObsidianAdapter, LogseqAdapter, FortemiAdapter, SUBSYSTEM_KEYS, getLoadedConfig, initStorage, resolveStorage, resolveSubsystemRoot, storageConfigPath, } from './index.js';
21
23
  import { projectAiwgPath, resolveProjectAiwgDir } from '../config/project-artifacts.js';
22
- export async function main(args) {
24
+ export async function main(args, projectRoot = process.cwd()) {
23
25
  const subcommand = args[0];
24
26
  const subArgs = args.slice(1);
25
- const projectRoot = process.cwd();
26
27
  switch (subcommand) {
27
28
  case 'show':
28
29
  await handleShow(projectRoot);
@@ -36,6 +37,9 @@ export async function main(args) {
36
37
  case 'migrate':
37
38
  await handleMigrate(projectRoot, subArgs);
38
39
  break;
40
+ case 'import-corpus':
41
+ await handleImportCorpus(projectRoot, subArgs);
42
+ break;
39
43
  default:
40
44
  printUsage();
41
45
  if (subcommand) {
@@ -166,7 +170,7 @@ async function handleMigrate(projectRoot, args) {
166
170
  }
167
171
  if (source.init)
168
172
  await source.init();
169
- if (destination.init)
173
+ if (!opts.dryRun && destination.init)
170
174
  await destination.init();
171
175
  console.log(`storage migrate (${opts.dryRun ? 'DRY RUN' : 'live'})`);
172
176
  console.log(` subsystem: ${opts.subsystem}`);
@@ -184,7 +188,13 @@ async function handleMigrate(projectRoot, args) {
184
188
  let copied = 0;
185
189
  let skipped = 0;
186
190
  let errored = 0;
191
+ let unsupported = 0;
187
192
  for (const entry of entries) {
193
+ if (opts.textOnly && !isTextCorpusEntry(entry.path)) {
194
+ unsupported++;
195
+ console.log(` Ā· ${entry.path} (non-text attachment skipped)`);
196
+ continue;
197
+ }
188
198
  if (completed.has(entry.path)) {
189
199
  skipped++;
190
200
  console.log(` āœ“ ${entry.path} (already migrated, skipped)`);
@@ -202,7 +212,7 @@ async function handleMigrate(projectRoot, args) {
202
212
  console.log(` āœ— ${entry.path} (read returned null)`);
203
213
  continue;
204
214
  }
205
- await destination.write(entry.path, content);
215
+ await destination.write(entry.path, content, migrationMetadata(entry.path, content));
206
216
  await recordCompletion(migrationLogPath, entry.path);
207
217
  copied++;
208
218
  console.log(` āœ“ ${entry.path}`);
@@ -217,7 +227,8 @@ async function handleMigrate(projectRoot, args) {
217
227
  if (destination.close)
218
228
  await destination.close();
219
229
  console.log('');
220
- console.log(`Summary: copied=${copied} skipped=${skipped} errored=${errored} total=${entries.length}`);
230
+ console.log(`Summary: copied=${copied} skipped=${skipped} unsupported=${unsupported} ` +
231
+ `errored=${errored} total=${entries.length}`);
221
232
  if (!opts.dryRun) {
222
233
  console.log(`Migration log: ${migrationLogPath}`);
223
234
  }
@@ -232,6 +243,7 @@ function parseMigrateArgs(args) {
232
243
  let fromFolder;
233
244
  let toFolder;
234
245
  let dryRun = false;
246
+ let textOnly = false;
235
247
  for (let i = 0; i < args.length; i++) {
236
248
  const a = args[i];
237
249
  if (a === '--from')
@@ -244,6 +256,8 @@ function parseMigrateArgs(args) {
244
256
  toFolder = args[++i];
245
257
  else if (a === '--dry-run')
246
258
  dryRun = true;
259
+ else if (a === '--text-only')
260
+ textOnly = true;
247
261
  else if (!a.startsWith('--') && !subsystem)
248
262
  subsystem = a;
249
263
  else
@@ -261,8 +275,73 @@ function parseMigrateArgs(args) {
261
275
  from: { ...parseSpec(from), ...(fromFolder ? { folder: fromFolder } : {}) },
262
276
  to: { ...parseSpec(to), ...(toFolder ? { folder: toFolder } : {}) },
263
277
  dryRun,
278
+ textOnly,
264
279
  };
265
280
  }
281
+ const TEXT_CORPUS_EXTENSIONS = new Set([
282
+ '.bib',
283
+ '.csv',
284
+ '.htm',
285
+ '.html',
286
+ '.json',
287
+ '.md',
288
+ '.ris',
289
+ '.txt',
290
+ '.xml',
291
+ '.yaml',
292
+ '.yml',
293
+ ]);
294
+ export function isTextCorpusEntry(entryPath) {
295
+ return TEXT_CORPUS_EXTENSIONS.has(extname(entryPath).toLowerCase());
296
+ }
297
+ function migrationMetadata(entryPath, content) {
298
+ const extension = extname(entryPath).toLowerCase();
299
+ const contentType = extension === '.md' ? 'text/markdown' : 'text/plain';
300
+ if (extension !== '.md')
301
+ return { contentType };
302
+ return { contentType, frontmatter: parseFrontmatter(content).data };
303
+ }
304
+ async function handleImportCorpus(projectRoot, args) {
305
+ let server = 'fortemi';
306
+ let destination;
307
+ let serverSelected = false;
308
+ let dryRun = false;
309
+ for (let index = 0; index < args.length; index++) {
310
+ const arg = args[index];
311
+ if (arg === '--server') {
312
+ server = args[++index] ?? '';
313
+ serverSelected = true;
314
+ if (!server)
315
+ throw new Error('storage import-corpus: --server requires a name');
316
+ }
317
+ else if (arg === '--to') {
318
+ destination = args[++index] ?? '';
319
+ if (!destination)
320
+ throw new Error('storage import-corpus: --to requires a backend spec');
321
+ }
322
+ else if (arg === '--dry-run') {
323
+ dryRun = true;
324
+ }
325
+ else {
326
+ throw new Error(`Unknown import-corpus flag: ${arg}`);
327
+ }
328
+ }
329
+ if (serverSelected && destination) {
330
+ throw new Error('storage import-corpus: use either --server or --to, not both');
331
+ }
332
+ await initStorage(projectRoot);
333
+ const config = await getLoadedConfig(projectRoot);
334
+ const sourceRoot = resolveSubsystemRoot('research', projectRoot, config);
335
+ await handleMigrate(projectRoot, [
336
+ 'research',
337
+ '--from',
338
+ `fs:${sourceRoot}`,
339
+ '--to',
340
+ destination ?? `fortemi:${server}`,
341
+ '--text-only',
342
+ ...(dryRun ? ['--dry-run'] : []),
343
+ ]);
344
+ }
266
345
  function parseSpec(raw) {
267
346
  const idx = raw.indexOf(':');
268
347
  if (idx === -1) {
@@ -449,11 +528,16 @@ Subcommands:
449
528
  list-backends [--json] Inventory of compiled-in adapters; --json emits structured output including tracking_issue URL for stubs
450
529
  test <subsystem> Round-trip read/write/list/delete through the configured backend
451
530
  migrate <subsystem> Copy entries from one backend to another (#955)
531
+ import-corpus Ingest local research text through a storage backend (#1508)
532
+ --to <type>:<location> Provider-neutral destination backend (default: fortemi:fortemi)
533
+ --server <name> MCP registry server name (default: fortemi)
534
+ --dry-run Preview corpus selection without connecting
452
535
  --from <type>:<location> Source spec (fs:./dir, obsidian:~/vault, logseq:./graph, fortemi:server)
453
536
  --to <type>:<location> Destination spec (same format)
454
537
  --from-folder <folder> Optional Obsidian subfolder for source
455
538
  --to-folder <folder> Optional Obsidian subfolder for destination
456
539
  --dry-run Preview operations without writing
540
+ --text-only Skip non-text attachments
457
541
 
458
542
  Subsystems: ${SUBSYSTEM_KEYS.join(', ')}
459
543
 
@@ -463,6 +547,9 @@ Examples:
463
547
  aiwg storage test activity_log
464
548
  aiwg storage migrate memory --from fs:.aiwg/memory --to obsidian:~/vault --to-folder AIWG/memory --dry-run
465
549
  aiwg storage migrate kb --from fs:.aiwg/kb --to fortemi:fortemi
550
+ aiwg storage import-corpus --dry-run
551
+ aiwg storage import-corpus --to obsidian:~/vault --dry-run
552
+ aiwg storage import-corpus --server fortemi-enterprise
466
553
 
467
554
  See @.aiwg/architecture/storage-design.md for the design.`);
468
555
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aiwg/cli",
3
- "version": "2026.8.2",
3
+ "version": "2026.8.4",
4
4
  "description": "Lightweight AIWG CLI for signed, versioned web-backed resources.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -64,7 +64,7 @@
64
64
  },
65
65
  "dependencies": {
66
66
  "@fortemi/core": "2026.7.15",
67
- "@modelcontextprotocol/sdk": "^1.29.0",
67
+ "@modelcontextprotocol/sdk": "^1.30.0",
68
68
  "chalk": "^4.1.2",
69
69
  "chokidar": "^4.0.3",
70
70
  "commander": "^12.1.0",
@@ -21,6 +21,9 @@ const __filename = fileURLToPath(import.meta.url);
21
21
  const __dirname = path.dirname(__filename);
22
22
  const ROOT_DIR = path.resolve(__dirname, '../..');
23
23
  const PLUGINS_DIR = path.join(ROOT_DIR, 'agentic/code/plugins');
24
+ const RELEASE_VERSION = JSON.parse(
25
+ fs.readFileSync(path.join(ROOT_DIR, 'package.json'), 'utf8'),
26
+ ).version;
24
27
 
25
28
  // Plugin configurations
26
29
  const PLUGIN_CONFIGS = {
@@ -34,6 +37,16 @@ const PLUGIN_CONFIGS = {
34
37
  commands: 'agentic/code/frameworks/sdlc-complete/commands',
35
38
  skills: 'agentic/code/frameworks/sdlc-complete/skills'
36
39
  },
40
+ extraCopy: [
41
+ { from: 'tools/security/threat-assessment.mjs', to: 'tools/security/threat-assessment.mjs' }
42
+ ],
43
+ rewrites: [
44
+ {
45
+ file: 'skills/address-issues-threat-assess/scripts/assess.mjs',
46
+ from: '../../../../../../../tools/security/threat-assessment.mjs',
47
+ to: '../../../tools/security/threat-assessment.mjs'
48
+ }
49
+ ],
37
50
  readme: `# AIWG SDLC Complete
38
51
 
39
52
  Complete Software Development Lifecycle framework with 180+ specialized agents.
@@ -81,7 +94,6 @@ Key agents include:
81
94
  description: 'Marketing automation framework with 37 specialized agents for campaign management.',
82
95
  sources: {
83
96
  agents: 'agentic/code/frameworks/media-marketing-kit/agents',
84
- commands: 'agentic/code/frameworks/media-marketing-kit/commands',
85
97
  skills: 'agentic/code/frameworks/media-marketing-kit/skills'
86
98
  },
87
99
  readme: `# AIWG Marketing Kit
@@ -213,7 +225,6 @@ Writing quality validation and AI pattern detection.
213
225
  description: 'Core AIWG utilities for context regeneration and workspace management.',
214
226
  sources: {
215
227
  agents: 'agentic/code/addons/aiwg-utils/agents',
216
- commands: 'agentic/code/addons/aiwg-utils/commands',
217
228
  skills: 'agentic/code/addons/aiwg-utils/skills'
218
229
  },
219
230
  readme: `# AIWG Utilities
@@ -292,7 +303,6 @@ Then install in Codex via the \`/plugins\` command or the repo marketplace.
292
303
  description: 'Digital forensics and incident response framework with 14 specialized agents covering target profiling, evidence acquisition, log/memory/container/cloud analysis, IOC extraction, and reporting.',
293
304
  sources: {
294
305
  agents: 'agentic/code/frameworks/forensics-complete/agents',
295
- commands: 'agentic/code/frameworks/forensics-complete/commands',
296
306
  skills: 'agentic/code/frameworks/forensics-complete/skills'
297
307
  },
298
308
  readme: `# AIWG Forensics Complete
@@ -336,7 +346,6 @@ Digital forensics and incident response framework with 14 specialized agents.
336
346
  description: 'Applied security framework for cryptographic primitive selection, chain-of-trust design, authentication factor analysis, supply-chain trust, and physical-threat modeling. Complements OWASP-style application audits.',
337
347
  sources: {
338
348
  agents: 'agentic/code/frameworks/security-engineering/agents',
339
- commands: 'agentic/code/frameworks/security-engineering/commands',
340
349
  skills: 'agentic/code/frameworks/security-engineering/skills'
341
350
  },
342
351
  readme: `# AIWG Security Engineering
@@ -383,7 +392,6 @@ Applied security engineering framework for cryptographic primitive selection, ch
383
392
  description: 'Research workflow framework with 9 specialized agents for discovery, acquisition, synthesis, citation management, GRADE quality assessment, and OAIS-compliant archival.',
384
393
  sources: {
385
394
  agents: 'agentic/code/frameworks/research-complete/agents',
386
- commands: 'agentic/code/frameworks/research-complete/commands',
387
395
  skills: 'agentic/code/frameworks/research-complete/skills'
388
396
  },
389
397
  readme: `# AIWG Research Complete
@@ -429,7 +437,6 @@ Research workflow framework with 9 specialized agents for academic and technical
429
437
  description: 'Media archive management framework with 7 specialized agents for source discovery, acquisition (yt-dlp / Internet Archive / Bandcamp), quality assessment, metadata tagging, and provenance tracking.',
430
438
  sources: {
431
439
  agents: 'agentic/code/frameworks/media-curator/agents',
432
- commands: 'agentic/code/frameworks/media-curator/commands',
433
440
  skills: 'agentic/code/frameworks/media-curator/skills'
434
441
  },
435
442
  readme: `# AIWG Media Curator
@@ -484,7 +491,6 @@ Media archive management framework with 7 specialized agents.
484
491
  description: 'Operational infrastructure framework with 12 specialized agents covering incident response, runbook execution, fleet inventory, certificate lifecycle, and disaster recovery planning.',
485
492
  sources: {
486
493
  agents: 'agentic/code/frameworks/ops-complete/agents',
487
- commands: 'agentic/code/frameworks/ops-complete/commands',
488
494
  skills: 'agentic/code/frameworks/ops-complete/skills'
489
495
  },
490
496
  readme: `# AIWG Ops Complete
@@ -603,6 +609,57 @@ Traces are written to \`.aiwg/traces/\` in JSONL format.
603
609
  }
604
610
  };
605
611
 
612
+ // Manifest-backed Claude plugins. Keep the curated framework bundles above,
613
+ // then expose every user-facing addon that contains a Claude-discoverable
614
+ // component. Copying the complete addon directory is intentional: Claude Code
615
+ // installs marketplace plugins into an isolated cache, so skills must not rely
616
+ // on ../ paths back into the AIWG monorepo for templates, schemas, or scripts.
617
+ const MANIFEST_PLUGIN_SOURCES = [
618
+ ['validation-complete', 'agentic/code/frameworks/validation-complete'],
619
+ ['agent-loop', 'agentic/code/addons/agent-loop'],
620
+ ['agent-persistence', 'agentic/code/addons/agent-persistence'],
621
+ ['agentic-installer', 'agentic/code/addons/agentic-installer'],
622
+ ['aiwg-evals', 'agentic/code/addons/aiwg-evals'],
623
+ ['aiwg-dev', 'agentic/code/addons/aiwg-dev'],
624
+ ['auto-memory', 'agentic/code/addons/auto-memory'],
625
+ ['browser-control', 'agentic/code/addons/browser-control'],
626
+ ['color-palette', 'agentic/code/addons/color-palette'],
627
+ ['context-curator', 'agentic/code/addons/context-curator'],
628
+ ['compound-memory', 'agentic/code/addons/compound-memory'],
629
+ ['daemon', 'agentic/code/addons/daemon'],
630
+ ['doc-intelligence', 'agentic/code/addons/doc-intelligence'],
631
+ ['droid-bridge', 'agentic/code/addons/droid-bridge'],
632
+ ['guided-implementation', 'agentic/code/addons/guided-implementation'],
633
+ ['line-memory', 'agentic/code/addons/line-memory'],
634
+ ['llm-wiki', 'agentic/code/addons/llm-wiki'],
635
+ ['nlp-prod', 'agentic/code/addons/nlp-prod'],
636
+ ['prose-integration', 'agentic/code/addons/prose-integration'],
637
+ ['rlm', 'agentic/code/addons/rlm'],
638
+ ['semantic-memory', 'agentic/code/addons/semantic-memory'],
639
+ ['skill-factory', 'agentic/code/addons/skill-factory'],
640
+ ['star-prompt', 'agentic/code/addons/star-prompt'],
641
+ ['testing-quality', 'agentic/code/addons/testing-quality'],
642
+ ['twelve-factor', 'agentic/code/addons/twelve-factor'],
643
+ ['uat-mcp', 'agentic/code/addons/uat-mcp'],
644
+ ['verbalized-sampling', 'agentic/code/addons/verbalized-sampling'],
645
+ ];
646
+
647
+ for (const [id, sourceRoot] of MANIFEST_PLUGIN_SOURCES) {
648
+ if (!fs.existsSync(path.join(ROOT_DIR, sourceRoot, 'manifest.json'))) continue;
649
+ const manifest = JSON.parse(
650
+ fs.readFileSync(path.join(ROOT_DIR, sourceRoot, 'manifest.json'), 'utf8'),
651
+ );
652
+ PLUGIN_CONFIGS[id] = {
653
+ name: id,
654
+ displayName: manifest.name || id,
655
+ version: manifest.version || '1.0.0',
656
+ description: manifest.description || `${manifest.name || id} for AIWG`,
657
+ keywords: manifest.keywords || manifest.tags || [],
658
+ category: manifest.category || 'productivity',
659
+ sourceRoot,
660
+ };
661
+ }
662
+
606
663
  // Parse command line arguments
607
664
  function parseArgs() {
608
665
  const args = process.argv.slice(2);
@@ -723,7 +780,7 @@ function cleanPlugin(pluginDir) {
723
780
 
724
781
  const entries = fs.readdirSync(pluginDir, { withFileTypes: true });
725
782
  for (const entry of entries) {
726
- if (entry.name === '.claude-plugin') continue;
783
+ if (entry.name.endsWith('-plugin') || entry.name === 'clawhub.json') continue;
727
784
 
728
785
  const fullPath = path.join(pluginDir, entry.name);
729
786
  if (entry.isDirectory()) {
@@ -747,6 +804,15 @@ function packagePlugin(name, config, options) {
747
804
  }
748
805
  }
749
806
 
807
+ // Copy a complete manifest-backed component so cached plugins retain every
808
+ // referenced script/template/schema. Curated legacy bundles continue to use
809
+ // their explicit source maps below.
810
+ if (config.sourceRoot) {
811
+ console.log(` šŸ“ Copying self-contained source from ${config.sourceRoot}...`);
812
+ const count = copyDir(config.sourceRoot, pluginDir, options.dryRun);
813
+ console.log(` ${count} files`);
814
+ }
815
+
750
816
  // Copy sources
751
817
  for (const [type, srcPath] of Object.entries(config.sources || {})) {
752
818
  const destPath = path.join(pluginDir, type);
@@ -761,10 +827,30 @@ function packagePlugin(name, config, options) {
761
827
  for (const extra of config.extraCopy || []) {
762
828
  const destPath = path.join(pluginDir, extra.to);
763
829
  console.log(` šŸ“ Copying ${extra.to}...`);
764
- const count = copyDir(extra.from, destPath, options.dryRun);
830
+ const sourcePath = path.join(ROOT_DIR, extra.from);
831
+ let count;
832
+ if (fs.statSync(sourcePath).isFile()) {
833
+ count = 1;
834
+ if (!options.dryRun) {
835
+ fs.mkdirSync(path.dirname(destPath), { recursive: true });
836
+ fs.copyFileSync(sourcePath, destPath);
837
+ }
838
+ } else {
839
+ count = copyDir(extra.from, destPath, options.dryRun);
840
+ }
765
841
  console.log(` ${count} files`);
766
842
  }
767
843
 
844
+ for (const rewrite of config.rewrites || []) {
845
+ if (options.dryRun) continue;
846
+ const target = path.join(pluginDir, rewrite.file);
847
+ const body = fs.readFileSync(target, 'utf8');
848
+ if (!body.includes(rewrite.from)) {
849
+ throw new Error(`${name}: rewrite source not found in ${rewrite.file}`);
850
+ }
851
+ fs.writeFileSync(target, body.replaceAll(rewrite.from, rewrite.to), 'utf8');
852
+ }
853
+
768
854
  // Write README
769
855
  if (config.readme && !options.dryRun) {
770
856
  const readmePath = path.join(pluginDir, 'README.md');
@@ -772,9 +858,74 @@ function packagePlugin(name, config, options) {
772
858
  console.log(' šŸ“„ Created README.md');
773
859
  }
774
860
 
861
+ if (!options.dryRun) {
862
+ const manifestDir = path.join(pluginDir, '.claude-plugin');
863
+ fs.mkdirSync(manifestDir, { recursive: true });
864
+ fs.writeFileSync(path.join(manifestDir, 'plugin.json'), `${JSON.stringify({
865
+ name: config.name,
866
+ // Claude caches pinned marketplace plugins by version. AIWG publishes
867
+ // these generated bundles with the repository release, so their
868
+ // distribution version must advance with package.json even when the
869
+ // canonical component's internal schema/API version does not.
870
+ version: RELEASE_VERSION,
871
+ description: config.description,
872
+ author: { name: 'AIWG Contributors', email: 'support@aiwg.io' },
873
+ homepage: 'https://aiwg.io',
874
+ repository: 'https://github.com/jmagly/aiwg',
875
+ license: 'MIT',
876
+ keywords: config.keywords || [],
877
+ }, null, 2)}\n`);
878
+ }
879
+
775
880
  console.log(` āœ… ${config.displayName} packaged successfully`);
776
881
  }
777
882
 
883
+ function writeClaudeMarketplaceManifest(options) {
884
+ const existingPath = path.join(ROOT_DIR, '.claude-plugin', 'marketplace.json');
885
+ const existing = JSON.parse(fs.readFileSync(existingPath, 'utf8'));
886
+ const external = existing.plugins
887
+ .filter((plugin) => typeof plugin.source !== 'string')
888
+ .map((plugin) => ({
889
+ ...plugin,
890
+ source: plugin.source?.type === 'github'
891
+ ? {
892
+ source: 'github',
893
+ repo: plugin.source.repo,
894
+ ...(plugin.source.branch ? { ref: plugin.source.branch } : {}),
895
+ }
896
+ : plugin.source,
897
+ }));
898
+ const local = Object.entries(PLUGIN_CONFIGS)
899
+ .filter(([, config]) => config.pluginType !== 'codex')
900
+ .map(([name, config]) => ({
901
+ name,
902
+ source: `./agentic/code/plugins/${name}`,
903
+ description: config.description,
904
+ version: RELEASE_VERSION,
905
+ author: { name: 'AIWG Contributors' },
906
+ license: 'MIT',
907
+ category: config.category || 'productivity',
908
+ keywords: config.keywords || [],
909
+ }));
910
+ const manifest = {
911
+ name: existing.name,
912
+ owner: existing.owner,
913
+ description: existing.metadata?.description || existing.description,
914
+ // The marketplace itself is a release artifact and must stay in lockstep
915
+ // with package.json. Preserving the previous manifest value silently
916
+ // leaves stale metadata after a release bump.
917
+ version: RELEASE_VERSION,
918
+ plugins: [...local, ...external],
919
+ };
920
+ if (options.dryRun) {
921
+ console.log(`\n[dry-run] Would write Claude marketplace: ${existingPath}`);
922
+ console.log(` ${manifest.plugins.length} plugins`);
923
+ return;
924
+ }
925
+ fs.writeFileSync(existingPath, `${JSON.stringify(manifest, null, 2)}\n`, 'utf8');
926
+ console.log(`\nāœ“ Wrote Claude marketplace: ${existingPath} (${manifest.plugins.length} plugins)`);
927
+ }
928
+
778
929
  // Package a Codex-format plugin (generates .codex-plugin/plugin.json + marketplace.json)
779
930
  async function packageCodexPlugin(name, config, options) {
780
931
  console.log(`\nšŸ“¦ Packaging ${config.displayName} (Codex plugin format)...`);
@@ -974,11 +1125,11 @@ async function main() {
974
1125
  // Package for each (provider, plugin) combination
975
1126
  for (const provider of providersToRun) {
976
1127
  for (const [name, config] of pluginsToPackage) {
977
- // Respect the legacy pluginType flag when selecting defaults:
978
- // if pluginType is set and provider is 'claude' (default), skip this
979
- // plugin for claude and route it to its declared pluginType instead.
980
- const effectiveProvider =
981
- provider === 'claude' && config.pluginType ? config.pluginType : provider;
1128
+ // Provider-specific bundles are not part of another provider's catalog.
1129
+ if (provider === 'claude' && config.pluginType && config.pluginType !== 'claude') {
1130
+ continue;
1131
+ }
1132
+ const effectiveProvider = provider;
982
1133
 
983
1134
  if (effectiveProvider === 'claude') {
984
1135
  packagePlugin(name, config, options);
@@ -990,8 +1141,12 @@ async function main() {
990
1141
  }
991
1142
  }
992
1143
 
1144
+ if (provider === 'claude') {
1145
+ writeClaudeMarketplaceManifest(options);
1146
+ }
1147
+
993
1148
  // After packaging for Codex, also write the root marketplace.json
994
- if (provider === 'codex' || (provider === 'claude' && pluginsToPackage.some(([, c]) => c.pluginType === 'codex'))) {
1149
+ if (provider === 'codex') {
995
1150
  writeCodexMarketplaceManifest(options);
996
1151
  }
997
1152
  }