@descix/cli 2.0.3 → 3.0.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 CHANGED
@@ -183,48 +183,6 @@ build variables the server returns injected into its environment:
183
183
  }
184
184
  ```
185
185
 
186
- ## Airdrop Admin Operations (WS-ADMIN-B1)
187
-
188
- Admin-only commands for triggering airdrop migration batch runs. Per CEO-D-MANUAL-TRIGGER-NO-CRON (2026-04-20), Cloud Scheduler cron was dropped from Round B in favor of operator-invoked manual triggers via this CLI.
189
-
190
- **Access:** requires platform-admin membership (enforced server-side via `isPlatformAdmin(user)`).
191
-
192
- ### `descix airdrop execute-queue`
193
-
194
- Manually trigger the server-side `airdrop_execute_queue` command on the target environment. Assembles `pending_migrations` rows into per-community batches and (when `BATCH_UPDATE_BALANCES_BROADCAST_ENABLED=true`) broadcasts to the Powch HD wallet. When the broadcast flag is off (default for DEV), batches are assembled but the downstream Powch handler returns `stopped_at_broadcast_boundary` — useful for verifying queue shape without spending MATIC.
195
-
196
- **Read-only is the default.** Without `--apply`, the command runs as a dry run.
197
-
198
- **Options:**
199
-
200
- - `--dry-run`: Read-only preview — encode calldata, estimate gas, validate the net-zero invariant. No PK, no transaction, no state mutation. Mutually exclusive with `--apply`.
201
- - `--apply`: Live execution. Requires `--signer-pk-file` or the interactive prompt, and requires `--community`.
202
- - `--community <slug>`: Community slug for per-community batch scoping. **Required for `--apply`**; optional in dry-run mode, where omitting it returns an aggregate preview across communities.
203
- - `--signer-pk-file <path>`: Path to a file containing the admin signer PK (`0x` + 64 hex). Used for `--apply`; when omitted, the PK is requested at an interactive no-echo prompt. Inline PK flags and environment variables are rejected on security grounds (process-listing leak).
204
- - `--batch-size <n>`: Cap on users processed this run (server caps at `AIRDROP_MAX_RUN_USERS`; a larger `--batch-size` is clamped).
205
-
206
- **Examples:**
207
-
208
- ```bash
209
- # Dry run against DEV — safe to run repeatedly; no PK, no tx, no state mutation
210
- descix airdrop execute-queue --env dev --dry-run
211
-
212
- # Dry run scoped to one community
213
- descix airdrop execute-queue --env dev --dry-run --community smile
214
-
215
- # Live execution, capped at 10 users this invocation (prompts for the signer PK)
216
- descix airdrop execute-queue --env dev --apply --community smile --batch-size 10
217
-
218
- # Live execution reading the signer PK from a file
219
- descix airdrop execute-queue --env dev --apply --community smile --signer-pk-file ./signer.key
220
- ```
221
-
222
- **Testing:**
223
-
224
- ```bash
225
- cd DeSciX_Core/descix-cli && npm test
226
- ```
227
-
228
186
  ## Documentation
229
187
 
230
188
  For full documentation, please refer to the [DeSciX SDK Documentation](./SERVICE_README_sdk.md).
package/bin/descix.js CHANGED
@@ -7,7 +7,7 @@
7
7
  * Combines CLI and MCP functionality with unified authentication.
8
8
  */
9
9
 
10
- import { Command } from 'commander';
10
+ import { Command, Option } from 'commander';
11
11
  import chalk from 'chalk';
12
12
  import { DeSciXApiClient } from '../lib/api-client.js';
13
13
  import { requireAuth } from '../lib/auth-guard.js';
@@ -28,7 +28,6 @@ import * as authCommands from '../lib/commands/auth.js';
28
28
  import * as configCommands from '../lib/commands/config.js';
29
29
  import * as buyCommands from '../lib/commands/buy.js';
30
30
  import * as creditsCommands from '../lib/commands/credits.js';
31
- import * as airdropCommands from '../lib/commands/airdrop.js';
32
31
  import { MICROSERVICE_AUTH_MODEL } from '../lib/commands/microservice-auth-model.js';
33
32
  import { runInit } from '../lib/commands/init.js';
34
33
  // "May I prompt?" has ONE OWNER. No command in this file derives it.
@@ -430,30 +429,6 @@ creditsCommand
430
429
  }
431
430
  });
432
431
 
433
- // ============ Airdrop Commands (WS-ADMIN-B1 manual-trigger) ============
434
- // Per CEO-D-MANUAL-TRIGGER-NO-CRON (2026-04-20), airdrop batch execution is operator-triggered
435
- // via this CLI command group rather than Cloud Scheduler cron. Server-side access is gated on
436
- // platform-admin membership (`isPlatformAdmin(user)`); the server emits an on_chain_log row
437
- // with `caller.operator_email` for audit.
438
-
439
- const airdropCommand = program
440
- .command('airdrop')
441
- .description('Admin airdrop migration operations (WS-ADMIN-B1)');
442
-
443
- airdropCommand
444
- .command('execute-queue')
445
- .description('DRY-RUN ONLY preview of the airdrop migration queue (admin-only). Execution lives in daita-sam (act plan + the role holder\'s own wallet) — the apply leg is removed.')
446
- .option('--community <slug>', 'Community slug for per-community batch scoping')
447
- .option('--dry-run', 'Read-only preview: encode calldata, estimate gas, validate net-zero invariant. No PK, no tx, no state mutation. (The default; the flag is accepted for scripts.)')
448
- .option('--batch-size <n>', 'Cap on users processed this run (server caps at AIRDROP_MAX_RUN_USERS)')
449
- .action(async (options) => {
450
- try {
451
- await airdropCommands.executeQueue(options);
452
- } catch (error) {
453
- fail(error);
454
- }
455
- });
456
-
457
432
  // ============ Sync Commands: REMOVED ============
458
433
  // `sync assets`, `sync site` and `sync kb` are all gone, so the `sync` GROUP is gone with
459
434
  // them: a registered verb with no working children is dead weight. Every retired verb is
@@ -1688,7 +1663,8 @@ appCommand
1688
1663
  // ============ App: set-default-model (WS-CONFIG-BOOTSTRAP-FIX item #10) ============
1689
1664
  //
1690
1665
  // Updates Community/{c}/Apps/{a}.default_app_model. Per CEO inheritance chain:
1691
- // options.model > kb.kb_model_override > app.default_app_model > levelConfig.model > DEFAULT_AI_MODEL
1666
+ // request intelligence_level > kb.kb_model_override > app.default_app_model > the level's model
1667
+ // (there is no separate default model: the default is the model of DEFAULT_INTELLIGENCE_LEVEL)
1692
1668
  //
1693
1669
  // --clear deletes the field via FieldValue.delete() (NOT null) so the resolver falls through
1694
1670
  // cleanly to per-level platform defaults.
@@ -1697,13 +1673,13 @@ appCommand
1697
1673
  .command('set-default-model')
1698
1674
  .description('Set or clear App.default_app_model (model used when no KB override is set)')
1699
1675
  .requiredOption('-a, --app <app_id>', 'App ID (e.g. unk-cos)')
1700
- .option('-m, --model <model_name>', 'Gemini model name (e.g. gemini-3.1-flash-lite). Mutually exclusive with --clear.')
1676
+ .option('-m, --model <model_name>', 'Model name (e.g. gemini-3.8-flash). Mutually exclusive with --clear.')
1701
1677
  .option('--clear', 'Delete App.default_app_model via FieldValue.delete() (resets to platform-default inheritance)')
1702
1678
  .addHelpText('after', `
