@llamaventures/cli 1.2.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/lib/client.mjs ADDED
@@ -0,0 +1,215 @@
1
+ // Shared HTTP / auth / token helpers for @llamaventures/cli.
2
+ //
3
+ // Used by:
4
+ // - bin/llama.mjs — the CLI command surface
5
+ // - bin/llama-mcp.mjs — the MCP server (forthcoming, v1.1)
6
+ //
7
+ // Zero deps. Lazy I/O — importing this module performs no network or
8
+ // filesystem work; everything happens at first call.
9
+
10
+ import fs from "fs";
11
+ import os from "os";
12
+ import path from "path";
13
+ import { fileURLToPath } from "url";
14
+ import { execFile as _execFile } from "child_process";
15
+ import { promisify } from "util";
16
+
17
+ const execFile = promisify(_execFile);
18
+
19
+ // Package root — the directory that contains package.json. Used to locate
20
+ // bundled assets (AGENT_BRIEFING.md, etc.) regardless of where the CLI
21
+ // runs from. lib/client.mjs sits one level deep, so go up one.
22
+ export const PACKAGE_ROOT = path.resolve(
23
+ path.dirname(fileURLToPath(import.meta.url)),
24
+ ".."
25
+ );
26
+
27
+ /**
28
+ * Read the bundled AGENT_BRIEFING.md and return it as a string.
29
+ * Used by both `llama agent-onboard` (CLI) and the `agent_briefing` MCP
30
+ * prompt — single source of truth.
31
+ */
32
+ export function readBriefing() {
33
+ const briefingPath = path.join(PACKAGE_ROOT, "AGENT_BRIEFING.md");
34
+ try {
35
+ return fs.readFileSync(briefingPath, "utf8");
36
+ } catch {
37
+ return (
38
+ "AGENT_BRIEFING.md not found at " +
39
+ briefingPath +
40
+ ". This shouldn't happen in a published @llamaventures/cli install — " +
41
+ "report at https://github.com/SoujiOkita98/llama-cli/issues."
42
+ );
43
+ }
44
+ }
45
+
46
+ // Canonical entrypoint. `llama-command.onrender.com` also serves the API
47
+ // but its NextAuth callback URL doesn't match, so browser login (needed
48
+ // to mint a token at /settings/tokens) fails there with a server-config
49
+ // error. Always default to canonical — override with $LLAMA_API_URL only
50
+ // if testing.
51
+ export const DEFAULT_BASE_URL = "https://command.llamaventures.vc";
52
+
53
+ // Canonical token location (single line, mode 0600). Aligns with the
54
+ // agent-discovery convention.
55
+ export const TOKEN_DIR = path.join(os.homedir(), ".llama");
56
+ export const TOKEN_FILE = path.join(TOKEN_DIR, "token");
57
+
58
+ // Legacy location used by CLI v0.1. Read for back-compat (silent migrate
59
+ // to canonical on first use); never written for the token, but still the
60
+ // home of the rarely-set `baseUrl` override.
61
+ export const LEGACY_DIR = path.join(os.homedir(), ".llama-command");
62
+ export const LEGACY_FILE = path.join(LEGACY_DIR, "config.json");
63
+
64
+ export function readLegacyConfig() {
65
+ try {
66
+ return JSON.parse(fs.readFileSync(LEGACY_FILE, "utf8"));
67
+ } catch {
68
+ return {};
69
+ }
70
+ }
71
+
72
+ export function writeLegacyConfig(config) {
73
+ fs.mkdirSync(LEGACY_DIR, { recursive: true });
74
+ fs.writeFileSync(LEGACY_FILE, `${JSON.stringify(config, null, 2)}\n`, { mode: 0o600 });
75
+ fs.chmodSync(LEGACY_FILE, 0o600);
76
+ }
77
+
78
+ function migrateLegacyTokenIfNeeded(token) {
79
+ if (fs.existsSync(TOKEN_FILE)) return;
80
+ try {
81
+ fs.mkdirSync(TOKEN_DIR, { recursive: true, mode: 0o700 });
82
+ fs.writeFileSync(TOKEN_FILE, `${token}\n`, { mode: 0o600 });
83
+ fs.chmodSync(TOKEN_FILE, 0o600);
84
+ } catch {
85
+ // Migration is best-effort; the env var / legacy fallback still works.
86
+ }
87
+ }
88
+
89
+ export function readCanonicalToken() {
90
+ try {
91
+ return fs.readFileSync(TOKEN_FILE, "utf8").trim();
92
+ } catch {
93
+ return "";
94
+ }
95
+ }
96
+
97
+ export function writeCanonicalToken(token) {
98
+ fs.mkdirSync(TOKEN_DIR, { recursive: true, mode: 0o700 });
99
+ fs.writeFileSync(TOKEN_FILE, `${token}\n`, { mode: 0o600 });
100
+ fs.chmodSync(TOKEN_FILE, 0o600);
101
+ }
102
+
103
+ export function getBaseUrl() {
104
+ return (process.env.LLAMA_API_URL || readLegacyConfig().baseUrl || DEFAULT_BASE_URL).replace(/\/$/, "");
105
+ }
106
+
107
+ export function getToken() {
108
+ // 1. env var — preferred for CI, cloud agents, sandboxed runners
109
+ if (process.env.LLAMA_TOKEN) return process.env.LLAMA_TOKEN;
110
+
111
+ // 2. canonical file
112
+ const canonical = readCanonicalToken();
113
+ if (canonical) return canonical;
114
+
115
+ // 3. legacy fallback — silently migrate forward so future invocations
116
+ // use the canonical path even if the user never re-runs `token set`.
117
+ const legacy = readLegacyConfig().token;
118
+ if (legacy) {
119
+ migrateLegacyTokenIfNeeded(legacy);
120
+ return legacy;
121
+ }
122
+
123
+ return "";
124
+ }
125
+
126
+ // Try `gcloud auth print-identity-token`. Returns the JWT or null. Zero-config
127
+ // win for any team member who has gcloud + their @llamaventures.vc account
128
+ // already set up — the server's Bearer auth path verifies and auto-creates
129
+ // the user row.
130
+ export async function tryGcloudIdentityToken() {
131
+ try {
132
+ const { stdout } = await execFile("gcloud", ["auth", "print-identity-token"], { timeout: 4000 });
133
+ const t = String(stdout).trim();
134
+ // Crude JWT shape check (header.payload.signature). Avoids passing
135
+ // junk like "ERROR: ..." to the server when gcloud misbehaves.
136
+ return t && t.split(".").length === 3 ? t : null;
137
+ } catch {
138
+ return null;
139
+ }
140
+ }
141
+
142
+ // Build the auth header set. If both Bearer and X-Llama-Token are available,
143
+ // send both — the server tries Bearer first and falls through to
144
+ // X-Llama-Token on verification failure.
145
+ export async function getAuthHeaders() {
146
+ const headers = {};
147
+ const bearer = await tryGcloudIdentityToken();
148
+ if (bearer) headers["Authorization"] = `Bearer ${bearer}`;
149
+ const token = getToken();
150
+ if (token) headers["X-Llama-Token"] = token;
151
+ return headers;
152
+ }
153
+
154
+ // Structured no-credential error. Format is stable so agents can pattern-match
155
+ // `Error[NO_AUTH]` and trigger a recovery flow.
156
+ function noAuthError() {
157
+ return new Error(
158
+ "Error[NO_AUTH]: No Llama Command credentials found.\n" +
159
+ "\n" +
160
+ " Llama Ventures team member?\n" +
161
+ " Quickest: run `gcloud auth login` with your @llamaventures.vc account.\n" +
162
+ " Or: get a token at https://command.llamaventures.vc/settings/tokens, then\n" +
163
+ " llama token set <llc_...> (saved to ~/.llama/token)\n" +
164
+ " or set $LLAMA_TOKEN in your shell env.\n" +
165
+ "\n" +
166
+ " Founder or external visitor (no Llama account)?\n" +
167
+ " Run `llama pitch start --name \"Your Name\" --email \"you@company.com\"`\n" +
168
+ " to chat with our intake agent — no token required."
169
+ );
170
+ }
171
+
172
+ // Structured 401 error after a request was attempted. Means the credentials
173
+ // we sent were rejected (revoked / expired / wrong account).
174
+ function unauthorizedError() {
175
+ return new Error(
176
+ "Error[UNAUTHORIZED]: Server rejected our credentials.\n" +
177
+ " If using gcloud: confirm `gcloud config get account` shows your @llamaventures.vc address.\n" +
178
+ " If using X-Llama-Token: the token may be revoked. Regenerate at\n" +
179
+ " https://command.llamaventures.vc/settings/tokens and run `llama token set <llc_...>`."
180
+ );
181
+ }
182
+
183
+ export async function request(method, endpoint, body) {
184
+ const authHeaders = await getAuthHeaders();
185
+ if (Object.keys(authHeaders).length === 0) throw noAuthError();
186
+ const res = await fetch(`${getBaseUrl()}${endpoint}`, {
187
+ method,
188
+ headers: {
189
+ "Content-Type": "application/json",
190
+ ...authHeaders,
191
+ },
192
+ body: body === undefined ? undefined : JSON.stringify(body),
193
+ });
194
+ if (res.status === 401) throw unauthorizedError();
195
+ const text = await res.text();
196
+ let data;
197
+ try {
198
+ data = text ? JSON.parse(text) : null;
199
+ } catch {
200
+ data = text;
201
+ }
202
+ if (!res.ok) {
203
+ const message = typeof data === "object" && data?.error ? data.error : `HTTP ${res.status}`;
204
+ throw new Error(message);
205
+ }
206
+ return data;
207
+ }
208
+
209
+ export function print(data) {
210
+ if (typeof data === "string") {
211
+ console.log(data);
212
+ } else {
213
+ console.log(JSON.stringify(data, null, 2));
214
+ }
215
+ }
@@ -0,0 +1,386 @@
1
+ // External-agent client for @llamaventures/cli.
2
+ //
3
+ // Talks to /api/external/* endpoints (founder pitch intake) — no Llama
4
+ // token required. Session is bootstrapped via PoW + email/name + cookie;
5
+ // subsequent calls reuse the cookie persisted to ~/.llama/external-session.json.
6
+ //
7
+ // All anti-abuse (PoW age, per-IP/email rate limits, disposable-domain block,
8
+ // global daily caps, message/token caps, idle timeout) is enforced server-side.
9
+ // CLI is a bug-for-bug equivalent of the web /external-agent flow — no extra
10
+ // trust given.
11
+
12
+ import fs from "fs";
13
+ import os from "os";
14
+ import path from "path";
15
+ import crypto from "crypto";
16
+ import { getBaseUrl } from "./client.mjs";
17
+
18
+ const SESSION_DIR = path.join(os.homedir(), ".llama");
19
+ const SESSION_FILE = path.join(SESSION_DIR, "external-session.json");
20
+
21
+ // Server-side proof-of-work prefix. Must agree with
22
+ // llama-command/src/lib/external-pow-client.ts. ~65k iterations average on
23
+ // commodity hardware (~50–500ms in node).
24
+ const POW_DIFFICULTY_PREFIX = "0000";
25
+
26
+ // Server requires ts_rendered to be at least 3s old (anti-replay). We
27
+ // backdate by 4s when computing PoW so the request lands inside the
28
+ // validity window without waiting.
29
+ const POW_BACKDATE_MS = 4_000;
30
+
31
+ // ============================================================
32
+ // Session state — ~/.llama/external-session.json
33
+ // ============================================================
34
+
35
+ export function readExternalSession() {
36
+ try {
37
+ return JSON.parse(fs.readFileSync(SESSION_FILE, "utf8"));
38
+ } catch {
39
+ return null;
40
+ }
41
+ }
42
+
43
+ export function writeExternalSession(session) {
44
+ fs.mkdirSync(SESSION_DIR, { recursive: true, mode: 0o700 });
45
+ fs.writeFileSync(SESSION_FILE, JSON.stringify(session, null, 2), { mode: 0o600 });
46
+ fs.chmodSync(SESSION_FILE, 0o600);
47
+ }
48
+
49
+ export function clearExternalSession() {
50
+ try {
51
+ fs.unlinkSync(SESSION_FILE);
52
+ } catch {
53
+ // already gone — fine
54
+ }
55
+ }
56
+
57
+ // ============================================================
58
+ // Proof-of-Work
59
+ // ============================================================
60
+
61
+ function solvePoW(tsRendered, maxIterations = 1_000_000) {
62
+ let nonce = 0;
63
+ while (nonce < maxIterations) {
64
+ const hash = crypto
65
+ .createHash("sha256")
66
+ .update(`${tsRendered}:${nonce}`)
67
+ .digest("hex");
68
+ if (hash.startsWith(POW_DIFFICULTY_PREFIX)) return String(nonce);
69
+ nonce++;
70
+ }
71
+ throw new Error("Could not find proof-of-work nonce within iteration budget.");
72
+ }
73
+
74
+ // ============================================================
75
+ // Start session
76
+ // ============================================================
77
+
78
+ export async function startExternalSession({ name, email }) {
79
+ if (!name || typeof name !== "string" || name.length > 100) {
80
+ throw new Error("name is required (max 100 chars)");
81
+ }
82
+ if (!email || typeof email !== "string") {
83
+ throw new Error("email is required");
84
+ }
85
+ if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
86
+ throw new Error("email format invalid");
87
+ }
88
+
89
+ const tsRendered = Date.now() - POW_BACKDATE_MS;
90
+ const powNonce = solvePoW(tsRendered);
91
+
92
+ const res = await fetch(`${getBaseUrl()}/api/external/start-session`, {
93
+ method: "POST",
94
+ headers: { "Content-Type": "application/json" },
95
+ body: JSON.stringify({
96
+ name,
97
+ email,
98
+ ts_rendered: tsRendered,
99
+ hp_field: "",
100
+ pow_nonce: powNonce,
101
+ user_agent: "@llamaventures/cli",
102
+ }),
103
+ });
104
+
105
+ if (!res.ok) {
106
+ const text = await res.text();
107
+ // Generic server message — could be PoW fail / disposable email /
108
+ // rate limit / global cap. Server intentionally hides which gate caught.
109
+ throw new Error(
110
+ `Could not start session (HTTP ${res.status}). ${text.slice(0, 200)}\n` +
111
+ ` Common causes: rate limit (5 sessions/IP/day, 3/email/day), ` +
112
+ `disposable email domain blocked, or global daily cap reached.`
113
+ );
114
+ }
115
+
116
+ const data = await res.json();
117
+ if (!data?.session_id) {
118
+ throw new Error("start-session response missing session_id");
119
+ }
120
+
121
+ const session = {
122
+ session_id: data.session_id,
123
+ name,
124
+ email,
125
+ started_at: new Date().toISOString(),
126
+ last_active_at: new Date().toISOString(),
127
+ finalized: false,
128
+ };
129
+ writeExternalSession(session);
130
+ return session;
131
+ }
132
+
133
+ // ============================================================
134
+ // Send message — SSE streaming
135
+ // ============================================================
136
+
137
+ /**
138
+ * Parse SSE events from a fetch ReadableStream. Yields parsed event objects
139
+ * as they arrive. Buffers partial frames across chunks.
140
+ */
141
+ async function* readSseEvents(stream) {
142
+ const reader = stream.getReader();
143
+ const decoder = new TextDecoder();
144
+ let buffer = "";
145
+
146
+ while (true) {
147
+ const { done, value } = await reader.read();
148
+ if (done) {
149
+ // Flush any trailing data
150
+ if (buffer.trim()) {
151
+ const event = parseSseFrame(buffer);
152
+ if (event) yield event;
153
+ }
154
+ return;
155
+ }
156
+ buffer += decoder.decode(value, { stream: true });
157
+
158
+ // SSE frames are separated by \n\n
159
+ let idx;
160
+ while ((idx = buffer.indexOf("\n\n")) !== -1) {
161
+ const frame = buffer.slice(0, idx);
162
+ buffer = buffer.slice(idx + 2);
163
+ const event = parseSseFrame(frame);
164
+ if (event) yield event;
165
+ }
166
+ }
167
+ }
168
+
169
+ function parseSseFrame(frame) {
170
+ const dataLines = frame
171
+ .split("\n")
172
+ .filter((l) => l.startsWith("data:"))
173
+ .map((l) => l.replace(/^data:\s?/, ""));
174
+ if (dataLines.length === 0) return null;
175
+ const payload = dataLines.join("\n");
176
+ try {
177
+ return JSON.parse(payload);
178
+ } catch {
179
+ return null;
180
+ }
181
+ }
182
+
183
+ /**
184
+ * Send a message to the external intake agent. Returns a result object
185
+ * with the collected reply text and finalization state.
186
+ *
187
+ * If `onChunk` is provided, it's called with each text chunk as it arrives
188
+ * (for streaming output to terminal). Returns the same final result.
189
+ */
190
+ export async function sendExternalMessage(message, { attachments, onChunk } = {}) {
191
+ const session = readExternalSession();
192
+ if (!session) {
193
+ throw new Error(
194
+ "No active pitch session. Run `llama pitch start --name \"...\" --email \"...\"` first."
195
+ );
196
+ }
197
+ if (session.finalized) {
198
+ throw new Error(
199
+ "This pitch session is finalized. Run `llama pitch end` to clear it, then `pitch start` for a new one."
200
+ );
201
+ }
202
+
203
+ const res = await fetch(`${getBaseUrl()}/api/external/chat`, {
204
+ method: "POST",
205
+ headers: {
206
+ "Content-Type": "application/json",
207
+ Cookie: `external_session=${session.session_id}`,
208
+ },
209
+ body: JSON.stringify({
210
+ message,
211
+ ...(attachments ? { attachments } : {}),
212
+ }),
213
+ });
214
+
215
+ if (!res.ok) {
216
+ if (res.status === 401) {
217
+ clearExternalSession();
218
+ throw new Error(
219
+ "Session expired or invalid. Run `llama pitch start ...` to start a new one."
220
+ );
221
+ }
222
+ if (res.status === 429) {
223
+ // Cap reached — server has already finalized the session row.
224
+ session.finalized = true;
225
+ writeExternalSession(session);
226
+ throw new Error(
227
+ "Session cap reached (message or token limit). The server has finalized this session. " +
228
+ "Run `llama pitch end` to clear local state, then `pitch start` for a new pitch."
229
+ );
230
+ }
231
+ if (res.status === 503) {
232
+ throw new Error(
233
+ "Llama Ventures intake is at daily capacity — please retry tomorrow."
234
+ );
235
+ }
236
+ const text = await res.text();
237
+ throw new Error(`chat failed (HTTP ${res.status}): ${text.slice(0, 300)}`);
238
+ }
239
+
240
+ const collectedText = [];
241
+ let finalized = false;
242
+ let finalizePayload = null;
243
+ let streamError = null;
244
+
245
+ for await (const event of readSseEvents(res.body)) {
246
+ if (typeof event.text === "string") {
247
+ collectedText.push(event.text);
248
+ if (typeof onChunk === "function") onChunk(event.text);
249
+ }
250
+ if (event.finalize === true) {
251
+ finalized = true;
252
+ finalizePayload = event.payload ?? null;
253
+ }
254
+ if (event.error) {
255
+ streamError = event.error;
256
+ }
257
+ // event.done === true → stream end; outer loop exits naturally
258
+ }
259
+
260
+ // Persist updated state
261
+ session.last_active_at = new Date().toISOString();
262
+ if (finalized) session.finalized = true;
263
+ writeExternalSession(session);
264
+
265
+ if (streamError) {
266
+ throw new Error(`server-side error during chat: ${streamError}`);
267
+ }
268
+
269
+ return {
270
+ text: collectedText.join(""),
271
+ finalized,
272
+ finalize_payload: finalizePayload,
273
+ };
274
+ }
275
+
276
+ // ============================================================
277
+ // Upload file
278
+ // ============================================================
279
+
280
+ const ALLOWED_MIME_BY_EXT = {
281
+ ".pdf": "application/pdf",
282
+ ".pptx":
283
+ "application/vnd.openxmlformats-officedocument.presentationml.presentation",
284
+ ".ppt": "application/vnd.ms-powerpoint",
285
+ ".docx":
286
+ "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
287
+ ".doc": "application/msword",
288
+ ".xlsx":
289
+ "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
290
+ ".xls": "application/vnd.ms-excel",
291
+ ".png": "image/png",
292
+ ".jpg": "image/jpeg",
293
+ ".jpeg": "image/jpeg",
294
+ ".webp": "image/webp",
295
+ ".heic": "image/heic",
296
+ ".heif": "image/heif",
297
+ ".txt": "text/plain",
298
+ ".md": "text/markdown",
299
+ };
300
+
301
+ function guessMimeType(filename) {
302
+ const ext = path.extname(filename).toLowerCase();
303
+ return ALLOWED_MIME_BY_EXT[ext] || "application/octet-stream";
304
+ }
305
+
306
+ export async function uploadExternalFile(filePath) {
307
+ const session = readExternalSession();
308
+ if (!session) {
309
+ throw new Error(
310
+ "No active pitch session. Run `llama pitch start ...` first."
311
+ );
312
+ }
313
+
314
+ if (!fs.existsSync(filePath)) {
315
+ throw new Error(`File not found: ${filePath}`);
316
+ }
317
+
318
+ const fileData = fs.readFileSync(filePath);
319
+ const filename = path.basename(filePath);
320
+ const mimetype = guessMimeType(filename);
321
+
322
+ if (mimetype === "application/octet-stream") {
323
+ throw new Error(
324
+ `File extension not in server allowlist. Supported: ${Object.keys(
325
+ ALLOWED_MIME_BY_EXT
326
+ ).join(", ")}`
327
+ );
328
+ }
329
+
330
+ const formData = new FormData();
331
+ const blob = new Blob([fileData], { type: mimetype });
332
+ formData.append("file", blob, filename);
333
+
334
+ const res = await fetch(`${getBaseUrl()}/api/external/upload`, {
335
+ method: "POST",
336
+ headers: { Cookie: `external_session=${session.session_id}` },
337
+ body: formData,
338
+ });
339
+
340
+ if (!res.ok) {
341
+ if (res.status === 413) {
342
+ throw new Error("File too large (max 50 MB).");
343
+ }
344
+ if (res.status === 415) {
345
+ throw new Error(`MIME type "${mimetype}" not in server allowlist.`);
346
+ }
347
+ if (res.status === 429) {
348
+ throw new Error("Upload cap reached (10 files per session).");
349
+ }
350
+ if (res.status === 401 || res.status === 403) {
351
+ throw new Error(
352
+ "Session expired or inactive. Run `llama pitch start ...` to start a new one."
353
+ );
354
+ }
355
+ const text = await res.text();
356
+ throw new Error(`upload failed (HTTP ${res.status}): ${text.slice(0, 300)}`);
357
+ }
358
+
359
+ return await res.json();
360
+ }
361
+
362
+ // ============================================================
363
+ // Status
364
+ // ============================================================
365
+
366
+ export function getExternalSessionStatus() {
367
+ const session = readExternalSession();
368
+ if (!session) {
369
+ return { active: false };
370
+ }
371
+ const idleMs = Date.now() - new Date(session.last_active_at).getTime();
372
+ const idleMin = Math.floor(idleMs / 60000);
373
+ return {
374
+ active: !session.finalized && idleMin < 30,
375
+ session_id: session.session_id,
376
+ name: session.name,
377
+ email: session.email,
378
+ started_at: session.started_at,
379
+ last_active_at: session.last_active_at,
380
+ idle_minutes: idleMin,
381
+ expired: idleMin >= 30,
382
+ finalized: session.finalized || false,
383
+ };
384
+ }
385
+
386
+ export const EXTERNAL_SESSION_FILE = SESSION_FILE;
package/package.json ADDED
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "@llamaventures/cli",
3
+ "version": "1.2.1",
4
+ "description": "Llama Ventures CLI + MCP server. Internal team tool for command.llamaventures.vc.",
5
+ "type": "module",
6
+ "bin": {
7
+ "llama": "bin/llama.mjs",
8
+ "llama-mcp": "bin/llama-mcp.mjs"
9
+ },
10
+ "files": [
11
+ "bin/",
12
+ "lib/",
13
+ "AGENT_BRIEFING.md",
14
+ "README.md",
15
+ "LICENSE"
16
+ ],
17
+ "engines": {
18
+ "node": ">=18"
19
+ },
20
+ "license": "MIT",
21
+ "homepage": "https://github.com/SoujiOkita98/llama-cli#readme",
22
+ "repository": {
23
+ "type": "git",
24
+ "url": "git+https://github.com/SoujiOkita98/llama-cli.git"
25
+ },
26
+ "bugs": {
27
+ "url": "https://github.com/SoujiOkita98/llama-cli/issues"
28
+ },
29
+ "keywords": [
30
+ "llama-ventures",
31
+ "cli",
32
+ "mcp",
33
+ "modelcontextprotocol"
34
+ ],
35
+ "author": "Llama Ventures, Inc.",
36
+ "publishConfig": {
37
+ "access": "public"
38
+ },
39
+ "dependencies": {
40
+ "@modelcontextprotocol/sdk": "1.29.0"
41
+ }
42
+ }