@agentwhy/cli 0.0.0-dev → 0.2.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/LICENSE +1 -1
- package/README.md +57 -5
- package/dist/adapter/claude-code/contract/assumptions.js +16 -4
- package/dist/adapter/claude-code/contract/denials.js +16 -1
- package/dist/adapter/claude-code/contract/desktop-sessions.js +28 -0
- package/dist/adapter/claude-code/contract/entry-points.js +6 -1
- package/dist/adapter/claude-code/contract/fields.js +2 -0
- package/dist/adapter/claude-code/contract/hooks.js +8 -0
- package/dist/adapter/claude-code/contract/identifiers.js +2 -0
- package/dist/adapter/claude-code/contract/layout.js +2 -0
- package/dist/adapter/claude-code/contract/line-types.js +2 -0
- package/dist/adapter/claude-code/contract/projects.js +2 -0
- package/dist/adapter/claude-code/contract/recognition.js +2 -0
- package/dist/adapter/claude-code/contract/settings.js +2 -0
- package/dist/adapter/claude-code/contract/task-notifications.js +2 -0
- package/dist/adapter/claude-code/contract/version.js +14 -1
- package/dist/adapter/claude-code/discovery/claude-code-project-catalogue.js +12 -12
- package/dist/adapter/claude-code/discovery/claude-code-recognition.js +2 -0
- package/dist/adapter/claude-code/discovery/claude-code-session-catalogue.js +28 -7
- package/dist/adapter/claude-code/discovery/claude-code-session-discovery.js +2 -0
- package/dist/adapter/claude-code/discovery/claude-code-session-titles.js +30 -6
- package/dist/adapter/claude-code/discovery/claude-desktop-titles.js +102 -0
- package/dist/adapter/claude-code/discovery/session-entries.js +2 -0
- package/dist/adapter/claude-code/events/delegation-index.js +2 -0
- package/dist/adapter/claude-code/events/transcript-scan.js +6 -5
- package/dist/adapter/claude-code/hooks/hook-output.js +2 -0
- package/dist/adapter/claude-code/hooks/pre-tool-use-input.js +2 -0
- package/dist/adapter/claude-code/hooks/stop-input.js +2 -0
- package/dist/adapter/claude-code/hooks/subagent-stop-input.js +4 -0
- package/dist/adapter/claude-code/probe/claude-code-probe.js +2 -0
- package/dist/adapter/claude-code/probe/collectors/agent-tool-input-key-collector.js +2 -0
- package/dist/adapter/claude-code/probe/collectors/denial-kind-collector.js +2 -0
- package/dist/adapter/claude-code/probe/collectors/denied-call-collector.js +2 -0
- package/dist/adapter/claude-code/probe/collectors/identifier-collector.js +2 -0
- package/dist/adapter/claude-code/probe/collectors/line-type-collector.js +2 -0
- package/dist/adapter/claude-code/probe/collectors/sidechain-collector.js +2 -0
- package/dist/adapter/claude-code/probe/collectors/task-notification-collector.js +2 -0
- package/dist/adapter/claude-code/probe/collectors/tool-result-reference-collector.js +2 -0
- package/dist/adapter/claude-code/probe/collectors/tool-use-collector.js +2 -0
- package/dist/adapter/claude-code/probe/collectors/tool-version-collector.js +2 -0
- package/dist/adapter/claude-code/probe/collectors/top-level-key-collector.js +2 -0
- package/dist/adapter/claude-code/probe/collectors/unknown-line-type-key-collector.js +2 -0
- package/dist/adapter/claude-code/probe/collectors/working-directory-collector.js +2 -0
- package/dist/adapter/claude-code/probe/meta-tally.js +2 -0
- package/dist/adapter/claude-code/probe/transcript-line.js +2 -0
- package/dist/adapter/claude-code/probe/transcript-tally.js +2 -0
- package/dist/adapter/claude-code/settings/deny-entries.js +2 -0
- package/dist/adapter/claude-code/settings/hook-entries.js +9 -0
- package/dist/adapter/claude-code/settings/refuse-project.js +99 -0
- package/dist/adapter/codex/contract/actions.js +2 -0
- package/dist/adapter/codex/contract/assumptions.js +2 -0
- package/dist/adapter/codex/contract/chat-folders.js +19 -0
- package/dist/adapter/codex/contract/delegations.js +2 -0
- package/dist/adapter/codex/contract/deliveries.js +2 -0
- package/dist/adapter/codex/contract/envelope.js +2 -0
- package/dist/adapter/codex/contract/hook-refusals.js +2 -0
- package/dist/adapter/codex/contract/hooks.js +51 -1
- package/dist/adapter/codex/contract/messages.js +2 -0
- package/dist/adapter/codex/contract/permissions.js +2 -0
- package/dist/adapter/codex/contract/reviews.js +2 -0
- package/dist/adapter/codex/contract/session.js +29 -1
- package/dist/adapter/codex/contract/turns.js +2 -0
- package/dist/adapter/codex/contract/version.js +2 -0
- package/dist/adapter/codex/discovery/codex-on-this-computer.js +13 -0
- package/dist/adapter/codex/discovery/codex-project-catalogue.js +2 -12
- package/dist/adapter/codex/discovery/codex-session-catalogue.js +2 -1
- package/dist/adapter/codex/discovery/codex-session-discovery.js +2 -0
- package/dist/adapter/codex/discovery/session-owners.js +2 -0
- package/dist/adapter/codex/events/action-items.js +2 -0
- package/dist/adapter/codex/events/codex-session-source.js +2 -0
- package/dist/adapter/codex/events/hook-refusals.js +2 -0
- package/dist/adapter/codex/events/rollout-scan.js +1 -1
- package/dist/adapter/codex/hooks/codex-conversation.js +54 -0
- package/dist/adapter/codex/hooks/pre-tool-use-input.js +2 -0
- package/dist/adapter/codex/hooks/reader.js +38 -0
- package/dist/adapter/codex/hooks/stop-input.js +32 -0
- package/dist/adapter/codex/hooks/stop-refusals.js +3 -6
- package/dist/adapter/codex/probe/codex-probe.js +2 -0
- package/dist/adapter/codex/settings/codex-check.js +38 -0
- package/dist/adapter/codex/settings/codex-hooks.js +168 -1
- package/dist/adapter/codex/settings/hook-approval.js +374 -0
- package/dist/cli/commands/check-cli-command.js +2 -0
- package/dist/cli/commands/codex-stop-cli-command.js +15 -12
- package/dist/cli/commands/doctor-arguments.js +2 -0
- package/dist/cli/commands/doctor-usage.js +2 -0
- package/dist/cli/commands/init-cli-command.js +10 -5
- package/dist/cli/commands/notify-cli-command.js +3 -1
- package/dist/cli/commands/refuse-cli-command.js +9 -4
- package/dist/cli/commands/report-arguments.js +2 -0
- package/dist/cli/commands/report-usage.js +2 -0
- package/dist/cli/commands/sessions-cli-command.js +9 -1
- package/dist/cli/commands/start-cli-command.js +29 -2
- package/dist/cli/commands/watch-cli-command.js +4 -2
- package/dist/cli/exit-codes.js +2 -0
- package/dist/cli.js +10 -0
- package/dist/composition-root.js +80 -15
- package/dist/core/access/command-line.js +48 -2
- package/dist/core/access/listing.js +2 -0
- package/dist/core/access/path-tokens.js +2 -0
- package/dist/core/access/protected-access.js +36 -3
- package/dist/core/access/protected-values.js +17 -0
- package/dist/core/access/search-output.js +2 -0
- package/dist/core/access/search-reach.js +2 -0
- package/dist/core/combined-project-catalogue.js +7 -1
- package/dist/core/correlation/correlate.js +13 -1
- package/dist/core/detector/prompt-signals.js +2 -0
- package/dist/core/guarded-places.js +42 -0
- package/dist/core/nearest-project.js +5 -2
- package/dist/core/policy/glob.js +2 -0
- package/dist/core/policy/parse-policy.js +2 -0
- package/dist/core/policy/policy.js +14 -2
- package/dist/core/policy/resolve-policy.js +2 -0
- package/dist/core/redaction/entropy.js +2 -0
- package/dist/core/redaction/redactor.js +2 -0
- package/dist/core/redaction/value-shapes.js +2 -0
- package/dist/core/redaction/value-trace.js +2 -0
- package/dist/core/session-catalogue.js +7 -0
- package/dist/core/session-filter.js +2 -0
- package/dist/infrastructure/clack-asker.js +2 -0
- package/dist/infrastructure/clack-chooser.js +2 -0
- package/dist/infrastructure/clack-multi-chooser.js +2 -0
- package/dist/infrastructure/file-alert-store.js +10 -3
- package/dist/infrastructure/file-checked-store.js +2 -0
- package/dist/infrastructure/file-mark-store.js +2 -0
- package/dist/infrastructure/file-onboarding-store.js +2 -0
- package/dist/infrastructure/file-page-servers.js +51 -0
- package/dist/infrastructure/folder-window.js +2 -0
- package/dist/infrastructure/node-agentwhy-invocation.js +2 -0
- package/dist/infrastructure/node-background-run.js +45 -0
- package/dist/infrastructure/node-browser.js +2 -0
- package/dist/infrastructure/node-file-system.js +42 -1
- package/dist/infrastructure/node-local-server.js +2 -0
- package/dist/infrastructure/node-page-probe.js +40 -0
- package/dist/infrastructure/os-notifier.js +2 -0
- package/dist/infrastructure/real-directory.js +2 -0
- package/dist/infrastructure/shell-command-trial.js +60 -0
- package/dist/infrastructure/terminal-banner.js +2 -0
- package/dist/ports/command-trial.js +1 -0
- package/dist/ports/directory-reader.js +14 -1
- package/dist/ports/file-replacer.js +1 -0
- package/dist/ports/mark-store.js +2 -0
- package/dist/ports/page-server.js +1 -0
- package/dist/refuse/command-reach.js +2 -0
- package/dist/refuse/command-refusal.js +19 -30
- package/dist/refuse/render/refusal-words.js +3 -1
- package/dist/report/build-report.js +59 -11
- package/dist/report/check/actions-digest.js +3 -0
- package/dist/report/check/check-lines.js +2 -1
- package/dist/report/check/render/text-digest-renderer.js +31 -9
- package/dist/report/check/session-actions.js +4 -6
- package/dist/report/check/session-check.js +8 -0
- package/dist/report/choose-policy.js +7 -1
- package/dist/report/nothing-saved-here.js +16 -0
- package/dist/report/private-files/tell-lists.js +2 -0
- package/dist/report/project-rules.js +2 -0
- package/dist/report/refusals.js +15 -0
- package/dist/report/render/home-relative.js +2 -0
- package/dist/report/render/html-head.js +2 -0
- package/dist/report/render/html-report-styles.js +2 -0
- package/dist/report/render/html-sidebar.js +2 -0
- package/dist/report/render/path-tail.js +2 -0
- package/dist/report/render/report-copy.js +2 -0
- package/dist/report/render/report-page/advanced-view.js +2 -0
- package/dist/report/render/report-page/files-view.js +2 -0
- package/dist/report/render/report-page/files.js +2 -0
- package/dist/report/render/report-page/fix-wizard-script.js +2 -0
- package/dist/report/render/report-page/helpers-view.js +2 -0
- package/dist/report/render/report-page/item-names.js +2 -0
- package/dist/report/render/report-page/protect-patterns.js +2 -0
- package/dist/report/render/report-page/report-views.js +2 -0
- package/dist/report/render/report-page/to-do-view.js +3 -2
- package/dist/report/render/text-report-renderer.js +2 -0
- package/dist/report/render/ui/add-file-popup.js +2 -0
- package/dist/report/render/ui/app-sidebar.js +2 -0
- package/dist/report/render/ui/ask-panel.js +2 -0
- package/dist/report/render/ui/avatar.js +2 -0
- package/dist/report/render/ui/backdrop.js +2 -0
- package/dist/report/render/ui/brand-mark.js +2 -0
- package/dist/report/render/ui/checklist.js +2 -0
- package/dist/report/render/ui/confirm-dialog.js +2 -0
- package/dist/report/render/ui/drawer.js +2 -0
- package/dist/report/render/ui/file-chip.js +2 -0
- package/dist/report/render/ui/fold-line.js +2 -0
- package/dist/report/render/ui/guide-card.js +2 -0
- package/dist/report/render/ui/live-script.js +2 -0
- package/dist/report/render/ui/page-shell.js +2 -0
- package/dist/report/render/ui/popup.js +2 -0
- package/dist/report/render/ui/progress.js +2 -0
- package/dist/report/render/ui/project-list.js +9 -9
- package/dist/report/render/ui/stats.js +2 -0
- package/dist/report/render/ui/status-icon.js +2 -0
- package/dist/report/render/ui/status-look.js +2 -0
- package/dist/report/render/ui/task-list.js +2 -0
- package/dist/report/render/ui/tokens.js +2 -0
- package/dist/report/render/ui/update-notice.js +2 -0
- package/dist/report/render/ui/words/app-words.js +3 -3
- package/dist/report/render/ui/words/onboarding-words.js +9 -9
- package/dist/report/render/ui/words/projects-words.js +3 -3
- package/dist/report/render/ui/words/report-words.js +11 -11
- package/dist/report/render/ui/words/settings-words.js +30 -6
- package/dist/report/rule-names.js +2 -0
- package/dist/report/session-report.js +2 -0
- package/dist/report/start/conversations/calendar-view.js +2 -0
- package/dist/report/start/conversations/conversation-columns.js +2 -0
- package/dist/report/start/conversations/empty-week.js +2 -0
- package/dist/report/start/conversations/files-window.js +2 -0
- package/dist/report/start/conversations/period-section.js +2 -0
- package/dist/report/start/conversations/periods-script.js +2 -0
- package/dist/report/start/conversations/periods.js +2 -0
- package/dist/report/start/conversations/week-view.js +2 -0
- package/dist/report/start/conversations/weeks.js +2 -0
- package/dist/report/start/detached-start.js +75 -0
- package/dist/report/start/month/month-view.js +2 -0
- package/dist/report/start/onboarding/done.js +9 -3
- package/dist/report/start/onboarding/finish-onboarding.js +6 -4
- package/dist/report/start/onboarding/intro.js +2 -0
- package/dist/report/start/onboarding/onboarding-renderer.js +4 -4
- package/dist/report/start/onboarding/onboarding-script.js +2 -0
- package/dist/report/start/onboarding/onboarding-view.js +1 -1
- package/dist/report/start/onboarding/project-step.js +1 -1
- package/dist/report/start/onboarding/steps.js +2 -0
- package/dist/report/start/onboarding/welcome.js +2 -0
- package/dist/report/start/page-notice.js +2 -0
- package/dist/report/start/projects/index-projects.js +5 -2
- package/dist/report/start/projects/moved-page.js +2 -0
- package/dist/report/start/projects/project-switch.js +2 -0
- package/dist/report/start/projects/projects-window.js +2 -0
- package/dist/report/start/projects/temporary-space.js +2 -0
- package/dist/report/start/render/start-words.js +15 -1
- package/dist/report/start/repository.js +2 -0
- package/dist/report/start/serve/index-handler.js +4 -1
- package/dist/report/start/serve/settings-setup.js +10 -0
- package/dist/report/start/session-start.js +69 -29
- package/dist/report/start/settings/alerts-tab.js +4 -1
- package/dist/report/start/settings/files-tab.js +18 -12
- package/dist/report/start/settings/general-tab.js +33 -1
- package/dist/report/start/settings/settings-renderer.js +2 -0
- package/dist/report/start/settings/settings-script.js +8 -1
- package/dist/report/start/settings/settings-view.js +29 -2
- package/dist/report/start/settings/settings-windows.js +16 -5
- package/dist/report/start/settings-files.js +2 -0
- package/dist/report/start/to-fix/done-list.js +2 -0
- package/dist/report/start/to-fix/file-window.js +2 -0
- package/dist/report/start/to-fix/to-fix-list.js +2 -0
- package/dist/report/start/to-fix/to-fix-renderer.js +2 -0
- package/dist/report/start/to-fix/to-fix-script.js +2 -0
- package/dist/report/watch/agent-alert.js +3 -0
- package/dist/report/watch/codex-turn-format.js +69 -0
- package/dist/report/watch/notice-choices.js +2 -0
- package/dist/report/watch/notice-settings.js +2 -0
- package/dist/report/watch/preferences.js +2 -0
- package/dist/report/watch/render/codex-request-words.js +106 -0
- package/dist/report/watch/render/instruction-words.js +114 -12
- package/dist/report/watch/render/notice-words.js +74 -4
- package/dist/report/watch/subagent-watch.js +270 -45
- package/dist/setup/behind.js +2 -0
- package/dist/setup/codex-mirror.js +311 -104
- package/dist/setup/not-a-project.js +2 -0
- package/dist/setup/project-setup.js +2 -0
- package/dist/shared/compare.js +2 -0
- package/dist/shared/content-version.js +2 -0
- package/dist/shared/counter.js +2 -0
- package/dist/shared/file-stamp.js +2 -0
- package/dist/shared/label.js +2 -0
- package/dist/shared/package-name.js +2 -0
- package/dist/shared/plain-invocation.js +2 -0
- package/dist/shared/printable.js +2 -0
- package/dist/shared/sentence.js +2 -0
- package/dist/shared/terminal-logo.js +2 -0
- package/dist/shared/text-table.js +2 -0
- package/dist/shared/wrap.js +2 -0
- package/package.json +2 -1
- package/dist/refuse/render/codex-stop-words.js +0 -21
package/LICENSE
CHANGED
|
@@ -187,7 +187,7 @@
|
|
|
187
187
|
same "printed page" as the copyright notice for easier
|
|
188
188
|
identification within third-party archives.
|
|
189
189
|
|
|
190
|
-
Copyright
|
|
190
|
+
Copyright 2026 Nessprim Karol Kozer
|
|
191
191
|
|
|
192
192
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
193
193
|
you may not use this file except in compliance with the License.
|
package/README.md
CHANGED
|
@@ -17,12 +17,47 @@
|
|
|
17
17
|
One command · runs on your computer · sends nothing anywhere · <a href="LICENSE">Apache 2.0</a>
|
|
18
18
|
</p>
|
|
19
19
|
|
|
20
|
+
<p align="center">
|
|
21
|
+
<img src="docs/screenshots/landing.png" alt="agentwhy: Your AI opens your files. Now you'll know which ones." width="820">
|
|
22
|
+
</p>
|
|
23
|
+
|
|
20
24
|
---
|
|
21
25
|
|
|
22
26
|
## What you see
|
|
23
27
|
|
|
24
28
|
You run one command in your project. A page opens in your browser. Most days it says there is nothing to do.
|
|
25
|
-
When there is, it
|
|
29
|
+
When there is, it tells you what to fix. The pictures below are of a demo project.
|
|
30
|
+
|
|
31
|
+
**Conversations.** Every time your AI worked for you this week, and which of those times it read something private.
|
|
32
|
+
|
|
33
|
+

