dsh-plugin-jules 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +335 -0
- package/cordis.patch.yml +37 -0
- package/lib/async.d.ts +11 -0
- package/lib/async.js +27 -0
- package/lib/client.d.ts +207 -0
- package/lib/client.js +368 -0
- package/lib/index.d.ts +107 -0
- package/lib/index.js +234 -0
- package/lib/journal.d.ts +78 -0
- package/lib/journal.js +110 -0
- package/lib/tools.d.ts +43 -0
- package/lib/tools.js +576 -0
- package/lib/types.d.ts +230 -0
- package/lib/types.js +128 -0
- package/lib/views.d.ts +240 -0
- package/lib/views.js +438 -0
- package/lib/watch.d.ts +87 -0
- package/lib/watch.js +387 -0
- package/package.json +86 -0
- package/scripts/build.mjs +37 -0
- package/scripts/link-dsh-deps.mjs +132 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Aurimas Šeputis
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
# dsh-plugin-jules
|
|
2
|
+
|
|
3
|
+
[](https://github.com/rbviz/dsh-plugin-jules/actions/workflows/ci.yml)
|
|
4
|
+
|
|
5
|
+
Google [Jules](https://jules.google.com) as a [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) capability.
|
|
6
|
+
|
|
7
|
+
Jules is a remote coding agent: you give it a task and a repository, and it works asynchronously in
|
|
8
|
+
Google's cloud on its own clone — planning, editing files, running commands, and opening a pull
|
|
9
|
+
request. This plugin puts that agent behind ten `jules_*` tools, so a harness model can hand off
|
|
10
|
+
long-running work, follow it, approve its plan, answer it, and pull back the diff.
|
|
11
|
+
|
|
12
|
+
It talks to the documented Jules v1alpha REST API directly and has **no runtime dependencies**.
|
|
13
|
+
|
|
14
|
+
## What it gives you
|
|
15
|
+
|
|
16
|
+
- **Delegation that outlives a turn.** `jules_create` returns a session id immediately; the work
|
|
17
|
+
continues in the cloud whether or not anything is listening.
|
|
18
|
+
- **Notification instead of polling.** `jules_watch` registers a background watch and the harness
|
|
19
|
+
delivers a completion notice, so the model ends its turn rather than looping on status checks.
|
|
20
|
+
- **Plan review.** `requirePlanApproval` genuinely gates edits, and `jules_approve_plan` releases them
|
|
21
|
+
once you have read the plan.
|
|
22
|
+
- **Results you can use.** `jules_patch` returns the unified diff — in pages for large ones, with a
|
|
23
|
+
list of every touched file — ready for `git apply`.
|
|
24
|
+
|
|
25
|
+
## Requirements
|
|
26
|
+
|
|
27
|
+
- DeepSeek Harness, with a base-backed profile (the base bundle provides `tools`, `systemPrompt`,
|
|
28
|
+
`jobs`, and the storage stack this plugin uses).
|
|
29
|
+
- Node 22 or newer.
|
|
30
|
+
- A Jules account with at least one repository connected through the Jules GitHub App.
|
|
31
|
+
- A Jules API key: **https://jules.google.com/settings**. Jules allows three keys per account and
|
|
32
|
+
shows each one once.
|
|
33
|
+
|
|
34
|
+
## Install
|
|
35
|
+
|
|
36
|
+
The package declares `dsh.bundle`, so it installs as an ordinary profile bundle. From a checkout of
|
|
37
|
+
this repository:
|
|
38
|
+
|
|
39
|
+
```sh
|
|
40
|
+
node scripts/link-dsh-deps.mjs # once, and after changing peerDependencies
|
|
41
|
+
npm run build # compiles src/ to lib/
|
|
42
|
+
|
|
43
|
+
dsh plugin --profile <your-profile> add /path/to/jules-plugin
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Then restart the harness. Confirm the layer composed:
|
|
47
|
+
|
|
48
|
+
```sh
|
|
49
|
+
dsh --profile <your-profile> --dump-config | grep -A 3 'id: jules'
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### From GitHub
|
|
53
|
+
|
|
54
|
+
```sh
|
|
55
|
+
dsh plugin --profile <your-profile> add github:rbviz/dsh-plugin-jules
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
pnpm 10 and later refuses to run a dependency's `prepare` script until it is explicitly allowed, so
|
|
59
|
+
the first attempt fails and prints the exact key to add to the profile's `pnpm-workspace.yaml`:
|
|
60
|
+
|
|
61
|
+
```yaml
|
|
62
|
+
allowBuilds:
|
|
63
|
+
dsh-plugin-jules: true
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Treat that allowance as **permission to execute this package's code on your machine at install time**,
|
|
67
|
+
outside any sandbox the agent runs under — only allow packages whose source you trust, and pin a commit
|
|
68
|
+
(`github:rbviz/dsh-plugin-jules#<sha>`) so a later push cannot silently change what runs.
|
|
69
|
+
|
|
70
|
+
`prepare` links the peer dependencies and compiles `src/` to `lib/`, because a git install fetches
|
|
71
|
+
sources and `lib/` is deliberately not committed. A registry install skips `prepare` entirely and ships
|
|
72
|
+
`lib/` prebuilt.
|
|
73
|
+
|
|
74
|
+
### Without installing
|
|
75
|
+
|
|
76
|
+
For a local loop you can skip the package entirely and insert the built entry point by absolute path
|
|
77
|
+
from your profile's `cordis.patch.yml`:
|
|
78
|
+
|
|
79
|
+
```yaml
|
|
80
|
+
- insert:
|
|
81
|
+
- id: jules
|
|
82
|
+
name: '/path/to/jules-plugin/lib/index.js'
|
|
83
|
+
config:
|
|
84
|
+
apiKeyEnv: JULES_API_KEY
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Do **not** do this while the bundle is installed. Both layers would insert the row id `jules`, and the
|
|
88
|
+
composition aborts at boot with `duplicate loader entry id: jules` rather than guess which wins. Use one
|
|
89
|
+
owner or the other; to change how an installed bundle behaves, **override its row by id** instead. A
|
|
90
|
+
patch replaces the row's whole config rather than merging into it, so restate every key you want.
|
|
91
|
+
|
|
92
|
+
## Authentication
|
|
93
|
+
|
|
94
|
+
1. Create a key at https://jules.google.com/settings.
|
|
95
|
+
2. Expose it under the credential name in `apiKeyEnv` (default `JULES_API_KEY`). Either export it
|
|
96
|
+
before starting the harness, or store it through the harness credential store so configuration keeps
|
|
97
|
+
naming a secret rather than containing one.
|
|
98
|
+
|
|
99
|
+
```sh
|
|
100
|
+
export JULES_API_KEY=...
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
The key is resolved on every request — literal `apiKey` first, then `ctx.credentials`, then the launch
|
|
104
|
+
environment — so a rotated key applies to the next call with no reload. With no key configured, tools
|
|
105
|
+
fail with an actionable message rather than a bare 401.
|
|
106
|
+
|
|
107
|
+
## Quick start
|
|
108
|
+
|
|
109
|
+
```text
|
|
110
|
+
jules_sources # which repositories Jules may work in
|
|
111
|
+
jules_create # prompt + source + requirePlanApproval + autoCreatePr
|
|
112
|
+
jules_watch # returns a job id; then END YOUR TURN
|
|
113
|
+
... the completion notice arrives ...
|
|
114
|
+
job_output # read the report
|
|
115
|
+
jules_status # read the plan, then jules_approve_plan
|
|
116
|
+
jules_patch # review or land the diff
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
## Tools
|
|
120
|
+
|
|
121
|
+
| Tool | Purpose |
|
|
122
|
+
|---|---|
|
|
123
|
+
| `jules_sources` | List repositories connected to Jules. A repository must appear here before `jules_create` can target it. |
|
|
124
|
+
| `jules_create` | Start a session and return a session id immediately. Takes `prompt`, optional `source`, `branch`, `title`, `requirePlanApproval`, `autoCreatePr`. Omit `source` for a repoless session. |
|
|
125
|
+
| `jules_list` | List recent sessions, newest first, with state and pull request links. |
|
|
126
|
+
| `jules_status` | One session in full: state, plan, approval, pull requests, generated files, latest command, last agent message. |
|
|
127
|
+
| `jules_activities` | The event log: plan generation and approval, messages, progress, completion, failure, artifacts. |
|
|
128
|
+
| `jules_approve_plan` | Approve a pending plan so the agent may edit files. |
|
|
129
|
+
| `jules_send_message` | Answer a question, correct a plan, or hand over follow-up work. |
|
|
130
|
+
| `jules_watch` | Watch in the background; returns a job id and notifies you when there is something to read. |
|
|
131
|
+
| `jules_wait` | Hold the turn open for the same wait, for when nothing else can proceed meanwhile. |
|
|
132
|
+
| `jules_patch` | The newest unified diff, with every touched file listed and `offset`/`nextOffset` paging. |
|
|
133
|
+
|
|
134
|
+
Every id argument accepts `123`, `sessions/123`, or the `jules.google.com/session/123` URL, and every
|
|
135
|
+
source argument accepts `owner/repo`, `github/owner/repo`, or the full resource name.
|
|
136
|
+
|
|
137
|
+
## Configuration
|
|
138
|
+
|
|
139
|
+
Set in the loader row's `config` block. Every field has a default except the credential.
|
|
140
|
+
|
|
141
|
+
| Field | Default | Meaning |
|
|
142
|
+
|---|---|---|
|
|
143
|
+
| `apiKey` | — | Literal key. Prefer `apiKeyEnv` so no secret enters a config file. |
|
|
144
|
+
| `apiKeyEnv` | `JULES_API_KEY` | Credential reference holding the key. |
|
|
145
|
+
| `baseURL` | `https://jules.googleapis.com/v1alpha` | API root. |
|
|
146
|
+
| `defaultSource` | — | Repository `jules_create` targets when the call omits one. Setting it makes repoless sessions unavailable. |
|
|
147
|
+
| `requestTimeoutMs` | `30000` | Per-request deadline. |
|
|
148
|
+
| `defaultPageSize` | `30` | Page size for list calls that do not choose one. |
|
|
149
|
+
| `maxPageSize` | `100` | Largest page size a caller may request. |
|
|
150
|
+
| `maxPatchBytes` | `200000` | Largest diff slice `jules_patch` returns. |
|
|
151
|
+
| `pollIntervalMs` | `5000` | Delay between status polls inside `jules_wait`. |
|
|
152
|
+
| `waitDefaultMs` | `120000` | Wait budget `jules_wait` uses when the caller does not set one. |
|
|
153
|
+
| `waitMaxMs` | `300000` | Largest wait budget a caller may request. |
|
|
154
|
+
| `maxActivityPages` | `10` | Activity pages one read may walk. |
|
|
155
|
+
| `enableWatch` | `true` | Expose `jules_watch`. |
|
|
156
|
+
| `watchPollIntervalMs` | `15000` | Delay between polls inside `jules_watch`. |
|
|
157
|
+
| `watchDefaultMs` | `1800000` | Watch budget `jules_watch` uses when the caller does not set one. |
|
|
158
|
+
| `watchMaxMs` | `7200000` | Largest watch budget a caller may request. |
|
|
159
|
+
| `watchSettleOnMessage` | `true` | End a watch when the agent posts a message, not only on a state change. |
|
|
160
|
+
| `retryMaxAttempts` | `4` | Attempts per request, including the first. |
|
|
161
|
+
| `retryBaseDelayMs` | `1000` | First backoff step; doubles per attempt. |
|
|
162
|
+
| `retryMaxDelayMs` | `30000` | Ceiling for one backoff step. |
|
|
163
|
+
|
|
164
|
+
Cross-field mistakes fail at load with a named error rather than at the first call: `defaultPageSize`
|
|
165
|
+
above `maxPageSize`, `waitDefaultMs` above `waitMaxMs`, `watchDefaultMs` above `watchMaxMs`,
|
|
166
|
+
`retryBaseDelayMs` above `retryMaxDelayMs`, and a `baseURL` that is not an absolute URL.
|
|
167
|
+
|
|
168
|
+
## Waiting: watch, do not poll
|
|
169
|
+
|
|
170
|
+
A Jules session runs for minutes to hours. The tooling is shaped so a model never has to poll, and says
|
|
171
|
+
so in three places, because polling is the failure mode that wastes the most for the least:
|
|
172
|
+
|
|
173
|
+
- the model guidance leads with it — create, watch, **end your turn**;
|
|
174
|
+
- every waiting-adjacent tool description names what it is *for* rather than what it does, so
|
|
175
|
+
`jules_status` reads as a confirmation step rather than an invitation;
|
|
176
|
+
- `jules_status` **says so when it is asked twice**. It remembers what it last reported, per caller and
|
|
177
|
+
session, and when nothing has moved it renders:
|
|
178
|
+
|
|
179
|
+
> note: nothing has changed since your last check 45s ago. Do not keep polling — a jules_watch
|
|
180
|
+
> notice is what tells you something happened.
|
|
181
|
+
|
|
182
|
+
The canonical value carries `unchangedForMs`, so the fact is data rather than prose, and the nudge is
|
|
183
|
+
additive: the full report is still there.
|
|
184
|
+
|
|
185
|
+
### A restart ends the watch, not the work
|
|
186
|
+
|
|
187
|
+
Background jobs are process-local: the harness holds them in memory and tears them down on shutdown.
|
|
188
|
+
Restarting therefore ends every watch silently — while the Jules sessions themselves carry on in
|
|
189
|
+
Google's cloud, because they were never ours to begin with.
|
|
190
|
+
|
|
191
|
+
The job cannot be restored. A job needs a live owning agent to receive a completion notice, and at
|
|
192
|
+
plugin load there is no agent; anything claiming to restore a watch at boot would be polling with
|
|
193
|
+
nobody to tell.
|
|
194
|
+
|
|
195
|
+
What survives is the *fact*. `jules_watch` records the session in a durable `jules_watches` domain,
|
|
196
|
+
and a watch that reaches its own conclusion removes it. Entries expire with the budget they recorded,
|
|
197
|
+
so nothing accumulates. After a restart the model guidance grows a paragraph naming what was still
|
|
198
|
+
armed:
|
|
199
|
+
|
|
200
|
+
> Note: background watches do not survive a harness restart, and 2 watches were still armed when this
|
|
201
|
+
> harness last stopped: 123, 456. The Jules sessions themselves kept running in the cloud. Re-arm the
|
|
202
|
+
> ones you still care about with `jules_watch`; a session that has since finished needs only a
|
|
203
|
+
> `jules_status`.
|
|
204
|
+
|
|
205
|
+
The section text is evaluated per assembly, so the note appears as soon as the plugin loads and clears
|
|
206
|
+
as watches are re-armed. A deliberately killed watch keeps its entry until its budget expires —
|
|
207
|
+
distinguishing "the user stopped this" from "the harness is going down" is not something the available
|
|
208
|
+
signals do reliably, and guessing wrong would drop exactly the record this exists to keep.
|
|
209
|
+
|
|
210
|
+
Records carry no owner, so once a restart makes everything non-live the note lists watches armed by any
|
|
211
|
+
session sharing that harness home. In a single-session setup that is invisible; with several it is
|
|
212
|
+
noise rather than a wrong action, since re-arming is harmless.
|
|
213
|
+
|
|
214
|
+
A composition without the storage domain simply loses the note; the tool family is unaffected.
|
|
215
|
+
|
|
216
|
+
## What the Jules API does not allow
|
|
217
|
+
|
|
218
|
+
Service limitations, not gaps in the plugin. Worth knowing before planning around them:
|
|
219
|
+
|
|
220
|
+
- **No cancel, pause, or resume.** `:cancelPlan`, `:pause` and `:resume` all 404. A session runs
|
|
221
|
+
to completion or failure, and a stuck command cannot be interrupted. The only lever is a message.
|
|
222
|
+
- **No separate "reject plan" action.** `:approvePlan` exists; there is no `:rejectPlan`. Sending a
|
|
223
|
+
message with corrections is the only way to change a plan.
|
|
224
|
+
- **A follow-up resumes from the session's own snapshot.** `:sendMessage` takes only a prompt — no base
|
|
225
|
+
or branch control. Treat a follow-up as iterating on *that session's* branch.
|
|
226
|
+
- **The PR base follows the starting branch.** There is no `prBase` at creation, so set `branch` on
|
|
227
|
+
`jules_create` to control it.
|
|
228
|
+
- **No cost, token, or runtime accounting**, and no way to ask an agent to report back after N minutes.
|
|
229
|
+
|
|
230
|
+
### Partial signals
|
|
231
|
+
|
|
232
|
+
- **`progressUpdated` often carries an empty summary.** `jules_status` therefore surfaces
|
|
233
|
+
`latestCommand`, taken from the newest `bashOutput` artifact, so a stalled session can at least be
|
|
234
|
+
traced to a command.
|
|
235
|
+
- **`generatedFiles` is empty in practice.** `jules_patch` — which reads the `changeSet` artifact —
|
|
236
|
+
is the authoritative change list, and its `files` field lists every touched path.
|
|
237
|
+
- **`requirePlanApproval` and `autoCreatePr` are inputs the service does not echo back.** They are
|
|
238
|
+
reported as `yes`/`no`/`unknown`, decided by evidence (a PR exists, a plan was approved) with
|
|
239
|
+
`unknown` when there is none, rather than a fabricated `false`.
|
|
240
|
+
- **An approval is confirmed by its event, not by the phase it returns with.** `jules_approve_plan`
|
|
241
|
+
polls briefly for the `planApproved` activity after approving, because the service is eventually
|
|
242
|
+
consistent about its own actions: the phase can still read `AWAITING_PLAN_APPROVAL` — or something
|
|
243
|
+
further along — and `planApproved` can still read false, for an approval that in fact landed and
|
|
244
|
+
started work. The reply says which of the two happened (`approvalConfirmed`), and when it could
|
|
245
|
+
not confirm in time it says that rather than implying the approval failed. If you need certainty,
|
|
246
|
+
read the `planApproved` event yourself, or wait for the agent's first progress event.
|
|
247
|
+
- **`Session.state` is advisory.** A session can post its final answer and leave `state` reading
|
|
248
|
+
`IN_PROGRESS` indefinitely — the Jules web UI shows the same stuck state. The plugin reads the
|
|
249
|
+
activity log instead of trusting the field.
|
|
250
|
+
- **The activity cursor is a filter, not a parameter.** The API reference documents `?createTime=`, but
|
|
251
|
+
the service rejects it ("Cannot bind query parameter. Field 'createTime' could not be found"). The
|
|
252
|
+
plugin sends the AIP-160 expression the official SDK uses, and treats a rejection as a bandwidth
|
|
253
|
+
problem rather than a failure.
|
|
254
|
+
|
|
255
|
+
## Design notes
|
|
256
|
+
|
|
257
|
+
**Canonical values, not prose.** Every tool declares an output schema and returns a value matching it;
|
|
258
|
+
`render` separately produces the text the model reads. Optional scalars project to `''` and absent
|
|
259
|
+
lists to `[]`, so a value is always lossless JSON with a fixed shape.
|
|
260
|
+
|
|
261
|
+
**Waiting is one implementation.** `jules_wait` and `jules_watch` share `runWatch`, so they cannot
|
|
262
|
+
disagree about when a session is done or what to report. A watch settles on new evidence: a terminal
|
|
263
|
+
state, a state it moved into, or activity that arrived after it started. The log it reads at the start
|
|
264
|
+
is baseline — used for the report, never a trigger — so watching a session whose last activity is the
|
|
265
|
+
question you just answered waits for the actual reply.
|
|
266
|
+
|
|
267
|
+
**Failures are typed, and partial failures are named.** Transport failures, timeouts, 404s,
|
|
268
|
+
auth rejections, rate limits, and a 2xx body that is not JSON each raise their own error class, so a
|
|
269
|
+
caller can tell them apart instead of reading prose. Retries cover exactly 429 and 5xx and are
|
|
270
|
+
bounded. Where a partial failure is survivable the result says so rather than filling in the blank:
|
|
271
|
+
if the session read succeeds but its activity log does not, the projection carries
|
|
272
|
+
`logRead: false` and the report opens with a warning, because "no plan pending" and "we could
|
|
273
|
+
not look" call for opposite actions. `jules_approve_plan` distinguishes three outcomes — confirmed,
|
|
274
|
+
not yet visible, and unverifiable — so an unreadable log never reads as a failed approval.
|
|
275
|
+
|
|
276
|
+
**Journal writes never reject.** A journal failure costs the restart note, never the watch, so it must
|
|
277
|
+
not take down the job doing the real work — but it is logged, because swallowing it silently is how
|
|
278
|
+
a broken journal went unnoticed for a release.
|
|
279
|
+
|
|
280
|
+
**Retries that match the service.** Jules documents 429 and 5xx but publishes no limits, and its own SDK
|
|
281
|
+
reports high concurrency failing *silently* with misleading errors. The client retries those statuses
|
|
282
|
+
with exponential backoff, honours `Retry-After`, and gives up with a typed error. A session that is not
|
|
283
|
+
queryable yet — Jules mints the id before the resource exists — is retried rather than reported.
|
|
284
|
+
|
|
285
|
+
**Module resolution.** A profile installs an out-of-tree bundle with pnpm's `link:` protocol, which
|
|
286
|
+
creates no `node_modules` beside the real package, and Node resolves a symlinked package to its
|
|
287
|
+
realpath. The plugin's own `import '@deepseek-ai/dsh-tools'` would therefore fail even though the harness
|
|
288
|
+
had just loaded the file. `scripts/link-dsh-deps.mjs` links each declared peer at the copy the running
|
|
289
|
+
installation already uses, so both sides share one module instance.
|
|
290
|
+
|
|
291
|
+
## Development
|
|
292
|
+
|
|
293
|
+
```sh
|
|
294
|
+
node scripts/link-dsh-deps.mjs # once, or after changing peerDependencies
|
|
295
|
+
npm run build # tsc, using the TypeScript the harness already ships
|
|
296
|
+
npm test # build, then the full suite
|
|
297
|
+
node scripts/link-dsh-deps.mjs --check # report missing peers, change nothing
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
`scripts/build.mjs` resolves TypeScript from `$DSH_TSC`, then the linked `node_modules`, then
|
|
301
|
+
`$DSH_HOME`, so the plugin needs no toolchain of its own and never runs `pnpm install`.
|
|
302
|
+
|
|
303
|
+
Tests run in two layers. Unit tests cover normalization, request construction, retry and error
|
|
304
|
+
classification against a stubbed transport, projections, and renders. Real-composition tests mount the
|
|
305
|
+
built plugin through the Cordis loader beside the genuine harness services and drive tools through the
|
|
306
|
+
real execution pipeline — including the storage stack, so the durable watch journal is exercised rather
|
|
307
|
+
than assumed.
|
|
308
|
+
|
|
309
|
+
### Source map
|
|
310
|
+
|
|
311
|
+
| File | Role |
|
|
312
|
+
|---|---|
|
|
313
|
+
| `src/index.ts` | Plugin entry: name, injections, Config schema, credential resolution, registration, model guidance. |
|
|
314
|
+
| `src/client.ts` | The `fetch`-based v1alpha client, error classes, and the retry policy. |
|
|
315
|
+
| `src/types.ts` | Wire types and the reference normalization the tools apply before calling. |
|
|
316
|
+
| `src/views.ts` | Projections to canonical values, and the text each one renders to. |
|
|
317
|
+
| `src/tools.ts` | The `defineTool` declarations. |
|
|
318
|
+
| `src/watch.ts` | `runWatch` — the settling rules shared by `jules_wait` and `jules_watch` — and the job adapter. |
|
|
319
|
+
| `src/journal.ts` | The durable watch journal and the restart note. |
|
|
320
|
+
| `src/async.ts` | The cancellable sleep both waiting paths use. |
|
|
321
|
+
| `cordis.patch.yml` | The bundle layer a profile applies. |
|
|
322
|
+
|
|
323
|
+
## License
|
|
324
|
+
|
|
325
|
+
MIT — see [LICENSE](LICENSE). Permissive by design: commercial use, modification, and redistribution are
|
|
326
|
+
all fine, and the only obligation is keeping the copyright notice. Nothing here is copyleft, so
|
|
327
|
+
building it into a commercial product needs no legal review of this repository.
|
|
328
|
+
|
|
329
|
+
The plugin bundles **no third-party code**. All ten of its imports — the `@deepseek-ai/*` packages and
|
|
330
|
+
`zod` — are peer dependencies that the harness provides, and every one of them is MIT, as is
|
|
331
|
+
DeepSeek Harness itself.
|
|
332
|
+
|
|
333
|
+
Two things separate from this licence, worth knowing before shipping: using the plugin requires a Jules
|
|
334
|
+
account and is subject to Google's terms of service, and *Jules* is Google's trademark, used here
|
|
335
|
+
descriptively — nothing in this project is affiliated with or endorsed by Google.
|
package/cordis.patch.yml
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# dsh-plugin-jules — the bundle layer for the Google Jules integration.
|
|
2
|
+
#
|
|
3
|
+
# One Loader row registers the whole plugin: the `jules` tool family plus the
|
|
4
|
+
# model guidance section. Nothing here is app-specific, so the row is the same
|
|
5
|
+
# for every profile that installs this bundle.
|
|
6
|
+
#
|
|
7
|
+
# This row OWNS the id `jules`. A profile that installs this bundle must not
|
|
8
|
+
# also insert a row with that id from its own cordis.patch.yml: two insert
|
|
9
|
+
# patches for one id fail the boot with "duplicate loader entry id: jules".
|
|
10
|
+
# To change a value, override this row by id instead of inserting a new one —
|
|
11
|
+
# and because a patch replaces the whole config object rather than merging into
|
|
12
|
+
# it, restate every key you still want.
|
|
13
|
+
- insert:
|
|
14
|
+
- id: jules
|
|
15
|
+
name: 'dsh-plugin-jules'
|
|
16
|
+
config:
|
|
17
|
+
apiKeyEnv: JULES_API_KEY
|
|
18
|
+
baseURL: https://jules.googleapis.com/v1alpha
|
|
19
|
+
requestTimeoutMs: 30000
|
|
20
|
+
defaultPageSize: 30
|
|
21
|
+
maxPageSize: 100
|
|
22
|
+
maxPatchBytes: 200000
|
|
23
|
+
waitDefaultMs: 120000
|
|
24
|
+
waitMaxMs: 300000
|
|
25
|
+
pollIntervalMs: 5000
|
|
26
|
+
maxActivityPages: 10
|
|
27
|
+
retryMaxAttempts: 4
|
|
28
|
+
retryBaseDelayMs: 1000
|
|
29
|
+
retryMaxDelayMs: 30000
|
|
30
|
+
# jules_watch: background watching through the job registry.
|
|
31
|
+
enableWatch: true
|
|
32
|
+
watchDefaultMs: 1800000
|
|
33
|
+
watchMaxMs: 7200000
|
|
34
|
+
watchPollIntervalMs: 15000
|
|
35
|
+
# Jules does not reliably move the session state when it hands work
|
|
36
|
+
# back, so the watcher also settles on what the activity log says.
|
|
37
|
+
watchSettleOnMessage: true
|
package/lib/async.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Small async helpers shared by the foreground tools and the background watcher.
|
|
3
|
+
* @module dsh-plugin-jules/async
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Sleep, rejecting as soon as the caller cancels.
|
|
7
|
+
* @param ms - milliseconds to wait.
|
|
8
|
+
* @param signal - cancellation signal.
|
|
9
|
+
* @returns settlement after the delay, or on cancellation.
|
|
10
|
+
*/
|
|
11
|
+
export declare function sleep(ms: number, signal: AbortSignal): Promise<void>;
|
package/lib/async.js
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Small async helpers shared by the foreground tools and the background watcher.
|
|
3
|
+
* @module dsh-plugin-jules/async
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Sleep, rejecting as soon as the caller cancels.
|
|
7
|
+
* @param ms - milliseconds to wait.
|
|
8
|
+
* @param signal - cancellation signal.
|
|
9
|
+
* @returns settlement after the delay, or on cancellation.
|
|
10
|
+
*/
|
|
11
|
+
export async function sleep(ms, signal) {
|
|
12
|
+
await new Promise((resolve, reject) => {
|
|
13
|
+
const onAbort = () => {
|
|
14
|
+
clearTimeout(timer);
|
|
15
|
+
reject(signal.reason instanceof Error ? signal.reason : new Error('aborted'));
|
|
16
|
+
};
|
|
17
|
+
const timer = setTimeout(() => {
|
|
18
|
+
signal.removeEventListener('abort', onAbort);
|
|
19
|
+
resolve();
|
|
20
|
+
}, ms);
|
|
21
|
+
if (signal.aborted) {
|
|
22
|
+
onAbort();
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
26
|
+
});
|
|
27
|
+
}
|
package/lib/client.d.ts
ADDED
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A dependency-free client for the Jules v1alpha REST API.
|
|
3
|
+
*
|
|
4
|
+
* The Jules CLI is not usable here: it authenticates with an interactive OAuth
|
|
5
|
+
* flow kept in the OS keyring, reads no `JULES_*` environment variable, emits
|
|
6
|
+
* no machine-readable output, and talks to an internal backend rather than the
|
|
7
|
+
* documented v1alpha service. The REST API is therefore the only interface a
|
|
8
|
+
* long-running harness plugin can drive, and it needs nothing beyond `fetch`.
|
|
9
|
+
*
|
|
10
|
+
* @module dsh-plugin-jules/client
|
|
11
|
+
*/
|
|
12
|
+
import type { Activity, CreateSessionRequest, ListActivitiesResponse, ListSessionsResponse, ListSourcesResponse, Session, Source } from './types.ts';
|
|
13
|
+
/** A Jules API call failed. */
|
|
14
|
+
export declare class JulesError extends Error {
|
|
15
|
+
/** HTTP status, when the failure came from a response. */
|
|
16
|
+
readonly status: number | undefined;
|
|
17
|
+
constructor(message: string, status?: number, options?: {
|
|
18
|
+
cause?: unknown;
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
/** The API rejected the credential, or no credential was configured. */
|
|
22
|
+
export declare class JulesAuthError extends JulesError {
|
|
23
|
+
constructor(message: string, status?: number, options?: {
|
|
24
|
+
cause?: unknown;
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
/** The API asked the caller to slow down and the retry budget was exhausted. */
|
|
28
|
+
export declare class JulesRateLimitError extends JulesError {
|
|
29
|
+
constructor(message: string, status?: number, options?: {
|
|
30
|
+
cause?: unknown;
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
/** The named resource does not exist, or is not visible to this credential. */
|
|
34
|
+
export declare class JulesNotFoundError extends JulesError {
|
|
35
|
+
constructor(message: string, status?: number, options?: {
|
|
36
|
+
cause?: unknown;
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
/** One request outlived its configured deadline. */
|
|
40
|
+
export declare class JulesTimeoutError extends JulesError {
|
|
41
|
+
constructor(message: string, options?: {
|
|
42
|
+
cause?: unknown;
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
/** The transport failed before a response arrived. */
|
|
46
|
+
export declare class JulesNetworkError extends JulesError {
|
|
47
|
+
constructor(message: string, options?: {
|
|
48
|
+
cause?: unknown;
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
/** Retry policy for the transient failures the service documents. */
|
|
52
|
+
export interface JulesRetryOptions {
|
|
53
|
+
/** Total number of attempts, including the first. Defaults to 4. */
|
|
54
|
+
maxAttempts: number;
|
|
55
|
+
/** First backoff step in milliseconds; doubles per attempt. Defaults to 1000. */
|
|
56
|
+
baseDelayMs: number;
|
|
57
|
+
/** Ceiling for one backoff step in milliseconds. Defaults to 30000. */
|
|
58
|
+
maxDelayMs: number;
|
|
59
|
+
}
|
|
60
|
+
/** Everything one client needs to reach the service. */
|
|
61
|
+
export interface JulesClientOptions {
|
|
62
|
+
/** Resolved API key. Read per request, so a rotation lands on the next call. */
|
|
63
|
+
resolveApiKey: () => Promise<string | undefined>;
|
|
64
|
+
/** API root without a trailing slash. */
|
|
65
|
+
baseURL: string;
|
|
66
|
+
/** Per-request deadline in milliseconds. */
|
|
67
|
+
requestTimeoutMs: number;
|
|
68
|
+
/** Page size used when a caller does not choose one. */
|
|
69
|
+
defaultPageSize: number;
|
|
70
|
+
/** Largest page size the service accepts. */
|
|
71
|
+
maxPageSize: number;
|
|
72
|
+
/** Retry policy for 429 and 5xx responses. */
|
|
73
|
+
retry: JulesRetryOptions;
|
|
74
|
+
/** Optional `User-Agent`, sent when the runtime allows it. */
|
|
75
|
+
userAgent?: string;
|
|
76
|
+
/**
|
|
77
|
+
* Test seam: replaces the global `fetch`.
|
|
78
|
+
*
|
|
79
|
+
* The suite drives every retry, timeout, and error-classification path through
|
|
80
|
+
* this rather than through a mock server, so the transport is the only thing
|
|
81
|
+
* swapped out — request construction and response handling stay the real code.
|
|
82
|
+
*/
|
|
83
|
+
fetchImpl?: typeof fetch;
|
|
84
|
+
}
|
|
85
|
+
/** One query string entry; `undefined` entries are dropped. */
|
|
86
|
+
type QueryValue = string | number | undefined;
|
|
87
|
+
/** What every request accepts. */
|
|
88
|
+
interface RequestOptions {
|
|
89
|
+
query?: Record<string, QueryValue>;
|
|
90
|
+
body?: unknown;
|
|
91
|
+
/** Caller cancellation, normally a tool call's `exec.signal`. */
|
|
92
|
+
signal?: AbortSignal;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Build the AIP-160 expression that selects activities newer than a cursor.
|
|
96
|
+
*
|
|
97
|
+
* The API reference documents `?createTime=<rfc3339>`, but the service rejects
|
|
98
|
+
* it outright — "Cannot bind query parameter. Field 'createTime' could not be
|
|
99
|
+
* found in request message" — so this sends the filter expression the official
|
|
100
|
+
* Jules SDK uses instead. Because that form is not in the reference, callers
|
|
101
|
+
* must treat a rejected cursor as a bandwidth problem rather than a failure;
|
|
102
|
+
* {@link runWatch} does exactly that.
|
|
103
|
+
* @param since - RFC 3339 timestamp; strictly newer activities are returned.
|
|
104
|
+
* @returns the filter expression.
|
|
105
|
+
*/
|
|
106
|
+
export declare function activitiesSinceFilter(since: string): string;
|
|
107
|
+
/** Drive the Jules v1alpha REST API. */
|
|
108
|
+
export declare class JulesClient {
|
|
109
|
+
#private;
|
|
110
|
+
constructor(options: JulesClientOptions);
|
|
111
|
+
/**
|
|
112
|
+
* Perform one authenticated request, retrying the transient failures the
|
|
113
|
+
* service documents.
|
|
114
|
+
* @param method - HTTP method.
|
|
115
|
+
* @param path - path below the API root, starting with `/`.
|
|
116
|
+
* @param options - query, body, and caller cancellation.
|
|
117
|
+
* @returns the decoded JSON body, or `undefined` for an empty response.
|
|
118
|
+
* @throws {JulesAuthError} when no key is configured or the key is rejected.
|
|
119
|
+
* @throws {JulesRateLimitError} when 429 persists past the retry budget.
|
|
120
|
+
* @throws {JulesTimeoutError} when the deadline passes first.
|
|
121
|
+
* @throws {JulesNetworkError} when the transport fails.
|
|
122
|
+
* @throws {JulesError} for every other non-2xx response.
|
|
123
|
+
*/
|
|
124
|
+
request<T>(method: string, path: string, options?: RequestOptions): Promise<T>;
|
|
125
|
+
/**
|
|
126
|
+
* List the repositories connected to Jules.
|
|
127
|
+
* @param options - paging and cancellation.
|
|
128
|
+
* @returns one page of sources.
|
|
129
|
+
*/
|
|
130
|
+
listSources(options?: {
|
|
131
|
+
pageSize?: number;
|
|
132
|
+
pageToken?: string;
|
|
133
|
+
signal?: AbortSignal;
|
|
134
|
+
}): Promise<ListSourcesResponse>;
|
|
135
|
+
/**
|
|
136
|
+
* Read one connected repository.
|
|
137
|
+
* @param name - canonical source resource name.
|
|
138
|
+
* @param signal - cancellation signal.
|
|
139
|
+
* @returns the source.
|
|
140
|
+
*/
|
|
141
|
+
getSource(name: string, signal?: AbortSignal): Promise<Source>;
|
|
142
|
+
/**
|
|
143
|
+
* Create a session, which starts the remote agent immediately.
|
|
144
|
+
* @param input - prompt, optional source context, and automation flags.
|
|
145
|
+
* @param signal - cancellation signal.
|
|
146
|
+
* @returns the created session.
|
|
147
|
+
*/
|
|
148
|
+
createSession(input: CreateSessionRequest, signal?: AbortSignal): Promise<Session>;
|
|
149
|
+
/**
|
|
150
|
+
* List sessions visible to this credential, newest first.
|
|
151
|
+
* @param options - paging and cancellation.
|
|
152
|
+
* @returns one page of sessions.
|
|
153
|
+
*/
|
|
154
|
+
listSessions(options?: {
|
|
155
|
+
pageSize?: number;
|
|
156
|
+
pageToken?: string;
|
|
157
|
+
signal?: AbortSignal;
|
|
158
|
+
}): Promise<ListSessionsResponse>;
|
|
159
|
+
/**
|
|
160
|
+
* Read one session, including its outputs once it finishes.
|
|
161
|
+
* @param id - bare session id.
|
|
162
|
+
* @param signal - cancellation signal.
|
|
163
|
+
* @returns the session.
|
|
164
|
+
*/
|
|
165
|
+
getSession(id: string, signal?: AbortSignal): Promise<Session>;
|
|
166
|
+
/**
|
|
167
|
+
* Read a session's event log, oldest first.
|
|
168
|
+
* @param id - bare session id.
|
|
169
|
+
* @param options - paging, an optional `since` cursor, and cancellation.
|
|
170
|
+
* @returns one page of activities.
|
|
171
|
+
*/
|
|
172
|
+
listActivities(id: string, options?: {
|
|
173
|
+
pageSize?: number;
|
|
174
|
+
pageToken?: string;
|
|
175
|
+
since?: string;
|
|
176
|
+
signal?: AbortSignal;
|
|
177
|
+
}): Promise<ListActivitiesResponse>;
|
|
178
|
+
/**
|
|
179
|
+
* Read every activity page, oldest first.
|
|
180
|
+
*
|
|
181
|
+
* The event log is append-only and immutable, so walking it whole is safe;
|
|
182
|
+
* the page cap keeps a runaway log from consuming unbounded memory.
|
|
183
|
+
* @param id - bare session id.
|
|
184
|
+
* @param options - page cap, cursor, and cancellation.
|
|
185
|
+
* @returns every activity the walk collected.
|
|
186
|
+
*/
|
|
187
|
+
listAllActivities(id: string, options?: {
|
|
188
|
+
maxPages?: number;
|
|
189
|
+
since?: string;
|
|
190
|
+
pageSize?: number;
|
|
191
|
+
signal?: AbortSignal;
|
|
192
|
+
}): Promise<Activity[]>;
|
|
193
|
+
/**
|
|
194
|
+
* Approve the plan a session is waiting on.
|
|
195
|
+
* @param id - bare session id.
|
|
196
|
+
* @param signal - cancellation signal.
|
|
197
|
+
*/
|
|
198
|
+
approvePlan(id: string, signal?: AbortSignal): Promise<void>;
|
|
199
|
+
/**
|
|
200
|
+
* Send a message to the session's agent.
|
|
201
|
+
* @param id - bare session id.
|
|
202
|
+
* @param prompt - what to tell the agent.
|
|
203
|
+
* @param signal - cancellation signal.
|
|
204
|
+
*/
|
|
205
|
+
sendMessage(id: string, prompt: string, signal?: AbortSignal): Promise<void>;
|
|
206
|
+
}
|
|
207
|
+
export {};
|