@openlines/opl-pi-sht 0.2.12 → 0.3.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.
Files changed (81) hide show
  1. package/README.md +92 -84
  2. package/configs/opl-browser.json.sample +3 -2
  3. package/configs/opl-footer.json.sample +3 -2
  4. package/configs/opl-guardian.json.sample +8 -2
  5. package/configs/opl-modes.json.sample +5 -0
  6. package/configs/opl-webaccess.json.sample +7 -1
  7. package/extensions/opl-browser/README.md +81 -55
  8. package/extensions/opl-browser/browser.ts +244 -50
  9. package/extensions/opl-browser/config.ts +15 -7
  10. package/extensions/opl-browser/index.ts +5 -4
  11. package/extensions/opl-browser/package.json +4 -4
  12. package/extensions/opl-browser/validate.ts +313 -60
  13. package/extensions/opl-configurator/README.md +40 -0
  14. package/extensions/opl-configurator/configurator.ts +148 -0
  15. package/extensions/opl-configurator/index.ts +12 -0
  16. package/extensions/opl-ctxtrim/README.md +45 -14
  17. package/extensions/opl-ctxtrim/index.ts +33 -13
  18. package/extensions/opl-footer/README.md +31 -6
  19. package/extensions/opl-footer/config.ts +11 -10
  20. package/extensions/opl-footer/configure.ts +157 -115
  21. package/extensions/opl-footer/git-status.ts +58 -1
  22. package/extensions/opl-footer/icons.ts +3 -0
  23. package/extensions/opl-footer/index.ts +209 -105
  24. package/extensions/opl-footer/segments/caveman.ts +9 -1
  25. package/extensions/opl-footer/segments/chat-mode.ts +8 -1
  26. package/extensions/opl-footer/segments/compactions.ts +13 -0
  27. package/extensions/opl-footer/segments/context.ts +6 -5
  28. package/extensions/opl-footer/segments/index.ts +2 -0
  29. package/extensions/opl-footer/segments/mode-switcher.ts +14 -1
  30. package/extensions/opl-footer/segments/path.ts +1 -1
  31. package/extensions/opl-footer/segments/plan-mode.ts +8 -1
  32. package/extensions/opl-footer/theme.ts +5 -2
  33. package/extensions/opl-footer/types.ts +4 -0
  34. package/extensions/opl-guardian/README.md +17 -5
  35. package/extensions/opl-guardian/config.ts +72 -6
  36. package/extensions/opl-guardian/guardian.ts +42 -10
  37. package/extensions/opl-guardian/index.ts +52 -17
  38. package/extensions/opl-guardian/policies.ts +52 -16
  39. package/extensions/opl-init/README.md +12 -4
  40. package/extensions/opl-init/index.ts +228 -27
  41. package/extensions/opl-input/README.md +17 -3
  42. package/extensions/opl-input/config.ts +59 -5
  43. package/extensions/opl-input/configure.ts +144 -0
  44. package/extensions/opl-input/index.ts +38 -30
  45. package/extensions/opl-input/mode-style.ts +6 -2
  46. package/extensions/opl-modes/README.md +16 -4
  47. package/extensions/opl-modes/config.ts +62 -8
  48. package/extensions/opl-modes/index.ts +48 -17
  49. package/extensions/opl-modes/state.ts +50 -2
  50. package/extensions/opl-modes/types.ts +6 -0
  51. package/extensions/opl-modes/utils.ts +18 -8
  52. package/extensions/opl-questionnaire/index.ts +10 -4
  53. package/extensions/opl-questionnaire/types.ts +4 -0
  54. package/extensions/opl-simplebench/README.md +21 -15
  55. package/extensions/opl-simplebench/artifact.ts +7 -4
  56. package/extensions/opl-simplebench/benchmark.ts +96 -35
  57. package/extensions/opl-simplebench/coding.ts +9 -4
  58. package/extensions/opl-simplebench/index.ts +19 -19
  59. package/extensions/opl-simplebench/llama-server.ts +3 -2
  60. package/extensions/opl-simplebench/report.ts +0 -6
  61. package/extensions/opl-simplebench/research.ts +5 -4
  62. package/extensions/opl-simplebench/scoring.ts +4 -2
  63. package/extensions/opl-simplebench/util/config.ts +15 -8
  64. package/extensions/opl-simplebench/util/exec-env.ts +36 -0
  65. package/extensions/opl-simplebench/util/format.ts +2 -2
  66. package/extensions/opl-simplebench/util/providers.ts +23 -194
  67. package/extensions/opl-todo/README.md +5 -5
  68. package/extensions/opl-todo/config.ts +7 -2
  69. package/extensions/opl-todo/index.ts +50 -9
  70. package/extensions/opl-webaccess/README.md +31 -5
  71. package/extensions/opl-webaccess/config.ts +36 -7
  72. package/extensions/opl-webaccess/extract.ts +37 -47
  73. package/extensions/opl-webaccess/http.ts +259 -0
  74. package/extensions/opl-webaccess/index.ts +12 -6
  75. package/extensions/opl-webaccess/package.json +4 -9
  76. package/extensions/opl-webaccess/pdf.ts +4 -4
  77. package/extensions/opl-webaccess/search.ts +19 -8
  78. package/extensions/opl-webaccess/storage.ts +134 -2
  79. package/extensions/opl-webaccess/types.ts +4 -0
  80. package/extensions/opl-webaccess/utils.ts +221 -33
  81. package/package.json +19 -12
