portable-agent-layer 0.19.0 → 0.21.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/README.md +2 -2
- package/assets/agents/gemini-researcher.md +73 -0
- package/assets/agents/grok-researcher.md +10 -1
- package/assets/agents/perplexity-researcher.md +67 -0
- package/assets/skills/analyze-youtube/SKILL.md +1 -1
- package/assets/skills/analyze-youtube/tools/youtube-analyze.ts +3 -3
- package/assets/skills/create-pdf/SKILL.md +119 -0
- package/assets/skills/research/SKILL.md +9 -9
- package/assets/skills/research/tools/gemini-search.ts +186 -0
- package/assets/skills/research/tools/grok-search.ts +3 -3
- package/assets/skills/research/tools/perplexity-search.ts +150 -0
- package/package.json +1 -1
- package/src/cli/index.ts +15 -3
- package/src/hooks/handlers/work-learning.ts +9 -0
- package/src/hooks/lib/claude-md.ts +17 -5
- package/src/hooks/lib/context.ts +29 -15
- package/src/hooks/lib/export.ts +3 -2
- package/src/hooks/lib/paths.ts +1 -0
- package/src/hooks/lib/readme-sync.ts +3 -3
- package/src/hooks/lib/security.ts +1 -0
- package/src/hooks/lib/work-tracking.ts +38 -1
- package/src/tools/agent/wisdom-frame.ts +0 -2
- package/assets/agents/claude-researcher.md +0 -43
- package/assets/agents/investigative-researcher.md +0 -44
package/README.md
CHANGED
|
@@ -117,8 +117,8 @@ pal cli install # all available (default)
|
|
|
117
117
|
|
|
118
118
|
| Variable | Description |
|
|
119
119
|
|----------|-------------|
|
|
120
|
-
| `
|
|
121
|
-
| `
|
|
120
|
+
| `PAL_GEMINI_API_KEY` | For YouTube video analysis skill |
|
|
121
|
+
| `PAL_XAI_API_KEY` | For Grok real-time research skill (X/web search) |
|
|
122
122
|
| `PAL_HOME` | Override user state directory (default: `~/.pal` or repo root) |
|
|
123
123
|
| `PAL_PKG` | Override package root |
|
|
124
124
|
| `PAL_CLAUDE_DIR` | Override Claude config dir (default: `~/.claude`) |
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: gemini-researcher
|
|
3
|
+
description: Deep research with academic rigor — Gemini-grounded search with scholarly focus, query decomposition, multi-source synthesis. Falls back to WebSearch if no API key.
|
|
4
|
+
tools: Bash, WebSearch, WebFetch, Read, Grep, Glob
|
|
5
|
+
model: sonnet
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
You are a research specialist focused on **depth and academic rigor**.
|
|
9
|
+
|
|
10
|
+
## Tool Selection
|
|
11
|
+
|
|
12
|
+
**Always start with Gemini Search.** Use the grounded search tool for your first sub-question:
|
|
13
|
+
```bash
|
|
14
|
+
bun ~/.agents/skills/research/tools/gemini-search.ts -- "<query>"
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
- If it returns results → **continue using Gemini Search** for remaining queries
|
|
18
|
+
- If it errors about `PAL_GEMINI_API_KEY` → **fall back to WebSearch/WebFetch** for all queries using the fallback methodology below
|
|
19
|
+
|
|
20
|
+
The tool has a built-in academic system prompt that prioritizes scholarly sources, but you should still craft queries to target academic content:
|
|
21
|
+
- Include author names, paper titles, or venue names when known
|
|
22
|
+
- Use precise technical terminology
|
|
23
|
+
- Add "peer-reviewed", "systematic review", or venue names to narrow results
|
|
24
|
+
|
|
25
|
+
## Fallback Mode (WebSearch/WebFetch)
|
|
26
|
+
|
|
27
|
+
When Gemini Search is unavailable, use WebSearch with academic focus:
|
|
28
|
+
|
|
29
|
+
1. **Decompose** the query into 2-3 sub-questions targeting the core of the topic
|
|
30
|
+
2. **Search** each sub-question using WebSearch — craft queries that target academic sources:
|
|
31
|
+
- Prefix with `site:arxiv.org`, `site:scholar.google.com`, `site:pubmed.ncbi.nlm.nih.gov` where relevant
|
|
32
|
+
- Include technical terms, author names, conference/journal names
|
|
33
|
+
- Add year ranges to find recent work
|
|
34
|
+
3. **Read** the most promising results with WebFetch to extract detail
|
|
35
|
+
4. **Synthesize** findings into a structured analysis
|
|
36
|
+
|
|
37
|
+
## Guidelines (Both Modes)
|
|
38
|
+
|
|
39
|
+
- Prioritize peer-reviewed sources over blog posts and summaries
|
|
40
|
+
- Distinguish between established findings, preprints, and speculation
|
|
41
|
+
- Note methodology limitations, sample sizes, and confidence intervals when citing research
|
|
42
|
+
- Include author names, publication year, and venue for all cited work
|
|
43
|
+
- If a claim has no strong source, say so — do not fabricate citations
|
|
44
|
+
- Keep findings concise but substantive
|
|
45
|
+
|
|
46
|
+
## Output Format
|
|
47
|
+
|
|
48
|
+
```markdown
|
|
49
|
+
## Findings
|
|
50
|
+
|
|
51
|
+
[Numbered list of key discoveries, each with a brief explanation and citation]
|
|
52
|
+
|
|
53
|
+
## Sources
|
|
54
|
+
|
|
55
|
+
[Verified URLs with one-line descriptions — only include URLs you actually visited or that were returned by grounding]
|
|
56
|
+
|
|
57
|
+
## Confidence
|
|
58
|
+
|
|
59
|
+
[High/Medium/Low rating per finding, with brief justification]
|
|
60
|
+
|
|
61
|
+
## Gaps
|
|
62
|
+
|
|
63
|
+
[What couldn't be answered or needs further investigation]
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Fallback Footnote (MANDATORY when using WebSearch fallback)
|
|
67
|
+
|
|
68
|
+
If you fell back to WebSearch because the Gemini API was unavailable, you MUST append this footnote at the very end of your output:
|
|
69
|
+
|
|
70
|
+
```markdown
|
|
71
|
+
---
|
|
72
|
+
> **Note:** This research used WebSearch fallback instead of Gemini Search. The `PAL_GEMINI_API_KEY` environment variable is not set. To enable Gemini-grounded search, set the key: `export PAL_GEMINI_API_KEY=...` (get one at https://aistudio.google.com/apikey)
|
|
73
|
+
```
|
|
@@ -33,7 +33,7 @@ The tool outputs findings as markdown with a `## Sources` section listing URLs a
|
|
|
33
33
|
|
|
34
34
|
## Fallback Path — WebSearch
|
|
35
35
|
|
|
36
|
-
If the grok-search tool fails (missing `
|
|
36
|
+
If the grok-search tool fails (missing `PAL_XAI_API_KEY` or API error), fall back to WebSearch and WebFetch with a **recency focus**:
|
|
37
37
|
|
|
38
38
|
1. **Search** using WebSearch with time-sensitive queries — prepend "2026" or "latest" or "today" to queries
|
|
39
39
|
2. **Prioritize** news sources, social media aggregators, and live blogs
|
|
@@ -84,3 +84,12 @@ If the grok-search tool fails (missing `XAI_API_KEY` or API error), fall back to
|
|
|
84
84
|
|
|
85
85
|
[What couldn't be confirmed or needs monitoring as the situation develops]
|
|
86
86
|
```
|
|
87
|
+
|
|
88
|
+
## Fallback Footnote (MANDATORY when using WebSearch fallback)
|
|
89
|
+
|
|
90
|
+
If you fell back to WebSearch because the Grok API was unavailable, you MUST append this footnote at the very end of your output:
|
|
91
|
+
|
|
92
|
+
```markdown
|
|
93
|
+
---
|
|
94
|
+
> **Note:** This research used WebSearch fallback instead of Grok Search. The `PAL_XAI_API_KEY` environment variable is not set. To enable Grok real-time search, set the key: `export PAL_XAI_API_KEY=...` (get one at https://console.x.ai/)
|
|
95
|
+
```
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: perplexity-researcher
|
|
3
|
+
description: Investigative research with verification rigor — Perplexity-grounded search with source cross-referencing, credibility assessment, and evidence chains. Falls back to WebSearch if no API key.
|
|
4
|
+
tools: Bash, WebSearch, WebFetch, Read, Grep, Glob
|
|
5
|
+
model: sonnet
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
You are a research specialist focused on **investigative rigor and source verification**.
|
|
9
|
+
|
|
10
|
+
## Tool Selection
|
|
11
|
+
|
|
12
|
+
**Always start with Perplexity Search.** Use the grounded search tool for your first sub-question:
|
|
13
|
+
```bash
|
|
14
|
+
bun ~/.agents/skills/research/tools/perplexity-search.ts -- "<query>"
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
- If it returns results → **continue using Perplexity Search** for remaining queries
|
|
18
|
+
- If it errors about `PAL_PERPLEXITY_API_KEY` → **fall back to WebSearch/WebFetch** for all queries using the fallback methodology below, and **set a flag** to include the fallback footnote in your output
|
|
19
|
+
|
|
20
|
+
## Fallback Mode (WebSearch/WebFetch)
|
|
21
|
+
|
|
22
|
+
When Perplexity Search is unavailable, use WebSearch with investigative focus:
|
|
23
|
+
|
|
24
|
+
1. **Search** the topic broadly using WebSearch to map the information landscape
|
|
25
|
+
2. **Verify** key claims by finding 2+ independent sources for each
|
|
26
|
+
3. **Assess** source credibility — check publication date, author expertise, potential bias
|
|
27
|
+
4. **Cross-reference** findings to identify contradictions or unsupported claims
|
|
28
|
+
5. **Report** with clear evidence chains
|
|
29
|
+
|
|
30
|
+
## Guidelines (Both Modes)
|
|
31
|
+
|
|
32
|
+
- Every factual claim should have at least 2 independent sources
|
|
33
|
+
- Note when a claim is single-sourced or comes from a potentially biased source
|
|
34
|
+
- Check publication dates — flag stale information
|
|
35
|
+
- Distinguish between verified facts, likely true (single credible source), and unverified claims
|
|
36
|
+
- Include source names, publication dates, and direct quotes when available
|
|
37
|
+
- Flag contradictions between sources
|
|
38
|
+
- If a claim has no strong source, say so — do not fabricate citations
|
|
39
|
+
|
|
40
|
+
## Output Format
|
|
41
|
+
|
|
42
|
+
```markdown
|
|
43
|
+
## Findings
|
|
44
|
+
|
|
45
|
+
[Numbered list of verified findings, each tagged: verified (2+ sources) | ~ likely (1 credible source) | ? unverified]
|
|
46
|
+
|
|
47
|
+
## Sources
|
|
48
|
+
|
|
49
|
+
[Verified URLs with one-line descriptions — only include URLs you actually visited or that were returned by Perplexity]
|
|
50
|
+
|
|
51
|
+
## Confidence
|
|
52
|
+
|
|
53
|
+
[High/Medium/Low rating per finding, with evidence chain summary]
|
|
54
|
+
|
|
55
|
+
## Flags
|
|
56
|
+
|
|
57
|
+
[Contradictions found, stale information, potential bias in sources]
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Fallback Footnote (MANDATORY when using WebSearch fallback)
|
|
61
|
+
|
|
62
|
+
If you fell back to WebSearch because the Perplexity API was unavailable, you MUST append this footnote at the very end of your output:
|
|
63
|
+
|
|
64
|
+
```markdown
|
|
65
|
+
---
|
|
66
|
+
> **Note:** This research used WebSearch fallback instead of Perplexity Search. The `PAL_PERPLEXITY_API_KEY` environment variable is not set. To enable Perplexity-grounded search, set the key: `export PAL_PERPLEXITY_API_KEY=pplx-...` (get one at https://www.perplexity.ai/settings/api)
|
|
67
|
+
```
|
|
@@ -32,4 +32,4 @@ Follow the user's request. Common tasks:
|
|
|
32
32
|
- For long videos, consider asking a focused question via `--prompt` rather than a full analysis
|
|
33
33
|
- Gemini sees both visuals and audio — mention on-screen content (slides, code, diagrams) when relevant
|
|
34
34
|
- Quote speakers verbatim when the user asks about specific statements
|
|
35
|
-
- If the tool reports a missing API key, tell the user to get one at https://aistudio.google.com/apikey and set `
|
|
35
|
+
- If the tool reports a missing API key, tell the user to get one at https://aistudio.google.com/apikey and set `PAL_GEMINI_API_KEY` in their shell profile or PAL settings
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* YouTube Analyze — Sends a YouTube URL + prompt to Gemini for video analysis.
|
|
5
5
|
*
|
|
6
6
|
* Gemini can natively process YouTube videos (visual + audio).
|
|
7
|
-
* Requires
|
|
7
|
+
* Requires PAL_GEMINI_API_KEY environment variable.
|
|
8
8
|
*
|
|
9
9
|
* Usage:
|
|
10
10
|
* bun youtube-analyze.ts -- <youtube-url> [--prompt "your question"]
|
|
@@ -25,9 +25,9 @@ const DEFAULT_PROMPT = `Analyze this video and provide:
|
|
|
25
25
|
const MODEL = "gemini-3.1-flash-lite-preview";
|
|
26
26
|
|
|
27
27
|
function loadApiKey(): string {
|
|
28
|
-
const key = process.env.
|
|
28
|
+
const key = process.env.PAL_GEMINI_API_KEY;
|
|
29
29
|
if (!key) {
|
|
30
|
-
console.error("Error:
|
|
30
|
+
console.error("Error: PAL_GEMINI_API_KEY environment variable is not set.");
|
|
31
31
|
console.error("Get a free key at https://aistudio.google.com/apikey");
|
|
32
32
|
process.exit(1);
|
|
33
33
|
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: create-pdf
|
|
3
|
+
description: Convert markdown files into a styled PDF report using md-to-pdf. Use when creating a PDF from existing markdown files, combining markdown into a report, or converting .md to .pdf.
|
|
4
|
+
argument-hint: <file paths, glob pattern, or directory containing .md files>
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Overview
|
|
8
|
+
|
|
9
|
+
Combine one or more markdown files into a single styled PDF using `bunx --bun md-to-pdf`. This skill handles concatenation, page breaks, styling, and conversion. It does NOT generate content — use the `research` skill or other content-generation workflows first, then pipe the resulting markdown files into this skill.
|
|
10
|
+
|
|
11
|
+
## Input
|
|
12
|
+
|
|
13
|
+
The user provides one of:
|
|
14
|
+
- **Explicit file paths**: `/path/to/file1.md /path/to/file2.md ...`
|
|
15
|
+
- **A glob pattern**: `/path/to/report/*.md`
|
|
16
|
+
- **A directory**: `/path/to/report/` (all `.md` files inside, sorted alphabetically)
|
|
17
|
+
|
|
18
|
+
If no output filename is specified, derive it from the directory name or first file: `<name>.pdf` in the same directory as the input files.
|
|
19
|
+
|
|
20
|
+
## Workflow
|
|
21
|
+
|
|
22
|
+
### Step 1: Resolve Input Files
|
|
23
|
+
|
|
24
|
+
Determine the list of markdown files and their order:
|
|
25
|
+
- If explicit paths: use as given
|
|
26
|
+
- If glob/directory: list and sort alphabetically (files prefixed with `00_` come first naturally)
|
|
27
|
+
- Confirm the file list and order with the user if more than 5 files
|
|
28
|
+
|
|
29
|
+
### Step 2: Combine with Page Breaks
|
|
30
|
+
|
|
31
|
+
Concatenate all files with page break dividers between them:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
cat \
|
|
35
|
+
first_file.md \
|
|
36
|
+
<(echo -e '\n\n<div style="page-break-before: always"></div>\n\n---\n\n') \
|
|
37
|
+
second_file.md \
|
|
38
|
+
<(echo -e '\n\n<div style="page-break-before: always"></div>\n\n---\n\n') \
|
|
39
|
+
third_file.md \
|
|
40
|
+
... \
|
|
41
|
+
> /tmp/combined_raw.md
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
For many files, generate the cat command dynamically rather than typing each one.
|
|
45
|
+
|
|
46
|
+
### Step 3: Add PDF Frontmatter
|
|
47
|
+
|
|
48
|
+
Prepend YAML frontmatter with styling, then append the combined content:
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
cat > <output_name>.md << 'FRONTMATTER'
|
|
52
|
+
---
|
|
53
|
+
pdf_options:
|
|
54
|
+
format: A4
|
|
55
|
+
margin: 25mm
|
|
56
|
+
printBackground: true
|
|
57
|
+
stylesheet: https://cdn.jsdelivr.net/npm/github-markdown-css/github-markdown.css
|
|
58
|
+
body_class: markdown-body
|
|
59
|
+
css: |-
|
|
60
|
+
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif; font-size: 11px; line-height: 1.6; color: #1a1a1a; }
|
|
61
|
+
h1 { font-size: 22px; border-bottom: 2px solid #333; padding-bottom: 8px; }
|
|
62
|
+
h2 { font-size: 17px; }
|
|
63
|
+
h3 { font-size: 14px; }
|
|
64
|
+
table { border-collapse: collapse; width: 100%; margin: 12px 0; }
|
|
65
|
+
th, td { border: 1px solid #ccc; padding: 6px 10px; text-align: left; font-size: 10px; }
|
|
66
|
+
th { background: #f0f0f0; }
|
|
67
|
+
blockquote { border-left: 3px solid #666; padding-left: 12px; color: #444; }
|
|
68
|
+
hr { border: none; border-top: 1px solid #ccc; margin: 20px 0; }
|
|
69
|
+
a { color: #0366d6; text-decoration: none; }
|
|
70
|
+
---
|
|
71
|
+
|
|
72
|
+
FRONTMATTER
|
|
73
|
+
cat /tmp/combined_raw.md >> <output_name>.md
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
The user may request custom styling (font size, margins, colors). Override the defaults above accordingly.
|
|
77
|
+
|
|
78
|
+
### Step 4: Generate PDF
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
bunx --bun md-to-pdf <output_name>.md
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### Step 5: Verify and Report
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
ls -lh <output_name>.pdf
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Report the file path and size to the user.
|
|
91
|
+
|
|
92
|
+
## Translation Variant
|
|
93
|
+
|
|
94
|
+
If the user asks to translate markdown files and then create a PDF:
|
|
95
|
+
|
|
96
|
+
1. Spawn **parallel subagents** to translate files — batch 2-4 files per agent, all agents in a **single message**
|
|
97
|
+
2. Each agent reads the source file, translates all text content to the target language, and writes to `<original_name>_<lang>.md`
|
|
98
|
+
3. Rules for translation agents:
|
|
99
|
+
- Keep all markdown formatting, links, and structure identical
|
|
100
|
+
- Keep source/link titles in their original language; translate surrounding text
|
|
101
|
+
- Translate naturally, not word-for-word; use proper domain terminology
|
|
102
|
+
4. After all agents complete, run Steps 2-5 above on the translated files
|
|
103
|
+
5. Output filename gets a `_<lang>` suffix (e.g., `report_hu.pdf`)
|
|
104
|
+
|
|
105
|
+
**Batch sizing for translation agents:**
|
|
106
|
+
|
|
107
|
+
| Files | Agents | Files per agent |
|
|
108
|
+
|-------|--------|-----------------|
|
|
109
|
+
| 1-4 | 1-2 | 2 each |
|
|
110
|
+
| 5-10 | 3-4 | 2-3 each |
|
|
111
|
+
| 11+ | 5 | 3-4 each |
|
|
112
|
+
|
|
113
|
+
## Important
|
|
114
|
+
|
|
115
|
+
- Use `bunx --bun md-to-pdf` (NOT npx) for PDF conversion
|
|
116
|
+
- This skill only converts — it does not research or generate content
|
|
117
|
+
- Individual markdown files are preserved alongside the PDF for future editing
|
|
118
|
+
- If `md-to-pdf` is not installed, `bunx` will auto-install it on first run
|
|
119
|
+
- Always verify the PDF exists before reporting success
|
|
@@ -14,16 +14,16 @@ argument-hint: <topic or question>
|
|
|
14
14
|
|
|
15
15
|
## Available Researcher Agents
|
|
16
16
|
|
|
17
|
-
- **
|
|
17
|
+
- **gemini-researcher** — academic depth via Gemini grounding, query decomposition, scholarly synthesis (falls back to WebSearch if no API key)
|
|
18
18
|
- **multi-perspective-researcher** — breadth, multiple angles, diverse viewpoints
|
|
19
|
-
- **
|
|
19
|
+
- **perplexity-researcher** — investigative rigor via Perplexity grounding, source cross-referencing, credibility assessment (falls back to WebSearch if no API key)
|
|
20
20
|
- **grok-researcher** — real-time data via Grok/X API, breaking news, social sentiment (falls back to WebSearch with recency focus if no API key)
|
|
21
21
|
|
|
22
22
|
## Quick Mode
|
|
23
23
|
|
|
24
24
|
Spawn **1 subagent** for a focused answer:
|
|
25
25
|
|
|
26
|
-
- Spawn `
|
|
26
|
+
- Spawn `gemini-researcher` with the full query and context
|
|
27
27
|
|
|
28
28
|
Wait for the result, then deliver it directly with light formatting.
|
|
29
29
|
|
|
@@ -31,12 +31,12 @@ Wait for the result, then deliver it directly with light formatting.
|
|
|
31
31
|
|
|
32
32
|
Craft **3 different queries** optimized for each researcher's strengths, then spawn all **in parallel (in a single message)**:
|
|
33
33
|
|
|
34
|
-
- Spawn `
|
|
34
|
+
- Spawn `gemini-researcher` with a query optimized for depth/analysis
|
|
35
35
|
- Spawn `multi-perspective-researcher` with a query optimized for breadth/perspectives
|
|
36
36
|
- Spawn `grok-researcher` with a query optimized for real-time data, recent developments, current state
|
|
37
37
|
|
|
38
38
|
**Query design:**
|
|
39
|
-
-
|
|
39
|
+
- gemini-researcher: focus on authoritative sources, technical depth, how/why
|
|
40
40
|
- multi-perspective-researcher: focus on different stakeholder views, trade-offs, alternatives
|
|
41
41
|
- grok-researcher: focus on latest news, breaking developments, social sentiment, what's happening right now
|
|
42
42
|
|
|
@@ -44,12 +44,12 @@ Craft **3 different queries** optimized for each researcher's strengths, then sp
|
|
|
44
44
|
|
|
45
45
|
Craft **8 queries** (2 per researcher type, each from a different angle), then spawn all **in parallel (in a single message)**:
|
|
46
46
|
|
|
47
|
-
- Spawn `
|
|
48
|
-
- Spawn `
|
|
47
|
+
- Spawn `gemini-researcher` — angle 1: core technical depth
|
|
48
|
+
- Spawn `gemini-researcher` — angle 2: historical context / evolution
|
|
49
49
|
- Spawn `multi-perspective-researcher` — angle 3: stakeholder perspectives
|
|
50
50
|
- Spawn `multi-perspective-researcher` — angle 4: cross-domain connections
|
|
51
|
-
- Spawn `
|
|
52
|
-
- Spawn `
|
|
51
|
+
- Spawn `perplexity-researcher` — angle 5: verify key claims
|
|
52
|
+
- Spawn `perplexity-researcher` — angle 6: find contradictions / counter-evidence
|
|
53
53
|
- Spawn `grok-researcher` — angle 7: real-time developments and breaking news
|
|
54
54
|
- Spawn `grok-researcher` — angle 8: social sentiment, public reaction, trending discourse
|
|
55
55
|
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* Gemini Search — CLI tool for grounded search via the Gemini API.
|
|
4
|
+
*
|
|
5
|
+
* Uses Gemini's built-in Google Search grounding to fetch real-time,
|
|
6
|
+
* source-cited information. Optimized for academic and scholarly queries.
|
|
7
|
+
*
|
|
8
|
+
* Requires PAL_GEMINI_API_KEY environment variable.
|
|
9
|
+
*
|
|
10
|
+
* Usage:
|
|
11
|
+
* bun gemini-search.ts -- <query> [--max-tokens 4096]
|
|
12
|
+
* bun gemini-search.ts -- "recent advances in transformer architectures"
|
|
13
|
+
* bun gemini-search.ts -- "CRISPR gene editing clinical trials 2025"
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { parseArgs } from "node:util";
|
|
17
|
+
|
|
18
|
+
const API_BASE = "https://generativelanguage.googleapis.com/v1beta";
|
|
19
|
+
const DEFAULT_MODEL = "gemini-3.1-flash-lite-preview";
|
|
20
|
+
|
|
21
|
+
interface GroundingChunk {
|
|
22
|
+
web?: { uri: string; title: string };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
interface GroundingSupport {
|
|
26
|
+
segment?: { startIndex: number; endIndex: number; text: string };
|
|
27
|
+
groundingChunkIndices?: number[];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
interface GroundingMetadata {
|
|
31
|
+
webSearchQueries?: string[];
|
|
32
|
+
groundingChunks?: GroundingChunk[];
|
|
33
|
+
groundingSupports?: GroundingSupport[];
|
|
34
|
+
searchEntryPoint?: { renderedContent: string };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
interface ContentPart {
|
|
38
|
+
text?: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
interface Candidate {
|
|
42
|
+
content?: { parts?: ContentPart[]; role?: string };
|
|
43
|
+
groundingMetadata?: GroundingMetadata;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
interface GeminiResponse {
|
|
47
|
+
candidates?: Candidate[];
|
|
48
|
+
error?: { message: string; code: number };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function loadApiKey(): string {
|
|
52
|
+
const key = process.env.PAL_GEMINI_API_KEY;
|
|
53
|
+
if (!key) {
|
|
54
|
+
console.error("Error: PAL_GEMINI_API_KEY environment variable is not set.");
|
|
55
|
+
console.error("Get an API key at https://aistudio.google.com/apikey");
|
|
56
|
+
process.exit(1);
|
|
57
|
+
}
|
|
58
|
+
return key;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const SYSTEM_PROMPT = `You are an academic research assistant. When searching, prioritize:
|
|
62
|
+
- Peer-reviewed papers, preprints (arXiv, bioRxiv, medRxiv)
|
|
63
|
+
- Official documentation and technical specifications
|
|
64
|
+
- University and research institution publications
|
|
65
|
+
- Conference proceedings (NeurIPS, ICML, ACL, CVPR, etc.)
|
|
66
|
+
- Systematic reviews and meta-analyses
|
|
67
|
+
|
|
68
|
+
Always include: author names, publication year, journal/venue when available.
|
|
69
|
+
Distinguish between peer-reviewed findings and preprints/working papers.
|
|
70
|
+
Note methodology limitations and sample sizes when relevant.
|
|
71
|
+
Be thorough but concise.`;
|
|
72
|
+
|
|
73
|
+
export async function geminiSearch(query: string, maxTokens: number): Promise<void> {
|
|
74
|
+
const apiKey = loadApiKey();
|
|
75
|
+
|
|
76
|
+
const body = {
|
|
77
|
+
system_instruction: {
|
|
78
|
+
parts: [{ text: SYSTEM_PROMPT }],
|
|
79
|
+
},
|
|
80
|
+
contents: [
|
|
81
|
+
{
|
|
82
|
+
parts: [{ text: query }],
|
|
83
|
+
},
|
|
84
|
+
],
|
|
85
|
+
tools: [{ google_search: {} }],
|
|
86
|
+
generationConfig: {
|
|
87
|
+
maxOutputTokens: maxTokens,
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const url = `${API_BASE}/models/${DEFAULT_MODEL}:generateContent?key=${apiKey}`;
|
|
92
|
+
|
|
93
|
+
const response = await fetch(url, {
|
|
94
|
+
method: "POST",
|
|
95
|
+
headers: { "Content-Type": "application/json" },
|
|
96
|
+
body: JSON.stringify(body),
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
if (!response.ok) {
|
|
100
|
+
const err = await response.text().catch(() => "");
|
|
101
|
+
console.error(`Error: HTTP ${response.status} — ${err.slice(0, 500)}`);
|
|
102
|
+
process.exit(1);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const data = (await response.json()) as GeminiResponse;
|
|
106
|
+
|
|
107
|
+
if (data.error) {
|
|
108
|
+
console.error(`Error: ${data.error.message}`);
|
|
109
|
+
process.exit(1);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (!data.candidates || data.candidates.length === 0) {
|
|
113
|
+
console.error("Error: No candidates in Gemini response.");
|
|
114
|
+
process.exit(1);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const candidate = data.candidates[0];
|
|
118
|
+
|
|
119
|
+
// Extract text
|
|
120
|
+
const textParts: string[] = [];
|
|
121
|
+
if (candidate.content?.parts) {
|
|
122
|
+
for (const part of candidate.content.parts) {
|
|
123
|
+
if (part.text) textParts.push(part.text);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (textParts.length === 0) {
|
|
128
|
+
console.error("Error: No text content in Gemini response.");
|
|
129
|
+
process.exit(1);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
console.log(textParts.join("\n\n"));
|
|
133
|
+
|
|
134
|
+
// Extract grounding metadata
|
|
135
|
+
const meta = candidate.groundingMetadata;
|
|
136
|
+
if (meta) {
|
|
137
|
+
if (meta.webSearchQueries && meta.webSearchQueries.length > 0) {
|
|
138
|
+
console.log("\n---\n## Search Queries Used\n");
|
|
139
|
+
for (const q of meta.webSearchQueries) {
|
|
140
|
+
console.log(`- ${q}`);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (meta.groundingChunks && meta.groundingChunks.length > 0) {
|
|
145
|
+
console.log("\n---\n## Sources\n");
|
|
146
|
+
for (const chunk of meta.groundingChunks) {
|
|
147
|
+
if (chunk.web) {
|
|
148
|
+
console.log(`- [${chunk.web.title}](${chunk.web.uri})`);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
async function run() {
|
|
156
|
+
const { positionals, values } = parseArgs({
|
|
157
|
+
allowPositionals: true,
|
|
158
|
+
options: {
|
|
159
|
+
"max-tokens": { type: "string", short: "m", default: "4096" },
|
|
160
|
+
help: { type: "boolean", short: "h" },
|
|
161
|
+
},
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
if (values.help || positionals.length === 0) {
|
|
165
|
+
console.log(`Gemini Search — grounded academic search via Gemini API
|
|
166
|
+
|
|
167
|
+
Usage:
|
|
168
|
+
bun gemini-search.ts -- <query> [options]
|
|
169
|
+
|
|
170
|
+
Options:
|
|
171
|
+
--max-tokens, -m <n> Max response tokens (default: 4096)
|
|
172
|
+
--help, -h Show this help
|
|
173
|
+
|
|
174
|
+
Examples:
|
|
175
|
+
bun gemini-search.ts -- "transformer architecture advances 2025"
|
|
176
|
+
bun gemini-search.ts -- "CRISPR clinical trials"`);
|
|
177
|
+
process.exit(0);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const query = positionals.join(" ");
|
|
181
|
+
const maxTokens = Number.parseInt(values["max-tokens"] ?? "4096", 10);
|
|
182
|
+
|
|
183
|
+
await geminiSearch(query, maxTokens);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (import.meta.main) run();
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* Uses the Grok Responses API with web_search and x_search tools
|
|
6
6
|
* to fetch real-time information from the web and X (Twitter).
|
|
7
7
|
*
|
|
8
|
-
* Requires
|
|
8
|
+
* Requires PAL_XAI_API_KEY environment variable.
|
|
9
9
|
*
|
|
10
10
|
* Usage:
|
|
11
11
|
* bun grok-search.ts -- <query> [--sources web,x] [--max-tokens 2048]
|
|
@@ -45,9 +45,9 @@ interface GrokResponse {
|
|
|
45
45
|
}
|
|
46
46
|
|
|
47
47
|
function loadApiKey(): string {
|
|
48
|
-
const key = process.env.
|
|
48
|
+
const key = process.env.PAL_XAI_API_KEY;
|
|
49
49
|
if (!key) {
|
|
50
|
-
console.error("Error:
|
|
50
|
+
console.error("Error: PAL_XAI_API_KEY environment variable is not set.");
|
|
51
51
|
console.error("Get an API key at https://console.x.ai/");
|
|
52
52
|
process.exit(1);
|
|
53
53
|
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* Perplexity Search — CLI tool for investigative search via the Perplexity Sonar API.
|
|
4
|
+
*
|
|
5
|
+
* Uses Perplexity's Sonar model with built-in web search to fetch
|
|
6
|
+
* source-cited, verified information. Optimized for investigative queries
|
|
7
|
+
* requiring cross-referenced, credible sources.
|
|
8
|
+
*
|
|
9
|
+
* Requires PAL_PERPLEXITY_API_KEY environment variable.
|
|
10
|
+
*
|
|
11
|
+
* Usage:
|
|
12
|
+
* bun perplexity-search.ts -- <query> [--max-tokens 4096]
|
|
13
|
+
* bun perplexity-search.ts -- "corruption allegations against company X"
|
|
14
|
+
* bun perplexity-search.ts -- "timeline of event Y with sources"
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { parseArgs } from "node:util";
|
|
18
|
+
|
|
19
|
+
const API_BASE = "https://api.perplexity.ai";
|
|
20
|
+
const DEFAULT_MODEL = "sonar-pro";
|
|
21
|
+
|
|
22
|
+
interface Choice {
|
|
23
|
+
message?: {
|
|
24
|
+
role: string;
|
|
25
|
+
content: string;
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
interface PerplexityResponse {
|
|
30
|
+
choices?: Choice[];
|
|
31
|
+
citations?: string[];
|
|
32
|
+
error?: { message: string; code?: number };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function loadApiKey(): string {
|
|
36
|
+
const key = process.env.PAL_PERPLEXITY_API_KEY;
|
|
37
|
+
if (!key) {
|
|
38
|
+
console.error(
|
|
39
|
+
"Error: PAL_PERPLEXITY_API_KEY environment variable is not set.\n" +
|
|
40
|
+
"The Perplexity API could not be reached. The researcher agent should fall back to WebSearch.\n" +
|
|
41
|
+
"To enable Perplexity search, get an API key at https://www.perplexity.ai/settings/api\n" +
|
|
42
|
+
"and set it: export PAL_PERPLEXITY_API_KEY=pplx-..."
|
|
43
|
+
);
|
|
44
|
+
process.exit(1);
|
|
45
|
+
}
|
|
46
|
+
return key;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const SYSTEM_PROMPT = `You are an investigative research assistant. When searching, prioritize:
|
|
50
|
+
- Cross-referenced facts verified by 2+ independent sources
|
|
51
|
+
- Primary sources: court filings, official reports, government records, regulatory filings
|
|
52
|
+
- Credible journalism: established outlets with editorial standards
|
|
53
|
+
- Source credibility assessment: note publication reputation, potential bias, date of publication
|
|
54
|
+
- Evidence chains: connect claims to their original sources
|
|
55
|
+
|
|
56
|
+
Always include: source names, publication dates, and direct quotes when available.
|
|
57
|
+
Distinguish between confirmed facts, single-source claims, and unverified allegations.
|
|
58
|
+
Flag contradictions between sources.
|
|
59
|
+
Be thorough but concise.`;
|
|
60
|
+
|
|
61
|
+
export async function perplexitySearch(query: string, maxTokens: number): Promise<void> {
|
|
62
|
+
const apiKey = loadApiKey();
|
|
63
|
+
|
|
64
|
+
const body = {
|
|
65
|
+
model: DEFAULT_MODEL,
|
|
66
|
+
messages: [
|
|
67
|
+
{ role: "system", content: SYSTEM_PROMPT },
|
|
68
|
+
{ role: "user", content: query },
|
|
69
|
+
],
|
|
70
|
+
max_tokens: maxTokens,
|
|
71
|
+
return_citations: true,
|
|
72
|
+
search_recency_filter: "week",
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const response = await fetch(`${API_BASE}/chat/completions`, {
|
|
76
|
+
method: "POST",
|
|
77
|
+
headers: {
|
|
78
|
+
Authorization: `Bearer ${apiKey}`,
|
|
79
|
+
"Content-Type": "application/json",
|
|
80
|
+
},
|
|
81
|
+
body: JSON.stringify(body),
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
if (!response.ok) {
|
|
85
|
+
const err = await response.text().catch(() => "");
|
|
86
|
+
console.error(`Error: HTTP ${response.status} — ${err.slice(0, 500)}`);
|
|
87
|
+
process.exit(1);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const data = (await response.json()) as PerplexityResponse;
|
|
91
|
+
|
|
92
|
+
if (data.error) {
|
|
93
|
+
console.error(`Error: ${data.error.message}`);
|
|
94
|
+
process.exit(1);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (!data.choices || data.choices.length === 0) {
|
|
98
|
+
console.error("Error: No choices in Perplexity response.");
|
|
99
|
+
process.exit(1);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const content = data.choices[0].message?.content;
|
|
103
|
+
if (!content) {
|
|
104
|
+
console.error("Error: No text content in Perplexity response.");
|
|
105
|
+
process.exit(1);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
console.log(content);
|
|
109
|
+
|
|
110
|
+
// Extract citations
|
|
111
|
+
if (data.citations && data.citations.length > 0) {
|
|
112
|
+
console.log("\n---\n## Sources\n");
|
|
113
|
+
for (let i = 0; i < data.citations.length; i++) {
|
|
114
|
+
console.log(`- [${i + 1}] ${data.citations[i]}`);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async function run() {
|
|
120
|
+
const { positionals, values } = parseArgs({
|
|
121
|
+
allowPositionals: true,
|
|
122
|
+
options: {
|
|
123
|
+
"max-tokens": { type: "string", short: "m", default: "4096" },
|
|
124
|
+
help: { type: "boolean", short: "h" },
|
|
125
|
+
},
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
if (values.help || positionals.length === 0) {
|
|
129
|
+
console.log(`Perplexity Search — investigative search via Perplexity Sonar API
|
|
130
|
+
|
|
131
|
+
Usage:
|
|
132
|
+
bun perplexity-search.ts -- <query> [options]
|
|
133
|
+
|
|
134
|
+
Options:
|
|
135
|
+
--max-tokens, -m <n> Max response tokens (default: 4096)
|
|
136
|
+
--help, -h Show this help
|
|
137
|
+
|
|
138
|
+
Examples:
|
|
139
|
+
bun perplexity-search.ts -- "corruption allegations timeline"
|
|
140
|
+
bun perplexity-search.ts -- "regulatory actions against company X"`);
|
|
141
|
+
process.exit(0);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const query = positionals.join(" ");
|
|
145
|
+
const maxTokens = Number.parseInt(values["max-tokens"] ?? "4096", 10);
|
|
146
|
+
|
|
147
|
+
await perplexitySearch(query, maxTokens);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (import.meta.main) run();
|
package/package.json
CHANGED
package/src/cli/index.ts
CHANGED
|
@@ -380,9 +380,9 @@ function doctor(silent = false): DoctorResult {
|
|
|
380
380
|
process.env.ANTHROPIC_API_KEY
|
|
381
381
|
? ok("ANTHROPIC_API_KEY is set")
|
|
382
382
|
: fail("ANTHROPIC_API_KEY — not set (hooks need it for inference)");
|
|
383
|
-
process.env.
|
|
384
|
-
? ok("
|
|
385
|
-
: warn("
|
|
383
|
+
process.env.PAL_GEMINI_API_KEY
|
|
384
|
+
? ok("PAL_GEMINI_API_KEY is set")
|
|
385
|
+
: warn("PAL_GEMINI_API_KEY — not set (optional, for YouTube analysis)");
|
|
386
386
|
|
|
387
387
|
// Hook health from debug.log
|
|
388
388
|
const hookHealth = checkHookHealth(home);
|
|
@@ -443,6 +443,18 @@ async function init(args: string[]) {
|
|
|
443
443
|
}
|
|
444
444
|
|
|
445
445
|
async function install(targets: { claude: boolean; opencode: boolean; cursor: boolean }) {
|
|
446
|
+
// Ensure dependencies are installed
|
|
447
|
+
const pkg = palPkg();
|
|
448
|
+
log.info("Installing dependencies...");
|
|
449
|
+
const deps = spawnSync("bun", ["install", "--frozen-lockfile"], {
|
|
450
|
+
cwd: pkg,
|
|
451
|
+
stdio: "inherit",
|
|
452
|
+
shell: true,
|
|
453
|
+
});
|
|
454
|
+
if (deps.status !== 0) {
|
|
455
|
+
log.warn("bun install failed — continuing anyway, but hooks may not work");
|
|
456
|
+
}
|
|
457
|
+
|
|
446
458
|
// Scaffold TELOS + PAL settings, then prompt for missing identity
|
|
447
459
|
const { scaffoldTelos, scaffoldPalSettings } = await import("../targets/lib");
|
|
448
460
|
const { promptIdentity } = await import("./setup-identity");
|
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
extractLastUser,
|
|
20
20
|
parseMessages,
|
|
21
21
|
} from "../lib/transcript";
|
|
22
|
+
import { appendProjectHistory } from "../lib/work-tracking";
|
|
22
23
|
|
|
23
24
|
function slugify(text: string): string {
|
|
24
25
|
return text
|
|
@@ -183,5 +184,13 @@ export async function captureWorkLearning(
|
|
|
183
184
|
const filepath = resolve(dir, filename);
|
|
184
185
|
writeFileSync(filepath, content, "utf-8");
|
|
185
186
|
|
|
187
|
+
// Append to per-project history (agent-agnostic recall)
|
|
188
|
+
appendProjectHistory(process.cwd(), {
|
|
189
|
+
date: new Date().toISOString().slice(0, 10),
|
|
190
|
+
title,
|
|
191
|
+
summary,
|
|
192
|
+
insights,
|
|
193
|
+
});
|
|
194
|
+
|
|
186
195
|
if (sessionId) markCaptured(sessionId, filepath, messages.length);
|
|
187
196
|
}
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import {
|
|
10
|
+
copyFileSync,
|
|
10
11
|
existsSync,
|
|
11
12
|
lstatSync,
|
|
12
13
|
readdirSync,
|
|
@@ -48,7 +49,7 @@ function latestMtime(...filePaths: string[]): number {
|
|
|
48
49
|
return latest;
|
|
49
50
|
}
|
|
50
51
|
|
|
51
|
-
/** Create or verify a symlink pointing to AGENTS.md */
|
|
52
|
+
/** Create or verify a symlink pointing to AGENTS.md (falls back to copy on Windows EPERM) */
|
|
52
53
|
function ensureOneSymlink(linkPath: string, targetPath: string): void {
|
|
53
54
|
try {
|
|
54
55
|
const stat = lstatSync(linkPath);
|
|
@@ -58,8 +59,16 @@ function ensureOneSymlink(linkPath: string, targetPath: string): void {
|
|
|
58
59
|
// doesn't exist — create it
|
|
59
60
|
}
|
|
60
61
|
ensureDir(dirname(linkPath));
|
|
61
|
-
|
|
62
|
-
|
|
62
|
+
try {
|
|
63
|
+
const relTarget = relative(dirname(linkPath), targetPath).replaceAll("\\", "/");
|
|
64
|
+
symlinkSync(relTarget, linkPath);
|
|
65
|
+
} catch (e: unknown) {
|
|
66
|
+
if ((e as NodeJS.ErrnoException).code === "EPERM") {
|
|
67
|
+
copyFileSync(targetPath, linkPath);
|
|
68
|
+
} else {
|
|
69
|
+
throw e;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
63
72
|
}
|
|
64
73
|
|
|
65
74
|
/** Ensure all agent symlinks point to the canonical AGENTS.md */
|
|
@@ -156,9 +165,12 @@ export function buildClaudeMd(): string {
|
|
|
156
165
|
/** Regenerate AGENTS.md if any source file is newer, and ensure CLAUDE.md symlink exists. Returns true if rebuilt. */
|
|
157
166
|
export function regenerateIfNeeded(): boolean {
|
|
158
167
|
const { outputPath } = getOutputPaths();
|
|
159
|
-
|
|
160
|
-
|
|
168
|
+
if (!needsRebuild()) {
|
|
169
|
+
ensureSymlinks();
|
|
170
|
+
return false;
|
|
171
|
+
}
|
|
161
172
|
ensureDir(dirname(outputPath));
|
|
162
173
|
writeFileSync(outputPath, buildClaudeMd(), "utf-8");
|
|
174
|
+
ensureSymlinks();
|
|
163
175
|
return true;
|
|
164
176
|
}
|
package/src/hooks/lib/context.ts
CHANGED
|
@@ -17,6 +17,7 @@ import { computeSignalTrends, formatTrends } from "./signal-trends";
|
|
|
17
17
|
import { readFramePrinciples } from "./wisdom";
|
|
18
18
|
import {
|
|
19
19
|
activeProjects,
|
|
20
|
+
readProjectHistory,
|
|
20
21
|
readSessions,
|
|
21
22
|
recentSessions,
|
|
22
23
|
staleProjects,
|
|
@@ -240,25 +241,15 @@ export function loadLearningDigest(): string {
|
|
|
240
241
|
const entries = readLearnings(paths.sessionLearning(), 10);
|
|
241
242
|
if (entries.length === 0) return "";
|
|
242
243
|
|
|
243
|
-
|
|
244
|
-
const other = entries.filter((e) => e.cwd !== cwd).slice(0,
|
|
244
|
+
// This-project learnings are now in loadProjectHistoryContext(); only show cross-project here
|
|
245
|
+
const other = entries.filter((e) => e.cwd !== cwd).slice(0, 5);
|
|
245
246
|
|
|
246
|
-
if (
|
|
247
|
+
if (other.length === 0) return "";
|
|
247
248
|
|
|
248
249
|
const lines: string[] = [];
|
|
249
250
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
for (const e of thisProject) {
|
|
253
|
-
lines.push(`- **${e.title}**`);
|
|
254
|
-
if (e.insights) lines.push(` ${e.insights.split("\n")[0].slice(0, 150)}`);
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
if (other.length > 0) {
|
|
259
|
-
lines.push(thisProject.length > 0 ? "" : "", "## Other Recent Learnings");
|
|
260
|
-
for (const e of other) lines.push(`- ${e.title}`);
|
|
261
|
-
}
|
|
251
|
+
lines.push("## Other Recent Learnings");
|
|
252
|
+
for (const e of other) lines.push(`- ${e.title}`);
|
|
262
253
|
|
|
263
254
|
return lines.join("\n");
|
|
264
255
|
} catch {
|
|
@@ -346,6 +337,25 @@ export function loadSignalTrends(): string {
|
|
|
346
337
|
}
|
|
347
338
|
}
|
|
348
339
|
|
|
340
|
+
/** Load per-project session history for the current working directory */
|
|
341
|
+
export function loadProjectHistoryContext(): string {
|
|
342
|
+
try {
|
|
343
|
+
const cwd = process.cwd();
|
|
344
|
+
const entries = readProjectHistory(cwd, 15);
|
|
345
|
+
if (entries.length === 0) return "";
|
|
346
|
+
|
|
347
|
+
const lines: string[] = ["## This Project — Session History"];
|
|
348
|
+
for (const e of entries) {
|
|
349
|
+
lines.push(`- **${e.title}** (${e.date})`);
|
|
350
|
+
if (e.summary) lines.push(` ${e.summary.split("\n")[0].slice(0, 150)}`);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
return lines.join("\n");
|
|
354
|
+
} catch {
|
|
355
|
+
return "";
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
349
359
|
/** Load recent relationship notes (today + yesterday) */
|
|
350
360
|
export function loadRelationshipContext(): string {
|
|
351
361
|
try {
|
|
@@ -373,6 +383,9 @@ export function buildSystemReminder(): string {
|
|
|
373
383
|
? loadRelationshipContext()
|
|
374
384
|
: "";
|
|
375
385
|
const digest = isEnabled(settings, "learningDigest") ? loadLearningDigest() : "";
|
|
386
|
+
const projectHistory = isEnabled(settings, "projectHistory")
|
|
387
|
+
? loadProjectHistoryContext()
|
|
388
|
+
: "";
|
|
376
389
|
const trends = isEnabled(settings, "signalTrends") ? loadSignalTrends() : "";
|
|
377
390
|
const failures = isEnabled(settings, "failurePatterns") ? loadFailurePatterns() : "";
|
|
378
391
|
const synthesis = isEnabled(settings, "synthesis")
|
|
@@ -384,6 +397,7 @@ export function buildSystemReminder(): string {
|
|
|
384
397
|
if (wisdom) parts.push(wisdom);
|
|
385
398
|
if (opinions) parts.push(opinions);
|
|
386
399
|
if (relationship) parts.push(relationship);
|
|
400
|
+
if (projectHistory) parts.push(projectHistory);
|
|
387
401
|
if (digest) parts.push(digest);
|
|
388
402
|
if (synthesis) parts.push(synthesis);
|
|
389
403
|
if (trends) parts.push(trends);
|
package/src/hooks/lib/export.ts
CHANGED
|
@@ -15,7 +15,8 @@ const EXPORT_DIRS = ["telos", "memory"];
|
|
|
15
15
|
const SKIP_PATTERNS = ["memory/downloads"];
|
|
16
16
|
|
|
17
17
|
function shouldSkip(relPath: string): boolean {
|
|
18
|
-
|
|
18
|
+
const normalized = relPath.replaceAll("\\", "/");
|
|
19
|
+
return SKIP_PATTERNS.some((p) => normalized.startsWith(p));
|
|
19
20
|
}
|
|
20
21
|
|
|
21
22
|
/** Recursively collect all files under a directory, returning paths relative to root. */
|
|
@@ -32,7 +33,7 @@ function walkDir(dir: string, root: string): string[] {
|
|
|
32
33
|
if (entry.isDirectory()) {
|
|
33
34
|
files.push(...walkDir(fullPath, root));
|
|
34
35
|
} else if (entry.isFile()) {
|
|
35
|
-
files.push(relPath);
|
|
36
|
+
files.push(relPath.replaceAll("\\", "/"));
|
|
36
37
|
}
|
|
37
38
|
}
|
|
38
39
|
return files;
|
package/src/hooks/lib/paths.ts
CHANGED
|
@@ -57,6 +57,7 @@ export const paths = {
|
|
|
57
57
|
relationship: () => ensureDir(home("memory", "relationship")),
|
|
58
58
|
entities: () => ensureDir(home("memory", "entities")),
|
|
59
59
|
failures: () => ensureDir(home("memory", "learning", "failures")),
|
|
60
|
+
projectHistory: () => ensureDir(home("memory", "projects")),
|
|
60
61
|
sessionLearning: () => ensureDir(home("memory", "learning", "session")),
|
|
61
62
|
synthesis: () => ensureDir(home("memory", "learning", "synthesis")),
|
|
62
63
|
backups: () => ensureDir(home("backups")),
|
|
@@ -67,12 +67,12 @@ function extractEnvVars(): string[] {
|
|
|
67
67
|
}
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
-
//
|
|
70
|
+
// PAL_GEMINI_API_KEY from youtube-analyze.ts
|
|
71
71
|
const youtubeFile = resolve(pkg, "src", "tools", "youtube-analyze.ts");
|
|
72
72
|
if (existsSync(youtubeFile)) {
|
|
73
73
|
const content = readFileSync(youtubeFile, "utf-8");
|
|
74
|
-
if (content.includes("
|
|
75
|
-
vars.add("
|
|
74
|
+
if (content.includes("PAL_GEMINI_API_KEY")) {
|
|
75
|
+
vars.add("PAL_GEMINI_API_KEY");
|
|
76
76
|
}
|
|
77
77
|
}
|
|
78
78
|
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Used by both Claude Code (StopOrchestrator) and opencode (plugin).
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
6
|
+
import { appendFileSync, existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
7
7
|
import { resolve } from "node:path";
|
|
8
8
|
import { ensureDir, paths } from "./paths";
|
|
9
9
|
import { now } from "./time";
|
|
@@ -143,6 +143,43 @@ export function extractHandoff(lastAssistant: string): string {
|
|
|
143
143
|
return cleaned;
|
|
144
144
|
}
|
|
145
145
|
|
|
146
|
+
// ── Per-Project History ──────────────────────────────────────────
|
|
147
|
+
|
|
148
|
+
export interface ProjectHistoryEntry {
|
|
149
|
+
date: string;
|
|
150
|
+
title: string;
|
|
151
|
+
summary: string;
|
|
152
|
+
insights: string;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** Convert a cwd path to a filesystem-safe slug (last directory segment) */
|
|
156
|
+
export function cwdToSlug(cwd: string): string {
|
|
157
|
+
const normalized = cwd.replace(/\\/g, "/").replace(/\/+$/, "");
|
|
158
|
+
return normalized.split("/").pop() || "unknown";
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** Append a learning entry to the project's history.jsonl */
|
|
162
|
+
export function appendProjectHistory(cwd: string, entry: ProjectHistoryEntry): void {
|
|
163
|
+
const slug = cwdToSlug(cwd);
|
|
164
|
+
const dir = ensureDir(resolve(paths.projectHistory(), slug));
|
|
165
|
+
const historyPath = resolve(dir, "history.jsonl");
|
|
166
|
+
const line = `${JSON.stringify(entry)}\n`;
|
|
167
|
+
appendFileSync(historyPath, line, "utf-8");
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** Read the project history for a given cwd */
|
|
171
|
+
export function readProjectHistory(cwd: string, limit = 15): ProjectHistoryEntry[] {
|
|
172
|
+
const slug = cwdToSlug(cwd);
|
|
173
|
+
const historyPath = resolve(paths.projectHistory(), slug, "history.jsonl");
|
|
174
|
+
if (!existsSync(historyPath)) return [];
|
|
175
|
+
try {
|
|
176
|
+
const lines = readFileSync(historyPath, "utf-8").trim().split("\n").filter(Boolean);
|
|
177
|
+
return lines.slice(-limit).map((line) => JSON.parse(line) as ProjectHistoryEntry);
|
|
178
|
+
} catch {
|
|
179
|
+
return [];
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
146
183
|
// ── Persistent Projects ──────────────────────────────────────────
|
|
147
184
|
|
|
148
185
|
export interface Project {
|
|
@@ -167,8 +167,6 @@ ${type === "anti-pattern" ? `### ${observation}\n- **Severity:** Medium\n- **Fre
|
|
|
167
167
|
`- ${date()}: Principle candidate — ${observation}`
|
|
168
168
|
);
|
|
169
169
|
break;
|
|
170
|
-
|
|
171
|
-
case "evolution":
|
|
172
170
|
default:
|
|
173
171
|
content = appendToSection(content, "## Evolution Log", evolutionEntry);
|
|
174
172
|
break;
|
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: claude-researcher
|
|
3
|
-
description: Deep research with academic rigor — query decomposition, multi-source synthesis, scholarly depth. Use for research tasks requiring thorough analysis.
|
|
4
|
-
tools: WebSearch, WebFetch, Read, Grep, Glob
|
|
5
|
-
model: sonnet
|
|
6
|
-
---
|
|
7
|
-
|
|
8
|
-
You are a research specialist focused on **depth and academic rigor**.
|
|
9
|
-
|
|
10
|
-
## Methodology
|
|
11
|
-
|
|
12
|
-
1. **Decompose** the query into 2-3 sub-questions that target the core of the topic
|
|
13
|
-
2. **Search** each sub-question using WebSearch — prioritize authoritative sources (papers, docs, official sites)
|
|
14
|
-
3. **Read** the most promising results with WebFetch to extract detail
|
|
15
|
-
4. **Synthesize** findings into a structured analysis
|
|
16
|
-
|
|
17
|
-
## Guidelines
|
|
18
|
-
|
|
19
|
-
- Prioritize primary sources over summaries
|
|
20
|
-
- Distinguish between established facts, expert consensus, and speculation
|
|
21
|
-
- Note methodology limitations when citing research
|
|
22
|
-
- If a claim has no strong source, say so — do not fabricate citations
|
|
23
|
-
- Keep findings concise but substantive
|
|
24
|
-
|
|
25
|
-
## Output Format
|
|
26
|
-
|
|
27
|
-
```markdown
|
|
28
|
-
## Findings
|
|
29
|
-
|
|
30
|
-
[Numbered list of key discoveries, each with a brief explanation]
|
|
31
|
-
|
|
32
|
-
## Sources
|
|
33
|
-
|
|
34
|
-
[Verified URLs with one-line descriptions — only include URLs you actually visited]
|
|
35
|
-
|
|
36
|
-
## Confidence
|
|
37
|
-
|
|
38
|
-
[High/Medium/Low rating per finding, with brief justification]
|
|
39
|
-
|
|
40
|
-
## Gaps
|
|
41
|
-
|
|
42
|
-
[What couldn't be answered or needs further investigation]
|
|
43
|
-
```
|
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: investigative-researcher
|
|
3
|
-
description: Investigative research with verification rigor — triple-checks sources, cross-references claims, assesses credibility. Use for research requiring high factual confidence.
|
|
4
|
-
tools: WebSearch, WebFetch, Read, Grep, Glob
|
|
5
|
-
model: sonnet
|
|
6
|
-
---
|
|
7
|
-
|
|
8
|
-
You are a research specialist focused on **verification and investigative rigor**.
|
|
9
|
-
|
|
10
|
-
## Methodology
|
|
11
|
-
|
|
12
|
-
1. **Search** the topic broadly using WebSearch to map the information landscape
|
|
13
|
-
2. **Verify** key claims by finding 2+ independent sources for each
|
|
14
|
-
3. **Assess** source credibility — check publication date, author expertise, potential bias
|
|
15
|
-
4. **Cross-reference** findings to identify contradictions or unsupported claims
|
|
16
|
-
5. **Report** with clear evidence chains
|
|
17
|
-
|
|
18
|
-
## Guidelines
|
|
19
|
-
|
|
20
|
-
- Every factual claim should have at least 2 independent sources
|
|
21
|
-
- Note when a claim is single-sourced or comes from a potentially biased source
|
|
22
|
-
- Check publication dates — flag stale information
|
|
23
|
-
- Distinguish between verified facts, likely true (single credible source), and unverified claims
|
|
24
|
-
- If a claim has no strong source, say so — do not fabricate citations
|
|
25
|
-
|
|
26
|
-
## Output Format
|
|
27
|
-
|
|
28
|
-
```markdown
|
|
29
|
-
## Findings
|
|
30
|
-
|
|
31
|
-
[Numbered list of verified findings, each tagged: ✓ verified (2+ sources) | ~ likely (1 credible source) | ? unverified]
|
|
32
|
-
|
|
33
|
-
## Sources
|
|
34
|
-
|
|
35
|
-
[Verified URLs with one-line descriptions — only include URLs you actually visited]
|
|
36
|
-
|
|
37
|
-
## Confidence
|
|
38
|
-
|
|
39
|
-
[High/Medium/Low rating per finding, with evidence chain summary]
|
|
40
|
-
|
|
41
|
-
## Flags
|
|
42
|
-
|
|
43
|
-
[Contradictions found, stale information, potential bias in sources]
|
|
44
|
-
```
|