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
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import type { CommandModule, JsonSchema, PositionalDefinition } from '../core/command.ts'
|
|
2
|
+
import type { CommandContext } from '../core/context.ts'
|
|
3
|
+
import { EXIT_INTERNAL, ToolError, UsageError } from '../core/errors.ts'
|
|
4
|
+
import { callService, expectObject, requiredInputString } from '../core/service.ts'
|
|
5
|
+
|
|
6
|
+
const CONTROL_PANEL = 'control-panel'
|
|
7
|
+
const OBJECT_OUTPUT: JsonSchema = { type: 'object', additionalProperties: true }
|
|
8
|
+
const AGENT_POSITIONAL: PositionalDefinition = {
|
|
9
|
+
name: 'agent_id',
|
|
10
|
+
description: 'Agent ID as `agent list` prints it (raw host name, e.g. jarvis.example.zone)',
|
|
11
|
+
}
|
|
12
|
+
const AGENT_INPUT: JsonSchema = {
|
|
13
|
+
type: 'object',
|
|
14
|
+
properties: { agent_id: { type: 'string', minLength: 1 } },
|
|
15
|
+
required: ['agent_id'],
|
|
16
|
+
additionalProperties: false,
|
|
17
|
+
}
|
|
18
|
+
const PROFILE_FIELDS = ['display_name', 'bio']
|
|
19
|
+
|
|
20
|
+
// Control Panel answers for Agents the caller owns; admins (and the device identity) see all.
|
|
21
|
+
export function createAgentModule(): CommandModule {
|
|
22
|
+
return {
|
|
23
|
+
name: 'agent',
|
|
24
|
+
summary: 'List Agents and read or change their profiles',
|
|
25
|
+
commands: [
|
|
26
|
+
{
|
|
27
|
+
verb: 'list',
|
|
28
|
+
summary: 'List the Agents visible to the caller',
|
|
29
|
+
description: 'Owners see their own Agents; admins see every Agent in the Zone.',
|
|
30
|
+
inputSchema: { type: 'object', properties: {}, additionalProperties: false },
|
|
31
|
+
outputSchema: {
|
|
32
|
+
type: 'object',
|
|
33
|
+
properties: { agents: { type: 'array', items: OBJECT_OUTPUT } },
|
|
34
|
+
required: ['agents'],
|
|
35
|
+
additionalProperties: false,
|
|
36
|
+
},
|
|
37
|
+
resultSchemaVersion: 1,
|
|
38
|
+
access: { mode: 'fixed', level: 'read' },
|
|
39
|
+
asyncMode: 'sync',
|
|
40
|
+
requiresSession: true,
|
|
41
|
+
examples: ['buckyos agent list'],
|
|
42
|
+
handler: async (ctx) => {
|
|
43
|
+
const response = await callAgent(ctx, 'agent.list', {})
|
|
44
|
+
if (!Array.isArray(response.agents)) {
|
|
45
|
+
throw new ToolError(
|
|
46
|
+
'INVALID_SERVICE_RESPONSE',
|
|
47
|
+
'agent.list agents must be an array',
|
|
48
|
+
EXIT_INTERNAL,
|
|
49
|
+
)
|
|
50
|
+
}
|
|
51
|
+
return { agents: response.agents }
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
verb: 'get',
|
|
56
|
+
summary: 'Get one Agent: profile, settings, install state, template, and runtime',
|
|
57
|
+
positionals: [AGENT_POSITIONAL],
|
|
58
|
+
inputSchema: AGENT_INPUT,
|
|
59
|
+
outputSchema: OBJECT_OUTPUT,
|
|
60
|
+
resultSchemaVersion: 1,
|
|
61
|
+
access: { mode: 'fixed', level: 'read' },
|
|
62
|
+
asyncMode: 'sync',
|
|
63
|
+
requiresSession: true,
|
|
64
|
+
examples: ['buckyos agent get jarvis.example.zone'],
|
|
65
|
+
handler: async (ctx, input) =>
|
|
66
|
+
await callAgent(ctx, 'agent.get', { agent_id: requiredInputString(input, 'agent_id') }),
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
verb: 'profile-get',
|
|
70
|
+
summary: 'Get the public profile (display name, bio, avatar) of an Agent',
|
|
71
|
+
positionals: [AGENT_POSITIONAL],
|
|
72
|
+
inputSchema: AGENT_INPUT,
|
|
73
|
+
outputSchema: OBJECT_OUTPUT,
|
|
74
|
+
resultSchemaVersion: 1,
|
|
75
|
+
access: { mode: 'fixed', level: 'read' },
|
|
76
|
+
asyncMode: 'sync',
|
|
77
|
+
requiresSession: true,
|
|
78
|
+
examples: ['buckyos agent profile-get jarvis.example.zone'],
|
|
79
|
+
handler: async (ctx, input) => {
|
|
80
|
+
const agentId = requiredInputString(input, 'agent_id')
|
|
81
|
+
return {
|
|
82
|
+
agent_id: agentId,
|
|
83
|
+
...await callAgent(ctx, 'agent.profile.get', { agent_id: agentId }),
|
|
84
|
+
}
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
verb: 'profile-set',
|
|
89
|
+
summary: 'Change the display name or bio of an Agent',
|
|
90
|
+
description:
|
|
91
|
+
'Only the given fields change; an empty value clears the field. Returns the profile before and after the change, and Message Center reloads the new name.',
|
|
92
|
+
positionals: [AGENT_POSITIONAL],
|
|
93
|
+
options: [
|
|
94
|
+
{
|
|
95
|
+
name: 'display-name',
|
|
96
|
+
description: 'New display name; empty clears it',
|
|
97
|
+
type: 'string',
|
|
98
|
+
},
|
|
99
|
+
{ name: 'bio', description: 'New bio; empty clears it', type: 'string' },
|
|
100
|
+
],
|
|
101
|
+
inputSchema: {
|
|
102
|
+
type: 'object',
|
|
103
|
+
properties: {
|
|
104
|
+
agent_id: { type: 'string', minLength: 1 },
|
|
105
|
+
display_name: { type: 'string' },
|
|
106
|
+
bio: { type: 'string' },
|
|
107
|
+
},
|
|
108
|
+
required: ['agent_id'],
|
|
109
|
+
additionalProperties: false,
|
|
110
|
+
},
|
|
111
|
+
outputSchema: {
|
|
112
|
+
type: 'object',
|
|
113
|
+
properties: {
|
|
114
|
+
agent_id: { type: 'string' },
|
|
115
|
+
before: OBJECT_OUTPUT,
|
|
116
|
+
profile: OBJECT_OUTPUT,
|
|
117
|
+
},
|
|
118
|
+
required: ['agent_id', 'before', 'profile'],
|
|
119
|
+
additionalProperties: false,
|
|
120
|
+
},
|
|
121
|
+
resultSchemaVersion: 1,
|
|
122
|
+
access: { mode: 'fixed', level: 'write' },
|
|
123
|
+
asyncMode: 'sync',
|
|
124
|
+
requiresSession: true,
|
|
125
|
+
examples: [
|
|
126
|
+
"buckyos agent profile-set jarvis.example.zone --display-name 'Jarvis'",
|
|
127
|
+
"buckyos agent profile-set jarvis.example.zone --bio ''",
|
|
128
|
+
],
|
|
129
|
+
handler: async (ctx, input) => {
|
|
130
|
+
const agentId = requiredInputString(input, 'agent_id')
|
|
131
|
+
const changes = Object.fromEntries(
|
|
132
|
+
PROFILE_FIELDS.filter((field) => typeof input[field] === 'string')
|
|
133
|
+
.map((field) => [field, input[field]]),
|
|
134
|
+
)
|
|
135
|
+
if (Object.keys(changes).length === 0) {
|
|
136
|
+
throw new UsageError('INVALID_ARGUMENT', 'give --display-name or --bio')
|
|
137
|
+
}
|
|
138
|
+
const before = await callAgent(ctx, 'agent.profile.get', { agent_id: agentId })
|
|
139
|
+
const after = await callAgent(ctx, 'agent.profile.set', { agent_id: agentId, ...changes })
|
|
140
|
+
return {
|
|
141
|
+
agent_id: agentId,
|
|
142
|
+
before: expectObject(before.profile, 'agent.profile.get profile'),
|
|
143
|
+
profile: expectObject(after.profile, 'agent.profile.set profile'),
|
|
144
|
+
}
|
|
145
|
+
},
|
|
146
|
+
},
|
|
147
|
+
],
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
async function callAgent(
|
|
152
|
+
ctx: CommandContext,
|
|
153
|
+
method: string,
|
|
154
|
+
params: Record<string, unknown>,
|
|
155
|
+
): Promise<Record<string, unknown>> {
|
|
156
|
+
try {
|
|
157
|
+
return expectObject(await callService(ctx, CONTROL_PANEL, method, params), method)
|
|
158
|
+
} catch (error) {
|
|
159
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
160
|
+
if (message.includes('agent_not_found')) {
|
|
161
|
+
throw new ToolError('RESOURCE_NOT_FOUND', `Agent not found or not yours: ${params.agent_id}`)
|
|
162
|
+
}
|
|
163
|
+
throw error
|
|
164
|
+
}
|
|
165
|
+
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import type { CommandDefinition, CommandModule, JsonSchema } from '../core/command.ts'
|
|
2
|
+
import { UsageError } from '../core/errors.ts'
|
|
3
|
+
import {
|
|
4
|
+
callService,
|
|
5
|
+
expectObject,
|
|
6
|
+
inputString,
|
|
7
|
+
parseTimestamp,
|
|
8
|
+
splitServices,
|
|
9
|
+
} from '../core/service.ts'
|
|
10
|
+
|
|
11
|
+
const AICC = 'aicc'
|
|
12
|
+
const OBJECT_OUTPUT: JsonSchema = { type: 'object', additionalProperties: true }
|
|
13
|
+
const EMPTY_INPUT: JsonSchema = { type: 'object', properties: {}, additionalProperties: false }
|
|
14
|
+
const RANGES = ['last1d', 'last7d', 'last30d']
|
|
15
|
+
const GROUPS = [
|
|
16
|
+
'provider_instance_name',
|
|
17
|
+
'provider_model',
|
|
18
|
+
'request_model',
|
|
19
|
+
'method',
|
|
20
|
+
'capability',
|
|
21
|
+
'caller_app_id',
|
|
22
|
+
'user_id',
|
|
23
|
+
'tenant_id',
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
// Read views of the AICC service (/kapi/aicc); provider and routing changes stay in Control Panel.
|
|
27
|
+
export function createAiModule(): CommandModule {
|
|
28
|
+
return {
|
|
29
|
+
name: 'ai',
|
|
30
|
+
summary: 'Read AI provider, routing, and usage state of the AICC service',
|
|
31
|
+
commands: [
|
|
32
|
+
readCommand({
|
|
33
|
+
verb: 'providers',
|
|
34
|
+
summary: 'List configured AI provider instances with auth, health, and model inventory',
|
|
35
|
+
method: 'provider.list',
|
|
36
|
+
description:
|
|
37
|
+
'Credentials are reported only as configured/kind; secrets are never returned.',
|
|
38
|
+
}),
|
|
39
|
+
readCommand({
|
|
40
|
+
verb: 'routing',
|
|
41
|
+
summary: 'Get the AICC routing overlay and its settings revision',
|
|
42
|
+
method: 'routing.get',
|
|
43
|
+
description: 'An empty routing object means the default routing of the model catalog.',
|
|
44
|
+
}),
|
|
45
|
+
usageCommand(),
|
|
46
|
+
],
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function readCommand(options: {
|
|
51
|
+
verb: string
|
|
52
|
+
summary: string
|
|
53
|
+
method: string
|
|
54
|
+
description: string
|
|
55
|
+
}): CommandDefinition {
|
|
56
|
+
return {
|
|
57
|
+
verb: options.verb,
|
|
58
|
+
summary: options.summary,
|
|
59
|
+
description: options.description,
|
|
60
|
+
inputSchema: EMPTY_INPUT,
|
|
61
|
+
outputSchema: OBJECT_OUTPUT,
|
|
62
|
+
resultSchemaVersion: 1,
|
|
63
|
+
access: { mode: 'fixed', level: 'read' },
|
|
64
|
+
asyncMode: 'sync',
|
|
65
|
+
requiresSession: true,
|
|
66
|
+
examples: [`buckyos ai ${options.verb}`],
|
|
67
|
+
handler: async (ctx) =>
|
|
68
|
+
expectObject(await callService(ctx, AICC, options.method, {}), options.method),
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function usageCommand(): CommandDefinition {
|
|
73
|
+
return {
|
|
74
|
+
verb: 'usage',
|
|
75
|
+
summary: "Aggregate the caller's AI usage: requests, tokens, and cost",
|
|
76
|
+
description:
|
|
77
|
+
"Scoped to the caller's own user (a device identity sees only its own calls, not the Zone total). Without --since the range defaults to last7d; --until defaults to now.",
|
|
78
|
+
options: [
|
|
79
|
+
{
|
|
80
|
+
name: 'range',
|
|
81
|
+
description: 'last1d, last7d (default), or last30d',
|
|
82
|
+
type: 'string',
|
|
83
|
+
enum: RANGES,
|
|
84
|
+
},
|
|
85
|
+
{ name: 'since', description: 'RFC 3339 or Unix ms lower bound', type: 'string' },
|
|
86
|
+
{ name: 'until', description: 'RFC 3339 or Unix ms upper bound', type: 'string' },
|
|
87
|
+
{
|
|
88
|
+
name: 'group-by',
|
|
89
|
+
description: `Comma-separated: ${GROUPS.join(', ')}`,
|
|
90
|
+
type: 'string',
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
name: 'bucket',
|
|
94
|
+
description: 'Time buckets: hour or day',
|
|
95
|
+
type: 'string',
|
|
96
|
+
enum: ['hour', 'day'],
|
|
97
|
+
},
|
|
98
|
+
{ name: 'events', description: 'Also return the individual usage events', type: 'boolean' },
|
|
99
|
+
{ name: 'limit', description: 'Event page size', type: 'integer' },
|
|
100
|
+
{ name: 'cursor', description: 'Opaque event page cursor', type: 'string' },
|
|
101
|
+
],
|
|
102
|
+
inputSchema: {
|
|
103
|
+
type: 'object',
|
|
104
|
+
properties: {
|
|
105
|
+
range: { type: 'string', enum: RANGES },
|
|
106
|
+
since: { type: 'string', minLength: 1 },
|
|
107
|
+
until: { type: 'string', minLength: 1 },
|
|
108
|
+
group_by: { type: 'string', minLength: 1 },
|
|
109
|
+
bucket: { type: 'string', enum: ['hour', 'day'] },
|
|
110
|
+
events: { type: 'boolean' },
|
|
111
|
+
limit: { type: 'integer', minimum: 1 },
|
|
112
|
+
cursor: { type: 'string', minLength: 1 },
|
|
113
|
+
},
|
|
114
|
+
additionalProperties: false,
|
|
115
|
+
},
|
|
116
|
+
outputSchema: OBJECT_OUTPUT,
|
|
117
|
+
resultSchemaVersion: 1,
|
|
118
|
+
access: { mode: 'fixed', level: 'read' },
|
|
119
|
+
asyncMode: 'sync',
|
|
120
|
+
requiresSession: true,
|
|
121
|
+
examples: [
|
|
122
|
+
'buckyos ai usage --range last1d --group-by provider_instance_name,provider_model',
|
|
123
|
+
'buckyos ai usage --since 2026-10-01T00:00:00Z --bucket day',
|
|
124
|
+
],
|
|
125
|
+
handler: async (ctx, input) => {
|
|
126
|
+
const groupBy = splitServices(input.group_by)
|
|
127
|
+
const unknown = groupBy.filter((group) => !GROUPS.includes(group))
|
|
128
|
+
if (unknown.length > 0) {
|
|
129
|
+
throw new UsageError(
|
|
130
|
+
'INVALID_ARGUMENT',
|
|
131
|
+
`unknown group-by ${unknown.join(', ')}; use ${GROUPS.join(', ')}`,
|
|
132
|
+
)
|
|
133
|
+
}
|
|
134
|
+
return expectObject(
|
|
135
|
+
await callService(ctx, AICC, 'usage.query', {
|
|
136
|
+
time_range: timeRange(input),
|
|
137
|
+
...(groupBy.length > 0 ? { group_by: groupBy } : {}),
|
|
138
|
+
...(input.bucket ? { time_bucket: input.bucket } : {}),
|
|
139
|
+
output_mode: input.events === true ? 'summary_and_events' : 'summary',
|
|
140
|
+
...(input.limit !== undefined ? { limit: input.limit } : {}),
|
|
141
|
+
...(inputString(input, 'cursor') ? { cursor: inputString(input, 'cursor') } : {}),
|
|
142
|
+
}),
|
|
143
|
+
'usage.query',
|
|
144
|
+
)
|
|
145
|
+
},
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function timeRange(input: Record<string, unknown>): Record<string, unknown> {
|
|
150
|
+
const since = parseTimestamp(input.since, 'since')
|
|
151
|
+
const until = parseTimestamp(input.until, 'until')
|
|
152
|
+
if (since === undefined) {
|
|
153
|
+
if (until !== undefined) throw new UsageError('INVALID_ARGUMENT', '--until requires --since')
|
|
154
|
+
return { kind: inputString(input, 'range') ?? 'last7d' }
|
|
155
|
+
}
|
|
156
|
+
if (input.range !== undefined) {
|
|
157
|
+
throw new UsageError('ARGUMENT_CONFLICT', '--range cannot be combined with --since')
|
|
158
|
+
}
|
|
159
|
+
return { kind: 'explicit', start_time_ms: since, end_time_ms: until ?? Date.now() }
|
|
160
|
+
}
|
package/cli/modules/app.ts
CHANGED
|
@@ -97,6 +97,7 @@ export function createAppModule(dependencies: AppModuleDependencies = {}): Comma
|
|
|
97
97
|
lifecycleCommand('stop', 'Stop an installed App'),
|
|
98
98
|
restartCommand(),
|
|
99
99
|
statusCommand(),
|
|
100
|
+
publishCommand(),
|
|
100
101
|
]
|
|
101
102
|
return {
|
|
102
103
|
name: 'app',
|
|
@@ -458,6 +459,91 @@ function statusCommand(): CommandDefinition {
|
|
|
458
459
|
}
|
|
459
460
|
}
|
|
460
461
|
|
|
462
|
+
function publishCommand(): CommandDefinition {
|
|
463
|
+
return {
|
|
464
|
+
verb: 'publish',
|
|
465
|
+
summary: 'Package a directory on the control-panel node into the Zone Repo as an App candidate',
|
|
466
|
+
description:
|
|
467
|
+
'Calls control-panel app.publish: packages <local_dir> (a path on the control-panel node; nothing is uploaded) with the AppDoc template, stores PackageMeta and the AppDoc in the Zone Repo/NamedStore, and stages a self-checked PIKG there. The AppDoc is not Owner-signed and no BNS/App DID record is updated, so this is not an authoritative App release.',
|
|
468
|
+
positionals: [
|
|
469
|
+
{ name: 'local_dir', description: 'Absolute source directory on the control-panel node' },
|
|
470
|
+
],
|
|
471
|
+
options: [
|
|
472
|
+
{
|
|
473
|
+
name: 'app-doc',
|
|
474
|
+
property: 'app_doc',
|
|
475
|
+
description: 'AppDoc template JSON file (pkg_list entries without pkg_objid)',
|
|
476
|
+
type: 'string',
|
|
477
|
+
required: true,
|
|
478
|
+
},
|
|
479
|
+
{
|
|
480
|
+
name: 'app-type',
|
|
481
|
+
property: 'app_type',
|
|
482
|
+
description: 'App type; defaults to the template app_type',
|
|
483
|
+
type: 'string',
|
|
484
|
+
enum: ['dapp', 'web', 'agent'],
|
|
485
|
+
},
|
|
486
|
+
],
|
|
487
|
+
inputSchema: {
|
|
488
|
+
type: 'object',
|
|
489
|
+
properties: {
|
|
490
|
+
local_dir: { type: 'string', minLength: 1 },
|
|
491
|
+
app_doc: { type: 'string', minLength: 1 },
|
|
492
|
+
app_type: { type: 'string', enum: ['dapp', 'web', 'agent'] },
|
|
493
|
+
},
|
|
494
|
+
required: ['local_dir', 'app_doc'],
|
|
495
|
+
additionalProperties: false,
|
|
496
|
+
},
|
|
497
|
+
outputSchema: OBJECT_OUTPUT,
|
|
498
|
+
resultSchemaVersion: 1,
|
|
499
|
+
access: { mode: 'fixed', level: 'write' },
|
|
500
|
+
asyncMode: 'sync',
|
|
501
|
+
requiresSession: true,
|
|
502
|
+
examples: [
|
|
503
|
+
'buckyos app publish /srv/build/demo-web --app-doc ./demo-web.appdoc.json',
|
|
504
|
+
'buckyos app publish /srv/build/demo-agent --app-doc ./demo-agent.appdoc.json --app-type agent',
|
|
505
|
+
],
|
|
506
|
+
handler: async (ctx, input) => {
|
|
507
|
+
const localDir = expectString(input, 'local_dir')
|
|
508
|
+
if (!isAbsolute(localDir)) {
|
|
509
|
+
throw new UsageError(
|
|
510
|
+
'INVALID_ARGUMENT',
|
|
511
|
+
'local_dir must be an absolute path on the control-panel node',
|
|
512
|
+
)
|
|
513
|
+
}
|
|
514
|
+
let appDoc: unknown
|
|
515
|
+
try {
|
|
516
|
+
appDoc = JSON.parse(
|
|
517
|
+
await getHost().readTextFile(resolveFromCwd(ctx, expectString(input, 'app_doc'))),
|
|
518
|
+
)
|
|
519
|
+
} catch (error) {
|
|
520
|
+
throw new UsageError(
|
|
521
|
+
'INVALID_APP_DOC',
|
|
522
|
+
`failed to read AppDoc template: ${errorMessage(error)}`,
|
|
523
|
+
)
|
|
524
|
+
}
|
|
525
|
+
if (!isObject(appDoc)) {
|
|
526
|
+
throw new UsageError('INVALID_APP_DOC', 'AppDoc template must be a JSON object')
|
|
527
|
+
}
|
|
528
|
+
const result = expectObject(
|
|
529
|
+
await callControl(ctx, 'app.publish', {
|
|
530
|
+
local_dir: localDir,
|
|
531
|
+
app_doc: appDoc,
|
|
532
|
+
...(typeof input.app_type === 'string' ? { app_type: input.app_type } : {}),
|
|
533
|
+
}),
|
|
534
|
+
'app.publish response',
|
|
535
|
+
)
|
|
536
|
+
const { ok: _ok, ...published } = result
|
|
537
|
+
return {
|
|
538
|
+
...published,
|
|
539
|
+
publish_status: expectString(result, 'publish_status'),
|
|
540
|
+
app_doc_signature: 'not-signed',
|
|
541
|
+
app_did_publication: 'not-published',
|
|
542
|
+
}
|
|
543
|
+
},
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
|
|
461
547
|
async function fetchApp(
|
|
462
548
|
ctx: CommandContext,
|
|
463
549
|
input: Record<string, unknown>,
|
package/cli/modules/log.ts
CHANGED
|
@@ -12,6 +12,8 @@ import {
|
|
|
12
12
|
|
|
13
13
|
const CONTROL_PANEL = 'control-panel'
|
|
14
14
|
const OBJECT_OUTPUT: JsonSchema = { type: 'object', additionalProperties: true }
|
|
15
|
+
// Stored entry levels (control-panel sys_log_mgr normalize_log_level); WARN is stored as warning.
|
|
16
|
+
const LOG_LEVELS = ['error', 'warning', 'info', 'debug', 'trace']
|
|
15
17
|
|
|
16
18
|
export interface LogModuleDependencies {
|
|
17
19
|
sleep?: (milliseconds: number, signal: AbortSignal) => Promise<void>
|
|
@@ -21,8 +23,40 @@ export interface LogModuleDependencies {
|
|
|
21
23
|
export function createLogModule(dependencies: LogModuleDependencies = {}): CommandModule {
|
|
22
24
|
return {
|
|
23
25
|
name: 'log',
|
|
24
|
-
summary: '
|
|
25
|
-
commands: [
|
|
26
|
+
summary: 'List services, query, follow, and export redacted system logs',
|
|
27
|
+
commands: [
|
|
28
|
+
servicesCommand(),
|
|
29
|
+
queryCommand(),
|
|
30
|
+
tailCommand(dependencies),
|
|
31
|
+
exportCommand(dependencies),
|
|
32
|
+
],
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function servicesCommand(): CommandDefinition {
|
|
37
|
+
return {
|
|
38
|
+
verb: 'services',
|
|
39
|
+
summary: 'List the service IDs that have system logs',
|
|
40
|
+
inputSchema: { type: 'object', properties: {}, additionalProperties: false },
|
|
41
|
+
outputSchema: {
|
|
42
|
+
type: 'object',
|
|
43
|
+
properties: { services: { type: 'array', items: OBJECT_OUTPUT } },
|
|
44
|
+
required: ['services'],
|
|
45
|
+
additionalProperties: false,
|
|
46
|
+
},
|
|
47
|
+
resultSchemaVersion: 1,
|
|
48
|
+
access: { mode: 'fixed', level: 'read' },
|
|
49
|
+
asyncMode: 'sync',
|
|
50
|
+
requiresSession: true,
|
|
51
|
+
examples: ['buckyos log services'],
|
|
52
|
+
handler: async (ctx) => {
|
|
53
|
+
const response = expectObject(
|
|
54
|
+
await callService(ctx, CONTROL_PANEL, 'system.logs.list', {}),
|
|
55
|
+
'Control Panel log service list',
|
|
56
|
+
)
|
|
57
|
+
if (!Array.isArray(response.services)) invalidResponse('log service list')
|
|
58
|
+
return { services: response.services }
|
|
59
|
+
},
|
|
26
60
|
}
|
|
27
61
|
}
|
|
28
62
|
|
|
@@ -178,7 +212,9 @@ function filterOptions(plural: boolean) {
|
|
|
178
212
|
{
|
|
179
213
|
name: plural ? 'services' : 'service',
|
|
180
214
|
property: plural ? 'services' : 'service',
|
|
181
|
-
description: plural
|
|
215
|
+
description: plural
|
|
216
|
+
? 'Comma-separated service IDs (buckyos log services)'
|
|
217
|
+
: 'Service ID (buckyos log services)',
|
|
182
218
|
type: 'string' as const,
|
|
183
219
|
required: true,
|
|
184
220
|
},
|
|
@@ -187,7 +223,11 @@ function filterOptions(plural: boolean) {
|
|
|
187
223
|
description: 'Exact filename in the service log directory',
|
|
188
224
|
type: 'string' as const,
|
|
189
225
|
},
|
|
190
|
-
{
|
|
226
|
+
{
|
|
227
|
+
name: 'level',
|
|
228
|
+
description: `Log level: ${LOG_LEVELS.join(', ')} (warn = warning)`,
|
|
229
|
+
type: 'string' as const,
|
|
230
|
+
},
|
|
191
231
|
{ name: 'keyword', description: 'Case-insensitive content filter', type: 'string' as const },
|
|
192
232
|
{ name: 'since', description: 'RFC 3339 lower time boundary', type: 'string' as const },
|
|
193
233
|
{ name: 'until', description: 'RFC 3339 upper time boundary', type: 'string' as const },
|
|
@@ -224,7 +264,7 @@ function normalizeFilter(
|
|
|
224
264
|
return compact({
|
|
225
265
|
services,
|
|
226
266
|
file: inputString(input, 'file'),
|
|
227
|
-
level: inputString(input, 'level')
|
|
267
|
+
level: normalizeLevel(inputString(input, 'level')),
|
|
228
268
|
keyword: inputString(input, 'keyword'),
|
|
229
269
|
since: normalizeLogTime(input, 'since'),
|
|
230
270
|
until: normalizeLogTime(input, 'until'),
|
|
@@ -234,6 +274,18 @@ function normalizeFilter(
|
|
|
234
274
|
}) as Record<string, unknown> & { services: string[] }
|
|
235
275
|
}
|
|
236
276
|
|
|
277
|
+
function normalizeLevel(value: string | undefined): string | undefined {
|
|
278
|
+
if (!value) return undefined
|
|
279
|
+
const level = value.toLowerCase() === 'warn' ? 'warning' : value.toLowerCase()
|
|
280
|
+
if (!LOG_LEVELS.includes(level)) {
|
|
281
|
+
throw new UsageError(
|
|
282
|
+
'INVALID_ARGUMENT',
|
|
283
|
+
`level must be one of ${LOG_LEVELS.join(', ')} (warn = warning)`,
|
|
284
|
+
)
|
|
285
|
+
}
|
|
286
|
+
return level
|
|
287
|
+
}
|
|
288
|
+
|
|
237
289
|
function normalizeLogTime(input: Record<string, unknown>, key: string): string | undefined {
|
|
238
290
|
const value = inputString(input, key)
|
|
239
291
|
if (!value) return undefined
|
package/cli/modules/pikg.ts
CHANGED
|
@@ -54,6 +54,12 @@ const SHA256_ID = /^sha256:[0-9a-f]{64}$/
|
|
|
54
54
|
const OBJECT_ID = /^(?:appdoc|pkg):[0-9a-f]{64}$/
|
|
55
55
|
const SAFE_GENERATED_FILE = /^[A-Za-z0-9._-]+$/
|
|
56
56
|
const OBJECT_OUTPUT = { type: 'object' as const, additionalProperties: true }
|
|
57
|
+
const APP_KINDS = ['static-web', 'script', 'docker', 'agent-skills']
|
|
58
|
+
const PATH_KINDS: Record<string, { key: string; category: string; selectorType: string }> = {
|
|
59
|
+
'static-web': { key: 'web', category: 'web', selectorType: 'static' },
|
|
60
|
+
script: { key: 'script', category: 'dapp', selectorType: 'single' },
|
|
61
|
+
'agent-skills': { key: 'agent_skills', category: 'agent_skills', selectorType: 'single' },
|
|
62
|
+
}
|
|
57
63
|
|
|
58
64
|
export interface DockerImageInfo {
|
|
59
65
|
id: string
|
|
@@ -201,7 +207,7 @@ export function createPikgModule(dependencies: PikgModuleDependencies = {}): Com
|
|
|
201
207
|
name: 'kind',
|
|
202
208
|
description: 'Initial App kind',
|
|
203
209
|
type: 'string',
|
|
204
|
-
enum:
|
|
210
|
+
enum: APP_KINDS,
|
|
205
211
|
},
|
|
206
212
|
{
|
|
207
213
|
name: 'source',
|
|
@@ -217,7 +223,7 @@ export function createPikgModule(dependencies: PikgModuleDependencies = {}): Com
|
|
|
217
223
|
project_dir: { type: 'string', minLength: 1 },
|
|
218
224
|
name: { type: 'string', minLength: 1 },
|
|
219
225
|
owner: { type: 'string', minLength: 1 },
|
|
220
|
-
kind: { type: 'string', enum:
|
|
226
|
+
kind: { type: 'string', enum: APP_KINDS },
|
|
221
227
|
source: { type: 'string', minLength: 1 },
|
|
222
228
|
version: { type: 'string', minLength: 1 },
|
|
223
229
|
app_did: { type: 'string', minLength: 1 },
|
|
@@ -234,6 +240,7 @@ export function createPikgModule(dependencies: PikgModuleDependencies = {}): Com
|
|
|
234
240
|
examples: [
|
|
235
241
|
'buckyos pikg init .',
|
|
236
242
|
'buckyos --non-interactive pikg init . --owner did:bns:root --kind static-web --source ./web/dist',
|
|
243
|
+
'buckyos --non-interactive pikg init . --owner did:bns:root --kind agent-skills --source ./skills',
|
|
237
244
|
],
|
|
238
245
|
handler: async (ctx, input) => await initCommand(ctx, input, docker),
|
|
239
246
|
},
|
|
@@ -440,11 +447,11 @@ async function initCommand(
|
|
|
440
447
|
kind = await requiredPrompt(
|
|
441
448
|
ctx,
|
|
442
449
|
canPrompt,
|
|
443
|
-
|
|
450
|
+
`App kind (${APP_KINDS.join('/')}): `,
|
|
444
451
|
'kind',
|
|
445
452
|
)
|
|
446
453
|
}
|
|
447
|
-
if (!
|
|
454
|
+
if (!APP_KINDS.includes(kind)) {
|
|
448
455
|
throw new UsageError('INVALID_APP_KIND', `invalid App kind: ${kind}`)
|
|
449
456
|
}
|
|
450
457
|
let source = optionalInputString(input, 'source')
|
|
@@ -474,7 +481,7 @@ async function initCommand(
|
|
|
474
481
|
sourceValue = { type: 'docker-image', image: source }
|
|
475
482
|
sourceSummary = { type: 'docker-image', image: image.canonicalName, image_id: image.id }
|
|
476
483
|
} else {
|
|
477
|
-
subpackageKey = kind
|
|
484
|
+
subpackageKey = PATH_KINDS[kind].key
|
|
478
485
|
const absoluteSource = resolve(projectDir, source)
|
|
479
486
|
const persistedSource = isAbsolute(source)
|
|
480
487
|
? resolve(source)
|
|
@@ -498,9 +505,9 @@ async function initCommand(
|
|
|
498
505
|
owner,
|
|
499
506
|
author: owner,
|
|
500
507
|
show_name: name,
|
|
501
|
-
categories: [kind
|
|
508
|
+
categories: [PATH_KINDS[kind]?.category ?? 'dapp'],
|
|
502
509
|
permissions: [],
|
|
503
|
-
selector_type: kind
|
|
510
|
+
selector_type: PATH_KINDS[kind]?.selectorType ?? 'single',
|
|
504
511
|
service_config_tips: {},
|
|
505
512
|
}
|
|
506
513
|
const pikgMeta = {
|
|
@@ -136,6 +136,89 @@ export async function gzipFile(source: string, destination: string): Promise<voi
|
|
|
136
136
|
await getHost().gzipFile(source, destination)
|
|
137
137
|
}
|
|
138
138
|
|
|
139
|
+
export interface TarFileEntry {
|
|
140
|
+
path: string
|
|
141
|
+
executable: boolean
|
|
142
|
+
bytes: Uint8Array
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export async function readTarGz(bytes: Uint8Array, limit: number): Promise<TarFileEntry[]> {
|
|
146
|
+
const tar = await gunzipBytes(bytes, limit)
|
|
147
|
+
const decoder = new TextDecoder('utf-8', { fatal: true })
|
|
148
|
+
const text = (offset: number, length: number) =>
|
|
149
|
+
decoder.decode(tar.subarray(offset, offset + length)).replace(/\0.*$/s, '')
|
|
150
|
+
const files: TarFileEntry[] = []
|
|
151
|
+
const seen = new Set<string>()
|
|
152
|
+
let offset = 0
|
|
153
|
+
while (
|
|
154
|
+
offset + BLOCK_SIZE <= tar.length && tar.subarray(offset, offset + BLOCK_SIZE).some(Boolean)
|
|
155
|
+
) {
|
|
156
|
+
let raw: string
|
|
157
|
+
let size: number
|
|
158
|
+
let mode: number
|
|
159
|
+
try {
|
|
160
|
+
const prefix = text(offset + 257, 6) === 'ustar' ? text(offset + 345, 155) : ''
|
|
161
|
+
raw = prefix ? `${prefix}/${text(offset, 100)}` : text(offset, 100)
|
|
162
|
+
size = Number.parseInt(text(offset + 124, 12).trim() || '0', 8)
|
|
163
|
+
mode = Number.parseInt(text(offset + 100, 8).trim() || '0', 8)
|
|
164
|
+
} catch {
|
|
165
|
+
throw invalidZip('payload', 'tar header is not valid UTF-8')
|
|
166
|
+
}
|
|
167
|
+
const type = String.fromCharCode(tar[offset + 156] || 0x30)
|
|
168
|
+
const path = raw.replace(/^(?:\.\/)+/, '').replace(/\/$/, '')
|
|
169
|
+
const start = offset + BLOCK_SIZE
|
|
170
|
+
if (!Number.isSafeInteger(size) || size < 0 || start + size > tar.length) {
|
|
171
|
+
throw invalidZip('payload', `tar entry is truncated: ${raw}`)
|
|
172
|
+
}
|
|
173
|
+
offset = start + Math.ceil(size / BLOCK_SIZE) * BLOCK_SIZE
|
|
174
|
+
if (type === '5' && (path === '' || path === '.')) continue
|
|
175
|
+
if (
|
|
176
|
+
!path || path.startsWith('/') || /^[A-Za-z]:/.test(path) || path.includes('\\') ||
|
|
177
|
+
path.split('/').some((segment) => !segment || segment === '.' || segment === '..')
|
|
178
|
+
) {
|
|
179
|
+
throw invalidZip('payload', `unsafe tar entry path: ${JSON.stringify(raw)}`)
|
|
180
|
+
}
|
|
181
|
+
if (type === '5') continue
|
|
182
|
+
if (type !== '0') {
|
|
183
|
+
throw invalidZip('payload', `tar entry is not a regular file or directory: ${path}`)
|
|
184
|
+
}
|
|
185
|
+
if (seen.has(path)) throw invalidZip('payload', `duplicate tar entry: ${path}`)
|
|
186
|
+
seen.add(path)
|
|
187
|
+
files.push({ path, executable: (mode & 0o111) !== 0, bytes: tar.slice(start, start + size) })
|
|
188
|
+
}
|
|
189
|
+
return files
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
async function gunzipBytes(bytes: Uint8Array, limit: number): Promise<Uint8Array> {
|
|
193
|
+
const stream = new ReadableStream<Uint8Array>({
|
|
194
|
+
start(controller) {
|
|
195
|
+
controller.enqueue(bytes)
|
|
196
|
+
controller.close()
|
|
197
|
+
},
|
|
198
|
+
}).pipeThrough(
|
|
199
|
+
new DecompressionStream('gzip') as unknown as ReadableWritablePair<Uint8Array, Uint8Array>,
|
|
200
|
+
)
|
|
201
|
+
const chunks: Uint8Array[] = []
|
|
202
|
+
let size = 0
|
|
203
|
+
try {
|
|
204
|
+
for await (const chunk of stream) {
|
|
205
|
+
size += chunk.length
|
|
206
|
+
if (size > limit) throw invalidZip('limits', 'payload expands beyond the size limit')
|
|
207
|
+
chunks.push(chunk)
|
|
208
|
+
}
|
|
209
|
+
} catch (error) {
|
|
210
|
+
if (error instanceof ToolError) throw error
|
|
211
|
+
throw invalidZip('compression', 'payload is not a valid gzip stream')
|
|
212
|
+
}
|
|
213
|
+
const result = new Uint8Array(size)
|
|
214
|
+
let position = 0
|
|
215
|
+
for (const chunk of chunks) {
|
|
216
|
+
result.set(chunk, position)
|
|
217
|
+
position += chunk.length
|
|
218
|
+
}
|
|
219
|
+
return result
|
|
220
|
+
}
|
|
221
|
+
|
|
139
222
|
export async function writeStoredZip(
|
|
140
223
|
destination: string,
|
|
141
224
|
sources: ZipWriteSource[],
|