@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,99 @@
|
|
|
1
|
+
import { constants } from 'node:fs';
|
|
2
|
+
import { access, readFile, realpath, stat } from 'node:fs/promises';
|
|
3
|
+
import { delimiter, dirname, join, sep } from 'node:path';
|
|
4
|
+
import { parseJsonObject } from '../shared/json.js';
|
|
5
|
+
import { PACKAGE } from '../shared/package-name.js';
|
|
6
|
+
import { plainVersion } from '../shared/plain-version.js';
|
|
7
|
+
/** The installed way, and the name of the bin a hook's shell looks up on `PATH`. */
|
|
8
|
+
const INSTALLED = 'agentwhy';
|
|
9
|
+
/**
|
|
10
|
+
* `agentwhy` where the first `agentwhy` on `PATH` belongs to the package this process runs from, and the published way
|
|
11
|
+
* everywhere else (`a-hook-runs-what-you-ran.md` J2, J3): `npx @agentwhy/cli@<version>`, pinned to this package's own
|
|
12
|
+
* version where it is a release, so no hook runs a version nobody chose (`nothing-updates-by-itself.md` U1).
|
|
13
|
+
*
|
|
14
|
+
* `PATH` is read without the entries `npx` adds for the length of its run - its cache, a `node_modules/.bin` for every
|
|
15
|
+
* directory up to `/`, npm's `node-gyp-bin` - each of which has a `node_modules` segment (JB2). Read with them, the
|
|
16
|
+
* answer inside `npx` is always "installed", and a hook written from it fails as soon as `npx` has exited.
|
|
17
|
+
*/
|
|
18
|
+
export class NodeAgentwhyInvocation {
|
|
19
|
+
#options;
|
|
20
|
+
constructor(options) {
|
|
21
|
+
this.#options = options;
|
|
22
|
+
}
|
|
23
|
+
async find() {
|
|
24
|
+
const { script, path, platform } = this.#options;
|
|
25
|
+
const running = script === undefined ? undefined : await packageOf(script);
|
|
26
|
+
const npx = published(running === undefined ? undefined : await versionIn(running));
|
|
27
|
+
// J3: a `.cmd` shim is what a global bin is on Windows, and nothing here has measured one.
|
|
28
|
+
if (platform === 'win32' || running === undefined || path === undefined)
|
|
29
|
+
return npx;
|
|
30
|
+
for (const directory of path.split(delimiter)) {
|
|
31
|
+
if (directory === '' || directory.split(sep).includes('node_modules'))
|
|
32
|
+
continue;
|
|
33
|
+
const bin = await executable(join(directory, INSTALLED));
|
|
34
|
+
// The first one found is the one a hook's shell runs, whichever package it is.
|
|
35
|
+
if (bin !== undefined)
|
|
36
|
+
return (await packageOf(bin)) === running ? INSTALLED : npx;
|
|
37
|
+
}
|
|
38
|
+
return npx;
|
|
39
|
+
}
|
|
40
|
+
async version() {
|
|
41
|
+
const { script } = this.#options;
|
|
42
|
+
const running = script === undefined ? undefined : await packageOf(script);
|
|
43
|
+
return running === undefined ? undefined : versionIn(running);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* The published way, written without `--yes`: a hook's first run installs without asking (JB5). Pinned to a release;
|
|
48
|
+
* a version that is not one (`0.0.0-dev`) is on no registry, and is left out rather than written (U1).
|
|
49
|
+
*/
|
|
50
|
+
function published(version) {
|
|
51
|
+
return version !== undefined && plainVersion(version) !== undefined ? `npx ${PACKAGE}@${version}` : `npx ${PACKAGE}`;
|
|
52
|
+
}
|
|
53
|
+
/** What a package's `package.json` names as its version, or `undefined` where it cannot be read. */
|
|
54
|
+
async function versionIn(packageRoot) {
|
|
55
|
+
try {
|
|
56
|
+
const version = parseJsonObject(await readFile(join(packageRoot, 'package.json'), 'utf8'))?.version;
|
|
57
|
+
return typeof version === 'string' ? version : undefined;
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
return undefined;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
/** The file a path leads to, where it exists and may be run; `undefined` for a broken link or anything else. */
|
|
64
|
+
async function executable(path) {
|
|
65
|
+
try {
|
|
66
|
+
const file = await realpath(path);
|
|
67
|
+
// A directory passes X_OK, and a shell looking a command up skips it. Found by a review: a checkout named
|
|
68
|
+
// agentwhy earlier on PATH hid the bin after it.
|
|
69
|
+
if (!(await stat(file)).isFile())
|
|
70
|
+
return undefined;
|
|
71
|
+
await access(file, constants.X_OK);
|
|
72
|
+
return file;
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
return undefined;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
/** The nearest directory above a file's real path that holds a `package.json`: the package it belongs to. */
|
|
79
|
+
async function packageOf(file) {
|
|
80
|
+
let directory;
|
|
81
|
+
try {
|
|
82
|
+
directory = dirname(await realpath(file));
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
return undefined;
|
|
86
|
+
}
|
|
87
|
+
for (;;) {
|
|
88
|
+
try {
|
|
89
|
+
await access(join(directory, 'package.json'));
|
|
90
|
+
return directory;
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
const parent = dirname(directory);
|
|
94
|
+
if (parent === directory)
|
|
95
|
+
return undefined;
|
|
96
|
+
directory = parent;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
/**
|
|
3
|
+
* What each platform calls to open a file with whatever is registered for it. The empty argument after `start`
|
|
4
|
+
* is not a mistake: `cmd` reads the first quoted argument as the window title, so a path that needs quoting
|
|
5
|
+
* would otherwise become one.
|
|
6
|
+
*/
|
|
7
|
+
function opener(platform) {
|
|
8
|
+
if (platform === 'darwin')
|
|
9
|
+
return { program: 'open', leading: [] };
|
|
10
|
+
if (platform === 'win32')
|
|
11
|
+
return { program: 'cmd', leading: ['/c', 'start', ''] };
|
|
12
|
+
return { program: 'xdg-open', leading: [] };
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Opens a file through the platform's own opener, detached and with its streams closed, so that whatever starts
|
|
16
|
+
* does not hold this process open and cannot write over the report the terminal is printing.
|
|
17
|
+
*/
|
|
18
|
+
export class NodeBrowser {
|
|
19
|
+
#platform;
|
|
20
|
+
constructor(platform) {
|
|
21
|
+
this.#platform = platform;
|
|
22
|
+
}
|
|
23
|
+
async open(path) {
|
|
24
|
+
const { program, leading } = opener(this.#platform);
|
|
25
|
+
return new Promise((resolve) => {
|
|
26
|
+
try {
|
|
27
|
+
const child = spawn(program, [...leading, path], { stdio: 'ignore', detached: true });
|
|
28
|
+
child.once('error', () => resolve(false));
|
|
29
|
+
child.once('spawn', () => {
|
|
30
|
+
child.unref();
|
|
31
|
+
resolve(true);
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
// A platform with no opener at all, or one that refuses to spawn: the file is still written.
|
|
36
|
+
resolve(false);
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { createReadStream } from 'node:fs';
|
|
2
|
+
import { mkdir, open, readdir, readFile, stat, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { FileAccessError } from '../ports/file-access-error.js';
|
|
4
|
+
/**
|
|
5
|
+
* The owner alone, for everything this writes. What it writes is a report: which protected files an agent
|
|
6
|
+
* reached, the titles a person gave their sessions, the paths of the project they were in. `report --open` and
|
|
7
|
+
* `start` both land that in the system's temporary directory, which on Linux is one `/tmp` shared by every
|
|
8
|
+
* account on the machine - and the default of `writeFile` there is a file the whole machine can read.
|
|
9
|
+
*
|
|
10
|
+
* `FileMarkStore` has held its record at `0o600` under a `0o700` directory since it was written; this is the
|
|
11
|
+
* same standard, applied to the larger of the two things this tool puts on disk.
|
|
12
|
+
*/
|
|
13
|
+
const OWNER_ONLY_FILE = 0o600;
|
|
14
|
+
const OWNER_ONLY_DIRECTORY = 0o700;
|
|
15
|
+
export class NodeFileSystem {
|
|
16
|
+
async writeText(path, text) {
|
|
17
|
+
await attempt(path, () => writeFile(path, text, { mode: OWNER_ONLY_FILE }));
|
|
18
|
+
}
|
|
19
|
+
async ensureDirectory(path) {
|
|
20
|
+
await attempt(path, () => mkdir(path, { recursive: true, mode: OWNER_ONLY_DIRECTORY }));
|
|
21
|
+
}
|
|
22
|
+
async kindOf(path) {
|
|
23
|
+
return entryKind(await attempt(path, () => stat(path)));
|
|
24
|
+
}
|
|
25
|
+
async modifiedAt(path) {
|
|
26
|
+
return (await attempt(path, () => stat(path))).mtimeMs;
|
|
27
|
+
}
|
|
28
|
+
async list(path) {
|
|
29
|
+
const entries = await attempt(path, () => readdir(path, { withFileTypes: true }));
|
|
30
|
+
return entries.map((entry) => ({ name: entry.name, kind: entryKind(entry) }));
|
|
31
|
+
}
|
|
32
|
+
readText(path) {
|
|
33
|
+
return attempt(path, () => readFile(path, 'utf8'));
|
|
34
|
+
}
|
|
35
|
+
/** Read by position, so a transcript of any size costs at most `bytes`. A read may return less than asked, hence the loop. */
|
|
36
|
+
readTail(path, bytes) {
|
|
37
|
+
return attempt(path, async () => {
|
|
38
|
+
const handle = await open(path, 'r');
|
|
39
|
+
try {
|
|
40
|
+
const { size } = await handle.stat();
|
|
41
|
+
const length = Math.min(bytes, size);
|
|
42
|
+
const buffer = Buffer.alloc(length);
|
|
43
|
+
let filled = 0;
|
|
44
|
+
while (filled < length) {
|
|
45
|
+
const { bytesRead } = await handle.read(buffer, filled, length - filled, size - length + filled);
|
|
46
|
+
if (bytesRead === 0)
|
|
47
|
+
break;
|
|
48
|
+
filled += bytesRead;
|
|
49
|
+
}
|
|
50
|
+
return buffer.subarray(0, filled).toString('utf8');
|
|
51
|
+
}
|
|
52
|
+
finally {
|
|
53
|
+
await handle.close();
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* JSONL records are delimited by \n alone. `readline` also ends a line on a lone \r, which JSON counts as
|
|
59
|
+
* ordinary whitespace between tokens - so a record containing one would be split into two halves, each
|
|
60
|
+
* unparsable, and the record itself lost. Measured on one session, where it cost one `user` line.
|
|
61
|
+
*/
|
|
62
|
+
async *readLines(path) {
|
|
63
|
+
const stream = createReadStream(path, { encoding: 'utf8' });
|
|
64
|
+
let pending = '';
|
|
65
|
+
try {
|
|
66
|
+
for await (const chunk of stream) {
|
|
67
|
+
const parts = (pending + chunk).split('\n');
|
|
68
|
+
pending = parts.pop() ?? '';
|
|
69
|
+
for (const line of parts)
|
|
70
|
+
yield withoutTrailingReturn(line);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
catch (error) {
|
|
74
|
+
throw accessError(path, error);
|
|
75
|
+
}
|
|
76
|
+
if (pending !== '')
|
|
77
|
+
yield withoutTrailingReturn(pending);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
/** Only a \r that terminates the line is a terminator: that is a CRLF file, not a record with a \r inside it. */
|
|
81
|
+
function withoutTrailingReturn(line) {
|
|
82
|
+
return line.endsWith('\r') ? line.slice(0, -1) : line;
|
|
83
|
+
}
|
|
84
|
+
function entryKind(entry) {
|
|
85
|
+
if (entry.isFile())
|
|
86
|
+
return 'file';
|
|
87
|
+
if (entry.isDirectory())
|
|
88
|
+
return 'directory';
|
|
89
|
+
return 'other';
|
|
90
|
+
}
|
|
91
|
+
async function attempt(path, operation) {
|
|
92
|
+
try {
|
|
93
|
+
return await operation();
|
|
94
|
+
}
|
|
95
|
+
catch (error) {
|
|
96
|
+
throw accessError(path, error);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
// Only the failures the application turns into a status are translated; anything else propagates unchanged.
|
|
100
|
+
function accessError(path, error) {
|
|
101
|
+
const code = error instanceof Error && 'code' in error ? error.code : undefined;
|
|
102
|
+
// A name too long for the system, or a loop of links, names nothing that can be opened: found by measuring `refuse`
|
|
103
|
+
// on real transcripts, where a script passed on a command line was looked up as a directory.
|
|
104
|
+
if (code === 'ENOENT' || code === 'ENOTDIR' || code === 'ENAMETOOLONG' || code === 'ELOOP') {
|
|
105
|
+
return new FileAccessError('not-found', path, { cause: error });
|
|
106
|
+
}
|
|
107
|
+
// A folder read as a file cannot be read as one: `report --input` given a folder of Codex rollouts, which `doctor` takes.
|
|
108
|
+
if (code === 'EACCES' || code === 'EPERM' || code === 'EISDIR')
|
|
109
|
+
return new FileAccessError('unreadable', path, { cause: error });
|
|
110
|
+
return error;
|
|
111
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { createServer } from 'node:http';
|
|
2
|
+
const LOOPBACK = '127.0.0.1';
|
|
3
|
+
/**
|
|
4
|
+
* `node:http` on 127.0.0.1 and a port the system picks. It stops after a stretch with no request, so a page left open in
|
|
5
|
+
* a tab does not keep a process running for ever (R54). Headers every answer carries are set here, once.
|
|
6
|
+
*
|
|
7
|
+
* One listener for the process (`.ai/specs/2026-09-27-which-project.md` V14, V18, amended 2026-09-28): a run a page
|
|
8
|
+
* switched to serves at the same address as the run it replaced, so the pages of the project shown before - in a tab's
|
|
9
|
+
* history, or a tab left open - still reach this process, and are told what happened instead of reaching nothing. Every
|
|
10
|
+
* `serve` is a run; requests go to the newest. Closing an older run settles it and nothing else; closing the newest - or
|
|
11
|
+
* a stretch with no request - stops the listener.
|
|
12
|
+
*/
|
|
13
|
+
export class NodeLocalServer {
|
|
14
|
+
#listening;
|
|
15
|
+
#current;
|
|
16
|
+
#timer;
|
|
17
|
+
#idleMs = 0;
|
|
18
|
+
async serve(handle, options) {
|
|
19
|
+
let settle = () => undefined;
|
|
20
|
+
const closed = new Promise((resolve) => {
|
|
21
|
+
settle = resolve;
|
|
22
|
+
});
|
|
23
|
+
const run = { handle, settle, maxBody: options.maxBody };
|
|
24
|
+
this.#idleMs = options.idleMs;
|
|
25
|
+
const listening = this.#listening ?? await this.#listen();
|
|
26
|
+
this.#current = run;
|
|
27
|
+
this.#restart();
|
|
28
|
+
let done = false;
|
|
29
|
+
const close = () => {
|
|
30
|
+
if (done)
|
|
31
|
+
return;
|
|
32
|
+
done = true;
|
|
33
|
+
if (this.#current === run) {
|
|
34
|
+
this.#stop();
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
settle();
|
|
38
|
+
};
|
|
39
|
+
return { origin: listening.origin, closed, close };
|
|
40
|
+
}
|
|
41
|
+
async #listen() {
|
|
42
|
+
const server = createServer((incoming, outgoing) => {
|
|
43
|
+
this.#restart();
|
|
44
|
+
const run = this.#current;
|
|
45
|
+
if (run === undefined) {
|
|
46
|
+
outgoing.writeHead(503, { 'Content-Type': 'text/plain; charset=utf-8', 'Cache-Control': 'no-store' });
|
|
47
|
+
outgoing.end('No project is being shown.');
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
void answer(incoming, outgoing, run.handle, run.maxBody);
|
|
51
|
+
});
|
|
52
|
+
await new Promise((resolve, reject) => {
|
|
53
|
+
server.once('error', reject);
|
|
54
|
+
server.listen(0, LOOPBACK, () => resolve());
|
|
55
|
+
});
|
|
56
|
+
const { port } = server.address();
|
|
57
|
+
this.#listening = { server, origin: `http://${LOOPBACK}:${port}` };
|
|
58
|
+
return this.#listening;
|
|
59
|
+
}
|
|
60
|
+
/** The newest run ends, and with it the listener: nothing is left to answer. */
|
|
61
|
+
#stop() {
|
|
62
|
+
if (this.#timer !== undefined)
|
|
63
|
+
clearTimeout(this.#timer);
|
|
64
|
+
const run = this.#current;
|
|
65
|
+
const listening = this.#listening;
|
|
66
|
+
this.#current = undefined;
|
|
67
|
+
this.#listening = undefined;
|
|
68
|
+
if (listening === undefined) {
|
|
69
|
+
run?.settle();
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
listening.server.close(() => run?.settle());
|
|
73
|
+
listening.server.closeAllConnections();
|
|
74
|
+
}
|
|
75
|
+
#restart() {
|
|
76
|
+
if (this.#timer !== undefined)
|
|
77
|
+
clearTimeout(this.#timer);
|
|
78
|
+
this.#timer = setTimeout(() => this.#stop(), this.#idleMs);
|
|
79
|
+
this.#timer.unref();
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
async function answer(incoming, outgoing, handle, maxBody) {
|
|
83
|
+
const send = (response) => {
|
|
84
|
+
outgoing.writeHead(response.status, {
|
|
85
|
+
'Content-Type': response.type,
|
|
86
|
+
'Cache-Control': 'no-store',
|
|
87
|
+
'Referrer-Policy': 'no-referrer',
|
|
88
|
+
'X-Content-Type-Options': 'nosniff',
|
|
89
|
+
...(response.location === undefined ? {} : { Location: response.location }),
|
|
90
|
+
});
|
|
91
|
+
outgoing.end(response.body, () => response.after?.());
|
|
92
|
+
};
|
|
93
|
+
try {
|
|
94
|
+
const chunks = [];
|
|
95
|
+
let size = 0;
|
|
96
|
+
for await (const chunk of incoming) {
|
|
97
|
+
size += chunk.length;
|
|
98
|
+
if (size > maxBody) {
|
|
99
|
+
send({ status: 413, type: 'text/plain; charset=utf-8', body: 'Too large.' });
|
|
100
|
+
incoming.destroy();
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
chunks.push(chunk);
|
|
104
|
+
}
|
|
105
|
+
const url = incoming.url ?? '/';
|
|
106
|
+
const headers = {};
|
|
107
|
+
for (const [name, value] of Object.entries(incoming.headers))
|
|
108
|
+
headers[name] = Array.isArray(value) ? value.join(', ') : value;
|
|
109
|
+
send(await handle({ method: incoming.method ?? 'GET', path: url.split('?')[0] ?? '/', headers, body: Buffer.concat(chunks).toString('utf8') }));
|
|
110
|
+
}
|
|
111
|
+
catch {
|
|
112
|
+
if (!outgoing.headersSent)
|
|
113
|
+
send({ status: 500, type: 'text/plain; charset=utf-8', body: 'Something went wrong.' });
|
|
114
|
+
}
|
|
115
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/** Reads a stream to its end, and gives up past a size rather than holding whatever arrives. */
|
|
2
|
+
export class NodeTextInput {
|
|
3
|
+
#stream;
|
|
4
|
+
constructor(stream) {
|
|
5
|
+
this.#stream = stream;
|
|
6
|
+
}
|
|
7
|
+
async readAll(maxBytes) {
|
|
8
|
+
const chunks = [];
|
|
9
|
+
let size = 0;
|
|
10
|
+
try {
|
|
11
|
+
for await (const chunk of this.#stream) {
|
|
12
|
+
const buffer = typeof chunk === 'string' ? Buffer.from(chunk) : chunk;
|
|
13
|
+
size += buffer.length;
|
|
14
|
+
if (size > maxBytes)
|
|
15
|
+
return undefined;
|
|
16
|
+
chunks.push(buffer);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
// A stream reports its failures as plain errors, and the port's promise is an answer, not an exception: a hook
|
|
21
|
+
// whose input broke off is told it was not checked (`specs/2026-09-16-when-an-agent-finishes.md` R8).
|
|
22
|
+
return undefined;
|
|
23
|
+
}
|
|
24
|
+
return Buffer.concat(chunks).toString('utf8');
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
|
+
/**
|
|
3
|
+
* A system notification (`specs/2026-09-16-when-an-agent-finishes.md` R15). On macOS through `osascript`, which showed
|
|
4
|
+
* in the terminal interface and, once notifications were allowed, outside it (B4f); on another platform the answer is
|
|
5
|
+
* `false`, and the terminal notice is what remains.
|
|
6
|
+
*
|
|
7
|
+
* The words travel as arguments to the script, never inside its source, so nothing in them can be read as AppleScript.
|
|
8
|
+
*/
|
|
9
|
+
export class OsNotifier {
|
|
10
|
+
#platform;
|
|
11
|
+
constructor(platform) {
|
|
12
|
+
this.#platform = platform;
|
|
13
|
+
}
|
|
14
|
+
notify(title, text) {
|
|
15
|
+
if (this.#platform !== 'darwin')
|
|
16
|
+
return Promise.resolve(false);
|
|
17
|
+
return new Promise((resolve) => {
|
|
18
|
+
execFile('osascript', ['-e', 'on run argv', '-e', 'display notification (item 2 of argv) with title (item 1 of argv)', '-e', 'end run', title, text], (error) => resolve(error === null));
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { chosenFolder, runProgram } from './folder-window.js';
|
|
2
|
+
/** What AppleScript answers when the person closes the window with Cancel. */
|
|
3
|
+
const CANCELLED = /-128\b/;
|
|
4
|
+
/**
|
|
5
|
+
* The script: forward through AppKit, which is quick (VB1), then Standard Additions' folder window, headed by the first
|
|
6
|
+
* argument and opened at the second where there is one. Its answer is the folder's path.
|
|
7
|
+
*/
|
|
8
|
+
const CHOOSE_FOLDER = [
|
|
9
|
+
'function run(argv) {',
|
|
10
|
+
" ObjC.import('AppKit');",
|
|
11
|
+
' $.NSApplication.sharedApplication.activateIgnoringOtherApps(true);',
|
|
12
|
+
' const app = Application.currentApplication();',
|
|
13
|
+
' app.includeStandardAdditions = true;',
|
|
14
|
+
' const options = { withPrompt: argv[0] };',
|
|
15
|
+
' if (argv[1]) options.defaultLocation = Path(argv[1]);',
|
|
16
|
+
' return app.chooseFolder(options).toString();',
|
|
17
|
+
'}',
|
|
18
|
+
].join('\n');
|
|
19
|
+
/**
|
|
20
|
+
* The folder window of macOS, opened through `osascript` (`.ai/specs/2026-09-27-which-project.md` V12, VD3), in front
|
|
21
|
+
* of the browser that asked for it. The prompt and the folder it starts in travel as arguments to the script, never inside
|
|
22
|
+
* its source, as `OsNotifier`'s words do. The answer is the folder's path with every link followed. Elsewhere there is no
|
|
23
|
+
* window this code has measured, and none is offered.
|
|
24
|
+
*
|
|
25
|
+
* VB1, measured 2026-09-28 on the maintainer's computer, from the moment `osascript` is started to the moment its window
|
|
26
|
+
* is on screen: AppleScript's `activate` then `choose folder` took 2.5 s and the window was still not in front; without
|
|
27
|
+
* `activate` 0.5 s, behind the browser; JavaScript for Automation bringing itself forward through AppKit, 0.6 s and in
|
|
28
|
+
* front. It is the last. The window starts in the folder it is given: left to itself it opens where it was last left,
|
|
29
|
+
* which on that computer is a folder iCloud keeps.
|
|
30
|
+
*/
|
|
31
|
+
export class OsascriptFolderChooser {
|
|
32
|
+
available;
|
|
33
|
+
#run;
|
|
34
|
+
constructor(platform, run = (args) => runProgram('osascript', args)) {
|
|
35
|
+
this.available = platform === 'darwin';
|
|
36
|
+
this.#run = run;
|
|
37
|
+
}
|
|
38
|
+
async choose(prompt, startIn) {
|
|
39
|
+
if (!this.available)
|
|
40
|
+
return { failed: 'This computer has no folder window agentwhy can open.' };
|
|
41
|
+
let answer;
|
|
42
|
+
try {
|
|
43
|
+
answer = await this.#run(['-l', 'JavaScript', '-e', CHOOSE_FOLDER, prompt, ...(startIn === undefined ? [] : [startIn])]);
|
|
44
|
+
}
|
|
45
|
+
catch (error) {
|
|
46
|
+
return { failed: error instanceof Error ? error.message : String(error) };
|
|
47
|
+
}
|
|
48
|
+
if (answer.code !== 0)
|
|
49
|
+
return CANCELLED.test(answer.stderr) ? { cancelled: true } : { failed: answer.stderr.trim() || 'The folder window could not open.' };
|
|
50
|
+
const path = answer.stdout.trim();
|
|
51
|
+
return path === '' ? { cancelled: true } : chosenFolder(path);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { chosenFolder, runProgram } from './folder-window.js';
|
|
2
|
+
/** What the script exits with when the person closes the window with Cancel. */
|
|
3
|
+
const CANCELLED = 3;
|
|
4
|
+
/** Text as the script receives it: base64 of its UTF-8, so nothing the page says is ever read as PowerShell. */
|
|
5
|
+
function encoded(text) {
|
|
6
|
+
return Buffer.from(text, 'utf8').toString('base64');
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* The script: Windows Forms' folder window, headed by the prompt and opened at the folder given where it is there, owned
|
|
10
|
+
* by an unseen window that stays on top, so it is not opened behind the browser that asked for it. The folder chosen is
|
|
11
|
+
* written as base64 of its UTF-8, since the console's code page would change a name that is not plain ASCII; Cancel exits
|
|
12
|
+
* with 3, and anything that went wrong is written after `failed:` - an error PowerShell wrote itself would reach stderr as
|
|
13
|
+
* serialized XML, which says nothing to a person.
|
|
14
|
+
*/
|
|
15
|
+
function script(prompt, startIn) {
|
|
16
|
+
return [
|
|
17
|
+
"$ErrorActionPreference = 'Stop'",
|
|
18
|
+
"$ProgressPreference = 'SilentlyContinue'",
|
|
19
|
+
'try {',
|
|
20
|
+
' Add-Type -AssemblyName System.Windows.Forms',
|
|
21
|
+
' [System.Windows.Forms.Application]::EnableVisualStyles()',
|
|
22
|
+
` $prompt = [Text.Encoding]::UTF8.GetString([Convert]::FromBase64String('${encoded(prompt)}'))`,
|
|
23
|
+
` $start = [Text.Encoding]::UTF8.GetString([Convert]::FromBase64String('${encoded(startIn ?? '')}'))`,
|
|
24
|
+
' $owner = New-Object System.Windows.Forms.Form',
|
|
25
|
+
' $owner.TopMost = $true',
|
|
26
|
+
' $owner.ShowInTaskbar = $false',
|
|
27
|
+
' $dialog = New-Object System.Windows.Forms.FolderBrowserDialog',
|
|
28
|
+
' $dialog.Description = $prompt',
|
|
29
|
+
' $dialog.ShowNewFolderButton = $true',
|
|
30
|
+
" if ($start -ne '' -and (Test-Path -LiteralPath $start -PathType Container)) { $dialog.SelectedPath = $start }",
|
|
31
|
+
' $result = $dialog.ShowDialog($owner)',
|
|
32
|
+
' $owner.Dispose()',
|
|
33
|
+
` if ($result -ne [System.Windows.Forms.DialogResult]::OK) { exit ${CANCELLED} }`,
|
|
34
|
+
' [Console]::Out.Write([Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes($dialog.SelectedPath)))',
|
|
35
|
+
' exit 0',
|
|
36
|
+
'} catch {',
|
|
37
|
+
" [Console]::Out.Write('failed:' + $_.Exception.Message)",
|
|
38
|
+
' exit 1',
|
|
39
|
+
'}',
|
|
40
|
+
].join('\n');
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* The folder window of Windows (`.ai/specs/2026-09-27-which-project.md` V12; VD3 amended 2026-09-28: "write it for Windows
|
|
44
|
+
* too"), opened through Windows PowerShell, which every Windows 10 and 11 has. The script travels as `-EncodedCommand` -
|
|
45
|
+
* UTF-16 in base64, as PowerShell reads it - and the prompt and the folder it starts in inside it as base64 too, so
|
|
46
|
+
* nothing the page sent is spliced into code.
|
|
47
|
+
*
|
|
48
|
+
* VB7, not measured: whether the window comes in front of the browser, how long PowerShell takes to open it, and what
|
|
49
|
+
* a folder with a name that is not plain ASCII comes back as. WSL is not this: there the platform is Linux, and no window
|
|
50
|
+
* is offered.
|
|
51
|
+
*/
|
|
52
|
+
export class PowershellFolderChooser {
|
|
53
|
+
available;
|
|
54
|
+
#run;
|
|
55
|
+
constructor(platform, run = (args) => runProgram('powershell.exe', args)) {
|
|
56
|
+
this.available = platform === 'win32';
|
|
57
|
+
this.#run = run;
|
|
58
|
+
}
|
|
59
|
+
async choose(prompt, startIn) {
|
|
60
|
+
if (!this.available)
|
|
61
|
+
return { failed: 'This computer has no folder window agentwhy can open.' };
|
|
62
|
+
let answer;
|
|
63
|
+
try {
|
|
64
|
+
answer = await this.#run(['-NoProfile', '-NonInteractive', '-STA', '-ExecutionPolicy', 'Bypass',
|
|
65
|
+
'-EncodedCommand', Buffer.from(script(prompt, startIn), 'utf16le').toString('base64')]);
|
|
66
|
+
}
|
|
67
|
+
catch (error) {
|
|
68
|
+
return { failed: error instanceof Error ? error.message : String(error) };
|
|
69
|
+
}
|
|
70
|
+
if (answer.code === CANCELLED)
|
|
71
|
+
return { cancelled: true };
|
|
72
|
+
const written = answer.stdout.trim();
|
|
73
|
+
if (answer.code !== 0)
|
|
74
|
+
return { failed: written.startsWith('failed:') ? written.slice('failed:'.length).trim() || 'The folder window could not open.' : answer.stderr.trim() || 'The folder window could not open.' };
|
|
75
|
+
const path = Buffer.from(written, 'base64').toString('utf8');
|
|
76
|
+
return path === '' ? { cancelled: true } : chosenFolder(path);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { realpathSync } from 'node:fs';
|
|
2
|
+
/**
|
|
3
|
+
* A directory as the system names it, every link followed: what a shell's working directory is given as, where `HOME`
|
|
4
|
+
* may reach the same directory through a link (`which-project.md` V6). Where a link cannot be followed - the path is
|
|
5
|
+
* missing, unreadable or a loop - the path as it was given, so a home that cannot be read is compared as it is written.
|
|
6
|
+
* An empty path names no directory and stays empty: `realpathSync` would resolve it to the directory the process
|
|
7
|
+
* stands in, and every run would be standing in its home.
|
|
8
|
+
*/
|
|
9
|
+
export function realDirectory(path) {
|
|
10
|
+
if (path === '')
|
|
11
|
+
return path;
|
|
12
|
+
try {
|
|
13
|
+
return realpathSync(path);
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return path;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/** Writes to a stream the shell owns. The shell decides which one; nothing here reads `process`. */
|
|
2
|
+
export class StreamPrinter {
|
|
3
|
+
#output;
|
|
4
|
+
constructor(output) {
|
|
5
|
+
this.#output = output;
|
|
6
|
+
}
|
|
7
|
+
write(text) {
|
|
8
|
+
if (text !== '')
|
|
9
|
+
this.#output.write(text);
|
|
10
|
+
}
|
|
11
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The lines drawn above the first question a command asks, and above none of the ones after it. `init` asks up to
|
|
3
|
+
* three things in a row; the mark belongs at the top of that conversation, not before every prompt. One banner is
|
|
4
|
+
* shared by the prompts of one command, and the first of them to be asked draws it.
|
|
5
|
+
*/
|
|
6
|
+
export class TerminalBanner {
|
|
7
|
+
#lines;
|
|
8
|
+
#drawn = false;
|
|
9
|
+
constructor(lines = []) {
|
|
10
|
+
this.#lines = lines;
|
|
11
|
+
}
|
|
12
|
+
/** Writes the lines the first time it is called, and nothing on any call after that. */
|
|
13
|
+
drawOn(output) {
|
|
14
|
+
if (this.#drawn || this.#lines.length === 0)
|
|
15
|
+
return;
|
|
16
|
+
this.#drawn = true;
|
|
17
|
+
output.write(`${this.#lines.join('\n')}\n`);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/** The only failure a file port reports. Implementations translate their own errors into it. */
|
|
2
|
+
export class FileAccessError extends Error {
|
|
3
|
+
failure;
|
|
4
|
+
path;
|
|
5
|
+
constructor(failure, path, options) {
|
|
6
|
+
super(`${failure}: ${path}`, options);
|
|
7
|
+
this.name = 'FileAccessError';
|
|
8
|
+
this.failure = failure;
|
|
9
|
+
this.path = path;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a person says they did about a file (R31; `for-people-who-build-with-ai.md` F25, F50): the keys it held were
|
|
3
|
+
* replaced, a template's values are placeholders, what could be done about a file whose contents cannot be changed was
|
|
4
|
+
* done, or the file holds nothing private.
|
|
5
|
+
*/
|
|
6
|
+
export const MARK_RESULTS = ['rotated', 'not-secret', 'handled', 'not-private'];
|
|
7
|
+
/** A result read from a file, a flag or a request, which this version knows. */
|
|
8
|
+
export function isMarkResult(value) {
|
|
9
|
+
return MARK_RESULTS.includes(value);
|
|
10
|
+
}
|