chore: reformat codebase with oxfmt
Apply prettier-compatible formatting across the repo to match the incoming oxfmt configuration. Mechanical reformat only — no logic changes. Covers YAML quote normalization, package.json key sorting, Markdown/TOML formatting, and minor TS whitespace tweaks.
This commit is contained in:
@@ -92,9 +92,7 @@ type DaemonClientConfig = {
|
||||
};
|
||||
|
||||
async function loadDaemonClientConstructor(): Promise<
|
||||
new (
|
||||
config: DaemonClientConfig,
|
||||
) => DaemonClientInstance
|
||||
new (config: DaemonClientConfig) => DaemonClientInstance
|
||||
> {
|
||||
const repoRoot = path.resolve(__dirname, "../../../../");
|
||||
const moduleUrl = pathToFileURL(
|
||||
|
||||
@@ -81,9 +81,7 @@ type ArchiveTabDaemonClientConfig = {
|
||||
};
|
||||
|
||||
async function loadDaemonClientConstructor(): Promise<
|
||||
new (
|
||||
config: ArchiveTabDaemonClientConfig,
|
||||
) => ArchiveTabDaemonClient
|
||||
new (config: ArchiveTabDaemonClientConfig) => ArchiveTabDaemonClient
|
||||
> {
|
||||
const repoRoot = path.resolve(__dirname, "../../../../");
|
||||
const moduleUrl = pathToFileURL(
|
||||
|
||||
@@ -49,9 +49,7 @@ function getDaemonWsUrl(): string {
|
||||
}
|
||||
|
||||
async function loadDaemonClientConstructor(): Promise<
|
||||
new (
|
||||
config: NewWorkspaceDaemonClientConfig,
|
||||
) => NewWorkspaceDaemonClient
|
||||
new (config: NewWorkspaceDaemonClientConfig) => NewWorkspaceDaemonClient
|
||||
> {
|
||||
const repoRoot = path.resolve(__dirname, "../../../../");
|
||||
const moduleUrl = pathToFileURL(
|
||||
|
||||
@@ -63,9 +63,7 @@ type TerminalPerfDaemonClientConfig = {
|
||||
};
|
||||
|
||||
async function loadDaemonClientConstructor(): Promise<
|
||||
new (
|
||||
config: TerminalPerfDaemonClientConfig,
|
||||
) => TerminalPerfDaemonClient
|
||||
new (config: TerminalPerfDaemonClientConfig) => TerminalPerfDaemonClient
|
||||
> {
|
||||
const repoRoot = path.resolve(__dirname, "../../../../");
|
||||
const moduleUrl = pathToFileURL(
|
||||
|
||||
@@ -24,4 +24,3 @@ appId: sh.paseo
|
||||
commands:
|
||||
- tapOn:
|
||||
point: "50%,20%"
|
||||
|
||||
|
||||
@@ -3,4 +3,3 @@ appId: sh.paseo
|
||||
- launchApp:
|
||||
clearState: true
|
||||
- runFlow: flows/dev-client.yaml
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"name": "@getpaseo/app",
|
||||
"main": "index.ts",
|
||||
"version": "0.1.62",
|
||||
"private": true,
|
||||
"main": "index.ts",
|
||||
"scripts": {
|
||||
"start": "APP_VARIANT=development expo start",
|
||||
"reset-project": "node ./scripts/reset-project.js",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<!DOCTYPE html>
|
||||
<!doctype html>
|
||||
<html lang="%LANG_ISO_CODE%">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
@@ -21,8 +21,9 @@
|
||||
}
|
||||
/* Prevent white flash before React mounts */
|
||||
@media (prefers-color-scheme: dark) {
|
||||
html, body {
|
||||
background-color: #181B1A;
|
||||
html,
|
||||
body {
|
||||
background-color: #181b1a;
|
||||
}
|
||||
}
|
||||
/* These styles make the root element full-height */
|
||||
@@ -38,17 +39,17 @@
|
||||
input,
|
||||
textarea,
|
||||
select,
|
||||
[role='button'],
|
||||
[role='link'],
|
||||
[role='textbox'],
|
||||
[role='combobox'],
|
||||
[role='tab'],
|
||||
[role='switch'],
|
||||
[role='checkbox'],
|
||||
[role='slider'],
|
||||
[role='menuitem'],
|
||||
[role="button"],
|
||||
[role="link"],
|
||||
[role="textbox"],
|
||||
[role="combobox"],
|
||||
[role="tab"],
|
||||
[role="switch"],
|
||||
[role="checkbox"],
|
||||
[role="slider"],
|
||||
[role="menuitem"],
|
||||
[tabindex],
|
||||
[contenteditable='true'] {
|
||||
[contenteditable="true"] {
|
||||
-webkit-app-region: no-drag !important;
|
||||
}
|
||||
|
||||
@@ -59,7 +60,7 @@
|
||||
outline: none;
|
||||
}
|
||||
*:focus-visible {
|
||||
outline: 2px solid #20744A;
|
||||
outline: 2px solid #20744a;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -7,9 +7,8 @@ let attachmentStorePromise: Promise<AttachmentStore> | null = null;
|
||||
async function createAttachmentStore(): Promise<AttachmentStore> {
|
||||
if (isWeb) {
|
||||
if (isElectronRuntime()) {
|
||||
const { createDesktopAttachmentStore } = await import(
|
||||
"../desktop/attachments/desktop-attachment-store"
|
||||
);
|
||||
const { createDesktopAttachmentStore } =
|
||||
await import("../desktop/attachments/desktop-attachment-store");
|
||||
return createDesktopAttachmentStore();
|
||||
}
|
||||
|
||||
|
||||
@@ -82,13 +82,7 @@ vi.mock("expo-image", () => ({
|
||||
|
||||
vi.mock("react-native", async () => {
|
||||
const actual = await vi.importActual<Record<string, unknown>>("react-native");
|
||||
const Modal = ({
|
||||
visible = true,
|
||||
children,
|
||||
}: {
|
||||
visible?: boolean;
|
||||
children?: React.ReactNode;
|
||||
}) =>
|
||||
const Modal = ({ visible = true, children }: { visible?: boolean; children?: React.ReactNode }) =>
|
||||
visible ? React.createElement("div", { "data-testid": "lightbox-modal" }, children) : null;
|
||||
return { ...actual, Modal };
|
||||
});
|
||||
|
||||
@@ -285,13 +285,7 @@ vi.mock("expo-image", () => ({
|
||||
|
||||
vi.mock("react-native", async () => {
|
||||
const actual = await vi.importActual<Record<string, unknown>>("react-native");
|
||||
const Modal = ({
|
||||
visible = true,
|
||||
children,
|
||||
}: {
|
||||
visible?: boolean;
|
||||
children?: React.ReactNode;
|
||||
}) =>
|
||||
const Modal = ({ visible = true, children }: { visible?: boolean; children?: React.ReactNode }) =>
|
||||
visible ? React.createElement("div", { "data-testid": "lightbox-modal" }, children) : null;
|
||||
return { ...actual, Modal };
|
||||
});
|
||||
|
||||
@@ -1954,8 +1954,8 @@ export function SidebarWorkspaceList({
|
||||
select: toProjectIconDataUri,
|
||||
enabled: Boolean(
|
||||
getHostRuntimeStore().getClient(request.serverId) &&
|
||||
isHostRuntimeConnected(getHostRuntimeStore().getSnapshot(request.serverId)) &&
|
||||
request.cwd,
|
||||
isHostRuntimeConnected(getHostRuntimeStore().getSnapshot(request.serverId)) &&
|
||||
request.cwd,
|
||||
),
|
||||
staleTime: Infinity,
|
||||
gcTime: 1000 * 60 * 60,
|
||||
|
||||
@@ -134,18 +134,17 @@ interface SplitNodeViewProps extends Omit<SplitContainerProps, "layout"> {
|
||||
tabDropPreview: TabDropPreview | null;
|
||||
}
|
||||
|
||||
interface SplitPaneViewProps
|
||||
extends Omit<
|
||||
SplitNodeViewProps,
|
||||
| "node"
|
||||
| "workspaceKey"
|
||||
| "focusedPaneId"
|
||||
| "activeDragTabId"
|
||||
| "showDropZones"
|
||||
| "dropPreview"
|
||||
| "onMoveTabToPane"
|
||||
| "onResizeSplit"
|
||||
> {
|
||||
interface SplitPaneViewProps extends Omit<
|
||||
SplitNodeViewProps,
|
||||
| "node"
|
||||
| "workspaceKey"
|
||||
| "focusedPaneId"
|
||||
| "activeDragTabId"
|
||||
| "showDropZones"
|
||||
| "dropPreview"
|
||||
| "onMoveTabToPane"
|
||||
| "onResizeSplit"
|
||||
> {
|
||||
pane: SplitPane;
|
||||
uiTabs: WorkspaceTab[];
|
||||
isFocused: boolean;
|
||||
|
||||
@@ -237,23 +237,24 @@ describe("providers snapshot hook cache scope", () => {
|
||||
it.each([
|
||||
{ name: "missing", entries: [] },
|
||||
{ name: "loading", entries: [codexEntry("loading")] },
|
||||
])("ensures a selected provider snapshot on selector open when it is $name", async ({
|
||||
entries,
|
||||
}) => {
|
||||
enableProvidersSnapshot();
|
||||
mockClient.getProvidersSnapshot
|
||||
.mockResolvedValueOnce(providersSnapshot(entries))
|
||||
.mockResolvedValueOnce(providersSnapshot([codexEntry("ready", [readyCodexModel])]));
|
||||
])(
|
||||
"ensures a selected provider snapshot on selector open when it is $name",
|
||||
async ({ entries }) => {
|
||||
enableProvidersSnapshot();
|
||||
mockClient.getProvidersSnapshot
|
||||
.mockResolvedValueOnce(providersSnapshot(entries))
|
||||
.mockResolvedValueOnce(providersSnapshot([codexEntry("ready", [readyCodexModel])]));
|
||||
|
||||
const { result } = renderProvidersSnapshotHook();
|
||||
const { result } = renderProvidersSnapshotHook();
|
||||
|
||||
await waitForSnapshotEntries(result, entries);
|
||||
await openSelectorForSelectedProvider(result);
|
||||
await waitForSnapshotReads(2);
|
||||
await waitForSnapshotEntries(result, entries);
|
||||
await openSelectorForSelectedProvider(result);
|
||||
await waitForSnapshotReads(2);
|
||||
|
||||
expect(mockClient.getProvidersSnapshot).toHaveBeenLastCalledWith({});
|
||||
expect(mockClient.refreshProvidersSnapshot).not.toHaveBeenCalled();
|
||||
});
|
||||
expect(mockClient.getProvidersSnapshot).toHaveBeenLastCalledWith({});
|
||||
expect(mockClient.refreshProvidersSnapshot).not.toHaveBeenCalled();
|
||||
},
|
||||
);
|
||||
|
||||
it("does not ensure a selected provider snapshot on selector open when the provider is ready with no models", async () => {
|
||||
enableProvidersSnapshot();
|
||||
|
||||
@@ -51,7 +51,7 @@ export function resolveKeyboardFocusScope(input: {
|
||||
candidates.some((element) =>
|
||||
Boolean(
|
||||
element.closest("[data-testid='command-center-panel']") ||
|
||||
element.closest("[data-testid='command-center-input']"),
|
||||
element.closest("[data-testid='command-center-input']"),
|
||||
),
|
||||
)
|
||||
) {
|
||||
|
||||
@@ -289,23 +289,19 @@ describe("keyboard-shortcuts", () => {
|
||||
},
|
||||
];
|
||||
|
||||
it.each(matchingCases)("$name", ({
|
||||
event,
|
||||
context,
|
||||
action,
|
||||
payload,
|
||||
preventDefault,
|
||||
stopPropagation,
|
||||
}) => {
|
||||
expectShortcutResolution({
|
||||
event,
|
||||
context,
|
||||
action,
|
||||
...(payload !== undefined ? { payload } : {}),
|
||||
...(preventDefault !== undefined ? { preventDefault } : {}),
|
||||
...(stopPropagation !== undefined ? { stopPropagation } : {}),
|
||||
});
|
||||
});
|
||||
it.each(matchingCases)(
|
||||
"$name",
|
||||
({ event, context, action, payload, preventDefault, stopPropagation }) => {
|
||||
expectShortcutResolution({
|
||||
event,
|
||||
context,
|
||||
action,
|
||||
...(payload !== undefined ? { payload } : {}),
|
||||
...(preventDefault !== undefined ? { preventDefault } : {}),
|
||||
...(stopPropagation !== undefined ? { stopPropagation } : {}),
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
const nonMatchingCases: NonMatchingShortcutCase[] = [
|
||||
{
|
||||
|
||||
@@ -491,10 +491,7 @@ export class HostRuntimeController {
|
||||
private probeRequestVersion = 0;
|
||||
private probeCycleInFlight: Promise<void> | null = null;
|
||||
|
||||
constructor(input: {
|
||||
host: HostProfile;
|
||||
deps?: HostRuntimeControllerDeps;
|
||||
}) {
|
||||
constructor(input: { host: HostProfile; deps?: HostRuntimeControllerDeps }) {
|
||||
this.host = input.host;
|
||||
this.deps = input.deps ?? createDefaultDeps();
|
||||
this.connectionMachineState = {
|
||||
@@ -1155,9 +1152,7 @@ export class HostRuntimeStore {
|
||||
private storageLoaded = false;
|
||||
private bootstrapAttempted = false;
|
||||
|
||||
constructor(input?: {
|
||||
deps?: HostRuntimeControllerDeps;
|
||||
}) {
|
||||
constructor(input?: { deps?: HostRuntimeControllerDeps }) {
|
||||
this.deps = input?.deps ?? createDefaultDeps();
|
||||
}
|
||||
|
||||
|
||||
@@ -230,11 +230,11 @@ export function WorkspaceDraftAgentTab({
|
||||
|
||||
const isReadyForPendingAutoSubmit = Boolean(
|
||||
pendingAutoSubmit &&
|
||||
draftInput.isHydrated &&
|
||||
workspaceDirectory &&
|
||||
client &&
|
||||
!isSubmitting &&
|
||||
!composerState.isModelLoading,
|
||||
draftInput.isHydrated &&
|
||||
workspaceDirectory &&
|
||||
client &&
|
||||
!isSubmitting &&
|
||||
!composerState.isModelLoading,
|
||||
);
|
||||
const autoSubmitKeyRef = useRef<string | null>(null);
|
||||
useEffect(() => {
|
||||
|
||||
@@ -36,10 +36,10 @@ export function hasMeaningfulToolCallDetail(detail: ToolCallDetail | undefined):
|
||||
case "search":
|
||||
return Boolean(
|
||||
detail.query.trim().length > 0 ||
|
||||
detail.content ||
|
||||
(detail.filePaths && detail.filePaths.length > 0) ||
|
||||
(detail.webResults && detail.webResults.length > 0) ||
|
||||
(detail.annotations && detail.annotations.length > 0),
|
||||
detail.content ||
|
||||
(detail.filePaths && detail.filePaths.length > 0) ||
|
||||
(detail.webResults && detail.webResults.length > 0) ||
|
||||
(detail.annotations && detail.annotations.length > 0),
|
||||
);
|
||||
case "fetch":
|
||||
return Boolean(detail.url || detail.result || detail.codeText);
|
||||
|
||||
@@ -2,17 +2,17 @@
|
||||
"name": "@getpaseo/cli",
|
||||
"version": "0.1.62",
|
||||
"description": "Paseo CLI - control your AI coding agents from the command line",
|
||||
"type": "module",
|
||||
"bin": {
|
||||
"paseo": "bin/paseo"
|
||||
},
|
||||
"files": [
|
||||
"bin",
|
||||
"dist"
|
||||
],
|
||||
"type": "module",
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"bin": {
|
||||
"paseo": "bin/paseo"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "node -e \"require('node:fs').rmSync('dist',{ recursive: true, force: true })\" && tsc -p tsconfig.json --incremental false",
|
||||
"prepack": "npm run build",
|
||||
|
||||
@@ -3,6 +3,16 @@
|
||||
"version": "0.1.62",
|
||||
"private": true,
|
||||
"description": "Paseo desktop app (Electron wrapper)",
|
||||
"homepage": "https://paseo.sh",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"author": {
|
||||
"name": "Mohamed Boudra",
|
||||
"email": "hello@moboudra.com"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/getpaseo/paseo.git"
|
||||
},
|
||||
"main": "dist/main.js",
|
||||
"scripts": {
|
||||
"build": "npm --prefix ../.. run build:daemon && npm run build:main && electron-builder --config electron-builder.yml",
|
||||
@@ -27,15 +37,5 @@
|
||||
"typescript": "5.9.3",
|
||||
"unzip-crx-3": "^0.2.0",
|
||||
"wait-on": "8.0.5"
|
||||
},
|
||||
"homepage": "https://paseo.sh",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/getpaseo/paseo.git"
|
||||
},
|
||||
"author": {
|
||||
"name": "Mohamed Boudra",
|
||||
"email": "hello@moboudra.com"
|
||||
},
|
||||
"license": "AGPL-3.0-or-later"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,23 +17,23 @@ diverse, inclusive, and healthy community.
|
||||
Examples of behavior that contributes to a positive environment for our
|
||||
community include:
|
||||
|
||||
* Demonstrating empathy and kindness toward other people
|
||||
* Being respectful of differing opinions, viewpoints, and experiences
|
||||
* Giving and gracefully accepting constructive feedback
|
||||
* Accepting responsibility and apologizing to those affected by our mistakes,
|
||||
- Demonstrating empathy and kindness toward other people
|
||||
- Being respectful of differing opinions, viewpoints, and experiences
|
||||
- Giving and gracefully accepting constructive feedback
|
||||
- Accepting responsibility and apologizing to those affected by our mistakes,
|
||||
and learning from the experience
|
||||
* Focusing on what is best not just for us as individuals, but for the
|
||||
- Focusing on what is best not just for us as individuals, but for the
|
||||
overall community
|
||||
|
||||
Examples of unacceptable behavior include:
|
||||
|
||||
* The use of sexualized language or imagery, and sexual attention or
|
||||
- The use of sexualized language or imagery, and sexual attention or
|
||||
advances of any kind
|
||||
* Trolling, insulting or derogatory comments, and personal or political attacks
|
||||
* Public or private harassment
|
||||
* Publishing others' private information, such as a physical or email
|
||||
- Trolling, insulting or derogatory comments, and personal or political attacks
|
||||
- Public or private harassment
|
||||
- Publishing others' private information, such as a physical or email
|
||||
address, without their explicit permission
|
||||
* Other conduct which could reasonably be considered inappropriate in a
|
||||
- Other conduct which could reasonably be considered inappropriate in a
|
||||
professional setting
|
||||
|
||||
## Enforcement Responsibilities
|
||||
@@ -106,7 +106,7 @@ Violating these terms may lead to a permanent ban.
|
||||
### 4. Permanent Ban
|
||||
|
||||
**Community Impact**: Demonstrating a pattern of violation of community
|
||||
standards, including sustained inappropriate behavior, harassment of an
|
||||
standards, including sustained inappropriate behavior, harassment of an
|
||||
individual, or aggression toward or disparagement of classes of individuals.
|
||||
|
||||
**Consequence**: A permanent ban from any sort of public interaction within
|
||||
@@ -125,4 +125,4 @@ enforcement ladder](https://github.com/mozilla/diversity).
|
||||
|
||||
For answers to common questions about this code of conduct, see the FAQ at
|
||||
https://www.contributor-covenant.org/faq. Translations are available at
|
||||
https://www.contributor-covenant.org/translations.
|
||||
https://www.contributor-covenant.org/translations.
|
||||
|
||||
@@ -18,18 +18,20 @@ This example showcases the basic functions of the expo-two-way-audio module:
|
||||
In the `examples/basic-usage` directory:
|
||||
|
||||
1. Install dependencies:
|
||||
```sh
|
||||
npm install
|
||||
```
|
||||
|
||||
```sh
|
||||
npm install
|
||||
```
|
||||
|
||||
2. Run the app:
|
||||
```sh
|
||||
# Run iOS
|
||||
npm run ios
|
||||
|
||||
# Run Android
|
||||
npm run android
|
||||
```
|
||||
```sh
|
||||
# Run iOS
|
||||
npm run ios
|
||||
|
||||
# Run Android
|
||||
npm run android
|
||||
```
|
||||
|
||||
## Notes
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"name": "expo-two-way-audio-example",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"main": "expo/AppEntry.js",
|
||||
"scripts": {
|
||||
"start": "expo start",
|
||||
@@ -11,17 +12,16 @@
|
||||
"dependencies": {
|
||||
"buffer": "^6.0.3",
|
||||
"expo": "^52.0.0",
|
||||
"react": "18.3.1",
|
||||
"react-native": "0.76.2",
|
||||
"expo-build-properties": "~0.13.1",
|
||||
"expo-dev-client": "~5.0.3",
|
||||
"expo-build-properties": "~0.13.1"
|
||||
"react": "18.3.1",
|
||||
"react-native": "0.76.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.20.0",
|
||||
"@types/react": "~18.3.12",
|
||||
"typescript": "^5.1.3"
|
||||
},
|
||||
"private": true,
|
||||
"expo": {
|
||||
"autolinking": {
|
||||
"nativeModulesDir": "../.."
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"name": "expo-two-way-audio-flow-example",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"main": "expo/AppEntry.js",
|
||||
"scripts": {
|
||||
"start": "expo start",
|
||||
@@ -25,7 +26,6 @@
|
||||
"@types/react": "~18.3.12",
|
||||
"typescript": "^5.1.3"
|
||||
},
|
||||
"private": true,
|
||||
"expo": {
|
||||
"autolinking": {
|
||||
"nativeModulesDir": "../.."
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
{
|
||||
"name": "@getpaseo/highlight",
|
||||
"version": "0.1.62",
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"type": "module",
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"main": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
"exports": {
|
||||
@@ -14,9 +14,9 @@
|
||||
"default": "./dist/index.js"
|
||||
}
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "node -e \"require('node:fs').rmSync('dist',{ recursive: true, force: true })\" && tsc -p tsconfig.json --incremental false",
|
||||
"prepack": "npm run build",
|
||||
|
||||
@@ -2,14 +2,11 @@
|
||||
"name": "@getpaseo/relay",
|
||||
"version": "0.1.62",
|
||||
"description": "Paseo relay for bridging daemon and client connections",
|
||||
"type": "module",
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"types": "./dist/index.d.ts",
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"type": "module",
|
||||
"types": "./dist/index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.ts",
|
||||
@@ -30,6 +27,9 @@
|
||||
"default": "./src/cloudflare-adapter.ts"
|
||||
}
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "node -e \"require('node:fs').rmSync('dist',{ recursive: true, force: true })\" && tsc -p tsconfig.json --incremental false",
|
||||
"prepack": "npm run build",
|
||||
|
||||
+146
-139
@@ -199,169 +199,176 @@ async function stopRelayProcess(relayProcess: ChildProcess): Promise<void> {
|
||||
}
|
||||
}, SHUTDOWN_TIMEOUT_MS);
|
||||
|
||||
it("full flow: daemon and client exchange encrypted messages through relay", {
|
||||
timeout: 90_000,
|
||||
}, async () => {
|
||||
const serverId = "test-session-" + Date.now();
|
||||
const connectionId = "clt_test_" + Date.now() + "_" + Math.random().toString(36).slice(2);
|
||||
it(
|
||||
"full flow: daemon and client exchange encrypted messages through relay",
|
||||
{
|
||||
timeout: 90_000,
|
||||
},
|
||||
async () => {
|
||||
const serverId = "test-session-" + Date.now();
|
||||
const connectionId = "clt_test_" + Date.now() + "_" + Math.random().toString(36).slice(2);
|
||||
|
||||
// === DAEMON SIDE ===
|
||||
// Generate keypair (public key goes in QR)
|
||||
const daemonKeyPair = await generateKeyPair();
|
||||
const daemonPubKeyB64 = await exportPublicKey(daemonKeyPair.publicKey);
|
||||
// === DAEMON SIDE ===
|
||||
// Generate keypair (public key goes in QR)
|
||||
const daemonKeyPair = await generateKeyPair();
|
||||
const daemonPubKeyB64 = await exportPublicKey(daemonKeyPair.publicKey);
|
||||
|
||||
// QR would contain: { serverId, daemonPubKeyB64, relay: { endpoint } }
|
||||
// QR would contain: { serverId, daemonPubKeyB64, relay: { endpoint } }
|
||||
|
||||
// Daemon connects to relay as "server" control role
|
||||
const daemonControlWs = new WebSocket(
|
||||
`ws://127.0.0.1:${relayPort}/ws?serverId=${serverId}&role=server&v=2`,
|
||||
);
|
||||
// Daemon connects to relay as "server" control role
|
||||
const daemonControlWs = new WebSocket(
|
||||
`ws://127.0.0.1:${relayPort}/ws?serverId=${serverId}&role=server&v=2`,
|
||||
);
|
||||
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
daemonControlWs.on("open", resolve);
|
||||
daemonControlWs.on("error", reject);
|
||||
});
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
daemonControlWs.on("open", resolve);
|
||||
daemonControlWs.on("error", reject);
|
||||
});
|
||||
|
||||
// === CLIENT SIDE ===
|
||||
// Client scans QR, gets daemon's public key and session ID
|
||||
// Client generates own keypair
|
||||
const clientKeyPair = await generateKeyPair();
|
||||
const clientPubKeyB64 = await exportPublicKey(clientKeyPair.publicKey);
|
||||
// === CLIENT SIDE ===
|
||||
// Client scans QR, gets daemon's public key and session ID
|
||||
// Client generates own keypair
|
||||
const clientKeyPair = await generateKeyPair();
|
||||
const clientPubKeyB64 = await exportPublicKey(clientKeyPair.publicKey);
|
||||
|
||||
// Client imports daemon's public key and derives shared secret
|
||||
const daemonPubKeyOnClient = await importPublicKey(daemonPubKeyB64);
|
||||
const clientSharedKey = await deriveSharedKey(clientKeyPair.secretKey, daemonPubKeyOnClient);
|
||||
// Client imports daemon's public key and derives shared secret
|
||||
const daemonPubKeyOnClient = await importPublicKey(daemonPubKeyB64);
|
||||
const clientSharedKey = await deriveSharedKey(clientKeyPair.secretKey, daemonPubKeyOnClient);
|
||||
|
||||
const waitForClientSeen = new Promise<void>((resolve, reject) => {
|
||||
const timeout = setTimeout(() => reject(new Error("timed out waiting for connected")), 5000);
|
||||
const onMessage = (raw: unknown) => {
|
||||
try {
|
||||
const text =
|
||||
typeof raw === "string"
|
||||
? raw
|
||||
: raw && typeof (raw as any).toString === "function"
|
||||
? (raw as any).toString()
|
||||
: "";
|
||||
const msg = JSON.parse(text);
|
||||
if (msg?.type === "connected" && msg.connectionId === connectionId) {
|
||||
clearTimeout(timeout);
|
||||
daemonControlWs.off("message", onMessage);
|
||||
resolve();
|
||||
return;
|
||||
const waitForClientSeen = new Promise<void>((resolve, reject) => {
|
||||
const timeout = setTimeout(
|
||||
() => reject(new Error("timed out waiting for connected")),
|
||||
5000,
|
||||
);
|
||||
const onMessage = (raw: unknown) => {
|
||||
try {
|
||||
const text =
|
||||
typeof raw === "string"
|
||||
? raw
|
||||
: raw && typeof (raw as any).toString === "function"
|
||||
? (raw as any).toString()
|
||||
: "";
|
||||
const msg = JSON.parse(text);
|
||||
if (msg?.type === "connected" && msg.connectionId === connectionId) {
|
||||
clearTimeout(timeout);
|
||||
daemonControlWs.off("message", onMessage);
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
if (
|
||||
msg?.type === "sync" &&
|
||||
Array.isArray(msg.connectionIds) &&
|
||||
msg.connectionIds.includes(connectionId)
|
||||
) {
|
||||
clearTimeout(timeout);
|
||||
daemonControlWs.off("message", onMessage);
|
||||
resolve();
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
if (
|
||||
msg?.type === "sync" &&
|
||||
Array.isArray(msg.connectionIds) &&
|
||||
msg.connectionIds.includes(connectionId)
|
||||
) {
|
||||
clearTimeout(timeout);
|
||||
daemonControlWs.off("message", onMessage);
|
||||
resolve();
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
};
|
||||
daemonControlWs.on("message", onMessage);
|
||||
});
|
||||
};
|
||||
daemonControlWs.on("message", onMessage);
|
||||
});
|
||||
|
||||
// Client connects to relay as "client" role (must include connectionId)
|
||||
const clientWs = new WebSocket(
|
||||
`ws://127.0.0.1:${relayPort}/ws?serverId=${serverId}&role=client&connectionId=${connectionId}&v=2`,
|
||||
);
|
||||
// Client connects to relay as "client" role (must include connectionId)
|
||||
const clientWs = new WebSocket(
|
||||
`ws://127.0.0.1:${relayPort}/ws?serverId=${serverId}&role=client&connectionId=${connectionId}&v=2`,
|
||||
);
|
||||
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
clientWs.on("open", resolve);
|
||||
clientWs.on("error", reject);
|
||||
});
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
clientWs.on("open", resolve);
|
||||
clientWs.on("error", reject);
|
||||
});
|
||||
|
||||
await waitForClientSeen;
|
||||
await waitForClientSeen;
|
||||
|
||||
const daemonWs = new WebSocket(
|
||||
`ws://127.0.0.1:${relayPort}/ws?serverId=${serverId}&role=server&connectionId=${connectionId}&v=2`,
|
||||
);
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
daemonWs.on("open", resolve);
|
||||
daemonWs.on("error", reject);
|
||||
});
|
||||
const daemonWs = new WebSocket(
|
||||
`ws://127.0.0.1:${relayPort}/ws?serverId=${serverId}&role=server&connectionId=${connectionId}&v=2`,
|
||||
);
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
daemonWs.on("open", resolve);
|
||||
daemonWs.on("error", reject);
|
||||
});
|
||||
|
||||
// Client sends hello with its public key (this message is NOT encrypted - it's the handshake)
|
||||
const helloMsg = JSON.stringify({ type: "hello", key: clientPubKeyB64 });
|
||||
clientWs.send(helloMsg);
|
||||
// Client sends hello with its public key (this message is NOT encrypted - it's the handshake)
|
||||
const helloMsg = JSON.stringify({ type: "hello", key: clientPubKeyB64 });
|
||||
clientWs.send(helloMsg);
|
||||
|
||||
// === DAEMON RECEIVES HELLO ===
|
||||
const daemonReceivedHello = await new Promise<string>((resolve) => {
|
||||
daemonWs.once("message", (data) => resolve(data.toString()));
|
||||
});
|
||||
// === DAEMON RECEIVES HELLO ===
|
||||
const daemonReceivedHello = await new Promise<string>((resolve) => {
|
||||
daemonWs.once("message", (data) => resolve(data.toString()));
|
||||
});
|
||||
|
||||
const hello = JSON.parse(daemonReceivedHello);
|
||||
expect(hello.type).toBe("hello");
|
||||
expect(hello.key).toBe(clientPubKeyB64);
|
||||
const hello = JSON.parse(daemonReceivedHello);
|
||||
expect(hello.type).toBe("hello");
|
||||
expect(hello.key).toBe(clientPubKeyB64);
|
||||
|
||||
// Daemon imports client's public key and derives shared secret
|
||||
const clientPubKeyOnDaemon = await importPublicKey(hello.key);
|
||||
const daemonSharedKey = await deriveSharedKey(daemonKeyPair.secretKey, clientPubKeyOnDaemon);
|
||||
// Daemon imports client's public key and derives shared secret
|
||||
const clientPubKeyOnDaemon = await importPublicKey(hello.key);
|
||||
const daemonSharedKey = await deriveSharedKey(daemonKeyPair.secretKey, clientPubKeyOnDaemon);
|
||||
|
||||
// === VERIFY BOTH HAVE SAME KEY - Exchange encrypted messages ===
|
||||
// === VERIFY BOTH HAVE SAME KEY - Exchange encrypted messages ===
|
||||
|
||||
// Daemon sends encrypted "ready" message
|
||||
const readyPlaintext = JSON.stringify({ type: "ready" });
|
||||
const readyCiphertext = await encrypt(daemonSharedKey, readyPlaintext);
|
||||
daemonWs.send(Buffer.from(readyCiphertext));
|
||||
// Daemon sends encrypted "ready" message
|
||||
const readyPlaintext = JSON.stringify({ type: "ready" });
|
||||
const readyCiphertext = await encrypt(daemonSharedKey, readyPlaintext);
|
||||
daemonWs.send(Buffer.from(readyCiphertext));
|
||||
|
||||
// Client receives and decrypts
|
||||
const clientReceivedReady = await new Promise<Buffer>((resolve) => {
|
||||
clientWs.once("message", (data) => resolve(data as Buffer));
|
||||
});
|
||||
const decryptedReady = await decrypt(
|
||||
clientSharedKey,
|
||||
clientReceivedReady.buffer.slice(
|
||||
clientReceivedReady.byteOffset,
|
||||
clientReceivedReady.byteOffset + clientReceivedReady.byteLength,
|
||||
),
|
||||
);
|
||||
expect(JSON.parse(decryptedReady as string)).toEqual({ type: "ready" });
|
||||
// Client receives and decrypts
|
||||
const clientReceivedReady = await new Promise<Buffer>((resolve) => {
|
||||
clientWs.once("message", (data) => resolve(data as Buffer));
|
||||
});
|
||||
const decryptedReady = await decrypt(
|
||||
clientSharedKey,
|
||||
clientReceivedReady.buffer.slice(
|
||||
clientReceivedReady.byteOffset,
|
||||
clientReceivedReady.byteOffset + clientReceivedReady.byteLength,
|
||||
),
|
||||
);
|
||||
expect(JSON.parse(decryptedReady as string)).toEqual({ type: "ready" });
|
||||
|
||||
// Client sends encrypted message
|
||||
const clientMessage = "Hello from client!";
|
||||
const clientCiphertext = await encrypt(clientSharedKey, clientMessage);
|
||||
clientWs.send(Buffer.from(clientCiphertext));
|
||||
// Client sends encrypted message
|
||||
const clientMessage = "Hello from client!";
|
||||
const clientCiphertext = await encrypt(clientSharedKey, clientMessage);
|
||||
clientWs.send(Buffer.from(clientCiphertext));
|
||||
|
||||
// Daemon receives and decrypts
|
||||
const daemonReceivedMsg = await new Promise<Buffer>((resolve) => {
|
||||
daemonWs.once("message", (data) => resolve(data as Buffer));
|
||||
});
|
||||
const decryptedClientMsg = await decrypt(
|
||||
daemonSharedKey,
|
||||
daemonReceivedMsg.buffer.slice(
|
||||
daemonReceivedMsg.byteOffset,
|
||||
daemonReceivedMsg.byteOffset + daemonReceivedMsg.byteLength,
|
||||
),
|
||||
);
|
||||
expect(decryptedClientMsg).toBe(clientMessage);
|
||||
// Daemon receives and decrypts
|
||||
const daemonReceivedMsg = await new Promise<Buffer>((resolve) => {
|
||||
daemonWs.once("message", (data) => resolve(data as Buffer));
|
||||
});
|
||||
const decryptedClientMsg = await decrypt(
|
||||
daemonSharedKey,
|
||||
daemonReceivedMsg.buffer.slice(
|
||||
daemonReceivedMsg.byteOffset,
|
||||
daemonReceivedMsg.byteOffset + daemonReceivedMsg.byteLength,
|
||||
),
|
||||
);
|
||||
expect(decryptedClientMsg).toBe(clientMessage);
|
||||
|
||||
// Daemon sends encrypted response
|
||||
const daemonMessage = "Hello from daemon!";
|
||||
const daemonCiphertext = await encrypt(daemonSharedKey, daemonMessage);
|
||||
daemonWs.send(Buffer.from(daemonCiphertext));
|
||||
// Daemon sends encrypted response
|
||||
const daemonMessage = "Hello from daemon!";
|
||||
const daemonCiphertext = await encrypt(daemonSharedKey, daemonMessage);
|
||||
daemonWs.send(Buffer.from(daemonCiphertext));
|
||||
|
||||
// Client receives and decrypts
|
||||
const clientReceivedMsg = await new Promise<Buffer>((resolve) => {
|
||||
clientWs.once("message", (data) => resolve(data as Buffer));
|
||||
});
|
||||
const decryptedDaemonMsg = await decrypt(
|
||||
clientSharedKey,
|
||||
clientReceivedMsg.buffer.slice(
|
||||
clientReceivedMsg.byteOffset,
|
||||
clientReceivedMsg.byteOffset + clientReceivedMsg.byteLength,
|
||||
),
|
||||
);
|
||||
expect(decryptedDaemonMsg).toBe(daemonMessage);
|
||||
// Client receives and decrypts
|
||||
const clientReceivedMsg = await new Promise<Buffer>((resolve) => {
|
||||
clientWs.once("message", (data) => resolve(data as Buffer));
|
||||
});
|
||||
const decryptedDaemonMsg = await decrypt(
|
||||
clientSharedKey,
|
||||
clientReceivedMsg.buffer.slice(
|
||||
clientReceivedMsg.byteOffset,
|
||||
clientReceivedMsg.byteOffset + clientReceivedMsg.byteLength,
|
||||
),
|
||||
);
|
||||
expect(decryptedDaemonMsg).toBe(daemonMessage);
|
||||
|
||||
// Cleanup
|
||||
daemonWs.close();
|
||||
clientWs.close();
|
||||
});
|
||||
// Cleanup
|
||||
daemonWs.close();
|
||||
clientWs.close();
|
||||
},
|
||||
);
|
||||
|
||||
it("relay only sees opaque bytes after handshake", { timeout: 90_000 }, async () => {
|
||||
const serverId = "opaque-test-" + Date.now();
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
export {
|
||||
createClientChannel,
|
||||
createDaemonChannel,
|
||||
EncryptedChannel,
|
||||
} from "./encrypted-channel.js";
|
||||
export { createClientChannel, createDaemonChannel, EncryptedChannel } from "./encrypted-channel.js";
|
||||
export type { Transport, EncryptedChannelEvents } from "./encrypted-channel.js";
|
||||
|
||||
export {
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
export type {
|
||||
ConnectionRole,
|
||||
RelaySessionAttachment,
|
||||
} from "./types.js";
|
||||
export type { ConnectionRole, RelaySessionAttachment } from "./types.js";
|
||||
|
||||
export {
|
||||
generateKeyPair,
|
||||
@@ -12,9 +9,5 @@ export {
|
||||
decrypt,
|
||||
} from "./crypto.js";
|
||||
|
||||
export {
|
||||
createClientChannel,
|
||||
createDaemonChannel,
|
||||
EncryptedChannel,
|
||||
} from "./encrypted-channel.js";
|
||||
export { createClientChannel, createDaemonChannel, EncryptedChannel } from "./encrypted-channel.js";
|
||||
export type { Transport, EncryptedChannelEvents } from "./encrypted-channel.js";
|
||||
|
||||
@@ -3,9 +3,7 @@ account_id = "10ed39a1dbf316e30abd0c409bed40d6"
|
||||
main = "src/cloudflare-adapter.ts"
|
||||
compatibility_date = "2024-12-01"
|
||||
|
||||
routes = [
|
||||
{ pattern = "relay.paseo.sh", custom_domain = true }
|
||||
]
|
||||
routes = [{ pattern = "relay.paseo.sh", custom_domain = true }]
|
||||
|
||||
[observability]
|
||||
enabled = true
|
||||
|
||||
+31
-10
@@ -11,6 +11,7 @@ Paseo is a mobile + CLI app for monitoring and controlling local AI coding agent
|
||||
## Build / Lint / Test Commands
|
||||
|
||||
### Root (monorepo)
|
||||
|
||||
```bash
|
||||
npm run dev # Start daemon + Expo in Tmux
|
||||
npm run build:daemon # Build: highlight + relay + server + cli
|
||||
@@ -20,6 +21,7 @@ npm run format # Format with Biome (in-place)
|
||||
```
|
||||
|
||||
### Server package (`packages/server`)
|
||||
|
||||
```bash
|
||||
npm run dev # Start dev daemon (tsx watch)
|
||||
npm run build # Build lib + scripts to dist/
|
||||
@@ -45,6 +47,7 @@ npm run test:ui # Vitest UI at localhost:51204
|
||||
```
|
||||
|
||||
### Other useful commands
|
||||
|
||||
```bash
|
||||
npm run build --workspace=@getpaseo/relay # Rebuild relay before daemon
|
||||
npm run build --workspace=@getpaseo/server # Rebuild server
|
||||
@@ -58,13 +61,22 @@ npm run cli -- daemon status # Check daemon status
|
||||
## Code Style
|
||||
|
||||
### Biome (formatting only, no linting)
|
||||
|
||||
```json
|
||||
{ "indentStyle": "space", "indentWidth": 2, "lineWidth": 100, "quoteStyle": "double", "trailingCommas": "all", "semicolons": "always" }
|
||||
{
|
||||
"indentStyle": "space",
|
||||
"indentWidth": 2,
|
||||
"lineWidth": 100,
|
||||
"quoteStyle": "double",
|
||||
"trailingCommas": "all",
|
||||
"semicolons": "always"
|
||||
}
|
||||
```
|
||||
|
||||
### TypeScript
|
||||
|
||||
- **Fully strict** — no `any`, no implicit `any`
|
||||
- **`interface`** over `type`** when possible
|
||||
- **`interface`** over `type`\*\* when possible
|
||||
- **`function` declarations** over arrow function assignments
|
||||
- **Named types** — no complex inline types in public signatures
|
||||
- **Object parameters** — use single object param when >1 argument
|
||||
@@ -72,15 +84,18 @@ npm run cli -- daemon status # Check daemon status
|
||||
- `noUnusedLocals: true`, `noUnusedParameters: true`, `noFallthroughCasesInSwitch: true`
|
||||
|
||||
### Imports
|
||||
|
||||
- Use path alias `@server/*` in server package (maps to `./src/`)
|
||||
- No barrel `index.ts` re-exports — they create unnecessary indirection
|
||||
|
||||
### Naming
|
||||
|
||||
- Files: `kebab-case.ts` named after the main export (`create-tool-call.ts`)
|
||||
- Tests: collocated with implementation (`thing.test.ts`)
|
||||
- No prefixes like `RpcX`, `DbX`, `UiX` — keep one canonical type per concept
|
||||
|
||||
### Error Handling
|
||||
|
||||
- **Fail explicitly** — throw instead of silently returning defaults
|
||||
- **Typed domain errors** — extend `Error` with structured metadata
|
||||
|
||||
@@ -97,10 +112,16 @@ class TimeoutError extends Error {
|
||||
```
|
||||
|
||||
### State Design
|
||||
|
||||
Discriminated unions over bags of booleans/optionals:
|
||||
|
||||
```typescript
|
||||
// Bad
|
||||
interface FetchState { isLoading: boolean; error?: Error; data?: Data }
|
||||
interface FetchState {
|
||||
isLoading: boolean;
|
||||
error?: Error;
|
||||
data?: Data;
|
||||
}
|
||||
|
||||
// Good
|
||||
type FetchState =
|
||||
@@ -168,10 +189,10 @@ npm run db:query -- "SELECT * FROM agent_timeline_rows..."
|
||||
|
||||
## Relevant Docs
|
||||
|
||||
| File | What it covers |
|
||||
|---|---|
|
||||
| [../CLAUDE.md](../CLAUDE.md) | Repository overview, critical rules, quick start |
|
||||
| [../docs/ARCHITECTURE.md](../docs/ARCHITECTURE.md) | System design, WebSocket protocol, data flow |
|
||||
| [../docs/CODING_STANDARDS.md](../docs/CODING_STANDARDS.md) | Type hygiene, error handling, React patterns |
|
||||
| [../docs/TESTING.md](../docs/TESTING.md) | TDD workflow, determinism, real deps over mocks |
|
||||
| [../SECURITY.md](../SECURITY.md) | Relay threat model, E2E encryption |
|
||||
| File | What it covers |
|
||||
| ---------------------------------------------------------- | ------------------------------------------------ |
|
||||
| [../CLAUDE.md](../CLAUDE.md) | Repository overview, critical rules, quick start |
|
||||
| [../docs/ARCHITECTURE.md](../docs/ARCHITECTURE.md) | System design, WebSocket protocol, data flow |
|
||||
| [../docs/CODING_STANDARDS.md](../docs/CODING_STANDARDS.md) | Type hygiene, error handling, React patterns |
|
||||
| [../docs/TESTING.md](../docs/TESTING.md) | TDD workflow, determinism, real deps over mocks |
|
||||
| [../SECURITY.md](../SECURITY.md) | Relay threat model, E2E encryption |
|
||||
|
||||
@@ -49,15 +49,18 @@ npm start
|
||||
## Project Status
|
||||
|
||||
**✅ Completed** (Phases 1-2):
|
||||
|
||||
- Package setup and configuration
|
||||
- Express server with WebSocket
|
||||
- React UI with Vite
|
||||
- WebSocket client with ping/pong testing
|
||||
|
||||
**⏳ In Progress** (Phase 3):
|
||||
|
||||
- Terminal control (tmux integration)
|
||||
|
||||
**📋 Planned** (Phases 4-9):
|
||||
|
||||
- LLM integration (OpenAI GPT-4)
|
||||
- Agent orchestrator
|
||||
- Speech-to-Text (Deepgram)
|
||||
@@ -95,6 +98,7 @@ PASEO_HOME=~/.paseo-blue PASEO_LISTEN=127.0.0.1:7777 npm run dev
|
||||
## Testing
|
||||
|
||||
Currently manual testing via:
|
||||
|
||||
1. Start servers: `npm run dev`
|
||||
2. Open http://localhost:5173
|
||||
3. Test WebSocket connection (green status indicator)
|
||||
|
||||
@@ -2,11 +2,6 @@
|
||||
"name": "@getpaseo/server",
|
||||
"version": "0.1.62",
|
||||
"description": "Paseo backend server",
|
||||
"type": "module",
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"types": "./dist/server/server/exports.d.ts",
|
||||
"files": [
|
||||
"dist/server",
|
||||
"dist/src",
|
||||
@@ -15,6 +10,8 @@
|
||||
"README.md",
|
||||
".env.example"
|
||||
],
|
||||
"type": "module",
|
||||
"types": "./dist/server/server/exports.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/server/server/exports.d.ts",
|
||||
@@ -27,6 +24,9 @@
|
||||
"default": "./dist/server/utils/tool-call-parsers.js"
|
||||
}
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "cross-env NODE_ENV=development node --import tsx scripts/dev-runner.ts",
|
||||
"dev:tsx": "cross-env NODE_ENV=development tsx watch --ignore '**/*.timestamp-*' src/server/index.ts",
|
||||
|
||||
@@ -232,11 +232,10 @@ export interface CreateAgentRequestOptions extends AgentConfigOverrides {
|
||||
labels?: Record<string, string>;
|
||||
}
|
||||
|
||||
export interface CreatePaseoWorktreeInput
|
||||
extends Pick<
|
||||
CreatePaseoWorktreeRequest,
|
||||
"cwd" | "worktreeSlug" | "attachments" | "refName" | "action" | "githubPrNumber"
|
||||
> {}
|
||||
export interface CreatePaseoWorktreeInput extends Pick<
|
||||
CreatePaseoWorktreeRequest,
|
||||
"cwd" | "worktreeSlug" | "attachments" | "refName" | "action" | "githubPrNumber"
|
||||
> {}
|
||||
|
||||
type CheckoutStatusPayload = CheckoutStatusResponse["payload"];
|
||||
type SubscribeCheckoutDiffPayload = Extract<
|
||||
|
||||
@@ -130,9 +130,7 @@ function buildZodValidator<T>(schema: z.ZodTypeAny, schemaName: string): SchemaV
|
||||
|
||||
function buildJsonSchemaValidator<T>(schema: JsonSchema): SchemaValidator<T> {
|
||||
const AjvConstructor = Ajv as unknown as {
|
||||
new (
|
||||
options?: AjvOptions,
|
||||
): {
|
||||
new (options?: AjvOptions): {
|
||||
compile: (input: JsonSchema) => ((value: unknown) => boolean) & {
|
||||
errors?: ErrorObject[] | null;
|
||||
};
|
||||
|
||||
@@ -1203,9 +1203,7 @@ export async function createAgentMcpServer(options: AgentMcpServerOptions): Prom
|
||||
? (
|
||||
await Promise.all(
|
||||
terminalManager.listDirectories().map(async (directory) =>
|
||||
(
|
||||
await terminalManager.getTerminals(directory)
|
||||
).map((terminal) => ({
|
||||
(await terminalManager.getTerminals(directory)).map((terminal) => ({
|
||||
id: terminal.id,
|
||||
name: terminal.name,
|
||||
cwd: terminal.cwd,
|
||||
|
||||
@@ -8,4 +8,3 @@
|
||||
* This file is kept as a stub to document the migration.
|
||||
* It can be deleted once the refactoring is confirmed working.
|
||||
*/
|
||||
|
||||
|
||||
@@ -49,9 +49,7 @@ export class ClaudeSidechainTracker {
|
||||
private readonly activeSidechains = new Map<string, SubAgentActivityState>();
|
||||
private readonly getToolInput: (toolUseId: string) => AgentMetadata | null | undefined;
|
||||
|
||||
constructor(input: {
|
||||
getToolInput: (toolUseId: string) => AgentMetadata | null | undefined;
|
||||
}) {
|
||||
constructor(input: { getToolInput: (toolUseId: string) => AgentMetadata | null | undefined }) {
|
||||
this.getToolInput = input.getToolInput;
|
||||
}
|
||||
|
||||
|
||||
@@ -291,11 +291,7 @@ export class MockLoadTestAgentSession implements AgentSession {
|
||||
private modeId: string | null;
|
||||
private modelId: string | null;
|
||||
|
||||
constructor(options: {
|
||||
config: AgentSessionConfig;
|
||||
sessionId: string;
|
||||
logger?: Logger;
|
||||
}) {
|
||||
constructor(options: { config: AgentSessionConfig; sessionId: string; logger?: Logger }) {
|
||||
this.id = options.sessionId;
|
||||
this.logger = options.logger;
|
||||
this.modeId = options.config.modeId ?? MOCK_LOAD_TEST_MODE_ID;
|
||||
|
||||
@@ -192,26 +192,23 @@ describe.runIf(process.platform === "win32")("Windows provider launch parity", (
|
||||
});
|
||||
});
|
||||
|
||||
test.each(
|
||||
providerLaunchCases,
|
||||
)("$provider launches a cmd shim from a path with spaces through spawnProcess", async ({
|
||||
binaryName,
|
||||
args,
|
||||
shell,
|
||||
}) => {
|
||||
const fixture = makeFixture(binaryName, args);
|
||||
test.each(providerLaunchCases)(
|
||||
"$provider launches a cmd shim from a path with spaces through spawnProcess",
|
||||
async ({ binaryName, args, shell }) => {
|
||||
const fixture = makeFixture(binaryName, args);
|
||||
|
||||
const result = await runProviderFixture({
|
||||
command: fixture.shim,
|
||||
args: fixture.expectedArgs,
|
||||
expectedArgs: fixture.expectedArgs,
|
||||
shell,
|
||||
});
|
||||
const result = await runProviderFixture({
|
||||
command: fixture.shim,
|
||||
args: fixture.expectedArgs,
|
||||
expectedArgs: fixture.expectedArgs,
|
||||
shell,
|
||||
});
|
||||
|
||||
expect(result.error).toBeNull();
|
||||
expect(result.code).toBe(0);
|
||||
expect(result.signal).toBeNull();
|
||||
expect(result.stderr).toBe("");
|
||||
expect(result.stdout.trim()).toBe("ARGV_OK");
|
||||
});
|
||||
expect(result.error).toBeNull();
|
||||
expect(result.code).toBe(0);
|
||||
expect(result.signal).toBeNull();
|
||||
expect(result.stderr).toBe("");
|
||||
expect(result.stdout.trim()).toBe("ARGV_OK");
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
@@ -78,52 +78,51 @@ describe("daemon E2E", () => {
|
||||
await ctx.cleanup();
|
||||
}, 60000);
|
||||
|
||||
describe.each([
|
||||
"claude",
|
||||
"codex",
|
||||
"opencode",
|
||||
] as const)("live model switching (%s)", (provider) => {
|
||||
const shouldRun =
|
||||
provider === "claude" ||
|
||||
(provider === "codex" && hasCodex) ||
|
||||
(provider === "opencode" && hasOpenCode);
|
||||
describe.each(["claude", "codex", "opencode"] as const)(
|
||||
"live model switching (%s)",
|
||||
(provider) => {
|
||||
const shouldRun =
|
||||
provider === "claude" ||
|
||||
(provider === "codex" && hasCodex) ||
|
||||
(provider === "opencode" && hasOpenCode);
|
||||
|
||||
test.runIf(shouldRun)(
|
||||
"updates agent model without restarting",
|
||||
async () => {
|
||||
const cwd = tmpCwd();
|
||||
try {
|
||||
const modelList = await ctx.client.listProviderModels(provider);
|
||||
if (!modelList.models || modelList.models.length === 0) {
|
||||
throw new Error(`No models returned for provider ${provider}`);
|
||||
test.runIf(shouldRun)(
|
||||
"updates agent model without restarting",
|
||||
async () => {
|
||||
const cwd = tmpCwd();
|
||||
try {
|
||||
const modelList = await ctx.client.listProviderModels(provider);
|
||||
if (!modelList.models || modelList.models.length === 0) {
|
||||
throw new Error(`No models returned for provider ${provider}`);
|
||||
}
|
||||
const [modelA, modelB] = pickTwoDistinctModels(modelList.models);
|
||||
|
||||
const agent = await ctx.client.createAgent({
|
||||
provider,
|
||||
cwd,
|
||||
title: `Model Switch (${provider})`,
|
||||
model: modelA,
|
||||
});
|
||||
|
||||
const startIndex = messages.length;
|
||||
await ctx.client.setAgentModel(agent.id, modelB);
|
||||
|
||||
const updated = await waitForAgentUpdate(
|
||||
messages,
|
||||
startIndex,
|
||||
(a) => a.id === agent.id && a.model === modelB,
|
||||
{ timeoutMs: 20000 },
|
||||
);
|
||||
|
||||
expect(updated.model).toBe(modelB);
|
||||
} finally {
|
||||
rmSync(cwd, { recursive: true, force: true });
|
||||
}
|
||||
const [modelA, modelB] = pickTwoDistinctModels(modelList.models);
|
||||
|
||||
const agent = await ctx.client.createAgent({
|
||||
provider,
|
||||
cwd,
|
||||
title: `Model Switch (${provider})`,
|
||||
model: modelA,
|
||||
});
|
||||
|
||||
const startIndex = messages.length;
|
||||
await ctx.client.setAgentModel(agent.id, modelB);
|
||||
|
||||
const updated = await waitForAgentUpdate(
|
||||
messages,
|
||||
startIndex,
|
||||
(a) => a.id === agent.id && a.model === modelB,
|
||||
{ timeoutMs: 20000 },
|
||||
);
|
||||
|
||||
expect(updated.model).toBe(modelB);
|
||||
} finally {
|
||||
rmSync(cwd, { recursive: true, force: true });
|
||||
}
|
||||
},
|
||||
180000,
|
||||
);
|
||||
});
|
||||
},
|
||||
180000,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
test("live thinking switching works for Claude (off -> on)", async () => {
|
||||
const cwd = tmpCwd();
|
||||
|
||||
@@ -35,11 +35,7 @@ export {
|
||||
type ProviderOverride,
|
||||
type ProviderProfileModel,
|
||||
} from "./agent/provider-launch-config.js";
|
||||
export {
|
||||
findExecutable,
|
||||
quoteWindowsArgument,
|
||||
quoteWindowsCommand,
|
||||
} from "../utils/executable.js";
|
||||
export { findExecutable, quoteWindowsArgument, quoteWindowsCommand } from "../utils/executable.js";
|
||||
export { execCommand, spawnProcess } from "../utils/spawn.js";
|
||||
|
||||
// Provider manifest (source of truth for provider definitions)
|
||||
|
||||
@@ -1776,9 +1776,9 @@ describe("workspace aggregation", () => {
|
||||
rmSync(tempDir, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
const response = emitted.find((message) => message.type === "create_paseo_worktree_response") as
|
||||
| { type: "create_paseo_worktree_response"; payload: any }
|
||||
| undefined;
|
||||
const response = emitted.find(
|
||||
(message) => message.type === "create_paseo_worktree_response",
|
||||
) as { type: "create_paseo_worktree_response"; payload: any } | undefined;
|
||||
|
||||
expect(response?.payload.error).toBeNull();
|
||||
expect(response?.payload.workspace).toMatchObject({
|
||||
|
||||
@@ -7,8 +7,5 @@ export {
|
||||
type DaemonEvent,
|
||||
type DaemonEventHandler,
|
||||
} from "./daemon-client.js";
|
||||
export {
|
||||
createDaemonTestContext,
|
||||
type DaemonTestContext,
|
||||
} from "./daemon-test-context.js";
|
||||
export { createDaemonTestContext, type DaemonTestContext } from "./daemon-test-context.js";
|
||||
export { useTempClaudeConfigDir } from "./claude-config.js";
|
||||
|
||||
@@ -172,20 +172,19 @@ describe("pull request timeline message schemas", () => {
|
||||
expect(parsed.payload.error).toEqual({ kind: "unknown", message: "x" });
|
||||
});
|
||||
|
||||
test.each([
|
||||
"rate_limited",
|
||||
"",
|
||||
undefined,
|
||||
])("normalizes malformed error kind %j to unknown", (kind) => {
|
||||
const parsed = PullRequestTimelineResponseSchema.parse({
|
||||
type: "pull_request_timeline_response",
|
||||
payload: {
|
||||
error: { kind },
|
||||
},
|
||||
});
|
||||
test.each(["rate_limited", "", undefined])(
|
||||
"normalizes malformed error kind %j to unknown",
|
||||
(kind) => {
|
||||
const parsed = PullRequestTimelineResponseSchema.parse({
|
||||
type: "pull_request_timeline_response",
|
||||
payload: {
|
||||
error: { kind },
|
||||
},
|
||||
});
|
||||
|
||||
expect(parsed.payload.error).toEqual({ kind: "unknown", message: "" });
|
||||
});
|
||||
expect(parsed.payload.error).toEqual({ kind: "unknown", message: "" });
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe("checkout PR status compatibility", () => {
|
||||
|
||||
@@ -6,7 +6,7 @@ main = "@tanstack/react-start/server-entry"
|
||||
|
||||
routes = [
|
||||
{ pattern = "paseo.sh", custom_domain = true },
|
||||
{ pattern = "www.paseo.sh", custom_domain = true }
|
||||
{ pattern = "www.paseo.sh", custom_domain = true },
|
||||
]
|
||||
|
||||
[observability]
|
||||
|
||||
Reference in New Issue
Block a user