@tiangong-ai/cli 0.0.13 → 0.0.15

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.
Files changed (63) hide show
  1. package/AGENTS.md +4 -2
  2. package/README.md +59 -2
  3. package/dist/args.d.ts +13 -0
  4. package/dist/args.js +72 -0
  5. package/dist/args.js.map +1 -0
  6. package/dist/cli.d.ts +5 -21
  7. package/dist/cli.js +27 -445
  8. package/dist/cli.js.map +1 -1
  9. package/dist/data.d.ts +3 -0
  10. package/dist/data.js +16 -0
  11. package/dist/data.js.map +1 -0
  12. package/dist/edge-search.d.ts +54 -0
  13. package/dist/edge-search.js +108 -0
  14. package/dist/edge-search.js.map +1 -0
  15. package/dist/education/commands.d.ts +2 -0
  16. package/dist/education/commands.js +144 -0
  17. package/dist/education/commands.js.map +1 -0
  18. package/dist/education/config.d.ts +24 -0
  19. package/dist/education/config.js +127 -0
  20. package/dist/education/config.js.map +1 -0
  21. package/dist/education/types.d.ts +1 -0
  22. package/dist/education/types.js +2 -0
  23. package/dist/education/types.js.map +1 -0
  24. package/dist/env.d.ts +2 -0
  25. package/dist/env.js +42 -0
  26. package/dist/env.js.map +1 -0
  27. package/dist/errors.d.ts +24 -0
  28. package/dist/errors.js +53 -0
  29. package/dist/errors.js.map +1 -0
  30. package/dist/http.d.ts +28 -0
  31. package/dist/http.js +80 -0
  32. package/dist/http.js.map +1 -0
  33. package/dist/io.d.ts +11 -0
  34. package/dist/io.js +33 -0
  35. package/dist/io.js.map +1 -0
  36. package/dist/kb/client.d.ts +19 -0
  37. package/dist/kb/client.js +71 -0
  38. package/dist/kb/client.js.map +1 -0
  39. package/dist/kb/config.d.ts +12 -0
  40. package/dist/kb/config.js +26 -0
  41. package/dist/kb/config.js.map +1 -0
  42. package/dist/kb/pipeline-health.d.ts +10 -0
  43. package/dist/kb/pipeline-health.js +68 -0
  44. package/dist/kb/pipeline-health.js.map +1 -0
  45. package/dist/kb/selector.d.ts +3 -0
  46. package/dist/kb/selector.js +40 -0
  47. package/dist/kb/selector.js.map +1 -0
  48. package/dist/kb/status.d.ts +19 -0
  49. package/dist/kb/status.js +74 -0
  50. package/dist/kb/status.js.map +1 -0
  51. package/dist/research/commands.d.ts +2 -0
  52. package/dist/research/commands.js +147 -0
  53. package/dist/research/commands.js.map +1 -0
  54. package/dist/research/config.d.ts +23 -0
  55. package/dist/research/config.js +120 -0
  56. package/dist/research/config.js.map +1 -0
  57. package/dist/research/types.d.ts +1 -0
  58. package/dist/research/types.js +2 -0
  59. package/dist/research/types.js.map +1 -0
  60. package/dist/strict-args.d.ts +8 -0
  61. package/dist/strict-args.js +63 -0
  62. package/dist/strict-args.js.map +1 -0
  63. package/package.json +2 -2
package/dist/cli.js CHANGED
@@ -1,14 +1,27 @@
1
1
  import { createHash, randomUUID } from "node:crypto";
2
- import { createReadStream, openAsBlob, readFileSync, statSync } from "node:fs";
2
+ import { createReadStream, openAsBlob, statSync } from "node:fs";
3
3
  import { mkdir, readFile, readdir, stat, writeFile } from "node:fs/promises";
4
4
  import { homedir, platform } from "node:os";
5
5
  import { basename, dirname, extname, isAbsolute, join, posix as pathPosix, relative, resolve, sep, } from "node:path";
