elliot-stack 1.0.33 → 1.0.37

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.
Files changed (82) hide show
  1. package/LICENSE +21 -21
  2. package/bin/install.cjs +981 -981
  3. package/hooks/repo-search-nudge.js +32 -32
  4. package/package.json +1 -1
  5. package/skills/estack-active-learning-tutor/SKILL.md +339 -339
  6. package/skills/estack-better-title/SKILL.md +64 -64
  7. package/skills/estack-better-title/scripts/rename.sh +55 -55
  8. package/skills/estack-chris-voss/SKILL.md +80 -80
  9. package/skills/estack-chris-voss/references/elliot-notes.md +120 -120
  10. package/skills/estack-chris-voss/references/voss-principles.md +210 -210
  11. package/skills/estack-customer-discovery/SKILL.md +60 -60
  12. package/skills/estack-flight-planner/SKILL.md +332 -332
  13. package/skills/estack-flight-planner/references/config_schema.md +156 -156
  14. package/skills/estack-flight-planner/references/flight_history_schema.md +97 -97
  15. package/skills/estack-flight-planner/references/shuttle_schedules.md +98 -98
  16. package/skills/estack-flight-planner/scripts/check_setup.sh +89 -89
  17. package/skills/estack-flight-planner/scripts/fetch_flights.py +99 -99
  18. package/skills/estack-flight-planner/scripts/filter_flights.py +265 -265
  19. package/skills/estack-flight-planner/scripts/pair_shuttles.py +173 -173
  20. package/skills/estack-github-issue-tracker/SKILL.md +322 -322
  21. package/skills/estack-github-issue-tracker/bin/tracker-tools.cjs +1358 -1358
  22. package/skills/estack-github-issue-tracker/references/gh-cli-patterns.md +124 -124
  23. package/skills/estack-github-issue-tracker/references/result-file-schema.md +156 -156
  24. package/skills/estack-github-issue-tracker/references/tracker-schema.md +96 -96
  25. package/skills/estack-github-issue-tracker/tracker-template.md +58 -58
  26. package/skills/estack-leadership-coach/SKILL.md +1 -1
  27. package/skills/estack-leadership-coach/adding-references.md +1 -1
  28. package/skills/estack-migrate-claude-session-history/SKILL.md +15 -2
  29. package/skills/estack-pdf-to-md/SKILL.md +1 -2
  30. package/skills/estack-prompt-builder-coach/SKILL.md +81 -81
  31. package/skills/estack-prompt-builder-coach/definition-of-done-generator.md +42 -42
  32. package/skills/estack-prompt-builder-coach/prompt-builder.md +37 -37
  33. package/skills/estack-prompt-builder-coach/task-shaper.md +36 -36
  34. package/skills/estack-prompt-builder-coach/vague-ask-auditor.md +37 -37
  35. package/skills/estack-read-claude-session-history/SKILL.md +224 -204
  36. package/skills/estack-read-claude-session-history/references/jsonl-schema.md +126 -126
  37. package/skills/estack-read-claude-session-history/references/modes.md +423 -423
  38. package/skills/estack-read-claude-session-history/references/recipes.md +271 -271
  39. package/skills/estack-read-claude-session-history/scripts/lib/__init__.py +1 -1
  40. package/skills/estack-read-claude-session-history/scripts/lib/parser.py +460 -460
  41. package/skills/estack-read-claude-session-history/scripts/lib/paths.py +234 -234
  42. package/skills/estack-read-claude-session-history/scripts/lib/search.py +179 -179
  43. package/skills/estack-read-claude-session-history/scripts/lib/subagents.py +88 -88
  44. package/skills/estack-read-claude-session-history/scripts/lib/tools.py +144 -144
  45. package/skills/estack-read-claude-session-history/scripts/read_transcript.py +1776 -1776
  46. package/skills/estack-read-claude-session-history/scripts/tests/conftest.py +40 -40
  47. package/skills/estack-read-claude-session-history/scripts/tests/fixtures/README.md +20 -20
  48. package/skills/estack-read-claude-session-history/scripts/tests/fixtures/all-noise.jsonl +4 -4
  49. package/skills/estack-read-claude-session-history/scripts/tests/fixtures/basic-session.jsonl +2 -2
  50. package/skills/estack-read-claude-session-history/scripts/tests/fixtures/engagement-gaps.jsonl +9 -9
  51. package/skills/estack-read-claude-session-history/scripts/tests/fixtures/engagement-noise.jsonl +7 -7
  52. package/skills/estack-read-claude-session-history/scripts/tests/fixtures/engagement-parallel-a.jsonl +3 -3
  53. package/skills/estack-read-claude-session-history/scripts/tests/fixtures/engagement-parallel-b.jsonl +3 -3
  54. package/skills/estack-read-claude-session-history/scripts/tests/fixtures/engagement-waiting.jsonl +5 -5
  55. package/skills/estack-read-claude-session-history/scripts/tests/fixtures/interrupted.jsonl +2 -2
  56. package/skills/estack-read-claude-session-history/scripts/tests/fixtures/multi-compact.jsonl +8 -8
  57. package/skills/estack-read-claude-session-history/scripts/tests/fixtures/pending-user.jsonl +2 -2
  58. package/skills/estack-read-claude-session-history/scripts/tests/fixtures/subagent-no-meta/subagents/agent-aaa.jsonl +2 -2
  59. package/skills/estack-read-claude-session-history/scripts/tests/fixtures/subagent-no-meta.jsonl +2 -2
  60. package/skills/estack-read-claude-session-history/scripts/tests/fixtures/subagent-parent/subagents/agent-xyz123.jsonl +2 -2
  61. package/skills/estack-read-claude-session-history/scripts/tests/fixtures/subagent-parent/subagents/agent-xyz123.meta.json +1 -1
  62. package/skills/estack-read-claude-session-history/scripts/tests/fixtures/subagent-parent.jsonl +4 -4
  63. package/skills/estack-read-claude-session-history/scripts/tests/fixtures/time-spread.jsonl +6 -6
  64. package/skills/estack-read-claude-session-history/scripts/tests/fixtures/timeline-day-test.jsonl +5 -5
  65. package/skills/estack-read-claude-session-history/scripts/tests/fixtures/tool-zoo.jsonl +10 -10
  66. package/skills/estack-read-claude-session-history/scripts/tests/fixtures/truncated.jsonl +2 -2
  67. package/skills/estack-read-claude-session-history/scripts/tests/fixtures/unicode.jsonl +2 -2
  68. package/skills/estack-read-claude-session-history/scripts/tests/fixtures/with-advisor.jsonl +3 -3
  69. package/skills/estack-read-claude-session-history/scripts/tests/fixtures/with-compact.jsonl +5 -5
  70. package/skills/estack-read-claude-session-history/scripts/tests/fixtures/with-thinking.jsonl +2 -2
  71. package/skills/estack-read-claude-session-history/scripts/tests/test_backup_roots.py +56 -56
  72. package/skills/estack-read-claude-session-history/scripts/tests/test_engagement.py +239 -239
  73. package/skills/estack-read-claude-session-history/scripts/tests/test_json_format.py +201 -201
  74. package/skills/estack-read-claude-session-history/scripts/tests/test_modes.py +199 -199
  75. package/skills/estack-read-claude-session-history/scripts/tests/test_parser.py +195 -195
  76. package/skills/estack-read-claude-session-history/scripts/tests/test_paths.py +133 -133
  77. package/skills/estack-read-claude-session-history/scripts/tests/test_search.py +78 -78
  78. package/skills/estack-read-claude-session-history/scripts/tests/test_subagents.py +43 -43
  79. package/skills/estack-read-claude-session-history/scripts/tests/test_timeline.py +179 -179
  80. package/skills/estack-read-claude-session-history/scripts/tests/test_timezone_and_project.py +212 -212
  81. package/skills/estack-read-claude-session-history/scripts/tests/test_tools.py +80 -80
  82. package/skills/estack-repo-search/SKILL.md +67 -65
