@oro.ad/nuxt-claude-devtools 1.2.0 → 1.3.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.
Files changed (115) hide show
  1. package/README.md +105 -13
  2. package/dist/client/200.html +1 -1
  3. package/dist/client/404.html +1 -1
  4. package/dist/client/_nuxt/B8uzckkK.js +1 -0
  5. package/dist/client/_nuxt/{CPQSDiF7.js → BAb1fJOF.js} +4 -4
  6. package/dist/client/_nuxt/BB1-kxmm.js +1 -0
  7. package/dist/client/_nuxt/BMZIbUUD.js +1 -0
  8. package/dist/client/_nuxt/BSF2Vz9o.js +1 -0
  9. package/dist/client/_nuxt/BSVkH7b6.js +1 -0
  10. package/dist/client/_nuxt/{08Mb3FOO.js → BYp73eMl.js} +1 -1
  11. package/dist/client/_nuxt/B_BoWmnX.js +1 -0
  12. package/dist/client/_nuxt/BcZxFXBD.js +1 -0
  13. package/dist/client/_nuxt/BflmC3YB.js +1 -0
  14. package/dist/client/_nuxt/BnXQTjo-.js +1 -0
  15. package/dist/client/_nuxt/C--9REmc.js +1 -0
  16. package/dist/client/_nuxt/CDQtmRaX.js +1 -0
  17. package/dist/client/_nuxt/CHeJJZL9.js +1 -0
  18. package/dist/client/_nuxt/{CSlPuO5s.js → COus5Ssl.js} +1 -1
  19. package/dist/client/_nuxt/{o1jjB-UO.js → CPA0s6N9.js} +1 -1
  20. package/dist/client/_nuxt/CRkq21kc.js +1 -0
  21. package/dist/client/_nuxt/Cgba93Y9.js +1 -0
  22. package/dist/client/_nuxt/D2l4TRxW.js +1 -0
  23. package/dist/client/_nuxt/DC_XB519.js +1 -0
  24. package/dist/client/_nuxt/DEys9N1G.js +1 -0
  25. package/dist/client/_nuxt/{b4Upel01.js → DGQ4s7ae.js} +1 -1
  26. package/dist/client/_nuxt/DH8Ugy8E.js +1 -0
  27. package/dist/client/_nuxt/DSt96JPY.js +4 -0
  28. package/dist/client/_nuxt/DbJLoP3G.js +1 -0
  29. package/dist/client/_nuxt/DeGmaFBY.js +1 -0
  30. package/dist/client/_nuxt/DgfRwrFR.js +1 -0
  31. package/dist/client/_nuxt/{CLKqRoht.js → DolUcBed.js} +1 -1
  32. package/dist/client/_nuxt/M6QPYocW.js +1 -0
  33. package/dist/client/_nuxt/QumocfwJ.js +1 -0
  34. package/dist/client/_nuxt/TQi6eIO6.js +1 -0
  35. package/dist/client/_nuxt/V4UvAcd3.js +1 -0
  36. package/dist/client/_nuxt/builds/latest.json +1 -1
  37. package/dist/client/_nuxt/builds/meta/2be12f06-336a-4fdd-b982-2f6c682c14a6.json +1 -0
  38. package/dist/client/_nuxt/d8BPa19J.js +1 -0
  39. package/dist/client/_nuxt/entry.BMxUr06A.css +1 -0
  40. package/dist/client/_nuxt/qbS8UemQ.js +1 -0
  41. package/dist/client/_nuxt/wDw60tEC.js +10 -0
  42. package/dist/client/_nuxt/xEjB6ozD.js +1 -0
  43. package/dist/client/agents/index.html +1 -1
  44. package/dist/client/commands/index.html +1 -1
  45. package/dist/client/docs/index.html +1 -1
  46. package/dist/client/index.html +1 -1
  47. package/dist/client/mcp/index.html +1 -1
  48. package/dist/client/plugins/index.html +1 -0
  49. package/dist/client/settings/index.html +1 -0
  50. package/dist/client/skills/index.html +1 -1
  51. package/dist/module.d.mts +12 -1
  52. package/dist/module.json +1 -1
  53. package/dist/module.mjs +27 -5
  54. package/dist/runtime/constants.d.ts +29 -0
  55. package/dist/runtime/constants.js +5 -0
  56. package/dist/runtime/overlay/components/ChatOverlay.d.vue.ts +7 -0
  57. package/dist/runtime/overlay/components/ChatOverlay.vue +893 -0
  58. package/dist/runtime/overlay/components/ChatOverlay.vue.d.ts +7 -0
  59. package/dist/runtime/overlay/components/MarkdownContent.d.vue.ts +6 -0
  60. package/dist/runtime/overlay/components/MarkdownContent.vue +31 -0
  61. package/dist/runtime/overlay/components/MarkdownContent.vue.d.ts +6 -0
  62. package/dist/runtime/overlay/components/ToolCallBlock.d.vue.ts +8 -0
  63. package/dist/runtime/overlay/components/ToolCallBlock.vue +77 -0
  64. package/dist/runtime/overlay/components/ToolCallBlock.vue.d.ts +8 -0
  65. package/dist/runtime/overlay/plugin.client.d.ts +6 -0
  66. package/dist/runtime/overlay/plugin.client.js +29 -0
  67. package/dist/runtime/server/agents-manager.d.ts +17 -4
  68. package/dist/runtime/server/agents-manager.js +38 -109
  69. package/dist/runtime/server/base-resource-manager.d.ts +90 -0
  70. package/dist/runtime/server/base-resource-manager.js +201 -0
  71. package/dist/runtime/server/claude-session.d.ts +11 -1
  72. package/dist/runtime/server/claude-session.js +246 -27
  73. package/dist/runtime/server/commands-manager.d.ts +12 -4
  74. package/dist/runtime/server/commands-manager.js +25 -100
  75. package/dist/runtime/server/constants.d.ts +94 -0
  76. package/dist/runtime/server/constants.js +18 -0
  77. package/dist/runtime/server/docs-manager.d.ts +7 -0
  78. package/dist/runtime/server/docs-manager.js +112 -3
  79. package/dist/runtime/server/history-manager.d.ts +1 -0
  80. package/dist/runtime/server/history-manager.js +25 -3
  81. package/dist/runtime/server/plugins/socket.io.js +5 -3
  82. package/dist/runtime/server/plugins-manager.d.ts +84 -0
  83. package/dist/runtime/server/plugins-manager.js +338 -0
  84. package/dist/runtime/server/settings-manager.d.ts +17 -0
  85. package/dist/runtime/server/settings-manager.js +70 -0
  86. package/dist/runtime/server/share-manager.d.ts +24 -0
  87. package/dist/runtime/server/share-manager.js +96 -0
  88. package/dist/runtime/server/skills-manager.d.ts +18 -4
  89. package/dist/runtime/server/skills-manager.js +32 -159
  90. package/dist/runtime/shared/composables/useClaudeChat.d.ts +35 -0
  91. package/dist/runtime/shared/composables/useClaudeChat.js +264 -0
  92. package/dist/runtime/shared/composables/useShare.d.ts +42 -0
  93. package/dist/runtime/shared/composables/useShare.js +192 -0
  94. package/dist/runtime/shared/composables/useVoiceInput.d.ts +8 -0
  95. package/dist/runtime/shared/composables/useVoiceInput.js +77 -0
  96. package/dist/runtime/shared/constants.d.ts +28 -0
  97. package/dist/runtime/shared/constants.js +6 -0
  98. package/dist/runtime/shared/index.d.ts +9 -0
  99. package/dist/runtime/shared/index.js +5 -0
  100. package/dist/runtime/shared/types.d.ts +48 -0
  101. package/dist/runtime/shared/types.js +0 -0
  102. package/dist/runtime/types.d.ts +2 -0
  103. package/dist/types.d.mts +1 -1
  104. package/package.json +5 -3
  105. package/dist/client/_nuxt/BMi2eT6G.js +0 -1
  106. package/dist/client/_nuxt/BiBLVxWh.js +0 -1
  107. package/dist/client/_nuxt/BnRGpZsC.js +0 -8
  108. package/dist/client/_nuxt/C2GhPw5d.js +0 -7
  109. package/dist/client/_nuxt/D8683igF.js +0 -7
  110. package/dist/client/_nuxt/DBIw6BGF.js +0 -1
  111. package/dist/client/_nuxt/DCgjfr8H.js +0 -9
  112. package/dist/client/_nuxt/bl5iU4Kz.js +0 -1
  113. package/dist/client/_nuxt/builds/meta/35284331-5e85-46e0-9058-988fea05336c.json +0 -1
  114. package/dist/client/_nuxt/entry.BhHeZ_Nj.css +0 -1
  115. package/dist/client/_nuxt/nKfsBgPE.js +0 -4
