Skip to content

Commit be1c853

Browse files
Outlook e-mails (.msg) read as text; required label languages; installer script order fix
1 parent 8536a93 commit be1c853

14 files changed

Lines changed: 338 additions & 35 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@ Screenshots and documents for Claude, D365 labels, checks before testing, ready
1313
- `ado_create_tree`: a tree of work items from an indented list, previewed, one undo for all of it.
1414
- `ado_metrics`: velocity per sprint, cycle and lead time, what is in progress longest.
1515
- `ado_changes file=a;b;c`: several diffs in one call.
16+
- Outlook e-mails (.msg) attached to work items are read: subject, sender, recipients, date, the files attached to the
17+
e-mail and its text (plain, or HTML turned into text).
18+
- Required label languages per connection (`adolens config --connection <c> --label-languages "en-US, pl"`, or the
19+
settings menu): `ado_review` and `ado_label` report missing texts only in those languages.
1620
- Prompts (ready commands in Claude Desktop "+" and Claude Code "/"): verify, explain, story, sprint, refine, test, wiki,
1721
label, dev_start, dev_pr, dev_build, dev_why - each runs its skill, no skill upload needed.
1822
- Skills `po-refine`, `po-test`, `po-wiki`, `dev-start`, `dev-pr`, `dev-build`, `dev-why`; `po-verify`, `po-explain`

‎README.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -214,11 +214,11 @@ What each MCP tool answers, when Claude should reach for it, and the CLI command
214214
| `ado_wiki_write` | `create` a page, `append` / `prepend` to it, or `replace` it with the version from the last full read — a page changed by someone else in the meantime is never overwritten; optional change comment | release notes, keeping docs current | `adolens wiki-write "Team/Release notes" --file notes.md --mode append` |
215215
| `ado_tests` | the tests of work items (ids) or of a sprint's requirements and bugs: their Test Cases with the latest outcome per plan, suite and configuration (who, when), items without tests; `detail=full` adds the steps | "is it tested", sprint review | `adolens tests 123`, `adolens tests` (current sprint) |
216216
| `ado_test_create` | Test Cases for a work item from markdown (`## title`, steps `1. action => expected`), linked as tested-by, in the item's area and iteration; `plan` + `suite` (id or name) add them to a suite | turning acceptance criteria into tests | `adolens test-create 123 --file cases.md --plan R1 --suite Login` |
217-
| `ado_attachments` | the files of a work item and the screenshots pasted into its text or comments; `file` shows an image to Claude or gives the text of a Word, Excel or text file; `add` attaches a local file | specifications and e-mails attached to items, "look at the screenshot" | `adolens attachments 123`, `adolens attachments 123 --file 2` |
217+
| `ado_attachments` | the files of a work item and the screenshots pasted into its text or comments; `file` shows an image to Claude or gives the text of a Word, Excel, Outlook e-mail (.msg) or text file; `add` attaches a local file | specifications and e-mails attached to items, "look at the screenshot" | `adolens attachments 123`, `adolens attachments 123 --file 2` |
218218
| `ado_create_tree` | a tree of work items from an indented list (`- Feature: …`, children indented, `\| description`, `@person`), under an existing item if given; previewed first, one undo removes it all | breaking a feature into requirements and tasks | — |
219219
| `ado_metrics` | done items and estimate per finished sprint (velocity), cycle and lead time (median, 85%), what is in progress longest | "are we getting faster", planning | `adolens metrics --sprints 6` |
220220
| `ado_label` | a D365 message pasted as seen (values in place of %1 %2 … are fine) or `@File:Id`: the label, its text in every language (missing ones flagged) and the objects, X++ methods, fields and controls using it | "where does this error come from" | `adolens label "Item 1234: received 12, on hand 5"` |
221-
| `ado_review` | checks of a change, or of every change of a work item and its tasks, before testing: work item linked, labels added or used exist in every language, X++ messages from labels, no TODO left | before moving to test, PR review | `adolens review 523`, `adolens review --item 123` |
221+
| `ado_review` | checks of a change, or of every change of a work item and its tasks, before testing: work item linked, labels added or used exist in every required language (`config --label-languages`), X++ messages from labels, no TODO left | before moving to test, PR review | `adolens review 523`, `adolens review --item 123` |
222222
| `ado_my_day` | since the previous working day (or `since`): where you were mentioned (all projects), your items and followed items changed by others, pull requests waiting for your review | the start of the day | `adolens myday --since 3d` |
223223
| `ado_search` | one search over work items (full text, with the field that matched), wiki pages and code (Git and TFVC, X++ objects named), grouped; a part that is not available (e.g. Code Search not installed) is reported and the rest still answers | "how does X work", "where is Y" | `adolens search "credit limit" --scope work;wiki` |
224224
| `ado_file` | a file by TFVC path, Git path + repo, or just its name (found through Code Search); X++ objects as their list of methods, `part` shows one method; `history=true`: who changed it, when, through which pull request and for which work items | "what does this object do", "why is it like this" | `adolens file CreditCheck.xml --part validate`, `adolens file CreditCheck.xml --history` |

