@namzu/sdk 45.1.0 → 46.0.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/CHANGELOG.md +111 -0
- package/dist/authorization/gate.d.ts +5 -2
- package/dist/authorization/gate.d.ts.map +1 -1
- package/dist/authorization/gate.js +25 -4
- package/dist/authorization/gate.js.map +1 -1
- package/dist/authorization/rules.d.ts.map +1 -1
- package/dist/authorization/rules.js +19 -0
- package/dist/authorization/rules.js.map +1 -1
- package/dist/authorization/shell-lexer.d.ts +24 -0
- package/dist/authorization/shell-lexer.d.ts.map +1 -1
- package/dist/authorization/shell-lexer.js +69 -51
- package/dist/authorization/shell-lexer.js.map +1 -1
- package/dist/pricing/catalogue.generated.d.ts.map +1 -1
- package/dist/pricing/catalogue.generated.js +28 -4
- package/dist/pricing/catalogue.generated.js.map +1 -1
- package/dist/public-runtime.d.ts +3 -3
- package/dist/public-runtime.d.ts.map +1 -1
- package/dist/public-runtime.js +4 -2
- package/dist/public-runtime.js.map +1 -1
- package/dist/public-tools.d.ts +3 -2
- package/dist/public-tools.d.ts.map +1 -1
- package/dist/public-tools.js +5 -2
- package/dist/public-tools.js.map +1 -1
- package/dist/public-types.d.ts +3 -3
- package/dist/public-types.d.ts.map +1 -1
- package/dist/registry/tool/callable.d.ts +22 -0
- package/dist/registry/tool/callable.d.ts.map +1 -0
- package/dist/registry/tool/callable.js +29 -0
- package/dist/registry/tool/callable.js.map +1 -0
- package/dist/registry/tool/execute.d.ts.map +1 -1
- package/dist/registry/tool/execute.js +8 -1
- package/dist/registry/tool/execute.js.map +1 -1
- package/dist/runtime/query/executor/tool-call-admission.d.ts +37 -11
- package/dist/runtime/query/executor/tool-call-admission.d.ts.map +1 -1
- package/dist/runtime/query/executor/tool-call-admission.js +38 -12
- package/dist/runtime/query/executor/tool-call-admission.js.map +1 -1
- package/dist/runtime/query/executor.d.ts +4 -2
- package/dist/runtime/query/executor.d.ts.map +1 -1
- package/dist/runtime/query/executor.js +18 -5
- package/dist/runtime/query/executor.js.map +1 -1
- package/dist/runtime/query/review-policy.d.ts +43 -0
- package/dist/runtime/query/review-policy.d.ts.map +1 -1
- package/dist/runtime/query/review-policy.js +59 -16
- package/dist/runtime/query/review-policy.js.map +1 -1
- package/dist/skills/registry.d.ts +6 -0
- package/dist/skills/registry.d.ts.map +1 -1
- package/dist/skills/registry.js +1 -0
- package/dist/skills/registry.js.map +1 -1
- package/dist/tools/builtins/computer-use-coordinates.d.ts +65 -0
- package/dist/tools/builtins/computer-use-coordinates.d.ts.map +1 -0
- package/dist/tools/builtins/computer-use-coordinates.js +123 -0
- package/dist/tools/builtins/computer-use-coordinates.js.map +1 -0
- package/dist/tools/builtins/computer-use-image.d.ts +77 -0
- package/dist/tools/builtins/computer-use-image.d.ts.map +1 -0
- package/dist/tools/builtins/computer-use-image.js +223 -0
- package/dist/tools/builtins/computer-use-image.js.map +1 -0
- package/dist/tools/builtins/computer-use.d.ts +519 -14
- package/dist/tools/builtins/computer-use.d.ts.map +1 -1
- package/dist/tools/builtins/computer-use.js +1188 -183
- package/dist/tools/builtins/computer-use.js.map +1 -1
- package/dist/tools/builtins/index.d.ts +2 -1
- package/dist/tools/builtins/index.d.ts.map +1 -1
- package/dist/tools/builtins/index.js +1 -1
- package/dist/tools/builtins/index.js.map +1 -1
- package/dist/tools/builtins/skill.d.ts +74 -0
- package/dist/tools/builtins/skill.d.ts.map +1 -1
- package/dist/tools/builtins/skill.js +214 -174
- package/dist/tools/builtins/skill.js.map +1 -1
- package/dist/tools/defineTool.d.ts +2 -0
- package/dist/tools/defineTool.d.ts.map +1 -1
- package/dist/tools/defineTool.js +7 -0
- package/dist/tools/defineTool.js.map +1 -1
- package/dist/tools/schedules/present.d.ts.map +1 -1
- package/dist/tools/schedules/present.js +2 -0
- package/dist/tools/schedules/present.js.map +1 -1
- package/dist/tools/schedules/schedule-tool.d.ts +6 -5
- package/dist/tools/schedules/schedule-tool.d.ts.map +1 -1
- package/dist/tools/schedules/schedule-tool.js +121 -30
- package/dist/tools/schedules/schedule-tool.js.map +1 -1
- package/dist/tools/schedules/types.d.ts +69 -1
- package/dist/tools/schedules/types.d.ts.map +1 -1
- package/dist/types/authorization/index.d.ts +21 -0
- package/dist/types/authorization/index.d.ts.map +1 -1
- package/dist/types/authorization/index.js +5 -0
- package/dist/types/authorization/index.js.map +1 -1
- package/dist/types/computer-use/index.d.ts +176 -0
- package/dist/types/computer-use/index.d.ts.map +1 -1
- package/dist/types/computer-use/index.js.map +1 -1
- package/dist/types/tool/index.d.ts +19 -0
- package/dist/types/tool/index.d.ts.map +1 -1
- package/dist/types/tool/index.js.map +1 -1
- package/package.json +3 -1
- package/src/authorization/gate.ts +25 -4
- package/src/authorization/rules.ts +18 -0
- package/src/authorization/shell-lexer.ts +73 -45
- package/src/pricing/catalogue.generated.ts +28 -4
- package/src/pricing/rates.source.json +27 -6
- package/src/public-runtime.ts +16 -2
- package/src/public-tools.ts +19 -1
- package/src/public-types.ts +5 -0
- package/src/registry/tool/callable.ts +37 -0
- package/src/registry/tool/execute.ts +8 -1
- package/src/runtime/query/executor/tool-call-admission.ts +60 -16
- package/src/runtime/query/executor.ts +19 -4
- package/src/runtime/query/review-policy.ts +103 -15
- package/src/skills/registry.ts +8 -0
- package/src/tools/builtins/computer-use-coordinates.ts +144 -0
- package/src/tools/builtins/computer-use-image.ts +278 -0
- package/src/tools/builtins/computer-use.ts +1485 -191
- package/src/tools/builtins/index.ts +7 -1
- package/src/tools/builtins/skill.ts +304 -174
- package/src/tools/defineTool.ts +10 -0
- package/src/tools/schedules/present.ts +2 -0
- package/src/tools/schedules/schedule-tool.ts +139 -33
- package/src/tools/schedules/types.ts +69 -1
- package/src/types/authorization/index.ts +21 -0
- package/src/types/computer-use/index.ts +202 -0
- package/src/types/tool/index.ts +19 -0
|
@@ -7,7 +7,13 @@ export { LSP_TOOL_NAME, LspTool, getCodeNavigationTools } from './lsp.js'
|
|
|
7
7
|
export { GrepTool } from './grep.js'
|
|
8
8
|
export { JobTool } from './job.js'
|
|
9
9
|
export { WaitForJobTool } from './wait-for-job.js'
|
|
10
|
-
export { SKILL_TOOL_NAME, SkillTool, parseAllowedTools } from './skill.js'
|
|
10
|
+
export { SKILL_TOOL_NAME, SkillTool, createSkillTool, parseAllowedTools } from './skill.js'
|
|
11
|
+
export type {
|
|
12
|
+
SkillDirectoryContext,
|
|
13
|
+
SkillDirectoryRequest,
|
|
14
|
+
SkillDirectoryResolver,
|
|
15
|
+
SkillToolOptions,
|
|
16
|
+
} from './skill.js'
|
|
11
17
|
export {
|
|
12
18
|
WEB_FETCH_TOOL_NAME,
|
|
13
19
|
WEB_SEARCH_TOOL_NAME,
|
|
@@ -2,6 +2,7 @@ import { createHash } from 'node:crypto'
|
|
|
2
2
|
import { z } from 'zod'
|
|
3
3
|
|
|
4
4
|
import { parseAllowedTools } from '../../authorization/skill-grant.js'
|
|
5
|
+
import type { Sandbox } from '../../types/sandbox/index.js'
|
|
5
6
|
import { isInvocableBy, skillInvocation } from '../../types/skills/index.js'
|
|
6
7
|
import { defineTool } from '../defineTool.js'
|
|
7
8
|
|
|
@@ -22,8 +23,63 @@ export { parseAllowedTools }
|
|
|
22
23
|
* else, from the next batch — which inverted what skill authors mean by it
|
|
23
24
|
* and left a model that loaded `allowed-tools: Read Grep` without `bash`.
|
|
24
25
|
* See `authorization/skill-grant.ts` for what a grant can and cannot do.
|
|
26
|
+
*
|
|
27
|
+
* A skill's body often names files beside it (`scripts/`, `references/`,
|
|
28
|
+
* `assets/`), and the load result carried no directory to open them from. The
|
|
29
|
+
* listing reported the registry's `location`, which is where the HOST reads
|
|
30
|
+
* the skill — a path the model cannot open once its tools run in a sandbox or
|
|
31
|
+
* a remote workspace, so it hard-coded what the skill said to read, or went
|
|
32
|
+
* searching the filesystem. Only the host knows what its tools can reach, so
|
|
33
|
+
* the host says it: {@link SkillToolOptions.resolveModelDirectory}.
|
|
25
34
|
*/
|
|
26
35
|
|
|
36
|
+
/** One skill, as the `skill` tool asks a host about it. */
|
|
37
|
+
export interface SkillDirectoryRequest {
|
|
38
|
+
/** The name the registry accepts, namespaced for a plugin skill (`plugin__skill`). */
|
|
39
|
+
readonly name: string
|
|
40
|
+
/**
|
|
41
|
+
* Where the host loads the skill from (`Skill.dirPath`, or the catalog
|
|
42
|
+
* entry's `directory`); undefined when the registry does not say.
|
|
43
|
+
*/
|
|
44
|
+
readonly directory: string | undefined
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** What the call knows about where the model's tools run. */
|
|
48
|
+
export interface SkillDirectoryContext {
|
|
49
|
+
/** The turn's sandbox, when its tools run in one. Absent on the host. */
|
|
50
|
+
readonly sandbox?: Sandbox
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* The directory the model's tools can open for a skill, or undefined when
|
|
55
|
+
* they cannot reach it.
|
|
56
|
+
*
|
|
57
|
+
* Asked per call, with the turn's sandbox, because the answer is a property
|
|
58
|
+
* of where the tools run and not of the skill: the same skill is at its own
|
|
59
|
+
* path on the host, somewhere else inside a container, and absent from a
|
|
60
|
+
* sandbox that does not mount it.
|
|
61
|
+
*/
|
|
62
|
+
export type SkillDirectoryResolver = (
|
|
63
|
+
skill: SkillDirectoryRequest,
|
|
64
|
+
context: SkillDirectoryContext,
|
|
65
|
+
) => string | undefined | Promise<string | undefined>
|
|
66
|
+
|
|
67
|
+
export interface SkillToolOptions {
|
|
68
|
+
/**
|
|
69
|
+
* Say which directory the model can open for each skill.
|
|
70
|
+
*
|
|
71
|
+
* Absent, the tool behaves as it always has: the listing carries the
|
|
72
|
+
* registry's `location` and a load names no directory. Present, a load
|
|
73
|
+
* opens with the directory it returns, or with a line saying the skill's
|
|
74
|
+
* files are not reachable when it returns undefined; the listing carries
|
|
75
|
+
* that `directory` and never the registry's `location`, which is the
|
|
76
|
+
* host's path; and `${CLAUDE_SKILL_DIR}` in `allowed-tools` expands to it,
|
|
77
|
+
* since a command line the model writes names the path the model was
|
|
78
|
+
* given. The skill is still LOADED from the registry's own path.
|
|
79
|
+
*/
|
|
80
|
+
readonly resolveModelDirectory?: SkillDirectoryResolver
|
|
81
|
+
}
|
|
82
|
+
|
|
27
83
|
const inputSchema = z.object({
|
|
28
84
|
name: z
|
|
29
85
|
.string()
|
|
@@ -53,6 +109,8 @@ interface SkillSnapshot {
|
|
|
53
109
|
readonly allowedTools: readonly string[] | undefined
|
|
54
110
|
/** Bound into the cursor because `${CLAUDE_SKILL_DIR}` in a grant expands to it. */
|
|
55
111
|
readonly skillDirectory: string | undefined
|
|
112
|
+
/** What the first page opens with: where the skill's files are, or that they are out of reach. */
|
|
113
|
+
readonly header: string
|
|
56
114
|
readonly invocation: ReturnType<typeof skillInvocation>
|
|
57
115
|
}
|
|
58
116
|
|
|
@@ -64,7 +122,10 @@ interface SkillPage {
|
|
|
64
122
|
interface ListedSkill {
|
|
65
123
|
readonly name: string
|
|
66
124
|
readonly description: string
|
|
67
|
-
|
|
125
|
+
/** The registry's path to the SKILL.md; only when no host resolver is configured. */
|
|
126
|
+
readonly location?: string
|
|
127
|
+
/** The directory the model can open, from the host's resolver. */
|
|
128
|
+
readonly directory?: string
|
|
68
129
|
readonly allowedTools?: string
|
|
69
130
|
}
|
|
70
131
|
|
|
@@ -85,6 +146,7 @@ function snapshotDigest(snapshot: SkillSnapshot): string {
|
|
|
85
146
|
...(snapshot.skillDirectory === undefined
|
|
86
147
|
? {}
|
|
87
148
|
: { skillDirectory: snapshot.skillDirectory }),
|
|
149
|
+
...(snapshot.header === '' ? {} : { header: snapshot.header }),
|
|
88
150
|
invocation: snapshot.invocation,
|
|
89
151
|
}),
|
|
90
152
|
)
|
|
@@ -218,7 +280,9 @@ function pageSkillBody(input: {
|
|
|
218
280
|
readonly maxChars: number | undefined
|
|
219
281
|
}): SkillPage | undefined {
|
|
220
282
|
const { snapshot, digest, start, notice, maxChars } = input
|
|
221
|
-
|
|
283
|
+
// The first page only: a continuation is read after it, in the same history.
|
|
284
|
+
const header = start === 0 ? snapshot.header : ''
|
|
285
|
+
const remaining = `${header}${snapshot.body.slice(start)}${notice}`
|
|
222
286
|
if (maxChars === undefined || maxChars <= 0 || remaining.length <= maxChars) {
|
|
223
287
|
return { output: remaining }
|
|
224
288
|
}
|
|
@@ -229,7 +293,7 @@ function pageSkillBody(input: {
|
|
|
229
293
|
let end = boundaryAtOrBefore(snapshot.body, Math.min(snapshot.body.length - 1, start + maxChars))
|
|
230
294
|
while (end > start) {
|
|
231
295
|
const nextCursor = cursorFor(end, digest)
|
|
232
|
-
const output = `${snapshot.body.slice(start, end)}${continuationNotice(
|
|
296
|
+
const output = `${header}${snapshot.body.slice(start, end)}${continuationNotice(
|
|
233
297
|
snapshot.name,
|
|
234
298
|
nextCursor,
|
|
235
299
|
)}${notice}`
|
|
@@ -242,216 +306,282 @@ function pageSkillBody(input: {
|
|
|
242
306
|
|
|
243
307
|
export const SKILL_TOOL_NAME = 'skill'
|
|
244
308
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
? 'List skills'
|
|
270
|
-
: 'List more skills'
|
|
271
|
-
: `Read skill ${name}${input.cursor === undefined ? '' : ' (continued)'}`,
|
|
272
|
-
}
|
|
273
|
-
},
|
|
274
|
-
presentResult: (_input: SkillInput, result) =>
|
|
275
|
-
result.success ? { kind: 'generic', label: 'read', visibility: 'hidden' } : undefined,
|
|
276
|
-
|
|
277
|
-
async execute(input: SkillInput, context) {
|
|
278
|
-
if (!context.skills) {
|
|
309
|
+
/**
|
|
310
|
+
* Build the `skill` tool, with what the host knows about where its model's
|
|
311
|
+
* tools run. {@link SkillTool} is this with no options.
|
|
312
|
+
*/
|
|
313
|
+
export function createSkillTool(options: SkillToolOptions = {}) {
|
|
314
|
+
const resolveModelDirectory = options.resolveModelDirectory
|
|
315
|
+
return defineTool({
|
|
316
|
+
name: SKILL_TOOL_NAME,
|
|
317
|
+
description:
|
|
318
|
+
'Lists model-invocable skills when called without a name, or loads one skill by its exact listed name. Long lists and bodies return an opaque continuation cursor; keep calling in the same mode with that cursor until no continuation remains. The manifest carries only names and descriptions.',
|
|
319
|
+
inputSchema,
|
|
320
|
+
category: 'analysis',
|
|
321
|
+
permissions: [],
|
|
322
|
+
// Reads instructions and changes nothing on disk. What it does change is
|
|
323
|
+
// the turn's approvals, through `grantSkillTools`, and only ever towards
|
|
324
|
+
// fewer prompts for calls the operator's policy already leaves to review.
|
|
325
|
+
readOnly: true,
|
|
326
|
+
destructive: false,
|
|
327
|
+
concurrencySafe: true,
|
|
328
|
+
|
|
329
|
+
// The body is instructions for the model, often a hundred lines; the
|
|
330
|
+
// person needs the row that says which skill was read, not the text.
|
|
331
|
+
presentCall(input: SkillInput) {
|
|
332
|
+
const name = typeof input?.name === 'string' ? input.name : undefined
|
|
279
333
|
return {
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
334
|
+
kind: 'generic',
|
|
335
|
+
presentation: 'activity',
|
|
336
|
+
label:
|
|
337
|
+
name === undefined
|
|
338
|
+
? input?.cursor === undefined
|
|
339
|
+
? 'List skills'
|
|
340
|
+
: 'List more skills'
|
|
341
|
+
: `Read skill ${name}${input.cursor === undefined ? '' : ' (continued)'}`,
|
|
284
342
|
}
|
|
285
|
-
}
|
|
343
|
+
},
|
|
344
|
+
presentResult: (_input: SkillInput, result) =>
|
|
345
|
+
result.success ? { kind: 'generic', label: 'read', visibility: 'hidden' } : undefined,
|
|
286
346
|
|
|
287
|
-
|
|
288
|
-
if (!context.skills
|
|
347
|
+
async execute(input: SkillInput, context) {
|
|
348
|
+
if (!context.skills) {
|
|
289
349
|
return {
|
|
290
350
|
success: false,
|
|
291
351
|
output: '',
|
|
292
352
|
error:
|
|
293
|
-
'This skills registry
|
|
353
|
+
'This turn has no skills registry, so there is nothing to load. Proceed without the skill.',
|
|
294
354
|
}
|
|
295
355
|
}
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
356
|
+
|
|
357
|
+
if (input.name === undefined) {
|
|
358
|
+
if (!context.skills.catalog) {
|
|
359
|
+
return {
|
|
360
|
+
success: false,
|
|
361
|
+
output: '',
|
|
362
|
+
error:
|
|
363
|
+
'This skills registry cannot enumerate model-safe metadata. Use a skill name from the available-skills manifest.',
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
const directoryContext = directoryContextOf(context.sandbox)
|
|
367
|
+
const skills = await Promise.all(
|
|
368
|
+
(await context.skills.catalog())
|
|
369
|
+
.filter(
|
|
370
|
+
(entry) =>
|
|
371
|
+
entry.invocation === undefined ||
|
|
372
|
+
entry.invocation === 'model' ||
|
|
373
|
+
entry.invocation === 'both',
|
|
374
|
+
)
|
|
375
|
+
.map(async (entry): Promise<ListedSkill> => {
|
|
376
|
+
// With a resolver the registry's `location` is left out, not
|
|
377
|
+
// shown beside the directory: it is the host's path, the one
|
|
378
|
+
// a sandboxed model cannot open.
|
|
379
|
+
let where: Pick<ListedSkill, 'location' | 'directory'>
|
|
380
|
+
if (resolveModelDirectory) {
|
|
381
|
+
const directory = nonEmpty(
|
|
382
|
+
await resolveModelDirectory(
|
|
383
|
+
{ name: entry.registeredName, directory: entry.directory },
|
|
384
|
+
directoryContext,
|
|
385
|
+
),
|
|
386
|
+
)
|
|
387
|
+
where = directory === undefined ? {} : { directory }
|
|
388
|
+
} else {
|
|
389
|
+
where = { location: entry.location }
|
|
390
|
+
}
|
|
391
|
+
return {
|
|
392
|
+
name: entry.registeredName,
|
|
393
|
+
description: entry.description,
|
|
394
|
+
...where,
|
|
395
|
+
...(entry.allowedTools === undefined ? {} : { allowedTools: entry.allowedTools }),
|
|
396
|
+
}
|
|
397
|
+
}),
|
|
310
398
|
)
|
|
311
|
-
|
|
312
|
-
|
|
399
|
+
const maxChars = activeOutputCap(context.maxToolOutputChars)
|
|
400
|
+
const digest = listDigest(skills, maxChars)
|
|
401
|
+
let start = 0
|
|
402
|
+
let warningAlreadyShown = false
|
|
403
|
+
if (input.cursor !== undefined) {
|
|
404
|
+
const parsed = parseListCursor(input.cursor)
|
|
405
|
+
if (!parsed || parsed.digest !== digest || parsed.offset >= skills.length) {
|
|
406
|
+
return {
|
|
407
|
+
success: false,
|
|
408
|
+
output: '',
|
|
409
|
+
error:
|
|
410
|
+
'The skill-list continuation cursor is stale or invalid. Call skill again without a cursor to read the current catalog.',
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
start = parsed.offset
|
|
414
|
+
warningAlreadyShown = parsed.warned
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
const page = pageSkillCatalog({
|
|
418
|
+
skills,
|
|
419
|
+
digest,
|
|
420
|
+
start,
|
|
421
|
+
warningAlreadyShown,
|
|
422
|
+
maxChars,
|
|
423
|
+
})
|
|
424
|
+
if (!page) {
|
|
425
|
+
return {
|
|
426
|
+
success: false,
|
|
427
|
+
output: '',
|
|
428
|
+
error:
|
|
429
|
+
'The model-visible tool-output budget is too small to list skill metadata safely. Increase maxToolOutputChars and retry.',
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
return {
|
|
434
|
+
success: true,
|
|
435
|
+
output: serializeListPage(page),
|
|
436
|
+
data: {
|
|
437
|
+
kind: 'list',
|
|
438
|
+
count: page.skills.length,
|
|
439
|
+
...(page.nextCursor === null ? {} : { nextCursor: page.nextCursor }),
|
|
440
|
+
},
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// The registry answers with a load RESULT, not a skill — the shape
|
|
445
|
+
// mirrors the implementation rather than an adapter, so there is
|
|
446
|
+
// nothing between them to drift.
|
|
447
|
+
const loaded = await context.skills.load(input.name)
|
|
448
|
+
if (!loaded) {
|
|
449
|
+
// Named, with what IS available. A bare "not found" sends the model
|
|
450
|
+
// guessing at spellings, and the manifest it is guessing from is
|
|
451
|
+
// right there in its own prompt.
|
|
452
|
+
const available = context.skills.names()
|
|
453
|
+
return {
|
|
454
|
+
success: false,
|
|
455
|
+
output: '',
|
|
456
|
+
error: `No skill named "${input.name}". Available: ${available.length > 0 ? available.join(', ') : '(none)'}`,
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
const skill = loaded.skill
|
|
461
|
+
const invocation = skillInvocation(skill)
|
|
462
|
+
if (!isInvocableBy(skill, 'model')) {
|
|
463
|
+
// Reachable even though the manifest omits it: the model can name
|
|
464
|
+
// anything, and a check that only filtered the listing would be a
|
|
465
|
+
// menu restriction rather than a kitchen one — the exact defect
|
|
466
|
+
// `allowedTools` had before it was enforced at dispatch.
|
|
467
|
+
return {
|
|
468
|
+
success: false,
|
|
469
|
+
output: '',
|
|
470
|
+
error: `The skill "${input.name}" is ${skillInvocation(skill)}-invocable; it is not for you to run.`,
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
const allowed = parseAllowedTools(skill.metadata.allowedTools)
|
|
475
|
+
// Asked on every call, first page or continuation: the digest binds
|
|
476
|
+
// the answer, so a mount that changed between pages is a stale cursor
|
|
477
|
+
// rather than a second half written for a different directory.
|
|
478
|
+
const modelDirectory = resolveModelDirectory
|
|
479
|
+
? nonEmpty(
|
|
480
|
+
await resolveModelDirectory(
|
|
481
|
+
{ name: input.name, directory: skill.dirPath },
|
|
482
|
+
directoryContextOf(context.sandbox),
|
|
483
|
+
),
|
|
484
|
+
)
|
|
485
|
+
: undefined
|
|
486
|
+
const snapshot: SkillSnapshot = {
|
|
487
|
+
name: input.name,
|
|
488
|
+
body: skill.body ?? '(this skill has no body)',
|
|
489
|
+
allowedTools: allowed,
|
|
490
|
+
// The model's path when the host gave one: `${CLAUDE_SKILL_DIR}` in a
|
|
491
|
+
// pattern is matched against a command line the model writes, and it
|
|
492
|
+
// writes the path it was told.
|
|
493
|
+
skillDirectory: resolveModelDirectory ? modelDirectory : skill.dirPath,
|
|
494
|
+
header: resolveModelDirectory ? directoryHeader(modelDirectory) : '',
|
|
495
|
+
invocation,
|
|
496
|
+
}
|
|
497
|
+
const digest = snapshotDigest(snapshot)
|
|
313
498
|
let start = 0
|
|
314
|
-
let warningAlreadyShown = false
|
|
315
499
|
if (input.cursor !== undefined) {
|
|
316
|
-
const parsed =
|
|
317
|
-
if (
|
|
500
|
+
const parsed = parseCursor(input.cursor)
|
|
501
|
+
if (
|
|
502
|
+
!parsed ||
|
|
503
|
+
parsed.digest !== digest ||
|
|
504
|
+
parsed.offset >= snapshot.body.length ||
|
|
505
|
+
!isCodePointBoundary(snapshot.body, parsed.offset)
|
|
506
|
+
) {
|
|
318
507
|
return {
|
|
319
508
|
success: false,
|
|
320
509
|
output: '',
|
|
321
|
-
error:
|
|
322
|
-
'The skill-list continuation cursor is stale or invalid. Call skill again without a cursor to read the current catalog.',
|
|
510
|
+
error: `The continuation cursor for "${input.name}" is stale or invalid. Call skill again without a cursor to read the current instructions.`,
|
|
323
511
|
}
|
|
324
512
|
}
|
|
325
513
|
start = parsed.offset
|
|
326
|
-
warningAlreadyShown = parsed.warned
|
|
327
514
|
}
|
|
328
515
|
|
|
329
|
-
|
|
330
|
-
|
|
516
|
+
// Compiled before paging so the notice can say what the grant is, and
|
|
517
|
+
// committed only after paging succeeded: a load that fails here gave
|
|
518
|
+
// the model no instructions, so it must not have approved anything.
|
|
519
|
+
// Idempotent: a continuation call grants the same entries again, and
|
|
520
|
+
// the turn's set keeps one copy.
|
|
521
|
+
let grant: ReturnType<NonNullable<typeof context.grantSkillTools>> | undefined
|
|
522
|
+
if (allowed !== undefined && allowed.length > 0 && context.grantSkillTools) {
|
|
523
|
+
grant = context.grantSkillTools({
|
|
524
|
+
skill: skill.metadata.name,
|
|
525
|
+
allowedTools: allowed,
|
|
526
|
+
...(snapshot.skillDirectory ? { skillDirectory: snapshot.skillDirectory } : {}),
|
|
527
|
+
})
|
|
528
|
+
}
|
|
529
|
+
const notice = grantNotice(allowed, grant, context.grantSkillTools !== undefined)
|
|
530
|
+
const page = pageSkillBody({
|
|
531
|
+
snapshot,
|
|
331
532
|
digest,
|
|
332
533
|
start,
|
|
333
|
-
|
|
334
|
-
maxChars,
|
|
534
|
+
notice,
|
|
535
|
+
maxChars: context.maxToolOutputChars,
|
|
335
536
|
})
|
|
336
537
|
if (!page) {
|
|
337
538
|
return {
|
|
338
539
|
success: false,
|
|
339
540
|
output: '',
|
|
340
|
-
error:
|
|
341
|
-
'The model-visible tool-output budget is too small to list skill metadata safely. Increase maxToolOutputChars and retry.',
|
|
541
|
+
error: `The model-visible tool-output budget is too small to read "${input.name}" safely. Increase maxToolOutputChars and retry.`,
|
|
342
542
|
}
|
|
343
543
|
}
|
|
544
|
+
grant?.commit()
|
|
344
545
|
|
|
345
546
|
return {
|
|
346
547
|
success: true,
|
|
347
|
-
output:
|
|
548
|
+
output: page.output,
|
|
348
549
|
data: {
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
...(
|
|
550
|
+
skill: skill.metadata.name,
|
|
551
|
+
...(modelDirectory === undefined ? {} : { directory: modelDirectory }),
|
|
552
|
+
...(allowed === undefined ? {} : { allowedTools: allowed }),
|
|
553
|
+
...(grant ? { granted: grant.granted, ignored: grant.ignored } : {}),
|
|
554
|
+
...(page.nextCursor === undefined ? {} : { nextCursor: page.nextCursor }),
|
|
352
555
|
},
|
|
353
556
|
}
|
|
354
|
-
}
|
|
557
|
+
},
|
|
558
|
+
})
|
|
559
|
+
}
|
|
355
560
|
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
// nothing between them to drift.
|
|
359
|
-
const loaded = await context.skills.load(input.name)
|
|
360
|
-
if (!loaded) {
|
|
361
|
-
// Named, with what IS available. A bare "not found" sends the model
|
|
362
|
-
// guessing at spellings, and the manifest it is guessing from is
|
|
363
|
-
// right there in its own prompt.
|
|
364
|
-
const available = context.skills.names()
|
|
365
|
-
return {
|
|
366
|
-
success: false,
|
|
367
|
-
output: '',
|
|
368
|
-
error: `No skill named "${input.name}". Available: ${available.length > 0 ? available.join(', ') : '(none)'}`,
|
|
369
|
-
}
|
|
370
|
-
}
|
|
561
|
+
/** The `skill` tool with no host options: the listing carries the registry's `location`. */
|
|
562
|
+
export const SkillTool = createSkillTool()
|
|
371
563
|
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
// Reachable even though the manifest omits it: the model can name
|
|
376
|
-
// anything, and a check that only filtered the listing would be a
|
|
377
|
-
// menu restriction rather than a kitchen one — the exact defect
|
|
378
|
-
// `allowedTools` had before it was enforced at dispatch.
|
|
379
|
-
return {
|
|
380
|
-
success: false,
|
|
381
|
-
output: '',
|
|
382
|
-
error: `The skill "${input.name}" is ${skillInvocation(skill)}-invocable; it is not for you to run.`,
|
|
383
|
-
}
|
|
384
|
-
}
|
|
564
|
+
function directoryContextOf(sandbox: Sandbox | undefined): SkillDirectoryContext {
|
|
565
|
+
return sandbox ? { sandbox } : {}
|
|
566
|
+
}
|
|
385
567
|
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
allowedTools: allowed,
|
|
391
|
-
skillDirectory: skill.dirPath,
|
|
392
|
-
invocation,
|
|
393
|
-
}
|
|
394
|
-
const digest = snapshotDigest(snapshot)
|
|
395
|
-
let start = 0
|
|
396
|
-
if (input.cursor !== undefined) {
|
|
397
|
-
const parsed = parseCursor(input.cursor)
|
|
398
|
-
if (
|
|
399
|
-
!parsed ||
|
|
400
|
-
parsed.digest !== digest ||
|
|
401
|
-
parsed.offset >= snapshot.body.length ||
|
|
402
|
-
!isCodePointBoundary(snapshot.body, parsed.offset)
|
|
403
|
-
) {
|
|
404
|
-
return {
|
|
405
|
-
success: false,
|
|
406
|
-
output: '',
|
|
407
|
-
error: `The continuation cursor for "${input.name}" is stale or invalid. Call skill again without a cursor to read the current instructions.`,
|
|
408
|
-
}
|
|
409
|
-
}
|
|
410
|
-
start = parsed.offset
|
|
411
|
-
}
|
|
568
|
+
/** An empty string names no directory; treated as the resolver saying none. */
|
|
569
|
+
function nonEmpty(directory: string | undefined): string | undefined {
|
|
570
|
+
return directory === undefined || directory === '' ? undefined : directory
|
|
571
|
+
}
|
|
412
572
|
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
}
|
|
426
|
-
const notice = grantNotice(allowed, grant, context.grantSkillTools !== undefined)
|
|
427
|
-
const page = pageSkillBody({
|
|
428
|
-
snapshot,
|
|
429
|
-
digest,
|
|
430
|
-
start,
|
|
431
|
-
notice,
|
|
432
|
-
maxChars: context.maxToolOutputChars,
|
|
433
|
-
})
|
|
434
|
-
if (!page) {
|
|
435
|
-
return {
|
|
436
|
-
success: false,
|
|
437
|
-
output: '',
|
|
438
|
-
error: `The model-visible tool-output budget is too small to read "${input.name}" safely. Increase maxToolOutputChars and retry.`,
|
|
439
|
-
}
|
|
440
|
-
}
|
|
441
|
-
grant?.commit()
|
|
442
|
-
|
|
443
|
-
return {
|
|
444
|
-
success: true,
|
|
445
|
-
output: page.output,
|
|
446
|
-
data: {
|
|
447
|
-
skill: skill.metadata.name,
|
|
448
|
-
...(allowed === undefined ? {} : { allowedTools: allowed }),
|
|
449
|
-
...(grant ? { granted: grant.granted, ignored: grant.ignored } : {}),
|
|
450
|
-
...(page.nextCursor === undefined ? {} : { nextCursor: page.nextCursor }),
|
|
451
|
-
},
|
|
452
|
-
}
|
|
453
|
-
},
|
|
454
|
-
})
|
|
573
|
+
/**
|
|
574
|
+
* The line a load opens with once a host has said where the skill's files are.
|
|
575
|
+
*
|
|
576
|
+
* The unreachable case is said out loud rather than left blank. A model given
|
|
577
|
+
* no directory for a skill that says "run scripts/render.sh" goes looking for
|
|
578
|
+
* it, and on a sandboxed turn that search can only fail.
|
|
579
|
+
*/
|
|
580
|
+
function directoryHeader(directory: string | undefined): string {
|
|
581
|
+
return directory === undefined
|
|
582
|
+
? "[This skill's directory is not reachable from your tools in this session, so a file these instructions name by a relative path (scripts/, references/, assets/) cannot be opened here. Do not search the filesystem for it; if the task needs one, say so.]\n\n"
|
|
583
|
+
: `[Skill directory: ${directory}. Relative paths in these instructions, such as scripts/, references/ or assets/, are inside it.]\n\n`
|
|
584
|
+
}
|
|
455
585
|
|
|
456
586
|
/**
|
|
457
587
|
* What the model is told about `allowed-tools`.
|
package/src/tools/defineTool.ts
CHANGED
|
@@ -21,6 +21,8 @@ export interface DefineToolOptions<S extends z.ZodType> {
|
|
|
21
21
|
readOnly: boolean | ((input: z.infer<S>) => boolean)
|
|
22
22
|
destructive: boolean | ((input: z.infer<S>) => boolean)
|
|
23
23
|
concurrencySafe: boolean
|
|
24
|
+
/** Whether this exact call sends the screen to the provider; see {@link ToolDefinition.capturesScreen}. */
|
|
25
|
+
capturesScreen?: boolean | ((input: z.infer<S>) => boolean)
|
|
24
26
|
/** Batch ordering boundary; see {@link ToolDefinition.executionBarrier}. */
|
|
25
27
|
executionBarrier?: boolean
|
|
26
28
|
tier?: string
|
|
@@ -139,6 +141,14 @@ export function defineTool<S extends z.ZodType>(
|
|
|
139
141
|
? options.destructive
|
|
140
142
|
: constantDestructive(options.destructive as boolean),
|
|
141
143
|
isConcurrencySafe: () => options.concurrencySafe,
|
|
144
|
+
...(options.capturesScreen !== undefined
|
|
145
|
+
? {
|
|
146
|
+
capturesScreen:
|
|
147
|
+
typeof options.capturesScreen === 'function'
|
|
148
|
+
? options.capturesScreen
|
|
149
|
+
: () => options.capturesScreen as boolean,
|
|
150
|
+
}
|
|
151
|
+
: {}),
|
|
142
152
|
|
|
143
153
|
async execute(input: TInput, context: ToolContext): Promise<ToolResult> {
|
|
144
154
|
try {
|
|
@@ -33,6 +33,8 @@ export function presentScheduleCall(input: {
|
|
|
33
33
|
}
|
|
34
34
|
case 'list':
|
|
35
35
|
return activity('List scheduled jobs')
|
|
36
|
+
case 'update':
|
|
37
|
+
return activity(`Change scheduled job · ${oneLine(input.job)}`)
|
|
36
38
|
case 'pause':
|
|
37
39
|
return activity(`Pause scheduled job · ${oneLine(input.job)}`)
|
|
38
40
|
case 'resume':
|