@bitpub/cli 2.0.4 → 2.0.5

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bitpub/cli",
3
- "version": "2.0.4",
3
+ "version": "2.0.5",
4
4
  "description": "BitPub CLI — local-first shared memory for AI agents. Six daily verbs (save/load/list/find/sync/delete), zero-config private namespace, encrypted client-side.",
5
5
  "bin": {
6
6
  "bitpub": "./bin/bitpub.js"
@@ -12,7 +12,23 @@
12
12
  * "did you mean…" search across the user's full private namespace.
13
13
  * With exactly one match, auto-load and print a transparency note to
14
14
  * stderr saying where it actually came from. With multiple, list
15
- * them and exit. With zero, print an actionable not-found error.
15
+ * them and exit. With zero, fall through.
16
+ * 4. If still empty, check whether the address is a *folder* — i.e. it
17
+ * has descendants under it — and if so, auto-load them in one shot.
18
+ * Capped at `--limit` (default 20) so a stray `load` on a deep
19
+ * prefix can't blow up output. A stderr breadcrumb tells the caller
20
+ * what we did and (if truncated) how to get the rest. This catches
21
+ * the common mistake of `load`-ing a prefix like
22
+ * `bitpub://group:foo/Agents/x` when the real slices live at
23
+ * `.../Agents/x/README`, `.../script`, etc. A single wildcard
24
+ * remote pull is attempted on cache miss so this works without a
25
+ * separate `sync` step.
26
+ * 5. Otherwise: actionable not-found error.
27
+ *
28
+ * Multi-slice raw output (from explicit wildcards or from the folder
29
+ * auto-load in step 4) is prefixed with a `=== <address> ===` header
30
+ * per slice so the caller can tell which content came from which slice.
31
+ * Single-slice loads keep their clean stdout-only output (no header).
16
32
  *
17
33
  * The DWIM step exists because agents holding a fully-qualified URL will
18
34
  * sometimes (incorrectly) strip it down to a short name before calling
@@ -33,6 +49,11 @@ module.exports = function registerLoad(program) {
33
49
  .description('Load a slice by short name (this project) or full bitpub:// address')
34
50
  .option('--no-fetch', 'Do not fetch from cloud if missing locally')
35
51
  .option('--format <raw|json>', 'Output format', 'raw')
52
+ .option(
53
+ '--limit <n>',
54
+ 'Max slices to return when the address resolves to a folder (default 20)',
55
+ '20'
56
+ )
36
57
  .action(async (name, opts) => {
37
58
  const config = requireConfig();
38
59
  const wasShortName = !name.startsWith('bitpub://') && !isAliasRef(name);
@@ -84,7 +105,58 @@ module.exports = function registerLoad(program) {
84
105
  console.error(` bitpub load <one-of-the-addresses-above>`);
85
106
  process.exit(1);
86
107
  }
87
- // Zero candidates: fall through to the not-found block below.
108
+ // Zero candidates: fall through to the folder/not-found block below.
109
+ }
110
+
111
+ // Folder check: the exact address didn't resolve to a slice, but it
112
+ // may be a *prefix* with children under it (e.g. user typed
113
+ // `bitpub://group:foo/Agents/x` when the real slices live at
114
+ // `.../Agents/x/README`, `.../script`, etc). Auto-load up to
115
+ // --limit children so the caller doesn't have to round-trip.
116
+ // Skip if the user already passed a wildcard — querySlices already
117
+ // handles that path and a 0-result wildcard is a real not-found.
118
+ if (slices.length === 0 && !hcu.includes('*')) {
119
+ const cleanHcu = hcu.replace(/\/$/, '');
120
+ const folderPattern = cleanHcu + '/**';
121
+ // Cap raised slightly above the user-facing default so a folder
122
+ // with N=21 items doesn't get silently truncated below the cap
123
+ // the user asked for; we still slice() to `limit` for display.
124
+ const limit = Math.max(1, parseInt(opts.limit, 10) || 20);
125
+ let descendants = querySlices(folderPattern);
126
+
127
+ if (descendants.length === 0 && opts.fetch !== false) {
128
+ try {
129
+ const api = createApiClient(config);
130
+ const remote = await api.pull(folderPattern, Math.max(limit, 50));
131
+ decryptSlices(remote, config.api_key);
132
+ remote.forEach(upsertSlice);
133
+ descendants = querySlices(folderPattern);
134
+ } catch {
135
+ // Non-fatal — if there's nothing remote either, we'll fall
136
+ // through to the regular not-found message below.
137
+ }
138
+ }
139
+
140
+ if (descendants.length > 0) {
141
+ const total = descendants.length;
142
+ const truncated = total > limit;
143
+ slices = descendants.slice(0, limit);
144
+
145
+ // Transparency breadcrumb → stderr (keeps stdout pure for pipes).
146
+ // The agent gets a single useful message that tells it (a) what
147
+ // we did and (b) how to escape the cap if 20 wasn't enough.
148
+ if (truncated) {
149
+ console.error(
150
+ `(loaded ${slices.length} of ${total} descendants of "${cleanHcu}" — ` +
151
+ `it's a folder, not a slice; pass --limit ${total} for all of them)`
152
+ );
153
+ } else {
154
+ console.error(
155
+ `(loaded ${slices.length} descendant${slices.length === 1 ? '' : 's'} of ` +
156
+ `"${cleanHcu}" — it's a folder, not a slice)`
157
+ );
158
+ }
159
+ }
88
160
  }
89
161
 
90
162
  if (slices.length === 0) {
@@ -113,10 +185,15 @@ module.exports = function registerLoad(program) {
113
185
  return;
114
186
  }
115
187
 
188
+ // Multi-slice raw output (wildcard input or folder auto-load) gets
189
+ // a `=== <address> ===` header per slice so the caller can attribute
190
+ // each chunk. Single-slice loads keep their clean header-free output.
116
191
  const parts = slices.map(s => {
117
192
  const payload = JSON.parse(s.payload);
118
- return payload.content ?? '';
193
+ const content = payload.content ?? '';
194
+ if (slices.length === 1) return content;
195
+ return `=== ${s.hcu} ===\n${content}`;
119
196
  });
120
- console.log(parts.join('\n---\n'));
197
+ console.log(parts.join(slices.length === 1 ? '' : '\n\n'));
121
198
  });
122
199
  };