@kybernesis/create 0.17.0 → 0.17.2
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/cli.js +3 -0
- package/dist/doctor.d.ts +17 -0
- package/dist/doctor.js +70 -0
- package/dist/init.d.ts +6 -0
- package/dist/init.js +22 -2
- package/dist/templates.d.ts +18 -0
- package/dist/templates.js +100 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -64,6 +64,7 @@ function initOptions(rest) {
|
|
|
64
64
|
const subs = flag(rest, 'subagents');
|
|
65
65
|
return {
|
|
66
66
|
engineer: rest.includes('--engineer'),
|
|
67
|
+
computer: rest.includes('--no-computer') ? false : rest.includes('--computer') ? true : undefined,
|
|
67
68
|
studio: rest.includes('--studio'),
|
|
68
69
|
channel: flag(rest, "channel"),
|
|
69
70
|
host: flag(rest, "host"),
|
|
@@ -104,6 +105,7 @@ function usage(command) {
|
|
|
104
105
|
--model-reach=<reach> ${dim("claude-sub (exe only; default: host route)")}
|
|
105
106
|
--subagents=a,b ${dim("department subagents")}
|
|
106
107
|
--engineer ${dim("add the engineer layer")}
|
|
108
|
+
--no-computer ${dim("skip the agent's own desktop + Chrome (on by default on exe)")}
|
|
107
109
|
--studio ${dim("wire KYBER Studio local/manage routes")}
|
|
108
110
|
--yes, -y ${dim("no prompts")}
|
|
109
111
|
|
|
@@ -228,6 +230,7 @@ ${dim(" npm i -g @kybernesis/create@latest")}
|
|
|
228
230
|
--model=<id> ${dim("provider/model-id; bare claude-opus-5 for claude-sub")}
|
|
229
231
|
--model-reach=<r> ${dim("claude-sub (exe only); env: KYB_MODEL_REACH")}
|
|
230
232
|
--engineer ${dim("add the engineer layer: workshop sandbox + vision dev loop")}
|
|
233
|
+
--no-computer ${dim("skip the agent's own computer (desktop + Chrome you can watch; on by default on exe hosts)")}
|
|
231
234
|
--studio ${dim("wire for KYBER Studio: local execution + management routes")}
|
|
232
235
|
--yes ${dim("no prompts; take flags and defaults")}
|
|
233
236
|
${bold("kyb add channel <kind>")}
|
package/dist/doctor.d.ts
CHANGED
|
@@ -32,4 +32,21 @@ export declare function diskUsageDoctorCheck(percent: number, dockerSummary: str
|
|
|
32
32
|
*/
|
|
33
33
|
export declare function inspectDockerTemplatesViaExe(cwd: string): Promise<DockerTemplateInspection | null>;
|
|
34
34
|
export declare function doctor(): Promise<void>;
|
|
35
|
+
export interface ComputerInspection {
|
|
36
|
+
/** agent/sandbox.ts imports @kybernesis/computer. */
|
|
37
|
+
configured: boolean;
|
|
38
|
+
/** COMPUTER_NAME from .env.local, or the default the sandbox file falls back to. */
|
|
39
|
+
name: string;
|
|
40
|
+
containerState: "running" | "stopped" | "missing" | "unknown";
|
|
41
|
+
/** The noVNC port is bound on loopback. */
|
|
42
|
+
screenBound: boolean | null;
|
|
43
|
+
vncPasswordSet: boolean;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* A computer that is configured but not running is the quiet failure here:
|
|
47
|
+
* every session would try to start it and the first turn pays the whole boot,
|
|
48
|
+
* or fails if Docker is off. Missing before the first build is normal.
|
|
49
|
+
*/
|
|
50
|
+
export declare function computerDoctorChecks(c: ComputerInspection): Check[];
|
|
51
|
+
export declare function inspectComputer(cwd: string): ComputerInspection | null;
|
|
35
52
|
export {};
|
package/dist/doctor.js
CHANGED
|
@@ -508,6 +508,10 @@ export async function doctor() {
|
|
|
508
508
|
]);
|
|
509
509
|
const diskCheck = diskUsageDoctorCheck(percent, dockerSummary);
|
|
510
510
|
add(diskCheck.verdict, diskCheck.label, diskCheck.detail);
|
|
511
|
+
const computer = inspectComputer(cwd);
|
|
512
|
+
if (computer)
|
|
513
|
+
for (const check of computerDoctorChecks(computer))
|
|
514
|
+
checks.push(check);
|
|
511
515
|
}
|
|
512
516
|
/**
|
|
513
517
|
* Instructions that name a tool the agent does not have.
|
|
@@ -679,3 +683,69 @@ export async function doctor() {
|
|
|
679
683
|
console.log(`\n ${fails ? red(`${fails} failing`) : green("0 failing")}, ${warns ? yellow(`${warns} warnings`) : "0 warnings"}\n`);
|
|
680
684
|
process.exit(fails ? 1 : 0);
|
|
681
685
|
}
|
|
686
|
+
/**
|
|
687
|
+
* A computer that is configured but not running is the quiet failure here:
|
|
688
|
+
* every session would try to start it and the first turn pays the whole boot,
|
|
689
|
+
* or fails if Docker is off. Missing before the first build is normal.
|
|
690
|
+
*/
|
|
691
|
+
export function computerDoctorChecks(c) {
|
|
692
|
+
if (!c.configured)
|
|
693
|
+
return [];
|
|
694
|
+
const checks = [];
|
|
695
|
+
switch (c.containerState) {
|
|
696
|
+
case "running":
|
|
697
|
+
checks.push({ verdict: "pass", label: `the agent's computer "${c.name}" is running` });
|
|
698
|
+
break;
|
|
699
|
+
case "stopped":
|
|
700
|
+
checks.push({
|
|
701
|
+
verdict: "warn",
|
|
702
|
+
label: `the agent's computer "${c.name}" exists but is stopped`,
|
|
703
|
+
detail: "the next session restarts it and pays the boot; `docker start " + c.name + "` brings it back now",
|
|
704
|
+
});
|
|
705
|
+
break;
|
|
706
|
+
case "missing":
|
|
707
|
+
checks.push({
|
|
708
|
+
verdict: "warn",
|
|
709
|
+
label: `the agent's computer "${c.name}" has not been built yet`,
|
|
710
|
+
detail: "`eve build` creates and prepares it (eve's desktop stack + Chrome, ~5 minutes)",
|
|
711
|
+
});
|
|
712
|
+
break;
|
|
713
|
+
default:
|
|
714
|
+
checks.push({ verdict: "warn", label: "could not inspect the agent's computer", detail: "is Docker running on this host?" });
|
|
715
|
+
}
|
|
716
|
+
if (c.containerState === "running" && c.screenBound === false) {
|
|
717
|
+
checks.push({
|
|
718
|
+
verdict: "fail",
|
|
719
|
+
label: "the computer is running but its screen is not published on :6080",
|
|
720
|
+
detail: "the container was created without the noVNC port mapping; `docker rm -f " + c.name + "` and `eve build` to recreate it (volumes keep the home and /workspace)",
|
|
721
|
+
});
|
|
722
|
+
}
|
|
723
|
+
if (!c.vncPasswordSet) {
|
|
724
|
+
checks.push({
|
|
725
|
+
verdict: "warn",
|
|
726
|
+
label: "the computer's screen has no VNC password",
|
|
727
|
+
detail: "exe's port-proxy sign-in is the only gate; set COMPUTER_VNC_PASSWORD in .env.local and `docker rm -f " + c.name + "` + `eve build` to apply",
|
|
728
|
+
});
|
|
729
|
+
}
|
|
730
|
+
return checks;
|
|
731
|
+
}
|
|
732
|
+
export function inspectComputer(cwd) {
|
|
733
|
+
const sandboxPath = join(cwd, "agent/sandbox.ts");
|
|
734
|
+
if (!existsSync(sandboxPath) || !readFileSync(sandboxPath, "utf8").includes("@kybernesis/computer"))
|
|
735
|
+
return null;
|
|
736
|
+
const envPath = join(cwd, ".env.local");
|
|
737
|
+
const env = existsSync(envPath) ? parseEnv(readFileSync(envPath, "utf8")) : {};
|
|
738
|
+
const sandboxSource = readFileSync(sandboxPath, "utf8");
|
|
739
|
+
const fallback = /COMPUTER_NAME \?\? "([^"]+)"/.exec(sandboxSource)?.[1] ?? "agent-computer";
|
|
740
|
+
const name = env.COMPUTER_NAME?.trim() || fallback;
|
|
741
|
+
const state = capture("docker", ["inspect", "-f", "{{.State.Running}}", name]);
|
|
742
|
+
const containerState = state === null ? "unknown" : state.trim() === "true" ? "running" : state.trim() === "false" ? "stopped" : "missing";
|
|
743
|
+
const ports = containerState === "running" ? capture("docker", ["port", name, "6080"]) : null;
|
|
744
|
+
return {
|
|
745
|
+
configured: true,
|
|
746
|
+
name,
|
|
747
|
+
containerState,
|
|
748
|
+
screenBound: containerState === "running" ? Boolean(ports && ports.trim().length > 0) : null,
|
|
749
|
+
vncPasswordSet: Boolean(env.COMPUTER_VNC_PASSWORD?.trim()),
|
|
750
|
+
};
|
|
751
|
+
}
|
package/dist/init.d.ts
CHANGED
|
@@ -6,6 +6,12 @@ import { type ChannelKind, type HostKind } from "./templates.js";
|
|
|
6
6
|
export declare function finalizeGithubToolsRegistryMount(cwd: string, registryAddSucceeded: boolean): boolean;
|
|
7
7
|
export interface InitOptions {
|
|
8
8
|
engineer?: boolean;
|
|
9
|
+
/**
|
|
10
|
+
* The agent's own computer: a persistent Docker desktop with Chrome a person
|
|
11
|
+
* can watch and take over. Part of the core install on exe hosts (a Vercel
|
|
12
|
+
* function cannot keep a desktop alive); `false` opts out.
|
|
13
|
+
*/
|
|
14
|
+
computer?: boolean;
|
|
9
15
|
/**
|
|
10
16
|
* Wire this agent for KYBER Studio: local execution on the user's own machine,
|
|
11
17
|
* and management routes so Studio can install capabilities and write routines.
|
package/dist/init.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { chmodSync, copyFileSync, cpSync, existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
|
|
1
|
+
import { appendFileSync, chmodSync, copyFileSync, cpSync, existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { join, resolve } from "node:path";
|
|
3
3
|
import { DEFAULT_ISSUER, EVE_VERSION, REGISTRY_URL, ask, bold, closePrompts, dim, green, run, slug, red, yellow, } from "./util.js";
|
|
4
|
-
import { CHANNEL_KINDS, channelPlan, engineerPlan, envExample, evalFileTs, exeEvalConfigTs, evalScript, hostAgentTs, hostSteps, githubToolsMountTs, identityMd, rootArcanaTs, subagentAgentTs, subagentArcanaTs, subagentInstructionsMd, } from "./templates.js";
|
|
4
|
+
import { CHANNEL_KINDS, channelPlan, computerPlan, engineerPlan, envExample, evalFileTs, exeEvalConfigTs, evalScript, hostAgentTs, hostSteps, githubToolsMountTs, identityMd, rootArcanaTs, subagentAgentTs, subagentArcanaTs, subagentInstructionsMd, } from "./templates.js";
|
|
5
5
|
import { suiteDir } from "./skills.js";
|
|
6
6
|
import { configureArcana } from "./arcana.js";
|
|
7
7
|
import { upsertEnv } from "./envfile.js";
|
|
@@ -36,6 +36,7 @@ export function finalizeGithubToolsRegistryMount(cwd, registryAddSucceeded) {
|
|
|
36
36
|
}
|
|
37
37
|
export async function init(rawName, options = {}) {
|
|
38
38
|
const engineer = options.engineer === true;
|
|
39
|
+
const computer = options.computer !== false;
|
|
39
40
|
const studio = options.studio === true;
|
|
40
41
|
const nonInteractive = options.yes === true;
|
|
41
42
|
const name = slug(rawName ?? (await ask("Agent name (kebab-case)?", "acme-agent")));
|
|
@@ -187,6 +188,14 @@ export async function init(rawName, options = {}) {
|
|
|
187
188
|
console.log(yellow(` ! ${item} did not install cleanly — re-run: npx eve add ${item}`));
|
|
188
189
|
}
|
|
189
190
|
}
|
|
191
|
+
const compPlan = computer && host === "exe" ? computerPlan(name) : null;
|
|
192
|
+
if (options.computer === true && host !== "exe") {
|
|
193
|
+
console.log(yellow("\n ! --computer needs a Docker host (exe.dev); a Vercel function cannot keep a desktop alive. Skipped."));
|
|
194
|
+
}
|
|
195
|
+
if (compPlan) {
|
|
196
|
+
console.log(bold("\n2e The agent's computer: persistent desktop + Chrome, sighted computer tool, browser guard …"));
|
|
197
|
+
run("npm", ["install", ...compPlan.deps, "--no-audit", "--no-fund"], { cwd: dir, allowFail: true });
|
|
198
|
+
}
|
|
190
199
|
console.log(bold("\n2d Seeding the FDE Claude Code skill suite (.claude/skills) …"));
|
|
191
200
|
try {
|
|
192
201
|
cpSync(suiteDir(), join(dir, ".claude/skills"), { recursive: true });
|
|
@@ -373,8 +382,17 @@ export async function init(rawName, options = {}) {
|
|
|
373
382
|
writeFileSync(full, file.content);
|
|
374
383
|
}
|
|
375
384
|
}
|
|
385
|
+
if (compPlan) {
|
|
386
|
+
for (const file of compPlan.files) {
|
|
387
|
+
const full = join(dir, file.path);
|
|
388
|
+
mkdirSync(join(full, ".."), { recursive: true });
|
|
389
|
+
writeFileSync(full, file.content);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
376
392
|
console.log(bold("\n5/6 Env template + hermetic eval script …"));
|
|
377
393
|
writeFileSync(join(dir, ".env.example"), envExample(name, depts, issuer, plan.env, host, model));
|
|
394
|
+
if (compPlan)
|
|
395
|
+
appendFileSync(join(dir, ".env.example"), compPlan.env);
|
|
378
396
|
const pkgPath = join(dir, "package.json");
|
|
379
397
|
const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
|
|
380
398
|
pkg.scripts = { ...pkg.scripts, eval: evalScript(name, depts) };
|
|
@@ -394,6 +412,7 @@ export async function init(rawName, options = {}) {
|
|
|
394
412
|
model.reach === "claude-sub" ? "Claude subscription reach" : null,
|
|
395
413
|
studio ? "KYBER Studio (local execution + management routes)" : null,
|
|
396
414
|
engineer ? "engineer subagent (workshop + vision loop)" : null,
|
|
415
|
+
compPlan ? "its own computer (desktop + Chrome, watch and take over)" : null,
|
|
397
416
|
depts.length ? `${depts.length} dept subagent(s)` : null,
|
|
398
417
|
].filter(Boolean);
|
|
399
418
|
const steps = [
|
|
@@ -401,6 +420,7 @@ export async function init(rawName, options = {}) {
|
|
|
401
420
|
...hostSteps(host, name, model),
|
|
402
421
|
...plan.steps,
|
|
403
422
|
...(engPlan?.steps ?? []),
|
|
423
|
+
...(compPlan?.steps ?? []),
|
|
404
424
|
`Control plane: register agent "${name}" at ${issuer}/agents + grant the pilot cohort`,
|
|
405
425
|
`npm run eval → green → deploy → live smoke + the revoke demo`,
|
|
406
426
|
];
|
package/dist/templates.d.ts
CHANGED
|
@@ -96,3 +96,21 @@ export declare function notifyMountTs(): string;
|
|
|
96
96
|
export declare function reasoningSandboxLibTs(): string;
|
|
97
97
|
/** What each reasoning-only subagent's `sandbox.ts` re-exports. */
|
|
98
98
|
export declare function reasoningSandboxReexportTs(): string;
|
|
99
|
+
export interface ComputerPlan {
|
|
100
|
+
files: Array<{
|
|
101
|
+
path: string;
|
|
102
|
+
content: string;
|
|
103
|
+
}>;
|
|
104
|
+
deps: string[];
|
|
105
|
+
steps: string[];
|
|
106
|
+
/** Lines appended to .env.example. */
|
|
107
|
+
env: string;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* A persistent Docker desktop with Chrome that the person can watch and take
|
|
111
|
+
* over, as the agent's ROOT sandbox, plus the sighted computer tool, the
|
|
112
|
+
* guarded open_browser, and the surface ladder in instructions. The engineer
|
|
113
|
+
* workshop (a subagent sandbox) is untouched. Needs Docker, so it is an exe
|
|
114
|
+
* host feature: a Vercel function has no daemon to keep a container alive.
|
|
115
|
+
*/
|
|
116
|
+
export declare function computerPlan(name: string): ComputerPlan;
|
package/dist/templates.js
CHANGED
|
@@ -765,6 +765,105 @@ export default defineSandbox(() => environment.open());
|
|
|
765
765
|
export function reasoningSandboxReexportTs() {
|
|
766
766
|
return `// Reasoning-only specialist: no real filesystem, so no Docker container.
|
|
767
767
|
// See agent/lib/reasoning-sandbox for why this exists and what it replaces.
|
|
768
|
-
export { default } from "../../lib/reasoning-sandbox";
|
|
768
|
+
export { default, environment } from "../../lib/reasoning-sandbox";
|
|
769
769
|
`;
|
|
770
770
|
}
|
|
771
|
+
/**
|
|
772
|
+
* A persistent Docker desktop with Chrome that the person can watch and take
|
|
773
|
+
* over, as the agent's ROOT sandbox, plus the sighted computer tool, the
|
|
774
|
+
* guarded open_browser, and the surface ladder in instructions. The engineer
|
|
775
|
+
* workshop (a subagent sandbox) is untouched. Needs Docker, so it is an exe
|
|
776
|
+
* host feature: a Vercel function has no daemon to keep a container alive.
|
|
777
|
+
*/
|
|
778
|
+
export function computerPlan(name) {
|
|
779
|
+
const files = [
|
|
780
|
+
{
|
|
781
|
+
path: "agent/sandbox.ts",
|
|
782
|
+
content: `import { defineSandbox } from "eve/sandbox";
|
|
783
|
+
import { DockerComputer, prepareComputer, startComputer } from "@kybernesis/computer";
|
|
784
|
+
|
|
785
|
+
// The agent's own computer: ONE persistent container (home and /workspace are
|
|
786
|
+
// volumes, so Chrome's logins survive sessions) with its screen shared over
|
|
787
|
+
// VNC/noVNC on this host's loopback :6080 — reachable only through exe.dev's
|
|
788
|
+
// signed-in port proxy or an SSH tunnel. A person watching can take over.
|
|
789
|
+
// This is the ROOT sandbox; the builder subagent keeps its own workshop.
|
|
790
|
+
export const environment = DockerComputer.environment({
|
|
791
|
+
name: process.env.COMPUTER_NAME ?? "${name}-computer",
|
|
792
|
+
vncPassword: process.env.COMPUTER_VNC_PASSWORD,
|
|
793
|
+
prepare: prepareComputer, // eve's desktop stack + Chrome + the launcher bar, once at \`eve build\`
|
|
794
|
+
});
|
|
795
|
+
|
|
796
|
+
export default defineSandbox(async () => {
|
|
797
|
+
const sandbox = await environment.open();
|
|
798
|
+
await startComputer(sandbox);
|
|
799
|
+
return sandbox;
|
|
800
|
+
});
|
|
801
|
+
`,
|
|
802
|
+
},
|
|
803
|
+
{
|
|
804
|
+
path: "agent/tools/computer.ts",
|
|
805
|
+
content: `import { computerTool } from "@kybernesis/computer";
|
|
806
|
+
|
|
807
|
+
// eve's computer_use with eyes: every action returns the screenshot as an
|
|
808
|
+
// image the model can see, not just a path.
|
|
809
|
+
export default computerTool();
|
|
810
|
+
`,
|
|
811
|
+
},
|
|
812
|
+
{
|
|
813
|
+
path: "agent/tools/open_browser.ts",
|
|
814
|
+
content: `import { openBrowserTool } from "@kybernesis/computer";
|
|
815
|
+
|
|
816
|
+
// Open a site in the computer's Chrome. Guarded: a site that has a connector
|
|
817
|
+
// parks for the person's approval unless the call states a legitimate
|
|
818
|
+
// override — connector first, browser only when there isn't one.
|
|
819
|
+
export default openBrowserTool();
|
|
820
|
+
`,
|
|
821
|
+
},
|
|
822
|
+
{
|
|
823
|
+
path: "agent/tools/close_tabs.ts",
|
|
824
|
+
content: `import { closeTabsTool } from "@kybernesis/computer";
|
|
825
|
+
|
|
826
|
+
// The computer shares memory with the agent host; stale tabs are the spender.
|
|
827
|
+
export default closeTabsTool();
|
|
828
|
+
`,
|
|
829
|
+
},
|
|
830
|
+
{
|
|
831
|
+
path: "agent/instructions/computer.ts",
|
|
832
|
+
content: `import { defineInstructions } from "eve/instructions";
|
|
833
|
+
import { COMPUTER_INSTRUCTIONS } from "@kybernesis/computer";
|
|
834
|
+
|
|
835
|
+
// The "which surface" rule: connected app, then a tool, then the agent's
|
|
836
|
+
// browser, then the person's machine. One source for every agent.
|
|
837
|
+
export default defineInstructions({ content: COMPUTER_INSTRUCTIONS });
|
|
838
|
+
`,
|
|
839
|
+
},
|
|
840
|
+
{
|
|
841
|
+
path: "evals/computer.eval.ts",
|
|
842
|
+
content: `import { computerSuite } from "@kybernesis/evals";
|
|
843
|
+
|
|
844
|
+
// Screen reading, file and cookie persistence across sessions, and the guard:
|
|
845
|
+
// a Gmail question is not answered by browsing mail.google.com. Add
|
|
846
|
+
// \`connected\` / \`disconnected\` once this agent has connectors to prove the
|
|
847
|
+
// order on — see the computerSuite docs.
|
|
848
|
+
export default computerSuite();
|
|
849
|
+
`,
|
|
850
|
+
},
|
|
851
|
+
];
|
|
852
|
+
return {
|
|
853
|
+
files,
|
|
854
|
+
deps: ["@kybernesis/computer"],
|
|
855
|
+
steps: [
|
|
856
|
+
"Enable Docker on the host (some images ship it disabled): sudo systemctl enable --now docker",
|
|
857
|
+
"Set COMPUTER_VNC_PASSWORD in .env.local (the second gate on the shared screen; exe's port-proxy sign-in is the first)",
|
|
858
|
+
"Watch the screen: https://<vm>.exe.xyz:6080/vnc.html (account-gated), or ssh -N -L 6080:localhost:6080 <vm> and open http://localhost:6080/vnc.html",
|
|
859
|
+
"First `eve build` prepares the computer (eve's desktop stack + Chrome): allow ~5 minutes and ~2 GB of memory for the container",
|
|
860
|
+
],
|
|
861
|
+
env: `
|
|
862
|
+
# The agent's own computer (@kybernesis/computer): a persistent Docker desktop
|
|
863
|
+
# with Chrome whose screen is shared on this host's loopback :6080 (noVNC),
|
|
864
|
+
# behind exe.dev's port-proxy sign-in. The VNC password is the second gate.
|
|
865
|
+
COMPUTER_NAME="${name}-computer"
|
|
866
|
+
COMPUTER_VNC_PASSWORD="..."
|
|
867
|
+
`,
|
|
868
|
+
};
|
|
869
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kybernesis/create",
|
|
3
|
-
"version": "0.17.
|
|
3
|
+
"version": "0.17.2",
|
|
4
4
|
"description": "The Kybernesis agent scaffolder and FDE toolkit: one command to a governed, remembering, multiplayer, self-testing eve agent \u2014 plus doctor and upgrade.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|