@moxxy/core 0.3.0
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/LICENSE +21 -0
- package/README.md +125 -0
- package/dist/events/factory.d.ts +6 -0
- package/dist/events/factory.d.ts.map +1 -0
- package/dist/events/factory.js +14 -0
- package/dist/events/factory.js.map +1 -0
- package/dist/events/log.d.ts +92 -0
- package/dist/events/log.d.ts.map +1 -0
- package/dist/events/log.js +244 -0
- package/dist/events/log.js.map +1 -0
- package/dist/index.d.ts +40 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +41 -0
- package/dist/index.js.map +1 -0
- package/dist/logger.d.ts +16 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +27 -0
- package/dist/logger.js.map +1 -0
- package/dist/permissions/engine.d.ts +94 -0
- package/dist/permissions/engine.d.ts.map +1 -0
- package/dist/permissions/engine.js +203 -0
- package/dist/permissions/engine.js.map +1 -0
- package/dist/permissions/resolvers.d.ts +8 -0
- package/dist/permissions/resolvers.d.ts.map +1 -0
- package/dist/permissions/resolvers.js +8 -0
- package/dist/permissions/resolvers.js.map +1 -0
- package/dist/plugins/discovery.d.ts +9 -0
- package/dist/plugins/discovery.d.ts.map +1 -0
- package/dist/plugins/discovery.js +112 -0
- package/dist/plugins/discovery.js.map +1 -0
- package/dist/plugins/host-options.d.ts +72 -0
- package/dist/plugins/host-options.d.ts.map +1 -0
- package/dist/plugins/host-options.js +2 -0
- package/dist/plugins/host-options.js.map +1 -0
- package/dist/plugins/host.d.ts +53 -0
- package/dist/plugins/host.d.ts.map +1 -0
- package/dist/plugins/host.js +269 -0
- package/dist/plugins/host.js.map +1 -0
- package/dist/plugins/lifecycle.d.ts +31 -0
- package/dist/plugins/lifecycle.d.ts.map +1 -0
- package/dist/plugins/lifecycle.js +100 -0
- package/dist/plugins/lifecycle.js.map +1 -0
- package/dist/plugins/loader.d.ts +7 -0
- package/dist/plugins/loader.d.ts.map +1 -0
- package/dist/plugins/loader.js +68 -0
- package/dist/plugins/loader.js.map +1 -0
- package/dist/plugins/package-requirements.d.ts +16 -0
- package/dist/plugins/package-requirements.d.ts.map +1 -0
- package/dist/plugins/package-requirements.js +52 -0
- package/dist/plugins/package-requirements.js.map +1 -0
- package/dist/plugins/registry-kinds.d.ts +58 -0
- package/dist/plugins/registry-kinds.d.ts.map +1 -0
- package/dist/plugins/registry-kinds.js +139 -0
- package/dist/plugins/registry-kinds.js.map +1 -0
- package/dist/plugins/toposort.d.ts +24 -0
- package/dist/plugins/toposort.d.ts.map +1 -0
- package/dist/plugins/toposort.js +67 -0
- package/dist/plugins/toposort.js.map +1 -0
- package/dist/preferences.d.ts +40 -0
- package/dist/preferences.d.ts.map +1 -0
- package/dist/preferences.js +58 -0
- package/dist/preferences.js.map +1 -0
- package/dist/registries/active-backend-registry.d.ts +65 -0
- package/dist/registries/active-backend-registry.d.ts.map +1 -0
- package/dist/registries/active-backend-registry.js +108 -0
- package/dist/registries/active-backend-registry.js.map +1 -0
- package/dist/registries/active-def-registry.d.ts +59 -0
- package/dist/registries/active-def-registry.d.ts.map +1 -0
- package/dist/registries/active-def-registry.js +63 -0
- package/dist/registries/active-def-registry.js.map +1 -0
- package/dist/registries/agents.d.ts +16 -0
- package/dist/registries/agents.d.ts.map +1 -0
- package/dist/registries/agents.js +17 -0
- package/dist/registries/agents.js.map +1 -0
- package/dist/registries/cache-strategies.d.ts +12 -0
- package/dist/registries/cache-strategies.d.ts.map +1 -0
- package/dist/registries/cache-strategies.js +13 -0
- package/dist/registries/cache-strategies.js.map +1 -0
- package/dist/registries/channels.d.ts +16 -0
- package/dist/registries/channels.d.ts.map +1 -0
- package/dist/registries/channels.js +35 -0
- package/dist/registries/channels.js.map +1 -0
- package/dist/registries/commands.d.ts +29 -0
- package/dist/registries/commands.d.ts.map +1 -0
- package/dist/registries/commands.js +72 -0
- package/dist/registries/commands.js.map +1 -0
- package/dist/registries/compactors.d.ts +12 -0
- package/dist/registries/compactors.d.ts.map +1 -0
- package/dist/registries/compactors.js +13 -0
- package/dist/registries/compactors.js.map +1 -0
- package/dist/registries/def-map-registry.d.ts +33 -0
- package/dist/registries/def-map-registry.d.ts.map +1 -0
- package/dist/registries/def-map-registry.js +36 -0
- package/dist/registries/def-map-registry.js.map +1 -0
- package/dist/registries/embedders.d.ts +16 -0
- package/dist/registries/embedders.d.ts.map +1 -0
- package/dist/registries/embedders.js +17 -0
- package/dist/registries/embedders.js.map +1 -0
- package/dist/registries/isolators.d.ts +21 -0
- package/dist/registries/isolators.d.ts.map +1 -0
- package/dist/registries/isolators.js +32 -0
- package/dist/registries/isolators.js.map +1 -0
- package/dist/registries/modes.d.ts +28 -0
- package/dist/registries/modes.d.ts.map +1 -0
- package/dist/registries/modes.js +81 -0
- package/dist/registries/modes.js.map +1 -0
- package/dist/registries/providers.d.ts +44 -0
- package/dist/registries/providers.d.ts.map +1 -0
- package/dist/registries/providers.js +95 -0
- package/dist/registries/providers.js.map +1 -0
- package/dist/registries/skills.d.ts +24 -0
- package/dist/registries/skills.d.ts.map +1 -0
- package/dist/registries/skills.js +81 -0
- package/dist/registries/skills.js.map +1 -0
- package/dist/registries/surfaces.d.ts +13 -0
- package/dist/registries/surfaces.d.ts.map +1 -0
- package/dist/registries/surfaces.js +14 -0
- package/dist/registries/surfaces.js.map +1 -0
- package/dist/registries/synthesizers.d.ts +26 -0
- package/dist/registries/synthesizers.d.ts.map +1 -0
- package/dist/registries/synthesizers.js +34 -0
- package/dist/registries/synthesizers.js.map +1 -0
- package/dist/registries/tools.d.ts +49 -0
- package/dist/registries/tools.d.ts.map +1 -0
- package/dist/registries/tools.js +98 -0
- package/dist/registries/tools.js.map +1 -0
- package/dist/registries/transcribers.d.ts +17 -0
- package/dist/registries/transcribers.d.ts.map +1 -0
- package/dist/registries/transcribers.js +18 -0
- package/dist/registries/transcribers.js.map +1 -0
- package/dist/registries/tunnel-providers.d.ts +12 -0
- package/dist/registries/tunnel-providers.d.ts.map +1 -0
- package/dist/registries/tunnel-providers.js +13 -0
- package/dist/registries/tunnel-providers.js.map +1 -0
- package/dist/registries/view-renderers.d.ts +13 -0
- package/dist/registries/view-renderers.d.ts.map +1 -0
- package/dist/registries/view-renderers.js +14 -0
- package/dist/registries/view-renderers.js.map +1 -0
- package/dist/registries/workflow-executors.d.ts +13 -0
- package/dist/registries/workflow-executors.d.ts.map +1 -0
- package/dist/registries/workflow-executors.js +14 -0
- package/dist/registries/workflow-executors.js.map +1 -0
- package/dist/requirements.d.ts +59 -0
- package/dist/requirements.d.ts.map +1 -0
- package/dist/requirements.js +149 -0
- package/dist/requirements.js.map +1 -0
- package/dist/run-turn.d.ts +6 -0
- package/dist/run-turn.d.ts.map +1 -0
- package/dist/run-turn.js +141 -0
- package/dist/run-turn.js.map +1 -0
- package/dist/session-runtime.d.ts +67 -0
- package/dist/session-runtime.d.ts.map +1 -0
- package/dist/session-runtime.js +2 -0
- package/dist/session-runtime.js.map +1 -0
- package/dist/session.d.ts +203 -0
- package/dist/session.d.ts.map +1 -0
- package/dist/session.js +399 -0
- package/dist/session.js.map +1 -0
- package/dist/sessions/persistence.d.ts +184 -0
- package/dist/sessions/persistence.d.ts.map +1 -0
- package/dist/sessions/persistence.js +442 -0
- package/dist/sessions/persistence.js.map +1 -0
- package/dist/setup-agent.d.ts +91 -0
- package/dist/setup-agent.d.ts.map +1 -0
- package/dist/setup-agent.js +138 -0
- package/dist/setup-agent.js.map +1 -0
- package/dist/skills/index.d.ts +4 -0
- package/dist/skills/index.d.ts.map +1 -0
- package/dist/skills/index.js +4 -0
- package/dist/skills/index.js.map +1 -0
- package/dist/skills/loader.d.ts +16 -0
- package/dist/skills/loader.d.ts.map +1 -0
- package/dist/skills/loader.js +99 -0
- package/dist/skills/loader.js.map +1 -0
- package/dist/skills/parse.d.ts +5 -0
- package/dist/skills/parse.d.ts.map +1 -0
- package/dist/skills/parse.js +8 -0
- package/dist/skills/parse.js.map +1 -0
- package/dist/skills/synthesize-draft.d.ts +8 -0
- package/dist/skills/synthesize-draft.d.ts.map +1 -0
- package/dist/skills/synthesize-draft.js +32 -0
- package/dist/skills/synthesize-draft.js.map +1 -0
- package/dist/skills/synthesize.d.ts +33 -0
- package/dist/skills/synthesize.d.ts.map +1 -0
- package/dist/skills/synthesize.js +241 -0
- package/dist/skills/synthesize.js.map +1 -0
- package/dist/subagents/events.d.ts +20 -0
- package/dist/subagents/events.d.ts.map +1 -0
- package/dist/subagents/events.js +146 -0
- package/dist/subagents/events.js.map +1 -0
- package/dist/subagents/registry.d.ts +40 -0
- package/dist/subagents/registry.d.ts.map +1 -0
- package/dist/subagents/registry.js +42 -0
- package/dist/subagents/registry.js.map +1 -0
- package/dist/subagents/run-child.d.ts +43 -0
- package/dist/subagents/run-child.d.ts.map +1 -0
- package/dist/subagents/run-child.js +276 -0
- package/dist/subagents/run-child.js.map +1 -0
- package/dist/subagents/spawn.d.ts +8 -0
- package/dist/subagents/spawn.d.ts.map +1 -0
- package/dist/subagents/spawn.js +8 -0
- package/dist/subagents/spawn.js.map +1 -0
- package/dist/subagents/tools.d.ts +4 -0
- package/dist/subagents/tools.d.ts.map +1 -0
- package/dist/subagents/tools.js +13 -0
- package/dist/subagents/tools.js.map +1 -0
- package/dist/subagents.d.ts +3 -0
- package/dist/subagents.d.ts.map +1 -0
- package/dist/subagents.js +3 -0
- package/dist/subagents.js.map +1 -0
- package/dist/surfaces/host.d.ts +53 -0
- package/dist/surfaces/host.d.ts.map +1 -0
- package/dist/surfaces/host.js +177 -0
- package/dist/surfaces/host.js.map +1 -0
- package/dist/tunnel/localhost.d.ts +7 -0
- package/dist/tunnel/localhost.d.ts.map +1 -0
- package/dist/tunnel/localhost.js +21 -0
- package/dist/tunnel/localhost.js.map +1 -0
- package/dist/usage-stats.d.ts +42 -0
- package/dist/usage-stats.d.ts.map +1 -0
- package/dist/usage-stats.js +83 -0
- package/dist/usage-stats.js.map +1 -0
- package/dist/view/default-renderer.d.ts +7 -0
- package/dist/view/default-renderer.d.ts.map +1 -0
- package/dist/view/default-renderer.js +14 -0
- package/dist/view/default-renderer.js.map +1 -0
- package/dist/view/parse.d.ts +7 -0
- package/dist/view/parse.d.ts.map +1 -0
- package/dist/view/parse.js +479 -0
- package/dist/view/parse.js.map +1 -0
- package/package.json +61 -0
- package/src/events/factory.ts +27 -0
- package/src/events/log.test.ts +470 -0
- package/src/events/log.ts +256 -0
- package/src/index.ts +102 -0
- package/src/logger.ts +46 -0
- package/src/permissions/engine.test.ts +206 -0
- package/src/permissions/engine.ts +247 -0
- package/src/permissions/resolvers.test.ts +40 -0
- package/src/permissions/resolvers.ts +17 -0
- package/src/plugins/discovery.test.ts +115 -0
- package/src/plugins/discovery.ts +124 -0
- package/src/plugins/host-options.ts +73 -0
- package/src/plugins/host-view-tunnel.test.ts +86 -0
- package/src/plugins/host.test.ts +434 -0
- package/src/plugins/host.ts +327 -0
- package/src/plugins/lifecycle.test.ts +187 -0
- package/src/plugins/lifecycle.ts +136 -0
- package/src/plugins/loader.ts +79 -0
- package/src/plugins/package-requirements.ts +50 -0
- package/src/plugins/registry-kinds.test.ts +162 -0
- package/src/plugins/registry-kinds.ts +210 -0
- package/src/plugins/toposort.test.ts +66 -0
- package/src/plugins/toposort.ts +69 -0
- package/src/preferences.test.ts +105 -0
- package/src/preferences.ts +84 -0
- package/src/registries/active-backend-registry.ts +142 -0
- package/src/registries/active-def-registry.test.ts +103 -0
- package/src/registries/active-def-registry.ts +97 -0
- package/src/registries/agents.ts +18 -0
- package/src/registries/cache-strategies.ts +14 -0
- package/src/registries/channels.ts +42 -0
- package/src/registries/commands.test.ts +71 -0
- package/src/registries/commands.ts +78 -0
- package/src/registries/compactors.ts +14 -0
- package/src/registries/def-map-registry.test.ts +67 -0
- package/src/registries/def-map-registry.ts +59 -0
- package/src/registries/embedders.test.ts +54 -0
- package/src/registries/embedders.ts +18 -0
- package/src/registries/isolators.test.ts +43 -0
- package/src/registries/isolators.ts +38 -0
- package/src/registries/modes.test.ts +66 -0
- package/src/registries/modes.ts +80 -0
- package/src/registries/providers.test.ts +50 -0
- package/src/registries/providers.ts +97 -0
- package/src/registries/semantics.test.ts +117 -0
- package/src/registries/skills.ts +87 -0
- package/src/registries/surfaces.ts +18 -0
- package/src/registries/synthesizers.test.ts +63 -0
- package/src/registries/synthesizers.ts +36 -0
- package/src/registries/tools.test.ts +142 -0
- package/src/registries/tools.ts +145 -0
- package/src/registries/transcribers.test.ts +70 -0
- package/src/registries/transcribers.ts +19 -0
- package/src/registries/tunnel-providers.test.ts +30 -0
- package/src/registries/tunnel-providers.ts +14 -0
- package/src/registries/view-renderers.test.ts +68 -0
- package/src/registries/view-renderers.ts +15 -0
- package/src/registries/workflow-executors.test.ts +53 -0
- package/src/registries/workflow-executors.ts +15 -0
- package/src/requirements.targetinfo.test.ts +70 -0
- package/src/requirements.test.ts +111 -0
- package/src/requirements.ts +262 -0
- package/src/run-turn.test.ts +178 -0
- package/src/run-turn.ts +156 -0
- package/src/session-runtime.ts +75 -0
- package/src/session.test.ts +216 -0
- package/src/session.ts +491 -0
- package/src/sessions/persistence.flush.test.ts +100 -0
- package/src/sessions/persistence.ready.test.ts +91 -0
- package/src/sessions/persistence.test.ts +274 -0
- package/src/sessions/persistence.ts +498 -0
- package/src/setup-agent.test.ts +72 -0
- package/src/setup-agent.ts +189 -0
- package/src/skills/index.ts +14 -0
- package/src/skills/loader.test.ts +102 -0
- package/src/skills/loader.ts +114 -0
- package/src/skills/parse.test.ts +59 -0
- package/src/skills/parse.ts +11 -0
- package/src/skills/synthesize-draft.test.ts +79 -0
- package/src/skills/synthesize-draft.ts +46 -0
- package/src/skills/synthesize.test.ts +170 -0
- package/src/skills/synthesize.ts +304 -0
- package/src/subagents/events.test.ts +56 -0
- package/src/subagents/events.ts +194 -0
- package/src/subagents/registry.ts +62 -0
- package/src/subagents/run-child.test.ts +185 -0
- package/src/subagents/run-child.ts +425 -0
- package/src/subagents/spawn.ts +12 -0
- package/src/subagents/tools.ts +19 -0
- package/src/subagents.ts +2 -0
- package/src/surfaces/host.test.ts +330 -0
- package/src/surfaces/host.ts +192 -0
- package/src/tunnel/localhost.test.ts +22 -0
- package/src/tunnel/localhost.ts +21 -0
- package/src/usage-stats.test.ts +77 -0
- package/src/usage-stats.ts +119 -0
- package/src/view/default-renderer.test.ts +29 -0
- package/src/view/default-renderer.ts +14 -0
- package/src/view/parse-extended.test.ts +255 -0
- package/src/view/parse.test.ts +232 -0
- package/src/view/parse.ts +520 -0
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { type ModelUsageTotals } from '@moxxy/sdk';
|
|
2
|
+
/**
|
|
3
|
+
* Cross-session token usage, persisted at ~/.moxxy/usage.json. A forward-going
|
|
4
|
+
* aggregate keyed by `"<provider>/<model>"`: each session folds its own
|
|
5
|
+
* `provider_response` usage and merges the delta in on shutdown (see
|
|
6
|
+
* `@moxxy/plugin-usage-stats`). Purely additive counters — no derived/cost
|
|
7
|
+
* fields — so a session's contribution is added once and the file stays a sum.
|
|
8
|
+
*
|
|
9
|
+
* Like preferences, this is best-effort: a missing or malformed file reads as
|
|
10
|
+
* empty, and a write failure never blocks shutdown.
|
|
11
|
+
*/
|
|
12
|
+
export interface StoredModelUsage extends ModelUsageTotals {
|
|
13
|
+
/** ISO timestamp of the first session that recorded usage for this model. */
|
|
14
|
+
readonly firstSeen: string;
|
|
15
|
+
/** ISO timestamp of the most recent session that recorded usage. */
|
|
16
|
+
readonly lastSeen: string;
|
|
17
|
+
}
|
|
18
|
+
export interface UsageStatsFile {
|
|
19
|
+
readonly version: 1;
|
|
20
|
+
/** ISO timestamp of the last merge/clear. */
|
|
21
|
+
readonly updatedAt: string;
|
|
22
|
+
/** Per-`provider/model` lifetime totals. */
|
|
23
|
+
readonly models: Record<string, StoredModelUsage>;
|
|
24
|
+
}
|
|
25
|
+
export declare function usageStatsPath(): string;
|
|
26
|
+
/**
|
|
27
|
+
* Read the usage aggregate. Returns an empty file when missing or unparseable —
|
|
28
|
+
* usage stats are an optional, non-load-blocking layer.
|
|
29
|
+
*/
|
|
30
|
+
export declare function loadUsageStats(filePath?: string): Promise<UsageStatsFile>;
|
|
31
|
+
/**
|
|
32
|
+
* Merge a session's per-model delta into the persisted aggregate and write it
|
|
33
|
+
* back. Reads the current file, adds each delta field-wise, refreshes
|
|
34
|
+
* first/lastSeen, and writes atomically. Returns the updated file.
|
|
35
|
+
*
|
|
36
|
+
* Best-effort: a write failure logs to stderr but does not throw — losing one
|
|
37
|
+
* session's stats must never block shutdown.
|
|
38
|
+
*/
|
|
39
|
+
export declare function mergeUsageStats(delta: Record<string, ModelUsageTotals>, filePath?: string): Promise<UsageStatsFile>;
|
|
40
|
+
/** Reset the aggregate to empty (the user-facing `/usage clear`). */
|
|
41
|
+
export declare function clearUsageStats(filePath?: string): Promise<void>;
|
|
42
|
+
//# sourceMappingURL=usage-stats.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"usage-stats.d.ts","sourceRoot":"","sources":["../src/usage-stats.ts"],"names":[],"mappings":"AAGA,OAAO,EAA+B,KAAK,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAGhF;;;;;;;;;GASG;AACH,MAAM,WAAW,gBAAiB,SAAQ,gBAAgB;IACxD,6EAA6E;IAC7E,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,oEAAoE;IACpE,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;CAC3B;AAED,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,OAAO,EAAE,CAAC,CAAC;IACpB,6CAA6C;IAC7C,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,4CAA4C;IAC5C,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAAC;CACnD;AAED,wBAAgB,cAAc,IAAI,MAAM,CAEvC;AAMD;;;GAGG;AACH,wBAAsB,cAAc,CAAC,QAAQ,GAAE,MAAyB,GAAG,OAAO,CAAC,cAAc,CAAC,CAgBjG;AAWD;;;;;;;GAOG;AACH,wBAAsB,eAAe,CACnC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAAC,EACvC,QAAQ,GAAE,MAAyB,GAClC,OAAO,CAAC,cAAc,CAAC,CAgCzB;AAED,qEAAqE;AACrE,wBAAsB,eAAe,CAAC,QAAQ,GAAE,MAAyB,GAAG,OAAO,CAAC,IAAI,CAAC,CAExF"}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { promises as fs } from 'node:fs';
|
|
2
|
+
import * as os from 'node:os';
|
|
3
|
+
import * as path from 'node:path';
|
|
4
|
+
import { addModelTotals, createMutex } from '@moxxy/sdk';
|
|
5
|
+
import { writeFileAtomic } from '@moxxy/sdk/server';
|
|
6
|
+
export function usageStatsPath() {
|
|
7
|
+
return path.join(os.homedir(), '.moxxy', 'usage.json');
|
|
8
|
+
}
|
|
9
|
+
function emptyStats() {
|
|
10
|
+
return { version: 1, updatedAt: new Date().toISOString(), models: {} };
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Read the usage aggregate. Returns an empty file when missing or unparseable —
|
|
14
|
+
* usage stats are an optional, non-load-blocking layer.
|
|
15
|
+
*/
|
|
16
|
+
export async function loadUsageStats(filePath = usageStatsPath()) {
|
|
17
|
+
try {
|
|
18
|
+
const raw = await fs.readFile(filePath, 'utf8');
|
|
19
|
+
const parsed = JSON.parse(raw);
|
|
20
|
+
if (parsed &&
|
|
21
|
+
typeof parsed === 'object' &&
|
|
22
|
+
typeof parsed.models === 'object' &&
|
|
23
|
+
parsed.models !== null) {
|
|
24
|
+
return parsed;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
// missing or malformed — start fresh
|
|
29
|
+
}
|
|
30
|
+
return emptyStats();
|
|
31
|
+
}
|
|
32
|
+
async function writeAtomic(file, filePath) {
|
|
33
|
+
await writeFileAtomic(filePath, JSON.stringify(file, null, 2) + '\n');
|
|
34
|
+
}
|
|
35
|
+
// Serializes the read-modify-write in `mergeUsageStats` so two concurrent
|
|
36
|
+
// merges (e.g. overlapping shutdowns) can't both read the same snapshot and
|
|
37
|
+
// have the second write clobber the first's delta.
|
|
38
|
+
const mergeMutex = createMutex();
|
|
39
|
+
/**
|
|
40
|
+
* Merge a session's per-model delta into the persisted aggregate and write it
|
|
41
|
+
* back. Reads the current file, adds each delta field-wise, refreshes
|
|
42
|
+
* first/lastSeen, and writes atomically. Returns the updated file.
|
|
43
|
+
*
|
|
44
|
+
* Best-effort: a write failure logs to stderr but does not throw — losing one
|
|
45
|
+
* session's stats must never block shutdown.
|
|
46
|
+
*/
|
|
47
|
+
export async function mergeUsageStats(delta, filePath = usageStatsPath()) {
|
|
48
|
+
// An empty delta writes nothing, so there's no read-modify-write to
|
|
49
|
+
// serialize: skip the mutex entirely and read the current file directly. A
|
|
50
|
+
// session with no recorded usage produces an empty delta on every shutdown,
|
|
51
|
+
// so this avoids needless I/O + contention on the common no-op path.
|
|
52
|
+
const keys = Object.keys(delta);
|
|
53
|
+
if (keys.length === 0)
|
|
54
|
+
return loadUsageStats(filePath);
|
|
55
|
+
return mergeMutex.run(async () => {
|
|
56
|
+
const current = await loadUsageStats(filePath);
|
|
57
|
+
const now = new Date().toISOString();
|
|
58
|
+
const models = { ...current.models };
|
|
59
|
+
for (const key of keys) {
|
|
60
|
+
const d = delta[key];
|
|
61
|
+
if (d.calls === 0)
|
|
62
|
+
continue;
|
|
63
|
+
const existing = models[key];
|
|
64
|
+
models[key] = existing
|
|
65
|
+
? { ...addModelTotals(existing, d), firstSeen: existing.firstSeen, lastSeen: now }
|
|
66
|
+
: { ...d, firstSeen: now, lastSeen: now };
|
|
67
|
+
}
|
|
68
|
+
const next = { version: 1, updatedAt: now, models };
|
|
69
|
+
try {
|
|
70
|
+
await writeAtomic(next, filePath);
|
|
71
|
+
}
|
|
72
|
+
catch (err) {
|
|
73
|
+
process.stderr.write(`moxxy: failed to persist usage stats to ${filePath}: ` +
|
|
74
|
+
`${err instanceof Error ? err.message : String(err)}\n`);
|
|
75
|
+
}
|
|
76
|
+
return next;
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
/** Reset the aggregate to empty (the user-facing `/usage clear`). */
|
|
80
|
+
export async function clearUsageStats(filePath = usageStatsPath()) {
|
|
81
|
+
await writeAtomic(emptyStats(), filePath);
|
|
82
|
+
}
|
|
83
|
+
//# sourceMappingURL=usage-stats.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"usage-stats.js","sourceRoot":"","sources":["../src/usage-stats.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The built-in view-spec renderer, seeded into every session's
|
|
3
|
+
* {@link ViewRendererRegistry}. Plugins may register/replace and `setActive`
|
|
4
|
+
* an alternative renderer with a different vocabulary.
|
|
5
|
+
*/
|
|
6
|
+
export declare const defaultViewRenderer: import("@moxxy/sdk").ViewRendererDef;
|
|
7
|
+
//# sourceMappingURL=default-renderer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"default-renderer.d.ts","sourceRoot":"","sources":["../../src/view/default-renderer.ts"],"names":[],"mappings":"AAGA;;;;GAIG;AACH,eAAO,MAAM,mBAAmB,sCAK9B,CAAC"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { DEFAULT_VIEW_TAGS, defineViewRenderer } from '@moxxy/sdk';
|
|
2
|
+
import { parseView, validateDoc } from './parse.js';
|
|
3
|
+
/**
|
|
4
|
+
* The built-in view-spec renderer, seeded into every session's
|
|
5
|
+
* {@link ViewRendererRegistry}. Plugins may register/replace and `setActive`
|
|
6
|
+
* an alternative renderer with a different vocabulary.
|
|
7
|
+
*/
|
|
8
|
+
export const defaultViewRenderer = defineViewRenderer({
|
|
9
|
+
name: 'moxxy/default',
|
|
10
|
+
allowList: DEFAULT_VIEW_TAGS,
|
|
11
|
+
parse: (source) => parseView(source, DEFAULT_VIEW_TAGS),
|
|
12
|
+
validate: (doc) => validateDoc(doc, DEFAULT_VIEW_TAGS),
|
|
13
|
+
});
|
|
14
|
+
//# sourceMappingURL=default-renderer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"default-renderer.js","sourceRoot":"","sources":["../../src/view/default-renderer.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AACnE,OAAO,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAEpD;;;;GAIG;AACH,MAAM,CAAC,MAAM,mBAAmB,GAAG,kBAAkB,CAAC;IACpD,IAAI,EAAE,eAAe;IACrB,SAAS,EAAE,iBAAiB;IAC5B,KAAK,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,SAAS,CAAC,MAAM,EAAE,iBAAiB,CAAC;IACvD,QAAQ,EAAE,CAAC,GAAG,EAAE,EAAE,CAAC,WAAW,CAAC,GAAG,EAAE,iBAAiB,CAAC;CACvD,CAAC,CAAC"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { countNodes } from '@moxxy/sdk';
|
|
2
|
+
import type { ViewDoc, ViewParseError, ViewParseResult, ViewTagSpec } from '@moxxy/sdk';
|
|
3
|
+
export declare function parseView(source: string, allowList: ReadonlyArray<ViewTagSpec>): ViewParseResult;
|
|
4
|
+
/** Validate an already-built (expanded) AST against the allow-list. */
|
|
5
|
+
export declare function validateDoc(doc: ViewDoc, allowList: ReadonlyArray<ViewTagSpec>): ViewParseError[];
|
|
6
|
+
export { countNodes };
|
|
7
|
+
//# sourceMappingURL=parse.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"parse.d.ts","sourceRoot":"","sources":["../../src/view/parse.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAiB,MAAM,YAAY,CAAC;AACvD,OAAO,KAAK,EAEV,OAAO,EAEP,cAAc,EACd,eAAe,EACf,WAAW,EACZ,MAAM,YAAY,CAAC;AA8bpB,wBAAgB,SAAS,CAAC,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,aAAa,CAAC,WAAW,CAAC,GAAG,eAAe,CAehG;AAED,uEAAuE;AACvE,wBAAgB,WAAW,CAAC,GAAG,EAAE,OAAO,EAAE,SAAS,EAAE,aAAa,CAAC,WAAW,CAAC,GAAG,cAAc,EAAE,CA0CjG;AAKD,OAAO,EAAE,UAAU,EAAE,CAAC"}
|
|
@@ -0,0 +1,479 @@
|
|
|
1
|
+
import { countNodes, isSafeViewUrl } from '@moxxy/sdk';
|
|
2
|
+
class ParseFail extends Error {
|
|
3
|
+
info;
|
|
4
|
+
constructor(info) {
|
|
5
|
+
super(info.message);
|
|
6
|
+
this.info = info;
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
function lineColAt(src, pos) {
|
|
10
|
+
let line = 1;
|
|
11
|
+
let col = 1;
|
|
12
|
+
for (let i = 0; i < pos && i < src.length; i++) {
|
|
13
|
+
if (src[i] === '\n') {
|
|
14
|
+
line++;
|
|
15
|
+
col = 1;
|
|
16
|
+
}
|
|
17
|
+
else {
|
|
18
|
+
col++;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return { line, col };
|
|
22
|
+
}
|
|
23
|
+
function fail(src, pos, message) {
|
|
24
|
+
throw new ParseFail({ message, ...lineColAt(src, pos) });
|
|
25
|
+
}
|
|
26
|
+
function tokenize(src) {
|
|
27
|
+
const tokens = [];
|
|
28
|
+
let i = 0;
|
|
29
|
+
const n = src.length;
|
|
30
|
+
while (i < n) {
|
|
31
|
+
const lt = src.indexOf('<', i);
|
|
32
|
+
if (lt === -1) {
|
|
33
|
+
if (src.slice(i).trim())
|
|
34
|
+
tokens.push({ t: 'text', value: src.slice(i), pos: i });
|
|
35
|
+
break;
|
|
36
|
+
}
|
|
37
|
+
if (lt > i && src.slice(i, lt).trim()) {
|
|
38
|
+
tokens.push({ t: 'text', value: src.slice(i, lt), pos: i });
|
|
39
|
+
}
|
|
40
|
+
// Comment: <!-- ... -->
|
|
41
|
+
if (src.startsWith('<!--', lt)) {
|
|
42
|
+
const end = src.indexOf('-->', lt + 4);
|
|
43
|
+
i = end === -1 ? n : end + 3;
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
// Closing tag
|
|
47
|
+
if (src[lt + 1] === '/') {
|
|
48
|
+
const gt = src.indexOf('>', lt);
|
|
49
|
+
if (gt === -1)
|
|
50
|
+
fail(src, lt, 'unterminated closing tag');
|
|
51
|
+
tokens.push({ t: 'close', tag: src.slice(lt + 2, gt).trim(), pos: lt });
|
|
52
|
+
i = gt + 1;
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
// Opening tag — read to the matching top-level '>' (quotes/braces aware)
|
|
56
|
+
const { end, inner } = readRawTag(src, lt);
|
|
57
|
+
let body = inner.trim();
|
|
58
|
+
const selfClose = body.endsWith('/');
|
|
59
|
+
if (selfClose)
|
|
60
|
+
body = body.slice(0, -1).trim();
|
|
61
|
+
const sp = body.search(/\s/);
|
|
62
|
+
const tag = (sp === -1 ? body : body.slice(0, sp)).trim();
|
|
63
|
+
if (!tag || !/^[A-Za-z][A-Za-z0-9-]*$/.test(tag))
|
|
64
|
+
fail(src, lt, `invalid tag name "${tag}"`);
|
|
65
|
+
const attrs = sp === -1 ? [] : parseAttrs(body.slice(sp));
|
|
66
|
+
tokens.push({ t: 'open', tag, attrs, selfClose, pos: lt });
|
|
67
|
+
i = end + 1;
|
|
68
|
+
}
|
|
69
|
+
return tokens;
|
|
70
|
+
}
|
|
71
|
+
function readRawTag(src, lt) {
|
|
72
|
+
let i = lt + 1;
|
|
73
|
+
const n = src.length;
|
|
74
|
+
let quote = null;
|
|
75
|
+
let brace = 0;
|
|
76
|
+
while (i < n) {
|
|
77
|
+
const c = src[i];
|
|
78
|
+
if (quote) {
|
|
79
|
+
if (c === quote)
|
|
80
|
+
quote = null;
|
|
81
|
+
}
|
|
82
|
+
else if (c === '"' || c === "'") {
|
|
83
|
+
quote = c;
|
|
84
|
+
}
|
|
85
|
+
else if (c === '{') {
|
|
86
|
+
brace++;
|
|
87
|
+
}
|
|
88
|
+
else if (c === '}') {
|
|
89
|
+
if (brace > 0)
|
|
90
|
+
brace--;
|
|
91
|
+
}
|
|
92
|
+
else if (c === '>' && brace === 0) {
|
|
93
|
+
return { end: i, inner: src.slice(lt + 1, i) };
|
|
94
|
+
}
|
|
95
|
+
i++;
|
|
96
|
+
}
|
|
97
|
+
fail(src, lt, 'unterminated tag');
|
|
98
|
+
}
|
|
99
|
+
function parseAttrs(s) {
|
|
100
|
+
const attrs = [];
|
|
101
|
+
let i = 0;
|
|
102
|
+
const n = s.length;
|
|
103
|
+
while (i < n) {
|
|
104
|
+
while (i < n && /\s/.test(s[i]))
|
|
105
|
+
i++;
|
|
106
|
+
if (i >= n)
|
|
107
|
+
break;
|
|
108
|
+
let name = '';
|
|
109
|
+
while (i < n && /[A-Za-z0-9\-_:]/.test(s[i])) {
|
|
110
|
+
name += s[i];
|
|
111
|
+
i++;
|
|
112
|
+
}
|
|
113
|
+
if (!name) {
|
|
114
|
+
i++; // skip a stray separator char
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
let j = i;
|
|
118
|
+
while (j < n && /\s/.test(s[j]))
|
|
119
|
+
j++;
|
|
120
|
+
if (s[j] === '=') {
|
|
121
|
+
i = j + 1;
|
|
122
|
+
while (i < n && /\s/.test(s[i]))
|
|
123
|
+
i++;
|
|
124
|
+
const q = s[i];
|
|
125
|
+
let value = '';
|
|
126
|
+
if (q === '"' || q === "'") {
|
|
127
|
+
i++;
|
|
128
|
+
while (i < n && s[i] !== q) {
|
|
129
|
+
value += s[i];
|
|
130
|
+
i++;
|
|
131
|
+
}
|
|
132
|
+
i++; // closing quote
|
|
133
|
+
}
|
|
134
|
+
else if (q === '{') {
|
|
135
|
+
const start = i;
|
|
136
|
+
let depth = 0;
|
|
137
|
+
while (i < n) {
|
|
138
|
+
if (s[i] === '{')
|
|
139
|
+
depth++;
|
|
140
|
+
else if (s[i] === '}') {
|
|
141
|
+
depth--;
|
|
142
|
+
if (depth === 0) {
|
|
143
|
+
i++;
|
|
144
|
+
break;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
i++;
|
|
148
|
+
}
|
|
149
|
+
value = s.slice(start, i); // keep braces; raw
|
|
150
|
+
}
|
|
151
|
+
else {
|
|
152
|
+
while (i < n && !/\s/.test(s[i])) {
|
|
153
|
+
value += s[i];
|
|
154
|
+
i++;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
attrs.push({ name, value });
|
|
158
|
+
}
|
|
159
|
+
else {
|
|
160
|
+
attrs.push({ name, value: true });
|
|
161
|
+
i = j;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return attrs;
|
|
165
|
+
}
|
|
166
|
+
function isBNode(c) {
|
|
167
|
+
return 'tag' in c;
|
|
168
|
+
}
|
|
169
|
+
function build(tokens, src) {
|
|
170
|
+
const top = [];
|
|
171
|
+
const stack = [];
|
|
172
|
+
for (const tok of tokens) {
|
|
173
|
+
const target = stack.length ? stack[stack.length - 1].children : top;
|
|
174
|
+
if (tok.t === 'text') {
|
|
175
|
+
target.push({ text: decodeEntities(tok.value) });
|
|
176
|
+
}
|
|
177
|
+
else if (tok.t === 'open') {
|
|
178
|
+
const node = { tag: tok.tag, attrs: tok.attrs, children: [], pos: tok.pos };
|
|
179
|
+
target.push(node);
|
|
180
|
+
if (!tok.selfClose)
|
|
181
|
+
stack.push(node);
|
|
182
|
+
}
|
|
183
|
+
else {
|
|
184
|
+
const open = stack.pop();
|
|
185
|
+
if (!open)
|
|
186
|
+
fail(src, tok.pos, `unexpected closing tag </${tok.tag}>`);
|
|
187
|
+
if (open.tag !== tok.tag) {
|
|
188
|
+
fail(src, tok.pos, `mismatched closing tag: expected </${open.tag}>, got </${tok.tag}>`);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
if (stack.length)
|
|
193
|
+
fail(src, stack[stack.length - 1].pos, `unclosed tag <${stack[stack.length - 1].tag}>`);
|
|
194
|
+
const elements = top.filter(isBNode);
|
|
195
|
+
if (elements.length !== 1) {
|
|
196
|
+
throw new ParseFail({ message: `expected exactly one root element, found ${elements.length}` });
|
|
197
|
+
}
|
|
198
|
+
return elements[0];
|
|
199
|
+
}
|
|
200
|
+
function decodeEntities(s) {
|
|
201
|
+
return s
|
|
202
|
+
.replace(/</g, '<')
|
|
203
|
+
.replace(/>/g, '>')
|
|
204
|
+
.replace(/"/g, '"')
|
|
205
|
+
.replace(/'/g, "'")
|
|
206
|
+
.replace(/'/g, "'")
|
|
207
|
+
.replace(/&/g, '&');
|
|
208
|
+
}
|
|
209
|
+
// ---------------------------------------------------------------------------
|
|
210
|
+
// Validation + coercion (BNode → ViewNode), collecting all errors
|
|
211
|
+
// ---------------------------------------------------------------------------
|
|
212
|
+
// URL safety: the canonical scheme allow-list is `isSafeViewUrl` in
|
|
213
|
+
// @moxxy/sdk (view-renderer.ts) — shared with `validateDoc` below and
|
|
214
|
+
// mirrored by the web frontend's render-time re-check.
|
|
215
|
+
function coerceValue(tag, name, value, spec, errors) {
|
|
216
|
+
if (spec.type === 'boolean') {
|
|
217
|
+
if (value === true)
|
|
218
|
+
return true;
|
|
219
|
+
return value !== 'false';
|
|
220
|
+
}
|
|
221
|
+
if (value === true) {
|
|
222
|
+
errors.push({ message: `<${tag}> attribute "${name}" expects a value` });
|
|
223
|
+
return '';
|
|
224
|
+
}
|
|
225
|
+
if (spec.type === 'number') {
|
|
226
|
+
const num = Number(value);
|
|
227
|
+
if (!Number.isFinite(num)) {
|
|
228
|
+
errors.push({ message: `<${tag}> attribute "${name}" must be a number` });
|
|
229
|
+
return 0;
|
|
230
|
+
}
|
|
231
|
+
if (spec.min !== undefined && num < spec.min) {
|
|
232
|
+
errors.push({ message: `<${tag}> attribute "${name}" must be ≥ ${spec.min}` });
|
|
233
|
+
}
|
|
234
|
+
if (spec.max !== undefined && num > spec.max) {
|
|
235
|
+
errors.push({ message: `<${tag}> attribute "${name}" must be ≤ ${spec.max}` });
|
|
236
|
+
}
|
|
237
|
+
return num;
|
|
238
|
+
}
|
|
239
|
+
// Decode HTML entities in attribute string values so they match the
|
|
240
|
+
// contract of text nodes (which build() decodes), e.g. a label of
|
|
241
|
+
// `a & b` renders `a & b` instead of the literal `a & b`. Decoding
|
|
242
|
+
// happens BEFORE the URL-safety check below, so an entity-encoded scheme
|
|
243
|
+
// (e.g. `href="javascript:…"`) is normalized to `javascript:` and still
|
|
244
|
+
// rejected — decoding can never smuggle a disallowed scheme past the check.
|
|
245
|
+
const decoded = decodeEntities(value);
|
|
246
|
+
if (spec.type === 'enum') {
|
|
247
|
+
if (!spec.values?.includes(decoded)) {
|
|
248
|
+
errors.push({ message: `<${tag}> attribute "${name}" must be one of: ${spec.values?.join(', ')}` });
|
|
249
|
+
}
|
|
250
|
+
return decoded;
|
|
251
|
+
}
|
|
252
|
+
// string
|
|
253
|
+
if ((name === 'href' || name === 'src') && !isSafeViewUrl(decoded, name)) {
|
|
254
|
+
errors.push({ message: `<${tag}> attribute "${name}" has a disallowed URL scheme` });
|
|
255
|
+
}
|
|
256
|
+
return decoded;
|
|
257
|
+
}
|
|
258
|
+
function coerceAttrs(b, spec, errors) {
|
|
259
|
+
const out = {};
|
|
260
|
+
const seen = new Set();
|
|
261
|
+
for (const a of b.attrs) {
|
|
262
|
+
if (/^on/i.test(a.name)) {
|
|
263
|
+
errors.push({ message: `<${b.tag}> forbidden attribute "${a.name}"` });
|
|
264
|
+
continue;
|
|
265
|
+
}
|
|
266
|
+
const aspec = spec?.attrs[a.name];
|
|
267
|
+
if (!aspec) {
|
|
268
|
+
// When the TAG itself is unknown (spec === undefined) convert() already
|
|
269
|
+
// pushed one `unknown tag` error — flagging every attribute as `unknown
|
|
270
|
+
// attribute` on top of it is redundant noise the agent reads back. The
|
|
271
|
+
// on* security check above still fires either way. Only emit the
|
|
272
|
+
// per-attribute error when the tag is known and the attribute isn't.
|
|
273
|
+
if (spec)
|
|
274
|
+
errors.push({ message: `<${b.tag}> unknown attribute "${a.name}"` });
|
|
275
|
+
continue;
|
|
276
|
+
}
|
|
277
|
+
seen.add(a.name);
|
|
278
|
+
out[a.name] = coerceValue(b.tag, a.name, a.value, aspec, errors);
|
|
279
|
+
}
|
|
280
|
+
if (spec) {
|
|
281
|
+
for (const [name, aspec] of Object.entries(spec.attrs)) {
|
|
282
|
+
if (aspec.required && !seen.has(name)) {
|
|
283
|
+
errors.push({ message: `<${b.tag}> missing required attribute "${name}"` });
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
return out;
|
|
288
|
+
}
|
|
289
|
+
function convert(b, specByTag, errors) {
|
|
290
|
+
const spec = specByTag.get(b.tag);
|
|
291
|
+
if (!spec)
|
|
292
|
+
errors.push({ message: `unknown tag <${b.tag}>` });
|
|
293
|
+
const props = coerceAttrs(b, spec, errors);
|
|
294
|
+
const children = [];
|
|
295
|
+
for (const c of b.children) {
|
|
296
|
+
if (!isBNode(c)) {
|
|
297
|
+
const v = c.text.replace(/\s+/g, ' ').trim();
|
|
298
|
+
if (!v)
|
|
299
|
+
continue; // drop whitespace-only text
|
|
300
|
+
if (spec && spec.allowedChildren !== 'any') {
|
|
301
|
+
errors.push({ message: `<${b.tag}> may not contain text` });
|
|
302
|
+
}
|
|
303
|
+
children.push({ kind: 'text', value: v });
|
|
304
|
+
}
|
|
305
|
+
else {
|
|
306
|
+
if (spec) {
|
|
307
|
+
if (spec.allowedChildren === 'none') {
|
|
308
|
+
errors.push({ message: `<${b.tag}> may not have children` });
|
|
309
|
+
}
|
|
310
|
+
else if (Array.isArray(spec.allowedChildren) && !spec.allowedChildren.includes(c.tag)) {
|
|
311
|
+
errors.push({ message: `<${b.tag}> may not contain <${c.tag}>` });
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
children.push(convert(c, specByTag, errors));
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
return finalizeNode({ kind: 'element', tag: b.tag, props, children }, spec);
|
|
318
|
+
}
|
|
319
|
+
// ---------------------------------------------------------------------------
|
|
320
|
+
// Action + navigation extraction
|
|
321
|
+
// ---------------------------------------------------------------------------
|
|
322
|
+
function collectFieldNames(node) {
|
|
323
|
+
const names = [];
|
|
324
|
+
const walk = (n) => {
|
|
325
|
+
if (n.kind !== 'element')
|
|
326
|
+
return;
|
|
327
|
+
if ((n.tag === 'input' || n.tag === 'select' || n.tag === 'checkbox') && typeof n.props.name === 'string') {
|
|
328
|
+
names.push(n.props.name);
|
|
329
|
+
}
|
|
330
|
+
n.children.forEach(walk);
|
|
331
|
+
};
|
|
332
|
+
walk(node);
|
|
333
|
+
return names;
|
|
334
|
+
}
|
|
335
|
+
function finalizeNode(node, spec) {
|
|
336
|
+
if (node.kind !== 'element')
|
|
337
|
+
return node;
|
|
338
|
+
let out = node;
|
|
339
|
+
// Client-side navigation: any element carrying a `to` (link/button).
|
|
340
|
+
if (typeof node.props.to === 'string' && node.props.to) {
|
|
341
|
+
out = { ...out, nav: node.props.to };
|
|
342
|
+
}
|
|
343
|
+
if (spec?.interactive) {
|
|
344
|
+
if (node.tag === 'form') {
|
|
345
|
+
const name = typeof node.props.action === 'string' ? node.props.action : '';
|
|
346
|
+
out = { ...out, action: { name, fields: collectFieldNames(node) } };
|
|
347
|
+
}
|
|
348
|
+
else if (node.tag === 'button' && typeof node.props.action === 'string' && node.props.action) {
|
|
349
|
+
const fields = typeof node.props.fields === 'string' && node.props.fields
|
|
350
|
+
? node.props.fields.split(',').map((f) => f.trim()).filter(Boolean)
|
|
351
|
+
: [];
|
|
352
|
+
out = { ...out, action: { name: node.props.action, fields } };
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
return out;
|
|
356
|
+
}
|
|
357
|
+
// ---------------------------------------------------------------------------
|
|
358
|
+
// Rich-component expansion (results → primitive card stack)
|
|
359
|
+
// ---------------------------------------------------------------------------
|
|
360
|
+
function el(tag, props, children, action, nav) {
|
|
361
|
+
return { kind: 'element', tag, props, children, ...(action ? { action } : {}), ...(nav ? { nav } : {}) };
|
|
362
|
+
}
|
|
363
|
+
function txt(value) {
|
|
364
|
+
return { kind: 'text', value };
|
|
365
|
+
}
|
|
366
|
+
/**
|
|
367
|
+
* `results` → a stack of selectable cards (one per `result`). Each card shows
|
|
368
|
+
* title/subtitle/badge and an "Open" affordance: `to` navigates client-side to a
|
|
369
|
+
* named view; else `action` (or `open:<id>` when only `id` is given) drives a turn.
|
|
370
|
+
*/
|
|
371
|
+
function expandResults(node) {
|
|
372
|
+
const cards = node.children
|
|
373
|
+
.filter((c) => c.kind === 'element' && c.tag === 'result')
|
|
374
|
+
.map((r) => {
|
|
375
|
+
const p = r.props;
|
|
376
|
+
const id = p.id != null ? String(p.id) : '';
|
|
377
|
+
const title = String(p.title ?? '');
|
|
378
|
+
const subtitle = p.subtitle != null ? String(p.subtitle) : '';
|
|
379
|
+
const badge = p.badge != null ? String(p.badge) : '';
|
|
380
|
+
const to = typeof p.to === 'string' && p.to ? p.to : undefined;
|
|
381
|
+
const action = typeof p.action === 'string' && p.action ? p.action : id ? `open:${id}` : undefined;
|
|
382
|
+
const left = el('stack', { gap: 'none' }, [
|
|
383
|
+
el('text', { weight: 'bold' }, [txt(title)]),
|
|
384
|
+
...(subtitle ? [el('text', { tone: 'muted' }, [txt(subtitle)])] : []),
|
|
385
|
+
]);
|
|
386
|
+
const right = [];
|
|
387
|
+
if (badge)
|
|
388
|
+
right.push(el('badge', {}, [txt(badge)]));
|
|
389
|
+
if (to)
|
|
390
|
+
right.push(el('button', { to, label: 'Open', variant: 'secondary' }, [], undefined, to));
|
|
391
|
+
else if (action)
|
|
392
|
+
right.push(el('button', { action, label: 'Open', variant: 'primary' }, [], { name: action, fields: [] }));
|
|
393
|
+
return el('card', {}, [
|
|
394
|
+
el('row', { justify: 'between', align: 'center' }, [left, el('row', { gap: 'md', align: 'center' }, right)]),
|
|
395
|
+
]);
|
|
396
|
+
});
|
|
397
|
+
return el('stack', { gap: 'md' }, cards);
|
|
398
|
+
}
|
|
399
|
+
function expand(node) {
|
|
400
|
+
if (node.kind !== 'element')
|
|
401
|
+
return node;
|
|
402
|
+
if (node.tag === 'results')
|
|
403
|
+
return expandResults(node);
|
|
404
|
+
return { ...node, children: node.children.map(expand) };
|
|
405
|
+
}
|
|
406
|
+
// ---------------------------------------------------------------------------
|
|
407
|
+
// Public API
|
|
408
|
+
// ---------------------------------------------------------------------------
|
|
409
|
+
export function parseView(source, allowList) {
|
|
410
|
+
try {
|
|
411
|
+
const tokens = tokenize(source);
|
|
412
|
+
const rootB = build(tokens, source);
|
|
413
|
+
const specByTag = new Map(allowList.map((s) => [s.tag, s]));
|
|
414
|
+
const errors = [];
|
|
415
|
+
let root = convert(rootB, specByTag, errors);
|
|
416
|
+
if (errors.length)
|
|
417
|
+
return { ok: false, errors };
|
|
418
|
+
root = expand(root);
|
|
419
|
+
const title = root.kind === 'element' && root.tag === 'view' && typeof root.props.title === 'string' ? root.props.title : undefined;
|
|
420
|
+
return { ok: true, doc: { root, ...(title ? { title } : {}) } };
|
|
421
|
+
}
|
|
422
|
+
catch (e) {
|
|
423
|
+
if (e instanceof ParseFail)
|
|
424
|
+
return { ok: false, errors: [e.info] };
|
|
425
|
+
return { ok: false, errors: [{ message: e instanceof Error ? e.message : String(e) }] };
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
/** Validate an already-built (expanded) AST against the allow-list. */
|
|
429
|
+
export function validateDoc(doc, allowList) {
|
|
430
|
+
const specByTag = new Map(allowList.map((s) => [s.tag, s]));
|
|
431
|
+
const errors = [];
|
|
432
|
+
const walk = (node) => {
|
|
433
|
+
if (node.kind !== 'element')
|
|
434
|
+
return;
|
|
435
|
+
const spec = specByTag.get(node.tag);
|
|
436
|
+
if (!spec) {
|
|
437
|
+
errors.push({ message: `unknown tag <${node.tag}>` });
|
|
438
|
+
return;
|
|
439
|
+
}
|
|
440
|
+
for (const [key, value] of Object.entries(node.props)) {
|
|
441
|
+
if (!spec.attrs[key]) {
|
|
442
|
+
errors.push({ message: `<${node.tag}> unknown attribute "${key}"` });
|
|
443
|
+
continue;
|
|
444
|
+
}
|
|
445
|
+
// URL-scheme allow-list — an AST handed straight to validateDoc (i.e.
|
|
446
|
+
// one that never went through parseView's coerceValue) must not smuggle
|
|
447
|
+
// a javascript:/data:text href past the renderer.
|
|
448
|
+
if ((key === 'href' || key === 'src') && typeof value === 'string' && !isSafeViewUrl(value, key)) {
|
|
449
|
+
errors.push({ message: `<${node.tag}> attribute "${key}" has a disallowed URL scheme` });
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
for (const [name, aspec] of Object.entries(spec.attrs)) {
|
|
453
|
+
if (aspec.required && !(name in node.props)) {
|
|
454
|
+
errors.push({ message: `<${node.tag}> missing required attribute "${name}"` });
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
for (const c of node.children) {
|
|
458
|
+
if (c.kind === 'element') {
|
|
459
|
+
if (spec.allowedChildren === 'none') {
|
|
460
|
+
errors.push({ message: `<${node.tag}> may not have children` });
|
|
461
|
+
}
|
|
462
|
+
else if (Array.isArray(spec.allowedChildren) && !spec.allowedChildren.includes(c.tag)) {
|
|
463
|
+
errors.push({ message: `<${node.tag}> may not contain <${c.tag}>` });
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
else if (spec.allowedChildren !== 'any' && c.value.trim()) {
|
|
467
|
+
errors.push({ message: `<${node.tag}> may not contain text` });
|
|
468
|
+
}
|
|
469
|
+
walk(c);
|
|
470
|
+
}
|
|
471
|
+
};
|
|
472
|
+
walk(doc.root);
|
|
473
|
+
return errors;
|
|
474
|
+
}
|
|
475
|
+
// Count element + text nodes (for tool result `nodeCount`). The recursion now
|
|
476
|
+
// lives in @moxxy/sdk so core's parser and the plugin-view tool share one
|
|
477
|
+
// implementation; re-exported here to keep core's public export shape.
|
|
478
|
+
export { countNodes };
|
|
479
|
+
//# sourceMappingURL=parse.js.map
|