@openparachute/hub 0.5.13 → 0.5.14-rc.1
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/package.json +2 -2
- package/src/__tests__/account-home-ui.test.ts +140 -0
- package/src/__tests__/admin-handlers.test.ts +74 -0
- package/src/__tests__/admin-host-admin-token.test.ts +62 -0
- package/src/__tests__/admin-vault-admin-token.test.ts +44 -0
- package/src/__tests__/api-account.test.ts +191 -1
- package/src/__tests__/api-modules.test.ts +32 -32
- package/src/__tests__/api-users.test.ts +192 -2
- package/src/__tests__/chrome-strip.test.ts +15 -15
- package/src/__tests__/hub-server.test.ts +23 -23
- package/src/__tests__/notes-redirect.test.ts +20 -20
- package/src/__tests__/services-manifest.test.ts +40 -40
- package/src/__tests__/setup-wizard.test.ts +157 -19
- package/src/__tests__/setup.test.ts +1 -1
- package/src/__tests__/status.test.ts +39 -0
- package/src/__tests__/users.test.ts +261 -0
- package/src/__tests__/well-known.test.ts +9 -9
- package/src/account-home-ui.ts +404 -0
- package/src/admin-handlers.ts +49 -17
- package/src/admin-host-admin-token.ts +25 -0
- package/src/admin-vault-admin-token.ts +17 -0
- package/src/api-account.ts +72 -6
- package/src/api-modules.ts +3 -3
- package/src/api-users.ts +173 -12
- package/src/chrome-strip.ts +6 -6
- package/src/commands/status.ts +10 -1
- package/src/help.ts +2 -2
- package/src/hub-server.ts +50 -10
- package/src/hub-settings.ts +2 -2
- package/src/hub.ts +6 -6
- package/src/notes-redirect.ts +5 -5
- package/src/service-spec.ts +39 -18
- package/src/setup-wizard.ts +335 -28
- package/src/users.ts +112 -0
- package/web/ui/dist/assets/index-Qf56GsGm.js +61 -0
- package/web/ui/dist/index.html +1 -1
- package/web/ui/dist/assets/index-Dzrbe6EP.js +0 -61
package/src/service-spec.ts
CHANGED
|
@@ -68,7 +68,7 @@ export const PORT_RESERVATIONS: readonly PortReservation[] = [
|
|
|
68
68
|
// fallback-port walker (`assignPort` in port-assign.ts) from handing this
|
|
69
69
|
// port out to a colliding third-party module. The matching KNOWN_MODULES
|
|
70
70
|
// row carries the canonicalPort + paths for status/expose surfaces.
|
|
71
|
-
{ port: 1946, name: "parachute-
|
|
71
|
+
{ port: 1946, name: "parachute-surface", status: "assigned" },
|
|
72
72
|
{ port: 1947, name: "unassigned", status: "reserved" },
|
|
73
73
|
{ port: 1948, name: "unassigned", status: "reserved" },
|
|
74
74
|
{ port: 1949, name: "unassigned", status: "reserved" },
|
|
@@ -281,7 +281,7 @@ const NOTES_FALLBACK: FirstPartyFallback = {
|
|
|
281
281
|
name: "notes",
|
|
282
282
|
manifestName: "parachute-notes",
|
|
283
283
|
displayName: "Notes",
|
|
284
|
-
tagline: "Notes PWA — daemon deprecated 2026-05-22; install `
|
|
284
|
+
tagline: "Notes PWA — daemon deprecated 2026-05-22; install `surface` for the current path.",
|
|
285
285
|
port: 1942,
|
|
286
286
|
paths: ["/notes"],
|
|
287
287
|
health: "/notes/health",
|
|
@@ -462,28 +462,29 @@ export const KNOWN_MODULES: Record<string, KnownModule> = {
|
|
|
462
462
|
hasAuth: true,
|
|
463
463
|
},
|
|
464
464
|
},
|
|
465
|
-
|
|
466
|
-
short: "
|
|
467
|
-
package: "@openparachute/
|
|
468
|
-
manifestName: "parachute-
|
|
465
|
+
surface: {
|
|
466
|
+
short: "surface",
|
|
467
|
+
package: "@openparachute/surface",
|
|
468
|
+
manifestName: "parachute-surface",
|
|
469
469
|
canonicalPort: 1946,
|
|
470
|
-
displayName: "
|
|
470
|
+
displayName: "Surface",
|
|
471
471
|
// Tagline telegraphs the auto-bootstrap so wizard + admin-SPA copy explain
|
|
472
|
-
// the architecture: installing `
|
|
473
|
-
// via the Phase 2.1 bootstrap-default-apps step. The notes-daemon
|
|
474
|
-
// still exists as a back-compat install (CURATED_MODULES still
|
|
475
|
-
// `notes`) but `
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
472
|
+
// the architecture: installing `surface` brings Notes (and other UIs)
|
|
473
|
+
// along via the Phase 2.1 bootstrap-default-apps step. The notes-daemon
|
|
474
|
+
// path still exists as a back-compat install (CURATED_MODULES still
|
|
475
|
+
// lists `notes`) but `surface` is the recommended first install
|
|
476
|
+
// post-vault. Renamed from `app` 2026-05-27 per patterns#102.
|
|
477
|
+
tagline: "Host module for Parachute surfaces — auto-installs Notes on first boot.",
|
|
478
|
+
canonicalPaths: ["/surface", "/.parachute"],
|
|
479
|
+
canonicalHealth: "/surface/healthz",
|
|
479
480
|
canonicalStripPrefix: false,
|
|
480
481
|
extras: {
|
|
481
482
|
// Backward-compat startCmd — same rationale as scribe / vault / runner
|
|
482
483
|
// above. Post-self-register, lifecycle reads module.json's startCmd via
|
|
483
484
|
// `composeKnownModuleSpec` and that path wins.
|
|
484
|
-
startCmd: () => ["parachute-
|
|
485
|
-
//
|
|
486
|
-
// doc §6 same-hub auto-trust + scope `
|
|
485
|
+
startCmd: () => ["parachute-surface", "serve"],
|
|
486
|
+
// Surface's admin + per-UI surfaces gate behind hub-issued JWTs (design
|
|
487
|
+
// doc §6 same-hub auto-trust + scope `surface:admin`). Surfaces in
|
|
487
488
|
// `parachute status` as auth-required by default, same posture as vault
|
|
488
489
|
// + runner.
|
|
489
490
|
hasAuth: true,
|
|
@@ -516,7 +517,27 @@ export const KNOWN_MODULES: Record<string, KnownModule> = {
|
|
|
516
517
|
export const RETIRED_MODULES: Record<string, { retiredAt: string; replacement?: string }> = {
|
|
517
518
|
agent: {
|
|
518
519
|
retiredAt: "2026-05-20",
|
|
519
|
-
replacement: "parachute-
|
|
520
|
+
replacement: "parachute-surface or parachute-runner (depending on use case)",
|
|
521
|
+
},
|
|
522
|
+
// 2026-05-20 retirement caught both forms of legacy rows.
|
|
523
|
+
"parachute-agent": {
|
|
524
|
+
retiredAt: "2026-05-20",
|
|
525
|
+
replacement: "parachute-surface or parachute-runner (depending on use case)",
|
|
526
|
+
},
|
|
527
|
+
// The `parachute-app` row name retires 2026-05-27 along with the
|
|
528
|
+
// app → surface rename (patterns#102). Operators upgrading from
|
|
529
|
+
// 0.5.13-stable will have a `parachute-app` row in services.json
|
|
530
|
+
// pointing at the now-removed @openparachute/app package; this entry
|
|
531
|
+
// drops it on load + steers them at `parachute install surface`.
|
|
532
|
+
// The short-name `app` form is included for legacy rows that used
|
|
533
|
+
// the short name as the `name` field.
|
|
534
|
+
app: {
|
|
535
|
+
retiredAt: "2026-05-27",
|
|
536
|
+
replacement: "parachute-surface (renamed from parachute-app — `parachute install surface`)",
|
|
537
|
+
},
|
|
538
|
+
"parachute-app": {
|
|
539
|
+
retiredAt: "2026-05-27",
|
|
540
|
+
replacement: "parachute-surface (renamed from parachute-app — `parachute install surface`)",
|
|
520
541
|
},
|
|
521
542
|
};
|
|
522
543
|
|
package/src/setup-wizard.ts
CHANGED
|
@@ -38,6 +38,8 @@
|
|
|
38
38
|
*/
|
|
39
39
|
|
|
40
40
|
import type { Database } from "bun:sqlite";
|
|
41
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
42
|
+
import { join } from "node:path";
|
|
41
43
|
import { type OperationsRegistry, runInstall, specFor } from "./api-modules-ops.ts";
|
|
42
44
|
import { CURATED_MODULES, type CuratedModuleShort } from "./api-modules.ts";
|
|
43
45
|
import { brandMarkSvg, WORDMARK_TEXT } from "./brand.ts";
|
|
@@ -449,6 +451,14 @@ export interface RenderVaultStepProps {
|
|
|
449
451
|
errorMessage?: string;
|
|
450
452
|
/** Pre-fill the vault name input after a validation failure. */
|
|
451
453
|
vaultName?: string;
|
|
454
|
+
/**
|
|
455
|
+
* When the runtime is a hosted container (Render / Fly), the scribe
|
|
456
|
+
* sub-form hides the "local provider" option — Whisper / parakeet
|
|
457
|
+
* don't run usefully in the constrained container. Defaults to false
|
|
458
|
+
* (treat as self-host, show local option) — production wizard renders
|
|
459
|
+
* always pass an explicit value via detectAutoExposeMode.
|
|
460
|
+
*/
|
|
461
|
+
cloudHost?: boolean;
|
|
452
462
|
/**
|
|
453
463
|
* When an install op is in progress, render the polling shape: no
|
|
454
464
|
* form, just the op log + auto-refresh.
|
|
@@ -458,11 +468,18 @@ export interface RenderVaultStepProps {
|
|
|
458
468
|
status: "pending" | "running" | "succeeded" | "failed";
|
|
459
469
|
log: readonly string[];
|
|
460
470
|
error?: string;
|
|
471
|
+
/**
|
|
472
|
+
* Optional scribe install op_id, threaded through so the success
|
|
473
|
+
* redirect carries `&op_scribe=<id>` and the done step picks up the
|
|
474
|
+
* in-flight scribe install via the existing per-tile op-poll
|
|
475
|
+
* mechanism (`buildInstallTiles` reads `op_<short>` query param).
|
|
476
|
+
*/
|
|
477
|
+
scribeOpId?: string;
|
|
461
478
|
};
|
|
462
479
|
}
|
|
463
480
|
|
|
464
481
|
export function renderVaultStep(props: RenderVaultStepProps): string {
|
|
465
|
-
const { csrfToken, errorMessage, operation, vaultName } = props;
|
|
482
|
+
const { csrfToken, errorMessage, operation, vaultName, cloudHost } = props;
|
|
466
483
|
if (operation) return renderVaultOpStep({ operation });
|
|
467
484
|
const error = errorMessage ? `<p class="error-banner">${escapeHtml(errorMessage)}</p>` : "";
|
|
468
485
|
// hub#267: the typed name now flows end-to-end via
|
|
@@ -523,12 +540,98 @@ export function renderVaultStep(props: RenderVaultStepProps): string {
|
|
|
523
540
|
<span class="field-hint">lowercase letters, digits, <code>-</code>, <code>_</code>;
|
|
524
541
|
2–32 chars. Leave blank for <code>${DEFAULT_VAULT_NAME}</code>.</span>
|
|
525
542
|
</label>
|
|
543
|
+
${renderScribeSubForm(cloudHost === true)}
|
|
526
544
|
<button type="submit" class="btn btn-primary">Create vault & finish</button>
|
|
527
545
|
</form>
|
|
528
546
|
</div>`;
|
|
529
547
|
return baseDocument("Set up your Parachute hub — vault", body);
|
|
530
548
|
}
|
|
531
549
|
|
|
550
|
+
/**
|
|
551
|
+
* Scribe install sub-form embedded in the vault step (folded in
|
|
552
|
+
* 2026-05-27 per Aaron's team-meeting directive: "folding the scribe
|
|
553
|
+
* question into the vault step is a good idea"). Operator answers
|
|
554
|
+
* scribe-related questions in the same form as vault name, the POST
|
|
555
|
+
* handler kicks both installs in parallel, and the done screen polls
|
|
556
|
+
* scribe's progress via the existing per-tile op-poll mechanism.
|
|
557
|
+
*
|
|
558
|
+
* The provider list adapts to the runtime context:
|
|
559
|
+
* - Cloud container (Render / Fly): local transcribers (parakeet,
|
|
560
|
+
* whisper) don't fit in 512MB + can't reach hardware acceleration.
|
|
561
|
+
* We hide them. Groq is the default (fast cloud Whisper, ~$0.04/hr
|
|
562
|
+
* of audio); OpenAI is the alternative.
|
|
563
|
+
* - Local (Mac / Linux): parakeet-mlx is the default on Mac (silicon
|
|
564
|
+
* MLX); falls back to onnx-asr cross-platform. Cloud providers
|
|
565
|
+
* stay available as choices for operators who'd rather pay than
|
|
566
|
+
* run local inference.
|
|
567
|
+
*
|
|
568
|
+
* The API key input shows conditionally — only when a cloud provider
|
|
569
|
+
* is selected. It's a plain text input (no `type=password`) because
|
|
570
|
+
* (a) the operator just pasted it from their provider's dashboard, and
|
|
571
|
+
* (b) showing it lets them verify they pasted correctly before submit.
|
|
572
|
+
* Mode-switching between providers via the radio is handled by an
|
|
573
|
+
* inline `<script>` block — no SPA bundle, no module deps.
|
|
574
|
+
*
|
|
575
|
+
* The "Skip — no transcription" option is third and unchecked by
|
|
576
|
+
* default. Most operators want voice transcription once they know
|
|
577
|
+
* they can; the default-on posture matches the auto-transcribe default
|
|
578
|
+
* flip that landed in vault#373.
|
|
579
|
+
*/
|
|
580
|
+
function renderScribeSubForm(cloudHost: boolean): string {
|
|
581
|
+
const localBlock = cloudHost
|
|
582
|
+
? ""
|
|
583
|
+
: `
|
|
584
|
+
<label class="scribe-provider-option">
|
|
585
|
+
<input type="radio" name="scribe_provider" value="local"${cloudHost ? "" : " checked"} data-needs-key="false" />
|
|
586
|
+
<span class="provider-name">Local <small>(Mac MLX or ONNX — no API key needed)</small></span>
|
|
587
|
+
</label>`;
|
|
588
|
+
const groqDefault = cloudHost ? " checked" : "";
|
|
589
|
+
return `
|
|
590
|
+
<details class="scribe-suboptions" open>
|
|
591
|
+
<summary class="cursor-pointer">
|
|
592
|
+
<span class="field-label">Enable voice transcription</span>
|
|
593
|
+
<span class="field-hint"> · Scribe installs alongside vault, transcribes audio attachments automatically</span>
|
|
594
|
+
</summary>
|
|
595
|
+
<div class="scribe-provider-block">
|
|
596
|
+
<p class="field-hint">Pick a transcription provider. You can change this later in <code>/admin/modules</code>.</p>
|
|
597
|
+
<div class="scribe-provider-list">
|
|
598
|
+
${localBlock}
|
|
599
|
+
<label class="scribe-provider-option">
|
|
600
|
+
<input type="radio" name="scribe_provider" value="groq"${groqDefault} data-needs-key="true" />
|
|
601
|
+
<span class="provider-name">Groq <small>(~\$0.04/hr of audio, fast)</small></span>
|
|
602
|
+
</label>
|
|
603
|
+
<label class="scribe-provider-option">
|
|
604
|
+
<input type="radio" name="scribe_provider" value="openai" data-needs-key="true" />
|
|
605
|
+
<span class="provider-name">OpenAI Whisper <small>(~\$0.36/hr of audio)</small></span>
|
|
606
|
+
</label>
|
|
607
|
+
<label class="scribe-provider-option">
|
|
608
|
+
<input type="radio" name="scribe_provider" value="none" data-needs-key="false" />
|
|
609
|
+
<span class="provider-name">Skip — no transcription</span>
|
|
610
|
+
</label>
|
|
611
|
+
</div>
|
|
612
|
+
<label class="field scribe-api-key-field" data-shows-on="cloud">
|
|
613
|
+
<span class="field-label">API key</span>
|
|
614
|
+
<input type="text" name="scribe_api_key" autocomplete="off" placeholder="gsk_… or sk-…" />
|
|
615
|
+
<span class="field-hint">Pasted directly into <code>~/.parachute/scribe/config.json</code> on this hub (file mode 0o600). Leave blank to skip and set later in the admin SPA.</span>
|
|
616
|
+
</label>
|
|
617
|
+
</div>
|
|
618
|
+
</details>
|
|
619
|
+
<script>
|
|
620
|
+
(function () {
|
|
621
|
+
var radios = document.querySelectorAll('input[name="scribe_provider"]');
|
|
622
|
+
var keyField = document.querySelector('.scribe-api-key-field');
|
|
623
|
+
function sync() {
|
|
624
|
+
var selected = document.querySelector('input[name="scribe_provider"]:checked');
|
|
625
|
+
var needsKey = selected && selected.dataset.needsKey === "true";
|
|
626
|
+
if (keyField) keyField.style.display = needsKey ? "" : "none";
|
|
627
|
+
}
|
|
628
|
+
radios.forEach(function (r) { r.addEventListener("change", sync); });
|
|
629
|
+
sync();
|
|
630
|
+
})();
|
|
631
|
+
</script>
|
|
632
|
+
`;
|
|
633
|
+
}
|
|
634
|
+
|
|
532
635
|
function renderVaultOpStep(props: {
|
|
533
636
|
operation: NonNullable<RenderVaultStepProps["operation"]>;
|
|
534
637
|
}): string {
|
|
@@ -567,7 +670,7 @@ function renderVaultOpStep(props: {
|
|
|
567
670
|
</section>
|
|
568
671
|
${
|
|
569
672
|
operation.status === "succeeded"
|
|
570
|
-
?
|
|
673
|
+
? `<meta http-equiv="refresh" content="1; url=/admin/setup?just_finished=1${operation.scribeOpId ? `&op_scribe=${encodeURIComponent(operation.scribeOpId)}` : ""}" />`
|
|
571
674
|
: ""
|
|
572
675
|
}
|
|
573
676
|
</div>`;
|
|
@@ -721,7 +824,7 @@ export interface RenderDoneStepProps {
|
|
|
721
824
|
/**
|
|
722
825
|
* Whether parachute-app is installed alongside the vault. Drives the
|
|
723
826
|
* "Start using your vault" lead tile (hub#342): when true, the tile
|
|
724
|
-
* links to `/
|
|
827
|
+
* links to `/surface/notes/` (the canonical user-facing surface — App
|
|
725
828
|
* auto-bootstraps Notes-as-UI per the 2026-05-21 migration). When
|
|
726
829
|
* false, it falls back to the vault's own admin UI at
|
|
727
830
|
* `/vault/<name>/admin/` so the operator still has a single obvious
|
|
@@ -738,7 +841,7 @@ export function renderDoneStep(props: RenderDoneStepProps): string {
|
|
|
738
841
|
const mcpTile = renderMcpTile(vaultName, hubOrigin, mintedToken);
|
|
739
842
|
const tiles = installTiles && installTiles.length > 0 ? installTiles : [];
|
|
740
843
|
const installSection = tiles.length > 0 ? renderInstallTiles(tiles) : "";
|
|
741
|
-
const startTile = renderStartUsingTile(vaultName, appInstalled === true);
|
|
844
|
+
const startTile = renderStartUsingTile(vaultName, appInstalled === true, hubOrigin);
|
|
742
845
|
// The done-grid hosts the MCP-connect tile + the admin-UI fallback.
|
|
743
846
|
// The install tiles sit above it as a "what's next?" surface (curated
|
|
744
847
|
// catalog of modules an operator might want next). The "Start using
|
|
@@ -762,6 +865,7 @@ export function renderDoneStep(props: RenderDoneStepProps): string {
|
|
|
762
865
|
</div>
|
|
763
866
|
${reachable}
|
|
764
867
|
${startTile}
|
|
868
|
+
${renderStarterPromptsSection()}
|
|
765
869
|
${installSection}
|
|
766
870
|
<section class="done-grid">
|
|
767
871
|
${mcpTile}
|
|
@@ -941,7 +1045,7 @@ function renderMcpTile(
|
|
|
941
1045
|
* command, admin UI, additional module installs).
|
|
942
1046
|
*
|
|
943
1047
|
* Two shapes:
|
|
944
|
-
* - **App installed** → primary tile targets `/
|
|
1048
|
+
* - **App installed** → primary tile targets `/surface/notes/` (the
|
|
945
1049
|
* Notes app reading the just-created vault). This is the
|
|
946
1050
|
* canonical surface post-Notes-as-app migration (parachute-app §17).
|
|
947
1051
|
* - **App NOT installed** → primary tile targets the vault's own
|
|
@@ -953,26 +1057,98 @@ function renderMcpTile(
|
|
|
953
1057
|
* "start using parachute" — not three competing tiles where the
|
|
954
1058
|
* "real" entry point is buried under the MCP command pre-hub#342.
|
|
955
1059
|
*/
|
|
956
|
-
|
|
1060
|
+
/**
|
|
1061
|
+
* Lead "Start using your vault" tile. Points at the canonical
|
|
1062
|
+
* notes.parachute.computer hosted PWA as the primary CTA — with the
|
|
1063
|
+
* operator's own hub URL pre-filled via `?url=` so the connect screen
|
|
1064
|
+
* auto-populates + auto-focuses (notes-ui AddVault route, see
|
|
1065
|
+
* parachute-app/packages/notes-ui/src/app/routes/AddVault.tsx).
|
|
1066
|
+
*
|
|
1067
|
+
* Aaron 2026-05-27 directive: "skipping the local surface install for
|
|
1068
|
+
* most operators is good ... showing notes.parachute.computer more
|
|
1069
|
+
* prominently is a good idea." The notes.parachute.computer PWA is the
|
|
1070
|
+
* canonical user-facing UI; operators no longer need to install the
|
|
1071
|
+
* Surface module locally to use Notes. They still can (local install
|
|
1072
|
+
* works the same way), but the wizard doesn't push them toward it as
|
|
1073
|
+
* the default.
|
|
1074
|
+
*
|
|
1075
|
+
* Secondary CTA: "Open vault admin" (the vault's own admin UI on this
|
|
1076
|
+
* hub) for operators who want to look at raw vault state.
|
|
1077
|
+
*
|
|
1078
|
+
* `appInstalled` is no longer load-bearing for the primary path —
|
|
1079
|
+
* notes.parachute.computer works regardless of whether Surface is
|
|
1080
|
+
* installed locally. Kept in the signature so the older test fixtures
|
|
1081
|
+
* + the boolean flag stay coherent; only the secondary fallback message
|
|
1082
|
+
* differs based on it.
|
|
1083
|
+
*/
|
|
1084
|
+
function renderStartUsingTile(
|
|
1085
|
+
vaultName: string,
|
|
1086
|
+
appInstalled: boolean,
|
|
1087
|
+
hubOrigin: string,
|
|
1088
|
+
): string {
|
|
957
1089
|
const safeVault = escapeHtml(vaultName);
|
|
958
1090
|
// Vault names pass `/^[a-z0-9][a-z0-9-]*$/i` so URL-encoding is mostly
|
|
959
1091
|
// a no-op today, but use encodeURIComponent defensively to match hub.ts:505.
|
|
960
1092
|
const urlVault = encodeURIComponent(vaultName);
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
1093
|
+
// The `?url=` query param is consumed by notes-ui's AddVault route
|
|
1094
|
+
// (packages/notes-ui/src/app/routes/AddVault.tsx) — it pre-fills the
|
|
1095
|
+
// vault URL input + auto-focuses Submit.
|
|
1096
|
+
const vaultUrlForAdd = encodeURIComponent(
|
|
1097
|
+
`${hubOrigin.replace(/\/+$/, "")}/vault/${vaultName}`,
|
|
1098
|
+
);
|
|
1099
|
+
// For appInstalled=false case (Surface NOT installed locally),
|
|
1100
|
+
// notes.parachute.computer is the recommended path. For appInstalled=true,
|
|
1101
|
+
// we mention the local option as a secondary affordance.
|
|
1102
|
+
const localNotesFallback = appInstalled
|
|
1103
|
+
? `<p class="start-using-secondary">
|
|
1104
|
+
<a href="/surface/notes/">Or use Notes installed locally on this hub →</a>
|
|
1105
|
+
</p>`
|
|
1106
|
+
: "";
|
|
969
1107
|
return `<section class="start-using" data-testid="start-using-tile">
|
|
970
1108
|
<h2>Start using your vault</h2>
|
|
971
|
-
<p>
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
1109
|
+
<p>Open Notes — the canonical browser UI for your vault <code>${safeVault}</code>.
|
|
1110
|
+
It connects to your hub over HTTPS and remembers your URL after the first OAuth.</p>
|
|
1111
|
+
<p><a class="btn btn-primary" href="https://notes.parachute.computer/add?url=${vaultUrlForAdd}" target="_blank" rel="noopener">Open Notes ↗</a></p>
|
|
1112
|
+
<p class="start-using-secondary">
|
|
1113
|
+
<a href="/vault/${urlVault}/admin/">Or browse the vault's admin UI →</a>
|
|
1114
|
+
</p>
|
|
1115
|
+
${localNotesFallback}
|
|
1116
|
+
</section>`;
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
/**
|
|
1120
|
+
* Starter-prompts tile on the done screen. Surfaces the two
|
|
1121
|
+
* interview-style prompts hosted at parachute.computer:
|
|
1122
|
+
*
|
|
1123
|
+
* 1. "Help me set up my vault" — AI interviews the operator about
|
|
1124
|
+
* where their data lives + proposes a tag/path structure
|
|
1125
|
+
* (parachute.computer/onboarding/vault-setup/).
|
|
1126
|
+
* 2. "Build a custom UI" — AI builds a static SPA against the vault's
|
|
1127
|
+
* HTTP API, hosted on the operator's own GitHub Pages
|
|
1128
|
+
* (parachute.computer/onboarding/surface-build/).
|
|
1129
|
+
*
|
|
1130
|
+
* Aaron 2026-05-27 directive: ship these as the "first AI assist"
|
|
1131
|
+
* surface so freshly-onboarded operators have a clear next thing to
|
|
1132
|
+
* do beyond clicking around the admin UI. The prompts live on
|
|
1133
|
+
* parachute.computer rather than embedded in the wizard so they can
|
|
1134
|
+
* be iterated without a hub release; the wizard just links.
|
|
1135
|
+
*/
|
|
1136
|
+
function renderStarterPromptsSection(): string {
|
|
1137
|
+
return `<section class="starter-prompts" data-testid="starter-prompts">
|
|
1138
|
+
<h2>Get help from your AI</h2>
|
|
1139
|
+
<p class="starter-prompts-subtitle">Two interview-style prompts to paste into Claude Code or Codex once your vault's MCP is wired up.</p>
|
|
1140
|
+
<div class="starter-prompts-grid">
|
|
1141
|
+
<a class="starter-prompt-tile" href="https://parachute.computer/onboarding/vault-setup/" target="_blank" rel="noopener">
|
|
1142
|
+
<h3>Set up your vault</h3>
|
|
1143
|
+
<p>Interview-style. AI asks where your notes live now + proposes a tag & path structure that fits how you actually think.</p>
|
|
1144
|
+
<p class="starter-prompt-cta">Open prompt ↗</p>
|
|
1145
|
+
</a>
|
|
1146
|
+
<a class="starter-prompt-tile" href="https://parachute.computer/onboarding/surface-build/" target="_blank" rel="noopener">
|
|
1147
|
+
<h3>Build a custom UI</h3>
|
|
1148
|
+
<p>AI generates a static SPA hosted on your own GitHub Pages — talks to your vault over HTTP. Notes UI works as a reference.</p>
|
|
1149
|
+
<p class="starter-prompt-cta">Open prompt ↗</p>
|
|
1150
|
+
</a>
|
|
1151
|
+
</div>
|
|
976
1152
|
</section>`;
|
|
977
1153
|
}
|
|
978
1154
|
|
|
@@ -1079,7 +1255,7 @@ function renderInstallTile(tile: ModuleInstallTileState): string {
|
|
|
1079
1255
|
* surface decision.
|
|
1080
1256
|
*/
|
|
1081
1257
|
const USE_IT_NOW_URLS: Partial<Record<CuratedModuleShort, string>> = {
|
|
1082
|
-
|
|
1258
|
+
surface: "/surface/notes/",
|
|
1083
1259
|
notes: "/notes/",
|
|
1084
1260
|
// Omitted: scribe + runner. They don't ship an admin SPA yet
|
|
1085
1261
|
// (scribe#53, runner#8 track). Pointing "Use it now" at /scribe/admin
|
|
@@ -1229,9 +1405,9 @@ export function handleSetupGet(req: Request, deps: SetupWizardDeps): Response {
|
|
|
1229
1405
|
const installTiles = buildInstallTiles(url, deps);
|
|
1230
1406
|
// hub#342: drive the lead "Start using your vault" tile's target.
|
|
1231
1407
|
// When parachute-app is installed alongside vault, the tile links
|
|
1232
|
-
// to `/
|
|
1408
|
+
// to `/surface/notes/` (auto-bootstrapped Notes-as-UI per parachute-app
|
|
1233
1409
|
// §17). Otherwise it falls back to the vault's own admin UI.
|
|
1234
|
-
const appInstalled = isModuleInstalled("
|
|
1410
|
+
const appInstalled = isModuleInstalled("surface", deps.manifestPath);
|
|
1235
1411
|
const doneProps: RenderDoneStepProps = {
|
|
1236
1412
|
vaultName,
|
|
1237
1413
|
hubOrigin: deps.issuer,
|
|
@@ -1270,25 +1446,33 @@ export function handleSetupGet(req: Request, deps: SetupWizardDeps): Response {
|
|
|
1270
1446
|
// Step 3 (vault) with an op in flight — render the poll page.
|
|
1271
1447
|
if (state.hasAdmin && !state.hasVault) {
|
|
1272
1448
|
const opId = url.searchParams.get("op");
|
|
1449
|
+
const cloudHost = detectAutoExposeMode(deps.env ?? process.env) === "public";
|
|
1273
1450
|
if (opId) {
|
|
1274
1451
|
const registry = deps.registry;
|
|
1275
1452
|
const op = registry?.get(opId);
|
|
1276
1453
|
if (op) {
|
|
1454
|
+
// Carry the scribe op_id forward via the query param so the
|
|
1455
|
+
// op-poll page's success-redirect threads it into the done
|
|
1456
|
+
// step's URL (where buildInstallTiles picks it up via the
|
|
1457
|
+
// existing per-tile `op_scribe` mechanism).
|
|
1458
|
+
const scribeOpIdParam = url.searchParams.get("op_scribe") ?? undefined;
|
|
1277
1459
|
return new Response(
|
|
1278
1460
|
renderVaultStep({
|
|
1279
1461
|
csrfToken: csrf.token,
|
|
1462
|
+
cloudHost,
|
|
1280
1463
|
operation: {
|
|
1281
1464
|
id: op.id,
|
|
1282
1465
|
status: op.status,
|
|
1283
1466
|
log: op.log,
|
|
1284
1467
|
...(op.error !== undefined ? { error: op.error } : {}),
|
|
1468
|
+
...(scribeOpIdParam !== undefined ? { scribeOpId: scribeOpIdParam } : {}),
|
|
1285
1469
|
},
|
|
1286
1470
|
}),
|
|
1287
1471
|
{ status: 200, headers: extraHeaders },
|
|
1288
1472
|
);
|
|
1289
1473
|
}
|
|
1290
1474
|
}
|
|
1291
|
-
return new Response(renderVaultStep({ csrfToken: csrf.token }), {
|
|
1475
|
+
return new Response(renderVaultStep({ csrfToken: csrf.token, cloudHost }), {
|
|
1292
1476
|
status: 200,
|
|
1293
1477
|
headers: extraHeaders,
|
|
1294
1478
|
});
|
|
@@ -1609,7 +1793,130 @@ export async function handleSetupVaultPost(req: Request, deps: SetupWizardDeps):
|
|
|
1609
1793
|
"[setup-wizard] handleSetupVaultPost called with no operations registry — install will NOT run. Wire deps.registry in the dispatcher.",
|
|
1610
1794
|
);
|
|
1611
1795
|
}
|
|
1612
|
-
|
|
1796
|
+
// Scribe sub-form fold (2026-05-27). The vault step's form lets
|
|
1797
|
+
// the operator answer "do you also want voice transcription?" in
|
|
1798
|
+
// the same submission. If they did, we (a) write the provider +
|
|
1799
|
+
// API key to `~/.parachute/surface/config.json` so scribe finds
|
|
1800
|
+
// them on first boot, and (b) kick a scribe install op in
|
|
1801
|
+
// parallel with vault install. The vault op-poll page threads the
|
|
1802
|
+
// scribe op_id through its success-redirect so the done step can
|
|
1803
|
+
// poll scribe progress via the existing per-tile mechanism.
|
|
1804
|
+
const scribeProvider = String(form.get("scribe_provider") ?? "").trim();
|
|
1805
|
+
let scribeOpId: string | undefined;
|
|
1806
|
+
if (scribeProvider !== "" && scribeProvider !== "none") {
|
|
1807
|
+
const scribeApiKey = String(form.get("scribe_api_key") ?? "").trim();
|
|
1808
|
+
// Write scribe config FIRST so scribe's first boot picks up the
|
|
1809
|
+
// provider + key without a second config edit. We don't fail the
|
|
1810
|
+
// wizard on a config-write error — log it + carry on; scribe will
|
|
1811
|
+
// boot with defaults + the operator can fix via /scribe/admin.
|
|
1812
|
+
try {
|
|
1813
|
+
writeScribeConfigForWizard(deps.configDir, scribeProvider, scribeApiKey);
|
|
1814
|
+
} catch (err) {
|
|
1815
|
+
console.warn(
|
|
1816
|
+
`[setup-wizard] failed to write scribe config: ${err instanceof Error ? err.message : String(err)} — kicking install anyway, operator can configure later.`,
|
|
1817
|
+
);
|
|
1818
|
+
}
|
|
1819
|
+
// Kick scribe install in parallel. Don't block on it; the done
|
|
1820
|
+
// step's per-tile op-poll surfaces progress.
|
|
1821
|
+
if (registry) {
|
|
1822
|
+
const scribeSpec = specFor("scribe");
|
|
1823
|
+
const scribeOp = registry.create("install", "scribe");
|
|
1824
|
+
scribeOpId = scribeOp.id;
|
|
1825
|
+
void runInstall(scribeOp.id, "scribe", scribeSpec, {
|
|
1826
|
+
db: deps.db,
|
|
1827
|
+
issuer: deps.issuer,
|
|
1828
|
+
manifestPath: deps.manifestPath,
|
|
1829
|
+
configDir: deps.configDir,
|
|
1830
|
+
supervisor: deps.supervisor,
|
|
1831
|
+
registry,
|
|
1832
|
+
...(deps.run ? { run: deps.run } : {}),
|
|
1833
|
+
}).catch((err) => {
|
|
1834
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1835
|
+
registry.update(
|
|
1836
|
+
scribeOp.id,
|
|
1837
|
+
{ status: "failed", error: msg },
|
|
1838
|
+
`scribe install failed: ${msg}`,
|
|
1839
|
+
);
|
|
1840
|
+
});
|
|
1841
|
+
}
|
|
1842
|
+
}
|
|
1843
|
+
const redirectUrl = scribeOpId
|
|
1844
|
+
? `/admin/setup?op=${encodeURIComponent(op.id)}&op_scribe=${encodeURIComponent(scribeOpId)}`
|
|
1845
|
+
: `/admin/setup?op=${encodeURIComponent(op.id)}`;
|
|
1846
|
+
return redirect(redirectUrl);
|
|
1847
|
+
}
|
|
1848
|
+
|
|
1849
|
+
/**
|
|
1850
|
+
* Write a minimal scribe config that selects the operator's chosen
|
|
1851
|
+
* transcribe provider + API key (when applicable). Idempotent: reads
|
|
1852
|
+
* any existing config, merges, writes back. File mode 0o600 — the
|
|
1853
|
+
* config holds API keys, owner-only.
|
|
1854
|
+
*
|
|
1855
|
+
* Lives in setup-wizard.ts (not scribe's own config-write.ts) because
|
|
1856
|
+
* (a) it's a one-time wizard write — the SPA's PUT /.parachute/config
|
|
1857
|
+
* surface is the canonical post-setup path, and (b) hub doesn't
|
|
1858
|
+
* import scribe-internal modules. The shape of `scribe-config.json`
|
|
1859
|
+
* is documented in parachute-scribe/src/config.ts; the fields we set
|
|
1860
|
+
* (transcribe.provider + transcribeProviders.<name>.apiKey) are
|
|
1861
|
+
* stable.
|
|
1862
|
+
*/
|
|
1863
|
+
function writeScribeConfigForWizard(
|
|
1864
|
+
configDir: string,
|
|
1865
|
+
provider: string,
|
|
1866
|
+
apiKey: string,
|
|
1867
|
+
): void {
|
|
1868
|
+
// For `local` (Mac MLX / cross-platform ONNX), just set the
|
|
1869
|
+
// provider name — no key needed.
|
|
1870
|
+
if (provider === "local") {
|
|
1871
|
+
persistScribeConfig(configDir, { transcribe: { provider: "parakeet-mlx" } });
|
|
1872
|
+
return;
|
|
1873
|
+
}
|
|
1874
|
+
// Cloud providers need a key. Empty key → just set provider; the
|
|
1875
|
+
// operator can paste the key later via /scribe/admin without a
|
|
1876
|
+
// restart (per provider-config.ts's per-request precedence).
|
|
1877
|
+
const update: Record<string, unknown> = { transcribe: { provider } };
|
|
1878
|
+
if (apiKey !== "") {
|
|
1879
|
+
update.transcribeProviders = { [provider]: { apiKey } };
|
|
1880
|
+
}
|
|
1881
|
+
persistScribeConfig(configDir, update);
|
|
1882
|
+
}
|
|
1883
|
+
|
|
1884
|
+
/**
|
|
1885
|
+
* Merge-write to scribe's config file at `<configDir>/scribe/config.json`.
|
|
1886
|
+
* Reads existing JSON when present, deep-merges `update`, writes back at
|
|
1887
|
+
* mode 0o600. Creates the parent dir if missing.
|
|
1888
|
+
*/
|
|
1889
|
+
function persistScribeConfig(configDir: string, update: Record<string, unknown>): void {
|
|
1890
|
+
const scribeDir = join(configDir, "scribe");
|
|
1891
|
+
const configPath = join(scribeDir, "config.json");
|
|
1892
|
+
mkdirSync(scribeDir, { recursive: true });
|
|
1893
|
+
let existing: Record<string, unknown> = {};
|
|
1894
|
+
if (existsSync(configPath)) {
|
|
1895
|
+
try {
|
|
1896
|
+
existing = JSON.parse(readFileSync(configPath, "utf8")) as Record<string, unknown>;
|
|
1897
|
+
} catch {
|
|
1898
|
+
// Malformed existing config — treat as empty + overwrite.
|
|
1899
|
+
existing = {};
|
|
1900
|
+
}
|
|
1901
|
+
}
|
|
1902
|
+
// Shallow merge at top level, deep merge for the two known sub-blocks
|
|
1903
|
+
// we touch (transcribe + transcribeProviders).
|
|
1904
|
+
const merged: Record<string, unknown> = { ...existing };
|
|
1905
|
+
for (const [key, value] of Object.entries(update)) {
|
|
1906
|
+
if (
|
|
1907
|
+
typeof value === "object" &&
|
|
1908
|
+
value !== null &&
|
|
1909
|
+
!Array.isArray(value) &&
|
|
1910
|
+
typeof merged[key] === "object" &&
|
|
1911
|
+
merged[key] !== null &&
|
|
1912
|
+
!Array.isArray(merged[key])
|
|
1913
|
+
) {
|
|
1914
|
+
merged[key] = { ...(merged[key] as Record<string, unknown>), ...value };
|
|
1915
|
+
} else {
|
|
1916
|
+
merged[key] = value;
|
|
1917
|
+
}
|
|
1918
|
+
}
|
|
1919
|
+
writeFileSync(configPath, `${JSON.stringify(merged, null, 2)}\n`, { mode: 0o600 });
|
|
1613
1920
|
}
|
|
1614
1921
|
|
|
1615
1922
|
/**
|
|
@@ -1711,9 +2018,9 @@ const INSTALL_TILE_PROPS: ReadonlyArray<{
|
|
|
1711
2018
|
tagline: string;
|
|
1712
2019
|
}> = [
|
|
1713
2020
|
{
|
|
1714
|
-
short: "
|
|
1715
|
-
displayName: "
|
|
1716
|
-
tagline: "Host module for Parachute
|
|
2021
|
+
short: "surface",
|
|
2022
|
+
displayName: "Surface",
|
|
2023
|
+
tagline: "Host module for Parachute surfaces — auto-installs Notes on first boot.",
|
|
1717
2024
|
},
|
|
1718
2025
|
{
|
|
1719
2026
|
short: "scribe",
|
|
@@ -1882,7 +2189,7 @@ function validateAccountFields(input: {
|
|
|
1882
2189
|
* Whether a given curated module is currently installed (has a row in
|
|
1883
2190
|
* services.json keyed by its canonical `manifestName`). Used by the
|
|
1884
2191
|
* done-step renderer (hub#342) to decide whether to point the "Start
|
|
1885
|
-
* using your vault" tile at `/
|
|
2192
|
+
* using your vault" tile at `/surface/notes/` (App installed → Notes UI
|
|
1886
2193
|
* auto-bootstrapped) vs the vault's own admin UI. Cheap manifest read
|
|
1887
2194
|
* shared with `buildInstallTiles`.
|
|
1888
2195
|
*/
|