cograph 0.1.15 → 0.1.16
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 +92 -8
- package/dist/{chunk-K45ZTU6U.js → chunk-K7B4PPX2.js} +86 -1
- package/dist/chunk-K7B4PPX2.js.map +1 -0
- package/dist/cli.js +31 -8
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +95 -1
- package/dist/index.js +85 -0
- package/dist/index.js.map +1 -1
- package/dist/{shell-H2JCK5AT.js → shell-VPPIRJSS.js} +366 -10
- package/dist/shell-VPPIRJSS.js.map +1 -0
- package/package.json +1 -1
- package/dist/chunk-K45ZTU6U.js.map +0 -1
- package/dist/shell-H2JCK5AT.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -124,6 +124,35 @@ var Client = class {
|
|
|
124
124
|
base() {
|
|
125
125
|
return `${this.baseUrl}/graphs/${this.tenant}`;
|
|
126
126
|
}
|
|
127
|
+
/**
|
|
128
|
+
* Probe the backend to determine reachability and whether endpoints
|
|
129
|
+
* require an X-API-Key header. Used at shell startup to distinguish
|
|
130
|
+
* cloud (auth required) from self-hosted open-access deployments.
|
|
131
|
+
*/
|
|
132
|
+
async healthCheck() {
|
|
133
|
+
const healthUrl = `${this.baseUrl}/health`;
|
|
134
|
+
try {
|
|
135
|
+
const res = await fetch(healthUrl, {
|
|
136
|
+
signal: AbortSignal.timeout(5e3)
|
|
137
|
+
});
|
|
138
|
+
if (!res.ok) return { ok: false, requiresAuth: false, url: this.baseUrl };
|
|
139
|
+
} catch {
|
|
140
|
+
return { ok: false, requiresAuth: false, url: this.baseUrl };
|
|
141
|
+
}
|
|
142
|
+
try {
|
|
143
|
+
const res = await fetch(`${this.base()}/kgs`, {
|
|
144
|
+
headers: { "Content-Type": "application/json" },
|
|
145
|
+
signal: AbortSignal.timeout(5e3)
|
|
146
|
+
});
|
|
147
|
+
return {
|
|
148
|
+
ok: true,
|
|
149
|
+
requiresAuth: res.status === 401,
|
|
150
|
+
url: this.baseUrl
|
|
151
|
+
};
|
|
152
|
+
} catch {
|
|
153
|
+
return { ok: true, requiresAuth: true, url: this.baseUrl };
|
|
154
|
+
}
|
|
155
|
+
}
|
|
127
156
|
async request(method, url, body, timeoutMs = 12e4) {
|
|
128
157
|
const controller = new AbortController();
|
|
129
158
|
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
@@ -340,6 +369,62 @@ var Client = class {
|
|
|
340
369
|
);
|
|
341
370
|
return Array.isArray(data) ? data : [];
|
|
342
371
|
}
|
|
372
|
+
/** Plan + run an enrichment job. Returns immediately with the job id. */
|
|
373
|
+
async enrichRun(req) {
|
|
374
|
+
return this.request(
|
|
375
|
+
"POST",
|
|
376
|
+
`${this.base()}/enrich/jobs`,
|
|
377
|
+
req,
|
|
378
|
+
3e4
|
|
379
|
+
);
|
|
380
|
+
}
|
|
381
|
+
/** List recent enrichment jobs for the current tenant. */
|
|
382
|
+
async enrichJobs() {
|
|
383
|
+
const data = await this.request(
|
|
384
|
+
"GET",
|
|
385
|
+
`${this.base()}/enrich/jobs`,
|
|
386
|
+
void 0,
|
|
387
|
+
15e3
|
|
388
|
+
);
|
|
389
|
+
return Array.isArray(data) ? data : [];
|
|
390
|
+
}
|
|
391
|
+
/** Fetch a single enrichment job (with truncated results). */
|
|
392
|
+
async enrichJob(jobId) {
|
|
393
|
+
return this.request(
|
|
394
|
+
"GET",
|
|
395
|
+
`${this.base()}/enrich/jobs/${encodeURIComponent(jobId)}`,
|
|
396
|
+
void 0,
|
|
397
|
+
15e3
|
|
398
|
+
);
|
|
399
|
+
}
|
|
400
|
+
/** Fetch the conflict review queue for a job. */
|
|
401
|
+
async enrichConflicts(jobId) {
|
|
402
|
+
const data = await this.request(
|
|
403
|
+
"GET",
|
|
404
|
+
`${this.base()}/enrich/jobs/${encodeURIComponent(jobId)}/conflicts`,
|
|
405
|
+
void 0,
|
|
406
|
+
3e4
|
|
407
|
+
);
|
|
408
|
+
return Array.isArray(data) ? data : [];
|
|
409
|
+
}
|
|
410
|
+
/** Apply a set of conflict review decisions to a job. */
|
|
411
|
+
async enrichApply(jobId, decisions) {
|
|
412
|
+
return this.request(
|
|
413
|
+
"POST",
|
|
414
|
+
`${this.base()}/enrich/jobs/${encodeURIComponent(jobId)}/apply`,
|
|
415
|
+
{ decisions },
|
|
416
|
+
6e4
|
|
417
|
+
);
|
|
418
|
+
}
|
|
419
|
+
/** Cancel an enrichment job. */
|
|
420
|
+
async enrichCancel(jobId) {
|
|
421
|
+
await this.request(
|
|
422
|
+
"DELETE",
|
|
423
|
+
`${this.base()}/enrich/jobs/${encodeURIComponent(jobId)}`,
|
|
424
|
+
void 0,
|
|
425
|
+
15e3
|
|
426
|
+
);
|
|
427
|
+
}
|
|
343
428
|
/** Per-type breakdown for one type in one KG: definition + counts + samples.
|
|
344
429
|
*
|
|
345
430
|
* System predicates (rdfs:label, ingested_at, source) are hidden by default
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/client.ts","../src/config.ts"],"sourcesContent":["import { existsSync, readFileSync, statSync } from \"node:fs\";\nimport { extname } from \"node:path\";\nimport { readConfig } from \"./config.js\";\n\nexport class CographError extends Error {\n status?: number;\n body?: string;\n\n constructor(message: string, opts?: { status?: number; body?: string }) {\n super(message);\n this.name = \"CographError\";\n this.status = opts?.status;\n this.body = opts?.body;\n }\n}\n\nexport interface ClientOptions {\n apiKey?: string;\n baseUrl?: string;\n tenant?: string;\n}\n\nexport interface IngestOptions {\n kg?: string;\n contentType?: \"text\" | \"csv\" | \"json\" | string;\n /** Rows per batch for CSV ingest. Default 200. Larger = fewer round-trips\n * but higher per-request memory; 200 is a good balance for typical KGs. */\n batchSize?: number;\n /** Max number of batches in flight at once. Default 4. Higher saturates\n * the backend faster but risks 429s on large ingests. */\n concurrency?: number;\n /** Called after each batch completes during CSV ingest, in batch order.\n * Use for progress UI. Not invoked for text/json ingest. */\n onProgress?: (progress: IngestProgress) => void;\n}\n\nexport interface IngestProgress {\n rowsProcessed: number;\n totalRows: number;\n entitiesResolved: number;\n triplesInserted: number;\n}\n\nexport interface AskOptions {\n kg?: string;\n model?: string;\n}\n\nfunction envVar(name: string, fallback?: string): string | undefined {\n // Prefer COGRAPH_, fall back to OMNIX_ so old configs keep working.\n return (\n process.env[`COGRAPH_${name}`] ||\n process.env[`OMNIX_${name}`] ||\n fallback\n );\n}\n\nconst EXT_FORMAT: Record<string, string> = {\n \".csv\": \"csv\",\n \".json\": \"json\",\n \".jsonl\": \"json\",\n \".txt\": \"text\",\n};\n\n/**\n * Parse a CSV string into an array of row objects.\n *\n * Minimal RFC-4180-ish parser: handles quoted fields with commas, escaped\n * quotes (`\"\"`), CRLF/LF line endings. Does not handle BOM stripping or\n * encoding detection — we assume UTF-8 text in.\n */\nexport function parseCsv(content: string): Record<string, string>[] {\n const rows: string[][] = [];\n let cur: string[] = [];\n let field = \"\";\n let inQuotes = false;\n\n for (let i = 0; i < content.length; i++) {\n const ch = content[i];\n if (inQuotes) {\n if (ch === '\"') {\n if (content[i + 1] === '\"') {\n field += '\"';\n i++;\n } else {\n inQuotes = false;\n }\n } else {\n field += ch;\n }\n } else {\n if (ch === '\"') {\n inQuotes = true;\n } else if (ch === \",\") {\n cur.push(field);\n field = \"\";\n } else if (ch === \"\\n\") {\n cur.push(field);\n rows.push(cur);\n cur = [];\n field = \"\";\n } else if (ch === \"\\r\") {\n // swallow; handled by the following \\n in CRLF, or treat lone \\r as line end\n if (content[i + 1] !== \"\\n\") {\n cur.push(field);\n rows.push(cur);\n cur = [];\n field = \"\";\n }\n } else {\n field += ch;\n }\n }\n }\n // flush trailing field/row\n if (field.length > 0 || cur.length > 0) {\n cur.push(field);\n rows.push(cur);\n }\n\n if (rows.length === 0) return [];\n const headers = rows[0]!.map((h) => h.trim());\n const out: Record<string, string>[] = [];\n for (let r = 1; r < rows.length; r++) {\n const row = rows[r]!;\n // skip blank trailing lines\n if (row.length === 1 && row[0] === \"\") continue;\n const obj: Record<string, string> = {};\n for (let c = 0; c < headers.length; c++) {\n obj[headers[c]!] = row[c] ?? \"\";\n }\n out.push(obj);\n }\n return out;\n}\n\nexport class Client {\n apiKey: string | undefined;\n baseUrl: string;\n tenant: string;\n\n constructor(opts: ClientOptions = {}) {\n // Resolution order for each field: explicit opts → env var → ~/.cograph/config.json\n // (written by `cograph login`) → built-in default. Reading the config eagerly\n // is cheap (small JSON file) and lets users skip env vars entirely after login.\n const cfg = readConfig();\n this.apiKey = opts.apiKey ?? envVar(\"API_KEY\") ?? cfg.apiKey;\n const url =\n opts.baseUrl ?? envVar(\"API_URL\") ?? cfg.apiUrl ?? \"https://api.cograph.cloud\";\n this.baseUrl = url.replace(/\\/+$/, \"\");\n this.tenant = opts.tenant ?? envVar(\"TENANT\") ?? cfg.tenant ?? \"demo-tenant\";\n }\n\n private headers(): Record<string, string> {\n const h: Record<string, string> = { \"Content-Type\": \"application/json\" };\n if (this.apiKey) h[\"X-API-Key\"] = this.apiKey;\n return h;\n }\n\n private base(): string {\n return `${this.baseUrl}/graphs/${this.tenant}`;\n }\n\n private async request<T = unknown>(\n method: string,\n url: string,\n body?: unknown,\n timeoutMs: number = 120_000,\n ): Promise<T> {\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(), timeoutMs);\n let res: Response;\n try {\n res = await fetch(url, {\n method,\n headers: this.headers(),\n body: body === undefined ? undefined : JSON.stringify(body),\n signal: controller.signal,\n });\n } catch (err) {\n clearTimeout(timer);\n if (err instanceof Error && err.name === \"AbortError\") {\n throw new CographError(`Request to ${url} timed out after ${timeoutMs}ms`);\n }\n throw new CographError(\n `Network error contacting ${url}: ${err instanceof Error ? err.message : String(err)}`,\n );\n }\n clearTimeout(timer);\n\n if (!res.ok) {\n let text = \"\";\n try {\n text = await res.text();\n } catch {\n // ignore\n }\n throw new CographError(`HTTP ${res.status}: ${text}`, {\n status: res.status,\n body: text,\n });\n }\n\n // 204 No Content\n if (res.status === 204) return undefined as T;\n\n const ct = res.headers.get(\"content-type\") ?? \"\";\n if (ct.includes(\"application/json\")) {\n return (await res.json()) as T;\n }\n // fall back to text\n const text = await res.text();\n try {\n return JSON.parse(text) as T;\n } catch {\n return text as unknown as T;\n }\n }\n\n /**\n * Ingest a file path or raw text into a knowledge graph.\n *\n * If `pathOrText` points to an existing file, its contents are read and the\n * format is inferred from the extension (.csv, .json, .txt) unless\n * `contentType` is given. CSV files use the two-step schema-inference + row\n * mapping flow.\n */\n async ingest(\n pathOrText: string,\n opts: IngestOptions = {},\n ): Promise<Record<string, unknown>> {\n let content: string;\n let fmt: string;\n\n let isFile = false;\n try {\n isFile = existsSync(pathOrText) && statSync(pathOrText).isFile();\n } catch {\n isFile = false;\n }\n\n if (isFile) {\n const ext = extname(pathOrText).toLowerCase();\n if (ext === \".pdf\") {\n throw new CographError(\n \"PDF ingest not yet supported in the Node CLI; use the Python CLI or POST raw bytes to the API.\",\n );\n }\n content = readFileSync(pathOrText, \"utf-8\");\n fmt = opts.contentType ?? EXT_FORMAT[ext] ?? \"text\";\n if (fmt === \"csv\") {\n return this.ingestCsv(content, opts);\n }\n } else {\n content = pathOrText;\n fmt = opts.contentType ?? \"text\";\n }\n\n const body: Record<string, unknown> = {\n content,\n content_type: fmt,\n source: \"client\",\n };\n if (opts.kg) body.kg_name = opts.kg;\n return this.request(\"POST\", `${this.base()}/ingest`, body, 120_000);\n }\n\n private async ingestCsv(\n content: string,\n opts: IngestOptions,\n ): Promise<Record<string, unknown>> {\n const kgName = opts.kg;\n const batchSize = opts.batchSize ?? 200;\n const concurrency = opts.concurrency ?? 4;\n\n const rows = parseCsv(content);\n if (rows.length === 0) throw new CographError(\"CSV is empty\");\n const headers = Object.keys(rows[0]!);\n\n // Pick the rows with the most non-empty fields for schema inference.\n // Mostly-empty leading rows (e.g. soft-deleted records) otherwise feed\n // the LLM a near-blank sample and reliably produce malformed JSON.\n // Stable on ties — original order preserved within equal scores.\n const sampleRows = rows\n .map((row, idx) => ({\n row,\n idx,\n score: Object.values(row).filter(\n (v) => v != null && String(v).trim() !== \"\",\n ).length,\n }))\n .sort((a, b) => b.score - a.score || a.idx - b.idx)\n .slice(0, 10)\n .map((s) => s.row);\n\n const schemaBody = {\n headers,\n sample_rows: sampleRows,\n total_rows: rows.length,\n };\n const mapping = await this.request<Record<string, unknown>>(\n \"POST\",\n `${this.base()}/ingest/csv/schema`,\n schemaBody,\n 300_000,\n );\n\n // Slice rows into batches up front so we can fire them off in a\n // bounded worker pool. Sequential 50-row batches over 891 rows took\n // ~60s end-to-end (18 round-trips); 200-row batches × 4 in flight\n // brings that to ~5s on the same backend.\n const batches: Array<Record<string, string>[]> = [];\n for (let i = 0; i < rows.length; i += batchSize) {\n batches.push(rows.slice(i, i + batchSize));\n }\n\n let totalEntities = 0;\n let totalTriples = 0;\n let rowsProcessed = 0;\n let nextBatch = 0;\n\n const postBatch = async (batch: Record<string, string>[]) => {\n const body: Record<string, unknown> = {\n mapping,\n rows: batch,\n source: \"client\",\n };\n if (kgName) body.kg_name = kgName;\n const result = await this.request<{\n entities_resolved?: number;\n triples_inserted?: number;\n }>(\"POST\", `${this.base()}/ingest/csv/rows`, body, 300_000);\n return {\n entities: result.entities_resolved ?? 0,\n triples: result.triples_inserted ?? 0,\n size: batch.length,\n };\n };\n\n const worker = async (): Promise<void> => {\n while (true) {\n const idx = nextBatch++;\n if (idx >= batches.length) return;\n const r = await postBatch(batches[idx]!);\n totalEntities += r.entities;\n totalTriples += r.triples;\n rowsProcessed += r.size;\n opts.onProgress?.({\n rowsProcessed,\n totalRows: rows.length,\n entitiesResolved: totalEntities,\n triplesInserted: totalTriples,\n });\n }\n };\n\n const workers: Array<Promise<void>> = [];\n for (let i = 0; i < Math.min(concurrency, batches.length); i++) {\n workers.push(worker());\n }\n await Promise.all(workers);\n\n return {\n entities_resolved: totalEntities,\n triples_inserted: totalTriples,\n mapping,\n };\n }\n\n /** Ask a natural language question and return the parsed response. */\n async ask(\n question: string,\n opts: AskOptions = {},\n ): Promise<Record<string, unknown>> {\n const body: Record<string, unknown> = { question };\n if (opts.kg) body.kg_name = opts.kg;\n if (opts.model) body.model = opts.model;\n return this.request(\"POST\", `${this.base()}/ask`, body, 60_000);\n }\n\n /** List all knowledge graphs for the current tenant. */\n async listKgs(): Promise<Array<Record<string, unknown>>> {\n const data = await this.request<unknown>(\n \"GET\",\n `${this.base()}/kgs`,\n undefined,\n 15_000,\n );\n if (Array.isArray(data)) return data as Array<Record<string, unknown>>;\n if (data && typeof data === \"object\" && \"kgs\" in data) {\n const kgs = (data as { kgs?: unknown }).kgs;\n if (Array.isArray(kgs)) return kgs as Array<Record<string, unknown>>;\n }\n return [];\n }\n\n /** Create a knowledge graph. */\n async createKg(\n name: string,\n description?: string,\n ): Promise<Record<string, unknown>> {\n const body: Record<string, unknown> = { name };\n if (description) body.description = description;\n return this.request(\"POST\", `${this.base()}/kgs`, body, 15_000);\n }\n\n /** Delete a knowledge graph by name. */\n async deleteKg(name: string): Promise<Record<string, unknown>> {\n return this.request(\n \"DELETE\",\n `${this.base()}/kgs/${encodeURIComponent(name)}`,\n undefined,\n 30_000,\n );\n }\n\n /** List ontology types. */\n async ontologyTypes(): Promise<Array<Record<string, unknown>>> {\n const data = await this.request<unknown>(\n \"GET\",\n `${this.base()}/ontology/types`,\n undefined,\n 15_000,\n );\n return Array.isArray(data) ? (data as Array<Record<string, unknown>>) : [];\n }\n\n /** Per-KG type counts: every type with ≥1 instance, sorted desc. */\n async typeCounts(kg: string): Promise<TypeCount[]> {\n const data = await this.request<unknown>(\n \"GET\",\n `${this.base()}/kgs/${encodeURIComponent(kg)}/type-counts`,\n undefined,\n 30_000,\n );\n return Array.isArray(data) ? (data as TypeCount[]) : [];\n }\n\n /** Per-type breakdown for one type in one KG: definition + counts + samples.\n *\n * System predicates (rdfs:label, ingested_at, source) are hidden by default\n * — they're attached to every entity at 100% and drown out the columns the\n * user cares about. Pass `includeSystem: true` to see them. */\n async typeUsage(\n kg: string,\n typeName: string,\n opts: { includeSystem?: boolean } = {},\n ): Promise<TypeUsage> {\n const qs = opts.includeSystem ? \"?include_system=true\" : \"\";\n return this.request<TypeUsage>(\n \"GET\",\n `${this.base()}/kgs/${encodeURIComponent(kg)}/types/${encodeURIComponent(typeName)}/usage${qs}`,\n undefined,\n 30_000,\n );\n }\n}\n\nexport interface TypeCount {\n name: string;\n entity_count: number;\n}\n\nexport interface AttributeUsage {\n name: string;\n datatype: string;\n count: number;\n}\n\nexport interface RelationshipUsage {\n name: string;\n target_type: string | null;\n count: number;\n}\n\nexport interface EntitySample {\n uri: string;\n label: string;\n}\n\nexport interface TypeUsage {\n name: string;\n description: string;\n parent_type: string | null;\n entity_count: number;\n attributes: AttributeUsage[];\n relationships: RelationshipUsage[];\n samples: EntitySample[];\n}\n","import { homedir } from \"node:os\";\nimport { join } from \"node:path\";\nimport { existsSync, mkdirSync, readFileSync, writeFileSync, chmodSync } from \"node:fs\";\n\nexport interface CographConfig {\n apiKey?: string;\n apiUrl?: string;\n tenant?: string;\n email?: string;\n}\n\nfunction configDir(): string {\n return join(homedir(), \".cograph\");\n}\n\nfunction configPath(): string {\n return join(configDir(), \"config.json\");\n}\n\n/**\n * Read `~/.cograph/config.json`. Returns an empty object if the file is\n * missing or unreadable — callers should treat fields as optional.\n */\nexport function readConfig(): CographConfig {\n const path = configPath();\n if (!existsSync(path)) return {};\n try {\n const raw = readFileSync(path, \"utf-8\");\n const parsed = JSON.parse(raw) as unknown;\n if (parsed && typeof parsed === \"object\") {\n return parsed as CographConfig;\n }\n } catch {\n // Corrupt or unreadable; behave as if absent so a fresh login can rewrite.\n }\n return {};\n}\n\n/**\n * Write `~/.cograph/config.json` with `chmod 600`. Creates the directory if\n * needed. Merges with the existing config so callers can update one field\n * without clobbering the others.\n */\nexport function writeConfig(patch: CographConfig): void {\n const dir = configDir();\n if (!existsSync(dir)) {\n mkdirSync(dir, { recursive: true, mode: 0o700 });\n }\n const merged = { ...readConfig(), ...patch };\n const path = configPath();\n writeFileSync(path, JSON.stringify(merged, null, 2) + \"\\n\", \"utf-8\");\n try {\n chmodSync(path, 0o600);\n } catch {\n // best-effort; some filesystems (e.g., FAT) don't honor chmod\n }\n}\n\nexport function configPathForDisplay(): string {\n return configPath();\n}\n"],"mappings":";AAAA,SAAS,cAAAA,aAAY,gBAAAC,eAAc,gBAAgB;AACnD,SAAS,eAAe;;;ACDxB,SAAS,eAAe;AACxB,SAAS,YAAY;AACrB,SAAS,YAAY,WAAW,cAAc,eAAe,iBAAiB;AAS9E,SAAS,YAAoB;AAC3B,SAAO,KAAK,QAAQ,GAAG,UAAU;AACnC;AAEA,SAAS,aAAqB;AAC5B,SAAO,KAAK,UAAU,GAAG,aAAa;AACxC;AAMO,SAAS,aAA4B;AAC1C,QAAM,OAAO,WAAW;AACxB,MAAI,CAAC,WAAW,IAAI,EAAG,QAAO,CAAC;AAC/B,MAAI;AACF,UAAM,MAAM,aAAa,MAAM,OAAO;AACtC,UAAM,SAAS,KAAK,MAAM,GAAG;AAC7B,QAAI,UAAU,OAAO,WAAW,UAAU;AACxC,aAAO;AAAA,IACT;AAAA,EACF,QAAQ;AAAA,EAER;AACA,SAAO,CAAC;AACV;;;ADhCO,IAAM,eAAN,cAA2B,MAAM;AAAA,EACtC;AAAA,EACA;AAAA,EAEA,YAAY,SAAiB,MAA2C;AACtE,UAAM,OAAO;AACb,SAAK,OAAO;AACZ,SAAK,SAAS,MAAM;AACpB,SAAK,OAAO,MAAM;AAAA,EACpB;AACF;AAkCA,SAAS,OAAO,MAAc,UAAuC;AAEnE,SACE,QAAQ,IAAI,WAAW,IAAI,EAAE,KAC7B,QAAQ,IAAI,SAAS,IAAI,EAAE,KAC3B;AAEJ;AAEA,IAAM,aAAqC;AAAA,EACzC,QAAQ;AAAA,EACR,SAAS;AAAA,EACT,UAAU;AAAA,EACV,QAAQ;AACV;AASO,SAAS,SAAS,SAA2C;AAClE,QAAM,OAAmB,CAAC;AAC1B,MAAI,MAAgB,CAAC;AACrB,MAAI,QAAQ;AACZ,MAAI,WAAW;AAEf,WAAS,IAAI,GAAG,IAAI,QAAQ,QAAQ,KAAK;AACvC,UAAM,KAAK,QAAQ,CAAC;AACpB,QAAI,UAAU;AACZ,UAAI,OAAO,KAAK;AACd,YAAI,QAAQ,IAAI,CAAC,MAAM,KAAK;AAC1B,mBAAS;AACT;AAAA,QACF,OAAO;AACL,qBAAW;AAAA,QACb;AAAA,MACF,OAAO;AACL,iBAAS;AAAA,MACX;AAAA,IACF,OAAO;AACL,UAAI,OAAO,KAAK;AACd,mBAAW;AAAA,MACb,WAAW,OAAO,KAAK;AACrB,YAAI,KAAK,KAAK;AACd,gBAAQ;AAAA,MACV,WAAW,OAAO,MAAM;AACtB,YAAI,KAAK,KAAK;AACd,aAAK,KAAK,GAAG;AACb,cAAM,CAAC;AACP,gBAAQ;AAAA,MACV,WAAW,OAAO,MAAM;AAEtB,YAAI,QAAQ,IAAI,CAAC,MAAM,MAAM;AAC3B,cAAI,KAAK,KAAK;AACd,eAAK,KAAK,GAAG;AACb,gBAAM,CAAC;AACP,kBAAQ;AAAA,QACV;AAAA,MACF,OAAO;AACL,iBAAS;AAAA,MACX;AAAA,IACF;AAAA,EACF;AAEA,MAAI,MAAM,SAAS,KAAK,IAAI,SAAS,GAAG;AACtC,QAAI,KAAK,KAAK;AACd,SAAK,KAAK,GAAG;AAAA,EACf;AAEA,MAAI,KAAK,WAAW,EAAG,QAAO,CAAC;AAC/B,QAAM,UAAU,KAAK,CAAC,EAAG,IAAI,CAAC,MAAM,EAAE,KAAK,CAAC;AAC5C,QAAM,MAAgC,CAAC;AACvC,WAAS,IAAI,GAAG,IAAI,KAAK,QAAQ,KAAK;AACpC,UAAM,MAAM,KAAK,CAAC;AAElB,QAAI,IAAI,WAAW,KAAK,IAAI,CAAC,MAAM,GAAI;AACvC,UAAM,MAA8B,CAAC;AACrC,aAAS,IAAI,GAAG,IAAI,QAAQ,QAAQ,KAAK;AACvC,UAAI,QAAQ,CAAC,CAAE,IAAI,IAAI,CAAC,KAAK;AAAA,IAC/B;AACA,QAAI,KAAK,GAAG;AAAA,EACd;AACA,SAAO;AACT;AAEO,IAAM,SAAN,MAAa;AAAA,EAClB;AAAA,EACA;AAAA,EACA;AAAA,EAEA,YAAY,OAAsB,CAAC,GAAG;AAIpC,UAAM,MAAM,WAAW;AACvB,SAAK,SAAS,KAAK,UAAU,OAAO,SAAS,KAAK,IAAI;AACtD,UAAM,MACJ,KAAK,WAAW,OAAO,SAAS,KAAK,IAAI,UAAU;AACrD,SAAK,UAAU,IAAI,QAAQ,QAAQ,EAAE;AACrC,SAAK,SAAS,KAAK,UAAU,OAAO,QAAQ,KAAK,IAAI,UAAU;AAAA,EACjE;AAAA,EAEQ,UAAkC;AACxC,UAAM,IAA4B,EAAE,gBAAgB,mBAAmB;AACvE,QAAI,KAAK,OAAQ,GAAE,WAAW,IAAI,KAAK;AACvC,WAAO;AAAA,EACT;AAAA,EAEQ,OAAe;AACrB,WAAO,GAAG,KAAK,OAAO,WAAW,KAAK,MAAM;AAAA,EAC9C;AAAA,EAEA,MAAc,QACZ,QACA,KACA,MACA,YAAoB,MACR;AACZ,UAAM,aAAa,IAAI,gBAAgB;AACvC,UAAM,QAAQ,WAAW,MAAM,WAAW,MAAM,GAAG,SAAS;AAC5D,QAAI;AACJ,QAAI;AACF,YAAM,MAAM,MAAM,KAAK;AAAA,QACrB;AAAA,QACA,SAAS,KAAK,QAAQ;AAAA,QACtB,MAAM,SAAS,SAAY,SAAY,KAAK,UAAU,IAAI;AAAA,QAC1D,QAAQ,WAAW;AAAA,MACrB,CAAC;AAAA,IACH,SAAS,KAAK;AACZ,mBAAa,KAAK;AAClB,UAAI,eAAe,SAAS,IAAI,SAAS,cAAc;AACrD,cAAM,IAAI,aAAa,cAAc,GAAG,oBAAoB,SAAS,IAAI;AAAA,MAC3E;AACA,YAAM,IAAI;AAAA,QACR,4BAA4B,GAAG,KAAK,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG,CAAC;AAAA,MACtF;AAAA,IACF;AACA,iBAAa,KAAK;AAElB,QAAI,CAAC,IAAI,IAAI;AACX,UAAIC,QAAO;AACX,UAAI;AACF,QAAAA,QAAO,MAAM,IAAI,KAAK;AAAA,MACxB,QAAQ;AAAA,MAER;AACA,YAAM,IAAI,aAAa,QAAQ,IAAI,MAAM,KAAKA,KAAI,IAAI;AAAA,QACpD,QAAQ,IAAI;AAAA,QACZ,MAAMA;AAAA,MACR,CAAC;AAAA,IACH;AAGA,QAAI,IAAI,WAAW,IAAK,QAAO;AAE/B,UAAM,KAAK,IAAI,QAAQ,IAAI,cAAc,KAAK;AAC9C,QAAI,GAAG,SAAS,kBAAkB,GAAG;AACnC,aAAQ,MAAM,IAAI,KAAK;AAAA,IACzB;AAEA,UAAM,OAAO,MAAM,IAAI,KAAK;AAC5B,QAAI;AACF,aAAO,KAAK,MAAM,IAAI;AAAA,IACxB,QAAQ;AACN,aAAO;AAAA,IACT;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAUA,MAAM,OACJ,YACA,OAAsB,CAAC,GACW;AAClC,QAAI;AACJ,QAAI;AAEJ,QAAI,SAAS;AACb,QAAI;AACF,eAASC,YAAW,UAAU,KAAK,SAAS,UAAU,EAAE,OAAO;AAAA,IACjE,QAAQ;AACN,eAAS;AAAA,IACX;AAEA,QAAI,QAAQ;AACV,YAAM,MAAM,QAAQ,UAAU,EAAE,YAAY;AAC5C,UAAI,QAAQ,QAAQ;AAClB,cAAM,IAAI;AAAA,UACR;AAAA,QACF;AAAA,MACF;AACA,gBAAUC,cAAa,YAAY,OAAO;AAC1C,YAAM,KAAK,eAAe,WAAW,GAAG,KAAK;AAC7C,UAAI,QAAQ,OAAO;AACjB,eAAO,KAAK,UAAU,SAAS,IAAI;AAAA,MACrC;AAAA,IACF,OAAO;AACL,gBAAU;AACV,YAAM,KAAK,eAAe;AAAA,IAC5B;AAEA,UAAM,OAAgC;AAAA,MACpC;AAAA,MACA,cAAc;AAAA,MACd,QAAQ;AAAA,IACV;AACA,QAAI,KAAK,GAAI,MAAK,UAAU,KAAK;AACjC,WAAO,KAAK,QAAQ,QAAQ,GAAG,KAAK,KAAK,CAAC,WAAW,MAAM,IAAO;AAAA,EACpE;AAAA,EAEA,MAAc,UACZ,SACA,MACkC;AAClC,UAAM,SAAS,KAAK;AACpB,UAAM,YAAY,KAAK,aAAa;AACpC,UAAM,cAAc,KAAK,eAAe;AAExC,UAAM,OAAO,SAAS,OAAO;AAC7B,QAAI,KAAK,WAAW,EAAG,OAAM,IAAI,aAAa,cAAc;AAC5D,UAAM,UAAU,OAAO,KAAK,KAAK,CAAC,CAAE;AAMpC,UAAM,aAAa,KAChB,IAAI,CAAC,KAAK,SAAS;AAAA,MAClB;AAAA,MACA;AAAA,MACA,OAAO,OAAO,OAAO,GAAG,EAAE;AAAA,QACxB,CAAC,MAAM,KAAK,QAAQ,OAAO,CAAC,EAAE,KAAK,MAAM;AAAA,MAC3C,EAAE;AAAA,IACJ,EAAE,EACD,KAAK,CAAC,GAAG,MAAM,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,EAAE,GAAG,EACjD,MAAM,GAAG,EAAE,EACX,IAAI,CAAC,MAAM,EAAE,GAAG;AAEnB,UAAM,aAAa;AAAA,MACjB;AAAA,MACA,aAAa;AAAA,MACb,YAAY,KAAK;AAAA,IACnB;AACA,UAAM,UAAU,MAAM,KAAK;AAAA,MACzB;AAAA,MACA,GAAG,KAAK,KAAK,CAAC;AAAA,MACd;AAAA,MACA;AAAA,IACF;AAMA,UAAM,UAA2C,CAAC;AAClD,aAAS,IAAI,GAAG,IAAI,KAAK,QAAQ,KAAK,WAAW;AAC/C,cAAQ,KAAK,KAAK,MAAM,GAAG,IAAI,SAAS,CAAC;AAAA,IAC3C;AAEA,QAAI,gBAAgB;AACpB,QAAI,eAAe;AACnB,QAAI,gBAAgB;AACpB,QAAI,YAAY;AAEhB,UAAM,YAAY,OAAO,UAAoC;AAC3D,YAAM,OAAgC;AAAA,QACpC;AAAA,QACA,MAAM;AAAA,QACN,QAAQ;AAAA,MACV;AACA,UAAI,OAAQ,MAAK,UAAU;AAC3B,YAAM,SAAS,MAAM,KAAK,QAGvB,QAAQ,GAAG,KAAK,KAAK,CAAC,oBAAoB,MAAM,GAAO;AAC1D,aAAO;AAAA,QACL,UAAU,OAAO,qBAAqB;AAAA,QACtC,SAAS,OAAO,oBAAoB;AAAA,QACpC,MAAM,MAAM;AAAA,MACd;AAAA,IACF;AAEA,UAAM,SAAS,YAA2B;AACxC,aAAO,MAAM;AACX,cAAM,MAAM;AACZ,YAAI,OAAO,QAAQ,OAAQ;AAC3B,cAAM,IAAI,MAAM,UAAU,QAAQ,GAAG,CAAE;AACvC,yBAAiB,EAAE;AACnB,wBAAgB,EAAE;AAClB,yBAAiB,EAAE;AACnB,aAAK,aAAa;AAAA,UAChB;AAAA,UACA,WAAW,KAAK;AAAA,UAChB,kBAAkB;AAAA,UAClB,iBAAiB;AAAA,QACnB,CAAC;AAAA,MACH;AAAA,IACF;AAEA,UAAM,UAAgC,CAAC;AACvC,aAAS,IAAI,GAAG,IAAI,KAAK,IAAI,aAAa,QAAQ,MAAM,GAAG,KAAK;AAC9D,cAAQ,KAAK,OAAO,CAAC;AAAA,IACvB;AACA,UAAM,QAAQ,IAAI,OAAO;AAEzB,WAAO;AAAA,MACL,mBAAmB;AAAA,MACnB,kBAAkB;AAAA,MAClB;AAAA,IACF;AAAA,EACF;AAAA;AAAA,EAGA,MAAM,IACJ,UACA,OAAmB,CAAC,GACc;AAClC,UAAM,OAAgC,EAAE,SAAS;AACjD,QAAI,KAAK,GAAI,MAAK,UAAU,KAAK;AACjC,QAAI,KAAK,MAAO,MAAK,QAAQ,KAAK;AAClC,WAAO,KAAK,QAAQ,QAAQ,GAAG,KAAK,KAAK,CAAC,QAAQ,MAAM,GAAM;AAAA,EAChE;AAAA;AAAA,EAGA,MAAM,UAAmD;AACvD,UAAM,OAAO,MAAM,KAAK;AAAA,MACtB;AAAA,MACA,GAAG,KAAK,KAAK,CAAC;AAAA,MACd;AAAA,MACA;AAAA,IACF;AACA,QAAI,MAAM,QAAQ,IAAI,EAAG,QAAO;AAChC,QAAI,QAAQ,OAAO,SAAS,YAAY,SAAS,MAAM;AACrD,YAAM,MAAO,KAA2B;AACxC,UAAI,MAAM,QAAQ,GAAG,EAAG,QAAO;AAAA,IACjC;AACA,WAAO,CAAC;AAAA,EACV;AAAA;AAAA,EAGA,MAAM,SACJ,MACA,aACkC;AAClC,UAAM,OAAgC,EAAE,KAAK;AAC7C,QAAI,YAAa,MAAK,cAAc;AACpC,WAAO,KAAK,QAAQ,QAAQ,GAAG,KAAK,KAAK,CAAC,QAAQ,MAAM,IAAM;AAAA,EAChE;AAAA;AAAA,EAGA,MAAM,SAAS,MAAgD;AAC7D,WAAO,KAAK;AAAA,MACV;AAAA,MACA,GAAG,KAAK,KAAK,CAAC,QAAQ,mBAAmB,IAAI,CAAC;AAAA,MAC9C;AAAA,MACA;AAAA,IACF;AAAA,EACF;AAAA;AAAA,EAGA,MAAM,gBAAyD;AAC7D,UAAM,OAAO,MAAM,KAAK;AAAA,MACtB;AAAA,MACA,GAAG,KAAK,KAAK,CAAC;AAAA,MACd;AAAA,MACA;AAAA,IACF;AACA,WAAO,MAAM,QAAQ,IAAI,IAAK,OAA0C,CAAC;AAAA,EAC3E;AAAA;AAAA,EAGA,MAAM,WAAW,IAAkC;AACjD,UAAM,OAAO,MAAM,KAAK;AAAA,MACtB;AAAA,MACA,GAAG,KAAK,KAAK,CAAC,QAAQ,mBAAmB,EAAE,CAAC;AAAA,MAC5C;AAAA,MACA;AAAA,IACF;AACA,WAAO,MAAM,QAAQ,IAAI,IAAK,OAAuB,CAAC;AAAA,EACxD;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,MAAM,UACJ,IACA,UACA,OAAoC,CAAC,GACjB;AACpB,UAAM,KAAK,KAAK,gBAAgB,yBAAyB;AACzD,WAAO,KAAK;AAAA,MACV;AAAA,MACA,GAAG,KAAK,KAAK,CAAC,QAAQ,mBAAmB,EAAE,CAAC,UAAU,mBAAmB,QAAQ,CAAC,SAAS,EAAE;AAAA,MAC7F;AAAA,MACA;AAAA,IACF;AAAA,EACF;AACF;","names":["existsSync","readFileSync","text","existsSync","readFileSync"]}
|
|
1
|
+
{"version":3,"sources":["../src/client.ts","../src/config.ts"],"sourcesContent":["import { existsSync, readFileSync, statSync } from \"node:fs\";\nimport { extname } from \"node:path\";\nimport { readConfig } from \"./config.js\";\n\nexport class CographError extends Error {\n status?: number;\n body?: string;\n\n constructor(message: string, opts?: { status?: number; body?: string }) {\n super(message);\n this.name = \"CographError\";\n this.status = opts?.status;\n this.body = opts?.body;\n }\n}\n\nexport interface ClientOptions {\n apiKey?: string;\n baseUrl?: string;\n tenant?: string;\n}\n\nexport interface IngestOptions {\n kg?: string;\n contentType?: \"text\" | \"csv\" | \"json\" | string;\n /** Rows per batch for CSV ingest. Default 200. Larger = fewer round-trips\n * but higher per-request memory; 200 is a good balance for typical KGs. */\n batchSize?: number;\n /** Max number of batches in flight at once. Default 4. Higher saturates\n * the backend faster but risks 429s on large ingests. */\n concurrency?: number;\n /** Called after each batch completes during CSV ingest, in batch order.\n * Use for progress UI. Not invoked for text/json ingest. */\n onProgress?: (progress: IngestProgress) => void;\n}\n\nexport interface IngestProgress {\n rowsProcessed: number;\n totalRows: number;\n entitiesResolved: number;\n triplesInserted: number;\n}\n\nexport interface AskOptions {\n kg?: string;\n model?: string;\n}\n\nfunction envVar(name: string, fallback?: string): string | undefined {\n // Prefer COGRAPH_, fall back to OMNIX_ so old configs keep working.\n return (\n process.env[`COGRAPH_${name}`] ||\n process.env[`OMNIX_${name}`] ||\n fallback\n );\n}\n\nconst EXT_FORMAT: Record<string, string> = {\n \".csv\": \"csv\",\n \".json\": \"json\",\n \".jsonl\": \"json\",\n \".txt\": \"text\",\n};\n\n/**\n * Parse a CSV string into an array of row objects.\n *\n * Minimal RFC-4180-ish parser: handles quoted fields with commas, escaped\n * quotes (`\"\"`), CRLF/LF line endings. Does not handle BOM stripping or\n * encoding detection — we assume UTF-8 text in.\n */\nexport function parseCsv(content: string): Record<string, string>[] {\n const rows: string[][] = [];\n let cur: string[] = [];\n let field = \"\";\n let inQuotes = false;\n\n for (let i = 0; i < content.length; i++) {\n const ch = content[i];\n if (inQuotes) {\n if (ch === '\"') {\n if (content[i + 1] === '\"') {\n field += '\"';\n i++;\n } else {\n inQuotes = false;\n }\n } else {\n field += ch;\n }\n } else {\n if (ch === '\"') {\n inQuotes = true;\n } else if (ch === \",\") {\n cur.push(field);\n field = \"\";\n } else if (ch === \"\\n\") {\n cur.push(field);\n rows.push(cur);\n cur = [];\n field = \"\";\n } else if (ch === \"\\r\") {\n // swallow; handled by the following \\n in CRLF, or treat lone \\r as line end\n if (content[i + 1] !== \"\\n\") {\n cur.push(field);\n rows.push(cur);\n cur = [];\n field = \"\";\n }\n } else {\n field += ch;\n }\n }\n }\n // flush trailing field/row\n if (field.length > 0 || cur.length > 0) {\n cur.push(field);\n rows.push(cur);\n }\n\n if (rows.length === 0) return [];\n const headers = rows[0]!.map((h) => h.trim());\n const out: Record<string, string>[] = [];\n for (let r = 1; r < rows.length; r++) {\n const row = rows[r]!;\n // skip blank trailing lines\n if (row.length === 1 && row[0] === \"\") continue;\n const obj: Record<string, string> = {};\n for (let c = 0; c < headers.length; c++) {\n obj[headers[c]!] = row[c] ?? \"\";\n }\n out.push(obj);\n }\n return out;\n}\n\nexport class Client {\n apiKey: string | undefined;\n baseUrl: string;\n tenant: string;\n\n constructor(opts: ClientOptions = {}) {\n // Resolution order for each field: explicit opts → env var → ~/.cograph/config.json\n // (written by `cograph login`) → built-in default. Reading the config eagerly\n // is cheap (small JSON file) and lets users skip env vars entirely after login.\n const cfg = readConfig();\n this.apiKey = opts.apiKey ?? envVar(\"API_KEY\") ?? cfg.apiKey;\n const url =\n opts.baseUrl ?? envVar(\"API_URL\") ?? cfg.apiUrl ?? \"https://api.cograph.cloud\";\n this.baseUrl = url.replace(/\\/+$/, \"\");\n this.tenant = opts.tenant ?? envVar(\"TENANT\") ?? cfg.tenant ?? \"demo-tenant\";\n }\n\n private headers(): Record<string, string> {\n const h: Record<string, string> = { \"Content-Type\": \"application/json\" };\n if (this.apiKey) h[\"X-API-Key\"] = this.apiKey;\n return h;\n }\n\n private base(): string {\n return `${this.baseUrl}/graphs/${this.tenant}`;\n }\n\n /**\n * Probe the backend to determine reachability and whether endpoints\n * require an X-API-Key header. Used at shell startup to distinguish\n * cloud (auth required) from self-hosted open-access deployments.\n */\n async healthCheck(): Promise<{\n ok: boolean;\n requiresAuth: boolean;\n url: string;\n }> {\n const healthUrl = `${this.baseUrl}/health`;\n try {\n const res = await fetch(healthUrl, {\n signal: AbortSignal.timeout(5000),\n });\n if (!res.ok) return { ok: false, requiresAuth: false, url: this.baseUrl };\n } catch {\n return { ok: false, requiresAuth: false, url: this.baseUrl };\n }\n // Probe whether endpoints require auth by hitting /kgs without X-API-Key.\n // 401 = requires auth; 200/empty = open access; anything else = treat as\n // auth-required to be safe.\n try {\n const res = await fetch(`${this.base()}/kgs`, {\n headers: { \"Content-Type\": \"application/json\" },\n signal: AbortSignal.timeout(5000),\n });\n return {\n ok: true,\n requiresAuth: res.status === 401,\n url: this.baseUrl,\n };\n } catch {\n return { ok: true, requiresAuth: true, url: this.baseUrl };\n }\n }\n\n private async request<T = unknown>(\n method: string,\n url: string,\n body?: unknown,\n timeoutMs: number = 120_000,\n ): Promise<T> {\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(), timeoutMs);\n let res: Response;\n try {\n res = await fetch(url, {\n method,\n headers: this.headers(),\n body: body === undefined ? undefined : JSON.stringify(body),\n signal: controller.signal,\n });\n } catch (err) {\n clearTimeout(timer);\n if (err instanceof Error && err.name === \"AbortError\") {\n throw new CographError(`Request to ${url} timed out after ${timeoutMs}ms`);\n }\n throw new CographError(\n `Network error contacting ${url}: ${err instanceof Error ? err.message : String(err)}`,\n );\n }\n clearTimeout(timer);\n\n if (!res.ok) {\n let text = \"\";\n try {\n text = await res.text();\n } catch {\n // ignore\n }\n throw new CographError(`HTTP ${res.status}: ${text}`, {\n status: res.status,\n body: text,\n });\n }\n\n // 204 No Content\n if (res.status === 204) return undefined as T;\n\n const ct = res.headers.get(\"content-type\") ?? \"\";\n if (ct.includes(\"application/json\")) {\n return (await res.json()) as T;\n }\n // fall back to text\n const text = await res.text();\n try {\n return JSON.parse(text) as T;\n } catch {\n return text as unknown as T;\n }\n }\n\n /**\n * Ingest a file path or raw text into a knowledge graph.\n *\n * If `pathOrText` points to an existing file, its contents are read and the\n * format is inferred from the extension (.csv, .json, .txt) unless\n * `contentType` is given. CSV files use the two-step schema-inference + row\n * mapping flow.\n */\n async ingest(\n pathOrText: string,\n opts: IngestOptions = {},\n ): Promise<Record<string, unknown>> {\n let content: string;\n let fmt: string;\n\n let isFile = false;\n try {\n isFile = existsSync(pathOrText) && statSync(pathOrText).isFile();\n } catch {\n isFile = false;\n }\n\n if (isFile) {\n const ext = extname(pathOrText).toLowerCase();\n if (ext === \".pdf\") {\n throw new CographError(\n \"PDF ingest not yet supported in the Node CLI; use the Python CLI or POST raw bytes to the API.\",\n );\n }\n content = readFileSync(pathOrText, \"utf-8\");\n fmt = opts.contentType ?? EXT_FORMAT[ext] ?? \"text\";\n if (fmt === \"csv\") {\n return this.ingestCsv(content, opts);\n }\n } else {\n content = pathOrText;\n fmt = opts.contentType ?? \"text\";\n }\n\n const body: Record<string, unknown> = {\n content,\n content_type: fmt,\n source: \"client\",\n };\n if (opts.kg) body.kg_name = opts.kg;\n return this.request(\"POST\", `${this.base()}/ingest`, body, 120_000);\n }\n\n private async ingestCsv(\n content: string,\n opts: IngestOptions,\n ): Promise<Record<string, unknown>> {\n const kgName = opts.kg;\n const batchSize = opts.batchSize ?? 200;\n const concurrency = opts.concurrency ?? 4;\n\n const rows = parseCsv(content);\n if (rows.length === 0) throw new CographError(\"CSV is empty\");\n const headers = Object.keys(rows[0]!);\n\n // Pick the rows with the most non-empty fields for schema inference.\n // Mostly-empty leading rows (e.g. soft-deleted records) otherwise feed\n // the LLM a near-blank sample and reliably produce malformed JSON.\n // Stable on ties — original order preserved within equal scores.\n const sampleRows = rows\n .map((row, idx) => ({\n row,\n idx,\n score: Object.values(row).filter(\n (v) => v != null && String(v).trim() !== \"\",\n ).length,\n }))\n .sort((a, b) => b.score - a.score || a.idx - b.idx)\n .slice(0, 10)\n .map((s) => s.row);\n\n const schemaBody = {\n headers,\n sample_rows: sampleRows,\n total_rows: rows.length,\n };\n const mapping = await this.request<Record<string, unknown>>(\n \"POST\",\n `${this.base()}/ingest/csv/schema`,\n schemaBody,\n 300_000,\n );\n\n // Slice rows into batches up front so we can fire them off in a\n // bounded worker pool. Sequential 50-row batches over 891 rows took\n // ~60s end-to-end (18 round-trips); 200-row batches × 4 in flight\n // brings that to ~5s on the same backend.\n const batches: Array<Record<string, string>[]> = [];\n for (let i = 0; i < rows.length; i += batchSize) {\n batches.push(rows.slice(i, i + batchSize));\n }\n\n let totalEntities = 0;\n let totalTriples = 0;\n let rowsProcessed = 0;\n let nextBatch = 0;\n\n const postBatch = async (batch: Record<string, string>[]) => {\n const body: Record<string, unknown> = {\n mapping,\n rows: batch,\n source: \"client\",\n };\n if (kgName) body.kg_name = kgName;\n const result = await this.request<{\n entities_resolved?: number;\n triples_inserted?: number;\n }>(\"POST\", `${this.base()}/ingest/csv/rows`, body, 300_000);\n return {\n entities: result.entities_resolved ?? 0,\n triples: result.triples_inserted ?? 0,\n size: batch.length,\n };\n };\n\n const worker = async (): Promise<void> => {\n while (true) {\n const idx = nextBatch++;\n if (idx >= batches.length) return;\n const r = await postBatch(batches[idx]!);\n totalEntities += r.entities;\n totalTriples += r.triples;\n rowsProcessed += r.size;\n opts.onProgress?.({\n rowsProcessed,\n totalRows: rows.length,\n entitiesResolved: totalEntities,\n triplesInserted: totalTriples,\n });\n }\n };\n\n const workers: Array<Promise<void>> = [];\n for (let i = 0; i < Math.min(concurrency, batches.length); i++) {\n workers.push(worker());\n }\n await Promise.all(workers);\n\n return {\n entities_resolved: totalEntities,\n triples_inserted: totalTriples,\n mapping,\n };\n }\n\n /** Ask a natural language question and return the parsed response. */\n async ask(\n question: string,\n opts: AskOptions = {},\n ): Promise<Record<string, unknown>> {\n const body: Record<string, unknown> = { question };\n if (opts.kg) body.kg_name = opts.kg;\n if (opts.model) body.model = opts.model;\n return this.request(\"POST\", `${this.base()}/ask`, body, 60_000);\n }\n\n /** List all knowledge graphs for the current tenant. */\n async listKgs(): Promise<Array<Record<string, unknown>>> {\n const data = await this.request<unknown>(\n \"GET\",\n `${this.base()}/kgs`,\n undefined,\n 15_000,\n );\n if (Array.isArray(data)) return data as Array<Record<string, unknown>>;\n if (data && typeof data === \"object\" && \"kgs\" in data) {\n const kgs = (data as { kgs?: unknown }).kgs;\n if (Array.isArray(kgs)) return kgs as Array<Record<string, unknown>>;\n }\n return [];\n }\n\n /** Create a knowledge graph. */\n async createKg(\n name: string,\n description?: string,\n ): Promise<Record<string, unknown>> {\n const body: Record<string, unknown> = { name };\n if (description) body.description = description;\n return this.request(\"POST\", `${this.base()}/kgs`, body, 15_000);\n }\n\n /** Delete a knowledge graph by name. */\n async deleteKg(name: string): Promise<Record<string, unknown>> {\n return this.request(\n \"DELETE\",\n `${this.base()}/kgs/${encodeURIComponent(name)}`,\n undefined,\n 30_000,\n );\n }\n\n /** List ontology types. */\n async ontologyTypes(): Promise<Array<Record<string, unknown>>> {\n const data = await this.request<unknown>(\n \"GET\",\n `${this.base()}/ontology/types`,\n undefined,\n 15_000,\n );\n return Array.isArray(data) ? (data as Array<Record<string, unknown>>) : [];\n }\n\n /** Per-KG type counts: every type with ≥1 instance, sorted desc. */\n async typeCounts(kg: string): Promise<TypeCount[]> {\n const data = await this.request<unknown>(\n \"GET\",\n `${this.base()}/kgs/${encodeURIComponent(kg)}/type-counts`,\n undefined,\n 30_000,\n );\n return Array.isArray(data) ? (data as TypeCount[]) : [];\n }\n\n /** Plan + run an enrichment job. Returns immediately with the job id. */\n async enrichRun(req: EnrichRequest): Promise<EnrichJobCreate> {\n return this.request<EnrichJobCreate>(\n \"POST\",\n `${this.base()}/enrich/jobs`,\n req,\n 30_000,\n );\n }\n\n /** List recent enrichment jobs for the current tenant. */\n async enrichJobs(): Promise<JobSummary[]> {\n const data = await this.request<unknown>(\n \"GET\",\n `${this.base()}/enrich/jobs`,\n undefined,\n 15_000,\n );\n return Array.isArray(data) ? (data as JobSummary[]) : [];\n }\n\n /** Fetch a single enrichment job (with truncated results). */\n async enrichJob(jobId: string): Promise<EnrichJob> {\n return this.request<EnrichJob>(\n \"GET\",\n `${this.base()}/enrich/jobs/${encodeURIComponent(jobId)}`,\n undefined,\n 15_000,\n );\n }\n\n /** Fetch the conflict review queue for a job. */\n async enrichConflicts(jobId: string): Promise<ConflictReview[]> {\n const data = await this.request<unknown>(\n \"GET\",\n `${this.base()}/enrich/jobs/${encodeURIComponent(jobId)}/conflicts`,\n undefined,\n 30_000,\n );\n return Array.isArray(data) ? (data as ConflictReview[]) : [];\n }\n\n /** Apply a set of conflict review decisions to a job. */\n async enrichApply(\n jobId: string,\n decisions: ConflictReview[],\n ): Promise<{ applied: number }> {\n return this.request<{ applied: number }>(\n \"POST\",\n `${this.base()}/enrich/jobs/${encodeURIComponent(jobId)}/apply`,\n { decisions },\n 60_000,\n );\n }\n\n /** Cancel an enrichment job. */\n async enrichCancel(jobId: string): Promise<void> {\n await this.request<void>(\n \"DELETE\",\n `${this.base()}/enrich/jobs/${encodeURIComponent(jobId)}`,\n undefined,\n 15_000,\n );\n }\n\n /** Per-type breakdown for one type in one KG: definition + counts + samples.\n *\n * System predicates (rdfs:label, ingested_at, source) are hidden by default\n * — they're attached to every entity at 100% and drown out the columns the\n * user cares about. Pass `includeSystem: true` to see them. */\n async typeUsage(\n kg: string,\n typeName: string,\n opts: { includeSystem?: boolean } = {},\n ): Promise<TypeUsage> {\n const qs = opts.includeSystem ? \"?include_system=true\" : \"\";\n return this.request<TypeUsage>(\n \"GET\",\n `${this.base()}/kgs/${encodeURIComponent(kg)}/types/${encodeURIComponent(typeName)}/usage${qs}`,\n undefined,\n 30_000,\n );\n }\n}\n\nexport interface TypeCount {\n name: string;\n entity_count: number;\n}\n\nexport interface AttributeUsage {\n name: string;\n datatype: string;\n count: number;\n}\n\nexport interface RelationshipUsage {\n name: string;\n target_type: string | null;\n count: number;\n}\n\nexport interface EntitySample {\n uri: string;\n label: string;\n}\n\nexport interface TypeUsage {\n name: string;\n description: string;\n parent_type: string | null;\n entity_count: number;\n attributes: AttributeUsage[];\n relationships: RelationshipUsage[];\n samples: EntitySample[];\n}\n\nexport type EnrichmentTier = \"lite\" | \"base\" | \"core\" | \"pro\";\nexport type JobStatus =\n | \"queued\"\n | \"running\"\n | \"review\"\n | \"applied\"\n | \"cancelled\"\n | \"failed\";\nexport type ConflictPolicy = \"skip\" | \"verify\" | \"overwrite\" | \"stage\";\nexport type RowAction =\n | \"filled\"\n | \"verified\"\n | \"conflict\"\n | \"skipped\"\n | \"no_match\";\nexport type ReviewDecision = \"accept\" | \"reject\" | \"skip\";\n\nexport interface EnrichRequest {\n type_name: string;\n attributes: string[];\n tier?: EnrichmentTier;\n kg_name: string;\n conflict_policy?: ConflictPolicy;\n confidence_min?: number;\n limit?: number;\n}\n\nexport interface EnrichJobCreate {\n job_id: string;\n status: JobStatus;\n estimated_cost_usd: number;\n total_entities: number;\n}\n\nexport interface Verdict {\n value: string;\n confidence: number;\n source: string;\n source_url?: string | null;\n reasoning?: string | null;\n}\n\nexport interface JobProgress {\n total: number;\n processed: number;\n filled: number;\n verified: number;\n conflicts: number;\n skipped: number;\n cache_hits: number;\n}\n\nexport interface RowResult {\n entity_uri: string;\n attribute: string;\n existing_value: string | null;\n verdict: Verdict | null;\n action: RowAction;\n}\n\nexport interface JobSummary {\n id: string;\n tenant_id: string;\n kg_name: string;\n type_name: string;\n attributes: string[];\n tier: EnrichmentTier;\n status: JobStatus;\n progress: JobProgress;\n created_at: string;\n started_at?: string | null;\n completed_at?: string | null;\n conflict_policy: ConflictPolicy;\n confidence_min: number;\n error?: string | null;\n}\n\nexport interface EnrichJob extends JobSummary {\n results?: RowResult[];\n limit?: number | null;\n}\n\nexport interface ConflictReview {\n entity_uri: string;\n attribute: string;\n existing_value: string;\n proposed: Verdict;\n decision?: ReviewDecision | null;\n}\n","import { homedir } from \"node:os\";\nimport { join } from \"node:path\";\nimport { existsSync, mkdirSync, readFileSync, writeFileSync, chmodSync } from \"node:fs\";\n\nexport interface CographConfig {\n apiKey?: string;\n apiUrl?: string;\n tenant?: string;\n email?: string;\n}\n\nfunction configDir(): string {\n return join(homedir(), \".cograph\");\n}\n\nfunction configPath(): string {\n return join(configDir(), \"config.json\");\n}\n\n/**\n * Read `~/.cograph/config.json`. Returns an empty object if the file is\n * missing or unreadable — callers should treat fields as optional.\n */\nexport function readConfig(): CographConfig {\n const path = configPath();\n if (!existsSync(path)) return {};\n try {\n const raw = readFileSync(path, \"utf-8\");\n const parsed = JSON.parse(raw) as unknown;\n if (parsed && typeof parsed === \"object\") {\n return parsed as CographConfig;\n }\n } catch {\n // Corrupt or unreadable; behave as if absent so a fresh login can rewrite.\n }\n return {};\n}\n\n/**\n * Write `~/.cograph/config.json` with `chmod 600`. Creates the directory if\n * needed. Merges with the existing config so callers can update one field\n * without clobbering the others.\n */\nexport function writeConfig(patch: CographConfig): void {\n const dir = configDir();\n if (!existsSync(dir)) {\n mkdirSync(dir, { recursive: true, mode: 0o700 });\n }\n const merged = { ...readConfig(), ...patch };\n const path = configPath();\n writeFileSync(path, JSON.stringify(merged, null, 2) + \"\\n\", \"utf-8\");\n try {\n chmodSync(path, 0o600);\n } catch {\n // best-effort; some filesystems (e.g., FAT) don't honor chmod\n }\n}\n\nexport function configPathForDisplay(): string {\n return configPath();\n}\n"],"mappings":"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","names":["existsSync","readFileSync","text","existsSync","readFileSync"]}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import {
|
|
3
3
|
Client,
|
|
4
4
|
CographError
|
|
5
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-K7B4PPX2.js";
|
|
6
6
|
import "./chunk-7VVBEUZQ.js";
|
|
7
7
|
|
|
8
8
|
// src/shell.ts
|
|
@@ -60,6 +60,10 @@ function showCommands() {
|
|
|
60
60
|
["/types [query]", "List types in the current KG (with entity counts)"],
|
|
61
61
|
["/type <name>", "Drill into one type \u2014 attributes, relationships, samples"],
|
|
62
62
|
["/type <name> --system", "\u2026also include auto-attached system attributes"],
|
|
63
|
+
["/enrich <Type> <attr> ...", "Plan + run an enrichment job (interactive)"],
|
|
64
|
+
["/enrich watch <job_id>", "Live progress for a running job"],
|
|
65
|
+
["/enrich jobs", "List recent enrichment jobs"],
|
|
66
|
+
["/enrich review <job_id>", "Walk through conflicts and accept/reject"],
|
|
63
67
|
["/login", "Re-authenticate (browser)"],
|
|
64
68
|
["/status", "Show graph stats"],
|
|
65
69
|
["/reset", "Clear the current KG"],
|
|
@@ -487,12 +491,321 @@ async function cmdType(client, kg, rl, input) {
|
|
|
487
491
|
}
|
|
488
492
|
stdout.write("\n");
|
|
489
493
|
}
|
|
490
|
-
function
|
|
494
|
+
function lastUriSegment(uri) {
|
|
495
|
+
if (!uri) return uri;
|
|
496
|
+
const hash = uri.lastIndexOf("#");
|
|
497
|
+
if (hash >= 0 && hash < uri.length - 1) return uri.slice(hash + 1);
|
|
498
|
+
const slash = uri.lastIndexOf("/");
|
|
499
|
+
if (slash >= 0 && slash < uri.length - 1) return uri.slice(slash + 1);
|
|
500
|
+
return uri;
|
|
501
|
+
}
|
|
502
|
+
function relativeTime(iso) {
|
|
503
|
+
if (!iso) return "\u2014";
|
|
504
|
+
const t = Date.parse(iso);
|
|
505
|
+
if (!Number.isFinite(t)) return "\u2014";
|
|
506
|
+
const diffMs = Date.now() - t;
|
|
507
|
+
const s = Math.max(0, Math.floor(diffMs / 1e3));
|
|
508
|
+
if (s < 60) return `${s}s ago`;
|
|
509
|
+
const m = Math.floor(s / 60);
|
|
510
|
+
if (m < 60) return `${m}m ago`;
|
|
511
|
+
const h = Math.floor(m / 60);
|
|
512
|
+
if (h < 24) return `${h}h ago`;
|
|
513
|
+
const d = Math.floor(h / 24);
|
|
514
|
+
return `${d}d ago`;
|
|
515
|
+
}
|
|
516
|
+
function progressBar(processed, total, width = 20) {
|
|
517
|
+
if (!total || total <= 0) return "[" + " ".repeat(width) + "]";
|
|
518
|
+
const ratio = Math.max(0, Math.min(1, processed / total));
|
|
519
|
+
const filled = Math.round(ratio * width);
|
|
520
|
+
return "[" + "\u2588".repeat(filled) + "\u2591".repeat(width - filled) + "]";
|
|
521
|
+
}
|
|
522
|
+
function statusColor(status) {
|
|
523
|
+
switch (status) {
|
|
524
|
+
case "applied":
|
|
525
|
+
return GREEN;
|
|
526
|
+
case "failed":
|
|
527
|
+
return RED;
|
|
528
|
+
case "review":
|
|
529
|
+
return YELLOW;
|
|
530
|
+
case "cancelled":
|
|
531
|
+
return DIM;
|
|
532
|
+
default:
|
|
533
|
+
return CYAN;
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
async function cmdEnrichRun(client, kg, rl, args) {
|
|
537
|
+
if (args.length < 2) {
|
|
538
|
+
stdout.write(
|
|
539
|
+
` ${YELLOW}Usage:${RESET} /enrich <Type> <attr1> [<attr2> ...]
|
|
540
|
+
`
|
|
541
|
+
);
|
|
542
|
+
return;
|
|
543
|
+
}
|
|
544
|
+
const typeInput = args[0];
|
|
545
|
+
const attrs = args.slice(1).map((a) => a.replace(/^\./, ""));
|
|
546
|
+
const typeName = await resolveTypeName(client, kg, rl, typeInput);
|
|
547
|
+
if (!typeName) return;
|
|
548
|
+
const tier = "lite";
|
|
549
|
+
const policy = "stage";
|
|
550
|
+
stdout.write(
|
|
551
|
+
`
|
|
552
|
+
${BOLD}Plan:${RESET} enrich ${CYAN}${typeName}${RESET}.${attrs.map((a) => `${CYAN}${a}${RESET}`).join(`, .`)} in ${BOLD}${kg}${RESET} ${DIM}\xB7${RESET} tier: ${tier} ${DIM}\xB7${RESET} policy: ${policy}
|
|
553
|
+
|
|
554
|
+
`
|
|
555
|
+
);
|
|
556
|
+
const sp = startSpinner(`Queueing enrichment for ${typeName}...`);
|
|
557
|
+
let created;
|
|
558
|
+
try {
|
|
559
|
+
created = await client.enrichRun({
|
|
560
|
+
type_name: typeName,
|
|
561
|
+
attributes: attrs,
|
|
562
|
+
tier,
|
|
563
|
+
kg_name: kg,
|
|
564
|
+
conflict_policy: policy
|
|
565
|
+
});
|
|
566
|
+
} catch (err) {
|
|
567
|
+
sp.stop();
|
|
568
|
+
if (err instanceof CographError) printError(err.message);
|
|
569
|
+
else printError(err instanceof Error ? err.message : String(err));
|
|
570
|
+
return;
|
|
571
|
+
}
|
|
572
|
+
sp.stop();
|
|
573
|
+
const cost = (created.estimated_cost_usd ?? 0).toFixed(4);
|
|
574
|
+
stdout.write(
|
|
575
|
+
` ${GREEN}\u2713${RESET} Job queued: ${CYAN_BOLD}${created.job_id}${RESET} ${DIM}\xB7${RESET} estimated cost ${BOLD}$${cost}${RESET} ${DIM}\xB7${RESET} ${fmtNum(created.total_entities ?? 0)} entities
|
|
576
|
+
`
|
|
577
|
+
);
|
|
578
|
+
const watch = (await ask(rl, ` Watch progress? [Y/n]: `)).trim().toLowerCase();
|
|
579
|
+
if (watch === "" || watch === "y" || watch === "yes") {
|
|
580
|
+
await watchJob(client, created.job_id);
|
|
581
|
+
} else {
|
|
582
|
+
stdout.write(
|
|
583
|
+
` ${DIM}Tip: /enrich watch ${created.job_id} to follow it.${RESET}
|
|
584
|
+
`
|
|
585
|
+
);
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
async function watchJob(client, jobId) {
|
|
589
|
+
const startedAt = Date.now();
|
|
590
|
+
let lastJob = null;
|
|
591
|
+
const draw = (job) => {
|
|
592
|
+
const p2 = job.progress;
|
|
593
|
+
const bar = progressBar(p2.processed, p2.total);
|
|
594
|
+
const elapsed = Math.max(1, Math.floor((Date.now() - startedAt) / 1e3));
|
|
595
|
+
const rate = p2.processed / elapsed;
|
|
596
|
+
let etaStr = "\u2014";
|
|
597
|
+
if (rate > 0 && p2.total > p2.processed) {
|
|
598
|
+
const remaining = Math.ceil((p2.total - p2.processed) / rate);
|
|
599
|
+
etaStr = remaining < 60 ? `${remaining}s` : remaining < 3600 ? `${Math.floor(remaining / 60)}m` : `${Math.floor(remaining / 3600)}h`;
|
|
600
|
+
}
|
|
601
|
+
const sc = statusColor(job.status);
|
|
602
|
+
stdout.write(
|
|
603
|
+
`\r\x1B[2K ${sc}${job.status}${RESET} ${bar} ${fmtNum(p2.processed)}/${fmtNum(p2.total)} ${DIM}\xB7${RESET} filled ${GREEN}${fmtNum(p2.filled)}${RESET} ${DIM}\xB7${RESET} verified ${CYAN}${fmtNum(p2.verified)}${RESET} ${DIM}\xB7${RESET} conflicts ${YELLOW}${fmtNum(p2.conflicts)}${RESET} ${DIM}\xB7${RESET} ETA ${etaStr}`
|
|
604
|
+
);
|
|
605
|
+
};
|
|
606
|
+
while (true) {
|
|
607
|
+
let job;
|
|
608
|
+
try {
|
|
609
|
+
job = await client.enrichJob(jobId);
|
|
610
|
+
} catch (err) {
|
|
611
|
+
stdout.write("\r\x1B[2K");
|
|
612
|
+
if (err instanceof CographError) printError(err.message);
|
|
613
|
+
else printError(err instanceof Error ? err.message : String(err));
|
|
614
|
+
return;
|
|
615
|
+
}
|
|
616
|
+
lastJob = job;
|
|
617
|
+
draw(job);
|
|
618
|
+
if (job.status !== "running" && job.status !== "queued") break;
|
|
619
|
+
await new Promise((r) => setTimeout(r, 1500));
|
|
620
|
+
}
|
|
621
|
+
stdout.write("\n");
|
|
622
|
+
if (!lastJob) return;
|
|
623
|
+
const p = lastJob.progress;
|
|
624
|
+
if (lastJob.status === "review") {
|
|
625
|
+
stdout.write(
|
|
626
|
+
` ${YELLOW}\u2726${RESET} ${fmtNum(p.conflicts)} conflict${p.conflicts === 1 ? "" : "s"} need review. ${DIM}Run${RESET} /enrich review ${lastJob.id}${DIM} to walk through them.${RESET}
|
|
627
|
+
`
|
|
628
|
+
);
|
|
629
|
+
} else if (lastJob.status === "applied") {
|
|
630
|
+
stdout.write(
|
|
631
|
+
` ${GREEN}\u2713${RESET} Applied ${DIM}\xB7${RESET} filled ${fmtNum(p.filled)}, verified ${fmtNum(p.verified)}, skipped ${fmtNum(p.skipped)}
|
|
632
|
+
`
|
|
633
|
+
);
|
|
634
|
+
} else if (lastJob.status === "failed") {
|
|
635
|
+
printError(`Job failed: ${lastJob.error ?? "(no error message)"}`);
|
|
636
|
+
} else if (lastJob.status === "cancelled") {
|
|
637
|
+
stdout.write(` ${DIM}Job cancelled.${RESET}
|
|
638
|
+
`);
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
async function cmdEnrichJobs(client) {
|
|
642
|
+
const sp = startSpinner("Loading enrichment jobs...");
|
|
643
|
+
let jobs;
|
|
644
|
+
try {
|
|
645
|
+
jobs = await client.enrichJobs();
|
|
646
|
+
} catch (err) {
|
|
647
|
+
sp.stop();
|
|
648
|
+
if (err instanceof CographError) printError(err.message);
|
|
649
|
+
else printError(err instanceof Error ? err.message : String(err));
|
|
650
|
+
return;
|
|
651
|
+
}
|
|
652
|
+
sp.stop();
|
|
653
|
+
if (jobs.length === 0) {
|
|
654
|
+
stdout.write(` ${DIM}No enrichment jobs yet.${RESET}
|
|
655
|
+
`);
|
|
656
|
+
return;
|
|
657
|
+
}
|
|
658
|
+
const truncAttrs = (attrs) => {
|
|
659
|
+
const max = 30;
|
|
660
|
+
const joined = attrs.join(", ");
|
|
661
|
+
if (joined.length <= max) return joined;
|
|
662
|
+
return joined.slice(0, max - 1) + "\u2026";
|
|
663
|
+
};
|
|
664
|
+
const rows = jobs.map((j) => ({
|
|
665
|
+
id: j.id,
|
|
666
|
+
type: j.type_name,
|
|
667
|
+
attrs: truncAttrs(j.attributes ?? []),
|
|
668
|
+
status: j.status,
|
|
669
|
+
progress: `${fmtNum(j.progress?.processed ?? 0)}/${fmtNum(j.progress?.total ?? 0)}`,
|
|
670
|
+
created: relativeTime(j.created_at)
|
|
671
|
+
}));
|
|
672
|
+
const w = {
|
|
673
|
+
id: Math.max("ID".length, ...rows.map((r) => r.id.length)),
|
|
674
|
+
type: Math.max("Type".length, ...rows.map((r) => r.type.length)),
|
|
675
|
+
attrs: Math.max("Attrs".length, ...rows.map((r) => r.attrs.length)),
|
|
676
|
+
status: Math.max("Status".length, ...rows.map((r) => r.status.length)),
|
|
677
|
+
progress: Math.max("Progress".length, ...rows.map((r) => r.progress.length))
|
|
678
|
+
};
|
|
679
|
+
stdout.write("\n");
|
|
680
|
+
stdout.write(
|
|
681
|
+
` ${BOLD}${"ID".padEnd(w.id)} ${"Type".padEnd(w.type)} ${"Attrs".padEnd(w.attrs)} ${"Status".padEnd(w.status)} ${"Progress".padEnd(w.progress)} Created${RESET}
|
|
682
|
+
`
|
|
683
|
+
);
|
|
684
|
+
for (const r of rows) {
|
|
685
|
+
const sc = statusColor(r.status);
|
|
686
|
+
stdout.write(
|
|
687
|
+
` ${CYAN}${r.id.padEnd(w.id)}${RESET} ${r.type.padEnd(w.type)} ${DIM}${r.attrs.padEnd(w.attrs)}${RESET} ${sc}${r.status.padEnd(w.status)}${RESET} ${r.progress.padEnd(w.progress)} ${DIM}${r.created}${RESET}
|
|
688
|
+
`
|
|
689
|
+
);
|
|
690
|
+
}
|
|
691
|
+
stdout.write("\n");
|
|
692
|
+
}
|
|
693
|
+
async function cmdEnrichReview(client, rl, jobId) {
|
|
694
|
+
if (!jobId) {
|
|
695
|
+
stdout.write(` ${YELLOW}Usage:${RESET} /enrich review <job_id>
|
|
696
|
+
`);
|
|
697
|
+
return;
|
|
698
|
+
}
|
|
699
|
+
const sp = startSpinner(`Loading conflicts for ${jobId}...`);
|
|
700
|
+
let conflicts;
|
|
701
|
+
try {
|
|
702
|
+
conflicts = await client.enrichConflicts(jobId);
|
|
703
|
+
} catch (err) {
|
|
704
|
+
sp.stop();
|
|
705
|
+
if (err instanceof CographError) printError(err.message);
|
|
706
|
+
else printError(err instanceof Error ? err.message : String(err));
|
|
707
|
+
return;
|
|
708
|
+
}
|
|
709
|
+
sp.stop();
|
|
710
|
+
if (conflicts.length === 0) {
|
|
711
|
+
stdout.write(` ${DIM}No conflicts to review.${RESET}
|
|
712
|
+
`);
|
|
713
|
+
return;
|
|
714
|
+
}
|
|
715
|
+
const decisions = [];
|
|
716
|
+
let acceptAll = false;
|
|
717
|
+
let quitEarly = false;
|
|
718
|
+
for (let i = 0; i < conflicts.length; i++) {
|
|
719
|
+
const c = conflicts[i];
|
|
720
|
+
const entity = lastUriSegment(c.entity_uri);
|
|
721
|
+
const conf = (c.proposed?.confidence ?? 0).toFixed(2);
|
|
722
|
+
stdout.write("\n");
|
|
723
|
+
stdout.write(
|
|
724
|
+
` ${DIM}[${i + 1}/${conflicts.length}]${RESET} ${BOLD}${entity}${RESET}.${CYAN}${c.attribute}${RESET}
|
|
725
|
+
`
|
|
726
|
+
);
|
|
727
|
+
stdout.write(
|
|
728
|
+
` ${DIM}existing \u2192${RESET} ${c.existing_value}
|
|
729
|
+
${DIM}proposed \u2192${RESET} ${BOLD}${c.proposed?.value ?? ""}${RESET} ${DIM}(conf ${conf}, src ${c.proposed?.source ?? "?"})${RESET}
|
|
730
|
+
`
|
|
731
|
+
);
|
|
732
|
+
if (c.proposed?.source_url) {
|
|
733
|
+
stdout.write(` ${DIM}url \u2192${RESET} ${c.proposed.source_url}
|
|
734
|
+
`);
|
|
735
|
+
}
|
|
736
|
+
let decision;
|
|
737
|
+
if (acceptAll) {
|
|
738
|
+
decision = "accept";
|
|
739
|
+
stdout.write(` ${GREEN}auto-accepted${RESET}
|
|
740
|
+
`);
|
|
741
|
+
} else {
|
|
742
|
+
const ans = (await ask(
|
|
743
|
+
rl,
|
|
744
|
+
` [a]ccept / [r]eject / [s]kip / [A]ccept all remaining / [q]uit (saves progress) [s]: `
|
|
745
|
+
)).trim();
|
|
746
|
+
if (ans === "A") {
|
|
747
|
+
acceptAll = true;
|
|
748
|
+
decision = "accept";
|
|
749
|
+
} else if (ans === "a") {
|
|
750
|
+
decision = "accept";
|
|
751
|
+
} else if (ans === "r") {
|
|
752
|
+
decision = "reject";
|
|
753
|
+
} else if (ans === "q") {
|
|
754
|
+
quitEarly = true;
|
|
755
|
+
break;
|
|
756
|
+
} else {
|
|
757
|
+
decision = "skip";
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
decisions.push({ ...c, decision });
|
|
761
|
+
}
|
|
762
|
+
if (quitEarly) {
|
|
763
|
+
if (decisions.length === 0) {
|
|
764
|
+
stdout.write(` ${DIM}No decisions made \u2014 nothing to save.${RESET}
|
|
765
|
+
`);
|
|
766
|
+
return;
|
|
767
|
+
}
|
|
768
|
+
const save = (await ask(rl, ` Save ${decisions.length} decision(s) so far? [Y/n]: `)).trim().toLowerCase();
|
|
769
|
+
if (save !== "" && save !== "y" && save !== "yes") {
|
|
770
|
+
stdout.write(` ${DIM}Discarded.${RESET}
|
|
771
|
+
`);
|
|
772
|
+
return;
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
if (decisions.length === 0) {
|
|
776
|
+
stdout.write(` ${DIM}No decisions to apply.${RESET}
|
|
777
|
+
`);
|
|
778
|
+
return;
|
|
779
|
+
}
|
|
780
|
+
const sp2 = startSpinner(`Applying ${decisions.length} decision(s)...`);
|
|
781
|
+
try {
|
|
782
|
+
const res = await client.enrichApply(jobId, decisions);
|
|
783
|
+
sp2.stop();
|
|
784
|
+
stdout.write(
|
|
785
|
+
` ${GREEN}\u2713${RESET} Applied ${BOLD}${fmtNum(res.applied)}${RESET} change${res.applied === 1 ? "" : "s"}.
|
|
786
|
+
`
|
|
787
|
+
);
|
|
788
|
+
} catch (err) {
|
|
789
|
+
sp2.stop();
|
|
790
|
+
if (err instanceof CographError) printError(err.message);
|
|
791
|
+
else printError(err instanceof Error ? err.message : String(err));
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
function urlHost(url) {
|
|
795
|
+
try {
|
|
796
|
+
return new URL(url).host;
|
|
797
|
+
} catch {
|
|
798
|
+
return url.replace(/^https?:\/\//, "").replace(/\/+$/, "");
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
function makePrompt(kg, triples, mode = "cloud", baseUrl) {
|
|
491
802
|
const kgPart = `${DIM}(${kg})${RESET}`;
|
|
492
|
-
|
|
493
|
-
|
|
803
|
+
const triplePart = triples > 0 ? `${DIM}[${fmtNum(triples)}]${RESET} ` : "";
|
|
804
|
+
if (mode === "self-hosted" && baseUrl) {
|
|
805
|
+
const host = urlHost(baseUrl);
|
|
806
|
+
return ` ${CYAN_BOLD}cograph${RESET}${DIM}@${host}${RESET} ${kgPart} ${triplePart}${CYAN_BOLD}\u25B8${RESET} `;
|
|
494
807
|
}
|
|
495
|
-
return ` ${CYAN_BOLD}cograph${RESET} ${kgPart} ${CYAN_BOLD}\u25B8${RESET} `;
|
|
808
|
+
return ` ${CYAN_BOLD}cograph${RESET} ${kgPart} ${triplePart}${CYAN_BOLD}\u25B8${RESET} `;
|
|
496
809
|
}
|
|
497
810
|
function splitArgs(s) {
|
|
498
811
|
const out = [];
|
|
@@ -517,8 +830,21 @@ function splitArgs(s) {
|
|
|
517
830
|
return out;
|
|
518
831
|
}
|
|
519
832
|
async function runShell(opts) {
|
|
520
|
-
|
|
521
|
-
|
|
833
|
+
const CLOUD_DEFAULT = "https://api.cograph.cloud";
|
|
834
|
+
const envUrl = process.env.COGRAPH_API_URL || process.env.OMNIX_API_URL;
|
|
835
|
+
const envIsSelfHosted = !!envUrl && envUrl !== CLOUD_DEFAULT;
|
|
836
|
+
const selfHostedHint = !!opts.local || !!opts.noLogin || envIsSelfHosted;
|
|
837
|
+
let client = opts.local ? new Client({ baseUrl: "http://localhost:8000", tenant: "default" }) : selfHostedHint ? new Client({ tenant: "default" }) : new Client();
|
|
838
|
+
const health = await client.healthCheck();
|
|
839
|
+
if (!health.ok) {
|
|
840
|
+
printError(
|
|
841
|
+
`Could not reach ${health.url}. Is the server running?`
|
|
842
|
+
);
|
|
843
|
+
return;
|
|
844
|
+
}
|
|
845
|
+
const selfHosted = selfHostedHint || !health.requiresAuth;
|
|
846
|
+
const mode = selfHosted ? "self-hosted" : "cloud";
|
|
847
|
+
if (!selfHosted && health.requiresAuth && !client.apiKey) {
|
|
522
848
|
stdout.write(
|
|
523
849
|
`
|
|
524
850
|
${DIM}Not signed in \u2014 opening your browser to log in...${RESET}
|
|
@@ -538,6 +864,13 @@ async function runShell(opts) {
|
|
|
538
864
|
terminal: true
|
|
539
865
|
});
|
|
540
866
|
showBanner();
|
|
867
|
+
if (selfHosted) {
|
|
868
|
+
stdout.write(
|
|
869
|
+
`${DIM} Self-hosted mode \xB7 ${client.baseUrl} \xB7 tenant=${client.tenant}${RESET}
|
|
870
|
+
|
|
871
|
+
`
|
|
872
|
+
);
|
|
873
|
+
}
|
|
541
874
|
let kg = opts.kg;
|
|
542
875
|
if (!kg) {
|
|
543
876
|
const picked = await selectKg(client, rl);
|
|
@@ -574,7 +907,7 @@ async function runShell(opts) {
|
|
|
574
907
|
while (running) {
|
|
575
908
|
let line;
|
|
576
909
|
try {
|
|
577
|
-
line = (await ask(rl, makePrompt(kg, triples))).trim();
|
|
910
|
+
line = (await ask(rl, makePrompt(kg, triples, mode, client.baseUrl))).trim();
|
|
578
911
|
} catch {
|
|
579
912
|
break;
|
|
580
913
|
}
|
|
@@ -604,6 +937,29 @@ async function runShell(opts) {
|
|
|
604
937
|
} else if (line.startsWith("/type ") || line === "/type") {
|
|
605
938
|
const arg = line === "/type" ? "" : line.slice("/type ".length);
|
|
606
939
|
await cmdType(client, kg, rl, arg);
|
|
940
|
+
} else if (line === "/enrich" || line.startsWith("/enrich ")) {
|
|
941
|
+
const args = splitArgs(line.slice("/enrich".length).trim());
|
|
942
|
+
if (args.length === 0) {
|
|
943
|
+
stdout.write(
|
|
944
|
+
` ${YELLOW}Usage:${RESET} /enrich <Type> <attr> ... | /enrich watch <id> | /enrich jobs | /enrich review <id>
|
|
945
|
+
`
|
|
946
|
+
);
|
|
947
|
+
} else if (args[0] === "jobs") {
|
|
948
|
+
await cmdEnrichJobs(client);
|
|
949
|
+
} else if (args[0] === "watch") {
|
|
950
|
+
const jid = args[1];
|
|
951
|
+
if (!jid) {
|
|
952
|
+
stdout.write(` ${YELLOW}Usage:${RESET} /enrich watch <job_id>
|
|
953
|
+
`);
|
|
954
|
+
} else {
|
|
955
|
+
await watchJob(client, jid);
|
|
956
|
+
}
|
|
957
|
+
} else if (args[0] === "review") {
|
|
958
|
+
await cmdEnrichReview(client, rl, args[1] ?? "");
|
|
959
|
+
} else {
|
|
960
|
+
await cmdEnrichRun(client, kg, rl, args);
|
|
961
|
+
await refresh();
|
|
962
|
+
}
|
|
607
963
|
} else if (line === "/status") {
|
|
608
964
|
await cmdStatus(client, kg);
|
|
609
965
|
await refresh();
|
|
@@ -728,7 +1084,7 @@ async function runShell(opts) {
|
|
|
728
1084
|
}
|
|
729
1085
|
} else if (line.startsWith("/")) {
|
|
730
1086
|
stdout.write(
|
|
731
|
-
` ${YELLOW}Unknown command.${RESET} Try /ingest, /ask, /kg, /types, /type, /login, /status, /reset, /help, /quit
|
|
1087
|
+
` ${YELLOW}Unknown command.${RESET} Try /ingest, /ask, /kg, /types, /type, /enrich, /login, /status, /reset, /help, /quit
|
|
732
1088
|
`
|
|
733
1089
|
);
|
|
734
1090
|
} else {
|
|
@@ -744,4 +1100,4 @@ async function runShell(opts) {
|
|
|
744
1100
|
export {
|
|
745
1101
|
runShell
|
|
746
1102
|
};
|
|
747
|
-
//# sourceMappingURL=shell-
|
|
1103
|
+
//# sourceMappingURL=shell-VPPIRJSS.js.map
|