|
|
34
|
+
|
|
35
|
+
**Your to-do list.** One conversation, opened: the private file your AI read, and a **Fix it** button that walks you
|
|
36
|
+
through making it safe.
|
|
37
|
+
|
|
38
|
+

|
|
39
|
+
|
|
40
|
+
**What happened.** The story of one file, step by step: who read it, where it was repeated, and where it ended up.
|
|
41
|
+
|
|
42
|
+

|
|
43
|
+
|
|
44
|
+
The same story as a diagram, from your request to the file:
|
|
45
|
+
|
|
46
|
+

|
|
47
|
+
|
|
48
|
+
And as the full record, one line for every time the file was opened:
|
|
49
|
+
|
|
50
|
+

|
|
51
|
+
|
|
52
|
+
**Files.** Everything your AI opened, with the private files on top and whether each one is blocked.
|
|
53
|
+
|
|
54
|
+

|
|
55
|
+
|
|
56
|
+
**How it went.** The helpers your AI brought in, and which of them saw your private files.
|
|
57
|
+
|
|
58
|
+

|
|
59
|
+
|
|
60
|
+
In the terminal, the same kind of finding looks like this:
|
|
26
61
|
|
|
27
62
|
```
|
|
28
63
|
the session itself · 4 actions · 1 file reached · wrote the value into files
|
|
@@ -103,8 +138,13 @@ In Claude Code, it stops the obvious routes: `init` adds rules for Claude Code's
|
|
|
103
138
|
refuses shell commands like `cat .env` or `grep -r` over a private file. It is not a sandbox. A path in a variable or a
|
|
104
139
|
script can still get through, and that is why the main job is telling you what actually happened. In Codex the same
|
|
105
140
|
check runs as a Codex hook, with the same list of files. When it blocks a command, a second hook asks Codex to explain
|
|
106
|
-
the block in its reply in the terminal or code editor, without asking you to paste a secret.
|
|
107
|
-
hooks
|
|
141
|
+
the block in its reply in the terminal or code editor, without asking you to paste a secret. The check is written into
|
|
142
|
+
your own `~/.codex/hooks.json` and approved by agentwhy itself in `~/.codex/config.toml` - its own two entries and
|
|
143
|
+
nothing else, and no folder is marked trusted - because Codex skips an unapproved hook without a word, and in VS Code
|
|
144
|
+
the first conversation starts before anything can be approved (measured on Codex 0.159). Before writing it, `init`
|
|
145
|
+
runs the check once the way Codex will, and writes nothing if that fails. So it blocks from the first message, in the
|
|
146
|
+
terminal and in VS Code, and it acts only in projects whose rules block files. If Codex changes how it checks
|
|
147
|
+
approvals, agentwhy's Settings page says the block may ask again instead of claiming it holds.
|
|
108
148
|
|
|
109
149
|
**Which agents does it work with?**
|
|
110
150
|
Claude Code, in the terminal and in code editors, and Codex. Not Cursor's own AI, Windsurf or chats on claude.ai. Each
|
|
@@ -122,8 +162,8 @@ Codex writes down less than Claude Code does, so a Codex report can answer fewer
|
|
|
122
162
|
| Which helper was asked to do what, and what it gave back | yes | yes |
|
|
123
163
|
| Which files the AI changed | yes | yes; a change that failed is not counted |
|
|
124
164
|
| What Codex was allowed to do at the time | - | yes, shown apart from your own rules |
|
|
125
|
-
| Blocking a file | yes | yes, the same files,
|
|
126
|
-
| A message in the chat after agentwhy blocks a command | yes | yes, in the terminal and code editor
|
|
165
|
+
| Blocking a file | yes | yes, the same files, from the first message - agentwhy approves its own check in your `~/.codex` files (measured on 0.159) |
|
|
166
|
+
| A message in the chat after agentwhy blocks a command | yes | yes, in the terminal and code editor |
|
|
127
167
|
| A warning when a value reached the chat, and `check` | yes | no |
|
|
128
168
|
|
|
129
169
|
Older Codex files keep the commands only inside the code that ran them, so their reports show no commands and say so.
|
|
@@ -168,8 +208,20 @@ npx @agentwhy/cli notify # what you are told when a turn ends, and whe
|
|
|
168
208
|
|
|
169
209
|
Every command, flag and hook, and what each word on the page means: [docs/reference.md](docs/reference.md).
|
|
170
210
|
|
|
211
|
+
## Author
|
|
212
|
+
|
|
213
|
+
agentwhy is made by **Karol Kozer** ([LinkedIn](https://www.linkedin.com/feed/update/urn:li:activity:7507715323744206849/)).
|
|
214
|
+
Before it, Karol made [Planby](https://planby.app/), which
|
|
215
|
+
[won first place at WaysAwards 2024](https://www.linkedin.com/posts/waysawards2024-digitalinnovation-techawards-ugcPost-7252302039907504129-uszQ/),
|
|
216
|
+
hosted by WaysConf, and has over [1,700 stars on GitHub](https://github.com/karolkozer/planby).
|
|
217
|
+
|
|
218
|
+
Something else in mind? Write to [karol@agentwhy.dev](mailto:karol@agentwhy.dev). Every message is read, and most get
|
|
219
|
+
an answer within a day or two.
|
|
220
|
+
|
|
171
221
|
## License
|
|
172
222
|
|
|
223
|
+
Copyright 2026 Nessprim Karol Kozer.
|
|
224
|
+
|
|
173
225
|
Open source, under the [Apache License 2.0](LICENSE). Use it, change it and share it for any purpose, commercial
|
|
174
226
|
use included. When you pass on a copy, keep the license, the copyright and [NOTICE](NOTICE) with it. The license
|
|
175
227
|
does not cover the name agentwhy. The full terms are in [LICENSE](LICENSE); this summary does not replace them.
|
|
@@ -123,9 +123,15 @@ export const ASSUMPTIONS = [
|
|
|
123
123
|
},
|
|
124
124
|
{
|
|
125
125
|
id: 'entry-point-per-session',
|
|
126
|
-
statement: 'The lines that hold a conversation carry entrypoint, which way into Claude Code the session was started by, and one session carries one value: cli, claude-vscode or
|
|
126
|
+
statement: 'The lines that hold a conversation carry entrypoint, which way into Claude Code the session was started by, and one session carries one value: cli, claude-vscode, sdk-cli or claude-desktop. Any other value is unknown, never the nearest of these.',
|
|
127
127
|
status: 'verified',
|
|
128
|
-
evidence: 'which-project VB5 (2026-09-28): the last megabyte of each of 244 transcripts carries it - claude-vscode 180, cli 62, sdk-cli 2 - and no transcript, tail or whole, carries two values. The same values the hooks are given as CLAUDE_CODE_ENTRYPOINT (the-agent-tells-you B9e2). The committed corpus holds a canary marker in its place, which reads as unknown',
|
|
128
|
+
evidence: 'which-project VB5 (2026-09-28): the last megabyte of each of 244 transcripts carries it - claude-vscode 180, cli 62, sdk-cli 2 - and no transcript, tail or whole, carries two values. The same values the hooks are given as CLAUDE_CODE_ENTRYPOINT (the-agent-tells-you B9e2). claude-desktop on every entrypoint-carrying line of 3 of 3 Claude desktop app conversations, one value each (claude-desktop-conversations CDB1, 2026-10-01). The committed corpus holds a canary marker in its place, which reads as unknown',
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
id: 'desktop-session-file',
|
|
132
|
+
statement: 'The Claude desktop app writes no ai-title line. It keeps one file per conversation on macOS, local_<uuid>.json two directory levels under ~/Library/Application Support/Claude/claude-code-sessions, one JSON object whose cliSessionId is the transcript session id and whose title is the name the app sidebar shows. Only those two fields are read; where the app keeps this on Windows is not measured.',
|
|
133
|
+
status: 'verified',
|
|
134
|
+
evidence: 'claude-desktop-conversations CDB1 (2026-10-01, app 2.16120.0 running Claude Code 2.1.284, macOS): 3 of 3 desktop conversations - no ai-title in any transcript read whole, against 1-2 in each of 4 terminal transcripts; 3 files, each one JSON object of 31 keys with a string cliSessionId equal to the transcript file name and the sessionId on its lines, and a non-empty string title; measured by scripts printing key names, counts, lengths and equalities only',
|
|
129
135
|
},
|
|
130
136
|
{
|
|
131
137
|
id: 'sidechain-discriminator',
|
|
@@ -135,9 +141,9 @@ export const ASSUMPTIONS = [
|
|
|
135
141
|
},
|
|
136
142
|
{
|
|
137
143
|
id: 'denial-marker',
|
|
138
|
-
statement: 'A
|
|
144
|
+
statement: 'A call that did not run is marked by toolDenialKind on its result line; permission-rule says a rule refused it, automode-blocked that auto mode did. user-rejected has been seen and is not classified.',
|
|
139
145
|
status: 'verified',
|
|
140
|
-
evidence: '5 of 5 denials',
|
|
146
|
+
evidence: '5 of 5 denials, permission-rule (2026-09-12); 5 of 5 markers of automode-blocked on a user line with one result (2026-10-01, Claude Code 2.1.236 and 2.1.284); user-rejected, 1 marker, its effect not measured',
|
|
141
147
|
},
|
|
142
148
|
{
|
|
143
149
|
id: 'meta-keys',
|
|
@@ -193,6 +199,12 @@ export const ASSUMPTIONS = [
|
|
|
193
199
|
status: 'verified',
|
|
194
200
|
evidence: '5 of 5 Stop inputs (2026-09-17, the-agent-nobody-watches B8b) and every SubagentStop input of B4 (2026-09-16). Read by `watch` to decide which project a person\'s notification choices were made for, before any of the session has been read',
|
|
195
201
|
},
|
|
202
|
+
{
|
|
203
|
+
id: 'hook-subagent-stop-agent-file',
|
|
204
|
+
statement: "A SubagentStop input names the finished agent's own file in agent_transcript_path, and a delegated agent's file is on disk when the hook runs. A named file that is not there is an agent that kept no record.",
|
|
205
|
+
status: 'verified',
|
|
206
|
+
evidence: "On disk on 4 of 4 delegated agents (2026-09-16, when-an-agent-finishes B4c) and on 2 more runs (B4f). Named and not on disk on 2 of 2 SubagentStop runs in the Claude desktop app on Claude Code 2.1.284 (2026-10-01, B4g), each about 3 seconds after the turn's Stop, with an empty agent_type, no file under subagents/ and the agent's id nowhere in the session. Read by `watch` to say such an agent once a session instead of after every turn",
|
|
207
|
+
},
|
|
196
208
|
{
|
|
197
209
|
id: 'session-title-latest-near-end',
|
|
198
210
|
statement: 'The title a session has now is its last ai-title line, and that line lies within the last megabyte of the transcript.',
|
|
@@ -1,4 +1,19 @@
|
|
|
1
|
-
|
|
1
|
+
// Copyright 2026 Nessprim Karol Kozer
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
export const KNOWN_DENIAL_KINDS = ['permission-rule', 'automode-blocked'];
|
|
4
|
+
/**
|
|
5
|
+
* Who each known value says refused the call (`specs/2026-10-01-who-stopped-it.md` WS1): a permission rule, or the
|
|
6
|
+
* reviewer - auto mode's classifier. Measured 2026-10-01 (spec §4.0): `automode-blocked` on 5 result lines of Claude
|
|
7
|
+
* Code 2.1.236 and 2.1.284, each a `user` line holding one result marked as an error; two were commands that would have
|
|
8
|
+
* started a server, and none started. A Record over the union: a value added above does not compile until it says who.
|
|
9
|
+
*
|
|
10
|
+
* `user-rejected` - a call the person turned down at a prompt - was seen once and is **not** listed: that such a call
|
|
11
|
+
* does not run has not been measured (WSB1), and a value read as "did not run" without that would be a guess.
|
|
12
|
+
*/
|
|
13
|
+
export const DENIAL_SOURCE = {
|
|
14
|
+
'permission-rule': 'rule',
|
|
15
|
+
'automode-blocked': 'reviewer',
|
|
16
|
+
};
|
|
2
17
|
// An unrecognised denial kind must surface as unknown, never be folded into a known one.
|
|
3
18
|
export function isKnownDenialKind(value) {
|
|
4
19
|
return typeof value === 'string' && KNOWN_DENIAL_KINDS.includes(value);
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// Copyright 2026 Nessprim Karol Kozer
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
/**
|
|
4
|
+
* Where the Claude desktop app keeps what it knows about a conversation, and the two fields agentwhy reads from it
|
|
5
|
+
* (`.ai/specs/2026-10-01-claude-desktop-conversations.md` CD1). The app runs Claude Code but writes no `ai-title` into
|
|
6
|
+
* the transcript: the name its sidebar shows is in a file of the app's own. Measured 2026-10-01 on macOS alone (CDB1,
|
|
7
|
+
* app 2.16120.0 running Claude Code 2.1.284, 3 of 3 desktop conversations): one JSON object per conversation, two
|
|
8
|
+
* directory levels under the folder, whose `cliSessionId` is the transcript's session id and whose `title` is the name
|
|
9
|
+
* the sidebar shows. The file also holds what must never be read - MCP server configuration, permissions, prompt
|
|
10
|
+
* snapshots - which is why the fields read are named here and nothing else is (CD4). Where the app keeps this on
|
|
11
|
+
* Windows is not measured (CDB5), so no path is guessed there (CD6).
|
|
12
|
+
*/
|
|
13
|
+
export const DESKTOP_SESSIONS = {
|
|
14
|
+
/** Under the home directory, on macOS. */
|
|
15
|
+
folder: ['Library', 'Application Support', 'Claude', 'claude-code-sessions'],
|
|
16
|
+
/** The file sits exactly this many directory levels under the folder - two ids whose meaning is not measured. */
|
|
17
|
+
depth: 2,
|
|
18
|
+
filePrefix: 'local_',
|
|
19
|
+
fileSuffix: '.json',
|
|
20
|
+
/** The transcript's session id - the join to `~/.claude/projects/<project>/<session id>.jsonl`. */
|
|
21
|
+
sessionIdField: 'cliSessionId',
|
|
22
|
+
/** The name the app's sidebar shows. A model wrote it from what the person typed: content, never structure (L009). */
|
|
23
|
+
titleField: 'title',
|
|
24
|
+
};
|
|
25
|
+
/** Whether a directory entry's name is a desktop session file. Anything else in those folders is not read. */
|
|
26
|
+
export function isDesktopSessionFileName(name) {
|
|
27
|
+
return name.startsWith(DESKTOP_SESSIONS.filePrefix) && name.endsWith(DESKTOP_SESSIONS.fileSuffix);
|
|
28
|
+
}
|
|
@@ -1,10 +1,14 @@
|
|
|
1
|
+
// Copyright 2026 Nessprim Karol Kozer
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
1
3
|
/**
|
|
2
4
|
* Which way into Claude Code a session was started by, as its transcript records it in `entrypoint` - by the words a
|
|
3
5
|
* person knows it by (`.ai/specs/2026-09-27-which-project.md` V4). These are the values Claude Code also gives its hooks
|
|
4
6
|
* as `CLAUDE_CODE_ENTRYPOINT` (`the-agent-tells-you.md` B9e2: terminal `claude`, the editor extension, `claude -p`).
|
|
5
7
|
*
|
|
6
8
|
* Measured, not guessed (which-project VB5, 2026-09-28): the last megabyte of each of 244 transcripts carries the field,
|
|
7
|
-
* `claude-vscode` in 180, `cli` in 62, `sdk-cli` in 2, and no transcript carries two values.
|
|
9
|
+
* `claude-vscode` in 180, `cli` in 62, `sdk-cli` in 2, and no transcript carries two values. A fourth value, measured
|
|
10
|
+
* 2026-10-01 (`claude-desktop-conversations.md` CDB1, CD7): the Claude desktop app writes `claude-desktop`, on every
|
|
11
|
+
* line that carries the field in 3 of 3 desktop transcripts, one value each. No other value has been
|
|
8
12
|
* seen: one that is not listed here is said to be unknown, never read as the nearest of these. What the extension writes
|
|
9
13
|
* inside Cursor, and the plugin inside JetBrains, is not measured (VB4), so `editor` is a code editor and not "VS Code".
|
|
10
14
|
*/
|
|
@@ -12,4 +16,5 @@ export const ENTRY_POINT_VALUES = {
|
|
|
12
16
|
terminal: 'cli',
|
|
13
17
|
editor: 'claude-vscode',
|
|
14
18
|
script: 'sdk-cli',
|
|
19
|
+
desktop: 'claude-desktop',
|
|
15
20
|
};
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
// Copyright 2026 Nessprim Karol Kozer
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
1
3
|
/**
|
|
2
4
|
* What a Claude Code hook is handed and what it may hand back (`specs/2026-09-16-when-an-agent-finishes.md` §2).
|
|
3
5
|
*
|
|
@@ -25,6 +27,12 @@ export const SUBAGENT_STOP = {
|
|
|
25
27
|
lastMessage: 'last_assistant_message',
|
|
26
28
|
/** The session the agent belongs to: what an alert is remembered under until its turn ends. */
|
|
27
29
|
sessionId: 'session_id',
|
|
30
|
+
/**
|
|
31
|
+
* The agent's own file, `subagents/agent-<id>.jsonl` beside the main transcript (D2). On disk when the hook ran for
|
|
32
|
+
* every delegated agent measured (B4c, B4f); named and **not** on disk on 2 of 2 runs in the Claude desktop app
|
|
33
|
+
* where nothing was delegated (B4g) - an agent that kept no record at all.
|
|
34
|
+
*/
|
|
35
|
+
agentTranscriptPath: 'agent_transcript_path',
|
|
28
36
|
},
|
|
29
37
|
};
|
|
30
38
|
/**
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
// Copyright 2026 Nessprim Karol Kozer
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
1
3
|
/**
|
|
2
4
|
* What makes a file a Claude Code transcript by its content (`2026-09-27-what-codex-wrote.md` X2): its first non-blank
|
|
3
5
|
* line is a JSON object with a string `type`, and either a string `sessionId` or a `type` the contract lists.
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
// Copyright 2026 Nessprim Karol Kozer
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
1
3
|
/**
|
|
2
4
|
* Where a project keeps Claude Code settings, and how hooks are written in them (`specs/2026-09-16-worth-running-every-day.md`
|
|
3
5
|
* R4-R8). **Documented, not measured** against this project's oracle: read from the settings and hooks references.
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
// Copyright 2026 Nessprim Karol Kozer
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
1
3
|
/**
|
|
2
4
|
* A report delivered late (`specs/2026-09-15-where-the-value-went.md` R1, R5). An agent started in the background
|
|
3
5
|
* answers its delegating call with a launch notice, and its report reaches the calling agent afterwards, in a
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
// Copyright 2026 Nessprim Karol Kozer
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
1
3
|
// The format contract: the only code that knows the Claude Code transcript format, one subject per file in this
|
|
2
4
|
// directory. Everything here was measured against a real session (spec §4.0, tests/fixtures/oracle.json). When
|
|
3
5
|
// `agentwhy doctor` reports drift, follow .ai/skills/update-format-contract/SKILL.md instead of patching callers.
|
|
@@ -43,7 +45,18 @@
|
|
|
43
45
|
// v13 (2026-09-29): recognition by content (`recognition.ts`), so a Codex rollout is never read as a transcript
|
|
44
46
|
// (`.ai/specs/2026-09-27-what-codex-wrote.md` X2, XD7). Measured over the first lines of 257 main transcripts: a string
|
|
45
47
|
// `type` and a string `sessionId` equal to the file's name in all 257; a listed `type` in 193.
|
|
46
|
-
|
|
48
|
+
// v14 (2026-10-01): a second value of `toolDenialKind`, `automode-blocked`, and who each value says refused the call
|
|
49
|
+
// (`denials.ts`, `.ai/specs/2026-10-01-who-stopped-it.md`). Measured on the 7 transcripts of one project, Claude Code
|
|
50
|
+
// 2.1.236 and 2.1.284: 5 markers, each on a `user` line holding exactly one result marked as an error, with
|
|
51
|
+
// `toolUseResult` and `sourceToolAssistantUUID` beside it - where `permission-rule` sits. A third value seen there,
|
|
52
|
+
// `user-rejected`, stays unknown until it is measured that its call does not run.
|
|
53
|
+
// v15 (2026-10-04): the Claude desktop app's session file (`desktop-sessions.ts`) - where the app keeps a
|
|
54
|
+
// conversation's title, since it writes no `ai-title` line - and `claude-desktop` as a fourth `entrypoint` value
|
|
55
|
+
// (`entry-points.ts`). Measured 2026-10-01 on macOS (`.ai/specs/2026-10-01-claude-desktop-conversations.md` CDB1,
|
|
56
|
+
// app 2.16120.0 running Claude Code 2.1.284): 3 of 3 desktop conversations, no `ai-title` in any, one
|
|
57
|
+
// `local_<uuid>.json` each whose `cliSessionId` is the transcript's session id and whose `title` is the sidebar's name.
|
|
58
|
+
// Only those two fields are read (CD4); Windows is not measured (CDB5), so no path is read there.
|
|
59
|
+
export const CONTRACT_VERSION = 15;
|
|
47
60
|
export const VERIFIED_AGAINST = {
|
|
48
61
|
claudeCode: '2.1.268',
|
|
49
62
|
session: '951c4f76-9d2a-40d2-9c76-1e57cf47ae4e',
|
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
|
|
1
|
+
// Copyright 2026 Nessprim Karol Kozer
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
import { basename, join } from 'node:path';
|
|
4
|
+
import { isDirectory } from '../../../ports/directory-reader.js';
|
|
2
5
|
import { FileAccessError } from '../../../ports/file-access-error.js';
|
|
3
6
|
import { LAYOUT } from '../contract/layout.js';
|
|
4
7
|
import { PROJECTS_DIRECTORY, matchesProject } from '../contract/projects.js';
|
|
@@ -68,10 +71,17 @@ export class ClaudeCodeProjectCatalogue {
|
|
|
68
71
|
}
|
|
69
72
|
if (path === undefined)
|
|
70
73
|
return 'unreadable';
|
|
74
|
+
// CD2: a conversation the Claude desktop app held has no `ai-title`; its row is titled by the app's own name,
|
|
75
|
+
// through this list's redactor, as the transcript's title would be.
|
|
76
|
+
if (recognised.title === undefined && this.#dependencies.desktop !== undefined) {
|
|
77
|
+
const named = await this.#dependencies.desktop.titleOf(basename(newest.path, LAYOUT.transcriptSuffix));
|
|
78
|
+
if (named !== undefined)
|
|
79
|
+
recognised = { ...recognised, title: this.#dependencies.redactor.scan(named) };
|
|
80
|
+
}
|
|
71
81
|
return {
|
|
72
82
|
id,
|
|
73
83
|
path,
|
|
74
|
-
|
|
84
|
+
folder: this.#dependencies.leaveAlone?.(path) === true ? 'not-looked' : (await isDirectory(this.#dependencies.directories, path)) ? 'there' : 'gone',
|
|
75
85
|
conversations: transcripts.length,
|
|
76
86
|
newest: { modifiedAt: newest.modifiedAt, ...recognised },
|
|
77
87
|
};
|
|
@@ -116,14 +126,4 @@ export class ClaudeCodeProjectCatalogue {
|
|
|
116
126
|
return 0;
|
|
117
127
|
}
|
|
118
128
|
}
|
|
119
|
-
async #isDirectory(path) {
|
|
120
|
-
try {
|
|
121
|
-
return (await this.#dependencies.directories.kindOf(path)) === 'directory';
|
|
122
|
-
}
|
|
123
|
-
catch (error) {
|
|
124
|
-
if (!(error instanceof FileAccessError))
|
|
125
|
-
throw error;
|
|
126
|
-
return false;
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
129
|
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
// Copyright 2026 Nessprim Karol Kozer
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
1
3
|
import { join } from 'node:path';
|
|
2
4
|
import { FileAccessError } from '../../../ports/file-access-error.js';
|
|
3
5
|
import { LAYOUT, parseSubagentFileName } from '../contract/layout.js';
|
|
@@ -15,9 +17,25 @@ export class ClaudeCodeSessionCatalogue {
|
|
|
15
17
|
this.#directories = directories;
|
|
16
18
|
this.#home = home;
|
|
17
19
|
}
|
|
20
|
+
/**
|
|
21
|
+
* Whether a folder is there and can be read: listed, not only seen, since a sandbox may show a folder it will not let
|
|
22
|
+
* an app list - and from inside it that is the same as no folder. An error the port names is "no", and nothing else
|
|
23
|
+
* is swallowed.
|
|
24
|
+
*/
|
|
25
|
+
async #canList(path) {
|
|
26
|
+
try {
|
|
27
|
+
await this.#directories.list(path);
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
30
|
+
catch (error) {
|
|
31
|
+
if (!(error instanceof FileAccessError))
|
|
32
|
+
throw error;
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
18
36
|
async list(workingDirectory) {
|
|
19
37
|
const root = join(this.#home, ...PROJECTS_DIRECTORY);
|
|
20
|
-
const directory = await this.#directoryFor(root, workingDirectory);
|
|
38
|
+
const { directory, storeListed } = await this.#directoryFor(root, workingDirectory);
|
|
21
39
|
let entries;
|
|
22
40
|
try {
|
|
23
41
|
entries = await this.#directories.list(directory);
|
|
@@ -25,8 +43,10 @@ export class ClaudeCodeSessionCatalogue {
|
|
|
25
43
|
catch (error) {
|
|
26
44
|
if (!(error instanceof FileAccessError))
|
|
27
45
|
throw error;
|
|
28
|
-
// Not found is an answer, not a failure: this project may simply never have been worked on here.
|
|
29
|
-
|
|
46
|
+
// Not found is an answer, not a failure: this project may simply never have been worked on here. Whether Claude
|
|
47
|
+
// Code keeps any conversations here at all is the other half of the answer (R28, amended 2026-10-01).
|
|
48
|
+
const store = (storeListed ?? (await this.#canList(root))) ? {} : { store: 'missing' };
|
|
49
|
+
return { directory, found: false, searched: [{ provider: 'claude-code', directory, found: false, ...store }], sessions: [] };
|
|
30
50
|
}
|
|
31
51
|
const sessions = await Promise.all(entries
|
|
32
52
|
.filter((entry) => entry.kind === 'file' && entry.name.endsWith(LAYOUT.transcriptSuffix))
|
|
@@ -41,13 +61,14 @@ export class ClaudeCodeSessionCatalogue {
|
|
|
41
61
|
/**
|
|
42
62
|
* The encoded name first, because it costs nothing when it is right. When it is not - and the rule is
|
|
43
63
|
* inferred from examples, so it will not always be - the stored directories are compared against the working
|
|
44
|
-
* directory instead of trusting the guess.
|
|
64
|
+
* directory instead of trusting the guess. Where the store was listed for that, whether it could be is said too, so
|
|
65
|
+
* it is not asked again.
|
|
45
66
|
*/
|
|
46
67
|
async #directoryFor(root, workingDirectory) {
|
|
47
68
|
const derived = join(root, projectDirectoryName(workingDirectory));
|
|
48
69
|
try {
|
|
49
70
|
if ((await this.#directories.kindOf(derived)) === 'directory')
|
|
50
|
-
return derived;
|
|
71
|
+
return { directory: derived };
|
|
51
72
|
}
|
|
52
73
|
catch (error) {
|
|
53
74
|
if (!(error instanceof FileAccessError))
|
|
@@ -56,12 +77,12 @@ export class ClaudeCodeSessionCatalogue {
|
|
|
56
77
|
try {
|
|
57
78
|
const stored = await this.#directories.list(root);
|
|
58
79
|
const match = stored.find((entry) => entry.kind === 'directory' && matchesProject(entry.name, workingDirectory));
|
|
59
|
-
return match === undefined ? derived : join(root, match.name);
|
|
80
|
+
return { directory: match === undefined ? derived : join(root, match.name), storeListed: true };
|
|
60
81
|
}
|
|
61
82
|
catch (error) {
|
|
62
83
|
if (!(error instanceof FileAccessError))
|
|
63
84
|
throw error;
|
|
64
|
-
return derived;
|
|
85
|
+
return { directory: derived, storeListed: false };
|
|
65
86
|
}
|
|
66
87
|
}
|
|
67
88
|
async #summarise(directory, fileName) {
|
|
@@ -5,24 +5,48 @@ import { recognitionIn } from './transcript-tail.js';
|
|
|
5
5
|
/**
|
|
6
6
|
* Reads what a person recognises a session by from the end of its transcript, in one read: the title Claude Code gave
|
|
7
7
|
* it, handed over only once it has passed the redactor, and which way into Claude Code it was held
|
|
8
|
-
* (`.ai/specs/2026-09-27-which-project.md` V4). Nothing else in the transcript is looked at, and nothing
|
|
8
|
+
* (`.ai/specs/2026-09-27-which-project.md` V4). Nothing else in the transcript is looked at, and nothing but the tail's
|
|
9
|
+
* answer is kept. A session the desktop app held has no `ai-title`: its title is the app's own, read the same way a
|
|
10
|
+
* Codex thread's name is (CD2) - so a row with no title can be asked again at the cost of a look at the app's folder,
|
|
11
|
+
* never another read of an unchanged transcript (CD5).
|
|
9
12
|
*/
|
|
10
13
|
export class ClaudeCodeSessionTitles {
|
|
11
14
|
#dependencies;
|
|
15
|
+
/** By the session's path: what its tail said when it was last this old. */
|
|
16
|
+
#known = new Map();
|
|
12
17
|
constructor(dependencies) {
|
|
13
18
|
this.#dependencies = dependencies;
|
|
14
19
|
}
|
|
15
20
|
async recognise(session) {
|
|
16
|
-
const {
|
|
17
|
-
|
|
21
|
+
const { redactor, desktop } = this.#dependencies;
|
|
22
|
+
const kept = this.#known.get(session.path);
|
|
23
|
+
let fromTail;
|
|
24
|
+
if (kept !== undefined && kept.modifiedAt === session.modifiedAt) {
|
|
25
|
+
fromTail = kept.recognition;
|
|
26
|
+
}
|
|
27
|
+
else {
|
|
28
|
+
const tail = await this.#tail(session.path + LAYOUT.transcriptSuffix);
|
|
29
|
+
// A transcript that is missing or unreadable is recognised by nothing, and nothing is kept: the next ask tries it.
|
|
30
|
+
if (tail === undefined)
|
|
31
|
+
return {};
|
|
32
|
+
fromTail = recognitionIn(tail, redactor);
|
|
33
|
+
this.#known.set(session.path, { modifiedAt: session.modifiedAt, recognition: fromTail });
|
|
34
|
+
}
|
|
35
|
+
if (fromTail.title !== undefined || desktop === undefined)
|
|
36
|
+
return fromTail;
|
|
37
|
+
const named = await desktop.titleOf(session.id);
|
|
38
|
+
if (named === undefined)
|
|
39
|
+
return fromTail;
|
|
40
|
+
return { ...fromTail, title: redactor.scan(named) };
|
|
41
|
+
}
|
|
42
|
+
async #tail(path) {
|
|
18
43
|
try {
|
|
19
|
-
|
|
44
|
+
return await this.#dependencies.transcripts.readTail(path, SESSION_TITLE.tailBytes);
|
|
20
45
|
}
|
|
21
46
|
catch (error) {
|
|
22
47
|
if (!(error instanceof FileAccessError))
|
|
23
48
|
throw error;
|
|
24
|
-
return
|
|
49
|
+
return undefined;
|
|
25
50
|
}
|
|
26
|
-
return recognitionIn(tail, redactor);
|
|
27
51
|
}
|
|
28
52
|
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
// Copyright 2026 Nessprim Karol Kozer
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { FileAccessError } from '../../../ports/file-access-error.js';
|
|
5
|
+
import { parseJsonObject } from '../../../shared/json.js';
|
|
6
|
+
import { oneLine } from '../../../shared/printable.js';
|
|
7
|
+
import { DESKTOP_SESSIONS, isDesktopSessionFileName } from '../contract/desktop-sessions.js';
|
|
8
|
+
/**
|
|
9
|
+
* The names the Claude desktop app gave its conversations, for the ones whose transcripts hold no `ai-title`
|
|
10
|
+
* (`.ai/specs/2026-10-01-claude-desktop-conversations.md` CD2). Each file is read for `cliSessionId` and `title` and
|
|
11
|
+
* nothing else is kept, counted or handed out (CD4); the whole walk answers nothing where the folder is missing or
|
|
12
|
+
* unreadable, which on a computer without the app is the ordinary case. The app names a conversation at its first turn,
|
|
13
|
+
* which can be after a list first showed it, so every ask walks the folders again - a look at their listings - while a
|
|
14
|
+
* file is read again only where it changed (CD5). A title leaves here raw: every caller passes it through its own
|
|
15
|
+
* redactor before anything shows it (§13.2 pitfall 7), exactly as `ai-title` is handled.
|
|
16
|
+
*/
|
|
17
|
+
export class ClaudeDesktopTitles {
|
|
18
|
+
#dependencies;
|
|
19
|
+
/** By file path. A file that could not be read is not kept, so the next ask tries it again. */
|
|
20
|
+
#read = new Map();
|
|
21
|
+
/** The walk in flight, shared by the asks of one batch so eight rows cost one walk. */
|
|
22
|
+
#names;
|
|
23
|
+
constructor(dependencies) {
|
|
24
|
+
this.#dependencies = dependencies;
|
|
25
|
+
}
|
|
26
|
+
/** The app's name for this session, raw, or nothing - which a caller never tells apart from "no app here". */
|
|
27
|
+
async titleOf(sessionId) {
|
|
28
|
+
if (this.#dependencies.folder === undefined)
|
|
29
|
+
return undefined;
|
|
30
|
+
if (this.#names === undefined) {
|
|
31
|
+
this.#names = this.#walk(this.#dependencies.folder).finally(() => {
|
|
32
|
+
this.#names = undefined;
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
return (await this.#names).get(sessionId);
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Every session file at exactly `DESKTOP_SESSIONS.depth` levels under the folder - one that sits shallower or deeper
|
|
39
|
+
* is not the measured shape and is not read. Two files naming one id with different titles name none (CD2): the
|
|
40
|
+
* contract has no way to pick, and a guess would show a person the wrong conversation's name.
|
|
41
|
+
*/
|
|
42
|
+
async #walk(folder) {
|
|
43
|
+
const names = new Map();
|
|
44
|
+
const conflicted = new Set();
|
|
45
|
+
for (const first of await this.#list(folder)) {
|
|
46
|
+
if (first.kind !== 'directory')
|
|
47
|
+
continue;
|
|
48
|
+
for (const second of await this.#list(join(folder, first.name))) {
|
|
49
|
+
if (second.kind !== 'directory')
|
|
50
|
+
continue;
|
|
51
|
+
for (const entry of await this.#list(join(folder, first.name, second.name))) {
|
|
52
|
+
if (entry.kind !== 'file' || !isDesktopSessionFileName(entry.name))
|
|
53
|
+
continue;
|
|
54
|
+
const file = await this.#file(join(folder, first.name, second.name, entry.name));
|
|
55
|
+
if (file?.id === undefined || file.title === undefined)
|
|
56
|
+
continue;
|
|
57
|
+
const known = names.get(file.id);
|
|
58
|
+
if (known !== undefined && known !== file.title) {
|
|
59
|
+
conflicted.add(file.id);
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
names.set(file.id, file.title);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
for (const id of conflicted)
|
|
67
|
+
names.delete(id);
|
|
68
|
+
return names;
|
|
69
|
+
}
|
|
70
|
+
async #list(path) {
|
|
71
|
+
try {
|
|
72
|
+
return await this.#dependencies.directories.list(path);
|
|
73
|
+
}
|
|
74
|
+
catch (error) {
|
|
75
|
+
if (!(error instanceof FileAccessError))
|
|
76
|
+
throw error;
|
|
77
|
+
return [];
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
/** One file's two fields, from the cache where it has not changed. A shape that is not CDB1's says nothing. */
|
|
81
|
+
async #file(path) {
|
|
82
|
+
const { directories, files } = this.#dependencies;
|
|
83
|
+
try {
|
|
84
|
+
const modifiedAt = await directories.modifiedAt(path);
|
|
85
|
+
const kept = this.#read.get(path);
|
|
86
|
+
if (kept !== undefined && kept.modifiedAt === modifiedAt)
|
|
87
|
+
return kept;
|
|
88
|
+
const record = parseJsonObject(await files.readText(path));
|
|
89
|
+
const id = record?.[DESKTOP_SESSIONS.sessionIdField];
|
|
90
|
+
const rawTitle = record?.[DESKTOP_SESSIONS.titleField];
|
|
91
|
+
const title = typeof rawTitle === 'string' ? oneLine(rawTitle) : '';
|
|
92
|
+
const read = { modifiedAt, ...(typeof id === 'string' && id !== '' && title !== '' ? { id, title } : {}) };
|
|
93
|
+
this.#read.set(path, read);
|
|
94
|
+
return read;
|
|
95
|
+
}
|
|
96
|
+
catch (error) {
|
|
97
|
+
if (!(error instanceof FileAccessError))
|
|
98
|
+
throw error;
|
|
99
|
+
return undefined;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
// Copyright 2026 Nessprim Karol Kozer
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
1
3
|
import { join } from 'node:path';
|
|
2
4
|
import { byString } from '../../../shared/compare.js';
|
|
3
5
|
import { LAYOUT, isToolResultFileName, parseSubagentFileName, subagentFileName } from '../contract/layout.js';
|