create-evda 0.1.6 → 0.1.8

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/CHANGELOG.md CHANGED
@@ -4,6 +4,29 @@ Plain-English notes on how Iris behaves differently after each version. Not a co
4
4
 
5
5
  ## Unreleased
6
6
 
7
+ Your phone can join your EVÐA. `evda pair` on the computer your record is on shows a one-time code
8
+ and a QR link; it works once, for 10 minutes, and the phone gets only the parts you name (the
9
+ journal by default). `evda sync` adds the phone once it has used the code, takes in what it wrote,
10
+ and sends what changed; a note written on the phone lands under the day, in your words. `evda
11
+ devices` lists what is paired and `evda remove <device>` removes one: it adds nothing more and
12
+ receives nothing new. You can also ask your AI to pair a phone (`evda_pair_device`). Pairing goes
13
+ through a relay that cannot read what it carries, and needs Node 22.13 or newer; everything else
14
+ still runs on Node 20.
15
+
16
+ Organizing is on from the start. Setup installs the morning organize on a Mac (a launchd job that
17
+ runs your own Claude each morning at 8:00, or when the computer wakes), says so in one line, and
18
+ offers off: answer no during setup, pass `--no-routines`, or later run `evda routine morning
19
+ --uninstall`. The weekly look back is still offered, not on. When your AI's part of a routine cannot
20
+ run, the report now says why in words you can act on (Claude is not signed in on this computer: open
21
+ Claude and sign in), and the filing part still runs.
22
+
23
+ Your agent can message you. `evda_message` (and `evda message`) writes one new file in your record's
24
+ `messages/` folder, which the EVÐA phone app shows as a card, and your reply comes back as a file beside
25
+ it. `evda_replies` (and `evda replies`) hands your agent each reply once at the start of its next run,
26
+ and again if you change it. A new record has the folder; an older one gets it the first time a message
27
+ is sent, and `evda.json` can map it elsewhere. A message refuses anything shaped like a password, and a
28
+ reply you seal or just note is not shown to your agent.
29
+
7
30
  A note you asked to have just noted, or a sealed one, stays that way wherever EVDA copies it.
8
31
  `evda organize` no longer copies a just-noted capture into its day: the day gets one line saying where
9
32
  it is, and once you release it the next organize files its words. Gathering a pile quotes nothing of a
package/README.md CHANGED
@@ -91,9 +91,12 @@ apply places only what is new, in the record's own folders.
91
91
  evda_write_skill writes or revises a skill at .claude/skills/<name>/SKILL.md. Never outside that folder, never a rule about your life
92
92
  evda_create_subrecord an EVÐA inside this one, with the four roles and its own AGENTS.md, then doctor. Your evda.json changes only on bounded with confirmed: true
93
93
  evda_doctor evda doctor, as a report
94
- evda_check_update whether newer EVDA tooling exists. The only tool that reaches the network
94
+ evda_pair_device a one-time code and QR link for your phone, and what to do on it; the phone gets only the parts named
95
+ evda_sync_devices adds a phone that used its code, takes in what your devices wrote, sends what changed
96
+ evda_devices the devices paired with your EVÐA and what each reads
97
+ evda_check_update whether newer EVDA tooling exists
95
98
 
96
- Every write appends, refuses anything shaped like a password, key or token, and says the path it wrote. The one call that leaves this machine is `evda_check_update` asking the npm registry for a version number; it carries nothing from the record and installs nothing, and the update itself is a command you run in a terminal. `evda connect` prints the setup for your record, with its path filled in:
99
+ Every write appends, refuses anything shaped like a password, key or token, and says the path it wrote. Two kinds of call leave this machine. `evda_check_update` asks the npm registry for a version number; it carries nothing from the record and installs nothing, and the update itself is a command you run in a terminal. The device tools talk to the relay that carries your EVÐA between your own devices, and everything they send is sealed here first: the relay cannot read it. `evda connect` prints the setup for your record, with its path filled in:
97
100
 
