@arcadiasystems/morse-cli 0.2.0 → 0.3.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/CHANGELOG.md CHANGED
@@ -4,6 +4,35 @@ All notable changes to `@arcadiasystems/morse-cli` are documented here. The
4
4
  format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and this
5
5
  project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
+ ## [0.3.0] - 2026-06-04
8
+
9
+ Event-based file listing, wrapping the SDK 0.3.0 reconcile helpers. Purely
10
+ additive; all existing commands are unchanged.
11
+
12
+ ### Added
13
+
14
+ - `morse file list`: list files owned by an address (default: the active
15
+ account), or, with `--accessible`, files decryptable via allowlist membership.
16
+ `--address <addr>` queries another address, `--hydrate` fetches the full record
17
+ per file (adds `blobId`; one read each), `--limit <n>` caps results, `--json`
18
+ emits the summary array.
19
+ - Event fetching via `cli/events.ts`: a paginator over `suix_queryEvents` feeding
20
+ the SDK's pure `reconcileFilesOwnedBy` / `reconcileFilesAccessibleBy` helpers.
21
+ `--indexer-url <url>` overrides the event source (any endpoint that speaks
22
+ `suix_queryEvents`).
23
+
24
+ ### Changed
25
+
26
+ - Depends on `@arcadiasystems/morse-sdk` `^0.3.0`.
27
+
28
+ ### Notes
29
+
30
+ - Listing reads `suix_queryEvents`, a deprecated Sui JSON-RPC endpoint Mysten is
31
+ sunsetting; on the public RPC it may degrade over time. Use `--indexer-url` to
32
+ point at your own indexer. Results are best-effort and eventually consistent.
33
+ - Summaries omit `blobId`/`blobObjectId` (not in the `FileCreated` event); use
34
+ `--hydrate` to fetch them.
35
+
7
36
  ## [0.2.0] - 2026-06-04
8
37
 
9
38
  Wraps the allowlist + encrypted-file surface from `@arcadiasystems/morse-sdk`
package/README.md CHANGED
@@ -6,7 +6,7 @@ content entries from your terminal, signing with a locally encrypted key.
6
6
  Content is stored on [Walrus](https://walrus.xyz); private entries are encrypted
7
7
  with [Seal](https://github.com/MystenLabs/seal).
8
8
 
9
- > Status: v0.2.0, targeting Sui testnet. The command surface is stable; mainnet
9
+ > Status: v0.3.0, targeting Sui testnet. The command surface is stable; mainnet
10
10
  > support lands when the contracts are frozen.
11
11
 
12
12
  ## Requirements
@@ -250,6 +250,7 @@ auto-resolved from the active account when `--cap` is omitted.
250
250
  | `file upload <path> --name <n> [-a <id>] [--public] [--content-type <m>] [--epochs <n>]` | Upload to Walrus and register; `-a` encrypts, `--public` is world-readable. |
251
251
  | `file register --blob-id <id> --name <n> --content-type <m> --size <bytes> [-a <id>] [--public] [--blob-object-id <id>]` | Register metadata for a blob already on Walrus. |
252
252
  | `file download <file> [--out <path>] [--seal-id <hex>] [--via-aggregator]` | Download content; decrypts in place when encrypted. |
253
+ | `file list [--address <addr>] [--accessible] [--hydrate] [--limit <n>] [--indexer-url <url>]` | List files owned by (or, with `--accessible`, decryptable by) an address. |
253
254
  | `file get <file>` | Fetch a file's on-chain metadata. |
254
255
  | `file update <file> --name <n> --content-type <m>` | Update name and MIME (owner only). |
255
256
  | `file transfer-ownership <file> <newOwner> [-y]` | Transfer the metadata right (not decrypt access). |
@@ -257,9 +258,18 @@ auto-resolved from the active account when `--cap` is omitted.
257
258
 
258
259
  Encrypting a file (`file upload -a <allowlist>`) prints a **seal id**. It is not
259
260
  recoverable from the ciphertext, so save it: `file download` needs it (via
260
- `--seal-id`) plus allowlist membership to decrypt. Listing "files I can decrypt
261
- as a member" is not supported on-chain (encrypted files are shared objects with
262
- no owner index); track file ids yourself or index the contract events.
261
+ `--seal-id`) plus allowlist membership to decrypt.
262
+
263
+ `file list` reconstructs the file set from contract events. Encrypted files are
264
+ shared objects with no on-chain owner index, so listing is event-derived, not a
265
+ direct query. By default the command reads events via `suix_queryEvents` on the
266
+ configured Sui RPC. That endpoint is **deprecated** (Mysten is sunsetting it), so
267
+ listing may degrade or stop working on the public RPC over time; point
268
+ `--indexer-url <url>` at any source that speaks `suix_queryEvents` (a self-hosted
269
+ indexer, a third-party endpoint) to stay in control. Results are best-effort and
270
+ eventually consistent (subject to indexer lag and retention). `EncryptedFileSummary`
271
+ rows omit `blobId`/`blobObjectId`; add `--hydrate` to fetch the full record per
272
+ file (one read each) when you need them.
263
273
 
