@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 +7 -0
- package/README.md +1 -0
- package/package.json +1 -1
- package/queue/store.ts +4 -1
- package/src/extension/hooks/hooks-runner.ts +13 -7
- package/src/extension/session/read-hints.ts +18 -9
- package/src/extension/session/session-lifecycle.ts +1 -1
- package/src/extension/settings/wishcraft-config.ts +17 -3
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
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
|
-
|
|
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
|
-
|
|
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
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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
|
|
107
|
+
return `[wishcraft] ${total} lines, showing ${start}–${end}, next offset ${next}`;
|
|
99
108
|
}
|
|
100
|
-
return
|
|
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
|
|
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
|
-
|
|
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,
|
|
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(
|