om-memory-system 4.30.1 → 4.31.2
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 +91 -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/providers/base-provider.d.ts +15 -0
- package/dist/services/ai/providers/base-provider.js +26 -0
- package/dist/services/ai/providers/openai-chat-completion.js +3 -6
- 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/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 +264 -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 +9 -21
- package/dist/v2/adapter.js +13 -8
- package/dist/web/assets/index-B_xLhsQO.css +2 -0
- package/dist/web/assets/index-BetjjXGs.js +129 -0
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
- package/dist/web/assets/index-BGDKFzoC.css +0 -2
- package/dist/web/assets/index-CE8tXPA_.js +0 -128
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/** Unchanged lines shown around each change. */
|
|
2
|
+
const CONTEXT_LINES = 3;
|
|
3
|
+
/** Lines of a text. A final newline does not add an empty line. */
|
|
4
|
+
function splitLines(text) {
|
|
5
|
+
if (text === "")
|
|
6
|
+
return [];
|
|
7
|
+
const lines = text.split("\n");
|
|
8
|
+
if (lines[lines.length - 1] === "")
|
|
9
|
+
lines.pop();
|
|
10
|
+
return lines;
|
|
11
|
+
}
|
|
12
|
+
/** Keep, remove and add steps from `before` to `after`, by longest common subsequence. */
|
|
13
|
+
function diffOps(before, after) {
|
|
14
|
+
// The shared start and end need no table; skill edits are usually local.
|
|
15
|
+
let start = 0;
|
|
16
|
+
while (start < before.length && start < after.length && before[start] === after[start])
|
|
17
|
+
start++;
|
|
18
|
+
let endA = before.length;
|
|
19
|
+
let endB = after.length;
|
|
20
|
+
while (endA > start && endB > start && before[endA - 1] === after[endB - 1]) {
|
|
21
|
+
endA--;
|
|
22
|
+
endB--;
|
|
23
|
+
}
|
|
24
|
+
const a = before.slice(start, endA);
|
|
25
|
+
const b = after.slice(start, endB);
|
|
26
|
+
const table = Array.from({ length: a.length + 1 }, () => new Uint32Array(b.length + 1));
|
|
27
|
+
for (let i = a.length - 1; i >= 0; i--) {
|
|
28
|
+
for (let j = b.length - 1; j >= 0; j--) {
|
|
29
|
+
table[i][j] =
|
|
30
|
+
a[i] === b[j] ? table[i + 1][j + 1] + 1 : Math.max(table[i + 1][j], table[i][j + 1]);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
const ops = before.slice(0, start).map((line) => ({ kind: " ", line }));
|
|
34
|
+
let i = 0;
|
|
35
|
+
let j = 0;
|
|
36
|
+
while (i < a.length && j < b.length) {
|
|
37
|
+
if (a[i] === b[j]) {
|
|
38
|
+
ops.push({ kind: " ", line: a[i++] });
|
|
39
|
+
j++;
|
|
40
|
+
}
|
|
41
|
+
else if (table[i + 1][j] >= table[i][j + 1]) {
|
|
42
|
+
ops.push({ kind: "-", line: a[i++] });
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
ops.push({ kind: "+", line: b[j++] });
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
while (i < a.length)
|
|
49
|
+
ops.push({ kind: "-", line: a[i++] });
|
|
50
|
+
while (j < b.length)
|
|
51
|
+
ops.push({ kind: "+", line: b[j++] });
|
|
52
|
+
for (const line of before.slice(endA))
|
|
53
|
+
ops.push({ kind: " ", line });
|
|
54
|
+
return ops;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* A unified-style line diff: `@@ -start,count +start,count @@` hunks with
|
|
58
|
+
* three lines of context, then lines marked ` `, `-` or `+`. Empty when the
|
|
59
|
+
* texts have the same lines. The web page renders it as text.
|
|
60
|
+
*/
|
|
61
|
+
export function lineDiff(before, after) {
|
|
62
|
+
const ops = diffOps(splitLines(before), splitLines(after));
|
|
63
|
+
const oldAt = [];
|
|
64
|
+
const newAt = [];
|
|
65
|
+
let oldLine = 1;
|
|
66
|
+
let newLine = 1;
|
|
67
|
+
for (const op of ops) {
|
|
68
|
+
oldAt.push(oldLine);
|
|
69
|
+
newAt.push(newLine);
|
|
70
|
+
if (op.kind !== "+")
|
|
71
|
+
oldLine++;
|
|
72
|
+
if (op.kind !== "-")
|
|
73
|
+
newLine++;
|
|
74
|
+
}
|
|
75
|
+
const changed = ops.flatMap((op, index) => (op.kind === " " ? [] : [index]));
|
|
76
|
+
const out = [];
|
|
77
|
+
for (let h = 0; h < changed.length; h++) {
|
|
78
|
+
const first = changed[h];
|
|
79
|
+
while (h + 1 < changed.length && changed[h + 1] - changed[h] <= 2 * CONTEXT_LINES)
|
|
80
|
+
h++;
|
|
81
|
+
const from = Math.max(0, first - CONTEXT_LINES);
|
|
82
|
+
const to = Math.min(ops.length - 1, changed[h] + CONTEXT_LINES);
|
|
83
|
+
const hunk = ops.slice(from, to + 1);
|
|
84
|
+
const oldCount = hunk.filter((op) => op.kind !== "+").length;
|
|
85
|
+
const newCount = hunk.filter((op) => op.kind !== "-").length;
|
|
86
|
+
// A side with no lines names the line before the hunk, as unified diffs do.
|
|
87
|
+
const oldStart = oldCount > 0 ? oldAt[from] : oldAt[from] - 1;
|
|
88
|
+
const newStart = newCount > 0 ? newAt[from] : newAt[from] - 1;
|
|
89
|
+
out.push(`@@ -${oldStart},${oldCount} +${newStart},${newCount} @@`);
|
|
90
|
+
for (const op of hunk)
|
|
91
|
+
out.push(op.kind + op.line);
|
|
92
|
+
}
|
|
93
|
+
return out.join("\n");
|
|
94
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { SkillOperationsDeps } from "../../core/skill-operations.js";
|
|
2
|
+
/**
|
|
3
|
+
* The wiring point between `CONFIG` and the `skills` operations. The terminal
|
|
4
|
+
* command passes `person` and the host tools pass `agent`. The store is not
|
|
5
|
+
* opened here; reads never create `skills.db`.
|
|
6
|
+
*/
|
|
7
|
+
export declare function skillOperationsDeps(directory: string, appliedBy: SkillOperationsDeps["appliedBy"]): SkillOperationsDeps;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { CONFIG } from "../../config.js";
|
|
2
|
+
import { skillTurnSettingsFor } from "./settings-for.js";
|
|
3
|
+
import { skillModel } from "./skill-model.js";
|
|
4
|
+
import { SkillStore, skillsDbPath } from "./store.js";
|
|
5
|
+
/**
|
|
6
|
+
* The wiring point between `CONFIG` and the `skills` operations. The terminal
|
|
7
|
+
* command passes `person` and the host tools pass `agent`. The store is not
|
|
8
|
+
* opened here; reads never create `skills.db`.
|
|
9
|
+
*/
|
|
10
|
+
export function skillOperationsDeps(directory, appliedBy) {
|
|
11
|
+
const settings = skillTurnSettingsFor(directory);
|
|
12
|
+
return {
|
|
13
|
+
enabled: settings.enabled,
|
|
14
|
+
store: new SkillStore(skillsDbPath(settings.storagePath)),
|
|
15
|
+
folders: settings.folders,
|
|
16
|
+
model: skillModel(CONFIG),
|
|
17
|
+
appliedBy,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { type SkillStore } from "./store.js";
|
|
2
|
+
/**
|
|
3
|
+
* Proposals, saved versions and per-skill settings in `skills.db`. Built on the
|
|
4
|
+
* `SkillStore` connection. Call `store.init()` before a write. Reads on a
|
|
5
|
+
* missing file return empty values and do not create it.
|
|
6
|
+
*/
|
|
7
|
+
export type SkillProposalKind = "improve" | "new";
|
|
8
|
+
export type SkillProposalState = "pending" | "applied" | "stale" | "rejected" | "undone";
|
|
9
|
+
export type SkillAppliedBy = "person" | "agent" | "automatic";
|
|
10
|
+
/** `model`: the external API drafted it. `agent`: the coding agent wrote it through `propose`. */
|
|
11
|
+
export type SkillDraftedBy = "model" | "agent";
|
|
12
|
+
export type SkillMode = "manual" | "automatic";
|
|
13
|
+
export interface SkillProposal {
|
|
14
|
+
id: string;
|
|
15
|
+
kind: SkillProposalKind;
|
|
16
|
+
skillName: string;
|
|
17
|
+
/** Full path of the `SKILL.md` the proposal writes. */
|
|
18
|
+
skillPath: string | null;
|
|
19
|
+
/** sha256 of the file the draft started from; null for a new skill. */
|
|
20
|
+
baseHash: string | null;
|
|
21
|
+
content: string;
|
|
22
|
+
diff: string | null;
|
|
23
|
+
summary: string | null;
|
|
24
|
+
evidenceTurnIds: number[];
|
|
25
|
+
state: SkillProposalState;
|
|
26
|
+
appliedBy: SkillAppliedBy | null;
|
|
27
|
+
draftedBy: SkillDraftedBy;
|
|
28
|
+
createdAt: number;
|
|
29
|
+
updatedAt: number;
|
|
30
|
+
}
|
|
31
|
+
export interface NewSkillProposal {
|
|
32
|
+
kind: SkillProposalKind;
|
|
33
|
+
skillName: string;
|
|
34
|
+
skillPath: string | null;
|
|
35
|
+
baseHash: string | null;
|
|
36
|
+
content: string;
|
|
37
|
+
diff: string | null;
|
|
38
|
+
summary: string | null;
|
|
39
|
+
evidenceTurnIds: number[];
|
|
40
|
+
/** Defaults to `model`. */
|
|
41
|
+
draftedBy?: SkillDraftedBy;
|
|
42
|
+
}
|
|
43
|
+
export interface SkillVersion {
|
|
44
|
+
id: number;
|
|
45
|
+
proposalId: string;
|
|
46
|
+
path: string;
|
|
47
|
+
/** The file before the apply, or null when the apply created it. */
|
|
48
|
+
previousContent: string | null;
|
|
49
|
+
writtenHash: string;
|
|
50
|
+
savedAt: number;
|
|
51
|
+
}
|
|
52
|
+
export interface SkillSettings {
|
|
53
|
+
skillPath: string;
|
|
54
|
+
mode: SkillMode;
|
|
55
|
+
lastReviewAt: number | null;
|
|
56
|
+
usesAtLastReview: number;
|
|
57
|
+
}
|
|
58
|
+
/** Saves a pending proposal with a new id. */
|
|
59
|
+
export declare function createProposal(store: SkillStore, input: NewSkillProposal, now: number): Promise<SkillProposal>;
|
|
60
|
+
export declare function getProposal(store: SkillStore, id: string): Promise<SkillProposal | null>;
|
|
61
|
+
export interface ProposalFilter {
|
|
62
|
+
state?: SkillProposalState;
|
|
63
|
+
skillName?: string;
|
|
64
|
+
skillPath?: string;
|
|
65
|
+
}
|
|
66
|
+
/** Proposals, newest first. */
|
|
67
|
+
export declare function listProposals(store: SkillStore, filter?: ProposalFilter): Promise<SkillProposal[]>;
|
|
68
|
+
/**
|
|
69
|
+
* Sets the state and the update time. `appliedBy` is written when given and
|
|
70
|
+
* kept otherwise. Returns false when no proposal has this id.
|
|
71
|
+
*/
|
|
72
|
+
export declare function updateProposalState(store: SkillStore, id: string, state: SkillProposalState, now: number, appliedBy?: SkillAppliedBy): Promise<boolean>;
|
|
73
|
+
/** Saves the content a write replaces. Returns the version id. */
|
|
74
|
+
export declare function saveSkillVersion(store: SkillStore, version: Pick<SkillVersion, "proposalId" | "path" | "previousContent" | "writtenHash">, now: number): Promise<number>;
|
|
75
|
+
/** Removes a version whose write did not happen. */
|
|
76
|
+
export declare function deleteSkillVersion(store: SkillStore, id: number): Promise<void>;
|
|
77
|
+
export declare function getLatestVersion(store: SkillStore, proposalId: string): Promise<SkillVersion | null>;
|
|
78
|
+
/** The settings for one skill. A skill with no row is in manual mode and never reviewed. */
|
|
79
|
+
export declare function getSkillSettings(store: SkillStore, skillPath: string): Promise<SkillSettings>;
|
|
80
|
+
export declare function setSkillMode(store: SkillStore, skillPath: string, mode: SkillMode): Promise<void>;
|
|
81
|
+
/** Records a review: its time and the use count at that time. */
|
|
82
|
+
export declare function setSkillReview(store: SkillStore, skillPath: string, reviewedAt: number, uses: number): Promise<void>;
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { SkillStoreError } from "./store.js";
|
|
3
|
+
async function writableDb(store) {
|
|
4
|
+
if (store.isReadOnly()) {
|
|
5
|
+
throw new SkillStoreError("skills.db is from a newer OMMS and is read-only", "read_only");
|
|
6
|
+
}
|
|
7
|
+
return store.db();
|
|
8
|
+
}
|
|
9
|
+
function text(value) {
|
|
10
|
+
return value === null || value === undefined ? null : String(value);
|
|
11
|
+
}
|
|
12
|
+
function toProposal(row) {
|
|
13
|
+
let evidence = [];
|
|
14
|
+
try {
|
|
15
|
+
const parsed = JSON.parse(String(row.evidence_turn_ids));
|
|
16
|
+
if (Array.isArray(parsed))
|
|
17
|
+
evidence = parsed.map(Number);
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
// A damaged list reads as no evidence.
|
|
21
|
+
}
|
|
22
|
+
return {
|
|
23
|
+
id: String(row.id),
|
|
24
|
+
kind: String(row.kind),
|
|
25
|
+
skillName: String(row.skill_name),
|
|
26
|
+
skillPath: text(row.skill_path),
|
|
27
|
+
baseHash: text(row.base_hash),
|
|
28
|
+
content: String(row.proposed_content),
|
|
29
|
+
diff: text(row.diff),
|
|
30
|
+
summary: text(row.summary),
|
|
31
|
+
evidenceTurnIds: evidence,
|
|
32
|
+
state: String(row.state),
|
|
33
|
+
appliedBy: text(row.applied_by),
|
|
34
|
+
draftedBy: row.drafted_by === "agent" ? "agent" : "model",
|
|
35
|
+
createdAt: Number(row.created_at),
|
|
36
|
+
updatedAt: Number(row.updated_at),
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
/** Saves a pending proposal with a new id. */
|
|
40
|
+
export async function createProposal(store, input, now) {
|
|
41
|
+
const db = await writableDb(store);
|
|
42
|
+
const proposal = {
|
|
43
|
+
id: randomUUID(),
|
|
44
|
+
...input,
|
|
45
|
+
draftedBy: input.draftedBy ?? "model",
|
|
46
|
+
state: "pending",
|
|
47
|
+
appliedBy: null,
|
|
48
|
+
createdAt: now,
|
|
49
|
+
updatedAt: now,
|
|
50
|
+
};
|
|
51
|
+
await db.run(`INSERT INTO skill_proposals
|
|
52
|
+
(id, kind, skill_name, skill_path, base_hash, proposed_content, diff, summary,
|
|
53
|
+
evidence_turn_ids, state, applied_by, drafted_by, created_at, updated_at)
|
|
54
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
55
|
+
proposal.id,
|
|
56
|
+
proposal.kind,
|
|
57
|
+
proposal.skillName,
|
|
58
|
+
proposal.skillPath,
|
|
59
|
+
proposal.baseHash,
|
|
60
|
+
proposal.content,
|
|
61
|
+
proposal.diff,
|
|
62
|
+
proposal.summary,
|
|
63
|
+
JSON.stringify(proposal.evidenceTurnIds),
|
|
64
|
+
proposal.state,
|
|
65
|
+
proposal.appliedBy,
|
|
66
|
+
proposal.draftedBy,
|
|
67
|
+
proposal.createdAt,
|
|
68
|
+
proposal.updatedAt,
|
|
69
|
+
]);
|
|
70
|
+
return proposal;
|
|
71
|
+
}
|
|
72
|
+
export async function getProposal(store, id) {
|
|
73
|
+
const db = await store.existingDb();
|
|
74
|
+
if (!db)
|
|
75
|
+
return null;
|
|
76
|
+
const row = await db.get("SELECT * FROM skill_proposals WHERE id = ?", [id]);
|
|
77
|
+
return row ? toProposal(row) : null;
|
|
78
|
+
}
|
|
79
|
+
/** Proposals, newest first. */
|
|
80
|
+
export async function listProposals(store, filter = {}) {
|
|
81
|
+
const db = await store.existingDb();
|
|
82
|
+
if (!db)
|
|
83
|
+
return [];
|
|
84
|
+
const where = [];
|
|
85
|
+
const args = [];
|
|
86
|
+
if (filter.state) {
|
|
87
|
+
where.push("state = ?");
|
|
88
|
+
args.push(filter.state);
|
|
89
|
+
}
|
|
90
|
+
if (filter.skillName) {
|
|
91
|
+
where.push("skill_name = ?");
|
|
92
|
+
args.push(filter.skillName);
|
|
93
|
+
}
|
|
94
|
+
if (filter.skillPath) {
|
|
95
|
+
where.push("skill_path = ?");
|
|
96
|
+
args.push(filter.skillPath);
|
|
97
|
+
}
|
|
98
|
+
const clause = where.length > 0 ? `WHERE ${where.join(" AND ")}` : "";
|
|
99
|
+
const rows = await db.all(`SELECT * FROM skill_proposals ${clause} ORDER BY created_at DESC, rowid DESC`, args);
|
|
100
|
+
return rows.map(toProposal);
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Sets the state and the update time. `appliedBy` is written when given and
|
|
104
|
+
* kept otherwise. Returns false when no proposal has this id.
|
|
105
|
+
*/
|
|
106
|
+
export async function updateProposalState(store, id, state, now, appliedBy) {
|
|
107
|
+
const db = await writableDb(store);
|
|
108
|
+
const changed = await db.run("UPDATE skill_proposals SET state = ?, applied_by = COALESCE(?, applied_by), updated_at = ? WHERE id = ?", [state, appliedBy ?? null, now, id]);
|
|
109
|
+
return changed > 0;
|
|
110
|
+
}
|
|
111
|
+
/** Saves the content a write replaces. Returns the version id. */
|
|
112
|
+
export async function saveSkillVersion(store, version, now) {
|
|
113
|
+
const db = await writableDb(store);
|
|
114
|
+
const result = await db.execute("INSERT INTO skill_versions (proposal_id, path, previous_content, written_hash, saved_at) VALUES (?, ?, ?, ?, ?)", [version.proposalId, version.path, version.previousContent, version.writtenHash, now]);
|
|
115
|
+
return Number(result.lastInsertRowid);
|
|
116
|
+
}
|
|
117
|
+
/** Removes a version whose write did not happen. */
|
|
118
|
+
export async function deleteSkillVersion(store, id) {
|
|
119
|
+
const db = await writableDb(store);
|
|
120
|
+
await db.run("DELETE FROM skill_versions WHERE id = ?", [id]);
|
|
121
|
+
}
|
|
122
|
+
export async function getLatestVersion(store, proposalId) {
|
|
123
|
+
const db = await store.existingDb();
|
|
124
|
+
if (!db)
|
|
125
|
+
return null;
|
|
126
|
+
const row = await db.get("SELECT * FROM skill_versions WHERE proposal_id = ? ORDER BY id DESC LIMIT 1", [proposalId]);
|
|
127
|
+
if (!row)
|
|
128
|
+
return null;
|
|
129
|
+
return {
|
|
130
|
+
id: Number(row.id),
|
|
131
|
+
proposalId: String(row.proposal_id),
|
|
132
|
+
path: String(row.path),
|
|
133
|
+
previousContent: text(row.previous_content),
|
|
134
|
+
writtenHash: String(row.written_hash),
|
|
135
|
+
savedAt: Number(row.saved_at),
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
/** The settings for one skill. A skill with no row is in manual mode and never reviewed. */
|
|
139
|
+
export async function getSkillSettings(store, skillPath) {
|
|
140
|
+
const db = await store.existingDb();
|
|
141
|
+
const row = db
|
|
142
|
+
? await db.get("SELECT * FROM skill_settings WHERE skill_path = ?", [skillPath])
|
|
143
|
+
: null;
|
|
144
|
+
return {
|
|
145
|
+
skillPath,
|
|
146
|
+
mode: row?.mode === "automatic" ? "automatic" : "manual",
|
|
147
|
+
lastReviewAt: row?.last_review_at == null ? null : Number(row.last_review_at),
|
|
148
|
+
usesAtLastReview: Number(row?.uses_at_last_review ?? 0),
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
export async function setSkillMode(store, skillPath, mode) {
|
|
152
|
+
const db = await writableDb(store);
|
|
153
|
+
await db.run(`INSERT INTO skill_settings (skill_path, mode) VALUES (?, ?)
|
|
154
|
+
ON CONFLICT(skill_path) DO UPDATE SET mode = excluded.mode`, [skillPath, mode]);
|
|
155
|
+
}
|
|
156
|
+
/** Records a review: its time and the use count at that time. */
|
|
157
|
+
export async function setSkillReview(store, skillPath, reviewedAt, uses) {
|
|
158
|
+
const db = await writableDb(store);
|
|
159
|
+
await db.run(`INSERT INTO skill_settings (skill_path, mode, last_review_at, uses_at_last_review)
|
|
160
|
+
VALUES (?, 'manual', ?, ?)
|
|
161
|
+
ON CONFLICT(skill_path) DO UPDATE SET
|
|
162
|
+
last_review_at = excluded.last_review_at,
|
|
163
|
+
uses_at_last_review = excluded.uses_at_last_review`, [skillPath, reviewedAt, uses]);
|
|
164
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { SkillMode } from "./proposal-store.js";
|
|
2
|
+
import type { SkillIndex } from "./skill-index.js";
|
|
3
|
+
import type { SkillStore } from "./store.js";
|
|
4
|
+
export declare const RANKING_WINDOW_MS: number;
|
|
5
|
+
/** A skill needs this many uses in the window before it can be Suggested. */
|
|
6
|
+
export declare const SUGGEST_MIN_USES = 5;
|
|
7
|
+
/** A skill also needs this attention score before it can be Suggested. */
|
|
8
|
+
export declare const SUGGEST_MIN_ATTENTION = 0.3;
|
|
9
|
+
export declare const SKILL_ORDERS: readonly ["uses", "attention"];
|
|
10
|
+
export type SkillOrder = (typeof SKILL_ORDERS)[number];
|
|
11
|
+
export interface SkillRanking {
|
|
12
|
+
name: string;
|
|
13
|
+
/** Full path of its `SKILL.md`, or null when no watched folder holds it. */
|
|
14
|
+
path: string | null;
|
|
15
|
+
/** Shown as **Not in a watched folder**; such a skill gets no draft. */
|
|
16
|
+
notInWatchedFolder: boolean;
|
|
17
|
+
/** Turns in the last 30 days that used the skill. */
|
|
18
|
+
uses: number;
|
|
19
|
+
/** Of those turns, the ones with a failed tool call after the skill was used. */
|
|
20
|
+
failedTurns: number;
|
|
21
|
+
/** (failed turns + 1) / (uses + 2), rounded to 2 decimals. 0 to 1; higher needs more care. */
|
|
22
|
+
attention: number;
|
|
23
|
+
lastUsedAt: number | null;
|
|
24
|
+
mode: SkillMode;
|
|
25
|
+
/** 5 uses, attention of 0.3, and a failed turn since the skill's last proposal or review. */
|
|
26
|
+
suggested: boolean;
|
|
27
|
+
}
|
|
28
|
+
/** (failed + 1) / (uses + 2): a skill with little evidence stays near the middle. */
|
|
29
|
+
export declare function attentionScore(failedTurns: number, uses: number): number;
|
|
30
|
+
/**
|
|
31
|
+
* Every watched skill and every recorded skill, with its 30-day counts.
|
|
32
|
+
* Sorted by use count, highest first, then name; order `attention` sorts by
|
|
33
|
+
* attention first, then uses, then name. Watched skills with no use are
|
|
34
|
+
* listed with zero counts. Counting happens in SQL.
|
|
35
|
+
*/
|
|
36
|
+
export declare function rankSkills(store: SkillStore, index: SkillIndex, now: number, order?: SkillOrder): Promise<SkillRanking[]>;
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
export const RANKING_WINDOW_MS = 30 * 86_400_000;
|
|
2
|
+
/** A skill needs this many uses in the window before it can be Suggested. */
|
|
3
|
+
export const SUGGEST_MIN_USES = 5;
|
|
4
|
+
/** A skill also needs this attention score before it can be Suggested. */
|
|
5
|
+
export const SUGGEST_MIN_ATTENTION = 0.3;
|
|
6
|
+
export const SKILL_ORDERS = ["uses", "attention"];
|
|
7
|
+
// A recorded path counts only while the index still lists it. A use with no
|
|
8
|
+
// path takes the index path when exactly one watched skill has that name.
|
|
9
|
+
// Each turn counts once per skill, even when it used the skill twice.
|
|
10
|
+
// A failed tool call counts only when it came after the skill was used: its
|
|
11
|
+
// order is above the earliest use position in that turn. A NULL position (a
|
|
12
|
+
// row from before the column) and -1 (a slash command) count every failure.
|
|
13
|
+
// A review (a draft with no change, or an automatic review) counts as a proposal.
|
|
14
|
+
const RANKING_SQL = `
|
|
15
|
+
WITH idx AS (
|
|
16
|
+
SELECT json_extract(value, '$.name') AS name, json_extract(value, '$.path') AS path
|
|
17
|
+
FROM json_each(?)
|
|
18
|
+
),
|
|
19
|
+
resolved AS (
|
|
20
|
+
SELECT
|
|
21
|
+
su.turn_id,
|
|
22
|
+
su.skill_name AS name,
|
|
23
|
+
CASE
|
|
24
|
+
WHEN su.skill_path IS NOT NULL AND EXISTS (SELECT 1 FROM idx WHERE idx.path = su.skill_path)
|
|
25
|
+
THEN su.skill_path
|
|
26
|
+
WHEN su.skill_path IS NULL AND (SELECT COUNT(*) FROM idx WHERE idx.name = su.skill_name) = 1
|
|
27
|
+
THEN (SELECT idx.path FROM idx WHERE idx.name = su.skill_name)
|
|
28
|
+
ELSE NULL
|
|
29
|
+
END AS path,
|
|
30
|
+
t.created_at,
|
|
31
|
+
COALESCE(su.position, -1) AS position
|
|
32
|
+
FROM skill_uses su
|
|
33
|
+
JOIN turns t ON t.id = su.turn_id
|
|
34
|
+
WHERE t.created_at >= ?
|
|
35
|
+
),
|
|
36
|
+
first_use AS (
|
|
37
|
+
SELECT turn_id, name, path, created_at, MIN(position) AS position
|
|
38
|
+
FROM resolved
|
|
39
|
+
GROUP BY turn_id, name, path
|
|
40
|
+
),
|
|
41
|
+
uses AS (
|
|
42
|
+
SELECT
|
|
43
|
+
f.turn_id,
|
|
44
|
+
f.name,
|
|
45
|
+
f.path,
|
|
46
|
+
f.created_at,
|
|
47
|
+
EXISTS (
|
|
48
|
+
SELECT 1 FROM tool_calls tc
|
|
49
|
+
WHERE tc.turn_id = f.turn_id AND tc.outcome = 'error' AND tc.position > f.position
|
|
50
|
+
) AS failed
|
|
51
|
+
FROM first_use f
|
|
52
|
+
),
|
|
53
|
+
last_proposal AS (
|
|
54
|
+
SELECT path, MAX(at) AS at FROM (
|
|
55
|
+
SELECT skill_path AS path, created_at AS at FROM skill_proposals WHERE skill_path IS NOT NULL
|
|
56
|
+
UNION ALL
|
|
57
|
+
SELECT skill_path, last_review_at FROM skill_settings WHERE last_review_at IS NOT NULL
|
|
58
|
+
)
|
|
59
|
+
GROUP BY path
|
|
60
|
+
)
|
|
61
|
+
SELECT
|
|
62
|
+
u.name,
|
|
63
|
+
u.path,
|
|
64
|
+
COUNT(DISTINCT u.turn_id) AS uses,
|
|
65
|
+
COUNT(DISTINCT CASE WHEN u.failed THEN u.turn_id END) AS failed_turns,
|
|
66
|
+
COUNT(DISTINCT CASE WHEN u.failed AND (lp.at IS NULL OR u.created_at > lp.at) THEN u.turn_id END)
|
|
67
|
+
AS failed_since_proposal,
|
|
68
|
+
MAX(u.created_at) AS last_used_at,
|
|
69
|
+
s.mode
|
|
70
|
+
FROM uses u
|
|
71
|
+
LEFT JOIN last_proposal lp ON lp.path = u.path
|
|
72
|
+
LEFT JOIN skill_settings s ON s.skill_path = u.path
|
|
73
|
+
GROUP BY u.name, u.path
|
|
74
|
+
`;
|
|
75
|
+
function modeOf(value) {
|
|
76
|
+
return value === "automatic" ? "automatic" : "manual";
|
|
77
|
+
}
|
|
78
|
+
/** (failed + 1) / (uses + 2): a skill with little evidence stays near the middle. */
|
|
79
|
+
export function attentionScore(failedTurns, uses) {
|
|
80
|
+
return Math.round(((failedTurns + 1) / (uses + 2)) * 100) / 100;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Every watched skill and every recorded skill, with its 30-day counts.
|
|
84
|
+
* Sorted by use count, highest first, then name; order `attention` sorts by
|
|
85
|
+
* attention first, then uses, then name. Watched skills with no use are
|
|
86
|
+
* listed with zero counts. Counting happens in SQL.
|
|
87
|
+
*/
|
|
88
|
+
export async function rankSkills(store, index, now, order = "uses") {
|
|
89
|
+
const db = await store.existingDb();
|
|
90
|
+
const entries = index.entries.map((e) => ({ name: e.name, path: e.path }));
|
|
91
|
+
const rankings = [];
|
|
92
|
+
const seen = new Set();
|
|
93
|
+
if (db) {
|
|
94
|
+
const rows = await db.all(RANKING_SQL, [JSON.stringify(entries), now - RANKING_WINDOW_MS]);
|
|
95
|
+
for (const row of rows) {
|
|
96
|
+
const path = row.path == null ? null : String(row.path);
|
|
97
|
+
const uses = Number(row.uses);
|
|
98
|
+
const failedTurns = Number(row.failed_turns);
|
|
99
|
+
const attention = attentionScore(failedTurns, uses);
|
|
100
|
+
if (path)
|
|
101
|
+
seen.add(path);
|
|
102
|
+
rankings.push({
|
|
103
|
+
name: String(row.name),
|
|
104
|
+
path,
|
|
105
|
+
notInWatchedFolder: path === null,
|
|
106
|
+
uses,
|
|
107
|
+
failedTurns,
|
|
108
|
+
attention,
|
|
109
|
+
lastUsedAt: row.last_used_at == null ? null : Number(row.last_used_at),
|
|
110
|
+
mode: modeOf(row.mode),
|
|
111
|
+
suggested: path !== null &&
|
|
112
|
+
uses >= SUGGEST_MIN_USES &&
|
|
113
|
+
attention >= SUGGEST_MIN_ATTENTION &&
|
|
114
|
+
Number(row.failed_since_proposal) > 0,
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
const unused = index.entries.filter((e) => !seen.has(e.path));
|
|
119
|
+
const modes = new Map();
|
|
120
|
+
if (db && unused.length > 0) {
|
|
121
|
+
const rows = await db.all("SELECT s.skill_path, s.mode FROM skill_settings s WHERE s.skill_path IN (SELECT value FROM json_each(?))", [JSON.stringify(unused.map((e) => e.path))]);
|
|
122
|
+
for (const row of rows)
|
|
123
|
+
modes.set(String(row.skill_path), modeOf(row.mode));
|
|
124
|
+
}
|
|
125
|
+
for (const entry of unused) {
|
|
126
|
+
rankings.push({
|
|
127
|
+
name: entry.name,
|
|
128
|
+
path: entry.path,
|
|
129
|
+
notInWatchedFolder: false,
|
|
130
|
+
uses: 0,
|
|
131
|
+
failedTurns: 0,
|
|
132
|
+
attention: attentionScore(0, 0),
|
|
133
|
+
lastUsedAt: null,
|
|
134
|
+
mode: modes.get(entry.path) ?? "manual",
|
|
135
|
+
suggested: false,
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
const byUses = (a, b) => b.uses - a.uses || a.name.localeCompare(b.name);
|
|
139
|
+
return rankings.sort(order === "attention" ? (a, b) => b.attention - a.attention || byUses(a, b) : byUses);
|
|
140
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { CaptureWorkUnit } from "../../core/capture.js";
|
|
2
|
+
import { type SkillTurnSettings, type SkillTurnStatus } from "./turn-output.js";
|
|
3
|
+
/**
|
|
4
|
+
* The wiring point between `CONFIG` and the pure skill-turn output. The live
|
|
5
|
+
* capture sites call it; `turn-output.ts` takes the settings as an argument.
|
|
6
|
+
*/
|
|
7
|
+
export type SkillTurnRecorder = (unit: CaptureWorkUnit) => Promise<SkillTurnStatus>;
|
|
8
|
+
/** The skill-turn settings for a project folder, with its project override applied. */
|
|
9
|
+
export declare function skillTurnSettingsFor(directory: string): SkillTurnSettings;
|
|
10
|
+
/** A recorder for turns in this folder, or undefined when skill learning is off there. */
|
|
11
|
+
export declare function skillTurnRecorder(directory: string): SkillTurnRecorder | undefined;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { CONFIG, refreshConfigIfChanged } from "../../config.js";
|
|
2
|
+
import { skillStoragePath, watchedSkillFolders } from "./skill-folders.js";
|
|
3
|
+
import { recordSkillTurn } from "./turn-output.js";
|
|
4
|
+
/** The skill-turn settings for a project folder, with its project override applied. */
|
|
5
|
+
export function skillTurnSettingsFor(directory) {
|
|
6
|
+
refreshConfigIfChanged(directory);
|
|
7
|
+
return {
|
|
8
|
+
enabled: CONFIG.skillLearning === true,
|
|
9
|
+
redact: CONFIG.skillLearningRedact !== false,
|
|
10
|
+
folders: watchedSkillFolders(CONFIG),
|
|
11
|
+
storagePath: skillStoragePath(CONFIG),
|
|
12
|
+
secrets: CONFIG,
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
/** A recorder for turns in this folder, or undefined when skill learning is off there. */
|
|
16
|
+
export function skillTurnRecorder(directory) {
|
|
17
|
+
const settings = skillTurnSettingsFor(directory);
|
|
18
|
+
if (!settings.enabled)
|
|
19
|
+
return undefined;
|
|
20
|
+
return (unit) => recordSkillTurn(unit, settings);
|
|
21
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Skill learning settings (ADR-036), kept out of `src/config.ts` to save its
|
|
3
|
+
* lines. Pure on purpose: callers pass the file config, and the loaded
|
|
4
|
+
* `CONFIG` is never imported here, because many tests stub `src/config.js`.
|
|
5
|
+
*/
|
|
6
|
+
/** The raw keys as they appear in a config file, before any check. */
|
|
7
|
+
export interface SkillFileConfig {
|
|
8
|
+
skillLearning?: unknown;
|
|
9
|
+
skillLearningRedact?: unknown;
|
|
10
|
+
skillFolders?: unknown;
|
|
11
|
+
}
|
|
12
|
+
export interface SkillConfig {
|
|
13
|
+
/** Record full turns into `skills.db`. Off by default. */
|
|
14
|
+
skillLearning: boolean;
|
|
15
|
+
/** Remove `<private>` text and secrets before a turn is stored. On by default. Global only. */
|
|
16
|
+
skillLearningRedact: boolean;
|
|
17
|
+
/** Folders OMMS reads skills from. Unset means `defaultSkillFolders`. Global only. */
|
|
18
|
+
skillFolders: string[] | undefined;
|
|
19
|
+
}
|
|
20
|
+
/** The three settings from a merged file config, with defaults applied. */
|
|
21
|
+
export declare function normaliseSkillConfig(fileConfig: SkillFileConfig, homeDir?: string): SkillConfig;
|
|
22
|
+
/**
|
|
23
|
+
* The first `skillFolders` entry, as written, that is not an existing folder,
|
|
24
|
+
* or null when every entry is one. The Settings save refuses a missing folder
|
|
25
|
+
* and names it. `isFolder` is passed in so the caller owns the file system access.
|
|
26
|
+
*/
|
|
27
|
+
export declare function missingSkillFolder(folders: readonly string[], isFolder: (path: string) => boolean, homeDir?: string): string | null;
|
|
28
|
+
/**
|
|
29
|
+
* What a project config may change. A checked-in project file must not start
|
|
30
|
+
* recording conversations, so it can only turn `skillLearning` off. Its
|
|
31
|
+
* `skillLearningRedact` and `skillFolders` are always ignored.
|
|
32
|
+
* `ignoredTurnOn` tells the caller to log a project value of `true`.
|
|
33
|
+
*/
|
|
34
|
+
export declare function projectSkillOverride(project: SkillFileConfig): {
|
|
35
|
+
override: {
|
|
36
|
+
skillLearning?: false;
|
|
37
|
+
};
|
|
38
|
+
ignoredTurnOn: boolean;
|
|
39
|
+
};
|
|
40
|
+
/**
|
|
41
|
+
* The folders to watch when `skillFolders` is unset: `~/.agents/skills` and
|
|
42
|
+
* `<Claude Code folder>/skills`, each only when it exists. `exists` is passed
|
|
43
|
+
* in so the caller owns the file system access.
|
|
44
|
+
*/
|
|
45
|
+
export declare function defaultSkillFolders(homeDir: string, claudeFolder: string, exists: (path: string) => boolean): string[];
|