@clien-ai/mcp 0.8.1 → 0.10.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 +38 -6
- package/dist/tools/collections.js +308 -5
- package/dist/tools/collections.js.map +1 -1
- package/dist/tools/content-type-display.js +166 -0
- package/dist/tools/content-type-display.js.map +1 -0
- package/dist/tools/credits.js +299 -0
- package/dist/tools/credits.js.map +1 -0
- package/dist/tools/interview-scripts.js +370 -0
- package/dist/tools/interview-scripts.js.map +1 -0
- package/dist/tools/interview.js +148 -6
- package/dist/tools/interview.js.map +1 -1
- package/dist/tools/output-schemas.js +47 -0
- package/dist/tools/output-schemas.js.map +1 -1
- package/dist/tools/personas.js +276 -48
- package/dist/tools/personas.js.map +1 -1
- package/dist/tools/registry.js +316 -24
- package/dist/tools/registry.js.map +1 -1
- package/dist/tools/report-digest.js +179 -3
- package/dist/tools/report-digest.js.map +1 -1
- package/dist/tools/reports.js +9 -2
- package/dist/tools/reports.js.map +1 -1
- package/dist/tools/research.js +102 -5
- package/dist/tools/research.js.map +1 -1
- package/dist/tools/scoped-research.js +270 -42
- package/dist/tools/scoped-research.js.map +1 -1
- package/dist/tools/status.js +2 -2
- package/dist/tools/status.js.map +1 -1
- package/dist/types/report.js +207 -5
- package/dist/types/report.js.map +1 -1
- package/package.json +3 -3
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FUL-341 / FUL-536 / FUL-560 — the one rule for reading and RENDERING a source's content type to
|
|
3
|
+
* an agent.
|
|
4
|
+
*
|
|
5
|
+
* ## Why this is a module and not two copies
|
|
6
|
+
*
|
|
7
|
+
* `list_community_signals` (`collections.ts`) and `search_forums` (`scoped-research.ts`) render the
|
|
8
|
+
* SAME underlying rows — a forum thread a run discovered, before and after it is promoted into
|
|
9
|
+
* `community_signals`. FUL-247's rule is that two surfaces over one dataset must not disagree about
|
|
10
|
+
* what is worth an agent's attention; FUL-536 is what happens when they do, and it took two green
|
|
11
|
+
* guards and a production run to notice. So the vocabulary, the fail-closed normalizer and the
|
|
12
|
+
* rendered line live in ONE place that both import, the same way `engagement-display.ts` owns the
|
|
13
|
+
* engagement line for the same two surfaces.
|
|
14
|
+
*
|
|
15
|
+
* ## The rule
|
|
16
|
+
*
|
|
17
|
+
* State the provenance in BOTH directions, always. Rendering a type only when one is present makes
|
|
18
|
+
* "unclassified" indistinguishable from "this surface was not told", and silence reads as
|
|
19
|
+
* reassurance — which is the fail-open FUL-341 exists to close. An agent deciding whether to trust
|
|
20
|
+
* a quote needs the negative case spelled out, and it needs the CONSEQUENCE, not just a null field.
|
|
21
|
+
*
|
|
22
|
+
* ⚠️ THE TWO EXCLUSIONS ARE THE SAME BUT ARE WORDED SEPARATELY ON PURPOSE. Since FUL-531 both kinds
|
|
23
|
+
* carry the identical exclusion — `GROUNDING_ELIGIBLE_CONTENT_TYPES` (`app/lib/db/community-db.ts`)
|
|
24
|
+
* is `CONTENT_TYPES` minus `FLAGGED_CONTENT_TYPES`, so neither reaches the automatic synthesis
|
|
25
|
+
* prompt, and `buildCommunityThemes` grounds no theme on either. They stay worded SEPARATELY
|
|
26
|
+
* because they do not MEAN the same thing: `marketing_seo` is a source we classified and judged
|
|
27
|
+
* vendor copy, `unclassified` is one nobody has looked at. Collapsing them would tell an agent that
|
|
28
|
+
* an unexamined source had been examined.
|
|
29
|
+
*/
|
|
30
|
+
/**
|
|
31
|
+
* FUL-341 — the canonical content-type vocabulary, hand-copied.
|
|
32
|
+
*
|
|
33
|
+
* `mcp/` publishes to npm on its own cadence and cannot import from the Next.js app, so this is a
|
|
34
|
+
* forced duplicate of `CONTENT_TYPES` in `app/lib/validation/claim-states.ts` (itself already
|
|
35
|
+
* pinned to the agent's copy). This repo's answer to exactly that situation is a coupling test:
|
|
36
|
+
* `__tests__/integration/mcp/community-content-type-coupling.test.ts` pins this set equal to the
|
|
37
|
+
* app's, so widening the vocabulary in one package fails the suite rather than silently teaching
|
|
38
|
+
* two surfaces to disagree about the same row.
|
|
39
|
+
*
|
|
40
|
+
* ⚠️ That test parses this declaration out of THIS FILE as text. Moving or renaming it means
|
|
41
|
+
* updating the test's path and regex, not just the import sites.
|
|
42
|
+
*/
|
|
43
|
+
export const CANONICAL_CONTENT_TYPES = ['user_voice', 'report_evidence', 'marketing_seo'];
|
|
44
|
+
/**
|
|
45
|
+
* The four states, enumerated for a tool DESCRIPTION or a schema `.describe()`.
|
|
46
|
+
*
|
|
47
|
+
* A description is an executable interface: an agent reads it BEFORE its first call and is primed
|
|
48
|
+
* by it, so a trust qualifier that exists only in the rendered output is discoverable one call too
|
|
49
|
+
* late (FUL-341). Carries no leading clause — each surface supplies its own ("every thread carries
|
|
50
|
+
* a `source type:` line", "every receipt carries…") — and no trailing space.
|
|
51
|
+
*/
|
|
52
|
+
export const CONTENT_TYPE_STATES = '`user_voice` (a real person\'s first-hand account), `report_evidence` ' +
|
|
53
|
+
'(authoritative/third-person reporting), `marketing_seo` (vendor marketing or SEO content), or ' +
|
|
54
|
+
'`unclassified`.';
|
|
55
|
+
/**
|
|
56
|
+
* The two exclusions, in the ONE wording every surface states them in.
|
|
57
|
+
*
|
|
58
|
+
* ⚠️ Both are real and both are enforced: `GROUNDING_ELIGIBLE_CONTENT_TYPES` (`community-db.ts`)
|
|
59
|
+
* is `CONTENT_TYPES` minus `FLAGGED_CONTENT_TYPES` since FUL-531, so neither kind reaches the
|
|
60
|
+
* automatic synthesis prompt, and `buildCommunityThemes` grounds no theme on either. They are
|
|
61
|
+
* still stated SEPARATELY because they do not mean the same thing — one source was classified and
|
|
62
|
+
* judged vendor copy, the other has not been looked at — and an agent that conflated them would
|
|
63
|
+
* treat an unexamined source as an examined one.
|
|
64
|
+
*
|
|
65
|
+
* ⚠️ FUL-560 extracted this from the two `registry.ts` adverts that already held it word-for-word
|
|
66
|
+
* by hand. The `get_report` receipt pools were the surface that had NO wording at all, and adding
|
|
67
|
+
* a fifth hand-copy to fix that is the drift FUL-247 exists to prevent — so the copies became one
|
|
68
|
+
* constant, pinned across every surface by `content-type-display.test.ts`. No leading or trailing
|
|
69
|
+
* space; each caller joins it.
|
|
70
|
+
*/
|
|
71
|
+
export const CONTENT_TYPE_EXCLUSIONS = '`marketing_seo` is shown for awareness but is EXCLUDED from grounding AND from automatic ' +
|
|
72
|
+
'community synthesis — it is vendor marketing or SEO content, never cite it as user ' +
|
|
73
|
+
'evidence. `unclassified` is EXCLUDED from both as well, and means the classification is ' +
|
|
74
|
+
'genuinely unknown, NOT that the source was checked and found benign.';
|
|
75
|
+
/**
|
|
76
|
+
* FUL-560 — the receipt-pool paragraph, in the ONE wording every tool that RENDERS the trust digest
|
|
77
|
+
* advertises it in.
|
|
78
|
+
*
|
|
79
|
+
* `composeReportText` puts the same two receipt pools in the result text of `get_report`,
|
|
80
|
+
* `clien_research` and `clien_research_status`, so all three emit these `source type:` lines and all
|
|
81
|
+
* three must explain them: an agent that ran the hero tool and never calls `get_report` would
|
|
82
|
+
* otherwise meet the vocabulary for the first time in the output, which is the discoverable-one-call-
|
|
83
|
+
* too-late failure {@link CONTENT_TYPE_STATES} exists to avoid. Three hand-copies of a paragraph is
|
|
84
|
+
* also exactly the drift that made this module necessary — hence one constant, and
|
|
85
|
+
* `content-type-display.test.ts` DERIVES the tools that must carry it from the source rather than
|
|
86
|
+
* listing them, so a fourth digest surface fails the suite instead of shipping unadverted.
|
|
87
|
+
*
|
|
88
|
+
* Opens as a noun phrase so it can be a numbered item in `get_report`'s list or the object of a
|
|
89
|
+
* lead-in sentence elsewhere. No trailing space; each caller joins it.
|
|
90
|
+
*/
|
|
91
|
+
export const CONTENT_TYPE_DIGEST_ADVERT = 'the SOURCE TYPE on every receipt — ' +
|
|
92
|
+
CONTENT_TYPE_STATES +
|
|
93
|
+
' Every "RRCP-" market/competitor receipt carries a `source type:` line; a "RCP-" persona receipt ' +
|
|
94
|
+
'is a first-hand `user_voice` post unless its row says otherwise, and any that is not carries the ' +
|
|
95
|
+
'same line marked \u26a0. ' +
|
|
96
|
+
CONTENT_TYPE_EXCLUSIONS;
|
|
97
|
+
/**
|
|
98
|
+
* Normalize a persisted classification, failing CLOSED.
|
|
99
|
+
*
|
|
100
|
+
* Anything outside the canonical set — a typo'd literal, a value from a newer app version this
|
|
101
|
+
* client predates — becomes `null`, exactly like a missing one. A `!== null` check here instead
|
|
102
|
+
* would let an unrecognized string read as CLASSIFIED and present unvetted evidence to an agent as
|
|
103
|
+
* grounding-eligible. Version skew makes that a real case for a published client, not a
|
|
104
|
+
* hypothetical one.
|
|
105
|
+
*
|
|
106
|
+
* ⚠️ FUL-536 adds a second reason on the `search_forums` side. There the value is read out of a
|
|
107
|
+
* stored `report_data` document rather than a CHECK-constrained column, and `ForumThreadSchema` is
|
|
108
|
+
* `.passthrough()` — so a hallucinated `contentType: 'user_voice'` from an older run is a shape the
|
|
109
|
+
* parser accepts. `tagFastPathContentTypes` (FUL-535) deletes model-emitted types at the agent's
|
|
110
|
+
* read chokepoint for NEW data; this is what stops an old one being rendered as a classification.
|
|
111
|
+
*/
|
|
112
|
+
export function canonicalContentType(value) {
|
|
113
|
+
return typeof value === 'string' && CANONICAL_CONTENT_TYPES.includes(value)
|
|
114
|
+
? value
|
|
115
|
+
: null;
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* The continuation-line indent every surface renders this on. One constant, so the two
|
|
119
|
+
* compositions below cannot drift apart by a space.
|
|
120
|
+
*/
|
|
121
|
+
const CONTINUATION = '\n ';
|
|
122
|
+
/**
|
|
123
|
+
* The sentence itself, without the indent — what a reader actually reads.
|
|
124
|
+
*
|
|
125
|
+
* Split out by FUL-560 purely so {@link contentTypeDeviationLine} can prefix a marker without
|
|
126
|
+
* restating any of the three branches. Nothing calls this directly; the exported line builders
|
|
127
|
+
* are the API, and holding the wording in ONE expression is the whole point of this module.
|
|
128
|
+
*/
|
|
129
|
+
function contentTypeSentence(value) {
|
|
130
|
+
const contentType = canonicalContentType(value);
|
|
131
|
+
return !contentType
|
|
132
|
+
? 'source type: unclassified - excluded from grounding and community synthesis'
|
|
133
|
+
: contentType === 'marketing_seo'
|
|
134
|
+
? 'source type: marketing_seo - vendor marketing, excluded from grounding and community synthesis'
|
|
135
|
+
: `source type: ${contentType}`;
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* The `source type:` continuation line, for a row rendered in the `\n ` continuation shape both
|
|
139
|
+
* surfaces use. Never returns an empty string: see the both-directions rule above.
|
|
140
|
+
*
|
|
141
|
+
* Accepts the absent value in either spelling — `null` for a `community_signals` column, `undefined`
|
|
142
|
+
* for a `report_data` thread key FUL-535 deliberately left unstamped — because the two surfaces
|
|
143
|
+
* store "we don't know" differently and neither should have to widen its own types to say so.
|
|
144
|
+
*/
|
|
145
|
+
export function contentTypeProvenanceLine(value) {
|
|
146
|
+
return `${CONTINUATION}${contentTypeSentence(value)}`;
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* FUL-560 — the same sentence, marked as a DEVIATION from a pool's stated invariant.
|
|
150
|
+
*
|
|
151
|
+
* `get_report`'s PERSONA receipt pool is the one surface where the type is a near-constant rather
|
|
152
|
+
* than a per-row fact: `stampEvidenceMetadata` prunes every non-`user_voice` source out of
|
|
153
|
+
* `persona.sources` upstream, so a per-row line there would repeat one word down the whole pool
|
|
154
|
+
* and bury the row that broke the rule. The pool states the invariant once in its header and each
|
|
155
|
+
* row that does NOT satisfy it carries this line — which is the both-directions rule kept, not
|
|
156
|
+
* traded away: a reader is told the rule AND told, loudly, wherever it does not hold.
|
|
157
|
+
*
|
|
158
|
+
* ⚠️ The marker is emphasis, NOT vocabulary. It prefixes the shared sentence rather than replacing
|
|
159
|
+
* any of it, so the wording an agent matches on stays byte-identical to every other surface — the
|
|
160
|
+
* FUL-247 drift this module exists to prevent. A row whose type is absent or non-canonical is a
|
|
161
|
+
* deviation too: `user_voice` is a positive claim, and anything else is the caller failing closed.
|
|
162
|
+
*/
|
|
163
|
+
export function contentTypeDeviationLine(value) {
|
|
164
|
+
return `${CONTINUATION}\u26a0 ${contentTypeSentence(value)}`;
|
|
165
|
+
}
|
|
166
|
+
//# sourceMappingURL=content-type-display.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"content-type-display.js","sourceRoot":"","sources":["../../src/tools/content-type-display.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAEH;;;;;;;;;;;;GAYG;AACH,MAAM,CAAC,MAAM,uBAAuB,GAAG,CAAC,YAAY,EAAE,iBAAiB,EAAE,eAAe,CAAU,CAAA;AAElG;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,mBAAmB,GAC9B,wEAAwE;IACxE,gGAAgG;IAChG,iBAAiB,CAAA;AAEnB;;;;;;;;;;;;;;;GAeG;AACH,MAAM,CAAC,MAAM,uBAAuB,GAClC,2FAA2F;IAC3F,qFAAqF;IACrF,0FAA0F;IAC1F,sEAAsE,CAAA;AAExE;;;;;;;;;;;;;;;GAeG;AACH,MAAM,CAAC,MAAM,0BAA0B,GACrC,qCAAqC;IACrC,mBAAmB;IACnB,mGAAmG;IACnG,mGAAmG;IACnG,2BAA2B;IAC3B,uBAAuB,CAAA;AAEzB;;;;;;;;;;;;;;GAcG;AACH,MAAM,UAAU,oBAAoB,CAAC,KAAc;IACjD,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAK,uBAA6C,CAAC,QAAQ,CAAC,KAAK,CAAC;QAChG,CAAC,CAAC,KAAK;QACP,CAAC,CAAC,IAAI,CAAA;AACV,CAAC;AAED;;;GAGG;AACH,MAAM,YAAY,GAAG,OAAO,CAAA;AAE5B;;;;;;GAMG;AACH,SAAS,mBAAmB,CAAC,KAAc;IACzC,MAAM,WAAW,GAAG,oBAAoB,CAAC,KAAK,CAAC,CAAA;IAC/C,OAAO,CAAC,WAAW;QACjB,CAAC,CAAC,6EAA6E;QAC/E,CAAC,CAAC,WAAW,KAAK,eAAe;YAC/B,CAAC,CAAC,gGAAgG;YAClG,CAAC,CAAC,gBAAgB,WAAW,EAAE,CAAA;AACrC,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,yBAAyB,CAAC,KAAc;IACtD,OAAO,GAAG,YAAY,GAAG,mBAAmB,CAAC,KAAK,CAAC,EAAE,CAAA;AACvD,CAAC;AAED;;;;;;;;;;;;;;GAcG;AACH,MAAM,UAAU,wBAAwB,CAAC,KAAc;IACrD,OAAO,GAAG,YAAY,UAAU,mBAAmB,CAAC,KAAK,CAAC,EAAE,CAAA;AAC9D,CAAC"}
|
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `get_credit_balance` (FUL-513) — what this account can afford, BEFORE it commits.
|
|
3
|
+
*
|
|
4
|
+
* Until this tool the MCP surface disclosed the balance REACTIVELY only, inside the
|
|
5
|
+
* insufficient-credit failure guidance ("you have N, this needs M"). An agent asked to
|
|
6
|
+
* "research these three ideas" therefore could not learn it could afford one until it
|
|
7
|
+
* had already failed — mid-plan, after some spend. Failure guidance is a good backstop
|
|
8
|
+
* and a poor planning input, and the actions on this surface are expensive: a
|
|
9
|
+
* `clien_research` run is the priciest thing any agent here can start.
|
|
10
|
+
*
|
|
11
|
+
* ⚠️ THIS TOOL CANNOT SPEND, AND MUST NEVER GAIN THE ABILITY (constraint 1).
|
|
12
|
+
* `/app/billing` is the product's single money surface. There is no top-up here, no
|
|
13
|
+
* plan change, no checkout, and no debit: the module makes exactly one request, a GET,
|
|
14
|
+
* and `credits.test.ts` asserts that structurally rather than trusting this paragraph.
|
|
15
|
+
* When an agent finds the balance short, the only correct move is to tell the human —
|
|
16
|
+
* an agent cannot buy credits on anyone's behalf and this tool gives it no way to try.
|
|
17
|
+
*
|
|
18
|
+
* ⚠️ THE NUMBER IS ADVISORY, NEVER A RESERVATION (constraint 2). It is a reading taken
|
|
19
|
+
* at call time and it holds nothing back. Between this read and a spend the balance can
|
|
20
|
+
* move — a concurrent run, a monthly grant, a refund, another client on the same
|
|
21
|
+
* account — and the atomic RPCs behind `start_validation_with_credits` /
|
|
22
|
+
* `spend_credits_for_tool` remain the ONLY authority on whether a spend happens. An
|
|
23
|
+
* agent that treats a stale read as a guarantee will report a run it never started; the
|
|
24
|
+
* rendered text and the tool description both say so, because an unread caveat is not a
|
|
25
|
+
* caveat.
|
|
26
|
+
*
|
|
27
|
+
* ⚠️ NOT ONE PRICE IS WRITTEN IN THIS FILE (constraint 3). Every credit number rendered
|
|
28
|
+
* below arrives from the server, which derives it from `getActionCost()` in
|
|
29
|
+
* `app/lib/billing/pricing-config.ts` — the single source of truth for what compute
|
|
30
|
+
* costs. `@clien-ai/mcp` publishes on its own cadence and cannot import that module, so
|
|
31
|
+
* a hardcoded figure here would go stale silently at the next repricing and quote a
|
|
32
|
+
* price the product does not charge. The renderer iterates whatever the server sent; it
|
|
33
|
+
* knows no action names and no amounts of its own.
|
|
34
|
+
*
|
|
35
|
+
* Contract:
|
|
36
|
+
* - get_credit_balance → GET /api/billing/credits →
|
|
37
|
+
* { balance: number, spendable: number, costs: [{ action, credits }] }
|
|
38
|
+
*
|
|
39
|
+
* ⚠️ TWO NUMBERS, NOT ONE, and they are different facts. `balance` is what the ledger
|
|
40
|
+
* holds; `spendable` is what may actually be spent today, with the FUL-73 rollover cap
|
|
41
|
+
* applied at read time. `spendable` is the one the pre-checks compare against, so it is
|
|
42
|
+
* the one that answers "can I afford this" — and it can be LOWER than the balance. A
|
|
43
|
+
* render that showed only the larger number would overstate what the account can do.
|
|
44
|
+
*
|
|
45
|
+
* Read-only, so a timeout is always safe to retry — no work lost, no credit spent.
|
|
46
|
+
*/
|
|
47
|
+
import { z } from 'zod';
|
|
48
|
+
import { CollectionToolError, formatZodError, makeCtx, } from './collections.js';
|
|
49
|
+
import { apiCall, categorizeFetchError } from './research.js';
|
|
50
|
+
import { safeInline, truncate } from './render-safety.js';
|
|
51
|
+
const CREDITS_FETCH_TIMEOUT_MS = 30_000;
|
|
52
|
+
/**
|
|
53
|
+
* No parameters. The balance belongs to the authenticated account and nothing else,
|
|
54
|
+
* so there is no id to pass — and deliberately no id to SUBSTITUTE: the endpoint reads
|
|
55
|
+
* whoever the bearer token says you are and accepts no user identifier from the caller.
|
|
56
|
+
*/
|
|
57
|
+
export const GetCreditBalanceInputSchema = z.object({});
|
|
58
|
+
/** A finite number, or null. Non-finite and non-numeric degrade rather than print. */
|
|
59
|
+
function asCount(value) {
|
|
60
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : null;
|
|
61
|
+
}
|
|
62
|
+
function asText(value) {
|
|
63
|
+
return typeof value === 'string' && value.trim().length > 0 ? value : null;
|
|
64
|
+
}
|
|
65
|
+
export function toCreditActionCost(row) {
|
|
66
|
+
return { action: asText(row.action), credits: asCount(row.credits) };
|
|
67
|
+
}
|
|
68
|
+
// ---------------------------------------------------------------------------
|
|
69
|
+
// Render caps
|
|
70
|
+
// ---------------------------------------------------------------------------
|
|
71
|
+
/**
|
|
72
|
+
* Upper bound on priced actions RENDERED. The server's cost table is far shorter, so
|
|
73
|
+
* this should never fire — but an unfired cap that would truncate silently is the thing
|
|
74
|
+
* this package refuses. When it fires it says so, and says where the rest are.
|
|
75
|
+
*/
|
|
76
|
+
const COST_RENDER_CAP = 25;
|
|
77
|
+
/** An action key is a short identifier; anything longer is drift, not a name. */
|
|
78
|
+
const ACTION_MAX = 60;
|
|
79
|
+
// ---------------------------------------------------------------------------
|
|
80
|
+
// Transport
|
|
81
|
+
// ---------------------------------------------------------------------------
|
|
82
|
+
/**
|
|
83
|
+
* Non-ok handling for this read.
|
|
84
|
+
*
|
|
85
|
+
* Deliberately NOT `collections.ts`'s `throwForResponse`: that helper's 404 branch names
|
|
86
|
+
* a project id, and this endpoint has no project. A 404 here means the app deploy on the
|
|
87
|
+
* other end predates this route, which is a version-skew fact an agent can act on (stop
|
|
88
|
+
* asking) rather than an ownership one.
|
|
89
|
+
*/
|
|
90
|
+
async function throwForCreditsResponse(response, deps, action) {
|
|
91
|
+
if (categorizeFetchError(response.status) === 'auth') {
|
|
92
|
+
if (response.refreshError?.kind !== 'transient')
|
|
93
|
+
deps.invalidateSession?.();
|
|
94
|
+
throw new CollectionToolError('Session expired. Re-run any Clien.ai tool to re-authenticate, then try again.');
|
|
95
|
+
}
|
|
96
|
+
if (response.status === 404) {
|
|
97
|
+
throw new CollectionToolError(`Could not ${action} — this Clien.ai deployment does not serve a credit-balance read. ` +
|
|
98
|
+
'Do not infer a balance from that, and do not treat it as zero: the account may be ' +
|
|
99
|
+
'fully funded. Ask the user what their balance is, or have them check the Billing ' +
|
|
100
|
+
'page in the Clien.ai web app.');
|
|
101
|
+
}
|
|
102
|
+
const text = await response.text().catch(() => '');
|
|
103
|
+
let detail = truncate(text, 200);
|
|
104
|
+
try {
|
|
105
|
+
const parsed = JSON.parse(text);
|
|
106
|
+
if (parsed?.error)
|
|
107
|
+
detail = `${parsed.error}${parsed.code ? ` (${parsed.code})` : ''}`;
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
// non-JSON body — fall back to the truncated raw text
|
|
111
|
+
}
|
|
112
|
+
throw new CollectionToolError(`Failed to ${action} (HTTP ${response.status}): ${detail}. This is a failed READ, not a ` +
|
|
113
|
+
'finding about the account — nothing was spent, and the balance is unknown rather than ' +
|
|
114
|
+
'empty.');
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* GET /api/billing/credits.
|
|
118
|
+
*
|
|
119
|
+
* ⚠️ AN UNREADABLE BODY IS NOT A ZERO BALANCE, and the difference is decided by KEY
|
|
120
|
+
* PRESENCE rather than truthiness. A 200 with no `balance` key at all (an HTML error page
|
|
121
|
+
* from a proxy, a truncated response, a reshaped envelope) is the endpoint saying nothing.
|
|
122
|
+
* Rendered as "0 credits" it would stop an agent from doing work the user has already paid
|
|
123
|
+
* for, and would have it report to that user that they are out of money. So this throws,
|
|
124
|
+
* and says the balance is unknown.
|
|
125
|
+
*/
|
|
126
|
+
async function fetchCreditBalance(deps) {
|
|
127
|
+
const action = 'read the credit balance';
|
|
128
|
+
const ctx = makeCtx(deps);
|
|
129
|
+
let response;
|
|
130
|
+
try {
|
|
131
|
+
response = await apiCall(ctx, 'GET', '/api/billing/credits', {
|
|
132
|
+
timeoutMs: CREDITS_FETCH_TIMEOUT_MS,
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
catch (err) {
|
|
136
|
+
const e = err;
|
|
137
|
+
if (e.name === 'AbortError' || e.name === 'TimeoutError') {
|
|
138
|
+
throw new CollectionToolError(`Timed out after ${CREDITS_FETCH_TIMEOUT_MS / 1000}s trying to ${action}. ` +
|
|
139
|
+
'Try again in a moment — this is a read, so nothing was spent and nothing was lost.');
|
|
140
|
+
}
|
|
141
|
+
throw err;
|
|
142
|
+
}
|
|
143
|
+
if (!response.ok)
|
|
144
|
+
await throwForCreditsResponse(response, deps, action);
|
|
145
|
+
const json = (await response.json().catch(() => null));
|
|
146
|
+
if (json === null || typeof json !== 'object' || Array.isArray(json) || !('balance' in json)) {
|
|
147
|
+
throw new CollectionToolError(`Could not ${action} — the server returned a 200 with an unreadable body (no \`balance\` ` +
|
|
148
|
+
'key). This is NOT "the account has no credits": the request failed and the balance is ' +
|
|
149
|
+
'unknown. Retry — this is a read, so nothing was spent and nothing was lost. Do not ' +
|
|
150
|
+
'tell anyone they are out of credits on the strength of this, and do not abandon paid ' +
|
|
151
|
+
'work you were asked to do because of it.');
|
|
152
|
+
}
|
|
153
|
+
// Cast only AFTER the presence check above: the declared shape says what this client
|
|
154
|
+
// reads, it does not promise the server sent it.
|
|
155
|
+
const body = json;
|
|
156
|
+
const rawCosts = body.costs;
|
|
157
|
+
const costs = Array.isArray(rawCosts)
|
|
158
|
+
? rawCosts
|
|
159
|
+
.filter((row) => row !== null && typeof row === 'object' && !Array.isArray(row))
|
|
160
|
+
.map(toCreditActionCost)
|
|
161
|
+
: [];
|
|
162
|
+
return {
|
|
163
|
+
balance: asCount(body.balance),
|
|
164
|
+
spendable: asCount(body.spendable),
|
|
165
|
+
costs,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
// ---------------------------------------------------------------------------
|
|
169
|
+
// Render
|
|
170
|
+
// ---------------------------------------------------------------------------
|
|
171
|
+
/**
|
|
172
|
+
* The caveat this tool cannot ship without (constraint 2).
|
|
173
|
+
*
|
|
174
|
+
* In the TEXT channel because that is the one an agent always receives (FUL-243: `_meta`
|
|
175
|
+
* is a mirror, and Claude Code drops it). Without it a reader treats a number as a
|
|
176
|
+
* promise: the realistic harm is an agent that reads 100 credits, plans two runs, has the
|
|
177
|
+
* second refused, and reports both as done.
|
|
178
|
+
*/
|
|
179
|
+
const ADVISORY_LINE = 'ADVISORY, NOT A RESERVATION: this is a reading taken just now and it holds nothing back. ' +
|
|
180
|
+
'The balance can change between this call and any spend — a concurrent run, a monthly ' +
|
|
181
|
+
'grant, a refund, or another client on the same account. Only the spend itself decides ' +
|
|
182
|
+
'whether it happens; a tool that returns an insufficient-credit error was NOT overridden by ' +
|
|
183
|
+
'this number. Never report an action as done because the balance said you could afford it.';
|
|
184
|
+
/**
|
|
185
|
+
* The read-only statement (constraint 1).
|
|
186
|
+
*
|
|
187
|
+
* Says what the agent should do INSTEAD when the balance is short, because "you cannot buy
|
|
188
|
+
* credits" without a next step is the kind of dead end an agent works around by retrying
|
|
189
|
+
* the failing call.
|
|
190
|
+
*/
|
|
191
|
+
const READ_ONLY_LINE = 'READ-ONLY: this tool spends nothing, buys nothing and changes no plan. No MCP tool can add ' +
|
|
192
|
+
'credits — topping up is something the account owner does themselves on the Billing page of ' +
|
|
193
|
+
'the Clien.ai web app. If the balance is short, say so and stop; do not retry a refused ' +
|
|
194
|
+
'action hoping for a different answer.';
|
|
195
|
+
/**
|
|
196
|
+
* Where the prices come from, and why they may differ from anything remembered.
|
|
197
|
+
*
|
|
198
|
+
* An agent that has seen a price before will otherwise trust its memory over this list —
|
|
199
|
+
* and a stale price is exactly what makes an affordability answer wrong.
|
|
200
|
+
*/
|
|
201
|
+
const PRICING_LINE = 'PRICES ARE THE SERVER\'S, read live from the same table the charge itself uses. Use these ' +
|
|
202
|
+
'figures and not any price you remember from an earlier session or from documentation — they ' +
|
|
203
|
+
'change, and a stale one makes every number you derive from it wrong.';
|
|
204
|
+
const CHANNEL_NOTE = 'The full record is in `structuredContent.credits` (also mirrored in `_meta`).';
|
|
205
|
+
/**
|
|
206
|
+
* `9007 credits` / `1 credit` — or the honest absence.
|
|
207
|
+
*
|
|
208
|
+
* The singular is not cosmetic. "1 credits each" reads as a template that did not
|
|
209
|
+
* finish, and a reader who distrusts the sentence distrusts the number in it.
|
|
210
|
+
*/
|
|
211
|
+
function creditsClause(value) {
|
|
212
|
+
return value === null ? 'not recorded' : `${value} credit${value === 1 ? '' : 's'}`;
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* The balance sentence.
|
|
216
|
+
*
|
|
217
|
+
* Both numbers, always, and never one standing in for the other: `spendable` is what a
|
|
218
|
+
* spend is checked against, so quoting `balance` alone would overstate the account
|
|
219
|
+
* whenever the FUL-73 rollover cap is biting.
|
|
220
|
+
*/
|
|
221
|
+
function balanceLine(credits) {
|
|
222
|
+
if (credits.balance === null && credits.spendable === null) {
|
|
223
|
+
return ('Credit balance: NOT RECORDED. The server answered without a usable balance, so this is ' +
|
|
224
|
+
'"unknown", not "empty" — do not treat the account as out of credits.');
|
|
225
|
+
}
|
|
226
|
+
return (`Credit balance: ${creditsClause(credits.balance)} on the ledger, ` +
|
|
227
|
+
`${creditsClause(credits.spendable)} spendable right now.`);
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Which number governs, spelled out.
|
|
231
|
+
*
|
|
232
|
+
* `spendable` applies the rollover cap at read time and can sit BELOW `balance`. Two
|
|
233
|
+
* numbers printed side by side with no ruling on which one counts is an invitation to
|
|
234
|
+
* quote the flattering one.
|
|
235
|
+
*/
|
|
236
|
+
const SPENDABLE_RULE_LINE = 'SPENDABLE IS THE NUMBER THAT GOVERNS. It is what every priced action is checked against, ' +
|
|
237
|
+
'and it can be LOWER than the ledger balance (an anti-hoarding cap is applied when the ' +
|
|
238
|
+
'balance is read). Budget with `spendable`; quoting the ledger balance overstates what this ' +
|
|
239
|
+
'account can actually do.';
|
|
240
|
+
/** `clien_research — 50 credits each, 2 affordable now` and its honest partial forms. */
|
|
241
|
+
function renderCost(cost, spendable) {
|
|
242
|
+
const action = safeInline(cost.action, ACTION_MAX) ?? '(action name not recorded)';
|
|
243
|
+
if (cost.credits === null) {
|
|
244
|
+
return ` ${action} — cost not recorded; treat it as unknown, not as free.`;
|
|
245
|
+
}
|
|
246
|
+
const price = creditsClause(cost.credits);
|
|
247
|
+
if (cost.credits <= 0) {
|
|
248
|
+
return ` ${action} — ${price} each, so this one is not metered.`;
|
|
249
|
+
}
|
|
250
|
+
if (spendable === null) {
|
|
251
|
+
return (` ${action} — ${price} each; affordability unknown ` +
|
|
252
|
+
'(no spendable balance was recorded).');
|
|
253
|
+
}
|
|
254
|
+
const affordable = Math.floor(spendable / cost.credits);
|
|
255
|
+
return ` ${action} — ${price} each, ${affordable} affordable now`;
|
|
256
|
+
}
|
|
257
|
+
/** The whole price list, capped and — when capped — saying so. */
|
|
258
|
+
function costBlock(costs, spendable) {
|
|
259
|
+
if (costs.length === 0) {
|
|
260
|
+
return ('The server sent NO price list with this balance. That is a failed read of the prices, ' +
|
|
261
|
+
'not a statement that anything is free — do not guess a cost, and do not assume an action ' +
|
|
262
|
+
'is affordable.');
|
|
263
|
+
}
|
|
264
|
+
const shown = costs.slice(0, COST_RENDER_CAP);
|
|
265
|
+
const dropped = costs.length - shown.length;
|
|
266
|
+
const more = dropped > 0
|
|
267
|
+
? `\n (${dropped} more in \`structuredContent.credits.costs\`)`
|
|
268
|
+
: '';
|
|
269
|
+
const heading = spendable === null
|
|
270
|
+
? 'What each metered action costs (the spendable balance was not recorded, so no ' +
|
|
271
|
+
'affordability count is derivable):'
|
|
272
|
+
: `What each metered action costs, and how many you could afford at ${spendable} ` +
|
|
273
|
+
'spendable credits:';
|
|
274
|
+
return `${heading}\n${shown.map((cost) => renderCost(cost, spendable)).join('\n')}${more}`;
|
|
275
|
+
}
|
|
276
|
+
export async function getCreditBalance(input, deps) {
|
|
277
|
+
// `input ?? {}`: a no-argument tool is routinely called with no `arguments` member at
|
|
278
|
+
// all, and `z.object({}).safeParse(undefined)` fails. Rejecting that would make the
|
|
279
|
+
// tool unusable from a spec-compliant client.
|
|
280
|
+
const parsed = GetCreditBalanceInputSchema.safeParse(input ?? {});
|
|
281
|
+
if (!parsed.success) {
|
|
282
|
+
throw new CollectionToolError(`Invalid input — ${formatZodError(parsed.error)}`);
|
|
283
|
+
}
|
|
284
|
+
const credits = await fetchCreditBalance(deps);
|
|
285
|
+
const text = [
|
|
286
|
+
balanceLine(credits),
|
|
287
|
+
SPENDABLE_RULE_LINE,
|
|
288
|
+
costBlock(credits.costs, credits.spendable),
|
|
289
|
+
PRICING_LINE,
|
|
290
|
+
ADVISORY_LINE,
|
|
291
|
+
READ_ONLY_LINE,
|
|
292
|
+
CHANNEL_NOTE,
|
|
293
|
+
].join('\n\n');
|
|
294
|
+
return {
|
|
295
|
+
content: [{ type: 'text', text }],
|
|
296
|
+
_meta: { credits },
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
//# sourceMappingURL=credits.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"credits.js","sourceRoot":"","sources":["../../src/tools/credits.ts"],"names":[],"mappings":"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"}
|