@biznetgio/neo-app 0.4.75 → 0.4.77

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@biznetgio/neo-app",
3
- "version": "0.4.75",
3
+ "version": "0.4.77",
4
4
  "description": "NEO App CLI — one-command agent onboarding, MCP server, and project ops for coding agents (Codex, Claude Code).",
5
5
  "type": "module",
6
6
  "bin": {
package/prompt.md CHANGED
@@ -6,7 +6,7 @@ Keep this in sync with cli/prompt.md (the npm package's bundled copy).
6
6
  -->
7
7
  # Set this repo up on NEO App
8
8
 
9
- **Version:** 0.4.74 · **Last updated:** 2026-11-05
9
+ **Version:** 0.4.77 · **Last updated:** 2026-11-07
10
10
 
11
11
  These are the official setup instructions from NEO App (Biznet Gio's managed-app
12
12
  platform). If you are an AI coding agent, complete the steps below yourself by running
@@ -9,6 +9,7 @@ import { getCredential } from '../creds.js';
9
9
  import { NeoApi } from '../api.js';
10
10
  import { readLink } from '../link.js';
11
11
  import { log, ok, fail, c } from '../util.js';
12
+ import { redactSecrets } from '../redact.js';
12
13
 
13
14
  // The full lifecycle, with the MCP tool that performs each one. Shown by `neo-app db`.
14
15
  const OPS = [
@@ -59,6 +60,9 @@ export async function db(sub, opts = {}, positionals = []) {
59
60
  let dbs;
60
61
  try { dbs = (await api.get(`/api/v1/projects/${projectId}/databases`)).data?.databases || []; }
61
62
  catch (e) { fail(`list failed: ${e.message || e}`); process.exit(1); }
63
+ // Credentials are write-only: the API hands an owner/admin an unredacted
64
+ // connectionString (Portal reveal), but a terminal must never echo it (#149 retest).
65
+ dbs = redactSecrets(dbs);
62
66
  if (opts.json) return void process.stdout.write(JSON.stringify({ databases: dbs }, null, 2) + '\n');
63
67
  if (!dbs.length) return void ok('no databases in this project');
64
68
  for (const d of dbs) {
@@ -75,6 +79,7 @@ export async function db(sub, opts = {}, positionals = []) {
75
79
  let view;
76
80
  try { view = (await api.get(`/api/v1/databases/${id}`)).data; }
77
81
  catch (e) { fail(`get failed: ${e.message || e}`); process.exit(1); }
82
+ view = redactSecrets(view); // write-only credentials never reach the terminal (#149 retest)
78
83
  if (opts.json) return void process.stdout.write(JSON.stringify(view, null, 2) + '\n');
79
84
  for (const [k, v] of Object.entries(view)) if (v !== undefined && v !== null && v !== '') log(` ${c.dim(k.padEnd(18))} ${v}`);
80
85
  return;
package/src/index.js CHANGED
@@ -109,7 +109,10 @@ export async function main(argv = process.argv.slice(2)) {
109
109
  // still error (nonzero exit) even with `--help`, so a bad command isn't silently
110
110
  // accepted (JNG #97). Per-subcommand help is tracked in #89.
111
111
  const KNOWN_COMMANDS = new Set(['agent', 'setup', 'config', 'login', 'deploy', 'estimate', 'link', 'project', 'status', 'whoami', 'mcp', 'db', 'version', 'help']);
112
- if (positionals.length === 0 || (opts.help && KNOWN_COMMANDS.has(cmd))) return void console.log(HELP);
112
+ // `db --help` renders the DB-specific help (its usage() lists the lifecycle), not the
113
+ // generic top-level help — #149 retest CLI discoverability caveat. Other known commands
114
+ // still fall back to the root HELP (per-subcommand help tracked in #89).
115
+ if (positionals.length === 0 || (opts.help && KNOWN_COMMANDS.has(cmd) && cmd !== 'db')) return void console.log(HELP);
113
116
 
114
117
  try {
115
118
  // `agent setup` and `setup agent` both work (parity with InstaCloud's `setup agent`).
package/src/mcp/tools.js CHANGED
@@ -7,6 +7,7 @@ import { statSync, readFileSync } from 'node:fs';
7
7
  import { deploySource } from '../deploySource.js';
8
8
  import { pkgVersion } from '../util.js';
9
9
  import { specToResources, formatCost } from '../cost.js';
10
+ import { redactSecrets } from '../redact.js';
10
11
 
11
12
  const str = (description) => ({ type: 'string', description });
12
13
  const int = (description) => ({ type: 'integer', description });
@@ -245,22 +246,8 @@ async function billableGate(api, { projectId, kind, name, confirmationId, confir
245
246
  };
246
247
  }
247
248
 
248
- const SECRET_KEY = /(password|secret|secretkey|accesskey|connectionstring|connstr|dsn|token|privatekey|apikey|authorization)/i;
249
- // Deployment snapshots (exact keys) embed rendered configs with plaintext creds — a
250
- // queue's `--pass`, a DB's POSTGRES_PASSWORD. Redact by EXACT key so we don't also
251
- // hit resourceId/source-of-truth-style names (substring match would catch "reSOURCEid").
252
- const SECRET_EXACT = new Set(['source', 'installed']);
253
- function redactSecrets(value) {
254
- if (Array.isArray(value)) return value.map(redactSecrets);
255
- if (value && typeof value === 'object') {
256
- const out = {};
257
- for (const [k, v] of Object.entries(value)) {
258
- out[k] = (SECRET_KEY.test(k) || SECRET_EXACT.has(k.toLowerCase())) ? '[redacted — credentials are write-only; bind them to a consumer instead]' : redactSecrets(v);
259
- }
260
- return out;
261
- }
262
- return value;
263
- }
249
+ // redactSecrets lives in ../redact.js — the single redactor the CLI `db` commands share,
250
+ // so every terminal/JSON output path masks credentials identically (#149 retest).
264
251
 
265
252
  export const TOOLS = [
266
253
  {
@@ -972,7 +959,7 @@ export const TOOLS = [
972
959
  {
973
960
  name: 'neo_estimate',
974
961
  description:
975
- 'Estimate the hourly and monthly cost (IDR) of a service or resource from the LIVE price book, so you can show the user the cost BEFORE a billable create/deploy (handoff 3.3/3.4). Read-only. Compute is resource-based (#144): priced per vCPU-hour + GB-RAM-hour independently (no packages), hourly, capped at 730 billable hours/month. Covers compute (vcpu+memoryMB, or a legacy tier shorthand that resolves to concrete vCPU/RAM), persistent storage, and object storage (all per GB-hour); variable usage (queue messages, cron runs) is excluded and noted. The monthly figure is a non-authoritative estimate (hourly × up to 730). Pass orgId to apply that org\'s price overrides.',
962
+ 'Estimate the hourly and monthly cost (IDR) of a service or resource from the LIVE price book, so you can show the user the cost BEFORE a billable create/deploy (handoff 3.3/3.4). Read-only. Compute is resource-based (#144): priced per vCPU-hour + GB-RAM-hour independently (no packages), hourly, capped at 730 billable hours/month. Covers compute (vcpu+memoryMB, or a legacy tier shorthand that resolves to concrete vCPU/RAM), persistent storage, and object storage (all per GB-hour), plus queue and cron (0 fixed cost — their per-message/per-run rates are disclosed in variable[]). The monthly figure is a non-authoritative estimate (hourly × up to 730). Pass orgId to apply that org\'s price overrides.',
976
963
  mutating: false,
977
964
  schema: {
978
965
  type: 'object',
@@ -982,6 +969,8 @@ export const TOOLS = [
982
969
  memoryMB: int('Memory in MB — priced per GB-RAM-hour (use with vcpu)'),
983
970
  storageGB: int('Persistent/database storage in GB (optional)'),
984
971
  objectStorageGB: int('Object-storage quota in GB (optional)'),
972
+ queue: { type: 'boolean', description: 'Include a NEO Queue — 0 fixed cost; discloses the per-message (queue_message) rate in variable[]' },
973
+ cron: { type: 'boolean', description: 'Include a Scheduled Job — 0 fixed cost; discloses the per-run (cron_run) rate in variable[]' },
985
974
  replicas: int('Replica count (default 1)'),
986
975
  orgId: str('Organization id (optional — applies that org\'s price overrides)'),
987
976
  },
@@ -993,7 +982,7 @@ export const TOOLS = [
993
982
  if (!orgId) { try { orgId = await api.resolveOrgId(); } catch { orgId = null; } }
994
983
  if (!orgId) throw new Error('no organization for this account — cannot estimate cost');
995
984
  const resources = specToResources(args);
996
- if (!resources.length) throw new Error('nothing to estimate — pass computeTier (or vcpu+memoryMB), storageGB, and/or objectStorageGB');
985
+ if (!resources.length) throw new Error('nothing to estimate — pass computeTier (or vcpu+memoryMB), storageGB, objectStorageGB, queue, and/or cron');
997
986
  const { cost } = await api.estimate({ orgId }, { resources });
998
987
  return cost; // normalized contract: currency/hourly/monthly/breakdown/variable/upfront/priceBook/priceBookVersion/assumptions
999
988
  },
package/src/redact.js ADDED
@@ -0,0 +1,26 @@
1
+ // Single redactor for every CLI/MCP path that prints a control-plane response to a
2
+ // terminal, a file, or JSON (#149 retest SECURITY BLOCKER): managed-DB/queue credentials
3
+ // are write-only. The control-plane API still returns an unredacted connectionString to an
4
+ // owner/admin (the Portal's RBAC-gated reveal), so a terminal tool that echoes that response
5
+ // raw leaks a plaintext password. Every CLI/MCP output routes through redactSecrets so a
6
+ // credential never reaches a shell, a log, or a JSON dump — matching MCP's write-only stance.
7
+
8
+ const SECRET_KEY = /(password|secret|secretkey|accesskey|connectionstring|connstr|dsn|token|privatekey|apikey|authorization)/i;
9
+ // Deployment snapshots (exact keys) embed rendered configs with plaintext creds — a
10
+ // queue's `--pass`, a DB's POSTGRES_PASSWORD. Redact by EXACT key so we don't also hit
11
+ // resourceId/source-of-truth-style names (a substring match would catch "reSOURCEid").
12
+ const SECRET_EXACT = new Set(['source', 'installed']);
13
+
14
+ export const REDACTED = '[redacted — credentials are write-only; bind them to a consumer instead]';
15
+
16
+ export function redactSecrets(value) {
17
+ if (Array.isArray(value)) return value.map(redactSecrets);
18
+ if (value && typeof value === 'object') {
19
+ const out = {};
20
+ for (const [k, v] of Object.entries(value)) {
21
+ out[k] = (SECRET_KEY.test(k) || SECRET_EXACT.has(k.toLowerCase())) ? REDACTED : redactSecrets(v);
22
+ }
23
+ return out;
24
+ }
25
+ return value;
26
+ }