openwork-server 0.18.60 → 0.18.62
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -2
- package/bin/openwork-server.mjs +1 -1
- package/bin/platform.mjs +7 -7
- package/dist/{opencode-plugin-deps-1.18.30.tgz → opencode-plugin-deps-1.18.33.tgz} +0 -0
- package/dist/opencode-plugins/drive-file-tools.js +10 -8
- package/dist/opencode-plugins/openwork-cloud-skills-v2.js +87 -41
- package/dist/opencode-plugins/openwork-context-v2.js +67 -35
- package/dist/opencode-plugins/openwork-extensions-preview.js +9 -7
- package/dist/opencode-plugins/openwork-gateway-quota-v2.js +187 -0
- package/dist/opencode-plugins/openwork-gateway-quota.js +232 -23
- package/dist/opencode-plugins/openwork-provider-sync-v2.js +85 -2
- package/dist/openwork-server.mjs +1234 -371
- package/package.json +2 -2
- package/web/assets/{abnfDiagram-VCTEODGH-D0_4pXEx.js → abnfDiagram-VCTEODGH-DJzrT8Ql.js} +1 -1
- package/web/assets/app-CkmqBm_1.css +32 -0
- package/web/assets/{app-BCXQnJUH.js → app-_2KkkZIM.js} +435 -425
- package/web/assets/{arc-BSTwaImw.js → arc-KehB1xN5.js} +1 -1
- package/web/assets/{architectureDiagram-5GKGNRK7-cXwF0iP0.js → architectureDiagram-5GKGNRK7-C_GL2639.js} +1 -1
- package/web/assets/{artifact-code-view-CUXv3CYK.js → artifact-code-view-CYn_58gV.js} +1 -1
- package/web/assets/{artifact-spreadsheet-editor-CxhNYmjP.js → artifact-spreadsheet-editor-f6V-tYvi.js} +1 -1
- package/web/assets/{artifact-text-editor-k2TGQI-i.js → artifact-text-editor-CiSlW3kl.js} +1 -1
- package/web/assets/{blockDiagram-OSKFZWR5-Cl9p_z8u.js → blockDiagram-OSKFZWR5-DErH-7gY.js} +1 -1
- package/web/assets/{c4Diagram-7LVT6UL2-YQjnoUQ0.js → c4Diagram-7LVT6UL2-aML4z_n9.js} +1 -1
- package/web/assets/channel-7oM0ncHI.js +1 -0
- package/web/assets/{chunk-2Q5K7J3B-YZaSwlzj.js → chunk-2Q5K7J3B-Dw_IH9rW.js} +1 -1
- package/web/assets/{chunk-5VM5RSS4-BICC_wIw.js → chunk-5VM5RSS4-aeJEQ945.js} +1 -1
- package/web/assets/{chunk-F27PBJKO-CBxmcmxs.js → chunk-F27PBJKO-hZwti-Oc.js} +1 -1
- package/web/assets/{chunk-HLEWEB6X-2h2z7zt8.js → chunk-HLEWEB6X-JFNzm3Ge.js} +1 -1
- package/web/assets/{chunk-JWPE2WC7-BnAMOpXl.js → chunk-JWPE2WC7-B34hvYk7.js} +1 -1
- package/web/assets/{chunk-POPQ4Y6H-pvDYQBBM.js → chunk-POPQ4Y6H-CDqgEp-q.js} +1 -1
- package/web/assets/{chunk-SVP7TREG-QuKLl3v0.js → chunk-SVP7TREG-B65ta9aQ.js} +1 -1
- package/web/assets/{chunk-XXDRQBXY-4Bt9wCBO.js → chunk-XXDRQBXY-DjTi6c9m.js} +1 -1
- package/web/assets/{chunk-ZLD2IHE6-C_RSrkqU.js → chunk-ZLD2IHE6-CbviXFvb.js} +1 -1
- package/web/assets/classDiagram-CYGNFDIV-BkidFauP.js +1 -0
- package/web/assets/classDiagram-v2-TLXNO2FR-BkidFauP.js +1 -0
- package/web/assets/{cose-bilkent-JH36ORCC-CciOYQb4.js → cose-bilkent-JH36ORCC-Cl9SO5zd.js} +1 -1
- package/web/assets/{cynefin-OW5HDTMX-k8-mUlq4.js → cynefin-OW5HDTMX-CAcz2tHt.js} +1 -1
- package/web/assets/{cynefinDiagram-5FMLGOSQ-D1qAHzq3.js → cynefinDiagram-5FMLGOSQ-BJHft4FO.js} +1 -1
- package/web/assets/{dagre-OS7QT2EB-BP1VuF_z.js → dagre-OS7QT2EB-DbdnY1uS.js} +1 -1
- package/web/assets/{diagram-S7CK7UJ4-crXkWleC.js → diagram-S7CK7UJ4-C07WD5Lc.js} +1 -1
- package/web/assets/{diagram-UQ7AKVKN-CtHAmCY-.js → diagram-UQ7AKVKN-D8VCjq9x.js} +1 -1
- package/web/assets/{diagram-VSXAHHWV-C-Fhuwy8.js → diagram-VSXAHHWV-D1XSfMnK.js} +1 -1
- package/web/assets/{diagram-VX7I27RA-DkRkNPuj.js → diagram-VX7I27RA-3GZe-yrf.js} +1 -1
- package/web/assets/{diagram-Z3DM3KII-DVERPCFS.js → diagram-Z3DM3KII-CI43WcIB.js} +1 -1
- package/web/assets/{ebnfDiagram-PWID7BFC-PrSifYGF.js → ebnfDiagram-PWID7BFC-CTzouade.js} +1 -1
- package/web/assets/{erDiagram-2YWLMYGG-D8-wfUYq.js → erDiagram-2YWLMYGG-CNcqsnqx.js} +1 -1
- package/web/assets/{flowDiagram-T62WH6J4-BS-_wkGe.js → flowDiagram-T62WH6J4-pomAqjgF.js} +1 -1
- package/web/assets/{ganttDiagram-EL5Y4UJY-D9PWmuWE.js → ganttDiagram-EL5Y4UJY-C1dNaxxj.js} +1 -1
- package/web/assets/{gitGraphDiagram-WWUBYQGX-DIP9p63c.js → gitGraphDiagram-WWUBYQGX-Ni8GECg1.js} +1 -1
- package/web/assets/{infoDiagram-FKFFQAWI-BzU6-acC.js → infoDiagram-FKFFQAWI-C0kvE7Zu.js} +1 -1
- package/web/assets/{ishikawaDiagram-5VMMS53U-CvYS2ayc.js → ishikawaDiagram-5VMMS53U-CxmP_irp.js} +1 -1
- package/web/assets/{journeyDiagram-EYS64GPL-Cr0qdK-D.js → journeyDiagram-EYS64GPL-60GkPgH5.js} +1 -1
- package/web/assets/{kanban-definition-UXKFOSKX-DhffGp7U.js → kanban-definition-UXKFOSKX-ByxcB0T_.js} +1 -1
- package/web/assets/{layout-b5jqOIam.js → layout-BD9ef90s.js} +1 -1
- package/web/assets/{linear-zLNMwGhI.js → linear-Dp9l0lBr.js} +1 -1
- package/web/assets/{mermaid.core-DB_TNiSG.js → mermaid.core-FKm3Akes.js} +6 -6
- package/web/assets/{mindmap-definition-THT77NOG-DkQz2hbh.js → mindmap-definition-THT77NOG-i7WJlWHV.js} +1 -1
- package/web/assets/{pegDiagram-XKGWAZYB-CB313CDF.js → pegDiagram-XKGWAZYB-DfI-LsJq.js} +1 -1
- package/web/assets/{pieDiagram-E7YTZNPT-yaDJVY4d.js → pieDiagram-E7YTZNPT-kcgWYBSb.js} +1 -1
- package/web/assets/{quadrantDiagram-AXDQQJYC-BkV8iwHS.js → quadrantDiagram-AXDQQJYC-DVyziZnc.js} +1 -1
- package/web/assets/{railroadDiagram-O6MQD6OU-T_JVDRpV.js → railroadDiagram-O6MQD6OU-CWgHq9nv.js} +1 -1
- package/web/assets/{requirementDiagram-IS5BZ75X-B68yTCqr.js → requirementDiagram-IS5BZ75X-B7fgi4lz.js} +1 -1
- package/web/assets/{sankeyDiagram-P5KCCOFB-Bx31O26O.js → sankeyDiagram-P5KCCOFB-D1-pD_qh.js} +1 -1
- package/web/assets/{sequenceDiagram-WJ2MYXX4-CsCoFTAg.js → sequenceDiagram-WJ2MYXX4-B6oeRmVj.js} +1 -1
- package/web/assets/{sizeCapture-INFHLROL-B3a49hkJ.js → sizeCapture-INFHLROL-BTpKGFt5.js} +1 -1
- package/web/assets/{stateDiagram-XQSTLZYL-DjCdHmx5.js → stateDiagram-XQSTLZYL-zXJRxE6h.js} +1 -1
- package/web/assets/stateDiagram-v2-IH3M54BS-CmwZ6tXj.js +1 -0
- package/web/assets/{swimlanes-V6O3JKXN-4u8S1I3e.js → swimlanes-V6O3JKXN-Dc7MXPb3.js} +1 -1
- package/web/assets/swimlanesDiagram-JKAHXJPX-BXoJEx2b.js +8 -0
- package/web/assets/{timeline-definition-24CTP7MA-r3GnOjJo.js → timeline-definition-24CTP7MA-DYhgbnjW.js} +1 -1
- package/web/assets/{vennDiagram-4TSXK5OY-59IlTU54.js → vennDiagram-4TSXK5OY-DRnme7za.js} +1 -1
- package/web/assets/{wardleyDiagram-VM6X3IG4-C3ALfLHE.js → wardleyDiagram-VM6X3IG4-wAoo4GsZ.js} +1 -1
- package/web/assets/{workspace-file-tree-C6GhNx3L.js → workspace-file-tree-DmAwvqtx.js} +1 -1
- package/web/assets/{xychartDiagram-S5SC5T6Z-CoYtWlkb.js → xychartDiagram-S5SC5T6Z-_Z_TOWBn.js} +1 -1
- package/web/index.html +2 -2
- package/web/assets/app-DVblmMbF.css +0 -32
- package/web/assets/channel-DwV664-P.js +0 -1
- package/web/assets/classDiagram-CYGNFDIV-BKhd9faI.js +0 -1
- package/web/assets/classDiagram-v2-TLXNO2FR-BKhd9faI.js +0 -1
- package/web/assets/stateDiagram-v2-IH3M54BS-Cf0tqY7x.js +0 -1
- package/web/assets/swimlanesDiagram-JKAHXJPX-COK5qx0u.js +0 -8
package/README.md
CHANGED
|
@@ -9,7 +9,7 @@ npm install -g openwork-server
|
|
|
9
9
|
openwork-server --workspace /path/to/workspace --approval auto
|
|
10
10
|
```
|
|
11
11
|
|
|
12
|
-
The npm package
|
|
12
|
+
The npm package requires stable Node.js 24.x, version 24.22.1 or newer within that major, on macOS, Linux, and Windows (arm64 or x64); Bun is not required at runtime. Older Node releases and other majors (including 25+) are rejected by the runtime gate. This security baseline also replaces the previous Node.js 22.13 minimum.
|
|
13
13
|
|
|
14
14
|
### Self-hosted web UI
|
|
15
15
|
|
|
@@ -34,8 +34,14 @@ persisted in `~/.openwork/openwork-server/web-tokens.json` so they survive
|
|
|
34
34
|
restarts; `--token`/`--host-token` or `OPENWORK_TOKEN`/`OPENWORK_HOST_TOKEN`
|
|
35
35
|
override them.
|
|
36
36
|
|
|
37
|
+
In `web` mode the UI's pages never carry CORS headers, and the server only
|
|
38
|
+
answers requests addressed to `localhost`, an IP address or the `--host` name.
|
|
39
|
+
Add other names with `--allowed-hosts` (or `OPENWORK_ALLOWED_HOSTS`,
|
|
40
|
+
comma-separated; `*` allows any).
|
|
41
|
+
|
|
37
42
|
To reach it from other machines, keep the server on `127.0.0.1` and put a TLS
|
|
38
|
-
proxy in front (e.g. `tailscale serve --bg --https=8787 http://127.0.0.1:8787`)
|
|
43
|
+
proxy in front (e.g. `tailscale serve --bg --https=8787 http://127.0.0.1:8787`),
|
|
44
|
+
then allow the proxy's name (e.g. `--allowed-hosts my-machine.tailnet.ts.net`).
|
|
39
45
|
Browsers require a secure context for the UI on any non-localhost origin.
|
|
40
46
|
Set `OPENWORK_OPENCODE_BIN` to use your own OpenCode install instead of the
|
|
41
47
|
managed download; `/health` reports both the pinned `opencodeVersion` and the
|
|
@@ -175,6 +181,7 @@ Defaults to `~/.config/openwork/server.json` (override with `OPENWORK_SERVER_CON
|
|
|
175
181
|
- `OPENWORK_APPROVAL_TIMEOUT_MS`
|
|
176
182
|
- `OPENWORK_WORKSPACES` (JSON array or comma-separated list of paths)
|
|
177
183
|
- `OPENWORK_CORS_ORIGINS` (comma-separated list or `*`)
|
|
184
|
+
- `OPENWORK_ALLOWED_HOSTS` (comma-separated host names or `*`; `web` mode always checks the Host)
|
|
178
185
|
- `OPENWORK_OPENCODE_BASE_URL`
|
|
179
186
|
- `OPENWORK_OPENCODE_DIRECTORY`
|
|
180
187
|
- `OPENWORK_OPENCODE_USERNAME`
|
package/bin/openwork-server.mjs
CHANGED
|
@@ -29,7 +29,7 @@ const nodeBundle = fromPackage("./dist/openwork-server.mjs");
|
|
|
29
29
|
if (existsSync(nodeBundle)) {
|
|
30
30
|
if (!nodeVersionSupported(process.versions.node)) {
|
|
31
31
|
console.error(
|
|
32
|
-
`openwork-server needs Node.js ${MIN_NODE_VERSION} or newer (found ${process.versions.node}). Install a
|
|
32
|
+
`openwork-server needs Node.js 24.x, version ${MIN_NODE_VERSION} or newer within that major (found ${process.versions.node}). Install a supported Node.js 24 release and try again.`,
|
|
33
33
|
);
|
|
34
34
|
process.exit(1);
|
|
35
35
|
}
|
package/bin/platform.mjs
CHANGED
|
@@ -16,13 +16,13 @@ export function serverBinaryName(platform, arch) {
|
|
|
16
16
|
return `openwork-server-${target.target}${platform === "win32" ? ".exe" : ""}`;
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
export const MIN_NODE_VERSION = "22.
|
|
19
|
+
// Security baseline: only patched releases on the supported Node 24 line.
|
|
20
|
+
// New majors must be reviewed explicitly, not accepted by a lower-bound check.
|
|
21
|
+
export const MIN_NODE_VERSION = "24.22.1";
|
|
22
22
|
|
|
23
23
|
export function nodeVersionSupported(version) {
|
|
24
|
-
const
|
|
25
|
-
if (
|
|
26
|
-
|
|
27
|
-
return major >
|
|
24
|
+
const match = /^v?(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/.exec(version);
|
|
25
|
+
if (!match) return false;
|
|
26
|
+
const [, major, minor, patch] = match.map(Number);
|
|
27
|
+
return major === 24 && (minor > 22 || (minor === 22 && patch >= 1));
|
|
28
28
|
}
|
|
Binary file
|
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
// The host performs these actions for its signed-in caller, as with drive_upload_file. Agents use these tools, which ask before writes.
|
|
3
3
|
export const DRIVE_FILE_ACTIONS = ["drive_pull_file", "drive_file_status", "drive_push_file"];
|
|
4
|
-
const connectionId = z.string().regex(/^(google-workspace|emc_[A-Za-z0-9]+)$/).optional()
|
|
5
|
-
.describe("Selected Google Workspace connection; omit for the default. Unavailable selections never fall back to another account.");
|
|
4
|
+
const connectionId = z.string().regex(/^(google-workspace|microsoft-365|emc_[A-Za-z0-9]+)$/).optional()
|
|
5
|
+
.describe("Selected Google Workspace or Microsoft 365 connection; omit for the default. Unavailable selections never fall back to another account.");
|
|
6
6
|
const workspacePath = z.string().min(1).max(4096);
|
|
7
7
|
export const drivePullFileArgs = z.object({
|
|
8
|
-
fileId: z.string().min(1).max(2048).describe("Drive file ID or link."),
|
|
8
|
+
fileId: z.string().min(1).max(2048).describe("Google Drive file ID or link, or a OneDrive, SharePoint or Teams file link or item id."),
|
|
9
|
+
driveId: z.string().min(1).max(512).optional().describe("Microsoft 365 only: the driveId that came with the item id from a search or folder listing."),
|
|
10
|
+
provider: z.enum(["google-workspace", "microsoft-365"]).optional().describe("Only needed for a Microsoft item id without driveId; links and driveId identify Microsoft 365 files."),
|
|
9
11
|
path: workspacePath.optional().describe("Workspace path to save to. Defaults to drive/<file name>."),
|
|
10
|
-
format: z.string().regex(/^[a-z]{2,5}$/).optional().describe("Docs, Sheets, Slides and Drawings
|
|
12
|
+
format: z.string().regex(/^[a-z]{2,5}$/).optional().describe("Docs, Sheets, Slides and Drawings: xlsx, csv (first tab only), docx, md, txt, pptx, pdf, png or svg. OneDrive and SharePoint: pdf converts an Office file."),
|
|
11
13
|
connectionId,
|
|
12
14
|
overwrite: z.boolean().optional().describe("Replace a local copy even if it has edits not in Drive. Asks the person first."),
|
|
13
15
|
}).strict();
|
|
@@ -16,7 +18,7 @@ export const driveFileStatusArgs = z.object({
|
|
|
16
18
|
}).strict();
|
|
17
19
|
export const drivePushFileArgs = z.object({
|
|
18
20
|
path: workspacePath.describe("Workspace path of a pulled file."),
|
|
19
|
-
mode: z.enum(["replace", "copy"]).optional().describe("replace (default for stored files) updates the same
|
|
21
|
+
mode: z.enum(["replace", "copy"]).optional().describe("replace (default for stored files) updates the same file; copy saves a new file next to it."),
|
|
20
22
|
name: z.string().min(1).max(255).optional().describe("Name for a copy."),
|
|
21
23
|
}).strict();
|
|
22
24
|
function isRecord(value) {
|
|
@@ -33,7 +35,7 @@ async function askPermission(context, permission, patterns, metadata) {
|
|
|
33
35
|
export function createDriveFileTools(call) {
|
|
34
36
|
return {
|
|
35
37
|
drive_pull_file: {
|
|
36
|
-
description: "Save a Google Drive file into the workspace and return its path and revision, never its content. Use it to analyze or edit Office, PDF, CSV or any large file with shell tools. Docs, Sheets and Slides arrive as docx, xlsx and pptx unless format says otherwise.",
|
|
38
|
+
description: "Save a Google Drive, OneDrive or SharePoint file into the workspace and return its path and revision, never its content. Use it to analyze or edit Office, PDF, CSV or any large file with shell tools. Docs, Sheets and Slides arrive as docx, xlsx and pptx unless format says otherwise.",
|
|
37
39
|
args: drivePullFileArgs.shape,
|
|
38
40
|
async execute(rawArgs, context) {
|
|
39
41
|
const args = drivePullFileArgs.parse(rawArgs ?? {});
|
|
@@ -43,14 +45,14 @@ export function createDriveFileTools(call) {
|
|
|
43
45
|
},
|
|
44
46
|
},
|
|
45
47
|
drive_file_status: {
|
|
46
|
-
description: "For files pulled with drive_pull_file, report whether the local copy has edits not saved
|
|
48
|
+
description: "For files pulled with drive_pull_file, report whether the local copy has edits not saved back and whether the file changed in Drive, OneDrive or SharePoint since the pull.",
|
|
47
49
|
args: driveFileStatusArgs.shape,
|
|
48
50
|
async execute(rawArgs, context) {
|
|
49
51
|
return JSON.stringify(await call("drive_file_status", driveFileStatusArgs.parse(rawArgs ?? {}), context), null, 2);
|
|
50
52
|
},
|
|
51
53
|
},
|
|
52
54
|
drive_push_file: {
|
|
53
|
-
description: "Save a pulled file back to
|
|
55
|
+
description: "Save a pulled file back to where it came from when the person asks. Asks the person first. Stored files, including Office files in OneDrive and SharePoint, are replaced in place as a new version; Google Docs, Sheets and Slides and PDF conversions are saved as a new file next to the original. Returns drive_conflict, and changes nothing, when someone changed the file since the pull: pull it again and reapply the edits, or save a copy.",
|
|
54
56
|
args: drivePushFileArgs.shape,
|
|
55
57
|
async execute(rawArgs, context) {
|
|
56
58
|
const args = drivePushFileArgs.parse(rawArgs ?? {});
|
|
@@ -19,6 +19,9 @@ const MAX_SKILLS = 150;
|
|
|
19
19
|
const NATIVE_BUDGET_CHARS = 64_000;
|
|
20
20
|
const NATIVE_DESCRIPTION_CHARS = 1_024;
|
|
21
21
|
const MAX_MENTIONED_SKILLS = 3;
|
|
22
|
+
// Bodies kept for the skills messages used most recently, so using one again
|
|
23
|
+
// needs no read before the model runs. Every other skill loads on use.
|
|
24
|
+
const MAX_WARM_SKILLS = 8;
|
|
22
25
|
const skillId = (skill) => `${CLOUD_SKILL_ID_PREFIX}${skill.name}`;
|
|
23
26
|
// Not SKILL.md: the native skill tool must not scan a fake directory. The engine
|
|
24
27
|
// requires path and disables this whole plugin, OpenWork's context tools
|
|
@@ -60,10 +63,10 @@ function budgeted(skills, budget, descriptionChars, content) {
|
|
|
60
63
|
return result;
|
|
61
64
|
}
|
|
62
65
|
/**
|
|
63
|
-
* Each organization skill is a native skill
|
|
64
|
-
*
|
|
65
|
-
* digest Den published
|
|
66
|
-
*
|
|
66
|
+
* Each organization skill is a native skill with its full description. A
|
|
67
|
+
* skill whose authorized SKILL.md has been read for a message, verified against
|
|
68
|
+
* the digest Den published, carries it; every other skill keeps the discovery
|
|
69
|
+
* note, so it still works through get_skill.
|
|
67
70
|
*/
|
|
68
71
|
export function nativeSkills(catalog, contents) {
|
|
69
72
|
return budgeted(catalog.skills, NATIVE_BUDGET_CHARS, NATIVE_DESCRIPTION_CHARS, skill => {
|
|
@@ -113,15 +116,19 @@ export function mentionedSkillIds(text, skills) {
|
|
|
113
116
|
}
|
|
114
117
|
/** Setup starts background discovery without making Cloud a dependency of
|
|
115
118
|
* chatting or local skill discovery. Only authorized catalog entries and
|
|
116
|
-
* digest-verified bodies are added to the native registry.
|
|
119
|
+
* digest-verified bodies are added to the native registry. Discovery reads
|
|
120
|
+
* metadata only; a body is read when a message names or attaches its skill. */
|
|
117
121
|
export async function registerCloudSkillDiscovery(context) {
|
|
118
122
|
let skills = [];
|
|
123
|
+
let catalog = { skills: [] };
|
|
119
124
|
let fingerprint = "[]";
|
|
120
125
|
let closed = false;
|
|
121
126
|
let refreshing = false;
|
|
122
|
-
// Verified SKILL.md bodies by digest, in memory only
|
|
123
|
-
// when Den publishes a new digest
|
|
127
|
+
// Verified SKILL.md bodies by digest, in memory only, for the warm skills.
|
|
128
|
+
// A body is re-read only when Den publishes a new digest for one of them.
|
|
124
129
|
const contents = new Map();
|
|
130
|
+
// Skill IDs messages used, most recent last.
|
|
131
|
+
const warm = [];
|
|
125
132
|
const controller = new AbortController();
|
|
126
133
|
let registration;
|
|
127
134
|
let promptHook;
|
|
@@ -134,44 +141,34 @@ export async function registerCloudSkillDiscovery(context) {
|
|
|
134
141
|
});
|
|
135
142
|
return response.ok ? await response.json() : null;
|
|
136
143
|
};
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
144
|
+
/** Read listed skills' bodies in one request and keep those matching Den's digest. */
|
|
145
|
+
const readBodies = async (entries, use) => {
|
|
146
|
+
const loadable = entries.filter(skill => skill.url && skill.digest && !skill.setup);
|
|
147
|
+
if (loadable.length === 0)
|
|
148
|
+
return;
|
|
149
|
+
const result = z.object({ contents: z.record(z.string(), z.string()) })
|
|
150
|
+
.safeParse(await call("openwork_skill_read", { urls: loadable.map(skill => skill.url), ...(use ? { use } : {}) }).catch(() => null));
|
|
151
|
+
if (!result.success)
|
|
152
|
+
return;
|
|
153
|
+
for (const skill of loadable) {
|
|
154
|
+
const body = result.data.contents[skill.url];
|
|
146
155
|
// Only a body that matches Den's published digest is used.
|
|
147
|
-
if (
|
|
148
|
-
contents.set(skill.digest,
|
|
156
|
+
if (body !== undefined && sha256Digest(body) === skill.digest)
|
|
157
|
+
contents.set(skill.digest, body);
|
|
149
158
|
}
|
|
150
159
|
};
|
|
151
|
-
const
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
promptHook = await context.session.hook("prompt", event => {
|
|
155
|
-
if (skills.length === 0)
|
|
156
|
-
return;
|
|
157
|
-
const attached = new Set((event.prompt.skills ?? []).map(skill => skill.id));
|
|
158
|
-
const mentioned = mentionedSkillIds(event.prompt.text, skills).filter(id => !attached.has(id));
|
|
159
|
-
if (mentioned.length === 0)
|
|
160
|
-
return;
|
|
161
|
-
event.prompt.skills = [...(event.prompt.skills ?? []), ...mentioned.map(id => ({ id }))];
|
|
162
|
-
});
|
|
160
|
+
const warmEntries = () => {
|
|
161
|
+
const listed = new Set(nativeSkills(catalog, contents).map(skill => skill.id));
|
|
162
|
+
return catalog.skills.filter(skill => warm.includes(skillId(skill)) && listed.has(skillId(skill)));
|
|
163
163
|
};
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
const
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
const catalog = catalogSchema.parse(value);
|
|
173
|
-
await loadContents(catalog);
|
|
174
|
-
await ensurePromptHook();
|
|
164
|
+
// Registry updates run one at a time, from refreshes and prompts alike.
|
|
165
|
+
let publishing = Promise.resolve();
|
|
166
|
+
const publish = () => {
|
|
167
|
+
publishing = publishing.catch(() => { }).then(async () => {
|
|
168
|
+
const wanted = new Set(warmEntries().flatMap(skill => skill.digest && !skill.setup ? [skill.digest] : []));
|
|
169
|
+
for (const digest of contents.keys())
|
|
170
|
+
if (!wanted.has(digest))
|
|
171
|
+
contents.delete(digest);
|
|
175
172
|
const next = nativeSkills(catalog, contents);
|
|
176
173
|
const key = JSON.stringify(next);
|
|
177
174
|
if (closed || key === fingerprint)
|
|
@@ -198,6 +195,54 @@ export async function registerCloudSkillDiscovery(context) {
|
|
|
198
195
|
await context.skill.reload();
|
|
199
196
|
}
|
|
200
197
|
fingerprint = key;
|
|
198
|
+
});
|
|
199
|
+
return publishing;
|
|
200
|
+
};
|
|
201
|
+
const ensurePromptHook = async () => {
|
|
202
|
+
if (promptHook || !context.session)
|
|
203
|
+
return;
|
|
204
|
+
promptHook = await context.session.hook("prompt", async (event) => {
|
|
205
|
+
if (skills.length === 0)
|
|
206
|
+
return;
|
|
207
|
+
const attached = new Set((event.prompt.skills ?? []).map(skill => skill.id));
|
|
208
|
+
const mentioned = mentionedSkillIds(event.prompt.text, skills).filter(id => !attached.has(id));
|
|
209
|
+
if (mentioned.length)
|
|
210
|
+
event.prompt.skills = [...(event.prompt.skills ?? []), ...mentioned.map(id => ({ id }))];
|
|
211
|
+
// Only registered skills: an unlisted ID never costs a read.
|
|
212
|
+
const registered = new Set(skills.map(skill => skill.id));
|
|
213
|
+
const used = new Set([...attached, ...mentioned].filter(id => registered.has(id)));
|
|
214
|
+
const entries = catalog.skills.filter(skill => used.has(skillId(skill)));
|
|
215
|
+
if (entries.length === 0)
|
|
216
|
+
return;
|
|
217
|
+
try {
|
|
218
|
+
// Freshly authorized for this message, and counted by Den as a use.
|
|
219
|
+
// A body that cannot be read leaves the discovery note: get_skill still works.
|
|
220
|
+
await readBodies(entries, true);
|
|
221
|
+
for (const skill of entries) {
|
|
222
|
+
const id = skillId(skill);
|
|
223
|
+
warm.splice(0, warm.length, ...warm.filter(entry => entry !== id), id);
|
|
224
|
+
}
|
|
225
|
+
warm.splice(0, Math.max(0, warm.length - MAX_WARM_SKILLS));
|
|
226
|
+
await publish();
|
|
227
|
+
}
|
|
228
|
+
catch {
|
|
229
|
+
// A message is never held back by Cloud skill loading.
|
|
230
|
+
}
|
|
231
|
+
});
|
|
232
|
+
};
|
|
233
|
+
const refresh = async () => {
|
|
234
|
+
if (closed || refreshing)
|
|
235
|
+
return;
|
|
236
|
+
refreshing = true;
|
|
237
|
+
try {
|
|
238
|
+
const value = await call("openwork_skills", {});
|
|
239
|
+
if (value === null)
|
|
240
|
+
return;
|
|
241
|
+
catalog = catalogSchema.parse(value);
|
|
242
|
+
// Only warm skills whose digest changed are read again.
|
|
243
|
+
await readBodies(warmEntries().filter(skill => skill.digest && !contents.has(skill.digest)), false);
|
|
244
|
+
await ensurePromptHook();
|
|
245
|
+
await publish();
|
|
201
246
|
}
|
|
202
247
|
catch {
|
|
203
248
|
// Cloud readiness is never a prerequisite to chatting. Discovery entries
|
|
@@ -216,6 +261,7 @@ export async function registerCloudSkillDiscovery(context) {
|
|
|
216
261
|
controller.abort();
|
|
217
262
|
skills = [];
|
|
218
263
|
contents.clear();
|
|
264
|
+
await publishing.catch(() => { });
|
|
219
265
|
await promptHook?.dispose();
|
|
220
266
|
await registration?.dispose();
|
|
221
267
|
};
|
|
@@ -13813,6 +13813,7 @@ var MAX_SKILLS = 150;
|
|
|
13813
13813
|
var NATIVE_BUDGET_CHARS = 64000;
|
|
13814
13814
|
var NATIVE_DESCRIPTION_CHARS = 1024;
|
|
13815
13815
|
var MAX_MENTIONED_SKILLS = 3;
|
|
13816
|
+
var MAX_WARM_SKILLS = 8;
|
|
13816
13817
|
var skillId = (skill) => `${CLOUD_SKILL_ID_PREFIX}${skill.name}`;
|
|
13817
13818
|
var skillPath = (skill) => `/openwork/remote-skills/${skill.name}.md`;
|
|
13818
13819
|
function discoveryContent(skill) {
|
|
@@ -13890,10 +13891,12 @@ function mentionedSkillIds(text2, skills) {
|
|
|
13890
13891
|
}
|
|
13891
13892
|
async function registerCloudSkillDiscovery(context) {
|
|
13892
13893
|
let skills = [];
|
|
13894
|
+
let catalog = { skills: [] };
|
|
13893
13895
|
let fingerprint = "[]";
|
|
13894
13896
|
let closed = false;
|
|
13895
13897
|
let refreshing = false;
|
|
13896
13898
|
const contents = new Map;
|
|
13899
|
+
const warm = [];
|
|
13897
13900
|
const controller = new AbortController;
|
|
13898
13901
|
let registration;
|
|
13899
13902
|
let promptHook;
|
|
@@ -13907,43 +13910,30 @@ async function registerCloudSkillDiscovery(context) {
|
|
|
13907
13910
|
});
|
|
13908
13911
|
return response.ok ? await response.json() : null;
|
|
13909
13912
|
};
|
|
13910
|
-
const
|
|
13911
|
-
const
|
|
13912
|
-
|
|
13913
|
-
if (!wanted.has(digest))
|
|
13914
|
-
contents.delete(digest);
|
|
13915
|
-
for (const skill of catalog.skills) {
|
|
13916
|
-
if (closed || !skill.digest || !skill.url || skill.setup || contents.has(skill.digest))
|
|
13917
|
-
continue;
|
|
13918
|
-
const result = exports_external.object({ content: exports_external.string() }).safeParse(await call("openwork_skill_read", { url: skill.url }).catch(() => null));
|
|
13919
|
-
if (result.success && sha256Digest(result.data.content) === skill.digest)
|
|
13920
|
-
contents.set(skill.digest, result.data.content);
|
|
13921
|
-
}
|
|
13922
|
-
};
|
|
13923
|
-
const ensurePromptHook = async () => {
|
|
13924
|
-
if (promptHook || !context.session)
|
|
13913
|
+
const readBodies = async (entries, use) => {
|
|
13914
|
+
const loadable = entries.filter((skill) => skill.url && skill.digest && !skill.setup);
|
|
13915
|
+
if (loadable.length === 0)
|
|
13925
13916
|
return;
|
|
13926
|
-
|
|
13927
|
-
|
|
13928
|
-
return;
|
|
13929
|
-
const attached = new Set((event.prompt.skills ?? []).map((skill) => skill.id));
|
|
13930
|
-
const mentioned = mentionedSkillIds(event.prompt.text, skills).filter((id) => !attached.has(id));
|
|
13931
|
-
if (mentioned.length === 0)
|
|
13932
|
-
return;
|
|
13933
|
-
event.prompt.skills = [...event.prompt.skills ?? [], ...mentioned.map((id) => ({ id }))];
|
|
13934
|
-
});
|
|
13935
|
-
};
|
|
13936
|
-
const refresh = async () => {
|
|
13937
|
-
if (closed || refreshing)
|
|
13917
|
+
const result = exports_external.object({ contents: exports_external.record(exports_external.string(), exports_external.string()) }).safeParse(await call("openwork_skill_read", { urls: loadable.map((skill) => skill.url), ...use ? { use } : {} }).catch(() => null));
|
|
13918
|
+
if (!result.success)
|
|
13938
13919
|
return;
|
|
13939
|
-
|
|
13940
|
-
|
|
13941
|
-
|
|
13942
|
-
|
|
13943
|
-
|
|
13944
|
-
|
|
13945
|
-
|
|
13946
|
-
|
|
13920
|
+
for (const skill of loadable) {
|
|
13921
|
+
const body = result.data.contents[skill.url];
|
|
13922
|
+
if (body !== undefined && sha256Digest(body) === skill.digest)
|
|
13923
|
+
contents.set(skill.digest, body);
|
|
13924
|
+
}
|
|
13925
|
+
};
|
|
13926
|
+
const warmEntries = () => {
|
|
13927
|
+
const listed = new Set(nativeSkills(catalog, contents).map((skill) => skill.id));
|
|
13928
|
+
return catalog.skills.filter((skill) => warm.includes(skillId(skill)) && listed.has(skillId(skill)));
|
|
13929
|
+
};
|
|
13930
|
+
let publishing = Promise.resolve();
|
|
13931
|
+
const publish = () => {
|
|
13932
|
+
publishing = publishing.catch(() => {}).then(async () => {
|
|
13933
|
+
const wanted = new Set(warmEntries().flatMap((skill) => skill.digest && !skill.setup ? [skill.digest] : []));
|
|
13934
|
+
for (const digest of contents.keys())
|
|
13935
|
+
if (!wanted.has(digest))
|
|
13936
|
+
contents.delete(digest);
|
|
13947
13937
|
const next = nativeSkills(catalog, contents);
|
|
13948
13938
|
const key = JSON.stringify(next);
|
|
13949
13939
|
if (closed || key === fingerprint)
|
|
@@ -13968,6 +13958,47 @@ async function registerCloudSkillDiscovery(context) {
|
|
|
13968
13958
|
await context.skill.reload();
|
|
13969
13959
|
}
|
|
13970
13960
|
fingerprint = key;
|
|
13961
|
+
});
|
|
13962
|
+
return publishing;
|
|
13963
|
+
};
|
|
13964
|
+
const ensurePromptHook = async () => {
|
|
13965
|
+
if (promptHook || !context.session)
|
|
13966
|
+
return;
|
|
13967
|
+
promptHook = await context.session.hook("prompt", async (event) => {
|
|
13968
|
+
if (skills.length === 0)
|
|
13969
|
+
return;
|
|
13970
|
+
const attached = new Set((event.prompt.skills ?? []).map((skill) => skill.id));
|
|
13971
|
+
const mentioned = mentionedSkillIds(event.prompt.text, skills).filter((id) => !attached.has(id));
|
|
13972
|
+
if (mentioned.length)
|
|
13973
|
+
event.prompt.skills = [...event.prompt.skills ?? [], ...mentioned.map((id) => ({ id }))];
|
|
13974
|
+
const registered = new Set(skills.map((skill) => skill.id));
|
|
13975
|
+
const used = new Set([...attached, ...mentioned].filter((id) => registered.has(id)));
|
|
13976
|
+
const entries = catalog.skills.filter((skill) => used.has(skillId(skill)));
|
|
13977
|
+
if (entries.length === 0)
|
|
13978
|
+
return;
|
|
13979
|
+
try {
|
|
13980
|
+
await readBodies(entries, true);
|
|
13981
|
+
for (const skill of entries) {
|
|
13982
|
+
const id = skillId(skill);
|
|
13983
|
+
warm.splice(0, warm.length, ...warm.filter((entry) => entry !== id), id);
|
|
13984
|
+
}
|
|
13985
|
+
warm.splice(0, Math.max(0, warm.length - MAX_WARM_SKILLS));
|
|
13986
|
+
await publish();
|
|
13987
|
+
} catch {}
|
|
13988
|
+
});
|
|
13989
|
+
};
|
|
13990
|
+
const refresh = async () => {
|
|
13991
|
+
if (closed || refreshing)
|
|
13992
|
+
return;
|
|
13993
|
+
refreshing = true;
|
|
13994
|
+
try {
|
|
13995
|
+
const value = await call("openwork_skills", {});
|
|
13996
|
+
if (value === null)
|
|
13997
|
+
return;
|
|
13998
|
+
catalog = catalogSchema.parse(value);
|
|
13999
|
+
await readBodies(warmEntries().filter((skill) => skill.digest && !contents.has(skill.digest)), false);
|
|
14000
|
+
await ensurePromptHook();
|
|
14001
|
+
await publish();
|
|
13971
14002
|
} catch {} finally {
|
|
13972
14003
|
refreshing = false;
|
|
13973
14004
|
}
|
|
@@ -13981,6 +14012,7 @@ async function registerCloudSkillDiscovery(context) {
|
|
|
13981
14012
|
controller.abort();
|
|
13982
14013
|
skills = [];
|
|
13983
14014
|
contents.clear();
|
|
14015
|
+
await publishing.catch(() => {});
|
|
13984
14016
|
await promptHook?.dispose();
|
|
13985
14017
|
await registration?.dispose();
|
|
13986
14018
|
};
|
|
@@ -13873,12 +13873,14 @@ async function uiBridgeRequest(path, options = {}) {
|
|
|
13873
13873
|
|
|
13874
13874
|
// src/opencode-plugins/drive-file-tools.ts
|
|
13875
13875
|
var DRIVE_FILE_ACTIONS = ["drive_pull_file", "drive_file_status", "drive_push_file"];
|
|
13876
|
-
var connectionId = exports_external.string().regex(/^(google-workspace|emc_[A-Za-z0-9]+)$/).optional().describe("Selected Google Workspace connection; omit for the default. Unavailable selections never fall back to another account.");
|
|
13876
|
+
var connectionId = exports_external.string().regex(/^(google-workspace|microsoft-365|emc_[A-Za-z0-9]+)$/).optional().describe("Selected Google Workspace or Microsoft 365 connection; omit for the default. Unavailable selections never fall back to another account.");
|
|
13877
13877
|
var workspacePath = exports_external.string().min(1).max(4096);
|
|
13878
13878
|
var drivePullFileArgs = exports_external.object({
|
|
13879
|
-
fileId: exports_external.string().min(1).max(2048).describe("Drive file ID or link."),
|
|
13879
|
+
fileId: exports_external.string().min(1).max(2048).describe("Google Drive file ID or link, or a OneDrive, SharePoint or Teams file link or item id."),
|
|
13880
|
+
driveId: exports_external.string().min(1).max(512).optional().describe("Microsoft 365 only: the driveId that came with the item id from a search or folder listing."),
|
|
13881
|
+
provider: exports_external.enum(["google-workspace", "microsoft-365"]).optional().describe("Only needed for a Microsoft item id without driveId; links and driveId identify Microsoft 365 files."),
|
|
13880
13882
|
path: workspacePath.optional().describe("Workspace path to save to. Defaults to drive/<file name>."),
|
|
13881
|
-
format: exports_external.string().regex(/^[a-z]{2,5}$/).optional().describe("Docs, Sheets, Slides and Drawings
|
|
13883
|
+
format: exports_external.string().regex(/^[a-z]{2,5}$/).optional().describe("Docs, Sheets, Slides and Drawings: xlsx, csv (first tab only), docx, md, txt, pptx, pdf, png or svg. OneDrive and SharePoint: pdf converts an Office file."),
|
|
13882
13884
|
connectionId,
|
|
13883
13885
|
overwrite: exports_external.boolean().optional().describe("Replace a local copy even if it has edits not in Drive. Asks the person first.")
|
|
13884
13886
|
}).strict();
|
|
@@ -13887,7 +13889,7 @@ var driveFileStatusArgs = exports_external.object({
|
|
|
13887
13889
|
}).strict();
|
|
13888
13890
|
var drivePushFileArgs = exports_external.object({
|
|
13889
13891
|
path: workspacePath.describe("Workspace path of a pulled file."),
|
|
13890
|
-
mode: exports_external.enum(["replace", "copy"]).optional().describe("replace (default for stored files) updates the same
|
|
13892
|
+
mode: exports_external.enum(["replace", "copy"]).optional().describe("replace (default for stored files) updates the same file; copy saves a new file next to it."),
|
|
13891
13893
|
name: exports_external.string().min(1).max(255).optional().describe("Name for a copy.")
|
|
13892
13894
|
}).strict();
|
|
13893
13895
|
function isRecord2(value) {
|
|
@@ -13902,7 +13904,7 @@ async function askPermission(context, permission, patterns, metadata) {
|
|
|
13902
13904
|
function createDriveFileTools(call) {
|
|
13903
13905
|
return {
|
|
13904
13906
|
drive_pull_file: {
|
|
13905
|
-
description: "Save a Google Drive file into the workspace and return its path and revision, never its content. Use it to analyze or edit Office, PDF, CSV or any large file with shell tools. Docs, Sheets and Slides arrive as docx, xlsx and pptx unless format says otherwise.",
|
|
13907
|
+
description: "Save a Google Drive, OneDrive or SharePoint file into the workspace and return its path and revision, never its content. Use it to analyze or edit Office, PDF, CSV or any large file with shell tools. Docs, Sheets and Slides arrive as docx, xlsx and pptx unless format says otherwise.",
|
|
13906
13908
|
args: drivePullFileArgs.shape,
|
|
13907
13909
|
async execute(rawArgs, context) {
|
|
13908
13910
|
const args = drivePullFileArgs.parse(rawArgs ?? {});
|
|
@@ -13912,14 +13914,14 @@ function createDriveFileTools(call) {
|
|
|
13912
13914
|
}
|
|
13913
13915
|
},
|
|
13914
13916
|
drive_file_status: {
|
|
13915
|
-
description: "For files pulled with drive_pull_file, report whether the local copy has edits not saved
|
|
13917
|
+
description: "For files pulled with drive_pull_file, report whether the local copy has edits not saved back and whether the file changed in Drive, OneDrive or SharePoint since the pull.",
|
|
13916
13918
|
args: driveFileStatusArgs.shape,
|
|
13917
13919
|
async execute(rawArgs, context) {
|
|
13918
13920
|
return JSON.stringify(await call("drive_file_status", driveFileStatusArgs.parse(rawArgs ?? {}), context), null, 2);
|
|
13919
13921
|
}
|
|
13920
13922
|
},
|
|
13921
13923
|
drive_push_file: {
|
|
13922
|
-
description: "Save a pulled file back to
|
|
13924
|
+
description: "Save a pulled file back to where it came from when the person asks. Asks the person first. Stored files, including Office files in OneDrive and SharePoint, are replaced in place as a new version; Google Docs, Sheets and Slides and PDF conversions are saved as a new file next to the original. Returns drive_conflict, and changes nothing, when someone changed the file since the pull: pull it again and reapply the edits, or save a copy.",
|
|
13923
13925
|
args: drivePushFileArgs.shape,
|
|
13924
13926
|
async execute(rawArgs, context) {
|
|
13925
13927
|
const args = drivePushFileArgs.parse(rawArgs ?? {});
|