@geraldmaron/construct 1.5.1 → 1.5.3
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 +7 -1
- package/bin/construct +354 -19
- package/lib/adapters-sync.mjs +1 -1
- package/lib/artifact-loop-core.mjs +26 -5
- package/lib/artifact-manifest-overlay.mjs +273 -0
- package/lib/artifact-manifest.mjs +13 -10
- package/lib/auto-docs.mjs +5 -1
- package/lib/cli-commands.mjs +51 -6
- package/lib/config/source-target-registry.mjs +1 -0
- package/lib/config/source-targets.mjs +30 -0
- package/lib/decisions/golden.mjs +17 -18
- package/lib/doc-stamp.mjs +12 -0
- package/lib/doctor/index.mjs +2 -1
- package/lib/doctor/source-target-health.mjs +101 -0
- package/lib/doctor/watchers/source-targets.mjs +44 -0
- package/lib/document-ingest.mjs +25 -3
- package/lib/embed/demand-fetch.mjs +30 -36
- package/lib/embed/providers/directory.mjs +117 -0
- package/lib/embed/providers/registry.mjs +7 -0
- package/lib/extensions/manifests/atlassian-jira.manifest.json +1 -1
- package/lib/extensions/manifests/directory.manifest.json +24 -1
- package/lib/extensions/manifests/github.manifest.json +10 -0
- package/lib/extensions/manifests/linear.manifest.json +1 -1
- package/lib/hooks/session-start.mjs +18 -11
- package/lib/knowledge/rag.mjs +52 -11
- package/lib/knowledge/search.mjs +69 -6
- package/lib/knowledge/synthesis.mjs +157 -0
- package/lib/mcp/server.mjs +2 -2
- package/lib/mcp/tool-definitions-workflow.mjs +35 -7
- package/lib/mcp/tools/artifact-author.mjs +126 -13
- package/lib/mcp/tools/orchestration-run.mjs +3 -0
- package/lib/mcp/tools/skills.mjs +17 -0
- package/lib/model-cheapest-provider.mjs +2 -1
- package/lib/model-policy.mjs +329 -0
- package/lib/model-router.mjs +10 -9
- package/lib/model-tiers.mjs +27 -0
- package/lib/models/catalog.mjs +5 -4
- package/lib/orchestration/classification.mjs +11 -0
- package/lib/orchestration/context-bindings.mjs +85 -0
- package/lib/orchestration/readiness.mjs +2 -1
- package/lib/orchestration/research-evidence-gate.mjs +104 -0
- package/lib/orchestration/runtime.mjs +35 -1
- package/lib/orchestration/worker.mjs +9 -0
- package/lib/review-pr.mjs +102 -0
- package/lib/setup.mjs +2 -1
- package/lib/sources/content-roots.mjs +147 -0
- package/lib/sources/repo-cache.mjs +142 -0
- package/lib/template-registry.mjs +2 -0
- package/lib/tracker/contribute.mjs +266 -0
- package/lib/validator.mjs +2 -3
- package/package.json +1 -5
- package/registry/agent-manifest.json +0 -4
- package/registry/capabilities.json +20 -1
- package/scripts/sync-specialists.mjs +12 -3
- package/templates/docs/README.md +22 -0
- package/templates/docs/adhoc.md +12 -0
- package/templates/docs/strategy-comparison.md +19 -0
package/README.md
CHANGED
|
@@ -22,6 +22,9 @@ Install the CLI (once per machine):
|
|
|
22
22
|
npm install -g @geraldmaron/construct
|
|
23
23
|
```
|
|
24
24
|
|
|
25
|
+
> [!NOTE]
|
|
26
|
+
> npm may print deprecation warnings for `boolean` and `node-domexception` during install. Both are transitive dependencies of upstream packages (`@huggingface/transformers` → `onnxruntime-node` → `global-agent` → `boolean`; `@lancedb/lancedb` → `openai@4.29.2` → `formdata-node` → `node-domexception`) and are harmless. They cannot be silenced from this package — `@lancedb/lancedb` pins `openai@4.29.2` exactly, and npm ignores a package's own `overrides` on end-user installs.
|
|
27
|
+
|
|
25
28
|
Bootstrap local services (once per machine, opt-in to machine-scope writes):
|
|
26
29
|
|
|
27
30
|
```bash
|
|
@@ -198,6 +201,7 @@ The embed daemon writes its supervisor stdout log to `~/.cx/runtime/embed-daemon
|
|
|
198
201
|
| `construct reflect` | Capture improvement feedback and update Construct core |
|
|
199
202
|
| `construct search` | Hybrid search across project state |
|
|
200
203
|
| `construct storage` | Manage storage backend |
|
|
204
|
+
| `construct synthesize` | Cross-project synthesis: map each registered project, reduce to an origin-cited answer |
|
|
201
205
|
| `construct tags` | Manage the controlled tag vocabulary (propose, add, deprecate, audit) |
|
|
202
206
|
| `construct team` | Team review, template listing, and custom team authoring (`team:add` / `team:remove` are internal registry editors) |
|
|
203
207
|
| `construct tools` | Detect optional publish pipeline binaries (Pandoc, D2, VHS, Playwright) |
|
|
@@ -220,6 +224,7 @@ The embed daemon writes its supervisor stdout log to `~/.cx/runtime/embed-daemon
|
|
|
220
224
|
| `construct models` | Show or update model tier assignments |
|
|
221
225
|
| `construct orchestrate` | Construct-owned local orchestration runtime and readiness preflight |
|
|
222
226
|
| `construct plugin` | Manage external Construct plugin manifests |
|
|
227
|
+
| `construct tracker` | Analyze registered projects and contribute governed issue proposals to an external tracker (Jira) |
|
|
223
228
|
|
|
224
229
|
### Integrations
|
|
225
230
|
|
|
@@ -241,7 +246,7 @@ The embed daemon writes its supervisor stdout log to `~/.cx/runtime/embed-daemon
|
|
|
241
246
|
| `construct improvement` | Governed improvement loop — review, approve, and record apply/rollback for proposals |
|
|
242
247
|
| `construct llm-judge` | Run LLM-as-a-judge evaluations on unscored traces for continuous quality feedback |
|
|
243
248
|
| `construct optimize` | Prompt optimization using telemetry trace quality scores |
|
|
244
|
-
| `construct review` |
|
|
249
|
+
| `construct review` | Agent performance review from telemetry (run\|legacy), or a deterministic PR-diff review for CI (pr) |
|
|
245
250
|
| `construct telemetry` | Query telemetry traces and latency data |
|
|
246
251
|
| `construct telemetry-backfill` | Backfill sparse traces with observations (trace backend) |
|
|
247
252
|
| `construct telemetry-setup` | Configure telemetry backend credentials and trace export (OTLP or Langfuse-compatible) |
|
|
@@ -288,6 +293,7 @@ The embed daemon writes its supervisor stdout log to `~/.cx/runtime/embed-daemon
|
|
|
288
293
|
| `construct scheduler` | Manage scheduled background jobs (tag-mining, doc-hygiene, skill-rollup) |
|
|
289
294
|
| `construct skills` | Skill relevance detection |
|
|
290
295
|
| `construct sources` | Manage typed integration source targets in construct.config.json |
|
|
296
|
+
| `construct templates` | List doc templates and register custom document classes (project-tier overlay; builtin manifest untouched) |
|
|
291
297
|
| `construct uninstall` | Remove Construct state |
|
|
292
298
|
| `construct update` | Reinstall this checkout |
|
|
293
299
|
| `construct upgrade` | Upgrade to latest npm version |
|
package/bin/construct
CHANGED
|
@@ -19,6 +19,7 @@ import { CLI_COMMANDS, CATEGORY_ORDER, formatCommandHelp } from '../lib/cli-comm
|
|
|
19
19
|
import { buildStatus, formatStatusReport } from '../lib/status.mjs';
|
|
20
20
|
import { validateRegistry } from '../lib/validator.mjs';
|
|
21
21
|
import { readCurrentModels, readOpenRouterApiKeyFromOpenCodeConfig, applyToEnv, resetEnv, setModelWithTierInference, listAvailableModels } from '../lib/model-router.mjs';
|
|
22
|
+
import { MODEL_TIERS } from '../lib/model-tiers.mjs';
|
|
22
23
|
import { pollFreeModels, topForTier, selectForTier } from '../lib/model-free-selector.mjs';
|
|
23
24
|
import { cmdMcpList, cmdMcpAdd, cmdMcpRemove, cmdMcpInfo } from '../lib/mcp-manager.mjs';
|
|
24
25
|
import { cmdOllama } from '../lib/ollama-manager.mjs';
|
|
@@ -1513,6 +1514,27 @@ async function cmdDoctor() {
|
|
|
1513
1514
|
add(`Reconciliation drift check failed: ${err.message}`, false, true);
|
|
1514
1515
|
}
|
|
1515
1516
|
|
|
1517
|
+
// Source-target health (B8): directory paths that vanished and corpus caches
|
|
1518
|
+
// past TTL, plus soft credential-presence notices. Filesystem + env only — no
|
|
1519
|
+
// outbound fetch — so the default doctor keeps its zero-network invariant. A
|
|
1520
|
+
// project with no registered targets stays silent.
|
|
1521
|
+
try {
|
|
1522
|
+
const { checkSourceTargetHealth } = await import('../lib/doctor/source-target-health.mjs');
|
|
1523
|
+
const { configured, findings } = checkSourceTargetHealth({ cwd: process.cwd() });
|
|
1524
|
+
if (configured > 0) {
|
|
1525
|
+
const problems = findings.filter((f) => !f.ok);
|
|
1526
|
+
if (problems.length === 0) {
|
|
1527
|
+
add(`Source targets healthy (${configured} configured)`, true);
|
|
1528
|
+
} else {
|
|
1529
|
+
for (const f of findings) {
|
|
1530
|
+
if (!f.ok) add(f.label, false, Boolean(f.optional));
|
|
1531
|
+
}
|
|
1532
|
+
}
|
|
1533
|
+
}
|
|
1534
|
+
} catch (err) {
|
|
1535
|
+
add(`Source-target health check failed: ${err.message}`, false, true);
|
|
1536
|
+
}
|
|
1537
|
+
|
|
1516
1538
|
// Docling document-extraction runtime (Python venv via uv). Provisioned lazily
|
|
1517
1539
|
// on first document ingest, or eagerly via `construct install --with-docling`.
|
|
1518
1540
|
// Advisory: absence is fine until a document is ingested.
|
|
@@ -1864,10 +1886,10 @@ async function cmdMigrate(args) {
|
|
|
1864
1886
|
}
|
|
1865
1887
|
|
|
1866
1888
|
async function cmdReview(args) {
|
|
1867
|
-
fs.mkdirSync(path.join(doctorRoot(), 'performance-reviews'), { recursive: true });
|
|
1868
1889
|
const sub = args[0];
|
|
1869
1890
|
|
|
1870
1891
|
if (!sub || sub === 'run') {
|
|
1892
|
+
fs.mkdirSync(path.join(doctorRoot(), 'performance-reviews'), { recursive: true });
|
|
1871
1893
|
const { runGenerator } = await import('../lib/performance/generate.mjs');
|
|
1872
1894
|
const result = await runGenerator({ env: process.env });
|
|
1873
1895
|
info(`Wrote ${result.filename} · ${result.agentCount} agent${result.agentCount === 1 ? '' : 's'}`);
|
|
@@ -1876,6 +1898,7 @@ async function cmdReview(args) {
|
|
|
1876
1898
|
}
|
|
1877
1899
|
|
|
1878
1900
|
if (sub === 'legacy') {
|
|
1901
|
+
fs.mkdirSync(path.join(doctorRoot(), 'performance-reviews'), { recursive: true });
|
|
1879
1902
|
if (!ENV.CONSTRUCT_TELEMETRY_PUBLIC_KEY || !ENV.CONSTRUCT_TELEMETRY_SECRET_KEY) {
|
|
1880
1903
|
errorln('Error: CONSTRUCT_TELEMETRY_PUBLIC_KEY and CONSTRUCT_TELEMETRY_SECRET_KEY must be set.');
|
|
1881
1904
|
errorln('Configure telemetry credentials in ~/.construct/config.env or .env.');
|
|
@@ -1885,7 +1908,22 @@ async function cmdReview(args) {
|
|
|
1885
1908
|
return;
|
|
1886
1909
|
}
|
|
1887
1910
|
|
|
1888
|
-
|
|
1911
|
+
if (sub === 'pr') {
|
|
1912
|
+
const { runReviewPrCli } = await import('../lib/review-pr.mjs');
|
|
1913
|
+
const { exitCode, error, result, json, outputPath } = runReviewPrCli(args.slice(1));
|
|
1914
|
+
if (error) {
|
|
1915
|
+
errorln(error);
|
|
1916
|
+
process.exit(exitCode);
|
|
1917
|
+
}
|
|
1918
|
+
if (outputPath) {
|
|
1919
|
+
info(`Wrote ${outputPath} · ${result.findings.length} finding${result.findings.length === 1 ? '' : 's'}`);
|
|
1920
|
+
} else {
|
|
1921
|
+
println(json);
|
|
1922
|
+
}
|
|
1923
|
+
return;
|
|
1924
|
+
}
|
|
1925
|
+
|
|
1926
|
+
errorln('Usage: construct review [run|legacy|pr --base=<ref> [--output=<file>]]');
|
|
1889
1927
|
process.exit(1);
|
|
1890
1928
|
}
|
|
1891
1929
|
|
|
@@ -3292,6 +3330,11 @@ async function cmdSources(args) {
|
|
|
3292
3330
|
findProjectConfigPath,
|
|
3293
3331
|
PROJECT_CONFIG_FILENAME,
|
|
3294
3332
|
} = await import('../lib/config/project-config.mjs');
|
|
3333
|
+
const {
|
|
3334
|
+
isCorpusTarget,
|
|
3335
|
+
corpusFreshness,
|
|
3336
|
+
syncCorpusTarget,
|
|
3337
|
+
} = await import('../lib/sources/repo-cache.mjs');
|
|
3295
3338
|
|
|
3296
3339
|
const cwd = process.cwd();
|
|
3297
3340
|
const action = args[0] ?? 'list';
|
|
@@ -3305,7 +3348,15 @@ async function cmdSources(args) {
|
|
|
3305
3348
|
println('');
|
|
3306
3349
|
if (configTargets.length === 0) println(' (no targets in construct.config.json)');
|
|
3307
3350
|
for (const t of configTargets) {
|
|
3308
|
-
|
|
3351
|
+
let content = '';
|
|
3352
|
+
if (isCorpusTarget(t)) {
|
|
3353
|
+
const fresh = corpusFreshness(t, { projectRoot: cwd });
|
|
3354
|
+
const freshness = !fresh.cached
|
|
3355
|
+
? 'not synced'
|
|
3356
|
+
: `synced ${fresh.lastFetch ?? 'unknown'}${fresh.stale ? ', stale' : ''}`;
|
|
3357
|
+
content = ` · corpus(${fresh.ref}) · ${freshness}`;
|
|
3358
|
+
}
|
|
3359
|
+
println(` ${t.id} · ${t.provider} · ${JSON.stringify(t.selector)}${content}`);
|
|
3309
3360
|
}
|
|
3310
3361
|
if (envTargets.length) {
|
|
3311
3362
|
println('');
|
|
@@ -3391,7 +3442,87 @@ async function cmdSources(args) {
|
|
|
3391
3442
|
return;
|
|
3392
3443
|
}
|
|
3393
3444
|
|
|
3394
|
-
|
|
3445
|
+
if (action === 'sync') {
|
|
3446
|
+
const onlyId = args[1] ?? null;
|
|
3447
|
+
const { config } = loadProjectConfig(cwd, process.env);
|
|
3448
|
+
const targets = resolveEffectiveSourceTargetsFromConfig(config, process.env)
|
|
3449
|
+
.filter((t) => isCorpusTarget(t))
|
|
3450
|
+
.filter((t) => !onlyId || t.id === onlyId);
|
|
3451
|
+
|
|
3452
|
+
if (onlyId && !targets.length) {
|
|
3453
|
+
errorln(`No corpus target with id: ${onlyId}`);
|
|
3454
|
+
process.exit(1);
|
|
3455
|
+
}
|
|
3456
|
+
if (!targets.length) {
|
|
3457
|
+
info('no corpus targets to sync');
|
|
3458
|
+
return;
|
|
3459
|
+
}
|
|
3460
|
+
|
|
3461
|
+
let failures = 0;
|
|
3462
|
+
for (const t of targets) {
|
|
3463
|
+
try {
|
|
3464
|
+
const result = syncCorpusTarget(t, { projectRoot: cwd });
|
|
3465
|
+
info(`synced ${t.id} (${result.mode}) → ${result.head.slice(0, 7)} @ ${result.dir}`);
|
|
3466
|
+
} catch (err) {
|
|
3467
|
+
failures++;
|
|
3468
|
+
errorln(`failed to sync ${t.id}: ${err.message}`);
|
|
3469
|
+
}
|
|
3470
|
+
}
|
|
3471
|
+
if (failures) process.exit(1);
|
|
3472
|
+
return;
|
|
3473
|
+
}
|
|
3474
|
+
|
|
3475
|
+
errorln(`Unknown sources action: ${action}. Available: list, add, remove, validate, sync`);
|
|
3476
|
+
process.exit(1);
|
|
3477
|
+
}
|
|
3478
|
+
|
|
3479
|
+
// Read a `--flag value` or `--flag=value` option out of an argv slice.
|
|
3480
|
+
|
|
3481
|
+
function readOption(args, flag) {
|
|
3482
|
+
const eq = args.find((a) => a.startsWith(`${flag}=`));
|
|
3483
|
+
if (eq) return eq.slice(flag.length + 1);
|
|
3484
|
+
const idx = args.indexOf(flag);
|
|
3485
|
+
if (idx >= 0 && args[idx + 1] && !args[idx + 1].startsWith('--')) return args[idx + 1];
|
|
3486
|
+
return undefined;
|
|
3487
|
+
}
|
|
3488
|
+
|
|
3489
|
+
async function cmdTemplates(args) {
|
|
3490
|
+
const { registerArtifactType } = await import('../lib/artifact-manifest-overlay.mjs');
|
|
3491
|
+
const { listTemplates } = await import('../lib/mcp/tools/skills.mjs');
|
|
3492
|
+
const cwd = process.cwd();
|
|
3493
|
+
const action = args[0] ?? 'list';
|
|
3494
|
+
|
|
3495
|
+
if (action === 'list' || action === 'show') {
|
|
3496
|
+
const res = listTemplates({ ROOT_DIR });
|
|
3497
|
+
println('Templates');
|
|
3498
|
+
println(` shipped: ${res.shipped.join(', ')}`);
|
|
3499
|
+
println(` overridden: ${res.overridden.length ? res.overridden.join(', ') : '(none)'}`);
|
|
3500
|
+
return;
|
|
3501
|
+
}
|
|
3502
|
+
|
|
3503
|
+
if (action === 'register') {
|
|
3504
|
+
const type = args.slice(1).find((a) => !a.startsWith('--'));
|
|
3505
|
+
if (!type) {
|
|
3506
|
+
errorln('Usage: construct templates register <type> [--description "..."] [--from <file>] [--force]');
|
|
3507
|
+
process.exit(1);
|
|
3508
|
+
}
|
|
3509
|
+
const description = readOption(args, '--description');
|
|
3510
|
+
const from = readOption(args, '--from');
|
|
3511
|
+
const force = args.includes('--force');
|
|
3512
|
+
try {
|
|
3513
|
+
const r = registerArtifactType({ type, description, from, cwd, force });
|
|
3514
|
+
ok(`registered document class '${r.type}'`);
|
|
3515
|
+
info(`template: ${path.relative(cwd, r.templatePath)}`);
|
|
3516
|
+
info(`overlay: ${path.relative(cwd, r.overlayPath)}`);
|
|
3517
|
+
info(`author it via MCP: author_artifact {type:"${r.type}", draft_markdown:"..."}`);
|
|
3518
|
+
} catch (err) {
|
|
3519
|
+
errorln(err.message);
|
|
3520
|
+
process.exit(1);
|
|
3521
|
+
}
|
|
3522
|
+
return;
|
|
3523
|
+
}
|
|
3524
|
+
|
|
3525
|
+
errorln(`Unknown templates action: ${action}. Available: list, register`);
|
|
3395
3526
|
process.exit(1);
|
|
3396
3527
|
}
|
|
3397
3528
|
|
|
@@ -4183,7 +4314,7 @@ async function cmdOrchestrate(args) {
|
|
|
4183
4314
|
println(summarizeOrchestrationReadiness(readiness));
|
|
4184
4315
|
println(`Reason: ${readiness.reasonCode}`);
|
|
4185
4316
|
println(`Next: ${readiness.nextStep}`);
|
|
4186
|
-
const modelLine =
|
|
4317
|
+
const modelLine = MODEL_TIERS
|
|
4187
4318
|
.map((tier) => `${tier}=${readiness.modelResolved?.[tier] ? 'resolved' : 'unresolved'}`)
|
|
4188
4319
|
.join(' ');
|
|
4189
4320
|
println(`Execution: workerBackend=${readiness.workerBackend} webMode=${readiness.webMode} credentialMaterializable=${readiness.credentialMaterializable} (${modelLine})`);
|
|
@@ -4255,7 +4386,7 @@ async function cmdOrchestrate(args) {
|
|
|
4255
4386
|
const { shapeRun } = await import('../lib/mcp/tools/orchestration-run.mjs');
|
|
4256
4387
|
const positional = rest.filter((a, i) => !a.startsWith('--') && !(i > 0 && rest[i - 1].startsWith('--') && !['--no-construct', '--no-execute', '--json'].includes(rest[i - 1])));
|
|
4257
4388
|
const text = flag('--text') || positional.join(' ').trim();
|
|
4258
|
-
if (!text) { errorln('Usage: construct orchestrate run "<request>" [--workflow-type T] [--strategy orchestrated|prompt-only|auto] [--host H] [--host-model M] [--no-construct] [--no-execute] [--json]'); process.exit(1); }
|
|
4389
|
+
if (!text) { errorln('Usage: construct orchestrate run "<request>" [--workflow-type T] [--strategy orchestrated|prompt-only|auto] [--context id[:role],...] [--host H] [--host-model M] [--no-construct] [--no-execute] [--json]'); process.exit(1); }
|
|
4259
4390
|
const cliWorkerBackend = flag('--worker-backend');
|
|
4260
4391
|
// The host worker backend materializes prompts for an attached MCP host
|
|
4261
4392
|
// session to execute and submit back via orchestration_task_result — a
|
|
@@ -4267,6 +4398,19 @@ async function cmdOrchestrate(args) {
|
|
|
4267
4398
|
errorln('--worker-backend=host requires an attached MCP host session to execute the materialized prompts and submit results back — the CLI has no such session. Use --worker-backend=inline (prepare only) or --worker-backend=provider (needs a provider key), or invoke orchestration_run through an MCP host instead.');
|
|
4268
4399
|
process.exit(1);
|
|
4269
4400
|
}
|
|
4401
|
+
// --context=id[:role],id[:role] binds the run to registered source targets;
|
|
4402
|
+
// an unknown id is rejected at plan time (below), never a silent skip. Accept
|
|
4403
|
+
// both `--context=…` and `--context …` since the epic UX documents the former
|
|
4404
|
+
// while sibling orchestrate flags use the latter.
|
|
4405
|
+
const contextInline = rest.find((a) => a.startsWith('--context='));
|
|
4406
|
+
const contextFlag = contextInline ? contextInline.slice('--context='.length) : flag('--context');
|
|
4407
|
+
const contextTargets = contextFlag
|
|
4408
|
+
? contextFlag.split(',').map((s) => s.trim()).filter(Boolean).map((entry) => {
|
|
4409
|
+
const colon = entry.indexOf(':');
|
|
4410
|
+
return colon === -1 ? { id: entry } : { id: entry.slice(0, colon), role: entry.slice(colon + 1) || undefined };
|
|
4411
|
+
})
|
|
4412
|
+
: undefined;
|
|
4413
|
+
|
|
4270
4414
|
const request = {
|
|
4271
4415
|
request: text,
|
|
4272
4416
|
workflowType: flag('--workflow-type'),
|
|
@@ -4278,13 +4422,20 @@ async function cmdOrchestrate(args) {
|
|
|
4278
4422
|
workerBackend: cliWorkerBackend,
|
|
4279
4423
|
fileCount: Number(flag('--file-count') || 0),
|
|
4280
4424
|
moduleCount: Number(flag('--module-count') || 0),
|
|
4425
|
+
contextTargets,
|
|
4281
4426
|
};
|
|
4282
4427
|
// runOrchestration/executeRun resolve the worker backend from opts.workerBackend,
|
|
4283
4428
|
// not from request — the flag must reach opts explicitly or an override is silently
|
|
4284
4429
|
// dropped (construct-1xlz).
|
|
4285
4430
|
|
|
4286
4431
|
const runOpts = cliWorkerBackend ? { workerBackend: cliWorkerBackend } : {};
|
|
4287
|
-
|
|
4432
|
+
let run;
|
|
4433
|
+
try {
|
|
4434
|
+
run = rest.includes('--no-execute') ? await planRun(request, runOpts) : await runOrchestration(request, runOpts);
|
|
4435
|
+
} catch (err) {
|
|
4436
|
+
if (err?.code === 'CONTEXT_TARGET_UNKNOWN') { errorln(err.message); process.exit(1); }
|
|
4437
|
+
throw err;
|
|
4438
|
+
}
|
|
4288
4439
|
if (wantsJson) { println(JSON.stringify(hostAdapterMetadata(run), null, 2)); return; }
|
|
4289
4440
|
|
|
4290
4441
|
// AP5.1/AP5.4: the CLI must never print the raw persisted status — a
|
|
@@ -4296,6 +4447,9 @@ async function cmdOrchestrate(args) {
|
|
|
4296
4447
|
if (shaped.degraded) println(` degraded: ${shaped.degradationReason}`);
|
|
4297
4448
|
if (run.tasks.length) println(` tasks: ${run.tasks.map((t) => `${t.role}(${t.status})`).join(' → ')}`);
|
|
4298
4449
|
else println(' tasks: none (no Construct-owned specialist sequence for this execution mode)');
|
|
4450
|
+
if (run.contextBindings?.length) {
|
|
4451
|
+
println(` context: ${run.contextBindings.map((b) => `${b.id}[${b.provider}${b.role ? `:${b.role}` : ''}]${b.contentRoot ? '✓' : ''}`).join(', ')}`);
|
|
4452
|
+
}
|
|
4299
4453
|
for (const t of run.tasks) {
|
|
4300
4454
|
if (t.reasoning) println(` ↳ ${t.role} reasoning: ${t.reasoning.length > 300 ? `${t.reasoning.slice(0, 300)}…` : t.reasoning}`);
|
|
4301
4455
|
}
|
|
@@ -4416,12 +4570,22 @@ async function cmdDb(args) {
|
|
|
4416
4570
|
async function cmdModels(args) {
|
|
4417
4571
|
const envPath = getUserEnvPath(HOME);
|
|
4418
4572
|
const sub = args[0] && !args[0].startsWith('-') ? args[0] : null;
|
|
4419
|
-
const KNOWN_SUBCOMMANDS = new Set(['list', 'set', 'free', 'reset', 'resolve']);
|
|
4573
|
+
const KNOWN_SUBCOMMANDS = new Set(['list', 'set', 'free', 'reset', 'resolve', 'policy', 'explain']);
|
|
4420
4574
|
if (sub && !KNOWN_SUBCOMMANDS.has(sub)) {
|
|
4421
4575
|
errorln(`Unknown models subcommand: ${sub}. Run \`construct models --help\` for the supported set.`);
|
|
4422
4576
|
process.exit(1);
|
|
4423
4577
|
}
|
|
4424
4578
|
|
|
4579
|
+
if (sub === 'policy') {
|
|
4580
|
+
await cmdModelsPolicy(args.slice(1));
|
|
4581
|
+
return;
|
|
4582
|
+
}
|
|
4583
|
+
|
|
4584
|
+
if (sub === 'explain') {
|
|
4585
|
+
await cmdModelsExplain(args.slice(1));
|
|
4586
|
+
return;
|
|
4587
|
+
}
|
|
4588
|
+
|
|
4425
4589
|
if (sub === 'resolve') {
|
|
4426
4590
|
const flag = (name) => { const i = args.indexOf(name); return i !== -1 ? args[i + 1] : undefined; };
|
|
4427
4591
|
const { resolveEmbeddedModel } = await import('../lib/embedded-contract/model-resolve.mjs');
|
|
@@ -4487,7 +4651,7 @@ async function cmdModels(args) {
|
|
|
4487
4651
|
}
|
|
4488
4652
|
const models = await pollFreeModels(apiKey);
|
|
4489
4653
|
println('Available free models on OpenRouter:\n');
|
|
4490
|
-
for (const currentTier of
|
|
4654
|
+
for (const currentTier of MODEL_TIERS) {
|
|
4491
4655
|
const top = topForTier(models, currentTier, 3);
|
|
4492
4656
|
println(` ${currentTier} (top ${top.length}):`);
|
|
4493
4657
|
top.forEach((model, index) => println(` ${index + 1}. ${model.id}`));
|
|
@@ -4499,7 +4663,7 @@ async function cmdModels(args) {
|
|
|
4499
4663
|
const apiKey = process.env.OPENROUTER_API_KEY || readOpenRouterApiKeyFromOpenCodeConfig();
|
|
4500
4664
|
const models = await pollFreeModels(apiKey);
|
|
4501
4665
|
const selections = {};
|
|
4502
|
-
for (const currentTier of
|
|
4666
|
+
for (const currentTier of MODEL_TIERS) {
|
|
4503
4667
|
const fallbacks = [];
|
|
4504
4668
|
const id = selectForTier(models, currentTier, fallbacks);
|
|
4505
4669
|
if (id) selections[currentTier] = id;
|
|
@@ -4526,7 +4690,7 @@ async function cmdModels(args) {
|
|
|
4526
4690
|
await import('../lib/model-cheapest-provider.mjs');
|
|
4527
4691
|
const allTiers = args.includes('--all-tiers') || !args.find((arg) => arg.startsWith('--tier='));
|
|
4528
4692
|
const tierArg = args.find((arg) => arg.startsWith('--tier='))?.split('=')[1];
|
|
4529
|
-
const tiers = allTiers ? [
|
|
4693
|
+
const tiers = allTiers ? [...MODEL_TIERS] : (tierArg ? [tierArg] : ['standard']);
|
|
4530
4694
|
const selections = {};
|
|
4531
4695
|
for (const currentTier of tiers) {
|
|
4532
4696
|
const { selectCheapestProvider } = await import('../lib/model-cheapest-provider.mjs');
|
|
@@ -4563,9 +4727,9 @@ async function cmdModels(args) {
|
|
|
4563
4727
|
return;
|
|
4564
4728
|
}
|
|
4565
4729
|
const result = readCurrentModels(envPath, {});
|
|
4566
|
-
const nothingSelected =
|
|
4730
|
+
const nothingSelected = MODEL_TIERS.every((t) => !result[t]);
|
|
4567
4731
|
println('Current model assignments:');
|
|
4568
|
-
for (const currentTier of
|
|
4732
|
+
for (const currentTier of MODEL_TIERS) {
|
|
4569
4733
|
const id = result[currentTier];
|
|
4570
4734
|
const source = result.sources?.[currentTier] || (id ? 'registry' : 'not configured');
|
|
4571
4735
|
println(` ${currentTier.padEnd(11)}${id ? `${id} [${source}]` : '(not configured)'}`);
|
|
@@ -4580,6 +4744,101 @@ async function cmdModels(args) {
|
|
|
4580
4744
|
}
|
|
4581
4745
|
}
|
|
4582
4746
|
|
|
4747
|
+
async function cmdModelsPolicy(args) {
|
|
4748
|
+
const sub = args[0] && !args[0].startsWith('-') ? args[0] : 'show';
|
|
4749
|
+
const asJson = args.includes('--json');
|
|
4750
|
+
const {
|
|
4751
|
+
computePolicyPreset, writeModelRegistry, readPolicyView, POLICY_PRESETS,
|
|
4752
|
+
} = await import('../lib/model-policy.mjs');
|
|
4753
|
+
const { defaultModelRegistryPath } = await import('../lib/model-router.mjs');
|
|
4754
|
+
const registryPath = defaultModelRegistryPath(process.env);
|
|
4755
|
+
|
|
4756
|
+
if (sub === 'show') {
|
|
4757
|
+
const view = readPolicyView({ env: process.env, registryPath });
|
|
4758
|
+
if (asJson) {
|
|
4759
|
+
println(JSON.stringify(view, null, 2));
|
|
4760
|
+
return;
|
|
4761
|
+
}
|
|
4762
|
+
println('Effective model policy:');
|
|
4763
|
+
for (const t of view.tiers) {
|
|
4764
|
+
const model = t.model ?? '(not configured)';
|
|
4765
|
+
println(` ${t.tier.padEnd(11)}${model} [${t.source}]`);
|
|
4766
|
+
}
|
|
4767
|
+
println('');
|
|
4768
|
+
println('Work category → tier:');
|
|
4769
|
+
for (const [category, tier] of Object.entries(view.workCategoryMap)) {
|
|
4770
|
+
println(` ${category.padEnd(11)}→ ${tier}`);
|
|
4771
|
+
}
|
|
4772
|
+
println('');
|
|
4773
|
+
println(`Registry: ${registryPath}`);
|
|
4774
|
+
println('Precedence: CX_MODEL_<TIER> env pins always win over registry defaults.');
|
|
4775
|
+
return;
|
|
4776
|
+
}
|
|
4777
|
+
|
|
4778
|
+
if (sub === 'set') {
|
|
4779
|
+
const preset = args[1] && !args[1].startsWith('-') ? args[1] : null;
|
|
4780
|
+
if (!preset) {
|
|
4781
|
+
errorln(`Usage: construct models policy set <${POLICY_PRESETS.join('|')}>`);
|
|
4782
|
+
process.exit(1);
|
|
4783
|
+
}
|
|
4784
|
+
const apiKey = process.env.OPENROUTER_API_KEY || readOpenRouterApiKeyFromOpenCodeConfig() || null;
|
|
4785
|
+
const result = await computePolicyPreset(preset, { env: process.env, apiKey });
|
|
4786
|
+
if (!result.ok) {
|
|
4787
|
+
errorln(result.refusal);
|
|
4788
|
+
for (const w of result.warnings ?? []) errorln(` ${w}`);
|
|
4789
|
+
process.exit(1);
|
|
4790
|
+
}
|
|
4791
|
+
writeModelRegistry(registryPath, result.models, { preset });
|
|
4792
|
+
if (asJson) {
|
|
4793
|
+
println(JSON.stringify({ preset, registryPath, models: result.models, warnings: result.warnings }, null, 2));
|
|
4794
|
+
return;
|
|
4795
|
+
}
|
|
4796
|
+
println(`Applied \`${preset}\` policy to ${registryPath}:`);
|
|
4797
|
+
for (const tier of MODEL_TIERS) {
|
|
4798
|
+
const def = result.models[tier];
|
|
4799
|
+
const primary = def?.primary ?? '(unset)';
|
|
4800
|
+
const fallback = def?.fallback?.length ? ` (fallback: ${def.fallback.join(', ')})` : '';
|
|
4801
|
+
println(` ${tier.padEnd(11)}${primary}${fallback}`);
|
|
4802
|
+
}
|
|
4803
|
+
for (const w of result.warnings ?? []) errorln(`Note: ${w}`);
|
|
4804
|
+
println('');
|
|
4805
|
+
println('Env pins (CX_MODEL_<TIER>) still override these. Run `construct sync` to propagate.');
|
|
4806
|
+
if (!args.includes('--no-sync')) await cmdSync([]);
|
|
4807
|
+
return;
|
|
4808
|
+
}
|
|
4809
|
+
|
|
4810
|
+
errorln(`Unknown models policy subcommand: ${sub}. Use \`show\` or \`set <preset>\`.`);
|
|
4811
|
+
process.exit(1);
|
|
4812
|
+
}
|
|
4813
|
+
|
|
4814
|
+
async function cmdModelsExplain(args) {
|
|
4815
|
+
const asJson = args.includes('--json');
|
|
4816
|
+
const roleFlagIdx = args.indexOf('--role');
|
|
4817
|
+
const role = roleFlagIdx !== -1 ? args[roleFlagIdx + 1] : (args[0] && !args[0].startsWith('-') ? args[0] : null);
|
|
4818
|
+
if (!role) {
|
|
4819
|
+
errorln('Usage: construct models explain --role <specialist>');
|
|
4820
|
+
process.exit(1);
|
|
4821
|
+
}
|
|
4822
|
+
const { explainRole } = await import('../lib/model-policy.mjs');
|
|
4823
|
+
const trace = explainRole(role, { env: process.env, rootDir: ROOT_DIR });
|
|
4824
|
+
if (!trace.ok) {
|
|
4825
|
+
errorln(trace.error);
|
|
4826
|
+
process.exit(1);
|
|
4827
|
+
}
|
|
4828
|
+
if (asJson) {
|
|
4829
|
+
println(JSON.stringify(trace, null, 2));
|
|
4830
|
+
return;
|
|
4831
|
+
}
|
|
4832
|
+
println(`Resolution trace for ${trace.role}:`);
|
|
4833
|
+
println(` declared tier: ${trace.declaredTier ?? '(none)'}`);
|
|
4834
|
+
if (trace.declaredModel) println(` declared model: ${trace.declaredModel}`);
|
|
4835
|
+
println(` resolved tier: ${trace.tier}`);
|
|
4836
|
+
println(` resolved model: ${trace.resolvedModel ?? '(not configured)'}`);
|
|
4837
|
+
println(` winning source: ${trace.source}`);
|
|
4838
|
+
println('');
|
|
4839
|
+
println(`Matches: construct models resolve --json --tier ${trace.tier}`);
|
|
4840
|
+
}
|
|
4841
|
+
|
|
4583
4842
|
async function cmdMcp(args) {
|
|
4584
4843
|
const subcmd = args[0] ?? 'list';
|
|
4585
4844
|
const id = args[1];
|
|
@@ -6545,6 +6804,7 @@ const handlers = new Map([
|
|
|
6545
6804
|
['config', cmdConfig],
|
|
6546
6805
|
['approvals', cmdApprovals],
|
|
6547
6806
|
['sources', cmdSources],
|
|
6807
|
+
['templates', cmdTemplates],
|
|
6548
6808
|
['intake', cmdIntake],
|
|
6549
6809
|
['recommendations', cmdRecommendations],
|
|
6550
6810
|
['integrations', cmdIntegrations],
|
|
@@ -7257,9 +7517,13 @@ const handlers = new Map([
|
|
|
7257
7517
|
process.exit(1);
|
|
7258
7518
|
}
|
|
7259
7519
|
const { ask, buildCorpus } = await import('../lib/knowledge/rag.mjs');
|
|
7520
|
+
const { resolveContentRootsFromConfig } = await import('../lib/sources/content-roots.mjs');
|
|
7521
|
+
const { loadProjectConfig } = await import('../lib/config/project-config.mjs');
|
|
7260
7522
|
const rootDir = process.cwd();
|
|
7261
|
-
|
|
7262
|
-
const
|
|
7523
|
+
const { config } = loadProjectConfig(rootDir);
|
|
7524
|
+
const roots = resolveContentRootsFromConfig(config, { projectRoot: rootDir });
|
|
7525
|
+
process.stdout.write(`Building knowledge index${roots.length ? ` (+${roots.length} registered project${roots.length === 1 ? '' : 's'})` : ''}…\n`);
|
|
7526
|
+
const corpus = buildCorpus(rootDir, { roots });
|
|
7263
7527
|
process.stdout.write(`Indexed ${corpus.length} chunks. Retrieving…\n\n`);
|
|
7264
7528
|
const result = await ask(question, { rootDir, corpus });
|
|
7265
7529
|
if (result.cliMissing) {
|
|
@@ -7269,7 +7533,8 @@ const handlers = new Map([
|
|
|
7269
7533
|
if (result.sources?.length) {
|
|
7270
7534
|
process.stdout.write('\n--- Sources ---\n');
|
|
7271
7535
|
for (const s of result.sources.slice(0, 5)) {
|
|
7272
|
-
|
|
7536
|
+
const proj = s.origin?.projectKey && s.origin.projectKey !== 'self' ? ` «${s.origin.projectKey}»` : '';
|
|
7537
|
+
process.stdout.write(` • [${s.source}]${proj} ${s.title} (score: ${s.score?.toFixed(3)})\n`);
|
|
7273
7538
|
}
|
|
7274
7539
|
}
|
|
7275
7540
|
}],
|
|
@@ -7384,9 +7649,10 @@ const handlers = new Map([
|
|
|
7384
7649
|
const tagFlag = restArgs.find((a) => a.startsWith('--tag='))?.replace('--tag=', '');
|
|
7385
7650
|
const tagMatchFlag = restArgs.find((a) => a.startsWith('--tag-match='))?.replace('--tag-match=', '');
|
|
7386
7651
|
const topKFlag = restArgs.find((a) => a.startsWith('--top='))?.replace('--top=', '');
|
|
7652
|
+
const projectsFlag = restArgs.find((a) => a.startsWith('--projects='))?.replace('--projects=', '');
|
|
7387
7653
|
const queryParts = restArgs.filter((a) => !a.startsWith('--'));
|
|
7388
7654
|
const query = queryParts.join(' ').trim();
|
|
7389
|
-
if (!query) { errorln('Usage: construct knowledge search "<query>" [--tag=<tag>] [--tag-match=any|all]'); process.exit(1); }
|
|
7655
|
+
if (!query) { errorln('Usage: construct knowledge search "<query>" [--tag=<tag>] [--tag-match=any|all] [--projects=<id,...>|all|self]'); process.exit(1); }
|
|
7390
7656
|
const { knowledgeSearch } = await import('../lib/knowledge/search.mjs');
|
|
7391
7657
|
const tags = tagFlag ? tagFlag.split(',') : undefined;
|
|
7392
7658
|
const result = knowledgeSearch({
|
|
@@ -7396,11 +7662,13 @@ const handlers = new Map([
|
|
|
7396
7662
|
rootDir: process.cwd(),
|
|
7397
7663
|
tags,
|
|
7398
7664
|
tagMatch: tagMatchFlag || 'any',
|
|
7665
|
+
projects: projectsFlag,
|
|
7399
7666
|
});
|
|
7400
7667
|
if (!result.ok) { errorln(result.message); process.exit(1); }
|
|
7401
|
-
println(`${result.hits.length} result(s) for "${query}"${tags ? ` [tag: ${tags.join(',')}]` : ''}\n`);
|
|
7668
|
+
println(`${result.hits.length} result(s) for "${query}"${tags ? ` [tag: ${tags.join(',')}]` : ''}${projectsFlag ? ` [projects: ${projectsFlag}]` : ''}\n`);
|
|
7402
7669
|
for (const h of result.hits) {
|
|
7403
|
-
|
|
7670
|
+
const proj = h.origin?.projectKey && h.origin.projectKey !== 'self' ? ` «${h.origin.projectKey}»` : '';
|
|
7671
|
+
println(` ${h.file}${h.heading ? ` § ${h.heading}` : ''}${proj} (score: ${h.score})`);
|
|
7404
7672
|
println(` ${h.text.slice(0, 120).replace(/\n/g, ' ')}`);
|
|
7405
7673
|
println('');
|
|
7406
7674
|
}
|
|
@@ -7409,6 +7677,73 @@ const handlers = new Map([
|
|
|
7409
7677
|
errorln(`Unknown knowledge subcommand: ${sub}. Available: trends, index, add, search`);
|
|
7410
7678
|
process.exit(1);
|
|
7411
7679
|
}],
|
|
7680
|
+
['synthesize', async (args) => {
|
|
7681
|
+
const flag = (name) => args.find((a) => a.startsWith(`--${name}=`))?.slice(name.length + 3);
|
|
7682
|
+
const projects = flag('projects') || 'all';
|
|
7683
|
+
const ask = flag('ask') || args.filter((a) => !a.startsWith('--')).join(' ').trim();
|
|
7684
|
+
const template = flag('template') || null;
|
|
7685
|
+
const dryRun = args.includes('--dry-run');
|
|
7686
|
+
const wantsJson = args.includes('--json');
|
|
7687
|
+
if (!ask) {
|
|
7688
|
+
errorln('Usage: construct synthesize --ask "<question>" [--projects=all|self|id,...] [--template <name>] [--dry-run] [--json]');
|
|
7689
|
+
process.exit(1);
|
|
7690
|
+
}
|
|
7691
|
+
const { synthesize } = await import('../lib/knowledge/synthesis.mjs');
|
|
7692
|
+
const result = await synthesize({ projects, ask, cwd: process.cwd(), template, dryRun, rootDir: ROOT_DIR });
|
|
7693
|
+
if (!result.ok) { errorln(result.message); process.exit(1); }
|
|
7694
|
+
if (wantsJson) { println(JSON.stringify(result, null, 2)); return; }
|
|
7695
|
+
if (dryRun) {
|
|
7696
|
+
println(result.context);
|
|
7697
|
+
println('\n--- Reduce prompt (not sent in --dry-run) ---\n');
|
|
7698
|
+
println(result.prompt);
|
|
7699
|
+
return;
|
|
7700
|
+
}
|
|
7701
|
+
if (result.cliMissing) println('[claude CLI not available — showing assembled context]\n');
|
|
7702
|
+
println(result.answer);
|
|
7703
|
+
return;
|
|
7704
|
+
}],
|
|
7705
|
+
['tracker', async (args) => {
|
|
7706
|
+
const sub = args[0] || 'contribute';
|
|
7707
|
+
const flag = (name) => args.find((a) => a.startsWith(`--${name}=`))?.slice(name.length + 3)
|
|
7708
|
+
?? (args.includes(`--${name}`) ? args[args.indexOf(`--${name}`) + 1] : undefined);
|
|
7709
|
+
if (sub !== 'contribute') { errorln(`Unknown tracker subcommand: ${sub}. Available: contribute`); process.exit(1); }
|
|
7710
|
+
const wantsJson = args.includes('--json');
|
|
7711
|
+
const applyId = flag('apply');
|
|
7712
|
+
const { analyzeAndPropose, applyProposal } = await import('../lib/tracker/contribute.mjs');
|
|
7713
|
+
|
|
7714
|
+
if (applyId) {
|
|
7715
|
+
const approve = flag('approve') || null;
|
|
7716
|
+
const res = await applyProposal({ proposalId: applyId, approveToken: approve, cwd: process.cwd() });
|
|
7717
|
+
if (!res.ok) { errorln(res.message); process.exit(1); }
|
|
7718
|
+
if (wantsJson) { println(JSON.stringify(res, null, 2)); return; }
|
|
7719
|
+
println(`Applied proposal ${res.proposalId} — ${res.dryRun ? 'DRY-RUN (nothing written)' : 'EXECUTED'}`);
|
|
7720
|
+
for (const r of res.results) println(` ${r.key} — ${r.status}`);
|
|
7721
|
+
if (res.dryRun) println('\nRe-run with --approve <token> to write the governed batch.');
|
|
7722
|
+
return;
|
|
7723
|
+
}
|
|
7724
|
+
|
|
7725
|
+
const target = flag('target');
|
|
7726
|
+
const against = flag('against') || 'all';
|
|
7727
|
+
if (!target) {
|
|
7728
|
+
errorln('Usage: construct tracker contribute --target <id> [--against <ids|all>] [--json]\n or: construct tracker contribute --apply <proposal-id> [--approve <token>] [--json]');
|
|
7729
|
+
process.exit(1);
|
|
7730
|
+
}
|
|
7731
|
+
let res;
|
|
7732
|
+
try {
|
|
7733
|
+
res = await analyzeAndPropose({ target, against, cwd: process.cwd() });
|
|
7734
|
+
} catch (err) {
|
|
7735
|
+
errorln(err.message); process.exit(1);
|
|
7736
|
+
}
|
|
7737
|
+
if (!res.ok) { errorln(res.message); process.exit(1); }
|
|
7738
|
+
if (wantsJson) { println(JSON.stringify(res.proposal, null, 2)); return; }
|
|
7739
|
+
const p = res.proposal;
|
|
7740
|
+
println(`Proposal ${p.id} for ${p.projectKey} (${p.targetId}) — ${p.proposals.length} proposed, ${p.suppressed.length} suppressed (${p.existingIssueCount} existing issues)`);
|
|
7741
|
+
println(` artifact: ${res.paths.md}`);
|
|
7742
|
+
for (const it of p.proposals) println(` + ${it.summary}`);
|
|
7743
|
+
for (const s of p.suppressed) println(` ~ suppressed: ${s.summary} → ${s.matchedIssueKey} (${s.similarity})`);
|
|
7744
|
+
println(`\nReview the artifact, then: construct tracker contribute --apply ${p.id} [--approve <token>]`);
|
|
7745
|
+
return;
|
|
7746
|
+
}],
|
|
7412
7747
|
['sandbox', async (args) => {
|
|
7413
7748
|
const sub = args[0] || 'create';
|
|
7414
7749
|
const { createSandbox, listSandboxes, deleteSandbox, pruneSandboxes } = await import('../lib/sandbox.mjs');
|
package/lib/adapters-sync.mjs
CHANGED
|
@@ -27,7 +27,7 @@ const HOST_ID_MAP = {
|
|
|
27
27
|
};
|
|
28
28
|
|
|
29
29
|
export function resolveAdapterHosts({ forceAll = false, extra = [] } = {}) {
|
|
30
|
-
if (forceAll) return ['claude', 'opencode', 'codex', 'vscode', 'cursor'];
|
|
30
|
+
if (forceAll) return ['claude', 'opencode', 'codex', 'vscode', 'cursor', 'copilot'];
|
|
31
31
|
const hosts = new Set(extra);
|
|
32
32
|
for (const entry of detectHostCapabilities()) {
|
|
33
33
|
if (entry.availability !== 'installed') continue;
|