agentxchain 2.78.0 → 2.80.0
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/bin/agentxchain.js +7 -0
- package/builtin-plugins/plugin-github-issues/README.md +31 -0
- package/builtin-plugins/plugin-github-issues/agentxchain-plugin.json +53 -0
- package/builtin-plugins/plugin-github-issues/hooks/_shared.js +305 -0
- package/builtin-plugins/plugin-github-issues/hooks/after-acceptance.js +3 -0
- package/builtin-plugins/plugin-github-issues/hooks/on-escalation.js +3 -0
- package/builtin-plugins/plugin-github-issues/package.json +8 -0
- package/builtin-plugins/plugin-json-report/README.md +30 -0
- package/builtin-plugins/plugin-json-report/agentxchain-plugin.json +44 -0
- package/builtin-plugins/plugin-json-report/hooks/_shared.js +92 -0
- package/builtin-plugins/plugin-json-report/hooks/after-acceptance.js +4 -0
- package/builtin-plugins/plugin-json-report/hooks/before-gate.js +4 -0
- package/builtin-plugins/plugin-json-report/hooks/on-escalation.js +4 -0
- package/builtin-plugins/plugin-json-report/package.json +8 -0
- package/builtin-plugins/plugin-slack-notify/README.md +34 -0
- package/builtin-plugins/plugin-slack-notify/agentxchain-plugin.json +47 -0
- package/builtin-plugins/plugin-slack-notify/hooks/_shared.js +115 -0
- package/builtin-plugins/plugin-slack-notify/hooks/after-acceptance.js +16 -0
- package/builtin-plugins/plugin-slack-notify/hooks/before-gate.js +18 -0
- package/builtin-plugins/plugin-slack-notify/hooks/on-escalation.js +15 -0
- package/builtin-plugins/plugin-slack-notify/package.json +8 -0
- package/package.json +2 -1
- package/src/commands/plugin.js +31 -1
- package/src/lib/plugins.js +54 -1
package/bin/agentxchain.js
CHANGED
|
@@ -82,6 +82,7 @@ import { reportCommand } from '../src/commands/report.js';
|
|
|
82
82
|
import {
|
|
83
83
|
pluginInstallCommand,
|
|
84
84
|
pluginListCommand,
|
|
85
|
+
pluginListAvailableCommand,
|
|
85
86
|
pluginRemoveCommand,
|
|
86
87
|
pluginUpgradeCommand,
|
|
87
88
|
} from '../src/commands/plugin.js';
|
|
@@ -485,6 +486,12 @@ pluginCmd
|
|
|
485
486
|
.option('-j, --json', 'Output as JSON')
|
|
486
487
|
.action(pluginListCommand);
|
|
487
488
|
|
|
489
|
+
pluginCmd
|
|
490
|
+
.command('list-available')
|
|
491
|
+
.description('List built-in plugins available for installation')
|
|
492
|
+
.option('-j, --json', 'Output as JSON')
|
|
493
|
+
.action(pluginListAvailableCommand);
|
|
494
|
+
|
|
488
495
|
pluginCmd
|
|
489
496
|
.command('remove <name>')
|
|
490
497
|
.description('Remove an installed plugin')
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# @agentxchain/plugin-github-issues
|
|
2
|
+
|
|
3
|
+
Built-in AgentXchain plugin that mirrors governed run status into a configured GitHub issue.
|
|
4
|
+
|
|
5
|
+
Install from this repo:
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
agentxchain plugin install ./plugins/plugin-github-issues
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Hook phases:
|
|
12
|
+
|
|
13
|
+
- `after_acceptance`
|
|
14
|
+
- `on_escalation`
|
|
15
|
+
|
|
16
|
+
Required config:
|
|
17
|
+
|
|
18
|
+
- `repo`: GitHub repository in `owner/name` form
|
|
19
|
+
- `issue_number`: GitHub issue number
|
|
20
|
+
|
|
21
|
+
Optional config:
|
|
22
|
+
|
|
23
|
+
- `token_env`: token environment variable name (default runtime fallback: `GITHUB_TOKEN`)
|
|
24
|
+
- `api_base_url`: GitHub API base URL (default runtime fallback: `https://api.github.com`)
|
|
25
|
+
- `label_prefix`: managed label prefix (default runtime fallback: `agentxchain`)
|
|
26
|
+
|
|
27
|
+
Scope notes:
|
|
28
|
+
|
|
29
|
+
- One plugin-owned comment per run, updated in place
|
|
30
|
+
- Managed labels track phase or blocked state only
|
|
31
|
+
- This plugin does **not** close issues or claim post-gate approval state because the hook surface does not provide post-gate truth
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schema_version": "0.1",
|
|
3
|
+
"name": "@agentxchain/plugin-github-issues",
|
|
4
|
+
"version": "0.1.0",
|
|
5
|
+
"description": "Mirrors governed run status into a configured GitHub issue using advisory hooks.",
|
|
6
|
+
"hooks": {
|
|
7
|
+
"after_acceptance": [
|
|
8
|
+
{
|
|
9
|
+
"name": "github_issues_acceptance",
|
|
10
|
+
"type": "process",
|
|
11
|
+
"command": ["node", "./hooks/after-acceptance.js"],
|
|
12
|
+
"timeout_ms": 8000,
|
|
13
|
+
"mode": "advisory"
|
|
14
|
+
}
|
|
15
|
+
],
|
|
16
|
+
"on_escalation": [
|
|
17
|
+
{
|
|
18
|
+
"name": "github_issues_escalation",
|
|
19
|
+
"type": "process",
|
|
20
|
+
"command": ["node", "./hooks/on-escalation.js"],
|
|
21
|
+
"timeout_ms": 8000,
|
|
22
|
+
"mode": "advisory"
|
|
23
|
+
}
|
|
24
|
+
]
|
|
25
|
+
},
|
|
26
|
+
"config_schema": {
|
|
27
|
+
"type": "object",
|
|
28
|
+
"required": ["repo", "issue_number"],
|
|
29
|
+
"additionalProperties": false,
|
|
30
|
+
"properties": {
|
|
31
|
+
"repo": {
|
|
32
|
+
"type": "string",
|
|
33
|
+
"pattern": "^[A-Za-z0-9_.-]+\\/[A-Za-z0-9_.-]+$"
|
|
34
|
+
},
|
|
35
|
+
"issue_number": {
|
|
36
|
+
"type": "integer",
|
|
37
|
+
"minimum": 1
|
|
38
|
+
},
|
|
39
|
+
"token_env": {
|
|
40
|
+
"type": "string",
|
|
41
|
+
"minLength": 1
|
|
42
|
+
},
|
|
43
|
+
"api_base_url": {
|
|
44
|
+
"type": "string",
|
|
45
|
+
"minLength": 1
|
|
46
|
+
},
|
|
47
|
+
"label_prefix": {
|
|
48
|
+
"type": "string",
|
|
49
|
+
"minLength": 1
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
import http from 'node:http';
|
|
2
|
+
import https from 'node:https';
|
|
3
|
+
import process from 'node:process';
|
|
4
|
+
|
|
5
|
+
const DEFAULT_API_BASE = 'https://api.github.com';
|
|
6
|
+
const DEFAULT_TOKEN_ENV = 'GITHUB_TOKEN';
|
|
7
|
+
const DEFAULT_LABEL_PREFIX = 'agentxchain';
|
|
8
|
+
|
|
9
|
+
export async function readEnvelope() {
|
|
10
|
+
let input = '';
|
|
11
|
+
for await (const chunk of process.stdin) {
|
|
12
|
+
input += chunk;
|
|
13
|
+
}
|
|
14
|
+
return input.trim() ? JSON.parse(input) : {};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function writeResult(result) {
|
|
18
|
+
process.stdout.write(JSON.stringify(result));
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function warn(message) {
|
|
22
|
+
writeResult({ verdict: 'warn', message });
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function ok(message) {
|
|
26
|
+
writeResult({ verdict: 'allow', message });
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function parsePluginConfig() {
|
|
30
|
+
try {
|
|
31
|
+
return JSON.parse(process.env.AGENTXCHAIN_PLUGIN_CONFIG || '{}');
|
|
32
|
+
} catch {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function normalizeConfig(raw) {
|
|
38
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const repo = typeof raw.repo === 'string' ? raw.repo.trim() : '';
|
|
43
|
+
const issueNumber = Number.isInteger(raw.issue_number) ? raw.issue_number : null;
|
|
44
|
+
if (!repo || !/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/.test(repo) || !issueNumber || issueNumber < 1) {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const [owner, name] = repo.split('/');
|
|
49
|
+
return {
|
|
50
|
+
owner,
|
|
51
|
+
repo: name,
|
|
52
|
+
issue_number: issueNumber,
|
|
53
|
+
token_env: typeof raw.token_env === 'string' && raw.token_env.trim() ? raw.token_env.trim() : DEFAULT_TOKEN_ENV,
|
|
54
|
+
api_base_url: typeof raw.api_base_url === 'string' && raw.api_base_url.trim()
|
|
55
|
+
? raw.api_base_url.trim().replace(/\/+$/, '')
|
|
56
|
+
: DEFAULT_API_BASE,
|
|
57
|
+
label_prefix: typeof raw.label_prefix === 'string' && raw.label_prefix.trim()
|
|
58
|
+
? raw.label_prefix.trim()
|
|
59
|
+
: DEFAULT_LABEL_PREFIX,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function getToken(config) {
|
|
64
|
+
return process.env[config.token_env] || '';
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function buildHeaders(token, body) {
|
|
68
|
+
return {
|
|
69
|
+
accept: 'application/vnd.github+json',
|
|
70
|
+
authorization: `Bearer ${token}`,
|
|
71
|
+
'content-type': 'application/json',
|
|
72
|
+
'content-length': Buffer.byteLength(body),
|
|
73
|
+
'user-agent': '@agentxchain/plugin-github-issues',
|
|
74
|
+
'x-github-api-version': '2022-11-28',
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async function requestJson(config, token, method, path, bodyValue) {
|
|
79
|
+
const url = new URL(path, `${config.api_base_url}/`);
|
|
80
|
+
const body = bodyValue === undefined ? '' : JSON.stringify(bodyValue);
|
|
81
|
+
const transport = url.protocol === 'https:' ? https : http;
|
|
82
|
+
|
|
83
|
+
const response = await new Promise((resolve, reject) => {
|
|
84
|
+
const req = transport.request(url, {
|
|
85
|
+
method,
|
|
86
|
+
headers: buildHeaders(token, body),
|
|
87
|
+
timeout: 7000,
|
|
88
|
+
}, (res) => {
|
|
89
|
+
let data = '';
|
|
90
|
+
res.on('data', (chunk) => {
|
|
91
|
+
data += chunk;
|
|
92
|
+
});
|
|
93
|
+
res.on('end', () => {
|
|
94
|
+
resolve({
|
|
95
|
+
status: res.statusCode || 500,
|
|
96
|
+
body: data,
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
req.on('timeout', () => req.destroy(new Error('request timed out')));
|
|
102
|
+
req.on('error', reject);
|
|
103
|
+
if (body) {
|
|
104
|
+
req.write(body);
|
|
105
|
+
}
|
|
106
|
+
req.end();
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
let parsed = null;
|
|
110
|
+
if (response.body) {
|
|
111
|
+
try {
|
|
112
|
+
parsed = JSON.parse(response.body);
|
|
113
|
+
} catch {
|
|
114
|
+
parsed = null;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return {
|
|
119
|
+
ok: response.status >= 200 && response.status < 300,
|
|
120
|
+
status: response.status,
|
|
121
|
+
body: parsed,
|
|
122
|
+
raw: response.body,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async function listComments(config, token) {
|
|
127
|
+
const comments = [];
|
|
128
|
+
for (let page = 1; page <= 10; page += 1) {
|
|
129
|
+
const response = await requestJson(
|
|
130
|
+
config,
|
|
131
|
+
token,
|
|
132
|
+
'GET',
|
|
133
|
+
`/repos/${config.owner}/${config.repo}/issues/${config.issue_number}/comments?per_page=100&page=${page}`,
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
if (!response.ok) {
|
|
137
|
+
return response;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const pageItems = Array.isArray(response.body) ? response.body : [];
|
|
141
|
+
comments.push(...pageItems);
|
|
142
|
+
if (pageItems.length < 100) {
|
|
143
|
+
break;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return { ok: true, body: comments };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function runMarker(runId) {
|
|
151
|
+
return `<!-- agentxchain:github-issues:run:${runId || 'unknown'} -->`;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function buildManagedLabels(config, event) {
|
|
155
|
+
const labels = [config.label_prefix];
|
|
156
|
+
if (event.type === 'after_acceptance' && event.phase) {
|
|
157
|
+
labels.push(`${config.label_prefix}:phase:${event.phase}`);
|
|
158
|
+
}
|
|
159
|
+
if (event.type === 'on_escalation') {
|
|
160
|
+
labels.push(`${config.label_prefix}:blocked`);
|
|
161
|
+
}
|
|
162
|
+
return labels;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function mergeLabels(existingLabels, managedLabels, labelPrefix) {
|
|
166
|
+
const retained = existingLabels
|
|
167
|
+
.map((entry) => (typeof entry === 'string' ? entry : entry?.name))
|
|
168
|
+
.filter((name) => typeof name === 'string' && name && !name.startsWith(`${labelPrefix}:`) && name !== labelPrefix);
|
|
169
|
+
|
|
170
|
+
return [...retained, ...managedLabels];
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function buildAcceptanceBody(envelope) {
|
|
174
|
+
const payload = envelope.payload || {};
|
|
175
|
+
return [
|
|
176
|
+
runMarker(envelope.run_id),
|
|
177
|
+
'## AgentXchain Run Update',
|
|
178
|
+
'',
|
|
179
|
+
`- Run: \`${envelope.run_id || 'unknown'}\``,
|
|
180
|
+
'- Event: `after_acceptance`',
|
|
181
|
+
`- Status: \`${payload.run_status || 'active'}\``,
|
|
182
|
+
`- Phase: \`${payload.phase || 'unknown'}\``,
|
|
183
|
+
`- Latest accepted turn: \`${payload.turn_id || 'unknown'}\` (\`${payload.role_id || 'unknown'}\`)`,
|
|
184
|
+
`- History index: \`${payload.history_entry_index ?? 'unknown'}\``,
|
|
185
|
+
`- Decisions on latest turn: \`${payload.decisions_count ?? 0}\``,
|
|
186
|
+
`- Objections on latest turn: \`${payload.objections_count ?? 0}\``,
|
|
187
|
+
`- Updated: \`${envelope.timestamp || new Date().toISOString()}\``,
|
|
188
|
+
'',
|
|
189
|
+
'Plugin-owned comment from `@agentxchain/plugin-github-issues`.',
|
|
190
|
+
].join('\n');
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function buildEscalationBody(envelope) {
|
|
194
|
+
const payload = envelope.payload || {};
|
|
195
|
+
return [
|
|
196
|
+
runMarker(envelope.run_id),
|
|
197
|
+
'## AgentXchain Run Update',
|
|
198
|
+
'',
|
|
199
|
+
`- Run: \`${envelope.run_id || 'unknown'}\``,
|
|
200
|
+
'- Event: `on_escalation`',
|
|
201
|
+
'- Status: `blocked`',
|
|
202
|
+
`- Blocked reason: \`${payload.blocked_reason || 'unknown'}\``,
|
|
203
|
+
`- Failed turn: \`${payload.failed_turn_id || 'unknown'}\` (\`${payload.failed_role || 'unknown'}\`)`,
|
|
204
|
+
`- Recovery action: ${payload.recovery_action || 'unknown'}`,
|
|
205
|
+
`- Last error: ${payload.last_error || 'unknown'}`,
|
|
206
|
+
`- Updated: \`${envelope.timestamp || new Date().toISOString()}\``,
|
|
207
|
+
'',
|
|
208
|
+
'Plugin-owned comment from `@agentxchain/plugin-github-issues`.',
|
|
209
|
+
].join('\n');
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
async function upsertComment(config, token, envelope, body) {
|
|
213
|
+
const comments = await listComments(config, token);
|
|
214
|
+
if (!comments.ok) {
|
|
215
|
+
return comments;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const marker = runMarker(envelope.run_id);
|
|
219
|
+
const existing = comments.body.find((comment) => typeof comment?.body === 'string' && comment.body.includes(marker));
|
|
220
|
+
|
|
221
|
+
if (existing) {
|
|
222
|
+
return requestJson(
|
|
223
|
+
config,
|
|
224
|
+
token,
|
|
225
|
+
'PATCH',
|
|
226
|
+
`/repos/${config.owner}/${config.repo}/issues/comments/${existing.id}`,
|
|
227
|
+
{ body },
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return requestJson(
|
|
232
|
+
config,
|
|
233
|
+
token,
|
|
234
|
+
'POST',
|
|
235
|
+
`/repos/${config.owner}/${config.repo}/issues/${config.issue_number}/comments`,
|
|
236
|
+
{ body },
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
async function syncLabels(config, token, event) {
|
|
241
|
+
const issue = await requestJson(
|
|
242
|
+
config,
|
|
243
|
+
token,
|
|
244
|
+
'GET',
|
|
245
|
+
`/repos/${config.owner}/${config.repo}/issues/${config.issue_number}`,
|
|
246
|
+
);
|
|
247
|
+
if (!issue.ok) {
|
|
248
|
+
return issue;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const labels = mergeLabels(issue.body?.labels || [], buildManagedLabels(config, event), config.label_prefix);
|
|
252
|
+
return requestJson(
|
|
253
|
+
config,
|
|
254
|
+
token,
|
|
255
|
+
'PUT',
|
|
256
|
+
`/repos/${config.owner}/${config.repo}/issues/${config.issue_number}/labels`,
|
|
257
|
+
{ labels },
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
export async function publishRunUpdate(kind) {
|
|
262
|
+
try {
|
|
263
|
+
const rawConfig = parsePluginConfig();
|
|
264
|
+
if (rawConfig === null) {
|
|
265
|
+
warn('Invalid AGENTXCHAIN_PLUGIN_CONFIG JSON');
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const config = normalizeConfig(rawConfig);
|
|
270
|
+
if (!config) {
|
|
271
|
+
warn('Missing or invalid GitHub issue plugin config');
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const token = getToken(config);
|
|
276
|
+
if (!token) {
|
|
277
|
+
warn(`Missing GitHub token env ${config.token_env}`);
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const envelope = await readEnvelope();
|
|
282
|
+
const body = kind === 'after_acceptance'
|
|
283
|
+
? buildAcceptanceBody(envelope)
|
|
284
|
+
: buildEscalationBody(envelope);
|
|
285
|
+
|
|
286
|
+
const comment = await upsertComment(config, token, envelope, body);
|
|
287
|
+
if (!comment.ok) {
|
|
288
|
+
warn(`GitHub comment sync failed with HTTP ${comment.status}`);
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
const labels = await syncLabels(config, token, {
|
|
293
|
+
type: kind,
|
|
294
|
+
phase: envelope.payload?.phase || null,
|
|
295
|
+
});
|
|
296
|
+
if (!labels.ok) {
|
|
297
|
+
warn(`GitHub label sync failed with HTTP ${labels.status}`);
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
ok('GitHub issue updated');
|
|
302
|
+
} catch (error) {
|
|
303
|
+
warn(`GitHub issue sync failed: ${error.message || String(error)}`);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# @agentxchain/plugin-json-report
|
|
2
|
+
|
|
3
|
+
Built-in AgentXchain plugin that writes structured lifecycle report artifacts into `.agentxchain/reports/`.
|
|
4
|
+
|
|
5
|
+
Install from this repo:
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
agentxchain plugin install ./plugins/plugin-json-report
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Optional install-time config:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
agentxchain plugin install ./plugins/plugin-json-report \
|
|
15
|
+
--config '{"report_dir":".agentxchain/custom-reports"}'
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Hook phases:
|
|
19
|
+
|
|
20
|
+
- `after_acceptance`
|
|
21
|
+
- `before_gate`
|
|
22
|
+
- `on_escalation`
|
|
23
|
+
|
|
24
|
+
Outputs:
|
|
25
|
+
|
|
26
|
+
- timestamped JSON file per invocation
|
|
27
|
+
- `latest.json`
|
|
28
|
+
- `latest-<hook_phase>.json`
|
|
29
|
+
- default output path `.agentxchain/reports`
|
|
30
|
+
- `report_dir` may override the path, but it must stay inside the governed project root
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schema_version": "0.1",
|
|
3
|
+
"name": "@agentxchain/plugin-json-report",
|
|
4
|
+
"version": "0.1.0",
|
|
5
|
+
"description": "Writes structured JSON lifecycle reports into .agentxchain/reports/.",
|
|
6
|
+
"hooks": {
|
|
7
|
+
"after_acceptance": [
|
|
8
|
+
{
|
|
9
|
+
"name": "json_report_acceptance",
|
|
10
|
+
"type": "process",
|
|
11
|
+
"command": ["node", "./hooks/after-acceptance.js"],
|
|
12
|
+
"timeout_ms": 5000,
|
|
13
|
+
"mode": "advisory"
|
|
14
|
+
}
|
|
15
|
+
],
|
|
16
|
+
"before_gate": [
|
|
17
|
+
{
|
|
18
|
+
"name": "json_report_gate",
|
|
19
|
+
"type": "process",
|
|
20
|
+
"command": ["node", "./hooks/before-gate.js"],
|
|
21
|
+
"timeout_ms": 5000,
|
|
22
|
+
"mode": "advisory"
|
|
23
|
+
}
|
|
24
|
+
],
|
|
25
|
+
"on_escalation": [
|
|
26
|
+
{
|
|
27
|
+
"name": "json_report_escalation",
|
|
28
|
+
"type": "process",
|
|
29
|
+
"command": ["node", "./hooks/on-escalation.js"],
|
|
30
|
+
"timeout_ms": 5000,
|
|
31
|
+
"mode": "advisory"
|
|
32
|
+
}
|
|
33
|
+
]
|
|
34
|
+
},
|
|
35
|
+
"config_schema": {
|
|
36
|
+
"type": "object",
|
|
37
|
+
"properties": {
|
|
38
|
+
"report_dir": {
|
|
39
|
+
"type": "string",
|
|
40
|
+
"default": ".agentxchain/reports"
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { mkdirSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { isAbsolute, join, relative, resolve } from 'node:path';
|
|
3
|
+
import process from 'node:process';
|
|
4
|
+
|
|
5
|
+
export async function readEnvelope() {
|
|
6
|
+
let input = '';
|
|
7
|
+
for await (const chunk of process.stdin) {
|
|
8
|
+
input += chunk;
|
|
9
|
+
}
|
|
10
|
+
return input.trim() ? JSON.parse(input) : {};
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function safeTimestamp(timestamp) {
|
|
14
|
+
return (timestamp || new Date().toISOString()).replace(/[:]/g, '-');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function parsePluginConfig() {
|
|
18
|
+
try {
|
|
19
|
+
const parsed = JSON.parse(process.env.AGENTXCHAIN_PLUGIN_CONFIG || '{}');
|
|
20
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
return parsed;
|
|
24
|
+
} catch {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function resolveReportDir(projectRoot, config) {
|
|
30
|
+
const configuredDir = typeof config?.report_dir === 'string' && config.report_dir.trim()
|
|
31
|
+
? config.report_dir.trim()
|
|
32
|
+
: '.agentxchain/reports';
|
|
33
|
+
const reportDir = resolve(projectRoot, configuredDir);
|
|
34
|
+
const relativePath = relative(projectRoot, reportDir);
|
|
35
|
+
if (relativePath === '' || (!relativePath.startsWith('..') && !isAbsolute(relativePath))) {
|
|
36
|
+
return reportDir;
|
|
37
|
+
}
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function writeReport(pluginName, envelope) {
|
|
42
|
+
const projectRoot = process.env.AGENTXCHAIN_PROJECT_ROOT;
|
|
43
|
+
if (!projectRoot) {
|
|
44
|
+
process.stdout.write(JSON.stringify({
|
|
45
|
+
verdict: 'warn',
|
|
46
|
+
message: 'Missing AGENTXCHAIN_PROJECT_ROOT',
|
|
47
|
+
}));
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const config = parsePluginConfig();
|
|
52
|
+
if (config === null) {
|
|
53
|
+
process.stdout.write(JSON.stringify({
|
|
54
|
+
verdict: 'warn',
|
|
55
|
+
message: 'Invalid AGENTXCHAIN_PLUGIN_CONFIG JSON',
|
|
56
|
+
}));
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const hookPhase = envelope.hook_phase || 'unknown';
|
|
61
|
+
const timestamp = envelope.timestamp || new Date().toISOString();
|
|
62
|
+
const reportDir = resolveReportDir(projectRoot, config);
|
|
63
|
+
if (!reportDir) {
|
|
64
|
+
process.stdout.write(JSON.stringify({
|
|
65
|
+
verdict: 'warn',
|
|
66
|
+
message: 'Configured report_dir must stay within the governed project root',
|
|
67
|
+
}));
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
mkdirSync(reportDir, { recursive: true });
|
|
72
|
+
|
|
73
|
+
const report = {
|
|
74
|
+
plugin_name: pluginName,
|
|
75
|
+
hook_phase: hookPhase,
|
|
76
|
+
run_id: envelope.run_id || null,
|
|
77
|
+
hook_name: envelope.hook_name || null,
|
|
78
|
+
turn_id: envelope.payload?.turn_id || envelope.turn_id || envelope.payload?.failed_turn_id || null,
|
|
79
|
+
timestamp,
|
|
80
|
+
payload: envelope.payload || {},
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const stampedName = `${safeTimestamp(timestamp)}-${hookPhase}.json`;
|
|
84
|
+
writeFileSync(join(reportDir, stampedName), JSON.stringify(report, null, 2) + '\n');
|
|
85
|
+
writeFileSync(join(reportDir, 'latest.json'), JSON.stringify(report, null, 2) + '\n');
|
|
86
|
+
writeFileSync(join(reportDir, `latest-${hookPhase}.json`), JSON.stringify(report, null, 2) + '\n');
|
|
87
|
+
|
|
88
|
+
process.stdout.write(JSON.stringify({
|
|
89
|
+
verdict: 'allow',
|
|
90
|
+
message: `Wrote ${stampedName}`,
|
|
91
|
+
}));
|
|
92
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# @agentxchain/plugin-slack-notify
|
|
2
|
+
|
|
3
|
+
Built-in AgentXchain plugin that posts advisory lifecycle notifications to a Slack incoming webhook.
|
|
4
|
+
|
|
5
|
+
Install from this repo:
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
agentxchain plugin install ./plugins/plugin-slack-notify
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Optional install-time config:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
agentxchain plugin install ./plugins/plugin-slack-notify \
|
|
15
|
+
--config '{"webhook_env":"MY_SLACK_WEBHOOK_URL","mention":"@ops"}'
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Runtime inputs:
|
|
19
|
+
|
|
20
|
+
- `webhook_env` (optional): which env var contains the Slack incoming webhook URL
|
|
21
|
+
- `mention` (optional): prefix added to each message
|
|
22
|
+
- default webhook lookup is `AGENTXCHAIN_SLACK_WEBHOOK_URL`, then `SLACK_WEBHOOK_URL`
|
|
23
|
+
- `AGENTXCHAIN_SLACK_MENTION` remains a runtime fallback when `mention` is not configured
|
|
24
|
+
|
|
25
|
+
Hook phases:
|
|
26
|
+
|
|
27
|
+
- `after_acceptance`
|
|
28
|
+
- `before_gate`
|
|
29
|
+
- `on_escalation`
|
|
30
|
+
|
|
31
|
+
Failure semantics:
|
|
32
|
+
|
|
33
|
+
- advisory only
|
|
34
|
+
- missing webhook config or delivery failures return `warn`, never `block`
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schema_version": "0.1",
|
|
3
|
+
"name": "@agentxchain/plugin-slack-notify",
|
|
4
|
+
"version": "0.1.0",
|
|
5
|
+
"description": "Posts governed lifecycle notifications to a Slack incoming webhook.",
|
|
6
|
+
"hooks": {
|
|
7
|
+
"after_acceptance": [
|
|
8
|
+
{
|
|
9
|
+
"name": "slack_notify_acceptance",
|
|
10
|
+
"type": "process",
|
|
11
|
+
"command": ["node", "./hooks/after-acceptance.js"],
|
|
12
|
+
"timeout_ms": 5000,
|
|
13
|
+
"mode": "advisory"
|
|
14
|
+
}
|
|
15
|
+
],
|
|
16
|
+
"before_gate": [
|
|
17
|
+
{
|
|
18
|
+
"name": "slack_notify_gate",
|
|
19
|
+
"type": "process",
|
|
20
|
+
"command": ["node", "./hooks/before-gate.js"],
|
|
21
|
+
"timeout_ms": 5000,
|
|
22
|
+
"mode": "advisory"
|
|
23
|
+
}
|
|
24
|
+
],
|
|
25
|
+
"on_escalation": [
|
|
26
|
+
{
|
|
27
|
+
"name": "slack_notify_escalation",
|
|
28
|
+
"type": "process",
|
|
29
|
+
"command": ["node", "./hooks/on-escalation.js"],
|
|
30
|
+
"timeout_ms": 5000,
|
|
31
|
+
"mode": "advisory"
|
|
32
|
+
}
|
|
33
|
+
]
|
|
34
|
+
},
|
|
35
|
+
"config_schema": {
|
|
36
|
+
"type": "object",
|
|
37
|
+
"properties": {
|
|
38
|
+
"webhook_env": {
|
|
39
|
+
"type": "string",
|
|
40
|
+
"default": "AGENTXCHAIN_SLACK_WEBHOOK_URL"
|
|
41
|
+
},
|
|
42
|
+
"mention": {
|
|
43
|
+
"type": "string"
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import http from 'node:http';
|
|
2
|
+
import https from 'node:https';
|
|
3
|
+
import process from 'node:process';
|
|
4
|
+
|
|
5
|
+
export async function readEnvelope() {
|
|
6
|
+
let input = '';
|
|
7
|
+
for await (const chunk of process.stdin) {
|
|
8
|
+
input += chunk;
|
|
9
|
+
}
|
|
10
|
+
return input.trim() ? JSON.parse(input) : {};
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function parsePluginConfig() {
|
|
14
|
+
try {
|
|
15
|
+
const parsed = JSON.parse(process.env.AGENTXCHAIN_PLUGIN_CONFIG || '{}');
|
|
16
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
return parsed;
|
|
20
|
+
} catch {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function resolveWebhookSetting(config) {
|
|
26
|
+
const configuredEnv = typeof config?.webhook_env === 'string' ? config.webhook_env.trim() : '';
|
|
27
|
+
if (configuredEnv) {
|
|
28
|
+
return {
|
|
29
|
+
envName: configuredEnv,
|
|
30
|
+
url: process.env[configuredEnv] || '',
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return {
|
|
35
|
+
envName: 'AGENTXCHAIN_SLACK_WEBHOOK_URL or SLACK_WEBHOOK_URL',
|
|
36
|
+
url: process.env.AGENTXCHAIN_SLACK_WEBHOOK_URL || process.env.SLACK_WEBHOOK_URL || '',
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function buildText(title, lines, config) {
|
|
41
|
+
const mention = typeof config?.mention === 'string' && config.mention.trim()
|
|
42
|
+
? config.mention.trim()
|
|
43
|
+
: (process.env.AGENTXCHAIN_SLACK_MENTION || '');
|
|
44
|
+
return [mention, title, ...lines.filter(Boolean)].filter(Boolean).join('\n');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export async function sendSlackMessage(title, lines) {
|
|
48
|
+
const config = parsePluginConfig();
|
|
49
|
+
if (config === null) {
|
|
50
|
+
return {
|
|
51
|
+
verdict: 'warn',
|
|
52
|
+
message: 'Invalid AGENTXCHAIN_PLUGIN_CONFIG JSON',
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const webhookSetting = resolveWebhookSetting(config);
|
|
57
|
+
const webhookUrl = webhookSetting.url;
|
|
58
|
+
if (!webhookUrl) {
|
|
59
|
+
return {
|
|
60
|
+
verdict: 'warn',
|
|
61
|
+
message: `Missing Slack webhook env ${webhookSetting.envName}`,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const body = JSON.stringify({
|
|
66
|
+
text: buildText(title, lines, config),
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
const url = new URL(webhookUrl);
|
|
70
|
+
const transport = url.protocol === 'https:' ? https : http;
|
|
71
|
+
let response;
|
|
72
|
+
try {
|
|
73
|
+
response = await new Promise((resolve, reject) => {
|
|
74
|
+
const req = transport.request(url, {
|
|
75
|
+
method: 'POST',
|
|
76
|
+
headers: {
|
|
77
|
+
'content-type': 'application/json',
|
|
78
|
+
'content-length': Buffer.byteLength(body),
|
|
79
|
+
},
|
|
80
|
+
timeout: 4000,
|
|
81
|
+
}, (res) => {
|
|
82
|
+
res.resume();
|
|
83
|
+
res.on('end', () => resolve({ ok: (res.statusCode || 500) >= 200 && (res.statusCode || 500) < 300, status: res.statusCode || 500 }));
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
req.on('timeout', () => {
|
|
87
|
+
req.destroy(new Error('request timed out'));
|
|
88
|
+
});
|
|
89
|
+
req.on('error', reject);
|
|
90
|
+
req.write(body);
|
|
91
|
+
req.end();
|
|
92
|
+
});
|
|
93
|
+
} catch (error) {
|
|
94
|
+
return {
|
|
95
|
+
verdict: 'warn',
|
|
96
|
+
message: `Slack webhook request failed: ${error.message}`,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (!response.ok) {
|
|
101
|
+
return {
|
|
102
|
+
verdict: 'warn',
|
|
103
|
+
message: `Slack webhook failed with HTTP ${response.status}`,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return {
|
|
108
|
+
verdict: 'allow',
|
|
109
|
+
message: 'Slack notification delivered',
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function writeResult(result) {
|
|
114
|
+
process.stdout.write(JSON.stringify(result));
|
|
115
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { readEnvelope, sendSlackMessage, writeResult } from './_shared.js';
|
|
2
|
+
|
|
3
|
+
const envelope = await readEnvelope();
|
|
4
|
+
const payload = envelope.payload || {};
|
|
5
|
+
|
|
6
|
+
const result = await sendSlackMessage('AgentXchain accepted turn', [
|
|
7
|
+
`run: ${envelope.run_id || 'unknown'}`,
|
|
8
|
+
`phase: ${payload.phase || 'unknown'}`,
|
|
9
|
+
`role: ${payload.role_id || 'unknown'}`,
|
|
10
|
+
`turn: ${payload.turn_id || 'unknown'}`,
|
|
11
|
+
`decisions: ${payload.decisions_count ?? 0}`,
|
|
12
|
+
`objections: ${payload.objections_count ?? 0}`,
|
|
13
|
+
`status: ${payload.run_status || 'unknown'}`,
|
|
14
|
+
]);
|
|
15
|
+
|
|
16
|
+
writeResult(result);
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { readEnvelope, sendSlackMessage, writeResult } from './_shared.js';
|
|
2
|
+
|
|
3
|
+
const envelope = await readEnvelope();
|
|
4
|
+
const payload = envelope.payload || {};
|
|
5
|
+
|
|
6
|
+
const title = payload.gate_type === 'run_completion'
|
|
7
|
+
? 'AgentXchain run completion awaiting approval'
|
|
8
|
+
: 'AgentXchain phase gate awaiting approval';
|
|
9
|
+
|
|
10
|
+
const result = await sendSlackMessage(title, [
|
|
11
|
+
`run: ${envelope.run_id || 'unknown'}`,
|
|
12
|
+
`gate type: ${payload.gate_type || 'unknown'}`,
|
|
13
|
+
`current phase: ${payload.current_phase || 'unknown'}`,
|
|
14
|
+
`target phase: ${payload.target_phase || 'n/a'}`,
|
|
15
|
+
`history length: ${payload.history_length ?? 0}`,
|
|
16
|
+
]);
|
|
17
|
+
|
|
18
|
+
writeResult(result);
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { readEnvelope, sendSlackMessage, writeResult } from './_shared.js';
|
|
2
|
+
|
|
3
|
+
const envelope = await readEnvelope();
|
|
4
|
+
const payload = envelope.payload || {};
|
|
5
|
+
|
|
6
|
+
const result = await sendSlackMessage('AgentXchain escalation', [
|
|
7
|
+
`run: ${envelope.run_id || 'unknown'}`,
|
|
8
|
+
`blocked reason: ${payload.blocked_reason || 'unknown'}`,
|
|
9
|
+
`recovery: ${payload.recovery_action || 'unknown'}`,
|
|
10
|
+
`role: ${payload.failed_role || 'unknown'}`,
|
|
11
|
+
`turn: ${payload.failed_turn_id || 'unknown'}`,
|
|
12
|
+
`error: ${payload.last_error || 'unknown'}`,
|
|
13
|
+
]);
|
|
14
|
+
|
|
15
|
+
writeResult(result);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agentxchain",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.80.0",
|
|
4
4
|
"description": "CLI for AgentXchain — governed multi-agent software delivery",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
"src/",
|
|
18
18
|
"dashboard/",
|
|
19
19
|
"scripts/",
|
|
20
|
+
"builtin-plugins/",
|
|
20
21
|
"README.md"
|
|
21
22
|
],
|
|
22
23
|
"scripts": {
|
package/src/commands/plugin.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { readFileSync } from 'node:fs';
|
|
2
2
|
import { resolve } from 'node:path';
|
|
3
3
|
|
|
4
|
-
import { installPlugin, listInstalledPlugins, removePlugin, upgradePlugin } from '../lib/plugins.js';
|
|
4
|
+
import { installPlugin, listInstalledPlugins, listAvailablePlugins, removePlugin, upgradePlugin } from '../lib/plugins.js';
|
|
5
5
|
|
|
6
6
|
function parsePluginConfigOptions(options) {
|
|
7
7
|
if (options.config && options.configFile) {
|
|
@@ -117,6 +117,36 @@ export async function pluginRemoveCommand(name, options) {
|
|
|
117
117
|
console.log(` Path: ${result.install_path}`);
|
|
118
118
|
}
|
|
119
119
|
|
|
120
|
+
export async function pluginListAvailableCommand(options) {
|
|
121
|
+
const result = listAvailablePlugins();
|
|
122
|
+
|
|
123
|
+
if (!result.ok) {
|
|
124
|
+
console.error(result.error);
|
|
125
|
+
if (options.json) {
|
|
126
|
+
console.log(JSON.stringify({ ok: false, error: result.error }, null, 2));
|
|
127
|
+
}
|
|
128
|
+
process.exitCode = 1;
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (options.json) {
|
|
133
|
+
console.log(JSON.stringify({ plugins: result.plugins }, null, 2));
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (result.plugins.length === 0) {
|
|
138
|
+
console.log('No built-in plugins available.');
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
console.log(`Available built-in plugins: ${result.plugins.length}`);
|
|
143
|
+
for (const plugin of result.plugins) {
|
|
144
|
+
console.log(` ${plugin.short_name}`);
|
|
145
|
+
console.log(` ${plugin.description}`);
|
|
146
|
+
console.log(` Install: ${plugin.install_command}`);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
120
150
|
export async function pluginUpgradeCommand(name, source, options) {
|
|
121
151
|
const config = parsePluginConfigOptions(options);
|
|
122
152
|
if (!config.ok) {
|
package/src/lib/plugins.js
CHANGED
|
@@ -9,7 +9,8 @@ import {
|
|
|
9
9
|
statSync,
|
|
10
10
|
} from 'fs';
|
|
11
11
|
import { createHash } from 'crypto';
|
|
12
|
-
import { join, resolve, relative } from 'path';
|
|
12
|
+
import { dirname, join, resolve, relative } from 'path';
|
|
13
|
+
import { fileURLToPath } from 'url';
|
|
13
14
|
import { tmpdir } from 'os';
|
|
14
15
|
import { spawnSync } from 'child_process';
|
|
15
16
|
|
|
@@ -23,6 +24,55 @@ export const PLUGINS_DIR = '.agentxchain/plugins';
|
|
|
23
24
|
const PLUGIN_SCHEMA_VERSION = '0.1';
|
|
24
25
|
const PLUGIN_NAME_RE = /^(?:@[a-z0-9._-]+\/)?[a-z0-9._-]+$/;
|
|
25
26
|
|
|
27
|
+
const __filename_local = fileURLToPath(import.meta.url);
|
|
28
|
+
const __dirname_local = dirname(__filename_local);
|
|
29
|
+
const BUILTIN_PLUGINS_DIR = join(__dirname_local, '../../builtin-plugins');
|
|
30
|
+
|
|
31
|
+
const BUILTIN_PLUGIN_SHORT_NAMES = {
|
|
32
|
+
'slack-notify': 'plugin-slack-notify',
|
|
33
|
+
'json-report': 'plugin-json-report',
|
|
34
|
+
'github-issues': 'plugin-github-issues',
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
function resolveBuiltinPlugin(spec) {
|
|
38
|
+
const dirName = BUILTIN_PLUGIN_SHORT_NAMES[spec];
|
|
39
|
+
if (!dirName) return null;
|
|
40
|
+
const pluginDir = join(BUILTIN_PLUGINS_DIR, dirName);
|
|
41
|
+
if (!existsSync(pluginDir)) return null;
|
|
42
|
+
const root = findManifestRoot(pluginDir);
|
|
43
|
+
if (!root) return null;
|
|
44
|
+
return {
|
|
45
|
+
ok: true,
|
|
46
|
+
type: 'builtin',
|
|
47
|
+
root,
|
|
48
|
+
sourceSpec: spec,
|
|
49
|
+
cleanup: null,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function listAvailablePlugins() {
|
|
54
|
+
const available = [];
|
|
55
|
+
for (const [shortName, dirName] of Object.entries(BUILTIN_PLUGIN_SHORT_NAMES)) {
|
|
56
|
+
const pluginDir = join(BUILTIN_PLUGINS_DIR, dirName);
|
|
57
|
+
if (!existsSync(pluginDir)) continue;
|
|
58
|
+
const manifestPath = join(pluginDir, PLUGIN_MANIFEST_FILE);
|
|
59
|
+
if (!existsSync(manifestPath)) continue;
|
|
60
|
+
try {
|
|
61
|
+
const manifest = readJson(manifestPath);
|
|
62
|
+
available.push({
|
|
63
|
+
short_name: shortName,
|
|
64
|
+
name: manifest.name,
|
|
65
|
+
version: manifest.version,
|
|
66
|
+
description: manifest.description,
|
|
67
|
+
install_command: `agentxchain plugin install ${shortName}`,
|
|
68
|
+
});
|
|
69
|
+
} catch {
|
|
70
|
+
// skip broken manifests
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return { ok: true, plugins: available };
|
|
74
|
+
}
|
|
75
|
+
|
|
26
76
|
function clone(value) {
|
|
27
77
|
return JSON.parse(JSON.stringify(value));
|
|
28
78
|
}
|
|
@@ -199,6 +249,9 @@ function resolvePluginSource(spec, startDir) {
|
|
|
199
249
|
}
|
|
200
250
|
}
|
|
201
251
|
|
|
252
|
+
const builtin = resolveBuiltinPlugin(spec);
|
|
253
|
+
if (builtin) return builtin;
|
|
254
|
+
|
|
202
255
|
const packed = packNpmPlugin(spec);
|
|
203
256
|
if (!packed.ok) {
|
|
204
257
|
return packed;
|