@aipoch/open-science 0.0.0-stage → 1.0.2

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/LICENSE ADDED
@@ -0,0 +1,201 @@
1
+ Apache License
2
+ Version 2.0, January 2004
3
+ http://www.apache.org/licenses/
4
+
5
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6
+
7
+ 1. Definitions.
8
+
9
+ "License" shall mean the terms and conditions for use, reproduction,
10
+ and distribution as defined by Sections 1 through 9 of this document.
11
+
12
+ "Licensor" shall mean the copyright owner or entity authorized by
13
+ the copyright owner that is granting the License.
14
+
15
+ "Legal Entity" shall mean the union of the acting entity and all
16
+ other entities that control, are controlled by, or are under common
17
+ control with that entity. For the purposes of this definition,
18
+ "control" means (i) the power, direct or indirect, to cause the
19
+ direction or management of such entity, whether by contract or
20
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
21
+ outstanding shares, or (iii) beneficial ownership of such entity.
22
+
23
+ "You" (or "Your") shall mean an individual or Legal Entity
24
+ exercising permissions granted by this License.
25
+
26
+ "Source" form shall mean the preferred form for making modifications,
27
+ including but not limited to software source code, documentation
28
+ source, and configuration files.
29
+
30
+ "Object" form shall mean any form resulting from mechanical
31
+ transformation or translation of a Source form, including but
32
+ not limited to compiled object code, generated documentation,
33
+ and conversions to other media types.
34
+
35
+ "Work" shall mean the work of authorship, whether in Source or
36
+ Object form, made available under the License, as indicated by a
37
+ copyright notice that is included in or attached to the work
38
+ (an example is provided in the Appendix below).
39
+
40
+ "Derivative Works" shall mean any work, whether in Source or Object
41
+ form, that is based on (or derived from) the Work and for which the
42
+ editorial revisions, annotations, elaborations, or other modifications
43
+ represent, as a whole, an original work of authorship. For the purposes
44
+ of this License, Derivative Works shall not include works that remain
45
+ separable from, or merely link (or bind by name) to the interfaces of,
46
+ the Work and Derivative Works thereof.
47
+
48
+ "Contribution" shall mean any work of authorship, including
49
+ the original version of the Work and any modifications or additions
50
+ to that Work or Derivative Works thereof, that is intentionally
51
+ submitted to Licensor for inclusion in the Work by the copyright owner
52
+ or by an individual or Legal Entity authorized to submit on behalf of
53
+ the copyright owner. For the purposes of this definition, "submitted"
54
+ means any form of electronic, verbal, or written communication sent
55
+ to the Licensor or its representatives, including but not limited to
56
+ communication on electronic mailing lists, source code control systems,
57
+ and issue tracking systems that are managed by, or on behalf of, the
58
+ Licensor for the purpose of discussing and improving the Work, but
59
+ excluding communication that is conspicuously marked or otherwise
60
+ designated in writing by the copyright owner as "Not a Contribution."
61
+
62
+ "Contributor" shall mean Licensor and any individual or Legal Entity
63
+ on behalf of whom a Contribution has been received by Licensor and
64
+ subsequently incorporated within the Work.
65
+
66
+ 2. Grant of Copyright License. Subject to the terms and conditions of
67
+ this License, each Contributor hereby grants to You a perpetual,
68
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69
+ copyright license to reproduce, prepare Derivative Works of,
70
+ publicly display, publicly perform, sublicense, and distribute the
71
+ Work and such Derivative Works in Source or Object form.
72
+
73
+ 3. Grant of Patent License. Subject to the terms and conditions of
74
+ this License, each Contributor hereby grants to You a perpetual,
75
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76
+ (except as stated in this section) patent license to make, have made,
77
+ use, offer to sell, sell, import, and otherwise transfer the Work,
78
+ where such license applies only to those patent claims licensable
79
+ by such Contributor that are necessarily infringed by their
80
+ Contribution(s) alone or by combination of their Contribution(s)
81
+ with the Work to which such Contribution(s) was submitted. If You
82
+ institute patent litigation against any entity (including a
83
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
84
+ or a Contribution incorporated within the Work constitutes direct
85
+ or contributory patent infringement, then any patent licenses
86
+ granted to You under this License for that Work shall terminate
87
+ as of the date such litigation is filed.
88
+
89
+ 4. Redistribution. You may reproduce and distribute copies of the
90
+ Work or Derivative Works thereof in any medium, with or without
91
+ modifications, and in Source or Object form, provided that You
92
+ meet the following conditions:
93
+
94
+ (a) You must give any other recipients of the Work or
95
+ Derivative Works a copy of this License; and
96
+
97
+ (b) You must cause any modified files to carry prominent notices
98
+ stating that You changed the files; and
99
+
100
+ (c) You must retain, in the Source form of any Derivative Works
101
+ that You distribute, all copyright, patent, trademark, and
102
+ attribution notices from the Source form of the Work,
103
+ excluding those notices that do not pertain to any part of
104
+ the Derivative Works; and
105
+
106
+ (d) If the Work includes a "NOTICE" text file as part of its
107
+ distribution, then any Derivative Works that You distribute must
108
+ include a readable copy of the attribution notices contained
109
+ within such NOTICE file, excluding those notices that do not
110
+ pertain to any part of the Derivative Works, in at least one
111
+ of the following places: within a NOTICE text file distributed
112
+ as part of the Derivative Works; within the Source form or
113
+ documentation, if provided along with the Derivative Works; or,
114
+ within a display generated by the Derivative Works, if and
115
+ wherever such third-party notices normally appear. The contents
116
+ of the NOTICE file are for informational purposes only and
117
+ do not modify the License. You may add Your own attribution
118
+ notices within Derivative Works that You distribute, alongside
119
+ or as an addendum to the NOTICE text from the Work, provided
120
+ that such additional attribution notices cannot be construed
121
+ as modifying the License.
122
+
123
+ You may add Your own copyright statement to Your modifications and
124
+ may provide additional or different license terms and conditions
125
+ for use, reproduction, or distribution of Your modifications, or
126
+ for any such Derivative Works as a whole, provided Your use,
127
+ reproduction, and distribution of the Work otherwise complies with
128
+ the conditions stated in this License.
129
+
130
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
131
+ any Contribution intentionally submitted for inclusion in the Work
132
+ by You to the Licensor shall be under the terms and conditions of
133
+ this License, without any additional terms or conditions.
134
+ Notwithstanding the above, nothing herein shall supersede or modify
135
+ the terms of any separate license agreement you may have executed
136
+ with Licensor regarding such Contributions.
137
+
138
+ 6. Trademarks. This License does not grant permission to use the trade
139
+ names, trademarks, service marks, or product names of the Licensor,
140
+ except as required for reasonable and customary use in describing the
141
+ origin of the Work and reproducing the content of the NOTICE file.
142
+
143
+ 7. Disclaimer of Warranty. Unless required by applicable law or
144
+ agreed to in writing, Licensor provides the Work (and each
145
+ Contributor provides its Contributions) on an "AS IS" BASIS,
146
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147
+ implied, including, without limitation, any warranties or conditions
148
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149
+ PARTICULAR PURPOSE. You are solely responsible for determining the
150
+ appropriateness of using or redistributing the Work and assume any
151
+ risks associated with Your exercise of permissions under this License.
152
+
153
+ 8. Limitation of Liability. In no event and under no legal theory,
154
+ whether in tort (including negligence), contract, or otherwise,
155
+ unless required by applicable law (such as deliberate and grossly
156
+ negligent acts) or agreed to in writing, shall any Contributor be
157
+ liable to You for damages, including any direct, indirect, special,
158
+ incidental, or consequential damages of any character arising as a
159
+ result of this License or out of the use or inability to use the
160
+ Work (including but not limited to damages for loss of goodwill,
161
+ work stoppage, computer failure or malfunction, or any and all
162
+ other commercial damages or losses), even if such Contributor
163
+ has been advised of the possibility of such damages.
164
+
165
+ 9. Accepting Warranty or Additional Liability. While redistributing
166
+ the Work or Derivative Works thereof, You may choose to offer,
167
+ and charge a fee for, acceptance of support, warranty, indemnity,
168
+ or other liability obligations and/or rights consistent with this
169
+ License. However, in accepting such obligations, You may act only
170
+ on Your own behalf and on Your sole responsibility, not on behalf
171
+ of any other Contributor, and only if You agree to indemnify,
172
+ defend, and hold each Contributor harmless for any liability
173
+ incurred by, or claims asserted against, such Contributor by reason
174
+ of your accepting any such warranty or additional liability.
175
+
176
+ END OF TERMS AND CONDITIONS
177
+
178
+ APPENDIX: How to apply the Apache License to your work.
179
+
180
+ To apply the Apache License to your work, attach the following
181
+ boilerplate notice, with the fields enclosed by brackets "[]"
182
+ replaced with your own identifying information. (Don't include
183
+ the brackets!) The text should be enclosed in the appropriate
184
+ comment syntax for the file format. We also recommend that a
185
+ file or class name and description of purpose be included on the
186
+ same "printed page" as the copyright notice for easier
187
+ identification within third-party archives.
188
+
189
+ Copyright 2026 AIPOCH
190
+
191
+ Licensed under the Apache License, Version 2.0 (the "License");
192
+ you may not use this file except in compliance with the License.
193
+ You may obtain a copy of the License at
194
+
195
+ http://www.apache.org/licenses/LICENSE-2.0
196
+
197
+ Unless required by applicable law or agreed to in writing, software
198
+ distributed under the License is distributed on an "AS IS" BASIS,
199
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200
+ See the License for the specific language governing permissions and
201
+ limitations under the License.
package/README.md CHANGED
@@ -1,3 +1,285 @@
1
- # Temporary Holding Version
1
+ # @aipoch/open-science
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
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.
80
+
81
+ ## Documentation
82
+
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
86
+ - [Release compatibility](https://github.com/aipoch/open-science/blob/main/docs/compatibility.md) - stable interfaces, versioning, and data upgrades
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
88
+
89
+ ## Safety and integration
90
+
91
+ - **Scope:** Authenticated control of a local Open-Science service; the client does not provide an
92
+ independent execution sandbox or isolate the service's Agents and tools.
93
+ - **Controls:** Requests use the service's permission profiles, existing grants, and host-owned
94
+ runtime controls. Disabling interactive permission prompts denies requests needing a human; it
95
+ does not erase previously granted authority.
96
+ - **Remaining risks:** A successful automated run can modify allowed files, contact configured
97
+ services, or start remote jobs. A timeout or aborted wait stops observation, not the underlying run.
98
+ Research outputs still need scientific review.
99
+ - **Safe integration:** Protect local service credentials, review the effective profile/grants and
100
+ recipients, and use isolated test data before unattended operation. Cancel explicitly and confirm
101
+ the resulting task/job state. Inspect downloaded or exported artifacts before sharing.
102
+
103
+ ## Browser-held package import
104
+
105
+ Use `@aipoch/open-science/browser-import` to send a downloaded `.science` Blob to the desktop app.
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,
107
+ limits, and browser requirements. The desktop never downloads the online package URL.
108
+
109
+ ## SDK quick start
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
+
120
+ ```js
121
+ import { connectToOpenScience } from '@aipoch/open-science'
122
+
123
+ const client = await connectToOpenScience()
124
+ const project = await client.createProject({ name: 'Evidence review' })
125
+ const run = await client.startRun({
126
+ project: project.id,
127
+ prompt: 'Summarize the evidence.',
128
+ permissionProfile: 'auto',
129
+ permissionPrompts: 'none' // Deny requests that would require a human; keep existing permission rules.
130
+ })
131
+ const result = await client.waitForRun(run.id)
132
+ console.log(result.output)
133
+ ```
134
+
135
+ Session, Project-default, and global Agent-routing configuration use the same authenticated local
136
+ API:
137
+
138
+ ```js
139
+ const configuration = await client.getSessionConfiguration(run.sessionId)
140
+ await client.updateSessionConfiguration(run.sessionId, {
141
+ expectedRevision: configuration.revision,
142
+ memoryEnabled: false
143
+ })
144
+
145
+ const defaults = await client.getProjectSessionDefaults(project.id)
146
+ await client.updateProjectSessionDefaults(project.id, {
147
+ expectedUpdatedAt: defaults.updatedAt,
148
+ patch: { permissionProfile: 'auto' }
149
+ })
150
+
151
+ await client.updateAgentRouting({
152
+ framework: 'codex',
153
+ reviewer: { mode: 'inherit' },
154
+ subagent: { mode: 'inherit' }
155
+ })
156
+
157
+ const runtimes = await client.listRuntimes()
158
+ ```
159
+
160
+ Session updates use `revision`; Project-default updates use `updatedAt`. Both reject stale writes.
161
+ Project defaults are copied only when a Session is created, with precedence `startRun` request,
162
+ Project defaults, application settings, then provider default. Agent-routing updates are atomic and
163
+ never return provider credentials.
164
+
165
+ Agent runtime listings expose only framework, readiness, version, and managed/external source. They
166
+ do not expose executable paths.
167
+
168
+ SDK requests have a 30-second default deadline that remains active while the response body is being
169
+ consumed. Override the client default with `requestTimeoutMs`, or pass `{ signal, timeoutMs }` as the
170
+ final options argument to an individual request method. `downloadArtifact` keeps the deadline active
171
+ while its returned `Response` body is streaming. `waitForRun` applies its overall `timeoutMs` and
172
+ caller signal to every in-flight polling request as well as the delay between polls. Each poll is
173
+ bounded by the smaller of the client's `requestTimeoutMs` and the remaining total wait budget.
174
+ A request timeout fails the wait immediately; it does not silently retry. Without a total
175
+ `timeoutMs`, waiting has no overall deadline. `pollIntervalMs` defaults to 250 and must be a finite
176
+ positive number.
177
+
178
+ Aborting or timing out a wait stops only the local observation. The Run may continue on the host;
179
+ call `getRun(run.id)` to check it, or explicitly call `cancelRun(run.id)` to stop it. With
180
+ `returnOnAttention: true`, a returned Run may still have `status: 'running'` and need a Plan decision before it can finish. Ordinary permissions do not populate Run attention.
181
+
182
+ ```js
183
+ const waitController = new AbortController()
184
+ const result = await client.waitForRun(run.id, {
185
+ timeoutMs: 120_000, // Total wait budget; each poll still has the client request deadline.
186
+ pollIntervalMs: 500,
187
+ returnOnAttention: true,
188
+ signal: waitController.signal // Aborts observation; does not call cancelRun.
189
+ })
190
+ console.log(result.status, result.attention)
191
+ ```
192
+
193
+ For retry-safe project creation and Run starts, pass the same `idempotencyKey` in the final options
194
+ argument on every attempt. The daemon replays the first response for up to 24 hours while it remains
195
+ running; reusing a key with a different request body fails with `idempotency_conflict`. If the
196
+ bounded replay registry is full, a new unique key fails with `idempotency_unavailable` rather than
197
+ evicting an existing guarantee. A lost response after submission does not prove that the Run was
198
+ not started. Retry with the original key and identical request while that daemon is still running,
199
+ or query the known Run ID. The replay registry is process-local: a daemon restart loses it, so
200
+ reusing a key after restart is not a durable exactly-once guarantee.
201
+
202
+ The `project` request field and the `listSessions(projectId)` argument both require a Project ID.
203
+ Project display names are not accepted as routing identifiers.
204
+
205
+ SDK and HTTP callers must supply an absolute `cwd`. Open-Science canonicalizes and validates it,
206
+ persists it as the Session working directory, and returns the effective path on every Run. Supplying
207
+ `cwd` with `sessionId` is allowed only when both paths resolve to the same directory. Omit `cwd` to
208
+ use a managed workspace. External working directories remain caller-owned and are never removed by
209
+ Open-Science.
210
+
211
+ For live automation feedback, subscribe before starting the Run. `run.progress` reports ordered
212
+ provider-neutral phases and emits a progress heartbeat every ten seconds until the first visible
213
+ provider output. This progress heartbeat describes Run activity; it is separate from the filtered
214
+ connection heartbeat that keeps an otherwise-idle WebSocket alive. The timer starts after Task has
215
+ prepared the Session and registered its Run; Session creation or resume time before registration is
216
+ outside this event stream:
217
+
218
+ ```js
219
+ const abortController = new AbortController()
220
+ const events = client.events({ signal: abortController.signal })
221
+ await events.ready
222
+
223
+ const progress = (async () => {
224
+ for await (const event of events) {
225
+ if (event.type === 'run.progress') {
226
+ console.log(event.data.phase, event.data.elapsedMs, event.data.heartbeat)
227
+ }
228
+ }
229
+ })()
230
+
231
+ const observedRun = await client.startRun({
232
+ project: project.id,
233
+ prompt: 'Summarize the evidence.',
234
+ permissionProfile: 'auto'
235
+ })
236
+ const observedResult = await client.waitForRun(observedRun.id)
237
+ abortController.abort()
238
+ await progress
239
+ console.log(observedResult.output)
240
+ ```
241
+
242
+ `events.ready` rejects if the socket fails, closes, or receives no liveness frame before opening.
243
+ After opening, the iterator fails with `timeout` when no event or connection heartbeat arrives for
244
+ 30 seconds. Pass `idleTimeoutMs` to change that connection-liveness window; it does not limit model,
245
+ Notebook, permission-approval, or other Run duration. Connection heartbeats are control frames and
246
+ are not yielded to consumers. Malformed frames and a consumer backlog above 1024 events terminate
247
+ the iterator with `event_stream_invalid_message` or `event_stream_overflow` instead of throwing
248
+ outside the iterator or growing memory without a bound.
249
+
250
+ Every Run event includes top-level `sequence`, `runId`, `sessionId`, and `projectId` fields in
251
+ addition to its existing `type` and `data`. After an established socket closes unexpectedly, the SDK
252
+ reconnects with its last sequence and the daemon replays the retained suffix. Replay is bounded and
253
+ process-local. If the daemon restarted or the suffix was evicted, the iterator yields
254
+ `stream.resync-required` with reason `stream-changed` or `cursor-expired`; read the Run or Session
255
+ through the HTTP API to restore authoritative state.
256
+
257
+ Plan First runs can opt into actionable waiting with returnOnAttention. When the returned Run has
258
+ attention.kind equal to plan-approval, use getSessionPlan and respondSessionPlan. Calling waitForRun
259
+ without returnOnAttention keeps the original terminal-only behavior.
260
+
261
+ Automatic review and Specialist binding reuse existing Session JSON fields. Delegation adds
262
+ delegationPolicy with values allow or deny; an omitted historical value restores as allow.
263
+
264
+ To stop a still-running task instead of waiting for it, cancel it explicitly. Cancellation waits for
265
+ provider work and application finalization to drain before returning the terminal Run:
266
+
267
+ ```js
268
+ const cancelled = await client.cancelRun(run.id)
269
+ console.log(cancelled.status) // cancelled
270
+ ```
271
+
272
+ The client discovers the local daemon and reads its authentication token from the Open-Science config
273
+ directory. HTTP calls send the token in request headers. `events()` authenticates its WebSocket using
274
+ a `token` URL query parameter; treat WebSocket URLs and request logs as sensitive. Tokens are
275
+ not included in normal command output.
276
+
277
+ ### Automation new-session defaults
278
+
279
+ `project session-defaults show|update` manages defaults for new Sessions created through the
280
+ Task CLI/API. Explicit run options override these defaults. Existing Sessions and desktop New
281
+ conversation drafts are not changed.
282
+
283
+ `--provider-default-model` keeps following the provider-owned default instead of pinning the
284
+ first model in its catalog. An explicit `--model` stays fixed. If a saved selection becomes
285
+ unavailable, choose a replacement explicitly or wait for it to become available again.
@@ -0,0 +1,90 @@
1
+ /* eslint-disable @typescript-eslint/explicit-function-return-type */
2
+ import { createHash, randomUUID } from 'node:crypto'
3
+ import { createReadStream } from 'node:fs'
4
+ import { cp, lstat, mkdir, readFile, rename, rm, writeFile } from 'node:fs/promises'
5
+ import { dirname, isAbsolute, join } from 'node:path'
6
+ import { homedir } from 'node:os'
7
+ import { spawn } from 'node:child_process'
8
+ import { fileURLToPath } from 'node:url'
9
+
10
+ // An AppImage mount belongs to the foreground CLI invocation. A detached server must never read
11
+ // native libraries/resources from that disappearing mount. Retain only this immutable, hashed
12
+ // installation payload in the user's cache; business settings and secrets stay in their usual roots.
13
+ export async function stageAppImageCli(image, backend, nodeRuntime, cache) {
14
+ if (!isAbsolute(image)) throw new Error('Expected an absolute AppImage path.')
15
+ const digest = createHash('sha256')
16
+ for await (const bytes of createReadStream(image)) digest.update(bytes)
17
+ const identity = digest.digest('hex')
18
+ await mkdir(cache, { recursive: true, mode: 0o700 })
19
+ const info = await lstat(cache)
20
+ if (
21
+ !info.isDirectory() ||
22
+ (process.platform !== 'win32' && (info.mode & 0o022) !== 0) ||
23
+ (process.getuid && info.uid !== process.getuid())
24
+ )
25
+ throw new Error('AppImage CLI cache must be a directory owned by the current user.')
26
+ const destination = join(cache, identity)
27
+ const marker = JSON.stringify({ image: identity })
28
+ const valid = async () => {
29
+ try {
30
+ const info = await lstat(destination)
31
+ return (
32
+ info.isDirectory() &&
33
+ (process.platform === 'win32' || (info.mode & 0o022) === 0) &&
34
+ (!process.getuid || info.uid === process.getuid()) &&
35
+ (await readFile(join(destination, 'complete.json'), 'utf8')) === marker
36
+ )
37
+ } catch (error) {
38
+ if (error.code === 'ENOENT') return false
39
+ throw error
40
+ }
41
+ }
42
+ if (await valid()) return destination
43
+ const temporary = join(cache, `.${identity}-${randomUUID()}`)
44
+ await mkdir(temporary, { mode: 0o700 })
45
+ try {
46
+ await cp(backend, join(temporary, 'backend'), { recursive: true, dereference: true })
47
+ await cp(nodeRuntime, join(temporary, 'node-runtime'), { recursive: true, dereference: true })
48
+ await writeFile(join(temporary, 'complete.json'), marker, { flag: 'wx', mode: 0o600 })
49
+ try {
50
+ await rename(temporary, destination)
51
+ } catch (error) {
52
+ const exists =
53
+ ['EEXIST', 'ENOTEMPTY'].includes(error.code) ||
54
+ (process.platform === 'win32' && error.code === 'EPERM')
55
+ if (!exists || !(await valid())) throw error
56
+ }
57
+ return destination
58
+ } finally {
59
+ await rm(temporary, { recursive: true, force: true })
60
+ }
61
+ }
62
+
63
+ async function main() {
64
+ const backend = dirname(fileURLToPath(import.meta.url))
65
+ const payload = await stageAppImageCli(
66
+ process.argv[2],
67
+ backend,
68
+ join(backend, '../node-runtime'),
69
+ join(process.env.XDG_CACHE_HOME || join(homedir(), '.cache'), 'open-science', 'appimage-cli')
70
+ )
71
+ const env = { ...process.env }
72
+ delete env.ELECTRON_RUN_AS_NODE
73
+ const child = spawn(
74
+ join(payload, 'node-runtime/node'),
75
+ [join(payload, 'backend/cli.mjs'), ...process.argv.slice(3)],
76
+ { env, stdio: 'inherit' }
77
+ )
78
+ child.once('error', (error) => {
79
+ console.error(error.message)
80
+ process.exitCode = 1
81
+ })
82
+ child.once('exit', (code) => {
83
+ process.exitCode = code ?? 1
84
+ })
85
+ }
86
+ if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1])
87
+ await main().catch((error) => {
88
+ console.error(error.message)
89
+ process.exitCode = 1
90
+ })
@@ -0,0 +1,66 @@
1
+ export type BrowserImportReadinessMode = 'attempts' | 'wait-for-launch'
2
+ export type BrowserImportPhase = 'waiting-for-receiver' | 'transferring'
3
+ export type BrowserImportProgress = { sentBytes: number; totalBytes: number }
4
+ export type BrowserImportFailureCode =
5
+ | 'unsupported-browser'
6
+ | 'timeout'
7
+ | 'readiness-exhausted'
8
+ | 'incompatible'
9
+ | 'launch-failed'
10
+ | 'transfer-failed'
11
+ | 'cancelled'
12
+ export class BrowserImportError extends Error {
13
+ readonly code: BrowserImportFailureCode
14
+ constructor(code: BrowserImportFailureCode, message: string)
15
+ }
16
+ /** Synchronous user-click launch request. Returning does not prove installation or delivery.
17
+ * Throws BrowserImportError: unsupported-browser (no navigation API), launch-failed (navigation threw). */
18
+ export function launchOpenScience(): void
19
+ export type BrowserImportDiscoveryOptions = {
20
+ signal?: AbortSignal
21
+ /** Must match the page origin. Inferred from location in browsers. */
22
+ origin?: string
23
+ /** A single configured endpoint, never a range to scan. Defaults to 49151. */
24
+ port?: number
25
+ timeoutMs?: number
26
+ }
27
+ export type BrowserImportUnavailable = {
28
+ /** unavailable also covers opaque CORS/local-network denials; it does not mean not installed. */
29
+ status: 'unavailable' | 'unsupported-browser' | 'incompatible' | 'timeout' | 'aborted'
30
+ }
31
+ /** Probes a running, reachable service, not installation/protocol registration. May prompt for
32
+ * local-network permission. Advisory only; the secret-bound handshake authenticates the receiver. */
33
+ export function checkBrowserImportAvailability(
34
+ options?: BrowserImportDiscoveryOptions
35
+ ): Promise<{ status: 'available' } | BrowserImportUnavailable>
36
+ export type BrowserImportTransfer = {
37
+ /** Contains a secret; never log or persist it. Browser policy may require a fresh user click. */
38
+ readonly url: string
39
+ /** Acknowledges receipt only, independently of destination selection and final import consent.
40
+ * Authenticated desktop cancellation throws code 'cancelled', terminal for this transfer.
41
+ * Keep the Blob and explicitly prepare a new transfer; never automatically retry cancellation. */
42
+ send(options?: {
43
+ signal?: AbortSignal
44
+ /** Waiting starts before readiness; transferring starts after identity verification, before bytes.
45
+ * No transferring event for an already-received package; cached receipts emit no events. */
46
+ onPhase?: (phase: BrowserImportPhase) => void
47
+ /** Acknowledged bytes only; first callback follows the first successful chunk acknowledgment. */
48
+ onProgress?: (progress: BrowserImportProgress) => void
49
+ /** Default attempts preserves the 1–5 attempt limit. wait-for-launch retries until the
50
+ * overall deadline or cancellation; it observes only this transfer's authenticated handshake. */
51
+ readinessMode?: BrowserImportReadinessMode
52
+ /** Readiness only; defaults to 120,000 ms. Capped at 120,000 ms in wait-for-launch,
53
+ * 300,000 ms in attempts mode. Timeout permits another send() on the same live transfer. */
54
+ readyTimeoutMs?: number
55
+ /** 1–5 attempts with 1,000 ms gaps; defaults to 5. Ignored in wait-for-launch mode. */
56
+ readinessAttempts?: number
57
+ /** 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 have 1,000 ms gaps. */
59
+ attemptTimeoutMs?: number
60
+ }): Promise<{ received: true }>
61
+ }
62
+ /** Takes a complete browser-held Blob/File. Never fetches a remote package URL. */
63
+ export function prepareBrowserImport(
64
+ file: Blob,
65
+ options?: { filename?: string; origin?: string; port?: number }
66
+ ): BrowserImportTransfer