myshell-tools 2.4.0 → 2.7.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/CHANGELOG.md +24 -0
- package/README.md +28 -12
- package/dist/cli.js +38 -3
- package/dist/cli.js.map +1 -1
- package/dist/commands/cost.js +4 -1
- package/dist/commands/cost.js.map +1 -1
- package/dist/commands/doctor.js +2 -2
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/login.d.ts +51 -5
- package/dist/commands/login.js +207 -14
- package/dist/commands/login.js.map +1 -1
- package/dist/core/assess.js +2 -62
- package/dist/core/assess.js.map +1 -1
- package/dist/core/budget.d.ts +26 -0
- package/dist/core/budget.js +37 -0
- package/dist/core/budget.js.map +1 -0
- package/dist/core/history.d.ts +35 -0
- package/dist/core/history.js +116 -0
- package/dist/core/history.js.map +1 -0
- package/dist/core/json-envelope.d.ts +49 -0
- package/dist/core/json-envelope.js +117 -0
- package/dist/core/json-envelope.js.map +1 -0
- package/dist/core/orchestrate.js +107 -8
- package/dist/core/orchestrate.js.map +1 -1
- package/dist/core/policy.js +17 -9
- package/dist/core/policy.js.map +1 -1
- package/dist/core/prompt.d.ts +9 -4
- package/dist/core/prompt.js +14 -5
- package/dist/core/prompt.js.map +1 -1
- package/dist/core/review.js +2 -49
- package/dist/core/review.js.map +1 -1
- package/dist/core/route.d.ts +13 -5
- package/dist/core/route.js +20 -6
- package/dist/core/route.js.map +1 -1
- package/dist/core/types.d.ts +37 -0
- package/dist/infra/credentials.d.ts +58 -0
- package/dist/infra/credentials.js +172 -0
- package/dist/infra/credentials.js.map +1 -0
- package/dist/infra/pricing.d.ts +17 -4
- package/dist/infra/pricing.js +73 -3
- package/dist/infra/pricing.js.map +1 -1
- package/dist/interface/menu.d.ts +26 -0
- package/dist/interface/menu.js +131 -26
- package/dist/interface/menu.js.map +1 -1
- package/dist/providers/detect.d.ts +17 -5
- package/dist/providers/detect.js +56 -4
- package/dist/providers/detect.js.map +1 -1
- package/dist/providers/install.js +1 -0
- package/dist/providers/install.js.map +1 -1
- package/dist/providers/opencode-parse.d.ts +49 -0
- package/dist/providers/opencode-parse.js +181 -0
- package/dist/providers/opencode-parse.js.map +1 -0
- package/dist/providers/opencode.d.ts +43 -0
- package/dist/providers/opencode.js +121 -0
- package/dist/providers/opencode.js.map +1 -0
- package/dist/providers/port.d.ts +1 -1
- package/dist/providers/registry.d.ts +2 -2
- package/dist/providers/registry.js +6 -2
- package/dist/providers/registry.js.map +1 -1
- package/package.json +2 -2
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/providers/opencode-parse.ts — stateful JSONL parser for `opencode run --format json`.
|
|
3
|
+
*
|
|
4
|
+
* STATEFUL MODULE: opencode streams assistant text via multiple `text` events;
|
|
5
|
+
* there is NO single terminal "done" line — the stdout stream simply ends after
|
|
6
|
+
* the last `text`/`step_finish` event. We therefore accumulate text, usage,
|
|
7
|
+
* and cost in a closure. The adapter calls `finalize()` after the for-await
|
|
8
|
+
* loop to obtain the final `done` event (or an error event on cancel/failure).
|
|
9
|
+
*
|
|
10
|
+
* Pure in all other respects: no I/O, no execa, no side effects. The factory
|
|
11
|
+
* function `createOpencodeParser()` returns a hermetic, fixture-testable object.
|
|
12
|
+
*
|
|
13
|
+
* opencode `opencode run --format json` event schema (JSONL, one object per line):
|
|
14
|
+
* - step_start → emit nothing (informational marker)
|
|
15
|
+
* - text (part.type=text) → text event + accumulate to buffer
|
|
16
|
+
* - tool_use → tool event (phase:'end', detail from state.title)
|
|
17
|
+
* - step_finish → usage event; accumulate tokens + cost
|
|
18
|
+
* - error → error event
|
|
19
|
+
* - unknown / malformed JSON → emit nothing (never throw)
|
|
20
|
+
*/
|
|
21
|
+
import { classifyError } from './errors.js';
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
// Helpers
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
function mapUsage(tokens) {
|
|
26
|
+
const base = {
|
|
27
|
+
inputTokens: tokens.input ?? 0,
|
|
28
|
+
outputTokens: tokens.output ?? 0,
|
|
29
|
+
};
|
|
30
|
+
// exactOptionalPropertyTypes: only include cachedInputTokens when it is a
|
|
31
|
+
// number — omit the key entirely otherwise.
|
|
32
|
+
const cacheRead = tokens.cache?.read;
|
|
33
|
+
if (typeof cacheRead === 'number') {
|
|
34
|
+
return { ...base, cachedInputTokens: cacheRead };
|
|
35
|
+
}
|
|
36
|
+
return base;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Create a stateful parser for `opencode run --format json` stdout.
|
|
40
|
+
*
|
|
41
|
+
* Returns an {@link OpencodeParser} whose `parseLine` method accepts one JSONL
|
|
42
|
+
* line at a time and returns 0 or more {@link ProviderEvent}s. The closure
|
|
43
|
+
* accumulates `text` content, token counts, and cost so that `finalize()` can
|
|
44
|
+
* return a complete `done` event after the stdout stream closes.
|
|
45
|
+
*
|
|
46
|
+
* The returned parser never throws — malformed JSON or unknown event types
|
|
47
|
+
* silently return [].
|
|
48
|
+
*/
|
|
49
|
+
export function createOpencodeParser() {
|
|
50
|
+
let accumulatedText = '';
|
|
51
|
+
let accumulatedInputTokens = 0;
|
|
52
|
+
let accumulatedOutputTokens = 0;
|
|
53
|
+
let accumulatedCachedInputTokens = undefined;
|
|
54
|
+
let accumulatedCostUsd = 0;
|
|
55
|
+
let terminalEmitted = false;
|
|
56
|
+
function parseLine(line) {
|
|
57
|
+
const trimmed = line.trim();
|
|
58
|
+
if (!trimmed)
|
|
59
|
+
return [];
|
|
60
|
+
let parsed;
|
|
61
|
+
try {
|
|
62
|
+
parsed = JSON.parse(trimmed);
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
return [];
|
|
66
|
+
}
|
|
67
|
+
if (typeof parsed !== 'object' || parsed === null)
|
|
68
|
+
return [];
|
|
69
|
+
const obj = parsed;
|
|
70
|
+
const eventType = obj['type'];
|
|
71
|
+
// -------------------------------------------------------------------------
|
|
72
|
+
// step_start — informational marker; emit nothing
|
|
73
|
+
// -------------------------------------------------------------------------
|
|
74
|
+
if (eventType === 'step_start') {
|
|
75
|
+
return [];
|
|
76
|
+
}
|
|
77
|
+
// -------------------------------------------------------------------------
|
|
78
|
+
// text — accumulate assistant text and emit a streaming delta
|
|
79
|
+
// -------------------------------------------------------------------------
|
|
80
|
+
if (eventType === 'text') {
|
|
81
|
+
const ev = parsed;
|
|
82
|
+
const part = ev.part;
|
|
83
|
+
if (typeof part !== 'object' || part === null)
|
|
84
|
+
return [];
|
|
85
|
+
if (part.type !== 'text')
|
|
86
|
+
return [];
|
|
87
|
+
const delta = typeof part.text === 'string' ? part.text : '';
|
|
88
|
+
if (delta.length > 0) {
|
|
89
|
+
accumulatedText += delta;
|
|
90
|
+
return [{ type: 'text', delta }];
|
|
91
|
+
}
|
|
92
|
+
return [];
|
|
93
|
+
}
|
|
94
|
+
// -------------------------------------------------------------------------
|
|
95
|
+
// tool_use — emit a tool event (phase:'end')
|
|
96
|
+
// -------------------------------------------------------------------------
|
|
97
|
+
if (eventType === 'tool_use') {
|
|
98
|
+
const ev = parsed;
|
|
99
|
+
const part = ev.part;
|
|
100
|
+
if (typeof part !== 'object' || part === null)
|
|
101
|
+
return [];
|
|
102
|
+
const toolName = typeof part.tool === 'string' && part.tool.length > 0
|
|
103
|
+
? part.tool
|
|
104
|
+
: 'unknown-tool';
|
|
105
|
+
const title = part.state?.title;
|
|
106
|
+
const toolEvent = typeof title === 'string' && title.length > 0
|
|
107
|
+
? { type: 'tool', name: toolName, phase: 'end', detail: title }
|
|
108
|
+
: { type: 'tool', name: toolName, phase: 'end' };
|
|
109
|
+
return [toolEvent];
|
|
110
|
+
}
|
|
111
|
+
// -------------------------------------------------------------------------
|
|
112
|
+
// step_finish — accumulate tokens + cost; emit a usage event
|
|
113
|
+
// -------------------------------------------------------------------------
|
|
114
|
+
if (eventType === 'step_finish') {
|
|
115
|
+
const ev = parsed;
|
|
116
|
+
const part = ev.part;
|
|
117
|
+
if (typeof part !== 'object' || part === null)
|
|
118
|
+
return [];
|
|
119
|
+
const tokens = part.tokens ?? {};
|
|
120
|
+
const stepUsage = mapUsage(tokens);
|
|
121
|
+
accumulatedInputTokens += stepUsage.inputTokens;
|
|
122
|
+
accumulatedOutputTokens += stepUsage.outputTokens;
|
|
123
|
+
if (typeof stepUsage.cachedInputTokens === 'number') {
|
|
124
|
+
accumulatedCachedInputTokens =
|
|
125
|
+
(accumulatedCachedInputTokens ?? 0) + stepUsage.cachedInputTokens;
|
|
126
|
+
}
|
|
127
|
+
if (typeof part.cost === 'number') {
|
|
128
|
+
accumulatedCostUsd += part.cost;
|
|
129
|
+
}
|
|
130
|
+
return [{ type: 'usage', usage: stepUsage }];
|
|
131
|
+
}
|
|
132
|
+
// -------------------------------------------------------------------------
|
|
133
|
+
// error — emit an error event and mark terminal as emitted
|
|
134
|
+
// -------------------------------------------------------------------------
|
|
135
|
+
if (eventType === 'error') {
|
|
136
|
+
const ev = parsed;
|
|
137
|
+
const message = ev.error?.data?.message ?? ev.error?.name ?? 'opencode error';
|
|
138
|
+
terminalEmitted = true;
|
|
139
|
+
return [{ type: 'error', error: classifyError(message, 1) }];
|
|
140
|
+
}
|
|
141
|
+
// Unknown event type — emit nothing
|
|
142
|
+
return [];
|
|
143
|
+
}
|
|
144
|
+
function finalize() {
|
|
145
|
+
// If a terminal event was already emitted (e.g. from an error line),
|
|
146
|
+
// do not emit a duplicate.
|
|
147
|
+
if (terminalEmitted)
|
|
148
|
+
return [];
|
|
149
|
+
// Build the accumulated usage object.
|
|
150
|
+
const usage = (() => {
|
|
151
|
+
const base = {
|
|
152
|
+
inputTokens: accumulatedInputTokens,
|
|
153
|
+
outputTokens: accumulatedOutputTokens,
|
|
154
|
+
};
|
|
155
|
+
if (typeof accumulatedCachedInputTokens === 'number') {
|
|
156
|
+
return { ...base, cachedInputTokens: accumulatedCachedInputTokens };
|
|
157
|
+
}
|
|
158
|
+
return base;
|
|
159
|
+
})();
|
|
160
|
+
// Build the done event. costUsd is only included when > 0 to avoid
|
|
161
|
+
// emitting a spurious $0 when opencode reported no cost (exactOptionalPropertyTypes).
|
|
162
|
+
const doneEvent = accumulatedCostUsd > 0
|
|
163
|
+
? {
|
|
164
|
+
type: 'done',
|
|
165
|
+
text: accumulatedText,
|
|
166
|
+
usage,
|
|
167
|
+
costUsd: accumulatedCostUsd,
|
|
168
|
+
raw: { accumulatedText, usage, costUsd: accumulatedCostUsd },
|
|
169
|
+
}
|
|
170
|
+
: {
|
|
171
|
+
type: 'done',
|
|
172
|
+
text: accumulatedText,
|
|
173
|
+
usage,
|
|
174
|
+
raw: { accumulatedText, usage },
|
|
175
|
+
};
|
|
176
|
+
terminalEmitted = true;
|
|
177
|
+
return [doneEvent];
|
|
178
|
+
}
|
|
179
|
+
return { parseLine, finalize };
|
|
180
|
+
}
|
|
181
|
+
//# sourceMappingURL=opencode-parse.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"opencode-parse.js","sourceRoot":"","sources":["../../src/providers/opencode-parse.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/providers/opencode.ts — opencode CLI adapter implementing the Provider port.
|
|
3
|
+
*
|
|
4
|
+
* Spawns `opencode run --format json -m <model>` via execa v9, delivers the
|
|
5
|
+
* prompt via STDIN (`input: req.prompt` — never as an argv argument), and
|
|
6
|
+
* streams parsed ProviderEvents to the caller as they arrive.
|
|
7
|
+
*
|
|
8
|
+
* Sandbox mapping:
|
|
9
|
+
* opencode has no sandbox flag analogous to `codex --sandbox`. We do NOT pass
|
|
10
|
+
* any permission-bypass flag. opencode's default behaviour is therefore used
|
|
11
|
+
* for all SandboxLevel values, which is conservative and appropriate.
|
|
12
|
+
*
|
|
13
|
+
* Authentication note:
|
|
14
|
+
* opencode ships free models (e.g. opencode/deepseek-v4-flash-free) that need
|
|
15
|
+
* no credentials. When installed, authenticated is always reported as true by
|
|
16
|
+
* detectProvider (see detect.ts). Premium providers require `opencode auth
|
|
17
|
+
* login -p <provider>`, but that is outside the scope of this adapter.
|
|
18
|
+
*
|
|
19
|
+
* Stream termination:
|
|
20
|
+
* opencode emits NO single terminal "done" line — the stdout stream simply ends
|
|
21
|
+
* after the last text/step_finish event. The adapter therefore calls
|
|
22
|
+
* `parser.finalize()` after the for-await loop to emit the accumulated `done`
|
|
23
|
+
* event, mirroring the pattern used in claude.ts for post-loop terminal events.
|
|
24
|
+
*
|
|
25
|
+
* costUsd note:
|
|
26
|
+
* Real cost is obtained by summing the `cost` field across all `step_finish`
|
|
27
|
+
* JSONL events (see opencode-parse.ts). This gives the ledger real numbers with
|
|
28
|
+
* no pricing-table dependency and accounts for any server-side discounts.
|
|
29
|
+
*
|
|
30
|
+
* Execa v9 streaming:
|
|
31
|
+
* We use `for await (const line of subprocess)` which iterates over stdout
|
|
32
|
+
* lines as strings. Confirmed from execa types: the subprocess IS an
|
|
33
|
+
* AsyncIterable<string>.
|
|
34
|
+
*/
|
|
35
|
+
import type { Provider } from './port.js';
|
|
36
|
+
/**
|
|
37
|
+
* Create an opencode provider adapter.
|
|
38
|
+
*
|
|
39
|
+
* @param opts.bin - Override the binary name/path (default: `'opencode'`).
|
|
40
|
+
*/
|
|
41
|
+
export declare function createOpencodeProvider(opts?: {
|
|
42
|
+
bin?: string;
|
|
43
|
+
}): Provider;
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/providers/opencode.ts — opencode CLI adapter implementing the Provider port.
|
|
3
|
+
*
|
|
4
|
+
* Spawns `opencode run --format json -m <model>` via execa v9, delivers the
|
|
5
|
+
* prompt via STDIN (`input: req.prompt` — never as an argv argument), and
|
|
6
|
+
* streams parsed ProviderEvents to the caller as they arrive.
|
|
7
|
+
*
|
|
8
|
+
* Sandbox mapping:
|
|
9
|
+
* opencode has no sandbox flag analogous to `codex --sandbox`. We do NOT pass
|
|
10
|
+
* any permission-bypass flag. opencode's default behaviour is therefore used
|
|
11
|
+
* for all SandboxLevel values, which is conservative and appropriate.
|
|
12
|
+
*
|
|
13
|
+
* Authentication note:
|
|
14
|
+
* opencode ships free models (e.g. opencode/deepseek-v4-flash-free) that need
|
|
15
|
+
* no credentials. When installed, authenticated is always reported as true by
|
|
16
|
+
* detectProvider (see detect.ts). Premium providers require `opencode auth
|
|
17
|
+
* login -p <provider>`, but that is outside the scope of this adapter.
|
|
18
|
+
*
|
|
19
|
+
* Stream termination:
|
|
20
|
+
* opencode emits NO single terminal "done" line — the stdout stream simply ends
|
|
21
|
+
* after the last text/step_finish event. The adapter therefore calls
|
|
22
|
+
* `parser.finalize()` after the for-await loop to emit the accumulated `done`
|
|
23
|
+
* event, mirroring the pattern used in claude.ts for post-loop terminal events.
|
|
24
|
+
*
|
|
25
|
+
* costUsd note:
|
|
26
|
+
* Real cost is obtained by summing the `cost` field across all `step_finish`
|
|
27
|
+
* JSONL events (see opencode-parse.ts). This gives the ledger real numbers with
|
|
28
|
+
* no pricing-table dependency and accounts for any server-side discounts.
|
|
29
|
+
*
|
|
30
|
+
* Execa v9 streaming:
|
|
31
|
+
* We use `for await (const line of subprocess)` which iterates over stdout
|
|
32
|
+
* lines as strings. Confirmed from execa types: the subprocess IS an
|
|
33
|
+
* AsyncIterable<string>.
|
|
34
|
+
*/
|
|
35
|
+
import { execa } from 'execa';
|
|
36
|
+
import { detectProvider } from './detect.js';
|
|
37
|
+
import { classifyError } from './errors.js';
|
|
38
|
+
import { createOpencodeParser } from './opencode-parse.js';
|
|
39
|
+
// ---------------------------------------------------------------------------
|
|
40
|
+
// Factory
|
|
41
|
+
// ---------------------------------------------------------------------------
|
|
42
|
+
/**
|
|
43
|
+
* Create an opencode provider adapter.
|
|
44
|
+
*
|
|
45
|
+
* @param opts.bin - Override the binary name/path (default: `'opencode'`).
|
|
46
|
+
*/
|
|
47
|
+
export function createOpencodeProvider(opts) {
|
|
48
|
+
const bin = opts?.bin ?? 'opencode';
|
|
49
|
+
return {
|
|
50
|
+
id: 'opencode',
|
|
51
|
+
detect() {
|
|
52
|
+
return detectProvider('opencode');
|
|
53
|
+
},
|
|
54
|
+
async *run(req, signal) {
|
|
55
|
+
const args = [
|
|
56
|
+
'run',
|
|
57
|
+
'--format',
|
|
58
|
+
'json',
|
|
59
|
+
'-m',
|
|
60
|
+
req.model,
|
|
61
|
+
];
|
|
62
|
+
// Spawn with reject:false so we always get the result object (never throws).
|
|
63
|
+
// cancelSignal wires our AbortSignal directly to execa's termination path.
|
|
64
|
+
// Prompt is delivered via STDIN (input:), never as an argv argument.
|
|
65
|
+
const subprocess = execa(bin, args, {
|
|
66
|
+
cwd: req.cwd,
|
|
67
|
+
input: req.prompt, // deliver prompt via STDIN, not argv
|
|
68
|
+
cancelSignal: signal,
|
|
69
|
+
timeout: req.timeoutMs,
|
|
70
|
+
reject: false,
|
|
71
|
+
});
|
|
72
|
+
// One parser instance per run — holds the accumulated text/usage/cost closure.
|
|
73
|
+
const parser = createOpencodeParser();
|
|
74
|
+
let emittedTerminal = false;
|
|
75
|
+
// Stream stdout line by line.
|
|
76
|
+
// execa v9: the subprocess itself is an AsyncIterable that yields one
|
|
77
|
+
// string per stdout line (from types/subprocess/subprocess.d.ts).
|
|
78
|
+
try {
|
|
79
|
+
for await (const line of subprocess) {
|
|
80
|
+
const events = parser.parseLine(line);
|
|
81
|
+
for (const ev of events) {
|
|
82
|
+
yield ev;
|
|
83
|
+
if (ev.type === 'done' || ev.type === 'error') {
|
|
84
|
+
emittedTerminal = true;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
// Iteration errors (e.g. stream abort) are handled below via the result.
|
|
91
|
+
}
|
|
92
|
+
// Wait for the subprocess to fully exit and collect the result.
|
|
93
|
+
const result = await subprocess;
|
|
94
|
+
if (!emittedTerminal) {
|
|
95
|
+
if (result.isCanceled) {
|
|
96
|
+
yield {
|
|
97
|
+
type: 'error',
|
|
98
|
+
error: classifyError('cancelled', 1),
|
|
99
|
+
};
|
|
100
|
+
emittedTerminal = true;
|
|
101
|
+
}
|
|
102
|
+
else if (result.failed || (result.exitCode !== undefined && result.exitCode !== 0)) {
|
|
103
|
+
yield {
|
|
104
|
+
type: 'error',
|
|
105
|
+
error: classifyError(typeof result.stderr === 'string' ? result.stderr : '', result.exitCode ?? 1),
|
|
106
|
+
};
|
|
107
|
+
emittedTerminal = true;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
// opencode has no terminal "done" line — emit the accumulated done event
|
|
111
|
+
// from the parser after the stream ends, unless a terminal was already emitted.
|
|
112
|
+
if (!emittedTerminal) {
|
|
113
|
+
const finalEvents = parser.finalize();
|
|
114
|
+
for (const ev of finalEvents) {
|
|
115
|
+
yield ev;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
},
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
//# sourceMappingURL=opencode.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"opencode.js","sourceRoot":"","sources":["../../src/providers/opencode.ts"],"names":[],"mappings":"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"}
|
package/dist/providers/port.d.ts
CHANGED
|
@@ -17,7 +17,7 @@ import type { ProviderStatus } from './detect.js';
|
|
|
17
17
|
export type { CliError } from './errors.js';
|
|
18
18
|
/** Privilege ladder mapped onto each CLI's sandbox flags. Default is workspace-write. */
|
|
19
19
|
export type SandboxLevel = 'read-only' | 'workspace-write' | 'full-access';
|
|
20
|
-
export type ProviderId = 'claude' | 'codex';
|
|
20
|
+
export type ProviderId = 'claude' | 'codex' | 'opencode';
|
|
21
21
|
export interface Usage {
|
|
22
22
|
readonly inputTokens: number;
|
|
23
23
|
readonly outputTokens: number;
|
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
* on `installed`, NOT `authenticated`, because auth is only truly known at
|
|
7
7
|
* call time and errors render honestly via classifyError().
|
|
8
8
|
*
|
|
9
|
-
* With
|
|
10
|
-
*
|
|
9
|
+
* With multiple providers installed, `deps.providers` will have entries for
|
|
10
|
+
* each — which automatically activates cross-vendor review in the orchestrator.
|
|
11
11
|
*/
|
|
12
12
|
import type { Provider, ProviderId } from './port.js';
|
|
13
13
|
/**
|
|
@@ -6,12 +6,13 @@
|
|
|
6
6
|
* on `installed`, NOT `authenticated`, because auth is only truly known at
|
|
7
7
|
* call time and errors render honestly via classifyError().
|
|
8
8
|
*
|
|
9
|
-
* With
|
|
10
|
-
*
|
|
9
|
+
* With multiple providers installed, `deps.providers` will have entries for
|
|
10
|
+
* each — which automatically activates cross-vendor review in the orchestrator.
|
|
11
11
|
*/
|
|
12
12
|
import { detectEnvironment } from './detect.js';
|
|
13
13
|
import { createClaudeProvider } from './claude.js';
|
|
14
14
|
import { createCodexProvider } from './codex.js';
|
|
15
|
+
import { createOpencodeProvider } from './opencode.js';
|
|
15
16
|
/**
|
|
16
17
|
* Discover which providers are available in the current environment and build
|
|
17
18
|
* the provider map for OrchestrateDeps.
|
|
@@ -29,6 +30,9 @@ export async function buildProviders(_cwd) {
|
|
|
29
30
|
if (env.codex.installed) {
|
|
30
31
|
providers.codex = createCodexProvider();
|
|
31
32
|
}
|
|
33
|
+
if (env.opencode.installed) {
|
|
34
|
+
providers.opencode = createOpencodeProvider();
|
|
35
|
+
}
|
|
32
36
|
return providers;
|
|
33
37
|
}
|
|
34
38
|
//# sourceMappingURL=registry.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"registry.js","sourceRoot":"","sources":["../../src/providers/registry.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAGH,OAAO,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAChD,OAAO,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AACnD,OAAO,EAAE,mBAAmB,EAAE,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"registry.js","sourceRoot":"","sources":["../../src/providers/registry.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAGH,OAAO,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAChD,OAAO,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AACnD,OAAO,EAAE,mBAAmB,EAAE,MAAM,YAAY,CAAC;AACjD,OAAO,EAAE,sBAAsB,EAAE,MAAM,eAAe,CAAC;AAEvD;;;;;;;GAOG;AACH,MAAM,CAAC,KAAK,UAAU,cAAc,CAClC,IAAY;IAEZ,MAAM,GAAG,GAAG,MAAM,iBAAiB,EAAE,CAAC;IACtC,MAAM,SAAS,GAA0C,EAAE,CAAC;IAE5D,IAAI,GAAG,CAAC,MAAM,CAAC,SAAS,EAAE,CAAC;QACzB,SAAS,CAAC,MAAM,GAAG,oBAAoB,EAAE,CAAC;IAC5C,CAAC;IAED,IAAI,GAAG,CAAC,KAAK,CAAC,SAAS,EAAE,CAAC;QACxB,SAAS,CAAC,KAAK,GAAG,mBAAmB,EAAE,CAAC;IAC1C,CAAC;IAED,IAAI,GAAG,CAAC,QAAQ,CAAC,SAAS,EAAE,CAAC;QAC3B,SAAS,CAAC,QAAQ,GAAG,sBAAsB,EAAE,CAAC;IAChD,CAAC;IAED,OAAO,SAAS,CAAC;AACnB,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "myshell-tools",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.7.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"bin": {
|
|
6
6
|
"myshell-tools": "dist/cli.js",
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
"knip": "knip",
|
|
21
21
|
"test:integration": "node --test \"test/integration/**/*.test.ts\"",
|
|
22
22
|
"clean": "node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\"",
|
|
23
|
-
"prepublishOnly": "npm run clean && npm run
|
|
23
|
+
"prepublishOnly": "npm run clean && npm run typecheck && npm run lint && npm run build"
|
|
24
24
|
},
|
|
25
25
|
"files": [
|
|
26
26
|
"dist/",
|