@jaybeeuu/agent-cortex 1.20.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/.beads/README.md +81 -0
- package/.beads/config.yaml +1 -0
- package/.beads/issues.jsonl +236 -0
- package/.beads/metadata.json +7 -0
- package/.claude-plugin/marketplace.json +16 -0
- package/.github/workflows/ci.test.ts +217 -0
- package/.github/workflows/ci.yml +35 -0
- package/.github/workflows/publish.yml +36 -0
- package/.junie/guidelines.md +87 -0
- package/.junie/mcp/mcp.json +10 -0
- package/AGENTS.md +102 -0
- package/CHANGELOG.md +705 -0
- package/README.md +209 -0
- package/agents/README.md +235 -0
- package/agents/plan.agent.md +56 -0
- package/agents/ralph-plan.agent.md +119 -0
- package/agents/ralph.agent.md +305 -0
- package/agents/strategy.agent.md +109 -0
- package/agents-native/ralph.md +193 -0
- package/bin/agent-cortex.mjs +27 -0
- package/claude/.claude-plugin/plugin.json +11 -0
- package/claude/.mcp.json +18 -0
- package/claude/agents/plan.md +56 -0
- package/claude/agents/ralph-plan.md +119 -0
- package/claude/agents/ralph.md +193 -0
- package/claude/agents/strategy.md +109 -0
- package/claude/hooks.json +23 -0
- package/claude/scripts/statusline-command.sh +124 -0
- package/docs/ideas/auto-model-selection.md +75 -0
- package/docs/ideas/improve-pi-system-prompt.md +34 -0
- package/docs/ideas/model-selector.md +40 -0
- package/docs/ideas/pi-task-memory.md +34 -0
- package/docs/ideas/qa-extension.md +43 -0
- package/docs/inspirations.md +33 -0
- package/docs/research/gas-city-research.md +256 -0
- package/docs/research/skill-design-research.md +286 -0
- package/docs/research/skill-improvements-analysis.md +117 -0
- package/docs/research/skill-patterns-research.md +24 -0
- package/docs/skills/skill-anatomy.md +149 -0
- package/docs/user-preferences.md +17 -0
- package/extensions/agent-modes/index.ts +507 -0
- package/extensions/auto-name.ts +107 -0
- package/extensions/context-loader.md +35 -0
- package/extensions/context-loader.ts +65 -0
- package/extensions/lib/tiny-model.ts +212 -0
- package/extensions/notify/index.ts +293 -0
- package/extensions/session-start.ts +44 -0
- package/extensions/skill-stats/README.md +74 -0
- package/extensions/skill-stats/context-loader.test.ts +107 -0
- package/extensions/skill-stats/discover.ts +119 -0
- package/extensions/skill-stats/format.ts +13 -0
- package/extensions/skill-stats/index.test.ts +452 -0
- package/extensions/skill-stats/index.ts +222 -0
- package/extensions/skill-stats/package.json +19 -0
- package/extensions/skill-stats/pnpm-lock.yaml +329 -0
- package/extensions/skill-stats/record.ts +48 -0
- package/extensions/skill-stats/store.ts +104 -0
- package/extensions/skill-stats/tsconfig.json +12 -0
- package/extensions/subagent/README.md +175 -0
- package/extensions/subagent/agents/planner.md +37 -0
- package/extensions/subagent/agents/reviewer.md +35 -0
- package/extensions/subagent/agents/scout.md +50 -0
- package/extensions/subagent/agents/worker.md +24 -0
- package/extensions/subagent/agents.ts +126 -0
- package/extensions/subagent/index.ts +1138 -0
- package/extensions/subagent/prompts/implement-and-review.md +10 -0
- package/extensions/subagent/prompts/implement.md +10 -0
- package/extensions/subagent/prompts/scout-and-plan.md +9 -0
- package/hooks.json +10 -0
- package/lib/cli.mjs +37 -0
- package/package.json +39 -0
- package/pi/keybindings.json +3 -0
- package/pi/settings.json +29 -0
- package/plugin.json +35 -0
- package/pnpm-workspace.yaml +7 -0
- package/scripts/build-claude-agents.mjs +179 -0
- package/scripts/install-instructions.sh +82 -0
- package/skills/engineering/design-an-interface/SKILL.md +94 -0
- package/skills/engineering/grill-with-docs/SKILL.md +88 -0
- package/skills/engineering/improve-codebase-architecture/REFERENCE.md +78 -0
- package/skills/engineering/improve-codebase-architecture/SKILL.md +81 -0
- package/skills/engineering/tdd/SKILL.md +150 -0
- package/skills/engineering/tdd/deep-modules.md +33 -0
- package/skills/engineering/tdd/interface-design.md +31 -0
- package/skills/engineering/tdd/mocking.md +59 -0
- package/skills/engineering/tdd/refactoring.md +10 -0
- package/skills/engineering/tdd/tests.md +61 -0
- package/skills/engineering/technical-direction/SKILL.md +86 -0
- package/skills/planning/classify-bead/SKILL.md +67 -0
- package/skills/planning/create-task/SKILL.md +96 -0
- package/skills/planning/create-task/pipeline.json +33 -0
- package/skills/planning/create-task/scripts/create-chores.ts +112 -0
- package/skills/planning/create-task/scripts/package.json +14 -0
- package/skills/planning/create-task/scripts/pnpm-lock.yaml +342 -0
- package/skills/planning/create-task/scripts/tsconfig.json +12 -0
- package/skills/planning/create-task/templates/code.md +40 -0
- package/skills/planning/create-task/templates/document.md +40 -0
- package/skills/planning/create-task/templates/fix.md +27 -0
- package/skills/planning/create-task/templates/review.md +36 -0
- package/skills/planning/create-task/templates/verify.md +43 -0
- package/skills/planning/prd-to-tasks/SKILL.md +227 -0
- package/skills/planning/record-idea/SKILL.md +62 -0
- package/skills/planning/record-idea/scripts/new-idea.sh +89 -0
- package/skills/planning/request-refactor-plan/SKILL.md +73 -0
- package/skills/planning/write-a-prd/SKILL.md +74 -0
- package/skills/planning/write-a-ticket/SKILL.md +97 -0
- package/skills/productivity/bd-tool/SKILL.md +90 -0
- package/skills/productivity/grill-me/SKILL.md +10 -0
- package/skills/productivity/handoff/SKILL.md +15 -0
- package/skills/productivity/init-beads/SKILL.md +93 -0
- package/skills/productivity/teach/GLOSSARY-FORMAT.md +35 -0
- package/skills/productivity/teach/LEARNING-RECORD-FORMAT.md +46 -0
- package/skills/productivity/teach/MISSION-FORMAT.md +31 -0
- package/skills/productivity/teach/RESOURCES-FORMAT.md +32 -0
- package/skills/productivity/teach/SKILL.md +109 -0
- package/skills/productivity/validate-agent-dir/scripts/package.json +15 -0
- package/skills/productivity/validate-agent-dir/scripts/pnpm-lock.yaml +329 -0
- package/skills/productivity/validate-agent-dir/scripts/pnpm-workspace.yaml +2 -0
- package/skills/productivity/validate-agent-dir/scripts/tsconfig.json +19 -0
- package/skills/productivity/validate-agent-dir/scripts/validate-agent-dir.test.ts +317 -0
- package/skills/productivity/validate-agent-dir/scripts/validate-agent-dir.ts +127 -0
- package/skills/productivity/write-a-skill/SKILL.md +74 -0
- package/skills/review/maintain-agent-docs/REFERENCE.md +246 -0
- package/skills/review/maintain-agent-docs/SKILL.md +83 -0
- package/skills/review/refactor-skill/EXAMPLES.md +20 -0
- package/skills/review/refactor-skill/SKILL.md +148 -0
- package/skills/review/review-security/SKILL.md +107 -0
- package/skills/review/review-security/scripts/install-gitleaks.sh +43 -0
- package/skills/style/style-code/REFERENCE.md +38 -0
- package/skills/style/style-code/SKILL.md +113 -0
- package/skills/style/style-comms/SKILL.md +93 -0
- package/skills/style/style-documentation/SKILL.md +112 -0
- package/skills/style/style-tests/SKILL.md +97 -0
- package/skills/workflow/git-workflow/SKILL.md +85 -0
- package/skills/workflow/hitl-collab/SKILL.md +29 -0
- package/skills/workflow/plan/SKILL.md +272 -0
- package/skills/workflow/ralph/REFERENCE.md +202 -0
- package/skills/workflow/ralph/SKILL.md +105 -0
- package/skills/workflow/run-pipeline-stage/SKILL.md +151 -0
- package/skills/workflow/run-pipeline-stage/playbooks/code.md +17 -0
- package/skills/workflow/run-pipeline-stage/playbooks/document.md +18 -0
- package/skills/workflow/run-pipeline-stage/playbooks/fix.md +17 -0
- package/skills/workflow/run-pipeline-stage/playbooks/review.md +19 -0
- package/skills/workflow/run-pipeline-stage/playbooks/verify.md +14 -0
- package/skills/workflow/run-pipeline-stage/prompts/stage-runner.md +20 -0
- package/skills/workflow/run-pipeline-stage/scripts/generate-progress.test.ts +783 -0
- package/skills/workflow/run-pipeline-stage/scripts/generate-progress.ts +586 -0
- package/skills/workflow/run-pipeline-stage/scripts/package.json +15 -0
- package/skills/workflow/run-pipeline-stage/scripts/pnpm-lock.yaml +342 -0
- package/skills/workflow/run-pipeline-stage/scripts/pnpm-workspace.yaml +2 -0
- package/skills/workflow/run-pipeline-stage/scripts/tsconfig.json +12 -0
- package/skills/workflow/using-agent-skills/SKILL.md +109 -0
- package/skills/workflow/using-agent-skills/scripts/validate-skill-structure.test.ts +73 -0
- package/test/cli.test.mjs +149 -0
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared helpers for making cheap LLM calls from extensions.
|
|
3
|
+
*
|
|
4
|
+
* Reads a `tinyModel` config key from settings.json (global + project merged)
|
|
5
|
+
* and provides functions for session naming and response summarization using
|
|
6
|
+
* the configured model — typically the tiniest free model available.
|
|
7
|
+
*
|
|
8
|
+
* Config (in ~/.pi/agent/settings.json or .pi/settings.json):
|
|
9
|
+
* ```json
|
|
10
|
+
* {
|
|
11
|
+
* "tinyModel": {
|
|
12
|
+
* "model": "opencode/mimo-v2.5-free",
|
|
13
|
+
* "maxNameLength": 50,
|
|
14
|
+
* "maxSummaryLength": 100
|
|
15
|
+
* }
|
|
16
|
+
* }
|
|
17
|
+
* ```
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { complete } from "@earendil-works/pi-ai";
|
|
21
|
+
import { homedir } from "node:os";
|
|
22
|
+
import { join } from "node:path";
|
|
23
|
+
import { readFileSync, existsSync } from "node:fs";
|
|
24
|
+
import type { Model, Api } from "@earendil-works/pi-ai";
|
|
25
|
+
|
|
26
|
+
/* ── Types ── */
|
|
27
|
+
|
|
28
|
+
export interface TinyModelConfig {
|
|
29
|
+
/** Provider/id string, e.g. "opencode/mimo-v2.5-free" */
|
|
30
|
+
model: string;
|
|
31
|
+
/** Global on/off toggle */
|
|
32
|
+
enabled: boolean;
|
|
33
|
+
/** Max characters for auto-generated session names */
|
|
34
|
+
maxNameLength: number;
|
|
35
|
+
/** Max characters for response summaries in notifications */
|
|
36
|
+
maxSummaryLength: number;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const DEFAULT_CONFIG: TinyModelConfig = {
|
|
40
|
+
model: "opencode/mimo-v2.5-free",
|
|
41
|
+
enabled: true,
|
|
42
|
+
maxNameLength: 40,
|
|
43
|
+
maxSummaryLength: 100,
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
/* ── Config loading ── */
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Load tiny-model config from merged global + project settings.
|
|
50
|
+
*
|
|
51
|
+
* Project .pi/settings.json overrides global ~/.pi/agent/settings.json.
|
|
52
|
+
*/
|
|
53
|
+
export function loadTinyModelConfig(cwd?: string): TinyModelConfig {
|
|
54
|
+
const paths = [join(homedir(), ".pi", "agent", "settings.json")];
|
|
55
|
+
if (cwd) paths.push(join(cwd, ".pi", "settings.json"));
|
|
56
|
+
|
|
57
|
+
const merged: Record<string, any> = {};
|
|
58
|
+
|
|
59
|
+
for (const filePath of paths) {
|
|
60
|
+
try {
|
|
61
|
+
if (existsSync(filePath)) {
|
|
62
|
+
const raw = readFileSync(filePath, "utf-8");
|
|
63
|
+
const parsed = JSON.parse(raw);
|
|
64
|
+
if (parsed?.tinyModel && typeof parsed.tinyModel === "object") {
|
|
65
|
+
Object.assign(merged, parsed.tinyModel);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
} catch {
|
|
69
|
+
// skip unreadable or invalid settings files
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return { ...DEFAULT_CONFIG, ...merged } as TinyModelConfig;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Parse a "provider/id" string into its parts.
|
|
78
|
+
*/
|
|
79
|
+
export function parseModelString(modelStr: string): {
|
|
80
|
+
provider: string;
|
|
81
|
+
id: string;
|
|
82
|
+
} {
|
|
83
|
+
const slash = modelStr.indexOf("/");
|
|
84
|
+
if (slash === -1) return { provider: "opencode", id: modelStr };
|
|
85
|
+
return {
|
|
86
|
+
provider: modelStr.slice(0, slash),
|
|
87
|
+
id: modelStr.slice(slash + 1),
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/* ── LLM helpers ── */
|
|
92
|
+
|
|
93
|
+
type ModelAny = Model<Api>;
|
|
94
|
+
type CompleteOptions = Record<string, any>;
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Generate a concise session title from the user's first prompt.
|
|
98
|
+
*
|
|
99
|
+
* Calls the configured tiny model with a system prompt tuned for naming.
|
|
100
|
+
* Returns null on failure (caller should fall back to truncation).
|
|
101
|
+
*/
|
|
102
|
+
export async function generateNameFromPrompt(
|
|
103
|
+
model: ModelAny,
|
|
104
|
+
apiKey: string | undefined,
|
|
105
|
+
headers: Record<string, string> | undefined,
|
|
106
|
+
prompt: string,
|
|
107
|
+
maxLength: number,
|
|
108
|
+
): Promise<string | null> {
|
|
109
|
+
const systemPrompt = [
|
|
110
|
+
"You are a session naming assistant. Generate a concise title for a coding session.",
|
|
111
|
+
"",
|
|
112
|
+
"Rules:",
|
|
113
|
+
"- Output ONLY the title, no quotes, no prefix, no explanation",
|
|
114
|
+
`- Maximum ${maxLength} characters`,
|
|
115
|
+
"- Summarize intent, do not copy verbatim",
|
|
116
|
+
"- Keep it short (3\u20135 words), capture the general theme",
|
|
117
|
+
"- If the message mentions files or modules, keep the key names brief",
|
|
118
|
+
].join("\n");
|
|
119
|
+
|
|
120
|
+
const truncated = prompt.length > 2000 ? prompt.slice(0, 2000) + "..." : prompt;
|
|
121
|
+
|
|
122
|
+
try {
|
|
123
|
+
const options: CompleteOptions = { maxTokens: 64 };
|
|
124
|
+
if (apiKey) options.apiKey = apiKey;
|
|
125
|
+
if (headers) options.headers = headers;
|
|
126
|
+
|
|
127
|
+
const result = await complete(model, {
|
|
128
|
+
systemPrompt,
|
|
129
|
+
messages: [{ role: "user", content: truncated, timestamp: Date.now() }],
|
|
130
|
+
}, options);
|
|
131
|
+
|
|
132
|
+
const text = result.content
|
|
133
|
+
?.filter((b: any): b is { type: "text"; text: string } => b.type === "text")
|
|
134
|
+
?.map((b) => b.text)
|
|
135
|
+
?.join("")
|
|
136
|
+
?.trim() ?? "";
|
|
137
|
+
|
|
138
|
+
return cleanTitle(text, maxLength);
|
|
139
|
+
} catch {
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Summarize an assistant response into a one-line summary.
|
|
146
|
+
*
|
|
147
|
+
* Calls the configured tiny model. Returns null on failure.
|
|
148
|
+
*/
|
|
149
|
+
export async function summarizeResponseText(
|
|
150
|
+
model: ModelAny,
|
|
151
|
+
apiKey: string | undefined,
|
|
152
|
+
headers: Record<string, string> | undefined,
|
|
153
|
+
responseText: string,
|
|
154
|
+
maxLength: number,
|
|
155
|
+
): Promise<string | null> {
|
|
156
|
+
const systemPrompt = [
|
|
157
|
+
"Summarize what the AI assistant did in this coding session response.",
|
|
158
|
+
"Be concise — one short sentence or phrase.",
|
|
159
|
+
`Maximum ${maxLength} characters.`,
|
|
160
|
+
"Focus on what changed or was accomplished.",
|
|
161
|
+
"Output ONLY the summary, no quotes, no prefix, no explanation.",
|
|
162
|
+
].join("\n");
|
|
163
|
+
|
|
164
|
+
const truncated =
|
|
165
|
+
responseText.length > 2000 ? responseText.slice(0, 2000) + "..." : responseText;
|
|
166
|
+
|
|
167
|
+
try {
|
|
168
|
+
const options: CompleteOptions = { maxTokens: 64 };
|
|
169
|
+
if (apiKey) options.apiKey = apiKey;
|
|
170
|
+
if (headers) options.headers = headers;
|
|
171
|
+
|
|
172
|
+
const result = await complete(model, {
|
|
173
|
+
systemPrompt,
|
|
174
|
+
messages: [{ role: "user", content: truncated, timestamp: Date.now() }],
|
|
175
|
+
}, options);
|
|
176
|
+
|
|
177
|
+
const text = result.content
|
|
178
|
+
?.filter((b: any): b is { type: "text"; text: string } => b.type === "text")
|
|
179
|
+
?.map((b) => b.text)
|
|
180
|
+
?.join("")
|
|
181
|
+
?.trim() ?? "";
|
|
182
|
+
|
|
183
|
+
return text || null;
|
|
184
|
+
} catch {
|
|
185
|
+
return null;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/* ── Internal helpers ── */
|
|
190
|
+
|
|
191
|
+
function cleanTitle(raw: string, maxLength: number): string | null {
|
|
192
|
+
let name = raw.trim();
|
|
193
|
+
if (!name) return null;
|
|
194
|
+
|
|
195
|
+
// Strip surrounding quotes if present
|
|
196
|
+
if (
|
|
197
|
+
(name.startsWith('"') && name.endsWith('"')) ||
|
|
198
|
+
(name.startsWith("'") && name.endsWith("'"))
|
|
199
|
+
) {
|
|
200
|
+
name = name.slice(1, -1);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Collapse newlines
|
|
204
|
+
name = name.replace(/\n/g, " ").trim();
|
|
205
|
+
|
|
206
|
+
// Truncate
|
|
207
|
+
if (name.length > maxLength) {
|
|
208
|
+
name = name.slice(0, maxLength - 3) + "...";
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return name || null;
|
|
212
|
+
}
|
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Desktop notification + terminal bell on task completion.
|
|
3
|
+
*
|
|
4
|
+
* Fires after multi-turn or long-running tasks (>1 turn or >30s) with a
|
|
5
|
+
* multiline notification showing:
|
|
6
|
+
* Line 1: working directory path
|
|
7
|
+
* Line 2: session name (if set)
|
|
8
|
+
* Line 3: LLM-generated summary of what the agent did
|
|
9
|
+
*
|
|
10
|
+
* The notification title is the tmux session:window.pane identifier (when
|
|
11
|
+
* available) or "pi".
|
|
12
|
+
*
|
|
13
|
+
* Response summarization uses the tiny model configured in settings.json
|
|
14
|
+
* under the `tinyModel` key.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
18
|
+
import type { AgentMessage } from "@earendil-works/pi-agent-core";
|
|
19
|
+
import { execSync } from "node:child_process";
|
|
20
|
+
import { homedir } from "node:os";
|
|
21
|
+
|
|
22
|
+
import {
|
|
23
|
+
loadTinyModelConfig,
|
|
24
|
+
parseModelString,
|
|
25
|
+
summarizeResponseText,
|
|
26
|
+
} from "../lib/tiny-model.js";
|
|
27
|
+
import type { TinyModelConfig } from "../lib/tiny-model.js";
|
|
28
|
+
|
|
29
|
+
/* ── Context helpers ── */
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Get the tmux session:window.pane identifier, or undefined if not in tmux.
|
|
33
|
+
*/
|
|
34
|
+
function getTmuxLabel(): string | undefined {
|
|
35
|
+
if (!process.env.TMUX) return undefined;
|
|
36
|
+
try {
|
|
37
|
+
return execSync(
|
|
38
|
+
'tmux display-message -p "#{session_name}:#{window_index}.#{pane_index}"',
|
|
39
|
+
{ encoding: "utf-8", timeout: 2000 },
|
|
40
|
+
).trim();
|
|
41
|
+
} catch {
|
|
42
|
+
return undefined;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Format a working directory path for display.
|
|
48
|
+
*
|
|
49
|
+
* Replaces $HOME with ~ and truncates from the left to ~40 chars when the
|
|
50
|
+
* path is too long, keeping the trailing directory name visible.
|
|
51
|
+
*/
|
|
52
|
+
function formatTruncatedPath(cwd: string): string {
|
|
53
|
+
const home = homedir();
|
|
54
|
+
let display = cwd;
|
|
55
|
+
if (display.startsWith(home)) {
|
|
56
|
+
display = "~" + display.slice(home.length);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const maxLen = 40;
|
|
60
|
+
if (display.length <= maxLen) return display;
|
|
61
|
+
|
|
62
|
+
const head = "~\u2026/";
|
|
63
|
+
const tailLen = maxLen - head.length;
|
|
64
|
+
if (tailLen <= 0) return display.slice(-maxLen);
|
|
65
|
+
|
|
66
|
+
return head + display.slice(-tailLen);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Extract plain text from the last assistant response for summarization.
|
|
71
|
+
*
|
|
72
|
+
* Returns the concatenated text content of the last assistant message,
|
|
73
|
+
* or empty string if none found.
|
|
74
|
+
*/
|
|
75
|
+
function extractAssistantText(messages: AgentMessage[]): string {
|
|
76
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
77
|
+
const msg = messages[i];
|
|
78
|
+
if (msg.role !== "assistant") continue;
|
|
79
|
+
|
|
80
|
+
const texts = msg.content
|
|
81
|
+
.filter(
|
|
82
|
+
(b): b is { type: "text"; text: string } => b.type === "text",
|
|
83
|
+
)
|
|
84
|
+
.map((b) => b.text)
|
|
85
|
+
.filter(Boolean);
|
|
86
|
+
|
|
87
|
+
if (texts.length === 0) continue;
|
|
88
|
+
|
|
89
|
+
return texts.join(" ").trim();
|
|
90
|
+
}
|
|
91
|
+
return "";
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/* ── Desktop notification helpers (OSC 777 / 99 / 9) ── */
|
|
95
|
+
|
|
96
|
+
function wrapForTmux(sequence: string): string {
|
|
97
|
+
if (!process.env.TMUX) return sequence;
|
|
98
|
+
const escaped = sequence.split("\x1b").join("\x1b\x1b");
|
|
99
|
+
return `\x1bPtmux;${escaped}\x1b\\`;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function notifyOSC777(title: string, body: string): void {
|
|
103
|
+
process.stdout.write(wrapForTmux(`\x1b]777;notify;${title};${body}\x07`));
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function notifyOSC9(message: string): void {
|
|
107
|
+
process.stdout.write(wrapForTmux(`\x1b]9;${message}\x07`));
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function notifyOSC99(title: string, body: string): void {
|
|
111
|
+
// d=1 = persistent notification (stays until dismissed)
|
|
112
|
+
process.stdout.write(wrapForTmux(`\x1b]99;i=1:d=1;${title}\x1b\\`));
|
|
113
|
+
process.stdout.write(wrapForTmux(`\x1b]99;i=1:p=body;${body}\x1b\\`));
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function sendDesktopNotification(title: string, sections: string[]): void {
|
|
117
|
+
const isIterm2 =
|
|
118
|
+
process.env.TERM_PROGRAM === "iTerm.app" ||
|
|
119
|
+
Boolean(process.env.ITERM_SESSION_ID);
|
|
120
|
+
|
|
121
|
+
// Flatten sections into a single notification body for non-Windows paths
|
|
122
|
+
const body = sections.join(" \u00b7 ");
|
|
123
|
+
|
|
124
|
+
if (process.env.KITTY_WINDOW_ID) {
|
|
125
|
+
notifyOSC99(title, body);
|
|
126
|
+
} else if (isIterm2) {
|
|
127
|
+
notifyOSC9(`${title}: ${body}`);
|
|
128
|
+
} else if (process.env.WT_SESSION || process.platform === 'win32') {
|
|
129
|
+
// Windows Terminal / Windows — PowerShell toast via execFileSync
|
|
130
|
+
const { execFileSync } = require("node:child_process");
|
|
131
|
+
const script = buildWindowsToastScript(title, sections);
|
|
132
|
+
try {
|
|
133
|
+
execFileSync("powershell.exe", ["-NoProfile", "-Command", script], { timeout: 5000 });
|
|
134
|
+
} catch (e) {
|
|
135
|
+
console.warn("[notify] toast failed:", e.message);
|
|
136
|
+
}
|
|
137
|
+
} else {
|
|
138
|
+
notifyOSC777(title, body);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function buildWindowsToastScript(title: string, sections: string[]): string {
|
|
143
|
+
// Escape single quotes for PowerShell single-quoted strings: double them up
|
|
144
|
+
const esc = (s: string) => s.replace(/'/g, "''").replace(/\r/g, '');
|
|
145
|
+
const psTitle = esc(title);
|
|
146
|
+
|
|
147
|
+
// Use ToastText04 for up to 4 text elements (title + 3 body sections)
|
|
148
|
+
const template = [
|
|
149
|
+
`[Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType = WindowsRuntime] > $null`,
|
|
150
|
+
`$t = [Windows.UI.Notifications.ToastNotificationManager]::GetTemplateContent([Windows.UI.Notifications.ToastTemplateType]::ToastText04)`,
|
|
151
|
+
`$null = $t.SelectSingleNode('/toast').SetAttribute('scenario', 'persistent')`,
|
|
152
|
+
// Title goes to text[0], body sections to text[1-3]
|
|
153
|
+
`$null = $t.SelectSingleNode('//text[@id=1]').AppendChild($t.CreateTextNode('${psTitle}'))`,
|
|
154
|
+
];
|
|
155
|
+
|
|
156
|
+
for (let i = 0; i < Math.min(sections.length, 3); i++) {
|
|
157
|
+
const escaped = esc(sections[i]);
|
|
158
|
+
template.push(`$null = $t.SelectSingleNode('//text[@id=${i + 2}]').AppendChild($t.CreateTextNode('${escaped}'))`);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
template.push(`$toast = [Windows.UI.Notifications.ToastNotification]::new($t)`);
|
|
162
|
+
template.push(`[Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier('pi').Show($toast) > $null`);
|
|
163
|
+
|
|
164
|
+
return template.join("; ");
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/* ── Bell ── */
|
|
168
|
+
|
|
169
|
+
function ringBell(): void {
|
|
170
|
+
try {
|
|
171
|
+
const { openSync, writeSync, closeSync } = require("node:fs");
|
|
172
|
+
const fd = openSync("/dev/tty", "w");
|
|
173
|
+
writeSync(fd, "\x07");
|
|
174
|
+
closeSync(fd);
|
|
175
|
+
} catch {
|
|
176
|
+
process.stdout.write("\x07");
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export default function (pi: ExtensionAPI) {
|
|
181
|
+
let agentStartTime = 0;
|
|
182
|
+
let turnCount = 0;
|
|
183
|
+
let config: TinyModelConfig;
|
|
184
|
+
|
|
185
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
186
|
+
config = loadTinyModelConfig(ctx.cwd);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
pi.on("agent_start", async () => {
|
|
190
|
+
agentStartTime = Date.now();
|
|
191
|
+
turnCount = 0;
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
pi.on("turn_end", async () => {
|
|
195
|
+
turnCount++;
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
pi.on("agent_end", async (event, ctx) => {
|
|
199
|
+
const elapsed = Date.now() - agentStartTime;
|
|
200
|
+
|
|
201
|
+
// Only notify on multi-turn or long tasks — not on quick chat replies
|
|
202
|
+
if (turnCount === 1 && elapsed < 30_000) return;
|
|
203
|
+
|
|
204
|
+
// ── Title: path · tmux identifier (prominent notification header) ──
|
|
205
|
+
const tmuxLabel = getTmuxLabel();
|
|
206
|
+
const truncatedPath = formatTruncatedPath(ctx.cwd);
|
|
207
|
+
const title = tmuxLabel
|
|
208
|
+
? `${truncatedPath} \u00b7 ${tmuxLabel}`
|
|
209
|
+
: truncatedPath;
|
|
210
|
+
|
|
211
|
+
// ── Body: sections separated by a blank line ──
|
|
212
|
+
const sections: string[] = [];
|
|
213
|
+
|
|
214
|
+
// Section 1: session name (if set)
|
|
215
|
+
const sessionName = pi.getSessionName();
|
|
216
|
+
if (sessionName) sections.push(sessionName);
|
|
217
|
+
|
|
218
|
+
// Section 2: LLM-generated summary of the assistant's response
|
|
219
|
+
const responseText = extractAssistantText(event.messages);
|
|
220
|
+
if (responseText) {
|
|
221
|
+
try {
|
|
222
|
+
const summary = await summarizeResponseWithFallback(
|
|
223
|
+
ctx,
|
|
224
|
+
responseText,
|
|
225
|
+
config,
|
|
226
|
+
);
|
|
227
|
+
sections.push(summary);
|
|
228
|
+
} catch {
|
|
229
|
+
// If summary fails (e.g. config not loaded), just show raw text
|
|
230
|
+
sections.push(responseText.slice(0, config?.maxSummaryLength ?? 100).replace(/\s+/g, ' ').trim());
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Desktop notification
|
|
235
|
+
sendDesktopNotification(title, sections);
|
|
236
|
+
|
|
237
|
+
// Terminal bell (tmux catches this via monitor-bell + visual-bell)
|
|
238
|
+
ringBell();
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Summarize response text using the configured tiny model, falling back
|
|
244
|
+
* to simple truncation if the LLM call fails or is disabled.
|
|
245
|
+
*/
|
|
246
|
+
async function summarizeResponseWithFallback(
|
|
247
|
+
ctx: any,
|
|
248
|
+
responseText: string,
|
|
249
|
+
config: TinyModelConfig,
|
|
250
|
+
): Promise<string> {
|
|
251
|
+
if (!config.enabled) {
|
|
252
|
+
return truncateToOneLine(responseText, config.maxSummaryLength);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
try {
|
|
256
|
+
const { provider, id } = parseModelString(config.model);
|
|
257
|
+
const model = ctx.modelRegistry.find(provider, id);
|
|
258
|
+
if (!model) {
|
|
259
|
+
return truncateToOneLine(responseText, config.maxSummaryLength);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const { apiKey, headers } =
|
|
263
|
+
await ctx.modelRegistry.getApiKeyAndHeaders(model);
|
|
264
|
+
|
|
265
|
+
const summary = await summarizeResponseText(
|
|
266
|
+
model,
|
|
267
|
+
apiKey,
|
|
268
|
+
headers,
|
|
269
|
+
responseText,
|
|
270
|
+
config.maxSummaryLength,
|
|
271
|
+
);
|
|
272
|
+
|
|
273
|
+
if (summary) return summary;
|
|
274
|
+
} catch (err) {
|
|
275
|
+
console.warn("[notify] LLM summary failed, falling back:", err);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
return truncateToOneLine(responseText, config.maxSummaryLength);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Truncate text to roughly `maxLen` characters on a single line,
|
|
283
|
+
* breaking at a word boundary.
|
|
284
|
+
*/
|
|
285
|
+
function truncateToOneLine(text: string, maxLen: number): string {
|
|
286
|
+
const singleLine = text.replace(/\s+/g, " ").trim();
|
|
287
|
+
if (singleLine.length <= maxLen) return singleLine;
|
|
288
|
+
|
|
289
|
+
const breakPoint = singleLine.lastIndexOf(" ", maxLen - 1);
|
|
290
|
+
const truncated =
|
|
291
|
+
breakPoint > 0 ? singleLine.slice(0, breakPoint) : singleLine.slice(0, maxLen);
|
|
292
|
+
return truncated + " \u2026";
|
|
293
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { loadSessionContext } from "./context-loader.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Session-start extension.
|
|
6
|
+
*
|
|
7
|
+
* Injects baseline context at the start of every PI session:
|
|
8
|
+
* 1. Skill availability hints (so the agent knows they exist)
|
|
9
|
+
* 2. Project context files (AGENTS.md, user-preferences.md)
|
|
10
|
+
*
|
|
11
|
+
* Context files are read dynamically from the project root (cwd).
|
|
12
|
+
*/
|
|
13
|
+
export default function (pi: ExtensionAPI) {
|
|
14
|
+
pi.on("before_agent_start", async (_event, ctx) => {
|
|
15
|
+
const skillHints = [
|
|
16
|
+
"Available skills — invoke when relevant (not pre-loaded to save context):",
|
|
17
|
+
"",
|
|
18
|
+
"• /skill:using-agent-skills — meta-skill that routes to the right skill",
|
|
19
|
+
" and enforces core agent behaviors. Check this first if unsure.",
|
|
20
|
+
"",
|
|
21
|
+
"• /skill:bd-tool — project context & task tracking with beads.",
|
|
22
|
+
" Run `bd ready` to see available work.",
|
|
23
|
+
"",
|
|
24
|
+
"• /skill:git-workflow — git discipline: feature branches,",
|
|
25
|
+
" worktrees for solo AFK work, PR workflow, and when to ask.",
|
|
26
|
+
"",
|
|
27
|
+
"Read the SKILL.md for full instructions when needed.",
|
|
28
|
+
].join("\n");
|
|
29
|
+
|
|
30
|
+
const contextFiles = loadSessionContext(ctx.cwd);
|
|
31
|
+
|
|
32
|
+
const content = contextFiles
|
|
33
|
+
? `${skillHints}\n\n---\n\n${contextFiles}`
|
|
34
|
+
: skillHints;
|
|
35
|
+
|
|
36
|
+
return {
|
|
37
|
+
message: {
|
|
38
|
+
customType: "session-start",
|
|
39
|
+
content,
|
|
40
|
+
display: false,
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
});
|
|
44
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# skill-stats
|
|
2
|
+
|
|
3
|
+
A PI extension that tracks skill usage across all sessions.
|
|
4
|
+
|
|
5
|
+
## What it tracks
|
|
6
|
+
|
|
7
|
+
| Signal | Source | What it tells you |
|
|
8
|
+
|---|---|---|
|
|
9
|
+
| **Loaded** | `before_agent_start` → `systemPromptOptions.skills` | Skill was injected into the system prompt (available to the LLM) |
|
|
10
|
+
| **Invoked** | `input` matching `/skill:name` | User explicitly invoked the skill |
|
|
11
|
+
| **Read** | `read` tool call to a SKILL.md | LLM actively loaded the skill's full instructions |
|
|
12
|
+
|
|
13
|
+
### Misuse detection
|
|
14
|
+
|
|
15
|
+
A skill that is *loaded* frequently but *never read* may be burning context budget without being consulted — a candidate for removal or tightening.
|
|
16
|
+
|
|
17
|
+
## Data
|
|
18
|
+
|
|
19
|
+
Usage data is stored in `~/.pi/agent-cortex/skill-usage.json` — a single file across all projects, with per-project breakdowns so you can slice by project.
|
|
20
|
+
|
|
21
|
+
## Installation
|
|
22
|
+
|
|
23
|
+
This extension is bundled with the `agent-cortex` PI package. Install the package for
|
|
24
|
+
auto-discovery:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
pi install git:https://github.com/jaybeeuu/agent-cortex
|
|
28
|
+
pi config enable extension:skill-stats
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
After install, `/reload` inside PI (or restart PI). The extension auto-registers.
|
|
32
|
+
|
|
33
|
+
### Development symlink (alternative)
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
ln -sf $(pwd)/extensions/skill-stats ~/.pi/agent/extensions/skill-stats
|
|
37
|
+
/reload
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Commands
|
|
41
|
+
|
|
42
|
+
| Command | Description |
|
|
43
|
+
|---|---|
|
|
44
|
+
| `/skill-stats` | Show usage dashboard with per-project slicing |
|
|
45
|
+
| `/skill-stats --project ./relative/path` | Filter to a specific project |
|
|
46
|
+
| `/skill-usage-reset` | Wipe all recorded data |
|
|
47
|
+
|
|
48
|
+
## Raw data
|
|
49
|
+
|
|
50
|
+
The JSON file at `~/.pi/agent-cortex/skill-usage.json` contains the full record:
|
|
51
|
+
|
|
52
|
+
```json
|
|
53
|
+
{
|
|
54
|
+
"version": 1,
|
|
55
|
+
"skills": {
|
|
56
|
+
"bd-tool": {
|
|
57
|
+
"loadedCount": 42,
|
|
58
|
+
"invokedCount": 5,
|
|
59
|
+
"readCount": 18,
|
|
60
|
+
"lastUsed": "2026-06-07T...",
|
|
61
|
+
"byProject": {
|
|
62
|
+
"/home/jaybeeuu/src/agent-cortex": {
|
|
63
|
+
"loadedCount": 40,
|
|
64
|
+
"invokedCount": 3,
|
|
65
|
+
"readCount": 16,
|
|
66
|
+
"lastUsed": "2026-06-07T..."
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
"totalTurns": 350,
|
|
72
|
+
"lastUpdated": "2026-06-07T..."
|
|
73
|
+
}
|
|
74
|
+
```
|