@proveanything/smartlinks 2.1.3 → 2.1.5
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/dist/docs/API_SUMMARY.md +21 -1
- package/dist/docs/agent-tools.md +6 -1
- package/dist/docs/app-manifest.md +24 -0
- package/dist/docs/assistant-apps.md +168 -0
- package/dist/openapi.yaml +35 -0
- package/dist/types/appManifest.d.ts +51 -0
- package/docs/API_SUMMARY.md +21 -1
- package/docs/agent-tools.md +6 -1
- package/docs/app-manifest.md +24 -0
- package/docs/assistant-apps.md +168 -0
- package/openapi.yaml +35 -0
- package/package.json +1 -1
- package/scripts/publish.mjs +100 -28
package/dist/docs/API_SUMMARY.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Smartlinks API Summary
|
|
2
2
|
|
|
3
|
-
Version: 2.1.
|
|
3
|
+
Version: 2.1.5 | Generated: 2026-10-30T13:51:55.292Z
|
|
4
4
|
|
|
5
5
|
This is a concise summary of all available API functions and types.
|
|
6
6
|
|
|
@@ -2713,6 +2713,7 @@ interface AppFunctionAgentExposure {
|
|
|
2713
2713
|
description?: string;
|
|
2714
2714
|
input?: Record<string, any>;
|
|
2715
2715
|
approval?: 'auto' | 'require';
|
|
2716
|
+
audience?: Array<'app' | 'public' | 'assistant'>;
|
|
2716
2717
|
}
|
|
2717
2718
|
```
|
|
2718
2719
|
|
|
@@ -3051,10 +3052,29 @@ interface AppManifest {
|
|
|
3051
3052
|
data?: AppDataDeclaration;
|
|
3052
3053
|
headless?: AppHeadlessDeclaration;
|
|
3053
3054
|
connector?: AppConnectorDeclaration;
|
|
3055
|
+
assistant?: AppAssistantDeclaration;
|
|
3054
3056
|
[key: string]: any;
|
|
3055
3057
|
}
|
|
3056
3058
|
```
|
|
3057
3059
|
|
|
3060
|
+
**AppAssistantFile** (interface)
|
|
3061
|
+
```typescript
|
|
3062
|
+
interface AppAssistantFile {
|
|
3063
|
+
name: string;
|
|
3064
|
+
description?: string;
|
|
3065
|
+
file: string;
|
|
3066
|
+
}
|
|
3067
|
+
```
|
|
3068
|
+
|
|
3069
|
+
**AppAssistantDeclaration** (interface)
|
|
3070
|
+
```typescript
|
|
3071
|
+
interface AppAssistantDeclaration {
|
|
3072
|
+
skills?: AppAssistantFile[];
|
|
3073
|
+
knowledge?: AppAssistantFile[];
|
|
3074
|
+
preview?: string;
|
|
3075
|
+
}
|
|
3076
|
+
```
|
|
3077
|
+
|
|
3058
3078
|
**ConnectorConnectionField** (interface)
|
|
3059
3079
|
```typescript
|
|
3060
3080
|
interface ConnectorConnectionField {
|
package/dist/docs/agent-tools.md
CHANGED
|
@@ -44,7 +44,8 @@ needs to call it — a description and an input schema:
|
|
|
44
44
|
"title": "Create an FAQ entry",
|
|
45
45
|
"description": "Add a question/answer to this collection's FAQ. Use when the user asks to add or draft an FAQ.",
|
|
46
46
|
"input": { /* JSON Schema for the arguments the agent supplies as event.body */ },
|
|
47
|
-
"approval": "auto"
|
|
47
|
+
"approval": "auto", // auto | require (require = human confirmation before the call)
|
|
48
|
+
"audience": ["app"] // optional: app | public | assistant — where it is offered (absent = everywhere)
|
|
48
49
|
}
|
|
49
50
|
}
|
|
50
51
|
]
|
|
@@ -103,6 +104,10 @@ Identical to server functions — nothing new to reason about:
|
|
|
103
104
|
- **`visibility`** — who may call (admin/public).
|
|
104
105
|
- **`authority`** — whose identity `ctx.sl` carries (`caller` vs `collection`).
|
|
105
106
|
- **`capabilities`** — least-privilege, capped even when elevated.
|
|
107
|
+
- **`agent.audience`** — where the tool is offered: `'app'` (the app's own agent runs, admin surface),
|
|
108
|
+
`'public'` (public assistants), `'assistant'` (the Studio assistant, the admin's agent —
|
|
109
|
+
[assistant-apps.md](assistant-apps.md)). Absent = everywhere the function's visibility allows. An
|
|
110
|
+
assistant-only tool declares `["assistant"]` and never joins an in-app or public run.
|
|
106
111
|
- **`agent.approval: "require"`** — the agent must get human confirmation before invoking (use for
|
|
107
112
|
destructive or public-facing actions).
|
|
108
113
|
|
|
@@ -460,6 +460,30 @@ lists it and draws its connect form and settings, and the integration engine cal
|
|
|
460
460
|
functions (`testConnection`, `describeTarget`, `sample`, `fetch`, `shape`, `serialize`, `send`). It adapts the provider's format; core does each customer's mapping. The full
|
|
461
461
|
contract is in the **[Connectors guide](connectors.md)**.
|
|
462
462
|
|
|
463
|
+
#### `assistant`
|
|
464
|
+
|
|
465
|
+
What the **Studio assistant** (the admin's own agent) knows about this app: skills and knowledge files
|
|
466
|
+
shipped with the build and loaded into its box on demand, and an optional page that renders a planned
|
|
467
|
+
change set before it is applied.
|
|
468
|
+
|
|
469
|
+
```jsonc
|
|
470
|
+
"assistant": {
|
|
471
|
+
"skills": [
|
|
472
|
+
{ "name": "theme", "description": "Changing the Hub's colours, fonts or logo", "file": "assistant/skills/theme.md" },
|
|
473
|
+
{ "name": "pages", "description": "Adding, ordering and editing pages and the menu", "file": "assistant/skills/pages.md" }
|
|
474
|
+
],
|
|
475
|
+
"knowledge": [
|
|
476
|
+
{ "name": "settings-schema", "description": "JSON Schema of the hub settings group", "file": "assistant/knowledge/settings.schema.json" }
|
|
477
|
+
],
|
|
478
|
+
"preview": "/admin/preview?changeSet={id}"
|
|
479
|
+
}
|
|
480
|
+
```
|
|
481
|
+
|
|
482
|
+
The files live under `public/assistant/` and ship with every release like any other static file. The
|
|
483
|
+
assistant sees one line per skill (its description) and reads the file only when the job calls for it,
|
|
484
|
+
so a big app can carry many skills at no cost to the conversation. Full guide, including what a skill
|
|
485
|
+
should say and how the assistant changes config with undo: **[assistant-apps.md](assistant-apps.md)**.
|
|
486
|
+
|
|
463
487
|
---
|
|
464
488
|
|
|
465
489
|
## `app.admin.json`
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
# The Studio assistant and your app
|
|
2
|
+
|
|
3
|
+
SmartLinks admins have **one assistant**: an agent that works for the signed-in admin on one
|
|
4
|
+
collection, with the admin's own authority, from a chat in Studio. It reads and changes data through
|
|
5
|
+
the SmartLinks API, every write is a **change set** the admin can see and undo, and the things that
|
|
6
|
+
can't be undone (deleting, sending, calling out, publishing, spending) wait for the admin's "Allow".
|
|
7
|
+
|
|
8
|
+
This page is for app authors: how the assistant learns what your app is, how it is set up, and what it
|
|
9
|
+
can do for the admin — without you building a chat of your own. Your app declares three things:
|
|
10
|
+
|
|
11
|
+
| What | Where | Status |
|
|
12
|
+
|---|---|---|
|
|
13
|
+
| **Skills and knowledge** — plain-words procedures for the admin side, and the facts they need | `assistant` block in `app.manifest.json`, files under `public/assistant/` | this page |
|
|
14
|
+
| **Tools** — logic the assistant should call rather than reimplement | server functions with an `agent` block (`audience: ["assistant"]` for assistant-only) | [agent-tools.md](agent-tools.md) |
|
|
15
|
+
| **Setup** — questions, config schema, how to save | `app.admin.json` + `ai-guide.md` | [app-manifest.md](app-manifest.md#appadminjson) |
|
|
16
|
+
|
|
17
|
+
An app with none of these still works with the assistant: it can read the app's records and config
|
|
18
|
+
and change them through change sets. The declarations make it *good* at your app.
|
|
19
|
+
|
|
20
|
+
## The `assistant` block
|
|
21
|
+
|
|
22
|
+
```jsonc
|
|
23
|
+
// app.manifest.json
|
|
24
|
+
"assistant": {
|
|
25
|
+
"skills": [
|
|
26
|
+
{ "name": "setup", "description": "First-time setup of the FAQ app: categories, tone, who answers", "file": "assistant/skills/setup.md" },
|
|
27
|
+
{ "name": "content", "description": "Adding, editing, ordering and retiring questions and answers", "file": "assistant/skills/content.md" }
|
|
28
|
+
],
|
|
29
|
+
"knowledge": [
|
|
30
|
+
{ "name": "config-schema", "description": "JSON Schema of the app's config document", "file": "assistant/knowledge/config.schema.json" },
|
|
31
|
+
{ "name": "examples", "description": "Three worked examples of good FAQ content", "file": "assistant/knowledge/examples.md" }
|
|
32
|
+
],
|
|
33
|
+
"preview": "/admin/preview?changeSet={id}"
|
|
34
|
+
}
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
The files sit in `public/assistant/` in your project and ship with every release like any other static
|
|
38
|
+
file (they are read from the release's CDN folder, on the channel the collection runs your app on).
|
|
39
|
+
|
|
40
|
+
### How they are loaded
|
|
41
|
+
|
|
42
|
+
When an admin opens a chat, the assistant's box gets the **index** of every installed app's skills: one
|
|
43
|
+
line each, the description. Nothing else. A skill's body, and the knowledge files it names, are read
|
|
44
|
+
only when the assistant decides the job needs them. Twenty apps with three skills each cost the
|
|
45
|
+
conversation about sixty lines. So: write several focused skills rather than one long one, and spend
|
|
46
|
+
care on the description — it is how the assistant decides to open the file.
|
|
47
|
+
|
|
48
|
+
### Writing a skill
|
|
49
|
+
|
|
50
|
+
A skill is a markdown file: what this area of your app is for, what each setting means, how to read and
|
|
51
|
+
change it, what a good result looks like, what to check afterwards. Write it for a capable colleague who
|
|
52
|
+
has never seen your app. The assistant works through the SmartLinks API with a small CLI (`sl`), so say
|
|
53
|
+
things in those terms:
|
|
54
|
+
|
|
55
|
+
```markdown
|
|
56
|
+
---
|
|
57
|
+
name: faq-content
|
|
58
|
+
description: Adding, editing, ordering and retiring questions and answers in the FAQ app
|
|
59
|
+
---
|
|
60
|
+
# FAQ content
|
|
61
|
+
|
|
62
|
+
Questions live as app records of type `question` (`sl get app/faq/records recordType=question`).
|
|
63
|
+
Fields: `data.question`, `data.answer` (markdown), `data.category` (one of the categories in the app's
|
|
64
|
+
config, `sl get app/faq` → `categories[]`), `data.order` (ascending), `status` (`published` | `draft`).
|
|
65
|
+
|
|
66
|
+
## Adding questions
|
|
67
|
+
Plan one change set per batch: items `{ "op": "create", "entity": "record", "appId": "faq",
|
|
68
|
+
"data": { "recordType": "question", "data": { … }, "status": "draft" } }`. Match on `data.question`
|
|
69
|
+
(case-insensitive) before creating, so a re-run updates instead of duplicating.
|
|
70
|
+
|
|
71
|
+
## A good answer
|
|
72
|
+
Two to five sentences, plain words, no marketing. Link to a product page when one exists
|
|
73
|
+
(`/p/<collectionId>/<productId>`). Never invent policy: if the answer depends on something the admin
|
|
74
|
+
hasn't said (returns window, delivery areas), ask.
|
|
75
|
+
|
|
76
|
+
## Check afterwards
|
|
77
|
+
`sl get app/faq/records recordType=question status=draft` — read three back and make sure the
|
|
78
|
+
category is one that exists.
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
The frontmatter is optional; when it is missing, the manifest entry's `name` and `description` are used.
|
|
82
|
+
Keep a skill under a few hundred lines; put long reference material in knowledge files and name them.
|
|
83
|
+
|
|
84
|
+
### Knowledge
|
|
85
|
+
|
|
86
|
+
Knowledge files are what the skills refer to: the schema of your config document, record shapes, worked
|
|
87
|
+
examples, a glossary of your app's words. The assistant reads them when a skill points at them (each
|
|
88
|
+
skill is told where the app's knowledge files are).
|
|
89
|
+
|
|
90
|
+
**Generate the schema, don't write it.** If your config is typed (a TypeScript interface, a zod schema),
|
|
91
|
+
produce the JSON Schema at build time (`ts-json-schema-generator`, `zod-to-json-schema`) into
|
|
92
|
+
`public/assistant/knowledge/`. A hand-written schema drifts the first week.
|
|
93
|
+
|
|
94
|
+
## Tools: when a skill is not enough
|
|
95
|
+
|
|
96
|
+
Nearly every admin action is a read or a patch of config or records, and the assistant does those
|
|
97
|
+
itself. Give it a **tool** only when a step can't be an API call it makes on its own:
|
|
98
|
+
|
|
99
|
+
- logic that lives in your code (validate a layout, render a page from a design direction, compute
|
|
100
|
+
something from your data model),
|
|
101
|
+
- a write that must be atomic across several objects,
|
|
102
|
+
- anything that needs your app's secrets.
|
|
103
|
+
|
|
104
|
+
A tool is an http server function with an `agent` block
|
|
105
|
+
([agent-tools.md](agent-tools.md)); the assistant calls it as
|
|
106
|
+
`sl post app/<appId>/functions/<name> '{ … }'`, under the function's own visibility, authority and
|
|
107
|
+
capabilities. Set `"audience": ["assistant"]` to keep an assistant-only tool out of your in-app and
|
|
108
|
+
public agent runs:
|
|
109
|
+
|
|
110
|
+
```jsonc
|
|
111
|
+
{
|
|
112
|
+
"name": "applyDesignDirection",
|
|
113
|
+
"trigger": { "type": "http" },
|
|
114
|
+
"visibility": "admin",
|
|
115
|
+
"authority": "caller",
|
|
116
|
+
"capabilities": ["sl:settings:write"],
|
|
117
|
+
"agent": {
|
|
118
|
+
"tool": true,
|
|
119
|
+
"title": "Apply a design direction",
|
|
120
|
+
"description": "Rewrites the theme, fonts and layout of the hub to match a named direction.",
|
|
121
|
+
"input": { "type": "object", "properties": { "direction": { "type": "string" } }, "required": ["direction"] },
|
|
122
|
+
"audience": ["assistant"]
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
A tool that changes config should **return the patch, not apply it**, so the assistant can put it in a
|
|
128
|
+
change set and the admin keeps undo. Tools that act directly are fine for reads and computations.
|
|
129
|
+
|
|
130
|
+
## How the assistant changes your config
|
|
131
|
+
|
|
132
|
+
Your config document (`SL.appConfiguration`, `sl get app/<appId>`) and the collection's settings
|
|
133
|
+
groups (`sl get settings/<group>`) are changed through change-set items:
|
|
134
|
+
|
|
135
|
+
```jsonc
|
|
136
|
+
{ "op": "update", "entity": "appConfig", "id": "<appId>", "data": { "theme": { "accent": "#0a7" } } }
|
|
137
|
+
{ "op": "update", "entity": "appConfig", "id": "<appId>", "scope": { "productId": "p1" }, "data": { … } }
|
|
138
|
+
{ "op": "update", "entity": "settings", "id": "hub", "data": { "pages": { "about": { … } } } }
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
Saves deep-merge, as your own admin UI's saves do (nested objects merge, arrays replace). The whole
|
|
142
|
+
document before the change is kept, so **undo puts it back exactly**. Say in your skills which keys
|
|
143
|
+
are arrays, so the assistant sends the whole array when it changes one element.
|
|
144
|
+
|
|
145
|
+
### Preview before apply
|
|
146
|
+
|
|
147
|
+
A planned change set can be rendered before it is applied. The API gives, for every document a set
|
|
148
|
+
touches, the live document and the document as it would become:
|
|
149
|
+
|
|
150
|
+
```
|
|
151
|
+
GET /admin/collection/:collectionId/assistant/changesets/:id/preview
|
|
152
|
+
→ { id, status, targets: [ { entity, id, scope?, before, after, ops } ] }
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
Declare `assistant.preview` — a page of your admin surface that takes `{id}`, calls that endpoint
|
|
156
|
+
with the admin's session, and renders `targets[].after` as your app would render the live config. The
|
|
157
|
+
Studio panel then shows "Preview" on the change set, and the admin sees the result before saying yes.
|
|
158
|
+
|
|
159
|
+
## Checklist
|
|
160
|
+
|
|
161
|
+
- `assistant.skills[]`: several focused skills with descriptions that say when they apply.
|
|
162
|
+
- `assistant.knowledge[]`: the config schema generated from your types; record shapes; examples.
|
|
163
|
+
- Every skill says which fields are arrays, what to match on before creating, what to check after.
|
|
164
|
+
- Logic the assistant can't reproduce: a server function with `agent.tool` and `audience: ["assistant"]`,
|
|
165
|
+
returning a patch for config changes.
|
|
166
|
+
- `assistant.preview` when your app has a visual surface worth seeing before a change lands.
|
|
167
|
+
- `ai-guide.md` still describes first-time setup for the setup wizard; the assistant reads it too when
|
|
168
|
+
an app declares no skills.
|
package/dist/openapi.yaml
CHANGED
|
@@ -20138,6 +20138,14 @@ components:
|
|
|
20138
20138
|
enum:
|
|
20139
20139
|
- auto
|
|
20140
20140
|
- require
|
|
20141
|
+
audience:
|
|
20142
|
+
type: array
|
|
20143
|
+
items:
|
|
20144
|
+
type: string
|
|
20145
|
+
enum:
|
|
20146
|
+
- app
|
|
20147
|
+
- public
|
|
20148
|
+
- assistant
|
|
20141
20149
|
required:
|
|
20142
20150
|
- tool
|
|
20143
20151
|
AppManifestFunctions:
|
|
@@ -20635,6 +20643,8 @@ components:
|
|
|
20635
20643
|
$ref: "#/components/schemas/AppHeadlessDeclaration"
|
|
20636
20644
|
connector:
|
|
20637
20645
|
$ref: "#/components/schemas/AppConnectorDeclaration"
|
|
20646
|
+
assistant:
|
|
20647
|
+
$ref: "#/components/schemas/AppAssistantDeclaration"
|
|
20638
20648
|
required:
|
|
20639
20649
|
- name
|
|
20640
20650
|
- version
|
|
@@ -20642,6 +20652,31 @@ components:
|
|
|
20642
20652
|
- function
|
|
20643
20653
|
- files
|
|
20644
20654
|
- components
|
|
20655
|
+
AppAssistantFile:
|
|
20656
|
+
type: object
|
|
20657
|
+
properties:
|
|
20658
|
+
name:
|
|
20659
|
+
type: string
|
|
20660
|
+
description:
|
|
20661
|
+
type: string
|
|
20662
|
+
file:
|
|
20663
|
+
type: string
|
|
20664
|
+
required:
|
|
20665
|
+
- name
|
|
20666
|
+
- file
|
|
20667
|
+
AppAssistantDeclaration:
|
|
20668
|
+
type: object
|
|
20669
|
+
properties:
|
|
20670
|
+
skills:
|
|
20671
|
+
type: array
|
|
20672
|
+
items:
|
|
20673
|
+
$ref: "#/components/schemas/AppAssistantFile"
|
|
20674
|
+
knowledge:
|
|
20675
|
+
type: array
|
|
20676
|
+
items:
|
|
20677
|
+
$ref: "#/components/schemas/AppAssistantFile"
|
|
20678
|
+
preview:
|
|
20679
|
+
type: string
|
|
20645
20680
|
ConnectorConnectionField:
|
|
20646
20681
|
type: object
|
|
20647
20682
|
properties:
|
|
@@ -349,6 +349,13 @@ export interface AppFunctionAgentExposure {
|
|
|
349
349
|
* the autonomous server-side loop (they'd otherwise run unconfirmed).
|
|
350
350
|
*/
|
|
351
351
|
approval?: 'auto' | 'require';
|
|
352
|
+
/**
|
|
353
|
+
* Where the tool is offered. `'app'` = the app's own agent runs (admin surface), `'public'` = public
|
|
354
|
+
* assistants, `'assistant'` = the Studio assistant (the admin's agent, docs/assistant-apps.md).
|
|
355
|
+
* Absent = everywhere the function's visibility allows. An assistant-only tool (logic the assistant
|
|
356
|
+
* needs that lives in your code) declares `['assistant']` and never shows in an in-app or public run.
|
|
357
|
+
*/
|
|
358
|
+
audience?: Array<'app' | 'public' | 'assistant'>;
|
|
352
359
|
}
|
|
353
360
|
/** The `functions` block in `app.manifest.json`. Presence means the app ships server functions. */
|
|
354
361
|
export interface AppManifestFunctions {
|
|
@@ -941,8 +948,52 @@ export interface AppManifest {
|
|
|
941
948
|
* @see AppConnectorDeclaration · docs/connectors.md
|
|
942
949
|
*/
|
|
943
950
|
connector?: AppConnectorDeclaration;
|
|
951
|
+
/**
|
|
952
|
+
* What the Studio assistant (the admin's agent) knows about this app: skills and knowledge files
|
|
953
|
+
* shipped with the build and loaded into its box on demand, and an optional page that renders a
|
|
954
|
+
* planned change set. See docs/assistant-apps.md. @see AppAssistantDeclaration
|
|
955
|
+
*/
|
|
956
|
+
assistant?: AppAssistantDeclaration;
|
|
944
957
|
[key: string]: any;
|
|
945
958
|
}
|
|
959
|
+
/** One skill or knowledge file the app ships for the assistant. */
|
|
960
|
+
export interface AppAssistantFile {
|
|
961
|
+
/** Short id, unique within the app (`theme`, `pages`, `settings-schema`). */
|
|
962
|
+
name: string;
|
|
963
|
+
/**
|
|
964
|
+
* One line: what it covers and when to reach for it. For a skill this is what the assistant sees
|
|
965
|
+
* before deciding to read the file, so say when it applies ("Changing the Hub's colours, fonts or
|
|
966
|
+
* logo"). A knowledge file's description says what is in it.
|
|
967
|
+
*/
|
|
968
|
+
description?: string;
|
|
969
|
+
/**
|
|
970
|
+
* Path of the file in the published build, relative to the app's root (`public/` in the project):
|
|
971
|
+
* `assistant/skills/theme.md`. Markdown for skills; markdown or JSON (a JSON Schema) for knowledge.
|
|
972
|
+
*/
|
|
973
|
+
file: string;
|
|
974
|
+
}
|
|
975
|
+
/** The `assistant` block in `app.manifest.json`. */
|
|
976
|
+
export interface AppAssistantDeclaration {
|
|
977
|
+
/**
|
|
978
|
+
* Skills: plain-words procedures for one area of the app's admin side — what the settings mean,
|
|
979
|
+
* which calls read and change them, what a good result looks like, what to check. Several for a big
|
|
980
|
+
* app (identity and theme; pages and navigation; domains and SEO). A skill file may start with
|
|
981
|
+
* YAML frontmatter (`name`, `description`); if it doesn't, these entries supply it.
|
|
982
|
+
*/
|
|
983
|
+
skills?: AppAssistantFile[];
|
|
984
|
+
/**
|
|
985
|
+
* Knowledge the skills refer to: the settings schema (generate it from your config types — never
|
|
986
|
+
* by hand), object definitions, worked examples, a glossary. Loaded only when a skill reaches for it.
|
|
987
|
+
*/
|
|
988
|
+
knowledge?: AppAssistantFile[];
|
|
989
|
+
/**
|
|
990
|
+
* A page of the app that renders a PLANNED change set over the live config, so the admin can see a
|
|
991
|
+
* change before it is applied. `{id}` is replaced with the change-set id; the page reads
|
|
992
|
+
* `GET /admin/collection/:cid/assistant/changesets/:id/preview` and renders `targets[].after`.
|
|
993
|
+
* Relative to the app's admin surface, e.g. `"/admin/preview?changeSet={id}"`.
|
|
994
|
+
*/
|
|
995
|
+
preview?: string;
|
|
996
|
+
}
|
|
946
997
|
/** One field of a connector's "Connect" form. */
|
|
947
998
|
export interface ConnectorConnectionField {
|
|
948
999
|
/** Key in the installation's connection settings (non-secret fields) — `name` for a secret too. */
|
package/docs/API_SUMMARY.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Smartlinks API Summary
|
|
2
2
|
|
|
3
|
-
Version: 2.1.
|
|
3
|
+
Version: 2.1.5 | Generated: 2026-10-30T13:51:55.292Z
|
|
4
4
|
|
|
5
5
|
This is a concise summary of all available API functions and types.
|
|
6
6
|
|
|
@@ -2713,6 +2713,7 @@ interface AppFunctionAgentExposure {
|
|
|
2713
2713
|
description?: string;
|
|
2714
2714
|
input?: Record<string, any>;
|
|
2715
2715
|
approval?: 'auto' | 'require';
|
|
2716
|
+
audience?: Array<'app' | 'public' | 'assistant'>;
|
|
2716
2717
|
}
|
|
2717
2718
|
```
|
|
2718
2719
|
|
|
@@ -3051,10 +3052,29 @@ interface AppManifest {
|
|
|
3051
3052
|
data?: AppDataDeclaration;
|
|
3052
3053
|
headless?: AppHeadlessDeclaration;
|
|
3053
3054
|
connector?: AppConnectorDeclaration;
|
|
3055
|
+
assistant?: AppAssistantDeclaration;
|
|
3054
3056
|
[key: string]: any;
|
|
3055
3057
|
}
|
|
3056
3058
|
```
|
|
3057
3059
|
|
|
3060
|
+
**AppAssistantFile** (interface)
|
|
3061
|
+
```typescript
|
|
3062
|
+
interface AppAssistantFile {
|
|
3063
|
+
name: string;
|
|
3064
|
+
description?: string;
|
|
3065
|
+
file: string;
|
|
3066
|
+
}
|
|
3067
|
+
```
|
|
3068
|
+
|
|
3069
|
+
**AppAssistantDeclaration** (interface)
|
|
3070
|
+
```typescript
|
|
3071
|
+
interface AppAssistantDeclaration {
|
|
3072
|
+
skills?: AppAssistantFile[];
|
|
3073
|
+
knowledge?: AppAssistantFile[];
|
|
3074
|
+
preview?: string;
|
|
3075
|
+
}
|
|
3076
|
+
```
|
|
3077
|
+
|
|
3058
3078
|
**ConnectorConnectionField** (interface)
|
|
3059
3079
|
```typescript
|
|
3060
3080
|
interface ConnectorConnectionField {
|
package/docs/agent-tools.md
CHANGED
|
@@ -44,7 +44,8 @@ needs to call it — a description and an input schema:
|
|
|
44
44
|
"title": "Create an FAQ entry",
|
|
45
45
|
"description": "Add a question/answer to this collection's FAQ. Use when the user asks to add or draft an FAQ.",
|
|
46
46
|
"input": { /* JSON Schema for the arguments the agent supplies as event.body */ },
|
|
47
|
-
"approval": "auto"
|
|
47
|
+
"approval": "auto", // auto | require (require = human confirmation before the call)
|
|
48
|
+
"audience": ["app"] // optional: app | public | assistant — where it is offered (absent = everywhere)
|
|
48
49
|
}
|
|
49
50
|
}
|
|
50
51
|
]
|
|
@@ -103,6 +104,10 @@ Identical to server functions — nothing new to reason about:
|
|
|
103
104
|
- **`visibility`** — who may call (admin/public).
|
|
104
105
|
- **`authority`** — whose identity `ctx.sl` carries (`caller` vs `collection`).
|
|
105
106
|
- **`capabilities`** — least-privilege, capped even when elevated.
|
|
107
|
+
- **`agent.audience`** — where the tool is offered: `'app'` (the app's own agent runs, admin surface),
|
|
108
|
+
`'public'` (public assistants), `'assistant'` (the Studio assistant, the admin's agent —
|
|
109
|
+
[assistant-apps.md](assistant-apps.md)). Absent = everywhere the function's visibility allows. An
|
|
110
|
+
assistant-only tool declares `["assistant"]` and never joins an in-app or public run.
|
|
106
111
|
- **`agent.approval: "require"`** — the agent must get human confirmation before invoking (use for
|
|
107
112
|
destructive or public-facing actions).
|
|
108
113
|
|
package/docs/app-manifest.md
CHANGED
|
@@ -460,6 +460,30 @@ lists it and draws its connect form and settings, and the integration engine cal
|
|
|
460
460
|
functions (`testConnection`, `describeTarget`, `sample`, `fetch`, `shape`, `serialize`, `send`). It adapts the provider's format; core does each customer's mapping. The full
|
|
461
461
|
contract is in the **[Connectors guide](connectors.md)**.
|
|
462
462
|
|
|
463
|
+
#### `assistant`
|
|
464
|
+
|
|
465
|
+
What the **Studio assistant** (the admin's own agent) knows about this app: skills and knowledge files
|
|
466
|
+
shipped with the build and loaded into its box on demand, and an optional page that renders a planned
|
|
467
|
+
change set before it is applied.
|
|
468
|
+
|
|
469
|
+
```jsonc
|
|
470
|
+
"assistant": {
|
|
471
|
+
"skills": [
|
|
472
|
+
{ "name": "theme", "description": "Changing the Hub's colours, fonts or logo", "file": "assistant/skills/theme.md" },
|
|
473
|
+
{ "name": "pages", "description": "Adding, ordering and editing pages and the menu", "file": "assistant/skills/pages.md" }
|
|
474
|
+
],
|
|
475
|
+
"knowledge": [
|
|
476
|
+
{ "name": "settings-schema", "description": "JSON Schema of the hub settings group", "file": "assistant/knowledge/settings.schema.json" }
|
|
477
|
+
],
|
|
478
|
+
"preview": "/admin/preview?changeSet={id}"
|
|
479
|
+
}
|
|
480
|
+
```
|
|
481
|
+
|
|
482
|
+
The files live under `public/assistant/` and ship with every release like any other static file. The
|
|
483
|
+
assistant sees one line per skill (its description) and reads the file only when the job calls for it,
|
|
484
|
+
so a big app can carry many skills at no cost to the conversation. Full guide, including what a skill
|
|
485
|
+
should say and how the assistant changes config with undo: **[assistant-apps.md](assistant-apps.md)**.
|
|
486
|
+
|
|
463
487
|
---
|
|
464
488
|
|
|
465
489
|
## `app.admin.json`
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
# The Studio assistant and your app
|
|
2
|
+
|
|
3
|
+
SmartLinks admins have **one assistant**: an agent that works for the signed-in admin on one
|
|
4
|
+
collection, with the admin's own authority, from a chat in Studio. It reads and changes data through
|
|
5
|
+
the SmartLinks API, every write is a **change set** the admin can see and undo, and the things that
|
|
6
|
+
can't be undone (deleting, sending, calling out, publishing, spending) wait for the admin's "Allow".
|
|
7
|
+
|
|
8
|
+
This page is for app authors: how the assistant learns what your app is, how it is set up, and what it
|
|
9
|
+
can do for the admin — without you building a chat of your own. Your app declares three things:
|
|
10
|
+
|
|
11
|
+
| What | Where | Status |
|
|
12
|
+
|---|---|---|
|
|
13
|
+
| **Skills and knowledge** — plain-words procedures for the admin side, and the facts they need | `assistant` block in `app.manifest.json`, files under `public/assistant/` | this page |
|
|
14
|
+
| **Tools** — logic the assistant should call rather than reimplement | server functions with an `agent` block (`audience: ["assistant"]` for assistant-only) | [agent-tools.md](agent-tools.md) |
|
|
15
|
+
| **Setup** — questions, config schema, how to save | `app.admin.json` + `ai-guide.md` | [app-manifest.md](app-manifest.md#appadminjson) |
|
|
16
|
+
|
|
17
|
+
An app with none of these still works with the assistant: it can read the app's records and config
|
|
18
|
+
and change them through change sets. The declarations make it *good* at your app.
|
|
19
|
+
|
|
20
|
+
## The `assistant` block
|
|
21
|
+
|
|
22
|
+
```jsonc
|
|
23
|
+
// app.manifest.json
|
|
24
|
+
"assistant": {
|
|
25
|
+
"skills": [
|
|
26
|
+
{ "name": "setup", "description": "First-time setup of the FAQ app: categories, tone, who answers", "file": "assistant/skills/setup.md" },
|
|
27
|
+
{ "name": "content", "description": "Adding, editing, ordering and retiring questions and answers", "file": "assistant/skills/content.md" }
|
|
28
|
+
],
|
|
29
|
+
"knowledge": [
|
|
30
|
+
{ "name": "config-schema", "description": "JSON Schema of the app's config document", "file": "assistant/knowledge/config.schema.json" },
|
|
31
|
+
{ "name": "examples", "description": "Three worked examples of good FAQ content", "file": "assistant/knowledge/examples.md" }
|
|
32
|
+
],
|
|
33
|
+
"preview": "/admin/preview?changeSet={id}"
|
|
34
|
+
}
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
The files sit in `public/assistant/` in your project and ship with every release like any other static
|
|
38
|
+
file (they are read from the release's CDN folder, on the channel the collection runs your app on).
|
|
39
|
+
|
|
40
|
+
### How they are loaded
|
|
41
|
+
|
|
42
|
+
When an admin opens a chat, the assistant's box gets the **index** of every installed app's skills: one
|
|
43
|
+
line each, the description. Nothing else. A skill's body, and the knowledge files it names, are read
|
|
44
|
+
only when the assistant decides the job needs them. Twenty apps with three skills each cost the
|
|
45
|
+
conversation about sixty lines. So: write several focused skills rather than one long one, and spend
|
|
46
|
+
care on the description — it is how the assistant decides to open the file.
|
|
47
|
+
|
|
48
|
+
### Writing a skill
|
|
49
|
+
|
|
50
|
+
A skill is a markdown file: what this area of your app is for, what each setting means, how to read and
|
|
51
|
+
change it, what a good result looks like, what to check afterwards. Write it for a capable colleague who
|
|
52
|
+
has never seen your app. The assistant works through the SmartLinks API with a small CLI (`sl`), so say
|
|
53
|
+
things in those terms:
|
|
54
|
+
|
|
55
|
+
```markdown
|
|
56
|
+
---
|
|
57
|
+
name: faq-content
|
|
58
|
+
description: Adding, editing, ordering and retiring questions and answers in the FAQ app
|
|
59
|
+
---
|
|
60
|
+
# FAQ content
|
|
61
|
+
|
|
62
|
+
Questions live as app records of type `question` (`sl get app/faq/records recordType=question`).
|
|
63
|
+
Fields: `data.question`, `data.answer` (markdown), `data.category` (one of the categories in the app's
|
|
64
|
+
config, `sl get app/faq` → `categories[]`), `data.order` (ascending), `status` (`published` | `draft`).
|
|
65
|
+
|
|
66
|
+
## Adding questions
|
|
67
|
+
Plan one change set per batch: items `{ "op": "create", "entity": "record", "appId": "faq",
|
|
68
|
+
"data": { "recordType": "question", "data": { … }, "status": "draft" } }`. Match on `data.question`
|
|
69
|
+
(case-insensitive) before creating, so a re-run updates instead of duplicating.
|
|
70
|
+
|
|
71
|
+
## A good answer
|
|
72
|
+
Two to five sentences, plain words, no marketing. Link to a product page when one exists
|
|
73
|
+
(`/p/<collectionId>/<productId>`). Never invent policy: if the answer depends on something the admin
|
|
74
|
+
hasn't said (returns window, delivery areas), ask.
|
|
75
|
+
|
|
76
|
+
## Check afterwards
|
|
77
|
+
`sl get app/faq/records recordType=question status=draft` — read three back and make sure the
|
|
78
|
+
category is one that exists.
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
The frontmatter is optional; when it is missing, the manifest entry's `name` and `description` are used.
|
|
82
|
+
Keep a skill under a few hundred lines; put long reference material in knowledge files and name them.
|
|
83
|
+
|
|
84
|
+
### Knowledge
|
|
85
|
+
|
|
86
|
+
Knowledge files are what the skills refer to: the schema of your config document, record shapes, worked
|
|
87
|
+
examples, a glossary of your app's words. The assistant reads them when a skill points at them (each
|
|
88
|
+
skill is told where the app's knowledge files are).
|
|
89
|
+
|
|
90
|
+
**Generate the schema, don't write it.** If your config is typed (a TypeScript interface, a zod schema),
|
|
91
|
+
produce the JSON Schema at build time (`ts-json-schema-generator`, `zod-to-json-schema`) into
|
|
92
|
+
`public/assistant/knowledge/`. A hand-written schema drifts the first week.
|
|
93
|
+
|
|
94
|
+
## Tools: when a skill is not enough
|
|
95
|
+
|
|
96
|
+
Nearly every admin action is a read or a patch of config or records, and the assistant does those
|
|
97
|
+
itself. Give it a **tool** only when a step can't be an API call it makes on its own:
|
|
98
|
+
|
|
99
|
+
- logic that lives in your code (validate a layout, render a page from a design direction, compute
|
|
100
|
+
something from your data model),
|
|
101
|
+
- a write that must be atomic across several objects,
|
|
102
|
+
- anything that needs your app's secrets.
|
|
103
|
+
|
|
104
|
+
A tool is an http server function with an `agent` block
|
|
105
|
+
([agent-tools.md](agent-tools.md)); the assistant calls it as
|
|
106
|
+
`sl post app/<appId>/functions/<name> '{ … }'`, under the function's own visibility, authority and
|
|
107
|
+
capabilities. Set `"audience": ["assistant"]` to keep an assistant-only tool out of your in-app and
|
|
108
|
+
public agent runs:
|
|
109
|
+
|
|
110
|
+
```jsonc
|
|
111
|
+
{
|
|
112
|
+
"name": "applyDesignDirection",
|
|
113
|
+
"trigger": { "type": "http" },
|
|
114
|
+
"visibility": "admin",
|
|
115
|
+
"authority": "caller",
|
|
116
|
+
"capabilities": ["sl:settings:write"],
|
|
117
|
+
"agent": {
|
|
118
|
+
"tool": true,
|
|
119
|
+
"title": "Apply a design direction",
|
|
120
|
+
"description": "Rewrites the theme, fonts and layout of the hub to match a named direction.",
|
|
121
|
+
"input": { "type": "object", "properties": { "direction": { "type": "string" } }, "required": ["direction"] },
|
|
122
|
+
"audience": ["assistant"]
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
A tool that changes config should **return the patch, not apply it**, so the assistant can put it in a
|
|
128
|
+
change set and the admin keeps undo. Tools that act directly are fine for reads and computations.
|
|
129
|
+
|
|
130
|
+
## How the assistant changes your config
|
|
131
|
+
|
|
132
|
+
Your config document (`SL.appConfiguration`, `sl get app/<appId>`) and the collection's settings
|
|
133
|
+
groups (`sl get settings/<group>`) are changed through change-set items:
|
|
134
|
+
|
|
135
|
+
```jsonc
|
|
136
|
+
{ "op": "update", "entity": "appConfig", "id": "<appId>", "data": { "theme": { "accent": "#0a7" } } }
|
|
137
|
+
{ "op": "update", "entity": "appConfig", "id": "<appId>", "scope": { "productId": "p1" }, "data": { … } }
|
|
138
|
+
{ "op": "update", "entity": "settings", "id": "hub", "data": { "pages": { "about": { … } } } }
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
Saves deep-merge, as your own admin UI's saves do (nested objects merge, arrays replace). The whole
|
|
142
|
+
document before the change is kept, so **undo puts it back exactly**. Say in your skills which keys
|
|
143
|
+
are arrays, so the assistant sends the whole array when it changes one element.
|
|
144
|
+
|
|
145
|
+
### Preview before apply
|
|
146
|
+
|
|
147
|
+
A planned change set can be rendered before it is applied. The API gives, for every document a set
|
|
148
|
+
touches, the live document and the document as it would become:
|
|
149
|
+
|
|
150
|
+
```
|
|
151
|
+
GET /admin/collection/:collectionId/assistant/changesets/:id/preview
|
|
152
|
+
→ { id, status, targets: [ { entity, id, scope?, before, after, ops } ] }
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
Declare `assistant.preview` — a page of your admin surface that takes `{id}`, calls that endpoint
|
|
156
|
+
with the admin's session, and renders `targets[].after` as your app would render the live config. The
|
|
157
|
+
Studio panel then shows "Preview" on the change set, and the admin sees the result before saying yes.
|
|
158
|
+
|
|
159
|
+
## Checklist
|
|
160
|
+
|
|
161
|
+
- `assistant.skills[]`: several focused skills with descriptions that say when they apply.
|
|
162
|
+
- `assistant.knowledge[]`: the config schema generated from your types; record shapes; examples.
|
|
163
|
+
- Every skill says which fields are arrays, what to match on before creating, what to check after.
|
|
164
|
+
- Logic the assistant can't reproduce: a server function with `agent.tool` and `audience: ["assistant"]`,
|
|
165
|
+
returning a patch for config changes.
|
|
166
|
+
- `assistant.preview` when your app has a visual surface worth seeing before a change lands.
|
|
167
|
+
- `ai-guide.md` still describes first-time setup for the setup wizard; the assistant reads it too when
|
|
168
|
+
an app declares no skills.
|
package/openapi.yaml
CHANGED
|
@@ -20138,6 +20138,14 @@ components:
|
|
|
20138
20138
|
enum:
|
|
20139
20139
|
- auto
|
|
20140
20140
|
- require
|
|
20141
|
+
audience:
|
|
20142
|
+
type: array
|
|
20143
|
+
items:
|
|
20144
|
+
type: string
|
|
20145
|
+
enum:
|
|
20146
|
+
- app
|
|
20147
|
+
- public
|
|
20148
|
+
- assistant
|
|
20141
20149
|
required:
|
|
20142
20150
|
- tool
|
|
20143
20151
|
AppManifestFunctions:
|
|
@@ -20635,6 +20643,8 @@ components:
|
|
|
20635
20643
|
$ref: "#/components/schemas/AppHeadlessDeclaration"
|
|
20636
20644
|
connector:
|
|
20637
20645
|
$ref: "#/components/schemas/AppConnectorDeclaration"
|
|
20646
|
+
assistant:
|
|
20647
|
+
$ref: "#/components/schemas/AppAssistantDeclaration"
|
|
20638
20648
|
required:
|
|
20639
20649
|
- name
|
|
20640
20650
|
- version
|
|
@@ -20642,6 +20652,31 @@ components:
|
|
|
20642
20652
|
- function
|
|
20643
20653
|
- files
|
|
20644
20654
|
- components
|
|
20655
|
+
AppAssistantFile:
|
|
20656
|
+
type: object
|
|
20657
|
+
properties:
|
|
20658
|
+
name:
|
|
20659
|
+
type: string
|
|
20660
|
+
description:
|
|
20661
|
+
type: string
|
|
20662
|
+
file:
|
|
20663
|
+
type: string
|
|
20664
|
+
required:
|
|
20665
|
+
- name
|
|
20666
|
+
- file
|
|
20667
|
+
AppAssistantDeclaration:
|
|
20668
|
+
type: object
|
|
20669
|
+
properties:
|
|
20670
|
+
skills:
|
|
20671
|
+
type: array
|
|
20672
|
+
items:
|
|
20673
|
+
$ref: "#/components/schemas/AppAssistantFile"
|
|
20674
|
+
knowledge:
|
|
20675
|
+
type: array
|
|
20676
|
+
items:
|
|
20677
|
+
$ref: "#/components/schemas/AppAssistantFile"
|
|
20678
|
+
preview:
|
|
20679
|
+
type: string
|
|
20645
20680
|
ConnectorConnectionField:
|
|
20646
20681
|
type: object
|
|
20647
20682
|
properties:
|
package/package.json
CHANGED
package/scripts/publish.mjs
CHANGED
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
|
|
19
19
|
import { readFileSync, readdirSync, statSync, watch } from 'node:fs'
|
|
20
20
|
import { join, relative, sep } from 'node:path'
|
|
21
|
+
import { createHash } from 'node:crypto'
|
|
21
22
|
|
|
22
23
|
const args = process.argv.slice(2)
|
|
23
24
|
const flag = (name, def) => { const i = args.indexOf(name); return i >= 0 ? (args[i + 1] && !args[i + 1].startsWith('--') ? args[i + 1] : true) : def }
|
|
@@ -66,6 +67,69 @@ const BATCH_FILES = 150
|
|
|
66
67
|
// through an API request, so no single file is ever too large to publish (e.g. a readable dev build).
|
|
67
68
|
const DIRECT_BYTES = 4 * 1024 * 1024
|
|
68
69
|
|
|
70
|
+
// A large bundle sends only what changed, and straight to storage (POST /publish/plan): the API compares
|
|
71
|
+
// each file's MD5 with what the dev folder already holds and hands out an upload session for the rest, which
|
|
72
|
+
// go up in parallel, never through the API. A site with 1,500 files, mostly images, sent all of them through
|
|
73
|
+
// the API on every publish (about two minutes); a text fix now sends the few files the build changed.
|
|
74
|
+
const PLAN_FILES = 500
|
|
75
|
+
const PARALLEL_UPLOADS = 8
|
|
76
|
+
const md5Of = (buf) => createHash('md5').update(buf).digest('base64')
|
|
77
|
+
|
|
78
|
+
async function pool(items, size, fn) {
|
|
79
|
+
let i = 0
|
|
80
|
+
await Promise.all(Array.from({ length: Math.min(size, items.length) }, async () => {
|
|
81
|
+
while (i < items.length) { const k = i++; await fn(items[k]) }
|
|
82
|
+
}))
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// PUT one file to its upload session; a failed upload asks for a fresh session and tries again (3 tries).
|
|
86
|
+
async function putFile(path, buf, url, qs) {
|
|
87
|
+
let at = url
|
|
88
|
+
for (let attempt = 1; ; attempt++) {
|
|
89
|
+
const put = await fetch(at, { method: 'PUT', body: buf }).catch((e) => ({ ok: false, status: 0, text: async () => e.message }))
|
|
90
|
+
if (put.ok) return true
|
|
91
|
+
const why = `${put.status}: ${(await put.text().catch(() => '')).slice(0, 200)}`
|
|
92
|
+
if (attempt >= 3) { console.error(`❌ upload of ${path} failed (${why})`); return false }
|
|
93
|
+
const { res, b } = await post(`/publish/uploads${qs}`, { files: [{ path, size: buf.length }] })
|
|
94
|
+
if (!res.ok || !b.ok || !b.uploads || !b.uploads[0]) { console.error(`❌ upload of ${path} failed (${why}); no new upload session`); return false }
|
|
95
|
+
at = b.uploads[0].url
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// → the paths now in the dev folder (unchanged + uploaded), null when this API has no plans yet (an older
|
|
100
|
+
// API: the caller sends batches, as before), or false when an upload failed.
|
|
101
|
+
async function publishByPlan(rest, qs) {
|
|
102
|
+
// Empty files can't take an upload session: they go in a batch (they cost nothing).
|
|
103
|
+
const planned = rest.filter(([, buf]) => buf.length > 0)
|
|
104
|
+
const have = []
|
|
105
|
+
const uploads = []
|
|
106
|
+
for (let i = 0; i < planned.length; i += PLAN_FILES) {
|
|
107
|
+
const part = planned.slice(i, i + PLAN_FILES)
|
|
108
|
+
const { res, b } = await post(`/publish/plan${qs}`, { files: part.map(([p, buf]) => ({ path: p, size: buf.length, md5: md5Of(buf) })) })
|
|
109
|
+
if (res.status === 404 && i === 0) return null
|
|
110
|
+
if (!res.ok || !b.ok) { reportFailure(res, b); return false }
|
|
111
|
+
have.push(...(b.have || []))
|
|
112
|
+
uploads.push(...(b.uploads || []))
|
|
113
|
+
}
|
|
114
|
+
const bytes = new Map(rest)
|
|
115
|
+
const sendBytes = uploads.reduce((n, u) => n + (bytes.get(u.path) || []).length, 0)
|
|
116
|
+
console.log(`📦 ${APP_ID}: ${planned.length} files — ${have.length} unchanged, ${uploads.length} to send (${(sendBytes / 1048576).toFixed(1)} MB, straight to storage)`)
|
|
117
|
+
let sent = 0
|
|
118
|
+
let failed = false
|
|
119
|
+
await pool(uploads, PARALLEL_UPLOADS, async (u) => {
|
|
120
|
+
if (failed) return
|
|
121
|
+
if (!(await putFile(u.path, bytes.get(u.path), u.url, qs))) { failed = true; return }
|
|
122
|
+
if (++sent % 100 === 0 || sent === uploads.length) console.log(` ↑ ${sent}/${uploads.length}`)
|
|
123
|
+
})
|
|
124
|
+
if (failed) return false
|
|
125
|
+
const empty = rest.filter(([, buf]) => buf.length === 0)
|
|
126
|
+
if (empty.length) {
|
|
127
|
+
const { res, b } = await post(`/publish/files${qs}`, { files: toB64(empty) })
|
|
128
|
+
if (!res.ok || !b.ok) { reportFailure(res, b); return false }
|
|
129
|
+
}
|
|
130
|
+
return rest.map(([p]) => p)
|
|
131
|
+
}
|
|
132
|
+
|
|
69
133
|
// Big files → storage directly. Returns the paths uploaded, or null when the server has no direct
|
|
70
134
|
// uploads yet (an older API: the caller falls back to batches).
|
|
71
135
|
async function uploadDirect(entries, qs) {
|
|
@@ -122,37 +186,45 @@ async function publishOnce() {
|
|
|
122
186
|
if (totalBytes <= BATCH_BYTES && entries.length <= BATCH_FILES) {
|
|
123
187
|
finalBody = { files: toB64(entries) } // small bundle: one request, as before
|
|
124
188
|
} else {
|
|
125
|
-
// Large bundle:
|
|
126
|
-
// the server only registers the release once every
|
|
189
|
+
// Large bundle: everything except the manifest first (only what changed, straight to storage; or in
|
|
190
|
+
// batches on an older API), then the manifest last — the server only registers the release once every
|
|
191
|
+
// referenced file is confirmed uploaded.
|
|
127
192
|
const rest = entries.filter(([p]) => p !== 'app.manifest.json').sort((a, b) => b[1].length - a[1].length)
|
|
128
|
-
const
|
|
129
|
-
|
|
130
|
-
if (
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
193
|
+
const byPlan = await publishByPlan(rest, qs)
|
|
194
|
+
if (byPlan === false) { if (WATCH) return false; process.exit(1) }
|
|
195
|
+
if (byPlan) {
|
|
196
|
+
finalBody = { files: toB64([['app.manifest.json', files['app.manifest.json']]]), uploaded: byPlan }
|
|
197
|
+
} else {
|
|
198
|
+
console.warn(' ⚠︎ this SmartLinks API has no publish plans yet — sending every file in batches')
|
|
199
|
+
const big = rest.filter(([, buf]) => buf.length >= DIRECT_BYTES)
|
|
200
|
+
let direct = []
|
|
201
|
+
if (big.length) {
|
|
202
|
+
const done = await uploadDirect(big, qs)
|
|
203
|
+
if (done === false) { if (WATCH) return false; process.exit(1) }
|
|
204
|
+
if (done) direct = done
|
|
205
|
+
else console.warn(' ⚠︎ this SmartLinks API has no direct uploads yet — sending big files in batches')
|
|
206
|
+
}
|
|
207
|
+
const viaBatches = rest.filter(([p]) => !direct.includes(p))
|
|
208
|
+
const batches = []
|
|
209
|
+
let cur = [], curBytes = 0
|
|
210
|
+
for (const e of viaBatches) {
|
|
211
|
+
if (cur.length && (curBytes + e[1].length > BATCH_BYTES || cur.length >= BATCH_FILES)) { batches.push(cur); cur = []; curBytes = 0 }
|
|
212
|
+
if (e[1].length > BATCH_BYTES) console.warn(` ⚠︎ ${e[0]} is ${(e[1].length / 1048576).toFixed(1)} MB — larger than one request allows; the server will likely reject it`)
|
|
213
|
+
cur.push(e); curBytes += e[1].length
|
|
214
|
+
}
|
|
215
|
+
if (cur.length) batches.push(cur)
|
|
216
|
+
console.log(`📦 ${APP_ID}: ${(totalBytes / 1048576).toFixed(1)} MB in ${entries.length} files — ${direct.length ? `${direct.length} sent directly, ` : ''}${batches.length} batch(es)`)
|
|
217
|
+
for (let i = 0; i < batches.length; i++) {
|
|
218
|
+
const { res, b } = await post(`/publish/files${qs}`, { files: toB64(batches[i]) })
|
|
219
|
+
if (!res.ok || !b.ok) {
|
|
220
|
+
reportFailure(res, b)
|
|
221
|
+
if (WATCH) return false
|
|
222
|
+
process.exit(1)
|
|
223
|
+
}
|
|
224
|
+
console.log(` ↑ batch ${i + 1}/${batches.length} (${batches[i].length} files)`)
|
|
152
225
|
}
|
|
153
|
-
|
|
226
|
+
finalBody = { files: toB64([['app.manifest.json', files['app.manifest.json']]]), uploaded: rest.map(([p]) => p) }
|
|
154
227
|
}
|
|
155
|
-
finalBody = { files: toB64([['app.manifest.json', files['app.manifest.json']]]), uploaded: rest.map(([p]) => p) }
|
|
156
228
|
}
|
|
157
229
|
|
|
158
230
|
const { res, b } = await post(`/publish${qs}`, finalBody)
|