‎README.pl.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -217,11 +217,11 @@ Na co odpowiada każde narzędzie MCP, kiedy Claude powinien po nie sięgnąć i
217217
| `ado_wiki_write` | `create` — nowa strona, `append` / `prepend` — dopisanie, `replace` — zastąpienie z wersją z ostatniego pełnego odczytu (strony zmienionej w międzyczasie przez kogoś innego nigdy nie nadpisze); opcjonalny komentarz zmiany | notatki wydania, aktualna dokumentacja | `adolens wiki-write "Team/Release notes" --file notes.md --mode append` |
218218
| `ado_tests` | testy work itemów (id) albo wymagań i błędów sprintu: ich Test Case'y z ostatnim wynikiem w każdym planie, zestawie i konfiguracji (kto, kiedy), elementy bez testów; `detail=full` dodaje kroki | „czy to przetestowane”, przegląd sprintu | `adolens tests 123`, `adolens tests` (bieżący sprint) |
219219
| `ado_test_create` | Test Case'y dla work itemu z markdownu (`## tytuł`, kroki `1. akcja => oczekiwany wynik`), podpięte jako tested-by, w obszarze i iteracji elementu; `plan` + `suite` (id albo nazwa) dodają je do zestawu | kryteria akceptacji zamienione w testy | `adolens test-create 123 --file przypadki.md --plan R1 --suite Logowanie` |
220-
| `ado_attachments` | pliki work itemu i zrzuty ekranu wklejone w jego treść lub komentarze; `file` pokazuje obraz Claude'owi albo daje tekst pliku Word, Excel lub tekstowego; `add` dołącza plik z dysku | specyfikacje i maile przy elementach, „zobacz zrzut” | `adolens attachments 123`, `adolens attachments 123 --file 2` |
220+
| `ado_attachments` | pliki work itemu i zrzuty ekranu wklejone w jego treść lub komentarze; `file` pokazuje obraz Claude'owi albo daje tekst pliku Word, Excel, maila Outlooka (.msg) lub tekstowego; `add` dołącza plik z dysku | specyfikacje i maile przy elementach, „zobacz zrzut” | `adolens attachments 123`, `adolens attachments 123 --file 2` |
221221
| `ado_create_tree` | drzewo work itemów z listy z wcięciami (`- Feature: …`, dzieci wcięte, `\| opis`, `@osoba`), opcjonalnie pod istniejącym elementem; najpierw podgląd, jedno cofnięcie usuwa całość | rozbicie feature'a na wymagania i taski | — |
222222
| `ado_metrics` | zrobione elementy i szacunek w zakończonych sprintach (prędkość), czas realizacji (mediana, 85%), co najdłużej w toku | „czy przyspieszamy”, planowanie | `adolens metrics --sprints 6` |
223223
| `ado_label` | komunikat D365 wklejony tak, jak widać na ekranie (z wartościami zamiast %1 %2 …) albo `@Plik:Id`: etykieta, jej tekst w każdym języku (brakujące oznaczone) i obiekty, metody X++, pola i kontrolki, które jej używają | „skąd ten błąd” | `adolens label "Towar 1234: przyjęto 12, stan 5"` |
224-
| `ado_review` | sprawdzenie zmiany albo wszystkich zmian work itemu i jego tasków przed testami: powiązany work item, etykiety we wszystkich językach, komunikaty X++ z etykiet, brak TODO | przed przekazaniem do testów, przegląd PR | `adolens review 523`, `adolens review --item 123` |
224+
| `ado_review` | sprawdzenie zmiany albo wszystkich zmian work itemu i jego tasków przed testami: powiązany work item, etykiety we wszystkich wymaganych językach (`config --label-languages`), komunikaty X++ z etykiet, brak TODO | przed przekazaniem do testów, przegląd PR | `adolens review 523`, `adolens review --item 123` |
225225
| `ado_my_day` | od poprzedniego dnia roboczego (albo `since`): gdzie cię oznaczono (wszystkie projekty), twoje i obserwowane elementy zmienione przez innych, pull requesty czekające na twój przegląd | początek dnia | `adolens myday --since 3d` |
226226
| `ado_search` | jedno wyszukiwanie po work itemach (pełny tekst, z polem, w którym znaleziono), stronach wiki i kodzie (Git i TFVC, z nazwami obiektów X++), pogrupowane; niedostępna część (np. brak Code Search) jest zgłaszana, reszta odpowiada | „jak działa X”, „gdzie jest Y” | `adolens search "limit kredytowy" --scope work;wiki` |
227227
| `ado_file` | plik po ścieżce TFVC, ścieżce Git + repo albo samej nazwie (szukanej przez Code Search); obiekty X++ jako lista metod, `part` pokazuje jedną metodę; `history=true`: kto zmieniał, kiedy, w którym pull requeście i dla jakich work itemów | „co robi ten obiekt”, „dlaczego tak działa” | `adolens file CreditCheck.xml --part validate`, `adolens file CreditCheck.xml --history` |

