@ham2k/extension-sdk 0.1.0 → 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/AGENTS.md +139 -0
- package/LICENSE +21 -0
- package/README.md +19 -8
- package/dist/activityExports.js +12 -6
- package/dist/activityScoring.js +2 -0
- package/dist/dxcc.js +15 -2
- package/dist/index.d.ts +71 -4
- package/dist/index.js +16 -14
- package/dist/modes.js +20 -0
- package/dist/refTransforms.js +34 -0
- package/dist/referenceActivity.js +89 -18
- package/dist/scoring.js +11 -4
- package/dist/segments.js +17 -0
- package/dist/templateContext.js +4 -0
- package/docs/distribution.md +445 -0
- package/docs/forms.md +279 -0
- package/docs/hooks.md +1379 -0
- package/docs/settings.md +524 -0
- package/docs/templates.md +206 -0
- package/package.json +22 -3
- package/samples/README.md +33 -0
- package/samples/k2hrc-cqww/build.mjs +6 -0
- package/samples/k2hrc-cqww/manifest.json +23 -0
- package/samples/k2hrc-cqww/src/index.ts +273 -0
- package/samples/k2hrc-hamqth/build.mjs +6 -0
- package/samples/k2hrc-hamqth/manifest.json +24 -0
- package/samples/k2hrc-hamqth/src/index.ts +202 -0
- package/samples/k2hrc-llota/build.mjs +6 -0
- package/samples/k2hrc-llota/manifest.json +25 -0
- package/samples/k2hrc-llota/src/index.ts +172 -0
- package/samples/k2hrc-radio/build.mjs +6 -0
- package/samples/k2hrc-radio/manifest.json +23 -0
- package/samples/k2hrc-radio/src/index.ts +145 -0
package/AGENTS.md
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
# Writing a Ham2K Logger extension
|
|
2
|
+
|
|
3
|
+
*This file describes the published `@ham2k/extension-sdk` package, as installed
|
|
4
|
+
in an extension project's `node_modules`. Working inside the Ham2K Logger
|
|
5
|
+
repository itself is a different setup — see `docs/extensions/README.md` there.*
|
|
6
|
+
|
|
7
|
+
You are building an extension for the Ham2K Logger — a TypeScript module the
|
|
8
|
+
app loads into a sandbox and calls through **hooks**. Extensions are where the
|
|
9
|
+
app's domain knowledge lives: award programs, contests, callsign lookups, spot
|
|
10
|
+
sources, exporters, panels.
|
|
11
|
+
|
|
12
|
+
Everything you need is in this package. `samples/` holds one worked extension
|
|
13
|
+
per shape — start there, with the one whose shape matches what you are
|
|
14
|
+
building. `docs/` is the reference, pinned to the version of the SDK installed
|
|
15
|
+
here; `dist/index.d.ts` is the typed surface, and is the final word wherever
|
|
16
|
+
prose and types disagree.
|
|
17
|
+
|
|
18
|
+
Paths like `app/lib/config/secret_policy.dart` or `app/experiments.yaml`,
|
|
19
|
+
mentioned in those docs, are files in the Ham2K Logger's own repository
|
|
20
|
+
(<https://github.com/ham2k/halo>). They are not part of your project and you
|
|
21
|
+
cannot read them here.
|
|
22
|
+
|
|
23
|
+
## The shape of a project
|
|
24
|
+
|
|
25
|
+
```
|
|
26
|
+
manifest.json your extension's public identity — read without running code
|
|
27
|
+
src/index.ts imports the manifest, calls defineExtension
|
|
28
|
+
build.mjs runs @ham2k/extension-tools' build preset
|
|
29
|
+
build/ generated: index.js + manifest.json
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
`npx -p @ham2k/extension-tools h2kext-init <yourcallsign>-<name>` writes all of it.
|
|
33
|
+
|
|
34
|
+
## The smallest whole extension
|
|
35
|
+
|
|
36
|
+
```ts
|
|
37
|
+
import { defineExtension } from '@ham2k/extension-sdk'
|
|
38
|
+
import type { PanelHook } from '@ham2k/extension-sdk'
|
|
39
|
+
|
|
40
|
+
import manifest from '../manifest.json'
|
|
41
|
+
|
|
42
|
+
const HelloPanel: PanelHook = {
|
|
43
|
+
async getPanels() {
|
|
44
|
+
return [{ key: 'hello', title: 'Hello World', icon: 'hand-wave-outline' }]
|
|
45
|
+
},
|
|
46
|
+
async render() {
|
|
47
|
+
return { kind: 'markdown', content: '# Hello, World!' }
|
|
48
|
+
},
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
defineExtension({
|
|
52
|
+
...manifest,
|
|
53
|
+
onActivation({ registerHook }) {
|
|
54
|
+
registerHook('panel', { key: manifest.key, hook: HelloPanel })
|
|
55
|
+
},
|
|
56
|
+
})
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
`manifest.json` needs at least `key`, `name`, `version`, `description`,
|
|
60
|
+
`category`, `api`, and the `sharedDependencies` below. The manifest is spread
|
|
61
|
+
into `defineExtension` rather than retyped: two sources for the same identity
|
|
62
|
+
drift, and the packer reads the file.
|
|
63
|
+
|
|
64
|
+
## Building and shipping
|
|
65
|
+
|
|
66
|
+
```sh
|
|
67
|
+
node build.mjs # → build/index.js
|
|
68
|
+
npx -p @ham2k/extension-tools h2kext-pack build -o my-extension.h2kext
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
The packer is also the checker. It reports every problem at once and refuses
|
|
72
|
+
anything that would install cleanly and then misbehave where you cannot watch
|
|
73
|
+
it, so a clean pack is the strongest evidence available outside the app itself.
|
|
74
|
+
Run it whenever you have changed the manifest or the hooks you register.
|
|
75
|
+
|
|
76
|
+
`tsc --noEmit` is the other half: the hook interfaces are typed, and a hook that
|
|
77
|
+
typechecks is registered under a category the host knows, with the methods it
|
|
78
|
+
will actually call.
|
|
79
|
+
|
|
80
|
+
## Where to read next
|
|
81
|
+
|
|
82
|
+
| Doing this | Read |
|
|
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) |
|
|
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
|
+
| A pane in a view: `getPanels`, `render`, panel content kinds | `docs/hooks.md` §`panel` |
|
|
87
|
+
| Callsign lookups, spot sources, exporters, reference types (POTA-style) | `docs/hooks.md` §`lookup`, §`spots`, §`export`, §`ref:<type>` |
|
|
88
|
+
| Contest or award scoring, and the UI an activity contributes | `docs/hooks.md` §`scoring`, §`activity` |
|
|
89
|
+
| A settings screen for your extension | `docs/settings.md` |
|
|
90
|
+
| Any form, dialog or field the user fills in | `docs/forms.md` |
|
|
91
|
+
| Panel text, export filenames, ADIF fields — anything with `{{ }}` in it | `docs/templates.md` |
|
|
92
|
+
| Packaging, the `.h2kext` format, keys, using the host's libraries | `docs/distribution.md` |
|
|
93
|
+
|
|
94
|
+
`docs/hooks.md` is long. Read the section for the category you are registering,
|
|
95
|
+
not the file.
|
|
96
|
+
|
|
97
|
+
## What the sandbox is
|
|
98
|
+
|
|
99
|
+
No Node, no DOM, no `fetch`, no `window`, no filesystem. One ES2020 engine
|
|
100
|
+
evaluating one script. Reach the outside world through `host`:
|
|
101
|
+
|
|
102
|
+
```ts
|
|
103
|
+
import { host } from '@ham2k/extension-sdk'
|
|
104
|
+
|
|
105
|
+
await host.fetch(url) // https only, and only domains your manifest lists
|
|
106
|
+
await host.kvGet('key') // storage, namespaced to your extension
|
|
107
|
+
await host.kvSet('key', value)
|
|
108
|
+
await host.showForm(form) // ask the user something
|
|
109
|
+
await host.showMessage({ … }) // tell them something
|
|
110
|
+
host.log('…') // the app's console
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
`dist/index.d.ts` has the rest, with the types.
|
|
114
|
+
|
|
115
|
+
## Traps worth knowing before you write anything
|
|
116
|
+
|
|
117
|
+
- **A panel returns content, not widgets.** `{ kind: 'markdown', content }` and
|
|
118
|
+
its siblings. There is no way to reach the app's UI toolkit, by design.
|
|
119
|
+
- **`host.fetch` only reaches domains your manifest's `domains` lists**, over
|
|
120
|
+
https. An undeclared host is refused at the call, not at build.
|
|
121
|
+
- **Using a shared library without declaring it fails the build.** The app
|
|
122
|
+
carries `@ham2k/lib-*`, `i18next` and `liquidjs`; naming one in
|
|
123
|
+
`sharedDependencies` rewrites your imports into lookups on the app's single
|
|
124
|
+
copy, which is worth roughly thirty times the bundle size. Don't install them.
|
|
125
|
+
`inline: ['liquidjs']` in your build ships your own copy, for when you need a
|
|
126
|
+
version the app does not carry. A scaffolded manifest already declares the
|
|
127
|
+
eight this SDK's own modules reach — importing anything from it pulls them
|
|
128
|
+
in — so deleting one from the manifest breaks a build that was working.
|
|
129
|
+
- **Your key is `<callsign>-<name>`**, lowercase. It namespaces your storage and
|
|
130
|
+
is what the app shows when it says where something came from. `ham2k-` is
|
|
131
|
+
reserved.
|
|
132
|
+
- **`host.secret()` returns null** for anything not built into the app. Degrade;
|
|
133
|
+
don't throw. A hook that throws takes its whole feature down.
|
|
134
|
+
- **Hook method names are routed by string.** The host finds your hook by its
|
|
135
|
+
registered key and then calls the method by name, so `renderPanel` where
|
|
136
|
+
`render` was meant is an extension that installs, activates, and does nothing.
|
|
137
|
+
This is what typing your hook as `PanelHook` (or its siblings) catches.
|
|
138
|
+
- **Every hook is async and crosses a bridge.** Some context reads carry a whole
|
|
139
|
+
log — fine when exporting, not on a per-keystroke path.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Sebastian Delmont <sd@ham2k.com>
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -26,7 +26,7 @@ const HelloPanel: PanelHook = {
|
|
|
26
26
|
defineExtension({
|
|
27
27
|
...manifest,
|
|
28
28
|
onActivation({ registerHook }) {
|
|
29
|
-
registerHook('panel', { key:
|
|
29
|
+
registerHook('panel', { key: manifest.key, hook: HelloPanel })
|
|
30
30
|
},
|
|
31
31
|
})
|
|
32
32
|
```
|
|
@@ -34,9 +34,24 @@ defineExtension({
|
|
|
34
34
|
A panel returns *content*, not widgets — which is what keeps extensions out of
|
|
35
35
|
the app's UI toolkit.
|
|
36
36
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
37
|
+
`npx -p @ham2k/extension-tools h2kext-init <yourcallsign>-<name>` writes
|
|
38
|
+
all of that and the build script; the same package packages the result into a
|
|
39
|
+
`.h2kext` the app can install.
|
|
40
|
+
|
|
41
|
+
## The reference is in this package
|
|
42
|
+
|
|
43
|
+
`samples/` holds one worked extension per shape — a callsign lookup with the
|
|
44
|
+
credentials it needs, an award program, a contest and its scorer, an HTML
|
|
45
|
+
panel. Each builds and packs; each says in its header what it leaves out.
|
|
46
|
+
|
|
47
|
+
`AGENTS.md` beside this file is the map — the manifest, the sandbox's limits,
|
|
48
|
+
and which of `docs/` answers what. `docs/` holds the references themselves:
|
|
49
|
+
every hook category, forms, settings panels, templates, and the bundle format.
|
|
50
|
+
|
|
51
|
+
They travel in the package rather than being linked to, so they are readable
|
|
52
|
+
offline and describe the version installed here rather than whatever the
|
|
53
|
+
repository says today. If an agent is writing your extension, point it at
|
|
54
|
+
`node_modules/@ham2k/extension-sdk/AGENTS.md` — `h2kext-init` does that for you.
|
|
40
55
|
|
|
41
56
|
## About the peer dependencies
|
|
42
57
|
|
|
@@ -52,7 +67,3 @@ in, so your editor works with only this package installed.
|
|
|
52
67
|
|
|
53
68
|
Install one only to `inline` it — to ship your own copy because you need a
|
|
54
69
|
version the host does not carry.
|
|
55
|
-
|
|
56
|
-
## License
|
|
57
|
-
|
|
58
|
-
MPL-2.0
|
package/dist/activityExports.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { exportFilename, startMillisOf } from "./exportNames.js";
|
|
2
|
+
import { segmentsWith } from "./segments.js";
|
|
2
3
|
import { createCachedTranslator } from "./i18n.js";
|
|
3
4
|
import en from "./i18n/activityExports.en.json";
|
|
4
5
|
import es from "./i18n/activityExports.es.json";
|
|
@@ -50,13 +51,16 @@ function activityExportHook(rules) {
|
|
|
50
51
|
if (!refsOfType(args.operation, rules.activationType).some((r) => r.ref === wanted)) {
|
|
51
52
|
throw new Error(`${rules.key}: operation has no ${rules.activationType} reference '${wanted}'`);
|
|
52
53
|
}
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
54
|
+
const claimOnly = (operation) => ({
|
|
55
|
+
...operation,
|
|
56
|
+
refs: (operation.refs ?? []).filter(
|
|
57
|
+
(r) => r?.type !== rules.activationType || r?.ref === wanted
|
|
58
|
+
)
|
|
59
|
+
});
|
|
57
60
|
const content = await adifForExport({
|
|
58
|
-
operation,
|
|
61
|
+
operation: claimOnly(args.operation),
|
|
59
62
|
qsos: args.qsos,
|
|
63
|
+
segments: segmentsWith(args.segments, claimOnly),
|
|
60
64
|
includePrivateData: args.includePrivateData,
|
|
61
65
|
mainHandler: rules.key,
|
|
62
66
|
includeFieldsFrom: rules.includeFieldsFrom
|
|
@@ -113,6 +117,7 @@ function huntingExportHook(rules) {
|
|
|
113
117
|
const content = await adifForExport({
|
|
114
118
|
operation: args.operation,
|
|
115
119
|
qsos: huntedQsos(args.qsos ?? []),
|
|
120
|
+
segments: args.segments,
|
|
116
121
|
includePrivateData: args.includePrivateData,
|
|
117
122
|
// The hunter file is submitted to ONE program too — see
|
|
118
123
|
// `activityExportHook.generateExport`. `rules.key` is the ACTIVITY's
|
|
@@ -131,11 +136,12 @@ function huntingExportHook(rules) {
|
|
|
131
136
|
}
|
|
132
137
|
};
|
|
133
138
|
}
|
|
134
|
-
async function adifForExport({ operation, qsos, includePrivateData, mainHandler, includeFieldsFrom }) {
|
|
139
|
+
async function adifForExport({ operation, qsos, segments, includePrivateData, mainHandler, includeFieldsFrom }) {
|
|
135
140
|
const { hooks } = await import("@ham2k/extension-sdk");
|
|
136
141
|
const entries = await hooks.invokeOne("export", "adif", "generateExport", {
|
|
137
142
|
operation,
|
|
138
143
|
qsos,
|
|
144
|
+
segments,
|
|
139
145
|
exportType: "adif",
|
|
140
146
|
// Forwarded, or the delegate defaults to withholding and the user's
|
|
141
147
|
// "include private info" setting silently does nothing here.
|
package/dist/activityScoring.js
CHANGED
|
@@ -127,6 +127,8 @@ function activityScorer(rules) {
|
|
|
127
127
|
score.alerts = ["duplicate"];
|
|
128
128
|
} else if (verdict) {
|
|
129
129
|
score.notices = [NOTICE_KEYS[verdict]];
|
|
130
|
+
} else if (priors.length > 0 && score.value > 0 && freshRefRescuesActivation) {
|
|
131
|
+
score.dupe = false;
|
|
130
132
|
}
|
|
131
133
|
const packed = packContact(day, band, mode, huntedRefs);
|
|
132
134
|
if (priors.length > 0) priors.push(packed);
|
package/dist/dxcc.js
CHANGED
|
@@ -2,8 +2,8 @@ import { parseCallsign } from "@ham2k/lib-callsigns";
|
|
|
2
2
|
import { annotateFromCountryFile, useBuiltinCountryFile } from "@ham2k/lib-country-files";
|
|
3
3
|
import { DXCC_BY_CODE } from "@ham2k/lib-dxcc-data";
|
|
4
4
|
useBuiltinCountryFile();
|
|
5
|
-
function annotateCallAgainstCountryFile(call) {
|
|
6
|
-
return annotateFromCountryFile(parseCallsign(call));
|
|
5
|
+
function annotateCallAgainstCountryFile(call, options) {
|
|
6
|
+
return annotateFromCountryFile(parseCallsign(call), options);
|
|
7
7
|
}
|
|
8
8
|
function entityPrefixForCall(call) {
|
|
9
9
|
if (!call) return void 0;
|
|
@@ -17,8 +17,21 @@ function regionAndCountryForDxccCode(dxccCode) {
|
|
|
17
17
|
const entity = DXCC_BY_CODE[dxccCode];
|
|
18
18
|
return { ituRegion: entity?.ituRegion, countryCode: entity?.countryCode };
|
|
19
19
|
}
|
|
20
|
+
function countryPrefixForCall(call) {
|
|
21
|
+
if (!call) return void 0;
|
|
22
|
+
const annotated = annotateCallAgainstCountryFile(call);
|
|
23
|
+
const { countryCode } = regionAndCountryForDxccCode(annotated.dxccCode);
|
|
24
|
+
return countryCode ? countryCode.toUpperCase() : annotated.entityPrefix;
|
|
25
|
+
}
|
|
26
|
+
function entityForCall(call) {
|
|
27
|
+
if (!call) return {};
|
|
28
|
+
const { dxccCode, continent, entityName } = annotateCallAgainstCountryFile(call);
|
|
29
|
+
return { dxccCode, continent, entityName };
|
|
30
|
+
}
|
|
20
31
|
export {
|
|
21
32
|
annotateCallAgainstCountryFile,
|
|
33
|
+
countryPrefixForCall,
|
|
34
|
+
entityForCall,
|
|
22
35
|
entityPrefixForCall,
|
|
23
36
|
flagForDxccCode,
|
|
24
37
|
regionAndCountryForDxccCode
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// @ham2k/extension-sdk 0.
|
|
1
|
+
// @ham2k/extension-sdk 0.3.0
|
|
2
2
|
export type CallInfo = {
|
|
3
3
|
call: string;
|
|
4
4
|
baseCall?: string;
|
|
@@ -103,8 +103,14 @@ export interface HookContext {
|
|
|
103
103
|
distanceUnits?: string;
|
|
104
104
|
decimalSeparator?: string;
|
|
105
105
|
groupingSeparator?: string;
|
|
106
|
+
gridPrecision?: number;
|
|
106
107
|
edition?: string;
|
|
107
108
|
enabledExperiments?: string[];
|
|
109
|
+
experiments?: {
|
|
110
|
+
key: string;
|
|
111
|
+
name: string;
|
|
112
|
+
aliases: string[];
|
|
113
|
+
}[];
|
|
108
114
|
getOperation?(uuid: string): Promise<Record<string, JSONValue> | null>;
|
|
109
115
|
getQsos?(operationUuid: string): Promise<Record<string, JSONValue>[] | null>;
|
|
110
116
|
getSpotsForCall?(call: string, maxAgeMinutes?: number): Promise<Spot[]>;
|
|
@@ -275,6 +281,7 @@ export interface SerialInputDescriptor {
|
|
|
275
281
|
export interface FormInputDescriptor {
|
|
276
282
|
kind: "form";
|
|
277
283
|
refType: string;
|
|
284
|
+
alsoHandles?: string[];
|
|
278
285
|
form: FormDefinition;
|
|
279
286
|
}
|
|
280
287
|
export interface TextInputDescriptor {
|
|
@@ -304,6 +311,10 @@ export interface OptionsInputDescriptor {
|
|
|
304
311
|
matchOn?: ("code" | "name")[];
|
|
305
312
|
allowFreeform?: boolean;
|
|
306
313
|
preferredCodes?: string[];
|
|
314
|
+
multiValue?: {
|
|
315
|
+
separator: string;
|
|
316
|
+
inheritPrefix?: number;
|
|
317
|
+
};
|
|
307
318
|
minCharsForSuggestions?: number;
|
|
308
319
|
placeholder?: string;
|
|
309
320
|
uppercase?: boolean;
|
|
@@ -380,6 +391,7 @@ export interface AdifImportHook {
|
|
|
380
391
|
export interface ExportRequest {
|
|
381
392
|
operation: Record<string, JSONValue>;
|
|
382
393
|
qsos: Record<string, JSONValue>[];
|
|
394
|
+
segments?: OperationSegmentPayload[];
|
|
383
395
|
exportType?: string;
|
|
384
396
|
compactFilenames?: boolean;
|
|
385
397
|
includePrivateData?: boolean;
|
|
@@ -464,6 +476,7 @@ export interface ScoreQsoRequest {
|
|
|
464
476
|
export interface QsoScoreNotices {
|
|
465
477
|
notices: string[];
|
|
466
478
|
alerts: string[];
|
|
479
|
+
dupe?: boolean;
|
|
467
480
|
}
|
|
468
481
|
export interface ScoreCandidatesRequest {
|
|
469
482
|
operation: Record<string, JSONValue>;
|
|
@@ -517,6 +530,10 @@ export interface CommandAction {
|
|
|
517
530
|
setPower?: {
|
|
518
531
|
watts: number;
|
|
519
532
|
};
|
|
533
|
+
setKeyerSpeed?: {
|
|
534
|
+
wpm: number;
|
|
535
|
+
};
|
|
536
|
+
showKeyer?: Record<string, never>;
|
|
520
537
|
updateQso?: Record<string, JSONValue>;
|
|
521
538
|
addQso?: Record<string, JSONValue>;
|
|
522
539
|
sendSpots?: {
|
|
@@ -540,6 +557,10 @@ export interface CommandAction {
|
|
|
540
557
|
operatorCall?: string;
|
|
541
558
|
multiIdentifier?: string;
|
|
542
559
|
};
|
|
560
|
+
markSegment?: {
|
|
561
|
+
kind: "start" | "break" | "stop";
|
|
562
|
+
setLocation?: boolean;
|
|
563
|
+
};
|
|
543
564
|
setCallField?: {
|
|
544
565
|
text: string;
|
|
545
566
|
};
|
|
@@ -575,6 +596,9 @@ export interface CommandAction {
|
|
|
575
596
|
deltaMillis?: number;
|
|
576
597
|
speed?: number;
|
|
577
598
|
};
|
|
599
|
+
devBench?: {
|
|
600
|
+
qsoCount?: number;
|
|
601
|
+
};
|
|
578
602
|
toggleExperiment?: {
|
|
579
603
|
token: string;
|
|
580
604
|
};
|
|
@@ -723,6 +747,14 @@ export interface ActivationApi {
|
|
|
723
747
|
registerHook(category: string, params: RegisterHookParams): void;
|
|
724
748
|
hostCall(method: string, params: Record<string, unknown>): Promise<unknown>;
|
|
725
749
|
}
|
|
750
|
+
export interface ExtensionRelevance {
|
|
751
|
+
entities?: string[];
|
|
752
|
+
countries?: string[];
|
|
753
|
+
continents?: string[];
|
|
754
|
+
ituRegions?: number[];
|
|
755
|
+
dates?: string[];
|
|
756
|
+
interests?: string[];
|
|
757
|
+
}
|
|
726
758
|
export interface ExtensionManifest {
|
|
727
759
|
key: string;
|
|
728
760
|
name: string;
|
|
@@ -739,6 +771,7 @@ export interface ExtensionManifest {
|
|
|
739
771
|
allowUserAgentOverride?: boolean;
|
|
740
772
|
requiresLocation?: boolean;
|
|
741
773
|
experiments?: string[];
|
|
774
|
+
relevance?: ExtensionRelevance;
|
|
742
775
|
translations?: Record<string, {
|
|
743
776
|
name?: string;
|
|
744
777
|
description?: string;
|
|
@@ -883,7 +916,7 @@ export interface FormHeader {
|
|
|
883
916
|
title: string;
|
|
884
917
|
subtitle?: string;
|
|
885
918
|
devMode?: boolean;
|
|
886
|
-
style?: "default" | "section";
|
|
919
|
+
style?: "default" | "section" | "category";
|
|
887
920
|
environment?: string | string[];
|
|
888
921
|
}
|
|
889
922
|
export interface FormSectionDivider {
|
|
@@ -1129,13 +1162,17 @@ type AnnotatedCallInfo$1 = CombinedCallInfo & {
|
|
|
1129
1162
|
originalValues?: AnnotatedCallInfo$1;
|
|
1130
1163
|
cfValues?: AnnotatedCallInfo$1;
|
|
1131
1164
|
};
|
|
1132
|
-
export declare function annotateCallAgainstCountryFile(call: string
|
|
1165
|
+
export declare function annotateCallAgainstCountryFile(call: string, options?: {
|
|
1166
|
+
wae?: boolean;
|
|
1167
|
+
}): AnnotatedCallInfo$1;
|
|
1133
1168
|
export declare function entityPrefixForCall(call: string | undefined): string | undefined;
|
|
1134
1169
|
export declare function flagForDxccCode(dxccCode: number | undefined): string | undefined;
|
|
1135
1170
|
export declare function regionAndCountryForDxccCode(dxccCode: number | undefined): {
|
|
1136
1171
|
ituRegion?: number;
|
|
1137
1172
|
countryCode?: string;
|
|
1138
1173
|
};
|
|
1174
|
+
export declare function countryPrefixForCall(call: string | undefined): string | undefined;
|
|
1175
|
+
export declare function entityForCall(call: string | undefined): Pick<AnnotatedCallInfo$1, "dxccCode" | "continent" | "entityName">;
|
|
1139
1176
|
export declare const LOCATION_ACCURACY: {
|
|
1140
1177
|
readonly EXACT: 0;
|
|
1141
1178
|
readonly ACCURATE: 2;
|
|
@@ -1149,6 +1186,14 @@ export declare function applyLatLonFromApiFields(target: {
|
|
|
1149
1186
|
}, raw: Record<string, unknown>): void;
|
|
1150
1187
|
export declare function canonicalMode(mode: string): string;
|
|
1151
1188
|
export declare function modesMatchForDupe(worked: string, candidate: string): boolean;
|
|
1189
|
+
export declare function expectedSideband({ freqKHz, band }: {
|
|
1190
|
+
freqKHz?: number;
|
|
1191
|
+
band?: string;
|
|
1192
|
+
}): string | undefined;
|
|
1193
|
+
export declare function isUnexpectedSideband(mode: string | undefined | null, { freqKHz, band }: {
|
|
1194
|
+
freqKHz?: number;
|
|
1195
|
+
band?: string;
|
|
1196
|
+
}): boolean;
|
|
1152
1197
|
export interface ContestScorerOptions {
|
|
1153
1198
|
scope?: ScoringScope;
|
|
1154
1199
|
}
|
|
@@ -1156,6 +1201,8 @@ export declare function contestScorer<S extends Scoresheet = Scoresheet>(scorer:
|
|
|
1156
1201
|
scope: ScoringScope;
|
|
1157
1202
|
};
|
|
1158
1203
|
export declare function tally(key: string, forScope: "day" | "operation", total: number, extra?: Partial<ScoreTally>): ScoreTally;
|
|
1204
|
+
export declare function operationForQso(base: Record<string, JSONValue>, qso: Record<string, JSONValue>, segments?: OperationSegmentPayload[]): Record<string, JSONValue>;
|
|
1205
|
+
export declare function segmentsWith(segments: OperationSegmentPayload[] | undefined, transform: (operation: Record<string, JSONValue>) => Record<string, JSONValue>): OperationSegmentPayload[] | undefined;
|
|
1159
1206
|
export type UniqueAxis = "day" | "band" | "mode" | "ref";
|
|
1160
1207
|
export interface ActivityScoringRules {
|
|
1161
1208
|
label: string;
|
|
@@ -1210,9 +1257,10 @@ export declare function huntingExportHook(rules: HuntingExportRules): {
|
|
|
1210
1257
|
suggestExportOptions({ operation, qsos, compactFilenames }: ExportOptionsRequest, ctx: HookContext): Promise<ExportOption[]>;
|
|
1211
1258
|
generateExport(args: ExportRequest, ctx: HookContext): Promise<ExportResult>;
|
|
1212
1259
|
};
|
|
1213
|
-
export declare function adifForExport({ operation, qsos, includePrivateData, mainHandler, includeFieldsFrom }: {
|
|
1260
|
+
export declare function adifForExport({ operation, qsos, segments, includePrivateData, mainHandler, includeFieldsFrom }: {
|
|
1214
1261
|
operation: Record<string, JSONValue>;
|
|
1215
1262
|
qsos: Record<string, JSONValue>[];
|
|
1263
|
+
segments?: OperationSegmentPayload[];
|
|
1216
1264
|
includePrivateData?: boolean;
|
|
1217
1265
|
mainHandler: string;
|
|
1218
1266
|
includeFieldsFrom?: string[];
|
|
@@ -1236,6 +1284,12 @@ export interface ExportNameParts {
|
|
|
1236
1284
|
}
|
|
1237
1285
|
export declare function exportFilename(parts: ExportNameParts): string;
|
|
1238
1286
|
export declare function startMillisOf(operation: Record<string, JSONValue>, qsos?: Record<string, JSONValue>[]): number | undefined;
|
|
1287
|
+
export type RefTransform = {
|
|
1288
|
+
pattern: string;
|
|
1289
|
+
replacement: string;
|
|
1290
|
+
flags?: string;
|
|
1291
|
+
};
|
|
1292
|
+
export declare function transformsForPrefix(prefix: string): RefTransform[];
|
|
1239
1293
|
export interface ReferenceActivityConfig {
|
|
1240
1294
|
key: string;
|
|
1241
1295
|
label: string;
|
|
@@ -1254,7 +1308,20 @@ export interface ReferenceActivityConfig {
|
|
|
1254
1308
|
adifRefField?: string;
|
|
1255
1309
|
allowsMultiple?: boolean;
|
|
1256
1310
|
linkUrl?: (reference: string) => string;
|
|
1311
|
+
transforms?: RefTransform[];
|
|
1312
|
+
refInput?: (args: RefInputArgs, ctx: HookContext) => Promise<RefInputExtras> | RefInputExtras;
|
|
1313
|
+
}
|
|
1314
|
+
export interface RefInputArgs {
|
|
1315
|
+
operation: Record<string, JSONValue>;
|
|
1316
|
+
qso?: Record<string, JSONValue>;
|
|
1317
|
+
side: "activation" | "hunting";
|
|
1318
|
+
}
|
|
1319
|
+
export interface RefInputExtras {
|
|
1320
|
+
placeholder?: string;
|
|
1321
|
+
transforms?: RefTransform[];
|
|
1257
1322
|
}
|
|
1323
|
+
export declare function learnedReferencePrefix(category: string, entityPrefix: string, fallback: string): Promise<string>;
|
|
1324
|
+
export declare function looksLikeReference(text: string, referenceRegex: RegExp): boolean;
|
|
1258
1325
|
export interface ReferenceActivityHooks {
|
|
1259
1326
|
refHandler: Record<string, unknown>;
|
|
1260
1327
|
activityHook: Record<string, unknown>;
|
package/dist/index.js
CHANGED
|
@@ -1,17 +1,19 @@
|
|
|
1
|
-
export * from "./types";
|
|
2
|
-
export * from "./i18n";
|
|
3
|
-
export * from "./dxcc";
|
|
4
|
-
export * from "./location";
|
|
5
|
-
export * from "./modes";
|
|
6
|
-
export * from "./scoring";
|
|
7
|
-
export * from "./
|
|
8
|
-
export * from "./
|
|
9
|
-
export * from "./
|
|
10
|
-
export * from "./
|
|
11
|
-
export * from "./
|
|
12
|
-
export * from "./
|
|
13
|
-
export * from "./
|
|
14
|
-
export * from "./
|
|
1
|
+
export * from "./types.js";
|
|
2
|
+
export * from "./i18n.js";
|
|
3
|
+
export * from "./dxcc.js";
|
|
4
|
+
export * from "./location.js";
|
|
5
|
+
export * from "./modes.js";
|
|
6
|
+
export * from "./scoring.js";
|
|
7
|
+
export * from "./segments.js";
|
|
8
|
+
export * from "./activityScoring.js";
|
|
9
|
+
export * from "./activityExports.js";
|
|
10
|
+
export * from "./activityAdifImport.js";
|
|
11
|
+
export * from "./exportNames.js";
|
|
12
|
+
export * from "./referenceActivity.js";
|
|
13
|
+
export * from "./refTransforms.js";
|
|
14
|
+
export * from "./templates.js";
|
|
15
|
+
export * from "./templateContext.js";
|
|
16
|
+
export * from "./testOperation.js";
|
|
15
17
|
let currentExtensionKey = "unknown";
|
|
16
18
|
const privilegedHostCalls = {};
|
|
17
19
|
function kernel() {
|
package/dist/modes.js
CHANGED
|
@@ -22,7 +22,27 @@ function modesMatchForDupe(worked, candidate) {
|
|
|
22
22
|
const superMode = COARSE_SUPER_MODES[c];
|
|
23
23
|
return Boolean(superMode) && KNOWN_MODES.has(w) && superModeForMode(w) === superMode;
|
|
24
24
|
}
|
|
25
|
+
const LSB_BANDS = /* @__PURE__ */ new Set(["2190m", "2200m", "630m", "560m", "160m", "80m", "75m", "40m"]);
|
|
26
|
+
const UPPER_SIDEBAND_FLOOR_KHZ = 9e3;
|
|
27
|
+
const SIXTY_METRE_LO_KHZ = 5e3;
|
|
28
|
+
const SIXTY_METRE_HI_KHZ = 6e3;
|
|
29
|
+
function expectedSideband({ freqKHz, band }) {
|
|
30
|
+
if (freqKHz != null) {
|
|
31
|
+
if (freqKHz >= UPPER_SIDEBAND_FLOOR_KHZ) return "USB";
|
|
32
|
+
return freqKHz >= SIXTY_METRE_LO_KHZ && freqKHz < SIXTY_METRE_HI_KHZ ? "USB" : "LSB";
|
|
33
|
+
}
|
|
34
|
+
if (!band) return void 0;
|
|
35
|
+
return LSB_BANDS.has(band) ? "LSB" : "USB";
|
|
36
|
+
}
|
|
37
|
+
function isUnexpectedSideband(mode, { freqKHz, band }) {
|
|
38
|
+
const upper = (mode ?? "").toUpperCase();
|
|
39
|
+
if (upper !== "USB" && upper !== "LSB") return false;
|
|
40
|
+
const expected = expectedSideband({ freqKHz, band });
|
|
41
|
+
return expected !== void 0 && upper !== expected;
|
|
42
|
+
}
|
|
25
43
|
export {
|
|
26
44
|
canonicalMode,
|
|
45
|
+
expectedSideband,
|
|
46
|
+
isUnexpectedSideband,
|
|
27
47
|
modesMatchForDupe
|
|
28
48
|
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
function transformsForPrefix(prefix) {
|
|
2
|
+
return [
|
|
3
|
+
// a run of spaces between two refs -> ", " (normalize to one separator style)
|
|
4
|
+
{ pattern: "([A-Z0-9]-\\d+|TEST) +(?=[A-Z0-9])", replacement: "${1}, ", flags: "gi" },
|
|
5
|
+
// bare number (or "TEST"), at the start or right after a separator -> prefix-number
|
|
6
|
+
// (the separator itself is captured and replayed, not consumed, so a
|
|
7
|
+
// pasted "US-1234,5678" keeps its comma instead of merging into one ref).
|
|
8
|
+
// Braced `${1}`/`${2}`, not bare `$1`/`$2` — `prefix` can itself start
|
|
9
|
+
// with a digit (e.g. DXCC "3D2" for Fiji), and a bare "$1" immediately
|
|
10
|
+
// followed by that would misparse as capture group 13.
|
|
11
|
+
{ pattern: "(^|,\\s*)(\\d\\d+|TEST)", replacement: `\${1}${prefix}-\${2}`, flags: "gi" },
|
|
12
|
+
// "US1234" -> "US-1234". Anchored with a negative lookbehind so it only
|
|
13
|
+
// fires at the START of a not-yet-dashed reference — without it, a
|
|
14
|
+
// multi-character DXCC prefix that mixes letters and digits (e.g.
|
|
15
|
+
// Fiji's "3D2") gets its OWN internal "D2" mismatched as an undashed
|
|
16
|
+
// reference once transform 2 above has already inserted "3D2-1234",
|
|
17
|
+
// corrupting it into "3D-2-1234".
|
|
18
|
+
{ pattern: "(?<![A-Z0-9])([A-Z]+)(\\d+|TEST)", replacement: "${1}-${2}", flags: "gi" },
|
|
19
|
+
// NOTE: deliberately no "eagerly insert the next prefix right after a
|
|
20
|
+
// trailing comma" rule here (app-polo's inputs have one) — transform 2
|
|
21
|
+
// above already prefixes a second reference once 2+ digits appear after
|
|
22
|
+
// the comma, the same way it does at the start of the field. An eager
|
|
23
|
+
// version that inserted "US-" the instant a trailing comma appeared
|
|
24
|
+
// makes deleting back through a second reference impossible: backspacing
|
|
25
|
+
// "US-1234,US-5678" down to "US-1234,US-" and then to "US-1234," would
|
|
26
|
+
// immediately re-grow the trailing "US-1234,US-" the transform had just
|
|
27
|
+
// reinserted, so the comma itself could never be reached.
|
|
28
|
+
// strip anything that can't appear in a reference list
|
|
29
|
+
{ pattern: "[^A-Z0-9\\-, ]", replacement: "", flags: "gi" }
|
|
30
|
+
];
|
|
31
|
+
}
|
|
32
|
+
export {
|
|
33
|
+
transformsForPrefix
|
|
34
|
+
};
|