kanna-code 0.37.0 → 0.39.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/dist/client/assets/{index-yvyssqF_.js → index-CWBXUZnG.js} +138 -138
- package/dist/client/assets/{index-pUhvjSZr.css → index-SIga6V_y.css} +1 -1
- package/dist/client/index.html +2 -2
- package/dist/export-viewer/assets/{index-CY-dlB67.css → index-CmyWpbI0.css} +1 -1
- package/dist/export-viewer/index.html +2 -2
- package/package.json +2 -2
- package/src/server/agent.ts +2 -0
- package/src/server/ws-router.test.ts +103 -2
- package/src/server/ws-router.ts +235 -1
- package/src/shared/protocol.ts +4 -0
- package/src/shared/types.ts +49 -0
- /package/dist/export-viewer/assets/{index-Dw6GRRjm.js → index-DoZWMQxo.js} +0 -0
|
@@ -1,11 +1,19 @@
|
|
|
1
1
|
import { describe, expect, test } from "bun:test"
|
|
2
|
-
import { mkdtemp, rm } from "node:fs/promises"
|
|
2
|
+
import { mkdtemp, rm, writeFile } from "node:fs/promises"
|
|
3
3
|
import { tmpdir } from "node:os"
|
|
4
4
|
import path from "node:path"
|
|
5
5
|
import type { AppSettingsSnapshot, KeybindingsSnapshot, LlmProviderSnapshot, UpdateSnapshot } from "../shared/types"
|
|
6
6
|
import { PROTOCOL_VERSION } from "../shared/types"
|
|
7
7
|
import { createEmptyState } from "./events"
|
|
8
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
assertSafeSkillId,
|
|
10
|
+
assertSafeSkillSource,
|
|
11
|
+
buildInstallSkillCommand,
|
|
12
|
+
buildUninstallSkillCommand,
|
|
13
|
+
createWsRouter,
|
|
14
|
+
listInstalledSkills,
|
|
15
|
+
parseInstalledSkillsLock,
|
|
16
|
+
} from "./ws-router"
|
|
9
17
|
|
|
10
18
|
function withSidebarGroupDefaults(group: {
|
|
11
19
|
groupKey: string
|
|
@@ -96,6 +104,99 @@ const DEFAULT_APP_SETTINGS_SNAPSHOT: AppSettingsSnapshot = {
|
|
|
96
104
|
filePathDisplay: "~/.kanna/data/settings.json",
|
|
97
105
|
}
|
|
98
106
|
|
|
107
|
+
describe("skills helpers", () => {
|
|
108
|
+
test("parses installed global skills from a lock payload", () => {
|
|
109
|
+
const snapshot = parseInstalledSkillsLock({
|
|
110
|
+
version: 1,
|
|
111
|
+
skills: {
|
|
112
|
+
zeta: {
|
|
113
|
+
source: "owner/zeta",
|
|
114
|
+
sourceType: "github",
|
|
115
|
+
sourceUrl: "https://github.com/owner/zeta",
|
|
116
|
+
skillPath: "skills/zeta/SKILL.md",
|
|
117
|
+
installedAt: "2026-05-01T01:00:00.000Z",
|
|
118
|
+
updatedAt: "2026-05-01T02:00:00.000Z",
|
|
119
|
+
pluginName: "zeta-plugin",
|
|
120
|
+
},
|
|
121
|
+
alpha: {
|
|
122
|
+
source: "owner/alpha",
|
|
123
|
+
sourceType: "github",
|
|
124
|
+
},
|
|
125
|
+
ignored: "not an object",
|
|
126
|
+
},
|
|
127
|
+
}, "/tmp/.skill-lock.json")
|
|
128
|
+
|
|
129
|
+
expect(snapshot.lockFilePath).toBe("/tmp/.skill-lock.json")
|
|
130
|
+
expect(snapshot.skills.map((skill) => skill.name)).toEqual(["alpha", "zeta"])
|
|
131
|
+
expect(snapshot.skills[0]).toMatchObject({
|
|
132
|
+
name: "alpha",
|
|
133
|
+
source: "owner/alpha",
|
|
134
|
+
sourceType: "github",
|
|
135
|
+
sourceUrl: "",
|
|
136
|
+
installedAt: "",
|
|
137
|
+
updatedAt: "",
|
|
138
|
+
})
|
|
139
|
+
expect(snapshot.skills[1]).toMatchObject({
|
|
140
|
+
name: "zeta",
|
|
141
|
+
source: "owner/zeta",
|
|
142
|
+
skillPath: "skills/zeta/SKILL.md",
|
|
143
|
+
pluginName: "zeta-plugin",
|
|
144
|
+
})
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
test("returns an empty installed skills snapshot when the lock file is missing or invalid", async () => {
|
|
148
|
+
const dir = await mkdtemp(path.join(tmpdir(), "kanna-skills-"))
|
|
149
|
+
try {
|
|
150
|
+
const missingPath = path.join(dir, "missing.json")
|
|
151
|
+
expect(await listInstalledSkills(missingPath)).toEqual({
|
|
152
|
+
lockFilePath: missingPath,
|
|
153
|
+
skills: [],
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
const invalidPath = path.join(dir, ".skill-lock.json")
|
|
157
|
+
await writeFile(invalidPath, "{", "utf8")
|
|
158
|
+
expect(await listInstalledSkills(invalidPath)).toEqual({
|
|
159
|
+
lockFilePath: invalidPath,
|
|
160
|
+
skills: [],
|
|
161
|
+
})
|
|
162
|
+
} finally {
|
|
163
|
+
await rm(dir, { recursive: true, force: true })
|
|
164
|
+
}
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
test("validates skill source and id before building commands", () => {
|
|
168
|
+
expect(assertSafeSkillSource(" owner/repo ")).toBe("owner/repo")
|
|
169
|
+
expect(assertSafeSkillId(" my-skill_1 ")).toBe("my-skill_1")
|
|
170
|
+
expect(() => assertSafeSkillSource("https://github.com/owner/repo")).toThrow("owner/repo")
|
|
171
|
+
expect(() => assertSafeSkillId("../nope")).toThrow("Skill id is invalid.")
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
test("builds global install and uninstall commands for universal and Claude Code aliases", () => {
|
|
175
|
+
expect(buildInstallSkillCommand("owner/repo", "my-skill").slice(1)).toEqual([
|
|
176
|
+
"skills",
|
|
177
|
+
"add",
|
|
178
|
+
"owner/repo",
|
|
179
|
+
"--skill",
|
|
180
|
+
"my-skill",
|
|
181
|
+
"--global",
|
|
182
|
+
"--agent",
|
|
183
|
+
"universal",
|
|
184
|
+
"claude-code",
|
|
185
|
+
"--yes",
|
|
186
|
+
])
|
|
187
|
+
expect(buildUninstallSkillCommand("my-skill").slice(1)).toEqual([
|
|
188
|
+
"skills",
|
|
189
|
+
"remove",
|
|
190
|
+
"my-skill",
|
|
191
|
+
"--global",
|
|
192
|
+
"--agent",
|
|
193
|
+
"universal",
|
|
194
|
+
"claude-code",
|
|
195
|
+
"--yes",
|
|
196
|
+
])
|
|
197
|
+
})
|
|
198
|
+
})
|
|
199
|
+
|
|
99
200
|
const DEFAULT_UPDATE_SNAPSHOT: UpdateSnapshot = {
|
|
100
201
|
currentVersion: "0.12.0",
|
|
101
202
|
latestVersion: null,
|
package/src/server/ws-router.ts
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises"
|
|
2
|
+
import os from "node:os"
|
|
3
|
+
import path from "node:path"
|
|
1
4
|
import type { ServerWebSocket } from "bun"
|
|
2
5
|
import { PROTOCOL_VERSION } from "../shared/types"
|
|
3
6
|
import type { ClientEnvelope, ServerEnvelope, SubscriptionTopic } from "../shared/protocol"
|
|
@@ -16,9 +19,19 @@ import { writeStandaloneTranscriptExport } from "./standalone-export"
|
|
|
16
19
|
import { TerminalManager } from "./terminal-manager"
|
|
17
20
|
import type { UpdateManager } from "./update-manager"
|
|
18
21
|
import { deriveChatSnapshot, deriveLocalProjectsSnapshot, deriveSidebarData } from "./read-models"
|
|
19
|
-
import type {
|
|
22
|
+
import type {
|
|
23
|
+
AppSettingsPatch,
|
|
24
|
+
AppSettingsSnapshot,
|
|
25
|
+
InstalledSkillsSnapshot,
|
|
26
|
+
LlmProviderSnapshot,
|
|
27
|
+
LlmProviderValidationResult,
|
|
28
|
+
SkillInstallResult,
|
|
29
|
+
SkillSearchSnapshot,
|
|
30
|
+
SkillUninstallResult,
|
|
31
|
+
} from "../shared/types"
|
|
20
32
|
|
|
21
33
|
const DEFAULT_CHAT_RECENT_LIMIT = 200
|
|
34
|
+
const SKILL_AGENT_ALIASES = ["universal", "claude-code"] as const
|
|
22
35
|
|
|
23
36
|
function isSendToStartingProfilingEnabled() {
|
|
24
37
|
return process.env.KANNA_PROFILE_SEND_TO_STARTING === "1"
|
|
@@ -149,6 +162,207 @@ function send(ws: ServerWebSocket<ClientState>, message: ServerEnvelope) {
|
|
|
149
162
|
return payload.length
|
|
150
163
|
}
|
|
151
164
|
|
|
165
|
+
export function assertSafeSkillSource(source: string) {
|
|
166
|
+
const normalized = source.trim()
|
|
167
|
+
if (!/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/.test(normalized)) {
|
|
168
|
+
throw new Error("Skill source must be an owner/repo pair.")
|
|
169
|
+
}
|
|
170
|
+
return normalized
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export function assertSafeSkillId(skillId: string) {
|
|
174
|
+
const normalized = skillId.trim()
|
|
175
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$/.test(normalized)) {
|
|
176
|
+
throw new Error("Skill id is invalid.")
|
|
177
|
+
}
|
|
178
|
+
return normalized
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export function getGlobalSkillLockPath() {
|
|
182
|
+
const xdgStateHome = process.env.XDG_STATE_HOME?.trim()
|
|
183
|
+
if (xdgStateHome) {
|
|
184
|
+
return path.join(xdgStateHome, "skills", ".skill-lock.json")
|
|
185
|
+
}
|
|
186
|
+
return path.join(os.homedir(), ".agents", ".skill-lock.json")
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function asString(value: unknown) {
|
|
190
|
+
return typeof value === "string" ? value : ""
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export function parseInstalledSkillsLock(parsed: unknown, lockFilePath: string): InstalledSkillsSnapshot {
|
|
194
|
+
const skillsRecord = parsed
|
|
195
|
+
&& typeof parsed === "object"
|
|
196
|
+
&& "skills" in parsed
|
|
197
|
+
&& parsed.skills
|
|
198
|
+
&& typeof parsed.skills === "object"
|
|
199
|
+
&& !Array.isArray(parsed.skills)
|
|
200
|
+
? parsed.skills as Record<string, unknown>
|
|
201
|
+
: {}
|
|
202
|
+
|
|
203
|
+
const skills = Object.entries(skillsRecord)
|
|
204
|
+
.filter(([, entry]) => entry && typeof entry === "object" && !Array.isArray(entry))
|
|
205
|
+
.map(([name, entry]) => {
|
|
206
|
+
const record = entry as Record<string, unknown>
|
|
207
|
+
return {
|
|
208
|
+
name,
|
|
209
|
+
source: asString(record.source),
|
|
210
|
+
sourceType: asString(record.sourceType),
|
|
211
|
+
sourceUrl: asString(record.sourceUrl),
|
|
212
|
+
skillPath: asString(record.skillPath) || undefined,
|
|
213
|
+
installedAt: asString(record.installedAt),
|
|
214
|
+
updatedAt: asString(record.updatedAt),
|
|
215
|
+
pluginName: asString(record.pluginName) || undefined,
|
|
216
|
+
}
|
|
217
|
+
})
|
|
218
|
+
.sort((a, b) => a.name.localeCompare(b.name))
|
|
219
|
+
|
|
220
|
+
return {
|
|
221
|
+
lockFilePath,
|
|
222
|
+
skills,
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
export async function listInstalledSkills(lockFilePath = getGlobalSkillLockPath()): Promise<InstalledSkillsSnapshot> {
|
|
227
|
+
try {
|
|
228
|
+
return parseInstalledSkillsLock(JSON.parse(await readFile(lockFilePath, "utf8")), lockFilePath)
|
|
229
|
+
} catch {
|
|
230
|
+
return {
|
|
231
|
+
lockFilePath,
|
|
232
|
+
skills: [],
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
export async function searchSkills(query: string, limit = 100): Promise<SkillSearchSnapshot> {
|
|
238
|
+
const normalizedQuery = query.trim()
|
|
239
|
+
if (normalizedQuery.length < 2) {
|
|
240
|
+
return {
|
|
241
|
+
query: normalizedQuery,
|
|
242
|
+
searchType: "fuzzy",
|
|
243
|
+
skills: [],
|
|
244
|
+
count: 0,
|
|
245
|
+
duration_ms: 0,
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const normalizedLimit = Math.max(1, Math.min(100, Math.trunc(limit)))
|
|
250
|
+
const url = new URL("https://skills.sh/api/search")
|
|
251
|
+
url.searchParams.set("q", normalizedQuery)
|
|
252
|
+
url.searchParams.set("limit", String(normalizedLimit))
|
|
253
|
+
|
|
254
|
+
const response = await fetch(url, {
|
|
255
|
+
signal: AbortSignal.timeout(10_000),
|
|
256
|
+
})
|
|
257
|
+
if (!response.ok) {
|
|
258
|
+
throw new Error(`Skills search failed with status ${response.status}.`)
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const payload = await response.json() as Partial<SkillSearchSnapshot>
|
|
262
|
+
return {
|
|
263
|
+
query: typeof payload.query === "string" ? payload.query : normalizedQuery,
|
|
264
|
+
searchType: typeof payload.searchType === "string" ? payload.searchType : "fuzzy",
|
|
265
|
+
skills: Array.isArray(payload.skills)
|
|
266
|
+
? payload.skills
|
|
267
|
+
.filter((skill) => (
|
|
268
|
+
skill
|
|
269
|
+
&& typeof skill === "object"
|
|
270
|
+
&& typeof skill.id === "string"
|
|
271
|
+
&& typeof skill.skillId === "string"
|
|
272
|
+
&& typeof skill.name === "string"
|
|
273
|
+
&& typeof skill.source === "string"
|
|
274
|
+
))
|
|
275
|
+
.map((skill) => ({
|
|
276
|
+
id: skill.id,
|
|
277
|
+
skillId: skill.skillId,
|
|
278
|
+
name: skill.name,
|
|
279
|
+
installs: typeof skill.installs === "number" ? skill.installs : 0,
|
|
280
|
+
source: skill.source,
|
|
281
|
+
}))
|
|
282
|
+
: [],
|
|
283
|
+
count: typeof payload.count === "number" ? payload.count : 0,
|
|
284
|
+
duration_ms: typeof payload.duration_ms === "number" ? payload.duration_ms : 0,
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
export function buildInstallSkillCommand(source: string, skillId: string) {
|
|
289
|
+
return [
|
|
290
|
+
process.platform === "win32" ? "npx.cmd" : "npx",
|
|
291
|
+
"skills",
|
|
292
|
+
"add",
|
|
293
|
+
assertSafeSkillSource(source),
|
|
294
|
+
"--skill",
|
|
295
|
+
assertSafeSkillId(skillId),
|
|
296
|
+
"--global",
|
|
297
|
+
"--agent",
|
|
298
|
+
...SKILL_AGENT_ALIASES,
|
|
299
|
+
"--yes",
|
|
300
|
+
]
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
export function buildUninstallSkillCommand(skillId: string) {
|
|
304
|
+
return [
|
|
305
|
+
process.platform === "win32" ? "npx.cmd" : "npx",
|
|
306
|
+
"skills",
|
|
307
|
+
"remove",
|
|
308
|
+
assertSafeSkillId(skillId),
|
|
309
|
+
"--global",
|
|
310
|
+
"--agent",
|
|
311
|
+
...SKILL_AGENT_ALIASES,
|
|
312
|
+
"--yes",
|
|
313
|
+
]
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
async function runSkillCommand(command: string[]) {
|
|
317
|
+
const cwd = os.homedir()
|
|
318
|
+
const subprocess = Bun.spawn(command, {
|
|
319
|
+
cwd,
|
|
320
|
+
stdout: "pipe",
|
|
321
|
+
stderr: "pipe",
|
|
322
|
+
env: {
|
|
323
|
+
...process.env,
|
|
324
|
+
DISABLE_TELEMETRY: process.env.DISABLE_TELEMETRY ?? "1",
|
|
325
|
+
},
|
|
326
|
+
})
|
|
327
|
+
|
|
328
|
+
const [stdout, stderr, exitCode] = await Promise.all([
|
|
329
|
+
new Response(subprocess.stdout).text(),
|
|
330
|
+
new Response(subprocess.stderr).text(),
|
|
331
|
+
subprocess.exited,
|
|
332
|
+
])
|
|
333
|
+
|
|
334
|
+
if (exitCode !== 0) {
|
|
335
|
+
throw new Error(stderr.trim() || stdout.trim() || `skills CLI exited with code ${exitCode}.`)
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
return { cwd, stdout, stderr }
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
export async function installSkill(source: string, skillId: string): Promise<SkillInstallResult> {
|
|
342
|
+
const command = buildInstallSkillCommand(source, skillId)
|
|
343
|
+
const { cwd, stdout, stderr } = await runSkillCommand(command)
|
|
344
|
+
return {
|
|
345
|
+
source: command[3],
|
|
346
|
+
skillId: command[5],
|
|
347
|
+
command,
|
|
348
|
+
cwd,
|
|
349
|
+
stdout,
|
|
350
|
+
stderr,
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
export async function uninstallSkill(skillId: string): Promise<SkillUninstallResult> {
|
|
355
|
+
const command = buildUninstallSkillCommand(skillId)
|
|
356
|
+
const { cwd, stdout, stderr } = await runSkillCommand(command)
|
|
357
|
+
return {
|
|
358
|
+
skillId: command[3],
|
|
359
|
+
command,
|
|
360
|
+
cwd,
|
|
361
|
+
stdout,
|
|
362
|
+
stderr,
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
152
366
|
function ensureSnapshotSignatures(ws: ServerWebSocket<ClientState>) {
|
|
153
367
|
if (!ws.data.snapshotSignatures) {
|
|
154
368
|
ws.data.snapshotSignatures = new Map()
|
|
@@ -913,6 +1127,26 @@ export function createWsRouter({
|
|
|
913
1127
|
send(ws, { v: PROTOCOL_VERSION, type: "ack", id, result })
|
|
914
1128
|
return
|
|
915
1129
|
}
|
|
1130
|
+
case "skills.search": {
|
|
1131
|
+
const snapshot = await searchSkills(command.query, command.limit)
|
|
1132
|
+
send(ws, { v: PROTOCOL_VERSION, type: "ack", id, result: snapshot })
|
|
1133
|
+
return
|
|
1134
|
+
}
|
|
1135
|
+
case "skills.install": {
|
|
1136
|
+
const result = await installSkill(command.source, command.skillId)
|
|
1137
|
+
send(ws, { v: PROTOCOL_VERSION, type: "ack", id, result })
|
|
1138
|
+
return
|
|
1139
|
+
}
|
|
1140
|
+
case "skills.uninstall": {
|
|
1141
|
+
const result = await uninstallSkill(command.skillId)
|
|
1142
|
+
send(ws, { v: PROTOCOL_VERSION, type: "ack", id, result })
|
|
1143
|
+
return
|
|
1144
|
+
}
|
|
1145
|
+
case "skills.listInstalled": {
|
|
1146
|
+
const result = await listInstalledSkills()
|
|
1147
|
+
send(ws, { v: PROTOCOL_VERSION, type: "ack", id, result })
|
|
1148
|
+
return
|
|
1149
|
+
}
|
|
916
1150
|
case "project.open": {
|
|
917
1151
|
await ensureProjectDirectory(command.localPath)
|
|
918
1152
|
const normalizedPath = resolveLocalPath(command.localPath)
|
package/src/shared/protocol.ts
CHANGED
|
@@ -68,6 +68,10 @@ export type ClientCommand =
|
|
|
68
68
|
| { type: "settings.writeAppSettings"; analyticsEnabled: boolean }
|
|
69
69
|
| { type: "settings.writeAppSettingsPatch"; patch: AppSettingsPatch }
|
|
70
70
|
| { type: "settings.readLlmProvider" }
|
|
71
|
+
| { type: "skills.search"; query: string; limit?: number }
|
|
72
|
+
| { type: "skills.install"; source: string; skillId: string }
|
|
73
|
+
| { type: "skills.uninstall"; skillId: string }
|
|
74
|
+
| { type: "skills.listInstalled" }
|
|
71
75
|
| {
|
|
72
76
|
type: "settings.writeLlmProvider"
|
|
73
77
|
provider: LlmProviderSnapshot["provider"]
|
package/src/shared/types.ts
CHANGED
|
@@ -15,6 +15,55 @@ export type AttachmentKind = "image" | "file"
|
|
|
15
15
|
export type StandaloneTranscriptAttachmentMode = "metadata" | "bundle"
|
|
16
16
|
export type StandaloneTranscriptTheme = "light" | "dark"
|
|
17
17
|
|
|
18
|
+
export interface SkillSearchResult {
|
|
19
|
+
id: string
|
|
20
|
+
skillId: string
|
|
21
|
+
name: string
|
|
22
|
+
installs: number
|
|
23
|
+
source: string
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface SkillSearchSnapshot {
|
|
27
|
+
query: string
|
|
28
|
+
searchType: string
|
|
29
|
+
skills: SkillSearchResult[]
|
|
30
|
+
count: number
|
|
31
|
+
duration_ms: number
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface SkillInstallResult {
|
|
35
|
+
source: string
|
|
36
|
+
skillId: string
|
|
37
|
+
command: string[]
|
|
38
|
+
cwd: string
|
|
39
|
+
stdout: string
|
|
40
|
+
stderr: string
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface SkillUninstallResult {
|
|
44
|
+
skillId: string
|
|
45
|
+
command: string[]
|
|
46
|
+
cwd: string
|
|
47
|
+
stdout: string
|
|
48
|
+
stderr: string
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface InstalledSkillSummary {
|
|
52
|
+
name: string
|
|
53
|
+
source: string
|
|
54
|
+
sourceType: string
|
|
55
|
+
sourceUrl: string
|
|
56
|
+
skillPath?: string
|
|
57
|
+
installedAt: string
|
|
58
|
+
updatedAt: string
|
|
59
|
+
pluginName?: string
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface InstalledSkillsSnapshot {
|
|
63
|
+
lockFilePath: string
|
|
64
|
+
skills: InstalledSkillSummary[]
|
|
65
|
+
}
|
|
66
|
+
|
|
18
67
|
export interface ChatAttachment {
|
|
19
68
|
id: string
|
|
20
69
|
kind: AttachmentKind
|
|
File without changes
|