opencarly 1.0.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 +78 -0
- package/bin/install.js +304 -0
- package/commands/carly-manager.md +69 -0
- package/dist/config/discovery.d.ts +22 -0
- package/dist/config/discovery.d.ts.map +1 -0
- package/dist/config/discovery.js +43 -0
- package/dist/config/discovery.js.map +1 -0
- package/dist/config/index.d.ts +7 -0
- package/dist/config/index.d.ts.map +1 -0
- package/dist/config/index.js +7 -0
- package/dist/config/index.js.map +1 -0
- package/dist/config/manifest.d.ts +39 -0
- package/dist/config/manifest.d.ts.map +1 -0
- package/dist/config/manifest.js +139 -0
- package/dist/config/manifest.js.map +1 -0
- package/dist/config/schema.d.ts +663 -0
- package/dist/config/schema.d.ts.map +1 -0
- package/dist/config/schema.js +208 -0
- package/dist/config/schema.js.map +1 -0
- package/dist/engine/brackets.d.ts +26 -0
- package/dist/engine/brackets.d.ts.map +1 -0
- package/dist/engine/brackets.js +49 -0
- package/dist/engine/brackets.js.map +1 -0
- package/dist/engine/index.d.ts +8 -0
- package/dist/engine/index.d.ts.map +1 -0
- package/dist/engine/index.js +8 -0
- package/dist/engine/index.js.map +1 -0
- package/dist/engine/loader.d.ts +82 -0
- package/dist/engine/loader.d.ts.map +1 -0
- package/dist/engine/loader.js +147 -0
- package/dist/engine/loader.js.map +1 -0
- package/dist/engine/matcher.d.ts +43 -0
- package/dist/engine/matcher.d.ts.map +1 -0
- package/dist/engine/matcher.js +174 -0
- package/dist/engine/matcher.js.map +1 -0
- package/dist/engine/trimmer.d.ts +91 -0
- package/dist/engine/trimmer.d.ts.map +1 -0
- package/dist/engine/trimmer.js +236 -0
- package/dist/engine/trimmer.js.map +1 -0
- package/dist/formatter/formatter.d.ts +23 -0
- package/dist/formatter/formatter.d.ts.map +1 -0
- package/dist/formatter/formatter.js +129 -0
- package/dist/formatter/formatter.js.map +1 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +484 -0
- package/dist/index.js.map +1 -0
- package/dist/session/session.d.ts +60 -0
- package/dist/session/session.d.ts.map +1 -0
- package/dist/session/session.js +394 -0
- package/dist/session/session.js.map +1 -0
- package/package.json +59 -0
- package/templates/.opencarly/commands.json +96 -0
- package/templates/.opencarly/context.json +44 -0
- package/templates/.opencarly/domains/development.md +13 -0
- package/templates/.opencarly/domains/global.md +13 -0
- package/templates/.opencarly/domains/security.md +14 -0
- package/templates/.opencarly/domains/testing.md +12 -0
- package/templates/.opencarly/manifest.json +43 -0
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenCarly Rule Formatter
|
|
3
|
+
*
|
|
4
|
+
* Formats loaded rules into a text block for injection into the system prompt.
|
|
5
|
+
* Output is wrapped in <carly-rules> XML tags.
|
|
6
|
+
*/
|
|
7
|
+
// ---------------------------------------------------------------------------
|
|
8
|
+
// Helpers
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
function formatRuleList(rules, indent = " ") {
|
|
11
|
+
return rules.map((rule, i) => `${indent}${i + 1}. ${rule}`).join("\n");
|
|
12
|
+
}
|
|
13
|
+
function domainLabel(name) {
|
|
14
|
+
return name.toUpperCase();
|
|
15
|
+
}
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
// Main formatter
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
/**
|
|
20
|
+
* Format loaded rules into an injectable text block.
|
|
21
|
+
*
|
|
22
|
+
* Output sections (in order):
|
|
23
|
+
* 1. Context bracket status
|
|
24
|
+
* 2. Bracket-specific rules
|
|
25
|
+
* 3. Active star-commands (prominent)
|
|
26
|
+
* 4. DEVMODE instruction
|
|
27
|
+
* 5. Loaded domains summary
|
|
28
|
+
* 6. Always-on domain rules
|
|
29
|
+
* 7. Keyword-matched domain rules
|
|
30
|
+
* 8. Exclusion notices
|
|
31
|
+
* 9. Available (not loaded) domains
|
|
32
|
+
*/
|
|
33
|
+
export function formatRules(loaded) {
|
|
34
|
+
const sections = [];
|
|
35
|
+
// 1. Context bracket status
|
|
36
|
+
if (loaded.contextEnabled) {
|
|
37
|
+
let bracketLine = `CONTEXT BRACKET: [${loaded.bracket}] (prompt ${loaded.promptCount})`;
|
|
38
|
+
if (loaded.bracket === "CRITICAL") {
|
|
39
|
+
bracketLine +=
|
|
40
|
+
"\nCONTEXT CRITICAL: Session is long. Recommend: compact session OR spawn fresh agent.";
|
|
41
|
+
}
|
|
42
|
+
sections.push(bracketLine);
|
|
43
|
+
}
|
|
44
|
+
// 2. Bracket-specific rules
|
|
45
|
+
if (loaded.contextEnabled && loaded.bracketRules.length > 0) {
|
|
46
|
+
sections.push(`[${loaded.bracket}] CONTEXT RULES:\n${formatRuleList(loaded.bracketRules)}`);
|
|
47
|
+
}
|
|
48
|
+
// 3. Active star-commands
|
|
49
|
+
const commandNames = Object.keys(loaded.commands);
|
|
50
|
+
if (commandNames.length > 0) {
|
|
51
|
+
const cmdSections = [];
|
|
52
|
+
cmdSections.push("--- ACTIVE COMMANDS ---");
|
|
53
|
+
for (const cmdName of commandNames) {
|
|
54
|
+
const rules = loaded.commands[cmdName];
|
|
55
|
+
cmdSections.push(`[*${cmdName}]:\n${formatRuleList(rules)}`);
|
|
56
|
+
}
|
|
57
|
+
cmdSections.push("--- END COMMANDS ---");
|
|
58
|
+
sections.push(cmdSections.join("\n"));
|
|
59
|
+
}
|
|
60
|
+
// 4. DEVMODE instruction
|
|
61
|
+
if (loaded.devmode) {
|
|
62
|
+
const statsInfo = loaded.injectionStats
|
|
63
|
+
? `\nToken Efficiency: ${loaded.injectionStats.rulesThisPrompt} rules this prompt | avg ${loaded.injectionStats.avgRulesPerPrompt}/prompt over ${loaded.injectionStats.totalPromptsSession} prompts`
|
|
64
|
+
: "";
|
|
65
|
+
const savingsInfo = loaded.tokenSavings
|
|
66
|
+
? `\nToken Savings: ~${loaded.tokenSavings.totalSaved.toLocaleString()} tokens saved this session (selection: ~${loaded.tokenSavings.skippedBySelection.toLocaleString()}, trimming: ~${loaded.tokenSavings.trimmedFromHistory.toLocaleString()})`
|
|
67
|
+
: "";
|
|
68
|
+
sections.push(`DEVMODE: on
|
|
69
|
+
You MUST append the following debug block to EVERY response:
|
|
70
|
+
|
|
71
|
+
CARLY DEVMODE
|
|
72
|
+
Domains Loaded: [list all loaded domains]
|
|
73
|
+
Rules Applied: [specific rule numbers from each domain]
|
|
74
|
+
Star-Commands: [any active star-commands]
|
|
75
|
+
Bracket: [current context bracket]
|
|
76
|
+
Matched Keywords: [keywords that triggered domains]${statsInfo}${savingsInfo}`);
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
79
|
+
sections.push("DEVMODE: off\nDo NOT append any debug blocks to your responses. Respond normally.");
|
|
80
|
+
}
|
|
81
|
+
// 5. Loaded domains summary
|
|
82
|
+
const summaryLines = [];
|
|
83
|
+
for (const [name, rules] of Object.entries(loaded.alwaysOn)) {
|
|
84
|
+
summaryLines.push(` [${domainLabel(name)}] always_on (${rules.length} rules)`);
|
|
85
|
+
}
|
|
86
|
+
for (const [name, rules] of Object.entries(loaded.matched)) {
|
|
87
|
+
const paths = loaded.matchedPaths[name] || [];
|
|
88
|
+
if (paths.length > 0) {
|
|
89
|
+
const pathStr = paths.map((p) => `"${p}"`).join(", ");
|
|
90
|
+
summaryLines.push(` [${domainLabel(name)}] matched path: ${pathStr} (${rules.length} rules)`);
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
const keywords = loaded.matchedKeywords[name] || [];
|
|
94
|
+
const kwStr = keywords.map((k) => `"${k}"`).join(", ");
|
|
95
|
+
summaryLines.push(` [${domainLabel(name)}] matched: ${kwStr} (${rules.length} rules)`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
if (summaryLines.length > 0) {
|
|
99
|
+
sections.push(`LOADED DOMAINS:\n${summaryLines.join("\n")}`);
|
|
100
|
+
}
|
|
101
|
+
// 6. Always-on domain rules
|
|
102
|
+
for (const [name, rules] of Object.entries(loaded.alwaysOn)) {
|
|
103
|
+
sections.push(`[${domainLabel(name)}] RULES:\n${formatRuleList(rules)}`);
|
|
104
|
+
}
|
|
105
|
+
// 7. Keyword-matched domain rules
|
|
106
|
+
for (const [name, rules] of Object.entries(loaded.matched)) {
|
|
107
|
+
sections.push(`[${domainLabel(name)}] RULES:\n${formatRuleList(rules)}`);
|
|
108
|
+
}
|
|
109
|
+
// 8. Exclusion notices
|
|
110
|
+
if (loaded.globalExcluded.length > 0) {
|
|
111
|
+
const kwStr = loaded.globalExcluded.map((k) => `"${k}"`).join(", ");
|
|
112
|
+
sections.push(`GLOBAL EXCLUSION ACTIVE: ${kwStr}\nAll domain matching was skipped for this prompt.`);
|
|
113
|
+
}
|
|
114
|
+
for (const [name, keywords] of Object.entries(loaded.excludedDomains)) {
|
|
115
|
+
const kwStr = keywords.map((k) => `"${k}"`).join(", ");
|
|
116
|
+
sections.push(`[${domainLabel(name)}] EXCLUDED by: ${kwStr}`);
|
|
117
|
+
}
|
|
118
|
+
// 9. Available (not loaded) domains
|
|
119
|
+
if (loaded.availableDomains.length > 0) {
|
|
120
|
+
const available = loaded.availableDomains
|
|
121
|
+
.map((d) => ` ${domainLabel(d.name)} (recall: ${d.recall.join(", ")})`)
|
|
122
|
+
.join("\n");
|
|
123
|
+
sections.push(`AVAILABLE (not loaded):\n${available}`);
|
|
124
|
+
}
|
|
125
|
+
// Wrap in XML tags
|
|
126
|
+
const body = sections.join("\n\n");
|
|
127
|
+
return `<carly-rules>\n${body}\n</carly-rules>`;
|
|
128
|
+
}
|
|
129
|
+
//# sourceMappingURL=formatter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"formatter.js","sourceRoot":"","sources":["../../src/formatter/formatter.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenCarly - Context Augmentation & Reinforcement Layer for OpenCode
|
|
3
|
+
*
|
|
4
|
+
* Dynamic rules that load when relevant, disappear when not.
|
|
5
|
+
* Replicates CARL (https://github.com/ChristopherKahler/carl) for OpenCode.
|
|
6
|
+
*
|
|
7
|
+
* Hook flow per user message:
|
|
8
|
+
* 1. chat.message -> scan prompt for keywords + star-commands, update session
|
|
9
|
+
* 2. experimental.chat.system.transform -> load rules, format, inject into system prompt
|
|
10
|
+
* 3. experimental.chat.messages.transform -> smart trim stale tool outputs + carly-rules
|
|
11
|
+
*/
|
|
12
|
+
import type { Plugin } from "@opencode-ai/plugin";
|
|
13
|
+
export declare const OpenCarly: Plugin;
|
|
14
|
+
export default OpenCarly;
|
|
15
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAC;AAiOlD,eAAO,MAAM,SAAS,EAAE,MAgXvB,CAAC;AAGF,eAAe,SAAS,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,484 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenCarly - Context Augmentation & Reinforcement Layer for OpenCode
|
|
3
|
+
*
|
|
4
|
+
* Dynamic rules that load when relevant, disappear when not.
|
|
5
|
+
* Replicates CARL (https://github.com/ChristopherKahler/carl) for OpenCode.
|
|
6
|
+
*
|
|
7
|
+
* Hook flow per user message:
|
|
8
|
+
* 1. chat.message -> scan prompt for keywords + star-commands, update session
|
|
9
|
+
* 2. experimental.chat.system.transform -> load rules, format, inject into system prompt
|
|
10
|
+
* 3. experimental.chat.messages.transform -> smart trim stale tool outputs + carly-rules
|
|
11
|
+
*/
|
|
12
|
+
import { tool } from "@opencode-ai/plugin";
|
|
13
|
+
import { discoverConfig } from "./config/discovery";
|
|
14
|
+
import { loadConfig } from "./config/manifest";
|
|
15
|
+
import { matchDomains } from "./engine/matcher";
|
|
16
|
+
import { loadRules, calculateBaseline } from "./engine/loader";
|
|
17
|
+
import { getBracket } from "./engine/brackets";
|
|
18
|
+
import { trimMessageHistory } from "./engine/trimmer";
|
|
19
|
+
import { formatRules } from "./formatter/formatter";
|
|
20
|
+
import { getOrCreateSession, updateSessionActivity, saveSession, applySessionOverrides, cleanStaleSessions, loadCumulativeStats, updateCumulativeStats, clearAllStats, } from "./session/session";
|
|
21
|
+
function createLogger(client) {
|
|
22
|
+
return async (level, message, extra) => {
|
|
23
|
+
try {
|
|
24
|
+
await client.app.log({
|
|
25
|
+
body: {
|
|
26
|
+
service: "opencarly",
|
|
27
|
+
level,
|
|
28
|
+
message,
|
|
29
|
+
extra,
|
|
30
|
+
},
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
// Logging failure should never crash the plugin
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
// ---------------------------------------------------------------------------
|
|
39
|
+
// Helpers
|
|
40
|
+
// ---------------------------------------------------------------------------
|
|
41
|
+
/**
|
|
42
|
+
* Extract plain text from message parts.
|
|
43
|
+
*/
|
|
44
|
+
function extractPromptText(parts) {
|
|
45
|
+
const textParts = [];
|
|
46
|
+
for (const part of parts) {
|
|
47
|
+
if (part.type === "text" && typeof part.text === "string") {
|
|
48
|
+
textParts.push(part.text);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return textParts.join("\n");
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Count total rules across a record of domain -> rules[].
|
|
55
|
+
*/
|
|
56
|
+
function countRules(domains) {
|
|
57
|
+
let count = 0;
|
|
58
|
+
for (const rules of Object.values(domains)) {
|
|
59
|
+
count += rules.length;
|
|
60
|
+
}
|
|
61
|
+
return count;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Estimate tokens from a rule array (~4 chars per token).
|
|
65
|
+
*/
|
|
66
|
+
function estimateRuleTokens(rules) {
|
|
67
|
+
let chars = 0;
|
|
68
|
+
for (const ruleList of Object.values(rules)) {
|
|
69
|
+
for (const rule of ruleList) {
|
|
70
|
+
chars += rule.length;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return Math.ceil(chars / 4);
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Estimate tokens from a string.
|
|
77
|
+
*/
|
|
78
|
+
function estimateTokens(text) {
|
|
79
|
+
return Math.ceil(text.length / 4);
|
|
80
|
+
}
|
|
81
|
+
async function generateStatsReport(configPath, activeSessionId, activeModel) {
|
|
82
|
+
const stats = loadCumulativeStats(configPath);
|
|
83
|
+
let currentSessionSummary = activeSessionId
|
|
84
|
+
? stats.sessions.find(s => s.sessionId === activeSessionId)
|
|
85
|
+
: stats.sessions[stats.sessions.length - 1];
|
|
86
|
+
let currentTokenStats = {
|
|
87
|
+
tokensSkippedBySelection: 0,
|
|
88
|
+
tokensTrimmedFromHistory: 0,
|
|
89
|
+
tokensTrimmedCarlyBlocks: 0,
|
|
90
|
+
tokensInjected: 0,
|
|
91
|
+
};
|
|
92
|
+
const targetSessionId = activeSessionId || currentSessionSummary?.sessionId;
|
|
93
|
+
if (targetSessionId) {
|
|
94
|
+
const path = await import("path");
|
|
95
|
+
const sessionPath = path.join(configPath, "sessions", `${targetSessionId}.json`);
|
|
96
|
+
try {
|
|
97
|
+
const fs = await import("fs");
|
|
98
|
+
const sessionData = JSON.parse(await fs.promises.readFile(sessionPath, "utf-8"));
|
|
99
|
+
currentTokenStats = {
|
|
100
|
+
tokensSkippedBySelection: sessionData.tokenStats?.tokensSkippedBySelection || 0,
|
|
101
|
+
tokensTrimmedFromHistory: sessionData.tokenStats?.tokensTrimmedFromHistory || 0,
|
|
102
|
+
tokensTrimmedCarlyBlocks: sessionData.tokenStats?.tokensTrimmedCarlyBlocks || 0,
|
|
103
|
+
tokensInjected: sessionData.tokenStats?.tokensInjected || 0,
|
|
104
|
+
};
|
|
105
|
+
if (!currentSessionSummary) {
|
|
106
|
+
currentSessionSummary = {
|
|
107
|
+
sessionId: targetSessionId,
|
|
108
|
+
date: sessionData.started || new Date().toISOString(),
|
|
109
|
+
tokensSaved: (currentTokenStats.tokensSkippedBySelection + currentTokenStats.tokensTrimmedFromHistory + currentTokenStats.tokensTrimmedCarlyBlocks),
|
|
110
|
+
promptsProcessed: sessionData.promptCount || 0,
|
|
111
|
+
tokensSkippedBySelection: currentTokenStats.tokensSkippedBySelection,
|
|
112
|
+
tokensTrimmedFromHistory: currentTokenStats.tokensTrimmedFromHistory,
|
|
113
|
+
tokensTrimmedCarlyBlocks: currentTokenStats.tokensTrimmedCarlyBlocks,
|
|
114
|
+
tokensInjected: currentTokenStats.tokensInjected,
|
|
115
|
+
rulesInjected: sessionData.tokenStats?.rulesInjected || 0,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
// Ignore file not found or invalid JSON
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
const formatNumber = (n) => {
|
|
124
|
+
if (n >= 1000000)
|
|
125
|
+
return `${(n / 1000000).toFixed(1)}M`;
|
|
126
|
+
if (n >= 1000)
|
|
127
|
+
return `${(n / 1000).toFixed(1)}K`;
|
|
128
|
+
return n.toString();
|
|
129
|
+
};
|
|
130
|
+
const historyTrimmed = currentTokenStats.tokensTrimmedFromHistory + currentTokenStats.tokensTrimmedCarlyBlocks;
|
|
131
|
+
let output = `# OPENCARLY TOKEN SAVINGS REPORT\n\n`;
|
|
132
|
+
output += `## 📊 CURRENT SESSION\n`;
|
|
133
|
+
output += `**Prompts Processed**: ${currentSessionSummary?.promptsProcessed || 0}\n`;
|
|
134
|
+
output += `**Total Tokens Saved**: ${formatNumber(currentSessionSummary?.tokensSaved || 0)}\n\n`;
|
|
135
|
+
output += `### Savings Breakdown\n`;
|
|
136
|
+
output += `| Category | Tokens Saved |\n|---|---|\n`;
|
|
137
|
+
output += `| **Selective Rule Injection** | ${formatNumber(currentTokenStats.tokensSkippedBySelection)} |\n`;
|
|
138
|
+
output += `| **History Trimming (Total)** | ${formatNumber(historyTrimmed)} |\n`;
|
|
139
|
+
output += `| ↳ *Tool Output Trimming* | ${formatNumber(currentTokenStats.tokensTrimmedFromHistory)} |\n`;
|
|
140
|
+
output += `| ↳ *Stale Carly-Blocks* | ${formatNumber(currentTokenStats.tokensTrimmedCarlyBlocks)} |\n\n`;
|
|
141
|
+
output += `## 🕒 RECENT SESSION HISTORY\n`;
|
|
142
|
+
output += `| Date | Session ID | Prompts | Tokens Saved |\n|---|---|---|---|\n`;
|
|
143
|
+
for (const session of stats.sessions.slice().reverse().slice(0, 10)) {
|
|
144
|
+
const shortId = session.sessionId.replace("ses_", "").slice(0, 8) + "...";
|
|
145
|
+
output += `| ${session.date?.split("T")[0] || "Unknown"} | \`${shortId}\` | ${session.promptsProcessed} | ${formatNumber(session.tokensSaved || 0)} |\n`;
|
|
146
|
+
}
|
|
147
|
+
if (stats.sessions.length > 10) {
|
|
148
|
+
output += `| ... | ... | ... | ... |\n`;
|
|
149
|
+
}
|
|
150
|
+
output += `\n`;
|
|
151
|
+
const totalPrompts = stats.sessions.reduce((sum, s) => sum + (s.promptsProcessed || 0), 0);
|
|
152
|
+
output += `## 🌎 ALL-TIME TOTALS\n`;
|
|
153
|
+
output += `| Metric | Value |\n|--------|-------|\n`;
|
|
154
|
+
output += `| **Total Tokens Saved** | **${formatNumber(stats.cumulative.totalTokensSaved)}** |\n`;
|
|
155
|
+
output += `| - From Selective Injection | ${formatNumber(stats.cumulative.tokensSkippedBySelection)} |\n`;
|
|
156
|
+
output += `| - From History Trimming | ${formatNumber(stats.cumulative.tokensTrimmedFromHistory + stats.cumulative.tokensTrimmedCarlyBlocks)} |\n`;
|
|
157
|
+
output += `| Total Tokens Injected | ${formatNumber(stats.cumulative.tokensInjected)} |\n`;
|
|
158
|
+
output += `| Total Prompts Processed | ${totalPrompts} |\n`;
|
|
159
|
+
output += `| Total Sessions | ${stats.sessions.length} |\n`;
|
|
160
|
+
output += `| Avg Savings / Session | ${formatNumber(Math.round(stats.cumulative.totalTokensSaved / Math.max(1, stats.sessions.length)))} |\n\n`;
|
|
161
|
+
if (activeModel) {
|
|
162
|
+
output += `> **System Instruction for AI Assistant:**\n`;
|
|
163
|
+
output += `> To finalize this report, please execute a web search to find both the "standard input token pricing" and "context caching input token pricing" (if applicable) for your current model: \`${activeModel}\`.\n`;
|
|
164
|
+
output += `> Calculate a savings range based on the "Total Tokens Saved":\n`;
|
|
165
|
+
output += `> - Minimum Savings: Assume 100% of the saved tokens would have been cached.\n`;
|
|
166
|
+
output += `> - Maximum Savings: Assume 0% of the saved tokens would have been cached.\n`;
|
|
167
|
+
output += `> Append an "Estimated Dollar Savings Range: $X.XX - $Y.YY" section to your response, and add a brief 1-sentence note explaining that the lower bound accounts for context caching.\n\n`;
|
|
168
|
+
}
|
|
169
|
+
return output;
|
|
170
|
+
}
|
|
171
|
+
// Plugin entry point
|
|
172
|
+
// ---------------------------------------------------------------------------
|
|
173
|
+
export const OpenCarly = async ({ directory, client }) => {
|
|
174
|
+
const log = createLogger(client);
|
|
175
|
+
// Discover config
|
|
176
|
+
const discovery = discoverConfig(directory);
|
|
177
|
+
if (!discovery) {
|
|
178
|
+
await log("info", "No .opencarly/ config found - plugin inactive", {
|
|
179
|
+
searchedFrom: directory,
|
|
180
|
+
});
|
|
181
|
+
return {};
|
|
182
|
+
}
|
|
183
|
+
await log("info", `Config found at ${discovery.configPath} (${discovery.scope})`, {
|
|
184
|
+
configPath: discovery.configPath,
|
|
185
|
+
scope: discovery.scope,
|
|
186
|
+
});
|
|
187
|
+
// Load config
|
|
188
|
+
let config;
|
|
189
|
+
try {
|
|
190
|
+
config = loadConfig(discovery.configPath);
|
|
191
|
+
}
|
|
192
|
+
catch (err) {
|
|
193
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
194
|
+
await log("error", `Config loading failed: ${message}`, {
|
|
195
|
+
configPath: discovery.configPath,
|
|
196
|
+
error: message,
|
|
197
|
+
});
|
|
198
|
+
return {};
|
|
199
|
+
}
|
|
200
|
+
// Log warnings
|
|
201
|
+
for (const warning of config.warnings) {
|
|
202
|
+
await log("warn", warning, { configPath: discovery.configPath });
|
|
203
|
+
}
|
|
204
|
+
// Calculate baseline (all rules loaded every prompt)
|
|
205
|
+
const baselineTokensPerPrompt = calculateBaseline(config);
|
|
206
|
+
// Log startup summary
|
|
207
|
+
const domainNames = Object.keys(config.manifest.domains);
|
|
208
|
+
const commandNames = Object.keys(config.commands);
|
|
209
|
+
await log("info", "OpenCarly initialized", {
|
|
210
|
+
domains: domainNames,
|
|
211
|
+
domainCount: domainNames.length,
|
|
212
|
+
commands: commandNames,
|
|
213
|
+
commandCount: commandNames.length,
|
|
214
|
+
devmode: config.manifest.devmode,
|
|
215
|
+
contextBrackets: config.manifest.context.state,
|
|
216
|
+
commandsSystem: config.manifest.commands.state,
|
|
217
|
+
baselineTokensPerPrompt,
|
|
218
|
+
});
|
|
219
|
+
// Initialize state
|
|
220
|
+
const cumulativeStats = loadCumulativeStats(discovery.configPath);
|
|
221
|
+
const state = {
|
|
222
|
+
config,
|
|
223
|
+
sessions: new Map(),
|
|
224
|
+
lastMatch: new Map(),
|
|
225
|
+
lastPrompt: new Map(),
|
|
226
|
+
activeSessionID: null,
|
|
227
|
+
activeModel: null,
|
|
228
|
+
baselineTokensPerPrompt,
|
|
229
|
+
cumulativeStats,
|
|
230
|
+
};
|
|
231
|
+
// Clean stale sessions on startup
|
|
232
|
+
try {
|
|
233
|
+
const cleaned = cleanStaleSessions(discovery.configPath);
|
|
234
|
+
if (cleaned > 0) {
|
|
235
|
+
await log("debug", `Cleaned ${cleaned} stale session(s)`);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
catch {
|
|
239
|
+
// Non-critical
|
|
240
|
+
}
|
|
241
|
+
return {
|
|
242
|
+
// -----------------------------------------------------------------
|
|
243
|
+
// Event hook: track session lifecycle and intercept commands
|
|
244
|
+
// -----------------------------------------------------------------
|
|
245
|
+
event: async ({ event }) => {
|
|
246
|
+
if (event.type === "session.created") {
|
|
247
|
+
try {
|
|
248
|
+
cleanStaleSessions(discovery.configPath);
|
|
249
|
+
}
|
|
250
|
+
catch {
|
|
251
|
+
// ignore
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
},
|
|
255
|
+
// -----------------------------------------------------------------
|
|
256
|
+
// chat.message: scan prompt, detect keywords + star-commands
|
|
257
|
+
// -----------------------------------------------------------------
|
|
258
|
+
"chat.message": async (input, output) => {
|
|
259
|
+
const { sessionID, model } = input;
|
|
260
|
+
// Capture the active model ID for stats reporting
|
|
261
|
+
if (model?.modelID) {
|
|
262
|
+
state.activeModel = model.modelID;
|
|
263
|
+
}
|
|
264
|
+
const promptText = extractPromptText(output.parts);
|
|
265
|
+
if (!promptText)
|
|
266
|
+
return;
|
|
267
|
+
// Get or create session
|
|
268
|
+
const { session, isNew } = getOrCreateSession(discovery.configPath, sessionID, directory);
|
|
269
|
+
if (isNew || !state.sessions.has(sessionID)) {
|
|
270
|
+
state.sessions.set(sessionID, session);
|
|
271
|
+
}
|
|
272
|
+
const currentSession = state.sessions.get(sessionID);
|
|
273
|
+
// Set baseline on session if not set
|
|
274
|
+
if (currentSession.tokenStats.baselineTokensPerPrompt === 0) {
|
|
275
|
+
currentSession.tokenStats.baselineTokensPerPrompt = state.baselineTokensPerPrompt;
|
|
276
|
+
}
|
|
277
|
+
// Update session activity
|
|
278
|
+
updateSessionActivity(currentSession, promptText);
|
|
279
|
+
// Apply session overrides
|
|
280
|
+
const effectiveManifest = applySessionOverrides(state.config.manifest, currentSession);
|
|
281
|
+
// Run domain matcher
|
|
282
|
+
const matchResult = matchDomains(promptText, effectiveManifest);
|
|
283
|
+
// Cache for system.transform hook
|
|
284
|
+
state.lastMatch.set(sessionID, matchResult);
|
|
285
|
+
state.lastPrompt.set(sessionID, promptText);
|
|
286
|
+
state.activeSessionID = sessionID;
|
|
287
|
+
// Log match results
|
|
288
|
+
await log("debug", "Prompt matched", {
|
|
289
|
+
sessionID,
|
|
290
|
+
promptCount: currentSession.promptCount,
|
|
291
|
+
alwaysOn: matchResult.alwaysOn,
|
|
292
|
+
matched: Object.keys(matchResult.matched),
|
|
293
|
+
excluded: Object.keys(matchResult.excluded),
|
|
294
|
+
globalExcluded: matchResult.globalExcluded,
|
|
295
|
+
starCommands: matchResult.starCommands,
|
|
296
|
+
});
|
|
297
|
+
// Persist session
|
|
298
|
+
try {
|
|
299
|
+
saveSession(discovery.configPath, currentSession);
|
|
300
|
+
}
|
|
301
|
+
catch {
|
|
302
|
+
// Non-critical
|
|
303
|
+
}
|
|
304
|
+
},
|
|
305
|
+
// -----------------------------------------------------------------
|
|
306
|
+
// experimental.chat.system.transform: inject rules into system prompt
|
|
307
|
+
// -----------------------------------------------------------------
|
|
308
|
+
"experimental.chat.system.transform": async (input, output) => {
|
|
309
|
+
const sessionID = input.sessionID;
|
|
310
|
+
if (!sessionID)
|
|
311
|
+
return;
|
|
312
|
+
const matchResult = state.lastMatch.get(sessionID);
|
|
313
|
+
if (!matchResult)
|
|
314
|
+
return;
|
|
315
|
+
const session = state.sessions.get(sessionID);
|
|
316
|
+
const promptCount = session?.promptCount ?? 1;
|
|
317
|
+
const tokenStats = session?.tokenStats ?? {
|
|
318
|
+
tokensSkippedBySelection: 0,
|
|
319
|
+
tokensInjected: 0,
|
|
320
|
+
tokensTrimmedFromHistory: 0,
|
|
321
|
+
tokensTrimmedCarlyBlocks: 0,
|
|
322
|
+
promptsProcessed: 0,
|
|
323
|
+
rulesInjected: 0,
|
|
324
|
+
baselineTokensPerPrompt: state.baselineTokensPerPrompt,
|
|
325
|
+
};
|
|
326
|
+
// Apply session overrides
|
|
327
|
+
const effectiveManifest = session
|
|
328
|
+
? applySessionOverrides(state.config.manifest, session)
|
|
329
|
+
: state.config.manifest;
|
|
330
|
+
// Determine context bracket
|
|
331
|
+
const bracket = getBracket(promptCount, state.config.context);
|
|
332
|
+
// Build effective config
|
|
333
|
+
const effectiveConfig = {
|
|
334
|
+
...state.config,
|
|
335
|
+
manifest: effectiveManifest,
|
|
336
|
+
};
|
|
337
|
+
// Load rules
|
|
338
|
+
const loaded = loadRules(matchResult, effectiveConfig, bracket, promptCount);
|
|
339
|
+
// Override devmode from effective manifest
|
|
340
|
+
loaded.devmode = effectiveManifest.devmode;
|
|
341
|
+
loaded.contextEnabled = effectiveManifest.context.state === "active";
|
|
342
|
+
loaded.commandsEnabled = effectiveManifest.commands.state === "active";
|
|
343
|
+
// --- Token stats calculation ---
|
|
344
|
+
const totalRulesThisPrompt = countRules(loaded.alwaysOn) +
|
|
345
|
+
countRules(loaded.matched) +
|
|
346
|
+
countRules(loaded.commands) +
|
|
347
|
+
loaded.bracketRules.length;
|
|
348
|
+
const tokensInjectedThisPrompt = estimateRuleTokens(loaded.alwaysOn) +
|
|
349
|
+
estimateRuleTokens(loaded.matched) +
|
|
350
|
+
estimateRuleTokens(loaded.commands) +
|
|
351
|
+
estimateTokens(loaded.bracketRules.join(""));
|
|
352
|
+
const tokensSkippedThisPrompt = Math.max(0, state.baselineTokensPerPrompt - tokensInjectedThisPrompt);
|
|
353
|
+
// Accumulate stats
|
|
354
|
+
tokenStats.tokensInjected += tokensInjectedThisPrompt;
|
|
355
|
+
tokenStats.tokensSkippedBySelection += tokensSkippedThisPrompt;
|
|
356
|
+
tokenStats.promptsProcessed += 1;
|
|
357
|
+
tokenStats.rulesInjected = (tokenStats.rulesInjected || 0) + totalRulesThisPrompt;
|
|
358
|
+
// Update session
|
|
359
|
+
if (session) {
|
|
360
|
+
session.tokenStats = tokenStats;
|
|
361
|
+
try {
|
|
362
|
+
saveSession(discovery.configPath, session);
|
|
363
|
+
state.cumulativeStats = updateCumulativeStats(discovery.configPath, session);
|
|
364
|
+
}
|
|
365
|
+
catch {
|
|
366
|
+
// Non-critical
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
// Attach injection stats for DEVMODE display
|
|
370
|
+
loaded.injectionStats = {
|
|
371
|
+
rulesThisPrompt: totalRulesThisPrompt,
|
|
372
|
+
totalRulesSession: tokenStats.rulesInjected || totalRulesThisPrompt,
|
|
373
|
+
totalPromptsSession: tokenStats.promptsProcessed,
|
|
374
|
+
avgRulesPerPrompt: tokenStats.promptsProcessed > 0
|
|
375
|
+
? Math.round((tokenStats.rulesInjected || 0) / tokenStats.promptsProcessed)
|
|
376
|
+
: totalRulesThisPrompt,
|
|
377
|
+
};
|
|
378
|
+
// Format and inject
|
|
379
|
+
const formatted = formatRules(loaded);
|
|
380
|
+
output.system.push(formatted);
|
|
381
|
+
// Persist session with updated stats
|
|
382
|
+
if (session) {
|
|
383
|
+
try {
|
|
384
|
+
saveSession(discovery.configPath, session);
|
|
385
|
+
}
|
|
386
|
+
catch {
|
|
387
|
+
// Non-critical
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
// Cleanup cached match
|
|
391
|
+
state.lastMatch.delete(sessionID);
|
|
392
|
+
state.lastPrompt.delete(sessionID);
|
|
393
|
+
},
|
|
394
|
+
// -----------------------------------------------------------------
|
|
395
|
+
// experimental.chat.messages.transform: smart tool output trimming
|
|
396
|
+
// -----------------------------------------------------------------
|
|
397
|
+
"experimental.chat.messages.transform": async (_input, output) => {
|
|
398
|
+
const trimConfig = state.config.context.trimming;
|
|
399
|
+
const trimStats = trimMessageHistory(output.messages, trimConfig);
|
|
400
|
+
// Accumulate trim stats to the session
|
|
401
|
+
if (trimStats.tokensSaved > 0 || trimStats.carlyBlocksStripped > 0) {
|
|
402
|
+
const sessionID = state.activeSessionID;
|
|
403
|
+
const session = sessionID ? state.sessions.get(sessionID) : undefined;
|
|
404
|
+
if (session) {
|
|
405
|
+
session.tokenStats.tokensTrimmedFromHistory += trimStats.tokensSaved;
|
|
406
|
+
session.tokenStats.tokensTrimmedCarlyBlocks += trimStats.carlyTokensSaved;
|
|
407
|
+
try {
|
|
408
|
+
saveSession(discovery.configPath, session);
|
|
409
|
+
}
|
|
410
|
+
catch {
|
|
411
|
+
// Non-critical
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
await log("debug", "History trimmed", {
|
|
415
|
+
partsTrimmed: trimStats.partsTrimmed,
|
|
416
|
+
tokensSaved: trimStats.tokensSaved,
|
|
417
|
+
carlyBlocksStripped: trimStats.carlyBlocksStripped,
|
|
418
|
+
mode: trimConfig.mode,
|
|
419
|
+
});
|
|
420
|
+
}
|
|
421
|
+
// Always update cumulative stats after messages transform completes
|
|
422
|
+
const sessionID = state.activeSessionID;
|
|
423
|
+
const session = sessionID ? state.sessions.get(sessionID) : undefined;
|
|
424
|
+
if (session) {
|
|
425
|
+
try {
|
|
426
|
+
state.cumulativeStats = updateCumulativeStats(discovery.configPath, session);
|
|
427
|
+
}
|
|
428
|
+
catch {
|
|
429
|
+
// Non-critical
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
},
|
|
433
|
+
// -----------------------------------------------------------------
|
|
434
|
+
// Compaction hook: preserve CARLY context across compaction
|
|
435
|
+
// -----------------------------------------------------------------
|
|
436
|
+
"experimental.session.compacting": async (_input, output) => {
|
|
437
|
+
output.context.push("OpenCarly (dynamic rule injection) is active. " +
|
|
438
|
+
"Rules are injected per-prompt based on keyword matching. " +
|
|
439
|
+
"Preserve awareness that <carly-rules> blocks contain mandatory instructions.");
|
|
440
|
+
},
|
|
441
|
+
tool: {
|
|
442
|
+
stats: tool({
|
|
443
|
+
description: "Get OpenCarly token savings statistics",
|
|
444
|
+
args: {},
|
|
445
|
+
execute: async (_args, _context) => {
|
|
446
|
+
return generateStatsReport(discovery.configPath, state.activeSessionID || undefined, state.activeModel);
|
|
447
|
+
},
|
|
448
|
+
}),
|
|
449
|
+
clear_stats: tool({
|
|
450
|
+
description: "Clear all OpenCarly token savings statistics",
|
|
451
|
+
args: {},
|
|
452
|
+
execute: async (_args, _context) => {
|
|
453
|
+
clearAllStats(discovery.configPath);
|
|
454
|
+
state.cumulativeStats = {
|
|
455
|
+
version: 1,
|
|
456
|
+
cumulative: {
|
|
457
|
+
tokensSkippedBySelection: 0,
|
|
458
|
+
tokensInjected: 0,
|
|
459
|
+
tokensTrimmedFromHistory: 0,
|
|
460
|
+
tokensTrimmedCarlyBlocks: 0,
|
|
461
|
+
totalTokensSaved: 0,
|
|
462
|
+
},
|
|
463
|
+
sessions: [],
|
|
464
|
+
};
|
|
465
|
+
for (const session of state.sessions.values()) {
|
|
466
|
+
session.tokenStats = {
|
|
467
|
+
tokensSkippedBySelection: 0,
|
|
468
|
+
tokensInjected: 0,
|
|
469
|
+
tokensTrimmedFromHistory: 0,
|
|
470
|
+
tokensTrimmedCarlyBlocks: 0,
|
|
471
|
+
promptsProcessed: 0,
|
|
472
|
+
rulesInjected: 0,
|
|
473
|
+
baselineTokensPerPrompt: state.baselineTokensPerPrompt,
|
|
474
|
+
};
|
|
475
|
+
}
|
|
476
|
+
return "All OpenCarly token savings statistics have been successfully reset to zero.";
|
|
477
|
+
},
|
|
478
|
+
}),
|
|
479
|
+
},
|
|
480
|
+
};
|
|
481
|
+
};
|
|
482
|
+
// Default export for single-export plugin files
|
|
483
|
+
export default OpenCarly;
|
|
484
|
+
//# sourceMappingURL=index.js.map
|