@tiangong-ai/cli 0.0.30 → 0.0.31

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.
@@ -8,10 +8,10 @@ import { CliError } from "../../errors.js";
8
8
  import { loadCapabilityDeclarations } from "./capabilities.js";
9
9
  import { inspectResearchContext } from "./context.js";
10
10
  import { inspectCapabilityCredentialEnvironment, loadCapabilityCredentialMapForIds, reconcileCapabilityCredentialEnvironment, setCapabilityCredentialValue, } from "./credentials.js";
11
- import { configureExternalSkillProfile, configureTiangongSciCapability, doctorExternalCapabilities, EXTERNAL_SKILL_CONTEXT_PROFILE, EXTERNAL_SKILL_MEDIA_PROFILE, EXTERNAL_SKILL_PROFILE, reconcileSetupManagedCapabilities, } from "./external-skills.js";
11
+ import { configureExternalSkillProfile, configureTiangongDatabaseCapability, doctorExternalCapabilities, EXTERNAL_SKILL_CONTEXT_PROFILE, EXTERNAL_SKILL_MEDIA_PROFILE, EXTERNAL_SKILL_PROFILE, reconcileSetupManagedCapabilities, } from "./external-skills.js";
12
12
  import { appendJournalEvent } from "./journal.js";
13
13
  import { configuredResearchSecrets, isSensitiveEnvironmentName, sanitizeResearchRecord, sanitizeResearchText, } from "./sanitization.js";
14
- import { inspectResearchSetupCatalog, RESEARCH_SETUP_CREDENTIALS, RESEARCH_SETUP_INSTALLER, RESEARCH_SETUP_SETTINGS, RESEARCH_SETUP_SKILLS, resolveSetupSkills, setupSkill, setupSource, setupTargetRoot, } from "./setup-catalog.js";
14
+ import { inspectResearchSetupCatalog, RESEARCH_SETUP_CREDENTIALS, RESEARCH_SETUP_INSTALLER, RESEARCH_SETUP_SETTINGS, RESEARCH_SETUP_SKILLS, resolveSetupSkills, setupSkill, setupSource, setupTargetRoot, verifyResearchSetupRuntimeContract, } from "./setup-catalog.js";
15
15
  import { acquireFileLock, canonicalJson, ensureDirectory, fileSize, hashRegularTree, isObject, pathExists, REGULAR_TREE_HASH_ALGORITHM, readJsonFile, sha256File, sha256Text, workspacePaths, writeJsonAtomic, writeTextAtomic, } from "./storage.js";
16
16
  import { packageVersion } from "./constants.js";
17
17
  import { doctorResearchWorkspace, initializeResearchWorkspace, loadWorkspaceConfig, } from "./workspace.js";
@@ -969,22 +969,24 @@ export async function checkResearchSetupUpdates(workspace, environment = process
969
969
  });
970
970
  }
971
971
  }
972
+ const cliVersionDrift = plan.cli.version === packageVersion()
973
+ ? null
974
+ : { planned: plan.cli.version, active: packageVersion() };
975
+ const updateAvailable = drift.length > 0 || cliVersionDrift !== null;
972
976
  return {
973
977
  schemaVersion: 1,
974
978
  workspace: root,
975
979
  checkedAt: new Date().toISOString(),
976
- updateAvailable: drift.length > 0 || plan.cli.version !== packageVersion(),
977
- cliVersionDrift: plan.cli.version === packageVersion()
978
- ? null
979
- : { planned: plan.cli.version, active: packageVersion() },
980
+ updateAvailable,
981
+ cliVersionDrift,
980
982
  drift,
981
983
  currentInstaller: RESEARCH_SETUP_INSTALLER,
982
984
  catalog,
983
985
  policy: {
984
986
  automaticUpdate: false,
985
987
  floatingUpdate: false,
986
- minimumAction: drift.length
987
- ? "Use the newer CLI to create and review a replacement immutable plan; upgrade never runs a floating skills update."
988
+ minimumAction: updateAvailable
989
+ ? "Use the active exact CLI release to create, review, and apply a replacement immutable plan; upgrade never runs a floating CLI or Skills update."
988
990
  : "No catalog migration is required. Installed tree drift is reported separately by setup status/doctor.",
989
991
  },
990
992
  };
@@ -1719,6 +1721,7 @@ async function ensureSetupSourceCheckout(plan, sourceId, runner, environment) {
1719
1721
  },
1720
1722
  });
1721
1723
  }
1724
+ await verifyResearchSetupRuntimeContract(sourcePath, setupSkill(skill.id));
1722
1725
  }
1723
1726
  return checkout;
1724
1727
  }
@@ -1759,8 +1762,30 @@ async function installSetupSkills(input) {
1759
1762
  }
