@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.
@@ -2790,6 +2790,9 @@ class HttpClient {
2790
2790
  post(path, body) {
2791
2791
  return this.request("POST", path, body);
2792
2792
  }
2793
+ patch(path, body) {
2794
+ return this.request("PATCH", path, body);
2795
+ }
2793
2796
  del(path, body) {
2794
2797
  return this.request("DELETE", path, body);
2795
2798
  }
@@ -3272,6 +3275,7 @@ class WSTransport extends EventCore {
3272
3275
  return `${this.id}:${event}`;
3273
3276
  }
3274
3277
  url;
3278
+ urlProvider;
3275
3279
  socket = null;
3276
3280
  _isConnected = false;
3277
3281
  _isConnecting = false;
@@ -3297,6 +3301,7 @@ class WSTransport extends EventCore {
3297
3301
  throw new Error("WSTransport: `url` is required");
3298
3302
  }
3299
3303
  this.url = opts.url;
3304
+ this.urlProvider = opts.urlProvider;
3300
3305
  this.id = opts.id ?? `ws${++__wsInstanceSeq}`;
3301
3306
  this.autoReconnect = opts.autoReconnect ?? true;
3302
3307
  this.reconnectDelay = opts.reconnectDelay ?? 3000;
@@ -3313,6 +3318,18 @@ class WSTransport extends EventCore {
3313
3318
  throw new Error("WSTransport: already connected or connecting");
3314
3319
  }
3315
3320
  this._isConnecting = true;
3321
+ // On a RECONNECT, refresh the URL (the baked-in single-use ticket is spent).
3322
+ // The first connect uses `url` as-is. If the provider throws (e.g. can't
3323
+ // mint a ticket), surface it and let scheduleReconnect retry.
3324
+ if (this.urlProvider && this.reconnectAttempts > 0) {
3325
+ try {
3326
+ this.url = await this.urlProvider();
3327
+ }
3328
+ catch (err) {
3329
+ this._isConnecting = false;
3330
+ throw err;
3331
+ }
3332
+ }
3316
3333
  return new Promise((resolve, reject) => {
3317
3334
  try {
3318
3335
  const socket = new WebSocket(this.url);
@@ -3485,7 +3502,12 @@ class WSTransport extends EventCore {
3485
3502
  await this.connect();
3486
3503
  }
3487
3504
  catch {
3488
- // scheduleReconnect will fire again from onclose
3505
+ // A failed reconnect never opened the socket, so its `onclose` won't
3506
+ // re-schedule (that path is guarded by `wasConnected`). Re-schedule here
3507
+ // so we keep retrying (with a fresh ticket each time) until we succeed
3508
+ // or hit the attempt cap.
3509
+ if (this.autoReconnect)
3510
+ this.scheduleReconnect();
3489
3511
  }
3490
3512
  }, this.reconnectDelay);
3491
3513
  }
@@ -3652,6 +3674,92 @@ class KvNamespace {
3652
3674
  }
3653
3675
  }
3654
3676
 