264
274
  ## Output and scripting
265
275
 
package/dist/index.js CHANGED
@@ -148,7 +148,7 @@ import { Command } from "commander";
148
148
  // package.json
149
149
  var package_default = {
150
150
  name: "@arcadiasystems/morse-cli",
151
- version: "0.2.0",
151
+ version: "0.3.0",
152
152
  description: "Command-line interface for the Morse decentralized CMS on Sui.",
153
153
  license: "MIT",
154
154
  type: "module",
@@ -203,7 +203,7 @@ var package_default = {
203
203
  prepublishOnly: "bun run check && bun run build"
204
204
  },
205
205
  dependencies: {
206
- "@arcadiasystems/morse-sdk": "^0.2.0",
206
+ "@arcadiasystems/morse-sdk": "^0.3.0",
207
207
  "@mysten/seal": "1.1.3",
208
208
  "@mysten/sui": "2.16.2",
209
209
  "@mysten/walrus": "1.1.6",
@@ -902,6 +902,7 @@ import {
902
902
 
903
903
  // src/cli/context.ts
904
904
  import {
905
+ buildFilesEventTypes,
905
906
  DefaultSealAdapter,
906
907
  DefaultWalrusReadAdapter,
907
908
  DefaultWalrusWriteAdapter,
@@ -912,6 +913,7 @@ import {
912
913
  RpcPublicationReader
913
914
  } from "@arcadiasystems/morse-sdk";
914
915
  import { SuiGrpcClient } from "@mysten/sui/grpc";
916
+ import { SuiJsonRpcClient } from "@mysten/sui/jsonRpc";
915
917
  async function buildReadContext(command) {
916
918
  const opts = globalOptions(command);
917
919
  const output = outputFor(command);
@@ -1026,6 +1028,23 @@ async function buildFileDownloadContext(command, opts = {}) {
1026
1028
  unlockSigner: () => resolveSigner(base.settings.account, process.env, base.signal)
1027
1029
  };
1028
1030
  }
1031
+ async function buildFileListContext(command, opts = {}) {
1032
+ const base = await buildReadContext(command);
1033
+ const originPackageId = base.config.filesEventOriginPackageId;
1034
+ if (originPackageId === undefined) {
1035
+ throw new CliError("File listing is unavailable on this network: no filesEventOriginPackageId in the config. Use testnet, or supply a config that sets it.", ExitCode.Usage);
1036
+ }
1037
+ const events = new SuiJsonRpcClient({
1038
+ network: base.settings.network,
1039
+ url: opts.indexerUrl ?? base.config.rpcUrl
1040
+ });
1041
+ return {
1042
+ ...base,
1043
+ filesReader: RpcFilesReader.fromMorseConfig(base.config, base.client),
1044
+ events,
1045
+ eventTypes: buildFilesEventTypes(originPackageId)
1046
+ };
1047
+ }
1029
1048
 
1030
1049
  // src/format/ids.ts
1031
1050
  var HEAD = 6;
@@ -1111,6 +1130,20 @@ function renderAllowlistCapList(caps) {
1111
1130
  return caps.map((cap) => `${cap.id} allowlist ${cap.allowlistId}`).join(`
1112
1131
  `);
1113
1132
  }
1133
+ function renderFileList(items) {
1134
+ if (items.length === 0) {
1135
+ return "No files.";
1136
+ }
1137
+ const header = "id name size encrypted allowlist created";
1138
+ const rows = items.map((file) => {
1139
+ const allowlist = file.allowlistId === null ? "public" : shortId(file.allowlistId);
1140
+ const created = new Date(file.createdAtMs).toISOString().slice(0, 10);
1141
+ const encrypted = file.encrypted ? "yes" : "no";
1142
+ return `${shortId(file.id)} ${file.name} ${file.size} ${encrypted} ${allowlist} ${created}`;
1143
+ });
1144
+ return [header, ...rows].join(`
1145
+ `);
1146
+ }
1114
1147
  function renderEncryptedFile(file) {
1115
1148
  const lines = [
1116
1149
  `${file.name} (${shortId(file.id)})`,
@@ -1997,6 +2030,8 @@ import {
1997
2030
  createEncryptedFile,
1998
2031
  createPublicFile,
1999
2032
  deleteFile,
2033
+ reconcileFilesAccessibleBy,
2034
+ reconcileFilesOwnedBy,
2000
2035
  toAllowlistId as toAllowlistId2,
2001
2036
  toBlobObjectId,
2002
2037
  toEncryptedFileId,
@@ -2009,6 +2044,38 @@ import {
2009
2044
  } from "@arcadiasystems/morse-sdk";
2010
2045
  import { SessionKey as SessionKey2 } from "@mysten/seal";
2011
2046
 
2047
+ // src/cli/events.ts
2048
+ var PAGE_LIMIT2 = 50;
2049
+ async function fetchEventStream(client, eventType, signal) {
2050
+ const out = [];
2051
+ let cursor;
2052
+ for (;; ) {
2053
+ const page = await client.queryEvents({
2054
+ query: { MoveEventType: eventType },
2055
+ limit: PAGE_LIMIT2,
2056
+ order: "descending",
2057
+ ...cursor === undefined ? {} : { cursor },
2058
+ ...signal === undefined ? {} : { signal }
2059
+ });
2060
+ for (const event of page.data) {
2061
+ out.push({
2062
+ type: event.type,
2063
+ parsedJson: event.parsedJson,
2064
+ timestampMs: event.timestampMs
2065
+ });
2066
+ }
2067
+ if (!page.hasNextPage || page.nextCursor === null || page.nextCursor === undefined) {
2068
+ break;
2069
+ }
2070
+ cursor = page.nextCursor;
2071
+ }
2072
+ return out;
2073
+ }
2074
+ async function fetchEventStreams(client, eventTypes, signal) {
2075
+ const streams = await Promise.all(eventTypes.map((type) => fetchEventStream(client, type, signal)));
2076
+ return streams.flat();
2077
+ }
2078
+
2012
2079
  // src/format/hex.ts
2013
2080
  var HEX = /^[0-9a-fA-F]+$/;
2014
2081
  function encodeHex(bytes) {
@@ -2216,12 +2283,53 @@ async function runFileDownload(ctx, target, options) {
2216
2283
  }
2217
2284
  process.stdout.write(content);
2218
2285
  }
2286
+ async function runFileList(ctx, options) {
2287
+ const limit = options.limit === undefined ? undefined : parseLimit(options.limit);
2288
+ const address = options.address === undefined ? ctx.ownerAddress : toSuiAddress5(options.address);
2289
+ if (address === undefined) {
2290
+ throw new UsageError("No address given and no active account. Pass --address or import an account.");
2291
+ }
2292
+ const types = ctx.eventTypes;
2293
+ let summaries;
2294
+ if (options.accessible) {
2295
+ ctx.output.info("Fetching membership and file events...");
2296
+ const events = await fetchEventStreams(ctx.events, [
2297
+ types.MemberAdded,
2298
+ types.MemberRemoved,
2299
+ types.AllowlistDeleted,
2300
+ types.FileCreated,
2301
+ types.FileDeleted
2302
+ ], ctx.signal);
2303
+ summaries = reconcileFilesAccessibleBy(events, address, types);
2304
+ } else {
2305
+ ctx.output.info("Fetching file events...");
2306
+ const events = await fetchEventStreams(ctx.events, [types.FileCreated, types.FileOwnershipTransferred, types.FileDeleted], ctx.signal);
2307
+ summaries = reconcileFilesOwnedBy(events, address, types);
2308
+ }
2309
+ const limited = limit === undefined ? summaries : summaries.slice(0, limit);
2310
+ let items = limited;
2311
+ if (options.hydrate) {
2312
+ ctx.output.info(`Hydrating ${limited.length} file(s)...`);
2313
+ const full = [];
2314
+ for (const summary of limited) {
2315
+ const record = await ctx.filesReader.getEncryptedFile(summary.id, ctx.signal);
2316
+ full.push({ ...record, kind: "full" });
2317
+ }
2318
+ items = full;
2319
+ }
2320
+ ctx.output.result(renderFileList(items), items);
2321
+ }
2219
2322
  function registerFileCommands(program) {
2220
2323
  const file = program.command("file").description("Upload, register, read, and manage files on Walrus");
2221
2324
  const upload = file.command("upload <path>").description("Encrypt (or not), upload to Walrus, and register a file").requiredOption("-n, --name <name>", "File name").option("--public", "Upload a world-readable (unencrypted) file").option("--content-type <mime>", "MIME content type (inferred from the path if omitted)").option("--epochs <n>", "Walrus storage epochs", "3");
2222
2325
  allowlistOption(upload).action(async (path, options, command) => {
2223
2326
  await runFileUpload(await buildEncryptContext(command), path, options);
2224
2327
  });
2328
+ file.command("list").description("List files owned by (or accessible to) an address, via event indexing").option("--address <addr>", "Address to query (default: the active account)").option("--accessible", "List files you can decrypt via allowlist membership, not just owned").option("--hydrate", "Fetch full records (adds blobId; one read per file)").option("--limit <n>", "Cap the number of results").option("--indexer-url <url>", "Event source URL (default: the RPC; uses suix_queryEvents, a deprecated Sui endpoint)").action(async (options, command) => {
2329
+ await runFileList(await buildFileListContext(command, {
2330
+ indexerUrl: options.indexerUrl
2331
+ }), options);
2332
+ });
2225
2333
  const download = file.command("download <file>").description("Download a file's content; decrypts in place when encrypted").option("--out <path>", "Write content to a file instead of stdout").option("--seal-id <hex>", "Seal id from upload (required to decrypt an encrypted file)");
2226
2334
  viaAggregatorOption(download).action(async (target, options, command) => {
2227
2335
  await runFileDownload(await buildFileDownloadContext(command, {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@arcadiasystems/morse-cli",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Command-line interface for the Morse decentralized CMS on Sui.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -55,7 +55,7 @@
55
55
  "prepublishOnly": "bun run check && bun run build"
56
56
  },
57
57
  "dependencies": {
58
- "@arcadiasystems/morse-sdk": "^0.2.0",
58
+ "@arcadiasystems/morse-sdk": "^0.3.0",
59
59
  "@mysten/seal": "1.1.3",
60
60
  "@mysten/sui": "2.16.2",
61
61
  "@mysten/walrus": "1.1.6",