6
6
  import { setTimeout as sleep } from "node:timers/promises";
7
- import { fileURLToPath } from "node:url";
8
7
  import { deflateRawSync, inflateRawSync } from "node:zlib";
9
8
  import sharp from "sharp";
10
- export const DEFAULT_API_BASE_URL = "https://thuenv.tiangong.world:7300";
11
- export const DEFAULT_API_PATH_PREFIX = "/api/v1/kb";
9
+ import { getBoolean, getNonNegativeInteger, getPositiveInteger, getString, nonNegativeIntegerValue, parseArgs, positiveIntegerValue, positiveNumberValue, } from "./args.js";
10
+ import { isObject, responseData, stringField } from "./data.js";
11
+ import { firstEnv, loadDotenv } from "./env.js";
12
+ import { CliError, HttpError } from "./errors.js";
13
+ import { jsonRequest } from "./http.js";
14
+ import { runEducationCommand } from "./education/commands.js";
15
+ import { collectionKey, collectionPath, listCollections, resolveCollection, resolveSelectorFields, } from "./kb/client.js";
16
+ import { resolveKbConfig as resolveConfig } from "./kb/config.js";
17
+ import { readBulkPipelineHealth } from "./kb/pipeline-health.js";
18
+ import { resolveCollectionSelector } from "./kb/selector.js";
19
+ import { batchDocumentStatuses, getDocumentStatus } from "./kb/status.js";
20
+ import { write } from "./io.js";
21
+ import { runResearchCommand } from "./research/commands.js";
22
+ export { parseArgs } from "./args.js";
23
+ export { DEFAULT_API_BASE_URL, DEFAULT_API_PATH_PREFIX } from "./kb/config.js";
24
+ export { resolveCollectionSelector } from "./kb/selector.js";
12
25
  const DEFAULT_RETRIES = 3;
13
26
  const DEFAULT_BULK_WINDOW_SIZE = 100;
14
27
  const DEFAULT_BULK_TOP_UP_MAX = 50;
@@ -17,11 +30,9 @@ const DEFAULT_BULK_POLL_INTERVAL_SECONDS = 30;
17
30
  const DEFAULT_BULK_MAX_POLLS = 0;
18
31
  const DEFAULT_BULK_MAX_UPLOAD_BYTES = 200 * 1024 * 1024;
19
32
  const DEFAULT_BULK_DERIVED_DIR = ".tiangong-kb-ingest-derived";
20
- const KB_DOCUMENT_STATUS_BATCH_LIMIT = 100;
21
33
  const DOCX_TARGET_IMAGE_DPI = 300;
22
34
  const DOCX_NORMALIZE_MIN_BYTES = 10 * 1024 * 1024;
23
35
  const EMUS_PER_INCH = 914400;
24
- const RETRYABLE_HTTP_STATUSES = new Set([408, 425, 429, 500, 502, 503, 504]);
25
36
  const BULK_FAILED_STATUSES = new Set(["failed", "deleted", "dead", "timeout"]);
26
37
  const BULK_IN_FLIGHT_STATUSES = new Set(["uploaded", "waiting_for_index_flags", "uploading"]);
