@gmickel/gno 1.24.0 → 1.26.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 +18 -4
- package/assets/skill/SKILL.md +40 -17
- package/assets/skill/recipes/capture-and-file.md +20 -5
- package/browser-extension/artifacts/gno-browser-clipper-v1.26.0.zip +0 -0
- package/browser-extension/artifacts/gno-browser-clipper-v1.26.0.zip.sha256 +1 -0
- package/browser-extension/dist/PRIVACY.md +55 -0
- package/browser-extension/dist/chunk-vn5f663b.js +50 -0
- package/browser-extension/dist/chunk-ydfx5d7p.css +1 -0
- package/browser-extension/dist/content.js +1 -0
- package/browser-extension/dist/manifest.json +25 -0
- package/browser-extension/dist/preview.html +13 -0
- package/browser-extension/dist/service-worker.js +40 -0
- package/package.json +13 -3
- package/spec/cli.md +196 -4
- package/spec/db/schema.sql +102 -1
- package/spec/mcp.md +10 -0
- package/spec/output-schemas/browser-clip-preview.schema.json +83 -0
- package/spec/output-schemas/browser-clip.schema.json +586 -0
- package/spec/output-schemas/capture-receipt.schema.json +22 -1
- package/spec/output-schemas/clipper-csrf.schema.json +12 -0
- package/spec/output-schemas/clipper-error.schema.json +46 -0
- package/spec/output-schemas/clipper-pair-approval.schema.json +17 -0
- package/spec/output-schemas/clipper-pair-start.schema.json +26 -0
- package/spec/output-schemas/clipper-pair-status.schema.json +46 -0
- package/spec/output-schemas/clipper-revoke.schema.json +28 -0
- package/spec/output-schemas/mcp-capture-result.schema.json +12 -1
- package/spec/output-schemas/project-profile-apply.schema.json +209 -0
- package/spec/output-schemas/project-profile-command.schema.json +160 -0
- package/spec/output-schemas/query-diagnose.schema.json +7 -1
- package/spec/output-schemas/setup-profile-result.schema.json +87 -0
- package/spec/project-profile.schema.json +301 -0
- package/src/cli/commands/collection/add.ts +39 -45
- package/src/cli/commands/collection/remove.ts +28 -28
- package/src/cli/commands/collection/rename.ts +55 -73
- package/src/cli/commands/context/add.ts +37 -26
- package/src/cli/commands/context/rm.ts +47 -20
- package/src/cli/commands/init.ts +55 -125
- package/src/cli/commands/models/use.ts +43 -38
- package/src/cli/commands/profile-apply.ts +334 -0
- package/src/cli/commands/profile.ts +409 -0
- package/src/cli/commands/setup-activation.ts +205 -54
- package/src/cli/commands/setup-profile.ts +223 -0
- package/src/cli/commands/setup.ts +3 -0
- package/src/cli/program.ts +110 -9
- package/src/config/index.ts +3 -0
- package/src/config/project-profile.ts +367 -0
- package/src/config/saver.ts +16 -7
- package/src/config/types.ts +42 -0
- package/src/core/browser-clip-provenance.ts +139 -0
- package/src/core/browser-clip.ts +473 -0
- package/src/core/capture-write.ts +5 -0
- package/src/core/capture.ts +75 -18
- package/src/core/config-mutation.ts +138 -76
- package/src/core/config-write-lock.ts +89 -0
- package/src/core/context-identity.ts +16 -0
- package/src/core/context-resolver.ts +2 -12
- package/src/core/file-lock.ts +20 -6
- package/src/core/folder-setup-planning.ts +6 -21
- package/src/core/folder-setup.ts +30 -2
- package/src/core/path-rules.ts +53 -0
- package/src/core/project-affinity-surface.ts +102 -7
- package/src/core/project-profile-apply-state.ts +268 -0
- package/src/core/project-profile-apply-validation.ts +95 -0
- package/src/core/project-profile-apply.ts +408 -0
- package/src/core/project-profile-canonical.ts +71 -0
- package/src/core/project-profile-diff.ts +302 -0
- package/src/core/project-profile-discovery.ts +519 -0
- package/src/core/project-profile-file.ts +37 -0
- package/src/core/project-profile-parser.ts +98 -0
- package/src/core/project-profile.ts +490 -0
- package/src/ingestion/walker.ts +85 -44
- package/src/llm/cache.ts +21 -0
- package/src/serve/capture-service.ts +420 -0
- package/src/serve/clipper-body.ts +62 -0
- package/src/serve/clipper-capture.ts +248 -0
- package/src/serve/clipper-contract.ts +57 -0
- package/src/serve/clipper-idempotency.ts +35 -0
- package/src/serve/clipper-pairing.ts +297 -0
- package/src/serve/clipper-security-errors.ts +23 -0
- package/src/serve/clipper-security.ts +449 -0
- package/src/serve/config-sync.ts +2 -2
- package/src/serve/public/app.tsx +8 -1
- package/src/serve/public/globals.built.css +1 -1
- package/src/serve/public/index.html +1 -0
- package/src/serve/public/lib/clipper-approval.ts +206 -0
- package/src/serve/public/pages/ClipperPairing.tsx +210 -0
- package/src/serve/resident-runtime.ts +1 -0
- package/src/serve/routes/api.ts +20 -115
- package/src/serve/routes/clipper.ts +394 -0
- package/src/serve/server.ts +22 -0
- package/src/store/migrations/020-browser-clipper-security.ts +128 -0
- package/src/store/migrations/021-multi-context-identity.ts +37 -0
- package/src/store/migrations/index.ts +4 -0
- package/src/store/sqlite/adapter.ts +83 -0
- package/src/store/sqlite/clipper-store-types.ts +104 -0
- package/src/store/sqlite/clipper-store.ts +496 -0
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
<meta charset="UTF-8" />
|
|
5
5
|
<base href="/" />
|
|
6
6
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
7
|
+
<meta name="referrer" content="no-referrer" />
|
|
7
8
|
<title>GNO - Local Knowledge Index</title>
|
|
8
9
|
<link rel="stylesheet" href="./globals.built.css" />
|
|
9
10
|
</head>
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
const PAIR_ID = /^[a-f0-9]{64}$/u;
|
|
2
|
+
const PAIRING_CODE = /^\d{8}$/u;
|
|
3
|
+
const TOKEN = /^[a-f0-9]{64}$/u;
|
|
4
|
+
const EXTENSION_ORIGIN = /^chrome-extension:\/\/[a-p]{32}$/u;
|
|
5
|
+
const KNOWN_ERROR_CODES = new Set([
|
|
6
|
+
"CLIPPER_ABORTED",
|
|
7
|
+
"CLIPPER_BODY_TOO_LARGE",
|
|
8
|
+
"CLIPPER_BUSY",
|
|
9
|
+
"CLIPPER_FORBIDDEN",
|
|
10
|
+
"CLIPPER_INVALID_JSON",
|
|
11
|
+
"CLIPPER_RATE_LIMITED",
|
|
12
|
+
"CLIPPER_UNAUTHORIZED",
|
|
13
|
+
"CLIPPER_PAIRING_UNAVAILABLE",
|
|
14
|
+
"CLIPPER_CSRF",
|
|
15
|
+
"CLIPPER_INVALID_REQUEST",
|
|
16
|
+
"CLIPPER_PAIR_NOT_FOUND",
|
|
17
|
+
"CLIPPER_PAIR_EXPIRED",
|
|
18
|
+
"CLIPPER_PAIR_INVALID_CODE",
|
|
19
|
+
"CLIPPER_PAIR_ALREADY_USED",
|
|
20
|
+
"CLIPPER_PREVIEW_MISMATCH",
|
|
21
|
+
"CLIPPER_PREVIEW_REQUIRED",
|
|
22
|
+
"CLIPPER_IDEMPOTENCY_PENDING",
|
|
23
|
+
"CLIPPER_IDEMPOTENCY_CONFLICT",
|
|
24
|
+
"CLIPPER_IDEMPOTENCY_RECOVERY_CONFLICT",
|
|
25
|
+
"CLIPPER_IDEMPOTENCY_GRANT_INACTIVE",
|
|
26
|
+
"CLIPPER_CAPTURE_FAILED",
|
|
27
|
+
"NOT_FOUND",
|
|
28
|
+
"RUNTIME",
|
|
29
|
+
"VALIDATION",
|
|
30
|
+
]);
|
|
31
|
+
|
|
32
|
+
export interface ClipperPairLaunch {
|
|
33
|
+
pairId: string | null;
|
|
34
|
+
valid: boolean;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface ClipperApproval {
|
|
38
|
+
origin: string;
|
|
39
|
+
expiresAt: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export class ClipperApprovalError extends Error {
|
|
43
|
+
readonly code: string;
|
|
44
|
+
readonly retryable: boolean;
|
|
45
|
+
|
|
46
|
+
constructor(code: string, message: string, retryable = false) {
|
|
47
|
+
super(message);
|
|
48
|
+
this.name = "ClipperApprovalError";
|
|
49
|
+
this.code = code;
|
|
50
|
+
this.retryable = retryable;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
|
55
|
+
value !== null && typeof value === "object" && !Array.isArray(value);
|
|
56
|
+
|
|
57
|
+
const hasExactKeys = (
|
|
58
|
+
value: Record<string, unknown>,
|
|
59
|
+
keys: readonly string[]
|
|
60
|
+
): boolean => {
|
|
61
|
+
const actual = Object.keys(value).sort();
|
|
62
|
+
const expected = [...keys].sort();
|
|
63
|
+
return (
|
|
64
|
+
actual.length === expected.length &&
|
|
65
|
+
actual.every((key, index) => key === expected[index])
|
|
66
|
+
);
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const isDateTime = (value: unknown): value is string =>
|
|
70
|
+
typeof value === "string" &&
|
|
71
|
+
Number.isFinite(Date.parse(value)) &&
|
|
72
|
+
value.includes("T");
|
|
73
|
+
|
|
74
|
+
export const consumeClipperPairLaunch = (
|
|
75
|
+
location: Pick<Location, "pathname" | "search" | "hash">,
|
|
76
|
+
history: Pick<History, "replaceState">
|
|
77
|
+
): ClipperPairLaunch => {
|
|
78
|
+
if (location.pathname !== "/clipper/pair") {
|
|
79
|
+
return { pairId: null, valid: false };
|
|
80
|
+
}
|
|
81
|
+
const match =
|
|
82
|
+
location.search === ""
|
|
83
|
+
? /^#pairId=(?<pairId>[a-f0-9]{64})$/u.exec(location.hash)
|
|
84
|
+
: null;
|
|
85
|
+
history.replaceState({}, "", "/clipper/pair");
|
|
86
|
+
const pairId = match?.groups?.pairId ?? null;
|
|
87
|
+
return { pairId, valid: pairId !== null };
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
const parseError = (value: unknown): ClipperApprovalError | null => {
|
|
91
|
+
if (
|
|
92
|
+
!isRecord(value) ||
|
|
93
|
+
!hasExactKeys(value, ["error"]) ||
|
|
94
|
+
!isRecord(value.error) ||
|
|
95
|
+
!hasExactKeys(value.error, ["code", "message"]) ||
|
|
96
|
+
typeof value.error.code !== "string" ||
|
|
97
|
+
!KNOWN_ERROR_CODES.has(value.error.code) ||
|
|
98
|
+
typeof value.error.message !== "string" ||
|
|
99
|
+
value.error.message.length === 0
|
|
100
|
+
) {
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
return new ClipperApprovalError(
|
|
104
|
+
value.error.code,
|
|
105
|
+
value.error.message,
|
|
106
|
+
value.error.code === "CLIPPER_PAIR_INVALID_CODE"
|
|
107
|
+
);
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
const readJson = async (response: Response): Promise<unknown> => {
|
|
111
|
+
if (
|
|
112
|
+
!(response.headers.get("content-type") ?? "").includes("application/json")
|
|
113
|
+
) {
|
|
114
|
+
throw new ClipperApprovalError(
|
|
115
|
+
"CLIPPER_INVALID_RESPONSE",
|
|
116
|
+
"GNO returned a non-JSON pairing response."
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
try {
|
|
120
|
+
return await response.json();
|
|
121
|
+
} catch {
|
|
122
|
+
throw new ClipperApprovalError(
|
|
123
|
+
"CLIPPER_INVALID_RESPONSE",
|
|
124
|
+
"GNO returned malformed pairing data."
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
const requireSuccess = (response: Response, value: unknown): void => {
|
|
130
|
+
if (response.ok) return;
|
|
131
|
+
const parsed = parseError(value);
|
|
132
|
+
if (parsed) throw parsed;
|
|
133
|
+
throw new ClipperApprovalError(
|
|
134
|
+
"CLIPPER_INVALID_RESPONSE",
|
|
135
|
+
"GNO returned an unknown pairing error."
|
|
136
|
+
);
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
export async function approveClipperPair(
|
|
140
|
+
pairId: string,
|
|
141
|
+
pairingCode: string,
|
|
142
|
+
fetcher: typeof fetch = fetch
|
|
143
|
+
): Promise<ClipperApproval> {
|
|
144
|
+
if (!PAIR_ID.test(pairId) || !PAIRING_CODE.test(pairingCode)) {
|
|
145
|
+
throw new ClipperApprovalError(
|
|
146
|
+
"CLIPPER_INVALID_REQUEST",
|
|
147
|
+
"Enter the exact eight-digit code shown by the extension."
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const csrfResponse = await fetcher("/api/clipper/pair/csrf", {
|
|
152
|
+
method: "GET",
|
|
153
|
+
cache: "no-store",
|
|
154
|
+
credentials: "same-origin",
|
|
155
|
+
});
|
|
156
|
+
const csrfBody = await readJson(csrfResponse);
|
|
157
|
+
requireSuccess(csrfResponse, csrfBody);
|
|
158
|
+
if (
|
|
159
|
+
!isRecord(csrfBody) ||
|
|
160
|
+
!hasExactKeys(csrfBody, ["schemaVersion", "csrfToken"]) ||
|
|
161
|
+
csrfBody.schemaVersion !== "1.0" ||
|
|
162
|
+
typeof csrfBody.csrfToken !== "string" ||
|
|
163
|
+
!TOKEN.test(csrfBody.csrfToken)
|
|
164
|
+
) {
|
|
165
|
+
throw new ClipperApprovalError(
|
|
166
|
+
"CLIPPER_INVALID_RESPONSE",
|
|
167
|
+
"GNO returned an unsupported pairing response."
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const approvalResponse = await fetcher("/api/clipper/pair/approve", {
|
|
172
|
+
method: "POST",
|
|
173
|
+
cache: "no-store",
|
|
174
|
+
credentials: "same-origin",
|
|
175
|
+
headers: {
|
|
176
|
+
"Content-Type": "application/json",
|
|
177
|
+
"X-GNO-CSRF": csrfBody.csrfToken,
|
|
178
|
+
},
|
|
179
|
+
body: JSON.stringify({ pairId, pairingCode }),
|
|
180
|
+
});
|
|
181
|
+
const approvalBody = await readJson(approvalResponse);
|
|
182
|
+
requireSuccess(approvalResponse, approvalBody);
|
|
183
|
+
if (
|
|
184
|
+
!isRecord(approvalBody) ||
|
|
185
|
+
!hasExactKeys(approvalBody, [
|
|
186
|
+
"schemaVersion",
|
|
187
|
+
"status",
|
|
188
|
+
"origin",
|
|
189
|
+
"expiresAt",
|
|
190
|
+
]) ||
|
|
191
|
+
approvalBody.schemaVersion !== "1.0" ||
|
|
192
|
+
approvalBody.status !== "approved" ||
|
|
193
|
+
typeof approvalBody.origin !== "string" ||
|
|
194
|
+
!EXTENSION_ORIGIN.test(approvalBody.origin) ||
|
|
195
|
+
!isDateTime(approvalBody.expiresAt)
|
|
196
|
+
) {
|
|
197
|
+
throw new ClipperApprovalError(
|
|
198
|
+
"CLIPPER_INVALID_RESPONSE",
|
|
199
|
+
"GNO returned an unsupported pairing response."
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
return {
|
|
203
|
+
origin: approvalBody.origin,
|
|
204
|
+
expiresAt: approvalBody.expiresAt,
|
|
205
|
+
};
|
|
206
|
+
}
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AlertCircleIcon,
|
|
3
|
+
CheckCircle2Icon,
|
|
4
|
+
LinkIcon,
|
|
5
|
+
Loader2Icon,
|
|
6
|
+
} from "lucide-react";
|
|
7
|
+
import { useState } from "react";
|
|
8
|
+
|
|
9
|
+
import { GnoLogo } from "../components/GnoLogo";
|
|
10
|
+
import { Button } from "../components/ui/button";
|
|
11
|
+
import {
|
|
12
|
+
Card,
|
|
13
|
+
CardContent,
|
|
14
|
+
CardDescription,
|
|
15
|
+
CardFooter,
|
|
16
|
+
CardHeader,
|
|
17
|
+
CardTitle,
|
|
18
|
+
} from "../components/ui/card";
|
|
19
|
+
import { Input } from "../components/ui/input";
|
|
20
|
+
import {
|
|
21
|
+
approveClipperPair,
|
|
22
|
+
ClipperApprovalError,
|
|
23
|
+
type ClipperApproval,
|
|
24
|
+
} from "../lib/clipper-approval";
|
|
25
|
+
|
|
26
|
+
interface ClipperPairingProps {
|
|
27
|
+
pairId: string | null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
type PairingState = "ready" | "approving" | "approved" | "terminal";
|
|
31
|
+
|
|
32
|
+
const terminalCodes = new Set([
|
|
33
|
+
"CLIPPER_PAIR_EXPIRED",
|
|
34
|
+
"CLIPPER_PAIR_ALREADY_USED",
|
|
35
|
+
"CLIPPER_PAIR_NOT_FOUND",
|
|
36
|
+
]);
|
|
37
|
+
|
|
38
|
+
export default function ClipperPairing({ pairId }: ClipperPairingProps) {
|
|
39
|
+
const [activePairId, setActivePairId] = useState(pairId);
|
|
40
|
+
const [pairingCode, setPairingCode] = useState("");
|
|
41
|
+
const [state, setState] = useState<PairingState>(
|
|
42
|
+
pairId === null ? "terminal" : "ready"
|
|
43
|
+
);
|
|
44
|
+
const [approval, setApproval] = useState<ClipperApproval | null>(null);
|
|
45
|
+
const [error, setError] = useState<string | null>(
|
|
46
|
+
pairId === null
|
|
47
|
+
? "This pairing link is invalid. Start pairing again from the extension."
|
|
48
|
+
: null
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
const clearSensitiveState = () => {
|
|
52
|
+
setPairingCode("");
|
|
53
|
+
setActivePairId(null);
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const approve = async () => {
|
|
57
|
+
if (activePairId === null || !/^\d{8}$/u.test(pairingCode)) return;
|
|
58
|
+
setState("approving");
|
|
59
|
+
setError(null);
|
|
60
|
+
try {
|
|
61
|
+
const result = await approveClipperPair(activePairId, pairingCode);
|
|
62
|
+
clearSensitiveState();
|
|
63
|
+
setApproval(result);
|
|
64
|
+
setState("approved");
|
|
65
|
+
} catch (cause) {
|
|
66
|
+
const parsed =
|
|
67
|
+
cause instanceof ClipperApprovalError
|
|
68
|
+
? cause
|
|
69
|
+
: new ClipperApprovalError(
|
|
70
|
+
"CLIPPER_NETWORK",
|
|
71
|
+
"Could not reach the local GNO gateway. Keep the extension open and retry.",
|
|
72
|
+
true
|
|
73
|
+
);
|
|
74
|
+
setError(parsed.message);
|
|
75
|
+
if (terminalCodes.has(parsed.code) || !parsed.retryable) {
|
|
76
|
+
clearSensitiveState();
|
|
77
|
+
setState("terminal");
|
|
78
|
+
} else {
|
|
79
|
+
setPairingCode("");
|
|
80
|
+
setState("ready");
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const cancel = () => {
|
|
86
|
+
clearSensitiveState();
|
|
87
|
+
setError("Pairing cancelled. Start again from the extension when ready.");
|
|
88
|
+
setState("terminal");
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
return (
|
|
92
|
+
<main className="relative flex min-h-screen items-center justify-center overflow-hidden bg-background p-6">
|
|
93
|
+
<div className="pointer-events-none absolute inset-0 bg-[radial-gradient(circle_at_top_left,oklch(0.68_0.11_55/0.12),transparent_42%),radial-gradient(circle_at_bottom_right,oklch(0.52_0.08_245/0.10),transparent_48%)]" />
|
|
94
|
+
<Card className="relative w-full max-w-lg border-border/70 shadow-2xl">
|
|
95
|
+
<CardHeader>
|
|
96
|
+
<div className="mb-3 flex items-center gap-3 text-primary">
|
|
97
|
+
<GnoLogo className="size-9" />
|
|
98
|
+
<span className="font-mono text-xs tracking-[0.2em] uppercase">
|
|
99
|
+
Local approval
|
|
100
|
+
</span>
|
|
101
|
+
</div>
|
|
102
|
+
<CardTitle className="font-semibold text-2xl">
|
|
103
|
+
Pair the GNO browser clipper
|
|
104
|
+
</CardTitle>
|
|
105
|
+
<CardDescription className="leading-6">
|
|
106
|
+
This extension may send only content you explicitly capture into
|
|
107
|
+
this local GNO. Access expires within 30 days and can be revoked
|
|
108
|
+
from the extension.
|
|
109
|
+
</CardDescription>
|
|
110
|
+
</CardHeader>
|
|
111
|
+
<CardContent className="space-y-5">
|
|
112
|
+
{state === "ready" || state === "approving" ? (
|
|
113
|
+
<>
|
|
114
|
+
<div className="rounded-lg border border-border/60 bg-muted/25 p-4 text-sm">
|
|
115
|
+
<div className="mb-2 flex items-center gap-2 font-medium">
|
|
116
|
+
<LinkIcon className="size-4 text-primary" />
|
|
117
|
+
Confirm this browser
|
|
118
|
+
</div>
|
|
119
|
+
<p className="text-muted-foreground">
|
|
120
|
+
Enter the eight-digit code shown in the extension. GNO never
|
|
121
|
+
gives this page the extension grant.
|
|
122
|
+
</p>
|
|
123
|
+
</div>
|
|
124
|
+
<label className="block space-y-2">
|
|
125
|
+
<span className="font-medium text-sm">Pairing code</span>
|
|
126
|
+
<Input
|
|
127
|
+
aria-label="Pairing code"
|
|
128
|
+
autoComplete="off"
|
|
129
|
+
autoFocus
|
|
130
|
+
className="h-12 font-mono text-lg tracking-[0.3em]"
|
|
131
|
+
disabled={state === "approving"}
|
|
132
|
+
inputMode="numeric"
|
|
133
|
+
maxLength={8}
|
|
134
|
+
onChange={(event) =>
|
|
135
|
+
setPairingCode(
|
|
136
|
+
event.currentTarget.value
|
|
137
|
+
.replaceAll(/\D/gu, "")
|
|
138
|
+
.slice(0, 8)
|
|
139
|
+
)
|
|
140
|
+
}
|
|
141
|
+
pattern="[0-9]{8}"
|
|
142
|
+
placeholder="00000000"
|
|
143
|
+
value={pairingCode}
|
|
144
|
+
/>
|
|
145
|
+
</label>
|
|
146
|
+
</>
|
|
147
|
+
) : null}
|
|
148
|
+
|
|
149
|
+
{approval ? (
|
|
150
|
+
<div
|
|
151
|
+
className="rounded-lg border border-emerald-500/30 bg-emerald-500/10 p-4"
|
|
152
|
+
role="status"
|
|
153
|
+
>
|
|
154
|
+
<div className="mb-2 flex items-center gap-2 font-medium text-emerald-700 dark:text-emerald-300">
|
|
155
|
+
<CheckCircle2Icon className="size-5" />
|
|
156
|
+
Browser paired
|
|
157
|
+
</div>
|
|
158
|
+
<p className="break-all text-muted-foreground text-sm">
|
|
159
|
+
{approval.origin}
|
|
160
|
+
</p>
|
|
161
|
+
<p className="mt-1 text-muted-foreground text-xs">
|
|
162
|
+
Expires {new Date(approval.expiresAt).toLocaleString()}
|
|
163
|
+
</p>
|
|
164
|
+
</div>
|
|
165
|
+
) : null}
|
|
166
|
+
|
|
167
|
+
{error ? (
|
|
168
|
+
<div
|
|
169
|
+
className="flex gap-3 rounded-lg border border-destructive/30 bg-destructive/10 p-4 text-sm"
|
|
170
|
+
role="alert"
|
|
171
|
+
>
|
|
172
|
+
<AlertCircleIcon className="mt-0.5 size-4 shrink-0 text-destructive" />
|
|
173
|
+
<span>{error}</span>
|
|
174
|
+
</div>
|
|
175
|
+
) : null}
|
|
176
|
+
</CardContent>
|
|
177
|
+
<CardFooter className="justify-between gap-3">
|
|
178
|
+
{state === "ready" || state === "approving" ? (
|
|
179
|
+
<>
|
|
180
|
+
<Button
|
|
181
|
+
disabled={state === "approving"}
|
|
182
|
+
onClick={cancel}
|
|
183
|
+
type="button"
|
|
184
|
+
variant="ghost"
|
|
185
|
+
>
|
|
186
|
+
Cancel
|
|
187
|
+
</Button>
|
|
188
|
+
<Button
|
|
189
|
+
disabled={
|
|
190
|
+
state === "approving" || !/^\d{8}$/u.test(pairingCode)
|
|
191
|
+
}
|
|
192
|
+
onClick={() => void approve()}
|
|
193
|
+
type="button"
|
|
194
|
+
>
|
|
195
|
+
{state === "approving" ? (
|
|
196
|
+
<Loader2Icon className="animate-spin" />
|
|
197
|
+
) : null}
|
|
198
|
+
Approve extension
|
|
199
|
+
</Button>
|
|
200
|
+
</>
|
|
201
|
+
) : (
|
|
202
|
+
<Button asChild className="ml-auto" variant="outline">
|
|
203
|
+
<a href="/">Back to GNO</a>
|
|
204
|
+
</Button>
|
|
205
|
+
)}
|
|
206
|
+
</CardFooter>
|
|
207
|
+
</Card>
|
|
208
|
+
</main>
|
|
209
|
+
);
|
|
210
|
+
}
|
package/src/serve/routes/api.ts
CHANGED
|
@@ -6,9 +6,9 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
// node:fs/promises structure ops have no Bun equivalent
|
|
9
|
-
import {
|
|
9
|
+
import { readdir } from "node:fs/promises";
|
|
10
10
|
// node:path has no Bun equivalent
|
|
11
|
-
import {
|
|
11
|
+
import { posix as pathPosix } from "node:path";
|
|
12
12
|
|
|
13
13
|
import type {
|
|
14
14
|
Collection,
|
|
@@ -48,14 +48,7 @@ import {
|
|
|
48
48
|
fingerprintContentTypeRules,
|
|
49
49
|
normalizeContentTypes,
|
|
50
50
|
} from "../../config";
|
|
51
|
-
import {
|
|
52
|
-
buildCaptureReceipt,
|
|
53
|
-
listCaptureDiskRelPaths,
|
|
54
|
-
planCapture,
|
|
55
|
-
type CapturePlan,
|
|
56
|
-
type PublicCaptureInput,
|
|
57
|
-
} from "../../core/capture";
|
|
58
|
-
import { writeCapturePlanFile } from "../../core/capture-write";
|
|
51
|
+
import { type PublicCaptureInput } from "../../core/capture";
|
|
59
52
|
import {
|
|
60
53
|
type ConnectorVerificationCode,
|
|
61
54
|
getConnectorVerificationRemediation,
|
|
@@ -146,6 +139,10 @@ import {
|
|
|
146
139
|
} from "../../publish/artifact";
|
|
147
140
|
import { exportPublishArtifact } from "../../publish/export-service";
|
|
148
141
|
import { buildBrowseTree, normalizeBrowsePath } from "../browse-tree";
|
|
142
|
+
import {
|
|
143
|
+
executeResidentCapturePlan,
|
|
144
|
+
planResidentCapture,
|
|
145
|
+
} from "../capture-service";
|
|
149
146
|
import { applyConfigChange, applyConfigChangeTyped } from "../config-sync";
|
|
150
147
|
import {
|
|
151
148
|
getConnectorStatuses,
|
|
@@ -171,6 +168,7 @@ import { handleChanges, handleDiff, handleImpact } from "./changes";
|
|
|
171
168
|
export interface ContextHolder {
|
|
172
169
|
current: ServerContext;
|
|
173
170
|
config: Config;
|
|
171
|
+
actualConfigPath?: string;
|
|
174
172
|
scheduler: EmbedScheduler | null;
|
|
175
173
|
eventBus: DocumentEventBus | null;
|
|
176
174
|
watchService: CollectionWatchService | null;
|
|
@@ -3167,114 +3165,21 @@ export async function handleCreateCapture(
|
|
|
3167
3165
|
);
|
|
3168
3166
|
}
|
|
3169
3167
|
|
|
3170
|
-
const
|
|
3171
|
-
|
|
3172
|
-
|
|
3173
|
-
);
|
|
3174
|
-
if (!
|
|
3175
|
-
return errorResponse(
|
|
3176
|
-
"NOT_FOUND",
|
|
3177
|
-
`Collection not found: ${body.collection}`,
|
|
3178
|
-
404
|
|
3179
|
-
);
|
|
3180
|
-
}
|
|
3181
|
-
|
|
3182
|
-
let plan: CapturePlan;
|
|
3183
|
-
try {
|
|
3184
|
-
plan = planCapture({
|
|
3185
|
-
input: {
|
|
3186
|
-
...body,
|
|
3187
|
-
collection: collection.name,
|
|
3188
|
-
},
|
|
3189
|
-
existingRelPaths: await listCollectionRelPaths(store, collection.name),
|
|
3190
|
-
diskRelPaths: await listCaptureDiskRelPaths(collection.path),
|
|
3191
|
-
});
|
|
3192
|
-
} catch (error) {
|
|
3193
|
-
return errorResponse(
|
|
3194
|
-
"VALIDATION",
|
|
3195
|
-
error instanceof Error ? error.message : String(error),
|
|
3196
|
-
409
|
|
3197
|
-
);
|
|
3198
|
-
}
|
|
3199
|
-
|
|
3200
|
-
const fullPath = pathJoin(collection.path, plan.relPath);
|
|
3201
|
-
if (plan.openedExisting) {
|
|
3202
|
-
const existingDoc = await store.getDocument(collection.name, plan.relPath);
|
|
3203
|
-
if (!existingDoc.ok) {
|
|
3204
|
-
return errorResponse("RUNTIME", existingDoc.error.message, 500);
|
|
3205
|
-
}
|
|
3206
|
-
return jsonResponse(
|
|
3207
|
-
buildCaptureReceipt({
|
|
3208
|
-
plan,
|
|
3209
|
-
absPath: fullPath,
|
|
3210
|
-
docid: existingDoc.value?.docid,
|
|
3211
|
-
sync: existingDoc.value
|
|
3212
|
-
? { status: "completed" }
|
|
3213
|
-
: {
|
|
3214
|
-
status: "skipped",
|
|
3215
|
-
reason: "Existing file is not indexed yet.",
|
|
3216
|
-
},
|
|
3217
|
-
})
|
|
3218
|
-
);
|
|
3168
|
+
const planned = await planResidentCapture(ctxHolder, store, {
|
|
3169
|
+
...body,
|
|
3170
|
+
collection: body.collection,
|
|
3171
|
+
});
|
|
3172
|
+
if (!planned.ok) {
|
|
3173
|
+
return errorResponse(planned.code, planned.message, planned.status);
|
|
3219
3174
|
}
|
|
3220
|
-
|
|
3221
3175
|
try {
|
|
3222
|
-
|
|
3223
|
-
|
|
3224
|
-
|
|
3225
|
-
|
|
3226
|
-
|
|
3227
|
-
const jobResult = await startJob(
|
|
3228
|
-
"sync",
|
|
3229
|
-
async (): Promise<SyncResult> => {
|
|
3230
|
-
const result = await syncResidentCollection(
|
|
3231
|
-
ctxHolder,
|
|
3232
|
-
collection,
|
|
3233
|
-
store,
|
|
3234
|
-
withContentTypeRules({ runUpdateCmd: false }, ctxHolder.config),
|
|
3235
|
-
deps?.syncCollection
|
|
3236
|
-
);
|
|
3237
|
-
ctxHolder.scheduler?.notifySyncComplete([plan.relPath]);
|
|
3238
|
-
ctxHolder.eventBus?.emit({
|
|
3239
|
-
type: "document-changed",
|
|
3240
|
-
uri: gnoUri,
|
|
3241
|
-
collection: collection.name,
|
|
3242
|
-
relPath: plan.relPath,
|
|
3243
|
-
origin: "create",
|
|
3244
|
-
changedAt: new Date().toISOString(),
|
|
3245
|
-
});
|
|
3246
|
-
return {
|
|
3247
|
-
collections: [result],
|
|
3248
|
-
totalDurationMs: result.durationMs,
|
|
3249
|
-
totalFilesProcessed: result.filesProcessed,
|
|
3250
|
-
totalFilesAdded: result.filesAdded,
|
|
3251
|
-
totalFilesUpdated: result.filesUpdated,
|
|
3252
|
-
totalFilesErrored: result.filesErrored,
|
|
3253
|
-
totalFilesSkipped: result.filesSkipped,
|
|
3254
|
-
};
|
|
3255
|
-
},
|
|
3256
|
-
ctxHolder.jobManager
|
|
3257
|
-
);
|
|
3258
|
-
|
|
3259
|
-
return jsonResponse(
|
|
3260
|
-
buildCaptureReceipt({
|
|
3261
|
-
plan,
|
|
3262
|
-
absPath: fullPath,
|
|
3263
|
-
sync: jobResult.ok
|
|
3264
|
-
? {
|
|
3265
|
-
status: "pending",
|
|
3266
|
-
jobId: jobResult.jobId,
|
|
3267
|
-
reason: "Sync job started; poll /api/jobs/:id for status.",
|
|
3268
|
-
}
|
|
3269
|
-
: {
|
|
3270
|
-
status: "skipped",
|
|
3271
|
-
jobId: jobResult.activeJobId,
|
|
3272
|
-
reason: "Sync skipped because another job is running.",
|
|
3273
|
-
error: jobResult.error,
|
|
3274
|
-
},
|
|
3275
|
-
}),
|
|
3276
|
-
202
|
|
3176
|
+
const result = await executeResidentCapturePlan(
|
|
3177
|
+
ctxHolder,
|
|
3178
|
+
store,
|
|
3179
|
+
planned,
|
|
3180
|
+
deps
|
|
3277
3181
|
);
|
|
3182
|
+
return jsonResponse(result.body, result.status);
|
|
3278
3183
|
} catch (error) {
|
|
3279
3184
|
return errorResponse(
|
|
3280
3185
|
"RUNTIME",
|