strray-ai 1.22.17 โ 1.22.19
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/dist/CHANGELOG.md +19 -0
- package/dist/integrations/hermes-agent/__pycache__/__init__.cpython-313.pyc +0 -0
- package/dist/integrations/hermes-agent/__pycache__/conftest.cpython-313.pyc +0 -0
- package/dist/integrations/hermes-agent/__pycache__/schemas.cpython-313.pyc +0 -0
- package/dist/integrations/hermes-agent/__pycache__/tools.cpython-313.pyc +0 -0
- package/dist/scripts/pre-command +26 -0
- package/dist/scripts/pre-command.mjs +359 -0
- package/dist/scripts/validate-stringray-comprehensive.js +634 -0
- package/package.json +6 -2
- package/scripts/helpers/resolve-config-path.cjs +59 -0
- package/scripts/helpers/resolve-config-path.mjs +75 -0
- package/scripts/hooks/pre-command +26 -0
- package/scripts/hooks/pre-command.mjs +359 -0
- package/scripts/hooks/run-hook.js +570 -0
- package/scripts/test/test-cjs-mjs-scripts.cjs +387 -0
- package/scripts/test/test-consumer-readiness.cjs +357 -0
- package/scripts/test/test-hermes-mcp-integration.cjs +229 -0
- package/scripts/test/test-openclaw-integration.cjs +240 -0
- package/scripts/test/test-unified-framework.mjs +249 -0
- package/scripts/validate-stringray-comprehensive.js +634 -0
package/dist/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,25 @@ All notable changes to this project will be documented in this file.
|
|
|
4
4
|
|
|
5
5
|
The format is based on [Conventional Commits](https://www.conventionalcommits.org/).
|
|
6
6
|
|
|
7
|
+
## [1.22.18] - 2026-04-27
|
|
8
|
+
|
|
9
|
+
### ๐ Changes
|
|
10
|
+
|
|
11
|
+
### ๐ Bug Fixes
|
|
12
|
+
- fix: add validate script and hooks to dist/scripts, include mcps registry in build (fc059458e)
|
|
13
|
+
- fix: add mcps directory to build, fix MCP registry path resolution (c17ca67e9)
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## [1.22.18] - 2026-04-27
|
|
18
|
+
|
|
19
|
+
### ๐ Changes
|
|
20
|
+
|
|
21
|
+
### ๐ Bug Fixes
|
|
22
|
+
- fix: add mcps directory to build, fix MCP registry path resolution (c17ca67e9)
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
7
26
|
## [1.22.17] - 2026-04-27
|
|
8
27
|
|
|
9
28
|
### ๐ Changes
|
|
Binary file
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Pre-Command Hook for StringRay
|
|
3
|
+
#
|
|
4
|
+
# Checks context window usage before each command.
|
|
5
|
+
# If context is at 95%+ (configurable), auto-generates a reflection.
|
|
6
|
+
#
|
|
7
|
+
# Usage:
|
|
8
|
+
# ./scripts/hooks/pre-command # Normal call
|
|
9
|
+
# ./scripts/hooks/pre-command --dry-run # Check without generating
|
|
10
|
+
# ./scripts/hooks/pre-command --ci # CI mode (less verbose)
|
|
11
|
+
|
|
12
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
13
|
+
PROJECT_ROOT="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
|
|
14
|
+
|
|
15
|
+
export HOOK_PROJECT_ROOT="$PROJECT_ROOT"
|
|
16
|
+
export HOOK_REFLECTIONS_DIR="${REFLECTIONS_DIR:-docs/reflections}"
|
|
17
|
+
export HOOK_DRY_RUN="${HOOK_DRY_RUN:-0}"
|
|
18
|
+
|
|
19
|
+
if [[ "$1" == "--dry-run" ]]; then
|
|
20
|
+
export HOOK_DRY_RUN=1
|
|
21
|
+
elif [[ "$1" == "--ci" ]]; then
|
|
22
|
+
export CI=true
|
|
23
|
+
fi
|
|
24
|
+
|
|
25
|
+
cd "$PROJECT_ROOT"
|
|
26
|
+
exec node "$SCRIPT_DIR/pre-command.mjs" "$@"
|
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Context-Window-Aware Pre-Command Hook
|
|
4
|
+
*
|
|
5
|
+
* Checks context window usage BEFORE each command is processed.
|
|
6
|
+
* If context is at 95%+ (configurable), auto-generates a reflection
|
|
7
|
+
* BEFORE the command runs to preserve learnings before context compaction.
|
|
8
|
+
*
|
|
9
|
+
* Usage:
|
|
10
|
+
* node scripts/hooks/pre-command.mjs [--estimate-tokens N] [--threshold N]
|
|
11
|
+
*
|
|
12
|
+
* Environment:
|
|
13
|
+
* HOOK_PROJECT_ROOT - Project root directory
|
|
14
|
+
* HOOK_REFLECTIONS_DIR - Reflections directory override
|
|
15
|
+
* HOOK_DRY_RUN - If "1", only check without generating
|
|
16
|
+
* CI - If "true", CI environment (less verbose)
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { existsSync, mkdirSync, writeFileSync, readFileSync } from "fs";
|
|
20
|
+
import { join, dirname } from "path";
|
|
21
|
+
import { execSync } from "child_process";
|
|
22
|
+
|
|
23
|
+
const PROJECT_ROOT = process.env.HOOK_PROJECT_ROOT || process.cwd();
|
|
24
|
+
const REFLECTIONS_DIR = process.env.HOOK_REFLECTIONS_DIR || "docs/reflections";
|
|
25
|
+
const DRY_RUN = process.env.HOOK_DRY_RUN === "1";
|
|
26
|
+
const IS_CI = process.env.CI === "true" || process.argv.includes("--ci");
|
|
27
|
+
|
|
28
|
+
function log(msg) {
|
|
29
|
+
if (!IS_CI) console.log(msg);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function warn(msg) {
|
|
33
|
+
if (!IS_CI) console.warn(msg);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function error(msg) {
|
|
37
|
+
console.error(msg);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function loadConfig() {
|
|
41
|
+
const configPaths = [
|
|
42
|
+
join(PROJECT_ROOT, ".opencode", "strray", "features.json"),
|
|
43
|
+
join(PROJECT_ROOT, ".strray", "features.json"),
|
|
44
|
+
join(PROJECT_ROOT, "node_modules", "strray-ai", ".opencode", "strray", "features.json"),
|
|
45
|
+
];
|
|
46
|
+
|
|
47
|
+
for (const configPath of configPaths) {
|
|
48
|
+
if (existsSync(configPath)) {
|
|
49
|
+
try {
|
|
50
|
+
const content = readFileSync(configPath, "utf-8");
|
|
51
|
+
const config = JSON.parse(content);
|
|
52
|
+
if (config.context_window_reflection) {
|
|
53
|
+
return config.context_window_reflection;
|
|
54
|
+
}
|
|
55
|
+
if (config.storytelling?.reflection_triggers) {
|
|
56
|
+
return {
|
|
57
|
+
enabled: true,
|
|
58
|
+
threshold_percent: 95,
|
|
59
|
+
check_interval_commands: 5,
|
|
60
|
+
story_type: "reflection",
|
|
61
|
+
max_reflections_per_session: 3,
|
|
62
|
+
cooldown_minutes: 10,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
} catch {
|
|
66
|
+
// Try next path
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return {
|
|
72
|
+
enabled: true,
|
|
73
|
+
threshold_percent: 95,
|
|
74
|
+
check_interval_commands: 5,
|
|
75
|
+
story_type: "reflection",
|
|
76
|
+
max_reflections_per_session: 3,
|
|
77
|
+
cooldown_minutes: 10,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function estimateContextUsage() {
|
|
82
|
+
let estimatedTokens = 0;
|
|
83
|
+
|
|
84
|
+
try {
|
|
85
|
+
const historyPath = join(PROJECT_ROOT, ".opencode", "logs", "conversation-history.json");
|
|
86
|
+
if (existsSync(historyPath)) {
|
|
87
|
+
const history = JSON.parse(readFileSync(historyPath, "utf-8"));
|
|
88
|
+
if (history.messages && Array.isArray(history.messages)) {
|
|
89
|
+
estimatedTokens = history.messages.reduce((sum, msg) => {
|
|
90
|
+
const content = msg.content || "";
|
|
91
|
+
const role = msg.role || "";
|
|
92
|
+
return sum + Math.ceil((content.length + role.length) / 4);
|
|
93
|
+
}, 0);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
} catch {
|
|
97
|
+
// History not available, use CLI estimate
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (process.argv.includes("--estimate-tokens")) {
|
|
101
|
+
const idx = process.argv.indexOf("--estimate-tokens") + 1;
|
|
102
|
+
if (idx < process.argv.length) {
|
|
103
|
+
estimatedTokens = parseInt(process.argv[idx], 10) || 0;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (estimatedTokens === 0) {
|
|
108
|
+
try {
|
|
109
|
+
const output = execSync("wc -c .opencode/logs/conversation-history.json 2>/dev/null || echo 0", {
|
|
110
|
+
encoding: "utf-8",
|
|
111
|
+
cwd: PROJECT_ROOT,
|
|
112
|
+
});
|
|
113
|
+
const bytes = parseInt(output.trim().split(" ")[0], 10) || 0;
|
|
114
|
+
estimatedTokens = Math.ceil(bytes / 4);
|
|
115
|
+
} catch {
|
|
116
|
+
estimatedTokens = 5000;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return estimatedTokens;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function getContextThreshold(config) {
|
|
124
|
+
let maxTokens = 20000;
|
|
125
|
+
|
|
126
|
+
try {
|
|
127
|
+
const configPath = join(PROJECT_ROOT, ".opencode", "strray", "features.json");
|
|
128
|
+
if (existsSync(configPath)) {
|
|
129
|
+
const content = readFileSync(configPath, "utf-8");
|
|
130
|
+
const config = JSON.parse(content);
|
|
131
|
+
if (config.token_optimization?.max_context_tokens) {
|
|
132
|
+
maxTokens = config.token_optimization.max_context_tokens;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
} catch {
|
|
136
|
+
// Use default
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return Math.floor(maxTokens * (config.threshold_percent / 100));
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function checkReflectionCooldown(stateFile, config) {
|
|
143
|
+
if (!existsSync(stateFile)) {
|
|
144
|
+
return { canReflect: true, minutesSince: config.cooldown_minutes + 1 };
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
try {
|
|
148
|
+
const state = JSON.parse(readFileSync(stateFile, "utf-8"));
|
|
149
|
+
const lastReflection = state.lastContextReflection || 0;
|
|
150
|
+
const minutesSince = Math.floor((Date.now() - lastReflection) / 60000);
|
|
151
|
+
const { max_reflections_per_session } = config;
|
|
152
|
+
|
|
153
|
+
return {
|
|
154
|
+
canReflect: state.contextReflectionCount < max_reflections_per_session &&
|
|
155
|
+
minutesSince >= config.cooldown_minutes,
|
|
156
|
+
minutesSince,
|
|
157
|
+
count: state.contextReflectionCount || 0,
|
|
158
|
+
};
|
|
159
|
+
} catch {
|
|
160
|
+
return { canReflect: true, minutesSince: config.cooldown_minutes + 1 };
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function updateReflectionState(stateFile, reflectionCount) {
|
|
165
|
+
try {
|
|
166
|
+
let state = { contextReflectionCount: 0, lastContextReflection: 0 };
|
|
167
|
+
if (existsSync(stateFile)) {
|
|
168
|
+
try {
|
|
169
|
+
state = JSON.parse(readFileSync(stateFile, "utf-8"));
|
|
170
|
+
} catch {
|
|
171
|
+
// Use defaults
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
state.contextReflectionCount = reflectionCount;
|
|
176
|
+
state.lastContextReflection = Date.now();
|
|
177
|
+
|
|
178
|
+
const dir = dirname(stateFile);
|
|
179
|
+
if (!existsSync(dir)) {
|
|
180
|
+
mkdirSync(dir, { recursive: true });
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
writeFileSync(stateFile, JSON.stringify(state, null, 2));
|
|
184
|
+
} catch {
|
|
185
|
+
// Non-blocking
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function generateReflection(percentUsed, config) {
|
|
190
|
+
const date = new Date().toISOString().split("T")[0];
|
|
191
|
+
const timestamp = new Date().toISOString().split("T")[1].replace(/[:-]/g, "").slice(0, 6);
|
|
192
|
+
const filename = `context-warning-${date}-${timestamp}.md`;
|
|
193
|
+
|
|
194
|
+
const outputPath = join(REFLECTIONS_DIR, filename);
|
|
195
|
+
const dir = dirname(outputPath);
|
|
196
|
+
|
|
197
|
+
if (!existsSync(dir)) {
|
|
198
|
+
mkdirSync(dir, { recursive: true });
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
let recentCommits = "";
|
|
202
|
+
try {
|
|
203
|
+
recentCommits = execSync("git log --oneline -5 --format='- %s (%h)'", {
|
|
204
|
+
encoding: "utf-8",
|
|
205
|
+
cwd: PROJECT_ROOT,
|
|
206
|
+
});
|
|
207
|
+
} catch {
|
|
208
|
+
recentCommits = "# No recent commits";
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const stub = `---
|
|
212
|
+
story_type: ${config.story_type || "reflection"}
|
|
213
|
+
title: "Context window warning at ${Math.round(percentUsed)}%"
|
|
214
|
+
date: ${date}
|
|
215
|
+
slug: context-warning-${date}
|
|
216
|
+
emotional_arc: "challenge โ discovery โ resolution"
|
|
217
|
+
codex_terms: ["reflection", "learning", "context-management"]
|
|
218
|
+
framework: three_act_structure
|
|
219
|
+
target_words: 3000
|
|
220
|
+
location: ${REFLECTIONS_DIR}
|
|
221
|
+
---
|
|
222
|
+
|
|
223
|
+
# Context Window Warning: ${Math.round(percentUsed)}% Usage
|
|
224
|
+
|
|
225
|
+
## 1. Executive Summary
|
|
226
|
+
|
|
227
|
+
Context window is at **${Math.round(percentUsed)}%** capacity. Auto-triggered reflection captured to preserve learn
|
|
228
|
+
|
|
229
|
+
gs before context compaction.
|
|
230
|
+
|
|
231
|
+
## 2. What Was / What Is / What Should Be
|
|
232
|
+
|
|
233
|
+
### What Was
|
|
234
|
+
Session was operating with available context headroom for current tasks.
|
|
235
|
+
|
|
236
|
+
### What Is
|
|
237
|
+
Context window is at **${Math.round(percentUsed)}%** capacity - approaching the ${config.threshold_percent}% threshold configured for auto-reflection trigger.
|
|
238
|
+
|
|
239
|
+
### What Should Be
|
|
240
|
+
Reflections captured before context compaction to preserve learnings and session context.
|
|
241
|
+
|
|
242
|
+
## 3. INNER DIALOGUE
|
|
243
|
+
|
|
244
|
+
What critical learnings need to be preserved before context is compacted? What patterns am I seeing in this session? What decisions have been made that should be documented?
|
|
245
|
+
|
|
246
|
+
## 4. Counterfactual Thinking
|
|
247
|
+
|
|
248
|
+
If we didn't capture this reflection now, what would be lost when context is compacted? What would I wish I had documented?
|
|
249
|
+
|
|
250
|
+
## 5. Personal Journey
|
|
251
|
+
|
|
252
|
+
Navigating the constraints of finite context windows while maximizing learning capture. Each session requires intentional reflection to preserve institutional knowledge.
|
|
253
|
+
|
|
254
|
+
## 6. Session Context
|
|
255
|
+
|
|
256
|
+
### Recent Commits
|
|
257
|
+
\`\`\`
|
|
258
|
+
${recentCommits}
|
|
259
|
+
\`\`\`
|
|
260
|
+
|
|
261
|
+
## 7. Architecture Impact
|
|
262
|
+
\`\`\`
|
|
263
|
+
Process improvement - context window awareness
|
|
264
|
+
\`\`\`
|
|
265
|
+
|
|
266
|
+
## 8. Key Learnings So Far
|
|
267
|
+
|
|
268
|
+
- [Fill in key learnings from this session]
|
|
269
|
+
- [Document important decisions made]
|
|
270
|
+
- [Note any technical insights]
|
|
271
|
+
|
|
272
|
+
## 9. What Still Doesn't Work
|
|
273
|
+
|
|
274
|
+
- [Identify ongoing issues]
|
|
275
|
+
- [Note areas needing investigation]
|
|
276
|
+
|
|
277
|
+
## 10. For Future AI
|
|
278
|
+
|
|
279
|
+
- Implement earlier reflection triggers to ensure no learnings are lost
|
|
280
|
+
- Consider session summarization before context compaction
|
|
281
|
+
- Track patterns across sessions for better context management
|
|
282
|
+
|
|
283
|
+
---
|
|
284
|
+
|
|
285
|
+
*Auto-generated context window reflection. Fill in details before context is compacted.*
|
|
286
|
+
`;
|
|
287
|
+
|
|
288
|
+
writeFileSync(outputPath, stub, "utf-8");
|
|
289
|
+
return outputPath;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
async function main() {
|
|
293
|
+
if (!IS_CI) {
|
|
294
|
+
log("๐ Context-Window Pre-Command Hook");
|
|
295
|
+
log("============================");
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const config = loadConfig();
|
|
299
|
+
|
|
300
|
+
if (!config.enabled) {
|
|
301
|
+
if (!IS_CI) log("โญ๏ธ Context reflection disabled, skipping");
|
|
302
|
+
process.exit(0);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
const estimatedTokens = estimateContextUsage();
|
|
306
|
+
const threshold = getContextThreshold(config);
|
|
307
|
+
const percentUsed = (estimatedTokens / threshold) * 100;
|
|
308
|
+
|
|
309
|
+
if (!IS_CI) {
|
|
310
|
+
log(`๐ Estimated tokens: ~${estimatedTokens.toLocaleString()}`);
|
|
311
|
+
log(`๐ Threshold: ${config.threshold_percent}% (${threshold.toLocaleString()} tokens)`);
|
|
312
|
+
log(`๐ Usage: ${Math.round(percentUsed)}%`);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
if (percentUsed < config.threshold_percent) {
|
|
316
|
+
if (!IS_CI) log(`โ
Context OK (${Math.round(percentUsed)}% < ${config.threshold_percent}%)`);
|
|
317
|
+
process.exit(0);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
warn(`โ ๏ธ Context at ${Math.round(percentUsed)}% - above ${config.threshold_percent}% threshold`);
|
|
321
|
+
|
|
322
|
+
const stateFile = join(PROJECT_ROOT, ".opencode", "logs", "reflection-state.json");
|
|
323
|
+
const cooldown = checkReflectionCooldown(stateFile, config);
|
|
324
|
+
|
|
325
|
+
if (!cooldown.canReflect) {
|
|
326
|
+
if (!IS_CI) {
|
|
327
|
+
log(`โญ๏ธ Cooldown active (${cooldown.minutesSince}min since last, ${cooldown.count}/${config.max_reflections_per_session} this session)`);
|
|
328
|
+
}
|
|
329
|
+
process.exit(0);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
if (DRY_RUN) {
|
|
333
|
+
if (!IS_CI) log(`๐งช DRY RUN: Would generate reflection at ${Math.round(percentUsed)}%`);
|
|
334
|
+
process.exit(0);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
const reflectionPath = generateReflection(percentUsed, config);
|
|
338
|
+
|
|
339
|
+
const newCount = (cooldown.count || 0) + 1;
|
|
340
|
+
updateReflectionState(stateFile, newCount);
|
|
341
|
+
|
|
342
|
+
warn(`โ
Context reflection generated: ${reflectionPath}`);
|
|
343
|
+
warn(`๐ Please fill in details before context is compacted!`);
|
|
344
|
+
|
|
345
|
+
if (!IS_CI) {
|
|
346
|
+
console.log("");
|
|
347
|
+
console.log("Next steps:");
|
|
348
|
+
console.log(`1. Open: ${reflectionPath}`);
|
|
349
|
+
console.log("2. Fill in key learnings");
|
|
350
|
+
console.log("3. Save before next command (context may be compacted)");
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
process.exit(0);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
main().catch((err) => {
|
|
357
|
+
error(`โ Hook error: ${err.message}`);
|
|
358
|
+
process.exit(0);
|
|
359
|
+
});
|