@oro.ad/nuxt-claude-devtools 1.0.7 → 1.1.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.
- package/README.md +149 -13
- package/dist/client/200.html +1 -1
- package/dist/client/404.html +1 -1
- package/dist/client/_nuxt/{BpkYThsl.js → BRCY8pHC.js} +1 -1
- package/dist/client/_nuxt/BVHVIm9H.js +12 -0
- package/dist/client/_nuxt/BZrcCMrf.js +1 -0
- package/dist/client/_nuxt/BbEuL4Z6.js +1 -0
- package/dist/client/_nuxt/BmjlsnUc.js +1 -0
- package/dist/client/_nuxt/D2NL8Xro.js +7 -0
- package/dist/client/_nuxt/D9qGFoJm.js +4 -0
- package/dist/client/_nuxt/DImlDIT-.js +59 -0
- package/dist/client/_nuxt/{B1H6wO_D.js → DV075BoS.js} +1 -1
- package/dist/client/_nuxt/{D-z88P1l.js → DYNukx3V.js} +1 -1
- package/dist/client/_nuxt/Dbw96V2H.js +7 -0
- package/dist/client/_nuxt/FllXIyfS.js +8 -0
- package/dist/client/_nuxt/MarkdownContent.WwTYmYZK.css +1 -0
- package/dist/client/_nuxt/TvBJGid1.js +1 -0
- package/dist/client/_nuxt/XJ4dJUK2.js +1 -0
- package/dist/client/_nuxt/builds/latest.json +1 -1
- package/dist/client/_nuxt/builds/meta/e8ae4dbb-462d-47a2-9aa2-50bed9498ab2.json +1 -0
- package/dist/client/_nuxt/e7kgpy_n.js +4 -0
- package/dist/client/_nuxt/entry.DwDQaFYc.css +1 -0
- package/dist/client/_nuxt/index.Bomb3OYy.css +1 -0
- package/dist/client/agents/index.html +1 -0
- package/dist/client/commands/index.html +1 -0
- package/dist/client/docs/index.html +1 -0
- package/dist/client/index.html +1 -1
- package/dist/client/mcp/index.html +1 -1
- package/dist/client/skills/index.html +1 -0
- package/dist/module.json +1 -1
- package/dist/runtime/server/agents-manager.d.ts +31 -0
- package/dist/runtime/server/agents-manager.js +193 -0
- package/dist/runtime/server/claude-session.d.ts +15 -0
- package/dist/runtime/server/claude-session.js +456 -5
- package/dist/runtime/server/commands-manager.d.ts +24 -0
- package/dist/runtime/server/commands-manager.js +132 -0
- package/dist/runtime/server/docs-manager.d.ts +48 -0
- package/dist/runtime/server/docs-manager.js +189 -0
- package/dist/runtime/server/history-manager.d.ts +24 -0
- package/dist/runtime/server/history-manager.js +184 -0
- package/dist/runtime/server/skills-manager.d.ts +36 -0
- package/dist/runtime/server/skills-manager.js +210 -0
- package/dist/runtime/types.d.ts +156 -0
- package/dist/runtime/types.js +0 -0
- package/package.json +16 -1
- package/dist/client/_nuxt/C2ORx7Gd.js +0 -1
- package/dist/client/_nuxt/CfGtRVGd.js +0 -4
- package/dist/client/_nuxt/DJn_CTvm.js +0 -1
- package/dist/client/_nuxt/EMyRkg8p.js +0 -1
- package/dist/client/_nuxt/PGt8fA_Y.js +0 -1
- package/dist/client/_nuxt/builds/meta/88c99fa0-10e0-4015-a61a-e0c6d7a01859.json +0 -1
- package/dist/client/_nuxt/entry.Ci1n7Rlt.css +0 -1
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
export interface DocFile {
|
|
2
|
+
path: string;
|
|
3
|
+
name: string;
|
|
4
|
+
content: string;
|
|
5
|
+
updatedAt: string;
|
|
6
|
+
}
|
|
7
|
+
export interface LlmsSource {
|
|
8
|
+
url: string;
|
|
9
|
+
domain: string;
|
|
10
|
+
title?: string;
|
|
11
|
+
description?: string;
|
|
12
|
+
addedAt: string;
|
|
13
|
+
}
|
|
14
|
+
export interface LlmsStore {
|
|
15
|
+
version: 1;
|
|
16
|
+
sources: LlmsSource[];
|
|
17
|
+
}
|
|
18
|
+
export declare class DocsManager {
|
|
19
|
+
private docsDir;
|
|
20
|
+
private llmsPath;
|
|
21
|
+
private projectPath;
|
|
22
|
+
constructor(projectPath: string);
|
|
23
|
+
getDocFiles(): DocFile[];
|
|
24
|
+
private scanDirectory;
|
|
25
|
+
getDocFile(relativePath: string): DocFile | null;
|
|
26
|
+
saveDocFile(relativePath: string, content: string): DocFile;
|
|
27
|
+
deleteDocFile(relativePath: string): boolean;
|
|
28
|
+
private get claudeMdPath();
|
|
29
|
+
getClaudeMd(): {
|
|
30
|
+
content: string;
|
|
31
|
+
exists: boolean;
|
|
32
|
+
updatedAt: string | null;
|
|
33
|
+
};
|
|
34
|
+
saveClaudeMd(content: string): {
|
|
35
|
+
content: string;
|
|
36
|
+
exists: boolean;
|
|
37
|
+
updatedAt: string;
|
|
38
|
+
};
|
|
39
|
+
private loadLlmsStore;
|
|
40
|
+
private saveLlmsStore;
|
|
41
|
+
getLlmsSources(): LlmsSource[];
|
|
42
|
+
addLlmsSource(url: string, title?: string, description?: string): LlmsSource;
|
|
43
|
+
removeLlmsSource(url: string): boolean;
|
|
44
|
+
updateLlmsSource(url: string, updates: {
|
|
45
|
+
title?: string;
|
|
46
|
+
description?: string;
|
|
47
|
+
}): LlmsSource | null;
|
|
48
|
+
}
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, unlinkSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { basename, dirname, join, relative } from "node:path";
|
|
3
|
+
import { createLogger } from "../logger.js";
|
|
4
|
+
const log = createLogger("docs", { timestamp: true });
|
|
5
|
+
export class DocsManager {
|
|
6
|
+
docsDir;
|
|
7
|
+
llmsPath;
|
|
8
|
+
projectPath;
|
|
9
|
+
constructor(projectPath) {
|
|
10
|
+
this.projectPath = projectPath;
|
|
11
|
+
this.docsDir = join(projectPath, ".claude", "docs");
|
|
12
|
+
this.llmsPath = join(projectPath, ".claude-devtools", "llms.json");
|
|
13
|
+
if (!existsSync(this.docsDir)) {
|
|
14
|
+
mkdirSync(this.docsDir, { recursive: true });
|
|
15
|
+
log("Created docs directory", { path: this.docsDir });
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
// ============ Doc Files ============
|
|
19
|
+
// Get all doc files recursively
|
|
20
|
+
getDocFiles() {
|
|
21
|
+
const files = [];
|
|
22
|
+
this.scanDirectory(this.docsDir, files);
|
|
23
|
+
return files.sort((a, b) => a.path.localeCompare(b.path));
|
|
24
|
+
}
|
|
25
|
+
scanDirectory(dir, files) {
|
|
26
|
+
if (!existsSync(dir)) return;
|
|
27
|
+
const entries = readdirSync(dir);
|
|
28
|
+
for (const entry of entries) {
|
|
29
|
+
const fullPath = join(dir, entry);
|
|
30
|
+
const stat = statSync(fullPath);
|
|
31
|
+
if (stat.isDirectory()) {
|
|
32
|
+
this.scanDirectory(fullPath, files);
|
|
33
|
+
} else if (entry.endsWith(".md")) {
|
|
34
|
+
const relativePath = relative(this.docsDir, fullPath);
|
|
35
|
+
const content = readFileSync(fullPath, "utf-8");
|
|
36
|
+
files.push({
|
|
37
|
+
path: relativePath,
|
|
38
|
+
name: basename(entry, ".md"),
|
|
39
|
+
content,
|
|
40
|
+
updatedAt: stat.mtime.toISOString()
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
// Get single doc file
|
|
46
|
+
getDocFile(relativePath) {
|
|
47
|
+
const fullPath = join(this.docsDir, relativePath);
|
|
48
|
+
if (!existsSync(fullPath)) return null;
|
|
49
|
+
const stat = statSync(fullPath);
|
|
50
|
+
const content = readFileSync(fullPath, "utf-8");
|
|
51
|
+
return {
|
|
52
|
+
path: relativePath,
|
|
53
|
+
name: basename(relativePath, ".md"),
|
|
54
|
+
content,
|
|
55
|
+
updatedAt: stat.mtime.toISOString()
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
// Create or update doc file
|
|
59
|
+
saveDocFile(relativePath, content) {
|
|
60
|
+
if (!relativePath.endsWith(".md")) {
|
|
61
|
+
relativePath += ".md";
|
|
62
|
+
}
|
|
63
|
+
const fullPath = join(this.docsDir, relativePath);
|
|
64
|
+
const dir = dirname(fullPath);
|
|
65
|
+
if (!existsSync(dir)) {
|
|
66
|
+
mkdirSync(dir, { recursive: true });
|
|
67
|
+
}
|
|
68
|
+
writeFileSync(fullPath, content, "utf-8");
|
|
69
|
+
log("Saved doc file", { path: relativePath });
|
|
70
|
+
return {
|
|
71
|
+
path: relativePath,
|
|
72
|
+
name: basename(relativePath, ".md"),
|
|
73
|
+
content,
|
|
74
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
// Delete doc file
|
|
78
|
+
deleteDocFile(relativePath) {
|
|
79
|
+
const fullPath = join(this.docsDir, relativePath);
|
|
80
|
+
if (!existsSync(fullPath)) return false;
|
|
81
|
+
unlinkSync(fullPath);
|
|
82
|
+
log("Deleted doc file", { path: relativePath });
|
|
83
|
+
return true;
|
|
84
|
+
}
|
|
85
|
+
// ============ CLAUDE.md ============
|
|
86
|
+
get claudeMdPath() {
|
|
87
|
+
return join(this.projectPath, "CLAUDE.md");
|
|
88
|
+
}
|
|
89
|
+
// Get CLAUDE.md content
|
|
90
|
+
getClaudeMd() {
|
|
91
|
+
const path = this.claudeMdPath;
|
|
92
|
+
if (!existsSync(path)) {
|
|
93
|
+
return { content: "", exists: false, updatedAt: null };
|
|
94
|
+
}
|
|
95
|
+
const stat = statSync(path);
|
|
96
|
+
const content = readFileSync(path, "utf-8");
|
|
97
|
+
return {
|
|
98
|
+
content,
|
|
99
|
+
exists: true,
|
|
100
|
+
updatedAt: stat.mtime.toISOString()
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
// Save CLAUDE.md content
|
|
104
|
+
saveClaudeMd(content) {
|
|
105
|
+
writeFileSync(this.claudeMdPath, content, "utf-8");
|
|
106
|
+
log("Saved CLAUDE.md");
|
|
107
|
+
return {
|
|
108
|
+
content,
|
|
109
|
+
exists: true,
|
|
110
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
// ============ LLMS Sources ============
|
|
114
|
+
loadLlmsStore() {
|
|
115
|
+
try {
|
|
116
|
+
if (existsSync(this.llmsPath)) {
|
|
117
|
+
const data = readFileSync(this.llmsPath, "utf-8");
|
|
118
|
+
return JSON.parse(data);
|
|
119
|
+
}
|
|
120
|
+
} catch (error) {
|
|
121
|
+
log("Failed to load llms store", { error });
|
|
122
|
+
}
|
|
123
|
+
return {
|
|
124
|
+
version: 1,
|
|
125
|
+
sources: []
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
saveLlmsStore(store) {
|
|
129
|
+
const dir = dirname(this.llmsPath);
|
|
130
|
+
if (!existsSync(dir)) {
|
|
131
|
+
mkdirSync(dir, { recursive: true });
|
|
132
|
+
}
|
|
133
|
+
writeFileSync(this.llmsPath, JSON.stringify(store, null, 2));
|
|
134
|
+
log("Saved llms store");
|
|
135
|
+
}
|
|
136
|
+
// Get all LLMS sources
|
|
137
|
+
getLlmsSources() {
|
|
138
|
+
return this.loadLlmsStore().sources;
|
|
139
|
+
}
|
|
140
|
+
// Add LLMS source
|
|
141
|
+
addLlmsSource(url, title, description) {
|
|
142
|
+
const store = this.loadLlmsStore();
|
|
143
|
+
let domain;
|
|
144
|
+
try {
|
|
145
|
+
const urlObj = new URL(url);
|
|
146
|
+
domain = urlObj.hostname;
|
|
147
|
+
} catch {
|
|
148
|
+
domain = url.replace(/^https?:\/\//, "").split("/")[0];
|
|
149
|
+
}
|
|
150
|
+
const existing = store.sources.find((s) => s.url === url);
|
|
151
|
+
if (existing) {
|
|
152
|
+
existing.title = title || existing.title;
|
|
153
|
+
existing.description = description || existing.description;
|
|
154
|
+
this.saveLlmsStore(store);
|
|
155
|
+
return existing;
|
|
156
|
+
}
|
|
157
|
+
const source = {
|
|
158
|
+
url,
|
|
159
|
+
domain,
|
|
160
|
+
title,
|
|
161
|
+
description,
|
|
162
|
+
addedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
163
|
+
};
|
|
164
|
+
store.sources.push(source);
|
|
165
|
+
this.saveLlmsStore(store);
|
|
166
|
+
log("Added LLMS source", { url, domain });
|
|
167
|
+
return source;
|
|
168
|
+
}
|
|
169
|
+
// Remove LLMS source
|
|
170
|
+
removeLlmsSource(url) {
|
|
171
|
+
const store = this.loadLlmsStore();
|
|
172
|
+
const index = store.sources.findIndex((s) => s.url === url);
|
|
173
|
+
if (index === -1) return false;
|
|
174
|
+
store.sources.splice(index, 1);
|
|
175
|
+
this.saveLlmsStore(store);
|
|
176
|
+
log("Removed LLMS source", { url });
|
|
177
|
+
return true;
|
|
178
|
+
}
|
|
179
|
+
// Update LLMS source metadata
|
|
180
|
+
updateLlmsSource(url, updates) {
|
|
181
|
+
const store = this.loadLlmsStore();
|
|
182
|
+
const source = store.sources.find((s) => s.url === url);
|
|
183
|
+
if (!source) return null;
|
|
184
|
+
if (updates.title !== void 0) source.title = updates.title;
|
|
185
|
+
if (updates.description !== void 0) source.description = updates.description;
|
|
186
|
+
this.saveLlmsStore(store);
|
|
187
|
+
return source;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { Conversation, Message } from '../types.js';
|
|
2
|
+
export declare class HistoryManager {
|
|
3
|
+
private storePath;
|
|
4
|
+
private store;
|
|
5
|
+
private projectPath;
|
|
6
|
+
constructor(projectPath: string);
|
|
7
|
+
private loadStore;
|
|
8
|
+
private saveStore;
|
|
9
|
+
private generateId;
|
|
10
|
+
createConversation(): Conversation;
|
|
11
|
+
getActiveConversation(): Conversation;
|
|
12
|
+
addMessage(message: Message): void;
|
|
13
|
+
updateLastAssistantMessage(updates: Partial<Message>): void;
|
|
14
|
+
getConversations(): Conversation[];
|
|
15
|
+
getConversation(id: string): Conversation | undefined;
|
|
16
|
+
setActiveConversation(id: string): Conversation | undefined;
|
|
17
|
+
deleteConversation(id: string): boolean;
|
|
18
|
+
resetSession(): Conversation;
|
|
19
|
+
getActiveConversationId(): string | null;
|
|
20
|
+
setClaudeSessionId(sessionId: string): void;
|
|
21
|
+
getClaudeSessionId(): string | null;
|
|
22
|
+
formatHistoryForSystemPrompt(): string | null;
|
|
23
|
+
hasHistoryForRecovery(): boolean;
|
|
24
|
+
}
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { createLogger } from "../logger.js";
|
|
4
|
+
const log = createLogger("history", { timestamp: true });
|
|
5
|
+
export class HistoryManager {
|
|
6
|
+
storePath;
|
|
7
|
+
store;
|
|
8
|
+
projectPath;
|
|
9
|
+
constructor(projectPath) {
|
|
10
|
+
this.projectPath = projectPath;
|
|
11
|
+
const historyDir = join(projectPath, ".claude-devtools");
|
|
12
|
+
this.storePath = join(historyDir, "history.json");
|
|
13
|
+
if (!existsSync(historyDir)) {
|
|
14
|
+
mkdirSync(historyDir, { recursive: true });
|
|
15
|
+
log("Created history directory", { path: historyDir });
|
|
16
|
+
}
|
|
17
|
+
this.store = this.loadStore();
|
|
18
|
+
}
|
|
19
|
+
loadStore() {
|
|
20
|
+
try {
|
|
21
|
+
if (existsSync(this.storePath)) {
|
|
22
|
+
const data = readFileSync(this.storePath, "utf-8");
|
|
23
|
+
const parsed = JSON.parse(data);
|
|
24
|
+
log("Loaded history store", { conversations: parsed.conversations.length });
|
|
25
|
+
return parsed;
|
|
26
|
+
}
|
|
27
|
+
} catch (error) {
|
|
28
|
+
log("Failed to load history store", { error });
|
|
29
|
+
}
|
|
30
|
+
return {
|
|
31
|
+
version: 1,
|
|
32
|
+
conversations: [],
|
|
33
|
+
activeConversationId: null
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
saveStore() {
|
|
37
|
+
try {
|
|
38
|
+
writeFileSync(this.storePath, JSON.stringify(this.store, null, 2));
|
|
39
|
+
log("Saved history store");
|
|
40
|
+
} catch (error) {
|
|
41
|
+
log("Failed to save history store", { error });
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
generateId() {
|
|
45
|
+
return `conv_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
|
|
46
|
+
}
|
|
47
|
+
// Create new conversation
|
|
48
|
+
createConversation() {
|
|
49
|
+
const conversation = {
|
|
50
|
+
id: this.generateId(),
|
|
51
|
+
messages: [],
|
|
52
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
53
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
54
|
+
projectPath: this.projectPath
|
|
55
|
+
};
|
|
56
|
+
this.store.conversations.unshift(conversation);
|
|
57
|
+
this.store.activeConversationId = conversation.id;
|
|
58
|
+
this.saveStore();
|
|
59
|
+
log("Created conversation", { id: conversation.id });
|
|
60
|
+
return conversation;
|
|
61
|
+
}
|
|
62
|
+
// Get active conversation or create new one
|
|
63
|
+
getActiveConversation() {
|
|
64
|
+
if (this.store.activeConversationId) {
|
|
65
|
+
const conv = this.store.conversations.find(
|
|
66
|
+
(c) => c.id === this.store.activeConversationId
|
|
67
|
+
);
|
|
68
|
+
if (conv) return conv;
|
|
69
|
+
}
|
|
70
|
+
return this.createConversation();
|
|
71
|
+
}
|
|
72
|
+
// Add message to active conversation
|
|
73
|
+
addMessage(message) {
|
|
74
|
+
const conversation = this.getActiveConversation();
|
|
75
|
+
conversation.messages.push(message);
|
|
76
|
+
conversation.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
77
|
+
if (!conversation.title && message.role === "user") {
|
|
78
|
+
conversation.title = message.content.substring(0, 50) + (message.content.length > 50 ? "..." : "");
|
|
79
|
+
}
|
|
80
|
+
this.saveStore();
|
|
81
|
+
}
|
|
82
|
+
// Update last assistant message (for streaming completion)
|
|
83
|
+
updateLastAssistantMessage(updates) {
|
|
84
|
+
const conversation = this.getActiveConversation();
|
|
85
|
+
const lastAssistant = [...conversation.messages].reverse().find((m) => m.role === "assistant");
|
|
86
|
+
if (lastAssistant) {
|
|
87
|
+
Object.assign(lastAssistant, updates);
|
|
88
|
+
conversation.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
89
|
+
this.saveStore();
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
// Get all conversations (for history list)
|
|
93
|
+
getConversations() {
|
|
94
|
+
return this.store.conversations;
|
|
95
|
+
}
|
|
96
|
+
// Get specific conversation by ID
|
|
97
|
+
getConversation(id) {
|
|
98
|
+
return this.store.conversations.find((c) => c.id === id);
|
|
99
|
+
}
|
|
100
|
+
// Set active conversation
|
|
101
|
+
setActiveConversation(id) {
|
|
102
|
+
const conversation = this.getConversation(id);
|
|
103
|
+
if (conversation) {
|
|
104
|
+
this.store.activeConversationId = id;
|
|
105
|
+
this.saveStore();
|
|
106
|
+
}
|
|
107
|
+
return conversation;
|
|
108
|
+
}
|
|
109
|
+
// Delete conversation
|
|
110
|
+
deleteConversation(id) {
|
|
111
|
+
const index = this.store.conversations.findIndex((c) => c.id === id);
|
|
112
|
+
if (index !== -1) {
|
|
113
|
+
this.store.conversations.splice(index, 1);
|
|
114
|
+
if (this.store.activeConversationId === id) {
|
|
115
|
+
this.store.activeConversationId = this.store.conversations[0]?.id || null;
|
|
116
|
+
}
|
|
117
|
+
this.saveStore();
|
|
118
|
+
return true;
|
|
119
|
+
}
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
// Reset (new conversation)
|
|
123
|
+
resetSession() {
|
|
124
|
+
this.store.activeConversationId = null;
|
|
125
|
+
return this.createConversation();
|
|
126
|
+
}
|
|
127
|
+
// Get active conversation ID
|
|
128
|
+
getActiveConversationId() {
|
|
129
|
+
return this.store.activeConversationId;
|
|
130
|
+
}
|
|
131
|
+
// Set Claude session ID for active conversation (empty string clears it)
|
|
132
|
+
setClaudeSessionId(sessionId) {
|
|
133
|
+
const conversation = this.getActiveConversation();
|
|
134
|
+
if (sessionId) {
|
|
135
|
+
conversation.claudeSessionId = sessionId;
|
|
136
|
+
} else {
|
|
137
|
+
delete conversation.claudeSessionId;
|
|
138
|
+
}
|
|
139
|
+
this.saveStore();
|
|
140
|
+
log("Set Claude session ID", { conversationId: conversation.id, sessionId: sessionId || "(cleared)" });
|
|
141
|
+
}
|
|
142
|
+
// Get Claude session ID for active conversation
|
|
143
|
+
getClaudeSessionId() {
|
|
144
|
+
const conversation = this.store.activeConversationId ? this.store.conversations.find((c) => c.id === this.store.activeConversationId) : null;
|
|
145
|
+
return conversation?.claudeSessionId || null;
|
|
146
|
+
}
|
|
147
|
+
// Format conversation history for system prompt (fallback when --resume fails)
|
|
148
|
+
formatHistoryForSystemPrompt() {
|
|
149
|
+
const conversation = this.store.activeConversationId ? this.store.conversations.find((c) => c.id === this.store.activeConversationId) : null;
|
|
150
|
+
if (!conversation || conversation.messages.length === 0) {
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
153
|
+
const lines = [
|
|
154
|
+
"=== CONVERSATION HISTORY (context recovery) ===",
|
|
155
|
+
"The following is the history of our previous conversation. Please continue from where we left off.",
|
|
156
|
+
""
|
|
157
|
+
];
|
|
158
|
+
for (const msg of conversation.messages) {
|
|
159
|
+
const role = msg.role === "user" ? "User" : "Assistant";
|
|
160
|
+
const timestamp = typeof msg.timestamp === "string" ? msg.timestamp : msg.timestamp.toISOString();
|
|
161
|
+
lines.push(`[${role}] (${timestamp})`);
|
|
162
|
+
if (msg.role === "assistant") {
|
|
163
|
+
if (msg.content) {
|
|
164
|
+
lines.push(msg.content);
|
|
165
|
+
}
|
|
166
|
+
} else {
|
|
167
|
+
lines.push(msg.content);
|
|
168
|
+
}
|
|
169
|
+
lines.push("");
|
|
170
|
+
}
|
|
171
|
+
lines.push("=== END OF HISTORY ===");
|
|
172
|
+
lines.push("");
|
|
173
|
+
log("Formatted history for system prompt", {
|
|
174
|
+
conversationId: conversation.id,
|
|
175
|
+
messageCount: conversation.messages.length
|
|
176
|
+
});
|
|
177
|
+
return lines.join("\n");
|
|
178
|
+
}
|
|
179
|
+
// Check if conversation has history that can be used for context recovery
|
|
180
|
+
hasHistoryForRecovery() {
|
|
181
|
+
const conversation = this.store.activeConversationId ? this.store.conversations.find((c) => c.id === this.store.activeConversationId) : null;
|
|
182
|
+
return !!conversation && conversation.messages.length > 0;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export interface Skill {
|
|
2
|
+
name: string;
|
|
3
|
+
description: string;
|
|
4
|
+
content: string;
|
|
5
|
+
rawContent: string;
|
|
6
|
+
argumentHint?: string;
|
|
7
|
+
disableModelInvocation?: boolean;
|
|
8
|
+
userInvocable?: boolean;
|
|
9
|
+
allowedTools?: string[];
|
|
10
|
+
model?: string;
|
|
11
|
+
context?: 'fork';
|
|
12
|
+
agent?: string;
|
|
13
|
+
updatedAt: string;
|
|
14
|
+
}
|
|
15
|
+
export declare class SkillsManager {
|
|
16
|
+
private skillsDir;
|
|
17
|
+
constructor(projectPath: string);
|
|
18
|
+
private parseFrontmatter;
|
|
19
|
+
private buildFrontmatter;
|
|
20
|
+
getSkills(): Skill[];
|
|
21
|
+
getSkill(name: string): Skill | null;
|
|
22
|
+
getSkillNames(): string[];
|
|
23
|
+
saveSkill(skill: {
|
|
24
|
+
name: string;
|
|
25
|
+
description: string;
|
|
26
|
+
content: string;
|
|
27
|
+
argumentHint?: string;
|
|
28
|
+
disableModelInvocation?: boolean;
|
|
29
|
+
userInvocable?: boolean;
|
|
30
|
+
allowedTools?: string[];
|
|
31
|
+
model?: string;
|
|
32
|
+
context?: 'fork';
|
|
33
|
+
agent?: string;
|
|
34
|
+
}): Skill;
|
|
35
|
+
deleteSkill(name: string): boolean;
|
|
36
|
+
}
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { createLogger } from "../logger.js";
|
|
4
|
+
const log = createLogger("skills", { timestamp: true });
|
|
5
|
+
export class SkillsManager {
|
|
6
|
+
skillsDir;
|
|
7
|
+
constructor(projectPath) {
|
|
8
|
+
this.skillsDir = join(projectPath, ".claude", "skills");
|
|
9
|
+
if (!existsSync(this.skillsDir)) {
|
|
10
|
+
mkdirSync(this.skillsDir, { recursive: true });
|
|
11
|
+
log("Created skills directory", { path: this.skillsDir });
|
|
12
|
+
}
|
|
13
|
+
}
|
|
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 = {};
|
|
23
|
+
for (const line of yaml.split("\n")) {
|
|
24
|
+
const colonIndex = line.indexOf(":");
|
|
25
|
+
if (colonIndex === -1) continue;
|
|
26
|
+
const key = line.slice(0, colonIndex).trim();
|
|
27
|
+
const value = line.slice(colonIndex + 1).trim();
|
|
28
|
+
switch (key) {
|
|
29
|
+
case "name":
|
|
30
|
+
frontmatter.name = value;
|
|
31
|
+
break;
|
|
32
|
+
case "description":
|
|
33
|
+
frontmatter.description = value;
|
|
34
|
+
break;
|
|
35
|
+
case "argument-hint":
|
|
36
|
+
frontmatter["argument-hint"] = value;
|
|
37
|
+
break;
|
|
38
|
+
case "disable-model-invocation":
|
|
39
|
+
frontmatter["disable-model-invocation"] = value === "true";
|
|
40
|
+
break;
|
|
41
|
+
case "user-invocable":
|
|
42
|
+
frontmatter["user-invocable"] = value === "true";
|
|
43
|
+
break;
|
|
44
|
+
case "allowed-tools":
|
|
45
|
+
frontmatter["allowed-tools"] = value;
|
|
46
|
+
break;
|
|
47
|
+
case "model":
|
|
48
|
+
frontmatter.model = value;
|
|
49
|
+
break;
|
|
50
|
+
case "context":
|
|
51
|
+
frontmatter.context = value;
|
|
52
|
+
break;
|
|
53
|
+
case "agent":
|
|
54
|
+
frontmatter.agent = value;
|
|
55
|
+
break;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return { frontmatter, body: body.trim() };
|
|
59
|
+
}
|
|
60
|
+
// Build frontmatter string
|
|
61
|
+
buildFrontmatter(skill) {
|
|
62
|
+
const lines = ["---"];
|
|
63
|
+
if (skill.name) {
|
|
64
|
+
lines.push(`name: ${skill.name}`);
|
|
65
|
+
}
|
|
66
|
+
if (skill.description) {
|
|
67
|
+
lines.push(`description: ${skill.description}`);
|
|
68
|
+
}
|
|
69
|
+
if (skill.argumentHint) {
|
|
70
|
+
lines.push(`argument-hint: ${skill.argumentHint}`);
|
|
71
|
+
}
|
|
72
|
+
if (skill.disableModelInvocation !== void 0) {
|
|
73
|
+
lines.push(`disable-model-invocation: ${skill.disableModelInvocation}`);
|
|
74
|
+
}
|
|
75
|
+
if (skill.userInvocable !== void 0) {
|
|
76
|
+
lines.push(`user-invocable: ${skill.userInvocable}`);
|
|
77
|
+
}
|
|
78
|
+
if (skill.allowedTools && skill.allowedTools.length > 0) {
|
|
79
|
+
lines.push(`allowed-tools: ${skill.allowedTools.join(", ")}`);
|
|
80
|
+
}
|
|
81
|
+
if (skill.model) {
|
|
82
|
+
lines.push(`model: ${skill.model}`);
|
|
83
|
+
}
|
|
84
|
+
if (skill.context) {
|
|
85
|
+
lines.push(`context: ${skill.context}`);
|
|
86
|
+
}
|
|
87
|
+
if (skill.agent) {
|
|
88
|
+
lines.push(`agent: ${skill.agent}`);
|
|
89
|
+
}
|
|
90
|
+
lines.push("---");
|
|
91
|
+
return lines.join("\n");
|
|
92
|
+
}
|
|
93
|
+
// Get all skills
|
|
94
|
+
getSkills() {
|
|
95
|
+
const skills = [];
|
|
96
|
+
if (!existsSync(this.skillsDir)) return skills;
|
|
97
|
+
const entries = readdirSync(this.skillsDir);
|
|
98
|
+
for (const entry of entries) {
|
|
99
|
+
const skillDir = join(this.skillsDir, entry);
|
|
100
|
+
const stat = statSync(skillDir);
|
|
101
|
+
if (!stat.isDirectory()) continue;
|
|
102
|
+
const skillFile = join(skillDir, "SKILL.md");
|
|
103
|
+
if (!existsSync(skillFile)) continue;
|
|
104
|
+
const fileStat = statSync(skillFile);
|
|
105
|
+
const rawContent = readFileSync(skillFile, "utf-8");
|
|
106
|
+
const { frontmatter, body } = this.parseFrontmatter(rawContent);
|
|
107
|
+
skills.push({
|
|
108
|
+
name: frontmatter.name || entry,
|
|
109
|
+
description: frontmatter.description || "",
|
|
110
|
+
content: body,
|
|
111
|
+
rawContent,
|
|
112
|
+
argumentHint: frontmatter["argument-hint"],
|
|
113
|
+
disableModelInvocation: frontmatter["disable-model-invocation"],
|
|
114
|
+
userInvocable: frontmatter["user-invocable"],
|
|
115
|
+
allowedTools: frontmatter["allowed-tools"] ? frontmatter["allowed-tools"].split(",").map((s) => s.trim()) : void 0,
|
|
116
|
+
model: frontmatter.model,
|
|
117
|
+
context: frontmatter.context === "fork" ? "fork" : void 0,
|
|
118
|
+
agent: frontmatter.agent,
|
|
119
|
+
updatedAt: fileStat.mtime.toISOString()
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
return skills.sort((a, b) => a.name.localeCompare(b.name));
|
|
123
|
+
}
|
|
124
|
+
// Get single skill
|
|
125
|
+
getSkill(name) {
|
|
126
|
+
const skillDir = join(this.skillsDir, name);
|
|
127
|
+
const skillFile = join(skillDir, "SKILL.md");
|
|
128
|
+
if (!existsSync(skillFile)) return null;
|
|
129
|
+
const stat = statSync(skillFile);
|
|
130
|
+
const rawContent = readFileSync(skillFile, "utf-8");
|
|
131
|
+
const { frontmatter, body } = this.parseFrontmatter(rawContent);
|
|
132
|
+
return {
|
|
133
|
+
name: frontmatter.name || name,
|
|
134
|
+
description: frontmatter.description || "",
|
|
135
|
+
content: body,
|
|
136
|
+
rawContent,
|
|
137
|
+
argumentHint: frontmatter["argument-hint"],
|
|
138
|
+
disableModelInvocation: frontmatter["disable-model-invocation"],
|
|
139
|
+
userInvocable: frontmatter["user-invocable"],
|
|
140
|
+
allowedTools: frontmatter["allowed-tools"] ? frontmatter["allowed-tools"].split(",").map((s) => s.trim()) : void 0,
|
|
141
|
+
model: frontmatter.model,
|
|
142
|
+
context: frontmatter.context === "fork" ? "fork" : void 0,
|
|
143
|
+
agent: frontmatter.agent,
|
|
144
|
+
updatedAt: stat.mtime.toISOString()
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
// Get skill names only (for agent skills selector)
|
|
148
|
+
getSkillNames() {
|
|
149
|
+
if (!existsSync(this.skillsDir)) return [];
|
|
150
|
+
const names = [];
|
|
151
|
+
const entries = readdirSync(this.skillsDir);
|
|
152
|
+
for (const entry of entries) {
|
|
153
|
+
const skillDir = join(this.skillsDir, entry);
|
|
154
|
+
const stat = statSync(skillDir);
|
|
155
|
+
if (!stat.isDirectory()) continue;
|
|
156
|
+
const skillFile = join(skillDir, "SKILL.md");
|
|
157
|
+
if (existsSync(skillFile)) {
|
|
158
|
+
names.push(entry);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return names.sort();
|
|
162
|
+
}
|
|
163
|
+
// Create or update skill
|
|
164
|
+
saveSkill(skill) {
|
|
165
|
+
const safeName = skill.name.replace(/[^\w-]/g, "-").toLowerCase();
|
|
166
|
+
const skillDir = join(this.skillsDir, safeName);
|
|
167
|
+
const skillFile = join(skillDir, "SKILL.md");
|
|
168
|
+
if (!existsSync(skillDir)) {
|
|
169
|
+
mkdirSync(skillDir, { recursive: true });
|
|
170
|
+
}
|
|
171
|
+
const frontmatter = this.buildFrontmatter({
|
|
172
|
+
name: safeName,
|
|
173
|
+
description: skill.description,
|
|
174
|
+
argumentHint: skill.argumentHint,
|
|
175
|
+
disableModelInvocation: skill.disableModelInvocation,
|
|
176
|
+
userInvocable: skill.userInvocable,
|
|
177
|
+
allowedTools: skill.allowedTools,
|
|
178
|
+
model: skill.model,
|
|
179
|
+
context: skill.context,
|
|
180
|
+
agent: skill.agent
|
|
181
|
+
});
|
|
182
|
+
const rawContent = `${frontmatter}
|
|
183
|
+
|
|
184
|
+
${skill.content}`;
|
|
185
|
+
writeFileSync(skillFile, rawContent, "utf-8");
|
|
186
|
+
log("Saved skill", { name: safeName, path: skillFile });
|
|
187
|
+
return {
|
|
188
|
+
name: safeName,
|
|
189
|
+
description: skill.description,
|
|
190
|
+
content: skill.content,
|
|
191
|
+
rawContent,
|
|
192
|
+
argumentHint: skill.argumentHint,
|
|
193
|
+
disableModelInvocation: skill.disableModelInvocation,
|
|
194
|
+
userInvocable: skill.userInvocable,
|
|
195
|
+
allowedTools: skill.allowedTools,
|
|
196
|
+
model: skill.model,
|
|
197
|
+
context: skill.context,
|
|
198
|
+
agent: skill.agent,
|
|
199
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
// Delete skill
|
|
203
|
+
deleteSkill(name) {
|
|
204
|
+
const skillDir = join(this.skillsDir, name);
|
|
205
|
+
if (!existsSync(skillDir)) return false;
|
|
206
|
+
rmSync(skillDir, { recursive: true });
|
|
207
|
+
log("Deleted skill", { name });
|
|
208
|
+
return true;
|
|
209
|
+
}
|
|
210
|
+
}
|