@rmrdeveloper/sideroom-pi 8.14.0 → 9.0.1
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/README.md +31 -22
- package/assets/artifacts/GUIDELINES_TEMPLATE.md +33 -2
- package/assets/visual-context/browser/INSTALL.md +11 -2
- package/assets/visual-context/browser/background.js +72 -60
- package/assets/visual-context/browser/content.js +45 -7
- package/assets/visual-context/browser/destinations.js +40 -0
- package/assets/visual-context/browser/manifest.json +1 -1
- package/extensions/guidelines/catalog.ts +9 -0
- package/extensions/guidelines/execute.ts +26 -0
- package/extensions/guidelines/guard.ts +46 -14
- package/extensions/guidelines/index.ts +31 -0
- package/extensions/guidelines/prompt.ts +2 -2
- package/extensions/guidelines/review.ts +1 -1
- package/extensions/jev/model.ts +13 -0
- package/extensions/visual-context/broker.ts +14 -6
- package/extensions/visual-context/compiler-config.ts +103 -0
- package/extensions/visual-context/configure.ts +421 -0
- package/extensions/visual-context/dialogs.ts +95 -0
- package/extensions/visual-context/files.ts +139 -0
- package/extensions/visual-context/index.ts +48 -56
- package/extensions/visual-context/match.ts +48 -16
- package/extensions/visual-context/model.ts +29 -3
- package/extensions/visual-context/projects.ts +226 -0
- package/extensions/visual-context/session.ts +150 -17
- package/extensions/visual-context/setup.ts +146 -0
- package/extensions/visual-context/source.ts +203 -100
- package/extensions/visual-context/ui.ts +5 -1
- package/package.json +6 -6
- package/skills/sideroom-guidelines/SKILL.md +8 -5
- package/skills/sideroom-guidelines/references/languages/go.md +35 -1
- package/skills/sideroom-guidelines/references/languages/java.md +34 -1
- package/skills/sideroom-guidelines/references/languages/javascript.md +34 -1
- package/skills/sideroom-guidelines/references/languages/php-laravel.md +34 -1
- package/skills/sideroom-guidelines/references/languages/python.md +30 -1
- package/skills/sideroom-guidelines/references/languages/rust.md +34 -1
- package/skills/sideroom-guidelines/references/languages/typescript.md +38 -1
- package/skills/sideroom-visual-setup/SKILL.md +49 -0
|
@@ -10,13 +10,16 @@ import {
|
|
|
10
10
|
MAX_CANDIDATES,
|
|
11
11
|
MAX_SELECTIONS,
|
|
12
12
|
type PendingSelection,
|
|
13
|
+
PROJECT_BINDINGS,
|
|
13
14
|
parseSelection,
|
|
14
15
|
SelectionSchema,
|
|
16
|
+
SOURCE_METHOD,
|
|
15
17
|
selectionContext,
|
|
16
18
|
VISUAL_SNAPSHOT,
|
|
17
19
|
type VisualSelection,
|
|
18
20
|
} from './model.ts';
|
|
19
|
-
import {
|
|
21
|
+
import { inspectProject } from './projects.ts';
|
|
22
|
+
import { currentCandidates, resolveProjectSelection } from './source.ts';
|
|
20
23
|
|
|
21
24
|
const CandidateSchema = Type.Object(
|
|
22
25
|
{
|
|
@@ -30,6 +33,8 @@ const CandidateSchema = Type.Object(
|
|
|
30
33
|
fingerprint: Type.String({ pattern: '^[a-f0-9]{64}$' }),
|
|
31
34
|
score: Type.Integer({ minimum: 0 }),
|
|
32
35
|
reasons: Type.Array(Type.String({ maxLength: 200 }), { maxItems: 40 }),
|
|
36
|
+
method: Type.Optional(Type.Literal('compiler')),
|
|
37
|
+
startColumn: Type.Optional(Type.Integer({ minimum: 1 })),
|
|
33
38
|
},
|
|
34
39
|
{ additionalProperties: false },
|
|
35
40
|
);
|
|
@@ -46,6 +51,7 @@ const SnapshotSchema = Type.Object(
|
|
|
46
51
|
}),
|
|
47
52
|
truncated: Type.Boolean(),
|
|
48
53
|
capped: Type.Boolean(),
|
|
54
|
+
issue: Type.Optional(Type.String({ maxLength: 512 })),
|
|
49
55
|
},
|
|
50
56
|
{ additionalProperties: false },
|
|
51
57
|
),
|
|
@@ -61,36 +67,129 @@ const SnapshotSchema = Type.Object(
|
|
|
61
67
|
|
|
62
68
|
export interface VisualStore {
|
|
63
69
|
pending: readonly PendingSelection[];
|
|
70
|
+
bindings: readonly ProjectBinding[];
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
interface ProjectBinding {
|
|
74
|
+
readonly origin: string;
|
|
75
|
+
readonly project: string;
|
|
76
|
+
}
|
|
77
|
+
const BindingsSchema = Type.Object(
|
|
78
|
+
{
|
|
79
|
+
bindings: Type.Array(
|
|
80
|
+
Type.Object(
|
|
81
|
+
{
|
|
82
|
+
origin: Type.String({ maxLength: 2_048 }),
|
|
83
|
+
project: Type.String({
|
|
84
|
+
minLength: 1,
|
|
85
|
+
maxLength: 2_048,
|
|
86
|
+
pattern: '^[^\\x00-\\x1f\\x7f]*$',
|
|
87
|
+
}),
|
|
88
|
+
},
|
|
89
|
+
{ additionalProperties: false },
|
|
90
|
+
),
|
|
91
|
+
{ maxItems: 64 },
|
|
92
|
+
),
|
|
93
|
+
},
|
|
94
|
+
{ additionalProperties: false },
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
function pageOrigin(page: string): string {
|
|
98
|
+
let url: URL;
|
|
99
|
+
try {
|
|
100
|
+
url = new URL(page);
|
|
101
|
+
} catch (cause) {
|
|
102
|
+
throw new Error('Invalid browser page address for project association.', {
|
|
103
|
+
cause,
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
if (!['http:', 'https:'].includes(url.protocol)) {
|
|
107
|
+
throw new Error('Associate an HTTP or HTTPS page.');
|
|
108
|
+
}
|
|
109
|
+
return url.origin;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function projectFor(store: VisualStore, page: string): string | undefined {
|
|
113
|
+
return store.bindings.find((binding) => binding.origin === pageOrigin(page))
|
|
114
|
+
?.project;
|
|
64
115
|
}
|
|
65
116
|
|
|
66
117
|
export function createVisualStore(): VisualStore {
|
|
67
|
-
return { pending: [] };
|
|
118
|
+
return { pending: [], bindings: [] };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
interface ProjectBindingInput {
|
|
122
|
+
readonly store: VisualStore;
|
|
123
|
+
readonly pi: Pick<ExtensionAPI, 'appendEntry'>;
|
|
124
|
+
readonly cwd: string;
|
|
125
|
+
readonly project: string;
|
|
126
|
+
readonly page: string;
|
|
68
127
|
}
|
|
69
128
|
|
|
70
|
-
export function
|
|
129
|
+
export function bindProject({
|
|
71
130
|
store,
|
|
72
131
|
pi,
|
|
73
|
-
|
|
74
|
-
|
|
132
|
+
cwd,
|
|
133
|
+
project,
|
|
134
|
+
page,
|
|
135
|
+
}: ProjectBindingInput): void {
|
|
136
|
+
const inspected = inspectProject(cwd, project);
|
|
137
|
+
const origin = pageOrigin(page);
|
|
138
|
+
const bindings = [
|
|
139
|
+
...store.bindings.filter((binding) => binding.origin !== origin),
|
|
140
|
+
{ origin, project: inspected.path },
|
|
141
|
+
];
|
|
142
|
+
if (bindings.length > 64) {
|
|
143
|
+
throw new Error(
|
|
144
|
+
'The session already has 64 project associations. Reuse an existing origin.',
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
const pending = store.pending.map((entry) => {
|
|
148
|
+
if (pageOrigin(entry.selection.page) !== origin) {
|
|
149
|
+
return entry;
|
|
150
|
+
}
|
|
151
|
+
const resolution = resolveProjectSelection({
|
|
152
|
+
cwd,
|
|
153
|
+
selection: entry.selection,
|
|
154
|
+
project: inspected.path,
|
|
155
|
+
});
|
|
156
|
+
return {
|
|
157
|
+
selection: entry.selection,
|
|
158
|
+
resolution,
|
|
159
|
+
chosen: resolution.candidates.find(
|
|
160
|
+
(candidate) => candidate.method === SOURCE_METHOD.compiler,
|
|
161
|
+
),
|
|
162
|
+
};
|
|
163
|
+
});
|
|
164
|
+
store.bindings = bindings;
|
|
165
|
+
pi.appendEntry(PROJECT_BINDINGS, { bindings });
|
|
166
|
+
commitPending({ store, pi, pending });
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
interface PendingCommit {
|
|
75
170
|
readonly store: VisualStore;
|
|
76
171
|
readonly pi: Pick<ExtensionAPI, 'appendEntry'>;
|
|
77
172
|
readonly pending: readonly PendingSelection[];
|
|
78
|
-
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export function commitPending({ store, pi, pending }: PendingCommit): void {
|
|
79
176
|
store.pending = pending;
|
|
80
177
|
pi.appendEntry(VISUAL_SNAPSHOT, { pending });
|
|
81
178
|
}
|
|
82
179
|
|
|
180
|
+
interface ReceiveSelectionInput {
|
|
181
|
+
readonly store: VisualStore;
|
|
182
|
+
readonly pi: Pick<ExtensionAPI, 'appendEntry'>;
|
|
183
|
+
readonly cwd: string;
|
|
184
|
+
readonly selection: VisualSelection;
|
|
185
|
+
}
|
|
186
|
+
|
|
83
187
|
export function receiveSelection({
|
|
84
188
|
store,
|
|
85
189
|
pi,
|
|
86
190
|
cwd,
|
|
87
191
|
selection,
|
|
88
|
-
}: {
|
|
89
|
-
readonly store: VisualStore;
|
|
90
|
-
readonly pi: Pick<ExtensionAPI, 'appendEntry'>;
|
|
91
|
-
readonly cwd: string;
|
|
92
|
-
readonly selection: VisualSelection;
|
|
93
|
-
}): { readonly candidates: number } {
|
|
192
|
+
}: ReceiveSelectionInput): { readonly candidates: number } {
|
|
94
193
|
const existing = store.pending.find(
|
|
95
194
|
(entry) => entry.selection.id === selection.id,
|
|
96
195
|
);
|
|
@@ -98,11 +197,21 @@ export function receiveSelection({
|
|
|
98
197
|
return { candidates: existing.resolution.candidates.length };
|
|
99
198
|
}
|
|
100
199
|
assertSelectionCapacity(store.pending);
|
|
101
|
-
const resolution =
|
|
200
|
+
const resolution = resolveProjectSelection({
|
|
201
|
+
cwd,
|
|
202
|
+
selection,
|
|
203
|
+
project: projectFor(store, selection.page),
|
|
204
|
+
});
|
|
102
205
|
commitPending({
|
|
103
206
|
store,
|
|
104
207
|
pi,
|
|
105
|
-
pending: addSelection(store.pending, {
|
|
208
|
+
pending: addSelection(store.pending, {
|
|
209
|
+
selection,
|
|
210
|
+
resolution,
|
|
211
|
+
chosen: resolution.candidates.find(
|
|
212
|
+
(candidate) => candidate.method === SOURCE_METHOD.compiler,
|
|
213
|
+
),
|
|
214
|
+
}),
|
|
106
215
|
});
|
|
107
216
|
return { candidates: resolution.candidates.length };
|
|
108
217
|
}
|
|
@@ -115,7 +224,20 @@ export function restorePending({
|
|
|
115
224
|
readonly ctx: Pick<ExtensionContext, 'sessionManager' | 'cwd'>;
|
|
116
225
|
}): void {
|
|
117
226
|
store.pending = [];
|
|
227
|
+
store.bindings = [];
|
|
118
228
|
const branch = ctx.sessionManager.getBranch();
|
|
229
|
+
for (const entry of [...branch].reverse()) {
|
|
230
|
+
if (entry.type !== 'custom' || entry.customType !== PROJECT_BINDINGS) {
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
233
|
+
if (Check(BindingsSchema, entry.data)) {
|
|
234
|
+
store.bindings = entry.data.bindings.map((binding) => ({
|
|
235
|
+
...binding,
|
|
236
|
+
origin: pageOrigin(binding.origin),
|
|
237
|
+
}));
|
|
238
|
+
}
|
|
239
|
+
break;
|
|
240
|
+
}
|
|
119
241
|
for (let index = branch.length - 1; index >= 0; index -= 1) {
|
|
120
242
|
const entry = branch[index];
|
|
121
243
|
if (entry?.type !== 'custom' || entry.customType !== VISUAL_SNAPSHOT) {
|
|
@@ -125,11 +247,16 @@ export function restorePending({
|
|
|
125
247
|
return;
|
|
126
248
|
}
|
|
127
249
|
const snapshot: Static<typeof SnapshotSchema> = entry.data;
|
|
250
|
+
const current = new Set(
|
|
251
|
+
currentCandidates(
|
|
252
|
+
ctx.cwd,
|
|
253
|
+
snapshot.pending.flatMap((pending) => pending.resolution.candidates),
|
|
254
|
+
),
|
|
255
|
+
);
|
|
128
256
|
store.pending = snapshot.pending.flatMap((pending) => {
|
|
129
257
|
const candidates = pending.resolution.candidates.filter(
|
|
130
258
|
(candidate) =>
|
|
131
|
-
candidate.startLine <= candidate.endLine &&
|
|
132
|
-
candidateIsCurrent(ctx.cwd, candidate),
|
|
259
|
+
candidate.startLine <= candidate.endLine && current.has(candidate),
|
|
133
260
|
);
|
|
134
261
|
let selection: VisualSelection;
|
|
135
262
|
try {
|
|
@@ -170,9 +297,15 @@ export function prepareContext({
|
|
|
170
297
|
if (store.pending.length === 0) {
|
|
171
298
|
return undefined;
|
|
172
299
|
}
|
|
300
|
+
const current = new Set(
|
|
301
|
+
currentCandidates(
|
|
302
|
+
cwd,
|
|
303
|
+
store.pending.flatMap((entry) => entry.resolution.candidates),
|
|
304
|
+
),
|
|
305
|
+
);
|
|
173
306
|
const pending = store.pending.map((entry) => {
|
|
174
307
|
const candidates = entry.resolution.candidates.filter((candidate) =>
|
|
175
|
-
|
|
308
|
+
current.has(candidate),
|
|
176
309
|
);
|
|
177
310
|
const chosen =
|
|
178
311
|
entry.chosen && candidates.includes(entry.chosen)
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { readFileSync, statSync } from 'node:fs';
|
|
2
|
+
import { dirname, join } from 'node:path';
|
|
3
|
+
import type { ExtensionAPI } from '@earendil-works/pi-coding-agent';
|
|
4
|
+
import { Type } from 'typebox';
|
|
5
|
+
import { compilerPackage, INSPECTOR_VERSION } from './compiler-config.ts';
|
|
6
|
+
import { configureSource } from './configure.ts';
|
|
7
|
+
import { allowedPath, MAX_FILE_BYTES } from './files.ts';
|
|
8
|
+
import { discoverProjects, inspectProject, readManifest } from './projects.ts';
|
|
9
|
+
|
|
10
|
+
export const VISUAL_SETUP_TOOL = 'sideroom_visual_setup';
|
|
11
|
+
const SetupSchema = Type.Object(
|
|
12
|
+
{
|
|
13
|
+
action: Type.Union([
|
|
14
|
+
Type.Literal('discover'),
|
|
15
|
+
Type.Literal('prepare'),
|
|
16
|
+
Type.Literal('bind'),
|
|
17
|
+
]),
|
|
18
|
+
project: Type.Optional(
|
|
19
|
+
Type.String({
|
|
20
|
+
minLength: 1,
|
|
21
|
+
maxLength: 2_048,
|
|
22
|
+
pattern: '^[^\\x00-\\x1f\\x7f]*$',
|
|
23
|
+
}),
|
|
24
|
+
),
|
|
25
|
+
page: Type.Optional(Type.String({ minLength: 1, maxLength: 2_048 })),
|
|
26
|
+
},
|
|
27
|
+
{ additionalProperties: false },
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
export function prepareSetup(cwd: string, project: string) {
|
|
31
|
+
const inspected = inspectProject(cwd, project);
|
|
32
|
+
if (!inspected.config || !inspected.kind) {
|
|
33
|
+
throw new Error(
|
|
34
|
+
'No supported build configuration was found. Create or identify the framework configuration before preparing inspector setup.',
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
const config = allowedPath(cwd, inspected.config);
|
|
38
|
+
if (!config || statSync(config).size > MAX_FILE_BYTES) {
|
|
39
|
+
throw new Error('The configuration is missing, excluded, or too large.');
|
|
40
|
+
}
|
|
41
|
+
const root = dirname(config);
|
|
42
|
+
const manifest = readManifest(root);
|
|
43
|
+
const nextVersion =
|
|
44
|
+
manifest.dependencies?.next ?? manifest.devDependencies?.next;
|
|
45
|
+
const original = readFileSync(config, 'utf8');
|
|
46
|
+
const prepared = configureSource({
|
|
47
|
+
source: original,
|
|
48
|
+
kind: inspected.kind,
|
|
49
|
+
commonjs: config.endsWith('.cjs'),
|
|
50
|
+
typescript: /\.(ts|mts)$/u.test(config),
|
|
51
|
+
nextMajor: nextVersion ? Number(nextVersion.match(/\d+/u)?.[0]) : undefined,
|
|
52
|
+
});
|
|
53
|
+
return {
|
|
54
|
+
project: inspected,
|
|
55
|
+
edit: {
|
|
56
|
+
path: inspected.config,
|
|
57
|
+
oldText: original,
|
|
58
|
+
newText: prepared.source,
|
|
59
|
+
},
|
|
60
|
+
install: {
|
|
61
|
+
cwd: join(cwd, inspected.path),
|
|
62
|
+
command: inspected.manager,
|
|
63
|
+
args:
|
|
64
|
+
inspected.manager === 'npm'
|
|
65
|
+
? [
|
|
66
|
+
'install',
|
|
67
|
+
'--save-dev',
|
|
68
|
+
`${compilerPackage(inspected.kind)}@${INSPECTOR_VERSION}`,
|
|
69
|
+
]
|
|
70
|
+
: [
|
|
71
|
+
'add',
|
|
72
|
+
'--dev',
|
|
73
|
+
`${compilerPackage(inspected.kind)}@${INSPECTOR_VERSION}`,
|
|
74
|
+
],
|
|
75
|
+
},
|
|
76
|
+
checkScripts: Object.keys(manifest.scripts ?? {}).filter((script) =>
|
|
77
|
+
['check', 'typecheck', 'lint', 'test', 'build'].includes(script),
|
|
78
|
+
),
|
|
79
|
+
instructions:
|
|
80
|
+
'Apply this plan through normal Pi edit/bash tools. Follow sideroom-visual-setup for runtime path portability, installation and browser verification.',
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function registerVisualSetup(
|
|
85
|
+
pi: ExtensionAPI,
|
|
86
|
+
onBind: (binding: {
|
|
87
|
+
readonly project: string;
|
|
88
|
+
readonly page: string;
|
|
89
|
+
readonly cwd: string;
|
|
90
|
+
}) => void,
|
|
91
|
+
): void {
|
|
92
|
+
pi.registerTool({
|
|
93
|
+
name: VISUAL_SETUP_TOOL,
|
|
94
|
+
label: 'Sideroom Visual Setup',
|
|
95
|
+
description:
|
|
96
|
+
'Discover child web projects, prepare a compiler-location integration for one selected project, or associate a browser page with its project. Prepare returns an exact configuration edit and dependency installation command; apply them through the normal Pi edit and bash tools. Does not edit project files itself.',
|
|
97
|
+
promptSnippet:
|
|
98
|
+
'Set up precise browser source locations in a selected web project, including projects below the Pi working directory.',
|
|
99
|
+
promptGuidelines: [
|
|
100
|
+
'Read sideroom-visual-setup before enabling or reviewing compiler context. The skill owns project selection, runtime path portability, installation and verification; prepare supplies the configuration template and commands.',
|
|
101
|
+
],
|
|
102
|
+
parameters: SetupSchema,
|
|
103
|
+
executionMode: 'sequential',
|
|
104
|
+
execute: async (_id, params, _signal, _update, ctx) => {
|
|
105
|
+
if (!ctx.isProjectTrusted()) {
|
|
106
|
+
throw new Error(
|
|
107
|
+
'Trust this workspace in Pi before setting up visual context.',
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
if (params.action === 'discover') {
|
|
111
|
+
const discovery = discoverProjects(ctx.cwd);
|
|
112
|
+
return {
|
|
113
|
+
content: [{ type: 'text', text: JSON.stringify(discovery, null, 2) }],
|
|
114
|
+
details: discovery,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
if (!params.project) {
|
|
118
|
+
throw new Error(
|
|
119
|
+
'Choose an explicit project path relative to the Pi working directory.',
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
if (params.action === 'prepare') {
|
|
123
|
+
const plan = prepareSetup(ctx.cwd, params.project);
|
|
124
|
+
return {
|
|
125
|
+
content: [{ type: 'text', text: JSON.stringify(plan, null, 2) }],
|
|
126
|
+
details: plan,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
if (!params.page) {
|
|
130
|
+
throw new Error(
|
|
131
|
+
'Provide the browser page to associate with the selected project.',
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
onBind({ project: params.project, page: params.page, cwd: ctx.cwd });
|
|
135
|
+
return {
|
|
136
|
+
content: [
|
|
137
|
+
{
|
|
138
|
+
type: 'text',
|
|
139
|
+
text: 'The page is associated with the project. Pending selections were resolved again; compiler locations still require local markup validation.',
|
|
140
|
+
},
|
|
141
|
+
],
|
|
142
|
+
details: {},
|
|
143
|
+
};
|
|
144
|
+
},
|
|
145
|
+
});
|
|
146
|
+
}
|