transcript-viewer 0.6.2__tar.gz → 0.7.1__tar.gz
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.
- {transcript_viewer-0.6.2 → transcript_viewer-0.7.1}/PKG-INFO +1 -1
- {transcript_viewer-0.6.2 → transcript_viewer-0.7.1}/pyproject.toml +1 -1
- {transcript_viewer-0.6.2 → transcript_viewer-0.7.1}/src/transcript_viewer/page.html +208 -10
- transcript_viewer-0.7.1/tests/fixtures/big-command.json +1 -0
- {transcript_viewer-0.6.2 → transcript_viewer-0.7.1}/tests/page.test.js +193 -0
- {transcript_viewer-0.6.2 → transcript_viewer-0.7.1}/uv.lock +1 -1
- {transcript_viewer-0.6.2 → transcript_viewer-0.7.1}/.coverage +0 -0
- {transcript_viewer-0.6.2 → transcript_viewer-0.7.1}/.github/workflows/publish.yml +0 -0
- {transcript_viewer-0.6.2 → transcript_viewer-0.7.1}/.github/workflows/test.yml +0 -0
- {transcript_viewer-0.6.2 → transcript_viewer-0.7.1}/.gitignore +0 -0
- {transcript_viewer-0.6.2 → transcript_viewer-0.7.1}/README.md +0 -0
- {transcript_viewer-0.6.2 → transcript_viewer-0.7.1}/src/transcript_viewer/__init__.py +0 -0
- {transcript_viewer-0.6.2 → transcript_viewer-0.7.1}/src/transcript_viewer/ai.py +0 -0
- {transcript_viewer-0.6.2 → transcript_viewer-0.7.1}/src/transcript_viewer/cli.py +0 -0
- {transcript_viewer-0.6.2 → transcript_viewer-0.7.1}/src/transcript_viewer/config.py +0 -0
- {transcript_viewer-0.6.2 → transcript_viewer-0.7.1}/src/transcript_viewer/corpus.py +0 -0
- {transcript_viewer-0.6.2 → transcript_viewer-0.7.1}/src/transcript_viewer/fetch.py +0 -0
- {transcript_viewer-0.6.2 → transcript_viewer-0.7.1}/src/transcript_viewer/library.py +0 -0
- {transcript_viewer-0.6.2 → transcript_viewer-0.7.1}/src/transcript_viewer/store.py +0 -0
- {transcript_viewer-0.6.2 → transcript_viewer-0.7.1}/src/transcript_viewer/viewer.py +0 -0
- {transcript_viewer-0.6.2 → transcript_viewer-0.7.1}/tests/test_ai.py +0 -0
- {transcript_viewer-0.6.2 → transcript_viewer-0.7.1}/tests/test_cli.py +0 -0
- {transcript_viewer-0.6.2 → transcript_viewer-0.7.1}/tests/test_config.py +0 -0
- {transcript_viewer-0.6.2 → transcript_viewer-0.7.1}/tests/test_corpus.py +0 -0
- {transcript_viewer-0.6.2 → transcript_viewer-0.7.1}/tests/test_fetch.py +0 -0
- {transcript_viewer-0.6.2 → transcript_viewer-0.7.1}/tests/test_library.py +0 -0
- {transcript_viewer-0.6.2 → transcript_viewer-0.7.1}/tests/test_page.py +0 -0
- {transcript_viewer-0.6.2 → transcript_viewer-0.7.1}/tests/test_readme.py +0 -0
- {transcript_viewer-0.6.2 → transcript_viewer-0.7.1}/tests/test_store.py +0 -0
- {transcript_viewer-0.6.2 → transcript_viewer-0.7.1}/tests/test_stream.py +0 -0
- {transcript_viewer-0.6.2 → transcript_viewer-0.7.1}/tests/test_style.py +0 -0
- {transcript_viewer-0.6.2 → transcript_viewer-0.7.1}/tests/test_tidiness.py +0 -0
- {transcript_viewer-0.6.2 → transcript_viewer-0.7.1}/tests/test_viewer.py +0 -0
|
@@ -36,6 +36,8 @@
|
|
|
36
36
|
--user:var(--accent); --agent:var(--ink); --system:var(--muted); --tool:var(--accent);
|
|
37
37
|
--user-bg:var(--soft); --agent-bg:transparent; --system-bg:transparent;
|
|
38
38
|
--j-key:var(--accent); --j-str:var(--ink); --j-num:var(--muted); --j-lit:var(--muted);
|
|
39
|
+
--sh-cmd:var(--accent); --sh-str:var(--ink); --sh-flag:var(--muted);
|
|
40
|
+
--sh-op:var(--muted); --sh-var:var(--accent); --sh-com:var(--faint);
|
|
39
41
|
}
|
|
40
42
|
*{box-sizing:border-box;margin:0}
|
|
41
43
|
a{color:var(--accent);text-decoration:none}
|
|
@@ -409,6 +411,29 @@ pre{background:var(--sunk);border-radius:7px;padding:10px;overflow:auto;font-siz
|
|
|
409
411
|
|
|
410
412
|
/* JSON syntax colours */
|
|
411
413
|
.j-key{color:var(--j-key)}
|
|
414
|
+
/* A command reads as a command: one gutter mark, then the script as written. */
|
|
415
|
+
.sh{background:var(--bg);border-radius:7px;padding:10px 12px 10px 26px;margin:0;
|
|
416
|
+
position:relative;font:12px/1.6 var(--font-mono);color:var(--ink);
|
|
417
|
+
/* Wrapped rather than scrolled: a command is read, and a line that runs off
|
|
418
|
+
the side is a line nobody reads. A continued line is indented so it stays
|
|
419
|
+
obvious which lines the shell would actually see as separate. */
|
|
420
|
+
white-space:pre-wrap;overflow-wrap:anywhere;text-indent:-1.4em;padding-left:calc(26px + 1.4em)}
|
|
421
|
+
.sh::before{content:"$";position:absolute;left:11px;top:10px;color:var(--faint);
|
|
422
|
+
user-select:none;text-indent:0}
|
|
423
|
+
.sh-cmd{color:var(--sh-cmd)}
|
|
424
|
+
.sh-str{color:var(--sh-str)}
|
|
425
|
+
.sh-flag{color:var(--sh-flag)}
|
|
426
|
+
.sh-op{color:var(--sh-op);font-weight:500}
|
|
427
|
+
.sh-var{color:var(--sh-var)}
|
|
428
|
+
.sh-com{color:var(--sh-com);font-style:italic}
|
|
429
|
+
/* What the call was for, beside what it was called. */
|
|
430
|
+
.tdesc{color:var(--muted);font-size:12px;overflow:hidden;text-overflow:ellipsis;
|
|
431
|
+
white-space:nowrap;flex:1;min-width:0}
|
|
432
|
+
.tflip{margin-left:auto;flex:none;font:11px/1 var(--font-mono);color:var(--faint);
|
|
433
|
+
border:1px solid var(--line);border-radius:20px;padding:3px 8px;cursor:pointer}
|
|
434
|
+
.tflip:hover{color:var(--ink);border-color:var(--accent)}
|
|
435
|
+
.turl{display:block;margin:0 0 8px;font-size:12.5px;overflow-wrap:anywhere}
|
|
436
|
+
.tasked{color:var(--muted);font-size:12.5px;line-height:1.55}
|
|
412
437
|
.j-str{color:var(--j-str)}
|
|
413
438
|
.j-num{color:var(--j-num)}
|
|
414
439
|
.j-lit{color:var(--j-lit)}
|
|
@@ -768,6 +793,170 @@ const escText = (s) =>
|
|
|
768
793
|
|
|
769
794
|
// Colourise a JSON value. Runs over already-escaped text, so nothing it emits
|
|
770
795
|
// can introduce markup.
|
|
796
|
+
/* A shell command, coloured. Every piece is escaped as it is taken, so nothing
|
|
797
|
+
in a log can reach the DOM as markup, and the result is passed through
|
|
798
|
+
autolink so paths and URLs inside a command stay clickable. */
|
|
799
|
+
const SH_OPS = "|&;<>()";
|
|
800
|
+
|
|
801
|
+
function sh(src) {
|
|
802
|
+
const s = String(src ?? "");
|
|
803
|
+
let out = "",
|
|
804
|
+
i = 0,
|
|
805
|
+
// Whether the next word is the thing being run, rather than an argument.
|
|
806
|
+
fresh = true;
|
|
807
|
+
const put = (cls, text) => {
|
|
808
|
+
out += cls ? `<span class="${cls}">${esc(text)}</span>` : esc(text);
|
|
809
|
+
};
|
|
810
|
+
|
|
811
|
+
while (i < s.length) {
|
|
812
|
+
const ch = s[i];
|
|
813
|
+
|
|
814
|
+
if (ch === "\n") { out += "\n"; i++; fresh = true; continue; }
|
|
815
|
+
if (ch === " " || ch === "\t") { out += ch; i++; continue; }
|
|
816
|
+
|
|
817
|
+
// A # only starts a comment where a word could start.
|
|
818
|
+
if (ch === "#" && fresh) {
|
|
819
|
+
let j = s.indexOf("\n", i);
|
|
820
|
+
if (j < 0) j = s.length;
|
|
821
|
+
put("sh-com", s.slice(i, j));
|
|
822
|
+
i = j;
|
|
823
|
+
continue;
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
if (ch === "'" || ch === '"') {
|
|
827
|
+
let j = i + 1;
|
|
828
|
+
while (j < s.length) {
|
|
829
|
+
if (s[j] === "\\" && ch === '"') { j += 2; continue; }
|
|
830
|
+
if (s[j] === ch) { j++; break; }
|
|
831
|
+
j++;
|
|
832
|
+
}
|
|
833
|
+
put("sh-str", s.slice(i, j));
|
|
834
|
+
i = j;
|
|
835
|
+
fresh = false;
|
|
836
|
+
continue;
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
if (ch === "$") {
|
|
840
|
+
// $(…) is a command substitution: mark the opening and let what is inside
|
|
841
|
+
// be scanned as the command it is.
|
|
842
|
+
if (s[i + 1] === "(") {
|
|
843
|
+
put("sh-var", "$(");
|
|
844
|
+
i += 2;
|
|
845
|
+
fresh = true;
|
|
846
|
+
continue;
|
|
847
|
+
}
|
|
848
|
+
const m = /^\$(?:\{[^}]*\}|[A-Za-z_]\w*|[@*?#$!0-9])/.exec(s.slice(i, i + 200));
|
|
849
|
+
if (m) { put("sh-var", m[0]); i += m[0].length; fresh = false; continue; }
|
|
850
|
+
// A lone $ is just a character. Said explicitly, because falling through
|
|
851
|
+
// to the word reader below leaves it unable to move.
|
|
852
|
+
put("", "$");
|
|
853
|
+
i++;
|
|
854
|
+
fresh = false;
|
|
855
|
+
continue;
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
if (SH_OPS.includes(ch)) {
|
|
859
|
+
let j = i;
|
|
860
|
+
while (j < s.length && SH_OPS.includes(s[j])) j++;
|
|
861
|
+
const op = s.slice(i, j);
|
|
862
|
+
put("sh-op", op);
|
|
863
|
+
i = j;
|
|
864
|
+
// What follows a pipe or a semicolon is another command; what follows a
|
|
865
|
+
// redirect is the file it is being written to, which is not one.
|
|
866
|
+
fresh = !/[<>]/.test(op);
|
|
867
|
+
continue;
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
let j = i;
|
|
871
|
+
while (j < s.length && !" \t\n'\"$".includes(s[j]) && !SH_OPS.includes(s[j])) j++;
|
|
872
|
+
// Every character this reaches must be consumed by something. If a branch
|
|
873
|
+
// above ever stops matching a character it used to, this keeps the loop
|
|
874
|
+
// moving instead of hanging the page — which is what it did, on any command
|
|
875
|
+
// containing $( .
|
|
876
|
+
if (j === i) j = i + 1;
|
|
877
|
+
const word = s.slice(i, j);
|
|
878
|
+
put(fresh ? "sh-cmd" : word[0] === "-" ? "sh-flag" : "", word);
|
|
879
|
+
i = j;
|
|
880
|
+
fresh = false;
|
|
881
|
+
}
|
|
882
|
+
return out;
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
const shBlock = (command) => `<pre class="sh">${autolink(sh(command))}</pre>`;
|
|
886
|
+
|
|
887
|
+
/* What a call is worth showing as, when it is not worth showing as JSON. Only
|
|
888
|
+
the shapes that carry a command or an address get a body of their own; the
|
|
889
|
+
rest gain a title, which is the part that was missing everywhere. */
|
|
890
|
+
const TOOLVIEW = {
|
|
891
|
+
Bash: {
|
|
892
|
+
title: (a) => a.description || "",
|
|
893
|
+
body: (a) =>
|
|
894
|
+
shBlock(a.command || "") +
|
|
895
|
+
(a.timeout ? `<div class="tasked">timeout ${num(a.timeout)} ms</div>` : ""),
|
|
896
|
+
},
|
|
897
|
+
WebFetch: {
|
|
898
|
+
title: (a) => {
|
|
899
|
+
try { return new URL(a.url).hostname.replace(/^www\./, ""); }
|
|
900
|
+
catch (e) { return a.url || ""; }
|
|
901
|
+
},
|
|
902
|
+
body: (a) =>
|
|
903
|
+
`<div class="turl">${autolink(esc(a.url || ""))}</div>` +
|
|
904
|
+
(a.prompt ? `<div class="tasked">${esc(a.prompt)}</div>` : ""),
|
|
905
|
+
},
|
|
906
|
+
WebSearch: { title: (a) => a.query || "" },
|
|
907
|
+
// The end of a path is the part worth reading; the full one is on hover.
|
|
908
|
+
Read: { title: (a) => short(a.file_path) },
|
|
909
|
+
Write: { title: (a) => short(a.file_path) },
|
|
910
|
+
Edit: { title: (a) => short(a.file_path) },
|
|
911
|
+
NotebookEdit: { title: (a) => short(a.notebook_path) },
|
|
912
|
+
Skill: { title: (a) => a.skill || "" },
|
|
913
|
+
Task: { title: (a) => a.description || "" },
|
|
914
|
+
TaskCreate: { title: (a) => a.subject || a.description || "" },
|
|
915
|
+
ToolSearch: { title: (a) => a.query || "" },
|
|
916
|
+
Grep: { title: (a) => a.pattern || "" },
|
|
917
|
+
Glob: { title: (a) => a.pattern || "" },
|
|
918
|
+
};
|
|
919
|
+
|
|
920
|
+
/* Every call rendered so far, so one box can be redrawn without repainting the
|
|
921
|
+
page and losing the reader's place. */
|
|
922
|
+
const CALLS = {};
|
|
923
|
+
// Boxes the reader has flipped. Absent means "do as the page is doing".
|
|
924
|
+
const TOOLRAW = {};
|
|
925
|
+
|
|
926
|
+
const toolIsRaw = (id) => (id in TOOLRAW ? TOOLRAW[id] : raw);
|
|
927
|
+
|
|
928
|
+
function toolBody(id) {
|
|
929
|
+
const held = CALLS[id];
|
|
930
|
+
if (!held) return "";
|
|
931
|
+
const { c, r } = held;
|
|
932
|
+
const view = TOOLVIEW[c.function_name];
|
|
933
|
+
const shown =
|
|
934
|
+
!toolIsRaw(id) && view && view.body
|
|
935
|
+
? view.body(c.arguments || {})
|
|
936
|
+
: `<pre class="json">${hjson(c.arguments)}</pre>`;
|
|
937
|
+
return (
|
|
938
|
+
`<div class="io in"><span class="iol">call</span>${shown}</div>` +
|
|
939
|
+
(r && r.content
|
|
940
|
+
? `<div class="io out"><span class="iol">output</span>${
|
|
941
|
+
typeof r.content === "string" ? pre(r.content) : body(r.content)
|
|
942
|
+
}</div>`
|
|
943
|
+
: "") +
|
|
944
|
+
(aiOn() ? callSummary(c.tool_call_id) : "")
|
|
945
|
+
);
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
/* Flip one box between what it says and what it is. Redrawing just this box
|
|
949
|
+
keeps every other box open and the page where the reader left it. */
|
|
950
|
+
function flipTool(ev, id) {
|
|
951
|
+
ev.preventDefault();
|
|
952
|
+
ev.stopPropagation();
|
|
953
|
+
TOOLRAW[id] = !toolIsRaw(id);
|
|
954
|
+
const box = document.getElementById("tb-" + id);
|
|
955
|
+
if (box) box.innerHTML = toolBody(id);
|
|
956
|
+
const flip = document.getElementById("tf-" + id);
|
|
957
|
+
if (flip) flip.textContent = toolIsRaw(id) ? "formatted" : "raw";
|
|
958
|
+
}
|
|
959
|
+
|
|
771
960
|
function hjson(value, indent = 2) {
|
|
772
961
|
let text;
|
|
773
962
|
try {
|
|
@@ -1921,7 +2110,12 @@ function pick(key){
|
|
|
1921
2110
|
|
|
1922
2111
|
const toggle=r=>{show[r]=!show[r];limit=PAGE_SIZE;render()};
|
|
1923
2112
|
const more=()=>{limit+=PAGE_SIZE*4;render()};
|
|
1924
|
-
const toggleRaw=()=>{
|
|
2113
|
+
const toggleRaw=()=>{
|
|
2114
|
+
raw=!raw;
|
|
2115
|
+
// Boxes flipped by hand were disagreeing with the old setting, not this one.
|
|
2116
|
+
for(const id of Object.keys(TOOLRAW))delete TOOLRAW[id];
|
|
2117
|
+
render();
|
|
2118
|
+
};
|
|
1925
2119
|
|
|
1926
2120
|
/* Open or close every tool call and branch at once. Applied to the elements
|
|
1927
2121
|
already on screen rather than by repainting: a repaint would drop the reader
|
|
@@ -2096,7 +2290,8 @@ function trajectoryTab(t,e,branches,branchSteps,counts,q){
|
|
|
2096
2290
|
<span class="fsep"></span>
|
|
2097
2291
|
<span class="chip${OPEN_ALL?" on":""}" id="xall" onclick="toggleExpand()">${
|
|
2098
2292
|
OPEN_ALL?"collapse all":"expand all"}</span>
|
|
2099
|
-
<span class="chip${raw?" on":""}" onclick="toggleRaw()"
|
|
2293
|
+
<span class="chip${raw?" on":""}" onclick="toggleRaw()"
|
|
2294
|
+
title="Show messages and calls as they were recorded. A single box can still disagree.">raw</span>
|
|
2100
2295
|
${q?`<span class="hits">${num(total)} of ${num(t.steps.length)} match “${esc(query)}”</span>`:""}
|
|
2101
2296
|
</div>
|
|
2102
2297
|
${shown.map(({s,i})=>step(s,t,0,i,"")).join("")
|
|
@@ -2324,15 +2519,18 @@ function step(s,ctx,depth,idx,prefix){
|
|
|
2324
2519
|
for(const c of s.tool_calls||[]){
|
|
2325
2520
|
const r=(s.observation?.results||[]).find(x=>x.source_call_id===c.tool_call_id);
|
|
2326
2521
|
const refs=r?.subagent_trajectory_ref||[];
|
|
2522
|
+
// An id of its own, so one box can be redrawn without repainting the page.
|
|
2523
|
+
// A call recorded without one still needs somewhere to be kept.
|
|
2524
|
+
const id=String(c.tool_call_id||`${key}c${Object.keys(CALLS).length}`);
|
|
2525
|
+
CALLS[id]={c,r};
|
|
2526
|
+
const view=TOOLVIEW[c.function_name];
|
|
2527
|
+
const said=view&&view.title?String(view.title(c.arguments||{})||""):"";
|
|
2327
2528
|
h+=`<details class="tool"${OPEN_ALL?" open":""}><summary><span class="tname">${esc(c.function_name)}</span>
|
|
2328
|
-
${
|
|
2329
|
-
|
|
2330
|
-
<
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
${typeof r.content==="string"?pre(r.content):body(r.content)}</div>`:""}
|
|
2334
|
-
${aiOn()?callSummary(c.tool_call_id):""}
|
|
2335
|
-
</div></details>`;
|
|
2529
|
+
${said?`<span class="tdesc" title="${esc(said)}">${esc(said)}</span>`:""}
|
|
2530
|
+
${refs.length?`<span class="pill br">delegates</span>`:""}
|
|
2531
|
+
<span class="tflip" id="tf-${esc(id)}" onclick="flipTool(event,'${esc(id)}')"
|
|
2532
|
+
title="Show this call as it was recorded">${toolIsRaw(id)?"formatted":"raw"}</span></summary>
|
|
2533
|
+
<div class="tbody" id="tb-${esc(id)}">${toolBody(id)}</div></details>`;
|
|
2336
2534
|
for(const ref of refs)h+=branch(ref,ctx,depth);
|
|
2337
2535
|
}
|
|
2338
2536
|
return h+`</div>`;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"cat > /Users/girish/Documents/personalwebsite/website/public/check-my-mac.sh <<'SCRIPT_EOF'\n#!/bin/bash\n#\n# check-my-mac.sh \u2014 check the parts of a personal security setup that live on\n# your Mac, and print a report you can paste back into the checklist at\n# https://girishgupta.com/beyond-the-parrot\n#\n# bash check-my-mac.sh readable summary\n# bash check-my-mac.sh --json JSON, for pasting into the checklist\n#\n# WHAT IT DOES AND DOESN'T DO\n# It reads settings and reports them. That's all. It changes nothing,\n# installs nothing, needs no sudo, and sends nothing anywhere \u2014 there is no\n# network call in this file, and you can check that yourself.\n#\n# It can see whether FileVault is on. It cannot see whether you froze your\n# credit, enrolled a hardware key, or turned on a port-out PIN. Those are\n# the items that matter most, and they stay manual on the checklist.\n#\n# Checks that would need root report \"unknown\" rather than asking for your\n# password. Nothing here should ever prompt for it. If something does, stop\n# and work out why.\n#\n# Exit status: 0 if nothing failed, 1 if at least one check failed.\n\nset -uo pipefail\n\nJSON_ONLY=0\n[[ \"${1:-}\" == \"--json\" ]] && JSON_ONLY=1\n\nPASS=0\nFAIL=0\nUNKNOWN=0\nROWS=()\n\nrecord() {\n local key=\"$1\" status=\"$2\" detail=\"$3\"\n case \"$status\" in\n pass) PASS=$((PASS + 1)) ;;\n fail) FAIL=$((FAIL + 1)) ;;\n *) UNKNOWN=$((UNKNOWN + 1)); status=\"unknown\" ;;\n esac\n local esc=${detail//\\\\/\\\\\\\\}\n esc=${esc//\\\"/\\\\\\\"}\n esc=$(printf '%s' \"$esc\" | tr -d '\\000-\\037')\n ROWS+=(\" \\\"$key\\\": { \\\"status\\\": \\\"$status\\\", \\\"detail\\\": \\\"$esc\\\" }\")\n if [[ $JSON_ONLY -eq 0 ]]; then\n local mark\n case \"$status\" in\n pass) mark=\" ok \" ;;\n fail) mark=\"FAIL\" ;;\n unknown) mark=\" ? \" ;;\n esac\n printf '[%s] %-18s %s\\n' \"$mark\" \"$key\" \"$detail\"\n fi\n}\n\nhave() { command -v \"$1\" >/dev/null 2>&1; }\n\nNOW=$(date +%s)\n\n# Days between a unix timestamp and now. Prints nothing if given nothing.\ndays_since() {\n local then=\"${1:-}\"\n [[ -n \"$then\" && \"$then\" =~ ^[0-9]+$ ]] || return 1\n echo $(( (NOW - then) / 86400 ))\n}\n\nif [[ \"$(uname -s)\" != \"Darwin\" ]]; then\n echo \"This one only knows how to check macOS. Detected: $(uname -s)\" >&2\n exit 2\nfi\n\n[[ $JSON_ONLY -eq 0 ]] && {\n echo\n echo \"Checking $(sw_vers -productName) $(sw_vers -productVersion) on $(scutil --get ComputerName 2>/dev/null || hostname -s)\"\n echo\n}\n\n# \u2500\u2500 Disk encryption \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n# Without this, your laptop is a filing cabinet with the key taped to it.\nfv=\"$(fdesetup status 2>/dev/null)\"\nif grep -qi \"FileVault is On\" <<<\"$fv\"; then\n record \"encryption\" pass \"FileVault is on\"\nelif [[ -n \"$fv\" ]]; then\n record \"encryption\" fail \"$fv \u2014 turn it on in System Settings > Privacy & Security\"\nelse\n record \"encryption\" unknown \"could not read FileVault status\"\nfi\n\n# \u2500\u2500 Screen lock \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n# Two settings combine: how long until the screen sleeps, and the grace\n# period before a password is demanded. A short screensaver with a ten\n# minute grace period is not a lock.\nidle=$(defaults -currentHost read com.apple.screensaver idleTime 2>/dev/null)\nlock_raw=$(sysadminctl -screenLock status 2>&1 | tail -1)\ngrace=\"\"\ncase \"$lock_raw\" in\n *immediate*) grace=0 ;;\n *\"is off\"*) grace=off ;;\n *[0-9]*seconds*) grace=$(grep -oE \"[0-9]+\" <<<\"$lock_raw\" | head -1) ;;\nesac\nif [[ \"$grace\" == \"off\" ]]; then\n record \"screenlock\" fail \"no password is required after the screen sleeps\"\nelif [[ -n \"$idle\" && \"$idle\" =~ ^[0-9]+$ && \"$idle\" -eq 0 ]]; then\n record \"screenlock\" fail \"the screen never sleeps on its own, so it never locks\"\nelif [[ -n \"$grace\" && -n \"$idle\" && \"$idle\" =~ ^[0-9]+$ ]]; then\n total=$((idle + grace))\n if [[ \"$total\" -le 300 ]]; then\n record \"screenlock\" pass \"locks after ${total}s (${idle}s idle + ${grace}s grace)\"\n else\n record \"screenlock\" fail \"takes ${total}s to lock \u2014 aim for five minutes or less\"\n fi\nelse\n record \"screenlock\" unknown \"could not read the screen lock settings\"\nfi\n\n# \u2500\u2500 Automatic login \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n# macOS can be set to log someone in at boot without asking for anything,\n# which turns a stolen laptop into an open laptop. FileVault mostly\n# suppresses it, so this matters most on machines without FileVault.\nautologin=$(defaults read /Library/Preferences/com.apple.loginwindow autoLoginUser 2>/dev/null)\nif [[ -n \"$autologin\" ]]; then\n record \"auto-login\" fail \"this Mac logs in as '$autologin' at boot without a password\"\nelse\n record \"auto-login\" pass \"no automatic login at boot\"\nfi\n\n# \u2500\u2500 Automatic updates \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n# Read the whole domain: individual keys are often absent, which means\n# \"left at the default\" (on), not \"off\". Then check the setting is actually\n# working \u2014 \"updates are enabled\" and \"updates are happening\" are different\n# claims, and only the second one keeps you patched.\nSU=/Library/Preferences/com.apple.SoftwareUpdate\nsu_dump=$(defaults read \"$SU\" 2>/dev/null)\nsu_key() { grep -oE \"^ *$1 = [0-9]+\" <<<\"$su_dump\" | grep -oE \"[0-9]+$\"; }\nif [[ -z \"$su_dump\" ]]; then\n record \"updates\" unknown \"could not read the software update settings\"\nelse\n auto_check=$(su_key AutomaticCheckEnabled)\n crit=$(su_key CriticalUpdateInstall)\n os_auto=$(su_key AutomaticallyInstallMacOSUpdates)\n\n # Last time a check actually succeeded, as recorded by macOS itself.\n last_su_raw=$(grep -oE 'LastSuccessfulDate = \"[^\"]+\"' <<<\"$su_dump\" | head -1 |\n sed -E 's/.*\"([0-9-]+ [0-9:]+).*/\\1/')\n last_su_epoch=\"\"\n [[ -n \"$last_su_raw\" ]] &&\n last_su_epoch=$(date -j -u -f \"%Y-%m-%d %H:%M:%S\" \"$last_su_raw\" +%s 2>/dev/null)\n su_age=$(days_since \"$last_su_epoch\") || su_age=\"\"\n\n if [[ \"${auto_check:-1}\" == \"0\" ]]; then\n record \"updates\" fail \"automatic update checks are turned off\"\n elif [[ \"${crit:-1}\" == \"0\" ]]; then\n record \"updates\" fail \"security responses are not installed automatically\"\n elif [[ -n \"$su_age\" && \"$su_age\" -gt 30 ]]; then\n record \"updates\" fail \"automatic updates are on, but the last successful check was ${su_age} days ago\"\n elif [[ -n \"$su_age\" ]]; then\n record \"updates\" pass \"automatic updates on and checking (last successful check ${su_age}d ago)\"\n else\n record \"updates\" pass \"automatic updates on (security=${crit:-default}, macOS=${os_auto:-default})\"\n fi\nfi\n\n# \u2500\u2500 Supported macOS \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n# Apple ships security fixes for the current release and the two before it.\n# That list changes every autumn, so it lives here with a date attached: if\n# this script has gone stale the check says so instead of guessing at you.\nOS_TABLE_DATE=\"2026-08-21\"\nSUPPORTED_MAJORS=\"26 25 15 14\"\ntable_epoch=$(date -j -f \"%Y-%m-%d\" \"$OS_TABLE_DATE\" +%s 2>/dev/null)\ntable_age=$(days_since \"$table_epoch\") || table_age=\"\"\nmajor=$(sw_vers -productVersion | cut -d. -f1)\nif [[ -n \"$table_age\" && \"$table_age\" -gt 400 ]]; then\n record \"supported-os\" unknown \"this script's list of supported macOS versions is ${table_age} days old \u2014 too stale to judge macOS $(sw_vers -productVersion)\"\nelif [[ \" $SUPPORTED_MAJORS \" == *\" $major \"* ]]; then\n record \"supported-os\" pass \"macOS $(sw_vers -productVersion) still gets security updates\"\nelse\n record \"supported-os\" fail \"macOS $(sw_vers -productVersion) is past its last security update\"\nfi\n\n# \u2500\u2500 System Integrity Protection \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n# SIP stops even root from modifying system files or injecting into Apple's\n# own processes. It is on by default, and turning it off takes a deliberate\n# trip to Recovery \u2014 which people make for some dev tool and then forget.\nsip_raw=$(csrutil status 2>/dev/null)\nif grep -qi \"status: enabled\" <<<\"$sip_raw\"; then\n record \"sip\" pass \"System Integrity Protection is on\"\nelif grep -qi \"status: disabled\" <<<\"$sip_raw\"; then\n record \"sip\" fail \"System Integrity Protection is off \u2014 re-enable it from Recovery unless you know why it's off\"\nelif [[ -n \"$sip_raw\" ]]; then\n record \"sip\" fail \"System Integrity Protection is partly disabled: $(tr -d '\\n' <<<\"$sip_raw\" | cut -c1-90)\"\nelse\n record \"sip\" unknown \"could not read System Integrity Protection status\"\nfi\n\n# \u2500\u2500 Secure Boot \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n# Apple Silicon only. Full Security means the Mac will only boot a signed,\n# current OS. Reduced Security is what you get after enabling third-party\n# kernel extensions, and it is often deliberate \u2014 but it should be a choice\n# you remember making rather than one you find out about here.\nbridge=$(system_profiler SPiBridgeDataType 2>/dev/null)\nsb=$(grep -oE \"Secure Boot: .*\" <<<\"$bridge\" | head -1 | sed 's/Secure Boot: //')\nkext=$(grep -oE \"Allow All Kernel Extensions: .*\" <<<\"$bridge\" | head -1 | sed 's/.*: //')\nif [[ -z \"$bridge\" ]]; then\n record \"secure-boot\" unknown \"no boot policy reported \u2014 Intel Macs don't have this\"\nelif [[ \"$sb\" == \"Full Security\" ]]; then\n record \"secure-boot\" pass \"Secure Boot is at Full Security\"\nelif [[ -n \"$sb\" ]]; then\n record \"secure-boot\" fail \"Secure Boot is at ${sb}${kext:+, third-party kernel extensions allowed: $kext}\"\nelse\n record \"secure-boot\" unknown \"could not read the boot policy\"\nfi\n\n# \u2500\u2500 Firewall \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nFW=/usr/libexec/ApplicationFirewall/socketfilterfw\nif [[ -x \"$FW\" ]]; then\n if \"$FW\" --getglobalstate 2>/dev/null | grep -qi \"enabled\"; then\n stealth=\"\"\n \"$FW\" --getstealthmode 2>/dev/null | grep -qi \"on\" && stealth=\", stealth mode on\"\n record \"firewall\" pass \"the built-in firewall is on$stealth\"\n else\n record \"firewall\" fail \"the firewall is off \u2014 System Settings > Network > Firewall\"\n fi\nelse\n record \"firewall\" unknown \"could not find socketfilterfw\"\nfi\n\n# \u2500\u2500 Gatekeeper \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nif spctl --status 2>&1 | grep -qi \"assessments enabled\"; then\n xp=$(defaults read /Library/Apple/System/Library/CoreServices/XProtect.bundle/Contents/Info.plist \\\n CFBundleShortVersionString 2>/dev/null)\n record \"gatekeeper\" pass \"Gatekeeper is on${xp:+, XProtect $xp}\"\nelse\n record \"gatekeeper\" fail \"Gatekeeper is disabled \u2014 it is the thing that stops you double-clicking malware\"\nfi\n\n# \u2500\u2500 Services listening to the network \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n# Anything bound to 127.0.0.1 is talking to your own machine and is nobody\n# else's business; development tools do it constantly. What matters is a\n# service bound to every interface, because that is the one that answers\n# strangers when you join caf\u00e9 wifi.\n#\n# This uses netstat rather than lsof deliberately. lsof run as you only\n# reports your own sockets, and sshd runs as root \u2014 so an lsof check would\n# cheerfully report a clean bill of health on a Mac with Remote Login on.\nlisteners=$(netstat -an -p tcp 2>/dev/null | awk '/LISTEN/ {print $4}')\nif [[ -z \"$listeners\" ]]; then\n record \"sharing\" unknown \"could not read the list of listening sockets\"\nelse\n # Strip loopback, then reduce each address to its port number.\n public_ports=$(grep -vE '^(127\\.0\\.0\\.1|::1|\\*\\.0$)' <<<\"$listeners\" |\n sed -E 's/.*[.:]([0-9]+)$/\\1/' | sort -un)\n named=\"\"\n for p in $public_ports; do\n case \"$p\" in\n 22) named=\"${named:+$named, }Remote Login (SSH)\" ;;\n 445) named=\"${named:+$named, }File Sharing (SMB)\" ;;\n 548) named=\"${named:+$named, }File Sharing (AFP)\" ;;\n 631) named=\"${named:+$named, }Printer Sharing\" ;;\n 3283) named=\"${named:+$named, }Remote Management\" ;;\n 5900) named=\"${named:+$named, }Screen Sharing\" ;;\n 5000|7000) named=\"${named:+$named, }AirPlay Receiver\" ;;\n esac\n done\n others=$(grep -cvE '^(22|445|548|631|3283|5900|5000|7000)$' <<<\"$public_ports\")\n if [[ -n \"$named\" ]]; then\n record \"sharing\" fail \"reachable from the network: $named \u2014 turn off what you don't use in System Settings > General > Sharing\"\n elif [[ \"$others\" -gt 0 ]]; then\n record \"sharing\" unknown \"$others service(s) listening on all interfaces, none of them a standard sharing service: $(tr '\\n' ' ' <<<\"$public_ports\")\"\n else\n record \"sharing\" pass \"nothing is listening to the network beyond your own machine\"\n fi\nfi\n\n# \u2500\u2500 Guest account \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nguest=$(defaults read /Library/Preferences/com.apple.loginwindow GuestEnabled 2>/dev/null)\nif [[ \"${guest:-0}\" == \"1\" ]]; then\n record \"guest-account\" fail \"the guest account is enabled\"\nelse\n record \"guest-account\" pass \"the guest account is off\"\nfi\n\n# \u2500\u2500 Find My \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n# The thing that lets you wipe a laptop you are never getting back. Note\n# that the findmydeviced process runs whether or not the feature is on, so\n# it proves nothing; these two signals do.\nfmm=$(defaults read /Library/Preferences/com.apple.FindMyMac FMMEnabled 2>/dev/null)\nfmm_token=$(nvram -p 2>/dev/null | grep -c 'fmm-mobileme-token-FMM')\nif [[ \"${fmm:-0}\" == \"1\" || \"${fmm_token:-0}\" -gt 0 ]]; then\n record \"find-my\" pass \"Find My Mac is on \u2014 a lost machine can be wiped remotely\"\nelif [[ -n \"$fmm\" || \"${fmm_token:-0}\" == \"0\" ]]; then\n record \"find-my\" fail \"Find My Mac is off \u2014 System Settings > Apple Account > iCloud > Find My Mac\"\nelse\n record \"find-my\" unknown \"could not read Find My status\"\nfi\n\n# \u2500\u2500 SSH keys \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n# Only relevant if you have them, and if you do, an unencrypted private key\n# is a password sitting in a file. ssh-keygen with an empty passphrase\n# succeeds only on a key that has none, and never prompts.\nif [[ -d \"$HOME/.ssh\" ]]; then\n dir_mode=$(stat -f \"%A\" \"$HOME/.ssh\" 2>/dev/null)\n bare=() loose=()\n for k in \"$HOME\"/.ssh/id_* \"$HOME\"/.ssh/*.pem; do\n [[ -f \"$k\" ]] || continue\n [[ \"$k\" == *.pub ]] && continue\n ssh-keygen -y -P \"\" -f \"$k\" >/dev/null 2>&1 </dev/null && bare+=(\"$(basename \"$k\")\")\n mode=$(stat -f \"%A\" \"$k\" 2>/dev/null)\n [[ -n \"$mode\" && \"$mode\" != \"600\" && \"$mode\" != \"400\" ]] && loose+=(\"$(basename \"$k\") is $mode\")\n done\n problems=\"\"\n [[ ${#bare[@]} -gt 0 ]] && problems=\"no passphrase: ${bare[*]}\"\n [[ ${#loose[@]} -gt 0 ]] && problems=\"${problems:+$problems; }readable by others: ${loose[*]}\"\n [[ -n \"$dir_mode\" && \"$dir_mode\" != \"700\" ]] && problems=\"${problems:+$problems; }~/.ssh is $dir_mode, should be 700\"\n if [[ -n \"$problems\" ]]; then\n record \"ssh-keys\" fail \"$problems\"\n else\n record \"ssh-keys\" pass \"SSH keys and permissions look right\"\n fi\nelse\n record \"ssh-keys\" pass \"no ~/.ssh directory, so nothing to get wrong\"\nfi\n\n# \u2500\u2500 Password manager \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\npm=\"\"\nfor app in \"1Password\" \"Bitwarden\" \"Proton Pass\" \"Dashlane\"; do\n [[ -d \"/Applications/$app.app\" ]] && { pm=\"$app\"; break; }\ndone\nif [[ -n \"$pm\" ]]; then\n record \"password-manager\" pass \"$pm is installed\"\nelif [[ -d \"/System/Applications/Passwords.app\" ]]; then\n record \"password-manager\" unknown \"no third-party manager found; Apple Passwords is available but this script can't tell whether you use it\"\nelse\n record \"password-manager\" fail \"no password manager found in /Applications\"\nfi\n\n# \u2500\u2500 Backups \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n# A backup you have never restored from is a hypothesis, not a backup \u2014 and\n# an app in /Applications is not even that. What matters is whether\n# something ran recently, so where a tool records its own history in a file\n# you can read, this reads it. Where it doesn't, it says so rather than\n# awarding a pass for owning the software.\nFRESH_DAYS=7\nbest_age=\"\" # smallest number of days since a verified backup\nverified=\"\" # tools with a real, dated last run\npresent=\"\" # tools installed but unverifiable from here\n\nnote_verified() {\n local name=\"$1\" age=\"$2\"\n verified=\"${verified:+$verified; }$name (${age}d ago)\"\n [[ -z \"$best_age\" || \"$age\" -lt \"$best_age\" ]] && best_age=\"$age\"\n}\n\n# Time Machine. A configured destination is not the point; a recent snapshot\n# is. Snapshot paths end in YYYY-MM-DD-HHMMSS.\nif have tmutil && tmutil destinationinfo >/dev/null 2>&1; then\n latest=$(tmutil latestbackup 2>/dev/null | tail -1)\n stamp=$(grep -oE '[0-9]{4}-[0-9]{2}-[0-9]{2}-[0-9]{6}' <<<\"$latest\" | tail -1)\n if [[ -n \"$stamp\" ]]; then\n tm_epoch=$(date -j -f \"%Y-%m-%d-%H%M%S\" \"$stamp\" +%s 2>/dev/null)\n tm_age=$(days_since \"$tm_epoch\") && note_verified \"Time Machine\" \"$tm_age\"\n else\n present=\"${present:+$present, }Time Machine (destination set, but no snapshot this script can date)\"\n fi\nfi\n\n# Arq. The agent keeps a world-readable log and writes a line on every\n# completed backup record, which is a far better signal than the app existing.\nif [[ -d /Applications/Arq.app ]]; then\n arq_log=$(ls -t /Library/Logs/ArqAgent/*.log 2>/dev/null | head -1)\n arq_done=\"\"\n if [[ -n \"$arq_log\" && -r \"$arq_log\" ]]; then\n arq_done=$(tail -c 2000000 \"$arq_log\" 2>/dev/null |\n grep -oE '[0-9]{2}-[A-Za-z]{3}-[0-9]{4} [0-9]{2}:[0-9]{2}:[0-9]{2}' | tail -1)\n fi\n if [[ -n \"$arq_done\" ]]; then\n arq_epoch=$(date -j -f \"%d-%b-%Y %H:%M:%S\" \"$arq_done\" +%s 2>/dev/null)\n arq_age=$(days_since \"$arq_epoch\") && note_verified \"Arq\" \"$arq_age\"\n else\n present=\"${present:+$present, }Arq (installed, no readable backup record)\"\n fi\nfi\n\n# Everything else: detectable, but without a log this script can honestly\n# read, so they count as \"something is here\" rather than \"you are covered\".\nfor app in \"Backblaze\" \"Carbon Copy Cloner\" \"SuperDuper!\" \"ChronoSync\"; do\n [[ -d \"/Applications/$app.app\" ]] && present=\"${present:+$present, }$app (installed, last run not checkable from here)\"\ndone\nfor tool in restic borg kopia duplicacy rclone; do\n have \"$tool\" && present=\"${present:+$present, }$tool (command-line tool present, schedule unknown)\"\ndone\n\nif [[ -n \"$best_age\" && \"$best_age\" -le \"$FRESH_DAYS\" ]]; then\n record \"backups\" pass \"backed up within the last ${FRESH_DAYS} days \u2014 $verified\"\nelif [[ -n \"$best_age\" ]]; then\n record \"backups\" fail \"the most recent backup was ${best_age} days ago \u2014 $verified\"\nelif [[ -n \"$present\" ]]; then\n record \"backups\" unknown \"backup software found but no run this script can date: $present\"\nelse\n record \"backups\" fail \"no backup software found and no Time Machine destination \u2014 one dead disk from losing everything\"\nfi\n\n# \u2500\u2500 Hardware security key \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n# Presence proves you own one. It says nothing about whether you enrolled it\n# anywhere, which is the part that matters, so this never reports a pass.\nif system_profiler SPUSBDataType 2>/dev/null | grep -qiE \"yubikey|security key\"; then\n record \"hardware-key\" unknown \"a security key is plugged in \u2014 whether it's enrolled on your accounts can't be checked from here\"\nelse\n record \"hardware-key\" unknown \"no security key plugged in right now; enrolment can't be checked from here either way\"\nfi\n\n# \u2500\u2500 Report \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\njson() {\n echo \"{\"\n echo \" \\\"tool\\\": \\\"check-my-mac.sh\\\",\"\n echo \" \\\"version\\\": 2,\"\n echo \" \\\"generated\\\": \\\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\\\",\"\n echo \" \\\"platform\\\": \\\"macOS $(sw_vers -productVersion)\\\",\"\n echo \" \\\"summary\\\": { \\\"pass\\\": $PASS, \\\"fail\\\": $FAIL, \\\"unknown\\\": $UNKNOWN },\"\n echo \" \\\"checks\\\": {\"\n local n=${#ROWS[@]} idx=0\n for row in \"${ROWS[@]}\"; do\n idx=$((idx + 1))\n if [[ $idx -lt $n ]]; then echo \"$row,\"; else echo \"$row\"; fi\n done\n echo \" }\"\n echo \"}\"\n}\n\nif [[ $JSON_ONLY -eq 1 ]]; then\n json\nelse\n echo\n echo \"$PASS passed, $FAIL failed, $UNKNOWN unknown.\"\n echo\n echo \"The things this cannot see \u2014 your credit freeze, your hardware keys,\"\n echo \"your phone carrier's port-out PIN \u2014 are the ones worth doing first.\"\n echo\n echo \"To tick these off in the checklist:\"\n echo \" bash check-my-mac.sh --json | pbcopy\"\n echo \"then paste into the box in the post.\"\n echo\nfi\n\n[[ $FAIL -eq 0 ]]\nSCRIPT_EOF\nbash -n /Users/girish/Documents/personalwebsite/website/public/check-my-mac.sh && echo \"syntax OK\""
|
|
@@ -1480,3 +1480,196 @@ test("a failure to open leaves the reason on screen", async () => {
|
|
|
1480
1480
|
assert.match(text.textContent, /Could not open your library/);
|
|
1481
1481
|
assert.strictEqual(el.hidden, false, "a blank window would say nothing at all");
|
|
1482
1482
|
});
|
|
1483
|
+
|
|
1484
|
+
// ---- how a tool call reads ---------------------------------------------------
|
|
1485
|
+
|
|
1486
|
+
test("a command is coloured as a command, not printed as JSON", () => {
|
|
1487
|
+
const html = run(`return shBlock('ls -la /tmp')`);
|
|
1488
|
+
assert.match(html, /class="sh"/, "it should be a shell block");
|
|
1489
|
+
assert.match(html, /class="sh-cmd">ls</, "the thing being run is the command");
|
|
1490
|
+
assert.match(html, /class="sh-flag">-la</, "a flag reads as a flag");
|
|
1491
|
+
});
|
|
1492
|
+
|
|
1493
|
+
test("what follows a pipe is another command", () => {
|
|
1494
|
+
const html = run(`return sh('grep -rn foo src/ | head -5')`);
|
|
1495
|
+
const commands = [...html.matchAll(/class="sh-cmd">([^<]+)</g)].map((m) => m[1]);
|
|
1496
|
+
assert.deepStrictEqual(commands, ["grep", "head"]);
|
|
1497
|
+
});
|
|
1498
|
+
|
|
1499
|
+
test("a quoted string is one piece, whatever is inside it", () => {
|
|
1500
|
+
const html = run(`return sh('grep "no assistant turns" src/')`);
|
|
1501
|
+
assert.match(html, /class="sh-str">"no assistant turns"</);
|
|
1502
|
+
// The words inside must not be mistaken for commands of their own.
|
|
1503
|
+
const commands = [...html.matchAll(/class="sh-cmd">([^<]+)</g)].map((m) => m[1]);
|
|
1504
|
+
assert.deepStrictEqual(commands, ["grep"]);
|
|
1505
|
+
});
|
|
1506
|
+
|
|
1507
|
+
test("a comment is a comment only where a word could start", () => {
|
|
1508
|
+
const commented = run(`return sh('# count them\\nwc -l x')`);
|
|
1509
|
+
assert.match(commented, /class="sh-com"># count them</);
|
|
1510
|
+
const inside = run(`return sh('curl https://x/a#b')`);
|
|
1511
|
+
assert.ok(!/sh-com/.test(inside), "a # inside a word is part of the word");
|
|
1512
|
+
});
|
|
1513
|
+
|
|
1514
|
+
test("a variable is picked out in either spelling", () => {
|
|
1515
|
+
const html = run(`return sh('echo $HOME and \${PATH}')`);
|
|
1516
|
+
const vars = [...html.matchAll(/class="sh-var">([^<]+)</g)].map((m) => m[1]);
|
|
1517
|
+
assert.deepStrictEqual(vars, ["$HOME", "${PATH}"]);
|
|
1518
|
+
});
|
|
1519
|
+
|
|
1520
|
+
test("markup in a command cannot reach the page as markup", () => {
|
|
1521
|
+
const html = run(`return sh('echo "<img src=x onerror=alert(1)>"')`);
|
|
1522
|
+
assert.ok(!html.includes("<img"), "it must be escaped");
|
|
1523
|
+
assert.match(html, /<img/);
|
|
1524
|
+
});
|
|
1525
|
+
|
|
1526
|
+
test("a path in a command still opens in Finder, and a URL still opens", () => {
|
|
1527
|
+
const withPath = run(`return shBlock('ls /Users/someone/Documents')`);
|
|
1528
|
+
assert.match(withPath, /api\/reveal\?path=/, "a path should still reveal");
|
|
1529
|
+
const withUrl = run(`return shBlock('curl -s https://pypi.org/simple/')`);
|
|
1530
|
+
assert.match(withUrl, /<a href="https:\/\/pypi\.org\/simple\/"/);
|
|
1531
|
+
});
|
|
1532
|
+
|
|
1533
|
+
test("a call says what it was for, beside what it was called", () => {
|
|
1534
|
+
const title = run(
|
|
1535
|
+
`return TOOLVIEW.Bash.title({command:"ls",description:"List vault root"})`,
|
|
1536
|
+
);
|
|
1537
|
+
assert.strictEqual(title, "List vault root");
|
|
1538
|
+
const fetched = run(
|
|
1539
|
+
`return TOOLVIEW.WebFetch.title({url:"https://help.obsidian.md/plugins/templates"})`,
|
|
1540
|
+
);
|
|
1541
|
+
assert.strictEqual(fetched, "help.obsidian.md");
|
|
1542
|
+
});
|
|
1543
|
+
|
|
1544
|
+
test("a fetch shows its address as a link and its question as prose", () => {
|
|
1545
|
+
const html = run(
|
|
1546
|
+
`return TOOLVIEW.WebFetch.body({url:"https://example.com/a",prompt:"What does it say?"})`,
|
|
1547
|
+
);
|
|
1548
|
+
assert.match(html, /<a href="https:\/\/example\.com\/a"/);
|
|
1549
|
+
assert.match(html, /What does it say\?/);
|
|
1550
|
+
});
|
|
1551
|
+
|
|
1552
|
+
test("a malformed address does not stop the box drawing", () => {
|
|
1553
|
+
const title = run(`return TOOLVIEW.WebFetch.title({url:"not a url"})`);
|
|
1554
|
+
assert.strictEqual(title, "not a url");
|
|
1555
|
+
});
|
|
1556
|
+
|
|
1557
|
+
test("one box can be shown raw without the rest following", () => {
|
|
1558
|
+
const out = run(`
|
|
1559
|
+
raw = false;
|
|
1560
|
+
CALLS["a"] = {c:{function_name:"Bash",tool_call_id:"a",arguments:{command:"ls",description:"d"}}, r:null};
|
|
1561
|
+
CALLS["b"] = {c:{function_name:"Bash",tool_call_id:"b",arguments:{command:"pwd",description:"d"}}, r:null};
|
|
1562
|
+
TOOLRAW["a"] = true;
|
|
1563
|
+
return [toolBody("a"), toolBody("b")];
|
|
1564
|
+
`);
|
|
1565
|
+
assert.match(out[0], /class="json"/, "the flipped one shows what was recorded");
|
|
1566
|
+
assert.match(out[1], /class="sh"/, "its neighbour is untouched");
|
|
1567
|
+
});
|
|
1568
|
+
|
|
1569
|
+
test("the page-wide raw setting is what a box does until it is told otherwise", () => {
|
|
1570
|
+
const out = run(`
|
|
1571
|
+
raw = true;
|
|
1572
|
+
CALLS["c"] = {c:{function_name:"Bash",tool_call_id:"c",arguments:{command:"ls"}}, r:null};
|
|
1573
|
+
const following = toolBody("c");
|
|
1574
|
+
TOOLRAW["c"] = false;
|
|
1575
|
+
const flipped = toolBody("c");
|
|
1576
|
+
raw = false;
|
|
1577
|
+
return [following, flipped];
|
|
1578
|
+
`);
|
|
1579
|
+
assert.match(out[0], /class="json"/, "it follows the page by default");
|
|
1580
|
+
assert.match(out[1], /class="sh"/, "and can disagree once flipped");
|
|
1581
|
+
});
|
|
1582
|
+
|
|
1583
|
+
test("a tool with no view of its own still gets its JSON", () => {
|
|
1584
|
+
const html = run(`
|
|
1585
|
+
CALLS["z"] = {c:{function_name:"SomethingNew",tool_call_id:"z",arguments:{a:1}}, r:null};
|
|
1586
|
+
return toolBody("z");
|
|
1587
|
+
`);
|
|
1588
|
+
assert.match(html, /class="json"/);
|
|
1589
|
+
});
|
|
1590
|
+
|
|
1591
|
+
test("a redirect target is a file, not a command", () => {
|
|
1592
|
+
const html = run(`return sh('wc -l a.txt > /tmp/out.txt')`);
|
|
1593
|
+
const commands = [...html.matchAll(/class="sh-cmd">([^<]+)</g)].map((m) => m[1]);
|
|
1594
|
+
assert.deepStrictEqual(commands, ["wc"], "only wc is being run");
|
|
1595
|
+
});
|
|
1596
|
+
|
|
1597
|
+
test("a subshell still starts a command", () => {
|
|
1598
|
+
const html = run(`return sh('(cd /tmp && ls)')`);
|
|
1599
|
+
const commands = [...html.matchAll(/class="sh-cmd">([^<]+)</g)].map((m) => m[1]);
|
|
1600
|
+
assert.deepStrictEqual(commands, ["cd", "ls"]);
|
|
1601
|
+
});
|
|
1602
|
+
|
|
1603
|
+
test("changing the page-wide setting means it, even for boxes flipped by hand", () => {
|
|
1604
|
+
const out = run(`
|
|
1605
|
+
raw = false;
|
|
1606
|
+
CALLS["g"] = {c:{function_name:"Bash",tool_call_id:"g",arguments:{command:"ls"}}, r:null};
|
|
1607
|
+
TOOLRAW["g"] = true; // this box was asked for the recording
|
|
1608
|
+
const before = toolBody("g");
|
|
1609
|
+
render = () => {}; // the repaint is not what is under test
|
|
1610
|
+
toggleRaw(); // now the whole page is
|
|
1611
|
+
const after = toolBody("g");
|
|
1612
|
+
return [before, after, raw];
|
|
1613
|
+
`);
|
|
1614
|
+
assert.match(out[0], /class="json"/, "it was showing the recording");
|
|
1615
|
+
assert.match(out[1], /class="json"/, "and still does, because the page now says so");
|
|
1616
|
+
assert.strictEqual(out[2], true);
|
|
1617
|
+
});
|
|
1618
|
+
|
|
1619
|
+
test("a file path is titled by its end, where the name is", () => {
|
|
1620
|
+
const title = run(
|
|
1621
|
+
`return TOOLVIEW.Read.title({file_path:"/private/tmp/very/long/way/down/ARCHITECTURE.md"})`,
|
|
1622
|
+
);
|
|
1623
|
+
assert.strictEqual(title, "down/ARCHITECTURE.md");
|
|
1624
|
+
});
|
|
1625
|
+
|
|
1626
|
+
test("a command substitution does not stop the scanner", () => {
|
|
1627
|
+
// $( matched no branch and the word reader could not move past $, so the page
|
|
1628
|
+
// hung for ever on any command using one. 58 of them in one real session.
|
|
1629
|
+
const html = run(`return sh('echo $(date) and $(basename /a/b)')`);
|
|
1630
|
+
assert.match(html, /sh-var/);
|
|
1631
|
+
assert.ok(html.includes("date"), "what is inside must still be there");
|
|
1632
|
+
});
|
|
1633
|
+
|
|
1634
|
+
test("a lone dollar is just a character", () => {
|
|
1635
|
+
const html = run(`return sh('echo 5$ and $')`);
|
|
1636
|
+
assert.ok(html.includes("5$"), "it should survive");
|
|
1637
|
+
});
|
|
1638
|
+
|
|
1639
|
+
test("every character survives the scanner", () => {
|
|
1640
|
+
// The strongest check there is: what goes in comes out, tags aside. A branch
|
|
1641
|
+
// that stops matching shows up here as mangled text rather than as a hang.
|
|
1642
|
+
const samples = [
|
|
1643
|
+
"echo $(date)",
|
|
1644
|
+
'for f in $HOME/*; do echo "$f"; done',
|
|
1645
|
+
"awk '{print $1}' file | sort -u > /tmp/o",
|
|
1646
|
+
"x=$((1+2)); echo $x $$ $? $",
|
|
1647
|
+
"cat <<'EOF'\nliteral $(not a call)\nEOF",
|
|
1648
|
+
"curl -s https://x/a#b -o /tmp/x && echo done # a comment",
|
|
1649
|
+
];
|
|
1650
|
+
for (const src of samples) {
|
|
1651
|
+
const out = run(`return sh(${JSON.stringify(src)})`);
|
|
1652
|
+
const back = out
|
|
1653
|
+
.replace(/<[^>]+>/g, "")
|
|
1654
|
+
.replace(/"/g, '"')
|
|
1655
|
+
.replace(/'/g, "'")
|
|
1656
|
+
.replace(/</g, "<")
|
|
1657
|
+
.replace(/>/g, ">")
|
|
1658
|
+
.replace(/&/g, "&");
|
|
1659
|
+
assert.strictEqual(back, src, `mangled: ${JSON.stringify(src)}`);
|
|
1660
|
+
}
|
|
1661
|
+
});
|
|
1662
|
+
|
|
1663
|
+
test("a real 20,000-character command draws, and quickly", () => {
|
|
1664
|
+
// This exact command hung the page: 448 lines of heredoc with 58 command
|
|
1665
|
+
// substitutions in it. Kept as a fixture because no invented sample had the
|
|
1666
|
+
// shape that broke it.
|
|
1667
|
+
const command = JSON.parse(
|
|
1668
|
+
readFileSync(join(__dirname, "fixtures", "big-command.json"), "utf8"),
|
|
1669
|
+
);
|
|
1670
|
+
const started = Date.now();
|
|
1671
|
+
const html = run(`return shBlock(${JSON.stringify(command)})`);
|
|
1672
|
+
const took = Date.now() - started;
|
|
1673
|
+
assert.ok(html.includes("class=\"sh\""), "it should render");
|
|
1674
|
+
assert.ok(took < 2000, `took ${took}ms, which is a page nobody waits for`);
|
|
1675
|
+
});
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|