@groeponline/pi-wishcraft 1.0.0 → 1.0.2

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/CHANGELOG.md CHANGED
@@ -2,6 +2,13 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [1.0.2] - 2026-08-22
6
+
7
+ ### Changed
8
+ - Release workflow pins `actions/checkout` and `actions/setup-node` to commit SHAs, so the publish lane cannot pick up a moved tag. Publish stays provenance-free by design: the contract test keeps `id-token: write` out of `release.yml`.
9
+
10
+ ## [1.0.1] - 2026-08-20
11
+
5
12
  ## [1.0.0] - 2026-08-20
6
13
 
7
14
  ### Added
package/README.md CHANGED
@@ -39,6 +39,7 @@ Then restart pi or `/reload`. Peer range is `@earendil-works/pi-coding-agent` `>
39
39
  | `/skills` | Overlay search on name, description, and path. Enter inserts. `/skills doctor` is the health table. `/skills new` writes a SKILL.md from a template. |
40
40
  | `!cmd` / bash mode | Managed shell with ghost suggestions from project history. No shell-native completion probes. |
41
41
  | Hooks + repairs | Command hooks on pi events. Custom-tool input repairs before execution. Kill-switch: `wishcraft.hooksEnabled`. |
42
+ | Read hints | Appends a one-line continuation hint after a partial `read`, so the model knows the next offset. Opt-out: `wishcraft.readHints: false`. |
42
43
  | Policy | In-process deny/inject rules in global settings. No spawn. Kill-switch: `wishcraft.policyEnabled`. |
43
44
 
44
45
  Pi owns the footer chrome, feed scrolling, and input. Wishcraft supplies widgets, overlays, and the bash/stash/editor integrations. The bar is not clickable; actions are commands and overlays.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@groeponline/pi-wishcraft",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "Wishcraft cockpit for the pi coding agent — powerline status, vibes, idea inbox, bash mode, and full customization.",
5
5
  "type": "module",
6
6
  "files": [
package/queue/store.ts CHANGED
@@ -432,6 +432,10 @@ export class PowerlineQueueStore {
432
432
  }
433
433
 
434
434
  if (archived.length > 0) {
435
+ // ponytail: commit the inbox (now without the archived items) before
436
+ // the archive. If the archive write fails, the items stay in the inbox
437
+ // and are re-archived next run instead of being duplicated.
438
+ this.writeItems(kept, now);
435
439
  mkdirSync(dirname(this.archivePath), { recursive: true });
436
440
  const existing = existsSync(this.archivePath)
437
441
  ? readFileSync(this.archivePath, "utf-8")
@@ -441,7 +445,6 @@ export class PowerlineQueueStore {
441
445
  this.archivePath,
442
446
  `${existing.trimEnd() ? `${existing.trimEnd()}\n` : ""}${lines}\n`,
443
447
  );
444
- this.writeItems(kept, now);
445
448
  }
446
449
 
447
450
  return { archived: archived.length, remainingSent };
@@ -112,18 +112,24 @@ export function runHookCommand(
112
112
  child.on("close", (code) => finish(code));
113
113
  // Hooks that exit before reading stdin (typical `exit 2` deny scripts)
114
114
  // close the pipe; ignore EPIPE so the harness still records the exit code.
115
- child.stdin?.on("error", (error: NodeJS.ErrnoException) => {
115
+ // One idempotent handler for the async stream event, the write callback,
116
+ // and the synchronous catch: EPIPE is expected (a hook that exits before
117
+ // reading stdin) and is ignored so its exit code still settles us; any
118
+ // other failure is recorded and settles the hook once.
119
+ const onStdinError = (error: NodeJS.ErrnoException): void => {
116
120
  if (error.code === "EPIPE") return;
117
- stderr += String(error);
118
- });
121
+ stderr += `wishcraft hook stdin error: ${error.message ?? String(error)}\n`;
122
+ finish(null);
123
+ };
119
124
 
125
+ child.stdin?.on("error", onStdinError);
120
126
  try {
121
- child.stdin?.write(JSON.stringify(payload));
127
+ child.stdin?.write(JSON.stringify(payload), (err) => {
128
+ if (err) onStdinError(err as NodeJS.ErrnoException);
129
+ });
122
130
  child.stdin?.end();
123
131
  } catch (error) {
124
- if ((error as NodeJS.ErrnoException).code !== "EPIPE") {
125
- stderr += String(error);
126
- }
132
+ onStdinError(error as NodeJS.ErrnoException);
127
133
  }
128
134
  });
129
135
  }
@@ -16,14 +16,13 @@ const CORE_SHOWING_LINES =
16
16
  /^\[Showing lines \d+[–-]\d+(?: of \d+)?\.(?: Use offset=\d+ to continue\.)?\]$/i;
17
17
  const CORE_MORE_LINES =
18
18
  /^\[?\d+ more lines in file\. Use offset=\d+ to continue\.?\]?$/i;
19
- const OWN_READ_HINT = /^\d+ lines, showing \d+[–-]\d+, next offset \d+$/;
19
+ const OWN_READ_HINT = /^\[wishcraft\] \d+ lines, showing \d+[–-]\d+, next offset \d+$/;
20
20
 
21
21
  function isCoreRangeFooter(line: string): boolean {
22
22
  const footer = line.trim();
23
23
  return (
24
24
  CORE_SHOWING_LINES.test(footer) ||
25
- CORE_MORE_LINES.test(footer) ||
26
- OWN_READ_HINT.test(footer)
25
+ CORE_MORE_LINES.test(footer)
27
26
  );
28
27
  }
29
28
 
@@ -33,6 +32,10 @@ function coreFooter(text: string): string {
33
32
  return lines[lines.length - 1]?.trim() ?? "";
34
33
  }
35
34
 
35
+ function hasOwnReadHint(text: string): boolean {
36
+ return OWN_READ_HINT.test(coreFooter(text));
37
+ }
38
+
36
39
  /** Default on. `wishcraft.readHints: false` is the opt-out. */
37
40
  export function readHintsEnabled(wishcraftSettings: unknown): boolean {
38
41
  if (
@@ -70,6 +73,7 @@ export function shouldAppendReadHint(
70
73
  if (!input) return false;
71
74
  if (input.offset === undefined && input.limit === undefined) return false;
72
75
  if (coreReadResultHasRangeSummary(text, details)) return false;
76
+ if (hasOwnReadHint(text)) return false;
73
77
 
74
78
  const lineCount = countContentLines(text);
75
79
  if (lineCount === 0) return false;
@@ -77,10 +81,15 @@ export function shouldAppendReadHint(
77
81
  const start = input.offset ?? 1;
78
82
  const end = start + lineCount - 1;
79
83
  const total = details?.truncation?.totalLines;
80
- if (total !== undefined && end >= total) return false;
81
- if (input.limit !== undefined && lineCount < input.limit) return false;
82
-
83
- return true;
84
+ if (total !== undefined) {
85
+ // With a known file length we can tell whether the window reached EOF.
86
+ return end < total;
87
+ }
88
+ // Without a total we can only infer more content when the window filled the
89
+ // requested limit exactly; otherwise the read likely reached EOF and we must
90
+ // not point the model past the end of the file.
91
+ if (input.limit === undefined) return false;
92
+ return lineCount >= input.limit;
84
93
  }
85
94
 
86
95
  /** Format the English continuation hint for a partial read window. */
@@ -95,9 +104,9 @@ export function formatReadHint(
95
104
  const next = end + 1;
96
105
  const total = details?.truncation?.totalLines;
97
106
  if (total !== undefined) {
98
- return `${total} lines, showing ${start}–${end}, next offset ${next}`;
107
+ return `[wishcraft] ${total} lines, showing ${start}–${end}, next offset ${next}`;
99
108
  }
100
- return `${lineCount} lines, showing ${start}–${end}, next offset ${next}`;
109
+ return `[wishcraft] ${lineCount} lines, showing ${start}–${end}, next offset ${next}`;
101
110
  }
102
111
 
103
112
  export function extractReadToolResultText(content: unknown): string {
@@ -70,7 +70,7 @@ import {
70
70
  import { CONTEXT_STATUS_RENDER_MS } from "../core/constants.ts";
71
71
  import type { RuntimeState } from "../core/types.ts";
72
72
  import { isStaleExtensionContextError } from "./stale-context.ts";
73
- import { dismissWelcome, scheduleDismissWelcome } from "../welcome/welcome-control.ts";
73
+ import { dismissWelcome } from "../welcome/welcome-control.ts";
74
74
  import {
75
75
  bindSkillsCountPublisher,
76
76
  clearSkillsCountPublisher,
@@ -35,6 +35,8 @@ export interface ConfigItem {
35
35
  hint?: string;
36
36
  /** Detail explanation (optional). */
37
37
  description?: string;
38
+ /** Effective value when the setting is absent (toggles only; defaults to false). */
39
+ default?: boolean;
38
40
  }
39
41
 
40
42
  export interface ConfigGroup {
@@ -166,7 +168,7 @@ export function buildConfigGroups(settings: Record<string, unknown>): ConfigGrou
166
168
  title: "Skills",
167
169
  items: [
168
170
  { label: "Inline expand /command and $skill", path: "wishcraft.inlineSkills", kind: "toggle", hint: "needs a restart to take effect" },
169
- { label: "Read hints", path: "wishcraft.readHints", kind: "toggle", hint: "off = no continuation hint after partial reads" },
171
+ { label: "Read hints", path: "wishcraft.readHints", kind: "toggle", default: true, hint: "off = no continuation hint after partial reads" },
170
172
  ],
171
173
  },
172
174
  {
@@ -200,12 +202,24 @@ function isPrintable(data: string): boolean {
200
202
  return data.length === 1 && data >= " " && data <= "~";
201
203
  }
202
204
 
203
- function displayValue(item: ConfigItem, value: ConfigValue): string {
205
+ /** Value shown for a config item given its stored value. */
206
+ export function displayValue(item: ConfigItem, value: ConfigValue): string {
207
+ // An unset toggle renders as its declared default (read hints default on).
208
+ if (item.kind === "toggle" && (value === null || value === undefined)) {
209
+ return (item.default ?? false) ? "on" : "off";
210
+ }
204
211
  if (value === null || value === undefined || value === "") return item.kind === "toggle" ? "off" : "—";
205
212
  if (item.kind === "toggle") return value ? "on" : "off";
206
213
  return String(value);
207
214
  }
208
215
 
216
+ /** Next stored boolean after toggling `item` from its current `value`. */
217
+ export function nextToggleValue(item: ConfigItem, value: ConfigValue): boolean {
218
+ const effective =
219
+ item.kind === "toggle" && value === null ? (item.default ?? false) : value === true;
220
+ return !effective;
221
+ }
222
+
209
223
  function coerce(item: ConfigItem, current: ConfigValue, next: string): ConfigValue {
210
224
  if (item.kind === "number") {
211
225
  const n = Number.parseInt(next, 10);
@@ -288,7 +302,7 @@ export async function showWishcraftConfig(rt: RuntimeState, ctx: any): Promise<v
288
302
 
289
303
  const toggle = (item: ConfigItem) => {
290
304
  const cur = readConfigPath(settings, item.path);
291
- const ok = writeConfigPath(cwd, item.path, !(cur === true));
305
+ const ok = writeConfigPath(cwd, item.path, nextToggleValue(item, cur));
292
306
  settings = readSettings(cwd);
293
307
  groups = buildConfigGroups(settings);
294
308
  ctx.ui.notify(