om-memory-system 4.29.1 → 4.31.1
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 +1 -0
- package/dist/adapters/opencode/conversation.js +22 -17
- package/dist/adapters/opencode/skills-tool.d.ts +3 -0
- package/dist/adapters/opencode/skills-tool.js +31 -0
- package/dist/adapters/pi/capture.js +39 -23
- package/dist/adapters/pi/extension.d.ts +1 -0
- package/dist/adapters/pi/extension.js +3 -0
- package/dist/adapters/pi/skills-tool.d.ts +3 -0
- package/dist/adapters/pi/skills-tool.js +34 -0
- package/dist/cli/index.js +5 -0
- package/dist/cli/skills-command.d.ts +12 -0
- package/dist/cli/skills-command.js +242 -0
- package/dist/cli/skills-record-past.d.ts +5 -0
- package/dist/cli/skills-record-past.js +85 -0
- package/dist/config-template.d.ts +1 -1
- package/dist/config-template.js +12 -0
- package/dist/config.d.ts +64 -48
- package/dist/config.js +28 -5
- package/dist/core/host.d.ts +7 -0
- package/dist/core/profile-backoff.d.ts +16 -8
- package/dist/core/profile-backoff.js +25 -16
- package/dist/core/skill-agent-modes.d.ts +4 -0
- package/dist/core/skill-agent-modes.js +145 -0
- package/dist/core/skill-operation-reply.d.ts +16 -0
- package/dist/core/skill-operation-reply.js +35 -0
- package/dist/core/skill-operations.d.ts +58 -0
- package/dist/core/skill-operations.js +296 -0
- package/dist/core/skills-tool.d.ts +90 -0
- package/dist/core/skills-tool.js +64 -0
- package/dist/core/tool-input.d.ts +7 -0
- package/dist/core/tool-input.js +21 -0
- package/dist/core/tool-outcome.d.ts +13 -0
- package/dist/core/tool-outcome.js +22 -0
- package/dist/core/turn-outputs.d.ts +30 -0
- package/dist/core/turn-outputs.js +22 -0
- package/dist/importer/auto-backfill.js +13 -1
- package/dist/importer/claude-conversation.d.ts +1 -0
- package/dist/importer/claude-conversation.js +44 -10
- package/dist/importer/claude-hook-api.d.ts +0 -8
- package/dist/importer/claude-hook-api.js +36 -65
- package/dist/importer/claude-import.js +3 -1
- package/dist/importer/claude-reader.d.ts +2 -0
- package/dist/importer/claude-reader.js +2 -2
- package/dist/importer/claude-turn-units.d.ts +26 -0
- package/dist/importer/claude-turn-units.js +85 -0
- package/dist/importer/import-args.d.ts +3 -0
- package/dist/importer/import-args.js +8 -2
- package/dist/importer/import-project.d.ts +6 -0
- package/dist/importer/import-project.js +9 -0
- package/dist/importer/import-runs.d.ts +8 -2
- package/dist/importer/import-runs.js +3 -3
- package/dist/importer/import-sessions.d.ts +8 -1
- package/dist/importer/import-sessions.js +17 -4
- package/dist/importer/import-turn-outputs.d.ts +49 -0
- package/dist/importer/import-turn-outputs.js +88 -0
- package/dist/importer/importer.d.ts +5 -2
- package/dist/importer/importer.js +18 -32
- package/dist/importer/opencode-import.d.ts +2 -0
- package/dist/importer/opencode-import.js +6 -3
- package/dist/importer/opencode-reader.js +11 -5
- package/dist/importer/pi-conversation.d.ts +5 -0
- package/dist/importer/pi-conversation.js +33 -9
- package/dist/importer/profile-catch-up.d.ts +1 -1
- package/dist/importer/record-past-turns.d.ts +28 -0
- package/dist/importer/record-past-turns.js +63 -0
- package/dist/importer/run-import.d.ts +4 -0
- package/dist/importer/run-import.js +24 -3
- package/dist/importer/web-import-api.d.ts +14 -0
- package/dist/importer/web-import-api.js +6 -0
- package/dist/importer/web-import-group.d.ts +4 -1
- package/dist/importer/web-import-group.js +26 -6
- package/dist/importer/web-import-jobs.js +6 -2
- package/dist/index.js +6 -1
- package/dist/services/ai/session/ai-session-manager.js +13 -0
- package/dist/services/auto-capture.d.ts +1 -1
- package/dist/services/auto-capture.js +71 -19
- package/dist/services/capture-attempt-store.d.ts +2 -0
- package/dist/services/capture-attempt-store.js +11 -0
- package/dist/services/chat/chat-runtime.d.ts +2 -0
- package/dist/services/chat/chat-runtime.js +3 -2
- package/dist/services/global-config-writer.js +32 -2
- package/dist/services/profile-cleanup-routes.d.ts +2 -2
- package/dist/services/profile-cleanup-routes.js +19 -2
- package/dist/services/settings-snapshot.js +7 -1
- package/dist/services/skill-learning/agent-proposals.d.ts +76 -0
- package/dist/services/skill-learning/agent-proposals.js +180 -0
- package/dist/services/skill-learning/apply.d.ts +50 -0
- package/dist/services/skill-learning/apply.js +247 -0
- package/dist/services/skill-learning/auto-review.d.ts +40 -0
- package/dist/services/skill-learning/auto-review.js +132 -0
- package/dist/services/skill-learning/draft-turns.d.ts +60 -0
- package/dist/services/skill-learning/draft-turns.js +131 -0
- package/dist/services/skill-learning/drafting.d.ts +73 -0
- package/dist/services/skill-learning/drafting.js +263 -0
- package/dist/services/skill-learning/line-diff.d.ts +6 -0
- package/dist/services/skill-learning/line-diff.js +94 -0
- package/dist/services/skill-learning/operations-deps.d.ts +7 -0
- package/dist/services/skill-learning/operations-deps.js +19 -0
- package/dist/services/skill-learning/proposal-store.d.ts +82 -0
- package/dist/services/skill-learning/proposal-store.js +164 -0
- package/dist/services/skill-learning/ranking.d.ts +36 -0
- package/dist/services/skill-learning/ranking.js +140 -0
- package/dist/services/skill-learning/settings-for.d.ts +11 -0
- package/dist/services/skill-learning/settings-for.js +21 -0
- package/dist/services/skill-learning/skill-config.d.ts +45 -0
- package/dist/services/skill-learning/skill-config.js +70 -0
- package/dist/services/skill-learning/skill-detect.d.ts +29 -0
- package/dist/services/skill-learning/skill-detect.js +130 -0
- package/dist/services/skill-learning/skill-folders.d.ts +24 -0
- package/dist/services/skill-learning/skill-folders.js +42 -0
- package/dist/services/skill-learning/skill-index.d.ts +28 -0
- package/dist/services/skill-learning/skill-index.js +118 -0
- package/dist/services/skill-learning/skill-model.d.ts +10 -0
- package/dist/services/skill-learning/skill-model.js +15 -0
- package/dist/services/skill-learning/store.d.ts +79 -0
- package/dist/services/skill-learning/store.js +245 -0
- package/dist/services/skill-learning/turn-output.d.ts +34 -0
- package/dist/services/skill-learning/turn-output.js +98 -0
- package/dist/services/skill-learning/turn-summaries.d.ts +24 -0
- package/dist/services/skill-learning/turn-summaries.js +64 -0
- package/dist/services/skill-learning/web-review.d.ts +18 -0
- package/dist/services/skill-learning/web-review.js +44 -0
- package/dist/services/skill-routes.d.ts +20 -0
- package/dist/services/skill-routes.js +249 -0
- package/dist/services/turso/connection-manager.js +11 -7
- package/dist/services/turso/table-lease.d.ts +40 -0
- package/dist/services/turso/table-lease.js +77 -0
- package/dist/services/turso/turso-db.d.ts +4 -0
- package/dist/services/turso/turso-db.js +13 -0
- package/dist/services/user-prompt/profile-catch-up-lease.d.ts +3 -22
- package/dist/services/user-prompt/profile-catch-up-lease.js +9 -65
- package/dist/services/web-jobs.d.ts +7 -0
- package/dist/services/web-jobs.js +14 -0
- package/dist/services/web-route-modules.d.ts +2 -2
- package/dist/services/web-route-modules.js +6 -1
- package/dist/services/web-server.js +13 -21
- package/dist/v2/adapter.js +13 -8
- package/dist/web/assets/index-9hu4Z7SV.css +2 -0
- package/dist/web/assets/index-BMt9gPNK.js +129 -0
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
- package/dist/web/assets/index-CHBKLTiT.js +0 -126
- package/dist/web/assets/index-DsQXsY3T.css +0 -2
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { homedir } from "node:os";
|
|
2
|
+
import { isAbsolute, join, resolve } from "node:path";
|
|
3
|
+
function expandHome(path, homeDir) {
|
|
4
|
+
return path.startsWith("~/") || path.startsWith("~\\") ? join(homeDir, path.slice(2)) : path;
|
|
5
|
+
}
|
|
6
|
+
function checkedBoolean(value, key, fallback) {
|
|
7
|
+
if (value === undefined)
|
|
8
|
+
return fallback;
|
|
9
|
+
if (typeof value !== "boolean")
|
|
10
|
+
throw new Error(`Invalid ${key} config`);
|
|
11
|
+
return value;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Validate `skillFolders`: absolute paths or paths that start with `~/`,
|
|
15
|
+
* expanded and de-duplicated. Throws on the first invalid entry, like the
|
|
16
|
+
* other list settings. Whether a folder exists is checked when the setting is
|
|
17
|
+
* saved, not here, so a folder on a drive that is not mounted does not break
|
|
18
|
+
* the config.
|
|
19
|
+
*/
|
|
20
|
+
function parseSkillFolders(value, homeDir) {
|
|
21
|
+
if (value === undefined)
|
|
22
|
+
return undefined;
|
|
23
|
+
if (!Array.isArray(value))
|
|
24
|
+
throw new Error("Invalid skillFolders config: must be a list");
|
|
25
|
+
const folders = value.map((entry, index) => {
|
|
26
|
+
const trimmed = typeof entry === "string" ? entry.trim() : "";
|
|
27
|
+
const expanded = expandHome(trimmed, homeDir);
|
|
28
|
+
if (!trimmed || !isAbsolute(expanded)) {
|
|
29
|
+
throw new Error(`Invalid skillFolders config: entry ${index} needs an absolute path or a path that starts with ~/`);
|
|
30
|
+
}
|
|
31
|
+
return resolve(expanded);
|
|
32
|
+
});
|
|
33
|
+
return [...new Set(folders)];
|
|
34
|
+
}
|
|
35
|
+
/** The three settings from a merged file config, with defaults applied. */
|
|
36
|
+
export function normaliseSkillConfig(fileConfig, homeDir = homedir()) {
|
|
37
|
+
return {
|
|
38
|
+
skillLearning: checkedBoolean(fileConfig.skillLearning, "skillLearning", false),
|
|
39
|
+
skillLearningRedact: checkedBoolean(fileConfig.skillLearningRedact, "skillLearningRedact", true),
|
|
40
|
+
skillFolders: parseSkillFolders(fileConfig.skillFolders, homeDir),
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* The first `skillFolders` entry, as written, that is not an existing folder,
|
|
45
|
+
* or null when every entry is one. The Settings save refuses a missing folder
|
|
46
|
+
* and names it. `isFolder` is passed in so the caller owns the file system access.
|
|
47
|
+
*/
|
|
48
|
+
export function missingSkillFolder(folders, isFolder, homeDir = homedir()) {
|
|
49
|
+
return folders.find((folder) => !isFolder(resolve(expandHome(folder.trim(), homeDir)))) ?? null;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* What a project config may change. A checked-in project file must not start
|
|
53
|
+
* recording conversations, so it can only turn `skillLearning` off. Its
|
|
54
|
+
* `skillLearningRedact` and `skillFolders` are always ignored.
|
|
55
|
+
* `ignoredTurnOn` tells the caller to log a project value of `true`.
|
|
56
|
+
*/
|
|
57
|
+
export function projectSkillOverride(project) {
|
|
58
|
+
return {
|
|
59
|
+
override: project.skillLearning === false ? { skillLearning: false } : {},
|
|
60
|
+
ignoredTurnOn: project.skillLearning === true,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* The folders to watch when `skillFolders` is unset: `~/.agents/skills` and
|
|
65
|
+
* `<Claude Code folder>/skills`, each only when it exists. `exists` is passed
|
|
66
|
+
* in so the caller owns the file system access.
|
|
67
|
+
*/
|
|
68
|
+
export function defaultSkillFolders(homeDir, claudeFolder, exists) {
|
|
69
|
+
return [join(homeDir, ".agents", "skills"), join(claudeFolder, "skills")].filter(exists);
|
|
70
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { SkillIndex } from "./skill-index.js";
|
|
2
|
+
import type { SkillUseRow } from "./store.js";
|
|
3
|
+
/** The parts of a work unit that skill detection reads. */
|
|
4
|
+
export interface SkillDetectInput {
|
|
5
|
+
prompt: string;
|
|
6
|
+
toolCalls: ReadonlyArray<DetectToolCall>;
|
|
7
|
+
}
|
|
8
|
+
/** `path` is the full read path the host readers keep; `input` can be cut. */
|
|
9
|
+
interface DetectToolCall {
|
|
10
|
+
name: string;
|
|
11
|
+
input: string;
|
|
12
|
+
path?: string;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Reads one string field from a tool input as the host readers format it:
|
|
16
|
+
* `key: "value", key2: ...` (Claude Code, Pi, OpenCode live) or JSON
|
|
17
|
+
* (OpenCode import). Inputs can be cut at 100 characters, so this does not
|
|
18
|
+
* parse the whole string.
|
|
19
|
+
*/
|
|
20
|
+
export declare function toolInputField(input: string, key: string): string | null;
|
|
21
|
+
/**
|
|
22
|
+
* Finds the skills a turn used. `tool`: the host's skill tool loaded it.
|
|
23
|
+
* `read`: a read tool opened a `SKILL.md` inside a watched folder.
|
|
24
|
+
* `command`: the prompt invoked a known skill by its slash form.
|
|
25
|
+
* Each skill is listed once per method. `position` is the index of the tool
|
|
26
|
+
* call that loaded the skill (the first one for a repeat), and -1 for `command`.
|
|
27
|
+
*/
|
|
28
|
+
export declare function detectSkillUses(unit: SkillDetectInput, index: SkillIndex): SkillUseRow[];
|
|
29
|
+
export {};
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { homedir } from "node:os";
|
|
2
|
+
import { basename, dirname, resolve, sep } from "node:path";
|
|
3
|
+
import { realPathOrSelf } from "./skill-folders.js";
|
|
4
|
+
const READ_TOOLS = new Set(["read", "view"]);
|
|
5
|
+
const READ_PATH_KEYS = ["file_path", "filePath", "path"];
|
|
6
|
+
/**
|
|
7
|
+
* Reads one string field from a tool input as the host readers format it:
|
|
8
|
+
* `key: "value", key2: ...` (Claude Code, Pi, OpenCode live) or JSON
|
|
9
|
+
* (OpenCode import). Inputs can be cut at 100 characters, so this does not
|
|
10
|
+
* parse the whole string.
|
|
11
|
+
*/
|
|
12
|
+
export function toolInputField(input, key) {
|
|
13
|
+
const escaped = key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
14
|
+
const pattern = new RegExp(`(?:^|[,{]\\s*)"?${escaped}"?\\s*:\\s*("(?:[^"\\\\]|\\\\.)*")`);
|
|
15
|
+
const match = pattern.exec(input);
|
|
16
|
+
if (!match)
|
|
17
|
+
return null;
|
|
18
|
+
try {
|
|
19
|
+
const value = JSON.parse(match[1]);
|
|
20
|
+
return typeof value === "string" ? value : null;
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
function byName(index, name) {
|
|
27
|
+
return index.entries.filter((entry) => entry.name === name);
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* A `plugin:skill` name matches a watched skill on the part after the colon.
|
|
31
|
+
* Otherwise it keeps its full form, so a command such as `opsx:apply` is not shown as `apply`.
|
|
32
|
+
*/
|
|
33
|
+
function skillName(name, index) {
|
|
34
|
+
const trimmed = name.trim();
|
|
35
|
+
const bare = trimmed.slice(trimmed.lastIndexOf(":") + 1);
|
|
36
|
+
return byName(index, bare).length > 0 ? bare : trimmed;
|
|
37
|
+
}
|
|
38
|
+
/** A path only when the name points at exactly one skill. */
|
|
39
|
+
function pathFor(index, name) {
|
|
40
|
+
const found = byName(index, name);
|
|
41
|
+
return found.length === 1 ? found[0].path : null;
|
|
42
|
+
}
|
|
43
|
+
function skillToolName(call) {
|
|
44
|
+
if (call.name === "Skill")
|
|
45
|
+
return toolInputField(call.input, "skill");
|
|
46
|
+
if (call.name === "skill")
|
|
47
|
+
return toolInputField(call.input, "name");
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
function expandHome(path) {
|
|
51
|
+
if (path === "~")
|
|
52
|
+
return homedir();
|
|
53
|
+
if (path.startsWith("~/"))
|
|
54
|
+
return resolve(homedir(), path.slice(2));
|
|
55
|
+
return path;
|
|
56
|
+
}
|
|
57
|
+
function insideFolder(path, folder) {
|
|
58
|
+
const root = resolve(expandHome(folder));
|
|
59
|
+
return path.startsWith(root.endsWith(sep) ? root : root + sep);
|
|
60
|
+
}
|
|
61
|
+
function readSkillUse(call, index) {
|
|
62
|
+
if (!READ_TOOLS.has(call.name.toLowerCase()))
|
|
63
|
+
return null;
|
|
64
|
+
// Prefer the full path; the input text is cut at 100 characters.
|
|
65
|
+
const candidates = call.path !== undefined
|
|
66
|
+
? [call.path]
|
|
67
|
+
: READ_PATH_KEYS.map((key) => toolInputField(call.input, key));
|
|
68
|
+
for (const raw of candidates) {
|
|
69
|
+
if (!raw || !/[\\/]SKILL\.md$/.test(raw))
|
|
70
|
+
continue;
|
|
71
|
+
const path = resolve(expandHome(raw));
|
|
72
|
+
// A read through a linked folder matches the indexed file by its real path.
|
|
73
|
+
const real = realPathOrSelf(path);
|
|
74
|
+
const entry = index.entries.find((e) => resolve(expandHome(e.path)) === path) ??
|
|
75
|
+
index.entries.find((e) => realPathOrSelf(e.path) === real);
|
|
76
|
+
if (entry)
|
|
77
|
+
return { name: entry.name, path: entry.path, method: "read" };
|
|
78
|
+
if (!index.folders.some((folder) => insideFolder(path, folder)))
|
|
79
|
+
return null;
|
|
80
|
+
return { name: basename(dirname(path)), path, method: "read" };
|
|
81
|
+
}
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
/** The skill names a prompt invokes by its slash form. Unknown names are left to the caller. */
|
|
85
|
+
function commandNames(prompt, index) {
|
|
86
|
+
const names = [];
|
|
87
|
+
const lead = /^\/([\w:.-]+)(?:\s|$)/.exec(prompt.trimStart());
|
|
88
|
+
if (lead)
|
|
89
|
+
names.push(skillName(lead[1], index));
|
|
90
|
+
for (const match of prompt.matchAll(/<command-name>\s*\/([\w:.-]+)\s*<\/command-name>/g)) {
|
|
91
|
+
names.push(skillName(match[1], index));
|
|
92
|
+
}
|
|
93
|
+
return names;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Finds the skills a turn used. `tool`: the host's skill tool loaded it.
|
|
97
|
+
* `read`: a read tool opened a `SKILL.md` inside a watched folder.
|
|
98
|
+
* `command`: the prompt invoked a known skill by its slash form.
|
|
99
|
+
* Each skill is listed once per method. `position` is the index of the tool
|
|
100
|
+
* call that loaded the skill (the first one for a repeat), and -1 for `command`.
|
|
101
|
+
*/
|
|
102
|
+
export function detectSkillUses(unit, index) {
|
|
103
|
+
const uses = [];
|
|
104
|
+
const seen = new Set();
|
|
105
|
+
const add = (use) => {
|
|
106
|
+
const key = `${use.method}\u0000${use.name}\u0000${use.path ?? ""}`;
|
|
107
|
+
if (seen.has(key))
|
|
108
|
+
return;
|
|
109
|
+
seen.add(key);
|
|
110
|
+
uses.push(use);
|
|
111
|
+
};
|
|
112
|
+
for (const name of commandNames(unit.prompt, index)) {
|
|
113
|
+
if (byName(index, name).length > 0) {
|
|
114
|
+
add({ name, path: pathFor(index, name), method: "command", position: -1 });
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
for (const [position, call] of unit.toolCalls.entries()) {
|
|
118
|
+
const toolName = skillToolName(call);
|
|
119
|
+
if (toolName) {
|
|
120
|
+
const name = skillName(toolName, index);
|
|
121
|
+
if (name)
|
|
122
|
+
add({ name, path: pathFor(index, name), method: "tool", position });
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
const read = readSkillUse(call, index);
|
|
126
|
+
if (read)
|
|
127
|
+
add({ ...read, position });
|
|
128
|
+
}
|
|
129
|
+
return uses;
|
|
130
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The watched skill folders, in one place for every caller. The caller passes
|
|
3
|
+
* `CONFIG`, because many tests stub `src/config.js`.
|
|
4
|
+
*/
|
|
5
|
+
/** The real path, or the path as given when it does not exist. */
|
|
6
|
+
export declare function realPathOrSelf(path: string): string;
|
|
7
|
+
/**
|
|
8
|
+
* `skillFolders`, or the default folders when it is unset. Folders with the
|
|
9
|
+
* same real path, such as `~/.claude/skills` linked to `~/.agents/skills`,
|
|
10
|
+
* are watched once, under the first spelling.
|
|
11
|
+
*/
|
|
12
|
+
export declare function watchedSkillFolders(config: {
|
|
13
|
+
skillFolders?: string[];
|
|
14
|
+
claudeConfigDir?: string;
|
|
15
|
+
}): string[];
|
|
16
|
+
/**
|
|
17
|
+
* Where `skills.db` lives: the global `storagePath`, never a project value.
|
|
18
|
+
* `initConfig` sets `skillStoragePath` only when a project config moved
|
|
19
|
+
* `storagePath`; otherwise `storagePath` is the global one.
|
|
20
|
+
*/
|
|
21
|
+
export declare function skillStoragePath(config: {
|
|
22
|
+
storagePath: string;
|
|
23
|
+
skillStoragePath?: string;
|
|
24
|
+
}): string;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { existsSync, realpathSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { resolveClaudeFolder } from "../claude-folder.js";
|
|
4
|
+
import { defaultSkillFolders } from "./skill-config.js";
|
|
5
|
+
/**
|
|
6
|
+
* The watched skill folders, in one place for every caller. The caller passes
|
|
7
|
+
* `CONFIG`, because many tests stub `src/config.js`.
|
|
8
|
+
*/
|
|
9
|
+
/** The real path, or the path as given when it does not exist. */
|
|
10
|
+
export function realPathOrSelf(path) {
|
|
11
|
+
try {
|
|
12
|
+
return realpathSync(path);
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
return path;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* `skillFolders`, or the default folders when it is unset. Folders with the
|
|
20
|
+
* same real path, such as `~/.claude/skills` linked to `~/.agents/skills`,
|
|
21
|
+
* are watched once, under the first spelling.
|
|
22
|
+
*/
|
|
23
|
+
export function watchedSkillFolders(config) {
|
|
24
|
+
const folders = config.skillFolders ??
|
|
25
|
+
defaultSkillFolders(homedir(), resolveClaudeFolder(config.claudeConfigDir).folder, existsSync);
|
|
26
|
+
const seen = new Set();
|
|
27
|
+
return folders.filter((folder) => {
|
|
28
|
+
const real = realPathOrSelf(folder);
|
|
29
|
+
if (seen.has(real))
|
|
30
|
+
return false;
|
|
31
|
+
seen.add(real);
|
|
32
|
+
return true;
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Where `skills.db` lives: the global `storagePath`, never a project value.
|
|
37
|
+
* `initConfig` sets `skillStoragePath` only when a project config moved
|
|
38
|
+
* `storagePath`; otherwise `storagePath` is the global one.
|
|
39
|
+
*/
|
|
40
|
+
export function skillStoragePath(config) {
|
|
41
|
+
return config.skillStoragePath ?? config.storagePath;
|
|
42
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/** How many folder levels below a watched folder are searched for `SKILL.md`. */
|
|
2
|
+
export declare const SKILL_INDEX_MAX_DEPTH = 4;
|
|
3
|
+
export declare const SKILL_INDEX_CACHE_MS = 60000;
|
|
4
|
+
export interface SkillIndexEntry {
|
|
5
|
+
/** The front matter `name`, or the folder name when the front matter has none. */
|
|
6
|
+
name: string;
|
|
7
|
+
/** Full path of the `SKILL.md` file. */
|
|
8
|
+
path: string;
|
|
9
|
+
/** The watched folder the skill was found in. */
|
|
10
|
+
folder: string;
|
|
11
|
+
description: string;
|
|
12
|
+
}
|
|
13
|
+
export interface SkillIndex {
|
|
14
|
+
folders: string[];
|
|
15
|
+
/** Every skill found. Two skills can share a name; both are kept. */
|
|
16
|
+
entries: SkillIndexEntry[];
|
|
17
|
+
}
|
|
18
|
+
/** Reads top-level `key: value` pairs from YAML front matter. Folded values join their lines. */
|
|
19
|
+
export declare function readFrontMatter(text: string): Record<string, string>;
|
|
20
|
+
/**
|
|
21
|
+
* Lists every `SKILL.md` up to four folder levels below each watched folder.
|
|
22
|
+
* A file reached twice through a link is listed once, under its first path. No cache.
|
|
23
|
+
*/
|
|
24
|
+
export declare function buildSkillIndex(folders: readonly string[]): SkillIndex;
|
|
25
|
+
/** The index for these folders, rebuilt when it is older than 60 seconds or the list changes. */
|
|
26
|
+
export declare function getSkillIndex(folders: readonly string[], now?: number): SkillIndex;
|
|
27
|
+
/** Drops the cached index, for example after OMMS writes a skill file. */
|
|
28
|
+
export declare function clearSkillIndexCache(): void;
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { readdirSync, readFileSync, statSync } from "node:fs";
|
|
2
|
+
import { basename, dirname, join } from "node:path";
|
|
3
|
+
import { realPathOrSelf } from "./skill-folders.js";
|
|
4
|
+
/** How many folder levels below a watched folder are searched for `SKILL.md`. */
|
|
5
|
+
export const SKILL_INDEX_MAX_DEPTH = 4;
|
|
6
|
+
export const SKILL_INDEX_CACHE_MS = 60_000;
|
|
7
|
+
const SKIP_FOLDERS = new Set([".git", "node_modules"]);
|
|
8
|
+
function unquote(value) {
|
|
9
|
+
const trimmed = value.trim();
|
|
10
|
+
if (trimmed.length >= 2 &&
|
|
11
|
+
(trimmed[0] === '"' || trimmed[0] === "'") &&
|
|
12
|
+
trimmed[trimmed.length - 1] === trimmed[0]) {
|
|
13
|
+
return trimmed.slice(1, -1);
|
|
14
|
+
}
|
|
15
|
+
return trimmed;
|
|
16
|
+
}
|
|
17
|
+
/** Reads top-level `key: value` pairs from YAML front matter. Folded values join their lines. */
|
|
18
|
+
export function readFrontMatter(text) {
|
|
19
|
+
const match = /^---\r?\n([\s\S]*?)\r?\n---/.exec(text);
|
|
20
|
+
if (!match)
|
|
21
|
+
return {};
|
|
22
|
+
const fields = {};
|
|
23
|
+
const lines = match[1].split(/\r?\n/);
|
|
24
|
+
for (let i = 0; i < lines.length; i++) {
|
|
25
|
+
const field = /^([A-Za-z_][\w-]*):\s*(.*)$/.exec(lines[i]);
|
|
26
|
+
if (!field)
|
|
27
|
+
continue;
|
|
28
|
+
let value = field[2].trim();
|
|
29
|
+
if (value === "" || /^[>|][+-]?$/.test(value)) {
|
|
30
|
+
const block = [];
|
|
31
|
+
while (i + 1 < lines.length && /^\s+\S/.test(lines[i + 1]))
|
|
32
|
+
block.push(lines[++i].trim());
|
|
33
|
+
value = block.join(" ");
|
|
34
|
+
}
|
|
35
|
+
fields[field[1]] = unquote(value);
|
|
36
|
+
}
|
|
37
|
+
return fields;
|
|
38
|
+
}
|
|
39
|
+
function isDirectory(path) {
|
|
40
|
+
try {
|
|
41
|
+
// statSync follows links, so linked skill folders are searched too.
|
|
42
|
+
return statSync(path).isDirectory();
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
function readEntry(path, folder) {
|
|
49
|
+
let fields = {};
|
|
50
|
+
try {
|
|
51
|
+
fields = readFrontMatter(readFileSync(path, "utf8"));
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
// An unreadable file still names a skill by its folder.
|
|
55
|
+
}
|
|
56
|
+
return {
|
|
57
|
+
name: fields.name || basename(dirname(path)),
|
|
58
|
+
path,
|
|
59
|
+
folder,
|
|
60
|
+
description: fields.description ?? "",
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
function walk(dir, depth, folder, out) {
|
|
64
|
+
let names;
|
|
65
|
+
try {
|
|
66
|
+
names = readdirSync(dir);
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
if (names.includes("SKILL.md"))
|
|
72
|
+
out.push(readEntry(join(dir, "SKILL.md"), folder));
|
|
73
|
+
if (depth >= SKILL_INDEX_MAX_DEPTH)
|
|
74
|
+
return;
|
|
75
|
+
for (const name of names.sort()) {
|
|
76
|
+
if (SKIP_FOLDERS.has(name))
|
|
77
|
+
continue;
|
|
78
|
+
const child = join(dir, name);
|
|
79
|
+
if (isDirectory(child))
|
|
80
|
+
walk(child, depth + 1, folder, out);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Lists every `SKILL.md` up to four folder levels below each watched folder.
|
|
85
|
+
* A file reached twice through a link is listed once, under its first path. No cache.
|
|
86
|
+
*/
|
|
87
|
+
export function buildSkillIndex(folders) {
|
|
88
|
+
const found = [];
|
|
89
|
+
for (const folder of folders) {
|
|
90
|
+
if (isDirectory(folder))
|
|
91
|
+
walk(folder, 0, folder, found);
|
|
92
|
+
}
|
|
93
|
+
const seen = new Set();
|
|
94
|
+
const entries = found.filter((entry) => {
|
|
95
|
+
const real = realPathOrSelf(entry.path);
|
|
96
|
+
if (seen.has(real))
|
|
97
|
+
return false;
|
|
98
|
+
seen.add(real);
|
|
99
|
+
return true;
|
|
100
|
+
});
|
|
101
|
+
return { folders: [...folders], entries };
|
|
102
|
+
}
|
|
103
|
+
const cache = new Map();
|
|
104
|
+
/** The index for these folders, rebuilt when it is older than 60 seconds or the list changes. */
|
|
105
|
+
export function getSkillIndex(folders, now = Date.now()) {
|
|
106
|
+
const key = JSON.stringify(folders);
|
|
107
|
+
const hit = cache.get(key);
|
|
108
|
+
if (hit && now - hit.builtAt < SKILL_INDEX_CACHE_MS)
|
|
109
|
+
return hit.index;
|
|
110
|
+
const index = buildSkillIndex(folders);
|
|
111
|
+
cache.clear();
|
|
112
|
+
cache.set(key, { builtAt: now, index });
|
|
113
|
+
return index;
|
|
114
|
+
}
|
|
115
|
+
/** Drops the cached index, for example after OMMS writes a skill file. */
|
|
116
|
+
export function clearSkillIndexCache() {
|
|
117
|
+
cache.clear();
|
|
118
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { type ChatRuntimeConfig } from "../chat/chat-runtime.js";
|
|
2
|
+
import type { SkillModel } from "./drafting.js";
|
|
3
|
+
/** A draft sends a whole SKILL.md and up to 60,000 characters of turns, so it gets longer than chat. */
|
|
4
|
+
export declare const SKILL_MODEL_TIMEOUT_MS = 120000;
|
|
5
|
+
/**
|
|
6
|
+
* The external API model through the chat's client and readiness check
|
|
7
|
+
* (ADR-033). Claude Code has no session model, so drafting uses this on
|
|
8
|
+
* every host. The caller passes `CONFIG`.
|
|
9
|
+
*/
|
|
10
|
+
export declare function skillModel(config: ChatRuntimeConfig): SkillModel;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { callTool } from "../chat/chat-runtime.js";
|
|
2
|
+
import { chatStatus } from "../chat/chat-status.js";
|
|
3
|
+
/** A draft sends a whole SKILL.md and up to 60,000 characters of turns, so it gets longer than chat. */
|
|
4
|
+
export const SKILL_MODEL_TIMEOUT_MS = 120_000;
|
|
5
|
+
/**
|
|
6
|
+
* The external API model through the chat's client and readiness check
|
|
7
|
+
* (ADR-033). Claude Code has no session model, so drafting uses this on
|
|
8
|
+
* every host. The caller passes `CONFIG`.
|
|
9
|
+
*/
|
|
10
|
+
export function skillModel(config) {
|
|
11
|
+
return {
|
|
12
|
+
ready: chatStatus(config).ready,
|
|
13
|
+
callTool: (system, user, tool) => callTool(config, system, user, tool, SKILL_MODEL_TIMEOUT_MS),
|
|
14
|
+
};
|
|
15
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import type { TursoDb } from "../turso/turso-db.js";
|
|
2
|
+
/** Bump when the tables change, and add the step to `init`. */
|
|
3
|
+
export declare const SKILL_SCHEMA_VERSION = 1;
|
|
4
|
+
export declare const SKILLS_DB_FILE = "skills.db";
|
|
5
|
+
export type SkillToolOutcome = "ok" | "error" | "unknown";
|
|
6
|
+
export type SkillUseMethod = "tool" | "read" | "command";
|
|
7
|
+
export interface SkillToolCallRow {
|
|
8
|
+
name: string;
|
|
9
|
+
input: string;
|
|
10
|
+
outcome: SkillToolOutcome;
|
|
11
|
+
error: string | null;
|
|
12
|
+
}
|
|
13
|
+
export interface SkillUseRow {
|
|
14
|
+
name: string;
|
|
15
|
+
path: string | null;
|
|
16
|
+
method: SkillUseMethod;
|
|
17
|
+
/**
|
|
18
|
+
* Index of the tool call that loaded the skill, or -1 for a slash command
|
|
19
|
+
* (every tool call in the turn follows it). Missing or null: treated as -1.
|
|
20
|
+
*/
|
|
21
|
+
position?: number | null;
|
|
22
|
+
}
|
|
23
|
+
/** One finished turn as it is written to `skills.db`. */
|
|
24
|
+
export interface SkillTurnRecord {
|
|
25
|
+
/** `<host>:<session id>:<user message id>`. */
|
|
26
|
+
turnKey: string;
|
|
27
|
+
host: string;
|
|
28
|
+
sessionId: string;
|
|
29
|
+
projectTag: string | null;
|
|
30
|
+
projectName: string | null;
|
|
31
|
+
provider: string | null;
|
|
32
|
+
model: string | null;
|
|
33
|
+
createdAt: number;
|
|
34
|
+
prompt: string;
|
|
35
|
+
replies: string[];
|
|
36
|
+
redacted: boolean;
|
|
37
|
+
toolCalls: SkillToolCallRow[];
|
|
38
|
+
skillUses: SkillUseRow[];
|
|
39
|
+
}
|
|
40
|
+
export declare class SkillStoreError extends Error {
|
|
41
|
+
readonly code: "read_only";
|
|
42
|
+
constructor(message: string, code: "read_only");
|
|
43
|
+
}
|
|
44
|
+
type GetConnection = (dbPath: string) => Promise<TursoDb>;
|
|
45
|
+
/** `skills.db` always sits in the global storage path, never in a project folder. */
|
|
46
|
+
export declare function skillsDbPath(storagePath: string): string;
|
|
47
|
+
/**
|
|
48
|
+
* Recorded turns, proposals and saved versions in `skills.db`. One instance
|
|
49
|
+
* per file. Writes check the schema version, so a file from a newer OMMS
|
|
50
|
+
* stays untouched. Reads on a missing file return empty values and do not
|
|
51
|
+
* create the file.
|
|
52
|
+
*/
|
|
53
|
+
export declare class SkillStore {
|
|
54
|
+
private readonly dbPath;
|
|
55
|
+
private readonly connect;
|
|
56
|
+
private readOnly;
|
|
57
|
+
constructor(dbPath: string, connect?: GetConnection);
|
|
58
|
+
/** The open connection. `proposal-store.ts` and `ranking.ts` share it. */
|
|
59
|
+
db(): Promise<TursoDb>;
|
|
60
|
+
/** Creates or upgrades the tables. Call it before any write. */
|
|
61
|
+
init(): Promise<void>;
|
|
62
|
+
isReadOnly(): boolean;
|
|
63
|
+
private writable;
|
|
64
|
+
/** Opens the file only when it and the `turns` table exist, so a read never creates it. */
|
|
65
|
+
existingDb(): Promise<TursoDb | null>;
|
|
66
|
+
/**
|
|
67
|
+
* Writes the turn, its tool calls and its skill uses in one transaction.
|
|
68
|
+
* Returns false when the `turn_key` is already stored; nothing is written then.
|
|
69
|
+
*/
|
|
70
|
+
insertTurn(turn: SkillTurnRecord): Promise<boolean>;
|
|
71
|
+
/** Read-only lookup; a missing file gives false and stays missing. */
|
|
72
|
+
hasTurn(turnKey: string): Promise<boolean>;
|
|
73
|
+
countTurns(): Promise<number>;
|
|
74
|
+
/** Deletes every turn with its tool calls and skill uses. Proposals and versions stay. */
|
|
75
|
+
deleteAllTurns(): Promise<number>;
|
|
76
|
+
/** Size of `skills.db` in bytes, or 0 when the file does not exist. */
|
|
77
|
+
fileSize(): number;
|
|
78
|
+
}
|
|
79
|
+
export {};
|