@llamaventures/cli 1.20.0 → 1.22.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/AGENT_BRIEFING.md CHANGED
@@ -8,6 +8,7 @@ You are not just an AI assistant. You're an **extension of a team member** — w
8
8
 
9
9
  - **Your access scope is whatever your token allows.** Run `llama auth status` first; the response shows your role, identity, and active token source.
10
10
  - **All your writes are logged.** `auth_events` and `deal_events` capture everything. Pipeline data can always be traced back to who/what changed it.
11
+ - **Never cue by implication.** A teammate cue creates an LC inbox item and may queue email under the user's identity. Cue a specific person only when the user explicitly authorized it in the current request. Referring to a teammate, ownership, responsibility, or writing their name is not permission.
11
12
  - **Public surfaces stay clean.** Anything that leaves the workspace — public repos, npm packages, external artifacts, shared links — must not leak internal people, deals, private URLs, or workflow nuance.
12
13
  - **Be direct, terse, action-oriented.** Save your words for the genuine judgment calls.
13
14
  - **Critical when thinking, helpful when executing.** Push back on weak logic, then ship the work cleanly.
@@ -133,7 +134,7 @@ These three look similar but land in different surfaces. Don't infer from the co
133
134
  |---|---|---|
134
135
  | File a **source packet** with multiple facts and/or one note | `llama deal ingest <dealId> --file packet.json` | Facts + optional note → deal **Feed**, atomically and retry-safe |
135
136
  | Record a **sourced, verifiable fact** | `llama deal fact add <dealId> --category <cat> --claim "…" --source "deck p3" --source-url <url>` | Facts → deal **Feed** (FACT card) + citable in the **Memo** |
136
- | Leave a **comment / opinion / question / reaction** for the team | `llama post <dealId> "…"` (`@name` to notify) | Posts → deal **Feed** (POST card); `@mention` fires email + UI badge |
137
+ | Leave a **comment / opinion / question / reaction** for the team | `llama post <dealId> "…"`; add `--cue` only after explicit permission | Posts → deal **Feed** (POST card); authorized cues create email + UI badge |
137
138
  | Write **narrative that belongs in the IC memo** | `llama brief add-text <dealId> --heading "…" --body "…"` | Brief blocks → **Memo tab only — NOT in the Feed** |
138
139
 
139
140
  ⚠️ The trap: `brief add-text` is **not** visible in the Activity Feed. If the team should see it in the feed, use `llama post`. If it's a claim that needs a source + verification, use `llama deal fact add`. (It's `deal fact add`, not `fact-add`.)
@@ -149,7 +150,7 @@ The table below details the exact CLI for each destination.
149
150
  | Insights, decisions, framework improvements | Wiki (markdown) | `llama wiki save <slug> --content "..."` (with attribution — see below) |
150
151
  | **HTML wiki entry — standalone HTML page hosted at `/wiki/<slug>`** (sector landscape, market map, dashboard, hand-styled thesis page) | **Wiki (HTML)** | `llama wiki save <slug> --title "..." --file <path.html> --sources "..."`. Auto-detects content_type=html from extension. Public page is full-viewport sandboxed iframe takeover (no wiki chrome). Sources/status/title still required; appears in `wiki search` + backlinks. Use when the user says "deploy this HTML to wiki", "wiki 词条", "make this page a wiki entry". HTML must be self-contained (inline CSS/JS, image data URIs or external URLs) — asset bundles aren't supported on wiki yet. **Native comments + working in-page (#) anchor links are injected automatically** — readers discuss inline and the table of contents scrolls; you don't wire anything up (pages that already embed the comment widget are left as-is). |
151
152
  | Large files (deck / PDF / transcript) | Drive deal folder | the deal's `folder_url` (from `llama deal show`) → upload via your filesystem / Drive tool |
152
- | Cross-team mentions | Inbox + email | `llama post <dealId> "@<teammate> ..."` — server fires email + UI badge to the recipient |
153
+ | Cross-team cues | Inbox + email | `llama post <dealId> "@<teammate> ..." --cue` — use `--cue` only after the user explicitly authorized that recipient |
153
154
 
154
155
  ### Attribution format (required for wiki writes)
155
156
 
@@ -167,8 +168,9 @@ Content. One block, one attribution. Don't mix fact and opinion in a single bloc
167
168
  | Level | Type | Behaviour |
168
169
  |---|---|---|
169
170
  | **L0** | Reads (`search`, `show`, `list`) | Just do it. Don't announce. Integrate the result into your reply. |
