buckyos 0.7.131 → 0.7.133
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 +9 -5
- package/cli/core/app.ts +8 -0
- package/cli/dist/cli.mjs +1360 -240
- package/cli/dist/cli.mjs.map +1 -1
- package/cli/modules/agent.ts +165 -0
- package/cli/modules/ai.ts +160 -0
- package/cli/modules/app.ts +86 -0
- package/cli/modules/log.ts +57 -5
- package/cli/modules/pikg.ts +14 -7
- package/cli/modules/pikg_archive.ts +83 -0
- package/cli/modules/pikg_protocol.ts +11 -1
- package/cli/modules/share.ts +356 -0
- package/cli/modules/skill.ts +256 -0
- package/cli/modules/system.ts +54 -15
- package/cli/runtime/host.ts +4 -0
- package/cli/version.ts +1 -1
- package/dist/browser.cjs +1 -1
- package/dist/browser.mjs +118 -110
- package/dist/index.cjs +1 -1
- package/dist/index.mjs +118 -110
- package/dist/msg_center_client.d.ts +156 -5
- package/dist/msg_center_client.d.ts.map +1 -1
- package/dist/{ndm_proxy-a167f2a4.mjs → ndm_proxy-501cb1e8.mjs} +201 -109
- package/dist/ndm_proxy-501cb1e8.mjs.map +1 -0
- package/dist/ndm_proxy-68b8160c.js +13 -0
- package/dist/ndm_proxy-68b8160c.js.map +1 -0
- package/dist/nfsp.cjs +1 -1
- package/dist/nfsp.cjs.map +1 -1
- package/dist/nfsp.d.ts +300 -0
- package/dist/nfsp.d.ts.map +1 -1
- package/dist/nfsp.mjs +105 -0
- package/dist/nfsp.mjs.map +1 -1
- package/dist/node.cjs +1 -1
- package/dist/node.mjs +118 -110
- package/dist/sdk_core.d.ts +2 -2
- package/dist/sdk_core.d.ts.map +1 -1
- package/package.json +1 -1
- package/dist/ndm_proxy-28e72ddf.js +0 -13
- package/dist/ndm_proxy-28e72ddf.js.map +0 -1
- package/dist/ndm_proxy-a167f2a4.mjs.map +0 -1
|
@@ -77,6 +77,8 @@ export interface PikgInspectionResult {
|
|
|
77
77
|
app: {
|
|
78
78
|
did: string
|
|
79
79
|
app_id: string
|
|
80
|
+
name?: string
|
|
81
|
+
app_type: string
|
|
80
82
|
version: string
|
|
81
83
|
owner: string
|
|
82
84
|
app_doc_object_id: string
|
|
@@ -574,6 +576,8 @@ export async function inspectPikg(path: string): Promise<PikgInspectionResult> {
|
|
|
574
576
|
app: {
|
|
575
577
|
did: String(appDoc.did),
|
|
576
578
|
app_id: appIdFromDid(String(appDoc.did)),
|
|
579
|
+
...(typeof appDoc.name === 'string' ? { name: appDoc.name } : {}),
|
|
580
|
+
app_type: String(appDoc.app_type),
|
|
577
581
|
version: String(appDoc.version),
|
|
578
582
|
owner: String(appDoc.owner),
|
|
579
583
|
app_doc_object_id: validated.appDocObjectId,
|
|
@@ -852,7 +856,7 @@ export function validateAppDocShape(appDoc: Record<string, unknown>): void {
|
|
|
852
856
|
expectNonEmptyString(appDoc[field], `APPDOC.${field}`)
|
|
853
857
|
}
|
|
854
858
|
if (appDoc.doc_type !== 'app') throw invalid('appdoc', 'APPDOC.doc_type must be app')
|
|
855
|
-
if (!['service', 'dapp', 'web', 'agent'].includes(String(appDoc.app_type))) {
|
|
859
|
+
if (!['service', 'dapp', 'web', 'agent', 'agent_skills'].includes(String(appDoc.app_type))) {
|
|
856
860
|
throw invalid('appdoc', 'APPDOC.app_type is invalid')
|
|
857
861
|
}
|
|
858
862
|
if (appDoc.name !== undefined) expectNonEmptyString(appDoc.name, 'APPDOC.name')
|
|
@@ -906,6 +910,12 @@ export function validateAppDocShape(appDoc: Record<string, unknown>): void {
|
|
|
906
910
|
if (Object.keys(pkgList).length === 0) {
|
|
907
911
|
throw invalid('appdoc', 'APPDOC.pkg_list must not be empty')
|
|
908
912
|
}
|
|
913
|
+
if (appDoc.app_type === 'agent_skills') {
|
|
914
|
+
expectNonEmptyString(appDoc.name, 'APPDOC.name')
|
|
915
|
+
if (Object.keys(pkgList).join() !== 'agent_skills') {
|
|
916
|
+
throw invalid('appdoc', 'an agent_skills App must contain only the agent_skills sub package')
|
|
917
|
+
}
|
|
918
|
+
}
|
|
909
919
|
deriveNamespaceForPackage(appDoc)
|
|
910
920
|
}
|
|
911
921
|
|
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
import type { CommandDefinition, CommandModule, JsonSchema } from '../core/command.ts'
|
|
2
|
+
import type { CommandContext } from '../core/context.ts'
|
|
3
|
+
import { EXIT_INTERNAL, ToolError, UsageError } from '../core/errors.ts'
|
|
4
|
+
import { expectObject, inputString, parseTimestamp, requiredInputString } from '../core/service.ts'
|
|
5
|
+
|
|
6
|
+
// Share links of the Zone file service (nfs-server `share.*` over NFSP,
|
|
7
|
+
// buckyos notepads/share-content-mgr-tech-design-todo.md §3.6).
|
|
8
|
+
const OBJECT_OUTPUT: JsonSchema = { type: 'object', additionalProperties: true }
|
|
9
|
+
const DAY_MS = 86_400_000
|
|
10
|
+
const EXPIRES_HELP = '1d, 7d (default), 30d, none, or an RFC 3339 time'
|
|
11
|
+
|
|
12
|
+
export function createShareModule(): CommandModule {
|
|
13
|
+
return {
|
|
14
|
+
name: 'share',
|
|
15
|
+
summary: 'Share files and folders as links, and manage what you have shared',
|
|
16
|
+
commands: [
|
|
17
|
+
createCommand(),
|
|
18
|
+
listCommand(),
|
|
19
|
+
getCommand(),
|
|
20
|
+
updateCommand(),
|
|
21
|
+
stopCommand(),
|
|
22
|
+
statsCommand(),
|
|
23
|
+
],
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
interface NfspSession {
|
|
28
|
+
call(method: string, args: Record<string, unknown>, write?: boolean): Promise<Record<string, unknown>>
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async function openNfsp(ctx: CommandContext): Promise<NfspSession> {
|
|
32
|
+
const session = ctx.session
|
|
33
|
+
if (!session) throw new ToolError('AUTH_REQUIRED', 'authenticated session is required', 3)
|
|
34
|
+
const token = (await session.ensureValid()).token
|
|
35
|
+
const base = gatewayOrigin(ctx.connection.endpoint)
|
|
36
|
+
let seq = 0
|
|
37
|
+
const post = async (method: string, body: Record<string, unknown>) => {
|
|
38
|
+
const response = await fetch(`${base}/nfs/v1/${method}`, {
|
|
39
|
+
method: 'POST',
|
|
40
|
+
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` },
|
|
41
|
+
body: JSON.stringify(body),
|
|
42
|
+
signal: ctx.signal,
|
|
43
|
+
})
|
|
44
|
+
let parsed: { ok?: boolean; result?: unknown; error?: Record<string, unknown> }
|
|
45
|
+
try {
|
|
46
|
+
parsed = await response.json()
|
|
47
|
+
} catch {
|
|
48
|
+
throw new ToolError('INVALID_SERVICE_RESPONSE', `file service answered HTTP ${response.status}`, EXIT_INTERNAL)
|
|
49
|
+
}
|
|
50
|
+
if (parsed.ok !== true) {
|
|
51
|
+
const error = parsed.error ?? {}
|
|
52
|
+
const reason = typeof error.reason === 'string' ? error.reason.toUpperCase() : String(error.code ?? 'INTERNAL')
|
|
53
|
+
throw new ToolError(`SHARE_${reason}`, String(error.message ?? 'share request failed'))
|
|
54
|
+
}
|
|
55
|
+
return expectObject(parsed.result, `${method} result`)
|
|
56
|
+
}
|
|
57
|
+
const hello = await post('hello', { args: { versions: ['nfsp/0'] } })
|
|
58
|
+
const sessionId = String(hello.session ?? '')
|
|
59
|
+
if (!Array.isArray(hello.features) || !hello.features.includes('share')) {
|
|
60
|
+
throw new ToolError('SHARE_UNSUPPORTED', 'the file service of this Zone does not support sharing')
|
|
61
|
+
}
|
|
62
|
+
return {
|
|
63
|
+
call: (method, args, write = false) =>
|
|
64
|
+
post(method, { session: sessionId, args: { ...args, source_hint: 'cli' }, ...(write ? { seq: ++seq } : {}) }),
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function gatewayOrigin(endpoint: string): string {
|
|
69
|
+
const url = new URL(endpoint)
|
|
70
|
+
const marker = url.pathname.indexOf('/kapi')
|
|
71
|
+
url.pathname = marker >= 0 ? url.pathname.slice(0, marker) || '/' : '/'
|
|
72
|
+
url.search = ''
|
|
73
|
+
url.hash = ''
|
|
74
|
+
return url.toString().replace(/\/$/, '')
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** `--expires` → unix seconds, `null` for no expiry, `undefined` for the server default (7 days). */
|
|
78
|
+
export function parseExpires(value: unknown): number | null | undefined {
|
|
79
|
+
if (value === undefined) return undefined
|
|
80
|
+
const text = String(value).trim().toLowerCase()
|
|
81
|
+
if (text === 'none' || text === 'never') return null
|
|
82
|
+
const days = text.match(/^(\d+)d$/)
|
|
83
|
+
if (days) return Math.floor((Date.now() + Number(days[1]) * DAY_MS) / 1000)
|
|
84
|
+
const at = parseTimestamp(value, 'expires')
|
|
85
|
+
if (at === undefined) return undefined
|
|
86
|
+
if (at <= Date.now()) throw new UsageError('INVALID_ARGUMENT', 'expires must be in the future')
|
|
87
|
+
return Math.floor(at / 1000)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function sourceLocator(path: string): Record<string, unknown> {
|
|
91
|
+
if (path.startsWith('cyfs://')) return { uri: path }
|
|
92
|
+
return { path: path.startsWith('/') ? path : `/${path}` }
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function summary(share: Record<string, unknown>): Record<string, unknown> {
|
|
96
|
+
const keep = ['share_id', 'kind', 'title', 'status', 'access', 'expires_at', 'url', 'target_app', 'delivery_state', 'source_path', 'revision', 'created_at', 'stats']
|
|
97
|
+
return Object.fromEntries(keep.filter((key) => share[key] !== undefined).map((key) => [key, share[key]]))
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function createCommand(): CommandDefinition {
|
|
101
|
+
return {
|
|
102
|
+
verb: 'create',
|
|
103
|
+
summary: 'Share a file or folder as a link (a snapshot of its current content)',
|
|
104
|
+
positionals: [{ name: 'path', description: 'File or folder, e.g. /home/alice/report.pdf or cyfs:///home/alice/report.pdf', required: true }],
|
|
105
|
+
options: [
|
|
106
|
+
{ name: 'expires', description: `Link expiry: ${EXPIRES_HELP}`, type: 'string' },
|
|
107
|
+
{ name: 'password', description: 'Require this password to open the link', type: 'string' },
|
|
108
|
+
{ name: 'title', description: 'Title shown to visitors', type: 'string' },
|
|
109
|
+
{ name: 'no-download', property: 'no_download', description: 'Preview only, no download button', type: 'boolean' },
|
|
110
|
+
{ name: 'wait', description: 'Wait for a large snapshot to finish', type: 'boolean' },
|
|
111
|
+
],
|
|
112
|
+
inputSchema: {
|
|
113
|
+
type: 'object',
|
|
114
|
+
properties: {
|
|
115
|
+
path: { type: 'string', minLength: 1 },
|
|
116
|
+
expires: { type: 'string', minLength: 1 },
|
|
117
|
+
password: { type: 'string', minLength: 1 },
|
|
118
|
+
title: { type: 'string', minLength: 1 },
|
|
119
|
+
no_download: { type: 'boolean' },
|
|
120
|
+
wait: { type: 'boolean' },
|
|
121
|
+
},
|
|
122
|
+
required: ['path'],
|
|
123
|
+
additionalProperties: false,
|
|
124
|
+
},
|
|
125
|
+
outputSchema: OBJECT_OUTPUT,
|
|
126
|
+
resultSchemaVersion: 1,
|
|
127
|
+
access: { mode: 'fixed', level: 'write' },
|
|
128
|
+
asyncMode: 'either',
|
|
129
|
+
requiresSession: true,
|
|
130
|
+
examples: [
|
|
131
|
+
'buckyos share create /home/alice/report.pdf',
|
|
132
|
+
'buckyos share create /home/alice/photos --expires 30d --password s3cret',
|
|
133
|
+
],
|
|
134
|
+
handler: async (ctx, input) => {
|
|
135
|
+
const nfsp = await openNfsp(ctx)
|
|
136
|
+
const expires = parseExpires(input.expires)
|
|
137
|
+
const password = inputString(input, 'password')
|
|
138
|
+
const policy: Record<string, unknown> = {
|
|
139
|
+
access: password ? 'password' : 'holder',
|
|
140
|
+
allow_download: input.no_download !== true,
|
|
141
|
+
...(password ? { password } : {}),
|
|
142
|
+
...(expires !== undefined ? { expires_at: expires } : {}),
|
|
143
|
+
}
|
|
144
|
+
let op = await nfsp.call('share.create', {
|
|
145
|
+
source: sourceLocator(requiredInputString(input, 'path')),
|
|
146
|
+
policy,
|
|
147
|
+
title: inputString(input, 'title'),
|
|
148
|
+
idempotency_key: ctx.idempotencyKey ?? crypto.randomUUID(),
|
|
149
|
+
}, true)
|
|
150
|
+
while (input.wait === true && (op.state === 'running' || op.state === 'preparing')) {
|
|
151
|
+
await new Promise((resolve) => setTimeout(resolve, 1000))
|
|
152
|
+
ctx.signal.throwIfAborted()
|
|
153
|
+
op = await nfsp.call('share.operation', { operation_id: op.operation_id })
|
|
154
|
+
}
|
|
155
|
+
if (op.state === 'failed' || op.state === 'canceled') {
|
|
156
|
+
const error = (op.error ?? {}) as Record<string, unknown>
|
|
157
|
+
throw new ToolError(`SHARE_${String(error.reason ?? op.state).toUpperCase()}`, String(error.message ?? `share ${op.state}`))
|
|
158
|
+
}
|
|
159
|
+
return {
|
|
160
|
+
operation_id: op.operation_id,
|
|
161
|
+
state: op.state,
|
|
162
|
+
share_id: op.share_id ?? null,
|
|
163
|
+
url: op.url ?? null,
|
|
164
|
+
source_changed: op.source_changed ?? false,
|
|
165
|
+
...(op.share ? { share: summary(op.share as Record<string, unknown>) } : {}),
|
|
166
|
+
}
|
|
167
|
+
},
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function listCommand(): CommandDefinition {
|
|
172
|
+
return {
|
|
173
|
+
verb: 'list',
|
|
174
|
+
summary: 'List your shares',
|
|
175
|
+
options: [
|
|
176
|
+
{ name: 'kind', description: 'link or app_delivery', type: 'string', enum: ['link', 'app_delivery'] },
|
|
177
|
+
{ name: 'status', description: 'active, stopped, expired, ...', type: 'string' },
|
|
178
|
+
{ name: 'q', description: 'Search title and path', type: 'string' },
|
|
179
|
+
{ name: 'include-hidden', property: 'include_hidden', description: 'Include hidden shares', type: 'boolean' },
|
|
180
|
+
{ name: 'cursor', description: 'Page cursor', type: 'string' },
|
|
181
|
+
{ name: 'limit', description: 'Page size, at most 200', type: 'integer' },
|
|
182
|
+
],
|
|
183
|
+
inputSchema: {
|
|
184
|
+
type: 'object',
|
|
185
|
+
properties: {
|
|
186
|
+
kind: { type: 'string', enum: ['link', 'app_delivery'] },
|
|
187
|
+
status: { type: 'string', minLength: 1 },
|
|
188
|
+
q: { type: 'string', minLength: 1 },
|
|
189
|
+
include_hidden: { type: 'boolean' },
|
|
190
|
+
cursor: { type: 'string', minLength: 1 },
|
|
191
|
+
limit: { type: 'integer', minimum: 1 },
|
|
192
|
+
},
|
|
193
|
+
additionalProperties: false,
|
|
194
|
+
},
|
|
195
|
+
outputSchema: OBJECT_OUTPUT,
|
|
196
|
+
resultSchemaVersion: 1,
|
|
197
|
+
access: { mode: 'fixed', level: 'read' },
|
|
198
|
+
asyncMode: 'sync',
|
|
199
|
+
requiresSession: true,
|
|
200
|
+
examples: ['buckyos share list --kind link --status active'],
|
|
201
|
+
handler: async (ctx, input) => {
|
|
202
|
+
const nfsp = await openNfsp(ctx)
|
|
203
|
+
const page = await nfsp.call('share.list', {
|
|
204
|
+
kind: input.kind,
|
|
205
|
+
status: input.status,
|
|
206
|
+
q: input.q,
|
|
207
|
+
include_hidden: input.include_hidden === true,
|
|
208
|
+
cursor: input.cursor,
|
|
209
|
+
limit: input.limit,
|
|
210
|
+
})
|
|
211
|
+
const items = Array.isArray(page.items) ? page.items as Record<string, unknown>[] : []
|
|
212
|
+
return { items: items.map(summary), next_cursor: page.next_cursor ?? null }
|
|
213
|
+
},
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function getCommand(): CommandDefinition {
|
|
218
|
+
return {
|
|
219
|
+
verb: 'get',
|
|
220
|
+
summary: 'Show one share: link, policy, snapshot, delivery, statistics and history',
|
|
221
|
+
positionals: [{ name: 'share_id', description: 'Share ID (sh_…)', required: true }],
|
|
222
|
+
inputSchema: {
|
|
223
|
+
type: 'object',
|
|
224
|
+
properties: { share_id: { type: 'string', minLength: 1 } },
|
|
225
|
+
required: ['share_id'],
|
|
226
|
+
additionalProperties: false,
|
|
227
|
+
},
|
|
228
|
+
outputSchema: OBJECT_OUTPUT,
|
|
229
|
+
resultSchemaVersion: 1,
|
|
230
|
+
access: { mode: 'fixed', level: 'read' },
|
|
231
|
+
asyncMode: 'sync',
|
|
232
|
+
requiresSession: true,
|
|
233
|
+
examples: ['buckyos share get sh_0123456789abcdef'],
|
|
234
|
+
handler: async (ctx, input) => {
|
|
235
|
+
const nfsp = await openNfsp(ctx)
|
|
236
|
+
return await nfsp.call('share.get', { share_id: requiredInputString(input, 'share_id') })
|
|
237
|
+
},
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function updateCommand(): CommandDefinition {
|
|
242
|
+
return {
|
|
243
|
+
verb: 'update',
|
|
244
|
+
summary: 'Change a link\'s expiry, password, title or download setting',
|
|
245
|
+
positionals: [{ name: 'share_id', description: 'Share ID (sh_…)', required: true }],
|
|
246
|
+
options: [
|
|
247
|
+
{ name: 'expires', description: `New expiry: ${EXPIRES_HELP}`, type: 'string' },
|
|
248
|
+
{ name: 'password', description: 'Set or change the password', type: 'string' },
|
|
249
|
+
{ name: 'no-password', property: 'no_password', description: 'Remove the password', type: 'boolean' },
|
|
250
|
+
{ name: 'title', description: 'New title', type: 'string' },
|
|
251
|
+
{ name: 'download', description: 'Allow (true) or forbid (false) downloading', type: 'boolean' },
|
|
252
|
+
],
|
|
253
|
+
inputSchema: {
|
|
254
|
+
type: 'object',
|
|
255
|
+
properties: {
|
|
256
|
+
share_id: { type: 'string', minLength: 1 },
|
|
257
|
+
expires: { type: 'string', minLength: 1 },
|
|
258
|
+
password: { type: 'string', minLength: 1 },
|
|
259
|
+
no_password: { type: 'boolean' },
|
|
260
|
+
title: { type: 'string', minLength: 1 },
|
|
261
|
+
download: { type: 'boolean' },
|
|
262
|
+
},
|
|
263
|
+
required: ['share_id'],
|
|
264
|
+
additionalProperties: false,
|
|
265
|
+
},
|
|
266
|
+
outputSchema: OBJECT_OUTPUT,
|
|
267
|
+
resultSchemaVersion: 1,
|
|
268
|
+
access: { mode: 'fixed', level: 'write' },
|
|
269
|
+
asyncMode: 'sync',
|
|
270
|
+
requiresSession: true,
|
|
271
|
+
examples: ['buckyos share update sh_0123456789abcdef --expires none', 'buckyos share update sh_0123456789abcdef --password n3w'],
|
|
272
|
+
handler: async (ctx, input) => {
|
|
273
|
+
if (input.password !== undefined && input.no_password === true) {
|
|
274
|
+
throw new UsageError('INVALID_ARGUMENT', 'use either --password or --no-password')
|
|
275
|
+
}
|
|
276
|
+
const changes: Record<string, unknown> = {}
|
|
277
|
+
const expires = parseExpires(input.expires)
|
|
278
|
+
if (expires !== undefined) changes.expires_at = expires
|
|
279
|
+
if (input.password !== undefined) changes.password = String(input.password)
|
|
280
|
+
if (input.no_password === true) changes.password = null
|
|
281
|
+
if (input.title !== undefined) changes.title = String(input.title)
|
|
282
|
+
if (input.download !== undefined) changes.allow_download = input.download === true
|
|
283
|
+
if (!Object.keys(changes).length) throw new UsageError('INVALID_ARGUMENT', 'nothing to change')
|
|
284
|
+
const nfsp = await openNfsp(ctx)
|
|
285
|
+
const shareId = requiredInputString(input, 'share_id')
|
|
286
|
+
const current = await nfsp.call('share.get', { share_id: shareId })
|
|
287
|
+
const result = await nfsp.call('share.update', { share_id: shareId, revision: current.revision, changes }, true)
|
|
288
|
+
return { revision: result.revision, share: summary(expectObject(result.share, 'share.update share')) }
|
|
289
|
+
},
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function stopCommand(): CommandDefinition {
|
|
294
|
+
return {
|
|
295
|
+
verb: 'stop',
|
|
296
|
+
summary: 'Stop a share: the link stops working and the kept snapshot is released',
|
|
297
|
+
positionals: [{ name: 'share_id', description: 'Share ID (sh_…)', required: true }],
|
|
298
|
+
options: [{ name: 'reason', description: 'Recorded in the share history', type: 'string' }],
|
|
299
|
+
inputSchema: {
|
|
300
|
+
type: 'object',
|
|
301
|
+
properties: { share_id: { type: 'string', minLength: 1 }, reason: { type: 'string', minLength: 1 } },
|
|
302
|
+
required: ['share_id'],
|
|
303
|
+
additionalProperties: false,
|
|
304
|
+
},
|
|
305
|
+
outputSchema: OBJECT_OUTPUT,
|
|
306
|
+
resultSchemaVersion: 1,
|
|
307
|
+
access: { mode: 'fixed', level: 'destructive' },
|
|
308
|
+
asyncMode: 'sync',
|
|
309
|
+
requiresSession: true,
|
|
310
|
+
examples: ['buckyos share stop sh_0123456789abcdef --reason "sent by mistake"'],
|
|
311
|
+
handler: async (ctx, input) => {
|
|
312
|
+
const nfsp = await openNfsp(ctx)
|
|
313
|
+
const shareId = requiredInputString(input, 'share_id')
|
|
314
|
+
const current = await nfsp.call('share.get', { share_id: shareId })
|
|
315
|
+
return await nfsp.call('share.stop', { share_id: shareId, revision: current.revision, reason: inputString(input, 'reason') }, true)
|
|
316
|
+
},
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function statsCommand(): CommandDefinition {
|
|
321
|
+
return {
|
|
322
|
+
verb: 'stats',
|
|
323
|
+
summary: 'Views, downloads, traffic and visitors of one share or all of yours',
|
|
324
|
+
options: [
|
|
325
|
+
{ name: 'share-id', property: 'share_id', description: 'One share instead of all', type: 'string' },
|
|
326
|
+
{ name: 'days', description: 'How many days back (default 30)', type: 'integer' },
|
|
327
|
+
{ name: 'granularity', description: 'day or hour', type: 'string', enum: ['day', 'hour'] },
|
|
328
|
+
],
|
|
329
|
+
inputSchema: {
|
|
330
|
+
type: 'object',
|
|
331
|
+
properties: {
|
|
332
|
+
share_id: { type: 'string', minLength: 1 },
|
|
333
|
+
days: { type: 'integer', minimum: 1 },
|
|
334
|
+
granularity: { type: 'string', enum: ['day', 'hour'] },
|
|
335
|
+
},
|
|
336
|
+
additionalProperties: false,
|
|
337
|
+
},
|
|
338
|
+
outputSchema: OBJECT_OUTPUT,
|
|
339
|
+
resultSchemaVersion: 1,
|
|
340
|
+
access: { mode: 'fixed', level: 'read' },
|
|
341
|
+
asyncMode: 'sync',
|
|
342
|
+
requiresSession: true,
|
|
343
|
+
examples: ['buckyos share stats --days 7', 'buckyos share stats --share-id sh_0123456789abcdef --granularity hour --days 2'],
|
|
344
|
+
handler: async (ctx, input) => {
|
|
345
|
+
const nfsp = await openNfsp(ctx)
|
|
346
|
+
const to = Math.floor(Date.now() / 1000) + 1
|
|
347
|
+
const days = typeof input.days === 'number' ? input.days : 30
|
|
348
|
+
return await nfsp.call('share.stats', {
|
|
349
|
+
share_id: inputString(input, 'share_id'),
|
|
350
|
+
from: to - days * 86_400,
|
|
351
|
+
to,
|
|
352
|
+
granularity: input.granularity ?? 'day',
|
|
353
|
+
})
|
|
354
|
+
},
|
|
355
|
+
}
|
|
356
|
+
}
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
import type { CommandModule, JsonSchema, OptionDefinition } from '../core/command.ts'
|
|
2
|
+
import type { CommandContext } from '../core/context.ts'
|
|
3
|
+
import { EXIT_INTERNAL, EXIT_PERMISSION, ToolError, UsageError } from '../core/errors.ts'
|
|
4
|
+
import { callService, requiredInputString } from '../core/service.ts'
|
|
5
|
+
import { getHost } from '../runtime/host.ts'
|
|
6
|
+
import { openZip, readTarGz, readZipEntry, sha256Bytes } from './pikg_archive.ts'
|
|
7
|
+
import { inspectPikg } from './pikg_protocol.ts'
|
|
8
|
+
|
|
9
|
+
const OBJECT_OUTPUT: JsonSchema = { type: 'object', additionalProperties: true }
|
|
10
|
+
const SKILL_KINDS = ['cmd', 'goal']
|
|
11
|
+
const SKILLS_SUB_PKG = 'agent_skills'
|
|
12
|
+
const SKILL_DIRS = ['cmd_skills', 'goal_skills', 'tools']
|
|
13
|
+
const MAX_SKILL_PAYLOAD_BYTES = 64 * 1024 * 1024
|
|
14
|
+
const AGENT_ID = /^[A-Za-z0-9][A-Za-z0-9._-]*$/
|
|
15
|
+
|
|
16
|
+
const AGENT_OPTION: OptionDefinition = {
|
|
17
|
+
name: 'agent',
|
|
18
|
+
property: 'agent_id',
|
|
19
|
+
description: 'Agent ID; the Agent serves kRPC at /kapi/<agent_id>',
|
|
20
|
+
type: 'string',
|
|
21
|
+
required: true,
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const KIND_OPTION: OptionDefinition = {
|
|
25
|
+
name: 'kind',
|
|
26
|
+
description: 'Skill kind',
|
|
27
|
+
type: 'string',
|
|
28
|
+
enum: SKILL_KINDS,
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function createSkillModule(): CommandModule {
|
|
32
|
+
return {
|
|
33
|
+
name: 'skill',
|
|
34
|
+
summary: 'List, install, and remove the cmd/goal skills of an Agent',
|
|
35
|
+
commands: [
|
|
36
|
+
{
|
|
37
|
+
verb: 'list',
|
|
38
|
+
summary: 'List the skills of an Agent',
|
|
39
|
+
options: [AGENT_OPTION, KIND_OPTION],
|
|
40
|
+
inputSchema: {
|
|
41
|
+
type: 'object',
|
|
42
|
+
properties: {
|
|
43
|
+
agent_id: { type: 'string', minLength: 1 },
|
|
44
|
+
kind: { type: 'string', enum: SKILL_KINDS },
|
|
45
|
+
},
|
|
46
|
+
required: ['agent_id'],
|
|
47
|
+
additionalProperties: false,
|
|
48
|
+
},
|
|
49
|
+
outputSchema: OBJECT_OUTPUT,
|
|
50
|
+
resultSchemaVersion: 1,
|
|
51
|
+
access: { mode: 'fixed', level: 'read' },
|
|
52
|
+
asyncMode: 'sync',
|
|
53
|
+
requiresSession: true,
|
|
54
|
+
examples: [
|
|
55
|
+
'buckyos skill list --agent jarvis.example.zone',
|
|
56
|
+
'buckyos skill list --agent jarvis.example.zone --kind cmd',
|
|
57
|
+
],
|
|
58
|
+
handler: async (ctx, input) => {
|
|
59
|
+
const agentId = agentIdFrom(input)
|
|
60
|
+
const kind = typeof input.kind === 'string' ? input.kind : undefined
|
|
61
|
+
const skills = await listSkills(ctx, agentId, kind)
|
|
62
|
+
return { agent_id: agentId, ...(kind ? { kind } : {}), total: skills.length, skills }
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
verb: 'install',
|
|
67
|
+
summary: 'Install the skills of a skill PIKG into an Agent',
|
|
68
|
+
description:
|
|
69
|
+
'Verifies the PIKG offline, requires app_type agent_skills, and sends the files of its agent_skills sub package (cmd_skills/, goal_skills/, tools/) to the Agent. Sessions created before keep their frozen manuals.',
|
|
70
|
+
positionals: [{ name: 'pikg_path', description: 'Local skill .pikg file' }],
|
|
71
|
+
options: [
|
|
72
|
+
AGENT_OPTION,
|
|
73
|
+
{
|
|
74
|
+
name: 'replace',
|
|
75
|
+
description: 'Replace skills of the same names from another package',
|
|
76
|
+
type: 'boolean',
|
|
77
|
+
},
|
|
78
|
+
],
|
|
79
|
+
inputSchema: {
|
|
80
|
+
type: 'object',
|
|
81
|
+
properties: {
|
|
82
|
+
pikg_path: { type: 'string', minLength: 1 },
|
|
83
|
+
agent_id: { type: 'string', minLength: 1 },
|
|
84
|
+
replace: { type: 'boolean' },
|
|
85
|
+
},
|
|
86
|
+
required: ['pikg_path', 'agent_id'],
|
|
87
|
+
additionalProperties: false,
|
|
88
|
+
},
|
|
89
|
+
outputSchema: OBJECT_OUTPUT,
|
|
90
|
+
resultSchemaVersion: 1,
|
|
91
|
+
access: { mode: 'fixed', level: 'write' },
|
|
92
|
+
asyncMode: 'sync',
|
|
93
|
+
requiresSession: true,
|
|
94
|
+
examples: [
|
|
95
|
+
'buckyos skill install ./dapp_dist/hello-skills-0.1.0.pikg --agent jarvis.example.zone',
|
|
96
|
+
'buckyos skill install ./hello-skills-0.2.0.pikg --agent jarvis.example.zone --replace',
|
|
97
|
+
],
|
|
98
|
+
handler: async (ctx, input) => await installSkills(ctx, input),
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
verb: 'remove',
|
|
102
|
+
summary: 'Remove an installed skill from an Agent',
|
|
103
|
+
description:
|
|
104
|
+
'Only installed skills can be removed. Without --kind the kind is looked up by name.',
|
|
105
|
+
positionals: [{ name: 'name', description: 'Skill name' }],
|
|
106
|
+
options: [AGENT_OPTION, KIND_OPTION],
|
|
107
|
+
inputSchema: {
|
|
108
|
+
type: 'object',
|
|
109
|
+
properties: {
|
|
110
|
+
name: { type: 'string', minLength: 1 },
|
|
111
|
+
agent_id: { type: 'string', minLength: 1 },
|
|
112
|
+
kind: { type: 'string', enum: SKILL_KINDS },
|
|
113
|
+
},
|
|
114
|
+
required: ['name', 'agent_id'],
|
|
115
|
+
additionalProperties: false,
|
|
116
|
+
},
|
|
117
|
+
outputSchema: OBJECT_OUTPUT,
|
|
118
|
+
resultSchemaVersion: 1,
|
|
119
|
+
access: { mode: 'fixed', level: 'destructive' },
|
|
120
|
+
asyncMode: 'sync',
|
|
121
|
+
requiresSession: true,
|
|
122
|
+
examples: ['buckyos --yes skill remove hello --agent jarvis.example.zone --kind cmd'],
|
|
123
|
+
handler: async (ctx, input) => await removeSkill(ctx, input),
|
|
124
|
+
},
|
|
125
|
+
],
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async function installSkills(
|
|
130
|
+
ctx: CommandContext,
|
|
131
|
+
input: Record<string, unknown>,
|
|
132
|
+
): Promise<Record<string, unknown>> {
|
|
133
|
+
const agentId = agentIdFrom(input)
|
|
134
|
+
const raw = requiredInputString(input, 'pikg_path')
|
|
135
|
+
const path = getHost().path.isAbsolute(raw)
|
|
136
|
+
? getHost().path.resolve(raw)
|
|
137
|
+
: getHost().path.resolve(ctx.cwd, raw)
|
|
138
|
+
const inspection = await inspectPikg(path)
|
|
139
|
+
if (inspection.app.app_type !== SKILLS_SUB_PKG) {
|
|
140
|
+
throw new UsageError(
|
|
141
|
+
'NOT_A_SKILL_PACKAGE',
|
|
142
|
+
`PIKG app_type is ${inspection.app.app_type}; a skill package has app_type ${SKILLS_SUB_PKG} (buckyos pikg init --kind agent-skills)`,
|
|
143
|
+
)
|
|
144
|
+
}
|
|
145
|
+
const payload = inspection.subpackages.find((item) => item.key === SKILLS_SUB_PKG)
|
|
146
|
+
?.payload as { path: string; digest: string }
|
|
147
|
+
const archive = await openZip(path)
|
|
148
|
+
const bytes = await readZipEntry(
|
|
149
|
+
archive,
|
|
150
|
+
archive.byName.get(payload.path)!,
|
|
151
|
+
MAX_SKILL_PAYLOAD_BYTES,
|
|
152
|
+
)
|
|
153
|
+
if (`sha256:${sha256Bytes(bytes)}` !== payload.digest) {
|
|
154
|
+
throw new ToolError('INVALID_PACKAGE', 'PIKG changed after it was verified')
|
|
155
|
+
}
|
|
156
|
+
const files: Record<string, { content: string; executable: boolean }> = {}
|
|
157
|
+
const ignored: string[] = []
|
|
158
|
+
for (const entry of await readTarGz(bytes, MAX_SKILL_PAYLOAD_BYTES)) {
|
|
159
|
+
if (!SKILL_DIRS.includes(entry.path.split('/')[0])) {
|
|
160
|
+
ignored.push(entry.path)
|
|
161
|
+
continue
|
|
162
|
+
}
|
|
163
|
+
files[entry.path] = { content: base64(entry.bytes), executable: entry.executable }
|
|
164
|
+
}
|
|
165
|
+
const result = await callService<unknown>(ctx, agentId, 'skills.install', {
|
|
166
|
+
package: {
|
|
167
|
+
package: inspection.app.name,
|
|
168
|
+
version: inspection.app.version,
|
|
169
|
+
pikg_digest: inspection.pikg_digest,
|
|
170
|
+
files,
|
|
171
|
+
},
|
|
172
|
+
replace: input.replace === true,
|
|
173
|
+
})
|
|
174
|
+
return {
|
|
175
|
+
agent_id: agentId,
|
|
176
|
+
pikg_path: inspection.pikg_path,
|
|
177
|
+
pikg_digest: inspection.pikg_digest,
|
|
178
|
+
...expectObject(result, 'skills.install'),
|
|
179
|
+
...(ignored.length ? { ignored_files: ignored } : {}),
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
async function removeSkill(
|
|
184
|
+
ctx: CommandContext,
|
|
185
|
+
input: Record<string, unknown>,
|
|
186
|
+
): Promise<Record<string, unknown>> {
|
|
187
|
+
const agentId = agentIdFrom(input)
|
|
188
|
+
const name = requiredInputString(input, 'name')
|
|
189
|
+
let kind = typeof input.kind === 'string' ? input.kind : undefined
|
|
190
|
+
if (!kind) {
|
|
191
|
+
const kinds = (await listSkills(ctx, agentId)).filter((skill) => skill.name === name)
|
|
192
|
+
.map((skill) => String(skill.kind))
|
|
193
|
+
if (kinds.length === 0) {
|
|
194
|
+
throw new ToolError('RESOURCE_NOT_FOUND', `Agent ${agentId} has no skill named ${name}`)
|
|
195
|
+
}
|
|
196
|
+
if (kinds.length > 1) {
|
|
197
|
+
throw new UsageError('AMBIGUOUS_SKILL', `${name} is both a cmd and a goal skill; use --kind`)
|
|
198
|
+
}
|
|
199
|
+
kind = kinds[0]
|
|
200
|
+
}
|
|
201
|
+
if (!ctx.confirmed) {
|
|
202
|
+
if (ctx.config.nonInteractive || !ctx.io.inputIsTerminal) {
|
|
203
|
+
throw new ToolError(
|
|
204
|
+
'CONFIRMATION_REQUIRED',
|
|
205
|
+
'skill remove requires --yes in non-interactive execution',
|
|
206
|
+
EXIT_PERMISSION,
|
|
207
|
+
)
|
|
208
|
+
}
|
|
209
|
+
const answer = (await ctx.io.prompt(`Remove ${kind} skill ${name} from ${agentId}? [y/N] `))
|
|
210
|
+
?.trim().toLowerCase()
|
|
211
|
+
if (answer !== 'y' && answer !== 'yes') {
|
|
212
|
+
throw new ToolError('CONFIRMATION_DECLINED', 'skill remove was declined', EXIT_PERMISSION)
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
const removed = await callService<unknown>(ctx, agentId, 'skills.remove', { kind, name })
|
|
216
|
+
return { agent_id: agentId, removed: expectObject(removed, 'skills.remove') }
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
async function listSkills(
|
|
220
|
+
ctx: CommandContext,
|
|
221
|
+
agentId: string,
|
|
222
|
+
kind?: string,
|
|
223
|
+
): Promise<Record<string, unknown>[]> {
|
|
224
|
+
const skills = await callService<unknown>(ctx, agentId, 'skills.list', kind ? { kind } : {})
|
|
225
|
+
if (!Array.isArray(skills)) invalidResponse('skills.list')
|
|
226
|
+
return (skills as unknown[]).map((skill) => expectObject(skill, 'skills.list'))
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function agentIdFrom(input: Record<string, unknown>): string {
|
|
230
|
+
const agentId = requiredInputString(input, 'agent_id')
|
|
231
|
+
if (!AGENT_ID.test(agentId)) {
|
|
232
|
+
throw new UsageError('INVALID_AGENT_ID', `invalid Agent ID: ${agentId}`)
|
|
233
|
+
}
|
|
234
|
+
return agentId
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function expectObject(value: unknown, method: string): Record<string, unknown> {
|
|
238
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) invalidResponse(method)
|
|
239
|
+
return value as Record<string, unknown>
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function invalidResponse(method: string): never {
|
|
243
|
+
throw new ToolError(
|
|
244
|
+
'INVALID_SERVICE_RESPONSE',
|
|
245
|
+
`Agent returned an invalid response for ${method}`,
|
|
246
|
+
EXIT_INTERNAL,
|
|
247
|
+
)
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function base64(bytes: Uint8Array): string {
|
|
251
|
+
let binary = ''
|
|
252
|
+
for (let index = 0; index < bytes.length; index += 0x8000) {
|
|
253
|
+
binary += String.fromCharCode(...bytes.subarray(index, index + 0x8000))
|
|
254
|
+
}
|
|
255
|
+
return btoa(binary)
|
|
256
|
+
}
|