lopebase 0.1.0 → 0.2.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/README.md +87 -20
- package/dist/lopebase.js +312 -2
- package/package.json +17 -7
package/README.md
CHANGED
|
@@ -1,28 +1,95 @@
|
|
|
1
|
-
|
|
1
|
+
<p align="center">
|
|
2
|
+
<a href="https://lopebase.com"><img src="https://lopebase.com/icon-512.png" width="72" height="72" alt="LopeBase"></a>
|
|
3
|
+
</p>
|
|
2
4
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
+
<h1 align="center">lopebase</h1>
|
|
6
|
+
|
|
7
|
+
<p align="center">
|
|
8
|
+
Set up <a href="https://lopebase.com">LopeBase</a> from your project's own folder.<br>
|
|
9
|
+
Your coding agent does the steps. It stops for you only when it has to.
|
|
10
|
+
</p>
|
|
11
|
+
|
|
12
|
+
<p align="center">
|
|
13
|
+
<a href="https://lopebase.com">Website</a> ·
|
|
14
|
+
<a href="https://lopebase.com/agents">For agents</a> ·
|
|
15
|
+
<a href="https://lopebase.com/pricing">Pricing</a> ·
|
|
16
|
+
<a href="https://www.npmjs.com/package/@lopebase/adapter">@lopebase/adapter</a>
|
|
17
|
+
</p>
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
LopeBase is one place to see and run all of your products: traffic, search,
|
|
22
|
+
signups, revenue, and errors side by side, with a weekly summary and an agent
|
|
23
|
+
that can help. This CLI connects a project to it in a few minutes.
|
|
24
|
+
|
|
25
|
+
## Quick start
|
|
26
|
+
|
|
27
|
+
In the folder of any project you run:
|
|
5
28
|
|
|
6
29
|
```sh
|
|
7
|
-
npx lopebase
|
|
8
|
-
npx lopebase login # connect this computer: approve a code in the browser
|
|
9
|
-
npx lopebase scan # what the scan found (--json for machines)
|
|
10
|
-
npx lopebase prompt # print the agent prompt instead of launching an agent
|
|
11
|
-
npx lopebase mcp # stdio MCP server that forwards to your workspace
|
|
30
|
+
npx lopebase
|
|
12
31
|
```
|
|
13
32
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
33
|
+
1. **Scan.** It reads your `package.json` files, config file names, and the
|
|
34
|
+
variable *names* in example env files, then shows your stack, the services
|
|
35
|
+
you already use, and what is missing (for example analytics or error
|
|
36
|
+
tracking).
|
|
37
|
+
2. **Connect this computer.** It opens your LopeBase workspace and shows a
|
|
38
|
+
short code. Check it matches and press Connect.
|
|
39
|
+
3. **Pick what to connect.** Services already in your code are ticked; the
|
|
40
|
+
ones you do not use yet are offered, never forced.
|
|
41
|
+
4. **Hand over to your agent.** It finds the coding agents you have installed
|
|
42
|
+
(Claude Code, Codex, Gemini CLI, opencode, Cursor Agent), gives the one you
|
|
43
|
+
choose the LopeBase tools, and starts it with what the scan found. The agent
|
|
44
|
+
does the setup and asks you when it needs a sign-in, a key, or a decision.
|
|
45
|
+
|
|
46
|
+
No agent installed? It prints the prompt for you to paste instead.
|
|
47
|
+
|
|
48
|
+
## Commands
|
|
49
|
+
|
|
50
|
+
| Command | What it does |
|
|
51
|
+
| --- | --- |
|
|
52
|
+
| `npx lopebase` | Scan, connect, choose, and hand over to your agent. |
|
|
53
|
+
| `npx lopebase login` | Connect this computer to your workspace with a short code. `--paste` to paste an agent token instead. |
|
|
54
|
+
| `npx lopebase scan` | Show what the scan found. `--json` for scripts. |
|
|
55
|
+
| `npx lopebase prompt` | Print the setup prompt instead of starting an agent. |
|
|
56
|
+
| `npx lopebase mcp` | Run the LopeBase tools as a local MCP server (stdio) for any agent. |
|
|
57
|
+
| `npx lopebase adapter secret` | Print a new signing secret for [@lopebase/adapter](https://www.npmjs.com/package/@lopebase/adapter). Pipe it straight into your secret store. |
|
|
58
|
+
| `npx lopebase adapter check <url>` | Test a running adapter, local or deployed, before you connect it. |
|
|
59
|
+
|
|
60
|
+
## What it reads, and what it keeps
|
|
61
|
+
|
|
62
|
+
- **Reads:** `package.json` files at the root and in `apps/`, `packages/`,
|
|
63
|
+
and similar folders; config file names such as `wrangler.toml` or
|
|
64
|
+
`vercel.json`; and the variable names in `.env.example` style files.
|
|
65
|
+
- **Never reads** a real `.env` file, and never sends a value anywhere. Only
|
|
66
|
+
names leave your machine, and only to your own workspace.
|
|
67
|
+
- **Keeps** one thing: an agent token in `~/.config/lopebase/auth.json`,
|
|
68
|
+
readable only by you. It lasts 30 days, acts as the workspace admin who
|
|
69
|
+
approved it, is limited to what that admin allowed, and can be revoked any
|
|
70
|
+
time under Settings, Agent access.
|
|
71
|
+
- `LOPEBASE_TOKEN` and `LOPEBASE_URL` override the saved values, for CI or
|
|
72
|
+
another machine.
|
|
73
|
+
|
|
74
|
+
## Use it with any agent
|
|
75
|
+
|
|
76
|
+
`npx lopebase` sets this up for you. To add the tools by hand, register this
|
|
77
|
+
as a stdio MCP server in your agent:
|
|
78
|
+
|
|
79
|
+
```sh
|
|
80
|
+
npx -y lopebase mcp
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Or point an agent that supports remote MCP straight at your workspace, for
|
|
84
|
+
example `https://yourteam.lopebase.com/api/mcp`. It will ask you to sign in.
|
|
85
|
+
|
|
86
|
+
## Requirements
|
|
87
|
+
|
|
88
|
+
Node 18 or newer. Works on macOS, Linux, and Windows.
|
|
17
89
|
|
|
18
|
-
|
|
19
|
-
`bun run build` makes `dist/lopebase.js` for Node, and `npm publish` runs
|
|
20
|
-
verify and build first.
|
|
90
|
+
## Help
|
|
21
91
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
The token is saved to `~/.config/lopebase/auth.json` (mode 600), or read from
|
|
25
|
-
`LOPEBASE_TOKEN` and `LOPEBASE_URL`.
|
|
92
|
+
Questions or problems: [contact@jackalope.digital](mailto:contact@jackalope.digital).
|
|
93
|
+
Privacy: [app.lopebase.com/legal/privacy](https://app.lopebase.com/legal/privacy).
|
|
26
94
|
|
|
27
|
-
|
|
28
|
-
terminal and the server give the same answer.
|
|
95
|
+
LopeBase is made by [Jackalope Digital](https://jackalope.digital).
|
package/dist/lopebase.js
CHANGED
|
@@ -812,9 +812,282 @@ var Y2 = ({ indicator: t = "dots" } = {}) => {
|
|
|
812
812
|
} };
|
|
813
813
|
};
|
|
814
814
|
|
|
815
|
+
// ../admin-adapter/src/security.ts
|
|
816
|
+
var SIGNATURE_HEADER = "lopebase-signature";
|
|
817
|
+
var enc = /* @__PURE__ */ new TextEncoder;
|
|
818
|
+
function hex(bytes) {
|
|
819
|
+
return [...new Uint8Array(bytes)].map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
820
|
+
}
|
|
821
|
+
async function sha256Hex(data) {
|
|
822
|
+
return hex(await crypto.subtle.digest("SHA-256", data));
|
|
823
|
+
}
|
|
824
|
+
async function hmacHex(secret, message) {
|
|
825
|
+
let key = await crypto.subtle.importKey("raw", enc.encode(secret), { name: "HMAC", hash: "SHA-256" }, !1, ["sign"]);
|
|
826
|
+
return hex(await crypto.subtle.sign("HMAC", key, enc.encode(message)));
|
|
827
|
+
}
|
|
828
|
+
function newSigningSecret() {
|
|
829
|
+
let bytes = crypto.getRandomValues(new Uint8Array(32)), bin = "";
|
|
830
|
+
for (let b of bytes)
|
|
831
|
+
bin += String.fromCharCode(b);
|
|
832
|
+
return "lbs_" + btoa(bin).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
833
|
+
}
|
|
834
|
+
function payload(t, method, pathAndQuery, bodyHash) {
|
|
835
|
+
return `${t}.${method.toUpperCase()}.${pathAndQuery}.${bodyHash}`;
|
|
836
|
+
}
|
|
837
|
+
async function signRequest(input) {
|
|
838
|
+
let t = input.timestamp ?? Math.floor(Date.now() / 1000), body = typeof input.body === "string" ? enc.encode(input.body) : input.body ?? new Uint8Array, sig = await hmacHex(input.secret, payload(t, input.method, input.pathAndQuery, await sha256Hex(body)));
|
|
839
|
+
return `t=${t},v1=${sig}`;
|
|
840
|
+
}
|
|
841
|
+
var SENSITIVE_PARTS = /* @__PURE__ */ new Set([
|
|
842
|
+
"password",
|
|
843
|
+
"passwd",
|
|
844
|
+
"passcode",
|
|
845
|
+
"pwd",
|
|
846
|
+
"hash",
|
|
847
|
+
"hashed",
|
|
848
|
+
"salt",
|
|
849
|
+
"secret",
|
|
850
|
+
"secrets",
|
|
851
|
+
"token",
|
|
852
|
+
"tokens",
|
|
853
|
+
"credential",
|
|
854
|
+
"credentials",
|
|
855
|
+
"ssn",
|
|
856
|
+
"card",
|
|
857
|
+
"cvv",
|
|
858
|
+
"cvc",
|
|
859
|
+
"iban",
|
|
860
|
+
"otp",
|
|
861
|
+
"mfa",
|
|
862
|
+
"totp",
|
|
863
|
+
"session",
|
|
864
|
+
"cookie"
|
|
865
|
+
]), SENSITIVE_JOINED = /apikey|privatekey|secretkey|accesskey|accountnumber|routingnumber|stripecustomer|paymentmethod|recoverycode/;
|
|
866
|
+
function isSensitiveFieldName(name) {
|
|
867
|
+
let parts = name.replace(/([a-z0-9])([A-Z])/g, "$1_$2").toLowerCase().split(/[^a-z0-9]+/).filter(Boolean);
|
|
868
|
+
if (parts.some((p) => SENSITIVE_PARTS.has(p)))
|
|
869
|
+
return !0;
|
|
870
|
+
return SENSITIVE_JOINED.test(parts.join(""));
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
// ../admin-adapter/src/adapter.ts
|
|
874
|
+
var DEFAULT_BASE_PATH = "/api/lopebase";
|
|
875
|
+
// ../admin-adapter/src/conformance.ts
|
|
876
|
+
import { strict as assert } from "node:assert";
|
|
877
|
+
async function body(res) {
|
|
878
|
+
return await res.json();
|
|
879
|
+
}
|
|
880
|
+
function conformanceChecks(opts) {
|
|
881
|
+
if (!opts.adapter && !opts.baseUrl)
|
|
882
|
+
throw Error("conformanceChecks needs an adapter or a baseUrl");
|
|
883
|
+
let remote = !opts.adapter, base = opts.baseUrl ? opts.baseUrl.replace(/\/$/, "") : `${opts.origin ?? "https://property.test"}${(opts.basePath ?? opts.adapter?.config.basePath ?? DEFAULT_BASE_PATH).replace(/\/$/, "")}`, send = (req) => opts.adapter ? opts.adapter.handle(req) : fetch(req);
|
|
884
|
+
async function signedHeaders(url, method, bodyText, timestamp) {
|
|
885
|
+
if (opts.secret) {
|
|
886
|
+
let u = new URL(url);
|
|
887
|
+
return {
|
|
888
|
+
[SIGNATURE_HEADER]: await signRequest({
|
|
889
|
+
secret: opts.secret,
|
|
890
|
+
method,
|
|
891
|
+
pathAndQuery: u.pathname + u.search,
|
|
892
|
+
body: bodyText,
|
|
893
|
+
timestamp
|
|
894
|
+
})
|
|
895
|
+
};
|
|
896
|
+
}
|
|
897
|
+
return opts.token ? { authorization: `Bearer ${opts.token}` } : {};
|
|
898
|
+
}
|
|
899
|
+
let authed = async (path, init) => {
|
|
900
|
+
let url = base + path, method = init?.method ?? "GET", bodyText = typeof init?.body === "string" ? init.body : void 0;
|
|
901
|
+
return send(new Request(url, {
|
|
902
|
+
...init,
|
|
903
|
+
headers: {
|
|
904
|
+
"content-type": "application/json",
|
|
905
|
+
...await signedHeaders(url, method, bodyText),
|
|
906
|
+
...init?.headers
|
|
907
|
+
}
|
|
908
|
+
}));
|
|
909
|
+
}, resources = () => authed("/resources").then((r) => body(r));
|
|
910
|
+
return [
|
|
911
|
+
{
|
|
912
|
+
name: "GET /health returns { ok } without auth",
|
|
913
|
+
async run() {
|
|
914
|
+
let res = await send(new Request(`${base}/health`));
|
|
915
|
+
assert.equal(res.status, 200);
|
|
916
|
+
let b = await body(res);
|
|
917
|
+
assert.equal(typeof b.ok, "boolean");
|
|
918
|
+
}
|
|
919
|
+
},
|
|
920
|
+
{
|
|
921
|
+
name: "requests without a valid token get 401",
|
|
922
|
+
async run() {
|
|
923
|
+
let res = await send(new Request(`${base}/resources`));
|
|
924
|
+
assert.equal(res.status, 401);
|
|
925
|
+
}
|
|
926
|
+
},
|
|
927
|
+
{
|
|
928
|
+
name: "GET /resources returns a catalog with field metadata",
|
|
929
|
+
async run() {
|
|
930
|
+
let res = await authed("/resources");
|
|
931
|
+
assert.equal(res.status, 200);
|
|
932
|
+
let b = await body(res);
|
|
933
|
+
assert.ok(Array.isArray(b.resources));
|
|
934
|
+
for (let r of b.resources)
|
|
935
|
+
assert.ok(r.name && r.label && r.readCapability), assert.ok(Array.isArray(r.fields));
|
|
936
|
+
}
|
|
937
|
+
},
|
|
938
|
+
{
|
|
939
|
+
name: "GET /actions returns a catalog with param specs",
|
|
940
|
+
async run() {
|
|
941
|
+
let res = await authed("/actions");
|
|
942
|
+
assert.equal(res.status, 200);
|
|
943
|
+
let b = await body(res);
|
|
944
|
+
assert.ok(Array.isArray(b.actions));
|
|
945
|
+
for (let a of b.actions)
|
|
946
|
+
assert.ok(a.name && a.label && a.capability), assert.ok(Array.isArray(a.params));
|
|
947
|
+
}
|
|
948
|
+
},
|
|
949
|
+
{
|
|
950
|
+
name: "GET /resources/:name paginates ({ rows, total? })",
|
|
951
|
+
async run() {
|
|
952
|
+
let cat = await resources(), name = opts.sampleResource ?? cat.resources[0]?.name;
|
|
953
|
+
if (!name)
|
|
954
|
+
return;
|
|
955
|
+
let res = await authed(`/resources/${name}?limit=5`);
|
|
956
|
+
assert.equal(res.status, 200);
|
|
957
|
+
let page = await body(res);
|
|
958
|
+
assert.ok(Array.isArray(page.rows));
|
|
959
|
+
}
|
|
960
|
+
},
|
|
961
|
+
{
|
|
962
|
+
name: "GET /resources/:name/:id returns a row or 404-null",
|
|
963
|
+
async run() {
|
|
964
|
+
let cat = await resources(), meta = opts.sampleResource ? cat.resources.find((r) => r.name === opts.sampleResource) : cat.resources[0];
|
|
965
|
+
if (!meta)
|
|
966
|
+
return;
|
|
967
|
+
let first = (await body(await authed(`/resources/${meta.name}?limit=1`))).rows[0];
|
|
968
|
+
if (!first)
|
|
969
|
+
return;
|
|
970
|
+
let idField = meta.fields[0]?.name ?? "id", res = await authed(`/resources/${meta.name}/${encodeURIComponent(String(first[idField]))}`);
|
|
971
|
+
assert.ok(res.status === 200 || res.status === 404);
|
|
972
|
+
}
|
|
973
|
+
},
|
|
974
|
+
{
|
|
975
|
+
name: "GET /audit returns { rows: AuditEvent[] }",
|
|
976
|
+
async run() {
|
|
977
|
+
let res = await authed("/audit");
|
|
978
|
+
assert.equal(res.status, 200);
|
|
979
|
+
let b = await body(res);
|
|
980
|
+
assert.ok(Array.isArray(b.rows));
|
|
981
|
+
}
|
|
982
|
+
},
|
|
983
|
+
{
|
|
984
|
+
name: "an unknown route returns 404",
|
|
985
|
+
async run() {
|
|
986
|
+
let res = await authed("/does-not-exist");
|
|
987
|
+
assert.equal(res.status, 404);
|
|
988
|
+
}
|
|
989
|
+
},
|
|
990
|
+
{
|
|
991
|
+
name: "no field in the catalog looks like a secret unless marked safe",
|
|
992
|
+
async run() {
|
|
993
|
+
let cat = await resources();
|
|
994
|
+
for (let r of cat.resources)
|
|
995
|
+
for (let f of r.fields)
|
|
996
|
+
assert.ok(!isSensitiveFieldName(f.name) || f.sensitive === "allow", `${r.name}.${f.name} looks sensitive`);
|
|
997
|
+
}
|
|
998
|
+
},
|
|
999
|
+
{
|
|
1000
|
+
name: "rows contain only declared fields, and a page is at most 100",
|
|
1001
|
+
async run() {
|
|
1002
|
+
let cat = await resources();
|
|
1003
|
+
for (let r of cat.resources) {
|
|
1004
|
+
let res = await authed(`/resources/${r.name}?limit=1000`);
|
|
1005
|
+
assert.equal(res.status, 200);
|
|
1006
|
+
let page = await body(res);
|
|
1007
|
+
assert.ok(page.rows.length <= 100, `${r.name} returned ${page.rows.length} rows`);
|
|
1008
|
+
let declared = /* @__PURE__ */ new Set(["id", ...r.fields.map((f) => f.name)]);
|
|
1009
|
+
for (let row of page.rows)
|
|
1010
|
+
for (let k of Object.keys(row))
|
|
1011
|
+
assert.ok(declared.has(k), `${r.name} returned undeclared field ${k}`);
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
},
|
|
1015
|
+
...opts.secret ? [
|
|
1016
|
+
{
|
|
1017
|
+
name: "a stale or tampered signature gets 401",
|
|
1018
|
+
async run() {
|
|
1019
|
+
let url = `${base}/resources`, stale = await signedHeaders(url, "GET", void 0, Math.floor(Date.now() / 1000) - 3600);
|
|
1020
|
+
assert.equal((await send(new Request(url, { headers: stale }))).status, 401);
|
|
1021
|
+
let good = await signedHeaders(url, "GET", void 0), tampered = `${url}?limit=1`;
|
|
1022
|
+
assert.equal((await send(new Request(tampered, { headers: good }))).status, 401);
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
] : [],
|
|
1026
|
+
{
|
|
1027
|
+
name: "a mutation writes an audit row (PATCH round-trips into /audit)",
|
|
1028
|
+
async run() {
|
|
1029
|
+
let writable = (await resources()).resources.find((r) => r.writeCapability && r.fields.some((f) => f.editable));
|
|
1030
|
+
if (!writable || remote)
|
|
1031
|
+
return;
|
|
1032
|
+
let row = (await body(await authed(`/resources/${writable.name}?limit=1`))).rows[0];
|
|
1033
|
+
if (!row)
|
|
1034
|
+
return;
|
|
1035
|
+
let idField = writable.fields[0]?.name ?? "id", editField = writable.fields.find((f) => f.editable)?.name;
|
|
1036
|
+
if (!editField)
|
|
1037
|
+
return;
|
|
1038
|
+
let before = await body(await authed("/audit")), res = await authed(`/resources/${writable.name}/${encodeURIComponent(String(row[idField]))}`, {
|
|
1039
|
+
method: "PATCH",
|
|
1040
|
+
body: JSON.stringify({ patch: { [editField]: row[editField] } })
|
|
1041
|
+
});
|
|
1042
|
+
assert.equal(res.status, 200);
|
|
1043
|
+
let after = await body(await authed("/audit"));
|
|
1044
|
+
assert.ok(after.rows.length > before.rows.length, "expected a new audit row");
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
];
|
|
1048
|
+
}
|
|
815
1049
|
// bin/lopebase.ts
|
|
816
1050
|
var import_picocolors3 = __toESM(require_picocolors(), 1);
|
|
817
1051
|
|
|
1052
|
+
// src/adapter-check.ts
|
|
1053
|
+
async function checkAdapter(input) {
|
|
1054
|
+
let log = input.log ?? (() => {}), failed = [], passed = 0, base = input.baseUrl.replace(/\/$/, ""), url = new URL(`${base}/resources`), status;
|
|
1055
|
+
try {
|
|
1056
|
+
let headers = input.secret ? {
|
|
1057
|
+
[SIGNATURE_HEADER]: await signRequest({
|
|
1058
|
+
secret: input.secret,
|
|
1059
|
+
method: "GET",
|
|
1060
|
+
pathAndQuery: url.pathname + url.search
|
|
1061
|
+
})
|
|
1062
|
+
} : { authorization: `Bearer ${input.token ?? ""}` };
|
|
1063
|
+
status = (await fetch(url, { headers })).status;
|
|
1064
|
+
} catch (e) {
|
|
1065
|
+
let error = `Could not reach ${base} (${e instanceof Error ? e.message : String(e)}).`;
|
|
1066
|
+
return log(`FAIL ${error}`), { passed, failed: [{ name: "reachable", error }] };
|
|
1067
|
+
}
|
|
1068
|
+
if (status === 401) {
|
|
1069
|
+
let error = input.secret ? "The adapter refused the signature. The secret here does not match the app's LOPEBASE_SIGNING_SECRET, or the two clocks are more than 5 minutes apart." : "The adapter refused the token.";
|
|
1070
|
+
return log(`FAIL ${error}`), { passed, failed: [{ name: "authenticated", error }] };
|
|
1071
|
+
}
|
|
1072
|
+
if (status === 404) {
|
|
1073
|
+
let error = `Nothing at ${base}/resources. Check the base URL, usually https://your-app/api/lopebase.`;
|
|
1074
|
+
return log(`FAIL ${error}`), { passed, failed: [{ name: "found", error }] };
|
|
1075
|
+
}
|
|
1076
|
+
for (let c of conformanceChecks({
|
|
1077
|
+
baseUrl: input.baseUrl,
|
|
1078
|
+
secret: input.secret,
|
|
1079
|
+
token: input.token
|
|
1080
|
+
}))
|
|
1081
|
+
try {
|
|
1082
|
+
await c.run(), passed++, log(`pass ${c.name}`);
|
|
1083
|
+
} catch (e) {
|
|
1084
|
+
let error = e instanceof Error ? e.message.split(`
|
|
1085
|
+
`)[0] : String(e);
|
|
1086
|
+
failed.push({ name: c.name, error }), log(`FAIL ${c.name}: ${error}`);
|
|
1087
|
+
}
|
|
1088
|
+
return { passed, failed };
|
|
1089
|
+
}
|
|
1090
|
+
|
|
818
1091
|
// src/agents.ts
|
|
819
1092
|
import { spawnSync } from "node:child_process";
|
|
820
1093
|
var AGENTS = [
|
|
@@ -1376,7 +1649,22 @@ function scanProject(root) {
|
|
|
1376
1649
|
}
|
|
1377
1650
|
|
|
1378
1651
|
// bin/lopebase.ts
|
|
1379
|
-
var [command = "init", ...rest] = process.argv.slice(2),
|
|
1652
|
+
var [command = "init", ...rest] = process.argv.slice(2), HELP = `
|
|
1653
|
+
${import_picocolors3.default.bgYellow(import_picocolors3.default.black(" LopeBase "))} Set up LopeBase from this project's folder.
|
|
1654
|
+
|
|
1655
|
+
${import_picocolors3.default.bold("Usage")}
|
|
1656
|
+
npx lopebase scan, connect, and hand over to your agent
|
|
1657
|
+
npx lopebase login connect this computer (--paste for a token)
|
|
1658
|
+
npx lopebase scan [--json] show what the scan found
|
|
1659
|
+
npx lopebase prompt print the setup prompt instead
|
|
1660
|
+
npx lopebase mcp run the LopeBase tools as a stdio MCP server
|
|
1661
|
+
npx lopebase adapter secret print a new @lopebase/adapter signing secret
|
|
1662
|
+
npx lopebase adapter check <url> test a running adapter (reads LOPEBASE_SIGNING_SECRET)
|
|
1663
|
+
|
|
1664
|
+
${import_picocolors3.default.bold("Learn more")}
|
|
1665
|
+
${import_picocolors3.default.cyan("https://lopebase.com")}
|
|
1666
|
+
Help: contact@jackalope.digital
|
|
1667
|
+
`, flag = (name) => rest.includes(`--${name}`), option = (name) => {
|
|
1380
1668
|
let i = rest.indexOf(`--${name}`);
|
|
1381
1669
|
return i >= 0 ? rest[i + 1] : rest.find((a) => a.startsWith(`--${name}=`))?.split("=")[1];
|
|
1382
1670
|
}, cwd = process.cwd();
|
|
@@ -1560,6 +1848,28 @@ async function main() {
|
|
|
1560
1848
|
let facts = scanProject(cwd), detection = detectStack(facts);
|
|
1561
1849
|
return console.log(onboardingPrompt({ facts, detection, chosen: [] }));
|
|
1562
1850
|
}
|
|
1851
|
+
case "adapter": {
|
|
1852
|
+
let sub = rest[0];
|
|
1853
|
+
if (sub === "secret")
|
|
1854
|
+
return process.stdout.write(`${newSigningSecret()}
|
|
1855
|
+
`);
|
|
1856
|
+
if (sub === "check" && rest[1]) {
|
|
1857
|
+
let envName = option("secret-env") ?? "LOPEBASE_SIGNING_SECRET", secret = process.env[envName]?.trim(), token = process.env.LOPEBASE_ADAPTER_TOKEN?.trim();
|
|
1858
|
+
if (!secret && !token)
|
|
1859
|
+
console.error(`Set ${envName} to the adapter's signing secret (or LOPEBASE_ADAPTER_TOKEN for an older bearer adapter).`), process.exit(1);
|
|
1860
|
+
console.log(`Checking ${rest[1]}`);
|
|
1861
|
+
let res = await checkAdapter({
|
|
1862
|
+
baseUrl: rest[1],
|
|
1863
|
+
secret,
|
|
1864
|
+
token: secret ? void 0 : token,
|
|
1865
|
+
log: (l) => console.log(l.startsWith("FAIL") ? import_picocolors3.default.red(l) : import_picocolors3.default.green(l))
|
|
1866
|
+
});
|
|
1867
|
+
console.log(res.failed.length ? import_picocolors3.default.red(`${res.failed.length} failed, ${res.passed} passed`) : import_picocolors3.default.green(`All ${res.passed} checks passed. Ready to connect.`)), process.exit(res.failed.length ? 1 : 0);
|
|
1868
|
+
}
|
|
1869
|
+
return console.log(`Usage:
|
|
1870
|
+
lopebase adapter secret print a new signing secret
|
|
1871
|
+
lopebase adapter check <base-url> test a running adapter (reads LOPEBASE_SIGNING_SECRET)`), process.exit(sub ? 1 : 0);
|
|
1872
|
+
}
|
|
1563
1873
|
case "mcp": {
|
|
1564
1874
|
let auth = loadAuth();
|
|
1565
1875
|
if (!auth)
|
|
@@ -1567,7 +1877,7 @@ async function main() {
|
|
|
1567
1877
|
return runMcpProxy(auth);
|
|
1568
1878
|
}
|
|
1569
1879
|
default:
|
|
1570
|
-
console.log(
|
|
1880
|
+
console.log(HELP), process.exit(command === "--help" || command === "-h" || command === "help" ? 0 : 1);
|
|
1571
1881
|
}
|
|
1572
1882
|
}
|
|
1573
1883
|
main();
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lopebase",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Set up
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Set up LopeBase from your project's folder: scan your stack, connect your workspace, and let your coding agent do the rest.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"lopebase": "dist/lopebase.js"
|
|
@@ -25,25 +25,35 @@
|
|
|
25
25
|
"dev": "bun bin/lopebase.ts"
|
|
26
26
|
},
|
|
27
27
|
"devDependencies": {
|
|
28
|
+
"@clack/prompts": "^0.11.0",
|
|
29
|
+
"@lopebase/adapter": "workspace:*",
|
|
28
30
|
"@types/node": "^22.0.0",
|
|
31
|
+
"picocolors": "^1.1.1",
|
|
29
32
|
"typescript": "~5.7.0",
|
|
30
|
-
"vitest": "^4.0.0"
|
|
31
|
-
"@clack/prompts": "^0.11.0",
|
|
32
|
-
"picocolors": "^1.1.1"
|
|
33
|
+
"vitest": "^4.0.0"
|
|
33
34
|
},
|
|
34
35
|
"license": "UNLICENSED",
|
|
35
36
|
"homepage": "https://lopebase.com",
|
|
36
37
|
"keywords": [
|
|
37
38
|
"lopebase",
|
|
38
|
-
"mcp",
|
|
39
39
|
"cli",
|
|
40
|
+
"mcp",
|
|
40
41
|
"agent",
|
|
41
|
-
"
|
|
42
|
+
"claude-code",
|
|
43
|
+
"codex",
|
|
44
|
+
"onboarding",
|
|
45
|
+
"analytics",
|
|
46
|
+
"search-console",
|
|
47
|
+
"dashboard"
|
|
42
48
|
],
|
|
43
49
|
"engines": {
|
|
44
50
|
"node": ">=18"
|
|
45
51
|
},
|
|
46
52
|
"publishConfig": {
|
|
47
53
|
"access": "public"
|
|
54
|
+
},
|
|
55
|
+
"author": "LopeBase by Jackalope Digital <contact@jackalope.digital> (https://lopebase.com)",
|
|
56
|
+
"bugs": {
|
|
57
|
+
"email": "contact@jackalope.digital"
|
|
48
58
|
}
|
|
49
59
|
}
|