@@ -1,4 +1,4 @@
1
- import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
1
+ import type { ExtensionContext, KeybindingsManager, Theme } from "@earendil-works/pi-coding-agent";
2
2
  import { getSettingsListTheme } from "@earendil-works/pi-coding-agent";
3
3
  import { Key, matchesKey, SettingsList, truncateToWidth, type SettingItem } from "@earendil-works/pi-tui";
4
4
 
@@ -29,129 +29,171 @@ function segmentLabel(segment: string): string {
29
29
  return segment.replaceAll("_", " ");
30
30
  }
31
31
 
32
- export async function showFooterConfigurator(ctx: ExtensionContext, onSaved: () => void): Promise<void> {
33
- if (ctx.mode !== "tui") {
34
- ctx.ui.notify("/configure-opl requires TUI mode", "error");
35
- return;
32
+ // The configurator seam. opl-configurator owns the /configurator command and reads this
33
+ // globalThis array lazily; the entry shape is duplicated by hand on both sides because
34
+ // install.sh copies one extension directory at a time.
35
+ export const CONFIGURATOR_REGISTRY_KEY = "__oplConfiguratorTabs";
36
+
37
+ /** What the configurator shell hands to each tab's factory. */
38
+ export interface ConfiguratorTabApi {
39
+ ctx: ExtensionContext;
40
+ tui: { requestRender(): void };
41
+ theme: Theme;
42
+ keybindings: KeybindingsManager;
43
+ done: (result: void) => void;
44
+ }
45
+
46
+ export interface ConfiguratorTabComponent {
47
+ render(width: number): string[];
48
+ invalidate?(): void;
49
+ handleInput?(data: string): void;
50
+ }
51
+
52
+ interface ConfiguratorTab {
53
+ id: string;
54
+ hotkey: string;
55
+ label: string;
56
+ create: (api: ConfiguratorTabApi) => ConfiguratorTabComponent;
57
+ }
58
+
59
+ /** Publish the footer's settings/reorder screen as a tab, once per extension load. */
60
+ export function registerFooterConfiguratorTab(onSaved: (ctx: ExtensionContext) => void): void {
61
+ const tab: ConfiguratorTab = {
62
+ id: "opl-footer",
63
+ hotkey: "f",
64
+ label: "Footer",
65
+ create: (api) => createFooterComponent(api, onSaved),
66
+ };
67
+
68
+ const holder = globalThis as Record<string, unknown>;
69
+ const registry = holder[CONFIGURATOR_REGISTRY_KEY];
70
+ if (Array.isArray(registry)) {
71
+ if (!registry.some((entry) => entry?.id === "opl-footer")) registry.push(tab);
72
+ } else {
73
+ holder[CONFIGURATOR_REGISTRY_KEY] = [tab];
36
74
  }
75
+ }
37
76
 
77
+ function createFooterComponent(
78
+ api: ConfiguratorTabApi,
79
+ onSaved: (ctx: ExtensionContext) => void,
80
+ ): ConfiguratorTabComponent {
81
+ const { ctx, tui, theme, done } = api;
38
82
  let config = loadUserConfig() ?? {};
39
- await ctx.ui.custom<void>((tui, theme, _keybindings, done) => {
40
- let activeTab = 0;
41
- let view: "settings" | "reorder" = "settings";
42
- let reorderIndex = 0;
43
- let settingsLists: SettingsList[] = [];
44
-
45
- const persist = (next: typeof config, selectedId?: string): boolean => {
46
- try {
47
- saveUserConfig(next);
48
- config = next;
49
- settingsLists = createSettingsLists();
50
- if (selectedId) restoreSelectedItem(settingsLists, activeTab, selectedId);
51
- onSaved();
52
- return true;
53
- } catch (error) {
54
- const message = error instanceof Error ? error.message : String(error);
55
- ctx.ui.notify(`Could not save footer config: ${message}`, "error");
56
- done(undefined);
57
- return false;
58
- }
59
- };
60
-
61
- const save = (id: string, value: string) => {
62
- const [key, segment, kind] = id.split(":") as [FooterLayoutKey, typeof CONFIGURABLE_SEGMENTS[number], string];
63
- const next = kind === "separator"
64
- ? setSegmentSeparator(config, key, segment, value === "shown")
65
- : setLayoutSegment(config, key, segment, value === "shown");
66
- persist(next, id);
67
- };
68
-
69
- const createSettingsLists = (): SettingsList[] => FOOTER_LAYOUT_KEYS.map((key) => {
70
- const layout = getLayoutSegments(config, key);
71
- const items: SettingItem[] = CONFIGURABLE_SEGMENTS.flatMap((segment) => {
72
- const index = layout.indexOf(segment);
73
- return [
74
- {
75
- id: `${key}:${segment}:segment`,
76
- label: segmentLabel(segment),
77
- currentValue: index === -1 ? "hidden" : "shown",
78
- values: ["shown", "hidden"],
79
- },
80
- {
81
- id: `${key}:${segment}:separator`,
82
- label: `${segmentLabel(segment)} separator`,
83
- currentValue: hasSegmentSeparator(config, key, segment) ? "shown" : "hidden",
84
- values: ["shown", "hidden"],
85
- },
86
- ];
87
- });
88
- return new SettingsList(items, 15, getSettingsListTheme(), save, () => done(undefined));
89
- });
83
+ let activeTab = 0;
84
+ let view: "settings" | "reorder" = "settings";
85
+ let reorderIndex = 0;
86
+ let settingsLists: SettingsList[] = [];
90
87
 
91
- const visibleSegments = (): typeof CONFIGURABLE_SEGMENTS[number][] =>
92
- getLayoutSegments(config, FOOTER_LAYOUT_KEYS[activeTab]!).filter((segment) => CONFIGURABLE_SEGMENTS.includes(segment));
88
+ const persist = (next: typeof config, selectedId?: string): boolean => {
89
+ try {
90
+ saveUserConfig(next);
91
+ config = next;
92
+ settingsLists = createSettingsLists();
93
+ if (selectedId) restoreSelectedItem(settingsLists, activeTab, selectedId);
94
+ onSaved(ctx);
95
+ return true;
96
+ } catch (error) {
97
+ const message = error instanceof Error ? error.message : String(error);
98
+ ctx.ui.notify(`Could not save footer config: ${message}`, "error");
99
+ done(undefined);
100
+ return false;
101
+ }
102
+ };
93
103
 
94
- settingsLists = createSettingsLists();
104
+ const save = (id: string, value: string) => {
105
+ const [key, segment, kind] = id.split(":") as [FooterLayoutKey, typeof CONFIGURABLE_SEGMENTS[number], string];
106
+ const next = kind === "separator"
107
+ ? setSegmentSeparator(config, key, segment, value === "shown")
108
+ : setLayoutSegment(config, key, segment, value === "shown");
109
+ persist(next, id);
110
+ };
95
111
 
96
- return {
97
- render(width: number) {
98
- const tabs = FOOTER_LAYOUT_KEYS.map((key, index) =>
99
- theme.fg(index === activeTab ? "accent" : "dim", `[${LAYOUT_LABELS[key]}]`),
100
- ).join(" ");
101
- const header = [
102
- truncateToWidth(theme.bold("Configure OPL Footer"), width),
103
- truncateToWidth(tabs, width),
104
- ];
105
- if (view === "settings") {
106
- return [
107
- ...header,
108
- truncateToWidth(theme.fg("dim", "←/→ switch layout · r reorder · Changes apply immediately"), width),
109
- ...settingsLists[activeTab]!.render(width),
110
- ];
111
- }
112
+ const createSettingsLists = (): SettingsList[] => FOOTER_LAYOUT_KEYS.map((key) => {
113
+ const layout = getLayoutSegments(config, key);
114
+ const items: SettingItem[] = CONFIGURABLE_SEGMENTS.flatMap((segment) => {
115
+ const index = layout.indexOf(segment);
116
+ return [
117
+ {
118
+ id: `${key}:${segment}:segment`,
119
+ label: segmentLabel(segment),
120
+ currentValue: index === -1 ? "hidden" : "shown",
121
+ values: ["shown", "hidden"],
122
+ },
123
+ {
124
+ id: `${key}:${segment}:separator`,
125
+ label: `${segmentLabel(segment)} separator`,
126
+ currentValue: hasSegmentSeparator(config, key, segment) ? "shown" : "hidden",
127
+ values: ["shown", "hidden"],
128
+ },
129
+ ];
130
+ });
131
+ return new SettingsList(items, 15, getSettingsListTheme(), save, () => done(undefined));
132
+ });
112
133
 
113
- const segments = visibleSegments();
114
- reorderIndex = Math.min(reorderIndex, Math.max(0, segments.length - 1));
115
- const rows = segments.length === 0
116
- ? [theme.fg("dim", "No visible standard segments.")]
117
- : segments.map((segment, index) => {
118
- const text = `${index === reorderIndex ? "›" : " "} ${segmentLabel(segment)}${hasSegmentSeparator(config, FOOTER_LAYOUT_KEYS[activeTab]!, segment) ? " + separator" : ""}`;
119
- return index === reorderIndex ? theme.fg("accent", text) : text;
120
- });
134
+ const visibleSegments = (): typeof CONFIGURABLE_SEGMENTS[number][] =>
135
+ getLayoutSegments(config, FOOTER_LAYOUT_KEYS[activeTab]!).filter((segment) => CONFIGURABLE_SEGMENTS.includes(segment));
136
+
137
+ settingsLists = createSettingsLists();
138
+
139
+ return {
140
+ render(width: number) {
141
+ const tabs = FOOTER_LAYOUT_KEYS.map((key, index) =>
142
+ theme.fg(index === activeTab ? "accent" : "dim", `[${LAYOUT_LABELS[key]}]`),
143
+ ).join(" ");
144
+ const header = [
145
+ truncateToWidth(theme.bold("Configure OPL Footer"), width),
146
+ truncateToWidth(tabs, width),
147
+ ];
148
+ if (view === "settings") {
121
149
  return [
122
150
  ...header,
123
- truncateToWidth(theme.fg("dim", "↑/↓ select · ,/. move · r settings · esc close"), width),
124
- ...rows.map((row) => truncateToWidth(row, width)),
151
+ truncateToWidth(theme.fg("dim", "←/→ switch layout · r reorder · Changes apply immediately"), width),
152
+ ...settingsLists[activeTab]!.render(width),
125
153
  ];
126
- },
127
- invalidate() { settingsLists.forEach((list) => list.invalidate()); },
128
- handleInput(data: string) {
129
- if (matchesKey(data, Key.left)) {
130
- activeTab = nextTabIndex(activeTab, "left", FOOTER_LAYOUT_KEYS.length);
131
- reorderIndex = 0;
132
- } else if (matchesKey(data, Key.right)) {
133
- activeTab = nextTabIndex(activeTab, "right", FOOTER_LAYOUT_KEYS.length);
134
- reorderIndex = 0;
135
- } else if (view === "reorder") {
136
- const segments = visibleSegments();
137
- if (data === "r") view = "settings";
138
- else if (matchesKey(data, Key.escape)) done(undefined);
139
- else if (matchesKey(data, Key.up) && segments.length > 0) reorderIndex = (reorderIndex + segments.length - 1) % segments.length;
140
- else if (matchesKey(data, Key.down) && segments.length > 0) reorderIndex = (reorderIndex + 1) % segments.length;
141
- else if ((data === "," || data === ".") && segments[reorderIndex]) {
142
- const next = moveLayoutSegment(config, FOOTER_LAYOUT_KEYS[activeTab]!, segments[reorderIndex]!, data === "," ? "up" : "down");
143
- if (next !== config && persist(next)) {
144
- reorderIndex = data === "," ? Math.max(0, reorderIndex - 1) : Math.min(segments.length - 1, reorderIndex + 1);
145
- }
154
+ }
155
+
156
+ const segments = visibleSegments();
157
+ reorderIndex = Math.min(reorderIndex, Math.max(0, segments.length - 1));
158
+ const rows = segments.length === 0
159
+ ? [theme.fg("dim", "No visible standard segments.")]
160
+ : segments.map((segment, index) => {
161
+ const text = `${index === reorderIndex ? "›" : " "} ${segmentLabel(segment)}${hasSegmentSeparator(config, FOOTER_LAYOUT_KEYS[activeTab]!, segment) ? " + separator" : ""}`;
162
+ return index === reorderIndex ? theme.fg("accent", text) : text;
163
+ });
164
+ return [
165
+ ...header,
166
+ truncateToWidth(theme.fg("dim", "↑/↓ select · ,/. move · r settings · esc close"), width),
167
+ ...rows.map((row) => truncateToWidth(row, width)),
168
+ ];
169
+ },
170
+ invalidate() { settingsLists.forEach((list) => list.invalidate()); },
171
+ handleInput(data: string) {
172
+ if (matchesKey(data, Key.left)) {
173
+ activeTab = nextTabIndex(activeTab, "left", FOOTER_LAYOUT_KEYS.length);
174
+ reorderIndex = 0;
175
+ } else if (matchesKey(data, Key.right)) {
176
+ activeTab = nextTabIndex(activeTab, "right", FOOTER_LAYOUT_KEYS.length);
177
+ reorderIndex = 0;
178
+ } else if (view === "reorder") {
179
+ const segments = visibleSegments();
180
+ if (data === "r") view = "settings";
181
+ else if (matchesKey(data, Key.escape)) done(undefined);
182
+ else if (matchesKey(data, Key.up) && segments.length > 0) reorderIndex = (reorderIndex + segments.length - 1) % segments.length;
183
+ else if (matchesKey(data, Key.down) && segments.length > 0) reorderIndex = (reorderIndex + 1) % segments.length;
184
+ else if ((data === "," || data === ".") && segments[reorderIndex]) {
185
+ const next = moveLayoutSegment(config, FOOTER_LAYOUT_KEYS[activeTab]!, segments[reorderIndex]!, data === "," ? "up" : "down");
186
+ if (next !== config && persist(next)) {
187
+ reorderIndex = data === "," ? Math.max(0, reorderIndex - 1) : Math.min(segments.length - 1, reorderIndex + 1);
146
188
  }
147
- } else if (data === "r") {
148
- view = "reorder";
149
- reorderIndex = 0;
150
- } else {
151
- settingsLists[activeTab]!.handleInput?.(data);
152
189
  }
153
- tui.requestRender();
154
- },
155
- };
156
- });
157
- }
190
+ } else if (data === "r") {
191
+ view = "reorder";
192
+ reorderIndex = 0;
193
+ } else {
194
+ settingsLists[activeTab]!.handleInput?.(data);
195
+ }
196
+ tui.requestRender();
197
+ },
198
+ };
199
+ }
@@ -78,9 +78,66 @@ function parseGitStatusOutput(output: string): { staged: number; unstaged: numbe
78
78
  return { staged, unstaged, untracked };
79
79
  }
80
80
 
81
+ // BEGIN SHARED GIT GUARD
82
+ // A repository is attacker-chosen data: its local config, `.gitattributes` and system
83
+ // config can name programs that git runs while answering what looks like a read-only query.
84
+ // Measured on git 2.54: `git diff HEAD` executes a declared `[diff "x"] textconv` driver,
85
+ // while `--name-only`, `status --porcelain`, `ls-files` and `rev-parse` do not. Every call
86
+ // still goes through this guard so a future content-diff call cannot reintroduce the
87
+ // execution path. Duplicated per extension because install.sh installs one directory at a
88
+ // time; tests/net-guard-parity.test.mjs fails if the copies drift.
89
+ const SAFE_GIT_GLOBAL_ARGS = [
90
+ "--no-pager",
91
+ "-c",
92
+ "core.fsmonitor=false",
93
+ "-c",
94
+ "core.hooksPath=",
95
+ "-c",
96
+ "protocol.ext.allow=never",
97
+ "-c",
98
+ "credential.helper=",
99
+ ];
100
+
101
+ /** Refuse external content handlers. Diff-family subcommands only: plumbing rejects them. */
102
+ const SAFE_GIT_DIFF_FLAGS = ["--no-ext-diff", "--no-textconv"];
103
+ const GIT_DIFF_COMMANDS = new Set(["diff", "log", "show", "stash", "range-diff", "format-patch"]);
104
+
105
+ const SAFE_GIT_ENV: Record<string, string> = {
106
+ // /etc/gitconfig and the user's own config can name helpers; a repo probe must not read them.
107
+ GIT_CONFIG_NOSYSTEM: "1",
108
+ GIT_ATTR_NOSYSTEM: "1",
109
+ GIT_PAGER: "cat",
110
+ // A credential prompt would hang the caller instead of failing fast.
111
+ GIT_TERMINAL_PROMPT: "0",
112
+ GIT_ASKPASS: "",
113
+ SSH_ASKPASS: "",
114
+ // Read-only plumbing must not create or wait on index.lock.
115
+ GIT_OPTIONAL_LOCKS: "0",
116
+ };
117
+
118
+ export function safeGitInvocation(args: string[]): string[] {
119
+ const [subcommand, ...rest] = args;
120
+ const diffFlags = subcommand && GIT_DIFF_COMMANDS.has(subcommand) ? SAFE_GIT_DIFF_FLAGS : [];
121
+ // A missing subcommand must not reach execFileSync/spawn as an undefined argv slot.
122
+ return subcommand
123
+ ? [...SAFE_GIT_GLOBAL_ARGS, subcommand, ...diffFlags, ...rest]
124
+ : [...SAFE_GIT_GLOBAL_ARGS, ...rest];
125
+ }
126
+
127
+ export function gitGuardEnv(base: Record<string, string | undefined> = process.env): Record<string, string> {
128
+ const env: Record<string, string> = {};
129
+ for (const [key, value] of Object.entries(base)) {
130
+ if (typeof value === "string") env[key] = value;
131
+ }
132
+ return { ...env, ...SAFE_GIT_ENV };
133
+ }
134
+ // END SHARED GIT GUARD
135
+
81
136
  function runGit(args: string[], timeoutMs = 200): Promise<string | null> {
137
+ // Porcelain/name-only only; the guard above refuses patch content.
82
138
  return new Promise((resolve) => {
83
- const proc = spawn("git", args, {
139
+ const proc = spawn("git", safeGitInvocation(args), {
140
+ env: gitGuardEnv(),
84
141
  stdio: ["ignore", "pipe", "pipe"],
85
142
  });
86
143
 
@@ -3,6 +3,7 @@ export interface IconSet {
3
3
  folder: string;
4
4
  branch: string;
5
5
  git: string;
6
+ compress: string;
6
7
  separator: string;
7
8
  }
8
9
 
@@ -12,6 +13,7 @@ export const NERD_ICONS: IconSet = {
12
13
  folder: "\uF115", // nf-fa-folder_open
13
14
  branch: "\uF126", // nf-fa-code_fork
14
15
  git: "\uF1D3", // nf-fa-git
16
+ compress: "\u{F05C4}", // nf-md-zip_box
15
17
  separator: "|",
16
18
  };
17
19
 
@@ -21,6 +23,7 @@ export const ASCII_ICONS: IconSet = {
21
23
  folder: "📂",
22
24
  branch: "⎇",
23
25
  git: "⎇",
26
+ compress: "📦",
24
27
  separator: "|",
25
28
  };
26
29