@aipoch/open-science 1.0.1 → 1.0.3
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/CLI.md +121 -15
- package/README.md +95 -8
- package/browser-import.d.ts +11 -5
- package/browser-import.mjs +81 -55
- package/cli.mjs +132 -10
- package/index.d.ts +1 -0
- package/index.mjs +49 -0
- package/package.json +6 -6
package/CLI.md
CHANGED
|
@@ -8,7 +8,29 @@ defines the 1.x contract for commands, machine-readable output and exit codes.
|
|
|
8
8
|
|
|
9
9
|
## Installation
|
|
10
10
|
|
|
11
|
-
###
|
|
11
|
+
### From npm
|
|
12
|
+
|
|
13
|
+
Install the published [standalone CLI](https://www.npmjs.com/package/@aipoch/open-science)
|
|
14
|
+
with Node.js **22.13.0 or later**:
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npm install --global @aipoch/open-science
|
|
18
|
+
open-science start --no-open
|
|
19
|
+
open-science url
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
The registry package installs the matching native backend, Web UI and resources through npm
|
|
23
|
+
optional dependencies. It runs without Electron or an installed desktop app. Supported targets
|
|
24
|
+
are macOS ARM64/x64, glibc Linux ARM64/x64 and Windows x64; musl Linux and Windows ARM64 are not
|
|
25
|
+
supported. Do not omit optional dependencies. If startup reports a missing native package, run
|
|
26
|
+
`npm install --global @aipoch/open-science --include=optional`.
|
|
27
|
+
|
|
28
|
+
Open the printed authenticated URL to configure an Agent runtime, model provider and optional
|
|
29
|
+
Python/R Notebook runtime, or follow [First-run setup (Codex)](#first-run-setup-codex) from the
|
|
30
|
+
terminal. See the [standalone runtime guide](https://github.com/aipoch/open-science/blob/main/docs/standalone-runtime.md#linux-prerequisites)
|
|
31
|
+
for Linux credential storage and sandbox prerequisites.
|
|
32
|
+
|
|
33
|
+
### Locally built standalone tarball
|
|
12
34
|
|
|
13
35
|
Install the target-specific standalone tarball built with `npm run pack:backend`, using ordinary
|
|
14
36
|
Node >=22.13.0. It includes the backend, Web UI, resources and native dependencies; Electron and
|
|
@@ -54,25 +76,58 @@ not require a separate Node.js installation.
|
|
|
54
76
|
If the launcher directory is not yet on PATH, the Settings panel shows the line to add. Open a new
|
|
55
77
|
terminal after updating PATH. Choose **Uninstall command** in the same panel to remove the launcher.
|
|
56
78
|
|
|
57
|
-
### From
|
|
79
|
+
### From a source checkout
|
|
58
80
|
|
|
59
|
-
|
|
60
|
-
To start an independent service, install the standalone artifact (Node >=22.13.0); the lightweight
|
|
61
|
-
package alone does not contain backend resources.
|
|
62
|
-
Install it globally after the package is published:
|
|
81
|
+
Replace `open-science` in the examples below with:
|
|
63
82
|
|
|
64
83
|
```bash
|
|
65
|
-
|
|
66
|
-
open-science --help
|
|
84
|
+
node packages/open-science/cli.mjs
|
|
67
85
|
```
|
|
68
86
|
|
|
69
|
-
|
|
87
|
+
## Local information and help
|
|
70
88
|
|
|
71
|
-
|
|
89
|
+
These commands work without starting or connecting to the service:
|
|
72
90
|
|
|
73
91
|
```bash
|
|
74
|
-
|
|
75
|
-
|
|
92
|
+
open-science --version
|
|
93
|
+
open-science -v
|
|
94
|
+
open-science version --json
|
|
95
|
+
open-science help
|
|
96
|
+
open-science help run
|
|
97
|
+
open-science paths --json
|
|
98
|
+
open-science paths --profile /absolute/path/to/profile
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
`--version`, `-v`, and `version` print the invoked CLI's application version, including any
|
|
102
|
+
nightly suffix. `version --json` prints `{ "version": "..." }`. Source checkouts use the root
|
|
103
|
+
application version; installed distributions use their own release metadata. This is not the
|
|
104
|
+
version of a running service. The version flags are standalone commands, not options for `run`,
|
|
105
|
+
`start`, or other commands.
|
|
106
|
+
|
|
107
|
+
`help` is equivalent to `--help`; `help <command>` is equivalent to `<command> --help`.
|
|
108
|
+
For example, `help project` lists Project commands. Unknown command names and unsupported
|
|
109
|
+
arguments fail with the existing usage-error exit code `2`.
|
|
110
|
+
|
|
111
|
+
`paths` prints human-readable locations; `paths --json` prints one object with:
|
|
112
|
+
|
|
113
|
+
| Field | Meaning |
|
|
114
|
+
| ---------------------- | ---------------------------------------------------------------------------------------- |
|
|
115
|
+
| `cliPath` | The CLI implementation file used by this invocation. |
|
|
116
|
+
| `nodePath` | The Node executable running the CLI. |
|
|
117
|
+
| `startupConfigRoot` | The configuration directory selected by the installation's default or explicit override. |
|
|
118
|
+
| `discoveryConfigRoots` | Ordered candidate configuration directories searched for an existing service. |
|
|
119
|
+
|
|
120
|
+
Source checkouts default to `~/.open-science-project` for startup; installations default to
|
|
121
|
+
`~/.open-science`. Without an override, service discovery checks the development directory first,
|
|
122
|
+
then the production directory. `--config-root` (or `--profile`) takes precedence over
|
|
123
|
+
`OPEN_SCIENCE_CONFIG_ROOT`, then `OPEN_SCIENCE_STORAGE_ROOT`; an override selects a single
|
|
124
|
+
absolute directory for both startup and discovery. Paths are reported even when they do not exist.
|
|
125
|
+
The output does not identify an active service or a research-data directory, which may differ.
|
|
126
|
+
|
|
127
|
+
These commands do not create profiles, modify settings, read credentials, or print authenticated
|
|
128
|
+
URLs. Successful queries exit `0`; missing or invalid version metadata produces an error rather
|
|
129
|
+
than a fallback version. `paths` accepts only the profile, JSON, and help options; `version`
|
|
130
|
+
accepts only JSON and help options, and `help` accepts only one optional command name and help flags.
|
|
76
131
|
|
|
77
132
|
## Service lifecycle
|
|
78
133
|
|
|
@@ -106,7 +161,7 @@ open-science url
|
|
|
106
161
|
`open-science url` is the only command that intentionally prints an authenticated browser URL. Normal
|
|
107
162
|
human-readable, JSON, and JSONL output never includes the local token.
|
|
108
163
|
|
|
109
|
-
Use `--port <port>` to override `OPEN_SCIENCE_WEB_PORT`, or the default port of `44100` when neither is set. `start` discovers the standalone
|
|
164
|
+
Use `--port <port>` to override `OPEN_SCIENCE_WEB_PORT`, or the default port of `44100` when neither is set. `start` discovers the matching npm platform package, the standalone
|
|
110
165
|
Node entry bundled beside the CLI, or `out/backend/index.cjs` in a source checkout. Build the
|
|
111
166
|
source entry with `npm run build:backend` and Web UI with `npm run build:web` first.
|
|
112
167
|
`--app-path` and `OPEN_SCIENCE_APP_PATH` no longer select a hidden Electron backend.
|
|
@@ -166,7 +221,7 @@ nonzero CLI error codes.
|
|
|
166
221
|
|
|
167
222
|
## First-run setup (Codex)
|
|
168
223
|
|
|
169
|
-
Install the standalone artifact or a desktop package containing the ordinary Node backend first.
|
|
224
|
+
Install the npm package, a standalone artifact or a desktop package containing the ordinary Node backend first.
|
|
170
225
|
`init` creates a configuration directory only; `--profile` selects an absolute configuration directory.
|
|
171
226
|
|
|
172
227
|
```bash
|
|
@@ -249,9 +304,23 @@ open-science runtime list --json
|
|
|
249
304
|
## Upgrading installations
|
|
250
305
|
|
|
251
306
|
Stop the backend before upgrading an installation, then restart it with the same profile.
|
|
307
|
+
For a global npm installation:
|
|
308
|
+
|
|
309
|
+
```bash
|
|
310
|
+
open-science stop
|
|
311
|
+
npm install --global @aipoch/open-science@latest
|
|
312
|
+
open-science start --no-open
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
Stop every profile started by that installation before updating it; use `--config-root` for
|
|
316
|
+
custom profiles. To uninstall, stop those services and run
|
|
317
|
+
`npm uninstall --global @aipoch/open-science`. Research data and credentials are retained.
|
|
318
|
+
See [release compatibility](https://github.com/aipoch/open-science/blob/main/docs/compatibility.md)
|
|
319
|
+
before downgrading; reinstalling an older package does not undo data migrations.
|
|
320
|
+
|
|
252
321
|
Use the installation channel that owns the CLI: Homebrew for a Formula, the new downloaded
|
|
253
322
|
package for deb/rpm or a local npm tarball, or a verified new Release archive in a new directory
|
|
254
|
-
with PATH/the symlink updated.
|
|
323
|
+
with PATH/the symlink updated.
|
|
255
324
|
The desktop update UI discovers and downloads packages. Automatic replacement remains closed for
|
|
256
325
|
macOS ZIP, Linux AppImage and Debian until each channel passes complete installed-package
|
|
257
326
|
acceptance. Windows automatic installation remains disabled; artifact checks and same-version copy tests do not certify a cross-version upgrade.
|
|
@@ -537,6 +606,43 @@ turns only and is rejected while root-agent, subagent, or Notebook work is activ
|
|
|
537
606
|
does not block the update. Stale revisions fail with `session_revision_conflict`; invalid or
|
|
538
607
|
unavailable references fail with `invalid_configuration`.
|
|
539
608
|
|
|
609
|
+
## Session package export and import
|
|
610
|
+
|
|
611
|
+
Export a Session to a `.science` package at an explicit path. The Session must be idle:
|
|
612
|
+
|
|
613
|
+
```bash
|
|
614
|
+
open-science session export <session-id> --project <id-or-name> --output ~/backups/session.science
|
|
615
|
+
```
|
|
616
|
+
|
|
617
|
+
`--project` resolves a Project by id or name and selects which Session to export. `--output` must
|
|
618
|
+
be an absolute or relative path ending in `.science`; when it is omitted the command fails instead
|
|
619
|
+
of opening a native save dialog, because the CLI runs without a desktop session. The command
|
|
620
|
+
returns the saved status and written path as JSON with `--json`.
|
|
621
|
+
|
|
622
|
+
Both package commands allow up to one hour by default. Use `--timeout-ms <milliseconds>`
|
|
623
|
+
to adjust the request deadline for large packages or slower disks.
|
|
624
|
+
|
|
625
|
+
Import a `.science` package. Import is non-interactive by design and requires an explicit
|
|
626
|
+
confirmation flag:
|
|
627
|
+
|
|
628
|
+
```bash
|
|
629
|
+
open-science session import ~/backups/session.science --new-project "Restored session" --non-interactive
|
|
630
|
+
open-science session import ~/backups/session.science --project <id-or-name> --non-interactive
|
|
631
|
+
```
|
|
632
|
+
|
|
633
|
+
`--non-interactive` confirms the import preview on the caller's behalf; without it the command
|
|
634
|
+
fails rather than waiting for an approval that a headless caller can never supply. Choose exactly
|
|
635
|
+
one destination: `--project` imports into an existing Project, `--new-project` creates one with
|
|
636
|
+
the given name, and omitting both uses the package's own Project name in a new Project. The
|
|
637
|
+
package file is read directly from disk, so no upload or drag-and-drop step is involved.
|
|
638
|
+
|
|
639
|
+
Both commands dispatch the same `sessions:export-package`, `sessions:import-package`, and
|
|
640
|
+
`sessions:package-operation` application commands the desktop app uses, over the authenticated
|
|
641
|
+
loopback HTTP channel. They are unavailable to remote Web sessions by design: the channel is
|
|
642
|
+
installed as a rejecting stub for remote callers, and the handler additionally requires a local
|
|
643
|
+
caller, so arbitrary-path file read and write stay confined to loopback callers that hold the
|
|
644
|
+
service token.
|
|
645
|
+
|
|
540
646
|
## Agent routing settings
|
|
541
647
|
|
|
542
648
|
Inspect or atomically update the global Agent framework plus Reviewer and Subagent model routing:
|
package/README.md
CHANGED
|
@@ -1,10 +1,88 @@
|
|
|
1
1
|
# @aipoch/open-science
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[AIPOCH Open-Science](https://github.com/aipoch/open-science) is an open-source, local-first,
|
|
4
|
+
model-agnostic AI research workbench. Use scientific Agents, Python and R Notebooks, and
|
|
5
|
+
scientific data connectors to turn research questions into reports, tables and figures.
|
|
6
|
+
|
|
7
|
+
This package provides the **standalone CLI, local Web UI and Node.js SDK**. npm installs the
|
|
8
|
+
matching platform package with the backend and resources; the desktop app is not required.
|
|
9
|
+
|
|
10
|
+
## Install
|
|
11
|
+
|
|
12
|
+
Install [Node.js](https://nodejs.org/) **22.13.0 or later**, then run:
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
npm install --global @aipoch/open-science
|
|
16
|
+
open-science --help
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Supported platforms are macOS Apple Silicon/Intel, Linux ARM64/x64 with glibc, and Windows x64.
|
|
20
|
+
Linux musl (including Alpine) and Windows ARM64 are not supported by the published native packages.
|
|
21
|
+
Keep npm optional dependencies enabled: they contain the platform runtime. If startup reports a
|
|
22
|
+
missing native package, reinstall with `npm install --global @aipoch/open-science --include=optional`.
|
|
23
|
+
|
|
24
|
+
Linux also needs working credential storage and Notebook sandbox prerequisites; see the
|
|
25
|
+
[Linux setup requirements](https://github.com/aipoch/open-science/blob/main/docs/standalone-runtime.md#linux-prerequisites).
|
|
26
|
+
Python/R and an Agent runtime/model provider are configured separately during setup.
|
|
27
|
+
For installations that include Node itself, see the
|
|
28
|
+
[Homebrew and Release archive options](https://github.com/aipoch/open-science/blob/main/docs/standalone-runtime.md).
|
|
29
|
+
|
|
30
|
+
## Get started
|
|
31
|
+
|
|
32
|
+
Start the local service and request its browser URL:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
open-science start --no-open
|
|
36
|
+
open-science url
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Open the printed URL, complete the setup checks, configure an Agent runtime and model provider,
|
|
40
|
+
and create your first research project. Set up a Python or R Notebook runtime if your work needs
|
|
41
|
+
code execution. The URL contains a local authentication token; keep it private.
|
|
42
|
+
For setup from the terminal, follow the
|
|
43
|
+
[CLI first-run guide](https://github.com/aipoch/open-science/blob/main/packages/open-science/CLI.md#first-run-setup-codex).
|
|
44
|
+
|
|
45
|
+
After setup, submit a task from the terminal:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
open-science doctor --json
|
|
49
|
+
open-science project create "First research" --json
|
|
50
|
+
open-science run --project "First research" --prompt "Draft a research plan for comparing two analysis methods." --wait --jsonl
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Confirm `ready: true` in the doctor output before submitting work. The CLI accepts a Project ID
|
|
54
|
+
or an exact, unambiguous name; SDK calls use IDs. Closing the browser does not stop the service:
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
open-science status --json
|
|
58
|
+
open-science stop
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Stopping the service affects all connected clients. For automated tasks, review the
|
|
62
|
+
[permissions and output options](https://github.com/aipoch/open-science/blob/main/packages/open-science/CLI.md)
|
|
63
|
+
and the safety guidance below.
|
|
64
|
+
|
|
65
|
+
## Update or uninstall
|
|
66
|
+
|
|
67
|
+
Stop services started by this installation before replacing it. Use the same `--config-root`
|
|
68
|
+
when stopping and restarting a custom profile.
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
open-science stop
|
|
72
|
+
npm install --global @aipoch/open-science@latest
|
|
73
|
+
open-science start --no-open
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
To uninstall, stop the service and run `npm uninstall --global @aipoch/open-science`.
|
|
77
|
+
Uninstalling the npm package retains research data and credentials. Review
|
|
78
|
+
[release compatibility](https://github.com/aipoch/open-science/blob/main/docs/compatibility.md)
|
|
79
|
+
before changing versions; replacing program files does not reverse data migrations.
|
|
4
80
|
|
|
5
81
|
## Documentation
|
|
6
82
|
|
|
7
|
-
- [CLI guide](
|
|
83
|
+
- [CLI guide](https://github.com/aipoch/open-science/blob/main/packages/open-science/CLI.md) - installation, daemon lifecycle, task automation, artifacts, and exit codes
|
|
84
|
+
- [Product overview](https://github.com/aipoch/open-science#readme) - research workflows, screenshots and desktop downloads
|
|
85
|
+
- [Report an issue](https://github.com/aipoch/open-science/issues) - bugs and feature requests
|
|
8
86
|
- [Release compatibility](https://github.com/aipoch/open-science/blob/main/docs/compatibility.md) - stable interfaces, versioning, and data upgrades
|
|
9
87
|
- [Intended use](https://github.com/aipoch/open-science/blob/main/docs/usage-scope.md) and [known limitations](https://github.com/aipoch/open-science/blob/main/docs/known-limitations.md) - workflow precautions, data disclosure, and execution boundaries
|
|
10
88
|
|
|
@@ -25,19 +103,28 @@ Node.js SDK and command-line client for an Open-Science daemon running on the lo
|
|
|
25
103
|
## Browser-held package import
|
|
26
104
|
|
|
27
105
|
Use `@aipoch/open-science/browser-import` to send a downloaded `.science` Blob to the desktop app.
|
|
28
|
-
See the [browser integration guide](
|
|
106
|
+
See the [browser integration guide](https://github.com/aipoch/open-science/blob/main/packages/open-science/BROWSER_IMPORT.md) for the click flow, API, security contract,
|
|
29
107
|
limits, and browser requirements. The desktop never downloads the online package URL.
|
|
30
108
|
|
|
31
109
|
## SDK quick start
|
|
32
110
|
|
|
111
|
+
Install the package in your Node.js project (a global CLI install does not make it importable):
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
npm install @aipoch/open-science
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Start and configure the local service as above. Save the example in an `.mjs` file and run it
|
|
118
|
+
with Node.js. It creates a Project and uses its ID for subsequent requests:
|
|
119
|
+
|
|
33
120
|
```js
|
|
34
121
|
import { connectToOpenScience } from '@aipoch/open-science'
|
|
35
122
|
|
|
36
123
|
const client = await connectToOpenScience()
|
|
124
|
+
const project = await client.createProject({ name: 'Evidence review' })
|
|
37
125
|
const run = await client.startRun({
|
|
38
|
-
project:
|
|
126
|
+
project: project.id,
|
|
39
127
|
prompt: 'Summarize the evidence.',
|
|
40
|
-
cwd: '/absolute/path/to/research',
|
|
41
128
|
permissionProfile: 'auto',
|
|
42
129
|
permissionPrompts: 'none' // Deny requests that would require a human; keep existing permission rules.
|
|
43
130
|
})
|
|
@@ -55,8 +142,8 @@ await client.updateSessionConfiguration(run.sessionId, {
|
|
|
55
142
|
memoryEnabled: false
|
|
56
143
|
})
|
|
57
144
|
|
|
58
|
-
const defaults = await client.getProjectSessionDefaults(
|
|
59
|
-
await client.updateProjectSessionDefaults(
|
|
145
|
+
const defaults = await client.getProjectSessionDefaults(project.id)
|
|
146
|
+
await client.updateProjectSessionDefaults(project.id, {
|
|
60
147
|
expectedUpdatedAt: defaults.updatedAt,
|
|
61
148
|
patch: { permissionProfile: 'auto' }
|
|
62
149
|
})
|
|
@@ -142,7 +229,7 @@ const progress = (async () => {
|
|
|
142
229
|
})()
|
|
143
230
|
|
|
144
231
|
const observedRun = await client.startRun({
|
|
145
|
-
project:
|
|
232
|
+
project: project.id,
|
|
146
233
|
prompt: 'Summarize the evidence.',
|
|
147
234
|
permissionProfile: 'auto'
|
|
148
235
|
})
|
package/browser-import.d.ts
CHANGED
|
@@ -36,6 +36,8 @@ export function checkBrowserImportAvailability(
|
|
|
36
36
|
export type BrowserImportTransfer = {
|
|
37
37
|
/** Contains a secret; never log or persist it. Browser policy may require a fresh user click. */
|
|
38
38
|
readonly url: string
|
|
39
|
+
/** Abort requests and timers, invalidate this transfer, and best-effort cancel the receiver. */
|
|
40
|
+
cancel(): Promise<void>
|
|
39
41
|
/** Acknowledges receipt only, independently of destination selection and final import consent.
|
|
40
42
|
* Authenticated desktop cancellation throws code 'cancelled', terminal for this transfer.
|
|
41
43
|
* Keep the Blob and explicitly prepare a new transfer; never automatically retry cancellation. */
|
|
@@ -46,16 +48,20 @@ export type BrowserImportTransfer = {
|
|
|
46
48
|
onPhase?: (phase: BrowserImportPhase) => void
|
|
47
49
|
/** Acknowledged bytes only; first callback follows the first successful chunk acknowledgment. */
|
|
48
50
|
onProgress?: (progress: BrowserImportProgress) => void
|
|
49
|
-
/** Default attempts preserves the 1–
|
|
51
|
+
/** Default attempts preserves the 1–30 attempt limit. wait-for-launch retries until the
|
|
50
52
|
* overall deadline or cancellation; it observes only this transfer's authenticated handshake. */
|
|
51
53
|
readinessMode?: BrowserImportReadinessMode
|
|
52
54
|
/** Readiness only; defaults to 120,000 ms. Capped at 120,000 ms in wait-for-launch,
|
|
53
|
-
* 300,000 ms in attempts mode.
|
|
54
|
-
|
|
55
|
-
|
|
55
|
+
* 300,000 ms in attempts mode. Set null only in attempts mode to stop by attempt count
|
|
56
|
+
* without an overall deadline (including across page suspension). Per-request timeouts
|
|
57
|
+
* still apply. Timeout/exhaustion permits another send() on the same live transfer. */
|
|
58
|
+
readyTimeoutMs?: number | null
|
|
59
|
+
/** 1–30 attempts with 1,000 ms gaps; defaults to 5. Ignored in wait-for-launch mode. */
|
|
56
60
|
readinessAttempts?: number
|
|
61
|
+
/** Delay after each failed attempt; defaults to 1,000 ms. Range: 0–30,000 ms. */
|
|
62
|
+
readinessIntervalMs?: number
|
|
57
63
|
/** Includes response body reads; defaults to 2,000 ms. Accepts up to 30,000 ms,
|
|
58
|
-
* clamped to 2,000 ms in wait-for-launch mode. Failed attempts
|
|
64
|
+
* clamped to 2,000 ms in wait-for-launch mode. Failed attempts use readinessIntervalMs. */
|
|
59
65
|
attemptTimeoutMs?: number
|
|
60
66
|
}): Promise<{ received: true }>
|
|
61
67
|
}
|
package/browser-import.mjs
CHANGED
|
@@ -178,9 +178,33 @@ export function prepareBrowserImport(file, options = {}) {
|
|
|
178
178
|
let sending = false
|
|
179
179
|
let received = false
|
|
180
180
|
let cancelled
|
|
181
|
+
const lifetime = new AbortController()
|
|
182
|
+
let cancellation
|
|
183
|
+
// Cancel this secret-bound receiver even while readiness is still pending.
|
|
184
|
+
const cancel = () => {
|
|
185
|
+
if (received) return Promise.resolve()
|
|
186
|
+
if (cancellation) return cancellation
|
|
187
|
+
cancelled = new BrowserImportError('cancelled', 'The transfer was cancelled.')
|
|
188
|
+
lifetime.abort(cancelled)
|
|
189
|
+
cancellation = bounded(undefined, 2000, async (signal) => {
|
|
190
|
+
const headers = await authorize('DELETE', '')
|
|
191
|
+
signal.throwIfAborted()
|
|
192
|
+
await fetch(base, {
|
|
193
|
+
method: 'DELETE',
|
|
194
|
+
mode: 'cors',
|
|
195
|
+
credentials: 'omit',
|
|
196
|
+
redirect: 'error',
|
|
197
|
+
headers,
|
|
198
|
+
signal,
|
|
199
|
+
targetAddressSpace: 'loopback'
|
|
200
|
+
})
|
|
201
|
+
}).catch(() => undefined)
|
|
202
|
+
return cancellation
|
|
203
|
+
}
|
|
181
204
|
|
|
182
205
|
return Object.freeze({
|
|
183
206
|
url,
|
|
207
|
+
cancel,
|
|
184
208
|
async send({
|
|
185
209
|
signal,
|
|
186
210
|
onProgress,
|
|
@@ -188,18 +212,23 @@ export function prepareBrowserImport(file, options = {}) {
|
|
|
188
212
|
readinessMode = 'attempts',
|
|
189
213
|
readyTimeoutMs = 120_000,
|
|
190
214
|
readinessAttempts = 5,
|
|
215
|
+
readinessIntervalMs = 1000,
|
|
191
216
|
attemptTimeoutMs = 2000
|
|
192
217
|
} = {}) {
|
|
193
218
|
if (!['attempts', 'wait-for-launch'].includes(readinessMode))
|
|
194
219
|
throw new Error('Invalid readiness mode.')
|
|
195
220
|
const waitingForLaunch = readinessMode === 'wait-for-launch'
|
|
196
|
-
|
|
221
|
+
const countOnly = !waitingForLaunch && readyTimeoutMs === null
|
|
222
|
+
if (!countOnly && (!Number.isFinite(readyTimeoutMs) || readyTimeoutMs <= 0))
|
|
197
223
|
throw new Error('Invalid receiver timeout.')
|
|
198
224
|
if (
|
|
199
225
|
(!waitingForLaunch &&
|
|
200
226
|
(!Number.isInteger(readinessAttempts) ||
|
|
201
227
|
readinessAttempts < 1 ||
|
|
202
|
-
readinessAttempts >
|
|
228
|
+
readinessAttempts > 30)) ||
|
|
229
|
+
!Number.isFinite(readinessIntervalMs) ||
|
|
230
|
+
readinessIntervalMs < 0 ||
|
|
231
|
+
readinessIntervalMs > 30_000 ||
|
|
203
232
|
!Number.isFinite(attemptTimeoutMs) ||
|
|
204
233
|
attemptTimeoutMs <= 0 ||
|
|
205
234
|
attemptTimeoutMs > 30_000
|
|
@@ -208,49 +237,54 @@ export function prepareBrowserImport(file, options = {}) {
|
|
|
208
237
|
if (sending) throw new Error('This browser transfer is already running.')
|
|
209
238
|
if (cancelled) throw cancelled
|
|
210
239
|
if (received) return { received: true }
|
|
211
|
-
signal
|
|
240
|
+
signal = signal ? AbortSignal.any([signal, lifetime.signal]) : lifetime.signal
|
|
241
|
+
signal.throwIfAborted()
|
|
212
242
|
sending = true
|
|
213
243
|
let verified = false
|
|
214
|
-
const request = async (path, init = {}) =>
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
[
|
|
224
|
-
)
|
|
225
|
-
})
|
|
226
|
-
if (response.status === 410) {
|
|
227
|
-
const value = await response.json().catch(() => {
|
|
228
|
-
throw new BrowserImportError(
|
|
229
|
-
'incompatible',
|
|
230
|
-
'Could not verify the local receiver identity.'
|
|
231
|
-
)
|
|
244
|
+
const request = async (path, init = {}) =>
|
|
245
|
+
bounded(signal, 30_000, async (requestSignal) => {
|
|
246
|
+
const response = await fetch(base + path, {
|
|
247
|
+
mode: 'cors',
|
|
248
|
+
credentials: 'omit',
|
|
249
|
+
cache: 'no-store',
|
|
250
|
+
redirect: 'error',
|
|
251
|
+
targetAddressSpace: 'loopback',
|
|
252
|
+
...init,
|
|
253
|
+
signal: AbortSignal.any([requestSignal, init.signal].filter(Boolean))
|
|
232
254
|
})
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
255
|
+
const value = await response.json().catch((error) => {
|
|
256
|
+
if (response.status === 410) {
|
|
257
|
+
throw new BrowserImportError(
|
|
258
|
+
'incompatible',
|
|
259
|
+
'Could not verify the local receiver identity.'
|
|
260
|
+
)
|
|
261
|
+
}
|
|
262
|
+
throw error
|
|
263
|
+
})
|
|
264
|
+
if (response.status === 410) {
|
|
265
|
+
if (value?.code !== 'cancelled' || value.proof !== (await sign('cancelled')))
|
|
266
|
+
throw new BrowserImportError(
|
|
267
|
+
'incompatible',
|
|
268
|
+
'Could not verify the local receiver identity.'
|
|
269
|
+
)
|
|
270
|
+
cancelled = new BrowserImportError(
|
|
271
|
+
'cancelled',
|
|
272
|
+
'The transfer was cancelled in Open-Science. Prepare a new transfer to try again.'
|
|
237
273
|
)
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
throw cancelled
|
|
243
|
-
}
|
|
244
|
-
return response
|
|
245
|
-
}
|
|
274
|
+
throw cancelled
|
|
275
|
+
}
|
|
276
|
+
return { ok: response.ok, status: response.status, value }
|
|
277
|
+
})
|
|
246
278
|
try {
|
|
247
279
|
onPhase?.('waiting-for-receiver')
|
|
248
|
-
//
|
|
249
|
-
//
|
|
250
|
-
const budget =
|
|
280
|
+
// Count-only polling survives page suspension without consuming an overall deadline.
|
|
281
|
+
// Numeric/default budgets retain the legacy deadline across requests, bodies and gaps.
|
|
282
|
+
const budget = countOnly
|
|
283
|
+
? null
|
|
284
|
+
: Math.min(readyTimeoutMs, waitingForLaunch ? 120_000 : 300_000)
|
|
251
285
|
const attemptBudget = waitingForLaunch ? Math.min(attemptTimeoutMs, 2000) : attemptTimeoutMs
|
|
252
|
-
const deadline = Date.now() + budget
|
|
253
|
-
const
|
|
286
|
+
const deadline = budget === null ? null : Date.now() + budget
|
|
287
|
+
const poll = async (readySignal) => {
|
|
254
288
|
for (let attempt = 0; waitingForLaunch || attempt < readinessAttempts; attempt++) {
|
|
255
289
|
readySignal.throwIfAborted()
|
|
256
290
|
try {
|
|
@@ -259,7 +293,7 @@ export function prepareBrowserImport(file, options = {}) {
|
|
|
259
293
|
const response = await request(`/ready?challenge=${challenge}`, {
|
|
260
294
|
signal: attemptSignal
|
|
261
295
|
})
|
|
262
|
-
const value =
|
|
296
|
+
const value = response.value
|
|
263
297
|
if (
|
|
264
298
|
!value ||
|
|
265
299
|
typeof value !== 'object' ||
|
|
@@ -277,7 +311,7 @@ export function prepareBrowserImport(file, options = {}) {
|
|
|
277
311
|
`open-science-import-v1:${challenge}:${value.offset}:${value.accepted ? 1 : 0}`
|
|
278
312
|
)
|
|
279
313
|
attemptSignal.throwIfAborted()
|
|
280
|
-
if (Date.now() >= deadline)
|
|
314
|
+
if (deadline !== null && Date.now() >= deadline)
|
|
281
315
|
throw new BrowserImportError(
|
|
282
316
|
'timeout',
|
|
283
317
|
'Open-Science did not become ready in time.'
|
|
@@ -297,14 +331,16 @@ export function prepareBrowserImport(file, options = {}) {
|
|
|
297
331
|
'incompatible',
|
|
298
332
|
'Could not verify the local receiver identity.'
|
|
299
333
|
)
|
|
300
|
-
if (waitingForLaunch || attempt + 1 < readinessAttempts)
|
|
334
|
+
if (waitingForLaunch || attempt + 1 < readinessAttempts)
|
|
335
|
+
await wait(readinessIntervalMs, readySignal)
|
|
301
336
|
}
|
|
302
337
|
}
|
|
303
338
|
throw new BrowserImportError(
|
|
304
339
|
'readiness-exhausted',
|
|
305
340
|
'Open-Science did not become ready. Open the app, allow local network access, and retry.'
|
|
306
341
|
)
|
|
307
|
-
}
|
|
342
|
+
}
|
|
343
|
+
const ready = budget === null ? await poll(signal) : await bounded(signal, budget, poll)
|
|
308
344
|
verified = true
|
|
309
345
|
if (
|
|
310
346
|
!Number.isSafeInteger(ready.offset) ||
|
|
@@ -338,7 +374,7 @@ export function prepareBrowserImport(file, options = {}) {
|
|
|
338
374
|
}
|
|
339
375
|
})
|
|
340
376
|
if (!chunk.ok) throw new Error(`Local package transfer failed (${chunk.status}).`)
|
|
341
|
-
const result =
|
|
377
|
+
const result = chunk.value
|
|
342
378
|
if (
|
|
343
379
|
result.offset !== offset + bytes.byteLength ||
|
|
344
380
|
result.proof !== (await sign(`offset:${result.offset}`))
|
|
@@ -351,7 +387,7 @@ export function prepareBrowserImport(file, options = {}) {
|
|
|
351
387
|
method: 'POST',
|
|
352
388
|
headers: await authorize('POST', '/complete')
|
|
353
389
|
})
|
|
354
|
-
const receipt =
|
|
390
|
+
const receipt = complete.value
|
|
355
391
|
if (
|
|
356
392
|
complete.status !== 202 ||
|
|
357
393
|
receipt.received !== true ||
|
|
@@ -362,17 +398,7 @@ export function prepareBrowserImport(file, options = {}) {
|
|
|
362
398
|
received = true
|
|
363
399
|
return { received: true }
|
|
364
400
|
} catch (error) {
|
|
365
|
-
if (
|
|
366
|
-
await fetch(base, {
|
|
367
|
-
method: 'DELETE',
|
|
368
|
-
mode: 'cors',
|
|
369
|
-
credentials: 'omit',
|
|
370
|
-
redirect: 'error',
|
|
371
|
-
headers: await authorize('DELETE', ''),
|
|
372
|
-
signal: AbortSignal.timeout(2000),
|
|
373
|
-
targetAddressSpace: 'loopback'
|
|
374
|
-
}).catch(() => undefined)
|
|
375
|
-
}
|
|
401
|
+
if (signal?.aborted) await cancel()
|
|
376
402
|
signal?.throwIfAborted()
|
|
377
403
|
if (error.code === 'cancelled' || error.code === 'incompatible') throw error
|
|
378
404
|
if (verified)
|
package/cli.mjs
CHANGED
|
@@ -5,14 +5,19 @@
|
|
|
5
5
|
import { randomUUID } from 'node:crypto'
|
|
6
6
|
import { closeSync, createWriteStream, existsSync, openSync, realpathSync } from 'node:fs'
|
|
7
7
|
import { chmod, lstat, mkdir, readFile, readlink, rename, rm, stat } from 'node:fs/promises'
|
|
8
|
-
import { dirname, join, resolve } from 'node:path'
|
|
8
|
+
import { basename, dirname, join, resolve } from 'node:path'
|
|
9
9
|
import { Readable } from 'node:stream'
|
|
10
10
|
import { pipeline } from 'node:stream/promises'
|
|
11
11
|
import { text as readStreamText } from 'node:stream/consumers'
|
|
12
12
|
import { spawn } from 'node:child_process'
|
|
13
13
|
import { fileURLToPath } from 'node:url'
|
|
14
14
|
|
|
15
|
-
import {
|
|
15
|
+
import {
|
|
16
|
+
candidateConfigRoots,
|
|
17
|
+
findServiceState,
|
|
18
|
+
readWebToken,
|
|
19
|
+
resolveConfigRoot
|
|
20
|
+
} from './config-root.mjs'
|
|
16
21
|
import { codexLoginCommand } from './codex-login.mjs'
|
|
17
22
|
import { connectToOpenScience, OpenScienceApiError } from './index.mjs'
|
|
18
23
|
import { locateBackend } from './locate-backend.mjs'
|
|
@@ -27,6 +32,9 @@ const MAX_DOWNLOAD_SYMLINK_HOPS = 40
|
|
|
27
32
|
const usage = `Usage: open-science <command> [options]
|
|
28
33
|
|
|
29
34
|
Commands:
|
|
35
|
+
version Print the local application version (also --version or -v)
|
|
36
|
+
help [command] Show general or command help
|
|
37
|
+
paths Show local CLI, Node and configuration discovery paths
|
|
30
38
|
init Create the local CLI configuration directory
|
|
31
39
|
start Start the headless backend and localhost web UI
|
|
32
40
|
stop Gracefully stop the backend
|
|
@@ -54,6 +62,8 @@ Commands:
|
|
|
54
62
|
session status <session-id>
|
|
55
63
|
session config show <session-id>
|
|
56
64
|
session config update <session-id> --revision <number> [session options]
|
|
65
|
+
session export <session-id> --project <id-or-name> --output <path>
|
|
66
|
+
session import <path> [--project <id-or-name> | --new-project <name>] --non-interactive
|
|
57
67
|
settings agent-routing show
|
|
58
68
|
settings agent-routing update [routing options]
|
|
59
69
|
plan show <session-id>
|
|
@@ -101,14 +111,17 @@ Options:
|
|
|
101
111
|
--wait Wait for the run to finish
|
|
102
112
|
--return-on-attention With --wait, return when the Plan needs approval
|
|
103
113
|
--timeout-ms <ms> Stop waiting after this many milliseconds
|
|
114
|
+
--output <path> Destination path (session export, artifacts download)
|
|
115
|
+
--new-project <name> Import into a new Project with this name (session import)
|
|
116
|
+
--non-interactive Import without an interactive preview confirmation (session import)
|
|
104
117
|
--cancel-on-timeout Cancel the server run when --timeout-ms expires
|
|
105
118
|
--jsonl With run --wait, stream one machine-readable event per line
|
|
106
|
-
--output <path> Artifact download destination
|
|
107
119
|
--credential-store <os|file> Settings credential storage (Linux headless; start only)
|
|
108
120
|
--no-open Do not open the browser after start
|
|
109
121
|
--no-sandbox Disable Chromium's process sandbox (security risk; start only)
|
|
110
122
|
--force Sign in again even when Codex credentials already exist
|
|
111
123
|
--json Emit one machine-readable result
|
|
124
|
+
-v, --version Print the local application version and exit
|
|
112
125
|
-h, --help Show this help`
|
|
113
126
|
|
|
114
127
|
// Flags that take a value, mapped to their camelCase option key (explicit so new hyphenated flags
|
|
@@ -151,7 +164,8 @@ const VALUE_OPTIONS = {
|
|
|
151
164
|
'--description': 'description',
|
|
152
165
|
'--agent-context': 'agentContext',
|
|
153
166
|
'--agent-context-file': 'agentContextFile',
|
|
154
|
-
'--output': 'output'
|
|
167
|
+
'--output': 'output',
|
|
168
|
+
'--new-project': 'newProject'
|
|
155
169
|
}
|
|
156
170
|
|
|
157
171
|
const TASK_COMMANDS = new Set([
|
|
@@ -186,6 +200,9 @@ const GROUP_COMMANDS = new Set([
|
|
|
186
200
|
// Project create, update, and session-defaults intentionally remain unbounded because their
|
|
187
201
|
// positional Project names may contain multiple unquoted words.
|
|
188
202
|
const POSITIONAL_LIMITS = new Map([
|
|
203
|
+
['version', 0],
|
|
204
|
+
['help', 1],
|
|
205
|
+
['paths', 0],
|
|
189
206
|
['doctor', 0],
|
|
190
207
|
['runtime list', 0],
|
|
191
208
|
['runtime install', 1],
|
|
@@ -216,6 +233,8 @@ const POSITIONAL_LIMITS = new Map([
|
|
|
216
233
|
['run cancel', 1],
|
|
217
234
|
['session status', 1],
|
|
218
235
|
['session config', 2],
|
|
236
|
+
['session export', 1],
|
|
237
|
+
['session import', 1],
|
|
219
238
|
['settings agent-routing', 1],
|
|
220
239
|
['plan show', 1],
|
|
221
240
|
['plan approve', 1],
|
|
@@ -255,7 +274,17 @@ const assertPositionalLimit = (command, subcommand, positionals) => {
|
|
|
255
274
|
|
|
256
275
|
export const parseCliArgs = (argv) => {
|
|
257
276
|
const args = [...argv]
|
|
258
|
-
const
|
|
277
|
+
const requestedCommand = args.shift()
|
|
278
|
+
const command =
|
|
279
|
+
requestedCommand === '--version' || requestedCommand === '-v' ? 'version' : requestedCommand
|
|
280
|
+
const localOptions =
|
|
281
|
+
command === 'version'
|
|
282
|
+
? ['--json', '--help', '-h']
|
|
283
|
+
: command === 'paths'
|
|
284
|
+
? ['--json', '--help', '-h', '--config-root', '--profile']
|
|
285
|
+
: command === 'help'
|
|
286
|
+
? ['--help', '-h']
|
|
287
|
+
: undefined
|
|
259
288
|
const subcommand =
|
|
260
289
|
(GROUP_COMMANDS.has(command) && args[0] && !args[0].startsWith('-')) ||
|
|
261
290
|
(command === 'run' && (args[0] === 'status' || args[0] === 'cancel'))
|
|
@@ -270,6 +299,9 @@ export const parseCliArgs = (argv) => {
|
|
|
270
299
|
const positionals = []
|
|
271
300
|
while (args.length > 0) {
|
|
272
301
|
const arg = args.shift()
|
|
302
|
+
if (localOptions && arg.startsWith('-') && !localOptions.includes(arg)) {
|
|
303
|
+
throw new CliUsageError(`Unknown option for ${command}: ${arg}`)
|
|
304
|
+
}
|
|
273
305
|
if (arg.startsWith('--credential-store=')) {
|
|
274
306
|
if (options.credentialStore !== undefined)
|
|
275
307
|
throw new CliUsageError('Specify --credential-store only once.')
|
|
@@ -284,6 +316,7 @@ export const parseCliArgs = (argv) => {
|
|
|
284
316
|
else if (arg === '--wait') options.wait = true
|
|
285
317
|
else if (arg === '--return-on-attention') options.returnOnAttention = true
|
|
286
318
|
else if (arg === '--plan-first') options.planFirst = true
|
|
319
|
+
else if (arg === '--non-interactive') options.nonInteractive = true
|
|
287
320
|
else if (arg === '--provider-default-model') options.providerDefaultModel = true
|
|
288
321
|
else if (arg === '--clear-compute-hosts') options.clearComputeHosts = true
|
|
289
322
|
else if (arg === '--memory') {
|
|
@@ -347,6 +380,10 @@ export const parseCliArgs = (argv) => {
|
|
|
347
380
|
...(positionals.length ? { positionals } : {}),
|
|
348
381
|
options
|
|
349
382
|
}
|
|
383
|
+
if (localOptions) {
|
|
384
|
+
assertPositionalLimit(command, subcommand, positionals)
|
|
385
|
+
return parsed
|
|
386
|
+
}
|
|
350
387
|
// Help is local: it must not require execution-only options or a running backend.
|
|
351
388
|
if (options.help) return parsed
|
|
352
389
|
if (options.port !== undefined) {
|
|
@@ -435,8 +472,18 @@ export const parseCliArgs = (argv) => {
|
|
|
435
472
|
if (options.jsonl && (command !== 'run' || subcommand || !options.wait)) {
|
|
436
473
|
throw new CliUsageError('--jsonl requires run --wait.')
|
|
437
474
|
}
|
|
438
|
-
|
|
439
|
-
|
|
475
|
+
const isPackageTransfer = command === 'session' && ['export', 'import'].includes(subcommand)
|
|
476
|
+
if (
|
|
477
|
+
options.timeoutMs !== undefined &&
|
|
478
|
+
!isPackageTransfer &&
|
|
479
|
+
(command !== 'run' || subcommand || !options.wait)
|
|
480
|
+
) {
|
|
481
|
+
throw new CliUsageError('--timeout-ms requires run --wait or session export/import.')
|
|
482
|
+
}
|
|
483
|
+
if (command === 'session' && subcommand === 'import') {
|
|
484
|
+
if (!options.nonInteractive) throw new CliUsageError('--non-interactive is required.')
|
|
485
|
+
if (options.project && options.newProject)
|
|
486
|
+
throw new CliUsageError('Use only one of --project and --new-project.')
|
|
440
487
|
}
|
|
441
488
|
if (options.returnOnAttention && (command !== 'run' || subcommand || !options.wait)) {
|
|
442
489
|
throw new CliUsageError('--return-on-attention requires run --wait.')
|
|
@@ -1514,6 +1561,40 @@ export const runTaskCommand = async (parsed, dependencies = {}) => {
|
|
|
1514
1561
|
)
|
|
1515
1562
|
return
|
|
1516
1563
|
}
|
|
1564
|
+
if (command === 'session' && subcommand === 'export') {
|
|
1565
|
+
const sessionId = positionals[0]
|
|
1566
|
+
if (!sessionId) throw new CliUsageError('Session id is required.')
|
|
1567
|
+
if (!options.project) throw new CliUsageError('--project is required.')
|
|
1568
|
+
if (!options.output) throw new CliUsageError('--output is required.')
|
|
1569
|
+
const projectId = await resolveCliProjectId(client, options.project)
|
|
1570
|
+
outputValue(
|
|
1571
|
+
await client.exportSessionPackage(projectId, sessionId, resolve(options.output), {
|
|
1572
|
+
timeoutMs: options.timeoutMs
|
|
1573
|
+
}),
|
|
1574
|
+
options,
|
|
1575
|
+
deps
|
|
1576
|
+
)
|
|
1577
|
+
return
|
|
1578
|
+
}
|
|
1579
|
+
if (command === 'session' && subcommand === 'import') {
|
|
1580
|
+
const packagePath = positionals[0]
|
|
1581
|
+
if (!packagePath) throw new CliUsageError('Session package path is required.')
|
|
1582
|
+
const resolvedPath = resolve(packagePath)
|
|
1583
|
+
const target = {}
|
|
1584
|
+
if (options.project) target.projectId = await resolveCliProjectId(client, options.project)
|
|
1585
|
+
else if (options.newProject) target.projectName = options.newProject
|
|
1586
|
+
outputValue(
|
|
1587
|
+
await client.importSessionPackage(
|
|
1588
|
+
target,
|
|
1589
|
+
resolvedPath,
|
|
1590
|
+
{ nonInteractive: true },
|
|
1591
|
+
{ timeoutMs: options.timeoutMs }
|
|
1592
|
+
),
|
|
1593
|
+
options,
|
|
1594
|
+
deps
|
|
1595
|
+
)
|
|
1596
|
+
return
|
|
1597
|
+
}
|
|
1517
1598
|
if (command === 'settings' && subcommand === 'agent-routing') {
|
|
1518
1599
|
const action = positionals[0]
|
|
1519
1600
|
if (action === 'show') {
|
|
@@ -1749,14 +1830,55 @@ ${commands.join('\n')}
|
|
|
1749
1830
|
Run "open-science --help" for the complete option reference.`
|
|
1750
1831
|
}
|
|
1751
1832
|
|
|
1833
|
+
const readCliMetadata = async () => {
|
|
1834
|
+
const here = dirname(fileURLToPath(import.meta.url))
|
|
1835
|
+
// Source SDK metadata has its own version; installed CLI manifests carry the release version.
|
|
1836
|
+
const development =
|
|
1837
|
+
basename(here) === 'open-science' &&
|
|
1838
|
+
basename(dirname(here)) === 'packages' &&
|
|
1839
|
+
existsSync(join(here, '../../src/main/node-entry.ts'))
|
|
1840
|
+
const manifestPath = join(development ? join(here, '../..') : here, 'package.json')
|
|
1841
|
+
const manifest = JSON.parse(await readFile(manifestPath, 'utf8'))
|
|
1842
|
+
if (
|
|
1843
|
+
manifest.name !== (development ? 'open-science' : '@aipoch/open-science') ||
|
|
1844
|
+
typeof manifest.version !== 'string' ||
|
|
1845
|
+
!/^\d+\.\d+\.\d+(?:-[\da-zA-Z.-]+)?(?:\+[\da-zA-Z.-]+)?$/.test(manifest.version)
|
|
1846
|
+
) {
|
|
1847
|
+
throw new Error(`Invalid Open-Science CLI metadata: ${manifestPath}`)
|
|
1848
|
+
}
|
|
1849
|
+
return { version: manifest.version, development }
|
|
1850
|
+
}
|
|
1851
|
+
|
|
1852
|
+
const pathsCommand = async (options) => {
|
|
1853
|
+
const { development } = await readCliMetadata()
|
|
1854
|
+
const paths = {
|
|
1855
|
+
cliPath: fileURLToPath(import.meta.url),
|
|
1856
|
+
nodePath: process.execPath,
|
|
1857
|
+
startupConfigRoot: resolveConfigRoot({
|
|
1858
|
+
packaged: !development,
|
|
1859
|
+
override: options.configRoot
|
|
1860
|
+
}),
|
|
1861
|
+
discoveryConfigRoots: candidateConfigRoots({ override: options.configRoot })
|
|
1862
|
+
}
|
|
1863
|
+
console.log(
|
|
1864
|
+
options.json
|
|
1865
|
+
? JSON.stringify(paths)
|
|
1866
|
+
: `CLI: ${paths.cliPath}\nNode: ${paths.nodePath}\nStartup configuration: ${paths.startupConfigRoot}\nService discovery (in order):\n${paths.discoveryConfigRoots.map((root) => ` ${root}`).join('\n')}`
|
|
1867
|
+
)
|
|
1868
|
+
}
|
|
1869
|
+
|
|
1752
1870
|
export const runCli = async (argv = process.argv.slice(2)) => {
|
|
1753
1871
|
const parsed = parseCliArgs(argv)
|
|
1754
1872
|
const { command, options } = parsed
|
|
1755
|
-
if (options.help || !command || command === '-h' || command === '--help') {
|
|
1756
|
-
console.log(commandHelp(command))
|
|
1873
|
+
if (options.help || !command || command === 'help' || command === '-h' || command === '--help') {
|
|
1874
|
+
console.log(commandHelp(command === 'help' ? parsed.positionals?.[0] : command))
|
|
1757
1875
|
return
|
|
1758
1876
|
}
|
|
1759
|
-
if (command === '
|
|
1877
|
+
if (command === 'version') {
|
|
1878
|
+
const { version } = await readCliMetadata()
|
|
1879
|
+
console.log(options.json ? JSON.stringify({ version }) : version)
|
|
1880
|
+
} else if (command === 'paths') await pathsCommand(options)
|
|
1881
|
+
else if (command === 'init') await initCommand(options)
|
|
1760
1882
|
else if (command === 'start') await startCommand(options)
|
|
1761
1883
|
else if (command === 'stop') await stopCommand(options)
|
|
1762
1884
|
else if (command === 'status') await statusCommand(options)
|
package/index.d.ts
CHANGED
|
@@ -99,6 +99,7 @@ export type ConnectorsSnapshot = {
|
|
|
99
99
|
ncbi: NcbiCredentialsView
|
|
100
100
|
// Optional only for compatibility with an older main process during local development.
|
|
101
101
|
openAlex?: OpenAlexCredentialView
|
|
102
|
+
semanticScholar?: { hasApiKey: boolean }
|
|
102
103
|
}
|
|
103
104
|
|
|
104
105
|
export type DeviceCredentialKind = 'api_key' | 'token' | 'oauth'
|
package/index.mjs
CHANGED
|
@@ -6,6 +6,7 @@ const defaultSleep = (milliseconds) =>
|
|
|
6
6
|
new Promise((resolveSleep) => setTimeout(resolveSleep, milliseconds))
|
|
7
7
|
|
|
8
8
|
const DEFAULT_REQUEST_TIMEOUT_MS = 30_000
|
|
9
|
+
const PACKAGE_REQUEST_TIMEOUT_MS = 60 * 60 * 1_000
|
|
9
10
|
const DEFAULT_EVENT_IDLE_TIMEOUT_MS = 30_000
|
|
10
11
|
const EVENT_RECONNECT_DELAY_MS = 100
|
|
11
12
|
const MAX_BUFFERED_EVENTS = 1_024
|
|
@@ -308,6 +309,54 @@ export class OpenScienceClient {
|
|
|
308
309
|
return this.request(`/api/v1/sessions/${encodeURIComponent(sessionId)}/config`, options)
|
|
309
310
|
}
|
|
310
311
|
|
|
312
|
+
// Web RPC seam: authenticated loopback dispatch of one application command channel.
|
|
313
|
+
// The CLI reaches headless-only commands (Session package export/import) this way.
|
|
314
|
+
async callRpc(channel, args, options = {}) {
|
|
315
|
+
return withRequestTimeout(
|
|
316
|
+
{
|
|
317
|
+
defaultTimeoutMs: this.requestTimeoutMs,
|
|
318
|
+
signal: options.signal,
|
|
319
|
+
timeoutMs: options.timeoutMs
|
|
320
|
+
},
|
|
321
|
+
async (signal) => {
|
|
322
|
+
const response = await this.fetch(`${this.baseUrl}/rpc/${encodeURIComponent(channel)}`, {
|
|
323
|
+
method: 'POST',
|
|
324
|
+
headers: {
|
|
325
|
+
authorization: `Bearer ${this.token}`,
|
|
326
|
+
accept: 'application/json',
|
|
327
|
+
'content-type': 'application/json'
|
|
328
|
+
},
|
|
329
|
+
body: JSON.stringify({ protocolVersion: 1, args }),
|
|
330
|
+
signal
|
|
331
|
+
})
|
|
332
|
+
const payload = await response.json().catch(() => undefined)
|
|
333
|
+
if (!response.ok || payload?.ok !== true) {
|
|
334
|
+
throw new OpenScienceApiError(
|
|
335
|
+
payload?.error?.message ?? `Open-Science Web RPC request failed (${response.status}).`,
|
|
336
|
+
{ code: payload?.error?.code, status: response.status }
|
|
337
|
+
)
|
|
338
|
+
}
|
|
339
|
+
return payload.result
|
|
340
|
+
}
|
|
341
|
+
)
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
exportSessionPackage(projectId, sessionId, destinationPath, options) {
|
|
345
|
+
return this.callRpc('sessions:export-package', [{ projectId, sessionId }, destinationPath], {
|
|
346
|
+
...options,
|
|
347
|
+
timeoutMs: options?.timeoutMs ?? PACKAGE_REQUEST_TIMEOUT_MS
|
|
348
|
+
})
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
importSessionPackage(target, sourcePath, options, rpcOptions) {
|
|
352
|
+
if (options?.nonInteractive !== true)
|
|
353
|
+
throw new TypeError('Non-interactive Session import requires explicit confirmation.')
|
|
354
|
+
return this.callRpc('sessions:import-package', [target, sourcePath, { nonInteractive: true }], {
|
|
355
|
+
...rpcOptions,
|
|
356
|
+
timeoutMs: rpcOptions?.timeoutMs ?? PACKAGE_REQUEST_TIMEOUT_MS
|
|
357
|
+
})
|
|
358
|
+
}
|
|
359
|
+
|
|
311
360
|
updateSessionConfiguration(sessionId, request, options) {
|
|
312
361
|
return this.request(`/api/v1/sessions/${encodeURIComponent(sessionId)}/config`, {
|
|
313
362
|
...options,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aipoch/open-science",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.3",
|
|
4
4
|
"description": "Node.js SDK and CLI for the Open-Science local daemon",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -38,10 +38,10 @@
|
|
|
38
38
|
"access": "public"
|
|
39
39
|
},
|
|
40
40
|
"optionalDependencies": {
|
|
41
|
-
"@aipoch/open-science-darwin-arm64": "1.0.
|
|
42
|
-
"@aipoch/open-science-darwin-x64": "1.0.
|
|
43
|
-
"@aipoch/open-science-linux-x64-gnu": "1.0.
|
|
44
|
-
"@aipoch/open-science-linux-arm64-gnu": "1.0.
|
|
45
|
-
"@aipoch/open-science-win32-x64": "1.0.
|
|
41
|
+
"@aipoch/open-science-darwin-arm64": "1.0.3",
|
|
42
|
+
"@aipoch/open-science-darwin-x64": "1.0.3",
|
|
43
|
+
"@aipoch/open-science-linux-x64-gnu": "1.0.3",
|
|
44
|
+
"@aipoch/open-science-linux-arm64-gnu": "1.0.3",
|
|
45
|
+
"@aipoch/open-science-win32-x64": "1.0.3"
|
|
46
46
|
}
|
|
47
47
|
}
|