dazzer-connect 0.2.0 → 0.3.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/README.md +54 -0
- package/dist/index.js +519 -94
- package/package.json +5 -5
package/README.md
CHANGED
|
@@ -67,6 +67,60 @@ The second carries a pass, for clients that cannot do that:
|
|
|
67
67
|
|
|
68
68
|
Merge either one with any `mcpServers` entries you already have, save, and restart your client.
|
|
69
69
|
|
|
70
|
+
## Put files into Dazzer
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
dazzer add report.pdf
|
|
74
|
+
dazzer add ./backlog
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
A folder is walked with its subfolders. Hidden files are left alone, and nothing outside the folder is ever sent. The first time, you are asked which space things go in: run the same command again with `--space "<name>"`, or with `--keep-to-myself`. A folder run ends with a count of what went in and what was skipped. Add `--why` to see each file that was skipped, and why.
|
|
78
|
+
|
|
79
|
+
### With what your assistant wrote about each file
|
|
80
|
+
|
|
81
|
+
Your own AI assistant can read the files and say what each one is. It writes that into one JSON file, called the descriptions file. Each key is a file's path inside the folder, or the file's name when you add just one, and what sits under a key is that file's entry:
|
|
82
|
+
|
|
83
|
+
```json
|
|
84
|
+
{
|
|
85
|
+
"notes/plan.md": {
|
|
86
|
+
"description": {
|
|
87
|
+
"what_it_is": { "text": "the plan for the spring launch", "quote": "Spring launch plan" },
|
|
88
|
+
"what_it_covers": { "text": "who ships what, week by week", "quote": "Week 1: design review" },
|
|
89
|
+
"what_it_decides": null,
|
|
90
|
+
"how_much_was_read": { "all": true }
|
|
91
|
+
},
|
|
92
|
+
"facts": [{ "text": "the launch is on 14 April", "quote": "Launch: 14 April" }],
|
|
93
|
+
"document_date": "2026-03-01"
|
|
94
|
+
},
|
|
95
|
+
"scan.png": {
|
|
96
|
+
"description": {
|
|
97
|
+
"what_it_is": { "text": "a photo of the gate log", "quote": "GATE LOG" },
|
|
98
|
+
"what_it_covers": { "text": "who signed for the gate overnight", "quote": "night porter" },
|
|
99
|
+
"what_it_decides": null,
|
|
100
|
+
"how_much_was_read": { "all": true }
|
|
101
|
+
},
|
|
102
|
+
"transcription": "GATE LOG\nSigned by the night porter at four."
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Save it beside the folder, never inside it — for example as `backlog-descriptions.json` next to `backlog`. Inside the folder, a later `dazzer add ./backlog` without `--described-in` would send it like any other file. Then:
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
dazzer add ./backlog --described-in ./backlog-descriptions.json
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Each file goes with its own entry. The descriptions file named by `--described-in` is not sent with them, even when it sits inside the folder, and the run says to move it.
|
|
114
|
+
|
|
115
|
+
- Each entry's form is checked before its file is sent. An entry in a form Dazzer refuses, such as a date written as "March 2026", holds its file back, and a line names the field and the form it takes. What the entry says is judged by Dazzer once the file has arrived.
|
|
116
|
+
- A file with no entry goes in as usual, and the count names it.
|
|
117
|
+
- An entry that names no file being added is reported, and nothing is sent for it. If no entry names a file being added, nothing is sent at all.
|
|
118
|
+
- A file Dazzer refuses although it had an entry is named, in Dazzer's own words.
|
|
119
|
+
- Whatever Dazzer did not keep of an entry is printed file by file, with what to change.
|
|
120
|
+
- Sent again, a folder already in Dazzer says how many files took the description sent now, how many already had one, and how many did not take the one sent.
|
|
121
|
+
|
|
122
|
+
An assistant that can run commands runs this one itself. One that cannot gives you the descriptions file to save beside the folder, and the line to run.
|
|
123
|
+
|
|
70
124
|
## Other commands
|
|
71
125
|
|
|
72
126
|
```bash
|
package/dist/index.js
CHANGED
|
@@ -265,6 +265,7 @@ function assertHttpsServiceAddress(address) {
|
|
|
265
265
|
}
|
|
266
266
|
return address.replace(/\/$/, "");
|
|
267
267
|
}
|
|
268
|
+
var CLIENT_HEADER = "x-dazzer-client";
|
|
268
269
|
function readOfferedPlaces(offered) {
|
|
269
270
|
if (!Array.isArray(offered)) return [];
|
|
270
271
|
return offered.flatMap((place) => {
|
|
@@ -357,7 +358,8 @@ async function callDazzer(request) {
|
|
|
357
358
|
const fetchFn = request.fetchFn ?? fetch;
|
|
358
359
|
const headers = {
|
|
359
360
|
accept: "application/json",
|
|
360
|
-
authorization: `Bearer ${credential.held}
|
|
361
|
+
authorization: `Bearer ${credential.held}`,
|
|
362
|
+
[CLIENT_HEADER]: request.client
|
|
361
363
|
};
|
|
362
364
|
if (request.body !== void 0) headers["content-type"] = "application/json";
|
|
363
365
|
let res;
|
|
@@ -733,7 +735,7 @@ async function walkFolder(root) {
|
|
|
733
735
|
var SPACE_PATH = "/documents/v1/space";
|
|
734
736
|
var LANDING_PATH = "/documents/v1/landing";
|
|
735
737
|
var LANDED_PATH = "/documents/v1/landed";
|
|
736
|
-
var cannotOpen = (
|
|
738
|
+
var cannotOpen = (name2) => `Couldn't open ${name2} \u2014 check its permissions.`;
|
|
737
739
|
var SLOWING_DOWN = "Slowing down to stay within limits \u2014 still going.";
|
|
738
740
|
var ALREADY_IN = "That one's already in \u2014 nothing to do.";
|
|
739
741
|
var waitingOnYou = (many) => `${many} waiting on your answer.`;
|
|
@@ -756,9 +758,34 @@ var NUMBER_WORDS = [
|
|
|
756
758
|
"ten"
|
|
757
759
|
];
|
|
758
760
|
var inWords = (many) => NUMBER_WORDS[many] ?? String(many);
|
|
759
|
-
var cannotFind = (
|
|
760
|
-
var didNotFinish = (
|
|
761
|
-
var wentWrong = (
|
|
761
|
+
var cannotFind = (name2) => `Couldn't find ${name2}.`;
|
|
762
|
+
var didNotFinish = (name2) => `The send didn't finish \u2014 ${name2} is not in Dazzer. Try again shortly.`;
|
|
763
|
+
var wentWrong = (name2) => `Something went wrong and ${name2} is not in Dazzer. Try again shortly.`;
|
|
764
|
+
var descriptionsNotFound = (name2) => `Couldn't find the descriptions file ${name2} \u2014 nothing was sent.`;
|
|
765
|
+
var descriptionsUnreadable = (name2) => `Couldn't read the descriptions file ${name2} \u2014 nothing was sent.`;
|
|
766
|
+
var descriptionsNotJson = (name2) => `The descriptions file ${name2} is not JSON \u2014 nothing was sent.`;
|
|
767
|
+
var descriptionsNotAnObject = (name2) => `The descriptions file ${name2} is not one JSON object keyed by file path \u2014 nothing was sent.`;
|
|
768
|
+
var isTheDescriptionsFile = (name2) => `${name2} is the descriptions file \u2014 nothing was sent.`;
|
|
769
|
+
function namingNoFile(keys) {
|
|
770
|
+
const many = keys.length;
|
|
771
|
+
const which = many <= NAMED_WHEN_AT_MOST ? ` \u2014 ${listed(keys.map(asPrinted))}` : "";
|
|
772
|
+
return `${many} ${many === 1 ? "entry names" : "entries name"} no file being added${which}. Nothing was sent for ${many === 1 ? "it" : "them"}.`;
|
|
773
|
+
}
|
|
774
|
+
var noEntryMatches = (name2, example) => `No entry in ${name2} names a file being added \u2014 nothing was sent.` + (example === void 0 ? "" : ` A key looks like ${JSON.stringify(asPrinted(example))}.`);
|
|
775
|
+
var keepItBeside = "The descriptions file is inside the folder. Keep it beside the folder instead, so no later run sends it as a document.";
|
|
776
|
+
var itHadNoDescription = "It had no description.";
|
|
777
|
+
function alreadyThereDescriptions(counted) {
|
|
778
|
+
const parts = [
|
|
779
|
+
...counted.filed > 0 ? [`${counted.filed} took the description sent now`] : [],
|
|
780
|
+
...counted.alreadyDescribed > 0 ? [`${counted.alreadyDescribed} already had a description`] : [],
|
|
781
|
+
...counted.notKept > 0 ? [`${counted.notKept} did not take the description sent`] : []
|
|
782
|
+
];
|
|
783
|
+
return `Of those already there: ${parts.join(", ")}.`;
|
|
784
|
+
}
|
|
785
|
+
var itsDescriptionFiledNow = "Its description was filed now.";
|
|
786
|
+
var itAlreadyHasOne = "It already has a description, and keeps it.";
|
|
787
|
+
var itsDescriptionNotKept = "The description sent was not kept.";
|
|
788
|
+
var NAMED_WHEN_AT_MOST = 5;
|
|
762
789
|
var choiceLine = (space, scopeId) => ` ${asPrinted(space)} \u2014 ${scopeId}`;
|
|
763
790
|
var answerWith = (keepToYourself, numbered) => {
|
|
764
791
|
const ways = [
|
|
@@ -788,8 +815,8 @@ var DECLARED_BY_EXTENSION = {
|
|
|
788
815
|
".webp": "image/webp"
|
|
789
816
|
};
|
|
790
817
|
var UNDECLARED = "application/octet-stream";
|
|
791
|
-
function asPrinted(
|
|
792
|
-
return
|
|
818
|
+
function asPrinted(name2) {
|
|
819
|
+
return name2.replace(/[\p{Cc}\p{Cf}\p{Zl}\p{Zp}]/gu, " ").trim();
|
|
793
820
|
}
|
|
794
821
|
function readEnvelope(body) {
|
|
795
822
|
if (typeof body !== "object" || body === null) return null;
|
|
@@ -865,10 +892,11 @@ function askedWith(options) {
|
|
|
865
892
|
if (answer === void 0 || answer === "") return {};
|
|
866
893
|
return { space: /^\d+$/.test(answer) ? Number(answer) : answer };
|
|
867
894
|
}
|
|
868
|
-
async function settleSpace(options, auth2, deps, voice,
|
|
895
|
+
async function settleSpace(options, auth2, deps, voice, name2) {
|
|
869
896
|
const answer = await callPaced(
|
|
870
897
|
{
|
|
871
898
|
baseUrl: options.baseUrl,
|
|
899
|
+
client: options.client,
|
|
872
900
|
path: SPACE_PATH,
|
|
873
901
|
method: "POST",
|
|
874
902
|
body: askedWith(options),
|
|
@@ -879,12 +907,12 @@ async function settleSpace(options, auth2, deps, voice, name) {
|
|
|
879
907
|
voice
|
|
880
908
|
);
|
|
881
909
|
if (answer.status !== "ok") {
|
|
882
|
-
say(voice.write, answer,
|
|
910
|
+
say(voice.write, answer, name2);
|
|
883
911
|
return null;
|
|
884
912
|
}
|
|
885
913
|
const data = readEnvelope(answer.body);
|
|
886
914
|
if (data === null) {
|
|
887
|
-
voice.write(wentWrong(
|
|
915
|
+
voice.write(wentWrong(name2));
|
|
888
916
|
return null;
|
|
889
917
|
}
|
|
890
918
|
const space = typeof data.space === "string" && data.space.trim() !== "" ? data.space : null;
|
|
@@ -893,7 +921,7 @@ async function settleSpace(options, auth2, deps, voice, name) {
|
|
|
893
921
|
}
|
|
894
922
|
const question = typeof data.question === "string" && data.question !== "" ? data.question : null;
|
|
895
923
|
if (question === null) {
|
|
896
|
-
voice.write(wentWrong(
|
|
924
|
+
voice.write(wentWrong(name2));
|
|
897
925
|
return null;
|
|
898
926
|
}
|
|
899
927
|
voice.write(question);
|
|
@@ -925,6 +953,7 @@ async function sendOne(input) {
|
|
|
925
953
|
const described = await callPaced(
|
|
926
954
|
{
|
|
927
955
|
baseUrl: options.baseUrl,
|
|
956
|
+
client: options.client,
|
|
928
957
|
path: LANDING_PATH,
|
|
929
958
|
method: "POST",
|
|
930
959
|
body: {
|
|
@@ -984,9 +1013,10 @@ async function sendOne(input) {
|
|
|
984
1013
|
const landed = await callPaced(
|
|
985
1014
|
{
|
|
986
1015
|
baseUrl: options.baseUrl,
|
|
1016
|
+
client: options.client,
|
|
987
1017
|
path: LANDED_PATH,
|
|
988
1018
|
method: "POST",
|
|
989
|
-
body: { ticket: landing.ticket },
|
|
1019
|
+
body: { ticket: landing.ticket, ...input.described },
|
|
990
1020
|
auth: input.auth,
|
|
991
1021
|
fetchFn: deps.fetchFn
|
|
992
1022
|
},
|
|
@@ -996,11 +1026,269 @@ async function sendOne(input) {
|
|
|
996
1026
|
if (landed.status !== "ok") return fromAnswer(landed);
|
|
997
1027
|
const stored = readEnvelope(landed.body);
|
|
998
1028
|
if (stored === null) return { kind: "went-wrong" };
|
|
999
|
-
|
|
1029
|
+
const alreadyHere = stored.already_here === true;
|
|
1030
|
+
const becameOfIt = whatBecameOfIt(stored, alreadyHere, "description" in (input.described ?? {}));
|
|
1031
|
+
if (alreadyHere) return { kind: "already-here", ...becameOfIt };
|
|
1000
1032
|
return {
|
|
1001
1033
|
kind: "added",
|
|
1002
1034
|
fileName: typeof stored.file_name === "string" ? asPrinted(stored.file_name) : asPrinted(basename(input.path)),
|
|
1003
|
-
space: typeof stored.space_name === "string" ? stored.space_name : null
|
|
1035
|
+
space: typeof stored.space_name === "string" ? stored.space_name : null,
|
|
1036
|
+
...becameOfIt
|
|
1037
|
+
};
|
|
1038
|
+
}
|
|
1039
|
+
var ENTRY_FIELDS = ["description", "facts", "document_date", "transcription"];
|
|
1040
|
+
var DESCRIPTION_FIELDS = [
|
|
1041
|
+
"what_it_is",
|
|
1042
|
+
"what_it_covers",
|
|
1043
|
+
"what_it_decides",
|
|
1044
|
+
"how_much_was_read"
|
|
1045
|
+
];
|
|
1046
|
+
var QUOTED_FIELDS = ["text", "quote"];
|
|
1047
|
+
var MOST_FACTS = 100;
|
|
1048
|
+
var MOST_TYPED_CHARACTERS = 1e6;
|
|
1049
|
+
var MOST_MESSAGE_BYTES = 1024 * 1024;
|
|
1050
|
+
var LONGEST_TICKET = 4096;
|
|
1051
|
+
var A_DAY = /^(\d{4})-(\d{2})-(\d{2})$/;
|
|
1052
|
+
var A_MOMENT_ON_THE_DAY = /^T(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|[+-](?:[01]\d|2[0-3]):[0-5]\d)$/;
|
|
1053
|
+
var EARLIEST_YEAR = 1e3;
|
|
1054
|
+
function aDateTheDoorReads(value) {
|
|
1055
|
+
const written = value.trim();
|
|
1056
|
+
const day = A_DAY.exec(written.slice(0, 10));
|
|
1057
|
+
if (day === null) return false;
|
|
1058
|
+
const [year, month, date] = [Number(day[1]), Number(day[2]), Number(day[3])];
|
|
1059
|
+
if (year < EARLIEST_YEAR) return false;
|
|
1060
|
+
const midnight = new Date(Date.UTC(year, month - 1, date));
|
|
1061
|
+
if (midnight.getUTCFullYear() !== year || midnight.getUTCMonth() !== month - 1 || midnight.getUTCDate() !== date) {
|
|
1062
|
+
return false;
|
|
1063
|
+
}
|
|
1064
|
+
if (written.length === 10) return true;
|
|
1065
|
+
return A_MOMENT_ON_THE_DAY.test(written.slice(10)) && !Number.isNaN(new Date(written).getTime());
|
|
1066
|
+
}
|
|
1067
|
+
var sameFile = (a, b) => a.dev === b.dev && a.ino === b.ino;
|
|
1068
|
+
var isPlainObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1069
|
+
function asKey(path) {
|
|
1070
|
+
let key = path.normalize("NFC").replaceAll("\\", "/");
|
|
1071
|
+
while (key.startsWith("./")) key = key.slice(2);
|
|
1072
|
+
return key;
|
|
1073
|
+
}
|
|
1074
|
+
async function readDescriptions(path) {
|
|
1075
|
+
const name2 = asPrinted(basename(path));
|
|
1076
|
+
let handle;
|
|
1077
|
+
try {
|
|
1078
|
+
handle = await open(path, "r");
|
|
1079
|
+
} catch (err) {
|
|
1080
|
+
const missing = err?.code === "ENOENT";
|
|
1081
|
+
return { said: missing ? descriptionsNotFound(name2) : descriptionsUnreadable(name2) };
|
|
1082
|
+
}
|
|
1083
|
+
let text;
|
|
1084
|
+
let identity;
|
|
1085
|
+
try {
|
|
1086
|
+
const info = await handle.stat({ bigint: true });
|
|
1087
|
+
identity = { dev: info.dev, ino: info.ino };
|
|
1088
|
+
text = await handle.readFile("utf8");
|
|
1089
|
+
} catch {
|
|
1090
|
+
return { said: descriptionsUnreadable(name2) };
|
|
1091
|
+
} finally {
|
|
1092
|
+
await handle.close();
|
|
1093
|
+
}
|
|
1094
|
+
const json = text.charCodeAt(0) === 65279 ? text.slice(1) : text;
|
|
1095
|
+
let parsed;
|
|
1096
|
+
try {
|
|
1097
|
+
parsed = JSON.parse(json);
|
|
1098
|
+
} catch {
|
|
1099
|
+
return { said: descriptionsNotJson(name2) };
|
|
1100
|
+
}
|
|
1101
|
+
if (!isPlainObject(parsed)) return { said: descriptionsNotAnObject(name2) };
|
|
1102
|
+
const entries = /* @__PURE__ */ new Map();
|
|
1103
|
+
for (const [key, value] of Object.entries(parsed)) {
|
|
1104
|
+
const matched = asKey(key);
|
|
1105
|
+
const already = entries.get(matched);
|
|
1106
|
+
entries.set(matched, { keys: [...already?.keys ?? [], key], value: already?.value ?? value });
|
|
1107
|
+
}
|
|
1108
|
+
return { name: name2, entries, identity };
|
|
1109
|
+
}
|
|
1110
|
+
function quoted(text) {
|
|
1111
|
+
const flat = asPrinted(text);
|
|
1112
|
+
return `"${flat.length > 40 ? `${flat.slice(0, 40)}\u2026` : flat}"`;
|
|
1113
|
+
}
|
|
1114
|
+
var A_QUOTED_SENTENCE = '{"text": "...", "quote": "..."}';
|
|
1115
|
+
function strayKey(value, fields, where, mayHold) {
|
|
1116
|
+
const stray = Object.keys(value).find((key) => !fields.includes(key));
|
|
1117
|
+
return stray === void 0 ? void 0 : `${where} holds ${quoted(stray)}. It holds only ${mayHold}.`;
|
|
1118
|
+
}
|
|
1119
|
+
function quotedSentenceWrong(value, where) {
|
|
1120
|
+
const stray = strayKey(value, QUOTED_FIELDS, where, "text and quote");
|
|
1121
|
+
if (stray !== void 0) return stray;
|
|
1122
|
+
for (const field of QUOTED_FIELDS) {
|
|
1123
|
+
const said = value[field];
|
|
1124
|
+
if (said !== void 0 && said !== null && typeof said !== "string") {
|
|
1125
|
+
return `the ${field} of ${where} is not text. Write it in quotes, like "Spring launch plan".`;
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
return void 0;
|
|
1129
|
+
}
|
|
1130
|
+
function lineWrong(description, line, mayBeNull) {
|
|
1131
|
+
if (!(line in description)) return void 0;
|
|
1132
|
+
const value = description[line];
|
|
1133
|
+
if (value === null && mayBeNull) return void 0;
|
|
1134
|
+
const where = `description.${line}`;
|
|
1135
|
+
if (!isPlainObject(value)) {
|
|
1136
|
+
return `${where} is not an object. Write it like ${A_QUOTED_SENTENCE}${mayBeNull ? ", or null when the document settles nothing" : ""}.`;
|
|
1137
|
+
}
|
|
1138
|
+
return quotedSentenceWrong(value, where);
|
|
1139
|
+
}
|
|
1140
|
+
function descriptionWrong(value) {
|
|
1141
|
+
if (!isPlainObject(value)) {
|
|
1142
|
+
return `description is not an object. Write it like {"what_it_is": ${A_QUOTED_SENTENCE}, ...}.`;
|
|
1143
|
+
}
|
|
1144
|
+
return strayKey(
|
|
1145
|
+
value,
|
|
1146
|
+
DESCRIPTION_FIELDS,
|
|
1147
|
+
"description",
|
|
1148
|
+
"what_it_is, what_it_covers, what_it_decides and how_much_was_read"
|
|
1149
|
+
) ?? lineWrong(value, "what_it_is", false) ?? lineWrong(value, "what_it_covers", false) ?? lineWrong(value, "what_it_decides", true);
|
|
1150
|
+
}
|
|
1151
|
+
function factsWrong(value) {
|
|
1152
|
+
if (!Array.isArray(value)) return `facts is not a list. Write it like [${A_QUOTED_SENTENCE}].`;
|
|
1153
|
+
if (value.length > MOST_FACTS) {
|
|
1154
|
+
return `facts holds ${value.length} facts. It takes at most ${MOST_FACTS}.`;
|
|
1155
|
+
}
|
|
1156
|
+
for (const [at, fact] of value.entries()) {
|
|
1157
|
+
const where = `fact ${at + 1}`;
|
|
1158
|
+
if (!isPlainObject(fact)) {
|
|
1159
|
+
return `${where} is not an object. Write each fact like ${A_QUOTED_SENTENCE}.`;
|
|
1160
|
+
}
|
|
1161
|
+
const wrong = quotedSentenceWrong(fact, where);
|
|
1162
|
+
if (wrong !== void 0) return wrong;
|
|
1163
|
+
}
|
|
1164
|
+
return void 0;
|
|
1165
|
+
}
|
|
1166
|
+
function dateWrong(value) {
|
|
1167
|
+
if (typeof value !== "string") return 'document_date is not text. Write it like "2026-03-01".';
|
|
1168
|
+
return aDateTheDoorReads(value) ? void 0 : `document_date ${quoted(value)} is not a date. Write it like "2026-03-01".`;
|
|
1169
|
+
}
|
|
1170
|
+
function typedWrong(value) {
|
|
1171
|
+
if (typeof value !== "string") {
|
|
1172
|
+
return "transcription is not text. Write the typed-out words as one string.";
|
|
1173
|
+
}
|
|
1174
|
+
return /\S/.test(value) && value.length > MOST_TYPED_CHARACTERS ? `transcription runs to ${value.length.toLocaleString("en-US")} characters. It takes at most ${MOST_TYPED_CHARACTERS.toLocaleString("en-US")}.` : void 0;
|
|
1175
|
+
}
|
|
1176
|
+
function sizeWrong(entry) {
|
|
1177
|
+
const bytes = Buffer.byteLength(
|
|
1178
|
+
JSON.stringify({ ticket: "t".repeat(LONGEST_TICKET), ...entry }),
|
|
1179
|
+
"utf8"
|
|
1180
|
+
);
|
|
1181
|
+
return bytes > MOST_MESSAGE_BYTES ? `its entry comes to ${bytes.toLocaleString("en-US")} bytes. One message takes at most ${MOST_MESSAGE_BYTES.toLocaleString("en-US")}.` : void 0;
|
|
1182
|
+
}
|
|
1183
|
+
function checkEntry(found) {
|
|
1184
|
+
if (found.keys.length > 1) {
|
|
1185
|
+
return {
|
|
1186
|
+
wrong: `${found.keys.length} entries name it \u2014 ${listed(found.keys.map(quoted))}. Keep one.`
|
|
1187
|
+
};
|
|
1188
|
+
}
|
|
1189
|
+
const entry = found.value;
|
|
1190
|
+
if (!isPlainObject(entry)) {
|
|
1191
|
+
return {
|
|
1192
|
+
wrong: 'its entry is not an object. Write it like {"description": {...}, "facts": [...], "document_date": "2026-03-01"}.'
|
|
1193
|
+
};
|
|
1194
|
+
}
|
|
1195
|
+
const wrong = strayKey(
|
|
1196
|
+
entry,
|
|
1197
|
+
ENTRY_FIELDS,
|
|
1198
|
+
"its entry",
|
|
1199
|
+
"description, facts, document_date and transcription"
|
|
1200
|
+
) ?? ("description" in entry ? descriptionWrong(entry.description) : void 0) ?? ("facts" in entry ? factsWrong(entry.facts) : void 0) ?? ("document_date" in entry ? dateWrong(entry.document_date) : void 0) ?? ("transcription" in entry ? typedWrong(entry.transcription) : void 0) ?? sizeWrong(entry);
|
|
1201
|
+
return wrong === void 0 ? { described: entry } : { wrong };
|
|
1202
|
+
}
|
|
1203
|
+
var wordsOf = (value) => typeof value === "string" && value.trim() !== "" ? value : void 0;
|
|
1204
|
+
var listOf = (value) => Array.isArray(value) ? value : [];
|
|
1205
|
+
var LINE_DROPPED = {
|
|
1206
|
+
"line-empty": "it has no words",
|
|
1207
|
+
"line-too-long": "it is too long",
|
|
1208
|
+
"line-verbatim-from-document": "its words are copied out of the document",
|
|
1209
|
+
"quote-empty": "it quotes nothing",
|
|
1210
|
+
"quote-too-long": "its quote is too long",
|
|
1211
|
+
"quote-not-in-document": "the words it quotes are not in the document"
|
|
1212
|
+
};
|
|
1213
|
+
var FACT_DROPPED = {
|
|
1214
|
+
"fact-empty": "it has no words",
|
|
1215
|
+
"fact-too-long": "it is too long",
|
|
1216
|
+
"quote-empty": "it quotes nothing",
|
|
1217
|
+
"quote-too-long": "its quote is too long",
|
|
1218
|
+
"unstorable-characters": "it holds a character that cannot be stored",
|
|
1219
|
+
"quote-not-in-document": "the words it quotes are not in the document",
|
|
1220
|
+
"nothing-to-check-it-against": "there are no words in the file to check it against"
|
|
1221
|
+
};
|
|
1222
|
+
var DATE_NOT_KEPT = {
|
|
1223
|
+
"later-than-today": "it is later than today",
|
|
1224
|
+
"document-already-held": "the document was already here, with the date it came with",
|
|
1225
|
+
"stored-as-the-moment-it-arrived": "that day had not begun in universal time, so the moment it arrived is kept"
|
|
1226
|
+
};
|
|
1227
|
+
var DESCRIPTION_NOT_FILED = {
|
|
1228
|
+
nothing_to_check_it_against: "there are no words in the file to check it against"
|
|
1229
|
+
};
|
|
1230
|
+
var TYPING_NOT_USED = {
|
|
1231
|
+
not_used_file_has_its_own_text: "the file has words of its own",
|
|
1232
|
+
not_used_document_already_held: "the document was already here"
|
|
1233
|
+
};
|
|
1234
|
+
var inPlainWords = (words, name2) => words[name2] ?? name2;
|
|
1235
|
+
var SHORT_EXAMPLE = 120;
|
|
1236
|
+
function whatBecameOfIt(stored, alreadyHere, descriptionSent) {
|
|
1237
|
+
const lines = [];
|
|
1238
|
+
let filing;
|
|
1239
|
+
const description = isPlainObject(stored.description) ? stored.description : null;
|
|
1240
|
+
if (description !== null) {
|
|
1241
|
+
const state = wordsOf(description.state);
|
|
1242
|
+
const what = wordsOf(description.what);
|
|
1243
|
+
const how = wordsOf(description.how);
|
|
1244
|
+
if (descriptionSent && state !== void 0) {
|
|
1245
|
+
filing = state === "filed" ? "filed" : alreadyHere && state === "not_filed" && what === void 0 ? "already-described" : "not-kept";
|
|
1246
|
+
}
|
|
1247
|
+
if (state === "not_filed" && what !== void 0) {
|
|
1248
|
+
lines.push(how === void 0 ? what : `${what} ${how}`);
|
|
1249
|
+
} else if (state !== void 0 && state !== "filed" && state !== "none_supplied" && !(alreadyHere && state === "not_filed")) {
|
|
1250
|
+
lines.push(
|
|
1251
|
+
state === "not_filed" ? "description not filed." : `description not filed: ${inPlainWords(DESCRIPTION_NOT_FILED, state)}`
|
|
1252
|
+
);
|
|
1253
|
+
}
|
|
1254
|
+
for (const dropped of listOf(description.dropped)) {
|
|
1255
|
+
if (!isPlainObject(dropped)) continue;
|
|
1256
|
+
const line = wordsOf(dropped.line);
|
|
1257
|
+
const reason = wordsOf(dropped.reason);
|
|
1258
|
+
const what2 = wordsOf(dropped.what);
|
|
1259
|
+
const how2 = wordsOf(dropped.how);
|
|
1260
|
+
const why = what2 === void 0 ? reason === void 0 ? void 0 : inPlainWords(LINE_DROPPED, reason) : how2 === void 0 ? what2 : `${what2} ${how2}`;
|
|
1261
|
+
if (line !== void 0 && why !== void 0) lines.push(`${line} dropped: ${why}`);
|
|
1262
|
+
}
|
|
1263
|
+
const howMuch = isPlainObject(description.how_much_was_read) ? description.how_much_was_read : null;
|
|
1264
|
+
const howMuchWhat = howMuch === null ? void 0 : wordsOf(howMuch.what);
|
|
1265
|
+
if (howMuch !== null && howMuchWhat !== void 0) {
|
|
1266
|
+
const howMuchHow = wordsOf(howMuch.how);
|
|
1267
|
+
const example = howMuch.example === void 0 ? "" : JSON.stringify(howMuch.example);
|
|
1268
|
+
lines.push(
|
|
1269
|
+
`how_much_was_read not kept: ${howMuchWhat}` + (howMuchHow === void 0 ? "" : ` ${howMuchHow}`) + (example !== "" && example.length <= SHORT_EXAMPLE ? ` Example: ${example}` : "")
|
|
1270
|
+
);
|
|
1271
|
+
}
|
|
1272
|
+
}
|
|
1273
|
+
const facts = isPlainObject(stored.facts) ? stored.facts : null;
|
|
1274
|
+
for (const dropped of listOf(facts?.dropped)) {
|
|
1275
|
+
if (!isPlainObject(dropped)) continue;
|
|
1276
|
+
const reason = wordsOf(dropped.reason);
|
|
1277
|
+
if (typeof dropped.fact === "number" && reason !== void 0) {
|
|
1278
|
+
lines.push(`fact ${dropped.fact} dropped: ${inPlainWords(FACT_DROPPED, reason)}`);
|
|
1279
|
+
}
|
|
1280
|
+
}
|
|
1281
|
+
const date = isPlainObject(stored.date_not_kept) ? wordsOf(stored.date_not_kept.reason) : void 0;
|
|
1282
|
+
if (date !== void 0 && !(alreadyHere && date === "document-already-held")) {
|
|
1283
|
+
lines.push(`document_date not kept: ${inPlainWords(DATE_NOT_KEPT, date)}`);
|
|
1284
|
+
}
|
|
1285
|
+
const typed = wordsOf(stored.transcription);
|
|
1286
|
+
if (typed !== void 0 && typed !== "used" && !(alreadyHere && typed === "not_used_document_already_held")) {
|
|
1287
|
+
lines.push(`transcription not used: ${inPlainWords(TYPING_NOT_USED, typed)}`);
|
|
1288
|
+
}
|
|
1289
|
+
return {
|
|
1290
|
+
notKept: lines.map(asPrinted),
|
|
1291
|
+
...filing === void 0 ? {} : { description: filing }
|
|
1004
1292
|
};
|
|
1005
1293
|
}
|
|
1006
1294
|
async function sendOneSafely(input) {
|
|
@@ -1024,8 +1312,12 @@ var REASON_ORDER = [
|
|
|
1024
1312
|
"refused",
|
|
1025
1313
|
"cannot-open",
|
|
1026
1314
|
"did-not-finish",
|
|
1027
|
-
"leads-out"
|
|
1315
|
+
"leads-out",
|
|
1316
|
+
"entry-wrong-shape",
|
|
1317
|
+
"the-descriptions-file"
|
|
1028
1318
|
];
|
|
1319
|
+
var NAMED_ALWAYS = /* @__PURE__ */ new Set(["entry-wrong-shape"]);
|
|
1320
|
+
var namedWithoutWhy = (skip) => NAMED_ALWAYS.has(skip.reason) || skip.named === true;
|
|
1029
1321
|
var FROM_THE_SERVICE = {
|
|
1030
1322
|
type_not_readable: "type-not-readable",
|
|
1031
1323
|
too_large: "too-large"
|
|
@@ -1037,7 +1329,9 @@ var COUNTED = {
|
|
|
1037
1329
|
refused: (many) => `${many} refused`,
|
|
1038
1330
|
"cannot-open": (many) => `${many} I couldn't open`,
|
|
1039
1331
|
"did-not-finish": (many) => `${many} that didn't finish`,
|
|
1040
|
-
"leads-out": (many) => `${many} shortcut${many === 1 ? "" : "s"} out of the folder
|
|
1332
|
+
"leads-out": (many) => `${many} shortcut${many === 1 ? "" : "s"} out of the folder`,
|
|
1333
|
+
"entry-wrong-shape": (many) => `${many} with an entry in the wrong shape`,
|
|
1334
|
+
"the-descriptions-file": (many) => `${many} that ${many === 1 ? "is" : "are"} the descriptions file`
|
|
1041
1335
|
};
|
|
1042
1336
|
var IN_DETAIL = {
|
|
1043
1337
|
"type-not-readable": "a file type I can't read",
|
|
@@ -1046,16 +1340,19 @@ var IN_DETAIL = {
|
|
|
1046
1340
|
refused: "refused",
|
|
1047
1341
|
"cannot-open": "I couldn't open it",
|
|
1048
1342
|
"did-not-finish": "the send didn't finish",
|
|
1049
|
-
"leads-out": "a shortcut out of the folder"
|
|
1343
|
+
"leads-out": "a shortcut out of the folder",
|
|
1344
|
+
"entry-wrong-shape": "not sent: its entry is in the wrong shape.",
|
|
1345
|
+
"the-descriptions-file": "the descriptions file itself, never sent"
|
|
1050
1346
|
};
|
|
1051
|
-
function countLine(added, space, skipped, why) {
|
|
1052
|
-
const documents = `${added} document${added === 1 ? "" : "s"} in ${named(space)}
|
|
1347
|
+
function countLine(added, space, skipped, why, undescribed = []) {
|
|
1348
|
+
const documents = `${added} document${added === 1 ? "" : "s"} in ${named(space)}.` + (undescribed.length === 0 ? "" : ` ${undescribed.length} had no description${undescribed.length <= NAMED_WHEN_AT_MOST ? ` \u2014 ${listed(undescribed.map(asPrinted))}` : ""}.`);
|
|
1053
1349
|
if (skipped.length === 0) return documents;
|
|
1054
1350
|
const reasons = REASON_ORDER.flatMap((reason) => {
|
|
1055
1351
|
const many = skipped.filter((skip) => skip.reason === reason).length;
|
|
1056
1352
|
return many === 0 ? [] : [COUNTED[reason](many)];
|
|
1057
1353
|
}).join(", ");
|
|
1058
|
-
|
|
1354
|
+
const pointAtWhy = !why && skipped.some((skip) => !namedWithoutWhy(skip) && skip.reason !== "the-descriptions-file");
|
|
1355
|
+
return `${documents} ${skipped.length} skipped \u2014 ${reasons}.${pointAtWhy ? " Run with --why to see each." : ""}`;
|
|
1059
1356
|
}
|
|
1060
1357
|
function whyLine(skip) {
|
|
1061
1358
|
const because = skip.said === void 0 ? IN_DETAIL[skip.reason] : skip.reason === "refused" ? `refused: ${skip.said}` : skip.said;
|
|
@@ -1068,90 +1365,139 @@ var FROM_THE_WALK = {
|
|
|
1068
1365
|
async function runAdd(options, deps = {}) {
|
|
1069
1366
|
const voice = voiceFor(deps.write ?? ((line) => process.stdout.write(`${line}
|
|
1070
1367
|
`)));
|
|
1071
|
-
const
|
|
1368
|
+
const name2 = asPrinted(basename(options.file));
|
|
1072
1369
|
let isFolder;
|
|
1073
1370
|
try {
|
|
1074
1371
|
isFolder = (await statPath(options.file)).isDirectory();
|
|
1075
1372
|
} catch (err) {
|
|
1076
1373
|
voice.write(
|
|
1077
|
-
err?.code === "ENOENT" ? cannotFind(
|
|
1374
|
+
err?.code === "ENOENT" ? cannotFind(name2) : cannotOpen(name2)
|
|
1078
1375
|
);
|
|
1079
1376
|
return { ok: false };
|
|
1080
1377
|
}
|
|
1081
|
-
|
|
1378
|
+
let descriptions;
|
|
1379
|
+
if (options.describedIn !== void 0) {
|
|
1380
|
+
const read = await readDescriptions(options.describedIn);
|
|
1381
|
+
if ("said" in read) {
|
|
1382
|
+
voice.write(read.said);
|
|
1383
|
+
return { ok: false };
|
|
1384
|
+
}
|
|
1385
|
+
descriptions = read;
|
|
1386
|
+
}
|
|
1387
|
+
return isFolder ? await addFolder(options, deps, voice, descriptions) : await addOneFile(options, deps, voice, descriptions);
|
|
1082
1388
|
}
|
|
1083
|
-
async function addOneFile(options, deps, voice) {
|
|
1084
|
-
const
|
|
1389
|
+
async function addOneFile(options, deps, voice, descriptions) {
|
|
1390
|
+
const name2 = asPrinted(basename(options.file));
|
|
1085
1391
|
const handle = await openOne(options.file);
|
|
1086
1392
|
if (typeof handle === "string") {
|
|
1087
|
-
voice.write(handle === "missing" ? cannotFind(
|
|
1393
|
+
voice.write(handle === "missing" ? cannotFind(name2) : cannotOpen(name2));
|
|
1088
1394
|
return { ok: false };
|
|
1089
1395
|
}
|
|
1090
1396
|
try {
|
|
1091
|
-
const
|
|
1397
|
+
const info = await handle.stat({ bigint: true });
|
|
1398
|
+
const byteSize = Number(info.size);
|
|
1399
|
+
let described;
|
|
1400
|
+
if (descriptions !== void 0) {
|
|
1401
|
+
if (sameFile({ dev: info.dev, ino: info.ino }, descriptions.identity)) {
|
|
1402
|
+
voice.write(isTheDescriptionsFile(name2));
|
|
1403
|
+
return { ok: false };
|
|
1404
|
+
}
|
|
1405
|
+
const key = asKey(basename(options.file));
|
|
1406
|
+
const found = descriptions.entries.get(key);
|
|
1407
|
+
if (found === void 0 && descriptions.entries.size > 0) {
|
|
1408
|
+
voice.write(noEntryMatches(descriptions.name, basename(options.file)));
|
|
1409
|
+
return { ok: false };
|
|
1410
|
+
}
|
|
1411
|
+
const namingNothing = [...descriptions.entries].filter(([other]) => other !== key).flatMap(([, other]) => other.keys);
|
|
1412
|
+
if (namingNothing.length > 0) voice.write(namingNoFile(namingNothing.sort()));
|
|
1413
|
+
if (found !== void 0) {
|
|
1414
|
+
const checked = checkEntry(found);
|
|
1415
|
+
if ("wrong" in checked) {
|
|
1416
|
+
voice.write(`${name2} \u2014 not sent: ${checked.wrong}`);
|
|
1417
|
+
return { ok: false };
|
|
1418
|
+
}
|
|
1419
|
+
described = checked.described;
|
|
1420
|
+
}
|
|
1421
|
+
}
|
|
1092
1422
|
const auth2 = deps.auth ?? await resolveAuth(options);
|
|
1093
1423
|
if (auth2 === null) {
|
|
1094
1424
|
voice.write(CANNOT_REACH_DAZZER);
|
|
1095
1425
|
return { ok: false };
|
|
1096
1426
|
}
|
|
1097
|
-
const settled = await settleSpace(options, auth2, deps, voice,
|
|
1427
|
+
const settled = await settleSpace(options, auth2, deps, voice, name2);
|
|
1098
1428
|
if (settled === null) return { ok: false };
|
|
1099
1429
|
const outcome = await sendOneSafely({
|
|
1100
1430
|
path: options.file,
|
|
1101
1431
|
handle,
|
|
1102
1432
|
byteSize,
|
|
1103
1433
|
scopeId: settled.scopeId,
|
|
1434
|
+
...described === void 0 ? {} : { described },
|
|
1104
1435
|
options,
|
|
1105
1436
|
deps,
|
|
1106
1437
|
auth: auth2,
|
|
1107
1438
|
voice
|
|
1108
1439
|
});
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
return { ok: true };
|
|
1113
|
-
case "already-here":
|
|
1114
|
-
voice.write(ALREADY_IN);
|
|
1115
|
-
return { ok: true };
|
|
1116
|
-
// ONE FILE, ONE QUESTION, IN THE SERVICE'S OWN WORDS. It is the only thing that knows what it
|
|
1117
|
-
// already holds and what the space is called, and rewriting its question here would be a
|
|
1118
|
-
// second wording of a question somebody approved. Nothing was added and nobody has answered,
|
|
1119
|
-
// so this run did not do what it was asked to — the same answer a run that asked where things
|
|
1120
|
-
// go gives.
|
|
1121
|
-
case "held":
|
|
1122
|
-
voice.write(outcome.question);
|
|
1123
|
-
return { ok: false };
|
|
1124
|
-
// ONE FILE, ONE REFUSAL, AND THE PLACES IT OFFERED. The sentence is the service's and is
|
|
1125
|
-
// printed as sent; the places under it are the service's too, and without them a refusal
|
|
1126
|
-
// that says to name one of them is a dead end for the person holding this one file.
|
|
1127
|
-
case "refused":
|
|
1128
|
-
voice.write(outcome.said ?? wentWrong(name));
|
|
1129
|
-
offerThePlaces(voice.write, outcome);
|
|
1130
|
-
return { ok: false };
|
|
1131
|
-
case "did-not-finish":
|
|
1132
|
-
voice.write(didNotFinish(name));
|
|
1133
|
-
return { ok: false };
|
|
1134
|
-
case "not-reached":
|
|
1135
|
-
voice.write(CANNOT_REACH_DAZZER);
|
|
1136
|
-
return { ok: false };
|
|
1137
|
-
case "went-wrong":
|
|
1138
|
-
voice.write(wentWrong(name));
|
|
1139
|
-
return { ok: false };
|
|
1140
|
-
// IT OPENED AND WOULD NOT READ — answered in the approved sentence for a file that cannot be
|
|
1141
|
-
// opened, because that is the same thing a person can act on, and never as a crash.
|
|
1142
|
-
case "cannot-read":
|
|
1143
|
-
voice.write(cannotOpen(name));
|
|
1144
|
-
return { ok: false };
|
|
1145
|
-
case "stop":
|
|
1146
|
-
say(voice.write, outcome.answer, name);
|
|
1147
|
-
return { ok: false };
|
|
1440
|
+
const result = answerForOneFile(outcome, name2, voice);
|
|
1441
|
+
if (outcome.kind === "added" && descriptions !== void 0 && !("description" in (described ?? {}))) {
|
|
1442
|
+
voice.write(itHadNoDescription);
|
|
1148
1443
|
}
|
|
1444
|
+
if (outcome.kind === "already-here" && outcome.description !== void 0) {
|
|
1445
|
+
voice.write(
|
|
1446
|
+
outcome.description === "filed" ? itsDescriptionFiledNow : outcome.description === "already-described" ? itAlreadyHasOne : itsDescriptionNotKept
|
|
1447
|
+
);
|
|
1448
|
+
}
|
|
1449
|
+
if (outcome.kind === "added" || outcome.kind === "already-here") {
|
|
1450
|
+
for (const line of outcome.notKept) voice.write(`${name2} \u2014 ${line}`);
|
|
1451
|
+
}
|
|
1452
|
+
return result;
|
|
1149
1453
|
} finally {
|
|
1150
1454
|
await handle.close();
|
|
1151
1455
|
}
|
|
1152
1456
|
}
|
|
1153
|
-
|
|
1154
|
-
|
|
1457
|
+
function answerForOneFile(outcome, name2, voice) {
|
|
1458
|
+
switch (outcome.kind) {
|
|
1459
|
+
case "added":
|
|
1460
|
+
voice.write(`${outcome.fileName} is in Dazzer, in ${named(outcome.space)}.`);
|
|
1461
|
+
return { ok: true };
|
|
1462
|
+
case "already-here":
|
|
1463
|
+
voice.write(ALREADY_IN);
|
|
1464
|
+
return { ok: true };
|
|
1465
|
+
// ONE FILE, ONE QUESTION, IN THE SERVICE'S OWN WORDS. It is the only thing that knows what it
|
|
1466
|
+
// already holds and what the space is called, and rewriting its question here would be a
|
|
1467
|
+
// second wording of a question somebody approved. Nothing was added and nobody has answered,
|
|
1468
|
+
// so this run did not do what it was asked to — the same answer a run that asked where things
|
|
1469
|
+
// go gives.
|
|
1470
|
+
case "held":
|
|
1471
|
+
voice.write(outcome.question);
|
|
1472
|
+
return { ok: false };
|
|
1473
|
+
// ONE FILE, ONE REFUSAL, AND THE PLACES IT OFFERED. The sentence is the service's and is
|
|
1474
|
+
// printed as sent; the places under it are the service's too, and without them a refusal
|
|
1475
|
+
// that says to name one of them is a dead end for the person holding this one file.
|
|
1476
|
+
case "refused":
|
|
1477
|
+
voice.write(outcome.said ?? wentWrong(name2));
|
|
1478
|
+
offerThePlaces(voice.write, outcome);
|
|
1479
|
+
return { ok: false };
|
|
1480
|
+
case "did-not-finish":
|
|
1481
|
+
voice.write(didNotFinish(name2));
|
|
1482
|
+
return { ok: false };
|
|
1483
|
+
case "not-reached":
|
|
1484
|
+
voice.write(CANNOT_REACH_DAZZER);
|
|
1485
|
+
return { ok: false };
|
|
1486
|
+
case "went-wrong":
|
|
1487
|
+
voice.write(wentWrong(name2));
|
|
1488
|
+
return { ok: false };
|
|
1489
|
+
// IT OPENED AND WOULD NOT READ — answered in the approved sentence for a file that cannot be
|
|
1490
|
+
// opened, because that is the same thing a person can act on, and never as a crash.
|
|
1491
|
+
case "cannot-read":
|
|
1492
|
+
voice.write(cannotOpen(name2));
|
|
1493
|
+
return { ok: false };
|
|
1494
|
+
case "stop":
|
|
1495
|
+
say(voice.write, outcome.answer, name2);
|
|
1496
|
+
return { ok: false };
|
|
1497
|
+
}
|
|
1498
|
+
}
|
|
1499
|
+
async function addFolder(options, deps, voice, descriptions) {
|
|
1500
|
+
const name2 = asPrinted(basename(options.file));
|
|
1155
1501
|
const walked = await (async () => {
|
|
1156
1502
|
try {
|
|
1157
1503
|
return await walkFolder(options.file);
|
|
@@ -1160,7 +1506,19 @@ async function addFolder(options, deps, voice) {
|
|
|
1160
1506
|
}
|
|
1161
1507
|
})();
|
|
1162
1508
|
if (walked === null) {
|
|
1163
|
-
voice.write(cannotOpen(
|
|
1509
|
+
voice.write(cannotOpen(name2));
|
|
1510
|
+
return { ok: false };
|
|
1511
|
+
}
|
|
1512
|
+
const found = new Set([...walked.files, ...walked.skipped].map((one) => asKey(one.relative)));
|
|
1513
|
+
const entries = [...descriptions?.entries ?? /* @__PURE__ */ new Map()];
|
|
1514
|
+
const namingNothing = entries.filter(([key]) => !found.has(key)).flatMap(([, entry]) => entry.keys).sort();
|
|
1515
|
+
if (descriptions !== void 0 && entries.length > 0 && entries.every(([key]) => !found.has(key))) {
|
|
1516
|
+
voice.write(
|
|
1517
|
+
noEntryMatches(
|
|
1518
|
+
descriptions.name,
|
|
1519
|
+
await aKeyItWouldMatch(walked.files, descriptions.identity)
|
|
1520
|
+
)
|
|
1521
|
+
);
|
|
1164
1522
|
return { ok: false };
|
|
1165
1523
|
}
|
|
1166
1524
|
const auth2 = deps.auth ?? await resolveAuth(options);
|
|
@@ -1168,7 +1526,7 @@ async function addFolder(options, deps, voice) {
|
|
|
1168
1526
|
voice.write(CANNOT_REACH_DAZZER);
|
|
1169
1527
|
return { ok: false };
|
|
1170
1528
|
}
|
|
1171
|
-
const settled = await settleSpace(options, auth2, deps, voice,
|
|
1529
|
+
const settled = await settleSpace(options, auth2, deps, voice, name2);
|
|
1172
1530
|
if (settled === null) return { ok: false };
|
|
1173
1531
|
const skipped = walked.skipped.map((skip) => ({
|
|
1174
1532
|
relative: skip.relative,
|
|
@@ -1177,7 +1535,24 @@ async function addFolder(options, deps, voice) {
|
|
|
1177
1535
|
const held = [];
|
|
1178
1536
|
let added = 0;
|
|
1179
1537
|
let ran = true;
|
|
1538
|
+
const undescribed = [];
|
|
1539
|
+
const notKept = [];
|
|
1540
|
+
const alreadyThere = { filed: 0, alreadyDescribed: 0, notKept: 0 };
|
|
1180
1541
|
for (const file of walked.files) {
|
|
1542
|
+
let described;
|
|
1543
|
+
const entry = descriptions?.entries.get(asKey(file.relative));
|
|
1544
|
+
if (entry !== void 0) {
|
|
1545
|
+
const checked = checkEntry(entry);
|
|
1546
|
+
if ("wrong" in checked) {
|
|
1547
|
+
skipped.push({
|
|
1548
|
+
relative: file.relative,
|
|
1549
|
+
reason: "entry-wrong-shape",
|
|
1550
|
+
said: `not sent: ${checked.wrong}`
|
|
1551
|
+
});
|
|
1552
|
+
continue;
|
|
1553
|
+
}
|
|
1554
|
+
described = checked.described;
|
|
1555
|
+
}
|
|
1181
1556
|
const handle = await openOne(file.path);
|
|
1182
1557
|
if (typeof handle === "string") {
|
|
1183
1558
|
skipped.push({ relative: file.relative, reason: "cannot-open" });
|
|
@@ -1185,11 +1560,13 @@ async function addFolder(options, deps, voice) {
|
|
|
1185
1560
|
}
|
|
1186
1561
|
let outcome;
|
|
1187
1562
|
try {
|
|
1188
|
-
|
|
1563
|
+
const info = await handle.stat({ bigint: true });
|
|
1564
|
+
outcome = descriptions !== void 0 && sameFile({ dev: info.dev, ino: info.ino }, descriptions.identity) ? "the-descriptions-file" : await sendOneSafely({
|
|
1189
1565
|
path: file.path,
|
|
1190
1566
|
handle,
|
|
1191
|
-
byteSize: (
|
|
1567
|
+
byteSize: Number(info.size),
|
|
1192
1568
|
scopeId: settled.scopeId,
|
|
1569
|
+
...described === void 0 ? {} : { described },
|
|
1193
1570
|
options,
|
|
1194
1571
|
deps,
|
|
1195
1572
|
auth: auth2,
|
|
@@ -1198,8 +1575,23 @@ async function addFolder(options, deps, voice) {
|
|
|
1198
1575
|
} finally {
|
|
1199
1576
|
await handle.close();
|
|
1200
1577
|
}
|
|
1578
|
+
if (outcome === "the-descriptions-file") {
|
|
1579
|
+
skipped.push({ relative: file.relative, reason: "the-descriptions-file" });
|
|
1580
|
+
continue;
|
|
1581
|
+
}
|
|
1582
|
+
if (outcome.kind === "added" || outcome.kind === "already-here") {
|
|
1583
|
+
if (outcome.notKept.length > 0)
|
|
1584
|
+
notKept.push({ relative: file.relative, lines: outcome.notKept });
|
|
1585
|
+
}
|
|
1586
|
+
if (outcome.kind === "already-here" && outcome.description !== void 0) {
|
|
1587
|
+
const answer = outcome.description;
|
|
1588
|
+
alreadyThere[answer === "filed" ? "filed" : answer === "already-described" ? "alreadyDescribed" : "notKept"] += 1;
|
|
1589
|
+
}
|
|
1201
1590
|
if (outcome.kind === "added") {
|
|
1202
1591
|
added += 1;
|
|
1592
|
+
if (descriptions !== void 0 && !("description" in (described ?? {}))) {
|
|
1593
|
+
undescribed.push(file.relative);
|
|
1594
|
+
}
|
|
1203
1595
|
continue;
|
|
1204
1596
|
}
|
|
1205
1597
|
if (outcome.kind === "already-here") {
|
|
@@ -1220,7 +1612,11 @@ async function addFolder(options, deps, voice) {
|
|
|
1220
1612
|
// KEYED ON THE SERVICE'S CODE, never on the words it sent: the words are the person's and
|
|
1221
1613
|
// may be reworded, the code is the label the service publishes.
|
|
1222
1614
|
reason: (outcome.code === void 0 ? void 0 : FROM_THE_SERVICE[outcome.code]) ?? "refused",
|
|
1223
|
-
...outcome.said === void 0 ? {} : { said: outcome.said }
|
|
1615
|
+
...outcome.said === void 0 ? {} : { said: outcome.said },
|
|
1616
|
+
// REFUSED ALTHOUGH IT CARRIED AN ENTRY: most likely about what the entry says, which only
|
|
1617
|
+
// the person's assistant can put right — so it is named, in the service's own sentence,
|
|
1618
|
+
// without anybody having to run the whole folder again with `--why` to find out.
|
|
1619
|
+
...described === void 0 ? {} : { named: true }
|
|
1224
1620
|
});
|
|
1225
1621
|
continue;
|
|
1226
1622
|
}
|
|
@@ -1233,15 +1629,26 @@ async function addFolder(options, deps, voice) {
|
|
|
1233
1629
|
continue;
|
|
1234
1630
|
}
|
|
1235
1631
|
if (outcome.kind === "not-reached") voice.write(CANNOT_REACH_DAZZER);
|
|
1236
|
-
else say(voice.write, outcome.answer,
|
|
1632
|
+
else say(voice.write, outcome.answer, name2);
|
|
1237
1633
|
ran = false;
|
|
1238
1634
|
break;
|
|
1239
1635
|
}
|
|
1240
1636
|
skipped.sort((a, b) => a.relative < b.relative ? -1 : a.relative > b.relative ? 1 : 0);
|
|
1241
1637
|
held.sort((a, b) => a.relative < b.relative ? -1 : a.relative > b.relative ? 1 : 0);
|
|
1242
|
-
|
|
1638
|
+
notKept.sort((a, b) => a.relative < b.relative ? -1 : a.relative > b.relative ? 1 : 0);
|
|
1639
|
+
voice.write(countLine(added, settled.space, skipped, options.why === true, undescribed));
|
|
1640
|
+
if (skipped.some((skip) => skip.reason === "the-descriptions-file")) voice.write(keepItBeside);
|
|
1641
|
+
if (alreadyThere.filed + alreadyThere.alreadyDescribed + alreadyThere.notKept > 0) {
|
|
1642
|
+
voice.write(alreadyThereDescriptions(alreadyThere));
|
|
1643
|
+
}
|
|
1644
|
+
if (namingNothing.length > 0) voice.write(namingNoFile(namingNothing));
|
|
1243
1645
|
if (held.length > 0) voice.write(waitingOnYou(held.length));
|
|
1244
|
-
|
|
1646
|
+
for (const skip of skipped) {
|
|
1647
|
+
if (options.why === true || namedWithoutWhy(skip)) voice.write(whyLine(skip));
|
|
1648
|
+
}
|
|
1649
|
+
for (const one of notKept) {
|
|
1650
|
+
for (const line of one.lines) voice.write(`${asPrinted(one.relative)} \u2014 ${line}`);
|
|
1651
|
+
}
|
|
1245
1652
|
const onlyOne = held.length === 1 ? held[0] : void 0;
|
|
1246
1653
|
if (onlyOne !== void 0) {
|
|
1247
1654
|
voice.write(onlyOne.question);
|
|
@@ -1255,6 +1662,16 @@ async function addFolder(options, deps, voice) {
|
|
|
1255
1662
|
}
|
|
1256
1663
|
return { ok: ran };
|
|
1257
1664
|
}
|
|
1665
|
+
async function aKeyItWouldMatch(files, descriptionsFile) {
|
|
1666
|
+
for (const file of files) {
|
|
1667
|
+
try {
|
|
1668
|
+
const info = await statPath(file.path, { bigint: true });
|
|
1669
|
+
if (!sameFile({ dev: info.dev, ino: info.ino }, descriptionsFile)) return file.relative;
|
|
1670
|
+
} catch {
|
|
1671
|
+
}
|
|
1672
|
+
}
|
|
1673
|
+
return void 0;
|
|
1674
|
+
}
|
|
1258
1675
|
function offerThePlaces(write, offer) {
|
|
1259
1676
|
const places = offer.choices ?? [];
|
|
1260
1677
|
const keepToYourself = offer.keepToYourself === true;
|
|
@@ -1262,10 +1679,10 @@ function offerThePlaces(write, offer) {
|
|
|
1262
1679
|
for (const place of places) write(choiceLine(place.space, place.scopeId));
|
|
1263
1680
|
write(answerWith(keepToYourself, places.length > 0));
|
|
1264
1681
|
}
|
|
1265
|
-
function say(write, answer,
|
|
1682
|
+
function say(write, answer, name2) {
|
|
1266
1683
|
switch (answer.status) {
|
|
1267
1684
|
case "refused":
|
|
1268
|
-
write(answer.detail ?? wentWrong(
|
|
1685
|
+
write(answer.detail ?? wentWrong(name2));
|
|
1269
1686
|
offerThePlaces(write, answer);
|
|
1270
1687
|
return;
|
|
1271
1688
|
case "sign-in-invalid":
|
|
@@ -1978,23 +2395,31 @@ function signInServerDefaults() {
|
|
|
1978
2395
|
|
|
1979
2396
|
// src/index.ts
|
|
1980
2397
|
var require2 = createRequire(import.meta.url);
|
|
1981
|
-
var { version } = require2("../package.json");
|
|
2398
|
+
var { name, version } = require2("../package.json");
|
|
2399
|
+
var client = `${name}/${version}`;
|
|
1982
2400
|
var program = new Command();
|
|
1983
2401
|
program.name("dazzer").description("Sign in to Dazzer from your terminal").version(version);
|
|
1984
|
-
program.command("add").description("Put a file, or a whole folder, into Dazzer").argument("<path>", "The file or folder to add").option("--space <name>", "The space to put it in \u2014 the name you were shown, or its number").option("--keep-to-myself", "Keep it to yourself, rather than putting it in a shared space").option("--why", "Show each document that was skipped, and why").
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
2402
|
+
program.command("add").description("Put a file, or a whole folder, into Dazzer").argument("<path>", "The file or folder to add").option("--space <name>", "The space to put it in \u2014 the name you were shown, or its number").option("--keep-to-myself", "Keep it to yourself, rather than putting it in a shared space").option("--why", "Show each document that was skipped, and why").option(
|
|
2403
|
+
"--described-in <file>",
|
|
2404
|
+
"A JSON file saying what each file is, keyed by its path inside the folder"
|
|
2405
|
+
).action(
|
|
2406
|
+
async (file, opts) => {
|
|
2407
|
+
const env = validateEnv();
|
|
2408
|
+
const result = await runAdd({
|
|
2409
|
+
file,
|
|
2410
|
+
space: opts.space,
|
|
2411
|
+
keepToMyself: opts.keepToMyself === true,
|
|
2412
|
+
why: opts.why === true,
|
|
2413
|
+
...opts.describedIn === void 0 ? {} : { describedIn: opts.describedIn },
|
|
2414
|
+
client,
|
|
2415
|
+
baseUrl: env.DAZZER_API_BASE_URL,
|
|
2416
|
+
issuer: env.DAZZER_AUTH_ISSUER,
|
|
2417
|
+
clientId: env.DAZZER_AUTH_CLIENT_ID,
|
|
2418
|
+
resource: env.DAZZER_AUTH_RESOURCE
|
|
2419
|
+
});
|
|
2420
|
+
if (!result.ok) process.exitCode = 1;
|
|
2421
|
+
}
|
|
2422
|
+
);
|
|
1998
2423
|
var auth = program.command("auth").description("Manage sign-in");
|
|
1999
2424
|
auth.command("login").description("Sign in through your browser; falls back to a device code when headless").option("--issuer <url>", "Authorization server URL").option("--client-id <id>", "OAuth client id").option("--resource <url>", "Resource indicator for access tokens").option("--no-browser", "Use the device-code flow instead of opening a browser").action(
|
|
2000
2425
|
async (opts) => {
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dazzer-connect",
|
|
3
3
|
"private": false,
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.3.0",
|
|
5
5
|
"description": "Sign in to Dazzer \u2014 the memory your AI tools share \u2014 from a terminal, for clients that cannot sign in themselves",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"type": "module",
|
|
@@ -34,16 +34,16 @@
|
|
|
34
34
|
"dev": "tsx src/index.ts"
|
|
35
35
|
},
|
|
36
36
|
"dependencies": {
|
|
37
|
-
"@napi-rs/keyring": "^2.
|
|
37
|
+
"@napi-rs/keyring": "^2.1.0",
|
|
38
38
|
"commander": "^15.0.0",
|
|
39
39
|
"env-paths": "^4.0.0",
|
|
40
|
-
"zod": "^4.6.
|
|
40
|
+
"zod": "^4.6.5"
|
|
41
41
|
},
|
|
42
42
|
"devDependencies": {
|
|
43
43
|
"@dazzer/auth-ui": "workspace:*",
|
|
44
|
-
"@types/node": "^26.
|
|
44
|
+
"@types/node": "^26.6.2",
|
|
45
45
|
"esbuild": "^0.28.2",
|
|
46
|
-
"tsx": "^4.23.
|
|
46
|
+
"tsx": "^4.23.15",
|
|
47
47
|
"typescript": "^5.7.2",
|
|
48
48
|
"vitest": "^4.1.11"
|
|
49
49
|
},
|