@andares/pi-toolkits 0.9.2 → 0.9.3
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@andares/pi-toolkits",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.3",
|
|
4
4
|
"description": "Integrated toolkit extension for pi — ask mode (read-only Q&A), session-scoped permission checks, model favorites, prompt stash, third-party code review, session autostart, and a dedicated compaction model",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -145,7 +145,10 @@ type SessionStartHandler = (
|
|
|
145
145
|
cwd: string;
|
|
146
146
|
isProjectTrusted: () => boolean;
|
|
147
147
|
ui: { notify: (message: string, type?: string) => void };
|
|
148
|
-
sessionManager: {
|
|
148
|
+
sessionManager: {
|
|
149
|
+
buildContextEntries: () => Array<{ type: string }>;
|
|
150
|
+
getSessionId: () => string;
|
|
151
|
+
};
|
|
149
152
|
},
|
|
150
153
|
) => Promise<void>;
|
|
151
154
|
|
|
@@ -159,6 +162,7 @@ interface Harness {
|
|
|
159
162
|
setTrusted(trusted: boolean): void;
|
|
160
163
|
/** Simulate a session that already had a conversation (resumed process). */
|
|
161
164
|
setHistory(entries: Array<{ type: string }>): void;
|
|
165
|
+
setSessionId(id: string): void;
|
|
162
166
|
start(reason: string): Promise<void>;
|
|
163
167
|
}
|
|
164
168
|
|
|
@@ -190,6 +194,7 @@ async function makeHarness(
|
|
|
190
194
|
{ type: "model_change" },
|
|
191
195
|
{ type: "thinking_level_change" },
|
|
192
196
|
];
|
|
197
|
+
let sessionId = "session-1";
|
|
193
198
|
let handler: SessionStartHandler = async () => {};
|
|
194
199
|
const sent: Harness["sent"] = [];
|
|
195
200
|
const notifications: Harness["notifications"] = [];
|
|
@@ -226,6 +231,9 @@ async function makeHarness(
|
|
|
226
231
|
setHistory(entries) {
|
|
227
232
|
history = entries;
|
|
228
233
|
},
|
|
234
|
+
setSessionId(id) {
|
|
235
|
+
sessionId = id;
|
|
236
|
+
},
|
|
229
237
|
async start(reason: string) {
|
|
230
238
|
await handler({ reason }, {
|
|
231
239
|
cwd,
|
|
@@ -235,7 +243,10 @@ async function makeHarness(
|
|
|
235
243
|
notifications.push({ message, type });
|
|
236
244
|
},
|
|
237
245
|
},
|
|
238
|
-
sessionManager: {
|
|
246
|
+
sessionManager: {
|
|
247
|
+
buildContextEntries: () => history,
|
|
248
|
+
getSessionId: () => sessionId,
|
|
249
|
+
},
|
|
239
250
|
});
|
|
240
251
|
},
|
|
241
252
|
cwd,
|
|
@@ -286,6 +297,8 @@ describe("registerAutostart", () => {
|
|
|
286
297
|
it("replays on new and fork", async () => {
|
|
287
298
|
const h = await harness('[command]\nrun = ["/ask on"]');
|
|
288
299
|
await h.start("new");
|
|
300
|
+
// A fork is a different session, so it replays again.
|
|
301
|
+
h.setSessionId("forked-session");
|
|
289
302
|
await h.start("fork");
|
|
290
303
|
expect(h.sent).toEqual([
|
|
291
304
|
{ command: "/ask on", options: { expandPromptTemplates: true } },
|
|
@@ -302,6 +315,21 @@ describe("registerAutostart", () => {
|
|
|
302
315
|
expect(h.notifications).toEqual([]);
|
|
303
316
|
});
|
|
304
317
|
|
|
318
|
+
it("replays once per session even if session_start is emitted twice", async () => {
|
|
319
|
+
const h = await harness(fullConfig);
|
|
320
|
+
await h.start("startup");
|
|
321
|
+
await h.start("startup");
|
|
322
|
+
expect(h.sent).toHaveLength(2);
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
it("replays again for a new session id", async () => {
|
|
326
|
+
const h = await harness(fullConfig);
|
|
327
|
+
await h.start("startup");
|
|
328
|
+
h.setSessionId("session-2");
|
|
329
|
+
await h.start("startup");
|
|
330
|
+
expect(h.sent).toHaveLength(4);
|
|
331
|
+
});
|
|
332
|
+
|
|
305
333
|
it("skips a process started on an existing session (pi -c reports startup)", async () => {
|
|
306
334
|
const h = await harness(fullConfig);
|
|
307
335
|
h.setHistory([
|
|
@@ -87,6 +87,15 @@ const HISTORY_ENTRY_TYPES: ReadonlySet<string> = new Set([
|
|
|
87
87
|
* session manager counts as new, which keeps the feature working in hosts where
|
|
88
88
|
* the session surface is unavailable.
|
|
89
89
|
*/
|
|
90
|
+
/** Session id, or `undefined` when the host exposes none. */
|
|
91
|
+
function sessionIdOf(ctx: ExtensionContext): string | undefined {
|
|
92
|
+
try {
|
|
93
|
+
return ctx.sessionManager.getSessionId();
|
|
94
|
+
} catch {
|
|
95
|
+
return undefined;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
90
99
|
function hasConversationHistory(ctx: ExtensionContext): boolean {
|
|
91
100
|
try {
|
|
92
101
|
return ctx.sessionManager
|
|
@@ -102,6 +111,11 @@ export function registerAutostart(
|
|
|
102
111
|
options: AutostartOptions = {},
|
|
103
112
|
): void {
|
|
104
113
|
const agentDir = options.agentDir ?? getAgentDir();
|
|
114
|
+
// One replay per session. The host can emit `session_start` more than once
|
|
115
|
+
// for the same session (a runtime rebind during startup reports `startup`
|
|
116
|
+
// again), and replaying on the second emission would announce everything
|
|
117
|
+
// twice.
|
|
118
|
+
const replayedSessions = new Set<string>();
|
|
105
119
|
|
|
106
120
|
async function readCommands(
|
|
107
121
|
ctx: ExtensionContext,
|
|
@@ -140,6 +154,12 @@ export function registerAutostart(
|
|
|
140
154
|
// shouldReplay in autostart.ts for the host-side evidence.
|
|
141
155
|
if (!shouldReplay(event.reason, hasConversationHistory(ctx))) return;
|
|
142
156
|
|
|
157
|
+
const sessionId = sessionIdOf(ctx);
|
|
158
|
+
if (sessionId !== undefined) {
|
|
159
|
+
if (replayedSessions.has(sessionId)) return;
|
|
160
|
+
replayedSessions.add(sessionId);
|
|
161
|
+
}
|
|
162
|
+
|
|
143
163
|
const sources: Source[] = [
|
|
144
164
|
{
|
|
145
165
|
path: join(agentDir, GLOBAL_AUTOSTART_FILENAME),
|
|
@@ -51,6 +51,7 @@ interface Harness {
|
|
|
51
51
|
readonly statuses: Array<[string, string | undefined]>;
|
|
52
52
|
readonly notes: Array<{ message: string; kind?: string }>;
|
|
53
53
|
readonly branch: Array<Record<string, unknown>>;
|
|
54
|
+
setSessionId(id: string): void;
|
|
54
55
|
evaluate(event: ToolCallEvent): Promise<unknown>;
|
|
55
56
|
command(args: string): Promise<void>;
|
|
56
57
|
sessionStart(): Promise<void>;
|
|
@@ -62,6 +63,7 @@ function makeHarness(): Harness {
|
|
|
62
63
|
const statuses: Harness["statuses"] = [];
|
|
63
64
|
const notes: Harness["notes"] = [];
|
|
64
65
|
const branch: Harness["branch"] = [];
|
|
66
|
+
let sessionId = "session-1";
|
|
65
67
|
const ctx = {
|
|
66
68
|
cwd,
|
|
67
69
|
hasUI: true,
|
|
@@ -73,7 +75,10 @@ function makeHarness(): Harness {
|
|
|
73
75
|
custom: async () => undefined,
|
|
74
76
|
select: async () => undefined,
|
|
75
77
|
},
|
|
76
|
-
sessionManager: {
|
|
78
|
+
sessionManager: {
|
|
79
|
+
getBranch: () => branch,
|
|
80
|
+
getSessionId: () => sessionId,
|
|
81
|
+
},
|
|
77
82
|
} as unknown as ExtensionContext;
|
|
78
83
|
|
|
79
84
|
const captured: ToolGate[] = [];
|
|
@@ -110,6 +115,9 @@ function makeHarness(): Harness {
|
|
|
110
115
|
statuses,
|
|
111
116
|
notes,
|
|
112
117
|
branch,
|
|
118
|
+
setSessionId(id: string) {
|
|
119
|
+
sessionId = id;
|
|
120
|
+
},
|
|
113
121
|
async evaluate(event) {
|
|
114
122
|
return gate.evaluate(event, ctx);
|
|
115
123
|
},
|
|
@@ -214,6 +222,25 @@ describe("/ask-perm", () => {
|
|
|
214
222
|
expect(await h.evaluate(bashEvent("rm -rf /tmp/outside"))).toBeUndefined();
|
|
215
223
|
});
|
|
216
224
|
|
|
225
|
+
it("keeps the state when the same session emits session_start again", async () => {
|
|
226
|
+
const h = makeHarness();
|
|
227
|
+
await h.sessionStart(); // the real first emission
|
|
228
|
+
await h.command("allow allow allow ask");
|
|
229
|
+
// A startup rebind reports the same session again; state must survive.
|
|
230
|
+
await h.sessionStart();
|
|
231
|
+
expect(asAsk(await h.evaluate(bashEvent("rm -rf /tmp/outside"))).kind).toBe(
|
|
232
|
+
"ask",
|
|
233
|
+
);
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
it("resets on session_start for a different session", async () => {
|
|
237
|
+
const h = makeHarness();
|
|
238
|
+
await h.command("allow allow allow ask");
|
|
239
|
+
h.setSessionId("session-2");
|
|
240
|
+
await h.sessionStart();
|
|
241
|
+
expect(await h.evaluate(bashEvent("rm -rf /tmp/outside"))).toBeUndefined();
|
|
242
|
+
});
|
|
243
|
+
|
|
217
244
|
it("resets on session_start", async () => {
|
|
218
245
|
const h = makeHarness();
|
|
219
246
|
await h.command("allow allow allow ask");
|
|
@@ -99,6 +99,10 @@ function toolUnit(
|
|
|
99
99
|
export function registerPermission(pi: ExtensionAPI, gates: GateHub): void {
|
|
100
100
|
let enabled = false;
|
|
101
101
|
let modes: PermissionModes = { ...DEFAULT_MODES };
|
|
102
|
+
// Session this state belongs to. A runtime rebind can emit `session_start`
|
|
103
|
+
// again for the same session; resetting then would wipe a mode that the
|
|
104
|
+
// autostart replay had just enabled, so only a different session resets.
|
|
105
|
+
let currentSessionId: string | undefined;
|
|
102
106
|
const rules = new SessionRules();
|
|
103
107
|
let rootsCache:
|
|
104
108
|
| { cwd: string; branchLength: number; roots: string[] }
|
|
@@ -111,6 +115,15 @@ export function registerPermission(pi: ExtensionAPI, gates: GateHub): void {
|
|
|
111
115
|
rootsCache = undefined;
|
|
112
116
|
}
|
|
113
117
|
|
|
118
|
+
/** Session id, or `undefined` when the host exposes none. */
|
|
119
|
+
function sessionIdOf(ctx: ExtensionContext): string | undefined {
|
|
120
|
+
try {
|
|
121
|
+
return ctx.sessionManager.getSessionId();
|
|
122
|
+
} catch {
|
|
123
|
+
return undefined;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
114
127
|
function syncStatus(ctx: ExtensionContext): void {
|
|
115
128
|
if (!enabled) {
|
|
116
129
|
ctx.ui.setStatus(PERMISSION_STATUS_KEY, undefined);
|
|
@@ -233,12 +246,16 @@ export function registerPermission(pi: ExtensionAPI, gates: GateHub): void {
|
|
|
233
246
|
// ── Lifecycle ──────────────────────────────────────────────────────────
|
|
234
247
|
|
|
235
248
|
pi.on("session_start", (_event, ctx) => {
|
|
249
|
+
const sessionId = sessionIdOf(ctx);
|
|
250
|
+
// Same session, repeated emission (a startup rebind): keep the state.
|
|
251
|
+
if (sessionId !== undefined && sessionId === currentSessionId) {
|
|
252
|
+
syncStatus(ctx);
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
currentSessionId = sessionId;
|
|
236
256
|
reset();
|
|
237
257
|
syncStatus(ctx);
|
|
238
258
|
});
|
|
239
|
-
pi.on("session_shutdown", () => {
|
|
240
|
-
reset();
|
|
241
|
-
});
|
|
242
259
|
|
|
243
260
|
// ── Command ────────────────────────────────────────────────────────────
|
|
244
261
|
|