@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.
Files changed (37) hide show
  1. package/README.md +31 -22
  2. package/assets/artifacts/GUIDELINES_TEMPLATE.md +33 -2
  3. package/assets/visual-context/browser/INSTALL.md +11 -2
  4. package/assets/visual-context/browser/background.js +72 -60
  5. package/assets/visual-context/browser/content.js +45 -7
  6. package/assets/visual-context/browser/destinations.js +40 -0
  7. package/assets/visual-context/browser/manifest.json +1 -1
  8. package/extensions/guidelines/catalog.ts +9 -0
  9. package/extensions/guidelines/execute.ts +26 -0
  10. package/extensions/guidelines/guard.ts +46 -14
  11. package/extensions/guidelines/index.ts +31 -0
  12. package/extensions/guidelines/prompt.ts +2 -2
  13. package/extensions/guidelines/review.ts +1 -1
  14. package/extensions/jev/model.ts +13 -0
  15. package/extensions/visual-context/broker.ts +14 -6
  16. package/extensions/visual-context/compiler-config.ts +103 -0
  17. package/extensions/visual-context/configure.ts +421 -0
  18. package/extensions/visual-context/dialogs.ts +95 -0
  19. package/extensions/visual-context/files.ts +139 -0
  20. package/extensions/visual-context/index.ts +48 -56
  21. package/extensions/visual-context/match.ts +48 -16
  22. package/extensions/visual-context/model.ts +29 -3
  23. package/extensions/visual-context/projects.ts +226 -0
  24. package/extensions/visual-context/session.ts +150 -17
  25. package/extensions/visual-context/setup.ts +146 -0
  26. package/extensions/visual-context/source.ts +203 -100
  27. package/extensions/visual-context/ui.ts +5 -1
  28. package/package.json +6 -6
  29. package/skills/sideroom-guidelines/SKILL.md +8 -5
  30. package/skills/sideroom-guidelines/references/languages/go.md +35 -1
  31. package/skills/sideroom-guidelines/references/languages/java.md +34 -1
  32. package/skills/sideroom-guidelines/references/languages/javascript.md +34 -1
  33. package/skills/sideroom-guidelines/references/languages/php-laravel.md +34 -1
  34. package/skills/sideroom-guidelines/references/languages/python.md +30 -1
  35. package/skills/sideroom-guidelines/references/languages/rust.md +34 -1
  36. package/skills/sideroom-guidelines/references/languages/typescript.md +38 -1
  37. 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 { candidateIsCurrent, resolveSelection } from './source.ts';
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 commitPending({
129
+ export function bindProject({
71
130
  store,
72
131
  pi,
73
- pending,
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
- }): void {
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 = resolveSelection(cwd, selection);
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, { selection, resolution }),
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
- candidateIsCurrent(cwd, candidate),
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
+ }