‎packaging/inno/ado-lens.iss‎

Lines changed: 25 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -209,30 +209,6 @@ begin
209209
RegWriteExpandStringValue(HKCU, 'Environment', 'Path', Kept);
210210
end;
211211
212-
var
213-
RemoveUserData: Boolean;
214-
215-
{ Before the files go: ask whether to remove the stored PATs and the local data (default: keep them), and sign out
216-
with the program while it is still there. After: take the program folder off the PATH and delete the data. }
217-
procedure CurUninstallStepChanged(CurUninstallStep: TUninstallStep);
218-
var
219-
Code: Integer;
220-
begin
221-
if CurUninstallStep = usUninstall then
222-
begin
223-
RemoveUserData := not UninstallSilent and
224-
(MsgBox(CustomMessage('RemoveData'), mbConfirmation, MB_YESNO or MB_DEFBUTTON2) = IDYES);
225-
if RemoveUserData then
226-
Exec(ExpandConstant('{app}\bin\adolens.exe'), 'logout --all', '', SW_HIDE, ewWaitUntilTerminated, Code);
227-
RemoveCodeSkills;
228-
end
229-
else if CurUninstallStep = usPostUninstall then
230-
begin
231-
RemoveFromPath(ExpandConstant('{app}\bin'));
232-
if RemoveUserData then DelTree(ExpandConstant('{localappdata}\ado-lens'), True, True, True);
233-
end;
234-
end;
235-
236212
function HasClaudeCode: Boolean;
237213
begin
238214
Result := DirExists(ExpandConstant('{%USERPROFILE}\.claude'));
@@ -301,6 +277,31 @@ begin
301277
end;
302278
end;
303279
280+
var
281+
RemoveUserData: Boolean;
282+
283+
{ Before the files go: ask whether to remove the stored PATs and the local data (default: keep them), and sign out
284+
with the program while it is still there. After: take the program folder off the PATH and delete the data. }
285+
procedure CurUninstallStepChanged(CurUninstallStep: TUninstallStep);
286+
var
287+
Code: Integer;
288+
begin
289+
if CurUninstallStep = usUninstall then
290+
begin
291+
RemoveUserData := not UninstallSilent and
292+
(MsgBox(CustomMessage('RemoveData'), mbConfirmation, MB_YESNO or MB_DEFBUTTON2) = IDYES);
293+
if RemoveUserData then
294+
Exec(ExpandConstant('{app}\bin\adolens.exe'), 'logout --all', '', SW_HIDE, ewWaitUntilTerminated, Code);
295+
RemoveCodeSkills;
296+
end
297+
else if CurUninstallStep = usPostUninstall then
298+
begin
299+
RemoveFromPath(ExpandConstant('{app}\bin'));
300+
if RemoveUserData then DelTree(ExpandConstant('{localappdata}\ado-lens'), True, True, True);
301+
end;
302+
end;
303+
304+
304305
function SettingsFile: String;
305306
begin
306307
Result := ExpandConstant('{localappdata}\ado-lens\adolens.json');

