@arke-institute/sdk 0.1.1 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/dist/content/index.cjs +591 -0
  2. package/dist/content/index.cjs.map +1 -0
  3. package/dist/content/index.d.cts +516 -0
  4. package/dist/content/index.d.ts +516 -0
  5. package/dist/content/index.js +558 -0
  6. package/dist/content/index.js.map +1 -0
  7. package/dist/edit/index.cjs +1503 -0
  8. package/dist/edit/index.cjs.map +1 -0
  9. package/dist/edit/index.d.cts +78 -0
  10. package/dist/edit/index.d.ts +78 -0
  11. package/dist/edit/index.js +1447 -0
  12. package/dist/edit/index.js.map +1 -0
  13. package/dist/{errors-BrNZWPE7.d.cts → errors-3L7IiHcr.d.cts} +3 -0
  14. package/dist/{errors-CCyp5KCg.d.ts → errors-BTe8GKRQ.d.ts} +3 -0
  15. package/dist/errors-CT7yzKkU.d.cts +874 -0
  16. package/dist/errors-CT7yzKkU.d.ts +874 -0
  17. package/dist/graph/index.cjs +427 -0
  18. package/dist/graph/index.cjs.map +1 -0
  19. package/dist/graph/index.d.cts +485 -0
  20. package/dist/graph/index.d.ts +485 -0
  21. package/dist/graph/index.js +396 -0
  22. package/dist/graph/index.js.map +1 -0
  23. package/dist/index.cjs +2726 -14
  24. package/dist/index.cjs.map +1 -1
  25. package/dist/index.d.cts +5 -1
  26. package/dist/index.d.ts +5 -1
  27. package/dist/index.js +2708 -14
  28. package/dist/index.js.map +1 -1
  29. package/dist/query/index.cjs +356 -0
  30. package/dist/query/index.cjs.map +1 -0
  31. package/dist/query/index.d.cts +636 -0
  32. package/dist/query/index.d.ts +636 -0
  33. package/dist/query/index.js +328 -0
  34. package/dist/query/index.js.map +1 -0
  35. package/dist/upload/index.cjs +3 -14
  36. package/dist/upload/index.cjs.map +1 -1
  37. package/dist/upload/index.d.cts +2 -2
  38. package/dist/upload/index.d.ts +2 -2
  39. package/dist/upload/index.js +3 -14
  40. package/dist/upload/index.js.map +1 -1
  41. 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: () => NetworkError3,