1703
- NOTE: Pinning a "Pro" model (e.g., gemini-2.5-pro, gemini-3.1-pro-preview) as
1704
- an app or KB override will fail at L1 — these models require thinking mode and
1705
- reject L1's thinkingBudget=0. If you intend an L1-compatible override, use a
1706
- "Flash" model (gemini-2.5-flash, gemini-3.1-flash-lite, gemini-3-flash-preview).
1679
+ NOTE: A pinned model is paired with a thinking_level, and which levels exist is a
1680
+ property of the MODEL. The server refuses, by name, a (model, thinking_level) pair
1681
+ it does not support. The current intelligence levels (each names its own model and
1682
+ thinking_level) are printed under "Per-level" after this command runs.
1707
1683
  `)
1708
1684
  .action(async (options) => {
1709
1685
  try {
@@ -1845,12 +1821,12 @@ kbCommand
1845
1821
  .description('Set KB.kb_model_override (per-KB model selection)')
1846
1822
  .requiredOption('-a, --app <app_id>', 'App ID')
1847
1823
  .requiredOption('-k, --kb <kb_name>', 'KB name')
1848
- .requiredOption('-m, --model <model_name>', 'Gemini model name (e.g. gemini-2.5-pro)')
1824
+ .requiredOption('-m, --model <model_name>', 'Model name (e.g. gemini-3.8-flash)')
1849
1825
  .addHelpText('after', `
