@opencode-cockpit/trust 0.0.0-stage → 0.8.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 +241 -2
- package/dist/cli/preview.js +251 -0
- package/dist/core/adapt/seen.js +24 -0
- package/dist/core/adapt/v1.js +109 -0
- package/dist/core/adapt/v2.js +109 -0
- package/dist/core/config.js +93 -0
- package/dist/core/danger.js +366 -0
- package/dist/core/engine.js +245 -0
- package/dist/core/family.js +512 -0
- package/dist/core/history.js +169 -0
- package/dist/core/index.js +16 -0
- package/dist/core/keys.js +149 -0
- package/dist/core/ledger.js +202 -0
- package/dist/core/paths.js +38 -0
- package/dist/core/policy.js +128 -0
- package/dist/core/rules.js +152 -0
- package/dist/core/sample.js +311 -0
- package/dist/core/shell.js +253 -0
- package/dist/core/signature.js +38 -0
- package/dist/core/view/actions.js +321 -0
- package/dist/core/view/activity.js +557 -0
- package/dist/core/view/explorer.js +970 -0
- package/dist/core/view/model.js +193 -0
- package/dist/core/view/parts.js +290 -0
- package/dist/core/view/rows.js +165 -0
- package/dist/core/view/sidebar.js +170 -0
- package/dist/tui/index.js +920 -0
- package/dist/tui/journal.js +80 -0
- package/dist/tui/render.js +85 -0
- package/dist/tui/source.js +112 -0
- package/dist/tui/view/dialog.js +43 -0
- package/dist/tui/view/rows.js +69 -0
- package/package.json +61 -4
- package/tui.js +6 -0
- package/types/cli/preview.d.ts +22 -0
- package/types/core/adapt/seen.d.ts +46 -0
- package/types/core/adapt/v1.d.ts +18 -0
- package/types/core/adapt/v2.d.ts +20 -0
- package/types/core/config.d.ts +57 -0
- package/types/core/danger.d.ts +50 -0
- package/types/core/engine.d.ts +117 -0
- package/types/core/family.d.ts +97 -0
- package/types/core/history.d.ts +81 -0
- package/types/core/index.d.ts +16 -0
- package/types/core/keys.d.ts +68 -0
- package/types/core/ledger.d.ts +158 -0
- package/types/core/paths.d.ts +24 -0
- package/types/core/policy.d.ts +52 -0
- package/types/core/rules.d.ts +55 -0
- package/types/core/sample.d.ts +22 -0
- package/types/core/shell.d.ts +40 -0
- package/types/core/signature.d.ts +18 -0
- package/types/core/view/actions.d.ts +76 -0
- package/types/core/view/activity.d.ts +101 -0
- package/types/core/view/explorer.d.ts +154 -0
- package/types/core/view/model.d.ts +106 -0
- package/types/core/view/parts.d.ts +76 -0
- package/types/core/view/rows.d.ts +60 -0
- package/types/core/view/sidebar.d.ts +62 -0
- package/types/tui/index.d.ts +15 -0
- package/types/tui/journal.d.ts +19 -0
- package/types/tui/render.d.ts +14 -0
- package/types/tui/source.d.ts +35 -0
- package/types/tui/view/dialog.d.ts +24 -0
- package/types/tui/view/rows.d.ts +19 -0
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The ledger file, read and appended without ever blocking the interface thread.
|
|
3
|
+
*
|
|
4
|
+
* Every read and every append goes through one chain, so this window's writes and reads never
|
|
5
|
+
* overlap each other (the lesson of Review's trace: unserialised appends dropped lines). Other
|
|
6
|
+
* windows append to the same file between our reads; `read` takes whatever whole lines arrived since
|
|
7
|
+
* the last one and leaves a line still being written for next time.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { appendFile, mkdir, open } from "node:fs/promises";
|
|
11
|
+
import { parseLines, serialize } from "../core/ledger.js";
|
|
12
|
+
export function createJournal(paths) {
|
|
13
|
+
let offset = 0;
|
|
14
|
+
let chain = Promise.resolve();
|
|
15
|
+
let made = false;
|
|
16
|
+
const decoder = new TextDecoder();
|
|
17
|
+
const queue = task => {
|
|
18
|
+
const run = chain.then(task, task);
|
|
19
|
+
chain = run.catch(() => {});
|
|
20
|
+
return run;
|
|
21
|
+
};
|
|
22
|
+
const read = async () => {
|
|
23
|
+
let handle;
|
|
24
|
+
try {
|
|
25
|
+
handle = await open(paths.events, "r");
|
|
26
|
+
} catch (error) {
|
|
27
|
+
if (error.code === "ENOENT") return {
|
|
28
|
+
events: [],
|
|
29
|
+
reset: false
|
|
30
|
+
};
|
|
31
|
+
throw error;
|
|
32
|
+
}
|
|
33
|
+
try {
|
|
34
|
+
const {
|
|
35
|
+
size
|
|
36
|
+
} = await handle.stat();
|
|
37
|
+
/** Smaller than what we read: someone replaced the file. Start again from its first line. */
|
|
38
|
+
const reset = size < offset;
|
|
39
|
+
if (reset) offset = 0;
|
|
40
|
+
if (size === offset) return {
|
|
41
|
+
events: [],
|
|
42
|
+
reset
|
|
43
|
+
};
|
|
44
|
+
const buffer = new Uint8Array(size - offset);
|
|
45
|
+
const {
|
|
46
|
+
bytesRead
|
|
47
|
+
} = await handle.read(buffer, 0, buffer.length, offset);
|
|
48
|
+
const bytes = buffer.subarray(0, bytesRead);
|
|
49
|
+
const end = bytes.lastIndexOf(10); // the last newline: what follows is still being written
|
|
50
|
+
if (end < 0) return {
|
|
51
|
+
events: [],
|
|
52
|
+
reset
|
|
53
|
+
};
|
|
54
|
+
const {
|
|
55
|
+
events
|
|
56
|
+
} = parseLines(decoder.decode(bytes.subarray(0, end + 1)));
|
|
57
|
+
offset += end + 1;
|
|
58
|
+
return {
|
|
59
|
+
events,
|
|
60
|
+
reset
|
|
61
|
+
};
|
|
62
|
+
} finally {
|
|
63
|
+
await handle.close();
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
return {
|
|
67
|
+
read: () => queue(read),
|
|
68
|
+
append: events => queue(async () => {
|
|
69
|
+
if (events.length === 0) return;
|
|
70
|
+
if (!made) {
|
|
71
|
+
await mkdir(paths.dir, {
|
|
72
|
+
recursive: true
|
|
73
|
+
});
|
|
74
|
+
made = true;
|
|
75
|
+
}
|
|
76
|
+
/** One write for the batch: whole lines, appended, never interleaved with another window's. */
|
|
77
|
+
await appendFile(paths.events, events.map(serialize).join(""));
|
|
78
|
+
})
|
|
79
|
+
};
|
|
80
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The only place that knows what a tone looks like: every colour from the user's OpenCode theme,
|
|
3
|
+
* through the host (so OpenCode 2's tokens arrive under the same names — client/host `themeFromV2`).
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { TINTS } from "../core/view/rows.js";
|
|
7
|
+
const opaque = colour => colour !== undefined && colour.a > 0;
|
|
8
|
+
|
|
9
|
+
/** What the dialog is drawn on: the panel colour, or the background when a theme leaves the panel clear. */
|
|
10
|
+
const surface = theme => opaque(theme.backgroundPanel) ? theme.backgroundPanel : opaque(theme.background) ? theme.background : undefined;
|
|
11
|
+
export function toneColour(theme, tone) {
|
|
12
|
+
switch (tone) {
|
|
13
|
+
case "muted":
|
|
14
|
+
return theme.textMuted;
|
|
15
|
+
case "accent":
|
|
16
|
+
return theme.accent;
|
|
17
|
+
case "info":
|
|
18
|
+
return theme.info;
|
|
19
|
+
case "tool":
|
|
20
|
+
return theme.primary;
|
|
21
|
+
case "success":
|
|
22
|
+
return theme.success;
|
|
23
|
+
case "error":
|
|
24
|
+
return theme.error;
|
|
25
|
+
case "warning":
|
|
26
|
+
return theme.warning;
|
|
27
|
+
case "border":
|
|
28
|
+
return theme.border;
|
|
29
|
+
/** Text cut out of a solid accent: the surface's own colour, or the text's when there is none. */
|
|
30
|
+
case "ink":
|
|
31
|
+
return surface(theme) ?? theme.text;
|
|
32
|
+
default:
|
|
33
|
+
return theme.text;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
const mixed = new WeakMap();
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* `tone` faded into `behind`, `amount` of the way from it: a tint that follows the theme. Built from
|
|
40
|
+
* the colour's own class, as Review's soften does, so nothing of OpenTUI is imported.
|
|
41
|
+
*/
|
|
42
|
+
function mix(tone, behind, amount) {
|
|
43
|
+
const byBack = mixed.get(tone) ?? new WeakMap();
|
|
44
|
+
mixed.set(tone, byBack);
|
|
45
|
+
const byAmount = byBack.get(behind) ?? new Map();
|
|
46
|
+
byBack.set(behind, byAmount);
|
|
47
|
+
const known = byAmount.get(amount);
|
|
48
|
+
if (known) return known;
|
|
49
|
+
const Colour = tone.constructor;
|
|
50
|
+
if (typeof Colour.clone !== "function" || typeof behind.r !== "number") return undefined;
|
|
51
|
+
const out = Colour.clone(behind);
|
|
52
|
+
out.r = behind.r + (tone.r - behind.r) * amount;
|
|
53
|
+
out.g = behind.g + (tone.g - behind.g) * amount;
|
|
54
|
+
out.b = behind.b + (tone.b - behind.b) * amount;
|
|
55
|
+
byAmount.set(amount, out);
|
|
56
|
+
return out;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* What sits behind a run. The row-wide surfaces and the buttons are the theme's raised element; a
|
|
61
|
+
* focused button is the accent; a chip or a badge is its tone's tint, and — on a theme whose dialog
|
|
62
|
+
* has no colour to tint — no fill at all: the word keeps its tone and still says what it is.
|
|
63
|
+
*/
|
|
64
|
+
export function fillColour(theme, fill) {
|
|
65
|
+
switch (fill) {
|
|
66
|
+
case "selected":
|
|
67
|
+
case "panel":
|
|
68
|
+
case "button":
|
|
69
|
+
return theme.backgroundElement;
|
|
70
|
+
case "buttonOn":
|
|
71
|
+
return theme.accent;
|
|
72
|
+
case "chip":
|
|
73
|
+
case "ok":
|
|
74
|
+
case "warn":
|
|
75
|
+
case "err":
|
|
76
|
+
{
|
|
77
|
+
const behind = surface(theme);
|
|
78
|
+
if (!behind) return undefined;
|
|
79
|
+
const tint = TINTS[fill];
|
|
80
|
+
return mix(toneColour(theme, tint.tone), behind, tint.amount);
|
|
81
|
+
}
|
|
82
|
+
default:
|
|
83
|
+
return undefined;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Trust's reach into a specific OpenCode: its events, its reply, its config and its pending list.
|
|
3
|
+
* The only file past `Host` — through `api.v1` / `api.v2`, as Subagents' source does — and every call
|
|
4
|
+
* was measured or read from the version's own client (docs/opencode/permissions.md):
|
|
5
|
+
*
|
|
6
|
+
* | | OpenCode 1 | OpenCode 2 |
|
|
7
|
+
* | --- | --- | --- |
|
|
8
|
+
* | events | `api.event.on(type)` | `ctx.data.listen` |
|
|
9
|
+
* | answer | `client.permission.reply({ requestID, reply })` | `client.permission.reply({ sessionID, requestID, decision })` |
|
|
10
|
+
* | config | `client.config.get()` — merged | `client.config.get({ location })` — documents, lowest first |
|
|
11
|
+
* | pending | `client.permission.list()` | not read (see `pending` below) |
|
|
12
|
+
*
|
|
13
|
+
* What it hears becomes `Seen` through the pure adapters in `core/adapt/`.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { commandOf, obj, str } from "../core/adapt/seen.js";
|
|
17
|
+
import { fromV1Event, fromV1Pending, V1_EVENTS } from "../core/adapt/v1.js";
|
|
18
|
+
import { fromV2Event } from "../core/adapt/v2.js";
|
|
19
|
+
|
|
20
|
+
// biome-ignore lint/suspicious/noExplicitAny: the host's objects, reached by measured shapes only
|
|
21
|
+
|
|
22
|
+
/** A client result: the plugin's client hands back `{ data }`, some calls the data itself. */
|
|
23
|
+
const unwrap = result => {
|
|
24
|
+
const value = obj(result);
|
|
25
|
+
if (value.error) throw new Error(str(obj(value.error).message) ?? JSON.stringify(value.error));
|
|
26
|
+
return "data" in value ? value.data : result;
|
|
27
|
+
};
|
|
28
|
+
export function createSource(api, log, emit) {
|
|
29
|
+
const offs = [];
|
|
30
|
+
const guard = fn => {
|
|
31
|
+
try {
|
|
32
|
+
fn();
|
|
33
|
+
} catch (error) {
|
|
34
|
+
log.error("event failed", {
|
|
35
|
+
error
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
if (api.v1) {
|
|
40
|
+
const v1 = api.v1;
|
|
41
|
+
for (const type of V1_EVENTS) {
|
|
42
|
+
const off = v1.event.on(type, event => guard(() => emit(fromV1Event(event))));
|
|
43
|
+
if (typeof off === "function") offs.push(off);
|
|
44
|
+
}
|
|
45
|
+
return {
|
|
46
|
+
async approve(request) {
|
|
47
|
+
unwrap(await v1.client.permission.reply({
|
|
48
|
+
requestID: request.id,
|
|
49
|
+
reply: "once"
|
|
50
|
+
}));
|
|
51
|
+
},
|
|
52
|
+
async config() {
|
|
53
|
+
return unwrap(await v1.client.config.get());
|
|
54
|
+
},
|
|
55
|
+
async pending() {
|
|
56
|
+
return fromV1Pending(unwrap(await v1.client.permission.list()));
|
|
57
|
+
},
|
|
58
|
+
call(request) {
|
|
59
|
+
if (!request.messageID || !request.call) return undefined;
|
|
60
|
+
const parts = v1.state.part?.(request.messageID) ?? [];
|
|
61
|
+
const part = parts.find(each => each?.callID === request.call);
|
|
62
|
+
return part ? commandOf(obj(obj(part.state).input)) : undefined;
|
|
63
|
+
},
|
|
64
|
+
agent(sessionID, messageID) {
|
|
65
|
+
const messages = v1.state.session?.messages?.(sessionID) ?? [];
|
|
66
|
+
const info = message => obj(message?.info ?? message);
|
|
67
|
+
const own = messageID ? messages.find(message => info(message).id === messageID) : undefined;
|
|
68
|
+
const found = [own, ...[...messages].reverse()].find(message => message && str(info(message).agent));
|
|
69
|
+
return found ? str(info(found).agent) : undefined;
|
|
70
|
+
},
|
|
71
|
+
dispose: () => {
|
|
72
|
+
for (const off of offs.splice(0)) off();
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
const v2 = api.v2;
|
|
77
|
+
const location = () => ({
|
|
78
|
+
directory: api.state.path.directory
|
|
79
|
+
});
|
|
80
|
+
const off = v2.data.listen(event => guard(() => emit(fromV2Event(event))));
|
|
81
|
+
if (typeof off === "function") offs.push(off);
|
|
82
|
+
return {
|
|
83
|
+
async approve(request) {
|
|
84
|
+
unwrap(await v2.client.permission.reply({
|
|
85
|
+
sessionID: request.sessionID,
|
|
86
|
+
requestID: request.id,
|
|
87
|
+
decision: "once"
|
|
88
|
+
}));
|
|
89
|
+
},
|
|
90
|
+
async config() {
|
|
91
|
+
return unwrap(await v2.client.config.get({
|
|
92
|
+
location: location()
|
|
93
|
+
}));
|
|
94
|
+
},
|
|
95
|
+
/**
|
|
96
|
+
* Not read on OpenCode 2. Its `permission.request.list({ location })` exists, but a real 2.0.18 run
|
|
97
|
+
* listed nothing for a request that was on screen, and an empty list read as "all answered" would
|
|
98
|
+
* forget the person's pending request — whose approval would then not count. Replies arrive as
|
|
99
|
+
* events; that is enough.
|
|
100
|
+
*/
|
|
101
|
+
async pending() {
|
|
102
|
+
return undefined;
|
|
103
|
+
},
|
|
104
|
+
call: () => undefined,
|
|
105
|
+
agent(sessionID) {
|
|
106
|
+
return str(obj(v2.data.session?.get?.(sessionID)).agent);
|
|
107
|
+
},
|
|
108
|
+
dispose: () => {
|
|
109
|
+
for (const each of offs.splice(0)) each();
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { insert as _$insert } from "@opentui/solid";
|
|
2
|
+
import { createComponent as _$createComponent } from "@opentui/solid";
|
|
3
|
+
import { setProp as _$setProp } from "@opentui/solid";
|
|
4
|
+
import { createElement as _$createElement } from "@opentui/solid";
|
|
5
|
+
/** @jsxImportSource @opentui/solid */
|
|
6
|
+
// biome-ignore-all lint/a11y/noStaticElementInteractions: these are terminal boxes, not DOM elements
|
|
7
|
+
|
|
8
|
+
import { Rows } from "./rows.js";
|
|
9
|
+
/**
|
|
10
|
+
* Trust's dialog in the host's: rows from `core/view/` — the activity or the ledger — and the keys and
|
|
11
|
+
* clicks that act on them. Clicks are read on release: on press, anything a click opens is up in time
|
|
12
|
+
* to catch the release and take it for its own (Review's overlay learned this).
|
|
13
|
+
*/
|
|
14
|
+
export function Dialog(props) {
|
|
15
|
+
props.api.keymap.useLayer(props.keys);
|
|
16
|
+
let rows;
|
|
17
|
+
return (() => {
|
|
18
|
+
var _el$ = _$createElement("box");
|
|
19
|
+
_$setProp(_el$, "onMouse", event => {
|
|
20
|
+
const scroll = event.scroll;
|
|
21
|
+
if (!scroll) return;
|
|
22
|
+
event.stopPropagation();
|
|
23
|
+
props.onScroll(scroll.direction === "up" ? -3 : 3);
|
|
24
|
+
});
|
|
25
|
+
_$setProp(_el$, "onMouseUp", event => {
|
|
26
|
+
if (!rows) return;
|
|
27
|
+
event.stopPropagation();
|
|
28
|
+
props.onClick(event.x - rows.x, event.y - rows.y);
|
|
29
|
+
});
|
|
30
|
+
_$insert(_el$, _$createComponent(Rows, {
|
|
31
|
+
get api() {
|
|
32
|
+
return props.api;
|
|
33
|
+
},
|
|
34
|
+
get rows() {
|
|
35
|
+
return props.rows;
|
|
36
|
+
},
|
|
37
|
+
onReady: box => {
|
|
38
|
+
rows = box;
|
|
39
|
+
}
|
|
40
|
+
}));
|
|
41
|
+
return _el$;
|
|
42
|
+
})();
|
|
43
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { insertNode as _$insertNode } from "@opentui/solid";
|
|
2
|
+
import { insert as _$insert } from "@opentui/solid";
|
|
3
|
+
import { createComponent as _$createComponent } from "@opentui/solid";
|
|
4
|
+
import { use as _$use } from "@opentui/solid";
|
|
5
|
+
import { setProp as _$setProp } from "@opentui/solid";
|
|
6
|
+
import { createElement as _$createElement } from "@opentui/solid";
|
|
7
|
+
/** @jsxImportSource @opentui/solid */
|
|
8
|
+
|
|
9
|
+
import { For } from "solid-js";
|
|
10
|
+
import { fillColour, toneColour } from "../render.js";
|
|
11
|
+
/**
|
|
12
|
+
* Rows of runs, one `<text>` per row — the drawing both the sidebar block and the ledger dialog use.
|
|
13
|
+
*
|
|
14
|
+
* Shell's and Subagents' pattern, proven live on both OpenCodes: a signal read by `<For>` redraws,
|
|
15
|
+
* where anything decided once in a slot's tree never would (docs/opencode/gotchas.md, "Slots").
|
|
16
|
+
* The shape never changes; only the list of rows does.
|
|
17
|
+
*/
|
|
18
|
+
export function Rows(props) {
|
|
19
|
+
const theme = () => props.api.theme.current;
|
|
20
|
+
return (() => {
|
|
21
|
+
var _el$ = _$createElement("box");
|
|
22
|
+
_$use(box => props.onReady?.(box), _el$);
|
|
23
|
+
_$setProp(_el$, "flexDirection", "column");
|
|
24
|
+
_$insert(_el$, _$createComponent(For, {
|
|
25
|
+
get each() {
|
|
26
|
+
return props.rows();
|
|
27
|
+
},
|
|
28
|
+
children: row => (() => {
|
|
29
|
+
var _el$2 = _$createElement("text");
|
|
30
|
+
_$setProp(_el$2, "wrapMode", "none");
|
|
31
|
+
_$setProp(_el$2, "flexShrink", 0);
|
|
32
|
+
_$insert(_el$2, _$createComponent(For, {
|
|
33
|
+
each: row,
|
|
34
|
+
children: run => {
|
|
35
|
+
const fill = fillColour(theme(), run.fill);
|
|
36
|
+
const style = {
|
|
37
|
+
fg: toneColour(theme(), run.tone),
|
|
38
|
+
...(fill ? {
|
|
39
|
+
bg: fill
|
|
40
|
+
} : {})
|
|
41
|
+
};
|
|
42
|
+
return run.bold ? (() => {
|
|
43
|
+
var _el$3 = _$createElement("span"),
|
|
44
|
+
_el$4 = _$createElement("b");
|
|
45
|
+
_$insertNode(_el$3, _el$4);
|
|
46
|
+
_$setProp(_el$3, "style", style);
|
|
47
|
+
_$insert(_el$4, () => run.text);
|
|
48
|
+
return _el$3;
|
|
49
|
+
})() : run.faint ? (() => {
|
|
50
|
+
var _el$5 = _$createElement("span"),
|
|
51
|
+
_el$6 = _$createElement("i");
|
|
52
|
+
_$insertNode(_el$5, _el$6);
|
|
53
|
+
_$setProp(_el$5, "style", style);
|
|
54
|
+
_$insert(_el$6, () => run.text);
|
|
55
|
+
return _el$5;
|
|
56
|
+
})() : (() => {
|
|
57
|
+
var _el$7 = _$createElement("span");
|
|
58
|
+
_$setProp(_el$7, "style", style);
|
|
59
|
+
_$insert(_el$7, () => run.text);
|
|
60
|
+
return _el$7;
|
|
61
|
+
})();
|
|
62
|
+
}
|
|
63
|
+
}));
|
|
64
|
+
return _el$2;
|
|
65
|
+
})()
|
|
66
|
+
}));
|
|
67
|
+
return _el$;
|
|
68
|
+
})();
|
|
69
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,63 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@opencode-cockpit/trust",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.8.0",
|
|
4
|
+
"description": "Permissions that learn: approve the same command enough times and it approves itself, visibly",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"author": "Codestz",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/Codestz/opencode-cockpit.git",
|
|
11
|
+
"directory": "packages/trust"
|
|
12
|
+
},
|
|
13
|
+
"homepage": "https://github.com/Codestz/opencode-cockpit#readme",
|
|
14
|
+
"bugs": {
|
|
15
|
+
"url": "https://github.com/Codestz/opencode-cockpit/issues"
|
|
16
|
+
},
|
|
17
|
+
"keywords": [
|
|
18
|
+
"opencode",
|
|
19
|
+
"opencode-plugin",
|
|
20
|
+
"permissions",
|
|
21
|
+
"trust",
|
|
22
|
+
"tui",
|
|
23
|
+
"terminal"
|
|
24
|
+
],
|
|
25
|
+
"exports": {
|
|
26
|
+
"./tui": {
|
|
27
|
+
"types": "./types/tui/index.d.ts",
|
|
28
|
+
"default": "./dist/tui/index.js"
|
|
29
|
+
},
|
|
30
|
+
"./core": {
|
|
31
|
+
"types": "./types/core/index.d.ts",
|
|
32
|
+
"default": "./dist/core/index.js"
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
"engines": {
|
|
36
|
+
"opencode": ">=1.18.0",
|
|
37
|
+
"bun": ">=1.3.5"
|
|
38
|
+
},
|
|
39
|
+
"files": [
|
|
40
|
+
"tui.js",
|
|
41
|
+
"dist",
|
|
42
|
+
"types",
|
|
43
|
+
"README.md",
|
|
44
|
+
"LICENSE"
|
|
45
|
+
],
|
|
46
|
+
"publishConfig": {
|
|
47
|
+
"access": "public"
|
|
48
|
+
},
|
|
49
|
+
"dependencies": {
|
|
50
|
+
"@opencode-cockpit/client": "0.8.0",
|
|
51
|
+
"@opencode-ai/plugin": "1.18.31"
|
|
52
|
+
},
|
|
53
|
+
"devDependencies": {
|
|
54
|
+
"@opentui/core": "0.4.5",
|
|
55
|
+
"@opentui/keymap": "0.4.5",
|
|
56
|
+
"@opentui/solid": "0.4.5",
|
|
57
|
+
"solid-js": "1.9.12"
|
|
58
|
+
},
|
|
59
|
+
"bin": {
|
|
60
|
+
"opencode-trust": "./dist/cli/preview.js",
|
|
61
|
+
"trust": "./dist/cli/preview.js"
|
|
62
|
+
}
|
|
63
|
+
}
|
package/tui.js
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenCode 2 finds a plugin configured by *path* by the files at its root — `<package>/tui`,
|
|
3
|
+
* `<package>/server` — rather than through `exports` (docs/opencode/v2.md). A package installed by
|
|
4
|
+
* name resolves through `exports` as before; this file is only the door for the path case.
|
|
5
|
+
*/
|
|
6
|
+
export { default } from "./dist/tui/index.js"
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* The see-it loop: the sidebar block and both Trust screens — the activity `/trust` opens on and the
|
|
4
|
+
* ledger behind `l` — drawn in this terminal from sample worlds, with no OpenCode running. The same
|
|
5
|
+
* rows OpenCode draws; only the colours come from a fixed palette (OpenCode's default theme) instead
|
|
6
|
+
* of the user's.
|
|
7
|
+
*
|
|
8
|
+
* bunx @opencode-cockpit/trust preview every sample, both screens
|
|
9
|
+
* bunx @opencode-cockpit/trust preview --sample busy one of them
|
|
10
|
+
* bunx @opencode-cockpit/trust preview --view activity only the screen /trust opens on
|
|
11
|
+
* bunx @opencode-cockpit/trust preview --view ledger only the ledger (the explorer and its card)
|
|
12
|
+
* bunx @opencode-cockpit/trust preview --columns 80 the dialog at another width
|
|
13
|
+
* bunx @opencode-cockpit/trust preview --rows 20 the dialog in a short window
|
|
14
|
+
* bunx @opencode-cockpit/trust preview --width 30 the sidebar at another width
|
|
15
|
+
* bunx @opencode-cockpit/trust preview --keys the key lists (`?`) too
|
|
16
|
+
* bunx @opencode-cockpit/trust preview --html > a.html the same, as a page, to judge colour in a browser
|
|
17
|
+
*
|
|
18
|
+
* The ledger is drawn more than once a sample: as it opens, then with the cursor on a trusted command
|
|
19
|
+
* in its open family, on a family you widened, on one still counting, on a dangerous command, on
|
|
20
|
+
* OpenCode's own "always", and with `tab` into the card's buttons.
|
|
21
|
+
*/
|
|
22
|
+
export {};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What Trust takes from the host's events, whichever OpenCode sent them. `v1.ts` and `v2.ts` turn
|
|
3
|
+
* their version's events into these, and nothing past them knows which version it is.
|
|
4
|
+
*/
|
|
5
|
+
import type { Reply } from "../engine.ts";
|
|
6
|
+
import type { Request } from "../keys.ts";
|
|
7
|
+
export type Seen = {
|
|
8
|
+
type: "asked";
|
|
9
|
+
request: Request;
|
|
10
|
+
} | {
|
|
11
|
+
type: "replied";
|
|
12
|
+
sessionID: string;
|
|
13
|
+
requestID: string;
|
|
14
|
+
reply: Reply;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* A tool call's arguments. A bash request's patterns lose the line's `cd`s (OpenCode leaves `cd`
|
|
18
|
+
* out of them), so the command line Trust judges is the call's own `command`, kept from here.
|
|
19
|
+
*/
|
|
20
|
+
| {
|
|
21
|
+
type: "call";
|
|
22
|
+
sessionID: string;
|
|
23
|
+
call: string;
|
|
24
|
+
messageID?: string;
|
|
25
|
+
input: Record<string, unknown>;
|
|
26
|
+
}
|
|
27
|
+
/** The agent a session runs as: part of every key, and not on the request itself. */
|
|
28
|
+
| {
|
|
29
|
+
type: "agent";
|
|
30
|
+
sessionID: string;
|
|
31
|
+
agent: string;
|
|
32
|
+
}
|
|
33
|
+
/** OpenCode's config changed: the rules have to be read again. */
|
|
34
|
+
| {
|
|
35
|
+
type: "config";
|
|
36
|
+
};
|
|
37
|
+
export type Json = Record<string, unknown>;
|
|
38
|
+
export declare const obj: (value: unknown) => Json;
|
|
39
|
+
export declare const str: (value: unknown) => string | undefined;
|
|
40
|
+
export declare const strings: (value: unknown) => string[];
|
|
41
|
+
export declare const isReply: (value: unknown) => value is Reply;
|
|
42
|
+
/** The command line and working directory from a shell call's arguments (v1 `workdir`, v2 `cwd`). */
|
|
43
|
+
export declare function commandOf(input: Json | undefined): {
|
|
44
|
+
line?: string;
|
|
45
|
+
workdir?: string;
|
|
46
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenCode 1's events, as what Trust takes from them.
|
|
3
|
+
*
|
|
4
|
+
* `api.event.on(type)` hands over `{ type, properties }`. Shapes measured on 1.18.32
|
|
5
|
+
* (docs/opencode/permissions.md, and Subagents' recorded run for the message events):
|
|
6
|
+
*
|
|
7
|
+
* permission.asked { id, sessionID, permission: "bash", patterns, always, metadata,
|
|
8
|
+
* tool: { messageID, callID } }
|
|
9
|
+
* permission.replied { sessionID, requestID, reply: "once" | "always" | "reject" }
|
|
10
|
+
* message.part.updated { part: { type: "tool", callID, sessionID, messageID, state: { input } } }
|
|
11
|
+
* message.updated { info: { sessionID, agent } }
|
|
12
|
+
* session.created/updated { info: { id, agent } } — a subagent's session names its agent
|
|
13
|
+
*/
|
|
14
|
+
import { type Seen } from "./seen.ts";
|
|
15
|
+
export declare const V1_EVENTS: readonly ["permission.asked", "permission.replied", "message.part.updated", "message.updated", "session.created", "session.updated"];
|
|
16
|
+
export declare function fromV1Event(raw: unknown): Seen[];
|
|
17
|
+
/** A pending request as `client.permission.list()` returns it: the same shape as the event. */
|
|
18
|
+
export declare function fromV1Pending(list: unknown): Seen[];
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenCode 2's events, as what Trust takes from them.
|
|
3
|
+
*
|
|
4
|
+
* `ctx.data.listen` hands over `{ name, details: { data } }`. Shapes measured on 2.0.18
|
|
5
|
+
* (docs/opencode/permissions.md) and read from its own schema (`Permission.Request`):
|
|
6
|
+
*
|
|
7
|
+
* permission.asked { id, sessionID, action: "shell", resources, save, metadata, message,
|
|
8
|
+
* source: { type: "tool", messageID, id } }
|
|
9
|
+
* permission.replied { sessionID, requestID, reply }
|
|
10
|
+
* session.tool.called { sessionID, assistantMessageID, id, input } — a shell call's `command`, `cwd`
|
|
11
|
+
* session.created { sessionID, agent }
|
|
12
|
+
* session.agent.selected { sessionID, agent }
|
|
13
|
+
* config.updated {}
|
|
14
|
+
*
|
|
15
|
+
* The request carries no agent (v2's schema has none), so it comes from the session's events.
|
|
16
|
+
*/
|
|
17
|
+
import { type Seen } from "./seen.ts";
|
|
18
|
+
export declare function fromV2Event(raw: unknown): Seen[];
|
|
19
|
+
/** A pending request from a list, in the same shape as the event's data. */
|
|
20
|
+
export declare function fromV2Pending(list: unknown): Seen[];
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Trust's settings, through the same merge every bay uses (docs/building/a-new-bay.md):
|
|
3
|
+
*
|
|
4
|
+
* ~/.config/opencode-cockpit/config.json → <project>/.cockpit.json → plugin-entry options
|
|
5
|
+
*
|
|
6
|
+
* Only the `trust` section of a file is read. A file without one says nothing about Trust — reading
|
|
7
|
+
* the whole file as Trust's settings is the trap that page warns about. An unreadable or invalid file
|
|
8
|
+
* is ignored rather than fatal: a typo in a config should never cost you the interface.
|
|
9
|
+
*
|
|
10
|
+
* This is Cockpit's config, not OpenCode's. What OpenCode allows, denies and asks about is read from
|
|
11
|
+
* OpenCode itself (`rules.ts`), and never written by Trust.
|
|
12
|
+
*/
|
|
13
|
+
export interface TrustConfig {
|
|
14
|
+
/** Off switch for this bay, wherever it is written. The bundle also has `features.trust: false`. */
|
|
15
|
+
enabled?: boolean;
|
|
16
|
+
/** Approvals in a row, by you, before Trust answers for you. Default 3. */
|
|
17
|
+
threshold?: number;
|
|
18
|
+
/** What a dangerous command costs on top (`rm`, `git push`, `--force`…). Default 5: eight in all. */
|
|
19
|
+
dangerExtra?: number;
|
|
20
|
+
/** Days a trusted command may go unused before it has to be earned again. Default 30; 0 never. */
|
|
21
|
+
expireDays?: number;
|
|
22
|
+
/**
|
|
23
|
+
* Whether Trust draws a block in the sidebar. Default false: the sidebar already carries the
|
|
24
|
+
* statusline, subagents and shells, and Trust works the same without it — `/trust` opens the
|
|
25
|
+
* ledger, and the palette's "Show or hide Trust in the sidebar" brings the block back for the session.
|
|
26
|
+
*/
|
|
27
|
+
sidebar?: boolean;
|
|
28
|
+
/** Answers by Trust listed in the sidebar. Default 3. */
|
|
29
|
+
sidebarRows?: number;
|
|
30
|
+
/** Where the block sits among sidebar blocks; lower draws first. */
|
|
31
|
+
sidebarOrder?: number;
|
|
32
|
+
keybinds?: Record<string, string>;
|
|
33
|
+
}
|
|
34
|
+
export interface TrustSettings {
|
|
35
|
+
enabled: boolean;
|
|
36
|
+
threshold: number;
|
|
37
|
+
dangerExtra: number;
|
|
38
|
+
expireDays: number;
|
|
39
|
+
sidebar: boolean;
|
|
40
|
+
sidebarRows: number;
|
|
41
|
+
}
|
|
42
|
+
export declare const DEFAULTS: TrustSettings;
|
|
43
|
+
export declare const CONFIG_FILE = "config.json";
|
|
44
|
+
export declare const PROJECT_FILE = ".cockpit.json";
|
|
45
|
+
export declare function globalConfigPath(env?: Record<string, string | undefined>): string;
|
|
46
|
+
/** The `trust` section of a config file. */
|
|
47
|
+
export declare function trustSection(raw: unknown): TrustConfig;
|
|
48
|
+
/**
|
|
49
|
+
* Plugin-entry options: a whole cockpit config (`{ trust: {...} }`) or Trust's own keys, because the
|
|
50
|
+
* bundle hands each bay its own section and a standalone entry carries them directly.
|
|
51
|
+
*/
|
|
52
|
+
export declare function asTrustConfig(input: unknown): TrustConfig;
|
|
53
|
+
export declare function mergeTrust(base: TrustConfig, over: TrustConfig): TrustConfig;
|
|
54
|
+
/** Reads and merges every source, without blocking the interface thread. */
|
|
55
|
+
export declare function loadTrustConfig(directory: string, options?: unknown, env?: Record<string, string | undefined>): Promise<TrustConfig>;
|
|
56
|
+
/** Settings with every gap filled and every number made sensible: a threshold of 0 would trust anything. */
|
|
57
|
+
export declare function resolveSettings(config: TrustConfig): TrustSettings;
|