@xingular/harness 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +799 -335
- package/dist/terminal-pi.js +2 -2
- package/package.json +4 -4
- package/src/accounts/env.test.ts +95 -0
- package/src/accounts/env.ts +176 -42
- package/src/capabilities/api.ts +3 -3
- package/src/harness.ts +12 -2
- package/src/index.ts +2 -1
- package/src/mcp/connections.ts +176 -3
- package/src/mcp/scopeMigration.ts +204 -0
- package/src/mcp/service.ts +18 -36
- package/src/mcp/types.ts +3 -0
- package/src/memory/api.ts +5 -3
- package/src/memory/dispatch.ts +2 -2
- package/src/memory/maintenance.test.ts +1 -1
- package/src/plugins/attachments.ts +13 -0
- package/src/plugins/catalogue.ts +3 -2
- package/src/plugins/manifest.ts +34 -2
- package/src/plugins/plugins.ts +8 -3
- package/src/plugins/types.ts +19 -0
- package/src/prompt/compile.ts +4 -4
- package/src/server/methods.ts +6 -0
- package/src/sessions/sessions.ts +10 -10
- package/src/skills/registry.ts +16 -3
- package/src/terminal/piExtension.ts +4 -2
- package/src/tools/memoryTools.ts +2 -2
package/src/plugins/catalogue.ts
CHANGED
|
@@ -75,6 +75,7 @@ function fromDescriptor(descriptor: PluginDescriptor, source: PluginSource, atta
|
|
|
75
75
|
required: descriptor.required ?? null,
|
|
76
76
|
manage: descriptor.manage ?? (descriptor.tools?.length ? 'tools' : null),
|
|
77
77
|
settings: (descriptor.settings ?? []).map((setting) => ({ ...setting, value: settingValue(setting, attachments?.settings[descriptor.id]) })),
|
|
78
|
+
connectorTemplates: descriptor.connectorTemplates ?? [],
|
|
78
79
|
attachment: switchable ? describe(attachments!, descriptor.id, switchable.on) : null,
|
|
79
80
|
contributions: [
|
|
80
81
|
...(descriptor.code ?? []).map((code) => contribution(descriptor.id, 'code', code.name, { description: code.description, state: on ? 'on' : 'off' })),
|
|
@@ -172,7 +173,7 @@ function foundContribution(plugin: string, item: Found, trusted: Trusted, input:
|
|
|
172
173
|
return contribution(plugin, kind, name, { ...rest, description: description ?? '', path: item.path })
|
|
173
174
|
}
|
|
174
175
|
|
|
175
|
-
const empty = (id: string, title: string, description: string, source: PluginSource, path: string | null): Plugin => ({ id, title, description, version: null, source, path, runsCode: false, trust: 'none', code: null, install: null, extends: [], required: null, manage: 'files', settings: [], attachment: null, contributions: [] })
|
|
176
|
+
const empty = (id: string, title: string, description: string, source: PluginSource, path: string | null): Plugin => ({ id, title, description, version: null, source, path, runsCode: false, trust: 'none', code: null, install: null, extends: [], required: null, manage: 'files', settings: [], connectorTemplates: [], attachment: null, contributions: [] })
|
|
176
177
|
|
|
177
178
|
export function catalogue(input: CatalogueInput): Plugin[] {
|
|
178
179
|
const plugins = new Map<string, Plugin>()
|
|
@@ -209,7 +210,7 @@ export function catalogue(input: CatalogueInput): Plugin[] {
|
|
|
209
210
|
const install = item.project ? null : item.packageRoot.startsWith(`${input.packagesDir}/`) ? ({ kind: 'folder', source: item.packageRoot } as const) : item.source ? ({ kind: 'settings', source: item.source } as const) : null
|
|
210
211
|
// Only parts that exist can be extended; a made-up name is dropped.
|
|
211
212
|
const extended = manifest.extends.filter((part) => input.descriptors.some((descriptor) => descriptor.id === part))
|
|
212
|
-
plugin = { ...empty(id, manifest.name, manifest.description, item.project ? 'project' : 'package', item.packageRoot), version: manifest.version, install, extends: extended }
|
|
213
|
+
plugin = { ...empty(id, manifest.name, manifest.description, item.project ? 'project' : 'package', item.packageRoot), version: manifest.version, install, extends: extended, connectorTemplates: manifest.connectorTemplates }
|
|
213
214
|
} else if (item.origin === 'home') plugin = empty(id, 'Home folder skills', 'Skills in the .agents folder of your home folder. Pi reads them for every account.', 'home', dirname(dirname(item.path)))
|
|
214
215
|
else if (item.origin === 'project') plugin = empty(id, 'Project folder', 'Found in the working folder: its .pi and .agents folders, and the instruction files Pi reads on the way up from it.', 'project', input.cwd)
|
|
215
216
|
else plugin = empty(id, 'Shared Pi folder', 'Extensions, skills, prompts and instruction files in the folder every account shares.', 'shared', input.sharedDir)
|
package/src/plugins/manifest.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
// What a package says about itself in its `package.json`: Pi's own fields, and under `harness`
|
|
2
2
|
// which part of the harness it extends and the file of hooks the harness runs for it.
|
|
3
3
|
import { basename, join } from 'node:path'
|
|
4
|
+
import { z } from 'zod'
|
|
4
5
|
|
|
5
6
|
import { readJsonOr, safeRelative } from '../core/fs'
|
|
7
|
+
import type { ConnectorTemplate } from './types'
|
|
6
8
|
|
|
7
9
|
export interface PackageManifest {
|
|
8
10
|
name: string
|
|
@@ -12,6 +14,8 @@ export interface PackageManifest {
|
|
|
12
14
|
extends: string[]
|
|
13
15
|
/** The hooks file, inside the package, or `null` when it has none. */
|
|
14
16
|
hooks: string | null
|
|
17
|
+
/** Data-only remote connector forms. Invalid templates are ignored; none can carry credentials. */
|
|
18
|
+
connectorTemplates: ConnectorTemplate[]
|
|
15
19
|
}
|
|
16
20
|
|
|
17
21
|
interface Raw {
|
|
@@ -21,6 +25,30 @@ interface Raw {
|
|
|
21
25
|
harness?: unknown
|
|
22
26
|
}
|
|
23
27
|
|
|
28
|
+
const connectorTemplateSchema = z.object({
|
|
29
|
+
id: z.string().regex(/^[a-z0-9-]+$/),
|
|
30
|
+
name: z.string().min(1).max(120),
|
|
31
|
+
description: z.string().max(500),
|
|
32
|
+
url: z.string().url().startsWith('https://'),
|
|
33
|
+
auth: z.enum(['none', 'headers', 'oauth']),
|
|
34
|
+
oauthClient: z.enum(['dynamic', 'user']).nullable(),
|
|
35
|
+
oauthScopes: z.array(z.string().max(500)).max(30),
|
|
36
|
+
docsUrl: z.string().url().startsWith('https://').nullable(),
|
|
37
|
+
fields: z.array(z.object({
|
|
38
|
+
header: z.string().regex(/^[A-Za-z0-9_-]+$/),
|
|
39
|
+
label: z.string().min(1).max(120),
|
|
40
|
+
prefix: z.string().max(100),
|
|
41
|
+
help: z.string().max(500)
|
|
42
|
+
}).strict()).max(12),
|
|
43
|
+
setup: z.array(z.string().max(1000)).max(20),
|
|
44
|
+
notice: z.string().max(1000).nullable()
|
|
45
|
+
}).strict().superRefine((template, context) => {
|
|
46
|
+
const oauth = template.auth === 'oauth'
|
|
47
|
+
const headers = template.auth === 'headers'
|
|
48
|
+
if (oauth !== (template.oauthClient !== null)) context.addIssue({ code: 'custom', message: 'OAuth templates must state a client type and other templates must not.', path: ['oauthClient'] })
|
|
49
|
+
if (headers !== (template.fields.length > 0)) context.addIssue({ code: 'custom', message: 'Header templates must declare input fields and other templates must not.', path: ['fields'] })
|
|
50
|
+
})
|
|
51
|
+
|
|
24
52
|
/** A path inside the package, or `null` for one that is missing, absolute, or climbs out of it. */
|
|
25
53
|
function inside(path: unknown): string | null {
|
|
26
54
|
if (typeof path !== 'string' || !path.trim()) return null
|
|
@@ -33,12 +61,16 @@ function inside(path: unknown): string | null {
|
|
|
33
61
|
|
|
34
62
|
export function packageManifest(packageRoot: string): PackageManifest {
|
|
35
63
|
const manifest = readJsonOr<Raw>(join(packageRoot, 'package.json'), {})
|
|
36
|
-
const own = (manifest.harness && typeof manifest.harness === 'object' && !Array.isArray(manifest.harness) ? manifest.harness : {}) as { extends?: unknown; hooks?: unknown }
|
|
64
|
+
const own = (manifest.harness && typeof manifest.harness === 'object' && !Array.isArray(manifest.harness) ? manifest.harness : {}) as { extends?: unknown; hooks?: unknown; connectors?: unknown }
|
|
37
65
|
return {
|
|
38
66
|
name: typeof manifest.name === 'string' && manifest.name ? manifest.name : basename(packageRoot),
|
|
39
67
|
description: typeof manifest.description === 'string' ? manifest.description : '',
|
|
40
68
|
version: typeof manifest.version === 'string' ? manifest.version : null,
|
|
41
69
|
extends: (Array.isArray(own.extends) ? own.extends : typeof own.extends === 'string' ? [own.extends] : []).filter((id): id is string => typeof id === 'string' && !!id),
|
|
42
|
-
hooks: inside(own.hooks)
|
|
70
|
+
hooks: inside(own.hooks),
|
|
71
|
+
connectorTemplates: (Array.isArray(own.connectors) ? own.connectors : []).flatMap((value) => {
|
|
72
|
+
const parsed = connectorTemplateSchema.safeParse(value)
|
|
73
|
+
return parsed.success ? [parsed.data] : []
|
|
74
|
+
})
|
|
43
75
|
}
|
|
44
76
|
}
|
package/src/plugins/plugins.ts
CHANGED
|
@@ -78,6 +78,8 @@ export function packageFolders(directory: string | null): string[] {
|
|
|
78
78
|
export interface LoadingInput extends ResolveInput {
|
|
79
79
|
/** Who the session is for. Something switched off for this agent or project does not load. */
|
|
80
80
|
caller?: AttachScope
|
|
81
|
+
/** The contributions frozen for a run. When present, this snapshot governs loading. */
|
|
82
|
+
contributions?: readonly string[]
|
|
81
83
|
}
|
|
82
84
|
|
|
83
85
|
/** Which code a person trusted: the working folder's as a whole, and each installed package's. */
|
|
@@ -142,8 +144,11 @@ export class Plugins {
|
|
|
142
144
|
if (item.kind === 'hook') return !attached(choices, [pluginIdOf(item)], {}, true)
|
|
143
145
|
return !attached(choices, foundIds(item), input.caller ?? {}, true)
|
|
144
146
|
}
|
|
145
|
-
const
|
|
146
|
-
|
|
147
|
+
const allowed = (item: Found): boolean => input.contributions === undefined
|
|
148
|
+
? !off(item)
|
|
149
|
+
: input.contributions.includes(foundIds(item)[0])
|
|
150
|
+
const detached = found.filter((item) => !allowed(item)).map((item) => item.path)
|
|
151
|
+
const chosen = found.filter((item) => loads(item, trusted) && allowed(item))
|
|
147
152
|
const paths = (kind: Found['kind'], builtin: boolean): string[] => chosen.filter((item) => item.kind === kind && (item.origin === 'builtin') === builtin).map((item) => item.path)
|
|
148
153
|
return {
|
|
149
154
|
extensionPaths: paths('extension', false),
|
|
@@ -154,7 +159,7 @@ export class Plugins {
|
|
|
154
159
|
hooks: chosen.filter((item) => item.kind === 'hook' && item.packageRoot).map((item) => ({ path: item.path, packageRoot: item.packageRoot!, hash: packageCodeState[item.packageRoot!]?.hash ?? '' })),
|
|
155
160
|
found,
|
|
156
161
|
detached,
|
|
157
|
-
resourceAllowed:
|
|
162
|
+
resourceAllowed: allowed,
|
|
158
163
|
projectCode: projectCodeState,
|
|
159
164
|
packageCode: packageCodeState
|
|
160
165
|
}
|
package/src/plugins/types.ts
CHANGED
|
@@ -65,6 +65,8 @@ export interface Plugin {
|
|
|
65
65
|
manage: PluginManage | null
|
|
66
66
|
/** Numbers a person can set on it, each with its current value. */
|
|
67
67
|
settings: (PluginSetting & { value: number })[]
|
|
68
|
+
/** Declarative connector setup templates; listing one never creates or enables a connection. */
|
|
69
|
+
connectorTemplates: ConnectorTemplate[]
|
|
68
70
|
/** Where the whole plugin is switched on and off, or `null` when only its parts are, or nothing is. */
|
|
69
71
|
attachment: Attachment | null
|
|
70
72
|
contributions: Contribution[]
|
|
@@ -80,6 +82,21 @@ export interface PluginSetting {
|
|
|
80
82
|
max: number
|
|
81
83
|
}
|
|
82
84
|
|
|
85
|
+
/** A remote connector setup form. It contains no endpoint credentials or saved connection state. */
|
|
86
|
+
export interface ConnectorTemplate {
|
|
87
|
+
id: string
|
|
88
|
+
name: string
|
|
89
|
+
description: string
|
|
90
|
+
url: string
|
|
91
|
+
auth: 'none' | 'headers' | 'oauth'
|
|
92
|
+
oauthClient: 'dynamic' | 'user' | null
|
|
93
|
+
oauthScopes: string[]
|
|
94
|
+
docsUrl: string | null
|
|
95
|
+
fields: { header: string; label: string; prefix: string; help: string }[]
|
|
96
|
+
setup: string[]
|
|
97
|
+
notice: string | null
|
|
98
|
+
}
|
|
99
|
+
|
|
83
100
|
/** What a part of the harness, or the embedding application, says about itself. Data only. */
|
|
84
101
|
export interface PluginDescriptor {
|
|
85
102
|
id: string
|
|
@@ -97,6 +114,8 @@ export interface PluginDescriptor {
|
|
|
97
114
|
switchable?: { on: boolean }
|
|
98
115
|
/** Numbers a person can set on it. */
|
|
99
116
|
settings?: PluginSetting[]
|
|
117
|
+
/** Remote connector setup templates; users must add, configure and enable each connection. */
|
|
118
|
+
connectorTemplates?: ConnectorTemplate[]
|
|
100
119
|
/** Code of its own that runs in a session. */
|
|
101
120
|
code?: { name: string; description: string }[]
|
|
102
121
|
}
|
package/src/prompt/compile.ts
CHANGED
|
@@ -405,10 +405,10 @@ function hostSection(host: HostSection, kept: boolean): Section | null {
|
|
|
405
405
|
}
|
|
406
406
|
|
|
407
407
|
/** Compiles the pack for a caller. It only reads the data root. */
|
|
408
|
-
export function compile(env: HarnessEnv, context: CallerContext, launch: Launch = {}): ContextPack {
|
|
408
|
+
export function compile(env: HarnessEnv, context: CallerContext, launch: Launch = {}, contributions?: readonly string[]): ContextPack {
|
|
409
409
|
const { root } = env
|
|
410
410
|
const wiki = store.wikiDir(root)
|
|
411
|
-
const active = registry.enabledFor(root, context.projectId ?? null, context.agentId ?? null)
|
|
411
|
+
const active = registry.enabledFor(root, context.projectId ?? null, context.agentId ?? null, contributions)
|
|
412
412
|
const groups = toolGroups(context, launch)
|
|
413
413
|
const contextProfile = profileFor(root, context)
|
|
414
414
|
const keep = (id: string): boolean => id === 'references' || keeps(contextProfile, id)
|
|
@@ -573,8 +573,8 @@ function sameSection(left: Section | undefined, right: Section | undefined): boo
|
|
|
573
573
|
* Recording the current pack does not mark an update delivered: the session saves that snapshot
|
|
574
574
|
* only when it appends the corresponding context message.
|
|
575
575
|
*/
|
|
576
|
-
export function sessionLaunch(env: HarnessEnv, context: CallerContext, launch: Launch, historyFile: string | null): { system: string; update: ContextUpdate | null } {
|
|
577
|
-
const pack = compile(env, context, launch)
|
|
576
|
+
export function sessionLaunch(env: HarnessEnv, context: CallerContext, launch: Launch, historyFile: string | null, contributions?: readonly string[]): { system: string; update: ContextUpdate | null } {
|
|
577
|
+
const pack = compile(env, context, launch, contributions)
|
|
578
578
|
const dynamic = [...BUILT_IN_DYNAMIC, ...(launch.sections ?? []).filter((host) => host.dynamic).map((host) => ({ id: host.id, order: host.order }))]
|
|
579
579
|
.map((slot, index) => ({ ...slot, index }))
|
|
580
580
|
.sort((left, right) => left.order - right.order || left.index - right.index)
|
package/src/server/methods.ts
CHANGED
|
@@ -10,6 +10,7 @@ import { HarnessError, invalid } from '../core/errors'
|
|
|
10
10
|
import { projectsOrNone } from '../core/projects'
|
|
11
11
|
import * as capabilities from '../capabilities/api'
|
|
12
12
|
import { loadRun as loadCapabilitySnapshot } from '../capabilities/runSnapshot'
|
|
13
|
+
import { capabilitySet } from '../capabilities/runSnapshot'
|
|
13
14
|
import type { Harness } from '../harness'
|
|
14
15
|
import * as blocks from '../memory/blocks'
|
|
15
16
|
import { validateScopeIds } from '../mcp/connections'
|
|
@@ -255,6 +256,11 @@ export function methods(harness: Harness): Record<string, Handler> {
|
|
|
255
256
|
'capabilities.detach': (params: Params) => capabilities.switchCapability(harness, { ...pluginScope(params), id: text(params, 'id'), scope: params.scope ?? null }, false),
|
|
256
257
|
'capabilities.resolve': (params: Params) => capabilities.resolve(harness, { ...pluginScope(params), tools: Array.isArray(params.tools) ? params.tools.filter((item: unknown): item is string => typeof item === 'string') : [] }),
|
|
257
258
|
'capabilities.run': (params: Params) => loadCapabilitySnapshot(root(), text(params, 'runId')),
|
|
259
|
+
'capabilities.intersect': (params: Params) => capabilities.toolIntersection(
|
|
260
|
+
capabilitySet(params.parent),
|
|
261
|
+
capabilitySet(params.assignee),
|
|
262
|
+
params.tools === undefined ? undefined : Array.isArray(params.tools) ? params.tools.filter((item: unknown): item is string => typeof item === 'string') : []
|
|
263
|
+
),
|
|
258
264
|
|
|
259
265
|
// ---- Runs --------------------------------------------------------------------------------------
|
|
260
266
|
'runs.load': (params: Params) => snapshot(root(), text(params, 'runId')),
|
package/src/sessions/sessions.ts
CHANGED
|
@@ -83,15 +83,15 @@ export interface SessionsOptions {
|
|
|
83
83
|
/** Chooses what a session loads. Without it Pi discovers extensions and skills on its own. */
|
|
84
84
|
plugins?: Plugins
|
|
85
85
|
/** Resolves the caller's capability set from the harness's stores. */
|
|
86
|
-
resolveCapabilities?: (caller: CallerContext, tools: readonly string[]) => Promise<CapabilitySet>
|
|
86
|
+
resolveCapabilities?: (caller: CallerContext, tools: readonly string[], cwd: string) => Promise<CapabilitySet>
|
|
87
87
|
factory?: SessionFactory
|
|
88
88
|
}
|
|
89
89
|
|
|
90
90
|
/** The harness's own plugins a person switched on for this caller, with the numbers they set. */
|
|
91
|
-
function firstPartyFor(root: string,
|
|
91
|
+
function firstPartyFor(root: string, contributions: readonly string[]): InlineExtension[] {
|
|
92
92
|
const choices = attachments.load(root)
|
|
93
|
-
const
|
|
94
|
-
return firstPartyExtensions((id) =>
|
|
93
|
+
const allowed = new Set(contributions)
|
|
94
|
+
return firstPartyExtensions((id) => FIRST_PARTY_PLUGINS.find((plugin) => plugin.id === id)?.code?.some((code) => allowed.has(`${id}/code/${code.name}`)) ?? false, choices.settings)
|
|
95
95
|
}
|
|
96
96
|
|
|
97
97
|
export class Sessions {
|
|
@@ -161,7 +161,7 @@ export class Sessions {
|
|
|
161
161
|
...(broker?.toolNames ?? [])
|
|
162
162
|
])]
|
|
163
163
|
const available = this.options.resolveCapabilities
|
|
164
|
-
? await this.options.resolveCapabilities(caller, requestedTools)
|
|
164
|
+
? await this.options.resolveCapabilities(caller, requestedTools, input.cwd)
|
|
165
165
|
: { version: 1 as const, tools: requestedTools, connectors: [], contributions: [] }
|
|
166
166
|
const requested = input.capabilities ? parseCapabilitySet(input.capabilities) : available
|
|
167
167
|
const capabilities: CapabilitySet = {
|
|
@@ -222,7 +222,7 @@ export class Sessions {
|
|
|
222
222
|
connectionTools: (launch.connectionTools ?? []).filter((name) => capabilities.tools.includes(name)),
|
|
223
223
|
toolGroups: launch.toolGroups?.map((group) => ({ ...group, tools: group.tools.filter((name) => capabilities.tools.includes(name)) })).filter((group) => group.tools.length > 0)
|
|
224
224
|
}
|
|
225
|
-
;({ system, update } = sessionLaunch(this.env, caller, effectiveLaunch, historyFile))
|
|
225
|
+
;({ system, update } = sessionLaunch(this.env, caller, effectiveLaunch, historyFile, capabilities.contributions))
|
|
226
226
|
} catch (error) {
|
|
227
227
|
fail(error)
|
|
228
228
|
throw error
|
|
@@ -239,7 +239,7 @@ export class Sessions {
|
|
|
239
239
|
try {
|
|
240
240
|
const tools: ToolSpec[] = [
|
|
241
241
|
...(broker ? broker.tools(session, { readOnly: typeof input.connections === 'object' && input.connections.readOnly === true }) : []),
|
|
242
|
-
...memoryTools(this.options.env, caller),
|
|
242
|
+
...memoryTools(this.options.env, caller, capabilities.contributions),
|
|
243
243
|
askUserTool(run),
|
|
244
244
|
pluginProposeTool(this.options.env, caller)
|
|
245
245
|
]
|
|
@@ -257,9 +257,9 @@ export class Sessions {
|
|
|
257
257
|
preamble: input.preamble ?? null,
|
|
258
258
|
automatic: input.automatic,
|
|
259
259
|
account: context,
|
|
260
|
-
resources: registry.resources(root, caller.projectId ?? null, caller.agentId ?? null),
|
|
261
|
-
inline: [...firstPartyFor(root,
|
|
262
|
-
...(plugins ? { plugins: { loading: () => plugins.loading({ cwd: input.cwd, agentDir: context.configDir, install: true, caller: { agentId: caller.agentId ?? null, projectId: caller.projectId ?? null } }), loaded: (extensions) => plugins.loaded(extensions) } } : {}),
|
|
260
|
+
resources: registry.resources(root, caller.projectId ?? null, caller.agentId ?? null, capabilities.contributions),
|
|
261
|
+
inline: [...firstPartyFor(root, capabilities.contributions), runLineageJoin(root, runId)],
|
|
262
|
+
...(plugins ? { plugins: { loading: () => plugins.loading({ cwd: input.cwd, agentDir: context.configDir, install: true, caller: { agentId: caller.agentId ?? null, projectId: caller.projectId ?? null }, contributions: capabilities.contributions }), loaded: (extensions) => plugins.loaded(extensions) } } : {}),
|
|
263
263
|
system,
|
|
264
264
|
update
|
|
265
265
|
}
|
package/src/skills/registry.ts
CHANGED
|
@@ -31,6 +31,18 @@ export interface RegistryItem {
|
|
|
31
31
|
builtin?: true
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
+
/** The capability id used when this registry item is shown in the plugin catalogue. */
|
|
35
|
+
export function contributionId(item: Pick<RegistryItem, 'id' | 'builtin'>): string {
|
|
36
|
+
return `${item.builtin ? 'included' : 'yours'}/${item.id}`
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Limits registry items to the contributions frozen for a run. `undefined` means no limit. */
|
|
40
|
+
export function limitToContributions(items: readonly RegistryItem[], contributions?: readonly string[]): RegistryItem[] {
|
|
41
|
+
if (contributions === undefined) return [...items]
|
|
42
|
+
const allowed = new Set(contributions)
|
|
43
|
+
return items.filter((item) => allowed.has(contributionId(item)))
|
|
44
|
+
}
|
|
45
|
+
|
|
34
46
|
export interface SkillBundleInput {
|
|
35
47
|
name: string
|
|
36
48
|
files: { path: string; content: string; encoding: 'utf8' | 'base64' }[]
|
|
@@ -138,7 +150,8 @@ export const isItemId = (id: string): boolean => KINDS.some((kind) => id.startsW
|
|
|
138
150
|
* Items active for a caller. A built-in is on until it is switched off; a person's own is off
|
|
139
151
|
* until it is switched on, everywhere, for the caller's project, or for the calling agent.
|
|
140
152
|
*/
|
|
141
|
-
export function enabledFor(root: string, projectId: string | null, agentId: string | null = null): RegistryItem[] {
|
|
153
|
+
export function enabledFor(root: string, projectId: string | null, agentId: string | null = null, contributions?: readonly string[]): RegistryItem[] {
|
|
154
|
+
if (contributions !== undefined) return limitToContributions(list(root), contributions)
|
|
142
155
|
const choices = attachments.load(root)
|
|
143
156
|
return list(root).filter((item) => attachments.attached(choices, [item.id], { agentId, projectId }, !!item.builtin))
|
|
144
157
|
}
|
|
@@ -160,8 +173,8 @@ export interface RegistryResources {
|
|
|
160
173
|
}
|
|
161
174
|
|
|
162
175
|
/** What a Pi session needs to load enabled items natively. Rule text reaches the model through the compiled prompt. */
|
|
163
|
-
export function resources(root: string, projectId: string | null, agentId: string | null = null): RegistryResources {
|
|
164
|
-
const items = enabledFor(root, projectId, agentId)
|
|
176
|
+
export function resources(root: string, projectId: string | null, agentId: string | null = null, contributions?: readonly string[]): RegistryResources {
|
|
177
|
+
const items = enabledFor(root, projectId, agentId, contributions)
|
|
165
178
|
return {
|
|
166
179
|
ids: items.map((item) => item.id),
|
|
167
180
|
skillPaths: items.filter((item) => item.kind === 'skills').map((item) => item.path),
|
|
@@ -86,7 +86,8 @@ export default async function (pi: ExtensionAPI) {
|
|
|
86
86
|
if (command?.type === 'interrupt') context?.abort()
|
|
87
87
|
else if (command?.type === 'send') {
|
|
88
88
|
if (context?.ui.getEditorText().trim()) throw new Error('The terminal contains a user draft.')
|
|
89
|
-
|
|
89
|
+
// Pi 1.2 needs deliverAs while a run is busy; a message sent then waits for that run to finish.
|
|
90
|
+
pi.sendUserMessage(command.text!, context?.isIdle() === false ? { deliverAs: 'followUp' } : undefined)
|
|
90
91
|
}
|
|
91
92
|
})
|
|
92
93
|
.catch(() => {
|
|
@@ -104,7 +105,8 @@ export default async function (pi: ExtensionAPI) {
|
|
|
104
105
|
usage = emptyUsage()
|
|
105
106
|
reasoningTokens = undefined
|
|
106
107
|
costReported = false
|
|
107
|
-
|
|
108
|
+
// A run a plugin starts with a custom message has no user prompt to record; it still begins a turn.
|
|
109
|
+
await terminalCall('begin', { text: event.messageType === 'user' ? event.prompt : '' })
|
|
108
110
|
})
|
|
109
111
|
pi.on('message_end', async (event) => {
|
|
110
112
|
if (event.message.role !== 'assistant') return
|
package/src/tools/memoryTools.ts
CHANGED
|
@@ -4,7 +4,7 @@ import type { CallerContext } from '../memory/caller'
|
|
|
4
4
|
import { describeTool, WIKI_AGENT_TOOLS } from './definitions'
|
|
5
5
|
import type { ToolSpec } from './types'
|
|
6
6
|
|
|
7
|
-
export function memoryTools(env: DispatchEnv, context: CallerContext): ToolSpec[] {
|
|
7
|
+
export function memoryTools(env: DispatchEnv, context: CallerContext, contributions?: readonly string[]): ToolSpec[] {
|
|
8
8
|
return WIKI_AGENT_TOOLS.map((tool) => ({
|
|
9
9
|
name: tool.name,
|
|
10
10
|
label: tool.label,
|
|
@@ -12,7 +12,7 @@ export function memoryTools(env: DispatchEnv, context: CallerContext): ToolSpec[
|
|
|
12
12
|
parameters: (Type) => Type.Unsafe(tool.parameters),
|
|
13
13
|
execute: async (input) => {
|
|
14
14
|
try {
|
|
15
|
-
return dispatch(env, context, tool.call, input) || 'Nothing found.'
|
|
15
|
+
return dispatch(env, context, tool.call, input, contributions) || 'Nothing found.'
|
|
16
16
|
} catch (error) {
|
|
17
17
|
// The model sees the reason and can adjust, rather than the run failing.
|
|
18
18
|
return `The Wiki could not do that: ${error instanceof Error ? error.message : String(error)}`
|