@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.
- package/dist/src/a2a/hitl-driver.js +28 -1
- package/dist/src/a2a/hitl.js +32 -8
- package/dist/src/audit/operator-decision.js +180 -0
- package/dist/src/cli/handlers/mc.js +270 -73
- package/dist/src/cli/handlers/subcommands.js +1 -0
- package/dist/src/cli/handlers/use.js +3 -1
- package/dist/src/config/aiwg-config.js +5 -0
- package/dist/src/extensions/claude-hooks-installer.js +9 -5
- package/dist/src/extensions/project-local-doctor.js +10 -26
- package/dist/src/extensions/project-local-remove.js +42 -5
- package/dist/src/mcp/cli.mjs +12 -0
- package/dist/src/mcp/registry.js +14 -1
- package/dist/src/mcp/registry.mjs +15 -1
- package/dist/src/research/query-cli.js +94 -24
- package/dist/src/serve/shared-host-scheduler.js +260 -0
- package/dist/src/storage/backends/fortemi.js +95 -13
- package/dist/src/storage/cli.js +93 -6
- package/package.json +2 -2
- package/tools/plugin/package-plugins.mjs +170 -15
|
@@ -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
|
-
|
|
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
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
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();
|
package/dist/src/storage/cli.js
CHANGED
|
@@ -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}
|
|
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.
|
|
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.
|
|
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 === '.
|
|
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
|
|
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
|
-
//
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
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'
|
|
1149
|
+
if (provider === 'codex') {
|
|
995
1150
|
writeCodexMarketplaceManifest(options);
|
|
996
1151
|
}
|
|
997
1152
|
}
|