@swarmclawai/swarmclaw 1.9.1 → 1.9.3
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 +21 -1
- package/electron-dist/main.js +218 -0
- package/package.json +3 -3
- package/scripts/ensure-sandbox-browser-image.mjs +12 -2
- package/src/app/api/extensions/managed-resources/route.test.ts +117 -0
- package/src/app/api/extensions/managed-resources/route.ts +116 -0
- package/src/app/api/knowledge/hygiene/route.ts +19 -1
- package/src/app/api/portability/export/route.test.ts +17 -0
- package/src/app/api/portability/export/route.ts +11 -2
- package/src/cli/index.js +2 -0
- package/src/cli/spec.js +2 -0
- package/src/lib/server/agents/delegation-advisory.test.ts +1 -0
- package/src/lib/server/agents/delegation-advisory.ts +10 -0
- package/src/lib/server/chat-execution/iteration-event-handler.ts +24 -8
- package/src/lib/server/chat-execution/reasoning-tag-scrubber.test.ts +117 -0
- package/src/lib/server/chat-execution/reasoning-tag-scrubber.ts +219 -0
- package/src/lib/server/extension-managed-resources.test.ts +159 -0
- package/src/lib/server/extension-managed-resources.ts +905 -0
- package/src/lib/server/extensions.ts +113 -2
- package/src/lib/server/knowledge-sources.test.ts +45 -0
- package/src/lib/server/knowledge-sources.ts +33 -0
- package/src/lib/server/portability/export.ts +10 -0
- package/src/lib/server/session-tools/crud.ts +25 -2
- package/src/lib/server/session-tools/extension-creator.ts +50 -0
- package/src/lib/server/session-tools/manage-tasks.test.ts +7 -2
- package/src/lib/server/tasks/task-route-service.ts +18 -1
- package/src/lib/server/tasks/task-service.test.ts +60 -2
- package/src/lib/server/tasks/task-service.ts +35 -0
- package/src/lib/validation/schemas.ts +2 -0
- package/src/types/agent.ts +2 -0
- package/src/types/app-settings.ts +8 -0
- package/src/types/extension.ts +132 -0
- package/src/types/misc.ts +1 -1
- package/src/types/schedule.ts +3 -0
- package/src/views/settings/extension-manager.tsx +157 -1
package/README.md
CHANGED
|
@@ -185,7 +185,7 @@ Full hosted deployment guides live at https://swarmclaw.ai/docs/deployment
|
|
|
185
185
|
- **Wallets**: linked Base wallet generation, address management, approval-oriented limits, and agent payout identity.
|
|
186
186
|
- **Connectors**: Discord, Slack, Telegram, WhatsApp, Teams, Matrix, OpenClaw, SwarmDock, SwarmFeed, and more.
|
|
187
187
|
- **MCP Servers**: connect any Model Context Protocol server (stdio, SSE, or streamable HTTP) and inject its tools into agents alongside built-ins. Configure, test, and assign per-agent from the MCP Servers panel.
|
|
188
|
-
- **Extensions**: external tool extensions, UI modules, hooks,
|
|
188
|
+
- **Extensions**: external tool extensions, UI modules, hooks, install/update flows, and managed resource manifests for extension-owned agents, routines, local folders, gateways, and setup checks.
|
|
189
189
|
|
|
190
190
|
## What SwarmClaw Focuses On
|
|
191
191
|
|
|
@@ -399,6 +399,26 @@ Operational docs: https://swarmclaw.ai/docs/observability
|
|
|
399
399
|
|
|
400
400
|
## Releases
|
|
401
401
|
|
|
402
|
+
### v1.9.3 Highlights
|
|
403
|
+
|
|
404
|
+
Bundled extension-orchestration release: Paperclip-style managed plugin resources, Hermes-style gateway/setup declarations, and safer local folder access in one release cycle.
|
|
405
|
+
|
|
406
|
+
- **Managed extension resources.** Extensions can now declare provisionable agents, schedules/routines, local folders, gateway platforms, and setup checks through `managedResources` or Paperclip-compatible top-level aliases.
|
|
407
|
+
- **Deterministic reconciliation.** `/api/extensions/managed-resources` can preview and reconcile extension-owned agents and routines with stable IDs and `managedByExtension` markers.
|
|
408
|
+
- **Trusted local folders.** Extension-declared local folders support root-bounded inspection and recursive listing with traversal and symlink-escape protection.
|
|
409
|
+
- **Operator UI.** The Extensions screen now shows managed-resource badges and a Managed tab with totals plus per-extension reconcile controls.
|
|
410
|
+
- **Extension authoring spec.** `extension_creator` now documents managed resources, gateway declarations, setup checks, and Paperclip-compatible manifest aliases.
|
|
411
|
+
|
|
412
|
+
### v1.9.2 Highlights
|
|
413
|
+
|
|
414
|
+
Bundled competitor-parity release: Hermes-style reasoning hygiene, deterministic delegation routing, Mission Control task workflow polish, OpenClaw export hardening, and Paperclip-style timeout hygiene.
|
|
415
|
+
|
|
416
|
+
- **Stateful reasoning tag scrubber.** String-streamed `<think>`, `<thinking>`, `<reasoning>`, `<thought>`, and `<REASONING_SCRATCHPAD>` blocks are removed across split deltas and routed into SwarmClaw's thinking stream instead of leaking into visible answers.
|
|
417
|
+
- **Deterministic delegation profiles.** `manage_tasks` now accepts explicit `workType` and `requiredCapabilities` routing hints, returns a stable `routeKey`, and can auto-assign unowned work without a classifier call when the profile is explicit.
|
|
418
|
+
- **Assignment workflow transitions.** Newly assigned backlog/triage/todo tasks move into the `in_progress` workflow lane without changing their runtime status or queueing execution.
|
|
419
|
+
- **Knowledge hygiene pruning.** Archived or superseded knowledge sources can now be pruned after a retention window, with prune actions recorded in the hygiene summary.
|
|
420
|
+
- **Collision-safe exports and timeout hardening.** Portability exports support timestamped attachment filenames, the sandbox browser image build has a configurable timeout, and release notes now carry the macOS quarantine workaround for ad-hoc signed desktop builds.
|
|
421
|
+
|
|
402
422
|
### v1.9.1 Highlights
|
|
403
423
|
|
|
404
424
|
Task execution workspace release: the first Paperclip-style work-control slice for task-scoped workspaces, preview handoffs, and liveness evidence.
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
+
};
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
const electron_1 = require("electron");
|
|
40
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
41
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
42
|
+
const paths_1 = require("./paths");
|
|
43
|
+
const server_lifecycle_1 = require("./server-lifecycle");
|
|
44
|
+
const menu_1 = require("./menu");
|
|
45
|
+
const DEV_URL_DEFAULT = 'http://127.0.0.1:3456';
|
|
46
|
+
const LOG_TAIL_BYTES = 1500;
|
|
47
|
+
let mainWindow = null;
|
|
48
|
+
let serverHandle = null;
|
|
49
|
+
let serverLogFile = null;
|
|
50
|
+
let isQuitting = false;
|
|
51
|
+
const gotLock = electron_1.app.requestSingleInstanceLock();
|
|
52
|
+
if (!gotLock) {
|
|
53
|
+
electron_1.app.quit();
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
electron_1.app.on('second-instance', () => {
|
|
57
|
+
if (mainWindow) {
|
|
58
|
+
if (mainWindow.isMinimized())
|
|
59
|
+
mainWindow.restore();
|
|
60
|
+
mainWindow.focus();
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
electron_1.app.on('ready', () => void onReady());
|
|
64
|
+
electron_1.app.on('window-all-closed', () => {
|
|
65
|
+
if (process.platform !== 'darwin')
|
|
66
|
+
electron_1.app.quit();
|
|
67
|
+
});
|
|
68
|
+
electron_1.app.on('activate', () => {
|
|
69
|
+
if (mainWindow !== null)
|
|
70
|
+
return;
|
|
71
|
+
if (serverHandle) {
|
|
72
|
+
createMainWindow(serverHandle.url);
|
|
73
|
+
}
|
|
74
|
+
else if (!electron_1.app.isPackaged) {
|
|
75
|
+
createMainWindow(process.env.SWARMCLAW_DEV_URL || DEV_URL_DEFAULT);
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
electron_1.app.on('before-quit', () => {
|
|
79
|
+
isQuitting = true;
|
|
80
|
+
});
|
|
81
|
+
electron_1.app.on('will-quit', async (event) => {
|
|
82
|
+
if (!serverHandle)
|
|
83
|
+
return;
|
|
84
|
+
event.preventDefault();
|
|
85
|
+
try {
|
|
86
|
+
await serverHandle.stop();
|
|
87
|
+
}
|
|
88
|
+
finally {
|
|
89
|
+
serverHandle = null;
|
|
90
|
+
electron_1.app.exit(0);
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
async function onReady() {
|
|
95
|
+
const paths = (0, paths_1.resolveRuntimePaths)();
|
|
96
|
+
(0, menu_1.buildAppMenu)(paths, () => mainWindow);
|
|
97
|
+
const iconPath = resolveIconPath();
|
|
98
|
+
if (process.platform === 'darwin' && iconPath && electron_1.app.dock) {
|
|
99
|
+
const img = electron_1.nativeImage.createFromPath(iconPath);
|
|
100
|
+
if (!img.isEmpty())
|
|
101
|
+
electron_1.app.dock.setIcon(img);
|
|
102
|
+
}
|
|
103
|
+
if (!electron_1.app.isPackaged) {
|
|
104
|
+
const devUrl = process.env.SWARMCLAW_DEV_URL || DEV_URL_DEFAULT;
|
|
105
|
+
console.log(`[swarmclaw] dev mode, loading ${devUrl}`);
|
|
106
|
+
createMainWindow(devUrl);
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
serverLogFile = node_path_1.default.join(electron_1.app.getPath('userData'), 'logs', 'server.log');
|
|
110
|
+
node_fs_1.default.mkdirSync(node_path_1.default.dirname(serverLogFile), { recursive: true });
|
|
111
|
+
try {
|
|
112
|
+
serverHandle = await (0, server_lifecycle_1.startEmbeddedServer)({
|
|
113
|
+
paths,
|
|
114
|
+
logFile: serverLogFile,
|
|
115
|
+
onStdout: (c) => process.stdout.write(`[swarmclaw] ${c}`),
|
|
116
|
+
onStderr: (c) => process.stderr.write(`[swarmclaw] ${c}`),
|
|
117
|
+
onExit: (code, signal) => {
|
|
118
|
+
if (!isQuitting) {
|
|
119
|
+
console.error(`[swarmclaw] server exited unexpectedly (code=${code}, signal=${signal ?? 'none'})`);
|
|
120
|
+
void showServerCrashDialog(code, signal);
|
|
121
|
+
}
|
|
122
|
+
},
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
catch (err) {
|
|
126
|
+
await showStartupFailureDialog(err, paths);
|
|
127
|
+
electron_1.app.exit(1);
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
createMainWindow(serverHandle.url);
|
|
131
|
+
void Promise.resolve().then(() => __importStar(require('./updater'))).then((m) => m.initAutoUpdater());
|
|
132
|
+
}
|
|
133
|
+
function resolveIconPath() {
|
|
134
|
+
const candidate = electron_1.app.isPackaged
|
|
135
|
+
? node_path_1.default.join(process.resourcesPath, 'icon.png')
|
|
136
|
+
: node_path_1.default.join(__dirname, '..', 'resources', 'icon.png');
|
|
137
|
+
return node_fs_1.default.existsSync(candidate) ? candidate : undefined;
|
|
138
|
+
}
|
|
139
|
+
function createMainWindow(startUrl) {
|
|
140
|
+
const iconPath = resolveIconPath();
|
|
141
|
+
mainWindow = new electron_1.BrowserWindow({
|
|
142
|
+
width: 1440,
|
|
143
|
+
height: 900,
|
|
144
|
+
minWidth: 1024,
|
|
145
|
+
minHeight: 640,
|
|
146
|
+
backgroundColor: '#0b0b0f',
|
|
147
|
+
show: true,
|
|
148
|
+
...(iconPath ? { icon: iconPath } : {}),
|
|
149
|
+
webPreferences: {
|
|
150
|
+
contextIsolation: true,
|
|
151
|
+
nodeIntegration: false,
|
|
152
|
+
sandbox: false,
|
|
153
|
+
},
|
|
154
|
+
});
|
|
155
|
+
const wc = mainWindow.webContents;
|
|
156
|
+
if (!electron_1.app.isPackaged)
|
|
157
|
+
wc.openDevTools({ mode: 'detach' });
|
|
158
|
+
wc.on('did-start-loading', () => console.log('[swarmclaw] did-start-loading'));
|
|
159
|
+
wc.on('did-finish-load', () => console.log('[swarmclaw] did-finish-load'));
|
|
160
|
+
wc.on('did-fail-load', (_e, code, desc, url) => console.error(`[swarmclaw] did-fail-load code=${code} desc=${desc} url=${url}`));
|
|
161
|
+
wc.on('render-process-gone', (_e, details) => console.error(`[swarmclaw] render-process-gone reason=${details.reason}`));
|
|
162
|
+
wc.on('unresponsive', () => console.error('[swarmclaw] webContents unresponsive'));
|
|
163
|
+
mainWindow.on('closed', () => {
|
|
164
|
+
mainWindow = null;
|
|
165
|
+
});
|
|
166
|
+
mainWindow.webContents.setWindowOpenHandler(({ url }) => {
|
|
167
|
+
if (url.startsWith(startUrl))
|
|
168
|
+
return { action: 'allow' };
|
|
169
|
+
void electron_1.shell.openExternal(url);
|
|
170
|
+
return { action: 'deny' };
|
|
171
|
+
});
|
|
172
|
+
void mainWindow.loadURL(startUrl).catch((err) => {
|
|
173
|
+
console.error('[swarmclaw] loadURL rejected:', err);
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
async function showServerCrashDialog(code, signal) {
|
|
177
|
+
const buttons = serverLogFile ? ['Open Logs Folder', 'Quit'] : ['Quit'];
|
|
178
|
+
const quitButtonId = buttons.length - 1;
|
|
179
|
+
const detail = buildLogDetail(`code=${code ?? 'null'} signal=${signal ?? 'none'}`);
|
|
180
|
+
const res = await electron_1.dialog.showMessageBox({
|
|
181
|
+
type: 'error',
|
|
182
|
+
buttons,
|
|
183
|
+
defaultId: quitButtonId,
|
|
184
|
+
cancelId: quitButtonId,
|
|
185
|
+
title: 'SwarmClaw stopped',
|
|
186
|
+
message: 'The SwarmClaw server exited unexpectedly.',
|
|
187
|
+
detail,
|
|
188
|
+
});
|
|
189
|
+
if (serverLogFile && res.response === 0)
|
|
190
|
+
electron_1.shell.showItemInFolder(serverLogFile);
|
|
191
|
+
electron_1.app.exit(1);
|
|
192
|
+
}
|
|
193
|
+
async function showStartupFailureDialog(err, paths) {
|
|
194
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
195
|
+
const base = `${message}\n\nStandalone entry: ${paths.standaloneEntry}\nData dir: ${paths.dataDir}`;
|
|
196
|
+
const detail = buildLogDetail(base);
|
|
197
|
+
const buttons = serverLogFile ? ['Open Logs Folder', 'Quit'] : ['Quit'];
|
|
198
|
+
const quitButtonId = buttons.length - 1;
|
|
199
|
+
const res = await electron_1.dialog.showMessageBox({
|
|
200
|
+
type: 'error',
|
|
201
|
+
buttons,
|
|
202
|
+
defaultId: quitButtonId,
|
|
203
|
+
cancelId: quitButtonId,
|
|
204
|
+
title: 'SwarmClaw failed to start',
|
|
205
|
+
message: 'The embedded server did not start.',
|
|
206
|
+
detail,
|
|
207
|
+
});
|
|
208
|
+
if (serverLogFile && res.response === 0)
|
|
209
|
+
electron_1.shell.showItemInFolder(serverLogFile);
|
|
210
|
+
}
|
|
211
|
+
function buildLogDetail(base) {
|
|
212
|
+
if (!serverLogFile)
|
|
213
|
+
return base;
|
|
214
|
+
const tail = (0, server_lifecycle_1.tailLogFile)(serverLogFile, LOG_TAIL_BYTES).trim();
|
|
215
|
+
if (!tail)
|
|
216
|
+
return `${base}\n\nLog file: ${serverLogFile}\n(no output captured yet)`;
|
|
217
|
+
return `${base}\n\nLog tail (${serverLogFile}):\n${tail}`;
|
|
218
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@swarmclawai/swarmclaw",
|
|
3
|
-
"version": "1.9.
|
|
3
|
+
"version": "1.9.3",
|
|
4
4
|
"description": "Build and run autonomous AI agents with OpenClaw, Hermes, multiple model providers, orchestration, delegation, memory, skills, schedules, and chat connectors.",
|
|
5
5
|
"main": "electron-dist/main.js",
|
|
6
6
|
"license": "MIT",
|
|
@@ -84,10 +84,10 @@
|
|
|
84
84
|
"lint:baseline": "node ./scripts/lint-baseline.mjs check",
|
|
85
85
|
"lint:baseline:update": "node ./scripts/lint-baseline.mjs update",
|
|
86
86
|
"cli": "node ./bin/swarmclaw.js",
|
|
87
|
-
"test:cli": "node --test src/cli/*.test.js bin/*.test.js scripts/electron-after-pack.test.mjs scripts/postinstall.test.mjs scripts/run-next-build.test.mjs scripts/run-next-typegen.test.mjs",
|
|
87
|
+
"test:cli": "node --test src/cli/*.test.js bin/*.test.js scripts/electron-after-pack.test.mjs scripts/ensure-sandbox-browser-image.test.mjs scripts/postinstall.test.mjs scripts/run-next-build.test.mjs scripts/run-next-typegen.test.mjs",
|
|
88
88
|
"test:setup": "tsx --test src/app/api/setup/check-provider/route.test.ts src/lib/server/provider-model-discovery.test.ts src/components/auth/setup-wizard/utils.test.ts src/components/auth/setup-wizard/types.test.ts src/hooks/setup-done-detection.test.ts src/lib/setup-defaults.test.ts src/lib/server/storage-auth.test.ts src/lib/server/storage-auth-docker.test.ts",
|
|
89
89
|
"test:openclaw": "tsx --test src/lib/openclaw/openclaw-agent-id.test.ts src/lib/openclaw/openclaw-endpoint.test.ts src/lib/server/agents/agent-runtime-config.test.ts src/lib/server/build-llm.test.ts src/lib/server/connectors/connector-routing.test.ts src/lib/server/connectors/openclaw.test.ts src/lib/server/connectors/swarmdock.test.ts src/lib/server/gateway/protocol.test.ts src/lib/server/gateways/gateway-topology.test.ts src/lib/server/llm-response-cache.test.ts src/lib/server/mcp-conformance.test.ts src/lib/server/openclaw/agent-resolver.test.ts src/lib/server/openclaw/deploy.test.ts src/lib/server/openclaw/skills-normalize.test.ts src/lib/server/session-tools/openclaw-nodes.test.ts src/lib/server/session-tools/swarmdock.test.ts src/lib/server/tasks/task-quality-gate.test.ts src/lib/server/tasks/task-validation.test.ts src/lib/server/tool-capability-policy.test.ts src/lib/providers/openai.test.ts src/lib/providers/openclaw-exports.test.ts src/app/api/gateways/topology-route.test.ts src/app/api/openclaw/dashboard-url/route.test.ts",
|
|
90
|
-
"test:runtime": "tsx --test src/lib/a2a/agent-card.test.ts src/lib/strip-internal-metadata.test.ts src/lib/provider-sets.test.ts src/lib/providers/opencode-cli.test.ts src/lib/providers/cli-provider-metadata.test.ts src/lib/providers/cli-utils.test.ts src/lib/providers/generic-cli.test.ts src/lib/server/cli-provider-readiness.test.ts src/lib/server/provider-health.test.ts src/lib/server/mcp-gateway-runtime.test.ts src/lib/server/mcp-connection-pool.test.ts src/lib/server/knowledge-sources.test.ts src/lib/server/chat-execution/chat-execution-grounding.test.ts src/lib/server/chat-execution/chat-turn-preparation.test.ts src/lib/server/chat-execution/iteration-timers.test.ts src/lib/server/chat-execution/post-stream-finalization.test.ts src/lib/server/chats/clear-undo-snapshots.test.ts src/lib/server/connectors/email.test.ts src/lib/server/protocols/protocol-service.test.ts src/lib/server/runtime/run-ledger.test.ts src/lib/server/runtime/queue-retry-policy.test.ts src/lib/server/runs/run-brief.test.ts src/lib/server/operations/operation-pulse.test.ts src/lib/server/artifacts/artifact-resolver.test.ts src/lib/server/observability/otel-config.test.ts src/lib/server/safe-parse-body.test.ts src/lib/server/missions/mission-templates.test.ts src/lib/server/sharing/share-link-repository.test.ts src/lib/server/sharing/share-resolver.test.ts src/lib/server/tasks/task-execution-workspace.test.ts src/lib/server/session-tools/execute.test.ts src/lib/app/view-constants.test.ts src/lib/quality/quality-summary.test.ts src/app/api/approvals/route.test.ts src/app/api/agents/agents-route.test.ts src/app/api/tasks/tasks-route.test.ts src/app/api/tasks/task-workspace-route.test.ts src/app/api/chats/chat-route.test.ts src/app/api/chats/clear-route.test.ts src/app/api/chats/compact-route.test.ts src/app/api/chats/context-status-route.test.ts src/app/api/connectors/connector-doctor-route.test.ts src/app/api/healthz/route.test.ts src/app/api/logs/route.test.ts src/app/api/providers/[id]/route.test.ts src/app/api/tts/route.test.ts",
|
|
90
|
+
"test:runtime": "tsx --test src/lib/a2a/agent-card.test.ts src/lib/strip-internal-metadata.test.ts src/lib/provider-sets.test.ts src/lib/providers/opencode-cli.test.ts src/lib/providers/cli-provider-metadata.test.ts src/lib/providers/cli-utils.test.ts src/lib/providers/generic-cli.test.ts src/lib/server/agents/delegation-advisory.test.ts src/lib/server/cli-provider-readiness.test.ts src/lib/server/provider-health.test.ts src/lib/server/mcp-gateway-runtime.test.ts src/lib/server/mcp-connection-pool.test.ts src/lib/server/knowledge-sources.test.ts src/lib/server/extension-managed-resources.test.ts src/lib/server/chat-execution/chat-execution-grounding.test.ts src/lib/server/chat-execution/chat-turn-preparation.test.ts src/lib/server/chat-execution/iteration-timers.test.ts src/lib/server/chat-execution/post-stream-finalization.test.ts src/lib/server/chat-execution/reasoning-tag-scrubber.test.ts src/lib/server/chats/clear-undo-snapshots.test.ts src/lib/server/connectors/email.test.ts src/lib/server/protocols/protocol-service.test.ts src/lib/server/runtime/run-ledger.test.ts src/lib/server/runtime/queue-retry-policy.test.ts src/lib/server/runs/run-brief.test.ts src/lib/server/operations/operation-pulse.test.ts src/lib/server/artifacts/artifact-resolver.test.ts src/lib/server/observability/otel-config.test.ts src/lib/server/safe-parse-body.test.ts src/lib/server/missions/mission-templates.test.ts src/lib/server/sharing/share-link-repository.test.ts src/lib/server/sharing/share-resolver.test.ts src/lib/server/tasks/task-execution-workspace.test.ts src/lib/server/tasks/task-service.test.ts src/lib/server/session-tools/execute.test.ts src/lib/server/session-tools/manage-tasks.test.ts src/lib/app/view-constants.test.ts src/lib/quality/quality-summary.test.ts src/app/api/approvals/route.test.ts src/app/api/agents/agents-route.test.ts src/app/api/tasks/tasks-route.test.ts src/app/api/tasks/task-workspace-route.test.ts src/app/api/chats/chat-route.test.ts src/app/api/chats/clear-route.test.ts src/app/api/chats/compact-route.test.ts src/app/api/chats/context-status-route.test.ts src/app/api/connectors/connector-doctor-route.test.ts src/app/api/extensions/managed-resources/route.test.ts src/app/api/healthz/route.test.ts src/app/api/logs/route.test.ts src/app/api/portability/export/route.test.ts src/app/api/providers/[id]/route.test.ts src/app/api/tts/route.test.ts",
|
|
91
91
|
"test:builder": "tsx --test src/features/protocols/builder/utils/nodes-to-template.test.ts src/features/protocols/builder/utils/template-to-nodes.test.ts src/features/protocols/builder/validators/dag-validator.test.ts",
|
|
92
92
|
"test:e2e": "node --import tsx scripts/browser-e2e-smoke.ts",
|
|
93
93
|
"test:mcp:conformance": "node --import tsx ./scripts/mcp-conformance-check.ts",
|
|
@@ -10,6 +10,7 @@ const args = new Set(process.argv.slice(2))
|
|
|
10
10
|
const quiet = args.has('--quiet')
|
|
11
11
|
const required = args.has('--required')
|
|
12
12
|
const image = process.env.SWARMCLAW_SANDBOX_BROWSER_IMAGE || 'swarmclaw-sandbox-browser:bookworm-slim'
|
|
13
|
+
const DEFAULT_BUILD_TIMEOUT_MS = 20 * 60 * 1000
|
|
13
14
|
const SOURCE_LABEL = 'swarmclaw.sandboxBrowserSourceHash'
|
|
14
15
|
|
|
15
16
|
function log(message) {
|
|
@@ -29,6 +30,11 @@ function run(command, commandArgs, options = {}) {
|
|
|
29
30
|
})
|
|
30
31
|
}
|
|
31
32
|
|
|
33
|
+
function readPositiveInteger(value, fallback) {
|
|
34
|
+
const parsed = Number.parseInt(String(value || ''), 10)
|
|
35
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback
|
|
36
|
+
}
|
|
37
|
+
|
|
32
38
|
function commandExists(name) {
|
|
33
39
|
const lookup = process.platform === 'win32' ? 'where' : 'which'
|
|
34
40
|
const result = run(lookup, [name])
|
|
@@ -56,6 +62,7 @@ function readImageLabel(name, label) {
|
|
|
56
62
|
}
|
|
57
63
|
|
|
58
64
|
function buildImage(sourceHash) {
|
|
65
|
+
const timeoutMs = readPositiveInteger(process.env.SWARMCLAW_SANDBOX_BROWSER_BUILD_TIMEOUT_MS, DEFAULT_BUILD_TIMEOUT_MS)
|
|
59
66
|
log(`Building sandbox browser image ${image}...`)
|
|
60
67
|
const result = spawnSync(
|
|
61
68
|
'docker',
|
|
@@ -69,13 +76,16 @@ function buildImage(sourceHash) {
|
|
|
69
76
|
{
|
|
70
77
|
cwd,
|
|
71
78
|
stdio: 'inherit',
|
|
79
|
+
timeout: timeoutMs,
|
|
72
80
|
},
|
|
73
81
|
)
|
|
74
82
|
if (result.error || (result.status ?? 1) !== 0) {
|
|
83
|
+
const timedOut = result.error?.code === 'ETIMEDOUT' || result.signal === 'SIGTERM'
|
|
84
|
+
const detail = timedOut ? ` Build timed out after ${timeoutMs}ms.` : ''
|
|
75
85
|
if (required) {
|
|
76
|
-
fail(`Failed to build sandbox browser image ${image}
|
|
86
|
+
fail(`Failed to build sandbox browser image ${image}.${detail}`, result.status ?? 1)
|
|
77
87
|
}
|
|
78
|
-
log(`Skipping sandbox browser image after build failure
|
|
88
|
+
log(`Skipping sandbox browser image after build failure.${detail}`)
|
|
79
89
|
return false
|
|
80
90
|
}
|
|
81
91
|
log(`Sandbox browser image ready: ${image}`)
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import test, { afterEach } from 'node:test'
|
|
3
|
+
import fs from 'node:fs'
|
|
4
|
+
import path from 'node:path'
|
|
5
|
+
import os from 'node:os'
|
|
6
|
+
|
|
7
|
+
import { getExtensionManager } from '@/lib/server/extensions'
|
|
8
|
+
import { loadAgents, loadSchedules, loadSettings, saveAgents, saveSchedules, saveSettings } from '@/lib/server/storage'
|
|
9
|
+
import { GET, POST } from './route'
|
|
10
|
+
|
|
11
|
+
const originalAgents = loadAgents()
|
|
12
|
+
const originalSchedules = loadSchedules()
|
|
13
|
+
const originalSettings = loadSettings()
|
|
14
|
+
|
|
15
|
+
let seq = 0
|
|
16
|
+
|
|
17
|
+
function extensionId(prefix: string): string {
|
|
18
|
+
seq += 1
|
|
19
|
+
return `${prefix}_${Date.now()}_${seq}`
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
afterEach(() => {
|
|
23
|
+
saveAgents(originalAgents)
|
|
24
|
+
saveSchedules(originalSchedules)
|
|
25
|
+
saveSettings(originalSettings)
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
test('managed resources route reconciles one extension', async () => {
|
|
29
|
+
const id = extensionId('route_managed_resources')
|
|
30
|
+
getExtensionManager().registerBuiltin(id, {
|
|
31
|
+
name: 'Route Managed Fixture',
|
|
32
|
+
managedResources: {
|
|
33
|
+
agents: [
|
|
34
|
+
{
|
|
35
|
+
agentKey: 'operator',
|
|
36
|
+
displayName: 'Route Operator',
|
|
37
|
+
provider: 'openai',
|
|
38
|
+
model: 'gpt-4o-mini',
|
|
39
|
+
},
|
|
40
|
+
],
|
|
41
|
+
},
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
const before = await GET(new Request('http://local/api/extensions/managed-resources'))
|
|
45
|
+
assert.equal(before.status, 200)
|
|
46
|
+
const beforeBody = await before.json()
|
|
47
|
+
const extension = beforeBody.extensions.find((entry: { extensionId: string }) => entry.extensionId === id)
|
|
48
|
+
assert.equal(extension.agents[0].status, 'missing')
|
|
49
|
+
|
|
50
|
+
const response = await POST(new Request('http://local/api/extensions/managed-resources', {
|
|
51
|
+
method: 'POST',
|
|
52
|
+
headers: { 'content-type': 'application/json' },
|
|
53
|
+
body: JSON.stringify({ action: 'reconcile', extensionId: id }),
|
|
54
|
+
}))
|
|
55
|
+
|
|
56
|
+
assert.equal(response.status, 200)
|
|
57
|
+
const body = await response.json()
|
|
58
|
+
assert.equal(body.createdAgents.length, 1)
|
|
59
|
+
const agent = loadAgents()[body.createdAgents[0]]
|
|
60
|
+
assert.ok(agent)
|
|
61
|
+
assert.equal(agent.managedByExtension?.extensionId, id)
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
test('managed resources route configures and lists a local folder', async () => {
|
|
65
|
+
const id = extensionId('route_managed_folder')
|
|
66
|
+
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'swarmclaw-route-folder-'))
|
|
67
|
+
fs.mkdirSync(path.join(tempDir, 'inputs'))
|
|
68
|
+
fs.writeFileSync(path.join(tempDir, 'inputs', 'task.txt'), 'task\n')
|
|
69
|
+
|
|
70
|
+
getExtensionManager().registerBuiltin(id, {
|
|
71
|
+
name: 'Route Folder Fixture',
|
|
72
|
+
managedResources: {
|
|
73
|
+
localFolders: [
|
|
74
|
+
{
|
|
75
|
+
folderKey: 'workspace',
|
|
76
|
+
displayName: 'Workspace Folder',
|
|
77
|
+
access: 'readWrite',
|
|
78
|
+
requiredDirectories: ['inputs'],
|
|
79
|
+
requiredFiles: ['inputs/task.txt'],
|
|
80
|
+
},
|
|
81
|
+
],
|
|
82
|
+
},
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
const configure = await POST(new Request('http://local/api/extensions/managed-resources', {
|
|
86
|
+
method: 'POST',
|
|
87
|
+
headers: { 'content-type': 'application/json' },
|
|
88
|
+
body: JSON.stringify({
|
|
89
|
+
action: 'configure_local_folder',
|
|
90
|
+
extensionId: id,
|
|
91
|
+
folderKey: 'workspace',
|
|
92
|
+
path: tempDir,
|
|
93
|
+
}),
|
|
94
|
+
}))
|
|
95
|
+
assert.equal(configure.status, 200)
|
|
96
|
+
const configured = await configure.json()
|
|
97
|
+
assert.equal(configured.status.healthy, true)
|
|
98
|
+
|
|
99
|
+
const listing = await GET(new Request(`http://local/api/extensions/managed-resources?action=list_local_folder&extensionId=${encodeURIComponent(id)}&folderKey=workspace&recursive=true`))
|
|
100
|
+
assert.equal(listing.status, 200)
|
|
101
|
+
const listingBody = await listing.json()
|
|
102
|
+
assert.ok(listingBody.entries.some((entry: { path: string }) => entry.path === 'inputs/task.txt'))
|
|
103
|
+
|
|
104
|
+
const traversal = await POST(new Request('http://local/api/extensions/managed-resources', {
|
|
105
|
+
method: 'POST',
|
|
106
|
+
headers: { 'content-type': 'application/json' },
|
|
107
|
+
body: JSON.stringify({
|
|
108
|
+
action: 'list_local_folder',
|
|
109
|
+
extensionId: id,
|
|
110
|
+
folderKey: 'workspace',
|
|
111
|
+
relativePath: '../outside',
|
|
112
|
+
}),
|
|
113
|
+
}))
|
|
114
|
+
assert.equal(traversal.status, 400)
|
|
115
|
+
|
|
116
|
+
fs.rmSync(tempDir, { recursive: true, force: true })
|
|
117
|
+
})
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server'
|
|
2
|
+
import { safeParseBody } from '@/lib/server/safe-parse-body'
|
|
3
|
+
import {
|
|
4
|
+
inspectExtensionLocalFolder,
|
|
5
|
+
listExtensionLocalFolderEntries,
|
|
6
|
+
listExtensionManagedResources,
|
|
7
|
+
reconcileExtensionManagedResources,
|
|
8
|
+
setExtensionLocalFolderConfig,
|
|
9
|
+
} from '@/lib/server/extension-managed-resources'
|
|
10
|
+
import { errorMessage } from '@/lib/shared-utils'
|
|
11
|
+
import '@/lib/server/builtin-extensions'
|
|
12
|
+
|
|
13
|
+
export const dynamic = 'force-dynamic'
|
|
14
|
+
|
|
15
|
+
function badRequest(message: string) {
|
|
16
|
+
return NextResponse.json({ error: message }, { status: 400 })
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function optionalText(value: unknown): string | null {
|
|
20
|
+
return typeof value === 'string' && value.trim() ? value.trim() : null
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export async function GET(req: Request) {
|
|
24
|
+
const { searchParams } = new URL(req.url)
|
|
25
|
+
const action = searchParams.get('action')
|
|
26
|
+
|
|
27
|
+
try {
|
|
28
|
+
if (action === 'inspect_local_folder') {
|
|
29
|
+
const extensionId = optionalText(searchParams.get('extensionId'))
|
|
30
|
+
const folderKey = optionalText(searchParams.get('folderKey'))
|
|
31
|
+
if (!extensionId || !folderKey) return badRequest('extensionId and folderKey required')
|
|
32
|
+
return NextResponse.json(await inspectExtensionLocalFolder({
|
|
33
|
+
extensionId,
|
|
34
|
+
folderKey,
|
|
35
|
+
}))
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (action === 'list_local_folder') {
|
|
39
|
+
const extensionId = optionalText(searchParams.get('extensionId'))
|
|
40
|
+
const folderKey = optionalText(searchParams.get('folderKey'))
|
|
41
|
+
if (!extensionId || !folderKey) return badRequest('extensionId and folderKey required')
|
|
42
|
+
return NextResponse.json(await listExtensionLocalFolderEntries({
|
|
43
|
+
extensionId,
|
|
44
|
+
folderKey,
|
|
45
|
+
relativePath: optionalText(searchParams.get('relativePath')),
|
|
46
|
+
recursive: searchParams.get('recursive') === 'true',
|
|
47
|
+
maxEntries: Number(searchParams.get('maxEntries') || 1000),
|
|
48
|
+
}))
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return NextResponse.json(listExtensionManagedResources())
|
|
52
|
+
} catch (err: unknown) {
|
|
53
|
+
return badRequest(errorMessage(err))
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export async function POST(req: Request) {
|
|
58
|
+
const { data: body, error } = await safeParseBody(req)
|
|
59
|
+
if (error) return error
|
|
60
|
+
const input = body && typeof body === 'object' ? body as Record<string, unknown> : {}
|
|
61
|
+
const action = optionalText(input.action)
|
|
62
|
+
|
|
63
|
+
try {
|
|
64
|
+
if (action === 'reconcile') {
|
|
65
|
+
return NextResponse.json(reconcileExtensionManagedResources(optionalText(input.extensionId)))
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (action === 'configure_local_folder') {
|
|
69
|
+
const extensionId = optionalText(input.extensionId)
|
|
70
|
+
const folderKey = optionalText(input.folderKey)
|
|
71
|
+
const folderPath = optionalText(input.path)
|
|
72
|
+
if (!extensionId || !folderKey || !folderPath) {
|
|
73
|
+
return badRequest('extensionId, folderKey, and path required')
|
|
74
|
+
}
|
|
75
|
+
const access = input.access === 'read' || input.access === 'readWrite'
|
|
76
|
+
? input.access
|
|
77
|
+
: undefined
|
|
78
|
+
const config = setExtensionLocalFolderConfig({
|
|
79
|
+
extensionId,
|
|
80
|
+
folderKey,
|
|
81
|
+
path: folderPath,
|
|
82
|
+
access,
|
|
83
|
+
})
|
|
84
|
+
const status = await inspectExtensionLocalFolder({ extensionId, folderKey })
|
|
85
|
+
return NextResponse.json({ ok: true, config, status })
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (action === 'inspect_local_folder') {
|
|
89
|
+
const extensionId = optionalText(input.extensionId)
|
|
90
|
+
const folderKey = optionalText(input.folderKey)
|
|
91
|
+
if (!extensionId || !folderKey) return badRequest('extensionId and folderKey required')
|
|
92
|
+
return NextResponse.json(await inspectExtensionLocalFolder({
|
|
93
|
+
extensionId,
|
|
94
|
+
folderKey,
|
|
95
|
+
overridePath: optionalText(input.path),
|
|
96
|
+
}))
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (action === 'list_local_folder') {
|
|
100
|
+
const extensionId = optionalText(input.extensionId)
|
|
101
|
+
const folderKey = optionalText(input.folderKey)
|
|
102
|
+
if (!extensionId || !folderKey) return badRequest('extensionId and folderKey required')
|
|
103
|
+
return NextResponse.json(await listExtensionLocalFolderEntries({
|
|
104
|
+
extensionId,
|
|
105
|
+
folderKey,
|
|
106
|
+
relativePath: optionalText(input.relativePath),
|
|
107
|
+
recursive: input.recursive === true,
|
|
108
|
+
maxEntries: typeof input.maxEntries === 'number' ? input.maxEntries : undefined,
|
|
109
|
+
}))
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return badRequest('action required')
|
|
113
|
+
} catch (err: unknown) {
|
|
114
|
+
return badRequest(errorMessage(err))
|
|
115
|
+
}
|
|
116
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { NextResponse } from 'next/server'
|
|
2
2
|
import {
|
|
3
3
|
getKnowledgeHygieneSummary,
|
|
4
|
+
pruneArchivedKnowledgeSources,
|
|
4
5
|
runKnowledgeHygieneMaintenance,
|
|
5
6
|
} from '@/lib/server/knowledge-sources'
|
|
6
7
|
|
|
@@ -8,6 +9,23 @@ export async function GET() {
|
|
|
8
9
|
return NextResponse.json(await getKnowledgeHygieneSummary())
|
|
9
10
|
}
|
|
10
11
|
|
|
11
|
-
export async function POST() {
|
|
12
|
+
export async function POST(req: Request) {
|
|
13
|
+
let body: Record<string, unknown> | null = null
|
|
14
|
+
if ((req.headers.get('content-type') || '').includes('application/json')) {
|
|
15
|
+
try {
|
|
16
|
+
const parsed = await req.json()
|
|
17
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) body = parsed as Record<string, unknown>
|
|
18
|
+
} catch {
|
|
19
|
+
body = null
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (body?.action === 'prune') {
|
|
24
|
+
const olderThanDays = typeof body.olderThanDays === 'number' ? body.olderThanDays : null
|
|
25
|
+
const result = await pruneArchivedKnowledgeSources({ olderThanDays })
|
|
26
|
+
const summary = await getKnowledgeHygieneSummary()
|
|
27
|
+
return NextResponse.json({ ...summary, prune: result })
|
|
28
|
+
}
|
|
29
|
+
|
|
12
30
|
return NextResponse.json(await runKnowledgeHygieneMaintenance())
|
|
13
31
|
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import { describe, it } from 'node:test'
|
|
3
|
+
|
|
4
|
+
import { GET } from './route'
|
|
5
|
+
import { buildPortableExportFilename } from '@/lib/server/portability/export'
|
|
6
|
+
|
|
7
|
+
describe('GET /api/portability/export', () => {
|
|
8
|
+
it('returns a collision-resistant attachment filename for downloads', async () => {
|
|
9
|
+
const response = await GET(new Request('http://local/api/portability/export?download=true'))
|
|
10
|
+
assert.equal(response.status, 200)
|
|
11
|
+
assert.equal(response.headers.get('content-type'), 'application/json; charset=utf-8')
|
|
12
|
+
const disposition = response.headers.get('content-disposition') || ''
|
|
13
|
+
assert.match(disposition, /^attachment; filename="swarmclaw-export-\d{8}-\d{6}\d{3}Z\.json"$/)
|
|
14
|
+
const body = await response.json()
|
|
15
|
+
assert.equal(disposition, `attachment; filename="${buildPortableExportFilename(body)}"`)
|
|
16
|
+
})
|
|
17
|
+
})
|
|
@@ -1,8 +1,17 @@
|
|
|
1
1
|
import { NextResponse } from 'next/server'
|
|
2
|
-
import { exportConfig } from '@/lib/server/portability/export'
|
|
2
|
+
import { buildPortableExportFilename, exportConfig } from '@/lib/server/portability/export'
|
|
3
3
|
export const dynamic = 'force-dynamic'
|
|
4
4
|
|
|
5
|
-
export async function GET() {
|
|
5
|
+
export async function GET(req: Request) {
|
|
6
6
|
const manifest = exportConfig()
|
|
7
|
+
const { searchParams } = new URL(req.url)
|
|
8
|
+
if (searchParams.get('download') === 'true') {
|
|
9
|
+
return new NextResponse(JSON.stringify(manifest, null, 2), {
|
|
10
|
+
headers: {
|
|
11
|
+
'content-type': 'application/json; charset=utf-8',
|
|
12
|
+
'content-disposition': `attachment; filename="${buildPortableExportFilename(manifest)}"`,
|
|
13
|
+
},
|
|
14
|
+
})
|
|
15
|
+
}
|
|
7
16
|
return NextResponse.json(manifest)
|
|
8
17
|
}
|
package/src/cli/index.js
CHANGED
|
@@ -526,6 +526,8 @@ const COMMAND_GROUPS = [
|
|
|
526
526
|
cmd('settings-set', 'PUT', '/extensions/settings', 'Set extension settings (use --query extensionId=extension_name and --data JSON)', { expectsJsonBody: true }),
|
|
527
527
|
cmd('ui', 'GET', '/extensions/ui', 'List extension UI modules (use --query type=sidebar|header|chat_actions|connectors)'),
|
|
528
528
|
cmd('builtins', 'GET', '/extensions/builtins', 'List built-in extensions'),
|
|
529
|
+
cmd('managed-resources', 'GET', '/extensions/managed-resources', 'Preview extension-managed agents, routines, folders, gateways, and setup checks'),
|
|
530
|
+
cmd('managed-resources-action', 'POST', '/extensions/managed-resources', 'Reconcile or inspect extension-managed resources', { expectsJsonBody: true }),
|
|
529
531
|
],
|
|
530
532
|
},
|
|
531
533
|
{
|
package/src/cli/spec.js
CHANGED
|
@@ -386,6 +386,8 @@ const COMMAND_GROUPS = {
|
|
|
386
386
|
install: { description: 'Install extension by URL', method: 'POST', path: '/extensions/install' },
|
|
387
387
|
'settings-get': { description: 'Read extension settings (supports --query extensionId=...)', method: 'GET', path: '/extensions/settings' },
|
|
388
388
|
'settings-set': { description: 'Write extension settings (supports --query extensionId=... and --data JSON)', method: 'PUT', path: '/extensions/settings' },
|
|
389
|
+
'managed-resources': { description: 'Preview extension-managed agents, routines, folders, gateways, and setup checks', method: 'GET', path: '/extensions/managed-resources' },
|
|
390
|
+
'managed-resources-action': { description: 'Reconcile or inspect extension-managed resources', method: 'POST', path: '/extensions/managed-resources' },
|
|
389
391
|
},
|
|
390
392
|
},
|
|
391
393
|
providers: {
|