@manta-eu/client 0.1.9 → 0.1.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +21 -0
- package/dist/generated/graphql.d.ts +658 -1
- package/dist/generated/graphql.d.ts.map +1 -1
- package/dist/generated/graphql.js +371 -0
- package/dist/generated/graphql.js.map +1 -0
- package/dist/graphql-clients.d.ts +10 -1
- package/dist/graphql-clients.d.ts.map +1 -1
- package/dist/graphql-clients.js +25 -3
- package/dist/graphql-clients.js.map +1 -0
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -0
- package/dist/shared/action-document.js +1 -0
- package/dist/shared/action-document.js.map +1 -0
- package/dist/shared/action-plan.js +1 -0
- package/dist/shared/action-plan.js.map +1 -0
- package/dist/shared/agent-mode.js +1 -0
- package/dist/shared/agent-mode.js.map +1 -0
- package/dist/shared/agent-types.js +1 -0
- package/dist/shared/agent-types.js.map +1 -0
- package/dist/shared/chat-context.js +1 -0
- package/dist/shared/chat-context.js.map +1 -0
- package/dist/shared/chat-manager.d.ts +2 -0
- package/dist/shared/chat-manager.d.ts.map +1 -1
- package/dist/shared/chat-manager.js +3 -0
- package/dist/shared/chat-manager.js.map +1 -0
- package/dist/shared/chat-turn-errors.js +1 -0
- package/dist/shared/chat-turn-errors.js.map +1 -0
- package/dist/shared/constants.js +1 -0
- package/dist/shared/constants.js.map +1 -0
- package/dist/shared/conversation-starter-defaults.js +1 -0
- package/dist/shared/conversation-starter-defaults.js.map +1 -0
- package/dist/shared/datastore-id.js +1 -0
- package/dist/shared/datastore-id.js.map +1 -0
- package/dist/shared/datastore-path.js +1 -0
- package/dist/shared/datastore-path.js.map +1 -0
- package/dist/shared/duration.js +1 -0
- package/dist/shared/duration.js.map +1 -0
- package/dist/shared/email.js +1 -0
- package/dist/shared/email.js.map +1 -0
- package/dist/shared/entity-set.js +1 -0
- package/dist/shared/entity-set.js.map +1 -0
- package/dist/shared/error-utils.js +1 -0
- package/dist/shared/error-utils.js.map +1 -0
- package/dist/shared/explorer-paths.js +1 -0
- package/dist/shared/explorer-paths.js.map +1 -0
- package/dist/shared/fileIri.js +1 -0
- package/dist/shared/fileIri.js.map +1 -0
- package/dist/shared/index.js +1 -0
- package/dist/shared/index.js.map +1 -0
- package/dist/shared/inferal-stream.js +1 -0
- package/dist/shared/inferal-stream.js.map +1 -0
- package/dist/shared/live-context.js +1 -0
- package/dist/shared/live-context.js.map +1 -0
- package/dist/shared/maintenance-schedule.js +1 -0
- package/dist/shared/maintenance-schedule.js.map +1 -0
- package/dist/shared/memory-citation-sampling.js +1 -0
- package/dist/shared/memory-citation-sampling.js.map +1 -0
- package/dist/shared/memory-confidence.js +1 -0
- package/dist/shared/memory-confidence.js.map +1 -0
- package/dist/shared/memory-consolidation-limits.js +1 -0
- package/dist/shared/memory-consolidation-limits.js.map +1 -0
- package/dist/shared/memory-dates.js +1 -0
- package/dist/shared/memory-dates.js.map +1 -0
- package/dist/shared/memory-decay.js +1 -0
- package/dist/shared/memory-decay.js.map +1 -0
- package/dist/shared/memory-diversity.js +1 -0
- package/dist/shared/memory-diversity.js.map +1 -0
- package/dist/shared/memory-entity-recall.js +1 -0
- package/dist/shared/memory-entity-recall.js.map +1 -0
- package/dist/shared/memory-importance.js +1 -0
- package/dist/shared/memory-importance.js.map +1 -0
- package/dist/shared/memory-keyword-recall.js +1 -0
- package/dist/shared/memory-keyword-recall.js.map +1 -0
- package/dist/shared/memory-kind-iris.js +1 -0
- package/dist/shared/memory-kind-iris.js.map +1 -0
- package/dist/shared/memory-preload-gate.js +1 -0
- package/dist/shared/memory-preload-gate.js.map +1 -0
- package/dist/shared/memory-sources.js +1 -0
- package/dist/shared/memory-sources.js.map +1 -0
- package/dist/shared/memory-teams.js +1 -0
- package/dist/shared/memory-teams.js.map +1 -0
- package/dist/shared/memory-token-budget.js +1 -0
- package/dist/shared/memory-token-budget.js.map +1 -0
- package/dist/shared/memory-volatility.js +1 -0
- package/dist/shared/memory-volatility.js.map +1 -0
- package/dist/shared/message-utils.js +1 -0
- package/dist/shared/message-utils.js.map +1 -0
- package/dist/shared/model.js +1 -0
- package/dist/shared/model.js.map +1 -0
- package/dist/shared/org-roles.js +1 -0
- package/dist/shared/org-roles.js.map +1 -0
- package/dist/shared/page-context.js +1 -0
- package/dist/shared/page-context.js.map +1 -0
- package/dist/shared/permissions.js +1 -0
- package/dist/shared/permissions.js.map +1 -0
- package/dist/shared/questionnaire.js +1 -0
- package/dist/shared/questionnaire.js.map +1 -0
- package/dist/shared/rdf-namespaces.js +1 -0
- package/dist/shared/rdf-namespaces.js.map +1 -0
- package/dist/shared/rule-iri.js +1 -0
- package/dist/shared/rule-iri.js.map +1 -0
- package/dist/shared/rule-search.js +1 -0
- package/dist/shared/rule-search.js.map +1 -0
- package/dist/shared/session-types.js +1 -0
- package/dist/shared/session-types.js.map +1 -0
- package/dist/shared/stakeholder-iri.js +1 -0
- package/dist/shared/stakeholder-iri.js.map +1 -0
- package/dist/shared/team-activation.js +1 -0
- package/dist/shared/team-activation.js.map +1 -0
- package/dist/shared/team-filters.js +1 -0
- package/dist/shared/team-filters.js.map +1 -0
- package/dist/shared/team.js +1 -0
- package/dist/shared/team.js.map +1 -0
- package/dist/shared/tools/coercers.js +1 -0
- package/dist/shared/tools/coercers.js.map +1 -0
- package/dist/shared/tools/index.js +1 -0
- package/dist/shared/tools/index.js.map +1 -0
- package/dist/shared/tools/metadata.js +1 -0
- package/dist/shared/tools/metadata.js.map +1 -0
- package/dist/shared/tools/test-triple.js +1 -0
- package/dist/shared/tools/test-triple.js.map +1 -0
- package/dist/shared/tools/types.js +1 -0
- package/dist/shared/tools/types.js.map +1 -0
- package/dist/shared/upload.js +1 -0
- package/dist/shared/upload.js.map +1 -0
- package/dist/shared/website-url.js +1 -0
- package/dist/shared/website-url.js.map +1 -0
- package/dist/shared/workspace-artifacts.js +1 -0
- package/dist/shared/workspace-artifacts.js.map +1 -0
- package/dist/shared/writable-shape-query.js +1 -0
- package/dist/shared/writable-shape-query.js.map +1 -0
- package/dist/types/jsonld.js +1 -0
- package/dist/types/jsonld.js.map +1 -0
- package/dist/types/sparql-types.js +1 -0
- package/dist/types/sparql-types.js.map +1 -0
- package/package.json +11 -6
- package/src/generated/graphql.ts +11329 -0
- package/src/graphql-clients.ts +67 -0
- package/src/index.ts +2 -0
- package/src/shared/action-document.ts +56 -0
- package/src/shared/action-plan.ts +1 -0
- package/src/shared/agent-mode.ts +46 -0
- package/src/shared/agent-types.ts +46 -0
- package/src/shared/chat-context.ts +47 -0
- package/src/shared/chat-manager.ts +197 -0
- package/src/shared/chat-turn-errors.ts +25 -0
- package/src/shared/constants.ts +17 -0
- package/src/shared/conversation-starter-defaults.ts +27 -0
- package/src/shared/datastore-id.ts +25 -0
- package/src/shared/datastore-path.ts +80 -0
- package/src/shared/duration.ts +61 -0
- package/src/shared/email.ts +54 -0
- package/src/shared/entity-set.ts +33 -0
- package/src/shared/error-utils.ts +79 -0
- package/src/shared/explorer-paths.ts +9 -0
- package/src/shared/fileIri.ts +25 -0
- package/src/shared/index.ts +31 -0
- package/src/shared/inferal-stream.ts +2 -0
- package/src/shared/live-context.ts +15 -0
- package/src/shared/maintenance-schedule.ts +47 -0
- package/src/shared/memory-citation-sampling.ts +26 -0
- package/src/shared/memory-confidence.ts +8 -0
- package/src/shared/memory-consolidation-limits.ts +9 -0
- package/src/shared/memory-dates.ts +7 -0
- package/src/shared/memory-decay.ts +70 -0
- package/src/shared/memory-diversity.ts +55 -0
- package/src/shared/memory-entity-recall.ts +26 -0
- package/src/shared/memory-importance.ts +12 -0
- package/src/shared/memory-keyword-recall.ts +149 -0
- package/src/shared/memory-kind-iris.ts +36 -0
- package/src/shared/memory-preload-gate.ts +150 -0
- package/src/shared/memory-sources.ts +5 -0
- package/src/shared/memory-teams.ts +3 -0
- package/src/shared/memory-token-budget.ts +31 -0
- package/src/shared/memory-volatility.ts +90 -0
- package/src/shared/message-utils.ts +37 -0
- package/src/shared/model.ts +117 -0
- package/src/shared/org-roles.ts +21 -0
- package/src/shared/page-context.ts +7 -0
- package/src/shared/permissions.ts +19 -0
- package/src/shared/questionnaire.ts +76 -0
- package/src/shared/rdf-namespaces.ts +62 -0
- package/src/shared/rule-iri.ts +15 -0
- package/src/shared/rule-search.ts +1 -0
- package/src/shared/session-types.ts +14 -0
- package/src/shared/stakeholder-iri.ts +23 -0
- package/src/shared/team-activation.ts +9 -0
- package/src/shared/team-filters.ts +33 -0
- package/src/shared/team.ts +243 -0
- package/src/shared/tools/coercers.ts +82 -0
- package/src/shared/tools/index.ts +49 -0
- package/src/shared/tools/metadata.ts +205 -0
- package/src/shared/tools/test-triple.ts +33 -0
- package/src/shared/tools/types.ts +302 -0
- package/src/shared/upload.ts +57 -0
- package/src/shared/website-url.ts +32 -0
- package/src/shared/workspace-artifacts.ts +68 -0
- package/src/shared/writable-shape-query.ts +24 -0
- package/src/types/jsonld.ts +19 -0
- package/src/types/sparql-types.ts +10 -0
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { ClientError, GraphQLClient } from "graphql-request";
|
|
2
|
+
|
|
3
|
+
// Default client constructors for the v1 and Manta GraphQL APIs
|
|
4
|
+
|
|
5
|
+
export const operatorRoleHeaders = { "x-hasura-role": "operator" } as const;
|
|
6
|
+
|
|
7
|
+
export const v1GQLPath = "/v1/graphql";
|
|
8
|
+
|
|
9
|
+
// To use custom keycloak headers, use headers: { "X-Keycloak-Token": keycloakToken } instead of credentials
|
|
10
|
+
export function v1GraphQLClient(baseUrl: URL) {
|
|
11
|
+
const url = new URL(v1GQLPath, baseUrl);
|
|
12
|
+
return new GraphQLClient(url.toString(), {
|
|
13
|
+
credentials: "include",
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const mantaGQLPath = "/graphql";
|
|
18
|
+
|
|
19
|
+
// A bearer is the credential: sending cookies with it needs credentialed CORS, which only trusted origins get.
|
|
20
|
+
export function mantaGraphQLCredentials(headers?: Record<string, string>): RequestCredentials {
|
|
21
|
+
const sendsBearer = Object.keys(headers ?? {}).some((name) => name.toLowerCase() === "authorization");
|
|
22
|
+
return sendsBearer ? "omit" : "include";
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Called before every request; a source that can also `renew()` is renewed before the one retry a 401 earns. */
|
|
26
|
+
export type AccessTokenSource = (() => Promise<string>) & { renew?: (rejected?: string) => void };
|
|
27
|
+
|
|
28
|
+
export type MantaGraphQLClientOptions = { getAccessToken?: AccessTokenSource };
|
|
29
|
+
|
|
30
|
+
export function bearerFetch(getAccessToken: AccessTokenSource, fetchImpl?: typeof fetch): typeof fetch {
|
|
31
|
+
const send = async (input: RequestInfo | URL, init: RequestInit | undefined, token: string) => {
|
|
32
|
+
const headers = new Headers(init?.headers);
|
|
33
|
+
headers.set("Authorization", `Bearer ${token}`);
|
|
34
|
+
return (fetchImpl ?? globalThis.fetch)(input, { ...init, headers, credentials: "omit" });
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
return async (input, init) => {
|
|
38
|
+
const token = await getAccessToken();
|
|
39
|
+
const response = await send(input, init, token);
|
|
40
|
+
if (response.status !== 401) return response;
|
|
41
|
+
|
|
42
|
+
getAccessToken.renew?.(token);
|
|
43
|
+
return send(input, init, await getAccessToken());
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function mantaGraphQLClient(
|
|
48
|
+
baseUrl: URL,
|
|
49
|
+
onUnauthorized?: () => void,
|
|
50
|
+
headers?: Record<string, string>,
|
|
51
|
+
options?: MantaGraphQLClientOptions,
|
|
52
|
+
) {
|
|
53
|
+
const url = new URL(mantaGQLPath, baseUrl);
|
|
54
|
+
const getAccessToken = options?.getAccessToken;
|
|
55
|
+
return new GraphQLClient(url.toString(), {
|
|
56
|
+
credentials: getAccessToken ? "omit" : mantaGraphQLCredentials(headers),
|
|
57
|
+
headers,
|
|
58
|
+
...(getAccessToken ? { fetch: bearerFetch(getAccessToken) } : {}),
|
|
59
|
+
responseMiddleware: onUnauthorized
|
|
60
|
+
? (response) => {
|
|
61
|
+
if (response instanceof ClientError && response.response.status === 401) {
|
|
62
|
+
onUnauthorized();
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
: undefined,
|
|
66
|
+
});
|
|
67
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import jsonld from "jsonld";
|
|
2
|
+
import { encodeEntity, type Lens, type Schema } from "ldkit";
|
|
3
|
+
import { xsd } from "ldkit/namespaces";
|
|
4
|
+
|
|
5
|
+
export type DocumentTerm = { value: string; termType: string };
|
|
6
|
+
|
|
7
|
+
export type DocumentQuad = {
|
|
8
|
+
subject: DocumentTerm;
|
|
9
|
+
predicate: { value: string };
|
|
10
|
+
object: DocumentTerm & { datatype?: { value: string }; language?: string };
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export type InsertEntity<S extends Schema> = Parameters<Lens<S>["insert"]>[number];
|
|
14
|
+
|
|
15
|
+
// jsonld keeps the `_:` prefix in a blank node value
|
|
16
|
+
function jsonldTerm(term: DocumentTerm): DocumentTerm {
|
|
17
|
+
return { termType: term.termType, value: term.termType === "BlankNode" ? `_:${term.value}` : term.value };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function jsonldQuad(quad: DocumentQuad) {
|
|
21
|
+
const { subject, predicate, object } = quad;
|
|
22
|
+
|
|
23
|
+
return {
|
|
24
|
+
subject: jsonldTerm(subject),
|
|
25
|
+
predicate: { termType: "NamedNode", value: predicate.value },
|
|
26
|
+
object:
|
|
27
|
+
object.termType === "Literal"
|
|
28
|
+
? {
|
|
29
|
+
termType: "Literal",
|
|
30
|
+
value: object.value,
|
|
31
|
+
language: object.language,
|
|
32
|
+
datatype: { value: object.datatype?.value ?? xsd.string },
|
|
33
|
+
}
|
|
34
|
+
: jsonldTerm(object),
|
|
35
|
+
graph: { termType: "DefaultGraph", value: "" },
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export async function documentFromQuads(
|
|
40
|
+
quads: readonly DocumentQuad[],
|
|
41
|
+
subjectIri: string,
|
|
42
|
+
): Promise<Record<string, unknown>> {
|
|
43
|
+
const described = quads.filter(({ subject }) => subject.value === subjectIri || subject.termType === "BlankNode");
|
|
44
|
+
const flattened = await jsonld.fromRDF(described.map(jsonldQuad));
|
|
45
|
+
|
|
46
|
+
return jsonld.frame(flattened, { "@id": subjectIri, "@embed": "@always" });
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function entityActionDocument<S extends Schema>(
|
|
50
|
+
schema: S,
|
|
51
|
+
entity: InsertEntity<S>,
|
|
52
|
+
): Promise<Record<string, unknown>> {
|
|
53
|
+
const quads = encodeEntity(schema, entity).filter((quad) => quad.object.termType !== "Variable");
|
|
54
|
+
|
|
55
|
+
return documentFromQuads(quads, entity.$id);
|
|
56
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { isActionPlanGraphIri } from "@manta-eu/rdf-lib/namespaces";
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { splitIdentifierWords } from "@manta-eu/lib/identifier-words";
|
|
2
|
+
import { baseIri, iriLocalName } from "./rdf-namespaces";
|
|
3
|
+
|
|
4
|
+
export function humanizeEntityName(raw: string): string {
|
|
5
|
+
return splitIdentifierWords(raw);
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function humanizePlanName(raw: string): string {
|
|
9
|
+
return humanizeEntityName(raw.replace(/^ActionPlan_/, ""));
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function extractPlanName(iri: string): string {
|
|
13
|
+
const lastSlash = iri.lastIndexOf("/");
|
|
14
|
+
const lastHash = iri.lastIndexOf("#");
|
|
15
|
+
const lastSeparator = Math.max(lastSlash, lastHash);
|
|
16
|
+
if (lastSeparator > 0) {
|
|
17
|
+
return humanizePlanName(iri.slice(lastSeparator + 1));
|
|
18
|
+
}
|
|
19
|
+
return iri;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function buildPlanIri(planName: string): string {
|
|
23
|
+
if (planName.startsWith("http://") || planName.startsWith("https://")) {
|
|
24
|
+
return planName;
|
|
25
|
+
}
|
|
26
|
+
return `${baseIri}${planName}`;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function planGraphIriToFragment(iri: string) {
|
|
30
|
+
return iriLocalName(iri) || "";
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function buildPlanHref(iri: string) {
|
|
34
|
+
return `/plans/${encodeURIComponent(planGraphIriToFragment(iri))}`;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function buildActivityIri(param: string): string {
|
|
38
|
+
if (param.startsWith("http://") || param.startsWith("https://")) {
|
|
39
|
+
return param;
|
|
40
|
+
}
|
|
41
|
+
return `${baseIri}${param}`;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function buildActivityHref(iri: string) {
|
|
45
|
+
return `/activity/${encodeURIComponent(iriLocalName(iri) || iri)}`;
|
|
46
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { IRI } from "./rdf-namespaces";
|
|
2
|
+
import type { ChatUIMessage } from "./index";
|
|
3
|
+
|
|
4
|
+
export enum ChatContextKind {
|
|
5
|
+
RuleEdit = "RULE_EDIT",
|
|
6
|
+
RuleCreate = "RULE_CREATE",
|
|
7
|
+
Plan = "PLAN",
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export type ChatContext =
|
|
11
|
+
| { kind: ChatContextKind.RuleEdit; ruleIri: IRI }
|
|
12
|
+
| { kind: ChatContextKind.RuleCreate; draftIri: IRI }
|
|
13
|
+
| { kind: ChatContextKind.Plan; planGraphIri: IRI };
|
|
14
|
+
|
|
15
|
+
export function isDraftChatContext(context: ChatContext | undefined): boolean {
|
|
16
|
+
return context?.kind === ChatContextKind.RuleCreate;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function isRuleScopedChatContext(context: ChatContext | undefined): boolean {
|
|
20
|
+
return context?.kind === ChatContextKind.RuleEdit || context?.kind === ChatContextKind.RuleCreate;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export type CachedMemorySchemaProperty = {
|
|
24
|
+
path: string;
|
|
25
|
+
label: string;
|
|
26
|
+
dataType: string;
|
|
27
|
+
required: boolean;
|
|
28
|
+
allowedValues: string[];
|
|
29
|
+
keyProperty: boolean;
|
|
30
|
+
inferred: boolean;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export type CachedMemorySchema = {
|
|
34
|
+
iri: string;
|
|
35
|
+
label: string;
|
|
36
|
+
description: string | null;
|
|
37
|
+
properties: CachedMemorySchemaProperty[];
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export type ChatState = {
|
|
41
|
+
chatIRI: IRI;
|
|
42
|
+
context?: ChatContext;
|
|
43
|
+
contextTokens: number;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export type SendAgentMessage = (message: ChatUIMessage) => Promise<void>;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { match } from "ts-pattern";
|
|
2
|
+
import { v7 as uuidv7 } from "uuid";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { extractPlanName, humanizeEntityName } from "./agent-mode";
|
|
5
|
+
import { ChatContextKind, type ChatContext } from "./agent-types";
|
|
6
|
+
import { expandPrefix, iriLocalName, type IRI } from "./rdf-namespaces";
|
|
7
|
+
|
|
8
|
+
const storedChatContextSchema = z.object({
|
|
9
|
+
kind: z.enum(ChatContextKind),
|
|
10
|
+
iri: z
|
|
11
|
+
.string()
|
|
12
|
+
.min(1)
|
|
13
|
+
.transform((iri) => iri as IRI),
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
export function generateRuleDraftIri(): IRI {
|
|
17
|
+
return expandPrefix(`:RuleDraft_${uuidv7()}`);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function contextChatName(context: ChatContext): string {
|
|
21
|
+
return match(context)
|
|
22
|
+
.with(
|
|
23
|
+
{ kind: ChatContextKind.RuleEdit },
|
|
24
|
+
({ ruleIri }) => `Edit rule: ${humanizeEntityName(iriLocalName(ruleIri).replace(/^Rule_/, ""))}`,
|
|
25
|
+
)
|
|
26
|
+
.with({ kind: ChatContextKind.RuleCreate }, () => "Create rule")
|
|
27
|
+
.with({ kind: ChatContextKind.Plan }, ({ planGraphIri }) => extractPlanName(planGraphIri))
|
|
28
|
+
.exhaustive();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function chatContextIri(context: ChatContext): IRI {
|
|
32
|
+
return match(context)
|
|
33
|
+
.with({ kind: ChatContextKind.RuleEdit }, (c) => c.ruleIri)
|
|
34
|
+
.with({ kind: ChatContextKind.RuleCreate }, (c) => c.draftIri)
|
|
35
|
+
.with({ kind: ChatContextKind.Plan }, (c) => c.planGraphIri)
|
|
36
|
+
.exhaustive();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function parseStoredChatContext(input: unknown): ChatContext {
|
|
40
|
+
const { kind, iri } = storedChatContextSchema.parse(input);
|
|
41
|
+
|
|
42
|
+
return match(kind)
|
|
43
|
+
.with(ChatContextKind.RuleEdit, (): ChatContext => ({ kind: ChatContextKind.RuleEdit, ruleIri: iri }))
|
|
44
|
+
.with(ChatContextKind.RuleCreate, (): ChatContext => ({ kind: ChatContextKind.RuleCreate, draftIri: iri }))
|
|
45
|
+
.with(ChatContextKind.Plan, (): ChatContext => ({ kind: ChatContextKind.Plan, planGraphIri: iri }))
|
|
46
|
+
.exhaustive();
|
|
47
|
+
}
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import { z } from "zod/v4";
|
|
2
|
+
import { ChatContextKind } from "./agent-types";
|
|
3
|
+
import type { IRI } from "./rdf-namespaces";
|
|
4
|
+
|
|
5
|
+
export const chatManagerBasePath = "agents/chat-manager";
|
|
6
|
+
export const chatTicketPath = "agents/chat-ticket";
|
|
7
|
+
export const chatTicketQueryParam = "chat_ticket";
|
|
8
|
+
export const thinkChatFacetPath = "think-chat";
|
|
9
|
+
export const defaultConversationName = "New chat";
|
|
10
|
+
export const defaultFolderName = "New folder";
|
|
11
|
+
|
|
12
|
+
export const chatManagerScopeSchema = z.strictObject({
|
|
13
|
+
version: z.literal(1),
|
|
14
|
+
realm: z.string().min(1),
|
|
15
|
+
datastoreId: z.string().min(1),
|
|
16
|
+
userId: z.string().min(1),
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
export type ChatManagerScope = z.infer<typeof chatManagerScopeSchema>;
|
|
20
|
+
|
|
21
|
+
const shortTextMaxLength = 200;
|
|
22
|
+
const descriptionMaxLength = 4000;
|
|
23
|
+
|
|
24
|
+
const idSchema = z.uuid();
|
|
25
|
+
export const chatIdSchema = idSchema;
|
|
26
|
+
const iriSchema = z
|
|
27
|
+
.string()
|
|
28
|
+
.min(1)
|
|
29
|
+
.transform((iri) => iri as IRI);
|
|
30
|
+
|
|
31
|
+
export const chatContextSchema = z.discriminatedUnion("kind", [
|
|
32
|
+
z.strictObject({ kind: z.literal(ChatContextKind.RuleEdit), ruleIri: iriSchema }),
|
|
33
|
+
z.strictObject({ kind: z.literal(ChatContextKind.RuleCreate), draftIri: iriSchema }),
|
|
34
|
+
z.strictObject({ kind: z.literal(ChatContextKind.Plan), planGraphIri: iriSchema }),
|
|
35
|
+
]);
|
|
36
|
+
|
|
37
|
+
export const chatManagerStateSchema = z.strictObject({
|
|
38
|
+
schemaVersion: z.literal(1),
|
|
39
|
+
scope: chatManagerScopeSchema.nullable(),
|
|
40
|
+
chatListRevision: z.number().int().nonnegative(),
|
|
41
|
+
lastRetentionCheckAt: z.iso.datetime().nullable().default(null),
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
export type ChatManagerState = z.infer<typeof chatManagerStateSchema>;
|
|
45
|
+
|
|
46
|
+
export const conversationLifecycleStatusSchema = z.enum(["creating", "active", "deleting"]);
|
|
47
|
+
export type ConversationLifecycleStatus = z.infer<typeof conversationLifecycleStatusSchema>;
|
|
48
|
+
|
|
49
|
+
export const conversationSummarySchema = z.strictObject({
|
|
50
|
+
id: chatIdSchema,
|
|
51
|
+
chatIRI: z.string().min(1),
|
|
52
|
+
title: z.string().min(1),
|
|
53
|
+
context: chatContextSchema.nullable(),
|
|
54
|
+
folderId: idSchema.nullable(),
|
|
55
|
+
createdAt: z.iso.datetime(),
|
|
56
|
+
updatedAt: z.iso.datetime(),
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
export type ConversationSummary = z.infer<typeof conversationSummarySchema>;
|
|
60
|
+
|
|
61
|
+
export const createConversationInputSchema = z.strictObject({
|
|
62
|
+
name: z.string().trim().min(1).max(shortTextMaxLength).optional(),
|
|
63
|
+
idempotencyKey: z.string().trim().min(1).max(shortTextMaxLength).optional(),
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
export type CreateConversationInput = z.input<typeof createConversationInputSchema>;
|
|
67
|
+
|
|
68
|
+
export const contextConversationInputSchema = z.strictObject({
|
|
69
|
+
context: chatContextSchema,
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
export type ContextConversationInput = z.input<typeof contextConversationInputSchema>;
|
|
73
|
+
|
|
74
|
+
export const reassignConversationContextInputSchema = z.strictObject({
|
|
75
|
+
context: chatContextSchema,
|
|
76
|
+
conversationId: chatIdSchema,
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
export type ReassignConversationContextInput = z.input<typeof reassignConversationContextInputSchema>;
|
|
80
|
+
|
|
81
|
+
export const conversationIdInputSchema = z.strictObject({ id: chatIdSchema });
|
|
82
|
+
export type ConversationIdInput = z.input<typeof conversationIdInputSchema>;
|
|
83
|
+
|
|
84
|
+
export const renameConversationInputSchema = z.strictObject({
|
|
85
|
+
id: chatIdSchema,
|
|
86
|
+
title: z.string().trim().min(1).max(shortTextMaxLength),
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
export type RenameConversationInput = z.input<typeof renameConversationInputSchema>;
|
|
90
|
+
|
|
91
|
+
const listInputSchema = z.strictObject({
|
|
92
|
+
search: z.string().trim().max(shortTextMaxLength).optional(),
|
|
93
|
+
orderBy: z.enum(["CREATED_AT", "UPDATED_AT"]).default("UPDATED_AT"),
|
|
94
|
+
limit: z.number().int().min(1).max(100).default(10),
|
|
95
|
+
offset: z.number().int().min(0).default(0),
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
export const listConversationsInputSchema = listInputSchema.extend({
|
|
99
|
+
folderId: idSchema.optional(),
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
export type ListConversationsInput = z.input<typeof listConversationsInputSchema>;
|
|
103
|
+
|
|
104
|
+
export const deleteConversationResultSchema = z.discriminatedUnion("status", [
|
|
105
|
+
z.strictObject({ status: z.literal("deleted"), id: chatIdSchema }),
|
|
106
|
+
z.strictObject({ status: z.literal("not_found") }),
|
|
107
|
+
]);
|
|
108
|
+
|
|
109
|
+
export const folderSummarySchema = z.strictObject({
|
|
110
|
+
id: idSchema,
|
|
111
|
+
title: z.string().min(1).max(shortTextMaxLength),
|
|
112
|
+
description: z.string().max(descriptionMaxLength).nullable(),
|
|
113
|
+
createdAt: z.iso.datetime(),
|
|
114
|
+
updatedAt: z.iso.datetime(),
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
export type FolderSummary = z.infer<typeof folderSummarySchema>;
|
|
118
|
+
|
|
119
|
+
export const createFolderInputSchema = z.strictObject({
|
|
120
|
+
title: z.string().trim().min(1).max(shortTextMaxLength).optional(),
|
|
121
|
+
description: z.string().trim().max(descriptionMaxLength).optional(),
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
export type CreateFolderInput = z.input<typeof createFolderInputSchema>;
|
|
125
|
+
|
|
126
|
+
export const updateFolderInputSchema = z.strictObject({
|
|
127
|
+
id: idSchema,
|
|
128
|
+
title: z.string().trim().min(1).max(shortTextMaxLength).optional(),
|
|
129
|
+
description: z.string().trim().max(descriptionMaxLength).optional(),
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
export type UpdateFolderInput = z.input<typeof updateFolderInputSchema>;
|
|
133
|
+
|
|
134
|
+
export const folderIdInputSchema = z.strictObject({ id: idSchema });
|
|
135
|
+
export type FolderIdInput = z.input<typeof folderIdInputSchema>;
|
|
136
|
+
|
|
137
|
+
export const listFoldersInputSchema = listInputSchema;
|
|
138
|
+
|
|
139
|
+
export type ListFoldersInput = z.input<typeof listFoldersInputSchema>;
|
|
140
|
+
|
|
141
|
+
export const assignConversationInputSchema = z.strictObject({
|
|
142
|
+
conversationId: chatIdSchema,
|
|
143
|
+
folderId: idSchema.nullable(),
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
export type AssignConversationInput = z.input<typeof assignConversationInputSchema>;
|
|
147
|
+
|
|
148
|
+
export const deleteFolderResultSchema = z.discriminatedUnion("status", [
|
|
149
|
+
z.strictObject({ status: z.literal("deleted"), id: idSchema }),
|
|
150
|
+
z.strictObject({ status: z.literal("not_found") }),
|
|
151
|
+
]);
|
|
152
|
+
|
|
153
|
+
export type DeleteFolderResult = z.infer<typeof deleteFolderResultSchema>;
|
|
154
|
+
|
|
155
|
+
export type DeleteConversationResult = z.infer<typeof deleteConversationResultSchema>;
|
|
156
|
+
|
|
157
|
+
export const conversationConfigSchema = z.strictObject({
|
|
158
|
+
version: z.literal(1),
|
|
159
|
+
scope: chatManagerScopeSchema,
|
|
160
|
+
chatId: chatIdSchema,
|
|
161
|
+
chatIRI: z.string().min(1),
|
|
162
|
+
context: chatContextSchema.nullable(),
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
export type ConversationConfig = z.infer<typeof conversationConfigSchema>;
|
|
166
|
+
|
|
167
|
+
export const thinkChatStateSchema = z.strictObject({
|
|
168
|
+
config: conversationConfigSchema.nullable(),
|
|
169
|
+
contextTokens: z.number().int().nonnegative(),
|
|
170
|
+
compacting: z.boolean().default(false),
|
|
171
|
+
automaticTitleGenerationComplete: z.boolean().default(false),
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
export type ThinkChatState = z.infer<typeof thinkChatStateSchema>;
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Browser-safe surface for the owner Agent. Keeping this contract separate
|
|
178
|
+
* from the Worker class prevents server-only types and internal RPC methods
|
|
179
|
+
* from entering the client bundle.
|
|
180
|
+
*/
|
|
181
|
+
export type ChatManagerClient = {
|
|
182
|
+
readonly state: ChatManagerState;
|
|
183
|
+
createConversation(input?: CreateConversationInput): Promise<ConversationSummary>;
|
|
184
|
+
findContextConversation(input: ContextConversationInput): Promise<ConversationSummary | null>;
|
|
185
|
+
ensureContextConversation(input: ContextConversationInput): Promise<ConversationSummary>;
|
|
186
|
+
reassignConversationContext(input: ReassignConversationContextInput): Promise<ConversationSummary>;
|
|
187
|
+
getConversation(input: ConversationIdInput): Promise<ConversationSummary | null>;
|
|
188
|
+
listConversations(input?: ListConversationsInput): Promise<ConversationSummary[]>;
|
|
189
|
+
renameConversation(input: RenameConversationInput): Promise<ConversationSummary | null>;
|
|
190
|
+
deleteConversation(input: ConversationIdInput): Promise<DeleteConversationResult>;
|
|
191
|
+
createFolder(input?: CreateFolderInput): Promise<FolderSummary>;
|
|
192
|
+
updateFolder(input: UpdateFolderInput): Promise<FolderSummary | null>;
|
|
193
|
+
getFolder(input: FolderIdInput): Promise<FolderSummary | null>;
|
|
194
|
+
listFolders(input?: ListFoldersInput): Promise<FolderSummary[]>;
|
|
195
|
+
deleteFolder(input: FolderIdInput): Promise<DeleteFolderResult>;
|
|
196
|
+
assignConversation(input: AssignConversationInput): Promise<ConversationSummary | null>;
|
|
197
|
+
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { z } from "zod/v4";
|
|
2
|
+
|
|
3
|
+
export const chatTurnErrorMessages = {
|
|
4
|
+
conversationScope: "This chat can't be used with the currently selected datastore.",
|
|
5
|
+
contextOverflow: "This chat has become too long to continue.",
|
|
6
|
+
messageNotSaved:
|
|
7
|
+
"Manta couldn't complete this response, and your message may not have been saved. Please send it again.",
|
|
8
|
+
unexpected: "Manta couldn't complete this response. Please try again.",
|
|
9
|
+
} as const;
|
|
10
|
+
|
|
11
|
+
export const chatTurnErrorMessageSchema = z.enum([
|
|
12
|
+
chatTurnErrorMessages.conversationScope,
|
|
13
|
+
chatTurnErrorMessages.contextOverflow,
|
|
14
|
+
chatTurnErrorMessages.messageNotSaved,
|
|
15
|
+
chatTurnErrorMessages.unexpected,
|
|
16
|
+
]);
|
|
17
|
+
|
|
18
|
+
export type ChatTurnErrorMessage = z.infer<typeof chatTurnErrorMessageSchema>;
|
|
19
|
+
|
|
20
|
+
export function resolveChatTurnErrorMessage(error: unknown): ChatTurnErrorMessage {
|
|
21
|
+
const message = error instanceof Error ? error.message : error;
|
|
22
|
+
const parsed = chatTurnErrorMessageSchema.safeParse(message);
|
|
23
|
+
|
|
24
|
+
return parsed.success ? parsed.data : chatTurnErrorMessages.unexpected;
|
|
25
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// A realm slug. Which slugs exist is the deploying application's roster, injected into
|
|
2
|
+
// @manta-eu/ldo at runtime - this package is published and must not name customers.
|
|
3
|
+
export type { KeycloakRealm } from "@manta-eu/ldo/realm";
|
|
4
|
+
|
|
5
|
+
export const selectedDatastoreCookieName = "selected-datastore";
|
|
6
|
+
|
|
7
|
+
export const errorRoutePath = "/error";
|
|
8
|
+
|
|
9
|
+
export const scheduleToolingFlagKey = "schedule-tooling-enabled";
|
|
10
|
+
export const teamActivationFlagKey = "team-activation-enabled";
|
|
11
|
+
export const memoryExtractionFlagKey = "memory-extraction-enabled";
|
|
12
|
+
export const memoryAutoSupersedeFlagKey = "memory-auto-supersede-enabled";
|
|
13
|
+
export const memoryReplayFlagKey = "memory-replay-enabled";
|
|
14
|
+
export const memoryMaintenanceFlagKey = "memory-maintenance-enabled";
|
|
15
|
+
|
|
16
|
+
// Gates "Remove"/"Restore" and "Show archived". Off: see wiki/infisical-secrets.md § Removal gap.
|
|
17
|
+
export const enableDatastoreRemoval = false;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export type ConversationStarterDefault = {
|
|
2
|
+
id: string;
|
|
3
|
+
title: string;
|
|
4
|
+
description: string;
|
|
5
|
+
priority: number;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export const defaultConversationStarters: ConversationStarterDefault[] = [
|
|
9
|
+
{
|
|
10
|
+
id: "default-top-advertising-strategies",
|
|
11
|
+
title: "What were the top advertising strategies for Meta in 2025?",
|
|
12
|
+
description: "Explore the most effective Meta advertising tactics and trends from 2025.",
|
|
13
|
+
priority: 1,
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
id: "default-common-causes-of-ad-spend-waste",
|
|
17
|
+
title: "What are the 3 most common causes of ad spend waste?",
|
|
18
|
+
description: "Discover the biggest budget drains and how to avoid them.",
|
|
19
|
+
priority: 2,
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
id: "default-top-benefits-of-advertising-on-snapchat",
|
|
23
|
+
title: "Top benefits of advertising on Snapchat?",
|
|
24
|
+
description: "Learn why Snapchat could be a valuable addition to your ad mix.",
|
|
25
|
+
priority: 3,
|
|
26
|
+
},
|
|
27
|
+
];
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export function slugify(name: string): string {
|
|
2
|
+
return name
|
|
3
|
+
.trim()
|
|
4
|
+
.toLowerCase()
|
|
5
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
6
|
+
.replace(/^-+|-+$/g, "");
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
// Org slug + full group path: "Demo Portfolio Co" + ["Vertical A","Team 1"] -> "demo-portfolio-co-vertical-a-team-1".
|
|
10
|
+
export function deriveDatastoreId(organizationName: string, segments: string[]): string {
|
|
11
|
+
return (
|
|
12
|
+
[organizationName, ...segments]
|
|
13
|
+
.map(slugify)
|
|
14
|
+
// eslint-disable-next-line @manta-eu/prefer-is-present -- agents/vite.config.ts loads this file without lib
|
|
15
|
+
.filter((slug) => slug.length > 0)
|
|
16
|
+
.join("-")
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Validated before reaching Infisical/KGC/RDFox since the Keycloak attribute is admin-settable.
|
|
21
|
+
const datastoreIdPattern = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
22
|
+
|
|
23
|
+
export function isValidDatastoreId(id: string): boolean {
|
|
24
|
+
return datastoreIdPattern.test(id);
|
|
25
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { isValidDatastoreId } from "./datastore-id";
|
|
2
|
+
|
|
3
|
+
export const datastoreRoutePrefix = "/d";
|
|
4
|
+
|
|
5
|
+
export const datastoreHeaderName = "x-manta-datastore";
|
|
6
|
+
|
|
7
|
+
export const datastoreQueryParam = "datastore";
|
|
8
|
+
|
|
9
|
+
export const redirectRoutePath = "/redirect";
|
|
10
|
+
|
|
11
|
+
export type DatastoreLocation = { datastoreId: string | undefined; path: string };
|
|
12
|
+
|
|
13
|
+
const datastorePathPattern = new RegExp(`^${datastoreRoutePrefix}/([^/]+)(/.*)?$`);
|
|
14
|
+
|
|
15
|
+
// "//evil.com" and "/\evil.com" both resolve off-origin - browsers treat "\" as "/" for special schemes.
|
|
16
|
+
export function isRootRelativePath(url: string): boolean {
|
|
17
|
+
return url.startsWith("/") && !url.startsWith("//") && !url.startsWith("/\\");
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function datastoreBasename(datastoreId: string): string {
|
|
21
|
+
return `${datastoreRoutePrefix}/${encodeURIComponent(datastoreId)}`;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function parseDatastorePath(pathname: string): DatastoreLocation {
|
|
25
|
+
const split = splitDatastorePath(pathname);
|
|
26
|
+
if (!split) return { datastoreId: undefined, path: pathname };
|
|
27
|
+
|
|
28
|
+
const datastoreId = safeDecode(split.segment);
|
|
29
|
+
if (!datastoreId || !isValidDatastoreId(datastoreId)) return { datastoreId: undefined, path: pathname };
|
|
30
|
+
|
|
31
|
+
return { datastoreId, path: split.rest };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function datastoreRelativePath(pathname: string): string {
|
|
35
|
+
return splitDatastorePath(pathname)?.rest ?? pathname;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function datastoreHeader(datastoreId: string | undefined): Record<string, string> {
|
|
39
|
+
return datastoreId ? { [datastoreHeaderName]: datastoreId } : {};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function datastoreSocketQuery(datastoreId: string | undefined): Record<string, string> {
|
|
43
|
+
return datastoreId ? { [datastoreQueryParam]: datastoreId } : {};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function withDatastoreQueryParam(href: string, datastoreId: string | undefined): string {
|
|
47
|
+
if (!datastoreId) return href;
|
|
48
|
+
|
|
49
|
+
const url = new URL(href, "http://placeholder");
|
|
50
|
+
url.searchParams.set(datastoreQueryParam, datastoreId);
|
|
51
|
+
return `${url.pathname}${url.search}${url.hash}`;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function withDatastorePrefix(datastoreId: string, path: string): string {
|
|
55
|
+
return `${datastoreBasename(datastoreId)}${path}`;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function datastoreScopedRoutePattern(path: string): string {
|
|
59
|
+
return `${datastoreRoutePrefix}/:datastoreId${path}`;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Matches a path with or without its "/d/<id>/" prefix, for a Workbox denylist entry.
|
|
63
|
+
export function datastoreScopedDenylist(pathWithoutSlash: string): RegExp {
|
|
64
|
+
return new RegExp(`^/(${datastoreRoutePrefix.slice(1)}/[^/]+/)?${pathWithoutSlash}\\b`);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function splitDatastorePath(pathname: string): { segment: string; rest: string } | undefined {
|
|
68
|
+
const match = datastorePathPattern.exec(pathname);
|
|
69
|
+
if (!match?.[1]) return undefined;
|
|
70
|
+
|
|
71
|
+
return { segment: match[1], rest: match[2] || "/" };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function safeDecode(segment: string): string | undefined {
|
|
75
|
+
try {
|
|
76
|
+
return decodeURIComponent(segment);
|
|
77
|
+
} catch {
|
|
78
|
+
return undefined;
|
|
79
|
+
}
|
|
80
|
+
}
|