@holokai/harbor-sdk 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +72 -0
- package/dist/chunks/chunk-G4KJ2OCU.js +319 -0
- package/dist/contract.d.ts +613 -0
- package/dist/contract.js +51 -0
- package/dist/holokai-logo.d.ts +5 -0
- package/dist/index.d.ts +104 -0
- package/dist/index.js +212 -0
- package/dist/manifest.d.ts +26 -0
- package/dist/manifest.js +1 -0
- package/dist/package.d.ts +79 -0
- package/dist/package.js +131 -0
- package/dist/page-reader.d.ts +53 -0
- package/dist/runtime.d.ts +220 -0
- package/dist/runtime.js +56 -0
- package/dist/sign-in-page.d.ts +5 -0
- package/dist/sign-in-page.js +96 -0
- package/package.json +58 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import type { App } from '@modelcontextprotocol/ext-apps';
|
|
2
|
+
import { type BarInput, type BarSetParams, type BarSetResult, type BrowserOpenParams, type BrowserOpenResult, type CapabilitiesListResult, type FilesAddParams, type FilesAddResult, type FilesLinkParams, type FilesOpenParams, type FilesOpenResult, type HarborEventMap, type HarborEventName, type HarborViewNames, type HarborViewState, type HarborMethod, type ViewsOpenParams, type ViewsOpenResult, type ViewStatusParams, type ViewStatusResult, type WorkflowsGetResult, type WorkflowsListParams, type WorkflowsListResult, type WorkflowsSubmitParams, type WorkflowsSubmitResult } from './contract';
|
|
3
|
+
export type * from './contract';
|
|
4
|
+
export type * from './page-reader';
|
|
5
|
+
export { BAR_LIMITS, HARBOR_EXTENSION_ID, HARBOR_EXTENSION_VERSION, HARBOR_METHODS, HARBOR_PAGE_READER, HARBOR_VIEW_NAME_LIMITS, HARBOR_VIEW_NAMES_ID, HARBOR_VIEW_STATE_LIMITS, MCP_APPS_PROTOCOL_VERSION, VIEW_STATUSES, } from './contract';
|
|
6
|
+
/** What a listener hears when a delivery was lost: re-read a snapshot. */
|
|
7
|
+
export interface HarborEventGap {
|
|
8
|
+
subscriptionId: string;
|
|
9
|
+
expected: number;
|
|
10
|
+
received: number;
|
|
11
|
+
}
|
|
12
|
+
export interface HarborEventOptions {
|
|
13
|
+
/** A `seq` was skipped — an event was lost. Events are invalidation signals; there is no replay. */
|
|
14
|
+
onGap?: (gap: HarborEventGap) => void;
|
|
15
|
+
}
|
|
16
|
+
export interface HarborConnection {
|
|
17
|
+
/** The extension version harbor advertised. Methods are additive within a major. */
|
|
18
|
+
readonly version: string;
|
|
19
|
+
/** What THIS view is granted — feature-detect per method and event. */
|
|
20
|
+
readonly granted: {
|
|
21
|
+
readonly methods: readonly HarborMethod[];
|
|
22
|
+
readonly events: readonly HarborEventName[];
|
|
23
|
+
};
|
|
24
|
+
readonly workflows: {
|
|
25
|
+
list(params?: WorkflowsListParams): Promise<WorkflowsListResult>;
|
|
26
|
+
/** Submit a DAG to BigBrain and start it. Its events arrive on `workflow.task` / `workflow.changed`. */
|
|
27
|
+
submit(params: WorkflowsSubmitParams): Promise<WorkflowsSubmitResult>;
|
|
28
|
+
/** Read a workflow this app submitted — the tasks, their states and outputs. */
|
|
29
|
+
get(workflowId: string): Promise<WorkflowsGetResult>;
|
|
30
|
+
};
|
|
31
|
+
readonly capabilities: {
|
|
32
|
+
/** What a DAG may name: harbor's built-ins, Holo apps, this app's own tools, other connectors'. */
|
|
33
|
+
list(): Promise<CapabilitiesListResult>;
|
|
34
|
+
};
|
|
35
|
+
readonly files: {
|
|
36
|
+
/** Ask harbor to open a file: it calls `tool` on this app's server for the bytes and opens a read-only copy. */
|
|
37
|
+
open(params: FilesOpenParams): Promise<FilesOpenResult>;
|
|
38
|
+
/** Ask harbor to add a file to the workspace this page was opened to pick for, through `tool` as `open` does. */
|
|
39
|
+
add(params: FilesAddParams): Promise<FilesAddResult>;
|
|
40
|
+
/** Ask harbor to link a folder or a file to the workspace this page was opened to pick for; its chats read it in place. */
|
|
41
|
+
link(params: FilesLinkParams): Promise<FilesAddResult>;
|
|
42
|
+
};
|
|
43
|
+
readonly browser: {
|
|
44
|
+
/** Ask harbor to open an https URL in the person's own browser. Anything else is refused. */
|
|
45
|
+
open(params: BrowserOpenParams): Promise<BrowserOpenResult>;
|
|
46
|
+
};
|
|
47
|
+
readonly bar: {
|
|
48
|
+
/** Hand harbor's window bar this page's search field and account menu. Replaces the whole bar. */
|
|
49
|
+
set(params: BarSetParams): Promise<BarSetResult>;
|
|
50
|
+
/** Hear what the person types or chooses in the bar, and the trail step they press. Returns the unsubscribe. */
|
|
51
|
+
on(handler: (input: BarInput) => void): () => void;
|
|
52
|
+
};
|
|
53
|
+
readonly views: {
|
|
54
|
+
/**
|
|
55
|
+
* Open this same page again in a new window beside this one, booted with
|
|
56
|
+
* `view` as its saved view and called `title` until it names what it shows.
|
|
57
|
+
*/
|
|
58
|
+
open(params: ViewsOpenParams): Promise<ViewsOpenResult>;
|
|
59
|
+
/**
|
|
60
|
+
* Set what this page's tray button shows about the work behind it. The
|
|
61
|
+
* button is the extension's, so every view of it shares the status.
|
|
62
|
+
*/
|
|
63
|
+
status(params: ViewStatusParams): Promise<ViewStatusResult>;
|
|
64
|
+
};
|
|
65
|
+
readonly events: {
|
|
66
|
+
/** Subscribe to one event. Returns the unsubscribe. */
|
|
67
|
+
on<K extends HarborEventName>(name: K, listener: (payload: HarborEventMap[K]) => void, options?: HarborEventOptions): () => void;
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* One report of what a view is showing: text for the model, the structured
|
|
72
|
+
* view for the host, the names of what is open, or any of them. ALWAYS the
|
|
73
|
+
* whole picture — the host slot is last-write-wins, so a field left out is a
|
|
74
|
+
* field erased.
|
|
75
|
+
*/
|
|
76
|
+
export interface HarborViewSnapshot {
|
|
77
|
+
/** A sentence for the model: what this page is showing, in words. */
|
|
78
|
+
text?: string;
|
|
79
|
+
/** The structured view — harbor's to remember and hand back on the next boot. */
|
|
80
|
+
state?: HarborViewState;
|
|
81
|
+
/** The names of what is open and where it sits, for the host's own sentence. */
|
|
82
|
+
names?: HarborViewNames;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* The view harbor saved for this page, handed back with the handshake.
|
|
86
|
+
* `null` under any host that saved nothing, and under every host but harbor.
|
|
87
|
+
* Call AFTER `app.connect()`.
|
|
88
|
+
*/
|
|
89
|
+
export declare function savedView(app: App): HarborViewState | null;
|
|
90
|
+
/**
|
|
91
|
+
* Tell the host what this page is showing. Works under ANY MCP Apps host, and
|
|
92
|
+
* sends each half only where the host advertised that modality: a host with no
|
|
93
|
+
* `structuredContent` marker gets the text block and NEITHER harbor key, so a
|
|
94
|
+
* pane width and a subject line never enter a foreign model's context.
|
|
95
|
+
*
|
|
96
|
+
* A snapshot with nothing the host takes sends nothing at all.
|
|
97
|
+
*/
|
|
98
|
+
export declare function reportView(app: App, snapshot: HarborViewSnapshot): Promise<void>;
|
|
99
|
+
/**
|
|
100
|
+
* Connect to harbor's extension, or learn that this host is not harbor.
|
|
101
|
+
* Call AFTER `app.connect()` — the host's capabilities arrive with the
|
|
102
|
+
* handshake. Idempotent per `App`: the same connection comes back.
|
|
103
|
+
*/
|
|
104
|
+
export declare function connectHarbor(app: App): HarborConnection | null;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import {createRequire as __harborCreateRequire} from 'node:module'; const require = __harborCreateRequire(import.meta.url);
|
|
2
|
+
import {
|
|
3
|
+
BAR_LIMITS,
|
|
4
|
+
HARBOR_BAR_NOTIFICATION,
|
|
5
|
+
HARBOR_EVENT_NOTIFICATION,
|
|
6
|
+
HARBOR_EXTENSION_ID,
|
|
7
|
+
HARBOR_EXTENSION_VERSION,
|
|
8
|
+
HARBOR_METHODS,
|
|
9
|
+
HARBOR_PAGE_READER,
|
|
10
|
+
HARBOR_VIEW_NAMES_ID,
|
|
11
|
+
HARBOR_VIEW_NAME_LIMITS,
|
|
12
|
+
HARBOR_VIEW_STATE_LIMITS,
|
|
13
|
+
MCP_APPS_PROTOCOL_VERSION,
|
|
14
|
+
VIEW_STATUSES
|
|
15
|
+
} from "./chunks/chunk-G4KJ2OCU.js";
|
|
16
|
+
|
|
17
|
+
// src/index.ts
|
|
18
|
+
var passThrough = () => ({
|
|
19
|
+
"~standard": { version: 1, vendor: "harbor-sdk", validate: (value) => ({ value }) }
|
|
20
|
+
});
|
|
21
|
+
function savedView(app) {
|
|
22
|
+
const carried = app.getHostContext()?.[HARBOR_EXTENSION_ID];
|
|
23
|
+
if (typeof carried !== "object" || carried === null) return null;
|
|
24
|
+
const state = carried.viewState;
|
|
25
|
+
return typeof state === "object" && state !== null ? state : null;
|
|
26
|
+
}
|
|
27
|
+
async function reportView(app, snapshot) {
|
|
28
|
+
const modalities = app.getHostCapabilities()?.updateModelContext;
|
|
29
|
+
const structured = modalities?.structuredContent !== void 0;
|
|
30
|
+
const text = modalities?.text !== void 0 && snapshot.text !== void 0 ? snapshot.text : void 0;
|
|
31
|
+
const state = structured && snapshot.state !== void 0 ? snapshot.state : void 0;
|
|
32
|
+
const names = structured && snapshot.names !== void 0 ? snapshot.names : void 0;
|
|
33
|
+
if (text === void 0 && state === void 0 && names === void 0) return;
|
|
34
|
+
await app.updateModelContext({
|
|
35
|
+
...text === void 0 ? {} : { content: [{ type: "text", text }] },
|
|
36
|
+
...state === void 0 && names === void 0 ? {} : {
|
|
37
|
+
structuredContent: {
|
|
38
|
+
...state === void 0 ? {} : { [HARBOR_EXTENSION_ID]: state },
|
|
39
|
+
...names === void 0 ? {} : { [HARBOR_VIEW_NAMES_ID]: names }
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
var connections = /* @__PURE__ */ new WeakMap();
|
|
45
|
+
function connectHarbor(app) {
|
|
46
|
+
if (connections.has(app)) return connections.get(app) ?? null;
|
|
47
|
+
const extension = readExtension(app.getHostCapabilities()?.experimental?.[HARBOR_EXTENSION_ID]);
|
|
48
|
+
const connection = extension ? new Connection(app, extension) : null;
|
|
49
|
+
connections.set(app, connection);
|
|
50
|
+
return connection;
|
|
51
|
+
}
|
|
52
|
+
function readExtension(value) {
|
|
53
|
+
if (typeof value !== "object" || value === null) return null;
|
|
54
|
+
const record = value;
|
|
55
|
+
if (typeof record["version"] !== "string") return null;
|
|
56
|
+
const methods = Array.isArray(record["methods"]) ? record["methods"].filter((m) => typeof m === "string") : [];
|
|
57
|
+
const events = Array.isArray(record["events"]) ? record["events"].filter((e) => typeof e === "string") : [];
|
|
58
|
+
return { version: record["version"], methods, events };
|
|
59
|
+
}
|
|
60
|
+
var Connection = class {
|
|
61
|
+
version;
|
|
62
|
+
granted;
|
|
63
|
+
workflows;
|
|
64
|
+
capabilities;
|
|
65
|
+
files;
|
|
66
|
+
browser;
|
|
67
|
+
bar;
|
|
68
|
+
views;
|
|
69
|
+
events;
|
|
70
|
+
#app;
|
|
71
|
+
#listeners = /* @__PURE__ */ new Set();
|
|
72
|
+
#barHandlers = /* @__PURE__ */ new Set();
|
|
73
|
+
#subscription = null;
|
|
74
|
+
/**
|
|
75
|
+
* A notification MAY arrive before the subscribe result that names its
|
|
76
|
+
* subscriptionId — main pushes on one channel and answers on another,
|
|
77
|
+
* and nothing orders the two. So the handler is registered ONCE, before
|
|
78
|
+
* any subscribe is sent, and holds what it cannot yet place until the
|
|
79
|
+
* result lands.
|
|
80
|
+
*/
|
|
81
|
+
#held = [];
|
|
82
|
+
#resync = Promise.resolve();
|
|
83
|
+
constructor(app, extension) {
|
|
84
|
+
this.#app = app;
|
|
85
|
+
this.version = extension.version;
|
|
86
|
+
this.granted = { methods: [...extension.methods], events: [...extension.events] };
|
|
87
|
+
this.workflows = {
|
|
88
|
+
list: (params = {}) => this.#request(HARBOR_METHODS.workflowsList, { ...params }),
|
|
89
|
+
submit: (params) => this.#request(HARBOR_METHODS.workflowsSubmit, { ...params }),
|
|
90
|
+
get: (workflowId) => this.#request(HARBOR_METHODS.workflowsGet, { workflowId })
|
|
91
|
+
};
|
|
92
|
+
this.capabilities = {
|
|
93
|
+
list: () => this.#request(HARBOR_METHODS.capabilitiesList, {})
|
|
94
|
+
};
|
|
95
|
+
this.files = {
|
|
96
|
+
open: (params) => this.#request(HARBOR_METHODS.filesOpen, { ...params }),
|
|
97
|
+
add: (params) => this.#request(HARBOR_METHODS.filesAdd, { ...params }),
|
|
98
|
+
link: (params) => this.#request(HARBOR_METHODS.filesLink, { ...params })
|
|
99
|
+
};
|
|
100
|
+
this.browser = {
|
|
101
|
+
open: (params) => this.#request(HARBOR_METHODS.browserOpen, { ...params })
|
|
102
|
+
};
|
|
103
|
+
this.bar = {
|
|
104
|
+
set: (params) => this.#request(HARBOR_METHODS.barSet, { ...params }),
|
|
105
|
+
on: (handler) => {
|
|
106
|
+
this.#barHandlers.add(handler);
|
|
107
|
+
return () => {
|
|
108
|
+
this.#barHandlers.delete(handler);
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
this.views = {
|
|
113
|
+
open: (params) => this.#request(HARBOR_METHODS.viewsOpen, { ...params }),
|
|
114
|
+
status: (params) => this.#request(HARBOR_METHODS.viewStatus, { ...params })
|
|
115
|
+
};
|
|
116
|
+
this.events = { on: (name, listener, options) => this.#on(name, listener, options) };
|
|
117
|
+
app.setNotificationHandler(HARBOR_BAR_NOTIFICATION, { params: passThrough() }, (input) => {
|
|
118
|
+
for (const handler of this.#barHandlers) handler(input);
|
|
119
|
+
});
|
|
120
|
+
app.setNotificationHandler(
|
|
121
|
+
HARBOR_EVENT_NOTIFICATION,
|
|
122
|
+
{ params: passThrough() },
|
|
123
|
+
(params) => this.#onEvent(params)
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
#request(method, params) {
|
|
127
|
+
return this.#app.request({ method, params }, passThrough());
|
|
128
|
+
}
|
|
129
|
+
#on(event, fn, options) {
|
|
130
|
+
const listener = {
|
|
131
|
+
event,
|
|
132
|
+
fn,
|
|
133
|
+
...options?.onGap ? { onGap: options.onGap } : {}
|
|
134
|
+
};
|
|
135
|
+
this.#listeners.add(listener);
|
|
136
|
+
this.#queueResync();
|
|
137
|
+
return () => {
|
|
138
|
+
this.#listeners.delete(listener);
|
|
139
|
+
this.#queueResync();
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
/** Serialized: one subscribe/unsubscribe in flight at a time, in call order. */
|
|
143
|
+
#queueResync() {
|
|
144
|
+
this.#resync = this.#resync.then(() => this.#resyncSubscription()).catch(() => void 0);
|
|
145
|
+
}
|
|
146
|
+
async #resyncSubscription() {
|
|
147
|
+
const wanted = /* @__PURE__ */ new Set();
|
|
148
|
+
for (const listener of this.#listeners) wanted.add(listener.event);
|
|
149
|
+
const current = this.#subscription;
|
|
150
|
+
const same = current !== null && current.events.size === wanted.size && [...wanted].every((e) => current.events.has(e));
|
|
151
|
+
if (same) return;
|
|
152
|
+
if (wanted.size === 0) {
|
|
153
|
+
this.#subscription = null;
|
|
154
|
+
if (current) await this.#unsubscribe(current.id);
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
const result = await this.#request(HARBOR_METHODS.eventsSubscribe, {
|
|
158
|
+
events: [...wanted]
|
|
159
|
+
});
|
|
160
|
+
this.#subscription = { id: result.subscriptionId, events: new Set(result.events), seq: 0 };
|
|
161
|
+
this.#flushHeld();
|
|
162
|
+
if (current) await this.#unsubscribe(current.id);
|
|
163
|
+
}
|
|
164
|
+
async #unsubscribe(subscriptionId) {
|
|
165
|
+
try {
|
|
166
|
+
await this.#request(HARBOR_METHODS.eventsUnsubscribe, { subscriptionId });
|
|
167
|
+
} catch {
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
#onEvent(params) {
|
|
171
|
+
const subscription = this.#subscription;
|
|
172
|
+
if (subscription === null || params.subscriptionId !== subscription.id) {
|
|
173
|
+
this.#held.push(params);
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
this.#dispatch(subscription, params);
|
|
177
|
+
}
|
|
178
|
+
#flushHeld() {
|
|
179
|
+
const subscription = this.#subscription;
|
|
180
|
+
const held = this.#held;
|
|
181
|
+
this.#held = [];
|
|
182
|
+
if (subscription === null) return;
|
|
183
|
+
for (const params of held.filter((p) => p.subscriptionId === subscription.id).sort((a, b) => a.seq - b.seq)) {
|
|
184
|
+
this.#dispatch(subscription, params);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
#dispatch(subscription, params) {
|
|
188
|
+
const expected = subscription.seq + 1;
|
|
189
|
+
if (params.seq < expected) return;
|
|
190
|
+
const gap = params.seq > expected ? { subscriptionId: subscription.id, expected, received: params.seq } : null;
|
|
191
|
+
subscription.seq = params.seq;
|
|
192
|
+
for (const listener of this.#listeners) {
|
|
193
|
+
if (gap && listener.onGap) listener.onGap(gap);
|
|
194
|
+
if (listener.event === params.event) listener.fn(params.payload);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
};
|
|
198
|
+
export {
|
|
199
|
+
BAR_LIMITS,
|
|
200
|
+
HARBOR_EXTENSION_ID,
|
|
201
|
+
HARBOR_EXTENSION_VERSION,
|
|
202
|
+
HARBOR_METHODS,
|
|
203
|
+
HARBOR_PAGE_READER,
|
|
204
|
+
HARBOR_VIEW_NAMES_ID,
|
|
205
|
+
HARBOR_VIEW_NAME_LIMITS,
|
|
206
|
+
HARBOR_VIEW_STATE_LIMITS,
|
|
207
|
+
MCP_APPS_PROTOCOL_VERSION,
|
|
208
|
+
VIEW_STATUSES,
|
|
209
|
+
connectHarbor,
|
|
210
|
+
reportView,
|
|
211
|
+
savedView
|
|
212
|
+
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/** The manifest type a first-party extension exports and harbor reads. */
|
|
2
|
+
import type { DeclaredGrant } from './contract';
|
|
3
|
+
/** An image for an extension, the MCP `Icon` shape; `src` is a data: URL. */
|
|
4
|
+
export interface ExtensionIcon {
|
|
5
|
+
src: string;
|
|
6
|
+
mimeType?: string;
|
|
7
|
+
sizes?: string[];
|
|
8
|
+
}
|
|
9
|
+
/** Who an extension is: what harbor shows for it and what it needs to run. */
|
|
10
|
+
export interface ExtensionManifest {
|
|
11
|
+
/** The path segment it is served at, lowercase letters, digits and dashes, starting with a letter. */
|
|
12
|
+
name: string;
|
|
13
|
+
title: string;
|
|
14
|
+
description: string;
|
|
15
|
+
version: string;
|
|
16
|
+
/**
|
|
17
|
+
* The name of the sign-in provider it signs in through, such as `microsoft`. Extensions that share one
|
|
18
|
+
* run in one process, a connection, so they share one sign-in.
|
|
19
|
+
*/
|
|
20
|
+
provider: string;
|
|
21
|
+
icon?: ExtensionIcon;
|
|
22
|
+
/** The names of the settings it reads from its environment, such as `MICROSOFT_CLIENT_ID`. */
|
|
23
|
+
settings: readonly string[];
|
|
24
|
+
/** The grants harbor gives only to an extension that names them, such as `files.add`. */
|
|
25
|
+
grants?: readonly DeclaredGrant[];
|
|
26
|
+
}
|
package/dist/manifest.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import {createRequire as __harborCreateRequire} from 'node:module'; const require = __harborCreateRequire(import.meta.url);
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The package contract: what makes an npm package an extension Harbor can run, and the installed catalog a tree carries.
|
|
3
|
+
* It imports nothing at run time, so Harbor's main process, the extension host and the build scripts all read it.
|
|
4
|
+
*/
|
|
5
|
+
import type { ExtensionManifest } from './manifest';
|
|
6
|
+
/** The extension-to-host API version. A host runs only extensions that declare it. */
|
|
7
|
+
export declare const EXTENSION_API_VERSION = 1;
|
|
8
|
+
/** Packages a tree holds exactly once: host-provided contracts and shared sign-in state. */
|
|
9
|
+
export declare const SINGLETON_PACKAGES: readonly ["@holokai/harbor-sdk", "@holokai/harbor-kit", "@holokai/harbor-google", "@holokai/harbor-microsoft", "@holokai/harbor-atlassian", "@holokai/harbor-relay"];
|
|
10
|
+
/** The `harbor` field of a published package's package.json: which kind of package it is. */
|
|
11
|
+
export type HarborPackageField = {
|
|
12
|
+
type: 'extension';
|
|
13
|
+
apiVersion: number;
|
|
14
|
+
} | {
|
|
15
|
+
type: 'library';
|
|
16
|
+
};
|
|
17
|
+
/** The package.json fields Harbor reads. */
|
|
18
|
+
export interface ExtensionPackageMetadata {
|
|
19
|
+
name: string;
|
|
20
|
+
version: string;
|
|
21
|
+
harbor: HarborPackageField;
|
|
22
|
+
}
|
|
23
|
+
/** One extension in an installed tree. Paths are relative to the folder holding catalog.json. */
|
|
24
|
+
export interface InstalledCatalogEntry {
|
|
25
|
+
/** The manifest's name, which is also the extension's folder name. */
|
|
26
|
+
name: string;
|
|
27
|
+
package: string;
|
|
28
|
+
version: string;
|
|
29
|
+
apiVersion: number;
|
|
30
|
+
manifest: ExtensionManifest;
|
|
31
|
+
/** `node_modules/<package>/dist/main.js` */
|
|
32
|
+
entry: string;
|
|
33
|
+
/** `pages/<name>/app.html` */
|
|
34
|
+
page: string;
|
|
35
|
+
}
|
|
36
|
+
/** `tree/catalog.json`: what one installed tree holds. */
|
|
37
|
+
export interface InstalledCatalog {
|
|
38
|
+
apiVersion: number;
|
|
39
|
+
/** The SDK version installed in this tree. */
|
|
40
|
+
sdkVersion: string;
|
|
41
|
+
/** The release set the tree was made from, or `local` for a tree packed from the workspace. */
|
|
42
|
+
releaseSet: string;
|
|
43
|
+
extensions: InstalledCatalogEntry[];
|
|
44
|
+
}
|
|
45
|
+
/** Why one entry of a catalog does not load. */
|
|
46
|
+
export interface CatalogSkip {
|
|
47
|
+
name: string;
|
|
48
|
+
package: string;
|
|
49
|
+
reason: 'api_version' | 'manifest' | 'shape';
|
|
50
|
+
}
|
|
51
|
+
/** The catalog file's name inside a tree. */
|
|
52
|
+
export declare const CATALOG_FILE = "catalog.json";
|
|
53
|
+
/** A manifest, or null when a field is missing or of the wrong type. Only ExtensionManifest's fields survive. */
|
|
54
|
+
export declare function parseExtensionManifest(value: unknown): ExtensionManifest | null;
|
|
55
|
+
/**
|
|
56
|
+
* The entries a host may run, and the ones it must skip. Null when the value is not an installed catalog at all.
|
|
57
|
+
* A repeated name, or an entry or page path that leaves the catalog's folder, is skipped as `shape`.
|
|
58
|
+
*/
|
|
59
|
+
export declare function parseInstalledCatalog(value: unknown): {
|
|
60
|
+
catalog: InstalledCatalog;
|
|
61
|
+
skipped: CatalogSkip[];
|
|
62
|
+
} | null;
|
|
63
|
+
/** The installed catalog's entry path for a package, relative to the tree. */
|
|
64
|
+
export declare const entryPathOf: (packageName: string) => string;
|
|
65
|
+
/** The installed catalog's page path for an extension, relative to the tree. */
|
|
66
|
+
export declare const pagePathOf: (name: string) => string;
|
|
67
|
+
/**
|
|
68
|
+
* The one writer of catalog.json: from each installed extension package's package.json and dist/manifest.json, sorted
|
|
69
|
+
* by name. Throws on a package that is not an extension or a manifest that does not parse, so a broken tree is never
|
|
70
|
+
* written.
|
|
71
|
+
*/
|
|
72
|
+
export declare function catalogFor(input: {
|
|
73
|
+
releaseSet: string;
|
|
74
|
+
sdkVersion: string;
|
|
75
|
+
packages: readonly {
|
|
76
|
+
metadata: ExtensionPackageMetadata;
|
|
77
|
+
manifest: unknown;
|
|
78
|
+
}[];
|
|
79
|
+
}): InstalledCatalog;
|
package/dist/package.js
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import {createRequire as __harborCreateRequire} from 'node:module'; const require = __harborCreateRequire(import.meta.url);
|
|
2
|
+
|
|
3
|
+
// src/package.ts
|
|
4
|
+
var EXTENSION_API_VERSION = 1;
|
|
5
|
+
var SINGLETON_PACKAGES = [
|
|
6
|
+
"@holokai/harbor-sdk",
|
|
7
|
+
"@holokai/harbor-kit",
|
|
8
|
+
"@holokai/harbor-google",
|
|
9
|
+
"@holokai/harbor-microsoft",
|
|
10
|
+
"@holokai/harbor-atlassian",
|
|
11
|
+
"@holokai/harbor-relay"
|
|
12
|
+
];
|
|
13
|
+
var CATALOG_FILE = "catalog.json";
|
|
14
|
+
var NAME = /^[a-z][a-z0-9-]*$/;
|
|
15
|
+
var VERSION_CORE = /^\d+\.\d+\.\d+$/;
|
|
16
|
+
var PRERELEASE = /^[0-9A-Za-z.-]+$/;
|
|
17
|
+
function isVersion(value) {
|
|
18
|
+
const dash = value.indexOf("-");
|
|
19
|
+
if (dash === -1) return VERSION_CORE.test(value);
|
|
20
|
+
return VERSION_CORE.test(value.slice(0, dash)) && PRERELEASE.test(value.slice(dash + 1));
|
|
21
|
+
}
|
|
22
|
+
var DECLARED_GRANTS = /* @__PURE__ */ new Set(["files.add"]);
|
|
23
|
+
var isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
24
|
+
var isString = (value) => typeof value === "string";
|
|
25
|
+
var isStringArray = (value) => Array.isArray(value) && value.every(isString);
|
|
26
|
+
function parseIcon(value) {
|
|
27
|
+
if (!isRecord(value) || !isString(value.src) || !value.src.startsWith("data:")) return null;
|
|
28
|
+
if (value.mimeType !== void 0 && !isString(value.mimeType)) return null;
|
|
29
|
+
if (value.sizes !== void 0 && !isStringArray(value.sizes)) return null;
|
|
30
|
+
return {
|
|
31
|
+
src: value.src,
|
|
32
|
+
...value.mimeType === void 0 ? {} : { mimeType: value.mimeType },
|
|
33
|
+
...value.sizes === void 0 ? {} : { sizes: [...value.sizes] }
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
function parseExtensionManifest(value) {
|
|
37
|
+
if (!isRecord(value)) return null;
|
|
38
|
+
const { name, title, description, version, provider, icon, settings, grants } = value;
|
|
39
|
+
if (!isString(name) || !NAME.test(name)) return null;
|
|
40
|
+
if (!isString(title) || !isString(description) || !isString(provider) || !NAME.test(provider)) return null;
|
|
41
|
+
if (!isString(version) || !isVersion(version)) return null;
|
|
42
|
+
if (!isStringArray(settings)) return null;
|
|
43
|
+
const parsedIcon = icon === void 0 ? void 0 : parseIcon(icon);
|
|
44
|
+
if (parsedIcon === null) return null;
|
|
45
|
+
if (grants !== void 0 && !(isStringArray(grants) && grants.every((grant) => DECLARED_GRANTS.has(grant)))) {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
return {
|
|
49
|
+
name,
|
|
50
|
+
title,
|
|
51
|
+
description,
|
|
52
|
+
version,
|
|
53
|
+
provider,
|
|
54
|
+
...parsedIcon === void 0 ? {} : { icon: parsedIcon },
|
|
55
|
+
settings: [...settings],
|
|
56
|
+
...grants === void 0 ? {} : { grants: [...grants] }
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
function staysInside(relative) {
|
|
60
|
+
return relative.length > 0 && !relative.startsWith("/") && !relative.includes("\\") && !/^[A-Za-z]:/.test(relative) && relative.split("/").every((segment) => segment !== ".." && segment !== "");
|
|
61
|
+
}
|
|
62
|
+
function parseInstalledCatalog(value) {
|
|
63
|
+
if (!isRecord(value) || !Array.isArray(value.extensions)) return null;
|
|
64
|
+
const { apiVersion, sdkVersion, releaseSet } = value;
|
|
65
|
+
if (typeof apiVersion !== "number" || !isString(sdkVersion) || !isString(releaseSet)) return null;
|
|
66
|
+
const extensions = [];
|
|
67
|
+
const skipped = [];
|
|
68
|
+
const seen = /* @__PURE__ */ new Set();
|
|
69
|
+
for (const raw of value.extensions) {
|
|
70
|
+
const name = isRecord(raw) && isString(raw.name) ? raw.name : "";
|
|
71
|
+
const packageName = isRecord(raw) && isString(raw.package) ? raw.package : "";
|
|
72
|
+
const skip = (reason) => skipped.push({ name, package: packageName, reason });
|
|
73
|
+
if (!isRecord(raw) || !NAME.test(name) || packageName === "" || seen.has(name) || !isString(raw.version) || !isString(raw.entry) || !isString(raw.page) || !staysInside(raw.entry) || !staysInside(raw.page)) {
|
|
74
|
+
skip("shape");
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
seen.add(name);
|
|
78
|
+
if (raw.apiVersion !== EXTENSION_API_VERSION) {
|
|
79
|
+
skip("api_version");
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
const manifest = parseExtensionManifest(raw.manifest);
|
|
83
|
+
if (manifest === null || manifest.name !== name || manifest.version !== raw.version) {
|
|
84
|
+
skip("manifest");
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
extensions.push({
|
|
88
|
+
name,
|
|
89
|
+
package: packageName,
|
|
90
|
+
version: raw.version,
|
|
91
|
+
apiVersion: raw.apiVersion,
|
|
92
|
+
manifest,
|
|
93
|
+
entry: raw.entry,
|
|
94
|
+
page: raw.page
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
return { catalog: { apiVersion, sdkVersion, releaseSet, extensions }, skipped };
|
|
98
|
+
}
|
|
99
|
+
var entryPathOf = (packageName) => `node_modules/${packageName}/dist/main.js`;
|
|
100
|
+
var pagePathOf = (name) => `pages/${name}/app.html`;
|
|
101
|
+
function catalogFor(input) {
|
|
102
|
+
const extensions = input.packages.map(({ metadata, manifest: raw }) => {
|
|
103
|
+
if (metadata.harbor.type !== "extension") throw new Error(`${metadata.name} is not an extension package`);
|
|
104
|
+
const manifest = parseExtensionManifest(raw);
|
|
105
|
+
if (manifest === null) throw new Error(`${metadata.name}: its dist/manifest.json is not a manifest`);
|
|
106
|
+
if (manifest.version !== metadata.version) {
|
|
107
|
+
throw new Error(`${metadata.name}: its manifest says ${manifest.version}, its package ${metadata.version}`);
|
|
108
|
+
}
|
|
109
|
+
return {
|
|
110
|
+
name: manifest.name,
|
|
111
|
+
package: metadata.name,
|
|
112
|
+
version: metadata.version,
|
|
113
|
+
apiVersion: metadata.harbor.apiVersion,
|
|
114
|
+
manifest,
|
|
115
|
+
entry: entryPathOf(metadata.name),
|
|
116
|
+
page: pagePathOf(manifest.name)
|
|
117
|
+
};
|
|
118
|
+
});
|
|
119
|
+
extensions.sort((a, b) => a.name.localeCompare(b.name));
|
|
120
|
+
return { apiVersion: EXTENSION_API_VERSION, sdkVersion: input.sdkVersion, releaseSet: input.releaseSet, extensions };
|
|
121
|
+
}
|
|
122
|
+
export {
|
|
123
|
+
CATALOG_FILE,
|
|
124
|
+
EXTENSION_API_VERSION,
|
|
125
|
+
SINGLETON_PACKAGES,
|
|
126
|
+
catalogFor,
|
|
127
|
+
entryPathOf,
|
|
128
|
+
pagePathOf,
|
|
129
|
+
parseExtensionManifest,
|
|
130
|
+
parseInstalledCatalog
|
|
131
|
+
};
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { HarborViewState } from './contract';
|
|
2
|
+
/**
|
|
3
|
+
* The page reader: one AI tool per extension, `read_page`, that takes a
|
|
4
|
+
* page's own reported location and answers what is there. Its answer reaches
|
|
5
|
+
* the model as that tool's result, never as pasted context. Harbor finds it
|
|
6
|
+
* by the mark `_meta['ai.holokai/harbor'].reads === 'page'`.
|
|
7
|
+
*/
|
|
8
|
+
/** A location on a page, as it reports its view: the place, item and filter ids and the search text, never names. */
|
|
9
|
+
export type PageAt = Pick<HarborViewState, 'place' | 'item' | 'filter' | 'query'>;
|
|
10
|
+
/**
|
|
11
|
+
* A place the page can go, with its name and how many items it holds. A
|
|
12
|
+
* hierarchy is a flat list in order, each place's `depth` below the top (0 when
|
|
13
|
+
* absent), because a recursive schema does not survive Harbor's tool catalog.
|
|
14
|
+
*/
|
|
15
|
+
export interface PagePlace {
|
|
16
|
+
at: PageAt;
|
|
17
|
+
name: string;
|
|
18
|
+
count?: number;
|
|
19
|
+
depth?: number;
|
|
20
|
+
}
|
|
21
|
+
/** One item worth opening from here, with where it is. */
|
|
22
|
+
export interface PageTarget {
|
|
23
|
+
at: PageAt;
|
|
24
|
+
name: string;
|
|
25
|
+
}
|
|
26
|
+
/** What the page reader answers: what is at the location, as text and as the extension's own data. */
|
|
27
|
+
export interface PageRead<D> {
|
|
28
|
+
/** The location read, with its name. */
|
|
29
|
+
shows: {
|
|
30
|
+
at: PageAt;
|
|
31
|
+
name: string;
|
|
32
|
+
};
|
|
33
|
+
/** What the model reads, sent as the tool's content; the other fields are its structured content. */
|
|
34
|
+
text: string;
|
|
35
|
+
data: D;
|
|
36
|
+
/** Where the page can go from here. */
|
|
37
|
+
places: PagePlace[];
|
|
38
|
+
/** Items worth opening from here. */
|
|
39
|
+
targets: PageTarget[];
|
|
40
|
+
}
|
|
41
|
+
/** Harbor's own entries in a tool's `_meta['ai.holokai/harbor']`. */
|
|
42
|
+
export interface HarborToolMeta {
|
|
43
|
+
/** The tool that opens the extension's page. */
|
|
44
|
+
entry?: true;
|
|
45
|
+
/** The tool that reads what the page shows. */
|
|
46
|
+
reads?: 'page';
|
|
47
|
+
/**
|
|
48
|
+
* On a page-only tool: the AI tool whose calls it describes for the Allow Once question. It takes
|
|
49
|
+
* that tool's input and answers what the call would do, without doing it. Harbor reads it only
|
|
50
|
+
* from a first-party extension.
|
|
51
|
+
*/
|
|
52
|
+
previews?: string;
|
|
53
|
+
}
|