zelari-code 0.1.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/LICENSE +21 -0
- package/README.md +156 -0
- package/bin/zelari-code.js +64 -0
- package/dist/agents/advancedTools.js +115 -0
- package/dist/agents/advancedTools.js.map +1 -0
- package/dist/agents/councilApi.js +375 -0
- package/dist/agents/councilApi.js.map +1 -0
- package/dist/agents/councilDirectives.js +100 -0
- package/dist/agents/councilDirectives.js.map +1 -0
- package/dist/agents/promoteMember.js +210 -0
- package/dist/agents/promoteMember.js.map +1 -0
- package/dist/agents/promptModules.js +139 -0
- package/dist/agents/promptModules.js.map +1 -0
- package/dist/agents/roles.js +281 -0
- package/dist/agents/roles.js.map +1 -0
- package/dist/agents/skills/builtin/debugging.js +248 -0
- package/dist/agents/skills/builtin/debugging.js.map +1 -0
- package/dist/agents/skills/builtin/docs.js +301 -0
- package/dist/agents/skills/builtin/docs.js.map +1 -0
- package/dist/agents/skills/builtin/git-ops.js +301 -0
- package/dist/agents/skills/builtin/git-ops.js.map +1 -0
- package/dist/agents/skills/builtin/planning.js +335 -0
- package/dist/agents/skills/builtin/planning.js.map +1 -0
- package/dist/agents/skills/builtin/refactoring.js +261 -0
- package/dist/agents/skills/builtin/refactoring.js.map +1 -0
- package/dist/agents/skills/builtin/review.js +302 -0
- package/dist/agents/skills/builtin/review.js.map +1 -0
- package/dist/agents/skills/builtin/testing.js +245 -0
- package/dist/agents/skills/builtin/testing.js.map +1 -0
- package/dist/agents/skills.js +272 -0
- package/dist/agents/skills.js.map +1 -0
- package/dist/agents/systemPromptBuilder.js +151 -0
- package/dist/agents/systemPromptBuilder.js.map +1 -0
- package/dist/agents/toolSchemas.js +249 -0
- package/dist/agents/toolSchemas.js.map +1 -0
- package/dist/agents/tools.js +196 -0
- package/dist/agents/tools.js.map +1 -0
- package/dist/agents/vaultTools.js +134 -0
- package/dist/agents/vaultTools.js.map +1 -0
- package/dist/cli/app.js +1823 -0
- package/dist/cli/app.js.map +1 -0
- package/dist/cli/branchManager.js +280 -0
- package/dist/cli/branchManager.js.map +1 -0
- package/dist/cli/compaction.js +63 -0
- package/dist/cli/compaction.js.map +1 -0
- package/dist/cli/components/ChatStream.js +23 -0
- package/dist/cli/components/ChatStream.js.map +1 -0
- package/dist/cli/components/CollapsibleToolOutput.js +38 -0
- package/dist/cli/components/CollapsibleToolOutput.js.map +1 -0
- package/dist/cli/components/Header.js +31 -0
- package/dist/cli/components/Header.js.map +1 -0
- package/dist/cli/components/InputBar.js +9 -0
- package/dist/cli/components/InputBar.js.map +1 -0
- package/dist/cli/components/Sidebar.js +17 -0
- package/dist/cli/components/Sidebar.js.map +1 -0
- package/dist/cli/councilCost.js +81 -0
- package/dist/cli/councilCost.js.map +1 -0
- package/dist/cli/councilDispatcher.js +56 -0
- package/dist/cli/councilDispatcher.js.map +1 -0
- package/dist/cli/councilFeedback.js +164 -0
- package/dist/cli/councilFeedback.js.map +1 -0
- package/dist/cli/crossProviderFailover.js +83 -0
- package/dist/cli/crossProviderFailover.js.map +1 -0
- package/dist/cli/gitOps.js +96 -0
- package/dist/cli/gitOps.js.map +1 -0
- package/dist/cli/grokOAuth.js +375 -0
- package/dist/cli/grokOAuth.js.map +1 -0
- package/dist/cli/keyStore.js +263 -0
- package/dist/cli/keyStore.js.map +1 -0
- package/dist/cli/keyValidator.js +83 -0
- package/dist/cli/keyValidator.js.map +1 -0
- package/dist/cli/main.bundled.js +21428 -0
- package/dist/cli/main.bundled.js.map +7 -0
- package/dist/cli/main.js +72 -0
- package/dist/cli/main.js.map +1 -0
- package/dist/cli/metrics.js +131 -0
- package/dist/cli/metrics.js.map +1 -0
- package/dist/cli/modelDiscovery.js +234 -0
- package/dist/cli/modelDiscovery.js.map +1 -0
- package/dist/cli/modelPricing.js +112 -0
- package/dist/cli/modelPricing.js.map +1 -0
- package/dist/cli/oauthCallbackServer.js +207 -0
- package/dist/cli/oauthCallbackServer.js.map +1 -0
- package/dist/cli/provider/openai-compatible.js +236 -0
- package/dist/cli/provider/openai-compatible.js.map +1 -0
- package/dist/cli/providerConfig.js +210 -0
- package/dist/cli/providerConfig.js.map +1 -0
- package/dist/cli/providerFailover.js +90 -0
- package/dist/cli/providerFailover.js.map +1 -0
- package/dist/cli/refreshRegistry.js +77 -0
- package/dist/cli/refreshRegistry.js.map +1 -0
- package/dist/cli/safety/auditLogger.js +111 -0
- package/dist/cli/safety/auditLogger.js.map +1 -0
- package/dist/cli/safety/sandboxPath.js +62 -0
- package/dist/cli/safety/sandboxPath.js.map +1 -0
- package/dist/cli/safety/shellBlocklist.js +71 -0
- package/dist/cli/safety/shellBlocklist.js.map +1 -0
- package/dist/cli/sessionManager.js +149 -0
- package/dist/cli/sessionManager.js.map +1 -0
- package/dist/cli/skillCache.js +205 -0
- package/dist/cli/skillCache.js.map +1 -0
- package/dist/cli/skillHistory.js +178 -0
- package/dist/cli/skillHistory.js.map +1 -0
- package/dist/cli/skillSuggest.js +132 -0
- package/dist/cli/skillSuggest.js.map +1 -0
- package/dist/cli/slashCommands.js +431 -0
- package/dist/cli/slashCommands.js.map +1 -0
- package/dist/cli/toolRegistry.js +187 -0
- package/dist/cli/toolRegistry.js.map +1 -0
- package/dist/cli/updater.js +157 -0
- package/dist/cli/updater.js.map +1 -0
- package/dist/main/core/AgentHarness.js +345 -0
- package/dist/main/core/AgentHarness.js.map +1 -0
- package/dist/main/core/providerStream.js +99 -0
- package/dist/main/core/providerStream.js.map +1 -0
- package/dist/main/core/sessionJsonl.js +99 -0
- package/dist/main/core/sessionJsonl.js.map +1 -0
- package/dist/main/core/tools/auditLog.js +26 -0
- package/dist/main/core/tools/auditLog.js.map +1 -0
- package/dist/main/core/tools/builtin/filesystem.js +110 -0
- package/dist/main/core/tools/builtin/filesystem.js.map +1 -0
- package/dist/main/core/tools/builtin/search.js +56 -0
- package/dist/main/core/tools/builtin/search.js.map +1 -0
- package/dist/main/core/tools/builtin/shell.js +51 -0
- package/dist/main/core/tools/builtin/shell.js.map +1 -0
- package/dist/main/core/tools/registry.js +71 -0
- package/dist/main/core/tools/registry.js.map +1 -0
- package/dist/main/core/tools/toolTypes.js +8 -0
- package/dist/main/core/tools/toolTypes.js.map +1 -0
- package/dist/main/core/tools/zodBridge.js +48 -0
- package/dist/main/core/tools/zodBridge.js.map +1 -0
- package/dist/shared/eventBus.js +104 -0
- package/dist/shared/eventBus.js.map +1 -0
- package/dist/shared/events.js +73 -0
- package/dist/shared/events.js.map +1 -0
- package/dist/shared/index.js +8 -0
- package/dist/shared/index.js.map +1 -0
- package/dist/types/context.js +3 -0
- package/dist/types/context.js.map +1 -0
- package/dist/types/index.js +2 -0
- package/dist/types/index.js.map +1 -0
- package/dist/types/knowledge.js +2 -0
- package/dist/types/knowledge.js.map +1 -0
- package/dist/types/systemTypes.js +10 -0
- package/dist/types/systemTypes.js.map +1 -0
- package/package.json +63 -0
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* skillCache — persistent JSON cache for skill invocation results (Task H.1).
|
|
3
|
+
*
|
|
4
|
+
* Avoids re-running a skill on identical input within the TTL window.
|
|
5
|
+
* Built directly on top of the v3-C `skillHistory.ts` telemetry:
|
|
6
|
+
* - skillHistory records WHAT ran + success rate (for stats + ranking)
|
|
7
|
+
* - skillCache stores the OUTPUT of past invocations (for instant replay)
|
|
8
|
+
*
|
|
9
|
+
* Privacy: only the SHA-256 of `skillId + '\0' + input` is used as the
|
|
10
|
+
* cache key. The `output` IS persisted (it's the whole point), but the
|
|
11
|
+
* raw `input` string is NOT — so reading the cache file alone never
|
|
12
|
+
* reveals user prompts. This matches v3-C's privacy stance for the
|
|
13
|
+
* history log.
|
|
14
|
+
*
|
|
15
|
+
* Storage: `~/.tmp/anathema-coder/skill-cache.json` (override via
|
|
16
|
+
* `ANATHEMA_SKILL_CACHE_FILE` env var). Bounded size: callers should
|
|
17
|
+
* purge periodically; the file grows by entry size, no rotation.
|
|
18
|
+
*
|
|
19
|
+
* Thread-safety: Node.js is single-threaded for JS execution. File
|
|
20
|
+
* reads/writes are atomic-enough for our needs (the OS doesn't interleave
|
|
21
|
+
* a `readFile` and `writeFile` of a small JSON file in any meaningful
|
|
22
|
+
* way). For multi-process safety, callers should wrap with flock(2)
|
|
23
|
+
* — out of scope for now.
|
|
24
|
+
*
|
|
25
|
+
* @see docs/plans/2026-06-29-anathema-coder-v3-H.md (Task H.1)
|
|
26
|
+
*/
|
|
27
|
+
import { promises as fs, existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
28
|
+
import path from 'node:path';
|
|
29
|
+
import os from 'node:os';
|
|
30
|
+
import { createHash } from 'node:crypto';
|
|
31
|
+
export const SKILL_CACHE_DEFAULT_TTL_MS = 24 * 60 * 60 * 1000; // 24h
|
|
32
|
+
/**
|
|
33
|
+
* Compute the SHA-256 cache key for a `(skillId, input)` pair. The
|
|
34
|
+
* null-byte delimiter prevents `id1+input2` from colliding with
|
|
35
|
+
* `id2+input1` (e.g. `("a", "bc")` vs `("ab", "c")`).
|
|
36
|
+
*
|
|
37
|
+
* Exported for testing — production code should not call this directly.
|
|
38
|
+
*/
|
|
39
|
+
export function computeInputHash(skillId, input) {
|
|
40
|
+
return createHash('sha256').update(skillId + '\0' + input, 'utf8').digest('hex');
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Persistent JSON cache for skill invocation results. Lazy-loaded on
|
|
44
|
+
* first access, saved on every mutation.
|
|
45
|
+
*/
|
|
46
|
+
export class SkillCache {
|
|
47
|
+
file;
|
|
48
|
+
defaultTtlMs;
|
|
49
|
+
now;
|
|
50
|
+
state = null;
|
|
51
|
+
constructor(options = {}) {
|
|
52
|
+
this.file = options.file ?? process.env.ANATHEMA_SKILL_CACHE_FILE
|
|
53
|
+
?? path.join(os.homedir(), '.tmp', 'zelari-code', 'skill-cache.json');
|
|
54
|
+
this.defaultTtlMs = options.defaultTtlMs ?? SKILL_CACHE_DEFAULT_TTL_MS;
|
|
55
|
+
this.now = options.now ?? (() => Date.now());
|
|
56
|
+
mkdirSync(path.dirname(this.file), { recursive: true });
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Look up a cached output for `(skillId, input)`. Returns null on
|
|
60
|
+
* miss OR on expired entry (the expired entry is removed eagerly
|
|
61
|
+
* to keep the file small).
|
|
62
|
+
*/
|
|
63
|
+
get(skillId, input) {
|
|
64
|
+
const entry = this.lookupEntry(skillId, input);
|
|
65
|
+
if (!entry)
|
|
66
|
+
return null;
|
|
67
|
+
if (entry.expiresAt <= this.now()) {
|
|
68
|
+
// Expired — remove eagerly
|
|
69
|
+
const key = computeInputHash(skillId, input);
|
|
70
|
+
this.mutate((s) => {
|
|
71
|
+
delete s.entries[key];
|
|
72
|
+
});
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
return entry.output;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Store an output for `(skillId, input)`. Optional `ttlMs` overrides
|
|
79
|
+
* the cache default for this entry (e.g. short-lived outputs can use
|
|
80
|
+
* 5min; long-lived deterministic ones can use 7 days).
|
|
81
|
+
*/
|
|
82
|
+
set(skillId, input, output, ttlMs) {
|
|
83
|
+
const key = computeInputHash(skillId, input);
|
|
84
|
+
const ts = this.now();
|
|
85
|
+
const ttl = ttlMs ?? this.defaultTtlMs;
|
|
86
|
+
const entry = {
|
|
87
|
+
skillId,
|
|
88
|
+
inputHash: key,
|
|
89
|
+
output,
|
|
90
|
+
ts,
|
|
91
|
+
expiresAt: ts + ttl,
|
|
92
|
+
};
|
|
93
|
+
this.mutate((s) => {
|
|
94
|
+
s.entries[key] = entry;
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Remove all expired entries. Returns the count of removed entries.
|
|
99
|
+
* Useful as a periodic maintenance call (e.g. from main.ts shutdown
|
|
100
|
+
* handler).
|
|
101
|
+
*/
|
|
102
|
+
clearExpired() {
|
|
103
|
+
const now = this.now();
|
|
104
|
+
let removed = 0;
|
|
105
|
+
this.mutate((s) => {
|
|
106
|
+
for (const [key, entry] of Object.entries(s.entries)) {
|
|
107
|
+
if (entry.expiresAt <= now) {
|
|
108
|
+
delete s.entries[key];
|
|
109
|
+
removed++;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
return removed;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Remove all entries for a specific skill (when `skillId` is given),
|
|
117
|
+
* or wipe the entire cache (when omitted). Returns the count of
|
|
118
|
+
* removed entries.
|
|
119
|
+
*/
|
|
120
|
+
purge(skillId) {
|
|
121
|
+
let removed = 0;
|
|
122
|
+
this.mutate((s) => {
|
|
123
|
+
if (skillId === undefined) {
|
|
124
|
+
removed = Object.keys(s.entries).length;
|
|
125
|
+
s.entries = {};
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
for (const [key, entry] of Object.entries(s.entries)) {
|
|
129
|
+
if (entry.skillId === skillId) {
|
|
130
|
+
delete s.entries[key];
|
|
131
|
+
removed++;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
return removed;
|
|
136
|
+
}
|
|
137
|
+
/** Number of entries currently in the cache (useful for tests + CLI status). */
|
|
138
|
+
size() {
|
|
139
|
+
return this.loadIfNeeded().entries ? Object.keys(this.loadIfNeeded().entries).length : 0;
|
|
140
|
+
}
|
|
141
|
+
/** Lazy-load the cache file. Idempotent. */
|
|
142
|
+
loadIfNeeded() {
|
|
143
|
+
if (this.state !== null)
|
|
144
|
+
return this.state;
|
|
145
|
+
if (!existsSync(this.file)) {
|
|
146
|
+
this.state = { version: 1, entries: {} };
|
|
147
|
+
return this.state;
|
|
148
|
+
}
|
|
149
|
+
try {
|
|
150
|
+
const raw = readFileSync(this.file, 'utf-8');
|
|
151
|
+
const parsed = JSON.parse(raw);
|
|
152
|
+
// Defensive: ensure shape matches expected schema.
|
|
153
|
+
if (typeof parsed !== 'object' ||
|
|
154
|
+
parsed === null ||
|
|
155
|
+
parsed.version !== 1 ||
|
|
156
|
+
typeof parsed.entries !== 'object' ||
|
|
157
|
+
parsed.entries === null) {
|
|
158
|
+
this.state = { version: 1, entries: {} };
|
|
159
|
+
return this.state;
|
|
160
|
+
}
|
|
161
|
+
this.state = parsed;
|
|
162
|
+
return this.state;
|
|
163
|
+
}
|
|
164
|
+
catch {
|
|
165
|
+
// Corrupt or unreadable — start fresh, don't crash the CLI.
|
|
166
|
+
this.state = { version: 1, entries: {} };
|
|
167
|
+
return this.state;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
/** Look up the entry without mutating state (used by get + tests). */
|
|
171
|
+
lookupEntry(skillId, input) {
|
|
172
|
+
const state = this.loadIfNeeded();
|
|
173
|
+
const key = computeInputHash(skillId, input);
|
|
174
|
+
return state.entries[key] ?? null;
|
|
175
|
+
}
|
|
176
|
+
/** Run `fn` on the in-memory state, then persist to disk. */
|
|
177
|
+
mutate(fn) {
|
|
178
|
+
const state = this.loadIfNeeded();
|
|
179
|
+
fn(state);
|
|
180
|
+
this.save(state);
|
|
181
|
+
}
|
|
182
|
+
/** Persist the in-memory state to disk. Synchronous to keep ordering simple. */
|
|
183
|
+
save(state) {
|
|
184
|
+
writeFileSync(this.file, JSON.stringify(state), { encoding: 'utf-8' });
|
|
185
|
+
}
|
|
186
|
+
/** Reset in-memory state (test-only helper). */
|
|
187
|
+
resetForTests() {
|
|
188
|
+
this.state = null;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Convenience: try async file-based access. Useful for tests that want
|
|
193
|
+
* to verify the file was written. Most production callers use the
|
|
194
|
+
* `SkillCache` class above (synchronous API for simplicity).
|
|
195
|
+
*/
|
|
196
|
+
export async function readSkillCacheFile(file) {
|
|
197
|
+
try {
|
|
198
|
+
const raw = await fs.readFile(file, 'utf-8');
|
|
199
|
+
return JSON.parse(raw);
|
|
200
|
+
}
|
|
201
|
+
catch {
|
|
202
|
+
return null;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
//# sourceMappingURL=skillCache.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"skillCache.js","sourceRoot":"","sources":["../../src/cli/skillCache.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* skillHistory — JSONL skill invocation history (Phase 25 — Skill History + Telemetry).
|
|
3
|
+
*
|
|
4
|
+
* Tracks every invocation of `/skill <name>` so the user can later query
|
|
5
|
+
* success rate + duration + token usage via `/skill-stats [name]`.
|
|
6
|
+
*
|
|
7
|
+
* Schema (per record):
|
|
8
|
+
* {
|
|
9
|
+
* ts: number, // epoch ms when invocation ended
|
|
10
|
+
* skillId: string, // e.g. 'coder-debug', 'brain-memory'
|
|
11
|
+
* invocationId: string, // unique per recordStart() call
|
|
12
|
+
* sessionId?: string,
|
|
13
|
+
* durationMs?: number, // wall-clock from recordStart to recordEnd
|
|
14
|
+
* tokensUsed?: number, // optional — caller-supplied
|
|
15
|
+
* ok: boolean, // true on success, false on error
|
|
16
|
+
* error?: string, // error message when ok=false
|
|
17
|
+
* }
|
|
18
|
+
*
|
|
19
|
+
* Storage: `~/.tmp/anathema-coder/skill-history.jsonl` (override via
|
|
20
|
+
* `ANATHEMA_SKILL_HISTORY_FILE` env var, useful for tests).
|
|
21
|
+
*
|
|
22
|
+
* Rotation: when the file exceeds 10MB, it's renamed to
|
|
23
|
+
* `skill-history.1.jsonl` (existing `.1.jsonl` is overwritten). Only one
|
|
24
|
+
* rotation step — older data is dropped. Same pattern as metrics.ts.
|
|
25
|
+
*
|
|
26
|
+
* Privacy: only metadata is stored (skillId, duration, ok). No prompt
|
|
27
|
+
* content or model outputs are persisted by this logger.
|
|
28
|
+
*/
|
|
29
|
+
import { promises as fs, existsSync, statSync, renameSync, appendFileSync, mkdirSync } from 'node:fs';
|
|
30
|
+
import path from 'node:path';
|
|
31
|
+
import os from 'node:os';
|
|
32
|
+
import { randomUUID } from 'node:crypto';
|
|
33
|
+
export const SKILL_HISTORY_ROTATE_BYTES = 10 * 1024 * 1024; // 10 MB
|
|
34
|
+
export class SkillHistoryLogger {
|
|
35
|
+
file;
|
|
36
|
+
writeQueue = Promise.resolve();
|
|
37
|
+
inflight = new Map();
|
|
38
|
+
constructor(file) {
|
|
39
|
+
this.file = file ?? process.env.ANATHEMA_SKILL_HISTORY_FILE
|
|
40
|
+
?? path.join(os.homedir(), '.tmp', 'zelari-code', 'skill-history.jsonl');
|
|
41
|
+
mkdirSync(path.dirname(this.file), { recursive: true });
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Mark the start of a skill invocation. Returns a unique invocationId
|
|
45
|
+
* that the caller must pass to `recordEnd` when the invocation completes.
|
|
46
|
+
*
|
|
47
|
+
* Multiple concurrent invocations are supported (one inflight entry per id).
|
|
48
|
+
*/
|
|
49
|
+
recordStart(skillId, sessionId) {
|
|
50
|
+
const invocationId = randomUUID();
|
|
51
|
+
this.inflight.set(invocationId, { ts: Date.now(), skillId, sessionId });
|
|
52
|
+
return invocationId;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Mark the end of a skill invocation. Writes a SkillHistoryRecord to disk.
|
|
56
|
+
*
|
|
57
|
+
* If `invocationId` is unknown (e.g. duplicate call or logger recreated),
|
|
58
|
+
* this is a no-op. The logger never throws from recordEnd — it's safe
|
|
59
|
+
* to call from finally blocks.
|
|
60
|
+
*/
|
|
61
|
+
recordEnd(invocationId, outcome) {
|
|
62
|
+
const start = this.inflight.get(invocationId);
|
|
63
|
+
if (!start)
|
|
64
|
+
return; // unknown id — graceful no-op
|
|
65
|
+
this.inflight.delete(invocationId);
|
|
66
|
+
const rec = {
|
|
67
|
+
ts: Date.now(),
|
|
68
|
+
skillId: start.skillId,
|
|
69
|
+
invocationId,
|
|
70
|
+
sessionId: start.sessionId,
|
|
71
|
+
durationMs: Date.now() - start.ts,
|
|
72
|
+
tokensUsed: outcome.tokensUsed,
|
|
73
|
+
ok: outcome.ok,
|
|
74
|
+
error: outcome.error,
|
|
75
|
+
};
|
|
76
|
+
this.writeQueue = this.writeQueue.then(() => this.append(rec));
|
|
77
|
+
void this.writeQueue.catch(() => { });
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Force the write queue to flush — used by tests + on graceful shutdown.
|
|
81
|
+
* Returns the underlying promise so callers can await it.
|
|
82
|
+
*/
|
|
83
|
+
async flush() {
|
|
84
|
+
await this.writeQueue;
|
|
85
|
+
}
|
|
86
|
+
/** Number of in-flight invocations (mostly for tests). */
|
|
87
|
+
inflightCount() {
|
|
88
|
+
return this.inflight.size;
|
|
89
|
+
}
|
|
90
|
+
/** Append a single record synchronously (file I/O). */
|
|
91
|
+
append(rec) {
|
|
92
|
+
return new Promise((resolve, reject) => {
|
|
93
|
+
try {
|
|
94
|
+
this.maybeRotate();
|
|
95
|
+
const line = JSON.stringify(rec) + '\n';
|
|
96
|
+
appendFileSync(this.file, line, { encoding: 'utf-8' });
|
|
97
|
+
resolve();
|
|
98
|
+
}
|
|
99
|
+
catch (err) {
|
|
100
|
+
reject(err);
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
/** If the file is over the rotation threshold, rotate it. */
|
|
105
|
+
maybeRotate() {
|
|
106
|
+
if (!existsSync(this.file))
|
|
107
|
+
return;
|
|
108
|
+
try {
|
|
109
|
+
const stat = statSync(this.file);
|
|
110
|
+
if (stat.size >= SKILL_HISTORY_ROTATE_BYTES) {
|
|
111
|
+
const rotated = this.file.replace(/\.jsonl$/, '.1.jsonl');
|
|
112
|
+
renameSync(this.file, rotated);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
// Best-effort.
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
/** Convenience helper: read all records from a skill history file. */
|
|
121
|
+
export async function readSkillHistory(file) {
|
|
122
|
+
let raw = '';
|
|
123
|
+
try {
|
|
124
|
+
raw = await fs.readFile(file, 'utf-8');
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
return [];
|
|
128
|
+
}
|
|
129
|
+
const out = [];
|
|
130
|
+
for (const line of raw.split('\n')) {
|
|
131
|
+
if (!line.trim())
|
|
132
|
+
continue;
|
|
133
|
+
try {
|
|
134
|
+
out.push(JSON.parse(line));
|
|
135
|
+
}
|
|
136
|
+
catch {
|
|
137
|
+
// Skip malformed lines.
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return out;
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Compute aggregated stats over a list of records.
|
|
144
|
+
*
|
|
145
|
+
* Pure function — no file I/O. Caller reads the file with readSkillHistory()
|
|
146
|
+
* and passes the records here.
|
|
147
|
+
*
|
|
148
|
+
* @param records — full record list (already loaded from disk)
|
|
149
|
+
* @param skillId — optional filter: only stats for this skill
|
|
150
|
+
* @param sinceTs — optional filter: only records with ts >= sinceTs
|
|
151
|
+
*/
|
|
152
|
+
export function getSkillStats(records, skillId, sinceTs) {
|
|
153
|
+
const filtered = records.filter((r) => {
|
|
154
|
+
if (skillId !== undefined && r.skillId !== skillId)
|
|
155
|
+
return false;
|
|
156
|
+
if (sinceTs !== undefined && r.ts < sinceTs)
|
|
157
|
+
return false;
|
|
158
|
+
return true;
|
|
159
|
+
});
|
|
160
|
+
if (filtered.length === 0) {
|
|
161
|
+
return { count: 0, successRate: 0, avgDurationMs: 0, totalTokens: 0 };
|
|
162
|
+
}
|
|
163
|
+
const successCount = filtered.filter((r) => r.ok).length;
|
|
164
|
+
const durations = filtered
|
|
165
|
+
.map((r) => r.durationMs)
|
|
166
|
+
.filter((d) => typeof d === 'number');
|
|
167
|
+
const avgDurationMs = durations.length === 0
|
|
168
|
+
? 0
|
|
169
|
+
: durations.reduce((a, b) => a + b, 0) / durations.length;
|
|
170
|
+
const totalTokens = filtered.reduce((sum, r) => sum + (r.tokensUsed ?? 0), 0);
|
|
171
|
+
return {
|
|
172
|
+
count: filtered.length,
|
|
173
|
+
successRate: successCount / filtered.length,
|
|
174
|
+
avgDurationMs,
|
|
175
|
+
totalTokens,
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
//# sourceMappingURL=skillHistory.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"skillHistory.js","sourceRoot":"","sources":["../../src/cli/skillHistory.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAEH,OAAO,EAAE,QAAQ,IAAI,EAAE,EAAE,UAAU,EAAE,QAAQ,EAAE,UAAU,EAAE,cAAc,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AACtG,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,MAAM,SAAS,CAAC;AACzB,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAEzC,MAAM,CAAC,MAAM,0BAA0B,GAAG,EAAE,GAAG,IAAI,GAAG,IAAI,CAAC,CAAC,QAAQ;AAkCpE,MAAM,OAAO,kBAAkB;IACZ,IAAI,CAAS;IACtB,UAAU,GAAkB,OAAO,CAAC,OAAO,EAAE,CAAC;IACrC,QAAQ,GAAG,IAAI,GAAG,EAA8B,CAAC;IAElE,YAAY,IAAa;QACvB,IAAI,CAAC,IAAI,GAAG,IAAI,IAAI,OAAO,CAAC,GAAG,CAAC,2BAA2B;eACtD,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,EAAE,MAAM,EAAE,aAAa,EAAE,qBAAqB,CAAC,CAAC;QAC3E,SAAS,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAC1D,CAAC;IAED;;;;;OAKG;IACH,WAAW,CAAC,OAAe,EAAE,SAAkB;QAC7C,MAAM,YAAY,GAAG,UAAU,EAAE,CAAC;QAClC,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,YAAY,EAAE,EAAE,EAAE,EAAE,IAAI,CAAC,GAAG,EAAE,EAAE,OAAO,EAAE,SAAS,EAAE,CAAC,CAAC;QACxE,OAAO,YAAY,CAAC;IACtB,CAAC;IAED;;;;;;OAMG;IACH,SAAS,CACP,YAAoB,EACpB,OAA6D;QAE7D,MAAM,KAAK,GAAG,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,YAAY,CAAC,CAAC;QAC9C,IAAI,CAAC,KAAK;YAAE,OAAO,CAAC,8BAA8B;QAClD,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,YAAY,CAAC,CAAC;QACnC,MAAM,GAAG,GAAuB;YAC9B,EAAE,EAAE,IAAI,CAAC,GAAG,EAAE;YACd,OAAO,EAAE,KAAK,CAAC,OAAO;YACtB,YAAY;YACZ,SAAS,EAAE,KAAK,CAAC,SAAS;YAC1B,UAAU,EAAE,IAAI,CAAC,GAAG,EAAE,GAAG,KAAK,CAAC,EAAE;YACjC,UAAU,EAAE,OAAO,CAAC,UAAU;YAC9B,EAAE,EAAE,OAAO,CAAC,EAAE;YACd,KAAK,EAAE,OAAO,CAAC,KAAK;SACrB,CAAC;QACF,IAAI,CAAC,UAAU,GAAG,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC;QAC/D,KAAK,IAAI,CAAC,UAAU,CAAC,KAAK,CAAC,GAAG,EAAE,GAAiC,CAAC,CAAC,CAAC;IACtE,CAAC;IAED;;;OAGG;IACH,KAAK,CAAC,KAAK;QACT,MAAM,IAAI,CAAC,UAAU,CAAC;IACxB,CAAC;IAED,0DAA0D;IAC1D,aAAa;QACX,OAAO,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC;IAC5B,CAAC;IAED,uDAAuD;IAC/C,MAAM,CAAC,GAAuB;QACpC,OAAO,IAAI,OAAO,CAAO,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;YAC3C,IAAI,CAAC;gBACH,IAAI,CAAC,WAAW,EAAE,CAAC;gBACnB,MAAM,IAAI,GAAG,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,GAAG,IAAI,CAAC;gBACxC,cAAc,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,EAAE,EAAE,QAAQ,EAAE,OAAO,EAAE,CAAC,CAAC;gBACvD,OAAO,EAAE,CAAC;YACZ,CAAC;YAAC,OAAO,GAAG,EAAE,CAAC;gBACb,MAAM,CAAC,GAAG,CAAC,CAAC;YACd,CAAC;QACH,CAAC,CAAC,CAAC;IACL,CAAC;IAED,6DAA6D;IACrD,WAAW;QACjB,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC;YAAE,OAAO;QACnC,IAAI,CAAC;YACH,MAAM,IAAI,GAAG,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YACjC,IAAI,IAAI,CAAC,IAAI,IAAI,0BAA0B,EAAE,CAAC;gBAC5C,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,UAAU,EAAE,UAAU,CAAC,CAAC;gBAC1D,UAAU,CAAC,IAAI,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;YACjC,CAAC;QACH,CAAC;QAAC,MAAM,CAAC;YACP,eAAe;QACjB,CAAC;IACH,CAAC;CACF;AAED,sEAAsE;AACtE,MAAM,CAAC,KAAK,UAAU,gBAAgB,CAAC,IAAY;IACjD,IAAI,GAAG,GAAG,EAAE,CAAC;IACb,IAAI,CAAC;QACH,GAAG,GAAG,MAAM,EAAE,CAAC,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;IACzC,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,EAAE,CAAC;IACZ,CAAC;IACD,MAAM,GAAG,GAAyB,EAAE,CAAC;IACrC,KAAK,MAAM,IAAI,IAAI,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC;QACnC,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE;YAAE,SAAS;QAC3B,IAAI,CAAC;YACH,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAuB,CAAC,CAAC;QACnD,CAAC;QAAC,MAAM,CAAC;YACP,wBAAwB;QAC1B,CAAC;IACH,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,UAAU,aAAa,CAC3B,OAA6B,EAC7B,OAAgB,EAChB,OAAgB;IAEhB,MAAM,QAAQ,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE;QACpC,IAAI,OAAO,KAAK,SAAS,IAAI,CAAC,CAAC,OAAO,KAAK,OAAO;YAAE,OAAO,KAAK,CAAC;QACjE,IAAI,OAAO,KAAK,SAAS,IAAI,CAAC,CAAC,EAAE,GAAG,OAAO;YAAE,OAAO,KAAK,CAAC;QAC1D,OAAO,IAAI,CAAC;IACd,CAAC,CAAC,CAAC;IACH,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC1B,OAAO,EAAE,KAAK,EAAE,CAAC,EAAE,WAAW,EAAE,CAAC,EAAE,aAAa,EAAE,CAAC,EAAE,WAAW,EAAE,CAAC,EAAE,CAAC;IACxE,CAAC;IACD,MAAM,YAAY,GAAG,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,MAAM,CAAC;IACzD,MAAM,SAAS,GAAG,QAAQ;SACvB,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,UAAU,CAAC;SACxB,MAAM,CAAC,CAAC,CAAC,EAAe,EAAE,CAAC,OAAO,CAAC,KAAK,QAAQ,CAAC,CAAC;IACrD,MAAM,aAAa,GAAG,SAAS,CAAC,MAAM,KAAK,CAAC;QAC1C,CAAC,CAAC,CAAC;QACH,CAAC,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,GAAG,SAAS,CAAC,MAAM,CAAC;IAC5D,MAAM,WAAW,GAAG,QAAQ,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,CAAC,EAAE,EAAE,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC,UAAU,IAAI,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;IAC9E,OAAO;QACL,KAAK,EAAE,QAAQ,CAAC,MAAM;QACtB,WAAW,EAAE,YAAY,GAAG,QAAQ,CAAC,MAAM;QAC3C,aAAa;QACb,WAAW;KACZ,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* skillSuggest — recommendation engine for skill selection (Task H.2).
|
|
3
|
+
*
|
|
4
|
+
* Given a user query (free-form text like "refactor this code" or
|
|
5
|
+
* "debug the failing test"), returns a ranked list of candidate skills
|
|
6
|
+
* from SKILL_CATALOG. Ranking combines:
|
|
7
|
+
* - **Tag match** (case-insensitive substring against `skill.tags`)
|
|
8
|
+
* - **Name match** (fallback when no tag match)
|
|
9
|
+
* - **Historical success rate** (from skillHistory.ts)
|
|
10
|
+
* - **Average duration** (lower = higher score, normalized to 30s)
|
|
11
|
+
*
|
|
12
|
+
* If nothing matches, falls back to top-5 by global success rate — the
|
|
13
|
+
* "popular + reliable" skills that always deserve a recommendation.
|
|
14
|
+
*
|
|
15
|
+
* Pure module (no React/Ink deps). Caller wires it into a slash command
|
|
16
|
+
* (e.g. `/skill-suggest <query>` — see Task H.3.1 if added, or use
|
|
17
|
+
* directly from `app.tsx` dispatch).
|
|
18
|
+
*
|
|
19
|
+
* @see docs/plans/2026-06-29-anathema-coder-v3-H.md (Task H.2)
|
|
20
|
+
*/
|
|
21
|
+
import { listSkills } from '../agents/skills.js';
|
|
22
|
+
import { readSkillHistory, getSkillStats } from './skillHistory.js';
|
|
23
|
+
/**
|
|
24
|
+
* Compute a score in [0, 1] for a skill given its stats + match reason.
|
|
25
|
+
*
|
|
26
|
+
* Formula (deterministic, no randomness):
|
|
27
|
+
* base = successRate (0..1)
|
|
28
|
+
* speed = 1 - min(avgDurationMs/30s, 1) (0..1)
|
|
29
|
+
* tagBoost = 1.0 (tag match) | 0.5 (name) | 0.0 (fallback)
|
|
30
|
+
*
|
|
31
|
+
* score = 0.6 * base + 0.2 * speed + 0.2 * tagBoost
|
|
32
|
+
*
|
|
33
|
+
* The weights favor reliability (success rate matters most), then
|
|
34
|
+
* speed, then recency/context match. Tunable later.
|
|
35
|
+
*/
|
|
36
|
+
export function computeSuggestionScore(stats, matchReason) {
|
|
37
|
+
const base = stats.successRate;
|
|
38
|
+
const speed = stats.count === 0
|
|
39
|
+
? 0.5 // no history → neutral score (don't penalize unknown skills)
|
|
40
|
+
: 1 - Math.min((stats.avgDurationMs ?? 0) / 30_000, 1);
|
|
41
|
+
const tagBoost = matchReason === 'tag' ? 1.0
|
|
42
|
+
: matchReason === 'name' ? 0.5
|
|
43
|
+
: 0.0;
|
|
44
|
+
return 0.6 * base + 0.2 * speed + 0.2 * tagBoost;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Lowercase + trim helper for tag/name matching.
|
|
48
|
+
*/
|
|
49
|
+
function norm(s) {
|
|
50
|
+
return s.toLowerCase().trim();
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Classify the match type for a single skill against the query.
|
|
54
|
+
* - 'tag': at least one of the skill's tags contains the query (case-insensitive substring)
|
|
55
|
+
* - 'name': name contains the query (fallback when no tag match)
|
|
56
|
+
* - null: no match at all
|
|
57
|
+
*/
|
|
58
|
+
function classifyMatch(skill, queryNorm) {
|
|
59
|
+
if (queryNorm.length === 0)
|
|
60
|
+
return null;
|
|
61
|
+
const tags = skill.tags ?? [];
|
|
62
|
+
for (const t of tags) {
|
|
63
|
+
if (norm(t).includes(queryNorm))
|
|
64
|
+
return 'tag';
|
|
65
|
+
}
|
|
66
|
+
if (norm(skill.name).includes(queryNorm) || norm(skill.id).includes(queryNorm)) {
|
|
67
|
+
return 'name';
|
|
68
|
+
}
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Rank all matching skills by score, with `limit` cap. If no matches,
|
|
73
|
+
* returns the top-5 by global success rate (fallback).
|
|
74
|
+
*/
|
|
75
|
+
export async function suggestSkills(query, options = {}) {
|
|
76
|
+
const limit = options.limit ?? 5;
|
|
77
|
+
const catalog = options.catalog ?? listSkills();
|
|
78
|
+
const records = options.records ?? await readSkillHistory(process.env.ANATHEMA_SKILL_HISTORY_FILE
|
|
79
|
+
?? `${process.env.HOME ?? '/tmp'}/.tmp/anathema-coder/skill-history.jsonl`);
|
|
80
|
+
const queryNorm = norm(query);
|
|
81
|
+
// 1. Find candidates + classify match
|
|
82
|
+
const candidates = [];
|
|
83
|
+
for (const skill of catalog) {
|
|
84
|
+
const m = classifyMatch(skill, queryNorm);
|
|
85
|
+
if (m !== null)
|
|
86
|
+
candidates.push({ skill, matchReason: m });
|
|
87
|
+
}
|
|
88
|
+
// 2a. If we have candidates, score and sort
|
|
89
|
+
if (candidates.length > 0) {
|
|
90
|
+
const scored = candidates.map(({ skill, matchReason }) => {
|
|
91
|
+
const stats = getSkillStats(records, skill.id);
|
|
92
|
+
const score = computeSuggestionScore(stats, matchReason);
|
|
93
|
+
return { skill, stats, score, matchReason };
|
|
94
|
+
});
|
|
95
|
+
scored.sort((a, b) => b.score - a.score);
|
|
96
|
+
return scored.slice(0, limit);
|
|
97
|
+
}
|
|
98
|
+
// 2b. Fallback: top-5 by global success rate
|
|
99
|
+
const fallback = catalog
|
|
100
|
+
.map((skill) => {
|
|
101
|
+
const stats = getSkillStats(records, skill.id);
|
|
102
|
+
return { skill, stats, matchReason: 'fallback-popular' };
|
|
103
|
+
})
|
|
104
|
+
.sort((a, b) => b.stats.successRate - a.stats.successRate || b.stats.count - a.stats.count)
|
|
105
|
+
.slice(0, 5)
|
|
106
|
+
.map((entry) => ({
|
|
107
|
+
...entry,
|
|
108
|
+
score: computeSuggestionScore(entry.stats, entry.matchReason),
|
|
109
|
+
}));
|
|
110
|
+
return fallback;
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Format suggestion entries as a human-readable CLI output string.
|
|
114
|
+
* Pure helper — testable without React/Ink.
|
|
115
|
+
*/
|
|
116
|
+
export function formatSuggestions(entries) {
|
|
117
|
+
if (entries.length === 0) {
|
|
118
|
+
return '[skill-suggest] no skills available';
|
|
119
|
+
}
|
|
120
|
+
const lines = ['[skill-suggest] recommended skills:'];
|
|
121
|
+
for (const e of entries) {
|
|
122
|
+
const reason = e.matchReason === 'tag' ? '(tag match)'
|
|
123
|
+
: e.matchReason === 'name' ? '(name match)'
|
|
124
|
+
: '(popular fallback)';
|
|
125
|
+
const statsStr = e.stats.count === 0
|
|
126
|
+
? 'no history yet'
|
|
127
|
+
: `${e.stats.count} runs, ${(e.stats.successRate * 100).toFixed(0)}% ok, avg ${e.stats.avgDurationMs.toFixed(0)}ms`;
|
|
128
|
+
lines.push(` ${e.skill.id} — ${e.skill.name} — score ${e.score.toFixed(2)} — ${statsStr} ${reason}`);
|
|
129
|
+
}
|
|
130
|
+
return lines.join('\n');
|
|
131
|
+
}
|
|
132
|
+
//# sourceMappingURL=skillSuggest.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"skillSuggest.js","sourceRoot":"","sources":["../../src/cli/skillSuggest.ts"],"names":[],"mappings":"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"}
|