thoth-plugin 1.1.0 → 1.1.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/dist/index.js CHANGED
@@ -405,7 +405,92 @@ Before stating any fact about Zeus's life:
405
405
  - *If NOT FOUND*: Say "I don't have that recorded" \u2014 never guess
406
406
  </Knowledge_Management>`;
407
407
  // src/specialization/prompt-builder.ts
408
- import { existsSync as existsSync3, readFileSync as readFileSync2 } from "fs";
408
+ import { existsSync as existsSync3, readFileSync as readFileSync2, readdirSync } from "fs";
409
+ import { join as join3 } from "path";
410
+ import { homedir } from "os";
411
+ function parseSkillTriggers(content) {
412
+ const frontmatterRegex = /^---\r?\n([\s\S]*?)\r?\n---/;
413
+ const match = content.match(frontmatterRegex);
414
+ if (!match)
415
+ return null;
416
+ const yaml = match[1];
417
+ let name = "";
418
+ const triggers = [];
419
+ let inTriggers = false;
420
+ for (const line of yaml.split(`
421
+ `)) {
422
+ const trimmed = line.trim();
423
+ if (trimmed.startsWith("name:")) {
424
+ name = trimmed.slice(5).trim();
425
+ inTriggers = false;
426
+ } else if (trimmed === "triggers:") {
427
+ inTriggers = true;
428
+ } else if (inTriggers && trimmed.startsWith("- ")) {
429
+ triggers.push(trimmed.slice(2).trim());
430
+ } else if (trimmed.includes(":") && !trimmed.startsWith("-")) {
431
+ inTriggers = false;
432
+ }
433
+ }
434
+ return triggers.length > 0 ? { name, triggers } : null;
435
+ }
436
+ function discoverTriggersFromDir(skillsDir) {
437
+ if (!existsSync3(skillsDir))
438
+ return [];
439
+ const results = [];
440
+ try {
441
+ const entries = readdirSync(skillsDir, { withFileTypes: true });
442
+ for (const entry of entries) {
443
+ if (entry.name.startsWith("."))
444
+ continue;
445
+ if (!entry.isDirectory())
446
+ continue;
447
+ const skillMdPath = join3(skillsDir, entry.name, "SKILL.md");
448
+ if (!existsSync3(skillMdPath))
449
+ continue;
450
+ try {
451
+ const content = readFileSync2(skillMdPath, "utf-8");
452
+ const parsed = parseSkillTriggers(content);
453
+ if (parsed) {
454
+ results.push(parsed);
455
+ }
456
+ } catch {}
457
+ }
458
+ } catch {}
459
+ return results;
460
+ }
461
+ function buildSkillRoutingSection() {
462
+ const projectSkillsDir = join3(process.cwd(), ".opencode", "skill");
463
+ const userSkillsDir = join3(homedir(), ".opencode", "skill");
464
+ const projectTriggers = discoverTriggersFromDir(projectSkillsDir);
465
+ const userTriggers = discoverTriggersFromDir(userSkillsDir);
466
+ const seen = new Set;
467
+ const allTriggers = [];
468
+ for (const t of [...projectTriggers, ...userTriggers]) {
469
+ if (!seen.has(t.name)) {
470
+ seen.add(t.name);
471
+ allTriggers.push(t);
472
+ }
473
+ }
474
+ if (allTriggers.length === 0) {
475
+ return "";
476
+ }
477
+ const lines = [
478
+ "<Skill_Routing>",
479
+ "## Skill Routing (CHECK BEFORE RESPONDING)",
480
+ "",
481
+ "Before responding to user requests, check if their intent matches a skill trigger:",
482
+ ""
483
+ ];
484
+ for (const skill of allTriggers) {
485
+ const triggers = skill.triggers.map((t) => `"${t}"`).join(", ");
486
+ lines.push(`- ${triggers} \u2192 \`skill({ skill: "${skill.name}" })\``);
487
+ }
488
+ lines.push("");
489
+ lines.push("**Rule**: If user intent matches a trigger, invoke the skill immediately. Don't improvise workflows that already exist.");
490
+ lines.push("</Skill_Routing>");
491
+ return lines.join(`
492
+ `);
493
+ }
409
494
  var THOTH_CORE_CAPABILITIES = `<Core_Capabilities>
410
495
  ## Core Capabilities
411
496
 
@@ -539,6 +624,10 @@ function buildThothPrompt(spec) {
539
624
  sections.push(THOTH_ANTI_PATTERNS);
540
625
  sections.push(THOTH_BEHAVIORAL_GUIDANCE);
541
626
  sections.push(THOTH_KNOWLEDGE_MANAGEMENT);
627
+ const skillRouting = buildSkillRoutingSection();
628
+ if (skillRouting) {
629
+ sections.push(skillRouting);
630
+ }
542
631
  sections.push(THOTH_CORE_CAPABILITIES);
543
632
  sections.push(THOTH_EXECUTION);
544
633
  sections.push(THOTH_PERMISSIONS);
@@ -3238,7 +3327,7 @@ To proceed, mark this as Emergency/P0 or wait until work hours.`
3238
3327
  }
