@unicity-astrid/sdk 0.1.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/README.md +120 -0
- package/dist/approval.d.ts +23 -0
- package/dist/approval.d.ts.map +1 -0
- package/dist/approval.js +29 -0
- package/dist/approval.js.map +1 -0
- package/dist/capabilities.d.ts +14 -0
- package/dist/capabilities.d.ts.map +1 -0
- package/dist/capabilities.js +19 -0
- package/dist/capabilities.js.map +1 -0
- package/dist/capsule.d.ts +39 -0
- package/dist/capsule.d.ts.map +1 -0
- package/dist/capsule.js +67 -0
- package/dist/capsule.js.map +1 -0
- package/dist/contracts.d.ts +1104 -0
- package/dist/contracts.d.ts.map +1 -0
- package/dist/contracts.js +4 -0
- package/dist/contracts.js.map +1 -0
- package/dist/elicit.d.ts +30 -0
- package/dist/elicit.d.ts.map +1 -0
- package/dist/elicit.js +103 -0
- package/dist/elicit.js.map +1 -0
- package/dist/env.d.ts +19 -0
- package/dist/env.d.ts.map +1 -0
- package/dist/env.js +27 -0
- package/dist/env.js.map +1 -0
- package/dist/errors.d.ts +46 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +108 -0
- package/dist/errors.js.map +1 -0
- package/dist/fs.d.ts +135 -0
- package/dist/fs.d.ts.map +1 -0
- package/dist/fs.js +257 -0
- package/dist/fs.js.map +1 -0
- package/dist/http.d.ts +90 -0
- package/dist/http.d.ts.map +1 -0
- package/dist/http.js +276 -0
- package/dist/http.js.map +1 -0
- package/dist/identity.d.ts +46 -0
- package/dist/identity.d.ts.map +1 -0
- package/dist/identity.js +69 -0
- package/dist/identity.js.map +1 -0
- package/dist/index.d.ts +30 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +27 -0
- package/dist/index.js.map +1 -0
- package/dist/interceptors.d.ts +21 -0
- package/dist/interceptors.d.ts.map +1 -0
- package/dist/interceptors.js +22 -0
- package/dist/interceptors.js.map +1 -0
- package/dist/ipc.d.ts +143 -0
- package/dist/ipc.d.ts.map +1 -0
- package/dist/ipc.js +261 -0
- package/dist/ipc.js.map +1 -0
- package/dist/kv.d.ts +45 -0
- package/dist/kv.d.ts.map +1 -0
- package/dist/kv.js +91 -0
- package/dist/kv.js.map +1 -0
- package/dist/log.d.ts +17 -0
- package/dist/log.d.ts.map +1 -0
- package/dist/log.js +40 -0
- package/dist/log.js.map +1 -0
- package/dist/net.d.ts +154 -0
- package/dist/net.d.ts.map +1 -0
- package/dist/net.js +421 -0
- package/dist/net.js.map +1 -0
- package/dist/process.d.ts +77 -0
- package/dist/process.d.ts.map +1 -0
- package/dist/process.js +128 -0
- package/dist/process.js.map +1 -0
- package/dist/runtime/bridge.d.ts +34 -0
- package/dist/runtime/bridge.d.ts.map +1 -0
- package/dist/runtime/bridge.js +326 -0
- package/dist/runtime/bridge.js.map +1 -0
- package/dist/runtime/index.d.ts +3 -0
- package/dist/runtime/index.d.ts.map +1 -0
- package/dist/runtime/index.js +3 -0
- package/dist/runtime/index.js.map +1 -0
- package/dist/runtime/registry.d.ts +58 -0
- package/dist/runtime/registry.d.ts.map +1 -0
- package/dist/runtime/registry.js +129 -0
- package/dist/runtime/registry.js.map +1 -0
- package/dist/runtime.d.ts +36 -0
- package/dist/runtime.d.ts.map +1 -0
- package/dist/runtime.js +50 -0
- package/dist/runtime.js.map +1 -0
- package/dist/time.d.ts +29 -0
- package/dist/time.d.ts.map +1 -0
- package/dist/time.js +43 -0
- package/dist/time.js.map +1 -0
- package/dist/tool.d.ts +48 -0
- package/dist/tool.d.ts.map +1 -0
- package/dist/tool.js +86 -0
- package/dist/tool.js.map +1 -0
- package/dist/uplink.d.ts +27 -0
- package/dist/uplink.d.ts.map +1 -0
- package/dist/uplink.js +36 -0
- package/dist/uplink.js.map +1 -0
- package/package.json +38 -0
- package/src/approval.ts +38 -0
- package/src/capabilities.ts +22 -0
- package/src/capsule.ts +90 -0
- package/src/contracts.ts +1189 -0
- package/src/elicit.ts +136 -0
- package/src/env.ts +31 -0
- package/src/errors.ts +122 -0
- package/src/fs.ts +357 -0
- package/src/http.ts +345 -0
- package/src/identity.ts +101 -0
- package/src/index.ts +83 -0
- package/src/interceptors.ts +25 -0
- package/src/ipc.ts +354 -0
- package/src/kv.ts +123 -0
- package/src/log.ts +43 -0
- package/src/net.ts +545 -0
- package/src/process.ts +205 -0
- package/src/runtime/bridge.ts +374 -0
- package/src/runtime/index.ts +11 -0
- package/src/runtime/registry.ts +178 -0
- package/src/runtime.ts +70 -0
- package/src/time.ts +48 -0
- package/src/tool.ts +125 -0
- package/src/uplink.ts +49 -0
- package/src/wit-imports.d.ts +689 -0
- package/wit-contracts/astrid-contracts.wit +1266 -0
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Module-scoped registry populated by the @capsule, @tool, @install,
|
|
3
|
+
* @upgrade, @interceptor, @command, @run decorators. The bridge reads from
|
|
4
|
+
* this registry to dispatch host export calls.
|
|
5
|
+
*
|
|
6
|
+
* Decorators run during class evaluation, which happens at module init.
|
|
7
|
+
* The bridge is constructed AFTER all decorators have populated the
|
|
8
|
+
* registry, so reads are safe.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export type CapsuleConstructor = new () => object;
|
|
12
|
+
|
|
13
|
+
export interface ToolEntry {
|
|
14
|
+
/** Name exposed to the LLM / kernel — matches `tool_execute_<name>` action. */
|
|
15
|
+
name: string;
|
|
16
|
+
/** TS method name on the class. */
|
|
17
|
+
methodName: string;
|
|
18
|
+
/** If true, the bridge loads state before and saves after on success. */
|
|
19
|
+
mutable: boolean;
|
|
20
|
+
/** Human-readable description from decorator or TSDoc. */
|
|
21
|
+
description: string | undefined;
|
|
22
|
+
/** JSON Schema for the tool's input. Built-time codegen fills this in. */
|
|
23
|
+
inputSchema: Record<string, unknown> | undefined;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface InterceptorEntry {
|
|
27
|
+
/** Topic pattern the interceptor reacts to (also the hook action name). */
|
|
28
|
+
topic: string;
|
|
29
|
+
methodName: string;
|
|
30
|
+
mutable: boolean;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface CommandEntry {
|
|
34
|
+
/** Command name (= hook action name, registered alongside interceptors). */
|
|
35
|
+
name: string;
|
|
36
|
+
methodName: string;
|
|
37
|
+
mutable: boolean;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface CapsuleRegistration {
|
|
41
|
+
ctor: CapsuleConstructor;
|
|
42
|
+
tools: Map<string, ToolEntry>;
|
|
43
|
+
interceptors: Map<string, InterceptorEntry>;
|
|
44
|
+
commands: Map<string, CommandEntry>;
|
|
45
|
+
installMethod: string | undefined;
|
|
46
|
+
upgradeMethod: string | undefined;
|
|
47
|
+
runMethod: string | undefined;
|
|
48
|
+
description: string | undefined;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
let registration: CapsuleRegistration | undefined;
|
|
52
|
+
|
|
53
|
+
function newRegistration(ctor: CapsuleConstructor, description: string | undefined): CapsuleRegistration {
|
|
54
|
+
return {
|
|
55
|
+
ctor,
|
|
56
|
+
tools: new Map(),
|
|
57
|
+
interceptors: new Map(),
|
|
58
|
+
commands: new Map(),
|
|
59
|
+
installMethod: undefined,
|
|
60
|
+
upgradeMethod: undefined,
|
|
61
|
+
runMethod: undefined,
|
|
62
|
+
description,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function registerCapsule(ctor: CapsuleConstructor, description?: string): void {
|
|
67
|
+
if (registration !== undefined && registration.ctor !== ctor) {
|
|
68
|
+
throw new Error(
|
|
69
|
+
`Only one @capsule class may be registered per WASM module. ` +
|
|
70
|
+
`Already have ${registration.ctor.name}; refusing to register ${ctor.name}.`,
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
if (registration === undefined) {
|
|
74
|
+
registration = newRegistration(ctor, description);
|
|
75
|
+
} else if (description !== undefined && registration.description === undefined) {
|
|
76
|
+
registration.description = description;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Decorators may run before `@capsule` if class fields appear before the
|
|
82
|
+
* class itself in source. Buffer pending entries on the constructor so the
|
|
83
|
+
* eventual @capsule call picks them up.
|
|
84
|
+
*/
|
|
85
|
+
const pendingByCtor = new WeakMap<CapsuleConstructor, CapsuleRegistration>();
|
|
86
|
+
|
|
87
|
+
function ensureRegistration(ctor: CapsuleConstructor): CapsuleRegistration {
|
|
88
|
+
if (registration !== undefined && registration.ctor === ctor) {
|
|
89
|
+
return registration;
|
|
90
|
+
}
|
|
91
|
+
let pending = pendingByCtor.get(ctor);
|
|
92
|
+
if (pending === undefined) {
|
|
93
|
+
pending = newRegistration(ctor, undefined);
|
|
94
|
+
pendingByCtor.set(ctor, pending);
|
|
95
|
+
}
|
|
96
|
+
return pending;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Adopt any decorator entries buffered before @capsule fired. */
|
|
100
|
+
export function adoptPending(ctor: CapsuleConstructor): void {
|
|
101
|
+
if (registration === undefined || registration.ctor !== ctor) return;
|
|
102
|
+
const pending = pendingByCtor.get(ctor);
|
|
103
|
+
if (pending === undefined) return;
|
|
104
|
+
for (const [name, entry] of pending.tools) registration.tools.set(name, entry);
|
|
105
|
+
for (const [topic, entry] of pending.interceptors) registration.interceptors.set(topic, entry);
|
|
106
|
+
for (const [name, entry] of pending.commands) registration.commands.set(name, entry);
|
|
107
|
+
if (pending.installMethod !== undefined && registration.installMethod === undefined) {
|
|
108
|
+
registration.installMethod = pending.installMethod;
|
|
109
|
+
}
|
|
110
|
+
if (pending.upgradeMethod !== undefined && registration.upgradeMethod === undefined) {
|
|
111
|
+
registration.upgradeMethod = pending.upgradeMethod;
|
|
112
|
+
}
|
|
113
|
+
if (pending.runMethod !== undefined && registration.runMethod === undefined) {
|
|
114
|
+
registration.runMethod = pending.runMethod;
|
|
115
|
+
}
|
|
116
|
+
if (pending.description !== undefined && registration.description === undefined) {
|
|
117
|
+
registration.description = pending.description;
|
|
118
|
+
}
|
|
119
|
+
pendingByCtor.delete(ctor);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function recordTool(ctor: CapsuleConstructor, entry: ToolEntry): void {
|
|
123
|
+
const target = ensureRegistration(ctor);
|
|
124
|
+
if (target.tools.has(entry.name)) {
|
|
125
|
+
throw new Error(`@tool("${entry.name}") declared twice on ${ctor.name}.`);
|
|
126
|
+
}
|
|
127
|
+
target.tools.set(entry.name, entry);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function recordInterceptor(ctor: CapsuleConstructor, entry: InterceptorEntry): void {
|
|
131
|
+
const target = ensureRegistration(ctor);
|
|
132
|
+
if (target.interceptors.has(entry.topic)) {
|
|
133
|
+
throw new Error(`@interceptor("${entry.topic}") declared twice on ${ctor.name}.`);
|
|
134
|
+
}
|
|
135
|
+
target.interceptors.set(entry.topic, entry);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function recordCommand(ctor: CapsuleConstructor, entry: CommandEntry): void {
|
|
139
|
+
const target = ensureRegistration(ctor);
|
|
140
|
+
if (target.commands.has(entry.name)) {
|
|
141
|
+
throw new Error(`@command("${entry.name}") declared twice on ${ctor.name}.`);
|
|
142
|
+
}
|
|
143
|
+
target.commands.set(entry.name, entry);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export function recordInstall(ctor: CapsuleConstructor, methodName: string): void {
|
|
147
|
+
const target = ensureRegistration(ctor);
|
|
148
|
+
if (target.installMethod !== undefined) {
|
|
149
|
+
throw new Error(`Only one @install method allowed on ${ctor.name}.`);
|
|
150
|
+
}
|
|
151
|
+
target.installMethod = methodName;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export function recordUpgrade(ctor: CapsuleConstructor, methodName: string): void {
|
|
155
|
+
const target = ensureRegistration(ctor);
|
|
156
|
+
if (target.upgradeMethod !== undefined) {
|
|
157
|
+
throw new Error(`Only one @upgrade method allowed on ${ctor.name}.`);
|
|
158
|
+
}
|
|
159
|
+
target.upgradeMethod = methodName;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export function recordRun(ctor: CapsuleConstructor, methodName: string): void {
|
|
163
|
+
const target = ensureRegistration(ctor);
|
|
164
|
+
if (target.runMethod !== undefined) {
|
|
165
|
+
throw new Error(`Only one @run method allowed on ${ctor.name}.`);
|
|
166
|
+
}
|
|
167
|
+
target.runMethod = methodName;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** Returns the registered capsule, or undefined if none has been declared. */
|
|
171
|
+
export function getRegistration(): CapsuleRegistration | undefined {
|
|
172
|
+
return registration;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** Test-only: reset the registry to a clean state. */
|
|
176
|
+
export function __resetRegistry(): void {
|
|
177
|
+
registration = undefined;
|
|
178
|
+
}
|
package/src/runtime.ts
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OS runtime introspection + signaling. Mirrors `astrid_sdk::runtime`.
|
|
3
|
+
*
|
|
4
|
+
* Also surfaces {@link randomBytes} (cryptographically secure host entropy
|
|
5
|
+
* via `sys::random-bytes`) for capsules that need an audit-traced CSPRNG —
|
|
6
|
+
* `globalThis.crypto.getRandomValues` works inside StarlingMonkey but
|
|
7
|
+
* bypasses the host audit trail.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import {
|
|
11
|
+
getCaller as hostGetCaller,
|
|
12
|
+
signalReady as hostSignalReady,
|
|
13
|
+
randomBytes as hostRandomBytes,
|
|
14
|
+
} from "astrid:sys/host@1.0.0";
|
|
15
|
+
import { SysError, callHost } from "./errors.js";
|
|
16
|
+
import { get as getEnv, CONFIG_SOCKET_PATH } from "./env.js";
|
|
17
|
+
|
|
18
|
+
/** Information about the caller that triggered the current execution. */
|
|
19
|
+
export interface CallerContext {
|
|
20
|
+
/** UUID of the capsule that originated the IPC message. */
|
|
21
|
+
sourceId: string;
|
|
22
|
+
/** The acting principal (user ID), if available. */
|
|
23
|
+
principal: string | undefined;
|
|
24
|
+
/** ISO 8601 timestamp of the originating message. */
|
|
25
|
+
timestamp: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Signal that the capsule's run loop is ready. Call after setting up IPC
|
|
30
|
+
* subscriptions inside `@run`; the kernel waits for this before loading
|
|
31
|
+
* dependent capsules.
|
|
32
|
+
*/
|
|
33
|
+
export function signalReady(): void {
|
|
34
|
+
callHost("runtime.signalReady", () => hostSignalReady());
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Caller context for the current invocation. */
|
|
38
|
+
export function caller(): CallerContext {
|
|
39
|
+
const ctx = callHost("runtime.caller", () => hostGetCaller());
|
|
40
|
+
return {
|
|
41
|
+
sourceId: ctx.sourceId,
|
|
42
|
+
principal: ctx.principal,
|
|
43
|
+
timestamp: ctx.timestamp,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Fill `length` bytes with cryptographically secure random data from the
|
|
49
|
+
* host's OS-level CSPRNG. Capped at 4096 bytes per call.
|
|
50
|
+
*/
|
|
51
|
+
export function randomBytes(length: number): Uint8Array {
|
|
52
|
+
return callHost(`runtime.randomBytes(${length})`, () =>
|
|
53
|
+
hostRandomBytes(BigInt(length)),
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Kernel's Unix domain socket path, injected via the well-known
|
|
59
|
+
* `ASTRID_SOCKET_PATH` config key.
|
|
60
|
+
*/
|
|
61
|
+
export function socketPath(): string {
|
|
62
|
+
const path = getEnv(CONFIG_SOCKET_PATH);
|
|
63
|
+
if (path === "") {
|
|
64
|
+
throw SysError.api("ASTRID_SOCKET_PATH config key is empty");
|
|
65
|
+
}
|
|
66
|
+
if (path.indexOf("\0") >= 0) {
|
|
67
|
+
throw SysError.api("ASTRID_SOCKET_PATH contains null byte");
|
|
68
|
+
}
|
|
69
|
+
return path;
|
|
70
|
+
}
|
package/src/time.ts
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wall-clock and monotonic time — mirrors `astrid_sdk::time`.
|
|
3
|
+
*
|
|
4
|
+
* The WASM guest has no direct access to the system clock; all time comes
|
|
5
|
+
* through the `sys` host package. {@link now} / {@link nowMs} return wall
|
|
6
|
+
* clock (subject to NTP adjustments); {@link monotonicNs} returns a host
|
|
7
|
+
* monotonic reading suitable for measuring elapsed time within a single
|
|
8
|
+
* capsule lifetime.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { clockMs, clockMonotonicNs, sleepNs as hostSleepNs } from "astrid:sys/host@1.0.0";
|
|
12
|
+
import { callHost } from "./errors.js";
|
|
13
|
+
|
|
14
|
+
/** Current wall-clock time as a JS `Date`. */
|
|
15
|
+
export function now(): Date {
|
|
16
|
+
const ms = callHost("time.now", () => clockMs());
|
|
17
|
+
return new Date(Number(ms));
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Current wall-clock time as a bigint of milliseconds since UNIX epoch. */
|
|
21
|
+
export function nowMs(): bigint {
|
|
22
|
+
return callHost("time.nowMs", () => clockMs());
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Current monotonic clock reading in nanoseconds. Suitable for measuring
|
|
27
|
+
* elapsed time; the absolute value is meaningless across processes or capsule
|
|
28
|
+
* reloads — only differences are.
|
|
29
|
+
*/
|
|
30
|
+
export function monotonicNs(): bigint {
|
|
31
|
+
return callHost("time.monotonicNs", () => clockMonotonicNs());
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Block the calling task for the given duration in milliseconds.
|
|
36
|
+
*
|
|
37
|
+
* Capped at 60_000 ms (60 s) per call by the host. Throws `cancelled` if
|
|
38
|
+
* the capsule unloads mid-sleep.
|
|
39
|
+
*/
|
|
40
|
+
export function sleepMs(ms: number): void {
|
|
41
|
+
const ns = BigInt(Math.max(0, Math.floor(ms))) * 1_000_000n;
|
|
42
|
+
callHost(`time.sleepMs(${ms})`, () => hostSleepNs(ns));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Block for `ns` nanoseconds. See {@link sleepMs} for the practical wrapper. */
|
|
46
|
+
export function sleepNs(ns: bigint): void {
|
|
47
|
+
callHost(`time.sleepNs(${ns})`, () => hostSleepNs(ns));
|
|
48
|
+
}
|
package/src/tool.ts
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@tool`, `@interceptor`, `@command` method decorators — mirror
|
|
3
|
+
* `#[astrid::tool]`, `#[astrid::interceptor]`, `#[astrid::command]` from the
|
|
4
|
+
* Rust SDK.
|
|
5
|
+
*
|
|
6
|
+
* Unlike Rust, JS has no `&mut self` signal at decoration time, so
|
|
7
|
+
* mutability is opt-in via the options bag. Without `mutable: true`, the
|
|
8
|
+
* bridge will not load or save `__state` around the call.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import {
|
|
12
|
+
type CapsuleConstructor,
|
|
13
|
+
recordTool,
|
|
14
|
+
recordInterceptor,
|
|
15
|
+
recordCommand,
|
|
16
|
+
} from "./runtime/registry.js";
|
|
17
|
+
|
|
18
|
+
export interface ToolOptions {
|
|
19
|
+
/** If true, the bridge auto-loads + auto-persists capsule state via KV `__state`. */
|
|
20
|
+
mutable?: boolean;
|
|
21
|
+
/** Human-readable tool description. If omitted, build-time codegen extracts from TSDoc. */
|
|
22
|
+
description?: string;
|
|
23
|
+
/** Pre-computed JSON Schema. If omitted, build-time codegen derives from TS types. */
|
|
24
|
+
inputSchema?: Record<string, unknown>;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface InterceptorOptions {
|
|
28
|
+
/** Mirror @tool: opt-in state load/save for handlers that mutate `this`. */
|
|
29
|
+
mutable?: boolean;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface CommandOptions {
|
|
33
|
+
mutable?: boolean;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Declares a method as an Astrid tool. The decorator records the tool in
|
|
38
|
+
* the registry; the bridge dispatches `tool_execute_<name>` hook actions
|
|
39
|
+
* to it.
|
|
40
|
+
*/
|
|
41
|
+
export function tool(name: string, options: ToolOptions = {}) {
|
|
42
|
+
requireName("tool", name);
|
|
43
|
+
|
|
44
|
+
return function <This extends object, Args, Result>(
|
|
45
|
+
_value: (this: This, args: Args) => Result | Promise<Result>,
|
|
46
|
+
context: ClassMethodDecoratorContext<This, (this: This, args: Args) => Result | Promise<Result>>,
|
|
47
|
+
): void {
|
|
48
|
+
if (context.private || context.static) {
|
|
49
|
+
throw new Error(`@tool("${name}") must be applied to a public instance method.`);
|
|
50
|
+
}
|
|
51
|
+
context.addInitializer(function () {
|
|
52
|
+
const ctor = (this as object).constructor as CapsuleConstructor;
|
|
53
|
+
recordTool(ctor, {
|
|
54
|
+
name,
|
|
55
|
+
methodName: String(context.name),
|
|
56
|
+
mutable: options.mutable === true,
|
|
57
|
+
description: options.description,
|
|
58
|
+
inputSchema: options.inputSchema,
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Declares a method as an interceptor on a specific IPC topic. The bridge
|
|
66
|
+
* routes hook-trigger actions named exactly `<topic>` to this method.
|
|
67
|
+
*
|
|
68
|
+
* Topic patterns follow IPC subscription rules (exact match or
|
|
69
|
+
* trailing-suffix wildcard). The kernel pre-registers the subscription
|
|
70
|
+
* when the capsule has both `@run` and `@interceptor` — for purely
|
|
71
|
+
* hook-driven (non-runnable) capsules the kernel dispatches directly.
|
|
72
|
+
*/
|
|
73
|
+
export function interceptor(topic: string, options: InterceptorOptions = {}) {
|
|
74
|
+
requireName("interceptor", topic);
|
|
75
|
+
|
|
76
|
+
return function <This extends object, Payload, Result>(
|
|
77
|
+
_value: (this: This, payload: Payload) => Result | Promise<Result>,
|
|
78
|
+
context: ClassMethodDecoratorContext<This, (this: This, payload: Payload) => Result | Promise<Result>>,
|
|
79
|
+
): void {
|
|
80
|
+
if (context.private || context.static) {
|
|
81
|
+
throw new Error(`@interceptor("${topic}") must be applied to a public instance method.`);
|
|
82
|
+
}
|
|
83
|
+
context.addInitializer(function () {
|
|
84
|
+
const ctor = (this as object).constructor as CapsuleConstructor;
|
|
85
|
+
recordInterceptor(ctor, {
|
|
86
|
+
topic,
|
|
87
|
+
methodName: String(context.name),
|
|
88
|
+
mutable: options.mutable === true,
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Declares a method as an RPC-style command. Commands and interceptors
|
|
96
|
+
* share the same dispatch table on the kernel side — the only difference
|
|
97
|
+
* is intent (commands are invoked directly by name, interceptors fire on
|
|
98
|
+
* topic matches).
|
|
99
|
+
*/
|
|
100
|
+
export function command(name: string, options: CommandOptions = {}) {
|
|
101
|
+
requireName("command", name);
|
|
102
|
+
|
|
103
|
+
return function <This extends object, Payload, Result>(
|
|
104
|
+
_value: (this: This, payload: Payload) => Result | Promise<Result>,
|
|
105
|
+
context: ClassMethodDecoratorContext<This, (this: This, payload: Payload) => Result | Promise<Result>>,
|
|
106
|
+
): void {
|
|
107
|
+
if (context.private || context.static) {
|
|
108
|
+
throw new Error(`@command("${name}") must be applied to a public instance method.`);
|
|
109
|
+
}
|
|
110
|
+
context.addInitializer(function () {
|
|
111
|
+
const ctor = (this as object).constructor as CapsuleConstructor;
|
|
112
|
+
recordCommand(ctor, {
|
|
113
|
+
name,
|
|
114
|
+
methodName: String(context.name),
|
|
115
|
+
mutable: options.mutable === true,
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function requireName(kind: string, name: string): void {
|
|
122
|
+
if (typeof name !== "string" || name.length === 0) {
|
|
123
|
+
throw new Error(`@${kind} requires a non-empty name (got ${JSON.stringify(name)})`);
|
|
124
|
+
}
|
|
125
|
+
}
|
package/src/uplink.ts
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Direct frontend messaging. Mirrors `astrid_sdk::uplink`.
|
|
3
|
+
*
|
|
4
|
+
* Capsules register uplinks (named endpoints) for platforms they bridge —
|
|
5
|
+
* Discord, Slack, Telegram, CLI proxy — and then forward inbound user
|
|
6
|
+
* messages to the kernel's processing pipeline.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import {
|
|
10
|
+
uplinkRegister,
|
|
11
|
+
uplinkSend,
|
|
12
|
+
type UplinkProfile,
|
|
13
|
+
} from "astrid:uplink/host@1.0.0";
|
|
14
|
+
import { callHost } from "./errors.js";
|
|
15
|
+
|
|
16
|
+
export type { UplinkProfile } from "astrid:uplink/host@1.0.0";
|
|
17
|
+
|
|
18
|
+
export class UplinkId {
|
|
19
|
+
readonly value: string;
|
|
20
|
+
constructor(value: string) {
|
|
21
|
+
this.value = value;
|
|
22
|
+
}
|
|
23
|
+
toString(): string {
|
|
24
|
+
return this.value;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Register an uplink. `profile` is one of `"chat"` / `"interactive"` /
|
|
30
|
+
* `"notify"` / `"bridge"`. Returns the assigned uplink UUID wrapped in
|
|
31
|
+
* {@link UplinkId}.
|
|
32
|
+
*/
|
|
33
|
+
export function register(name: string, platform: string, profile: UplinkProfile): UplinkId {
|
|
34
|
+
const id = callHost(`uplink.register(${JSON.stringify(name)})`, () =>
|
|
35
|
+
uplinkRegister(name, platform, profile),
|
|
36
|
+
);
|
|
37
|
+
return new UplinkId(id);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Forward an inbound message through a registered uplink. Returns `true` if
|
|
42
|
+
* sent, `false` if intentionally dropped (e.g. no active session for the
|
|
43
|
+
* principal).
|
|
44
|
+
*/
|
|
45
|
+
export function send(uplink: UplinkId, platformUserId: string, content: string): boolean {
|
|
46
|
+
return callHost(`uplink.send(${uplink.value})`, () =>
|
|
47
|
+
uplinkSend(uplink.value, platformUserId, content),
|
|
48
|
+
);
|
|
49
|
+
}
|