1850
- NOTE: Pinning a "Pro" model (e.g., gemini-2.5-pro, gemini-3.1-pro-preview) as
1851
- an app or KB override will fail at L1 — these models require thinking mode and
1852
- reject L1's thinkingBudget=0. If you intend an L1-compatible override, use a
1853
- "Flash" model (gemini-2.5-flash, gemini-3.1-flash-lite, gemini-3-flash-preview).
1826
+ NOTE: A pinned model is paired with a thinking_level, and which levels exist is a
1827
+ property of the MODEL. The server refuses, by name, a (model, thinking_level) pair
1828
+ it does not support. The current intelligence levels (each names its own model and
1829
+ thinking_level) are printed under "Per-level" after this command runs.
1854
1830
  `)
1855
1831
  .action(async (options) => {
1856
1832
  try {
@@ -2372,7 +2348,11 @@ siteCommand
2372
2348
 
2373
2349
  console.log(chalk.cyan(`\n Site Upload: ${appId} → GCS\n`));
2374
2350
  console.log(chalk.gray(` Source: ${sourceLabel}`));
2375
- console.log(chalk.gray(` Target: ${communityId}/${appId}`));
2351
+ // The write prefix is server-owned and FLAT ({env}/{appId}/site/ — community_id is
2352
+ // derived server-side from Products/{app_id} and a caller-supplied one is ignored).
2353
+ // Printing a locally-guessed community here once read as a changed write target and
2354
+ // held up a prod upload (FRAQTL wall, 2026-10-12) — say only what this client knows.
2355
+ console.log(chalk.gray(` Target: {env}/${appId}/site/ (flat; community resolved server-side)`));
2376
2356
  if (options.preview) console.log(chalk.yellow(` Mode: PREVIEW`));
2377
2357
  if (gitStatus.isGitRepo) {
2378
2358
  console.log(chalk.gray(` Git: ${gitStatus.branch} @ ${gitStatus.shortHash}`));
@@ -3381,11 +3361,15 @@ program
3381
3361
  .option('--apps <ids>', 'Comma-separated app IDs for cross-app query')
3382
3362
  .option('--level <n>', 'Intelligence level (1-5)', parseInt)
3383
3363
  .option('--model <name>', 'Explicit model override')
3384
- .option('--thinking <budget>', 'Thinking token budget (-1=dynamic, 0=off, N=fixed)', parseInt)
3364
+ .option('--thinking-level <level>', 'Thinking level for this call: minimal, low, medium or high. The server refuses a level the resolved model does not support.')
3365
+ .addOption(new Option('--thinking <budget>').hideHelp())
3385
3366
  .option('--tokens', 'Show token usage in response footer')
3386
3367
  .option('--new', 'Start a new conversation (clear session)')
3387
3368
  .action(async (questionArgs, options) => {
3388
3369
  try {
3370
+ if (options.thinking !== undefined) {
3371
+ throw new Error('--thinking (a token budget) was removed. Use --thinking-level <level>, one of minimal, low, medium, high.');
3372
+ }
3389
3373
  const apiClient = new DeSciXApiClient();
3390
3374
  await requireAuth(apiClient);
3391
3375
 
@@ -3411,7 +3395,7 @@ program
3411
3395
  user_input: question,
3412
3396
  intelligence_level: options.level,
3413
3397
  model: options.model,
3414
- thinking_budget: options.thinking,
3398
+ thinking_level: options.thinkingLevel,
3415
3399
  streaming: false
3416
3400
  });
3417
3401
  const result = response.message || response;
@@ -3518,7 +3502,7 @@ program
3518
3502
  streaming: false,
3519
3503
  intelligence_level: options.level,
3520
3504
  model: options.model,
3521
- thinking_budget: options.thinking,
3505
+ thinking_level: options.thinkingLevel,
3522
3506
  };
3523
3507
 
3524
3508
  if (useMultiKb) {
@@ -12,6 +12,7 @@ import * as path from 'path';
12
12
  import { fileURLToPath } from 'url';
13
13
  import { readIdentity, isIdentityNamed } from './workspace-identity.js';
14
14
  import { mcpServerEntry } from './invocation.js';
15
+ import { CLI_VERSION } from './cli-version.js';
15
16
 
16
17
  const __filename = fileURLToPath(import.meta.url);
17
18
  const __dirname = path.dirname(__filename);
@@ -71,7 +72,7 @@ async function loadContext(workspaceRoot) {
71
72
  // Friendly name: capitalize app_id or use as-is
72
73
  const appName = appId.charAt(0).toUpperCase() + appId.slice(1);
73
74
 
74
- return { appId, communityId, apiUrl: originLine, appName, originSource };
75
+ return { appId, communityId, apiUrl: originLine, appName, originSource, cliVersion: CLI_VERSION };
75
76
  }
76
77
 
77
78
  /**
@@ -82,7 +83,9 @@ function render(template, vars) {
82
83
  .replace(/\{\{appId\}\}/g, vars.appId)
83
84
  .replace(/\{\{communityId\}\}/g, vars.communityId)
84
85
  .replace(/\{\{apiUrl\}\}/g, vars.apiUrl)
85
- .replace(/\{\{appName\}\}/g, vars.appName);
86
+ .replace(/\{\{appName\}\}/g, vars.appName)
87
+ .replace(/\{\{originSource\}\}/g, vars.originSource)
88
+ .replace(/\{\{cliVersion\}\}/g, vars.cliVersion);
86
89
  }
87
90
 
88
91
  /**
@@ -0,0 +1,68 @@
1
+ /**
2
+ * THE ONE OWNER of the files `descix app init` stamps into an app repo beyond the workspace and
3
+ * the agent instruction files (proposal 2026-10-09-standard-app-repo-form, items 3 and 5):
4
+ *
5
+ * .descix/manifests/site.json the deployable site set (`descix site upload` reads it)
6
+ * .claude-plugin/plugin.json the Claude plugin stub
7
+ * site/index.html the front-page template (templates/scaffolds/frontpage/)
8
+ *
9
+ * ONLY-IF-ABSENT: a file that exists is never read-modified, never overwritten, never reported as
10
+ * written. The agent files (CLAUDE.md …) stay with lib/agent-files.js; this module does not write
11
+ * them.
12
+ */
13
+ import fs from 'fs/promises';
14
+ import path from 'path';
15
+ import { fileURLToPath } from 'url';
16
+
17
+ const FRONTPAGE_TEMPLATE = path.join(
18
+ path.dirname(fileURLToPath(import.meta.url)), '..', 'templates', 'scaffolds', 'frontpage', 'index.html',
19
+ );
20
+
21
+ const escapeHtml = (s) => String(s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
22
+
23
+ async function writeIfAbsent(file, content) {
24
+ await fs.mkdir(path.dirname(file), { recursive: true });
25
+ try {
26
+ await fs.writeFile(file, content, { encoding: 'utf8', flag: 'wx' });
27
+ return true;
28
+ } catch (err) {
29
+ if (err.code === 'EEXIST') return false;
30
+ throw err;
31
+ }
32
+ }
33
+
34
+ /**
35
+ * @param {string} appPath the app repo root
36
+ * @param {{appId: string, communityId: string}} identity
37
+ * @returns {Promise<string[]>} repo-relative paths actually created
38
+ */
39
+ export async function scaffoldAppRepo(appPath, { appId, communityId }) {
40
+ if (!appId || !communityId) {
41
+ throw new Error('scaffoldAppRepo: appId and communityId are required (no default would be true).');
42
+ }
43
+ const siteManifest = {
44
+ _purpose: `Deployable site set for the ${appId} app; sources are uploaded by descix site upload.`,
45
+ sources: [{ path: 'site' }],
46
+ buildCommand: null,
47
+ };
48
+ const plugin = {
49
+ name: appId,
50
+ description: `${appId}, an app of the ${communityId} community on DeSciX.`,
51
+ version: '0.1.0',
52
+ author: { name: communityId },
53
+ };
54
+ const frontpage = (await fs.readFile(FRONTPAGE_TEMPLATE, 'utf8'))
55
+ .replace(/\{\{APP_ID\}\}/g, escapeHtml(appId))
56
+ .replace(/\{\{COMMUNITY_ID\}\}/g, escapeHtml(communityId));
57
+
58
+ const files = [
59
+ ['.descix/manifests/site.json', JSON.stringify(siteManifest, null, 2) + '\n'],
60
+ ['.claude-plugin/plugin.json', JSON.stringify(plugin, null, 2) + '\n'],
61
+ ['site/index.html', frontpage],
62
+ ];
63
+ const created = [];
64
+ for (const [rel, content] of files) {
65
+ if (await writeIfAbsent(path.join(appPath, rel), content)) created.push(rel);
66
+ }
67
+ return created;
68
+ }
@@ -15,6 +15,7 @@ import path from 'path';
15
15
  import { WorkspaceConfig, assertNoParentWorkspace } from '../workspace-config.js';
16
16
  import { refreshCommunityIdentity, printIdentityReceipt } from './communityIdentity.js';
17
17
  import { generateAgentFiles } from '../agent-files.js';
18
+ import { scaffoldAppRepo } from '../app-scaffold.js';
18
19
 
19
20
  /**
20
21
  * @param {Object} apiClient an authenticated DeSciXApiClient
@@ -130,6 +131,7 @@ export async function runAppInit(apiClient, options) {
130
131
  await fs.writeFile(descPath, `# ${appId}\n\nApplication description goes here.\n`);
