@biznetgio/neo-app 0.4.70 → 0.4.73
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/package.json +1 -1
- package/src/commands/deploy.js +13 -0
- package/src/commands/estimate.js +82 -0
- package/src/cost.js +7 -0
- package/src/index.js +19 -1
- package/src/mcp/tools.js +55 -30
package/package.json
CHANGED
package/src/commands/deploy.js
CHANGED
|
@@ -35,6 +35,9 @@ function showCost(r) {
|
|
|
35
35
|
const e = r.estimatedCost;
|
|
36
36
|
const note = (e.assumptions || []).join('; ');
|
|
37
37
|
log(c.bold(`Estimated cost: ${formatCost(e)}`) + (note ? c.dim(` — ${note}`) : ''));
|
|
38
|
+
// Identify the exact price-book snapshot the quote (and any approval) is pinned to (#170 r18a D1).
|
|
39
|
+
const ver = e.priceBookVersion || r.priceBookVersion;
|
|
40
|
+
if (ver) log(c.dim(` price book: ${ver}`));
|
|
38
41
|
}
|
|
39
42
|
}
|
|
40
43
|
|
|
@@ -95,6 +98,16 @@ export async function deploy(source, opts = {}) {
|
|
|
95
98
|
log: step,
|
|
96
99
|
};
|
|
97
100
|
|
|
101
|
+
// --json: machine-readable + non-interactive. A real deploy still requires --yes; otherwise
|
|
102
|
+
// deploySource returns the priced gate (requiresConfirmation + estimatedCost + confirmToken)
|
|
103
|
+
// and deploys nothing. One shape, mirroring the MCP cost schema (#170).
|
|
104
|
+
if (opts.json) {
|
|
105
|
+
const r = await deploySource(api, { ...base, log: () => {}, dryRun, confirm: !!opts.yes });
|
|
106
|
+
process.stdout.write(JSON.stringify(r, null, 2) + '\n');
|
|
107
|
+
if (r.result && r.result !== 'success' && !r.dryRun && !r.requiresConfirmation && !r.pending) process.exitCode = 1;
|
|
108
|
+
return r;
|
|
109
|
+
}
|
|
110
|
+
|
|
98
111
|
// Cost gate: a real deploy is billable, so it needs explicit approval. `--yes` approves
|
|
99
112
|
// up front (headless/CI); otherwise preview the cost first, then confirm (interactive)
|
|
100
113
|
// or require `--yes` (non-interactive). A spec-bound confirmToken links the approval to
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
// `neo-app estimate` — a read-only cost quote from the server's authoritative price book
|
|
2
|
+
// (#170 r18a D2). Works WITHOUT a project (org-scoped), unlike `deploy --dry-run` which
|
|
3
|
+
// needs one, so a vibe coder can price a resource before creating anything. Zero-write.
|
|
4
|
+
|
|
5
|
+
import { resolveConfig } from '../config.js';
|
|
6
|
+
import { getCredential } from '../creds.js';
|
|
7
|
+
import { NeoApi } from '../api.js';
|
|
8
|
+
import { readLink } from '../link.js';
|
|
9
|
+
import { specToResources, formatCost } from '../cost.js';
|
|
10
|
+
import { log, ok, fail, c } from '../util.js';
|
|
11
|
+
|
|
12
|
+
// buildSpec turns the CLI flags into the flat cost spec specToResources understands.
|
|
13
|
+
function buildSpec(opts) {
|
|
14
|
+
const spec = {};
|
|
15
|
+
if (opts.tier) spec.computeTier = opts.tier;
|
|
16
|
+
if (opts.vcpu) spec.vcpu = Number(opts.vcpu);
|
|
17
|
+
if (opts.mem) spec.memoryMB = Number(opts.mem);
|
|
18
|
+
if (opts.storage) spec.storageGB = Number(opts.storage);
|
|
19
|
+
if (opts.object) spec.objectStorageGB = Number(opts.object);
|
|
20
|
+
if (opts.replicas) spec.replicas = Number(opts.replicas);
|
|
21
|
+
if (opts.queue) spec.queue = true;
|
|
22
|
+
if (opts.cron) spec.cron = true;
|
|
23
|
+
return spec;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export async function estimate(opts = {}) {
|
|
27
|
+
const cfg = resolveConfig(opts);
|
|
28
|
+
const cred = getCredential(cfg.baseUrl);
|
|
29
|
+
if (!cred?.token) {
|
|
30
|
+
fail(`not authenticated for ${cfg.baseUrl} — run 'neo-app login'`);
|
|
31
|
+
process.exit(1);
|
|
32
|
+
}
|
|
33
|
+
const api = new NeoApi({ baseUrl: cfg.baseUrl, token: cred.token });
|
|
34
|
+
|
|
35
|
+
const spec = buildSpec(opts);
|
|
36
|
+
const resources = specToResources(spec);
|
|
37
|
+
if (!resources.length) {
|
|
38
|
+
fail('nothing to estimate — pass --tier <micro|small|…> (or --vcpu N --mem MB), --storage GB, --object GB, --queue, or --cron');
|
|
39
|
+
process.exit(2);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Scope: an explicit/linked project (exact org context) else the account's org — the
|
|
43
|
+
// server authority is the same, so a project-less quote still works.
|
|
44
|
+
const projectId = opts.project || readLink()?.projectId;
|
|
45
|
+
let scope;
|
|
46
|
+
if (projectId) scope = { projectId };
|
|
47
|
+
else {
|
|
48
|
+
let orgId = opts.org;
|
|
49
|
+
if (!orgId) { try { orgId = await api.resolveOrgId(); } catch { orgId = null; } }
|
|
50
|
+
if (!orgId) { fail('no project or organization — run \'neo-app login\', or pass --project <id> / --org <id>'); process.exit(1); }
|
|
51
|
+
scope = { orgId };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
let cost;
|
|
55
|
+
try {
|
|
56
|
+
cost = (await api.estimate(scope, { resources })).cost;
|
|
57
|
+
} catch (e) {
|
|
58
|
+
fail(`estimate failed: ${e.message || e}`);
|
|
59
|
+
process.exit(1);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Fail closed when the server can't price a metric (#170: never quote at a guessed rate).
|
|
63
|
+
if (cost?.pricingUnavailable?.length) {
|
|
64
|
+
if (opts.json) process.stdout.write(JSON.stringify({ cost }, null, 2) + '\n');
|
|
65
|
+
fail(`pricing unavailable for: ${cost.pricingUnavailable.join(', ')} — cannot quote`);
|
|
66
|
+
process.exit(1);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (opts.json) {
|
|
70
|
+
process.stdout.write(JSON.stringify({ cost }, null, 2) + '\n');
|
|
71
|
+
return cost;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
log(c.bold(`Estimated cost: ${formatCost(cost)}`));
|
|
75
|
+
for (const l of cost.breakdown || []) log(` · ${l.item}: ${l.monthly} ${cost.currency}/mo (${l.hourly}/h)`);
|
|
76
|
+
for (const u of cost.upfront || []) log(` · ${u.item}: ${u.monthly} ${cost.currency} upfront (hourly: n/a)`);
|
|
77
|
+
for (const v of cost.variable || []) log(c.dim(` usage: ${v.rate} ${cost.currency}/${v.unit || v.metric}`));
|
|
78
|
+
if (cost.priceBookVersion) log(c.dim(` price book: ${cost.priceBookVersion} · billing quantum: ${cost.billingQuantum || 'hour'}`));
|
|
79
|
+
for (const a of cost.assumptions || []) log(c.dim(` assumption: ${a}`));
|
|
80
|
+
ok('estimate complete — nothing was created');
|
|
81
|
+
return cost;
|
|
82
|
+
}
|
package/src/cost.js
CHANGED
|
@@ -26,6 +26,13 @@ export function specToResources(spec = {}) {
|
|
|
26
26
|
}
|
|
27
27
|
if (spec.storageGB) resources.push({ kind: 'database', name: 'storage', volumeSize: `${spec.storageGB}Gi` });
|
|
28
28
|
if (spec.objectStorageGB) resources.push({ kind: 'bucket', name: 'object', quotaGB: spec.objectStorageGB });
|
|
29
|
+
// A cron has no fixed cost — it's billed per run. The server returns it as a 0 fixed
|
|
30
|
+
// line plus the `cron_run` rate in `variable[]`, which is what the gate surfaces (#170).
|
|
31
|
+
if (spec.cron) resources.push({ kind: 'cron', name: 'cron' });
|
|
32
|
+
// A queue bills ONLY per message (queue_message) — never compute/storage (JNG #52). It is
|
|
33
|
+
// its OWN kind, not a database: mapping queue storage to a `database` resource priced it as
|
|
34
|
+
// DB compute+storage and mislabeled it "database storage" (#170 r18a D5).
|
|
35
|
+
if (spec.queue) resources.push({ kind: 'queue', name: 'queue' });
|
|
29
36
|
return resources;
|
|
30
37
|
}
|
|
31
38
|
|
package/src/index.js
CHANGED
|
@@ -16,6 +16,7 @@ ${c.bold('Commands')}
|
|
|
16
16
|
login Authenticate with a NEO App API key (kap_...)
|
|
17
17
|
link <project-id> Link this repo to a project (writes .neo-app.json)
|
|
18
18
|
deploy [path|.zip|git-url] Deploy a directory (default: cwd), a .zip, a Git repo, or --image <ref>
|
|
19
|
+
estimate Price a resource from the server price book (read-only; no project needed)
|
|
19
20
|
(or pass the same via --source <path|.zip|git-url>)
|
|
20
21
|
project create [name] Create a project (defaults to the dir name) and link it
|
|
21
22
|
project list List projects in your organization (find ids to link)
|
|
@@ -35,6 +36,13 @@ ${c.bold('Options')}
|
|
|
35
36
|
--token <kap_...> API key (also NEO_APP_TOKEN; prompted if omitted)
|
|
36
37
|
--yes Non-interactive (headless/CI); also approves a billable deploy without the cost prompt
|
|
37
38
|
--dry-run Print intended changes without writing
|
|
39
|
+
--json Machine-readable output incl. the normalized cost (deploy, estimate)
|
|
40
|
+
--tier <key> estimate: compute tier (micro|small|medium|large|xlarge)
|
|
41
|
+
--vcpu <n> --mem <MB> estimate: custom compute (instead of --tier)
|
|
42
|
+
--storage <GB> estimate: database/persistent storage
|
|
43
|
+
--object <GB> estimate: object-storage quota (billed monthly-upfront)
|
|
44
|
+
--queue / --cron estimate: a queue (per-message) / cron (per-run) resource
|
|
45
|
+
--replicas <n> estimate: replica count
|
|
38
46
|
-v, --version Print version
|
|
39
47
|
-h, --help Show this help`;
|
|
40
48
|
|
|
@@ -63,6 +71,15 @@ const options = {
|
|
|
63
71
|
prod: { type: 'boolean' },
|
|
64
72
|
yes: { type: 'boolean' },
|
|
65
73
|
'dry-run': { type: 'boolean' },
|
|
74
|
+
json: { type: 'boolean' },
|
|
75
|
+
tier: { type: 'string' },
|
|
76
|
+
vcpu: { type: 'string' },
|
|
77
|
+
mem: { type: 'string' },
|
|
78
|
+
storage: { type: 'string' },
|
|
79
|
+
object: { type: 'string' },
|
|
80
|
+
replicas: { type: 'string' },
|
|
81
|
+
queue: { type: 'boolean' },
|
|
82
|
+
cron: { type: 'boolean' },
|
|
66
83
|
version: { type: 'boolean', short: 'v' },
|
|
67
84
|
help: { type: 'boolean', short: 'h' },
|
|
68
85
|
};
|
|
@@ -85,7 +102,7 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
85
102
|
// Root help, and `--help` on a KNOWN command, print usage. An UNKNOWN command must
|
|
86
103
|
// still error (nonzero exit) even with `--help`, so a bad command isn't silently
|
|
87
104
|
// accepted (JNG #97). Per-subcommand help is tracked in #89.
|
|
88
|
-
const KNOWN_COMMANDS = new Set(['agent', 'setup', 'config', 'login', 'deploy', 'link', 'project', 'status', 'whoami', 'mcp', 'version', 'help']);
|
|
105
|
+
const KNOWN_COMMANDS = new Set(['agent', 'setup', 'config', 'login', 'deploy', 'estimate', 'link', 'project', 'status', 'whoami', 'mcp', 'version', 'help']);
|
|
89
106
|
if (positionals.length === 0 || (opts.help && KNOWN_COMMANDS.has(cmd))) return void console.log(HELP);
|
|
90
107
|
|
|
91
108
|
try {
|
|
@@ -96,6 +113,7 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
96
113
|
if (cmd === 'config' && sub === 'install-mcp') return void (await (await import('./commands/install-mcp.js')).installMcp(opts));
|
|
97
114
|
if (cmd === 'login') return void (await (await import('./commands/login.js')).login(opts));
|
|
98
115
|
if (cmd === 'deploy') return void (await (await import('./commands/deploy.js')).deploy(sub, opts));
|
|
116
|
+
if (cmd === 'estimate') return void (await (await import('./commands/estimate.js')).estimate(opts));
|
|
99
117
|
if (cmd === 'link') return void (await (await import('./commands/link.js')).link(sub, opts));
|
|
100
118
|
if (cmd === 'project' && sub === 'link') return void (await (await import('./commands/link.js')).link(positionals[2], opts));
|
|
101
119
|
if (cmd === 'project' && sub === 'create') return void (await (await import('./commands/link.js')).createProject(positionals[2], opts));
|
package/src/mcp/tools.js
CHANGED
|
@@ -153,26 +153,33 @@ function validateBucketArgs(args) {
|
|
|
153
153
|
// args.confirm===true it returns the live-priced cost and creates NOTHING — the
|
|
154
154
|
// agent shows the user, then re-calls with confirm:true. plan/apply remains the
|
|
155
155
|
// server-immutable costed path; this is the lighter client-side gate (handoff 3.3).
|
|
156
|
-
async function confirmCost(api, args, spec) {
|
|
156
|
+
async function confirmCost(api, args, spec, currentSpec) {
|
|
157
157
|
if (args.confirm === true) return null;
|
|
158
158
|
let orgId = null;
|
|
159
159
|
try { orgId = await api.resolveOrgId(args.orgId); } catch { orgId = null; }
|
|
160
160
|
// The server's /estimate is the sole price authority (#170) — the CLI no longer prices
|
|
161
161
|
// locally. Prefer the project scope (exact org context); fall back to org scope for a
|
|
162
|
-
// project-less quote.
|
|
163
|
-
|
|
162
|
+
// project-less quote. When a CURRENT spec is given (a change flow), ask the server for
|
|
163
|
+
// the delta too (current → proposed → increase). Best-effort: a failed estimate must
|
|
164
|
+
// never fail the gate.
|
|
165
|
+
let cost = null, delta = null;
|
|
164
166
|
const resources = specToResources(spec);
|
|
167
|
+
const current = currentSpec ? specToResources(currentSpec) : null;
|
|
165
168
|
if (resources.length && (args.projectId || orgId)) {
|
|
166
|
-
try {
|
|
167
|
-
|
|
169
|
+
try {
|
|
170
|
+
const body = current && current.length ? { resources, current } : { resources };
|
|
171
|
+
const r = await api.estimate(args.projectId ? { projectId: args.projectId } : { orgId }, body);
|
|
172
|
+
cost = r.cost; delta = r.delta || null;
|
|
173
|
+
} catch { cost = null; }
|
|
168
174
|
}
|
|
169
175
|
return {
|
|
170
176
|
requiresConfirmation: true,
|
|
171
177
|
cost,
|
|
178
|
+
delta, // current → proposed → increase, when a current spec was given (#170 change delta)
|
|
172
179
|
priceBookVersion: cost?.priceBookVersion, // pins the quote to a price-book snapshot (#170)
|
|
173
180
|
acting: await actingOf(api, { orgId, projectId: args.projectId }), // whose/where — JNG #131
|
|
174
181
|
priceBook: cost?.priceBook || (orgId ? `org:${orgId}` : 'global-defaults'),
|
|
175
|
-
message: 'Billable
|
|
182
|
+
message: 'Billable change — show the user BOTH the hourly and monthly cost (and the increase delta, plus any per-use variable rates), then re-call with confirm:true to apply. For an immutable, server-pinned quote use neo_plan_stack → neo_apply_stack.',
|
|
176
183
|
};
|
|
177
184
|
}
|
|
178
185
|
|
|
@@ -203,7 +210,7 @@ async function billableGate(api, { projectId, kind, name, confirmationId, confir
|
|
|
203
210
|
// from failing an otherwise-fine create (same spirit as the api-layer idempotent retry).
|
|
204
211
|
let mint;
|
|
205
212
|
for (let attempt = 1; ; attempt++) {
|
|
206
|
-
try { mint = (await api.post('/api/v1/confirmations', { action: 'billable_create', projectId, kind, name, environment, estimatedCost: costNote, priceBookVersion: cost?.priceBookVersion })).data; break; }
|
|
213
|
+
try { mint = (await api.post('/api/v1/confirmations', { action: 'billable_create', projectId, kind, name, environment, estimatedCost: costNote, priceBookVersion: cost?.priceBookVersion, cost })).data; break; }
|
|
207
214
|
catch (e) {
|
|
208
215
|
if (attempt < 3 && (e?.status >= 500 || e?.status === 0)) { await new Promise((r) => setTimeout(r, 300 * attempt)); continue; }
|
|
209
216
|
throw e;
|
|
@@ -499,21 +506,26 @@ export const TOOLS = [
|
|
|
499
506
|
// effective compute — the one being set, else the service's CURRENT compute. Without
|
|
500
507
|
// the current compute, an autoscale-only change estimated to IDR 0 (#123).
|
|
501
508
|
const replicas = args.replicas ?? args.autoscaleMax ?? args.autoscaleMin;
|
|
509
|
+
// Read the service's CURRENT compute/storage once — for the empty-tier default (#123)
|
|
510
|
+
// AND the change delta (current → proposed → increase).
|
|
511
|
+
let svc = {};
|
|
512
|
+
try { const d = (await api.get(`/api/v1/apps/${appId}`)).data; svc = (d && d.application) || d || {}; } catch { /* best-effort */ }
|
|
513
|
+
// A bundled service with no explicit tier bills at the default "micro" (store.DefaultTierKey);
|
|
514
|
+
// using the empty cpuLimit as-is priced the gate at IDR 0 (#123).
|
|
515
|
+
const svcSpec = (svc.billingMode === 'unbundled' && (svc.cpuLimit || svc.memoryLimit))
|
|
516
|
+
? { vcpu: cpuToVcpu(svc.cpuLimit), memoryMB: memToMB(svc.memoryLimit) }
|
|
517
|
+
: { computeTier: svc.cpuLimit || 'micro' };
|
|
502
518
|
let spec;
|
|
503
519
|
if (computeTier) spec = { computeTier, replicas };
|
|
504
520
|
else if (args.cpuLimit || args.memoryLimit) spec = { vcpu: cpuToVcpu(args.cpuLimit), memoryMB: memToMB(args.memoryLimit), replicas };
|
|
505
|
-
else {
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
spec = (svc.billingMode === 'unbundled' && (svc.cpuLimit || svc.memoryLimit))
|
|
511
|
-
? { vcpu: cpuToVcpu(svc.cpuLimit), memoryMB: memToMB(svc.memoryLimit), replicas }
|
|
512
|
-
: { computeTier: svc.cpuLimit || 'micro', replicas };
|
|
521
|
+
else spec = { ...svcSpec, replicas };
|
|
522
|
+
const currentSpec = { ...svcSpec, replicas: svc.replicas ?? svc.scaleMin };
|
|
523
|
+
if (args.volumeSize !== undefined) { // persistent-volume price in the gate (JNG #129)
|
|
524
|
+
spec = { ...spec, storageGB: gbFromSize(args.volumeSize) };
|
|
525
|
+
if (svc.volumeSize) currentSpec.storageGB = gbFromSize(svc.volumeSize);
|
|
513
526
|
}
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
if (gate) return { ...gate, change: 'update service compute/scaling/volume', message: 'This changes cost — re-call with confirm:true to apply.' };
|
|
527
|
+
const gate = await confirmCost(api, args, spec, currentSpec);
|
|
528
|
+
if (gate) return { ...gate, change: 'update service compute/scaling/volume', message: 'This changes cost — show BOTH the hourly and monthly cost and the increase, then re-call with confirm:true to apply.' };
|
|
517
529
|
}
|
|
518
530
|
const { status, data } = await api.put(`/api/v1/apps/${appId}`, body);
|
|
519
531
|
const out = redactSecrets(result(status, data));
|
|
@@ -864,15 +876,16 @@ export const TOOLS = [
|
|
|
864
876
|
},
|
|
865
877
|
async call(api, args) {
|
|
866
878
|
if (args.confirm !== true) {
|
|
867
|
-
let computeTier;
|
|
879
|
+
let computeTier, currentReplicas;
|
|
868
880
|
try {
|
|
869
881
|
const apps = (await api.get(`/api/v1/projects/${args.projectId}/apps`)).data;
|
|
870
882
|
const list = Array.isArray(apps) ? apps : (apps.applications || []);
|
|
871
883
|
const svc = list.find((a) => a.name === args.service || a.id === args.service);
|
|
872
|
-
computeTier = svc
|
|
884
|
+
if (svc) { computeTier = svc.cpuLimit || svc.computeTier; currentReplicas = svc.replicas ?? svc.scaleMin; }
|
|
873
885
|
} catch { /* best effort — gate regardless */ }
|
|
874
|
-
|
|
875
|
-
|
|
886
|
+
// Price the delta against the current replica count so the user sees current → proposed → increase.
|
|
887
|
+
const gate = await confirmCost(api, args, { computeTier, replicas: args.replicas }, { computeTier, replicas: currentReplicas });
|
|
888
|
+
if (gate) return { ...gate, change: `scale "${args.service}" to ${args.replicas} replica(s)`, message: 'Scaling changes cost — show the user BOTH time bases and the increase, then re-call with confirm:true to apply.' };
|
|
876
889
|
}
|
|
877
890
|
const { status, data } = await api.post(
|
|
878
891
|
`/api/v1/projects/${args.projectId}/services/${args.service}/scale`,
|
|
@@ -1184,8 +1197,12 @@ export const TOOLS = [
|
|
|
1184
1197
|
async call(api, args) {
|
|
1185
1198
|
const costChanging = args.volumeSize || args.replicas !== undefined;
|
|
1186
1199
|
if (costChanging && args.confirm !== true) {
|
|
1187
|
-
|
|
1188
|
-
|
|
1200
|
+
// Read current storage for the change delta (current → proposed → increase).
|
|
1201
|
+
let cur = {};
|
|
1202
|
+
try { const d = (await api.get(`/api/v1/databases/${args.databaseId}`)).data; cur = (d && d.database) || d || {}; } catch { /* best-effort */ }
|
|
1203
|
+
const currentSpec = { storageGB: cur.volumeSize ? gbFromSize(cur.volumeSize) : undefined, replicas: cur.replicas };
|
|
1204
|
+
const gate = await confirmCost(api, args, { storageGB: gbFromSize(args.volumeSize), replicas: args.replicas }, currentSpec);
|
|
1205
|
+
if (gate) return { ...gate, change: 'resize database (storage/replicas)', message: 'Resizing changes cost — show BOTH time bases and the increase, then re-call with confirm:true to apply.' };
|
|
1189
1206
|
}
|
|
1190
1207
|
const body = {};
|
|
1191
1208
|
if (args.volumeSize) body.volumeSize = args.volumeSize;
|
|
@@ -1304,7 +1321,7 @@ export const TOOLS = [
|
|
|
1304
1321
|
},
|
|
1305
1322
|
async call(api, args) {
|
|
1306
1323
|
const environment = requireEnv(args.environment, args.allowCrossEnvironment); // explicit, else the session pin (JNG #136)
|
|
1307
|
-
const gate = await billableGate(api, { projectId: args.projectId, kind: 'queue', name: args.name, confirmationId: args.confirmationId, confirm: args.confirm, environment }, {
|
|
1324
|
+
const gate = await billableGate(api, { projectId: args.projectId, kind: 'queue', name: args.name, confirmationId: args.confirmationId, confirm: args.confirm, environment }, { queue: true }); // queue bills per-message only (JNG #52) — 0 fixed + queue_message rate
|
|
1308
1325
|
if (gate.requiresConfirmation) return { environment, ...gate };
|
|
1309
1326
|
const { projectId, confirm, confirmationId, orgId, ...body } = args;
|
|
1310
1327
|
body.environment = environment; // send the resolved env so the pin actually targets it
|
|
@@ -1359,8 +1376,10 @@ export const TOOLS = [
|
|
|
1359
1376
|
async call(api, args) {
|
|
1360
1377
|
const costChanging = args.volumeSize || args.planClass;
|
|
1361
1378
|
if (costChanging && args.confirm !== true) {
|
|
1362
|
-
|
|
1363
|
-
|
|
1379
|
+
// A queue has no fixed cost — it bills per message (queue_message, JNG #52), so a
|
|
1380
|
+
// storage/plan change doesn't move the fixed bill. The gate surfaces the per-message rate.
|
|
1381
|
+
const gate = await confirmCost(api, args, { queue: true });
|
|
1382
|
+
if (gate) return { ...gate, change: 'update queue (storage/plan)', message: 'Queue bills per message — no fixed-cost change; the queue_message rate is shown. Re-call with confirm:true to apply.' };
|
|
1364
1383
|
}
|
|
1365
1384
|
const { queueId, confirm, orgId, ...body } = args;
|
|
1366
1385
|
const { status, data } = await api.put(`/api/v1/queues/${queueId}`, body);
|
|
@@ -1444,7 +1463,7 @@ export const TOOLS = [
|
|
|
1444
1463
|
},
|
|
1445
1464
|
async call(api, args) {
|
|
1446
1465
|
const environment = requireEnv(args.environment, args.allowCrossEnvironment); // explicit, else the session pin (JNG #136)
|
|
1447
|
-
const gate = await billableGate(api, { projectId: args.projectId, kind: 'cron', name: args.name, confirmationId: args.confirmationId, confirm: args.confirm }, {}); // cron
|
|
1466
|
+
const gate = await billableGate(api, { projectId: args.projectId, kind: 'cron', name: args.name, confirmationId: args.confirmationId, confirm: args.confirm }, { cron: true }); // cron has no fixed cost — the contract surfaces the per-run `cron_run` rate in cost.variable
|
|
1448
1467
|
if (gate.requiresConfirmation) return { environment, ...gate };
|
|
1449
1468
|
// Merge contentType into headers for convenience (unless already set).
|
|
1450
1469
|
if (args.contentType) {
|
|
@@ -2027,7 +2046,11 @@ export const TOOLS = [
|
|
|
2027
2046
|
},
|
|
2028
2047
|
async call(api, args) {
|
|
2029
2048
|
const { status, data } = await api.post(`/api/v1/databases/${args.databaseId}/public-access`, { allowlistCidrs: args.allowlistCidrs || [] });
|
|
2030
|
-
|
|
2049
|
+
const out = redactSecrets(result(status, data));
|
|
2050
|
+
// Public access adds no fixed monthly charge, but egress through the gateway is metered
|
|
2051
|
+
// per GB (usage-based). Surface that so enabling it isn't a silent billing change (#170).
|
|
2052
|
+
if (out && typeof out === 'object') out.billingNote = 'No fixed monthly cost is added. Data transfer (egress) through the public TLS gateway is metered per GB — see the usage-based rates via neo_estimate (cost.variable).';
|
|
2053
|
+
return out;
|
|
2031
2054
|
},
|
|
2032
2055
|
},
|
|
2033
2056
|
{
|
|
@@ -2054,7 +2077,9 @@ export const TOOLS = [
|
|
|
2054
2077
|
},
|
|
2055
2078
|
async call(api, args) {
|
|
2056
2079
|
const { status, data } = await api.post(`/api/v1/queues/${args.queueId}/public-access`, { allowlistCidrs: args.allowlistCidrs || [] });
|
|
2057
|
-
|
|
2080
|
+
const out = redactSecrets(result(status, data));
|
|
2081
|
+
if (out && typeof out === 'object') out.billingNote = 'No fixed monthly cost is added. Data transfer (egress) through the public TLS gateway is metered per GB — see the usage-based rates via neo_estimate (cost.variable).';
|
|
2082
|
+
return out;
|
|
2058
2083
|
},
|
|
2059
2084
|
},
|
|
2060
2085
|
{
|