thoth-plugin 1.1.2 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -13,6 +13,8 @@ declare const HooksConfigSchema: z.ZodObject<{
13
13
  "knowledge-persistence": z.ZodOptional<z.ZodBoolean>;
14
14
  "directory-agents-injector": z.ZodOptional<z.ZodBoolean>;
15
15
  "frontmatter-enforcer": z.ZodOptional<z.ZodBoolean>;
16
+ "read-confirmation": z.ZodOptional<z.ZodBoolean>;
17
+ "write-confirmation": z.ZodOptional<z.ZodBoolean>;
16
18
  "todo-continuation": z.ZodOptional<z.ZodBoolean>;
17
19
  "session-recovery": z.ZodOptional<z.ZodBoolean>;
18
20
  "context-window-monitor": z.ZodOptional<z.ZodBoolean>;
@@ -67,6 +69,8 @@ export declare const ThothPluginConfigSchema: z.ZodObject<{
67
69
  "knowledge-persistence": z.ZodOptional<z.ZodBoolean>;
68
70
  "directory-agents-injector": z.ZodOptional<z.ZodBoolean>;
69
71
  "frontmatter-enforcer": z.ZodOptional<z.ZodBoolean>;
72
+ "read-confirmation": z.ZodOptional<z.ZodBoolean>;
73
+ "write-confirmation": z.ZodOptional<z.ZodBoolean>;
70
74
  "todo-continuation": z.ZodOptional<z.ZodBoolean>;
71
75
  "session-recovery": z.ZodOptional<z.ZodBoolean>;
72
76
  "context-window-monitor": z.ZodOptional<z.ZodBoolean>;
@@ -3,3 +3,5 @@ export { createTrustLevelTrackerHook, type TrustLevelTrackerHook, type TrustLeve
3
3
  export { createContextApertureHook, type ContextApertureHook, type ContextApertureConfig, } from "./context-aperture";
4
4
  export { createTemporalAwarenessHook, type TemporalAwarenessHook, type TemporalAwarenessConfig, } from "./temporal-awareness";
5
5
  export { createFrontmatterEnforcerHook, type FrontmatterEnforcerHook, type FrontmatterEnforcerConfig, } from "./frontmatter-enforcer";
6
+ export { createReadConfirmationHook, type ReadConfirmationHook, type ReadConfirmationConfig, } from "./read-confirmation";
7
+ export { createWriteConfirmationHook, type WriteConfirmationHook, type WriteConfirmationConfig, } from "./write-confirmation";
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Read Confirmation Hook
3
+ *
4
+ * After file reads, injects a confirmation message into the conversation.
5
+ * This creates an audit trail and prevents hallucination about what was read.
6
+ *
7
+ * Value:
8
+ * - Prevents "I read file X" when it wasn't actually read
9
+ * - Creates audit trail of file access
10
+ * - Helps with context management
11
+ */
12
+ export interface ReadConfirmationConfig {
13
+ knowledgeBasePath: string;
14
+ enabled?: boolean;
15
+ /** Only confirm reads within the knowledge base (default: true) */
16
+ kbOnly?: boolean;
17
+ }
18
+ export declare function createReadConfirmationHook(config: ReadConfirmationConfig): {
19
+ "tool.execute.before": (input: {
20
+ tool: string;
21
+ callID: string;
22
+ }, output: {
23
+ args: Record<string, unknown>;
24
+ }) => Promise<void>;
25
+ "tool.execute.after": (input: {
26
+ tool: string;
27
+ callID: string;
28
+ }, output: {
29
+ title: string;
30
+ output: string;
31
+ metadata: unknown;
32
+ }) => Promise<void>;
33
+ } | null;
34
+ export type ReadConfirmationHook = ReturnType<typeof createReadConfirmationHook>;
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Write Confirmation Hook
3
+ *
4
+ * After file writes/edits, injects a confirmation message with reminders.
5
+ * This creates an audit trail and reinforces the Smart Merge protocol.
6
+ *
7
+ * Value:
8
+ * - Reminds about _index.md updates for new files
9
+ * - Creates audit trail of file modifications
10
+ * - Reinforces Smart Merge protocol
11
+ */
12
+ export interface WriteConfirmationConfig {
13
+ knowledgeBasePath: string;
14
+ enabled?: boolean;
15
+ /** Only confirm writes within the knowledge base (default: true) */
16
+ kbOnly?: boolean;
17
+ }
18
+ export declare function createWriteConfirmationHook(config: WriteConfirmationConfig): {
19
+ "tool.execute.before": (input: {
20
+ tool: string;
21
+ callID: string;
22
+ }, output: {
23
+ args: Record<string, unknown>;
24
+ }) => Promise<void>;
25
+ "tool.execute.after": (input: {
26
+ tool: string;
27
+ callID: string;
28
+ }, output: {
29
+ title: string;
30
+ output: string;
31
+ metadata: unknown;
32
+ }) => Promise<void>;
33
+ } | null;
34
+ export type WriteConfirmationHook = ReturnType<typeof createWriteConfirmationHook>;
package/dist/index.js CHANGED
@@ -3554,6 +3554,91 @@ function createFrontmatterEnforcerHook(config) {
3554
3554
  }
3555
3555
  };
3556
3556
  }
3557
+ // src/hooks/read-confirmation.ts
3558
+ import * as path6 from "path";
3559
+ function createReadConfirmationHook(config) {
3560
+ const { knowledgeBasePath, enabled = true, kbOnly = true } = config;
3561
+ if (!enabled) {
3562
+ return null;
3563
+ }
3564
+ const kbPath = expandPath(knowledgeBasePath);
3565
+ const tracker = {
3566
+ pendingReadPaths: new Map
3567
+ };
3568
+ return {
3569
+ "tool.execute.before": async (input, output) => {
3570
+ if (input.tool !== "read")
3571
+ return;
3572
+ const filePath = output.args?.filePath;
3573
+ if (filePath && input.callID) {
3574
+ tracker.pendingReadPaths.set(input.callID, filePath);
3575
+ }
3576
+ },
3577
+ "tool.execute.after": async (input, output) => {
3578
+ if (input.tool !== "read")
3579
+ return;
3580
+ const filePath = tracker.pendingReadPaths.get(input.callID);
3581
+ tracker.pendingReadPaths.delete(input.callID);
3582
+ if (!filePath)
3583
+ return;
3584
+ if (kbOnly && !filePath.startsWith(kbPath)) {
3585
+ return;
3586
+ }
3587
+ const lineCount = output.output?.split(`
3588
+ `).length || 0;
3589
+ const relativePath = filePath.startsWith(kbPath) ? filePath.slice(kbPath.length + 1) : path6.basename(filePath);
3590
+ log(`[Read confirmed: ${relativePath} (${lineCount} lines)]`);
3591
+ }
3592
+ };
3593
+ }
3594
+ // src/hooks/write-confirmation.ts
3595
+ import * as path7 from "path";
3596
+ function createWriteConfirmationHook(config) {
3597
+ const { knowledgeBasePath, enabled = true, kbOnly = true } = config;
3598
+ if (!enabled) {
3599
+ return null;
3600
+ }
3601
+ const kbPath = expandPath(knowledgeBasePath);
3602
+ const tracker = {
3603
+ pendingWritePaths: new Map
3604
+ };
3605
+ return {
3606
+ "tool.execute.before": async (input, output) => {
3607
+ if (input.tool !== "write" && input.tool !== "edit")
3608
+ return;
3609
+ const filePath = output.args?.filePath;
3610
+ if (filePath && input.callID) {
3611
+ tracker.pendingWritePaths.set(input.callID, {
3612
+ filePath,
3613
+ action: input.tool
3614
+ });
3615
+ }
3616
+ },
3617
+ "tool.execute.after": async (input, output) => {
3618
+ if (input.tool !== "write" && input.tool !== "edit")
3619
+ return;
3620
+ const pending = tracker.pendingWritePaths.get(input.callID);
3621
+ tracker.pendingWritePaths.delete(input.callID);
3622
+ if (!pending)
3623
+ return;
3624
+ const { filePath, action } = pending;
3625
+ if (kbOnly && !filePath.startsWith(kbPath)) {
3626
+ return;
3627
+ }
3628
+ const relativePath = filePath.startsWith(kbPath) ? filePath.slice(kbPath.length + 1) : path7.basename(filePath);
3629
+ const actionLabel = action === "write" ? "Created/Overwrote" : "Edited";
3630
+ const isNewFile = action === "write";
3631
+ const isMarkdownFile = filePath.endsWith(".md");
3632
+ const isIndexFile = relativePath.includes("_index.md") || relativePath.includes("registry.md");
3633
+ let message = `[${actionLabel}: ${relativePath}]`;
3634
+ if (isNewFile && isMarkdownFile && !isIndexFile) {
3635
+ message += `
3636
+ Reminder: Update _index.md if this is a new file. Check bidirectional links.`;
3637
+ }
3638
+ log(message);
3639
+ }
3640
+ };
3641
+ }
3557
3642
  // src/hooks/directory-agents-injector/index.ts
3558
3643
  import { existsSync as existsSync6, readFileSync as readFileSync6 } from "fs";
3559
3644
  import { dirname as dirname4, join as join9, resolve as resolve2 } from "path";
@@ -3678,8 +3763,8 @@ function createDirectoryAgentsInjectorHook(options) {
3678
3763
  }
3679
3764
  if (toInject.length === 0)
3680
3765
  return;
3681
- for (const { path: path6, content } of toInject) {
3682
- const relativePath = path6.replace(knowledgeBasePath, "").replace(/^\//, "");
3766
+ for (const { path: path8, content } of toInject) {
3767
+ const relativePath = path8.replace(knowledgeBasePath, "").replace(/^\//, "");
3683
3768
  output.output += `
3684
3769
 
3685
3770
  [Directory Context: ${relativePath}]
@@ -3766,8 +3851,8 @@ function findNearestMessageWithFields(messageDir) {
3766
3851
  // src/shared-hooks/utils/logger.ts
3767
3852
  import * as fs2 from "fs";
3768
3853
  import * as os2 from "os";
3769
- import * as path6 from "path";
3770
- var logFile = path6.join(os2.tmpdir(), "thoth-plugin.log");
3854
+ import * as path8 from "path";
3855
+ var logFile = path8.join(os2.tmpdir(), "thoth-plugin.log");
3771
3856
  function log2(message, data) {
3772
3857
  try {
3773
3858
  const timestamp = new Date().toISOString();
@@ -5567,10 +5652,10 @@ function mergeDefs(...defs) {
5567
5652
  function cloneDef(schema) {
5568
5653
  return mergeDefs(schema._zod.def);
5569
5654
  }
5570
- function getElementAtPath(obj, path7) {
5571
- if (!path7)
5655
+ function getElementAtPath(obj, path9) {
5656
+ if (!path9)
5572
5657
  return obj;
5573
- return path7.reduce((acc, key) => acc?.[key], obj);
5658
+ return path9.reduce((acc, key) => acc?.[key], obj);
5574
5659
  }
5575
5660
  function promiseAllObject(promisesObj) {
5576
5661
  const keys = Object.keys(promisesObj);
@@ -5929,11 +6014,11 @@ function aborted(x, startIndex = 0) {
5929
6014
  }
5930
6015
  return false;
5931
6016
  }
5932
- function prefixIssues(path7, issues) {
6017
+ function prefixIssues(path9, issues) {
5933
6018
  return issues.map((iss) => {
5934
6019
  var _a;
5935
6020
  (_a = iss).path ?? (_a.path = []);
5936
- iss.path.unshift(path7);
6021
+ iss.path.unshift(path9);
5937
6022
  return iss;
5938
6023
  });
5939
6024
  }
@@ -6101,7 +6186,7 @@ function treeifyError(error, _mapper) {
6101
6186
  return issue2.message;
6102
6187
  };
6103
6188
  const result = { errors: [] };
6104
- const processError = (error2, path7 = []) => {
6189
+ const processError = (error2, path9 = []) => {
6105
6190
  var _a, _b;
6106
6191
  for (const issue2 of error2.issues) {
6107
6192
  if (issue2.code === "invalid_union" && issue2.errors.length) {
@@ -6111,7 +6196,7 @@ function treeifyError(error, _mapper) {
6111
6196
  } else if (issue2.code === "invalid_element") {
6112
6197
  processError({ issues: issue2.issues }, issue2.path);
6113
6198
  } else {
6114
- const fullpath = [...path7, ...issue2.path];
6199
+ const fullpath = [...path9, ...issue2.path];
6115
6200
  if (fullpath.length === 0) {
6116
6201
  result.errors.push(mapper(issue2));
6117
6202
  continue;
@@ -6143,8 +6228,8 @@ function treeifyError(error, _mapper) {
6143
6228
  }
6144
6229
  function toDotPath(_path) {
6145
6230
  const segs = [];
6146
- const path7 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
6147
- for (const seg of path7) {
6231
+ const path9 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
6232
+ for (const seg of path9) {
6148
6233
  if (typeof seg === "number")
6149
6234
  segs.push(`[${seg}]`);
6150
6235
  else if (typeof seg === "symbol")
@@ -17435,7 +17520,7 @@ Status: ${task.status}`;
17435
17520
  // src/tools/skill/tools.ts
17436
17521
  import { existsSync as existsSync11, readdirSync as readdirSync6, readFileSync as readFileSync9, lstatSync, readlinkSync } from "fs";
17437
17522
  import { homedir as homedir5 } from "os";
17438
- import { join as join17, basename as basename3, resolve as resolve3, dirname as dirname5 } from "path";
17523
+ import { join as join17, basename as basename5, resolve as resolve3, dirname as dirname5 } from "path";
17439
17524
  import { fileURLToPath as fileURLToPath2 } from "url";
17440
17525
  var __filename3 = fileURLToPath2(import.meta.url);
17441
17526
  var __dirname3 = dirname5(__filename3);
@@ -17585,7 +17670,7 @@ async function parseSkillMd(skillPath) {
17585
17670
  const { data, body } = parseFrontmatter(content);
17586
17671
  const frontmatter = parseSkillFrontmatter(data);
17587
17672
  const metadata = {
17588
- name: frontmatter.name || basename3(skillPath),
17673
+ name: frontmatter.name || basename5(skillPath),
17589
17674
  description: frontmatter.description,
17590
17675
  license: frontmatter.license,
17591
17676
  allowedTools: frontmatter["allowed-tools"],
@@ -18524,10 +18609,10 @@ function mergeDefs2(...defs) {
18524
18609
  function cloneDef2(schema) {
18525
18610
  return mergeDefs2(schema._zod.def);
18526
18611
  }
18527
- function getElementAtPath2(obj, path7) {
18528
- if (!path7)
18612
+ function getElementAtPath2(obj, path9) {
18613
+ if (!path9)
18529
18614
  return obj;
18530
- return path7.reduce((acc, key) => acc?.[key], obj);
18615
+ return path9.reduce((acc, key) => acc?.[key], obj);
18531
18616
  }
18532
18617
  function promiseAllObject2(promisesObj) {
18533
18618
  const keys = Object.keys(promisesObj);
@@ -18908,11 +18993,11 @@ function aborted2(x, startIndex = 0) {
18908
18993
  }
18909
18994
  return false;
18910
18995
  }
18911
- function prefixIssues2(path7, issues) {
18996
+ function prefixIssues2(path9, issues) {
18912
18997
  return issues.map((iss) => {
18913
18998
  var _a;
18914
18999
  (_a = iss).path ?? (_a.path = []);
18915
- iss.path.unshift(path7);
19000
+ iss.path.unshift(path9);
18916
19001
  return iss;
18917
19002
  });
18918
19003
  }
@@ -19095,7 +19180,7 @@ function formatError2(error45, mapper = (issue3) => issue3.message) {
19095
19180
  }
19096
19181
  function treeifyError2(error45, mapper = (issue3) => issue3.message) {
19097
19182
  const result = { errors: [] };
19098
- const processError = (error46, path7 = []) => {
19183
+ const processError = (error46, path9 = []) => {
19099
19184
  var _a, _b;
19100
19185
  for (const issue3 of error46.issues) {
19101
19186
  if (issue3.code === "invalid_union" && issue3.errors.length) {
@@ -19105,7 +19190,7 @@ function treeifyError2(error45, mapper = (issue3) => issue3.message) {
19105
19190
  } else if (issue3.code === "invalid_element") {
19106
19191
  processError({ issues: issue3.issues }, issue3.path);
19107
19192
  } else {
19108
- const fullpath = [...path7, ...issue3.path];
19193
+ const fullpath = [...path9, ...issue3.path];
19109
19194
  if (fullpath.length === 0) {
19110
19195
  result.errors.push(mapper(issue3));
19111
19196
  continue;
@@ -19137,8 +19222,8 @@ function treeifyError2(error45, mapper = (issue3) => issue3.message) {
19137
19222
  }
19138
19223
  function toDotPath2(_path) {
19139
19224
  const segs = [];
19140
- const path7 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
19141
- for (const seg of path7) {
19225
+ const path9 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
19226
+ for (const seg of path9) {
19142
19227
  if (typeof seg === "number")
19143
19228
  segs.push(`[${seg}]`);
19144
19229
  else if (typeof seg === "symbol")
@@ -30885,13 +30970,13 @@ function resolveRef(ref, ctx) {
30885
30970
  if (!ref.startsWith("#")) {
30886
30971
  throw new Error("External $ref is not supported, only local refs (#/...) are allowed");
30887
30972
  }
30888
- const path7 = ref.slice(1).split("/").filter(Boolean);
30889
- if (path7.length === 0) {
30973
+ const path9 = ref.slice(1).split("/").filter(Boolean);
30974
+ if (path9.length === 0) {
30890
30975
  return ctx.rootSchema;
30891
30976
  }
30892
30977
  const defsKey = ctx.version === "draft-2020-12" ? "$defs" : "definitions";
30893
- if (path7[0] === defsKey) {
30894
- const key = path7[1];
30978
+ if (path9[0] === defsKey) {
30979
+ const key = path9[1];
30895
30980
  if (!key || !ctx.defs[key]) {
30896
30981
  throw new Error(`Reference not found: ${ref}`);
30897
30982
  }
@@ -31306,6 +31391,8 @@ var HooksConfigSchema = exports_external2.object({
31306
31391
  "knowledge-persistence": exports_external2.boolean().optional(),
31307
31392
  "directory-agents-injector": exports_external2.boolean().optional(),
31308
31393
  "frontmatter-enforcer": exports_external2.boolean().optional(),
31394
+ "read-confirmation": exports_external2.boolean().optional(),
31395
+ "write-confirmation": exports_external2.boolean().optional(),
31309
31396
  "todo-continuation": exports_external2.boolean().optional(),
31310
31397
  "session-recovery": exports_external2.boolean().optional(),
31311
31398
  "context-window-monitor": exports_external2.boolean().optional(),
@@ -31343,7 +31430,7 @@ var ThothPluginConfigSchema = exports_external2.object({
31343
31430
  }).strict();
31344
31431
  // src/index.ts
31345
31432
  import * as fs3 from "fs";
31346
- import * as path7 from "path";
31433
+ import * as path9 from "path";
31347
31434
  var sessionSpecializations = new Map;
31348
31435
  function loadConfigFromPath(configPath) {
31349
31436
  try {
@@ -31375,8 +31462,8 @@ function mergeConfigs(base, override) {
31375
31462
  };
31376
31463
  }
31377
31464
  function loadPluginConfig(directory) {
31378
- const userConfigPath = path7.join(getUserConfigDir(), "opencode", "thoth-plugin.json");
31379
- const projectConfigPath = path7.join(directory, ".opencode", "thoth-plugin.json");
31465
+ const userConfigPath = path9.join(getUserConfigDir(), "opencode", "thoth-plugin.json");
31466
+ const projectConfigPath = path9.join(directory, ".opencode", "thoth-plugin.json");
31380
31467
  let config3 = loadConfigFromPath(userConfigPath) ?? {};
31381
31468
  const projectConfig = loadConfigFromPath(projectConfigPath);
31382
31469
  if (projectConfig) {
@@ -31390,15 +31477,15 @@ function resolveKnowledgeBasePath(config3, directory) {
31390
31477
  return expandPath(config3.knowledge_base);
31391
31478
  }
31392
31479
  const commonLocations = [
31393
- path7.join(process.env.HOME || "", "Repos", "thoth"),
31394
- path7.join(process.env.HOME || "", "repos", "thoth"),
31395
- path7.join(process.env.HOME || "", "Projects", "thoth"),
31396
- path7.join(process.env.HOME || "", "projects", "thoth"),
31397
- path7.join(process.env.HOME || "", "thoth"),
31398
- path7.join(directory, "thoth")
31480
+ path9.join(process.env.HOME || "", "Repos", "thoth"),
31481
+ path9.join(process.env.HOME || "", "repos", "thoth"),
31482
+ path9.join(process.env.HOME || "", "Projects", "thoth"),
31483
+ path9.join(process.env.HOME || "", "projects", "thoth"),
31484
+ path9.join(process.env.HOME || "", "thoth"),
31485
+ path9.join(directory, "thoth")
31399
31486
  ];
31400
31487
  for (const location of commonLocations) {
31401
- const kernelPath = path7.join(location, "kernel");
31488
+ const kernelPath = path9.join(location, "kernel");
31402
31489
  if (fs3.existsSync(kernelPath)) {
31403
31490
  log(`Found knowledge base at: ${location}`);
31404
31491
  return location;
@@ -31420,6 +31507,8 @@ var ThothPlugin = async (ctx) => {
31420
31507
  const contextAperture = hooksConfig["context-aperture"] !== false ? createContextApertureHook({ knowledgeBasePath }) : null;
31421
31508
  const temporalAwareness = hooksConfig["temporal-awareness"] !== false ? createTemporalAwarenessHook() : null;
31422
31509
  const frontmatterEnforcer = hooksConfig["frontmatter-enforcer"] !== false ? createFrontmatterEnforcerHook({ knowledgeBasePath }) : null;
31510
+ const readConfirmation = hooksConfig["read-confirmation"] !== false ? createReadConfirmationHook({ knowledgeBasePath }) : null;
31511
+ const writeConfirmation = hooksConfig["write-confirmation"] !== false ? createWriteConfirmationHook({ knowledgeBasePath }) : null;
31423
31512
  const todoContinuationEnforcer = hooksConfig["todo-continuation"] !== false ? createTodoContinuationEnforcer(ctx) : null;
31424
31513
  const sessionRecoveryHook = hooksConfig["session-recovery"] !== false ? createSessionRecoveryHook(ctx, { experimental: { auto_resume: true } }) : null;
31425
31514
  const contextWindowMonitor = hooksConfig["context-window-monitor"] !== false ? createContextWindowMonitorHook(ctx) : null;
@@ -31525,11 +31614,15 @@ var ThothPlugin = async (ctx) => {
31525
31614
  await contextAperture?.["tool.execute.before"]?.(input, output);
31526
31615
  await trustLevelTracker?.["tool.execute.before"]?.(input, output);
31527
31616
  await frontmatterEnforcer?.["tool.execute.before"]?.(input, output);
31617
+ await readConfirmation?.["tool.execute.before"]?.(input, output);
31618
+ await writeConfirmation?.["tool.execute.before"]?.(input, output);
31528
31619
  },
31529
31620
  "tool.execute.after": async (input, output) => {
31530
31621
  await trustLevelTracker?.["tool.execute.after"]?.(input, output);
31531
31622
  await contextAperture?.["tool.execute.after"]?.(input, output);
31532
31623
  await frontmatterEnforcer?.["tool.execute.after"]?.(input, output);
31624
+ await readConfirmation?.["tool.execute.after"]?.(input, output);
31625
+ await writeConfirmation?.["tool.execute.after"]?.(input, output);
31533
31626
  await directoryAgentsInjector?.["tool.execute.after"]?.(input, output);
31534
31627
  await contextWindowMonitor?.["tool.execute.after"]?.(input, output);
31535
31628
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "thoth-plugin",
3
- "version": "1.1.2",
3
+ "version": "1.2.0",
4
4
  "description": "Thoth - Root-level life orchestrator for OpenCode. Unified AI chief of staff combining Sisyphus execution quality, Personal-OS rhythms, and Thoth relationship model.",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",