@follenfang/fupload 0.0.23 → 0.0.24

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/fupload/SKILL.md CHANGED
@@ -2,7 +2,7 @@
2
2
  name: fupload
3
3
  description: Explicit author-publishing workflow for World of Warcraft plugins, configuration shares, and WA/strings on NewBeeBox, NetEase DD, CurseForge, Heybox Workshop, and ModUs.Creator, including local Creator login reuse and plugin ZIP publishing. Use only when the user explicitly invokes `$fupload`, explicitly asks to use the Fupload Skill, or loads this Skill by path. Do not trigger from ordinary mentions of publishing, NewBeeBox, DD, CurseForge, Heybox, ModUs, plugins, configurations, or WA.
4
4
  metadata:
5
- version: "0.0.23"
5
+ version: "0.0.24"
6
6
  ---
7
7
 
8
8
  # Fupload
@@ -22,7 +22,7 @@ Map natural language consistently:
22
22
 
23
23
  - `create`: create the main record. If first content is a separate platform action, plan it as a following atomic step.
24
24
  - `update`: publish a plugin version, change selected configuration-backup content, or publish a WA/string version.
25
- - `edit`: change fields allowed by the target platform's action allowlist. For DD plugins, this includes `description`, `html_desc`, and existing-record commercial, association, room/channel, and creation-statement settings; other first-publication metadata, categories, media, and version fields are create/update-only. Description edits read both detail and author-list projections; either may confirm the value, but if neither matches, report verification-required.
25
+ - `edit`: change fields allowed by the target platform's action allowlist. For DD plugins, this includes `description`, `html_desc`, `update_desc`, and existing-record commercial, association, room/channel, and creation-statement settings; other first-publication metadata, categories, media, and version fields are create/update-only. Announcement edits preserve the published version and package; this edits the current plugin announcement, not an arbitrary historical version. Edits read both detail and author-list projections; either may confirm the value, but if neither matches, report verification-required.
26
26
  - `delete`: delete one main record only. It never deletes an individual version, media object, or associated record.
27
27
 
28
28
  Never offer bulk delete, drafts, guides, messages, or GUI control automation. Heybox supports individual version deletion with asynchronous readback; whole-module deletion is not supported. The DD task-session command may close verified official GUI processes only after the consent flow below.
@@ -73,7 +73,7 @@ A published version's `update_desc` cannot be corrected through the CLI: `plugin
73
73
 
74
74
  Every path in `detail_img_files` must exist as a regular local file. Validation reports the exact indexed path, such as `$.detail_img_files[0]`, before any upload or mutation.
75
75
 
76
- `game_type` and the outer free/paid state are create-only. `plugin update` requires `sn`, `game_versions`, `version`, and `update_desc`; optional `file`, `detail_url`, and `release_type` publish a new version. `plugin edit` requires `sn` and accepts `description`, `html_desc`, plus the official existing-record commercial and association controls (`scope`, payment/lifetime/VIP fields, room/channel linkage, `creation_statement`, and associated content). Other first-publication metadata (`addon_type`, `name`, logo, detail images, and categories) and version fields are not edit fields. The CLI verifies description edits through both detail and author-list readback; if DD accepts the request but leaves the remote value unchanged, it reports a verification-required error rather than false success.
76
+ `game_type` and the outer free/paid state are create-only. `plugin update` requires `sn`, `game_versions`, `version`, and `update_desc`; optional `file`, `detail_url`, and `release_type` publish a new version. `plugin edit` requires `sn` and accepts `description`, `html_desc`, `update_desc`, plus the official existing-record commercial and association controls (`scope`, payment/lifetime/VIP fields, room/channel linkage, `creation_statement`, and associated content). Other first-publication metadata (`addon_type`, `name`, logo, detail images, and categories) and package/version fields are not edit fields. To repair the current announcement, supply only `sn` and HTML `update_desc` (at most 1000 characters including tags) to `plugin edit`; the existing published version and package URL are preserved. This does not edit arbitrary rows in version history. The CLI verifies description edits through both detail and author-list readback; if DD accepts the request but leaves the remote value unchanged, it reports a verification-required error rather than false success.
77
77
 
78
78
  When `assign_user_sn` is present, the official UI exposes only the public scope option. Python validates the final rebuilt form and rejects a private scope before upload or mutation.