1760
1763
  async function configureSelectedCapabilities(plan, _environment) {
1761
1764
  const hasBraveProfile = plan.selection.evidenceProfile !== "none";
1762
- const hasTiangongSci = plan.selection.skillIds.includes("tiangong.kb-sci-search");
1763
- const codexRoot = hasBraveProfile || hasTiangongSci ? plannedTargetRoot(plan, "codex") : null;
1765
+ const tiangongDatabases = [
1766
+ {
1767
+ kind: "sci",
1768
+ skillId: "tiangong.kb-sci-search",
1769
+ settingPrefix: "tiangong.sci",
1770
+ catalogId: "first-party.tiangong.kb-sci-search",
1771
+ capabilityId: "database.tiangong.sci-search",
1772
+ },
1773
+ {
1774
+ kind: "report",
1775
+ skillId: "tiangong.kb-report-search",
1776
+ settingPrefix: "tiangong.report",
1777
+ catalogId: "first-party.tiangong.kb-report-search",
1778
+ capabilityId: "database.tiangong.report-search",
1779
+ },
1780
+ {
1781
+ kind: "patent",
1782
+ skillId: "tiangong.kb-patent-search",
1783
+ settingPrefix: "tiangong.patent",
1784
+ catalogId: "first-party.tiangong.kb-patent-search",
1785
+ capabilityId: "database.tiangong.patent-search",
1786
+ },
1787
+ ].filter((entry) => plan.selection.skillIds.includes(entry.skillId));
1788
+ const codexRoot = hasBraveProfile || tiangongDatabases.length ? plannedTargetRoot(plan, "codex") : null;
1764
1789
  if (plan.selection.evidenceProfile !== "none") {
1765
1790
  await configureExternalSkillProfile({
1766
1791
  workspace: plan.workspace.path,
@@ -1768,10 +1793,11 @@ async function configureSelectedCapabilities(plan, _environment) {
1768
1793
  skillRoot: codexRoot,
1769
1794
  });
1770
1795
  }
1771
- if (hasTiangongSci) {
1772
- const skill = setupSkill("tiangong.kb-sci-search");
1796
+ for (const database of tiangongDatabases) {
1797
+ const skill = setupSkill(database.skillId);
1773
1798
  const source = setupSource(skill.sourceId);
1774
- await configureTiangongSciCapability({
1799
+ await configureTiangongDatabaseCapability({
1800
+ kind: database.kind,
1775
1801
  workspace: plan.workspace.path,
1776
1802
  skillPath: join(codexRoot, skill.skillName),
1777
1803
  source: {
@@ -1780,19 +1806,19 @@ async function configureSelectedCapabilities(plan, _environment) {
1780
1806
  immutableRef: source.immutableRef,
1781
1807
  expectedTreeSha256: skill.expectedTreeSha256,
1782
1808
  license: "MIT",
1783
- catalogId: "first-party.tiangong.kb-sci-search",
1809
+ catalogId: database.catalogId,
1784
1810
  },
1785
- endpoint: plan.settings["tiangong.sci.endpoint"],
1786
- ...(plan.settings["tiangong.sci.region"] === undefined
1811
+ endpoint: plan.settings[`${database.settingPrefix}.endpoint`],
1812
+ ...(plan.settings[`${database.settingPrefix}.region`] === undefined
1787
1813
  ? {}
1788
- : { region: plan.settings["tiangong.sci.region"] }),
1814
+ : { region: plan.settings[`${database.settingPrefix}.region`] }),
1789
1815
  });
1790
1816
  }
1791
1817
  await reconcileSetupManagedCapabilities({
1792
1818
  workspace: plan.workspace.path,
1793
1819
  selectedCapabilityIds: [
1794
1820
  ...BRAVE_PROFILE_SKILLS[plan.selection.evidenceProfile].map(setupManagedCapabilityId),
1795
- ...(hasTiangongSci ? ["database.tiangong.sci-search"] : []),
1821
+ ...tiangongDatabases.map((database) => database.capabilityId),
1796
1822
  ],
1797
1823
  });
1798
1824
  }
@@ -1887,6 +1913,14 @@ async function assertRequiredCredentialPreflight(plan, environment) {
1887
1913
  .join(", ")}), then retry this exact step.`,
1888
1914
  retryCommand: `tiangong-ai research setup retry --step credential-preflight --workspace ${plan.workspace.path} --json`,
1889
1915
  exitCode: 3,
1916
+ diagnostics: {
1917
+ executionMode: "setup-preflight",
1918
+ credentialScope: [
1919
+ ...new Set(failures.map((failure) => definitions.find((definition) => definition.id === failure.id).storage)),
1920
+ ].join("+"),
1921
+ networkAttempted: false,
1922
+ missingCredentialIds: failures.map((failure) => failure.id),
1923
+ },
1890
1924
  });
1891
1925
  }
1892
1926
  }
@@ -2885,7 +2919,7 @@ function commandFailure(step, command, result, root, environment) {
2885
2919
  exitCode: 3,
2886
2920
  });
2887
2921
  }
2888
- async function setupDoctorCheck(checks, id, category, callback) {
2922
+ async function setupDoctorCheck(checks, id, category, callback, failureMinimumAction = `Resolve ${id}, then rerun research setup doctor.`) {
2889
2923
  try {
2890
2924
  checks.push({
2891
2925
  id,
@@ -2901,7 +2935,7 @@ async function setupDoctorCheck(checks, id, category, callback) {
2901
2935
  category,
2902
2936
  status: "fail",
2903
2937
  detail: sanitizeResearchText(error instanceof Error ? error.message : String(error)),
2904
- minimumAction: `Resolve ${id}, then rerun research setup doctor.`,
2938
+ minimumAction: failureMinimumAction,
2905
2939
  });
2906
2940
  }
2907
2941
  }
@@ -2955,35 +2989,12 @@ async function appendDependencyChecks(checks, selected, runner, root, environmen
2955
2989
  return `${dependency.requirement} is active.`;
2956
2990
  }
2957
2991
  throw new Error(`No automatic dependency check is declared for ${dependency.id}. ${dependency.minimumAction}`);
2958
- });
2992
+ }, dependency.minimumAction);
2959
2993
  }
2960
2994
  }
2961
2995
  async function appendCompanionLiveChecks(checks, input) {
2962
- void input.sleeper;
2963
2996
  if (input.selected.some((skill) => skill.id === "tiangong.academic-paper-download")) {
2964
- await setupDoctorCheck(checks, "live.semantic-scholar", "live-check", async () => {
2965
- const apiKey = input.adapterCredentials.get("semantic-scholar.api-key");
2966
- const headers = new Headers({ Accept: "application/json" });
2967
- if (apiKey)
2968
- headers.set("x-api-key", apiKey);
2969
- const response = await input.fetcher("https://api.semanticscholar.org/graph/v1/paper/search?query=reproducible%20research&limit=1&fields=paperId", {
2970
- method: "GET",
2971
- headers,
2972
- redirect: "manual",
2973
- signal: AbortSignal.timeout(30_000),
2974
- });
2975
- if (response.status >= 300 && response.status < 400) {
2976
- throw new Error("Semantic Scholar returned a redirect; credential-bearing redirects are not followed.");
2977
- }
2978
- if (!response.ok) {
2979
- const detail = await boundedResponseText(response, 2_000, apiKey ? [apiKey] : []);
2980
- throw new Error(`Semantic Scholar live check returned HTTP ${response.status}${detail ? `: ${detail}` : ""}.`);
2981
- }
2982
- await response.body?.cancel().catch(() => undefined);
2983
- return apiKey
2984
- ? "Semantic Scholar accepted the configured optional API key."
2985
- : "Semantic Scholar public API is reachable without an optional API key.";
2986
- });
2997
+ await appendSemanticScholarLiveCheck(checks, input);
2987
2998
  }
2988
2999
  if (input.selected.some((skill) => skill.id === "tiangong.document-granular-decompose")) {
2989
3000
  if (!input.allowSyntheticUnstructureUpload) {
@@ -3029,6 +3040,116 @@ async function appendCompanionLiveChecks(checks, input) {
3029
3040
  }
3030
3041
  }
3031
3042
  }
3043
+ async function appendSemanticScholarLiveCheck(checks, input) {
3044
+ const apiKey = input.adapterCredentials.get("semantic-scholar.api-key");
3045
+ const headers = new Headers({ Accept: "application/json" });
3046
+ if (apiKey)
3047
+ headers.set("x-api-key", apiKey);
3048
+ const url = "https://api.semanticscholar.org/graph/v1/paper/search?query=reproducible%20research&limit=1&fields=paperId";
3049
+ try {
3050
+ let response;
3051
+ let retryAfterSeconds = null;
3052
+ for (let attempt = 0; attempt < 2; attempt += 1) {
3053
+ response = await input.fetcher(url, {
3054
+ method: "GET",
3055
+ headers,
3056
+ redirect: "manual",
3057
+ signal: AbortSignal.timeout(30_000),
3058
+ });
3059
+ if (response.status !== 429 || attempt === 1)
3060
+ break;
3061
+ retryAfterSeconds = setupRetryAfterSeconds(response.headers.get("retry-after"));
3062
+ await response.body?.cancel().catch(() => undefined);
3063
+ await input.sleeper(Math.min(5, retryAfterSeconds ?? 1) * 1_000);
3064
+ }
3065
+ retryAfterSeconds ??= setupRetryAfterSeconds(response.headers.get("retry-after"));
3066
+ if (response.status >= 300 && response.status < 400) {
3067
+ await response.body?.cancel().catch(() => undefined);
3068
+ checks.push({
3069
+ id: "live.semantic-scholar",
3070
+ category: "live-check",
3071
+ status: "fail",
3072
+ detail: "Semantic Scholar returned a redirect; credential-bearing redirects are not followed.",
3073
+ minimumAction: "Verify the fixed Semantic Scholar endpoint and network policy, then rerun setup doctor; do not fall back to a standalone source wrapper.",
3074
+ diagnostics: {
3075
+ code: "PROVIDER_REDIRECT_REJECTED",
3076
+ executionMode: "setup-doctor",
3077
+ credentialScope: "adapter",
3078
+ networkAttempted: true,
3079
+ httpStatus: response.status,
3080
+ },
3081
+ });
3082
+ return;
3083
+ }
3084
+ if (!response.ok) {
3085
+ const authenticationFailure = response.status === 401 || response.status === 403;
3086
+ const rateLimited = response.status === 429;
3087
+ const detail = authenticationFailure
3088
+ ? ""
3089
+ : await boundedResponseText(response, 2_000, apiKey ? [apiKey] : []);
3090
+ checks.push({
3091
+ id: "live.semantic-scholar",
3092
+ category: "live-check",
3093
+ status: "fail",
3094
+ detail: sanitizeResearchText(`Semantic Scholar live check returned HTTP ${response.status}${detail ? `: ${detail}` : ""}.`, apiKey ? [apiKey] : []),
3095
+ minimumAction: rateLimited
3096
+ ? `Wait for the provider quota window${retryAfterSeconds === null ? "" : ` (Retry-After ${retryAfterSeconds}s)`}, then rerun setup doctor. The production workflow remains blocked and must not downgrade to standalone search.`
3097
+ : authenticationFailure
3098
+ ? "Replace or remove the optional Semantic Scholar API key, verify provider entitlement, and rerun setup doctor; do not expose the key in output."
3099
+ : "Verify Semantic Scholar availability and the fixed API contract, then rerun setup doctor; do not downgrade the research workflow.",
3100
+ diagnostics: {
3101
+ code: authenticationFailure
3102
+ ? "PROVIDER_AUTHENTICATION_FAILED"
3103
+ : rateLimited
3104
+ ? "PROVIDER_RATE_LIMITED"
3105
+ : "PROVIDER_REQUEST_FAILED",
3106
+ executionMode: "setup-doctor",
3107
+ credentialScope: "adapter",
3108
+ networkAttempted: true,
3109
+ httpStatus: response.status,
3110
+ retryAfterSeconds,
3111
+ },
3112
+ });
3113
+ return;
3114
+ }
3115
+ await response.body?.cancel().catch(() => undefined);
3116
+ checks.push({
3117
+ id: "live.semantic-scholar",
3118
+ category: "live-check",
3119
+ status: "pass",
3120
+ detail: apiKey
3121
+ ? "Semantic Scholar accepted the configured optional API key."
3122
+ : "Semantic Scholar public API is reachable without an optional API key.",
3123
+ minimumAction: null,
3124
+ });
3125
+ }
3126
+ catch (error) {
3127
+ checks.push({
3128
+ id: "live.semantic-scholar",
3129
+ category: "live-check",
3130
+ status: "fail",
3131
+ detail: sanitizeResearchText(error instanceof Error ? error.message : String(error), apiKey ? [apiKey] : []),
3132
+ minimumAction: "Restore provider connectivity, then rerun setup doctor; the production workflow must remain blocked instead of using a standalone fallback.",
3133
+ diagnostics: {
3134
+ code: "PROVIDER_TRANSPORT_FAILED",
3135
+ executionMode: "setup-doctor",
3136
+ credentialScope: "adapter",
3137
+ networkAttempted: true,
3138
+ },
3139
+ });
3140
+ }
3141
+ }
3142
+ function setupRetryAfterSeconds(value) {
3143
+ if (!value)
3144
+ return null;
3145
+ const seconds = Number(value);
3146
+ if (Number.isFinite(seconds) && seconds >= 0)
3147
+ return Math.ceil(seconds);
3148
+ const timestamp = Date.parse(value);
3149
+ if (!Number.isFinite(timestamp))
3150
+ return null;
3151
+ return Math.max(0, Math.ceil((timestamp - Date.now()) / 1_000));
3152
+ }
3032
3153
  async function boundedResponseText(response, maximumBytes, secrets) {
3033
3154
  if (!response.body)
3034
3155
  return "";