@agentwhy/cli 0.0.0-dev
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 +202 -0
- package/NOTICE +2 -0
- package/README.md +181 -0
- package/THIRD_PARTY_NOTICES.md +31 -0
- package/dist/adapter/claude-code/contract/assumptions.js +208 -0
- package/dist/adapter/claude-code/contract/denials.js +15 -0
- package/dist/adapter/claude-code/contract/entry-points.js +15 -0
- package/dist/adapter/claude-code/contract/fields.js +73 -0
- package/dist/adapter/claude-code/contract/hooks.js +96 -0
- package/dist/adapter/claude-code/contract/identifiers.js +15 -0
- package/dist/adapter/claude-code/contract/layout.js +29 -0
- package/dist/adapter/claude-code/contract/line-types.js +26 -0
- package/dist/adapter/claude-code/contract/projects.js +23 -0
- package/dist/adapter/claude-code/contract/recognition.js +14 -0
- package/dist/adapter/claude-code/contract/session-title.js +20 -0
- package/dist/adapter/claude-code/contract/settings.js +27 -0
- package/dist/adapter/claude-code/contract/task-notifications.js +33 -0
- package/dist/adapter/claude-code/contract/tools.js +37 -0
- package/dist/adapter/claude-code/contract/version.js +51 -0
- package/dist/adapter/claude-code/discovery/claude-code-project-catalogue.js +129 -0
- package/dist/adapter/claude-code/discovery/claude-code-recognition.js +46 -0
- package/dist/adapter/claude-code/discovery/claude-code-session-catalogue.js +100 -0
- package/dist/adapter/claude-code/discovery/claude-code-session-discovery.js +61 -0
- package/dist/adapter/claude-code/discovery/claude-code-session-titles.js +28 -0
- package/dist/adapter/claude-code/discovery/discovered-session.js +1 -0
- package/dist/adapter/claude-code/discovery/session-entries.js +41 -0
- package/dist/adapter/claude-code/discovery/transcript-tail.js +81 -0
- package/dist/adapter/claude-code/events/claude-code-session-source.js +197 -0
- package/dist/adapter/claude-code/events/delegation-index.js +15 -0
- package/dist/adapter/claude-code/events/transcript-scan.js +179 -0
- package/dist/adapter/claude-code/hooks/hook-output.js +42 -0
- package/dist/adapter/claude-code/hooks/pre-tool-use-input.js +23 -0
- package/dist/adapter/claude-code/hooks/stop-input.js +25 -0
- package/dist/adapter/claude-code/hooks/subagent-stop-input.js +26 -0
- package/dist/adapter/claude-code/policy/deny-rules.js +46 -0
- package/dist/adapter/claude-code/probe/claude-code-probe.js +86 -0
- package/dist/adapter/claude-code/probe/collectors/agent-tool-input-key-collector.js +24 -0
- package/dist/adapter/claude-code/probe/collectors/denial-kind-collector.js +20 -0
- package/dist/adapter/claude-code/probe/collectors/denied-call-collector.js +11 -0
- package/dist/adapter/claude-code/probe/collectors/identifier-collector.js +19 -0
- package/dist/adapter/claude-code/probe/collectors/line-collector.js +1 -0
- package/dist/adapter/claude-code/probe/collectors/line-type-collector.js +29 -0
- package/dist/adapter/claude-code/probe/collectors/sidechain-collector.js +20 -0
- package/dist/adapter/claude-code/probe/collectors/task-notification-collector.js +51 -0
- package/dist/adapter/claude-code/probe/collectors/tool-result-reference-collector.js +31 -0
- package/dist/adapter/claude-code/probe/collectors/tool-use-collector.js +24 -0
- package/dist/adapter/claude-code/probe/collectors/tool-version-collector.js +14 -0
- package/dist/adapter/claude-code/probe/collectors/top-level-key-collector.js +14 -0
- package/dist/adapter/claude-code/probe/collectors/unknown-line-type-key-collector.js +24 -0
- package/dist/adapter/claude-code/probe/collectors/working-directory-collector.js +28 -0
- package/dist/adapter/claude-code/probe/doctor-report.js +1 -0
- package/dist/adapter/claude-code/probe/meta-tally.js +42 -0
- package/dist/adapter/claude-code/probe/session-probe.js +1 -0
- package/dist/adapter/claude-code/probe/session-tally.js +41 -0
- package/dist/adapter/claude-code/probe/transcript-line.js +30 -0
- package/dist/adapter/claude-code/probe/transcript-tally.js +74 -0
- package/dist/adapter/claude-code/settings/deny-entries.js +65 -0
- package/dist/adapter/claude-code/settings/hook-entries.js +303 -0
- package/dist/adapter/codex/contract/actions.js +69 -0
- package/dist/adapter/codex/contract/assumptions.js +55 -0
- package/dist/adapter/codex/contract/capabilities.js +24 -0
- package/dist/adapter/codex/contract/delegations.js +30 -0
- package/dist/adapter/codex/contract/deliveries.js +25 -0
- package/dist/adapter/codex/contract/envelope.js +33 -0
- package/dist/adapter/codex/contract/hook-refusals.js +14 -0
- package/dist/adapter/codex/contract/hooks.js +46 -0
- package/dist/adapter/codex/contract/messages.js +47 -0
- package/dist/adapter/codex/contract/permissions.js +27 -0
- package/dist/adapter/codex/contract/reviews.js +13 -0
- package/dist/adapter/codex/contract/session.js +31 -0
- package/dist/adapter/codex/contract/turns.js +23 -0
- package/dist/adapter/codex/contract/version.js +15 -0
- package/dist/adapter/codex/discovery/codex-conversations.js +44 -0
- package/dist/adapter/codex/discovery/codex-project-catalogue.js +49 -0
- package/dist/adapter/codex/discovery/codex-recognition.js +20 -0
- package/dist/adapter/codex/discovery/codex-session-catalogue.js +28 -0
- package/dist/adapter/codex/discovery/codex-session-discovery.js +115 -0
- package/dist/adapter/codex/discovery/codex-session-index.js +21 -0
- package/dist/adapter/codex/discovery/codex-session-titles.js +135 -0
- package/dist/adapter/codex/discovery/session-header.js +86 -0
- package/dist/adapter/codex/discovery/session-owners.js +11 -0
- package/dist/adapter/codex/discovery/session-roots.js +49 -0
- package/dist/adapter/codex/events/action-items.js +138 -0
- package/dist/adapter/codex/events/capability-records.js +26 -0
- package/dist/adapter/codex/events/codex-session-source.js +121 -0
- package/dist/adapter/codex/events/hook-refusals.js +27 -0
- package/dist/adapter/codex/events/rollout-scan.js +614 -0
- package/dist/adapter/codex/events/turn-permissions.js +60 -0
- package/dist/adapter/codex/hooks/pre-tool-use-input.js +24 -0
- package/dist/adapter/codex/hooks/stop-refusals.js +57 -0
- package/dist/adapter/codex/probe/codex-doctor-report.js +1 -0
- package/dist/adapter/codex/probe/codex-probe.js +268 -0
- package/dist/adapter/codex/settings/codex-hooks.js +83 -0
- package/dist/cli/cli-command.js +1 -0
- package/dist/cli/command-router.js +42 -0
- package/dist/cli/commands/check-cli-command.js +128 -0
- package/dist/cli/commands/codex-stop-cli-command.js +27 -0
- package/dist/cli/commands/doctor-arguments.js +32 -0
- package/dist/cli/commands/doctor-cli-command.js +31 -0
- package/dist/cli/commands/doctor-usage.js +14 -0
- package/dist/cli/commands/init-cli-command.js +123 -0
- package/dist/cli/commands/menu-cli-command.js +58 -0
- package/dist/cli/commands/notify-cli-command.js +158 -0
- package/dist/cli/commands/refuse-cli-command.js +79 -0
- package/dist/cli/commands/report-arguments.js +62 -0
- package/dist/cli/commands/report-cli-command.js +36 -0
- package/dist/cli/commands/report-usage.js +41 -0
- package/dist/cli/commands/sessions-cli-command.js +184 -0
- package/dist/cli/commands/start-cli-command.js +83 -0
- package/dist/cli/commands/watch-cli-command.js +142 -0
- package/dist/cli/exit-codes.js +13 -0
- package/dist/cli/project-switches.js +46 -0
- package/dist/cli.js +80 -0
- package/dist/composition-root.js +411 -0
- package/dist/core/access/command-line.js +454 -0
- package/dist/core/access/listing.js +58 -0
- package/dist/core/access/path-shape.js +59 -0
- package/dist/core/access/path-tokens.js +37 -0
- package/dist/core/access/protected-access.js +310 -0
- package/dist/core/access/protected-values.js +102 -0
- package/dist/core/access/recorded-effect.js +67 -0
- package/dist/core/access/returns.js +173 -0
- package/dist/core/access/search-output.js +34 -0
- package/dist/core/access/search-reach.js +268 -0
- package/dist/core/access/uses.js +143 -0
- package/dist/core/access/words-before.js +52 -0
- package/dist/core/agent.js +1 -0
- package/dist/core/capability.js +11 -0
- package/dist/core/combined-project-catalogue.js +36 -0
- package/dist/core/combined-session-catalogue.js +25 -0
- package/dist/core/completeness.js +10 -0
- package/dist/core/context.js +1 -0
- package/dist/core/correlation/correlate.js +329 -0
- package/dist/core/delegation.js +1 -0
- package/dist/core/detector/prompt-signals.js +78 -0
- package/dist/core/entry-point.js +1 -0
- package/dist/core/event.js +1 -0
- package/dist/core/evidence.js +9 -0
- package/dist/core/late-message.js +32 -0
- package/dist/core/message.js +1 -0
- package/dist/core/nearest-project.js +28 -0
- package/dist/core/policy/default-policy.js +32 -0
- package/dist/core/policy/glob.js +56 -0
- package/dist/core/policy/parse-policy.js +70 -0
- package/dist/core/policy/policy.js +20 -0
- package/dist/core/policy/resolve-policy.js +16 -0
- package/dist/core/project-catalogue.js +1 -0
- package/dist/core/project-root.js +133 -0
- package/dist/core/provider-titles.js +10 -0
- package/dist/core/redaction/entropy.js +46 -0
- package/dist/core/redaction/redacted.js +4 -0
- package/dist/core/redaction/redactor.js +339 -0
- package/dist/core/redaction/secret-patterns.js +22 -0
- package/dist/core/redaction/value-shapes.js +81 -0
- package/dist/core/redaction/value-trace.js +110 -0
- package/dist/core/review.js +1 -0
- package/dist/core/session-catalogue.js +19 -0
- package/dist/core/session-filter.js +42 -0
- package/dist/core/session-format.js +27 -0
- package/dist/core/session-model.js +1 -0
- package/dist/core/session-reader.js +22 -0
- package/dist/core/session-records.js +1 -0
- package/dist/core/session-source.js +1 -0
- package/dist/core/session-titles.js +21 -0
- package/dist/core/turn.js +1 -0
- package/dist/doctor/codex-doctor.js +14 -0
- package/dist/doctor/doctor-use-case.js +1 -0
- package/dist/doctor/format-selecting-doctor.js +28 -0
- package/dist/doctor/render/attention/attention-rule.js +1 -0
- package/dist/doctor/render/attention/doctor-attention-rules.js +28 -0
- package/dist/doctor/render/attention/rules/damaged-meta-files-rule.js +8 -0
- package/dist/doctor/render/attention/rules/damaged-transcripts-rule.js +9 -0
- package/dist/doctor/render/attention/rules/incomplete-subagent-pairs-rule.js +6 -0
- package/dist/doctor/render/attention/rules/missing-main-transcript-rule.js +5 -0
- package/dist/doctor/render/attention/rules/missing-spilled-results-rule.js +6 -0
- package/dist/doctor/render/attention/rules/multiple-working-directories-rule.js +15 -0
- package/dist/doctor/render/attention/rules/unknown-denial-kinds-rule.js +5 -0
- package/dist/doctor/render/attention/rules/unknown-line-types-rule.js +6 -0
- package/dist/doctor/render/attention/rules/unrecognised-entries-rule.js +6 -0
- package/dist/doctor/render/attention/rules/unusable-directories-rule.js +12 -0
- package/dist/doctor/render/attention/transcript-scopes.js +6 -0
- package/dist/doctor/render/codex-text-doctor-renderer.js +97 -0
- package/dist/doctor/render/doctor-renderers.js +1 -0
- package/dist/doctor/render/json-doctor-renderer.js +7 -0
- package/dist/doctor/render/output-format.js +1 -0
- package/dist/doctor/render/text-doctor-renderer.js +124 -0
- package/dist/doctor/session-doctor.js +16 -0
- package/dist/infrastructure/clack-asker.js +24 -0
- package/dist/infrastructure/clack-chooser.js +40 -0
- package/dist/infrastructure/clack-multi-chooser.js +31 -0
- package/dist/infrastructure/file-alert-store.js +120 -0
- package/dist/infrastructure/file-checked-store.js +55 -0
- package/dist/infrastructure/file-mark-store.js +67 -0
- package/dist/infrastructure/file-onboarding-store.js +93 -0
- package/dist/infrastructure/folder-window.js +26 -0
- package/dist/infrastructure/node-agentwhy-invocation.js +99 -0
- package/dist/infrastructure/node-browser.js +40 -0
- package/dist/infrastructure/node-file-system.js +111 -0
- package/dist/infrastructure/node-local-server.js +115 -0
- package/dist/infrastructure/node-text-input.js +26 -0
- package/dist/infrastructure/os-notifier.js +21 -0
- package/dist/infrastructure/osascript-folder-chooser.js +53 -0
- package/dist/infrastructure/powershell-folder-chooser.js +78 -0
- package/dist/infrastructure/real-directory.js +18 -0
- package/dist/infrastructure/stream-printer.js +11 -0
- package/dist/infrastructure/terminal-banner.js +19 -0
- package/dist/ports/agentwhy-invocation.js +1 -0
- package/dist/ports/alert-store.js +1 -0
- package/dist/ports/asker.js +1 -0
- package/dist/ports/browser.js +1 -0
- package/dist/ports/checked-store.js +1 -0
- package/dist/ports/chooser.js +1 -0
- package/dist/ports/directory-reader.js +1 -0
- package/dist/ports/file-access-error.js +11 -0
- package/dist/ports/file-reader.js +1 -0
- package/dist/ports/file-tail-reader.js +1 -0
- package/dist/ports/file-writer.js +1 -0
- package/dist/ports/folder-chooser.js +1 -0
- package/dist/ports/local-server.js +1 -0
- package/dist/ports/mark-store.js +10 -0
- package/dist/ports/multi-chooser.js +1 -0
- package/dist/ports/notifier.js +1 -0
- package/dist/ports/onboarding-store.js +1 -0
- package/dist/ports/printer.js +1 -0
- package/dist/ports/text-input.js +1 -0
- package/dist/refuse/command-reach.js +135 -0
- package/dist/refuse/command-refusal.js +88 -0
- package/dist/refuse/render/codex-stop-words.js +21 -0
- package/dist/refuse/render/refusal-words.js +34 -0
- package/dist/report/build-report.js +1037 -0
- package/dist/report/check/actions-digest.js +73 -0
- package/dist/report/check/check-lines.js +83 -0
- package/dist/report/check/mark-request.js +90 -0
- package/dist/report/check/marks.js +84 -0
- package/dist/report/check/render/text-digest-renderer.js +332 -0
- package/dist/report/check/session-actions.js +90 -0
- package/dist/report/check/session-check.js +91 -0
- package/dist/report/choose-policy.js +61 -0
- package/dist/report/flow-reads.js +18 -0
- package/dist/report/private-files/tell-lists.js +84 -0
- package/dist/report/project-rules.js +33 -0
- package/dist/report/render/home-relative.js +18 -0
- package/dist/report/render/html-head.js +26 -0
- package/dist/report/render/html-report-components.js +48 -0
- package/dist/report/render/html-report-styles.js +500 -0
- package/dist/report/render/html-sidebar.js +42 -0
- package/dist/report/render/path-tail.js +14 -0
- package/dist/report/render/report-copy.js +1177 -0
- package/dist/report/render/report-page/advanced-view.js +167 -0
- package/dist/report/render/report-page/file-story.js +114 -0
- package/dist/report/render/report-page/files-view.js +381 -0
- package/dist/report/render/report-page/files.js +77 -0
- package/dist/report/render/report-page/fix-wizard-script.js +269 -0
- package/dist/report/render/report-page/fix-wizard.js +310 -0
- package/dist/report/render/report-page/helpers-view.js +383 -0
- package/dist/report/render/report-page/helpers.js +51 -0
- package/dist/report/render/report-page/item-names.js +63 -0
- package/dist/report/render/report-page/protect-patterns.js +13 -0
- package/dist/report/render/report-page/providers.js +208 -0
- package/dist/report/render/report-page/record-view.js +367 -0
- package/dist/report/render/report-page/report-page-renderer.js +129 -0
- package/dist/report/render/report-page/report-views.js +31 -0
- package/dist/report/render/report-page/story-window.js +356 -0
- package/dist/report/render/report-page/times.js +25 -0
- package/dist/report/render/report-page/to-do-view.js +299 -0
- package/dist/report/render/report-page/to-do.js +152 -0
- package/dist/report/render/report-page.js +1 -0
- package/dist/report/render/session-view.js +250 -0
- package/dist/report/render/text-report-renderer.js +981 -0
- package/dist/report/render/ui/add-file-popup.js +122 -0
- package/dist/report/render/ui/app-sidebar.js +78 -0
- package/dist/report/render/ui/ask-panel.js +80 -0
- package/dist/report/render/ui/avatar.js +19 -0
- package/dist/report/render/ui/backdrop.js +29 -0
- package/dist/report/render/ui/brand-mark.js +4 -0
- package/dist/report/render/ui/button.js +67 -0
- package/dist/report/render/ui/callout.js +20 -0
- package/dist/report/render/ui/chat-example.js +17 -0
- package/dist/report/render/ui/checklist.js +50 -0
- package/dist/report/render/ui/confirm-dialog.js +34 -0
- package/dist/report/render/ui/data-table.js +49 -0
- package/dist/report/render/ui/drawer.js +25 -0
- package/dist/report/render/ui/file-chip.js +37 -0
- package/dist/report/render/ui/fold-line.js +22 -0
- package/dist/report/render/ui/guide-card.js +25 -0
- package/dist/report/render/ui/hero.js +37 -0
- package/dist/report/render/ui/labelled-select.js +10 -0
- package/dist/report/render/ui/live-script.js +222 -0
- package/dist/report/render/ui/local-date.js +58 -0
- package/dist/report/render/ui/mode-icon.js +6 -0
- package/dist/report/render/ui/page-shell.js +101 -0
- package/dist/report/render/ui/pill-tabs.js +47 -0
- package/dist/report/render/ui/popup.js +77 -0
- package/dist/report/render/ui/progress.js +35 -0
- package/dist/report/render/ui/project-list.js +284 -0
- package/dist/report/render/ui/scope-choice.js +19 -0
- package/dist/report/render/ui/stats.js +21 -0
- package/dist/report/render/ui/status-icon.js +30 -0
- package/dist/report/render/ui/status-look.js +21 -0
- package/dist/report/render/ui/switch.js +11 -0
- package/dist/report/render/ui/tag.js +19 -0
- package/dist/report/render/ui/task-list.js +56 -0
- package/dist/report/render/ui/tokens.js +46 -0
- package/dist/report/render/ui/update-notice.js +146 -0
- package/dist/report/render/ui/words/app-words.js +163 -0
- package/dist/report/render/ui/words/conversations-words.js +457 -0
- package/dist/report/render/ui/words/month-words.js +59 -0
- package/dist/report/render/ui/words/onboarding-words.js +446 -0
- package/dist/report/render/ui/words/projects-words.js +217 -0
- package/dist/report/render/ui/words/report-words.js +2099 -0
- package/dist/report/render/ui/words/settings-words.js +611 -0
- package/dist/report/render/ui/words/to-fix-words.js +287 -0
- package/dist/report/report-model.js +2 -0
- package/dist/report/report-use-case.js +1 -0
- package/dist/report/rule-names.js +20 -0
- package/dist/report/session-report.js +163 -0
- package/dist/report/start/app-nav.js +37 -0
- package/dist/report/start/conversations/calendar-view.js +104 -0
- package/dist/report/start/conversations/conversation-columns.js +187 -0
- package/dist/report/start/conversations/conversations-renderer.js +54 -0
- package/dist/report/start/conversations/empty-week.js +120 -0
- package/dist/report/start/conversations/files-window.js +166 -0
- package/dist/report/start/conversations/period-section.js +280 -0
- package/dist/report/start/conversations/periods-script.js +440 -0
- package/dist/report/start/conversations/periods.js +110 -0
- package/dist/report/start/conversations/week-view.js +30 -0
- package/dist/report/start/conversations/weeks.js +17 -0
- package/dist/report/start/month/month-renderer.js +50 -0
- package/dist/report/start/month/month-view.js +135 -0
- package/dist/report/start/month/months.js +17 -0
- package/dist/report/start/onboarding/done.js +171 -0
- package/dist/report/start/onboarding/finish-onboarding.js +93 -0
- package/dist/report/start/onboarding/intro.js +100 -0
- package/dist/report/start/onboarding/onboarding-changes.js +40 -0
- package/dist/report/start/onboarding/onboarding-renderer.js +195 -0
- package/dist/report/start/onboarding/onboarding-script.js +400 -0
- package/dist/report/start/onboarding/onboarding-view.js +98 -0
- package/dist/report/start/onboarding/project-step.js +210 -0
- package/dist/report/start/onboarding/steps.js +239 -0
- package/dist/report/start/onboarding/welcome.js +66 -0
- package/dist/report/start/page-notice.js +10 -0
- package/dist/report/start/projects/index-projects.js +43 -0
- package/dist/report/start/projects/moved-page.js +82 -0
- package/dist/report/start/projects/project-switch.js +138 -0
- package/dist/report/start/projects/projects-window.js +67 -0
- package/dist/report/start/projects/temporary-space.js +18 -0
- package/dist/report/start/render/air-datepicker-vendor.js +27 -0
- package/dist/report/start/render/session-status.js +41 -0
- package/dist/report/start/render/start-words.js +84 -0
- package/dist/report/start/repository.js +28 -0
- package/dist/report/start/serve/index-handler.js +372 -0
- package/dist/report/start/serve/onboarding-request.js +50 -0
- package/dist/report/start/serve/settings-request.js +1 -0
- package/dist/report/start/serve/settings-setup.js +193 -0
- package/dist/report/start/session-index.js +1 -0
- package/dist/report/start/session-start.js +974 -0
- package/dist/report/start/settings/alerts-tab.js +104 -0
- package/dist/report/start/settings/files-tab.js +169 -0
- package/dist/report/start/settings/general-tab.js +71 -0
- package/dist/report/start/settings/settings-renderer.js +254 -0
- package/dist/report/start/settings/settings-script.js +161 -0
- package/dist/report/start/settings/settings-view.js +240 -0
- package/dist/report/start/settings/settings-windows.js +334 -0
- package/dist/report/start/settings-files.js +43 -0
- package/dist/report/start/to-fix/done-list.js +45 -0
- package/dist/report/start/to-fix/file-window.js +110 -0
- package/dist/report/start/to-fix/to-fix-list.js +70 -0
- package/dist/report/start/to-fix/to-fix-renderer.js +237 -0
- package/dist/report/start/to-fix/to-fix-script.js +94 -0
- package/dist/report/start/to-fix/to-fix-view.js +124 -0
- package/dist/report/watch/agent-alert.js +72 -0
- package/dist/report/watch/notice-choices.js +35 -0
- package/dist/report/watch/notice-settings.js +65 -0
- package/dist/report/watch/preferences.js +100 -0
- package/dist/report/watch/render/instruction-words.js +54 -0
- package/dist/report/watch/render/notice-words.js +182 -0
- package/dist/report/watch/subagent-watch.js +345 -0
- package/dist/report/watch/watch-notice.js +1 -0
- package/dist/setup/behind.js +18 -0
- package/dist/setup/codex-mirror.js +171 -0
- package/dist/setup/not-a-project.js +19 -0
- package/dist/setup/project-setup.js +644 -0
- package/dist/setup/protected-patterns.js +30 -0
- package/dist/shared/colour.js +23 -0
- package/dist/shared/compare.js +4 -0
- package/dist/shared/content-version.js +13 -0
- package/dist/shared/counter.js +11 -0
- package/dist/shared/file-stamp.js +4 -0
- package/dist/shared/json.js +13 -0
- package/dist/shared/label.js +13 -0
- package/dist/shared/package-name.js +6 -0
- package/dist/shared/plain-invocation.js +44 -0
- package/dist/shared/plain-version.js +21 -0
- package/dist/shared/printable.js +25 -0
- package/dist/shared/renderer.js +1 -0
- package/dist/shared/sentence.js +20 -0
- package/dist/shared/terminal-logo.js +41 -0
- package/dist/shared/text-table.js +15 -0
- package/dist/shared/wrap.js +29 -0
- package/package.json +70 -0
|
@@ -0,0 +1,454 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The paths a command line could be addressing.
|
|
3
|
+
*
|
|
4
|
+
* A command has structure, which spec §6.5 names as the reason an earlier hook could be fixed: separators, and
|
|
5
|
+
* quoting. Both are used here instead of a list of "commands that read", because such a list is never complete
|
|
6
|
+
* and the first program missing from it passes unseen.
|
|
7
|
+
*
|
|
8
|
+
* Quoting does most of the work. `grep -rn "SECRET" apps` names `apps`; `echo "see .env for the value"` names
|
|
9
|
+
* nothing, and splitting that sentence into words - which is what a plain tokeniser does - is where most of the
|
|
10
|
+
* noise came from. A quoted string stays **one** token, so a sentence mentioning a file no longer matches a
|
|
11
|
+
* path pattern, while a genuinely quoted path (`cat "my file.env"`) still does.
|
|
12
|
+
*
|
|
13
|
+
* **A command is read once, a character at a time, as a shell reads it** (`CommandReader`). It used to be read in
|
|
14
|
+
* passes - lines, then operators, then words - and each pass had to guess what the next one knew: a heredoc body
|
|
15
|
+
* looked like commands, a quoted script was cut at its own semicolons, and a quote spanning lines hid the command
|
|
16
|
+
* after it. Each of those was measured on a real session (`specs/2026-09-15-findings-worth-reading.md` §2).
|
|
17
|
+
*
|
|
18
|
+
* **Code an interpreter is handed on the command line is code** (`specs/2026-09-15-paths-not-fragments.md` R1, added
|
|
19
|
+
* 2026-09-15). `node -e "log('check apps/web/.env')"` names no file, and the string literal inside it was being
|
|
20
|
+
* reported as one. From such a word only a candidate shaped like a whole path is kept.
|
|
21
|
+
*
|
|
22
|
+
* **Known limits.** A path built at runtime (`$DIR/.env`) is not recognised. A quoted string that is exactly a
|
|
23
|
+
* path is counted, which is correct, and a quoted sentence that is exactly a path-shaped word would be too. A
|
|
24
|
+
* string handed to `sh -c` is one word, not a command; none was found on the sessions measured. A heredoc inside
|
|
25
|
+
* `$(...)` can end the substitution early at a `)` in its body. An interpreter's option that takes a value of its
|
|
26
|
+
* own before the code flag (`node -r x -e "…"`) ends the search for its code, and that code then reads as any other
|
|
27
|
+
* argument does - noise, never silence - as does an interpreter started through another program (`npx tsx -e`).
|
|
28
|
+
*/
|
|
29
|
+
var _a;
|
|
30
|
+
import { shapedLikePath } from './path-shape.js';
|
|
31
|
+
/**
|
|
32
|
+
* Programs whose output is the **content** of what they were pointed at, rather than a list of what they
|
|
33
|
+
* reached. `cat .env` prints a file; the paths inside that file are the file's text, not places the command
|
|
34
|
+
* went. Anything not on this list keeps the conservative reading - its output is treated as a listing - so a
|
|
35
|
+
* program missing from here costs noise, never silence.
|
|
36
|
+
*/
|
|
37
|
+
const PRINTS_CONTENT = new Set([
|
|
38
|
+
'cat',
|
|
39
|
+
'head',
|
|
40
|
+
'tail',
|
|
41
|
+
'bat',
|
|
42
|
+
'less',
|
|
43
|
+
'more',
|
|
44
|
+
'echo',
|
|
45
|
+
'printf',
|
|
46
|
+
'jq',
|
|
47
|
+
'base64',
|
|
48
|
+
'strings',
|
|
49
|
+
'xxd',
|
|
50
|
+
'od',
|
|
51
|
+
]);
|
|
52
|
+
/** `NAME=value` before a program sets a variable; it runs nothing. */
|
|
53
|
+
const ASSIGNMENT = /^[A-Za-z_][A-Za-z0-9_]*=/;
|
|
54
|
+
/** Unquoted, each of these ends a simple command. `&` directly before `>` redirects instead (`&>file`). */
|
|
55
|
+
const ENDS_A_COMMAND = new Set([';', '|', '&', '(', ')']);
|
|
56
|
+
/** Unquoted, each of these ends a word and nothing more. A newline ends the command too, and is read apart. */
|
|
57
|
+
const BLANK = new Set([' ', '\t', '\r', '\f', '\v']);
|
|
58
|
+
/** What may follow the first character of a redirection and still belong to it: `>>`, `>|`, `2>&1`. */
|
|
59
|
+
const REDIRECTION = new Set(['<', '>', '&', '|']);
|
|
60
|
+
/** Inside double quotes a backslash escapes only these; before anything else it is itself. */
|
|
61
|
+
const ESCAPED_IN_DOUBLE_QUOTES = new Set(['"', '\\', '$', '`', '\n']);
|
|
62
|
+
/**
|
|
63
|
+
* A command line read the way a shell reads it, as far as the questions asked of it need.
|
|
64
|
+
*
|
|
65
|
+
* **Added 2026-09-14** (`specs/2026-09-15-findings-worth-reading.md` R1-R4b), in three measured steps. A heredoc body is
|
|
66
|
+
* **data** - a script, a JSON document, prose on standard input - and splitting the command on newlines had made
|
|
67
|
+
* each of its lines a command: `import`, `const` and `x` were reported as programs, the largest single source of
|
|
68
|
+
* findings that named no file. Operators were then cut before quoting was known, so a quoted `node -e` script fell
|
|
69
|
+
* apart at its own semicolons. And a reading that fixed that line by line still lost the command after a quote
|
|
70
|
+
* spanning lines, and every program inside a quoted `$(...)`.
|
|
71
|
+
*
|
|
72
|
+
* A heredoc body with no terminator runs to the end of the command, as a quote left open does.
|
|
73
|
+
*/
|
|
74
|
+
function readCommand(command) {
|
|
75
|
+
return new CommandReader(command).read();
|
|
76
|
+
}
|
|
77
|
+
class CommandReader {
|
|
78
|
+
#text;
|
|
79
|
+
#at = 0;
|
|
80
|
+
#simples = [];
|
|
81
|
+
/** Heredocs opened on the current line. Their bodies begin after its newline, in the order they were opened. */
|
|
82
|
+
#pending = [];
|
|
83
|
+
#words = [];
|
|
84
|
+
#word = '';
|
|
85
|
+
#next = 'argument';
|
|
86
|
+
constructor(text) {
|
|
87
|
+
this.#text = text;
|
|
88
|
+
}
|
|
89
|
+
read() {
|
|
90
|
+
const text = this.#text;
|
|
91
|
+
while (this.#at < text.length) {
|
|
92
|
+
const character = text.charAt(this.#at);
|
|
93
|
+
const following = text.charAt(this.#at + 1);
|
|
94
|
+
if (this.#readSubstitution())
|
|
95
|
+
continue;
|
|
96
|
+
if (character === '\\') {
|
|
97
|
+
// Before a newline it joins two lines; before anything else it makes that character plain.
|
|
98
|
+
if (following !== '\n')
|
|
99
|
+
this.#word += following;
|
|
100
|
+
this.#at += 2;
|
|
101
|
+
}
|
|
102
|
+
else if (character === "'") {
|
|
103
|
+
this.#singleQuoted();
|
|
104
|
+
}
|
|
105
|
+
else if (character === '"') {
|
|
106
|
+
this.#doubleQuoted();
|
|
107
|
+
}
|
|
108
|
+
else if (character === '#' && this.#word === '') {
|
|
109
|
+
this.#skipComment();
|
|
110
|
+
}
|
|
111
|
+
else if (character === '\n') {
|
|
112
|
+
this.#at += 1;
|
|
113
|
+
this.#endCommand();
|
|
114
|
+
this.#skipHeredocBodies();
|
|
115
|
+
}
|
|
116
|
+
else if (BLANK.has(character)) {
|
|
117
|
+
this.#at += 1;
|
|
118
|
+
this.#endWord();
|
|
119
|
+
}
|
|
120
|
+
else if (character === '<' && following === '<') {
|
|
121
|
+
this.#openHeredoc();
|
|
122
|
+
}
|
|
123
|
+
else if (character === '<' || character === '>' || (character === '&' && following === '>')) {
|
|
124
|
+
this.#skipRedirection();
|
|
125
|
+
}
|
|
126
|
+
else if (ENDS_A_COMMAND.has(character)) {
|
|
127
|
+
this.#at += 1;
|
|
128
|
+
this.#endCommand();
|
|
129
|
+
}
|
|
130
|
+
else {
|
|
131
|
+
this.#word += character;
|
|
132
|
+
this.#at += 1;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
this.#endCommand();
|
|
136
|
+
return this.#simples;
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* `$(...)` or a backtick at the current position, read as a command in its own right (R4b). It adds no word to
|
|
140
|
+
* the command around it: its text is not what the shell passes on, and offered as a candidate, `$(cat x/.env)`
|
|
141
|
+
* would become a finding named `cat x/.env`. False when no substitution starts here.
|
|
142
|
+
*/
|
|
143
|
+
#readSubstitution() {
|
|
144
|
+
const text = this.#text;
|
|
145
|
+
if (text.charAt(this.#at) === '`') {
|
|
146
|
+
this.#at += 1;
|
|
147
|
+
this.#simples.push(...new _a(this.#backticked()).read());
|
|
148
|
+
return true;
|
|
149
|
+
}
|
|
150
|
+
if (text.charAt(this.#at) === '$' && text.charAt(this.#at + 1) === '(') {
|
|
151
|
+
this.#at += 2;
|
|
152
|
+
const inner = this.#parenthesised();
|
|
153
|
+
// `$((...))` is arithmetic: nothing in it runs.
|
|
154
|
+
if (!inner.startsWith('('))
|
|
155
|
+
this.#simples.push(...new _a(inner).read());
|
|
156
|
+
return true;
|
|
157
|
+
}
|
|
158
|
+
return false;
|
|
159
|
+
}
|
|
160
|
+
/** The text of a backtick substitution, its closing backtick consumed. Inside, an escaped backtick is one. */
|
|
161
|
+
#backticked() {
|
|
162
|
+
const text = this.#text;
|
|
163
|
+
let inner = '';
|
|
164
|
+
while (this.#at < text.length && text.charAt(this.#at) !== '`') {
|
|
165
|
+
if (text.charAt(this.#at) === '\\' && text.charAt(this.#at + 1) === '`') {
|
|
166
|
+
inner += '`';
|
|
167
|
+
this.#at += 2;
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
inner += text.charAt(this.#at);
|
|
171
|
+
this.#at += 1;
|
|
172
|
+
}
|
|
173
|
+
this.#at += 1;
|
|
174
|
+
return inner;
|
|
175
|
+
}
|
|
176
|
+
/** The text of `$(...)`, its closing parenthesis consumed. A parenthesis inside quotes does not count. */
|
|
177
|
+
#parenthesised() {
|
|
178
|
+
const text = this.#text;
|
|
179
|
+
const start = this.#at;
|
|
180
|
+
let depth = 1;
|
|
181
|
+
let quote = '';
|
|
182
|
+
while (this.#at < text.length) {
|
|
183
|
+
const character = text.charAt(this.#at);
|
|
184
|
+
this.#at += 1;
|
|
185
|
+
if (quote !== '') {
|
|
186
|
+
if (character === '\\' && quote === '"')
|
|
187
|
+
this.#at += 1;
|
|
188
|
+
else if (character === quote)
|
|
189
|
+
quote = '';
|
|
190
|
+
}
|
|
191
|
+
else if (character === '\\') {
|
|
192
|
+
this.#at += 1;
|
|
193
|
+
}
|
|
194
|
+
else if (character === "'" || character === '"') {
|
|
195
|
+
quote = character;
|
|
196
|
+
}
|
|
197
|
+
else if (character === '(') {
|
|
198
|
+
depth += 1;
|
|
199
|
+
}
|
|
200
|
+
else if (character === ')') {
|
|
201
|
+
depth -= 1;
|
|
202
|
+
if (depth === 0)
|
|
203
|
+
return text.slice(start, this.#at - 1);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
return text.slice(start);
|
|
207
|
+
}
|
|
208
|
+
/** Nothing is special inside single quotes, a newline included. Left open, the quote runs to the end. */
|
|
209
|
+
#singleQuoted() {
|
|
210
|
+
const close = this.#text.indexOf("'", this.#at + 1);
|
|
211
|
+
const end = close === -1 ? this.#text.length : close;
|
|
212
|
+
this.#word += this.#text.slice(this.#at + 1, end);
|
|
213
|
+
this.#at = end + 1;
|
|
214
|
+
}
|
|
215
|
+
/** Inside double quotes a backslash escapes a few characters, and `$(...)` and backticks still run. */
|
|
216
|
+
#doubleQuoted() {
|
|
217
|
+
const text = this.#text;
|
|
218
|
+
this.#at += 1;
|
|
219
|
+
while (this.#at < text.length) {
|
|
220
|
+
const character = text.charAt(this.#at);
|
|
221
|
+
const following = text.charAt(this.#at + 1);
|
|
222
|
+
if (character === '"') {
|
|
223
|
+
this.#at += 1;
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
if (this.#readSubstitution())
|
|
227
|
+
continue;
|
|
228
|
+
if (character === '\\' && ESCAPED_IN_DOUBLE_QUOTES.has(following)) {
|
|
229
|
+
if (following !== '\n')
|
|
230
|
+
this.#word += following;
|
|
231
|
+
this.#at += 2;
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
234
|
+
this.#word += character;
|
|
235
|
+
this.#at += 1;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
/** A comment runs to the end of its line. The newline is left to end the command. */
|
|
239
|
+
#skipComment() {
|
|
240
|
+
const end = this.#text.indexOf('\n', this.#at);
|
|
241
|
+
this.#at = end === -1 ? this.#text.length : end;
|
|
242
|
+
}
|
|
243
|
+
/** `<<WORD` and `<<-WORD` open a body after this line; `<<<` hands the next word to the program as input. */
|
|
244
|
+
#openHeredoc() {
|
|
245
|
+
this.#endWord();
|
|
246
|
+
const third = this.#text.charAt(this.#at + 2);
|
|
247
|
+
if (third === '<') {
|
|
248
|
+
this.#at += 3;
|
|
249
|
+
this.#next = 'data';
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
this.#at += third === '-' ? 3 : 2;
|
|
253
|
+
this.#next = { stripTabs: third === '-' };
|
|
254
|
+
}
|
|
255
|
+
/** `<`, `>`, `>>`, `2>&1`, `&>`: a redirection ends a word and ends no command. Its target is still a word. */
|
|
256
|
+
#skipRedirection() {
|
|
257
|
+
this.#endWord();
|
|
258
|
+
this.#at += 1;
|
|
259
|
+
while (REDIRECTION.has(this.#text.charAt(this.#at)))
|
|
260
|
+
this.#at += 1;
|
|
261
|
+
}
|
|
262
|
+
/** Called just past a newline: the bodies of the heredocs opened on the line before, skipped as data. */
|
|
263
|
+
#skipHeredocBodies() {
|
|
264
|
+
const text = this.#text;
|
|
265
|
+
while (this.#pending.length > 0 && this.#at < text.length) {
|
|
266
|
+
const end = text.indexOf('\n', this.#at);
|
|
267
|
+
const raw = text.slice(this.#at, end === -1 ? text.length : end);
|
|
268
|
+
this.#at = end === -1 ? text.length : end + 1;
|
|
269
|
+
const line = raw.endsWith('\r') ? raw.slice(0, -1) : raw;
|
|
270
|
+
const open = this.#pending[0];
|
|
271
|
+
if (open !== undefined && (open.stripTabs ? line.replace(/^\t+/, '') : line) === open.marker) {
|
|
272
|
+
this.#pending.shift();
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
#endWord() {
|
|
277
|
+
const word = this.#word;
|
|
278
|
+
this.#word = '';
|
|
279
|
+
if (word === '')
|
|
280
|
+
return;
|
|
281
|
+
const next = this.#next;
|
|
282
|
+
this.#next = 'argument';
|
|
283
|
+
if (next === 'argument')
|
|
284
|
+
this.#words.push(word);
|
|
285
|
+
else if (next !== 'data')
|
|
286
|
+
this.#pending.push({ marker: word, stripTabs: next.stripTabs });
|
|
287
|
+
}
|
|
288
|
+
#endCommand() {
|
|
289
|
+
this.#endWord();
|
|
290
|
+
// A `<<` with no marker before the command ended opens nothing.
|
|
291
|
+
this.#next = 'argument';
|
|
292
|
+
const words = this.#words;
|
|
293
|
+
this.#words = [];
|
|
294
|
+
if (words.length === 0)
|
|
295
|
+
return;
|
|
296
|
+
const program = words.find((word) => !ASSIGNMENT.test(word));
|
|
297
|
+
this.#simples.push(program === undefined ? { words } : { words, program });
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
_a = CommandReader;
|
|
301
|
+
/**
|
|
302
|
+
* The programs a command line runs, by name. A program name is structure, not content: it is what makes
|
|
303
|
+
* "ran a search" sayable in a report without quoting the command, which §7.3 does not allow.
|
|
304
|
+
*/
|
|
305
|
+
export function programsIn(command) {
|
|
306
|
+
return readCommand(command)
|
|
307
|
+
.flatMap((simple) => (simple.program === undefined ? [] : [basename(simple.program)]))
|
|
308
|
+
// A program is named like one: letters, and no dot. Reading heredocs as data removed most of what used to
|
|
309
|
+
// slip through here; this stays for what that cannot know, and it costs `python3.11`, worth less than noise.
|
|
310
|
+
.filter((program) => /^[A-Za-z][A-Za-z0-9_-]*$/.test(program));
|
|
311
|
+
}
|
|
312
|
+
/**
|
|
313
|
+
* Each simple command a line runs: the program by its base name, and the words after it, quoting removed. For a
|
|
314
|
+
* reader that needs one program's arguments in order - which ones are options, which one is a pattern - rather than
|
|
315
|
+
* every word as a possible path.
|
|
316
|
+
*/
|
|
317
|
+
export function simpleCommandsIn(command) {
|
|
318
|
+
return readCommand(command).flatMap((simple) => simple.program === undefined
|
|
319
|
+
? []
|
|
320
|
+
: [{ program: basename(simple.program), args: simple.words.slice(simple.words.indexOf(simple.program) + 1) }]);
|
|
321
|
+
}
|
|
322
|
+
/** Options git takes before its subcommand that consume the word after them. */
|
|
323
|
+
const GIT_OPTIONS_WITH_A_VALUE = new Set(['-C', '-c', '--git-dir', '--work-tree', '--namespace', '--exec-path']);
|
|
324
|
+
/**
|
|
325
|
+
* Whether the line runs `git commit` (`specs/2026-09-15-where-the-value-went.md` R7): the first word past git's own
|
|
326
|
+
* options is the subcommand. A message that says "commit", or `git log --grep commit`, does not commit anything.
|
|
327
|
+
*/
|
|
328
|
+
export function commitsIn(command) {
|
|
329
|
+
return readCommand(command).some((simple) => {
|
|
330
|
+
if (simple.program === undefined || basename(simple.program) !== 'git')
|
|
331
|
+
return false;
|
|
332
|
+
const words = simple.words.slice(simple.words.indexOf(simple.program) + 1);
|
|
333
|
+
for (let at = 0; at < words.length; at += 1) {
|
|
334
|
+
const word = words[at] ?? '';
|
|
335
|
+
if (GIT_OPTIONS_WITH_A_VALUE.has(word))
|
|
336
|
+
at += 1;
|
|
337
|
+
else if (!word.startsWith('-'))
|
|
338
|
+
return word === 'commit';
|
|
339
|
+
}
|
|
340
|
+
return false;
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
/** True when every command in the line only prints what it was given, so its output enumerates nothing. */
|
|
344
|
+
export function printsContentOnly(commands) {
|
|
345
|
+
const programs = commands.flatMap((command) => readCommand(command).flatMap((simple) => (simple.program === undefined ? [] : [simple.program])));
|
|
346
|
+
return programs.length > 0 && programs.every((program) => PRINTS_CONTENT.has(basename(program)));
|
|
347
|
+
}
|
|
348
|
+
function basename(program) {
|
|
349
|
+
const parts = program.split('/');
|
|
350
|
+
return parts[parts.length - 1] ?? program;
|
|
351
|
+
}
|
|
352
|
+
/**
|
|
353
|
+
* Programs whose arguments are text and never a file they open. `echo "check apps/web/.env"` addresses
|
|
354
|
+
* nothing, and reading its argument as a path is how a sentence about a file became a file that was read.
|
|
355
|
+
* Unknown programs keep the conservative reading, so absence from this list costs noise, never silence.
|
|
356
|
+
*/
|
|
357
|
+
const ADDRESSES_NOTHING = new Set(['echo', 'printf', 'true', 'false', ':', 'sleep']);
|
|
358
|
+
/** Whether a program, by its base name, takes only text and never opens a file its arguments name. */
|
|
359
|
+
export function addressesNothing(program) {
|
|
360
|
+
return ADDRESSES_NOTHING.has(basename(program));
|
|
361
|
+
}
|
|
362
|
+
/**
|
|
363
|
+
* Interpreters, and the words that hand them code rather than a file to open. `deno` takes a subcommand where the
|
|
364
|
+
* others take a flag, and the search is the same either way.
|
|
365
|
+
*/
|
|
366
|
+
const HANDS_OVER_CODE = new Map([
|
|
367
|
+
['python', new Set(['-c'])],
|
|
368
|
+
['python3', new Set(['-c'])],
|
|
369
|
+
['node', new Set(['-e', '-p', '--eval', '--print'])],
|
|
370
|
+
['bun', new Set(['-e', '-p', '--eval', '--print'])],
|
|
371
|
+
['deno', new Set(['eval'])],
|
|
372
|
+
['ruby', new Set(['-e'])],
|
|
373
|
+
['perl', new Set(['-e', '-E'])],
|
|
374
|
+
['php', new Set(['-r'])],
|
|
375
|
+
]);
|
|
376
|
+
/**
|
|
377
|
+
* The word a simple command hands its program as code, if it does. The search passes over the interpreter's own
|
|
378
|
+
* options and stops at the first word that is neither an option nor the code flag: that word is a script to run,
|
|
379
|
+
* and everything after it belongs to the script, not to the interpreter - `node script.js -e x` opens `x` as the
|
|
380
|
+
* script's own argument.
|
|
381
|
+
*/
|
|
382
|
+
function inlineCodeOf(simple) {
|
|
383
|
+
const program = simple.program;
|
|
384
|
+
if (program === undefined)
|
|
385
|
+
return [];
|
|
386
|
+
const flags = HANDS_OVER_CODE.get(basename(program));
|
|
387
|
+
if (flags === undefined)
|
|
388
|
+
return [];
|
|
389
|
+
const words = simple.words.slice(simple.words.indexOf(program) + 1);
|
|
390
|
+
for (let at = 0; at < words.length; at += 1) {
|
|
391
|
+
const word = words[at] ?? '';
|
|
392
|
+
// The whole word is returned for `--eval=…`, because that is what the caller matches its words against.
|
|
393
|
+
const separator = word.indexOf('=');
|
|
394
|
+
if (flags.has(word))
|
|
395
|
+
return words[at + 1] === undefined ? [] : [words[at + 1] ?? ''];
|
|
396
|
+
if (separator > 0 && flags.has(word.slice(0, separator)))
|
|
397
|
+
return [word];
|
|
398
|
+
if (!word.startsWith('-'))
|
|
399
|
+
return [];
|
|
400
|
+
}
|
|
401
|
+
return [];
|
|
402
|
+
}
|
|
403
|
+
/**
|
|
404
|
+
* Whether every program the line runs is an interpreter handed code, or prints only what it was given, and the code
|
|
405
|
+
* names `path` - `python3 -c "print(open('.env').read())"`, alone or piped to `head`. Such output is read for values
|
|
406
|
+
* as a `cat`'s is (`docs/detection.md`, `python-open`). An interpreter can print anything, so this holds only where its
|
|
407
|
+
* own code names the file: a script that is a file of its own, or a path built while it runs, stays out, and an
|
|
408
|
+
* ordinary line it printed is dropped by the same shape rules as any other value.
|
|
409
|
+
*/
|
|
410
|
+
export function codeReadsNamedFile(commands, path) {
|
|
411
|
+
const simples = commands.flatMap((command) => readCommand(command)).filter((simple) => simple.program !== undefined);
|
|
412
|
+
const code = simples.flatMap(inlineCodeOf);
|
|
413
|
+
return (code.some((words) => words.includes(path)) &&
|
|
414
|
+
simples.every((simple) => inlineCodeOf(simple).length > 0 || PRINTS_CONTENT.has(basename(simple.program ?? ''))));
|
|
415
|
+
}
|
|
416
|
+
/** The code a command line hands to an interpreter, as words. For the measurement, which counts where a path sat. */
|
|
417
|
+
export function inlineCode(command) {
|
|
418
|
+
return readCommand(command).flatMap(inlineCodeOf);
|
|
419
|
+
}
|
|
420
|
+
/**
|
|
421
|
+
* The paths a command line could be addressing: the words of every simple command it runs, unless the program
|
|
422
|
+
* addresses nothing. Read through `readCommand`, so a heredoc body contributes no word at all (R4). From a word
|
|
423
|
+
* that is code an interpreter was handed, only a candidate shaped like a whole path is kept (R1).
|
|
424
|
+
*/
|
|
425
|
+
export function commandPathCandidates(command) {
|
|
426
|
+
return readCommand(command)
|
|
427
|
+
.flatMap((simple) => {
|
|
428
|
+
if (simple.program !== undefined && ADDRESSES_NOTHING.has(basename(simple.program)))
|
|
429
|
+
return [];
|
|
430
|
+
const code = new Set(inlineCodeOf(simple));
|
|
431
|
+
return simple.words.flatMap((word) => code.has(word) ? candidatesOf(word).filter((candidate) => shapedLikePath(candidate)) : candidatesOf(word));
|
|
432
|
+
})
|
|
433
|
+
.filter((candidate) => candidate !== '');
|
|
434
|
+
}
|
|
435
|
+
/**
|
|
436
|
+
* Brackets and quotes hold a path inside an expression. `python3 -c "print(open('apps/web/.env').read())"` reads
|
|
437
|
+
* a file, and the token that survives quoting is the whole expression - which was being reported, verbatim, as
|
|
438
|
+
* the name of a file. Splitting on them keeps the finding and loses the noise around it.
|
|
439
|
+
*
|
|
440
|
+
* Only brackets and quotes: splitting on commas and semicolons as well would cut a quoted sentence into
|
|
441
|
+
* fragments and hand each one to the policy, which is the noise this module exists to avoid.
|
|
442
|
+
*/
|
|
443
|
+
const HOLDS_A_PATH = /[()'"`]+/;
|
|
444
|
+
/**
|
|
445
|
+
* A flag is not a path, but `--file=.env` carries one. An assignment before the program - `FOO=bar cmd` - is
|
|
446
|
+
* not a path either, and its value is treated the same way: what follows the first `=` is the candidate.
|
|
447
|
+
*/
|
|
448
|
+
function candidatesOf(token) {
|
|
449
|
+
const separator = token.indexOf('=');
|
|
450
|
+
if (separator === -1 && token.startsWith('-'))
|
|
451
|
+
return [];
|
|
452
|
+
const value = separator === -1 ? token : token.slice(separator + 1);
|
|
453
|
+
return value.split(HOLDS_A_PATH).filter((candidate) => candidate !== '');
|
|
454
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The paths a listing could be naming.
|
|
3
|
+
*
|
|
4
|
+
* A listing has structure too. `grep` writes `path:line:matched text`, `find` and `ls -1` write one path per
|
|
5
|
+
* line, `ls -l` writes the name last after a permission string. So a path counts by **where it sits**, not by
|
|
6
|
+
* being somewhere in the line.
|
|
7
|
+
*
|
|
8
|
+
* The difference is not cosmetic: a matched line is arbitrary file content, and taking every word of it made a
|
|
9
|
+
* document that mentions a file read as a file that was reached. **Measured across seven sessions
|
|
10
|
+
* (2026-09-15):** of 125 findings that came from a result, 71 had the path inside a line of prose - a table, a
|
|
11
|
+
* numbered list, an arrow in someone's notes - against 52 where the line was the path and 2 search hits. Taking
|
|
12
|
+
* the last field of any line, and the whole of any line, is where those 71 came from, and both are gone.
|
|
13
|
+
*
|
|
14
|
+
* **And a line taken whole is a path only when the whole of it is one**
|
|
15
|
+
* (`specs/2026-09-15-paths-not-fragments.md` R2, added 2026-09-15). Position says where a path would sit; it does not
|
|
16
|
+
* say that a program wrote one there. `…/id_rsa -> outside the project` and `…/id_rsa in a result · succeeded · 2×`
|
|
17
|
+
* sit where a path sits and are sentences about a file; `[apps/web/.env,` is one line of a printed array. Measured
|
|
18
|
+
* on nine sessions: six such findings, and no line with words before its path, which is why a field keeps its words.
|
|
19
|
+
*/
|
|
20
|
+
import { holdsCodePunctuation, shapedLikePath } from './path-shape.js';
|
|
21
|
+
/** Where a path written out in full starts: the root, the home directory, or the directory the command ran in. */
|
|
22
|
+
const PATH_START = /^(?:\/|~\/|\.\.?\/)/;
|
|
23
|
+
/** How `ls -l` opens a line, and nothing else does: a file type and nine permission characters. */
|
|
24
|
+
const LONG_LISTING = /^[-dlbcps][rwxSsTt-]{9}[ @+]/;
|
|
25
|
+
/** A field of a line is where a path sits; punctuation of code in it says a program printed an expression (R2). */
|
|
26
|
+
const asField = (text) => (text !== '' && !holdsCodePunctuation(text) ? text : '');
|
|
27
|
+
/** A line taken whole, and a line of one word, name a file only when the whole of the line is a path (R2). */
|
|
28
|
+
const asWholeLine = (text) => (shapedLikePath(text) ? text : '');
|
|
29
|
+
export function listingPathCandidates(text) {
|
|
30
|
+
return text
|
|
31
|
+
.split('\n')
|
|
32
|
+
.map((line) => {
|
|
33
|
+
const trimmed = line.trim();
|
|
34
|
+
if (trimmed === '')
|
|
35
|
+
return [];
|
|
36
|
+
// `grep -n` without a file name writes `49:matched text`, and that leading number is not part of a path.
|
|
37
|
+
// Dropping it is what stops `49:packages/app/.env` from being reported as a file.
|
|
38
|
+
const withoutLineNumber = trimmed.replace(/^\d+:/, '');
|
|
39
|
+
const colon = withoutLineNumber.indexOf(':');
|
|
40
|
+
const fields = withoutLineNumber.split(/\s+/);
|
|
41
|
+
// In order of preference. A whole grep line matches a pattern as readily as the path that starts it, so
|
|
42
|
+
// the narrower candidate is offered first and only the first match of a line is taken: one line of a
|
|
43
|
+
// listing names one file.
|
|
44
|
+
const candidates = [
|
|
45
|
+
{ text: asField(colon === -1 ? '' : withoutLineNumber.slice(0, colon)), positional: true },
|
|
46
|
+
// `find` prints whole paths, one per line. A line with no search hit's colon that starts where a path starts
|
|
47
|
+
// is that path because of where it sits, space in a directory name and all. Offered before the final field,
|
|
48
|
+
// which cut `…/Web Portal/app/.env` into `Portal/app/.env`: a second finding, for a file that exists nowhere.
|
|
49
|
+
{ text: colon === -1 && PATH_START.test(withoutLineNumber) ? asWholeLine(withoutLineNumber) : '', positional: true },
|
|
50
|
+
// A line that is one word is that word: `ls -1` and `git status --porcelain` write listings like that.
|
|
51
|
+
{ text: fields.length === 1 ? asWholeLine(withoutLineNumber) : '', positional: true },
|
|
52
|
+
// `ls -l` writes the name last, after a permission string that nothing else begins with.
|
|
53
|
+
{ text: LONG_LISTING.test(withoutLineNumber) ? asField(fields[fields.length - 1] ?? '') : '', positional: false },
|
|
54
|
+
];
|
|
55
|
+
return candidates.filter((candidate) => candidate.text !== '');
|
|
56
|
+
})
|
|
57
|
+
.filter((candidates) => candidates.length > 0);
|
|
58
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/** Punctuation that belongs to an expression and to no file name on the systems this reads. */
|
|
2
|
+
const CODE_PUNCTUATION = /[{}<>,;]|->|=>/;
|
|
3
|
+
/** A shell operator standing between words: what a command line holds and a path does not. */
|
|
4
|
+
const OPERATOR = /(?:^|\s)(?:&&|\|\||\|)(?:\s|$)/;
|
|
5
|
+
/** A word of a path holds a separator, or starts where a path starts. */
|
|
6
|
+
function partOfAPath(word) {
|
|
7
|
+
return word.includes('/') || word.startsWith('.') || word.startsWith('~');
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* A square bracket belongs to a path only inside one segment of it. A framework names directories
|
|
11
|
+
* `app/[locale]/page.tsx` and files `[...slug].env`; a program printing an array writes `[apps/web/.env,`. So a
|
|
12
|
+
* bracket that does not open and close within its own segment is code, and one that does is a name someone chose.
|
|
13
|
+
*/
|
|
14
|
+
function bracketsPairWithinSegments(text) {
|
|
15
|
+
for (const segment of text.split('/')) {
|
|
16
|
+
let depth = 0;
|
|
17
|
+
for (const character of segment) {
|
|
18
|
+
if (character === '[')
|
|
19
|
+
depth += 1;
|
|
20
|
+
else if (character === ']')
|
|
21
|
+
depth -= 1;
|
|
22
|
+
if (depth < 0)
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
if (depth !== 0)
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
30
|
+
/** Whether the string carries punctuation of code. A line of output that does names no file (R2). */
|
|
31
|
+
export function holdsCodePunctuation(text) {
|
|
32
|
+
return CODE_PUNCTUATION.test(text) || !bracketsPairWithinSegments(text);
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* The shape, by the first rule that matches: an operator makes it command text, punctuation makes it code, and
|
|
36
|
+
* what is left is read by its words. One word is a path; more are a path only when the first and the last are both
|
|
37
|
+
* part of one - `/Users/someone/Client Name/app/.env` is a path, `check apps/web/.env` is a sentence.
|
|
38
|
+
*/
|
|
39
|
+
export function pathShape(text) {
|
|
40
|
+
if (OPERATOR.test(text))
|
|
41
|
+
return 'command-text';
|
|
42
|
+
if (holdsCodePunctuation(text))
|
|
43
|
+
return 'code-punctuation';
|
|
44
|
+
const words = text.trim().split(/\s+/);
|
|
45
|
+
if (words.length === 1)
|
|
46
|
+
return 'path';
|
|
47
|
+
if (!partOfAPath(words[0] ?? ''))
|
|
48
|
+
return 'words-before';
|
|
49
|
+
if (!partOfAPath(words[words.length - 1] ?? ''))
|
|
50
|
+
return 'words-after';
|
|
51
|
+
return 'path-with-space';
|
|
52
|
+
}
|
|
53
|
+
/** Whether the whole string is a path, space in a directory name and all. */
|
|
54
|
+
export function shapedLikePath(text) {
|
|
55
|
+
if (text.trim() === '')
|
|
56
|
+
return false;
|
|
57
|
+
const shape = pathShape(text);
|
|
58
|
+
return shape === 'path' || shape === 'path-with-space';
|
|
59
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pulls path-like tokens out of a piece of text, so they can be held against the policy.
|
|
3
|
+
*
|
|
4
|
+
* The separators are the reason this file exists. A path rarely sits alone: it arrives inside a command
|
|
5
|
+
* (`grep -rn SECRET apps/web`), inside a quoted argument, or inside a line of output that reads
|
|
6
|
+
* `apps/web/.env.development:12:SECRET=…`. Splitting on whitespace alone would leave that whole line as one
|
|
7
|
+
* token and no pattern would ever match it - which is exactly the shape the motivating case took.
|
|
8
|
+
*
|
|
9
|
+
* **Known limits, deliberately not guessed around:** a path assembled at runtime (`$HOME/.ssh/id_rsa`,
|
|
10
|
+
* `"$dir"/.env`) is not recognised as the file it names, and a token that merely looks like a path is offered
|
|
11
|
+
* to the policy like any other. The error runs toward reporting rather than passing over, and every fact says
|
|
12
|
+
* where it was found so a reader can judge it.
|
|
13
|
+
*/
|
|
14
|
+
const SEPARATORS = /[\s"'`,;:=()<>|{}[\]]+/;
|
|
15
|
+
export function pathTokens(text) {
|
|
16
|
+
return text
|
|
17
|
+
.split(SEPARATORS)
|
|
18
|
+
.filter((token) => token !== '')
|
|
19
|
+
.map(stripTrailingPunctuation)
|
|
20
|
+
.filter((token) => token !== '');
|
|
21
|
+
}
|
|
22
|
+
// Prose puts a full stop after a path. A trailing slash is kept: a directory pattern such as the one for
|
|
23
|
+
// `secrets` matches the form with the separator and not the bare name, so stripping it lost every call that
|
|
24
|
+
// addressed a protected directory as a whole.
|
|
25
|
+
function stripTrailingPunctuation(token) {
|
|
26
|
+
return token.replace(/[.]+$/, '');
|
|
27
|
+
}
|
|
28
|
+
/** Every string inside a value, however deeply it sits. Tool inputs are shaped by the tool, not by us. */
|
|
29
|
+
export function stringsIn(value) {
|
|
30
|
+
if (typeof value === 'string')
|
|
31
|
+
return [value];
|
|
32
|
+
if (Array.isArray(value))
|
|
33
|
+
return value.flatMap(stringsIn);
|
|
34
|
+
if (value !== null && typeof value === 'object')
|
|
35
|
+
return Object.values(value).flatMap(stringsIn);
|
|
36
|
+
return [];
|
|
37
|
+
}
|