@thatopen/services 0.28.2 → 0.29.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +129 -4
- package/dist/cli.js.map +1 -1
- package/dist/core/documents.d.ts +59 -0
- package/dist/index.cjs.js +5 -5
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.es.js +1038 -905
- package/dist/index.es.js.map +1 -1
- package/docs/ai-quickstart.md +24 -1
- package/docs/cli/mcp.md +5 -0
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -6960,6 +6960,14 @@ class ProjectDocuments {
|
|
|
6960
6960
|
projectId: this.projectId
|
|
6961
6961
|
});
|
|
6962
6962
|
}
|
|
6963
|
+
/**
|
|
6964
|
+
* The document's DRAFTS: each user's working copy, kept so that closing the
|
|
6965
|
+
* browser never loses work. A draft is not a version — it never enters the
|
|
6966
|
+
* history and never shows in the CDE. See {@link DocumentDrafts}.
|
|
6967
|
+
*/
|
|
6968
|
+
drafts(folderId) {
|
|
6969
|
+
return new DocumentDrafts(this.client, () => this.#descriptorFile(folderId));
|
|
6970
|
+
}
|
|
6963
6971
|
/**
|
|
6964
6972
|
* The legacy `derivedFileId` link, read from the newest version metadata
|
|
6965
6973
|
* that carries one. Only `fromFile` looks at it, to migrate the link into
|
|
@@ -6990,6 +6998,120 @@ class ProjectDocuments {
|
|
|
6990
6998
|
return found ? { id: idOf$1(found) } : void 0;
|
|
6991
6999
|
}
|
|
6992
7000
|
}
|
|
7001
|
+
const DRAFT_TAG = "draft:";
|
|
7002
|
+
const DRAFT_MAGIC = "TODRAFT1";
|
|
7003
|
+
class DocumentDrafts {
|
|
7004
|
+
constructor(client, holder) {
|
|
7005
|
+
this.client = client;
|
|
7006
|
+
this.holder = holder;
|
|
7007
|
+
}
|
|
7008
|
+
/** Every user's latest draft, without downloading any. Newest first. */
|
|
7009
|
+
async list() {
|
|
7010
|
+
const holder = await this.holder();
|
|
7011
|
+
if (!holder) return [];
|
|
7012
|
+
const latest = /* @__PURE__ */ new Map();
|
|
7013
|
+
for (const entry of await this.#entries(holder.id)) {
|
|
7014
|
+
const known = latest.get(entry.userId);
|
|
7015
|
+
if (!known || entry.savedAt > known.savedAt)
|
|
7016
|
+
latest.set(entry.userId, { userId: entry.userId, savedAt: entry.savedAt });
|
|
7017
|
+
}
|
|
7018
|
+
return [...latest.values()].sort((a, b) => +b.savedAt - +a.savedAt);
|
|
7019
|
+
}
|
|
7020
|
+
/**
|
|
7021
|
+
* One user's draft, downloaded — or, without `userId`, the newest of anyone.
|
|
7022
|
+
* Null when there is none.
|
|
7023
|
+
*/
|
|
7024
|
+
async get(options) {
|
|
7025
|
+
const holder = await this.holder();
|
|
7026
|
+
if (!holder) return null;
|
|
7027
|
+
const entries = (await this.#entries(holder.id)).filter((e) => !options?.userId || e.userId === options.userId).sort((a, b) => +b.savedAt - +a.savedAt);
|
|
7028
|
+
const newest = entries[0];
|
|
7029
|
+
if (!newest) return null;
|
|
7030
|
+
const response = await this.client.downloadHiddenFile(newest.hiddenId);
|
|
7031
|
+
const { meta, blob } = await unwrapDraft(await response.blob());
|
|
7032
|
+
return { userId: newest.userId, savedAt: newest.savedAt, meta, blob };
|
|
7033
|
+
}
|
|
7034
|
+
/**
|
|
7035
|
+
* Stores this user's draft, replacing their previous one: only the latest is
|
|
7036
|
+
* kept. Needs the document to have a descriptor (convert loose files first
|
|
7037
|
+
* with `fromFile`).
|
|
7038
|
+
*/
|
|
7039
|
+
async put(blob, options) {
|
|
7040
|
+
const holder = await this.holder();
|
|
7041
|
+
if (!holder) {
|
|
7042
|
+
throw new Error("This folder is not a document (no document.json): drafts need one.");
|
|
7043
|
+
}
|
|
7044
|
+
const tag = draftTag(options.userId);
|
|
7045
|
+
const previous = (await this.#entries(holder.id)).filter((e) => e.userId === options.userId);
|
|
7046
|
+
const created = await this.client.createHiddenFile(
|
|
7047
|
+
await wrapDraft(blob, options.meta ?? {}),
|
|
7048
|
+
holder.id,
|
|
7049
|
+
tag
|
|
7050
|
+
);
|
|
7051
|
+
const hiddenId = created.hiddenFileId;
|
|
7052
|
+
if (!hiddenId) throw new Error("The draft upload answered without a hiddenFileId.");
|
|
7053
|
+
await this.#deleteAll(previous.map((e) => e.hiddenId));
|
|
7054
|
+
return { userId: options.userId, savedAt: savedAtOf(hiddenId) };
|
|
7055
|
+
}
|
|
7056
|
+
/** Deletes every draft of this user (on an explicit save, typically). */
|
|
7057
|
+
async delete(options) {
|
|
7058
|
+
const holder = await this.holder();
|
|
7059
|
+
if (!holder) return;
|
|
7060
|
+
const mine = (await this.#entries(holder.id)).filter((e) => e.userId === options.userId);
|
|
7061
|
+
await this.#deleteAll(mine.map((e) => e.hiddenId));
|
|
7062
|
+
}
|
|
7063
|
+
async #entries(holderId) {
|
|
7064
|
+
const hidden = await this.client.getHiddenFilesByParent(holderId);
|
|
7065
|
+
return hidden.filter((h) => typeof h.tag === "string" && h.tag.startsWith(DRAFT_TAG)).map((h) => {
|
|
7066
|
+
const hiddenId = String(h._id);
|
|
7067
|
+
return {
|
|
7068
|
+
hiddenId,
|
|
7069
|
+
userId: h.tag.slice(DRAFT_TAG.length),
|
|
7070
|
+
savedAt: savedAtOf(hiddenId)
|
|
7071
|
+
};
|
|
7072
|
+
});
|
|
7073
|
+
}
|
|
7074
|
+
async #deleteAll(hiddenIds) {
|
|
7075
|
+
for (const id2 of hiddenIds) {
|
|
7076
|
+
try {
|
|
7077
|
+
await this.client.deleteHiddenFile(id2);
|
|
7078
|
+
} catch {
|
|
7079
|
+
}
|
|
7080
|
+
}
|
|
7081
|
+
}
|
|
7082
|
+
}
|
|
7083
|
+
function draftTag(userId) {
|
|
7084
|
+
const tag = `${DRAFT_TAG}${userId}`;
|
|
7085
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9:._-]{0,49}$/.test(tag)) {
|
|
7086
|
+
throw new Error(`A draft cannot be tagged for user "${userId}": ids must be letters, digits or :._- and short.`);
|
|
7087
|
+
}
|
|
7088
|
+
return tag;
|
|
7089
|
+
}
|
|
7090
|
+
function savedAtOf(objectId) {
|
|
7091
|
+
const seconds = parseInt(objectId.slice(0, 8), 16);
|
|
7092
|
+
return new Date(Number.isFinite(seconds) ? seconds * 1e3 : 0);
|
|
7093
|
+
}
|
|
7094
|
+
async function wrapDraft(blob, meta) {
|
|
7095
|
+
const metaBytes = new TextEncoder().encode(JSON.stringify(meta));
|
|
7096
|
+
const length = new Uint8Array(4);
|
|
7097
|
+
new DataView(length.buffer).setUint32(0, metaBytes.length, true);
|
|
7098
|
+
return new Blob([new TextEncoder().encode(DRAFT_MAGIC), length, metaBytes, blob]);
|
|
7099
|
+
}
|
|
7100
|
+
async function unwrapDraft(blob) {
|
|
7101
|
+
const head = new Uint8Array(await blob.slice(0, DRAFT_MAGIC.length + 4).arrayBuffer());
|
|
7102
|
+
if (new TextDecoder().decode(head.slice(0, DRAFT_MAGIC.length)) !== DRAFT_MAGIC) {
|
|
7103
|
+
return { meta: {}, blob };
|
|
7104
|
+
}
|
|
7105
|
+
const length = new DataView(head.buffer).getUint32(DRAFT_MAGIC.length, true);
|
|
7106
|
+
const start = DRAFT_MAGIC.length + 4;
|
|
7107
|
+
const metaText = await blob.slice(start, start + length).text();
|
|
7108
|
+
let meta = {};
|
|
7109
|
+
try {
|
|
7110
|
+
meta = JSON.parse(metaText);
|
|
7111
|
+
} catch {
|
|
7112
|
+
}
|
|
7113
|
+
return { meta, blob: blob.slice(start + length) };
|
|
7114
|
+
}
|
|
6993
7115
|
function formatOf(fileName, extension) {
|
|
6994
7116
|
const fromExt = (extension ?? "").replace(/^\./, "").toLowerCase();
|
|
6995
7117
|
if (fromExt) return fromExt;
|
|
@@ -33556,6 +33678,9 @@ function projectIdFrom(input) {
|
|
|
33556
33678
|
if (/^[0-9a-f]{24}$/i.test(trimmed)) return trimmed;
|
|
33557
33679
|
return trimmed.match(/projects?\/([0-9a-f]{24})/i)?.[1] ?? null;
|
|
33558
33680
|
}
|
|
33681
|
+
function appIdFrom(input) {
|
|
33682
|
+
return input.trim().match(/apps\/([0-9a-f]{24})/i)?.[1];
|
|
33683
|
+
}
|
|
33559
33684
|
class ChannelConnection {
|
|
33560
33685
|
constructor(credentials) {
|
|
33561
33686
|
this.credentials = credentials;
|
|
@@ -33716,12 +33841,12 @@ async function startMcpServer() {
|
|
|
33716
33841
|
);
|
|
33717
33842
|
server.tool(
|
|
33718
33843
|
"send-app-command",
|
|
33719
|
-
"Sends a command to a running That Open app through the platform channel and returns its reply. The app must be open in a browser tab and listening (apps scaffolded by `thatopen create` handle `ping` and `get-loaded-models` out of the box; apps declare their own commands in src/setups/channel.ts). While the user iterates with `thatopen serve`, omit app_id — the dev channel is keyed to the account; pass it once the app is published. `delivered: 0` means no tab of the app has the channel joined, usually because the app is simply not open — ask the user to open or reload the app tab.",
|
|
33844
|
+
"Sends a command to a running That Open app through the platform channel and returns its reply. To USE an app the person has open (rather than build one), pass the URL of their app tab as `project` and send `help` first: the app answers with its own commands and how to use them, and that is the manual to follow. The app must be open in a browser tab and listening (apps scaffolded by `thatopen create` handle `ping` and `get-loaded-models` out of the box; apps declare their own commands in src/setups/channel.ts). While the user iterates with `thatopen serve`, omit app_id — the dev channel is keyed to the account; pass it once the app is published. `delivered: 0` means no tab of the app has the channel joined, usually because the app is simply not open — ask the user to open or reload the app tab.",
|
|
33720
33845
|
{
|
|
33721
|
-
project: stringType().describe("The project id (24 hex characters), or the
|
|
33846
|
+
project: stringType().describe("The project id (24 hex characters), or the dashboard URL of the app tab: a URL with `/apps/<id>` gives the app_id too."),
|
|
33722
33847
|
command: stringType().describe('The command name the app handles, e.g. "ping" or "get-loaded-models".'),
|
|
33723
33848
|
payload: unknownType().optional().describe("JSON payload for the command, if it takes one."),
|
|
33724
|
-
app_id: stringType().regex(/^[0-9a-f]{24}$/i).optional().describe("The published app's id. Omit while the app runs under `thatopen serve
|
|
33849
|
+
app_id: stringType().regex(/^[0-9a-f]{24}$/i).optional().describe("The published app's id. Omit while the app runs under `thatopen serve`, or when `project` is a URL that carries it."),
|
|
33725
33850
|
wait_for_reply: booleanType().optional().describe("Default true. Set false for fire-and-forget notifications."),
|
|
33726
33851
|
timeout_ms: numberType().int().min(1e3).max(MAX_REPLY_TIMEOUT_MS).optional().describe(`How long to wait for the app's reply. Default ${DEFAULT_REPLY_TIMEOUT_MS}.`)
|
|
33727
33852
|
},
|
|
@@ -33734,7 +33859,7 @@ async function startMcpServer() {
|
|
|
33734
33859
|
}
|
|
33735
33860
|
const result = await connect2().send({
|
|
33736
33861
|
projectId,
|
|
33737
|
-
appId: args.app_id,
|
|
33862
|
+
appId: args.app_id ?? appIdFrom(args.project),
|
|
33738
33863
|
type: args.command,
|
|
33739
33864
|
payload: args.payload,
|
|
33740
33865
|
waitForReply: args.wait_for_reply !== false,
|