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.
Files changed (3) hide show
  1. package/README.md +54 -0
  2. package/dist/index.js +519 -94
  3. 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 = (name) => `Couldn't open ${name} \u2014 check its permissions.`;
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 = (name) => `Couldn't find ${name}.`;
760
- var didNotFinish = (name) => `The send didn't finish \u2014 ${name} is not in Dazzer. Try again shortly.`;
761
- var wentWrong = (name) => `Something went wrong and ${name} is not in Dazzer. Try again shortly.`;
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(name) {
792
- return name.replace(/[\p{Cc}\p{Cf}\p{Zl}\p{Zp}]/gu, " ").trim();
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, name) {
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, name);
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(name));
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(name));
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
- if (stored.already_here === true) return { kind: "already-here" };
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
- return `${documents} ${skipped.length} skipped \u2014 ${reasons}.${why ? "" : " Run with --why to see each."}`;
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 name = asPrinted(basename(options.file));
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(name) : cannotOpen(name)
1374
+ err?.code === "ENOENT" ? cannotFind(name2) : cannotOpen(name2)
1078
1375
  );
1079
1376
  return { ok: false };
1080
1377
  }
1081
- return isFolder ? await addFolder(options, deps, voice) : await addOneFile(options, deps, voice);
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 name = asPrinted(basename(options.file));
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(name) : cannotOpen(name));
1393
+ voice.write(handle === "missing" ? cannotFind(name2) : cannotOpen(name2));
1088
1394
  return { ok: false };
1089
1395
  }
1090
1396
  try {
1091
- const byteSize = (await handle.stat()).size;
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, name);
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
- switch (outcome.kind) {
1110
- case "added":
1111
- voice.write(`${outcome.fileName} is in Dazzer, in ${named(outcome.space)}.`);
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
- async function addFolder(options, deps, voice) {
1154
- const name = asPrinted(basename(options.file));
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(name));
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, name);
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
- outcome = await sendOneSafely({
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: (await handle.stat()).size,
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, name);
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
- voice.write(countLine(added, settled.space, skipped, options.why === true));
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
- if (options.why === true) for (const skip of skipped) voice.write(whyLine(skip));
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, name) {
1682
+ function say(write, answer, name2) {
1266
1683
  switch (answer.status) {
1267
1684
  case "refused":
1268
- write(answer.detail ?? wentWrong(name));
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").action(async (file, opts) => {
1985
- const env = validateEnv();
1986
- const result = await runAdd({
1987
- file,
1988
- space: opts.space,
1989
- keepToMyself: opts.keepToMyself === true,
1990
- why: opts.why === true,
1991
- baseUrl: env.DAZZER_API_BASE_URL,
1992
- issuer: env.DAZZER_AUTH_ISSUER,
1993
- clientId: env.DAZZER_AUTH_CLIENT_ID,
1994
- resource: env.DAZZER_AUTH_RESOURCE
1995
- });
1996
- if (!result.ok) process.exitCode = 1;
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.2.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.0.0",
37
+ "@napi-rs/keyring": "^2.1.0",
38
38
  "commander": "^15.0.0",
39
39
  "env-paths": "^4.0.0",
40
- "zod": "^4.6.1"
40
+ "zod": "^4.6.5"
41
41
  },
42
42
  "devDependencies": {
43
43
  "@dazzer/auth-ui": "workspace:*",
44
- "@types/node": "^26.5.1",
44
+ "@types/node": "^26.6.2",
45
45
  "esbuild": "^0.28.2",
46
- "tsx": "^4.23.13",
46
+ "tsx": "^4.23.15",
47
47
  "typescript": "^5.7.2",
48
48
  "vitest": "^4.1.11"
49
49
  },