131
132
  }
132
133
  console.log(chalk.gray(` Created: site/, microservice/, assets/`));
134
+ for (const f of await scaffoldAppRepo(appPath, { appId, communityId })) console.log(chalk.green(` ✓ ${f}`));
133
135
 
134
136
  // 3. The default KB — CREATE leg only (see the adopt block above: map never creates).
135
137
  let kbResult = null;
@@ -7,8 +7,9 @@
7
7
  * - descix kb set-override-model -a <app_id> -k <kb_name> -m <model_name> [--env=dev|demo|prod]
8
8
  * - descix kb clear-override-model -a <app_id> -k <kb_name> [--env=dev|demo|prod]
9
9
  *
10
- * Per CEO 2026-05-26 inheritance chain:
11
- * options.model || kb.kb_model_override || app.default_app_model || levelConfig.model || utils.DEFAULT_AI_MODEL
10
+ * Resolver order (the server owns it; this CLI prints the chain the server reports):
11
+ * request intelligence_level > kb.kb_model_override (+ its thinking_level) > app.default_app_model > the level's model
12
+ * There is no separate default model: the default is the model of the platform's default intelligence level.
12
13
  *
13
14
  * --clear flows use FieldValue.delete() server-side (NOT null) per tripwire #2.
14
15
  *