170
- | **L1** | Low-risk writes (timeline post, wiki append, add fact, add tag) | Do it, then tell the user **one line** afterward. |
171
+ | **L1** | Low-risk writes (cue-free timeline post, wiki append, add fact, add tag) | Do it, then tell the user **one line** afterward. |
171
172
  | **L2** | Medium-risk writes (new deal, change stage, change owner, new wiki page) | Ask once: "Y/n — I'm about to do X". On yes, execute and report. Don't re-ask details. |
173
+ | **L2-cue** | Any assistant-authored write that resolves to a teammate cue | Require explicit permission for the resolved people and inbox/email channels. Retry the exact write with `--cue` only after yes. If the user already explicitly asked to cue them, do not ask twice. |
172
174
  | **L3** | High-risk (delete deal, bulk change, overwrite someone else's wiki, force-push, regulatory-relevant) | Detailed explanation + explicit confirmation. Provide a dry-run / undo path when possible. |
173
175
 
174
176
  When in doubt, lean to a higher level (more confirmation), not lower.
@@ -290,7 +292,8 @@ llama wiki restore <slug> [--lang en|zh]
290
292
 
291
293
  # Timeline + posts
292
294
  llama timeline <dealId>
293
- llama post <dealId> "message"
295
+ llama post <dealId> "message" # cue-free note
296
+ llama post <dealId> "@name respond" --cue # only after explicit user approval
294
297
 
295
298
  # Mentions inbox
296
299
  llama mentions
package/CHANGELOG.md CHANGED
@@ -6,6 +6,35 @@ this project adheres to [Semantic Versioning](https://semver.org).
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [1.22.1] - 2026-08-04
10
+
11
+ ### Changed
12
+ - Refresh the transitive `ip-address` dependency from 10.2.0 to 10.4.0 and
13
+ retain the same CLI and MCP behavior across the supported Node 18/20/22
14
+ matrix.
15
+
16
+ ## [1.22.0] - 2026-07-28
17
+
18
+ ### Added
19
+ - Consume Llama Command's server-owned CLI/MCP compatibility headers on every
20
+ shared API request. Recommended upgrades are surfaced on stderr at most once
21
+ per day, including for non-TTY coding agents, while structured stdout remains
22
+ unchanged.
23
+ - Surface hard `CLI_VERSION_UNSUPPORTED` responses with a stable error code,
24
+ exact upgrade command, and retry instruction.
25
+
26
+ ## [1.21.0] - 2026-07-24
27
+
28
+ ### Added
29
+ - Add explicit `--cue` (CLI) and `cueAuthorized` (MCP) authorization for
30
+ teammate cues in posts and brief writes. Cue-free writes remain unchanged;
31
+ agents must obtain permission for the resolved recipients before setting it.
32
+
33
+ ### Changed
34
+ - Agent onboarding now treats teammate cues as approval-required outbound
35
+ actions rather than ordinary low-risk timeline posts. Structured
36
+ `MENTION_APPROVAL_REQUIRED` errors retain recipient and channel context.
37
+
9
38
  ## [1.20.0] - 2026-07-22
10
39
 
11
40
  ### Added
@@ -544,7 +573,9 @@ assistant-drafted (🤖) and enforces the AI trust ceiling.
544
573
 
545
574
  ---
546
575
 
547
- [Unreleased]: https://github.com/Llama-Ventures/llama-cli/compare/v1.18.1...HEAD
576
+ [Unreleased]: https://github.com/Llama-Ventures/llama-cli/compare/v1.22.0...HEAD
577
+ [1.22.0]: https://github.com/Llama-Ventures/llama-cli/compare/v1.21.0...v1.22.0
578
+ [1.21.0]: https://github.com/Llama-Ventures/llama-cli/compare/v1.20.0...v1.21.0
548
579
  [1.18.1]: https://github.com/Llama-Ventures/llama-cli/compare/v1.18.0...v1.18.1
549
580
  [1.18.0]: https://github.com/Llama-Ventures/llama-cli/compare/v1.17.3...v1.18.0
550
581
  [1.17.3]: https://github.com/Llama-Ventures/llama-cli/compare/v1.17.2...v1.17.3
package/README.md CHANGED
@@ -102,6 +102,7 @@ llama deal ingest <dealId> --file packet.json # atomic multi-fact + optional Fe
102
102
  llama deal fact add <dealId> --category funding --claim "Raised a seed round" --source "deck p3" --source-url https://...
103
103
  llama deal update <dealId> status Diligence
104
104
  llama post <dealId> "note body"
105
+ llama post <dealId> "@name please respond" --cue # only after explicit approval
105
106
  llama brief add-text <dealId> --heading "..." --body "..."
106
107
  llama wiki search "<query>"
107
108
  llama wiki save <slug> --title "..." --content "..."
package/README.zh-CN.md CHANGED
@@ -124,6 +124,7 @@ llama deal ingest <dealId> --file packet.json # 多条 facts + 可选 Feed note
124
124
  llama deal fact add <dealId> --category funding --claim "Raised a seed round" --source "deck p3" --source-url https://...
125
125
  llama deal update <dealId> status Diligence
126
126
  llama post <dealId> "备注内容"
127
+ llama post <dealId> "@name 请回复" --cue # 仅在用户明确授权后使用
127
128
  llama brief add-text <dealId> --heading "..." --body "..."
128
129
  llama wiki search "<查询词>"
129
130
  llama wiki save <slug> --title "..." --content "..."
package/bin/llama-mcp.mjs CHANGED
@@ -161,7 +161,7 @@ async function runDealAgentTool({ dealId, message, title = "MCP agent run" }) {
161
161
  // full-array PUTs (no POST), so we GET current blocks, prepend the new
162
162
  // one (matches UI default since 2026-05-03), and PUT the merged array.
163
163
  // Server stamps identity meta on PUT; we don't send any.
164
- async function addBriefBlock(dealId, block) {
164
+ async function addBriefBlock(dealId, block, cueAuthorized = false) {
165
165
  try {
166
166
  const id = globalThis.crypto.randomUUID();
167
167
  const cur = await request("GET", `/api/deals/${encodeURIComponent(dealId)}/blocks`);
@@ -169,7 +169,7 @@ async function addBriefBlock(dealId, block) {
169
169
  const result = await request(
170
170
  "PUT",
171
171
  `/api/deals/${encodeURIComponent(dealId)}/blocks`,
172
- { blocks: [{ id, ...block }, ...existing] }
172
+ { blocks: [{ id, ...block }, ...existing], cue_authorized: cueAuthorized === true }
173
173
  );
174
174
  const text = JSON.stringify(
175
175
  { ok: result?.ok ?? true, id, count: result?.count ?? existing.length + 1 },
@@ -681,10 +681,13 @@ server.registerTool(
681
681
  dealId: z.string(),
682
682
  heading: z.string().optional().describe("optional block heading"),
683
683
  body: z.string().describe("markdown body"),
684
+ cueAuthorized: z.boolean().optional().describe(
685
+ "true only after the user explicitly approved every resolved cue recipient",
686
+ ),
684
687
  },
685
688
  },
686
- async ({ dealId, heading, body }) =>
687
- addBriefBlock(dealId, { type: "text", heading: heading ?? "", body })
689
+ async ({ dealId, heading, body, cueAuthorized }) =>
690
+ addBriefBlock(dealId, { type: "text", heading: heading ?? "", body }, cueAuthorized)
688
691
  );
689
692
 
690
693
  server.registerTool(
@@ -696,10 +699,13 @@ server.registerTool(
696
699
  dealId: z.string(),
697
700
  url: z.string(),
698
701
  label: z.string().optional().describe("optional human-readable label"),
702
+ cueAuthorized: z.boolean().optional().describe(
703
+ "true only after the user explicitly approved every resolved cue recipient",
704
+ ),
699
705
  },
700
706
  },
701
- async ({ dealId, url, label }) =>
702
- addBriefBlock(dealId, { type: "link", url, label: label ?? "" })
707
+ async ({ dealId, url, label, cueAuthorized }) =>
708
+ addBriefBlock(dealId, { type: "link", url, label: label ?? "" }, cueAuthorized)
703
709
  );
704
710
 
705
711
  server.registerTool(
@@ -712,10 +718,13 @@ server.registerTool(
712
718
  tone: z.string().describe("insight | warning | info | success"),
713
719
  heading: z.string().optional(),
714
720
  body: z.string(),
721
+ cueAuthorized: z.boolean().optional().describe(
722
+ "true only after the user explicitly approved every resolved cue recipient",
723
+ ),
715
724
  },
716
725
  },
717
- async ({ dealId, tone, heading, body }) =>
718
- addBriefBlock(dealId, { type: "callout", tone, heading: heading ?? "", body })
726
+ async ({ dealId, tone, heading, body, cueAuthorized }) =>
727
+ addBriefBlock(dealId, { type: "callout", tone, heading: heading ?? "", body }, cueAuthorized)
719
728
  );
720
729
 
721
730
  server.registerTool(
@@ -738,9 +747,12 @@ server.registerTool(
738
747
  locked: z.boolean().optional(),
739
748
  hidden: z.boolean().optional(),
740
749
  sourceSection: z.string().optional(),
750
+ cueAuthorized: z.boolean().optional().describe(
751
+ "true only after the user explicitly approved every resolved cue recipient",
752
+ ),
741
753
  },
742
754
  },
743
- async ({ dealId, blockId, heading, body, url, label, description, tone, locked, hidden, sourceSection }) => {
755
+ async ({ dealId, blockId, heading, body, url, label, description, tone, locked, hidden, sourceSection, cueAuthorized }) => {
744
756
  const patch = {};
745
757
  for (const [k, v] of Object.entries({ heading, body, url, label, description, tone })) {
746
758
  if (v !== undefined) patch[k] = v;
@@ -750,6 +762,7 @@ server.registerTool(
750
762
  if (hidden !== undefined) meta.hidden = hidden;
751
763
  if (sourceSection !== undefined) meta.sourceSection = sourceSection;
752
764
  if (Object.keys(meta).length > 0) patch.meta = meta;
765
+ if (cueAuthorized === true) patch.cue_authorized = true;
753
766
  return callApi(
754
767
  "PATCH",
755
768
  `/api/deals/${encodeURIComponent(dealId)}/blocks/${encodeURIComponent(blockId)}`,
@@ -988,16 +1001,22 @@ server.registerTool(
988
1001
  "post",
989
1002
  {
990
1003
  description:
991
- "Post a message to a deal's timeline. Message can include @-mentions " +
992
- "(e.g. @<first-name> or @<email@llamaventures.vc>) the system fires " +
993
- "email + inbox notifications to mentioned users.",
1004
+ "Post a message to a deal's timeline. Cue-free posts are autonomous. " +
1005
+ "Explicit or implicit teammate cues create inbox/email notifications and require " +
1006
+ "cueAuthorized=true only after explicit user permission.",
994
1007
  inputSchema: {
995
1008
  dealId: z.string(),
996
1009
  message: z.string(),
1010
+ cueAuthorized: z.boolean().optional().describe(
1011
+ "true only after the user explicitly approved cueing every resolved recipient",
1012
+ ),
997
1013
  },
998
1014
  },
999
- async ({ dealId, message }) =>
1000
- callApi("POST", `/api/deals/${encodeURIComponent(dealId)}/posts`, { message })
1015
+ async ({ dealId, message, cueAuthorized }) =>
1016
+ callApi("POST", `/api/deals/${encodeURIComponent(dealId)}/posts`, {
1017
+ message,
1018
+ cue_authorized: cueAuthorized === true,
1019
+ })
1001
1020
  );
1002
1021
 
1003
1022
  // ============================================================
package/bin/llama.mjs CHANGED
@@ -457,16 +457,17 @@ Approvals (partner queue — self-claim approvals):
457
457
 
458
458
  Timeline / Posts:
459
459
  llama timeline <dealId> # full unified feed
460
- llama post <dealId> "message body" [--link url] [--link-name "name"]
460
+ llama post <dealId> "message body" [--link url] [--link-name "name"] [--cue]
461
+ # --cue only after explicit user approval
461
462
 
462
463
  Brief blocks:
463
464
  llama brief blocks <dealId> # list current block array
464
465
  llama brief block <dealId> <blockId> # fetch one block's body (manifest in 'llama deal show')
465
- llama brief add-text <dealId> --heading "..." --body "..."
466
- llama brief add-link <dealId> --url "..." --label "..." [--description "..."]
467
- llama brief add-embed <dealId> --url "..." [--label "..."]
468
- llama brief add-callout <dealId> --tone insight|info|warning|success --heading "..." --body "..."
469
- llama brief edit <dealId> <blockId> [--heading ...] [--body ...] [--url ...] [--label ...] [--tone ...]
466
+ llama brief add-text <dealId> --heading "..." --body "..." [--cue]
467
+ llama brief add-link <dealId> --url "..." --label "..." [--description "..."] [--cue]
468
+ llama brief add-embed <dealId> --url "..." [--label "..."] [--cue]
469
+ llama brief add-callout <dealId> --tone insight|info|warning|success --heading "..." --body "..." [--cue]
470
+ llama brief edit <dealId> <blockId> [--heading ...] [--body ...] [--url ...] [--label ...] [--tone ...] [--cue]
470
471
  [--source-section <key>] [--lock|--unlock] [--hide|--unhide]
471
472
  llama brief delete <dealId> <blockId>
472
473
  llama brief history <dealId> <blockId> [--limit 50] # prior versions of this block (newest first)
@@ -624,7 +625,7 @@ Command groups — run \`llama help <group>\` for that group's commands:
624
625
  deal create · show · feed · update · enrich · search · collaborators · links · delete
625
626
  activity new-deals · updated-deals · events for agent read models
626
627
  brief brief blocks: list · add · edit · history · refresh
627
- facts deal facts + skill corrections (the sourced, trust-rated layer)
628
+ facts deal facts the sourced, trust-rated layer
628
629
  timeline timeline · posts · mentions
629
630
  wiki cross-deal knowledge entries (markdown or HTML)
630
631
  pref standing agent preferences: list · add · retire · approve
@@ -644,10 +645,20 @@ the CLI auto-detects it — no token needed (\`llc_\` tokens are a fallback).`;
644
645
 
645
646
  // Area → which top-level sections of HELP_FULL belong to it.
646
647
  const HELP_AREA_MATCH = {
647
- deal: [/^Deals/, /^Collaborators/, /^Soft-delete/, /^Deal links/, /^Deal soft-delete/],
648
+ deal: [
649
+ /^Deals/,
650
+ /^Collaborators/,
651
+ /^Soft-delete/,
652
+ /^Deal links/,
653
+ /^Deal soft-delete/,
654
+ // `llama deal refresh-brief` / `revert-run` live under this heading. Without
655
+ // it here the section matched no area and was reachable only via `help all`,
656
+ // which read as "the command does not exist".
657
+ /^Brief refresh/,
658
+ ],
648
659
  activity: [/^Agent activity/],
649
- brief: [/^Brief blocks/],
650
- facts: [/^Deal facts/, /^Skill corrections/],
660
+ brief: [/^Brief blocks/, /^Brief refresh/],
661
+ facts: [/^Deal facts/],
651
662
  timeline: [/^Timeline/, /^Mentions/],
652
663
  wiki: [/^Wiki/, /^Where does this HTML/],
653
664
  memo: [/^Memo/],
@@ -1934,7 +1945,7 @@ async function main() {
1934
1945
  const { flags, positional } = parseFlags(rest);
1935
1946
  const body = positional[0];
1936
1947
  if (!dealId || !body) {
1937
- throw new Error(`Usage: llama post <dealId> "message body" [--link url] [--link-name "name"]`);
1948
+ throw new Error(`Usage: llama post <dealId> "message body" [--link url] [--link-name "name"] [--cue]`);
1938
1949
  }
1939
1950
  const attachments = flags.link
1940
1951
  ? [{ url: String(flags.link), name: flags["link-name"] ? String(flags["link-name"]) : String(flags.link) }]
@@ -1942,7 +1953,7 @@ async function main() {
1942
1953
  print(await request(
1943
1954
  "POST",
1944
1955
  `/api/deals/${encodeURIComponent(dealId)}/posts`,
1945
- { body, attachments }
1956
+ { body, attachments, cue_authorized: flags.cue === true }
1946
1957
  ));
1947
1958
  return;
1948
1959
  }
@@ -2168,7 +2179,10 @@ Routing — is this the right command?
2168
2179
  if (!cur) cur = await request("GET", `/api/deals/${encodeURIComponent(dealId)}/blocks`);
2169
2180
  const existing = cur.blocks ?? [];
2170
2181
  const next = position === "top" ? [block, ...existing] : [...existing, block];
2171
- print(await request("PUT", `/api/deals/${encodeURIComponent(dealId)}/blocks`, { blocks: next }));
2182
+ print(await request("PUT", `/api/deals/${encodeURIComponent(dealId)}/blocks`, {
2183
+ blocks: next,
2184
+ cue_authorized: flags.cue === true,
2185
+ }));
2172
2186
  console.log(`Created block ${id}`);
2173
2187
  return;
2174
2188
  }
@@ -2178,7 +2192,7 @@ Routing — is this the right command?
2178
2192
  const dealId = rest[0];
2179
2193
  const blockId = rest[1];
2180
2194
  if (!dealId || !blockId) {
2181
- throw new Error("Usage: llama brief edit <dealId> <blockId> [--heading ...] [--body ...] [--url ...] [--label ...] [--description ...] [--tone ...] [--source-section ...] [--lock|--unlock] [--hide|--unhide]");
2195
+ throw new Error("Usage: llama brief edit <dealId> <blockId> [--heading ...] [--body ...] [--url ...] [--label ...] [--description ...] [--tone ...] [--source-section ...] [--lock|--unlock] [--hide|--unhide] [--cue]");
2182
2196
  }
2183
2197
  const patch = {};
2184
2198
  for (const k of ["heading", "body", "url", "label", "description", "tone"]) {
@@ -2198,8 +2212,11 @@ Routing — is this the right command?
2198
2212
  metaPatch.sourceSection = String(flags["source-section"]);
2199
2213
  }
2200
2214
  if (Object.keys(metaPatch).length > 0) patch.meta = metaPatch;
2215
+ if (flags.cue === true) patch.cue_authorized = true;
2201
2216
 
2202
- if (Object.keys(patch).length === 0) throw new Error("at least one field flag required");
2217
+ if (Object.keys(patch).length === 0 || (Object.keys(patch).length === 1 && patch.cue_authorized === true)) {
2218
+ throw new Error("at least one field flag required");
2219
+ }
2203
2220
  print(await request("PATCH", `/api/deals/${encodeURIComponent(dealId)}/blocks/${encodeURIComponent(blockId)}`, patch));
2204
2221
  return;
2205
2222
  }
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "format": "llama.core-api-contract.v1",
3
3
  "name": "llama-core-api",
4
- "apiVersion": "3.1.0",
4
+ "apiVersion": "3.5.0",
5
5
  "openapiVersion": "3.0.3",
6
- "sha256": "51b134af7a63534406b3a216aeab190fd595f5270b2a10263730662f1c2f771a",
7
- "pathCount": 212,
8
- "operationCount": 275
6
+ "sha256": "31c7d23ee06bd4282aa2451698604cc30df4792d4a0e9674593a809b4eb55767",
7
+ "pathCount": 216,
8
+ "operationCount": 281
9
9
  }
@@ -1,15 +1,15 @@
1
1
  {
2
2
  "format": "llama.cli-build.v1",
3
3
  "packageName": "@llamaventures/cli",
4
- "packageVersion": "1.20.0",
5
- "sourceSha": "c9708c7ae6881e9e4a82972a521e748932a380ce",
4
+ "packageVersion": "1.22.1",
5
+ "sourceSha": "3280cbaa5ef4ea18ac3ecff7785d15112f9e9fdb",
6
6
  "sourceKind": "github",
7
7
  "sourceDirty": false,
8
8
  "coreApiContract": {
9
9
  "format": "llama.core-api-contract.v1",
10
10
  "name": "llama-core-api",
11
- "apiVersion": "3.1.0",
11
+ "apiVersion": "3.5.0",
12
12
  "openapiVersion": "3.0.3",
13
- "sha256": "51b134af7a63534406b3a216aeab190fd595f5270b2a10263730662f1c2f771a"
13
+ "sha256": "31c7d23ee06bd4282aa2451698604cc30df4792d4a0e9674593a809b4eb55767"
14
14
  }
15
15
  }
package/lib/client.mjs CHANGED
@@ -15,6 +15,7 @@ import { execFile as _execFile } from "child_process";
15
15
  import { promisify } from "util";
16
16
  import { createHash, randomUUID } from "crypto";
17
17
  import { getBuildInfo } from "./build-info.mjs";
18
+ import { maybeNudgeServerCompatibility } from "./server-compatibility.mjs";
18
19
 
19
20
  const execFile = promisify(_execFile);
20
21
 
@@ -523,6 +524,48 @@ export async function requestSse(method, endpoint, body, opts = {}) {
523
524
  return requestSseWithRetry(method, endpoint, body, opts, /* allowRetry */ true);
524
525
  }
525
526
 
527
+ function apiResponseError(data, status) {
528
+ const message =
529
+ typeof data === "object" && data?.error ? data.error : `HTTP ${status}`;
530
+ if (data?.code === "CLI_VERSION_UNSUPPORTED") {
531
+ const recovery = data?.recovery?.command || "npm i -g @llamaventures/cli@latest";
532
+ const error = new Error(
533
+ `Error[CLI_VERSION_UNSUPPORTED]: ${message}\n` +
534
+ `Recovery: ${recovery}\n` +
535
+ "Retry the original Llama command after upgrading.",
536
+ );
537
+ error.code = data.code;
538
+ error.details = data;
539
+ return error;
540
+ }
541
+ if (data?.code !== "MENTION_APPROVAL_REQUIRED") {
542
+ const error = new Error(message);
543
+ if (typeof data?.code === "string") error.code = data.code;
544
+ error.details = data;
545
+ return error;
546
+ }
547
+ const recipients = Array.isArray(data?.cue?.recipients)
548
+ ? data.cue.recipients
549
+ .map((recipient) => {
550
+ const name = recipient?.name || `user ${recipient?.user_id ?? "unknown"}`;
551
+ return recipient?.intent ? `${name} (${recipient.intent})` : name;
552
+ })
553
+ .join(", ")
554
+ : "unknown";
555
+ const channels = Array.isArray(data?.cue?.channels)
556
+ ? data.cue.channels.join(" + ")
557
+ : "inbox + email";
558
+ const error = new Error(
559
+ `Error[MENTION_APPROVAL_REQUIRED]: ${message}\n` +
560
+ `Recipients: ${recipients}\n` +
561
+ `Channels: ${channels}\n` +
562
+ "Ask the user for explicit permission, then retry the exact write with --cue (CLI) or cueAuthorized=true (MCP).",
563
+ );
564
+ error.code = data.code;
565
+ error.details = data;
566
+ return error;
567
+ }
568
+
526
569
  async function requestWithRetry(method, endpoint, body, opts, allowRetry) {
527
570
  const authHeaders = await getAuthHeaders();
528
571
  if (Object.keys(authHeaders).length === 0) throw noAuthError();
@@ -538,6 +581,7 @@ async function requestWithRetry(method, endpoint, body, opts, allowRetry) {
538
581
  },
539
582
  body: body === undefined ? undefined : JSON.stringify(body),
540
583
  });
584
+ maybeNudgeServerCompatibility(res.headers);
541
585
 
542
586
  // 401 + we sent an OAuth Bearer + this is the first attempt → try a
543
587
  // forced refresh once. Covers two cases: (a) clock skew between client
@@ -582,7 +626,7 @@ async function requestWithRetry(method, endpoint, body, opts, allowRetry) {
582
626
  data: null,
583
627
  errorMessage: message,
584
628
  });
585
- throw new Error(message);
629
+ throw apiResponseError(data, res.status);
586
630
  }
587
631
  await recordClientTelemetry({
588
632
  authHeaders,
@@ -613,6 +657,7 @@ async function requestSseWithRetry(method, endpoint, body, opts, allowRetry) {
613
657
  },
614
658
  body: body === undefined ? undefined : JSON.stringify(body),
615
659
  });
660
+ maybeNudgeServerCompatibility(res.headers);
616
661
 
617
662
  if (res.status === 401 && allowRetry && (await bearerCameFromOAuth())) {
618
663
  let refreshed = null;
@@ -651,7 +696,7 @@ async function requestSseWithRetry(method, endpoint, body, opts, allowRetry) {
651
696
  data: null,
652
697
  errorMessage: message,
653
698
  });
654
- throw new Error(message);
699
+ throw apiResponseError(data, res.status);
655
700
  }
656
701
 
657
702
  const reader = res.body?.getReader();
@@ -0,0 +1,96 @@
1
+ // Consume the compatibility policy returned by Llama Command.
2
+ //
3
+ // Unlike the npm-registry nudge, this path is intentionally NOT TTY-gated:
4
+ // coding agents and MCP hosts need to see the server's recommendation too.
5
+ // It remains safe for scripts because it writes only to stderr and shares the
6
+ // existing once-per-day update stamp with lib/version-check.mjs.
7
+
8
+ import fs from "fs";
9
+ import os from "os";
10
+ import path from "path";
11
+ import { createRequire } from "module";
12
+
13
+ const DEFAULT_STAMP_FILE = path.join(os.homedir(), ".llama", ".update-check");
14
+ const THROTTLE_MS = 24 * 60 * 60 * 1000;
15
+
16
+ function installedVersion() {
17
+ try {
18
+ const requireFromHere = createRequire(import.meta.url);
19
+ return String(requireFromHere("../package.json").version || "unknown");
20
+ } catch {
21
+ return "unknown";
22
+ }
23
+ }
24
+
25
+ function readHeader(headers, name) {
26
+ if (typeof headers?.get === "function") return headers.get(name);
27
+ if (!headers || typeof headers !== "object") return null;
28
+ const match = Object.entries(headers).find(
29
+ ([key]) => key.toLowerCase() === name.toLowerCase(),
30
+ );
31
+ return match ? String(match[1]) : null;
32
+ }
33
+
34
+ export function parseServerCompatibility(headers) {
35
+ const status = readHeader(headers, "x-llama-cli-status");
36
+ if (!["ok", "stale", "unsupported", "unknown"].includes(status)) return null;
37
+ return {
38
+ status,
39
+ recommendedVersion: readHeader(headers, "x-llama-cli-recommended-version"),
40
+ minimumVersion: readHeader(headers, "x-llama-cli-minimum-version"),
41
+ upgradeCommand:
42
+ readHeader(headers, "x-llama-cli-upgrade-command") ||
43
+ "npm i -g @llamaventures/cli@latest",
44
+ };
45
+ }
46
+
47
+ function checkedRecently(stampFile, now) {
48
+ try {
49
+ const last = Number.parseInt(fs.readFileSync(stampFile, "utf8").trim(), 10);
50
+ return Number.isFinite(last) && now - last < THROTTLE_MS;
51
+ } catch {
52
+ return false;
53
+ }
54
+ }
55
+
56
+ function touchStamp(stampFile, now) {
57
+ try {
58
+ fs.mkdirSync(path.dirname(stampFile), { recursive: true, mode: 0o700 });
59
+ fs.writeFileSync(stampFile, `${now}\n`, { mode: 0o600 });
60
+ } catch {
61
+ // Best-effort only. Compatibility hints must never break a command.
62
+ }
63
+ }
64
+
65
+ export function formatServerCompatibilityNudge(policy, currentVersion = installedVersion()) {
66
+ if (policy?.status !== "stale" || !policy.recommendedVersion) return null;
67
+ return (
68
+ `warning[CLI_UPDATE_RECOMMENDED]: Llama Command recommends ` +
69
+ `@llamaventures/cli ${policy.recommendedVersion}; this client is ${currentVersion}. ` +
70
+ `Upgrade: ${policy.upgradeCommand}`
71
+ );
72
+ }
73
+
74
+ export function maybeNudgeServerCompatibility(headers, options = {}) {
75
+ try {
76
+ const env = options.env ?? process.env;
77
+ if (env.LLAMA_NO_UPDATE_CHECK) return false;
78
+ const policy = parseServerCompatibility(headers);
79
+ const nudge = formatServerCompatibilityNudge(
80
+ policy,
81
+ options.currentVersion ?? installedVersion(),
82
+ );
83
+ if (!nudge) return false;
84
+ const now = options.now ?? Date.now();
85
+ const stampFile = options.stampFile ?? DEFAULT_STAMP_FILE;
86
+ if (checkedRecently(stampFile, now)) return false;
87
+ touchStamp(stampFile, now);
88
+ const write = options.write ?? ((line) => process.stderr.write(`${line}\n`));
89
+ write(nudge);
90
+ return true;
91
+ } catch {
92
+ return false;
93
+ }
94
+ }
95
+
96
+ export const SERVER_COMPATIBILITY_STAMP_FILE = DEFAULT_STAMP_FILE;
package/package.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@llamaventures/cli",
3
- "version": "1.20.0",
3
+ "version": "1.22.1",
4
4
  "description": "CLI + MCP server for the Llama Ventures investment workbench (command.llamaventures.vc).",
5
5
  "type": "module",
6
6
  "scripts": {
7
7
  "test": "npm run test:agent-routing && npm run test:contract",
8
8
  "test:agent-routing": "node scripts/verify-agent-routing.mjs",
9
- "test:contract": "node --test scripts/build-manifest.test.mjs scripts/core-api-call-sites.test.mjs scripts/verify-core-api-contract.test.mjs && node scripts/verify-core-api-contract.mjs",
9
+ "test:contract": "node --test scripts/build-manifest.test.mjs scripts/core-api-call-sites.test.mjs scripts/server-compatibility.test.mjs scripts/verify-core-api-contract.test.mjs && node scripts/verify-core-api-contract.mjs",
10
10
  "verify:artifact": "node scripts/verify-release-artifact.mjs",
11
11
  "verify:release": "npm test && npm run verify:artifact && node scripts/verify-tarball-clean.mjs",
12
12
  "prepack": "node scripts/prepare-build-manifest.mjs",