@jeffober/thatch 0.1.35 → 0.1.36
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 +15 -0
- package/bin/thatch +157 -0
- package/docs/dev/README.md +10 -3
- package/docs/dev/features/README.md +1 -0
- package/docs/dev/features/multi-host.md +16 -10
- package/docs/dev/features/watchers.md +150 -0
- package/docs/dev/mcp-parity.md +1 -1
- package/docs/dev/skills.md +6 -4
- package/docs/user/README.md +24 -0
- package/docs/user/watchers.md +79 -0
- package/package.json +1 -1
- package/src/index.ts +61 -2
- package/src/mcp.ts +4 -1
- package/src/prompts.ts +43 -7
- package/src/session-db.ts +452 -0
- package/src/skills.ts +22 -46
- package/src/tool-defs.ts +269 -3
- package/src/tools.ts +11 -3
- package/src/watchers.ts +511 -0
package/src/skills.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { mkdirSync, readdirSync, readFileSync, rmSync, unlinkSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
|
-
import pkg from "../package.json";
|
|
4
3
|
|
|
5
4
|
export interface SkillFile {
|
|
6
5
|
name: string;
|
|
@@ -73,46 +72,32 @@ export const OPENCODE_ONLY_SKILLS: SkillDef[] = loadOpencodeOnlySkills();
|
|
|
73
72
|
// Stale skill cleanup - runs before install on every skillsDir
|
|
74
73
|
// ---------------------------------------------------------------------------
|
|
75
74
|
|
|
76
|
-
//
|
|
77
|
-
//
|
|
78
|
-
//
|
|
79
|
-
|
|
80
|
-
//
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
//
|
|
88
|
-
//
|
|
89
|
-
// old skill directories. Users who skip past this version must manually
|
|
90
|
-
// delete the stale directories.
|
|
91
|
-
const RENAME_MIGRATION_MAX_VERSION = "0.1.35";
|
|
92
|
-
|
|
93
|
-
function compareVersions(a: string, b: string): number {
|
|
94
|
-
const [aMaj, aMin, aPatch] = a.split(".").map(Number);
|
|
95
|
-
const [bMaj, bMin, bPatch] = b.split(".").map(Number);
|
|
96
|
-
if (aMaj !== bMaj) return aMaj - bMaj;
|
|
97
|
-
if (aMin !== bMin) return aMin - bMin;
|
|
98
|
-
return aPatch - bPatch;
|
|
99
|
-
}
|
|
75
|
+
// ---------------------------------------------------------------------------
|
|
76
|
+
// Stale skill cleanup - runs before install on every skillsDir
|
|
77
|
+
// ---------------------------------------------------------------------------
|
|
78
|
+
|
|
79
|
+
// Skills renamed from non-prefixed to thatch-prefixed in v0.1.27
|
|
80
|
+
// (pr-description, ticket-description, split-overlarge-pr) were cleaned up
|
|
81
|
+
// during install through a version-gated window that closed with v0.1.35.
|
|
82
|
+
// The gate is gone because it can never reopen: package versions only grow,
|
|
83
|
+
// so this code only ever runs at a version past the window. The standing
|
|
84
|
+
// contract it protected remains: non-prefixed skill names belong to third
|
|
85
|
+
// parties and are NEVER removed - cleanup only touches thatch-* dirs, where
|
|
86
|
+
// the prefix is our namespace. Users upgrading from before v0.1.27 delete
|
|
87
|
+
// the old directories manually.
|
|
100
88
|
|
|
101
89
|
/**
|
|
102
90
|
* Remove stale skill directories before installing the current set.
|
|
103
91
|
*
|
|
104
|
-
*
|
|
92
|
+
* One rule: delete any `thatch-*` skill directory that is not in the current
|
|
93
|
+
* install set. The `thatch-` prefix is our namespace - no third party should
|
|
94
|
+
* use it, so unconditional removal is safe. This catches skills we've
|
|
95
|
+
* renamed or removed in any release.
|
|
105
96
|
*
|
|
106
|
-
*
|
|
107
|
-
*
|
|
108
|
-
*
|
|
109
|
-
*
|
|
110
|
-
*
|
|
111
|
-
* 2. Version-gated: delete directories matching old non-prefixed skill names
|
|
112
|
-
* (pr-description, ticket-description, split-overlarge-pr) through
|
|
113
|
-
* RENAME_MIGRATION_MAX_VERSION. These names lack the thatch- prefix and
|
|
114
|
-
* could be adopted by third parties after we abandon them, so the cleanup
|
|
115
|
-
* is time-boxed. Users who upgrade past the window must manually clean up.
|
|
97
|
+
* Non-prefixed directories are never touched. The v0.1.27 rename migration
|
|
98
|
+
* (pr-description -> thatch-pr-description and siblings) removed old
|
|
99
|
+
* non-prefixed dirs through a version window that closed with v0.1.35; see
|
|
100
|
+
* the block comment above.
|
|
116
101
|
*/
|
|
117
102
|
function cleanupStaleSkills(skillsDir: string, currentNames: string[]): void {
|
|
118
103
|
const currentSet = new Set(currentNames);
|
|
@@ -123,9 +108,6 @@ function cleanupStaleSkills(skillsDir: string, currentNames: string[]): void {
|
|
|
123
108
|
return; // dir doesn't exist or isn't readable - nothing to clean
|
|
124
109
|
}
|
|
125
110
|
|
|
126
|
-
const withinMigrationWindow =
|
|
127
|
-
compareVersions(pkg.version, RENAME_MIGRATION_MAX_VERSION) <= 0;
|
|
128
|
-
|
|
129
111
|
for (const entry of entries) {
|
|
130
112
|
// isDirectory() returns false for symlinks (even symlinked dirs), so
|
|
131
113
|
// check isSymbolicLink() too - a symlinked skill dir is just as stale
|
|
@@ -149,15 +131,9 @@ function cleanupStaleSkills(skillsDir: string, currentNames: string[]): void {
|
|
|
149
131
|
}
|
|
150
132
|
};
|
|
151
133
|
|
|
152
|
-
//
|
|
134
|
+
// Stale thatch-* dirs: our namespace, always safe to remove.
|
|
153
135
|
if (name.startsWith("thatch-") && !currentSet.has(name)) {
|
|
154
136
|
remove();
|
|
155
|
-
continue;
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
// Rule 2: renamed non-prefixed dirs (version-gated, third-party risk).
|
|
159
|
-
if (RENAMED_SKILLS.has(name) && withinMigrationWindow) {
|
|
160
|
-
remove();
|
|
161
137
|
}
|
|
162
138
|
}
|
|
163
139
|
}
|
package/src/tool-defs.ts
CHANGED
|
@@ -2,6 +2,8 @@ import { z } from "zod";
|
|
|
2
2
|
import type { ThatchDB, DedupCandidate, MemoryRow } from "./db";
|
|
3
3
|
import type { EmbeddingModel } from "./embeddings";
|
|
4
4
|
import { predictionVerb } from "./prompts";
|
|
5
|
+
import { resolveOpencodeDbPath, SessionDB, partToTimelineEntry, partToFullJson, messageToFullJson } from "./session-db";
|
|
6
|
+
import { WATCHER_EVENT_TYPES, type WatcherRegistry, type WatcherEventType } from "./watchers";
|
|
5
7
|
|
|
6
8
|
// Near-duplicate thresholds for matcher/prediction/behavior dedup at
|
|
7
9
|
// creation time. Matches the thatch_find_duplicates threshold (0.85).
|
|
@@ -36,6 +38,21 @@ export interface CoreContext {
|
|
|
36
38
|
* childToParent). Called by extraction_done when the sub-agent passes
|
|
37
39
|
* the parent's session_id. */
|
|
38
40
|
drainExtractionQueue?: (sessionID: string) => void;
|
|
41
|
+
/** The plugin's in-memory watcher registry. Only wired on the opencode
|
|
42
|
+
* path - MCP hosts have no poller, no event bus, and no way to deliver a
|
|
43
|
+
* proactive prompt, so watch tools are opencode-only. */
|
|
44
|
+
watchers?: WatcherRegistry;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Per-call host context identifying the session a tool call belongs to.
|
|
49
|
+
* opencode's plugin system supplies it on every tool execution; MCP servers
|
|
50
|
+
* have no session concept and omit it. Only tools that need session identity
|
|
51
|
+
* consume it - the rest ignore the third execute parameter.
|
|
52
|
+
*/
|
|
53
|
+
export interface HostToolContext {
|
|
54
|
+
sessionID: string;
|
|
55
|
+
agent: string;
|
|
39
56
|
}
|
|
40
57
|
|
|
41
58
|
/**
|
|
@@ -49,7 +66,13 @@ export interface ToolDef {
|
|
|
49
66
|
name: string;
|
|
50
67
|
description: string;
|
|
51
68
|
args: Record<string, z.ZodType>;
|
|
52
|
-
execute(args: Record<string, unknown>, ctx: CoreContext): Promise<string>;
|
|
69
|
+
execute(args: Record<string, unknown>, ctx: CoreContext, host?: HostToolContext): Promise<string>;
|
|
70
|
+
/**
|
|
71
|
+
* When true, the tool exists only on the opencode plugin path and the MCP
|
|
72
|
+
* server filters it out of tools/list. Used for tools that depend on host
|
|
73
|
+
* capabilities MCP hosts lack (e.g. session identity).
|
|
74
|
+
*/
|
|
75
|
+
opencodeOnly?: boolean;
|
|
53
76
|
}
|
|
54
77
|
|
|
55
78
|
// ---------------------------------------------------------------------------
|
|
@@ -795,10 +818,247 @@ const behaviorDeleteDef: ToolDef = {
|
|
|
795
818
|
},
|
|
796
819
|
};
|
|
797
820
|
|
|
821
|
+
/**
|
|
822
|
+
* Reports the calling session's identity. opencode does not surface its
|
|
823
|
+
* session ID to the model, so the model cannot learn it any other way -
|
|
824
|
+
* yet it needs the ID to call thatch_get_extraction_payload /
|
|
825
|
+
* thatch_extraction_done on the parent's behalf and for session
|
|
826
|
+
* archaeology against opencode.db. MCP hosts have no session concept,
|
|
827
|
+
* hence opencodeOnly.
|
|
828
|
+
*/
|
|
829
|
+
const getSessionInfoDef: ToolDef = {
|
|
830
|
+
name: "get_session_info",
|
|
831
|
+
description:
|
|
832
|
+
"Return the current session's identity: the host session ID and the " +
|
|
833
|
+
"agent name running this turn (e.g. build, plan, or a sub-agent type). " +
|
|
834
|
+
"opencode-only: MCP hosts have no session concept. Use the session ID " +
|
|
835
|
+
"when a nudge tells you to call a tool with the parent's session_id, " +
|
|
836
|
+
"or to query past sessions in ~/.local/share/opencode/opencode.db.",
|
|
837
|
+
args: {},
|
|
838
|
+
opencodeOnly: true,
|
|
839
|
+
async execute(_args, _ctx, host) {
|
|
840
|
+
if (!host) {
|
|
841
|
+
return "Session identity is unavailable: this host did not provide a session context.";
|
|
842
|
+
}
|
|
843
|
+
return `sessionID: ${host.sessionID}\nagent: ${host.agent}`;
|
|
844
|
+
},
|
|
845
|
+
};
|
|
846
|
+
|
|
847
|
+
/**
|
|
848
|
+
* Lets the model search its own past opencode conversations. The model only
|
|
849
|
+
* knows the current session id (via get_session_info); everything before
|
|
850
|
+
* compaction or in earlier sessions is invisible to it otherwise. Returns
|
|
851
|
+
* the same JSONL timeline shape as `thatch session search`, with previews
|
|
852
|
+
* truncated so a result set stays context-affordable; session_get fetches
|
|
853
|
+
* full content for interesting hits.
|
|
854
|
+
*/
|
|
855
|
+
const sessionSearchDef: ToolDef = {
|
|
856
|
+
name: "session_search",
|
|
857
|
+
description:
|
|
858
|
+
"Search past opencode session conversations by substring (or regex with " +
|
|
859
|
+
"regex: true). Matches decoded message text, tool outputs, and reasoning " +
|
|
860
|
+
"across all sessions (or one session with session_id). Returns JSONL " +
|
|
861
|
+
"entries with ids for follow-up: call session_get with a hit's part_id " +
|
|
862
|
+
"or msg_id to retrieve the full content. opencode-only.",
|
|
863
|
+
args: {
|
|
864
|
+
query: z.string().describe(
|
|
865
|
+
"Substring to find (case-insensitive), or a regular expression when regex is true.",
|
|
866
|
+
),
|
|
867
|
+
regex: z.boolean().optional().describe(
|
|
868
|
+
"Treat query as a regular expression instead of a plain substring.",
|
|
869
|
+
),
|
|
870
|
+
session_id: z.string().optional().describe(
|
|
871
|
+
"Narrow the search to one session. Omit to search all sessions.",
|
|
872
|
+
),
|
|
873
|
+
limit: z.number().optional().describe(
|
|
874
|
+
"Maximum matches to return (default 200, oldest first).",
|
|
875
|
+
),
|
|
876
|
+
},
|
|
877
|
+
opencodeOnly: true,
|
|
878
|
+
async execute(args) {
|
|
879
|
+
const dbPath = resolveOpencodeDbPath();
|
|
880
|
+
if (!dbPath) {
|
|
881
|
+
return "Session search is unavailable: no opencode database was found (set OPENCODE_DB to point at one).";
|
|
882
|
+
}
|
|
883
|
+
const db = new SessionDB(dbPath);
|
|
884
|
+
try {
|
|
885
|
+
const matches = db.search(args.query as string, {
|
|
886
|
+
regex: args.regex === true,
|
|
887
|
+
sessionID: args.session_id as string | undefined,
|
|
888
|
+
limit: typeof args.limit === "number" ? args.limit : undefined,
|
|
889
|
+
});
|
|
890
|
+
if (matches.length === 0) return `No matches for "${args.query}".`;
|
|
891
|
+
// Session titles orient cross-session results the same way the CLI
|
|
892
|
+
// search does; cached because matches cluster by session.
|
|
893
|
+
const titles = new Map<string, string | null>();
|
|
894
|
+
return matches
|
|
895
|
+
.map((part) => {
|
|
896
|
+
if (!titles.has(part.sessionId)) {
|
|
897
|
+
titles.set(part.sessionId, db.getSession(part.sessionId)?.title ?? null);
|
|
898
|
+
}
|
|
899
|
+
return JSON.stringify({
|
|
900
|
+
session_id: part.sessionId,
|
|
901
|
+
session_title: titles.get(part.sessionId),
|
|
902
|
+
...partToTimelineEntry(part),
|
|
903
|
+
});
|
|
904
|
+
})
|
|
905
|
+
.join("\n");
|
|
906
|
+
} finally {
|
|
907
|
+
db.close();
|
|
908
|
+
}
|
|
909
|
+
},
|
|
910
|
+
};
|
|
911
|
+
|
|
912
|
+
/**
|
|
913
|
+
* Fetches the full, untruncated content of one part or message found via
|
|
914
|
+
* session_search. Tool parts carry their complete args and output - the
|
|
915
|
+
* detail the timeline preview truncates.
|
|
916
|
+
*/
|
|
917
|
+
const sessionGetDef: ToolDef = {
|
|
918
|
+
name: "session_get",
|
|
919
|
+
description:
|
|
920
|
+
"Retrieve the full content of one conversation item by id. Pass a " +
|
|
921
|
+
"part_id (from session_search or session list) for one content piece, " +
|
|
922
|
+
"or a msg_id for an entire message with all of its parts. Tool parts " +
|
|
923
|
+
"include their complete input arguments and output. opencode-only.",
|
|
924
|
+
args: {
|
|
925
|
+
id: z.string().describe(
|
|
926
|
+
"A part id (prt_...) or message id (msg_...) from session_search results.",
|
|
927
|
+
),
|
|
928
|
+
},
|
|
929
|
+
opencodeOnly: true,
|
|
930
|
+
async execute(args) {
|
|
931
|
+
const dbPath = resolveOpencodeDbPath();
|
|
932
|
+
if (!dbPath) {
|
|
933
|
+
return "Session retrieval is unavailable: no opencode database was found (set OPENCODE_DB to point at one).";
|
|
934
|
+
}
|
|
935
|
+
const db = new SessionDB(dbPath);
|
|
936
|
+
try {
|
|
937
|
+
const id = args.id as string;
|
|
938
|
+
if (id.startsWith("msg_")) {
|
|
939
|
+
const message = db.getMessage(id);
|
|
940
|
+
if (!message) return `No message "${id}" found.`;
|
|
941
|
+
return JSON.stringify(messageToFullJson(message), null, 2);
|
|
942
|
+
}
|
|
943
|
+
const part = db.getPart(id);
|
|
944
|
+
if (!part) return `No part "${id}" found.`;
|
|
945
|
+
return JSON.stringify(partToFullJson(part), null, 2);
|
|
946
|
+
} finally {
|
|
947
|
+
db.close();
|
|
948
|
+
}
|
|
949
|
+
},
|
|
950
|
+
};
|
|
951
|
+
|
|
952
|
+
/**
|
|
953
|
+
* Registers a background watcher on a GitHub PR. The plugin process polls
|
|
954
|
+
* the PR (via the gh CLI, using the user's existing gh auth) and prompts the
|
|
955
|
+
* session with a synthetic notification when a watched event happens.
|
|
956
|
+
* opencode-only: MCP hosts have no poller and no proactive-prompt channel.
|
|
957
|
+
*/
|
|
958
|
+
const watchCreateDef: ToolDef = {
|
|
959
|
+
name: "watch_create",
|
|
960
|
+
description:
|
|
961
|
+
"Watch a GitHub PR for events and get notified in this session when they " +
|
|
962
|
+
"happen. Events: new top-level comments, new inline review comments, " +
|
|
963
|
+
"replies to review comments, new commits (head SHA change), PR status " +
|
|
964
|
+
"changes, title/description edits, and completed CI check runs. The " +
|
|
965
|
+
"watcher polls in the background and injects a notification prompt; " +
|
|
966
|
+
"notifications carry pointers only (author, URL) - fetch details yourself " +
|
|
967
|
+
"with the gh CLI when you decide to act. Watches live until cancelled, " +
|
|
968
|
+
"the session ends, or opencode restarts. Requires the gh CLI.",
|
|
969
|
+
args: {
|
|
970
|
+
pr: z.number().int().positive().describe(
|
|
971
|
+
"The PR number to watch.",
|
|
972
|
+
),
|
|
973
|
+
repo: z.string().optional().describe(
|
|
974
|
+
"GitHub repo as owner/repo. Defaults to this project's repo from the git remote.",
|
|
975
|
+
),
|
|
976
|
+
events: z.array(z.enum(WATCHER_EVENT_TYPES as [WatcherEventType, ...WatcherEventType[]])).optional().describe(
|
|
977
|
+
"Which event types to watch. Omit for all event types.",
|
|
978
|
+
),
|
|
979
|
+
},
|
|
980
|
+
opencodeOnly: true,
|
|
981
|
+
async execute(args, ctx, host) {
|
|
982
|
+
if (!host) {
|
|
983
|
+
return "Watching is unavailable: this host did not provide a session context.";
|
|
984
|
+
}
|
|
985
|
+
if (!ctx.watchers) {
|
|
986
|
+
return "Watching is unavailable: no watcher registry was wired by this host.";
|
|
987
|
+
}
|
|
988
|
+
const repo = (args.repo as string | undefined) ?? ctx.defaultStore;
|
|
989
|
+
const events = (args.events as WatcherEventType[] | undefined) ?? [...WATCHER_EVENT_TYPES];
|
|
990
|
+
const result = await ctx.watchers.create(host.sessionID, repo, args.pr as number, events);
|
|
991
|
+
if (!result.ok) return `Watcher not created: ${result.error}`;
|
|
992
|
+
const w = result.watcher;
|
|
993
|
+
return (
|
|
994
|
+
`[watching] ${w.repo}#${w.pr}\n` +
|
|
995
|
+
`id: ${w.id}\n` +
|
|
996
|
+
`events: ${w.events.join(", ")}\n` +
|
|
997
|
+
`head: ${w.state.headSha.slice(0, 7)} (${w.state.state}${w.state.merged ? ", merged" : ""})\n\n` +
|
|
998
|
+
`The baseline is captured now - only changes from this point notify. ` +
|
|
999
|
+
`You will receive a system notification in this session when a watched event happens. ` +
|
|
1000
|
+
`State any handling policy for those notifications now (e.g. what to act on, what just to report), ` +
|
|
1001
|
+
`since the notification itself carries only pointers.`
|
|
1002
|
+
);
|
|
1003
|
+
},
|
|
1004
|
+
};
|
|
1005
|
+
|
|
1006
|
+
/**
|
|
1007
|
+
* Lists the calling session's active watchers. opencode-only, session-scoped:
|
|
1008
|
+
* a session can only ever see its own watches.
|
|
1009
|
+
*/
|
|
1010
|
+
const watchListDef: ToolDef = {
|
|
1011
|
+
name: "watch_list",
|
|
1012
|
+
description:
|
|
1013
|
+
"List this session's active watchers with their ids, targets, watched " +
|
|
1014
|
+
"event types, and remaining time. opencode-only.",
|
|
1015
|
+
args: {},
|
|
1016
|
+
opencodeOnly: true,
|
|
1017
|
+
async execute(_args, ctx, host) {
|
|
1018
|
+
if (!host) {
|
|
1019
|
+
return "Watching is unavailable: this host did not provide a session context.";
|
|
1020
|
+
}
|
|
1021
|
+
if (!ctx.watchers) {
|
|
1022
|
+
return "Watching is unavailable: no watcher registry was wired by this host.";
|
|
1023
|
+
}
|
|
1024
|
+
const watchers = ctx.watchers.listForSession(host.sessionID);
|
|
1025
|
+
if (watchers.length === 0) return "No active watchers.";
|
|
1026
|
+
const minutesLeft = (w: { expiresAt: number }) => Math.max(0, Math.round((w.expiresAt - Date.now()) / 60_000));
|
|
1027
|
+
return watchers
|
|
1028
|
+
.map((w) => `${w.id}: ${w.repo}#${w.pr} events=[${w.events.join(",")}] expires in ${minutesLeft(w)}m head=${w.state.headSha.slice(0, 7)}`)
|
|
1029
|
+
.join("\n");
|
|
1030
|
+
},
|
|
1031
|
+
};
|
|
1032
|
+
|
|
1033
|
+
/**
|
|
1034
|
+
* Cancels one watcher. opencode-only, session-scoped: cancelling by id only
|
|
1035
|
+
* works for the session that created it.
|
|
1036
|
+
*/
|
|
1037
|
+
const watchCancelDef: ToolDef = {
|
|
1038
|
+
name: "watch_cancel",
|
|
1039
|
+
description: "Cancel one of this session's watchers by id. opencode-only.",
|
|
1040
|
+
args: {
|
|
1041
|
+
id: z.string().describe("The watcher id from watch_create or watch_list."),
|
|
1042
|
+
},
|
|
1043
|
+
opencodeOnly: true,
|
|
1044
|
+
async execute(args, ctx, host) {
|
|
1045
|
+
if (!host) {
|
|
1046
|
+
return "Watching is unavailable: this host did not provide a session context.";
|
|
1047
|
+
}
|
|
1048
|
+
if (!ctx.watchers) {
|
|
1049
|
+
return "Watching is unavailable: no watcher registry was wired by this host.";
|
|
1050
|
+
}
|
|
1051
|
+
const cancelled = ctx.watchers.cancel(host.sessionID, args.id as string);
|
|
1052
|
+
if (!cancelled) return `No watcher "${args.id}" in this session.`;
|
|
1053
|
+
return `[cancelled] ${args.id}`;
|
|
1054
|
+
},
|
|
1055
|
+
};
|
|
1056
|
+
|
|
798
1057
|
/**
|
|
799
1058
|
* All tool definitions, in the order they should be presented to the agent.
|
|
800
|
-
* The opencode plugin wraps each in `tool()`; the MCP server exposes
|
|
801
|
-
* via `tools/list` and dispatches `tools/call` to
|
|
1059
|
+
* The opencode plugin wraps each in `tool()`; the MCP server exposes the
|
|
1060
|
+
* non-opencodeOnly ones via `tools/list` and dispatches `tools/call` to
|
|
1061
|
+
* their execute functions.
|
|
802
1062
|
*/
|
|
803
1063
|
export const TOOL_DEFS: ToolDef[] = [
|
|
804
1064
|
rememberDef,
|
|
@@ -819,4 +1079,10 @@ export const TOOL_DEFS: ToolDef[] = [
|
|
|
819
1079
|
behaviorFeedbackDef,
|
|
820
1080
|
behaviorListDef,
|
|
821
1081
|
behaviorDeleteDef,
|
|
1082
|
+
getSessionInfoDef,
|
|
1083
|
+
sessionSearchDef,
|
|
1084
|
+
sessionGetDef,
|
|
1085
|
+
watchCreateDef,
|
|
1086
|
+
watchListDef,
|
|
1087
|
+
watchCancelDef,
|
|
822
1088
|
];
|
package/src/tools.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { tool } from "@opencode-ai/plugin";
|
|
2
2
|
import type { ThatchDB } from "./db";
|
|
3
3
|
import type { EmbeddingModel } from "./embeddings";
|
|
4
|
-
import { TOOL_DEFS, type CoreContext } from "./tool-defs";
|
|
4
|
+
import { TOOL_DEFS, type CoreContext, type HostToolContext } from "./tool-defs";
|
|
5
|
+
import type { WatcherRegistry } from "./watchers";
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* Builds the opencode tool map from shared tool definitions. Each definition
|
|
@@ -16,6 +17,7 @@ export function createTools(
|
|
|
16
17
|
extensions?: {
|
|
17
18
|
extractionPayloadProvider?: CoreContext["extractionPayloadProvider"];
|
|
18
19
|
drainExtractionQueue?: CoreContext["drainExtractionQueue"];
|
|
20
|
+
watcherRegistry?: WatcherRegistry;
|
|
19
21
|
},
|
|
20
22
|
): Record<string, ReturnType<typeof tool>> {
|
|
21
23
|
const ctx: CoreContext = {
|
|
@@ -24,6 +26,7 @@ export function createTools(
|
|
|
24
26
|
defaultStore,
|
|
25
27
|
extractionPayloadProvider: extensions?.extractionPayloadProvider,
|
|
26
28
|
drainExtractionQueue: extensions?.drainExtractionQueue,
|
|
29
|
+
watchers: extensions?.watcherRegistry,
|
|
27
30
|
};
|
|
28
31
|
|
|
29
32
|
const tools: Record<string, ReturnType<typeof tool>> = {};
|
|
@@ -31,8 +34,13 @@ export function createTools(
|
|
|
31
34
|
tools[`thatch_${def.name}`] = tool({
|
|
32
35
|
description: def.description,
|
|
33
36
|
args: def.args,
|
|
34
|
-
async execute(args) {
|
|
35
|
-
|
|
37
|
+
async execute(args, hostContext) {
|
|
38
|
+
// Trim opencode's ToolContext to the host-agnostic fields the shared
|
|
39
|
+
// definitions know about. Tests and MCP paths pass no host context.
|
|
40
|
+
const host: HostToolContext | undefined = hostContext
|
|
41
|
+
? { sessionID: hostContext.sessionID, agent: hostContext.agent }
|
|
42
|
+
: undefined;
|
|
43
|
+
return def.execute(args as Record<string, unknown>, ctx, host);
|
|
36
44
|
},
|
|
37
45
|
});
|
|
38
46
|
}
|