@xtrape/capsule-agent-node 0.1.0-public-review.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +48 -0
- package/NOTICE +4 -0
- package/README.md +319 -0
- package/dist/index.cjs +293 -0
- package/dist/index.d.cts +139 -0
- package/dist/index.d.ts +139 -0
- package/dist/index.js +263 -0
- package/package.json +62 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
Apache License
|
|
2
|
+
Version 2.0, January 2004
|
|
3
|
+
http://www.apache.org/licenses/
|
|
4
|
+
|
|
5
|
+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
|
6
|
+
|
|
7
|
+
1. Definitions.
|
|
8
|
+
|
|
9
|
+
"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document.
|
|
10
|
+
|
|
11
|
+
"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License.
|
|
12
|
+
|
|
13
|
+
"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity.
|
|
14
|
+
|
|
15
|
+
"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License.
|
|
16
|
+
|
|
17
|
+
"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files.
|
|
18
|
+
|
|
19
|
+
"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form.
|
|
20
|
+
|
|
21
|
+
"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License.
|
|
22
|
+
|
|
23
|
+
"Derivative Works" shall mean any work, whether in Source or Object form, that is based on the Work and for which editorial revisions, annotations, elaborations, or other modifications represent an original work of authorship.
|
|
24
|
+
|
|
25
|
+
"Contribution" shall mean any work of authorship intentionally submitted to Licensor for inclusion in the Work by the copyright owner or authorized representative.
|
|
26
|
+
|
|
27
|
+
2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor grants You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and Derivative Works in Source or Object form.
|
|
28
|
+
|
|
29
|
+
3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor grants You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable patent license to make, use, sell, offer to sell, import, and otherwise transfer the Work.
|
|
30
|
+
|
|
31
|
+
4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works in any medium, with or without modifications, provided that You meet the following conditions:
|
|
32
|
+
|
|
33
|
+
(a) You must give recipients a copy of this License; and
|
|
34
|
+
(b) You must cause modified files to carry prominent notices; and
|
|
35
|
+
(c) You must retain copyright, patent, trademark, and attribution notices; and
|
|
36
|
+
(d) If the Work includes a NOTICE file, any Derivative Works that You distribute must include a readable copy of attribution notices from that NOTICE file.
|
|
37
|
+
|
|
38
|
+
5. Submission of Contributions. Unless explicitly stated otherwise, any Contribution submitted for inclusion in the Work shall be under the terms of this License.
|
|
39
|
+
|
|
40
|
+
6. Trademarks. This License does not grant permission to use trade names, trademarks, service marks, or product names of the Licensor.
|
|
41
|
+
|
|
42
|
+
7. Disclaimer of Warranty. Unless required by law or agreed to in writing, Licensor provides the Work on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND.
|
|
43
|
+
|
|
44
|
+
8. Limitation of Liability. In no event shall any Contributor be liable for damages arising from use of the Work.
|
|
45
|
+
|
|
46
|
+
9. Accepting Warranty or Additional Liability. While redistributing the Work, You may offer support, warranty, or other liability obligations only on Your own behalf.
|
|
47
|
+
|
|
48
|
+
END OF TERMS AND CONDITIONS
|
package/NOTICE
ADDED
package/README.md
ADDED
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
# @xtrape/capsule-agent-node
|
|
2
|
+
|
|
3
|
+
> Node.js embedded Agent SDK for connecting Capsule Services to Opstage.
|
|
4
|
+
|
|
5
|
+
[](./LICENSE)
|
|
6
|
+
[](https://xtrape-com.github.io/xtrape-capsule-site/)
|
|
7
|
+
[](https://xtrape-com.github.io/xtrape-capsule-site/agents/node-embedded-agent)
|
|
8
|
+
|
|
9
|
+
`@xtrape/capsule-agent-node` embeds an Opstage Agent inside a Node.js service.
|
|
10
|
+
The Agent registers with Opstage CE, persists its Agent token, sends heartbeats,
|
|
11
|
+
reports service metadata, exposes configs and health, and polls Commands for
|
|
12
|
+
operator-triggered Actions.
|
|
13
|
+
|
|
14
|
+
> **Package status:** Xtrape Capsule is currently in **Public Review** before
|
|
15
|
+
> the `v0.1.0 Public Preview` release. This package is published under the
|
|
16
|
+
> `public-review` dist-tag. APIs, contracts, deployment instructions, and SDK
|
|
17
|
+
> interfaces may still change.
|
|
18
|
+
|
|
19
|
+
## Install
|
|
20
|
+
|
|
21
|
+
During Public Review, install the prerelease package with:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
pnpm add @xtrape/capsule-agent-node@public-review
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
The current Public Review version may change before `v0.1.0`.
|
|
28
|
+
|
|
29
|
+
For this repository itself:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
pnpm install
|
|
33
|
+
pnpm build
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Minimal Example
|
|
37
|
+
|
|
38
|
+
This example matches the current SDK API: create a `CapsuleAgent`, configure
|
|
39
|
+
providers with `.health()` / `.configs()`, register Actions with `.action()`,
|
|
40
|
+
then call `start()`.
|
|
41
|
+
|
|
42
|
+
```ts
|
|
43
|
+
import { CapsuleAgent } from "@xtrape/capsule-agent-node";
|
|
44
|
+
|
|
45
|
+
const agent = new CapsuleAgent({
|
|
46
|
+
backendUrl: process.env.OPSTAGE_BACKEND_URL ?? "http://localhost:8080",
|
|
47
|
+
registrationToken: process.env.OPSTAGE_REGISTRATION_TOKEN,
|
|
48
|
+
tokenStore: { file: "./data/agent-token.txt" },
|
|
49
|
+
service: {
|
|
50
|
+
code: "hello-capsule",
|
|
51
|
+
name: "Hello Capsule",
|
|
52
|
+
version: "0.1.0",
|
|
53
|
+
runtime: "nodejs",
|
|
54
|
+
description: "Minimal Capsule Service example",
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
agent.health(() => ({
|
|
59
|
+
status: "UP",
|
|
60
|
+
message: "ok",
|
|
61
|
+
details: {
|
|
62
|
+
uptimeSeconds: Math.floor(process.uptime()),
|
|
63
|
+
},
|
|
64
|
+
}));
|
|
65
|
+
|
|
66
|
+
agent.configs(() => [
|
|
67
|
+
{
|
|
68
|
+
key: "HELLO_MODE",
|
|
69
|
+
label: "Hello mode",
|
|
70
|
+
type: "string",
|
|
71
|
+
editable: false,
|
|
72
|
+
sensitive: false,
|
|
73
|
+
valuePreview: process.env.HELLO_MODE ?? "default",
|
|
74
|
+
},
|
|
75
|
+
]);
|
|
76
|
+
|
|
77
|
+
agent.action({
|
|
78
|
+
name: "echo",
|
|
79
|
+
label: "Echo",
|
|
80
|
+
description: "Return the submitted message.",
|
|
81
|
+
dangerLevel: "LOW",
|
|
82
|
+
requiresConfirmation: false,
|
|
83
|
+
inputSchema: {
|
|
84
|
+
type: "object",
|
|
85
|
+
required: ["message"],
|
|
86
|
+
properties: {
|
|
87
|
+
message: { type: "string", default: "hello" },
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
prepare: () => ({
|
|
91
|
+
initialPayload: { message: "hello" },
|
|
92
|
+
currentState: { service: "ready" },
|
|
93
|
+
}),
|
|
94
|
+
handler: async (payload) => ({
|
|
95
|
+
success: true,
|
|
96
|
+
data: { echo: payload.message },
|
|
97
|
+
}),
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
await agent.start();
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## How Registration Works
|
|
104
|
+
|
|
105
|
+
1. An operator creates a single-use Registration Token in Opstage CE.
|
|
106
|
+
2. The service starts with `registrationToken` and calls the Agent registration
|
|
107
|
+
API.
|
|
108
|
+
3. Opstage returns an Agent ID and Agent token.
|
|
109
|
+
4. The SDK stores the issued credentials in `tokenStore.file` as `<agentId>:<agentToken>`.
|
|
110
|
+
5. Future restarts reuse the stored Agent token; the Registration Token is not
|
|
111
|
+
needed again.
|
|
112
|
+
|
|
113
|
+
If the token file is lost or the Agent is revoked, create a new Registration
|
|
114
|
+
Token and restart the service with it.
|
|
115
|
+
|
|
116
|
+
## Service Manifest
|
|
117
|
+
|
|
118
|
+
The `service` option describes the Capsule Service reported to Opstage:
|
|
119
|
+
|
|
120
|
+
```ts
|
|
121
|
+
service: {
|
|
122
|
+
code: "hello-capsule", // stable unique service code
|
|
123
|
+
name: "Hello Capsule", // operator-facing display name
|
|
124
|
+
description: "...", // optional
|
|
125
|
+
version: "0.1.0", // service version
|
|
126
|
+
runtime: "nodejs", // nodejs | java | python | go | other
|
|
127
|
+
manifest: { labels: { team: "ai" } }, // optional passthrough metadata
|
|
128
|
+
}
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
The SDK wraps this as a `CapsuleService` manifest with `kind: "CapsuleService"`,
|
|
132
|
+
`schemaVersion: "1.0"`, and `agentMode: "embedded"`.
|
|
133
|
+
|
|
134
|
+
## Health Reporting
|
|
135
|
+
|
|
136
|
+
Use `.health(provider)` to report protocol-level health:
|
|
137
|
+
|
|
138
|
+
```ts
|
|
139
|
+
agent.health(async () => ({
|
|
140
|
+
status: "UP", // UP | DEGRADED | DOWN | UNKNOWN
|
|
141
|
+
message: "queue healthy",
|
|
142
|
+
details: { queueDepth: 0 },
|
|
143
|
+
}));
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
The provider runs for heartbeats and service reports. Do not include secrets in
|
|
147
|
+
`details`.
|
|
148
|
+
|
|
149
|
+
Agent health providers return protocol-level `HealthStatus` values: `UP`,
|
|
150
|
+
`DEGRADED`, `DOWN`, `UNKNOWN`.
|
|
151
|
+
|
|
152
|
+
Opstage may derive an operator-facing `effectiveStatus`: `HEALTHY`, `UNHEALTHY`,
|
|
153
|
+
`STALE`, `OFFLINE`.
|
|
154
|
+
|
|
155
|
+
## Config Reporting
|
|
156
|
+
|
|
157
|
+
Use `.configs(provider)` to report observed config metadata:
|
|
158
|
+
|
|
159
|
+
```ts
|
|
160
|
+
agent.configs(() => [
|
|
161
|
+
{
|
|
162
|
+
key: "UPSTREAM_BASE_URL",
|
|
163
|
+
type: "string",
|
|
164
|
+
sensitive: false,
|
|
165
|
+
editable: false,
|
|
166
|
+
valuePreview: process.env.UPSTREAM_BASE_URL,
|
|
167
|
+
},
|
|
168
|
+
{
|
|
169
|
+
key: "UPSTREAM_API_KEY",
|
|
170
|
+
type: "secret",
|
|
171
|
+
sensitive: true,
|
|
172
|
+
editable: false,
|
|
173
|
+
valuePreview: "[REDACTED]",
|
|
174
|
+
secretRef: "env://UPSTREAM_API_KEY",
|
|
175
|
+
},
|
|
176
|
+
]);
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
Configs are reported to Opstage for visibility; the current CE flow does not
|
|
180
|
+
push config values from Opstage into the service.
|
|
181
|
+
|
|
182
|
+
## Actions
|
|
183
|
+
|
|
184
|
+
Use `.action()` to expose an operator-triggerable operation:
|
|
185
|
+
|
|
186
|
+
```ts
|
|
187
|
+
agent.action({
|
|
188
|
+
name: "reload-cache",
|
|
189
|
+
label: "Reload Cache",
|
|
190
|
+
dangerLevel: "MEDIUM",
|
|
191
|
+
requiresConfirmation: true,
|
|
192
|
+
timeoutSeconds: 30,
|
|
193
|
+
handler: async () => {
|
|
194
|
+
await reloadCache();
|
|
195
|
+
return { success: true, message: "Cache reloaded." };
|
|
196
|
+
},
|
|
197
|
+
});
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
Action metadata is published in the service report. The SDK intentionally strips
|
|
201
|
+
runtime-only handler functions before reporting the Action Catalog.
|
|
202
|
+
|
|
203
|
+
## Action Prepare / Execute
|
|
204
|
+
|
|
205
|
+
Opstage uses two Command types for actions:
|
|
206
|
+
|
|
207
|
+
1. `ACTION_PREPARE` — created when the UI opens an Action panel. The SDK calls
|
|
208
|
+
`prepare()` if present and returns dynamic form metadata such as
|
|
209
|
+
`inputSchema`, `initialPayload`, and `currentState`.
|
|
210
|
+
2. `ACTION_EXECUTE` — created when the operator confirms the Action. The SDK
|
|
211
|
+
calls `handler(payload)` and reports the result.
|
|
212
|
+
|
|
213
|
+
If an Action has no `prepare` handler, the SDK returns a default prepare payload
|
|
214
|
+
based on the action metadata and `inputSchema` defaults.
|
|
215
|
+
|
|
216
|
+
## Command Polling
|
|
217
|
+
|
|
218
|
+
The embedded Agent starts three loops by default:
|
|
219
|
+
|
|
220
|
+
| Loop | Default | Purpose |
|
|
221
|
+
| -------------- | ---------: | ---------------------------------- |
|
|
222
|
+
| Heartbeat | 30 seconds | Agent liveness and latest health |
|
|
223
|
+
| Service report | 60 seconds | Manifest, configs, actions, health |
|
|
224
|
+
| Command poll | 5 seconds | Fetch and execute pending Commands |
|
|
225
|
+
|
|
226
|
+
You can override intervals:
|
|
227
|
+
|
|
228
|
+
```ts
|
|
229
|
+
new CapsuleAgent({
|
|
230
|
+
// ...
|
|
231
|
+
intervals: {
|
|
232
|
+
heartbeatMs: 30_000,
|
|
233
|
+
serviceReportMs: 60_000,
|
|
234
|
+
commandPollMs: 5_000,
|
|
235
|
+
},
|
|
236
|
+
});
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
For tests, set `autoStartLoops: false` and call `start()` to perform one
|
|
240
|
+
registration/report/heartbeat/poll cycle.
|
|
241
|
+
|
|
242
|
+
## Token Storage
|
|
243
|
+
|
|
244
|
+
The default token store is file-based via `FileTokenStore`. Store the token file
|
|
245
|
+
in a private data directory:
|
|
246
|
+
|
|
247
|
+
```ts
|
|
248
|
+
new CapsuleAgent({
|
|
249
|
+
// ...
|
|
250
|
+
tokenStore: { file: "./data/agent-token.txt" },
|
|
251
|
+
});
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
Security recommendations:
|
|
255
|
+
|
|
256
|
+
- chmod the containing directory so only the service user can read it;
|
|
257
|
+
- never commit token files;
|
|
258
|
+
- rotate by revoking the Agent in Opstage and registering a new one;
|
|
259
|
+
- prefer secret managers for production wrappers when available.
|
|
260
|
+
|
|
261
|
+
## Security Notes
|
|
262
|
+
|
|
263
|
+
- Registration Tokens are single-use bootstrap credentials.
|
|
264
|
+
- Agent Tokens are long-lived bearer tokens; treat them as secrets.
|
|
265
|
+
- Actions are remote operational capabilities. Use `dangerLevel`,
|
|
266
|
+
`requiresConfirmation`, and server-side validation in handlers.
|
|
267
|
+
- Do not report raw passwords, API keys, cookies, OTPs, browser storage, or
|
|
268
|
+
session files through health/config/action results.
|
|
269
|
+
- Logs are redacted by the SDK where possible, but service handlers remain
|
|
270
|
+
responsible for avoiding secret leakage.
|
|
271
|
+
|
|
272
|
+
## API Reference
|
|
273
|
+
|
|
274
|
+
Main exports:
|
|
275
|
+
|
|
276
|
+
- `CapsuleAgent`
|
|
277
|
+
- `FileTokenStore`
|
|
278
|
+
- `AgentApiClient`
|
|
279
|
+
- `AgentApiError`
|
|
280
|
+
- SDK option and provider types from `types.ts`
|
|
281
|
+
|
|
282
|
+
Core methods:
|
|
283
|
+
|
|
284
|
+
| Method | Description |
|
|
285
|
+
| --------------------------- | ------------------------------------------------------------- |
|
|
286
|
+
| `new CapsuleAgent(options)` | Creates the embedded Agent. |
|
|
287
|
+
| `.health(provider)` | Registers a health provider. |
|
|
288
|
+
| `.configs(provider)` | Registers a config provider. |
|
|
289
|
+
| `.action(action)` | Registers an operator Action. |
|
|
290
|
+
| `.start()` | Registers if needed, reports service state, and starts loops. |
|
|
291
|
+
| `.stop()` | Stops background loops. |
|
|
292
|
+
| `.runHealth()` | Runs the configured health provider. |
|
|
293
|
+
|
|
294
|
+
## Version Compatibility
|
|
295
|
+
|
|
296
|
+
| Package | Compatible with |
|
|
297
|
+
| ---------------------------------- | ------------------------------------------------------------- |
|
|
298
|
+
| `@xtrape/capsule-agent-node@0.1.x` | Opstage CE `0.1.x` and `@xtrape/capsule-contracts-node@0.1.x` |
|
|
299
|
+
|
|
300
|
+
Use matching minor versions across CE, Agent SDK, and Contracts during Public
|
|
301
|
+
Review and Public Preview. The wire protocol may still change before `v1.0`.
|
|
302
|
+
|
|
303
|
+
## Documentation
|
|
304
|
+
|
|
305
|
+
- Site: https://xtrape-com.github.io/xtrape-capsule-site/
|
|
306
|
+
- Node embedded Agent guide: https://xtrape-com.github.io/xtrape-capsule-site/agents/node-embedded-agent
|
|
307
|
+
- Action model: https://xtrape-com.github.io/xtrape-capsule-site/agents/action-model
|
|
308
|
+
- Opstage CE: https://xtrape-com.github.io/xtrape-capsule-site/opstage-ce/overview
|
|
309
|
+
|
|
310
|
+
## Contributing
|
|
311
|
+
|
|
312
|
+
See [CONTRIBUTING.md](./CONTRIBUTING.md) for development workflow and PR checks.
|
|
313
|
+
See [SECURITY.md](./SECURITY.md) for vulnerability reporting and token/action
|
|
314
|
+
safety guidance.
|
|
315
|
+
|
|
316
|
+
## License
|
|
317
|
+
|
|
318
|
+
Apache-2.0. "Xtrape", "Xtrape Capsule", and "Opstage" are trademarks of their
|
|
319
|
+
respective owners; the open-source license does not grant trademark rights.
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
AgentApiClient: () => AgentApiClient,
|
|
24
|
+
AgentApiError: () => AgentApiError,
|
|
25
|
+
CapsuleAgent: () => CapsuleAgent,
|
|
26
|
+
FileTokenStore: () => FileTokenStore
|
|
27
|
+
});
|
|
28
|
+
module.exports = __toCommonJS(index_exports);
|
|
29
|
+
|
|
30
|
+
// src/client/errors.ts
|
|
31
|
+
var AgentApiError = class extends Error {
|
|
32
|
+
constructor(status, message, body) {
|
|
33
|
+
super(message);
|
|
34
|
+
this.status = status;
|
|
35
|
+
this.body = body;
|
|
36
|
+
}
|
|
37
|
+
status;
|
|
38
|
+
body;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
// src/client/agent-api-client.ts
|
|
42
|
+
var AgentApiClient = class {
|
|
43
|
+
constructor(backendUrl) {
|
|
44
|
+
this.backendUrl = backendUrl;
|
|
45
|
+
}
|
|
46
|
+
backendUrl;
|
|
47
|
+
url(path) {
|
|
48
|
+
return new URL(path, this.backendUrl).toString();
|
|
49
|
+
}
|
|
50
|
+
async request(path, init = {}) {
|
|
51
|
+
const headers = new Headers(init.headers);
|
|
52
|
+
headers.set("content-type", "application/json");
|
|
53
|
+
if (init.token) headers.set("authorization", `Bearer ${init.token}`);
|
|
54
|
+
const res = await fetch(this.url(path), { ...init, headers });
|
|
55
|
+
const text = await res.text();
|
|
56
|
+
const body = text ? JSON.parse(text) : void 0;
|
|
57
|
+
if (!res.ok) throw new AgentApiError(res.status, body?.error?.message ?? res.statusText, body);
|
|
58
|
+
return body?.data ?? body;
|
|
59
|
+
}
|
|
60
|
+
register(req) {
|
|
61
|
+
return this.request("/api/agents/register", { method: "POST", body: JSON.stringify(req) });
|
|
62
|
+
}
|
|
63
|
+
heartbeat(agentId, token, req) {
|
|
64
|
+
return this.request(`/api/agents/${agentId}/heartbeat`, { method: "POST", token, body: JSON.stringify(req) });
|
|
65
|
+
}
|
|
66
|
+
reportServices(agentId, token, req) {
|
|
67
|
+
return this.request(`/api/agents/${agentId}/services/report`, { method: "POST", token, body: JSON.stringify(req) });
|
|
68
|
+
}
|
|
69
|
+
pollCommands(agentId, token) {
|
|
70
|
+
return this.request(`/api/agents/${agentId}/commands`, { method: "GET", token });
|
|
71
|
+
}
|
|
72
|
+
reportResult(agentId, token, commandId, req) {
|
|
73
|
+
return this.request(`/api/agents/${agentId}/commands/${commandId}/result`, { method: "POST", token, body: JSON.stringify(req) });
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
// src/token-store/file-token-store.ts
|
|
78
|
+
var import_promises = require("fs/promises");
|
|
79
|
+
var import_node_path = require("path");
|
|
80
|
+
var FileTokenStore = class {
|
|
81
|
+
constructor(filePath = "./data/agent-token.json") {
|
|
82
|
+
this.filePath = filePath;
|
|
83
|
+
}
|
|
84
|
+
filePath;
|
|
85
|
+
async load() {
|
|
86
|
+
try {
|
|
87
|
+
const parsed = JSON.parse(await (0, import_promises.readFile)(this.filePath, "utf8"));
|
|
88
|
+
if (typeof parsed.agentId === "string" && typeof parsed.agentToken === "string") return `${parsed.agentId}:${parsed.agentToken}`;
|
|
89
|
+
return typeof parsed.agentToken === "string" ? parsed.agentToken : null;
|
|
90
|
+
} catch (e) {
|
|
91
|
+
if (e?.code === "ENOENT") return null;
|
|
92
|
+
throw e;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
async save(token) {
|
|
96
|
+
const [agentId, agentToken] = token.includes(":") ? token.split(":", 2) : [void 0, token];
|
|
97
|
+
await (0, import_promises.mkdir)((0, import_node_path.dirname)(this.filePath), { recursive: true });
|
|
98
|
+
await (0, import_promises.writeFile)(this.filePath, JSON.stringify({ ...agentId ? { agentId } : {}, agentToken, savedAt: (/* @__PURE__ */ new Date()).toISOString() }, null, 2), { mode: 384 });
|
|
99
|
+
try {
|
|
100
|
+
await (0, import_promises.chmod)(this.filePath, 384);
|
|
101
|
+
} catch {
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
async clear() {
|
|
105
|
+
await (0, import_promises.rm)(this.filePath, { force: true });
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
// src/security/redaction.ts
|
|
110
|
+
function redact(value) {
|
|
111
|
+
if (typeof value === "string") return value.replace(/opstage_(reg|agent)_[A-Za-z0-9_-]+/g, "opstage_$1_[REDACTED]");
|
|
112
|
+
if (!value || typeof value !== "object") return value;
|
|
113
|
+
if (Array.isArray(value)) return value.map(redact);
|
|
114
|
+
return Object.fromEntries(Object.entries(value).map(([k, v]) => [/token|secret|password|authorization/i.test(k) ? [k, "[REDACTED]"] : [k, redact(v)]]));
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// src/capsule-agent.ts
|
|
118
|
+
var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
119
|
+
var CapsuleAgent = class {
|
|
120
|
+
constructor(options) {
|
|
121
|
+
this.options = options;
|
|
122
|
+
this.client = new AgentApiClient(options.backendUrl);
|
|
123
|
+
this.store = new FileTokenStore(options.tokenStore?.file);
|
|
124
|
+
this.logger = options.logger ?? console;
|
|
125
|
+
}
|
|
126
|
+
options;
|
|
127
|
+
client;
|
|
128
|
+
store;
|
|
129
|
+
logger;
|
|
130
|
+
agentId;
|
|
131
|
+
token;
|
|
132
|
+
stopped = true;
|
|
133
|
+
timers = [];
|
|
134
|
+
healthProvider = () => ({ status: "UNKNOWN" });
|
|
135
|
+
configProvider = () => [];
|
|
136
|
+
actions = /* @__PURE__ */ new Map();
|
|
137
|
+
health(provider) {
|
|
138
|
+
this.healthProvider = provider;
|
|
139
|
+
return this;
|
|
140
|
+
}
|
|
141
|
+
configs(provider) {
|
|
142
|
+
this.configProvider = provider;
|
|
143
|
+
return this;
|
|
144
|
+
}
|
|
145
|
+
action(action) {
|
|
146
|
+
this.actions.set(action.name, action);
|
|
147
|
+
return this;
|
|
148
|
+
}
|
|
149
|
+
async start() {
|
|
150
|
+
this.stopped = false;
|
|
151
|
+
try {
|
|
152
|
+
await this.ensureRegistered();
|
|
153
|
+
await this.reportService();
|
|
154
|
+
await this.heartbeat();
|
|
155
|
+
await this.pollOnce();
|
|
156
|
+
} catch (e) {
|
|
157
|
+
this.log("error", "agent start failed", e);
|
|
158
|
+
if (this.options.failOnStartError) throw e;
|
|
159
|
+
}
|
|
160
|
+
if (this.options.autoStartLoops !== false) {
|
|
161
|
+
this.loop("heartbeat", this.options.intervals?.heartbeatMs ?? (this.options.heartbeatIntervalSeconds ?? 30) * 1e3, () => this.heartbeat());
|
|
162
|
+
this.loop("service-report", this.options.intervals?.serviceReportMs ?? 6e4, () => this.reportService());
|
|
163
|
+
this.loop("command-poll", this.options.intervals?.commandPollMs ?? (this.options.commandPollIntervalSeconds ?? 5) * 1e3, () => this.pollOnce());
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
async stop() {
|
|
167
|
+
this.stopped = true;
|
|
168
|
+
for (const t of this.timers) clearInterval(t);
|
|
169
|
+
this.timers = [];
|
|
170
|
+
}
|
|
171
|
+
loop(name, ms, fn) {
|
|
172
|
+
const t = setInterval(async () => {
|
|
173
|
+
if (this.stopped) return;
|
|
174
|
+
try {
|
|
175
|
+
await fn();
|
|
176
|
+
} catch (e) {
|
|
177
|
+
this.log("warn", `${name} failed`, e);
|
|
178
|
+
}
|
|
179
|
+
}, ms);
|
|
180
|
+
this.timers.push(t);
|
|
181
|
+
}
|
|
182
|
+
async ensureRegistered() {
|
|
183
|
+
const stored = await this.store.load();
|
|
184
|
+
if (stored) {
|
|
185
|
+
const [agentId, token] = stored.includes(":") ? stored.split(":", 2) : [void 0, stored];
|
|
186
|
+
this.agentId = agentId;
|
|
187
|
+
this.token = token;
|
|
188
|
+
if (this.agentId) return;
|
|
189
|
+
}
|
|
190
|
+
if (!this.options.registrationToken) throw new Error("OPSTAGE registration token is required for first registration");
|
|
191
|
+
const agent = this.options.agent ?? { code: this.options.service.code, name: this.options.service.name, runtime: this.options.service.runtime };
|
|
192
|
+
const res = await this.retry(() => this.client.register({ registrationToken: this.options.registrationToken, agent: { code: agent.code, name: agent.name, mode: "embedded", runtime: agent.runtime ?? "nodejs" }, service: this.serviceSnapshot() }));
|
|
193
|
+
this.agentId = res.agentId;
|
|
194
|
+
this.token = res.agentToken;
|
|
195
|
+
await this.store.save(`${this.agentId}:${this.token}`);
|
|
196
|
+
this.log("info", `registered agent ${this.agentId}`);
|
|
197
|
+
}
|
|
198
|
+
serviceSnapshot() {
|
|
199
|
+
const manifest = { kind: "CapsuleService", schemaVersion: "1.0", code: this.options.service.code, name: this.options.service.name, description: this.options.service.description, version: this.options.service.version, runtime: this.options.service.runtime, agentMode: "embedded", ...this.options.service.manifest ?? {} };
|
|
200
|
+
return { code: this.options.service.code, name: this.options.service.name, description: this.options.service.description, version: this.options.service.version, runtime: this.options.service.runtime, manifest, actions: [...this.actions.values()].map(({ handler, prepare, inputSchema, outputSchema, ...a }) => a) };
|
|
201
|
+
}
|
|
202
|
+
async runHealth() {
|
|
203
|
+
return this.healthProvider();
|
|
204
|
+
}
|
|
205
|
+
async reportService() {
|
|
206
|
+
if (!this.agentId || !this.token) return;
|
|
207
|
+
const [health, configs] = await Promise.all([this.runHealth(), this.configProvider()]);
|
|
208
|
+
const svc = { ...this.serviceSnapshot(), health, configs };
|
|
209
|
+
await this.retry(() => this.client.reportServices(this.agentId, this.token, { services: [svc] }));
|
|
210
|
+
}
|
|
211
|
+
async heartbeat() {
|
|
212
|
+
if (!this.agentId || !this.token) return;
|
|
213
|
+
const health = await this.runHealth();
|
|
214
|
+
await this.retry(() => this.client.heartbeat(this.agentId, this.token, { health }));
|
|
215
|
+
}
|
|
216
|
+
async pollOnce() {
|
|
217
|
+
if (!this.agentId || !this.token) return;
|
|
218
|
+
const commands = await this.retry(() => this.client.pollCommands(this.agentId, this.token));
|
|
219
|
+
for (const c of commands) await this.execute(c);
|
|
220
|
+
}
|
|
221
|
+
async execute(command) {
|
|
222
|
+
const startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
223
|
+
const action = this.actions.get(command.actionName);
|
|
224
|
+
if (!action) {
|
|
225
|
+
await this.client.reportResult(this.agentId, this.token, command.id, { success: false, message: `No handler registered for action ${command.actionName}`, error: { code: "ACTION_HANDLER_NOT_FOUND" }, startedAt, finishedAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
try {
|
|
229
|
+
if (command.type === "ACTION_PREPARE") {
|
|
230
|
+
const out2 = action.prepare ? await Promise.resolve(action.prepare()) : this.defaultPrepare(action);
|
|
231
|
+
await this.client.reportResult(this.agentId, this.token, command.id, { success: true, data: out2, startedAt, finishedAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
const out = await this.withTimeout(Promise.resolve(action.handler(command.payload ?? {})), action.timeoutSeconds);
|
|
235
|
+
const result = out;
|
|
236
|
+
const success = result.success !== false;
|
|
237
|
+
await this.client.reportResult(this.agentId, this.token, command.id, { success, message: result.message, data: result.data ?? (success ? out : void 0), error: result.error, startedAt, finishedAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
238
|
+
} catch (e) {
|
|
239
|
+
await this.client.reportResult(this.agentId, this.token, command.id, { success: false, message: e?.message ?? "Action failed", error: { code: "ACTION_FAILED", message: e?.message ?? String(e) }, startedAt, finishedAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
defaultPrepare(action) {
|
|
243
|
+
const { handler: _handler, prepare: _prepare, ...meta } = action;
|
|
244
|
+
return { action: meta, initialPayload: this.initialPayloadFromSchema(action.inputSchema), currentState: {} };
|
|
245
|
+
}
|
|
246
|
+
initialPayloadFromSchema(schema) {
|
|
247
|
+
const props = schema && typeof schema === "object" && !Array.isArray(schema) ? schema.properties : void 0;
|
|
248
|
+
if (!props || typeof props !== "object" || Array.isArray(props)) return {};
|
|
249
|
+
return Object.fromEntries(Object.entries(props).map(([k, m]) => {
|
|
250
|
+
if (m.default !== void 0) return [k, m.default];
|
|
251
|
+
if (m.type === "number" || m.type === "integer") return [k, 0];
|
|
252
|
+
if (m.type === "boolean") return [k, false];
|
|
253
|
+
if (m.type === "array") return [k, []];
|
|
254
|
+
if (m.type === "object") return [k, {}];
|
|
255
|
+
return [k, ""];
|
|
256
|
+
}));
|
|
257
|
+
}
|
|
258
|
+
async withTimeout(p, seconds) {
|
|
259
|
+
if (!seconds) return p;
|
|
260
|
+
let t;
|
|
261
|
+
const timeout = new Promise((_, rej) => {
|
|
262
|
+
t = setTimeout(() => rej(new Error("Action timed out")), seconds * 1e3);
|
|
263
|
+
});
|
|
264
|
+
try {
|
|
265
|
+
return await Promise.race([p, timeout]);
|
|
266
|
+
} finally {
|
|
267
|
+
clearTimeout(t);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
async retry(fn, attempts = 5) {
|
|
271
|
+
let last;
|
|
272
|
+
for (let i = 0; i < attempts; i++) {
|
|
273
|
+
try {
|
|
274
|
+
return await fn();
|
|
275
|
+
} catch (e) {
|
|
276
|
+
last = e;
|
|
277
|
+
if (e instanceof AgentApiError && e.status < 500) throw e;
|
|
278
|
+
await sleep(Math.min(1e3 * 2 ** i, 15e3));
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
throw last;
|
|
282
|
+
}
|
|
283
|
+
log(level, msg, obj) {
|
|
284
|
+
this.logger[level](msg, obj ? redact(obj) : void 0);
|
|
285
|
+
}
|
|
286
|
+
};
|
|
287
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
288
|
+
0 && (module.exports = {
|
|
289
|
+
AgentApiClient,
|
|
290
|
+
AgentApiError,
|
|
291
|
+
CapsuleAgent,
|
|
292
|
+
FileTokenStore
|
|
293
|
+
});
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { HealthReportInput, ConfigItemInput, ActionDefinitionInput, ReportedService, RegisterAgentRequest, AgentHeartbeatRequest, ServiceReportRequest, ReportCommandResultRequest } from '@xtrape/capsule-contracts-node';
|
|
2
|
+
|
|
3
|
+
type AgentMode = "embedded";
|
|
4
|
+
type ActionHandler = (payload: Record<string, unknown>) => Promise<{
|
|
5
|
+
success?: boolean;
|
|
6
|
+
message?: string;
|
|
7
|
+
data?: Record<string, unknown>;
|
|
8
|
+
error?: Record<string, unknown>;
|
|
9
|
+
} | Record<string, unknown>>;
|
|
10
|
+
type ActionPrepareHandler = () => Promise<{
|
|
11
|
+
action?: Record<string, unknown>;
|
|
12
|
+
initialPayload?: Record<string, unknown>;
|
|
13
|
+
currentState?: Record<string, unknown>;
|
|
14
|
+
} | Record<string, unknown>> | {
|
|
15
|
+
action?: Record<string, unknown>;
|
|
16
|
+
initialPayload?: Record<string, unknown>;
|
|
17
|
+
currentState?: Record<string, unknown>;
|
|
18
|
+
} | Record<string, unknown>;
|
|
19
|
+
interface CapsuleAgentOptions {
|
|
20
|
+
backendUrl: string;
|
|
21
|
+
registrationToken?: string;
|
|
22
|
+
tokenStore?: {
|
|
23
|
+
file?: string;
|
|
24
|
+
};
|
|
25
|
+
agent?: {
|
|
26
|
+
code: string;
|
|
27
|
+
name?: string;
|
|
28
|
+
runtime?: string;
|
|
29
|
+
};
|
|
30
|
+
service: {
|
|
31
|
+
code: string;
|
|
32
|
+
name: string;
|
|
33
|
+
description?: string;
|
|
34
|
+
version: string;
|
|
35
|
+
runtime: "nodejs" | "java" | "python" | "go" | "other";
|
|
36
|
+
manifest?: Record<string, unknown>;
|
|
37
|
+
};
|
|
38
|
+
intervals?: {
|
|
39
|
+
heartbeatMs?: number;
|
|
40
|
+
commandPollMs?: number;
|
|
41
|
+
serviceReportMs?: number;
|
|
42
|
+
};
|
|
43
|
+
heartbeatIntervalSeconds?: number;
|
|
44
|
+
commandPollIntervalSeconds?: number;
|
|
45
|
+
autoStartLoops?: boolean;
|
|
46
|
+
logger?: Pick<Console, "debug" | "info" | "warn" | "error">;
|
|
47
|
+
failOnStartError?: boolean;
|
|
48
|
+
}
|
|
49
|
+
type HealthProvider = () => Promise<HealthReportInput> | HealthReportInput;
|
|
50
|
+
type ConfigProvider = () => Promise<ConfigItemInput[]> | ConfigItemInput[];
|
|
51
|
+
interface RegisteredAction extends ActionDefinitionInput {
|
|
52
|
+
handler: ActionHandler;
|
|
53
|
+
prepare?: ActionPrepareHandler;
|
|
54
|
+
}
|
|
55
|
+
interface TokenStore {
|
|
56
|
+
load(): Promise<string | null>;
|
|
57
|
+
save(token: string): Promise<void>;
|
|
58
|
+
clear(): Promise<void>;
|
|
59
|
+
}
|
|
60
|
+
type ServiceSnapshot = ReportedService;
|
|
61
|
+
|
|
62
|
+
declare class CapsuleAgent {
|
|
63
|
+
private readonly options;
|
|
64
|
+
private readonly client;
|
|
65
|
+
private readonly store;
|
|
66
|
+
private readonly logger;
|
|
67
|
+
private agentId?;
|
|
68
|
+
private token?;
|
|
69
|
+
private stopped;
|
|
70
|
+
private timers;
|
|
71
|
+
private healthProvider;
|
|
72
|
+
private configProvider;
|
|
73
|
+
private actions;
|
|
74
|
+
constructor(options: CapsuleAgentOptions);
|
|
75
|
+
health(provider: HealthProvider): this;
|
|
76
|
+
configs(provider: ConfigProvider): this;
|
|
77
|
+
action(action: RegisteredAction): this;
|
|
78
|
+
start(): Promise<void>;
|
|
79
|
+
stop(): Promise<void>;
|
|
80
|
+
private loop;
|
|
81
|
+
private ensureRegistered;
|
|
82
|
+
private serviceSnapshot;
|
|
83
|
+
runHealth(): Promise<HealthReportInput>;
|
|
84
|
+
private reportService;
|
|
85
|
+
private heartbeat;
|
|
86
|
+
private pollOnce;
|
|
87
|
+
private execute;
|
|
88
|
+
private defaultPrepare;
|
|
89
|
+
private initialPayloadFromSchema;
|
|
90
|
+
private withTimeout;
|
|
91
|
+
private retry;
|
|
92
|
+
private log;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
declare class FileTokenStore implements TokenStore {
|
|
96
|
+
private readonly filePath;
|
|
97
|
+
constructor(filePath?: string);
|
|
98
|
+
load(): Promise<string | null>;
|
|
99
|
+
save(token: string): Promise<void>;
|
|
100
|
+
clear(): Promise<void>;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
declare class AgentApiClient {
|
|
104
|
+
private readonly backendUrl;
|
|
105
|
+
constructor(backendUrl: string);
|
|
106
|
+
private url;
|
|
107
|
+
private request;
|
|
108
|
+
register(req: RegisterAgentRequest): Promise<{
|
|
109
|
+
agentId: string;
|
|
110
|
+
agentToken: string;
|
|
111
|
+
heartbeatIntervalSeconds: number;
|
|
112
|
+
commandPollIntervalSeconds: number;
|
|
113
|
+
}>;
|
|
114
|
+
heartbeat(agentId: string, token: string, req: AgentHeartbeatRequest): Promise<unknown>;
|
|
115
|
+
reportServices(agentId: string, token: string, req: ServiceReportRequest): Promise<unknown>;
|
|
116
|
+
pollCommands(agentId: string, token: string): Promise<{
|
|
117
|
+
type: "ACTION_PREPARE" | "ACTION_EXECUTE" | "ACTION";
|
|
118
|
+
id: string;
|
|
119
|
+
createdAt: string;
|
|
120
|
+
status: "PENDING" | "RUNNING" | "SUCCEEDED" | "FAILED" | "EXPIRED" | "CANCELLED";
|
|
121
|
+
agentId: string;
|
|
122
|
+
serviceId: string;
|
|
123
|
+
actionName: string;
|
|
124
|
+
expiresAt?: string | null | undefined;
|
|
125
|
+
payload?: Record<string, any> | undefined;
|
|
126
|
+
createdByUserId?: string | null | undefined;
|
|
127
|
+
startedAt?: string | null | undefined;
|
|
128
|
+
completedAt?: string | null | undefined;
|
|
129
|
+
}[]>;
|
|
130
|
+
reportResult(agentId: string, token: string, commandId: string, req: ReportCommandResultRequest): Promise<unknown>;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
declare class AgentApiError extends Error {
|
|
134
|
+
readonly status: number;
|
|
135
|
+
readonly body?: unknown | undefined;
|
|
136
|
+
constructor(status: number, message: string, body?: unknown | undefined);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export { type ActionHandler, type ActionPrepareHandler, AgentApiClient, AgentApiError, type AgentMode, CapsuleAgent, type CapsuleAgentOptions, type ConfigProvider, FileTokenStore, type HealthProvider, type RegisteredAction, type ServiceSnapshot, type TokenStore };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { HealthReportInput, ConfigItemInput, ActionDefinitionInput, ReportedService, RegisterAgentRequest, AgentHeartbeatRequest, ServiceReportRequest, ReportCommandResultRequest } from '@xtrape/capsule-contracts-node';
|
|
2
|
+
|
|
3
|
+
type AgentMode = "embedded";
|
|
4
|
+
type ActionHandler = (payload: Record<string, unknown>) => Promise<{
|
|
5
|
+
success?: boolean;
|
|
6
|
+
message?: string;
|
|
7
|
+
data?: Record<string, unknown>;
|
|
8
|
+
error?: Record<string, unknown>;
|
|
9
|
+
} | Record<string, unknown>>;
|
|
10
|
+
type ActionPrepareHandler = () => Promise<{
|
|
11
|
+
action?: Record<string, unknown>;
|
|
12
|
+
initialPayload?: Record<string, unknown>;
|
|
13
|
+
currentState?: Record<string, unknown>;
|
|
14
|
+
} | Record<string, unknown>> | {
|
|
15
|
+
action?: Record<string, unknown>;
|
|
16
|
+
initialPayload?: Record<string, unknown>;
|
|
17
|
+
currentState?: Record<string, unknown>;
|
|
18
|
+
} | Record<string, unknown>;
|
|
19
|
+
interface CapsuleAgentOptions {
|
|
20
|
+
backendUrl: string;
|
|
21
|
+
registrationToken?: string;
|
|
22
|
+
tokenStore?: {
|
|
23
|
+
file?: string;
|
|
24
|
+
};
|
|
25
|
+
agent?: {
|
|
26
|
+
code: string;
|
|
27
|
+
name?: string;
|
|
28
|
+
runtime?: string;
|
|
29
|
+
};
|
|
30
|
+
service: {
|
|
31
|
+
code: string;
|
|
32
|
+
name: string;
|
|
33
|
+
description?: string;
|
|
34
|
+
version: string;
|
|
35
|
+
runtime: "nodejs" | "java" | "python" | "go" | "other";
|
|
36
|
+
manifest?: Record<string, unknown>;
|
|
37
|
+
};
|
|
38
|
+
intervals?: {
|
|
39
|
+
heartbeatMs?: number;
|
|
40
|
+
commandPollMs?: number;
|
|
41
|
+
serviceReportMs?: number;
|
|
42
|
+
};
|
|
43
|
+
heartbeatIntervalSeconds?: number;
|
|
44
|
+
commandPollIntervalSeconds?: number;
|
|
45
|
+
autoStartLoops?: boolean;
|
|
46
|
+
logger?: Pick<Console, "debug" | "info" | "warn" | "error">;
|
|
47
|
+
failOnStartError?: boolean;
|
|
48
|
+
}
|
|
49
|
+
type HealthProvider = () => Promise<HealthReportInput> | HealthReportInput;
|
|
50
|
+
type ConfigProvider = () => Promise<ConfigItemInput[]> | ConfigItemInput[];
|
|
51
|
+
interface RegisteredAction extends ActionDefinitionInput {
|
|
52
|
+
handler: ActionHandler;
|
|
53
|
+
prepare?: ActionPrepareHandler;
|
|
54
|
+
}
|
|
55
|
+
interface TokenStore {
|
|
56
|
+
load(): Promise<string | null>;
|
|
57
|
+
save(token: string): Promise<void>;
|
|
58
|
+
clear(): Promise<void>;
|
|
59
|
+
}
|
|
60
|
+
type ServiceSnapshot = ReportedService;
|
|
61
|
+
|
|
62
|
+
declare class CapsuleAgent {
|
|
63
|
+
private readonly options;
|
|
64
|
+
private readonly client;
|
|
65
|
+
private readonly store;
|
|
66
|
+
private readonly logger;
|
|
67
|
+
private agentId?;
|
|
68
|
+
private token?;
|
|
69
|
+
private stopped;
|
|
70
|
+
private timers;
|
|
71
|
+
private healthProvider;
|
|
72
|
+
private configProvider;
|
|
73
|
+
private actions;
|
|
74
|
+
constructor(options: CapsuleAgentOptions);
|
|
75
|
+
health(provider: HealthProvider): this;
|
|
76
|
+
configs(provider: ConfigProvider): this;
|
|
77
|
+
action(action: RegisteredAction): this;
|
|
78
|
+
start(): Promise<void>;
|
|
79
|
+
stop(): Promise<void>;
|
|
80
|
+
private loop;
|
|
81
|
+
private ensureRegistered;
|
|
82
|
+
private serviceSnapshot;
|
|
83
|
+
runHealth(): Promise<HealthReportInput>;
|
|
84
|
+
private reportService;
|
|
85
|
+
private heartbeat;
|
|
86
|
+
private pollOnce;
|
|
87
|
+
private execute;
|
|
88
|
+
private defaultPrepare;
|
|
89
|
+
private initialPayloadFromSchema;
|
|
90
|
+
private withTimeout;
|
|
91
|
+
private retry;
|
|
92
|
+
private log;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
declare class FileTokenStore implements TokenStore {
|
|
96
|
+
private readonly filePath;
|
|
97
|
+
constructor(filePath?: string);
|
|
98
|
+
load(): Promise<string | null>;
|
|
99
|
+
save(token: string): Promise<void>;
|
|
100
|
+
clear(): Promise<void>;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
declare class AgentApiClient {
|
|
104
|
+
private readonly backendUrl;
|
|
105
|
+
constructor(backendUrl: string);
|
|
106
|
+
private url;
|
|
107
|
+
private request;
|
|
108
|
+
register(req: RegisterAgentRequest): Promise<{
|
|
109
|
+
agentId: string;
|
|
110
|
+
agentToken: string;
|
|
111
|
+
heartbeatIntervalSeconds: number;
|
|
112
|
+
commandPollIntervalSeconds: number;
|
|
113
|
+
}>;
|
|
114
|
+
heartbeat(agentId: string, token: string, req: AgentHeartbeatRequest): Promise<unknown>;
|
|
115
|
+
reportServices(agentId: string, token: string, req: ServiceReportRequest): Promise<unknown>;
|
|
116
|
+
pollCommands(agentId: string, token: string): Promise<{
|
|
117
|
+
type: "ACTION_PREPARE" | "ACTION_EXECUTE" | "ACTION";
|
|
118
|
+
id: string;
|
|
119
|
+
createdAt: string;
|
|
120
|
+
status: "PENDING" | "RUNNING" | "SUCCEEDED" | "FAILED" | "EXPIRED" | "CANCELLED";
|
|
121
|
+
agentId: string;
|
|
122
|
+
serviceId: string;
|
|
123
|
+
actionName: string;
|
|
124
|
+
expiresAt?: string | null | undefined;
|
|
125
|
+
payload?: Record<string, any> | undefined;
|
|
126
|
+
createdByUserId?: string | null | undefined;
|
|
127
|
+
startedAt?: string | null | undefined;
|
|
128
|
+
completedAt?: string | null | undefined;
|
|
129
|
+
}[]>;
|
|
130
|
+
reportResult(agentId: string, token: string, commandId: string, req: ReportCommandResultRequest): Promise<unknown>;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
declare class AgentApiError extends Error {
|
|
134
|
+
readonly status: number;
|
|
135
|
+
readonly body?: unknown | undefined;
|
|
136
|
+
constructor(status: number, message: string, body?: unknown | undefined);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export { type ActionHandler, type ActionPrepareHandler, AgentApiClient, AgentApiError, type AgentMode, CapsuleAgent, type CapsuleAgentOptions, type ConfigProvider, FileTokenStore, type HealthProvider, type RegisteredAction, type ServiceSnapshot, type TokenStore };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
// src/client/errors.ts
|
|
2
|
+
var AgentApiError = class extends Error {
|
|
3
|
+
constructor(status, message, body) {
|
|
4
|
+
super(message);
|
|
5
|
+
this.status = status;
|
|
6
|
+
this.body = body;
|
|
7
|
+
}
|
|
8
|
+
status;
|
|
9
|
+
body;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
// src/client/agent-api-client.ts
|
|
13
|
+
var AgentApiClient = class {
|
|
14
|
+
constructor(backendUrl) {
|
|
15
|
+
this.backendUrl = backendUrl;
|
|
16
|
+
}
|
|
17
|
+
backendUrl;
|
|
18
|
+
url(path) {
|
|
19
|
+
return new URL(path, this.backendUrl).toString();
|
|
20
|
+
}
|
|
21
|
+
async request(path, init = {}) {
|
|
22
|
+
const headers = new Headers(init.headers);
|
|
23
|
+
headers.set("content-type", "application/json");
|
|
24
|
+
if (init.token) headers.set("authorization", `Bearer ${init.token}`);
|
|
25
|
+
const res = await fetch(this.url(path), { ...init, headers });
|
|
26
|
+
const text = await res.text();
|
|
27
|
+
const body = text ? JSON.parse(text) : void 0;
|
|
28
|
+
if (!res.ok) throw new AgentApiError(res.status, body?.error?.message ?? res.statusText, body);
|
|
29
|
+
return body?.data ?? body;
|
|
30
|
+
}
|
|
31
|
+
register(req) {
|
|
32
|
+
return this.request("/api/agents/register", { method: "POST", body: JSON.stringify(req) });
|
|
33
|
+
}
|
|
34
|
+
heartbeat(agentId, token, req) {
|
|
35
|
+
return this.request(`/api/agents/${agentId}/heartbeat`, { method: "POST", token, body: JSON.stringify(req) });
|
|
36
|
+
}
|
|
37
|
+
reportServices(agentId, token, req) {
|
|
38
|
+
return this.request(`/api/agents/${agentId}/services/report`, { method: "POST", token, body: JSON.stringify(req) });
|
|
39
|
+
}
|
|
40
|
+
pollCommands(agentId, token) {
|
|
41
|
+
return this.request(`/api/agents/${agentId}/commands`, { method: "GET", token });
|
|
42
|
+
}
|
|
43
|
+
reportResult(agentId, token, commandId, req) {
|
|
44
|
+
return this.request(`/api/agents/${agentId}/commands/${commandId}/result`, { method: "POST", token, body: JSON.stringify(req) });
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
// src/token-store/file-token-store.ts
|
|
49
|
+
import { mkdir, readFile, rm, writeFile, chmod } from "fs/promises";
|
|
50
|
+
import { dirname } from "path";
|
|
51
|
+
var FileTokenStore = class {
|
|
52
|
+
constructor(filePath = "./data/agent-token.json") {
|
|
53
|
+
this.filePath = filePath;
|
|
54
|
+
}
|
|
55
|
+
filePath;
|
|
56
|
+
async load() {
|
|
57
|
+
try {
|
|
58
|
+
const parsed = JSON.parse(await readFile(this.filePath, "utf8"));
|
|
59
|
+
if (typeof parsed.agentId === "string" && typeof parsed.agentToken === "string") return `${parsed.agentId}:${parsed.agentToken}`;
|
|
60
|
+
return typeof parsed.agentToken === "string" ? parsed.agentToken : null;
|
|
61
|
+
} catch (e) {
|
|
62
|
+
if (e?.code === "ENOENT") return null;
|
|
63
|
+
throw e;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
async save(token) {
|
|
67
|
+
const [agentId, agentToken] = token.includes(":") ? token.split(":", 2) : [void 0, token];
|
|
68
|
+
await mkdir(dirname(this.filePath), { recursive: true });
|
|
69
|
+
await writeFile(this.filePath, JSON.stringify({ ...agentId ? { agentId } : {}, agentToken, savedAt: (/* @__PURE__ */ new Date()).toISOString() }, null, 2), { mode: 384 });
|
|
70
|
+
try {
|
|
71
|
+
await chmod(this.filePath, 384);
|
|
72
|
+
} catch {
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
async clear() {
|
|
76
|
+
await rm(this.filePath, { force: true });
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
// src/security/redaction.ts
|
|
81
|
+
function redact(value) {
|
|
82
|
+
if (typeof value === "string") return value.replace(/opstage_(reg|agent)_[A-Za-z0-9_-]+/g, "opstage_$1_[REDACTED]");
|
|
83
|
+
if (!value || typeof value !== "object") return value;
|
|
84
|
+
if (Array.isArray(value)) return value.map(redact);
|
|
85
|
+
return Object.fromEntries(Object.entries(value).map(([k, v]) => [/token|secret|password|authorization/i.test(k) ? [k, "[REDACTED]"] : [k, redact(v)]]));
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// src/capsule-agent.ts
|
|
89
|
+
var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
90
|
+
var CapsuleAgent = class {
|
|
91
|
+
constructor(options) {
|
|
92
|
+
this.options = options;
|
|
93
|
+
this.client = new AgentApiClient(options.backendUrl);
|
|
94
|
+
this.store = new FileTokenStore(options.tokenStore?.file);
|
|
95
|
+
this.logger = options.logger ?? console;
|
|
96
|
+
}
|
|
97
|
+
options;
|
|
98
|
+
client;
|
|
99
|
+
store;
|
|
100
|
+
logger;
|
|
101
|
+
agentId;
|
|
102
|
+
token;
|
|
103
|
+
stopped = true;
|
|
104
|
+
timers = [];
|
|
105
|
+
healthProvider = () => ({ status: "UNKNOWN" });
|
|
106
|
+
configProvider = () => [];
|
|
107
|
+
actions = /* @__PURE__ */ new Map();
|
|
108
|
+
health(provider) {
|
|
109
|
+
this.healthProvider = provider;
|
|
110
|
+
return this;
|
|
111
|
+
}
|
|
112
|
+
configs(provider) {
|
|
113
|
+
this.configProvider = provider;
|
|
114
|
+
return this;
|
|
115
|
+
}
|
|
116
|
+
action(action) {
|
|
117
|
+
this.actions.set(action.name, action);
|
|
118
|
+
return this;
|
|
119
|
+
}
|
|
120
|
+
async start() {
|
|
121
|
+
this.stopped = false;
|
|
122
|
+
try {
|
|
123
|
+
await this.ensureRegistered();
|
|
124
|
+
await this.reportService();
|
|
125
|
+
await this.heartbeat();
|
|
126
|
+
await this.pollOnce();
|
|
127
|
+
} catch (e) {
|
|
128
|
+
this.log("error", "agent start failed", e);
|
|
129
|
+
if (this.options.failOnStartError) throw e;
|
|
130
|
+
}
|
|
131
|
+
if (this.options.autoStartLoops !== false) {
|
|
132
|
+
this.loop("heartbeat", this.options.intervals?.heartbeatMs ?? (this.options.heartbeatIntervalSeconds ?? 30) * 1e3, () => this.heartbeat());
|
|
133
|
+
this.loop("service-report", this.options.intervals?.serviceReportMs ?? 6e4, () => this.reportService());
|
|
134
|
+
this.loop("command-poll", this.options.intervals?.commandPollMs ?? (this.options.commandPollIntervalSeconds ?? 5) * 1e3, () => this.pollOnce());
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
async stop() {
|
|
138
|
+
this.stopped = true;
|
|
139
|
+
for (const t of this.timers) clearInterval(t);
|
|
140
|
+
this.timers = [];
|
|
141
|
+
}
|
|
142
|
+
loop(name, ms, fn) {
|
|
143
|
+
const t = setInterval(async () => {
|
|
144
|
+
if (this.stopped) return;
|
|
145
|
+
try {
|
|
146
|
+
await fn();
|
|
147
|
+
} catch (e) {
|
|
148
|
+
this.log("warn", `${name} failed`, e);
|
|
149
|
+
}
|
|
150
|
+
}, ms);
|
|
151
|
+
this.timers.push(t);
|
|
152
|
+
}
|
|
153
|
+
async ensureRegistered() {
|
|
154
|
+
const stored = await this.store.load();
|
|
155
|
+
if (stored) {
|
|
156
|
+
const [agentId, token] = stored.includes(":") ? stored.split(":", 2) : [void 0, stored];
|
|
157
|
+
this.agentId = agentId;
|
|
158
|
+
this.token = token;
|
|
159
|
+
if (this.agentId) return;
|
|
160
|
+
}
|
|
161
|
+
if (!this.options.registrationToken) throw new Error("OPSTAGE registration token is required for first registration");
|
|
162
|
+
const agent = this.options.agent ?? { code: this.options.service.code, name: this.options.service.name, runtime: this.options.service.runtime };
|
|
163
|
+
const res = await this.retry(() => this.client.register({ registrationToken: this.options.registrationToken, agent: { code: agent.code, name: agent.name, mode: "embedded", runtime: agent.runtime ?? "nodejs" }, service: this.serviceSnapshot() }));
|
|
164
|
+
this.agentId = res.agentId;
|
|
165
|
+
this.token = res.agentToken;
|
|
166
|
+
await this.store.save(`${this.agentId}:${this.token}`);
|
|
167
|
+
this.log("info", `registered agent ${this.agentId}`);
|
|
168
|
+
}
|
|
169
|
+
serviceSnapshot() {
|
|
170
|
+
const manifest = { kind: "CapsuleService", schemaVersion: "1.0", code: this.options.service.code, name: this.options.service.name, description: this.options.service.description, version: this.options.service.version, runtime: this.options.service.runtime, agentMode: "embedded", ...this.options.service.manifest ?? {} };
|
|
171
|
+
return { code: this.options.service.code, name: this.options.service.name, description: this.options.service.description, version: this.options.service.version, runtime: this.options.service.runtime, manifest, actions: [...this.actions.values()].map(({ handler, prepare, inputSchema, outputSchema, ...a }) => a) };
|
|
172
|
+
}
|
|
173
|
+
async runHealth() {
|
|
174
|
+
return this.healthProvider();
|
|
175
|
+
}
|
|
176
|
+
async reportService() {
|
|
177
|
+
if (!this.agentId || !this.token) return;
|
|
178
|
+
const [health, configs] = await Promise.all([this.runHealth(), this.configProvider()]);
|
|
179
|
+
const svc = { ...this.serviceSnapshot(), health, configs };
|
|
180
|
+
await this.retry(() => this.client.reportServices(this.agentId, this.token, { services: [svc] }));
|
|
181
|
+
}
|
|
182
|
+
async heartbeat() {
|
|
183
|
+
if (!this.agentId || !this.token) return;
|
|
184
|
+
const health = await this.runHealth();
|
|
185
|
+
await this.retry(() => this.client.heartbeat(this.agentId, this.token, { health }));
|
|
186
|
+
}
|
|
187
|
+
async pollOnce() {
|
|
188
|
+
if (!this.agentId || !this.token) return;
|
|
189
|
+
const commands = await this.retry(() => this.client.pollCommands(this.agentId, this.token));
|
|
190
|
+
for (const c of commands) await this.execute(c);
|
|
191
|
+
}
|
|
192
|
+
async execute(command) {
|
|
193
|
+
const startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
194
|
+
const action = this.actions.get(command.actionName);
|
|
195
|
+
if (!action) {
|
|
196
|
+
await this.client.reportResult(this.agentId, this.token, command.id, { success: false, message: `No handler registered for action ${command.actionName}`, error: { code: "ACTION_HANDLER_NOT_FOUND" }, startedAt, finishedAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
try {
|
|
200
|
+
if (command.type === "ACTION_PREPARE") {
|
|
201
|
+
const out2 = action.prepare ? await Promise.resolve(action.prepare()) : this.defaultPrepare(action);
|
|
202
|
+
await this.client.reportResult(this.agentId, this.token, command.id, { success: true, data: out2, startedAt, finishedAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
const out = await this.withTimeout(Promise.resolve(action.handler(command.payload ?? {})), action.timeoutSeconds);
|
|
206
|
+
const result = out;
|
|
207
|
+
const success = result.success !== false;
|
|
208
|
+
await this.client.reportResult(this.agentId, this.token, command.id, { success, message: result.message, data: result.data ?? (success ? out : void 0), error: result.error, startedAt, finishedAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
209
|
+
} catch (e) {
|
|
210
|
+
await this.client.reportResult(this.agentId, this.token, command.id, { success: false, message: e?.message ?? "Action failed", error: { code: "ACTION_FAILED", message: e?.message ?? String(e) }, startedAt, finishedAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
defaultPrepare(action) {
|
|
214
|
+
const { handler: _handler, prepare: _prepare, ...meta } = action;
|
|
215
|
+
return { action: meta, initialPayload: this.initialPayloadFromSchema(action.inputSchema), currentState: {} };
|
|
216
|
+
}
|
|
217
|
+
initialPayloadFromSchema(schema) {
|
|
218
|
+
const props = schema && typeof schema === "object" && !Array.isArray(schema) ? schema.properties : void 0;
|
|
219
|
+
if (!props || typeof props !== "object" || Array.isArray(props)) return {};
|
|
220
|
+
return Object.fromEntries(Object.entries(props).map(([k, m]) => {
|
|
221
|
+
if (m.default !== void 0) return [k, m.default];
|
|
222
|
+
if (m.type === "number" || m.type === "integer") return [k, 0];
|
|
223
|
+
if (m.type === "boolean") return [k, false];
|
|
224
|
+
if (m.type === "array") return [k, []];
|
|
225
|
+
if (m.type === "object") return [k, {}];
|
|
226
|
+
return [k, ""];
|
|
227
|
+
}));
|
|
228
|
+
}
|
|
229
|
+
async withTimeout(p, seconds) {
|
|
230
|
+
if (!seconds) return p;
|
|
231
|
+
let t;
|
|
232
|
+
const timeout = new Promise((_, rej) => {
|
|
233
|
+
t = setTimeout(() => rej(new Error("Action timed out")), seconds * 1e3);
|
|
234
|
+
});
|
|
235
|
+
try {
|
|
236
|
+
return await Promise.race([p, timeout]);
|
|
237
|
+
} finally {
|
|
238
|
+
clearTimeout(t);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
async retry(fn, attempts = 5) {
|
|
242
|
+
let last;
|
|
243
|
+
for (let i = 0; i < attempts; i++) {
|
|
244
|
+
try {
|
|
245
|
+
return await fn();
|
|
246
|
+
} catch (e) {
|
|
247
|
+
last = e;
|
|
248
|
+
if (e instanceof AgentApiError && e.status < 500) throw e;
|
|
249
|
+
await sleep(Math.min(1e3 * 2 ** i, 15e3));
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
throw last;
|
|
253
|
+
}
|
|
254
|
+
log(level, msg, obj) {
|
|
255
|
+
this.logger[level](msg, obj ? redact(obj) : void 0);
|
|
256
|
+
}
|
|
257
|
+
};
|
|
258
|
+
export {
|
|
259
|
+
AgentApiClient,
|
|
260
|
+
AgentApiError,
|
|
261
|
+
CapsuleAgent,
|
|
262
|
+
FileTokenStore
|
|
263
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@xtrape/capsule-agent-node",
|
|
3
|
+
"version": "0.1.0-public-review.0",
|
|
4
|
+
"description": "Node.js Agent SDK for embedding Capsule Services into the Opstage governance loop.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"xtrape",
|
|
7
|
+
"capsule",
|
|
8
|
+
"opstage",
|
|
9
|
+
"agent-sdk",
|
|
10
|
+
"automation",
|
|
11
|
+
"control-plane"
|
|
12
|
+
],
|
|
13
|
+
"repository": {
|
|
14
|
+
"type": "git",
|
|
15
|
+
"url": "git+https://github.com/xtrape-com/xtrape-capsule-agent-node.git"
|
|
16
|
+
},
|
|
17
|
+
"homepage": "https://github.com/xtrape-com/xtrape-capsule-agent-node#readme",
|
|
18
|
+
"bugs": {
|
|
19
|
+
"url": "https://github.com/xtrape-com/xtrape-capsule-agent-node/issues"
|
|
20
|
+
},
|
|
21
|
+
"type": "module",
|
|
22
|
+
"main": "./dist/index.cjs",
|
|
23
|
+
"module": "./dist/index.js",
|
|
24
|
+
"types": "./dist/index.d.ts",
|
|
25
|
+
"exports": {
|
|
26
|
+
".": {
|
|
27
|
+
"types": "./dist/index.d.ts",
|
|
28
|
+
"import": "./dist/index.js",
|
|
29
|
+
"require": "./dist/index.cjs"
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
"files": [
|
|
33
|
+
"dist",
|
|
34
|
+
"README.md",
|
|
35
|
+
"LICENSE",
|
|
36
|
+
"NOTICE"
|
|
37
|
+
],
|
|
38
|
+
"scripts": {
|
|
39
|
+
"build": "tsup src/index.ts --format esm,cjs --dts",
|
|
40
|
+
"prepack": "pnpm build",
|
|
41
|
+
"test": "vitest run",
|
|
42
|
+
"typecheck": "tsc --noEmit",
|
|
43
|
+
"lint": "tsc --noEmit"
|
|
44
|
+
},
|
|
45
|
+
"dependencies": {
|
|
46
|
+
"@xtrape/capsule-contracts-node": "^0.1.0-public-review.0"
|
|
47
|
+
},
|
|
48
|
+
"devDependencies": {
|
|
49
|
+
"tsup": "^8.3.5",
|
|
50
|
+
"typescript": "^5.6.3",
|
|
51
|
+
"vitest": "^2.1.9",
|
|
52
|
+
"@types/node": "^22.10.2"
|
|
53
|
+
},
|
|
54
|
+
"engines": {
|
|
55
|
+
"node": ">=20"
|
|
56
|
+
},
|
|
57
|
+
"publishConfig": {
|
|
58
|
+
"registry": "https://registry.npmjs.org/",
|
|
59
|
+
"access": "public"
|
|
60
|
+
},
|
|
61
|
+
"license": "Apache-2.0"
|
|
62
|
+
}
|