@alisio/sdk 0.5.2 → 0.7.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/dist/index.d.ts +390 -3
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -1848,6 +1848,8 @@ export interface InflightState {
|
|
|
1848
1848
|
startedAt: number;
|
|
1849
1849
|
/** Latest progress output, bounded. */
|
|
1850
1850
|
tail: string;
|
|
1851
|
+
/** Latest `ForgeProgress` of a `js_run`/`python_run` call; never part of `tail`. */
|
|
1852
|
+
forge?: ForgeProgress;
|
|
1851
1853
|
}>;
|
|
1852
1854
|
}
|
|
1853
1855
|
/** An approval waiting for a decision from a web client. */
|
|
@@ -1871,8 +1873,11 @@ export interface PendingApproval {
|
|
|
1871
1873
|
capability?: AnalysisCapability;
|
|
1872
1874
|
/** A short preview for capability approvals (the first 40 lines of the script). */
|
|
1873
1875
|
preview?: string;
|
|
1874
|
-
/**
|
|
1875
|
-
|
|
1876
|
+
/**
|
|
1877
|
+
* Capability approvals: `managed` (not sandboxed), `oci` (container), `wasm` (Pyodide in a
|
|
1878
|
+
* restricted process) or `auto` (the most isolated backend that is ready, decided at run time).
|
|
1879
|
+
*/
|
|
1880
|
+
runtime?: "auto" | "managed" | "oci" | "wasm";
|
|
1876
1881
|
/** `analysis.install` approvals: only `once` and `deny` are offered. */
|
|
1877
1882
|
install?: InstallPreview;
|
|
1878
1883
|
}
|
|
@@ -2359,7 +2364,12 @@ export interface AnalysisStatus {
|
|
|
2359
2364
|
/** `analysis.enabled` and not `--read-only`: python_run is registered. */
|
|
2360
2365
|
enabled: boolean;
|
|
2361
2366
|
readOnly: boolean;
|
|
2362
|
-
|
|
2367
|
+
/** `analysis.runtime`: `auto` picks the most isolated backend that is ready. */
|
|
2368
|
+
mode: "auto" | "managed" | "oci" | "wasm";
|
|
2369
|
+
/** `analysis.isolation`: what `auto` may fall back to. */
|
|
2370
|
+
isolation?: "preferred" | "required" | "strict";
|
|
2371
|
+
/** The Python backends with their live state (isolation, ready, profiles, reason). */
|
|
2372
|
+
backends?: EngineDescriptor[];
|
|
2363
2373
|
/** The discovered (or `--python`) interpreter and the extras environment. */
|
|
2364
2374
|
python: {
|
|
2365
2375
|
found: true;
|
|
@@ -2649,4 +2659,381 @@ export declare const textResult: (text: string, isError?: boolean) => ToolResult
|
|
|
2649
2659
|
* raw bytes or structured blocks never reach the model prompt.
|
|
2650
2660
|
*/
|
|
2651
2661
|
export declare function textProjection(result: ToolResult): ToolResult;
|
|
2662
|
+
/** The language a Forja engine runs. */
|
|
2663
|
+
export type EngineLanguage = "javascript" | "python";
|
|
2664
|
+
/** Stable identifier of a Forja execution engine. */
|
|
2665
|
+
export type EngineId = "javascript-quickjs" | "python-managed" | "python-oci" | "python-wasm";
|
|
2666
|
+
/** Isolation an engine declares and has verified, ordered from the strongest guarantee to none. */
|
|
2667
|
+
export type IsolationLevel = "os-container" | "wasm-isolate" | "wasm-process" | "none";
|
|
2668
|
+
/** What an engine reports about itself. An engine that is not `ready` never runs code. */
|
|
2669
|
+
export interface EngineDescriptor {
|
|
2670
|
+
id: EngineId;
|
|
2671
|
+
language: EngineLanguage;
|
|
2672
|
+
/** Engine build, e.g. `quickjs-wasi@3.6.2` or `cpython-3.12.4`. */
|
|
2673
|
+
version: string;
|
|
2674
|
+
isolation: IsolationLevel;
|
|
2675
|
+
ready: boolean;
|
|
2676
|
+
/** Actionable reason the engine is not ready. */
|
|
2677
|
+
reason?: string;
|
|
2678
|
+
capabilities: {
|
|
2679
|
+
cooperativeInterrupt: boolean;
|
|
2680
|
+
/** The host can terminate the guest without its cooperation. */
|
|
2681
|
+
hardTermination: boolean;
|
|
2682
|
+
hardMemoryLimit: boolean;
|
|
2683
|
+
networkDeniedByOS: boolean;
|
|
2684
|
+
/** The guest can call core tools. */
|
|
2685
|
+
toolCalls: boolean;
|
|
2686
|
+
};
|
|
2687
|
+
/** Package profiles available (Python). */
|
|
2688
|
+
profiles?: string[];
|
|
2689
|
+
}
|
|
2690
|
+
/**
|
|
2691
|
+
* `GET /api/forge`: the Forja engines with their live state. `engines` lists the JavaScript engine
|
|
2692
|
+
* first and then the Python backends (`python-oci`, `python-wasm`, `python-managed`), each with its
|
|
2693
|
+
* isolation, readiness, reason and capabilities (`networkDeniedByOS` comes from a self-probe).
|
|
2694
|
+
*/
|
|
2695
|
+
export interface ForgeOverview {
|
|
2696
|
+
engines: EngineDescriptor[];
|
|
2697
|
+
javascript: {
|
|
2698
|
+
verified: boolean;
|
|
2699
|
+
verification?: string;
|
|
2700
|
+
enabled: boolean;
|
|
2701
|
+
host: "worker" | "process";
|
|
2702
|
+
hostSetting: "auto" | "worker" | "process";
|
|
2703
|
+
wasmSha256: string;
|
|
2704
|
+
version: string;
|
|
2705
|
+
limits: {
|
|
2706
|
+
maxSourceBytes: number;
|
|
2707
|
+
timeoutMs: number;
|
|
2708
|
+
memoryBytes: number;
|
|
2709
|
+
stackBytes: number;
|
|
2710
|
+
maxToolCalls: number;
|
|
2711
|
+
maxConcurrency: number;
|
|
2712
|
+
maxCallInputBytes: number;
|
|
2713
|
+
maxCallResponseBytes: number;
|
|
2714
|
+
maxRetainedOutputBytes: number;
|
|
2715
|
+
maxOutputChars: number;
|
|
2716
|
+
};
|
|
2717
|
+
};
|
|
2718
|
+
/** The scratchpad settings (`forge.scratch.*`); the folder is a host path and never listed. */
|
|
2719
|
+
scratch: {
|
|
2720
|
+
enabled: boolean;
|
|
2721
|
+
retentionDays: number;
|
|
2722
|
+
maxBytes: number;
|
|
2723
|
+
};
|
|
2724
|
+
state: {
|
|
2725
|
+
enabled: boolean;
|
|
2726
|
+
};
|
|
2727
|
+
guides: {
|
|
2728
|
+
enabled: boolean;
|
|
2729
|
+
};
|
|
2730
|
+
/** Python settings only the user decides, read only: `analysis.runtime` and `analysis.isolation`. */
|
|
2731
|
+
python?: {
|
|
2732
|
+
runtime: "auto" | "managed" | "oci" | "wasm";
|
|
2733
|
+
isolation: "preferred" | "required" | "strict";
|
|
2734
|
+
};
|
|
2735
|
+
}
|
|
2736
|
+
/** One file of a session's scratchpad (`GET /api/sessions/:sid/scratch`); never an absolute path. */
|
|
2737
|
+
export interface ScratchFile {
|
|
2738
|
+
/** Path relative to the scratchpad, `/`-separated; also the tail of the download URL. */
|
|
2739
|
+
name: string;
|
|
2740
|
+
/** Bytes. */
|
|
2741
|
+
size: number;
|
|
2742
|
+
/** Last modification, ms epoch. */
|
|
2743
|
+
mtime: number;
|
|
2744
|
+
}
|
|
2745
|
+
/** Answer of `GET /api/sessions/:sid/scratch`. */
|
|
2746
|
+
export interface ScratchListing {
|
|
2747
|
+
/** False while Forja's JavaScript engine or its scratchpad is off: there is nothing to list. */
|
|
2748
|
+
available: boolean;
|
|
2749
|
+
files: ScratchFile[];
|
|
2750
|
+
/** Bytes the quota counts (`forge.scratch.maxBytes`), symlinks as links. */
|
|
2751
|
+
usedBytes: number;
|
|
2752
|
+
quotaBytes: number;
|
|
2753
|
+
/** The list was cut at 2 000 files; `usedBytes` still counts all of them. */
|
|
2754
|
+
truncated?: boolean;
|
|
2755
|
+
}
|
|
2756
|
+
/** Answer of `DELETE /api/sessions/:sid/scratch`. */
|
|
2757
|
+
export interface ScratchClearResult {
|
|
2758
|
+
/** Files (and links) removed. */
|
|
2759
|
+
removed: number;
|
|
2760
|
+
freedBytes: number;
|
|
2761
|
+
}
|
|
2762
|
+
/** Typed payload of a `tool_progress` event emitted by a Forja execution. */
|
|
2763
|
+
export interface ForgeProgress {
|
|
2764
|
+
kind: "forge";
|
|
2765
|
+
executionId: string;
|
|
2766
|
+
engine: EngineId;
|
|
2767
|
+
phase: "preparing" | "running" | "call" | "collecting";
|
|
2768
|
+
message?: string;
|
|
2769
|
+
completed?: number;
|
|
2770
|
+
total?: number;
|
|
2771
|
+
call?: {
|
|
2772
|
+
callId: number;
|
|
2773
|
+
tool: string;
|
|
2774
|
+
status: "started" | "succeeded" | "failed" | "denied";
|
|
2775
|
+
durationMs?: number;
|
|
2776
|
+
};
|
|
2777
|
+
}
|
|
2778
|
+
/** Hard limits of one reliability-lab task (`specs/010`). A run that exceeds one ends `budget_exceeded`. */
|
|
2779
|
+
export interface EvalBudget {
|
|
2780
|
+
maxTurns: number;
|
|
2781
|
+
maxTokens?: number;
|
|
2782
|
+
maxWallMs: number;
|
|
2783
|
+
maxToolCalls?: number;
|
|
2784
|
+
}
|
|
2785
|
+
/** Assertion over the sequence of tool events of one run. */
|
|
2786
|
+
export type ToolSequenceRule =
|
|
2787
|
+
/** `first` must occur before `then`. */
|
|
2788
|
+
{
|
|
2789
|
+
type: "before";
|
|
2790
|
+
first: string;
|
|
2791
|
+
then: string;
|
|
2792
|
+
} | {
|
|
2793
|
+
type: "never";
|
|
2794
|
+
tool: string;
|
|
2795
|
+
} | {
|
|
2796
|
+
type: "max-calls";
|
|
2797
|
+
tool: string;
|
|
2798
|
+
max: number;
|
|
2799
|
+
} | {
|
|
2800
|
+
type: "no-repeat-failure";
|
|
2801
|
+
tool: string;
|
|
2802
|
+
};
|
|
2803
|
+
/** Deterministic, local check of a task result. `kind` stays open for future adapters. */
|
|
2804
|
+
export type EvalVerifier = {
|
|
2805
|
+
kind: "command";
|
|
2806
|
+
id: string;
|
|
2807
|
+
command: string[];
|
|
2808
|
+
timeoutMs: number;
|
|
2809
|
+
expectExit?: number;
|
|
2810
|
+
expectStdoutIncludes?: string;
|
|
2811
|
+
} | {
|
|
2812
|
+
kind: "file-equals";
|
|
2813
|
+
id: string;
|
|
2814
|
+
path: string;
|
|
2815
|
+
sha256: string;
|
|
2816
|
+
} | {
|
|
2817
|
+
kind: "file-contains";
|
|
2818
|
+
id: string;
|
|
2819
|
+
path: string;
|
|
2820
|
+
text: string;
|
|
2821
|
+
} | {
|
|
2822
|
+
kind: "file-absent";
|
|
2823
|
+
id: string;
|
|
2824
|
+
path: string;
|
|
2825
|
+
} | {
|
|
2826
|
+
kind: "tree-diff-within";
|
|
2827
|
+
id: string;
|
|
2828
|
+
allowed: string[];
|
|
2829
|
+
} | {
|
|
2830
|
+
kind: "tool-sequence";
|
|
2831
|
+
id: string;
|
|
2832
|
+
rule: ToolSequenceRule;
|
|
2833
|
+
};
|
|
2834
|
+
export interface EvalTask {
|
|
2835
|
+
id: string;
|
|
2836
|
+
prompt: string;
|
|
2837
|
+
/** Fixture directory, relative to the suite directory. */
|
|
2838
|
+
fixture: string;
|
|
2839
|
+
agent?: string;
|
|
2840
|
+
budget: EvalBudget;
|
|
2841
|
+
/**
|
|
2842
|
+
* `allow` lists extra effects on top of the mode: `write`, `process`, `external` or `analysis`
|
|
2843
|
+
* (never MCP or agents). A task can only equal or restrict what the invoker already holds.
|
|
2844
|
+
*/
|
|
2845
|
+
policy: {
|
|
2846
|
+
mode: "ask" | "auto" | "full";
|
|
2847
|
+
readOnly?: boolean;
|
|
2848
|
+
allow?: string[];
|
|
2849
|
+
};
|
|
2850
|
+
verifiers: EvalVerifier[];
|
|
2851
|
+
tags?: string[];
|
|
2852
|
+
/** Initialize a git repository (identity set, no global config) in the run copy of the fixture. */
|
|
2853
|
+
fixtureGit?: boolean;
|
|
2854
|
+
/**
|
|
2855
|
+
* Replay-only noise filter, versioned with the suite: each `pattern` (a JavaScript regular
|
|
2856
|
+
* expression, applied globally) found in a string of a request is replaced by `replace` BEFORE
|
|
2857
|
+
* the request is fingerprinted, after the lab's own path and id normalization. Use it for
|
|
2858
|
+
* non-deterministic fixture output (a timestamp a command prints). Over-broad rules hide real
|
|
2859
|
+
* regressions; keep them narrow.
|
|
2860
|
+
*/
|
|
2861
|
+
replayNormalize?: Array<{
|
|
2862
|
+
pattern: string;
|
|
2863
|
+
replace: string;
|
|
2864
|
+
}>;
|
|
2865
|
+
}
|
|
2866
|
+
/** Content of a versioned `suite.json`. */
|
|
2867
|
+
export interface EvalSuite {
|
|
2868
|
+
schemaVersion: 1;
|
|
2869
|
+
suiteId: string;
|
|
2870
|
+
suiteVersion: string;
|
|
2871
|
+
description: string;
|
|
2872
|
+
tasks: EvalTask[];
|
|
2873
|
+
}
|
|
2874
|
+
/** One repetition of one task, with the conditions it was measured under. */
|
|
2875
|
+
export interface EvalRunRecord {
|
|
2876
|
+
schemaVersion: 1;
|
|
2877
|
+
runId: string;
|
|
2878
|
+
suiteId: string;
|
|
2879
|
+
suiteVersion: string;
|
|
2880
|
+
taskId: string;
|
|
2881
|
+
repetition: number;
|
|
2882
|
+
seed: number;
|
|
2883
|
+
harnessVersion: string;
|
|
2884
|
+
configHash: string;
|
|
2885
|
+
provider: string;
|
|
2886
|
+
model: string;
|
|
2887
|
+
mode: "live" | "replay";
|
|
2888
|
+
startedAt: number;
|
|
2889
|
+
endedAt: number;
|
|
2890
|
+
status: "pass" | "fail" | "budget_exceeded" | "error";
|
|
2891
|
+
/** Which budget ended the run; present only when `status` is `budget_exceeded`. */
|
|
2892
|
+
budgetExceeded?: "maxTurns" | "maxTokens" | "maxWallMs" | "maxToolCalls";
|
|
2893
|
+
/**
|
|
2894
|
+
* Why the run ended `error` (infrastructure or refusal), or the detail of a failure the model
|
|
2895
|
+
* caused (`fail` with a `run_aborted` incident). Never contains file contents.
|
|
2896
|
+
*/
|
|
2897
|
+
error?: string;
|
|
2898
|
+
/**
|
|
2899
|
+
* `sha256:` of the compaction-related configuration the run was measured under. Compaction
|
|
2900
|
+
* calls are not in `measured` tokens, so two runs with different compaction settings are not
|
|
2901
|
+
* comparable on tokens or cost.
|
|
2902
|
+
*/
|
|
2903
|
+
compactionHash?: string;
|
|
2904
|
+
verifiers: Array<{
|
|
2905
|
+
id: string;
|
|
2906
|
+
status: "pass" | "fail" | "error";
|
|
2907
|
+
detail?: string;
|
|
2908
|
+
}>;
|
|
2909
|
+
/** Provider-reported usage, wall time and event counts. Never mixed with `estimated`. */
|
|
2910
|
+
measured: {
|
|
2911
|
+
inputTokens?: number;
|
|
2912
|
+
outputTokens?: number;
|
|
2913
|
+
/**
|
|
2914
|
+
* True when only SOME turns reported usage: the token totals above understate the run and are
|
|
2915
|
+
* treated as not reported by every aggregation.
|
|
2916
|
+
*/
|
|
2917
|
+
usagePartial?: true;
|
|
2918
|
+
wallMs: number;
|
|
2919
|
+
turns: number;
|
|
2920
|
+
toolCalls: number;
|
|
2921
|
+
};
|
|
2922
|
+
/** Derived values (cost from configured rates, approximate tokens). */
|
|
2923
|
+
estimated?: {
|
|
2924
|
+
costUsd?: number;
|
|
2925
|
+
approxTokens?: number;
|
|
2926
|
+
};
|
|
2927
|
+
incidents: Array<{
|
|
2928
|
+
kind: string;
|
|
2929
|
+
count: number;
|
|
2930
|
+
}>;
|
|
2931
|
+
/**
|
|
2932
|
+
* Present on a replay run that ended `error` with `error: "replay_divergence"`: the harness asked
|
|
2933
|
+
* the provider for something the cassette did not record.
|
|
2934
|
+
*/
|
|
2935
|
+
divergence?: EvalReplayDivergence;
|
|
2936
|
+
}
|
|
2937
|
+
/** One field of a request that differs from the recorded one (excerpts are redacted, at most ~240 characters). */
|
|
2938
|
+
export interface EvalDivergenceEntry {
|
|
2939
|
+
call: number;
|
|
2940
|
+
/** For example `instructions`, `messages[2].tool.result`, `tools[read_file].description`, `model`. */
|
|
2941
|
+
field: string;
|
|
2942
|
+
excerpt: {
|
|
2943
|
+
before: string;
|
|
2944
|
+
after: string;
|
|
2945
|
+
};
|
|
2946
|
+
}
|
|
2947
|
+
/** Why a replay stopped: a changed request, a call past the recording, or recorded calls never asked for. */
|
|
2948
|
+
export interface EvalReplayDivergence {
|
|
2949
|
+
kind: "fingerprint" | "exhausted" | "unconsumed";
|
|
2950
|
+
message: string;
|
|
2951
|
+
/** Empty unless `kind` is `fingerprint`; at most 10 entries. */
|
|
2952
|
+
diff: EvalDivergenceEntry[];
|
|
2953
|
+
}
|
|
2954
|
+
/**
|
|
2955
|
+
* Rates the caller supplies to turn measured tokens into an estimated cost (USD per million
|
|
2956
|
+
* tokens). Alisio never ships or guesses rates: without them no cost is computed.
|
|
2957
|
+
*/
|
|
2958
|
+
export interface EvalRates {
|
|
2959
|
+
inputPerMTokUsd: number;
|
|
2960
|
+
outputPerMTokUsd: number;
|
|
2961
|
+
}
|
|
2962
|
+
/**
|
|
2963
|
+
* Reliability metrics of one task over its repetitions. Runs that ended `error` (infrastructure)
|
|
2964
|
+
* are not repetitions of the task: they are excluded from every number here and counted in
|
|
2965
|
+
* `errored`. `k` is the number of judged runs (pass, fail or budget_exceeded).
|
|
2966
|
+
*/
|
|
2967
|
+
export interface EvalTaskMetrics {
|
|
2968
|
+
taskId: string;
|
|
2969
|
+
k: number;
|
|
2970
|
+
/** Judged runs only. With `k: 0` the value is 0 and the interval [0, 1] carries no information. */
|
|
2971
|
+
successRate: {
|
|
2972
|
+
value: number;
|
|
2973
|
+
wilsonLow: number;
|
|
2974
|
+
wilsonHigh: number;
|
|
2975
|
+
};
|
|
2976
|
+
passAtK: boolean;
|
|
2977
|
+
passPowK: boolean;
|
|
2978
|
+
/** Runs of this task that ended `error` and were excluded from the numbers (omitted when 0). */
|
|
2979
|
+
errored?: number;
|
|
2980
|
+
/** `configHash` the task was measured under (a hash covers the task's own policy and budget). */
|
|
2981
|
+
configHash?: string;
|
|
2982
|
+
/**
|
|
2983
|
+
* Provider-reported usage and wall time only. A value that cannot be computed honestly is
|
|
2984
|
+
* omitted, never invented: the median needs one judged run, a variance two samples, the
|
|
2985
|
+
* per-success figures at least one success (and usage on every judged run for tokens).
|
|
2986
|
+
*/
|
|
2987
|
+
measured: {
|
|
2988
|
+
medianWallMs?: number;
|
|
2989
|
+
varianceWallMs?: number;
|
|
2990
|
+
varianceTokens?: number;
|
|
2991
|
+
tokensPerSuccess?: number;
|
|
2992
|
+
wallMsPerSuccess?: number;
|
|
2993
|
+
};
|
|
2994
|
+
/** Present only when the caller supplied rates and every judged run reported usage. */
|
|
2995
|
+
estimated?: {
|
|
2996
|
+
costUsdPerSuccess?: number;
|
|
2997
|
+
};
|
|
2998
|
+
}
|
|
2999
|
+
export interface EvalReport {
|
|
3000
|
+
schemaVersion: 1;
|
|
3001
|
+
suiteId: string;
|
|
3002
|
+
suiteVersion: string;
|
|
3003
|
+
conditions: {
|
|
3004
|
+
harnessVersion: string;
|
|
3005
|
+
configHash: string;
|
|
3006
|
+
provider: string;
|
|
3007
|
+
model: string;
|
|
3008
|
+
mode: "live" | "replay";
|
|
3009
|
+
k: number;
|
|
3010
|
+
seedBase: number;
|
|
3011
|
+
/** Compaction identity shared by every run (omitted for reports of older runs). */
|
|
3012
|
+
compactionHash?: string;
|
|
3013
|
+
};
|
|
3014
|
+
/** The variant this report was measured as (`eval variants`): its id and config overrides. */
|
|
3015
|
+
variant?: {
|
|
3016
|
+
id: string;
|
|
3017
|
+
overrides: Record<string, unknown>;
|
|
3018
|
+
};
|
|
3019
|
+
/** Caveats about what the numbers mean (for example which tokens are counted). */
|
|
3020
|
+
notes?: string[];
|
|
3021
|
+
tasks: EvalTaskMetrics[];
|
|
3022
|
+
/** IDs of the `EvalRunRecord`s, errored ones included (the detail lives in `runs.jsonl`). */
|
|
3023
|
+
runs: string[];
|
|
3024
|
+
/**
|
|
3025
|
+
* Counts of RUNS, except `tasks`: `passed`, `failed`, `errored` and `budgetExceeded` add up to
|
|
3026
|
+
* `runs`. `errored` runs are infrastructure and are excluded from every rate and spread.
|
|
3027
|
+
*/
|
|
3028
|
+
summary: {
|
|
3029
|
+
tasks: number;
|
|
3030
|
+
passed: number;
|
|
3031
|
+
errored: number;
|
|
3032
|
+
budgetExceeded: number;
|
|
3033
|
+
runs?: number;
|
|
3034
|
+
failed?: number;
|
|
3035
|
+
};
|
|
3036
|
+
/** The rates behind every `estimated` figure; absent when none was supplied. */
|
|
3037
|
+
rates?: EvalRates;
|
|
3038
|
+
}
|
|
2652
3039
|
export {};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@alisio/sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "Typed plugin SDK for Alisio: the stable contract for tools, commands, context, compaction and session hooks, model completions and storage. Types only plus tiny helpers; zero runtime dependencies.",
|
|
5
5
|
"author": "Gustavo Gutiérrez",
|
|
6
6
|
"license": "MIT",
|