@catheadowl/ukp 0.1.1 → 0.2.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 +34 -24
- package/package.json +2 -4
- package/src/capabilities/nav.ts +4 -2
- package/src/capabilities/read.ts +33 -3
- package/src/capabilities/remote-client.ts +306 -60
- package/src/capabilities/rg.ts +2 -0
- package/src/capabilities/search.ts +25 -5
- package/src/capabilities/tls-identity.ts +172 -30
- package/src/cli.ts +1 -1
- package/src/commands/guide-content/client.txt +12 -0
- package/src/commands/guide-content/remote.txt +64 -0
- package/src/commands/guide-content/service-qmd.txt +1 -1
- package/src/commands/guide-content/service.txt +33 -32
- package/src/commands/guide.ts +11 -4
- package/src/commands/init.ts +9 -12
- package/src/commands/inventory.ts +188 -47
- package/src/commands/nav.ts +1 -1
- package/src/commands/read.ts +48 -9
- package/src/commands/rg.ts +4 -1
- package/src/commands/search.ts +4 -1
- package/src/commands/serve.ts +86 -12
- package/src/config/manifest.ts +6 -0
- package/src/registry.ts +104 -16
- package/src/server.ts +267 -44
- package/bun.lock +0 -35
- package/tsconfig.json +0 -12
package/README.md
CHANGED
|
@@ -70,7 +70,8 @@ predictable scope, and a single command surface. Use UKP when:
|
|
|
70
70
|
a host door — when you want it reachable from other machines.
|
|
71
71
|
- Want a read-only endpoint? Declare zero capabilities — an empty
|
|
72
72
|
`[capabilities]` table still registers one (derived `read`/`nav` only).
|
|
73
|
-
- Give agents JSON output (`--format json`),
|
|
73
|
+
- Give agents JSON output (`--json`; `ukp read` uses `--format json`),
|
|
74
|
+
reference sidecars, and
|
|
74
75
|
`ukp://` handoff keys when they need machine-readable results.
|
|
75
76
|
|
|
76
77
|
UKP owns endpoint names, Registry bindings, Client scope, capability
|
|
@@ -80,23 +81,24 @@ it does not rewrite provider config for you.
|
|
|
80
81
|
|
|
81
82
|
## Install
|
|
82
83
|
|
|
83
|
-
```bash
|
|
84
|
-
npm install -g @catheadowl/ukp
|
|
85
|
-
ukp --version
|
|
86
|
-
ukp guide service
|
|
87
|
-
```
|
|
88
|
-
|
|
89
|
-
One-off execution: `npx @catheadowl/ukp --version` (or `pnpm dlx` / `bunx`).
|
|
90
|
-
|
|
91
84
|
Requirements:
|
|
92
85
|
|
|
93
|
-
- Bun `1.3.14` or newer — the CLI runs
|
|
86
|
+
- Bun `1.3.14` or newer — npm installs the `ukp` command, but the CLI runs
|
|
87
|
+
on Bun.
|
|
94
88
|
- QMD on `PATH` only for the QMD-backed capabilities (`search/qmd`,
|
|
95
89
|
`read/qmd`, `update/qmd`). QMD is an external tool maintained as a
|
|
96
90
|
separate project; see its own release channel for installation. UKP
|
|
97
91
|
installs and runs without it — those capabilities report as unavailable
|
|
98
92
|
until `qmd` is available.
|
|
99
93
|
|
|
94
|
+
```bash
|
|
95
|
+
npm install -g @catheadowl/ukp
|
|
96
|
+
ukp --version
|
|
97
|
+
ukp guide service
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
One-off execution: `npx @catheadowl/ukp --version` (or `pnpm dlx` / `bunx`).
|
|
101
|
+
|
|
100
102
|
## First Run
|
|
101
103
|
|
|
102
104
|
For a human or agent starting from a folder:
|
|
@@ -108,11 +110,12 @@ For a human or agent starting from a folder:
|
|
|
108
110
|
4. `ukp register` adds the folder to the Host Registry (`~/.ukp/registry.toml`).
|
|
109
111
|
5. `ukp inspect --endpoint <name>` shows the current binding, manifest, and
|
|
110
112
|
provider availability.
|
|
111
|
-
6. `ukp
|
|
112
|
-
7. `ukp
|
|
113
|
+
6. `ukp nav`, `ukp rg`, and `ukp read` give the provider-free baseline.
|
|
114
|
+
7. `ukp search "<query>" --endpoint <name>` finds indexed matches after
|
|
115
|
+
provider setup.
|
|
113
116
|
|
|
114
|
-
No QMD installed?
|
|
115
|
-
endpoint without it.
|
|
117
|
+
No QMD installed? Stop at step 6 — `nav`, `read`, `rg`, and `propose` work on
|
|
118
|
+
any endpoint without it.
|
|
116
119
|
|
|
117
120
|
An agent can carry out the same flow on your behalf.
|
|
118
121
|
|
|
@@ -143,7 +146,7 @@ Registry commands:
|
|
|
143
146
|
| Command | What it does |
|
|
144
147
|
|---|---|
|
|
145
148
|
| `ukp init service` | Creates a minimal `.ukp/service.toml`. |
|
|
146
|
-
| `ukp register` / `ukp unregister --endpoint <name>` | Manages Host Registry bindings. `ukp register --url <url>` registers a remote endpoint — the name
|
|
149
|
+
| `ukp register` / `ukp unregister --endpoint <name>` | Manages Host Registry bindings. `ukp register --url <url>` registers a remote endpoint under a local handle — the declared name by default, or `--name <handle>` when that name is already taken (two hosts both declaring `notes` become your `notes` and `ali-notes`); `--endpoint <name>` stays an expected-name assertion on the declared name, and `--token <token>` stores the bearer credential in the binding. Identity is pinned TOFU-style (trust on first use), and self-signed certificates are pinned automatically. A **host door** url imports every endpoint behind it — the `--endpoint` assertion narrows the import to exactly that one endpoint, `--select` narrows to a subset, and re-running refreshes idempotently. |
|
|
147
150
|
| `ukp list` | Lists registered endpoints with their declared capabilities. Door drift shows as a stderr note (`door <origin>: N unimported endpoint(s) …`) — importing stays an explicit gesture. |
|
|
148
151
|
|
|
149
152
|
Operations commands:
|
|
@@ -163,6 +166,7 @@ Help commands:
|
|
|
163
166
|
| `ukp guide service` | Provider-agnostic Service setup path. |
|
|
164
167
|
| `ukp guide service qmd` | Provider-owned setup for the QMD provider. |
|
|
165
168
|
| `ukp guide client` | How a workspace uses registered Services by default. |
|
|
169
|
+
| `ukp guide remote` | How to serve and consume endpoints across machines. |
|
|
166
170
|
| `ukp guide propose` | The propose quickstart. |
|
|
167
171
|
|
|
168
172
|
## Remote in 60 seconds
|
|
@@ -170,16 +174,23 @@ Help commands:
|
|
|
170
174
|
`ukp serve` exposes an endpoint over HTTP. Authentication is deny-by-default:
|
|
171
175
|
serving with a `--host` beyond loopback requires `UKP_SERVE_TOKEN` (tokenless
|
|
172
176
|
serving needs an explicit `--allow-anonymous` and is refused off loopback).
|
|
173
|
-
The two fastest paths
|
|
177
|
+
The two fastest paths — each block notes where its commands run:
|
|
174
178
|
|
|
175
179
|
```bash
|
|
176
|
-
# SSH between two personal machines
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
180
|
+
# From the consumer machine — SSH between two personal machines; the door is
|
|
181
|
+
# woken on demand, nothing to start on the host (prerequisites: ssh
|
|
182
|
+
# reachable + ukp on its PATH):
|
|
183
|
+
ukp register --url ssh://<host> # once; TOFU stored
|
|
184
|
+
ukp read --endpoint <name> notes/x.md # just works, like local
|
|
185
|
+
|
|
186
|
+
# Native TLS on a bare IP — self-signed, pinned automatically at registration.
|
|
187
|
+
# The serve line runs ON THE HOST (keep it under your process manager); the
|
|
188
|
+
# register line runs on the consumer machine:
|
|
181
189
|
UKP_SERVE_TOKEN=<token> ukp serve --endpoint <name> --host 0.0.0.0 --port 8570 --tls
|
|
182
|
-
ukp register --url https://<ip>:8570 --token <token>
|
|
190
|
+
ukp register --url https://<ip>:8570 --endpoint <name> --token <token>
|
|
191
|
+
# NAT/EIP cloud host (public IP on no NIC)? Name it explicitly — the persisted
|
|
192
|
+
# cert re-signs over the same key, so the pin (and every registration) survives:
|
|
193
|
+
# ukp serve … --tls --tls-san <public-ip>
|
|
183
194
|
```
|
|
184
195
|
|
|
185
196
|
Remote endpoints take the same commands as local ones — search, read, nav,
|
|
@@ -192,8 +203,7 @@ and the hardening posture — lives in the
|
|
|
192
203
|
|
|
193
204
|
- remote operation of `update`, and a formally specified network protocol
|
|
194
205
|
beyond the current ukp-remote wire v1;
|
|
195
|
-
- semantic search tier,
|
|
196
|
-
deduplication;
|
|
206
|
+
- semantic search tier, query rewrite, reranking, or deduplication;
|
|
197
207
|
- full Client Scope with aliases, visibility, inheritance, or profiles;
|
|
198
208
|
- automatic artifact browsing, cleanup, or "select result N" references;
|
|
199
209
|
- standalone binary distribution.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@catheadowl/ukp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"engines": {
|
|
6
6
|
"bun": ">=1.3.14"
|
|
@@ -30,9 +30,7 @@
|
|
|
30
30
|
"src",
|
|
31
31
|
"README.md",
|
|
32
32
|
"LICENSE",
|
|
33
|
-
"package.json"
|
|
34
|
-
"bun.lock",
|
|
35
|
-
"tsconfig.json"
|
|
33
|
+
"package.json"
|
|
36
34
|
],
|
|
37
35
|
"scripts": {
|
|
38
36
|
"dev": "bun run src/cli.ts",
|
package/src/capabilities/nav.ts
CHANGED
|
@@ -57,7 +57,7 @@ export function resolveNavVisibility(capability: ManifestCapability): NavVisibil
|
|
|
57
57
|
}
|
|
58
58
|
|
|
59
59
|
/** Safety cap on collected route entries; the recursive truncation count
|
|
60
|
-
* still walks everything so `
|
|
60
|
+
* still walks everything so the `(+N .md)` totals stay exact. */
|
|
61
61
|
export const NAV_MAX_ENTRIES = 2000;
|
|
62
62
|
|
|
63
63
|
/** Budget on per-entry description reads (ADR 0018): the expensive
|
|
@@ -614,7 +614,9 @@ export function renderNavHuman(envelope: NavEnvelope): string {
|
|
|
614
614
|
}
|
|
615
615
|
for (const entry of envelope.entries) {
|
|
616
616
|
if (entry.kind === "folder") {
|
|
617
|
-
|
|
617
|
+
// Self-explanatory unexpanded form: an unexpanded folder plus the exact
|
|
618
|
+
// recursive .md count inside (never "truncated" — nothing was cut).
|
|
619
|
+
const line = `${entry.path}/ (+${entry.omittedMarkdownCount} .md)`;
|
|
618
620
|
lines.push(entry.description ? `${line} | ${entry.description}` : line);
|
|
619
621
|
} else if (entry.descriptionOmitted === "budget") {
|
|
620
622
|
lines.push(`${entry.path} | (description omitted: budget reached)`);
|
package/src/capabilities/read.ts
CHANGED
|
@@ -5,7 +5,7 @@ import { loadManifest } from "../config/manifest.ts";
|
|
|
5
5
|
import { isRemoteBinding, localPathOf, readRegistry, type RegistryBinding } from "../registry.ts";
|
|
6
6
|
import { resolveScope } from "../scope.ts";
|
|
7
7
|
import { buildQmdInvocation, defaultQmdCommand, isBareDocidReference, providerTimeoutMs, stripDocidHash, stripQmdHeader, toQmdGetArgument } from "./qmd.ts";
|
|
8
|
-
import { isValidPin, pinFromSourceDocument, recoverRenamedResource, type RecoveryCandidate } from "./rename-recovery.ts";
|
|
8
|
+
import { isValidPin, pinFromSourceDocument, pinHashOf, recoverRenamedResource, type RecoveryCandidate } from "./rename-recovery.ts";
|
|
9
9
|
import { isInsideRealRoot, splitEndpointRelativeSegments } from "../path-safety.ts";
|
|
10
10
|
|
|
11
11
|
export interface ReadRequest {
|
|
@@ -30,6 +30,13 @@ export interface ReadRequest {
|
|
|
30
30
|
* read-rename-recovery): the ukp-pin verification key for the miss-path
|
|
31
31
|
* recovery descent. Absent = stale-unknown verification. */
|
|
32
32
|
pin?: string;
|
|
33
|
+
/** Emission request: compute the ukp-pin for the read resource and return
|
|
34
|
+
* it as `result.pin`. The pin always covers the WHOLE file (LF-normalized)
|
|
35
|
+
* regardless of a `lines` window — computed before windowing in
|
|
36
|
+
* readTargetWithLines. Provider-tier references never carry it: the
|
|
37
|
+
* ukp-pin is a file-slot concept (same boundary as pin verification), and
|
|
38
|
+
* the command layer rejects the combination at parse time. */
|
|
39
|
+
emitPin?: boolean;
|
|
33
40
|
/** The reference exactly as the caller wrote it before tolerant-tier
|
|
34
41
|
* rewriting — used to locate the same-line ukp-pin annotation in the
|
|
35
42
|
* --from source document (Q1 spelling). */
|
|
@@ -108,6 +115,10 @@ export interface ReadFailure {
|
|
|
108
115
|
* purity); echoes, recovery metadata, and warnings are sideband fields. */
|
|
109
116
|
export interface ReadSuccess {
|
|
110
117
|
content: string;
|
|
118
|
+
/** ukp-pin for the read resource (`sha256-<64 hex>`), present only when
|
|
119
|
+
* the request asked for emission (`emitPin`). Whole-file content,
|
|
120
|
+
* LF-normalized — independent of any `lines` window. */
|
|
121
|
+
pin?: string;
|
|
111
122
|
recovery?: ReadRecoveryMeta;
|
|
112
123
|
/** Recovery-descent warnings (factual lines; adapter prefixes each). */
|
|
113
124
|
recoveryWarnings: string[];
|
|
@@ -607,7 +618,13 @@ function attemptRenameRecovery(
|
|
|
607
618
|
function formatRecoveryCandidates(candidates: readonly RecoveryCandidate[] | undefined): string {
|
|
608
619
|
if (!candidates || candidates.length === 0) return "";
|
|
609
620
|
const lines = candidates.map((candidate) => ` - ${candidate.path} (recovery candidate, ${candidate.verified})`);
|
|
610
|
-
|
|
621
|
+
// A mismatched candidate is the pin contract doing its job (path recycling
|
|
622
|
+
// guard) — the reader's next question is "what IS the current pin", so the
|
|
623
|
+
// emission flag answers it in place (no manual hashing, D-088).
|
|
624
|
+
const mismatchNote = candidates.some((candidate) => candidate.verified === "mismatch")
|
|
625
|
+
? `\n (a mismatched ukp-pin means the candidate is not the pinned content; check the current content pin with 'ukp read --show-pin')`
|
|
626
|
+
: "";
|
|
627
|
+
return `Rename recovery candidates:\n${lines.join("\n")}${mismatchNote}\n`;
|
|
611
628
|
}
|
|
612
629
|
|
|
613
630
|
function readTargetWithLines(targetPath: string, request: ReadRequest): ReadOutcome {
|
|
@@ -629,6 +646,9 @@ function readTargetWithLines(targetPath: string, request: ReadRequest): ReadOutc
|
|
|
629
646
|
}
|
|
630
647
|
throw error;
|
|
631
648
|
}
|
|
649
|
+
// Emission hashes the whole file BEFORE any windowing: a pin alongside a
|
|
650
|
+
// `--lines` read must still verify the whole resource.
|
|
651
|
+
const pin = request.emitPin === true ? `sha256-${pinHashOf(content)}` : undefined;
|
|
632
652
|
const rangeResult = applyLineRange(content, request.lines);
|
|
633
653
|
if (rangeResult.kind === "start-beyond-eof") {
|
|
634
654
|
// Word by input origin: a URI #L<line> fragment never mentions --lines.
|
|
@@ -638,7 +658,10 @@ function readTargetWithLines(targetPath: string, request: ReadRequest): ReadOutc
|
|
|
638
658
|
`${origin} ${rangeResult.start} is beyond the end of '${request.path}' (${rangeResult.lineCount} lines)`,
|
|
639
659
|
);
|
|
640
660
|
}
|
|
641
|
-
return {
|
|
661
|
+
return {
|
|
662
|
+
ok: true,
|
|
663
|
+
result: { content: rangeResult.content, ...(pin !== undefined ? { pin } : {}), recoveryWarnings: [] },
|
|
664
|
+
};
|
|
642
665
|
}
|
|
643
666
|
|
|
644
667
|
/** ADR 0021 core entry: runs the read capability and returns the structured
|
|
@@ -949,6 +972,9 @@ function renderReadSideband(result: ReadSuccess): string {
|
|
|
949
972
|
const layerLabel = result.recovery.layer === "search-reanchor" ? "search re-anchor" : "git history";
|
|
950
973
|
parts.push(`ukp read: recovered: '${result.recovery.from}' moved to '${result.recovery.recoveredTo}' (${layerLabel})`);
|
|
951
974
|
}
|
|
975
|
+
if (result.pin !== undefined) {
|
|
976
|
+
parts.push(`ukp read: pin: <!-- ukp-pin: ${result.pin} -->`);
|
|
977
|
+
}
|
|
952
978
|
for (const warning of result.recoveryWarnings) {
|
|
953
979
|
parts.push(`ukp read: warning: ${warning}`);
|
|
954
980
|
}
|
|
@@ -984,6 +1010,9 @@ export interface ReadEnvelope {
|
|
|
984
1010
|
ok: boolean;
|
|
985
1011
|
endpoint?: string;
|
|
986
1012
|
reference: string;
|
|
1013
|
+
/** ukp-pin (`sha256-<64 hex>`) on success, when the request asked for
|
|
1014
|
+
* emission (`--show-pin`). */
|
|
1015
|
+
pin?: string;
|
|
987
1016
|
recovered_to?: string;
|
|
988
1017
|
error?: {
|
|
989
1018
|
class: string;
|
|
@@ -1003,6 +1032,7 @@ export function projectReadEnvelope(request: ReadRequest, outcome: ReadOutcome):
|
|
|
1003
1032
|
ok: true,
|
|
1004
1033
|
...(request.endpoint !== undefined ? { endpoint: request.endpoint } : {}),
|
|
1005
1034
|
reference: request.path,
|
|
1035
|
+
...(outcome.result.pin !== undefined ? { pin: outcome.result.pin } : {}),
|
|
1006
1036
|
...(outcome.result.recovery?.recoveredTo !== undefined
|
|
1007
1037
|
? { recovered_to: outcome.result.recovery.recoveredTo }
|
|
1008
1038
|
: {}),
|