@saccolabs/pi-claude-cli 0.4.4 → 0.4.6
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/index.ts +42 -1
- package/package.json +1 -1
- package/src/prompt-builder.ts +20 -25
- package/src/provider.ts +14 -0
- package/src/types.ts +20 -1
package/index.ts
CHANGED
|
@@ -22,6 +22,44 @@ process.on("exit", killAllProcesses);
|
|
|
22
22
|
|
|
23
23
|
const PROVIDER_ID = "pi-claude-cli";
|
|
24
24
|
|
|
25
|
+
/**
|
|
26
|
+
* Status key carrying account rate-limit state to the front-end. Neutral
|
|
27
|
+
* (not pidex-specific) because any pi front-end can read it.
|
|
28
|
+
*/
|
|
29
|
+
const RATE_LIMIT_STATUS_KEY = "claude-rate-limit";
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* The stream runs deep inside streamSimple, which has no ExtensionContext,
|
|
33
|
+
* so the ctx handed to session_start is kept for its `ui.setStatus`.
|
|
34
|
+
*/
|
|
35
|
+
let uiContext:
|
|
36
|
+
{ ui?: { setStatus?(key: string, text?: string): void } } | undefined;
|
|
37
|
+
/** Last payload pushed — the CLI repeats this event on every turn. */
|
|
38
|
+
let lastRateLimitJson: string | undefined;
|
|
39
|
+
|
|
40
|
+
function publishRateLimit(info: Record<string, unknown>): void {
|
|
41
|
+
const setStatus = uiContext?.ui?.setStatus;
|
|
42
|
+
if (typeof setStatus !== "function") return;
|
|
43
|
+
const payload = JSON.stringify({
|
|
44
|
+
status: info.status,
|
|
45
|
+
resetsAt: info.resetsAt,
|
|
46
|
+
rateLimitType: info.rateLimitType,
|
|
47
|
+
overageStatus: info.overageStatus,
|
|
48
|
+
isUsingOverage: info.isUsingOverage === true,
|
|
49
|
+
observedAt: Math.floor(Date.now() / 1000),
|
|
50
|
+
});
|
|
51
|
+
// Push only on change: the event repeats every turn, and a status that
|
|
52
|
+
// rewrites itself constantly is noise for whatever renders it.
|
|
53
|
+
const withoutObservedAt = payload.replace(/,"observedAt":\d+/, "");
|
|
54
|
+
if (withoutObservedAt === lastRateLimitJson) return;
|
|
55
|
+
lastRateLimitJson = withoutObservedAt;
|
|
56
|
+
try {
|
|
57
|
+
setStatus.call(uiContext!.ui, RATE_LIMIT_STATUS_KEY, payload);
|
|
58
|
+
} catch {
|
|
59
|
+
/* never break a turn over a status push */
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
25
63
|
let mcpConfigPath: string | undefined;
|
|
26
64
|
let mcpConfigResolved = false;
|
|
27
65
|
|
|
@@ -91,7 +129,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
91
129
|
|
|
92
130
|
// Ensure all registered tools are active so pi can execute them.
|
|
93
131
|
// Some tools (find, grep, ls) are registered but not activated by default.
|
|
94
|
-
pi.on("session_start", async () => {
|
|
132
|
+
pi.on("session_start", async (_event: unknown, ctx: unknown) => {
|
|
133
|
+
uiContext = ctx as typeof uiContext;
|
|
134
|
+
lastRateLimitJson = undefined;
|
|
95
135
|
const allTools = pi.getAllTools();
|
|
96
136
|
if (Array.isArray(allTools)) {
|
|
97
137
|
pi.setActiveTools(allTools.map((t: any) => t.name));
|
|
@@ -107,6 +147,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
107
147
|
return streamViaCli(model, context, {
|
|
108
148
|
...options,
|
|
109
149
|
mcpConfigPath: configPath,
|
|
150
|
+
onRateLimit: publishRateLimit,
|
|
110
151
|
});
|
|
111
152
|
};
|
|
112
153
|
|
package/package.json
CHANGED
package/src/prompt-builder.ts
CHANGED
|
@@ -140,15 +140,22 @@ function buildCustomToolResultPrompt(messages: any[]): string | null {
|
|
|
140
140
|
/**
|
|
141
141
|
* Build a prompt for a resumed session.
|
|
142
142
|
*
|
|
143
|
-
* When resuming via --resume, the CLI already has the full conversation history
|
|
144
|
-
*
|
|
145
|
-
*
|
|
143
|
+
* When resuming via --resume, the CLI already has the full conversation history
|
|
144
|
+
* up through the most recent assistant turn it produced. We only need to send
|
|
145
|
+
* the delta since that turn: trailing tool results for the last assistant
|
|
146
|
+
* tool_use, and/or a new user message.
|
|
146
147
|
*
|
|
147
|
-
*
|
|
148
|
-
*
|
|
149
|
-
*
|
|
148
|
+
* Why anchor on the last assistant message rather than the last user message?
|
|
149
|
+
* Pi's tool loop appends [user, assistant(toolUse), toolResult,
|
|
150
|
+
* assistant(toolUse), toolResult, ...] — the only `user` entry stays at index 0
|
|
151
|
+
* across many provider invocations. Anchoring there made every iteration replay
|
|
152
|
+
* the entire transcript, and when that first user message carried an image the
|
|
153
|
+
* image branch below returned early with just the image, so tool results never
|
|
154
|
+
* reached the model at all. The model then re-issued the same tool call
|
|
155
|
+
* indefinitely while each turn re-billed the whole accumulated context.
|
|
150
156
|
*
|
|
151
|
-
*
|
|
157
|
+
* Returns "" when there's nothing new to send (e.g. only an assistant message
|
|
158
|
+
* exists in the context — can happen mid-shutdown).
|
|
152
159
|
*/
|
|
153
160
|
export function buildResumePrompt(context: {
|
|
154
161
|
messages: any[];
|
|
@@ -156,28 +163,16 @@ export function buildResumePrompt(context: {
|
|
|
156
163
|
const messages = context.messages;
|
|
157
164
|
if (messages.length === 0) return "";
|
|
158
165
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
// Collect new messages: everything from the last assistant turn onwards
|
|
164
|
-
// (tool results from the last assistant + the new user message)
|
|
165
|
-
const newMessages: any[] = [];
|
|
166
|
-
|
|
167
|
-
// Walk backwards from finalUserIndex to find where new content starts.
|
|
168
|
-
// Include trailing toolResult messages that follow the last assistant turn.
|
|
169
|
-
let startIdx = finalUserIndex;
|
|
170
|
-
for (let i = finalUserIndex - 1; i >= 0; i--) {
|
|
171
|
-
if (messages[i].role === "toolResult") {
|
|
172
|
-
startIdx = i;
|
|
173
|
-
} else {
|
|
166
|
+
let lastAssistantIdx = -1;
|
|
167
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
168
|
+
if (messages[i].role === "assistant") {
|
|
169
|
+
lastAssistantIdx = i;
|
|
174
170
|
break;
|
|
175
171
|
}
|
|
176
172
|
}
|
|
177
173
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
}
|
|
174
|
+
const newMessages = messages.slice(lastAssistantIdx + 1);
|
|
175
|
+
if (newMessages.length === 0) return "";
|
|
181
176
|
|
|
182
177
|
// If there are only tool results + one user message, build a combined prompt
|
|
183
178
|
const parts: string[] = [];
|
package/src/provider.ts
CHANGED
|
@@ -55,6 +55,8 @@ const INACTIVITY_TIMEOUT_MS =
|
|
|
55
55
|
type StreamViaCLiOptions = SimpleStreamOptions & {
|
|
56
56
|
cwd?: string;
|
|
57
57
|
mcpConfigPath?: string;
|
|
58
|
+
/** Called with account rate-limit state as the CLI reports it. */
|
|
59
|
+
onRateLimit?: (info: Record<string, unknown>) => void;
|
|
58
60
|
};
|
|
59
61
|
|
|
60
62
|
/**
|
|
@@ -296,6 +298,18 @@ export function streamViaCli(
|
|
|
296
298
|
rl.close();
|
|
297
299
|
return; // Don't process further -- done event already pushed by event bridge
|
|
298
300
|
}
|
|
301
|
+
} else if (msg.type === "rate_limit_event") {
|
|
302
|
+
// Account-level state, not turn content: hand it to the host so a
|
|
303
|
+
// front-end can surface the window and its reset. Never touches
|
|
304
|
+
// the assistant message.
|
|
305
|
+
const info = (msg as any).rate_limit_info;
|
|
306
|
+
if (info && typeof info === "object") {
|
|
307
|
+
try {
|
|
308
|
+
options?.onRateLimit?.(info);
|
|
309
|
+
} catch {
|
|
310
|
+
/* a status push must never break a turn */
|
|
311
|
+
}
|
|
312
|
+
}
|
|
299
313
|
} else if (msg.type === "assistant") {
|
|
300
314
|
// Complete-block envelopes: marker text for CLI-side tools that
|
|
301
315
|
// would otherwise be invisible between cycles.
|
package/src/types.ts
CHANGED
|
@@ -43,6 +43,24 @@ export interface ClaudeAssistantEnvelope {
|
|
|
43
43
|
};
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
+
/**
|
|
47
|
+
* Emitted when the API reports rate-limit state for the account. Carries the
|
|
48
|
+
* window and its reset, not a utilization percentage — the percentages the
|
|
49
|
+
* Claude Code TUI shows come from `anthropic-ratelimit-unified-*` response
|
|
50
|
+
* headers, which the CLI consumes in-process and does not forward here.
|
|
51
|
+
*/
|
|
52
|
+
export interface ClaudeRateLimitEvent {
|
|
53
|
+
type: "rate_limit_event";
|
|
54
|
+
rate_limit_info?: {
|
|
55
|
+
status?: string;
|
|
56
|
+
resetsAt?: number;
|
|
57
|
+
rateLimitType?: string;
|
|
58
|
+
overageStatus?: string;
|
|
59
|
+
overageDisabledReason?: string;
|
|
60
|
+
isUsingOverage?: boolean;
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
46
64
|
/** Tool results the CLI feeds back between cycles (top-level only). */
|
|
47
65
|
export interface ClaudeUserEnvelope {
|
|
48
66
|
type: "user";
|
|
@@ -81,7 +99,8 @@ export type NdjsonMessage =
|
|
|
81
99
|
| ClaudeSystemMessage
|
|
82
100
|
| ClaudeControlRequest
|
|
83
101
|
| ClaudeAssistantEnvelope
|
|
84
|
-
| ClaudeUserEnvelope
|
|
102
|
+
| ClaudeUserEnvelope
|
|
103
|
+
| ClaudeRateLimitEvent;
|
|
85
104
|
|
|
86
105
|
// Claude API event types (inside stream_event wrapper)
|
|
87
106
|
|