litmus-cli 1.4.40 → 1.4.42
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/README.md +7 -0
- package/dist/commands/connect.d.ts +2 -0
- package/dist/commands/connect.d.ts.map +1 -1
- package/dist/commands/connect.js +27 -8
- package/dist/commands/connect.js.map +1 -1
- package/dist/commands/doctor.d.ts +20 -2
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/doctor.js +29 -5
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/submit.d.ts.map +1 -1
- package/dist/commands/submit.js +1 -42
- package/dist/commands/submit.js.map +1 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/lib/ai-tracking.d.ts +13 -0
- package/dist/lib/ai-tracking.d.ts.map +1 -1
- package/dist/lib/ai-tracking.js +71 -0
- package/dist/lib/ai-tracking.js.map +1 -1
- package/dist/lib/api.d.ts +28 -0
- package/dist/lib/api.d.ts.map +1 -1
- package/dist/lib/api.js +74 -0
- package/dist/lib/api.js.map +1 -1
- package/dist/lib/connect-skills.d.ts +40 -0
- package/dist/lib/connect-skills.d.ts.map +1 -0
- package/dist/lib/connect-skills.js +150 -0
- package/dist/lib/connect-skills.js.map +1 -0
- package/dist/lib/errors.d.ts.map +1 -1
- package/dist/lib/errors.js +4 -0
- package/dist/lib/errors.js.map +1 -1
- package/dist/lib/hook-logger.cjs +137 -7
- package/dist/lib/session-over.d.ts +32 -0
- package/dist/lib/session-over.d.ts.map +1 -0
- package/dist/lib/session-over.js +99 -0
- package/dist/lib/session-over.js.map +1 -0
- package/dist/lib/skill-import.d.ts +82 -0
- package/dist/lib/skill-import.d.ts.map +1 -0
- package/dist/lib/skill-import.js +228 -0
- package/dist/lib/skill-import.js.map +1 -0
- package/dist/lib/skill-picker.d.ts +28 -0
- package/dist/lib/skill-picker.d.ts.map +1 -0
- package/dist/lib/skill-picker.js +100 -0
- package/dist/lib/skill-picker.js.map +1 -0
- package/dist/lib/tracker.d.ts.map +1 -1
- package/dist/lib/tracker.js +10 -0
- package/dist/lib/tracker.js.map +1 -1
- package/dist/lib/watcher.js +8 -0
- package/dist/lib/watcher.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { readFileSync } from "fs";
|
|
2
|
+
import readline from "readline";
|
|
3
|
+
import chalk from "chalk";
|
|
4
|
+
import ora from "ora";
|
|
5
|
+
import { importSkills, planSkillImport, ServerError } from "./api.js";
|
|
6
|
+
import { info, warn } from "./errors.js";
|
|
7
|
+
import { pickSkills } from "./skill-picker.js";
|
|
8
|
+
import { collectSkillFiles, importSummaryLines, lastAnswered, overUploadCaps, planRequest, rememberAnswered, skillNames, skillSetDigest, SKILLS_PICKUP, } from "./skill-import.js";
|
|
9
|
+
const NAMES_SHOWN = 5;
|
|
10
|
+
const ASK_ATTEMPTS = 3;
|
|
11
|
+
export function describeSkills(names) {
|
|
12
|
+
const shown = names.slice(0, NAMES_SHOWN).join(", ");
|
|
13
|
+
const more = names.length - NAMES_SHOWN;
|
|
14
|
+
return more > 0 ? `${shown} and ${more} more` : shown;
|
|
15
|
+
}
|
|
16
|
+
function askLine(question) {
|
|
17
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
18
|
+
return new Promise((resolve) => rl.question(question, (a) => {
|
|
19
|
+
rl.close();
|
|
20
|
+
resolve(a);
|
|
21
|
+
}));
|
|
22
|
+
}
|
|
23
|
+
/** Enter or yes imports, no declines. Anything else is asked again, and never read as
|
|
24
|
+
* consent: after a few tries it is a no. */
|
|
25
|
+
async function askYesNo(ask, question) {
|
|
26
|
+
let q = question;
|
|
27
|
+
for (let i = 0; i < ASK_ATTEMPTS; i++) {
|
|
28
|
+
const a = (await ask(q)).trim().toLowerCase();
|
|
29
|
+
if (a === "" || a === "y" || a === "yes")
|
|
30
|
+
return true;
|
|
31
|
+
if (a === "n" || a === "no")
|
|
32
|
+
return false;
|
|
33
|
+
q = ` Please answer y or n. ${chalk.dim("[Y/n]")} `;
|
|
34
|
+
}
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
/** Not offered at all: the assessment does not allow it (403) or the backend has no
|
|
38
|
+
* such route (404). Nothing to tell the candidate. */
|
|
39
|
+
function notOffered(err) {
|
|
40
|
+
return err instanceof ServerError && (err.statusCode === 403 || err.statusCode === 404);
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* The server's plan for these files: one request for every source folder, so the
|
|
44
|
+
* server dedupes across them and applies the upload's skill cap to the whole set. A
|
|
45
|
+
* skill it cannot read is left out and named, with the server's reason, never the
|
|
46
|
+
* skills beside it. Null when there is nothing to offer.
|
|
47
|
+
*/
|
|
48
|
+
async function planFiles(input, all) {
|
|
49
|
+
try {
|
|
50
|
+
const plan = await planSkillImport(input.backendUrl, input.token, planRequest(all));
|
|
51
|
+
for (const r of plan.refused)
|
|
52
|
+
warn(`Leaving out ~/.${r.path}: ${r.reason}`);
|
|
53
|
+
return plan;
|
|
54
|
+
}
|
|
55
|
+
catch (err) {
|
|
56
|
+
if (notOffered(err)) {
|
|
57
|
+
if (input.verbose)
|
|
58
|
+
info(`Skill import not offered: ${err instanceof Error ? err.message : String(err)}`);
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
// A refusal of the set as a whole (over the skill cap, say): say why, offer nothing.
|
|
62
|
+
warn(`Couldn't offer your skills for import: ${err instanceof Error ? err.message : String(err)}`);
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
export async function offerSkillImport(input) {
|
|
67
|
+
if (input.flag === false)
|
|
68
|
+
return null;
|
|
69
|
+
const all = collectSkillFiles(input.home);
|
|
70
|
+
if (all.length === 0)
|
|
71
|
+
return null;
|
|
72
|
+
const plan = await planFiles(input, all);
|
|
73
|
+
if (!plan)
|
|
74
|
+
return null;
|
|
75
|
+
const wanted = new Set(plan.upload);
|
|
76
|
+
const files = all.filter((f) => wanted.has(f.path));
|
|
77
|
+
// The server's own grouping, so a row in the picker is exactly one skill it will
|
|
78
|
+
// install. A backend without it gets the whole set, as before.
|
|
79
|
+
const byName = new Map(plan.skills.map((s) => [s.name, s.paths]));
|
|
80
|
+
const names = byName.size > 0 ? [...byName.keys()].sort() : skillNames(plan.upload);
|
|
81
|
+
if (names.length === 0)
|
|
82
|
+
return null;
|
|
83
|
+
const n = names.length;
|
|
84
|
+
const found = `Found ${n} AI skill${n === 1 ? "" : "s"} on this computer (${describeSkills(names)}).`;
|
|
85
|
+
const over = overUploadCaps(files);
|
|
86
|
+
if (over) {
|
|
87
|
+
info(`${found} Together they are ${over}, more than one import takes.`);
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
const pending = { files, names, digest: skillSetDigest(files), assessmentId: input.assessmentId };
|
|
91
|
+
if (input.flag === true)
|
|
92
|
+
return pending;
|
|
93
|
+
// The picker draws on stdout and reads keys from stdin: both must be the terminal.
|
|
94
|
+
if (!(input.isTTY ?? (process.stdin.isTTY && process.stdout.isTTY))) {
|
|
95
|
+
info(`${found} Run with --skills to import ${n === 1 ? "it" : "them"} into your workspace.`);
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
// Asked once per set of skills: a candidate who reconnects every day is asked
|
|
99
|
+
// again only after they add or change one.
|
|
100
|
+
if (lastAnswered(input.assessmentId, input.home) === pending.digest)
|
|
101
|
+
return null;
|
|
102
|
+
const ask = input.ask ?? askLine;
|
|
103
|
+
const yes = await askYesNo(ask, `Do you want to import your AI skills into this workspace? ${chalk.dim("[Y/n]")} `);
|
|
104
|
+
// Then the skills themselves, each switched on or off. The digest stays the whole
|
|
105
|
+
// set's, so a candidate who kept some is not asked again until a skill changes.
|
|
106
|
+
const chosen = !yes ? [] : byName.size === 0 ? names : ((await (input.pick ?? pickSkills)(names)) ?? []);
|
|
107
|
+
if (chosen.length === 0) {
|
|
108
|
+
rememberAnswered(input.assessmentId, pending.digest, input.home);
|
|
109
|
+
info("Skipped. To import them later, add --skills to your `litmus connect` command.");
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
if (byName.size === 0)
|
|
113
|
+
return pending;
|
|
114
|
+
const keep = new Set(chosen.flatMap((name) => byName.get(name) ?? []));
|
|
115
|
+
return { ...pending, files: files.filter((f) => keep.has(f.path)), names: chosen };
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Start the upload and return what to print when it is done. Never rejects: the
|
|
119
|
+
* workspace opens while this runs, and its outcome is reported at the end of connect.
|
|
120
|
+
*/
|
|
121
|
+
export function startSkillImport(backendUrl, token, pending, home) {
|
|
122
|
+
info(`Importing your ${pending.names.length === 1 ? "skill" : "skills"} in the background...`);
|
|
123
|
+
return (async () => {
|
|
124
|
+
try {
|
|
125
|
+
const result = await importSkills(backendUrl, token, pending.files.map((f) => ({ path: f.path, data: readFileSync(f.abs) })));
|
|
126
|
+
rememberAnswered(pending.assessmentId, pending.digest, home);
|
|
127
|
+
const lines = importSummaryLines(result);
|
|
128
|
+
return () => {
|
|
129
|
+
if (result.installed.length > 0) {
|
|
130
|
+
ora().succeed(`${lines[0]} ${SKILLS_PICKUP}`);
|
|
131
|
+
for (const line of lines.slice(1))
|
|
132
|
+
warn(line);
|
|
133
|
+
}
|
|
134
|
+
else {
|
|
135
|
+
warn("No skills were imported.");
|
|
136
|
+
for (const line of lines)
|
|
137
|
+
warn(line);
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
catch (err) {
|
|
142
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
143
|
+
return () => {
|
|
144
|
+
warn(`Couldn't import your skills: ${message}`);
|
|
145
|
+
info("Your workspace is fine. Run `litmus connect` again to retry.");
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
})();
|
|
149
|
+
}
|
|
150
|
+
//# sourceMappingURL=connect-skills.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"connect-skills.js","sourceRoot":"","sources":["../../src/lib/connect-skills.ts"],"names":[],"mappings":"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"}
|
package/dist/lib/errors.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../../src/lib/errors.ts"],"names":[],"mappings":"AAUA,eAAO,MAAM,YAAY,6BAA6B,CAAA;AAEtD,qBAAa,UAAW,SAAQ,KAAK;;CAEpC;AAID,QAAA,IAAI,YAAY,EAAE;IAChB,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,cAAc,CAAC,EAAE,MAAM,CAAA;CACnB,CAAA;AAEN,wBAAgB,eAAe,CAAC,GAAG,EAAE,OAAO,CAAC,OAAO,YAAY,CAAC,GAAG,IAAI,CAEvE;AAED,MAAM,MAAM,QAAQ,GAAG,OAAO,GAAG,SAAS,GAAG,MAAM,CAAA;AAEnD;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,WAAW,CACzB,QAAQ,EAAE,QAAQ,EAClB,OAAO,EAAE,MAAM,EACf,IAAI,CAAC,EAAE,MAAM,EACb,KAAK,CAAC,EAAE,MAAM,GACb,OAAO,CAAC,IAAI,CAAC,
|
|
1
|
+
{"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../../src/lib/errors.ts"],"names":[],"mappings":"AAUA,eAAO,MAAM,YAAY,6BAA6B,CAAA;AAEtD,qBAAa,UAAW,SAAQ,KAAK;;CAEpC;AAID,QAAA,IAAI,YAAY,EAAE;IAChB,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,cAAc,CAAC,EAAE,MAAM,CAAA;CACnB,CAAA;AAEN,wBAAgB,eAAe,CAAC,GAAG,EAAE,OAAO,CAAC,OAAO,YAAY,CAAC,GAAG,IAAI,CAEvE;AAED,MAAM,MAAM,QAAQ,GAAG,OAAO,GAAG,SAAS,GAAG,MAAM,CAAA;AAEnD;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,WAAW,CACzB,QAAQ,EAAE,QAAQ,EAClB,OAAO,EAAE,MAAM,EACf,IAAI,CAAC,EAAE,MAAM,EACb,KAAK,CAAC,EAAE,MAAM,GACb,OAAO,CAAC,IAAI,CAAC,CA0Bf;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI,CAElE;AAED;;;;;;;;GAQG;AACH,wBAAgB,KAAK,CACnB,OAAO,EAAE,MAAM,EACf,IAAI,CAAC,EAAE,MAAM,EACb,IAAI,CAAC,EAAE,MAAM,GAAG;IAAE,cAAc,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,QAAQ,CAAA;CAAE,GAC/D,KAAK,CA2BP;AAED;;GAEG;AACH,wBAAgB,IAAI,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAE1C;AAED;;GAEG;AACH,wBAAgB,OAAO,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAE7C;AAED;;GAEG;AACH,wBAAgB,IAAI,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAE1C"}
|
package/dist/lib/errors.js
CHANGED
|
@@ -41,9 +41,13 @@ export function setErrorContext(ctx) {
|
|
|
41
41
|
export function reportEvent(severity, message, hint, event) {
|
|
42
42
|
const apiBase = errorContext.apiBase || process.env.LITMUS_API_URL || DEFAULT_API_BASE;
|
|
43
43
|
const url = `${apiBase}/api/cli/errors`;
|
|
44
|
+
// ENG-2763: the token is what lets the server tell a report from this CLI apart
|
|
45
|
+
// from anyone else posting to the same public endpoint. The route resolves it
|
|
46
|
+
// to a candidate and never logs it; it is absent only before a command has one.
|
|
44
47
|
const payload = JSON.stringify({
|
|
45
48
|
severity,
|
|
46
49
|
event,
|
|
50
|
+
token: errorContext.token,
|
|
47
51
|
message,
|
|
48
52
|
hint,
|
|
49
53
|
candidateEmail: errorContext.candidateEmail,
|
package/dist/lib/errors.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"errors.js","sourceRoot":"","sources":["../../src/lib/errors.ts"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"errors.js","sourceRoot":"","sources":["../../src/lib/errors.ts"],"names":[],"mappings":"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"}
|
package/dist/lib/hook-logger.cjs
CHANGED
|
@@ -465,6 +465,96 @@ function versionHeader(config) {
|
|
|
465
465
|
: {}
|
|
466
466
|
}
|
|
467
467
|
|
|
468
|
+
// ── Session over (ENG-2768) ──────────────────────────────────────
|
|
469
|
+
// Only `litmus submit` releases the hooks and the registry. A session that
|
|
470
|
+
// ends any other way (web submit, deadline, the company ending it) leaves the
|
|
471
|
+
// hooks live, and the walk-up above finds the folder's config whatever the
|
|
472
|
+
// registry says, so every AI turn in an ended assessment folder kept sending
|
|
473
|
+
// the candidate's prompt and reply to a server that answers `400 Session over`
|
|
474
|
+
// every time: 12,229 of them over three days in production.
|
|
475
|
+
//
|
|
476
|
+
// So the refusal is remembered in the folder. The first refusal writes
|
|
477
|
+
// `.litmus/session-over`, and every later invocation that resolves to that
|
|
478
|
+
// folder stops before it records, appends, uploads or revives anything. The
|
|
479
|
+
// watcher writes the same marker when IT is told the session is over (see
|
|
480
|
+
// watcher.ts), which usually comes first.
|
|
481
|
+
//
|
|
482
|
+
// Only a refusal that can never turn into an acceptance counts: "Session over"
|
|
483
|
+
// (which `/cli/activity` also returns for a token that matches no candidate,
|
|
484
|
+
// ENG-2749) and "Invalid token" (its older spelling there, and other routes').
|
|
485
|
+
// A 5xx, a timeout or a 429 is transient and must never silence capture of a
|
|
486
|
+
// live session.
|
|
487
|
+
//
|
|
488
|
+
// The marker names the token it was written for, as a hash. A folder re-used
|
|
489
|
+
// by a later `litmus init` carries a new token, and the old marker does not
|
|
490
|
+
// silence it. A marker that names no token (unreadable, or hand-made) still
|
|
491
|
+
// silences: it can only ever stop capture, never start it.
|
|
492
|
+
//
|
|
493
|
+
// The same hash is also written as an empty file under
|
|
494
|
+
// ~/.litmus/sessions-over/, because the folder may not be writable, and a
|
|
495
|
+
// folder we could not mark would otherwise keep uploading for as long as
|
|
496
|
+
// another assessment keeps the user-scope hooks installed. Either copy is
|
|
497
|
+
// enough.
|
|
498
|
+
const SESSION_OVER_MARKER = "session-over"
|
|
499
|
+
const SESSIONS_OVER_DIR = path.join(os.homedir() || os.tmpdir(), ".litmus", "sessions-over")
|
|
500
|
+
const MAX_REFUSAL_BODY_BYTES = 16 * 1024
|
|
501
|
+
const FINAL_REFUSAL_CODES = new Set(["session over", "invalid token"])
|
|
502
|
+
|
|
503
|
+
function isFinalRefusal(status, rawBody) {
|
|
504
|
+
if (status < 400 || status >= 500 || status === 429) return false
|
|
505
|
+
let body
|
|
506
|
+
try { body = JSON.parse(rawBody) } catch { return false }
|
|
507
|
+
const detail = body && body.detail
|
|
508
|
+
const code = typeof detail === "string"
|
|
509
|
+
? detail
|
|
510
|
+
: detail && typeof detail === "object" && typeof detail.error === "string" ? detail.error : null
|
|
511
|
+
return typeof code === "string" && FINAL_REFUSAL_CODES.has(code.trim().toLowerCase())
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
function tokenDigest(token) {
|
|
515
|
+
return typeof token === "string" && token
|
|
516
|
+
? crypto.createHash("sha256").update(token).digest("hex")
|
|
517
|
+
: null
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
function writeSessionOverMarker(litmusDir, config, reason) {
|
|
521
|
+
const digest = tokenDigest(config && config.token)
|
|
522
|
+
try {
|
|
523
|
+
fs.writeFileSync(path.join(litmusDir, SESSION_OVER_MARKER), JSON.stringify({
|
|
524
|
+
reason,
|
|
525
|
+
at: new Date().toISOString(),
|
|
526
|
+
tokenSha256: digest,
|
|
527
|
+
}) + "\n")
|
|
528
|
+
} catch { /* unwritable folder; the home copy below still counts */ }
|
|
529
|
+
if (!digest) return
|
|
530
|
+
try {
|
|
531
|
+
fs.mkdirSync(SESSIONS_OVER_DIR, { recursive: true })
|
|
532
|
+
fs.writeFileSync(path.join(SESSIONS_OVER_DIR, digest), "")
|
|
533
|
+
} catch { /* best effort; the next refusal tries again */ }
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
function sessionOverMarked(litmusDir) {
|
|
537
|
+
const current = tokenDigest((readConfig(litmusDir) || {}).token)
|
|
538
|
+
if (current) {
|
|
539
|
+
try {
|
|
540
|
+
if (fs.statSync(path.join(SESSIONS_OVER_DIR, current)).isFile()) return true
|
|
541
|
+
} catch { /* not recorded at home */ }
|
|
542
|
+
}
|
|
543
|
+
let raw
|
|
544
|
+
try {
|
|
545
|
+
raw = fs.readFileSync(path.join(litmusDir, SESSION_OVER_MARKER), "utf8")
|
|
546
|
+
} catch {
|
|
547
|
+
return false
|
|
548
|
+
}
|
|
549
|
+
let marked = null
|
|
550
|
+
try {
|
|
551
|
+
const parsed = JSON.parse(raw)
|
|
552
|
+
if (parsed && typeof parsed.tokenSha256 === "string" && parsed.tokenSha256) marked = parsed.tokenSha256
|
|
553
|
+
} catch { /* names no token */ }
|
|
554
|
+
if (!marked) return true
|
|
555
|
+
return !current || current === marked
|
|
556
|
+
}
|
|
557
|
+
|
|
468
558
|
/**
|
|
469
559
|
* POST the event to /cli/activity and WAIT for it to complete, bounded.
|
|
470
560
|
*
|
|
@@ -474,16 +564,17 @@ function versionHeader(config) {
|
|
|
474
564
|
* the in-zip activity.jsonl) kept working, so nothing surfaced it.
|
|
475
565
|
*
|
|
476
566
|
* Resolves rather than rejects on every failure path: a telemetry upload must
|
|
477
|
-
* never fail a candidate's prompt. Both timers are cleared on settle.
|
|
567
|
+
* never fail a candidate's prompt. Both timers are cleared on settle. Resolves
|
|
568
|
+
* `{ sessionOver }`, true only when the server refused the token for good.
|
|
478
569
|
*/
|
|
479
570
|
function uploadEvent(config, event) {
|
|
480
|
-
if (!config || !config.token || !config.backendUrl) return Promise.resolve()
|
|
571
|
+
if (!config || !config.token || !config.backendUrl) return Promise.resolve({ sessionOver: false })
|
|
481
572
|
|
|
482
573
|
let url
|
|
483
574
|
try {
|
|
484
575
|
url = new URL(`${config.backendUrl}/cli/activity`)
|
|
485
576
|
} catch {
|
|
486
|
-
return Promise.resolve() // malformed backendUrl — nothing to do
|
|
577
|
+
return Promise.resolve({ sessionOver: false }) // malformed backendUrl — nothing to do
|
|
487
578
|
}
|
|
488
579
|
|
|
489
580
|
const body = JSON.stringify({ events: [event] })
|
|
@@ -492,11 +583,12 @@ function uploadEvent(config, event) {
|
|
|
492
583
|
return new Promise((resolve) => {
|
|
493
584
|
let settled = false
|
|
494
585
|
let guard = null
|
|
586
|
+
let sessionOver = false
|
|
495
587
|
const done = () => {
|
|
496
588
|
if (settled) return
|
|
497
589
|
settled = true
|
|
498
590
|
if (guard) clearTimeout(guard)
|
|
499
|
-
resolve()
|
|
591
|
+
resolve({ sessionOver })
|
|
500
592
|
}
|
|
501
593
|
|
|
502
594
|
const req = reqFn(url, {
|
|
@@ -518,7 +610,24 @@ function uploadEvent(config, event) {
|
|
|
518
610
|
timeout: UPLOAD_TIMEOUT_MS,
|
|
519
611
|
})
|
|
520
612
|
// Drain the response so the socket can close and the event loop empty.
|
|
521
|
-
|
|
613
|
+
// A 4xx body is kept (capped) because it is the only place the server says
|
|
614
|
+
// the assessment is over for good (ENG-2768, see isFinalRefusal).
|
|
615
|
+
req.on("response", (res) => {
|
|
616
|
+
const status = res.statusCode || 0
|
|
617
|
+
const chunks = []
|
|
618
|
+
let kept = 0
|
|
619
|
+
res.on("data", (chunk) => {
|
|
620
|
+
if (status >= 400 && status < 500 && kept < MAX_REFUSAL_BODY_BYTES) {
|
|
621
|
+
chunks.push(chunk)
|
|
622
|
+
kept += chunk.length
|
|
623
|
+
}
|
|
624
|
+
})
|
|
625
|
+
res.on("end", () => {
|
|
626
|
+
if (chunks.length) sessionOver = isFinalRefusal(status, Buffer.concat(chunks).toString("utf8"))
|
|
627
|
+
done()
|
|
628
|
+
})
|
|
629
|
+
res.on("error", done)
|
|
630
|
+
})
|
|
522
631
|
req.on("error", done) // network down, DNS, refused, destroyed
|
|
523
632
|
req.on("timeout", () => { req.destroy(); done() })
|
|
524
633
|
// Backstop in case none of the above fires (a socket wedged before connect).
|
|
@@ -1775,6 +1884,15 @@ async function main() {
|
|
|
1775
1884
|
try { parsed = JSON.parse(raw) } catch { /* extractPrompt reports the failure */ }
|
|
1776
1885
|
const cursorShaped = isCursorPayload(parsed)
|
|
1777
1886
|
|
|
1887
|
+
// ENG-2768: an assessment whose session is over records nothing more. Asked
|
|
1888
|
+
// here, before any transcript read, against the same placement the event
|
|
1889
|
+
// would get (the payload's workspace roots are the only roots any tool's
|
|
1890
|
+
// extractor reports), and asked again below once the event's own placement
|
|
1891
|
+
// is known.
|
|
1892
|
+
const earlyRoots = parsed && Array.isArray(parsed.workspace_roots) ? parsed.workspace_roots : []
|
|
1893
|
+
const earlyLitmusDir = resolveAssessmentDir(parsed, earlyRoots)
|
|
1894
|
+
if (earlyLitmusDir && sessionOverMarked(earlyLitmusDir)) return
|
|
1895
|
+
|
|
1778
1896
|
// ENG-2477: a capture SELF-TEST probe. `litmus init` (and `litmus doctor`)
|
|
1779
1897
|
// run this script the way the tool would — the tool's own payload shape, on
|
|
1780
1898
|
// stdin, through the command string in the tool's own config file — plus
|
|
@@ -1826,7 +1944,7 @@ async function main() {
|
|
|
1826
1944
|
if (effectiveTool === "cursor" && parsed && parsed.hook_event_name === CURSOR_EDIT_EVENT) {
|
|
1827
1945
|
const roots = Array.isArray(parsed.workspace_roots) ? parsed.workspace_roots : []
|
|
1828
1946
|
const editLitmusDir = resolveAssessmentDir(parsed, roots)
|
|
1829
|
-
if (!editLitmusDir) return
|
|
1947
|
+
if (!editLitmusDir || sessionOverMarked(editLitmusDir)) return
|
|
1830
1948
|
recordCursorFileEdit(parsed, parsed.session_id || parsed.conversation_id || null,
|
|
1831
1949
|
path.dirname(editLitmusDir))
|
|
1832
1950
|
return
|
|
@@ -1864,6 +1982,7 @@ async function main() {
|
|
|
1864
1982
|
|
|
1865
1983
|
const litmusDir = resolveAssessmentDir(parsed, workspaceRoots)
|
|
1866
1984
|
if (!litmusDir) return
|
|
1985
|
+
if (litmusDir !== earlyLitmusDir && sessionOverMarked(litmusDir)) return
|
|
1867
1986
|
|
|
1868
1987
|
// Claim as LATE as possible — only once we know we have content and somewhere
|
|
1869
1988
|
// to put it. Claiming earlier would let an invocation that then bails (no
|
|
@@ -1993,7 +2112,15 @@ async function main() {
|
|
|
1993
2112
|
|
|
1994
2113
|
// 2. Upload to the server. Awaited, bounded — see uploadEvent.
|
|
1995
2114
|
const config = readConfig(litmusDir)
|
|
1996
|
-
await uploadEvent(config, event)
|
|
2115
|
+
const upload = await uploadEvent(config, event)
|
|
2116
|
+
|
|
2117
|
+
// ENG-2768: the server says this token will never be accepted again. Mark
|
|
2118
|
+
// the folder so the next turn stops at the top of main(), and skip the
|
|
2119
|
+
// revival below: there is no session left for a watcher to serve.
|
|
2120
|
+
if (upload.sessionOver) {
|
|
2121
|
+
writeSessionOverMarker(litmusDir, config, "server refused the token")
|
|
2122
|
+
return
|
|
2123
|
+
}
|
|
1997
2124
|
|
|
1998
2125
|
// 3. Revive a dead watcher (ENG-1491). Strictly after the capture/upload
|
|
1999
2126
|
// work — this is a courtesy, and it must never cost the prompt anything.
|
|
@@ -2008,6 +2135,9 @@ async function main() {
|
|
|
2008
2135
|
// Exported for unit tests only; every hook invocation runs this file directly.
|
|
2009
2136
|
module.exports = {
|
|
2010
2137
|
watcherPresumedDead,
|
|
2138
|
+
isFinalRefusal,
|
|
2139
|
+
sessionOverMarked,
|
|
2140
|
+
SESSION_OVER_MARKER,
|
|
2011
2141
|
reviveThrottleAllows,
|
|
2012
2142
|
buildDrainFrame,
|
|
2013
2143
|
trimEventToBytes,
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ENG-2768: the "this session is over" marker the hook logger reads at the top
|
|
3
|
+
* of every invocation. Kept in step with `writeSessionOverMarker` and
|
|
4
|
+
* `sessionOverMarked` in hook-logger.cjs (that file is standalone CommonJS and
|
|
5
|
+
* cannot import this one): same names, same JSON shape.
|
|
6
|
+
*
|
|
7
|
+
* Two copies, either one enough. `.litmus/session-over` names the token as a
|
|
8
|
+
* sha256 hash, so a later `litmus init` into the same folder, which carries a
|
|
9
|
+
* new token, is not silenced by it. `~/.litmus/sessions-over/<hash>` is the
|
|
10
|
+
* same fact recorded outside the folder, for a folder that cannot be written.
|
|
11
|
+
*/
|
|
12
|
+
export declare const SESSION_OVER_MARKER = "session-over";
|
|
13
|
+
export declare function sessionsOverDir(): string;
|
|
14
|
+
export declare function writeSessionOverMarker(projectDir: string, token: string | undefined, reason: string): boolean;
|
|
15
|
+
/** Has the session this folder's config belongs to been marked over? */
|
|
16
|
+
export declare function sessionOverMarked(projectDir: string): boolean;
|
|
17
|
+
/**
|
|
18
|
+
* What the watcher does with its own AI capture when the server says the
|
|
19
|
+
* session is over. The marker silences the hook logger in this folder even
|
|
20
|
+
* while another assessment keeps the user-scope hooks installed; the release
|
|
21
|
+
* drops the registry entry, the folder's own hooks and trust entries, and the
|
|
22
|
+
* user-scope hooks once no assessment is left. Only `litmus submit` used to do
|
|
23
|
+
* the release, so a session that ended any other way kept a live hook.
|
|
24
|
+
*
|
|
25
|
+
* Run by the watcher because it is alive exactly when the session ends, so no
|
|
26
|
+
* hook writer races it (the ENG-1289 concurrency problem). Marker first: it
|
|
27
|
+
* alone already stops capture, so a release that throws halfway loses nothing.
|
|
28
|
+
* The marker also keeps startTracker from bringing a watcher back here, whose
|
|
29
|
+
* hook self-heal would otherwise reinstall everything this just released.
|
|
30
|
+
*/
|
|
31
|
+
export declare function retireAiCaptureForEndedSession(projectDir: string, token: string | undefined): void;
|
|
32
|
+
//# sourceMappingURL=session-over.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session-over.d.ts","sourceRoot":"","sources":["../../src/lib/session-over.ts"],"names":[],"mappings":"AAMA;;;;;;;;;;GAUG;AACH,eAAO,MAAM,mBAAmB,iBAAiB,CAAA;AAEjD,wBAAgB,eAAe,IAAI,MAAM,CAExC;AAMD,wBAAgB,sBAAsB,CAAC,UAAU,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,SAAS,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAmB7G;AAED,wEAAwE;AACxE,wBAAgB,iBAAiB,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAuB7D;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,8BAA8B,CAAC,UAAU,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,CAGlG"}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { mkdirSync, readFileSync, statSync, writeFileSync } from "fs";
|
|
2
|
+
import os from "os";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import { createHash } from "crypto";
|
|
5
|
+
import { releaseAiTrackingForAssessment } from "./ai-tracking.js";
|
|
6
|
+
/**
|
|
7
|
+
* ENG-2768: the "this session is over" marker the hook logger reads at the top
|
|
8
|
+
* of every invocation. Kept in step with `writeSessionOverMarker` and
|
|
9
|
+
* `sessionOverMarked` in hook-logger.cjs (that file is standalone CommonJS and
|
|
10
|
+
* cannot import this one): same names, same JSON shape.
|
|
11
|
+
*
|
|
12
|
+
* Two copies, either one enough. `.litmus/session-over` names the token as a
|
|
13
|
+
* sha256 hash, so a later `litmus init` into the same folder, which carries a
|
|
14
|
+
* new token, is not silenced by it. `~/.litmus/sessions-over/<hash>` is the
|
|
15
|
+
* same fact recorded outside the folder, for a folder that cannot be written.
|
|
16
|
+
*/
|
|
17
|
+
export const SESSION_OVER_MARKER = "session-over";
|
|
18
|
+
export function sessionsOverDir() {
|
|
19
|
+
return path.join(os.homedir() || os.tmpdir(), ".litmus", "sessions-over");
|
|
20
|
+
}
|
|
21
|
+
function tokenDigest(token) {
|
|
22
|
+
return typeof token === "string" && token ? createHash("sha256").update(token).digest("hex") : null;
|
|
23
|
+
}
|
|
24
|
+
export function writeSessionOverMarker(projectDir, token, reason) {
|
|
25
|
+
const digest = tokenDigest(token);
|
|
26
|
+
let written = false;
|
|
27
|
+
try {
|
|
28
|
+
writeFileSync(path.join(projectDir, ".litmus", SESSION_OVER_MARKER), JSON.stringify({
|
|
29
|
+
reason,
|
|
30
|
+
at: new Date().toISOString(),
|
|
31
|
+
tokenSha256: digest,
|
|
32
|
+
}) + "\n");
|
|
33
|
+
written = true;
|
|
34
|
+
}
|
|
35
|
+
catch { /* unwritable folder; the home copy below still counts */ }
|
|
36
|
+
if (digest) {
|
|
37
|
+
try {
|
|
38
|
+
mkdirSync(sessionsOverDir(), { recursive: true });
|
|
39
|
+
writeFileSync(path.join(sessionsOverDir(), digest), "");
|
|
40
|
+
written = true;
|
|
41
|
+
}
|
|
42
|
+
catch { /* best effort */ }
|
|
43
|
+
}
|
|
44
|
+
return written;
|
|
45
|
+
}
|
|
46
|
+
/** Has the session this folder's config belongs to been marked over? */
|
|
47
|
+
export function sessionOverMarked(projectDir) {
|
|
48
|
+
let current = null;
|
|
49
|
+
try {
|
|
50
|
+
current = tokenDigest(JSON.parse(readFileSync(path.join(projectDir, ".litmus", "config.json"), "utf8")).token);
|
|
51
|
+
}
|
|
52
|
+
catch { /* no readable config */ }
|
|
53
|
+
if (current) {
|
|
54
|
+
try {
|
|
55
|
+
if (statSync(path.join(sessionsOverDir(), current)).isFile())
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
58
|
+
catch { /* not recorded at home */ }
|
|
59
|
+
}
|
|
60
|
+
let raw;
|
|
61
|
+
try {
|
|
62
|
+
raw = readFileSync(path.join(projectDir, ".litmus", SESSION_OVER_MARKER), "utf8");
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
let marked = null;
|
|
68
|
+
try {
|
|
69
|
+
const parsed = JSON.parse(raw);
|
|
70
|
+
if (typeof parsed?.tokenSha256 === "string" && parsed.tokenSha256)
|
|
71
|
+
marked = parsed.tokenSha256;
|
|
72
|
+
}
|
|
73
|
+
catch { /* names no token */ }
|
|
74
|
+
if (!marked)
|
|
75
|
+
return true;
|
|
76
|
+
return !current || current === marked;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* What the watcher does with its own AI capture when the server says the
|
|
80
|
+
* session is over. The marker silences the hook logger in this folder even
|
|
81
|
+
* while another assessment keeps the user-scope hooks installed; the release
|
|
82
|
+
* drops the registry entry, the folder's own hooks and trust entries, and the
|
|
83
|
+
* user-scope hooks once no assessment is left. Only `litmus submit` used to do
|
|
84
|
+
* the release, so a session that ended any other way kept a live hook.
|
|
85
|
+
*
|
|
86
|
+
* Run by the watcher because it is alive exactly when the session ends, so no
|
|
87
|
+
* hook writer races it (the ENG-1289 concurrency problem). Marker first: it
|
|
88
|
+
* alone already stops capture, so a release that throws halfway loses nothing.
|
|
89
|
+
* The marker also keeps startTracker from bringing a watcher back here, whose
|
|
90
|
+
* hook self-heal would otherwise reinstall everything this just released.
|
|
91
|
+
*/
|
|
92
|
+
export function retireAiCaptureForEndedSession(projectDir, token) {
|
|
93
|
+
writeSessionOverMarker(projectDir, token, "watcher: server signaled session over");
|
|
94
|
+
try {
|
|
95
|
+
releaseAiTrackingForAssessment(projectDir);
|
|
96
|
+
}
|
|
97
|
+
catch { /* best effort */ }
|
|
98
|
+
}
|
|
99
|
+
//# sourceMappingURL=session-over.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session-over.js","sourceRoot":"","sources":["../../src/lib/session-over.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ENG-2814 — find the candidate's own AI tool skills on THIS machine, for `litmus
|
|
3
|
+
* connect` to offer to import into their workspace.
|
|
4
|
+
*
|
|
5
|
+
* Runs on the laptop because that is the only place the skills are: the workspace
|
|
6
|
+
* cannot see the candidate's home. The upload goes to the same `/workspace/skills`
|
|
7
|
+
* routes the assessment page's Import skills dialog uses, and NOTHING about which
|
|
8
|
+
* files are skills is decided here: `POST /workspace/skills/plan` runs the upload's
|
|
9
|
+
* own parser over the paths and SKILL.md heads and answers with the files to send
|
|
10
|
+
* (`backend/services/candidate_skills.py`). One parser cannot disagree with itself.
|
|
11
|
+
*
|
|
12
|
+
* Every folder an AI tool reads Agent Skills (`<name>/SKILL.md`) from is looked in.
|
|
13
|
+
* A folder that does not exist costs nothing; a skill found in two of them (a
|
|
14
|
+
* `.claude/skills` copy of an `.agents/skills` one, say) is imported once, and the
|
|
15
|
+
* server reports the other as a duplicate.
|
|
16
|
+
*/
|
|
17
|
+
export declare const SKILL_SOURCES: ReadonlyArray<{
|
|
18
|
+
dir: string;
|
|
19
|
+
tools: string;
|
|
20
|
+
}>;
|
|
21
|
+
export declare const PLAN_HEAD_CHARS: number;
|
|
22
|
+
export interface LocalSkillFile {
|
|
23
|
+
/** Sent to the server. The source folder without its dot (`claude/skills/x/SKILL.md`):
|
|
24
|
+
* the server skips hidden folders other than the ones it knows hold skills, and
|
|
25
|
+
* `.cursor` or `.gemini` are not among them. */
|
|
26
|
+
path: string;
|
|
27
|
+
abs: string;
|
|
28
|
+
size: number;
|
|
29
|
+
mtimeMs: number;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Every file under each skills folder that exists.
|
|
33
|
+
*
|
|
34
|
+
* A link is followed only where it stays inside what the candidate agreed to share.
|
|
35
|
+
* An entry directly under a skills folder may be a link (a skill kept in dotfiles and
|
|
36
|
+
* linked in is common), and it sets the boundary: below it, a file or folder whose
|
|
37
|
+
* real path leaves that entry is not collected, so a link in a skill cannot carry an
|
|
38
|
+
* unrelated file off the laptop under the skill's name.
|
|
39
|
+
*/
|
|
40
|
+
export declare function collectSkillFiles(home?: string): LocalSkillFile[];
|
|
41
|
+
export declare const MAX_UPLOAD_BYTES: number;
|
|
42
|
+
export declare const MAX_UPLOAD_FILES = 1000;
|
|
43
|
+
/** Why these files cannot be uploaded in one import, or null when they fit. */
|
|
44
|
+
export declare function overUploadCaps(files: LocalSkillFile[]): string | null;
|
|
45
|
+
export interface SkillPlanRequest {
|
|
46
|
+
/** Leave out a skill the server cannot read and name it in `refused`, rather than
|
|
47
|
+
* refusing every skill over one malformed SKILL.md. */
|
|
48
|
+
skipInvalid: true;
|
|
49
|
+
paths: string[];
|
|
50
|
+
skillFiles: Record<string, string>;
|
|
51
|
+
unreadable: string[];
|
|
52
|
+
truncated: string[];
|
|
53
|
+
}
|
|
54
|
+
/** The plan route's body: every path, and the head of each SKILL.md. */
|
|
55
|
+
export declare function planRequest(files: LocalSkillFile[]): SkillPlanRequest;
|
|
56
|
+
/** The skill folder names among the paths the server said to send, for the prompt. */
|
|
57
|
+
export declare function skillNames(kept: string[]): string[];
|
|
58
|
+
/**
|
|
59
|
+
* What the candidate was last asked about, per assessment, so a candidate who
|
|
60
|
+
* reconnects every day of a work trial is asked again only when their skills changed.
|
|
61
|
+
* A digest of the kept files' paths, sizes and modification times: cheap, and any
|
|
62
|
+
* edit to a skill changes it.
|
|
63
|
+
*/
|
|
64
|
+
export declare function skillSetDigest(files: LocalSkillFile[]): string;
|
|
65
|
+
export declare function lastAnswered(assessmentId: string, home?: string): string | null;
|
|
66
|
+
/** Best effort: failing to remember only means being asked again next time. */
|
|
67
|
+
export declare function rememberAnswered(assessmentId: string, digest: string, home?: string): void;
|
|
68
|
+
export interface SkillImportResult {
|
|
69
|
+
installed: string[];
|
|
70
|
+
skipped: {
|
|
71
|
+
name: string;
|
|
72
|
+
dest: string;
|
|
73
|
+
reason: string;
|
|
74
|
+
}[];
|
|
75
|
+
ignored: string[];
|
|
76
|
+
duplicates: string[];
|
|
77
|
+
aider: string | null;
|
|
78
|
+
}
|
|
79
|
+
/** What the import did, in the same words the assessment page's dialog used. */
|
|
80
|
+
export declare function importSummaryLines(result: SkillImportResult): string[];
|
|
81
|
+
export declare const SKILLS_PICKUP = "In Copilot, run /skills reload.";
|
|
82
|
+
//# sourceMappingURL=skill-import.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"skill-import.d.ts","sourceRoot":"","sources":["../../src/lib/skill-import.ts"],"names":[],"mappings":"AAKA;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,aAAa,EAAE,aAAa,CAAC;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAOvE,CAAA;AAUD,eAAO,MAAM,eAAe,QAAY,CAAA;AAExC,MAAM,WAAW,cAAc;IAC7B;;qDAEiD;IACjD,IAAI,EAAE,MAAM,CAAA;IACZ,GAAG,EAAE,MAAM,CAAA;IACX,IAAI,EAAE,MAAM,CAAA;IACZ,OAAO,EAAE,MAAM,CAAA;CAChB;AAQD;;;;;;;;GAQG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,GAAE,MAAqB,GAAG,cAAc,EAAE,CAiD/E;AAID,eAAO,MAAM,gBAAgB,QAAmB,CAAA;AAChD,eAAO,MAAM,gBAAgB,OAAO,CAAA;AAEpC,+EAA+E;AAC/E,wBAAgB,cAAc,CAAC,KAAK,EAAE,cAAc,EAAE,GAAG,MAAM,GAAG,IAAI,CAKrE;AAED,MAAM,WAAW,gBAAgB;IAC/B;4DACwD;IACxD,WAAW,EAAE,IAAI,CAAA;IACjB,KAAK,EAAE,MAAM,EAAE,CAAA;IACf,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAClC,UAAU,EAAE,MAAM,EAAE,CAAA;IACpB,SAAS,EAAE,MAAM,EAAE,CAAA;CACpB;AAED,wEAAwE;AACxE,wBAAgB,WAAW,CAAC,KAAK,EAAE,cAAc,EAAE,GAAG,gBAAgB,CAkBrE;AAED,sFAAsF;AACtF,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,MAAM,EAAE,CAOnD;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,cAAc,EAAE,GAAG,MAAM,CAM9D;AAMD,wBAAgB,YAAY,CAAC,YAAY,EAAE,MAAM,EAAE,IAAI,GAAE,MAAqB,GAAG,MAAM,GAAG,IAAI,CAQ7F;AAED,+EAA+E;AAC/E,wBAAgB,gBAAgB,CAAC,YAAY,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,GAAE,MAAqB,GAAG,IAAI,CAexG;AAED,MAAM,WAAW,iBAAiB;IAChC,SAAS,EAAE,MAAM,EAAE,CAAA;IACnB,OAAO,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,EAAE,CAAA;IACzD,OAAO,EAAE,MAAM,EAAE,CAAA;IACjB,UAAU,EAAE,MAAM,EAAE,CAAA;IACpB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;CACrB;AAQD,gFAAgF;AAChF,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,iBAAiB,GAAG,MAAM,EAAE,CAgBtE;AAED,eAAO,MAAM,aAAa,oCAAoC,CAAA"}
|