@possibl/rcrt-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/CHANGELOG.md +56 -0
- package/README.md +154 -0
- package/package.json +54 -0
- package/src/auth.ts +56 -0
- package/src/authn.ts +159 -0
- package/src/breadcrumbs.ts +111 -0
- package/src/capabilities.ts +93 -0
- package/src/cards.ts +110 -0
- package/src/chat.ts +83 -0
- package/src/client.ts +97 -0
- package/src/errors.ts +101 -0
- package/src/files.ts +135 -0
- package/src/grants.ts +99 -0
- package/src/index.ts +103 -0
- package/src/internal/fetch.ts +133 -0
- package/src/internal/sse.ts +236 -0
- package/src/sessions.ts +110 -0
- package/src/types/breadcrumb.ts +77 -0
- package/src/types/card.ts +298 -0
- package/src/types/index.ts +2 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to `@possibl/rcrt-sdk` are documented here. The
|
|
4
|
+
format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/);
|
|
5
|
+
this package follows [SemVer](https://semver.org/spec/v2.0.0.html) once
|
|
6
|
+
published.
|
|
7
|
+
|
|
8
|
+
## [0.1.0] — 2026-04-30
|
|
9
|
+
|
|
10
|
+
First public release. Drops the `-alpha.1` suffix; signals the SDK is
|
|
11
|
+
ready for frontend consumption (canonical RCRT mobile + web template
|
|
12
|
+
imports it directly).
|
|
13
|
+
|
|
14
|
+
### Added — modules
|
|
15
|
+
|
|
16
|
+
- **`FilesModule`** (`client.files`) — multipart upload, list, get,
|
|
17
|
+
signed-URL download, server-side text extraction, soft delete.
|
|
18
|
+
Defaults to `tenant` scope; supports `org` / `user` for cross-
|
|
19
|
+
workspace assets.
|
|
20
|
+
- **`SessionsModule`** (`client.sessions`) — `getConstellation()`
|
|
21
|
+
graph view of related sessions, `listParticipants` /
|
|
22
|
+
`addParticipant` / `removeParticipant` for multi-agent sessions.
|
|
23
|
+
- **`CapabilitiesModule`** (`client.capabilities`) — `list(type)` for
|
|
24
|
+
tools / agents / services / knowledge, plus `getChattableAgents()`
|
|
25
|
+
helper that filters `interpret:promptable` to entries tagged
|
|
26
|
+
`interface:chat` / `interface:chat-default`.
|
|
27
|
+
- **`IdentityModule.getUserProfile` / `updateUserProfile`** — workspace-
|
|
28
|
+
scoped editable profile breadcrumb. Returns `null` (instead of
|
|
29
|
+
throwing) when no profile has been written yet.
|
|
30
|
+
- **`GrantsModule.resolveService(name)`** — server-side credential
|
|
31
|
+
resolution for tools that need to act on behalf of a workspace's
|
|
32
|
+
active service grant.
|
|
33
|
+
|
|
34
|
+
### Notes
|
|
35
|
+
|
|
36
|
+
- `IdentityModule.listPendingInvitations` / `acceptInvitation` /
|
|
37
|
+
`declineInvitation` were already shipped in the alpha — no change.
|
|
38
|
+
- Global-events SSE (`/v1/events`) is exposed as `chat.globalStream`
|
|
39
|
+
rather than its own module — an extra module would have meant a
|
|
40
|
+
duplicate config builder.
|
|
41
|
+
- Tenant admin endpoints (members, resource overrides) deliberately
|
|
42
|
+
out of scope for `0.1.0`. Will land in a `0.2.x` once a consumer
|
|
43
|
+
needs them.
|
|
44
|
+
|
|
45
|
+
### Dependencies
|
|
46
|
+
|
|
47
|
+
- No runtime deps. The SDK is fetch + EventSource + plain TS. Bring
|
|
48
|
+
your own `EventSource` polyfill on React Native (see README).
|
|
49
|
+
|
|
50
|
+
### Compatibility
|
|
51
|
+
|
|
52
|
+
- Node 18+ (native fetch + WHATWG URL).
|
|
53
|
+
- Browsers with `fetch`, `URL`, `URLSearchParams`, `EventSource`.
|
|
54
|
+
- React Native — pass an `eventSource` constructor in
|
|
55
|
+
`chat.sessionStream({ eventSource })` and bring a fetch polyfill if
|
|
56
|
+
using a runtime older than RN 0.74.
|
package/README.md
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
# @possibl/rcrt-sdk
|
|
2
|
+
|
|
3
|
+
TypeScript client for the [RCRT](https://github.com/possibl-ai/rcrt-v2)
|
|
4
|
+
multi-tenant AI BaaS. Isomorphic — works in browsers, Node 18+, and
|
|
5
|
+
React Native with a small polyfill.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install @possibl/rcrt-sdk
|
|
11
|
+
# or pnpm / yarn / bun
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Quickstart
|
|
15
|
+
|
|
16
|
+
```ts
|
|
17
|
+
import { RcrtClient, staticTokenProvider } from '@possibl/rcrt-sdk';
|
|
18
|
+
|
|
19
|
+
const rcrt = new RcrtClient({
|
|
20
|
+
apiUrl: 'https://rcrt-api-gateway-<hash>.run.app',
|
|
21
|
+
tokenProvider: staticTokenProvider(process.env.WORKSPACE_API_KEY!),
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
// 1. Resolve workspace (for tests — UI apps go through Firebase):
|
|
25
|
+
const { tenants } = await rcrt.auth.me().then((m) => ({ tenants: m.tenants }));
|
|
26
|
+
rcrt.setTenantId(tenants[0].id);
|
|
27
|
+
|
|
28
|
+
// 2. Send a chat:
|
|
29
|
+
const { session_id } = await rcrt.chat.send({
|
|
30
|
+
message: 'Summarise my inbox',
|
|
31
|
+
target_agent: 'life-coordinator',
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
// 3. Stream the reply:
|
|
35
|
+
const stream = rcrt.chat.sessionStream(session_id, {
|
|
36
|
+
onDelta: ({ delta }) => process.stdout.write(delta),
|
|
37
|
+
onMessage: (bc) => {
|
|
38
|
+
if (bc.tags.includes('interpret:pending-action')) {
|
|
39
|
+
const card = extractCard(bc);
|
|
40
|
+
console.log('card', card?.layout, card?.header.title);
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
// 4. When the user taps an action on a card:
|
|
46
|
+
await rcrt.cards.resolve(cardBreadcrumbId, { status: 'approved' });
|
|
47
|
+
|
|
48
|
+
stream.close();
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Browser + Firebase
|
|
52
|
+
|
|
53
|
+
```ts
|
|
54
|
+
import { getAuth } from 'firebase/auth';
|
|
55
|
+
import { RcrtClient, TokenProvider } from '@possibl/rcrt-sdk';
|
|
56
|
+
|
|
57
|
+
const auth = getAuth();
|
|
58
|
+
const tokenProvider: TokenProvider = {
|
|
59
|
+
async getIdToken() {
|
|
60
|
+
const user = auth.currentUser;
|
|
61
|
+
if (!user) throw new Error('not signed in');
|
|
62
|
+
return user.getIdToken(false);
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const rcrt = new RcrtClient({
|
|
67
|
+
apiUrl: import.meta.env.VITE_RCRT_API,
|
|
68
|
+
tokenProvider,
|
|
69
|
+
});
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## React Native
|
|
73
|
+
|
|
74
|
+
React Native has no built-in `EventSource`. Install a polyfill and
|
|
75
|
+
pass it through:
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
npm install react-native-sse
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
```ts
|
|
82
|
+
import EventSource from 'react-native-sse';
|
|
83
|
+
|
|
84
|
+
const stream = rcrt.chat.sessionStream(sessionId, handlers, {
|
|
85
|
+
eventSource: EventSource,
|
|
86
|
+
useHeaderAuth: true, // RN polyfill supports custom headers
|
|
87
|
+
});
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## What's exported
|
|
91
|
+
|
|
92
|
+
| Symbol | Purpose |
|
|
93
|
+
| --- | --- |
|
|
94
|
+
| `RcrtClient` | The client. One instance per workspace session. |
|
|
95
|
+
| `TokenProvider` | Interface — wire any IdP. |
|
|
96
|
+
| `staticTokenProvider` | Static-token convenience (tests, `tk_*` workspace keys). |
|
|
97
|
+
| `ApiError`, `SdkError` | Error classes. Typed + discriminable by `.status` and `.detail.code`. |
|
|
98
|
+
| `Breadcrumb`, `Card`, `Row`, `ChartSpec`, ... | Every public type from the RCRT contract. Re-exportable as your UI types. |
|
|
99
|
+
|
|
100
|
+
Browse `src/index.ts` for the full re-export list.
|
|
101
|
+
|
|
102
|
+
### Module surface on `RcrtClient`
|
|
103
|
+
|
|
104
|
+
```ts
|
|
105
|
+
const rcrt = new RcrtClient({ apiUrl, tokenProvider });
|
|
106
|
+
|
|
107
|
+
rcrt.auth // identity, tenants, invitations, user profile
|
|
108
|
+
rcrt.breadcrumbs // CRUD + tag query + semantic search
|
|
109
|
+
rcrt.chat // send + sessionStream (per-session SSE) + globalStream (/v1/events)
|
|
110
|
+
rcrt.cards // JIT-UI card resolve + helpers
|
|
111
|
+
rcrt.grants // OAuth grants + initiate + resolveService
|
|
112
|
+
rcrt.files // upload / list / get / signed download / text extract / delete
|
|
113
|
+
rcrt.sessions // constellation + participants
|
|
114
|
+
rcrt.capabilities // tools / agents / services / knowledge discovery
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
## Design notes
|
|
118
|
+
|
|
119
|
+
- **Isomorphic**: zero DOM or Node-only APIs in the core. The only
|
|
120
|
+
platform-touching code is the SSE client, and it takes an
|
|
121
|
+
`EventSource` constructor through config.
|
|
122
|
+
- **Token provider is the seam**: the SDK never touches Firebase /
|
|
123
|
+
Auth0 / Clerk directly. You bring a `TokenProvider` and it asks.
|
|
124
|
+
- **Everything is a breadcrumb**: `rcrt.breadcrumbs.*` is the
|
|
125
|
+
fundamental CRUD surface; all other modules build on it. `cards`,
|
|
126
|
+
`grants`, `auth` are conveniences.
|
|
127
|
+
- **Optimistic locking just works**: `breadcrumbs.update(id, req, {
|
|
128
|
+
autoRetryConflict: true })` and `cards.resolve(...)` both refetch
|
|
129
|
+
and retry on 409.
|
|
130
|
+
- **Errors are normalised**: the backend is mid-migration from
|
|
131
|
+
`{"error": "string"}` to a typed `{"error": {"code", "message",
|
|
132
|
+
"details"}}` envelope. `ApiError.detail.code` is set either way.
|
|
133
|
+
|
|
134
|
+
## Matching docs
|
|
135
|
+
|
|
136
|
+
Full narrative + concepts + operations playbook in
|
|
137
|
+
[`packages/docs/`](../docs/). Start with:
|
|
138
|
+
|
|
139
|
+
- [`docs/guides/01-quickstart.md`](../docs/guides/01-quickstart.md)
|
|
140
|
+
- [`docs/concepts/05-jit-ui.md`](../docs/concepts/05-jit-ui.md)
|
|
141
|
+
- [`docs/openapi/v1.yaml`](../docs/openapi/v1.yaml)
|
|
142
|
+
|
|
143
|
+
## Status
|
|
144
|
+
|
|
145
|
+
**0.1.0** — first public release. Used by `possibl-ai/rcrt-template-mobile`
|
|
146
|
+
and downstream apps (e.g. `possibl-ai/ritual-app`, `possibl-ai/properlii-mobile`)
|
|
147
|
+
as their canonical RCRT client. Surface stable; minor adjustments
|
|
148
|
+
possible before 1.0.
|
|
149
|
+
|
|
150
|
+
See [`CHANGELOG.md`](./CHANGELOG.md) for what's new in this release.
|
|
151
|
+
|
|
152
|
+
## Licence
|
|
153
|
+
|
|
154
|
+
MIT.
|
package/package.json
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@possibl/rcrt-sdk",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "TypeScript SDK for the RCRT multi-tenant AI BaaS — isomorphic (web + node + React Native)",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./src/index.ts",
|
|
7
|
+
"types": "./src/index.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./src/index.ts",
|
|
11
|
+
"default": "./src/index.ts"
|
|
12
|
+
},
|
|
13
|
+
"./types": {
|
|
14
|
+
"types": "./src/types/index.ts",
|
|
15
|
+
"default": "./src/types/index.ts"
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
"files": [
|
|
19
|
+
"src",
|
|
20
|
+
"README.md",
|
|
21
|
+
"CHANGELOG.md"
|
|
22
|
+
],
|
|
23
|
+
"keywords": [
|
|
24
|
+
"rcrt",
|
|
25
|
+
"ai",
|
|
26
|
+
"baas",
|
|
27
|
+
"agents",
|
|
28
|
+
"jit-ui",
|
|
29
|
+
"typescript",
|
|
30
|
+
"sdk"
|
|
31
|
+
],
|
|
32
|
+
"author": "Possibl (https://possibl.ai)",
|
|
33
|
+
"license": "MIT",
|
|
34
|
+
"repository": {
|
|
35
|
+
"type": "git",
|
|
36
|
+
"url": "https://github.com/possibl-ai/rcrt-v2",
|
|
37
|
+
"directory": "packages/rcrt-sdk"
|
|
38
|
+
},
|
|
39
|
+
"homepage": "https://github.com/possibl-ai/rcrt-v2/tree/development/packages/docs",
|
|
40
|
+
"bugs": "https://github.com/possibl-ai/rcrt-v2/issues",
|
|
41
|
+
"engines": {
|
|
42
|
+
"node": ">=18"
|
|
43
|
+
},
|
|
44
|
+
"scripts": {
|
|
45
|
+
"type-check": "tsc --noEmit && tsc --noEmit -p tsconfig.tests.json",
|
|
46
|
+
"build": "tsc",
|
|
47
|
+
"test": "tsx --test tests/*.test.ts"
|
|
48
|
+
},
|
|
49
|
+
"devDependencies": {
|
|
50
|
+
"@types/node": "^20.0.0",
|
|
51
|
+
"tsx": "^4.19.2",
|
|
52
|
+
"typescript": "~5.6.0"
|
|
53
|
+
}
|
|
54
|
+
}
|
package/src/auth.ts
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TokenProvider — the seam between RCRT and your identity source.
|
|
3
|
+
*
|
|
4
|
+
* The SDK never talks to Firebase / Auth0 / Clerk directly. You wire
|
|
5
|
+
* up whichever IdP you use by implementing this interface once. On
|
|
6
|
+
* every request the SDK calls `getIdToken()` to get a fresh bearer.
|
|
7
|
+
*
|
|
8
|
+
* The provider owns refresh. If your IdP supports it (Firebase does),
|
|
9
|
+
* it should return a token that's valid for at least the next ~60s;
|
|
10
|
+
* the SDK doesn't track expiry itself.
|
|
11
|
+
*
|
|
12
|
+
* Example Firebase (web):
|
|
13
|
+
*
|
|
14
|
+
* import { getAuth } from 'firebase/auth';
|
|
15
|
+
* const auth = getAuth();
|
|
16
|
+
* const tokenProvider: TokenProvider = {
|
|
17
|
+
* async getIdToken() {
|
|
18
|
+
* const u = auth.currentUser;
|
|
19
|
+
* if (!u) throw new SdkError('NOT_SIGNED_IN', 'Sign in first');
|
|
20
|
+
* return u.getIdToken(false);
|
|
21
|
+
* },
|
|
22
|
+
* };
|
|
23
|
+
*
|
|
24
|
+
* Example test stub:
|
|
25
|
+
*
|
|
26
|
+
* const tokenProvider: TokenProvider = {
|
|
27
|
+
* async getIdToken() { return 'tk_test_workspace_api_key'; },
|
|
28
|
+
* };
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
import { SdkError } from './errors.js';
|
|
32
|
+
|
|
33
|
+
export interface TokenProvider {
|
|
34
|
+
/**
|
|
35
|
+
* Return a bearer token for the next request. Throws or rejects if
|
|
36
|
+
* the user isn't signed in / the key is missing.
|
|
37
|
+
*
|
|
38
|
+
* Implementations SHOULD refresh transparently if they know the
|
|
39
|
+
* token is close to expiry. The SDK calls this per-request and
|
|
40
|
+
* doesn't cache.
|
|
41
|
+
*/
|
|
42
|
+
getIdToken(): Promise<string>;
|
|
43
|
+
|
|
44
|
+
/** Optional hook: called on 401. Implementations can force a refresh. */
|
|
45
|
+
onUnauthorized?(): Promise<void>;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Convenience: a provider that returns a static string. For workspace API keys or tests. */
|
|
49
|
+
export function staticTokenProvider(token: string): TokenProvider {
|
|
50
|
+
if (!token) throw new SdkError('EMPTY_TOKEN', 'staticTokenProvider called with empty token');
|
|
51
|
+
return {
|
|
52
|
+
async getIdToken() {
|
|
53
|
+
return token;
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
}
|
package/src/authn.ts
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Identity module — `/v1/auth/*` endpoints.
|
|
3
|
+
*
|
|
4
|
+
* Distinct from the TokenProvider abstraction: this is the server-side
|
|
5
|
+
* identity surface that runs _after_ you've presented a bearer token.
|
|
6
|
+
* See `packages/docs/guides/02-auth.md`.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { FetchContext } from './internal/fetch.js';
|
|
10
|
+
import { request } from './internal/fetch.js';
|
|
11
|
+
|
|
12
|
+
export interface MeResponse {
|
|
13
|
+
user: {
|
|
14
|
+
id: string;
|
|
15
|
+
email: string;
|
|
16
|
+
name?: string;
|
|
17
|
+
picture?: string;
|
|
18
|
+
};
|
|
19
|
+
is_platform_admin: boolean;
|
|
20
|
+
organizations: Array<{ id: string; name: string; role: string }>;
|
|
21
|
+
tenants: Array<{ id: string; name: string; role: string }>;
|
|
22
|
+
active_tenant?: { id: string; name: string; role: string };
|
|
23
|
+
permissions?: string[];
|
|
24
|
+
grants?: unknown[];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface Tenant {
|
|
28
|
+
id: string;
|
|
29
|
+
name: string;
|
|
30
|
+
org_id?: string;
|
|
31
|
+
role?: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface PendingInvitation {
|
|
35
|
+
id: string;
|
|
36
|
+
org_id?: string | null;
|
|
37
|
+
tenant_id?: string | null;
|
|
38
|
+
email: string;
|
|
39
|
+
role: string;
|
|
40
|
+
status: 'pending' | 'accepted' | 'declined' | 'expired' | 'cancelled';
|
|
41
|
+
expires_at: string;
|
|
42
|
+
invited_by?: { id: string; name?: string };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export class IdentityModule {
|
|
46
|
+
constructor(private readonly ctx: FetchContext) {}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* `GET /v1/auth/me` — returns identity + all accessible workspaces.
|
|
50
|
+
*
|
|
51
|
+
* Creates the user row on first sign-in. Call this before any other
|
|
52
|
+
* RCRT endpoint for brand-new Firebase users.
|
|
53
|
+
*/
|
|
54
|
+
async me(): Promise<MeResponse> {
|
|
55
|
+
return request<MeResponse>(this.ctx, '/v1/auth/me', { skipTenant: true });
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** List workspaces the current user is a member of. */
|
|
59
|
+
async listTenants(): Promise<Tenant[]> {
|
|
60
|
+
const res = await request<Tenant[] | { tenants: Tenant[] }>(this.ctx, '/v1/auth/tenants', {
|
|
61
|
+
skipTenant: true,
|
|
62
|
+
});
|
|
63
|
+
return Array.isArray(res) ? res : (res.tenants ?? []);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Confirm workspace membership. Client code is still responsible for setting X-Tenant-ID. */
|
|
67
|
+
async selectTenant(tenantId: string): Promise<void> {
|
|
68
|
+
await request<void>(this.ctx, `/v1/auth/tenants/${tenantId}/select`, {
|
|
69
|
+
method: 'POST',
|
|
70
|
+
skipTenant: true,
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** `POST /v1/auth/logout` — server-side no-op; included for symmetry + audit. */
|
|
75
|
+
async logout(): Promise<void> {
|
|
76
|
+
await request<void>(this.ctx, '/v1/auth/logout', {
|
|
77
|
+
method: 'POST',
|
|
78
|
+
skipTenant: true,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* `POST /v1/auth/delete-account` — cascading account deletion.
|
|
84
|
+
*
|
|
85
|
+
* **NOT YET ON `development`** at publish time — pending a follow-up
|
|
86
|
+
* PR. The SDK method exists so consumer code can compile against the
|
|
87
|
+
* intended shape; invoking it against a gateway that lacks the
|
|
88
|
+
* handler returns 404.
|
|
89
|
+
*/
|
|
90
|
+
async deleteAccount(): Promise<void> {
|
|
91
|
+
await request<void>(this.ctx, '/v1/auth/delete-account', {
|
|
92
|
+
method: 'POST',
|
|
93
|
+
skipTenant: true,
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Pending invitations keyed by the caller's email. */
|
|
98
|
+
async listPendingInvitations(): Promise<PendingInvitation[]> {
|
|
99
|
+
const res = await request<
|
|
100
|
+
PendingInvitation[] | { invitations: PendingInvitation[]; total?: number }
|
|
101
|
+
>(this.ctx, '/v1/invitations/pending');
|
|
102
|
+
return Array.isArray(res) ? res : (res.invitations ?? []);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async acceptInvitation(token: string): Promise<void> {
|
|
106
|
+
await request<void>(this.ctx, '/v1/invitations/accept', {
|
|
107
|
+
method: 'POST',
|
|
108
|
+
body: { token },
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async declineInvitation(invitationId: string): Promise<void> {
|
|
113
|
+
await request<void>(this.ctx, `/v1/invitations/${invitationId}/decline`, {
|
|
114
|
+
method: 'POST',
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* `GET /v1/user/profile` — workspace-scoped user profile breadcrumb.
|
|
120
|
+
*
|
|
121
|
+
* Distinct from `me()` — this is the user's editable profile content
|
|
122
|
+
* (name, timezone, preferences) rather than their identity envelope.
|
|
123
|
+
* Returns `null` when no profile has been written yet (404 swallowed).
|
|
124
|
+
*/
|
|
125
|
+
async getUserProfile(): Promise<UserProfileBreadcrumb | null> {
|
|
126
|
+
try {
|
|
127
|
+
return await request<UserProfileBreadcrumb>(this.ctx, '/v1/user/profile');
|
|
128
|
+
} catch (err) {
|
|
129
|
+
// 404 is the no-profile-yet case — caller renders the onboarding flow.
|
|
130
|
+
if (err && typeof err === 'object' && 'status' in err && (err as { status: number }).status === 404) {
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
throw err;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* `PUT /v1/user/profile` — upsert. Body fields land on the
|
|
139
|
+
* underlying breadcrumb's `content` payload.
|
|
140
|
+
*/
|
|
141
|
+
async updateUserProfile(
|
|
142
|
+
patch: Record<string, unknown>,
|
|
143
|
+
): Promise<UserProfileBreadcrumb> {
|
|
144
|
+
return request<UserProfileBreadcrumb>(this.ctx, '/v1/user/profile', {
|
|
145
|
+
method: 'PUT',
|
|
146
|
+
body: patch,
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export interface UserProfileBreadcrumb {
|
|
152
|
+
id: string;
|
|
153
|
+
title?: string;
|
|
154
|
+
content: Record<string, unknown>;
|
|
155
|
+
tags: string[];
|
|
156
|
+
version: number;
|
|
157
|
+
created_at?: string;
|
|
158
|
+
updated_at?: string;
|
|
159
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Breadcrumbs module — CRUD + tag query + semantic search.
|
|
3
|
+
*
|
|
4
|
+
* See `packages/docs/guides/04-breadcrumbs.md` for the narrative
|
|
5
|
+
* description of each operation.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { FetchContext } from './internal/fetch.js';
|
|
9
|
+
import { request } from './internal/fetch.js';
|
|
10
|
+
import type {
|
|
11
|
+
Breadcrumb,
|
|
12
|
+
BreadcrumbResponse,
|
|
13
|
+
CreateBreadcrumbRequest,
|
|
14
|
+
UpdateBreadcrumbRequest,
|
|
15
|
+
QueryByTagsOptions,
|
|
16
|
+
SemanticSearchOptions,
|
|
17
|
+
} from './types/breadcrumb.js';
|
|
18
|
+
import { ApiError, SdkError } from './errors.js';
|
|
19
|
+
|
|
20
|
+
export class BreadcrumbsModule {
|
|
21
|
+
constructor(private readonly ctx: FetchContext) {}
|
|
22
|
+
|
|
23
|
+
/** `POST /v1/breadcrumbs` */
|
|
24
|
+
async create(req: CreateBreadcrumbRequest): Promise<Breadcrumb> {
|
|
25
|
+
const res = await request<BreadcrumbResponse | Breadcrumb>(this.ctx, '/v1/breadcrumbs', {
|
|
26
|
+
method: 'POST',
|
|
27
|
+
body: req,
|
|
28
|
+
});
|
|
29
|
+
return 'breadcrumb' in (res as BreadcrumbResponse)
|
|
30
|
+
? (res as BreadcrumbResponse).breadcrumb
|
|
31
|
+
: (res as Breadcrumb);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** `GET /v1/breadcrumbs?tags=...` — AND semantics. */
|
|
35
|
+
async queryByTags(tags: string[], options: QueryByTagsOptions = {}): Promise<Breadcrumb[]> {
|
|
36
|
+
if (tags.length === 0) {
|
|
37
|
+
throw new SdkError('EMPTY_TAGS', 'queryByTags requires at least one tag');
|
|
38
|
+
}
|
|
39
|
+
const res = await request<Breadcrumb[] | { breadcrumbs: Breadcrumb[] }>(this.ctx, '/v1/breadcrumbs', {
|
|
40
|
+
query: {
|
|
41
|
+
tags: tags.join(','),
|
|
42
|
+
limit: options.limit,
|
|
43
|
+
offset: options.offset,
|
|
44
|
+
name: options.name,
|
|
45
|
+
order: options.order,
|
|
46
|
+
},
|
|
47
|
+
});
|
|
48
|
+
return Array.isArray(res) ? res : (res.breadcrumbs ?? []);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** `GET /v1/breadcrumbs/search?q=...` — cosine-similarity semantic search. */
|
|
52
|
+
async search(q: string, options: SemanticSearchOptions = {}): Promise<Breadcrumb[]> {
|
|
53
|
+
const query: Record<string, string | number | undefined> = {
|
|
54
|
+
q,
|
|
55
|
+
limit: options.limit,
|
|
56
|
+
};
|
|
57
|
+
if (options.tags?.length) query.tags = options.tags.join(',');
|
|
58
|
+
const res = await request<Breadcrumb[] | { breadcrumbs: Breadcrumb[] }>(this.ctx, '/v1/breadcrumbs/search', { query });
|
|
59
|
+
return Array.isArray(res) ? res : (res.breadcrumbs ?? []);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** `GET /v1/breadcrumbs/{id}` */
|
|
63
|
+
async get(id: string): Promise<Breadcrumb> {
|
|
64
|
+
return request<Breadcrumb>(this.ctx, `/v1/breadcrumbs/${id}`);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* `PATCH /v1/breadcrumbs/{id}` — optimistic-locking update.
|
|
69
|
+
*
|
|
70
|
+
* If `req.version` is wrong, the server returns 409. Pass
|
|
71
|
+
* `autoRetryConflict: true` to have the SDK refetch + retry once.
|
|
72
|
+
*/
|
|
73
|
+
async update(
|
|
74
|
+
id: string,
|
|
75
|
+
req: UpdateBreadcrumbRequest,
|
|
76
|
+
opts: { autoRetryConflict?: boolean } = {},
|
|
77
|
+
): Promise<Breadcrumb> {
|
|
78
|
+
const refetchBeforeRetry = opts.autoRetryConflict
|
|
79
|
+
? async () => {
|
|
80
|
+
const fresh = await this.get(id);
|
|
81
|
+
return {
|
|
82
|
+
body: {
|
|
83
|
+
...req,
|
|
84
|
+
version: fresh.version,
|
|
85
|
+
} satisfies UpdateBreadcrumbRequest,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
: undefined;
|
|
89
|
+
|
|
90
|
+
const res = await request<BreadcrumbResponse | Breadcrumb>(this.ctx, `/v1/breadcrumbs/${id}`, {
|
|
91
|
+
method: 'PATCH',
|
|
92
|
+
body: req,
|
|
93
|
+
...(refetchBeforeRetry
|
|
94
|
+
? { maxConflictRetries: 2, refetchBeforeRetry }
|
|
95
|
+
: {}),
|
|
96
|
+
});
|
|
97
|
+
return 'breadcrumb' in (res as BreadcrumbResponse)
|
|
98
|
+
? (res as BreadcrumbResponse).breadcrumb
|
|
99
|
+
: (res as Breadcrumb);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** `DELETE /v1/breadcrumbs/{id}` — soft delete. */
|
|
103
|
+
async delete(id: string): Promise<void> {
|
|
104
|
+
try {
|
|
105
|
+
await request<void>(this.ctx, `/v1/breadcrumbs/${id}`, { method: 'DELETE' });
|
|
106
|
+
} catch (err) {
|
|
107
|
+
if (err instanceof ApiError && err.status === 404) return; // idempotent
|
|
108
|
+
throw err;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Capabilities module — discover what the current workspace can do.
|
|
3
|
+
*
|
|
4
|
+
* In RCRT, capabilities are breadcrumbs tagged with `interpret:*`:
|
|
5
|
+
* - `interpret:executable` — tools (functions an agent can call)
|
|
6
|
+
* - `interpret:promptable` — agents (entities the user / other
|
|
7
|
+
* agents can `chat.send()` to)
|
|
8
|
+
* - `interpret:service-def` — connectable services (Gmail, Notion,
|
|
9
|
+
* etc.) with their OAuth metadata
|
|
10
|
+
* - `interpret:knowledge` — static knowledge bases attached to the
|
|
11
|
+
* workspace
|
|
12
|
+
*
|
|
13
|
+
* This module is a thin convenience layer over `breadcrumbs.queryByTags`
|
|
14
|
+
* that maps the `type` argument to the right tag.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import type { FetchContext } from './internal/fetch.js';
|
|
18
|
+
import { request } from './internal/fetch.js';
|
|
19
|
+
import type { Breadcrumb } from './types/breadcrumb.js';
|
|
20
|
+
|
|
21
|
+
export type CapabilityType = 'tools' | 'agents' | 'services' | 'knowledge';
|
|
22
|
+
|
|
23
|
+
const TAG_BY_TYPE: Record<CapabilityType, string> = {
|
|
24
|
+
tools: 'interpret:executable',
|
|
25
|
+
agents: 'interpret:promptable',
|
|
26
|
+
services: 'interpret:service-def',
|
|
27
|
+
knowledge: 'interpret:knowledge',
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export interface ChattableAgent {
|
|
31
|
+
/** The id you pass to `chat.send({ target_agent: ... })`. */
|
|
32
|
+
id: string;
|
|
33
|
+
name: string;
|
|
34
|
+
description: string;
|
|
35
|
+
/** True when this agent has the `interface:chat-default` tag. */
|
|
36
|
+
isDefault: boolean;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export class CapabilitiesModule {
|
|
40
|
+
constructor(private readonly ctx: FetchContext) {}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* List every capability of every type. Useful when surfacing the
|
|
44
|
+
* full toolbelt + agent roster to a debug / admin view.
|
|
45
|
+
*/
|
|
46
|
+
async listAll(limit: number = 200): Promise<Breadcrumb[]> {
|
|
47
|
+
const res = await request<Breadcrumb[] | { breadcrumbs: Breadcrumb[] }>(
|
|
48
|
+
this.ctx,
|
|
49
|
+
'/v1/breadcrumbs',
|
|
50
|
+
{ query: { tags: TAG_BY_TYPE.tools, limit } },
|
|
51
|
+
);
|
|
52
|
+
return Array.isArray(res) ? res : (res.breadcrumbs ?? []);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** List capabilities of one specific type. */
|
|
56
|
+
async list(type: CapabilityType, limit: number = 200): Promise<Breadcrumb[]> {
|
|
57
|
+
const tag = TAG_BY_TYPE[type];
|
|
58
|
+
const res = await request<Breadcrumb[] | { breadcrumbs: Breadcrumb[] }>(
|
|
59
|
+
this.ctx,
|
|
60
|
+
'/v1/breadcrumbs',
|
|
61
|
+
{ query: { tags: tag, limit } },
|
|
62
|
+
);
|
|
63
|
+
return Array.isArray(res) ? res : (res.breadcrumbs ?? []);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Convenience: agents the user can chat with directly.
|
|
68
|
+
*
|
|
69
|
+
* Filters `interpret:promptable` to those tagged `interface:chat`
|
|
70
|
+
* or `interface:chat-default`. Falls back to all promptables when
|
|
71
|
+
* no agents have a chat interface tag (older deploys).
|
|
72
|
+
*/
|
|
73
|
+
async getChattableAgents(limit: number = 50): Promise<ChattableAgent[]> {
|
|
74
|
+
const promptables = await this.list('agents', limit);
|
|
75
|
+
const chatAgents = promptables.filter((bc) =>
|
|
76
|
+
bc.tags?.some(
|
|
77
|
+
(t) => t === 'interface:chat' || t === 'interface:chat-default',
|
|
78
|
+
),
|
|
79
|
+
);
|
|
80
|
+
const source = chatAgents.length > 0 ? chatAgents : promptables;
|
|
81
|
+
return source.map<ChattableAgent>((bc) => {
|
|
82
|
+
const content = (bc.content ?? {}) as Record<string, unknown>;
|
|
83
|
+
const id = (bc as { name?: string }).name ?? bc.id;
|
|
84
|
+
const name =
|
|
85
|
+
(content.name as string | undefined) ??
|
|
86
|
+
(bc as { title?: string }).title ??
|
|
87
|
+
id;
|
|
88
|
+
const description = (content.description as string | undefined) ?? '';
|
|
89
|
+
const isDefault = !!bc.tags?.includes('interface:chat-default');
|
|
90
|
+
return { id, name, description, isDefault };
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
}
|