27
38
  const BULK_SUPPORTED_EXTENSIONS = new Set([
@@ -40,24 +51,6 @@ const BULK_SUPPORTED_EXTENSIONS = new Set([
40
51
  ".xls",
41
52
  ".xlsx",
42
53
  ]);
43
- class CliError extends Error {
44
- exitCode;
45
- constructor(message, exitCode = 1) {
46
- super(message);
47
- this.exitCode = exitCode;
48
- }
49
- }
50
- class HttpError extends CliError {
51
- status;
52
- retryAfterSeconds;
53
- retryable;
54
- constructor(message, status, retryAfterSeconds, retryable) {
55
- super(message);
56
- this.status = status;
57
- this.retryAfterSeconds = retryAfterSeconds;
58
- this.retryable = retryable;
59
- }
60
- }
61
54
  export async function runCli(argv, io) {
62
55
  try {
63
56
  loadDotenv(io.env);
@@ -120,6 +113,12 @@ export async function runCli(argv, io) {
120
113
  write(io.stdout, kbHelp());
121
114
  return 1;
122
115
  }
116
+ if (command === "research") {
117
+ return await runResearchCommand(subcommand ? [subcommand, ...rest] : rest, io);
118
+ }
119
+ if (command === "education") {
120
+ return await runEducationCommand(subcommand ? [subcommand, ...rest] : rest, io);
121
+ }
123
122
  throw new CliError(`Unknown command: ${command}`);
124
123
  }
125
124
  catch (error) {
@@ -130,63 +129,6 @@ export async function runCli(argv, io) {
130
129
  throw error;
131
130
  }
132
131
  }
133
- export function parseArgs(argv) {
134
- const positionals = [];
135
- const flags = new Map();
136
- for (let index = 0; index < argv.length; index += 1) {
137
- const arg = argv[index];
138
- if (!arg)
139
- continue;
140
- if (!arg.startsWith("--")) {
141
- positionals.push(arg);
142
- continue;
143
- }
144
- const withoutPrefix = arg.slice(2);
145
- const equalsIndex = withoutPrefix.indexOf("=");
146
- if (equalsIndex >= 0) {
147
- flags.set(withoutPrefix.slice(0, equalsIndex), withoutPrefix.slice(equalsIndex + 1));
148
- continue;
149
- }
150
- const next = argv[index + 1];
151
- if (next && !next.startsWith("--")) {
152
- flags.set(withoutPrefix, next);
153
- index += 1;
154
- }
155
- else {
156
- flags.set(withoutPrefix, true);
157
- }
158
- }
159
- return { positionals, flags };
160
- }
161
- export function resolveCollectionSelector(args, env) {
162
- const selectors = [];
163
- addSelector(selectors, "primary_collection_id", getString(args, "collection-id"));
164
- addSelector(selectors, "collection_path", getString(args, "collection-path"));
165
- addSelector(selectors, "collection_key", getString(args, "collection-key"));
166
- addSelector(selectors, "collection_name", getString(args, "collection-name"));
167
- if (selectors.length > 1) {
168
- throw new CliError("Provide exactly one collection selector.");
169
- }
170
- if (selectors.length === 1)
171
- return selectors[0];
172
- const envName = firstEnv(env, "TIANGONG_KB_DEFAULT_COLLECTION_NAME");
173
- if (envName)
174
- return { field: "collection_name", value: envName };
175
- const legacyName = firstEnv(env, "TIANGONG_KB_DEFAULT_COLLECTION_ID");
176
- if (legacyName) {
177
- if (isUuid(legacyName)) {
178
- throw new CliError("TIANGONG_KB_DEFAULT_COLLECTION_ID is treated as a collection name. Use --collection-id for UUID uploads.");
179
- }
180
- return { field: "collection_name", value: legacyName };
181
- }
182
- const envPath = firstEnv(env, "TIANGONG_KB_DEFAULT_COLLECTION_PATH");
183
- if (envPath)
184
- return { field: "collection_path", value: envPath };
185
- const envKey = firstEnv(env, "TIANGONG_KB_DEFAULT_COLLECTION_KEY");
186
- if (envKey)
187
- return { field: "collection_key", value: envKey };
188
- throw new CliError("Missing collection selector. Provide --collection-name, --collection-key, --collection-path, --collection-id, or set TIANGONG_KB_DEFAULT_COLLECTION_NAME.");
189
- }
190
132
  async function doctor(argv, io) {
191
133
  const args = parseArgs(argv);
192
134
  const config = resolveConfig(args, io.env, { requireKey: false });
@@ -818,120 +760,6 @@ function judgeBulkStatus(item) {
818
760
  }
819
761
  return { status: "uploaded" };
820
762
  }
821
- async function batchDocumentStatuses(config, documentIds) {
822
- const statuses = [];
823
- for (let index = 0; index < documentIds.length; index += KB_DOCUMENT_STATUS_BATCH_LIMIT) {
824
- const chunk = documentIds.slice(index, index + KB_DOCUMENT_STATUS_BATCH_LIMIT);
825
- try {
826
- const payload = await jsonRequest(config, "documents/status:batch", {
827
- method: "POST",
828
- body: JSON.stringify({ documentIds: chunk }),
829
- });
830
- statuses.push(...batchStatusItems(payload));
831
- }
832
- catch (error) {
833
- if (error instanceof HttpError && error.status && [404, 405, 501].includes(error.status)) {
834
- statuses.push(...(await Promise.all(chunk.map(async (documentId) => statusItemFromPayload(await getDocumentStatus(config, documentId), documentId)))));
835
- continue;
836
- }
837
- throw error;
838
- }
839
- }
840
- return statuses;
841
- }
842
- async function getDocumentStatus(config, documentId) {
843
- return jsonRequest(config, `documents/${encodeURIComponent(documentId)}/status`);
844
- }
845
- function batchStatusItems(payload) {
846
- const data = responseData(payload);
847
- const items = isObject(data) && Array.isArray(data.documents)
848
- ? data.documents
849
- : isObject(data) && Array.isArray(data.results)
850
- ? data.results
851
- : Array.isArray(data)
852
- ? data
853
- : [];
854
- return items.filter(isObject).map((item) => batchStatusItemFromPayload(item));
855
- }
856
- function statusItemFromPayload(payload, fallbackDocumentId = "") {
857
- const data = responseData(payload);
858
- const item = isObject(data) ? data : {};
859
- return {
860
- documentId: stringField(item, "documentId") ?? stringField(item, "document_id") ?? fallbackDocumentId,
861
- status: stringField(item, "status"),
862
- terminal: typeof item.terminal === "boolean" ? item.terminal : undefined,
863
- opensearchIndexed: typeof item.opensearchIndexed === "boolean" ? item.opensearchIndexed : undefined,
864
- pineconeIndexed: typeof item.pineconeIndexed === "boolean" ? item.pineconeIndexed : undefined,
865
- indexRecordCount: typeof item.indexRecordCount === "number" ? item.indexRecordCount : undefined,
866
- lastError: typeof item.lastError === "string" ? item.lastError : undefined,
867
- lastErrorStage: typeof item.lastErrorStage === "string" ? item.lastErrorStage : undefined,
868
- raw: payload,
869
- };
870
- }
871
- async function readBulkPipelineHealth(config, fallbackPollAfterSeconds, selectorFields = {}) {
872
- try {
873
- const payload = await jsonRequest(config, pipelineHealthPath(selectorFields));
874
- return pipelineHealthFromPayload(payload, fallbackPollAfterSeconds);
875
- }
876
- catch (error) {
877
- if (error instanceof HttpError && error.status && [404, 405, 501].includes(error.status)) {
878
- return {
879
- healthy: true,
880
- pressure: "unknown",
881
- recommendedAction: "continue",
882
- recommendedPollAfterSeconds: fallbackPollAfterSeconds,
883
- message: "Pipeline health endpoint is unavailable; continuing without backpressure.",
884
- };
885
- }
886
- if (error instanceof HttpError) {
887
- return {
888
- healthy: false,
889
- pressure: "paused",
890
- recommendedAction: "pause_top_up",
891
- recommendedPollAfterSeconds: error.retryAfterSeconds ?? Math.max(fallbackPollAfterSeconds, 30),
892
- message: error.message,
893
- };
894
- }
895
- return {
896
- healthy: false,
897
- pressure: "paused",
898
- recommendedAction: "pause_top_up",
899
- recommendedPollAfterSeconds: Math.max(fallbackPollAfterSeconds, 30),
900
- message: error instanceof Error ? error.message : String(error),
901
- };
902
- }
903
- }
904
- function pipelineHealthPath(selectorFields) {
905
- const params = new URLSearchParams();
906
- for (const key of ["primary_collection_id", "collection_path", "collection_key"]) {
907
- const value = selectorFields[key];
908
- if (value)
909
- params.set(key, value);
910
- }
911
- const query = params.toString();
912
- return query ? `pipeline/health?${query}` : "pipeline/health";
913
- }
914
- function pipelineHealthFromPayload(payload, fallbackPollAfterSeconds) {
915
- const data = responseData(payload);
916
- if (!isObject(data)) {
917
- throw new CliError("Pipeline health response did not contain an object payload.");
918
- }
919
- const action = stringField(data, "recommendedAction");
920
- if (action !== "continue" && action !== "slow_down" && action !== "pause_top_up") {
921
- throw new CliError("Pipeline health response did not contain a valid recommendedAction.");
922
- }
923
- const pressure = stringField(data, "pressure");
924
- const pollAfter = Number(data.recommendedPollAfterSeconds);
925
- return {
926
- healthy: typeof data.healthy === "boolean" ? data.healthy : action === "continue",
927
- pressure: pressure === "ok" || pressure === "degraded" || pressure === "paused" ? pressure : "unknown",
928
- recommendedAction: action,
929
- recommendedPollAfterSeconds: Number.isFinite(pollAfter) && pollAfter > 0 ? pollAfter : fallbackPollAfterSeconds,
930
- checkedAt: stringField(data, "checkedAt"),
931
- message: stringField(data, "message") ??
932
- (isObject(data.indexPreflight) ? stringField(data.indexPreflight, "message") : undefined),
933
- };
934
- }
935
763
  async function saveBulkPipelineHealth(statePath, health) {
936
764
  const db = await openSqlite(statePath);
937
765
  const now = new Date().toISOString();
@@ -943,25 +771,6 @@ async function saveBulkPipelineHealth(statePath, health) {
943
771
  db.close();
944
772
  }
945
773
  }
946
- function batchStatusItemFromPayload(item) {
947
- const documentId = stringField(item, "documentId") ?? stringField(item, "document_id") ?? "";
948
- if (item.ok === true && isObject(item.status)) {
949
- return statusItemFromPayload(item.status, documentId);
950
- }
951
- if (item.ok === false && isObject(item.error)) {
952
- const error = item.error;
953
- return {
954
- documentId,
955
- itemError: {
956
- code: stringField(error, "code") ?? "STATUS_ITEM_ERROR",
957
- message: stringField(error, "message") ?? "Document status lookup failed.",
958
- retryable: typeof error.retryable === "boolean" ? error.retryable : false,
959
- },
960
- raw: item,
961
- };
962
- }
963
- return statusItemFromPayload(item, documentId);
964
- }
965
774
  async function initializeBulkJob(input) {
966
775
  await mkdir(dirname(input.statePath), { recursive: true });
967
776
  const db = await openSqlite(input.statePath);
@@ -2295,13 +2104,6 @@ async function loadOptionalSchemaSnapshot(args, env) {
2295
2104
  throw error;
2296
2105
  }
2297
2106
  }
2298
- async function resolveCollection(config, selector, options) {
2299
- const params = new URLSearchParams({ action: "upload" });
2300
- params.set(selector.field, selector.value);
2301
- if (options.includeSchema)
2302
- params.set("include_schema", "true");
2303
- return jsonRequest(config, `collections/resolve?${params.toString()}`);
2304
- }
2305
2107
  async function loadMetadataMap(args) {
2306
2108
  const metadataMapFile = getString(args, "metadata-map");
2307
2109
  if (!metadataMapFile) {
@@ -2748,93 +2550,6 @@ async function uploadOne(input) {
2748
2550
  },
2749
2551
  });
2750
2552
  }
2751
- async function resolveSelectorFields(config, selector) {
2752
- if (selector.field !== "collection_name") {
2753
- return { [selector.field]: selector.value };
2754
- }
2755
- const matches = (await listCollections(config, "upload", 100)).filter((item) => item.name === selector.value);
2756
- if (matches.length === 0)
2757
- throw new CliError(`No uploadable collection matched name: ${selector.value}`);
2758
- if (matches.length > 1) {
2759
- const choices = matches.map((item) => item.key ?? item.path ?? item.id ?? "").join(", ");
2760
- throw new CliError(`Collection name is not unique: ${selector.value}. Use --collection-key or --collection-path. Matches: ${choices}`);
2761
- }
2762
- const match = matches[0];
2763
- const key = collectionKey(match);
2764
- if (key)
2765
- return { collection_key: key };
2766
- const path = collectionPath(match);
2767
- if (path)
2768
- return { collection_path: path };
2769
- if (typeof match.id === "string" && match.id)
2770
- return { primary_collection_id: match.id };
2771
- throw new CliError(`Collection matched name ${selector.value}, but response had no key, path, or id.`);
2772
- }
2773
- async function listCollections(config, capability, limit) {
2774
- const collections = [];
2775
- let offset = 0;
2776
- while (true) {
2777
- const payload = await jsonRequest(config, `collections?${new URLSearchParams({
2778
- capability,
2779
- limit: String(limit),
2780
- offset: String(offset),
2781
- }).toString()}`);
2782
- const page = collectionItems(payload);
2783
- collections.push(...page);
2784
- if (page.length < limit)
2785
- return collections;
2786
- offset += limit;
2787
- }
2788
- }
2789
- async function jsonRequest(config, path, init = {}) {
2790
- const url = `${config.apiBaseUrl}${config.apiPathPrefix}/${path.replace(/^\/+/, "")}`;
2791
- const headers = new Headers(init.headers);
2792
- headers.set("Authorization", `Bearer ${config.apiKey}`);
2793
- headers.set("Accept", "application/json");
2794
- if (init.body && !(init.body instanceof FormData)) {
2795
- headers.set("Content-Type", "application/json");
2796
- }
2797
- let response;
2798
- try {
2799
- response = await fetch(url, {
2800
- ...init,
2801
- headers,
2802
- signal: AbortSignal.timeout(config.timeoutSeconds * 1000),
2803
- });
2804
- }
2805
- catch (error) {
2806
- throw new HttpError(`Request failed for ${url}: ${error instanceof Error ? error.message : String(error)}`, undefined, undefined, true);
2807
- }
2808
- const text = await response.text();
2809
- if (!response.ok) {
2810
- throw new HttpError(`HTTP ${response.status} from ${url}: ${text}`, response.status, retryAfterSeconds(response.headers), RETRYABLE_HTTP_STATUSES.has(response.status));
2811
- }
2812
- if (!text.trim())
2813
- return {};
2814
- try {
2815
- return JSON.parse(text);
2816
- }
2817
- catch {
2818
- throw new CliError(`Expected JSON from ${url}, got: ${text.slice(0, 200)}`);
2819
- }
2820
- }
2821
- function resolveConfig(args, env, options = {}) {
2822
- const requireKey = options.requireKey ?? true;
2823
- const apiKey = getString(args, "api-key") ?? firstEnv(env, "TIANGONG_AI_API_KEY", "TIANGONG_KB_API_KEY") ?? "";
2824
- if (requireKey && !apiKey) {
2825
- throw new CliError("Missing API key. Provide --api-key or set TIANGONG_AI_API_KEY.");
2826
- }
2827
- return {
2828
- apiBaseUrl: (getString(args, "api-base-url") ??
2829
- firstEnv(env, "TIANGONG_KB_API_BASE_URL") ??
2830
- DEFAULT_API_BASE_URL).replace(/\/+$/, ""),
2831
- apiPathPrefix: normalizePrefix(getString(args, "api-path-prefix") ??
2832
- firstEnv(env, "TIANGONG_KB_API_PATH_PREFIX") ??
2833
- DEFAULT_API_PATH_PREFIX),
2834
- apiKey,
2835
- timeoutSeconds: positiveNumberValue(getString(args, "timeout") ?? firstEnv(env, "TIANGONG_KB_TIMEOUT"), 120, "--timeout"),
2836
- };
2837
- }
2838
2553
  async function collectFiles(path, recursive, options = {}) {
2839
2554
  const item = await stat(path).catch(() => undefined);
2840
2555
  if (!item)
@@ -2897,36 +2612,6 @@ async function runPool(items, concurrency, worker) {
2897
2612
  await Promise.all(workers);
2898
2613
  return results;
2899
2614
  }
2900
- function collectionItems(payload) {
2901
- const data = responseData(payload);
2902
- if (Array.isArray(data))
2903
- return data.filter(isObject);
2904
- if (isObject(data) && Array.isArray(data.collections))
2905
- return data.collections.filter(isObject);
2906
- if (isObject(data) && Array.isArray(data.data))
2907
- return data.data.filter(isObject);
2908
- if (isObject(payload) && Array.isArray(payload.data))
2909
- return payload.data.filter(isObject);
2910
- throw new CliError("Collection list response did not contain a data array.");
2911
- }
2912
- function collectionKey(item) {
2913
- return (stringField(item, "key") ??
2914
- stringField(item, "collectionKey") ??
2915
- stringField(item, "collection_key"));
2916
- }
2917
- function collectionPath(item) {
2918
- return (stringField(item, "path") ??
2919
- stringField(item, "collectionPath") ??
2920
- stringField(item, "collection_path"));
2921
- }
2922
- function responseData(payload) {
2923
- if (isObject(payload) &&
2924
- "data" in payload &&
2925
- ("api_version" in payload || "request_id" in payload || isObject(payload.data))) {
2926
- return payload.data;
2927
- }
2928
- return payload;
2929
- }
2930
2615
  function documentIdFromUpload(payload) {
2931
2616
  const data = responseData(payload);
2932
2617
  if (!isObject(data))
@@ -2958,70 +2643,6 @@ function formatUploadLine(result) {
2958
2643
  const existing = result.existingDocumentId ? ` existing=${result.existingDocumentId}` : "";
2959
2644
  return `OK ${result.path} document=${result.documentId ?? ""}${duplicate}${existing}\n`;
2960
2645
  }
2961
- function addSelector(selectors, field, value) {
2962
- if (value)
2963
- selectors.push({ field, value });
2964
- }
2965
- function getString(args, name) {
2966
- const value = args.flags.get(name);
2967
- if (typeof value !== "string" || value.trim() === "")
2968
- return undefined;
2969
- return value.trim();
2970
- }
2971
- function getBoolean(args, name) {
2972
- return args.flags.get(name) === true || args.flags.get(name) === "true";
2973
- }
2974
- function getPositiveInteger(args, name, defaultValue) {
2975
- return positiveIntegerValue(getString(args, name), defaultValue, `--${name}`);
2976
- }
2977
- function positiveIntegerValue(value, defaultValue, label) {
2978
- if (!value)
2979
- return defaultValue;
2980
- const parsed = Number(value);
2981
- if (!Number.isInteger(parsed) || parsed <= 0)
2982
- throw new CliError(`${label} must be a positive integer.`);
2983
- return parsed;
2984
- }
2985
- function getNonNegativeInteger(args, name, defaultValue) {
2986
- return nonNegativeIntegerValue(getString(args, name), defaultValue, `--${name}`);
2987
- }
2988
- function nonNegativeIntegerValue(value, defaultValue, label) {
2989
- if (!value)
2990
- return defaultValue;
2991
- const parsed = Number(value);
2992
- if (!Number.isInteger(parsed) || parsed < 0)
2993
- throw new CliError(`${label} must be a non-negative integer.`);
2994
- return parsed;
2995
- }
2996
- function getPositiveNumber(args, name, defaultValue) {
2997
- return positiveNumberValue(getString(args, name), defaultValue, `--${name}`);
2998
- }
2999
- function positiveNumberValue(value, defaultValue, label) {
3000
- if (!value)
3001
- return defaultValue;
3002
- const parsed = Number(value);
3003
- if (!Number.isFinite(parsed) || parsed <= 0)
3004
- throw new CliError(`${label} must be a positive number.`);
3005
- return parsed;
3006
- }
3007
- function firstEnv(env, ...names) {
3008
- for (const name of names) {
3009
- const value = env[name]?.trim();
3010
- if (value)
3011
- return value;
3012
- }
3013
- return undefined;
3014
- }
3015
- function normalizePrefix(value) {
3016
- return `/${value.replace(/^\/+|\/+$/g, "")}`;
3017
- }
3018
- function retryAfterSeconds(headers) {
3019
- const value = headers.get("Retry-After");
3020
- if (!value)
3021
- return undefined;
3022
- const parsed = Number(value);
3023
- return Number.isFinite(parsed) && parsed > 0 ? parsed : undefined;
3024
- }
3025
2646
  function retryDelay(error, attempt) {
3026
2647
  if (error instanceof HttpError && error.retryAfterSeconds)
3027
2648
  return Math.min(error.retryAfterSeconds, 30);
@@ -3047,49 +2668,6 @@ function mimeType(path) {
3047
2668
  }
3048
2669
  return "application/octet-stream";
3049
2670
  }
3050
- function isUuid(value) {
3051
- return /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(value);
3052
- }
3053
- function isObject(value) {
3054
- return Boolean(value) && typeof value === "object" && !Array.isArray(value);
3055
- }
3056
- function stringField(value, field) {
3057
- const candidate = value[field];
3058
- return typeof candidate === "string" && candidate ? candidate : undefined;
3059
- }
3060
- function write(output, text) {
3061
- output.write(text);
3062
- }
3063
- function loadDotenv(env) {
3064
- const cwd = dirname(fileURLToPath(import.meta.url));
3065
- const candidates = [resolve(process.cwd(), ".env"), resolve(cwd, "../.env")];
3066
- for (const candidate of candidates) {
3067
- const content = readEnvFile(candidate);
3068
- if (!content)
3069
- continue;
3070
- for (const line of content.split(/\r?\n/)) {
3071
- const trimmed = line.trim();
3072
- if (!trimmed || trimmed.startsWith("#") || !trimmed.includes("="))
3073
- continue;
3074
- const [rawKey, ...rawValue] = trimmed.replace(/^export\s+/, "").split("=");
3075
- const key = rawKey?.trim();
3076
- if (!key || env[key])
3077
- continue;
3078
- env[key] = rawValue
3079
- .join("=")
3080
- .trim()
3081
- .replace(/^["']|["']$/g, "");
3082
- }
3083
- }
3084
- }
3085
- function readEnvFile(path) {
3086
- try {
3087
- return readFileSync(path, "utf8");
3088
- }
3089
- catch {
3090
- return undefined;
3091
- }
3092
- }
3093
2671
  function topHelp() {
3094
2672
  return `Tiangong AI CLI
3095
2673
 
@@ -3099,8 +2677,12 @@ Usage:
3099
2677
  tiangong-ai kb ingest jobs
3100
2678
  tiangong-ai kb ingest status <document-id>
3101
2679
  tiangong-ai kb collections list [--capability upload]
2680
+ tiangong-ai research search --input <request.json>|--query <query> [--sources default|all|sci|report|patent]
2681
+ tiangong-ai education search --input <request.json>|--query <query> [--sources default|all|course|edu|textbook]
3102
2682
 
3103
2683
  Run "tiangong-ai kb --help" for KB options.
2684
+ Run "tiangong-ai research --help" for research options.
2685
+ Run "tiangong-ai education --help" for education options.
3104
2686
  `;
3105
2687
  }
3106
2688
  function kbHelp() {