@bitpub/cli 2.1.0 → 2.1.1
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 +1 -1
- package/src/commands/browser.js +170 -0
- package/static/console.html +117 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bitpub/cli",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.1",
|
|
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"
|
package/src/commands/browser.js
CHANGED
|
@@ -210,6 +210,158 @@ const FUNCTIONS = {
|
|
|
210
210
|
return { cmd: 'bash', args: ['-lc', `open -R "${p}"`] };
|
|
211
211
|
},
|
|
212
212
|
},
|
|
213
|
+
|
|
214
|
+
// ── GitHub Pack ────────────────────────────────────────────
|
|
215
|
+
// The orchestrator slice lives at a GROUP address so every teammate
|
|
216
|
+
// in the group can load the same logic — they don't fork it. Each
|
|
217
|
+
// entry point injects BITPUB_OWNER from local config so the script
|
|
218
|
+
// writes to *the running user's* private namespace, not the author's.
|
|
219
|
+
//
|
|
220
|
+
// Auth model: orchestrator probes (1) macOS keychain entry from
|
|
221
|
+
// GitHub Desktop, (2) gh CLI, (3) BitPub-managed token. Tokens are
|
|
222
|
+
// never passed through args/URLs — child reads them itself so
|
|
223
|
+
// secrets never traverse the bridge.
|
|
224
|
+
|
|
225
|
+
'github.check_auth': {
|
|
226
|
+
description: 'Detect available GitHub credentials (Desktop keychain, gh CLI, BitPub-managed token).',
|
|
227
|
+
build() {
|
|
228
|
+
return {
|
|
229
|
+
cmd: 'bash',
|
|
230
|
+
args: ['-lc',
|
|
231
|
+
'set -e; ' +
|
|
232
|
+
'bitpub load bitpub://group:tollbit.com/Apps/github/orchestrator > "$T" && ' +
|
|
233
|
+
'python3 "$T"',
|
|
234
|
+
],
|
|
235
|
+
env: {
|
|
236
|
+
T: path.join(os.tmpdir(), '.bitpub-fn-github-authcheck.py'),
|
|
237
|
+
BITPUB_GH_ACTION: 'auth_check',
|
|
238
|
+
BITPUB_OWNER: (readConfig() || {}).owner || '',
|
|
239
|
+
},
|
|
240
|
+
};
|
|
241
|
+
},
|
|
242
|
+
},
|
|
243
|
+
|
|
244
|
+
'github.ingest': {
|
|
245
|
+
description: 'Mirror your GitHub PRs, issues, and recent activity into your namespace.',
|
|
246
|
+
build(args) {
|
|
247
|
+
const a = args || {};
|
|
248
|
+
const recipes = Array.isArray(a.recipes) && a.recipes.length
|
|
249
|
+
? a.recipes.filter(r => typeof r === 'string' && /^[a-z_]+$/.test(r)).slice(0, 8)
|
|
250
|
+
: ['prs_review_requested', 'prs_mine', 'prs_org_recent', 'issues_assigned', 'activity_recent'];
|
|
251
|
+
return {
|
|
252
|
+
cmd: 'bash',
|
|
253
|
+
args: ['-lc',
|
|
254
|
+
'set -e; ' +
|
|
255
|
+
'bitpub load bitpub://group:tollbit.com/Apps/github/orchestrator > "$T" && ' +
|
|
256
|
+
'python3 "$T"',
|
|
257
|
+
],
|
|
258
|
+
env: {
|
|
259
|
+
T: path.join(os.tmpdir(), '.bitpub-fn-github-ingest.py'),
|
|
260
|
+
BITPUB_GH_RECIPES: recipes.join(','),
|
|
261
|
+
BITPUB_OWNER: (readConfig() || {}).owner || '',
|
|
262
|
+
},
|
|
263
|
+
};
|
|
264
|
+
},
|
|
265
|
+
},
|
|
266
|
+
|
|
267
|
+
'github.open': {
|
|
268
|
+
description: 'Open a GitHub URL in the default browser.',
|
|
269
|
+
build(args) {
|
|
270
|
+
const url = String((args && args.url) || '');
|
|
271
|
+
if (!/^https:\/\/github\.com\//.test(url) || url.includes('"')) {
|
|
272
|
+
throw new Error('invalid github url');
|
|
273
|
+
}
|
|
274
|
+
return { cmd: 'bash', args: ['-lc', `open "${url}"`] };
|
|
275
|
+
},
|
|
276
|
+
},
|
|
277
|
+
|
|
278
|
+
'github.open_clone': {
|
|
279
|
+
description: 'Open a repo\'s local clone in Finder (or VS Code if "in" === "vscode").',
|
|
280
|
+
build(args) {
|
|
281
|
+
const repo = String((args && args.repo) || '');
|
|
282
|
+
const where = String((args && args.in) || 'finder');
|
|
283
|
+
if (!/^[A-Za-z0-9._-]+\/[A-Za-z0-9._-]+$/.test(repo)) {
|
|
284
|
+
throw new Error('invalid repo "owner/name"');
|
|
285
|
+
}
|
|
286
|
+
const opener = where === 'vscode' ? 'code' :
|
|
287
|
+
where === 'iterm' ? `open -a iTerm` :
|
|
288
|
+
'open';
|
|
289
|
+
// Search common project roots for a directory whose `git remote -v`
|
|
290
|
+
// points at this repo. Local-first move: we surface the user's
|
|
291
|
+
// clone if they have one, instead of dumping them at github.com.
|
|
292
|
+
const script = `
|
|
293
|
+
set -e
|
|
294
|
+
REPO='${repo}'
|
|
295
|
+
ROOTS=(~/projects ~/code ~/dev ~/src ~/Documents/projects ~/work)
|
|
296
|
+
for root in "\${ROOTS[@]}"; do
|
|
297
|
+
[ -d "$root" ] || continue
|
|
298
|
+
while IFS= read -r dir; do
|
|
299
|
+
if (cd "$dir" && git remote -v 2>/dev/null) | grep -qiE "[:/]\${REPO}(\\.git)?( |\\\$)"; then
|
|
300
|
+
echo "found clone: $dir"
|
|
301
|
+
${opener} "$dir"
|
|
302
|
+
exit 0
|
|
303
|
+
fi
|
|
304
|
+
done < <(find "$root" -mindepth 1 -maxdepth 3 -type d -name .git -prune -exec dirname {} \\; 2>/dev/null)
|
|
305
|
+
done
|
|
306
|
+
echo "no local clone of $REPO found in common roots — opening on github.com instead"
|
|
307
|
+
open "https://github.com/$REPO"
|
|
308
|
+
`;
|
|
309
|
+
return { cmd: 'bash', args: ['-lc', script] };
|
|
310
|
+
},
|
|
311
|
+
},
|
|
312
|
+
|
|
313
|
+
'github.summarize_pr': {
|
|
314
|
+
description: 'Summarize a PR with claude (the only LLM-in-the-loop step).',
|
|
315
|
+
build(args) {
|
|
316
|
+
const hcu = String((args && args.hcu) || '');
|
|
317
|
+
// Any private namespace, as long as it's pointed at GitHub/PRs/.
|
|
318
|
+
// We constrain the shape so an app can't trick the bridge into
|
|
319
|
+
// pulling arbitrary slices through claude.
|
|
320
|
+
if (!/^bitpub:\/\/private:[a-z0-9_-]+\/GitHub\/PRs\//i.test(hcu) || hcu.includes('"')) {
|
|
321
|
+
throw new Error('invalid PR hcu');
|
|
322
|
+
}
|
|
323
|
+
// Deterministic read (bitpub load) -> single LLM call (claude -p) ->
|
|
324
|
+
// structured text back to the UI. No MCP, no network round-trip for
|
|
325
|
+
// the data — only for the synthesis.
|
|
326
|
+
const script =
|
|
327
|
+
'set -e; ' +
|
|
328
|
+
`bitpub load "${hcu}" > "$T" && ` +
|
|
329
|
+
'claude -p "$(printf \'%s\\n\\nSummarize this GitHub PR in 3 short bullets: what it does, who it affects, anything risky. Be concrete.\' "$(cat "$T")")" ' +
|
|
330
|
+
'--output-format text --permission-mode bypassPermissions';
|
|
331
|
+
return {
|
|
332
|
+
cmd: 'bash',
|
|
333
|
+
args: ['-lc', script],
|
|
334
|
+
env: { T: path.join(os.tmpdir(), `.bitpub-fn-github-summarize.md`) },
|
|
335
|
+
};
|
|
336
|
+
},
|
|
337
|
+
},
|
|
338
|
+
|
|
339
|
+
'github.action.approve': {
|
|
340
|
+
description: 'Approve a pull request via the GitHub API (uses inherited credentials).',
|
|
341
|
+
build(args) {
|
|
342
|
+
const repo = String((args && args.repo) || '');
|
|
343
|
+
const number = parseInt((args && args.number), 10);
|
|
344
|
+
const body = String((args && args.body) || '');
|
|
345
|
+
if (!/^[A-Za-z0-9._-]+\/[A-Za-z0-9._-]+$/.test(repo)) throw new Error('invalid repo');
|
|
346
|
+
if (!Number.isFinite(number) || number <= 0) throw new Error('invalid PR number');
|
|
347
|
+
return {
|
|
348
|
+
cmd: 'bash',
|
|
349
|
+
args: ['-lc',
|
|
350
|
+
'set -e; ' +
|
|
351
|
+
'bitpub load bitpub://group:tollbit.com/Apps/github/orchestrator > "$T" && ' +
|
|
352
|
+
'python3 "$T"',
|
|
353
|
+
],
|
|
354
|
+
env: {
|
|
355
|
+
T: path.join(os.tmpdir(), '.bitpub-fn-github-action.py'),
|
|
356
|
+
BITPUB_GH_ACTION: 'approve',
|
|
357
|
+
BITPUB_GH_REPO: repo,
|
|
358
|
+
BITPUB_GH_PR: String(number),
|
|
359
|
+
BITPUB_GH_BODY: body,
|
|
360
|
+
BITPUB_OWNER: (readConfig() || {}).owner || '',
|
|
361
|
+
},
|
|
362
|
+
};
|
|
363
|
+
},
|
|
364
|
+
},
|
|
213
365
|
};
|
|
214
366
|
|
|
215
367
|
function clampInt(value, lo, hi, dflt) {
|
|
@@ -415,6 +567,24 @@ function startBrowserServer(opts = {}) {
|
|
|
415
567
|
return;
|
|
416
568
|
}
|
|
417
569
|
|
|
570
|
+
// Bridge: who is this BitPub installation? Apps that ship via a
|
|
571
|
+
// group/public namespace can't hardcode an owner ID — they need to
|
|
572
|
+
// know "what is the current user's private prefix" at runtime so
|
|
573
|
+
// their namespace.list() / save() calls hit the right cache.
|
|
574
|
+
// Returns the minimum info needed: owner id, the domain (for
|
|
575
|
+
// display), and the canonical private namespace prefix.
|
|
576
|
+
if (url.pathname === '/bridge/identity') {
|
|
577
|
+
res.setHeader('Content-Type', 'application/json');
|
|
578
|
+
const owner = (cfg && cfg.owner) || null;
|
|
579
|
+
const domain = (cfg && cfg.domain) || null;
|
|
580
|
+
res.end(JSON.stringify({
|
|
581
|
+
owner,
|
|
582
|
+
domain,
|
|
583
|
+
private_prefix: owner ? `bitpub://private:${owner}` : null,
|
|
584
|
+
}));
|
|
585
|
+
return;
|
|
586
|
+
}
|
|
587
|
+
|
|
418
588
|
// Bridge: list registered functions (for the permission UI to
|
|
419
589
|
// describe what an app is about to run).
|
|
420
590
|
if (url.pathname === '/bridge/functions') {
|
package/static/console.html
CHANGED
|
@@ -1613,8 +1613,18 @@ function renderBranch(scope, children, path) {
|
|
|
1613
1613
|
|
|
1614
1614
|
let html = '';
|
|
1615
1615
|
|
|
1616
|
-
//
|
|
1617
|
-
|
|
1616
|
+
// Direct leaves: render by name when small enough to be useful as a
|
|
1617
|
+
// package contents listing (e.g. an Apps/github folder showing its
|
|
1618
|
+
// `orchestrator` child by name). Aggregate into a single "N context
|
|
1619
|
+
// slices" summary above a threshold so very large folders (282
|
|
1620
|
+
// transcripts, hundreds of PRs) don't blow up the tree.
|
|
1621
|
+
const LEAF_NAME_THRESHOLD = 8;
|
|
1622
|
+
if (directLeaves.length > 0 && directLeaves.length <= LEAF_NAME_THRESHOLD) {
|
|
1623
|
+
const sorted = [...directLeaves].sort((a, b) => a.name.localeCompare(b.name));
|
|
1624
|
+
for (const { name, slice } of sorted) {
|
|
1625
|
+
html += renderLeaf(slice, name);
|
|
1626
|
+
}
|
|
1627
|
+
} else if (directLeaves.length > LEAF_NAME_THRESHOLD) {
|
|
1618
1628
|
const mostRecent = directLeaves.reduce(
|
|
1619
1629
|
(a, b) => ((a.slice.last_synced || '') > (b.slice.last_synced || '') ? a : b)
|
|
1620
1630
|
);
|
|
@@ -1623,7 +1633,11 @@ function renderBranch(scope, children, path) {
|
|
|
1623
1633
|
const pathArg = JSON.stringify(path).replace(/"/g, '"');
|
|
1624
1634
|
const isActiveSummary = S.view === 'namespace' && S.currentScope === scope && arrayEq(S.currentPath, path);
|
|
1625
1635
|
const when = mostRecent.slice.last_synced ? timeAgo(mostRecent.slice.last_synced) : '';
|
|
1626
|
-
|
|
1636
|
+
// Aggregated summary goes to the namespace listing (not the self-slice).
|
|
1637
|
+
// Calls navigateToNamespace to bypass the directory-index resolution in
|
|
1638
|
+
// navigateTo() — clicking "47 PRs" should show the listing, not whatever
|
|
1639
|
+
// happens to live at the parent address.
|
|
1640
|
+
html += `<div class="tree-leaf tree-summary${isActiveSummary ? ' active' : ''}" onclick="navigateToNamespace('${escAttr(scope)}', ${pathArg})" title="View ${directLeaves.length} slice${directLeaves.length !== 1 ? 's' : ''} at this level">
|
|
1627
1641
|
<span class="fresh-dot ${freshClass}"></span>
|
|
1628
1642
|
<span class="tree-label">${directLeaves.length} context slice${directLeaves.length !== 1 ? 's' : ''}</span>
|
|
1629
1643
|
<span class="tree-badge">${esc(when)}</span>
|
|
@@ -1934,6 +1948,24 @@ function goRoot() {
|
|
|
1934
1948
|
}
|
|
1935
1949
|
|
|
1936
1950
|
function navigateTo(scope, path) {
|
|
1951
|
+
const segs = Array.isArray(path) ? path : [];
|
|
1952
|
+
// Directory-index semantics: if a slice exists AT this folder address
|
|
1953
|
+
// (e.g. an HTML app at `Apps/github` that also has `Apps/github/orchestrator`
|
|
1954
|
+
// as a child), render the slice as the primary view — same as a
|
|
1955
|
+
// browser serving /foo/index.html when you visit /foo/. Children stay
|
|
1956
|
+
// reachable via the tree chevron and named-leaf entries underneath.
|
|
1957
|
+
const selfHcu = `bitpub://${scope}` + (segs.length ? '/' + segs.join('/') : '');
|
|
1958
|
+
const self = S.slices.find(s => s.hcu === selfHcu);
|
|
1959
|
+
if (self) { selectSlice(selfHcu); return; }
|
|
1960
|
+
|
|
1961
|
+
navigateToNamespace(scope, segs);
|
|
1962
|
+
}
|
|
1963
|
+
|
|
1964
|
+
// Explicit "show me the folder listing" navigation. Used by the
|
|
1965
|
+
// aggregated "N context slices" summary in the tree, and anywhere
|
|
1966
|
+
// else we want to skip the directory-index check (e.g. "see all in
|
|
1967
|
+
// this namespace" links).
|
|
1968
|
+
function navigateToNamespace(scope, path) {
|
|
1937
1969
|
S.view = 'namespace';
|
|
1938
1970
|
S.currentScope = scope;
|
|
1939
1971
|
S.currentPath = Array.isArray(path) ? path : [];
|
|
@@ -2595,6 +2627,16 @@ const BRIDGE_SHIM_SOURCE = String.raw`
|
|
|
2595
2627
|
readHandlers.delete(d.requestId);
|
|
2596
2628
|
if (d.ok) rh.resolve({ slices: d.slices || [], count: d.count || 0, pattern: d.pattern || '' });
|
|
2597
2629
|
else rh.reject(new Error(d.error || 'namespace read failed'));
|
|
2630
|
+
} else if (d.type === 'bitpub.identity.result') {
|
|
2631
|
+
var ih = readHandlers.get(d.requestId);
|
|
2632
|
+
if (!ih) return;
|
|
2633
|
+
readHandlers.delete(d.requestId);
|
|
2634
|
+
if (d.ok) ih.resolve({
|
|
2635
|
+
owner: d.owner,
|
|
2636
|
+
domain: d.domain,
|
|
2637
|
+
private_prefix: d.private_prefix,
|
|
2638
|
+
});
|
|
2639
|
+
else ih.reject(new Error(d.error || 'identity lookup failed'));
|
|
2598
2640
|
}
|
|
2599
2641
|
});
|
|
2600
2642
|
window.bitpub = {
|
|
@@ -2638,6 +2680,43 @@ const BRIDGE_SHIM_SOURCE = String.raw`
|
|
|
2638
2680
|
}, 10000);
|
|
2639
2681
|
});
|
|
2640
2682
|
}
|
|
2683
|
+
},
|
|
2684
|
+
navigate: function (hcu) {
|
|
2685
|
+
// Hand off to another slice in the parent BitPub Browser
|
|
2686
|
+
// (e.g. discovery page → installed app). HCU must be a
|
|
2687
|
+
// bitpub:// address; everything else is dropped silently.
|
|
2688
|
+
window.parent.postMessage({
|
|
2689
|
+
__bitpub: true,
|
|
2690
|
+
type: 'bitpub.navigate',
|
|
2691
|
+
hcu: String(hcu || ''),
|
|
2692
|
+
}, '*');
|
|
2693
|
+
},
|
|
2694
|
+
identity: {
|
|
2695
|
+
// me() -> Promise<{ owner, domain, private_prefix }>
|
|
2696
|
+
// owner e.g. "agent_ewim9gf01nq3"
|
|
2697
|
+
// domain e.g. "tollbit.com"
|
|
2698
|
+
// private_prefix e.g. "bitpub://private:agent_ewim9gf01nq3"
|
|
2699
|
+
//
|
|
2700
|
+
// Lets apps that are distributed via a group/public namespace
|
|
2701
|
+
// construct addresses that point at the *running user's* private
|
|
2702
|
+
// data without hardcoding an owner ID. Use this if your app ships
|
|
2703
|
+
// to other people.
|
|
2704
|
+
me: function () {
|
|
2705
|
+
return new Promise(function (resolve, reject) {
|
|
2706
|
+
var requestId = ++nextId;
|
|
2707
|
+
readHandlers.set(requestId, { resolve: resolve, reject: reject });
|
|
2708
|
+
window.parent.postMessage({
|
|
2709
|
+
__bitpub: true,
|
|
2710
|
+
type: 'bitpub.identity.me',
|
|
2711
|
+
requestId: requestId,
|
|
2712
|
+
}, '*');
|
|
2713
|
+
setTimeout(function () {
|
|
2714
|
+
if (!readHandlers.has(requestId)) return;
|
|
2715
|
+
readHandlers.delete(requestId);
|
|
2716
|
+
reject(new Error('identity.me timed out'));
|
|
2717
|
+
}, 5000);
|
|
2718
|
+
});
|
|
2719
|
+
}
|
|
2641
2720
|
}
|
|
2642
2721
|
};
|
|
2643
2722
|
})();
|
|
@@ -2794,11 +2873,46 @@ async function handleBridgeNamespaceListMessage(source, msg) {
|
|
|
2794
2873
|
}
|
|
2795
2874
|
}
|
|
2796
2875
|
|
|
2876
|
+
async function handleBridgeIdentityMessage(source, msg) {
|
|
2877
|
+
const { requestId } = msg;
|
|
2878
|
+
if (typeof requestId !== 'number') return;
|
|
2879
|
+
const reply = (extra) => {
|
|
2880
|
+
try {
|
|
2881
|
+
source.postMessage({
|
|
2882
|
+
__bitpub: true,
|
|
2883
|
+
type: 'bitpub.identity.result',
|
|
2884
|
+
requestId,
|
|
2885
|
+
...extra,
|
|
2886
|
+
}, '*');
|
|
2887
|
+
} catch (_) { /* iframe gone */ }
|
|
2888
|
+
};
|
|
2889
|
+
try {
|
|
2890
|
+
const r = await fetch('/bridge/identity');
|
|
2891
|
+
const body = await r.json();
|
|
2892
|
+
if (!r.ok) { reply({ ok: false, error: `http ${r.status}` }); return; }
|
|
2893
|
+
reply({ ok: true, owner: body.owner, domain: body.domain, private_prefix: body.private_prefix });
|
|
2894
|
+
} catch (err) {
|
|
2895
|
+
reply({ ok: false, error: err && err.message || 'fetch failed' });
|
|
2896
|
+
}
|
|
2897
|
+
}
|
|
2898
|
+
|
|
2899
|
+
function handleBridgeNavigateMessage(_source, msg) {
|
|
2900
|
+
// Apps can hand off to another slice in the running user's Browser.
|
|
2901
|
+
// We only allow bitpub:// addresses (no http:, no javascript:), and
|
|
2902
|
+
// we route through submitAddress() which already does all the
|
|
2903
|
+
// existing scope/path parsing.
|
|
2904
|
+
const hcu = String((msg && msg.hcu) || '').trim();
|
|
2905
|
+
if (!/^bitpub:\/\/[^\s]+$/.test(hcu)) return;
|
|
2906
|
+
try { submitAddress(hcu); } catch (_) { /* ignore */ }
|
|
2907
|
+
}
|
|
2908
|
+
|
|
2797
2909
|
window.addEventListener('message', (e) => {
|
|
2798
2910
|
const d = e.data;
|
|
2799
2911
|
if (!d || typeof d !== 'object' || d.__bitpub !== true) return;
|
|
2800
2912
|
if (d.type === 'bitpub.run') handleBridgeRunMessage(e.source, d);
|
|
2801
2913
|
else if (d.type === 'bitpub.namespace.list') handleBridgeNamespaceListMessage(e.source, d);
|
|
2914
|
+
else if (d.type === 'bitpub.identity.me') handleBridgeIdentityMessage(e.source, d);
|
|
2915
|
+
else if (d.type === 'bitpub.navigate' || d.type === 'navigate') handleBridgeNavigateMessage(e.source, d);
|
|
2802
2916
|
});
|
|
2803
2917
|
|
|
2804
2918
|
function renderWriteGuide(sl) {
|