3677
+ /**
3678
+ * `client.db` — the app's scalable database. A developer defines tables in the
3679
+ * Muhkoo portal (columns + indexes); each table is backed by its own Durable
3680
+ * Object SQLite instance and reachable through a sanitized REST gateway. This
3681
+ * namespace wraps that gateway:
3682
+ *
3683
+ * const todos = client.db.table('todos');
3684
+ * const { id } = await todos.insert({ title: 'Buy milk', done: false });
3685
+ * const one = await todos.get(id);
3686
+ * const { rows, nextCursor } = await todos.query({
3687
+ * where: [{ column: 'done', op: 'eq', value: false }],
3688
+ * orderBy: { column: 'rank', dir: 'asc' },
3689
+ * limit: 50,
3690
+ * });
3691
+ * await todos.update(id, { done: true });
3692
+ * await todos.delete(id);
3693
+ *
3694
+ * Authorization is the **app key** ({@link HttpClient} stamps it on every
3695
+ * request); the appId + environment are resolved server-side from the key, so a
3696
+ * key can only ever reach its own app's tables. The front end never touches the
3697
+ * database directly — this REST surface is the only gateway, and all input is
3698
+ * validated against the table's schema (parameterized statements, column +
3699
+ * operator allowlists). There is no raw-SQL passthrough.
3700
+ *
3701
+ * Schema is authored in the portal, not here (the SDK reads + writes rows, it
3702
+ * doesn't create tables). Pagination is keyset-based: pass the `nextCursor` from
3703
+ * a previous `query()` back as `cursor` to fetch the next page.
3704
+ */
3705
+ /** A handle to one table — the ergonomic surface for row operations. */
3706
+ class DbTable {
3707
+ http;
3708
+ name;
3709
+ constructor(http, name) {
3710
+ this.http = http;
3711
+ this.name = name;
3712
+ }
3713
+ base() {
3714
+ return `/api/db/${encodeURIComponent(this.name)}`;
3715
+ }
3716
+ /** Insert a row. Returns the created row and its primary-key value. */
3717
+ async insert(values) {
3718
+ return this.http.post(this.base(), { values });
3719
+ }
3720
+ /** Fetch a row by primary key, or `null` if it doesn't exist. */
3721
+ async get(id) {
3722
+ try {
3723
+ const res = await this.http.get(`${this.base()}/${encodeURIComponent(String(id))}`);
3724
+ return res.row;
3725
+ }
3726
+ catch (e) {
3727
+ if (e?.status === 404)
3728
+ return null;
3729
+ throw e;
3730
+ }
3731
+ }
3732
+ /** Query rows with filters, ordering, and keyset pagination. */
3733
+ async query(query = {}) {
3734
+ return this.http.post(`${this.base()}/query`, query);
3735
+ }
3736
+ /** Update a row by primary key. Returns the updated row. */
3737
+ async update(id, values) {
3738
+ return this.http.patch(`${this.base()}/${encodeURIComponent(String(id))}`, { values });
3739
+ }
3740
+ /** Delete a row by primary key. Returns the number of rows removed (0 or 1). */
3741
+ async delete(id) {
3742
+ const res = await this.http.del(`${this.base()}/${encodeURIComponent(String(id))}`);
3743
+ return res.deleted ?? 0;
3744
+ }
3745
+ }
3746
+ class DbNamespace {
3747
+ deps;
3748
+ constructor(deps) {
3749
+ this.deps = deps;
3750
+ }
3751
+ /**
3752
+ * A handle to a table defined in the portal. The optional type parameter
3753
+ * gives row operations a typed shape:
3754
+ *
3755
+ * interface Todo { _id: number; title: string; done: boolean }
3756
+ * const todos = client.db.table<Todo>('todos');
3757
+ */
3758
+ table(name) {
3759
+ return new DbTable(this.deps.http, name);
3760
+ }
3761
+ }
3762
+
3655
3763
  /**
3656
3764
  * Wire types for the storage layer.
3657
3765
  *
@@ -4266,6 +4374,8 @@ class FileStorage {
4266
4374
  const fileId = input.fileId ?? generateId();
4267
4375
  const createdAt = Date.now();
4268
4376
  const chunks = [];
4377
+ const totalChunks = Math.max(1, Math.ceil(data.length / this.chunkSize));
4378
+ input.onProgress?.(0, totalChunks);
4269
4379
  // Slicing + encrypting + encoding + uploading is done per-chunk so
4270
4380
  // the peak memory footprint stays at ~chunkSize even for huge files.
4271
4381
  // Within a chunk, the shard uploads are parallelized up to
@@ -4275,6 +4385,7 @@ class FileStorage {
4275
4385
  const end = Math.min(start + this.chunkSize, data.length);
4276
4386
  const plaintext = data.subarray(start, end);
4277
4387
  chunks.push(await this.writeChunk(plaintext, chunkIndex));
4388
+ input.onProgress?.(chunks.length, totalChunks);
4278
4389
  // Stop after one iteration if the file is empty — we still emit a
4279
4390
  // single zero-length chunk so the manifest has a stable shape.
4280
4391
  if (data.length === 0)
@@ -5390,12 +5501,27 @@ class Space {
5390
5501
  /** Open the space websocket (resolves the ticket + identity lazily). */