98
101
  ```bash
99
102
  claude mcp add --scope user evda -- /path/to/node /path/to/evda.mjs mcp ~/my-evda # Claude Code
package/bin/evda.mjs CHANGED
@@ -26,7 +26,7 @@ import { holderOf, HOLDER_KINDS } from "../lib/manifest.mjs";
26
26
  import { EvdaError, isEntry } from "../lib/errors.mjs";
27
27
  import { publishCommand } from "../lib/publish.mjs";
28
28
  import { checkForUpdate, summarize, stampInstalledVersion } from "../lib/update-check.mjs";
29
- import { planCircles, coverage, declare, deviceCircles, planBeside } from "../lib/circle-plan.mjs";
29
+ import { planCircles, coverage, declare, deviceCircles, planBeside, defaultParts } from "../lib/circle-plan.mjs";
30
30
  import { circleIndex, grantView } from "../lib/filechanges.mjs";
31
31
  import { mountRoot } from "../lib/declarations.mjs";
32
32
  import { excluder } from "../lib/sync-rules.mjs";
@@ -34,7 +34,8 @@ import { parity, formatParity } from "../lib/parity.mjs";
34
34
  import { note, releaseNote, ownerSearch, readContext, readersFor, describeReaders, parseReaders, READ_BY } from "../lib/readers.mjs";
35
35
  import { runHook } from "../lib/guard.mjs";
36
36
  import { markdownFiles } from "../lib/circles.mjs";
37
- import { refuseSecrets } from "../lib/tools.mjs";
37
+ import { refuseSecrets, message as messageTool, replies as repliesTool } from "../lib/tools.mjs";
38
+ import * as pairing from "../lib/pairing.mjs";
38
39
 
39
40
  const HELP = `evda: operate an EVDA. The files are the truth; this is optional.
40
41
 
@@ -102,15 +103,24 @@ const HELP = `evda: operate an EVDA. The files are the truth; this is optional.
102
103
  undo an apply byte for byte; refused when a placed file was edited
103
104
  evda look-back [dir] [--model M]
104
105
  the week, read back by the owner's claude -p, appended to a report
105
- evda routine <morning|weekly> [dir] [--launchd] [--install]
106
+ evda routine <morning|weekly> [dir] [--launchd] [--install] [--uninstall]
106
107
  print a scheduled routine for Claude, or a launchd fallback; --install
107
- writes the launchd job, and nothing is scheduled without it
108
+ writes the launchd job, --uninstall removes it (setup installs the
109
+ morning organize unless told no)
108
110
  evda preset list the organizations this package ships, and who each suits
109
111
  evda preset show <id> one preset's folders
110
112
  evda preset apply <id> [--dry] [dir]
111
113
  add the preset's folders and manifests. Only adds: an existing
112
114
  manifest that differs is a CONFLICT and is left as it is
113
115
  evda preset diff <id> [dir] what the preset has that this record does not
116
+ evda pair [dir] [--name phone] [--parts journal[,people]] [--json]
117
+ a one-time code (and a QR link) for a phone to join this EVÐA. It
118
+ works once, for 10 minutes, and gives the device only the parts named
119
+ evda devices [dir] [--json] the devices paired with this EVÐA, and what each reads
120
+ evda remove <device> [dir] remove a device: it adds nothing more and receives nothing new
121
+ evda sync [dir] add devices that used their code, take in what they sent, send what
122
+ changed. Pairing needs Node 22.13 or newer; --relay <url> for another relay
123
+ --json on pair, devices, remove and sync prints data for an app, no ids
114
124
  evda status [dir] one screen summary
115
125
  evda publish public [dir] --out <dir> --key <seed file>
116
126
  the public circle as a signed plaintext export; refuses (exit 2) on a
@@ -122,6 +132,10 @@ const HELP = `evda: operate an EVDA. The files are the truth; this is optional.
122
132
  circles and grants roles and their folders; moves and changes no file.
123
133
  A circle rooted at "" is written as the root itself, and a circle
124
134
  already declared keeps its id, release, rules, mount and policy
135
+ evda circles default [--dry-run] [--json] [dir]
136
+ split a record that has no parts into the default two: journal (the
137
+ day files and captures) and private (everything else). A record that
138
+ has parts is left as it is. --dry-run says what it would make
125
139
  evda circles check [--plan <file>] [dir]
126
140
  every syncable file in exactly one circle (CHECKED n files); fails on a
127
141
  file no circle covers, and on a symlink, iCloud stub or oversize file.
