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,35 @@
|
|
|
1
|
+
import { homedir } from "node:os";
|
|
2
|
+
import { join, resolve } from "node:path";
|
|
3
|
+
/** Helpers that `skill-operations.ts` and `skill-agent-modes.ts` share. */
|
|
4
|
+
export function reply(mode, success, code, text, data) {
|
|
5
|
+
return data ? { success, mode, code, text, data } : { success, mode, code, text };
|
|
6
|
+
}
|
|
7
|
+
export function expandHome(path) {
|
|
8
|
+
return path.startsWith("~/") ? join(homedir(), path.slice(2)) : path;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* The `SKILL.md` path for a skill name or path, or the reply that explains
|
|
12
|
+
* why there is none: no skill of that name, or more than one.
|
|
13
|
+
*/
|
|
14
|
+
export function resolveSkillPath(mode, skill, index) {
|
|
15
|
+
if (/[/\\]/.test(skill))
|
|
16
|
+
return { path: resolve(expandHome(skill)) };
|
|
17
|
+
const matches = index.entries.filter((e) => e.name === skill);
|
|
18
|
+
if (matches.length === 0) {
|
|
19
|
+
return reply(mode, false, "not-found", `No watched skill is named ${skill}. Run list to see the watched skills.`);
|
|
20
|
+
}
|
|
21
|
+
if (matches.length > 1) {
|
|
22
|
+
const paths = matches.map((e) => `- ${e.path}`).join("\n");
|
|
23
|
+
return reply(mode, false, "ambiguous", `More than one skill is named ${skill}. Give skill as one of these paths:\n${paths}`, { paths: matches.map((e) => e.path) });
|
|
24
|
+
}
|
|
25
|
+
return { path: matches[0].path };
|
|
26
|
+
}
|
|
27
|
+
/** `folder` when it is one of the watched folders, or the reply that lists them. */
|
|
28
|
+
export function watchedTarget(mode, folders, folder) {
|
|
29
|
+
const watched = folders.map((f) => resolve(expandHome(f)));
|
|
30
|
+
const target = folder ? resolve(expandHome(folder)) : null;
|
|
31
|
+
if (target && watched.includes(target))
|
|
32
|
+
return { folder: target };
|
|
33
|
+
const known = watched.map((f) => `- ${f}`).join("\n") || "(none)";
|
|
34
|
+
return reply(mode, false, "not-watched", `Give folder as one of the watched folders:\n${known}`);
|
|
35
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { type SkillIdea, type SkillModel } from "../services/skill-learning/drafting.js";
|
|
2
|
+
import type { SkillStore } from "../services/skill-learning/store.js";
|
|
3
|
+
/**
|
|
4
|
+
* The `skills` tool and the `om-memory-system skills` command share these
|
|
5
|
+
* mode handlers and replies (design §8). The caller builds the deps from the
|
|
6
|
+
* config; `services/skill-learning/operations-deps.ts` does that.
|
|
7
|
+
*/
|
|
8
|
+
export declare const SKILL_OPERATION_MODES: readonly ["list", "ideas", "draft", "evidence", "propose", "proposals", "show", "apply", "reject", "undo", "help"];
|
|
9
|
+
export type SkillOperationMode = (typeof SKILL_OPERATION_MODES)[number];
|
|
10
|
+
export interface SkillOperationArgs {
|
|
11
|
+
/** Draft: a skill name, or the full path of its `SKILL.md`. */
|
|
12
|
+
skill?: string;
|
|
13
|
+
/** Draft a new skill: an idea from `ideas`. Needs `folder`. */
|
|
14
|
+
idea?: Partial<SkillIdea>;
|
|
15
|
+
/** Draft a new skill: the watched folder to put it in. */
|
|
16
|
+
folder?: string;
|
|
17
|
+
/** Show, apply, reject: the proposal id. */
|
|
18
|
+
id?: string;
|
|
19
|
+
/** Proposals: only proposals in this state. */
|
|
20
|
+
state?: string;
|
|
21
|
+
/** List: `uses` (default) or `attention`. */
|
|
22
|
+
order?: string;
|
|
23
|
+
/** Evidence: how many recorded turns, 1 to 20 (default 10). */
|
|
24
|
+
limit?: number | string;
|
|
25
|
+
/** Propose: the full new `SKILL.md`. */
|
|
26
|
+
content?: string;
|
|
27
|
+
/** Propose: what changed and why. */
|
|
28
|
+
summary?: string;
|
|
29
|
+
/** Propose: the turn ids that show the need, as numbers, `T12` labels or `12,14`. */
|
|
30
|
+
evidence?: Array<number | string> | string;
|
|
31
|
+
}
|
|
32
|
+
export interface SkillOperationsDeps {
|
|
33
|
+
/** `skillLearning` in the config. */
|
|
34
|
+
enabled: boolean;
|
|
35
|
+
store: SkillStore;
|
|
36
|
+
/** The watched skill folders. */
|
|
37
|
+
folders: readonly string[];
|
|
38
|
+
model: SkillModel;
|
|
39
|
+
/** Recorded on apply: `agent` for the host tools, `person` for the terminal. */
|
|
40
|
+
appliedBy: "person" | "agent";
|
|
41
|
+
now?: () => number;
|
|
42
|
+
}
|
|
43
|
+
export interface SkillOperationResult {
|
|
44
|
+
success: boolean;
|
|
45
|
+
mode: string;
|
|
46
|
+
/** A fixed outcome code, such as `ok`, `recording-off` or `not-found`. */
|
|
47
|
+
code: string;
|
|
48
|
+
/** The plain-English reply for an agent or a terminal. */
|
|
49
|
+
text: string;
|
|
50
|
+
/** Structured data for JSON output. */
|
|
51
|
+
data?: Record<string, unknown>;
|
|
52
|
+
}
|
|
53
|
+
export declare const RECORDING_OFF_TEXT = "Skill learning is off, so OMMS records no turns for skills. Turn it on in Settings \u2192 Skill learning, or set `skillLearning: true` in the global config.";
|
|
54
|
+
/**
|
|
55
|
+
* Runs one `skills` mode. Expected outcomes return a result with a code;
|
|
56
|
+
* an unexpected error returns `success: false` with code `error`.
|
|
57
|
+
*/
|
|
58
|
+
export declare function runSkillOperation(mode: string | undefined, args: SkillOperationArgs, deps: SkillOperationsDeps): Promise<SkillOperationResult>;
|
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
import { applyProposal } from "../services/skill-learning/apply.js";
|
|
2
|
+
import { draftImprovement, draftNewSkill, findSkillIdeas, } from "../services/skill-learning/drafting.js";
|
|
3
|
+
import { getProposal, listProposals, updateProposalState, } from "../services/skill-learning/proposal-store.js";
|
|
4
|
+
import { SKILL_ORDERS, rankSkills, } from "../services/skill-learning/ranking.js";
|
|
5
|
+
import { getSkillIndex } from "../services/skill-learning/skill-index.js";
|
|
6
|
+
import { evidence, propose, undo } from "./skill-agent-modes.js";
|
|
7
|
+
import { reply, resolveSkillPath, watchedTarget } from "./skill-operation-reply.js";
|
|
8
|
+
/**
|
|
9
|
+
* The `skills` tool and the `om-memory-system skills` command share these
|
|
10
|
+
* mode handlers and replies (design §8). The caller builds the deps from the
|
|
11
|
+
* config; `services/skill-learning/operations-deps.ts` does that.
|
|
12
|
+
*/
|
|
13
|
+
export const SKILL_OPERATION_MODES = [
|
|
14
|
+
"list",
|
|
15
|
+
"ideas",
|
|
16
|
+
"draft",
|
|
17
|
+
"evidence",
|
|
18
|
+
"propose",
|
|
19
|
+
"proposals",
|
|
20
|
+
"show",
|
|
21
|
+
"apply",
|
|
22
|
+
"reject",
|
|
23
|
+
"undo",
|
|
24
|
+
"help",
|
|
25
|
+
];
|
|
26
|
+
const PROPOSAL_STATES = [
|
|
27
|
+
"pending",
|
|
28
|
+
"applied",
|
|
29
|
+
"stale",
|
|
30
|
+
"rejected",
|
|
31
|
+
"undone",
|
|
32
|
+
];
|
|
33
|
+
export const RECORDING_OFF_TEXT = "Skill learning is off, so OMMS records no turns for skills. Turn it on in Settings → Skill learning, or set `skillLearning: true` in the global config.";
|
|
34
|
+
const HELP = [
|
|
35
|
+
{
|
|
36
|
+
mode: "list",
|
|
37
|
+
args: "order?",
|
|
38
|
+
description: "Skills ranked by use in the last 30 days, or by attention with order attention",
|
|
39
|
+
},
|
|
40
|
+
{ mode: "ideas", args: "", description: "Up to three new skill ideas from recorded turns" },
|
|
41
|
+
{
|
|
42
|
+
mode: "draft",
|
|
43
|
+
args: "skill | idea + folder",
|
|
44
|
+
description: "Draft an improvement for a skill, or a new skill from an idea",
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
mode: "evidence",
|
|
48
|
+
args: "skill, limit?",
|
|
49
|
+
description: "A skill's SKILL.md, its numbers and its recorded turns, failed ones first",
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
mode: "propose",
|
|
53
|
+
args: "skill | idea + folder, content, summary, evidence?",
|
|
54
|
+
description: "Save your own fixed SKILL.md as a pending proposal; no external API call",
|
|
55
|
+
},
|
|
56
|
+
{ mode: "proposals", args: "state?", description: "List proposals, newest first" },
|
|
57
|
+
{ mode: "show", args: "id", description: "One proposal with its summary, evidence and diff" },
|
|
58
|
+
{ mode: "apply", args: "id", description: "Write a pending proposal to its SKILL.md" },
|
|
59
|
+
{ mode: "reject", args: "id", description: "Reject a pending proposal" },
|
|
60
|
+
{ mode: "undo", args: "id", description: "Restore the file an applied proposal changed" },
|
|
61
|
+
{ mode: "help", args: "", description: "This list" },
|
|
62
|
+
];
|
|
63
|
+
function day(ms) {
|
|
64
|
+
return ms === null ? "never" : new Date(ms).toISOString().slice(0, 10);
|
|
65
|
+
}
|
|
66
|
+
function plural(count, word) {
|
|
67
|
+
return `${count} ${word}${count === 1 ? "" : "s"}`;
|
|
68
|
+
}
|
|
69
|
+
function rankingLine(skill) {
|
|
70
|
+
const parts = [
|
|
71
|
+
plural(skill.uses, "use"),
|
|
72
|
+
plural(skill.failedTurns, "failed turn"),
|
|
73
|
+
`last used ${day(skill.lastUsedAt)}`,
|
|
74
|
+
`attention ${skill.attention.toFixed(2)}`,
|
|
75
|
+
skill.mode,
|
|
76
|
+
];
|
|
77
|
+
if (skill.suggested)
|
|
78
|
+
parts.push("Suggested");
|
|
79
|
+
if (skill.notInWatchedFolder)
|
|
80
|
+
parts.push("Not in a watched folder");
|
|
81
|
+
const line = `- ${skill.name}: ${parts.join(", ")}`;
|
|
82
|
+
return skill.path ? `${line}\n ${skill.path}` : line;
|
|
83
|
+
}
|
|
84
|
+
function proposalLine(p) {
|
|
85
|
+
const summary = p.summary ? `: ${p.summary}` : "";
|
|
86
|
+
return `- ${p.id} ${p.state} ${p.kind} ${p.skillName} (${day(p.createdAt)})${summary}`;
|
|
87
|
+
}
|
|
88
|
+
function stateNote(p) {
|
|
89
|
+
if (p.state === "pending" && p.draftedBy === "agent")
|
|
90
|
+
return "Written by the agent";
|
|
91
|
+
if (p.state === "stale")
|
|
92
|
+
return "Stale: the file changed since this draft";
|
|
93
|
+
if (p.state === "applied" && p.appliedBy === "automatic")
|
|
94
|
+
return "Applied automatically";
|
|
95
|
+
if (p.state === "applied" && p.appliedBy)
|
|
96
|
+
return `Applied by ${p.appliedBy}`;
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
function proposalText(p) {
|
|
100
|
+
const lines = [
|
|
101
|
+
`Proposal ${p.id}`,
|
|
102
|
+
`Skill: ${p.skillName} (${p.kind === "new" ? "new skill" : "improvement"})`,
|
|
103
|
+
`File: ${p.skillPath ?? "none"}`,
|
|
104
|
+
`State: ${p.state}`,
|
|
105
|
+
];
|
|
106
|
+
const note = stateNote(p);
|
|
107
|
+
if (note)
|
|
108
|
+
lines.push(note);
|
|
109
|
+
lines.push(`Summary: ${p.summary || "none"}`);
|
|
110
|
+
const evidence = p.evidenceTurnIds.map((id) => `T${id}`).join(", ");
|
|
111
|
+
lines.push(`Evidence turns: ${evidence || "none"}`);
|
|
112
|
+
lines.push("", "Diff:", p.diff || "(no diff)");
|
|
113
|
+
return lines.join("\n");
|
|
114
|
+
}
|
|
115
|
+
function draftReply(mode, result) {
|
|
116
|
+
if (!result.ok)
|
|
117
|
+
return reply(mode, false, result.code, result.message);
|
|
118
|
+
if (result.code === "no-change")
|
|
119
|
+
return reply(mode, true, "no-change", result.message);
|
|
120
|
+
const p = result.proposal;
|
|
121
|
+
return reply(mode, true, "proposed", `Saved pending proposal ${p.id} for ${p.skillName}. Review it with show, then apply or reject it.`, { proposal: p });
|
|
122
|
+
}
|
|
123
|
+
async function list(args, deps, now) {
|
|
124
|
+
const order = (args.order ?? "uses");
|
|
125
|
+
if (!SKILL_ORDERS.includes(order)) {
|
|
126
|
+
return reply("list", false, "invalid-args", `order must be one of: ${SKILL_ORDERS.join(", ")}.`);
|
|
127
|
+
}
|
|
128
|
+
const skills = await rankSkills(deps.store, getSkillIndex(deps.folders, now), now, order);
|
|
129
|
+
if (skills.length === 0) {
|
|
130
|
+
return reply("list", true, "ok", "No skills found in the watched folders.", {
|
|
131
|
+
skills,
|
|
132
|
+
folders: deps.folders,
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
const heading = order === "attention" ? "Skills by attention" : "Skills by use";
|
|
136
|
+
const text = [`${heading} in the last 30 days:`, ...skills.map(rankingLine)].join("\n");
|
|
137
|
+
return reply("list", true, "ok", text, { skills });
|
|
138
|
+
}
|
|
139
|
+
async function ideas(deps, now) {
|
|
140
|
+
const result = await findSkillIdeas(draftDeps(deps, now));
|
|
141
|
+
if (!result.ok)
|
|
142
|
+
return reply("ideas", false, result.code, result.message);
|
|
143
|
+
if (result.ideas.length === 0) {
|
|
144
|
+
return reply("ideas", true, "ok", "No new skill ideas in the recorded turns.", { ideas: [] });
|
|
145
|
+
}
|
|
146
|
+
const lines = result.ideas.map((idea, i) => {
|
|
147
|
+
const evidence = idea.evidenceTurnIds.map((id) => `T${id}`).join(", ") || "none";
|
|
148
|
+
return `${i + 1}. ${idea.name}: ${idea.description}\n Why: ${idea.reason}\n Evidence turns: ${evidence}`;
|
|
149
|
+
});
|
|
150
|
+
lines.push("To draft one, call draft with the idea and a watched folder.");
|
|
151
|
+
return reply("ideas", true, "ok", lines.join("\n"), { ideas: result.ideas });
|
|
152
|
+
}
|
|
153
|
+
function draftDeps(deps, now) {
|
|
154
|
+
return {
|
|
155
|
+
store: deps.store,
|
|
156
|
+
index: getSkillIndex(deps.folders, now),
|
|
157
|
+
model: deps.model,
|
|
158
|
+
now: () => now,
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
async function draftNew(args, deps, now) {
|
|
162
|
+
const { idea, folder } = args;
|
|
163
|
+
if (!idea?.name || !idea.description) {
|
|
164
|
+
return reply("draft", false, "invalid-args", "An idea needs a name and a description.");
|
|
165
|
+
}
|
|
166
|
+
const target = watchedTarget("draft", deps.folders, folder);
|
|
167
|
+
if (isReply(target))
|
|
168
|
+
return target;
|
|
169
|
+
await deps.store.init();
|
|
170
|
+
const full = {
|
|
171
|
+
name: idea.name,
|
|
172
|
+
description: idea.description,
|
|
173
|
+
reason: idea.reason ?? "",
|
|
174
|
+
evidenceTurnIds: idea.evidenceTurnIds ?? [],
|
|
175
|
+
};
|
|
176
|
+
return draftReply("draft", await draftNewSkill(full, target.folder, draftDeps(deps, now)));
|
|
177
|
+
}
|
|
178
|
+
async function draft(args, deps, now) {
|
|
179
|
+
if (args.idea)
|
|
180
|
+
return draftNew(args, deps, now);
|
|
181
|
+
const skill = args.skill?.trim();
|
|
182
|
+
if (!skill)
|
|
183
|
+
return reply("draft", false, "invalid-args", "Give skill, or idea and folder.");
|
|
184
|
+
const index = getSkillIndex(deps.folders, now);
|
|
185
|
+
const found = resolveSkillPath("draft", skill, index);
|
|
186
|
+
if (isReply(found))
|
|
187
|
+
return found;
|
|
188
|
+
await deps.store.init();
|
|
189
|
+
return draftReply("draft", await draftImprovement(found.path, { ...draftDeps(deps, now), index }));
|
|
190
|
+
}
|
|
191
|
+
async function proposals(args, deps) {
|
|
192
|
+
const state = args.state;
|
|
193
|
+
if (state && !PROPOSAL_STATES.includes(state)) {
|
|
194
|
+
return reply("proposals", false, "invalid-args", `state must be one of: ${PROPOSAL_STATES.join(", ")}.`);
|
|
195
|
+
}
|
|
196
|
+
const found = await listProposals(deps.store, state ? { state } : {});
|
|
197
|
+
const text = found.length === 0
|
|
198
|
+
? `No ${state ? `${state} ` : ""}proposals.`
|
|
199
|
+
: ["Proposals, newest first:", ...found.map(proposalLine)].join("\n");
|
|
200
|
+
return reply("proposals", true, "ok", text, { proposals: found });
|
|
201
|
+
}
|
|
202
|
+
/** The proposal for `args.id`, or the reply that explains why there is none. */
|
|
203
|
+
async function findProposal(mode, args, deps) {
|
|
204
|
+
if (!args.id)
|
|
205
|
+
return reply(mode, false, "invalid-args", "Give the proposal id.");
|
|
206
|
+
const proposal = await getProposal(deps.store, args.id);
|
|
207
|
+
return proposal ?? reply(mode, false, "not-found", `No proposal ${args.id}.`);
|
|
208
|
+
}
|
|
209
|
+
function isReply(value) {
|
|
210
|
+
return "code" in value;
|
|
211
|
+
}
|
|
212
|
+
async function show(args, deps) {
|
|
213
|
+
const found = await findProposal("show", args, deps);
|
|
214
|
+
if (isReply(found))
|
|
215
|
+
return found;
|
|
216
|
+
return reply("show", true, "ok", proposalText(found), { proposal: found });
|
|
217
|
+
}
|
|
218
|
+
async function apply(args, deps, now) {
|
|
219
|
+
const found = await findProposal("apply", args, deps);
|
|
220
|
+
if (isReply(found))
|
|
221
|
+
return found;
|
|
222
|
+
await deps.store.init();
|
|
223
|
+
const result = await applyProposal(found.id, deps.appliedBy, {
|
|
224
|
+
store: deps.store,
|
|
225
|
+
folders: deps.folders,
|
|
226
|
+
now: () => now,
|
|
227
|
+
});
|
|
228
|
+
if (!result.ok)
|
|
229
|
+
return reply("apply", false, result.code, result.message);
|
|
230
|
+
return reply("apply", true, "applied", `Applied proposal ${found.id} to ${result.path}.`, {
|
|
231
|
+
proposal: result.proposal,
|
|
232
|
+
path: result.path,
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
async function reject(args, deps, now) {
|
|
236
|
+
const found = await findProposal("reject", args, deps);
|
|
237
|
+
if (isReply(found))
|
|
238
|
+
return found;
|
|
239
|
+
if (found.state !== "pending") {
|
|
240
|
+
return reply("reject", false, "not-pending", `Proposal ${found.id} is ${found.state}. Only a pending proposal can be rejected.`);
|
|
241
|
+
}
|
|
242
|
+
await deps.store.init();
|
|
243
|
+
await updateProposalState(deps.store, found.id, "rejected", now);
|
|
244
|
+
return reply("reject", true, "rejected", `Rejected proposal ${found.id}.`, {
|
|
245
|
+
proposal: { ...found, state: "rejected", updatedAt: now },
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
function help() {
|
|
249
|
+
const lines = HELP.map((h) => `- ${h.mode}${h.args ? ` (${h.args})` : ""}: ${h.description}`);
|
|
250
|
+
return reply("help", true, "ok", ["Skills tool modes:", ...lines].join("\n"), { modes: HELP });
|
|
251
|
+
}
|
|
252
|
+
/** Modes that read recorded turns. They need skill learning on. */
|
|
253
|
+
const RECORDING_MODES = new Set(["list", "ideas", "draft", "evidence"]);
|
|
254
|
+
/**
|
|
255
|
+
* Runs one `skills` mode. Expected outcomes return a result with a code;
|
|
256
|
+
* an unexpected error returns `success: false` with code `error`.
|
|
257
|
+
*/
|
|
258
|
+
export async function runSkillOperation(mode, args, deps) {
|
|
259
|
+
const name = mode || "help";
|
|
260
|
+
if (!SKILL_OPERATION_MODES.includes(name)) {
|
|
261
|
+
return reply(name, false, "invalid-args", `Unknown mode ${name}. Use one of: ${SKILL_OPERATION_MODES.join(", ")}.`);
|
|
262
|
+
}
|
|
263
|
+
if (RECORDING_MODES.has(name) && !deps.enabled) {
|
|
264
|
+
return reply(name, false, "recording-off", RECORDING_OFF_TEXT);
|
|
265
|
+
}
|
|
266
|
+
const now = (deps.now ?? Date.now)();
|
|
267
|
+
try {
|
|
268
|
+
switch (name) {
|
|
269
|
+
case "list":
|
|
270
|
+
return await list(args, deps, now);
|
|
271
|
+
case "ideas":
|
|
272
|
+
return await ideas(deps, now);
|
|
273
|
+
case "draft":
|
|
274
|
+
return await draft(args, deps, now);
|
|
275
|
+
case "evidence":
|
|
276
|
+
return await evidence(args, deps, now);
|
|
277
|
+
case "propose":
|
|
278
|
+
return await propose(args, deps, now);
|
|
279
|
+
case "undo":
|
|
280
|
+
return await undo(args, deps, now);
|
|
281
|
+
case "proposals":
|
|
282
|
+
return await proposals(args, deps);
|
|
283
|
+
case "show":
|
|
284
|
+
return await show(args, deps);
|
|
285
|
+
case "apply":
|
|
286
|
+
return await apply(args, deps, now);
|
|
287
|
+
case "reject":
|
|
288
|
+
return await reject(args, deps, now);
|
|
289
|
+
case "help":
|
|
290
|
+
return help();
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
catch (error) {
|
|
294
|
+
return reply(name, false, "error", error instanceof Error ? error.message : String(error));
|
|
295
|
+
}
|
|
296
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `skills` tool that Pi and OpenCode register beside `memory` (design §8).
|
|
3
|
+
* The operations, their store and the model load with `import()` on the first
|
|
4
|
+
* call, so the plugin bundles stay light. A call records `agent` on apply.
|
|
5
|
+
*/
|
|
6
|
+
/** Same list as `SKILL_OPERATION_MODES`; a test keeps the two equal. */
|
|
7
|
+
export declare const SKILLS_TOOL_MODES: readonly ["list", "ideas", "draft", "evidence", "propose", "proposals", "show", "apply", "reject", "undo", "help"];
|
|
8
|
+
export declare const SKILLS_TOOL_DESCRIPTION: string;
|
|
9
|
+
/** JSON Schema for the tool input; the V2 adapter passes it as is. */
|
|
10
|
+
export declare const SKILLS_TOOL_INPUT_SCHEMA: {
|
|
11
|
+
readonly type: "object";
|
|
12
|
+
readonly properties: {
|
|
13
|
+
readonly mode: {
|
|
14
|
+
readonly type: "string";
|
|
15
|
+
readonly enum: readonly ["list", "ideas", "draft", "evidence", "propose", "proposals", "show", "apply", "reject", "undo", "help"];
|
|
16
|
+
};
|
|
17
|
+
readonly skill: {
|
|
18
|
+
readonly type: "string";
|
|
19
|
+
};
|
|
20
|
+
readonly id: {
|
|
21
|
+
readonly type: "string";
|
|
22
|
+
};
|
|
23
|
+
readonly state: {
|
|
24
|
+
readonly type: "string";
|
|
25
|
+
};
|
|
26
|
+
readonly order: {
|
|
27
|
+
readonly type: "string";
|
|
28
|
+
readonly enum: readonly ["uses", "attention"];
|
|
29
|
+
};
|
|
30
|
+
readonly folder: {
|
|
31
|
+
readonly type: "string";
|
|
32
|
+
};
|
|
33
|
+
readonly idea: {
|
|
34
|
+
readonly type: "object";
|
|
35
|
+
readonly properties: {
|
|
36
|
+
readonly name: {
|
|
37
|
+
readonly type: "string";
|
|
38
|
+
};
|
|
39
|
+
readonly description: {
|
|
40
|
+
readonly type: "string";
|
|
41
|
+
};
|
|
42
|
+
readonly reason: {
|
|
43
|
+
readonly type: "string";
|
|
44
|
+
};
|
|
45
|
+
readonly evidenceTurnIds: {
|
|
46
|
+
readonly type: "array";
|
|
47
|
+
readonly items: {
|
|
48
|
+
readonly type: "number";
|
|
49
|
+
};
|
|
50
|
+
};
|
|
51
|
+
};
|
|
52
|
+
};
|
|
53
|
+
readonly limit: {
|
|
54
|
+
readonly type: "number";
|
|
55
|
+
};
|
|
56
|
+
readonly content: {
|
|
57
|
+
readonly type: "string";
|
|
58
|
+
};
|
|
59
|
+
readonly summary: {
|
|
60
|
+
readonly type: "string";
|
|
61
|
+
};
|
|
62
|
+
readonly evidence: {
|
|
63
|
+
readonly type: "array";
|
|
64
|
+
readonly items: {
|
|
65
|
+
readonly type: "number";
|
|
66
|
+
};
|
|
67
|
+
};
|
|
68
|
+
};
|
|
69
|
+
readonly additionalProperties: false;
|
|
70
|
+
};
|
|
71
|
+
export interface SkillsToolArgs {
|
|
72
|
+
mode?: string;
|
|
73
|
+
skill?: string;
|
|
74
|
+
id?: string;
|
|
75
|
+
state?: string;
|
|
76
|
+
order?: string;
|
|
77
|
+
folder?: string;
|
|
78
|
+
idea?: {
|
|
79
|
+
name?: string;
|
|
80
|
+
description?: string;
|
|
81
|
+
reason?: string;
|
|
82
|
+
evidenceTurnIds?: number[];
|
|
83
|
+
};
|
|
84
|
+
limit?: number;
|
|
85
|
+
content?: string;
|
|
86
|
+
summary?: string;
|
|
87
|
+
evidence?: number[];
|
|
88
|
+
}
|
|
89
|
+
/** Run one `skills` tool call for the project in `directory`; returns the result as JSON. */
|
|
90
|
+
export declare function runSkillsTool(args: SkillsToolArgs, directory: string): Promise<string>;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `skills` tool that Pi and OpenCode register beside `memory` (design §8).
|
|
3
|
+
* The operations, their store and the model load with `import()` on the first
|
|
4
|
+
* call, so the plugin bundles stay light. A call records `agent` on apply.
|
|
5
|
+
*/
|
|
6
|
+
/** Same list as `SKILL_OPERATION_MODES`; a test keeps the two equal. */
|
|
7
|
+
export const SKILLS_TOOL_MODES = [
|
|
8
|
+
"list",
|
|
9
|
+
"ideas",
|
|
10
|
+
"draft",
|
|
11
|
+
"evidence",
|
|
12
|
+
"propose",
|
|
13
|
+
"proposals",
|
|
14
|
+
"show",
|
|
15
|
+
"apply",
|
|
16
|
+
"reject",
|
|
17
|
+
"undo",
|
|
18
|
+
"help",
|
|
19
|
+
];
|
|
20
|
+
export const SKILLS_TOOL_DESCRIPTION = [
|
|
21
|
+
"Lists this project's skills ranked by use, drafts improvements as reviewable proposals, and applies a proposal only when the user asks.",
|
|
22
|
+
'mode "list" (order "uses" or "attention"): skills ranked by use in the last 30 days, or by attention. "ideas": up to three new skill ideas from recorded turns.',
|
|
23
|
+
'"draft" (skill, or an idea from "ideas" {name, description, reason, evidenceTurnIds} with folder): the external API drafts a pending proposal; it changes no file.',
|
|
24
|
+
'To fix a skill yourself: call "evidence" (skill, limit) for its SKILL.md and recorded turns, write the fixed SKILL.md, then call "propose" (skill, or idea with folder; content, summary, evidence turn ids). Show the user the diff.',
|
|
25
|
+
"This path makes no external API call, but the recorded turn text enters this session.",
|
|
26
|
+
'"proposals" (state), "show" (id): review proposals and their diffs.',
|
|
27
|
+
'"apply" (id): write a pending proposal to its SKILL.md, only when the user asks. "reject" (id), "undo" (id), "help".',
|
|
28
|
+
].join(" ");
|
|
29
|
+
/** JSON Schema for the tool input; the V2 adapter passes it as is. */
|
|
30
|
+
export const SKILLS_TOOL_INPUT_SCHEMA = {
|
|
31
|
+
type: "object",
|
|
32
|
+
properties: {
|
|
33
|
+
mode: { type: "string", enum: [...SKILLS_TOOL_MODES] },
|
|
34
|
+
skill: { type: "string" },
|
|
35
|
+
id: { type: "string" },
|
|
36
|
+
state: { type: "string" },
|
|
37
|
+
order: { type: "string", enum: ["uses", "attention"] },
|
|
38
|
+
folder: { type: "string" },
|
|
39
|
+
idea: {
|
|
40
|
+
type: "object",
|
|
41
|
+
properties: {
|
|
42
|
+
name: { type: "string" },
|
|
43
|
+
description: { type: "string" },
|
|
44
|
+
reason: { type: "string" },
|
|
45
|
+
evidenceTurnIds: { type: "array", items: { type: "number" } },
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
limit: { type: "number" },
|
|
49
|
+
content: { type: "string" },
|
|
50
|
+
summary: { type: "string" },
|
|
51
|
+
evidence: { type: "array", items: { type: "number" } },
|
|
52
|
+
},
|
|
53
|
+
additionalProperties: false,
|
|
54
|
+
};
|
|
55
|
+
/** Run one `skills` tool call for the project in `directory`; returns the result as JSON. */
|
|
56
|
+
export async function runSkillsTool(args, directory) {
|
|
57
|
+
const [{ runSkillOperation }, { skillOperationsDeps }] = await Promise.all([
|
|
58
|
+
import("./skill-operations.js"),
|
|
59
|
+
import("../services/skill-learning/operations-deps.js"),
|
|
60
|
+
]);
|
|
61
|
+
const { mode, ...rest } = args;
|
|
62
|
+
const result = await runSkillOperation(mode, rest, skillOperationsDeps(directory, "agent"));
|
|
63
|
+
return JSON.stringify(result);
|
|
64
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/** Cut a formatted tool input to 100 characters plus "...", as memory capture expects. */
|
|
2
|
+
export declare function boundToolInput(text: string): string;
|
|
3
|
+
/**
|
|
4
|
+
* The full path field of a read-type call (`read`, `Read`, `view`), so skill
|
|
5
|
+
* detection sees a `SKILL.md` path that the bounded input cuts off.
|
|
6
|
+
*/
|
|
7
|
+
export declare function readToolPath(name: string, args: unknown): string | undefined;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
const MAX_TOOL_INPUT_LENGTH = 100;
|
|
2
|
+
const READ_TOOLS = new Set(["read", "Read", "view"]);
|
|
3
|
+
const READ_PATH_KEYS = ["file_path", "filePath", "path"];
|
|
4
|
+
/** Cut a formatted tool input to 100 characters plus "...", as memory capture expects. */
|
|
5
|
+
export function boundToolInput(text) {
|
|
6
|
+
return text.length > MAX_TOOL_INPUT_LENGTH
|
|
7
|
+
? text.substring(0, MAX_TOOL_INPUT_LENGTH) + "..."
|
|
8
|
+
: text;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* The full path field of a read-type call (`read`, `Read`, `view`), so skill
|
|
12
|
+
* detection sees a `SKILL.md` path that the bounded input cuts off.
|
|
13
|
+
*/
|
|
14
|
+
export function readToolPath(name, args) {
|
|
15
|
+
if (!READ_TOOLS.has(name) || !args || typeof args !== "object" || Array.isArray(args)) {
|
|
16
|
+
return undefined;
|
|
17
|
+
}
|
|
18
|
+
const record = args;
|
|
19
|
+
const key = READ_PATH_KEYS.find((candidate) => typeof record[candidate] === "string");
|
|
20
|
+
return key ? record[key] : undefined;
|
|
21
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { CaptureToolCall } from "./host.js";
|
|
2
|
+
export declare const MAX_TOOL_ERROR_LENGTH = 2000;
|
|
3
|
+
type ToolOutcomeFields = Pick<CaptureToolCall, "outcome" | "error">;
|
|
4
|
+
/** Error text as the host reported it, cut to at most 2,000 characters. */
|
|
5
|
+
export declare function boundToolError(text: string): string;
|
|
6
|
+
/** The outcome of a failed call, with its error text when the host gave one. */
|
|
7
|
+
export declare function errorOutcome(text: unknown): ToolOutcomeFields;
|
|
8
|
+
/**
|
|
9
|
+
* The outcome of an OpenCode tool part from its `state`. The live reader and
|
|
10
|
+
* the history import read the same part shape.
|
|
11
|
+
*/
|
|
12
|
+
export declare function opencodeToolOutcome(state: unknown): ToolOutcomeFields;
|
|
13
|
+
export {};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export const MAX_TOOL_ERROR_LENGTH = 2000;
|
|
2
|
+
/** Error text as the host reported it, cut to at most 2,000 characters. */
|
|
3
|
+
export function boundToolError(text) {
|
|
4
|
+
return text.length > MAX_TOOL_ERROR_LENGTH ? text.slice(0, MAX_TOOL_ERROR_LENGTH) : text;
|
|
5
|
+
}
|
|
6
|
+
/** The outcome of a failed call, with its error text when the host gave one. */
|
|
7
|
+
export function errorOutcome(text) {
|
|
8
|
+
const error = typeof text === "string" ? text.trim() : "";
|
|
9
|
+
return error ? { outcome: "error", error: boundToolError(error) } : { outcome: "error" };
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* The outcome of an OpenCode tool part from its `state`. The live reader and
|
|
13
|
+
* the history import read the same part shape.
|
|
14
|
+
*/
|
|
15
|
+
export function opencodeToolOutcome(state) {
|
|
16
|
+
const { status, error } = (state && typeof state === "object" ? state : {});
|
|
17
|
+
if (status === "completed")
|
|
18
|
+
return { outcome: "ok" };
|
|
19
|
+
if (status === "error")
|
|
20
|
+
return errorOutcome(error);
|
|
21
|
+
return { outcome: "unknown" };
|
|
22
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { type CaptureResult, type CaptureWorkUnit } from "./capture.js";
|
|
2
|
+
import type { CaptureSummaryProvider } from "./host.js";
|
|
3
|
+
/** Which outputs a finished turn goes to. */
|
|
4
|
+
export interface TurnOutputs {
|
|
5
|
+
memories: boolean;
|
|
6
|
+
skills: boolean;
|
|
7
|
+
}
|
|
8
|
+
export interface TurnOutputDeps {
|
|
9
|
+
/** The capture model. Needed when `memories` is on. */
|
|
10
|
+
provider?: CaptureSummaryProvider;
|
|
11
|
+
/**
|
|
12
|
+
* The skill-turn output. Injected so `src/core/` imports nothing from
|
|
13
|
+
* `skill-learning`; the live sites build it from `skillTurnRecorder`.
|
|
14
|
+
*/
|
|
15
|
+
recordSkillTurn?: (unit: CaptureWorkUnit) => Promise<unknown>;
|
|
16
|
+
}
|
|
17
|
+
/** The memory result, or `not-run` when the memory output was off for this turn. */
|
|
18
|
+
export type TurnMemoryResult = CaptureResult | {
|
|
19
|
+
status: "not-run";
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* The one path for a finished turn (design decision 3). The skill output runs
|
|
23
|
+
* first and only logs its failures. The memory output is `captureConversation`
|
|
24
|
+
* unchanged: same result, same errors.
|
|
25
|
+
*/
|
|
26
|
+
export declare function handleFinishedTurn(unit: CaptureWorkUnit, outputs: {
|
|
27
|
+
memories: true;
|
|
28
|
+
skills: boolean;
|
|
29
|
+
}, deps: TurnOutputDeps): Promise<CaptureResult>;
|
|
30
|
+
export declare function handleFinishedTurn(unit: CaptureWorkUnit, outputs: TurnOutputs, deps: TurnOutputDeps): Promise<TurnMemoryResult>;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { log } from "../services/logger.js";
|
|
2
|
+
import { captureConversation } from "./capture.js";
|
|
3
|
+
export async function handleFinishedTurn(unit, outputs, deps) {
|
|
4
|
+
if (outputs.skills && deps.recordSkillTurn) {
|
|
5
|
+
try {
|
|
6
|
+
await deps.recordSkillTurn(unit);
|
|
7
|
+
}
|
|
8
|
+
catch (error) {
|
|
9
|
+
// Codes only: the error may quote turn text.
|
|
10
|
+
log("Skill turn record failed", {
|
|
11
|
+
host: unit.host,
|
|
12
|
+
code: "skill-record-failed",
|
|
13
|
+
error: error instanceof Error ? error.name : "unknown",
|
|
14
|
+
}, "warn");
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
if (!outputs.memories)
|
|
18
|
+
return { status: "not-run" };
|
|
19
|
+
if (!deps.provider)
|
|
20
|
+
throw new Error("handleFinishedTurn needs a provider for memories");
|
|
21
|
+
return captureConversation(unit, deps.provider);
|
|
22
|
+
}
|