@frockbot/plugin-shell 0.0.0 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/frockbot.json +68 -0
- package/package.json +87 -6
- package/src/agent.test.ts +372 -0
- package/src/agent.ts +335 -0
- package/src/approvals.test.ts +224 -0
- package/src/approvals.ts +530 -0
- package/src/backend-assignment.test.ts +161 -0
- package/src/backend-assignment.ts +274 -0
- package/src/backend-authoring.test.ts +518 -0
- package/src/backend-authoring.ts +531 -0
- package/src/backend-bot-identity.test.ts +215 -0
- package/src/backend-completion.test.ts +289 -0
- package/src/backend-completion.ts +95 -0
- package/src/backend-composition.ts +242 -0
- package/src/backend-computer.ts +76 -0
- package/src/backend-configuration.test.ts +1757 -0
- package/src/backend-contracts.test.ts +189 -0
- package/src/backend-contracts.ts +44 -0
- package/src/backend-debug.test.ts +202 -0
- package/src/backend-execution.ts +55 -0
- package/src/backend-flock.ts +96 -0
- package/src/backend-image.test.ts +115 -0
- package/src/backend-image.ts +180 -0
- package/src/backend-isolate.test.ts +238 -0
- package/src/backend-isolate.ts +409 -0
- package/src/backend-machine.ts +144 -0
- package/src/backend-memory.ts +89 -0
- package/src/backend-recovery-integration.test.ts +1575 -0
- package/src/backend-recovery.ts +106 -0
- package/src/backend-routines.ts +375 -0
- package/src/backend-runner.ts +251 -0
- package/src/backend-skills.test.ts +126 -0
- package/src/backend-skills.ts +198 -0
- package/src/backend-stop.test.ts +356 -0
- package/src/backend-subagents.ts +459 -0
- package/src/backend.ts +6035 -0
- package/src/client/FrockBotApp.vue +1026 -0
- package/src/client/SendPayloadView.vue +337 -0
- package/src/client/composer-draft.test.ts +31 -0
- package/src/client/composer-draft.ts +35 -0
- package/src/client/cordis-client-shim.d.ts +15 -0
- package/src/client/index.test.ts +2548 -0
- package/src/client/index.ts +2346 -0
- package/src/client/model-presentation.test.ts +35 -0
- package/src/client/model-presentation.ts +19 -0
- package/src/client/notify.test.ts +89 -0
- package/src/client/notify.ts +101 -0
- package/src/client/skill-invocation.test.ts +143 -0
- package/src/client/skill-invocation.ts +175 -0
- package/src/client/styles.css +1043 -0
- package/src/composition-views.ts +118 -0
- package/src/debug-protocol.test.ts +80 -0
- package/src/debug-protocol.ts +165 -0
- package/src/env.d.ts +10 -0
- package/src/history.test.ts +163 -0
- package/src/history.ts +108 -0
- package/src/host.ts +20 -0
- package/src/index.ts +2 -0
- package/src/manifest.ts +3 -0
- package/src/run-cursor.ts +28 -0
- package/src/run-protocol.test.ts +1281 -0
- package/src/run-protocol.ts +1417 -0
- package/src/settings-links.test.ts +106 -0
- package/src/settings-links.ts +289 -0
- package/src/shared.ts +338 -0
- package/src/skill-protocol.ts +117 -0
- package/src/terminal-records.test.ts +217 -0
- package/src/terminal-records.ts +150 -0
- package/src/unread.test.ts +362 -0
- package/src/unread.ts +675 -0
- package/tsconfig.json +18 -0
- package/vite.config.ts +32 -0
- package/README.md +0 -3
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import {
|
|
3
|
+
decodeSettingsLinkV1,
|
|
4
|
+
renderSettingsLinkV1,
|
|
5
|
+
settingsAnchorV1,
|
|
6
|
+
settingsLinkV1,
|
|
7
|
+
SETTINGS_ANCHORS_V1,
|
|
8
|
+
} from "./settings-links.js";
|
|
9
|
+
|
|
10
|
+
describe("settings link scheme", () => {
|
|
11
|
+
test("renders a Bot row link carrying the Bot, the surface and the anchor", () => {
|
|
12
|
+
expect(settingsLinkV1({ anchor: "bot-name", botId: "alpha" })).toBe(
|
|
13
|
+
"/?bot=alpha&settings=bot-settings#bot-name",
|
|
14
|
+
);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
test("omits the Bot from a User row even when one is selected", () => {
|
|
18
|
+
expect(
|
|
19
|
+
settingsLinkV1({ anchor: "user-default-model", botId: "alpha" }),
|
|
20
|
+
).toBe("/?settings=models#user-default-model");
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test("renders against an origin when one is supplied", () => {
|
|
24
|
+
expect(
|
|
25
|
+
settingsLinkV1({
|
|
26
|
+
anchor: "bot-model",
|
|
27
|
+
botId: "alpha",
|
|
28
|
+
origin: "https://app.example/",
|
|
29
|
+
}),
|
|
30
|
+
).toBe("https://app.example/?bot=alpha&settings=bot-settings#bot-model");
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test("refuses an anchor this build does not ship", () => {
|
|
34
|
+
expect(() => settingsLinkV1({ anchor: "invented" })).toThrow(
|
|
35
|
+
"unknown settings anchor: invented",
|
|
36
|
+
);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test("renders a Markdown citation a payload can carry verbatim", () => {
|
|
40
|
+
expect(
|
|
41
|
+
renderSettingsLinkV1({ anchor: "bot-capabilities", botId: "alpha" }),
|
|
42
|
+
).toBe(
|
|
43
|
+
"[Capability Assignments](/?bot=alpha&settings=bot-settings#bot-capabilities)",
|
|
44
|
+
);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test("decodes an absolute link back to its surface, anchor and Bot", () => {
|
|
48
|
+
expect(
|
|
49
|
+
decodeSettingsLinkV1(
|
|
50
|
+
"https://app.example/?bot=alpha&settings=bot-panel#bot-info-computer",
|
|
51
|
+
),
|
|
52
|
+
).toEqual({
|
|
53
|
+
surface: "bot-panel",
|
|
54
|
+
anchor: "bot-info-computer",
|
|
55
|
+
botId: "alpha",
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test("decodes the relative form a location carries", () => {
|
|
60
|
+
expect(decodeSettingsLinkV1("/?settings=plugins#user-packages")).toEqual({
|
|
61
|
+
surface: "plugins",
|
|
62
|
+
anchor: "user-packages",
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test("decodes a surface with no row", () => {
|
|
67
|
+
expect(decodeSettingsLinkV1("/?bot=alpha&settings=bot-settings")).toEqual({
|
|
68
|
+
surface: "bot-settings",
|
|
69
|
+
botId: "alpha",
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test("drops a fragment belonging to another surface", () => {
|
|
74
|
+
expect(
|
|
75
|
+
decodeSettingsLinkV1("/?settings=bot-settings#bot-info-computer"),
|
|
76
|
+
).toEqual({ surface: "bot-settings" });
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test("drops an anchor nobody registered", () => {
|
|
80
|
+
expect(decodeSettingsLinkV1("/?settings=bot-settings#invented")).toEqual({
|
|
81
|
+
surface: "bot-settings",
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test("refuses a URL naming no surface", () => {
|
|
86
|
+
expect(decodeSettingsLinkV1("/?bot=alpha#bot-name")).toBeUndefined();
|
|
87
|
+
expect(decodeSettingsLinkV1("/?settings=nowhere#bot-name")).toBeUndefined();
|
|
88
|
+
expect(decodeSettingsLinkV1("not a url at all")).toBeUndefined();
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test("every anchor round-trips through its own link", () => {
|
|
92
|
+
for (const entry of SETTINGS_ANCHORS_V1) {
|
|
93
|
+
const decoded = decodeSettingsLinkV1(
|
|
94
|
+
settingsLinkV1({ anchor: entry.anchor, botId: "alpha" }),
|
|
95
|
+
);
|
|
96
|
+
expect(decoded?.surface).toBe(entry.surface);
|
|
97
|
+
expect(decoded?.anchor).toBe(entry.anchor);
|
|
98
|
+
expect(settingsAnchorV1(entry.anchor)).toBe(entry);
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test("anchors are unique", () => {
|
|
103
|
+
const anchors = SETTINGS_ANCHORS_V1.map((entry) => entry.anchor);
|
|
104
|
+
expect(new Set(anchors).size).toBe(anchors.length);
|
|
105
|
+
});
|
|
106
|
+
});
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The settings deep-link scheme.
|
|
3
|
+
*
|
|
4
|
+
* GrokBot's harness may cite a settings row by anchor — `app-ui.md` enumerates
|
|
5
|
+
* every linkable anchor and instructs the agent never to invent one (register
|
|
6
|
+
* row 50). FrockBot copies the property, not the custom protocol: the hosted
|
|
7
|
+
* WebUI is the product UI, so a citable settings row is an ordinary URL of the
|
|
8
|
+
* application the User already has open.
|
|
9
|
+
*
|
|
10
|
+
* <origin>/?bot=<botId>&settings=<surface>#<anchor>
|
|
11
|
+
*
|
|
12
|
+
* `bot` is the shell's existing Bot selector, already read by the Flock client
|
|
13
|
+
* and written by every selection. `settings` names a registered client surface
|
|
14
|
+
* or the default Bot panel, and the fragment names one row inside it. Every part is optional in the
|
|
15
|
+
* decode and refused when unknown: a link that names a surface nobody
|
|
16
|
+
* registered, or an anchor this build does not ship, decodes to `undefined`
|
|
17
|
+
* rather than opening the wrong thing. That is the whole point of holding the
|
|
18
|
+
* anchors in one table — a Bot may cite a link, and cannot invent one.
|
|
19
|
+
*
|
|
20
|
+
* The module is deliberately free of Vue and of every client type, so a
|
|
21
|
+
* backend Contribution rendering an error message or a `send_to_user` payload
|
|
22
|
+
* can cite the same anchors the panel renders.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
/** A client surface a settings link may open. */
|
|
26
|
+
export type SettingsSurfaceIdV1 =
|
|
27
|
+
| "bot-settings"
|
|
28
|
+
| "bot-panel"
|
|
29
|
+
| "user-settings"
|
|
30
|
+
| "plugins"
|
|
31
|
+
| "models"
|
|
32
|
+
| "connections";
|
|
33
|
+
|
|
34
|
+
export const SETTINGS_SURFACE_IDS_V1: readonly SettingsSurfaceIdV1[] = [
|
|
35
|
+
"bot-settings",
|
|
36
|
+
"bot-panel",
|
|
37
|
+
"user-settings",
|
|
38
|
+
"plugins",
|
|
39
|
+
"models",
|
|
40
|
+
"connections",
|
|
41
|
+
];
|
|
42
|
+
|
|
43
|
+
/** One linkable row or section. */
|
|
44
|
+
export interface SettingsAnchorV1 {
|
|
45
|
+
/** The fragment identifier, and the DOM id of the row it names. */
|
|
46
|
+
anchor: string;
|
|
47
|
+
/** The surface that has to be open for the row to exist. */
|
|
48
|
+
surface: SettingsSurfaceIdV1;
|
|
49
|
+
/** What the row is called on screen; used as the link text. */
|
|
50
|
+
label: string;
|
|
51
|
+
/**
|
|
52
|
+
* `bot` rows only exist for a selected Bot, so their links carry `bot=`.
|
|
53
|
+
* `user` rows are the same for every Bot.
|
|
54
|
+
*/
|
|
55
|
+
scope: "bot" | "user";
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Every anchor this build ships. A row absent here has no link, and a link
|
|
60
|
+
* naming an anchor absent here does not resolve — GrokBot's `app-ui.md` makes
|
|
61
|
+
* the same promise ("rows can be absent per account, build and state").
|
|
62
|
+
*/
|
|
63
|
+
export const SETTINGS_ANCHORS_V1: readonly SettingsAnchorV1[] = [
|
|
64
|
+
// The Bot settings panel, row by row.
|
|
65
|
+
{
|
|
66
|
+
anchor: "bot-avatar",
|
|
67
|
+
surface: "bot-settings",
|
|
68
|
+
label: "Avatar",
|
|
69
|
+
scope: "bot",
|
|
70
|
+
},
|
|
71
|
+
{ anchor: "bot-name", surface: "bot-settings", label: "Name", scope: "bot" },
|
|
72
|
+
{
|
|
73
|
+
anchor: "bot-title",
|
|
74
|
+
surface: "bot-settings",
|
|
75
|
+
label: "Title",
|
|
76
|
+
scope: "bot",
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
anchor: "bot-label",
|
|
80
|
+
surface: "bot-settings",
|
|
81
|
+
label: "Label",
|
|
82
|
+
scope: "bot",
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
anchor: "bot-description",
|
|
86
|
+
surface: "bot-settings",
|
|
87
|
+
label: "Description",
|
|
88
|
+
scope: "bot",
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
anchor: "bot-notifications",
|
|
92
|
+
surface: "bot-settings",
|
|
93
|
+
label: "Notifications",
|
|
94
|
+
scope: "bot",
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
anchor: "bot-hidden-from-sidebar",
|
|
98
|
+
surface: "bot-settings",
|
|
99
|
+
label: "Hidden from sidebar",
|
|
100
|
+
scope: "bot",
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
// Present only while something is waiting: a pending decision is state,
|
|
104
|
+
// not a permanent row, and `app-ui.md`'s own promise is that a row "can be
|
|
105
|
+
// absent per account, build and state".
|
|
106
|
+
anchor: "bot-approvals",
|
|
107
|
+
surface: "bot-settings",
|
|
108
|
+
label: "Waiting on you",
|
|
109
|
+
scope: "bot",
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
anchor: "bot-model",
|
|
113
|
+
surface: "bot-settings",
|
|
114
|
+
label: "Model",
|
|
115
|
+
scope: "bot",
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
anchor: "bot-capabilities",
|
|
119
|
+
surface: "bot-settings",
|
|
120
|
+
label: "Capability Assignments",
|
|
121
|
+
scope: "bot",
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
anchor: "bot-routines",
|
|
125
|
+
surface: "bot-settings",
|
|
126
|
+
label: "Routines",
|
|
127
|
+
scope: "bot",
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
anchor: "bot-audit",
|
|
131
|
+
surface: "bot-settings",
|
|
132
|
+
label: "Audit log",
|
|
133
|
+
scope: "bot",
|
|
134
|
+
},
|
|
135
|
+
|
|
136
|
+
// Former info-pane anchors keep working at their new homes.
|
|
137
|
+
{
|
|
138
|
+
anchor: "bot-info-identity",
|
|
139
|
+
surface: "bot-settings",
|
|
140
|
+
label: "Identity",
|
|
141
|
+
scope: "bot",
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
anchor: "bot-info-members",
|
|
145
|
+
surface: "bot-settings",
|
|
146
|
+
label: "Members",
|
|
147
|
+
scope: "bot",
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
anchor: "bot-info-computer",
|
|
151
|
+
surface: "bot-panel",
|
|
152
|
+
label: "Computer",
|
|
153
|
+
scope: "bot",
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
anchor: "bot-info-routines",
|
|
157
|
+
surface: "bot-panel",
|
|
158
|
+
label: "Routines",
|
|
159
|
+
scope: "bot",
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
anchor: "bot-info-notifications",
|
|
163
|
+
surface: "bot-settings",
|
|
164
|
+
label: "Notifications",
|
|
165
|
+
scope: "bot",
|
|
166
|
+
},
|
|
167
|
+
|
|
168
|
+
// Application settings, the Plugins catalog, and the two configuration
|
|
169
|
+
// surfaces that own what a Package declares: Models and Connections.
|
|
170
|
+
{
|
|
171
|
+
anchor: "user-profile",
|
|
172
|
+
surface: "user-settings",
|
|
173
|
+
label: "Your profile",
|
|
174
|
+
scope: "user",
|
|
175
|
+
},
|
|
176
|
+
{
|
|
177
|
+
anchor: "user-package-settings",
|
|
178
|
+
surface: "user-settings",
|
|
179
|
+
label: "Package settings",
|
|
180
|
+
scope: "user",
|
|
181
|
+
},
|
|
182
|
+
{
|
|
183
|
+
anchor: "user-default-model",
|
|
184
|
+
surface: "models",
|
|
185
|
+
label: "Default model",
|
|
186
|
+
scope: "user",
|
|
187
|
+
},
|
|
188
|
+
{
|
|
189
|
+
anchor: "user-model-providers",
|
|
190
|
+
surface: "models",
|
|
191
|
+
label: "Model providers",
|
|
192
|
+
scope: "user",
|
|
193
|
+
},
|
|
194
|
+
{
|
|
195
|
+
anchor: "user-connections",
|
|
196
|
+
surface: "connections",
|
|
197
|
+
label: "Connections",
|
|
198
|
+
scope: "user",
|
|
199
|
+
},
|
|
200
|
+
{
|
|
201
|
+
anchor: "user-machines",
|
|
202
|
+
surface: "user-settings",
|
|
203
|
+
label: "Registered machines",
|
|
204
|
+
scope: "user",
|
|
205
|
+
},
|
|
206
|
+
{
|
|
207
|
+
anchor: "user-packages",
|
|
208
|
+
surface: "plugins",
|
|
209
|
+
label: "Packages",
|
|
210
|
+
scope: "user",
|
|
211
|
+
},
|
|
212
|
+
];
|
|
213
|
+
|
|
214
|
+
const anchorsByName = new Map(
|
|
215
|
+
SETTINGS_ANCHORS_V1.map((entry) => [entry.anchor, entry]),
|
|
216
|
+
);
|
|
217
|
+
|
|
218
|
+
/** The anchor record, or `undefined` for one this build does not ship. */
|
|
219
|
+
export function settingsAnchorV1(anchor: string): SettingsAnchorV1 | undefined {
|
|
220
|
+
return anchorsByName.get(anchor);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
export interface SettingsLinkTargetV1 {
|
|
224
|
+
surface: SettingsSurfaceIdV1;
|
|
225
|
+
/** Absent when the link names a surface but no row inside it. */
|
|
226
|
+
anchor?: string;
|
|
227
|
+
/** The Bot the link is about, when it names one. */
|
|
228
|
+
botId?: string;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
export interface SettingsLinkInputV1 {
|
|
232
|
+
anchor: string;
|
|
233
|
+
botId?: string;
|
|
234
|
+
/** Defaults to a relative link, which is what the running client wants. */
|
|
235
|
+
origin?: string;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Render a link to one settings row. Throws for an unknown anchor: the caller
|
|
240
|
+
* is a Package citing a row, and a citation that resolves to nothing is worse
|
|
241
|
+
* than a missing one.
|
|
242
|
+
*/
|
|
243
|
+
export function settingsLinkV1(input: SettingsLinkInputV1): string {
|
|
244
|
+
const entry = settingsAnchorV1(input.anchor);
|
|
245
|
+
if (!entry) throw new Error(`unknown settings anchor: ${input.anchor}`);
|
|
246
|
+
const parameters = new URLSearchParams();
|
|
247
|
+
if (entry.scope === "bot" && input.botId) parameters.set("bot", input.botId);
|
|
248
|
+
parameters.set("settings", entry.surface);
|
|
249
|
+
const path = `/?${parameters.toString()}#${entry.anchor}`;
|
|
250
|
+
if (!input.origin) return path;
|
|
251
|
+
return new URL(path, input.origin).href;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/** A Markdown link a send payload or an error message can carry verbatim. */
|
|
255
|
+
export function renderSettingsLinkV1(input: SettingsLinkInputV1): string {
|
|
256
|
+
const entry = settingsAnchorV1(input.anchor);
|
|
257
|
+
if (!entry) throw new Error(`unknown settings anchor: ${input.anchor}`);
|
|
258
|
+
return `[${entry.label}](${settingsLinkV1(input)})`;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function surfaceId(value: string | null): SettingsSurfaceIdV1 | undefined {
|
|
262
|
+
return SETTINGS_SURFACE_IDS_V1.find((candidate) => candidate === value);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Read a settings link. Accepts an absolute URL or the `?query#fragment` a
|
|
267
|
+
* browser's `location` carries, and returns `undefined` for anything that does
|
|
268
|
+
* not name a surface this build ships.
|
|
269
|
+
*
|
|
270
|
+
* A fragment naming an anchor that belongs to another surface is dropped
|
|
271
|
+
* rather than honoured: opening `bot-settings` and then scrolling to a row that
|
|
272
|
+
* only exists in another panel would leave the User looking at nothing.
|
|
273
|
+
*/
|
|
274
|
+
export function decodeSettingsLinkV1(
|
|
275
|
+
href: string,
|
|
276
|
+
): SettingsLinkTargetV1 | undefined {
|
|
277
|
+
const url = URL.parse(href) ?? URL.parse(href, "http://frockbot.invalid/");
|
|
278
|
+
if (!url) return undefined;
|
|
279
|
+
const surface = surfaceId(url.searchParams.get("settings"));
|
|
280
|
+
if (!surface) return undefined;
|
|
281
|
+
const fragment = decodeURIComponent(url.hash.replace(/^#/u, ""));
|
|
282
|
+
const entry = fragment ? settingsAnchorV1(fragment) : undefined;
|
|
283
|
+
const botId = url.searchParams.get("bot") ?? undefined;
|
|
284
|
+
return {
|
|
285
|
+
surface,
|
|
286
|
+
...(entry && entry.surface === surface ? { anchor: entry.anchor } : {}),
|
|
287
|
+
...(botId ? { botId } : {}),
|
|
288
|
+
};
|
|
289
|
+
}
|