79
79
 
@@ -131,12 +131,12 @@ PUT are separate stages.
131
131
  | `file_id` | `fileId` | positive integer | update/edit/delete required; upload allocates | `GET /game/data/author/project/fileId/{projectId}` | allocated ID is used by signature; create metadata omits it | update/delete/signature | `id`/`fileId` in release detail |
132
132
  | `version` | `version` | non-empty string, max 120 | create/upload required after ZIP preflight | ZIP `.toc` plus caller label | caller value preserved | metadata create/update | `version` |
133
133
  | `type` | `type` | non-empty string, max 40 | create/upload required | Creator release-type choices/API | must be a server-supported value | metadata create/update | `type` |
134
- | `supported_game_versions` | `supportedGameVersionsReqs[]` | non-empty object array | required; ZIP-derived | installed `.toc` `Interface` values and game config | each entry requires string `gameVersion` and `server`; explicit caller value must match ZIP | metadata create/update | `supportedGameVersionsReqs`/version compatibility rows |
134
+ | `supported_game_versions` | `supportedGameVersionsReqs[]` | non-empty object array | required; ZIP-derived | installed `.toc` `Interface` values and game config | each entry requires string `gameVersion` and `server`; explicit caller value must be a nonempty subset of ZIP and live `wow_builds` choices | metadata create/update | `supportedGameVersionsReqs`/version compatibility rows |
135
135
  | `md5` | `md5` | hex string, max 64 | derived from exact ZIP bytes | local ZIP preflight | must describe bytes later PUT | metadata create/update | `md5` |
136
136
  | `zip_size` | `zipSize` | non-negative integer | derived | local ZIP stat | must describe exact ZIP | metadata create/update | `zipSize` |
137
137
  | `unzip_size` | `unzipSize` | non-negative integer | derived | ZIP central directory | bounded by ZIP preflight | metadata create/update | `unzipSize` |
138
138
  | `path` | `path` | string, max 500 | metadata optional | Creator/object response | local path is never binary upload | metadata create/update | `path` |
139
- | `toc_version` | `tocVersion` | string, max 80 | derived | each addon `.toc` `Interface`; TOCs under a `Libs` directory are ignored | incompatible/missing/mixed addon values fail before write | metadata create/update | `tocVersion` |
139
+ | `toc_version` | `tocVersion` | string, max 80 | derived | each addon `.toc` `Interface`; TOCs under a `Libs` directory are ignored | recognized flavor TOCs form an addon union; missing values and conflicting independent addons fail before write | metadata create/update | `tocVersion` |
140
140
  | `changelog` | `changelog` | string/null, max 10000 | optional | free text | omitted/empty behavior is action-specific | metadata create/update | `changelog` |
141
141
  | `file` | not in JSON; raw bytes | local path | create/upload required | local filesystem | valid ZIP, max 200 MiB; bytes PUT only after metadata/signature | signed URL from `GET .../upload/signature/{projectId}/{fileId}`, then HTTP `PUT` | object upload status plus release detail hashes/sizes |
142
142
  | `transaction_log` | not sent | local path string | optional | caller | redacted durable stage record | local record only | transaction JSON |
@@ -149,7 +149,7 @@ before file-ID allocation and ZIP preflight, so those early failures are also
149
149
  durable. Binary-upload errors expose only a query-free endpoint identifier,
150
150
  status when available, and a bounded redacted response summary. The provider parses each addon `.toc`
151
151
  `Interface` value to derive
152
- `toc_version` and `supported_game_versions`, and ignores TOCs under a `Libs` directory. Explicit caller values must match.
152
+ `toc_version` and `supported_game_versions`, and ignores TOCs under a `Libs` directory. Explicit `toc_version` must match. Known clients absent from live `wow_builds` are excluded from automatic selection; unknown Interface codes still fail. Explicit `supported_game_versions` may narrow selection to a product's supported scope, but must be a nonempty subset of both ZIP and live choices. Creator `plugin get/versions` uses the Creator session; main-client addon reads retain their separate session.
153
153
  The provider validates the ZIP, computes `md5`, `zip_size`, and `unzip_size`,
154
154
  registers metadata, obtains a signed URL, then uploads the ZIP bytes with HTTP
155
155
  `PUT` and `Content-Type: application/zip`. A local path is metadata only; it is
