regent-code 3.0.0 → 3.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/.opencode/INSTALL.md +16 -2
- package/.opencode/package.json +1 -1
- package/.opencode/plugins/regent.js +168 -6
- package/README.md +35 -3
- package/mcp/cli.js +80 -6
- package/mcp/index.js +26 -5
- package/mcp/install.js +40 -31
- package/mcp/shared.js +130 -0
- package/package.json +4 -4
- package/regent-code-3.0.2.tgz +0 -0
package/.opencode/INSTALL.md
CHANGED
|
@@ -1,11 +1,25 @@
|
|
|
1
1
|
# Installation
|
|
2
2
|
|
|
3
|
+
## One-command install
|
|
4
|
+
|
|
5
|
+
If the package is published to npm, both the plugin and the MCP server install
|
|
6
|
+
with a single command against any existing `opencode.json` / `opencode.jsonc`:
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
npx -y regent-code@3.0.1 install
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
Patches the project config (or the global `~/.config/opencode/` config) to add
|
|
13
|
+
`mcp.servers.regent` and the `plugins` entry. Idempotent and non-destructive;
|
|
14
|
+
`--global` forces the user config, `--file <path>` targets an exact file,
|
|
15
|
+
`--help` for options. Restart the OpenCode session afterwards.
|
|
16
|
+
|
|
3
17
|
## Add to opencode.jsonc
|
|
4
18
|
|
|
5
19
|
```jsonc
|
|
6
20
|
{
|
|
7
21
|
"$schema": "https://opencode.ai/config.json",
|
|
8
|
-
"plugins": ["regent-code@git+https://github.com/nathwn12/regent-code.git#
|
|
22
|
+
"plugins": ["regent-code@git+https://github.com/nathwn12/regent-code.git#v3.0.1"],
|
|
9
23
|
}
|
|
10
24
|
```
|
|
11
25
|
|
|
@@ -18,7 +32,7 @@
|
|
|
18
32
|
}
|
|
19
33
|
```
|
|
20
34
|
|
|
21
|
-
The pinned version is recommended. Use the unpinned branch only when you intentionally want the latest changes. The `
|
|
35
|
+
The pinned version is recommended. Use the unpinned branch only when you intentionally want the latest changes. The `v3.0.1` git tag must be pushed to GitHub before the pinned spec resolves.
|
|
22
36
|
|
|
23
37
|
## Single-source rule (duplicate plugin ID)
|
|
24
38
|
|
package/.opencode/package.json
CHANGED
|
@@ -530,6 +530,135 @@ function unwrapData(result) {
|
|
|
530
530
|
return result?.data ?? result;
|
|
531
531
|
}
|
|
532
532
|
|
|
533
|
+
// ── Worker-turn completion (version-adaptive) ─────────────────
|
|
534
|
+
// The service resolves `session.generate` with the FIRST generated text
|
|
535
|
+
// chunk while the turn continues asynchronously (behavior introduced after
|
|
536
|
+
// beta-18314). The helpers below make the turn protocol version-agnostic:
|
|
537
|
+
// extract whatever shape the generation result has, wait until the session
|
|
538
|
+
// counters prove the turn finished, then read the final assistant text from
|
|
539
|
+
// the transcript. All of them degrade gracefully on legacy session handles.
|
|
540
|
+
|
|
541
|
+
/** @param {unknown} text */
|
|
542
|
+
function isTextPart(text) {
|
|
543
|
+
if (typeof text !== 'object' || text === null) return false;
|
|
544
|
+
/** @type {Record<string, any>} */
|
|
545
|
+
const obj = text;
|
|
546
|
+
return obj.type === 'text' && typeof obj.text === 'string';
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
/**
|
|
550
|
+
* Extract worker text from a generation result regardless of its shape.
|
|
551
|
+
* Supports `{ text }`, `{ message: { text } }`, `{ parts: [...] }`,
|
|
552
|
+
* `{ content: [...] | string }`, and raw strings.
|
|
553
|
+
* @param {unknown} result
|
|
554
|
+
* @returns {string}
|
|
555
|
+
*/
|
|
556
|
+
function extractGenerationText(result) {
|
|
557
|
+
if (typeof result === 'string' && result.trim()) return result;
|
|
558
|
+
if (!result || typeof result !== 'object') return '';
|
|
559
|
+
/** @type {Record<string, any>} */
|
|
560
|
+
const obj = result;
|
|
561
|
+
if (typeof obj.text === 'string' && obj.text.trim()) return obj.text;
|
|
562
|
+
const parts = obj.parts ?? obj.content ?? obj.message?.parts ?? obj.message?.content;
|
|
563
|
+
if (typeof parts === 'string') return parts.trim();
|
|
564
|
+
if (Array.isArray(parts)) {
|
|
565
|
+
const chunks = parts.filter(isTextPart).map((part) => part.text);
|
|
566
|
+
if (chunks.length > 0) return chunks.join('\n').trim();
|
|
567
|
+
}
|
|
568
|
+
return '';
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
/**
|
|
572
|
+
* Join the assistant text of a session transcript (SessionMessageInfo[]).
|
|
573
|
+
* Only `type: "assistant"` messages contribute; reasoning/tool parts are
|
|
574
|
+
* skipped. Handles both chronological and reverse orderings.
|
|
575
|
+
* @param {any[]} messages
|
|
576
|
+
* @returns {string}
|
|
577
|
+
*/
|
|
578
|
+
function joinAssistantText(messages) {
|
|
579
|
+
if (!Array.isArray(messages)) return '';
|
|
580
|
+
const chunks = [];
|
|
581
|
+
for (const message of messages) {
|
|
582
|
+
if (!message || message.type !== 'assistant') continue;
|
|
583
|
+
const content = message.content;
|
|
584
|
+
if (typeof content === 'string') {
|
|
585
|
+
chunks.push(content);
|
|
586
|
+
continue;
|
|
587
|
+
}
|
|
588
|
+
if (Array.isArray(content)) {
|
|
589
|
+
for (const part of content) {
|
|
590
|
+
if (isTextPart(part)) chunks.push(part.text);
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
return chunks.join('\n').trim();
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
/**
|
|
598
|
+
* Read the latest session transcript through whatever the session handle
|
|
599
|
+
* exposes: `message.list` first, then `session.context`, then nothing.
|
|
600
|
+
* @param {any} sessionApi
|
|
601
|
+
* @param {string} sessionID
|
|
602
|
+
* @returns {Promise<any[]>}
|
|
603
|
+
*/
|
|
604
|
+
async function readTurnTranscript(sessionApi, sessionID) {
|
|
605
|
+
if (typeof sessionApi?.message?.list === 'function') {
|
|
606
|
+
try {
|
|
607
|
+
const response = await sessionApi.message.list({ sessionID });
|
|
608
|
+
const data = unwrapData(response);
|
|
609
|
+
if (Array.isArray(data)) return data;
|
|
610
|
+
if (Array.isArray(data?.data)) return data.data;
|
|
611
|
+
return [];
|
|
612
|
+
} catch {
|
|
613
|
+
/* fall through to context */
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
if (typeof sessionApi?.context === 'function') {
|
|
617
|
+
try {
|
|
618
|
+
const response = await sessionApi.context({ sessionID });
|
|
619
|
+
const data = unwrapData(response);
|
|
620
|
+
return Array.isArray(data) ? data : [];
|
|
621
|
+
} catch {
|
|
622
|
+
return [];
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
return [];
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
/**
|
|
629
|
+
* Collect the final worker answer for a dispatched turn. Version-adaptive:
|
|
630
|
+
* 1. A persisted transcript (message.list / session.context) wins when the
|
|
631
|
+
* service stores turns.
|
|
632
|
+
* 2. Otherwise the generation result ("seed") is the answer — current
|
|
633
|
+
* service semantics are synchronous: `generate` blocks until the turn ends
|
|
634
|
+
* and returns the full assistant text in `{text}`, persisting nothing.
|
|
635
|
+
* 3. With neither available yet, wait briefly for async persistence, then
|
|
636
|
+
* give up with whatever exists (bounded, never hangs).
|
|
637
|
+
* @param {any} sessionApi
|
|
638
|
+
* @param {string} sessionID
|
|
639
|
+
* @param {string} seed text returned by `session.generate`
|
|
640
|
+
* @param {{ timeoutMs?: number, intervalMs?: number }} [options]
|
|
641
|
+
* @returns {Promise<string>}
|
|
642
|
+
*/
|
|
643
|
+
async function collectWorkerAnswer(
|
|
644
|
+
sessionApi,
|
|
645
|
+
sessionID,
|
|
646
|
+
seed,
|
|
647
|
+
{ timeoutMs = 30000, intervalMs = 800 } = {},
|
|
648
|
+
) {
|
|
649
|
+
const transcriptText = joinAssistantText(await readTurnTranscript(sessionApi, sessionID));
|
|
650
|
+
if (transcriptText) return transcriptText;
|
|
651
|
+
if (seed) return seed;
|
|
652
|
+
|
|
653
|
+
const deadline = Date.now() + timeoutMs;
|
|
654
|
+
while (Date.now() < deadline) {
|
|
655
|
+
await new Promise((resolve) => setTimeout(resolve, intervalMs));
|
|
656
|
+
const text = joinAssistantText(await readTurnTranscript(sessionApi, sessionID));
|
|
657
|
+
if (text) return text;
|
|
658
|
+
}
|
|
659
|
+
return '';
|
|
660
|
+
}
|
|
661
|
+
|
|
533
662
|
function normalizeAgents(result) {
|
|
534
663
|
const data = unwrapData(result);
|
|
535
664
|
if (Array.isArray(data)) return data;
|
|
@@ -564,6 +693,10 @@ function isUnavailableAgentError(err) {
|
|
|
564
693
|
}
|
|
565
694
|
|
|
566
695
|
async function createWorkerResolver(agentApi, options = {}) {
|
|
696
|
+
const locationInput =
|
|
697
|
+
options && typeof options.location === 'string'
|
|
698
|
+
? { location: { directory: options.location } }
|
|
699
|
+
: {};
|
|
567
700
|
let catalog = null;
|
|
568
701
|
if (typeof agentApi?.list === 'function') {
|
|
569
702
|
try {
|
|
@@ -571,6 +704,15 @@ async function createWorkerResolver(agentApi, options = {}) {
|
|
|
571
704
|
} catch {
|
|
572
705
|
catalog = null;
|
|
573
706
|
}
|
|
707
|
+
// Some service versions require an explicit location scope; retry when
|
|
708
|
+
// the un-scoped call came back empty instead of giving up on the catalog.
|
|
709
|
+
if ((!Array.isArray(catalog) || catalog.length === 0) && locationInput.location) {
|
|
710
|
+
try {
|
|
711
|
+
catalog = normalizeAgents(await agentApi.list(locationInput));
|
|
712
|
+
} catch {
|
|
713
|
+
catalog = null;
|
|
714
|
+
}
|
|
715
|
+
}
|
|
574
716
|
}
|
|
575
717
|
|
|
576
718
|
const findAgent = async (id) => {
|
|
@@ -578,7 +720,7 @@ async function createWorkerResolver(agentApi, options = {}) {
|
|
|
578
720
|
if (fromCatalog) return fromCatalog;
|
|
579
721
|
if (catalog !== null || typeof agentApi?.get !== 'function') return undefined;
|
|
580
722
|
try {
|
|
581
|
-
return unwrapData(await agentApi.get({ agentID: id }));
|
|
723
|
+
return unwrapData(await agentApi.get({ agentID: id, ...locationInput }));
|
|
582
724
|
} catch {
|
|
583
725
|
return undefined;
|
|
584
726
|
}
|
|
@@ -642,8 +784,22 @@ async function createWorkerResolver(agentApi, options = {}) {
|
|
|
642
784
|
? caller.id
|
|
643
785
|
: '';
|
|
644
786
|
const callerAgent = callerId ? await findAgent(callerId) : undefined;
|
|
645
|
-
|
|
646
|
-
|
|
787
|
+
|
|
788
|
+
// Resolvable caller: strict primary-capable gate.
|
|
789
|
+
if (callerAgent) {
|
|
790
|
+
if (!isPrimaryCapableAgent(callerAgent)) {
|
|
791
|
+
return 'caller is not a visible primary-capable agent; subagent dispatch is blocked';
|
|
792
|
+
}
|
|
793
|
+
return null;
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
// Unresolvable caller — the agent API shape or visibility policy changed
|
|
797
|
+
// at runtime. Degrade OPEN instead of breaking dispatch for every primary
|
|
798
|
+
// session. The only hard block that survives is recursion from a worker
|
|
799
|
+
// session this plugin created.
|
|
800
|
+
const callerSessionId = typeof toolContext?.sessionID === 'string' ? toolContext.sessionID : '';
|
|
801
|
+
if (callerSessionId && pluginWorkerSessionIds.has(callerSessionId)) {
|
|
802
|
+
return 'caller is a Regent worker session; nested subagent dispatch is blocked';
|
|
647
803
|
}
|
|
648
804
|
return null;
|
|
649
805
|
};
|
|
@@ -772,8 +928,11 @@ async function dispatchSubagent(
|
|
|
772
928
|
].join('\n');
|
|
773
929
|
|
|
774
930
|
const result = await withRetry(() => sessionApi.generate({ sessionID: session.id, prompt }));
|
|
775
|
-
const
|
|
776
|
-
|
|
931
|
+
const seed = extractGenerationText(unwrapData(result));
|
|
932
|
+
|
|
933
|
+
// Collect the final answer: persisted transcript wins, otherwise the
|
|
934
|
+
// synchronous generation result is the answer.
|
|
935
|
+
const output = await collectWorkerAnswer(sessionApi, session.id, seed);
|
|
777
936
|
const parsed = parseSubagentTextResponse(output);
|
|
778
937
|
const { status, concerns, filesChanged } = parsed;
|
|
779
938
|
|
|
@@ -971,7 +1130,10 @@ export default Plugin.define({
|
|
|
971
1130
|
async setup(ctx) {
|
|
972
1131
|
const registrations = [];
|
|
973
1132
|
const options = ctx.options && typeof ctx.options === 'object' ? ctx.options : {};
|
|
974
|
-
const resolveWorker = await createWorkerResolver(ctx.agent,
|
|
1133
|
+
const resolveWorker = await createWorkerResolver(ctx.agent, {
|
|
1134
|
+
...options,
|
|
1135
|
+
location: options.location ?? ctx.location?.directory ?? process.cwd(),
|
|
1136
|
+
});
|
|
975
1137
|
|
|
976
1138
|
if (typeof options.primaryAgent === 'string' && options.primaryAgent.trim()) {
|
|
977
1139
|
if (typeof ctx.agent?.transform === 'function') {
|
package/README.md
CHANGED
|
@@ -51,14 +51,46 @@ Add Regent to your OpenCode configuration:
|
|
|
51
51
|
```jsonc
|
|
52
52
|
{
|
|
53
53
|
"$schema": "https://opencode.ai/config.json",
|
|
54
|
-
"plugins": ["regent-code@git+https://github.com/nathwn12/regent-code.git#
|
|
54
|
+
"plugins": ["regent-code@git+https://github.com/nathwn12/regent-code.git#v3.0.2"],
|
|
55
55
|
}
|
|
56
56
|
```
|
|
57
57
|
|
|
58
|
-
|
|
58
|
+
### Version compatibility (dynamic, not pinned)
|
|
59
|
+
|
|
60
|
+
Since v3.0.2 regent-code does **not** pin a specific OpenCode beta. Both the
|
|
61
|
+
plugin and the MCP server depend on `@opencode-ai/client` and
|
|
62
|
+
`@opencode-ai/plugin` through a floating beta range
|
|
63
|
+
(`>=0.0.0-beta-18314 <0.0.0-beta-99999`) that resolves the newest beta on every
|
|
64
|
+
install — matching whatever service version that machine runs, no manual pin
|
|
65
|
+
updates. The dispatch code is *runtime-adaptive* on top: generation results
|
|
66
|
+
are read from the persisted transcript when the service stores turns, otherwise
|
|
67
|
+
from the synchronous `session.generate` text; agent catalogs are queried with
|
|
68
|
+
and without an explicit location scope; and the caller-authorization guardrail
|
|
69
|
+
degrades OPEN when the runtime agent API is unresolvable (only recursion from
|
|
70
|
+
known worker sessions stays hard-blocked). If the beta track ever renumbers
|
|
71
|
+
(e.g. `0.1.0`), raise the range's upper bound in `package.json` and
|
|
72
|
+
`.opencode/package.json`.
|
|
73
|
+
The `v3.0.2` git tag must be pushed to GitHub before this pinned spec resolves.
|
|
59
74
|
|
|
60
75
|
> **Windows dev-machine warning (single-source rule):** when this repository is open as an OpenCode project, its own `.opencode/plugins/regent.js` is auto-loaded as a project plugin. Do NOT also pin regent in `opencode.jsonc` on the same machine — two active sources make host plugin reloads fail with `Duplicate plugin ID: regent`, leaving sessions with a torn tool surface and blocking live skill/plugin edits. Either develop unpinned (project plugin only) or pin the repo file directly: `"plugins": ["file:///Q:/PROJECTS/PERSONAL/regent-code/.opencode/plugins/regent.js"]`. One source of truth, always.
|
|
61
76
|
|
|
77
|
+
## One-command install
|
|
78
|
+
|
|
79
|
+
The fastest way to get **both** the plugin and the MCP server on any machine — no cloning, no manual config edits, no local files:
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
npx -y regent-code@3.0.2 install
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
The installer finds an existing `opencode.json` / `opencode.jsonc` (project config in the current directory first, then the global `~/.config/opencode/` config) and adds both entries:
|
|
86
|
+
|
|
87
|
+
- **MCP server**: `mcp.servers.regent` → runs `["npx", "-y", "regent-code@3.0.2"]`
|
|
88
|
+
- **Plugin**: `plugins` → `regent-code@3.0.2`
|
|
89
|
+
|
|
90
|
+
It is **idempotent and non-destructive** — it only adds or updates regent entries, preserving comments, trailing commas, and every unrelated setting in the file. Re-run it to upgrade the pinned version. Flags: `--global` forces the user config, `--file <path>` targets an exact file, `--help` explains all options. Restart the OpenCode session afterwards — plugin load and MCP connection happen on config load.
|
|
91
|
+
|
|
92
|
+
> On the machine that develops regent-code itself, respect the single-source rule above: do not add a second pin when the repo is open as a project.
|
|
93
|
+
|
|
62
94
|
## MCP Server
|
|
63
95
|
|
|
64
96
|
Since v3.0.0, regent-code ships a second distribution alongside the plugin: a Model Context Protocol (MCP) server that exposes the same six tools (`delegate`, `delegate_many`, `research`, `explore`, `changed-files`, `verify`) plus the Regent command and skill corpus as MCP prompts. It is out-of-process and works from any MCP client (Claude Desktop, Cursor, or OpenCode).
|
|
@@ -91,7 +123,7 @@ Configure it in OpenCode by adding a local MCP server:
|
|
|
91
123
|
"servers": {
|
|
92
124
|
"regent": {
|
|
93
125
|
"type": "local",
|
|
94
|
-
"command": ["npx", "-y", "regent-code@3.0.
|
|
126
|
+
"command": ["npx", "-y", "regent-code@3.0.2"]
|
|
95
127
|
}
|
|
96
128
|
}
|
|
97
129
|
}
|
package/mcp/cli.js
CHANGED
|
@@ -1,9 +1,83 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
//
|
|
1
|
+
// Regent CLI entry (package `bin`).
|
|
2
|
+
// no arguments -> run the MCP server over stdio (that is what a local
|
|
3
|
+
// `mcp.servers` entry in OpenCode invokes)
|
|
4
|
+
// install -> patch an existing opencode.json / opencode.jsonc to
|
|
5
|
+
// register the Regent MCP server AND plugin; idempotent and
|
|
6
|
+
// non-destructive, can create the config when missing
|
|
7
|
+
// anything else -> usage error
|
|
4
8
|
import { main } from './index.js';
|
|
9
|
+
import { install, InstallError, PLUGIN_SPEC, SERVER_NAME } from './install.js';
|
|
5
10
|
|
|
6
|
-
|
|
7
|
-
|
|
11
|
+
const USAGE = `Usage: npx -y ${PLUGIN_SPEC} install [--global] [--file <path>] [--help]
|
|
12
|
+
|
|
13
|
+
Patches an existing opencode.json / opencode.jsonc (V2 OpenCode config) to
|
|
14
|
+
register:
|
|
15
|
+
- MCP server "${SERVER_NAME}" running ${PLUGIN_SPEC}
|
|
16
|
+
- plugin ${PLUGIN_SPEC}
|
|
17
|
+
|
|
18
|
+
Without flags: patches ./opencode.json(c) or ./.opencode/opencode.json(c) when
|
|
19
|
+
present, otherwise the global ~/.config/opencode config, creating it if needed.
|
|
20
|
+
|
|
21
|
+
--global patch (or create) the user-global config instead
|
|
22
|
+
--file P patch the exact file path (wins over --global)
|
|
23
|
+
--help show this help
|
|
24
|
+
|
|
25
|
+
Non-destructive and idempotent: only adds or updates regent entries, leaving
|
|
26
|
+
comments, formatting, and unrelated settings untouched. Restart the OpenCode
|
|
27
|
+
session afterwards — plugin load and MCP connection happen on config load.`;
|
|
28
|
+
|
|
29
|
+
function runInstaller(args) {
|
|
30
|
+
const options = { cwd: process.cwd() };
|
|
31
|
+
for (let i = 0; i < args.length; i++) {
|
|
32
|
+
switch (args[i]) {
|
|
33
|
+
case '--global':
|
|
34
|
+
options.global = true;
|
|
35
|
+
break;
|
|
36
|
+
case '--file':
|
|
37
|
+
if (!args[i + 1]) throw new InstallError('--file requires a path argument');
|
|
38
|
+
options.file = args[++i];
|
|
39
|
+
break;
|
|
40
|
+
case '--help':
|
|
41
|
+
console.log(USAGE);
|
|
42
|
+
process.exit(0);
|
|
43
|
+
break;
|
|
44
|
+
default:
|
|
45
|
+
throw new InstallError(`unknown argument: ${args[i]}\n\n${USAGE}`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const report = install(options);
|
|
50
|
+
const lines = [
|
|
51
|
+
`Installed into: ${report.path} (${report.scope}${report.created ? ', created' : ''})`,
|
|
52
|
+
` MCP server "${SERVER_NAME}": ${report.mcp}`,
|
|
53
|
+
` Plugin ${PLUGIN_SPEC}: ${report.plugin}`,
|
|
54
|
+
'',
|
|
55
|
+
...report.warnings.map((warning) => ` warning: ${warning}`),
|
|
56
|
+
'',
|
|
57
|
+
'Restart your OpenCode session — the plugin registers on load and the',
|
|
58
|
+
'MCP server connects on the next config load.',
|
|
59
|
+
];
|
|
60
|
+
console.log(lines.join('\n'));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const args = process.argv.slice(2);
|
|
64
|
+
|
|
65
|
+
if (args[0] === 'install') {
|
|
66
|
+
try {
|
|
67
|
+
runInstaller(args.slice(1));
|
|
68
|
+
} catch (err) {
|
|
69
|
+
if (err instanceof InstallError) {
|
|
70
|
+
console.error(err.message);
|
|
71
|
+
process.exit(1);
|
|
72
|
+
}
|
|
73
|
+
throw err;
|
|
74
|
+
}
|
|
75
|
+
} else if (args.length === 0) {
|
|
76
|
+
main().catch((err) => {
|
|
77
|
+
console.error(err);
|
|
78
|
+
process.exit(1);
|
|
79
|
+
});
|
|
80
|
+
} else {
|
|
81
|
+
console.error(`Unknown arguments: ${args.join(' ')}\n\n${USAGE}`);
|
|
8
82
|
process.exit(1);
|
|
9
|
-
}
|
|
83
|
+
}
|
package/mcp/index.js
CHANGED
|
@@ -29,6 +29,8 @@ import {
|
|
|
29
29
|
isSensitiveFocusPath,
|
|
30
30
|
parseSubagentTextResponse,
|
|
31
31
|
unwrapData,
|
|
32
|
+
extractGenerationText,
|
|
33
|
+
collectWorkerAnswer,
|
|
32
34
|
sessionFileChanges,
|
|
33
35
|
workerSessionIds,
|
|
34
36
|
dispatchRateLimit,
|
|
@@ -41,7 +43,7 @@ import {
|
|
|
41
43
|
|
|
42
44
|
import { readPackagePrompts, renderPrompt } from './prompts.js';
|
|
43
45
|
|
|
44
|
-
const version = '3.0.
|
|
46
|
+
const version = '3.0.1';
|
|
45
47
|
|
|
46
48
|
// ── OpenCode client (lazy singleton) ─────────────────────────
|
|
47
49
|
let clientPromise = null;
|
|
@@ -104,23 +106,36 @@ function isUnavailableAgentError(err) {
|
|
|
104
106
|
|
|
105
107
|
/**
|
|
106
108
|
* @param {ReturnType<typeof OpenCode.make>} client
|
|
107
|
-
* @param {{ workerAgent?: string }} [options]
|
|
109
|
+
* @param {{ workerAgent?: string, location?: string }} [options]
|
|
108
110
|
* @returns {Promise<(requestedAgent?: string) => Promise<{ ok: true, agents: string[], automatic: boolean } | { ok: false, error: string }>>}
|
|
109
111
|
*/
|
|
110
112
|
async function createWorkerResolver(client, options = {}) {
|
|
113
|
+
const locationInput =
|
|
114
|
+
options && typeof options.location === 'string'
|
|
115
|
+
? { location: { directory: options.location } }
|
|
116
|
+
: {};
|
|
111
117
|
let catalog = null;
|
|
112
118
|
try {
|
|
113
119
|
catalog = normalizeAgents(await client.agent.list());
|
|
114
120
|
} catch {
|
|
115
121
|
catalog = null;
|
|
116
122
|
}
|
|
123
|
+
// Some service versions require an explicit location scope; retry when the
|
|
124
|
+
// un-scoped call came back empty instead of giving up on the catalog.
|
|
125
|
+
if ((!Array.isArray(catalog) || catalog.length === 0) && locationInput.location) {
|
|
126
|
+
try {
|
|
127
|
+
catalog = normalizeAgents(await client.agent.list(locationInput));
|
|
128
|
+
} catch {
|
|
129
|
+
catalog = null;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
117
132
|
|
|
118
133
|
const findAgent = async (id) => {
|
|
119
134
|
const fromCatalog = catalog?.find((agent) => agent.id === id);
|
|
120
135
|
if (fromCatalog) return fromCatalog;
|
|
121
136
|
if (catalog !== null) return undefined;
|
|
122
137
|
try {
|
|
123
|
-
return unwrapData(await client.agent.get({ agentID: id }));
|
|
138
|
+
return unwrapData(await client.agent.get({ agentID: id, ...locationInput }));
|
|
124
139
|
} catch {
|
|
125
140
|
return undefined;
|
|
126
141
|
}
|
|
@@ -269,8 +284,11 @@ async function dispatchSubagent(client, resolveWorker, args, taskId = '') {
|
|
|
269
284
|
const result = await withRetry(() =>
|
|
270
285
|
client.session.generate({ sessionID: session.id, prompt }),
|
|
271
286
|
);
|
|
272
|
-
const
|
|
273
|
-
|
|
287
|
+
const seed = extractGenerationText(unwrapData(result));
|
|
288
|
+
|
|
289
|
+
// Collect the final answer: persisted transcript wins, otherwise the
|
|
290
|
+
// synchronous generation result is the answer.
|
|
291
|
+
const output = await collectWorkerAnswer(client, session.id, seed);
|
|
274
292
|
const parsed = parseSubagentTextResponse(output);
|
|
275
293
|
const { status, concerns, filesChanged } = parsed;
|
|
276
294
|
|
|
@@ -595,6 +613,7 @@ export function createRegentServer() {
|
|
|
595
613
|
const client = await getClient();
|
|
596
614
|
const resolveWorker = await createWorkerResolver(client, {
|
|
597
615
|
workerAgent: process.env.REGENT_WORKER_AGENT,
|
|
616
|
+
location: process.cwd(),
|
|
598
617
|
});
|
|
599
618
|
const result = await dispatchSubagent(client, resolveWorker, /** @type {any} */ (args));
|
|
600
619
|
return toContent(result);
|
|
@@ -619,6 +638,7 @@ export function createRegentServer() {
|
|
|
619
638
|
const client = await getClient();
|
|
620
639
|
const resolveWorker = await createWorkerResolver(client, {
|
|
621
640
|
workerAgent: process.env.REGENT_WORKER_AGENT,
|
|
641
|
+
location: process.cwd(),
|
|
622
642
|
});
|
|
623
643
|
const queue = [...args.tasks];
|
|
624
644
|
const results = [];
|
|
@@ -663,6 +683,7 @@ export function createRegentServer() {
|
|
|
663
683
|
const client = await getClient();
|
|
664
684
|
const resolveWorker = await createWorkerResolver(client, {
|
|
665
685
|
workerAgent: process.env.REGENT_WORKER_AGENT,
|
|
686
|
+
location: process.cwd(),
|
|
666
687
|
});
|
|
667
688
|
const results = await Promise.all(
|
|
668
689
|
args.questions.map(async (q) => {
|
package/mcp/install.js
CHANGED
|
@@ -9,13 +9,7 @@ import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'node:fs';
|
|
|
9
9
|
import { join, dirname, resolve } from 'node:path';
|
|
10
10
|
import { homedir } from 'node:os';
|
|
11
11
|
import { createRequire } from 'node:module';
|
|
12
|
-
import {
|
|
13
|
-
parseTree,
|
|
14
|
-
findNodeAtLocation,
|
|
15
|
-
getNodeValue,
|
|
16
|
-
modify,
|
|
17
|
-
applyEdits,
|
|
18
|
-
} from 'jsonc-parser';
|
|
12
|
+
import { parseTree, findNodeAtLocation, getNodeValue, modify, applyEdits } from 'jsonc-parser';
|
|
19
13
|
|
|
20
14
|
const require = createRequire(import.meta.url);
|
|
21
15
|
const { version } = require('../package.json');
|
|
@@ -58,10 +52,20 @@ function findExisting(candidates) {
|
|
|
58
52
|
return null;
|
|
59
53
|
}
|
|
60
54
|
|
|
55
|
+
/**
|
|
56
|
+
* @typedef {Object} InstallOptions
|
|
57
|
+
* @property {string} [cwd] Working directory for resolving relative paths and project configs.
|
|
58
|
+
* @property {string} [file] Explicit config path (wins over every other mode).
|
|
59
|
+
* @property {boolean} [global] Force the user-global config.
|
|
60
|
+
*/
|
|
61
|
+
|
|
61
62
|
// Decide which file to patch. Never writes; the installer writes after
|
|
62
63
|
// resolving. Explicit --file wins, then forced global, then: an existing
|
|
63
64
|
// project config in cwd, else an existing global config, else the global
|
|
64
65
|
// location (created on demand).
|
|
66
|
+
/**
|
|
67
|
+
* @param {InstallOptions} [options]
|
|
68
|
+
*/
|
|
65
69
|
export function resolveTarget({ cwd = process.cwd(), file, global = false } = {}) {
|
|
66
70
|
if (file) return { path: resolve(cwd, file), scope: 'explicit' };
|
|
67
71
|
if (global) {
|
|
@@ -91,7 +95,9 @@ export function patchText(
|
|
|
91
95
|
{ pluginSpec = PLUGIN_SPEC, serverName = SERVER_NAME, serverConfig = SERVER_CONFIG } = {},
|
|
92
96
|
) {
|
|
93
97
|
const errors = [];
|
|
94
|
-
|
|
98
|
+
// JSONC by contract: comments are allowed by default, trailing commas must
|
|
99
|
+
// be opted into — both are common in real opencode.json(c) files.
|
|
100
|
+
const root = parseTree(text, errors, { allowTrailingComma: true });
|
|
95
101
|
if (!root || errors.length) {
|
|
96
102
|
throw new InstallError(`invalid JSONC: ${errors[0]?.error ?? 'could not parse'}`);
|
|
97
103
|
}
|
|
@@ -101,8 +107,17 @@ export function patchText(
|
|
|
101
107
|
|
|
102
108
|
const eol = detectEol(text);
|
|
103
109
|
const formattingOptions = { insertSpaces: true, tabSize: 2, eol };
|
|
110
|
+
/** @type {{ mcp: string, plugin: string, warnings: string[] }} */
|
|
104
111
|
const report = { mcp: 'unchanged', plugin: 'unchanged', warnings: [] };
|
|
105
|
-
|
|
112
|
+
|
|
113
|
+
// Apply each edit batch against the CURRENT text, never the original:
|
|
114
|
+
// two insertions at the same location (e.g. missing mcp + missing plugins,
|
|
115
|
+
// both inserted before the closing brace) overlap when computed against one
|
|
116
|
+
// shared base text and make jsonc-parser throw "Overlapping edit".
|
|
117
|
+
let current = text;
|
|
118
|
+
const apply = (edits) => {
|
|
119
|
+
if (edits.length) current = applyEdits(current, edits);
|
|
120
|
+
};
|
|
106
121
|
|
|
107
122
|
// ---- MCP server entry ----
|
|
108
123
|
const mcpNode = findNodeAtLocation(root, ['mcp']);
|
|
@@ -115,24 +130,18 @@ export function patchText(
|
|
|
115
130
|
if (deepEqual(existing, serverConfig)) {
|
|
116
131
|
report.mcp = 'unchanged';
|
|
117
132
|
} else {
|
|
118
|
-
|
|
119
|
-
modify(text, ['mcp', 'servers', serverName], serverConfig, { formattingOptions }),
|
|
120
|
-
);
|
|
133
|
+
apply(modify(current, ['mcp', 'servers', serverName], serverConfig, { formattingOptions }));
|
|
121
134
|
report.mcp = 'updated';
|
|
122
135
|
}
|
|
123
136
|
} else {
|
|
124
|
-
report.warnings.push(
|
|
125
|
-
`mcp.servers.${serverName} exists but is not an object; left untouched`,
|
|
126
|
-
);
|
|
137
|
+
report.warnings.push(`mcp.servers.${serverName} exists but is not an object; left untouched`);
|
|
127
138
|
}
|
|
128
139
|
} else if (mcpNode && mcpNode.type !== 'object') {
|
|
129
140
|
report.warnings.push('existing "mcp" key is not an object; left untouched');
|
|
130
141
|
} else if (serversNode && serversNode.type !== 'object') {
|
|
131
142
|
report.warnings.push('existing "mcp.servers" is not an object; left untouched');
|
|
132
143
|
} else {
|
|
133
|
-
|
|
134
|
-
modify(text, ['mcp', 'servers', serverName], serverConfig, { formattingOptions }),
|
|
135
|
-
);
|
|
144
|
+
apply(modify(current, ['mcp', 'servers', serverName], serverConfig, { formattingOptions }));
|
|
136
145
|
report.mcp = 'added';
|
|
137
146
|
}
|
|
138
147
|
|
|
@@ -150,12 +159,11 @@ export function patchText(
|
|
|
150
159
|
(entry) => /^file:/.test(entry) && entry.toLowerCase().includes('regent'),
|
|
151
160
|
);
|
|
152
161
|
const hasNpmPin =
|
|
153
|
-
entries.some(
|
|
154
|
-
|
|
162
|
+
entries.some(
|
|
163
|
+
(entry) => entry.startsWith('regent-code@') && !/^regent-code@git/.test(entry),
|
|
164
|
+
) || entries.includes('regent-code');
|
|
155
165
|
|
|
156
|
-
const next = entries
|
|
157
|
-
.filter((entry) => !gitEntries.includes(entry))
|
|
158
|
-
.concat(nonString);
|
|
166
|
+
const next = entries.filter((entry) => !gitEntries.includes(entry)).concat(nonString);
|
|
159
167
|
let reportPlugin;
|
|
160
168
|
if (hasNpmPin && gitEntries.length === 0) {
|
|
161
169
|
reportPlugin = 'unchanged';
|
|
@@ -164,31 +172,32 @@ export function patchText(
|
|
|
164
172
|
}
|
|
165
173
|
if (!hasNpmPin) next.push(pluginSpec);
|
|
166
174
|
if (filePin) {
|
|
167
|
-
report.warnings.push(
|
|
168
|
-
'regent plugin pinned via file:// (dev loop); entry left untouched',
|
|
169
|
-
);
|
|
175
|
+
report.warnings.push('regent plugin pinned via file:// (dev loop); entry left untouched');
|
|
170
176
|
}
|
|
171
177
|
if (!deepEqual(next, existing)) {
|
|
172
|
-
|
|
178
|
+
apply(modify(current, ['plugins'], next, { formattingOptions }));
|
|
173
179
|
report.plugin = reportPlugin;
|
|
174
180
|
}
|
|
175
181
|
}
|
|
176
182
|
} else {
|
|
177
|
-
|
|
183
|
+
apply(modify(current, ['plugins'], [pluginSpec], { formattingOptions }));
|
|
178
184
|
report.plugin = 'added';
|
|
179
185
|
}
|
|
180
186
|
|
|
181
|
-
return { text:
|
|
187
|
+
return { text: current, report };
|
|
182
188
|
}
|
|
183
189
|
|
|
184
190
|
// Full install against the filesystem: resolves the target config, patches it
|
|
185
191
|
// (creating the file with only the regent entries when none exists), and
|
|
186
192
|
// returns the report plus target info.
|
|
193
|
+
/**
|
|
194
|
+
* @param {InstallOptions} [options]
|
|
195
|
+
*/
|
|
187
196
|
export function install(options = {}) {
|
|
188
197
|
const target = resolveTarget(options);
|
|
189
198
|
const existingText = existsSync(target.path) ? readFileSync(target.path, 'utf8') : null;
|
|
190
199
|
const base = existingText ?? `{\n}\n`;
|
|
191
|
-
const { text, report } = patchText(base
|
|
200
|
+
const { text, report } = patchText(base);
|
|
192
201
|
|
|
193
202
|
if (text !== existingText) {
|
|
194
203
|
mkdirSync(dirname(target.path), { recursive: true });
|
|
@@ -201,4 +210,4 @@ export function install(options = {}) {
|
|
|
201
210
|
scope: target.scope,
|
|
202
211
|
created: existingText === null,
|
|
203
212
|
};
|
|
204
|
-
}
|
|
213
|
+
}
|
package/mcp/shared.js
CHANGED
|
@@ -272,6 +272,136 @@ export function unwrapData(result) {
|
|
|
272
272
|
return result?.data ?? result;
|
|
273
273
|
}
|
|
274
274
|
|
|
275
|
+
// ── Worker-turn completion (version-adaptive) ─────────────────
|
|
276
|
+
// The service resolves `session.generate` with the FIRST generated text
|
|
277
|
+
// chunk while the turn continues asynchronously (behavior introduced after
|
|
278
|
+
// beta-18314). These helpers make the turn completion protocol version-
|
|
279
|
+
// agnostic: extract whatever shape the generation result has, wait until the
|
|
280
|
+
// session's token/time counters prove the turn finished, then read the final
|
|
281
|
+
// assistant text from the transcript.
|
|
282
|
+
|
|
283
|
+
/** @param {unknown} text */
|
|
284
|
+
function isTextPart(text) {
|
|
285
|
+
if (typeof text !== 'object' || text === null) return false;
|
|
286
|
+
/** @type {Record<string, any>} */
|
|
287
|
+
const obj = text;
|
|
288
|
+
return obj.type === 'text' && typeof obj.text === 'string';
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Extract worker text from a generation result regardless of its shape.
|
|
293
|
+
* Supports: `{ text }`, `{ message: { text } }`, `{ parts: [{type:"text",text}] }`,
|
|
294
|
+
* `{ content: [{type:"text",text}] | string }`, and raw string results.
|
|
295
|
+
* @param {unknown} result
|
|
296
|
+
* @returns {string}
|
|
297
|
+
*/
|
|
298
|
+
export function extractGenerationText(result) {
|
|
299
|
+
if (typeof result === 'string' && result.trim()) return result;
|
|
300
|
+
if (!result || typeof result !== 'object') return '';
|
|
301
|
+
/** @type {Record<string, any>} */
|
|
302
|
+
const obj = result;
|
|
303
|
+
if (typeof obj.text === 'string' && obj.text.trim()) return obj.text;
|
|
304
|
+
const parts = obj.parts ?? obj.content ?? obj.message?.parts ?? obj.message?.content;
|
|
305
|
+
if (typeof parts === 'string') return parts.trim();
|
|
306
|
+
if (Array.isArray(parts)) {
|
|
307
|
+
const chunks = parts.filter(isTextPart).map((part) => part.text);
|
|
308
|
+
if (chunks.length > 0) return chunks.join('\n').trim();
|
|
309
|
+
}
|
|
310
|
+
return '';
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Join the assistant text of a session transcript (SessionMessageInfo[]).
|
|
315
|
+
* Only `type: "assistant"` messages contribute; reasoning/tool parts are
|
|
316
|
+
* skipped. Handles both chronological and reverse (desc) orderings.
|
|
317
|
+
* @param {any[]} messages
|
|
318
|
+
* @returns {string}
|
|
319
|
+
*/
|
|
320
|
+
export function joinAssistantText(messages) {
|
|
321
|
+
if (!Array.isArray(messages)) return '';
|
|
322
|
+
const chunks = [];
|
|
323
|
+
for (const message of messages) {
|
|
324
|
+
if (!message || message.type !== 'assistant') continue;
|
|
325
|
+
const content = message.content;
|
|
326
|
+
if (typeof content === 'string') {
|
|
327
|
+
chunks.push(content);
|
|
328
|
+
continue;
|
|
329
|
+
}
|
|
330
|
+
if (Array.isArray(content)) {
|
|
331
|
+
for (const part of content) {
|
|
332
|
+
if (isTextPart(part)) chunks.push(part.text);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
return chunks.join('\n').trim();
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Read the latest session transcript through whatever API the session handle
|
|
341
|
+
* exposes: `message.list` (client) first, then `session.context` (plugin
|
|
342
|
+
* domain), then nothing.
|
|
343
|
+
* @param {any} sessionApi session or client handle
|
|
344
|
+
* @param {string} sessionID
|
|
345
|
+
* @returns {Promise<any[]>}
|
|
346
|
+
*/
|
|
347
|
+
export async function readTurnTranscript(sessionApi, sessionID) {
|
|
348
|
+
if (typeof sessionApi?.message?.list === 'function') {
|
|
349
|
+
try {
|
|
350
|
+
const response = await sessionApi.message.list({ sessionID });
|
|
351
|
+
const data = unwrapData(response);
|
|
352
|
+
if (Array.isArray(data)) return data;
|
|
353
|
+
if (Array.isArray(data?.data)) return data.data;
|
|
354
|
+
return [];
|
|
355
|
+
} catch {
|
|
356
|
+
/* fall through to context */
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
if (typeof sessionApi?.context === 'function') {
|
|
360
|
+
try {
|
|
361
|
+
const response = await sessionApi.context({ sessionID });
|
|
362
|
+
const data = unwrapData(response);
|
|
363
|
+
return Array.isArray(data) ? data : [];
|
|
364
|
+
} catch {
|
|
365
|
+
return [];
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
return [];
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* Collect the final worker answer for a dispatched turn. Version-adaptive:
|
|
373
|
+
* 1. A persisted transcript (message.list / session.context) wins when the
|
|
374
|
+
* service stores turns.
|
|
375
|
+
* 2. Otherwise the generation result ("seed") is the answer — current
|
|
376
|
+
* service semantics are synchronous: `generate` blocks until the turn ends
|
|
377
|
+
* and returns the full assistant text in `{text}`, persisting nothing.
|
|
378
|
+
* 3. With neither available yet, wait briefly for async persistence, then
|
|
379
|
+
* give up with whatever exists (bounded, never hangs).
|
|
380
|
+
* @param {any} sessionApi session or client handle
|
|
381
|
+
* @param {string} sessionID
|
|
382
|
+
* @param {string} seed text returned by `session.generate`
|
|
383
|
+
* @param {{ timeoutMs?: number, intervalMs?: number }} [options]
|
|
384
|
+
* @returns {Promise<string>}
|
|
385
|
+
*/
|
|
386
|
+
export async function collectWorkerAnswer(
|
|
387
|
+
sessionApi,
|
|
388
|
+
sessionID,
|
|
389
|
+
seed,
|
|
390
|
+
{ timeoutMs = 30000, intervalMs = 800 } = {},
|
|
391
|
+
) {
|
|
392
|
+
const transcriptText = joinAssistantText(await readTurnTranscript(sessionApi, sessionID));
|
|
393
|
+
if (transcriptText) return transcriptText;
|
|
394
|
+
if (seed) return seed;
|
|
395
|
+
|
|
396
|
+
const deadline = Date.now() + timeoutMs;
|
|
397
|
+
while (Date.now() < deadline) {
|
|
398
|
+
await new Promise((resolve) => setTimeout(resolve, intervalMs));
|
|
399
|
+
const text = joinAssistantText(await readTurnTranscript(sessionApi, sessionID));
|
|
400
|
+
if (text) return text;
|
|
401
|
+
}
|
|
402
|
+
return '';
|
|
403
|
+
}
|
|
404
|
+
|
|
275
405
|
// ── State (single MCP process scope; no session lineage) ──
|
|
276
406
|
/** @type {Map<string, { taskId?: string, files: string[], timestamp: number, verified: boolean }>} */
|
|
277
407
|
export const sessionFileChanges = new Map();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "regent-code",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.2",
|
|
4
4
|
"description": "Agent orchestration for OpenCode. From idea to shipped — zero ceremony. Plugin + MCP server.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": ".opencode/plugins/regent.js",
|
|
@@ -15,8 +15,8 @@
|
|
|
15
15
|
"author": "nathwn12",
|
|
16
16
|
"dependencies": {
|
|
17
17
|
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
18
|
-
"@opencode-ai/client": "0.0.0-beta-
|
|
19
|
-
"@opencode-ai/plugin": "0.0.0-beta-
|
|
18
|
+
"@opencode-ai/client": "^0.0.0-beta-18414",
|
|
19
|
+
"@opencode-ai/plugin": "^0.0.0-beta-18414",
|
|
20
20
|
"jsonc-parser": "3.3.1"
|
|
21
21
|
},
|
|
22
22
|
"repository": {
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
"format": "prettier --write .opencode/plugins/ skills/ mcp/ --no-error-on-unmatched-pattern",
|
|
31
31
|
"format:check": "prettier --check .opencode/plugins/ skills/ mcp/ --no-error-on-unmatched-pattern",
|
|
32
32
|
"typecheck": "tsc --noEmit",
|
|
33
|
-
"test": "node --test .opencode/tests/regent.test.js .opencode/tests/regent.live-test.js .opencode/tests/regent.v2.test.js .opencode/tests/regent.hybrid.v2.6.1.test.js .opencode/tests/regent.runtime.v2.6.1.test.js mcp/tests/mcp.test.js",
|
|
33
|
+
"test": "node --test .opencode/tests/regent.test.js .opencode/tests/regent.live-test.js .opencode/tests/regent.v2.test.js .opencode/tests/regent.hybrid.v2.6.1.test.js .opencode/tests/regent.runtime.v2.6.1.test.js mcp/tests/mcp.test.js mcp/tests/install.test.js",
|
|
34
34
|
"verify": "npm run format:check && npm run lint && npm run typecheck && npm test"
|
|
35
35
|
},
|
|
36
36
|
"devDependencies": {
|
|
Binary file
|