@@ -134,6 +148,12 @@ const HELP = `evda: operate an EVDA. The files are the truth; this is optional.
134
148
  or project, or a folder of effort files. CHECKED n, or SKIPPED and why
135
149
  evda note "<words>" [--in dir] just note it: a capture no AI reads until you release it (words from stdin with -)
136
150
  evda release <path> [dir] you can read that now: clear the just-noted flag on one entry
151
+ evda message --title "<title>" [--from NAME] [--in dir] "<body>"
152
+ tell the owner something: one new file in the messages role, a card
153
+ in their phone app (body from stdin with -)
154
+ evda replies [--peek] [--in dir] [--json]
155
+ the owner's replies not seen yet, marked seen unless --peek; read
156
+ under read_by as the AI $EVDA_READER names (claude, local, ...)
137
157
  evda search <words> [dir] your own search, on this device, no model; just-noted and sealed entries included and marked
138
158
  evda readers [dir] which AI reads each part of the record, from read_by
139
159
  evda local-model how to run a model on your own computer and point a circle at it
@@ -165,7 +185,7 @@ function isDirArg(a) {
165
185
  }
166
186
 
167
187
  // `--name value` or `--name=value`. The value of a `--name value` pair is not a positional.
168
- const VALUE_FLAGS = ["to", "kind", "name", "role", "by", "as", "model", "project", "hour", "weekday", "out", "key", "plan", "device", "shadow", "record-id", "env-file", "port", "host", "public-url", "log", "lifeline", "ttl"];
188
+ const VALUE_FLAGS = ["to", "kind", "name", "role", "by", "as", "model", "project", "hour", "weekday", "out", "key", "plan", "device", "shadow", "record-id", "env-file", "port", "host", "public-url", "log", "lifeline", "ttl", "parts", "relay"];
169
189
  function flagValue(args, name) {
170
190
  const eq = args.find((a) => a.startsWith(`--${name}=`));
171
191
  if (eq) return eq.slice(name.length + 3);
@@ -410,7 +430,7 @@ async function main(argv) {
410
430
  }
411
431
 
412
432
  case "routine": {
413
- // evda routine <morning|weekly> [dir] [--launchd] [--install]
433
+ // evda routine <morning|weekly> [dir] [--launchd] [--install] [--uninstall]
414
434
  const { routineMain } = await import("../lib/routines.mjs");
415
435
  return routineMain(args, { pickDir, flagValue });
416
436
  }
@@ -485,6 +505,41 @@ async function main(argv) {
485
505
  return 0;
486
506
  }
487
507
 
508
+ case "pair": {
509
+ const { dir } = pickDir(args);
510
+ const r = await pairing.pair(dir, { name: flagValue(args, "name"), parts: flagValue(args, "parts"), relay: flagValue(args, "relay") });
511
+ // --json is for an app (the EVÐA Mac app, HQ-1925): the code and its QR link, and nothing with an id.
512
+ if (args.includes("--json")) console.log(JSON.stringify({ name: r.name, code: r.code, qr: r.qr, computer: r.computer, expiresAt: r.expiresAt, parts: r.parts }));
513
+ else for (const line of r.lines) console.log(line);
514
+ return 0;
515
+ }
516
+
517
+ case "devices":
518
+ case "sync": {
519
+ const { dir } = pickDir(args);
520
+ const relay = flagValue(args, "relay");
521
+ if (args.includes("--json")) {
522
+ console.log(JSON.stringify(cmd === "devices" ? await pairing.deviceData(dir, { relay }) : await pairing.syncData(dir, { relay })));
523
+ return 0;
524
+ }
525
+ const lines = await pairing[cmd](dir, { relay });
526
+ for (const line of lines) console.log(line);
527
+ return 0;
528
+ }
529
+
530
+ case "remove": {
531
+ // The device's name comes first and is never read as the record, even when a folder by
532
+ // that name sits in the working directory.
533
+ const values = new Set(VALUE_FLAGS.map((n) => args.indexOf(`--${n}`)).filter((i) => i >= 0).map((i) => i + 1));
534
+ const at = args.findIndex((a, i) => !a.startsWith("--") && !values.has(i));
535
+ const name = at >= 0 ? args[at] : flagValue(args, "name");
536
+ const { dir } = pickDir(at >= 0 ? args.filter((_, i) => i !== at) : args);
537
+ const lines = await pairing.remove(dir, name, { relay: flagValue(args, "relay") });
538
+ if (args.includes("--json")) console.log(JSON.stringify({ removed: String(name).trim(), lines }));
539
+ else for (const line of lines) console.log(line);
540
+ return 0;
541
+ }
542
+
488
543
  case "doctor": {
489
544
  const { dir, via } = pickDir(args);
490
545
  return doctor(dir, { via }).exit;
@@ -507,6 +562,32 @@ async function main(argv) {
507
562
  return 0;
508
563
  }
509
564
 
565
+ case "message": {
566
+ // Like note, the body may hold a slash, so the record is --in <dir> or the locator.
567
+ const take = (flag) => { const i = args.indexOf(flag); if (i < 0) return undefined; const v = args[i + 1]; if (v === undefined) throw new EvdaError(`${flag} needs a value`, { exit: 2 }); args.splice(i, 2); return v; };
568
+ const dirArg = take("--in");
569
+ const title = take("--title");
570
+ const from = take("--from");
571
+ const dir = locate({ explicit: dirArg }).dir;
572
+ let body = args.join(" ");
573
+ if (body === "-" || body === "") body = readFileSync(0, "utf8");
574
+ const r = messageTool(openEvda(dir), { title, body, ...(from === undefined ? {} : { from }) });
575
+ console.log(`Sent the message ${r.id}, filed at ${r.path}.`);
576
+ return 0;
577
+ }
578
+
579
+ case "replies": {
580
+ const i = args.indexOf("--in");
581
+ const dir = locate({ explicit: i >= 0 ? args[i + 1] : undefined }).dir;
582
+ const peek = args.includes("--peek");
583
+ const r = repliesTool(openEvda(dir), peek ? { peek: true } : {});
584
+ if (args.includes("--json")) { console.log(JSON.stringify({ replies: r.replies, marked: r.marked }, null, 2)); return 0; }
585
+ for (const x of r.replies) console.log(`## ${x.title} (${x.id}, ${x.date})\n\n${x.reply}\n`);
586
+ if (r.heldBack) console.log(r.heldBack);
587
+ console.log(`CHECKED ${r.dir}/ ${r.replies.length} new repl${r.replies.length === 1 ? "y" : "ies"}, ${r.marked} marked seen${peek ? " (peek)" : ""}`);
588
+ return 0;
589
+ }
590
+
510
591
  case "release": {
511
592
  const { dir, entry } = pickReleaseArgs(args);
512
593
  const r = releaseNote(openEvda(dir), entry);
@@ -806,6 +887,16 @@ async function main(argv) {
806
887
  console.log(`wrote ${written.length} files: ${written.map((p) => relative(dir, p)).join(", ")}`);
807
888
  return 0;
808
889
  }
890
+ if (sub === "default") {
891
+ // The default parts (HQ-1925), for a record that has none: the app's one button, on the
892
+ // owner's yes. --dry-run is the question and writes nothing.
893
+ const r = defaultParts(dir, { dryRun: args.includes("--dry-run") });
894
+ if (args.includes("--json")) { console.log(JSON.stringify(r)); return 0; }
895
+ if (r.declared) console.log(`Split into parts: ${r.parts.join(", ")}. Nothing in your files moved.`);
896
+ else if (r.needed) console.log(`Not split into parts yet. Saying yes makes: ${r.parts.join(", ")}. Nothing was written.`);
897
+ else console.log(`This EVÐA is already split into parts: ${r.parts.join(", ")}. Nothing was written.`);
898
+ return 0;
899
+ }
809
900
  if (sub === "check") {
810
901
  const evda = openEvda(dir);
811
902
  // Without --plan, the plan kept beside the circles (`<circles role>/plan.json`) says what is
@@ -822,7 +913,7 @@ async function main(argv) {
822
913
  if (r.checked === 0) { console.log("FAIL CHECKED 0: nothing was examined"); return 1; }
823
914
  return r.problems.length ? 1 : 0;
824
915
  }
825
- throw new EvdaError("circles needs plan, declare or check", { exit: 2 });
916
+ throw new EvdaError("circles needs plan, declare, default or check", { exit: 2 });
826
917
  }
827
918
 
828
919
  case "parity": {
package/create-evda.mjs CHANGED
@@ -3,7 +3,7 @@
3
3
  //
4
4
  // create-evda <dir> [--agent-name NAME] [--owner NAME] [--purpose TEXT] [--voice TEXT]
5
5
  // [--platform claude|chatgpt|gemini|cursor|other] [--training-off] [--yes]
6
- // [--filing everything|asked] [--no-prompt-hook]
6
+ // [--filing everything|asked] [--no-prompt-hook] [--no-parts]
7
7
  // [--name NAME] [--layers record,ledger[,circles]] [--preset ID|none] [--force]
8
8
  //
9
9
  // On a terminal, any setup answer not given as a flag is asked, one question at a time, and setup
@@ -13,7 +13,10 @@
13
13
  //
14
14
  // Writes the manifest, the operating layer with the owner's answers in its owner section, the
15
15
  // loaders for the tools that do not read AGENTS.md on their own, the corpus, the ledger, and
16
- // .evda/. Then runs `evda doctor` on what it wrote. It creates no example notes: an empty record
16
+ // .evda/. Splits the record into its default parts (HQ-1925): journal, the day files and captures,
17
+ // and private, everything else, both held by this computer, so a phone can be added with no
18
+ // terminal step (--no-parts leaves it unsplit, for parts declared by hand). Then runs `evda doctor`
19
+ // on what it wrote. It creates no example notes: an empty record
17
20
  // that fills with a real life is the product. The one file it writes under record/ is today's
18
21
  // daily file holding a true event, the record's own creation, so the first session has something
19
22
  // dated to read and the owner sees where their words go.
@@ -30,12 +33,13 @@ import { readPointer, setPointer, pointerPath } from "./lib/locate.mjs";
30
33
  import { FOLDER_MANIFEST } from "./lib/folders.mjs";
31
34
  import { EvdaError } from "./lib/errors.mjs";
32
35
  import { clients, installSkill, installGuard } from "./lib/connect.mjs";
33
- import { desktopInstructions } from "./lib/routines.mjs";
36
+ import { defaultRoutines, installAtSetup } from "./lib/routines.mjs";
34
37
  import { installHook } from "./lib/hooks.mjs";
35
38
  import { applyPreset, listPresets, planLines, presetIds, NONE } from "./lib/presets.mjs";
36
39
  import { FILING_MODES } from "./lib/day-file.mjs";
37
40
  import { installPromptHook } from "./lib/hooks.mjs";
38
- import { PLATFORMS, PLATFORM_KEYS, LOADERS, DESKTOP_FILESYSTEM, DICTATION, DEFAULT_AGENT_NAME, platformKey, fillOwnerSection, aiLine, interview, trainingSection, termsSection, readerFor, INTRO } from "./lib/setup.mjs";
41
+ import { defaultParts } from "./lib/circle-plan.mjs";
42
+ import { PLATFORMS, PLATFORM_KEYS, PARTS_LINE, LOADERS, DESKTOP_FILESYSTEM, DICTATION, DEFAULT_AGENT_NAME, platformKey, fillOwnerSection, aiLine, interview, trainingSection, termsSection, readerFor, INTRO } from "./lib/setup.mjs";
39
43
  import { describeReaders, parseReaders } from "./lib/readers.mjs";
40
44
 
41
45
  const HERE = dirname(fileURLToPath(import.meta.url));
@@ -53,6 +57,7 @@ const CANONICAL = {
53
57
  circles: "circles",
54
58
  grants: "grants",
55
59
  mounts: "mounts",
60
+ messages: "messages",
56
61
  };
57
62
 
58
63
  // What each record folder holds (Spec 07, section 2), written as folder.evda.json beside the
@@ -61,15 +66,16 @@ const FOLDER_TYPES = {
61
66
  "record/daily": "record",
62
67
  "record/captures": "capture",
63
68
  "record/people": "person",
69
+ messages: "message",
64
70
  };
65
71
 
66
- const USAGE = `usage: create-evda <dir> [--agent-name NAME] [--owner NAME] [--purpose TEXT] [--voice TEXT] [--platform ${PLATFORM_KEYS.join("|")}] [--training-off] [--filing everything|asked] [--no-prompt-hook] [--yes] [--name NAME] [--kind person|agent|company] [--id ATLAS-ID] [--layers record,ledger] [--skill] [--routines morning,weekly] [--preset ${presetIds().join("|")}|none] [--use] [--no-pointer] [--no-guard]`;
72
+ const USAGE = `usage: create-evda <dir> [--agent-name NAME] [--owner NAME] [--purpose TEXT] [--voice TEXT] [--platform ${PLATFORM_KEYS.join("|")}] [--training-off] [--filing everything|asked] [--no-prompt-hook] [--no-parts] [--yes] [--name NAME] [--kind person|agent|company] [--id ATLAS-ID] [--layers record,ledger] [--skill] [--routines morning,weekly] [--preset ${presetIds().join("|")}|none] [--use] [--no-pointer] [--no-guard]`;
67
73
 
68
74
  export function parseArgs(argv) {
69
75
  const opts = {
70
76
  layers: ["record", "ledger"], force: false, name: null, owner: "", kind: "person", id: null, pointer: true, use: false,
71
77
  yes: false, trainingOff: false, agentName: null, purpose: "", voice: "", platform: null, platformName: null,
72
- skill: null, routines: null, preset: null, guard: true, filing: null, promptHook: true,
78
+ skill: null, routines: null, preset: null, guard: true, filing: null, promptHook: true, parts: true,
73
79
  };
74
80
  const args = argv.flatMap((a) => {
75
81
  const eq = /^(--[a-z-]+)=([\s\S]*)$/.exec(a);
@@ -90,6 +96,7 @@ export function parseArgs(argv) {
90
96
  else if (a === "--skill") opts.skill = true;
91
97
  else if (a === "--no-guard") opts.guard = false;
92
98
  else if (a === "--no-prompt-hook") opts.promptHook = false;
99
+ else if (a === "--no-parts") opts.parts = false;
93
100
  else if (a === "--filing") {
94
101
  opts.filing = value(++i, a).trim().toLowerCase();
95
102
  if (!FILING_MODES.includes(opts.filing)) throw new EvdaError(`--filing takes ${FILING_MODES.join(" or ")}, got ${args[i]}`, { exit: 2 });
@@ -149,6 +156,9 @@ export function scaffold(opts) {
149
156
  const today = localDate();
150
157
  const mapping = { agents: CANONICAL.agents };
151
158
  for (const layer of opts.layers) for (const role of rolesForLayer(layer)) mapping[role] = CANONICAL[role];
159
+ // Messages from the owner's agent, and their replies (HQ-1246): every record has the folder, in
160
+ // whatever layers, because the phone app's Dispatch reads it.
161
+ mapping.messages = CANONICAL.messages;
152
162
 
153
163
  mkdirSync(dir, { recursive: true });
154
164
  for (const [role, rel] of Object.entries(mapping)) if (role !== "agents") mkdirSync(join(dir, rel), { recursive: true });
@@ -297,6 +307,12 @@ export async function main(argv, io = null) {
297
307
  for (const l of planLines(plan)) log(l);
298
308
  log("");
299
309
  }
310
+ // The default parts, after the preset so its folders exist, and before doctor so doctor reads them.
311
+ if (opts.parts !== false) {
312
+ const parts = defaultParts(dir);
313
+ log(parts.parts.includes("journal") ? PARTS_LINE : `parts: ${parts.parts.join(", ")}`);
314
+ log("");
315
+ }
300
316
  const report = doctor(dir, { out: log, via: "create-evda" });
301
317
  if (report.exit !== 0) {
302
318
  console.error("\ncreate-evda wrote the layout but doctor does not pass on it. That is a bug in create-evda; the output above says where.");
@@ -322,11 +338,10 @@ export async function main(argv, io = null) {
322
338
  log("");
323
339
  log(`Skill: ${r.action} ${r.path}`);
324
340
  }
325
- for (const key of opts.routines ?? []) {
326
- log("");
327
- for (const l of desktopInstructions(key, dir).split("\n")) log(l);
328
- log(`Or schedule it with launchd on a Mac: evda routine ${key} ${dir} --install`);
329
- }
341
+ // Organizing is on unless the owner said otherwise (HQ-1246): no flag and no answer means the
342
+ // morning organize is installed.
343
+ const env = io?.env ?? process.env;
344
+ for (const l of installAtSetup(opts.routines ?? defaultRoutines(env), dir, { env, platform: io?.platform ?? process.platform })) log(l);
330
345
  log("");
331
346
  for (const l of INTRO.split("\n")) log(l);
332
347
  log("");