@descix/cli 1.0.3 → 1.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/agent-assets/reference/mcp-integration.md +12 -6
- package/bin/descix.js +39 -6
- package/lib/api-client.js +9 -0
- package/lib/commands/briefer/sources/entitlements.js +13 -2
- package/lib/commands/config.js +44 -3
- package/lib/commands/doctor.js +88 -21
- package/lib/commands/status.js +28 -4
- package/lib/origin.js +82 -9
- package/package.json +1 -1
- package/templates/scaffolds/microservice/SERVICE_README_sdk.md +76 -151
|
@@ -85,9 +85,11 @@ The broker validates this context and propagates it to downstream services.
|
|
|
85
85
|
|
|
86
86
|
### 3.1 Primary Discovery Mechanism
|
|
87
87
|
|
|
88
|
-
`tell_me_how` is
|
|
88
|
+
**Prerequisite: the developer is signed in.** `tell_me_how` returns caller-specific context (and `kb corpus sync` writes to a KB namespace) — both correctly refuse a credential-free caller, loud: `Authentication required`, exit 1. There is no guest/unauthenticated form of discovery, and that refusal must not be treated as a bug to work around. The step before discovery is `descix login --env dev`: it starts a device-code flow — opens a browser and mints a fresh sign-in code for that one invocation — and completes once the browser confirms it.
|
|
89
89
|
|
|
90
|
-
|
|
90
|
+
Once signed in, `tell_me_how` is the **primary entry point** for discovering platform capabilities. Always use it before attempting platform operations.
|
|
91
|
+
|
|
92
|
+
**Rule:** Sign in, then ask `tell_me_how` first, then execute the recommended command.
|
|
91
93
|
|
|
92
94
|
### 3.2 Three Scopes
|
|
93
95
|
|
|
@@ -100,6 +102,9 @@ The broker validates this context and propagates it to downstream services.
|
|
|
100
102
|
### 3.3 CLI Usage
|
|
101
103
|
|
|
102
104
|
```bash
|
|
105
|
+
# Sign in first — device-code flow, opens a browser (skip if already authenticated)
|
|
106
|
+
descix login --env dev
|
|
107
|
+
|
|
103
108
|
# Default (entitlements scope)
|
|
104
109
|
descix tell-me-how "How do I create a new app?"
|
|
105
110
|
|
|
@@ -341,10 +346,11 @@ Custom services integrate with the same pattern:
|
|
|
341
346
|
|
|
342
347
|
### 7.1 For AI Agents
|
|
343
348
|
|
|
344
|
-
1. **
|
|
345
|
-
2. **
|
|
346
|
-
3. **
|
|
347
|
-
4. **
|
|
349
|
+
1. **Be signed in before discovery** - `descix login --env dev` (device-code flow) is the step before `tell_me_how`; it refuses loud, not silently, for a credential-free caller
|
|
350
|
+
2. **Always use tell_me_how first (once signed in)** - Don't guess at command names
|
|
351
|
+
3. **Use project scope when in a workspace** - More focused results
|
|
352
|
+
4. **Chain operations** - tell_me_how -> execute_remote_command
|
|
353
|
+
5. **Handle errors gracefully** - Show user-friendly messages
|
|
348
354
|
|
|
349
355
|
### 7.2 For Service Developers
|
|
350
356
|
|
package/bin/descix.js
CHANGED
|
@@ -13,6 +13,7 @@ import { DeSciXApiClient } from '../lib/api-client.js';
|
|
|
13
13
|
import { requireAuth } from '../lib/auth-guard.js';
|
|
14
14
|
import { WorkspaceConfig } from '../lib/workspace-config.js';
|
|
15
15
|
import { CLI_VERSION } from '../lib/cli-version.js';
|
|
16
|
+
import { recordInvocationOrigin } from '../lib/origin.js';
|
|
16
17
|
// Chat session pointer + the ONE rule for when a dead pointer may be self-healed.
|
|
17
18
|
import {
|
|
18
19
|
getSessionInteractionId,
|
|
@@ -107,12 +108,24 @@ const ENV_URL_MAP = Object.fromEntries(
|
|
|
107
108
|
Object.entries(WorkspaceConfig.ENV_MAP).map(([k, v]) => [k, v.url])
|
|
108
109
|
);
|
|
109
110
|
|
|
111
|
+
// The fold into DESCIX_API_URL is what makes every consumer see ONE variable — and it is also
|
|
112
|
+
// what used to DESTROY the answer to "who chose this origin", so the origin owner printed a
|
|
113
|
+
// static disjunction ("DESCIX_API_URL (or --api-url / --env)") for all three cases. The fold
|
|
114
|
+
// stays; the provenance is now RECORDED alongside it, in the same breath, so the owner can name
|
|
115
|
+
// the actual source. Recording it here rather than deriving it in the owner keeps one owner of
|
|
116
|
+
// the fact: the only code that knows a flag was passed is the code that read the flag.
|
|
110
117
|
program.hook('preAction', (thisCommand) => {
|
|
111
|
-
|
|
118
|
+
// Read from the ROOT program, not from the action command. `--env` and `--api-url` are
|
|
119
|
+
// declared ONCE, globally, and Commander binds them to the root — an identically-named option
|
|
120
|
+
// redeclared on a subcommand is shadowed and silently never populated (measured on 1.0.4:
|
|
121
|
+
// `descix config init --env dev` reached its action with `{}` and rejected its own advertised
|
|
122
|
+
// flag). One declaration, read from where Commander actually puts it.
|
|
123
|
+
const opts = program.opts();
|
|
112
124
|
|
|
113
125
|
// --api-url takes highest priority
|
|
114
126
|
if (opts.apiUrl) {
|
|
115
127
|
process.env.DESCIX_API_URL = opts.apiUrl;
|
|
128
|
+
recordInvocationOrigin(opts.apiUrl, 'flagApiUrl');
|
|
116
129
|
return;
|
|
117
130
|
}
|
|
118
131
|
|
|
@@ -120,12 +133,23 @@ program.hook('preAction', (thisCommand) => {
|
|
|
120
133
|
if (opts.env) {
|
|
121
134
|
const envName = opts.env.toLowerCase();
|
|
122
135
|
if (!(envName in ENV_URL_MAP)) {
|
|
123
|
-
|
|
136
|
+
// THE REFUSAL CARRIES THE CUSTOM-ENVIRONMENT PATH. This validator runs BEFORE any command
|
|
137
|
+
// action, so it is now the only thing a developer typing `--env staging` ever sees — the
|
|
138
|
+
// richer text inside `config init` that names `config set-env` for exactly this case can
|
|
139
|
+
// no longer be reached through the flag. A refusal that names only the three built-in
|
|
140
|
+
// names would strand every self-hosted or port-forwarded gateway with no next step, which
|
|
141
|
+
// is the same dead end as a remedy string for a command that does not run.
|
|
142
|
+
console.error(chalk.red(
|
|
143
|
+
`Unknown environment: ${opts.env}. Known environments: ${Object.keys(ENV_URL_MAP).join(', ')}.\n\n` +
|
|
144
|
+
'For a self-hosted or port-forwarded gateway, name it with its origin instead:\n' +
|
|
145
|
+
` descix config set-env ${envName} --url https://...`
|
|
146
|
+
));
|
|
124
147
|
process.exit(1);
|
|
125
148
|
}
|
|
126
149
|
// Every named environment resolves to a URL (including dev → cloud DEV).
|
|
127
150
|
// A local backend is a URL you name, not an environment.
|
|
128
151
|
process.env.DESCIX_API_URL = ENV_URL_MAP[envName];
|
|
152
|
+
recordInvocationOrigin(ENV_URL_MAP[envName], 'flagEnv');
|
|
129
153
|
}
|
|
130
154
|
});
|
|
131
155
|
|
|
@@ -2327,7 +2351,7 @@ corpusCommand
|
|
|
2327
2351
|
.option('-k, --kb <name>', 'KB name (syncs specific manifest; default: all)')
|
|
2328
2352
|
.option('-v, --verbose', 'Show verbose output')
|
|
2329
2353
|
.option('--ref <ref>', 'Override the git ref for ALL manifest sources (e.g., --ref ws-admin-b1). Precedence: --ref > manifest source.ref > "main".')
|
|
2330
|
-
.option('--rebuild', 'Reconcile Pinecone against
|
|
2354
|
+
.option('--rebuild', 'Reconcile Pinecone against REMOTE state: enumerate remote file_ids, purge any not in the current corpus, then re-sync from scratch. NOT needed to remove deleted files — an ordinary sync already purges them, diffing the walk against sync-state\'s last_sync_commit (measured: a plain sync purged 1962 chunks across 124 stale blob SHAs). Use --rebuild ONLY when that local sync-state cannot be trusted: it is lost, hand-edited, or vectors were orphaned outside the history it tracks. Prompts before deleting unless --yes is supplied.')
|
|
2331
2355
|
.option('--dry-run', 'Enumerate would-be-purged file_ids and would-be-upserted chunks without ANY Pinecone writes. Exit 0 if no drift, 1 if drift. Read-only.')
|
|
2332
2356
|
.option('--show-walk', 'Print the resolved ref + the first 50 walked files BEFORE any Pinecone operations. Useful for verifying --ref / manifest source resolution.')
|
|
2333
2357
|
.option('--yes', 'Skip the interactive purge confirmation in --rebuild mode. Use in scripting/CI.')
|
|
@@ -4334,15 +4358,24 @@ configCommand
|
|
|
4334
4358
|
|
|
4335
4359
|
configCommand
|
|
4336
4360
|
.command('init')
|
|
4337
|
-
.description('
|
|
4361
|
+
.description('Pin this workspace to an environment (requires the global --env dev|demo|prod)')
|
|
4338
4362
|
// `--dev` DELETED, not deprecated: its absence meant PRODUCTION, so the most common
|
|
4339
4363
|
// invocation (`descix config init`) wrote a prod origin nobody named. There is no default.
|
|
4340
|
-
|
|
4364
|
+
//
|
|
4365
|
+
// `--env` IS NOT REDECLARED HERE, and that is the fix, not an omission. It was declared both
|
|
4366
|
+
// globally (program.option, above) and on this subcommand; Commander 14 binds `--env dev` to
|
|
4367
|
+
// the ROOT program, so this action received `{}` and threw "requires an environment" — the
|
|
4368
|
+
// command REJECTED THE FLAG ITS OWN --help ADVERTISED. Measured on published 1.0.4, and worse
|
|
4369
|
+
// than a missing command: the CLI printed `descix config init --env dev` as the remedy for an
|
|
4370
|
+
// unconfigured origin (origin.js DEFAULT_ORIGIN_SOURCE) and that exact copy-pasted string
|
|
4371
|
+
// exited 1. Two declarations of one flag is the mirror-drift class; one owner is the cure.
|
|
4341
4372
|
.option('--url <url>', 'Explicit origin (for a self-hosted or port-forwarded gateway)')
|
|
4342
4373
|
.option('-g, --global', 'Save to global config (~/.descixrc)')
|
|
4374
|
+
.addHelpText('after', '\nEnvironment comes from the global flag:\n' +
|
|
4375
|
+
' descix config init --env dev|demo|prod\n')
|
|
4343
4376
|
.action(async (options) => {
|
|
4344
4377
|
try {
|
|
4345
|
-
await configCommands.init(
|
|
4378
|
+
await configCommands.init(program.opts().env, options);
|
|
4346
4379
|
} catch (error) {
|
|
4347
4380
|
fail(error);
|
|
4348
4381
|
}
|
package/lib/api-client.js
CHANGED
|
@@ -66,6 +66,15 @@ export class DeSciXApiClient {
|
|
|
66
66
|
this.baseUrl = String(this.baseUrl).trim().replace(/\/+$/, '');
|
|
67
67
|
originSource = EXPLICIT_ORIGIN_SOURCE;
|
|
68
68
|
}
|
|
69
|
+
// KEPT ON THE CLIENT, not just printed and dropped. `status` and `doctor` have to SHOW the
|
|
70
|
+
// resolved origin and its source in their own report bodies; when this was a local variable
|
|
71
|
+
// the only way for them to state an origin was to derive one themselves, and both did —
|
|
72
|
+
// from GlobalConfig.api_url, which is legitimately null. That produced three different
|
|
73
|
+
// answers for one state seconds apart (status "production (null)", doctor "API URL: null",
|
|
74
|
+
// app list the real origin). A consumer can only ferry the owner's answer if the owner's
|
|
75
|
+
// answer is still reachable.
|
|
76
|
+
this.originSource = originSource;
|
|
77
|
+
|
|
69
78
|
// I1 (A', rev 2): ALWAYS, not default-only. Silence was the defect; PROD was not.
|
|
70
79
|
reportEnvironment({ origin: this.baseUrl, source: originSource });
|
|
71
80
|
|
|
@@ -85,12 +85,23 @@ export async function extract({ env, cliPaths } = {}) {
|
|
|
85
85
|
relPath: CM_FILE,
|
|
86
86
|
section: `§${SECTION.number} ${SECTION.heading}`
|
|
87
87
|
});
|
|
88
|
+
// The symbol is matched WITHIN an export list rather than as the list's sole member. The old
|
|
89
|
+
// pattern anchored on the closing brace immediately after the name, so it broke the moment a
|
|
90
|
+
// second symbol was re-exported beside it — the extractor reported the re-export missing when
|
|
91
|
+
// it was present and one token wider.
|
|
92
|
+
//
|
|
93
|
+
// HONEST LIMITATION, stated rather than implied: this is still textual matching, so it remains a
|
|
94
|
+
// derivation of a fact the module itself owns. The robust fix is to consume the owner's actual
|
|
95
|
+
// exports, but this extractor cannot import communityManagement.js — that would pull the whole
|
|
96
|
+
// Cloud service graph (config bootstrap, Firestore) into the CLI. Matching the identifier inside
|
|
97
|
+
// any export list is what survives a re-ordering; a move to `export * from` would still need
|
|
98
|
+
// this line changed.
|
|
88
99
|
const reExportMatch = findInLines({
|
|
89
100
|
lines: cm.lines,
|
|
90
|
-
regex: /export\s*\{\
|
|
101
|
+
regex: /export\s*\{[^}]*\bhydrateCommunityIdFromProducts\b/,
|
|
91
102
|
section: `§${SECTION.number} ${SECTION.heading}`,
|
|
92
103
|
source: `${CM_FILE} (hydrate re-export)`,
|
|
93
|
-
expected: '
|
|
104
|
+
expected: 'hydrateCommunityIdFromProducts named in an export list',
|
|
94
105
|
recovery: `Verify ${CM_FILE} still re-exports hydrateCommunityIdFromProducts.`
|
|
95
106
|
});
|
|
96
107
|
|
package/lib/commands/config.js
CHANGED
|
@@ -101,13 +101,29 @@ export async function init(env, options = {}) {
|
|
|
101
101
|
// Write through the CANONICAL writer. This function used to assign the LEGACY top-level
|
|
102
102
|
// `workspaceConfig.apiUrl`, while `config set-env` writes `env.apiUrl` — two writers of one
|
|
103
103
|
// fact, writing two different keys, one of which `getApiUrl()` only consults as a fallback.
|
|
104
|
-
|
|
104
|
+
//
|
|
105
|
+
// `tryLoad`, NOT `load`, AND IT CREATES ONE WHEN THERE IS NONE. This is what closes the
|
|
106
|
+
// PROD WINDOW at the start of every onboarding (measured on published 1.0.4): in a fresh
|
|
107
|
+
// directory `config init --env dev` threw "Workspace not configured", `config set-env dev`
|
|
108
|
+
// threw the same, and so there was NO WORKING WAY TO PIN THE ENVIRONMENT BEFORE the first
|
|
109
|
+
// command ran — every one of them silently targeted PROD while printing a remedy that could
|
|
110
|
+
// not be executed. Pinning the environment must not require a workspace to already exist:
|
|
111
|
+
// choosing the origin is the FIRST thing a developer does, not something they may only do
|
|
112
|
+
// after they have already talked to the wrong one.
|
|
113
|
+
const existing = await WorkspaceConfig.tryLoad();
|
|
114
|
+
const workspaceConfig = existing
|
|
115
|
+
|| new WorkspaceConfig({ version: '2.1', type: 'workspace', env: {} }, process.cwd());
|
|
116
|
+
const created = !existing;
|
|
105
117
|
const result = await workspaceConfig.setEnvironment(normalized, options.url || null);
|
|
106
118
|
|
|
107
119
|
console.log(chalk.green('\n✅ Configuration initialized!\n'));
|
|
108
120
|
console.log(chalk.white(` Environment: ${result.environment}`));
|
|
109
121
|
console.log(chalk.white(` API URL: ${result.apiUrl}`));
|
|
110
|
-
console.log(chalk.gray(` Saved to: ${result.configPath}
|
|
122
|
+
console.log(chalk.gray(` Saved to: ${result.configPath}`));
|
|
123
|
+
if (created) {
|
|
124
|
+
console.log(chalk.gray(' (new workspace created here — this directory is now the workspace root)'));
|
|
125
|
+
}
|
|
126
|
+
console.log('');
|
|
111
127
|
|
|
112
128
|
} catch (error) {
|
|
113
129
|
console.error(chalk.red('Error initializing config:', error.message));
|
|
@@ -124,7 +140,32 @@ export async function init(env, options = {}) {
|
|
|
124
140
|
*/
|
|
125
141
|
export async function setEnv(envName, options = {}) {
|
|
126
142
|
try {
|
|
127
|
-
|
|
143
|
+
// ONE SURFACE PER CAPABILITY. `config init` and `config set-env` both write through the same
|
|
144
|
+
// canonical writer (setEnvironment) — that shared owner is correct — but they must not both
|
|
145
|
+
// accept the SAME input, or there are two commands for one act and the CLI's own remedy
|
|
146
|
+
// string can name the loser. `config init` is canonical for the three KNOWN environments:
|
|
147
|
+
// the origin owner prints `descix config init --env dev` as the remedy, so that command must
|
|
148
|
+
// be the one that does it. `set-env` keeps the capability `init` genuinely does not have —
|
|
149
|
+
// a CUSTOM environment name with an explicit --url — and REFUSES the known names outright,
|
|
150
|
+
// naming its replacement. A refusal, not a warning: a warning would leave two working paths.
|
|
151
|
+
const knownName = WorkspaceConfig.ENV_MAP[String(envName).toLowerCase()];
|
|
152
|
+
if (knownName) {
|
|
153
|
+
throw new Error(
|
|
154
|
+
`"${envName}" is a known environment — use the canonical command:\n` +
|
|
155
|
+
` descix config init --env ${String(envName).toLowerCase()}\n\n` +
|
|
156
|
+
'descix config set-env is for a CUSTOM environment name with an explicit origin, e.g.\n' +
|
|
157
|
+
' descix config set-env staging --url https://staging.example.com'
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Same tryLoad-or-create as `config init`, for the same reason: `config init`'s own error
|
|
162
|
+
// text sends a developer HERE for the self-hosted / port-forwarded case, so this command has
|
|
163
|
+
// to work where that developer actually is. If it still required a pre-existing workspace,
|
|
164
|
+
// init's remedy would point at a command that refuses — the identical closed loop, moved one
|
|
165
|
+
// hop along. A remedy string is only true if the command it names runs.
|
|
166
|
+
const existing = await WorkspaceConfig.tryLoad();
|
|
167
|
+
const workspaceConfig = existing
|
|
168
|
+
|| new WorkspaceConfig({ version: '2.1', type: 'workspace', env: {} }, process.cwd());
|
|
128
169
|
|
|
129
170
|
const result = await workspaceConfig.setEnvironment(envName, options.url || null);
|
|
130
171
|
|
package/lib/commands/doctor.js
CHANGED
|
@@ -13,7 +13,6 @@ import fs from 'fs/promises';
|
|
|
13
13
|
import { exec } from 'child_process';
|
|
14
14
|
import { promisify } from 'util';
|
|
15
15
|
import { DeSciXApiClient } from '../api-client.js';
|
|
16
|
-
import { GlobalConfig } from '../global-config.js';
|
|
17
16
|
import { isAuthenticated } from '../auth-guard.js';
|
|
18
17
|
|
|
19
18
|
const execAsync = promisify(exec);
|
|
@@ -62,6 +61,28 @@ async function checkNode() {
|
|
|
62
61
|
* Check API Connectivity
|
|
63
62
|
*/
|
|
64
63
|
async function checkConnectivity(apiClient) {
|
|
64
|
+
// NO NETWORK VERDICT ON AN ORIGIN WE DO NOT HAVE. Measured on published 1.0.4: doctor was
|
|
65
|
+
// handed `baseUrl: globalConfig.api_url`, which is legitimately null on a machine that has
|
|
66
|
+
// never written ~/.descix/config.json, and then called `fetch(null)`. That throws, so doctor
|
|
67
|
+
// reported `[✗] API Connectivity — Unreachable / Fix: Check your internet connection.
|
|
68
|
+
// API URL: null` — UNCONFIGURED STATE MISREPORTED AS A NETWORK FAILURE, sending the developer
|
|
69
|
+
// to debug their wifi over a config fact. A red verdict on a null origin is not a measurement
|
|
70
|
+
// of anything; neither would a green one be.
|
|
71
|
+
//
|
|
72
|
+
// This is now unreachable through runDoctor, which resolves via the origin owner (never null,
|
|
73
|
+
// or a loud OriginInvalidError). It stays as a hard guard because "the caller always passes a
|
|
74
|
+
// good origin" is exactly the assumption that produced the defect.
|
|
75
|
+
if (!apiClient.baseUrl) {
|
|
76
|
+
return {
|
|
77
|
+
ok: false,
|
|
78
|
+
message: 'NOT CHECKED — no origin resolved',
|
|
79
|
+
remediation:
|
|
80
|
+
'This is a configuration fact, not a network problem, and no connectivity result is\n' +
|
|
81
|
+
' reported for it. Choose an origin:\n' +
|
|
82
|
+
' descix config init --env dev|demo|prod',
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
65
86
|
try {
|
|
66
87
|
// Just check if we can reach the health endpoint or root
|
|
67
88
|
// The API client doesn't expose a raw ping, but we can try a lightweight call
|
|
@@ -148,10 +169,11 @@ export async function checkGcloud() {
|
|
|
148
169
|
const version = stdout.split('\n')[0].trim(); // e.g. "Google Cloud SDK 456.0.0"
|
|
149
170
|
return { ok: true, message: version };
|
|
150
171
|
} catch (error) {
|
|
151
|
-
return {
|
|
152
|
-
ok: false,
|
|
153
|
-
|
|
154
|
-
|
|
172
|
+
return {
|
|
173
|
+
ok: false,
|
|
174
|
+
advisory: true,
|
|
175
|
+
message: 'Not installed or not in PATH',
|
|
176
|
+
remediation: `Only needed to administer DeSciX infrastructure. If you are building on DeSciX, ignore this.\n Install (admins): https://cloud.google.com/sdk/docs/install`
|
|
155
177
|
};
|
|
156
178
|
}
|
|
157
179
|
}
|
|
@@ -166,10 +188,11 @@ export async function checkAdc() {
|
|
|
166
188
|
await fs.access(process.env.GOOGLE_APPLICATION_CREDENTIALS);
|
|
167
189
|
return { ok: true, message: 'Configured via Env Var' };
|
|
168
190
|
} catch {
|
|
169
|
-
return {
|
|
170
|
-
ok: false,
|
|
171
|
-
|
|
172
|
-
|
|
191
|
+
return {
|
|
192
|
+
ok: false,
|
|
193
|
+
advisory: true,
|
|
194
|
+
message: 'Env Var set but file missing',
|
|
195
|
+
remediation: `Check GOOGLE_APPLICATION_CREDENTIALS path: ${process.env.GOOGLE_APPLICATION_CREDENTIALS}`
|
|
173
196
|
};
|
|
174
197
|
}
|
|
175
198
|
}
|
|
@@ -191,10 +214,11 @@ export async function checkAdc() {
|
|
|
191
214
|
// For now, existence is a good enough proxy for "attempted setup"
|
|
192
215
|
return { ok: true, message: 'Configured (Default Location)' };
|
|
193
216
|
} catch {
|
|
194
|
-
return {
|
|
195
|
-
ok: false,
|
|
196
|
-
|
|
197
|
-
|
|
217
|
+
return {
|
|
218
|
+
ok: false,
|
|
219
|
+
advisory: true,
|
|
220
|
+
message: 'Missing Credentials',
|
|
221
|
+
remediation: `Only needed for Google Drive-backed KB authoring and infrastructure admin.\n If you are building on DeSciX, ignore this.\n Admins run: gcloud auth application-default login --scopes=https://www.googleapis.com/auth/drive.file,https://www.googleapis.com/auth/drive`
|
|
198
222
|
};
|
|
199
223
|
}
|
|
200
224
|
}
|
|
@@ -209,9 +233,26 @@ export async function runDoctor(options = {}) {
|
|
|
209
233
|
const results = [];
|
|
210
234
|
|
|
211
235
|
try {
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
236
|
+
// THE ORIGIN COMES FROM THE ONE OWNER. This was `new DeSciXApiClient({ baseUrl:
|
|
237
|
+
// globalConfig.api_url })` — a THIRD independent derivation of the origin, beside status's
|
|
238
|
+
// and beside the real one. ~/.descix/config.json names no origin on a fresh machine, so
|
|
239
|
+
// doctor ran its whole diagnosis against `null`. Passing NO baseUrl routes through
|
|
240
|
+
// initialize() → resolveOrigin(), which never returns null, fails loud on an origin that was
|
|
241
|
+
// named but unusable, and prints the `env:` line doctor was missing.
|
|
242
|
+
const apiClient = new DeSciXApiClient();
|
|
243
|
+
try {
|
|
244
|
+
await apiClient.ensureInitialized();
|
|
245
|
+
} catch (error) {
|
|
246
|
+
// A CONFIGURED-BUT-INVALID origin is not a diagnosis to render in a results table — it is
|
|
247
|
+
// the answer, and it already carries its own remedy from the origin owner. Fail loud
|
|
248
|
+
// rather than continuing into checks whose results would all be meaningless.
|
|
249
|
+
spinner.stop();
|
|
250
|
+
console.error(chalk.red(`\n${error.message}\n`));
|
|
251
|
+
process.exitCode = 1;
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
|
|
215
256
|
// 1. Node Version
|
|
216
257
|
spinner.text = 'Checking Node.js...';
|
|
217
258
|
const nodeRes = await checkNode();
|
|
@@ -247,9 +288,19 @@ export async function runDoctor(options = {}) {
|
|
|
247
288
|
// Print Results
|
|
248
289
|
let hasFailures = false;
|
|
249
290
|
|
|
291
|
+
// AN ADVISORY IS NOT A FAILURE. gcloud and ADC are administrator dependencies: an external
|
|
292
|
+
// developer building ON DeSciX has no business holding Google ADC credentials, yet doctor
|
|
293
|
+
// showed them a red [✗] and exited non-zero for not having them — telling every non-admin
|
|
294
|
+
// caller their environment is broken when it is correct and complete. A check that cries
|
|
295
|
+
// wolf on correct behaviour trains people to ignore the checks that matter.
|
|
250
296
|
results.forEach(res => {
|
|
251
297
|
if (res.ok) {
|
|
252
298
|
console.log(`${chalk.green('[✓]')} ${chalk.white(res.label)} ${chalk.gray(res.message ? `(${res.message})` : '')}`);
|
|
299
|
+
} else if (res.advisory) {
|
|
300
|
+
console.log(`${chalk.gray('[-]')} ${chalk.white(res.label)} ${chalk.gray(`(${res.message} — not required)`)}`);
|
|
301
|
+
if (res.remediation) {
|
|
302
|
+
console.log(` ${chalk.gray('Note:')} ${chalk.gray(res.remediation)}`);
|
|
303
|
+
}
|
|
253
304
|
} else {
|
|
254
305
|
hasFailures = true;
|
|
255
306
|
console.log(`${chalk.red('[✗]')} ${chalk.white(res.label)}`);
|
|
@@ -259,19 +310,35 @@ export async function runDoctor(options = {}) {
|
|
|
259
310
|
}
|
|
260
311
|
}
|
|
261
312
|
});
|
|
262
|
-
|
|
313
|
+
|
|
263
314
|
console.log();
|
|
264
|
-
|
|
315
|
+
|
|
316
|
+
// COVERAGE BOUNDARY, printed on green as well as red: the reader of a pass must see where
|
|
317
|
+
// the pass stops.
|
|
318
|
+
console.log(chalk.gray(
|
|
319
|
+
'Checked: Node version, API origin resolution + reachability, session, local write\n' +
|
|
320
|
+
'permissions. [-] rows are admin-only dependencies and never fail this command. NOT\n' +
|
|
321
|
+
'checked: your app\'s own build, chain state, or any credential\'s permissions at the API.'
|
|
322
|
+
));
|
|
323
|
+
console.log();
|
|
324
|
+
|
|
265
325
|
if (hasFailures) {
|
|
266
326
|
console.log(chalk.yellow('⚠️ Some checks failed. Please review the issues above.'));
|
|
267
|
-
process.exit(1)
|
|
327
|
+
// `process.exitCode` rather than `process.exit(1)`: it lets Node drain stdout before
|
|
328
|
+
// exiting, where an immediate process.exit can truncate buffered output on a pipe.
|
|
329
|
+
// NO DEFECT WAS MEASURED HERE — this is a hardening, and the distinction matters. A
|
|
330
|
+
// suspected "exit code says 0 while the text says failed" was investigated on published
|
|
331
|
+
// 1.0.4 and DID NOT REPRODUCE: the 0 came from reading `$?` after a shell PIPELINE, which
|
|
332
|
+
// reports the exit of the last stage (`head`), not of `descix`. Recorded so the next
|
|
333
|
+
// reader does not re-open it: 1.0.4 exits 1 correctly.
|
|
334
|
+
process.exitCode = 1;
|
|
268
335
|
} else {
|
|
269
336
|
console.log(chalk.green('✨ All checks passed! Your environment is ready.'));
|
|
270
337
|
}
|
|
271
|
-
|
|
338
|
+
|
|
272
339
|
} catch (error) {
|
|
273
340
|
spinner.fail('Doctor crashed');
|
|
274
341
|
console.error(chalk.red(`\nUnexpected error: ${error.message}`));
|
|
275
|
-
process.
|
|
342
|
+
process.exitCode = 1;
|
|
276
343
|
}
|
|
277
344
|
}
|
package/lib/commands/status.js
CHANGED
|
@@ -19,6 +19,7 @@ import { promisify } from 'util';
|
|
|
19
19
|
import { DeSciXApiClient } from '../api-client.js';
|
|
20
20
|
import { GlobalConfig } from '../global-config.js';
|
|
21
21
|
import { isAuthenticated } from '../auth-guard.js';
|
|
22
|
+
import { environmentNameFor } from '../environment-report.js';
|
|
22
23
|
|
|
23
24
|
const execAsync = promisify(exec);
|
|
24
25
|
|
|
@@ -89,7 +90,24 @@ export async function runStatus(options = {}) {
|
|
|
89
90
|
console.log(`OS: ${os.type()} ${os.release()}`);
|
|
90
91
|
|
|
91
92
|
const globalConfig = await GlobalConfig.load();
|
|
92
|
-
|
|
93
|
+
|
|
94
|
+
// THE ORIGIN COMES FROM THE ONE OWNER, VIA ITS CONSUMER. This line used to read
|
|
95
|
+
// `${globalConfig.environment} (${globalConfig.api_url})` — a SECOND, independent derivation
|
|
96
|
+
// of the origin that consulted only ~/.descix/config.json. That file legitimately names no
|
|
97
|
+
// origin, so on an unconfigured workspace status printed "production (null)": an environment
|
|
98
|
+
// NAME it had not resolved, beside a null origin, while `descix app list` seconds later
|
|
99
|
+
// correctly reported prod at https://descix.net from the actual owner. `environment` here is
|
|
100
|
+
// GlobalConfig's own unrelated default string, not a resolution — it was never an answer to
|
|
101
|
+
// "which origin will this CLI talk to".
|
|
102
|
+
//
|
|
103
|
+
// Constructing the client with NO baseUrl is what routes through the owner: initialize()
|
|
104
|
+
// resolves via resolveOrigin() and prints the `env:` line (I1 — every network-bound command,
|
|
105
|
+
// always). status and doctor were the two network-bound commands missing that line, which is
|
|
106
|
+
// exactly why the three-way disagreement stayed invisible.
|
|
107
|
+
const apiClient = new DeSciXApiClient();
|
|
108
|
+
await apiClient.ensureInitialized();
|
|
109
|
+
console.log(`Environment: ${environmentNameFor(apiClient.baseUrl)} (${apiClient.originSource})`);
|
|
110
|
+
console.log(`API Origin: ${chalk.green(apiClient.baseUrl)}`);
|
|
93
111
|
console.log();
|
|
94
112
|
|
|
95
113
|
// Load workspace context via WorkspaceConfig
|
|
@@ -110,7 +128,6 @@ export async function runStatus(options = {}) {
|
|
|
110
128
|
console.log(chalk.white('Authentication'));
|
|
111
129
|
console.log(chalk.white('--------------'));
|
|
112
130
|
|
|
113
|
-
const apiClient = new DeSciXApiClient({ baseUrl: globalConfig.api_url });
|
|
114
131
|
const isAuth = await isAuthenticated(apiClient);
|
|
115
132
|
|
|
116
133
|
if (isAuth) {
|
|
@@ -150,8 +167,15 @@ export async function runStatus(options = {}) {
|
|
|
150
167
|
console.log(`Local Apps: ${allApps.join(', ')}`);
|
|
151
168
|
}
|
|
152
169
|
|
|
153
|
-
|
|
154
|
-
|
|
170
|
+
// WHAT THE WORKSPACE FILE ITSELF SAYS — deliberately NOT a resolved origin. The resolved
|
|
171
|
+
// origin and its source are reported once, above, by the owner's consumer. This line used
|
|
172
|
+
// to call getApiUrl() with a `|| '(not configured …)'` fallback; getApiUrl() now consumes
|
|
173
|
+
// the origin owner and so NEVER returns null, making that branch unreachable — a dead
|
|
174
|
+
// fallback that could only ever misreport. Printing the raw key answers the different and
|
|
175
|
+
// genuinely useful question "is this origin coming from my workspace, or from elsewhere?"
|
|
176
|
+
const workspaceApiUrl = workspaceConfig.env?.apiUrl;
|
|
177
|
+
console.log(`API (this file): ${workspaceApiUrl
|
|
178
|
+
|| '(unset — origin resolves from the sources printed above)'}`);
|
|
155
179
|
console.log(`Local Root: ${workspaceRoot}`);
|
|
156
180
|
} catch (error) {
|
|
157
181
|
console.log(`Mode: ${chalk.red('Unknown/Invalid')} (${error.message})`);
|
package/lib/origin.js
CHANGED
|
@@ -60,15 +60,87 @@ export class OriginInvalidError extends Error {
|
|
|
60
60
|
}
|
|
61
61
|
|
|
62
62
|
/**
|
|
63
|
-
* The
|
|
64
|
-
*
|
|
63
|
+
* The SOURCE label for each thing that can name an origin. One key, one spelling.
|
|
64
|
+
*
|
|
65
|
+
* WHY EACH LABEL NAMES EXACTLY ONE THING (measured 2026-09-09 on published 1.0.4): the envVar
|
|
66
|
+
* label used to read `DESCIX_API_URL (or --api-url / --env)` — a STATIC DISJUNCTION printed
|
|
67
|
+
* whichever of the three had actually chosen the origin, and printed verbatim even with
|
|
68
|
+
* DESCIX_API_URL provably unset. `--env dev`, `--api-url https://dev.descix.net` and a real
|
|
69
|
+
* `DESCIX_API_URL=...` produced three byte-identical source strings. A source that lists the
|
|
70
|
+
* possibilities is not a source: it is the same "who chose this origin" silence this module
|
|
71
|
+
* exists to end, one level in from the null it already fixed.
|
|
72
|
+
*
|
|
73
|
+
* The flag labels are reachable because `bin/descix.js` RECORDS which flag set the variable
|
|
74
|
+
* (see `recordInvocationOrigin`) instead of only folding it into `process.env` and discarding
|
|
75
|
+
* the provenance.
|
|
76
|
+
*/
|
|
77
|
+
export const ORIGIN_SOURCE_LABELS = {
|
|
78
|
+
flagApiUrl: '--api-url flag',
|
|
79
|
+
flagEnv: '--env flag',
|
|
80
|
+
envVar: 'DESCIX_API_URL environment variable',
|
|
81
|
+
workspaceEnvApiUrl: '.descix/workspace.json env.apiUrl',
|
|
82
|
+
legacyApiUrl: '.descix/workspace.json apiUrl (legacy)',
|
|
83
|
+
globalApiUrl: '~/.descix/config.json api_url',
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* The resolution order, most explicit first. The ORDER is the contract; this array is the only
|
|
88
|
+
* place it is written down. Labels come from ORIGIN_SOURCE_LABELS, never from a second literal.
|
|
89
|
+
*/
|
|
90
|
+
const PRECEDENCE = ['envVar', 'workspaceEnvApiUrl', 'legacyApiUrl', 'globalApiUrl'];
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* What this invocation's FLAGS named, if anything: `{ origin, sourceKey }`.
|
|
94
|
+
*
|
|
95
|
+
* `--api-url` and `--env` are folded into `process.env.DESCIX_API_URL` by the CLI's preAction
|
|
96
|
+
* hook so that every consumer sees one variable. That fold is what destroys the provenance, so
|
|
97
|
+
* the hook records it here in the same breath. Module-level because there is exactly ONE
|
|
98
|
+
* invocation per process, and because this module is imported by every consumer of the origin —
|
|
99
|
+
* one owner of the fact, not a copy ferried through four call signatures.
|
|
100
|
+
*/
|
|
101
|
+
let invocationOrigin = null;
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Record that a COMMAND-LINE FLAG chose this invocation's origin.
|
|
105
|
+
*
|
|
106
|
+
* @param {string} origin - the origin the flag resolved to
|
|
107
|
+
* @param {'flagApiUrl'|'flagEnv'} sourceKey - which flag it was
|
|
108
|
+
* @throws when handed a key that has no published label — a source with no name is the defect.
|
|
65
109
|
*/
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
110
|
+
export function recordInvocationOrigin(origin, sourceKey) {
|
|
111
|
+
if (!Object.prototype.hasOwnProperty.call(ORIGIN_SOURCE_LABELS, sourceKey)) {
|
|
112
|
+
throw new Error(
|
|
113
|
+
`recordInvocationOrigin: "${sourceKey}" is not a known origin source. ` +
|
|
114
|
+
`Known: ${Object.keys(ORIGIN_SOURCE_LABELS).join(', ')}.`,
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
invocationOrigin = { origin: normalizeOrigin(origin), sourceKey };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** TEST SEAM ONLY: forget this invocation's recorded flag provenance. */
|
|
121
|
+
export function _resetInvocationOriginForTests() {
|
|
122
|
+
invocationOrigin = null;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* The label for the value that won, naming WHICH source supplied it.
|
|
127
|
+
*
|
|
128
|
+
* The envVar slot is the only one with more than one possible author, because the flags are
|
|
129
|
+
* folded into it. It reports a flag ONLY when the recorded flag actually accounts for the value
|
|
130
|
+
* in hand — if a flag was recorded and DESCIX_API_URL nevertheless holds something else, the
|
|
131
|
+
* honest answer is the environment variable.
|
|
132
|
+
*
|
|
133
|
+
* @param {string} key
|
|
134
|
+
* @param {string} value
|
|
135
|
+
* @returns {string}
|
|
136
|
+
*/
|
|
137
|
+
function sourceLabelFor(key, value) {
|
|
138
|
+
if (key === 'envVar' && invocationOrigin
|
|
139
|
+
&& invocationOrigin.origin === normalizeOrigin(value)) {
|
|
140
|
+
return ORIGIN_SOURCE_LABELS[invocationOrigin.sourceKey];
|
|
141
|
+
}
|
|
142
|
+
return ORIGIN_SOURCE_LABELS[key];
|
|
143
|
+
}
|
|
72
144
|
|
|
73
145
|
/** The remedy text every failure prints. One spelling, so every surface says the same thing. */
|
|
74
146
|
export const ORIGIN_REMEDY =
|
|
@@ -120,9 +192,10 @@ function isUsableOrigin(value) {
|
|
|
120
192
|
* same class of defect from the other direction.
|
|
121
193
|
*/
|
|
122
194
|
export function resolveOrigin(sources = {}) {
|
|
123
|
-
for (const
|
|
195
|
+
for (const key of PRECEDENCE) {
|
|
124
196
|
const value = sources[key];
|
|
125
197
|
if (typeof value === 'string' && value.trim() !== '') {
|
|
198
|
+
const label = sourceLabelFor(key, value);
|
|
126
199
|
if (!isUsableOrigin(value)) {
|
|
127
200
|
throw new OriginInvalidError(
|
|
128
201
|
`The DeSciX API origin configured in ${label} is not a usable origin: "${String(value).trim()}".\n` +
|
package/package.json
CHANGED
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
## Overview
|
|
4
4
|
|
|
5
|
-
The DeSciX SDK provides command-line tools and MCP (Model Context Protocol) integration for building applications on the DeSciX platform. The SDK enables developers to authenticate, manage workspaces, create apps, and leverage AI-assisted development through
|
|
5
|
+
The DeSciX SDK provides command-line tools and MCP (Model Context Protocol) integration for building applications on the DeSciX platform. The SDK enables developers to authenticate, manage workspaces, create apps, and leverage AI-assisted development through editor MCP integration (Claude Code, Copilot, Cursor, Cline).
|
|
6
6
|
|
|
7
7
|
**Key capabilities:**
|
|
8
8
|
- CLI commands for all platform operations
|
|
9
|
-
- MCP server for Cursor
|
|
9
|
+
- MCP server integration for Claude Code, Copilot, Cursor and Cline
|
|
10
10
|
- Intelligent tool discovery via `tell_me_how`
|
|
11
11
|
- Entitlements-driven workspace setup
|
|
12
12
|
- Git-aware project initialization
|
|
@@ -28,23 +28,22 @@ npm link
|
|
|
28
28
|
|
|
29
29
|
## CLI Commands
|
|
30
30
|
|
|
31
|
-
### descix
|
|
31
|
+
### descix quickstart
|
|
32
32
|
|
|
33
|
-
**Description:** One-
|
|
34
|
-
**Use when:** First time setting up DeSciX in a new workspace
|
|
33
|
+
**Description:** One-command setup: auth → workspace → agent files → MCP config
|
|
34
|
+
**Use when:** First time setting up DeSciX in a new workspace
|
|
35
35
|
**What it does:**
|
|
36
|
-
1.
|
|
37
|
-
2.
|
|
38
|
-
3.
|
|
39
|
-
4.
|
|
40
|
-
5.
|
|
36
|
+
1. Authenticates via device login if no valid session exists
|
|
37
|
+
2. Initializes `.descix/workspace.json` if missing
|
|
38
|
+
3. Generates agent instruction files (`CLAUDE.md`, `.github/copilot-instructions.md`, `.cursorrules`, `.clinerules`) from the workspace's registered app/community
|
|
39
|
+
4. Generates `.vscode/mcp.json` for MCP server config (skipped if the DeSciX VS Code extension already handles MCP)
|
|
40
|
+
5. Copies SDK assets into `.descix/sdk-assets/`
|
|
41
41
|
|
|
42
42
|
**Example:**
|
|
43
43
|
```bash
|
|
44
44
|
cd ~/Projects/MyApp
|
|
45
|
-
descix
|
|
45
|
+
descix quickstart
|
|
46
46
|
# Follow the prompts
|
|
47
|
-
# Restart Cursor after completion
|
|
48
47
|
```
|
|
49
48
|
|
|
50
49
|
---
|
|
@@ -54,8 +53,11 @@ descix setup
|
|
|
54
53
|
**Description:** Authenticate with the DeSciX platform
|
|
55
54
|
**Use when:** Session expired or first-time authentication
|
|
56
55
|
**Options:**
|
|
57
|
-
-
|
|
58
|
-
- `--wallet`: Direct wallet signature authentication
|
|
56
|
+
- (default, no flag needed): Device login — opens a browser for Powch authentication
|
|
57
|
+
- `--wallet`: Direct wallet signature authentication (advanced, not yet implemented)
|
|
58
|
+
- `-u, --url <url>`: API URL override
|
|
59
|
+
- `--no-oauth`: Skip the OAuth long-lived token leg (wallet-signature login only)
|
|
60
|
+
- `--scope <scope>`: OAuth scope to request (default: `mcp:read mcp:tools mcp:write mcp:admin`)
|
|
59
61
|
|
|
60
62
|
**Example:**
|
|
61
63
|
```bash
|
|
@@ -107,41 +109,44 @@ descix tell-me-how --scope discovery "What AI training tools exist?"
|
|
|
107
109
|
|
|
108
110
|
---
|
|
109
111
|
|
|
110
|
-
### descix app
|
|
112
|
+
### descix app init
|
|
111
113
|
|
|
112
|
-
**Description:** Create
|
|
113
|
-
**Use when:** Starting a new project or hydrating an existing app
|
|
114
|
+
**Description:** Create (if needed) and initialize an app: platform record + default KB + local workspace registration and scaffold. Idempotent.
|
|
115
|
+
**Use when:** Starting a new project or hydrating an existing app. This is the single canonical path — the old two-step `app create` then `app init` is deleted; `descix app create` now hard-fails naming this replacement.
|
|
114
116
|
**Options:**
|
|
115
|
-
- `-
|
|
116
|
-
- `-
|
|
117
|
-
-
|
|
118
|
-
- `--
|
|
119
|
-
|
|
120
|
-
|
|
117
|
+
- `-a, --app <app_id>` (required): App ID. With `-c` this is the app NAME to create.
|
|
118
|
+
- `-c, --community <id>`: Community ID — required to create an app that does not exist yet
|
|
119
|
+
- `-s, --short <short_name>`: Short id segment (no hyphens) used when creating; defaults to `--app`
|
|
120
|
+
- `--overwrite`: When creating, overwrite an existing app record intentionally
|
|
121
|
+
- `--kb <name>`: Knowledge base name (default: `General`)
|
|
122
|
+
- `-p, --path <dir>`: Local app directory (default: auto-detected or cwd)
|
|
121
123
|
|
|
122
124
|
**Example:**
|
|
123
125
|
```bash
|
|
124
|
-
|
|
125
|
-
descix app create --wizard
|
|
126
|
+
descix app init -a myapp -c descix
|
|
126
127
|
```
|
|
127
128
|
|
|
128
129
|
---
|
|
129
130
|
|
|
130
131
|
### descix kb
|
|
131
132
|
|
|
132
|
-
**Description:** Manage Knowledge Base
|
|
133
|
-
**Use when:**
|
|
133
|
+
**Description:** Manage Knowledge Base registration, corpus sync and per-KB configuration
|
|
134
|
+
**Use when:** Creating a KB, syncing its corpus to Pinecone, or managing per-KB settings
|
|
134
135
|
|
|
135
136
|
**Subcommands:**
|
|
136
|
-
- `
|
|
137
|
-
- `
|
|
138
|
-
- `
|
|
139
|
-
- `
|
|
137
|
+
- `create` / `list` / `delete`: Register, list or remove a KB
|
|
138
|
+
- `corpus sync`: The one KB sync surface — walks the corpus manifest, chunks in-memory, pushes to Pinecone
|
|
139
|
+
- `set-override-model` / `clear-override-model`: Pin or clear a per-KB model override
|
|
140
|
+
- `status`: Show corpus sync state (files, chunks, last sync, resolved ref)
|
|
141
|
+
- `doctor`: Detect drift between local sync-state and live Pinecone vector count
|
|
142
|
+
- `records put|query|get|delete`: Structured record CRUD on a KB
|
|
143
|
+
|
|
144
|
+
**Drive content authoring is a separate command:** `descix drive pull` / `descix drive push` (not under `kb`) move source documents between Drive and the local working tree.
|
|
140
145
|
|
|
141
146
|
**Example:**
|
|
142
147
|
```bash
|
|
143
|
-
#
|
|
144
|
-
descix kb
|
|
148
|
+
# Sync a KB's corpus manifest to Pinecone
|
|
149
|
+
descix kb corpus sync -a myapp -k General
|
|
145
150
|
```
|
|
146
151
|
|
|
147
152
|
---
|
|
@@ -159,8 +164,7 @@ The scaffold reads them via `utils.DEVELOPER_WALLET_ADDRESS` / `utils.DEVELOPER_
|
|
|
159
164
|
|
|
160
165
|
**Canonical microservice onboarding sequence:**
|
|
161
166
|
```bash
|
|
162
|
-
descix app
|
|
163
|
-
descix app init -a <community>-<app>
|
|
167
|
+
descix app init -a <app> -c <community>
|
|
164
168
|
descix app set-port -a <community>-<app> -p <port>
|
|
165
169
|
descix microservice register
|
|
166
170
|
# then: cp dev-overrides.example.json dev-overrides.json and fill in the developer credential
|
|
@@ -200,14 +204,15 @@ descix purchases
|
|
|
200
204
|
|
|
201
205
|
---
|
|
202
206
|
|
|
203
|
-
## MCP Tools
|
|
207
|
+
## MCP Tools
|
|
204
208
|
|
|
205
|
-
After running `descix
|
|
209
|
+
After running `descix quickstart` and reloading your editor/session, these MCP tools are available to the AI agent. `descix quickstart` authenticates via device login as its first step (opens a browser device-code flow) before it writes workspace, agent-instruction and MCP config files; there is no unauthenticated form of any of this.
|
|
206
210
|
|
|
207
211
|
### tell_me_how
|
|
208
212
|
|
|
209
213
|
**Description:** Primary discovery tool - finds relevant commands for any task
|
|
210
|
-
**
|
|
214
|
+
**Prerequisite:** the caller is signed in — it returns caller-specific context and is refused loud (`Authentication required`, exit 1) for a credential-free caller, never a silent partial answer
|
|
215
|
+
**Use first:** Once signed in, always use this before attempting platform operations
|
|
211
216
|
|
|
212
217
|
```javascript
|
|
213
218
|
tell_me_how({
|
|
@@ -230,7 +235,7 @@ tell_me_how({
|
|
|
230
235
|
|
|
231
236
|
```javascript
|
|
232
237
|
execute_remote_command({
|
|
233
|
-
command: "
|
|
238
|
+
command: "create_app_for_community",
|
|
234
239
|
params: {
|
|
235
240
|
community_id: "descix",
|
|
236
241
|
app_name: "My New App"
|
|
@@ -240,90 +245,31 @@ execute_remote_command({
|
|
|
240
245
|
|
|
241
246
|
---
|
|
242
247
|
|
|
243
|
-
###
|
|
244
|
-
|
|
245
|
-
**Description:** Initialize a local project for DeSciX deployment
|
|
246
|
-
**Use when:** Setting up a new project folder
|
|
247
|
-
|
|
248
|
-
```javascript
|
|
249
|
-
descix_init({
|
|
250
|
-
projectPath: "/Users/dev/Projects/MyApp",
|
|
251
|
-
appName: "my-app",
|
|
252
|
-
communityId: "descix"
|
|
253
|
-
})
|
|
254
|
-
```
|
|
255
|
-
|
|
256
|
-
**Creates:**
|
|
257
|
-
- `.descix/workspace.json` or `.descix/context.json`
|
|
258
|
-
- `app_description.md` from README if missing
|
|
259
|
-
- Folder structure for knowledge base
|
|
260
|
-
|
|
261
|
-
---
|
|
262
|
-
|
|
263
|
-
### descix_wizard_step
|
|
248
|
+
### query_knowledge_base
|
|
264
249
|
|
|
265
|
-
**Description:**
|
|
266
|
-
**Use when:**
|
|
250
|
+
**Description:** Vector-similarity search over a knowledge base — returns raw source chunks with dereferenceable citations (retrieval, no synthesis, stateless)
|
|
251
|
+
**Use when:** You want primary-source passages/citations, or to ground/check an `ask_question_to_app` answer
|
|
267
252
|
|
|
268
253
|
```javascript
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
params: {
|
|
273
|
-
community_id: "descix",
|
|
274
|
-
app_name: "My App"
|
|
275
|
-
}
|
|
276
|
-
})
|
|
277
|
-
```
|
|
278
|
-
|
|
279
|
-
**Wizard types:**
|
|
280
|
-
- `create_app`: App creation with folder setup
|
|
281
|
-
- `create_community`: Community creation with token
|
|
282
|
-
- `publish_kb`: Knowledge base sync and publish
|
|
283
|
-
|
|
284
|
-
---
|
|
285
|
-
|
|
286
|
-
### git_sync_ops
|
|
287
|
-
|
|
288
|
-
**Description:** Git operations with auto-generated commit messages
|
|
289
|
-
**Use when:** Committing changes, checking status
|
|
290
|
-
|
|
291
|
-
```javascript
|
|
292
|
-
git_sync_ops({
|
|
293
|
-
operation: "status", // or "commit", "diff", "sync_info"
|
|
294
|
-
projectPath: "/Users/dev/Projects/MyApp"
|
|
295
|
-
})
|
|
296
|
-
```
|
|
297
|
-
|
|
298
|
-
---
|
|
299
|
-
|
|
300
|
-
### search_knowledge_base
|
|
301
|
-
|
|
302
|
-
**Description:** Search a specific knowledge base via RAG
|
|
303
|
-
**Use when:** You know the exact KB to search
|
|
304
|
-
|
|
305
|
-
```javascript
|
|
306
|
-
search_knowledge_base({
|
|
307
|
-
communityId: "descix",
|
|
308
|
-
appId: "docs",
|
|
309
|
-
kbId: "sdk",
|
|
254
|
+
query_knowledge_base({
|
|
255
|
+
app_id: "docs",
|
|
256
|
+
kb_id: "sdk",
|
|
310
257
|
query: "How do I authenticate?"
|
|
311
258
|
})
|
|
312
259
|
```
|
|
313
260
|
|
|
314
261
|
---
|
|
315
262
|
|
|
316
|
-
###
|
|
263
|
+
### ask_question_to_app
|
|
317
264
|
|
|
318
|
-
**Description:**
|
|
319
|
-
**Use when:** Complex questions requiring AI reasoning
|
|
265
|
+
**Description:** Ask a natural-language question against an app's knowledge base and get a synthesized, cited answer (stateful — pass `previous_interaction_id` to continue a thread)
|
|
266
|
+
**Use when:** Complex questions requiring AI reasoning over a community's own sources
|
|
320
267
|
|
|
321
268
|
```javascript
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
message: "Explain the authentication flow step by step"
|
|
269
|
+
ask_question_to_app({
|
|
270
|
+
app_id: "docs",
|
|
271
|
+
knowledgebase_name: "sdk",
|
|
272
|
+
user_input: "Explain the authentication flow step by step"
|
|
327
273
|
})
|
|
328
274
|
```
|
|
329
275
|
|
|
@@ -331,65 +277,44 @@ chat_with_kb({
|
|
|
331
277
|
|
|
332
278
|
## Workspace Configuration
|
|
333
279
|
|
|
334
|
-
### workspace.json (
|
|
280
|
+
### workspace.json (v2.1 — canonical)
|
|
335
281
|
|
|
336
|
-
|
|
282
|
+
`.descix/workspace.json` is written and mutated by `descix init` / `descix app init` / `descix app set-*` — never hand-edited. The v2.1 format is `env.platform` (the shell/store app, served at root) plus `env.products[]` (apps hosted inside the shell); the older `communities`-based v1 format is not supported and hard-errors on load ("v1 workspace format is not supported. Migrate to v2.1."):
|
|
337
283
|
|
|
338
284
|
```json
|
|
339
285
|
{
|
|
340
|
-
"version": "2.
|
|
341
|
-
"
|
|
342
|
-
|
|
343
|
-
"
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
},
|
|
347
|
-
"defaultContext": {
|
|
348
|
-
"communityId": "descix",
|
|
349
|
-
"appId": "docs",
|
|
350
|
-
"kbId": "sdk"
|
|
286
|
+
"version": "2.1",
|
|
287
|
+
"env": {
|
|
288
|
+
"platform": { "appId": "daita", "communityId": "descix", "localPath": ".", "kbId": "General" },
|
|
289
|
+
"products": [
|
|
290
|
+
{ "appId": "docs", "communityId": "descix", "kbId": "sdk", "localPath": "descix-docs" }
|
|
291
|
+
]
|
|
351
292
|
}
|
|
352
293
|
}
|
|
353
294
|
```
|
|
354
295
|
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
For individual app folders, create `.descix/context.json`:
|
|
358
|
-
|
|
359
|
-
```json
|
|
360
|
-
{
|
|
361
|
-
"version": "2.0",
|
|
362
|
-
"type": "app",
|
|
363
|
-
"community_id": "descix",
|
|
364
|
-
"app_id": "myapp",
|
|
365
|
-
"app_name": "My Application",
|
|
366
|
-
"folders": {
|
|
367
|
-
"app": { "local_path": ".", "sync_type": "assets" },
|
|
368
|
-
"kb": { "local_path": "docs", "sync_type": "rag" }
|
|
369
|
-
}
|
|
370
|
-
}
|
|
371
|
-
```
|
|
296
|
+
A single-app (non-monorepo) workspace uses the same v2.1 shape with only `env.platform` set and an empty (or omitted) `env.products[]` — there is no separate single-app file format.
|
|
372
297
|
|
|
373
298
|
---
|
|
374
299
|
|
|
375
300
|
## Common Workflows
|
|
376
301
|
|
|
377
|
-
### New Developer Onboarding
|
|
302
|
+
### New Developer Onboarding
|
|
378
303
|
|
|
379
304
|
1. Install CLI: `npm install -g @descix/cli`
|
|
380
|
-
2. Run
|
|
381
|
-
3.
|
|
305
|
+
2. Run quickstart: `descix quickstart` — authenticates, initializes `.descix/workspace.json`, generates agent instruction files (`CLAUDE.md`, `.github/copilot-instructions.md`, `.cursorrules`, `.clinerules`) for Claude Code, Copilot, Cursor and Cline, and writes `.vscode/mcp.json` (skipped if the DeSciX VS Code extension is installed — it registers MCP natively)
|
|
306
|
+
3. Reload the editor/session so it picks up the generated files
|
|
382
307
|
4. AI agent can now use MCP tools
|
|
383
308
|
5. Ask: "How do I create an app?"
|
|
384
309
|
6. AI uses `tell_me_how` → `execute_remote_command`
|
|
385
310
|
|
|
386
311
|
### Entitlements-First App Creation
|
|
387
312
|
|
|
388
|
-
1. AI calls `
|
|
313
|
+
1. AI calls `fetch_my_purchases()` directly (it is its own MCP tool, not an `execute_remote_command` target)
|
|
389
314
|
2. AI presents user's communities and apps
|
|
390
315
|
3. User selects community (or creates new)
|
|
391
|
-
4. AI calls `
|
|
392
|
-
5. AI
|
|
316
|
+
4. AI calls `execute_remote_command({ command: "create_app_for_community", params: {...} })` for app creation
|
|
317
|
+
5. AI runs `descix app init` to register the local workspace mapping in `.descix/workspace.json`
|
|
393
318
|
|
|
394
319
|
### Monorepo Setup
|
|
395
320
|
|
|
@@ -404,9 +329,9 @@ For individual app folders, create `.descix/context.json`:
|
|
|
404
329
|
## Troubleshooting
|
|
405
330
|
|
|
406
331
|
### "MCP tools not available"
|
|
407
|
-
1. Run `descix
|
|
408
|
-
2.
|
|
409
|
-
3. Check `.
|
|
332
|
+
1. Run `descix quickstart` in workspace root
|
|
333
|
+
2. Reload your editor/session completely
|
|
334
|
+
3. Check `.vscode/mcp.json` exists with a `descix` server (this file is skipped, by design, if the DeSciX VS Code extension is installed — it registers MCP natively)
|
|
410
335
|
|
|
411
336
|
### "401 Unauthorized on a loopback / mesh call"
|
|
412
337
|
For the app data plane, your service authenticates AS the developer (wallet_address + signature). A 401 means the
|
|
@@ -421,7 +346,7 @@ Run `descix reconnect` or `descix login`
|
|
|
421
346
|
Service READMEs may not be vectorized. Ask admin to run `descix microservice vectorize` for relevant services.
|
|
422
347
|
|
|
423
348
|
### "workspace.json not found" for project scope
|
|
424
|
-
Run `descix init` or
|
|
349
|
+
Run `descix init` (or `descix quickstart`, which runs it for you if missing).
|
|
425
350
|
|
|
426
351
|
---
|
|
427
352
|
|