elliot-stack 1.0.30 → 1.0.36
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 -21
- package/README.md +4 -0
- package/bin/install.cjs +981 -950
- package/hooks/repo-search-nudge.js +32 -32
- package/package.json +1 -1
- package/skills/estack-active-learning-tutor/SKILL.md +339 -339
- package/skills/estack-better-title/SKILL.md +64 -64
- package/skills/estack-better-title/scripts/rename.sh +55 -55
- package/skills/estack-chris-voss/SKILL.md +80 -80
- package/skills/estack-chris-voss/references/elliot-notes.md +120 -120
- package/skills/estack-chris-voss/references/voss-principles.md +210 -210
- package/skills/estack-customer-discovery/SKILL.md +60 -60
- package/skills/estack-flight-planner/SKILL.md +332 -332
- package/skills/estack-flight-planner/references/config_schema.md +156 -156
- package/skills/estack-flight-planner/references/flight_history_schema.md +97 -97
- package/skills/estack-flight-planner/references/shuttle_schedules.md +98 -98
- package/skills/estack-flight-planner/scripts/check_setup.sh +89 -89
- package/skills/estack-flight-planner/scripts/fetch_flights.py +99 -99
- package/skills/estack-flight-planner/scripts/filter_flights.py +265 -265
- package/skills/estack-flight-planner/scripts/pair_shuttles.py +173 -173
- package/skills/estack-github-issue-tracker/SKILL.md +322 -322
- package/skills/estack-github-issue-tracker/bin/tracker-tools.cjs +1358 -1358
- package/skills/estack-github-issue-tracker/references/gh-cli-patterns.md +124 -124
- package/skills/estack-github-issue-tracker/references/result-file-schema.md +156 -156
- package/skills/estack-github-issue-tracker/references/tracker-schema.md +96 -96
- package/skills/estack-github-issue-tracker/tracker-template.md +58 -58
- package/skills/estack-leadership-coach/SKILL.md +235 -0
- package/skills/estack-leadership-coach/adding-references.md +280 -0
- package/skills/estack-leadership-coach/frameworks/delegation/flows/post-mortem.md +120 -0
- package/skills/estack-leadership-coach/frameworks/delegation/flows/pre-delegation.md +138 -0
- package/skills/estack-leadership-coach/frameworks/delegation/phases/1-intake.md +145 -0
- package/skills/estack-leadership-coach/frameworks/delegation/phases/2-trm-assessment.md +119 -0
- package/skills/estack-leadership-coach/frameworks/delegation/phases/3-enrollment.md +132 -0
- package/skills/estack-leadership-coach/frameworks/delegation/phases/4-build-brief.md +171 -0
- package/skills/estack-leadership-coach/frameworks/delegation/phases/5-monitoring.md +134 -0
- package/skills/estack-leadership-coach/frameworks/delegation/phases/6-reverse-delegation.md +118 -0
- package/skills/estack-leadership-coach/frameworks/delegation/phases/7-diagnose.md +200 -0
- package/skills/estack-leadership-coach/references/.source-files/deci-ryan_self-determination-theory__deci-olafsen-ryan-2017-self-determination-theory-in-work-organizations.md +1881 -0
- package/skills/estack-leadership-coach/references/.source-files/deci-ryan_self-determination-theory__gagne-deci-2005-self-determination-theory-and-work-motivation.md +2058 -0
- package/skills/estack-leadership-coach/references/.source-files/deci-ryan_self-determination-theory__selfdeterminationtheory-org-theory-overview-page.md +61 -0
- package/skills/estack-leadership-coach/references/.source-files/gallup_engagement-research__gallup-3-key-insights-into-the-global-workplace-2024.md +57 -0
- package/skills/estack-leadership-coach/references/.source-files/gallup_engagement-research__gallup-managers-account-for-70-percent-of-variance-in-employee-engagement-2015.md +40 -0
- package/skills/estack-leadership-coach/references/.source-files/gallup_engagement-research__gallup-state-of-the-global-workplace-2026-global-data-summary.md +73 -0
- package/skills/estack-leadership-coach/references/.source-files/gallup_engagement-research__gallup-state-of-the-global-workplace-2026-report-landing.md +42 -0
- package/skills/estack-leadership-coach/references/.source-files/hormozi-leila_4-stages__leila-hormozi-the-art-of-delegation-blog-post.md +91 -0
- package/skills/estack-leadership-coach/references/.source-files/oncken-wass_monkeys-hbr-1974__oncken-wass-management-time-whos-got-the-monkey-hbr-classic-1974.md +969 -0
- package/skills/estack-leadership-coach/references/.source-files/sanchez_main-street-millionaire__codie-sanchez-afford-anything-podcast-ep-565-show-notes.md +89 -0
- package/skills/estack-leadership-coach/references/.source-files/sullivan_who-not-how__dan-sullivan-impact-filter-tool-and-guide-booklet.md +565 -0
- package/skills/estack-leadership-coach/references/.source-files/van-edwards_cues__vanessa-van-edwards-lewis-howes-school-of-greatness-ep-1231-show-notes.md +122 -0
- package/skills/estack-leadership-coach/references/.source-files/van-edwards_cues__vanessa-van-edwards-roger-dooley-cues-interview.md +194 -0
- package/skills/estack-leadership-coach/references/deci-ryan_self-determination-theory.md +166 -0
- package/skills/estack-leadership-coach/references/doerr_measure-what-matters.md +154 -0
- package/skills/estack-leadership-coach/references/ferriss_4hww.md +189 -0
- package/skills/estack-leadership-coach/references/gallup_engagement-research.md +105 -0
- package/skills/estack-leadership-coach/references/gerber_e-myth-revisited.md +118 -0
- package/skills/estack-leadership-coach/references/grove_high-output-management.md +95 -0
- package/skills/estack-leadership-coach/references/hormozi-alex_followthrough.md +152 -0
- package/skills/estack-leadership-coach/references/hormozi-leila_4-stages.md +146 -0
- package/skills/estack-leadership-coach/references/oncken-wass_monkeys-hbr-1974.md +128 -0
- package/skills/estack-leadership-coach/references/sanchez_main-street-millionaire.md +196 -0
- package/skills/estack-leadership-coach/references/sullivan_who-not-how.md +137 -0
- package/skills/estack-leadership-coach/references/van-edwards_cues.md +189 -0
- package/skills/estack-migrate-claude-session-history/SKILL.md +226 -0
- package/skills/estack-migrate-claude-session-history/references/path-encoding.md +55 -0
- package/skills/estack-migrate-claude-session-history/references/troubleshooting.md +96 -0
- package/skills/estack-migrate-claude-session-history/scripts/migrate-claude-history.js +1123 -0
- package/skills/estack-migrate-claude-session-history/scripts/test-append-note.js +48 -0
- package/skills/estack-migrate-claude-session-history/scripts/test-validate-migration.py +326 -0
- package/skills/estack-migrate-claude-session-history/scripts/validate-migration.py +493 -0
- package/skills/estack-pdf-to-md/SKILL.md +180 -0
- package/skills/estack-pdf-to-md/scripts/pdf_to_md.py +596 -0
- package/skills/estack-productivity-prioritization-coach/SKILL.md +124 -0
- package/skills/estack-productivity-prioritization-coach/sources/01-tony-robbins-rpm.md +39 -0
- package/skills/estack-productivity-prioritization-coach/sources/02-justin-sung-task-prioritization.md +34 -0
- package/skills/estack-prompt-builder-coach/SKILL.md +81 -81
- package/skills/estack-prompt-builder-coach/definition-of-done-generator.md +42 -42
- package/skills/estack-prompt-builder-coach/prompt-builder.md +37 -37
- package/skills/estack-prompt-builder-coach/task-shaper.md +36 -36
- package/skills/estack-prompt-builder-coach/vague-ask-auditor.md +37 -37
- package/skills/estack-read-claude-session-history/SKILL.md +204 -204
- package/skills/estack-read-claude-session-history/references/jsonl-schema.md +126 -126
- package/skills/estack-read-claude-session-history/references/modes.md +423 -423
- package/skills/estack-read-claude-session-history/references/recipes.md +271 -271
- package/skills/estack-read-claude-session-history/scripts/lib/__init__.py +1 -1
- package/skills/estack-read-claude-session-history/scripts/lib/parser.py +460 -460
- package/skills/estack-read-claude-session-history/scripts/lib/paths.py +234 -234
- package/skills/estack-read-claude-session-history/scripts/lib/search.py +179 -179
- package/skills/estack-read-claude-session-history/scripts/lib/subagents.py +88 -88
- package/skills/estack-read-claude-session-history/scripts/lib/tools.py +144 -144
- package/skills/estack-read-claude-session-history/scripts/read_transcript.py +1776 -1776
- package/skills/estack-read-claude-session-history/scripts/tests/conftest.py +40 -40
- package/skills/estack-read-claude-session-history/scripts/tests/fixtures/README.md +20 -20
- package/skills/estack-read-claude-session-history/scripts/tests/fixtures/all-noise.jsonl +4 -4
- package/skills/estack-read-claude-session-history/scripts/tests/fixtures/basic-session.jsonl +2 -2
- package/skills/estack-read-claude-session-history/scripts/tests/fixtures/engagement-gaps.jsonl +9 -9
- package/skills/estack-read-claude-session-history/scripts/tests/fixtures/engagement-noise.jsonl +7 -7
- package/skills/estack-read-claude-session-history/scripts/tests/fixtures/engagement-parallel-a.jsonl +3 -3
- package/skills/estack-read-claude-session-history/scripts/tests/fixtures/engagement-parallel-b.jsonl +3 -3
- package/skills/estack-read-claude-session-history/scripts/tests/fixtures/engagement-waiting.jsonl +5 -5
- package/skills/estack-read-claude-session-history/scripts/tests/fixtures/interrupted.jsonl +2 -2
- package/skills/estack-read-claude-session-history/scripts/tests/fixtures/multi-compact.jsonl +8 -8
- package/skills/estack-read-claude-session-history/scripts/tests/fixtures/pending-user.jsonl +2 -2
- package/skills/estack-read-claude-session-history/scripts/tests/fixtures/subagent-no-meta/subagents/agent-aaa.jsonl +2 -2
- package/skills/estack-read-claude-session-history/scripts/tests/fixtures/subagent-no-meta.jsonl +2 -2
- package/skills/estack-read-claude-session-history/scripts/tests/fixtures/subagent-parent/subagents/agent-xyz123.jsonl +2 -2
- package/skills/estack-read-claude-session-history/scripts/tests/fixtures/subagent-parent/subagents/agent-xyz123.meta.json +1 -1
- package/skills/estack-read-claude-session-history/scripts/tests/fixtures/subagent-parent.jsonl +4 -4
- package/skills/estack-read-claude-session-history/scripts/tests/fixtures/time-spread.jsonl +6 -6
- package/skills/estack-read-claude-session-history/scripts/tests/fixtures/timeline-day-test.jsonl +5 -5
- package/skills/estack-read-claude-session-history/scripts/tests/fixtures/tool-zoo.jsonl +10 -10
- package/skills/estack-read-claude-session-history/scripts/tests/fixtures/truncated.jsonl +2 -2
- package/skills/estack-read-claude-session-history/scripts/tests/fixtures/unicode.jsonl +2 -2
- package/skills/estack-read-claude-session-history/scripts/tests/fixtures/with-advisor.jsonl +3 -3
- package/skills/estack-read-claude-session-history/scripts/tests/fixtures/with-compact.jsonl +5 -5
- package/skills/estack-read-claude-session-history/scripts/tests/fixtures/with-thinking.jsonl +2 -2
- package/skills/estack-read-claude-session-history/scripts/tests/test_backup_roots.py +56 -56
- package/skills/estack-read-claude-session-history/scripts/tests/test_engagement.py +239 -239
- package/skills/estack-read-claude-session-history/scripts/tests/test_json_format.py +201 -201
- package/skills/estack-read-claude-session-history/scripts/tests/test_modes.py +199 -199
- package/skills/estack-read-claude-session-history/scripts/tests/test_parser.py +195 -195
- package/skills/estack-read-claude-session-history/scripts/tests/test_paths.py +133 -133
- package/skills/estack-read-claude-session-history/scripts/tests/test_search.py +78 -78
- package/skills/estack-read-claude-session-history/scripts/tests/test_subagents.py +43 -43
- package/skills/estack-read-claude-session-history/scripts/tests/test_timeline.py +179 -179
- package/skills/estack-read-claude-session-history/scripts/tests/test_timezone_and_project.py +212 -212
- package/skills/estack-read-claude-session-history/scripts/tests/test_tools.py +80 -80
- package/skills/estack-repo-search/SKILL.md +3 -1
|
@@ -1,179 +1,179 @@
|
|
|
1
|
-
"""Unified search engine across sessions, projects, and roots."""
|
|
2
|
-
|
|
3
|
-
from __future__ import annotations
|
|
4
|
-
|
|
5
|
-
import json
|
|
6
|
-
import sys
|
|
7
|
-
from collections import namedtuple
|
|
8
|
-
from datetime import datetime
|
|
9
|
-
from pathlib import Path
|
|
10
|
-
from typing import Iterator, Literal
|
|
11
|
-
|
|
12
|
-
from . import parser as _parser
|
|
13
|
-
from . import paths as _paths
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
Match = namedtuple(
|
|
17
|
-
"Match",
|
|
18
|
-
["session_path", "mtime", "role", "where", "timestamp", "window_text"],
|
|
19
|
-
)
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
def _block_text(block: dict, where: str) -> str:
|
|
23
|
-
"""Extract searchable text for a given 'where' channel from one block."""
|
|
24
|
-
bt = block.get("type")
|
|
25
|
-
if where == "text" and bt == "text":
|
|
26
|
-
return block.get("text", "")
|
|
27
|
-
if where == "thinking" and bt == "thinking":
|
|
28
|
-
return block.get("thinking", "") or block.get("text", "")
|
|
29
|
-
if where == "tool_use" and bt == "tool_use":
|
|
30
|
-
name = block.get("name", "")
|
|
31
|
-
try:
|
|
32
|
-
inp = json.dumps(block.get("input", {}))
|
|
33
|
-
except (TypeError, ValueError):
|
|
34
|
-
inp = str(block.get("input", ""))
|
|
35
|
-
return f"[tool:{name}] {inp}"
|
|
36
|
-
if where == "tool_result" and bt == "tool_result":
|
|
37
|
-
inner = block.get("content", "")
|
|
38
|
-
if isinstance(inner, str):
|
|
39
|
-
return inner
|
|
40
|
-
if isinstance(inner, list):
|
|
41
|
-
parts = []
|
|
42
|
-
for item in inner:
|
|
43
|
-
if isinstance(item, dict) and item.get("type") == "text":
|
|
44
|
-
parts.append(item.get("text", ""))
|
|
45
|
-
return "\n".join(parts)
|
|
46
|
-
return ""
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
def _entry_search_text(obj: dict, in_channel: str) -> list[tuple[str, str]]:
|
|
50
|
-
"""Return list of (where, text) for an entry filtered to in_channel."""
|
|
51
|
-
msg = obj.get("message", {})
|
|
52
|
-
if not isinstance(msg, dict):
|
|
53
|
-
return []
|
|
54
|
-
content = msg.get("content")
|
|
55
|
-
out: list[tuple[str, str]] = []
|
|
56
|
-
if isinstance(content, str):
|
|
57
|
-
if in_channel in ("text", "all"):
|
|
58
|
-
out.append(("text", content))
|
|
59
|
-
return out
|
|
60
|
-
if not isinstance(content, list):
|
|
61
|
-
return out
|
|
62
|
-
channels = ["text", "tool_use", "thinking", "tool_result"] if in_channel == "all" else [in_channel]
|
|
63
|
-
for block in content:
|
|
64
|
-
if not isinstance(block, dict):
|
|
65
|
-
continue
|
|
66
|
-
for ch in channels:
|
|
67
|
-
t = _block_text(block, ch)
|
|
68
|
-
if t:
|
|
69
|
-
out.append((ch, t))
|
|
70
|
-
return out
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
def _window(text: str, q: str, n: int = 200) -> str:
|
|
74
|
-
"""Return up to n chars of context around the first match of q."""
|
|
75
|
-
if not text or not q:
|
|
76
|
-
return ""
|
|
77
|
-
lower = text.lower()
|
|
78
|
-
idx = lower.find(q.lower())
|
|
79
|
-
if idx == -1:
|
|
80
|
-
return text[:n]
|
|
81
|
-
start = max(0, idx - n // 2)
|
|
82
|
-
end = min(len(text), idx + len(q) + n // 2)
|
|
83
|
-
window = text[start:end]
|
|
84
|
-
prefix = "…" if start > 0 else ""
|
|
85
|
-
suffix = "…" if end < len(text) else ""
|
|
86
|
-
return prefix + window + suffix
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
def search_session(
|
|
90
|
-
path: Path,
|
|
91
|
-
query: str,
|
|
92
|
-
role: Literal["user", "assistant", "both"] = "both",
|
|
93
|
-
in_channel: Literal["text", "tool_use", "thinking", "all"] = "text",
|
|
94
|
-
since: datetime | None = None,
|
|
95
|
-
until: datetime | None = None,
|
|
96
|
-
) -> list[Match]:
|
|
97
|
-
"""Search one transcript file. Returns ordered matches."""
|
|
98
|
-
lines = _parser.parse_lines(path)
|
|
99
|
-
try:
|
|
100
|
-
mtime = path.stat().st_mtime
|
|
101
|
-
except OSError:
|
|
102
|
-
mtime = 0
|
|
103
|
-
matches: list[Match] = []
|
|
104
|
-
q = query.lower()
|
|
105
|
-
for obj in lines:
|
|
106
|
-
cls = _parser.classify_entry(obj)
|
|
107
|
-
if cls == "noise" or cls == "title":
|
|
108
|
-
continue
|
|
109
|
-
msg_role = "user" if cls in ("user", "compact") else "assistant"
|
|
110
|
-
if role != "both" and msg_role != role:
|
|
111
|
-
continue
|
|
112
|
-
ts = obj.get("timestamp")
|
|
113
|
-
ts_dt = None
|
|
114
|
-
if since is not None or until is not None:
|
|
115
|
-
ts_dt = _parser._parse_timestamp(ts)
|
|
116
|
-
if ts_dt is None:
|
|
117
|
-
continue
|
|
118
|
-
if ts_dt.tzinfo is not None:
|
|
119
|
-
ts_dt = ts_dt.replace(tzinfo=None)
|
|
120
|
-
if since is not None and ts_dt < since:
|
|
121
|
-
continue
|
|
122
|
-
if until is not None and ts_dt > until:
|
|
123
|
-
continue
|
|
124
|
-
for where, text in _entry_search_text(obj, in_channel):
|
|
125
|
-
if q in text.lower():
|
|
126
|
-
matches.append(Match(
|
|
127
|
-
session_path=path,
|
|
128
|
-
mtime=mtime,
|
|
129
|
-
role=msg_role,
|
|
130
|
-
where=where,
|
|
131
|
-
timestamp=ts,
|
|
132
|
-
window_text=_window(text, query),
|
|
133
|
-
))
|
|
134
|
-
return matches
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
def search_project(
|
|
138
|
-
project_dir: Path,
|
|
139
|
-
query: str,
|
|
140
|
-
role: Literal["user", "assistant", "both"] = "both",
|
|
141
|
-
in_channel: Literal["text", "tool_use", "thinking", "all"] = "text",
|
|
142
|
-
since: datetime | None = None,
|
|
143
|
-
until: datetime | None = None,
|
|
144
|
-
progress: bool = True,
|
|
145
|
-
) -> Iterator[Match]:
|
|
146
|
-
"""Search every transcript in a project directory, newest first."""
|
|
147
|
-
files = _paths.list_transcripts(project_dir, since=since, until=until)
|
|
148
|
-
for i, f in enumerate(files, 1):
|
|
149
|
-
if progress:
|
|
150
|
-
print(
|
|
151
|
-
f"Searching {i}/{len(files)}: {f.name}...",
|
|
152
|
-
file=sys.stderr,
|
|
153
|
-
end="\r",
|
|
154
|
-
)
|
|
155
|
-
try:
|
|
156
|
-
for m in search_session(f, query, role, in_channel, since, until):
|
|
157
|
-
yield m
|
|
158
|
-
except Exception as e:
|
|
159
|
-
print(f"\nError reading {f.name}: {e}", file=sys.stderr)
|
|
160
|
-
if progress:
|
|
161
|
-
print(file=sys.stderr)
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
def search_all_projects(
|
|
165
|
-
root: Path,
|
|
166
|
-
query: str,
|
|
167
|
-
role: Literal["user", "assistant", "both"] = "both",
|
|
168
|
-
in_channel: Literal["text", "tool_use", "thinking", "all"] = "text",
|
|
169
|
-
since: datetime | None = None,
|
|
170
|
-
until: datetime | None = None,
|
|
171
|
-
progress: bool = True,
|
|
172
|
-
) -> Iterator[Match]:
|
|
173
|
-
"""Walk every project directory under root."""
|
|
174
|
-
for project_dir in _paths.list_projects(root):
|
|
175
|
-
if progress:
|
|
176
|
-
print(f"--- {project_dir.name} ---", file=sys.stderr)
|
|
177
|
-
yield from search_project(
|
|
178
|
-
project_dir, query, role, in_channel, since, until, progress
|
|
179
|
-
)
|
|
1
|
+
"""Unified search engine across sessions, projects, and roots."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import sys
|
|
7
|
+
from collections import namedtuple
|
|
8
|
+
from datetime import datetime
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Iterator, Literal
|
|
11
|
+
|
|
12
|
+
from . import parser as _parser
|
|
13
|
+
from . import paths as _paths
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
Match = namedtuple(
|
|
17
|
+
"Match",
|
|
18
|
+
["session_path", "mtime", "role", "where", "timestamp", "window_text"],
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _block_text(block: dict, where: str) -> str:
|
|
23
|
+
"""Extract searchable text for a given 'where' channel from one block."""
|
|
24
|
+
bt = block.get("type")
|
|
25
|
+
if where == "text" and bt == "text":
|
|
26
|
+
return block.get("text", "")
|
|
27
|
+
if where == "thinking" and bt == "thinking":
|
|
28
|
+
return block.get("thinking", "") or block.get("text", "")
|
|
29
|
+
if where == "tool_use" and bt == "tool_use":
|
|
30
|
+
name = block.get("name", "")
|
|
31
|
+
try:
|
|
32
|
+
inp = json.dumps(block.get("input", {}))
|
|
33
|
+
except (TypeError, ValueError):
|
|
34
|
+
inp = str(block.get("input", ""))
|
|
35
|
+
return f"[tool:{name}] {inp}"
|
|
36
|
+
if where == "tool_result" and bt == "tool_result":
|
|
37
|
+
inner = block.get("content", "")
|
|
38
|
+
if isinstance(inner, str):
|
|
39
|
+
return inner
|
|
40
|
+
if isinstance(inner, list):
|
|
41
|
+
parts = []
|
|
42
|
+
for item in inner:
|
|
43
|
+
if isinstance(item, dict) and item.get("type") == "text":
|
|
44
|
+
parts.append(item.get("text", ""))
|
|
45
|
+
return "\n".join(parts)
|
|
46
|
+
return ""
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _entry_search_text(obj: dict, in_channel: str) -> list[tuple[str, str]]:
|
|
50
|
+
"""Return list of (where, text) for an entry filtered to in_channel."""
|
|
51
|
+
msg = obj.get("message", {})
|
|
52
|
+
if not isinstance(msg, dict):
|
|
53
|
+
return []
|
|
54
|
+
content = msg.get("content")
|
|
55
|
+
out: list[tuple[str, str]] = []
|
|
56
|
+
if isinstance(content, str):
|
|
57
|
+
if in_channel in ("text", "all"):
|
|
58
|
+
out.append(("text", content))
|
|
59
|
+
return out
|
|
60
|
+
if not isinstance(content, list):
|
|
61
|
+
return out
|
|
62
|
+
channels = ["text", "tool_use", "thinking", "tool_result"] if in_channel == "all" else [in_channel]
|
|
63
|
+
for block in content:
|
|
64
|
+
if not isinstance(block, dict):
|
|
65
|
+
continue
|
|
66
|
+
for ch in channels:
|
|
67
|
+
t = _block_text(block, ch)
|
|
68
|
+
if t:
|
|
69
|
+
out.append((ch, t))
|
|
70
|
+
return out
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _window(text: str, q: str, n: int = 200) -> str:
|
|
74
|
+
"""Return up to n chars of context around the first match of q."""
|
|
75
|
+
if not text or not q:
|
|
76
|
+
return ""
|
|
77
|
+
lower = text.lower()
|
|
78
|
+
idx = lower.find(q.lower())
|
|
79
|
+
if idx == -1:
|
|
80
|
+
return text[:n]
|
|
81
|
+
start = max(0, idx - n // 2)
|
|
82
|
+
end = min(len(text), idx + len(q) + n // 2)
|
|
83
|
+
window = text[start:end]
|
|
84
|
+
prefix = "…" if start > 0 else ""
|
|
85
|
+
suffix = "…" if end < len(text) else ""
|
|
86
|
+
return prefix + window + suffix
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def search_session(
|
|
90
|
+
path: Path,
|
|
91
|
+
query: str,
|
|
92
|
+
role: Literal["user", "assistant", "both"] = "both",
|
|
93
|
+
in_channel: Literal["text", "tool_use", "thinking", "all"] = "text",
|
|
94
|
+
since: datetime | None = None,
|
|
95
|
+
until: datetime | None = None,
|
|
96
|
+
) -> list[Match]:
|
|
97
|
+
"""Search one transcript file. Returns ordered matches."""
|
|
98
|
+
lines = _parser.parse_lines(path)
|
|
99
|
+
try:
|
|
100
|
+
mtime = path.stat().st_mtime
|
|
101
|
+
except OSError:
|
|
102
|
+
mtime = 0
|
|
103
|
+
matches: list[Match] = []
|
|
104
|
+
q = query.lower()
|
|
105
|
+
for obj in lines:
|
|
106
|
+
cls = _parser.classify_entry(obj)
|
|
107
|
+
if cls == "noise" or cls == "title":
|
|
108
|
+
continue
|
|
109
|
+
msg_role = "user" if cls in ("user", "compact") else "assistant"
|
|
110
|
+
if role != "both" and msg_role != role:
|
|
111
|
+
continue
|
|
112
|
+
ts = obj.get("timestamp")
|
|
113
|
+
ts_dt = None
|
|
114
|
+
if since is not None or until is not None:
|
|
115
|
+
ts_dt = _parser._parse_timestamp(ts)
|
|
116
|
+
if ts_dt is None:
|
|
117
|
+
continue
|
|
118
|
+
if ts_dt.tzinfo is not None:
|
|
119
|
+
ts_dt = ts_dt.replace(tzinfo=None)
|
|
120
|
+
if since is not None and ts_dt < since:
|
|
121
|
+
continue
|
|
122
|
+
if until is not None and ts_dt > until:
|
|
123
|
+
continue
|
|
124
|
+
for where, text in _entry_search_text(obj, in_channel):
|
|
125
|
+
if q in text.lower():
|
|
126
|
+
matches.append(Match(
|
|
127
|
+
session_path=path,
|
|
128
|
+
mtime=mtime,
|
|
129
|
+
role=msg_role,
|
|
130
|
+
where=where,
|
|
131
|
+
timestamp=ts,
|
|
132
|
+
window_text=_window(text, query),
|
|
133
|
+
))
|
|
134
|
+
return matches
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def search_project(
|
|
138
|
+
project_dir: Path,
|
|
139
|
+
query: str,
|
|
140
|
+
role: Literal["user", "assistant", "both"] = "both",
|
|
141
|
+
in_channel: Literal["text", "tool_use", "thinking", "all"] = "text",
|
|
142
|
+
since: datetime | None = None,
|
|
143
|
+
until: datetime | None = None,
|
|
144
|
+
progress: bool = True,
|
|
145
|
+
) -> Iterator[Match]:
|
|
146
|
+
"""Search every transcript in a project directory, newest first."""
|
|
147
|
+
files = _paths.list_transcripts(project_dir, since=since, until=until)
|
|
148
|
+
for i, f in enumerate(files, 1):
|
|
149
|
+
if progress:
|
|
150
|
+
print(
|
|
151
|
+
f"Searching {i}/{len(files)}: {f.name}...",
|
|
152
|
+
file=sys.stderr,
|
|
153
|
+
end="\r",
|
|
154
|
+
)
|
|
155
|
+
try:
|
|
156
|
+
for m in search_session(f, query, role, in_channel, since, until):
|
|
157
|
+
yield m
|
|
158
|
+
except Exception as e:
|
|
159
|
+
print(f"\nError reading {f.name}: {e}", file=sys.stderr)
|
|
160
|
+
if progress:
|
|
161
|
+
print(file=sys.stderr)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def search_all_projects(
|
|
165
|
+
root: Path,
|
|
166
|
+
query: str,
|
|
167
|
+
role: Literal["user", "assistant", "both"] = "both",
|
|
168
|
+
in_channel: Literal["text", "tool_use", "thinking", "all"] = "text",
|
|
169
|
+
since: datetime | None = None,
|
|
170
|
+
until: datetime | None = None,
|
|
171
|
+
progress: bool = True,
|
|
172
|
+
) -> Iterator[Match]:
|
|
173
|
+
"""Walk every project directory under root."""
|
|
174
|
+
for project_dir in _paths.list_projects(root):
|
|
175
|
+
if progress:
|
|
176
|
+
print(f"--- {project_dir.name} ---", file=sys.stderr)
|
|
177
|
+
yield from search_project(
|
|
178
|
+
project_dir, query, role, in_channel, since, until, progress
|
|
179
|
+
)
|
|
@@ -1,88 +1,88 @@
|
|
|
1
|
-
"""Subagent transcript discovery and metadata loading."""
|
|
2
|
-
|
|
3
|
-
from __future__ import annotations
|
|
4
|
-
|
|
5
|
-
import json
|
|
6
|
-
from pathlib import Path
|
|
7
|
-
from typing import Optional
|
|
8
|
-
|
|
9
|
-
from . import parser as _parser
|
|
10
|
-
from . import paths as _paths
|
|
11
|
-
from . import tools as _tools
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
def load_meta(agent_path: Path) -> dict:
|
|
15
|
-
"""Read the sibling agent-<id>.meta.json sidecar.
|
|
16
|
-
|
|
17
|
-
Returns {"agentType": "unknown", "description": ""} on miss.
|
|
18
|
-
"""
|
|
19
|
-
meta_path = agent_path.with_suffix(".meta.json")
|
|
20
|
-
if not meta_path.exists():
|
|
21
|
-
# Some sidecars use .meta.json appended to full name
|
|
22
|
-
alt = agent_path.parent / f"{agent_path.stem}.meta.json"
|
|
23
|
-
if alt.exists():
|
|
24
|
-
meta_path = alt
|
|
25
|
-
else:
|
|
26
|
-
return {"agentType": "unknown", "description": ""}
|
|
27
|
-
try:
|
|
28
|
-
with open(meta_path, encoding="utf-8") as f:
|
|
29
|
-
data = json.load(f)
|
|
30
|
-
return {
|
|
31
|
-
"agentType": data.get("agentType", data.get("subagent_type", "unknown")),
|
|
32
|
-
"description": data.get("description", ""),
|
|
33
|
-
}
|
|
34
|
-
except (OSError, json.JSONDecodeError):
|
|
35
|
-
return {"agentType": "unknown", "description": ""}
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
def _last_assistant_text(path: Path) -> str:
|
|
39
|
-
"""Pull the last assistant text from a subagent transcript."""
|
|
40
|
-
lines = _parser.parse_lines(path)
|
|
41
|
-
messages = _parser.get_messages(lines)
|
|
42
|
-
for m in reversed(messages):
|
|
43
|
-
if m["role"] == "assistant" and m["texts"]:
|
|
44
|
-
return "\n".join(m["texts"])
|
|
45
|
-
return ""
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
def agent_finals(parent_session: Path) -> list[tuple[str, dict, str]]:
|
|
49
|
-
"""For each subagent of a session: (agent_id, meta, last_assistant_text)."""
|
|
50
|
-
out: list[tuple[str, dict, str]] = []
|
|
51
|
-
for sa in _paths.list_subagents(parent_session):
|
|
52
|
-
agent_id = sa.stem # e.g., "agent-xxxx"
|
|
53
|
-
meta = load_meta(sa)
|
|
54
|
-
text = _last_assistant_text(sa)
|
|
55
|
-
out.append((agent_id, meta, text))
|
|
56
|
-
return out
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
def agent_tools(agent_path: Path, tool_filter: Optional[set[str]] = None) -> list[dict]:
|
|
60
|
-
return _tools.extract_tool_calls(_parser.parse_lines(agent_path), tool_filter)
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
def agent_files(agent_path: Path) -> dict[Path, list[str]]:
|
|
64
|
-
return _tools.files_touched(_parser.parse_lines(agent_path))
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
def group_by_parent(
|
|
68
|
-
root: Path, agent_type_filter: Optional[str] = None
|
|
69
|
-
) -> dict[Path, list[tuple[Path, dict]]]:
|
|
70
|
-
"""For every parent session under root, list its subagents + meta.
|
|
71
|
-
|
|
72
|
-
Optionally filter by agentType.
|
|
73
|
-
"""
|
|
74
|
-
out: dict[Path, list[tuple[Path, dict]]] = {}
|
|
75
|
-
for project_dir in _paths.list_projects(root):
|
|
76
|
-
for parent in _paths.list_transcripts(project_dir):
|
|
77
|
-
subs = _paths.list_subagents(parent)
|
|
78
|
-
if not subs:
|
|
79
|
-
continue
|
|
80
|
-
entries = []
|
|
81
|
-
for sa in subs:
|
|
82
|
-
meta = load_meta(sa)
|
|
83
|
-
if agent_type_filter and meta["agentType"] != agent_type_filter:
|
|
84
|
-
continue
|
|
85
|
-
entries.append((sa, meta))
|
|
86
|
-
if entries:
|
|
87
|
-
out[parent] = entries
|
|
88
|
-
return out
|
|
1
|
+
"""Subagent transcript discovery and metadata loading."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Optional
|
|
8
|
+
|
|
9
|
+
from . import parser as _parser
|
|
10
|
+
from . import paths as _paths
|
|
11
|
+
from . import tools as _tools
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def load_meta(agent_path: Path) -> dict:
|
|
15
|
+
"""Read the sibling agent-<id>.meta.json sidecar.
|
|
16
|
+
|
|
17
|
+
Returns {"agentType": "unknown", "description": ""} on miss.
|
|
18
|
+
"""
|
|
19
|
+
meta_path = agent_path.with_suffix(".meta.json")
|
|
20
|
+
if not meta_path.exists():
|
|
21
|
+
# Some sidecars use .meta.json appended to full name
|
|
22
|
+
alt = agent_path.parent / f"{agent_path.stem}.meta.json"
|
|
23
|
+
if alt.exists():
|
|
24
|
+
meta_path = alt
|
|
25
|
+
else:
|
|
26
|
+
return {"agentType": "unknown", "description": ""}
|
|
27
|
+
try:
|
|
28
|
+
with open(meta_path, encoding="utf-8") as f:
|
|
29
|
+
data = json.load(f)
|
|
30
|
+
return {
|
|
31
|
+
"agentType": data.get("agentType", data.get("subagent_type", "unknown")),
|
|
32
|
+
"description": data.get("description", ""),
|
|
33
|
+
}
|
|
34
|
+
except (OSError, json.JSONDecodeError):
|
|
35
|
+
return {"agentType": "unknown", "description": ""}
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _last_assistant_text(path: Path) -> str:
|
|
39
|
+
"""Pull the last assistant text from a subagent transcript."""
|
|
40
|
+
lines = _parser.parse_lines(path)
|
|
41
|
+
messages = _parser.get_messages(lines)
|
|
42
|
+
for m in reversed(messages):
|
|
43
|
+
if m["role"] == "assistant" and m["texts"]:
|
|
44
|
+
return "\n".join(m["texts"])
|
|
45
|
+
return ""
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def agent_finals(parent_session: Path) -> list[tuple[str, dict, str]]:
|
|
49
|
+
"""For each subagent of a session: (agent_id, meta, last_assistant_text)."""
|
|
50
|
+
out: list[tuple[str, dict, str]] = []
|
|
51
|
+
for sa in _paths.list_subagents(parent_session):
|
|
52
|
+
agent_id = sa.stem # e.g., "agent-xxxx"
|
|
53
|
+
meta = load_meta(sa)
|
|
54
|
+
text = _last_assistant_text(sa)
|
|
55
|
+
out.append((agent_id, meta, text))
|
|
56
|
+
return out
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def agent_tools(agent_path: Path, tool_filter: Optional[set[str]] = None) -> list[dict]:
|
|
60
|
+
return _tools.extract_tool_calls(_parser.parse_lines(agent_path), tool_filter)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def agent_files(agent_path: Path) -> dict[Path, list[str]]:
|
|
64
|
+
return _tools.files_touched(_parser.parse_lines(agent_path))
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def group_by_parent(
|
|
68
|
+
root: Path, agent_type_filter: Optional[str] = None
|
|
69
|
+
) -> dict[Path, list[tuple[Path, dict]]]:
|
|
70
|
+
"""For every parent session under root, list its subagents + meta.
|
|
71
|
+
|
|
72
|
+
Optionally filter by agentType.
|
|
73
|
+
"""
|
|
74
|
+
out: dict[Path, list[tuple[Path, dict]]] = {}
|
|
75
|
+
for project_dir in _paths.list_projects(root):
|
|
76
|
+
for parent in _paths.list_transcripts(project_dir):
|
|
77
|
+
subs = _paths.list_subagents(parent)
|
|
78
|
+
if not subs:
|
|
79
|
+
continue
|
|
80
|
+
entries = []
|
|
81
|
+
for sa in subs:
|
|
82
|
+
meta = load_meta(sa)
|
|
83
|
+
if agent_type_filter and meta["agentType"] != agent_type_filter:
|
|
84
|
+
continue
|
|
85
|
+
entries.append((sa, meta))
|
|
86
|
+
if entries:
|
|
87
|
+
out[parent] = entries
|
|
88
|
+
return out
|