@seclai/cli 1.1.0 → 1.2.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/README.md +108 -1
- package/dist/cli.js +971 -157
- package/dist/cli.js.map +1 -1
- package/package.json +2 -2
package/dist/cli.js
CHANGED
|
@@ -72,6 +72,9 @@ function getCliVersion() {
|
|
|
72
72
|
function createClient(opts) {
|
|
73
73
|
const seclaiOpts = {};
|
|
74
74
|
if (opts.apiKey !== void 0) seclaiOpts.apiKey = opts.apiKey;
|
|
75
|
+
if (opts.profile !== void 0) seclaiOpts.profile = opts.profile;
|
|
76
|
+
if (opts.configDir !== void 0) seclaiOpts.configDir = opts.configDir;
|
|
77
|
+
if (opts.accountId !== void 0) seclaiOpts.accountId = opts.accountId;
|
|
75
78
|
const envUrl = process2.env.SECLAI_API_URL;
|
|
76
79
|
seclaiOpts.baseUrl = envUrl && envUrl.length > 0 ? envUrl : "https://api.seclai.com";
|
|
77
80
|
return new Seclai(seclaiOpts);
|
|
@@ -302,8 +305,8 @@ function register(program, rt) {
|
|
|
302
305
|
agents.command("upload-input").description("Upload a file as agent input.").argument("<agentId>", "Agent ID.").requiredOption("--file <path>", "File to upload.").option("--file-name <name>", "Override filename.").option("--mime-type <type>", "MIME type.").action(async (agentId, opts) => {
|
|
303
306
|
await run(rt, async () => {
|
|
304
307
|
const client = createClient(program.opts());
|
|
305
|
-
const { readFile:
|
|
306
|
-
const bytes = new Uint8Array(await
|
|
308
|
+
const { readFile: readFile5 } = await import("fs/promises");
|
|
309
|
+
const bytes = new Uint8Array(await readFile5(opts.file));
|
|
307
310
|
const o = { file: bytes };
|
|
308
311
|
if (opts.fileName) o.fileName = opts.fileName;
|
|
309
312
|
if (opts.mimeType) o.mimeType = opts.mimeType;
|
|
@@ -362,9 +365,11 @@ function register2(program, rt) {
|
|
|
362
365
|
const sources = program.command("sources").alias("source").description("Manage content sources.");
|
|
363
366
|
sources.command("list").description("List sources.").option("--page <n>", "Page number.", (v) => Number(v)).option("--limit <n>", "Page size.", (v) => Number(v)).option("--sort <field>", "Sort field.").option("--order <asc|desc>", "Sort direction.").option("--account-id <id>", "Filter by account ID.").action(async (opts) => {
|
|
364
367
|
await run(rt, async () => {
|
|
365
|
-
const
|
|
368
|
+
const globalOpts = program.opts();
|
|
369
|
+
const client = createClient(globalOpts);
|
|
366
370
|
const o = listOpts(opts);
|
|
367
|
-
|
|
371
|
+
const acctId = opts.accountId || globalOpts.accountId;
|
|
372
|
+
if (acctId) o.accountId = acctId;
|
|
368
373
|
printJson(rt, await client.listSources(o));
|
|
369
374
|
});
|
|
370
375
|
});
|
|
@@ -1172,35 +1177,42 @@ The Seclai CLI (\`seclai\` / \`npx @seclai/cli\`) manages agents, knowledge base
|
|
|
1172
1177
|
|
|
1173
1178
|
All commands output JSON to stdout. Pipe into \`jq\` for filtering.
|
|
1174
1179
|
|
|
1180
|
+
## Quick start
|
|
1181
|
+
|
|
1182
|
+
\`\`\`bash
|
|
1183
|
+
# authenticate
|
|
1184
|
+
export SECLAI_API_KEY="sk-..."
|
|
1185
|
+
|
|
1186
|
+
# create an agent
|
|
1187
|
+
seclai agents create --json '{"name":"My Agent","description":"QA chatbot"}'
|
|
1188
|
+
|
|
1189
|
+
# configure steps via AI assistant
|
|
1190
|
+
seclai agents ai gen-steps <agentId> --user-input "Build a QA chatbot that uses a knowledge base"
|
|
1191
|
+
|
|
1192
|
+
# accept the generated plan
|
|
1193
|
+
seclai agents ai mark <agentId> <conversationId> --json '{"accepted":true}'
|
|
1194
|
+
|
|
1195
|
+
# run the agent
|
|
1196
|
+
seclai agents run <agentId> --json '{"input":"How do I reset my password?"}' --stream
|
|
1197
|
+
|
|
1198
|
+
# list runs
|
|
1199
|
+
seclai agents runs list <agentId>
|
|
1200
|
+
\`\`\`
|
|
1201
|
+
|
|
1175
1202
|
## Authentication
|
|
1176
1203
|
|
|
1177
1204
|
Set \`SECLAI_API_KEY\` env var or pass \`--api-key <key>\`.
|
|
1178
1205
|
Override the API URL with \`SECLAI_API_URL\` (default: https://api.seclai.com).
|
|
1179
1206
|
|
|
1180
|
-
##
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
| Source Exports | \`seclai sources exports list/create/get/cancel/delete/download/estimate\` | Export management |
|
|
1190
|
-
| Contents | \`seclai contents get/delete/upload/replace-text/embeddings\` | Indexed content |
|
|
1191
|
-
| Knowledge Bases | \`seclai kb list/create/get/update/delete\` | Knowledge bases |
|
|
1192
|
-
| Memory Banks | \`seclai memory list/create/get/update/delete/stats/agents/compact/templates\` | Memory banks |
|
|
1193
|
-
| Evaluations | \`seclai evals criteria list/create/get/update/delete/summary\` | Eval criteria |
|
|
1194
|
-
| Evaluations | \`seclai evals results list/create\` | Eval results |
|
|
1195
|
-
| Solutions | \`seclai solutions list/create/get/update/delete/link/unlink\` | Solutions |
|
|
1196
|
-
| Governance | \`seclai governance ai generate/list/accept/decline\` | Governance AI |
|
|
1197
|
-
| Alerts | \`seclai alerts list/get/status/comment/subscribe/unsubscribe\` | Alerts |
|
|
1198
|
-
| Alert Config | \`seclai alerts configs list/create/get/update/delete\` | Alert configs |
|
|
1199
|
-
| Models | \`seclai models alerts list/mark-read/mark-all-read/unread-count\` | Model alerts |
|
|
1200
|
-
| Search | \`seclai search --query "text"\` | Global search |
|
|
1201
|
-
| AI Assistant | \`seclai ai feedback/kb/source/solution/memory/accept/decline/memory-accept\` | AI assistant |
|
|
1202
|
-
|
|
1203
|
-
## Common Patterns
|
|
1207
|
+
## Global options
|
|
1208
|
+
|
|
1209
|
+
\`\`\`bash
|
|
1210
|
+
--api-key <key> # Seclai API key (or set SECLAI_API_KEY)
|
|
1211
|
+
--compact # Output compact single-line JSON
|
|
1212
|
+
-V, --version # Print version
|
|
1213
|
+
\`\`\`
|
|
1214
|
+
|
|
1215
|
+
## Common patterns
|
|
1204
1216
|
|
|
1205
1217
|
### JSON input
|
|
1206
1218
|
Most create/update commands accept \`--json '{"key":"value"}'\` or \`--json-file path.json\`.
|
|
@@ -1208,76 +1220,45 @@ Use \`--json -\` or \`--json-file -\` to read from stdin.
|
|
|
1208
1220
|
|
|
1209
1221
|
### AI assistant shorthand
|
|
1210
1222
|
AI generation commands accept \`--user-input <text>\` as shorthand for \`--json '{"user_input":"<text>"}'\`.
|
|
1211
|
-
\`\`\`bash
|
|
1212
|
-
seclai agents ai gen-steps <id> --user-input "Build a QA chatbot"
|
|
1213
|
-
seclai ai kb --user-input "Create a support knowledge base"
|
|
1214
|
-
\`\`\`
|
|
1215
|
-
|
|
1216
|
-
### Compact output
|
|
1217
|
-
Use \`--compact\` for single-line JSON output (useful for scripting):
|
|
1218
|
-
\`\`\`bash
|
|
1219
|
-
seclai agents list --compact | jq -c '.[]'
|
|
1220
|
-
\`\`\`
|
|
1221
1223
|
|
|
1222
1224
|
### Pagination
|
|
1223
1225
|
List commands support \`--page <n>\` and \`--limit <n>\`. Some also support \`--sort <field>\` and \`--order asc|desc\`.
|
|
1224
1226
|
|
|
1225
|
-
### Streaming agent runs
|
|
1226
|
-
\`\`\`bash
|
|
1227
|
-
# Wait for completion via SSE, print final result
|
|
1228
|
-
seclai agents run <id> --json '{"input":"Hello"}' --stream
|
|
1229
|
-
|
|
1230
|
-
# Stream individual SSE events as NDJSON
|
|
1231
|
-
seclai agents run <id> --json '{"input":"Hello"}' --events
|
|
1232
|
-
|
|
1233
|
-
# Filter event types
|
|
1234
|
-
seclai agents run <id> --json '{"input":"Hello"}' --events --event-filter "status,data"
|
|
1235
|
-
|
|
1236
|
-
# Poll-based waiting
|
|
1237
|
-
seclai agents run <id> --json '{"input":"Hello"}' --poll --poll-interval-ms 2000
|
|
1238
|
-
\`\`\`
|
|
1239
|
-
|
|
1240
1227
|
### File uploads
|
|
1241
|
-
|
|
1242
|
-
seclai sources upload <sourceId> --file ./doc.pdf --title "My Doc" --metadata '{"category":"docs"}'
|
|
1243
|
-
seclai contents upload <contentVersionId> --file ./updated.pdf
|
|
1244
|
-
\`\`\`
|
|
1228
|
+
Upload commands accept \`--file <path>\` (required), plus optional \`--title\`, \`--metadata '{"k":"v"}'\`, \`--metadata-file path.json\`, \`--file-name\`, \`--mime-type\`.
|
|
1245
1229
|
|
|
1246
|
-
##
|
|
1230
|
+
## Commands
|
|
1247
1231
|
|
|
1248
|
-
|
|
1249
|
-
- [Sources](references/sources.md) \u2014 content sources, uploads, exports, migrations
|
|
1250
|
-
- [Knowledge Bases & Memory Banks](references/kb-memory.md) \u2014 KB CRUD, memory banks, compaction
|
|
1251
|
-
- [Evaluations & Solutions](references/evals-solutions.md) \u2014 criteria, results, solutions, links
|
|
1252
|
-
- [Alerts, Governance & More](references/alerts-governance.md) \u2014 alerts, governance, models, search, AI
|
|
1253
|
-
`;
|
|
1254
|
-
var AGENTS_SKILL = `# Seclai CLI \u2014 Agents
|
|
1232
|
+
### Agents
|
|
1255
1233
|
|
|
1256
|
-
## CRUD
|
|
1257
1234
|
\`\`\`bash
|
|
1258
1235
|
seclai agents list [--page N] [--limit N]
|
|
1259
|
-
seclai agents create --json '{"name":"My Agent"
|
|
1236
|
+
seclai agents create --json '{"name":"My Agent","description":"..."}'
|
|
1260
1237
|
seclai agents get <agentId>
|
|
1261
|
-
seclai agents update <agentId> --json '{"name":"
|
|
1238
|
+
seclai agents update <agentId> --json '{"name":"Renamed"}'
|
|
1262
1239
|
seclai agents delete <agentId>
|
|
1263
1240
|
\`\`\`
|
|
1264
1241
|
|
|
1265
|
-
|
|
1242
|
+
### Running agents
|
|
1243
|
+
|
|
1266
1244
|
\`\`\`bash
|
|
1267
|
-
#
|
|
1245
|
+
# simple run \u2014 returns the final result
|
|
1268
1246
|
seclai agents run <agentId> --json '{"input":"Hello"}'
|
|
1269
1247
|
|
|
1270
|
-
#
|
|
1248
|
+
# stream \u2014 wait for completion via SSE, print final result
|
|
1271
1249
|
seclai agents run <agentId> --json '{"input":"Hello"}' --stream [--timeout-ms 60000]
|
|
1272
1250
|
|
|
1273
|
-
#
|
|
1274
|
-
|
|
1251
|
+
# events \u2014 stream individual SSE events as NDJSON lines
|
|
1252
|
+
# --output: full (entire event), data (event data only), status (status events only)
|
|
1253
|
+
# --event-filter: comma-separated event types to include, e.g. "status,data"
|
|
1254
|
+
seclai agents run <agentId> --json '{"input":"Hello"}' --events [--output full|data|status] [--event-filter "status,data"]
|
|
1275
1255
|
|
|
1276
|
-
#
|
|
1256
|
+
# poll \u2014 poll for completion
|
|
1277
1257
|
seclai agents run <agentId> --json '{"input":"Hello"}' --poll [--poll-interval-ms 2000] [--include-step-outputs]
|
|
1278
1258
|
\`\`\`
|
|
1279
1259
|
|
|
1280
|
-
|
|
1260
|
+
### Agent runs
|
|
1261
|
+
|
|
1281
1262
|
\`\`\`bash
|
|
1282
1263
|
seclai agents runs list <agentId> [--page N] [--limit N] [--status <status>]
|
|
1283
1264
|
seclai agents runs get <runId> [--include-step-outputs]
|
|
@@ -1287,185 +1268,202 @@ seclai agents runs search --json '{"query":"..."}'
|
|
|
1287
1268
|
seclai agents runs eval-results <agentId> <runId> [--page N] [--limit N]
|
|
1288
1269
|
\`\`\`
|
|
1289
1270
|
|
|
1290
|
-
|
|
1271
|
+
### Agent definitions
|
|
1272
|
+
|
|
1291
1273
|
\`\`\`bash
|
|
1292
1274
|
seclai agents def get <agentId>
|
|
1293
|
-
seclai agents def update <agentId> --json '{"steps":[...]}'
|
|
1275
|
+
seclai agents def update <agentId> --json '{"steps":[{"step_type":"llm","config":{...}}]}'
|
|
1294
1276
|
\`\`\`
|
|
1295
1277
|
|
|
1296
|
-
|
|
1278
|
+
### Agent input uploads
|
|
1279
|
+
|
|
1297
1280
|
\`\`\`bash
|
|
1298
1281
|
seclai agents upload-input <agentId> --file ./input.pdf [--file-name name] [--mime-type type]
|
|
1299
1282
|
seclai agents input-status <agentId> <uploadId>
|
|
1300
1283
|
\`\`\`
|
|
1301
1284
|
|
|
1302
|
-
|
|
1285
|
+
### Agent AI assistant
|
|
1286
|
+
|
|
1303
1287
|
\`\`\`bash
|
|
1304
|
-
seclai agents ai gen-steps <agentId> --user-input "Build a
|
|
1305
|
-
seclai agents ai step-config <agentId> --json '{"step_type":"
|
|
1288
|
+
seclai agents ai gen-steps <agentId> --user-input "Build a QA chatbot"
|
|
1289
|
+
seclai agents ai step-config <agentId> --json '{"step_type":"llm","user_input":"Configure the LLM step"}'
|
|
1306
1290
|
seclai agents ai history <agentId>
|
|
1307
1291
|
seclai agents ai mark <agentId> <conversationId> --json '{"accepted":true}'
|
|
1308
1292
|
\`\`\`
|
|
1309
|
-
`;
|
|
1310
|
-
var SOURCES_SKILL = `# Seclai CLI \u2014 Sources
|
|
1311
1293
|
|
|
1312
|
-
|
|
1294
|
+
### Sources
|
|
1295
|
+
|
|
1313
1296
|
\`\`\`bash
|
|
1314
1297
|
seclai sources list [--page N] [--limit N] [--sort field] [--order asc|desc] [--account-id id]
|
|
1315
|
-
seclai sources create --json '{"name":"
|
|
1298
|
+
seclai sources create --json '{"name":"Docs","description":"Product documentation"}'
|
|
1316
1299
|
seclai sources get <sourceId>
|
|
1317
|
-
seclai sources update <sourceId> --json '{"name":"Updated"}'
|
|
1300
|
+
seclai sources update <sourceId> --json '{"name":"Updated Docs"}'
|
|
1318
1301
|
seclai sources delete <sourceId>
|
|
1319
1302
|
\`\`\`
|
|
1320
1303
|
|
|
1321
|
-
|
|
1304
|
+
### Source uploads
|
|
1305
|
+
|
|
1322
1306
|
\`\`\`bash
|
|
1323
|
-
seclai sources upload <sourceId> --file ./doc.pdf [--title "
|
|
1324
|
-
seclai sources upload-text <sourceId> --json '{"text":"...","title":"
|
|
1307
|
+
seclai sources upload <sourceId> --file ./doc.pdf [--title "My Doc"] [--metadata '{"category":"docs"}'] [--file-name name] [--mime-type type]
|
|
1308
|
+
seclai sources upload-text <sourceId> --json '{"text":"Article content here...","title":"My Article"}'
|
|
1325
1309
|
\`\`\`
|
|
1326
1310
|
|
|
1327
|
-
|
|
1311
|
+
### Source exports
|
|
1312
|
+
|
|
1328
1313
|
\`\`\`bash
|
|
1329
1314
|
seclai sources exports list <sourceId> [--page N] [--limit N]
|
|
1330
|
-
seclai sources exports create <sourceId> --json '{"format":"
|
|
1315
|
+
seclai sources exports create <sourceId> --json '{"format":"jsonl"}'
|
|
1331
1316
|
seclai sources exports get <sourceId> <exportId>
|
|
1332
1317
|
seclai sources exports cancel <sourceId> <exportId>
|
|
1333
1318
|
seclai sources exports delete <sourceId> <exportId>
|
|
1334
1319
|
seclai sources exports download <sourceId> <exportId>
|
|
1335
|
-
seclai sources exports estimate <sourceId> --json '{"format":"
|
|
1320
|
+
seclai sources exports estimate <sourceId> --json '{"format":"jsonl"}'
|
|
1336
1321
|
\`\`\`
|
|
1337
1322
|
|
|
1338
|
-
|
|
1323
|
+
### Embedding migration
|
|
1324
|
+
|
|
1339
1325
|
\`\`\`bash
|
|
1340
1326
|
seclai sources migration get <sourceId>
|
|
1341
|
-
seclai sources migration start <sourceId> --json '{"target_model":"
|
|
1327
|
+
seclai sources migration start <sourceId> --json '{"target_model":"text-embedding-3-large"}'
|
|
1342
1328
|
seclai sources migration cancel <sourceId>
|
|
1343
1329
|
\`\`\`
|
|
1344
|
-
`;
|
|
1345
|
-
var KB_MEMORY_SKILL = `# Seclai CLI \u2014 Knowledge Bases & Memory Banks
|
|
1346
1330
|
|
|
1347
|
-
|
|
1331
|
+
### Contents (indexed content)
|
|
1332
|
+
|
|
1333
|
+
\`\`\`bash
|
|
1334
|
+
seclai contents get <contentVersionId> [--start N] [--end N]
|
|
1335
|
+
seclai contents delete <contentVersionId>
|
|
1336
|
+
seclai contents upload <contentVersionId> --file ./updated.pdf [--title "Title"] [--file-name name] [--mime-type type]
|
|
1337
|
+
seclai contents replace-text <contentVersionId> --json '{"text":"Replacement text","title":"Updated"}'
|
|
1338
|
+
seclai contents embeddings <contentVersionId> [--page N] [--limit N]
|
|
1339
|
+
\`\`\`
|
|
1340
|
+
|
|
1341
|
+
### Knowledge bases
|
|
1342
|
+
|
|
1348
1343
|
\`\`\`bash
|
|
1349
1344
|
seclai kb list [--page N] [--limit N] [--sort field] [--order asc|desc]
|
|
1350
|
-
seclai kb create --json '{"name":"
|
|
1345
|
+
seclai kb create --json '{"name":"Support KB","description":"Customer support articles"}'
|
|
1351
1346
|
seclai kb get <kbId>
|
|
1352
|
-
seclai kb update <kbId> --json '{"name":"Updated"}'
|
|
1347
|
+
seclai kb update <kbId> --json '{"name":"Updated KB"}'
|
|
1353
1348
|
seclai kb delete <kbId>
|
|
1354
1349
|
\`\`\`
|
|
1355
1350
|
|
|
1356
|
-
|
|
1351
|
+
### Memory banks
|
|
1352
|
+
|
|
1357
1353
|
\`\`\`bash
|
|
1358
1354
|
seclai memory list [--page N] [--limit N] [--sort field] [--order asc|desc]
|
|
1359
|
-
|
|
1355
|
+
# type: "conversation" (chat history) or "general" (structured facts)
|
|
1356
|
+
seclai memory create --json '{"name":"Chat Memory","type":"conversation"}'
|
|
1360
1357
|
seclai memory get <memoryBankId>
|
|
1361
|
-
seclai memory update <memoryBankId> --json '{"name":"
|
|
1358
|
+
seclai memory update <memoryBankId> --json '{"name":"Renamed"}'
|
|
1362
1359
|
seclai memory delete <memoryBankId>
|
|
1363
1360
|
\`\`\`
|
|
1364
1361
|
|
|
1365
|
-
|
|
1362
|
+
### Memory bank utilities
|
|
1363
|
+
|
|
1366
1364
|
\`\`\`bash
|
|
1367
1365
|
seclai memory stats <memoryBankId>
|
|
1368
1366
|
seclai memory agents <memoryBankId>
|
|
1369
1367
|
seclai memory compact <memoryBankId>
|
|
1370
1368
|
seclai memory delete-source <memoryBankId>
|
|
1371
1369
|
seclai memory templates
|
|
1372
|
-
seclai memory test-compaction <memoryBankId> --json '{"prompt":"
|
|
1373
|
-
seclai memory test-compaction-standalone --json '{"prompt":"
|
|
1370
|
+
seclai memory test-compaction <memoryBankId> --json '{"prompt":"Summarize the conversation"}'
|
|
1371
|
+
seclai memory test-compaction-standalone --json '{"prompt":"Summarize the conversation"}'
|
|
1374
1372
|
\`\`\`
|
|
1375
1373
|
|
|
1376
|
-
|
|
1374
|
+
### Memory bank AI
|
|
1375
|
+
|
|
1377
1376
|
\`\`\`bash
|
|
1378
1377
|
seclai memory ai generate --user-input "Configure compaction for chat memory"
|
|
1379
1378
|
seclai memory ai last
|
|
1380
1379
|
seclai memory ai accept <conversationId> --json '{"accepted":true}'
|
|
1381
1380
|
\`\`\`
|
|
1382
|
-
`;
|
|
1383
|
-
var EVALS_SOLUTIONS_SKILL = `# Seclai CLI \u2014 Evaluations & Solutions
|
|
1384
1381
|
|
|
1385
|
-
|
|
1382
|
+
### Evaluations \u2014 criteria
|
|
1383
|
+
|
|
1386
1384
|
\`\`\`bash
|
|
1387
1385
|
seclai evals criteria list <agentId> [--page N] [--limit N]
|
|
1388
|
-
seclai evals criteria create <agentId> --json '{"name":"Quality"
|
|
1386
|
+
seclai evals criteria create <agentId> --json '{"name":"Response Quality","description":"...","eval_type":"llm_judge"}'
|
|
1389
1387
|
seclai evals criteria get <criteriaId>
|
|
1390
|
-
seclai evals criteria update <criteriaId> --json '{"name":"Updated"}'
|
|
1388
|
+
seclai evals criteria update <criteriaId> --json '{"name":"Updated Criteria"}'
|
|
1391
1389
|
seclai evals criteria delete <criteriaId>
|
|
1392
1390
|
seclai evals criteria summary <criteriaId>
|
|
1393
1391
|
\`\`\`
|
|
1394
1392
|
|
|
1395
|
-
|
|
1393
|
+
### Evaluations \u2014 results & runs
|
|
1394
|
+
|
|
1396
1395
|
\`\`\`bash
|
|
1397
1396
|
seclai evals results list <criteriaId> [--page N] [--limit N]
|
|
1398
1397
|
seclai evals results create <criteriaId> --json '{"run_id":"...","score":0.9}'
|
|
1399
|
-
\`\`\`
|
|
1400
|
-
|
|
1401
|
-
## Other Evaluation Commands
|
|
1402
|
-
\`\`\`bash
|
|
1403
1398
|
seclai evals compatible-runs <criteriaId> [--page N] [--limit N]
|
|
1404
|
-
seclai evals test-draft <agentId> --json '{"criteria":{
|
|
1399
|
+
seclai evals test-draft <agentId> --json '{"criteria":{"name":"Test","eval_type":"llm_judge"},"run_id":"..."}'
|
|
1405
1400
|
seclai evals agent-results <agentId> [--page N] [--limit N]
|
|
1406
1401
|
seclai evals agent-runs <agentId> [--page N] [--limit N]
|
|
1407
1402
|
seclai evals non-manual-summary <agentId>
|
|
1408
1403
|
\`\`\`
|
|
1409
1404
|
|
|
1410
|
-
|
|
1405
|
+
### Solutions
|
|
1406
|
+
|
|
1411
1407
|
\`\`\`bash
|
|
1412
1408
|
seclai solutions list [--page N] [--limit N] [--sort field] [--order asc|desc]
|
|
1413
|
-
seclai solutions create --json '{"name":"
|
|
1409
|
+
seclai solutions create --json '{"name":"Customer Support Solution"}'
|
|
1414
1410
|
seclai solutions get <solutionId>
|
|
1415
1411
|
seclai solutions update <solutionId> --json '{"name":"Updated"}'
|
|
1416
1412
|
seclai solutions delete <solutionId>
|
|
1417
1413
|
\`\`\`
|
|
1418
1414
|
|
|
1419
|
-
|
|
1415
|
+
### Solution links
|
|
1416
|
+
|
|
1420
1417
|
\`\`\`bash
|
|
1421
|
-
|
|
1422
|
-
seclai solutions
|
|
1418
|
+
# link resources \u2014 each flag takes a JSON array of IDs
|
|
1419
|
+
seclai solutions link <solutionId> --agents '["agentId1"]' --kb '["kbId1"]' --sources '["sourceId1"]'
|
|
1420
|
+
seclai solutions unlink <solutionId> --agents '["agentId1"]'
|
|
1423
1421
|
\`\`\`
|
|
1424
1422
|
|
|
1425
|
-
|
|
1423
|
+
### Solution conversations & AI
|
|
1424
|
+
|
|
1426
1425
|
\`\`\`bash
|
|
1427
1426
|
seclai solutions convos list <solutionId>
|
|
1428
|
-
seclai solutions convos add <solutionId> --json '{"message":"
|
|
1427
|
+
seclai solutions convos add <solutionId> --json '{"message":"How should I structure this?"}'
|
|
1429
1428
|
seclai solutions convos mark <solutionId> <conversationId> --json '{"accepted":true}'
|
|
1430
|
-
\`\`\`
|
|
1431
1429
|
|
|
1432
|
-
## Solution AI
|
|
1433
|
-
\`\`\`bash
|
|
1434
1430
|
seclai solutions ai generate <solutionId> --user-input "Add an FAQ source"
|
|
1435
|
-
seclai solutions ai kb <solutionId> --user-input "Create a knowledge base"
|
|
1436
|
-
seclai solutions ai source <solutionId> --user-input "Create a file source"
|
|
1431
|
+
seclai solutions ai kb <solutionId> --user-input "Create a knowledge base for docs"
|
|
1432
|
+
seclai solutions ai source <solutionId> --user-input "Create a file source for PDFs"
|
|
1437
1433
|
seclai solutions ai accept <solutionId> <conversationId> --json '{"accepted":true}'
|
|
1438
1434
|
seclai solutions ai decline <solutionId> <conversationId>
|
|
1439
1435
|
\`\`\`
|
|
1440
|
-
`;
|
|
1441
|
-
var ALERTS_GOVERNANCE_SKILL = `# Seclai CLI \u2014 Alerts, Governance, Models & Search
|
|
1442
1436
|
|
|
1443
|
-
|
|
1437
|
+
### Alerts
|
|
1438
|
+
|
|
1444
1439
|
\`\`\`bash
|
|
1445
|
-
seclai alerts list [--page N] [--limit N] [--status <
|
|
1440
|
+
seclai alerts list [--page N] [--limit N] [--status <status>] [--severity <severity>]
|
|
1446
1441
|
seclai alerts get <alertId>
|
|
1447
1442
|
seclai alerts status <alertId> --json '{"status":"resolved"}'
|
|
1448
|
-
seclai alerts comment <alertId> --json '{"comment":"Fixed"}'
|
|
1443
|
+
seclai alerts comment <alertId> --json '{"comment":"Fixed the issue"}'
|
|
1449
1444
|
seclai alerts subscribe <alertId>
|
|
1450
1445
|
seclai alerts unsubscribe <alertId>
|
|
1451
1446
|
\`\`\`
|
|
1452
1447
|
|
|
1453
|
-
|
|
1448
|
+
### Alert configurations
|
|
1449
|
+
|
|
1454
1450
|
\`\`\`bash
|
|
1455
1451
|
seclai alerts configs list [--page N] [--limit N]
|
|
1456
|
-
seclai alerts configs create --json '{"name":"
|
|
1452
|
+
seclai alerts configs create --json '{"name":"Latency Alert","description":"...","threshold":5000}'
|
|
1457
1453
|
seclai alerts configs get <configId>
|
|
1458
|
-
seclai alerts configs update <configId> --json '{"
|
|
1454
|
+
seclai alerts configs update <configId> --json '{"threshold":3000}'
|
|
1459
1455
|
seclai alerts configs delete <configId>
|
|
1460
1456
|
\`\`\`
|
|
1461
1457
|
|
|
1462
|
-
|
|
1458
|
+
### Alert preferences
|
|
1459
|
+
|
|
1463
1460
|
\`\`\`bash
|
|
1464
1461
|
seclai alerts prefs list
|
|
1465
|
-
seclai alerts prefs update <
|
|
1462
|
+
seclai alerts prefs update <organizationId> <alertType> --json '{"enabled":true}'
|
|
1466
1463
|
\`\`\`
|
|
1467
1464
|
|
|
1468
|
-
|
|
1465
|
+
### Governance AI
|
|
1466
|
+
|
|
1469
1467
|
\`\`\`bash
|
|
1470
1468
|
seclai governance ai generate --user-input "Create a content safety policy"
|
|
1471
1469
|
seclai governance ai list
|
|
@@ -1473,7 +1471,8 @@ seclai governance ai accept <conversationId>
|
|
|
1473
1471
|
seclai governance ai decline <conversationId>
|
|
1474
1472
|
\`\`\`
|
|
1475
1473
|
|
|
1476
|
-
|
|
1474
|
+
### Model alerts
|
|
1475
|
+
|
|
1477
1476
|
\`\`\`bash
|
|
1478
1477
|
seclai models alerts list [--page N] [--limit N]
|
|
1479
1478
|
seclai models alerts mark-read <alertId>
|
|
@@ -1482,14 +1481,16 @@ seclai models alerts unread-count
|
|
|
1482
1481
|
seclai models recommendations <modelId>
|
|
1483
1482
|
\`\`\`
|
|
1484
1483
|
|
|
1485
|
-
|
|
1484
|
+
### Search
|
|
1485
|
+
|
|
1486
1486
|
\`\`\`bash
|
|
1487
1487
|
seclai search --query "deployment guide" [--limit N] [--entity-type <type>]
|
|
1488
1488
|
\`\`\`
|
|
1489
1489
|
|
|
1490
|
-
|
|
1490
|
+
### AI assistant (global)
|
|
1491
|
+
|
|
1491
1492
|
\`\`\`bash
|
|
1492
|
-
seclai ai feedback --json '{"feedback":"
|
|
1493
|
+
seclai ai feedback --json '{"feedback":"The response was helpful"}'
|
|
1493
1494
|
seclai ai kb --user-input "Create a support knowledge base"
|
|
1494
1495
|
seclai ai source --user-input "Create a documentation source"
|
|
1495
1496
|
seclai ai solution --user-input "Build a customer support solution"
|
|
@@ -1499,15 +1500,237 @@ seclai ai accept <conversationId> --json '{"accepted":true}'
|
|
|
1499
1500
|
seclai ai decline <conversationId>
|
|
1500
1501
|
seclai ai memory-accept <conversationId> --json '{"accepted":true}'
|
|
1501
1502
|
\`\`\`
|
|
1503
|
+
|
|
1504
|
+
### Skills
|
|
1505
|
+
|
|
1506
|
+
\`\`\`bash
|
|
1507
|
+
# install skill files into AI coding tool directories (auto-detects or specify)
|
|
1508
|
+
seclai skills install [--tool copilot|claude|cursor|windsurf|codex|kiro|cline|roo|gemini|antigravity|all] [--dir .]
|
|
1509
|
+
\`\`\`
|
|
1510
|
+
|
|
1511
|
+
### MCP server
|
|
1512
|
+
|
|
1513
|
+
\`\`\`bash
|
|
1514
|
+
# configure MCP server access in AI coding tool config files
|
|
1515
|
+
seclai mcp configure --key <apiKey> [--target claude-code|cursor|claude-desktop|windsurf|all] [--dir .]
|
|
1516
|
+
|
|
1517
|
+
# show the MCP config JSON snippet
|
|
1518
|
+
seclai mcp show [--key <apiKey>]
|
|
1519
|
+
\`\`\`
|
|
1520
|
+
|
|
1521
|
+
## Example: Create a source and upload content
|
|
1522
|
+
|
|
1523
|
+
\`\`\`bash
|
|
1524
|
+
seclai sources create --json '{"name":"Product Docs","description":"Product documentation source"}'
|
|
1525
|
+
# note the id from the output
|
|
1526
|
+
seclai sources upload <sourceId> --file ./docs.pdf --title "Product Manual" --metadata '{"version":"2.0"}'
|
|
1527
|
+
seclai sources get <sourceId>
|
|
1528
|
+
\`\`\`
|
|
1529
|
+
|
|
1530
|
+
## Example: Set up a knowledge base with an agent
|
|
1531
|
+
|
|
1532
|
+
\`\`\`bash
|
|
1533
|
+
seclai kb create --json '{"name":"Support KB","description":"Customer support articles"}'
|
|
1534
|
+
seclai agents create --json '{"name":"Support Bot","description":"Answers customer questions"}'
|
|
1535
|
+
seclai agents ai gen-steps <agentId> --user-input "Build a QA chatbot that searches the Support KB"
|
|
1536
|
+
seclai agents ai mark <agentId> <conversationId> --json '{"accepted":true}'
|
|
1537
|
+
seclai agents run <agentId> --json '{"input":"How do I reset my password?"}' --stream
|
|
1538
|
+
\`\`\`
|
|
1539
|
+
|
|
1540
|
+
## Example: Evaluate agent quality
|
|
1541
|
+
|
|
1542
|
+
\`\`\`bash
|
|
1543
|
+
# create eval criteria
|
|
1544
|
+
seclai evals criteria create <agentId> --json '{"name":"Answer Accuracy","eval_type":"llm_judge","description":"Does the answer correctly address the question?"}'
|
|
1545
|
+
# find compatible runs
|
|
1546
|
+
seclai evals compatible-runs <criteriaId> --limit 5
|
|
1547
|
+
# test the criteria against a run without persisting
|
|
1548
|
+
seclai evals test-draft <agentId> --json '{"criteria":{"name":"Answer Accuracy","eval_type":"llm_judge"},"run_id":"<runId>"}'
|
|
1549
|
+
# create a persisted result
|
|
1550
|
+
seclai evals results create <criteriaId> --json '{"run_id":"<runId>","score":0.95}'
|
|
1551
|
+
# view summary
|
|
1552
|
+
seclai evals criteria summary <criteriaId>
|
|
1553
|
+
\`\`\`
|
|
1554
|
+
|
|
1555
|
+
## Example: Solution with linked resources
|
|
1556
|
+
|
|
1557
|
+
\`\`\`bash
|
|
1558
|
+
seclai solutions create --json '{"name":"Customer Support"}'
|
|
1559
|
+
seclai solutions link <solutionId> --agents '["<agentId>"]' --kb '["<kbId>"]' --sources '["<sourceId>"]'
|
|
1560
|
+
seclai solutions get <solutionId>
|
|
1561
|
+
\`\`\`
|
|
1562
|
+
|
|
1563
|
+
## Example: Memory-powered agent
|
|
1564
|
+
|
|
1565
|
+
\`\`\`bash
|
|
1566
|
+
seclai memory create --json '{"name":"User Preferences","type":"general"}'
|
|
1567
|
+
seclai agents create --json '{"name":"Personal Assistant","description":"Remembers user preferences"}'
|
|
1568
|
+
seclai agents ai gen-steps <agentId> --user-input "Build a chat agent that remembers user preferences. Use general memory bank <memoryBankId>"
|
|
1569
|
+
seclai agents ai mark <agentId> <conversationId> --json '{"accepted":true}'
|
|
1570
|
+
\`\`\`
|
|
1571
|
+
|
|
1572
|
+
## Example: Governance policy setup
|
|
1573
|
+
|
|
1574
|
+
\`\`\`bash
|
|
1575
|
+
seclai governance ai generate --user-input "Create a content safety policy that blocks harmful outputs"
|
|
1576
|
+
seclai governance ai list
|
|
1577
|
+
seclai governance ai accept <conversationId>
|
|
1578
|
+
\`\`\`
|
|
1579
|
+
|
|
1580
|
+
## Specific topics
|
|
1581
|
+
|
|
1582
|
+
* **Streaming & event modes** [references/streaming.md](references/streaming.md)
|
|
1583
|
+
* **File uploads & content management** [references/uploads.md](references/uploads.md)
|
|
1584
|
+
* **Evaluations workflow** [references/evaluations.md](references/evaluations.md)
|
|
1585
|
+
`;
|
|
1586
|
+
var STREAMING_REF = `# Streaming Agent Runs
|
|
1587
|
+
|
|
1588
|
+
## Modes
|
|
1589
|
+
|
|
1590
|
+
### --stream
|
|
1591
|
+
Wait for the agent run to complete via SSE. Prints the final result as a single JSON object.
|
|
1592
|
+
Useful when you want to block until done.
|
|
1593
|
+
|
|
1594
|
+
\`\`\`bash
|
|
1595
|
+
seclai agents run <agentId> --json '{"input":"Hello"}' --stream
|
|
1596
|
+
seclai agents run <agentId> --json '{"input":"Hello"}' --stream --timeout-ms 120000
|
|
1597
|
+
\`\`\`
|
|
1598
|
+
|
|
1599
|
+
### --events
|
|
1600
|
+
Stream individual SSE events as NDJSON (one JSON object per line). Use for real-time processing.
|
|
1601
|
+
|
|
1602
|
+
\`\`\`bash
|
|
1603
|
+
# all events, full event objects
|
|
1604
|
+
seclai agents run <agentId> --json '{"input":"Hello"}' --events
|
|
1605
|
+
|
|
1606
|
+
# only data payloads (no event metadata)
|
|
1607
|
+
seclai agents run <agentId> --json '{"input":"Hello"}' --events --output data
|
|
1608
|
+
|
|
1609
|
+
# only status events
|
|
1610
|
+
seclai agents run <agentId> --json '{"input":"Hello"}' --events --output status
|
|
1611
|
+
|
|
1612
|
+
# filter specific event types
|
|
1613
|
+
seclai agents run <agentId> --json '{"input":"Hello"}' --events --event-filter "status,data"
|
|
1614
|
+
\`\`\`
|
|
1615
|
+
|
|
1616
|
+
Output modes for --events:
|
|
1617
|
+
- \`full\`: entire SSE event object (default)
|
|
1618
|
+
- \`data\`: only the data payload of each event
|
|
1619
|
+
- \`status\`: only events with status information
|
|
1620
|
+
|
|
1621
|
+
### --poll
|
|
1622
|
+
Poll the API at intervals for run completion. Does not use SSE.
|
|
1623
|
+
|
|
1624
|
+
\`\`\`bash
|
|
1625
|
+
seclai agents run <agentId> --json '{"input":"Hello"}' --poll
|
|
1626
|
+
seclai agents run <agentId> --json '{"input":"Hello"}' --poll --poll-interval-ms 5000 --include-step-outputs
|
|
1627
|
+
\`\`\`
|
|
1628
|
+
|
|
1629
|
+
### No flag
|
|
1630
|
+
Fire-and-forget: starts the run and immediately returns the run ID.
|
|
1631
|
+
|
|
1632
|
+
\`\`\`bash
|
|
1633
|
+
seclai agents run <agentId> --json '{"input":"Hello"}'
|
|
1634
|
+
# returns: {"id":"run_...","status":"queued",...}
|
|
1635
|
+
# check later:
|
|
1636
|
+
seclai agents runs get <runId>
|
|
1637
|
+
\`\`\`
|
|
1638
|
+
`;
|
|
1639
|
+
var UPLOADS_REF = `# File Uploads & Content Management
|
|
1640
|
+
|
|
1641
|
+
## Upload to a source
|
|
1642
|
+
\`\`\`bash
|
|
1643
|
+
seclai sources upload <sourceId> --file ./doc.pdf
|
|
1644
|
+
seclai sources upload <sourceId> --file ./doc.pdf --title "My Doc" --metadata '{"category":"docs"}' --file-name "custom-name.pdf" --mime-type "application/pdf"
|
|
1645
|
+
seclai sources upload <sourceId> --file ./doc.pdf --metadata-file ./meta.json
|
|
1646
|
+
\`\`\`
|
|
1647
|
+
|
|
1648
|
+
## Upload text directly
|
|
1649
|
+
\`\`\`bash
|
|
1650
|
+
seclai sources upload-text <sourceId> --json '{"text":"Article content here...","title":"My Article"}'
|
|
1651
|
+
\`\`\`
|
|
1652
|
+
|
|
1653
|
+
## Upload input for agent runs
|
|
1654
|
+
\`\`\`bash
|
|
1655
|
+
seclai agents upload-input <agentId> --file ./input.pdf
|
|
1656
|
+
seclai agents upload-input <agentId> --file ./data.csv --file-name "report.csv" --mime-type "text/csv"
|
|
1657
|
+
seclai agents input-status <agentId> <uploadId>
|
|
1658
|
+
\`\`\`
|
|
1659
|
+
|
|
1660
|
+
## Replace content
|
|
1661
|
+
\`\`\`bash
|
|
1662
|
+
# replace with file
|
|
1663
|
+
seclai contents upload <contentVersionId> --file ./updated.pdf
|
|
1664
|
+
|
|
1665
|
+
# replace with text
|
|
1666
|
+
seclai contents replace-text <contentVersionId> --json '{"text":"Updated content","title":"Revised Article"}'
|
|
1667
|
+
\`\`\`
|
|
1668
|
+
|
|
1669
|
+
## Read content
|
|
1670
|
+
\`\`\`bash
|
|
1671
|
+
# full content
|
|
1672
|
+
seclai contents get <contentVersionId>
|
|
1673
|
+
|
|
1674
|
+
# text slice (0-based offsets)
|
|
1675
|
+
seclai contents get <contentVersionId> --start 0 --end 1000
|
|
1676
|
+
|
|
1677
|
+
# view embeddings
|
|
1678
|
+
seclai contents embeddings <contentVersionId> [--page N] [--limit N]
|
|
1679
|
+
\`\`\`
|
|
1680
|
+
`;
|
|
1681
|
+
var EVALUATIONS_REF = `# Evaluations Workflow
|
|
1682
|
+
|
|
1683
|
+
## Step 1: Create evaluation criteria for an agent
|
|
1684
|
+
\`\`\`bash
|
|
1685
|
+
seclai evals criteria create <agentId> --json '{"name":"Answer Accuracy","description":"Does the answer correctly address the question?","eval_type":"llm_judge"}'
|
|
1686
|
+
\`\`\`
|
|
1687
|
+
|
|
1688
|
+
## Step 2: Find runs to evaluate
|
|
1689
|
+
\`\`\`bash
|
|
1690
|
+
# list all runs for an agent
|
|
1691
|
+
seclai agents runs list <agentId> --limit 10
|
|
1692
|
+
|
|
1693
|
+
# or find runs compatible with specific criteria
|
|
1694
|
+
seclai evals compatible-runs <criteriaId> --limit 10
|
|
1695
|
+
\`\`\`
|
|
1696
|
+
|
|
1697
|
+
## Step 3: Test criteria before committing
|
|
1698
|
+
\`\`\`bash
|
|
1699
|
+
seclai evals test-draft <agentId> --json '{"criteria":{"name":"Answer Accuracy","eval_type":"llm_judge","description":"..."},"run_id":"<runId>"}'
|
|
1700
|
+
\`\`\`
|
|
1701
|
+
|
|
1702
|
+
## Step 4: Create evaluation results
|
|
1703
|
+
\`\`\`bash
|
|
1704
|
+
seclai evals results create <criteriaId> --json '{"run_id":"<runId>","score":0.95}'
|
|
1705
|
+
\`\`\`
|
|
1706
|
+
|
|
1707
|
+
## Step 5: Review summaries
|
|
1708
|
+
\`\`\`bash
|
|
1709
|
+
seclai evals criteria summary <criteriaId>
|
|
1710
|
+
seclai evals agent-results <agentId>
|
|
1711
|
+
seclai evals agent-runs <agentId> --limit 20
|
|
1712
|
+
seclai evals non-manual-summary <agentId>
|
|
1713
|
+
\`\`\`
|
|
1714
|
+
|
|
1715
|
+
## Managing criteria
|
|
1716
|
+
\`\`\`bash
|
|
1717
|
+
seclai evals criteria list <agentId>
|
|
1718
|
+
seclai evals criteria get <criteriaId>
|
|
1719
|
+
seclai evals criteria update <criteriaId> --json '{"name":"Updated Name"}'
|
|
1720
|
+
seclai evals criteria delete <criteriaId>
|
|
1721
|
+
\`\`\`
|
|
1722
|
+
|
|
1723
|
+
## Viewing results
|
|
1724
|
+
\`\`\`bash
|
|
1725
|
+
seclai evals results list <criteriaId> [--page N] [--limit N]
|
|
1726
|
+
\`\`\`
|
|
1502
1727
|
`;
|
|
1503
1728
|
function getToolConfig(tool, destDir) {
|
|
1504
1729
|
const skillFiles = [
|
|
1505
1730
|
{ name: "SKILL.md", content: SKILL_MD },
|
|
1506
|
-
{ name: "references/
|
|
1507
|
-
{ name: "references/
|
|
1508
|
-
{ name: "references/
|
|
1509
|
-
{ name: "references/evals-solutions.md", content: EVALS_SOLUTIONS_SKILL },
|
|
1510
|
-
{ name: "references/alerts-governance.md", content: ALERTS_GOVERNANCE_SKILL }
|
|
1731
|
+
{ name: "references/streaming.md", content: STREAMING_REF },
|
|
1732
|
+
{ name: "references/uploads.md", content: UPLOADS_REF },
|
|
1733
|
+
{ name: "references/evaluations.md", content: EVALUATIONS_REF }
|
|
1511
1734
|
];
|
|
1512
1735
|
switch (tool) {
|
|
1513
1736
|
case "copilot":
|
|
@@ -1586,6 +1809,126 @@ function register13(program, rt) {
|
|
|
1586
1809
|
});
|
|
1587
1810
|
}
|
|
1588
1811
|
|
|
1812
|
+
// src/commands/mcp.ts
|
|
1813
|
+
import { existsSync as existsSync2 } from "fs";
|
|
1814
|
+
import { mkdir as mkdir2, readFile as readFile2, writeFile as writeFile2 } from "fs/promises";
|
|
1815
|
+
import { dirname as dirname2, join as join2 } from "path";
|
|
1816
|
+
import { homedir, platform } from "os";
|
|
1817
|
+
var MCP_URL = "https://api.seclai.com/mcp";
|
|
1818
|
+
function buildMcpEntry(apiKey) {
|
|
1819
|
+
return {
|
|
1820
|
+
type: "streamable-http",
|
|
1821
|
+
url: MCP_URL,
|
|
1822
|
+
headers: { "X-API-Key": apiKey }
|
|
1823
|
+
};
|
|
1824
|
+
}
|
|
1825
|
+
function getTargets(destDir) {
|
|
1826
|
+
const home = homedir();
|
|
1827
|
+
const os = platform();
|
|
1828
|
+
const targets = [
|
|
1829
|
+
// Project-scoped configs
|
|
1830
|
+
{ name: "claude-code", path: join2(destDir, ".mcp.json"), scope: "project" },
|
|
1831
|
+
{ name: "cursor", path: join2(destDir, ".cursor", "mcp.json"), scope: "project" }
|
|
1832
|
+
];
|
|
1833
|
+
if (os === "win32") {
|
|
1834
|
+
targets.push({
|
|
1835
|
+
name: "claude-desktop",
|
|
1836
|
+
path: join2(process.env["APPDATA"] ?? join2(home, "AppData", "Roaming"), "Claude", "claude_desktop_config.json"),
|
|
1837
|
+
scope: "global"
|
|
1838
|
+
});
|
|
1839
|
+
} else if (os === "darwin") {
|
|
1840
|
+
targets.push({
|
|
1841
|
+
name: "claude-desktop",
|
|
1842
|
+
path: join2(home, "Library", "Application Support", "Claude", "claude_desktop_config.json"),
|
|
1843
|
+
scope: "global"
|
|
1844
|
+
});
|
|
1845
|
+
}
|
|
1846
|
+
targets.push({ name: "windsurf", path: join2(home, ".codeium", "windsurf", "mcp_config.json"), scope: "global" });
|
|
1847
|
+
return targets;
|
|
1848
|
+
}
|
|
1849
|
+
async function mergeConfig(filePath, apiKey) {
|
|
1850
|
+
let existing = {};
|
|
1851
|
+
if (existsSync2(filePath)) {
|
|
1852
|
+
try {
|
|
1853
|
+
const parsed = JSON.parse(await readFile2(filePath, "utf8"));
|
|
1854
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return false;
|
|
1855
|
+
existing = parsed;
|
|
1856
|
+
} catch {
|
|
1857
|
+
return false;
|
|
1858
|
+
}
|
|
1859
|
+
}
|
|
1860
|
+
const raw = existing["mcpServers"];
|
|
1861
|
+
const servers = typeof raw === "object" && raw !== null && !Array.isArray(raw) ? raw : {};
|
|
1862
|
+
servers["seclai"] = buildMcpEntry(apiKey);
|
|
1863
|
+
existing["mcpServers"] = servers;
|
|
1864
|
+
await mkdir2(dirname2(filePath), { recursive: true });
|
|
1865
|
+
await writeFile2(filePath, JSON.stringify(existing, null, 2) + "\n", "utf8");
|
|
1866
|
+
return true;
|
|
1867
|
+
}
|
|
1868
|
+
function detectTargets(destDir) {
|
|
1869
|
+
const all = getTargets(destDir);
|
|
1870
|
+
return all.filter((t) => {
|
|
1871
|
+
if (t.scope === "global") return existsSync2(dirname2(t.path));
|
|
1872
|
+
if (t.name === "claude-code") return existsSync2(join2(destDir, ".claude")) || existsSync2(join2(destDir, "CLAUDE.md"));
|
|
1873
|
+
if (t.name === "cursor") return existsSync2(join2(destDir, ".cursor"));
|
|
1874
|
+
return false;
|
|
1875
|
+
});
|
|
1876
|
+
}
|
|
1877
|
+
function register14(program, rt) {
|
|
1878
|
+
const mcp = program.command("mcp").description("Configure the Seclai MCP server for AI coding tools.");
|
|
1879
|
+
mcp.command("configure").description(
|
|
1880
|
+
"Add the Seclai MCP server to AI coding tool config files.\n\nTargets: claude-code, cursor, claude-desktop, windsurf.\nUse --target to pick a specific tool, or 'all' for all known targets."
|
|
1881
|
+
).requiredOption("--key <key>", "Seclai API key to embed in the config.").option("--target <name>", "Target tool (claude-code|cursor|claude-desktop|windsurf|all). Auto-detects if omitted.").option("--dir <path>", "Project directory for project-scoped configs (default: current directory).", ".").action(async (opts) => {
|
|
1882
|
+
await run(rt, async () => {
|
|
1883
|
+
const destDir = opts.dir;
|
|
1884
|
+
const apiKey = opts.key;
|
|
1885
|
+
const allTargets = getTargets(destDir);
|
|
1886
|
+
let targets;
|
|
1887
|
+
if (opts.target === "all") {
|
|
1888
|
+
targets = allTargets;
|
|
1889
|
+
} else if (opts.target) {
|
|
1890
|
+
const found = allTargets.find((t) => t.name === opts.target);
|
|
1891
|
+
if (!found) {
|
|
1892
|
+
rt.writeErr(`Unknown target "${opts.target}". Use: claude-code, cursor, claude-desktop, windsurf, or all.
|
|
1893
|
+
`);
|
|
1894
|
+
rt.setExitCode(1);
|
|
1895
|
+
return;
|
|
1896
|
+
}
|
|
1897
|
+
targets = [found];
|
|
1898
|
+
} else {
|
|
1899
|
+
targets = detectTargets(destDir);
|
|
1900
|
+
if (targets.length === 0) {
|
|
1901
|
+
targets = [allTargets[0]];
|
|
1902
|
+
rt.writeErr("No MCP-compatible tool detected, defaulting to claude-code (.mcp.json).\n");
|
|
1903
|
+
}
|
|
1904
|
+
}
|
|
1905
|
+
let configured = 0;
|
|
1906
|
+
const failures = [];
|
|
1907
|
+
for (const target of targets) {
|
|
1908
|
+
const ok = await mergeConfig(target.path, apiKey);
|
|
1909
|
+
if (ok) {
|
|
1910
|
+
configured++;
|
|
1911
|
+
rt.writeErr(`Configured seclai MCP for ${target.name} \u2192 ${target.path}
|
|
1912
|
+
`);
|
|
1913
|
+
} else {
|
|
1914
|
+
failures.push(target.name);
|
|
1915
|
+
rt.writeErr(`Failed to parse existing config at ${target.path}, skipping.
|
|
1916
|
+
`);
|
|
1917
|
+
}
|
|
1918
|
+
}
|
|
1919
|
+
const allOk = failures.length === 0;
|
|
1920
|
+
printJson(rt, { ok: allOk, targets: targets.map((t) => t.name), filesWritten: configured, ...failures.length > 0 ? { failures } : {} });
|
|
1921
|
+
if (!allOk) rt.setExitCode(1);
|
|
1922
|
+
});
|
|
1923
|
+
});
|
|
1924
|
+
mcp.command("show").description("Show the Seclai MCP server JSON configuration snippet.").option("--key <key>", "API key to include (default: placeholder).").action(async (opts) => {
|
|
1925
|
+
await run(rt, async () => {
|
|
1926
|
+
const entry = buildMcpEntry(opts.key ?? "YOUR_API_KEY");
|
|
1927
|
+
printJson(rt, { mcpServers: { seclai: entry } });
|
|
1928
|
+
});
|
|
1929
|
+
});
|
|
1930
|
+
}
|
|
1931
|
+
|
|
1589
1932
|
// src/commands/completion.ts
|
|
1590
1933
|
var BASH = `#!/usr/bin/env bash
|
|
1591
1934
|
# seclai bash completion \u2014 add to ~/.bashrc:
|
|
@@ -1597,7 +1940,7 @@ _seclai_completions() {
|
|
|
1597
1940
|
prev="\${COMP_WORDS[COMP_CWORD-1]}"
|
|
1598
1941
|
|
|
1599
1942
|
# Top-level commands
|
|
1600
|
-
commands="agents sources contents kb memory evals solutions governance alerts models search ai skills completion help"
|
|
1943
|
+
commands="agents sources contents kb memory evals solutions governance alerts models search ai skills mcp completion help"
|
|
1601
1944
|
|
|
1602
1945
|
case "\${COMP_WORDS[1]}" in
|
|
1603
1946
|
agents)
|
|
@@ -1650,6 +1993,7 @@ _seclai_completions() {
|
|
|
1650
1993
|
esac ;;
|
|
1651
1994
|
ai) COMPREPLY=( $(compgen -W "feedback kb source solution memory memory-history accept decline memory-accept" -- "$cur") ); return ;;
|
|
1652
1995
|
skills) COMPREPLY=( $(compgen -W "install" -- "$cur") ); return ;;
|
|
1996
|
+
mcp) COMPREPLY=( $(compgen -W "configure show" -- "$cur") ); return ;;
|
|
1653
1997
|
completion) COMPREPLY=( $(compgen -W "bash zsh fish" -- "$cur") ); return ;;
|
|
1654
1998
|
esac
|
|
1655
1999
|
|
|
@@ -1678,6 +2022,7 @@ _seclai() {
|
|
|
1678
2022
|
'search:Search across Seclai resources'
|
|
1679
2023
|
'ai:Top-level AI assistant'
|
|
1680
2024
|
'skills:Install skill files for AI coding tools'
|
|
2025
|
+
'mcp:Configure the Seclai MCP server'
|
|
1681
2026
|
'completion:Generate shell completion scripts'
|
|
1682
2027
|
'help:Display help for command'
|
|
1683
2028
|
)
|
|
@@ -1730,6 +2075,9 @@ _seclai() {
|
|
|
1730
2075
|
skills)
|
|
1731
2076
|
local -a sub=(install)
|
|
1732
2077
|
_describe 'subcommand' sub ;;
|
|
2078
|
+
mcp)
|
|
2079
|
+
local -a sub=(configure show)
|
|
2080
|
+
_describe 'subcommand' sub ;;
|
|
1733
2081
|
completion)
|
|
1734
2082
|
local -a sub=(bash zsh fish)
|
|
1735
2083
|
_describe 'shell' sub ;;
|
|
@@ -1742,7 +2090,7 @@ _seclai "$@"
|
|
|
1742
2090
|
var FISH = `# seclai fish completion \u2014 save to ~/.config/fish/completions/seclai.fish
|
|
1743
2091
|
# seclai completion fish > ~/.config/fish/completions/seclai.fish
|
|
1744
2092
|
|
|
1745
|
-
set -l top agents sources contents kb memory evals solutions governance alerts models search ai skills completion help
|
|
2093
|
+
set -l top agents sources contents kb memory evals solutions governance alerts models search ai skills mcp completion help
|
|
1746
2094
|
|
|
1747
2095
|
# Top-level
|
|
1748
2096
|
complete -c seclai -n "not __fish_seen_subcommand_from $top" -f -a "agents" -d "Manage agents"
|
|
@@ -1758,6 +2106,7 @@ complete -c seclai -n "not __fish_seen_subcommand_from $top" -f -a "models" -d "
|
|
|
1758
2106
|
complete -c seclai -n "not __fish_seen_subcommand_from $top" -f -a "search" -d "Search resources"
|
|
1759
2107
|
complete -c seclai -n "not __fish_seen_subcommand_from $top" -f -a "ai" -d "AI assistant"
|
|
1760
2108
|
complete -c seclai -n "not __fish_seen_subcommand_from $top" -f -a "skills" -d "Skill files"
|
|
2109
|
+
complete -c seclai -n "not __fish_seen_subcommand_from $top" -f -a "mcp" -d "MCP server config"
|
|
1761
2110
|
complete -c seclai -n "not __fish_seen_subcommand_from $top" -f -a "completion" -d "Shell completions"
|
|
1762
2111
|
|
|
1763
2112
|
# agents
|
|
@@ -1796,6 +2145,9 @@ complete -c seclai -n "__fish_seen_subcommand_from ai; and not __fish_seen_subco
|
|
|
1796
2145
|
# skills
|
|
1797
2146
|
complete -c seclai -n "__fish_seen_subcommand_from skills; and not __fish_seen_subcommand_from install" -f -a "install"
|
|
1798
2147
|
|
|
2148
|
+
# mcp
|
|
2149
|
+
complete -c seclai -n "__fish_seen_subcommand_from mcp; and not __fish_seen_subcommand_from configure show" -f -a "configure show"
|
|
2150
|
+
|
|
1799
2151
|
# completion
|
|
1800
2152
|
complete -c seclai -n "__fish_seen_subcommand_from completion; and not __fish_seen_subcommand_from bash zsh fish" -f -a "bash zsh fish"
|
|
1801
2153
|
|
|
@@ -1805,7 +2157,7 @@ complete -c seclai -l compact -d "Output compact JSON"
|
|
|
1805
2157
|
complete -c seclai -s V -l version -d "Output version"
|
|
1806
2158
|
`;
|
|
1807
2159
|
var SCRIPTS = { bash: BASH, zsh: ZSH, fish: FISH };
|
|
1808
|
-
function
|
|
2160
|
+
function register15(program, rt) {
|
|
1809
2161
|
const completion = program.command("completion").description("Generate shell completion scripts.").argument("<shell>", "Shell type: bash, zsh, or fish.").action(async (shell) => {
|
|
1810
2162
|
const script = SCRIPTS[shell];
|
|
1811
2163
|
if (!script) {
|
|
@@ -1818,6 +2170,453 @@ function register14(program, rt) {
|
|
|
1818
2170
|
});
|
|
1819
2171
|
}
|
|
1820
2172
|
|
|
2173
|
+
// src/commands/auth.ts
|
|
2174
|
+
import { randomBytes, createHash } from "crypto";
|
|
2175
|
+
import { createServer } from "http";
|
|
2176
|
+
import { readFile as readFile3, writeFile as writeFile3 } from "fs/promises";
|
|
2177
|
+
import { join as join3 } from "path";
|
|
2178
|
+
import { URL as URL2, URLSearchParams } from "url";
|
|
2179
|
+
import process3 from "process";
|
|
2180
|
+
import {
|
|
2181
|
+
loadSsoProfile,
|
|
2182
|
+
readSsoCache,
|
|
2183
|
+
writeSsoCache,
|
|
2184
|
+
deleteSsoCache,
|
|
2185
|
+
isTokenValid
|
|
2186
|
+
} from "@seclai/sdk";
|
|
2187
|
+
function generateCodeVerifier() {
|
|
2188
|
+
return randomBytes(32).toString("base64url");
|
|
2189
|
+
}
|
|
2190
|
+
function computeCodeChallenge(verifier) {
|
|
2191
|
+
return createHash("sha256").update(verifier).digest("base64url");
|
|
2192
|
+
}
|
|
2193
|
+
function waitForAuthCode(port, state) {
|
|
2194
|
+
return new Promise((resolve, reject) => {
|
|
2195
|
+
const server = createServer((req, res) => {
|
|
2196
|
+
const url = new URL2(req.url ?? "/", `http://localhost:${port}`);
|
|
2197
|
+
if (url.pathname !== "/callback") {
|
|
2198
|
+
res.writeHead(404);
|
|
2199
|
+
res.end();
|
|
2200
|
+
return;
|
|
2201
|
+
}
|
|
2202
|
+
const code = url.searchParams.get("code");
|
|
2203
|
+
const returnedState = url.searchParams.get("state");
|
|
2204
|
+
const error = url.searchParams.get("error");
|
|
2205
|
+
if (error) {
|
|
2206
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
2207
|
+
res.end("<html><body><h2>Authentication failed</h2><p>You can close this tab.</p></body></html>");
|
|
2208
|
+
reject(new Error(`OAuth error: ${error}`));
|
|
2209
|
+
server.close();
|
|
2210
|
+
return;
|
|
2211
|
+
}
|
|
2212
|
+
if (!code || returnedState !== state) {
|
|
2213
|
+
res.writeHead(400, { "Content-Type": "text/html" });
|
|
2214
|
+
res.end("<html><body><h2>Invalid callback</h2></body></html>");
|
|
2215
|
+
reject(new Error("Invalid callback: missing code or state mismatch"));
|
|
2216
|
+
server.close();
|
|
2217
|
+
return;
|
|
2218
|
+
}
|
|
2219
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
2220
|
+
res.end("<html><body><h2>Authenticated successfully!</h2><p>You can close this tab.</p></body></html>");
|
|
2221
|
+
resolve({
|
|
2222
|
+
code,
|
|
2223
|
+
cleanup: () => server.close()
|
|
2224
|
+
});
|
|
2225
|
+
});
|
|
2226
|
+
server.listen(port, "127.0.0.1");
|
|
2227
|
+
server.on("error", reject);
|
|
2228
|
+
});
|
|
2229
|
+
}
|
|
2230
|
+
async function exchangeCodeForTokens(profile, code, codeVerifier, redirectUri) {
|
|
2231
|
+
const tokenUrl = `https://${profile.ssoDomain}/oauth2/token`;
|
|
2232
|
+
const body = new URLSearchParams({
|
|
2233
|
+
grant_type: "authorization_code",
|
|
2234
|
+
client_id: profile.ssoClientId,
|
|
2235
|
+
code,
|
|
2236
|
+
redirect_uri: redirectUri,
|
|
2237
|
+
code_verifier: codeVerifier
|
|
2238
|
+
});
|
|
2239
|
+
const resp = await fetch(tokenUrl, {
|
|
2240
|
+
method: "POST",
|
|
2241
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
2242
|
+
body: body.toString()
|
|
2243
|
+
});
|
|
2244
|
+
if (!resp.ok) {
|
|
2245
|
+
const text = await resp.text();
|
|
2246
|
+
throw new Error(`Token exchange failed (HTTP ${resp.status}): ${text}`);
|
|
2247
|
+
}
|
|
2248
|
+
const data = await resp.json();
|
|
2249
|
+
const expiresAt = new Date(Date.now() + data.expires_in * 1e3).toISOString();
|
|
2250
|
+
const entry = {
|
|
2251
|
+
accessToken: data.access_token,
|
|
2252
|
+
expiresAt,
|
|
2253
|
+
clientId: profile.ssoClientId,
|
|
2254
|
+
region: profile.ssoRegion,
|
|
2255
|
+
cognitoDomain: profile.ssoDomain
|
|
2256
|
+
};
|
|
2257
|
+
if (data.refresh_token) entry.refreshToken = data.refresh_token;
|
|
2258
|
+
if (data.id_token) entry.idToken = data.id_token;
|
|
2259
|
+
return entry;
|
|
2260
|
+
}
|
|
2261
|
+
var DEFAULT_CALLBACK_PORT = 9876;
|
|
2262
|
+
function resolveBaseUrl() {
|
|
2263
|
+
const envUrl = process3.env.SECLAI_API_URL;
|
|
2264
|
+
return envUrl && envUrl.length > 0 ? envUrl : "https://api.seclai.com";
|
|
2265
|
+
}
|
|
2266
|
+
async function fetchAccountId(accessToken) {
|
|
2267
|
+
const baseUrl = resolveBaseUrl();
|
|
2268
|
+
const resp = await fetch(`${baseUrl}/me`, {
|
|
2269
|
+
headers: { authorization: `Bearer ${accessToken}` }
|
|
2270
|
+
});
|
|
2271
|
+
if (!resp.ok) {
|
|
2272
|
+
const text = await resp.text();
|
|
2273
|
+
throw new Error(`Failed to resolve account ID from /me (HTTP ${resp.status}): ${text}`);
|
|
2274
|
+
}
|
|
2275
|
+
const data = await resp.json();
|
|
2276
|
+
return data.account_id;
|
|
2277
|
+
}
|
|
2278
|
+
async function updateConfigKey(configDir, profileName, key, value) {
|
|
2279
|
+
const configPath = join3(configDir, "config");
|
|
2280
|
+
let content = "";
|
|
2281
|
+
try {
|
|
2282
|
+
content = await readFile3(configPath, "utf-8");
|
|
2283
|
+
} catch {
|
|
2284
|
+
}
|
|
2285
|
+
const sectionHeader = profileName === "default" ? "[default]" : `[profile ${profileName}]`;
|
|
2286
|
+
const sectionIdx = content.indexOf(sectionHeader);
|
|
2287
|
+
if (sectionIdx === -1) {
|
|
2288
|
+
if (content.length > 0 && !content.endsWith("\n")) {
|
|
2289
|
+
content += "\n";
|
|
2290
|
+
}
|
|
2291
|
+
content += `
|
|
2292
|
+
${sectionHeader}
|
|
2293
|
+
${key} = ${value}
|
|
2294
|
+
`;
|
|
2295
|
+
} else {
|
|
2296
|
+
const afterHeader = sectionIdx + sectionHeader.length;
|
|
2297
|
+
const nextSectionMatch = content.slice(afterHeader).match(/\n\[/);
|
|
2298
|
+
const sectionEnd = nextSectionMatch ? afterHeader + nextSectionMatch.index : content.length;
|
|
2299
|
+
const sectionBody = content.slice(afterHeader, sectionEnd);
|
|
2300
|
+
const keyRegex = new RegExp(`^${key}\\s*=.*$`, "m");
|
|
2301
|
+
let newSectionBody;
|
|
2302
|
+
if (keyRegex.test(sectionBody)) {
|
|
2303
|
+
newSectionBody = sectionBody.replace(keyRegex, `${key} = ${value}`);
|
|
2304
|
+
} else {
|
|
2305
|
+
const trimmed = sectionBody.trimEnd();
|
|
2306
|
+
newSectionBody = `${trimmed}
|
|
2307
|
+
${key} = ${value}
|
|
2308
|
+
`;
|
|
2309
|
+
}
|
|
2310
|
+
content = content.slice(0, afterHeader) + newSectionBody + content.slice(sectionEnd);
|
|
2311
|
+
}
|
|
2312
|
+
const { mkdir: mkdir4 } = await import("fs/promises");
|
|
2313
|
+
await mkdir4(configDir, { recursive: true });
|
|
2314
|
+
await writeFile3(configPath, content, { mode: 384 });
|
|
2315
|
+
}
|
|
2316
|
+
function resolveProfile(opts) {
|
|
2317
|
+
const profileName = opts.profile || process3.env.SECLAI_PROFILE || "default";
|
|
2318
|
+
let configDir = opts.configDir || process3.env.SECLAI_CONFIG_DIR;
|
|
2319
|
+
if (!configDir) {
|
|
2320
|
+
const home = process3.env.HOME ?? process3.env.USERPROFILE;
|
|
2321
|
+
if (home && home.trim() !== "") {
|
|
2322
|
+
configDir = join3(home, ".seclai");
|
|
2323
|
+
} else {
|
|
2324
|
+
configDir = join3(process3.cwd(), ".seclai");
|
|
2325
|
+
}
|
|
2326
|
+
}
|
|
2327
|
+
return { profileName, configDir };
|
|
2328
|
+
}
|
|
2329
|
+
async function loadProfile(rt, opts) {
|
|
2330
|
+
const { profileName, configDir } = resolveProfile(opts);
|
|
2331
|
+
const profile = await loadSsoProfile(configDir, profileName);
|
|
2332
|
+
return { profile, profileName, configDir };
|
|
2333
|
+
}
|
|
2334
|
+
function register16(program, rt) {
|
|
2335
|
+
const group = program.command("auth").description("SSO authentication (login/logout/status/refresh).");
|
|
2336
|
+
group.command("login").description("Authenticate via SSO using Authorization Code + PKCE flow.").option("--port <port>", "Local callback port", String(DEFAULT_CALLBACK_PORT)).option("--no-browser", "Print the URL instead of opening a browser").action(async (opts) => {
|
|
2337
|
+
await run(rt, async () => {
|
|
2338
|
+
const globalOpts = program.opts();
|
|
2339
|
+
const { profile, profileName, configDir } = await loadProfile(rt, globalOpts);
|
|
2340
|
+
const port = parseInt(opts.port ?? String(DEFAULT_CALLBACK_PORT), 10);
|
|
2341
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
2342
|
+
throw new Error(`Invalid port: ${opts.port}. Must be an integer between 1 and 65535.`);
|
|
2343
|
+
}
|
|
2344
|
+
const redirectUri = `http://localhost:${port}/callback`;
|
|
2345
|
+
const codeVerifier = generateCodeVerifier();
|
|
2346
|
+
const codeChallenge = computeCodeChallenge(codeVerifier);
|
|
2347
|
+
const state = randomBytes(16).toString("hex");
|
|
2348
|
+
const authUrl = new URL2(`https://${profile.ssoDomain}/oauth2/authorize`);
|
|
2349
|
+
authUrl.searchParams.set("response_type", "code");
|
|
2350
|
+
authUrl.searchParams.set("client_id", profile.ssoClientId);
|
|
2351
|
+
authUrl.searchParams.set("redirect_uri", redirectUri);
|
|
2352
|
+
authUrl.searchParams.set("scope", "openid profile email");
|
|
2353
|
+
authUrl.searchParams.set("state", state);
|
|
2354
|
+
authUrl.searchParams.set("code_challenge", codeChallenge);
|
|
2355
|
+
authUrl.searchParams.set("code_challenge_method", "S256");
|
|
2356
|
+
const authUrlStr = authUrl.toString();
|
|
2357
|
+
const codePromise = waitForAuthCode(port, state);
|
|
2358
|
+
if (opts.browser !== false) {
|
|
2359
|
+
const { spawn } = await import("child_process");
|
|
2360
|
+
const openCmd = process3.platform === "darwin" ? { cmd: "open", args: [authUrlStr] } : process3.platform === "win32" ? { cmd: "cmd", args: ["/c", "start", "", authUrlStr] } : { cmd: "xdg-open", args: [authUrlStr] };
|
|
2361
|
+
spawn(openCmd.cmd, openCmd.args, { stdio: "ignore", detached: true }).unref();
|
|
2362
|
+
rt.writeErr(`Opening browser for authentication...
|
|
2363
|
+
`);
|
|
2364
|
+
} else {
|
|
2365
|
+
rt.writeErr(`Open this URL in your browser:
|
|
2366
|
+
|
|
2367
|
+
${authUrlStr}
|
|
2368
|
+
|
|
2369
|
+
`);
|
|
2370
|
+
}
|
|
2371
|
+
rt.writeErr("Waiting for authentication callback...\n");
|
|
2372
|
+
const { code, cleanup } = await codePromise;
|
|
2373
|
+
let tokens;
|
|
2374
|
+
try {
|
|
2375
|
+
rt.writeErr("Exchanging code for tokens...\n");
|
|
2376
|
+
tokens = await exchangeCodeForTokens(profile, code, codeVerifier, redirectUri);
|
|
2377
|
+
await writeSsoCache(configDir, profile, tokens);
|
|
2378
|
+
} finally {
|
|
2379
|
+
cleanup();
|
|
2380
|
+
}
|
|
2381
|
+
let accountId = profile.ssoAccountId;
|
|
2382
|
+
try {
|
|
2383
|
+
rt.writeErr("Resolving account ID...\n");
|
|
2384
|
+
accountId = await fetchAccountId(tokens.accessToken);
|
|
2385
|
+
await updateConfigKey(configDir, profileName, "sso_account_id", accountId);
|
|
2386
|
+
} catch {
|
|
2387
|
+
rt.writeErr("Warning: Could not resolve account ID from /me. You can set it manually with `seclai configure sso`.\n");
|
|
2388
|
+
}
|
|
2389
|
+
rt.writeErr("Successfully authenticated!\n");
|
|
2390
|
+
const loginResult = {
|
|
2391
|
+
status: "authenticated",
|
|
2392
|
+
profile: profileName,
|
|
2393
|
+
expiresAt: tokens.expiresAt
|
|
2394
|
+
};
|
|
2395
|
+
if (accountId) loginResult.accountId = accountId;
|
|
2396
|
+
printJson(rt, loginResult);
|
|
2397
|
+
});
|
|
2398
|
+
});
|
|
2399
|
+
group.command("logout").description("Remove cached SSO tokens for the current profile.").action(async () => {
|
|
2400
|
+
await run(rt, async () => {
|
|
2401
|
+
const globalOpts = program.opts();
|
|
2402
|
+
const { profile, profileName, configDir } = await loadProfile(rt, globalOpts);
|
|
2403
|
+
await deleteSsoCache(configDir, profile);
|
|
2404
|
+
rt.writeErr("Logged out successfully.\n");
|
|
2405
|
+
printJson(rt, { status: "logged_out", profile: profileName });
|
|
2406
|
+
});
|
|
2407
|
+
});
|
|
2408
|
+
group.command("status").description("Show current authentication status for the active profile.").action(async () => {
|
|
2409
|
+
await run(rt, async () => {
|
|
2410
|
+
const globalOpts = program.opts();
|
|
2411
|
+
const { profile: profileLoaded, profileName, configDir } = await loadProfile(rt, globalOpts);
|
|
2412
|
+
const cached = await readSsoCache(configDir, profileLoaded);
|
|
2413
|
+
if (!cached) {
|
|
2414
|
+
const notAuthResult = {
|
|
2415
|
+
profile: profileName,
|
|
2416
|
+
status: "not_authenticated"
|
|
2417
|
+
};
|
|
2418
|
+
if (profileLoaded.ssoAccountId) notAuthResult.accountId = profileLoaded.ssoAccountId;
|
|
2419
|
+
printJson(rt, notAuthResult);
|
|
2420
|
+
return;
|
|
2421
|
+
}
|
|
2422
|
+
const valid = isTokenValid(cached);
|
|
2423
|
+
const statusResult = {
|
|
2424
|
+
profile: profileName,
|
|
2425
|
+
status: valid ? "authenticated" : "expired",
|
|
2426
|
+
expiresAt: cached.expiresAt,
|
|
2427
|
+
hasRefreshToken: Boolean(cached.refreshToken)
|
|
2428
|
+
};
|
|
2429
|
+
if (profileLoaded.ssoAccountId) statusResult.accountId = profileLoaded.ssoAccountId;
|
|
2430
|
+
printJson(rt, statusResult);
|
|
2431
|
+
});
|
|
2432
|
+
});
|
|
2433
|
+
group.command("refresh").description("Manually refresh the SSO token for the current profile.").action(async () => {
|
|
2434
|
+
await run(rt, async () => {
|
|
2435
|
+
const globalOpts = program.opts();
|
|
2436
|
+
const { profile, profileName, configDir } = await loadProfile(rt, globalOpts);
|
|
2437
|
+
const cached = await readSsoCache(configDir, profile);
|
|
2438
|
+
if (!cached?.refreshToken) {
|
|
2439
|
+
throw new Error("No cached token with refresh token. Run `seclai auth login` first.");
|
|
2440
|
+
}
|
|
2441
|
+
const tokenUrl = `https://${profile.ssoDomain}/oauth2/token`;
|
|
2442
|
+
const body = new URLSearchParams({
|
|
2443
|
+
grant_type: "refresh_token",
|
|
2444
|
+
client_id: profile.ssoClientId,
|
|
2445
|
+
refresh_token: cached.refreshToken
|
|
2446
|
+
});
|
|
2447
|
+
const resp = await fetch(tokenUrl, {
|
|
2448
|
+
method: "POST",
|
|
2449
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
2450
|
+
body: body.toString()
|
|
2451
|
+
});
|
|
2452
|
+
if (!resp.ok) {
|
|
2453
|
+
const text = await resp.text();
|
|
2454
|
+
throw new Error(`Token refresh failed (HTTP ${resp.status}): ${text}`);
|
|
2455
|
+
}
|
|
2456
|
+
const data = await resp.json();
|
|
2457
|
+
const refreshed = {
|
|
2458
|
+
accessToken: data.access_token,
|
|
2459
|
+
refreshToken: data.refresh_token ?? cached.refreshToken,
|
|
2460
|
+
expiresAt: new Date(Date.now() + data.expires_in * 1e3).toISOString(),
|
|
2461
|
+
clientId: profile.ssoClientId,
|
|
2462
|
+
region: profile.ssoRegion,
|
|
2463
|
+
cognitoDomain: profile.ssoDomain
|
|
2464
|
+
};
|
|
2465
|
+
if (data.id_token) refreshed.idToken = data.id_token;
|
|
2466
|
+
await writeSsoCache(configDir, profile, refreshed);
|
|
2467
|
+
rt.writeErr("Token refreshed successfully.\n");
|
|
2468
|
+
printJson(rt, {
|
|
2469
|
+
status: "refreshed",
|
|
2470
|
+
profile: profileName,
|
|
2471
|
+
expiresAt: refreshed.expiresAt
|
|
2472
|
+
});
|
|
2473
|
+
});
|
|
2474
|
+
});
|
|
2475
|
+
}
|
|
2476
|
+
|
|
2477
|
+
// src/commands/configure.ts
|
|
2478
|
+
import { mkdir as mkdir3, readFile as readFile4, writeFile as writeFile4 } from "fs/promises";
|
|
2479
|
+
import { join as join4 } from "path";
|
|
2480
|
+
import process4 from "process";
|
|
2481
|
+
import { createInterface } from "readline";
|
|
2482
|
+
import { DEFAULT_SSO_DOMAIN, DEFAULT_SSO_CLIENT_ID, DEFAULT_SSO_REGION } from "@seclai/sdk";
|
|
2483
|
+
function prompt(rt, question, defaultValue) {
|
|
2484
|
+
return new Promise((resolve) => {
|
|
2485
|
+
const rl = createInterface({
|
|
2486
|
+
input: rt.stdin,
|
|
2487
|
+
output: { write: (s) => {
|
|
2488
|
+
rt.writeErr(s);
|
|
2489
|
+
return true;
|
|
2490
|
+
} },
|
|
2491
|
+
terminal: false
|
|
2492
|
+
});
|
|
2493
|
+
const display = defaultValue ? `${question} [${defaultValue}]: ` : `${question}: `;
|
|
2494
|
+
rt.writeErr(display);
|
|
2495
|
+
rl.once("line", (answer) => {
|
|
2496
|
+
rl.close();
|
|
2497
|
+
resolve(answer.trim() || defaultValue || "");
|
|
2498
|
+
});
|
|
2499
|
+
});
|
|
2500
|
+
}
|
|
2501
|
+
function resolveConfigDir(opts) {
|
|
2502
|
+
if (opts.configDir) return opts.configDir;
|
|
2503
|
+
const env = process4.env.SECLAI_CONFIG_DIR;
|
|
2504
|
+
if (env) return env;
|
|
2505
|
+
const home = process4.env.HOME ?? process4.env.USERPROFILE ?? "";
|
|
2506
|
+
return join4(home, ".seclai");
|
|
2507
|
+
}
|
|
2508
|
+
function register17(program, rt) {
|
|
2509
|
+
const group = program.command("configure").description("Configure CLI profiles and settings.");
|
|
2510
|
+
group.command("sso").description("Configure an SSO profile with optional overrides. Defaults to production Seclai SSO.").option("--profile-name <name>", "Profile name to configure (default: from --profile flag)").action(async (opts) => {
|
|
2511
|
+
await run(rt, async () => {
|
|
2512
|
+
const globalOpts = program.opts();
|
|
2513
|
+
const profileName = opts.profileName || globalOpts.profile || "default";
|
|
2514
|
+
const configDir = resolveConfigDir(globalOpts);
|
|
2515
|
+
const configPath = join4(configDir, "config");
|
|
2516
|
+
rt.writeErr(`
|
|
2517
|
+
Configuring SSO profile "${profileName}".
|
|
2518
|
+
`);
|
|
2519
|
+
rt.writeErr(`Defaults: domain=${DEFAULT_SSO_DOMAIN}, region=${DEFAULT_SSO_REGION}
|
|
2520
|
+
`);
|
|
2521
|
+
rt.writeErr(`Press Enter to accept defaults.
|
|
2522
|
+
|
|
2523
|
+
`);
|
|
2524
|
+
const domain = await prompt(rt, "SSO domain", DEFAULT_SSO_DOMAIN);
|
|
2525
|
+
const clientId = await prompt(rt, "SSO client ID", DEFAULT_SSO_CLIENT_ID);
|
|
2526
|
+
const region = await prompt(rt, "SSO region", DEFAULT_SSO_REGION);
|
|
2527
|
+
const accountId = await prompt(rt, "Account ID (optional, resolved after login)");
|
|
2528
|
+
const isDefault = domain === DEFAULT_SSO_DOMAIN && clientId === DEFAULT_SSO_CLIENT_ID && region === DEFAULT_SSO_REGION && !accountId;
|
|
2529
|
+
if (isDefault && profileName === "default") {
|
|
2530
|
+
rt.writeErr(`
|
|
2531
|
+
Using built-in defaults \u2014 no config file needed.
|
|
2532
|
+
`);
|
|
2533
|
+
rt.writeErr(`Run \`seclai auth login\` to authenticate.
|
|
2534
|
+
`);
|
|
2535
|
+
printJson(rt, {
|
|
2536
|
+
profile: profileName,
|
|
2537
|
+
sso_domain: domain,
|
|
2538
|
+
sso_client_id: clientId,
|
|
2539
|
+
sso_region: region,
|
|
2540
|
+
note: "using built-in defaults"
|
|
2541
|
+
});
|
|
2542
|
+
return;
|
|
2543
|
+
}
|
|
2544
|
+
let content = "";
|
|
2545
|
+
try {
|
|
2546
|
+
content = await readFile4(configPath, "utf-8");
|
|
2547
|
+
} catch {
|
|
2548
|
+
}
|
|
2549
|
+
const sectionHeader = profileName === "default" ? "[default]" : `[profile ${profileName}]`;
|
|
2550
|
+
const lines = [];
|
|
2551
|
+
if (domain !== DEFAULT_SSO_DOMAIN) lines.push(`sso_domain = ${domain}`);
|
|
2552
|
+
if (clientId !== DEFAULT_SSO_CLIENT_ID) lines.push(`sso_client_id = ${clientId}`);
|
|
2553
|
+
if (region !== DEFAULT_SSO_REGION) lines.push(`sso_region = ${region}`);
|
|
2554
|
+
if (accountId) lines.push(`sso_account_id = ${accountId}`);
|
|
2555
|
+
const sectionBody = lines.join("\n");
|
|
2556
|
+
const sectionRegex = profileName === "default" ? /^\[default\][^\[]*(?=\[|$(?![\s\S]))/m : new RegExp(`^\\[profile ${escapeRegExp(profileName)}\\][^\\[]*(?=\\[|$(?![\\s\\S]))`, "m");
|
|
2557
|
+
if (sectionRegex.test(content)) {
|
|
2558
|
+
content = content.replace(sectionRegex, `${sectionHeader}
|
|
2559
|
+
${sectionBody}
|
|
2560
|
+
`);
|
|
2561
|
+
} else {
|
|
2562
|
+
if (content.length > 0 && !content.endsWith("\n")) {
|
|
2563
|
+
content += "\n";
|
|
2564
|
+
}
|
|
2565
|
+
content += `
|
|
2566
|
+
${sectionHeader}
|
|
2567
|
+
${sectionBody}
|
|
2568
|
+
`;
|
|
2569
|
+
}
|
|
2570
|
+
await mkdir3(configDir, { recursive: true });
|
|
2571
|
+
await writeFile4(configPath, content, { mode: 384 });
|
|
2572
|
+
rt.writeErr(`
|
|
2573
|
+
Profile "${profileName}" saved to ${configPath}
|
|
2574
|
+
`);
|
|
2575
|
+
rt.writeErr(`Run \`seclai auth login --profile ${profileName}\` to authenticate.
|
|
2576
|
+
`);
|
|
2577
|
+
const result = {
|
|
2578
|
+
profile: profileName,
|
|
2579
|
+
configFile: configPath,
|
|
2580
|
+
sso_domain: domain,
|
|
2581
|
+
sso_client_id: clientId,
|
|
2582
|
+
sso_region: region
|
|
2583
|
+
};
|
|
2584
|
+
if (accountId) result.sso_account_id = accountId;
|
|
2585
|
+
printJson(rt, result);
|
|
2586
|
+
});
|
|
2587
|
+
});
|
|
2588
|
+
group.command("list").description("List all configured profiles.").action(async () => {
|
|
2589
|
+
await run(rt, async () => {
|
|
2590
|
+
const globalOpts = program.opts();
|
|
2591
|
+
const configDir = resolveConfigDir(globalOpts);
|
|
2592
|
+
const configPath = join4(configDir, "config");
|
|
2593
|
+
let content;
|
|
2594
|
+
try {
|
|
2595
|
+
content = await readFile4(configPath, "utf-8");
|
|
2596
|
+
} catch {
|
|
2597
|
+
printJson(rt, { profiles: [], configFile: configPath });
|
|
2598
|
+
return;
|
|
2599
|
+
}
|
|
2600
|
+
const profiles = [];
|
|
2601
|
+
for (const line of content.split("\n")) {
|
|
2602
|
+
const trimmed = line.trim();
|
|
2603
|
+
if (trimmed.startsWith("[") && trimmed.endsWith("]")) {
|
|
2604
|
+
const raw = trimmed.slice(1, -1).trim();
|
|
2605
|
+
if (raw.startsWith("profile ")) {
|
|
2606
|
+
profiles.push(raw.slice("profile ".length).trim());
|
|
2607
|
+
} else {
|
|
2608
|
+
profiles.push(raw);
|
|
2609
|
+
}
|
|
2610
|
+
}
|
|
2611
|
+
}
|
|
2612
|
+
printJson(rt, { profiles, configFile: configPath });
|
|
2613
|
+
});
|
|
2614
|
+
});
|
|
2615
|
+
}
|
|
2616
|
+
function escapeRegExp(s) {
|
|
2617
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
2618
|
+
}
|
|
2619
|
+
|
|
1821
2620
|
// src/cli.ts
|
|
1822
2621
|
function createProgram(rt = defaultRuntime()) {
|
|
1823
2622
|
const program = new Command();
|
|
@@ -1831,6 +2630,15 @@ All commands return JSON to stdout, making it easy to pipe into jq or other tool
|
|
|
1831
2630
|
).version(cliVersion, "-V, --version", "output the version").option(
|
|
1832
2631
|
"--api-key <key>",
|
|
1833
2632
|
"Seclai API key (defaults to SECLAI_API_KEY)."
|
|
2633
|
+
).option(
|
|
2634
|
+
"--profile <name>",
|
|
2635
|
+
"SSO profile name (defaults to SECLAI_PROFILE, then 'default')."
|
|
2636
|
+
).option(
|
|
2637
|
+
"--account-id <id>",
|
|
2638
|
+
"Account ID for multi-org targeting (X-Account-Id header)."
|
|
2639
|
+
).option(
|
|
2640
|
+
"--config-dir <path>",
|
|
2641
|
+
"Config directory (defaults to SECLAI_CONFIG_DIR, then ~/.seclai)."
|
|
1834
2642
|
).option(
|
|
1835
2643
|
"--compact",
|
|
1836
2644
|
"Output compact JSON (no indentation)."
|
|
@@ -1839,16 +2647,19 @@ All commands return JSON to stdout, making it easy to pipe into jq or other tool
|
|
|
1839
2647
|
"after",
|
|
1840
2648
|
`
|
|
1841
2649
|
Environment:
|
|
1842
|
-
SECLAI_API_KEY
|
|
1843
|
-
SECLAI_API_URL
|
|
2650
|
+
SECLAI_API_KEY Default API key (alternative to --api-key)
|
|
2651
|
+
SECLAI_API_URL Override API base URL (default: https://api.seclai.com)
|
|
2652
|
+
SECLAI_PROFILE Default SSO profile (alternative to --profile)
|
|
2653
|
+
SECLAI_CONFIG_DIR Config directory (alternative to --config-dir)
|
|
1844
2654
|
|
|
1845
2655
|
Examples:
|
|
1846
2656
|
seclai agents list
|
|
1847
2657
|
seclai agents run <agentId> --json '{"input":"Hello"}'
|
|
1848
2658
|
seclai agents run <agentId> --json '{"input":"Hi"}' --events
|
|
1849
|
-
seclai
|
|
1850
|
-
seclai
|
|
1851
|
-
seclai
|
|
2659
|
+
seclai configure sso
|
|
2660
|
+
seclai auth login
|
|
2661
|
+
seclai auth status
|
|
2662
|
+
seclai sources list --profile dev
|
|
1852
2663
|
npx @seclai/cli agents list
|
|
1853
2664
|
`
|
|
1854
2665
|
);
|
|
@@ -1875,6 +2686,9 @@ Examples:
|
|
|
1875
2686
|
register12(program, rt);
|
|
1876
2687
|
register13(program, rt);
|
|
1877
2688
|
register14(program, rt);
|
|
2689
|
+
register15(program, rt);
|
|
2690
|
+
register16(program, rt);
|
|
2691
|
+
register17(program, rt);
|
|
1878
2692
|
return program;
|
|
1879
2693
|
}
|
|
1880
2694
|
async function runCli(argv, rt = defaultRuntime()) {
|