@lovrozagar/grunt 0.9.4 → 0.10.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/.agents/skills/jev/SKILL.md +18 -0
- package/.claude/skills/jev/SKILL.md +18 -0
- package/.grok/hooks/orchestrate-parent.js +9 -1
- package/.grok/skills/jev/SKILL.md +18 -0
- package/.rulesync/reference/INDEX.md +4 -2
- package/.rulesync/reference/hooks.md +1 -1
- package/.rulesync/reference/jev.md +56 -0
- package/.rulesync/reference/law.md +2 -2
- package/.rulesync/reference/refs-map.md +1 -0
- package/.rulesync/reference/setup.md +3 -1
- package/.rulesync/reference/skills-map.md +1 -0
- package/.rulesync/skills/jev/SKILL.md +15 -0
- package/CHANGELOG.md +15 -0
- package/README.md +19 -7
- package/cli/grunt.mjs +60 -8
- package/cli/grunt.test.ts +122 -1
- package/cli/init.mjs +118 -10
- package/cli/init.test.ts +302 -5
- package/cli/prompt.mjs +1 -0
- package/cli/prompt.test.ts +29 -0
- package/package.json +4 -1
- package/scripts/board.mjs +390 -0
- package/scripts/doctor.mjs +53 -15
- package/scripts/feature-packs.mjs +123 -0
- package/scripts/guarded-md.mjs +28 -0
- package/scripts/jev.mjs +355 -0
- package/scripts/listen.mjs +2 -1
- package/scripts/pipeline.mjs +84 -2
- package/scripts/prompt.mjs +4 -0
- package/scripts/session-map.mjs +4 -2
- package/scripts/setup.mjs +85 -47
- package/scripts/speak.mjs +2 -0
package/scripts/setup.mjs
CHANGED
|
@@ -5,38 +5,28 @@ import os from "node:os";
|
|
|
5
5
|
import path from "node:path";
|
|
6
6
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
7
7
|
import { isInteractive as ttyInteractive } from "./interactive.mjs";
|
|
8
|
+
import { selectedPacks } from "./feature-packs.mjs";
|
|
8
9
|
import {
|
|
9
10
|
CHROMIUM_BINS,
|
|
10
11
|
installHints,
|
|
12
|
+
jevDoctorStatus,
|
|
11
13
|
speakDoctorStatus,
|
|
12
14
|
whichBin,
|
|
13
15
|
workspaceDoctorStatus,
|
|
14
16
|
} from "./doctor.mjs";
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
setAccount,
|
|
19
|
-
tokenStorePath,
|
|
20
|
-
} from "./google-workspace.mjs";
|
|
21
|
-
import {
|
|
22
|
-
downloadGgml,
|
|
23
|
-
findWhisperBin,
|
|
24
|
-
loadCfg as loadListenCfg,
|
|
25
|
-
probeStt,
|
|
26
|
-
} from "./listen.mjs";
|
|
27
|
-
import { loadCfg as loadSpeakCfg, whoami as speakWhoami } from "./speak.mjs";
|
|
28
|
-
|
|
29
|
-
export const TARGETS = ["speak", "listen", "google-workspace", "browser"];
|
|
30
|
-
export const USAGE = `usage: setup [speak|listen|google-workspace|browser]
|
|
17
|
+
|
|
18
|
+
export const TARGETS = ["speak", "listen", "google-workspace", "browser", "jev"];
|
|
19
|
+
export const USAGE = `usage: setup [speak|listen|google-workspace|browser|jev]
|
|
31
20
|
speak --elevenlabs-key KEY | --openai-key KEY [--provider elevenlabs|openai] [--skip-verify]
|
|
32
21
|
listen [--openai-key KEY] [--device ID] [--skip-download]
|
|
33
22
|
google-workspace [--creds PATH] [--account NAME] [--project ID]
|
|
34
23
|
browser (engine install hints; no secrets)
|
|
24
|
+
jev --api-key KEY
|
|
35
25
|
--redo redo a target that is already ok
|
|
36
26
|
|
|
37
27
|
TTY menu shows ok/missing. Missing bins: print OS command, then re-check PATH. Never installs.
|
|
38
|
-
Already ok: confirm redo (TTY) unless --redo. Ends with
|
|
39
|
-
Secrets: ~/.grunt/ (not git). Env still wins over the file.
|
|
28
|
+
Already ok: confirm redo (TTY) unless --redo. Ends with one status line per target (not a README).
|
|
29
|
+
Secrets: ~/.grunt/ (not git). Env still wins over the file. Setup does not export keys.
|
|
40
30
|
`;
|
|
41
31
|
|
|
42
32
|
export const ELEVENLABS_KEY_URL = "https://elevenlabs.io/app/settings/api-keys";
|
|
@@ -207,7 +197,7 @@ export function workspaceConsoleUrls(projectId) {
|
|
|
207
197
|
};
|
|
208
198
|
}
|
|
209
199
|
|
|
210
|
-
export function openUrl(
|
|
200
|
+
export async function openUrl(
|
|
211
201
|
url,
|
|
212
202
|
{ platform = process.platform, spawnSyncFn, openUrlFn } = {},
|
|
213
203
|
) {
|
|
@@ -215,6 +205,7 @@ export function openUrl(
|
|
|
215
205
|
openUrlFn(url);
|
|
216
206
|
return;
|
|
217
207
|
}
|
|
208
|
+
const { openUrl: openWorkspaceUrl } = await import("./google-workspace.mjs");
|
|
218
209
|
openWorkspaceUrl(url, { platform, spawnSyncFn });
|
|
219
210
|
}
|
|
220
211
|
|
|
@@ -243,46 +234,60 @@ export function browserEngineStatus(lp, cr, platform = process.platform) {
|
|
|
243
234
|
return { ok: false, extra: have.concat(need).join(",") };
|
|
244
235
|
}
|
|
245
236
|
|
|
246
|
-
export function
|
|
237
|
+
export function setupTargets(packs = []) {
|
|
238
|
+
const on = new Set(packs);
|
|
239
|
+
return TARGETS.filter((target) => target === "browser" || target === "jev" || on.has(target));
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
export async function targetStatuses({
|
|
247
243
|
home = os.homedir(),
|
|
248
244
|
env = process.env,
|
|
249
245
|
whichFn = whichBin,
|
|
250
246
|
pathEnv,
|
|
251
247
|
platform = process.platform,
|
|
248
|
+
targets = TARGETS,
|
|
252
249
|
} = {}) {
|
|
253
250
|
const pEnv = pathEnv || env.PATH || process.env.PATH;
|
|
254
251
|
const speak = speakDoctorStatus({ env, home });
|
|
252
|
+
const jev = jevDoctorStatus({ env, home });
|
|
255
253
|
const ws = workspaceDoctorStatus({ home });
|
|
256
|
-
const cfg = loadListenCfg({ env, home });
|
|
257
|
-
const stt = probeStt({ cfg, whichFn, pathEnv: pEnv, platform, home, cwd: "" });
|
|
258
|
-
const ffmpeg = whichFn("ffmpeg", pEnv, platform);
|
|
259
|
-
const whisper = findWhisperBin(whichFn, pEnv, platform);
|
|
260
|
-
const listenBits = [stt && stt.kind, ffmpeg && "ffmpeg", whisper && "whisper"].filter(Boolean);
|
|
261
254
|
const lp = whichFn("lightpanda", pEnv, platform);
|
|
262
255
|
const cr = lookupChromium(whichFn, pEnv, platform);
|
|
256
|
+
let listen = { ok: false, extra: "" };
|
|
257
|
+
if (targets.includes("listen")) {
|
|
258
|
+
const { loadCfg, probeStt, findWhisperBin } = await import("./listen.mjs");
|
|
259
|
+
const cfg = loadCfg({ env, home });
|
|
260
|
+
const stt = probeStt({ cfg, whichFn, pathEnv: pEnv, platform, home, cwd: "" });
|
|
261
|
+
const ffmpeg = whichFn("ffmpeg", pEnv, platform);
|
|
262
|
+
const whisper = findWhisperBin(whichFn, pEnv, platform);
|
|
263
|
+
const listenBits = [stt && stt.kind, ffmpeg && "ffmpeg", whisper && "whisper"].filter(Boolean);
|
|
264
|
+
listen = { ok: Boolean(stt), extra: listenBits.join(",") };
|
|
265
|
+
}
|
|
263
266
|
return {
|
|
264
267
|
speak: { ok: speak.ok, extra: speak.extra },
|
|
265
|
-
|
|
268
|
+
jev: { ok: jev.ok, extra: jev.extra },
|
|
269
|
+
listen,
|
|
266
270
|
"google-workspace": { ok: ws.ok, extra: ws.extra },
|
|
267
271
|
browser: browserEngineStatus(lp, cr, platform),
|
|
268
272
|
};
|
|
269
273
|
}
|
|
270
274
|
|
|
271
|
-
export function formatStatus(statuses) {
|
|
272
|
-
return
|
|
275
|
+
export function formatStatus(statuses, targets = TARGETS) {
|
|
276
|
+
return targets.map((t) => {
|
|
273
277
|
const st = (statuses && statuses[t]) || { ok: false, extra: "" };
|
|
274
278
|
const b = st.ok ? "ok" : "missing";
|
|
275
279
|
return st.extra ? `${t} ${b} ${st.extra}` : `${t} ${b}`;
|
|
276
280
|
}).join("\n");
|
|
277
281
|
}
|
|
278
282
|
|
|
279
|
-
export function menuOptions(statuses) {
|
|
283
|
+
export function menuOptions(statuses, targets = TARGETS) {
|
|
280
284
|
const base = [
|
|
281
285
|
{ value: "speak", label: "speak (ElevenLabs / OpenAI TTS)" },
|
|
282
286
|
{ value: "listen", label: "listen (STT)" },
|
|
283
287
|
{ value: "google-workspace", label: "google-workspace" },
|
|
284
288
|
{ value: "browser", label: "browser (PATH engines)" },
|
|
285
|
-
|
|
289
|
+
{ value: "jev", label: "jev (TypeSafe research decision)" },
|
|
290
|
+
].filter((o) => targets.includes(o.value));
|
|
286
291
|
return base.map((o) => {
|
|
287
292
|
const st = (statuses && statuses[o.value]) || { ok: false, extra: "" };
|
|
288
293
|
const tag = st.ok
|
|
@@ -296,11 +301,11 @@ export function menuOptions(statuses) {
|
|
|
296
301
|
});
|
|
297
302
|
}
|
|
298
303
|
|
|
299
|
-
export function firstMissing(statuses) {
|
|
300
|
-
for (const t of
|
|
304
|
+
export function firstMissing(statuses, targets = TARGETS) {
|
|
305
|
+
for (const t of targets) {
|
|
301
306
|
if (!statuses || !statuses[t] || !statuses[t].ok) return t;
|
|
302
307
|
}
|
|
303
|
-
return "
|
|
308
|
+
return targets[0] || "browser";
|
|
304
309
|
}
|
|
305
310
|
|
|
306
311
|
export async function reprobeHint({
|
|
@@ -374,6 +379,7 @@ export async function setupSpeak({
|
|
|
374
379
|
spawnSyncFn,
|
|
375
380
|
interactive = false,
|
|
376
381
|
} = {}) {
|
|
382
|
+
const { loadCfg: loadSpeakCfg, whoami: speakWhoami } = await import("./speak.mjs");
|
|
377
383
|
let elevenKey = str(flags["elevenlabs-key"] || flags.elevenlabs);
|
|
378
384
|
let openaiKey = str(flags["openai-key"] || flags.openai);
|
|
379
385
|
let provider = str(flags.provider).toLowerCase();
|
|
@@ -399,12 +405,12 @@ export async function setupSpeak({
|
|
|
399
405
|
if (interactive && io) {
|
|
400
406
|
if (askEleven && !elevenKey) {
|
|
401
407
|
logLine(io, `Open: ${ELEVENLABS_KEY_URL}`);
|
|
402
|
-
openUrl(ELEVENLABS_KEY_URL, { platform, spawnSyncFn, openUrlFn });
|
|
408
|
+
await openUrl(ELEVENLABS_KEY_URL, { platform, spawnSyncFn, openUrlFn });
|
|
403
409
|
elevenKey = str(await io.password({ message: "ElevenLabs API key" }));
|
|
404
410
|
}
|
|
405
411
|
if (askOpenAi && !openaiKey) {
|
|
406
412
|
logLine(io, `Open: ${OPENAI_KEY_URL}`);
|
|
407
|
-
openUrl(OPENAI_KEY_URL, { platform, spawnSyncFn, openUrlFn });
|
|
413
|
+
await openUrl(OPENAI_KEY_URL, { platform, spawnSyncFn, openUrlFn });
|
|
408
414
|
openaiKey = str(await io.password({ message: "OpenAI API key" }));
|
|
409
415
|
}
|
|
410
416
|
}
|
|
@@ -453,6 +459,7 @@ export async function setupListen({
|
|
|
453
459
|
pathEnv = process.env.PATH,
|
|
454
460
|
interactive = false,
|
|
455
461
|
} = {}) {
|
|
462
|
+
const { downloadGgml, findWhisperBin, loadCfg: loadListenCfg, probeStt } = await import("./listen.mjs");
|
|
456
463
|
const ffmpegBin = await reprobeHint({
|
|
457
464
|
id: "ffmpeg",
|
|
458
465
|
found: whichFn("ffmpeg", pathEnv, platform),
|
|
@@ -491,7 +498,7 @@ export async function setupListen({
|
|
|
491
498
|
if (!whisperBin && !cfgNow.apiKey && !openaiKey) {
|
|
492
499
|
if (interactive && io) {
|
|
493
500
|
logLine(io, `Local STT is unavailable. OpenAI Whisper fallback: ${OPENAI_KEY_URL}`);
|
|
494
|
-
openUrl(OPENAI_KEY_URL, { platform, spawnSyncFn, openUrlFn });
|
|
501
|
+
await openUrl(OPENAI_KEY_URL, { platform, spawnSyncFn, openUrlFn });
|
|
495
502
|
openaiKey = str(await io.password({ message: "OpenAI API key (fallback STT)" }));
|
|
496
503
|
} else {
|
|
497
504
|
throw new Error(
|
|
@@ -540,6 +547,7 @@ export async function setupGoogleWorkspace({
|
|
|
540
547
|
interactive = false,
|
|
541
548
|
loginFn,
|
|
542
549
|
} = {}) {
|
|
550
|
+
const { loginLoopback, setAccount, tokenStorePath } = await import("./google-workspace.mjs");
|
|
543
551
|
const prevHome = process.env.HOME;
|
|
544
552
|
const prevProfile = process.env.USERPROFILE;
|
|
545
553
|
process.env.HOME = home;
|
|
@@ -556,7 +564,7 @@ export async function setupGoogleWorkspace({
|
|
|
556
564
|
const urls0 = workspaceConsoleUrls(projectId);
|
|
557
565
|
logLine(io, "Each person uses their own GCP Desktop OAuth client. Do not share JSON or tokens.");
|
|
558
566
|
logLine(io, `Open: ${urls0.projectCreate}`);
|
|
559
|
-
openUrl(urls0.projectCreate, { platform, spawnSyncFn, openUrlFn });
|
|
567
|
+
await openUrl(urls0.projectCreate, { platform, spawnSyncFn, openUrlFn });
|
|
560
568
|
if (typeof io.confirm === "function") {
|
|
561
569
|
await io.confirm({ message: "GCP project created? Continue", initialValue: true });
|
|
562
570
|
}
|
|
@@ -572,18 +580,18 @@ export async function setupGoogleWorkspace({
|
|
|
572
580
|
const urls = workspaceConsoleUrls(projectId);
|
|
573
581
|
logLine(io, "Consent: Internal on paid Workspace, else Testing + add your email as a test user.");
|
|
574
582
|
logLine(io, `Open: ${urls.branding}`);
|
|
575
|
-
openUrl(urls.branding, { platform, spawnSyncFn, openUrlFn });
|
|
583
|
+
await openUrl(urls.branding, { platform, spawnSyncFn, openUrlFn });
|
|
576
584
|
if (typeof io.confirm === "function") {
|
|
577
585
|
await io.confirm({ message: "Branding / audience done? Continue", initialValue: true });
|
|
578
586
|
}
|
|
579
587
|
logLine(io, `Open: ${urls.enableApis}`);
|
|
580
|
-
openUrl(urls.enableApis, { platform, spawnSyncFn, openUrlFn });
|
|
588
|
+
await openUrl(urls.enableApis, { platform, spawnSyncFn, openUrlFn });
|
|
581
589
|
if (typeof io.confirm === "function") {
|
|
582
590
|
await io.confirm({ message: "APIs enabled? Continue", initialValue: true });
|
|
583
591
|
}
|
|
584
592
|
logLine(io, "Create OAuth client: application type Desktop app → Download JSON.");
|
|
585
593
|
logLine(io, `Open: ${urls.clientCreate}`);
|
|
586
|
-
openUrl(urls.clientCreate, { platform, spawnSyncFn, openUrlFn });
|
|
594
|
+
await openUrl(urls.clientCreate, { platform, spawnSyncFn, openUrlFn });
|
|
587
595
|
if (typeof io.confirm === "function") {
|
|
588
596
|
await io.confirm({ message: "JSON downloaded? Continue", initialValue: true });
|
|
589
597
|
}
|
|
@@ -655,11 +663,35 @@ export async function setupBrowser({
|
|
|
655
663
|
return { ok: st.ok, lightpanda: lp, chromium };
|
|
656
664
|
}
|
|
657
665
|
|
|
666
|
+
export async function setupJev({
|
|
667
|
+
flags = {},
|
|
668
|
+
home = os.homedir(),
|
|
669
|
+
io,
|
|
670
|
+
interactive = false,
|
|
671
|
+
} = {}) {
|
|
672
|
+
let apiKey = str(flags["api-key"] || flags.key);
|
|
673
|
+
if (interactive && io && !apiKey && typeof io.password === "function") {
|
|
674
|
+
logLine(io, "Open: https://console.typesafe.ai/keys");
|
|
675
|
+
apiKey = str(await io.password({ message: "TypeSafe API key" }));
|
|
676
|
+
}
|
|
677
|
+
if (!apiKey) {
|
|
678
|
+
throw new Error(
|
|
679
|
+
interactive
|
|
680
|
+
? "need a TypeSafe API key"
|
|
681
|
+
: "need --api-key (or TTY: node scripts/setup.mjs jev)",
|
|
682
|
+
);
|
|
683
|
+
}
|
|
684
|
+
const dest = writeSecretJson(path.join(home, ".grunt", "jev.json"), { apiKey });
|
|
685
|
+
logLine(io, `wrote ${dest}`);
|
|
686
|
+
return { ok: true, dest };
|
|
687
|
+
}
|
|
688
|
+
|
|
658
689
|
async function runTarget(target, ctx) {
|
|
659
690
|
if (target === "speak") return setupSpeak(ctx);
|
|
660
691
|
if (target === "listen") return setupListen(ctx);
|
|
661
692
|
if (target === "google-workspace") return setupGoogleWorkspace(ctx);
|
|
662
693
|
if (target === "browser") return setupBrowser(ctx);
|
|
694
|
+
if (target === "jev") return setupJev(ctx);
|
|
663
695
|
throw new Error(`unknown target ${target}`);
|
|
664
696
|
}
|
|
665
697
|
|
|
@@ -667,7 +699,8 @@ export async function main(argv = process.argv.slice(2), opts = {}) {
|
|
|
667
699
|
const { _, flags } = parseArgv(argv);
|
|
668
700
|
const env = opts.env || process.env;
|
|
669
701
|
const home = opts.home || env.HOME || env.USERPROFILE || os.homedir();
|
|
670
|
-
const
|
|
702
|
+
const cwd = opts.cwd || process.cwd();
|
|
703
|
+
const targets = setupTargets(selectedPacks(cwd));
|
|
671
704
|
const interactive =
|
|
672
705
|
opts.interactive != null
|
|
673
706
|
? Boolean(opts.interactive)
|
|
@@ -677,6 +710,7 @@ export async function main(argv = process.argv.slice(2), opts = {}) {
|
|
|
677
710
|
stdin: opts.stdin || process.stdin,
|
|
678
711
|
stdout: opts.stdout || process.stdout,
|
|
679
712
|
});
|
|
713
|
+
const io = opts.io || (interactive ? await defaultIo() : null);
|
|
680
714
|
const ctx = {
|
|
681
715
|
flags,
|
|
682
716
|
env,
|
|
@@ -699,14 +733,18 @@ export async function main(argv = process.argv.slice(2), opts = {}) {
|
|
|
699
733
|
pathEnv: ctx.pathEnv,
|
|
700
734
|
platform: ctx.platform,
|
|
701
735
|
};
|
|
702
|
-
let statuses = targetStatuses(statusOpts);
|
|
736
|
+
let statuses = await targetStatuses({ ...statusOpts, targets });
|
|
703
737
|
let target = normalizeTarget(_[0]);
|
|
738
|
+
if (target && !targets.includes(target)) {
|
|
739
|
+
errLine(io, `${target} is off. Enable it with \`grunt init\` or \`grunt upgrade\`.`);
|
|
740
|
+
return 1;
|
|
741
|
+
}
|
|
704
742
|
if (!target && interactive && io && typeof io.select === "function") {
|
|
705
743
|
target = normalizeTarget(
|
|
706
744
|
await io.select({
|
|
707
745
|
message: "Setup",
|
|
708
|
-
options: menuOptions(statuses),
|
|
709
|
-
initialValue: firstMissing(statuses),
|
|
746
|
+
options: menuOptions(statuses, targets),
|
|
747
|
+
initialValue: firstMissing(statuses, targets),
|
|
710
748
|
}),
|
|
711
749
|
);
|
|
712
750
|
}
|
|
@@ -725,13 +763,13 @@ export async function main(argv = process.argv.slice(2), opts = {}) {
|
|
|
725
763
|
});
|
|
726
764
|
if (!again) {
|
|
727
765
|
logLine(io, `skip ${target} (already ok)`);
|
|
728
|
-
logLine(io, formatStatus(statuses));
|
|
766
|
+
logLine(io, formatStatus(statuses, targets));
|
|
729
767
|
return 0;
|
|
730
768
|
}
|
|
731
769
|
}
|
|
732
770
|
await runTarget(target, ctx);
|
|
733
|
-
statuses = targetStatuses(statusOpts);
|
|
734
|
-
logLine(io, formatStatus(statuses));
|
|
771
|
+
statuses = await targetStatuses({ ...statusOpts, targets });
|
|
772
|
+
logLine(io, formatStatus(statuses, targets));
|
|
735
773
|
return 0;
|
|
736
774
|
} catch (e) {
|
|
737
775
|
errLine(io, String(e && e.message ? e.message : e));
|
package/scripts/speak.mjs
CHANGED
|
@@ -5,6 +5,7 @@ import fs from "node:fs";
|
|
|
5
5
|
import os from "node:os";
|
|
6
6
|
import path from "node:path";
|
|
7
7
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
8
|
+
import { loadOptionalConsumerEnv } from "./doctor.mjs";
|
|
8
9
|
|
|
9
10
|
export const DEFAULT_ELEVEN_VOICE = "JBFqnCBsd6RMkjVDRZzb";
|
|
10
11
|
export const DEFAULT_ELEVEN_MODEL = "eleven_multilingual_v2";
|
|
@@ -246,6 +247,7 @@ export async function say({
|
|
|
246
247
|
|
|
247
248
|
export async function main(argv = process.argv.slice(2), opts = {}) {
|
|
248
249
|
const { _, flags } = parseArgv(argv);
|
|
250
|
+
if (!opts.env) await loadOptionalConsumerEnv();
|
|
249
251
|
const env = opts.env || process.env;
|
|
250
252
|
const home = opts.home || os.homedir();
|
|
251
253
|
const cwd = opts.cwd || process.cwd();
|