@@ -1,3 +1,3 @@
1
1
  """Fupload Python CLI."""
2
2
 
3
- __version__ = "0.0.23"
3
+ __version__ = "0.0.24"
@@ -400,7 +400,7 @@ def main(argv: Optional[Sequence[str]] = None) -> int:
400
400
  write_output(platform, operation, data)
401
401
  return 0
402
402
  modus_doctor = platform == "modus" and resource == "session" and action == "doctor"
403
- modus_main_session = resource in ("addon", "plugin", "config", "wa", "builds") or (
403
+ modus_main_session = resource in ("addon", "config", "wa", "builds") or (
404
404
  modus_doctor and getattr(args, "client", "creator") == "main"
405
405
  )
406
406
  provider = NewBee() if platform == "newbee" else (DD() if platform == "dd" else (Blackbox() if platform == "blackbox" else (CurseForge() if platform == "curseforge" else _modus_provider(authenticate=not modus_doctor, main_session=modus_main_session))))
@@ -1469,7 +1469,7 @@ PLUGIN_CREATE_DEFAULTS = {
1469
1469
  # exceptions and are verified by detail and author-list readback after every
1470
1470
  # edit. Other first-publication metadata remains create-only.
1471
1471
  PLUGIN_EDIT_FIELDS = (
1472
- "description", "html_desc",
1472
+ "description", "html_desc", "update_desc",
1473
1473
  "scope", "share_code_life_type", "need_buy", "price_fen", "buy_life_type",
1474
1474
  "jump_room", "room_id", "channel_id", "channel_type", "sync_room",
1475
1475
  "creation_statement", "with_associate", "associated_acts", "need_anchor_vip",
@@ -24,7 +24,7 @@ from typing import Any, Dict, Mapping, Optional
24
24
 
25
25
  from .errors import FuploadError, ValidationError, redact
26
26
  from .state_machine import COMPLETE, ProjectStateMachine
27
- from .modus_zip import parse_modus_zip
27
+ from .modus_zip import parse_modus_zip, select_game_versions
28
28
  from .transport import json_request, multipart_request
29
29
 
30
30
 
@@ -1289,14 +1289,17 @@ class ModUs:
1289
1289
  supplied_games = doc.get("supported_game_versions")
1290
1290
  if supplied_toc is not None and str(supplied_toc) != derived["toc_version"]:
1291
1291
  raise ValidationError("toc_version does not match the ZIP Interface field", path="$.toc_version")
1292
- if supplied_games is not None and _supported_game_versions(supplied_games) != derived["supported_game_versions"]:
1293
- raise ValidationError("supported_game_versions does not match the ZIP Interface field", path="$.supported_game_versions")
1292
+ selected_games = select_game_versions(
1293
+ derived["supported_game_versions"],
1294
+ self.options("game-versions", keys=["wow_builds"]),
1295
+ _supported_game_versions(supplied_games) if supplied_games is not None else None,
1296
+ )
1294
1297
  metadata.update({
1295
1298
  "md5": md5,
1296
1299
  "zip_size": size,
1297
1300
  "unzip_size": unzip_size,
1298
1301
  "toc_version": derived["toc_version"],
1299
- "supported_game_versions": derived["supported_game_versions"],
1302
+ "supported_game_versions": selected_games,
1300
1303
  "path": doc.get("path") or archive.name,
1301
1304
  })
1302
1305
  current_stage = "release_metadata_update" if update else "release_metadata"
@@ -14,6 +14,7 @@ of being silently classified.
14
14
  from __future__ import annotations
15
15
 
16
16
  import io
17
+ import json
17
18
  import os
18
19
  import re
19
20
  import zipfile
@@ -38,9 +39,17 @@ INTERFACE_GAME_VERSION_MAP: Mapping[str, Mapping[str, str]] = {
38
39
  "11507": {"gameVersion": "Classic Era", "server": "wow_classic_era"},
39
40
  "11508": {"gameVersion": "Classic Era", "server": "wow_classic_era"},
40
41
  "40401": {"gameVersion": "Cataclysm Classic", "server": "wow_classic_cata"},
41
- "40402": {"gameVersion": "Cataclysm Classic", "server": "wow_classic_cata"},
42
+ "11509": {"gameVersion": "1.15.9", "server": "wow_classic_era"},
43
+ "20506": {"gameVersion": "2.5.6", "server": "wow_anniversary"},
44
+ "38002": {"gameVersion": "3.80.2", "server": "wow_classic_titan"},
45
+ "40402": {"gameVersion": "4.4.2", "server": "wow_classic_cata"},
46
+ "50504": {"gameVersion": "5.5.4", "server": "wow_classic"},
42
47
  }
43
48
 
49
+ # Recognized WoW clients in universal ZIPs which currently have no Creator
50
+ # server mapping. Preserve their Interface codes, without inventing wire keys.
51
+ KNOWN_UNMAPPED_INTERFACES = {"16001", "30405"}
52
+
44
53
  _INTERFACE_RE = re.compile(r"^\s*##\s*Interface\s*:\s*(.*?)\s*$", re.IGNORECASE)
45
54
  _INTERFACE_VALUE_RE = re.compile(r"^\d+$")
46
55
  _SOURCE = Union[str, os.PathLike[str], bytes, bytearray, memoryview, BinaryIO]
@@ -138,7 +147,8 @@ def _zip_entries(raw: bytes, source_name: str) -> Iterable[Tuple[str, bytes]]:
138
147
  def parse_modus_zip(source: _SOURCE) -> Dict[str, Any]:
139
148
  """Return ModUs metadata inferred from ``source``.
140
149
 
141
- Addon ``.toc`` files must declare the same Interface set. This avoids
150
+ Each addon's base and recognized flavor TOCs form one Interface union.
151
+ Independent addons must declare the same union. This avoids
142
152
  choosing an arbitrary addon when a multi-addon archive contains
143
153
  incompatible game versions. TOCs under a ``Libs`` directory are ignored.
144
154
  Multiple Interface values in one TOC are supported and become a
@@ -160,13 +170,20 @@ def parse_modus_zip(source: _SOURCE) -> Dict[str, Any]:
160
170
  for name, toc_raw in addon_entries:
161
171
  values = tuple(_interface_values(_decode_toc(toc_raw, name), name))
162
172
  signatures.append((name, values))
163
- expected = signatures[0][1]
164
- mismatches = [name for name, values in signatures[1:] if values != expected]
173
+ groups: Dict[str, set[str]] = {}
174
+ for name, values in signatures:
175
+ normalized = name.replace("\\", "/").casefold()
176
+ addon = re.sub(r"_(?:mainline|vanilla|classic|bcc|tbc|wrath|cata|mists)\.toc$", ".toc", normalized)
177
+ groups.setdefault(addon, set()).update(values)
178
+ first = next(iter(groups))
179
+ expected = tuple(sorted(groups[first], key=lambda item: (int(item), item)))
180
+ mismatches = [name for name, values in groups.items() if values != set(expected)]
165
181
  if mismatches:
166
- names = ", ".join([signatures[0][0], *mismatches])
182
+ names = ", ".join([first, *mismatches])
167
183
  raise ValidationError("addon TOC Interface values are ambiguous across files: %s" % names, path="$.file")
168
184
 
169
- unknown = [value for value in expected if value not in INTERFACE_GAME_VERSION_MAP]
185
+ unknown = [value for value in expected
186
+ if value not in INTERFACE_GAME_VERSION_MAP and value not in KNOWN_UNMAPPED_INTERFACES]
170
187
  if unknown:
171
188
  raise ValidationError(
172
189
  "unsupported addon TOC Interface value(s): %s" % ", ".join(unknown),
@@ -175,6 +192,8 @@ def parse_modus_zip(source: _SOURCE) -> Dict[str, Any]:
175
192
 
176
193
  games: List[Dict[str, str]] = []
177
194
  for interface in expected:
195
+ if interface in KNOWN_UNMAPPED_INTERFACES:
196
+ continue
178
197
  candidate = dict(INTERFACE_GAME_VERSION_MAP[interface])
179
198
  if candidate not in games:
180
199
  games.append(candidate)
@@ -183,6 +202,7 @@ def parse_modus_zip(source: _SOURCE) -> Dict[str, Any]:
183
202
  "supported_game_versions": games,
184
203
  "interface_values": list(expected),
185
204
  "toc_files": [name for name, _ in signatures],
205
+ "unmapped_interface_values": [value for value in expected if value in KNOWN_UNMAPPED_INTERFACES],
186
206
  }
187
207
 
188
208
 
@@ -191,4 +211,29 @@ def parse_modus_zip(source: _SOURCE) -> Dict[str, Any]:
191
211
  parse_zip_metadata = parse_modus_zip
192
212
 
193
213
 
194
- __all__ = ["INTERFACE_GAME_VERSION_MAP", "parse_modus_zip", "parse_zip_metadata"]
214
+ def select_game_versions(derived: Sequence[Mapping[str, str]], config: Any,
215
+ supplied: Any = None) -> List[Dict[str, str]]:
216
+ """Intersect known ZIP clients with live Creator choices and caller scope."""
217
+ rows = config if isinstance(config, list) else []
218
+ row = next((item for item in rows if isinstance(item, Mapping) and item.get("key") == "wow_builds"), None)
219
+ try:
220
+ builds = json.loads(row["value"]) if row and isinstance(row.get("value"), str) else None
221
+ except (ValueError, TypeError):
222
+ builds = None
223
+ if not isinstance(builds, Mapping) or not builds or any(
224
+ not isinstance(value, Mapping) or not isinstance(value.get("versions"), list)
225
+ or any(not isinstance(version, str) or not version.strip() for version in value["versions"])
226
+ for value in builds.values()
227
+ ):
228
+ raise ValidationError("live wow_builds config is missing or malformed", path="$.supported_game_versions")
229
+ available = [dict(item) for item in derived
230
+ if item["server"] in builds and item["gameVersion"] in builds[item["server"]]["versions"]]
231
+ selected = available if supplied is None else supplied
232
+ if not isinstance(selected, list) or not selected or any(item not in available for item in selected):
233
+ raise ValidationError("supported_game_versions must be a nonempty subset of ZIP and live Creator choices", path="$.supported_game_versions")
234
+ if len({(item["server"], item["gameVersion"]) for item in selected}) != len(selected):
235
+ raise ValidationError("supported_game_versions must not contain duplicates", path="$.supported_game_versions")
236
+ return [dict(item) for item in selected]
237
+
238
+
239
+ __all__ = ["INTERFACE_GAME_VERSION_MAP", "parse_modus_zip", "parse_zip_metadata", "select_game_versions"]
@@ -591,6 +591,9 @@ DD_PLUGIN_VERSION = {
591
591
  "version": f("string", nonempty=True, max_length=80),
592
592
  "update_desc": f("string", nonempty=True, max_length=1000),
593
593
  }
594
+ # The official existing-plugin editor includes the update announcement in
595
+ # its /addon/modify payload. Editing it does not require a new ZIP/version.
596
+ DD_PLUGIN_EDIT_META["update_desc"] = DD_PLUGIN_VERSION["update_desc"]
594
597
 
595
598
  DD_CONFIG_META = {
596
599
  "scope": DD_COMMERCIAL["scope"], "title": f("string", nonempty=True, max_length=40),
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "schema": "fupload.npm-skill-manifest.v1",
3
3
  "package_name": "@follenfang/fupload",
4
- "package_version": "0.0.23",
5
- "skill_version": "0.0.23",
6
- "tree_sha256": "92ada2b43090f9d3d44c90da840fba1ed420e79ef8166275dc790c5a200d53db",
4
+ "package_version": "0.0.24",
5
+ "skill_version": "0.0.24",
6
+ "tree_sha256": "53bc8cdcaab317ddace896026d8668614dfaeb19f421cf0195fe4846f6e9933c",
7
7
  "files": [
8
8
  {
9
9
  "path": "agents/openai.yaml",
@@ -87,13 +87,13 @@
87
87
  },
88
88
  {
89
89
  "path": "references/dd.md",
90
- "bytes": 20837,
91
- "sha256": "4a174fa9470a23aee5c607b85123575c157559570a2d3fd4caa9a0874f9ae934"
90
+ "bytes": 21111,
91
+ "sha256": "18895d95e5221b3c2243922bbfc6f2e826f9084a0d5d11c37c3eb923a9dd7547"
92
92
  },
93
93
  {
94
94
  "path": "references/modus.md",
95
- "bytes": 37236,
96
- "sha256": "396bf899cef28dfdd66558a555c37bc9af7afaa444f96b2b5b5b1f9b68508783"
95
+ "bytes": 37716,
96
+ "sha256": "f462731d05d786cdfd290eceaea9104b0f28814c9a5bd793358e4dc1a7abcdce"
97
97
  },
98
98
  {
99
99
  "path": "references/newbee-official-cli.md",
@@ -113,7 +113,7 @@
113
113
  {
114
114
  "path": "scripts/fupload_cli/__init__.py",
115
115
  "bytes": 50,
116
- "sha256": "36c61f2ff8e0593fc5d731f120d8406354634b1ae61805d20c2d73da59ced3d0"
116
+ "sha256": "8903ed57cd646124d87b730c4c5923b97cacd906cef920de53c6b91acfb554f4"
117
117
  },
118
118
  {
119
119
  "path": "scripts/fupload_cli/blackbox_web.py",
@@ -127,8 +127,8 @@
127
127
  },
128
128
  {
129
129
  "path": "scripts/fupload_cli/cli.py",
130
- "bytes": 37420,
131
- "sha256": "3a052ec53c29ca95eafc9ba845d448ef45e4bd0231bf41ec1906e55281aa1650"
130
+ "bytes": 37410,
131
+ "sha256": "97ce8e53f916f619a16351c636d90b9ddf4a64ebb301a2cf28da81d65522863d"
132
132
  },
133
133
  {
134
134
  "path": "scripts/fupload_cli/curseforge.py",
@@ -147,8 +147,8 @@
147
147
  },
148
148
  {
149
149
  "path": "scripts/fupload_cli/dd.py",
150
- "bytes": 117372,
151
- "sha256": "19a303e84ce08dd3af9a905d04d24f7224c3302f83f0d62f899618cf45fe0433"
150
+ "bytes": 117387,
151
+ "sha256": "01051273f0e03010c88590305583f404de9588e60fc708999222aa80263ebb31"
152
152
  },
153
153
  {
154
154
  "path": "scripts/fupload_cli/errors.py",
@@ -162,13 +162,13 @@
162
162
  },
163
163
  {
164
164
  "path": "scripts/fupload_cli/modus_zip.py",
165
- "bytes": 8590,
166
- "sha256": "6c3949d59356e3e28aa15458eb09019f64fdb0d383af26e7bb0c4c262b857559"
165
+ "bytes": 11445,
166
+ "sha256": "ddf603cb24d5e9758be0428446f134e02aa1a44a8be39f697b8985ac34463e2f"
167
167
  },
168
168
  {
169
169
  "path": "scripts/fupload_cli/modus.py",
170
- "bytes": 74134,
171
- "sha256": "9841ce7188083b62c1c217d89436b3c3ddc3ecf726719d3c44c0b83027172fac"
170
+ "bytes": 74167,
171
+ "sha256": "f4ac5820476593f3db0a621faac61cbed319b0a046fb35945f9a684f292d0e5f"
172
172
  },
173
173
  {
174
174
  "path": "scripts/fupload_cli/newbee_auth.py",
@@ -182,8 +182,8 @@
182
182
  },
183
183
  {
184
184
  "path": "scripts/fupload_cli/schema.py",
185
- "bytes": 54911,
186
- "sha256": "bede6cc93de8a9d766e463cab0acb8725745da781aebabba73744394b594b93c"
185
+ "bytes": 55131,
186
+ "sha256": "b651963c504896cfef46f65b61d5fb638884aacb14a8e09593162526e239133f"
187
187
  },
188
188
  {
189
189
  "path": "scripts/fupload_cli/state_machine.py",
@@ -207,8 +207,8 @@
207
207
  },
208
208
  {
209
209
  "path": "SKILL.md",
210
- "bytes": 28582,
211
- "sha256": "f150b547db8ce091c1d1e5493026f2c30fce8c9458042a76df33844bac214972"
210
+ "bytes": 28729,
211
+ "sha256": "9625fb102b1fcc347978d0b9fe73a51703ec8296d344550938e8a2dbe593565f"
212
212
  }
213
213
  ]
214
214
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@follenfang/fupload",
3
- "version": "0.0.23",
3
+ "version": "0.0.24",
4
4
  "description": "Install and run the Fuploader Agent Skill and Python CLI.",
5
5
  "type": "module",
6
6
  "bin": {