5391
5502
  async connect() {
5392
5503
  if (!this.channel) {
5393
- const ticket = await this.deps.fetchTicket();
5394
- const url = `${this.deps.wsBaseUrl}/api/spaces/${encodeURIComponent(this.name)}/websocket` +
5395
- (ticket ? `?ticket=${encodeURIComponent(ticket)}` : "");
5504
+ // The WS upgrade ticket is single-use + short-TTL, so a reconnect
5505
+ // must mint a FRESH one — otherwise the socket drops (idle timeout,
5506
+ // network blip) and never comes back. `buildUrl` is handed to the
5507
+ // transport as a `urlProvider` it calls before each reconnect.
5508
+ const buildUrl = async () => {
5509
+ const ticket = await this.deps.fetchTicket();
5510
+ return (`${this.deps.wsBaseUrl}/api/spaces/${encodeURIComponent(this.name)}/websocket` +
5511
+ (ticket ? `?ticket=${encodeURIComponent(ticket)}` : ""));
5512
+ };
5513
+ const url = await buildUrl();
5396
5514
  this.channel = this.deps.createChannel
5397
5515
  ? this.deps.createChannel(url, this.deps.myId())
5398
- : new BroadcastChannel({ url, myId: this.deps.myId(), autoAnnounce: false });
5516
+ : new BroadcastChannel({
5517
+ url,
5518
+ myId: this.deps.myId(),
5519
+ autoAnnounce: false,
5520
+ urlProvider: buildUrl,
5521
+ // Keep recovering across network flaps (each attempt mints a
5522
+ // fresh ticket); teardown calls disconnect() which stops it.
5523
+ maxReconnectAttempts: 0,
5524
+ });
5399
5525
  for (const [event, handler] of this.pending)
5400
5526
  this.channel.on(event, handler);
5401
5527
  this.pending.length = 0;
@@ -5599,8 +5725,8 @@ class Space {
5599
5725
  return this.requireKeyring().rotate(roster);
5600
5726
  }
5601
5727
  // -- room-scoped files (no websocket needed) -------------------------------
5602
- async putFile(file, metadata) {
5603
- return this.fileStorage().writeFileToShards({ data: file, metadata });
5728
+ async putFile(file, metadata, opts = {}) {
5729
+ return this.fileStorage().writeFileToShards({ data: file, metadata, onProgress: opts.onProgress });
5604
5730
  }
5605
5731
  async getFile(manifest) {
5606
5732
  return this.fileStorage().readFileFromShards(manifest);
@@ -5757,6 +5883,12 @@ class Space {
5757
5883
  * nothing to verify against → reject.
5758
5884
  */
5759
5885
  async verifySender(packet) {
5886
+ // Trust our OWN messages — we sent them, so there's nothing to prove
5887
+ // against the roster. This also keeps a returning member (who loaded a
5888
+ // cached key and never re-published a join-request) from dropping the
5889
+ // echo of their own messages when their `ecdsaPub` isn't in the roster.
5890
+ if (packet.source === this.deps.myId())
5891
+ return true;
5760
5892
  const keyring = this.deps.keyring;
5761
5893
  const sig = packet.headers?.sig;
5762
5894
  if (!keyring || typeof sig !== "string")
@@ -6253,6 +6385,34 @@ class KeyringClient {
6253
6385
  async invite(username) {
6254
6386
  await this.post("/keyring/invite", { username });
6255
6387
  }
6388
+ // -- Shareable invite links ----------------------------------------------
6389
+ // A capability token tied to this space. Redeeming it allowlists the holder
6390
+ // so the keeper admits them — the link IS the invitation. Mint/list/revoke
6391
+ // require the caller to be the creator or an existing member.
6392
+ async createInviteLink(opts = {}) {
6393
+ return this.post("/keyring/link", opts);
6394
+ }
6395
+ async listInviteLinks() {
6396
+ const body = await this.get("/keyring/links");
6397
+ return body.links ?? [];
6398
+ }
6399
+ /** Set a member's role ("viewer" | "editor"). Owner-only on the server. */
6400
+ async setMemberRole(username, role) {
6401
+ await this.post("/keyring/role", { username, role });
6402
+ }
6403
+ /** Roster with roles (owner/members only). */
6404
+ async members() {
6405
+ const body = await this.get("/keyring/members");
6406
+ return body.members ?? [];
6407
+ }
6408
+ async revokeInviteLink(token) {
6409
+ await this.post("/keyring/link-revoke", { token });
6410
+ }
6411
+ /** Redeem a link: the server allowlists this member and asks the keeper to
6412
+ * wrap the group key for their identity. Caller must be authenticated. */
6413
+ async redeemInvite(req) {
6414
+ await this.post("/keyring/redeem", req);
6415
+ }
6256
6416
  async fetchMetadata() {
6257
6417
  try {
6258
6418
  return await this.get("/metadata");
@@ -6390,6 +6550,56 @@ class SpaceNamespace {
6390
6550
  return this.joinSpace(spaceId, opts);
6391
6551
  }
6392
6552
  // -------------------------------------------------------------------------
6553
+ // Invite links — shareable, keeper-gated invitations to a space. The token
6554
+ // is a capability: anyone signed in who redeems it is admitted by the
6555
+ // keeper. Mint/list/revoke require the caller to be the space's creator or
6556
+ // an existing member.
6557
+ // -------------------------------------------------------------------------
6558
+ /** Mint a shareable invite link for a space. `opts.expiresInSec` / `maxUses`
6559
+ * bound it (0/undefined = unlimited); `opts.role` ("viewer"|"editor") is the
6560
+ * access granted to redeemers (default viewer). Returns the link. */
6561
+ async createInviteLink(spaceId, opts = {}) {
6562
+ return this.keyringFor(spaceId).createInviteLink(opts);
6563
+ }
6564
+ /** Set a member's role on a space ("viewer" | "editor"). Owner-only. */
6565
+ async setMemberRole(spaceId, username, role) {
6566
+ await this.keyringFor(spaceId).setMemberRole(username, role);
6567
+ }
6568
+ /** Roster with roles (owner/members only). */
6569
+ async members(spaceId) {
6570
+ return this.keyringFor(spaceId).members();
6571
+ }
6572
+ /** List a space's active invite links (creator/members only). */
6573
+ async listInviteLinks(spaceId) {
6574
+ return this.keyringFor(spaceId).listInviteLinks();
6575
+ }
6576
+ /** Revoke an invite link by token. */
6577
+ async revokeInviteLink(spaceId, token) {
6578
+ await this.keyringFor(spaceId).revokeInviteLink(token);
6579
+ }
6580
+ /**
6581
+ * Join a space using an invite link: redeem the token (the keeper admits
6582
+ * this member), then connect and resolve once the group key arrives.
6583
+ * Returns the open {@link Space}.
6584
+ */
6585
+ async joinByInvite(spaceId, token, opts = {}) {
6586
+ const memberId = this.myId();
6587
+ const { ecdhPub, ecdsaPub } = await this.ensureIdentity(memberId);
6588
+ await this.keyringFor(spaceId).redeemInvite({ token, identityEcdhPub: ecdhPub, identityEcdsaPub: ecdsaPub });
6589
+ return this.joinSpace(spaceId, opts);
6590
+ }
6591
+ /** The current member roster of a space (member id → identity public keys). */
6592
+ async roster(spaceId) {
6593
+ return this.keyringFor(spaceId).fetchRoster();
6594
+ }
6595
+ keyringFor(spaceId) {
6596
+ return new KeyringClient({
6597
+ spaceId,
6598
+ httpBaseUrl: this.deps.http.baseUrl,
6599
+ fetch: this.deps.http.fetch,
6600
+ });
6601
+ }
6602
+ // -------------------------------------------------------------------------
6393
6603
  // Internals
6394
6604
  // -------------------------------------------------------------------------
6395
6605
  async build(spaceId, historyPolicy) {
@@ -6456,6 +6666,138 @@ class SpaceNamespace {
6456
6666
  }
6457
6667
  }
6458
6668
 
6669
+ /**
6670
+ * `client.agents` — manage an app's **Programmable Agents** (server-side
6671
+ * "virtual users" backed by Workers AI).
6672
+ *
6673
+ * const { config } = await client.agents.create(appId, {
6674
+ * handle: '@assistant', displayName: 'Assistant',
6675
+ * systemPrompt: 'You are a helpful teammate in this Space.',
6676
+ * triggers: [{ type: 'mention' }],
6677
+ * });
6678
+ * await client.agents.enable(appId, config.agentId, spaceId); // per-Space opt-in
6679
+ *
6680
+ * These are MANAGEMENT calls — the agent itself runs on the accelerator. Only an
6681
+ * app's owner or an editor of its shared Space may create/update/enable an agent
6682
+ * (viewers can read). Authorization rides the user session token; non-owner
6683
+ * editors pass their management Space via `opts.space`.
6684
+ *
6685
+ * An agent is "prompts + persona": a system prompt, a model, named prompt
6686
+ * `skills`, and `triggers` that decide when it speaks — plus optional `tools`
6687
+ * that let it act on the app (query/write its database, call its functions,
6688
+ * resolve its channels) via a server-side function-calling loop. See
6689
+ * {@link AgentToolsConfig}. Pair with the `@MuhkooAgent`/`@MuhkooSpace`/
6690
+ * `@MuhkooDB`/`@MuhkooFunction` decorators + `ejectAgentPrompt`/`ejectAgentTools`
6691
+ * to generate `systemPrompt` and `tools` straight from your app's surface.
6692
+ */
6693
+ class AgentsNamespace {
6694
+ deps;
6695
+ constructor(deps) {
6696
+ this.deps = deps;
6697
+ }
6698
+ /** List the app's agents. */
6699
+ async list(appId, opts = {}) {
6700
+ const res = await this.deps.http.get(this.path(appId, "", opts));
6701
+ return res.agents ?? [];
6702
+ }
6703
+ /** Read one agent (config + identity keys). */
6704
+ async get(appId, agentId, opts = {}) {
6705
+ return this.deps.http.get(this.path(appId, `/${encodeURIComponent(agentId)}`, opts));
6706
+ }
6707
+ /** Create an agent (owner/editor only). */
6708
+ async create(appId, input, opts = {}) {
6709
+ return this.deps.http.post(this.path(appId, "", {}), { ...input, space: opts.space });
6710
+ }
6711
+ /** Update an agent's editor-settable fields (owner/editor only). */
6712
+ async update(appId, agentId, patch, opts = {}) {
6713
+ return this.deps.http.patch(this.path(appId, `/${encodeURIComponent(agentId)}`, {}), { ...patch, space: opts.space });
6714
+ }
6715
+ /** Delete an agent (owner/editor only). */
6716
+ async delete(appId, agentId, opts = {}) {
6717
+ return this.deps.http.del(this.path(appId, `/${encodeURIComponent(agentId)}`, {}), { space: opts.space });
6718
+ }
6719
+ /** Enable the agent on a Space (per-Space opt-in). */
6720
+ async enable(appId, agentId, spaceId, opts = {}) {
6721
+ return this.deps.http.post(this.path(appId, `/${encodeURIComponent(agentId)}/enable`, {}), { targetSpaceId: spaceId, space: opts.space });
6722
+ }
6723
+ /** Disable the agent on a Space. */
6724
+ async disable(appId, agentId, spaceId, opts = {}) {
6725
+ return this.deps.http.post(this.path(appId, `/${encodeURIComponent(agentId)}/disable`, {}), { targetSpaceId: spaceId, space: opts.space });
6726
+ }
6727
+ path(appId, suffix, opts) {
6728
+ const q = opts.space ? `?space=${encodeURIComponent(opts.space)}` : "";
6729
+ return `/api/apps/${encodeURIComponent(appId)}/agents${suffix}${q}`;
6730
+ }
6731
+ }
6732
+
6733
+ /**
6734
+ * `client.functions` — manage an app's **serverless functions** (developer-
6735
+ * authored code that runs on the accelerator).
6736
+ *
6737
+ * const { config } = await client.functions.deploy(appId, {
6738
+ * name: 'hello', displayName: 'Hello',
6739
+ * code: `export default { fetch: () => new Response('hi') }`,
6740
+ * triggers: { http: { enabled: true } },
6741
+ * });
6742
+ * // HTTP-reachable at hello--<slug>.fns.muhkoo.dev
6743
+ * await client.functions.enable(appId, config.functionId, spaceId); // Space-bound
6744
+ *
6745
+ * These are MANAGEMENT calls — the function itself runs on the accelerator
6746
+ * (uploaded just-in-time, decrypted only at invocation). Only an app's owner or
6747
+ * an editor of its shared Space may deploy/update/enable (viewers read).
6748
+ * Authorization rides the user session token; non-owner editors pass their
6749
+ * management Space via `opts.space`.
6750
+ *
6751
+ * v1 is "compute-only": a function is an untrusted single-module ES worker
6752
+ * (`export default { fetch }`) with NO platform bindings — it reaches the
6753
+ * platform as an external client (the end-user session is passed through). Two
6754
+ * triggers: `http` (its own subdomain) and `space` (invoked on Space messages,
6755
+ * like a Programmable Agent).
6756
+ */
6757
+ class FunctionsNamespace {
6758
+ deps;
6759
+ constructor(deps) {
6760
+ this.deps = deps;
6761
+ }
6762
+ /** List the app's functions. */
6763
+ async list(appId, opts = {}) {
6764
+ const res = await this.deps.http.get(this.path(appId, "", opts));
6765
+ return res.functions ?? [];
6766
+ }
6767
+ /** Read one function's config. */
6768
+ async get(appId, functionId, opts = {}) {
6769
+ return this.deps.http.get(this.path(appId, `/${encodeURIComponent(functionId)}`, opts));
6770
+ }
6771
+ /** Read a function's decrypted source (owner/editor; for the editor UI). */
6772
+ async code(appId, functionId, opts = {}) {
6773
+ return this.deps.http.get(this.path(appId, `/${encodeURIComponent(functionId)}/code`, opts));
6774
+ }
6775
+ /** Deploy (create) a function (owner/editor only). */
6776
+ async deploy(appId, input, opts = {}) {
6777
+ return this.deps.http.post(this.path(appId, "", {}), { ...input, space: opts.space });
6778
+ }
6779
+ /** Update a function's editor-settable fields and/or redeploy its `code`. */
6780
+ async update(appId, functionId, patch, opts = {}) {
6781
+ return this.deps.http.patch(this.path(appId, `/${encodeURIComponent(functionId)}`, {}), { ...patch, space: opts.space });
6782
+ }
6783
+ /** Delete a function (owner/editor only). */
6784
+ async delete(appId, functionId, opts = {}) {
6785
+ return this.deps.http.del(this.path(appId, `/${encodeURIComponent(functionId)}`, {}), { space: opts.space });
6786
+ }
6787
+ /** Enable the function on a Space (per-Space opt-in for the `space` trigger). */
6788
+ async enable(appId, functionId, spaceId, opts = {}) {
6789
+ return this.deps.http.post(this.path(appId, `/${encodeURIComponent(functionId)}/enable`, {}), { targetSpaceId: spaceId, space: opts.space });
6790
+ }
6791
+ /** Disable the function on a Space. */
6792
+ async disable(appId, functionId, spaceId, opts = {}) {
6793
+ return this.deps.http.post(this.path(appId, `/${encodeURIComponent(functionId)}/disable`, {}), { targetSpaceId: spaceId, space: opts.space });
6794
+ }
6795
+ path(appId, suffix, opts) {
6796
+ const q = opts.space ? `?space=${encodeURIComponent(opts.space)}` : "";
6797
+ return `/api/apps/${encodeURIComponent(appId)}/functions${suffix}${q}`;
6798
+ }
6799
+ }
6800
+
6459
6801
  /**
6460
6802
  * `Client` — the single entry point to the Muhkoo SDK.
6461
6803
  *
@@ -6494,12 +6836,18 @@ class Client {
6494
6836
  auth;
6495
6837
  /** Per-user key/value storage — `client.kv.set(...)`, etc. */
6496
6838
  kv;
6839
+ /** App scalable database — `client.db.table('todos').query(...)`, etc. */
6840
+ db;
6497
6841
  /** File storage — `client.storage.writeFile(...)`, etc. */
6498
6842
  storage;
6499
6843
  /** Realtime messaging — `client.message.subscribe(...)`, etc. */
6500
6844
  message;
6501
6845
  /** Fan-out group spaces — `client.space.createSpace(...)`, etc. */
6502
6846
  space;
6847
+ /** Programmable Agents — `client.agents.create(appId, …)`, etc. */
6848
+ agents;
6849
+ /** Serverless functions — `client.functions.deploy(appId, …)`, etc. */
6850
+ functions;
6503
6851
  session;
6504
6852
  http;
6505
6853
  /** Subscribers notified when a session expires and can't be recovered. */
@@ -6527,6 +6875,7 @@ class Client {
6527
6875
  const wsBaseUrl = toWsBase(this.baseUrl);
6528
6876
  this.auth = new AuthNamespace({ auth: authClient, circuits, session: this.session });
6529
6877
  this.kv = new KvNamespace({ http: this.http, session: this.session, wsBaseUrl });
6878
+ this.db = new DbNamespace({ http: this.http });
6530
6879
  this.storage = new StorageNamespace({ http: this.http, baseUrl: this.baseUrl, kv: this.kv });
6531
6880
  this.message = new MessageNamespace({ http: this.http, session: this.session, wsBaseUrl });
6532
6881
  // Group keys are cached in the user's PersonalSpace (encrypted at rest
@@ -6542,6 +6891,8 @@ class Client {
6542
6891
  wsBaseUrl,
6543
6892
  cache: spaceKeyCache,
6544
6893
  });
6894
+ this.agents = new AgentsNamespace({ http: this.http });
6895
+ this.functions = new FunctionsNamespace({ http: this.http });
6545
6896
  }
6546
6897
  /** The currently signed-in user, or `null`. */
6547
6898
  get user() {
@@ -6595,6 +6946,177 @@ class Client {
6595
6946
  }
6596
6947
  }
6597
6948
 
6949
+ /**
6950
+ * Muhkoo app decorators — declare your app's agent-facing surface in code, then
6951
+ * "eject" a system prompt you hand to a Programmable Agent.
6952
+ *
6953
+ * You annotate a plain class with `@MuhkooAgent` (the app's identity + how the
6954
+ * agent should behave) plus per-surface decorators — `@MuhkooSpace`,
6955
+ * `@MuhkooDB`, `@MuhkooFunction` — that describe what each channel, table, and
6956
+ * function is *for*. {@link ejectAgentPrompt} reads those annotations and
6957
+ * produces a `systemPrompt` string:
6958
+ *
6959
+ * ```ts
6960
+ * @MuhkooAgent({
6961
+ * name: "Chat Assistant",
6962
+ * purpose: "A helpful assistant inside a real-time team chat.",
6963
+ * guidance: ["Keep replies short.", "Only chime in when relevant."],
6964
+ * })
6965
+ * class ChatAssistant {
6966
+ * @MuhkooSpace({ description: "Main team discussion." }) general!: string;
6967
+ * @MuhkooDB({ access: "read", description: "Chat message history." }) messages!: unknown;
6968
+ * @MuhkooFunction({ description: "Open a support ticket." }) openTicket!: () => void;
6969
+ * }
6970
+ *
6971
+ * const systemPrompt = ejectAgentPrompt(ChatAssistant);
6972
+ * await client.agents.create({ handle: "assistant", displayName: "Assistant", systemPrompt });
6973
+ * ```
6974
+ *
6975
+ * The ejected prompt owns the **semantic** layer — what the app is, how to act,
6976
+ * and what each surface means. The Muhkoo runtime separately appends the
6977
+ * **authoritative** roster (exact columns, function parameters, and the closed
6978
+ * list of callable tools), so the prompt here never needs to restate schema and
6979
+ * can't drift from it. Decorators only describe; they never change runtime
6980
+ * behavior, so they're safe to apply to a throwaway descriptor class.
6981
+ *
6982
+ * Requires `experimentalDecorators` (already enabled in this package). Metadata
6983
+ * is captured in a per-class registry — no `reflect-metadata` dependency.
6984
+ */
6985
+ // Per-class metadata store, keyed by the class constructor. Member decorators
6986
+ // run during class-body evaluation (before the class decorator), so each
6987
+ // decorator get-or-creates the descriptor and they all converge on one entry.
6988
+ const REGISTRY = new WeakMap();
6989
+ function descriptorFor(ctor) {
6990
+ let d = REGISTRY.get(ctor);
6991
+ if (!d) {
6992
+ d = { spaces: [], tables: [], functions: [] };
6993
+ REGISTRY.set(ctor, d);
6994
+ }
6995
+ return d;
6996
+ }
6997
+ /** Resolve the owning class constructor from a decorator `target` — the
6998
+ * prototype for instance members, the constructor itself for static ones. */
6999
+ function ctorOf(target) {
7000
+ return typeof target === "function" ? target : target.constructor;
7001
+ }
7002
+ /**
7003
+ * Class decorator. Declares the app's agent identity, purpose, and behavioral
7004
+ * guidance. Apply it to the class whose members carry the surface decorators.
7005
+ */
7006
+ function MuhkooAgent(meta) {
7007
+ return (target) => {
7008
+ descriptorFor(target).agent = meta;
7009
+ };
7010
+ }
7011
+ /** Member decorator. Marks a channel/Space the agent can resolve and post to. */
7012
+ function MuhkooSpace(meta) {
7013
+ return ((target, propertyKey) => {
7014
+ descriptorFor(ctorOf(target)).spaces.push({
7015
+ name: meta.name ?? String(propertyKey),
7016
+ description: meta.description,
7017
+ });
7018
+ });
7019
+ }
7020
+ /** Member decorator. Marks an app database table the agent can query/mutate. */
7021
+ function MuhkooDB(meta) {
7022
+ return ((target, propertyKey) => {
7023
+ descriptorFor(ctorOf(target)).tables.push({
7024
+ table: meta.table ?? String(propertyKey),
7025
+ access: meta.access ?? "read",
7026
+ description: meta.description,
7027
+ });
7028
+ });
7029
+ }
7030
+ /** Member decorator. Marks a serverless function the agent can call. */
7031
+ function MuhkooFunction(meta) {
7032
+ return ((target, propertyKey) => {
7033
+ descriptorFor(ctorOf(target)).functions.push({
7034
+ name: meta.name ?? String(propertyKey),
7035
+ description: meta.description,
7036
+ });
7037
+ });
7038
+ }
7039
+ /** Read back the raw descriptor a class accumulated (for tooling/tests). */
7040
+ function getMuhkooAppDescriptor(appClass) {
7041
+ return REGISTRY.get(appClass);
7042
+ }
7043
+ /**
7044
+ * Compose the agent system prompt from a `@MuhkooAgent`-decorated class. Throws
7045
+ * if the class isn't decorated. The result is the semantic layer only — the
7046
+ * runtime appends exact schema + the closed tool list at invocation time.
7047
+ */
7048
+ function ejectAgentPrompt(appClass) {
7049
+ const d = REGISTRY.get(appClass);
7050
+ if (!d || !d.agent) {
7051
+ throw new Error("ejectAgentPrompt: the class must be decorated with @MuhkooAgent");
7052
+ }
7053
+ const { agent } = d;
7054
+ const lines = [];
7055
+ lines.push(`You are ${agent.name}. ${agent.purpose}`.trim());
7056
+ if (agent.guidance?.length) {
7057
+ lines.push("", "Guidance:");
7058
+ for (const g of agent.guidance)
7059
+ lines.push(`- ${g}`);
7060
+ }
7061
+ if (d.spaces.length) {
7062
+ lines.push("", "Channels (Spaces) you can resolve with resolve_channel and post messages into:");
7063
+ for (const s of d.spaces)
7064
+ lines.push(`- ${s.name}: ${s.description}`);
7065
+ }
7066
+ if (d.tables.length) {
7067
+ lines.push("", "App database tables:");
7068
+ for (const t of d.tables) {
7069
+ const verbs = t.access === "write"
7070
+ ? "read & write — db_query/db_get/db_insert/db_update/db_delete"
7071
+ : "read-only — db_query/db_get";
7072
+ lines.push(`- ${t.table} (${verbs}): ${t.description}`);
7073
+ }
7074
+ }
7075
+ if (d.functions.length) {
7076
+ lines.push("", "Functions you can invoke with call_function:");
7077
+ for (const f of d.functions)
7078
+ lines.push(`- ${f.name}: ${f.description}`);
7079
+ }
7080
+ if (agent.instructions?.trim()) {
7081
+ lines.push("", agent.instructions.trim());
7082
+ }
7083
+ // How to respond — the load-bearing behavioral contract. Without this, models
7084
+ // (especially gpt-oss) tend to run tool calls and then end their turn with no
7085
+ // user-facing message, so the user sees the work happen but gets no reply.
7086
+ const hasTools = d.tables.length > 0 || d.functions.length > 0;
7087
+ lines.push("", "How to respond:");
7088
+ if (hasTools) {
7089
+ 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.");
7090
+ }
7091
+ 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.");
7092
+ 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.");
7093
+ return lines.join("\n");
7094
+ }
7095
+ /**
7096
+ * Derive a suggested tools allowlist from a decorated class — the db tables
7097
+ * (and whether any are writable), the callable functions, and whether channels
7098
+ * are in play. Pair with {@link ejectAgentPrompt} when configuring the agent.
7099
+ *
7100
+ * If any tables/functions are present the result has `enabled: true`; to
7101
+ * actually grant tools you must also pass a function-calling `model` in the same
7102
+ * `client.agents.create`/`update` call (the server enforces this).
7103
+ */
7104
+ function ejectAgentTools(appClass, opts) {
7105
+ const d = REGISTRY.get(appClass);
7106
+ if (!d) {
7107
+ throw new Error("ejectAgentTools: the class must be decorated with @MuhkooAgent");
7108
+ }
7109
+ const anyWrite = d.tables.some((t) => t.access === "write");
7110
+ const mode = d.tables.length === 0 ? "off" : anyWrite ? "write" : "read";
7111
+ return {
7112
+ enabled: d.tables.length > 0 || d.functions.length > 0 || d.spaces.length > 0,
7113
+ db: { mode, tables: d.tables.map((t) => t.table) },
7114
+ functions: d.functions.map((f) => f.name),
7115
+ channels: d.spaces.length > 0,
7116
+ maxIterations: opts?.maxIterations ?? 6,
7117
+ };
7118
+ }
7119
+
6598
7120
  const appLogger$1 = new Logger("connect", 'ERROR');
6599
7121
  globalThis.appLogger = appLogger$1;
6600
7122
 
@@ -7131,5 +7653,5 @@ var bundledLoader = /*#__PURE__*/Object.freeze({
7131
7653
  default: loadRsWasm
7132
7654
  });
7133
7655
 
7134
- 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, Logger, MemorySessionStore, Message, Message as MessageDefault, MessageNamespace, PBKDF2_DEFAULT_ITERATIONS, PREIMAGE_POK_VERIFICATION_KEY, Packet, PersonalSpaceClient, Poseidon, PreimagePoK, Ready, ReedSolomonCodec, Retry, Space as Room, SerializeMessage, SessionState, ShardClient, SharedSpaceClient, Space, SpaceKeyring, SpaceNamespace, SpacePacketCipher, StorageNamespace, WSTransport, ZkAuth, _accountId, _formatBytes, _messageId, _objectId, _packetId, _socketId, _userId, appLogger$1 as appLogger, assertType, base58Decode, base58Encode, buildCommitment, compilePrograms, concatBytes, decodeFromHex, decorators, decryptAesGcm, defaultCircuitUrls, defaultSessionStore, deriveAesKeyFromPassphrase, deriveBitsHkdf, deriveIdentity, deserialize, encodeToHex, encryptAesGcm, exportPublicKeyBase64, exportPublicKeyHex, fromBase64, fromBase64Url, fromHex, generateAuthProof, generateChecksum, generateId, getIPAddress, getId, getSubtle, initBn128Wasm, initRsWasm, initializeCircuits, isNumber, log, manifestToStat, poseidonHash, quickVerify, randomBytes, serialize, shardHash, signMessage, toBase64, toBase64Url, toField, toHex, unwrapWithPassphrase, utf8Decode, utf8Encode, verify, verifyChecksum, verifyGroth16, verifyHashKnowledge, verifyPreimagePoK, wrapWithPassphrase };
7656
+ 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, DbNamespace, DbTable, DoubleRatchet, DoubleRatchetCipher, DoubleRatchetManager, EncryptedSession, EventCore, EventCoreEvents, Field, Field as FieldElement, FileStorage, FunctionsNamespace, HashKnowledge, HttpClient, HttpError, KEEPER_MEMBER_ID, KeyStore, KeyringClient, KvNamespace, LocalStorageSessionStore, Logger, MemorySessionStore, Message, Message as MessageDefault, MessageNamespace, MuhkooAgent, MuhkooDB, MuhkooFunction, MuhkooSpace, PBKDF2_DEFAULT_ITERATIONS, PREIMAGE_POK_VERIFICATION_KEY, Packet, PersonalSpaceClient, Poseidon, PreimagePoK, Ready, ReedSolomonCodec, Retry, Space as Room, SerializeMessage, SessionState, ShardClient, SharedSpaceClient, Space, SpaceKeyring, SpaceNamespace, SpacePacketCipher, StorageNamespace, WSTransport, ZkAuth, _accountId, _formatBytes, _messageId, _objectId, _packetId, _socketId, _userId, appLogger$1 as appLogger, assertType, base58Decode, base58Encode, buildCommitment, compilePrograms, concatBytes, decodeFromHex, decorators, decryptAesGcm, defaultCircuitUrls, defaultSessionStore, deriveAesKeyFromPassphrase, deriveBitsHkdf, deriveIdentity, deserialize, ejectAgentPrompt, ejectAgentTools, encodeToHex, encryptAesGcm, exportPublicKeyBase64, exportPublicKeyHex, fromBase64, fromBase64Url, fromHex, generateAuthProof, generateChecksum, generateId, getIPAddress, getId, getMuhkooAppDescriptor, getSubtle, initBn128Wasm, initRsWasm, initializeCircuits, isNumber, log, manifestToStat, poseidonHash, quickVerify, randomBytes, serialize, shardHash, signMessage, toBase64, toBase64Url, toField, toHex, unwrapWithPassphrase, utf8Decode, utf8Encode, verify, verifyChecksum, verifyGroth16, verifyHashKnowledge, verifyPreimagePoK, wrapWithPassphrase };
7135
7657
  //# sourceMappingURL=index.js.map