@standardagents/code-plugin-sdk 0.0.0-stub.0 → 1.0.0-alpha.1
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 +174 -3
- package/bin/standard-plugin.mjs +149 -0
- package/package.json +17 -4
- package/src/collection.mjs +0 -0
- package/src/index.d.ts +310 -0
- package/src/index.mjs +13 -0
- package/src/internal.d.ts +39 -0
- package/src/internal.mjs +5 -0
- package/src/manifest.mjs +86 -0
- package/src/protocol.mjs +166 -0
- package/src/publish.mjs +62 -0
- package/src/runtime.mjs +354 -0
- package/src/testing.d.ts +24 -0
- package/src/testing.mjs +104 -0
package/README.md
CHANGED
|
@@ -1,4 +1,175 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Standard Code plugin SDK
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
`@standardagents/code-plugin-sdk` is the authoring package for Standard Code
|
|
4
|
+
plugins. Standard Code bundles the SDK runtime into its signed release. The
|
|
5
|
+
manifest `apiVersion` gates compatibility between a plugin and the product.
|
|
6
|
+
|
|
7
|
+
A plugin exports `definePlugin({ id, activate })` as its default export.
|
|
8
|
+
Its `package.json` includes a static `standardPlugin` manifest.
|
|
9
|
+
|
|
10
|
+
```json
|
|
11
|
+
{
|
|
12
|
+
"name": "example-status",
|
|
13
|
+
"type": "module",
|
|
14
|
+
"peerDependencies": { "@standardagents/code-plugin-sdk": "^1.0.0-alpha.0" },
|
|
15
|
+
"devDependencies": { "@standardagents/code-plugin-sdk": "^1.0.0-alpha.0" },
|
|
16
|
+
"standardPlugin": {
|
|
17
|
+
"apiVersion": 1,
|
|
18
|
+
"id": "example-status",
|
|
19
|
+
"name": "Example status",
|
|
20
|
+
"version": "0.1.0",
|
|
21
|
+
"entry": "./index.ts",
|
|
22
|
+
"capabilities": ["surfaces"],
|
|
23
|
+
"contributions": [{ "id": "status", "kind": "section", "anchor": "plugins" }]
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
```ts
|
|
29
|
+
import { definePlugin } from '@standardagents/code-plugin-sdk'
|
|
30
|
+
|
|
31
|
+
export default definePlugin({
|
|
32
|
+
id: 'example-status',
|
|
33
|
+
activate(ctx) {
|
|
34
|
+
const section = ctx.section('status')
|
|
35
|
+
section.replace({ kind: 'rows', rows: [{ id: 'ready', spans: [{ text: 'Ready' }] }] })
|
|
36
|
+
},
|
|
37
|
+
})
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
The SDK is a peer dependency. Inside Standard Code the exact specifier
|
|
41
|
+
`@standardagents/code-plugin-sdk` resolves to the bundled copy, so a plugin
|
|
42
|
+
never loads a second runtime from `node_modules`. Subpath imports of the SDK
|
|
43
|
+
package, such as `@standardagents/code-plugin-sdk/testing`, are refused when
|
|
44
|
+
the plugin runs inside Standard Code. The `testing` export serves the
|
|
45
|
+
plugin's own test suite.
|
|
46
|
+
|
|
47
|
+
The runtime disposes publishers, subscriptions, schedules and pending requests.
|
|
48
|
+
Plugins register additional cleanup through `ctx.onDispose` or an activation return value.
|
|
49
|
+
Handlers receive an abort signal.
|
|
50
|
+
Schedules allow one invocation at a time.
|
|
51
|
+
|
|
52
|
+
Capabilities control SDK and daemon operations.
|
|
53
|
+
Plugins retain Node access to files, networking and subprocesses under the user's identity.
|
|
54
|
+
Worker threads provide JavaScript fault isolation.
|
|
55
|
+
Memory exhaustion outside V8 limits and native crashes can affect the runner process.
|
|
56
|
+
|
|
57
|
+
## Collections
|
|
58
|
+
|
|
59
|
+
A plugin installs from a Git repository or an npm package. One source holds
|
|
60
|
+
one plugin or a collection of plugins.
|
|
61
|
+
|
|
62
|
+
A collection lists its plugins in `standard-plugins.json` at the root of the
|
|
63
|
+
repository or package:
|
|
64
|
+
|
|
65
|
+
```json
|
|
66
|
+
{
|
|
67
|
+
"schema": 1,
|
|
68
|
+
"plugins": [
|
|
69
|
+
{ "id": "builds-monitor", "path": "builds-monitor" },
|
|
70
|
+
{ "id": "pong", "path": "games/pong" }
|
|
71
|
+
]
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
`schema` is `1`. `plugins` holds 1 to 256 entries. Each `id` follows the
|
|
76
|
+
plugin id pattern of the manifest and equals the `standardPlugin.id` in that
|
|
77
|
+
entry's `package.json`. Each `path` is a relative POSIX path of at most 512
|
|
78
|
+
characters with no leading slash, no backslash, and no empty, `.`, or `..`
|
|
79
|
+
segment. Ids and paths are unique within one collection. An installer
|
|
80
|
+
selects one entry with `--plugin <id>`.
|
|
81
|
+
|
|
82
|
+
A root without `standard-plugins.json` is a single plugin when its
|
|
83
|
+
`package.json` carries a valid `standardPlugin` manifest. Discovery treats it
|
|
84
|
+
as one entry at path `""`.
|
|
85
|
+
|
|
86
|
+
`validateCollection(value)` checks a parsed collection file and returns a
|
|
87
|
+
frozen `PluginCollection`. `resolveCollection({ collection, packageJson })`
|
|
88
|
+
applies the single-plugin fallback. `COLLECTION_FILE` names the file.
|
|
89
|
+
|
|
90
|
+
## Dependencies
|
|
91
|
+
|
|
92
|
+
A plugin with `dependencies` or `optionalDependencies` ships a lockfile
|
|
93
|
+
beside its `package.json`, so every machine installs the same dependency
|
|
94
|
+
tree. The installer refuses a plugin with dependencies and no lockfile.
|
|
95
|
+
|
|
96
|
+
For an npm package the lockfile is `npm-shrinkwrap.json`. npm never
|
|
97
|
+
publishes `package-lock.json`; `npm shrinkwrap` converts an existing
|
|
98
|
+
`package-lock.json` into `npm-shrinkwrap.json`, and npm includes that file
|
|
99
|
+
when it publishes the package. For a Git repository either
|
|
100
|
+
`package-lock.json` or `npm-shrinkwrap.json` satisfies the rule.
|
|
101
|
+
|
|
102
|
+
The SDK belongs under `peerDependencies`, with a `devDependencies` copy for
|
|
103
|
+
the plugin's own tests. Inside Standard Code the bundled runtime supplies it.
|
|
104
|
+
|
|
105
|
+
`lockfileRequirement({ packageJson, sourceKind })` returns whether the rule
|
|
106
|
+
applies and which lockfile names satisfy it for `"npm"` or `"git"`.
|
|
107
|
+
`checkPackageForPublish({ packageJson, files })` reports problems as
|
|
108
|
+
`{ code, message }` objects: a missing lockfile, the SDK under
|
|
109
|
+
`dependencies`, a missing peer declaration, a missing or invalid manifest, an
|
|
110
|
+
entry outside the package files, or an id that differs from the collection
|
|
111
|
+
entry. Both helpers are pure; they read no files.
|
|
112
|
+
|
|
113
|
+
In a collection, each plugin directory holds its own `package.json` and its
|
|
114
|
+
own lockfile.
|
|
115
|
+
|
|
116
|
+
## Publishing a plugin
|
|
117
|
+
|
|
118
|
+
The package installs a `standard-plugin` command for authoring.
|
|
119
|
+
|
|
120
|
+
```sh
|
|
121
|
+
npx standard-plugin check
|
|
122
|
+
npx standard-plugin pack
|
|
123
|
+
npm publish
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
`check [dir] [--source npm|git]` reads `package.json`, the optional
|
|
127
|
+
`standard-plugins.json`, every collection entry's `package.json`, and the
|
|
128
|
+
lockfiles. It prints each problem as `<path>: <code>: <message>` and exits
|
|
129
|
+
with status 1 when it finds one. `--source git` applies the Git lockfile
|
|
130
|
+
rule; the default is `npm`.
|
|
131
|
+
|
|
132
|
+
`pack [dir]` runs `check`. When a plugin has dependencies and no
|
|
133
|
+
`npm-shrinkwrap.json`, it runs `npm shrinkwrap`, which converts
|
|
134
|
+
`package-lock.json`, and asks you to commit the new file. It then runs
|
|
135
|
+
`npm pack --dry-run` and prints the files that npm will publish.
|
|
136
|
+
|
|
137
|
+
The command is authoring tooling. The SDK entry never imports it, and a
|
|
138
|
+
plugin never depends on it at run time.
|
|
139
|
+
|
|
140
|
+
## Testing
|
|
141
|
+
|
|
142
|
+
`@standardagents/code-plugin-sdk/testing` exports `createHarness`.
|
|
143
|
+
It uses explicit fixture handlers and a manual clock.
|
|
144
|
+
`activate`, `emit`, `visibility`, `advance`, `flush` and `dispose` drive its runtime.
|
|
145
|
+
`trace`, `surfaces`, `subscriptions` and `resources` expose recorded behavior.
|
|
146
|
+
`drainTrace` clears the bounded trace.
|
|
147
|
+
The harness starts no subprocesses.
|
|
148
|
+
|
|
149
|
+
## Supported interface
|
|
150
|
+
|
|
151
|
+
The public interface is the package's default export, its `testing` export,
|
|
152
|
+
the declarations in `src/index.d.ts` and `src/testing.d.ts`, and the
|
|
153
|
+
`standard-plugin` command's `check` and `pack` behavior.
|
|
154
|
+
|
|
155
|
+
The harness records the frames that the SDK runtime exchanges with its host,
|
|
156
|
+
and `harness.receive` accepts such a frame. That wire protocol between the
|
|
157
|
+
SDK runtime and the Standard Code plugin runner is visible through the
|
|
158
|
+
testing helpers, and it is not a supported public interface. Its frame
|
|
159
|
+
shapes, operation names, limits and version can change in any release
|
|
160
|
+
without notice. Plugin code and plugin tests should treat recorded frames as
|
|
161
|
+
opaque values and drive the plugin through `PluginContext` and the harness
|
|
162
|
+
methods.
|
|
163
|
+
|
|
164
|
+
Local `ctx.state` belongs to one machine.
|
|
165
|
+
The public context declarations are in `src/index.d.ts`.
|
|
166
|
+
|
|
167
|
+
## Releases
|
|
168
|
+
|
|
169
|
+
The Standard Code build workflow publishes this package when the version in
|
|
170
|
+
`package.json` changes. Prereleases publish under the `next` dist-tag and
|
|
171
|
+
releases under `latest`.
|
|
172
|
+
|
|
173
|
+
## License
|
|
174
|
+
|
|
175
|
+
MIT. See `LICENSE`.
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Authoring tool for plugin packages. The SDK entry never imports this file.
|
|
3
|
+
import { spawnSync } from 'node:child_process'
|
|
4
|
+
import { existsSync, readdirSync, readFileSync } from 'node:fs'
|
|
5
|
+
import { join, resolve } from 'node:path'
|
|
6
|
+
import { COLLECTION_FILE, PluginError, checkPackageForPublish, validateCollection } from '../src/index.mjs'
|
|
7
|
+
|
|
8
|
+
const USAGE = `usage: standard-plugin check [dir] [--source npm|git]
|
|
9
|
+
standard-plugin pack [dir]
|
|
10
|
+
|
|
11
|
+
check validates package.json, ${COLLECTION_FILE}, each collection entry, and lockfiles
|
|
12
|
+
pack runs check, creates npm-shrinkwrap.json when dependencies need one, then runs npm pack --dry-run`
|
|
13
|
+
const IGNORED_DIRECTORIES = new Set(['node_modules', '.git'])
|
|
14
|
+
const FILE_LIMIT = 50000
|
|
15
|
+
|
|
16
|
+
function readJson(path) {
|
|
17
|
+
let text
|
|
18
|
+
try { text = readFileSync(path, 'utf8') } catch (error) {
|
|
19
|
+
if (error.code === 'ENOENT') return undefined
|
|
20
|
+
throw error
|
|
21
|
+
}
|
|
22
|
+
return JSON.parse(text)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Relative POSIX paths of regular files under root, without following links. */
|
|
26
|
+
function listFiles(root) {
|
|
27
|
+
const files = []
|
|
28
|
+
const pending = ['']
|
|
29
|
+
while (pending.length) {
|
|
30
|
+
const directory = pending.pop()
|
|
31
|
+
for (const entry of readdirSync(join(root, directory), { withFileTypes: true })) {
|
|
32
|
+
const path = directory ? `${directory}/${entry.name}` : entry.name
|
|
33
|
+
if (entry.isDirectory()) { if (!IGNORED_DIRECTORIES.has(entry.name)) pending.push(path) }
|
|
34
|
+
else if (entry.isFile()) files.push(path)
|
|
35
|
+
if (files.length > FILE_LIMIT) throw new Error(`More than ${FILE_LIMIT} files under ${root}`)
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return files
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function tryJson(root, name, problems, location) {
|
|
42
|
+
try { return readJson(join(root, name)) } catch (error) {
|
|
43
|
+
problems.push({ location, code: 'invalid_json', message: `${name}: ${error.message}` })
|
|
44
|
+
return undefined
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function collect(problems, location, list) {
|
|
49
|
+
for (const problem of list) problems.push({ location, ...problem })
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Checks one source directory. Returns problems with the directory each one belongs to. */
|
|
53
|
+
function checkDirectory(root, { sourceKind = 'npm' } = {}) {
|
|
54
|
+
const problems = []
|
|
55
|
+
const files = listFiles(root)
|
|
56
|
+
const packageJson = tryJson(root, 'package.json', problems, '.')
|
|
57
|
+
const collectionInput = tryJson(root, COLLECTION_FILE, problems, '.')
|
|
58
|
+
if (collectionInput === undefined) {
|
|
59
|
+
if (packageJson === undefined) problems.push({ location: '.', code: 'package_missing', message: `package.json or ${COLLECTION_FILE} is required` })
|
|
60
|
+
else collect(problems, '.', checkPackageForPublish({ packageJson, files, sourceKind }))
|
|
61
|
+
return problems
|
|
62
|
+
}
|
|
63
|
+
let collection
|
|
64
|
+
try { collection = validateCollection(collectionInput) } catch (error) {
|
|
65
|
+
if (!(error instanceof PluginError)) throw error
|
|
66
|
+
problems.push({ location: '.', code: error.code, message: `${COLLECTION_FILE}: ${error.message}` })
|
|
67
|
+
return problems
|
|
68
|
+
}
|
|
69
|
+
if (packageJson !== undefined) collect(problems, '.', checkPackageForPublish({ packageJson, files, sourceKind, requireManifest: false }))
|
|
70
|
+
else if (sourceKind === 'npm') problems.push({ location: '.', code: 'package_missing', message: 'an npm package needs a root package.json' })
|
|
71
|
+
for (const entry of collection.plugins) {
|
|
72
|
+
const entryPackage = tryJson(join(root, entry.path), 'package.json', problems, entry.path)
|
|
73
|
+
if (entryPackage === undefined) {
|
|
74
|
+
if (!problems.some(problem => problem.location === entry.path)) {
|
|
75
|
+
problems.push({ location: entry.path, code: 'package_missing', message: `collection entry ${entry.id} has no package.json` })
|
|
76
|
+
}
|
|
77
|
+
continue
|
|
78
|
+
}
|
|
79
|
+
const prefix = `${entry.path}/`
|
|
80
|
+
const entryFiles = files.filter(file => file.startsWith(prefix)).map(file => file.slice(prefix.length))
|
|
81
|
+
collect(problems, entry.path, checkPackageForPublish({ packageJson: entryPackage, files: entryFiles, sourceKind, expectedId: entry.id }))
|
|
82
|
+
}
|
|
83
|
+
return problems
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function report(problems) {
|
|
87
|
+
for (const problem of problems) console.log(`${problem.location}: ${problem.code}: ${problem.message}`)
|
|
88
|
+
console.log(problems.length ? `${problems.length} problem${problems.length === 1 ? '' : 's'}` : 'ok')
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function npm(args, cwd) {
|
|
92
|
+
const result = spawnSync('npm', args, { cwd, stdio: 'inherit', shell: process.platform === 'win32' })
|
|
93
|
+
if (result.error) throw result.error
|
|
94
|
+
return result.status ?? 1
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function parse(argv) {
|
|
98
|
+
const [command, ...rest] = argv
|
|
99
|
+
const options = { directory: '.', sourceKind: 'npm' }
|
|
100
|
+
for (let index = 0; index < rest.length; index++) {
|
|
101
|
+
const argument = rest[index]
|
|
102
|
+
if (argument === '--source') {
|
|
103
|
+
options.sourceKind = rest[++index]
|
|
104
|
+
if (!['npm', 'git'].includes(options.sourceKind)) throw new Error('--source takes npm or git')
|
|
105
|
+
} else if (argument.startsWith('-')) throw new Error(`unknown option ${argument}`)
|
|
106
|
+
else options.directory = argument
|
|
107
|
+
}
|
|
108
|
+
return { command, ...options, directory: resolve(options.directory) }
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function check(options) {
|
|
112
|
+
const problems = checkDirectory(options.directory, options)
|
|
113
|
+
report(problems)
|
|
114
|
+
return problems.length ? 1 : 0
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function pack(options) {
|
|
118
|
+
if (options.sourceKind !== 'npm') throw new Error('pack checks an npm package; --source git does not apply')
|
|
119
|
+
let problems = checkDirectory(options.directory, options)
|
|
120
|
+
const missing = problems.filter(problem => problem.code === 'lockfile_missing')
|
|
121
|
+
for (const problem of missing) {
|
|
122
|
+
const directory = resolve(options.directory, problem.location)
|
|
123
|
+
if (!existsSync(join(directory, 'package-lock.json'))) {
|
|
124
|
+
console.log(`${problem.location}: run npm install first so npm shrinkwrap has a package-lock.json to convert`)
|
|
125
|
+
continue
|
|
126
|
+
}
|
|
127
|
+
console.log(`${problem.location}: creating npm-shrinkwrap.json from package-lock.json`)
|
|
128
|
+
const status = npm(['shrinkwrap'], directory)
|
|
129
|
+
if (status !== 0) return status
|
|
130
|
+
console.log(`${problem.location}: commit npm-shrinkwrap.json; npm publishes it with the package`)
|
|
131
|
+
}
|
|
132
|
+
if (missing.length) problems = checkDirectory(options.directory, options)
|
|
133
|
+
report(problems)
|
|
134
|
+
if (problems.length) return 1
|
|
135
|
+
return npm(['pack', '--dry-run'], options.directory)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function main(argv) {
|
|
139
|
+
let options
|
|
140
|
+
try { options = parse(argv) } catch (error) { console.error(error.message); console.error(USAGE); return 2 }
|
|
141
|
+
try {
|
|
142
|
+
if (options.command === 'check') return check(options)
|
|
143
|
+
if (options.command === 'pack') return pack(options)
|
|
144
|
+
} catch (error) { console.error(`standard-plugin: ${error.message}`); return 2 }
|
|
145
|
+
console.error(USAGE)
|
|
146
|
+
return 2
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
process.exitCode = main(process.argv.slice(2))
|
package/package.json
CHANGED
|
@@ -1,8 +1,21 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@standardagents/code-plugin-sdk",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
3
|
+
"version": "1.0.0-alpha.1",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Standard Code plugin authoring SDK",
|
|
5
6
|
"license": "MIT",
|
|
6
|
-
"
|
|
7
|
-
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "https://github.com/standardagents/code-rs.git",
|
|
10
|
+
"directory": "packages/plugin-sdk"
|
|
11
|
+
},
|
|
12
|
+
"publishConfig": { "access": "public" },
|
|
13
|
+
"engines": { "node": ">=20" },
|
|
14
|
+
"exports": {
|
|
15
|
+
".": { "types": "./src/index.d.ts", "import": "./src/index.mjs" },
|
|
16
|
+
"./testing": { "types": "./src/testing.d.ts", "import": "./src/testing.mjs" }
|
|
17
|
+
},
|
|
18
|
+
"bin": { "standard-plugin": "./bin/standard-plugin.mjs" },
|
|
19
|
+
"files": ["src", "bin", "README.md", "LICENSE"],
|
|
20
|
+
"scripts": { "test": "node --test test/*.test.mjs" }
|
|
8
21
|
}
|
|
Binary file
|
package/src/index.d.ts
ADDED
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
export type Json = null | boolean | number | string | Json[] | { [key: string]: Json };
|
|
2
|
+
export type Capability = 'surfaces' | 'events' | 'hooks' | 'panes' | 'projects' |
|
|
3
|
+
'notifications' | 'url' | 'fetch' | 'secrets' | 'webhook';
|
|
4
|
+
export type SurfaceKind = 'section' | 'slot' | 'badge' | 'panel' | 'overlay' |
|
|
5
|
+
'menu' | 'command' | 'key' | 'link';
|
|
6
|
+
export type MenuPosition = 'top' | 'after-open' | 'before-danger' | 'bottom';
|
|
7
|
+
export type Anchor = 'plugins' | 'machine.before' | 'machine.after' |
|
|
8
|
+
'project.before' | 'project.after' | 'pane.header' | 'pane.footer' |
|
|
9
|
+
'account' | 'machine' | 'project' | 'pane' | 'section';
|
|
10
|
+
export interface ContributionDeclaration {
|
|
11
|
+
id: string;
|
|
12
|
+
kind: SurfaceKind;
|
|
13
|
+
anchor: Anchor;
|
|
14
|
+
title?: string;
|
|
15
|
+
merge?: 'by-machine' | 'by-identity';
|
|
16
|
+
width?: 'full' | 'half';
|
|
17
|
+
position?: MenuPosition;
|
|
18
|
+
group?: string;
|
|
19
|
+
chord?: string;
|
|
20
|
+
pattern?: string;
|
|
21
|
+
actionId?: string;
|
|
22
|
+
}
|
|
23
|
+
/** The package.json standardPlugin field is read before any plugin code runs. */
|
|
24
|
+
export interface PluginManifest {
|
|
25
|
+
apiVersion: 1;
|
|
26
|
+
id: string;
|
|
27
|
+
name: string;
|
|
28
|
+
version: string;
|
|
29
|
+
entry: string;
|
|
30
|
+
singleton: boolean;
|
|
31
|
+
order: number;
|
|
32
|
+
capabilities: Capability[];
|
|
33
|
+
contributions: ContributionDeclaration[];
|
|
34
|
+
configSchema?: { [key: string]: Json };
|
|
35
|
+
hookTimeoutMs: number;
|
|
36
|
+
}
|
|
37
|
+
export type ManifestInput = Pick<PluginManifest, 'apiVersion' | 'id' | 'name' | 'version' | 'entry'> &
|
|
38
|
+
Partial<Omit<PluginManifest, 'apiVersion' | 'id' | 'name' | 'version' | 'entry'>>;
|
|
39
|
+
export interface EntityRef {
|
|
40
|
+
kind: 'account' | 'machine' | 'project' | 'pane' | 'section';
|
|
41
|
+
id: string;
|
|
42
|
+
machineId?: string;
|
|
43
|
+
/** Decimal strings preserve 64-bit unsigned values across the JSON boundary. */
|
|
44
|
+
generation?: string;
|
|
45
|
+
}
|
|
46
|
+
export interface Producer {
|
|
47
|
+
pluginId: string;
|
|
48
|
+
machineId: string;
|
|
49
|
+
epoch: string;
|
|
50
|
+
}
|
|
51
|
+
export interface ContributionKey {
|
|
52
|
+
contributionId: string;
|
|
53
|
+
anchor: Anchor;
|
|
54
|
+
entity?: EntityRef;
|
|
55
|
+
}
|
|
56
|
+
export interface TextSpan {
|
|
57
|
+
text: string;
|
|
58
|
+
foreground?: string;
|
|
59
|
+
background?: string;
|
|
60
|
+
bold?: boolean;
|
|
61
|
+
italic?: boolean;
|
|
62
|
+
underline?: boolean;
|
|
63
|
+
actionId?: string;
|
|
64
|
+
}
|
|
65
|
+
export interface NativeRow {
|
|
66
|
+
id: string;
|
|
67
|
+
identity?: string;
|
|
68
|
+
providerRevision?: string;
|
|
69
|
+
spans: TextSpan[];
|
|
70
|
+
actionId?: string;
|
|
71
|
+
meter?: { value: number; max: number; label?: string };
|
|
72
|
+
spark?: number[];
|
|
73
|
+
divider?: boolean;
|
|
74
|
+
}
|
|
75
|
+
export type NativeContent = { kind: 'rows'; rows: NativeRow[] } |
|
|
76
|
+
{ kind: 'text'; lines: TextSpan[][] } |
|
|
77
|
+
{ kind: 'badge'; spans: TextSpan[]; actionId?: string };
|
|
78
|
+
export interface CanvasSpec {
|
|
79
|
+
columns: number;
|
|
80
|
+
rows: number;
|
|
81
|
+
transparent?: boolean;
|
|
82
|
+
shade?: number;
|
|
83
|
+
captureInput?: boolean;
|
|
84
|
+
}
|
|
85
|
+
export type SurfaceContent = NativeContent | { kind: 'canvas'; canvas: CanvasSpec };
|
|
86
|
+
export interface RequestOptions { signal?: AbortSignal; timeoutMs?: number }
|
|
87
|
+
export interface Disposable { dispose(): void }
|
|
88
|
+
export type Cleanup = () => void | Promise<void>;
|
|
89
|
+
export type Condition = { kind: 'always' } |
|
|
90
|
+
{ kind: 'section-visible' | 'slot-visible' | 'panel-open'; contributionId: string; entity?: EntityRef };
|
|
91
|
+
export interface LaunchSpec {
|
|
92
|
+
version: 1;
|
|
93
|
+
argv: string[];
|
|
94
|
+
cwd: string;
|
|
95
|
+
env: Record<string, string>;
|
|
96
|
+
agent?: { id: string; prompt?: string; resumeId?: string; delivery: 'argv' | 'stdin' };
|
|
97
|
+
bootstrap?: { argv: string[]; on: 'create' };
|
|
98
|
+
}
|
|
99
|
+
export interface PaneCreate {
|
|
100
|
+
machineId: string;
|
|
101
|
+
projectId?: string;
|
|
102
|
+
launch: LaunchSpec;
|
|
103
|
+
title?: string;
|
|
104
|
+
presentation?: 'workspace' | 'popup';
|
|
105
|
+
restart?: 'never' | 'on-failure';
|
|
106
|
+
contributionId?: string;
|
|
107
|
+
}
|
|
108
|
+
export interface PaneResult { pane: EntityRef; operationId: string }
|
|
109
|
+
export interface Selection { entity?: EntityRef; actionId: string; value?: Json }
|
|
110
|
+
export interface LinkSelection extends Selection { url: string }
|
|
111
|
+
export type ActionHandler = (event: Selection, context: HandlerContext) => Json | void | Promise<Json | void>;
|
|
112
|
+
/** True consumes the URL; false allows the next matching handler to run. */
|
|
113
|
+
export type LinkHandler = (event: LinkSelection, context: HandlerContext) => boolean | Promise<boolean>;
|
|
114
|
+
export interface RegistrationOptions {
|
|
115
|
+
entity?: EntityRef;
|
|
116
|
+
actionId?: string;
|
|
117
|
+
title?: string;
|
|
118
|
+
condition?: Condition;
|
|
119
|
+
}
|
|
120
|
+
export interface MenuOptions extends RegistrationOptions { position?: MenuPosition }
|
|
121
|
+
export interface CommandOptions extends RegistrationOptions { group?: string }
|
|
122
|
+
/** Chords use the native serialized form, such as ctrl+k or ctrl+b w. */
|
|
123
|
+
export interface KeyOptions extends RegistrationOptions { chord?: string }
|
|
124
|
+
/** Patterns are bounded URL match expressions interpreted by the host. */
|
|
125
|
+
export interface LinkOptions extends RegistrationOptions { pattern?: string }
|
|
126
|
+
export interface GroupCommand extends RegistrationOptions { id: string; handler: ActionHandler }
|
|
127
|
+
/** Registration metadata travels with an action subscription. */
|
|
128
|
+
export type ActionRegistration = ContributionKey & { actionId: string; title: string } & (
|
|
129
|
+
{ kind: 'menu'; position: MenuPosition } |
|
|
130
|
+
{ kind: 'command'; group?: string } |
|
|
131
|
+
{ kind: 'key'; chord: string } |
|
|
132
|
+
{ kind: 'link'; pattern: string }
|
|
133
|
+
);
|
|
134
|
+
export interface HookEvent {
|
|
135
|
+
name: string;
|
|
136
|
+
entity?: EntityRef;
|
|
137
|
+
launch?: LaunchSpec;
|
|
138
|
+
ancestry: string[];
|
|
139
|
+
operationId: string;
|
|
140
|
+
deadlineMs: number;
|
|
141
|
+
}
|
|
142
|
+
export type HookResult = { decision: 'proceed'; launch?: LaunchSpec } |
|
|
143
|
+
{ decision: 'cancel'; reason?: string };
|
|
144
|
+
export interface PluginEvent {
|
|
145
|
+
name: string;
|
|
146
|
+
entity?: EntityRef;
|
|
147
|
+
data: Json;
|
|
148
|
+
deliveryId?: string;
|
|
149
|
+
}
|
|
150
|
+
export interface Popover {
|
|
151
|
+
kind: 'chooser' | 'form' | 'confirm';
|
|
152
|
+
title: string;
|
|
153
|
+
message?: string;
|
|
154
|
+
entity?: EntityRef;
|
|
155
|
+
choices?: { id: string; label: string; destructive?: boolean }[];
|
|
156
|
+
fields?: { id: string; label: string; type: 'text' | 'secret' | 'boolean'; value?: Json }[];
|
|
157
|
+
}
|
|
158
|
+
export interface OperationMap {
|
|
159
|
+
'pane.create': { input: PaneCreate; output: PaneResult };
|
|
160
|
+
'pane.close': { input: { pane: EntityRef; confirmationId?: string }; output: { operationId: string } };
|
|
161
|
+
'pane.restart': { input: { pane: EntityRef; launch?: LaunchSpec }; output: PaneResult };
|
|
162
|
+
'pane.input': { input: { pane: EntityRef; text: string }; output: null };
|
|
163
|
+
'pane.focus': { input: { pane: EntityRef }; output: null };
|
|
164
|
+
'pane.wait': { input: { pane: EntityRef }; output: { exitCode: number | null; lastLine: string } };
|
|
165
|
+
'project.create': { input: { machineId: string; path: string; name?: string }; output: EntityRef };
|
|
166
|
+
'project.remove': { input: { project: EntityRef; confirmationId?: string }; output: { operationId: string } };
|
|
167
|
+
'notification.show': { input: { title: string; message: string }; output: null };
|
|
168
|
+
'url.open': { input: { url: string }; output: null };
|
|
169
|
+
'fetch': { input: { url: string; method?: string; headers?: Record<string, string>; body?: string }; output: { status: number; headers: Record<string, string>; body: string } };
|
|
170
|
+
'secret.get': { input: { name: string }; output: string | null };
|
|
171
|
+
'config.get': { input: Record<string, never>; output: Record<string, Json> };
|
|
172
|
+
'state.get': { input: { key: string }; output: Json };
|
|
173
|
+
'state.set': { input: { key: string; value: Json }; output: null };
|
|
174
|
+
'context.get': { input: Record<string, never>; output: Json };
|
|
175
|
+
'popover.open': { input: Popover; output: { choiceId?: string; values?: Record<string, Json>; confirmationId?: string } | null };
|
|
176
|
+
'canvas.write': { input: { key: ContributionKey; ansi: string }; output: null };
|
|
177
|
+
'canvas.focus': { input: { key: ContributionKey; capture: boolean }; output: null };
|
|
178
|
+
'subscription.add': { input: { id: string; name: string; condition: Condition } & (
|
|
179
|
+
{ kind: 'action'; registration?: ActionRegistration } |
|
|
180
|
+
{ kind: 'event' | 'hook' | 'input' | 'select' | 'resize' | 'activate' | 'deactivate' | 'visibility' }
|
|
181
|
+
); output: null };
|
|
182
|
+
'subscription.remove': { input: { id: string }; output: null };
|
|
183
|
+
'health.set': { input: { status: 'healthy' | 'degraded' | 'error'; message?: string }; output: null };
|
|
184
|
+
'webhook.ack': { input: { deliveryId: string }; output: null };
|
|
185
|
+
}
|
|
186
|
+
export type OperationName = keyof OperationMap;
|
|
187
|
+
export type Operation = { [K in OperationName]: { op: K; args: OperationMap[K]['input'] } }[OperationName];
|
|
188
|
+
export type Request = <K extends OperationName>(op: K, args: OperationMap[K]['input'], options?: RequestOptions) => Promise<OperationMap[K]['output']>;
|
|
189
|
+
export interface Publisher extends Disposable {
|
|
190
|
+
replace(content: SurfaceContent): void;
|
|
191
|
+
clear(): void;
|
|
192
|
+
}
|
|
193
|
+
export interface Canvas extends Publisher {
|
|
194
|
+
write(ansi: string, options?: RequestOptions): Promise<null>;
|
|
195
|
+
focus(capture: boolean, options?: RequestOptions): Promise<null>;
|
|
196
|
+
}
|
|
197
|
+
export interface Subscription extends Disposable { ready: Promise<null> }
|
|
198
|
+
export interface KeyRegistration extends Subscription {
|
|
199
|
+
/** Replaces this subscription's chord after its initial registration is ready. */
|
|
200
|
+
/** One update may be pending per registration. */
|
|
201
|
+
update(chord: string, options?: RequestOptions): Promise<null>;
|
|
202
|
+
}
|
|
203
|
+
export interface HandlerContext { signal: AbortSignal }
|
|
204
|
+
export interface PluginContext {
|
|
205
|
+
readonly manifest: Readonly<PluginManifest>;
|
|
206
|
+
readonly producer: Readonly<Producer>;
|
|
207
|
+
readonly signal: AbortSignal;
|
|
208
|
+
request: Request;
|
|
209
|
+
section(id: string, entity?: EntityRef): Publisher;
|
|
210
|
+
slot(id: string, entity: EntityRef): Publisher;
|
|
211
|
+
badge(id: string, entity: EntityRef): Publisher;
|
|
212
|
+
panel(id: string, entity?: EntityRef): Publisher;
|
|
213
|
+
overlay(id: string, entity?: EntityRef): Publisher;
|
|
214
|
+
canvas(id: string, spec: CanvasSpec, entity?: EntityRef): Canvas;
|
|
215
|
+
/** IDs match static manifest contributions. Options override declaration defaults. */
|
|
216
|
+
menu(id: string, options: MenuOptions, handler: ActionHandler): Subscription;
|
|
217
|
+
menu(id: string, handler: ActionHandler): Subscription;
|
|
218
|
+
/** Registers a palette command in its declared context and optional group. */
|
|
219
|
+
command(id: string, options: CommandOptions, handler: ActionHandler): Subscription;
|
|
220
|
+
command(id: string, handler: ActionHandler): Subscription;
|
|
221
|
+
/** Each command has a static contribution ID. Disposal removes the whole group. */
|
|
222
|
+
commandGroup(group: string, commands: readonly GroupCommand[]): Subscription;
|
|
223
|
+
key(id: string, options: KeyOptions, handler: ActionHandler): KeyRegistration;
|
|
224
|
+
key(id: string, handler: ActionHandler): KeyRegistration;
|
|
225
|
+
link(id: string, options: LinkOptions, handler: LinkHandler): Subscription;
|
|
226
|
+
link(id: string, handler: LinkHandler): Subscription;
|
|
227
|
+
onEvent(name: string, handler: (event: PluginEvent, context: HandlerContext) => void | Promise<void>, condition?: Condition): Subscription;
|
|
228
|
+
onHook(name: string, handler: (event: HookEvent, context: HandlerContext) => HookResult | Promise<HookResult>): Subscription;
|
|
229
|
+
onAction(name: string, handler: (event: Selection, context: HandlerContext) => Json | void | Promise<Json | void>): Subscription;
|
|
230
|
+
onInput(name: string, handler: (event: Json, context: HandlerContext) => void | Promise<void>): Subscription;
|
|
231
|
+
onSelect(name: string, handler: (event: Selection, context: HandlerContext) => void | Promise<void>): Subscription;
|
|
232
|
+
onResize(name: string, handler: (event: { columns: number; rows: number }, context: HandlerContext) => void | Promise<void>): Subscription;
|
|
233
|
+
onActivate(name: string, handler: (event: Json, context: HandlerContext) => void | Promise<void>): Subscription;
|
|
234
|
+
onDeactivate(name: string, handler: (event: Json, context: HandlerContext) => void | Promise<void>): Subscription;
|
|
235
|
+
schedule(intervalMs: number, handler: (context: HandlerContext) => void | Promise<void>, options?: { condition?: Condition; immediate?: boolean }): Disposable;
|
|
236
|
+
onDispose(cleanup: Cleanup): Disposable;
|
|
237
|
+
panes: {
|
|
238
|
+
create(args: PaneCreate, options?: RequestOptions): Promise<PaneResult>;
|
|
239
|
+
close(args: OperationMap['pane.close']['input'], options?: RequestOptions): Promise<{ operationId: string }>;
|
|
240
|
+
restart(args: OperationMap['pane.restart']['input'], options?: RequestOptions): Promise<PaneResult>;
|
|
241
|
+
input(args: OperationMap['pane.input']['input'], options?: RequestOptions): Promise<null>;
|
|
242
|
+
focus(args: OperationMap['pane.focus']['input'], options?: RequestOptions): Promise<null>;
|
|
243
|
+
wait(args: OperationMap['pane.wait']['input'], options?: RequestOptions): Promise<OperationMap['pane.wait']['output']>;
|
|
244
|
+
};
|
|
245
|
+
projects: { create(args: OperationMap['project.create']['input'], options?: RequestOptions): Promise<EntityRef>; remove(args: OperationMap['project.remove']['input'], options?: RequestOptions): Promise<{ operationId: string }> };
|
|
246
|
+
notifications: { show(args: OperationMap['notification.show']['input'], options?: RequestOptions): Promise<null> };
|
|
247
|
+
url: { open(url: string, options?: RequestOptions): Promise<null> };
|
|
248
|
+
fetch(args: OperationMap['fetch']['input'], options?: RequestOptions): Promise<OperationMap['fetch']['output']>;
|
|
249
|
+
secrets: { get(name: string, options?: RequestOptions): Promise<string | null> };
|
|
250
|
+
config: { get(options?: RequestOptions): Promise<Record<string, Json>> };
|
|
251
|
+
/** Local state for one machine. It is never shared with other machines. */
|
|
252
|
+
state: { get(key: string, options?: RequestOptions): Promise<Json>; set(key: string, value: Json, options?: RequestOptions): Promise<null> };
|
|
253
|
+
context: { get(options?: RequestOptions): Promise<Json> };
|
|
254
|
+
popover: { open(args: Popover, options?: RequestOptions): Promise<OperationMap['popover.open']['output']> };
|
|
255
|
+
health: { set(args: OperationMap['health.set']['input'], options?: RequestOptions): Promise<null> };
|
|
256
|
+
webhook: { ack(deliveryId: string, options?: RequestOptions): Promise<null> };
|
|
257
|
+
}
|
|
258
|
+
export interface PluginDefinition {
|
|
259
|
+
id: string;
|
|
260
|
+
activate(context: PluginContext): void | Cleanup | Promise<void | Cleanup>;
|
|
261
|
+
}
|
|
262
|
+
export function definePlugin(definition: PluginDefinition): Readonly<PluginDefinition>;
|
|
263
|
+
export function validateManifest(value: unknown): Readonly<PluginManifest>;
|
|
264
|
+
export class PluginError extends Error { code: string; constructor(code: string, message: string) }
|
|
265
|
+
|
|
266
|
+
/** One plugin inside a collection. The path is relative to the source root. */
|
|
267
|
+
export interface PluginCollectionEntry {
|
|
268
|
+
id: string;
|
|
269
|
+
/** Relative POSIX path; "" names the source root for a single-plugin source. */
|
|
270
|
+
path: string;
|
|
271
|
+
}
|
|
272
|
+
/** The standard-plugins.json file at the root of a repository or npm package. */
|
|
273
|
+
export interface PluginCollection {
|
|
274
|
+
schema: 1;
|
|
275
|
+
plugins: PluginCollectionEntry[];
|
|
276
|
+
}
|
|
277
|
+
/** File name of the collection manifest: standard-plugins.json. */
|
|
278
|
+
export const COLLECTION_FILE: 'standard-plugins.json';
|
|
279
|
+
export function validateCollection(value: unknown): Readonly<PluginCollection>;
|
|
280
|
+
/** A collection file wins; without one, a valid package manifest makes the root one plugin at path "". */
|
|
281
|
+
export function resolveCollection(source: { collection?: unknown; packageJson?: unknown }): Readonly<PluginCollection>;
|
|
282
|
+
|
|
283
|
+
export type SourceKind = 'git' | 'npm';
|
|
284
|
+
export type LockfileName = 'package-lock.json' | 'npm-shrinkwrap.json';
|
|
285
|
+
export interface LockfileRequirement {
|
|
286
|
+
/** True when the package declares dependencies or optionalDependencies. */
|
|
287
|
+
required: boolean;
|
|
288
|
+
dependencies: readonly string[];
|
|
289
|
+
/** Lockfile names that satisfy the rule for this source kind. */
|
|
290
|
+
lockfiles: readonly LockfileName[];
|
|
291
|
+
}
|
|
292
|
+
/** Package name of this SDK, which a plugin lists under peerDependencies. */
|
|
293
|
+
export const SDK_PACKAGE: '@standardagents/code-plugin-sdk';
|
|
294
|
+
export function lockfileRequirement(input: { packageJson: unknown; sourceKind: SourceKind }): LockfileRequirement;
|
|
295
|
+
export interface PublishProblem {
|
|
296
|
+
code: 'invalid_package' | 'package_private' | 'lockfile_missing' | 'sdk_dependency' | 'sdk_peer_missing' |
|
|
297
|
+
'manifest_missing' | 'invalid_manifest' | 'invalid_payload' | 'payload_too_large' | 'incompatible_version' |
|
|
298
|
+
'entry_missing' | 'id_mismatch';
|
|
299
|
+
message: string;
|
|
300
|
+
}
|
|
301
|
+
/** Pure checks over a package.json and the relative paths of the files that ship with it. */
|
|
302
|
+
export function checkPackageForPublish(input: {
|
|
303
|
+
packageJson: unknown;
|
|
304
|
+
files: readonly string[];
|
|
305
|
+
sourceKind?: SourceKind;
|
|
306
|
+
/** False for a collection root whose package.json carries no plugin manifest. */
|
|
307
|
+
requireManifest?: boolean;
|
|
308
|
+
/** The collection entry id the manifest must match. */
|
|
309
|
+
expectedId?: string;
|
|
310
|
+
}): readonly PublishProblem[];
|