@@ -1,98 +1,98 @@
1
- # Shuttle Schedules — Template & Format Reference
2
-
3
- This skill optionally pairs flights with a ground shuttle service. Pairing is **off by default** — the skill only runs it if the user's config has a `shuttle_service` block set.
4
-
5
- This file describes:
6
- 1. How to configure your shuttle service in `~/.flight-planner/config.json`
7
- 2. The JSON format the pairing script (`scripts/pair_shuttles.py`) expects
8
- 3. How the skill builds that JSON from your shuttle company's website each run
9
-
10
- ## 1. Config block
11
-
12
- Add this to `~/.flight-planner/config.json` if you regularly use a shuttle:
13
-
14
- ```json
15
- "shuttle_service": {
16
- "name": "Acme Airport Express",
17
- "schedule_urls": [
18
- "https://acmeairport.example.com/schedules/ord",
19
- "https://acmeairport.example.com/schedules/mdw"
20
- ],
21
- "costs": {
22
- "ORD": 60,
23
- "MDW": 55
24
- },
25
- "home_timezone": "America/New_York",
26
- "airport_timezones": {
27
- "ORD": "America/Chicago",
28
- "MDW": "America/Chicago"
29
- }
30
- }
31
- ```
32
-
33
- **Fields:**
34
-
35
- - `name` — display name for the company
36
- - `schedule_urls` — one or more URLs the skill will fetch with WebFetch each run. Should be public schedule pages.
37
- - `costs` — one-way USD cost per destination airport (IATA code)
38
- - `home_timezone` — IANA timezone string for the city/town the shuttle picks you up from
39
- - `airport_timezones` — IANA timezone string per destination airport
40
-
41
- Set `shuttle_service: null` (or omit the field entirely) if you don't use a shuttle.
42
-
43
- ## 2. JSON the pairing script expects
44
-
45
- After fetching schedule URLs, the skill assembles this JSON and saves it as `shuttles.json`:
46
-
47
- ```json
48
- {
49
- "shuttles": [
50
- {
51
- "company": "Acme Airport Express",
52
- "from": "Home",
53
- "to": "ORD",
54
- "pickup_location": "Downtown station",
55
- "departs_local": "06:00",
56
- "arrives_local": "08:30"
57
- },
58
- {
59
- "company": "Acme Airport Express",
60
- "from": "Home",
61
- "to": "ORD",
62
- "pickup_location": "Downtown station",
63
- "departs_local": "10:00",
64
- "arrives_local": "12:30"
65
- }
66
- ]
67
- }
68
- ```
69
-
70
- **Field rules:**
71
-
72
- - `from` — descriptive label for the pickup region (not used for matching)
73
- - `to` — destination airport IATA code (used to match flights)
74
- - `departs_local` — pickup time in **home timezone**, HH:MM 24-hour
75
- - `arrives_local` — arrival time in **destination airport's local timezone**, HH:MM 24-hour
76
- - `pickup_location` — optional, shown in output
77
-
78
- The pairing script uses `--tz-offsets` to translate `arrives_local` into the home timezone for buffer math. The skill computes those offsets from `home_timezone` and `airport_timezones` in your config.
79
-
80
- ## 3. Extracting schedules from a website
81
-
82
- Most shuttle companies post fixed weekly schedules on their site. To turn a schedule page into the JSON above:
83
-
84
- 1. Use WebFetch on each URL in `schedule_urls`. Prompt: "extract every shuttle run with pickup time, destination airport, and arrival time."
85
- 2. For each run, build one object with `from`, `to`, `departs_local`, `arrives_local`.
86
- 3. Append all objects to a single `"shuttles"` array.
87
-
88
- If the company has multiple runs to the same airport on different days, treat each as a separate entry. If schedules change on weekends, fetch the relevant day's schedule when the user's flight date is a weekend.
89
-
90
- ## 4. Cost overrides for a single run
91
-
92
- If the user wants to override shuttle costs for one run only (e.g., a friend is driving them for free), the skill passes `--shuttle-costs "ORD:0"` instead of the config value. This doesn't modify the saved config.
93
-
94
- ## 5. Tightness tuning
95
-
96
- `--min-buffer-min` (default 90) sets the floor for "viable" — flights with less buffer than this between shuttle arrival and flight departure are marked TOO_TIGHT. Increase to 120 if your airport has long security lines, decrease to 60 if you trust your shuttle and have TSA PreCheck.
97
-
98
- `--max-wait-min` (default 240) caps how much pre-flight wait time is acceptable. Pairings beyond this get a LONG_WAIT label but still appear in the output.
1
+ # Shuttle Schedules — Template & Format Reference
2
+
3
+ This skill optionally pairs flights with a ground shuttle service. Pairing is **off by default** — the skill only runs it if the user's config has a `shuttle_service` block set.
4
+
5
+ This file describes:
6
+ 1. How to configure your shuttle service in `~/.flight-planner/config.json`
7
+ 2. The JSON format the pairing script (`scripts/pair_shuttles.py`) expects
8
+ 3. How the skill builds that JSON from your shuttle company's website each run
9
+
10
+ ## 1. Config block
11
+
12
+ Add this to `~/.flight-planner/config.json` if you regularly use a shuttle:
13
+
14
+ ```json
15
+ "shuttle_service": {
16
+ "name": "Acme Airport Express",
17
+ "schedule_urls": [
18
+ "https://acmeairport.example.com/schedules/ord",
19
+ "https://acmeairport.example.com/schedules/mdw"
20
+ ],
21
+ "costs": {
22
+ "ORD": 60,
23
+ "MDW": 55
24
+ },
25
+ "home_timezone": "America/New_York",
26
+ "airport_timezones": {
27
+ "ORD": "America/Chicago",
28
+ "MDW": "America/Chicago"
29
+ }
30
+ }
31
+ ```
32
+
33
+ **Fields:**
34
+
35
+ - `name` — display name for the company
36
+ - `schedule_urls` — one or more URLs the skill will fetch with WebFetch each run. Should be public schedule pages.
37
+ - `costs` — one-way USD cost per destination airport (IATA code)
38
+ - `home_timezone` — IANA timezone string for the city/town the shuttle picks you up from
39
+ - `airport_timezones` — IANA timezone string per destination airport
40
+
41
+ Set `shuttle_service: null` (or omit the field entirely) if you don't use a shuttle.
42
+
43
+ ## 2. JSON the pairing script expects
44
+
45
+ After fetching schedule URLs, the skill assembles this JSON and saves it as `shuttles.json`:
46
+
47
+ ```json
48
+ {
49
+ "shuttles": [
50
+ {
51
+ "company": "Acme Airport Express",
52
+ "from": "Home",
53
+ "to": "ORD",
54
+ "pickup_location": "Downtown station",
55
+ "departs_local": "06:00",
56
+ "arrives_local": "08:30"
57
+ },
58
+ {
59
+ "company": "Acme Airport Express",
60
+ "from": "Home",
61
+ "to": "ORD",
62
+ "pickup_location": "Downtown station",
63
+ "departs_local": "10:00",
64
+ "arrives_local": "12:30"
65
+ }
66
+ ]
67
+ }
68
+ ```
69
+
70
+ **Field rules:**
71
+
72
+ - `from` — descriptive label for the pickup region (not used for matching)
73
+ - `to` — destination airport IATA code (used to match flights)
74
+ - `departs_local` — pickup time in **home timezone**, HH:MM 24-hour
75
+ - `arrives_local` — arrival time in **destination airport's local timezone**, HH:MM 24-hour
76
+ - `pickup_location` — optional, shown in output
77
+
78
+ The pairing script uses `--tz-offsets` to translate `arrives_local` into the home timezone for buffer math. The skill computes those offsets from `home_timezone` and `airport_timezones` in your config.
79
+
80
+ ## 3. Extracting schedules from a website
81
+
82
+ Most shuttle companies post fixed weekly schedules on their site. To turn a schedule page into the JSON above:
83
+
84
+ 1. Use WebFetch on each URL in `schedule_urls`. Prompt: "extract every shuttle run with pickup time, destination airport, and arrival time."
85
+ 2. For each run, build one object with `from`, `to`, `departs_local`, `arrives_local`.
86
+ 3. Append all objects to a single `"shuttles"` array.
87
+
88
+ If the company has multiple runs to the same airport on different days, treat each as a separate entry. If schedules change on weekends, fetch the relevant day's schedule when the user's flight date is a weekend.
89
+
90
+ ## 4. Cost overrides for a single run
91
+
92
+ If the user wants to override shuttle costs for one run only (e.g., a friend is driving them for free), the skill passes `--shuttle-costs "ORD:0"` instead of the config value. This doesn't modify the saved config.
93
+
94
+ ## 5. Tightness tuning
95
+
96
+ `--min-buffer-min` (default 90) sets the floor for "viable" — flights with less buffer than this between shuttle arrival and flight departure are marked TOO_TIGHT. Increase to 120 if your airport has long security lines, decrease to 60 if you trust your shuttle and have TSA PreCheck.
97
+
98
+ `--max-wait-min` (default 240) caps how much pre-flight wait time is acceptable. Pairings beyond this get a LONG_WAIT label but still appear in the output.
@@ -1,89 +1,89 @@
1
- #!/usr/bin/env bash
2
- # Deterministic setup check. Run at skill load time via the ```! fence in SKILL.md.
3
- # Outputs a human-readable report of:
4
- # - whether ~/.flight-planner/config.json exists and its contents (key masked)
5
- # - whether ~/.flight-planner/flight_history.json exists and its entry count
6
- # - today's date (for resolving relative dates in Phase 1)
7
- # - whether SERPAPI_KEY env var is set
8
- # No side effects. Safe to run on every skill invocation.
9
-
10
- set -u
11
-
12
- CONFIG_DIR="$HOME/.flight-planner"
13
- CONFIG_FILE="$CONFIG_DIR/config.json"
14
- HISTORY_FILE="$CONFIG_DIR/flight_history.json"
15
-
16
- echo "=== Flight Planner Setup ==="
17
- echo "Today: $(date +%Y-%m-%d) (timezone: $(date +%Z))"
18
- echo ""
19
-
20
- # --- Config ---
21
- if [ -f "$CONFIG_FILE" ]; then
22
- echo "Config: $CONFIG_FILE (exists)"
23
- echo ""
24
- echo "--- Saved preferences ---"
25
- # Mask serpapi_key value: show as "set" or "null" but never the actual key
26
- python -c "
27
- import json, sys
28
- try:
29
- with open(r'''$CONFIG_FILE''', encoding='utf-8') as f:
30
- c = json.load(f)
31
- except Exception as e:
32
- print(f' ERROR reading config: {e}')
33
- sys.exit(0)
34
-
35
- key = c.get('serpapi_key')
36
- print(' SerpAPI key: ' + ('set' if key else 'null (will use WebSearch fallback)'))
37
- print(' Budget: \$' + str(c.get('budget_usd', '?')) + ' (' + str(c.get('budget_strength', '?')) + ')')
38
- airlines = c.get('airline_preferences') or []
39
- print(' Airlines: ' + (', '.join(airlines) if airlines else 'any') + ' (' + str(c.get('airline_preference_strength', '?')) + ')')
40
- np = c.get('nonstop_preference', '?')
41
- ns = c.get('nonstop_strength', '?')
42
- print(' Nonstop: ' + str(np) + ' (' + str(ns) + ')')
43
- bands = c.get('time_priority_bands') or []
44
- print(' Time priority: ' + (', '.join(bands) if bands else 'none') + ' (' + str(c.get('time_priority_strength', '?')) + ')')
45
- print(' Home airport: ' + str(c.get('home_airport') or 'not set'))
46
- freq = c.get('frequent_destinations') or []
47
- print(' Frequent destinations: ' + (', '.join(freq) if freq else 'not set'))
48
- shuttle = c.get('shuttle_service')
49
- if shuttle:
50
- name = shuttle.get('name', '?')
51
- costs = shuttle.get('costs', {}) or {}
52
- cost_str = ', '.join(f'{k}: \${v}' for k, v in costs.items()) if costs else 'no costs configured'
53
- print(' Shuttle service: ' + str(name) + ' (' + cost_str + ')')
54
- else:
55
- print(' Shuttle service: none (pairing step will be skipped)')
56
- " 2>&1 || echo " (python not available; cannot parse config — read the file directly)"
57
- else
58
- echo "Config: $CONFIG_FILE (NOT FOUND)"
59
- echo ""
60
- echo "First-run setup is needed. The skill will walk the user through Phase 2"
61
- echo "to create this file."
62
- fi
63
-
64
- echo ""
65
-
66
- # --- Environment SERPAPI_KEY ---
67
- if [ -n "${SERPAPI_KEY:-}" ]; then
68
- echo "SERPAPI_KEY: set in environment (will be used if config key is null)"
69
- else
70
- echo "SERPAPI_KEY: not set in environment"
71
- fi
72
-
73
- echo ""
74
-
75
- # --- Flight history ---
76
- if [ -f "$HISTORY_FILE" ]; then
77
- count=$(python -c "
78
- import json
79
- try:
80
- with open(r'''$HISTORY_FILE''', encoding='utf-8') as f:
81
- data = json.load(f)
82
- print(len(data) if isinstance(data, list) else 0)
83
- except Exception:
84
- print(0)
85
- " 2>/dev/null || echo "?")
86
- echo "History: $HISTORY_FILE ($count entries)"
87
- else
88
- echo "History: $HISTORY_FILE (not created yet — first search will create it)"
89
- fi
1
+ #!/usr/bin/env bash
2
+ # Deterministic setup check. Run at skill load time via the ```! fence in SKILL.md.
3
+ # Outputs a human-readable report of:
4
+ # - whether ~/.flight-planner/config.json exists and its contents (key masked)
5
+ # - whether ~/.flight-planner/flight_history.json exists and its entry count
6
+ # - today's date (for resolving relative dates in Phase 1)
7
+ # - whether SERPAPI_KEY env var is set
8
+ # No side effects. Safe to run on every skill invocation.
9
+
10
+ set -u
11
+
12
+ CONFIG_DIR="$HOME/.flight-planner"
13
+ CONFIG_FILE="$CONFIG_DIR/config.json"
14
+ HISTORY_FILE="$CONFIG_DIR/flight_history.json"
15
+
16
+ echo "=== Flight Planner Setup ==="
17
+ echo "Today: $(date +%Y-%m-%d) (timezone: $(date +%Z))"
18
+ echo ""
19
+
20
+ # --- Config ---
21
+ if [ -f "$CONFIG_FILE" ]; then
22
+ echo "Config: $CONFIG_FILE (exists)"
23
+ echo ""
24
+ echo "--- Saved preferences ---"
25
+ # Mask serpapi_key value: show as "set" or "null" but never the actual key
26
+ python -c "
27
+ import json, sys
28
+ try:
29
+ with open(r'''$CONFIG_FILE''', encoding='utf-8') as f:
30
+ c = json.load(f)
31
+ except Exception as e:
32
+ print(f' ERROR reading config: {e}')
33
+ sys.exit(0)
34
+
35
+ key = c.get('serpapi_key')
36
+ print(' SerpAPI key: ' + ('set' if key else 'null (will use WebSearch fallback)'))
37
+ print(' Budget: \$' + str(c.get('budget_usd', '?')) + ' (' + str(c.get('budget_strength', '?')) + ')')
38
+ airlines = c.get('airline_preferences') or []
39
+ print(' Airlines: ' + (', '.join(airlines) if airlines else 'any') + ' (' + str(c.get('airline_preference_strength', '?')) + ')')
40
+ np = c.get('nonstop_preference', '?')
41
+ ns = c.get('nonstop_strength', '?')
42
+ print(' Nonstop: ' + str(np) + ' (' + str(ns) + ')')
43
+ bands = c.get('time_priority_bands') or []
44
+ print(' Time priority: ' + (', '.join(bands) if bands else 'none') + ' (' + str(c.get('time_priority_strength', '?')) + ')')
45
+ print(' Home airport: ' + str(c.get('home_airport') or 'not set'))
46
+ freq = c.get('frequent_destinations') or []
47
+ print(' Frequent destinations: ' + (', '.join(freq) if freq else 'not set'))
48
+ shuttle = c.get('shuttle_service')
49
+ if shuttle:
50
+ name = shuttle.get('name', '?')
51
+ costs = shuttle.get('costs', {}) or {}
52
+ cost_str = ', '.join(f'{k}: \${v}' for k, v in costs.items()) if costs else 'no costs configured'
53
+ print(' Shuttle service: ' + str(name) + ' (' + cost_str + ')')
54
+ else:
55
+ print(' Shuttle service: none (pairing step will be skipped)')
56
+ " 2>&1 || echo " (python not available; cannot parse config — read the file directly)"
57
+ else
58
+ echo "Config: $CONFIG_FILE (NOT FOUND)"
59
+ echo ""
60
+ echo "First-run setup is needed. The skill will walk the user through Phase 2"
61
+ echo "to create this file."
62
+ fi
63
+
64
+ echo ""
65
+
66
+ # --- Environment SERPAPI_KEY ---
67
+ if [ -n "${SERPAPI_KEY:-}" ]; then
68
+ echo "SERPAPI_KEY: set in environment (will be used if config key is null)"
69
+ else
70
+ echo "SERPAPI_KEY: not set in environment"
71
+ fi
72
+
73
+ echo ""
74
+
75
+ # --- Flight history ---
76
+ if [ -f "$HISTORY_FILE" ]; then
77
+ count=$(python -c "
78
+ import json
79
+ try:
80
+ with open(r'''$HISTORY_FILE''', encoding='utf-8') as f:
81
+ data = json.load(f)
82
+ print(len(data) if isinstance(data, list) else 0)
83
+ except Exception:
84
+ print(0)
85
+ " 2>/dev/null || echo "?")
86
+ echo "History: $HISTORY_FILE ($count entries)"
87
+ else
88
+ echo "History: $HISTORY_FILE (not created yet — first search will create it)"
89
+ fi
@@ -1,99 +1,99 @@
1
- """
2
- Fetch flight data from SerpAPI Google Flights.
3
-
4
- Saves raw JSON responses to a platform-aware temp directory, one file per
5
- route x date combination. Prints the temp directory path on stdout so the
6
- caller can pipe it to filter_flights.py.
7
-
8
- Usage:
9
- python fetch_flights.py --dates 2026-05-09,2026-05-10 --routes IND-EWR,ORD-LGA
10
- python fetch_flights.py --dates 2026-05-09 --routes IND-EWR --airlines UA,DL --stops 1
11
- python fetch_flights.py --dates 2026-05-09 --routes IND-EWR --output-dir /tmp/x
12
-
13
- Args:
14
- --airlines Optional comma-separated IATA airline codes (e.g. UA,DL). Omit for any airline.
15
- --stops Optional. 0=any (default), 1=nonstop only, 2=one-stop-max.
16
-
17
- Auth:
18
- Reads SERPAPI_KEY from environment. Override with --api-key.
19
- """
20
- import argparse
21
- import os
22
- import sys
23
- import tempfile
24
- import urllib.parse
25
- import urllib.request
26
- from pathlib import Path
27
-
28
- ENDPOINT = "https://serpapi.com/search"
29
- BASE_PARAMS = {
30
- "engine": "google_flights",
31
- "type": "2", # one-way
32
- "sort_by": "2", # price
33
- "currency": "USD",
34
- }
35
-
36
-
37
- def default_output_dir() -> Path:
38
- base = Path(tempfile.gettempdir()) / "estack-flight-planner"
39
- base.mkdir(parents=True, exist_ok=True)
40
- return base
41
-
42
-
43
- def fetch_one(api_key: str, dep: str, arr: str, date: str, out_path: Path,
44
- airlines: str = None, stops: str = None) -> int:
45
- params = {**BASE_PARAMS, "departure_id": dep, "arrival_id": arr,
46
- "outbound_date": date, "api_key": api_key}
47
- if airlines:
48
- params["include_airlines"] = airlines
49
- if stops and stops != "0":
50
- params["stops"] = stops
51
- url = f"{ENDPOINT}?{urllib.parse.urlencode(params)}"
52
- with urllib.request.urlopen(url, timeout=60) as resp:
53
- data = resp.read()
54
- out_path.write_bytes(data)
55
- return len(data)
56
-
57
-
58
- def main() -> int:
59
- p = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
60
- p.add_argument("--dates", required=True, help="Comma-separated YYYY-MM-DD")
61
- p.add_argument("--routes", required=True, help="Comma-separated DEP-ARR (e.g. IND-EWR,ORD-LGA)")
62
- p.add_argument("--airlines", default=None,
63
- help="Optional comma-separated IATA codes (e.g. UA,DL). Omit for any airline.")
64
- p.add_argument("--stops", default="0",
65
- help="0=any (default), 1=nonstop only, 2=one-stop-max")
66
- p.add_argument("--output-dir", default=None, help="Override temp dir")
67
- p.add_argument("--api-key", default=None, help="SerpAPI key (else uses $SERPAPI_KEY)")
68
- args = p.parse_args()
69
-
70
- api_key = args.api_key or os.environ.get("SERPAPI_KEY")
71
- if not api_key:
72
- print("ERROR: provide --api-key or set SERPAPI_KEY env var", file=sys.stderr)
73
- return 2
74
-
75
- out_dir = Path(args.output_dir) if args.output_dir else default_output_dir()
76
- out_dir.mkdir(parents=True, exist_ok=True)
77
-
78
- dates = [d.strip() for d in args.dates.split(",") if d.strip()]
79
- routes = [tuple(r.strip().split("-")) for r in args.routes.split(",") if r.strip()]
80
-
81
- total = 0
82
- for dep, arr in routes:
83
- for date in dates:
84
- fname = out_dir / f"{dep}_{arr}_{date}.json"
85
- try:
86
- size = fetch_one(api_key, dep, arr, date, fname,
87
- airlines=args.airlines, stops=args.stops)
88
- print(f" {dep}->{arr} {date}: {size} bytes", file=sys.stderr)
89
- total += 1
90
- except Exception as e:
91
- print(f" {dep}->{arr} {date}: FAILED ({e})", file=sys.stderr)
92
-
93
- print(f"Fetched {total} files to:", file=sys.stderr)
94
- print(out_dir) # stdout = the path, for piping
95
- return 0
96
-
97
-
98
- if __name__ == "__main__":
99
- sys.exit(main())
1
+ """
2
+ Fetch flight data from SerpAPI Google Flights.
3
+
4
+ Saves raw JSON responses to a platform-aware temp directory, one file per
5
+ route x date combination. Prints the temp directory path on stdout so the
6
+ caller can pipe it to filter_flights.py.
7
+
8
+ Usage:
9
+ python fetch_flights.py --dates 2026-05-09,2026-05-10 --routes IND-EWR,ORD-LGA
10
+ python fetch_flights.py --dates 2026-05-09 --routes IND-EWR --airlines UA,DL --stops 1
11
+ python fetch_flights.py --dates 2026-05-09 --routes IND-EWR --output-dir /tmp/x
12
+
13
+ Args:
14
+ --airlines Optional comma-separated IATA airline codes (e.g. UA,DL). Omit for any airline.
15
+ --stops Optional. 0=any (default), 1=nonstop only, 2=one-stop-max.
16
+
17
+ Auth:
18
+ Reads SERPAPI_KEY from environment. Override with --api-key.
19
+ """
20
+ import argparse
21
+ import os
22
+ import sys
23
+ import tempfile
24
+ import urllib.parse
25
+ import urllib.request
26
+ from pathlib import Path
27
+
28
+ ENDPOINT = "https://serpapi.com/search"
29
+ BASE_PARAMS = {
30
+ "engine": "google_flights",
31
+ "type": "2", # one-way
32
+ "sort_by": "2", # price
33
+ "currency": "USD",
34
+ }
35
+
36
+
37
+ def default_output_dir() -> Path:
38
+ base = Path(tempfile.gettempdir()) / "estack-flight-planner"
39
+ base.mkdir(parents=True, exist_ok=True)
40
+ return base
41
+
42
+
43
+ def fetch_one(api_key: str, dep: str, arr: str, date: str, out_path: Path,
44
+ airlines: str = None, stops: str = None) -> int:
45
+ params = {**BASE_PARAMS, "departure_id": dep, "arrival_id": arr,
46
+ "outbound_date": date, "api_key": api_key}
47
+ if airlines:
48
+ params["include_airlines"] = airlines
49
+ if stops and stops != "0":
50
+ params["stops"] = stops
51
+ url = f"{ENDPOINT}?{urllib.parse.urlencode(params)}"
52
+ with urllib.request.urlopen(url, timeout=60) as resp:
53
+ data = resp.read()
54
+ out_path.write_bytes(data)
55
+ return len(data)
56
+
57
+
58
+ def main() -> int:
59
+ p = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
60
+ p.add_argument("--dates", required=True, help="Comma-separated YYYY-MM-DD")
61
+ p.add_argument("--routes", required=True, help="Comma-separated DEP-ARR (e.g. IND-EWR,ORD-LGA)")
62
+ p.add_argument("--airlines", default=None,
63
+ help="Optional comma-separated IATA codes (e.g. UA,DL). Omit for any airline.")
64
+ p.add_argument("--stops", default="0",
65
+ help="0=any (default), 1=nonstop only, 2=one-stop-max")
66
+ p.add_argument("--output-dir", default=None, help="Override temp dir")
67
+ p.add_argument("--api-key", default=None, help="SerpAPI key (else uses $SERPAPI_KEY)")
68
+ args = p.parse_args()
69
+
70
+ api_key = args.api_key or os.environ.get("SERPAPI_KEY")
71
+ if not api_key:
72
+ print("ERROR: provide --api-key or set SERPAPI_KEY env var", file=sys.stderr)
73
+ return 2
74
+
75
+ out_dir = Path(args.output_dir) if args.output_dir else default_output_dir()
76
+ out_dir.mkdir(parents=True, exist_ok=True)
77
+
78
+ dates = [d.strip() for d in args.dates.split(",") if d.strip()]
79
+ routes = [tuple(r.strip().split("-")) for r in args.routes.split(",") if r.strip()]
80
+
81
+ total = 0
82
+ for dep, arr in routes:
83
+ for date in dates:
84
+ fname = out_dir / f"{dep}_{arr}_{date}.json"
85
+ try:
86
+ size = fetch_one(api_key, dep, arr, date, fname,
87
+ airlines=args.airlines, stops=args.stops)
88
+ print(f" {dep}->{arr} {date}: {size} bytes", file=sys.stderr)
89
+ total += 1
90
+ except Exception as e:
91
+ print(f" {dep}->{arr} {date}: FAILED ({e})", file=sys.stderr)
92
+
93
+ print(f"Fetched {total} files to:", file=sys.stderr)
94
+ print(out_dir) # stdout = the path, for piping
95
+ return 0
96
+
97
+
98
+ if __name__ == "__main__":
99
+ sys.exit(main())