@@ -0,0 +1,7 @@
1
+ type __VLS_Props = {
2
+ /** Custom socket URL (for tunnel) */
3
+ socketUrl?: string;
4
+ };
5
+ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
6
+ declare const _default: typeof __VLS_export;
7
+ export default _default;
@@ -0,0 +1,6 @@
1
+ type __VLS_Props = {
2
+ content: string;
3
+ };
4
+ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
5
+ declare const _default: typeof __VLS_export;
6
+ export default _default;
@@ -0,0 +1,31 @@
1
+ <script setup>
2
+ import { computed } from "vue";
3
+ import { marked } from "marked";
4
+ const props = defineProps({
5
+ content: { type: String, required: true }
6
+ });
7
+ marked.setOptions({
8
+ breaks: true,
9
+ gfm: true
10
+ });
11
+ const renderedContent = computed(() => {
12
+ if (!props.content) return "";
13
+ try {
14
+ return marked.parse(props.content);
15
+ } catch (e) {
16
+ console.error("Markdown parsing error:", e);
17
+ return props.content;
18
+ }
19
+ });
20
+ </script>
21
+
22
+ <template>
23
+ <div
24
+ class="markdown-content"
25
+ v-html="renderedContent"
26
+ />
27
+ </template>
28
+
29
+ <style>
30
+ .markdown-content{font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,sans-serif;font-size:14px;line-height:1.625;word-break:break-word}.markdown-content h1{font-size:1.25rem;font-weight:700;margin-bottom:.5rem;margin-top:1rem}.markdown-content h2{font-size:1.125rem;font-weight:700;margin-bottom:.5rem;margin-top:.75rem}.markdown-content h3{font-size:1rem;font-weight:600;margin-bottom:.25rem;margin-top:.5rem}.markdown-content p{margin:.5rem 0}.markdown-content ol,.markdown-content ul{margin:.5rem 0 .5rem 1rem}.markdown-content ul{list-style-type:disc}.markdown-content ol{list-style-type:decimal}.markdown-content li{margin:.25rem 0}.markdown-content code{background:hsla(0,0%,100%,.1);border-radius:4px;font-family:SF Mono,Monaco,Cascadia Code,Consolas,monospace;font-size:.75rem;padding:.125rem .375rem}.markdown-content pre{background:rgba(0,0,0,.3);border-radius:8px;margin:.5rem 0;overflow-x:auto;padding:.75rem}.markdown-content pre code{background:transparent;font-size:.8rem;line-height:1.5;padding:0}.markdown-content blockquote{border-left:4px solid hsla(0,0%,100%,.2);color:var(--claude-text-muted,#a0a0a0);font-style:italic;margin:.5rem 0;padding-left:1rem}.markdown-content a{color:#3b82f6;text-decoration:none}.markdown-content a:hover{text-decoration:underline}.markdown-content table{border-collapse:collapse;font-size:.875rem;margin:.5rem 0;width:100%}.markdown-content td,.markdown-content th{border:1px solid hsla(0,0%,100%,.15);padding:.5rem .75rem;text-align:left}.markdown-content th{background:hsla(0,0%,100%,.05);font-weight:600}.markdown-content tr:nth-child(2n){background:hsla(0,0%,100%,.02)}.markdown-content hr{border:none;border-top:1px solid hsla(0,0%,100%,.15);margin:1rem 0}.markdown-content img{border-radius:8px;height:auto;max-width:100%}.markdown-content>:first-child{margin-top:0}.markdown-content>:last-child{margin-bottom:0}
31
+ </style>
@@ -0,0 +1,6 @@
1
+ type __VLS_Props = {
2
+ content: string;
3
+ };
4
+ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
5
+ declare const _default: typeof __VLS_export;
6
+ export default _default;
@@ -0,0 +1,8 @@
1
+ import type { ContentBlock } from '../../shared/types.js';
2
+ type __VLS_Props = {
3
+ block: ContentBlock;
4
+ result?: ContentBlock;
5
+ };
6
+ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
7
+ declare const _default: typeof __VLS_export;
8
+ export default _default;
@@ -0,0 +1,77 @@
1
+ <script setup>
2
+ import { computed, ref } from "vue";
3
+ const props = defineProps({
4
+ block: { type: Object, required: true },
5
+ result: { type: Object, required: false }
6
+ });
7
+ const isExpanded = ref(false);
8
+ const statusIcon = computed(() => {
9
+ if (!props.result) return "\u23F3";
10
+ if (props.result.is_error) return "\u274C";
11
+ return "\u2713";
12
+ });
13
+ const statusColor = computed(() => {
14
+ if (!props.result) return "var(--claude-text-muted)";
15
+ if (props.result.is_error) return "#ef4444";
16
+ return "#22c55e";
17
+ });
18
+ const resultPreview = computed(() => {
19
+ if (!props.result?.content) return "";
20
+ const content = typeof props.result.content === "string" ? props.result.content : JSON.stringify(props.result.content, null, 2);
21
+ if (content.length > 200) {
22
+ return content.slice(0, 200) + "...";
23
+ }
24
+ return content;
25
+ });
26
+ </script>
27
+
28
+ <template>
29
+ <div class="claude-tool-block">
30
+ <button
31
+ class="claude-tool-header"
32
+ @click="isExpanded = !isExpanded"
33
+ >
34
+ <span
35
+ class="claude-tool-icon"
36
+ :style="{ color: statusColor }"
37
+ >{{ statusIcon }}</span>
38
+ <span class="claude-tool-name">{{ block.name }}</span>
39
+ <span class="claude-tool-expand">{{ isExpanded ? "\u25BC" : "\u25B6" }}</span>
40
+ </button>
41
+
42
+ <div
43
+ v-if="isExpanded"
44
+ class="claude-tool-details"
45
+ >
46
+ <!-- Input -->
47
+ <div
48
+ v-if="block.input"
49
+ class="claude-tool-section"
50
+ >
51
+ <div class="claude-tool-section-title">
52
+ Input
53
+ </div>
54
+ <pre class="claude-tool-code">{{ JSON.stringify(block.input, null, 2) }}</pre>
55
+ </div>
56
+
57
+ <!-- Result -->
58
+ <div
59
+ v-if="result"
60
+ class="claude-tool-section"
61
+ >
62
+ <div class="claude-tool-section-title">
63
+ Result
64
+ <span
65
+ v-if="result.is_error"
66
+ class="claude-tool-error-badge"
67
+ >Error</span>
68
+ </div>
69
+ <pre class="claude-tool-code">{{ resultPreview }}</pre>
70
+ </div>
71
+ </div>
72
+ </div>
73
+ </template>
74
+
75
+ <style>
76
+ .claude-tool-block{background:rgba(0,0,0,.2);border-radius:6px;margin:8px 0;overflow:hidden}.claude-tool-header{align-items:center;background:transparent;border:none;color:var(--claude-text);cursor:pointer;display:flex;font-size:13px;gap:8px;padding:8px 12px;text-align:left;transition:background .15s;width:100%}.claude-tool-header:hover{background:hsla(0,0%,100%,.05)}.claude-tool-icon{font-size:12px}.claude-tool-name{color:var(--claude-primary);flex:1;font-family:monospace}.claude-tool-expand{color:var(--claude-text-muted);font-size:10px}.claude-tool-details{padding:0 12px 12px}.claude-tool-section{margin-top:8px}.claude-tool-section-title{align-items:center;color:var(--claude-text-muted);display:flex;font-size:11px;gap:8px;margin-bottom:4px}.claude-tool-error-badge{background:#ef4444;border-radius:4px;color:#fff;font-size:10px;padding:1px 6px}.claude-tool-code{background:#0d0d0d;border-radius:4px;font-family:SF Mono,Monaco,monospace;font-size:11px;margin:0;max-height:200px;overflow-x:auto;overflow-y:auto;padding:8px;white-space:pre-wrap;word-break:break-all}
77
+ </style>
@@ -0,0 +1,8 @@
1
+ import type { ContentBlock } from '../../shared/types.js';
2
+ type __VLS_Props = {
3
+ block: ContentBlock;
4
+ result?: ContentBlock;
5
+ };
6
+ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
7
+ declare const _default: typeof __VLS_export;
8
+ export default _default;
@@ -0,0 +1,6 @@
1
+ declare const _default: import("#app").Plugin<{
2
+ claudeOverlay: {};
3
+ }> & import("#app").ObjectPlugin<{
4
+ claudeOverlay: {};
5
+ }>;
6
+ export default _default;
@@ -0,0 +1,29 @@
1
+ import { createApp, h } from "vue";
2
+ import { defineNuxtPlugin, useRuntimeConfig } from "#app";
3
+ import ChatOverlay from "./components/ChatOverlay.vue";
4
+ export default defineNuxtPlugin((nuxtApp) => {
5
+ if (import.meta.server) return;
6
+ const config = useRuntimeConfig();
7
+ const mountPoint = document.createElement("div");
8
+ mountPoint.id = "claude-chat-overlay-root";
9
+ document.body.appendChild(mountPoint);
10
+ const tunnelOrigin = config.public.claudeDevtools?.tunnelOrigin;
11
+ const overlayApp = createApp({
12
+ render() {
13
+ return h(ChatOverlay, {
14
+ socketUrl: tunnelOrigin || void 0
15
+ });
16
+ }
17
+ });
18
+ overlayApp.config.globalProperties = nuxtApp.vueApp.config.globalProperties;
19
+ overlayApp.mount(mountPoint);
20
+ nuxtApp.hook("app:beforeMount", () => {
21
+ });
22
+ return {
23
+ provide: {
24
+ claudeOverlay: {
25
+ // Could expose methods to control overlay programmatically
26
+ }
27
+ }
28
+ };
29
+ });
@@ -1,3 +1,4 @@
1
+ import { MarkdownResourceManager } from './base-resource-manager.js';
1
2
  export interface Agent {
2
3
  name: string;
3
4
  description: string;
@@ -9,12 +10,23 @@ export interface Agent {
9
10
  permissionMode?: 'default' | 'acceptEdits' | 'dontAsk' | 'bypassPermissions' | 'plan';
10
11
  skills?: string[];
11
12
  updatedAt: string;
13
+ /** Source of the agent: 'project' for local .claude/, or plugin name */
14
+ source?: string;
12
15
  }
13
- export declare class AgentsManager {
14
- private agentsDir;
16
+ interface AgentFrontmatter {
17
+ name?: string;
18
+ description?: string;
19
+ tools?: string;
20
+ disallowedTools?: string;
21
+ model?: string;
22
+ permissionMode?: string;
23
+ skills?: string[];
24
+ }
25
+ export declare class AgentsManager extends MarkdownResourceManager<Agent, AgentFrontmatter> {
15
26
  constructor(projectPath: string);
16
- private parseFrontmatter;
17
- private buildFrontmatter;
27
+ protected parseYaml(yaml: string): AgentFrontmatter;
28
+ protected buildFrontmatter(agent: Partial<Agent>): string;
29
+ protected toResource(name: string, frontmatter: AgentFrontmatter, body: string, rawContent: string, updatedAt: string): Agent;
18
30
  getAgents(): Agent[];
19
31
  getAgent(name: string): Agent | null;
20
32
  saveAgent(agent: {
@@ -29,3 +41,4 @@ export declare class AgentsManager {
29
41
  }): Agent;
30
42
  deleteAgent(name: string): boolean;
31
43
  }
44
+ export {};
@@ -1,25 +1,13 @@
1
- import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, unlinkSync, writeFileSync } from "node:fs";
2
- import { basename, join } from "node:path";
3
1
  import { createLogger } from "../logger.js";
2
+ import { MarkdownResourceManager } from "./base-resource-manager.js";
3
+ import { AGENTS_SUBDIR } from "./constants.js";
4
4
  const log = createLogger("agents", { timestamp: true });
5
- export class AgentsManager {
6
- agentsDir;
5
+ export class AgentsManager extends MarkdownResourceManager {
7
6
  constructor(projectPath) {
8
- this.agentsDir = join(projectPath, ".claude", "agents");
9
- if (!existsSync(this.agentsDir)) {
10
- mkdirSync(this.agentsDir, { recursive: true });
11
- log("Created agents directory", { path: this.agentsDir });
12
- }
7
+ super(projectPath, AGENTS_SUBDIR, log);
13
8
  }
14
- // Parse YAML frontmatter from markdown content
15
- parseFrontmatter(content) {
16
- const frontmatterRegex = /^---\n([\s\S]*?)\n---\n([\s\S]*)$/;
17
- const match = content.match(frontmatterRegex);
18
- if (!match) {
19
- return { frontmatter: {}, body: content.trim() };
20
- }
21
- const [, yaml, body] = match;
22
- const frontmatter = {};
9
+ parseYaml(yaml) {
10
+ const result = {};
23
11
  let inSkillsArray = false;
24
12
  const skillsList = [];
25
13
  for (const line of yaml.split("\n")) {
@@ -40,26 +28,26 @@ export class AgentsManager {
40
28
  const value = line.slice(colonIndex + 1).trim();
41
29
  switch (key) {
42
30
  case "name":
43
- frontmatter.name = value;
31
+ result.name = value;
44
32
  break;
45
33
  case "description":
46
- frontmatter.description = value;
34
+ result.description = value;
47
35
  break;
48
36
  case "tools":
49
- frontmatter.tools = value;
37
+ result.tools = value;
50
38
  break;
51
39
  case "disallowedTools":
52
- frontmatter.disallowedTools = value;
40
+ result.disallowedTools = value;
53
41
  break;
54
42
  case "model":
55
- frontmatter.model = value;
43
+ result.model = value;
56
44
  break;
57
45
  case "permissionMode":
58
- frontmatter.permissionMode = value;
46
+ result.permissionMode = value;
59
47
  break;
60
48
  case "skills":
61
49
  if (value) {
62
- frontmatter.skills = value.split(",").map((s) => s.trim()).filter((s) => s);
50
+ result.skills = value.split(",").map((s) => s.trim()).filter((s) => s);
63
51
  } else {
64
52
  inSkillsArray = true;
65
53
  }
@@ -67,77 +55,24 @@ export class AgentsManager {
67
55
  }
68
56
  }
69
57
  if (skillsList.length > 0) {
70
- frontmatter.skills = skillsList;
58
+ result.skills = skillsList;
71
59
  }
72
- return { frontmatter, body: body.trim() };
60
+ return result;
73
61
  }
74
- // Build frontmatter string
75
62
  buildFrontmatter(agent) {
76
- const lines = ["---"];
77
- if (agent.name) {
78
- lines.push(`name: ${agent.name}`);
79
- }
80
- if (agent.description) {
81
- lines.push(`description: ${agent.description}`);
82
- }
83
- if (agent.tools && agent.tools.length > 0) {
84
- lines.push(`tools: ${agent.tools.join(", ")}`);
85
- }
86
- if (agent.disallowedTools && agent.disallowedTools.length > 0) {
87
- lines.push(`disallowedTools: ${agent.disallowedTools.join(", ")}`);
88
- }
89
- if (agent.model) {
90
- lines.push(`model: ${agent.model}`);
91
- }
92
- if (agent.permissionMode) {
93
- lines.push(`permissionMode: ${agent.permissionMode}`);
94
- }
95
- if (agent.skills && agent.skills.length > 0) {
96
- lines.push("skills:");
97
- for (const skill of agent.skills) {
98
- lines.push(` - ${skill}`);
99
- }
100
- }
101
- lines.push("---");
102
- return lines.join("\n");
63
+ return this.buildFrontmatterWithArrays([
64
+ { key: "name", value: agent.name },
65
+ { key: "description", value: agent.description },
66
+ { key: "tools", value: agent.tools },
67
+ { key: "disallowedTools", value: agent.disallowedTools },
68
+ { key: "model", value: agent.model },
69
+ { key: "permissionMode", value: agent.permissionMode },
70
+ { key: "skills", value: agent.skills, multiline: true }
71
+ ]);
103
72
  }
104
- // Get all agents
105
- getAgents() {
106
- const agents = [];
107
- if (!existsSync(this.agentsDir)) return agents;
108
- const entries = readdirSync(this.agentsDir);
109
- for (const entry of entries) {
110
- if (!entry.endsWith(".md")) continue;
111
- const fullPath = join(this.agentsDir, entry);
112
- const stat = statSync(fullPath);
113
- if (stat.isDirectory()) continue;
114
- const rawContent = readFileSync(fullPath, "utf-8");
115
- const { frontmatter, body } = this.parseFrontmatter(rawContent);
116
- agents.push({
117
- name: frontmatter.name || basename(entry, ".md"),
118
- description: frontmatter.description || "",
119
- prompt: body,
120
- rawContent,
121
- tools: frontmatter.tools ? frontmatter.tools.split(",").map((s) => s.trim()) : void 0,
122
- disallowedTools: frontmatter.disallowedTools ? frontmatter.disallowedTools.split(",").map((s) => s.trim()) : void 0,
123
- model: frontmatter.model,
124
- permissionMode: frontmatter.permissionMode,
125
- skills: frontmatter.skills,
126
- updatedAt: stat.mtime.toISOString()
127
- });
128
- }
129
- return agents.sort((a, b) => a.name.localeCompare(b.name));
130
- }
131
- // Get single agent
132
- getAgent(name) {
133
- const fileName = name.endsWith(".md") ? name : `${name}.md`;
134
- const fullPath = join(this.agentsDir, fileName);
135
- if (!existsSync(fullPath)) return null;
136
- const stat = statSync(fullPath);
137
- const rawContent = readFileSync(fullPath, "utf-8");
138
- const { frontmatter, body } = this.parseFrontmatter(rawContent);
73
+ toResource(name, frontmatter, body, rawContent, updatedAt) {
139
74
  return {
140
- name: frontmatter.name || basename(fileName, ".md"),
75
+ name: frontmatter.name || name,
141
76
  description: frontmatter.description || "",
142
77
  prompt: body,
143
78
  rawContent,
@@ -146,14 +81,18 @@ export class AgentsManager {
146
81
  model: frontmatter.model,
147
82
  permissionMode: frontmatter.permissionMode,
148
83
  skills: frontmatter.skills,
149
- updatedAt: stat.mtime.toISOString()
84
+ updatedAt
150
85
  };
151
86
  }
152
- // Create or update agent
87
+ // Public API methods
88
+ getAgents() {
89
+ return this.getAll();
90
+ }
91
+ getAgent(name) {
92
+ return this.getOne(name);
93
+ }
153
94
  saveAgent(agent) {
154
- const safeName = agent.name.replace(/[^\w-]/g, "-").toLowerCase();
155
- const fileName = `${safeName}.md`;
156
- const fullPath = join(this.agentsDir, fileName);
95
+ const safeName = this.sanitizeName(agent.name);
157
96
  const frontmatter = this.buildFrontmatter({
158
97
  name: safeName,
159
98
  description: agent.description,
@@ -163,11 +102,7 @@ export class AgentsManager {
163
102
  permissionMode: agent.permissionMode,
164
103
  skills: agent.skills
165
104
  });
166
- const rawContent = `${frontmatter}
167
-
168
- ${agent.prompt}`;
169
- writeFileSync(fullPath, rawContent, "utf-8");
170
- log("Saved agent", { name: safeName, path: fullPath });
105
+ const { rawContent, updatedAt } = this.saveResource(safeName, frontmatter, agent.prompt);
171
106
  return {
172
107
  name: safeName,
173
108
  description: agent.description,
@@ -178,16 +113,10 @@ ${agent.prompt}`;
178
113
  model: agent.model,
179
114
  permissionMode: agent.permissionMode,
180
115
  skills: agent.skills,
181
- updatedAt: (/* @__PURE__ */ new Date()).toISOString()
116
+ updatedAt
182
117
  };
183
118
  }
184
- // Delete agent
185
119
  deleteAgent(name) {
186
- const fileName = name.endsWith(".md") ? name : `${name}.md`;
187
- const fullPath = join(this.agentsDir, fileName);
188
- if (!existsSync(fullPath)) return false;
189
- unlinkSync(fullPath);
190
- log("Deleted agent", { name });
191
- return true;
120
+ return this.delete(name);
192
121
  }
193
122
  }
@@ -0,0 +1,90 @@
1
+ type Logger = (message: string, data?: unknown) => void;
2
+ /**
3
+ * Base interface for all markdown-based resources
4
+ */
5
+ export interface BaseResource {
6
+ name: string;
7
+ rawContent: string;
8
+ updatedAt: string;
9
+ }
10
+ /**
11
+ * Configuration for resource storage
12
+ */
13
+ export interface ResourceStorageConfig {
14
+ /** Use subdirectories for each resource (like skills: .claude/skills/<name>/SKILL.md) */
15
+ useSubdirectories?: boolean;
16
+ /** Filename when using subdirectories (default: SKILL.md) */
17
+ subdirectoryFilename?: string;
18
+ /** File extension (default: .md) */
19
+ extension?: string;
20
+ }
21
+ /**
22
+ * Base class for managing markdown resources with YAML frontmatter
23
+ * Handles common file operations, frontmatter parsing, and CRUD operations
24
+ */
25
+ export declare abstract class MarkdownResourceManager<TResource extends BaseResource, TFrontmatter> {
26
+ protected readonly resourceDir: string;
27
+ protected readonly log: Logger;
28
+ protected readonly config: Required<ResourceStorageConfig>;
29
+ constructor(projectPath: string, subPath: string, log: Logger, config?: ResourceStorageConfig);
30
+ /**
31
+ * Parse YAML frontmatter from markdown content
32
+ */
33
+ protected parseFrontmatter(content: string): {
34
+ frontmatter: TFrontmatter;
35
+ body: string;
36
+ };
37
+ /**
38
+ * Parse simple YAML - override for complex parsing (arrays, nested objects)
39
+ */
40
+ protected parseYaml(yaml: string): TFrontmatter;
41
+ /**
42
+ * Build YAML frontmatter string from resource data
43
+ */
44
+ protected abstract buildFrontmatter(resource: Partial<TResource>): string;
45
+ /**
46
+ * Convert frontmatter + body to resource object
47
+ */
48
+ protected abstract toResource(name: string, frontmatter: TFrontmatter, body: string, rawContent: string, updatedAt: string): TResource;
49
+ /**
50
+ * Get all resources
51
+ */
52
+ getAll(): TResource[];
53
+ /**
54
+ * Get single resource by name
55
+ */
56
+ getOne(name: string): TResource | null;
57
+ /**
58
+ * Save (create or update) a resource
59
+ */
60
+ protected saveResource(name: string, frontmatter: string, body: string): {
61
+ rawContent: string;
62
+ updatedAt: string;
63
+ };
64
+ /**
65
+ * Delete a resource
66
+ */
67
+ delete(name: string): boolean;
68
+ /**
69
+ * Sanitize name for filesystem (kebab-case)
70
+ */
71
+ protected sanitizeName(name: string): string;
72
+ /**
73
+ * Helper to build frontmatter lines
74
+ */
75
+ protected buildFrontmatterLines(fields: Array<{
76
+ key: string;
77
+ value: unknown;
78
+ }>): string;
79
+ /**
80
+ * Helper to build frontmatter with multiline arrays
81
+ */
82
+ protected buildFrontmatterWithArrays(fields: Array<{
83
+ key: string;
84
+ value: unknown;
85
+ multiline?: boolean;
86
+ }>): string;
87
+ private readFromFile;
88
+ private readFromSubdirectory;
89
+ }
90
+ export {};