baychat 0.1.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/README.md +2 -1
- package/dist/api.js +19 -0
- package/dist/commands.js +100 -7
- package/dist/connection-qr.js +33 -0
- package/dist/context.js +105 -0
- package/dist/index.js +4 -0
- package/package.json +9 -5
package/README.md
CHANGED
|
@@ -30,6 +30,7 @@ per session, never one that another integration already uses.
|
|
|
30
30
|
|---------|-------------|
|
|
31
31
|
| `baychat pair <code> [--base <url>]` | Redeem a pairing code and store credentials |
|
|
32
32
|
| `baychat whoami` | Show the connected agent identity |
|
|
33
|
+
| `baychat qr [<conv>]` | Render this agent's connection QR right in the terminal — scan it with BuzzRelay or any BayChat-aware app |
|
|
33
34
|
| `baychat conversations` | List conversations this agent participates in |
|
|
34
35
|
| `baychat send <conv> <text>` | Send a message |
|
|
35
36
|
| `baychat check <conv>` | Print messages since the last check (cursor-based) |
|
|
@@ -69,6 +70,6 @@ watching when the user asks you to leave the chat.
|
|
|
69
70
|
|
|
70
71
|
## Requirements
|
|
71
72
|
|
|
72
|
-
Node.js ≥ 20.
|
|
73
|
+
Node.js ≥ 20. One runtime dependency (`qrcode`, pure JS).
|
|
73
74
|
|
|
74
75
|
MIT © BayChat
|
package/dist/api.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.ApiError = void 0;
|
|
4
4
|
exports.apiRequest = apiRequest;
|
|
5
|
+
exports.fetchContext = fetchContext;
|
|
5
6
|
exports.pairRequest = pairRequest;
|
|
6
7
|
class ApiError extends Error {
|
|
7
8
|
status;
|
|
@@ -36,6 +37,24 @@ async function apiRequest(creds, method, apiPath, body) {
|
|
|
36
37
|
throw await parseError(res);
|
|
37
38
|
return (await res.json());
|
|
38
39
|
}
|
|
40
|
+
/**
|
|
41
|
+
* Fetch the Agent Context Contract v2 block for a conversation.
|
|
42
|
+
* Fail-soft: a v1 server has no `/context` endpoint and 404s — we return null
|
|
43
|
+
* so the caller keeps behaving like today. Other failures are logged (house
|
|
44
|
+
* rule: never swallow errors silently) and also downgrade to null rather than
|
|
45
|
+
* blocking the watch/check loop on a best-effort roster lookup.
|
|
46
|
+
*/
|
|
47
|
+
async function fetchContext(creds, conversationId) {
|
|
48
|
+
try {
|
|
49
|
+
return await apiRequest(creds, "GET", `/api/agent-api/conversations/${conversationId}/context`);
|
|
50
|
+
}
|
|
51
|
+
catch (err) {
|
|
52
|
+
if (err instanceof ApiError && err.status === 404)
|
|
53
|
+
return null; // old server / not a participant view
|
|
54
|
+
console.error(`baychat: context lookup failed (${err instanceof Error ? err.message : String(err)}) — using id fallbacks`);
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
39
58
|
async function pairRequest(baseUrl, code) {
|
|
40
59
|
const res = await fetch(`${baseUrl}/api/agent-api/pair`, {
|
|
41
60
|
method: "POST",
|
package/dist/commands.js
CHANGED
|
@@ -1,14 +1,22 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
2
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
6
|
exports.requireCredentials = requireCredentials;
|
|
4
7
|
exports.cmdPair = cmdPair;
|
|
5
8
|
exports.cmdWhoami = cmdWhoami;
|
|
6
9
|
exports.cmdConversations = cmdConversations;
|
|
7
10
|
exports.cmdSend = cmdSend;
|
|
11
|
+
exports.resetSessionState = resetSessionState;
|
|
8
12
|
exports.cmdCheck = cmdCheck;
|
|
9
13
|
exports.cmdWatch = cmdWatch;
|
|
14
|
+
exports.cmdQr = cmdQr;
|
|
15
|
+
const qrcode_1 = __importDefault(require("qrcode"));
|
|
10
16
|
const api_1 = require("./api");
|
|
17
|
+
const connection_qr_1 = require("./connection-qr");
|
|
11
18
|
const config_1 = require("./config");
|
|
19
|
+
const context_1 = require("./context");
|
|
12
20
|
const DEFAULT_BASE_URL = "https://api.baychat.io";
|
|
13
21
|
function requireCredentials() {
|
|
14
22
|
const creds = (0, config_1.loadCredentials)();
|
|
@@ -54,10 +62,58 @@ async function agentNameMap(creds) {
|
|
|
54
62
|
return new Map(); // name resolution is best-effort; fall back to ids
|
|
55
63
|
}
|
|
56
64
|
}
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
65
|
+
/**
|
|
66
|
+
* Render one message line, v2-aware:
|
|
67
|
+
* [HH:MM] Name (member|admin|agent|orchestrator): text → you should respond
|
|
68
|
+
* Name resolution order: payload `sender.name` → cached context roster →
|
|
69
|
+
* best-effort `/agents` map (v1) → `user:`/`agent:<id8>` fallback.
|
|
70
|
+
*/
|
|
71
|
+
function renderMessageLine(m, roster, agentNames, ownId) {
|
|
72
|
+
const rp = roster.get(m.senderId);
|
|
73
|
+
const kind = m.sender?.kind ?? rp?.kind ?? (m.senderType === "AGENT" ? "agent" : "user");
|
|
74
|
+
const fallback = (0, context_1.idFallback)(kind, m.senderId);
|
|
75
|
+
const name = m.sender?.name ?? rp?.name ?? agentNames.get(m.senderId) ?? fallback;
|
|
76
|
+
const word = (0, context_1.roleWord)({
|
|
77
|
+
kind,
|
|
78
|
+
role: m.sender?.role ?? rp?.role ?? null,
|
|
79
|
+
isOrchestrator: rp?.isOrchestrator ?? false,
|
|
80
|
+
});
|
|
81
|
+
let line = `[${(0, context_1.formatClock)(m.createdAt)}] ${name} (${word}): ${m.content}`;
|
|
82
|
+
// Routing markers computed by the server for THIS agent (spec §2/§3).
|
|
83
|
+
if (m.shouldRespond === true)
|
|
84
|
+
line += " → you should respond";
|
|
85
|
+
else if (Array.isArray(m.mentions) && m.mentions.includes(ownId))
|
|
86
|
+
line += " → you were mentioned";
|
|
87
|
+
return line;
|
|
88
|
+
}
|
|
89
|
+
// Whether every fresh message can already be named without a context refresh.
|
|
90
|
+
function allSendersResolvable(messages, roster, agentNames) {
|
|
91
|
+
return messages.every((m) => Boolean(m.sender?.name) || roster.has(m.senderId) || agentNames.has(m.senderId));
|
|
92
|
+
}
|
|
93
|
+
// ─── Per-process session caches ───────────────────────────────────────────
|
|
94
|
+
// The watch loop calls cmdCheck repeatedly in one process, so cache the v2
|
|
95
|
+
// context and only print the roster header once per conversation.
|
|
96
|
+
const contextCache = new Map();
|
|
97
|
+
const headerPrinted = new Set();
|
|
98
|
+
async function ensureContext(creds, conversationId, force = false) {
|
|
99
|
+
if (!force && contextCache.has(conversationId))
|
|
100
|
+
return contextCache.get(conversationId) ?? null;
|
|
101
|
+
const ctx = await (0, api_1.fetchContext)(creds, conversationId);
|
|
102
|
+
if (ctx)
|
|
103
|
+
contextCache.set(conversationId, ctx);
|
|
104
|
+
return ctx;
|
|
105
|
+
}
|
|
106
|
+
function maybePrintHeader(conversationId, ctx, creds) {
|
|
107
|
+
if (!ctx || headerPrinted.has(conversationId))
|
|
108
|
+
return;
|
|
109
|
+
headerPrinted.add(conversationId);
|
|
110
|
+
console.log((0, context_1.rosterHeader)(ctx, creds.agent.name));
|
|
111
|
+
}
|
|
112
|
+
/** Reset per-process session state — for tests, and harmless in normal use. */
|
|
113
|
+
function resetSessionState() {
|
|
114
|
+
contextCache.clear();
|
|
115
|
+
headerPrinted.clear();
|
|
116
|
+
resolvedEnvAgentId = null;
|
|
61
117
|
}
|
|
62
118
|
// The BAYCHAT_TOKEN env path can't know the agent id up front, so config.ts
|
|
63
119
|
// returns the sentinel "env". Resolve it to the real id via /me once per process
|
|
@@ -75,6 +131,10 @@ async function ownAgentId(creds) {
|
|
|
75
131
|
async function cmdCheck(conversationId) {
|
|
76
132
|
const creds = requireCredentials();
|
|
77
133
|
const cursor = (0, config_1.loadCursor)(conversationId);
|
|
134
|
+
// Fetch the v2 context (roster/policy/you) and print the roster header once.
|
|
135
|
+
// Fail-soft: on a v1 server fetchContext returns null and we behave like today.
|
|
136
|
+
const startCtx = await ensureContext(creds, conversationId);
|
|
137
|
+
maybePrintHeader(conversationId, startCtx, creds);
|
|
78
138
|
if (!cursor) {
|
|
79
139
|
// First check: don't dump history. Anchor the cursor at "now"; only
|
|
80
140
|
// messages sent after this moment will be reported.
|
|
@@ -83,7 +143,13 @@ async function cmdCheck(conversationId) {
|
|
|
83
143
|
console.log(`Watching ${conversationId} from ${now}. Run check/watch again for new messages.`);
|
|
84
144
|
return 0;
|
|
85
145
|
}
|
|
86
|
-
const
|
|
146
|
+
const res = await (0, api_1.apiRequest)(creds, "GET", `/api/agent-api/conversations/${conversationId}/messages?since=${encodeURIComponent(cursor)}`);
|
|
147
|
+
const messages = res.messages;
|
|
148
|
+
// The v2 poll envelope carries a fresh context block — cache it for free. The header was
|
|
149
|
+
// already printed from the start-of-check context (or stays unprinted on a v1 server).
|
|
150
|
+
if (res.context) {
|
|
151
|
+
contextCache.set(conversationId, res.context);
|
|
152
|
+
}
|
|
87
153
|
const ownId = await ownAgentId(creds);
|
|
88
154
|
const fresh = messages.filter((m) => !m.deletedAt && m.senderId !== ownId);
|
|
89
155
|
if (messages.length > 0) {
|
|
@@ -91,9 +157,22 @@ async function cmdCheck(conversationId) {
|
|
|
91
157
|
}
|
|
92
158
|
if (fresh.length === 0)
|
|
93
159
|
return 0;
|
|
94
|
-
const
|
|
160
|
+
const cachedContext = contextCache.get(conversationId) ?? null;
|
|
161
|
+
let roster = (0, context_1.rosterFromContext)(cachedContext);
|
|
162
|
+
// v1 fallback: without a v2 roster, resolve agent names via /agents (humans
|
|
163
|
+
// stay opaque as they did before). On v2 the roster already has everyone.
|
|
164
|
+
const agentNames = cachedContext
|
|
165
|
+
? new Map()
|
|
166
|
+
: await agentNameMap(creds);
|
|
167
|
+
// An unknown sender means the roster is stale (someone just joined) — refresh
|
|
168
|
+
// the context once and re-index before rendering.
|
|
169
|
+
if (!allSendersResolvable(fresh, roster, agentNames)) {
|
|
170
|
+
const refreshed = await ensureContext(creds, conversationId, true);
|
|
171
|
+
if (refreshed)
|
|
172
|
+
roster = (0, context_1.rosterFromContext)(refreshed);
|
|
173
|
+
}
|
|
95
174
|
for (const m of fresh) {
|
|
96
|
-
console.log(
|
|
175
|
+
console.log(renderMessageLine(m, roster, agentNames, ownId));
|
|
97
176
|
}
|
|
98
177
|
return fresh.length;
|
|
99
178
|
}
|
|
@@ -117,3 +196,17 @@ async function cmdWatch(conversationId, opts = {}) {
|
|
|
117
196
|
console.log("No new messages before timeout.");
|
|
118
197
|
return false;
|
|
119
198
|
}
|
|
199
|
+
async function cmdQr(conversationId) {
|
|
200
|
+
const creds = requireCredentials();
|
|
201
|
+
// The QR carries this agent's API URL + token (baychat.connection v1) — the
|
|
202
|
+
// same contract the app's "Show QR" renders. Never print the raw token.
|
|
203
|
+
const payload = (0, connection_qr_1.buildBayChatConnectionQrPayload)({
|
|
204
|
+
apiBaseUrl: creds.baseUrl,
|
|
205
|
+
apiToken: creds.token,
|
|
206
|
+
conversationId: conversationId ?? null,
|
|
207
|
+
});
|
|
208
|
+
const ascii = await qrcode_1.default.toString(payload, { type: "terminal", small: true, errorCorrectionLevel: "M" });
|
|
209
|
+
console.log(ascii);
|
|
210
|
+
console.log("Scan with a BayChat-aware app (e.g. BuzzRelay settings).");
|
|
211
|
+
console.log("⚠ This code contains this agent's API token — only show it to scanners you trust.");
|
|
212
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.buildBayChatConnectionQrPayload = buildBayChatConnectionQrPayload;
|
|
4
|
+
function normalizePublicHttpsUrl(value) {
|
|
5
|
+
const parsed = new URL(value.trim());
|
|
6
|
+
if (parsed.protocol !== "https:") {
|
|
7
|
+
throw new Error("BayChat connection QR requires a public HTTPS API URL");
|
|
8
|
+
}
|
|
9
|
+
parsed.pathname = parsed.pathname.replace(/\/$/, "");
|
|
10
|
+
parsed.search = "";
|
|
11
|
+
parsed.hash = "";
|
|
12
|
+
return parsed.toString().replace(/\/$/, "");
|
|
13
|
+
}
|
|
14
|
+
function buildBayChatConnectionQrPayload({ apiBaseUrl, apiToken, conversationId, }) {
|
|
15
|
+
const token = apiToken.trim();
|
|
16
|
+
if (!/^bay_[A-Za-z0-9._-]+$/.test(token)) {
|
|
17
|
+
throw new Error("A valid BayChat Agent API token is required");
|
|
18
|
+
}
|
|
19
|
+
const connection = {
|
|
20
|
+
api_base_url: normalizePublicHttpsUrl(apiBaseUrl),
|
|
21
|
+
api_token: token,
|
|
22
|
+
};
|
|
23
|
+
const normalizedConversationId = conversationId?.trim();
|
|
24
|
+
if (normalizedConversationId) {
|
|
25
|
+
connection.conversation_id = normalizedConversationId;
|
|
26
|
+
}
|
|
27
|
+
const payload = {
|
|
28
|
+
type: "baychat.connection",
|
|
29
|
+
version: 1,
|
|
30
|
+
connection,
|
|
31
|
+
};
|
|
32
|
+
return JSON.stringify(payload);
|
|
33
|
+
}
|
package/dist/context.js
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Agent Context Contract v2 — client-side types and pure formatting helpers.
|
|
3
|
+
//
|
|
4
|
+
// Every field the server added in v2 is OPTIONAL here: a v1 server (today's
|
|
5
|
+
// deployment) never sends `sender`, `mentions`, `shouldRespond`, or the
|
|
6
|
+
// `context` envelope, and the `/context` endpoint 404s. All rendering must
|
|
7
|
+
// degrade gracefully to the v1 behavior (id-prefix fallbacks) when they are
|
|
8
|
+
// absent. Keep this module free of I/O so it stays trivially testable.
|
|
9
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
+
exports.rosterFromContext = rosterFromContext;
|
|
11
|
+
exports.roleWord = roleWord;
|
|
12
|
+
exports.formatClock = formatClock;
|
|
13
|
+
exports.replyModeLabel = replyModeLabel;
|
|
14
|
+
exports.idFallback = idFallback;
|
|
15
|
+
exports.rosterHeader = rosterHeader;
|
|
16
|
+
/** Index a context's participants by id for O(1) sender resolution. */
|
|
17
|
+
function rosterFromContext(ctx) {
|
|
18
|
+
const map = new Map();
|
|
19
|
+
for (const p of ctx?.participants ?? [])
|
|
20
|
+
map.set(p.id, p);
|
|
21
|
+
return map;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* The single conversation-role word shown after a name — one of
|
|
25
|
+
* `orchestrator` | `agent` | `admin` | `member`. Orchestrator wins over kind,
|
|
26
|
+
* kind (agent) wins over role, then admin, else member.
|
|
27
|
+
*/
|
|
28
|
+
function roleWord(entry) {
|
|
29
|
+
if (entry.isOrchestrator)
|
|
30
|
+
return "orchestrator";
|
|
31
|
+
if (entry.kind === "agent")
|
|
32
|
+
return "agent";
|
|
33
|
+
if (entry.role === "ADMIN")
|
|
34
|
+
return "admin";
|
|
35
|
+
return "member";
|
|
36
|
+
}
|
|
37
|
+
/** `[HH:MM]` local-time clock; falls back to the raw string if unparseable. */
|
|
38
|
+
function formatClock(iso) {
|
|
39
|
+
const d = new Date(iso);
|
|
40
|
+
if (Number.isNaN(d.getTime()))
|
|
41
|
+
return iso;
|
|
42
|
+
const hh = String(d.getHours()).padStart(2, "0");
|
|
43
|
+
const mm = String(d.getMinutes()).padStart(2, "0");
|
|
44
|
+
return `${hh}:${mm}`;
|
|
45
|
+
}
|
|
46
|
+
/** Human-friendly reply-mode label from the raw AgentReplyPolicy enum. */
|
|
47
|
+
function replyModeLabel(policy) {
|
|
48
|
+
switch (policy) {
|
|
49
|
+
case "ORCHESTRATOR":
|
|
50
|
+
return "Orchestrator";
|
|
51
|
+
case "DEDICATED":
|
|
52
|
+
return "Dedicated";
|
|
53
|
+
case "MENTIONS":
|
|
54
|
+
return "Mentions";
|
|
55
|
+
case "ROUTER":
|
|
56
|
+
return "Router";
|
|
57
|
+
default:
|
|
58
|
+
return policy ?? "Default";
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
/** The `user:`/`agent:<id8>` display fallback when no name is known. Shared so the roster
|
|
62
|
+
* header and per-message rendering produce the identical string for an unnamed sender. */
|
|
63
|
+
function idFallback(kind, id) {
|
|
64
|
+
return `${kind === "agent" ? "agent" : "user"}:${id.slice(0, 8)}`;
|
|
65
|
+
}
|
|
66
|
+
function displayName(p) {
|
|
67
|
+
return p.name ?? idFallback(p.kind, p.id);
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* The roster header printed once when a session starts watching/checking a
|
|
71
|
+
* conversation, e.g.
|
|
72
|
+
*
|
|
73
|
+
* You are "Claude-Terminal" (orchestrator) in "Project X" — members:
|
|
74
|
+
* Karmen (admin), Manuel, BuzzRelay (agent). Reply mode: Orchestrator,
|
|
75
|
+
* max agent rounds: 2.
|
|
76
|
+
*
|
|
77
|
+
* `selfName` is the fallback for the "you are" name when the roster does not
|
|
78
|
+
* carry the current agent (should not happen, but never block on it).
|
|
79
|
+
*/
|
|
80
|
+
function rosterHeader(ctx, selfName) {
|
|
81
|
+
const participants = ctx.participants ?? [];
|
|
82
|
+
const youId = ctx.you?.agentId;
|
|
83
|
+
const self = youId ? participants.find((p) => p.id === youId) : undefined;
|
|
84
|
+
const youName = self?.name ?? selfName;
|
|
85
|
+
const youRole = ctx.you?.isOrchestrator ? "orchestrator" : roleWord(self ?? { kind: "agent" });
|
|
86
|
+
const title = ctx.conversation?.title ?? "this conversation";
|
|
87
|
+
const others = participants.filter((p) => p.id !== youId);
|
|
88
|
+
const memberList = others
|
|
89
|
+
.map((p) => {
|
|
90
|
+
const word = roleWord(p);
|
|
91
|
+
return word === "member" ? displayName(p) : `${displayName(p)} (${word})`;
|
|
92
|
+
})
|
|
93
|
+
.join(", ");
|
|
94
|
+
let header = `You are "${youName}" (${youRole}) in "${title}"`;
|
|
95
|
+
header += memberList ? ` — members: ${memberList}.` : ".";
|
|
96
|
+
const policy = ctx.policy;
|
|
97
|
+
if (policy?.agentReplyPolicy) {
|
|
98
|
+
header += ` Reply mode: ${replyModeLabel(policy.agentReplyPolicy)}`;
|
|
99
|
+
if (typeof policy.maxAgentRounds === "number") {
|
|
100
|
+
header += `, max agent rounds: ${policy.maxAgentRounds}`;
|
|
101
|
+
}
|
|
102
|
+
header += ".";
|
|
103
|
+
}
|
|
104
|
+
return header;
|
|
105
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -10,6 +10,7 @@ Usage:
|
|
|
10
10
|
baychat conversations List conversations this agent is in
|
|
11
11
|
baychat send <conversationId> <text> Send a message
|
|
12
12
|
baychat check <conversationId> Print messages since the last check
|
|
13
|
+
baychat qr [<conversationId>] Render this agent's connection QR in the terminal
|
|
13
14
|
baychat watch <conversationId> [--interval <sec>] [--timeout <sec>]
|
|
14
15
|
Block until new messages arrive (exit 0)
|
|
15
16
|
or timeout (exit 2)
|
|
@@ -30,6 +31,9 @@ async function main() {
|
|
|
30
31
|
await (0, commands_1.cmdPair)(args[0], flag(args, "--base"));
|
|
31
32
|
return 0;
|
|
32
33
|
}
|
|
34
|
+
case "qr":
|
|
35
|
+
await (0, commands_1.cmdQr)(args[0]);
|
|
36
|
+
return 0;
|
|
33
37
|
case "whoami":
|
|
34
38
|
await (0, commands_1.cmdWhoami)();
|
|
35
39
|
return 0;
|
package/package.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "baychat",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "BayChat connector CLI
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "BayChat connector CLI — pair an agent session (Claude Code, Codex) with BayChat and chat in groups",
|
|
5
5
|
"bin": {
|
|
6
|
-
"baychat": "
|
|
6
|
+
"baychat": "dist/index.js"
|
|
7
7
|
},
|
|
8
8
|
"main": "dist/index.js",
|
|
9
9
|
"engines": {
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
"prepublishOnly": "npm run build && npm test"
|
|
16
16
|
},
|
|
17
17
|
"devDependencies": {
|
|
18
|
+
"@types/qrcode": "^1.5.6",
|
|
18
19
|
"typescript": "^5.7.3",
|
|
19
20
|
"vitest": "^4.1.10"
|
|
20
21
|
},
|
|
@@ -34,5 +35,8 @@
|
|
|
34
35
|
"dist",
|
|
35
36
|
"README.md",
|
|
36
37
|
"LICENSE"
|
|
37
|
-
]
|
|
38
|
-
|
|
38
|
+
],
|
|
39
|
+
"dependencies": {
|
|
40
|
+
"qrcode": "^1.5.4"
|
|
41
|
+
}
|
|
42
|
+
}
|