@arke-institute/sdk 0.1.1 → 0.1.2
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/dist/content/index.cjs +506 -0
- package/dist/content/index.cjs.map +1 -0
- package/dist/content/index.d.cts +403 -0
- package/dist/content/index.d.ts +403 -0
- package/dist/content/index.js +473 -0
- package/dist/content/index.js.map +1 -0
- package/dist/edit/index.cjs +1029 -0
- package/dist/edit/index.cjs.map +1 -0
- package/dist/edit/index.d.cts +78 -0
- package/dist/edit/index.d.ts +78 -0
- package/dist/edit/index.js +983 -0
- package/dist/edit/index.js.map +1 -0
- package/dist/{errors-BrNZWPE7.d.cts → errors-3L7IiHcr.d.cts} +3 -0
- package/dist/errors-B82BMmRP.d.cts +343 -0
- package/dist/errors-B82BMmRP.d.ts +343 -0
- package/dist/{errors-CCyp5KCg.d.ts → errors-BTe8GKRQ.d.ts} +3 -0
- package/dist/graph/index.cjs +433 -0
- package/dist/graph/index.cjs.map +1 -0
- package/dist/graph/index.d.cts +456 -0
- package/dist/graph/index.d.ts +456 -0
- package/dist/graph/index.js +402 -0
- package/dist/graph/index.js.map +1 -0
- package/dist/index.cjs +2126 -14
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +5 -1
- package/dist/index.d.ts +5 -1
- package/dist/index.js +2108 -14
- package/dist/index.js.map +1 -1
- package/dist/query/index.cjs +289 -0
- package/dist/query/index.cjs.map +1 -0
- package/dist/query/index.d.cts +541 -0
- package/dist/query/index.d.ts +541 -0
- package/dist/query/index.js +261 -0
- package/dist/query/index.js.map +1 -0
- package/dist/upload/index.cjs +3 -14
- package/dist/upload/index.cjs.map +1 -1
- package/dist/upload/index.d.cts +2 -2
- package/dist/upload/index.d.ts +2 -2
- package/dist/upload/index.js +3 -14
- package/dist/upload/index.js.map +1 -1
- package/package.json +26 -1
package/dist/index.cjs
CHANGED
|
@@ -649,11 +649,29 @@ __export(src_exports, {
|
|
|
649
649
|
ArkeUploader: () => ArkeUploader,
|
|
650
650
|
CollectionsClient: () => CollectionsClient,
|
|
651
651
|
CollectionsError: () => CollectionsError,
|
|
652
|
+
ComponentNotFoundError: () => ComponentNotFoundError,
|
|
653
|
+
ContentClient: () => ContentClient,
|
|
654
|
+
ContentError: () => ContentError,
|
|
655
|
+
ContentNetworkError: () => NetworkError2,
|
|
656
|
+
ContentNotFoundError: () => ContentNotFoundError,
|
|
657
|
+
EditClient: () => EditClient,
|
|
658
|
+
EditError: () => EditError,
|
|
659
|
+
EditSession: () => EditSession,
|
|
660
|
+
EntityNotFoundError: () => EntityNotFoundError2,
|
|
661
|
+
GraphClient: () => GraphClient,
|
|
662
|
+
GraphEntityNotFoundError: () => GraphEntityNotFoundError,
|
|
663
|
+
GraphError: () => GraphError,
|
|
664
|
+
GraphNetworkError: () => NetworkError3,
|
|
652
665
|
NetworkError: () => NetworkError,
|
|
666
|
+
NoPathFoundError: () => NoPathFoundError,
|
|
667
|
+
PermissionError: () => PermissionError,
|
|
668
|
+
QueryClient: () => QueryClient,
|
|
669
|
+
QueryError: () => QueryError,
|
|
653
670
|
ScanError: () => ScanError,
|
|
654
671
|
UploadClient: () => UploadClient,
|
|
655
672
|
UploadError: () => UploadError,
|
|
656
673
|
ValidationError: () => ValidationError,
|
|
674
|
+
VersionNotFoundError: () => VersionNotFoundError,
|
|
657
675
|
WorkerAPIError: () => WorkerAPIError
|
|
658
676
|
});
|
|
659
677
|
module.exports = __toCommonJS(src_exports);
|
|
@@ -1504,7 +1522,6 @@ var ArkeUploader = class {
|
|
|
1504
1522
|
};
|
|
1505
1523
|
|
|
1506
1524
|
// src/upload/client.ts
|
|
1507
|
-
init_errors();
|
|
1508
1525
|
function getUserIdFromToken(token) {
|
|
1509
1526
|
try {
|
|
1510
1527
|
const parts = token.split(".");
|
|
@@ -1589,22 +1606,12 @@ var UploadClient = class {
|
|
|
1589
1606
|
*
|
|
1590
1607
|
* Requires owner or editor role on the collection containing the parent PI.
|
|
1591
1608
|
* Use this to add a folder or files to an existing collection hierarchy.
|
|
1609
|
+
*
|
|
1610
|
+
* Note: Permission checks are enforced server-side by the ingest worker.
|
|
1611
|
+
* The server will return 403 if the user lacks edit access to the parent PI.
|
|
1592
1612
|
*/
|
|
1593
1613
|
async addToCollection(options) {
|
|
1594
1614
|
const { files, parentPi, customPrompts, processing, onProgress, dryRun } = options;
|
|
1595
|
-
if (!dryRun) {
|
|
1596
|
-
const permissions = await this.collectionsClient.getPiPermissions(parentPi);
|
|
1597
|
-
if (!permissions.canEdit) {
|
|
1598
|
-
if (!permissions.collection) {
|
|
1599
|
-
throw new ValidationError(
|
|
1600
|
-
`Cannot add files: PI "${parentPi}" is not part of any collection`
|
|
1601
|
-
);
|
|
1602
|
-
}
|
|
1603
|
-
throw new ValidationError(
|
|
1604
|
-
`Cannot add files to collection "${permissions.collection.title}": you need editor or owner role (current role: ${permissions.collection.role || "none"})`
|
|
1605
|
-
);
|
|
1606
|
-
}
|
|
1607
|
-
}
|
|
1608
1615
|
const uploader = new ArkeUploader({
|
|
1609
1616
|
gatewayUrl: this.config.gatewayUrl,
|
|
1610
1617
|
authToken: this.config.authToken,
|
|
@@ -1634,16 +1641,2121 @@ var UploadClient = class {
|
|
|
1634
1641
|
|
|
1635
1642
|
// src/index.ts
|
|
1636
1643
|
init_errors();
|
|
1644
|
+
|
|
1645
|
+
// src/query/errors.ts
|
|
1646
|
+
var QueryError = class extends Error {
|
|
1647
|
+
constructor(message, code2 = "UNKNOWN_ERROR", details) {
|
|
1648
|
+
super(message);
|
|
1649
|
+
this.code = code2;
|
|
1650
|
+
this.details = details;
|
|
1651
|
+
this.name = "QueryError";
|
|
1652
|
+
}
|
|
1653
|
+
};
|
|
1654
|
+
|
|
1655
|
+
// src/query/client.ts
|
|
1656
|
+
var QueryClient = class {
|
|
1657
|
+
constructor(config) {
|
|
1658
|
+
this.baseUrl = config.gatewayUrl.replace(/\/$/, "");
|
|
1659
|
+
this.fetchImpl = config.fetchImpl ?? fetch;
|
|
1660
|
+
}
|
|
1661
|
+
// ---------------------------------------------------------------------------
|
|
1662
|
+
// Request helpers
|
|
1663
|
+
// ---------------------------------------------------------------------------
|
|
1664
|
+
buildUrl(path2, query) {
|
|
1665
|
+
const url = new URL(`${this.baseUrl}${path2}`);
|
|
1666
|
+
if (query) {
|
|
1667
|
+
Object.entries(query).forEach(([key, value]) => {
|
|
1668
|
+
if (value !== void 0 && value !== null) {
|
|
1669
|
+
url.searchParams.set(key, String(value));
|
|
1670
|
+
}
|
|
1671
|
+
});
|
|
1672
|
+
}
|
|
1673
|
+
return url.toString();
|
|
1674
|
+
}
|
|
1675
|
+
async request(path2, options = {}) {
|
|
1676
|
+
const url = this.buildUrl(path2, options.query);
|
|
1677
|
+
const headers = new Headers({ "Content-Type": "application/json" });
|
|
1678
|
+
if (options.headers) {
|
|
1679
|
+
Object.entries(options.headers).forEach(([k, v]) => {
|
|
1680
|
+
if (v !== void 0) headers.set(k, v);
|
|
1681
|
+
});
|
|
1682
|
+
}
|
|
1683
|
+
const response = await this.fetchImpl(url, { ...options, headers });
|
|
1684
|
+
if (response.ok) {
|
|
1685
|
+
const contentType = response.headers.get("content-type") || "";
|
|
1686
|
+
if (contentType.includes("application/json")) {
|
|
1687
|
+
return await response.json();
|
|
1688
|
+
}
|
|
1689
|
+
return await response.text();
|
|
1690
|
+
}
|
|
1691
|
+
let body;
|
|
1692
|
+
const text = await response.text();
|
|
1693
|
+
try {
|
|
1694
|
+
body = JSON.parse(text);
|
|
1695
|
+
} catch {
|
|
1696
|
+
body = text;
|
|
1697
|
+
}
|
|
1698
|
+
const message = body?.error && typeof body.error === "string" ? body.error : body?.message && typeof body.message === "string" ? body.message : `Request failed with status ${response.status}`;
|
|
1699
|
+
throw new QueryError(message, "HTTP_ERROR", {
|
|
1700
|
+
status: response.status,
|
|
1701
|
+
body
|
|
1702
|
+
});
|
|
1703
|
+
}
|
|
1704
|
+
// ---------------------------------------------------------------------------
|
|
1705
|
+
// Query methods
|
|
1706
|
+
// ---------------------------------------------------------------------------
|
|
1707
|
+
/**
|
|
1708
|
+
* Execute a path query against the knowledge graph.
|
|
1709
|
+
*
|
|
1710
|
+
* @param pathQuery - The path query string (e.g., '"alice austen" -[*]{,4}-> type:person')
|
|
1711
|
+
* @param options - Query options (k, k_explore, lineage, enrich, etc.)
|
|
1712
|
+
* @returns Query results with entities, paths, and metadata
|
|
1713
|
+
*
|
|
1714
|
+
* @example
|
|
1715
|
+
* ```typescript
|
|
1716
|
+
* // Simple semantic search
|
|
1717
|
+
* const results = await query.path('"Washington" type:person');
|
|
1718
|
+
*
|
|
1719
|
+
* // Multi-hop traversal
|
|
1720
|
+
* const results = await query.path('"alice austen" -[*]{,4}-> type:person ~ "photographer"');
|
|
1721
|
+
*
|
|
1722
|
+
* // With lineage filtering (collection scope)
|
|
1723
|
+
* const results = await query.path('"letters" type:document', {
|
|
1724
|
+
* lineage: { sourcePi: 'arke:my_collection', direction: 'descendants' },
|
|
1725
|
+
* k: 10,
|
|
1726
|
+
* });
|
|
1727
|
+
* ```
|
|
1728
|
+
*/
|
|
1729
|
+
async path(pathQuery, options = {}) {
|
|
1730
|
+
return this.request("/query/path", {
|
|
1731
|
+
method: "POST",
|
|
1732
|
+
body: JSON.stringify({
|
|
1733
|
+
path: pathQuery,
|
|
1734
|
+
...options
|
|
1735
|
+
})
|
|
1736
|
+
});
|
|
1737
|
+
}
|
|
1738
|
+
/**
|
|
1739
|
+
* Execute a natural language query.
|
|
1740
|
+
*
|
|
1741
|
+
* The query is translated to a path query using an LLM, then executed.
|
|
1742
|
+
*
|
|
1743
|
+
* @param question - Natural language question
|
|
1744
|
+
* @param options - Query options including custom_instructions for the LLM
|
|
1745
|
+
* @returns Query results with translation info
|
|
1746
|
+
*
|
|
1747
|
+
* @example
|
|
1748
|
+
* ```typescript
|
|
1749
|
+
* const results = await query.natural('Find photographers connected to Alice Austen');
|
|
1750
|
+
* console.log('Generated query:', results.translation.path);
|
|
1751
|
+
* console.log('Explanation:', results.translation.explanation);
|
|
1752
|
+
* ```
|
|
1753
|
+
*/
|
|
1754
|
+
async natural(question, options = {}) {
|
|
1755
|
+
const { custom_instructions, ...queryOptions } = options;
|
|
1756
|
+
return this.request("/query/natural", {
|
|
1757
|
+
method: "POST",
|
|
1758
|
+
body: JSON.stringify({
|
|
1759
|
+
query: question,
|
|
1760
|
+
custom_instructions,
|
|
1761
|
+
...queryOptions
|
|
1762
|
+
})
|
|
1763
|
+
});
|
|
1764
|
+
}
|
|
1765
|
+
/**
|
|
1766
|
+
* Translate a natural language question to a path query without executing it.
|
|
1767
|
+
*
|
|
1768
|
+
* Useful for understanding how questions are translated or for manual execution later.
|
|
1769
|
+
*
|
|
1770
|
+
* @param question - Natural language question
|
|
1771
|
+
* @param customInstructions - Optional additional instructions for the LLM
|
|
1772
|
+
* @returns Translation result with path query and explanation
|
|
1773
|
+
*
|
|
1774
|
+
* @example
|
|
1775
|
+
* ```typescript
|
|
1776
|
+
* const result = await query.translate('Who wrote letters from Philadelphia?');
|
|
1777
|
+
* console.log('Path query:', result.path);
|
|
1778
|
+
* // '"letters" <-[authored, wrote]- type:person -[located]-> "Philadelphia"'
|
|
1779
|
+
* ```
|
|
1780
|
+
*/
|
|
1781
|
+
async translate(question, customInstructions) {
|
|
1782
|
+
return this.request("/query/translate", {
|
|
1783
|
+
method: "POST",
|
|
1784
|
+
body: JSON.stringify({
|
|
1785
|
+
query: question,
|
|
1786
|
+
custom_instructions: customInstructions
|
|
1787
|
+
})
|
|
1788
|
+
});
|
|
1789
|
+
}
|
|
1790
|
+
/**
|
|
1791
|
+
* Parse and validate a path query without executing it.
|
|
1792
|
+
*
|
|
1793
|
+
* Returns the AST (Abstract Syntax Tree) if valid, or throws an error.
|
|
1794
|
+
*
|
|
1795
|
+
* @param pathQuery - The path query to parse
|
|
1796
|
+
* @returns Parsed AST
|
|
1797
|
+
* @throws QueryError if the query has syntax errors
|
|
1798
|
+
*
|
|
1799
|
+
* @example
|
|
1800
|
+
* ```typescript
|
|
1801
|
+
* try {
|
|
1802
|
+
* const result = await query.parse('"test" -[*]-> type:person');
|
|
1803
|
+
* console.log('Valid query, AST:', result.ast);
|
|
1804
|
+
* } catch (err) {
|
|
1805
|
+
* console.error('Invalid query:', err.message);
|
|
1806
|
+
* }
|
|
1807
|
+
* ```
|
|
1808
|
+
*/
|
|
1809
|
+
async parse(pathQuery) {
|
|
1810
|
+
const url = this.buildUrl("/query/parse", { path: pathQuery });
|
|
1811
|
+
const response = await this.fetchImpl(url, {
|
|
1812
|
+
method: "GET",
|
|
1813
|
+
headers: { "Content-Type": "application/json" }
|
|
1814
|
+
});
|
|
1815
|
+
const body = await response.json();
|
|
1816
|
+
if ("error" in body && body.error === "Parse error") {
|
|
1817
|
+
throw new QueryError(
|
|
1818
|
+
body.message,
|
|
1819
|
+
"PARSE_ERROR",
|
|
1820
|
+
{ position: body.position }
|
|
1821
|
+
);
|
|
1822
|
+
}
|
|
1823
|
+
if (!response.ok) {
|
|
1824
|
+
throw new QueryError(
|
|
1825
|
+
body.error || `Request failed with status ${response.status}`,
|
|
1826
|
+
"HTTP_ERROR",
|
|
1827
|
+
{ status: response.status, body }
|
|
1828
|
+
);
|
|
1829
|
+
}
|
|
1830
|
+
return body;
|
|
1831
|
+
}
|
|
1832
|
+
/**
|
|
1833
|
+
* Get the path query syntax documentation.
|
|
1834
|
+
*
|
|
1835
|
+
* Returns comprehensive documentation including entry points, edge traversal,
|
|
1836
|
+
* filters, examples, and constraints.
|
|
1837
|
+
*
|
|
1838
|
+
* @returns Syntax documentation
|
|
1839
|
+
*
|
|
1840
|
+
* @example
|
|
1841
|
+
* ```typescript
|
|
1842
|
+
* const syntax = await query.syntax();
|
|
1843
|
+
*
|
|
1844
|
+
* // List all entry point types
|
|
1845
|
+
* syntax.entryPoints.types.forEach(ep => {
|
|
1846
|
+
* console.log(`${ep.syntax} - ${ep.description}`);
|
|
1847
|
+
* });
|
|
1848
|
+
*
|
|
1849
|
+
* // Show examples
|
|
1850
|
+
* syntax.examples.forEach(ex => {
|
|
1851
|
+
* console.log(`${ex.description}: ${ex.query}`);
|
|
1852
|
+
* });
|
|
1853
|
+
* ```
|
|
1854
|
+
*/
|
|
1855
|
+
async syntax() {
|
|
1856
|
+
return this.request("/query/syntax", {
|
|
1857
|
+
method: "GET"
|
|
1858
|
+
});
|
|
1859
|
+
}
|
|
1860
|
+
/**
|
|
1861
|
+
* Check the health of the query service.
|
|
1862
|
+
*
|
|
1863
|
+
* @returns Health status
|
|
1864
|
+
*/
|
|
1865
|
+
async health() {
|
|
1866
|
+
return this.request("/query/health", { method: "GET" });
|
|
1867
|
+
}
|
|
1868
|
+
/**
|
|
1869
|
+
* Search for collections by semantic similarity.
|
|
1870
|
+
*
|
|
1871
|
+
* Searches the dedicated collections index for fast semantic matching.
|
|
1872
|
+
*
|
|
1873
|
+
* @param query - Search query text
|
|
1874
|
+
* @param options - Search options (limit, visibility filter)
|
|
1875
|
+
* @returns Matching collections with similarity scores
|
|
1876
|
+
*
|
|
1877
|
+
* @example
|
|
1878
|
+
* ```typescript
|
|
1879
|
+
* // Search for photography-related collections
|
|
1880
|
+
* const results = await query.searchCollections('photography');
|
|
1881
|
+
* console.log(results.collections[0].title);
|
|
1882
|
+
*
|
|
1883
|
+
* // Search only public collections
|
|
1884
|
+
* const publicResults = await query.searchCollections('history', {
|
|
1885
|
+
* visibility: 'public',
|
|
1886
|
+
* limit: 20,
|
|
1887
|
+
* });
|
|
1888
|
+
* ```
|
|
1889
|
+
*/
|
|
1890
|
+
async searchCollections(query, options = {}) {
|
|
1891
|
+
return this.request("/query/search/collections", {
|
|
1892
|
+
method: "GET",
|
|
1893
|
+
query: {
|
|
1894
|
+
q: query,
|
|
1895
|
+
limit: options.limit?.toString(),
|
|
1896
|
+
visibility: options.visibility
|
|
1897
|
+
}
|
|
1898
|
+
});
|
|
1899
|
+
}
|
|
1900
|
+
};
|
|
1901
|
+
|
|
1902
|
+
// src/edit/errors.ts
|
|
1903
|
+
var EditError = class extends Error {
|
|
1904
|
+
constructor(message, code2 = "UNKNOWN_ERROR", details) {
|
|
1905
|
+
super(message);
|
|
1906
|
+
this.code = code2;
|
|
1907
|
+
this.details = details;
|
|
1908
|
+
this.name = "EditError";
|
|
1909
|
+
}
|
|
1910
|
+
};
|
|
1911
|
+
var EntityNotFoundError = class extends EditError {
|
|
1912
|
+
constructor(pi) {
|
|
1913
|
+
super(`Entity not found: ${pi}`, "ENTITY_NOT_FOUND", { pi });
|
|
1914
|
+
this.name = "EntityNotFoundError";
|
|
1915
|
+
}
|
|
1916
|
+
};
|
|
1917
|
+
var CASConflictError = class extends EditError {
|
|
1918
|
+
constructor(pi, expectedTip, actualTip) {
|
|
1919
|
+
super(
|
|
1920
|
+
`CAS conflict: entity ${pi} was modified (expected ${expectedTip}, got ${actualTip})`,
|
|
1921
|
+
"CAS_CONFLICT",
|
|
1922
|
+
{ pi, expectedTip, actualTip }
|
|
1923
|
+
);
|
|
1924
|
+
this.name = "CASConflictError";
|
|
1925
|
+
}
|
|
1926
|
+
};
|
|
1927
|
+
var ReprocessError = class extends EditError {
|
|
1928
|
+
constructor(message, batchId) {
|
|
1929
|
+
super(message, "REPROCESS_ERROR", { batchId });
|
|
1930
|
+
this.name = "ReprocessError";
|
|
1931
|
+
}
|
|
1932
|
+
};
|
|
1933
|
+
var ValidationError2 = class extends EditError {
|
|
1934
|
+
constructor(message, field) {
|
|
1935
|
+
super(message, "VALIDATION_ERROR", { field });
|
|
1936
|
+
this.name = "ValidationError";
|
|
1937
|
+
}
|
|
1938
|
+
};
|
|
1939
|
+
var PermissionError = class extends EditError {
|
|
1940
|
+
constructor(message, pi) {
|
|
1941
|
+
super(message, "PERMISSION_DENIED", { pi });
|
|
1942
|
+
this.name = "PermissionError";
|
|
1943
|
+
}
|
|
1944
|
+
};
|
|
1945
|
+
|
|
1946
|
+
// src/edit/client.ts
|
|
1947
|
+
var DEFAULT_RETRY_OPTIONS = {
|
|
1948
|
+
maxRetries: 5,
|
|
1949
|
+
initialDelayMs: 2e3,
|
|
1950
|
+
// Start with 2s delay (orchestrator needs time to initialize)
|
|
1951
|
+
maxDelayMs: 3e4,
|
|
1952
|
+
// Cap at 30s
|
|
1953
|
+
backoffMultiplier: 2
|
|
1954
|
+
// Double each retry
|
|
1955
|
+
};
|
|
1956
|
+
var EditClient = class {
|
|
1957
|
+
constructor(config) {
|
|
1958
|
+
this.gatewayUrl = config.gatewayUrl.replace(/\/$/, "");
|
|
1959
|
+
this.authToken = config.authToken;
|
|
1960
|
+
this.statusUrlTransform = config.statusUrlTransform;
|
|
1961
|
+
}
|
|
1962
|
+
/**
|
|
1963
|
+
* Update the auth token (useful for token refresh)
|
|
1964
|
+
*/
|
|
1965
|
+
setAuthToken(token) {
|
|
1966
|
+
this.authToken = token;
|
|
1967
|
+
}
|
|
1968
|
+
/**
|
|
1969
|
+
* Sleep for a given number of milliseconds
|
|
1970
|
+
*/
|
|
1971
|
+
sleep(ms) {
|
|
1972
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
1973
|
+
}
|
|
1974
|
+
/**
|
|
1975
|
+
* Execute a fetch with exponential backoff retry on transient errors
|
|
1976
|
+
*/
|
|
1977
|
+
async fetchWithRetry(url, options, retryOptions = DEFAULT_RETRY_OPTIONS) {
|
|
1978
|
+
let lastError = null;
|
|
1979
|
+
let delay = retryOptions.initialDelayMs;
|
|
1980
|
+
for (let attempt = 0; attempt <= retryOptions.maxRetries; attempt++) {
|
|
1981
|
+
try {
|
|
1982
|
+
const response = await fetch(url, options);
|
|
1983
|
+
if (response.status >= 500 && attempt < retryOptions.maxRetries) {
|
|
1984
|
+
lastError = new Error(`Server error: ${response.status} ${response.statusText}`);
|
|
1985
|
+
await this.sleep(delay);
|
|
1986
|
+
delay = Math.min(delay * retryOptions.backoffMultiplier, retryOptions.maxDelayMs);
|
|
1987
|
+
continue;
|
|
1988
|
+
}
|
|
1989
|
+
return response;
|
|
1990
|
+
} catch (error) {
|
|
1991
|
+
lastError = error;
|
|
1992
|
+
if (attempt < retryOptions.maxRetries) {
|
|
1993
|
+
await this.sleep(delay);
|
|
1994
|
+
delay = Math.min(delay * retryOptions.backoffMultiplier, retryOptions.maxDelayMs);
|
|
1995
|
+
}
|
|
1996
|
+
}
|
|
1997
|
+
}
|
|
1998
|
+
throw lastError || new Error("Request failed after retries");
|
|
1999
|
+
}
|
|
2000
|
+
getHeaders() {
|
|
2001
|
+
const headers = {
|
|
2002
|
+
"Content-Type": "application/json"
|
|
2003
|
+
};
|
|
2004
|
+
if (this.authToken) {
|
|
2005
|
+
headers["Authorization"] = `Bearer ${this.authToken}`;
|
|
2006
|
+
}
|
|
2007
|
+
return headers;
|
|
2008
|
+
}
|
|
2009
|
+
/**
|
|
2010
|
+
* Handle common error responses
|
|
2011
|
+
*/
|
|
2012
|
+
handleErrorResponse(response, context) {
|
|
2013
|
+
if (response.status === 403) {
|
|
2014
|
+
throw new PermissionError(`Permission denied: ${context}`);
|
|
2015
|
+
}
|
|
2016
|
+
throw new EditError(
|
|
2017
|
+
`${context}: ${response.statusText}`,
|
|
2018
|
+
"API_ERROR",
|
|
2019
|
+
{ status: response.status }
|
|
2020
|
+
);
|
|
2021
|
+
}
|
|
2022
|
+
// ===========================================================================
|
|
2023
|
+
// IPFS Wrapper Operations (via /api/*)
|
|
2024
|
+
// ===========================================================================
|
|
2025
|
+
/**
|
|
2026
|
+
* Fetch an entity by PI
|
|
2027
|
+
*/
|
|
2028
|
+
async getEntity(pi) {
|
|
2029
|
+
const response = await fetch(`${this.gatewayUrl}/api/entities/${pi}`, {
|
|
2030
|
+
headers: this.getHeaders()
|
|
2031
|
+
});
|
|
2032
|
+
if (response.status === 404) {
|
|
2033
|
+
throw new EntityNotFoundError(pi);
|
|
2034
|
+
}
|
|
2035
|
+
if (!response.ok) {
|
|
2036
|
+
this.handleErrorResponse(response, `Failed to fetch entity ${pi}`);
|
|
2037
|
+
}
|
|
2038
|
+
return response.json();
|
|
2039
|
+
}
|
|
2040
|
+
/**
|
|
2041
|
+
* Fetch content by CID
|
|
2042
|
+
*/
|
|
2043
|
+
async getContent(cid) {
|
|
2044
|
+
const response = await fetch(`${this.gatewayUrl}/api/cat/${cid}`, {
|
|
2045
|
+
headers: this.getHeaders()
|
|
2046
|
+
});
|
|
2047
|
+
if (!response.ok) {
|
|
2048
|
+
this.handleErrorResponse(response, `Failed to fetch content ${cid}`);
|
|
2049
|
+
}
|
|
2050
|
+
return response.text();
|
|
2051
|
+
}
|
|
2052
|
+
/**
|
|
2053
|
+
* Upload content and get CID
|
|
2054
|
+
*/
|
|
2055
|
+
async uploadContent(content, filename) {
|
|
2056
|
+
const formData = new FormData();
|
|
2057
|
+
const blob = new Blob([content], { type: "text/plain" });
|
|
2058
|
+
formData.append("file", blob, filename);
|
|
2059
|
+
const headers = {};
|
|
2060
|
+
if (this.authToken) {
|
|
2061
|
+
headers["Authorization"] = `Bearer ${this.authToken}`;
|
|
2062
|
+
}
|
|
2063
|
+
const response = await fetch(`${this.gatewayUrl}/api/upload`, {
|
|
2064
|
+
method: "POST",
|
|
2065
|
+
headers,
|
|
2066
|
+
body: formData
|
|
2067
|
+
});
|
|
2068
|
+
if (!response.ok) {
|
|
2069
|
+
this.handleErrorResponse(response, "Failed to upload content");
|
|
2070
|
+
}
|
|
2071
|
+
const result = await response.json();
|
|
2072
|
+
return result[0].cid;
|
|
2073
|
+
}
|
|
2074
|
+
/**
|
|
2075
|
+
* Update an entity with new components
|
|
2076
|
+
*/
|
|
2077
|
+
async updateEntity(pi, update) {
|
|
2078
|
+
const response = await fetch(`${this.gatewayUrl}/api/entities/${pi}/versions`, {
|
|
2079
|
+
method: "POST",
|
|
2080
|
+
headers: this.getHeaders(),
|
|
2081
|
+
body: JSON.stringify({
|
|
2082
|
+
expect_tip: update.expect_tip,
|
|
2083
|
+
components: update.components,
|
|
2084
|
+
components_remove: update.components_remove,
|
|
2085
|
+
note: update.note
|
|
2086
|
+
})
|
|
2087
|
+
});
|
|
2088
|
+
if (response.status === 409) {
|
|
2089
|
+
const entity = await this.getEntity(pi);
|
|
2090
|
+
throw new CASConflictError(
|
|
2091
|
+
pi,
|
|
2092
|
+
update.expect_tip,
|
|
2093
|
+
entity.manifest_cid
|
|
2094
|
+
);
|
|
2095
|
+
}
|
|
2096
|
+
if (!response.ok) {
|
|
2097
|
+
this.handleErrorResponse(response, `Failed to update entity ${pi}`);
|
|
2098
|
+
}
|
|
2099
|
+
return response.json();
|
|
2100
|
+
}
|
|
2101
|
+
// ===========================================================================
|
|
2102
|
+
// Reprocess API Operations (via /reprocess/*)
|
|
2103
|
+
// ===========================================================================
|
|
2104
|
+
/**
|
|
2105
|
+
* Trigger reprocessing for an entity
|
|
2106
|
+
*/
|
|
2107
|
+
async reprocess(request) {
|
|
2108
|
+
const response = await fetch(`${this.gatewayUrl}/reprocess/reprocess`, {
|
|
2109
|
+
method: "POST",
|
|
2110
|
+
headers: this.getHeaders(),
|
|
2111
|
+
body: JSON.stringify({
|
|
2112
|
+
pi: request.pi,
|
|
2113
|
+
phases: request.phases,
|
|
2114
|
+
cascade: request.cascade,
|
|
2115
|
+
options: request.options
|
|
2116
|
+
})
|
|
2117
|
+
});
|
|
2118
|
+
if (response.status === 403) {
|
|
2119
|
+
const error = await response.json().catch(() => ({}));
|
|
2120
|
+
throw new PermissionError(
|
|
2121
|
+
error.message || `Permission denied to reprocess ${request.pi}`,
|
|
2122
|
+
request.pi
|
|
2123
|
+
);
|
|
2124
|
+
}
|
|
2125
|
+
if (!response.ok) {
|
|
2126
|
+
const error = await response.json().catch(() => ({}));
|
|
2127
|
+
throw new ReprocessError(
|
|
2128
|
+
error.message || `Reprocess failed: ${response.statusText}`,
|
|
2129
|
+
void 0
|
|
2130
|
+
);
|
|
2131
|
+
}
|
|
2132
|
+
return response.json();
|
|
2133
|
+
}
|
|
2134
|
+
/**
|
|
2135
|
+
* Get reprocessing status by batch ID
|
|
2136
|
+
*
|
|
2137
|
+
* Uses exponential backoff retry to handle transient 500 errors
|
|
2138
|
+
* that occur when the orchestrator is initializing.
|
|
2139
|
+
*
|
|
2140
|
+
* @param statusUrl - The status URL returned from reprocess()
|
|
2141
|
+
* @param isFirstPoll - If true, uses a longer initial delay (orchestrator warmup)
|
|
2142
|
+
*/
|
|
2143
|
+
async getReprocessStatus(statusUrl, isFirstPoll = false) {
|
|
2144
|
+
const retryOptions = isFirstPoll ? { ...DEFAULT_RETRY_OPTIONS, initialDelayMs: 3e3 } : DEFAULT_RETRY_OPTIONS;
|
|
2145
|
+
const fetchUrl = this.statusUrlTransform ? this.statusUrlTransform(statusUrl) : statusUrl;
|
|
2146
|
+
const response = await this.fetchWithRetry(
|
|
2147
|
+
fetchUrl,
|
|
2148
|
+
{ headers: this.getHeaders() },
|
|
2149
|
+
retryOptions
|
|
2150
|
+
);
|
|
2151
|
+
if (!response.ok) {
|
|
2152
|
+
throw new EditError(
|
|
2153
|
+
`Failed to fetch reprocess status: ${response.statusText}`,
|
|
2154
|
+
"STATUS_ERROR",
|
|
2155
|
+
{ status: response.status }
|
|
2156
|
+
);
|
|
2157
|
+
}
|
|
2158
|
+
return response.json();
|
|
2159
|
+
}
|
|
2160
|
+
};
|
|
2161
|
+
|
|
2162
|
+
// src/edit/diff.ts
|
|
2163
|
+
var Diff = __toESM(require("diff"), 1);
|
|
2164
|
+
var DiffEngine = class {
|
|
2165
|
+
/**
|
|
2166
|
+
* Compute diff between two strings
|
|
2167
|
+
*/
|
|
2168
|
+
static diff(original, modified) {
|
|
2169
|
+
const changes = Diff.diffLines(original, modified);
|
|
2170
|
+
const diffs = [];
|
|
2171
|
+
let lineNumber = 1;
|
|
2172
|
+
for (const change of changes) {
|
|
2173
|
+
if (change.added) {
|
|
2174
|
+
diffs.push({
|
|
2175
|
+
type: "addition",
|
|
2176
|
+
modified: change.value.trimEnd(),
|
|
2177
|
+
lineNumber
|
|
2178
|
+
});
|
|
2179
|
+
} else if (change.removed) {
|
|
2180
|
+
diffs.push({
|
|
2181
|
+
type: "deletion",
|
|
2182
|
+
original: change.value.trimEnd(),
|
|
2183
|
+
lineNumber
|
|
2184
|
+
});
|
|
2185
|
+
} else {
|
|
2186
|
+
const lines = change.value.split("\n").length - 1;
|
|
2187
|
+
lineNumber += lines;
|
|
2188
|
+
}
|
|
2189
|
+
if (change.added) {
|
|
2190
|
+
lineNumber += change.value.split("\n").length - 1;
|
|
2191
|
+
}
|
|
2192
|
+
}
|
|
2193
|
+
return diffs;
|
|
2194
|
+
}
|
|
2195
|
+
/**
|
|
2196
|
+
* Compute word-level diff for more granular changes
|
|
2197
|
+
*/
|
|
2198
|
+
static diffWords(original, modified) {
|
|
2199
|
+
const changes = Diff.diffWords(original, modified);
|
|
2200
|
+
const diffs = [];
|
|
2201
|
+
for (const change of changes) {
|
|
2202
|
+
if (change.added) {
|
|
2203
|
+
diffs.push({
|
|
2204
|
+
type: "addition",
|
|
2205
|
+
modified: change.value
|
|
2206
|
+
});
|
|
2207
|
+
} else if (change.removed) {
|
|
2208
|
+
diffs.push({
|
|
2209
|
+
type: "deletion",
|
|
2210
|
+
original: change.value
|
|
2211
|
+
});
|
|
2212
|
+
}
|
|
2213
|
+
}
|
|
2214
|
+
return diffs;
|
|
2215
|
+
}
|
|
2216
|
+
/**
|
|
2217
|
+
* Create a ComponentDiff from original and modified content
|
|
2218
|
+
*/
|
|
2219
|
+
static createComponentDiff(componentName, original, modified) {
|
|
2220
|
+
const diffs = this.diff(original, modified);
|
|
2221
|
+
const hasChanges = diffs.length > 0;
|
|
2222
|
+
let summary;
|
|
2223
|
+
if (!hasChanges) {
|
|
2224
|
+
summary = "No changes";
|
|
2225
|
+
} else {
|
|
2226
|
+
const additions = diffs.filter((d) => d.type === "addition").length;
|
|
2227
|
+
const deletions = diffs.filter((d) => d.type === "deletion").length;
|
|
2228
|
+
const parts = [];
|
|
2229
|
+
if (additions > 0) parts.push(`${additions} addition${additions > 1 ? "s" : ""}`);
|
|
2230
|
+
if (deletions > 0) parts.push(`${deletions} deletion${deletions > 1 ? "s" : ""}`);
|
|
2231
|
+
summary = parts.join(", ");
|
|
2232
|
+
}
|
|
2233
|
+
return {
|
|
2234
|
+
componentName,
|
|
2235
|
+
diffs,
|
|
2236
|
+
summary,
|
|
2237
|
+
hasChanges
|
|
2238
|
+
};
|
|
2239
|
+
}
|
|
2240
|
+
/**
|
|
2241
|
+
* Format diffs for AI prompt consumption
|
|
2242
|
+
*/
|
|
2243
|
+
static formatForPrompt(diffs) {
|
|
2244
|
+
if (diffs.length === 0) {
|
|
2245
|
+
return "No changes detected.";
|
|
2246
|
+
}
|
|
2247
|
+
const lines = [];
|
|
2248
|
+
for (const diff of diffs) {
|
|
2249
|
+
const linePrefix = diff.lineNumber ? `Line ${diff.lineNumber}: ` : "";
|
|
2250
|
+
if (diff.type === "addition") {
|
|
2251
|
+
lines.push(`${linePrefix}+ ${diff.modified}`);
|
|
2252
|
+
} else if (diff.type === "deletion") {
|
|
2253
|
+
lines.push(`${linePrefix}- ${diff.original}`);
|
|
2254
|
+
} else if (diff.type === "change") {
|
|
2255
|
+
lines.push(`${linePrefix}"${diff.original}" \u2192 "${diff.modified}"`);
|
|
2256
|
+
}
|
|
2257
|
+
}
|
|
2258
|
+
return lines.join("\n");
|
|
2259
|
+
}
|
|
2260
|
+
/**
|
|
2261
|
+
* Format component diffs for AI prompt
|
|
2262
|
+
*/
|
|
2263
|
+
static formatComponentDiffsForPrompt(componentDiffs) {
|
|
2264
|
+
const sections = [];
|
|
2265
|
+
for (const cd of componentDiffs) {
|
|
2266
|
+
if (!cd.hasChanges) continue;
|
|
2267
|
+
sections.push(`## Changes to ${cd.componentName}:`);
|
|
2268
|
+
sections.push(this.formatForPrompt(cd.diffs));
|
|
2269
|
+
sections.push("");
|
|
2270
|
+
}
|
|
2271
|
+
return sections.join("\n");
|
|
2272
|
+
}
|
|
2273
|
+
/**
|
|
2274
|
+
* Create a unified diff view
|
|
2275
|
+
*/
|
|
2276
|
+
static unifiedDiff(original, modified, options) {
|
|
2277
|
+
const filename = options?.filename || "content";
|
|
2278
|
+
const patch = Diff.createPatch(filename, original, modified, "", "", {
|
|
2279
|
+
context: options?.context ?? 3
|
|
2280
|
+
});
|
|
2281
|
+
return patch;
|
|
2282
|
+
}
|
|
2283
|
+
/**
|
|
2284
|
+
* Extract corrections from diffs (specific text replacements)
|
|
2285
|
+
*/
|
|
2286
|
+
static extractCorrections(original, modified, sourceFile) {
|
|
2287
|
+
const wordDiffs = Diff.diffWords(original, modified);
|
|
2288
|
+
const corrections = [];
|
|
2289
|
+
let i = 0;
|
|
2290
|
+
while (i < wordDiffs.length) {
|
|
2291
|
+
const current = wordDiffs[i];
|
|
2292
|
+
if (current.removed && i + 1 < wordDiffs.length && wordDiffs[i + 1].added) {
|
|
2293
|
+
const removed = current.value.trim();
|
|
2294
|
+
const added = wordDiffs[i + 1].value.trim();
|
|
2295
|
+
if (removed && added && removed !== added) {
|
|
2296
|
+
corrections.push({
|
|
2297
|
+
original: removed,
|
|
2298
|
+
corrected: added,
|
|
2299
|
+
sourceFile
|
|
2300
|
+
});
|
|
2301
|
+
}
|
|
2302
|
+
i += 2;
|
|
2303
|
+
} else {
|
|
2304
|
+
i++;
|
|
2305
|
+
}
|
|
2306
|
+
}
|
|
2307
|
+
return corrections;
|
|
2308
|
+
}
|
|
2309
|
+
/**
|
|
2310
|
+
* Check if two strings are meaningfully different
|
|
2311
|
+
* (ignoring whitespace differences)
|
|
2312
|
+
*/
|
|
2313
|
+
static hasSignificantChanges(original, modified) {
|
|
2314
|
+
const normalizedOriginal = original.replace(/\s+/g, " ").trim();
|
|
2315
|
+
const normalizedModified = modified.replace(/\s+/g, " ").trim();
|
|
2316
|
+
return normalizedOriginal !== normalizedModified;
|
|
2317
|
+
}
|
|
2318
|
+
};
|
|
2319
|
+
|
|
2320
|
+
// src/edit/prompts.ts
|
|
2321
|
+
var PromptBuilder = class {
|
|
2322
|
+
/**
|
|
2323
|
+
* Build prompt for AI-first mode (user provides instructions)
|
|
2324
|
+
*/
|
|
2325
|
+
static buildAIPrompt(userPrompt, component, entityContext, currentContent) {
|
|
2326
|
+
const sections = [];
|
|
2327
|
+
sections.push(`## Instructions for ${component}`);
|
|
2328
|
+
sections.push(userPrompt);
|
|
2329
|
+
sections.push("");
|
|
2330
|
+
sections.push("## Entity Context");
|
|
2331
|
+
sections.push(`- PI: ${entityContext.pi}`);
|
|
2332
|
+
sections.push(`- Current version: ${entityContext.ver}`);
|
|
2333
|
+
if (entityContext.parentPi) {
|
|
2334
|
+
sections.push(`- Parent: ${entityContext.parentPi}`);
|
|
2335
|
+
}
|
|
2336
|
+
if (entityContext.childrenCount > 0) {
|
|
2337
|
+
sections.push(`- Children: ${entityContext.childrenCount}`);
|
|
2338
|
+
}
|
|
2339
|
+
sections.push("");
|
|
2340
|
+
if (currentContent) {
|
|
2341
|
+
sections.push(`## Current ${component} content for reference:`);
|
|
2342
|
+
sections.push("```");
|
|
2343
|
+
sections.push(currentContent.slice(0, 2e3));
|
|
2344
|
+
if (currentContent.length > 2e3) {
|
|
2345
|
+
sections.push("... [truncated]");
|
|
2346
|
+
}
|
|
2347
|
+
sections.push("```");
|
|
2348
|
+
}
|
|
2349
|
+
return sections.join("\n");
|
|
2350
|
+
}
|
|
2351
|
+
/**
|
|
2352
|
+
* Build prompt incorporating manual edits and diffs
|
|
2353
|
+
*/
|
|
2354
|
+
static buildEditReviewPrompt(componentDiffs, corrections, component, userInstructions) {
|
|
2355
|
+
const sections = [];
|
|
2356
|
+
sections.push("## Manual Edits Made");
|
|
2357
|
+
sections.push("");
|
|
2358
|
+
sections.push("The following manual edits were made to this entity:");
|
|
2359
|
+
sections.push("");
|
|
2360
|
+
const diffContent = DiffEngine.formatComponentDiffsForPrompt(componentDiffs);
|
|
2361
|
+
if (diffContent) {
|
|
2362
|
+
sections.push(diffContent);
|
|
2363
|
+
}
|
|
2364
|
+
if (corrections.length > 0) {
|
|
2365
|
+
sections.push("## Corrections Identified");
|
|
2366
|
+
sections.push("");
|
|
2367
|
+
for (const correction of corrections) {
|
|
2368
|
+
const source = correction.sourceFile ? ` (in ${correction.sourceFile})` : "";
|
|
2369
|
+
sections.push(`- "${correction.original}" \u2192 "${correction.corrected}"${source}`);
|
|
2370
|
+
}
|
|
2371
|
+
sections.push("");
|
|
2372
|
+
}
|
|
2373
|
+
sections.push("## Instructions");
|
|
2374
|
+
if (userInstructions) {
|
|
2375
|
+
sections.push(userInstructions);
|
|
2376
|
+
} else {
|
|
2377
|
+
sections.push(
|
|
2378
|
+
`Update the ${component} to accurately reflect these changes. Ensure any corrections are incorporated and the content is consistent.`
|
|
2379
|
+
);
|
|
2380
|
+
}
|
|
2381
|
+
sections.push("");
|
|
2382
|
+
sections.push("## Guidance");
|
|
2383
|
+
switch (component) {
|
|
2384
|
+
case "pinax":
|
|
2385
|
+
sections.push(
|
|
2386
|
+
"Update metadata fields to reflect any corrections. Pay special attention to dates, names, and other factual information that may have been corrected."
|
|
2387
|
+
);
|
|
2388
|
+
break;
|
|
2389
|
+
case "description":
|
|
2390
|
+
sections.push(
|
|
2391
|
+
"Regenerate the description incorporating the changes. Maintain the overall tone and structure while ensuring accuracy based on the corrections."
|
|
2392
|
+
);
|
|
2393
|
+
break;
|
|
2394
|
+
case "cheimarros":
|
|
2395
|
+
sections.push(
|
|
2396
|
+
"Update the knowledge graph to reflect any new or corrected entities, relationships, and facts identified in the changes."
|
|
2397
|
+
);
|
|
2398
|
+
break;
|
|
2399
|
+
}
|
|
2400
|
+
return sections.join("\n");
|
|
2401
|
+
}
|
|
2402
|
+
/**
|
|
2403
|
+
* Build cascade-aware prompt additions
|
|
2404
|
+
*/
|
|
2405
|
+
static buildCascadePrompt(basePrompt, cascadeContext) {
|
|
2406
|
+
const sections = [basePrompt];
|
|
2407
|
+
sections.push("");
|
|
2408
|
+
sections.push("## Cascade Context");
|
|
2409
|
+
sections.push("");
|
|
2410
|
+
sections.push(
|
|
2411
|
+
"This edit is part of a cascading update. After updating this entity, parent entities will also be updated to reflect these changes."
|
|
2412
|
+
);
|
|
2413
|
+
sections.push("");
|
|
2414
|
+
if (cascadeContext.path.length > 1) {
|
|
2415
|
+
sections.push(`Cascade path: ${cascadeContext.path.join(" \u2192 ")}`);
|
|
2416
|
+
sections.push(`Depth: ${cascadeContext.depth}`);
|
|
2417
|
+
}
|
|
2418
|
+
if (cascadeContext.stopAtPi) {
|
|
2419
|
+
sections.push(`Cascade will stop at: ${cascadeContext.stopAtPi}`);
|
|
2420
|
+
}
|
|
2421
|
+
sections.push("");
|
|
2422
|
+
sections.push(
|
|
2423
|
+
"Ensure the content accurately represents the source material so parent aggregations will be correct."
|
|
2424
|
+
);
|
|
2425
|
+
return sections.join("\n");
|
|
2426
|
+
}
|
|
2427
|
+
/**
|
|
2428
|
+
* Build a general prompt combining multiple instructions
|
|
2429
|
+
*/
|
|
2430
|
+
static buildCombinedPrompt(generalPrompt, componentPrompt, component) {
|
|
2431
|
+
const sections = [];
|
|
2432
|
+
if (generalPrompt) {
|
|
2433
|
+
sections.push("## General Instructions");
|
|
2434
|
+
sections.push(generalPrompt);
|
|
2435
|
+
sections.push("");
|
|
2436
|
+
}
|
|
2437
|
+
if (componentPrompt) {
|
|
2438
|
+
sections.push(`## Specific Instructions for ${component}`);
|
|
2439
|
+
sections.push(componentPrompt);
|
|
2440
|
+
sections.push("");
|
|
2441
|
+
}
|
|
2442
|
+
if (sections.length === 0) {
|
|
2443
|
+
return `Regenerate the ${component} based on the current entity content.`;
|
|
2444
|
+
}
|
|
2445
|
+
return sections.join("\n");
|
|
2446
|
+
}
|
|
2447
|
+
/**
|
|
2448
|
+
* Build prompt for correction-based updates
|
|
2449
|
+
*/
|
|
2450
|
+
static buildCorrectionPrompt(corrections) {
|
|
2451
|
+
if (corrections.length === 0) {
|
|
2452
|
+
return "";
|
|
2453
|
+
}
|
|
2454
|
+
const sections = [];
|
|
2455
|
+
sections.push("## Corrections Applied");
|
|
2456
|
+
sections.push("");
|
|
2457
|
+
sections.push("The following corrections were made to the source content:");
|
|
2458
|
+
sections.push("");
|
|
2459
|
+
for (const correction of corrections) {
|
|
2460
|
+
const source = correction.sourceFile ? ` in ${correction.sourceFile}` : "";
|
|
2461
|
+
sections.push(`- "${correction.original}" was corrected to "${correction.corrected}"${source}`);
|
|
2462
|
+
if (correction.context) {
|
|
2463
|
+
sections.push(` Context: ${correction.context}`);
|
|
2464
|
+
}
|
|
2465
|
+
}
|
|
2466
|
+
sections.push("");
|
|
2467
|
+
sections.push(
|
|
2468
|
+
"Update the metadata and description to reflect these corrections. Previous content may have contained errors based on the incorrect text."
|
|
2469
|
+
);
|
|
2470
|
+
return sections.join("\n");
|
|
2471
|
+
}
|
|
2472
|
+
/**
|
|
2473
|
+
* Get component-specific regeneration guidance
|
|
2474
|
+
*/
|
|
2475
|
+
static getComponentGuidance(component) {
|
|
2476
|
+
switch (component) {
|
|
2477
|
+
case "pinax":
|
|
2478
|
+
return "Extract and structure metadata including: institution, creator, title, date range, subjects, type, and other relevant fields. Ensure accuracy based on the source content.";
|
|
2479
|
+
case "description":
|
|
2480
|
+
return "Generate a clear, informative description that summarizes the entity content. Focus on what the material contains, its historical significance, and context. Write for a general audience unless otherwise specified.";
|
|
2481
|
+
case "cheimarros":
|
|
2482
|
+
return "Extract entities (people, places, organizations, events) and their relationships. Build a knowledge graph that captures the key facts and connections in the content.";
|
|
2483
|
+
default:
|
|
2484
|
+
return "";
|
|
2485
|
+
}
|
|
2486
|
+
}
|
|
2487
|
+
};
|
|
2488
|
+
|
|
2489
|
+
// src/edit/session.ts
|
|
2490
|
+
var DEFAULT_SCOPE = {
|
|
2491
|
+
components: [],
|
|
2492
|
+
cascade: false
|
|
2493
|
+
};
|
|
2494
|
+
var DEFAULT_POLL_OPTIONS = {
|
|
2495
|
+
intervalMs: 2e3,
|
|
2496
|
+
timeoutMs: 3e5
|
|
2497
|
+
// 5 minutes
|
|
2498
|
+
};
|
|
2499
|
+
var EditSession = class {
|
|
2500
|
+
constructor(client, pi, config) {
|
|
2501
|
+
this.entity = null;
|
|
2502
|
+
this.loadedComponents = {};
|
|
2503
|
+
// AI mode state
|
|
2504
|
+
this.prompts = {};
|
|
2505
|
+
// Manual mode state
|
|
2506
|
+
this.editedContent = {};
|
|
2507
|
+
this.corrections = [];
|
|
2508
|
+
// Scope
|
|
2509
|
+
this.scope = { ...DEFAULT_SCOPE };
|
|
2510
|
+
// Execution state
|
|
2511
|
+
this.submitting = false;
|
|
2512
|
+
this.result = null;
|
|
2513
|
+
this.statusUrl = null;
|
|
2514
|
+
this.client = client;
|
|
2515
|
+
this.pi = pi;
|
|
2516
|
+
this.mode = config?.mode ?? "ai-prompt";
|
|
2517
|
+
this.aiReviewEnabled = config?.aiReviewEnabled ?? true;
|
|
2518
|
+
}
|
|
2519
|
+
// ===========================================================================
|
|
2520
|
+
// Loading
|
|
2521
|
+
// ===========================================================================
|
|
2522
|
+
/**
|
|
2523
|
+
* Load the entity and its key components
|
|
2524
|
+
*/
|
|
2525
|
+
async load() {
|
|
2526
|
+
this.entity = await this.client.getEntity(this.pi);
|
|
2527
|
+
const priorityComponents = ["description.md", "pinax.json", "cheimarros.json"];
|
|
2528
|
+
await Promise.all(
|
|
2529
|
+
priorityComponents.map(async (name) => {
|
|
2530
|
+
const cid = this.entity.components[name];
|
|
2531
|
+
if (cid) {
|
|
2532
|
+
try {
|
|
2533
|
+
this.loadedComponents[name] = await this.client.getContent(cid);
|
|
2534
|
+
} catch {
|
|
2535
|
+
}
|
|
2536
|
+
}
|
|
2537
|
+
})
|
|
2538
|
+
);
|
|
2539
|
+
}
|
|
2540
|
+
/**
|
|
2541
|
+
* Load a specific component on demand
|
|
2542
|
+
*/
|
|
2543
|
+
async loadComponent(name) {
|
|
2544
|
+
if (this.loadedComponents[name]) {
|
|
2545
|
+
return this.loadedComponents[name];
|
|
2546
|
+
}
|
|
2547
|
+
if (!this.entity) {
|
|
2548
|
+
throw new ValidationError2("Session not loaded");
|
|
2549
|
+
}
|
|
2550
|
+
const cid = this.entity.components[name];
|
|
2551
|
+
if (!cid) {
|
|
2552
|
+
return void 0;
|
|
2553
|
+
}
|
|
2554
|
+
const content = await this.client.getContent(cid);
|
|
2555
|
+
this.loadedComponents[name] = content;
|
|
2556
|
+
return content;
|
|
2557
|
+
}
|
|
2558
|
+
/**
|
|
2559
|
+
* Get the loaded entity
|
|
2560
|
+
*/
|
|
2561
|
+
getEntity() {
|
|
2562
|
+
if (!this.entity) {
|
|
2563
|
+
throw new ValidationError2("Session not loaded. Call load() first.");
|
|
2564
|
+
}
|
|
2565
|
+
return this.entity;
|
|
2566
|
+
}
|
|
2567
|
+
/**
|
|
2568
|
+
* Get loaded component content
|
|
2569
|
+
*/
|
|
2570
|
+
getComponents() {
|
|
2571
|
+
return { ...this.loadedComponents };
|
|
2572
|
+
}
|
|
2573
|
+
// ===========================================================================
|
|
2574
|
+
// AI Prompt Mode
|
|
2575
|
+
// ===========================================================================
|
|
2576
|
+
/**
|
|
2577
|
+
* Set a prompt for AI regeneration
|
|
2578
|
+
*/
|
|
2579
|
+
setPrompt(target, prompt) {
|
|
2580
|
+
if (this.mode === "manual-only") {
|
|
2581
|
+
throw new ValidationError2("Cannot set prompts in manual-only mode");
|
|
2582
|
+
}
|
|
2583
|
+
this.prompts[target] = prompt;
|
|
2584
|
+
}
|
|
2585
|
+
/**
|
|
2586
|
+
* Get all prompts
|
|
2587
|
+
*/
|
|
2588
|
+
getPrompts() {
|
|
2589
|
+
return { ...this.prompts };
|
|
2590
|
+
}
|
|
2591
|
+
/**
|
|
2592
|
+
* Clear a prompt
|
|
2593
|
+
*/
|
|
2594
|
+
clearPrompt(target) {
|
|
2595
|
+
delete this.prompts[target];
|
|
2596
|
+
}
|
|
2597
|
+
// ===========================================================================
|
|
2598
|
+
// Manual Edit Mode
|
|
2599
|
+
// ===========================================================================
|
|
2600
|
+
/**
|
|
2601
|
+
* Set edited content for a component
|
|
2602
|
+
*/
|
|
2603
|
+
setContent(componentName, content) {
|
|
2604
|
+
if (this.mode === "ai-prompt") {
|
|
2605
|
+
throw new ValidationError2("Cannot set content in ai-prompt mode");
|
|
2606
|
+
}
|
|
2607
|
+
this.editedContent[componentName] = content;
|
|
2608
|
+
}
|
|
2609
|
+
/**
|
|
2610
|
+
* Get all edited content
|
|
2611
|
+
*/
|
|
2612
|
+
getEditedContent() {
|
|
2613
|
+
return { ...this.editedContent };
|
|
2614
|
+
}
|
|
2615
|
+
/**
|
|
2616
|
+
* Clear edited content for a component
|
|
2617
|
+
*/
|
|
2618
|
+
clearContent(componentName) {
|
|
2619
|
+
delete this.editedContent[componentName];
|
|
2620
|
+
}
|
|
2621
|
+
/**
|
|
2622
|
+
* Add a correction (for OCR fixes, etc.)
|
|
2623
|
+
*/
|
|
2624
|
+
addCorrection(original, corrected, sourceFile) {
|
|
2625
|
+
this.corrections.push({ original, corrected, sourceFile });
|
|
2626
|
+
}
|
|
2627
|
+
/**
|
|
2628
|
+
* Get all corrections
|
|
2629
|
+
*/
|
|
2630
|
+
getCorrections() {
|
|
2631
|
+
return [...this.corrections];
|
|
2632
|
+
}
|
|
2633
|
+
/**
|
|
2634
|
+
* Clear corrections
|
|
2635
|
+
*/
|
|
2636
|
+
clearCorrections() {
|
|
2637
|
+
this.corrections = [];
|
|
2638
|
+
}
|
|
2639
|
+
// ===========================================================================
|
|
2640
|
+
// Scope Configuration
|
|
2641
|
+
// ===========================================================================
|
|
2642
|
+
/**
|
|
2643
|
+
* Set the edit scope
|
|
2644
|
+
*/
|
|
2645
|
+
setScope(scope) {
|
|
2646
|
+
this.scope = { ...this.scope, ...scope };
|
|
2647
|
+
}
|
|
2648
|
+
/**
|
|
2649
|
+
* Get the current scope
|
|
2650
|
+
*/
|
|
2651
|
+
getScope() {
|
|
2652
|
+
return { ...this.scope };
|
|
2653
|
+
}
|
|
2654
|
+
// ===========================================================================
|
|
2655
|
+
// Preview & Summary
|
|
2656
|
+
// ===========================================================================
|
|
2657
|
+
/**
|
|
2658
|
+
* Get diffs for manual changes
|
|
2659
|
+
*/
|
|
2660
|
+
getDiff() {
|
|
2661
|
+
const diffs = [];
|
|
2662
|
+
for (const [name, edited] of Object.entries(this.editedContent)) {
|
|
2663
|
+
const original = this.loadedComponents[name] || "";
|
|
2664
|
+
if (DiffEngine.hasSignificantChanges(original, edited)) {
|
|
2665
|
+
diffs.push(DiffEngine.createComponentDiff(name, original, edited));
|
|
2666
|
+
}
|
|
2667
|
+
}
|
|
2668
|
+
return diffs;
|
|
2669
|
+
}
|
|
2670
|
+
/**
|
|
2671
|
+
* Preview what prompts will be sent to AI
|
|
2672
|
+
*/
|
|
2673
|
+
previewPrompt() {
|
|
2674
|
+
const result = {};
|
|
2675
|
+
if (!this.entity) return result;
|
|
2676
|
+
const entityContext = {
|
|
2677
|
+
pi: this.entity.pi,
|
|
2678
|
+
ver: this.entity.ver,
|
|
2679
|
+
parentPi: this.entity.parent_pi,
|
|
2680
|
+
childrenCount: this.entity.children_pi.length,
|
|
2681
|
+
currentContent: this.loadedComponents
|
|
2682
|
+
};
|
|
2683
|
+
for (const component of this.scope.components) {
|
|
2684
|
+
let prompt;
|
|
2685
|
+
if (this.mode === "ai-prompt") {
|
|
2686
|
+
const componentPrompt = this.prompts[component];
|
|
2687
|
+
const generalPrompt = this.prompts["general"];
|
|
2688
|
+
const combined = PromptBuilder.buildCombinedPrompt(generalPrompt, componentPrompt, component);
|
|
2689
|
+
prompt = PromptBuilder.buildAIPrompt(
|
|
2690
|
+
combined,
|
|
2691
|
+
component,
|
|
2692
|
+
entityContext,
|
|
2693
|
+
this.loadedComponents[`${component}.json`] || this.loadedComponents[`${component}.md`]
|
|
2694
|
+
);
|
|
2695
|
+
} else {
|
|
2696
|
+
const diffs = this.getDiff();
|
|
2697
|
+
const userInstructions = this.prompts["general"] || this.prompts[component];
|
|
2698
|
+
prompt = PromptBuilder.buildEditReviewPrompt(diffs, this.corrections, component, userInstructions);
|
|
2699
|
+
}
|
|
2700
|
+
if (this.scope.cascade) {
|
|
2701
|
+
prompt = PromptBuilder.buildCascadePrompt(prompt, {
|
|
2702
|
+
path: [this.entity.pi, this.entity.parent_pi || "root"].filter(Boolean),
|
|
2703
|
+
depth: 0,
|
|
2704
|
+
stopAtPi: this.scope.stopAtPi
|
|
2705
|
+
});
|
|
2706
|
+
}
|
|
2707
|
+
result[component] = prompt;
|
|
2708
|
+
}
|
|
2709
|
+
return result;
|
|
2710
|
+
}
|
|
2711
|
+
/**
|
|
2712
|
+
* Get a summary of pending changes
|
|
2713
|
+
*/
|
|
2714
|
+
getChangeSummary() {
|
|
2715
|
+
const diffs = this.getDiff();
|
|
2716
|
+
const hasManualEdits = diffs.some((d) => d.hasChanges);
|
|
2717
|
+
return {
|
|
2718
|
+
mode: this.mode,
|
|
2719
|
+
hasManualEdits,
|
|
2720
|
+
editedComponents: Object.keys(this.editedContent),
|
|
2721
|
+
corrections: [...this.corrections],
|
|
2722
|
+
prompts: { ...this.prompts },
|
|
2723
|
+
scope: { ...this.scope },
|
|
2724
|
+
willRegenerate: [...this.scope.components],
|
|
2725
|
+
willCascade: this.scope.cascade,
|
|
2726
|
+
willSave: hasManualEdits,
|
|
2727
|
+
willReprocess: this.scope.components.length > 0
|
|
2728
|
+
};
|
|
2729
|
+
}
|
|
2730
|
+
// ===========================================================================
|
|
2731
|
+
// Execution
|
|
2732
|
+
// ===========================================================================
|
|
2733
|
+
/**
|
|
2734
|
+
* Submit changes (saves first if manual edits, then reprocesses)
|
|
2735
|
+
*/
|
|
2736
|
+
async submit(note) {
|
|
2737
|
+
if (this.submitting) {
|
|
2738
|
+
throw new ValidationError2("Submit already in progress");
|
|
2739
|
+
}
|
|
2740
|
+
if (!this.entity) {
|
|
2741
|
+
throw new ValidationError2("Session not loaded. Call load() first.");
|
|
2742
|
+
}
|
|
2743
|
+
this.submitting = true;
|
|
2744
|
+
this.result = {};
|
|
2745
|
+
try {
|
|
2746
|
+
const diffs = this.getDiff();
|
|
2747
|
+
const hasManualEdits = diffs.some((d) => d.hasChanges);
|
|
2748
|
+
if (hasManualEdits) {
|
|
2749
|
+
const componentUpdates = {};
|
|
2750
|
+
for (const [name, content] of Object.entries(this.editedContent)) {
|
|
2751
|
+
const original = this.loadedComponents[name] || "";
|
|
2752
|
+
if (DiffEngine.hasSignificantChanges(original, content)) {
|
|
2753
|
+
const cid = await this.client.uploadContent(content, name);
|
|
2754
|
+
componentUpdates[name] = cid;
|
|
2755
|
+
}
|
|
2756
|
+
}
|
|
2757
|
+
const version = await this.client.updateEntity(this.pi, {
|
|
2758
|
+
expect_tip: this.entity.manifest_cid,
|
|
2759
|
+
components: componentUpdates,
|
|
2760
|
+
note
|
|
2761
|
+
});
|
|
2762
|
+
this.result.saved = {
|
|
2763
|
+
pi: version.pi,
|
|
2764
|
+
newVersion: version.ver,
|
|
2765
|
+
newTip: version.tip
|
|
2766
|
+
};
|
|
2767
|
+
this.entity.manifest_cid = version.tip;
|
|
2768
|
+
this.entity.ver = version.ver;
|
|
2769
|
+
}
|
|
2770
|
+
if (this.scope.components.length > 0) {
|
|
2771
|
+
const customPrompts = this.buildCustomPrompts();
|
|
2772
|
+
const reprocessResult = await this.client.reprocess({
|
|
2773
|
+
pi: this.pi,
|
|
2774
|
+
phases: this.scope.components,
|
|
2775
|
+
cascade: this.scope.cascade,
|
|
2776
|
+
options: {
|
|
2777
|
+
stop_at_pi: this.scope.stopAtPi,
|
|
2778
|
+
custom_prompts: customPrompts,
|
|
2779
|
+
custom_note: note
|
|
2780
|
+
}
|
|
2781
|
+
});
|
|
2782
|
+
this.result.reprocess = reprocessResult;
|
|
2783
|
+
this.statusUrl = reprocessResult.status_url;
|
|
2784
|
+
}
|
|
2785
|
+
return this.result;
|
|
2786
|
+
} finally {
|
|
2787
|
+
this.submitting = false;
|
|
2788
|
+
}
|
|
2789
|
+
}
|
|
2790
|
+
/**
|
|
2791
|
+
* Wait for reprocessing to complete
|
|
2792
|
+
*/
|
|
2793
|
+
async waitForCompletion(options) {
|
|
2794
|
+
const opts = { ...DEFAULT_POLL_OPTIONS, ...options };
|
|
2795
|
+
if (!this.statusUrl) {
|
|
2796
|
+
return {
|
|
2797
|
+
phase: "complete",
|
|
2798
|
+
saveComplete: true
|
|
2799
|
+
};
|
|
2800
|
+
}
|
|
2801
|
+
const startTime = Date.now();
|
|
2802
|
+
let isFirstPoll = true;
|
|
2803
|
+
while (true) {
|
|
2804
|
+
const status = await this.client.getReprocessStatus(this.statusUrl, isFirstPoll);
|
|
2805
|
+
isFirstPoll = false;
|
|
2806
|
+
const editStatus = {
|
|
2807
|
+
phase: status.status === "DONE" ? "complete" : status.status === "ERROR" ? "error" : "reprocessing",
|
|
2808
|
+
saveComplete: true,
|
|
2809
|
+
reprocessStatus: status,
|
|
2810
|
+
error: status.error
|
|
2811
|
+
};
|
|
2812
|
+
if (opts.onProgress) {
|
|
2813
|
+
opts.onProgress(editStatus);
|
|
2814
|
+
}
|
|
2815
|
+
if (status.status === "DONE" || status.status === "ERROR") {
|
|
2816
|
+
return editStatus;
|
|
2817
|
+
}
|
|
2818
|
+
if (Date.now() - startTime > opts.timeoutMs) {
|
|
2819
|
+
return {
|
|
2820
|
+
phase: "error",
|
|
2821
|
+
saveComplete: true,
|
|
2822
|
+
reprocessStatus: status,
|
|
2823
|
+
error: "Timeout waiting for reprocessing to complete"
|
|
2824
|
+
};
|
|
2825
|
+
}
|
|
2826
|
+
await new Promise((resolve) => setTimeout(resolve, opts.intervalMs));
|
|
2827
|
+
}
|
|
2828
|
+
}
|
|
2829
|
+
/**
|
|
2830
|
+
* Get current status without waiting
|
|
2831
|
+
*/
|
|
2832
|
+
async getStatus() {
|
|
2833
|
+
if (!this.statusUrl) {
|
|
2834
|
+
return {
|
|
2835
|
+
phase: this.result?.saved ? "complete" : "idle",
|
|
2836
|
+
saveComplete: !!this.result?.saved
|
|
2837
|
+
};
|
|
2838
|
+
}
|
|
2839
|
+
const status = await this.client.getReprocessStatus(this.statusUrl);
|
|
2840
|
+
return {
|
|
2841
|
+
phase: status.status === "DONE" ? "complete" : status.status === "ERROR" ? "error" : "reprocessing",
|
|
2842
|
+
saveComplete: true,
|
|
2843
|
+
reprocessStatus: status,
|
|
2844
|
+
error: status.error
|
|
2845
|
+
};
|
|
2846
|
+
}
|
|
2847
|
+
// ===========================================================================
|
|
2848
|
+
// Private Helpers
|
|
2849
|
+
// ===========================================================================
|
|
2850
|
+
buildCustomPrompts() {
|
|
2851
|
+
const custom = {};
|
|
2852
|
+
if (this.mode === "ai-prompt") {
|
|
2853
|
+
if (this.prompts["general"]) custom.general = this.prompts["general"];
|
|
2854
|
+
if (this.prompts["pinax"]) custom.pinax = this.prompts["pinax"];
|
|
2855
|
+
if (this.prompts["description"]) custom.description = this.prompts["description"];
|
|
2856
|
+
if (this.prompts["cheimarros"]) custom.cheimarros = this.prompts["cheimarros"];
|
|
2857
|
+
} else {
|
|
2858
|
+
const diffs = this.getDiff();
|
|
2859
|
+
const diffContext = DiffEngine.formatComponentDiffsForPrompt(diffs);
|
|
2860
|
+
const correctionContext = PromptBuilder.buildCorrectionPrompt(this.corrections);
|
|
2861
|
+
const basePrompt = [diffContext, correctionContext, this.prompts["general"]].filter(Boolean).join("\n\n");
|
|
2862
|
+
if (basePrompt) {
|
|
2863
|
+
custom.general = basePrompt;
|
|
2864
|
+
}
|
|
2865
|
+
if (this.prompts["pinax"]) custom.pinax = this.prompts["pinax"];
|
|
2866
|
+
if (this.prompts["description"]) custom.description = this.prompts["description"];
|
|
2867
|
+
if (this.prompts["cheimarros"]) custom.cheimarros = this.prompts["cheimarros"];
|
|
2868
|
+
}
|
|
2869
|
+
return custom;
|
|
2870
|
+
}
|
|
2871
|
+
};
|
|
2872
|
+
|
|
2873
|
+
// src/content/errors.ts
|
|
2874
|
+
var ContentError = class extends Error {
|
|
2875
|
+
constructor(message, code2 = "CONTENT_ERROR", details) {
|
|
2876
|
+
super(message);
|
|
2877
|
+
this.code = code2;
|
|
2878
|
+
this.details = details;
|
|
2879
|
+
this.name = "ContentError";
|
|
2880
|
+
}
|
|
2881
|
+
};
|
|
2882
|
+
var EntityNotFoundError2 = class extends ContentError {
|
|
2883
|
+
constructor(pi) {
|
|
2884
|
+
super(`Entity not found: ${pi}`, "ENTITY_NOT_FOUND", { pi });
|
|
2885
|
+
this.name = "EntityNotFoundError";
|
|
2886
|
+
}
|
|
2887
|
+
};
|
|
2888
|
+
var ContentNotFoundError = class extends ContentError {
|
|
2889
|
+
constructor(cid) {
|
|
2890
|
+
super(`Content not found: ${cid}`, "CONTENT_NOT_FOUND", { cid });
|
|
2891
|
+
this.name = "ContentNotFoundError";
|
|
2892
|
+
}
|
|
2893
|
+
};
|
|
2894
|
+
var ComponentNotFoundError = class extends ContentError {
|
|
2895
|
+
constructor(pi, componentName) {
|
|
2896
|
+
super(
|
|
2897
|
+
`Component '${componentName}' not found on entity ${pi}`,
|
|
2898
|
+
"COMPONENT_NOT_FOUND",
|
|
2899
|
+
{ pi, componentName }
|
|
2900
|
+
);
|
|
2901
|
+
this.name = "ComponentNotFoundError";
|
|
2902
|
+
}
|
|
2903
|
+
};
|
|
2904
|
+
var VersionNotFoundError = class extends ContentError {
|
|
2905
|
+
constructor(pi, selector) {
|
|
2906
|
+
super(
|
|
2907
|
+
`Version not found: ${selector} for entity ${pi}`,
|
|
2908
|
+
"VERSION_NOT_FOUND",
|
|
2909
|
+
{ pi, selector }
|
|
2910
|
+
);
|
|
2911
|
+
this.name = "VersionNotFoundError";
|
|
2912
|
+
}
|
|
2913
|
+
};
|
|
2914
|
+
var NetworkError2 = class extends ContentError {
|
|
2915
|
+
constructor(message, statusCode) {
|
|
2916
|
+
super(message, "NETWORK_ERROR", { statusCode });
|
|
2917
|
+
this.statusCode = statusCode;
|
|
2918
|
+
this.name = "NetworkError";
|
|
2919
|
+
}
|
|
2920
|
+
};
|
|
2921
|
+
|
|
2922
|
+
// src/content/client.ts
|
|
2923
|
+
var ContentClient = class {
|
|
2924
|
+
constructor(config) {
|
|
2925
|
+
this.baseUrl = config.gatewayUrl.replace(/\/$/, "");
|
|
2926
|
+
this.fetchImpl = config.fetchImpl ?? fetch;
|
|
2927
|
+
}
|
|
2928
|
+
// ---------------------------------------------------------------------------
|
|
2929
|
+
// Request helpers
|
|
2930
|
+
// ---------------------------------------------------------------------------
|
|
2931
|
+
buildUrl(path2, query) {
|
|
2932
|
+
const url = new URL(`${this.baseUrl}${path2}`);
|
|
2933
|
+
if (query) {
|
|
2934
|
+
Object.entries(query).forEach(([key, value]) => {
|
|
2935
|
+
if (value !== void 0 && value !== null) {
|
|
2936
|
+
url.searchParams.set(key, String(value));
|
|
2937
|
+
}
|
|
2938
|
+
});
|
|
2939
|
+
}
|
|
2940
|
+
return url.toString();
|
|
2941
|
+
}
|
|
2942
|
+
async request(path2, options = {}) {
|
|
2943
|
+
const url = this.buildUrl(path2, options.query);
|
|
2944
|
+
const headers = new Headers({ "Content-Type": "application/json" });
|
|
2945
|
+
if (options.headers) {
|
|
2946
|
+
Object.entries(options.headers).forEach(([k, v]) => {
|
|
2947
|
+
if (v !== void 0) headers.set(k, v);
|
|
2948
|
+
});
|
|
2949
|
+
}
|
|
2950
|
+
let response;
|
|
2951
|
+
try {
|
|
2952
|
+
response = await this.fetchImpl(url, { ...options, headers });
|
|
2953
|
+
} catch (err) {
|
|
2954
|
+
throw new NetworkError2(
|
|
2955
|
+
err instanceof Error ? err.message : "Network request failed"
|
|
2956
|
+
);
|
|
2957
|
+
}
|
|
2958
|
+
if (response.ok) {
|
|
2959
|
+
const contentType = response.headers.get("content-type") || "";
|
|
2960
|
+
if (contentType.includes("application/json")) {
|
|
2961
|
+
return await response.json();
|
|
2962
|
+
}
|
|
2963
|
+
return await response.text();
|
|
2964
|
+
}
|
|
2965
|
+
let body;
|
|
2966
|
+
const text = await response.text();
|
|
2967
|
+
try {
|
|
2968
|
+
body = JSON.parse(text);
|
|
2969
|
+
} catch {
|
|
2970
|
+
body = text;
|
|
2971
|
+
}
|
|
2972
|
+
if (response.status === 404) {
|
|
2973
|
+
const errorCode = body?.error;
|
|
2974
|
+
if (errorCode === "NOT_FOUND" || errorCode === "ENTITY_NOT_FOUND") {
|
|
2975
|
+
throw new ContentError(
|
|
2976
|
+
body?.message || "Not found",
|
|
2977
|
+
"NOT_FOUND",
|
|
2978
|
+
body
|
|
2979
|
+
);
|
|
2980
|
+
}
|
|
2981
|
+
}
|
|
2982
|
+
const message = body?.error && typeof body.error === "string" ? body.error : body?.message && typeof body.message === "string" ? body.message : `Request failed with status ${response.status}`;
|
|
2983
|
+
throw new ContentError(message, "HTTP_ERROR", {
|
|
2984
|
+
status: response.status,
|
|
2985
|
+
body
|
|
2986
|
+
});
|
|
2987
|
+
}
|
|
2988
|
+
// ---------------------------------------------------------------------------
|
|
2989
|
+
// Entity Operations
|
|
2990
|
+
// ---------------------------------------------------------------------------
|
|
2991
|
+
/**
|
|
2992
|
+
* Get an entity by its Persistent Identifier (PI).
|
|
2993
|
+
*
|
|
2994
|
+
* @param pi - Persistent Identifier (ULID or test PI with II prefix)
|
|
2995
|
+
* @returns Full entity manifest
|
|
2996
|
+
* @throws EntityNotFoundError if the entity doesn't exist
|
|
2997
|
+
*
|
|
2998
|
+
* @example
|
|
2999
|
+
* ```typescript
|
|
3000
|
+
* const entity = await content.get('01K75HQQXNTDG7BBP7PS9AWYAN');
|
|
3001
|
+
* console.log('Version:', entity.ver);
|
|
3002
|
+
* console.log('Components:', Object.keys(entity.components));
|
|
3003
|
+
* ```
|
|
3004
|
+
*/
|
|
3005
|
+
async get(pi) {
|
|
3006
|
+
try {
|
|
3007
|
+
return await this.request(`/api/entities/${encodeURIComponent(pi)}`);
|
|
3008
|
+
} catch (err) {
|
|
3009
|
+
if (err instanceof ContentError && err.code === "NOT_FOUND") {
|
|
3010
|
+
throw new EntityNotFoundError2(pi);
|
|
3011
|
+
}
|
|
3012
|
+
throw err;
|
|
3013
|
+
}
|
|
3014
|
+
}
|
|
3015
|
+
/**
|
|
3016
|
+
* List entities with pagination.
|
|
3017
|
+
*
|
|
3018
|
+
* @param options - Pagination and metadata options
|
|
3019
|
+
* @returns Paginated list of entity summaries
|
|
3020
|
+
*
|
|
3021
|
+
* @example
|
|
3022
|
+
* ```typescript
|
|
3023
|
+
* // Get first page
|
|
3024
|
+
* const page1 = await content.list({ limit: 20, include_metadata: true });
|
|
3025
|
+
*
|
|
3026
|
+
* // Get next page
|
|
3027
|
+
* if (page1.next_cursor) {
|
|
3028
|
+
* const page2 = await content.list({ cursor: page1.next_cursor });
|
|
3029
|
+
* }
|
|
3030
|
+
* ```
|
|
3031
|
+
*/
|
|
3032
|
+
async list(options = {}) {
|
|
3033
|
+
return this.request("/api/entities", {
|
|
3034
|
+
query: {
|
|
3035
|
+
limit: options.limit,
|
|
3036
|
+
cursor: options.cursor,
|
|
3037
|
+
include_metadata: options.include_metadata
|
|
3038
|
+
}
|
|
3039
|
+
});
|
|
3040
|
+
}
|
|
3041
|
+
/**
|
|
3042
|
+
* Get version history for an entity.
|
|
3043
|
+
*
|
|
3044
|
+
* @param pi - Persistent Identifier
|
|
3045
|
+
* @param options - Pagination options
|
|
3046
|
+
* @returns Version history (newest first)
|
|
3047
|
+
*
|
|
3048
|
+
* @example
|
|
3049
|
+
* ```typescript
|
|
3050
|
+
* const history = await content.versions('01K75HQQXNTDG7BBP7PS9AWYAN');
|
|
3051
|
+
* console.log('Total versions:', history.items.length);
|
|
3052
|
+
* history.items.forEach(v => {
|
|
3053
|
+
* console.log(`v${v.ver}: ${v.ts} - ${v.note || 'no note'}`);
|
|
3054
|
+
* });
|
|
3055
|
+
* ```
|
|
3056
|
+
*/
|
|
3057
|
+
async versions(pi, options = {}) {
|
|
3058
|
+
try {
|
|
3059
|
+
return await this.request(
|
|
3060
|
+
`/api/entities/${encodeURIComponent(pi)}/versions`,
|
|
3061
|
+
{
|
|
3062
|
+
query: {
|
|
3063
|
+
limit: options.limit,
|
|
3064
|
+
cursor: options.cursor
|
|
3065
|
+
}
|
|
3066
|
+
}
|
|
3067
|
+
);
|
|
3068
|
+
} catch (err) {
|
|
3069
|
+
if (err instanceof ContentError && err.code === "NOT_FOUND") {
|
|
3070
|
+
throw new EntityNotFoundError2(pi);
|
|
3071
|
+
}
|
|
3072
|
+
throw err;
|
|
3073
|
+
}
|
|
3074
|
+
}
|
|
3075
|
+
/**
|
|
3076
|
+
* Get a specific version of an entity.
|
|
3077
|
+
*
|
|
3078
|
+
* @param pi - Persistent Identifier
|
|
3079
|
+
* @param selector - Version selector: 'ver:N' for version number or 'cid:...' for CID
|
|
3080
|
+
* @returns Entity manifest for the specified version
|
|
3081
|
+
*
|
|
3082
|
+
* @example
|
|
3083
|
+
* ```typescript
|
|
3084
|
+
* // Get version 2
|
|
3085
|
+
* const v2 = await content.getVersion('01K75HQQXNTDG7BBP7PS9AWYAN', 'ver:2');
|
|
3086
|
+
*
|
|
3087
|
+
* // Get by CID
|
|
3088
|
+
* const vByCid = await content.getVersion('01K75HQQXNTDG7BBP7PS9AWYAN', 'cid:bafybeih...');
|
|
3089
|
+
* ```
|
|
3090
|
+
*/
|
|
3091
|
+
async getVersion(pi, selector) {
|
|
3092
|
+
try {
|
|
3093
|
+
return await this.request(
|
|
3094
|
+
`/api/entities/${encodeURIComponent(pi)}/versions/${encodeURIComponent(selector)}`
|
|
3095
|
+
);
|
|
3096
|
+
} catch (err) {
|
|
3097
|
+
if (err instanceof ContentError && err.code === "NOT_FOUND") {
|
|
3098
|
+
throw new EntityNotFoundError2(pi);
|
|
3099
|
+
}
|
|
3100
|
+
throw err;
|
|
3101
|
+
}
|
|
3102
|
+
}
|
|
3103
|
+
/**
|
|
3104
|
+
* Resolve a PI to its tip CID (fast lookup without fetching manifest).
|
|
3105
|
+
*
|
|
3106
|
+
* @param pi - Persistent Identifier
|
|
3107
|
+
* @returns PI and tip CID
|
|
3108
|
+
*
|
|
3109
|
+
* @example
|
|
3110
|
+
* ```typescript
|
|
3111
|
+
* const { tip } = await content.resolve('01K75HQQXNTDG7BBP7PS9AWYAN');
|
|
3112
|
+
* console.log('Latest manifest CID:', tip);
|
|
3113
|
+
* ```
|
|
3114
|
+
*/
|
|
3115
|
+
async resolve(pi) {
|
|
3116
|
+
try {
|
|
3117
|
+
return await this.request(`/api/resolve/${encodeURIComponent(pi)}`);
|
|
3118
|
+
} catch (err) {
|
|
3119
|
+
if (err instanceof ContentError && err.code === "NOT_FOUND") {
|
|
3120
|
+
throw new EntityNotFoundError2(pi);
|
|
3121
|
+
}
|
|
3122
|
+
throw err;
|
|
3123
|
+
}
|
|
3124
|
+
}
|
|
3125
|
+
/**
|
|
3126
|
+
* Get the list of child PIs for an entity (fast, returns only PIs).
|
|
3127
|
+
*
|
|
3128
|
+
* @param pi - Persistent Identifier of parent entity
|
|
3129
|
+
* @returns Array of child PIs
|
|
3130
|
+
*
|
|
3131
|
+
* @example
|
|
3132
|
+
* ```typescript
|
|
3133
|
+
* const childPis = await content.children('01K75HQQXNTDG7BBP7PS9AWYAN');
|
|
3134
|
+
* console.log('Children:', childPis);
|
|
3135
|
+
* ```
|
|
3136
|
+
*/
|
|
3137
|
+
async children(pi) {
|
|
3138
|
+
const entity = await this.get(pi);
|
|
3139
|
+
return entity.children_pi || [];
|
|
3140
|
+
}
|
|
3141
|
+
/**
|
|
3142
|
+
* Get all child entities for a parent (fetches full entity for each child).
|
|
3143
|
+
*
|
|
3144
|
+
* @param pi - Persistent Identifier of parent entity
|
|
3145
|
+
* @returns Array of child entities
|
|
3146
|
+
*
|
|
3147
|
+
* @example
|
|
3148
|
+
* ```typescript
|
|
3149
|
+
* const childEntities = await content.childrenEntities('01K75HQQXNTDG7BBP7PS9AWYAN');
|
|
3150
|
+
* childEntities.forEach(child => {
|
|
3151
|
+
* console.log(`${child.pi}: v${child.ver}`);
|
|
3152
|
+
* });
|
|
3153
|
+
* ```
|
|
3154
|
+
*/
|
|
3155
|
+
async childrenEntities(pi) {
|
|
3156
|
+
const childPis = await this.children(pi);
|
|
3157
|
+
if (childPis.length === 0) {
|
|
3158
|
+
return [];
|
|
3159
|
+
}
|
|
3160
|
+
const results = await Promise.allSettled(
|
|
3161
|
+
childPis.map((childPi) => this.get(childPi))
|
|
3162
|
+
);
|
|
3163
|
+
return results.filter((r) => r.status === "fulfilled").map((r) => r.value);
|
|
3164
|
+
}
|
|
3165
|
+
/**
|
|
3166
|
+
* Get the Arke origin block (root of the archive tree).
|
|
3167
|
+
*
|
|
3168
|
+
* @returns Arke origin entity
|
|
3169
|
+
*
|
|
3170
|
+
* @example
|
|
3171
|
+
* ```typescript
|
|
3172
|
+
* const origin = await content.arke();
|
|
3173
|
+
* console.log('Arke origin:', origin.pi);
|
|
3174
|
+
* ```
|
|
3175
|
+
*/
|
|
3176
|
+
async arke() {
|
|
3177
|
+
return this.request("/api/arke");
|
|
3178
|
+
}
|
|
3179
|
+
// ---------------------------------------------------------------------------
|
|
3180
|
+
// Content Download
|
|
3181
|
+
// ---------------------------------------------------------------------------
|
|
3182
|
+
/**
|
|
3183
|
+
* Download content by CID.
|
|
3184
|
+
*
|
|
3185
|
+
* Returns Blob in browser environments, Buffer in Node.js.
|
|
3186
|
+
*
|
|
3187
|
+
* @param cid - Content Identifier
|
|
3188
|
+
* @returns Content as Blob (browser) or Buffer (Node)
|
|
3189
|
+
* @throws ContentNotFoundError if the content doesn't exist
|
|
3190
|
+
*
|
|
3191
|
+
* @example
|
|
3192
|
+
* ```typescript
|
|
3193
|
+
* const content = await client.download('bafybeih...');
|
|
3194
|
+
*
|
|
3195
|
+
* // In browser
|
|
3196
|
+
* const url = URL.createObjectURL(content as Blob);
|
|
3197
|
+
*
|
|
3198
|
+
* // In Node.js
|
|
3199
|
+
* fs.writeFileSync('output.bin', content as Buffer);
|
|
3200
|
+
* ```
|
|
3201
|
+
*/
|
|
3202
|
+
async download(cid) {
|
|
3203
|
+
const url = this.buildUrl(`/api/cat/${encodeURIComponent(cid)}`);
|
|
3204
|
+
let response;
|
|
3205
|
+
try {
|
|
3206
|
+
response = await this.fetchImpl(url);
|
|
3207
|
+
} catch (err) {
|
|
3208
|
+
throw new NetworkError2(
|
|
3209
|
+
err instanceof Error ? err.message : "Network request failed"
|
|
3210
|
+
);
|
|
3211
|
+
}
|
|
3212
|
+
if (!response.ok) {
|
|
3213
|
+
if (response.status === 404) {
|
|
3214
|
+
throw new ContentNotFoundError(cid);
|
|
3215
|
+
}
|
|
3216
|
+
throw new ContentError(
|
|
3217
|
+
`Failed to download content: ${response.status}`,
|
|
3218
|
+
"DOWNLOAD_ERROR",
|
|
3219
|
+
{ status: response.status }
|
|
3220
|
+
);
|
|
3221
|
+
}
|
|
3222
|
+
if (typeof window !== "undefined") {
|
|
3223
|
+
return response.blob();
|
|
3224
|
+
} else {
|
|
3225
|
+
const arrayBuffer = await response.arrayBuffer();
|
|
3226
|
+
return Buffer.from(arrayBuffer);
|
|
3227
|
+
}
|
|
3228
|
+
}
|
|
3229
|
+
/**
|
|
3230
|
+
* Get a direct URL for content by CID.
|
|
3231
|
+
*
|
|
3232
|
+
* This is useful for embedding in img tags or for direct downloads.
|
|
3233
|
+
*
|
|
3234
|
+
* @param cid - Content Identifier
|
|
3235
|
+
* @returns URL string
|
|
3236
|
+
*
|
|
3237
|
+
* @example
|
|
3238
|
+
* ```typescript
|
|
3239
|
+
* const url = content.getUrl('bafybeih...');
|
|
3240
|
+
* // Use in img tag: <img src={url} />
|
|
3241
|
+
* ```
|
|
3242
|
+
*/
|
|
3243
|
+
getUrl(cid) {
|
|
3244
|
+
return `${this.baseUrl}/api/cat/${encodeURIComponent(cid)}`;
|
|
3245
|
+
}
|
|
3246
|
+
/**
|
|
3247
|
+
* Stream content by CID.
|
|
3248
|
+
*
|
|
3249
|
+
* @param cid - Content Identifier
|
|
3250
|
+
* @returns ReadableStream of the content
|
|
3251
|
+
* @throws ContentNotFoundError if the content doesn't exist
|
|
3252
|
+
*
|
|
3253
|
+
* @example
|
|
3254
|
+
* ```typescript
|
|
3255
|
+
* const stream = await content.stream('bafybeih...');
|
|
3256
|
+
* const reader = stream.getReader();
|
|
3257
|
+
* while (true) {
|
|
3258
|
+
* const { done, value } = await reader.read();
|
|
3259
|
+
* if (done) break;
|
|
3260
|
+
* // Process chunk
|
|
3261
|
+
* }
|
|
3262
|
+
* ```
|
|
3263
|
+
*/
|
|
3264
|
+
async stream(cid) {
|
|
3265
|
+
const url = this.buildUrl(`/api/cat/${encodeURIComponent(cid)}`);
|
|
3266
|
+
let response;
|
|
3267
|
+
try {
|
|
3268
|
+
response = await this.fetchImpl(url);
|
|
3269
|
+
} catch (err) {
|
|
3270
|
+
throw new NetworkError2(
|
|
3271
|
+
err instanceof Error ? err.message : "Network request failed"
|
|
3272
|
+
);
|
|
3273
|
+
}
|
|
3274
|
+
if (!response.ok) {
|
|
3275
|
+
if (response.status === 404) {
|
|
3276
|
+
throw new ContentNotFoundError(cid);
|
|
3277
|
+
}
|
|
3278
|
+
throw new ContentError(
|
|
3279
|
+
`Failed to stream content: ${response.status}`,
|
|
3280
|
+
"STREAM_ERROR",
|
|
3281
|
+
{ status: response.status }
|
|
3282
|
+
);
|
|
3283
|
+
}
|
|
3284
|
+
if (!response.body) {
|
|
3285
|
+
throw new ContentError("Response body is not available", "STREAM_ERROR");
|
|
3286
|
+
}
|
|
3287
|
+
return response.body;
|
|
3288
|
+
}
|
|
3289
|
+
// ---------------------------------------------------------------------------
|
|
3290
|
+
// Component Helpers
|
|
3291
|
+
// ---------------------------------------------------------------------------
|
|
3292
|
+
/**
|
|
3293
|
+
* Download a component from an entity.
|
|
3294
|
+
*
|
|
3295
|
+
* @param entity - Entity containing the component
|
|
3296
|
+
* @param componentName - Name of the component (e.g., 'pinax', 'description', 'source')
|
|
3297
|
+
* @returns Component content as Blob (browser) or Buffer (Node)
|
|
3298
|
+
* @throws ComponentNotFoundError if the component doesn't exist
|
|
3299
|
+
*
|
|
3300
|
+
* @example
|
|
3301
|
+
* ```typescript
|
|
3302
|
+
* const entity = await content.get('01K75HQQXNTDG7BBP7PS9AWYAN');
|
|
3303
|
+
* const pinax = await content.getComponent(entity, 'pinax');
|
|
3304
|
+
* ```
|
|
3305
|
+
*/
|
|
3306
|
+
async getComponent(entity, componentName) {
|
|
3307
|
+
const cid = entity.components[componentName];
|
|
3308
|
+
if (!cid) {
|
|
3309
|
+
throw new ComponentNotFoundError(entity.pi, componentName);
|
|
3310
|
+
}
|
|
3311
|
+
return this.download(cid);
|
|
3312
|
+
}
|
|
3313
|
+
/**
|
|
3314
|
+
* Get the URL for a component from an entity.
|
|
3315
|
+
*
|
|
3316
|
+
* @param entity - Entity containing the component
|
|
3317
|
+
* @param componentName - Name of the component
|
|
3318
|
+
* @returns URL string
|
|
3319
|
+
* @throws ComponentNotFoundError if the component doesn't exist
|
|
3320
|
+
*
|
|
3321
|
+
* @example
|
|
3322
|
+
* ```typescript
|
|
3323
|
+
* const entity = await content.get('01K75HQQXNTDG7BBP7PS9AWYAN');
|
|
3324
|
+
* const imageUrl = content.getComponentUrl(entity, 'source');
|
|
3325
|
+
* // Use in img tag: <img src={imageUrl} />
|
|
3326
|
+
* ```
|
|
3327
|
+
*/
|
|
3328
|
+
getComponentUrl(entity, componentName) {
|
|
3329
|
+
const cid = entity.components[componentName];
|
|
3330
|
+
if (!cid) {
|
|
3331
|
+
throw new ComponentNotFoundError(entity.pi, componentName);
|
|
3332
|
+
}
|
|
3333
|
+
return this.getUrl(cid);
|
|
3334
|
+
}
|
|
3335
|
+
};
|
|
3336
|
+
|
|
3337
|
+
// src/graph/errors.ts
|
|
3338
|
+
var GraphError = class extends Error {
|
|
3339
|
+
constructor(message, code2 = "GRAPH_ERROR", details) {
|
|
3340
|
+
super(message);
|
|
3341
|
+
this.code = code2;
|
|
3342
|
+
this.details = details;
|
|
3343
|
+
this.name = "GraphError";
|
|
3344
|
+
}
|
|
3345
|
+
};
|
|
3346
|
+
var GraphEntityNotFoundError = class extends GraphError {
|
|
3347
|
+
constructor(canonicalId) {
|
|
3348
|
+
super(`Graph entity not found: ${canonicalId}`, "ENTITY_NOT_FOUND", { canonicalId });
|
|
3349
|
+
this.name = "GraphEntityNotFoundError";
|
|
3350
|
+
}
|
|
3351
|
+
};
|
|
3352
|
+
var NoPathFoundError = class extends GraphError {
|
|
3353
|
+
constructor(sourceIds, targetIds) {
|
|
3354
|
+
super(
|
|
3355
|
+
`No path found between sources and targets`,
|
|
3356
|
+
"NO_PATH_FOUND",
|
|
3357
|
+
{ sourceIds, targetIds }
|
|
3358
|
+
);
|
|
3359
|
+
this.name = "NoPathFoundError";
|
|
3360
|
+
}
|
|
3361
|
+
};
|
|
3362
|
+
var NetworkError3 = class extends GraphError {
|
|
3363
|
+
constructor(message, statusCode) {
|
|
3364
|
+
super(message, "NETWORK_ERROR", { statusCode });
|
|
3365
|
+
this.statusCode = statusCode;
|
|
3366
|
+
this.name = "NetworkError";
|
|
3367
|
+
}
|
|
3368
|
+
};
|
|
3369
|
+
|
|
3370
|
+
// src/graph/client.ts
|
|
3371
|
+
var GraphClient = class {
|
|
3372
|
+
constructor(config) {
|
|
3373
|
+
this.baseUrl = config.gatewayUrl.replace(/\/$/, "");
|
|
3374
|
+
this.fetchImpl = config.fetchImpl ?? fetch;
|
|
3375
|
+
}
|
|
3376
|
+
// ---------------------------------------------------------------------------
|
|
3377
|
+
// Request helpers
|
|
3378
|
+
// ---------------------------------------------------------------------------
|
|
3379
|
+
buildUrl(path2, query) {
|
|
3380
|
+
const url = new URL(`${this.baseUrl}${path2}`);
|
|
3381
|
+
if (query) {
|
|
3382
|
+
Object.entries(query).forEach(([key, value]) => {
|
|
3383
|
+
if (value !== void 0 && value !== null) {
|
|
3384
|
+
url.searchParams.set(key, String(value));
|
|
3385
|
+
}
|
|
3386
|
+
});
|
|
3387
|
+
}
|
|
3388
|
+
return url.toString();
|
|
3389
|
+
}
|
|
3390
|
+
async request(path2, options = {}) {
|
|
3391
|
+
const url = this.buildUrl(path2, options.query);
|
|
3392
|
+
const headers = new Headers({ "Content-Type": "application/json" });
|
|
3393
|
+
if (options.headers) {
|
|
3394
|
+
Object.entries(options.headers).forEach(([k, v]) => {
|
|
3395
|
+
if (v !== void 0) headers.set(k, v);
|
|
3396
|
+
});
|
|
3397
|
+
}
|
|
3398
|
+
let response;
|
|
3399
|
+
try {
|
|
3400
|
+
response = await this.fetchImpl(url, { ...options, headers });
|
|
3401
|
+
} catch (err) {
|
|
3402
|
+
throw new NetworkError3(
|
|
3403
|
+
err instanceof Error ? err.message : "Network request failed"
|
|
3404
|
+
);
|
|
3405
|
+
}
|
|
3406
|
+
if (response.ok) {
|
|
3407
|
+
const contentType = response.headers.get("content-type") || "";
|
|
3408
|
+
if (contentType.includes("application/json")) {
|
|
3409
|
+
return await response.json();
|
|
3410
|
+
}
|
|
3411
|
+
return await response.text();
|
|
3412
|
+
}
|
|
3413
|
+
let body;
|
|
3414
|
+
const text = await response.text();
|
|
3415
|
+
try {
|
|
3416
|
+
body = JSON.parse(text);
|
|
3417
|
+
} catch {
|
|
3418
|
+
body = text;
|
|
3419
|
+
}
|
|
3420
|
+
if (response.status === 404) {
|
|
3421
|
+
throw new GraphError(
|
|
3422
|
+
body?.message || "Not found",
|
|
3423
|
+
"NOT_FOUND",
|
|
3424
|
+
body
|
|
3425
|
+
);
|
|
3426
|
+
}
|
|
3427
|
+
const message = body?.error && typeof body.error === "string" ? body.error : body?.message && typeof body.message === "string" ? body.message : `Request failed with status ${response.status}`;
|
|
3428
|
+
throw new GraphError(message, "HTTP_ERROR", {
|
|
3429
|
+
status: response.status,
|
|
3430
|
+
body
|
|
3431
|
+
});
|
|
3432
|
+
}
|
|
3433
|
+
// ---------------------------------------------------------------------------
|
|
3434
|
+
// Entity Operations
|
|
3435
|
+
// ---------------------------------------------------------------------------
|
|
3436
|
+
/**
|
|
3437
|
+
* Get an entity by its canonical ID.
|
|
3438
|
+
*
|
|
3439
|
+
* @param canonicalId - Entity UUID
|
|
3440
|
+
* @returns Entity data
|
|
3441
|
+
* @throws GraphEntityNotFoundError if the entity doesn't exist
|
|
3442
|
+
*
|
|
3443
|
+
* @example
|
|
3444
|
+
* ```typescript
|
|
3445
|
+
* const entity = await graph.getEntity('uuid-123');
|
|
3446
|
+
* console.log('Entity:', entity.label, entity.type);
|
|
3447
|
+
* ```
|
|
3448
|
+
*/
|
|
3449
|
+
async getEntity(canonicalId) {
|
|
3450
|
+
const response = await this.request(
|
|
3451
|
+
`/graphdb/entity/${encodeURIComponent(canonicalId)}`
|
|
3452
|
+
);
|
|
3453
|
+
if (!response.found || !response.entity) {
|
|
3454
|
+
throw new GraphEntityNotFoundError(canonicalId);
|
|
3455
|
+
}
|
|
3456
|
+
return response.entity;
|
|
3457
|
+
}
|
|
3458
|
+
/**
|
|
3459
|
+
* Check if an entity exists by its canonical ID.
|
|
3460
|
+
*
|
|
3461
|
+
* @param canonicalId - Entity UUID
|
|
3462
|
+
* @returns True if entity exists
|
|
3463
|
+
*
|
|
3464
|
+
* @example
|
|
3465
|
+
* ```typescript
|
|
3466
|
+
* if (await graph.entityExists('uuid-123')) {
|
|
3467
|
+
* console.log('Entity exists');
|
|
3468
|
+
* }
|
|
3469
|
+
* ```
|
|
3470
|
+
*/
|
|
3471
|
+
async entityExists(canonicalId) {
|
|
3472
|
+
const response = await this.request(
|
|
3473
|
+
`/graphdb/entity/exists/${encodeURIComponent(canonicalId)}`
|
|
3474
|
+
);
|
|
3475
|
+
return response.exists;
|
|
3476
|
+
}
|
|
3477
|
+
/**
|
|
3478
|
+
* Query entities by code with optional type filter.
|
|
3479
|
+
*
|
|
3480
|
+
* @param code - Entity code to search for
|
|
3481
|
+
* @param type - Optional entity type filter
|
|
3482
|
+
* @returns Matching entities
|
|
3483
|
+
*
|
|
3484
|
+
* @example
|
|
3485
|
+
* ```typescript
|
|
3486
|
+
* // Find by code
|
|
3487
|
+
* const entities = await graph.queryByCode('person_john');
|
|
3488
|
+
*
|
|
3489
|
+
* // With type filter
|
|
3490
|
+
* const people = await graph.queryByCode('john', 'person');
|
|
3491
|
+
* ```
|
|
3492
|
+
*/
|
|
3493
|
+
async queryByCode(code2, type) {
|
|
3494
|
+
const response = await this.request(
|
|
3495
|
+
"/graphdb/entity/query",
|
|
3496
|
+
{
|
|
3497
|
+
method: "POST",
|
|
3498
|
+
body: JSON.stringify({ code: code2, type })
|
|
3499
|
+
}
|
|
3500
|
+
);
|
|
3501
|
+
if (!response.found || !response.entity) {
|
|
3502
|
+
return [];
|
|
3503
|
+
}
|
|
3504
|
+
return [response.entity];
|
|
3505
|
+
}
|
|
3506
|
+
/**
|
|
3507
|
+
* Look up entities by code across all PIs.
|
|
3508
|
+
*
|
|
3509
|
+
* @param code - Entity code to search for
|
|
3510
|
+
* @param type - Optional entity type filter
|
|
3511
|
+
* @returns Matching entities
|
|
3512
|
+
*
|
|
3513
|
+
* @example
|
|
3514
|
+
* ```typescript
|
|
3515
|
+
* const entities = await graph.lookupByCode('alice_austen', 'person');
|
|
3516
|
+
* ```
|
|
3517
|
+
*/
|
|
3518
|
+
async lookupByCode(code2, type) {
|
|
3519
|
+
const response = await this.request(
|
|
3520
|
+
"/graphdb/entities/lookup-by-code",
|
|
3521
|
+
{
|
|
3522
|
+
method: "POST",
|
|
3523
|
+
body: JSON.stringify({ code: code2, type })
|
|
3524
|
+
}
|
|
3525
|
+
);
|
|
3526
|
+
return response.entities || [];
|
|
3527
|
+
}
|
|
3528
|
+
// ---------------------------------------------------------------------------
|
|
3529
|
+
// PI-based Operations
|
|
3530
|
+
// ---------------------------------------------------------------------------
|
|
3531
|
+
/**
|
|
3532
|
+
* List entities from a specific PI or multiple PIs.
|
|
3533
|
+
*
|
|
3534
|
+
* @param pi - Single PI or array of PIs
|
|
3535
|
+
* @param options - Filter options
|
|
3536
|
+
* @returns Entities from the PI(s)
|
|
3537
|
+
*
|
|
3538
|
+
* @example
|
|
3539
|
+
* ```typescript
|
|
3540
|
+
* // From single PI
|
|
3541
|
+
* const entities = await graph.listEntitiesFromPi('01K75HQQXNTDG7BBP7PS9AWYAN');
|
|
3542
|
+
*
|
|
3543
|
+
* // With type filter
|
|
3544
|
+
* const people = await graph.listEntitiesFromPi('01K75HQQXNTDG7BBP7PS9AWYAN', { type: 'person' });
|
|
3545
|
+
*
|
|
3546
|
+
* // From multiple PIs
|
|
3547
|
+
* const all = await graph.listEntitiesFromPi(['pi-1', 'pi-2']);
|
|
3548
|
+
* ```
|
|
3549
|
+
*/
|
|
3550
|
+
async listEntitiesFromPi(pi, options = {}) {
|
|
3551
|
+
const pis = Array.isArray(pi) ? pi : [pi];
|
|
3552
|
+
const response = await this.request(
|
|
3553
|
+
"/graphdb/entities/list",
|
|
3554
|
+
{
|
|
3555
|
+
method: "POST",
|
|
3556
|
+
body: JSON.stringify({
|
|
3557
|
+
pis,
|
|
3558
|
+
type: options.type
|
|
3559
|
+
})
|
|
3560
|
+
}
|
|
3561
|
+
);
|
|
3562
|
+
return response.entities || [];
|
|
3563
|
+
}
|
|
3564
|
+
/**
|
|
3565
|
+
* Get entities with their relationships from a PI.
|
|
3566
|
+
*
|
|
3567
|
+
* This is an optimized query that returns entities along with all their
|
|
3568
|
+
* relationship data in a single request.
|
|
3569
|
+
*
|
|
3570
|
+
* @param pi - Persistent Identifier
|
|
3571
|
+
* @param type - Optional entity type filter
|
|
3572
|
+
* @returns Entities with relationships
|
|
3573
|
+
*
|
|
3574
|
+
* @example
|
|
3575
|
+
* ```typescript
|
|
3576
|
+
* const entities = await graph.getEntitiesWithRelationships('01K75HQQXNTDG7BBP7PS9AWYAN');
|
|
3577
|
+
* entities.forEach(e => {
|
|
3578
|
+
* console.log(`${e.label} has ${e.relationships.length} relationships`);
|
|
3579
|
+
* });
|
|
3580
|
+
* ```
|
|
3581
|
+
*/
|
|
3582
|
+
async getEntitiesWithRelationships(pi, type) {
|
|
3583
|
+
const response = await this.request(
|
|
3584
|
+
"/graphdb/pi/entities-with-relationships",
|
|
3585
|
+
{
|
|
3586
|
+
method: "POST",
|
|
3587
|
+
body: JSON.stringify({ pi, type })
|
|
3588
|
+
}
|
|
3589
|
+
);
|
|
3590
|
+
return response.entities || [];
|
|
3591
|
+
}
|
|
3592
|
+
/**
|
|
3593
|
+
* Get the lineage (ancestors and/or descendants) of a PI.
|
|
3594
|
+
*
|
|
3595
|
+
* @param pi - Source PI
|
|
3596
|
+
* @param direction - 'ancestors', 'descendants', or 'both'
|
|
3597
|
+
* @returns Lineage data
|
|
3598
|
+
*
|
|
3599
|
+
* @example
|
|
3600
|
+
* ```typescript
|
|
3601
|
+
* // Get ancestors
|
|
3602
|
+
* const lineage = await graph.getLineage('01K75HQQXNTDG7BBP7PS9AWYAN', 'ancestors');
|
|
3603
|
+
*
|
|
3604
|
+
* // Get both directions
|
|
3605
|
+
* const full = await graph.getLineage('01K75HQQXNTDG7BBP7PS9AWYAN', 'both');
|
|
3606
|
+
* ```
|
|
3607
|
+
*/
|
|
3608
|
+
async getLineage(pi, direction = "both", maxHops = 10) {
|
|
3609
|
+
return this.request("/graphdb/pi/lineage", {
|
|
3610
|
+
method: "POST",
|
|
3611
|
+
body: JSON.stringify({ sourcePi: pi, direction, maxHops })
|
|
3612
|
+
});
|
|
3613
|
+
}
|
|
3614
|
+
// ---------------------------------------------------------------------------
|
|
3615
|
+
// Relationship Operations
|
|
3616
|
+
// ---------------------------------------------------------------------------
|
|
3617
|
+
/**
|
|
3618
|
+
* Get all relationships for an entity.
|
|
3619
|
+
*
|
|
3620
|
+
* @param canonicalId - Entity UUID
|
|
3621
|
+
* @returns Array of relationships
|
|
3622
|
+
*
|
|
3623
|
+
* @example
|
|
3624
|
+
* ```typescript
|
|
3625
|
+
* const relationships = await graph.getRelationships('uuid-123');
|
|
3626
|
+
* relationships.forEach(r => {
|
|
3627
|
+
* console.log(`${r.direction}: ${r.predicate} -> ${r.target_label}`);
|
|
3628
|
+
* });
|
|
3629
|
+
* ```
|
|
3630
|
+
*/
|
|
3631
|
+
async getRelationships(canonicalId) {
|
|
3632
|
+
const response = await this.request(`/graphdb/relationships/${encodeURIComponent(canonicalId)}`);
|
|
3633
|
+
if (!response.found || !response.relationships) {
|
|
3634
|
+
return [];
|
|
3635
|
+
}
|
|
3636
|
+
return response.relationships.map((rel) => ({
|
|
3637
|
+
direction: rel.direction,
|
|
3638
|
+
predicate: rel.predicate,
|
|
3639
|
+
target_id: rel.target_id,
|
|
3640
|
+
target_code: rel.target_code || "",
|
|
3641
|
+
target_label: rel.target_label,
|
|
3642
|
+
target_type: rel.target_type,
|
|
3643
|
+
properties: rel.properties,
|
|
3644
|
+
source_pi: rel.source_pi,
|
|
3645
|
+
created_at: rel.created_at
|
|
3646
|
+
}));
|
|
3647
|
+
}
|
|
3648
|
+
// ---------------------------------------------------------------------------
|
|
3649
|
+
// Path Finding
|
|
3650
|
+
// ---------------------------------------------------------------------------
|
|
3651
|
+
/**
|
|
3652
|
+
* Find shortest paths between sets of entities.
|
|
3653
|
+
*
|
|
3654
|
+
* @param sourceIds - Starting entity IDs
|
|
3655
|
+
* @param targetIds - Target entity IDs
|
|
3656
|
+
* @param options - Path finding options
|
|
3657
|
+
* @returns Found paths
|
|
3658
|
+
*
|
|
3659
|
+
* @example
|
|
3660
|
+
* ```typescript
|
|
3661
|
+
* const paths = await graph.findPaths(
|
|
3662
|
+
* ['uuid-alice'],
|
|
3663
|
+
* ['uuid-bob'],
|
|
3664
|
+
* { max_depth: 4, direction: 'both' }
|
|
3665
|
+
* );
|
|
3666
|
+
*
|
|
3667
|
+
* paths.forEach(path => {
|
|
3668
|
+
* console.log(`Path of length ${path.length}:`);
|
|
3669
|
+
* path.edges.forEach(e => {
|
|
3670
|
+
* console.log(` ${e.subject_label} -[${e.predicate}]-> ${e.object_label}`);
|
|
3671
|
+
* });
|
|
3672
|
+
* });
|
|
3673
|
+
* ```
|
|
3674
|
+
*/
|
|
3675
|
+
async findPaths(sourceIds, targetIds, options = {}) {
|
|
3676
|
+
const response = await this.request("/graphdb/paths/between", {
|
|
3677
|
+
method: "POST",
|
|
3678
|
+
body: JSON.stringify({
|
|
3679
|
+
source_ids: sourceIds,
|
|
3680
|
+
target_ids: targetIds,
|
|
3681
|
+
max_depth: options.max_depth,
|
|
3682
|
+
direction: options.direction,
|
|
3683
|
+
limit: options.limit
|
|
3684
|
+
})
|
|
3685
|
+
});
|
|
3686
|
+
return response.paths || [];
|
|
3687
|
+
}
|
|
3688
|
+
/**
|
|
3689
|
+
* Find entities of a specific type reachable from starting entities.
|
|
3690
|
+
*
|
|
3691
|
+
* @param startIds - Starting entity IDs
|
|
3692
|
+
* @param targetType - Type of entities to find
|
|
3693
|
+
* @param options - Search options
|
|
3694
|
+
* @returns Reachable entities of the specified type
|
|
3695
|
+
*
|
|
3696
|
+
* @example
|
|
3697
|
+
* ```typescript
|
|
3698
|
+
* // Find all people reachable from an event
|
|
3699
|
+
* const people = await graph.findReachable(
|
|
3700
|
+
* ['uuid-event'],
|
|
3701
|
+
* 'person',
|
|
3702
|
+
* { max_depth: 3 }
|
|
3703
|
+
* );
|
|
3704
|
+
* ```
|
|
3705
|
+
*/
|
|
3706
|
+
async findReachable(startIds, targetType, options = {}) {
|
|
3707
|
+
const response = await this.request(
|
|
3708
|
+
"/graphdb/paths/reachable",
|
|
3709
|
+
{
|
|
3710
|
+
method: "POST",
|
|
3711
|
+
body: JSON.stringify({
|
|
3712
|
+
start_ids: startIds,
|
|
3713
|
+
target_type: targetType,
|
|
3714
|
+
max_depth: options.max_depth,
|
|
3715
|
+
direction: options.direction,
|
|
3716
|
+
limit: options.limit
|
|
3717
|
+
})
|
|
3718
|
+
}
|
|
3719
|
+
);
|
|
3720
|
+
return response.entities || [];
|
|
3721
|
+
}
|
|
3722
|
+
/**
|
|
3723
|
+
* Check the health of the graph service.
|
|
3724
|
+
*
|
|
3725
|
+
* @returns Health status
|
|
3726
|
+
*/
|
|
3727
|
+
async health() {
|
|
3728
|
+
return this.request("/graphdb/health", { method: "GET" });
|
|
3729
|
+
}
|
|
3730
|
+
};
|
|
1637
3731
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1638
3732
|
0 && (module.exports = {
|
|
1639
3733
|
ArkeUploader,
|
|
1640
3734
|
CollectionsClient,
|
|
1641
3735
|
CollectionsError,
|
|
3736
|
+
ComponentNotFoundError,
|
|
3737
|
+
ContentClient,
|
|
3738
|
+
ContentError,
|
|
3739
|
+
ContentNetworkError,
|
|
3740
|
+
ContentNotFoundError,
|
|
3741
|
+
EditClient,
|
|
3742
|
+
EditError,
|
|
3743
|
+
EditSession,
|
|
3744
|
+
EntityNotFoundError,
|
|
3745
|
+
GraphClient,
|
|
3746
|
+
GraphEntityNotFoundError,
|
|
3747
|
+
GraphError,
|
|
3748
|
+
GraphNetworkError,
|
|
1642
3749
|
NetworkError,
|
|
3750
|
+
NoPathFoundError,
|
|
3751
|
+
PermissionError,
|
|
3752
|
+
QueryClient,
|
|
3753
|
+
QueryError,
|
|
1643
3754
|
ScanError,
|
|
1644
3755
|
UploadClient,
|
|
1645
3756
|
UploadError,
|
|
1646
3757
|
ValidationError,
|
|
3758
|
+
VersionNotFoundError,
|
|
1647
3759
|
WorkerAPIError
|
|
1648
3760
|
});
|
|
1649
3761
|
//# sourceMappingURL=index.cjs.map
|