akanjs 3.0.0-alpha.10 → 3.0.0-alpha.12
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/base/symbols.ts +4 -0
- package/common/index.ts +10 -0
- package/common/mcpExposure.ts +106 -0
- package/constant/immerify.ts +1 -1
- package/constant/index.ts +1 -0
- package/constant/mask.ts +60 -0
- package/dictionary/DictionaryLookup.ts +30 -0
- package/dictionary/dictInfo.ts +89 -1
- package/dictionary/index.ts +1 -0
- package/dictionary/locale.ts +16 -2
- package/document/index.ts +1 -0
- package/document/noDocumentError.ts +12 -0
- package/fetch/client/fetchClient.ts +12 -0
- package/fetch/fetchType/endpointFetch.type.ts +10 -4
- package/package.json +1 -1
- package/server/akanApp.ts +35 -1
- package/server/akanServer.ts +154 -5
- package/server/devtools/signalSerializer.ts +2 -2
- package/server/devtools/types.ts +2 -2
- package/server/mcp/McpAuth.ts +177 -0
- package/server/mcp/McpDispatcher.ts +245 -0
- package/server/mcp/McpEventStream.ts +76 -0
- package/server/mcp/McpExecutionContext.ts +105 -0
- package/server/mcp/McpRouter.ts +604 -0
- package/server/mcp/index.ts +5 -0
- package/server/resolver/database.resolver.ts +5 -4
- package/server/resolver/signal.resolver.ts +10 -0
- package/server/subRouteIndexDocument.tsx +61 -0
- package/server/systemPageDocument.tsx +2 -2
- package/server/systemPages.tsx +49 -16
- package/server/webRouter.ts +27 -1
- package/service/predefinedAdaptor/database.adaptor.ts +4 -3
- package/service/predefinedAdaptor/index.ts +1 -0
- package/service/predefinedAdaptor/insightQuery.ts +183 -0
- package/signal/agent/AgentCatalogue.ts +165 -0
- package/signal/agent/index.ts +1 -0
- package/signal/endpointInfo.ts +38 -2
- package/signal/guard.ts +11 -1
- package/signal/guards.ts +3 -1
- package/signal/index.ts +3 -0
- package/signal/mcp/McpDocument.ts +322 -0
- package/signal/mcp/McpProgress.ts +106 -0
- package/signal/mcp/McpUriTemplate.ts +85 -0
- package/signal/mcp/Msg.ts +368 -0
- package/signal/mcp/index.ts +5 -0
- package/signal/mcp/mcpProtocol.ts +120 -0
- package/signal/openapi/openapi.ts +32 -171
- package/signal/schema/JsonSchemaBuilder.ts +211 -0
- package/signal/schema/index.ts +1 -0
- package/signal/serializer/fetch.serializer.ts +3 -0
- package/signal/signalContext.ts +45 -5
- package/signal/slice.ts +15 -2
- package/signal/types.ts +33 -1
- package/store/action.ts +10 -21
- package/store/actionTag.ts +28 -0
- package/store/agent/AgentBridge.ts +281 -0
- package/store/agent/StoreCatalogue.ts +296 -0
- package/store/agent/index.ts +3 -0
- package/store/agent/types.ts +50 -0
- package/store/databaseStateNames.ts +31 -0
- package/store/formSetterNames.ts +21 -0
- package/store/index.ts +7 -0
- package/store/rootStore.ts +2 -1
- package/store/sliceRole.ts +36 -0
- package/store/state.ts +2 -12
- package/store/store.ts +5 -0
- package/store/storeInstance.ts +55 -17
- package/store/storeRegistry.ts +10 -0
- package/types/base/symbols.d.ts +4 -0
- package/types/common/index.d.ts +1 -0
- package/types/common/mcpExposure.d.ts +69 -0
- package/types/constant/index.d.ts +1 -0
- package/types/constant/mask.d.ts +34 -0
- package/types/dictionary/DictionaryLookup.d.ts +14 -0
- package/types/dictionary/base.dictionary.d.ts +1 -1
- package/types/dictionary/dictInfo.d.ts +50 -13
- package/types/dictionary/dictionary.d.ts +8 -8
- package/types/dictionary/index.d.ts +1 -0
- package/types/dictionary/locale.d.ts +9 -1
- package/types/document/index.d.ts +1 -0
- package/types/document/noDocumentError.d.ts +12 -0
- package/types/fetch/client/fetchClient.d.ts +9 -0
- package/types/fetch/fetchType/endpointFetch.type.d.ts +5 -3
- package/types/server/akanApp.d.ts +6 -0
- package/types/server/akanServer.d.ts +43 -0
- package/types/server/devtools/types.d.ts +2 -2
- package/types/server/mcp/McpAuth.d.ts +62 -0
- package/types/server/mcp/McpDispatcher.d.ts +54 -0
- package/types/server/mcp/McpEventStream.d.ts +18 -0
- package/types/server/mcp/McpExecutionContext.d.ts +44 -0
- package/types/server/mcp/McpRouter.d.ts +68 -0
- package/types/server/mcp/index.d.ts +5 -0
- package/types/server/mcp.d.ts +1 -0
- package/types/server/subRouteIndexDocument.d.ts +8 -0
- package/types/server/systemPageDocument.d.ts +1 -0
- package/types/server/systemPages.d.ts +5 -0
- package/types/service/predefinedAdaptor/index.d.ts +1 -0
- package/types/service/predefinedAdaptor/insightQuery.d.ts +50 -0
- package/types/signal/agent/AgentCatalogue.d.ts +100 -0
- package/types/signal/agent/index.d.ts +1 -0
- package/types/signal/agent.d.ts +1 -0
- package/types/signal/endpointInfo.d.ts +21 -3
- package/types/signal/guard.d.ts +10 -0
- package/types/signal/guards.d.ts +3 -1
- package/types/signal/index.d.ts +3 -0
- package/types/signal/mcp/McpDocument.d.ts +76 -0
- package/types/signal/mcp/McpProgress.d.ts +40 -0
- package/types/signal/mcp/McpUriTemplate.d.ts +26 -0
- package/types/signal/mcp/Msg.d.ts +143 -0
- package/types/signal/mcp/index.d.ts +5 -0
- package/types/signal/mcp/mcpProtocol.d.ts +107 -0
- package/types/signal/mcp.d.ts +1 -0
- package/types/signal/openapi/openapi.d.ts +3 -3
- package/types/signal/schema/JsonSchemaBuilder.d.ts +47 -0
- package/types/signal/schema/index.d.ts +1 -0
- package/types/signal/schema.d.ts +1 -0
- package/types/signal/signalContext.d.ts +16 -1
- package/types/signal/slice.d.ts +3 -1
- package/types/signal/types.d.ts +31 -1
- package/types/store/actionTag.d.ts +17 -0
- package/types/store/agent/AgentBridge.d.ts +70 -0
- package/types/store/agent/StoreCatalogue.d.ts +21 -0
- package/types/store/agent/index.d.ts +3 -0
- package/types/store/agent/types.d.ts +49 -0
- package/types/store/agent.d.ts +1 -0
- package/types/store/databaseStateNames.d.ts +25 -0
- package/types/store/formSetterNames.d.ts +16 -0
- package/types/store/index.d.ts +6 -0
- package/types/store/rootStore.d.ts +4 -1
- package/types/store/sliceRole.d.ts +25 -0
- package/types/store/store.d.ts +4 -1
- package/types/store/storeInstance.d.ts +16 -0
- package/types/store/storeRegistry.d.ts +3 -0
- package/types/ui/Agent/Dock.d.ts +16 -0
- package/types/ui/Agent/Section.d.ts +10 -0
- package/types/ui/Agent/StateKey.d.ts +15 -0
- package/types/ui/Agent/Tool.d.ts +15 -0
- package/types/ui/Agent/Transcript.d.ts +13 -0
- package/types/ui/Agent/index.d.ts +11 -0
- package/types/ui/Agent.d.ts +1 -0
- package/types/ui/Badge.d.ts +1 -1
- package/types/ui/Button.d.ts +1 -1
- package/types/ui/Signal/RestApi.d.ts +3 -2
- package/types/ui/Signal/style.d.ts +3 -0
- package/types/ui/agentAttrs.d.ts +14 -0
- package/types/ui/index.d.ts +2 -0
- package/ui/Agent/Dock.tsx +61 -0
- package/ui/Agent/Section.tsx +24 -0
- package/ui/Agent/StateKey.tsx +42 -0
- package/ui/Agent/Tool.tsx +66 -0
- package/ui/Agent/Transcript.tsx +33 -0
- package/ui/Agent/index.ts +7 -0
- package/ui/Badge.tsx +1 -1
- package/ui/Button.tsx +3 -1
- package/ui/DatePicker.tsx +1 -1
- package/ui/Field.tsx +15 -7
- package/ui/Input.tsx +7 -0
- package/ui/Select.tsx +2 -1
- package/ui/Signal/RestApi.tsx +57 -17
- package/ui/Signal/WebSocket.tsx +1 -1
- package/ui/Signal/style.ts +6 -1
- package/ui/Switch.tsx +2 -0
- package/ui/agentAttrs.ts +19 -0
- package/ui/index.ts +2 -0
package/base/symbols.ts
CHANGED
|
@@ -13,6 +13,10 @@ export const STATE_META = Symbol.for("akan.state");
|
|
|
13
13
|
export const STATE_INIT_META = Symbol.for("akan.state.init");
|
|
14
14
|
export const STATE_DERIVED_META = Symbol.for("akan.state.derived");
|
|
15
15
|
export const ACTION_META = Symbol.for("akan.action");
|
|
16
|
+
/** Which module declared each action, which is the dictionary node its words are written in. */
|
|
17
|
+
export const ACTION_OWNER_META = Symbol.for("akan.action.owner");
|
|
18
|
+
/** What a dispatcher does, carried on the function so a component handed one can annotate the DOM with it. */
|
|
19
|
+
export const ACTION_TAG = Symbol.for("akan.action.tag");
|
|
16
20
|
export const SERVER_VALUE = Symbol.for("akan.value.server");
|
|
17
21
|
export const CLIENT_VALUE = Symbol.for("akan.value.client");
|
|
18
22
|
export const DEFAULT_VALUE = Symbol.for("akan.value.default");
|
package/common/index.ts
CHANGED
|
@@ -27,6 +27,16 @@ export {
|
|
|
27
27
|
resolveAkanI18nConfig,
|
|
28
28
|
} from "./localeConfig";
|
|
29
29
|
export { lowerlize } from "./lowerlize";
|
|
30
|
+
export {
|
|
31
|
+
isMcpDescribableArg,
|
|
32
|
+
type McpExposureEndpoint,
|
|
33
|
+
type McpExposureHints,
|
|
34
|
+
type McpExposureOption,
|
|
35
|
+
mcpBaseVerbOf,
|
|
36
|
+
mcpHintsOf,
|
|
37
|
+
mcpPromptRefusalOf,
|
|
38
|
+
mcpRefusalOf,
|
|
39
|
+
} from "./mcpExposure";
|
|
30
40
|
export { mergeVersion } from "./mergeVersion";
|
|
31
41
|
export { objectify } from "./objectify";
|
|
32
42
|
export { pathGet } from "./pathGet";
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { capitalize } from "./capitalize";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* What MCP says about one endpoint: whether it is published, why it is not, and the hints it carries.
|
|
5
|
+
*
|
|
6
|
+
* Structurally typed like the rest of `common/` so the same rules answer on both sides — the server builds its
|
|
7
|
+
* catalogue from them and the browser API explorer badges an endpoint from them. A second implementation would
|
|
8
|
+
* eventually disagree, and an audit surface that disagrees with the catalogue is worse than none.
|
|
9
|
+
*
|
|
10
|
+
* Every rejection returns the sentence an author reads, at boot in the server log and in the explorer. Fail-closed
|
|
11
|
+
* with no reason is what left a deliberate `mcp: { expose: true }` vanishing with nowhere to look but the source.
|
|
12
|
+
*/
|
|
13
|
+
export interface McpExposureEndpoint {
|
|
14
|
+
type: string;
|
|
15
|
+
returns: { refName: string };
|
|
16
|
+
args: { name: string; refName: string; type: string; arrDepth?: number; nullable?: boolean }[];
|
|
17
|
+
guards?: string[];
|
|
18
|
+
fileUpload?: boolean;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface McpExposureOption {
|
|
22
|
+
/**
|
|
23
|
+
* The read-only deployment valve, which is server configuration. The browser explorer cannot know it and so
|
|
24
|
+
* badges what the code decided; the boot log is where a read-only deployment says what it dropped.
|
|
25
|
+
*/
|
|
26
|
+
readOnly?: boolean;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** The author's overrides for the hints derived below. */
|
|
30
|
+
export interface McpExposureHints {
|
|
31
|
+
readOnly?: boolean;
|
|
32
|
+
destructive?: boolean;
|
|
33
|
+
idempotent?: boolean;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** `Any` publishes as the empty schema, which tells a model nothing — so it is left out rather than described. */
|
|
37
|
+
export const isMcpDescribableArg = (arg: { refName: string }) => arg.refName !== "Any";
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Which slice-level verb opts in each CRUD endpoint a model generates. They carry no signal option of their own,
|
|
41
|
+
* so `mcp: { get: true }` on the slice is the only place their exposure can be written.
|
|
42
|
+
*/
|
|
43
|
+
export const mcpBaseVerbOf = (refName: string, key: string) => {
|
|
44
|
+
const cap = capitalize(refName);
|
|
45
|
+
if (key === refName || key === `light${cap}`) return "get" as const;
|
|
46
|
+
if (key === `create${cap}`) return "create" as const;
|
|
47
|
+
if (key === `update${cap}`) return "update" as const;
|
|
48
|
+
if (key === `remove${cap}`) return "remove" as const;
|
|
49
|
+
return null;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* The hints a client renders beside a tool. Hints only — clients are told to distrust them, so they inform a UI
|
|
54
|
+
* and never stand in for a guard. `openWorldHint` is always false: every endpoint reaches this app's own
|
|
55
|
+
* database, not the wider internet.
|
|
56
|
+
*/
|
|
57
|
+
export const mcpHintsOf = (key: string, endpoint: { type: string }, mcp: McpExposureHints = {}) => {
|
|
58
|
+
const readOnly = mcp.readOnly ?? endpoint.type === "query";
|
|
59
|
+
const destructive = mcp.destructive ?? (!readOnly && /^(remove|delete)/.test(key));
|
|
60
|
+
return {
|
|
61
|
+
readOnlyHint: readOnly,
|
|
62
|
+
destructiveHint: destructive,
|
|
63
|
+
idempotentHint: mcp.idempotent ?? (readOnly || /^(set|update)/.test(key)),
|
|
64
|
+
openWorldHint: false,
|
|
65
|
+
};
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
/** The sentence explaining why this endpoint is not in the catalogue, or `null` when it is. */
|
|
69
|
+
export const mcpRefusalOf = (endpoint: McpExposureEndpoint, { readOnly }: McpExposureOption = {}): string | null => {
|
|
70
|
+
if (endpoint.type === "prompt") return mcpPromptRefusalOf(endpoint);
|
|
71
|
+
if (endpoint.type === "pubsub" || endpoint.type === "message")
|
|
72
|
+
return `\`${endpoint.type}\` rides the websocket, and its internal arguments read a socket an MCP request does not have.`;
|
|
73
|
+
if (readOnly && endpoint.type !== "query")
|
|
74
|
+
return "this deployment is read-only, which drops every endpoint that is not a query.";
|
|
75
|
+
if (endpoint.returns.refName === "Any" || endpoint.returns.refName === "Upload")
|
|
76
|
+
return `a return typed \`${endpoint.returns.refName}\` cannot be described to a model.`;
|
|
77
|
+
if (endpoint.fileUpload || endpoint.args.some((arg) => arg.refName === "Upload"))
|
|
78
|
+
return "a file upload has no MCP representation.";
|
|
79
|
+
if (endpoint.type === "mutation" && !endpoint.guards?.some((name) => name !== "Public"))
|
|
80
|
+
return "a mutation needs a real guard — `[Public]` is having none, spelled out.";
|
|
81
|
+
const opaque = endpoint.args.find((arg) => !isMcpDescribableArg(arg) && arg.type !== "search" && !arg.nullable);
|
|
82
|
+
if (opaque)
|
|
83
|
+
return `its required argument \`${opaque.name}\` is typed \`Any\`, which is left out of the published schema — expose a named filter slice instead.`;
|
|
84
|
+
return null;
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* A prompt is a read exposed on the same terms as a query, so every rejection here is one thing: an argument
|
|
89
|
+
* `prompts/get` cannot carry. Its `arguments` is a flat string map — one string per name, and no schema beside it
|
|
90
|
+
* — which rules out the argument *kinds* the builder already refuses and, just as surely, two argument *types* it
|
|
91
|
+
* accepts. A tool escapes both because it publishes a real JSON Schema.
|
|
92
|
+
*/
|
|
93
|
+
export const mcpPromptRefusalOf = (endpoint: McpExposureEndpoint): string | null => {
|
|
94
|
+
const carried = endpoint.args.find((arg) => arg.type === "body" || arg.type === "msg" || arg.type === "room");
|
|
95
|
+
if (carried)
|
|
96
|
+
return `a prompt's arguments travel as a flat string map, so its \`${carried.type}\` argument \`${carried.name}\` cannot be carried.`;
|
|
97
|
+
|
|
98
|
+
const list = endpoint.args.find((arg) => arg.arrDepth);
|
|
99
|
+
if (list)
|
|
100
|
+
return `a prompt argument is one string, so its list argument \`${list.name}\` could never carry more than one value.`;
|
|
101
|
+
|
|
102
|
+
const opaque = endpoint.args.find((arg) => !isMcpDescribableArg(arg));
|
|
103
|
+
if (opaque)
|
|
104
|
+
return `its argument \`${opaque.name}\` is typed \`Any\`, and a prompt has no schema in which to describe one.`;
|
|
105
|
+
return null;
|
|
106
|
+
};
|
package/constant/immerify.ts
CHANGED
|
@@ -9,7 +9,7 @@ export const immerify = <T extends object>(modelRef: ConstantModelRef, objOrArr:
|
|
|
9
9
|
}) as Record<string, unknown>;
|
|
10
10
|
const objRecord = objOrArr as Record<string, unknown>;
|
|
11
11
|
Object.entries(modelRef[FIELD_META]).forEach(([key, field]) => {
|
|
12
|
-
if (field.isScalar && field.isClass &&
|
|
12
|
+
if (field.isScalar && field.isClass && objRecord[key])
|
|
13
13
|
immeredObj[key] = immerify(field.modelRef, objRecord[key] as object);
|
|
14
14
|
});
|
|
15
15
|
return immeredObj as T;
|
package/constant/index.ts
CHANGED
package/constant/mask.ts
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { FIELD_META } from "akanjs/base";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A model as masking reads it — the constructor, for the field metadata it carries at runtime.
|
|
5
|
+
*
|
|
6
|
+
* Structural rather than `ConstantModelRef` so that anything holding the class can name it, and read through
|
|
7
|
+
* `FIELD_META` the way `resolveReturn` reads it.
|
|
8
|
+
*/
|
|
9
|
+
export interface MaskModel {
|
|
10
|
+
name: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** The part of a field's metadata masking turns on. Mirrors what `resolveReturn` branches over. */
|
|
14
|
+
interface MaskField {
|
|
15
|
+
fieldType?: string;
|
|
16
|
+
isClass?: boolean;
|
|
17
|
+
modelRef?: MaskModel;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export const maskFieldsOf = (model: MaskModel): Record<string, MaskField> | null => {
|
|
21
|
+
const fields = (model as unknown as { [key: symbol]: unknown })[FIELD_META];
|
|
22
|
+
return fields && typeof fields === "object" ? (fields as Record<string, MaskField>) : null;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/** The `hidden` and `secret` field names of `model` that `value` still carries populated. */
|
|
26
|
+
export const leakingFieldsOf = (model: MaskModel, value: Record<string, unknown>): string[] => {
|
|
27
|
+
const fields = maskFieldsOf(model);
|
|
28
|
+
if (!fields) return [];
|
|
29
|
+
return Object.entries(fields)
|
|
30
|
+
.filter(([key, field]) => (field.fieldType === "hidden" || field.fieldType === "secret") && key in value)
|
|
31
|
+
.map(([key]) => key);
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Strips what a model marks `hidden` or `secret`, by the model the caller names rather than by the one the value
|
|
36
|
+
* happens to still carry.
|
|
37
|
+
*
|
|
38
|
+
* That distinction is the whole point. A check that reads the class off the value can only mask what arrives as an
|
|
39
|
+
* instance, so a `{ ...doc }` spread, a `toJSON()`, an `immerify()`, or a round-trip through `JSON.stringify` reaches
|
|
40
|
+
* its destination with the metadata already gone and nothing can be done about it. A named model is metadata the
|
|
41
|
+
* value cannot lose, so a hydrated document and a plain object copied out of one mask identically.
|
|
42
|
+
*
|
|
43
|
+
* This is the field half of `resolveReturn` and deliberately not the whole of it. That one also loads every relation
|
|
44
|
+
* it walks past, which is right for a query's return value and wrong here, where the value is already in hand.
|
|
45
|
+
*
|
|
46
|
+
* Returns `unknown` rather than the argument's type, because what comes back is missing fields that type promises.
|
|
47
|
+
*/
|
|
48
|
+
export const mask = (model: MaskModel, value: unknown): unknown => {
|
|
49
|
+
if (value === null || value === undefined || typeof value !== "object") return value;
|
|
50
|
+
if (Array.isArray(value)) return value.map((item: unknown) => mask(model, item));
|
|
51
|
+
const fields = maskFieldsOf(model);
|
|
52
|
+
if (!fields) return value;
|
|
53
|
+
const source = value as Record<string, unknown>;
|
|
54
|
+
const masked: Record<string, unknown> = {};
|
|
55
|
+
for (const [key, field] of Object.entries(fields)) {
|
|
56
|
+
if (field.fieldType === "hidden" || field.fieldType === "secret" || !(key in source)) continue;
|
|
57
|
+
masked[key] = field.isClass && field.modelRef ? mask(field.modelRef, source[key]) : source[key];
|
|
58
|
+
}
|
|
59
|
+
return masked;
|
|
60
|
+
};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { pathGet } from "akanjs/common";
|
|
2
|
+
import { DictionaryRegistry } from "./dictionaryRegistry";
|
|
3
|
+
import type { DictionaryNode } from "./trans";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Reads translated text out of the merged dictionary tree by dotted key (`user.signal.createUser.arg.data.desc`).
|
|
7
|
+
*
|
|
8
|
+
* Unlike `translate`, a missing key resolves to `undefined` rather than echoing the key back: callers here are
|
|
9
|
+
* document generators (OpenAPI, MCP) that must omit an absent description instead of emitting `"user.signal.…"`
|
|
10
|
+
* as if it were prose. The merged root is snapshotted once per instance, so build a fresh one per document.
|
|
11
|
+
*/
|
|
12
|
+
export class DictionaryLookup {
|
|
13
|
+
readonly language: string;
|
|
14
|
+
readonly #models: Record<string, DictionaryNode>;
|
|
15
|
+
constructor(language?: string) {
|
|
16
|
+
const root = DictionaryRegistry.getRoot();
|
|
17
|
+
this.language = language && root[language] ? language : (Object.keys(root).at(0) ?? "en");
|
|
18
|
+
this.#models = root[this.language] ?? {};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** A bare refName names the model node itself, which holds no text of its own — its label is `<refName>.modelName`. */
|
|
22
|
+
text(key: string): string | undefined {
|
|
23
|
+
const [refName, ...rest] = key.split(".");
|
|
24
|
+
if (!refName) return undefined;
|
|
25
|
+
const model = this.#models[refName];
|
|
26
|
+
const node = (rest.length ? pathGet(rest.join("."), model) : model) as { t?: unknown } | null;
|
|
27
|
+
const text = node?.t;
|
|
28
|
+
return typeof text === "string" && text.length ? text : undefined;
|
|
29
|
+
}
|
|
30
|
+
}
|
package/dictionary/dictInfo.ts
CHANGED
|
@@ -56,6 +56,17 @@ type DictEndpointShape<Endpoint> =
|
|
|
56
56
|
: Endpoint extends Record<string, EndpointInfo>
|
|
57
57
|
? EndpointCompactShape<Endpoint>
|
|
58
58
|
: Record<never, never>;
|
|
59
|
+
/**
|
|
60
|
+
* The actions of a store, which is every method on it that dispatches.
|
|
61
|
+
*
|
|
62
|
+
* Derived from the shape rather than from a marker like `ENDPOINT_DICT_SHAPE`, because a store's custom actions
|
|
63
|
+
* are declared on the subclass — after `store()` has already returned, so there is nothing for it to stamp. What
|
|
64
|
+
* makes the shape readable anyway is that `st.do.<action>()` is typed `void`: an action returns nothing, so the
|
|
65
|
+
* void-returning methods are exactly the actions and `get` / `pick` / `slice` fall out on their own.
|
|
66
|
+
*/
|
|
67
|
+
type DictStoreShape<Store> = {
|
|
68
|
+
[K in keyof Store as Store[K] extends (...args: never[]) => void | Promise<void> ? K & string : never]: true;
|
|
69
|
+
};
|
|
59
70
|
type DictArgNames<ArgNames> = ArgNames extends readonly string[] ? ArgNames[number] : never;
|
|
60
71
|
type DictFilterQuery<Filter> = DictFilterShape<Filter>["query"];
|
|
61
72
|
type DictFilterSort<Filter> = DictFilterShape<Filter>["sort"];
|
|
@@ -140,6 +151,7 @@ export class ModelDictInfo<
|
|
|
140
151
|
BaseSignalKey extends string = never,
|
|
141
152
|
SliceKey extends string = "",
|
|
142
153
|
EndpointKey extends string = never,
|
|
154
|
+
StoreKey extends string = never,
|
|
143
155
|
ErrorKey extends string = never,
|
|
144
156
|
EtcKey extends string = never,
|
|
145
157
|
> {
|
|
@@ -245,6 +257,9 @@ export class ModelDictInfo<
|
|
|
245
257
|
endpointDictionary: { [K in EndpointKey]: FunctionTranslation<Languages> } = {} as {
|
|
246
258
|
[K in EndpointKey]: FunctionTranslation<Languages>;
|
|
247
259
|
};
|
|
260
|
+
storeDictionary: { [K in StoreKey]: FieldTranslation<Languages> } = {} as {
|
|
261
|
+
[K in StoreKey]: FieldTranslation<Languages>;
|
|
262
|
+
};
|
|
248
263
|
errorDictionary: { [K in ErrorKey]: Languages } = {} as {
|
|
249
264
|
[K in ErrorKey]: Languages;
|
|
250
265
|
};
|
|
@@ -278,6 +293,7 @@ export class ModelDictInfo<
|
|
|
278
293
|
BaseSignalKey,
|
|
279
294
|
SliceKey,
|
|
280
295
|
EndpointKey,
|
|
296
|
+
StoreKey,
|
|
281
297
|
ErrorKey,
|
|
282
298
|
EtcKey
|
|
283
299
|
>;
|
|
@@ -302,6 +318,7 @@ export class ModelDictInfo<
|
|
|
302
318
|
BaseSignalKey,
|
|
303
319
|
SliceKey,
|
|
304
320
|
EndpointKey,
|
|
321
|
+
StoreKey,
|
|
305
322
|
ErrorKey,
|
|
306
323
|
EtcKey
|
|
307
324
|
>;
|
|
@@ -327,6 +344,7 @@ export class ModelDictInfo<
|
|
|
327
344
|
BaseSignalKey,
|
|
328
345
|
SliceKey,
|
|
329
346
|
EndpointKey,
|
|
347
|
+
StoreKey,
|
|
330
348
|
ErrorKey,
|
|
331
349
|
EtcKey
|
|
332
350
|
>;
|
|
@@ -351,6 +369,7 @@ export class ModelDictInfo<
|
|
|
351
369
|
BaseSignalKey,
|
|
352
370
|
SliceKey,
|
|
353
371
|
EndpointKey,
|
|
372
|
+
StoreKey,
|
|
354
373
|
ErrorKey,
|
|
355
374
|
EtcKey
|
|
356
375
|
>;
|
|
@@ -375,6 +394,7 @@ export class ModelDictInfo<
|
|
|
375
394
|
BaseSignalKey,
|
|
376
395
|
SliceKey,
|
|
377
396
|
EndpointKey,
|
|
397
|
+
StoreKey,
|
|
378
398
|
ErrorKey,
|
|
379
399
|
EtcKey
|
|
380
400
|
>;
|
|
@@ -400,6 +420,7 @@ export class ModelDictInfo<
|
|
|
400
420
|
BaseSignalKey,
|
|
401
421
|
keyof DictSliceShape<Slice> & string,
|
|
402
422
|
EndpointKey,
|
|
423
|
+
StoreKey,
|
|
403
424
|
ErrorKey,
|
|
404
425
|
EtcKey
|
|
405
426
|
>;
|
|
@@ -425,6 +446,41 @@ export class ModelDictInfo<
|
|
|
425
446
|
BaseSignalKey,
|
|
426
447
|
SliceKey,
|
|
427
448
|
keyof DictEndpointShape<Endpoint> & string,
|
|
449
|
+
StoreKey,
|
|
450
|
+
ErrorKey,
|
|
451
|
+
EtcKey
|
|
452
|
+
>;
|
|
453
|
+
}
|
|
454
|
+
/**
|
|
455
|
+
* What a store's custom actions are called, in the words a person would use.
|
|
456
|
+
*
|
|
457
|
+
* Optional, and the only stage that is: an action whose name matches the endpoint it wraps already reads as
|
|
458
|
+
* that endpoint's `.desc()`, which is most of them — the house naming rule (`st.do.X` reads the same as
|
|
459
|
+
* `fetch.X`) is what makes that true. This stage is for the rest, where inheriting the endpoint's words would
|
|
460
|
+
* be actively wrong: nine `getSummaryListIn*` actions that all call one endpoint, or `logout` over
|
|
461
|
+
* `signoutUser`, where the store name is the verb a user would say and the endpoint name is the verb the API
|
|
462
|
+
* has. `akan quality scan` names those and no others.
|
|
463
|
+
*
|
|
464
|
+
* Labels only, no `.arg()`. Parameter names are not in a class's type the way an endpoint builder's are, and an
|
|
465
|
+
* action mostly takes none anyway — its data comes from the form state the user already filled in.
|
|
466
|
+
*/
|
|
467
|
+
store<Store>(
|
|
468
|
+
translate: (t: (trans: Languages) => FieldTranslation<Languages>) => Partial<{
|
|
469
|
+
[K in keyof DictStoreShape<Store>]: FieldTranslation<Languages>;
|
|
470
|
+
}>,
|
|
471
|
+
) {
|
|
472
|
+
Object.assign(this.storeDictionary, translate(FieldTranslation.translate));
|
|
473
|
+
return this as unknown as ModelDictInfo<
|
|
474
|
+
Languages,
|
|
475
|
+
ModelKey,
|
|
476
|
+
InsightKey,
|
|
477
|
+
QueryKey,
|
|
478
|
+
SortKey,
|
|
479
|
+
EnumKey,
|
|
480
|
+
BaseSignalKey,
|
|
481
|
+
SliceKey,
|
|
482
|
+
EndpointKey,
|
|
483
|
+
StoreKey | (keyof DictStoreShape<Store> & string),
|
|
428
484
|
ErrorKey,
|
|
429
485
|
EtcKey
|
|
430
486
|
>;
|
|
@@ -441,6 +497,7 @@ export class ModelDictInfo<
|
|
|
441
497
|
BaseSignalKey,
|
|
442
498
|
SliceKey,
|
|
443
499
|
EndpointKey,
|
|
500
|
+
StoreKey,
|
|
444
501
|
ErrorKey | (keyof ErrorDict & string),
|
|
445
502
|
EtcKey
|
|
446
503
|
>;
|
|
@@ -457,6 +514,7 @@ export class ModelDictInfo<
|
|
|
457
514
|
BaseSignalKey,
|
|
458
515
|
SliceKey,
|
|
459
516
|
EndpointKey,
|
|
517
|
+
StoreKey,
|
|
460
518
|
ErrorKey,
|
|
461
519
|
EtcKey | (keyof EtcDict & string)
|
|
462
520
|
>;
|
|
@@ -473,6 +531,7 @@ export class ModelDictInfo<
|
|
|
473
531
|
GetBaseSignalKey<RefName>,
|
|
474
532
|
SliceKey,
|
|
475
533
|
EndpointKey,
|
|
534
|
+
StoreKey,
|
|
476
535
|
ErrorKey,
|
|
477
536
|
EtcKey
|
|
478
537
|
>;
|
|
@@ -496,6 +555,7 @@ export class ModelDictInfo<
|
|
|
496
555
|
this.#registerBaseSignalToRoot(refName, rootDict);
|
|
497
556
|
this.#registerSliceToRoot(refName, rootDict);
|
|
498
557
|
this.#registerEndpointToRoot(refName, rootDict);
|
|
558
|
+
this.#registerStoreToRoot(refName, rootDict);
|
|
499
559
|
this.#registerErrorToRoot(refName, rootDict);
|
|
500
560
|
this.#registerModelToRoot(refName, rootDict);
|
|
501
561
|
this.#registerEtcToRoot(refName, rootDict);
|
|
@@ -777,6 +837,23 @@ export class ModelDictInfo<
|
|
|
777
837
|
});
|
|
778
838
|
});
|
|
779
839
|
}
|
|
840
|
+
/** Under its own `store` node rather than beside `signal`, because the two hold the same key by design. */
|
|
841
|
+
#registerStoreToRoot(refName: string, rootDict: RootDictionary) {
|
|
842
|
+
this.languages.forEach((language) => {
|
|
843
|
+
ensureNode(getRootModelNode(rootDict, language, refName), "store");
|
|
844
|
+
});
|
|
845
|
+
Object.entries(this.storeDictionary as { [key: string]: FieldTranslation<Languages> }).forEach(([key, value]) => {
|
|
846
|
+
value.trans.forEach((t, idx) => {
|
|
847
|
+
ensureNode(ensureNode(getTranslatedRootModelNode(rootDict, this.languages, idx, refName), "store"), key).t = t;
|
|
848
|
+
});
|
|
849
|
+
value.descTrans?.forEach((t, idx) => {
|
|
850
|
+
ensureNode(
|
|
851
|
+
ensureNode(ensureNode(getTranslatedRootModelNode(rootDict, this.languages, idx, refName), "store"), key),
|
|
852
|
+
"desc",
|
|
853
|
+
).t = t;
|
|
854
|
+
});
|
|
855
|
+
});
|
|
856
|
+
}
|
|
780
857
|
#registerErrorToRoot(refName: string, rootDict: RootDictionary) {
|
|
781
858
|
this.languages.forEach((language) => {
|
|
782
859
|
ensureNode(getRootModelNode(rootDict, language, refName), "error");
|
|
@@ -803,7 +880,15 @@ export class ModelDictInfo<
|
|
|
803
880
|
}
|
|
804
881
|
}
|
|
805
882
|
|
|
806
|
-
|
|
883
|
+
/**
|
|
884
|
+
* Every parameter of `ModelDictInfo` is listed here positionally, so a parameter added to the class has to be
|
|
885
|
+
* added to all three lists below in the same slot. Omitting one does not fail to compile — inference silently
|
|
886
|
+
* shifts, so the last parameter falls off the end and becomes its default `never`: adding `StoreKey` before
|
|
887
|
+
* `ErrorKey` once cost an extending app the whole of the lib's `EtcKey` (`.translate()`) union, which reads at
|
|
888
|
+
* the call site as `l("<model>.<key>")` no longer existing.
|
|
889
|
+
*/
|
|
890
|
+
|
|
891
|
+
type AnyModelDictInfo = ModelDictInfo<any, any, any, any, any, any, any, any, any, any, any, any>;
|
|
807
892
|
|
|
808
893
|
type MergeTwoModelDicts<ModelDict1, ModelDict2> =
|
|
809
894
|
ModelDict1 extends ModelDictInfo<
|
|
@@ -816,6 +901,7 @@ type MergeTwoModelDicts<ModelDict1, ModelDict2> =
|
|
|
816
901
|
infer BaseSignalKey1,
|
|
817
902
|
infer SliceKey1,
|
|
818
903
|
infer EndpointKey1,
|
|
904
|
+
infer StoreKey1,
|
|
819
905
|
infer ErrorKey1,
|
|
820
906
|
infer EtcKey1
|
|
821
907
|
>
|
|
@@ -829,6 +915,7 @@ type MergeTwoModelDicts<ModelDict1, ModelDict2> =
|
|
|
829
915
|
infer BaseSignalKey2,
|
|
830
916
|
infer SliceKey2,
|
|
831
917
|
infer EndpointKey2,
|
|
918
|
+
infer StoreKey2,
|
|
832
919
|
infer ErrorKey2,
|
|
833
920
|
infer EtcKey2
|
|
834
921
|
>
|
|
@@ -842,6 +929,7 @@ type MergeTwoModelDicts<ModelDict1, ModelDict2> =
|
|
|
842
929
|
BaseSignalKey1 | BaseSignalKey2,
|
|
843
930
|
SliceKey1 | SliceKey2,
|
|
844
931
|
EndpointKey1 | EndpointKey2,
|
|
932
|
+
StoreKey1 | StoreKey2,
|
|
845
933
|
ErrorKey1 | ErrorKey2,
|
|
846
934
|
EtcKey1 | EtcKey2
|
|
847
935
|
>
|
package/dictionary/index.ts
CHANGED
package/dictionary/locale.ts
CHANGED
|
@@ -168,6 +168,14 @@ export type ModelTrans<
|
|
|
168
168
|
}>;
|
|
169
169
|
error: { [K in ErrorKey]: Trans };
|
|
170
170
|
} & { [K in EtcKey]: Trans };
|
|
171
|
+
/**
|
|
172
|
+
* Store action labels, keyed off the resolved `.store()` keys rather than off the store class.
|
|
173
|
+
*
|
|
174
|
+
* The endpoint half needs the class because it reads argument names out of it. A store entry is a label and a
|
|
175
|
+
* description with no arguments to name, so the keys the stage already resolved are the whole of it — which is
|
|
176
|
+
* also why a generated `dict.ts` passes no store type at all.
|
|
177
|
+
*/
|
|
178
|
+
export type StoreTranslatorKey<T extends string, StoreKey extends string> = `${T}.store.${StoreKey}${"" | ".desc"}`;
|
|
171
179
|
export type ModelTranslatorKey<T extends string, Model, Insight, Filter, Slice, Endpoint, EtcKey extends string> =
|
|
172
180
|
| `${T}.modelName`
|
|
173
181
|
| `${T}.modelDesc`
|
|
@@ -234,11 +242,14 @@ export const registerModelTrans = <
|
|
|
234
242
|
infer _BaseSignalKey,
|
|
235
243
|
infer _SliceKey,
|
|
236
244
|
infer _EndpointKey,
|
|
245
|
+
infer StoreKey,
|
|
237
246
|
infer ErrorKey,
|
|
238
247
|
infer EtcKey
|
|
239
248
|
>
|
|
240
249
|
? DictModule<
|
|
241
|
-
ModelTranslatorKey<RefName, Model, Insight, Filter, Slice, Endpoint, EtcKey>
|
|
250
|
+
| ModelTranslatorKey<RefName, Model, Insight, Filter, Slice, Endpoint, EtcKey>
|
|
251
|
+
| EnumTranslatorKey<EnumKey>
|
|
252
|
+
| StoreTranslatorKey<RefName, StoreKey>,
|
|
242
253
|
`${RefName}.error.${ErrorKey}`
|
|
243
254
|
>
|
|
244
255
|
: never => {
|
|
@@ -252,11 +263,14 @@ export const registerModelTrans = <
|
|
|
252
263
|
infer _BaseSignalKey,
|
|
253
264
|
infer _SliceKey,
|
|
254
265
|
infer _EndpointKey,
|
|
266
|
+
infer StoreKey,
|
|
255
267
|
infer ErrorKey,
|
|
256
268
|
infer EtcKey
|
|
257
269
|
>
|
|
258
270
|
? DictModule<
|
|
259
|
-
ModelTranslatorKey<RefName, Model, Insight, Filter, Slice, Endpoint, EtcKey>
|
|
271
|
+
| ModelTranslatorKey<RefName, Model, Insight, Filter, Slice, Endpoint, EtcKey>
|
|
272
|
+
| EnumTranslatorKey<EnumKey>
|
|
273
|
+
| StoreTranslatorKey<RefName, StoreKey>,
|
|
260
274
|
`${RefName}.error.${ErrorKey}`
|
|
261
275
|
>
|
|
262
276
|
: never;
|
package/document/index.ts
CHANGED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A `pick` or a `get` that matched nothing. Typed so a caller can tell "you asked for something that is not
|
|
3
|
+
* there" from "we broke" — an agent-facing transport in particular must not log a stack and answer "the server
|
|
4
|
+
* failed" every time a model guesses an id.
|
|
5
|
+
*
|
|
6
|
+
* The message is unchanged from the bare `Error` this replaces, because callers already match on it, and the
|
|
7
|
+
* `statusCode` does not reach the HTTP layer: `isExceptionLike` wants a `toJSON` too, so a missing document
|
|
8
|
+
* stays a 500 there until someone decides framework-wide that it should be a 404.
|
|
9
|
+
*/
|
|
10
|
+
export class NoDocumentError extends Error {
|
|
11
|
+
readonly statusCode = 404;
|
|
12
|
+
}
|
|
@@ -86,6 +86,15 @@ export class FetchClient {
|
|
|
86
86
|
this.handler = this.#makeHandlerProxy();
|
|
87
87
|
this.applySignal(serializedSignal);
|
|
88
88
|
}
|
|
89
|
+
/**
|
|
90
|
+
* Every signal any client in this process has applied, which is the whole callable surface of the app.
|
|
91
|
+
*
|
|
92
|
+
* A copy, because this is the registry each client merges its own signals into and a reader that mutated it
|
|
93
|
+
* would change what the next client applies. Read by the agent catalogue, which needs the argument schemas.
|
|
94
|
+
*/
|
|
95
|
+
static get sharedSerializedSignal(): { [key: string]: SerializedSignal } {
|
|
96
|
+
return { ...FetchClient.#sharedSerializedSignal };
|
|
97
|
+
}
|
|
89
98
|
static resetSharedRegistry() {
|
|
90
99
|
FetchClient.#sharedSerializedSignal = {};
|
|
91
100
|
FetchClient.#sharedRegistryVersion++;
|
|
@@ -246,6 +255,8 @@ export class FetchClient {
|
|
|
246
255
|
const parseReturn = this.#makeReturnParser(endpoint.returns);
|
|
247
256
|
const { bodyArgs, uploadArgs } = FetchClient.classifyHttpArgs(endpoint.args);
|
|
248
257
|
switch (endpoint.type) {
|
|
258
|
+
|
|
259
|
+
case "prompt":
|
|
249
260
|
case "query": {
|
|
250
261
|
const queryFn = async (...argData: unknown[]) => {
|
|
251
262
|
const args = argData.slice(0, argLength);
|
|
@@ -286,6 +297,7 @@ export class FetchClient {
|
|
|
286
297
|
}
|
|
287
298
|
#registerEndpoint(key: string, endpoint: SerializedEndpoint, prefix?: string) {
|
|
288
299
|
switch (endpoint.type) {
|
|
300
|
+
case "prompt":
|
|
289
301
|
case "query": {
|
|
290
302
|
this.#setHandlerFactory(key, () => this.#makeHttpFn(key, endpoint, prefix));
|
|
291
303
|
return;
|
|
@@ -7,6 +7,7 @@ import type {
|
|
|
7
7
|
EndpInfoReqType,
|
|
8
8
|
EndpointCls,
|
|
9
9
|
EndpointInfo,
|
|
10
|
+
PromptMessage,
|
|
10
11
|
SlceCnstFull,
|
|
11
12
|
SlceCnstInsight,
|
|
12
13
|
SlceCnstLight,
|
|
@@ -40,6 +41,9 @@ type QueryOrMutationFetchFn<E, SlceCls extends SliceCls | never> = (
|
|
|
40
41
|
...args: [...EndpInfoArgs<E>, fetchPolicy?: FetchPolicy]
|
|
41
42
|
) => Promise<EndpInfoReturns<E, SlceCls>>;
|
|
42
43
|
|
|
44
|
+
/** Typed off `PromptResult` rather than the endpoint's return ref, which is the `Any` carrier a prompt rides on. */
|
|
45
|
+
type PromptFetchFn<E> = (...args: [...EndpInfoArgs<E>, fetchPolicy?: FetchPolicy]) => Promise<PromptMessage[]>;
|
|
46
|
+
|
|
43
47
|
type MessageEmitFn<E> = (...args: EndpInfoArgs<E>) => void;
|
|
44
48
|
|
|
45
49
|
type MessageListenFn<E, SlceCls extends SliceCls | never> = (
|
|
@@ -58,12 +62,14 @@ type PubsubSubscribeFn<E, SlceCls extends SliceCls | never> = (
|
|
|
58
62
|
type PrimaryFetchFn<E, SlceCls extends SliceCls | never> =
|
|
59
63
|
EndpInfoReqType<E> extends "query" | "mutation"
|
|
60
64
|
? QueryOrMutationFetchFn<E, SlceCls>
|
|
61
|
-
: EndpInfoReqType<E> extends "
|
|
62
|
-
?
|
|
63
|
-
:
|
|
65
|
+
: EndpInfoReqType<E> extends "prompt"
|
|
66
|
+
? PromptFetchFn<E>
|
|
67
|
+
: EndpInfoReqType<E> extends "message"
|
|
68
|
+
? MessageEmitFn<E>
|
|
69
|
+
: never;
|
|
64
70
|
|
|
65
71
|
type PrimaryFetchType<EInfoObj extends { [key: string]: EndpointInfo }, SlceCls extends SliceCls | never> = {
|
|
66
|
-
[K in keyof EInfoObj as EndpInfoReqType<EInfoObj[K]> extends "query" | "mutation" | "message"
|
|
72
|
+
[K in keyof EInfoObj as EndpInfoReqType<EInfoObj[K]> extends "query" | "mutation" | "prompt" | "message"
|
|
67
73
|
? K
|
|
68
74
|
: never]: PrimaryFetchFn<EInfoObj[K], SlceCls>;
|
|
69
75
|
};
|