‎src/AdoLens/Attachments.cs‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ void From(string? html, string where)
7878
if (list.Count == 0) return ($"{head}: no attachments and no pasted images", null);
7979
if (string.IsNullOrWhiteSpace(file))
8080
return ($"{head}: {list.Count} file(s)\n" + string.Join('\n', list.Select((a, i) => $"{i + 1}. {a.Name}{Size(a.Size)}{(a.Comment is { Length: > 0 } c ? $" - {c}" : "")}")) +
81-
"\nfile=<name or number> reads one: documents as text (Word, Excel, text), images are shown.", null);
81+
"\nfile=<name or number> reads one: documents as text (Word, Excel, e-mail, text), images are shown.", null);
8282

8383
var pick = int.TryParse(file, out var n) && n >= 1 && n <= list.Count ? list[n - 1]
8484
: list.FirstOrDefault(a => a.Name.Equals(file.Trim(), StringComparison.OrdinalIgnoreCase))
@@ -117,6 +117,8 @@ void From(string? html, string where)
117117
return Word(bytes);
118118
case ".xlsx" or ".xlsm":
119119
return Excel(bytes);
120+
case ".msg":
121+
return OutlookMsg.Read(bytes);
120122
case ".txt" or ".md" or ".csv" or ".json" or ".xml" or ".log" or ".sql" or ".cs" or ".xpp" or ".yml" or ".yaml" or ".html" or ".htm":
121123
return Encoding.UTF8.GetString(bytes).TrimStart('\uFEFF');
122124
}

‎src/AdoLens/Cli.cs‎

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ config show or change settings
3030
--default-connection <name> --mode po|dev|full --usage-log true|false
3131
--show-tokens true|false --add-toolset <set> --remove-toolset <set> --confirm-above <n>
3232
--language pl|en|auto (settings console and installer texts)
33+
--connection <c> --label-languages "en-US, pl"|all (languages review and label require)
3334
status configuration, connection check, metadata and Claude registrations
3435
--connection <name> --offline --quiet
3536
register add / update the 'ado-lens' MCP server in Claude Desktop and Claude Code
@@ -238,7 +239,8 @@ public int Connections()
238239
var pat = CredentialStore.Read(name) is not null ? "PAT stored" : "no stored PAT";
239240
Console.WriteLine($"{(name.Equals(settings.DefaultConnection, StringComparison.OrdinalIgnoreCase) ? "*" : " ")} {name,-16} " +
240241
$"https://dev.azure.com/{c.Organization} project {c.Project ?? "-"} team {c.Team ?? "-"} ({pat})" +
241-
(c.Environments.Count > 0 ? $"\n {"",-16} environments: {string.Join(", ", c.Environments.Select(e => $"{e.Key} = {e.Value}"))}" : ""));
242+
(c.Environments.Count > 0 ? $"\n {"",-16} environments: {string.Join(", ", c.Environments.Select(e => $"{e.Key} = {e.Value}"))}" : "") +
243+
(c.LabelLanguages.Count > 0 ? $"\n {"",-16} label languages: {string.Join(", ", c.LabelLanguages)}" : ""));
242244
}
243245
return 0;
244246
}
@@ -263,9 +265,15 @@ public int Config()
263265
changed = true;
264266
}
265267
while (cl.Opt("remove-env") is { } r) { conn.Environments.Remove(r.Trim()); changed = true; }
268+
if (cl.Opt("label-languages") is { } ll)
269+
{
270+
conn.LabelLanguages = SplitLanguages(ll);
271+
changed = true;
272+
}
266273
}
267-
else if (cl.Peek("project") is not null || cl.Peek("team") is not null || cl.Peek("env") is not null || cl.Peek("remove-env") is not null)
268-
throw new ArgumentException("--project / --team / --env need --connection <name>");
274+
else if (cl.Peek("project") is not null || cl.Peek("team") is not null || cl.Peek("env") is not null || cl.Peek("remove-env") is not null
275+
|| cl.Peek("label-languages") is not null)
276+
throw new ArgumentException("--project / --team / --env / --label-languages need --connection <name>");
269277
if (cl.Opt("default-connection") is { } d)
270278
{
271279
if (!settings.Connections.ContainsKey(d)) throw new ArgumentException($"no connection named '{d}'");
@@ -723,6 +731,7 @@ async Task<int> SetupMenuAsync()
723731
Console.WriteLine(Ui.T(" 4. Remove a connection", " 4. Usuń połączenie"));
724732
Console.WriteLine(Ui.T(" 5. Status (is everything working)", " 5. Status (czy wszystko działa)"));
725733
Console.WriteLine(Ui.T(" 6. Language: Polish / English", " 6. Język: polski / angielski"));
734+
Console.WriteLine(Ui.T(" 7. Required label languages (checks of D365 changes)", " 7. Wymagane języki etykiet (kontrola zmian D365)"));
726735
var choice = Prompt(Ui.T("Choice (Enter = done): ", "Wybór (Enter = koniec): "));
727736
switch (choice?.Trim())
728737
{
@@ -762,10 +771,30 @@ async Task<int> SetupMenuAsync()
762771
settings.Save(SettingsPath);
763772
Ui.Polish = settings.Language == "pl" || settings.Language is null && Ui.Polish;
764773
break;
774+
case "7":
775+
var cn = PickConnection(settings);
776+
if (cn is null) break;
777+
var now = settings.Connections[cn].LabelLanguages;
778+
Console.WriteLine(Ui.T($" now: {(now.Count == 0 ? "all languages of the label files" : string.Join(", ", now))}",
779+
$" teraz: {(now.Count == 0 ? "wszystkie języki plików etykiet" : string.Join(", ", now))}"));
780+
var langs = Prompt(Ui.T(" languages every label must have, e.g. en-US, pl ('all' = every one, Enter = keep): ",
781+
" języki, które musi mieć każda etykieta, np. en-US, pl ('all' = wszystkie, Enter = bez zmian): "));
782+
if (!string.IsNullOrWhiteSpace(langs))
783+
{
784+
settings.Connections[cn].LabelLanguages = SplitLanguages(langs);
785+
settings.Save(SettingsPath);
786+
Console.WriteLine(Ui.T(" saved", " zapisane"));
787+
}
788+
break;
765789
}
766790
}
767791
}
768792

