harnery 0.0.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +84 -2
- package/bin/agent-coord +42 -0
- package/bin/agent-hook +44 -0
- package/bin/harn +40 -0
- package/dist/cli.d.ts +9 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +18 -0
- package/dist/commander.d.ts +128 -0
- package/dist/commander.d.ts.map +1 -0
- package/dist/commander.js +126 -0
- package/dist/commands/agents.d.ts +18 -0
- package/dist/commands/agents.d.ts.map +1 -0
- package/dist/commands/agents.js +3946 -0
- package/dist/commands/backup.d.ts +22 -0
- package/dist/commands/backup.d.ts.map +1 -0
- package/dist/commands/backup.js +262 -0
- package/dist/commands/browse-ai.d.ts +4 -0
- package/dist/commands/browse-ai.d.ts.map +1 -0
- package/dist/commands/browse-ai.js +156 -0
- package/dist/commands/browse.d.ts +4 -0
- package/dist/commands/browse.d.ts.map +1 -0
- package/dist/commands/browse.js +590 -0
- package/dist/commands/callers.d.ts +4 -0
- package/dist/commands/callers.d.ts.map +1 -0
- package/dist/commands/callers.js +276 -0
- package/dist/commands/completion.d.ts +17 -0
- package/dist/commands/completion.d.ts.map +1 -0
- package/dist/commands/completion.js +158 -0
- package/dist/commands/config-get.d.ts +4 -0
- package/dist/commands/config-get.d.ts.map +1 -0
- package/dist/commands/config-get.js +131 -0
- package/dist/commands/context.d.ts +11 -0
- package/dist/commands/context.d.ts.map +1 -0
- package/dist/commands/context.js +185 -0
- package/dist/commands/cookies.d.ts +4 -0
- package/dist/commands/cookies.d.ts.map +1 -0
- package/dist/commands/cookies.js +140 -0
- package/dist/commands/docs.d.ts +4 -0
- package/dist/commands/docs.d.ts.map +1 -0
- package/dist/commands/docs.js +137 -0
- package/dist/commands/doctor.d.ts +25 -0
- package/dist/commands/doctor.d.ts.map +1 -0
- package/dist/commands/doctor.js +200 -0
- package/dist/commands/edit-batch.d.ts +18 -0
- package/dist/commands/edit-batch.d.ts.map +1 -0
- package/dist/commands/edit-batch.js +172 -0
- package/dist/commands/eml.d.ts +4 -0
- package/dist/commands/eml.d.ts.map +1 -0
- package/dist/commands/eml.js +428 -0
- package/dist/commands/env.d.ts +4 -0
- package/dist/commands/env.d.ts.map +1 -0
- package/dist/commands/env.js +201 -0
- package/dist/commands/fetch.d.ts +4 -0
- package/dist/commands/fetch.d.ts.map +1 -0
- package/dist/commands/fetch.js +99 -0
- package/dist/commands/file-history.d.ts +4 -0
- package/dist/commands/file-history.d.ts.map +1 -0
- package/dist/commands/file-history.js +152 -0
- package/dist/commands/grep.d.ts +4 -0
- package/dist/commands/grep.d.ts.map +1 -0
- package/dist/commands/grep.js +317 -0
- package/dist/commands/init.d.ts +82 -0
- package/dist/commands/init.d.ts.map +1 -0
- package/dist/commands/init.js +288 -0
- package/dist/commands/outline.d.ts +4 -0
- package/dist/commands/outline.d.ts.map +1 -0
- package/dist/commands/outline.js +494 -0
- package/dist/commands/presence.d.ts +12 -0
- package/dist/commands/presence.d.ts.map +1 -0
- package/dist/commands/presence.js +123 -0
- package/dist/commands/read.d.ts +7 -0
- package/dist/commands/read.d.ts.map +1 -0
- package/dist/commands/read.js +46 -0
- package/dist/commands/scratch.d.ts +4 -0
- package/dist/commands/scratch.d.ts.map +1 -0
- package/dist/commands/scratch.js +426 -0
- package/dist/commands/session.d.ts +4 -0
- package/dist/commands/session.d.ts.map +1 -0
- package/dist/commands/session.js +162 -0
- package/dist/commands/sync.d.ts +24 -0
- package/dist/commands/sync.d.ts.map +1 -0
- package/dist/commands/sync.js +275 -0
- package/dist/commands/toc.d.ts +5 -0
- package/dist/commands/toc.d.ts.map +1 -0
- package/dist/commands/toc.js +153 -0
- package/dist/commands/tokens.d.ts +4 -0
- package/dist/commands/tokens.d.ts.map +1 -0
- package/dist/commands/tokens.js +48 -0
- package/dist/commands/tunnel.d.ts +4 -0
- package/dist/commands/tunnel.d.ts.map +1 -0
- package/dist/commands/tunnel.js +513 -0
- package/dist/commands/uninstall.d.ts +22 -0
- package/dist/commands/uninstall.d.ts.map +1 -0
- package/dist/commands/uninstall.js +126 -0
- package/dist/commands/web.d.ts +4 -0
- package/dist/commands/web.d.ts.map +1 -0
- package/dist/commands/web.js +165 -0
- package/dist/core/agents/canonical-emit.d.ts +27 -0
- package/dist/core/agents/canonical-emit.d.ts.map +1 -0
- package/dist/core/agents/canonical-emit.js +72 -0
- package/dist/core/agents/cli-emit.d.ts +27 -0
- package/dist/core/agents/cli-emit.d.ts.map +1 -0
- package/dist/core/agents/cli-emit.js +57 -0
- package/dist/core/agents/cli.d.ts +10 -0
- package/dist/core/agents/cli.d.ts.map +1 -0
- package/dist/core/agents/cli.js +757 -0
- package/dist/core/agents/codex-replay.d.ts +29 -0
- package/dist/core/agents/codex-replay.d.ts.map +1 -0
- package/dist/core/agents/codex-replay.js +138 -0
- package/dist/core/agents/coord-client.d.ts +98 -0
- package/dist/core/agents/coord-client.d.ts.map +1 -0
- package/dist/core/agents/coord-client.js +212 -0
- package/dist/core/agents/events/consume.d.ts +59 -0
- package/dist/core/agents/events/consume.d.ts.map +1 -0
- package/dist/core/agents/events/consume.js +147 -0
- package/dist/core/agents/events/emit.d.ts +42 -0
- package/dist/core/agents/events/emit.d.ts.map +1 -0
- package/dist/core/agents/events/emit.js +70 -0
- package/dist/core/agents/events/ulid.d.ts +11 -0
- package/dist/core/agents/events/ulid.d.ts.map +1 -0
- package/dist/core/agents/events/ulid.js +47 -0
- package/dist/core/agents/index.d.ts +14 -0
- package/dist/core/agents/index.d.ts.map +1 -0
- package/dist/core/agents/index.js +13 -0
- package/dist/core/agents/paths.d.ts +6 -0
- package/dist/core/agents/paths.d.ts.map +1 -0
- package/dist/core/agents/paths.js +17 -0
- package/dist/core/agents/render/prompt-context.d.ts +43 -0
- package/dist/core/agents/render/prompt-context.d.ts.map +1 -0
- package/dist/core/agents/render/prompt-context.js +335 -0
- package/dist/core/agents/render/session-context.d.ts +39 -0
- package/dist/core/agents/render/session-context.d.ts.map +1 -0
- package/dist/core/agents/render/session-context.js +283 -0
- package/dist/core/agents/rules/claim-conflict.d.ts +35 -0
- package/dist/core/agents/rules/claim-conflict.d.ts.map +1 -0
- package/dist/core/agents/rules/claim-conflict.js +244 -0
- package/dist/core/agents/rules/commit-conflict.d.ts +59 -0
- package/dist/core/agents/rules/commit-conflict.d.ts.map +1 -0
- package/dist/core/agents/rules/commit-conflict.js +244 -0
- package/dist/core/agents/rules/stop-hook.d.ts +44 -0
- package/dist/core/agents/rules/stop-hook.d.ts.map +1 -0
- package/dist/core/agents/rules/stop-hook.js +161 -0
- package/dist/core/agents/session-events.d.ts +41 -0
- package/dist/core/agents/session-events.d.ts.map +1 -0
- package/dist/core/agents/session-events.js +205 -0
- package/dist/core/agents/state/activity-log.d.ts +18 -0
- package/dist/core/agents/state/activity-log.d.ts.map +1 -0
- package/dist/core/agents/state/activity-log.js +34 -0
- package/dist/core/agents/state/council.d.ts +39 -0
- package/dist/core/agents/state/council.d.ts.map +1 -0
- package/dist/core/agents/state/council.js +216 -0
- package/dist/core/agents/state/heartbeat-projector.d.ts +59 -0
- package/dist/core/agents/state/heartbeat-projector.d.ts.map +1 -0
- package/dist/core/agents/state/heartbeat-projector.js +436 -0
- package/dist/core/agents/state/heartbeat-writer.d.ts +64 -0
- package/dist/core/agents/state/heartbeat-writer.d.ts.map +1 -0
- package/dist/core/agents/state/heartbeat-writer.js +271 -0
- package/dist/core/agents/state/names.d.ts +35 -0
- package/dist/core/agents/state/names.d.ts.map +1 -0
- package/dist/core/agents/state/names.js +376 -0
- package/dist/core/agents/state/pidmap.d.ts +11 -0
- package/dist/core/agents/state/pidmap.d.ts.map +1 -0
- package/dist/core/agents/state/pidmap.js +32 -0
- package/dist/core/agents/state/scratch.d.ts +27 -0
- package/dist/core/agents/state/scratch.d.ts.map +1 -0
- package/dist/core/agents/state/scratch.js +90 -0
- package/dist/core/agents/state/shell-mutation.d.ts +17 -0
- package/dist/core/agents/state/shell-mutation.d.ts.map +1 -0
- package/dist/core/agents/state/shell-mutation.js +41 -0
- package/dist/core/agents/state/stale-sweep.d.ts +16 -0
- package/dist/core/agents/state/stale-sweep.d.ts.map +1 -0
- package/dist/core/agents/state/stale-sweep.js +166 -0
- package/dist/core/config.d.ts +29 -0
- package/dist/core/config.d.ts.map +1 -0
- package/dist/core/config.js +108 -0
- package/dist/core/hooks/cli.d.ts +21 -0
- package/dist/core/hooks/cli.d.ts.map +1 -0
- package/dist/core/hooks/cli.js +1123 -0
- package/dist/core/hooks/effects/image-capture.d.ts +43 -0
- package/dist/core/hooks/effects/image-capture.d.ts.map +1 -0
- package/dist/core/hooks/effects/image-capture.js +288 -0
- package/dist/core/hooks/effects/index.d.ts +64 -0
- package/dist/core/hooks/effects/index.d.ts.map +1 -0
- package/dist/core/hooks/effects/index.js +197 -0
- package/dist/core/hooks/events/emit.d.ts +31 -0
- package/dist/core/hooks/events/emit.d.ts.map +1 -0
- package/dist/core/hooks/events/emit.js +89 -0
- package/dist/core/hooks/events/schema.d.ts +235 -0
- package/dist/core/hooks/events/schema.d.ts.map +1 -0
- package/dist/core/hooks/events/schema.js +12 -0
- package/dist/core/hooks/events/ulid.d.ts +10 -0
- package/dist/core/hooks/events/ulid.d.ts.map +1 -0
- package/dist/core/hooks/events/ulid.js +47 -0
- package/dist/core/hooks/harness/detect.d.ts +9 -0
- package/dist/core/hooks/harness/detect.d.ts.map +1 -0
- package/dist/core/hooks/harness/detect.js +29 -0
- package/dist/core/hooks/harness/events.d.ts +45 -0
- package/dist/core/hooks/harness/events.d.ts.map +1 -0
- package/dist/core/hooks/harness/events.js +71 -0
- package/dist/core/hooks/harness/output.d.ts +46 -0
- package/dist/core/hooks/harness/output.d.ts.map +1 -0
- package/dist/core/hooks/harness/output.js +87 -0
- package/dist/core/hooks/harness/parse.d.ts +67 -0
- package/dist/core/hooks/harness/parse.d.ts.map +1 -0
- package/dist/core/hooks/harness/parse.js +132 -0
- package/dist/core/hooks/index.d.ts +8 -0
- package/dist/core/hooks/index.d.ts.map +1 -0
- package/dist/core/hooks/index.js +7 -0
- package/dist/core/hooks/resolve/anchor.d.ts +37 -0
- package/dist/core/hooks/resolve/anchor.d.ts.map +1 -0
- package/dist/core/hooks/resolve/anchor.js +48 -0
- package/dist/core/hooks/resolve/coord-root.d.ts +6 -0
- package/dist/core/hooks/resolve/coord-root.d.ts.map +1 -0
- package/dist/core/hooks/resolve/coord-root.js +27 -0
- package/dist/core/hooks/resolve/intent.d.ts +33 -0
- package/dist/core/hooks/resolve/intent.d.ts.map +1 -0
- package/dist/core/hooks/resolve/intent.js +79 -0
- package/dist/core/hooks/resolve/owner.d.ts +42 -0
- package/dist/core/hooks/resolve/owner.d.ts.map +1 -0
- package/dist/core/hooks/resolve/owner.js +140 -0
- package/dist/core/hooks/resolve/transcript.d.ts +26 -0
- package/dist/core/hooks/resolve/transcript.d.ts.map +1 -0
- package/dist/core/hooks/resolve/transcript.js +73 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +13 -0
- package/dist/lib/agent-browser/client.d.ts +99 -0
- package/dist/lib/agent-browser/client.d.ts.map +1 -0
- package/dist/lib/agent-browser/client.js +177 -0
- package/dist/lib/agent-browser/index.d.ts +2 -0
- package/dist/lib/agent-browser/index.d.ts.map +1 -0
- package/dist/lib/agent-browser/index.js +1 -0
- package/dist/lib/browser/client.d.ts +193 -0
- package/dist/lib/browser/client.d.ts.map +1 -0
- package/dist/lib/browser/client.js +325 -0
- package/dist/lib/browser/dev-overlay.d.ts +23 -0
- package/dist/lib/browser/dev-overlay.d.ts.map +1 -0
- package/dist/lib/browser/dev-overlay.js +153 -0
- package/dist/lib/browser/index.d.ts +5 -0
- package/dist/lib/browser/index.d.ts.map +1 -0
- package/dist/lib/browser/index.js +2 -0
- package/dist/lib/browser/layout.d.ts +79 -0
- package/dist/lib/browser/layout.d.ts.map +1 -0
- package/dist/lib/browser/layout.js +220 -0
- package/dist/lib/browser/visibility.d.ts +86 -0
- package/dist/lib/browser/visibility.d.ts.map +1 -0
- package/dist/lib/browser/visibility.js +333 -0
- package/dist/lib/browser/visual-diff.d.ts +38 -0
- package/dist/lib/browser/visual-diff.d.ts.map +1 -0
- package/dist/lib/browser/visual-diff.js +107 -0
- package/dist/lib/completion/bash.d.ts +25 -0
- package/dist/lib/completion/bash.d.ts.map +1 -0
- package/dist/lib/completion/bash.js +284 -0
- package/dist/lib/completion/fish.d.ts +16 -0
- package/dist/lib/completion/fish.d.ts.map +1 -0
- package/dist/lib/completion/fish.js +118 -0
- package/dist/lib/completion/index.d.ts +5 -0
- package/dist/lib/completion/index.d.ts.map +1 -0
- package/dist/lib/completion/index.js +4 -0
- package/dist/lib/completion/walk.d.ts +68 -0
- package/dist/lib/completion/walk.d.ts.map +1 -0
- package/dist/lib/completion/walk.js +102 -0
- package/dist/lib/completion/zsh.d.ts +13 -0
- package/dist/lib/completion/zsh.d.ts.map +1 -0
- package/dist/lib/completion/zsh.js +249 -0
- package/dist/lib/context/index.d.ts +107 -0
- package/dist/lib/context/index.d.ts.map +1 -0
- package/dist/lib/context/index.js +275 -0
- package/dist/lib/cookies/client.d.ts +131 -0
- package/dist/lib/cookies/client.d.ts.map +1 -0
- package/dist/lib/cookies/client.js +239 -0
- package/dist/lib/cookies/index.d.ts +2 -0
- package/dist/lib/cookies/index.d.ts.map +1 -0
- package/dist/lib/cookies/index.js +1 -0
- package/dist/lib/council/index.d.ts +266 -0
- package/dist/lib/council/index.d.ts.map +1 -0
- package/dist/lib/council/index.js +674 -0
- package/dist/lib/docs-index.d.ts +28 -0
- package/dist/lib/docs-index.d.ts.map +1 -0
- package/dist/lib/docs-index.js +169 -0
- package/dist/lib/docs-lint.d.ts +26 -0
- package/dist/lib/docs-lint.d.ts.map +1 -0
- package/dist/lib/docs-lint.js +378 -0
- package/dist/lib/docs-sweep.d.ts +34 -0
- package/dist/lib/docs-sweep.d.ts.map +1 -0
- package/dist/lib/docs-sweep.js +304 -0
- package/dist/lib/docs.d.ts +27 -0
- package/dist/lib/docs.d.ts.map +1 -0
- package/dist/lib/docs.js +142 -0
- package/dist/lib/env.d.ts +11 -0
- package/dist/lib/env.d.ts.map +1 -0
- package/dist/lib/env.js +12 -0
- package/dist/lib/exec.d.ts +32 -0
- package/dist/lib/exec.d.ts.map +1 -0
- package/dist/lib/exec.js +54 -0
- package/dist/lib/format.d.ts +29 -0
- package/dist/lib/format.d.ts.map +1 -0
- package/dist/lib/format.js +139 -0
- package/dist/lib/http/client.d.ts +56 -0
- package/dist/lib/http/client.d.ts.map +1 -0
- package/dist/lib/http/client.js +160 -0
- package/dist/lib/http/index.d.ts +2 -0
- package/dist/lib/http/index.d.ts.map +1 -0
- package/dist/lib/http/index.js +1 -0
- package/dist/lib/identities/index.d.ts +77 -0
- package/dist/lib/identities/index.d.ts.map +1 -0
- package/dist/lib/identities/index.js +190 -0
- package/dist/lib/machine.d.ts +19 -0
- package/dist/lib/machine.d.ts.map +1 -0
- package/dist/lib/machine.js +61 -0
- package/dist/lib/presence.d.ts +48 -0
- package/dist/lib/presence.d.ts.map +1 -0
- package/dist/lib/presence.js +123 -0
- package/dist/lib/readability/client.d.ts +32 -0
- package/dist/lib/readability/client.d.ts.map +1 -0
- package/dist/lib/readability/client.js +119 -0
- package/dist/lib/readability/index.d.ts +2 -0
- package/dist/lib/readability/index.d.ts.map +1 -0
- package/dist/lib/readability/index.js +1 -0
- package/dist/lib/scratch/index.d.ts +74 -0
- package/dist/lib/scratch/index.d.ts.map +1 -0
- package/dist/lib/scratch/index.js +393 -0
- package/dist/lib/tunnel/gate.d.ts +12 -0
- package/dist/lib/tunnel/gate.d.ts.map +1 -0
- package/dist/lib/tunnel/gate.js +101 -0
- package/dist/lib/tunnel/state.d.ts +34 -0
- package/dist/lib/tunnel/state.d.ts.map +1 -0
- package/dist/lib/tunnel/state.js +132 -0
- package/package.json +160 -8
- package/schemas/.gitkeep +0 -0
- package/schemas/config.schema.json +109 -0
- package/src/cli.ts +22 -0
- package/src/commander.ts +242 -0
- package/src/commands/.gitkeep +0 -0
- package/src/commands/agents.ts +4567 -0
- package/src/commands/backup.ts +305 -0
- package/src/commands/browse-ai.ts +198 -0
- package/src/commands/browse.ts +849 -0
- package/src/commands/callers.ts +363 -0
- package/src/commands/completion.ts +193 -0
- package/src/commands/config-get.ts +161 -0
- package/src/commands/context.ts +209 -0
- package/src/commands/cookies.ts +198 -0
- package/src/commands/docs.ts +174 -0
- package/src/commands/doctor.ts +231 -0
- package/src/commands/edit-batch.ts +233 -0
- package/src/commands/eml.ts +519 -0
- package/src/commands/env.ts +254 -0
- package/src/commands/fetch.ts +136 -0
- package/src/commands/file-history.ts +202 -0
- package/src/commands/grep.ts +371 -0
- package/src/commands/init.ts +335 -0
- package/src/commands/outline.ts +564 -0
- package/src/commands/presence.ts +152 -0
- package/src/commands/read.ts +64 -0
- package/src/commands/scratch.ts +445 -0
- package/src/commands/session.ts +187 -0
- package/src/commands/sync.ts +306 -0
- package/src/commands/toc.ts +218 -0
- package/src/commands/tokens.ts +79 -0
- package/src/commands/tunnel.ts +633 -0
- package/src/commands/uninstall.ts +144 -0
- package/src/commands/web.ts +193 -0
- package/src/core/agents/canonical-emit.ts +77 -0
- package/src/core/agents/cli-emit.ts +64 -0
- package/src/core/agents/cli.ts +838 -0
- package/src/core/agents/codex-replay.ts +163 -0
- package/src/core/agents/coord-client.ts +249 -0
- package/src/core/agents/events/consume.ts +196 -0
- package/src/core/agents/events/emit.ts +108 -0
- package/src/core/agents/events/ulid.ts +51 -0
- package/src/core/agents/index.ts +14 -0
- package/src/core/agents/paths.ts +16 -0
- package/src/core/agents/render/prompt-context.ts +401 -0
- package/src/core/agents/render/session-context.ts +341 -0
- package/src/core/agents/rules/claim-conflict.ts +282 -0
- package/src/core/agents/rules/commit-conflict.ts +303 -0
- package/src/core/agents/rules/stop-hook.ts +229 -0
- package/src/core/agents/session-events.ts +228 -0
- package/src/core/agents/state/activity-log.ts +33 -0
- package/src/core/agents/state/council.ts +265 -0
- package/src/core/agents/state/heartbeat-projector.ts +488 -0
- package/src/core/agents/state/heartbeat-writer.ts +333 -0
- package/src/core/agents/state/names.ts +399 -0
- package/src/core/agents/state/pidmap.ts +38 -0
- package/src/core/agents/state/scratch.ts +121 -0
- package/src/core/agents/state/shell-mutation.ts +44 -0
- package/src/core/agents/state/stale-sweep.ts +190 -0
- package/src/core/config.ts +111 -0
- package/src/core/hooks/cli.ts +1247 -0
- package/src/core/hooks/effects/image-capture.ts +330 -0
- package/src/core/hooks/effects/index.ts +210 -0
- package/src/core/hooks/events/emit.ts +120 -0
- package/src/core/hooks/events/schema.ts +430 -0
- package/src/core/hooks/events/ulid.ts +51 -0
- package/src/core/hooks/harness/detect.ts +30 -0
- package/src/core/hooks/harness/events.ts +102 -0
- package/src/core/hooks/harness/output.ts +100 -0
- package/src/core/hooks/harness/parse.ts +180 -0
- package/src/core/hooks/index.ts +16 -0
- package/src/core/hooks/resolve/anchor.ts +51 -0
- package/src/core/hooks/resolve/coord-root.ts +25 -0
- package/src/core/hooks/resolve/intent.ts +89 -0
- package/src/core/hooks/resolve/owner.ts +140 -0
- package/src/core/hooks/resolve/transcript.ts +72 -0
- package/src/hooks/.gitkeep +0 -0
- package/src/index.ts +15 -0
- package/src/lib/agent-browser/client.ts +239 -0
- package/src/lib/agent-browser/index.ts +1 -0
- package/src/lib/browser/client.ts +449 -0
- package/src/lib/browser/dev-overlay.ts +207 -0
- package/src/lib/browser/index.ts +24 -0
- package/src/lib/browser/layout.ts +288 -0
- package/src/lib/browser/visibility.ts +419 -0
- package/src/lib/browser/visual-diff.ts +150 -0
- package/src/lib/completion/bash.ts +291 -0
- package/src/lib/completion/fish.ts +134 -0
- package/src/lib/completion/index.ts +10 -0
- package/src/lib/completion/walk.ts +184 -0
- package/src/lib/completion/zsh.ts +262 -0
- package/src/lib/context/index.ts +386 -0
- package/src/lib/cookies/client.ts +301 -0
- package/src/lib/cookies/index.ts +13 -0
- package/src/lib/council/index.ts +803 -0
- package/src/lib/docs-index.ts +216 -0
- package/src/lib/docs-lint.ts +413 -0
- package/src/lib/docs-sweep.ts +348 -0
- package/src/lib/docs.ts +199 -0
- package/src/lib/env.ts +12 -0
- package/src/lib/exec.ts +74 -0
- package/src/lib/format.ts +147 -0
- package/src/lib/http/client.ts +211 -0
- package/src/lib/http/index.ts +1 -0
- package/src/lib/identities/index.ts +210 -0
- package/src/lib/machine.ts +61 -0
- package/src/lib/presence.ts +154 -0
- package/src/lib/readability/client.ts +156 -0
- package/src/lib/readability/index.ts +5 -0
- package/src/lib/readability/turndown-plugin-gfm.d.ts +10 -0
- package/src/lib/scratch/index.ts +470 -0
- package/src/lib/tunnel/gate.ts +113 -0
- package/src/lib/tunnel/state.ts +167 -0
- package/src/web/.gitkeep +0 -0
- package/index.js +0 -1
|
@@ -0,0 +1,519 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import type { Command } from "commander";
|
|
3
|
+
import { type ParsedMail, simpleParser } from "mailparser";
|
|
4
|
+
import type { EmitContext } from "../commander.ts";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* `eml` - Parse Gmail .eml thread exports into clean chronological markdown.
|
|
8
|
+
*
|
|
9
|
+
* Gmail exports a thread as a single .eml where older messages are nested
|
|
10
|
+
* inside `<div class="gmail_quote">` blocks with attribution lines in
|
|
11
|
+
* `<div class="gmail_attr">`. This command reconstructs the full thread
|
|
12
|
+
* in chronological order. Falls back to plain-text quote parsing (`> `
|
|
13
|
+
* prefixes + "On ... wrote:") when HTML is unavailable. Uses the injected
|
|
14
|
+
* EmitContext so composed and standalone consumers share one code path.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
interface ThreadMessage {
|
|
18
|
+
from: string;
|
|
19
|
+
date: Date | null;
|
|
20
|
+
body: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function registerEmlCommand(program: Command, emit: EmitContext): void {
|
|
24
|
+
program
|
|
25
|
+
.command("eml")
|
|
26
|
+
.description("Parse a Gmail .eml file into a clean chronological markdown thread")
|
|
27
|
+
.argument("<file>", "Path to the .eml file")
|
|
28
|
+
.option("-o, --output <path>", "Write output to a file instead of stdout")
|
|
29
|
+
.option("--format <type>", "Output format: markdown, json", "markdown")
|
|
30
|
+
.option("--headers", "Include full email headers per message")
|
|
31
|
+
.option("--attachments", "List attachment filenames and sizes")
|
|
32
|
+
.action(async (file: string, opts: EmlOpts) => {
|
|
33
|
+
try {
|
|
34
|
+
await handleEml(file, opts, emit);
|
|
35
|
+
} catch (err) {
|
|
36
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
37
|
+
emit.error({ code: "eml_error", message: msg });
|
|
38
|
+
process.exit(1);
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
interface EmlOpts {
|
|
44
|
+
output?: string;
|
|
45
|
+
format: string;
|
|
46
|
+
headers?: boolean;
|
|
47
|
+
attachments?: boolean;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async function handleEml(file: string, opts: EmlOpts, emit: EmitContext): Promise<void> {
|
|
51
|
+
const raw = readFileSync(file);
|
|
52
|
+
const parsed = await simpleParser(raw);
|
|
53
|
+
|
|
54
|
+
const messages = extractThread(parsed);
|
|
55
|
+
|
|
56
|
+
if (messages.length === 0) {
|
|
57
|
+
emit.error({ code: "no_messages", message: "No messages found in the .eml file." });
|
|
58
|
+
process.exit(1);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (opts.format === "json") {
|
|
62
|
+
const rows = messages.map((m, i) => ({
|
|
63
|
+
index: i + 1,
|
|
64
|
+
from: m.from,
|
|
65
|
+
date: m.date?.toISOString() ?? null,
|
|
66
|
+
body: m.body,
|
|
67
|
+
}));
|
|
68
|
+
if (opts.output) {
|
|
69
|
+
writeFileSync(opts.output, JSON.stringify(rows));
|
|
70
|
+
emit.file(opts.output, { messages: messages.length, format: "json" });
|
|
71
|
+
} else {
|
|
72
|
+
emit.config({ format: "json" });
|
|
73
|
+
emit.rows(rows as Record<string, unknown>[]);
|
|
74
|
+
}
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const output = renderMarkdown(parsed, messages, opts);
|
|
79
|
+
if (opts.output) {
|
|
80
|
+
writeFileSync(opts.output, output);
|
|
81
|
+
emit.file(opts.output, { messages: messages.length, format: "markdown" });
|
|
82
|
+
} else {
|
|
83
|
+
emit.text(output);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Extract individual messages from the parsed email.
|
|
89
|
+
* Strategy: use HTML body (Gmail quote structure) first, fall back to plain text.
|
|
90
|
+
*/
|
|
91
|
+
function extractThread(parsed: ParsedMail): ThreadMessage[] {
|
|
92
|
+
if (parsed.html && typeof parsed.html === "string") {
|
|
93
|
+
const messages = extractFromHtml(parsed.html, parsed);
|
|
94
|
+
if (messages.length > 0) return messages;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Fallback: plain text
|
|
98
|
+
if (parsed.text) {
|
|
99
|
+
return extractFromPlainText(parsed.text, parsed);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return [];
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Parse Gmail's nested HTML quote structure.
|
|
107
|
+
*
|
|
108
|
+
* Gmail wraps each quoted reply in:
|
|
109
|
+
* <div class="gmail_quote">
|
|
110
|
+
* <div class="gmail_attr">On <date>, <name> <<email>> wrote:</div>
|
|
111
|
+
* <blockquote class="gmail_quote">...nested older messages...</blockquote>
|
|
112
|
+
* </div>
|
|
113
|
+
*/
|
|
114
|
+
function extractFromHtml(html: string, parsed: ParsedMail): ThreadMessage[] {
|
|
115
|
+
const messages: ThreadMessage[] = [];
|
|
116
|
+
|
|
117
|
+
// Work with the decoded HTML
|
|
118
|
+
const doc = html;
|
|
119
|
+
|
|
120
|
+
// Extract attribution lines and their positions to find message boundaries
|
|
121
|
+
// Pattern: <div ... class="gmail_attr">On ..., ... wrote:<br></div>
|
|
122
|
+
const attrPattern = /class=["']gmail_attr["'][^>]*>([\s\S]*?)<\/div>/gi;
|
|
123
|
+
const attrs: { text: string; index: number }[] = [];
|
|
124
|
+
|
|
125
|
+
let match: RegExpExecArray | null = attrPattern.exec(doc);
|
|
126
|
+
while (match !== null) {
|
|
127
|
+
attrs.push({ text: stripHtml(match[1]), index: match.index });
|
|
128
|
+
match = attrPattern.exec(doc);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// The top-level message (newest) is before the first gmail_quote
|
|
132
|
+
const firstQuoteIdx = doc.search(/class=["']gmail_quote\s*(gmail_quote_container)?["']/i);
|
|
133
|
+
if (firstQuoteIdx > 0) {
|
|
134
|
+
// Extract the top-level body (everything before the first quote container)
|
|
135
|
+
const topBody = extractBodyBeforeQuote(doc, firstQuoteIdx);
|
|
136
|
+
if (topBody.trim()) {
|
|
137
|
+
messages.push({
|
|
138
|
+
from: formatAddress(parsed.from),
|
|
139
|
+
date: parsed.date ?? null,
|
|
140
|
+
body: topBody,
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
} else {
|
|
144
|
+
// No quotes - single message
|
|
145
|
+
messages.push({
|
|
146
|
+
from: formatAddress(parsed.from),
|
|
147
|
+
date: parsed.date ?? null,
|
|
148
|
+
body: cleanBody(stripHtml(doc)),
|
|
149
|
+
});
|
|
150
|
+
return messages;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Now extract each quoted message using attribution lines
|
|
154
|
+
for (let i = 0; i < attrs.length; i++) {
|
|
155
|
+
const attr = attrs[i];
|
|
156
|
+
const { name, date } = parseAttribution(attr.text);
|
|
157
|
+
|
|
158
|
+
// Find the blockquote that follows this attribution
|
|
159
|
+
const afterAttr = doc.indexOf("</div>", attr.index + 10);
|
|
160
|
+
if (afterAttr === -1) continue;
|
|
161
|
+
|
|
162
|
+
// Find the blockquote content after the attribution div
|
|
163
|
+
const searchRegion = doc.slice(afterAttr, afterAttr + 500);
|
|
164
|
+
const bqStart = searchRegion.indexOf("<blockquote");
|
|
165
|
+
if (bqStart === -1) continue;
|
|
166
|
+
|
|
167
|
+
const bqStartAbsolute = afterAttr + bqStart;
|
|
168
|
+
const bqContent = extractBlockquoteContent(doc, bqStartAbsolute);
|
|
169
|
+
|
|
170
|
+
// The body is everything in this blockquote BEFORE any nested gmail_quote
|
|
171
|
+
const nestedQuoteIdx = bqContent.search(/class=["']gmail_quote["']/i);
|
|
172
|
+
const bodyHtml = nestedQuoteIdx > 0 ? bqContent.slice(0, nestedQuoteIdx) : bqContent;
|
|
173
|
+
|
|
174
|
+
// Find the actual start of content (skip the opening blockquote tag)
|
|
175
|
+
const tagEnd = bodyHtml.indexOf(">");
|
|
176
|
+
const content = tagEnd > 0 ? bodyHtml.slice(tagEnd + 1) : bodyHtml;
|
|
177
|
+
|
|
178
|
+
const body = cleanBody(stripHtml(content));
|
|
179
|
+
if (body.trim()) {
|
|
180
|
+
messages.push({ from: name, date, body });
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Reverse: messages are newest-first in the .eml, we want oldest-first
|
|
185
|
+
messages.reverse();
|
|
186
|
+
return messages;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Extract content of a blockquote tag, handling nested blockquotes.
|
|
191
|
+
*/
|
|
192
|
+
function extractBlockquoteContent(html: string, startIdx: number): string {
|
|
193
|
+
let depth = 0;
|
|
194
|
+
let i = startIdx;
|
|
195
|
+
const openTag = /<blockquote/gi;
|
|
196
|
+
const closeTag = /<\/blockquote>/gi;
|
|
197
|
+
|
|
198
|
+
// Find the end of the opening tag
|
|
199
|
+
const tagEnd = html.indexOf(">", startIdx);
|
|
200
|
+
if (tagEnd === -1) return "";
|
|
201
|
+
|
|
202
|
+
i = tagEnd + 1;
|
|
203
|
+
depth = 1;
|
|
204
|
+
|
|
205
|
+
while (depth > 0 && i < html.length) {
|
|
206
|
+
openTag.lastIndex = i;
|
|
207
|
+
closeTag.lastIndex = i;
|
|
208
|
+
|
|
209
|
+
const nextOpen = openTag.exec(html);
|
|
210
|
+
const nextClose = closeTag.exec(html);
|
|
211
|
+
|
|
212
|
+
if (!nextClose) break;
|
|
213
|
+
|
|
214
|
+
if (nextOpen && nextOpen.index < nextClose.index) {
|
|
215
|
+
depth++;
|
|
216
|
+
i = nextOpen.index + 12;
|
|
217
|
+
} else {
|
|
218
|
+
depth--;
|
|
219
|
+
if (depth === 0) {
|
|
220
|
+
return html.slice(tagEnd + 1, nextClose.index);
|
|
221
|
+
}
|
|
222
|
+
i = nextClose.index + 13;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
return html.slice(tagEnd + 1, Math.min(startIdx + 50000, html.length));
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Get body content before the first gmail_quote div.
|
|
231
|
+
*/
|
|
232
|
+
function extractBodyBeforeQuote(html: string, quoteIdx: number): string {
|
|
233
|
+
// Walk backwards from the quote to find the containing div
|
|
234
|
+
// The body is typically in the main content area before the quote
|
|
235
|
+
let bodyHtml = html.slice(0, quoteIdx);
|
|
236
|
+
|
|
237
|
+
// Remove the outermost container divs and get to the content
|
|
238
|
+
// Look for the last closing tag before the quote div's opening
|
|
239
|
+
const lastDivOpen = bodyHtml.lastIndexOf('<div class="gmail_quote');
|
|
240
|
+
if (lastDivOpen === -1) {
|
|
241
|
+
// Try the simpler approach: strip from after <body> or start of content
|
|
242
|
+
const bodyTag = bodyHtml.indexOf("<body");
|
|
243
|
+
if (bodyTag > 0) {
|
|
244
|
+
const bodyEnd = bodyHtml.indexOf(">", bodyTag);
|
|
245
|
+
bodyHtml = bodyHtml.slice(bodyEnd + 1);
|
|
246
|
+
}
|
|
247
|
+
} else {
|
|
248
|
+
bodyHtml = bodyHtml.slice(0, lastDivOpen);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
return cleanBody(stripHtml(bodyHtml));
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Fallback: parse plain text thread using "> " quote markers and "On ... wrote:" patterns.
|
|
256
|
+
*/
|
|
257
|
+
function extractFromPlainText(text: string, parsed: ParsedMail): ThreadMessage[] {
|
|
258
|
+
const messages: ThreadMessage[] = [];
|
|
259
|
+
const wrotePattern = /^On\s+.+\s+wrote:\s*$/m;
|
|
260
|
+
|
|
261
|
+
const parts = text.split(wrotePattern);
|
|
262
|
+
|
|
263
|
+
if (parts.length <= 1) {
|
|
264
|
+
// No quoted thread found, single message
|
|
265
|
+
messages.push({
|
|
266
|
+
from: formatAddress(parsed.from),
|
|
267
|
+
date: parsed.date ?? null,
|
|
268
|
+
body: cleanBody(text),
|
|
269
|
+
});
|
|
270
|
+
return messages;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// First part is the newest message body
|
|
274
|
+
messages.push({
|
|
275
|
+
from: formatAddress(parsed.from),
|
|
276
|
+
date: parsed.date ?? null,
|
|
277
|
+
body: cleanBody(parts[0]),
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
// Find all "On ... wrote:" lines to get attributions
|
|
281
|
+
const attrMatches = [...text.matchAll(/^(On\s+.+\s+wrote:)\s*$/gm)];
|
|
282
|
+
for (let i = 0; i < attrMatches.length; i++) {
|
|
283
|
+
const attrText = attrMatches[i][1];
|
|
284
|
+
const { name, date } = parseAttribution(attrText);
|
|
285
|
+
|
|
286
|
+
// The body is the next part, with ">" prefixes stripped
|
|
287
|
+
if (i + 1 < parts.length) {
|
|
288
|
+
const quotedBody = parts[i + 1]
|
|
289
|
+
.split("\n")
|
|
290
|
+
.map((line) => line.replace(/^>+\s?/, ""))
|
|
291
|
+
.join("\n");
|
|
292
|
+
messages.push({ from: name, date, body: cleanBody(quotedBody) });
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
messages.reverse();
|
|
297
|
+
return messages;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Parse an attribution line like "On Fri, Apr 10, 2026 at 9:21 AM Tyler Prins <tyler@...> wrote:"
|
|
302
|
+
*/
|
|
303
|
+
function parseAttribution(text: string): { name: string; date: Date | null } {
|
|
304
|
+
const cleaned = text.replace(/\s+/g, " ").trim();
|
|
305
|
+
|
|
306
|
+
// Extract date: "On <date>, <name> ... wrote:"
|
|
307
|
+
// Pattern: On <day>, <month> <day>, <year> at <time> <AM/PM>
|
|
308
|
+
const dateMatch = cleaned.match(
|
|
309
|
+
/On\s+\w+,\s+(\w+\s+\d{1,2},\s+\d{4})\s+at\s+(\d{1,2}:\d{2})\s*(?:\u202F|\s)*(AM|PM)?/i,
|
|
310
|
+
);
|
|
311
|
+
|
|
312
|
+
let date: Date | null = null;
|
|
313
|
+
if (dateMatch) {
|
|
314
|
+
const dateStr = `${dateMatch[1]} ${dateMatch[2]} ${dateMatch[3] ?? ""}`.trim();
|
|
315
|
+
const parsed = new Date(dateStr);
|
|
316
|
+
if (!Number.isNaN(parsed.getTime())) {
|
|
317
|
+
date = parsed;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// Extract name: everything after the time and before "wrote:" or before < email >
|
|
322
|
+
const nameMatch = cleaned.match(/(?:AM|PM)\s+(.+?)(?:\s*<[^>]+>)?\s+wrote:/i);
|
|
323
|
+
let name = "Unknown";
|
|
324
|
+
if (nameMatch) {
|
|
325
|
+
name = nameMatch[1].replace(/<[^>]+>/g, "").trim();
|
|
326
|
+
} else {
|
|
327
|
+
// Try simpler pattern without AM/PM
|
|
328
|
+
const simpleMatch = cleaned.match(/at\s+[\d:]+\s+(.+?)(?:\s*<[^>]+>)?\s+wrote:/i);
|
|
329
|
+
if (simpleMatch) {
|
|
330
|
+
name = simpleMatch[1].replace(/<[^>]+>/g, "").trim();
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
return { name, date };
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Strip HTML tags, decode entities, and normalize whitespace.
|
|
339
|
+
*/
|
|
340
|
+
function stripHtml(html: string): string {
|
|
341
|
+
let text = html;
|
|
342
|
+
|
|
343
|
+
// Replace <br>, <br/>, <br /> with newlines
|
|
344
|
+
text = text.replace(/<br\s*\/?>/gi, "\n");
|
|
345
|
+
|
|
346
|
+
// Replace block-level elements with newlines
|
|
347
|
+
text = text.replace(/<\/(p|div|tr|li|h[1-6])>/gi, "\n");
|
|
348
|
+
text = text.replace(/<(p|div|tr|li|h[1-6])\b[^>]*>/gi, "\n");
|
|
349
|
+
|
|
350
|
+
// Replace <td> with tab
|
|
351
|
+
text = text.replace(/<\/td>/gi, "\t");
|
|
352
|
+
|
|
353
|
+
// Remove all remaining HTML tags
|
|
354
|
+
text = text.replace(/<[^>]+>/g, "");
|
|
355
|
+
|
|
356
|
+
// Decode HTML entities
|
|
357
|
+
text = decodeEntities(text);
|
|
358
|
+
|
|
359
|
+
// Normalize whitespace (but preserve newlines)
|
|
360
|
+
text = text
|
|
361
|
+
.split("\n")
|
|
362
|
+
.map((line) => line.replace(/[ \t]+/g, " ").trim())
|
|
363
|
+
.join("\n");
|
|
364
|
+
|
|
365
|
+
return text;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* Decode common HTML entities.
|
|
370
|
+
*/
|
|
371
|
+
function decodeEntities(text: string): string {
|
|
372
|
+
const entities: Record<string, string> = {
|
|
373
|
+
"&": "&",
|
|
374
|
+
"<": "<",
|
|
375
|
+
">": ">",
|
|
376
|
+
""": '"',
|
|
377
|
+
"'": "'",
|
|
378
|
+
"'": "'",
|
|
379
|
+
" ": " ",
|
|
380
|
+
" ": " ",
|
|
381
|
+
"–": "-",
|
|
382
|
+
"—": "-",
|
|
383
|
+
"«": "<<",
|
|
384
|
+
"»": ">>",
|
|
385
|
+
"•": "*",
|
|
386
|
+
"…": "...",
|
|
387
|
+
"©": "(c)",
|
|
388
|
+
"®": "(R)",
|
|
389
|
+
"™": "(TM)",
|
|
390
|
+
};
|
|
391
|
+
|
|
392
|
+
let result = text;
|
|
393
|
+
for (const [entity, char] of Object.entries(entities)) {
|
|
394
|
+
result = result.replaceAll(entity, char);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// Decode numeric entities (&#NNN; and &#xHHH;)
|
|
398
|
+
result = result.replace(/&#(\d+);/g, (_, code) => String.fromCharCode(Number.parseInt(code, 10)));
|
|
399
|
+
result = result.replace(/&#x([0-9a-fA-F]+);/g, (_, code) =>
|
|
400
|
+
String.fromCharCode(Number.parseInt(code, 16)),
|
|
401
|
+
);
|
|
402
|
+
|
|
403
|
+
return result;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* Clean up message body text.
|
|
408
|
+
*/
|
|
409
|
+
function cleanBody(text: string): string {
|
|
410
|
+
let body = text;
|
|
411
|
+
|
|
412
|
+
// Remove common email signatures
|
|
413
|
+
const sigPatterns = [
|
|
414
|
+
/^--\s*$/m, // standard sig delimiter
|
|
415
|
+
/^Sent from my (iPhone|iPad|Galaxy|Android|Samsung)/m,
|
|
416
|
+
/^Get Outlook for/m,
|
|
417
|
+
];
|
|
418
|
+
|
|
419
|
+
for (const pattern of sigPatterns) {
|
|
420
|
+
const match = body.match(pattern);
|
|
421
|
+
if (match?.index !== undefined) {
|
|
422
|
+
// Only strip if the signature is in the latter half of the message
|
|
423
|
+
if (match.index > body.length * 0.3) {
|
|
424
|
+
body = body.slice(0, match.index);
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// Remove stray partial HTML tags at the end of the body
|
|
430
|
+
body = body.replace(/<\w+\s*$/, "");
|
|
431
|
+
|
|
432
|
+
// Remove any remaining HTML tags that slipped through
|
|
433
|
+
body = body.replace(/<[^>]+>/g, "");
|
|
434
|
+
|
|
435
|
+
// Collapse 3+ consecutive newlines into 2
|
|
436
|
+
body = body.replace(/\n{3,}/g, "\n\n");
|
|
437
|
+
|
|
438
|
+
return body.trim();
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
/**
|
|
442
|
+
* Format an address object from mailparser into "Name <email>" or just "email".
|
|
443
|
+
*/
|
|
444
|
+
function formatAddress(addr: ParsedMail["from"]): string {
|
|
445
|
+
if (!addr?.value?.[0]) return "Unknown";
|
|
446
|
+
const a = addr.value[0];
|
|
447
|
+
if (a.name) return a.name;
|
|
448
|
+
return a.address ?? "Unknown";
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
/**
|
|
452
|
+
* Render the thread as markdown.
|
|
453
|
+
*/
|
|
454
|
+
function renderMarkdown(parsed: ParsedMail, messages: ThreadMessage[], opts: EmlOpts): string {
|
|
455
|
+
const subject = parsed.subject ?? "Untitled Thread";
|
|
456
|
+
const participants = new Map<string, boolean>();
|
|
457
|
+
for (const m of messages) {
|
|
458
|
+
participants.set(m.from, true);
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
const dates = messages.filter((m) => m.date).map((m) => m.date!);
|
|
462
|
+
const minDate =
|
|
463
|
+
dates.length > 0 ? formatDate(new Date(Math.min(...dates.map((d) => d.getTime())))) : "unknown";
|
|
464
|
+
const maxDate =
|
|
465
|
+
dates.length > 0 ? formatDate(new Date(Math.max(...dates.map((d) => d.getTime())))) : "unknown";
|
|
466
|
+
|
|
467
|
+
const lines: string[] = [];
|
|
468
|
+
lines.push(`# ${subject}`);
|
|
469
|
+
lines.push("");
|
|
470
|
+
lines.push(`**Participants:** ${[...participants.keys()].join(", ")}`);
|
|
471
|
+
lines.push(`**Date range:** ${minDate} to ${maxDate}`);
|
|
472
|
+
lines.push(`**Messages:** ${messages.length}`);
|
|
473
|
+
|
|
474
|
+
if (opts.attachments && parsed.attachments?.length) {
|
|
475
|
+
lines.push("");
|
|
476
|
+
lines.push("**Attachments:**");
|
|
477
|
+
for (const att of parsed.attachments) {
|
|
478
|
+
const size = att.size ? `(${formatSize(att.size)})` : "";
|
|
479
|
+
lines.push(`- ${att.filename ?? "unnamed"} ${size}`);
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
for (let i = 0; i < messages.length; i++) {
|
|
484
|
+
const m = messages[i];
|
|
485
|
+
const dateStr = m.date ? formatDateTime(m.date) : "unknown date";
|
|
486
|
+
|
|
487
|
+
lines.push("");
|
|
488
|
+
lines.push("---");
|
|
489
|
+
lines.push("");
|
|
490
|
+
lines.push(`## ${i + 1}. ${m.from} - ${dateStr}`);
|
|
491
|
+
lines.push("");
|
|
492
|
+
lines.push(m.body);
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
lines.push("");
|
|
496
|
+
return lines.join("\n");
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
function formatDate(d: Date): string {
|
|
500
|
+
return d.toISOString().slice(0, 10);
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
function formatDateTime(d: Date): string {
|
|
504
|
+
return d.toLocaleString("en-US", {
|
|
505
|
+
year: "numeric",
|
|
506
|
+
month: "short",
|
|
507
|
+
day: "numeric",
|
|
508
|
+
hour: "numeric",
|
|
509
|
+
minute: "2-digit",
|
|
510
|
+
hour12: true,
|
|
511
|
+
timeZone: "America/Chicago",
|
|
512
|
+
});
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
function formatSize(bytes: number): string {
|
|
516
|
+
if (bytes < 1024) return `${bytes}B`;
|
|
517
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;
|
|
518
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
|
|
519
|
+
}
|