pi-umbra 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.
Files changed (66) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +27 -0
  3. package/node_modules/pi-umbra-ask/LICENSE +21 -0
  4. package/node_modules/pi-umbra-ask/README.md +21 -0
  5. package/node_modules/pi-umbra-ask/checks/umbra-ask.check.ts +132 -0
  6. package/node_modules/pi-umbra-ask/extensions/umbra-ask.ts +484 -0
  7. package/node_modules/pi-umbra-ask/lib/clean.ts +25 -0
  8. package/node_modules/pi-umbra-ask/package.json +37 -0
  9. package/node_modules/pi-umbra-copy-chat/LICENSE +21 -0
  10. package/node_modules/pi-umbra-copy-chat/README.md +19 -0
  11. package/node_modules/pi-umbra-copy-chat/extensions/umbra-copy-chat.ts +154 -0
  12. package/node_modules/pi-umbra-copy-chat/package.json +32 -0
  13. package/node_modules/pi-umbra-help/LICENSE +21 -0
  14. package/node_modules/pi-umbra-help/README.md +21 -0
  15. package/node_modules/pi-umbra-help/checks/umbra-help.check.ts +108 -0
  16. package/node_modules/pi-umbra-help/extensions/umbra-help.ts +223 -0
  17. package/node_modules/pi-umbra-help/lib/umbra-settings.ts +52 -0
  18. package/node_modules/pi-umbra-help/package.json +34 -0
  19. package/node_modules/pi-umbra-inputbar/LICENSE +21 -0
  20. package/node_modules/pi-umbra-inputbar/README.md +19 -0
  21. package/node_modules/pi-umbra-inputbar/checks/umbra-inputbar.check.ts +53 -0
  22. package/node_modules/pi-umbra-inputbar/extensions/umbra-inputbar.ts +130 -0
  23. package/node_modules/pi-umbra-inputbar/package.json +33 -0
  24. package/node_modules/pi-umbra-preview/LICENSE +21 -0
  25. package/node_modules/pi-umbra-preview/README.md +32 -0
  26. package/node_modules/pi-umbra-preview/checks/umbra-preview.check.ts +38 -0
  27. package/node_modules/pi-umbra-preview/extensions/umbra-preview.ts +147 -0
  28. package/node_modules/pi-umbra-preview/package.json +33 -0
  29. package/node_modules/pi-umbra-rename/LICENSE +21 -0
  30. package/node_modules/pi-umbra-rename/README.md +19 -0
  31. package/node_modules/pi-umbra-rename/checks/umbra-rename.check.ts +55 -0
  32. package/node_modules/pi-umbra-rename/extensions/umbra-rename.ts +22 -0
  33. package/node_modules/pi-umbra-rename/package.json +33 -0
  34. package/node_modules/pi-umbra-shimmer/LICENSE +21 -0
  35. package/node_modules/pi-umbra-shimmer/README.md +19 -0
  36. package/node_modules/pi-umbra-shimmer/checks/umbra-shimmer.check.ts +89 -0
  37. package/node_modules/pi-umbra-shimmer/extensions/umbra-shimmer.ts +183 -0
  38. package/node_modules/pi-umbra-shimmer/package.json +33 -0
  39. package/node_modules/pi-umbra-skill-matcher/LICENSE +21 -0
  40. package/node_modules/pi-umbra-skill-matcher/README.md +19 -0
  41. package/node_modules/pi-umbra-skill-matcher/checks/umbra-skill-matcher.check.ts +94 -0
  42. package/node_modules/pi-umbra-skill-matcher/extensions/umbra-skill-matcher.ts +124 -0
  43. package/node_modules/pi-umbra-skill-matcher/package.json +35 -0
  44. package/node_modules/pi-umbra-theme/LICENSE +21 -0
  45. package/node_modules/pi-umbra-theme/README.md +38 -0
  46. package/node_modules/pi-umbra-theme/checks/umbra-background.check.ts +158 -0
  47. package/node_modules/pi-umbra-theme/checks/umbra-footer.check.ts +72 -0
  48. package/node_modules/pi-umbra-theme/checks/umbra-gutter.check.ts +59 -0
  49. package/node_modules/pi-umbra-theme/checks/umbra-toolbox.check.ts +71 -0
  50. package/node_modules/pi-umbra-theme/checks/umbra-working.check.ts +120 -0
  51. package/node_modules/pi-umbra-theme/extensions/umbra-background.ts +155 -0
  52. package/node_modules/pi-umbra-theme/extensions/umbra-footer.ts +150 -0
  53. package/node_modules/pi-umbra-theme/extensions/umbra-gutter.ts +35 -0
  54. package/node_modules/pi-umbra-theme/extensions/umbra-toolbox.ts +129 -0
  55. package/node_modules/pi-umbra-theme/extensions/umbra-working.ts +209 -0
  56. package/node_modules/pi-umbra-theme/lib/umbra-format.ts +16 -0
  57. package/node_modules/pi-umbra-theme/lib/umbra-settings.ts +52 -0
  58. package/node_modules/pi-umbra-theme/package.json +45 -0
  59. package/node_modules/pi-umbra-theme/themes/umbra-astral-veil.json +88 -0
  60. package/node_modules/pi-umbra-theme/themes/umbra-deep-current.json +88 -0
  61. package/node_modules/pi-umbra-theme/themes/umbra-ember-ash.json +88 -0
  62. package/node_modules/pi-umbra-theme/themes/umbra-onyx-slate.json +88 -0
  63. package/node_modules/pi-umbra-theme/themes/umbra-tidal-drift.json +88 -0
  64. package/node_modules/pi-umbra-theme/themes/umbra-venom-dusk.json +88 -0
  65. package/node_modules/pi-umbra-theme/themes/umbra-violet-forge.json +88 -0
  66. package/package.json +68 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 grkn
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,27 @@
1
+ # pi-umbra
2
+
3
+ Every pi-umbra package in one install.
4
+
5
+ ```sh
6
+ pi install npm:pi-umbra
7
+ ```
8
+
9
+ ```
10
+ pi-umbra
11
+ ├── pi-umbra-theme
12
+ ├── pi-umbra-ask
13
+ ├── pi-umbra-copy-chat
14
+ ├── pi-umbra-help
15
+ ├── pi-umbra-inputbar
16
+ ├── pi-umbra-preview
17
+ ├── pi-umbra-rename
18
+ ├── pi-umbra-shimmer
19
+ └── pi-umbra-skill-matcher
20
+ ```
21
+
22
+ Each one also installs on its own; see the [repository](https://github.com/grknbyk/pi-umbra).
23
+
24
+ `pi-umbra-shimmer` and `pi-umbra-skill-matcher` reach past pi's extension API and do
25
+ nothing without a bundle patch, which is not in this repo.
26
+
27
+ MIT.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 grkn
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,21 @@
1
+ # pi-umbra-ask
2
+
3
+ A tool the model can call to ask you a question mid-turn. Answers are stripped of control and bidi characters before any renderer sees them.
4
+
5
+ ```
6
+ pi-umbra-ask/
7
+ ├── checks
8
+ │ └── umbra-ask.check.ts
9
+ ├── extensions
10
+ │ └── umbra-ask.ts
11
+ └── lib
12
+ └── clean.ts
13
+ ```
14
+
15
+ ## Install
16
+
17
+ ```sh
18
+ pi install npm:pi-umbra-ask
19
+ ```
20
+
21
+ MIT.
@@ -0,0 +1,132 @@
1
+ // The one runnable check for ask.ts, the measured 375-token win. It asserts the three
2
+ import { readFileSync } from "node:fs";
3
+ // things a pi upgrade can silently break: the tool registers under its name with the
4
+ // schema the model is prompted with, it drops itself from the active set when there is
5
+ // no UI to answer in, and a call with no TUI returns a sentence rather than hanging.
6
+ //
7
+ // Run it with: bun run umbra-ask.check.ts (or: node --experimental-strip-types ask.check.ts)
8
+ import assert from "node:assert/strict";
9
+ import ask from "../extensions/umbra-ask.ts";
10
+
11
+ type Tool = {
12
+ name: string;
13
+ parameters: { properties: Record<string, any> };
14
+ execute: (id: string, params: unknown, signal: unknown, onUpdate: unknown, ctx: unknown) => Promise<{ content: { text: string }[] }>;
15
+ };
16
+
17
+ let tool: Tool | undefined;
18
+ let activeSet: string[] | undefined;
19
+ const handlers: Record<string, (event: unknown, ctx: unknown) => unknown> = {};
20
+ const pi = {
21
+ on: (name: string, fn: (event: unknown, ctx: unknown) => unknown) => (handlers[name] = fn),
22
+ registerTool: (t: Tool) => (tool = t),
23
+ getActiveTools: () => ["read", "ask_user_question", "bash"],
24
+ setActiveTools: (names: string[]) => (activeSet = names),
25
+ };
26
+
27
+ ask(pi as never);
28
+ assert.ok(tool, "registers a tool");
29
+ assert.equal(tool!.name, "ask_user_question");
30
+
31
+ const questions = tool!.parameters.properties.questions;
32
+ assert.equal(questions.minItems, 1);
33
+ assert.equal(questions.maxItems, 4, "up to four questions");
34
+ const options = questions.items.properties.options;
35
+ assert.equal(options.minItems, 2);
36
+ assert.equal(options.maxItems, 8, "eight options: the open row makes nine, the last single digit");
37
+ assert.equal(questions.items.properties.recommended.minimum, 0, "recommended is 0-based");
38
+
39
+ // Headless: the tool costs 405 prompt tokens nobody can answer, so it deactivates itself.
40
+ handlers.before_agent_start({}, { hasUI: false });
41
+ assert.deepEqual(activeSet, ["read", "bash"], "no UI: removed from the active set, nothing else touched");
42
+ activeSet = undefined;
43
+ handlers.before_agent_start({}, { hasUI: true });
44
+ assert.equal(activeSet, undefined, "with a UI: left alone");
45
+
46
+ // Called outside the TUI it answers in one sentence instead of opening a dialog.
47
+ const result = await tool!.execute(
48
+ "call-1",
49
+ { questions: [{ question: "Which?", header: "Pick", options: [{ label: "a" }, { label: "b" }], recommended: 1 }] },
50
+ undefined,
51
+ undefined,
52
+ { mode: "rpc" },
53
+ );
54
+ assert.match(result.content[0].text, /No interactive UI/);
55
+
56
+ console.log("umbra-ask.check.ts ok - name, schema bounds, headless deactivation, no-TUI answer");
57
+
58
+ // --- preview column ---------------------------------------------------------------------------
59
+ // The one piece of arithmetic the preview added. Rows carry ANSI colour, so their length in
60
+ // characters is not their width on screen; getting that wrong tilts the whole right column.
61
+ {
62
+ const { beside, padTo } = await import("../extensions/umbra-ask.ts");
63
+ const RED = "\x1b[31m";
64
+ const OFF = "\x1b[39m";
65
+
66
+ assert.equal(padTo(`${RED}abc${OFF}`, 6), `${RED}abc${OFF} `, "padding must count visible width, not bytes");
67
+ assert.equal(padTo("abcdef", 3), "abcdef", "an over-wide line is left alone rather than cut here");
68
+
69
+ const merged = beside([`${RED}one${OFF}`, "two"], ["A"], 5, (gap: string) => gap);
70
+ assert.equal(merged.length, 2, "the taller column decides the height");
71
+ assert.equal(merged[0], `${RED}one${OFF} \u2502 A`, `row 0 misaligned: ${JSON.stringify(merged[0])}`);
72
+ assert.equal(merged[1], "two", "past the last preview row the separator stops rather than trailing");
73
+
74
+ // The row under the cursor is often "none of these", which has no sample. An empty right
75
+ // column must produce no separator at all, not a column of bars with nothing after them.
76
+ assert.deepEqual(beside(["a", "b"], [], 5, (gap: string) => gap), ["a", "b"], "no sample means no rule");
77
+
78
+ const tall = beside(["x"], ["A", "B", "C"], 3, (gap: string) => gap);
79
+ assert.equal(tall.length, 3, "a taller preview also decides the height");
80
+ assert.equal(tall[2], ` │ C`, "left stays padded under its last row");
81
+
82
+ console.log("umbra-ask.check.ts ok - preview column alignment");
83
+ }
84
+
85
+ // --- wrapping ---------------------------------------------------------------------------------
86
+ // A narrow column is the point of wrapping, so the cases that matter are the awkward ones: a word
87
+ // that does not fit at all, and a column too small to be a column.
88
+ {
89
+ const { wrap } = await import("../extensions/umbra-ask.ts");
90
+
91
+ assert.deepEqual(wrap("one two three", 7), ["one two", "three"], "breaks at the space");
92
+ assert.deepEqual(wrap("one two three", 40), ["one two three"], "a line that fits is left whole");
93
+ assert.deepEqual(wrap("", 10), [""], "empty text stays one empty line, never zero");
94
+
95
+ // No line may exceed the column, which is the invariant the preview alignment depends on.
96
+ const long = wrap("supercalifragilistic word", 8);
97
+ assert.ok(long.every((line) => line.length <= 8), `over-wide line: ${JSON.stringify(long)}`);
98
+ assert.equal(long.join("").replace(/ /g, ""), "supercalifragilisticword", "no character is lost");
99
+
100
+ // Turkish is measured, not assumed: these are single-cell characters and must not wrap early.
101
+ assert.deepEqual(wrap("ışık ığdır", 9), ["ışık", "ığdır"], "Turkish letters count as one column");
102
+
103
+ // A column of zero cannot wrap anything, so the text comes back untouched rather than sliced
104
+ // into an infinite list of empty strings.
105
+ assert.deepEqual(wrap("abc", 0), ["abc"], "a zero column returns the text unwrapped");
106
+
107
+ console.log("umbra-ask.check.ts ok - wrapping");
108
+ }
109
+
110
+ // A preview survives sanitisation, and reaches the renderer at all.
111
+ {
112
+ const { clean, cleanBlock } = await import("../lib/clean.ts");
113
+ const art = "+---+\n| a |\n+---+";
114
+
115
+ // Why cleanBlock has to exist: \n sits inside the C0 range clean sweeps, so the plain filter
116
+ // folds an ASCII mockup onto one row.
117
+ assert.equal(clean(art), "+---+| a |+---+", "clean is expected to drop newlines");
118
+ assert.equal(cleanBlock(art), art, "cleanBlock must keep the block a block");
119
+ assert.equal(cleanBlock("a\x1b[31mb\rc"), "a[31mbc", "everything else in C0/C1 still goes");
120
+
121
+ // The trap this check exists for: execute() rebuilds each option from a whitelist, so a field
122
+ // added to the schema and to the renderer but not to that line arrives as undefined and draws
123
+ // nothing, with no error anywhere. That is what happened to `preview`.
124
+ const source = readFileSync(new URL("../extensions/umbra-ask.ts", import.meta.url), "utf8");
125
+ const whitelist = /options: q\.options\.map\(\(o\) => \(\{([^]*?)\}\)\)/.exec(source)?.[1];
126
+ assert.ok(whitelist, "the option whitelist must be findable, or this check asserts nothing");
127
+ for (const field of ["label", "consequence", "preview"]) {
128
+ assert.ok(whitelist.includes(field + ":"), "the whitelist drops " + field + " before the renderer sees it");
129
+ }
130
+
131
+ console.log("umbra-ask.check.ts ok - preview reaches the renderer");
132
+ }