@@ -110,12 +111,10 @@ function identityFromApiClient(apiClient) {
110
111
  function printResolution({ app_id, resolution }) {
111
112
  if (!resolution) return;
112
113
  const dam = resolution.default_app_model === null ? 'null' : `"${resolution.default_app_model}"`;
113
- const dai = resolution.default_ai_model ? `"${resolution.default_ai_model}"` : 'null';
114
114
 
115
115
  console.log(chalk.cyan('\nResolution chain that will apply going forward:'));
116
116
  console.log(chalk.gray(` ${resolution.chain}`));
117
117
  console.log(chalk.gray(` app.default_app_model = ${dam}`));
118
- console.log(chalk.gray(` utils.DEFAULT_AI_MODEL = ${dai}`));
119
118
 
120
119
  const levels = resolution.intelligence_levels || {};
121
120
  const levelKeys = Object.keys(levels).sort();
@@ -24,6 +24,11 @@ const kbSyncRefusal = (invocation) =>
24
24
  `\`${CANONICAL_KB_CREATE}\`.`;
25
25
  const KB_SYNC_DESCRIPTION = `REMOVED — use \`${CANONICAL_KB_SYNC}\``;
26
26
 
27
+ const AIRDROP_QUEUE_REFUSAL = (invocation) =>
28
+ `❌ \`${invocation}\` has been REMOVED: the pending_migrations queue it previewed (lane B) no longer ` +
29
+ `exists, and neither does the server command behind it.\n\n` +
30
+ `Distributions run through the offers rail on daita-sam (act plan + the role holder's own wallet signs).`;
31
+
27
32
  /**
28
33
  * Every retired verb. `invocation` is what a user types; `parent` is the id of the commander
29
34
  * parent it hangs off (a root passed to registerAllRetiredVerbs, or an earlier row); `refusal`
@@ -52,6 +57,12 @@ export const RETIRED_VERBS = Object.freeze([
52
57
  refusal: () =>
53
58
  '"descix microservice register-delegate" has been removed: there is no per-app microservice ' +
54
59
  'delegate key.\n' + MICROSERVICE_AUTH_MODEL },
60
+ // Lane B (the pending_migrations queue) is struck (CEO word Unkamon 1530eb6, daita-sam#4).
61
+ { id: 'airdrop', parent: 'program', name: 'airdrop', invocation: 'descix airdrop',
62
+ description: 'removed — see error text', refusal: (invocation) => AIRDROP_QUEUE_REFUSAL(invocation) },
63
+ { id: 'airdrop.execute-queue', parent: 'airdrop', name: 'execute-queue',
64
+ invocation: 'descix airdrop execute-queue', description: 'removed — see error text',
65
+ refusal: (invocation) => AIRDROP_QUEUE_REFUSAL(invocation) },
55
66
  ]);
56
67
 
57
68
  /** Implementations deleted with the KB sync surfaces. No exported symbol survives without a caller. */
@@ -2,9 +2,6 @@
2
2
  "admin-login": [
3
3
  "admin_bootstrap_login"
4
4
  ],
5
- "airdrop execute-queue": [
6
- "airdrop_execute_queue"
7
- ],
8
5
  "app delete": [
9
6
  "delete_app"
10
7
  ],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@descix/cli",
3
- "version": "2.0.3",
3
+ "version": "3.0.0",
4
4
  "description": "DeSciX CLI and MCP Server - Unified command-line interface for DeSciX platform",
5
5
  "repository": {
6
6
  "type": "git",
@@ -40,7 +40,7 @@
40
40
  "googleapis": "^170.1.0",
41
41
  "inquirer": "^12.1.0",
42
42
  "ora": "^9.0.0",
43
- "@descix/platform-api": "^2.0.0"
43
+ "@descix/platform-api": "^4.0.0"
44
44
  },
45
45
  "devDependencies": {
46
46
  "acorn": "^8.18.0"
@@ -1,3 +1,9 @@
1
+ ## DeSciX lane
2
+ LANE: app ENV: {{apiUrl}} (origin source: {{originSource}}; this repo's own .descix/workspace.json env.apiUrl and wallet — `descix whoami` must agree, never an environment variable)
3
+ SDK: ONLY the published packages, as an external developer, invoked as `npx -y -p @descix/cli@{{cliVersion}} descix …` (an `npx` call without `-p @descix/cli` resolves the wrong package); never platform source
4
+ HANDBOOK: 1 `descix tell-me-how "<question>"` 2 the docs shipped inside the published packages (@descix/cli agent-assets/, @descix/app-sdk README.md) 3 the app-developer runbook, live at https://descix.net/prod/daita-docs/site/app-sdk/app-developer-runbook.html
5
+ PLATFORM WALL: when the handbook does not answer — stop, write the question with evidence (command, output, the CLI's origin banner line) and hand it to Sam to open a platform session. Never open platform source to answer it.
6
+
1
7
  # DeSciX App Development
2
8
 
3
9
  <!-- BOOTSTRAP MODE: This file was auto-generated by DeSciX.
@@ -282,7 +288,7 @@ You are bonded to cloud knowledge bases. **BEFORE starting any task**, query the
282
288
 
283
289
  **Multi-app query (preferred — richer context):**
284
290
  ```bash
285
- descix chat --apps APP1,APP2 -q "YOUR SPECIFIC QUESTION" --level 3 --thinking -1 --tokens
291
+ descix chat --apps APP1,APP2 -q "YOUR SPECIFIC QUESTION" --level 3 --thinking-level low --tokens
286
292
  ```
287
293
 
288
294
  **Single-app query:**
@@ -297,7 +303,7 @@ ask_question_to_app({ app_id: "APP_ID", user_input: "YOUR SPECIFIC QUESTION" })
297
303
 
298
304
  **Query parameters:**
299
305
  - `--level 1-5` — intelligence (1=fast lookup, 3=balanced, 5=deep reasoning)
300
- - `--thinking -1` — dynamic thinking budget; `0`=off, `N`=fixed tokens
306
+ - `--thinking-level minimal|low|medium|high` — how much the model thinks; the server refuses a level the resolved model does not support
301
307
  - `--tokens` — show token usage in response
302
308
  - Omit `--new` to continue the session thread (Gemini remembers context)
303
309
 
@@ -39,6 +39,10 @@ A complete Express microservice with:
39
39
  descix microservice init
40
40
  ```
41
41
 
42
+ ### Front page (`frontpage/`)
43
+
44
+ The shared front-page template (`index.html`: head styles, `<nav class="nav">`, `<header class="hero" id="top">`, `<section class="sec" id="…">` blocks, `<footer>`). It is stamped by `descix app init` as `site/index.html`, ONLY if that file is absent, with `{{APP_ID}}` / `{{COMMUNITY_ID}}` filled by `lib/app-scaffold.js`. It is not a `site init` scaffold type. Each repo owns its own divergence after the stamp; no repo reads this template across repositories.
45
+
42
46
  ## Usage
43
47
 
44
48
  Run scaffold commands from your app directory (auto-detects context):
@@ -0,0 +1,124 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
6
+ <title>{{APP_ID}} — a {{COMMUNITY_ID}} app on DeSciX</title>
7
+ <meta name="description" content="{{APP_ID}}, an app of the {{COMMUNITY_ID}} community on DeSciX. Replace this description with what the app is for.">
8
+ <style>
9
+ :root{
10
+ --bg:#0b0f14; --bg2:#10161e; --panel:#151d27; --line:#233040; --line2:#2f3f52;
11
+ --ink:#e9eef3; --ink2:#b3bfcc; --mute:#7f8ea0;
12
+ --cyan:#5fcbe6; --cyan2:#3aa9c6; --cyan-ink:#06202a;
13
+ --serif:Georgia,"Times New Roman",serif;
14
+ --sans:"Helvetica Neue",Arial,sans-serif;
15
+ --mono:ui-monospace,SFMono-Regular,Menlo,monospace;
16
+ color-scheme:dark;
17
+ }
18
+ *{box-sizing:border-box}
19
+ [hidden]{display:none!important}
20
+ body{margin:0;background:var(--bg);color:var(--ink);font-family:var(--sans);font-size:17px;line-height:1.55;-webkit-font-smoothing:antialiased}
21
+ a{color:var(--cyan);text-decoration:none}
22
+ a:hover{text-decoration:underline}
23
+ a:focus-visible,button:focus-visible{outline:2px solid var(--cyan);outline-offset:2px}
24
+ img{max-width:100%;display:block}
25
+ h1,h2,h3{font-family:var(--serif);font-weight:500;line-height:1.08;margin:0;text-wrap:balance;letter-spacing:-.01em}
26
+ h2{font-size:clamp(30px,4.2vw,46px)}
27
+ h3{font-size:22px}
28
+ p{margin:0}
29
+ .wrap{max-width:1120px;margin:0 auto;padding-inline:20px}
30
+ .eyebrow{font-family:var(--mono);font-size:12px;letter-spacing:.12em;text-transform:uppercase;color:var(--cyan)}
31
+ .sec{padding-block:88px;border-top:1px solid var(--line)}
32
+ .sec-head{display:grid;gap:14px;max-width:760px;margin-bottom:44px}
33
+ .lede{font-size:19px;color:var(--ink2);max-width:62ch}
34
+ .btn{display:inline-flex;align-items:center;gap:10px;padding:14px 22px;border-radius:4px;font-family:var(--sans);font-weight:600;font-size:16px;border:1px solid transparent;cursor:pointer;text-decoration:none!important}
35
+ .btn-primary{background:var(--cyan);color:var(--cyan-ink)}
36
+ .btn-primary:hover{background:#7ad6ec}
37
+ .btn-ghost{background:transparent;color:var(--ink);border-color:var(--line2)}
38
+ .btn-ghost:hover{border-color:var(--cyan);color:var(--cyan)}
39
+ code{font-family:var(--mono);font-size:.92em;color:var(--cyan);background:rgba(95,203,230,.08);padding:2px 6px;border-radius:3px}
40
+
41
+ /* nav */
42
+ .nav{position:sticky;top:env(safe-area-inset-top,0px);z-index:20;background:rgba(11,15,20,.86);backdrop-filter:blur(10px);border-bottom:1px solid var(--line)}
43
+ .nav .wrap{display:flex;align-items:center;justify-content:space-between;gap:20px;height:64px}
44
+ .brand{display:flex;align-items:baseline;gap:10px;color:var(--ink);text-decoration:none!important}
45
+ .brand b{font-family:var(--serif);font-weight:600;font-size:20px;letter-spacing:.01em}
46
+ .brand span{font-family:var(--mono);font-size:11px;letter-spacing:.14em;text-transform:uppercase;color:var(--mute)}
47
+ .navlinks{display:flex;gap:26px;font-size:15px}
48
+ .navlinks a{color:var(--ink2)}
49
+ .navlinks a:hover{color:var(--cyan);text-decoration:none}
50
+ @media (max-width:820px){.navlinks{display:none}}
51
+
52
+ /* hero */
53
+ .hero{position:relative;overflow:hidden;border-bottom:1px solid var(--line)}
54
+ .hero .wrap{position:relative;padding-block:96px 84px;display:grid;gap:26px}
55
+ .hero h1{font-size:clamp(40px,6.4vw,78px);font-weight:300;max-width:12ch}
56
+ .hero h1 em{font-style:italic;font-weight:500;color:var(--cyan)}
57
+ .hero .lede{font-size:20px;max-width:56ch}
58
+ .cta-row{display:flex;flex-wrap:wrap;gap:12px;margin-top:6px}
59
+
60
+ /* skeleton cards */
61
+ .cards{display:grid;grid-template-columns:repeat(3,1fr);gap:18px}
62
+ .card{border:1px solid var(--line);border-radius:6px;background:var(--bg2);padding:24px 22px;display:grid;gap:10px;align-content:start}
63
+ .card p{font-size:15px;color:var(--ink2)}
64
+ @media (max-width:820px){.cards{grid-template-columns:1fr}}
65
+
66
+ /* footer */
67
+ footer{border-top:1px solid var(--line);padding-block:44px 60px;color:var(--mute);font-size:14px}
68
+ footer .wrap{display:grid;gap:22px}
69
+ footer .row{display:flex;flex-wrap:wrap;gap:8px 24px}
70
+ footer .row a{color:var(--ink2)}
71
+ footer .legal{max-width:80ch;font-size:13px}
72
+ </style>
73
+ </head>
74
+ <body>
75
+
76
+ <nav class="nav"><div class="wrap">
77
+ <a class="brand" href="#top"><b>{{APP_ID}}</b><span>{{COMMUNITY_ID}}</span></a>
78
+ <div class="navlinks">
79
+ <a href="#about">About</a><a href="#next">Next steps</a>
80
+ </div>
81
+ </div></nav>
82
+
83
+ <header class="hero" id="top">
84
+ <div class="wrap">
85
+ <p class="eyebrow">A {{COMMUNITY_ID}} app on DeSciX</p>
86
+ <h1>{{APP_ID}}, <em>in one sentence.</em></h1>
87
+ <p class="lede">This is the scaffold front page <code>descix app init</code> stamped for {{APP_ID}}. It is yours: replace this paragraph with what the app does and who it is for.</p>
88
+ <div class="cta-row">
89
+ <a class="btn btn-primary" href="#about">Learn more</a>
90
+ <a class="btn btn-ghost" href="#next">Make this page yours</a>
91
+ </div>
92
+ </div>
93
+ </header>
94
+
95
+ <section class="sec" id="about"><div class="wrap">
96
+ <div class="sec-head">
97
+ <p class="eyebrow">About</p>
98
+ <h2>What {{APP_ID}} is for.</h2>
99
+ </div>
100
+ <p class="lede">Placeholder copy from the scaffold. Say what {{APP_ID}} offers to the {{COMMUNITY_ID}} community, in the app's own words.</p>
101
+ </div></section>
102
+
103
+ <section class="sec" id="next"><div class="wrap">
104
+ <div class="sec-head">
105
+ <p class="eyebrow">Next steps</p>
106
+ <h2>Make this page yours.</h2>
107
+ </div>
108
+ <div class="cards">
109
+ <div class="card"><h3>Edit it</h3><p>This file is <code>site/index.html</code> in your repository. Nothing regenerates it; your edits are kept.</p></div>
110
+ <div class="card"><h3>Upload it</h3><p>Deploy the site set with <code>descix site upload</code>.</p></div>
111
+ <div class="card"><h3>Ask the handbook</h3><p>Run <code>descix tell-me-how</code> for every capability the CLI offers.</p></div>
112
+ </div>
113
+ </div></section>
114
+
115
+ <footer><div class="wrap">
116
+ <div class="brand"><b>{{APP_ID}}</b><span>{{COMMUNITY_ID}}</span></div>
117
+ <div class="row">
118
+ <a href="#about">About</a><a href="#next">Next steps</a>
119
+ </div>
120
+ <p class="legal">{{APP_ID}} is an app of the {{COMMUNITY_ID}} community on DeSciX. Replace this line with your own legal text.</p>
121
+ </div></footer>
122
+
123
+ </body>
124
+ </html>
@@ -1,149 +0,0 @@
1
- /**
2
- * Airdrop Commands — DRY-RUN ONLY.
3
- *
4
- * descix airdrop execute-queue [--community <slug>] [--dry-run] [--batch-size <n>]
5
- *
6
- * The apply leg (an admin PK forwarded through the server to Powch's broadcast) is REMOVED,
7
- * CLI and server both (CEO 2026-10-10, daita-sam M2 envelope constraint 6): migration batches
8
- * execute in daita-sam — act plan + typed confirm + the role holder's own wallet, client-signed.
9
- *
10
- * Access: requires an authenticated session (`requireAuth`); the server additionally requires
11
- * platform admin for the preview.
12
- */
13
-
14
- import chalk from 'chalk';
15
- import { DeSciXApiClient } from '../api-client.js';
16
- import { requireAuth } from '../auth-guard.js';
17
-
18
- /**
19
- * Preview (dry-run) the airdrop migration batch queue on the target environment.
20
- *
21
- * @param {Object} options
22
- * @param {string} [options.community] - Community slug scoping the preview (omitted = aggregate).
23
- * @param {boolean} [options.dryRun] - Accepted for scripts; dry-run is the only mode.
24
- * @param {number} [options.batchSize] - Optional cap on number of users processed this run.
25
- * @param {Object} [options.apiClient] - Optional injected API client (for tests).
26
- */
27
- export async function executeQueue(options) {
28
- // THE APPLY LEG IS STRUCK — refused before auth, before any network touch (CEO 2026-10-10, daita-sam M2 envelope constraint 6; the server
29
- // refuses it too): migration batches execute in daita-sam — act plan + the role holder's own
30
- // wallet signs, client-side. No PK ever transits this CLI again.
31
- if (options.apply || options.signerPkFile) {
32
- throw new Error(
33
- 'airdrop execute-queue is DRY-RUN ONLY: the apply leg (admin PK forwarded through the ' +
34
- 'server) is removed. Execute migration batches in daita-sam (act plan + your own wallet).'
35
- );
36
- }
37
- const isDryRun = true;
38
-
39
- const apiClient = options.apiClient || new DeSciXApiClient();
40
- await requireAuth(apiClient);
41
-
42
- const params = {};
43
- if (options.community) params.community = options.community;
44
- params.mode = 'dry-run';
45
- if (options.batchSize !== undefined && options.batchSize !== null) {
46
- const bs = Number(options.batchSize);
47
- if (!Number.isFinite(bs) || bs <= 0) {
48
- throw new Error(`--batch-size must be a positive integer, got: ${options.batchSize}`);
49
- }
50
- params.batch_size = bs;
51
- }
52
- // Legacy boolean dry_run parameter (preserved for backwards-compat with existing scripts).
53
- if (isDryRun && !options.community) {
54
- params.dry_run = true;
55
- }
56
-
57
- const envLabel = process.env.DESCIX_API_URL || '(workspace default)';
58
- console.log(chalk.cyan(`\nairdrop execute-queue [${chalk.yellow('DRY-RUN')}] target=${envLabel}`));
59
- if (options.community) {
60
- console.log(chalk.gray(` community: ${options.community}`));
61
- }
62
- if (params.batch_size) {
63
- console.log(chalk.gray(` batch_size: ${params.batch_size}`));
64
- }
65
- console.log('');
66
-
67
- let response;
68
- try {
69
- response = await apiClient.invoke('airdrop_execute_queue', params);
70
- } catch (error) {
71
- // Auth/permission errors surface as HTTP-level failures with readable messages.
72
- // CRITICAL: ensure no echo of params.signer_pk_hex into any error message we print.
73
- const safeMsg = String(error.message || '').replace(/0x[a-fA-F0-9]{64}/g, '0x[REDACTED]');
74
- console.error(chalk.red(`\nairdrop_execute_queue failed: ${safeMsg}\n`));
75
- if (/platform-admin|service-account/i.test(error.message)) {
76
- console.error(chalk.gray(' This command requires a platform-admin account.'));
77
- console.error(chalk.gray(' Verify your membership in the DESCIX_ADMIN_GROUP Google Group.\n'));
78
- }
79
- throw error;
80
- }
81
-
82
- const result = response?.message || response;
83
- if (!result || typeof result !== 'object') {
84
- throw new Error('airdrop_execute_queue returned no result');
85
- }
86
-
87
- if (isDryRun && result.mode === 'dry-run') {
88
- // Render the §4.F dry-run summary table.
89
- console.log(chalk.bold('Dry-Run Summary:'));
90
- console.log(` community: ${chalk.cyan(result.community || '(all)')}`);
91
- if (result.token_symbol) console.log(` token_symbol: ${result.token_symbol}`);
92
- if (result.token_contract_address) console.log(` token_contract: ${result.token_contract_address}`);
93
- if (result.abi_source) console.log(` abi_source: ${result.abi_source.doc_id} (v${result.abi_source.version || '?'})`);
94
- console.log(` transfers_total: ${chalk.yellow(result.transfers_total)}`);
95
- console.log(` batches_total: ${result.batches_total}`);
96
- console.log(` debit_total: ${result.debit_total}`);
97
- console.log(` credit_total: ${result.credit_total}`);
98
- const nz = result.net_zero_assertion || {};
99
- const nzLabel = nz.passes ? chalk.green('PASS') : chalk.red('FAIL');
100
- console.log(` net_zero_assertion: ${nzLabel} (Σ=${nz.sum})`);
101
- const pbc = result.pre_batch_checks || {};
102
- console.log(` pre_batch_checks: ${pbc.passes ? chalk.green('PASS') : chalk.red('FAIL — an execute would refuse')}`);
103
- for (const r of (pbc.refusals || [])) {
104
- console.log(chalk.red(` ${r.kind} ${r.wallet} (${r.community_id}) holds ${r.held}, needs ${r.required} base units — rows ${(r.pending_ids || []).join(', ')}`));
105
- }
106
- console.log(` unique_source_wallets: ${result.unique_source_wallets}`);
107
- console.log(` unique_master_wallets: ${result.unique_master_wallets}`);
108
- console.log(` prospective_batch_id: ${result.prospective_batch_id}`);
109
- if (result.gas_estimate) {
110
- console.log(` gas_estimate: ${result.gas_estimate.units || '?'} units @ ${result.gas_estimate.gas_price_gwei || '?'} gwei`);
111
- if (result.gas_estimate.matic_cost_estimate) {
112
- console.log(` estimated_cost: ~${result.gas_estimate.matic_cost_estimate} MATIC`);
113
- }
114
- }
115
- console.log('');
116
- console.log(chalk.gray('No tx submitted. No DB rows mutated.'));
117
- console.log(chalk.gray('To execute migration batches: daita-sam (act plan + your own wallet).'));
118
- console.log('');
119
- return result;
120
- }
121
-
122
- console.log(chalk.bold('Result:'));
123
- console.log(` batches_executed: ${chalk.yellow(result.batches_executed ?? 0)}`);
124
- console.log(` total_users: ${chalk.yellow(result.total_users ?? 0)}`);
125
- console.log(` dry_run: ${result.dry_run ? chalk.yellow('true') : chalk.gray('false')}`);
126
-
127
- const perCommunity = result.per_community || {};
128
- const communityKeys = Object.keys(perCommunity);
129
- if (communityKeys.length) {
130
- console.log(` per_community:`);
131
- for (const cid of communityKeys) {
132
- console.log(` ${cid}: ${perCommunity[cid]}`);
133
- }
134
- } else {
135
- console.log(` per_community: ${chalk.gray('{}')}`);
136
- }
137
-
138
- if (result.caller) {
139
- const who = result.caller.operator_email || result.caller.service_account || '(unknown)';
140
- console.log(chalk.gray(` caller: ${who}`));
141
- }
142
-
143
- console.log('');
144
- console.log(chalk.gray('Dry-run complete. No broadcast attempted. No on_chain_log broadcast row written.'));
145
- console.log('');
146
- return result;
147
- }
148
-
149
- export default { executeQueue };