656
+ ContentNotFoundError: () => ContentNotFoundError2,
657
+ EditClient: () => EditClient,
658
+ EditError: () => EditError,
659
+ EditSession: () => EditSession,
660
+ EntityNotFoundError: () => EntityNotFoundError2,
661
+ GraphClient: () => GraphClient,
662
+ GraphEntityNotFoundError: () => GraphEntityNotFoundError,
663
+ GraphError: () => GraphError,
664
+ GraphNetworkError: () => NetworkError4,
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,2721 @@ 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
+ * Direct semantic search against the vector index.
1870
+ *
1871
+ * This bypasses the path query syntax and directly queries Pinecone for
1872
+ * semantically similar entities. Useful for:
1873
+ * - Simple semantic searches without graph traversal
1874
+ * - Scoped searches filtered by source_pi (collection scope)
1875
+ * - Type-filtered semantic searches
1876
+ *
1877
+ * For graph traversal and path-based queries, use `path()` instead.
1878
+ *
1879
+ * @param text - Search query text
1880
+ * @param options - Search options (namespace, filters, top_k)
1881
+ * @returns Matching entities with similarity scores
1882
+ *
1883
+ * @example
1884
+ * ```typescript
1885
+ * // Simple semantic search
1886
+ * const results = await query.semanticSearch('photographers from New York');
1887
+ *
1888
+ * // Scoped to a specific PI (collection)
1889
+ * const scoped = await query.semanticSearch('portraits', {
1890
+ * filter: { source_pi: '01K75HQQXNTDG7BBP7PS9AWYAN' },
1891
+ * top_k: 20,
1892
+ * });
1893
+ *
1894
+ * // Filter by type
1895
+ * const people = await query.semanticSearch('artists', {
1896
+ * filter: { type: 'person' },
1897
+ * });
1898
+ *
1899
+ * // Search across merged entities from multiple source PIs
1900
+ * const merged = await query.semanticSearch('historical documents', {
1901
+ * filter: { merged_entities_source_pis: ['pi-1', 'pi-2'] },
1902
+ * });
1903
+ * ```
1904
+ */
1905
+ async semanticSearch(text, options = {}) {
1906
+ let pineconeFilter;
1907
+ if (options.filter) {
1908
+ pineconeFilter = {};
1909
+ if (options.filter.type) {
1910
+ const types = Array.isArray(options.filter.type) ? options.filter.type : [options.filter.type];
1911
+ pineconeFilter.type = types.length === 1 ? { $eq: types[0] } : { $in: types };
1912
+ }
1913
+ if (options.filter.source_pi) {
1914
+ const pis = Array.isArray(options.filter.source_pi) ? options.filter.source_pi : [options.filter.source_pi];
1915
+ pineconeFilter.source_pi = pis.length === 1 ? { $eq: pis[0] } : { $in: pis };
1916
+ }
1917
+ if (options.filter.merged_entities_source_pis) {
1918
+ const pis = Array.isArray(options.filter.merged_entities_source_pis) ? options.filter.merged_entities_source_pis : [options.filter.merged_entities_source_pis];
1919
+ pineconeFilter.merged_entities_source_pis = { $in: pis };
1920
+ }
1921
+ if (Object.keys(pineconeFilter).length === 0) {
1922
+ pineconeFilter = void 0;
1923
+ }
1924
+ }
1925
+ return this.request("/query/search/semantic", {
1926
+ method: "POST",
1927
+ body: JSON.stringify({
1928
+ text,
1929
+ namespace: options.namespace,
1930
+ filter: pineconeFilter,
1931
+ top_k: options.top_k
1932
+ })
1933
+ });
1934
+ }
1935
+ /**
1936
+ * Search for collections by semantic similarity.
1937
+ *
1938
+ * Searches the dedicated collections index for fast semantic matching.
1939
+ *
1940
+ * @param query - Search query text
1941
+ * @param options - Search options (limit, visibility filter)
1942
+ * @returns Matching collections with similarity scores
1943
+ *
1944
+ * @example
1945
+ * ```typescript
1946
+ * // Search for photography-related collections
1947
+ * const results = await query.searchCollections('photography');
1948
+ * console.log(results.collections[0].title);
1949
+ *
1950
+ * // Search only public collections
1951
+ * const publicResults = await query.searchCollections('history', {
1952
+ * visibility: 'public',
1953
+ * limit: 20,
1954
+ * });
1955
+ * ```
1956
+ */
1957
+ async searchCollections(query, options = {}) {
1958
+ return this.request("/query/search/collections", {
1959
+ method: "GET",
1960
+ query: {
1961
+ q: query,
1962
+ limit: options.limit?.toString(),
1963
+ visibility: options.visibility
1964
+ }
1965
+ });
1966
+ }
1967
+ };
1968
+
1969
+ // src/edit/types.ts
1970
+ var DEFAULT_RETRY_CONFIG = {
1971
+ maxRetries: 10,
1972
+ baseDelay: 100,
1973
+ maxDelay: 5e3,
1974
+ jitterFactor: 0.3
1975
+ };
1976
+
1977
+ // src/edit/errors.ts
1978
+ var EditError = class extends Error {
1979
+ constructor(message, code2 = "UNKNOWN_ERROR", details) {
1980
+ super(message);
1981
+ this.code = code2;
1982
+ this.details = details;
1983
+ this.name = "EditError";
1984
+ }
1985
+ };
1986
+ var EntityNotFoundError = class extends EditError {
1987
+ constructor(id) {
1988
+ super(`Entity not found: ${id}`, "ENTITY_NOT_FOUND", { id });
1989
+ this.name = "EntityNotFoundError";
1990
+ }
1991
+ };
1992
+ var CASConflictError = class extends EditError {
1993
+ constructor(id, expectedTip, actualTip) {
1994
+ super(
1995
+ `CAS conflict: entity ${id} was modified (expected ${expectedTip}, got ${actualTip})`,
1996
+ "CAS_CONFLICT",
1997
+ { id, expectedTip, actualTip }
1998
+ );
1999
+ this.name = "CASConflictError";
2000
+ }
2001
+ };
2002
+ var EntityExistsError = class extends EditError {
2003
+ constructor(id) {
2004
+ super(`Entity already exists: ${id}`, "ENTITY_EXISTS", { id });
2005
+ this.name = "EntityExistsError";
2006
+ }
2007
+ };
2008
+ var MergeError = class extends EditError {
2009
+ constructor(message, sourceId, targetId) {
2010
+ super(message, "MERGE_ERROR", { sourceId, targetId });
2011
+ this.name = "MergeError";
2012
+ }
2013
+ };
2014
+ var UnmergeError = class extends EditError {
2015
+ constructor(message, sourceId, targetId) {
2016
+ super(message, "UNMERGE_ERROR", { sourceId, targetId });
2017
+ this.name = "UnmergeError";
2018
+ }
2019
+ };
2020
+ var DeleteError = class extends EditError {
2021
+ constructor(message, id) {
2022
+ super(message, "DELETE_ERROR", { id });
2023
+ this.name = "DeleteError";
2024
+ }
2025
+ };
2026
+ var UndeleteError = class extends EditError {
2027
+ constructor(message, id) {
2028
+ super(message, "UNDELETE_ERROR", { id });
2029
+ this.name = "UndeleteError";
2030
+ }
2031
+ };
2032
+ var ReprocessError = class extends EditError {
2033
+ constructor(message, batchId) {
2034
+ super(message, "REPROCESS_ERROR", { batchId });
2035
+ this.name = "ReprocessError";
2036
+ }
2037
+ };
2038
+ var ValidationError2 = class extends EditError {
2039
+ constructor(message, field) {
2040
+ super(message, "VALIDATION_ERROR", { field });
2041
+ this.name = "ValidationError";
2042
+ }
2043
+ };
2044
+ var PermissionError = class extends EditError {
2045
+ constructor(message, id) {
2046
+ super(message, "PERMISSION_DENIED", { id });
2047
+ this.name = "PermissionError";
2048
+ }
2049
+ };
2050
+ var NetworkError2 = class extends EditError {
2051
+ constructor(message, statusCode) {
2052
+ super(message, "NETWORK_ERROR", { statusCode });
2053
+ this.name = "NetworkError";
2054
+ }
2055
+ };
2056
+ var ContentNotFoundError = class extends EditError {
2057
+ constructor(cid) {
2058
+ super(`Content not found: ${cid}`, "CONTENT_NOT_FOUND", { cid });
2059
+ this.name = "ContentNotFoundError";
2060
+ }
2061
+ };
2062
+ var IPFSError = class extends EditError {
2063
+ constructor(message) {
2064
+ super(message, "IPFS_ERROR");
2065
+ this.name = "IPFSError";
2066
+ }
2067
+ };
2068
+ var BackendError = class extends EditError {
2069
+ constructor(message) {
2070
+ super(message, "BACKEND_ERROR");
2071
+ this.name = "BackendError";
2072
+ }
2073
+ };
2074
+
2075
+ // src/edit/client.ts
2076
+ var RETRYABLE_STATUS_CODES = [409, 503];
2077
+ var RETRYABLE_ERRORS = ["ECONNRESET", "ETIMEDOUT", "fetch failed"];
2078
+ var EditClient = class {
2079
+ constructor(config) {
2080
+ this.gatewayUrl = config.gatewayUrl.replace(/\/$/, "");
2081
+ this.authToken = config.authToken;
2082
+ this.network = config.network || "main";
2083
+ this.userId = config.userId;
2084
+ this.retryConfig = { ...DEFAULT_RETRY_CONFIG, ...config.retryConfig };
2085
+ this.statusUrlTransform = config.statusUrlTransform;
2086
+ this.apiPrefix = config.apiPrefix ?? "/api";
2087
+ }
2088
+ // ===========================================================================
2089
+ // Configuration Methods
2090
+ // ===========================================================================
2091
+ /**
2092
+ * Update the auth token (useful for token refresh)
2093
+ */
2094
+ setAuthToken(token) {
2095
+ this.authToken = token;
2096
+ }
2097
+ /**
2098
+ * Set the network (main or test)
2099
+ */
2100
+ setNetwork(network) {
2101
+ this.network = network;
2102
+ }
2103
+ /**
2104
+ * Set the user ID for permission checks
2105
+ */
2106
+ setUserId(userId) {
2107
+ this.userId = userId;
2108
+ }
2109
+ // ===========================================================================
2110
+ // Internal Helpers
2111
+ // ===========================================================================
2112
+ /**
2113
+ * Build URL with API prefix
2114
+ */
2115
+ buildUrl(path2) {
2116
+ return `${this.gatewayUrl}${this.apiPrefix}${path2}`;
2117
+ }
2118
+ sleep(ms) {
2119
+ return new Promise((resolve) => setTimeout(resolve, ms));
2120
+ }
2121
+ getHeaders(contentType = "application/json") {
2122
+ const headers = {};
2123
+ if (contentType) {
2124
+ headers["Content-Type"] = contentType;
2125
+ }
2126
+ if (this.authToken) {
2127
+ headers["Authorization"] = `Bearer ${this.authToken}`;
2128
+ }
2129
+ headers["X-Arke-Network"] = this.network;
2130
+ if (this.userId) {
2131
+ headers["X-User-Id"] = this.userId;
2132
+ }
2133
+ return headers;
2134
+ }
2135
+ calculateDelay(attempt) {
2136
+ const { baseDelay, maxDelay, jitterFactor } = this.retryConfig;
2137
+ const exponentialDelay = baseDelay * Math.pow(2, attempt);
2138
+ const cappedDelay = Math.min(exponentialDelay, maxDelay);
2139
+ const jitter = cappedDelay * jitterFactor * (Math.random() * 2 - 1);
2140
+ return Math.max(0, cappedDelay + jitter);
2141
+ }
2142
+ isRetryableStatus(status) {
2143
+ return RETRYABLE_STATUS_CODES.includes(status);
2144
+ }
2145
+ isRetryableError(error) {
2146
+ const message = error.message.toLowerCase();
2147
+ return RETRYABLE_ERRORS.some((e) => message.includes(e.toLowerCase()));
2148
+ }
2149
+ /**
2150
+ * Execute a fetch with exponential backoff retry on transient errors
2151
+ */
2152
+ async fetchWithRetry(url, options, context) {
2153
+ let lastError = null;
2154
+ for (let attempt = 0; attempt <= this.retryConfig.maxRetries; attempt++) {
2155
+ try {
2156
+ const response = await fetch(url, options);
2157
+ if (this.isRetryableStatus(response.status) && attempt < this.retryConfig.maxRetries) {
2158
+ const delay = this.calculateDelay(attempt);
2159
+ lastError = new Error(`${context}: ${response.status} ${response.statusText}`);
2160
+ await this.sleep(delay);
2161
+ continue;
2162
+ }
2163
+ return response;
2164
+ } catch (error) {
2165
+ lastError = error;
2166
+ if (this.isRetryableError(lastError) && attempt < this.retryConfig.maxRetries) {
2167
+ const delay = this.calculateDelay(attempt);
2168
+ await this.sleep(delay);
2169
+ continue;
2170
+ }
2171
+ throw new NetworkError2(lastError.message);
2172
+ }
2173
+ }
2174
+ throw lastError || new NetworkError2("Request failed after retries");
2175
+ }
2176
+ /**
2177
+ * Handle common error responses and throw appropriate error types
2178
+ */
2179
+ async handleErrorResponse(response, context) {
2180
+ let errorData = {};
2181
+ try {
2182
+ errorData = await response.json();
2183
+ } catch {
2184
+ }
2185
+ const message = errorData.message || `${context}: ${response.statusText}`;
2186
+ const errorCode = errorData.error || "";
2187
+ switch (response.status) {
2188
+ case 400:
2189
+ throw new ValidationError2(message);
2190
+ case 403:
2191
+ throw new PermissionError(message);
2192
+ case 404:
2193
+ throw new EntityNotFoundError(message);
2194
+ case 409:
2195
+ if (errorCode === "CAS_FAILURE") {
2196
+ const details = errorData.details;
2197
+ throw new CASConflictError(
2198
+ context,
2199
+ details?.expect || "unknown",
2200
+ details?.actual || "unknown"
2201
+ );
2202
+ }
2203
+ if (errorCode === "CONFLICT") {
2204
+ throw new EntityExistsError(message);
2205
+ }
2206
+ throw new EditError(message, errorCode, errorData.details);
2207
+ case 503:
2208
+ if (errorCode === "IPFS_ERROR") {
2209
+ throw new IPFSError(message);
2210
+ }
2211
+ if (errorCode === "BACKEND_ERROR") {
2212
+ throw new BackendError(message);
2213
+ }
2214
+ throw new NetworkError2(message, response.status);
2215
+ default:
2216
+ throw new EditError(message, errorCode || "API_ERROR", { status: response.status });
2217
+ }
2218
+ }
2219
+ // ===========================================================================
2220
+ // Entity CRUD Operations
2221
+ // ===========================================================================
2222
+ /**
2223
+ * Create a new entity
2224
+ */
2225
+ async createEntity(request) {
2226
+ const url = this.buildUrl("/entities");
2227
+ const response = await this.fetchWithRetry(
2228
+ url,
2229
+ {
2230
+ method: "POST",
2231
+ headers: this.getHeaders(),
2232
+ body: JSON.stringify(request)
2233
+ },
2234
+ "Create entity"
2235
+ );
2236
+ if (!response.ok) {
2237
+ await this.handleErrorResponse(response, "Create entity");
2238
+ }
2239
+ return response.json();
2240
+ }
2241
+ /**
2242
+ * Get an entity by ID
2243
+ */
2244
+ async getEntity(id) {
2245
+ const url = this.buildUrl(`/entities/${encodeURIComponent(id)}`);
2246
+ const response = await this.fetchWithRetry(
2247
+ url,
2248
+ { headers: this.getHeaders() },
2249
+ `Get entity ${id}`
2250
+ );
2251
+ if (!response.ok) {
2252
+ await this.handleErrorResponse(response, `Get entity ${id}`);
2253
+ }
2254
+ return response.json();
2255
+ }
2256
+ /**
2257
+ * List entities with pagination
2258
+ */
2259
+ async listEntities(options = {}) {
2260
+ const params = new URLSearchParams();
2261
+ if (options.limit) params.set("limit", options.limit.toString());
2262
+ if (options.cursor) params.set("cursor", options.cursor);
2263
+ if (options.include_metadata) params.set("include_metadata", "true");
2264
+ const queryString = params.toString();
2265
+ const url = this.buildUrl(`/entities${queryString ? `?${queryString}` : ""}`);
2266
+ const response = await this.fetchWithRetry(url, { headers: this.getHeaders() }, "List entities");
2267
+ if (!response.ok) {
2268
+ await this.handleErrorResponse(response, "List entities");
2269
+ }
2270
+ return response.json();
2271
+ }
2272
+ /**
2273
+ * Update an entity (append new version)
2274
+ */
2275
+ async updateEntity(id, update) {
2276
+ const url = this.buildUrl(`/entities/${encodeURIComponent(id)}/versions`);
2277
+ const response = await this.fetchWithRetry(
2278
+ url,
2279
+ {
2280
+ method: "POST",
2281
+ headers: this.getHeaders(),
2282
+ body: JSON.stringify(update)
2283
+ },
2284
+ `Update entity ${id}`
2285
+ );
2286
+ if (!response.ok) {
2287
+ await this.handleErrorResponse(response, `Update entity ${id}`);
2288
+ }
2289
+ return response.json();
2290
+ }
2291
+ // ===========================================================================
2292
+ // Version Operations
2293
+ // ===========================================================================
2294
+ /**
2295
+ * List version history for an entity
2296
+ */
2297
+ async listVersions(id, options = {}) {
2298
+ const params = new URLSearchParams();
2299
+ if (options.limit) params.set("limit", options.limit.toString());
2300
+ if (options.cursor) params.set("cursor", options.cursor);
2301
+ const queryString = params.toString();
2302
+ const url = this.buildUrl(`/entities/${encodeURIComponent(id)}/versions${queryString ? `?${queryString}` : ""}`);
2303
+ const response = await this.fetchWithRetry(url, { headers: this.getHeaders() }, `List versions for ${id}`);
2304
+ if (!response.ok) {
2305
+ await this.handleErrorResponse(response, `List versions for ${id}`);
2306
+ }
2307
+ return response.json();
2308
+ }
2309
+ /**
2310
+ * Get a specific version of an entity
2311
+ */
2312
+ async getVersion(id, selector) {
2313
+ const url = this.buildUrl(`/entities/${encodeURIComponent(id)}/versions/${encodeURIComponent(selector)}`);
2314
+ const response = await this.fetchWithRetry(
2315
+ url,
2316
+ { headers: this.getHeaders() },
2317
+ `Get version ${selector} for ${id}`
2318
+ );
2319
+ if (!response.ok) {
2320
+ await this.handleErrorResponse(response, `Get version ${selector} for ${id}`);
2321
+ }
2322
+ return response.json();
2323
+ }
2324
+ /**
2325
+ * Resolve an entity ID to its current tip CID (fast lookup)
2326
+ */
2327
+ async resolve(id) {
2328
+ const url = this.buildUrl(`/resolve/${encodeURIComponent(id)}`);
2329
+ const response = await this.fetchWithRetry(
2330
+ url,
2331
+ { headers: this.getHeaders() },
2332
+ `Resolve ${id}`
2333
+ );
2334
+ if (!response.ok) {
2335
+ await this.handleErrorResponse(response, `Resolve ${id}`);
2336
+ }
2337
+ return response.json();
2338
+ }
2339
+ // ===========================================================================
2340
+ // Hierarchy Operations
2341
+ // ===========================================================================
2342
+ /**
2343
+ * Update parent-child hierarchy relationships
2344
+ */
2345
+ async updateHierarchy(request) {
2346
+ const apiRequest = {
2347
+ parent_pi: request.parent_id,
2348
+ expect_tip: request.expect_tip,
2349
+ add_children: request.add_children,
2350
+ remove_children: request.remove_children,
2351
+ note: request.note
2352
+ };
2353
+ const url = this.buildUrl("/hierarchy");
2354
+ const response = await this.fetchWithRetry(
2355
+ url,
2356
+ {
2357
+ method: "POST",
2358
+ headers: this.getHeaders(),
2359
+ body: JSON.stringify(apiRequest)
2360
+ },
2361
+ `Update hierarchy for ${request.parent_id}`
2362
+ );
2363
+ if (!response.ok) {
2364
+ await this.handleErrorResponse(response, `Update hierarchy for ${request.parent_id}`);
2365
+ }
2366
+ return response.json();
2367
+ }
2368
+ // ===========================================================================
2369
+ // Merge Operations
2370
+ // ===========================================================================
2371
+ /**
2372
+ * Merge source entity into target entity
2373
+ */
2374
+ async mergeEntity(sourceId, request) {
2375
+ const url = this.buildUrl(`/entities/${encodeURIComponent(sourceId)}/merge`);
2376
+ const response = await this.fetchWithRetry(
2377
+ url,
2378
+ {
2379
+ method: "POST",
2380
+ headers: this.getHeaders(),
2381
+ body: JSON.stringify(request)
2382
+ },
2383
+ `Merge ${sourceId} into ${request.target_id}`
2384
+ );
2385
+ if (!response.ok) {
2386
+ try {
2387
+ const error = await response.json();
2388
+ throw new MergeError(
2389
+ error.message || `Merge failed: ${response.statusText}`,
2390
+ sourceId,
2391
+ request.target_id
2392
+ );
2393
+ } catch (e) {
2394
+ if (e instanceof MergeError) throw e;
2395
+ await this.handleErrorResponse(response, `Merge ${sourceId}`);
2396
+ }
2397
+ }
2398
+ return response.json();
2399
+ }
2400
+ /**
2401
+ * Unmerge (restore) a previously merged entity
2402
+ */
2403
+ async unmergeEntity(sourceId, request) {
2404
+ const url = this.buildUrl(`/entities/${encodeURIComponent(sourceId)}/unmerge`);
2405
+ const response = await this.fetchWithRetry(
2406
+ url,
2407
+ {
2408
+ method: "POST",
2409
+ headers: this.getHeaders(),
2410
+ body: JSON.stringify(request)
2411
+ },
2412
+ `Unmerge ${sourceId}`
2413
+ );
2414
+ if (!response.ok) {
2415
+ try {
2416
+ const error = await response.json();
2417
+ throw new UnmergeError(
2418
+ error.message || `Unmerge failed: ${response.statusText}`,
2419
+ sourceId,
2420
+ request.target_id
2421
+ );
2422
+ } catch (e) {
2423
+ if (e instanceof UnmergeError) throw e;
2424
+ await this.handleErrorResponse(response, `Unmerge ${sourceId}`);
2425
+ }
2426
+ }
2427
+ return response.json();
2428
+ }
2429
+ // ===========================================================================
2430
+ // Delete Operations
2431
+ // ===========================================================================
2432
+ /**
2433
+ * Soft delete an entity (creates tombstone, preserves history)
2434
+ */
2435
+ async deleteEntity(id, request) {
2436
+ const url = this.buildUrl(`/entities/${encodeURIComponent(id)}/delete`);
2437
+ const response = await this.fetchWithRetry(
2438
+ url,
2439
+ {
2440
+ method: "POST",
2441
+ headers: this.getHeaders(),
2442
+ body: JSON.stringify(request)
2443
+ },
2444
+ `Delete ${id}`
2445
+ );
2446
+ if (!response.ok) {
2447
+ try {
2448
+ const error = await response.json();
2449
+ throw new DeleteError(error.message || `Delete failed: ${response.statusText}`, id);
2450
+ } catch (e) {
2451
+ if (e instanceof DeleteError) throw e;
2452
+ await this.handleErrorResponse(response, `Delete ${id}`);
2453
+ }
2454
+ }
2455
+ return response.json();
2456
+ }
2457
+ /**
2458
+ * Restore a deleted entity
2459
+ */
2460
+ async undeleteEntity(id, request) {
2461
+ const url = this.buildUrl(`/entities/${encodeURIComponent(id)}/undelete`);
2462
+ const response = await this.fetchWithRetry(
2463
+ url,
2464
+ {
2465
+ method: "POST",
2466
+ headers: this.getHeaders(),
2467
+ body: JSON.stringify(request)
2468
+ },
2469
+ `Undelete ${id}`
2470
+ );
2471
+ if (!response.ok) {
2472
+ try {
2473
+ const error = await response.json();
2474
+ throw new UndeleteError(error.message || `Undelete failed: ${response.statusText}`, id);
2475
+ } catch (e) {
2476
+ if (e instanceof UndeleteError) throw e;
2477
+ await this.handleErrorResponse(response, `Undelete ${id}`);
2478
+ }
2479
+ }
2480
+ return response.json();
2481
+ }
2482
+ // ===========================================================================
2483
+ // Content Operations
2484
+ // ===========================================================================
2485
+ /**
2486
+ * Upload files to IPFS
2487
+ */
2488
+ async upload(files) {
2489
+ let formData;
2490
+ if (files instanceof FormData) {
2491
+ formData = files;
2492
+ } else {
2493
+ formData = new FormData();
2494
+ const fileArray = Array.isArray(files) ? files : [files];
2495
+ for (const file of fileArray) {
2496
+ if (file instanceof File) {
2497
+ formData.append("file", file, file.name);
2498
+ } else {
2499
+ formData.append("file", file, "file");
2500
+ }
2501
+ }
2502
+ }
2503
+ const url = this.buildUrl("/upload");
2504
+ const response = await this.fetchWithRetry(
2505
+ url,
2506
+ {
2507
+ method: "POST",
2508
+ headers: this.getHeaders(null),
2509
+ // No Content-Type for multipart
2510
+ body: formData
2511
+ },
2512
+ "Upload files"
2513
+ );
2514
+ if (!response.ok) {
2515
+ await this.handleErrorResponse(response, "Upload files");
2516
+ }
2517
+ return response.json();
2518
+ }
2519
+ /**
2520
+ * Upload text content and return CID
2521
+ */
2522
+ async uploadContent(content, filename) {
2523
+ const blob = new Blob([content], { type: "text/plain" });
2524
+ const file = new File([blob], filename, { type: "text/plain" });
2525
+ const [result] = await this.upload(file);
2526
+ return result.cid;
2527
+ }
2528
+ /**
2529
+ * Download file content by CID
2530
+ */
2531
+ async getContent(cid) {
2532
+ const url = this.buildUrl(`/cat/${encodeURIComponent(cid)}`);
2533
+ const response = await this.fetchWithRetry(
2534
+ url,
2535
+ { headers: this.getHeaders() },
2536
+ `Get content ${cid}`
2537
+ );
2538
+ if (response.status === 404) {
2539
+ throw new ContentNotFoundError(cid);
2540
+ }
2541
+ if (!response.ok) {
2542
+ await this.handleErrorResponse(response, `Get content ${cid}`);
2543
+ }
2544
+ return response.text();
2545
+ }
2546
+ /**
2547
+ * Download a DAG node (JSON) by CID
2548
+ */
2549
+ async getDag(cid) {
2550
+ const url = this.buildUrl(`/dag/${encodeURIComponent(cid)}`);
2551
+ const response = await this.fetchWithRetry(
2552
+ url,
2553
+ { headers: this.getHeaders() },
2554
+ `Get DAG ${cid}`
2555
+ );
2556
+ if (response.status === 404) {
2557
+ throw new ContentNotFoundError(cid);
2558
+ }
2559
+ if (!response.ok) {
2560
+ await this.handleErrorResponse(response, `Get DAG ${cid}`);
2561
+ }
2562
+ return response.json();
2563
+ }
2564
+ // ===========================================================================
2565
+ // Arke Origin Operations
2566
+ // ===========================================================================
2567
+ /**
2568
+ * Get the Arke origin block (genesis entity)
2569
+ */
2570
+ async getArke() {
2571
+ const url = this.buildUrl("/arke");
2572
+ const response = await this.fetchWithRetry(
2573
+ url,
2574
+ { headers: this.getHeaders() },
2575
+ "Get Arke"
2576
+ );
2577
+ if (!response.ok) {
2578
+ await this.handleErrorResponse(response, "Get Arke");
2579
+ }
2580
+ return response.json();
2581
+ }
2582
+ /**
2583
+ * Initialize the Arke origin block (creates if doesn't exist)
2584
+ */
2585
+ async initArke() {
2586
+ const url = this.buildUrl("/arke/init");
2587
+ const response = await this.fetchWithRetry(
2588
+ url,
2589
+ {
2590
+ method: "POST",
2591
+ headers: this.getHeaders()
2592
+ },
2593
+ "Init Arke"
2594
+ );
2595
+ if (!response.ok) {
2596
+ await this.handleErrorResponse(response, "Init Arke");
2597
+ }
2598
+ return response.json();
2599
+ }
2600
+ // ===========================================================================
2601
+ // Reprocess API Operations (via /reprocess/*)
2602
+ // ===========================================================================
2603
+ /**
2604
+ * Trigger reprocessing for an entity
2605
+ */
2606
+ async reprocess(request) {
2607
+ const response = await this.fetchWithRetry(
2608
+ `${this.gatewayUrl}/reprocess/reprocess`,
2609
+ {
2610
+ method: "POST",
2611
+ headers: this.getHeaders(),
2612
+ body: JSON.stringify({
2613
+ pi: request.pi,
2614
+ phases: request.phases,
2615
+ cascade: request.cascade,
2616
+ options: request.options
2617
+ })
2618
+ },
2619
+ `Reprocess ${request.pi}`
2620
+ );
2621
+ if (response.status === 403) {
2622
+ const error = await response.json().catch(() => ({}));
2623
+ throw new PermissionError(
2624
+ error.message || `Permission denied to reprocess ${request.pi}`,
2625
+ request.pi
2626
+ );
2627
+ }
2628
+ if (!response.ok) {
2629
+ const error = await response.json().catch(() => ({}));
2630
+ throw new ReprocessError(error.message || `Reprocess failed: ${response.statusText}`);
2631
+ }
2632
+ return response.json();
2633
+ }
2634
+ /**
2635
+ * Get reprocessing status by batch ID
2636
+ */
2637
+ async getReprocessStatus(statusUrl, isFirstPoll = false) {
2638
+ const fetchUrl = this.statusUrlTransform ? this.statusUrlTransform(statusUrl) : statusUrl;
2639
+ const delay = isFirstPoll ? 3e3 : this.retryConfig.baseDelay;
2640
+ if (isFirstPoll) {
2641
+ await this.sleep(delay);
2642
+ }
2643
+ const response = await this.fetchWithRetry(
2644
+ fetchUrl,
2645
+ { headers: this.getHeaders() },
2646
+ "Get reprocess status"
2647
+ );
2648
+ if (!response.ok) {
2649
+ throw new EditError(
2650
+ `Failed to fetch reprocess status: ${response.statusText}`,
2651
+ "STATUS_ERROR",
2652
+ { status: response.status }
2653
+ );
2654
+ }
2655
+ return response.json();
2656
+ }
2657
+ // ===========================================================================
2658
+ // Utility Methods
2659
+ // ===========================================================================
2660
+ /**
2661
+ * Execute an operation with automatic CAS retry
2662
+ */
2663
+ async withCAS(id, operation, maxRetries = 3) {
2664
+ let lastError = null;
2665
+ for (let attempt = 0; attempt < maxRetries; attempt++) {
2666
+ try {
2667
+ const entity = await this.getEntity(id);
2668
+ return await operation(entity);
2669
+ } catch (error) {
2670
+ if (error instanceof CASConflictError && attempt < maxRetries - 1) {
2671
+ lastError = error;
2672
+ const delay = this.calculateDelay(attempt);
2673
+ await this.sleep(delay);
2674
+ continue;
2675
+ }
2676
+ throw error;
2677
+ }
2678
+ }
2679
+ throw lastError || new EditError("withCAS failed after retries");
2680
+ }
2681
+ };
2682
+
2683
+ // src/edit/diff.ts
2684
+ var Diff = __toESM(require("diff"), 1);
2685
+ var DiffEngine = class {
2686
+ /**
2687
+ * Compute diff between two strings
2688
+ */
2689
+ static diff(original, modified) {
2690
+ const changes = Diff.diffLines(original, modified);
2691
+ const diffs = [];
2692
+ let lineNumber = 1;
2693
+ for (const change of changes) {
2694
+ if (change.added) {
2695
+ diffs.push({
2696
+ type: "addition",
2697
+ modified: change.value.trimEnd(),
2698
+ lineNumber
2699
+ });
2700
+ } else if (change.removed) {
2701
+ diffs.push({
2702
+ type: "deletion",
2703
+ original: change.value.trimEnd(),
2704
+ lineNumber
2705
+ });
2706
+ } else {
2707
+ const lines = change.value.split("\n").length - 1;
2708
+ lineNumber += lines;
2709
+ }
2710
+ if (change.added) {
2711
+ lineNumber += change.value.split("\n").length - 1;
2712
+ }
2713
+ }
2714
+ return diffs;
2715
+ }
2716
+ /**
2717
+ * Compute word-level diff for more granular changes
2718
+ */
2719
+ static diffWords(original, modified) {
2720
+ const changes = Diff.diffWords(original, modified);
2721
+ const diffs = [];
2722
+ for (const change of changes) {
2723
+ if (change.added) {
2724
+ diffs.push({
2725
+ type: "addition",
2726
+ modified: change.value
2727
+ });
2728
+ } else if (change.removed) {
2729
+ diffs.push({
2730
+ type: "deletion",
2731
+ original: change.value
2732
+ });
2733
+ }
2734
+ }
2735
+ return diffs;
2736
+ }
2737
+ /**
2738
+ * Create a ComponentDiff from original and modified content
2739
+ */
2740
+ static createComponentDiff(componentName, original, modified) {
2741
+ const diffs = this.diff(original, modified);
2742
+ const hasChanges = diffs.length > 0;
2743
+ let summary;
2744
+ if (!hasChanges) {
2745
+ summary = "No changes";
2746
+ } else {
2747
+ const additions = diffs.filter((d) => d.type === "addition").length;
2748
+ const deletions = diffs.filter((d) => d.type === "deletion").length;
2749
+ const parts = [];
2750
+ if (additions > 0) parts.push(`${additions} addition${additions > 1 ? "s" : ""}`);
2751
+ if (deletions > 0) parts.push(`${deletions} deletion${deletions > 1 ? "s" : ""}`);
2752
+ summary = parts.join(", ");
2753
+ }
2754
+ return {
2755
+ componentName,
2756
+ diffs,
2757
+ summary,
2758
+ hasChanges
2759
+ };
2760
+ }
2761
+ /**
2762
+ * Format diffs for AI prompt consumption
2763
+ */
2764
+ static formatForPrompt(diffs) {
2765
+ if (diffs.length === 0) {
2766
+ return "No changes detected.";
2767
+ }
2768
+ const lines = [];
2769
+ for (const diff of diffs) {
2770
+ const linePrefix = diff.lineNumber ? `Line ${diff.lineNumber}: ` : "";
2771
+ if (diff.type === "addition") {
2772
+ lines.push(`${linePrefix}+ ${diff.modified}`);
2773
+ } else if (diff.type === "deletion") {
2774
+ lines.push(`${linePrefix}- ${diff.original}`);
2775
+ } else if (diff.type === "change") {
2776
+ lines.push(`${linePrefix}"${diff.original}" \u2192 "${diff.modified}"`);
2777
+ }
2778
+ }
2779
+ return lines.join("\n");
2780
+ }
2781
+ /**
2782
+ * Format component diffs for AI prompt
2783
+ */
2784
+ static formatComponentDiffsForPrompt(componentDiffs) {
2785
+ const sections = [];
2786
+ for (const cd of componentDiffs) {
2787
+ if (!cd.hasChanges) continue;
2788
+ sections.push(`## Changes to ${cd.componentName}:`);
2789
+ sections.push(this.formatForPrompt(cd.diffs));
2790
+ sections.push("");
2791
+ }
2792
+ return sections.join("\n");
2793
+ }
2794
+ /**
2795
+ * Create a unified diff view
2796
+ */
2797
+ static unifiedDiff(original, modified, options) {
2798
+ const filename = options?.filename || "content";
2799
+ const patch = Diff.createPatch(filename, original, modified, "", "", {
2800
+ context: options?.context ?? 3
2801
+ });
2802
+ return patch;
2803
+ }
2804
+ /**
2805
+ * Extract corrections from diffs (specific text replacements)
2806
+ */
2807
+ static extractCorrections(original, modified, sourceFile) {
2808
+ const wordDiffs = Diff.diffWords(original, modified);
2809
+ const corrections = [];
2810
+ let i = 0;
2811
+ while (i < wordDiffs.length) {
2812
+ const current = wordDiffs[i];
2813
+ if (current.removed && i + 1 < wordDiffs.length && wordDiffs[i + 1].added) {
2814
+ const removed = current.value.trim();
2815
+ const added = wordDiffs[i + 1].value.trim();
2816
+ if (removed && added && removed !== added) {
2817
+ corrections.push({
2818
+ original: removed,
2819
+ corrected: added,
2820
+ sourceFile
2821
+ });
2822
+ }
2823
+ i += 2;
2824
+ } else {
2825
+ i++;
2826
+ }
2827
+ }
2828
+ return corrections;
2829
+ }
2830
+ /**
2831
+ * Check if two strings are meaningfully different
2832
+ * (ignoring whitespace differences)
2833
+ */
2834
+ static hasSignificantChanges(original, modified) {
2835
+ const normalizedOriginal = original.replace(/\s+/g, " ").trim();
2836
+ const normalizedModified = modified.replace(/\s+/g, " ").trim();
2837
+ return normalizedOriginal !== normalizedModified;
2838
+ }
2839
+ };
2840
+
2841
+ // src/edit/prompts.ts
2842
+ var PromptBuilder = class {
2843
+ /**
2844
+ * Build prompt for AI-first mode (user provides instructions)
2845
+ */
2846
+ static buildAIPrompt(userPrompt, component, entityContext, currentContent) {
2847
+ const sections = [];
2848
+ sections.push(`## Instructions for ${component}`);
2849
+ sections.push(userPrompt);
2850
+ sections.push("");
2851
+ sections.push("## Entity Context");
2852
+ sections.push(`- PI: ${entityContext.pi}`);
2853
+ sections.push(`- Current version: ${entityContext.ver}`);
2854
+ if (entityContext.parentPi) {
2855
+ sections.push(`- Parent: ${entityContext.parentPi}`);
2856
+ }
2857
+ if (entityContext.childrenCount > 0) {
2858
+ sections.push(`- Children: ${entityContext.childrenCount}`);
2859
+ }
2860
+ sections.push("");
2861
+ if (currentContent) {
2862
+ sections.push(`## Current ${component} content for reference:`);
2863
+ sections.push("```");
2864
+ sections.push(currentContent.slice(0, 2e3));
2865
+ if (currentContent.length > 2e3) {
2866
+ sections.push("... [truncated]");
2867
+ }
2868
+ sections.push("```");
2869
+ }
2870
+ return sections.join("\n");
2871
+ }
2872
+ /**
2873
+ * Build prompt incorporating manual edits and diffs
2874
+ */
2875
+ static buildEditReviewPrompt(componentDiffs, corrections, component, userInstructions) {
2876
+ const sections = [];
2877
+ sections.push("## Manual Edits Made");
2878
+ sections.push("");
2879
+ sections.push("The following manual edits were made to this entity:");
2880
+ sections.push("");
2881
+ const diffContent = DiffEngine.formatComponentDiffsForPrompt(componentDiffs);
2882
+ if (diffContent) {
2883
+ sections.push(diffContent);
2884
+ }
2885
+ if (corrections.length > 0) {
2886
+ sections.push("## Corrections Identified");
2887
+ sections.push("");
2888
+ for (const correction of corrections) {
2889
+ const source = correction.sourceFile ? ` (in ${correction.sourceFile})` : "";
2890
+ sections.push(`- "${correction.original}" \u2192 "${correction.corrected}"${source}`);
2891
+ }
2892
+ sections.push("");
2893
+ }
2894
+ sections.push("## Instructions");
2895
+ if (userInstructions) {
2896
+ sections.push(userInstructions);
2897
+ } else {
2898
+ sections.push(
2899
+ `Update the ${component} to accurately reflect these changes. Ensure any corrections are incorporated and the content is consistent.`
2900
+ );
2901
+ }
2902
+ sections.push("");
2903
+ sections.push("## Guidance");
2904
+ switch (component) {
2905
+ case "pinax":
2906
+ sections.push(
2907
+ "Update metadata fields to reflect any corrections. Pay special attention to dates, names, and other factual information that may have been corrected."
2908
+ );
2909
+ break;
2910
+ case "description":
2911
+ sections.push(
2912
+ "Regenerate the description incorporating the changes. Maintain the overall tone and structure while ensuring accuracy based on the corrections."
2913
+ );
2914
+ break;
2915
+ case "cheimarros":
2916
+ sections.push(
2917
+ "Update the knowledge graph to reflect any new or corrected entities, relationships, and facts identified in the changes."
2918
+ );
2919
+ break;
2920
+ }
2921
+ return sections.join("\n");
2922
+ }
2923
+ /**
2924
+ * Build cascade-aware prompt additions
2925
+ */
2926
+ static buildCascadePrompt(basePrompt, cascadeContext) {
2927
+ const sections = [basePrompt];
2928
+ sections.push("");
2929
+ sections.push("## Cascade Context");
2930
+ sections.push("");
2931
+ sections.push(
2932
+ "This edit is part of a cascading update. After updating this entity, parent entities will also be updated to reflect these changes."
2933
+ );
2934
+ sections.push("");
2935
+ if (cascadeContext.path.length > 1) {
2936
+ sections.push(`Cascade path: ${cascadeContext.path.join(" \u2192 ")}`);
2937
+ sections.push(`Depth: ${cascadeContext.depth}`);
2938
+ }
2939
+ if (cascadeContext.stopAtPi) {
2940
+ sections.push(`Cascade will stop at: ${cascadeContext.stopAtPi}`);
2941
+ }
2942
+ sections.push("");
2943
+ sections.push(
2944
+ "Ensure the content accurately represents the source material so parent aggregations will be correct."
2945
+ );
2946
+ return sections.join("\n");
2947
+ }
2948
+ /**
2949
+ * Build a general prompt combining multiple instructions
2950
+ */
2951
+ static buildCombinedPrompt(generalPrompt, componentPrompt, component) {
2952
+ const sections = [];
2953
+ if (generalPrompt) {
2954
+ sections.push("## General Instructions");
2955
+ sections.push(generalPrompt);
2956
+ sections.push("");
2957
+ }
2958
+ if (componentPrompt) {
2959
+ sections.push(`## Specific Instructions for ${component}`);
2960
+ sections.push(componentPrompt);
2961
+ sections.push("");
2962
+ }
2963
+ if (sections.length === 0) {
2964
+ return `Regenerate the ${component} based on the current entity content.`;
2965
+ }
2966
+ return sections.join("\n");
2967
+ }
2968
+ /**
2969
+ * Build prompt for correction-based updates
2970
+ */
2971
+ static buildCorrectionPrompt(corrections) {
2972
+ if (corrections.length === 0) {
2973
+ return "";
2974
+ }
2975
+ const sections = [];
2976
+ sections.push("## Corrections Applied");
2977
+ sections.push("");
2978
+ sections.push("The following corrections were made to the source content:");
2979
+ sections.push("");
2980
+ for (const correction of corrections) {
2981
+ const source = correction.sourceFile ? ` in ${correction.sourceFile}` : "";
2982
+ sections.push(`- "${correction.original}" was corrected to "${correction.corrected}"${source}`);
2983
+ if (correction.context) {
2984
+ sections.push(` Context: ${correction.context}`);
2985
+ }
2986
+ }
2987
+ sections.push("");
2988
+ sections.push(
2989
+ "Update the metadata and description to reflect these corrections. Previous content may have contained errors based on the incorrect text."
2990
+ );
2991
+ return sections.join("\n");
2992
+ }
2993
+ /**
2994
+ * Get component-specific regeneration guidance
2995
+ */
2996
+ static getComponentGuidance(component) {
2997
+ switch (component) {
2998
+ case "pinax":
2999
+ return "Extract and structure metadata including: institution, creator, title, date range, subjects, type, and other relevant fields. Ensure accuracy based on the source content.";
3000
+ case "description":
3001
+ 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.";
3002
+ case "cheimarros":
3003
+ return "Extract entities (people, places, organizations, events) and their relationships. Build a knowledge graph that captures the key facts and connections in the content.";
3004
+ default:
3005
+ return "";
3006
+ }
3007
+ }
3008
+ };
3009
+
3010
+ // src/edit/session.ts
3011
+ var DEFAULT_SCOPE = {
3012
+ components: [],
3013
+ cascade: false
3014
+ };
3015
+ var DEFAULT_POLL_OPTIONS = {
3016
+ intervalMs: 2e3,
3017
+ timeoutMs: 3e5
3018
+ // 5 minutes
3019
+ };
3020
+ var EditSession = class {
3021
+ constructor(client, pi, config) {
3022
+ this.entity = null;
3023
+ this.loadedComponents = {};
3024
+ // AI mode state
3025
+ this.prompts = {};
3026
+ // Manual mode state
3027
+ this.editedContent = {};
3028
+ this.corrections = [];
3029
+ // Scope
3030
+ this.scope = { ...DEFAULT_SCOPE };
3031
+ // Execution state
3032
+ this.submitting = false;
3033
+ this.result = null;
3034
+ this.statusUrl = null;
3035
+ this.client = client;
3036
+ this.pi = pi;
3037
+ this.mode = config?.mode ?? "ai-prompt";
3038
+ this.aiReviewEnabled = config?.aiReviewEnabled ?? true;
3039
+ }
3040
+ // ===========================================================================
3041
+ // Loading
3042
+ // ===========================================================================
3043
+ /**
3044
+ * Load the entity and its key components
3045
+ */
3046
+ async load() {
3047
+ this.entity = await this.client.getEntity(this.pi);
3048
+ const priorityComponents = ["description.md", "pinax.json", "cheimarros.json"];
3049
+ await Promise.all(
3050
+ priorityComponents.map(async (name) => {
3051
+ const cid = this.entity.components[name];
3052
+ if (cid) {
3053
+ try {
3054
+ this.loadedComponents[name] = await this.client.getContent(cid);
3055
+ } catch {
3056
+ }
3057
+ }
3058
+ })
3059
+ );
3060
+ }
3061
+ /**
3062
+ * Load a specific component on demand
3063
+ */
3064
+ async loadComponent(name) {
3065
+ if (this.loadedComponents[name]) {
3066
+ return this.loadedComponents[name];
3067
+ }
3068
+ if (!this.entity) {
3069
+ throw new ValidationError2("Session not loaded");
3070
+ }
3071
+ const cid = this.entity.components[name];
3072
+ if (!cid) {
3073
+ return void 0;
3074
+ }
3075
+ const content = await this.client.getContent(cid);
3076
+ this.loadedComponents[name] = content;
3077
+ return content;
3078
+ }
3079
+ /**
3080
+ * Get the loaded entity
3081
+ */
3082
+ getEntity() {
3083
+ if (!this.entity) {
3084
+ throw new ValidationError2("Session not loaded. Call load() first.");
3085
+ }
3086
+ return this.entity;
3087
+ }
3088
+ /**
3089
+ * Get loaded component content
3090
+ */
3091
+ getComponents() {
3092
+ return { ...this.loadedComponents };
3093
+ }
3094
+ // ===========================================================================
3095
+ // AI Prompt Mode
3096
+ // ===========================================================================
3097
+ /**
3098
+ * Set a prompt for AI regeneration
3099
+ */
3100
+ setPrompt(target, prompt) {
3101
+ if (this.mode === "manual-only") {
3102
+ throw new ValidationError2("Cannot set prompts in manual-only mode");
3103
+ }
3104
+ this.prompts[target] = prompt;
3105
+ }
3106
+ /**
3107
+ * Get all prompts
3108
+ */
3109
+ getPrompts() {
3110
+ return { ...this.prompts };
3111
+ }
3112
+ /**
3113
+ * Clear a prompt
3114
+ */
3115
+ clearPrompt(target) {
3116
+ delete this.prompts[target];
3117
+ }
3118
+ // ===========================================================================
3119
+ // Manual Edit Mode
3120
+ // ===========================================================================
3121
+ /**
3122
+ * Set edited content for a component
3123
+ */
3124
+ setContent(componentName, content) {
3125
+ if (this.mode === "ai-prompt") {
3126
+ throw new ValidationError2("Cannot set content in ai-prompt mode");
3127
+ }
3128
+ this.editedContent[componentName] = content;
3129
+ }
3130
+ /**
3131
+ * Get all edited content
3132
+ */
3133
+ getEditedContent() {
3134
+ return { ...this.editedContent };
3135
+ }
3136
+ /**
3137
+ * Clear edited content for a component
3138
+ */
3139
+ clearContent(componentName) {
3140
+ delete this.editedContent[componentName];
3141
+ }
3142
+ /**
3143
+ * Add a correction (for OCR fixes, etc.)
3144
+ */
3145
+ addCorrection(original, corrected, sourceFile) {
3146
+ this.corrections.push({ original, corrected, sourceFile });
3147
+ }
3148
+ /**
3149
+ * Get all corrections
3150
+ */
3151
+ getCorrections() {
3152
+ return [...this.corrections];
3153
+ }
3154
+ /**
3155
+ * Clear corrections
3156
+ */
3157
+ clearCorrections() {
3158
+ this.corrections = [];
3159
+ }
3160
+ // ===========================================================================
3161
+ // Scope Configuration
3162
+ // ===========================================================================
3163
+ /**
3164
+ * Set the edit scope
3165
+ */
3166
+ setScope(scope) {
3167
+ this.scope = { ...this.scope, ...scope };
3168
+ }
3169
+ /**
3170
+ * Get the current scope
3171
+ */
3172
+ getScope() {
3173
+ return { ...this.scope };
3174
+ }
3175
+ // ===========================================================================
3176
+ // Preview & Summary
3177
+ // ===========================================================================
3178
+ /**
3179
+ * Get diffs for manual changes
3180
+ */
3181
+ getDiff() {
3182
+ const diffs = [];
3183
+ for (const [name, edited] of Object.entries(this.editedContent)) {
3184
+ const original = this.loadedComponents[name] || "";
3185
+ if (DiffEngine.hasSignificantChanges(original, edited)) {
3186
+ diffs.push(DiffEngine.createComponentDiff(name, original, edited));
3187
+ }
3188
+ }
3189
+ return diffs;
3190
+ }
3191
+ /**
3192
+ * Preview what prompts will be sent to AI
3193
+ */
3194
+ previewPrompt() {
3195
+ const result = {};
3196
+ if (!this.entity) return result;
3197
+ const entityContext = {
3198
+ pi: this.entity.id,
3199
+ ver: this.entity.ver,
3200
+ parentPi: this.entity.parent_pi,
3201
+ childrenCount: this.entity.children_pi?.length ?? 0,
3202
+ currentContent: this.loadedComponents
3203
+ };
3204
+ for (const component of this.scope.components) {
3205
+ let prompt;
3206
+ if (this.mode === "ai-prompt") {
3207
+ const componentPrompt = this.prompts[component];
3208
+ const generalPrompt = this.prompts["general"];
3209
+ const combined = PromptBuilder.buildCombinedPrompt(generalPrompt, componentPrompt, component);
3210
+ prompt = PromptBuilder.buildAIPrompt(
3211
+ combined,
3212
+ component,
3213
+ entityContext,
3214
+ this.loadedComponents[`${component}.json`] || this.loadedComponents[`${component}.md`]
3215
+ );
3216
+ } else {
3217
+ const diffs = this.getDiff();
3218
+ const userInstructions = this.prompts["general"] || this.prompts[component];
3219
+ prompt = PromptBuilder.buildEditReviewPrompt(diffs, this.corrections, component, userInstructions);
3220
+ }
3221
+ if (this.scope.cascade) {
3222
+ prompt = PromptBuilder.buildCascadePrompt(prompt, {
3223
+ path: [this.entity.id, this.entity.parent_pi || "root"].filter(Boolean),
3224
+ depth: 0,
3225
+ stopAtPi: this.scope.stopAtPi
3226
+ });
3227
+ }
3228
+ result[component] = prompt;
3229
+ }
3230
+ return result;
3231
+ }
3232
+ /**
3233
+ * Get a summary of pending changes
3234
+ */
3235
+ getChangeSummary() {
3236
+ const diffs = this.getDiff();
3237
+ const hasManualEdits = diffs.some((d) => d.hasChanges);
3238
+ return {
3239
+ mode: this.mode,
3240
+ hasManualEdits,
3241
+ editedComponents: Object.keys(this.editedContent),
3242
+ corrections: [...this.corrections],
3243
+ prompts: { ...this.prompts },
3244
+ scope: { ...this.scope },
3245
+ willRegenerate: [...this.scope.components],
3246
+ willCascade: this.scope.cascade,
3247
+ willSave: hasManualEdits,
3248
+ willReprocess: this.scope.components.length > 0
3249
+ };
3250
+ }
3251
+ // ===========================================================================
3252
+ // Execution
3253
+ // ===========================================================================
3254
+ /**
3255
+ * Submit changes (saves first if manual edits, then reprocesses)
3256
+ */
3257
+ async submit(note) {
3258
+ if (this.submitting) {
3259
+ throw new ValidationError2("Submit already in progress");
3260
+ }
3261
+ if (!this.entity) {
3262
+ throw new ValidationError2("Session not loaded. Call load() first.");
3263
+ }
3264
+ this.submitting = true;
3265
+ this.result = {};
3266
+ try {
3267
+ const diffs = this.getDiff();
3268
+ const hasManualEdits = diffs.some((d) => d.hasChanges);
3269
+ if (hasManualEdits) {
3270
+ const componentUpdates = {};
3271
+ for (const [name, content] of Object.entries(this.editedContent)) {
3272
+ const original = this.loadedComponents[name] || "";
3273
+ if (DiffEngine.hasSignificantChanges(original, content)) {
3274
+ const cid = await this.client.uploadContent(content, name);
3275
+ componentUpdates[name] = cid;
3276
+ }
3277
+ }
3278
+ const version = await this.client.updateEntity(this.pi, {
3279
+ expect_tip: this.entity.manifest_cid,
3280
+ components: componentUpdates,
3281
+ note
3282
+ });
3283
+ this.result.saved = {
3284
+ pi: version.id,
3285
+ newVersion: version.ver,
3286
+ newTip: version.tip
3287
+ };
3288
+ this.entity.manifest_cid = version.tip;
3289
+ this.entity.ver = version.ver;
3290
+ }
3291
+ if (this.scope.components.length > 0) {
3292
+ const customPrompts = this.buildCustomPrompts();
3293
+ const reprocessResult = await this.client.reprocess({
3294
+ pi: this.pi,
3295
+ phases: this.scope.components,
3296
+ cascade: this.scope.cascade,
3297
+ options: {
3298
+ stop_at_pi: this.scope.stopAtPi,
3299
+ custom_prompts: customPrompts,
3300
+ custom_note: note
3301
+ }
3302
+ });
3303
+ this.result.reprocess = reprocessResult;
3304
+ this.statusUrl = reprocessResult.status_url;
3305
+ }
3306
+ return this.result;
3307
+ } finally {
3308
+ this.submitting = false;
3309
+ }
3310
+ }
3311
+ /**
3312
+ * Wait for reprocessing to complete
3313
+ */
3314
+ async waitForCompletion(options) {
3315
+ const opts = { ...DEFAULT_POLL_OPTIONS, ...options };
3316
+ if (!this.statusUrl) {
3317
+ return {
3318
+ phase: "complete",
3319
+ saveComplete: true
3320
+ };
3321
+ }
3322
+ const startTime = Date.now();
3323
+ let isFirstPoll = true;
3324
+ while (true) {
3325
+ const status = await this.client.getReprocessStatus(this.statusUrl, isFirstPoll);
3326
+ isFirstPoll = false;
3327
+ const editStatus = {
3328
+ phase: status.status === "DONE" ? "complete" : status.status === "ERROR" ? "error" : "reprocessing",
3329
+ saveComplete: true,
3330
+ reprocessStatus: status,
3331
+ error: status.error
3332
+ };
3333
+ if (opts.onProgress) {
3334
+ opts.onProgress(editStatus);
3335
+ }
3336
+ if (status.status === "DONE" || status.status === "ERROR") {
3337
+ return editStatus;
3338
+ }
3339
+ if (Date.now() - startTime > opts.timeoutMs) {
3340
+ return {
3341
+ phase: "error",
3342
+ saveComplete: true,
3343
+ reprocessStatus: status,
3344
+ error: "Timeout waiting for reprocessing to complete"
3345
+ };
3346
+ }
3347
+ await new Promise((resolve) => setTimeout(resolve, opts.intervalMs));
3348
+ }
3349
+ }
3350
+ /**
3351
+ * Get current status without waiting
3352
+ */
3353
+ async getStatus() {
3354
+ if (!this.statusUrl) {
3355
+ return {
3356
+ phase: this.result?.saved ? "complete" : "idle",
3357
+ saveComplete: !!this.result?.saved
3358
+ };
3359
+ }
3360
+ const status = await this.client.getReprocessStatus(this.statusUrl);
3361
+ return {
3362
+ phase: status.status === "DONE" ? "complete" : status.status === "ERROR" ? "error" : "reprocessing",
3363
+ saveComplete: true,
3364
+ reprocessStatus: status,
3365
+ error: status.error
3366
+ };
3367
+ }
3368
+ // ===========================================================================
3369
+ // Private Helpers
3370
+ // ===========================================================================
3371
+ buildCustomPrompts() {
3372
+ const custom = {};
3373
+ if (this.mode === "ai-prompt") {
3374
+ if (this.prompts["general"]) custom.general = this.prompts["general"];
3375
+ if (this.prompts["pinax"]) custom.pinax = this.prompts["pinax"];
3376
+ if (this.prompts["description"]) custom.description = this.prompts["description"];
3377
+ if (this.prompts["cheimarros"]) custom.cheimarros = this.prompts["cheimarros"];
3378
+ } else {
3379
+ const diffs = this.getDiff();
3380
+ const diffContext = DiffEngine.formatComponentDiffsForPrompt(diffs);
3381
+ const correctionContext = PromptBuilder.buildCorrectionPrompt(this.corrections);
3382
+ const basePrompt = [diffContext, correctionContext, this.prompts["general"]].filter(Boolean).join("\n\n");
3383
+ if (basePrompt) {
3384
+ custom.general = basePrompt;
3385
+ }
3386
+ if (this.prompts["pinax"]) custom.pinax = this.prompts["pinax"];
3387
+ if (this.prompts["description"]) custom.description = this.prompts["description"];
3388
+ if (this.prompts["cheimarros"]) custom.cheimarros = this.prompts["cheimarros"];
3389
+ }
3390
+ return custom;
3391
+ }
3392
+ };
3393
+
3394
+ // src/content/errors.ts
3395
+ var ContentError = class extends Error {
3396
+ constructor(message, code2 = "CONTENT_ERROR", details) {
3397
+ super(message);
3398
+ this.code = code2;
3399
+ this.details = details;
3400
+ this.name = "ContentError";
3401
+ }
3402
+ };
3403
+ var EntityNotFoundError2 = class extends ContentError {
3404
+ constructor(id) {
3405
+ super(`Entity not found: ${id}`, "ENTITY_NOT_FOUND", { id });
3406
+ this.name = "EntityNotFoundError";
3407
+ }
3408
+ };
3409
+ var ContentNotFoundError2 = class extends ContentError {
3410
+ constructor(cid) {
3411
+ super(`Content not found: ${cid}`, "CONTENT_NOT_FOUND", { cid });
3412
+ this.name = "ContentNotFoundError";
3413
+ }
3414
+ };
3415
+ var ComponentNotFoundError = class extends ContentError {
3416
+ constructor(id, componentName) {
3417
+ super(
3418
+ `Component '${componentName}' not found on entity ${id}`,
3419
+ "COMPONENT_NOT_FOUND",
3420
+ { id, componentName }
3421
+ );
3422
+ this.name = "ComponentNotFoundError";
3423
+ }
3424
+ };
3425
+ var VersionNotFoundError = class extends ContentError {
3426
+ constructor(id, selector) {
3427
+ super(
3428
+ `Version not found: ${selector} for entity ${id}`,
3429
+ "VERSION_NOT_FOUND",
3430
+ { id, selector }
3431
+ );
3432
+ this.name = "VersionNotFoundError";
3433
+ }
3434
+ };
3435
+ var NetworkError3 = class extends ContentError {
3436
+ constructor(message, statusCode) {
3437
+ super(message, "NETWORK_ERROR", { statusCode });
3438
+ this.statusCode = statusCode;
3439
+ this.name = "NetworkError";
3440
+ }
3441
+ };
3442
+
3443
+ // src/content/client.ts
3444
+ var ContentClient = class {
3445
+ constructor(config) {
3446
+ this.baseUrl = config.gatewayUrl.replace(/\/$/, "");
3447
+ this.fetchImpl = config.fetchImpl ?? fetch;
3448
+ }
3449
+ // ---------------------------------------------------------------------------
3450
+ // Request helpers
3451
+ // ---------------------------------------------------------------------------
3452
+ buildUrl(path2, query) {
3453
+ const url = new URL(`${this.baseUrl}${path2}`);
3454
+ if (query) {
3455
+ Object.entries(query).forEach(([key, value]) => {
3456
+ if (value !== void 0 && value !== null) {
3457
+ url.searchParams.set(key, String(value));
3458
+ }
3459
+ });
3460
+ }
3461
+ return url.toString();
3462
+ }
3463
+ async request(path2, options = {}) {
3464
+ const url = this.buildUrl(path2, options.query);
3465
+ const headers = new Headers({ "Content-Type": "application/json" });
3466
+ if (options.headers) {
3467
+ Object.entries(options.headers).forEach(([k, v]) => {
3468
+ if (v !== void 0) headers.set(k, v);
3469
+ });
3470
+ }
3471
+ let response;
3472
+ try {
3473
+ response = await this.fetchImpl(url, { ...options, headers });
3474
+ } catch (err) {
3475
+ throw new NetworkError3(
3476
+ err instanceof Error ? err.message : "Network request failed"
3477
+ );
3478
+ }
3479
+ if (response.ok) {
3480
+ const contentType = response.headers.get("content-type") || "";
3481
+ if (contentType.includes("application/json")) {
3482
+ return await response.json();
3483
+ }
3484
+ return await response.text();
3485
+ }
3486
+ let body;
3487
+ const text = await response.text();
3488
+ try {
3489
+ body = JSON.parse(text);
3490
+ } catch {
3491
+ body = text;
3492
+ }
3493
+ if (response.status === 404) {
3494
+ const errorCode = body?.error;
3495
+ if (errorCode === "NOT_FOUND" || errorCode === "ENTITY_NOT_FOUND") {
3496
+ throw new ContentError(
3497
+ body?.message || "Not found",
3498
+ "NOT_FOUND",
3499
+ body
3500
+ );
3501
+ }
3502
+ }
3503
+ const message = body?.error && typeof body.error === "string" ? body.error : body?.message && typeof body.message === "string" ? body.message : `Request failed with status ${response.status}`;
3504
+ throw new ContentError(message, "HTTP_ERROR", {
3505
+ status: response.status,
3506
+ body
3507
+ });
3508
+ }
3509
+ // ---------------------------------------------------------------------------
3510
+ // Entity Operations
3511
+ // ---------------------------------------------------------------------------
3512
+ /**
3513
+ * Get an entity by its Persistent Identifier (PI).
3514
+ *
3515
+ * @param pi - Persistent Identifier (ULID or test PI with II prefix)
3516
+ * @returns Full entity manifest
3517
+ * @throws EntityNotFoundError if the entity doesn't exist
3518
+ *
3519
+ * @example
3520
+ * ```typescript
3521
+ * const entity = await content.get('01K75HQQXNTDG7BBP7PS9AWYAN');
3522
+ * console.log('Version:', entity.ver);
3523
+ * console.log('Components:', Object.keys(entity.components));
3524
+ * ```
3525
+ */
3526
+ async get(pi) {
3527
+ try {
3528
+ return await this.request(`/api/entities/${encodeURIComponent(pi)}`);
3529
+ } catch (err) {
3530
+ if (err instanceof ContentError && err.code === "NOT_FOUND") {
3531
+ throw new EntityNotFoundError2(pi);
3532
+ }
3533
+ throw err;
3534
+ }
3535
+ }
3536
+ /**
3537
+ * List entities with pagination.
3538
+ *
3539
+ * @param options - Pagination and metadata options
3540
+ * @returns Paginated list of entity summaries
3541
+ *
3542
+ * @example
3543
+ * ```typescript
3544
+ * // Get first page
3545
+ * const page1 = await content.list({ limit: 20, include_metadata: true });
3546
+ *
3547
+ * // Get next page
3548
+ * if (page1.next_cursor) {
3549
+ * const page2 = await content.list({ cursor: page1.next_cursor });
3550
+ * }
3551
+ * ```
3552
+ */
3553
+ async list(options = {}) {
3554
+ return this.request("/api/entities", {
3555
+ query: {
3556
+ limit: options.limit,
3557
+ cursor: options.cursor,
3558
+ include_metadata: options.include_metadata
3559
+ }
3560
+ });
3561
+ }
3562
+ /**
3563
+ * Get version history for an entity.
3564
+ *
3565
+ * @param pi - Persistent Identifier
3566
+ * @param options - Pagination options
3567
+ * @returns Version history (newest first)
3568
+ *
3569
+ * @example
3570
+ * ```typescript
3571
+ * const history = await content.versions('01K75HQQXNTDG7BBP7PS9AWYAN');
3572
+ * console.log('Total versions:', history.items.length);
3573
+ * history.items.forEach(v => {
3574
+ * console.log(`v${v.ver}: ${v.ts} - ${v.note || 'no note'}`);
3575
+ * });
3576
+ * ```
3577
+ */
3578
+ async versions(pi, options = {}) {
3579
+ try {
3580
+ return await this.request(
3581
+ `/api/entities/${encodeURIComponent(pi)}/versions`,
3582
+ {
3583
+ query: {
3584
+ limit: options.limit,
3585
+ cursor: options.cursor
3586
+ }
3587
+ }
3588
+ );
3589
+ } catch (err) {
3590
+ if (err instanceof ContentError && err.code === "NOT_FOUND") {
3591
+ throw new EntityNotFoundError2(pi);
3592
+ }
3593
+ throw err;
3594
+ }
3595
+ }
3596
+ /**
3597
+ * Get a specific version of an entity.
3598
+ *
3599
+ * @param pi - Persistent Identifier
3600
+ * @param selector - Version selector: 'ver:N' for version number or 'cid:...' for CID
3601
+ * @returns Entity manifest for the specified version
3602
+ *
3603
+ * @example
3604
+ * ```typescript
3605
+ * // Get version 2
3606
+ * const v2 = await content.getVersion('01K75HQQXNTDG7BBP7PS9AWYAN', 'ver:2');
3607
+ *
3608
+ * // Get by CID
3609
+ * const vByCid = await content.getVersion('01K75HQQXNTDG7BBP7PS9AWYAN', 'cid:bafybeih...');
3610
+ * ```
3611
+ */
3612
+ async getVersion(pi, selector) {
3613
+ try {
3614
+ return await this.request(
3615
+ `/api/entities/${encodeURIComponent(pi)}/versions/${encodeURIComponent(selector)}`
3616
+ );
3617
+ } catch (err) {
3618
+ if (err instanceof ContentError && err.code === "NOT_FOUND") {
3619
+ throw new EntityNotFoundError2(pi);
3620
+ }
3621
+ throw err;
3622
+ }
3623
+ }
3624
+ /**
3625
+ * Resolve a PI to its tip CID (fast lookup without fetching manifest).
3626
+ *
3627
+ * @param pi - Persistent Identifier
3628
+ * @returns PI and tip CID
3629
+ *
3630
+ * @example
3631
+ * ```typescript
3632
+ * const { tip } = await content.resolve('01K75HQQXNTDG7BBP7PS9AWYAN');
3633
+ * console.log('Latest manifest CID:', tip);
3634
+ * ```
3635
+ */
3636
+ async resolve(pi) {
3637
+ try {
3638
+ return await this.request(`/api/resolve/${encodeURIComponent(pi)}`);
3639
+ } catch (err) {
3640
+ if (err instanceof ContentError && err.code === "NOT_FOUND") {
3641
+ throw new EntityNotFoundError2(pi);
3642
+ }
3643
+ throw err;
3644
+ }
3645
+ }
3646
+ /**
3647
+ * Get the list of child PIs for an entity (fast, returns only PIs).
3648
+ *
3649
+ * @param pi - Persistent Identifier of parent entity
3650
+ * @returns Array of child PIs
3651
+ *
3652
+ * @example
3653
+ * ```typescript
3654
+ * const childPis = await content.children('01K75HQQXNTDG7BBP7PS9AWYAN');
3655
+ * console.log('Children:', childPis);
3656
+ * ```
3657
+ */
3658
+ async children(pi) {
3659
+ const entity = await this.get(pi);
3660
+ return entity.children_pi || [];
3661
+ }
3662
+ /**
3663
+ * Get all child entities for a parent (fetches full entity for each child).
3664
+ *
3665
+ * @param pi - Persistent Identifier of parent entity
3666
+ * @returns Array of child entities
3667
+ *
3668
+ * @example
3669
+ * ```typescript
3670
+ * const childEntities = await content.childrenEntities('01K75HQQXNTDG7BBP7PS9AWYAN');
3671
+ * childEntities.forEach(child => {
3672
+ * console.log(`${child.pi}: v${child.ver}`);
3673
+ * });
3674
+ * ```
3675
+ */
3676
+ async childrenEntities(pi) {
3677
+ const childPis = await this.children(pi);
3678
+ if (childPis.length === 0) {
3679
+ return [];
3680
+ }
3681
+ const results = await Promise.allSettled(
3682
+ childPis.map((childPi) => this.get(childPi))
3683
+ );
3684
+ return results.filter((r) => r.status === "fulfilled").map((r) => r.value);
3685
+ }
3686
+ /**
3687
+ * Get the Arke origin block (root of the archive tree).
3688
+ *
3689
+ * @returns Arke origin entity
3690
+ *
3691
+ * @example
3692
+ * ```typescript
3693
+ * const origin = await content.arke();
3694
+ * console.log('Arke origin:', origin.pi);
3695
+ * ```
3696
+ */
3697
+ async arke() {
3698
+ return this.request("/api/arke");
3699
+ }
3700
+ // ---------------------------------------------------------------------------
3701
+ // Content Download
3702
+ // ---------------------------------------------------------------------------
3703
+ /**
3704
+ * Download content by CID.
3705
+ *
3706
+ * Returns Blob in browser environments, Buffer in Node.js.
3707
+ *
3708
+ * @param cid - Content Identifier
3709
+ * @returns Content as Blob (browser) or Buffer (Node)
3710
+ * @throws ContentNotFoundError if the content doesn't exist
3711
+ *
3712
+ * @example
3713
+ * ```typescript
3714
+ * const content = await client.download('bafybeih...');
3715
+ *
3716
+ * // In browser
3717
+ * const url = URL.createObjectURL(content as Blob);
3718
+ *
3719
+ * // In Node.js
3720
+ * fs.writeFileSync('output.bin', content as Buffer);
3721
+ * ```
3722
+ */
3723
+ async download(cid) {
3724
+ const url = this.buildUrl(`/api/cat/${encodeURIComponent(cid)}`);
3725
+ let response;
3726
+ try {
3727
+ response = await this.fetchImpl(url);
3728
+ } catch (err) {
3729
+ throw new NetworkError3(
3730
+ err instanceof Error ? err.message : "Network request failed"
3731
+ );
3732
+ }
3733
+ if (!response.ok) {
3734
+ if (response.status === 404) {
3735
+ throw new ContentNotFoundError2(cid);
3736
+ }
3737
+ throw new ContentError(
3738
+ `Failed to download content: ${response.status}`,
3739
+ "DOWNLOAD_ERROR",
3740
+ { status: response.status }
3741
+ );
3742
+ }
3743
+ if (typeof window !== "undefined") {
3744
+ return response.blob();
3745
+ } else {
3746
+ const arrayBuffer = await response.arrayBuffer();
3747
+ return Buffer.from(arrayBuffer);
3748
+ }
3749
+ }
3750
+ /**
3751
+ * Get a direct URL for content by CID.
3752
+ *
3753
+ * This is useful for embedding in img tags or for direct downloads.
3754
+ *
3755
+ * @param cid - Content Identifier
3756
+ * @returns URL string
3757
+ *
3758
+ * @example
3759
+ * ```typescript
3760
+ * const url = content.getUrl('bafybeih...');
3761
+ * // Use in img tag: <img src={url} />
3762
+ * ```
3763
+ */
3764
+ getUrl(cid) {
3765
+ return `${this.baseUrl}/api/cat/${encodeURIComponent(cid)}`;
3766
+ }
3767
+ /**
3768
+ * Stream content by CID.
3769
+ *
3770
+ * @param cid - Content Identifier
3771
+ * @returns ReadableStream of the content
3772
+ * @throws ContentNotFoundError if the content doesn't exist
3773
+ *
3774
+ * @example
3775
+ * ```typescript
3776
+ * const stream = await content.stream('bafybeih...');
3777
+ * const reader = stream.getReader();
3778
+ * while (true) {
3779
+ * const { done, value } = await reader.read();
3780
+ * if (done) break;
3781
+ * // Process chunk
3782
+ * }
3783
+ * ```
3784
+ */
3785
+ async stream(cid) {
3786
+ const url = this.buildUrl(`/api/cat/${encodeURIComponent(cid)}`);
3787
+ let response;
3788
+ try {
3789
+ response = await this.fetchImpl(url);
3790
+ } catch (err) {
3791
+ throw new NetworkError3(
3792
+ err instanceof Error ? err.message : "Network request failed"
3793
+ );
3794
+ }
3795
+ if (!response.ok) {
3796
+ if (response.status === 404) {
3797
+ throw new ContentNotFoundError2(cid);
3798
+ }
3799
+ throw new ContentError(
3800
+ `Failed to stream content: ${response.status}`,
3801
+ "STREAM_ERROR",
3802
+ { status: response.status }
3803
+ );
3804
+ }
3805
+ if (!response.body) {
3806
+ throw new ContentError("Response body is not available", "STREAM_ERROR");
3807
+ }
3808
+ return response.body;
3809
+ }
3810
+ /**
3811
+ * Download a DAG node (JSON) by CID.
3812
+ *
3813
+ * Use this to fetch JSON components like properties and relationships.
3814
+ *
3815
+ * @param cid - Content Identifier of the DAG node
3816
+ * @returns Parsed JSON object
3817
+ * @throws ContentNotFoundError if the content doesn't exist
3818
+ *
3819
+ * @example
3820
+ * ```typescript
3821
+ * const relationships = await content.getDag<RelationshipsComponent>(
3822
+ * entity.components.relationships
3823
+ * );
3824
+ * console.log('Relationships:', relationships.relationships);
3825
+ * ```
3826
+ */
3827
+ async getDag(cid) {
3828
+ const url = this.buildUrl(`/api/dag/${encodeURIComponent(cid)}`);
3829
+ let response;
3830
+ try {
3831
+ response = await this.fetchImpl(url);
3832
+ } catch (err) {
3833
+ throw new NetworkError3(
3834
+ err instanceof Error ? err.message : "Network request failed"
3835
+ );
3836
+ }
3837
+ if (!response.ok) {
3838
+ if (response.status === 404) {
3839
+ throw new ContentNotFoundError2(cid);
3840
+ }
3841
+ throw new ContentError(
3842
+ `Failed to fetch DAG node: ${response.status}`,
3843
+ "DAG_ERROR",
3844
+ { status: response.status }
3845
+ );
3846
+ }
3847
+ return await response.json();
3848
+ }
3849
+ // ---------------------------------------------------------------------------
3850
+ // Component Helpers
3851
+ // ---------------------------------------------------------------------------
3852
+ /**
3853
+ * Download a component from an entity.
3854
+ *
3855
+ * @param entity - Entity containing the component
3856
+ * @param componentName - Name of the component (e.g., 'pinax', 'description', 'source')
3857
+ * @returns Component content as Blob (browser) or Buffer (Node)
3858
+ * @throws ComponentNotFoundError if the component doesn't exist
3859
+ *
3860
+ * @example
3861
+ * ```typescript
3862
+ * const entity = await content.get('01K75HQQXNTDG7BBP7PS9AWYAN');
3863
+ * const pinax = await content.getComponent(entity, 'pinax');
3864
+ * ```
3865
+ */
3866
+ async getComponent(entity, componentName) {
3867
+ const cid = entity.components[componentName];
3868
+ if (!cid) {
3869
+ throw new ComponentNotFoundError(entity.id, componentName);
3870
+ }
3871
+ return this.download(cid);
3872
+ }
3873
+ /**
3874
+ * Get the URL for a component from an entity.
3875
+ *
3876
+ * @param entity - Entity containing the component
3877
+ * @param componentName - Name of the component
3878
+ * @returns URL string
3879
+ * @throws ComponentNotFoundError if the component doesn't exist
3880
+ *
3881
+ * @example
3882
+ * ```typescript
3883
+ * const entity = await content.get('01K75HQQXNTDG7BBP7PS9AWYAN');
3884
+ * const imageUrl = content.getComponentUrl(entity, 'source');
3885
+ * // Use in img tag: <img src={imageUrl} />
3886
+ * ```
3887
+ */
3888
+ getComponentUrl(entity, componentName) {
3889
+ const cid = entity.components[componentName];
3890
+ if (!cid) {
3891
+ throw new ComponentNotFoundError(entity.id, componentName);
3892
+ }
3893
+ return this.getUrl(cid);
3894
+ }
3895
+ /**
3896
+ * Get the properties component for an entity.
3897
+ *
3898
+ * @param entity - Entity containing the properties component
3899
+ * @returns Properties object, or null if no properties component exists
3900
+ *
3901
+ * @example
3902
+ * ```typescript
3903
+ * const entity = await content.get('01K75HQQXNTDG7BBP7PS9AWYAN');
3904
+ * const props = await content.getProperties(entity);
3905
+ * if (props) {
3906
+ * console.log('Title:', props.title);
3907
+ * }
3908
+ * ```
3909
+ */
3910
+ async getProperties(entity) {
3911
+ const cid = entity.components.properties;
3912
+ if (!cid) {
3913
+ return null;
3914
+ }
3915
+ return this.getDag(cid);
3916
+ }
3917
+ /**
3918
+ * Get the relationships component for an entity.
3919
+ *
3920
+ * @param entity - Entity containing the relationships component
3921
+ * @returns Relationships component, or null if no relationships exist
3922
+ *
3923
+ * @example
3924
+ * ```typescript
3925
+ * const entity = await content.get('01K75HQQXNTDG7BBP7PS9AWYAN');
3926
+ * const rels = await content.getRelationships(entity);
3927
+ * if (rels) {
3928
+ * rels.relationships.forEach(r => {
3929
+ * console.log(`${r.predicate} -> ${r.target_label}`);
3930
+ * });
3931
+ * }
3932
+ * ```
3933
+ */
3934
+ async getRelationships(entity) {
3935
+ const cid = entity.components.relationships;
3936
+ if (!cid) {
3937
+ return null;
3938
+ }
3939
+ return this.getDag(cid);
3940
+ }
3941
+ };
3942
+
3943
+ // src/graph/errors.ts
3944
+ var GraphError = class extends Error {
3945
+ constructor(message, code2 = "GRAPH_ERROR", details) {
3946
+ super(message);
3947
+ this.code = code2;
3948
+ this.details = details;
3949
+ this.name = "GraphError";
3950
+ }
3951
+ };
3952
+ var GraphEntityNotFoundError = class extends GraphError {
3953
+ constructor(canonicalId) {
3954
+ super(`Graph entity not found: ${canonicalId}`, "ENTITY_NOT_FOUND", { canonicalId });
3955
+ this.name = "GraphEntityNotFoundError";
3956
+ }
3957
+ };
3958
+ var NoPathFoundError = class extends GraphError {
3959
+ constructor(sourceIds, targetIds) {
3960
+ super(
3961
+ `No path found between sources and targets`,
3962
+ "NO_PATH_FOUND",
3963
+ { sourceIds, targetIds }
3964
+ );
3965
+ this.name = "NoPathFoundError";
3966
+ }
3967
+ };
3968
+ var NetworkError4 = class extends GraphError {
3969
+ constructor(message, statusCode) {
3970
+ super(message, "NETWORK_ERROR", { statusCode });
3971
+ this.statusCode = statusCode;
3972
+ this.name = "NetworkError";
3973
+ }
3974
+ };
3975
+
3976
+ // src/graph/client.ts
3977
+ var GraphClient = class {
3978
+ constructor(config) {
3979
+ this.baseUrl = config.gatewayUrl.replace(/\/$/, "");
3980
+ this.fetchImpl = config.fetchImpl ?? fetch;
3981
+ }
3982
+ // ---------------------------------------------------------------------------
3983
+ // Request helpers
3984
+ // ---------------------------------------------------------------------------
3985
+ buildUrl(path2, query) {
3986
+ const url = new URL(`${this.baseUrl}${path2}`);
3987
+ if (query) {
3988
+ Object.entries(query).forEach(([key, value]) => {
3989
+ if (value !== void 0 && value !== null) {
3990
+ url.searchParams.set(key, String(value));
3991
+ }
3992
+ });
3993
+ }
3994
+ return url.toString();
3995
+ }
3996
+ async request(path2, options = {}) {
3997
+ const url = this.buildUrl(path2, options.query);
3998
+ const headers = new Headers({ "Content-Type": "application/json" });
3999
+ if (options.headers) {
4000
+ Object.entries(options.headers).forEach(([k, v]) => {
4001
+ if (v !== void 0) headers.set(k, v);
4002
+ });
4003
+ }
4004
+ let response;
4005
+ try {
4006
+ response = await this.fetchImpl(url, { ...options, headers });
4007
+ } catch (err) {
4008
+ throw new NetworkError4(
4009
+ err instanceof Error ? err.message : "Network request failed"
4010
+ );
4011
+ }
4012
+ if (response.ok) {
4013
+ const contentType = response.headers.get("content-type") || "";
4014
+ if (contentType.includes("application/json")) {
4015
+ return await response.json();
4016
+ }
4017
+ return await response.text();
4018
+ }
4019
+ let body;
4020
+ const text = await response.text();
4021
+ try {
4022
+ body = JSON.parse(text);
4023
+ } catch {
4024
+ body = text;
4025
+ }
4026
+ if (response.status === 404) {
4027
+ throw new GraphError(
4028
+ body?.message || "Not found",
4029
+ "NOT_FOUND",
4030
+ body
4031
+ );
4032
+ }
4033
+ const message = body?.error && typeof body.error === "string" ? body.error : body?.message && typeof body.message === "string" ? body.message : `Request failed with status ${response.status}`;
4034
+ throw new GraphError(message, "HTTP_ERROR", {
4035
+ status: response.status,
4036
+ body
4037
+ });
4038
+ }
4039
+ // ---------------------------------------------------------------------------
4040
+ // Code-based Lookups (indexed in GraphDB)
4041
+ // ---------------------------------------------------------------------------
4042
+ /**
4043
+ * Query entities by code with optional type filter.
4044
+ *
4045
+ * @param code - Entity code to search for
4046
+ * @param type - Optional entity type filter
4047
+ * @returns Matching entities
4048
+ *
4049
+ * @example
4050
+ * ```typescript
4051
+ * // Find by code
4052
+ * const entities = await graph.queryByCode('person_john');
4053
+ *
4054
+ * // With type filter
4055
+ * const people = await graph.queryByCode('john', 'person');
4056
+ * ```
4057
+ */
4058
+ async queryByCode(code2, type) {
4059
+ const response = await this.request(
4060
+ "/graphdb/entity/query",
4061
+ {
4062
+ method: "POST",
4063
+ body: JSON.stringify({ code: code2, type })
4064
+ }
4065
+ );
4066
+ if (!response.found || !response.entity) {
4067
+ return [];
4068
+ }
4069
+ return [response.entity];
4070
+ }
4071
+ /**
4072
+ * Look up entities by code across all PIs.
4073
+ *
4074
+ * @param code - Entity code to search for
4075
+ * @param type - Optional entity type filter
4076
+ * @returns Matching entities
4077
+ *
4078
+ * @example
4079
+ * ```typescript
4080
+ * const entities = await graph.lookupByCode('alice_austen', 'person');
4081
+ * ```
4082
+ */
4083
+ async lookupByCode(code2, type) {
4084
+ const response = await this.request(
4085
+ "/graphdb/entities/lookup-by-code",
4086
+ {
4087
+ method: "POST",
4088
+ body: JSON.stringify({ code: code2, type })
4089
+ }
4090
+ );
4091
+ return response.entities || [];
4092
+ }
4093
+ // ---------------------------------------------------------------------------
4094
+ // PI-based Operations
4095
+ // ---------------------------------------------------------------------------
4096
+ /**
4097
+ * List entities extracted from a specific PI or multiple PIs.
4098
+ *
4099
+ * This returns knowledge graph entities (persons, places, events, etc.)
4100
+ * that were extracted from the given PI(s), not the PI entity itself.
4101
+ *
4102
+ * @param pi - Single PI or array of PIs
4103
+ * @param options - Filter options
4104
+ * @returns Extracted entities from the PI(s)
4105
+ *
4106
+ * @example
4107
+ * ```typescript
4108
+ * // From single PI
4109
+ * const entities = await graph.listEntitiesFromPi('01K75HQQXNTDG7BBP7PS9AWYAN');
4110
+ *
4111
+ * // With type filter
4112
+ * const people = await graph.listEntitiesFromPi('01K75HQQXNTDG7BBP7PS9AWYAN', { type: 'person' });
4113
+ *
4114
+ * // From multiple PIs
4115
+ * const all = await graph.listEntitiesFromPi(['pi-1', 'pi-2']);
4116
+ * ```
4117
+ */
4118
+ async listEntitiesFromPi(pi, options = {}) {
4119
+ const pis = Array.isArray(pi) ? pi : [pi];
4120
+ const response = await this.request(
4121
+ "/graphdb/entities/list",
4122
+ {
4123
+ method: "POST",
4124
+ body: JSON.stringify({
4125
+ pis,
4126
+ type: options.type
4127
+ })
4128
+ }
4129
+ );
4130
+ return response.entities || [];
4131
+ }
4132
+ /**
4133
+ * Get entities with their relationships from a PI.
4134
+ *
4135
+ * This is an optimized query that returns entities along with all their
4136
+ * relationship data in a single request.
4137
+ *
4138
+ * @param pi - Persistent Identifier
4139
+ * @param type - Optional entity type filter
4140
+ * @returns Entities with relationships
4141
+ *
4142
+ * @example
4143
+ * ```typescript
4144
+ * const entities = await graph.getEntitiesWithRelationships('01K75HQQXNTDG7BBP7PS9AWYAN');
4145
+ * entities.forEach(e => {
4146
+ * console.log(`${e.label} has ${e.relationships.length} relationships`);
4147
+ * });
4148
+ * ```
4149
+ */
4150
+ async getEntitiesWithRelationships(pi, type) {
4151
+ const response = await this.request(
4152
+ "/graphdb/pi/entities-with-relationships",
4153
+ {
4154
+ method: "POST",
4155
+ body: JSON.stringify({ pi, type })
4156
+ }
4157
+ );
4158
+ return response.entities || [];
4159
+ }
4160
+ /**
4161
+ * Get the lineage (ancestors and/or descendants) of a PI.
4162
+ *
4163
+ * This traverses the PI hierarchy (parent_pi/children_pi relationships)
4164
+ * which is indexed in GraphDB for fast lookups.
4165
+ *
4166
+ * @param pi - Source PI
4167
+ * @param direction - 'ancestors', 'descendants', or 'both'
4168
+ * @param maxHops - Maximum depth to traverse (default: 10)
4169
+ * @returns Lineage data with PIs at each hop level
4170
+ *
4171
+ * @example
4172
+ * ```typescript
4173
+ * // Get ancestors (parent chain)
4174
+ * const lineage = await graph.getLineage('01K75HQQXNTDG7BBP7PS9AWYAN', 'ancestors');
4175
+ *
4176
+ * // Get both directions
4177
+ * const full = await graph.getLineage('01K75HQQXNTDG7BBP7PS9AWYAN', 'both');
4178
+ * ```
4179
+ */
4180
+ async getLineage(pi, direction = "both", maxHops = 10) {
4181
+ return this.request("/graphdb/pi/lineage", {
4182
+ method: "POST",
4183
+ body: JSON.stringify({ sourcePi: pi, direction, maxHops })
4184
+ });
4185
+ }
4186
+ // ---------------------------------------------------------------------------
4187
+ // Relationship Operations
4188
+ // ---------------------------------------------------------------------------
4189
+ /**
4190
+ * Get relationships for an entity from the GraphDB index.
4191
+ *
4192
+ * **Important distinction from ContentClient.getRelationships():**
4193
+ * - **ContentClient.getRelationships()**: Returns OUTBOUND relationships only
4194
+ * (from the entity's relationships.json in IPFS - source of truth)
4195
+ * - **GraphClient.getRelationships()**: Returns BOTH inbound AND outbound
4196
+ * relationships (from the indexed GraphDB mirror)
4197
+ *
4198
+ * Use this method when you need to find "what references this entity" (inbound)
4199
+ * or want a complete bidirectional view.
4200
+ *
4201
+ * @param id - Entity identifier (works for both PIs and KG entities)
4202
+ * @param direction - Filter by direction: 'outgoing', 'incoming', or 'both' (default)
4203
+ * @returns Array of relationships with direction indicator
4204
+ *
4205
+ * @example
4206
+ * ```typescript
4207
+ * // Get all relationships (both directions)
4208
+ * const all = await graph.getRelationships('01K75HQQXNTDG7BBP7PS9AWYAN');
4209
+ *
4210
+ * // Get only inbound relationships ("who references this entity?")
4211
+ * const incoming = await graph.getRelationships('01K75HQQXNTDG7BBP7PS9AWYAN', 'incoming');
4212
+ *
4213
+ * // Get only outbound relationships (similar to IPFS, but from index)
4214
+ * const outgoing = await graph.getRelationships('01K75HQQXNTDG7BBP7PS9AWYAN', 'outgoing');
4215
+ *
4216
+ * // Process by direction
4217
+ * const rels = await graph.getRelationships('entity-id');
4218
+ * rels.forEach(r => {
4219
+ * if (r.direction === 'incoming') {
4220
+ * console.log(`${r.target_label} references this entity via ${r.predicate}`);
4221
+ * } else {
4222
+ * console.log(`This entity ${r.predicate} -> ${r.target_label}`);
4223
+ * }
4224
+ * });
4225
+ * ```
4226
+ */
4227
+ async getRelationships(id, direction = "both") {
4228
+ const response = await this.request(`/graphdb/relationships/${encodeURIComponent(id)}`);
4229
+ if (!response.found || !response.relationships) {
4230
+ return [];
4231
+ }
4232
+ let relationships = response.relationships;
4233
+ if (direction !== "both") {
4234
+ relationships = relationships.filter((rel) => rel.direction === direction);
4235
+ }
4236
+ return relationships.map((rel) => ({
4237
+ direction: rel.direction,
4238
+ predicate: rel.predicate,
4239
+ target_id: rel.target_id,
4240
+ target_code: rel.target_code || "",
4241
+ target_label: rel.target_label,
4242
+ target_type: rel.target_type,
4243
+ properties: rel.properties,
4244
+ source_pi: rel.source_pi,
4245
+ created_at: rel.created_at
4246
+ }));
4247
+ }
4248
+ // ---------------------------------------------------------------------------
4249
+ // Path Finding
4250
+ // ---------------------------------------------------------------------------
4251
+ /**
4252
+ * Find shortest paths between sets of entities.
4253
+ *
4254
+ * @param sourceIds - Starting entity IDs
4255
+ * @param targetIds - Target entity IDs
4256
+ * @param options - Path finding options
4257
+ * @returns Found paths
4258
+ *
4259
+ * @example
4260
+ * ```typescript
4261
+ * const paths = await graph.findPaths(
4262
+ * ['entity-alice'],
4263
+ * ['entity-bob'],
4264
+ * { max_depth: 4, direction: 'both' }
4265
+ * );
4266
+ *
4267
+ * paths.forEach(path => {
4268
+ * console.log(`Path of length ${path.length}:`);
4269
+ * path.edges.forEach(e => {
4270
+ * console.log(` ${e.subject_label} -[${e.predicate}]-> ${e.object_label}`);
4271
+ * });
4272
+ * });
4273
+ * ```
4274
+ */
4275
+ async findPaths(sourceIds, targetIds, options = {}) {
4276
+ const response = await this.request("/graphdb/paths/between", {
4277
+ method: "POST",
4278
+ body: JSON.stringify({
4279
+ source_ids: sourceIds,
4280
+ target_ids: targetIds,
4281
+ max_depth: options.max_depth,
4282
+ direction: options.direction,
4283
+ limit: options.limit
4284
+ })
4285
+ });
4286
+ return response.paths || [];
4287
+ }
4288
+ /**
4289
+ * Find entities of a specific type reachable from starting entities.
4290
+ *
4291
+ * @param startIds - Starting entity IDs
4292
+ * @param targetType - Type of entities to find
4293
+ * @param options - Search options
4294
+ * @returns Reachable entities of the specified type
4295
+ *
4296
+ * @example
4297
+ * ```typescript
4298
+ * // Find all people reachable from an event
4299
+ * const people = await graph.findReachable(
4300
+ * ['event-id'],
4301
+ * 'person',
4302
+ * { max_depth: 3 }
4303
+ * );
4304
+ * ```
4305
+ */
4306
+ async findReachable(startIds, targetType, options = {}) {
4307
+ const response = await this.request(
4308
+ "/graphdb/paths/reachable",
4309
+ {
4310
+ method: "POST",
4311
+ body: JSON.stringify({
4312
+ start_ids: startIds,
4313
+ target_type: targetType,
4314
+ max_depth: options.max_depth,
4315
+ direction: options.direction,
4316
+ limit: options.limit
4317
+ })
4318
+ }
4319
+ );
4320
+ return response.entities || [];
4321
+ }
4322
+ /**
4323
+ * Check the health of the graph service.
4324
+ *
4325
+ * @returns Health status
4326
+ */
4327
+ async health() {
4328
+ return this.request("/graphdb/health", { method: "GET" });
4329
+ }
4330
+ };
1637
4331
  // Annotate the CommonJS export names for ESM import in node:
1638
4332
  0 && (module.exports = {
1639
4333
  ArkeUploader,
1640
4334
  CollectionsClient,
1641
4335
  CollectionsError,
4336
+ ComponentNotFoundError,
4337
+ ContentClient,
4338
+ ContentError,
4339
+ ContentNetworkError,
4340
+ ContentNotFoundError,
4341
+ EditClient,
4342
+ EditError,
4343
+ EditSession,
4344
+ EntityNotFoundError,
4345
+ GraphClient,
4346
+ GraphEntityNotFoundError,
4347
+ GraphError,
4348
+ GraphNetworkError,
1642
4349
  NetworkError,
4350
+ NoPathFoundError,
4351
+ PermissionError,
4352
+ QueryClient,
4353
+ QueryError,
1643
4354
  ScanError,
1644
4355
  UploadClient,
1645
4356
  UploadError,
1646
4357
  ValidationError,
4358
+ VersionNotFoundError,
1647
4359
  WorkerAPIError
1648
4360
  });
1649
4361
  //# sourceMappingURL=index.cjs.map