793+
/// <summary>"en-US, pl" → [en-US, pl]; "all" or empty → [] (every language required).</summary>
794+
static List<string> SplitLanguages(string text) =>
795+
text.Trim().Equals("all", StringComparison.OrdinalIgnoreCase) ? []
796+
: text.Split([',', ';', ' '], StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries).Distinct(StringComparer.OrdinalIgnoreCase).ToList();
797+
769798
string? PickConnection(Settings settings)
770799
{
771800
var names = settings.Connections.Keys.ToList();

‎src/AdoLens/CommandLine.cs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ public sealed partial class CommandLine
2323
["pr"] = ["connection=", "project=", "repo=", "list=", "detail="],
2424
["pipeline"] = ["connection=", "project=", "definition=", "branch=", "top=", "detail="],
2525
["changes"] = ["connection=", "repo=", "file=", "detail="],
26-
["config"] = ["env=", "remove-env=", "connection=", "default-connection=", "project=", "team=", "mode=", "usage-log=", "add-toolset=", "remove-toolset=", "confirm-above=", "show-tokens=", "language="],
26+
["config"] = ["env=", "remove-env=", "connection=", "default-connection=", "project=", "team=", "mode=", "usage-log=", "add-toolset=", "remove-toolset=", "confirm-above=", "show-tokens=", "language=", "label-languages="],
2727
["item"] = ["connection=", "include=", "detail="],
2828
["query"] = ["connection=", "project=", "wiql=", "query=", "top="],
2929
["create"] = ["connection=", "project=", "type=", "title=", "description=", "criteria=", "assign=", "parent=", "iteration=", "area=", "tags=", "state=", "field=", "template="],

‎src/AdoLens/Labels.cs‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,8 @@ public static async Task<string> FindAsync(Lens lens, string? connection, string
137137
{
138138
var content = await ReadAsync(client, lf, ct);
139139
var value = content is null ? null : Parse(content).GetValueOrDefault(id);
140+
// A language the connection does not require is left out when the label has no text in it.
141+
if (value is null && !session.Config.LabelLanguageRequired(lf.Language)) continue;
140142
sb.AppendLine($" {lf.Language}: {(value is null ? "MISSING" : Formatting.Collapse(value, 200))}");
141143
}
142144
sb.AppendLine(" used in:");

0 commit comments

Comments
 (0)