3239
3328
  // src/hooks/directory-agents-injector/index.ts
3240
3329
  import { existsSync as existsSync6, readFileSync as readFileSync6 } from "fs";
3241
- import { dirname as dirname4, join as join8, resolve } from "path";
3330
+ import { dirname as dirname4, join as join9, resolve } from "path";
3242
3331
 
3243
3332
  // src/hooks/directory-agents-injector/storage.ts
3244
3333
  import {
@@ -3248,18 +3337,18 @@ import {
3248
3337
  writeFileSync as writeFileSync3,
3249
3338
  unlinkSync
3250
3339
  } from "fs";
3251
- import { join as join7 } from "path";
3340
+ import { join as join8 } from "path";
3252
3341
 
3253
3342
  // src/hooks/directory-agents-injector/constants.ts
3254
- import { join as join6 } from "path";
3255
- var xdgData = process.env.XDG_DATA_HOME || join6(process.env.HOME || "", ".local", "share");
3256
- var OPENCODE_STORAGE = join6(xdgData, "opencode", "storage");
3257
- var AGENTS_INJECTOR_STORAGE = join6(OPENCODE_STORAGE, "thoth-directory-agents");
3343
+ import { join as join7 } from "path";
3344
+ var xdgData = process.env.XDG_DATA_HOME || join7(process.env.HOME || "", ".local", "share");
3345
+ var OPENCODE_STORAGE = join7(xdgData, "opencode", "storage");
3346
+ var AGENTS_INJECTOR_STORAGE = join7(OPENCODE_STORAGE, "thoth-directory-agents");
3258
3347
  var AGENTS_FILENAME2 = "AGENTS.md";
3259
3348
 
3260
3349
  // src/hooks/directory-agents-injector/storage.ts
3261
3350
  function getStoragePath(sessionID) {
3262
- return join7(AGENTS_INJECTOR_STORAGE, `${sessionID}.json`);
3351
+ return join8(AGENTS_INJECTOR_STORAGE, `${sessionID}.json`);
3263
3352
  }
3264
3353
  function loadInjectedPaths(sessionID) {
3265
3354
  const filePath = getStoragePath(sessionID);
@@ -3315,7 +3404,7 @@ function createDirectoryAgentsInjectorHook(options) {
3315
3404
  let current = startDir;
3316
3405
  const normalizedBase = knowledgeBasePath.endsWith("/") ? knowledgeBasePath.slice(0, -1) : knowledgeBasePath;
3317
3406
  while (true) {
3318
- const agentsPath = join8(current, AGENTS_FILENAME2);
3407
+ const agentsPath = join9(current, AGENTS_FILENAME2);
3319
3408
  if (existsSync6(agentsPath)) {
3320
3409
  found.push(agentsPath);
3321
3410
  }
@@ -3396,8 +3485,8 @@ ${content}`;
3396
3485
  }
3397
3486
 
3398
3487
  // src/shared-hooks/todo-continuation-enforcer.ts
3399
- import { existsSync as existsSync8, readdirSync as readdirSync2 } from "fs";
3400
- import { join as join12 } from "path";
3488
+ import { existsSync as existsSync8, readdirSync as readdirSync3 } from "fs";
3489
+ import { join as join13 } from "path";
3401
3490
 
3402
3491
  // src/shared-hooks/utils/session-state.ts
3403
3492
  var subagentSessions = new Set;
@@ -3410,16 +3499,16 @@ function getMainSessionID() {
3410
3499
  }
3411
3500
 
3412
3501
  // src/shared-hooks/utils/message-storage.ts
3413
- import { existsSync as existsSync7, mkdirSync as mkdirSync3, readFileSync as readFileSync7, readdirSync, writeFileSync as writeFileSync4 } from "fs";
3414
- import { join as join10 } from "path";
3502
+ import { existsSync as existsSync7, mkdirSync as mkdirSync3, readFileSync as readFileSync7, readdirSync as readdirSync2, writeFileSync as writeFileSync4 } from "fs";
3503
+ import { join as join11 } from "path";
3415
3504
 
3416
3505
  // src/shared-hooks/utils/constants.ts
3417
- import { join as join9 } from "path";
3418
- import { homedir as homedir2 } from "os";
3419
- var xdgData2 = process.env.XDG_DATA_HOME || join9(homedir2(), ".local", "share");
3420
- var OPENCODE_STORAGE2 = join9(xdgData2, "opencode", "storage");
3421
- var MESSAGE_STORAGE = join9(OPENCODE_STORAGE2, "message");
3422
- var PART_STORAGE = join9(OPENCODE_STORAGE2, "part");
3506
+ import { join as join10 } from "path";
3507
+ import { homedir as homedir3 } from "os";
3508
+ var xdgData2 = process.env.XDG_DATA_HOME || join10(homedir3(), ".local", "share");
3509
+ var OPENCODE_STORAGE2 = join10(xdgData2, "opencode", "storage");
3510
+ var MESSAGE_STORAGE = join10(OPENCODE_STORAGE2, "message");
3511
+ var PART_STORAGE = join10(OPENCODE_STORAGE2, "part");
3423
3512
  var THINKING_TYPES = new Set(["thinking", "redacted_thinking", "reasoning"]);
3424
3513
  var META_TYPES = new Set(["step-start", "step-finish"]);
3425
3514
  var CONTENT_TYPES = new Set(["text", "tool", "tool_use", "tool_result"]);
@@ -3427,10 +3516,10 @@ var CONTENT_TYPES = new Set(["text", "tool", "tool_use", "tool_result"]);
3427
3516
  // src/shared-hooks/utils/message-storage.ts
3428
3517
  function findNearestMessageWithFields(messageDir) {
3429
3518
  try {
3430
- const files = readdirSync(messageDir).filter((f) => f.endsWith(".json")).sort().reverse();
3519
+ const files = readdirSync2(messageDir).filter((f) => f.endsWith(".json")).sort().reverse();
3431
3520
  for (const file of files) {
3432
3521
  try {
3433
- const content = readFileSync7(join10(messageDir, file), "utf-8");
3522
+ const content = readFileSync7(join11(messageDir, file), "utf-8");
3434
3523
  const msg = JSON.parse(content);
3435
3524
  if (msg.agent && msg.model?.providerID && msg.model?.modelID) {
3436
3525
  return msg;
@@ -3471,11 +3560,11 @@ Incomplete tasks remain in your todo list. Continue working on the next pending
3471
3560
  function getMessageDir(sessionID) {
3472
3561
  if (!existsSync8(MESSAGE_STORAGE))
3473
3562
  return null;
3474
- const directPath = join12(MESSAGE_STORAGE, sessionID);
3563
+ const directPath = join13(MESSAGE_STORAGE, sessionID);
3475
3564
  if (existsSync8(directPath))
3476
3565
  return directPath;
3477
- for (const dir of readdirSync2(MESSAGE_STORAGE)) {
3478
- const sessionPath = join12(MESSAGE_STORAGE, dir, sessionID);
3566
+ for (const dir of readdirSync3(MESSAGE_STORAGE)) {
3567
+ const sessionPath = join13(MESSAGE_STORAGE, dir, sessionID);
3479
3568
  if (existsSync8(sessionPath))
3480
3569
  return sessionPath;
3481
3570
  }
@@ -3703,16 +3792,16 @@ function createTodoContinuationEnforcer(ctx) {
3703
3792
  };
3704
3793
  }
3705
3794
  // src/shared-hooks/session-recovery/storage.ts
3706
- import { existsSync as existsSync9, mkdirSync as mkdirSync4, readdirSync as readdirSync3, readFileSync as readFileSync8, unlinkSync as unlinkSync2, writeFileSync as writeFileSync5 } from "fs";
3707
- import { join as join14 } from "path";
3795
+ import { existsSync as existsSync9, mkdirSync as mkdirSync4, readdirSync as readdirSync4, readFileSync as readFileSync8, unlinkSync as unlinkSync2, writeFileSync as writeFileSync5 } from "fs";
3796
+ import { join as join15 } from "path";
3708
3797
 
3709
3798
  // src/shared-hooks/session-recovery/constants.ts
3710
- import { join as join13 } from "path";
3711
- import { homedir as homedir3 } from "os";
3712
- var xdgData3 = process.env.XDG_DATA_HOME || join13(homedir3(), ".local", "share");
3713
- var OPENCODE_STORAGE3 = join13(xdgData3, "opencode", "storage");
3714
- var MESSAGE_STORAGE2 = join13(OPENCODE_STORAGE3, "message");
3715
- var PART_STORAGE2 = join13(OPENCODE_STORAGE3, "part");
3799
+ import { join as join14 } from "path";
3800
+ import { homedir as homedir4 } from "os";
3801
+ var xdgData3 = process.env.XDG_DATA_HOME || join14(homedir4(), ".local", "share");
3802
+ var OPENCODE_STORAGE3 = join14(xdgData3, "opencode", "storage");
3803
+ var MESSAGE_STORAGE2 = join14(OPENCODE_STORAGE3, "message");
3804
+ var PART_STORAGE2 = join14(OPENCODE_STORAGE3, "part");
3716
3805
  var THINKING_TYPES2 = new Set(["thinking", "redacted_thinking", "reasoning"]);
3717
3806
  var META_TYPES2 = new Set(["step-start", "step-finish"]);
3718
3807
  var CONTENT_TYPES2 = new Set(["text", "tool", "tool_use", "tool_result"]);
@@ -3721,12 +3810,12 @@ var CONTENT_TYPES2 = new Set(["text", "tool", "tool_use", "tool_result"]);
3721
3810
  function getMessageDir2(sessionID) {
3722
3811
  if (!existsSync9(MESSAGE_STORAGE2))
3723
3812
  return "";
3724
- const directPath = join14(MESSAGE_STORAGE2, sessionID);
3813
+ const directPath = join15(MESSAGE_STORAGE2, sessionID);
3725
3814
  if (existsSync9(directPath)) {
3726
3815
  return directPath;
3727
3816
  }
3728
- for (const dir of readdirSync3(MESSAGE_STORAGE2)) {
3729
- const sessionPath = join14(MESSAGE_STORAGE2, dir, sessionID);
3817
+ for (const dir of readdirSync4(MESSAGE_STORAGE2)) {
3818
+ const sessionPath = join15(MESSAGE_STORAGE2, dir, sessionID);
3730
3819
  if (existsSync9(sessionPath)) {
3731
3820
  return sessionPath;
3732
3821
  }
@@ -3738,11 +3827,11 @@ function readMessages(sessionID) {
3738
3827
  if (!messageDir || !existsSync9(messageDir))
3739
3828
  return [];
3740
3829
  const messages = [];
3741
- for (const file of readdirSync3(messageDir)) {
3830
+ for (const file of readdirSync4(messageDir)) {
3742
3831
  if (!file.endsWith(".json"))
3743
3832
  continue;
3744
3833
  try {
3745
- const content = readFileSync8(join14(messageDir, file), "utf-8");
3834
+ const content = readFileSync8(join15(messageDir, file), "utf-8");
3746
3835
  messages.push(JSON.parse(content));
3747
3836
  } catch {
3748
3837
  continue;
@@ -3757,15 +3846,15 @@ function readMessages(sessionID) {
3757
3846
  });
3758
3847
  }
3759
3848
  function readParts(messageID) {
3760
- const partDir = join14(PART_STORAGE2, messageID);
3849
+ const partDir = join15(PART_STORAGE2, messageID);
3761
3850
  if (!existsSync9(partDir))
3762
3851
  return [];
3763
3852
  const parts = [];
3764
- for (const file of readdirSync3(partDir)) {
3853
+ for (const file of readdirSync4(partDir)) {
3765
3854
  if (!file.endsWith(".json"))
3766
3855
  continue;
3767
3856
  try {
3768
- const content = readFileSync8(join14(partDir, file), "utf-8");
3857
+ const content = readFileSync8(join15(partDir, file), "utf-8");
3769
3858
  parts.push(JSON.parse(content));
3770
3859
  } catch {
3771
3860
  continue;
@@ -3807,7 +3896,7 @@ function findMessagesWithOrphanThinking(sessionID) {
3807
3896
  return result;
3808
3897
  }
3809
3898
  function prependThinkingPart(sessionID, messageID) {
3810
- const partDir = join14(PART_STORAGE2, messageID);
3899
+ const partDir = join15(PART_STORAGE2, messageID);
3811
3900
  if (!existsSync9(partDir)) {
3812
3901
  mkdirSync4(partDir, { recursive: true });
3813
3902
  }
@@ -3821,22 +3910,22 @@ function prependThinkingPart(sessionID, messageID) {
3821
3910
  synthetic: true
3822
3911
  };
3823
3912
  try {
3824
- writeFileSync5(join14(partDir, `${partId}.json`), JSON.stringify(part, null, 2));
3913
+ writeFileSync5(join15(partDir, `${partId}.json`), JSON.stringify(part, null, 2));
3825
3914
  return true;
3826
3915
  } catch {
3827
3916
  return false;
3828
3917
  }
3829
3918
  }
3830
3919
  function stripThinkingParts(messageID) {
3831
- const partDir = join14(PART_STORAGE2, messageID);
3920
+ const partDir = join15(PART_STORAGE2, messageID);
3832
3921
  if (!existsSync9(partDir))
3833
3922
  return false;
3834
3923
  let anyRemoved = false;
3835
- for (const file of readdirSync3(partDir)) {
3924
+ for (const file of readdirSync4(partDir)) {
3836
3925
  if (!file.endsWith(".json"))
3837
3926
  continue;
3838
3927
  try {
3839
- const filePath = join14(partDir, file);
3928
+ const filePath = join15(partDir, file);
3840
3929
  const content = readFileSync8(filePath, "utf-8");
3841
3930
  const part = JSON.parse(content);
3842
3931
  if (THINKING_TYPES2.has(part.type)) {
@@ -4163,16 +4252,16 @@ ${CONTEXT_REMINDER}
4163
4252
  };
4164
4253
  }
4165
4254
  // src/shared-hooks/background-agent/manager.ts
4166
- import { existsSync as existsSync10, readdirSync as readdirSync4 } from "fs";
4167
- import { join as join15 } from "path";
4255
+ import { existsSync as existsSync10, readdirSync as readdirSync5 } from "fs";
4256
+ import { join as join16 } from "path";
4168
4257
  function getMessageDir3(sessionID) {
4169
4258
  if (!existsSync10(MESSAGE_STORAGE))
4170
4259
  return null;
4171
- const directPath = join15(MESSAGE_STORAGE, sessionID);
4260
+ const directPath = join16(MESSAGE_STORAGE, sessionID);
4172
4261
  if (existsSync10(directPath))
4173
4262
  return directPath;
4174
- for (const dir of readdirSync4(MESSAGE_STORAGE)) {
4175
- const sessionPath = join15(MESSAGE_STORAGE, dir, sessionID);
4263
+ for (const dir of readdirSync5(MESSAGE_STORAGE)) {
4264
+ const sessionPath = join16(MESSAGE_STORAGE, dir, sessionID);
4176
4265
  if (existsSync10(sessionPath))
4177
4266
  return sessionPath;
4178
4267
  }
@@ -17115,13 +17204,13 @@ Status: ${task.status}`;
17115
17204
  });
17116
17205
  }
17117
17206
  // src/tools/skill/tools.ts
17118
- import { existsSync as existsSync11, readdirSync as readdirSync5, readFileSync as readFileSync9, lstatSync, readlinkSync } from "fs";
17119
- import { homedir as homedir4 } from "os";
17120
- import { join as join16, basename as basename2, resolve as resolve2, dirname as dirname5 } from "path";
17207
+ import { existsSync as existsSync11, readdirSync as readdirSync6, readFileSync as readFileSync9, lstatSync, readlinkSync } from "fs";
17208
+ import { homedir as homedir5 } from "os";
17209
+ import { join as join17, basename as basename2, resolve as resolve2, dirname as dirname5 } from "path";
17121
17210
  import { fileURLToPath as fileURLToPath2 } from "url";
17122
17211
  var __filename3 = fileURLToPath2(import.meta.url);
17123
17212
  var __dirname3 = dirname5(__filename3);
17124
- var NPM_DEFAULTS_SKILL_DIR = join16(__dirname3, "..", "..", "defaults", "skill");
17213
+ var NPM_DEFAULTS_SKILL_DIR = join17(__dirname3, "..", "..", "defaults", "skill");
17125
17214
  function deduplicateSkills(skills) {
17126
17215
  const seen = new Set;
17127
17216
  return skills.filter((skill) => {
@@ -17194,6 +17283,7 @@ function parseSkillFrontmatter(data) {
17194
17283
  description: typeof data.description === "string" ? data.description : "",
17195
17284
  license: typeof data.license === "string" ? data.license : undefined,
17196
17285
  "allowed-tools": Array.isArray(data["allowed-tools"]) ? data["allowed-tools"] : undefined,
17286
+ triggers: Array.isArray(data.triggers) ? data.triggers : undefined,
17197
17287
  metadata: typeof data.metadata === "object" && data.metadata !== null ? data.metadata : undefined
17198
17288
  };
17199
17289
  }
@@ -17201,15 +17291,15 @@ function discoverSkillsFromDir(skillsDir, scope) {
17201
17291
  if (!existsSync11(skillsDir)) {
17202
17292
  return [];
17203
17293
  }
17204
- const entries = readdirSync5(skillsDir, { withFileTypes: true });
17294
+ const entries = readdirSync6(skillsDir, { withFileTypes: true });
17205
17295
  const skills = [];
17206
17296
  for (const entry of entries) {
17207
17297
  if (entry.name.startsWith("."))
17208
17298
  continue;
17209
- const skillPath = join16(skillsDir, entry.name);
17299
+ const skillPath = join17(skillsDir, entry.name);
17210
17300
  if (entry.isDirectory() || entry.isSymbolicLink()) {
17211
17301
  const resolvedPath = resolveSymlink(skillPath);
17212
- const skillMdPath = join16(resolvedPath, "SKILL.md");
17302
+ const skillMdPath = join17(resolvedPath, "SKILL.md");
17213
17303
  if (!existsSync11(skillMdPath))
17214
17304
  continue;
17215
17305
  try {
@@ -17218,7 +17308,8 @@ function discoverSkillsFromDir(skillsDir, scope) {
17218
17308
  skills.push({
17219
17309
  name: data.name || entry.name,
17220
17310
  description: data.description || "",
17221
- scope
17311
+ scope,
17312
+ triggers: Array.isArray(data.triggers) ? data.triggers : undefined
17222
17313
  });
17223
17314
  } catch {
17224
17315
  continue;
@@ -17228,16 +17319,35 @@ function discoverSkillsFromDir(skillsDir, scope) {
17228
17319
  return skills;
17229
17320
  }
17230
17321
  function discoverSkillsSync() {
17231
- const userSkillsDir = join16(homedir4(), ".opencode", "skill");
17232
- const projectSkillsDir = join16(process.cwd(), ".opencode", "skill");
17322
+ const userSkillsDir = join17(homedir5(), ".opencode", "skill");
17323
+ const projectSkillsDir = join17(process.cwd(), ".opencode", "skill");
17233
17324
  const builtinSkills = discoverSkillsFromDir(NPM_DEFAULTS_SKILL_DIR, "builtin");
17234
17325
  const userSkills = discoverSkillsFromDir(userSkillsDir, "user");
17235
17326
  const projectSkills = discoverSkillsFromDir(projectSkillsDir, "project");
17236
17327
  return deduplicateSkills([...projectSkills, ...userSkills, ...builtinSkills]);
17237
17328
  }
17329
+ function buildTriggerSection(skills) {
17330
+ const skillsWithTriggers = skills.filter((s) => s.triggers && s.triggers.length > 0);
17331
+ if (skillsWithTriggers.length === 0) {
17332
+ return "";
17333
+ }
17334
+ const lines = [
17335
+ `
17336
+
17337
+ Skill Triggers (invoke skill when user says these phrases):`
17338
+ ];
17339
+ for (const skill of skillsWithTriggers) {
17340
+ const triggers = skill.triggers.map((t) => `"${t}"`).join(", ");
17341
+ lines.push(`- ${triggers} \u2192 ${skill.name}`);
17342
+ }
17343
+ lines.push(`
17344
+ IMPORTANT: If user intent matches a trigger phrase, invoke the skill immediately. Don't improvise.`);
17345
+ return lines.join(`
17346
+ `);
17347
+ }
17238
17348
  async function parseSkillMd(skillPath) {
17239
17349
  const resolvedPath = resolveSymlink(skillPath);
17240
- const skillMdPath = join16(resolvedPath, "SKILL.md");
17350
+ const skillMdPath = join17(resolvedPath, "SKILL.md");
17241
17351
  if (!existsSync11(skillMdPath)) {
17242
17352
  return null;
17243
17353
  }
@@ -17250,14 +17360,15 @@ async function parseSkillMd(skillPath) {
17250
17360
  description: frontmatter.description,
17251
17361
  license: frontmatter.license,
17252
17362
  allowedTools: frontmatter["allowed-tools"],
17363
+ triggers: frontmatter.triggers,
17253
17364
  metadata: frontmatter.metadata
17254
17365
  };
17255
- const referencesDir = join16(resolvedPath, "references");
17256
- const scriptsDir = join16(resolvedPath, "scripts");
17257
- const assetsDir = join16(resolvedPath, "assets");
17258
- const references = existsSync11(referencesDir) ? readdirSync5(referencesDir).filter((f) => !f.startsWith(".")) : [];
17259
- const scripts = existsSync11(scriptsDir) ? readdirSync5(scriptsDir).filter((f) => !f.startsWith(".") && !f.startsWith("__")) : [];
17260
- const assets = existsSync11(assetsDir) ? readdirSync5(assetsDir).filter((f) => !f.startsWith(".")) : [];
17366
+ const referencesDir = join17(resolvedPath, "references");
17367
+ const scriptsDir = join17(resolvedPath, "scripts");
17368
+ const assetsDir = join17(resolvedPath, "assets");
17369
+ const references = existsSync11(referencesDir) ? readdirSync6(referencesDir).filter((f) => !f.startsWith(".")) : [];
17370
+ const scripts = existsSync11(scriptsDir) ? readdirSync6(scriptsDir).filter((f) => !f.startsWith(".") && !f.startsWith("__")) : [];
17371
+ const assets = existsSync11(assetsDir) ? readdirSync6(assetsDir).filter((f) => !f.startsWith(".")) : [];
17261
17372
  return {
17262
17373
  name: metadata.name,
17263
17374
  path: resolvedPath,
@@ -17276,12 +17387,12 @@ async function discoverSkillsFromDirAsync(skillsDir) {
17276
17387
  if (!existsSync11(skillsDir)) {
17277
17388
  return [];
17278
17389
  }
17279
- const entries = readdirSync5(skillsDir, { withFileTypes: true });
17390
+ const entries = readdirSync6(skillsDir, { withFileTypes: true });
17280
17391
  const skills = [];
17281
17392
  for (const entry of entries) {
17282
17393
  if (entry.name.startsWith("."))
17283
17394
  continue;
17284
- const skillPath = join16(skillsDir, entry.name);
17395
+ const skillPath = join17(skillsDir, entry.name);
17285
17396
  if (entry.isDirectory() || entry.isSymbolicLink()) {
17286
17397
  const skillInfo = await parseSkillMd(skillPath);
17287
17398
  if (skillInfo) {
@@ -17292,8 +17403,8 @@ async function discoverSkillsFromDirAsync(skillsDir) {
17292
17403
  return skills;
17293
17404
  }
17294
17405
  async function discoverSkills() {
17295
- const userSkillsDir = join16(homedir4(), ".opencode", "skill");
17296
- const projectSkillsDir = join16(process.cwd(), ".opencode", "skill");
17406
+ const userSkillsDir = join17(homedir5(), ".opencode", "skill");
17407
+ const projectSkillsDir = join17(process.cwd(), ".opencode", "skill");
17297
17408
  const builtinSkills = await discoverSkillsFromDirAsync(NPM_DEFAULTS_SKILL_DIR);
17298
17409
  const userSkills = await discoverSkillsFromDirAsync(userSkillsDir);
17299
17410
  const projectSkills = await discoverSkillsFromDirAsync(projectSkillsDir);
@@ -17323,7 +17434,7 @@ async function loadSkillWithReferences(skill, includeRefs) {
17323
17434
  const referencesLoaded = [];
17324
17435
  if (includeRefs && skill.references.length > 0) {
17325
17436
  for (const ref of skill.references) {
17326
- const refPath = join16(skill.path, "references", ref);
17437
+ const refPath = join17(skill.path, "references", ref);
17327
17438
  try {
17328
17439
  const content = readFileSync9(refPath, "utf-8");
17329
17440
  referencesLoaded.push({ path: ref, content });
@@ -17384,13 +17495,14 @@ function createSkillTool() {
17384
17495
  const availableSkills = discoverSkillsSync();
17385
17496
  const skillListForDescription = availableSkills.map((s) => `- ${s.name}: ${s.description} (${s.scope})`).join(`
17386
17497
  `);
17498
+ const triggerSection = buildTriggerSection(availableSkills);
17387
17499
  return tool({
17388
17500
  description: `Execute a skill within the main conversation.
17389
17501
 
17390
17502
  When you invoke a skill, the skill's prompt will expand and provide detailed instructions on how to complete the task.
17391
17503
 
17392
17504
  Available Skills:
17393
- ${skillListForDescription || "(No skills discovered yet)"}`,
17505
+ ${skillListForDescription || "(No skills discovered yet)"}${triggerSection}`,
17394
17506
  args: {
17395
17507
  skill: tool.schema.string().describe("The skill name or search query to find and load. Can be exact skill name (e.g., 'morning-boot') or keywords (e.g., 'morning', 'boot').")
17396
17508
  },
@@ -5,6 +5,7 @@ export declare const SkillFrontmatterSchema: z.ZodObject<{
5
5
  description: z.ZodString;
6
6
  license: z.ZodOptional<z.ZodString>;
7
7
  "allowed-tools": z.ZodOptional<z.ZodArray<z.ZodString>>;
8
+ triggers: z.ZodOptional<z.ZodArray<z.ZodString>>;
8
9
  metadata: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
9
10
  }, z.core.$strip>;
10
11
  export type SkillFrontmatter = z.infer<typeof SkillFrontmatterSchema>;
@@ -13,6 +14,7 @@ export interface SkillMetadata {
13
14
  description: string;
14
15
  license?: string;
15
16
  allowedTools?: string[];
17
+ triggers?: string[];
16
18
  metadata?: Record<string, string>;
17
19
  }
18
20
  export interface SkillInfo {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "thoth-plugin",
3
- "version": "1.1.0",
3
+ "version": "1.1.1",
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",