@dsh-workbench/adapter-supply 0.0.0-stage → 1.0.0-beta.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.
@@ -0,0 +1,21 @@
1
+ export type PackageKind = 'opencli-plugin' | 'dsh-bundle' | 'both' | 'neither';
2
+ export interface PackageVerdict {
3
+ readonly kind: PackageKind;
4
+ readonly pkg?: string;
5
+ readonly pluginName?: string;
6
+ }
7
+ export interface NpmRef {
8
+ readonly name: string;
9
+ readonly version?: string;
10
+ }
11
+ export declare const OPENCLI_MANIFEST_FILE = "opencli-plugin.json";
12
+ export declare function splitNpmSpec(spec: string): NpmRef | undefined;
13
+ export declare function isCommandFile(name: string): boolean;
14
+ export declare function pluginManifestOf(dir: string): Readonly<Record<string, unknown>> | undefined;
15
+ export declare function hasCommandFile(dir: string): boolean;
16
+ export declare function pluginAt(dir: string): {
17
+ readonly name?: string;
18
+ } | undefined;
19
+ export declare function dshMarkersOf(raw: unknown): boolean;
20
+ export declare function verdictOf(dir: string): PackageVerdict | undefined;
21
+ export declare function refusalReason(): string;
@@ -0,0 +1,73 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { isPackageName } from './installed-module.js';
4
+ import { dshRecordOf, packageManifestOf, packageNameOf } from './registry-declaration.js';
5
+ export const OPENCLI_MANIFEST_FILE = 'opencli-plugin.json';
6
+ const VERSION = /^[a-zA-Z0-9][a-zA-Z0-9._+-]*$/;
7
+ export function splitNpmSpec(spec) {
8
+ const value = spec.trim();
9
+ const at = value.lastIndexOf('@');
10
+ const name = at > 0 ? value.slice(0, at) : value;
11
+ const version = at > 0 ? value.slice(at + 1) : '';
12
+ if (!isPackageName(name))
13
+ return undefined;
14
+ if (version !== '' && !VERSION.test(version))
15
+ return undefined;
16
+ return { name, ...(version === '' ? {} : { version }) };
17
+ }
18
+ function asRecord(value) {
19
+ return typeof value === 'object' && value !== null ? value : undefined;
20
+ }
21
+ export function isCommandFile(name) {
22
+ if (name.endsWith('.d.ts') || name.endsWith('.test.ts'))
23
+ return false;
24
+ if (name.endsWith('.d.js'))
25
+ return false;
26
+ return name.endsWith('.ts') || name.endsWith('.js');
27
+ }
28
+ export function pluginManifestOf(dir) {
29
+ try {
30
+ const parsed = JSON.parse(fs.readFileSync(path.join(dir, OPENCLI_MANIFEST_FILE), 'utf8'));
31
+ if (Array.isArray(parsed))
32
+ return undefined;
33
+ return asRecord(parsed);
34
+ }
35
+ catch {
36
+ return undefined;
37
+ }
38
+ }
39
+ export function hasCommandFile(dir) {
40
+ try {
41
+ return fs.readdirSync(dir).some(isCommandFile);
42
+ }
43
+ catch {
44
+ return false;
45
+ }
46
+ }
47
+ export function pluginAt(dir) {
48
+ const manifest = pluginManifestOf(dir);
49
+ if (manifest === undefined || !hasCommandFile(dir))
50
+ return undefined;
51
+ const name = manifest.name;
52
+ return typeof name === 'string' && name.trim() !== '' ? { name: name.trim() } : {};
53
+ }
54
+ export function dshMarkersOf(raw) {
55
+ const record = asRecord(raw);
56
+ return record !== undefined && ('bundle' in record || 'workbench' in record);
57
+ }
58
+ export function verdictOf(dir) {
59
+ const plugin = pluginAt(dir);
60
+ if (packageManifestOf(dir) === undefined && plugin === undefined)
61
+ return undefined;
62
+ const dsh = dshMarkersOf(dshRecordOf(dir));
63
+ const kind = plugin !== undefined && dsh ? 'both' : plugin !== undefined ? 'opencli-plugin' : dsh ? 'dsh-bundle' : 'neither';
64
+ const pkg = packageNameOf(dir);
65
+ return {
66
+ kind,
67
+ ...(pkg === undefined ? {} : { pkg }),
68
+ ...(plugin?.name === undefined ? {} : { pluginName: plugin.name }),
69
+ };
70
+ }
71
+ export function refusalReason() {
72
+ return '这个包既不是工作台的模块,也不是 opencli 的插件,装了也用不上';
73
+ }
@@ -0,0 +1,23 @@
1
+ import type { DshShell } from '@dsh-workbench/shared';
2
+ import { type ResolvedPluginSource } from './plugin-source.js';
3
+ export type PackageRouteKind = 'opencli-plugin' | 'dsh-bundle' | 'neither';
4
+ export interface PackageRoute {
5
+ readonly kind: PackageRouteKind;
6
+ readonly pkg?: string;
7
+ readonly pluginName?: string;
8
+ }
9
+ export declare const LOCAL_PLUGIN_ENV = "DSH_WB_OPENCLI_PLUGIN_PATH";
10
+ export declare const PLUGIN_REGISTRY_ENV = "DSH_WB_OPENCLI_PLUGIN_REGISTRY";
11
+ export declare function packageRouteOf(options: {
12
+ readonly spec: string;
13
+ readonly shell?: DshShell | undefined;
14
+ readonly registry?: string | undefined;
15
+ readonly force?: boolean;
16
+ }): Promise<PackageRoute | undefined>;
17
+ export declare function resetPackageRouteCache(): void;
18
+ export declare function pluginInstallPlan(options: {
19
+ readonly spec: string;
20
+ readonly command: string;
21
+ readonly registry?: string | undefined;
22
+ readonly pluginName?: string | undefined;
23
+ }): ResolvedPluginSource;
@@ -0,0 +1,74 @@
1
+ import { VERSION_CHECK_TTL_MS } from '@dsh-workbench/shared/host';
2
+ import { resolvePluginSource } from './plugin-source.js';
3
+ import { dshMarkersOf, splitNpmSpec, verdictOf } from './package-kind.js';
4
+ import { withPackageDir } from './package-content.js';
5
+ import { localDirOf, registryDshOf } from './registry-declaration.js';
6
+ export const LOCAL_PLUGIN_ENV = 'DSH_WB_OPENCLI_PLUGIN_PATH';
7
+ export const PLUGIN_REGISTRY_ENV = 'DSH_WB_OPENCLI_PLUGIN_REGISTRY';
8
+ const cache = new Map();
9
+ function routeOfVerdict(verdict, fallback) {
10
+ const pkg = verdict.pkg ?? fallback;
11
+ if (verdict.kind === 'neither')
12
+ return { kind: 'neither', ...(pkg === undefined ? {} : { pkg }) };
13
+ if (verdict.kind === 'dsh-bundle' || verdict.kind === 'both')
14
+ return { kind: 'dsh-bundle' };
15
+ return {
16
+ kind: 'opencli-plugin',
17
+ ...(pkg === undefined ? {} : { pkg }),
18
+ ...(verdict.pluginName === undefined ? {} : { pluginName: verdict.pluginName }),
19
+ };
20
+ }
21
+ export async function packageRouteOf(options) {
22
+ const local = localDirOf(options.spec);
23
+ if (local !== undefined) {
24
+ const verdict = verdictOf(local);
25
+ return verdict === undefined ? undefined : routeOfVerdict(verdict, splitNpmSpec(options.spec)?.name);
26
+ }
27
+ const ref = splitNpmSpec(options.spec);
28
+ if (ref === undefined)
29
+ return undefined;
30
+ const shell = options.shell;
31
+ const registry = options.registry;
32
+ if (shell === undefined || registry === undefined)
33
+ return undefined;
34
+ const key = `${registry}|${options.spec}`;
35
+ const hit = cache.get(key);
36
+ if (options.force !== true && hit !== undefined && Date.now() - hit.at < VERSION_CHECK_TTL_MS) {
37
+ return hit.route ?? undefined;
38
+ }
39
+ const found = await registryDshOf({ shell, name: options.spec, registry });
40
+ let route;
41
+ if (found.readable && dshMarkersOf(found.raw)) {
42
+ route = { kind: 'dsh-bundle' };
43
+ }
44
+ else {
45
+ const verdict = await withPackageDir({ shell, spec: options.spec, registry }, (dir) => verdictOf(dir));
46
+ route = verdict === undefined ? undefined : routeOfVerdict(verdict, ref.name);
47
+ }
48
+ cache.set(key, { at: Date.now(), route: route ?? null });
49
+ return route;
50
+ }
51
+ export function resetPackageRouteCache() {
52
+ cache.clear();
53
+ }
54
+ export function pluginInstallPlan(options) {
55
+ const local = localDirOf(options.spec);
56
+ if (local !== undefined) {
57
+ return resolvePluginSource({ kind: 'local', env: LOCAL_PLUGIN_ENV }, { [LOCAL_PLUGIN_ENV]: local }, { command: options.command });
58
+ }
59
+ const ref = splitNpmSpec(options.spec);
60
+ if (ref === undefined)
61
+ return { ok: false, problem: 'malformed' };
62
+ const source = {
63
+ kind: 'npm',
64
+ package: ref.name,
65
+ registryEnv: PLUGIN_REGISTRY_ENV,
66
+ ...(options.registry === undefined ? {} : { registryDefault: options.registry }),
67
+ };
68
+ return resolvePluginSource(source, { [PLUGIN_REGISTRY_ENV]: options.registry }, {
69
+ command: options.command,
70
+ ...(options.pluginName === undefined ? {} : { plugin: options.pluginName }),
71
+ ...(ref.version === undefined ? {} : { version: ref.version }),
72
+ mode: 'install',
73
+ });
74
+ }
@@ -0,0 +1,65 @@
1
+ import type { DshShell } from '@dsh-workbench/shared';
2
+ export interface PluginCommandArg {
3
+ readonly name: string;
4
+ readonly type: string;
5
+ readonly required: boolean;
6
+ readonly positional: boolean;
7
+ readonly valueRequired: boolean;
8
+ readonly choices: readonly string[];
9
+ readonly defaultValue?: unknown;
10
+ readonly help: string;
11
+ }
12
+ export interface PluginCommand {
13
+ readonly plugin: string;
14
+ readonly name: string;
15
+ readonly description: string;
16
+ readonly access: string;
17
+ readonly browser: boolean;
18
+ readonly siteSession: string;
19
+ readonly domain?: string;
20
+ readonly args: readonly PluginCommandArg[];
21
+ }
22
+ export interface PluginCommandSet {
23
+ readonly plugin: string;
24
+ readonly version: string;
25
+ readonly commands: readonly PluginCommand[];
26
+ }
27
+ export interface PluginRow {
28
+ readonly name: string;
29
+ readonly version: string;
30
+ readonly commands: readonly string[];
31
+ }
32
+ export interface CommandRow {
33
+ readonly site: string;
34
+ readonly name: string;
35
+ readonly description: string;
36
+ readonly access: string;
37
+ readonly browser: boolean;
38
+ readonly siteSession: string;
39
+ readonly domain?: string;
40
+ readonly args: readonly PluginCommandArg[];
41
+ }
42
+ export interface PluginCatalogSnapshot {
43
+ readonly ok: boolean;
44
+ readonly at: number;
45
+ readonly sets: readonly PluginCommandSet[];
46
+ readonly problems: readonly string[];
47
+ }
48
+ export interface PluginCatalog {
49
+ refresh(options?: {
50
+ readonly force?: boolean;
51
+ }): Promise<PluginCatalogSnapshot>;
52
+ snapshot(): PluginCatalogSnapshot;
53
+ }
54
+ export interface PluginCatalogOptions {
55
+ readonly shell?: DshShell | undefined;
56
+ readonly command: string;
57
+ readonly timeoutMs?: number;
58
+ }
59
+ export declare function parsePluginRows(stdout: string): PluginRow[] | undefined;
60
+ export declare function parseCommandRows(stdout: string): CommandRow[] | undefined;
61
+ export declare function attributeCommands(plugins: readonly PluginRow[], rows: readonly CommandRow[]): {
62
+ readonly sets: PluginCommandSet[];
63
+ readonly problems: string[];
64
+ };
65
+ export declare function createPluginCatalog(options: PluginCatalogOptions): PluginCatalog;
@@ -0,0 +1,192 @@
1
+ import { OPENCLI_TIMEOUT_MS, parseJsonLoose, runShell } from '@dsh-workbench/shared/host';
2
+ function recordOf(value) {
3
+ return typeof value === 'object' && value !== null ? value : undefined;
4
+ }
5
+ function textOf(value) {
6
+ return typeof value === 'string' ? value : '';
7
+ }
8
+ function stringsOf(value) {
9
+ return Array.isArray(value) ? value.filter((item) => typeof item === 'string') : [];
10
+ }
11
+ function argsOf(raw) {
12
+ if (!Array.isArray(raw))
13
+ return [];
14
+ return raw.flatMap((item) => {
15
+ const record = recordOf(item);
16
+ if (record === undefined)
17
+ return [];
18
+ const name = textOf(record['name']).trim();
19
+ if (name === '')
20
+ return [];
21
+ const fallback = record['default'];
22
+ return [
23
+ {
24
+ name,
25
+ type: textOf(record['type']) || 'string',
26
+ required: record['required'] === true,
27
+ positional: record['positional'] === true,
28
+ valueRequired: record['valueRequired'] === true,
29
+ choices: stringsOf(record['choices']),
30
+ ...(fallback === undefined || fallback === null ? {} : { defaultValue: fallback }),
31
+ help: textOf(record['help']),
32
+ },
33
+ ];
34
+ });
35
+ }
36
+ export function parsePluginRows(stdout) {
37
+ const parsed = parseJsonLoose(stdout);
38
+ if (!Array.isArray(parsed))
39
+ return undefined;
40
+ const rows = [];
41
+ for (const item of parsed) {
42
+ const record = recordOf(item);
43
+ if (record === undefined)
44
+ return undefined;
45
+ const name = textOf(record['name']).trim();
46
+ if (name === '')
47
+ return undefined;
48
+ rows.push({ name, version: textOf(record['version']), commands: stringsOf(record['commands']) });
49
+ }
50
+ return rows;
51
+ }
52
+ export function parseCommandRows(stdout) {
53
+ const parsed = parseJsonLoose(stdout);
54
+ if (!Array.isArray(parsed))
55
+ return undefined;
56
+ const rows = [];
57
+ for (const item of parsed) {
58
+ const record = recordOf(item);
59
+ if (record === undefined)
60
+ return undefined;
61
+ const site = textOf(record['site']).trim();
62
+ const name = textOf(record['name']).trim();
63
+ if (site === '' || name === '')
64
+ return undefined;
65
+ rows.push({
66
+ site,
67
+ name,
68
+ description: textOf(record['description']),
69
+ access: textOf(record['access']),
70
+ browser: record['browser'] === true,
71
+ siteSession: textOf(record['siteSession']),
72
+ ...(textOf(record['domain']) === '' ? {} : { domain: textOf(record['domain']) }),
73
+ args: argsOf(record['args']),
74
+ });
75
+ }
76
+ return rows;
77
+ }
78
+ function commandOf(plugin, row) {
79
+ return {
80
+ plugin,
81
+ name: row.name,
82
+ description: row.description,
83
+ access: row.access,
84
+ browser: row.browser,
85
+ siteSession: row.siteSession,
86
+ ...(row.domain === undefined ? {} : { domain: row.domain }),
87
+ args: row.args,
88
+ };
89
+ }
90
+ export function attributeCommands(plugins, rows) {
91
+ const bySite = new Map();
92
+ for (const row of rows) {
93
+ const site = bySite.get(row.site) ?? new Map();
94
+ site.set(row.name, row);
95
+ bySite.set(row.site, site);
96
+ }
97
+ const sets = [];
98
+ const problems = [];
99
+ if (plugins.length > 0 && rows.length === 0) {
100
+ problems.push(`opencli 报了 ${String(plugins.length)} 个插件,但命令面(opencli list -f json)里一条命令都没有 —— 两份清单对不上`);
101
+ }
102
+ for (const plugin of plugins) {
103
+ const found = bySite.get(plugin.name) ?? new Map();
104
+ if (found.size === 0) {
105
+ problems.push(`「${plugin.name}」在 opencli 插件清单里,但命令面里没有一条 site=${plugin.name} 的命令 —— 归属不了,它这次不合成工具`);
106
+ continue;
107
+ }
108
+ const declared = plugin.commands.length > 0 ? plugin.commands : [...found.keys()];
109
+ const missing = declared.filter((name) => !found.has(name));
110
+ if (missing.length > 0) {
111
+ problems.push(`「${plugin.name}」自己的清单里有 ${String(missing.length)} 条命令在命令面里找不到:${missing.join('、')}(插件版本旧或命令面没更新)`);
112
+ }
113
+ const commands = declared.flatMap((name) => {
114
+ const row = found.get(name);
115
+ return row === undefined ? [] : [commandOf(plugin.name, row)];
116
+ });
117
+ if (commands.length === 0) {
118
+ problems.push(`「${plugin.name}」没有任何一条能归属的命令 —— 它这次不合成工具`);
119
+ continue;
120
+ }
121
+ sets.push({ plugin: plugin.name, version: plugin.version, commands });
122
+ }
123
+ return { sets, problems };
124
+ }
125
+ function fingerprintOf(plugins) {
126
+ return plugins
127
+ .map((plugin) => `${plugin.name}@${plugin.version}:${[...plugin.commands].sort().join(',')}`)
128
+ .sort()
129
+ .join('|');
130
+ }
131
+ function messageOf(error) {
132
+ return error instanceof Error ? error.message : String(error);
133
+ }
134
+ export function createPluginCatalog(options) {
135
+ const timeoutMs = options.timeoutMs ?? OPENCLI_TIMEOUT_MS;
136
+ let current = {
137
+ ok: false,
138
+ at: 0,
139
+ sets: [],
140
+ problems: ['还没探过 opencli 的插件命令面'],
141
+ };
142
+ let heavy;
143
+ async function read(line) {
144
+ const shell = options.shell;
145
+ if (shell === undefined) {
146
+ return { ok: false, problem: '没有执行缝(shell 服务不可用),探不了这个命令面' };
147
+ }
148
+ try {
149
+ const result = await runShell(shell, line, { timeoutMs });
150
+ if (result.timedOut)
151
+ return { ok: false, problem: `${line} 超过 ${String(Math.round(timeoutMs / 1000))} 秒没返回` };
152
+ if (result.exitCode !== 0) {
153
+ const tail = result.stderr.trim() || result.stdout.trim();
154
+ return { ok: false, problem: `${line} 退出码 ${String(result.exitCode)}${tail === '' ? '' : `:${tail.split('\n').slice(-3).join(' ')}`}` };
155
+ }
156
+ return { ok: true, stdout: result.stdout };
157
+ }
158
+ catch (error) {
159
+ return { ok: false, problem: `${line} 没跑起来:${messageOf(error)}` };
160
+ }
161
+ }
162
+ function fail(problem) {
163
+ current = { ok: false, at: Date.now(), sets: current.sets, problems: [problem] };
164
+ return current;
165
+ }
166
+ async function refresh(callOptions = {}) {
167
+ const pluginsRun = await read(`${options.command} plugin list -f json`);
168
+ if (!pluginsRun.ok)
169
+ return fail(pluginsRun.problem);
170
+ const plugins = parsePluginRows(pluginsRun.stdout);
171
+ if (plugins === undefined) {
172
+ return fail('opencli plugin list -f json 的输出读不懂(不是带 name 的 JSON 数组)');
173
+ }
174
+ const fingerprint = fingerprintOf(plugins);
175
+ if (!callOptions.force && heavy !== undefined && heavy.fingerprint === fingerprint) {
176
+ current = { ok: true, at: Date.now(), sets: heavy.sets, problems: heavy.problems };
177
+ return current;
178
+ }
179
+ const commandsRun = await read(`${options.command} list -f json`);
180
+ if (!commandsRun.ok)
181
+ return fail(commandsRun.problem);
182
+ const rows = parseCommandRows(commandsRun.stdout);
183
+ if (rows === undefined) {
184
+ return fail('opencli list -f json 的输出读不懂(不是带 site / name 的 JSON 数组)');
185
+ }
186
+ const attributed = attributeCommands(plugins, rows);
187
+ heavy = { fingerprint, sets: attributed.sets, problems: attributed.problems };
188
+ current = { ok: true, at: Date.now(), sets: attributed.sets, problems: attributed.problems };
189
+ return current;
190
+ }
191
+ return { refresh, snapshot: () => current };
192
+ }
@@ -0,0 +1,2 @@
1
+ export declare function loginUrlOfDomain(domain: string | undefined): string | undefined;
2
+ export declare function pluginLoginHint(error: unknown, domain: string | undefined): string;
@@ -0,0 +1,33 @@
1
+ const NOT_LOGGED_IN = /未登录|请先登录|not logged in/i;
2
+ function propertyOf(error, key) {
3
+ return typeof error === 'object' && error !== null
4
+ ? error[key]
5
+ : undefined;
6
+ }
7
+ function helpUrlOfError(error) {
8
+ const raw = propertyOf(error, 'helpUrl');
9
+ if (typeof raw !== 'string')
10
+ return undefined;
11
+ const trimmed = raw.trim();
12
+ return trimmed === '' ? undefined : trimmed;
13
+ }
14
+ function isAuthFailure(error, message) {
15
+ if (NOT_LOGGED_IN.test(message))
16
+ return true;
17
+ return propertyOf(error, 'code') === 'auth-required';
18
+ }
19
+ export function loginUrlOfDomain(domain) {
20
+ const bare = (domain ?? '').trim().replace(/\/+$/, '');
21
+ if (bare === '' || bare.includes('*'))
22
+ return undefined;
23
+ return /^https?:\/\//.test(bare) ? bare : `https://${bare}/login`;
24
+ }
25
+ export function pluginLoginHint(error, domain) {
26
+ const message = error instanceof Error ? error.message : String(error);
27
+ if (!isAuthFailure(error, message))
28
+ return message;
29
+ const url = helpUrlOfError(error) ?? loginUrlOfDomain(domain);
30
+ if (url !== undefined)
31
+ return `${message}\n在 opencli 那扇浏览器窗里打开 ${url} 登一次,然后重试。`;
32
+ return `${message}\n在 opencli 那扇浏览器窗里登录这个站点(插件没给出具体地址,工作台拼不出来)—— 或跑一次工作台的体检/预热,然后重试。`;
33
+ }
@@ -0,0 +1,2 @@
1
+ export type { ProvisionProblem } from '@dsh-workbench/shared/host';
2
+ export { npmRegistryOf, resolvePluginSource, type PluginSourceCommand, type PluginSourceOptions, type ResolvedPluginSource, } from '@dsh-workbench/shared/host';
@@ -0,0 +1 @@
1
+ export { npmRegistryOf, resolvePluginSource, } from '@dsh-workbench/shared/host';
@@ -0,0 +1,15 @@
1
+ import type { ApprovalDetail, ApprovalPolicy } from '@dsh-workbench/shared/host';
2
+ import type { OpencliSession } from '@dsh-workbench/adapter-runtime';
3
+ import type { PluginCommand } from './plugin-catalog.js';
4
+ export declare const RUN_CODE_TOOL = "run_code";
5
+ export declare const PLUGIN_PROMPT_BUDGET = 4000;
6
+ export declare function pluginToolName(plugin: string, command: string): string;
7
+ export declare function pluginPromptChars(command: PluginCommand): number;
8
+ export declare function pluginToolParameters(command: PluginCommand): Record<string, unknown>;
9
+ export declare function pluginToolDescription(command: PluginCommand): string;
10
+ export declare function pluginToolWrites(command: PluginCommand): boolean;
11
+ export declare function pluginToolSession(command: PluginCommand): OpencliSession;
12
+ export declare function pluginToolArgv(command: PluginCommand, raw: unknown): string[];
13
+ export declare function pluginKeyArgs(command: PluginCommand, raw: unknown): string;
14
+ export declare function pluginApprovalQuestion(command: PluginCommand, raw: unknown): ApprovalDetail;
15
+ export declare function pluginApprovalFor(command: PluginCommand): ApprovalPolicy;
@@ -0,0 +1,134 @@
1
+ import { withApprovalNote } from '@dsh-workbench/shared/host';
2
+ const SCHEMA_TYPE = {
3
+ int: 'integer',
4
+ integer: 'integer',
5
+ number: 'number',
6
+ float: 'number',
7
+ bool: 'boolean',
8
+ boolean: 'boolean',
9
+ string: 'string',
10
+ };
11
+ const KEY_ARG_LIMIT = 3;
12
+ const NO_KEY_ARGS = '没有带参数';
13
+ export const RUN_CODE_TOOL = 'run_code';
14
+ export const PLUGIN_PROMPT_BUDGET = 4_000;
15
+ export function pluginToolName(plugin, command) {
16
+ return `${plugin}_${command}`
17
+ .toLowerCase()
18
+ .replace(/[^a-z0-9_]+/g, '_')
19
+ .replace(/_{2,}/g, '_')
20
+ .replace(/^_+|_+$/g, '');
21
+ }
22
+ function schemaTypeOf(arg) {
23
+ return SCHEMA_TYPE[arg.type.toLowerCase()] ?? 'string';
24
+ }
25
+ function propertyOf(arg) {
26
+ const type = schemaTypeOf(arg);
27
+ const enumValues = type === 'string' && arg.choices.length > 0 ? [...arg.choices] : undefined;
28
+ return {
29
+ type,
30
+ ...(arg.help.trim() === '' ? {} : { description: arg.help.trim() }),
31
+ ...(enumValues === undefined ? {} : { enum: enumValues }),
32
+ ...(arg.defaultValue === undefined ? {} : { default: arg.defaultValue }),
33
+ };
34
+ }
35
+ export function pluginPromptChars(command) {
36
+ return (pluginToolDescription(command).length +
37
+ command.args.reduce((sum, arg) => sum + arg.help.trim().length, 0));
38
+ }
39
+ export function pluginToolParameters(command) {
40
+ const properties = {};
41
+ for (const arg of command.args)
42
+ properties[arg.name] = propertyOf(arg);
43
+ const required = command.args.filter((arg) => arg.required).map((arg) => arg.name);
44
+ const schema = {
45
+ type: 'object',
46
+ properties,
47
+ ...(required.length === 0 ? {} : { required }),
48
+ additionalProperties: false,
49
+ };
50
+ return pluginToolWrites(command) ? withApprovalNote(schema, { required: true }) : schema;
51
+ }
52
+ export function pluginToolDescription(command) {
53
+ const described = command.description.trim();
54
+ return described === '' ? `${command.plugin} ${command.name}(opencli 命令)` : described;
55
+ }
56
+ export function pluginToolWrites(command) {
57
+ return command.access !== 'read';
58
+ }
59
+ export function pluginToolSession(command) {
60
+ if (!command.browser)
61
+ return 'none';
62
+ if (command.siteSession === 'persistent' || command.siteSession === 'ephemeral') {
63
+ return command.siteSession;
64
+ }
65
+ return command.access === 'read' ? 'persistent' : 'ephemeral';
66
+ }
67
+ function valuesOf(raw) {
68
+ return typeof raw === 'object' && raw !== null ? raw : {};
69
+ }
70
+ export function pluginToolArgv(command, raw) {
71
+ const values = valuesOf(raw);
72
+ const argv = [];
73
+ for (const arg of command.args) {
74
+ if (!arg.positional)
75
+ continue;
76
+ const value = values[arg.name];
77
+ if (value === undefined || value === null)
78
+ continue;
79
+ argv.push(String(value));
80
+ }
81
+ for (const arg of command.args) {
82
+ if (arg.positional)
83
+ continue;
84
+ const value = values[arg.name];
85
+ if (value === undefined || value === null)
86
+ continue;
87
+ if (typeof value === 'boolean')
88
+ argv.push(`--${arg.name}=${value ? 'true' : 'false'}`);
89
+ else if (Array.isArray(value))
90
+ argv.push(`--${arg.name}=${value.map((item) => String(item)).join(',')}`);
91
+ else
92
+ argv.push(`--${arg.name}=${String(value)}`);
93
+ }
94
+ return argv;
95
+ }
96
+ export function pluginKeyArgs(command, raw) {
97
+ const values = valuesOf(raw);
98
+ const ordered = [...command.args.filter((arg) => arg.required), ...command.args.filter((arg) => !arg.required)];
99
+ const parts = [];
100
+ for (const arg of ordered) {
101
+ const value = values[arg.name];
102
+ if (value === undefined || value === null || value === '')
103
+ continue;
104
+ parts.push(`${arg.name}=${String(value)}`);
105
+ if (parts.length === KEY_ARG_LIMIT)
106
+ break;
107
+ }
108
+ return parts.length === 0 ? NO_KEY_ARGS : parts.join('、');
109
+ }
110
+ export function pluginApprovalQuestion(command, raw) {
111
+ const key = pluginKeyArgs(command, raw);
112
+ const zhKey = key === NO_KEY_ARGS ? '这次没带参数' : key;
113
+ const enKey = key === NO_KEY_ARGS ? 'no arguments' : key;
114
+ return {
115
+ zh: `agent 想执行工作台 [${command.plugin}] opencli 插件的 [${command.name}] 命令(${zhKey}),允许吗?`,
116
+ en: `The agent wants to run the [${command.name}] command of the workbench opencli plugin [${command.plugin}] (${enKey}). Allow?`,
117
+ };
118
+ }
119
+ export function pluginApprovalFor(command) {
120
+ const denied = command.access === 'read';
121
+ return (call) => {
122
+ if (denied)
123
+ return { kind: 'read' };
124
+ const question = pluginApprovalQuestion(command, call.arguments);
125
+ return {
126
+ kind: 'ask',
127
+ reason: command.access === 'write'
128
+ ? `${command.plugin} ${command.name} 在 opencli 里标着 write`
129
+ : `${command.plugin} ${command.name} 没有拿到 access(fail-safe:按写处理)`,
130
+ question,
131
+ fallback: question,
132
+ };
133
+ };
134
+ }