@ham2k/extension-sdk 0.12.10 → 0.12.11
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/AGENTS.md +1 -1
- package/dist/hookCategories.js +1 -0
- package/dist/index.d.ts +57 -1
- package/dist/index.js +23 -0
- package/docs/distribution.md +1 -1
- package/docs/forms.md +4 -0
- package/docs/hooks.md +43 -1
- package/package.json +1 -1
- package/samples/README.md +1 -0
- package/samples/k2hrc-hello-world/build.mjs +6 -0
- package/samples/k2hrc-hello-world/manifest.json +22 -0
- package/samples/k2hrc-hello-world/src/index.ts +53 -0
package/AGENTS.md
CHANGED
|
@@ -81,7 +81,7 @@ will actually call.
|
|
|
81
81
|
|
|
82
82
|
| Doing this | Read |
|
|
83
83
|
| --- | --- |
|
|
84
|
-
| Anything, before the reference — a whole worked extension of that shape | `samples/`: `k2hrc-hamqth` (lookup + credentials), `k2hrc-llota` (award program), `k2hrc-cqww` (contest + scoring), `k2hrc-radio` (HTML panel), `k2hrc-panel-scenes` (experimental interactive panel scene) |
|
|
84
|
+
| Anything, before the reference — a whole worked extension of that shape | `samples/`: `k2hrc-hello-world` (the smallest panel), `k2hrc-hamqth` (lookup + credentials), `k2hrc-llota` (award program), `k2hrc-cqww` (contest + scoring), `k2hrc-radio` (HTML panel), `k2hrc-panel-scenes` (experimental interactive panel scene), `k2hrc-panafall` (panadapter and waterfall scene) |
|
|
85
85
|
| Any hook at all — what the category is, what it's handed, what it must return | `docs/hooks.md`, the section named for the category |
|
|
86
86
|
| A pane in a view: `getPanels`, `render`, panel content kinds | `docs/hooks.md` §`panel` |
|
|
87
87
|
| Callsign lookups, spot sources, exporters, reference types (POTA-style) | `docs/hooks.md` §`lookup`, §`spots`, §`export`, §`ref:<type>` |
|
package/dist/hookCategories.js
CHANGED
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// @ham2k/extension-sdk 0.12.
|
|
1
|
+
// @ham2k/extension-sdk 0.12.11
|
|
2
2
|
/** Experimental v1 panel scene contract. All coordinates are in the scene's viewBox. */
|
|
3
3
|
export interface PanelScene {
|
|
4
4
|
version: 1;
|
|
@@ -475,6 +475,7 @@ export declare const HOOK_CATEGORIES: readonly [
|
|
|
475
475
|
"bench",
|
|
476
476
|
"callNotes",
|
|
477
477
|
"chasing",
|
|
478
|
+
"cluster",
|
|
478
479
|
"command",
|
|
479
480
|
"dataFile",
|
|
480
481
|
"export",
|
|
@@ -733,6 +734,39 @@ export interface SpotsHook {
|
|
|
733
734
|
postSelfSpot?(args: PostSelfSpotRequest, ctx: HookContext): Promise<PostResult>;
|
|
734
735
|
postOtherSpot?(args: PostOtherSpotRequest, ctx: HookContext): Promise<PostResult>;
|
|
735
736
|
}
|
|
737
|
+
export interface ClusterLine {
|
|
738
|
+
seq: number;
|
|
739
|
+
at: number;
|
|
740
|
+
text: string;
|
|
741
|
+
}
|
|
742
|
+
export interface ClusterStatus {
|
|
743
|
+
state: "idle" | "connecting" | "connected" | "failed";
|
|
744
|
+
node: {
|
|
745
|
+
name: string;
|
|
746
|
+
host: string;
|
|
747
|
+
port: number;
|
|
748
|
+
};
|
|
749
|
+
call?: string;
|
|
750
|
+
error?: string;
|
|
751
|
+
message?: string;
|
|
752
|
+
lastSeq: number;
|
|
753
|
+
}
|
|
754
|
+
export interface ClusterReply {
|
|
755
|
+
ok: boolean;
|
|
756
|
+
lines: string[];
|
|
757
|
+
error?: string;
|
|
758
|
+
message?: string;
|
|
759
|
+
}
|
|
760
|
+
export interface ClusterPostSpotRequest {
|
|
761
|
+
freq: number;
|
|
762
|
+
call: string;
|
|
763
|
+
comment?: string;
|
|
764
|
+
}
|
|
765
|
+
export interface ClusterHook {
|
|
766
|
+
onLines(args: {
|
|
767
|
+
lines: ClusterLine[];
|
|
768
|
+
}, ctx: HookContext): Promise<void>;
|
|
769
|
+
}
|
|
736
770
|
export interface Ref {
|
|
737
771
|
type: string;
|
|
738
772
|
ref?: string;
|
|
@@ -1219,6 +1253,10 @@ export interface CommandAction {
|
|
|
1219
1253
|
comment?: string;
|
|
1220
1254
|
target?: "self" | "last";
|
|
1221
1255
|
};
|
|
1256
|
+
autoRespot?: {
|
|
1257
|
+
minutes?: number;
|
|
1258
|
+
off?: boolean;
|
|
1259
|
+
};
|
|
1222
1260
|
syncAll?: Record<string, never>;
|
|
1223
1261
|
purgeTests?: Record<string, never>;
|
|
1224
1262
|
updateSettings?: {
|
|
@@ -1480,6 +1518,9 @@ export interface ExtensionManifest {
|
|
|
1480
1518
|
requiresLocation?: boolean;
|
|
1481
1519
|
requiresRadioRead?: boolean;
|
|
1482
1520
|
requiresRadioWrite?: boolean;
|
|
1521
|
+
requiresClusterRead?: boolean;
|
|
1522
|
+
requiresClusterSpot?: boolean;
|
|
1523
|
+
requiresClusterCommand?: boolean;
|
|
1483
1524
|
experiments?: string[];
|
|
1484
1525
|
relevance?: ExtensionRelevance;
|
|
1485
1526
|
translations?: Record<string, {
|
|
@@ -1623,6 +1664,7 @@ export interface FormField {
|
|
|
1623
1664
|
templateContext?: "export" | "adif" | "text";
|
|
1624
1665
|
templateSample?: Record<string, JSONValue>;
|
|
1625
1666
|
uppercase?: boolean;
|
|
1667
|
+
storedText?: string;
|
|
1626
1668
|
suggestions?: {
|
|
1627
1669
|
code: string;
|
|
1628
1670
|
name?: string;
|
|
@@ -2501,6 +2543,20 @@ export declare const host: {
|
|
|
2501
2543
|
}): Promise<PanelRadioResult>;
|
|
2502
2544
|
/** Stops all CW, voice and PTT the app is keying, and a tune it started (extension API 9), as Escape does. Needs `requiresRadioWrite` and extension API 7. */
|
|
2503
2545
|
stopRadioTransmit(args: PanelRadioTarget): Promise<PanelRadioResult>;
|
|
2546
|
+
/** The app's DX cluster connection, as Settings shows it. Needs `requiresClusterRead` and extension API 10. */
|
|
2547
|
+
clusterStatus(): Promise<ClusterStatus>;
|
|
2548
|
+
/** The node's recent lines after `after`, oldest first, at most `limit` of the newest. Needs `requiresClusterRead` and extension API 10. */
|
|
2549
|
+
clusterLines(args?: {
|
|
2550
|
+
after?: number;
|
|
2551
|
+
limit?: number;
|
|
2552
|
+
}): Promise<ClusterLine[]>;
|
|
2553
|
+
/** Connects if need be and delivers every new line to this extension's `cluster` hook until `clusterUnsubscribe`. Needs `requiresClusterRead` and extension API 10. */
|
|
2554
|
+
clusterSubscribe(): Promise<ClusterStatus>;
|
|
2555
|
+
clusterUnsubscribe(): Promise<ClusterStatus>;
|
|
2556
|
+
/** Posts a spot, logged in as the operator. Needs `requiresClusterSpot` and extension API 10. */
|
|
2557
|
+
clusterPostSpot(args: ClusterPostSpotRequest): Promise<ClusterReply>;
|
|
2558
|
+
/** Sends any one command line, in the operator's name. Needs `requiresClusterCommand` and extension API 10. */
|
|
2559
|
+
clusterSend(command: string): Promise<ClusterReply>;
|
|
2504
2560
|
fetch(url: string, options?: FetchOptions): Promise<FetchResponse>;
|
|
2505
2561
|
webSocket(url: string, options?: WebSocketOptions): ExtensionWebSocket;
|
|
2506
2562
|
kvGet(key: string): Promise<JSONValue | null>;
|
package/dist/index.js
CHANGED
|
@@ -197,6 +197,29 @@ const host = {
|
|
|
197
197
|
async stopRadioTransmit(args) {
|
|
198
198
|
return await hostCallFn()("stopRadioTransmit", { ...args });
|
|
199
199
|
},
|
|
200
|
+
/** The app's DX cluster connection, as Settings shows it. Needs `requiresClusterRead` and extension API 10. */
|
|
201
|
+
async clusterStatus() {
|
|
202
|
+
return await hostCallFn()("clusterStatus", {});
|
|
203
|
+
},
|
|
204
|
+
/** The node's recent lines after `after`, oldest first, at most `limit` of the newest. Needs `requiresClusterRead` and extension API 10. */
|
|
205
|
+
async clusterLines(args = {}) {
|
|
206
|
+
return await hostCallFn()("clusterLines", { ...args });
|
|
207
|
+
},
|
|
208
|
+
/** Connects if need be and delivers every new line to this extension's `cluster` hook until `clusterUnsubscribe`. Needs `requiresClusterRead` and extension API 10. */
|
|
209
|
+
async clusterSubscribe() {
|
|
210
|
+
return await hostCallFn()("clusterSubscribe", {});
|
|
211
|
+
},
|
|
212
|
+
async clusterUnsubscribe() {
|
|
213
|
+
return await hostCallFn()("clusterUnsubscribe", {});
|
|
214
|
+
},
|
|
215
|
+
/** Posts a spot, logged in as the operator. Needs `requiresClusterSpot` and extension API 10. */
|
|
216
|
+
async clusterPostSpot(args) {
|
|
217
|
+
return await hostCallFn()("clusterPostSpot", { ...args });
|
|
218
|
+
},
|
|
219
|
+
/** Sends any one command line, in the operator's name. Needs `requiresClusterCommand` and extension API 10. */
|
|
220
|
+
async clusterSend(command) {
|
|
221
|
+
return await hostCallFn()("clusterSend", { command });
|
|
222
|
+
},
|
|
200
223
|
async fetch(url, options) {
|
|
201
224
|
return await hostCallFn()("fetch", { url, ...options });
|
|
202
225
|
},
|
package/docs/distribution.md
CHANGED
|
@@ -240,7 +240,7 @@ what you want, which is how you use a version the host does not carry, say so:
|
|
|
240
240
|
await buildExtension(build, { dir: import.meta.dirname, inline: ["liquidjs"] })
|
|
241
241
|
```
|
|
242
242
|
|
|
243
|
-
`extensions/samples/hello-world` is the shortest complete example. Declaring
|
|
243
|
+
`extensions/samples/k2hrc-hello-world` is the shortest complete example. Declaring
|
|
244
244
|
what it uses rather than carrying it took the packaged bundle from 185 KB to
|
|
245
245
|
**4 KB**.
|
|
246
246
|
|
package/docs/forms.md
CHANGED
|
@@ -38,6 +38,10 @@ export interface FormField {
|
|
|
38
38
|
value?: any; // initial state value if not provided in host call state
|
|
39
39
|
placeholder?: string;
|
|
40
40
|
uppercase?: boolean; // force typed input to uppercase (callsign fields always are)
|
|
41
|
+
// Only used when fieldType === 'secret' in a settings list: what the row
|
|
42
|
+
// says while a value is stored, in place of the fixed mask. A host older
|
|
43
|
+
// than this shows the mask.
|
|
44
|
+
storedText?: string;
|
|
41
45
|
// Only used when fieldType === 'text': the codes the field takes, offered
|
|
42
46
|
// in a list under it as the operator types (matched on code and name, as a
|
|
43
47
|
// `kind: 'options'` logging control matches) and named beside it — a value
|
package/docs/hooks.md
CHANGED
|
@@ -330,6 +330,48 @@ directly to the POTA API, no auth required). `sota`'s spot API requires an
|
|
|
330
330
|
authenticated account app-halo doesn't have yet, so `sota` stays
|
|
331
331
|
fetch-only for now.
|
|
332
332
|
|
|
333
|
+
## ✅ `cluster` — the DX cluster connection
|
|
334
|
+
|
|
335
|
+
```ts
|
|
336
|
+
interface ClusterHook {
|
|
337
|
+
onLines(args: { lines: ClusterLine[] }, ctx): Promise<void>
|
|
338
|
+
}
|
|
339
|
+
// ClusterLine: {seq, at, text}
|
|
340
|
+
// host.clusterSubscribe(): Promise<ClusterStatus> // then onLines, until
|
|
341
|
+
// host.clusterUnsubscribe(): Promise<ClusterStatus> // this
|
|
342
|
+
// host.clusterLines({after?, limit?}): Promise<ClusterLine[]>
|
|
343
|
+
// host.clusterStatus(): Promise<ClusterStatus>
|
|
344
|
+
// host.clusterPostSpot({freq, call, comment?}): Promise<ClusterReply>
|
|
345
|
+
// host.clusterSend(command): Promise<ClusterReply>
|
|
346
|
+
// ClusterReply: {ok, lines: string[], error?, message?}
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
The app holds one telnet connection to the DX cluster node the operator picked
|
|
350
|
+
in Settings, logged in as the operator. Extensions share it rather than open
|
|
351
|
+
their own, because a node logs a call in once
|
|
352
|
+
([docs/design/cluster.md](https://github.com/ham2k/halo/blob/main/docs/design/cluster.md)). Not on web, where every call
|
|
353
|
+
answers `error: 'unsupported'`. Extension API 10.
|
|
354
|
+
|
|
355
|
+
`clusterPostSpot` is how a `spots` hook posts to the cluster: the host builds
|
|
356
|
+
the `DX` command, sends at most 60 characters of comment, and spaces spots five
|
|
357
|
+
seconds apart. A reply with `ok: true` means the node took the spot without
|
|
358
|
+
refusing it. It does not prove the spot left that node: a DXSpider node can keep
|
|
359
|
+
a self-spot local and still echo it.
|
|
360
|
+
|
|
361
|
+
`onLines` is called only for an extension that called `clusterSubscribe()`,
|
|
362
|
+
with the lines since the last batch, at most once a second. Register the hook
|
|
363
|
+
under the extension's own key (the default): that is the key the host delivers
|
|
364
|
+
to. A subscription lasts until `clusterUnsubscribe()` or the runtime restarts,
|
|
365
|
+
and keeps the session open while it lasts. A reader that only polls
|
|
366
|
+
`clusterLines({ after })` holds nothing open.
|
|
367
|
+
|
|
368
|
+
Three manifest grants gate it, each including the ones before it:
|
|
369
|
+
`"requiresClusterRead": true` for status, lines and subscribing;
|
|
370
|
+
`"requiresClusterSpot": true` adds `clusterPostSpot`;
|
|
371
|
+
`"requiresClusterCommand": true` adds `clusterSend`, which sends any command in
|
|
372
|
+
the operator's name. Declare the least you need: the consent screen names it.
|
|
373
|
+
Implemented by: `ki2d-cluster-spotter` (spot).
|
|
374
|
+
|
|
333
375
|
## ✅ `export` — log file generation
|
|
334
376
|
|
|
335
377
|
```ts
|
|
@@ -1070,7 +1112,7 @@ interface CommandHook {
|
|
|
1070
1112
|
// CommandInterpretation: {expectsParams?, mixedCase?, error?, describe?, confirm?, commands?: CommandAction[]}
|
|
1071
1113
|
// CommandCatalogEntry: {command, describe, params?, category?, needsOperation?, expectsParams?, suggest?}
|
|
1072
1114
|
// CommandAction (closed set): {setVfo?, setPower?, updateQso?, addQso?,
|
|
1073
|
-
// setKeyerSpeed?, showKeyer?, setEnterSends?, sendSpots?, syncAll?, updateSettings?, updateSetting?, openSettings?,
|
|
1115
|
+
// setKeyerSpeed?, showKeyer?, setEnterSends?, sendSpots?, autoRespot?, syncAll?, updateSettings?, updateSetting?, openSettings?,
|
|
1074
1116
|
// updateOperation?, markSegment?, setCallField?, reloadDataFiles?, devNotice?, devStatus?,
|
|
1075
1117
|
// devProgress?, devHammer?, devSplash?, devOnboard?, devTester?, devPopup?,
|
|
1076
1118
|
// devTimeTravel?, toggleExperiment?}
|
package/package.json
CHANGED
package/samples/README.md
CHANGED
|
@@ -6,6 +6,7 @@ building — the header comment in each `src/index.ts` says what it leaves out.
|
|
|
6
6
|
|
|
7
7
|
| | |
|
|
8
8
|
| --- | --- |
|
|
9
|
+
| `k2hrc-hello-world` | The smallest extension there is: one panel, saying hello |
|
|
9
10
|
| `k2hrc-hamqth` | A callsign lookup, and the `account` hook that asks for a password |
|
|
10
11
|
| `k2hrc-llota` | An award program: references, an offline list, activation scoring |
|
|
11
12
|
| `k2hrc-cqww` | A contest: an exchange to type, and a score to keep |
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"key": "k2hrc-hello-world",
|
|
3
|
+
"name": "Hello World",
|
|
4
|
+
"shortName": "Hello",
|
|
5
|
+
"version": "1.0.0",
|
|
6
|
+
"description": "The smallest possible Ham2K extension — a panel that says hello",
|
|
7
|
+
"category": "dashboard",
|
|
8
|
+
"icon": "hand-wave-outline",
|
|
9
|
+
"api": 1,
|
|
10
|
+
"keywords": ["sample", "example", "hello"],
|
|
11
|
+
"hooks": ["panel"],
|
|
12
|
+
"sharedDependencies": {
|
|
13
|
+
"@ham2k/lib-callsigns": "^1.0.2",
|
|
14
|
+
"@ham2k/lib-country-files": "^1.0.202605301",
|
|
15
|
+
"@ham2k/lib-dxcc-data": "^1.0.1",
|
|
16
|
+
"@ham2k/lib-format-tools": "^1.1.3",
|
|
17
|
+
"@ham2k/lib-geo-tools": "^1.1.1",
|
|
18
|
+
"@ham2k/lib-operation-data": "^1.1.3",
|
|
19
|
+
"i18next": "^23.16.8",
|
|
20
|
+
"liquidjs": "^10.28.0"
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
// Copyright ©️ 2026 Sebastian Delmont <sd@ham2k.com>
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
//
|
|
4
|
+
// The smallest extension that does something visible: one panel, saying hello.
|
|
5
|
+
//
|
|
6
|
+
// It exists to be COPIED. Everything here is the minimum — one hook, one
|
|
7
|
+
// descriptor, one render — so that what a real extension adds is obvious by
|
|
8
|
+
// comparison. See the catalog's `ham2k-custom-text` for a panel that earns its
|
|
9
|
+
// keep, and docs/extensions/hooks.md for every other hook there is.
|
|
10
|
+
//
|
|
11
|
+
// Its manifest still lists the host's shared libraries in `sharedDependencies`:
|
|
12
|
+
// the SDK reaches them, and declaring them makes the bundle use the host's
|
|
13
|
+
// copy rather than carry its own — see docs/extensions/distribution.md.
|
|
14
|
+
|
|
15
|
+
import { defineExtension } from "@ham2k/extension-sdk"
|
|
16
|
+
import type { HookContext, PanelContent, PanelDescriptor, PanelHook, PanelRenderArgs } from "@ham2k/extension-sdk"
|
|
17
|
+
|
|
18
|
+
import manifest from "../manifest.json"
|
|
19
|
+
|
|
20
|
+
const HelloPanel: PanelHook = {
|
|
21
|
+
/// What the operator sees when choosing a panel to place.
|
|
22
|
+
async getPanels(_args: Record<string, never>, _ctx: HookContext): Promise<PanelDescriptor[]> {
|
|
23
|
+
return [
|
|
24
|
+
{
|
|
25
|
+
key: "hello",
|
|
26
|
+
title: "Hello World",
|
|
27
|
+
description: "A panel that says hello",
|
|
28
|
+
icon: "hand-wave-outline",
|
|
29
|
+
},
|
|
30
|
+
]
|
|
31
|
+
},
|
|
32
|
+
|
|
33
|
+
/// What the pane shows. Markdown, rendered by the app — a panel returns
|
|
34
|
+
/// content, never widgets, which is what keeps extensions out of the UI
|
|
35
|
+
/// toolkit's way.
|
|
36
|
+
///
|
|
37
|
+
/// The method is `render`, not `renderPanel`: the host routes to the hook by
|
|
38
|
+
/// its registered key and then calls this by name, so a near-miss is a panel
|
|
39
|
+
/// that installs, activates, and then renders nothing.
|
|
40
|
+
async render(_args: PanelRenderArgs, _ctx: HookContext): Promise<PanelContent> {
|
|
41
|
+
return {
|
|
42
|
+
kind: "markdown",
|
|
43
|
+
content: ["# Hello, World!", "", "This panel came from an extension installed from a `.h2kext` file."].join("\n"),
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
defineExtension({
|
|
49
|
+
...manifest,
|
|
50
|
+
onActivation({ registerHook }) {
|
|
51
|
+
registerHook("panel", { key: "k2hrc-hello-world", hook: HelloPanel })
|
|
52
|
+
},
|
|
53
|
+
})
|