@xingular/harness 1.0.0 → 1.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/CHANGELOG.md +46 -5
- package/README.md +75 -21
- package/dist/cli.js +17421 -8711
- package/dist/sdk/accounts/accounts.d.ts +27 -3
- package/dist/sdk/accounts/accounts.js +78 -24
- package/dist/sdk/accounts/secrets.d.ts +12 -0
- package/dist/sdk/accounts/secrets.js +114 -7
- package/dist/sdk/accounts/store.d.ts +26 -1
- package/dist/sdk/accounts/store.js +1 -1
- package/dist/sdk/agents/builtin.d.ts +2 -0
- package/dist/sdk/agents/builtin.js +48 -0
- package/dist/sdk/agents/narrow.d.ts +13 -0
- package/dist/sdk/agents/narrow.js +29 -0
- package/dist/sdk/agents/planResearch.d.ts +5 -0
- package/dist/sdk/agents/planResearch.js +19 -0
- package/dist/sdk/agents/profiles.d.ts +81 -0
- package/dist/sdk/agents/profiles.js +226 -0
- package/dist/sdk/annotations/bridge.d.ts +42 -0
- package/dist/sdk/annotations/bridge.js +264 -0
- package/dist/sdk/annotations/capture.d.ts +116 -0
- package/dist/sdk/annotations/capture.js +308 -0
- package/dist/sdk/annotations/delivery.d.ts +72 -0
- package/dist/sdk/annotations/delivery.js +130 -0
- package/dist/sdk/annotations/inspect.d.ts +10 -0
- package/dist/sdk/annotations/inspect.js +33 -0
- package/dist/sdk/annotations/isolated.d.ts +6 -0
- package/dist/sdk/annotations/isolated.js +51 -0
- package/dist/sdk/annotations/pageScript.d.ts +28 -0
- package/dist/sdk/annotations/pageScript.js +871 -0
- package/dist/sdk/annotations/part.d.ts +10 -0
- package/dist/sdk/annotations/part.js +155 -0
- package/dist/sdk/annotations/queue.d.ts +4 -0
- package/dist/sdk/annotations/queue.js +40 -0
- package/dist/sdk/annotations/serverMethods.d.ts +3 -0
- package/dist/sdk/annotations/serverMethods.js +62 -0
- package/dist/sdk/annotations/store.d.ts +167 -0
- package/dist/sdk/annotations/store.js +394 -0
- package/dist/sdk/browser/backend.d.ts +16 -0
- package/dist/sdk/browser/backend.js +17 -0
- package/dist/sdk/browser/contracts.d.ts +19 -0
- package/dist/sdk/browser/contracts.js +116 -0
- package/dist/sdk/browser/index.d.ts +14 -3
- package/dist/sdk/browser/index.js +11 -4
- package/dist/sdk/browser/serverMethods.d.ts +3 -0
- package/dist/sdk/browser/serverMethods.js +7 -0
- package/dist/sdk/browser/session.d.ts +1 -0
- package/dist/sdk/browser/session.js +45 -9
- package/dist/sdk/browser/tools.d.ts +1 -4
- package/dist/sdk/browser/tools.js +14 -91
- package/dist/sdk/cli.js +24 -15
- package/dist/sdk/commands/list.d.ts +3 -0
- package/dist/sdk/commands/list.js +75 -0
- package/dist/sdk/commands/registry.d.ts +250 -0
- package/dist/sdk/commands/registry.js +47 -0
- package/dist/sdk/commands/routes.d.ts +16 -0
- package/dist/sdk/commands/routes.js +33 -0
- package/dist/sdk/core/bus.js +1 -1
- package/dist/sdk/core/commandBudget.d.ts +23 -0
- package/dist/sdk/core/commandBudget.js +57 -0
- package/dist/sdk/core/env.d.ts +1 -1
- package/dist/sdk/core/env.js +1 -1
- package/dist/sdk/core/index.js +17 -1
- package/dist/sdk/core/interactiveProfile.d.ts +46 -0
- package/dist/sdk/core/interactiveProfile.js +143 -0
- package/dist/sdk/core/workspace.d.ts +13 -0
- package/dist/sdk/core/workspace.js +88 -0
- package/dist/sdk/core/workspaceBashTool.d.ts +4 -0
- package/dist/sdk/core/workspaceBashTool.js +36 -0
- package/dist/sdk/core/workspaceSandbox.d.ts +31 -0
- package/dist/sdk/core/workspaceSandbox.js +320 -0
- package/dist/sdk/core/workspaceWriteOperations.d.ts +22 -0
- package/dist/sdk/core/workspaceWriteOperations.js +206 -0
- package/dist/sdk/doctor/serverMethods.d.ts +3 -0
- package/dist/sdk/doctor/serverMethods.js +8 -0
- package/dist/sdk/doctor/types.d.ts +2 -2
- package/dist/sdk/harness.d.ts +22 -0
- package/dist/sdk/harness.js +84 -5
- package/dist/sdk/history/recall.js +8 -8
- package/dist/sdk/history/serverMethods.d.ts +3 -0
- package/dist/sdk/history/serverMethods.js +8 -0
- package/dist/sdk/hub/cli.d.ts +1 -1
- package/dist/sdk/hub/cli.js +1 -1
- package/dist/sdk/hub/schema.d.ts +1 -1
- package/dist/sdk/hub/serverMethods.d.ts +3 -0
- package/dist/sdk/hub/serverMethods.js +11 -0
- package/dist/sdk/index.d.ts +16 -1
- package/dist/sdk/index.js +10 -0
- package/dist/sdk/mcp/connections.js +14 -7
- package/dist/sdk/mcp/serverMethods.d.ts +3 -0
- package/dist/sdk/mcp/serverMethods.js +22 -0
- package/dist/sdk/pi/runtime.d.ts +2 -1
- package/dist/sdk/pi/runtime.js +2 -2
- package/dist/sdk/plugins/builtin.d.ts +1 -0
- package/dist/sdk/plugins/builtin.js +145 -33
- package/dist/sdk/plugins/builtinRegistry.d.ts +60 -0
- package/dist/sdk/plugins/builtinRegistry.js +71 -0
- package/dist/sdk/plugins/catalogue.js +2 -1
- package/dist/sdk/plugins/cli.d.ts +1 -1
- package/dist/sdk/plugins/cli.js +4 -3
- package/dist/sdk/plugins/commandPrefix.d.ts +8 -0
- package/dist/sdk/plugins/commandPrefix.js +21 -0
- package/dist/sdk/plugins/firstParty.d.ts +1 -1
- package/dist/sdk/plugins/firstParty.js +8 -2
- package/dist/sdk/plugins/inventory.js +4 -3
- package/dist/sdk/plugins/manifest.d.ts +2 -0
- package/dist/sdk/plugins/manifest.js +2 -1
- package/dist/sdk/plugins/surfaces.d.ts +5 -0
- package/dist/sdk/plugins/surfaces.js +11 -0
- package/dist/sdk/plugins/types.d.ts +15 -0
- package/dist/sdk/prompt/compile.d.ts +19 -2
- package/dist/sdk/prompt/compile.js +79 -26
- package/dist/sdk/prompt/standard.d.ts +17 -0
- package/dist/sdk/prompt/standard.js +21 -0
- package/dist/sdk/refine/cli.d.ts +1 -1
- package/dist/sdk/refine/cli.js +2 -2
- package/dist/sdk/refine/serverMethods.d.ts +3 -0
- package/dist/sdk/refine/serverMethods.js +20 -0
- package/dist/sdk/server/methods.js +27 -86
- package/dist/sdk/server/params.d.ts +4 -0
- package/dist/sdk/server/params.js +11 -0
- package/dist/sdk/server/remote.js +36 -11
- package/dist/sdk/sessions/compactionCheckpoint.d.ts +11 -0
- package/dist/sdk/sessions/compactionCheckpoint.js +79 -0
- package/dist/sdk/sessions/continuation.d.ts +3 -0
- package/dist/sdk/sessions/continuation.js +5 -1
- package/dist/sdk/sessions/contracts.d.ts +185 -0
- package/dist/sdk/sessions/contracts.js +101 -4
- package/dist/sdk/sessions/control.d.ts +72 -0
- package/dist/sdk/sessions/control.js +158 -0
- package/dist/sdk/sessions/initialBash.d.ts +7 -0
- package/dist/sdk/sessions/initialBash.js +7 -0
- package/dist/sdk/sessions/planMode.d.ts +18 -4
- package/dist/sdk/sessions/planMode.js +82 -16
- package/dist/sdk/sessions/reasoningSummaries.d.ts +2 -2
- package/dist/sdk/sessions/reasoningSummaries.js +3 -3
- package/dist/sdk/sessions/registry.d.ts +2 -1
- package/dist/sdk/sessions/registry.js +6 -3
- package/dist/sdk/sessions/runIndex.js +7 -7
- package/dist/sdk/sessions/runStore.d.ts +14 -0
- package/dist/sdk/sessions/runStore.js +38 -1
- package/dist/sdk/sessions/runner.d.ts +40 -2
- package/dist/sdk/sessions/runner.js +163 -15
- package/dist/sdk/sessions/sessions.d.ts +40 -8
- package/dist/sdk/sessions/sessions.js +279 -27
- package/dist/sdk/sessions/sideQuestionServerMethods.d.ts +3 -0
- package/dist/sdk/sessions/sideQuestionServerMethods.js +19 -0
- package/dist/sdk/sessions/sideQuestions.d.ts +66 -0
- package/dist/sdk/sessions/sideQuestions.js +290 -0
- package/dist/sdk/sessions/subagentServerMethods.d.ts +3 -0
- package/dist/sdk/sessions/subagentServerMethods.js +41 -0
- package/dist/sdk/sessions/subagentService.d.ts +22 -2
- package/dist/sdk/sessions/subagentService.js +93 -16
- package/dist/sdk/sessions/subagents.d.ts +9 -1
- package/dist/sdk/sessions/subagents.js +19 -0
- package/dist/sdk/sessions/workSlice.d.ts +103 -0
- package/dist/sdk/sessions/workSlice.js +297 -0
- package/dist/sdk/sessions/workspaceBoundary.d.ts +15 -0
- package/dist/sdk/sessions/workspaceBoundary.js +46 -0
- package/dist/sdk/terminal/browser.d.ts +62 -0
- package/dist/sdk/terminal/browser.js +41 -0
- package/dist/sdk/terminal/browserControl.d.ts +29 -0
- package/dist/sdk/terminal/browserControl.js +187 -0
- package/dist/sdk/terminal/commandHelp.d.ts +17 -0
- package/dist/sdk/terminal/commandHelp.js +20 -0
- package/dist/sdk/terminal/controller.d.ts +15 -1
- package/dist/sdk/terminal/controller.js +67 -8
- package/dist/sdk/terminal/opentui/subagentsWidget.d.ts +28 -0
- package/dist/sdk/terminal/opentui/subagentsWidget.js +52 -0
- package/dist/sdk/terminal/opentui/theme.d.ts +43 -0
- package/dist/sdk/terminal/opentui/theme.js +87 -0
- package/dist/sdk/terminal/piExtension.d.ts +6 -1
- package/dist/sdk/terminal/piExtension.js +169 -84
- package/dist/sdk/terminal/socket.d.ts +6 -3
- package/dist/sdk/terminal/socket.js +40 -9
- package/dist/sdk/terminal/standalone.d.ts +9 -0
- package/dist/sdk/terminal/standalone.js +118 -19
- package/dist/sdk/terminal/subagentMonitor.d.ts +47 -0
- package/dist/sdk/terminal/subagentMonitor.js +152 -0
- package/dist/sdk/terminal/terminals.d.ts +60 -7
- package/dist/sdk/terminal/terminals.js +598 -47
- package/dist/sdk/terminal/trajectory.js +2 -2
- package/dist/sdk/terminal/tui/annotateCommand.d.ts +3 -0
- package/dist/sdk/terminal/tui/annotateCommand.js +22 -0
- package/dist/sdk/terminal/tui/commandPages.d.ts +7 -1
- package/dist/sdk/terminal/tui/commandPages.js +57 -6
- package/dist/sdk/terminal/tui/commands.d.ts +10 -4
- package/dist/sdk/terminal/tui/commands.js +269 -65
- package/dist/sdk/terminal/tui/dialogGeometry.d.ts +2 -2
- package/dist/sdk/terminal/tui/dialogGeometry.js +4 -2
- package/dist/sdk/terminal/tui/dialogs.d.ts +6 -1
- package/dist/sdk/terminal/tui/dialogs.js +9 -2
- package/dist/sdk/terminal/tui/list.d.ts +14 -1
- package/dist/sdk/terminal/tui/list.js +96 -14
- package/dist/sdk/terminal/tui/modelPicker.js +1 -6
- package/dist/sdk/terminal/tui/page.d.ts +3 -0
- package/dist/sdk/terminal/tui/pageOverlay.js +27 -7
- package/dist/sdk/terminal/tui/pages/accountsPage.d.ts +26 -4
- package/dist/sdk/terminal/tui/pages/accountsPage.js +264 -36
- package/dist/sdk/terminal/tui/pages/agentsPage.d.ts +74 -0
- package/dist/sdk/terminal/tui/pages/agentsPage.js +190 -0
- package/dist/sdk/terminal/tui/pages/annotatePage.d.ts +94 -0
- package/dist/sdk/terminal/tui/pages/annotatePage.js +341 -0
- package/dist/sdk/terminal/tui/pages/browserPage.d.ts +87 -0
- package/dist/sdk/terminal/tui/pages/browserPage.js +738 -0
- package/dist/sdk/terminal/tui/pages/doctorPage.d.ts +2 -0
- package/dist/sdk/terminal/tui/pages/doctorPage.js +103 -15
- package/dist/sdk/terminal/tui/pages/notesPage.d.ts +75 -0
- package/dist/sdk/terminal/tui/pages/notesPage.js +201 -0
- package/dist/sdk/terminal/tui/pages/pluginsPage.d.ts +0 -5
- package/dist/sdk/terminal/tui/pages/pluginsPage.js +9 -3
- package/dist/sdk/terminal/tui/pages/settingsPage.d.ts +8 -0
- package/dist/sdk/terminal/tui/pages/settingsPage.js +16 -1
- package/dist/sdk/terminal/tui/pages/subagentsPage.d.ts +24 -0
- package/dist/sdk/terminal/tui/pages/subagentsPage.js +152 -0
- package/dist/sdk/terminal/tui/pages/trajectoryPage.d.ts +12 -5
- package/dist/sdk/terminal/tui/pages/trajectoryPage.js +156 -83
- package/dist/sdk/terminal/tui/pages/usagePage.d.ts +43 -0
- package/dist/sdk/terminal/tui/pages/usagePage.js +370 -0
- package/dist/sdk/terminal/tui/pages/worktreesPage.d.ts +72 -0
- package/dist/sdk/terminal/tui/pages/worktreesPage.js +203 -0
- package/dist/sdk/terminal/tui/piAdapter.d.ts +1 -2
- package/dist/sdk/terminal/tui/piAdapter.js +3 -5
- package/dist/sdk/terminal/tui/planMode.d.ts +1 -1
- package/dist/sdk/terminal/tui/planMode.js +1 -1
- package/dist/sdk/terminal/tui/pluginCommands.d.ts +16 -0
- package/dist/sdk/terminal/tui/pluginCommands.js +31 -0
- package/dist/sdk/terminal/tui/questions.d.ts +2 -0
- package/dist/sdk/terminal/tui/runtime.js +27 -5
- package/dist/sdk/terminal/tui/sessionPicker.d.ts +40 -0
- package/dist/sdk/terminal/tui/sessionPicker.js +141 -0
- package/dist/sdk/terminal/tui/settingsModel.d.ts +2 -0
- package/dist/sdk/terminal/tui/settingsModel.js +32 -2
- package/dist/sdk/terminal/tui/sideQuestion.d.ts +26 -26
- package/dist/sdk/terminal/tui/sideQuestion.js +131 -98
- package/dist/sdk/terminal/tui/ui.d.ts +2 -0
- package/dist/sdk/terminal/tui/ui.js +1 -0
- package/dist/sdk/tools/askUser.js +26 -8
- package/dist/sdk/tools/checklist.js +3 -2
- package/dist/sdk/tools/checkpoint.d.ts +27 -0
- package/dist/sdk/tools/checkpoint.js +154 -0
- package/dist/sdk/tools/getCapabilities.d.ts +31 -0
- package/dist/sdk/tools/getCapabilities.js +34 -0
- package/dist/sdk/tools/harnessTools.d.ts +2 -0
- package/dist/sdk/tools/harnessTools.js +1 -1
- package/dist/sdk/tools/spawnSubagent.js +14 -4
- package/dist/sdk/usage/analytics.d.ts +20 -0
- package/dist/sdk/usage/analytics.js +151 -0
- package/dist/sdk/usage/ledger.d.ts +38 -0
- package/dist/sdk/usage/ledger.js +142 -0
- package/dist/sdk/usage/limits/adapters.d.ts +11 -0
- package/dist/sdk/usage/limits/adapters.js +349 -0
- package/dist/sdk/usage/limits/capture.d.ts +8 -0
- package/dist/sdk/usage/limits/capture.js +47 -0
- package/dist/sdk/usage/limits/http.d.ts +12 -0
- package/dist/sdk/usage/limits/http.js +51 -0
- package/dist/sdk/usage/limits/normalize.d.ts +15 -0
- package/dist/sdk/usage/limits/normalize.js +211 -0
- package/dist/sdk/usage/limits/service.d.ts +30 -0
- package/dist/sdk/usage/limits/service.js +176 -0
- package/dist/sdk/usage/limits/types.d.ts +65 -0
- package/dist/sdk/usage/limits/types.js +1 -0
- package/dist/sdk/usage/serverMethods.d.ts +3 -0
- package/dist/sdk/usage/serverMethods.js +22 -0
- package/dist/sdk/usage/service.d.ts +32 -0
- package/dist/sdk/usage/service.js +66 -0
- package/dist/sdk/usage/settings.d.ts +7 -0
- package/dist/sdk/usage/settings.js +28 -0
- package/dist/sdk/usage/types.d.ts +60 -0
- package/dist/sdk/usage/types.js +4 -0
- package/dist/sdk/web/part.d.ts +10 -0
- package/dist/sdk/web/part.js +6 -0
- package/dist/sdk/web/serverMethods.d.ts +3 -0
- package/dist/sdk/web/serverMethods.js +7 -0
- package/dist/sdk/wiki/serverMethods.d.ts +3 -0
- package/dist/sdk/wiki/serverMethods.js +26 -0
- package/dist/sdk/worktrees/cli.d.ts +11 -0
- package/dist/sdk/worktrees/cli.js +146 -0
- package/dist/sdk/worktrees/copy.d.ts +9 -0
- package/dist/sdk/worktrees/copy.js +111 -0
- package/dist/sdk/worktrees/git.d.ts +16 -0
- package/dist/sdk/worktrees/git.js +81 -0
- package/dist/sdk/worktrees/serverMethods.d.ts +3 -0
- package/dist/sdk/worktrees/serverMethods.js +30 -0
- package/dist/sdk/worktrees/service.d.ts +73 -0
- package/dist/sdk/worktrees/service.js +483 -0
- package/dist/sdk/worktrees/setup.d.ts +23 -0
- package/dist/sdk/worktrees/setup.js +65 -0
- package/dist/sdk/worktrees/types.d.ts +91 -0
- package/dist/sdk/worktrees/types.js +1 -0
- package/dist/sdk/world/cli.d.ts +2 -2
- package/dist/sdk/world/cli.js +6 -6
- package/dist/sdk/world/world.d.ts +3 -1
- package/dist/sdk/world/world.js +8 -9
- package/dist/terminal-opentui.js +16456 -5966
- package/dist/terminal-pi.js +1579 -573
- package/docs/README.md +22 -2
- package/docs/architecture/00-overview.md +28 -16
- package/docs/architecture/02-sessions-and-runs.md +184 -25
- package/docs/architecture/03-variants.md +10 -2
- package/docs/architecture/04-prompt.md +65 -7
- package/docs/architecture/06-plugins-and-capabilities.md +57 -4
- package/docs/architecture/07-tools-and-gate.md +112 -17
- package/docs/architecture/08-memory-and-history.md +4 -2
- package/docs/architecture/09-accounts-and-connectors.md +39 -1
- package/docs/architecture/10-server-protocol.md +168 -4
- package/docs/architecture/11-terminal.md +251 -8
- package/docs/architecture/12-storage.md +45 -3
- package/docs/architecture/13-world-tools.md +49 -6
- package/docs/architecture/14-surfaces-and-parity.md +136 -0
- package/docs/architecture/README.md +17 -4
- package/docs/architecture/proposals/PROP-0006-run-checklist.md +4 -2
- package/docs/architecture/proposals/PROP-0013-resource-browser.md +58 -0
- package/docs/architecture/proposals/PROP-0014-fullscreen-browser-tabs.md +58 -0
- package/docs/architecture/proposals/PROP-0015-one-capability-surface.md +88 -0
- package/docs/architecture/proposals/PROP-0016-managed-worktrees.md +156 -0
- package/docs/architecture/proposals/PROP-0017-page-annotations.md +131 -0
- package/docs/architecture/proposals/PROP-0018-annotation-queue-and-app-targets.md +179 -0
- package/docs/architecture/proposals/PROP-0019-contextual-side-questions.md +122 -0
- package/docs/architecture/proposals/PROP-0020-builtin-feature-plugins.md +105 -0
- package/docs/architecture/proposals/PROP-0021-command-namespaces-and-duplicates.md +99 -0
- package/docs/architecture/proposals/PROP-0022-plan-only-questions.md +102 -0
- package/docs/architecture/proposals/PROP-0023-xagent-cli-name.md +61 -0
- package/docs/architecture/proposals/PROP-0024-question-sketch-previews.md +130 -0
- package/docs/architecture/proposals/PROP-0025-contextual-question-wireframes.md +49 -0
- package/docs/architecture/proposals/PROP-0026-connected-sketch-display.md +94 -0
- package/docs/architecture/proposals/PROP-0027-xingular-agent-product-identity.md +60 -0
- package/docs/architecture/proposals/PROP-0028-normalize-question-wireframes.md +55 -0
- package/docs/architecture/proposals/PROP-0029-agent-profiles.md +124 -0
- package/docs/architecture/proposals/PROP-0030-subagent-title.md +86 -0
- package/docs/architecture/proposals/PROP-0031-plugin-command-owner-namespaces.md +54 -0
- package/docs/architecture/proposals/PROP-0032-planning-quality.md +196 -0
- package/docs/architecture/proposals/PROP-0033-compaction-plan-checkpoints.md +75 -0
- package/docs/architecture/proposals/PROP-0034-conversation-fast-mode.md +120 -0
- package/docs/architecture/proposals/PROP-0035-retain-subagent-status.md +47 -0
- package/docs/architecture/proposals/PROP-0036-first-class-harness-commands.md +59 -0
- package/docs/architecture/proposals/PROP-0037-workspace-write-boundary.md +63 -0
- package/docs/architecture/proposals/PROP-0038-annotate-open-shadow-roots.md +73 -0
- package/docs/architecture/proposals/PROP-0039-workspace-boundary-hardening.md +139 -0
- package/docs/architecture/proposals/PROP-0040-one-launch-path.md +87 -0
- package/docs/architecture/proposals/PROP-0041-bounded-work-slices.md +247 -0
- package/docs/architecture/proposals/PROP-0042-usage-and-provider-limits.md +127 -0
- package/docs/architecture/proposals/PROP-0043-read-only-plan-bash.md +74 -0
- package/docs/architecture/proposals/README.md +39 -0
- package/docs/decisions/ADR-0001-owned-terminal.md +1 -1
- package/docs/decisions/ADR-0009-xharness-command.md +6 -5
- package/docs/decisions/ADR-0014-public-release-baseline.md +3 -2
- package/docs/decisions/ADR-0015-resource-browser.md +28 -0
- package/docs/decisions/ADR-0016-fullscreen-browser-tabs.md +31 -0
- package/docs/decisions/ADR-0017-one-capability-surface.md +34 -0
- package/docs/decisions/ADR-0018-builtin-feature-plugins.md +35 -0
- package/docs/decisions/ADR-0019-managed-git-worktrees.md +31 -0
- package/docs/decisions/ADR-0020-page-annotations.md +37 -0
- package/docs/decisions/ADR-0021-required-command-namespaces.md +40 -0
- package/docs/decisions/ADR-0022-plan-only-question-tools.md +34 -0
- package/docs/decisions/ADR-0023-contextual-side-question-runs.md +35 -0
- package/docs/decisions/ADR-0024-xagent-command.md +40 -0
- package/docs/decisions/ADR-0025-question-choice-sketches.md +49 -0
- package/docs/decisions/ADR-0026-contextual-question-wireframes.md +58 -0
- package/docs/decisions/ADR-0027-connected-sketch-display.md +60 -0
- package/docs/decisions/ADR-0028-xingular-agent-product-identity.md +56 -0
- package/docs/decisions/ADR-0029-normalized-question-wireframe-borders.md +33 -0
- package/docs/decisions/ADR-0030-agent-profiles.md +43 -0
- package/docs/decisions/ADR-0031-plugin-command-owner-namespaces.md +37 -0
- package/docs/decisions/ADR-0032-compaction-run-checkpoints.md +33 -0
- package/docs/decisions/ADR-0033-plan-mode-research-delegation.md +47 -0
- package/docs/decisions/ADR-0034-first-class-harness-terminal-commands.md +25 -0
- package/docs/decisions/ADR-0035-one-launch-path.md +40 -0
- package/docs/decisions/ADR-0036-workspace-write-operation-boundary.md +54 -0
- package/docs/decisions/ADR-0037-usage-ledger-and-provider-limits.md +44 -0
- package/docs/decisions/ADR-0038-plan-bash-os-read-only.md +29 -0
- package/docs/doctor.md +10 -4
- package/docs/experiments/EXP-0003-performance-audit/data/session-runtime-replication.json +420 -0
- package/docs/experiments/EXP-0003-performance-audit/data/session-runtime.json +420 -0
- package/docs/experiments/EXP-0003-performance-audit/data/terminal-compact.json +376 -0
- package/docs/experiments/EXP-0003-performance-audit/data/terminal-history.json +422 -0
- package/docs/experiments/EXP-0003-performance-audit/data/terminal-question.json +95 -0
- package/docs/experiments/EXP-0003-performance-audit/data/terminal-wide.json +1036 -0
- package/docs/experiments/EXP-0003-performance-audit/plan.md +109 -0
- package/docs/experiments/EXP-0003-performance-audit/results.md +151 -0
- package/docs/features/README.md +26 -0
- package/docs/features/interfaces/README.md +1 -1
- package/docs/future-work.md +48 -0
- package/docs/handoff-2026-10-25-app-parity-and-solo-tabs.md +202 -0
- package/docs/parity.md +134 -0
- package/docs/releases/1.0.0.md +2 -2
- package/docs/releases/1.1.1.md +28 -0
- package/docs/releases/1.2.0.md +49 -0
- package/docs/releases/4.0.0.md +47 -0
- package/docs/releases/README.md +37 -27
- package/docs/study/00-index.md +8 -0
- package/docs/study/STUDY-0003-long-horizon-runs.md +210 -0
- package/docs/study/autonomous-engineering-sources.md +277 -0
- package/docs/study/autonomy-sources.md +147 -0
- package/docs/study/proactive-autonomy.md +408 -0
- package/docs/study/tui-resource-browser.md +78 -0
- package/docs/terminal-tui.md +386 -113
- package/package.json +6 -5
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { type Effort } from "../core/effort.js";
|
|
2
|
+
export declare const PROFILE_NAME: RegExp;
|
|
3
|
+
export declare const PROFILE_COLORS: readonly ["blue", "green", "yellow", "red", "magenta", "cyan", "orange", "purple"];
|
|
4
|
+
export type ProfileColor = (typeof PROFILE_COLORS)[number];
|
|
5
|
+
export type ProfileSource = 'builtin' | 'global' | 'project' | 'variant';
|
|
6
|
+
/** Where a person's own profile file lives. */
|
|
7
|
+
export type ProfileScope = 'global' | 'project';
|
|
8
|
+
/** Which variant a profile's subagent runs: a scope's head or version, none, or the parent's (null). */
|
|
9
|
+
export type ProfileVariant = {
|
|
10
|
+
scope: string;
|
|
11
|
+
version: string | null;
|
|
12
|
+
} | 'none' | null;
|
|
13
|
+
export interface AgentProfile {
|
|
14
|
+
name: string;
|
|
15
|
+
description: string;
|
|
16
|
+
color: ProfileColor;
|
|
17
|
+
/** The role instructions: the file's body. */
|
|
18
|
+
instructions: string;
|
|
19
|
+
/** Tool allowlist; null keeps every tool the parent has. */
|
|
20
|
+
tools: string[] | null;
|
|
21
|
+
/** Plugin ids whose contributions the subagent keeps; null keeps all. Required plugins always stay. */
|
|
22
|
+
plugins: string[] | null;
|
|
23
|
+
variant: ProfileVariant;
|
|
24
|
+
model: string | null;
|
|
25
|
+
effort: Effort | null;
|
|
26
|
+
source: ProfileSource;
|
|
27
|
+
/** The file it was read from; null for built-ins. */
|
|
28
|
+
path: string | null;
|
|
29
|
+
}
|
|
30
|
+
export interface ProfileProblem {
|
|
31
|
+
name: string;
|
|
32
|
+
source: ProfileSource;
|
|
33
|
+
path: string;
|
|
34
|
+
error: string;
|
|
35
|
+
}
|
|
36
|
+
export interface ProfileListing {
|
|
37
|
+
profiles: AgentProfile[];
|
|
38
|
+
problems: ProfileProblem[];
|
|
39
|
+
}
|
|
40
|
+
/** The built-in profile definitions, before a source is attached. */
|
|
41
|
+
export type ProfileDefinition = Omit<AgentProfile, 'source' | 'path'>;
|
|
42
|
+
/** Nearest folder above `cwd` that holds `.git`, or `cwd` itself. */
|
|
43
|
+
export declare function projectRoot(cwd: string): string;
|
|
44
|
+
export declare function profileDirs(root: string, cwd?: string | null): {
|
|
45
|
+
global: string;
|
|
46
|
+
project: string | null;
|
|
47
|
+
};
|
|
48
|
+
export declare function parseVariant(value: unknown): ProfileVariant;
|
|
49
|
+
export declare function formatVariant(variant: ProfileVariant): string | null;
|
|
50
|
+
/** Reads one profile file. Throws an `invalid_request` error that names what is wrong. */
|
|
51
|
+
export declare function parseProfile(text: string, name: string, source: ProfileSource, path: string | null): AgentProfile;
|
|
52
|
+
/** The file text for a profile, as `createProfile` writes it. */
|
|
53
|
+
export declare function profileText(profile: ProfileDefinition): string;
|
|
54
|
+
/**
|
|
55
|
+
* Every profile a session in `cwd` can use. An unreadable file is reported in `problems`; it hides
|
|
56
|
+
* nothing, so a broken project file leaves the global or built-in profile of that name in force.
|
|
57
|
+
*/
|
|
58
|
+
export declare function listProfiles(root: string, cwd?: string | null, variantDir?: string | null): ProfileListing;
|
|
59
|
+
export declare function resolveProfile(root: string, cwd: string | null | undefined, name: string, variantDir?: string | null): AgentProfile | null;
|
|
60
|
+
/** Writes a new profile file from the template, or a copy of `from`. Refuses to overwrite one. */
|
|
61
|
+
export declare function createProfile(root: string, cwd: string | null | undefined, input: {
|
|
62
|
+
name: string;
|
|
63
|
+
scope: ProfileScope;
|
|
64
|
+
from?: string | null;
|
|
65
|
+
}): AgentProfile;
|
|
66
|
+
/** Deletes a person's own profile file. Built-ins and variant subagents cannot be removed here. */
|
|
67
|
+
export declare function removeProfile(root: string, cwd: string | null | undefined, input: {
|
|
68
|
+
name: string;
|
|
69
|
+
scope: ProfileScope;
|
|
70
|
+
}): void;
|
|
71
|
+
/** Where the profile list sits among the prompt sections: the slot a variant's subagents used. */
|
|
72
|
+
export declare const PROFILE_SECTION_ORDER = 64;
|
|
73
|
+
/** The prompt section that lists the profiles a run can start subagents from. */
|
|
74
|
+
export declare function profileSection(root: string, cwd: string | null | undefined, variantDir?: string | null, planResearchOnly?: boolean): {
|
|
75
|
+
id: string;
|
|
76
|
+
title: string;
|
|
77
|
+
body: string;
|
|
78
|
+
budget: number;
|
|
79
|
+
order: number;
|
|
80
|
+
dynamic: true;
|
|
81
|
+
};
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
// Agent profiles (PROP-0029): named bundles a subagent starts from, with its instructions, colour,
|
|
2
|
+
// tools, plugins, variant, model and effort. They come from four places, later ones winning:
|
|
3
|
+
// built-ins in code, `<data root>/agents/*.md`, `<project>/.xingular/agents/*.md`; a variant's own
|
|
4
|
+
// c5 subagents fill in names nobody else took. Profiles only narrow what the parent has.
|
|
5
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync } from 'node:fs';
|
|
6
|
+
import { dirname, join } from 'node:path';
|
|
7
|
+
import { EFFORTS } from "../core/effort.js";
|
|
8
|
+
import { invalid, notFound } from "../core/errors.js";
|
|
9
|
+
import { parse, serialize } from "../core/frontmatter.js";
|
|
10
|
+
import { atomicWrite } from "../core/fs.js";
|
|
11
|
+
import { subagentOf } from "../variant/materialise.js";
|
|
12
|
+
import { BUILTIN_PROFILES } from "./builtin.js";
|
|
13
|
+
import { planResearchProfile } from "./planResearch.js";
|
|
14
|
+
export const PROFILE_NAME = /^[a-z0-9][a-z0-9_-]*$/;
|
|
15
|
+
export const PROFILE_COLORS = ['blue', 'green', 'yellow', 'red', 'magenta', 'cyan', 'orange', 'purple'];
|
|
16
|
+
/** Nearest folder above `cwd` that holds `.git`, or `cwd` itself. */
|
|
17
|
+
export function projectRoot(cwd) {
|
|
18
|
+
let dir = cwd;
|
|
19
|
+
for (;;) {
|
|
20
|
+
if (existsSync(join(dir, '.git')))
|
|
21
|
+
return dir;
|
|
22
|
+
const parent = dirname(dir);
|
|
23
|
+
if (parent === dir)
|
|
24
|
+
return cwd;
|
|
25
|
+
dir = parent;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
export function profileDirs(root, cwd) {
|
|
29
|
+
return { global: join(root, 'agents'), project: cwd ? join(projectRoot(cwd), '.xingular', 'agents') : null };
|
|
30
|
+
}
|
|
31
|
+
function stringList(value, field) {
|
|
32
|
+
if (value === undefined || value === null)
|
|
33
|
+
return null;
|
|
34
|
+
const list = typeof value === 'string' ? value.split(/[\s,]+/u) : value;
|
|
35
|
+
if (!Array.isArray(list) || list.some(item => typeof item !== 'string'))
|
|
36
|
+
throw invalid(`${field} must be a list of names.`);
|
|
37
|
+
return [...new Set(list.map(item => item.trim()).filter(Boolean))];
|
|
38
|
+
}
|
|
39
|
+
function optionalText(value, field) {
|
|
40
|
+
if (value === undefined || value === null || value === '')
|
|
41
|
+
return null;
|
|
42
|
+
if (typeof value !== 'string')
|
|
43
|
+
throw invalid(`${field} must be text.`);
|
|
44
|
+
return value.trim() || null;
|
|
45
|
+
}
|
|
46
|
+
export function parseVariant(value) {
|
|
47
|
+
const text = optionalText(value, 'variant');
|
|
48
|
+
if (!text)
|
|
49
|
+
return null;
|
|
50
|
+
if (text === 'none')
|
|
51
|
+
return 'none';
|
|
52
|
+
const [scope, version, ...rest] = text.split('@');
|
|
53
|
+
if (!scope || rest.length || (version !== undefined && !version))
|
|
54
|
+
throw invalid('variant must be <scope>, <scope>@<version> or none.');
|
|
55
|
+
return { scope, version: version ?? null };
|
|
56
|
+
}
|
|
57
|
+
export function formatVariant(variant) {
|
|
58
|
+
if (!variant)
|
|
59
|
+
return null;
|
|
60
|
+
if (variant === 'none')
|
|
61
|
+
return 'none';
|
|
62
|
+
return variant.version ? `${variant.scope}@${variant.version}` : variant.scope;
|
|
63
|
+
}
|
|
64
|
+
/** Reads one profile file. Throws an `invalid_request` error that names what is wrong. */
|
|
65
|
+
export function parseProfile(text, name, source, path) {
|
|
66
|
+
if (!PROFILE_NAME.test(name))
|
|
67
|
+
throw invalid('A profile name uses lowercase letters, digits, - and _, and starts with a letter or digit.');
|
|
68
|
+
const parsed = parse(text);
|
|
69
|
+
if (!parsed)
|
|
70
|
+
throw invalid('The profile needs a frontmatter block between --- lines.');
|
|
71
|
+
const front = parsed.front;
|
|
72
|
+
if (front.name !== undefined && front.name !== name)
|
|
73
|
+
throw invalid(`The file is named ${name} but its frontmatter says ${String(front.name)}.`);
|
|
74
|
+
const description = optionalText(front.description, 'description');
|
|
75
|
+
if (!description)
|
|
76
|
+
throw invalid('The profile needs a description.');
|
|
77
|
+
const color = optionalText(front.color, 'color') ?? 'blue';
|
|
78
|
+
if (!PROFILE_COLORS.includes(color))
|
|
79
|
+
throw invalid(`color must be one of ${PROFILE_COLORS.join(', ')}.`);
|
|
80
|
+
const effort = optionalText(front.effort, 'effort');
|
|
81
|
+
if (effort && !EFFORTS.includes(effort))
|
|
82
|
+
throw invalid(`effort must be one of ${EFFORTS.join(', ')}.`);
|
|
83
|
+
return {
|
|
84
|
+
name, description, color: color, instructions: parsed.body.trim(),
|
|
85
|
+
tools: stringList(front.tools, 'tools'), plugins: stringList(front.plugins, 'plugins'),
|
|
86
|
+
variant: parseVariant(front.variant), model: optionalText(front.model, 'model'), effort: effort,
|
|
87
|
+
source, path
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
/** The file text for a profile, as `createProfile` writes it. */
|
|
91
|
+
export function profileText(profile) {
|
|
92
|
+
const front = { name: profile.name, description: profile.description, color: profile.color };
|
|
93
|
+
if (profile.tools)
|
|
94
|
+
front.tools = profile.tools;
|
|
95
|
+
if (profile.plugins)
|
|
96
|
+
front.plugins = profile.plugins;
|
|
97
|
+
const variant = formatVariant(profile.variant);
|
|
98
|
+
if (variant)
|
|
99
|
+
front.variant = variant;
|
|
100
|
+
if (profile.model)
|
|
101
|
+
front.model = profile.model;
|
|
102
|
+
if (profile.effort)
|
|
103
|
+
front.effort = profile.effort;
|
|
104
|
+
return serialize(front, profile.instructions);
|
|
105
|
+
}
|
|
106
|
+
function readFolder(dir, source, into, problems) {
|
|
107
|
+
if (!dir || !existsSync(dir))
|
|
108
|
+
return;
|
|
109
|
+
for (const file of readdirSync(dir).filter(entry => entry.endsWith('.md')).sort()) {
|
|
110
|
+
const path = join(dir, file);
|
|
111
|
+
const name = file.slice(0, -3);
|
|
112
|
+
try {
|
|
113
|
+
if (!statSync(path).isFile())
|
|
114
|
+
continue;
|
|
115
|
+
into.set(name, parseProfile(readFileSync(path, 'utf8'), name, source, path));
|
|
116
|
+
}
|
|
117
|
+
catch (error) {
|
|
118
|
+
problems.push({ name, source, path, error: error instanceof Error ? error.message : String(error) });
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
function variantDefinitions(variantDir) {
|
|
123
|
+
const folder = variantDir ? join(variantDir, 'sub_agents') : null;
|
|
124
|
+
if (!folder || !existsSync(folder))
|
|
125
|
+
return [];
|
|
126
|
+
return readdirSync(folder).sort().flatMap(name => {
|
|
127
|
+
const definition = subagentOf(variantDir, name);
|
|
128
|
+
if (!definition)
|
|
129
|
+
return [];
|
|
130
|
+
return [{
|
|
131
|
+
name, description: definition.description || name, color: 'blue', instructions: definition.instructions || definition.description,
|
|
132
|
+
tools: null, plugins: null, variant: null, model: null, effort: null,
|
|
133
|
+
source: 'variant', path: join(folder, name, 'AGENT.md')
|
|
134
|
+
}];
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Every profile a session in `cwd` can use. An unreadable file is reported in `problems`; it hides
|
|
139
|
+
* nothing, so a broken project file leaves the global or built-in profile of that name in force.
|
|
140
|
+
*/
|
|
141
|
+
export function listProfiles(root, cwd, variantDir) {
|
|
142
|
+
const profiles = new Map();
|
|
143
|
+
const problems = [];
|
|
144
|
+
for (const definition of BUILTIN_PROFILES)
|
|
145
|
+
profiles.set(definition.name, { ...definition, source: 'builtin', path: null });
|
|
146
|
+
const dirs = profileDirs(root, cwd);
|
|
147
|
+
readFolder(dirs.global, 'global', profiles, problems);
|
|
148
|
+
readFolder(dirs.project, 'project', profiles, problems);
|
|
149
|
+
for (const definition of variantDefinitions(variantDir))
|
|
150
|
+
if (!profiles.has(definition.name))
|
|
151
|
+
profiles.set(definition.name, definition);
|
|
152
|
+
return { profiles: [...profiles.values()].sort((left, right) => left.name.localeCompare(right.name)), problems };
|
|
153
|
+
}
|
|
154
|
+
export function resolveProfile(root, cwd, name, variantDir) {
|
|
155
|
+
if (!PROFILE_NAME.test(name))
|
|
156
|
+
return null;
|
|
157
|
+
return listProfiles(root, cwd, variantDir).profiles.find(profile => profile.name === name) ?? null;
|
|
158
|
+
}
|
|
159
|
+
function scopeDir(root, cwd, scope) {
|
|
160
|
+
const dirs = profileDirs(root, cwd);
|
|
161
|
+
if (scope === 'global')
|
|
162
|
+
return dirs.global;
|
|
163
|
+
if (!dirs.project)
|
|
164
|
+
throw invalid('A project profile needs a working folder.');
|
|
165
|
+
return dirs.project;
|
|
166
|
+
}
|
|
167
|
+
const TEMPLATE = {
|
|
168
|
+
description: 'What this agent is for, in one line.',
|
|
169
|
+
color: 'blue',
|
|
170
|
+
instructions: 'Describe how this agent should work: what to do with a brief, what to avoid, and what to report back.',
|
|
171
|
+
tools: null, plugins: null, variant: null, model: null, effort: null
|
|
172
|
+
};
|
|
173
|
+
/** Writes a new profile file from the template, or a copy of `from`. Refuses to overwrite one. */
|
|
174
|
+
export function createProfile(root, cwd, input) {
|
|
175
|
+
const name = typeof input.name === 'string' ? input.name.trim() : '';
|
|
176
|
+
if (!PROFILE_NAME.test(name))
|
|
177
|
+
throw invalid('A profile name uses lowercase letters, digits, - and _, and starts with a letter or digit.');
|
|
178
|
+
if (input.scope !== 'global' && input.scope !== 'project')
|
|
179
|
+
throw invalid('scope must be global or project.');
|
|
180
|
+
const dir = scopeDir(root, cwd, input.scope);
|
|
181
|
+
const path = join(dir, `${name}.md`);
|
|
182
|
+
if (existsSync(path))
|
|
183
|
+
throw invalid(`There is already a ${input.scope} profile named ${name}.`);
|
|
184
|
+
let source = { ...TEMPLATE, name };
|
|
185
|
+
if (input.from) {
|
|
186
|
+
const original = resolveProfile(root, cwd, input.from);
|
|
187
|
+
if (!original)
|
|
188
|
+
throw notFound(`The profile ${input.from}`);
|
|
189
|
+
source = { ...original, name };
|
|
190
|
+
}
|
|
191
|
+
mkdirSync(dir, { recursive: true });
|
|
192
|
+
const text = profileText(source);
|
|
193
|
+
atomicWrite(path, text);
|
|
194
|
+
return parseProfile(text, name, input.scope, path);
|
|
195
|
+
}
|
|
196
|
+
/** Deletes a person's own profile file. Built-ins and variant subagents cannot be removed here. */
|
|
197
|
+
export function removeProfile(root, cwd, input) {
|
|
198
|
+
if (!PROFILE_NAME.test(input.name))
|
|
199
|
+
throw invalid('That is not a profile name.');
|
|
200
|
+
if (input.scope !== 'global' && input.scope !== 'project')
|
|
201
|
+
throw invalid('scope must be global or project.');
|
|
202
|
+
const path = join(scopeDir(root, cwd, input.scope), `${input.name}.md`);
|
|
203
|
+
if (!existsSync(path))
|
|
204
|
+
throw notFound(`The ${input.scope} profile ${input.name}`);
|
|
205
|
+
rmSync(path);
|
|
206
|
+
}
|
|
207
|
+
/** Where the profile list sits among the prompt sections: the slot a variant's subagents used. */
|
|
208
|
+
export const PROFILE_SECTION_ORDER = 64;
|
|
209
|
+
/** The prompt section that lists the profiles a run can start subagents from. */
|
|
210
|
+
export function profileSection(root, cwd, variantDir, planResearchOnly = false) {
|
|
211
|
+
const { profiles } = listProfiles(root, cwd, variantDir);
|
|
212
|
+
const listed = planResearchOnly ? profiles.filter(planResearchProfile) : profiles;
|
|
213
|
+
return {
|
|
214
|
+
id: 'subagents',
|
|
215
|
+
title: 'Subagents',
|
|
216
|
+
body: [
|
|
217
|
+
planResearchOnly
|
|
218
|
+
? 'Plan mode may start only a named profile listed here, for read-only research. Do not omit agent or use a profile not listed.'
|
|
219
|
+
: 'Delegate when it meaningfully reduces work: choose a substantial, bounded task that can proceed independently, such as unfamiliar cross-file research likely to take several searches or a self-contained implementation. Do a known-file lookup, one or two simple tool calls, a tiny edit or tightly coupled work yourself. Prefer a named profile: explorer for repository research, researcher for public-source research, and worker for a defined change it can own end to end; omit agent only when no profile fits. Each child starts without this conversation, so give it the goal, relevant context or paths, non-goals, expected result and acceptance or verification; use its own checklist for multi-step work. Prefer context-heavy research with concise evidence-backed findings. Parallelize only independent tasks and never assign overlapping edits. Return without waiting only when useful independent work remains; otherwise wait. You own integration, verification and the user-facing report. A profile can only narrow your tools.',
|
|
220
|
+
...(listed.length ? listed.map((profile) => `- ${profile.name} — ${profile.description}`) : ['- No eligible read-only research profiles are configured.'])
|
|
221
|
+
].join('\n'),
|
|
222
|
+
budget: 6_000,
|
|
223
|
+
order: PROFILE_SECTION_ORDER,
|
|
224
|
+
dynamic: true
|
|
225
|
+
};
|
|
226
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { type PageOperation } from "./pageScript.js";
|
|
2
|
+
export interface BridgeWindow {
|
|
3
|
+
id: string;
|
|
4
|
+
url: string;
|
|
5
|
+
title: string;
|
|
6
|
+
lastSeen: number;
|
|
7
|
+
}
|
|
8
|
+
export interface BridgeInfo {
|
|
9
|
+
port: number;
|
|
10
|
+
token: string;
|
|
11
|
+
scriptUrl: string;
|
|
12
|
+
/** What to put in the app while developing it. */
|
|
13
|
+
snippet: string;
|
|
14
|
+
}
|
|
15
|
+
/** The script a window loads: the page helper plus a loop that pulls operations from the bridge. */
|
|
16
|
+
export declare function bridgeScript(base: string, token: string): string;
|
|
17
|
+
export declare class NotesBridge {
|
|
18
|
+
private readonly options;
|
|
19
|
+
private server;
|
|
20
|
+
private starting;
|
|
21
|
+
private info;
|
|
22
|
+
private readonly windows;
|
|
23
|
+
private nextWindow;
|
|
24
|
+
private nextCall;
|
|
25
|
+
constructor(options: {
|
|
26
|
+
stateDir: string;
|
|
27
|
+
port?: number;
|
|
28
|
+
});
|
|
29
|
+
private settingsFile;
|
|
30
|
+
/** Starts the bridge (once) on the port and token kept from last time, so the app's snippet stays valid. */
|
|
31
|
+
start(): Promise<BridgeInfo>;
|
|
32
|
+
private listen;
|
|
33
|
+
list(): BridgeWindow[];
|
|
34
|
+
/** Sends one helper operation to a window and waits for its JSON reply. */
|
|
35
|
+
call(windowId: string, operation: PageOperation): Promise<unknown>;
|
|
36
|
+
private flush;
|
|
37
|
+
private sweep;
|
|
38
|
+
private json;
|
|
39
|
+
private body;
|
|
40
|
+
private handle;
|
|
41
|
+
close(): Promise<void>;
|
|
42
|
+
}
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
// The page bridge (PROP-0018): page notes in a web view that has no DevTools port, such as a Tauri
|
|
2
|
+
// window on macOS. The developer loads one script into their app while developing it; the script
|
|
3
|
+
// carries the page helper and pulls operations from this loopback server, answering each with the
|
|
4
|
+
// helper's JSON. Only 127.0.0.1 listens, every request carries the bridge token, and everything a
|
|
5
|
+
// window answers is untrusted page data.
|
|
6
|
+
import { randomBytes } from 'node:crypto';
|
|
7
|
+
import { chmodSync, existsSync } from 'node:fs';
|
|
8
|
+
import { createServer } from 'node:http';
|
|
9
|
+
import { join } from 'node:path';
|
|
10
|
+
import { readJson, writeJson } from "../core/fs.js";
|
|
11
|
+
import { PAGE_SCRIPT } from "./pageScript.js";
|
|
12
|
+
const POLL_MS = 20_000;
|
|
13
|
+
const GONE_MS = 45_000;
|
|
14
|
+
const CALL_MS = 10_000;
|
|
15
|
+
const MAX_BODY = 1_000_000;
|
|
16
|
+
const MAX_WINDOWS = 32;
|
|
17
|
+
const bounded = (value, size) => (typeof value === 'string' ? Array.from(value).slice(0, size).join('') : '');
|
|
18
|
+
/** The script a window loads: the page helper plus a loop that pulls operations from the bridge. */
|
|
19
|
+
export function bridgeScript(base, token) {
|
|
20
|
+
const helper = PAGE_SCRIPT.replace('__ARGS__', () => '__op');
|
|
21
|
+
return `(() => {
|
|
22
|
+
if (window.__harnessPageNotesBridge) return
|
|
23
|
+
window.__harnessPageNotesBridge = true
|
|
24
|
+
const base = ${JSON.stringify(base)}
|
|
25
|
+
const token = ${JSON.stringify(token)}
|
|
26
|
+
const call = (__op) => ${helper}
|
|
27
|
+
let windowId = null
|
|
28
|
+
const post = (path, body) => fetch(base + path, { method: 'POST', headers: { 'content-type': 'text/plain' }, body: JSON.stringify({ token, ...body }) })
|
|
29
|
+
.then(response => response.ok ? response.json() : Promise.reject(new Error(String(response.status))))
|
|
30
|
+
const pause = ms => new Promise(resolve => setTimeout(resolve, ms))
|
|
31
|
+
const loop = async () => {
|
|
32
|
+
for (;;) {
|
|
33
|
+
try {
|
|
34
|
+
if (!windowId) windowId = (await post('/register', { url: location.href, title: document.title })).windowId
|
|
35
|
+
const query = '?token=' + encodeURIComponent(token) + '&window=' + encodeURIComponent(windowId) + '&url=' + encodeURIComponent(location.href.slice(0, 2000)) + '&title=' + encodeURIComponent(document.title.slice(0, 200))
|
|
36
|
+
const response = await fetch(base + '/poll' + query)
|
|
37
|
+
if (response.status === 410) { windowId = null; continue }
|
|
38
|
+
if (response.status !== 200) { if (response.status !== 204) await pause(2000); continue }
|
|
39
|
+
const { id, operation } = await response.json()
|
|
40
|
+
let reply
|
|
41
|
+
try { reply = call(operation) } catch (error) { reply = JSON.stringify({ ok: false, error: String(error && error.message || error) }) }
|
|
42
|
+
await post('/reply', { window: windowId, id, reply })
|
|
43
|
+
} catch {
|
|
44
|
+
await pause(2000)
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', loop, { once: true })
|
|
49
|
+
else loop()
|
|
50
|
+
})()
|
|
51
|
+
`;
|
|
52
|
+
}
|
|
53
|
+
export class NotesBridge {
|
|
54
|
+
options;
|
|
55
|
+
server = null;
|
|
56
|
+
starting = null;
|
|
57
|
+
info = null;
|
|
58
|
+
windows = new Map();
|
|
59
|
+
nextWindow = 1;
|
|
60
|
+
nextCall = 1;
|
|
61
|
+
constructor(options) {
|
|
62
|
+
this.options = options;
|
|
63
|
+
}
|
|
64
|
+
settingsFile() {
|
|
65
|
+
return join(this.options.stateDir, 'notes-bridge.json');
|
|
66
|
+
}
|
|
67
|
+
/** Starts the bridge (once) on the port and token kept from last time, so the app's snippet stays valid. */
|
|
68
|
+
start() {
|
|
69
|
+
if (this.info)
|
|
70
|
+
return Promise.resolve(this.info);
|
|
71
|
+
this.starting ??= this.listen().finally(() => { this.starting = null; });
|
|
72
|
+
return this.starting;
|
|
73
|
+
}
|
|
74
|
+
async listen() {
|
|
75
|
+
const settingsFile = this.settingsFile();
|
|
76
|
+
if (existsSync(settingsFile))
|
|
77
|
+
chmodSync(settingsFile, 0o600);
|
|
78
|
+
const saved = existsSync(settingsFile) ? readJson(settingsFile) : null;
|
|
79
|
+
const token = typeof saved?.token === 'string' && /^[a-f0-9]{48}$/.test(saved.token) ? saved.token : randomBytes(24).toString('hex');
|
|
80
|
+
const wanted = this.options.port ?? (Number.isSafeInteger(saved?.port) ? saved.port : 0);
|
|
81
|
+
const server = createServer((request, response) => void this.handle(request, response, token));
|
|
82
|
+
const port = await new Promise((resolve, reject) => {
|
|
83
|
+
const attempt = (port, retry) => {
|
|
84
|
+
server.once('error', (error) => {
|
|
85
|
+
if (retry && error.code === 'EADDRINUSE')
|
|
86
|
+
attempt(0, false);
|
|
87
|
+
else
|
|
88
|
+
reject(error);
|
|
89
|
+
});
|
|
90
|
+
server.listen(port, '127.0.0.1', () => resolve(server.address().port));
|
|
91
|
+
};
|
|
92
|
+
attempt(wanted, wanted !== 0);
|
|
93
|
+
});
|
|
94
|
+
this.server = server;
|
|
95
|
+
writeJson(settingsFile, { port, token });
|
|
96
|
+
chmodSync(settingsFile, 0o600);
|
|
97
|
+
const base = `http://127.0.0.1:${port}`;
|
|
98
|
+
const scriptUrl = `${base}/page-notes.js?token=${token}`;
|
|
99
|
+
this.info = {
|
|
100
|
+
port,
|
|
101
|
+
token,
|
|
102
|
+
scriptUrl,
|
|
103
|
+
snippet: `<script src="${scriptUrl}"></script>`
|
|
104
|
+
};
|
|
105
|
+
return this.info;
|
|
106
|
+
}
|
|
107
|
+
list() {
|
|
108
|
+
this.sweep();
|
|
109
|
+
return [...this.windows.values()].map(({ id, url, title, lastSeen }) => ({ id, url, title, lastSeen }));
|
|
110
|
+
}
|
|
111
|
+
/** Sends one helper operation to a window and waits for its JSON reply. */
|
|
112
|
+
call(windowId, operation) {
|
|
113
|
+
this.sweep();
|
|
114
|
+
const live = this.windows.get(windowId);
|
|
115
|
+
if (!live)
|
|
116
|
+
return Promise.reject(new Error('That app window is no longer connected.'));
|
|
117
|
+
return new Promise((resolve, reject) => {
|
|
118
|
+
const id = String(this.nextCall++);
|
|
119
|
+
const pending = {
|
|
120
|
+
id, operation, resolve, reject,
|
|
121
|
+
timer: setTimeout(() => {
|
|
122
|
+
live.sent.delete(id);
|
|
123
|
+
live.queue = live.queue.filter((entry) => entry !== pending);
|
|
124
|
+
reject(new Error('The app window did not answer.'));
|
|
125
|
+
}, CALL_MS)
|
|
126
|
+
};
|
|
127
|
+
live.queue.push(pending);
|
|
128
|
+
this.flush(live);
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
flush(live) {
|
|
132
|
+
if (!live.waiter || !live.queue.length)
|
|
133
|
+
return;
|
|
134
|
+
const pending = live.queue.shift();
|
|
135
|
+
live.sent.set(pending.id, pending);
|
|
136
|
+
const waiter = live.waiter;
|
|
137
|
+
live.waiter = null;
|
|
138
|
+
if (live.waiterTimer)
|
|
139
|
+
clearTimeout(live.waiterTimer);
|
|
140
|
+
live.waiterTimer = null;
|
|
141
|
+
this.json(waiter, 200, { id: pending.id, operation: pending.operation });
|
|
142
|
+
}
|
|
143
|
+
sweep() {
|
|
144
|
+
const now = Date.now();
|
|
145
|
+
for (const live of this.windows.values()) {
|
|
146
|
+
if (now - live.lastSeen <= GONE_MS || live.waiter)
|
|
147
|
+
continue;
|
|
148
|
+
this.windows.delete(live.id);
|
|
149
|
+
for (const pending of [...live.queue, ...live.sent.values()]) {
|
|
150
|
+
clearTimeout(pending.timer);
|
|
151
|
+
pending.reject(new Error('That app window is no longer connected.'));
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
json(response, status, body) {
|
|
156
|
+
response.writeHead(status, { 'content-type': 'application/json', 'access-control-allow-origin': '*', 'cache-control': 'no-store' });
|
|
157
|
+
response.end(body === undefined ? undefined : JSON.stringify(body));
|
|
158
|
+
}
|
|
159
|
+
async body(request) {
|
|
160
|
+
let size = 0;
|
|
161
|
+
const chunks = [];
|
|
162
|
+
for await (const chunk of request) {
|
|
163
|
+
size += chunk.length;
|
|
164
|
+
if (size > MAX_BODY)
|
|
165
|
+
return null;
|
|
166
|
+
chunks.push(chunk);
|
|
167
|
+
}
|
|
168
|
+
try {
|
|
169
|
+
const value = JSON.parse(Buffer.concat(chunks).toString('utf8'));
|
|
170
|
+
return value && typeof value === 'object' ? value : null;
|
|
171
|
+
}
|
|
172
|
+
catch {
|
|
173
|
+
return null;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
async handle(request, response, token) {
|
|
177
|
+
try {
|
|
178
|
+
const port = this.server?.address()?.port;
|
|
179
|
+
// Refuse other host names, so a web page cannot reach the bridge by DNS rebinding.
|
|
180
|
+
if (request.headers.host !== `127.0.0.1:${port}` && request.headers.host !== `localhost:${port}`)
|
|
181
|
+
return this.json(response, 421);
|
|
182
|
+
const url = new URL(request.url ?? '/', `http://127.0.0.1:${port}`);
|
|
183
|
+
if (request.method === 'OPTIONS')
|
|
184
|
+
return this.json(response, 204);
|
|
185
|
+
if (request.method === 'GET' && url.pathname === '/page-notes.js') {
|
|
186
|
+
if (url.searchParams.get('token') !== token)
|
|
187
|
+
return this.json(response, 403);
|
|
188
|
+
response.writeHead(200, { 'content-type': 'application/javascript; charset=utf-8', 'access-control-allow-origin': '*', 'cache-control': 'no-store' });
|
|
189
|
+
response.end(bridgeScript(`http://127.0.0.1:${port}`, token));
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
if (request.method === 'GET' && url.pathname === '/poll') {
|
|
193
|
+
if (url.searchParams.get('token') !== token)
|
|
194
|
+
return this.json(response, 403);
|
|
195
|
+
const live = this.windows.get(url.searchParams.get('window') ?? '');
|
|
196
|
+
if (!live)
|
|
197
|
+
return this.json(response, 410);
|
|
198
|
+
live.lastSeen = Date.now();
|
|
199
|
+
live.url = bounded(url.searchParams.get('url'), 2000) || live.url;
|
|
200
|
+
live.title = bounded(url.searchParams.get('title'), 200);
|
|
201
|
+
if (live.waiter)
|
|
202
|
+
this.json(live.waiter, 204);
|
|
203
|
+
live.waiter = response;
|
|
204
|
+
live.waiterTimer = setTimeout(() => {
|
|
205
|
+
if (live.waiter !== response)
|
|
206
|
+
return;
|
|
207
|
+
live.waiter = null;
|
|
208
|
+
this.json(response, 204);
|
|
209
|
+
}, POLL_MS);
|
|
210
|
+
response.on('close', () => { if (live.waiter === response)
|
|
211
|
+
live.waiter = null; });
|
|
212
|
+
this.flush(live);
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
if (request.method === 'POST' && (url.pathname === '/register' || url.pathname === '/reply')) {
|
|
216
|
+
const body = await this.body(request);
|
|
217
|
+
if (!body || body.token !== token)
|
|
218
|
+
return this.json(response, 403);
|
|
219
|
+
if (url.pathname === '/register') {
|
|
220
|
+
this.sweep();
|
|
221
|
+
if (this.windows.size >= MAX_WINDOWS)
|
|
222
|
+
return this.json(response, 429);
|
|
223
|
+
const id = `a${this.nextWindow++}`;
|
|
224
|
+
this.windows.set(id, { id, url: bounded(body.url, 2000), title: bounded(body.title, 200), lastSeen: Date.now(), queue: [], sent: new Map(), waiter: null, waiterTimer: null });
|
|
225
|
+
return this.json(response, 200, { windowId: id });
|
|
226
|
+
}
|
|
227
|
+
const live = this.windows.get(typeof body.window === 'string' ? body.window : '');
|
|
228
|
+
const pending = live?.sent.get(typeof body.id === 'string' ? body.id : '');
|
|
229
|
+
if (!live || !pending)
|
|
230
|
+
return this.json(response, 410);
|
|
231
|
+
live.sent.delete(pending.id);
|
|
232
|
+
clearTimeout(pending.timer);
|
|
233
|
+
pending.resolve(body.reply);
|
|
234
|
+
return this.json(response, 200, {});
|
|
235
|
+
}
|
|
236
|
+
this.json(response, 404);
|
|
237
|
+
}
|
|
238
|
+
catch {
|
|
239
|
+
if (!response.headersSent)
|
|
240
|
+
this.json(response, 500);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
async close() {
|
|
244
|
+
for (const live of this.windows.values()) {
|
|
245
|
+
if (live.waiter)
|
|
246
|
+
this.json(live.waiter, 204);
|
|
247
|
+
if (live.waiterTimer)
|
|
248
|
+
clearTimeout(live.waiterTimer);
|
|
249
|
+
for (const pending of [...live.queue, ...live.sent.values()]) {
|
|
250
|
+
clearTimeout(pending.timer);
|
|
251
|
+
pending.reject(new Error('The page bridge closed.'));
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
this.windows.clear();
|
|
255
|
+
const server = this.server;
|
|
256
|
+
this.server = null;
|
|
257
|
+
this.info = null;
|
|
258
|
+
if (server) {
|
|
259
|
+
const closed = new Promise((resolve) => server.close(() => resolve()));
|
|
260
|
+
server.closeAllConnections();
|
|
261
|
+
await closed;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|