@muhkoo/connect 0.3.0-alpha.0 → 0.6.0-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +107 -10
- package/dist/browser/index.js +530 -8
- package/dist/browser/index.js.map +1 -1
- package/dist/connect.d.ts +587 -3
- package/dist/server/index.js +530 -8
- package/dist/server/index.js.map +1 -1
- package/dist/workers/index.js +20 -1
- package/dist/workers/index.js.map +1 -1
- package/package.json +2 -2
package/dist/browser/index.js
CHANGED
|
@@ -2407,6 +2407,7 @@ class WSTransport extends EventCore {
|
|
|
2407
2407
|
return `${this.id}:${event}`;
|
|
2408
2408
|
}
|
|
2409
2409
|
url;
|
|
2410
|
+
urlProvider;
|
|
2410
2411
|
socket = null;
|
|
2411
2412
|
_isConnected = false;
|
|
2412
2413
|
_isConnecting = false;
|
|
@@ -2432,6 +2433,7 @@ class WSTransport extends EventCore {
|
|
|
2432
2433
|
throw new Error("WSTransport: `url` is required");
|
|
2433
2434
|
}
|
|
2434
2435
|
this.url = opts.url;
|
|
2436
|
+
this.urlProvider = opts.urlProvider;
|
|
2435
2437
|
this.id = opts.id ?? `ws${++__wsInstanceSeq}`;
|
|
2436
2438
|
this.autoReconnect = opts.autoReconnect ?? true;
|
|
2437
2439
|
this.reconnectDelay = opts.reconnectDelay ?? 3000;
|
|
@@ -2448,6 +2450,18 @@ class WSTransport extends EventCore {
|
|
|
2448
2450
|
throw new Error("WSTransport: already connected or connecting");
|
|
2449
2451
|
}
|
|
2450
2452
|
this._isConnecting = true;
|
|
2453
|
+
// On a RECONNECT, refresh the URL (the baked-in single-use ticket is spent).
|
|
2454
|
+
// The first connect uses `url` as-is. If the provider throws (e.g. can't
|
|
2455
|
+
// mint a ticket), surface it and let scheduleReconnect retry.
|
|
2456
|
+
if (this.urlProvider && this.reconnectAttempts > 0) {
|
|
2457
|
+
try {
|
|
2458
|
+
this.url = await this.urlProvider();
|
|
2459
|
+
}
|
|
2460
|
+
catch (err) {
|
|
2461
|
+
this._isConnecting = false;
|
|
2462
|
+
throw err;
|
|
2463
|
+
}
|
|
2464
|
+
}
|
|
2451
2465
|
return new Promise((resolve, reject) => {
|
|
2452
2466
|
try {
|
|
2453
2467
|
const socket = new WebSocket(this.url);
|
|
@@ -2620,7 +2634,12 @@ class WSTransport extends EventCore {
|
|
|
2620
2634
|
await this.connect();
|
|
2621
2635
|
}
|
|
2622
2636
|
catch {
|
|
2623
|
-
//
|
|
2637
|
+
// A failed reconnect never opened the socket, so its `onclose` won't
|
|
2638
|
+
// re-schedule (that path is guarded by `wasConnected`). Re-schedule here
|
|
2639
|
+
// so we keep retrying (with a fresh ticket each time) until we succeed
|
|
2640
|
+
// or hit the attempt cap.
|
|
2641
|
+
if (this.autoReconnect)
|
|
2642
|
+
this.scheduleReconnect();
|
|
2624
2643
|
}
|
|
2625
2644
|
}, this.reconnectDelay);
|
|
2626
2645
|
}
|
|
@@ -3936,6 +3955,8 @@ class FileStorage {
|
|
|
3936
3955
|
const fileId = input.fileId ?? generateId();
|
|
3937
3956
|
const createdAt = Date.now();
|
|
3938
3957
|
const chunks = [];
|
|
3958
|
+
const totalChunks = Math.max(1, Math.ceil(data.length / this.chunkSize));
|
|
3959
|
+
input.onProgress?.(0, totalChunks);
|
|
3939
3960
|
// Slicing + encrypting + encoding + uploading is done per-chunk so
|
|
3940
3961
|
// the peak memory footprint stays at ~chunkSize even for huge files.
|
|
3941
3962
|
// Within a chunk, the shard uploads are parallelized up to
|
|
@@ -3945,6 +3966,7 @@ class FileStorage {
|
|
|
3945
3966
|
const end = Math.min(start + this.chunkSize, data.length);
|
|
3946
3967
|
const plaintext = data.subarray(start, end);
|
|
3947
3968
|
chunks.push(await this.writeChunk(plaintext, chunkIndex));
|
|
3969
|
+
input.onProgress?.(chunks.length, totalChunks);
|
|
3948
3970
|
// Stop after one iteration if the file is empty — we still emit a
|
|
3949
3971
|
// single zero-length chunk so the manifest has a stable shape.
|
|
3950
3972
|
if (data.length === 0)
|
|
@@ -4691,6 +4713,9 @@ class HttpClient {
|
|
|
4691
4713
|
post(path, body) {
|
|
4692
4714
|
return this.request("POST", path, body);
|
|
4693
4715
|
}
|
|
4716
|
+
patch(path, body) {
|
|
4717
|
+
return this.request("PATCH", path, body);
|
|
4718
|
+
}
|
|
4694
4719
|
del(path, body) {
|
|
4695
4720
|
return this.request("DELETE", path, body);
|
|
4696
4721
|
}
|
|
@@ -5280,6 +5305,92 @@ class KvNamespace {
|
|
|
5280
5305
|
}
|
|
5281
5306
|
}
|
|
5282
5307
|
|
|
5308
|
+
/**
|
|
5309
|
+
* `client.db` — the app's scalable database. A developer defines tables in the
|
|
5310
|
+
* Muhkoo portal (columns + indexes); each table is backed by its own Durable
|
|
5311
|
+
* Object SQLite instance and reachable through a sanitized REST gateway. This
|
|
5312
|
+
* namespace wraps that gateway:
|
|
5313
|
+
*
|
|
5314
|
+
* const todos = client.db.table('todos');
|
|
5315
|
+
* const { id } = await todos.insert({ title: 'Buy milk', done: false });
|
|
5316
|
+
* const one = await todos.get(id);
|
|
5317
|
+
* const { rows, nextCursor } = await todos.query({
|
|
5318
|
+
* where: [{ column: 'done', op: 'eq', value: false }],
|
|
5319
|
+
* orderBy: { column: 'rank', dir: 'asc' },
|
|
5320
|
+
* limit: 50,
|
|
5321
|
+
* });
|
|
5322
|
+
* await todos.update(id, { done: true });
|
|
5323
|
+
* await todos.delete(id);
|
|
5324
|
+
*
|
|
5325
|
+
* Authorization is the **app key** ({@link HttpClient} stamps it on every
|
|
5326
|
+
* request); the appId + environment are resolved server-side from the key, so a
|
|
5327
|
+
* key can only ever reach its own app's tables. The front end never touches the
|
|
5328
|
+
* database directly — this REST surface is the only gateway, and all input is
|
|
5329
|
+
* validated against the table's schema (parameterized statements, column +
|
|
5330
|
+
* operator allowlists). There is no raw-SQL passthrough.
|
|
5331
|
+
*
|
|
5332
|
+
* Schema is authored in the portal, not here (the SDK reads + writes rows, it
|
|
5333
|
+
* doesn't create tables). Pagination is keyset-based: pass the `nextCursor` from
|
|
5334
|
+
* a previous `query()` back as `cursor` to fetch the next page.
|
|
5335
|
+
*/
|
|
5336
|
+
/** A handle to one table — the ergonomic surface for row operations. */
|
|
5337
|
+
class DbTable {
|
|
5338
|
+
http;
|
|
5339
|
+
name;
|
|
5340
|
+
constructor(http, name) {
|
|
5341
|
+
this.http = http;
|
|
5342
|
+
this.name = name;
|
|
5343
|
+
}
|
|
5344
|
+
base() {
|
|
5345
|
+
return `/api/db/${encodeURIComponent(this.name)}`;
|
|
5346
|
+
}
|
|
5347
|
+
/** Insert a row. Returns the created row and its primary-key value. */
|
|
5348
|
+
async insert(values) {
|
|
5349
|
+
return this.http.post(this.base(), { values });
|
|
5350
|
+
}
|
|
5351
|
+
/** Fetch a row by primary key, or `null` if it doesn't exist. */
|
|
5352
|
+
async get(id) {
|
|
5353
|
+
try {
|
|
5354
|
+
const res = await this.http.get(`${this.base()}/${encodeURIComponent(String(id))}`);
|
|
5355
|
+
return res.row;
|
|
5356
|
+
}
|
|
5357
|
+
catch (e) {
|
|
5358
|
+
if (e?.status === 404)
|
|
5359
|
+
return null;
|
|
5360
|
+
throw e;
|
|
5361
|
+
}
|
|
5362
|
+
}
|
|
5363
|
+
/** Query rows with filters, ordering, and keyset pagination. */
|
|
5364
|
+
async query(query = {}) {
|
|
5365
|
+
return this.http.post(`${this.base()}/query`, query);
|
|
5366
|
+
}
|
|
5367
|
+
/** Update a row by primary key. Returns the updated row. */
|
|
5368
|
+
async update(id, values) {
|
|
5369
|
+
return this.http.patch(`${this.base()}/${encodeURIComponent(String(id))}`, { values });
|
|
5370
|
+
}
|
|
5371
|
+
/** Delete a row by primary key. Returns the number of rows removed (0 or 1). */
|
|
5372
|
+
async delete(id) {
|
|
5373
|
+
const res = await this.http.del(`${this.base()}/${encodeURIComponent(String(id))}`);
|
|
5374
|
+
return res.deleted ?? 0;
|
|
5375
|
+
}
|
|
5376
|
+
}
|
|
5377
|
+
class DbNamespace {
|
|
5378
|
+
deps;
|
|
5379
|
+
constructor(deps) {
|
|
5380
|
+
this.deps = deps;
|
|
5381
|
+
}
|
|
5382
|
+
/**
|
|
5383
|
+
* A handle to a table defined in the portal. The optional type parameter
|
|
5384
|
+
* gives row operations a typed shape:
|
|
5385
|
+
*
|
|
5386
|
+
* interface Todo { _id: number; title: string; done: boolean }
|
|
5387
|
+
* const todos = client.db.table<Todo>('todos');
|
|
5388
|
+
*/
|
|
5389
|
+
table(name) {
|
|
5390
|
+
return new DbTable(this.deps.http, name);
|
|
5391
|
+
}
|
|
5392
|
+
}
|
|
5393
|
+
|
|
5283
5394
|
/**
|
|
5284
5395
|
* `client.storage` — file storage. Hides the `FileStorage` / `ShardClient` /
|
|
5285
5396
|
* `SharedSpaceClient` primitives behind the unified client.
|
|
@@ -5659,12 +5770,27 @@ class Space {
|
|
|
5659
5770
|
/** Open the space websocket (resolves the ticket + identity lazily). */
|
|
5660
5771
|
async connect() {
|
|
5661
5772
|
if (!this.channel) {
|
|
5662
|
-
|
|
5663
|
-
|
|
5664
|
-
|
|
5773
|
+
// The WS upgrade ticket is single-use + short-TTL, so a reconnect
|
|
5774
|
+
// must mint a FRESH one — otherwise the socket drops (idle timeout,
|
|
5775
|
+
// network blip) and never comes back. `buildUrl` is handed to the
|
|
5776
|
+
// transport as a `urlProvider` it calls before each reconnect.
|
|
5777
|
+
const buildUrl = async () => {
|
|
5778
|
+
const ticket = await this.deps.fetchTicket();
|
|
5779
|
+
return (`${this.deps.wsBaseUrl}/api/spaces/${encodeURIComponent(this.name)}/websocket` +
|
|
5780
|
+
(ticket ? `?ticket=${encodeURIComponent(ticket)}` : ""));
|
|
5781
|
+
};
|
|
5782
|
+
const url = await buildUrl();
|
|
5665
5783
|
this.channel = this.deps.createChannel
|
|
5666
5784
|
? this.deps.createChannel(url, this.deps.myId())
|
|
5667
|
-
: new BroadcastChannel({
|
|
5785
|
+
: new BroadcastChannel({
|
|
5786
|
+
url,
|
|
5787
|
+
myId: this.deps.myId(),
|
|
5788
|
+
autoAnnounce: false,
|
|
5789
|
+
urlProvider: buildUrl,
|
|
5790
|
+
// Keep recovering across network flaps (each attempt mints a
|
|
5791
|
+
// fresh ticket); teardown calls disconnect() which stops it.
|
|
5792
|
+
maxReconnectAttempts: 0,
|
|
5793
|
+
});
|
|
5668
5794
|
for (const [event, handler] of this.pending)
|
|
5669
5795
|
this.channel.on(event, handler);
|
|
5670
5796
|
this.pending.length = 0;
|
|
@@ -5868,8 +5994,8 @@ class Space {
|
|
|
5868
5994
|
return this.requireKeyring().rotate(roster);
|
|
5869
5995
|
}
|
|
5870
5996
|
// -- room-scoped files (no websocket needed) -------------------------------
|
|
5871
|
-
async putFile(file, metadata) {
|
|
5872
|
-
return this.fileStorage().writeFileToShards({ data: file, metadata });
|
|
5997
|
+
async putFile(file, metadata, opts = {}) {
|
|
5998
|
+
return this.fileStorage().writeFileToShards({ data: file, metadata, onProgress: opts.onProgress });
|
|
5873
5999
|
}
|
|
5874
6000
|
async getFile(manifest) {
|
|
5875
6001
|
return this.fileStorage().readFileFromShards(manifest);
|
|
@@ -6026,6 +6152,12 @@ class Space {
|
|
|
6026
6152
|
* nothing to verify against → reject.
|
|
6027
6153
|
*/
|
|
6028
6154
|
async verifySender(packet) {
|
|
6155
|
+
// Trust our OWN messages — we sent them, so there's nothing to prove
|
|
6156
|
+
// against the roster. This also keeps a returning member (who loaded a
|
|
6157
|
+
// cached key and never re-published a join-request) from dropping the
|
|
6158
|
+
// echo of their own messages when their `ecdsaPub` isn't in the roster.
|
|
6159
|
+
if (packet.source === this.deps.myId())
|
|
6160
|
+
return true;
|
|
6029
6161
|
const keyring = this.deps.keyring;
|
|
6030
6162
|
const sig = packet.headers?.sig;
|
|
6031
6163
|
if (!keyring || typeof sig !== "string")
|
|
@@ -6522,6 +6654,34 @@ class KeyringClient {
|
|
|
6522
6654
|
async invite(username) {
|
|
6523
6655
|
await this.post("/keyring/invite", { username });
|
|
6524
6656
|
}
|
|
6657
|
+
// -- Shareable invite links ----------------------------------------------
|
|
6658
|
+
// A capability token tied to this space. Redeeming it allowlists the holder
|
|
6659
|
+
// so the keeper admits them — the link IS the invitation. Mint/list/revoke
|
|
6660
|
+
// require the caller to be the creator or an existing member.
|
|
6661
|
+
async createInviteLink(opts = {}) {
|
|
6662
|
+
return this.post("/keyring/link", opts);
|
|
6663
|
+
}
|
|
6664
|
+
async listInviteLinks() {
|
|
6665
|
+
const body = await this.get("/keyring/links");
|
|
6666
|
+
return body.links ?? [];
|
|
6667
|
+
}
|
|
6668
|
+
/** Set a member's role ("viewer" | "editor"). Owner-only on the server. */
|
|
6669
|
+
async setMemberRole(username, role) {
|
|
6670
|
+
await this.post("/keyring/role", { username, role });
|
|
6671
|
+
}
|
|
6672
|
+
/** Roster with roles (owner/members only). */
|
|
6673
|
+
async members() {
|
|
6674
|
+
const body = await this.get("/keyring/members");
|
|
6675
|
+
return body.members ?? [];
|
|
6676
|
+
}
|
|
6677
|
+
async revokeInviteLink(token) {
|
|
6678
|
+
await this.post("/keyring/link-revoke", { token });
|
|
6679
|
+
}
|
|
6680
|
+
/** Redeem a link: the server allowlists this member and asks the keeper to
|
|
6681
|
+
* wrap the group key for their identity. Caller must be authenticated. */
|
|
6682
|
+
async redeemInvite(req) {
|
|
6683
|
+
await this.post("/keyring/redeem", req);
|
|
6684
|
+
}
|
|
6525
6685
|
async fetchMetadata() {
|
|
6526
6686
|
try {
|
|
6527
6687
|
return await this.get("/metadata");
|
|
@@ -6659,6 +6819,56 @@ class SpaceNamespace {
|
|
|
6659
6819
|
return this.joinSpace(spaceId, opts);
|
|
6660
6820
|
}
|
|
6661
6821
|
// -------------------------------------------------------------------------
|
|
6822
|
+
// Invite links — shareable, keeper-gated invitations to a space. The token
|
|
6823
|
+
// is a capability: anyone signed in who redeems it is admitted by the
|
|
6824
|
+
// keeper. Mint/list/revoke require the caller to be the space's creator or
|
|
6825
|
+
// an existing member.
|
|
6826
|
+
// -------------------------------------------------------------------------
|
|
6827
|
+
/** Mint a shareable invite link for a space. `opts.expiresInSec` / `maxUses`
|
|
6828
|
+
* bound it (0/undefined = unlimited); `opts.role` ("viewer"|"editor") is the
|
|
6829
|
+
* access granted to redeemers (default viewer). Returns the link. */
|
|
6830
|
+
async createInviteLink(spaceId, opts = {}) {
|
|
6831
|
+
return this.keyringFor(spaceId).createInviteLink(opts);
|
|
6832
|
+
}
|
|
6833
|
+
/** Set a member's role on a space ("viewer" | "editor"). Owner-only. */
|
|
6834
|
+
async setMemberRole(spaceId, username, role) {
|
|
6835
|
+
await this.keyringFor(spaceId).setMemberRole(username, role);
|
|
6836
|
+
}
|
|
6837
|
+
/** Roster with roles (owner/members only). */
|
|
6838
|
+
async members(spaceId) {
|
|
6839
|
+
return this.keyringFor(spaceId).members();
|
|
6840
|
+
}
|
|
6841
|
+
/** List a space's active invite links (creator/members only). */
|
|
6842
|
+
async listInviteLinks(spaceId) {
|
|
6843
|
+
return this.keyringFor(spaceId).listInviteLinks();
|
|
6844
|
+
}
|
|
6845
|
+
/** Revoke an invite link by token. */
|
|
6846
|
+
async revokeInviteLink(spaceId, token) {
|
|
6847
|
+
await this.keyringFor(spaceId).revokeInviteLink(token);
|
|
6848
|
+
}
|
|
6849
|
+
/**
|
|
6850
|
+
* Join a space using an invite link: redeem the token (the keeper admits
|
|
6851
|
+
* this member), then connect and resolve once the group key arrives.
|
|
6852
|
+
* Returns the open {@link Space}.
|
|
6853
|
+
*/
|
|
6854
|
+
async joinByInvite(spaceId, token, opts = {}) {
|
|
6855
|
+
const memberId = this.myId();
|
|
6856
|
+
const { ecdhPub, ecdsaPub } = await this.ensureIdentity(memberId);
|
|
6857
|
+
await this.keyringFor(spaceId).redeemInvite({ token, identityEcdhPub: ecdhPub, identityEcdsaPub: ecdsaPub });
|
|
6858
|
+
return this.joinSpace(spaceId, opts);
|
|
6859
|
+
}
|
|
6860
|
+
/** The current member roster of a space (member id → identity public keys). */
|
|
6861
|
+
async roster(spaceId) {
|
|
6862
|
+
return this.keyringFor(spaceId).fetchRoster();
|
|
6863
|
+
}
|
|
6864
|
+
keyringFor(spaceId) {
|
|
6865
|
+
return new KeyringClient({
|
|
6866
|
+
spaceId,
|
|
6867
|
+
httpBaseUrl: this.deps.http.baseUrl,
|
|
6868
|
+
fetch: this.deps.http.fetch,
|
|
6869
|
+
});
|
|
6870
|
+
}
|
|
6871
|
+
// -------------------------------------------------------------------------
|
|
6662
6872
|
// Internals
|
|
6663
6873
|
// -------------------------------------------------------------------------
|
|
6664
6874
|
async build(spaceId, historyPolicy) {
|
|
@@ -6725,6 +6935,138 @@ class SpaceNamespace {
|
|
|
6725
6935
|
}
|
|
6726
6936
|
}
|
|
6727
6937
|
|
|
6938
|
+
/**
|
|
6939
|
+
* `client.agents` — manage an app's **Programmable Agents** (server-side
|
|
6940
|
+
* "virtual users" backed by Workers AI).
|
|
6941
|
+
*
|
|
6942
|
+
* const { config } = await client.agents.create(appId, {
|
|
6943
|
+
* handle: '@assistant', displayName: 'Assistant',
|
|
6944
|
+
* systemPrompt: 'You are a helpful teammate in this Space.',
|
|
6945
|
+
* triggers: [{ type: 'mention' }],
|
|
6946
|
+
* });
|
|
6947
|
+
* await client.agents.enable(appId, config.agentId, spaceId); // per-Space opt-in
|
|
6948
|
+
*
|
|
6949
|
+
* These are MANAGEMENT calls — the agent itself runs on the accelerator. Only an
|
|
6950
|
+
* app's owner or an editor of its shared Space may create/update/enable an agent
|
|
6951
|
+
* (viewers can read). Authorization rides the user session token; non-owner
|
|
6952
|
+
* editors pass their management Space via `opts.space`.
|
|
6953
|
+
*
|
|
6954
|
+
* An agent is "prompts + persona": a system prompt, a model, named prompt
|
|
6955
|
+
* `skills`, and `triggers` that decide when it speaks — plus optional `tools`
|
|
6956
|
+
* that let it act on the app (query/write its database, call its functions,
|
|
6957
|
+
* resolve its channels) via a server-side function-calling loop. See
|
|
6958
|
+
* {@link AgentToolsConfig}. Pair with the `@MuhkooAgent`/`@MuhkooSpace`/
|
|
6959
|
+
* `@MuhkooDB`/`@MuhkooFunction` decorators + `ejectAgentPrompt`/`ejectAgentTools`
|
|
6960
|
+
* to generate `systemPrompt` and `tools` straight from your app's surface.
|
|
6961
|
+
*/
|
|
6962
|
+
class AgentsNamespace {
|
|
6963
|
+
deps;
|
|
6964
|
+
constructor(deps) {
|
|
6965
|
+
this.deps = deps;
|
|
6966
|
+
}
|
|
6967
|
+
/** List the app's agents. */
|
|
6968
|
+
async list(appId, opts = {}) {
|
|
6969
|
+
const res = await this.deps.http.get(this.path(appId, "", opts));
|
|
6970
|
+
return res.agents ?? [];
|
|
6971
|
+
}
|
|
6972
|
+
/** Read one agent (config + identity keys). */
|
|
6973
|
+
async get(appId, agentId, opts = {}) {
|
|
6974
|
+
return this.deps.http.get(this.path(appId, `/${encodeURIComponent(agentId)}`, opts));
|
|
6975
|
+
}
|
|
6976
|
+
/** Create an agent (owner/editor only). */
|
|
6977
|
+
async create(appId, input, opts = {}) {
|
|
6978
|
+
return this.deps.http.post(this.path(appId, "", {}), { ...input, space: opts.space });
|
|
6979
|
+
}
|
|
6980
|
+
/** Update an agent's editor-settable fields (owner/editor only). */
|
|
6981
|
+
async update(appId, agentId, patch, opts = {}) {
|
|
6982
|
+
return this.deps.http.patch(this.path(appId, `/${encodeURIComponent(agentId)}`, {}), { ...patch, space: opts.space });
|
|
6983
|
+
}
|
|
6984
|
+
/** Delete an agent (owner/editor only). */
|
|
6985
|
+
async delete(appId, agentId, opts = {}) {
|
|
6986
|
+
return this.deps.http.del(this.path(appId, `/${encodeURIComponent(agentId)}`, {}), { space: opts.space });
|
|
6987
|
+
}
|
|
6988
|
+
/** Enable the agent on a Space (per-Space opt-in). */
|
|
6989
|
+
async enable(appId, agentId, spaceId, opts = {}) {
|
|
6990
|
+
return this.deps.http.post(this.path(appId, `/${encodeURIComponent(agentId)}/enable`, {}), { targetSpaceId: spaceId, space: opts.space });
|
|
6991
|
+
}
|
|
6992
|
+
/** Disable the agent on a Space. */
|
|
6993
|
+
async disable(appId, agentId, spaceId, opts = {}) {
|
|
6994
|
+
return this.deps.http.post(this.path(appId, `/${encodeURIComponent(agentId)}/disable`, {}), { targetSpaceId: spaceId, space: opts.space });
|
|
6995
|
+
}
|
|
6996
|
+
path(appId, suffix, opts) {
|
|
6997
|
+
const q = opts.space ? `?space=${encodeURIComponent(opts.space)}` : "";
|
|
6998
|
+
return `/api/apps/${encodeURIComponent(appId)}/agents${suffix}${q}`;
|
|
6999
|
+
}
|
|
7000
|
+
}
|
|
7001
|
+
|
|
7002
|
+
/**
|
|
7003
|
+
* `client.functions` — manage an app's **serverless functions** (developer-
|
|
7004
|
+
* authored code that runs on the accelerator).
|
|
7005
|
+
*
|
|
7006
|
+
* const { config } = await client.functions.deploy(appId, {
|
|
7007
|
+
* name: 'hello', displayName: 'Hello',
|
|
7008
|
+
* code: `export default { fetch: () => new Response('hi') }`,
|
|
7009
|
+
* triggers: { http: { enabled: true } },
|
|
7010
|
+
* });
|
|
7011
|
+
* // HTTP-reachable at hello--<slug>.fns.muhkoo.dev
|
|
7012
|
+
* await client.functions.enable(appId, config.functionId, spaceId); // Space-bound
|
|
7013
|
+
*
|
|
7014
|
+
* These are MANAGEMENT calls — the function itself runs on the accelerator
|
|
7015
|
+
* (uploaded just-in-time, decrypted only at invocation). Only an app's owner or
|
|
7016
|
+
* an editor of its shared Space may deploy/update/enable (viewers read).
|
|
7017
|
+
* Authorization rides the user session token; non-owner editors pass their
|
|
7018
|
+
* management Space via `opts.space`.
|
|
7019
|
+
*
|
|
7020
|
+
* v1 is "compute-only": a function is an untrusted single-module ES worker
|
|
7021
|
+
* (`export default { fetch }`) with NO platform bindings — it reaches the
|
|
7022
|
+
* platform as an external client (the end-user session is passed through). Two
|
|
7023
|
+
* triggers: `http` (its own subdomain) and `space` (invoked on Space messages,
|
|
7024
|
+
* like a Programmable Agent).
|
|
7025
|
+
*/
|
|
7026
|
+
class FunctionsNamespace {
|
|
7027
|
+
deps;
|
|
7028
|
+
constructor(deps) {
|
|
7029
|
+
this.deps = deps;
|
|
7030
|
+
}
|
|
7031
|
+
/** List the app's functions. */
|
|
7032
|
+
async list(appId, opts = {}) {
|
|
7033
|
+
const res = await this.deps.http.get(this.path(appId, "", opts));
|
|
7034
|
+
return res.functions ?? [];
|
|
7035
|
+
}
|
|
7036
|
+
/** Read one function's config. */
|
|
7037
|
+
async get(appId, functionId, opts = {}) {
|
|
7038
|
+
return this.deps.http.get(this.path(appId, `/${encodeURIComponent(functionId)}`, opts));
|
|
7039
|
+
}
|
|
7040
|
+
/** Read a function's decrypted source (owner/editor; for the editor UI). */
|
|
7041
|
+
async code(appId, functionId, opts = {}) {
|
|
7042
|
+
return this.deps.http.get(this.path(appId, `/${encodeURIComponent(functionId)}/code`, opts));
|
|
7043
|
+
}
|
|
7044
|
+
/** Deploy (create) a function (owner/editor only). */
|
|
7045
|
+
async deploy(appId, input, opts = {}) {
|
|
7046
|
+
return this.deps.http.post(this.path(appId, "", {}), { ...input, space: opts.space });
|
|
7047
|
+
}
|
|
7048
|
+
/** Update a function's editor-settable fields and/or redeploy its `code`. */
|
|
7049
|
+
async update(appId, functionId, patch, opts = {}) {
|
|
7050
|
+
return this.deps.http.patch(this.path(appId, `/${encodeURIComponent(functionId)}`, {}), { ...patch, space: opts.space });
|
|
7051
|
+
}
|
|
7052
|
+
/** Delete a function (owner/editor only). */
|
|
7053
|
+
async delete(appId, functionId, opts = {}) {
|
|
7054
|
+
return this.deps.http.del(this.path(appId, `/${encodeURIComponent(functionId)}`, {}), { space: opts.space });
|
|
7055
|
+
}
|
|
7056
|
+
/** Enable the function on a Space (per-Space opt-in for the `space` trigger). */
|
|
7057
|
+
async enable(appId, functionId, spaceId, opts = {}) {
|
|
7058
|
+
return this.deps.http.post(this.path(appId, `/${encodeURIComponent(functionId)}/enable`, {}), { targetSpaceId: spaceId, space: opts.space });
|
|
7059
|
+
}
|
|
7060
|
+
/** Disable the function on a Space. */
|
|
7061
|
+
async disable(appId, functionId, spaceId, opts = {}) {
|
|
7062
|
+
return this.deps.http.post(this.path(appId, `/${encodeURIComponent(functionId)}/disable`, {}), { targetSpaceId: spaceId, space: opts.space });
|
|
7063
|
+
}
|
|
7064
|
+
path(appId, suffix, opts) {
|
|
7065
|
+
const q = opts.space ? `?space=${encodeURIComponent(opts.space)}` : "";
|
|
7066
|
+
return `/api/apps/${encodeURIComponent(appId)}/functions${suffix}${q}`;
|
|
7067
|
+
}
|
|
7068
|
+
}
|
|
7069
|
+
|
|
6728
7070
|
/**
|
|
6729
7071
|
* `Client` — the single entry point to the Muhkoo SDK.
|
|
6730
7072
|
*
|
|
@@ -6763,12 +7105,18 @@ class Client {
|
|
|
6763
7105
|
auth;
|
|
6764
7106
|
/** Per-user key/value storage — `client.kv.set(...)`, etc. */
|
|
6765
7107
|
kv;
|
|
7108
|
+
/** App scalable database — `client.db.table('todos').query(...)`, etc. */
|
|
7109
|
+
db;
|
|
6766
7110
|
/** File storage — `client.storage.writeFile(...)`, etc. */
|
|
6767
7111
|
storage;
|
|
6768
7112
|
/** Realtime messaging — `client.message.subscribe(...)`, etc. */
|
|
6769
7113
|
message;
|
|
6770
7114
|
/** Fan-out group spaces — `client.space.createSpace(...)`, etc. */
|
|
6771
7115
|
space;
|
|
7116
|
+
/** Programmable Agents — `client.agents.create(appId, …)`, etc. */
|
|
7117
|
+
agents;
|
|
7118
|
+
/** Serverless functions — `client.functions.deploy(appId, …)`, etc. */
|
|
7119
|
+
functions;
|
|
6772
7120
|
session;
|
|
6773
7121
|
http;
|
|
6774
7122
|
/** Subscribers notified when a session expires and can't be recovered. */
|
|
@@ -6796,6 +7144,7 @@ class Client {
|
|
|
6796
7144
|
const wsBaseUrl = toWsBase(this.baseUrl);
|
|
6797
7145
|
this.auth = new AuthNamespace({ auth: authClient, circuits, session: this.session });
|
|
6798
7146
|
this.kv = new KvNamespace({ http: this.http, session: this.session, wsBaseUrl });
|
|
7147
|
+
this.db = new DbNamespace({ http: this.http });
|
|
6799
7148
|
this.storage = new StorageNamespace({ http: this.http, baseUrl: this.baseUrl, kv: this.kv });
|
|
6800
7149
|
this.message = new MessageNamespace({ http: this.http, session: this.session, wsBaseUrl });
|
|
6801
7150
|
// Group keys are cached in the user's PersonalSpace (encrypted at rest
|
|
@@ -6811,6 +7160,8 @@ class Client {
|
|
|
6811
7160
|
wsBaseUrl,
|
|
6812
7161
|
cache: spaceKeyCache,
|
|
6813
7162
|
});
|
|
7163
|
+
this.agents = new AgentsNamespace({ http: this.http });
|
|
7164
|
+
this.functions = new FunctionsNamespace({ http: this.http });
|
|
6814
7165
|
}
|
|
6815
7166
|
/** The currently signed-in user, or `null`. */
|
|
6816
7167
|
get user() {
|
|
@@ -6864,6 +7215,177 @@ class Client {
|
|
|
6864
7215
|
}
|
|
6865
7216
|
}
|
|
6866
7217
|
|
|
7218
|
+
/**
|
|
7219
|
+
* Muhkoo app decorators — declare your app's agent-facing surface in code, then
|
|
7220
|
+
* "eject" a system prompt you hand to a Programmable Agent.
|
|
7221
|
+
*
|
|
7222
|
+
* You annotate a plain class with `@MuhkooAgent` (the app's identity + how the
|
|
7223
|
+
* agent should behave) plus per-surface decorators — `@MuhkooSpace`,
|
|
7224
|
+
* `@MuhkooDB`, `@MuhkooFunction` — that describe what each channel, table, and
|
|
7225
|
+
* function is *for*. {@link ejectAgentPrompt} reads those annotations and
|
|
7226
|
+
* produces a `systemPrompt` string:
|
|
7227
|
+
*
|
|
7228
|
+
* ```ts
|
|
7229
|
+
* @MuhkooAgent({
|
|
7230
|
+
* name: "Chat Assistant",
|
|
7231
|
+
* purpose: "A helpful assistant inside a real-time team chat.",
|
|
7232
|
+
* guidance: ["Keep replies short.", "Only chime in when relevant."],
|
|
7233
|
+
* })
|
|
7234
|
+
* class ChatAssistant {
|
|
7235
|
+
* @MuhkooSpace({ description: "Main team discussion." }) general!: string;
|
|
7236
|
+
* @MuhkooDB({ access: "read", description: "Chat message history." }) messages!: unknown;
|
|
7237
|
+
* @MuhkooFunction({ description: "Open a support ticket." }) openTicket!: () => void;
|
|
7238
|
+
* }
|
|
7239
|
+
*
|
|
7240
|
+
* const systemPrompt = ejectAgentPrompt(ChatAssistant);
|
|
7241
|
+
* await client.agents.create({ handle: "assistant", displayName: "Assistant", systemPrompt });
|
|
7242
|
+
* ```
|
|
7243
|
+
*
|
|
7244
|
+
* The ejected prompt owns the **semantic** layer — what the app is, how to act,
|
|
7245
|
+
* and what each surface means. The Muhkoo runtime separately appends the
|
|
7246
|
+
* **authoritative** roster (exact columns, function parameters, and the closed
|
|
7247
|
+
* list of callable tools), so the prompt here never needs to restate schema and
|
|
7248
|
+
* can't drift from it. Decorators only describe; they never change runtime
|
|
7249
|
+
* behavior, so they're safe to apply to a throwaway descriptor class.
|
|
7250
|
+
*
|
|
7251
|
+
* Requires `experimentalDecorators` (already enabled in this package). Metadata
|
|
7252
|
+
* is captured in a per-class registry — no `reflect-metadata` dependency.
|
|
7253
|
+
*/
|
|
7254
|
+
// Per-class metadata store, keyed by the class constructor. Member decorators
|
|
7255
|
+
// run during class-body evaluation (before the class decorator), so each
|
|
7256
|
+
// decorator get-or-creates the descriptor and they all converge on one entry.
|
|
7257
|
+
const REGISTRY = new WeakMap();
|
|
7258
|
+
function descriptorFor(ctor) {
|
|
7259
|
+
let d = REGISTRY.get(ctor);
|
|
7260
|
+
if (!d) {
|
|
7261
|
+
d = { spaces: [], tables: [], functions: [] };
|
|
7262
|
+
REGISTRY.set(ctor, d);
|
|
7263
|
+
}
|
|
7264
|
+
return d;
|
|
7265
|
+
}
|
|
7266
|
+
/** Resolve the owning class constructor from a decorator `target` — the
|
|
7267
|
+
* prototype for instance members, the constructor itself for static ones. */
|
|
7268
|
+
function ctorOf(target) {
|
|
7269
|
+
return typeof target === "function" ? target : target.constructor;
|
|
7270
|
+
}
|
|
7271
|
+
/**
|
|
7272
|
+
* Class decorator. Declares the app's agent identity, purpose, and behavioral
|
|
7273
|
+
* guidance. Apply it to the class whose members carry the surface decorators.
|
|
7274
|
+
*/
|
|
7275
|
+
function MuhkooAgent(meta) {
|
|
7276
|
+
return (target) => {
|
|
7277
|
+
descriptorFor(target).agent = meta;
|
|
7278
|
+
};
|
|
7279
|
+
}
|
|
7280
|
+
/** Member decorator. Marks a channel/Space the agent can resolve and post to. */
|
|
7281
|
+
function MuhkooSpace(meta) {
|
|
7282
|
+
return ((target, propertyKey) => {
|
|
7283
|
+
descriptorFor(ctorOf(target)).spaces.push({
|
|
7284
|
+
name: meta.name ?? String(propertyKey),
|
|
7285
|
+
description: meta.description,
|
|
7286
|
+
});
|
|
7287
|
+
});
|
|
7288
|
+
}
|
|
7289
|
+
/** Member decorator. Marks an app database table the agent can query/mutate. */
|
|
7290
|
+
function MuhkooDB(meta) {
|
|
7291
|
+
return ((target, propertyKey) => {
|
|
7292
|
+
descriptorFor(ctorOf(target)).tables.push({
|
|
7293
|
+
table: meta.table ?? String(propertyKey),
|
|
7294
|
+
access: meta.access ?? "read",
|
|
7295
|
+
description: meta.description,
|
|
7296
|
+
});
|
|
7297
|
+
});
|
|
7298
|
+
}
|
|
7299
|
+
/** Member decorator. Marks a serverless function the agent can call. */
|
|
7300
|
+
function MuhkooFunction(meta) {
|
|
7301
|
+
return ((target, propertyKey) => {
|
|
7302
|
+
descriptorFor(ctorOf(target)).functions.push({
|
|
7303
|
+
name: meta.name ?? String(propertyKey),
|
|
7304
|
+
description: meta.description,
|
|
7305
|
+
});
|
|
7306
|
+
});
|
|
7307
|
+
}
|
|
7308
|
+
/** Read back the raw descriptor a class accumulated (for tooling/tests). */
|
|
7309
|
+
function getMuhkooAppDescriptor(appClass) {
|
|
7310
|
+
return REGISTRY.get(appClass);
|
|
7311
|
+
}
|
|
7312
|
+
/**
|
|
7313
|
+
* Compose the agent system prompt from a `@MuhkooAgent`-decorated class. Throws
|
|
7314
|
+
* if the class isn't decorated. The result is the semantic layer only — the
|
|
7315
|
+
* runtime appends exact schema + the closed tool list at invocation time.
|
|
7316
|
+
*/
|
|
7317
|
+
function ejectAgentPrompt(appClass) {
|
|
7318
|
+
const d = REGISTRY.get(appClass);
|
|
7319
|
+
if (!d || !d.agent) {
|
|
7320
|
+
throw new Error("ejectAgentPrompt: the class must be decorated with @MuhkooAgent");
|
|
7321
|
+
}
|
|
7322
|
+
const { agent } = d;
|
|
7323
|
+
const lines = [];
|
|
7324
|
+
lines.push(`You are ${agent.name}. ${agent.purpose}`.trim());
|
|
7325
|
+
if (agent.guidance?.length) {
|
|
7326
|
+
lines.push("", "Guidance:");
|
|
7327
|
+
for (const g of agent.guidance)
|
|
7328
|
+
lines.push(`- ${g}`);
|
|
7329
|
+
}
|
|
7330
|
+
if (d.spaces.length) {
|
|
7331
|
+
lines.push("", "Channels (Spaces) you can resolve with resolve_channel and post messages into:");
|
|
7332
|
+
for (const s of d.spaces)
|
|
7333
|
+
lines.push(`- ${s.name}: ${s.description}`);
|
|
7334
|
+
}
|
|
7335
|
+
if (d.tables.length) {
|
|
7336
|
+
lines.push("", "App database tables:");
|
|
7337
|
+
for (const t of d.tables) {
|
|
7338
|
+
const verbs = t.access === "write"
|
|
7339
|
+
? "read & write — db_query/db_get/db_insert/db_update/db_delete"
|
|
7340
|
+
: "read-only — db_query/db_get";
|
|
7341
|
+
lines.push(`- ${t.table} (${verbs}): ${t.description}`);
|
|
7342
|
+
}
|
|
7343
|
+
}
|
|
7344
|
+
if (d.functions.length) {
|
|
7345
|
+
lines.push("", "Functions you can invoke with call_function:");
|
|
7346
|
+
for (const f of d.functions)
|
|
7347
|
+
lines.push(`- ${f.name}: ${f.description}`);
|
|
7348
|
+
}
|
|
7349
|
+
if (agent.instructions?.trim()) {
|
|
7350
|
+
lines.push("", agent.instructions.trim());
|
|
7351
|
+
}
|
|
7352
|
+
// How to respond — the load-bearing behavioral contract. Without this, models
|
|
7353
|
+
// (especially gpt-oss) tend to run tool calls and then end their turn with no
|
|
7354
|
+
// user-facing message, so the user sees the work happen but gets no reply.
|
|
7355
|
+
const hasTools = d.tables.length > 0 || d.functions.length > 0;
|
|
7356
|
+
lines.push("", "How to respond:");
|
|
7357
|
+
if (hasTools) {
|
|
7358
|
+
lines.push("- When you need information or need to make a change, call your tools immediately — don't ask the user to do it, and don't describe or name the tools to them.", "- After your tool calls return, you MUST send one final reply to the channel, in plain language, that answers the request using the results.");
|
|
7359
|
+
}
|
|
7360
|
+
lines.push("- Always end your turn with a short written reply (one or two sentences). Never finish with only tool calls and no message, and never reply with an empty message.", "- Don't restate function names, parameters, JSON, or your list of tools to the user — just give them the answer.");
|
|
7361
|
+
lines.push("", "The exact columns, function parameters, and the full list of callable tools are supplied to you by the runtime. Follow that authoritative list — never invent tables, columns, functions, or tools.");
|
|
7362
|
+
return lines.join("\n");
|
|
7363
|
+
}
|
|
7364
|
+
/**
|
|
7365
|
+
* Derive a suggested tools allowlist from a decorated class — the db tables
|
|
7366
|
+
* (and whether any are writable), the callable functions, and whether channels
|
|
7367
|
+
* are in play. Pair with {@link ejectAgentPrompt} when configuring the agent.
|
|
7368
|
+
*
|
|
7369
|
+
* If any tables/functions are present the result has `enabled: true`; to
|
|
7370
|
+
* actually grant tools you must also pass a function-calling `model` in the same
|
|
7371
|
+
* `client.agents.create`/`update` call (the server enforces this).
|
|
7372
|
+
*/
|
|
7373
|
+
function ejectAgentTools(appClass, opts) {
|
|
7374
|
+
const d = REGISTRY.get(appClass);
|
|
7375
|
+
if (!d) {
|
|
7376
|
+
throw new Error("ejectAgentTools: the class must be decorated with @MuhkooAgent");
|
|
7377
|
+
}
|
|
7378
|
+
const anyWrite = d.tables.some((t) => t.access === "write");
|
|
7379
|
+
const mode = d.tables.length === 0 ? "off" : anyWrite ? "write" : "read";
|
|
7380
|
+
return {
|
|
7381
|
+
enabled: d.tables.length > 0 || d.functions.length > 0 || d.spaces.length > 0,
|
|
7382
|
+
db: { mode, tables: d.tables.map((t) => t.table) },
|
|
7383
|
+
functions: d.functions.map((f) => f.name),
|
|
7384
|
+
channels: d.spaces.length > 0,
|
|
7385
|
+
maxIterations: opts?.maxIterations ?? 6,
|
|
7386
|
+
};
|
|
7387
|
+
}
|
|
7388
|
+
|
|
6867
7389
|
/**
|
|
6868
7390
|
* PacketCipher — the strategy `Network` uses to seal/open a packet's payload.
|
|
6869
7391
|
*
|
|
@@ -6930,5 +7452,5 @@ var bundledLoader = /*#__PURE__*/Object.freeze({
|
|
|
6930
7452
|
default: loadRsWasm
|
|
6931
7453
|
});
|
|
6932
7454
|
|
|
6933
|
-
export { AES_GCM_IV_BYTES, AES_GCM_KEY_BYTES, AES_GCM_TAG_BYTES, AuthClient, AuthNamespace, AuthPublicInput, Authenticator, BroadcastChannel, BroadcastChannelEvents, ChannelExistsError, ChannelNotFoundError, ChunkCipher, Client, DEFAULT_BASE_URL, DoubleRatchet, DoubleRatchetCipher, DoubleRatchetManager, EncryptedSession, EventCore, EventCoreEvents, Field, Field as FieldElement, FileStorage, HashKnowledge, HttpClient, HttpError, KEEPER_MEMBER_ID, KeyStore, KeyringClient, KvNamespace, LocalStorageSessionStore, MemorySessionStore, Message, Message as MessageDefault, MessageNamespace, PBKDF2_DEFAULT_ITERATIONS, PREIMAGE_POK_VERIFICATION_KEY, Packet, PersonalSpaceClient, Poseidon, PreimagePoK, ReedSolomonCodec, Space as Room, SerializeMessage, SessionState, ShardClient, SharedSpaceClient, Space, SpaceKeyring, SpaceNamespace, SpacePacketCipher, StorageNamespace, WSTransport, ZkAuth, buildCommitment, compilePrograms, decodeFromHex, decorators, decryptAesGcm, defaultCircuitUrls, defaultSessionStore, deriveAesKeyFromPassphrase, deriveBitsHkdf, deriveIdentity, encodeToHex, encryptAesGcm, exportPublicKeyBase64, exportPublicKeyHex, generateAuthProof, getSubtle, initBn128Wasm, initRsWasm, initializeCircuits, manifestToStat, poseidonHash, quickVerify, randomBytes, shardHash, signMessage, toField, unwrapWithPassphrase, verify, verifyGroth16, verifyHashKnowledge, verifyPreimagePoK, wrapWithPassphrase };
|
|
7455
|
+
export { AES_GCM_IV_BYTES, AES_GCM_KEY_BYTES, AES_GCM_TAG_BYTES, AgentsNamespace, AuthClient, AuthNamespace, AuthPublicInput, Authenticator, BroadcastChannel, BroadcastChannelEvents, ChannelExistsError, ChannelNotFoundError, ChunkCipher, Client, DEFAULT_BASE_URL, DoubleRatchet, DoubleRatchetCipher, DoubleRatchetManager, EncryptedSession, EventCore, EventCoreEvents, Field, Field as FieldElement, FileStorage, FunctionsNamespace, HashKnowledge, HttpClient, HttpError, KEEPER_MEMBER_ID, KeyStore, KeyringClient, KvNamespace, LocalStorageSessionStore, MemorySessionStore, Message, Message as MessageDefault, MessageNamespace, MuhkooAgent, MuhkooDB, MuhkooFunction, MuhkooSpace, PBKDF2_DEFAULT_ITERATIONS, PREIMAGE_POK_VERIFICATION_KEY, Packet, PersonalSpaceClient, Poseidon, PreimagePoK, ReedSolomonCodec, Space as Room, SerializeMessage, SessionState, ShardClient, SharedSpaceClient, Space, SpaceKeyring, SpaceNamespace, SpacePacketCipher, StorageNamespace, WSTransport, ZkAuth, buildCommitment, compilePrograms, decodeFromHex, decorators, decryptAesGcm, defaultCircuitUrls, defaultSessionStore, deriveAesKeyFromPassphrase, deriveBitsHkdf, deriveIdentity, ejectAgentPrompt, ejectAgentTools, encodeToHex, encryptAesGcm, exportPublicKeyBase64, exportPublicKeyHex, generateAuthProof, getMuhkooAppDescriptor, getSubtle, initBn128Wasm, initRsWasm, initializeCircuits, manifestToStat, poseidonHash, quickVerify, randomBytes, shardHash, signMessage, toField, unwrapWithPassphrase, verify, verifyGroth16, verifyHashKnowledge, verifyPreimagePoK, wrapWithPassphrase };
|
|
6934
7456
|
//# sourceMappingURL=index.js.map
|