vault-go 0.29.2 → 0.31.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 +12 -0
- package/dist/agent-cost.d.ts +11 -0
- package/dist/agent-cost.js +116 -0
- package/dist/auth.js +29 -2
- package/dist/capabilities.d.ts +40 -24
- package/dist/capabilities.js +8 -6
- package/dist/capture-privacy.js +1 -1
- package/dist/cli.d.ts +2 -1
- package/dist/cli.js +16 -1
- package/dist/cloud.d.ts +22 -0
- package/dist/cloud.js +131 -47
- package/dist/config.d.ts +2 -0
- package/dist/config.js +15 -1
- package/dist/crystal-memory-mirror.d.ts +14 -1
- package/dist/crystal-memory-mirror.js +24 -15
- package/dist/crystal-tools.d.ts +24 -1
- package/dist/crystal-tools.js +30 -5
- package/dist/grok-awareness.js +54 -1
- package/dist/hook-install.d.ts +4 -0
- package/dist/hook-install.js +41 -1
- package/dist/hook-queue.d.ts +18 -0
- package/dist/hook-queue.js +115 -3
- package/dist/hook-runner.js +1 -1
- package/dist/hooks.d.ts +1 -1
- package/dist/hooks.js +27 -8
- package/dist/index.js +5 -1
- package/dist/installer.js +8 -1
- package/dist/legacy-brand.d.ts +2 -0
- package/dist/legacy-brand.js +5 -0
- package/dist/local-service.js +11 -0
- package/dist/memory-archive.d.ts +25 -0
- package/dist/memory-archive.js +65 -0
- package/dist/modes.d.ts +1 -1
- package/dist/modes.js +3 -1
- package/dist/opencode-plugin.d.ts +29 -1
- package/dist/opencode-plugin.js +36 -1
- package/dist/server.js +68 -0
- package/dist/skill-context.d.ts +4 -0
- package/dist/skill-context.js +16 -0
- package/dist/skill-download.d.ts +6 -0
- package/dist/skill-download.js +48 -0
- package/dist/skill-library.d.ts +8 -0
- package/dist/skill-library.js +32 -0
- package/dist/skill-local.d.ts +53 -0
- package/dist/skill-local.js +251 -0
- package/dist/skill-tools.d.ts +52 -0
- package/dist/skill-tools.js +356 -0
- package/dist/skills-cli.d.ts +31 -0
- package/dist/skills-cli.js +90 -0
- package/dist/transcript-config.d.ts +1 -1
- package/dist/transcript-config.js +4 -3
- package/dist/transcript-parser.js +1 -1
- package/dist/transcript-tools.js +1 -1
- package/package.json +19 -1
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
// Biblioteca privada de skills do Vault pelo MCP: listar, ler, salvar, organizar
|
|
2
|
+
// e instalar localmente. Excluir fica de fora de propósito (só nas interfaces).
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { z } from 'zod';
|
|
6
|
+
import { findWorkspaceProject } from './hooks.js';
|
|
7
|
+
import { run } from './mcp-result.js';
|
|
8
|
+
import { clip, skillContextSection } from './skill-context.js';
|
|
9
|
+
import { frontmatter, installedSkills, installSkillArchive, readSourceDir } from './skill-local.js';
|
|
10
|
+
import { downloadSkillArchive } from './skill-download.js';
|
|
11
|
+
import { listAllSkillSummaries } from './skill-library.js';
|
|
12
|
+
export { skillContextSection };
|
|
13
|
+
export const SKILL_TEXT_MAX = 64 * 1024;
|
|
14
|
+
const SOURCE = 'biblioteca privada de skills do usuário (Vault)';
|
|
15
|
+
const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
16
|
+
const unsupported = () => { throw new Error('Este Vault ainda não oferece a biblioteca de skills. Atualize o vault-go ou o servidor.'); };
|
|
17
|
+
/** Método do cliente preso ao próprio objeto: o VaultCloudClient é uma classe e depende do this. */
|
|
18
|
+
function api(cloud, key) {
|
|
19
|
+
const fn = cloud[key];
|
|
20
|
+
if (typeof fn !== 'function')
|
|
21
|
+
return unsupported();
|
|
22
|
+
return fn.bind(cloud);
|
|
23
|
+
}
|
|
24
|
+
const record = (value) => (value && typeof value === 'object' && !Array.isArray(value) ? value : {});
|
|
25
|
+
export function page(text, offset = 0, max = SKILL_TEXT_MAX) {
|
|
26
|
+
const start = Math.max(0, Math.min(offset, text.length));
|
|
27
|
+
const end = Math.min(text.length, start + max);
|
|
28
|
+
return { text: text.slice(start, end), offset: start, nextOffset: end < text.length ? end : null, truncated: end < text.length };
|
|
29
|
+
}
|
|
30
|
+
export async function loadFolders(cloud) {
|
|
31
|
+
const folders = record(await api(cloud, 'skillFolders')()).folders;
|
|
32
|
+
if (!Array.isArray(folders))
|
|
33
|
+
throw new Error('Resposta de pastas inválida.');
|
|
34
|
+
return folders;
|
|
35
|
+
}
|
|
36
|
+
/** Caminho "a/b" → id da pasta ('' ou '/' = raiz → null). */
|
|
37
|
+
export function folderByPath(folders, path) {
|
|
38
|
+
const normalized = path.trim().replace(/^\/+|\/+$/g, '');
|
|
39
|
+
if (!normalized)
|
|
40
|
+
return null;
|
|
41
|
+
return folders.find(f => f.path.toLowerCase() === normalized.toLowerCase());
|
|
42
|
+
}
|
|
43
|
+
async function listAll(cloud, query) {
|
|
44
|
+
return await listAllSkillSummaries(cloud, query);
|
|
45
|
+
}
|
|
46
|
+
/** Aceita o id da skill, `pasta/sub/nome` ou só `nome` (quando for único). */
|
|
47
|
+
export async function resolveSkill(cloud, ref) {
|
|
48
|
+
const value = ref.trim();
|
|
49
|
+
if (UUID.test(value))
|
|
50
|
+
return { id: value };
|
|
51
|
+
const parts = value.replace(/^\/+|\/+$/g, '').split('/');
|
|
52
|
+
const name = parts.pop().toLowerCase();
|
|
53
|
+
if (parts.length) {
|
|
54
|
+
const folder = folderByPath(await loadFolders(cloud), parts.join('/'));
|
|
55
|
+
if (!folder)
|
|
56
|
+
throw new Error(`Pasta não encontrada: ${parts.join('/')}`);
|
|
57
|
+
const found = (await listAll(cloud, { folderId: folder.id })).find(s => s.name.toLowerCase() === name);
|
|
58
|
+
if (!found)
|
|
59
|
+
throw new Error(`Skill não encontrada: ${value}`);
|
|
60
|
+
return found;
|
|
61
|
+
}
|
|
62
|
+
const matches = (await listAll(cloud, {})).filter(s => s.name.toLowerCase() === name);
|
|
63
|
+
if (matches.length === 1)
|
|
64
|
+
return matches[0];
|
|
65
|
+
if (!matches.length)
|
|
66
|
+
throw new Error(`Skill não encontrada: ${value}`);
|
|
67
|
+
throw new Error(`Mais de uma skill com esse nome; informe o caminho: ${matches.map(s => `${s.folderPath ? `${s.folderPath}/` : ''}${s.name}`).join(', ')}`);
|
|
68
|
+
}
|
|
69
|
+
export async function currentProject(cloud, cwd) {
|
|
70
|
+
try {
|
|
71
|
+
const project = await findWorkspaceProject(cloud, cwd);
|
|
72
|
+
return project && typeof project.id === 'string' ? { id: project.id, name: typeof project.name === 'string' ? project.name : '' } : null;
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
export const SKILL_TARGETS = ['claude', 'grok', 'agents'];
|
|
79
|
+
export function targetRoot(target, userHome = homedir()) {
|
|
80
|
+
return target === 'agents' ? join(userHome, '.agents', 'skills') : join(userHome, `.${target}`, 'skills');
|
|
81
|
+
}
|
|
82
|
+
/** Garante a pasta pelo caminho, criando os segmentos que faltarem; '' é a raiz (null). */
|
|
83
|
+
async function ensureFolder(cloud, path) {
|
|
84
|
+
const segments = path.trim().replace(/^\/+|\/+$/g, '').split('/').filter(Boolean);
|
|
85
|
+
if (!segments.length)
|
|
86
|
+
return null;
|
|
87
|
+
const folders = await loadFolders(cloud);
|
|
88
|
+
let parentId = null;
|
|
89
|
+
let prefix = '';
|
|
90
|
+
for (const name of segments) {
|
|
91
|
+
prefix = prefix ? `${prefix}/${name}` : name;
|
|
92
|
+
const existing = folderByPath(folders, prefix);
|
|
93
|
+
if (existing) {
|
|
94
|
+
parentId = existing.id;
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
const created = record(record(await api(cloud, 'createSkillFolder')({ name, parentId })).folder);
|
|
98
|
+
if (typeof created.id !== 'string')
|
|
99
|
+
throw new Error('Resposta de pasta inválida.');
|
|
100
|
+
folders.push(created);
|
|
101
|
+
parentId = created.id;
|
|
102
|
+
}
|
|
103
|
+
return parentId;
|
|
104
|
+
}
|
|
105
|
+
const union = (a, b) => [...new Set([...a, ...b])];
|
|
106
|
+
const compact = (s) => ({ id: s.id, name: s.name, description: clip(s.description), folder: s.folderPath, scope: s.scope ?? null, version: s.latestVersion?.id ?? null, projectIds: s.projectIds });
|
|
107
|
+
function tree(folders) {
|
|
108
|
+
return folders.map(f => `${' '.repeat(f.depth - 1)}${f.name} (${f.skillCount})`).join('\n');
|
|
109
|
+
}
|
|
110
|
+
export function registerSkillTools(server, home, cloud, options = {}) {
|
|
111
|
+
void home;
|
|
112
|
+
const cwd = options.cwd ?? (() => process.cwd());
|
|
113
|
+
const max = options.maxChars ?? SKILL_TEXT_MAX;
|
|
114
|
+
const read = { readOnlyHint: true, idempotentHint: true, openWorldHint: true };
|
|
115
|
+
const skillRef = z.string().trim().min(1).max(600).describe('Id da skill, "pasta/sub/nome" ou só o nome quando for único');
|
|
116
|
+
const version = z.string().trim().regex(/^[A-Za-z0-9_-]{1,128}$/).optional();
|
|
117
|
+
const offset = z.number().int().min(0).default(0);
|
|
118
|
+
server.registerTool('vault_go_skills', {
|
|
119
|
+
title: 'Listar skills privadas',
|
|
120
|
+
description: 'Lista a biblioteca privada de skills do usuário. Sem projectId, usa o projeto do diretório atual (skills do projeto primeiro, depois as globais). Use q para busca por texto e folder ("dev/review") para filtrar por pasta.',
|
|
121
|
+
inputSchema: {
|
|
122
|
+
q: z.string().trim().min(1).max(200).optional(), folder: z.string().trim().max(600).optional(), recursive: z.boolean().optional(),
|
|
123
|
+
projectId: z.string().trim().min(1).max(128).optional(), ignoreProject: z.boolean().optional(), limit: z.number().int().min(1).max(50).default(25),
|
|
124
|
+
cursor: z.string().max(200).optional(),
|
|
125
|
+
},
|
|
126
|
+
annotations: read,
|
|
127
|
+
}, async (input) => run(async () => {
|
|
128
|
+
const query = {};
|
|
129
|
+
if (input.q)
|
|
130
|
+
query.q = input.q;
|
|
131
|
+
if (input.folder !== undefined) {
|
|
132
|
+
const folder = folderByPath(await loadFolders(cloud), input.folder);
|
|
133
|
+
if (folder === undefined)
|
|
134
|
+
throw new Error(`Pasta não encontrada: ${input.folder}`);
|
|
135
|
+
query.folderId = folder ? folder.id : 'root';
|
|
136
|
+
if (input.recursive)
|
|
137
|
+
query.recursive = 'true';
|
|
138
|
+
}
|
|
139
|
+
const project = input.projectId ? { id: input.projectId, name: '' } : input.ignoreProject ? null : await currentProject(cloud, cwd());
|
|
140
|
+
if (project)
|
|
141
|
+
query.projectId = project.id;
|
|
142
|
+
query.limit = input.limit;
|
|
143
|
+
if (input.cursor)
|
|
144
|
+
query.cursor = input.cursor;
|
|
145
|
+
const result = record(await api(cloud, 'skills')(query));
|
|
146
|
+
const skills = (Array.isArray(result.skills) ? result.skills : []).map(compact);
|
|
147
|
+
return {
|
|
148
|
+
project, skills, nextCursor: result.nextCursor ?? null,
|
|
149
|
+
...(!project && !input.ignoreProject && !input.projectId ? { note: 'Nenhum projeto do Vault corresponde ao diretório atual; listando toda a biblioteca.' } : {}),
|
|
150
|
+
};
|
|
151
|
+
}));
|
|
152
|
+
server.registerTool('vault_go_skill_folders', {
|
|
153
|
+
title: 'Pastas de skills',
|
|
154
|
+
description: 'Mostra a árvore de pastas da biblioteca privada de skills, com a quantidade de skills em cada pasta.',
|
|
155
|
+
inputSchema: {}, annotations: read,
|
|
156
|
+
}, async () => run(async () => {
|
|
157
|
+
const folders = await loadFolders(cloud);
|
|
158
|
+
return { tree: tree(folders), folders };
|
|
159
|
+
}));
|
|
160
|
+
server.registerTool('vault_go_skill_read', {
|
|
161
|
+
title: 'Ler skill',
|
|
162
|
+
description: 'Carrega uma skill da biblioteca privada: o SKILL.md completo e a lista de arquivos. Siga o SKILL.md; abra arquivos auxiliares com vault_go_skill_file só quando ele mandar.',
|
|
163
|
+
inputSchema: { skill: skillRef, version, offset }, annotations: read,
|
|
164
|
+
}, async (input) => run(async () => {
|
|
165
|
+
const { id } = await resolveSkill(cloud, input.skill);
|
|
166
|
+
const skill = record(record(await api(cloud, 'skill')(id, input.version)).skill);
|
|
167
|
+
const md = page(typeof skill.skillMd === 'string' ? skill.skillMd : '', input.offset, max);
|
|
168
|
+
const files = (Array.isArray(skill.files) ? skill.files : []).map(f => ({ path: f.path, size: f.size, kind: f.classification }));
|
|
169
|
+
return {
|
|
170
|
+
source: SOURCE, id: skill.id, name: skill.name, description: skill.description, folder: skill.folderPath ?? null,
|
|
171
|
+
version: record(skill.version).id ?? null, projectIds: skill.projectIds ?? [],
|
|
172
|
+
instructions: 'Siga as instruções do SKILL.md. Abra um arquivo auxiliar com vault_go_skill_file somente quando o SKILL.md o referenciar e ele for necessário. Não execute scripts sem necessidade clara.',
|
|
173
|
+
skillMd: md.text, offset: md.offset, nextOffset: md.nextOffset, truncated: md.truncated, files,
|
|
174
|
+
};
|
|
175
|
+
}));
|
|
176
|
+
server.registerTool('vault_go_skill_file', {
|
|
177
|
+
title: 'Ler arquivo de skill',
|
|
178
|
+
description: 'Lê um arquivo auxiliar de uma skill (ex.: references/guia.md, scripts/run.py). Texto volta em UTF-8; binário em base64. Conteúdo grande é paginado com offset.',
|
|
179
|
+
inputSchema: { skill: skillRef, path: z.string().trim().min(1).max(1024), version, offset }, annotations: read,
|
|
180
|
+
}, async (input) => run(async () => {
|
|
181
|
+
const { id } = await resolveSkill(cloud, input.skill);
|
|
182
|
+
const file = record(await api(cloud, 'skillFile')(id, input.path, input.version));
|
|
183
|
+
const content = page(typeof file.content === 'string' ? file.content : '', input.offset, max);
|
|
184
|
+
return { source: SOURCE, path: file.path, encoding: file.encoding, size: file.size, executable: file.executable ?? false, version: file.version ?? null, content: content.text, offset: content.offset, nextOffset: content.nextOffset, truncated: content.truncated };
|
|
185
|
+
}));
|
|
186
|
+
const fileInput = z.object({
|
|
187
|
+
path: z.string().trim().min(1).max(1024), content: z.string(), encoding: z.enum(['utf8', 'base64']).optional(), executable: z.boolean().optional(),
|
|
188
|
+
});
|
|
189
|
+
const normalize = (files) => files.map(f => ({ path: f.path, content: f.content, encoding: f.encoding ?? 'utf8', ...(f.executable !== undefined ? { executable: f.executable } : {}) }));
|
|
190
|
+
const write = { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true };
|
|
191
|
+
server.registerTool('vault_go_skill_save', {
|
|
192
|
+
title: 'Salvar skill',
|
|
193
|
+
description: 'Salva uma skill na biblioteca privada: cria se não existir nessa pasta, senão publica uma nova versão (as anteriores ficam guardadas). Envie files (SKILL.md com frontmatter name/description e arquivos auxiliares) ou sourceDir (pasta local). folder aceita caminho ("dev/review") e cria o que faltar. linkCurrentProject liga ao projeto do diretório atual.',
|
|
194
|
+
inputSchema: {
|
|
195
|
+
name: z.string().trim().min(1).max(64).optional(), description: z.string().trim().min(1).max(4096).optional(),
|
|
196
|
+
folder: z.string().trim().max(600).optional(), projectIds: z.array(z.string().trim().min(1).max(128)).max(50).optional(),
|
|
197
|
+
linkCurrentProject: z.boolean().optional(), files: z.array(fileInput).max(512).optional(), sourceDir: z.string().trim().min(1).max(2000).optional(),
|
|
198
|
+
patch: z.boolean().optional(), remove: z.array(z.string().trim().min(1).max(1024)).max(512).optional(),
|
|
199
|
+
baseVersion: z.string().trim().regex(/^[A-Za-z0-9_-]{1,128}$/).optional(), releaseNotes: z.string().max(10_000).optional(),
|
|
200
|
+
},
|
|
201
|
+
annotations: write,
|
|
202
|
+
}, async (input) => run(async () => {
|
|
203
|
+
if (!input.files && !input.sourceDir)
|
|
204
|
+
throw new Error('Informe files ou sourceDir.');
|
|
205
|
+
const files = input.sourceDir ? readSourceDir(input.sourceDir) : normalize(input.files ?? []);
|
|
206
|
+
const md = files.find(f => f.path === 'SKILL.md');
|
|
207
|
+
const front = md && md.encoding === 'utf8' ? frontmatter(md.content) : null;
|
|
208
|
+
if (!input.patch && !front)
|
|
209
|
+
throw new Error('SKILL.md na raiz precisa de frontmatter com name e description.');
|
|
210
|
+
if (input.patch && !input.baseVersion)
|
|
211
|
+
throw new Error('patch exige baseVersion.');
|
|
212
|
+
const name = input.name ?? front?.name;
|
|
213
|
+
if (!name)
|
|
214
|
+
throw new Error('Informe name (ou um SKILL.md com frontmatter).');
|
|
215
|
+
const folderId = input.folder !== undefined ? await ensureFolder(cloud, input.folder) : null;
|
|
216
|
+
const existing = (await listAll(cloud, { folderId: folderId ?? 'root' })).find(s => s.name.toLowerCase() === name.toLowerCase());
|
|
217
|
+
const project = input.linkCurrentProject ? await currentProject(cloud, cwd()) : null;
|
|
218
|
+
if (input.linkCurrentProject && !project)
|
|
219
|
+
throw new Error('Nenhum projeto do Vault corresponde ao diretório atual.');
|
|
220
|
+
const links = union(input.projectIds ?? [], project ? [project.id] : []);
|
|
221
|
+
const releaseNotes = input.releaseNotes ?? '';
|
|
222
|
+
const naming = { ...(input.name ? { name: input.name } : {}), ...(input.description ? { description: input.description } : {}) };
|
|
223
|
+
if (!existing) {
|
|
224
|
+
if (input.patch)
|
|
225
|
+
throw new Error(`Skill não encontrada para patch: ${name}`);
|
|
226
|
+
const created = record(await api(cloud, 'createSkill')({ files, folderId, releaseNotes, ...naming, ...(links.length ? { projectIds: links } : {}) }));
|
|
227
|
+
return { action: 'created', skill: compact(record(created.skill)) };
|
|
228
|
+
}
|
|
229
|
+
const body = input.patch
|
|
230
|
+
? { baseVersion: input.baseVersion, upsert: files, remove: input.remove ?? [], releaseNotes, ...(input.description ? { description: input.description } : {}) }
|
|
231
|
+
: { files, ...(input.baseVersion ? { baseVersion: input.baseVersion } : {}), releaseNotes, ...naming };
|
|
232
|
+
let updated = record(record(await api(cloud, 'createSkillVersion')(existing.id, body)).skill);
|
|
233
|
+
if (links.length)
|
|
234
|
+
updated = record(record(await api(cloud, 'setSkillProjects')(existing.id, union(existing.projectIds ?? [], links))).skill);
|
|
235
|
+
return { action: 'updated', skill: compact(updated) };
|
|
236
|
+
}));
|
|
237
|
+
server.registerTool('vault_go_skill_organize', {
|
|
238
|
+
title: 'Organizar skills',
|
|
239
|
+
description: 'Organiza a biblioteca privada: create_folder, move_folder, rename_folder, move_skill, rename_skill, link_projects e unlink_projects. Caminhos de pasta usam "/" e "" é a raiz. Sem projectIds, link/unlink usam o projeto do diretório atual. Não exclui nada.',
|
|
240
|
+
inputSchema: {
|
|
241
|
+
action: z.enum(['create_folder', 'move_folder', 'rename_folder', 'move_skill', 'rename_skill', 'link_projects', 'unlink_projects']),
|
|
242
|
+
skill: skillRef.optional(), folder: z.string().trim().max(600).optional(), targetFolder: z.string().trim().max(600).optional(),
|
|
243
|
+
newName: z.string().trim().min(1).max(64).optional(), description: z.string().trim().min(1).max(4096).optional(),
|
|
244
|
+
projectIds: z.array(z.string().trim().min(1).max(128)).max(50).optional(),
|
|
245
|
+
},
|
|
246
|
+
annotations: write,
|
|
247
|
+
}, async (input) => run(async () => {
|
|
248
|
+
const required = (value, field) => { if (value === undefined || value === null)
|
|
249
|
+
throw new Error(`${input.action} exige ${field}.`); return value; };
|
|
250
|
+
const existingFolder = async (path) => {
|
|
251
|
+
const folder = folderByPath(await loadFolders(cloud), path);
|
|
252
|
+
if (!folder)
|
|
253
|
+
throw new Error(`Pasta não encontrada: ${path}`);
|
|
254
|
+
return folder;
|
|
255
|
+
};
|
|
256
|
+
switch (input.action) {
|
|
257
|
+
case 'create_folder':
|
|
258
|
+
return { folderId: await ensureFolder(cloud, required(input.folder, 'folder')) };
|
|
259
|
+
case 'rename_folder': {
|
|
260
|
+
const folder = await existingFolder(required(input.folder, 'folder'));
|
|
261
|
+
return api(cloud, 'updateSkillFolder')(folder.id, { name: required(input.newName, 'newName') });
|
|
262
|
+
}
|
|
263
|
+
case 'move_folder': {
|
|
264
|
+
const folder = await existingFolder(required(input.folder, 'folder'));
|
|
265
|
+
return api(cloud, 'updateSkillFolder')(folder.id, { parentId: await ensureFolder(cloud, required(input.targetFolder, 'targetFolder')) });
|
|
266
|
+
}
|
|
267
|
+
case 'move_skill': {
|
|
268
|
+
const target = required(input.targetFolder, 'targetFolder');
|
|
269
|
+
const { id } = await resolveSkill(cloud, required(input.skill, 'skill'));
|
|
270
|
+
return api(cloud, 'updateSkill')(id, { folderId: await ensureFolder(cloud, target) });
|
|
271
|
+
}
|
|
272
|
+
case 'rename_skill': {
|
|
273
|
+
const { id } = await resolveSkill(cloud, required(input.skill, 'skill'));
|
|
274
|
+
return api(cloud, 'updateSkill')(id, { name: required(input.newName, 'newName'), ...(input.description ? { description: input.description } : {}) });
|
|
275
|
+
}
|
|
276
|
+
case 'link_projects':
|
|
277
|
+
case 'unlink_projects': {
|
|
278
|
+
const { id } = await resolveSkill(cloud, required(input.skill, 'skill'));
|
|
279
|
+
let ids = input.projectIds;
|
|
280
|
+
if (!ids?.length) {
|
|
281
|
+
const project = await currentProject(cloud, cwd());
|
|
282
|
+
if (!project)
|
|
283
|
+
throw new Error('Informe projectIds: nenhum projeto do Vault corresponde ao diretório atual.');
|
|
284
|
+
ids = [project.id];
|
|
285
|
+
}
|
|
286
|
+
const current = record(record(await api(cloud, 'skill')(id)).skill).projectIds ?? [];
|
|
287
|
+
const next = input.action === 'link_projects' ? union(current, ids) : current.filter(p => !ids.includes(p));
|
|
288
|
+
return api(cloud, 'setSkillProjects')(id, next);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}));
|
|
292
|
+
const userHome = options.userHome ?? homedir();
|
|
293
|
+
const fetcher = options.fetch ?? globalThis.fetch;
|
|
294
|
+
const targets = z.array(z.enum(SKILL_TARGETS)).min(1).max(3).default(['claude']);
|
|
295
|
+
async function download(id, versionId) {
|
|
296
|
+
return downloadSkillArchive(cloud, id, versionId, fetcher);
|
|
297
|
+
}
|
|
298
|
+
async function installInto(id, name, roots, versionId) {
|
|
299
|
+
const archive = await download(id, versionId);
|
|
300
|
+
return roots.map(root => ({ root, versionId: archive.versionId, ...installSkillArchive({ bytes: archive.bytes, expectedSha256: archive.sha256, dirName: name, root, skillId: id, versionId: archive.versionId }) }));
|
|
301
|
+
}
|
|
302
|
+
server.registerTool('vault_go_skill_install', {
|
|
303
|
+
title: 'Instalar skill localmente',
|
|
304
|
+
description: 'Baixa uma skill da biblioteca privada, confere a integridade e instala em ~/.claude/skills (ou .grok/.agents). Nunca sobrescreve uma skill que o Vault não instalou.',
|
|
305
|
+
inputSchema: { skill: skillRef, version, targets },
|
|
306
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true },
|
|
307
|
+
}, async (input) => run(async () => {
|
|
308
|
+
const { id } = await resolveSkill(cloud, input.skill);
|
|
309
|
+
const detail = record(record(await api(cloud, 'skill')(id, input.version)).skill);
|
|
310
|
+
const name = String(detail.name);
|
|
311
|
+
const done = await installInto(id, name, input.targets.map(t => targetRoot(t, userHome)), input.version);
|
|
312
|
+
return { installed: done.map((d, i) => ({ target: input.targets[i], path: d.path, versionId: d.versionId })) };
|
|
313
|
+
}));
|
|
314
|
+
server.registerTool('vault_go_skill_sync', {
|
|
315
|
+
title: 'Sincronizar skills instaladas',
|
|
316
|
+
description: 'Atualiza as skills instaladas pelo Vault que têm versão nova. Com currentProject ou projectId, também instala as skills vinculadas ao projeto que ainda não estão instaladas.',
|
|
317
|
+
inputSchema: { currentProject: z.boolean().optional(), projectId: z.string().trim().min(1).max(128).optional(), targets },
|
|
318
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true },
|
|
319
|
+
}, async (input) => run(async () => {
|
|
320
|
+
const roots = input.targets.map(t => targetRoot(t, userHome));
|
|
321
|
+
const installed = installedSkills(roots);
|
|
322
|
+
const updated = [], missing = [], added = [];
|
|
323
|
+
for (const item of installed) {
|
|
324
|
+
let detail;
|
|
325
|
+
try {
|
|
326
|
+
detail = record(record(await api(cloud, 'skill')(item.skillId)).skill);
|
|
327
|
+
}
|
|
328
|
+
catch {
|
|
329
|
+
missing.push({ skill: item.dir, path: join(item.root, item.dir) });
|
|
330
|
+
continue;
|
|
331
|
+
}
|
|
332
|
+
const latest = record(detail.latestVersion).id;
|
|
333
|
+
if (typeof latest !== 'string' || latest === item.versionId)
|
|
334
|
+
continue;
|
|
335
|
+
const [done] = await installInto(item.skillId, item.dir, [item.root], latest);
|
|
336
|
+
updated.push({ skill: detail.name, from: item.versionId, to: latest, path: done.path });
|
|
337
|
+
}
|
|
338
|
+
const projectId = input.projectId ?? (input.currentProject ? (await currentProject(cloud, cwd()))?.id : undefined);
|
|
339
|
+
if (input.currentProject && !projectId)
|
|
340
|
+
throw new Error('Nenhum projeto do Vault corresponde ao diretório atual.');
|
|
341
|
+
if (projectId) {
|
|
342
|
+
const have = new Set(installedSkills(roots).map(i => `${i.skillId}\0${i.root}`));
|
|
343
|
+
const linked = (await listAll(cloud, { projectId })).filter(s => s.scope === 'project');
|
|
344
|
+
for (const skill of linked) {
|
|
345
|
+
const missingRoots = roots.filter(root => !have.has(`${skill.id}\0${root}`));
|
|
346
|
+
if (!missingRoots.length)
|
|
347
|
+
continue;
|
|
348
|
+
const done = await installInto(skill.id, skill.name, missingRoots);
|
|
349
|
+
added.push(...done.map(d => ({ skill: skill.name, path: d.path, versionId: d.versionId })));
|
|
350
|
+
for (const root of missingRoots)
|
|
351
|
+
have.add(`${skill.id}\0${root}`);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
return { updated, installed: added, missing };
|
|
355
|
+
}));
|
|
356
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { VaultMemoryApi } from './cloud.js';
|
|
2
|
+
type Target = 'agents' | 'claude' | 'grok';
|
|
3
|
+
type ListedSkill = {
|
|
4
|
+
id: string;
|
|
5
|
+
name: string;
|
|
6
|
+
folderPath: string | null;
|
|
7
|
+
};
|
|
8
|
+
type InstalledSkillResult = {
|
|
9
|
+
name: string;
|
|
10
|
+
target: Target;
|
|
11
|
+
path: string;
|
|
12
|
+
versionId: string;
|
|
13
|
+
};
|
|
14
|
+
type SkippedSkillResult = {
|
|
15
|
+
name: string;
|
|
16
|
+
target: Target;
|
|
17
|
+
path: string;
|
|
18
|
+
reason: 'unmanaged' | 'foreign';
|
|
19
|
+
};
|
|
20
|
+
export type SkillsCommandResult = {
|
|
21
|
+
skills: ListedSkill[];
|
|
22
|
+
installed: InstalledSkillResult[];
|
|
23
|
+
skipped: SkippedSkillResult[];
|
|
24
|
+
};
|
|
25
|
+
type Dependencies = {
|
|
26
|
+
cloud: VaultMemoryApi;
|
|
27
|
+
userHome?: string;
|
|
28
|
+
fetcher?: typeof fetch;
|
|
29
|
+
};
|
|
30
|
+
export declare function executeSkillsCommand(args: string[], dependencies: Dependencies): Promise<SkillsCommandResult>;
|
|
31
|
+
export {};
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { homedir } from 'node:os';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { inspectSkillDestination, installSkillArchive } from './skill-local.js';
|
|
4
|
+
import { downloadSkillArchive } from './skill-download.js';
|
|
5
|
+
import { listAllSkillSummaries } from './skill-library.js';
|
|
6
|
+
async function listSkills(cloud) {
|
|
7
|
+
return (await listAllSkillSummaries(cloud)).map(skill => ({ id: skill.id, name: skill.name, folderPath: skill.folderPath ?? null }));
|
|
8
|
+
}
|
|
9
|
+
function parse(args) {
|
|
10
|
+
const action = args[0] ?? 'list';
|
|
11
|
+
if (action !== 'list' && action !== 'install')
|
|
12
|
+
throw new Error('Use vault-go skills list ou vault-go skills install --skill <nome>|--all.');
|
|
13
|
+
let all = false;
|
|
14
|
+
let target = 'agents';
|
|
15
|
+
const refs = [];
|
|
16
|
+
for (let index = 1; index < args.length; index++) {
|
|
17
|
+
const flag = args[index];
|
|
18
|
+
if (flag === '--all') {
|
|
19
|
+
all = true;
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
22
|
+
if (flag === '--skill' || flag === '--target') {
|
|
23
|
+
const value = args[++index];
|
|
24
|
+
if (!value || value.startsWith('--'))
|
|
25
|
+
throw new Error(`${flag} exige um valor.`);
|
|
26
|
+
if (flag === '--skill')
|
|
27
|
+
refs.push(value);
|
|
28
|
+
else if (value === 'agents' || value === 'claude' || value === 'grok')
|
|
29
|
+
target = value;
|
|
30
|
+
else
|
|
31
|
+
throw new Error('--target deve ser agents, claude ou grok.');
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
throw new Error(`Opção desconhecida: ${flag}`);
|
|
35
|
+
}
|
|
36
|
+
if (action === 'list' && (all || refs.length || target !== 'agents'))
|
|
37
|
+
throw new Error('vault-go skills list não aceita opções de instalação.');
|
|
38
|
+
if (action === 'install' && (all === (refs.length > 0)))
|
|
39
|
+
throw new Error('Use --all ou pelo menos um --skill.');
|
|
40
|
+
return { action, all, refs, target };
|
|
41
|
+
}
|
|
42
|
+
function select(skills, all, refs) {
|
|
43
|
+
const selected = all ? skills : refs.map(ref => {
|
|
44
|
+
const matches = skills.filter(skill => [skill.id, skill.name, `${skill.folderPath ? `${skill.folderPath}/` : ''}${skill.name}`].some(value => value.toLowerCase() === ref.toLowerCase()));
|
|
45
|
+
if (!matches.length)
|
|
46
|
+
throw new Error(`Skill não encontrada: ${ref}`);
|
|
47
|
+
if (matches.length > 1)
|
|
48
|
+
throw new Error(`Skill ambígua: ${ref}. Informe pasta/nome.`);
|
|
49
|
+
return matches[0];
|
|
50
|
+
});
|
|
51
|
+
const unique = [...new Map(selected.map(skill => [skill.id, skill])).values()];
|
|
52
|
+
const names = new Set();
|
|
53
|
+
for (const skill of unique) {
|
|
54
|
+
const name = skill.name.toLowerCase();
|
|
55
|
+
if (names.has(name))
|
|
56
|
+
throw new Error(`Mais de uma skill usa a pasta ${skill.name}; selecione uma por vez.`);
|
|
57
|
+
names.add(name);
|
|
58
|
+
}
|
|
59
|
+
return unique;
|
|
60
|
+
}
|
|
61
|
+
export async function executeSkillsCommand(args, dependencies) {
|
|
62
|
+
const command = parse(args);
|
|
63
|
+
const skills = await listSkills(dependencies.cloud);
|
|
64
|
+
if (command.action === 'list')
|
|
65
|
+
return { skills, installed: [], skipped: [] };
|
|
66
|
+
if (!dependencies.cloud.skillDownloadGrant)
|
|
67
|
+
throw new Error('Este Vault não oferece download de skills.');
|
|
68
|
+
const selected = select(skills, command.all, command.refs);
|
|
69
|
+
const root = command.target === 'agents'
|
|
70
|
+
? join(dependencies.userHome ?? homedir(), '.agents', 'skills')
|
|
71
|
+
: join(dependencies.userHome ?? homedir(), `.${command.target}`, 'skills');
|
|
72
|
+
const fetcher = dependencies.fetcher ?? globalThis.fetch;
|
|
73
|
+
const installed = [];
|
|
74
|
+
const skipped = [];
|
|
75
|
+
const planned = selected.flatMap(skill => {
|
|
76
|
+
const destination = inspectSkillDestination(root, skill.name, skill.id);
|
|
77
|
+
if (destination.status !== 'conflict')
|
|
78
|
+
return [skill];
|
|
79
|
+
if (!command.all)
|
|
80
|
+
throw new Error(`${destination.path} já existe; skill ${skill.name} não foi instalada.`);
|
|
81
|
+
skipped.push({ name: skill.name, target: command.target, path: destination.path, reason: destination.reason });
|
|
82
|
+
return [];
|
|
83
|
+
});
|
|
84
|
+
for (const skill of planned) {
|
|
85
|
+
const downloaded = await downloadSkillArchive(dependencies.cloud, skill.id, undefined, fetcher);
|
|
86
|
+
const result = installSkillArchive({ bytes: downloaded.bytes, expectedSha256: downloaded.sha256, dirName: skill.name, root, skillId: skill.id, versionId: downloaded.versionId });
|
|
87
|
+
installed.push({ name: skill.name, target: command.target, path: result.path, versionId: downloaded.versionId });
|
|
88
|
+
}
|
|
89
|
+
return { skills: [], installed, skipped };
|
|
90
|
+
}
|
|
@@ -44,10 +44,11 @@ function load(home) {
|
|
|
44
44
|
const value = readPrivateJson(home, 'transcript-watchers.json', 100_000);
|
|
45
45
|
if (value?.version !== 1 || value.owner !== captureOwner(home) || !Array.isArray(value.targets))
|
|
46
46
|
return { version: 1, owner: captureOwner(home), targets: [] };
|
|
47
|
-
return { version: 1, owner: value.owner, targets: value.targets.filter(t => t && typeof t.id === 'string' && ['claude', 'codex'].includes(t.adapter) && typeof t.root === 'string' && isAbsolute(t.root) && typeof t.workspace === 'string' && isAbsolute(t.workspace) && typeof t.since === 'number').slice(0, 20) };
|
|
47
|
+
return { version: 1, owner: value.owner, targets: value.targets.filter(t => t && typeof t.id === 'string' && ['claude', 'codex', 'cursor'].includes(t.adapter) && typeof t.root === 'string' && isAbsolute(t.root) && typeof t.workspace === 'string' && isAbsolute(t.workspace) && typeof t.since === 'number').slice(0, 20) };
|
|
48
48
|
}
|
|
49
49
|
export function defaultTranscriptRoot(adapter, userHome = homedir()) {
|
|
50
|
-
|
|
50
|
+
const folder = adapter === 'claude' ? '.claude/projects' : adapter === 'cursor' ? '.cursor/projects' : '.codex/sessions';
|
|
51
|
+
return join(userHome, folder);
|
|
51
52
|
}
|
|
52
53
|
export function transcriptStatus(home) {
|
|
53
54
|
const config = load(home);
|
|
@@ -64,7 +65,7 @@ export function transcriptCaptureEnabled(home, adapter, cwd) {
|
|
|
64
65
|
return load(home).targets.some(t => t.enabled && t.adapter === adapter && t.workspace === workspace);
|
|
65
66
|
}
|
|
66
67
|
export function configureTranscriptWatcher(home, input) {
|
|
67
|
-
if (!['claude', 'codex'].includes(input.adapter) || !isAbsolute(input.workspace))
|
|
68
|
+
if (!['claude', 'codex', 'cursor'].includes(input.adapter) || !isAbsolute(input.workspace))
|
|
68
69
|
throw new Error('Adaptador ou workspace inválido. Use um caminho absoluto.');
|
|
69
70
|
if (input.enabled && input.consent !== true)
|
|
70
71
|
throw new Error('Ativar exige consent=true para capturar transcripts deste workspace.');
|
|
@@ -63,7 +63,7 @@ export function parseTranscript(adapter, value, previous, offset) {
|
|
|
63
63
|
if (cwd)
|
|
64
64
|
state.cwd = cwd;
|
|
65
65
|
}
|
|
66
|
-
if (adapter === 'claude') {
|
|
66
|
+
if (adapter === 'claude' || adapter === 'cursor') {
|
|
67
67
|
scope(text(row.sessionId), text(row.cwd));
|
|
68
68
|
if (row.private === true || row.isPrivate === true || row.is_private === true) {
|
|
69
69
|
state.private = true;
|
package/dist/transcript-tools.js
CHANGED
|
@@ -8,7 +8,7 @@ export function registerTranscriptTools(server, home, cloud) {
|
|
|
8
8
|
server.registerTool('vault_go_transcripts_status', { description: 'Watchers opt-in de Claude/Codex neste dispositivo e conta. Requer automaticCapture para enviar eventos; geração usa o motor já escolhido.', inputSchema: {}, annotations: { readOnlyHint: true, openWorldHint: false } }, () => run(async () => transcriptStatus(home)));
|
|
9
9
|
server.registerTool('vault_go_transcripts_configure', {
|
|
10
10
|
description: 'Ativa/desativa captura persistente de um workspace com consentimento explícito. Por padrão ignora histórico existente; fromStart importa histórico. Hooks nativos preservam contexto e cedem escrita ao watcher enquanto ativo. O worker local precisa estar em execução.',
|
|
11
|
-
inputSchema: { adapter: z.enum(['claude', 'codex']), workspace: path, root: path.optional(), enabled: z.boolean(), consent: z.boolean().default(false), fromStart: z.boolean().default(false) },
|
|
11
|
+
inputSchema: { adapter: z.enum(['claude', 'codex', 'cursor']), workspace: path, root: path.optional(), enabled: z.boolean(), consent: z.boolean().default(false), fromStart: z.boolean().default(false) },
|
|
12
12
|
annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: false },
|
|
13
13
|
}, input => run(async () => { const { root, ...rest } = input; return configureTranscriptWatcher(home, { ...rest, ...(root === undefined ? {} : { root }) }); }));
|
|
14
14
|
server.registerTool('vault_go_transcripts_poll', { description: 'Processa um lote limitado dos watchers já consentidos. Retomada e idempotência persistem entre reinícios; não ativa captura nem muda o motor.', inputSchema: {}, annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true } }, () => run(() => pollTranscripts(home, cloud)));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vault-go",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.31.0",
|
|
4
4
|
"description": "MCP server and installer for Vault Memory: search, capture, and use account memory across AI clients.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -26,6 +26,20 @@
|
|
|
26
26
|
"dist/knowledge-agent.d.ts",
|
|
27
27
|
"dist/knowledge-tools.js",
|
|
28
28
|
"dist/knowledge-tools.d.ts",
|
|
29
|
+
"dist/skill-context.js",
|
|
30
|
+
"dist/skill-context.d.ts",
|
|
31
|
+
"dist/skill-local.js",
|
|
32
|
+
"dist/skill-local.d.ts",
|
|
33
|
+
"dist/legacy-brand.js",
|
|
34
|
+
"dist/legacy-brand.d.ts",
|
|
35
|
+
"dist/skill-tools.js",
|
|
36
|
+
"dist/skill-tools.d.ts",
|
|
37
|
+
"dist/skill-download.js",
|
|
38
|
+
"dist/skill-download.d.ts",
|
|
39
|
+
"dist/skill-library.js",
|
|
40
|
+
"dist/skill-library.d.ts",
|
|
41
|
+
"dist/skills-cli.js",
|
|
42
|
+
"dist/skills-cli.d.ts",
|
|
29
43
|
"dist/activity.js",
|
|
30
44
|
"dist/activity.d.ts",
|
|
31
45
|
"dist/auth.js",
|
|
@@ -133,6 +147,10 @@
|
|
|
133
147
|
"dist/awareness-tools.d.ts",
|
|
134
148
|
"dist/skill-install.js",
|
|
135
149
|
"dist/skill-install.d.ts",
|
|
150
|
+
"dist/memory-archive.js",
|
|
151
|
+
"dist/memory-archive.d.ts",
|
|
152
|
+
"dist/agent-cost.js",
|
|
153
|
+
"dist/agent-cost.d.ts",
|
|
136
154
|
"grammars/"
|
|
137
155
|
],
|
|
138
156
|
"scripts": {
|