@valni/cli-darwin-x64 0.1.5 → 0.1.7

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 (20) hide show
  1. package/package.json +1 -1
  2. package/runtime/extensions/pi-switchboard/extensions/switchboard/catalog.ts +20 -18
  3. package/runtime/extensions/pi-switchboard/extensions/switchboard/index.ts +13 -8
  4. package/runtime/extensions/pi-switchboard/extensions/switchboard/toolProxy.ts +3 -2
  5. package/runtime/extensions/pi-switchboard/extensions/switchboard/tools.ts +3 -1
  6. package/runtime/extensions/pi-switchboard/extensions/switchboard/types.ts +3 -0
  7. package/runtime/extensions/pi-switchboard/package.json +3 -3
  8. package/runtime/extensions/switchboard-local/extensions/local/eventStream.ts +88 -0
  9. package/runtime/extensions/switchboard-local/extensions/local/hostContext.ts +49 -0
  10. package/runtime/extensions/switchboard-local/extensions/local/jsonParse.ts +124 -0
  11. package/runtime/extensions/switchboard-local/extensions/local/nativeStream.ts +20 -12
  12. package/runtime/extensions/switchboard-local/node_modules/partial-json/LICENSE +21 -0
  13. package/runtime/extensions/switchboard-local/node_modules/partial-json/README.md +131 -0
  14. package/runtime/extensions/switchboard-local/node_modules/partial-json/dist/index.js +220 -0
  15. package/runtime/extensions/switchboard-local/node_modules/partial-json/dist/options.js +72 -0
  16. package/runtime/extensions/switchboard-local/node_modules/partial-json/package.json +43 -0
  17. package/runtime/extensions/switchboard-local/package.json +5 -4
  18. package/runtime/native/darwin/prebuilds/darwin-x64/darwin-modifiers.node +0 -0
  19. package/runtime/package.json +6 -6
  20. package/runtime/valni +1 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@valni/cli-darwin-x64",
3
- "version": "0.1.5",
3
+ "version": "0.1.7",
4
4
  "license": "SEE LICENSE IN LICENSE",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -48,13 +48,11 @@ export async function loadRegistryModels(): Promise<Record<string, RegistryModel
48
48
  const distDirectory = findPiAiDist();
49
49
  const registry: Record<string, RegistryModel> = {};
50
50
  if (distDirectory === undefined) {
51
- const names = AI_PACKAGES.map(([scope, name]) => `${scope}/${name}`).join(" or ");
52
- console.error(
53
- `pi-switchboard: no ${names} beside the host executable, so models list without a context window or display name.`,
54
- );
51
+ // The catalog supplies the context window and the display name, and a
52
+ // genuinely missing context window is reported precisely in
53
+ // buildModelConfigs. The registry is only a source of last resort.
55
54
  return registry;
56
55
  }
57
- const missing: string[] = [];
58
56
  for (const file of REGISTRY_FILES) {
59
57
  let loaded: Record<string, Record<string, RegistryModel>>;
60
58
  try {
@@ -63,10 +61,9 @@ export async function loadRegistryModels(): Promise<Record<string, RegistryModel
63
61
  Record<string, RegistryModel>
64
62
  >;
65
63
  } catch (error) {
66
- if (isModuleNotFound(error)) {
67
- missing.push(file);
68
- continue;
69
- }
64
+ // A registry file that is not there is not a problem: everything a
65
+ // user sees first comes from the catalog.
66
+ if (isModuleNotFound(error)) continue;
70
67
  throw error;
71
68
  }
72
69
  for (const exported of Object.values(loaded)) {
@@ -76,11 +73,6 @@ export async function loadRegistryModels(): Promise<Record<string, RegistryModel
76
73
  }
77
74
  }
78
75
  }
79
- if (missing.length === REGISTRY_FILES.length) {
80
- console.error(
81
- `pi-switchboard: no model registry in ${distDirectory}/providers (${missing.join(", ")}). Models will list without a context window or display name.`,
82
- );
83
- }
84
76
  return registry;
85
77
  }
86
78
 
@@ -109,10 +101,11 @@ function toModelConfig(
109
101
  price: ModelRecordPrice | undefined,
110
102
  registryModel: RegistryModel | undefined,
111
103
  catalogContextWindow: number | undefined,
104
+ catalogDisplayName: string | undefined,
112
105
  ) {
113
106
  return {
114
107
  id: record.id,
115
- name: registryModel?.name ?? record.id,
108
+ name: catalogDisplayName ?? registryModel?.name ?? record.id,
116
109
  api: KIND_TO_API[kindTag],
117
110
  baseUrl: `${resolveBaseUrl()}${SENTINEL_SEGMENT}${kindTag}`,
118
111
  reasoning: supportsReasoning(kindTag, profile) ?? registryModel?.reasoning ?? false,
@@ -127,12 +120,13 @@ function toModelConfig(
127
120
  };
128
121
  }
129
122
 
130
- export async function discoverCatalog(baseUrl: string, bearer: string): Promise<ModelsPage> {
123
+ export async function discoverCatalog(baseUrl: string, bearer: string, signal?: AbortSignal): Promise<ModelsPage> {
124
+ const timeout = AbortSignal.timeout(CATALOG_FETCH_TIMEOUT_MS);
131
125
  let response: Response;
132
126
  try {
133
127
  response = await fetch(`${baseUrl}${MODELS_PATH}`, {
134
128
  headers: { Authorization: `Bearer ${bearer}` },
135
- signal: AbortSignal.timeout(CATALOG_FETCH_TIMEOUT_MS),
129
+ signal: signal ? AbortSignal.any([signal, timeout]) : timeout,
136
130
  });
137
131
  } catch (error) {
138
132
  if (isAbortError(error)) throw error;
@@ -168,7 +162,15 @@ export function buildModelConfigs(catalog: ModelsPage, registry: Record<string,
168
162
  const catalogContextWindow = catalog.context_windows?.[record.id];
169
163
  if (catalogContextWindow === undefined && registry[record.id]?.contextWindow === undefined) assumed++;
170
164
  modelConfigs.push(
171
- toModelConfig(record, kindTag, record.kind[kindTag], catalog.prices[record.id], registry[record.id], catalogContextWindow),
165
+ toModelConfig(
166
+ record,
167
+ kindTag,
168
+ record.kind[kindTag],
169
+ catalog.prices[record.id],
170
+ registry[record.id],
171
+ catalogContextWindow,
172
+ catalog.display_names?.[record.id],
173
+ ),
172
174
  );
173
175
  }
174
176
  if (skipped.length > 0) {
@@ -99,22 +99,27 @@ export default async function (pi: ExtensionAPI) {
99
99
  refreshModels: async (context: RefreshModelsContext) => {
100
100
  const bearer = credentialBearer(context.credential);
101
101
  if (!bearer) {
102
- await context.store.delete();
102
+ await context.publish({ persist: null });
103
103
  return [];
104
104
  }
105
- const stored = await context.store.read();
106
105
  if (!context.allowNetwork) {
107
- return stored ? ([...stored.models] as ReturnType<typeof buildModelConfigs>) : startupModels;
106
+ return context.stored
107
+ ? ([...context.stored.models] as ReturnType<typeof buildModelConfigs>)
108
+ : startupModels;
108
109
  }
109
- const catalog = await discoverCatalog(baseUrl, bearer);
110
+ const catalog = await discoverCatalog(baseUrl, bearer, context.signal);
110
111
  const configs = buildModelConfigs(catalog, registry);
111
- await context.store.write({
112
- models: configs as unknown as ModelsStoreEntry["models"],
113
- checkedAt: Math.floor(Date.now() / MILLISECONDS_PER_SECOND),
112
+ const published = await context.publish({
113
+ persist: {
114
+ models: configs as unknown as ModelsStoreEntry["models"],
115
+ checkedAt: Math.floor(Date.now() / MILLISECONDS_PER_SECOND),
116
+ },
114
117
  });
118
+ if (!published) return configs;
115
119
  if (context.credential?.type === "oauth") {
116
120
  try {
117
- registerServerTools(pi, await discoverTools(baseUrl, bearer));
121
+ const tools = await discoverTools(baseUrl, bearer, context.signal);
122
+ if (!context.signal.aborted) registerServerTools(pi, tools);
118
123
  } catch (error) {
119
124
  console.error("pi-switchboard: server tool discovery failed", error);
120
125
  }
@@ -28,14 +28,15 @@ export interface AdvertisedTool {
28
28
  version?: string;
29
29
  }
30
30
 
31
- export async function discoverTools(baseUrl: string, bearer: string): Promise<AdvertisedTool[]> {
31
+ export async function discoverTools(baseUrl: string, bearer: string, signal?: AbortSignal): Promise<AdvertisedTool[]> {
32
+ const timeout = AbortSignal.timeout(DISCOVER_TIMEOUT_MS);
32
33
  let response: Response;
33
34
  try {
34
35
  response = await fetch(`${baseUrl}${MCP_PATH}`, {
35
36
  method: "POST",
36
37
  headers: { "Content-Type": "application/json", Accept: "application/json", Authorization: `Bearer ${bearer}` },
37
38
  body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "tools/list", params: {} }),
38
- signal: AbortSignal.timeout(DISCOVER_TIMEOUT_MS),
39
+ signal: signal ? AbortSignal.any([signal, timeout]) : timeout,
39
40
  });
40
41
  } catch {
41
42
  return [];
@@ -1,4 +1,6 @@
1
- import { Type } from "@earendil-works/pi-ai";
1
+ // typebox directly, not through a harness package: this extension runs in
2
+ // more than one host and must not name any of them at runtime.
3
+ import { Type } from "typebox";
2
4
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
3
5
  import { type AdvertisedTool, makeProxyExecute } from "./toolProxy.ts";
4
6
 
@@ -31,7 +31,10 @@ export interface ModelRecordPrice {
31
31
  export interface ModelsPage {
32
32
  models: ModelRecord[];
33
33
  prices: Record<string, ModelRecordPrice>;
34
+ /** Real context window per model id, from the catalog. */
34
35
  context_windows?: Record<string, number>;
36
+ /** Human-readable name per model id, from the catalog. */
37
+ display_names?: Record<string, string>;
35
38
  }
36
39
 
37
40
  export type RegistryModel = Model<(typeof KIND_TO_API)[SwitchboardKind]>;
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-switchboard",
3
- "version": "0.11.2",
3
+ "version": "0.11.3",
4
4
  "type": "module",
5
5
  "description": "Route the pi coding agent through Switchboard: one prepaid balance, spend controls, and a ledger for every model in the catalog",
6
6
  "keywords": [
@@ -23,8 +23,8 @@
23
23
  ]
24
24
  },
25
25
  "devDependencies": {
26
- "@earendil-works/pi-ai": "0.82.1",
27
- "@earendil-works/pi-coding-agent": "0.82.1",
26
+ "@earendil-works/pi-ai": ">=1.0.0",
27
+ "@earendil-works/pi-coding-agent": ">=1.0.0",
28
28
  "typescript": "5.9.3"
29
29
  }
30
30
  }
@@ -0,0 +1,88 @@
1
+ import type { AssistantMessage, AssistantMessageEvent } from "@earendil-works/pi-ai";
2
+
3
+ // Generic event stream class for async iteration
4
+ export class EventStream<T, R = T> implements AsyncIterable<T> {
5
+ private queue: T[] = [];
6
+ private waiting: ((value: IteratorResult<T>) => void)[] = [];
7
+ private done = false;
8
+ private finalResultPromise: Promise<R>;
9
+ private resolveFinalResult!: (result: R) => void;
10
+ private isComplete: (event: T) => boolean;
11
+ private extractResult: (event: T) => R;
12
+
13
+ constructor(isComplete: (event: T) => boolean, extractResult: (event: T) => R) {
14
+ this.isComplete = isComplete;
15
+ this.extractResult = extractResult;
16
+ this.finalResultPromise = new Promise((resolve) => {
17
+ this.resolveFinalResult = resolve;
18
+ });
19
+ }
20
+
21
+ push(event: T): void {
22
+ if (this.done) return;
23
+
24
+ if (this.isComplete(event)) {
25
+ this.done = true;
26
+ this.resolveFinalResult(this.extractResult(event));
27
+ }
28
+
29
+ // Deliver to waiting consumer or queue it
30
+ const waiter = this.waiting.shift();
31
+ if (waiter) {
32
+ waiter({ value: event, done: false });
33
+ } else {
34
+ this.queue.push(event);
35
+ }
36
+ }
37
+
38
+ end(result?: R): void {
39
+ this.done = true;
40
+ if (result !== undefined) {
41
+ this.resolveFinalResult(result);
42
+ }
43
+ // Notify all waiting consumers that we're done
44
+ while (this.waiting.length > 0) {
45
+ const waiter = this.waiting.shift()!;
46
+ waiter({ value: undefined as any, done: true });
47
+ }
48
+ }
49
+
50
+ async *[Symbol.asyncIterator](): AsyncIterator<T> {
51
+ while (true) {
52
+ if (this.queue.length > 0) {
53
+ yield this.queue.shift()!;
54
+ } else if (this.done) {
55
+ return;
56
+ } else {
57
+ const result = await new Promise<IteratorResult<T>>((resolve) => this.waiting.push(resolve));
58
+ if (result.done) return;
59
+ yield result.value;
60
+ }
61
+ }
62
+ }
63
+
64
+ result(): Promise<R> {
65
+ return this.finalResultPromise;
66
+ }
67
+ }
68
+
69
+ export class AssistantMessageEventStream extends EventStream<AssistantMessageEvent, AssistantMessage> {
70
+ constructor() {
71
+ super(
72
+ (event) => event.type === "done" || event.type === "error",
73
+ (event) => {
74
+ if (event.type === "done") {
75
+ return event.message;
76
+ } else if (event.type === "error") {
77
+ return event.error;
78
+ }
79
+ throw new Error("Unexpected event type for final result");
80
+ },
81
+ );
82
+ }
83
+ }
84
+
85
+ /** Factory function for AssistantMessageEventStream (for use in extensions) */
86
+ export function createAssistantMessageEventStream(): AssistantMessageEventStream {
87
+ return new AssistantMessageEventStream();
88
+ }
@@ -0,0 +1,49 @@
1
+ import type { TextContent, Tool, TranscriptContext } from "@earendil-works/pi-ai";
2
+
3
+ /**
4
+ * Read the system prompt and the tools out of a transcript.
5
+ *
6
+ * Both hosts hand a provider the same shape now: system messages replayed in
7
+ * order, where later content appends to the prompt and tools are added and
8
+ * removed by name. Doing the replay here rather than calling a host helper is
9
+ * what keeps a host package out of the runtime imports.
10
+ */
11
+
12
+ interface SystemMessage {
13
+ role: string;
14
+ content: string | TextContent[];
15
+ toolsAdded?: Tool[];
16
+ toolsRemoved?: { name: string }[];
17
+ }
18
+
19
+ function isSystemMessage(message: unknown): message is SystemMessage {
20
+ return message !== null && typeof message === "object" && (message as { role?: unknown }).role === "system";
21
+ }
22
+
23
+ function contentText(content: string | TextContent[]): string {
24
+ if (typeof content === "string") return content;
25
+ return content
26
+ .filter((part) => part.type === "text")
27
+ .map((part) => part.text)
28
+ .join("");
29
+ }
30
+
31
+ export function hostSystemPrompt(context: TranscriptContext): string | undefined {
32
+ const parts: string[] = [];
33
+ for (const message of context.messages) {
34
+ if (!isSystemMessage(message)) continue;
35
+ const text = contentText(message.content);
36
+ if (text.length > 0) parts.push(text);
37
+ }
38
+ return parts.length > 0 ? parts.join("\n\n") : undefined;
39
+ }
40
+
41
+ export function hostTools(context: TranscriptContext): Tool[] {
42
+ const tools = new Map<string, Tool>();
43
+ for (const message of context.messages) {
44
+ if (!isSystemMessage(message)) continue;
45
+ for (const tool of message.toolsRemoved ?? []) tools.delete(tool.name);
46
+ for (const tool of message.toolsAdded ?? []) tools.set(tool.name, tool);
47
+ }
48
+ return [...tools.values()];
49
+ }
@@ -0,0 +1,124 @@
1
+ import { parse as partialParse } from "partial-json";
2
+
3
+ const VALID_JSON_ESCAPES = new Set(['"', "\\", "/", "b", "f", "n", "r", "t", "u"]);
4
+
5
+ function isControlCharacter(char: string): boolean {
6
+ const codePoint = char.codePointAt(0);
7
+ return codePoint !== undefined && codePoint >= 0x00 && codePoint <= 0x1f;
8
+ }
9
+
10
+ function escapeControlCharacter(char: string): string {
11
+ switch (char) {
12
+ case "\b":
13
+ return "\\b";
14
+ case "\f":
15
+ return "\\f";
16
+ case "\n":
17
+ return "\\n";
18
+ case "\r":
19
+ return "\\r";
20
+ case "\t":
21
+ return "\\t";
22
+ default:
23
+ return `\\u${char.codePointAt(0)?.toString(16).padStart(4, "0") ?? "0000"}`;
24
+ }
25
+ }
26
+
27
+ /**
28
+ * Repairs malformed JSON string literals by:
29
+ * - escaping raw control characters inside strings
30
+ * - doubling backslashes before invalid escape characters
31
+ */
32
+ export function repairJson(json: string): string {
33
+ let repaired = "";
34
+ let inString = false;
35
+
36
+ for (let index = 0; index < json.length; index++) {
37
+ const char = json[index];
38
+
39
+ if (!inString) {
40
+ repaired += char;
41
+ if (char === '"') {
42
+ inString = true;
43
+ }
44
+ continue;
45
+ }
46
+
47
+ if (char === '"') {
48
+ repaired += char;
49
+ inString = false;
50
+ continue;
51
+ }
52
+
53
+ if (char === "\\") {
54
+ const nextChar = json[index + 1];
55
+ if (nextChar === undefined) {
56
+ repaired += "\\\\";
57
+ continue;
58
+ }
59
+
60
+ if (nextChar === "u") {
61
+ const unicodeDigits = json.slice(index + 2, index + 6);
62
+ if (/^[0-9a-fA-F]{4}$/.test(unicodeDigits)) {
63
+ repaired += `\\u${unicodeDigits}`;
64
+ index += 5;
65
+ continue;
66
+ }
67
+ }
68
+
69
+ if (VALID_JSON_ESCAPES.has(nextChar)) {
70
+ repaired += `\\${nextChar}`;
71
+ index += 1;
72
+ continue;
73
+ }
74
+
75
+ repaired += "\\\\";
76
+ continue;
77
+ }
78
+
79
+ repaired += isControlCharacter(char) ? escapeControlCharacter(char) : char;
80
+ }
81
+
82
+ return repaired;
83
+ }
84
+
85
+ export function parseJsonWithRepair<T>(json: string): T {
86
+ try {
87
+ return JSON.parse(json) as T;
88
+ } catch (error) {
89
+ const repairedJson = repairJson(json);
90
+ if (repairedJson !== json) {
91
+ return JSON.parse(repairedJson) as T;
92
+ }
93
+ throw error;
94
+ }
95
+ }
96
+
97
+ /**
98
+ * Attempts to parse potentially incomplete JSON during streaming.
99
+ * Always returns a valid object, even if the JSON is incomplete.
100
+ *
101
+ * @param partialJson The partial JSON string from streaming
102
+ * @returns Parsed object or empty object if parsing fails
103
+ */
104
+ export function parseStreamingJson<T = Record<string, unknown>>(partialJson: string | undefined): T {
105
+ if (!partialJson || partialJson.trim() === "") {
106
+ return {} as T;
107
+ }
108
+
109
+ try {
110
+ return parseJsonWithRepair<T>(partialJson);
111
+ } catch {
112
+ try {
113
+ const result = partialParse(partialJson);
114
+ return (result ?? {}) as T;
115
+ } catch {
116
+ try {
117
+ const result = partialParse(repairJson(partialJson));
118
+ return (result ?? {}) as T;
119
+ } catch {
120
+ return {} as T;
121
+ }
122
+ }
123
+ }
124
+ }
@@ -1,13 +1,10 @@
1
1
  import {
2
2
  type Api,
3
+ type AssistantMessageEventStream as HostAssistantMessageEventStream,
3
4
  type AssistantMessage,
4
- type AssistantMessageEventStream,
5
- createAssistantMessageEventStream,
6
- type Context,
7
5
  type ImageContent,
8
6
  type Message,
9
7
  type Model,
10
- parseStreamingJson,
11
8
  type SimpleStreamOptions,
12
9
  type StopReason,
13
10
  type TextContent,
@@ -15,7 +12,11 @@ import {
15
12
  type Tool,
16
13
  type ToolCall,
17
14
  type ToolResultMessage,
15
+ type TranscriptContext,
18
16
  } from "@earendil-works/pi-ai";
17
+ import { type AssistantMessageEventStream, createAssistantMessageEventStream } from "./eventStream.ts";
18
+ import { hostSystemPrompt, hostTools } from "./hostContext.ts";
19
+ import { parseStreamingJson } from "./jsonParse.ts";
19
20
  import { messages as sdkMessages, native } from "@valni/switchboard";
20
21
 
21
22
  const SSE_DATA_PREFIX = "data:";
@@ -111,6 +112,7 @@ export function normalizeContextMessages(model: Model<KnownApi>, contextMessages
111
112
 
112
113
  const transformed: Message[] = [];
113
114
  for (const message of imageAware) {
115
+ if (message.role === "system") continue;
114
116
  if (message.role === "user") {
115
117
  transformed.push(message);
116
118
  continue;
@@ -246,14 +248,16 @@ function chatUserParts(content: (TextContent | ImageContent)[]): sdkMessages.Par
246
248
 
247
249
  function buildOpenAIChatRequest(
248
250
  model: Model<KnownApi>,
249
- context: Context,
251
+ context: TranscriptContext,
250
252
  options: SimpleStreamOptions | undefined,
251
253
  ): native.OpenAIChatRequest {
252
254
  const normalized = normalizeContextMessages(model, context.messages);
253
255
  const requestMessages: sdkMessages.Message[] = [];
256
+ const systemPrompt = hostSystemPrompt(context);
257
+ const tools = hostTools(context);
254
258
 
255
- if (context.systemPrompt) {
256
- requestMessages.push(sdkMessages.Message.system(sanitizeText(context.systemPrompt)));
259
+ if (systemPrompt) {
260
+ requestMessages.push(sdkMessages.Message.system(sanitizeText(systemPrompt)));
257
261
  }
258
262
 
259
263
  for (let index = 0; index < normalized.length; index++) {
@@ -320,7 +324,7 @@ function buildOpenAIChatRequest(
320
324
  messages: requestMessages,
321
325
  max_tokens: resolveMaxTokens(model, options),
322
326
  temperature: options?.temperature ?? null,
323
- tools: context.tools && context.tools.length > 0 ? context.tools.map((tool) => new JsonSchemaTool(tool)) : null,
327
+ tools: tools.length > 0 ? tools.map((tool) => new JsonSchemaTool(tool)) : null,
324
328
  reasoning_effort: reasoningLevel ?? null,
325
329
  });
326
330
  }
@@ -520,9 +524,9 @@ function completionsUrl(baseUrl: string): string {
520
524
 
521
525
  export function streamLocalModel(
522
526
  model: Model<Api>,
523
- context: Context,
527
+ context: TranscriptContext,
524
528
  options?: SimpleStreamOptions,
525
- ): AssistantMessageEventStream {
529
+ ): HostAssistantMessageEventStream {
526
530
  const stream = createAssistantMessageEventStream();
527
531
 
528
532
  (async () => {
@@ -579,7 +583,7 @@ export function streamLocalModel(
579
583
  }
580
584
 
581
585
  if (options?.signal?.aborted) throw new Error("Request was aborted");
582
- if (output.stopReason === "aborted" || output.stopReason === "error") {
586
+ if (output.stopReason === "aborted" || output.stopReason === "error" || output.stopReason === "pending") {
583
587
  throw new Error(output.errorMessage || "An unknown error occurred");
584
588
  }
585
589
  if (!state.sawTerminalEvent) throw new Error("The stream ended before a terminal event");
@@ -598,5 +602,9 @@ export function streamLocalModel(
598
602
  }
599
603
  })();
600
604
 
601
- return stream;
605
+ // The host's stream type has private members, so only its own class satisfies
606
+ // it nominally. The object here is the same shape and the host only iterates
607
+ // it, so the cast sits at this one boundary rather than importing the host's
608
+ // constructor and naming its package at runtime.
609
+ return stream as unknown as HostAssistantMessageEventStream;
602
610
  }
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2023 Promplate Dev Team
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,131 @@
1
+ # Partial JSON Parser
2
+
3
+ Sometimes we need **LLM (Large Language Models)** to produce **structural information** instead of natural language. The easiest way is to use JSON.
4
+
5
+ But before receiving the last token of response, the JSON is broken, which means you can't use `JSON.parse` to decode it. But we still want to stream the data to the user.
6
+
7
+ Here comes `partial-json`, a lightweight and customizable library for parsing partial JSON strings. Here is a [demo](https://promplate.dev/partial-json-parser).
8
+
9
+ (Note that there is [a Python implementation](https://github.com/promplate/partial-json-parser) too)
10
+
11
+ ## Installation
12
+
13
+ ```sh
14
+ npm i partial-json # or pnpm / bun / yarn
15
+ ```
16
+
17
+ `partial-json` is implemented purely in JavaScript, and have both `commonjs` and `esm` builds.
18
+
19
+ ## Usage
20
+
21
+ ### Importing the library
22
+
23
+ You can import the `parse` function and the `Allow` object from the library like this:
24
+
25
+ ```js
26
+ import { parse, Allow } from "partial-json";
27
+ ```
28
+
29
+ The `Allow` object is just an Enum for options. It determines what types can be partial. types not included in `allow` only appears after its completion can be ensured.
30
+
31
+ ### Parsing complete / partial JSON strings
32
+
33
+ The `parse` function works just like the built-in `JSON.parse` when parsing a complete JSON string:
34
+
35
+ ```js
36
+ let result = parse('{"key":"value"}');
37
+ console.log(result); // Outputs: { key: 'value' }
38
+ ```
39
+
40
+ You can parse a partial JSON string by passing an additional parameter to the `parse` function. This parameter is a **bitwise OR** of the constants from the `Allow` object:
41
+
42
+ (Note that you can directly import the constants you need from `partial-json`)
43
+
44
+ ```js
45
+ import { parse, STR, OBJ } from "partial-json";
46
+
47
+ result = parse('{"key": "v', STR | OBJ);
48
+ console.log(result); // Outputs: { key: 'v' }
49
+ ```
50
+
51
+ In this example, `Allow.STR` tells the parser that it's okay if a string is incomplete, and `Allow.OBJ` tells the parser so as an object. The parser then try to return as much data as it can.
52
+
53
+ If you don't allow partial strings, then it will not add `"key"` to the object because `"v` is not close:
54
+
55
+ ```js
56
+ result = parse('{"key": "v', OBJ);
57
+ console.log(result); // Outputs: {}
58
+
59
+ result = parse('{"key": "value"', OBJ);
60
+ console.log(result); // Outputs: { key: 'value' }
61
+ ```
62
+
63
+ Similarity, you can parse partial arrays or even partial special values if you allow it:
64
+
65
+ (Note that `allow` defaults to `Allow.ALL`)
66
+
67
+ ```js
68
+ result = parse('[ {"key1": "value1", "key2": [ "value2');
69
+ console.log(result); // Outputs: [ { key1: 'value1', key2: [ 'value2' ] } ]
70
+
71
+ result = parse("-Inf");
72
+ console.log(result); // Outputs: -Infinity
73
+ ```
74
+
75
+ ### Handling malformed JSON
76
+
77
+ If the JSON string is malformed, the `parse` function will throw an error:
78
+
79
+ ```js
80
+ parse("wrong"); // MalformedJSON [Error]: SyntaxError: Unexpected token 'w', "wrong" is not valid JSON at position 0
81
+ ```
82
+
83
+ ## API Reference
84
+
85
+ ### parse(jsonString, [allowPartial])
86
+
87
+ - `jsonString` `<string>`: The JSON string to parse.
88
+ - `allowPartial` `<number>`: Specify what kind of partialness is allowed during JSON parsing (default: `Allow.ALL`).
89
+
90
+ Returns the parsed JavaScript value.
91
+
92
+ ### Allow
93
+
94
+ An object that specifies what kind of partialness is allowed during JSON parsing. It has the following properties:
95
+
96
+ - `STR`: Allow partial string.
97
+ - `NUM`: Allow partial number.
98
+ - `ARR`: Allow partial array.
99
+ - `OBJ`: Allow partial object.
100
+ - `NULL`: Allow partial null.
101
+ - `BOOL`: Allow partial boolean.
102
+ - `NAN`: Allow partial NaN.
103
+ - `INFINITY`: Allow partial Infinity.
104
+ - `_INFINITY`: Allow partial -Infinity.
105
+ - `INF`: Allow both partial Infinity and -Infinity.
106
+ - `SPECIAL`: Allow all special values.
107
+ - `ATOM`: Allow all atomic values.
108
+ - `COLLECTION`: Allow all collection values.
109
+ - `ALL`: Allow all values.
110
+
111
+ ## Testing
112
+
113
+ To run the tests for this library, you should clone the repository and install the dependencies:
114
+
115
+ ```sh
116
+ git clone https://github.com/promplate/partial-json-parser-js.git
117
+ cd partial-json-parser-js
118
+ npm i
119
+ ```
120
+
121
+ Then, you can run the tests using [Vitest](https://vitest.dev/):
122
+
123
+ ```sh
124
+ npm run test
125
+ ```
126
+
127
+ Please note that while we strive to cover as many edge cases as possible, it's always possible that some cases might not be covered.
128
+
129
+ ## License
130
+
131
+ This project is licensed under the MIT License.
@@ -0,0 +1,220 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ exports.Allow = exports.MalformedJSON = exports.PartialJSON = exports.parseJSON = exports.parse = void 0;
18
+ const options_1 = require("./options");
19
+ Object.defineProperty(exports, "Allow", { enumerable: true, get: function () { return options_1.Allow; } });
20
+ __exportStar(require("./options"), exports);
21
+ class PartialJSON extends Error {
22
+ }
23
+ exports.PartialJSON = PartialJSON;
24
+ class MalformedJSON extends Error {
25
+ }
26
+ exports.MalformedJSON = MalformedJSON;
27
+ /**
28
+ * Parse incomplete JSON
29
+ * @param {string} jsonString Partial JSON to be parsed
30
+ * @param {number} allowPartial Specify what types are allowed to be partial, see {@link Allow} for details
31
+ * @returns The parsed JSON
32
+ * @throws {PartialJSON} If the JSON is incomplete (related to the `allow` parameter)
33
+ * @throws {MalformedJSON} If the JSON is malformed
34
+ */
35
+ function parseJSON(jsonString, allowPartial = options_1.Allow.ALL) {
36
+ if (typeof jsonString !== "string") {
37
+ throw new TypeError(`expecting str, got ${typeof jsonString}`);
38
+ }
39
+ if (!jsonString.trim()) {
40
+ throw new Error(`${jsonString} is empty`);
41
+ }
42
+ return _parseJSON(jsonString.trim(), allowPartial);
43
+ }
44
+ exports.parseJSON = parseJSON;
45
+ ;
46
+ const _parseJSON = (jsonString, allow) => {
47
+ const length = jsonString.length;
48
+ let index = 0;
49
+ const markPartialJSON = (msg) => {
50
+ throw new PartialJSON(`${msg} at position ${index}`);
51
+ };
52
+ const throwMalformedError = (msg) => {
53
+ throw new MalformedJSON(`${msg} at position ${index}`);
54
+ };
55
+ const parseAny = () => {
56
+ skipBlank();
57
+ if (index >= length)
58
+ markPartialJSON("Unexpected end of input");
59
+ if (jsonString[index] === '"')
60
+ return parseStr();
61
+ if (jsonString[index] === "{")
62
+ return parseObj();
63
+ if (jsonString[index] === "[")
64
+ return parseArr();
65
+ if (jsonString.substring(index, index + 4) === "null" || (options_1.Allow.NULL & allow && length - index < 4 && "null".startsWith(jsonString.substring(index)))) {
66
+ index += 4;
67
+ return null;
68
+ }
69
+ if (jsonString.substring(index, index + 4) === "true" || (options_1.Allow.BOOL & allow && length - index < 4 && "true".startsWith(jsonString.substring(index)))) {
70
+ index += 4;
71
+ return true;
72
+ }
73
+ if (jsonString.substring(index, index + 5) === "false" || (options_1.Allow.BOOL & allow && length - index < 5 && "false".startsWith(jsonString.substring(index)))) {
74
+ index += 5;
75
+ return false;
76
+ }
77
+ if (jsonString.substring(index, index + 8) === "Infinity" || (options_1.Allow.INFINITY & allow && length - index < 8 && "Infinity".startsWith(jsonString.substring(index)))) {
78
+ index += 8;
79
+ return Infinity;
80
+ }
81
+ if (jsonString.substring(index, index + 9) === "-Infinity" || (options_1.Allow._INFINITY & allow && 1 < length - index && length - index < 9 && "-Infinity".startsWith(jsonString.substring(index)))) {
82
+ index += 9;
83
+ return -Infinity;
84
+ }
85
+ if (jsonString.substring(index, index + 3) === "NaN" || (options_1.Allow.NAN & allow && length - index < 3 && "NaN".startsWith(jsonString.substring(index)))) {
86
+ index += 3;
87
+ return NaN;
88
+ }
89
+ return parseNum();
90
+ };
91
+ const parseStr = () => {
92
+ const start = index;
93
+ let escape = false;
94
+ index++; // skip initial quote
95
+ while (index < length && (jsonString[index] !== '"' || (escape && jsonString[index - 1] === "\\"))) {
96
+ escape = jsonString[index] === "\\" ? !escape : false;
97
+ index++;
98
+ }
99
+ if (jsonString.charAt(index) == '"') {
100
+ try {
101
+ return JSON.parse(jsonString.substring(start, ++index - Number(escape)));
102
+ }
103
+ catch (e) {
104
+ throwMalformedError(String(e));
105
+ }
106
+ }
107
+ else if (options_1.Allow.STR & allow) {
108
+ try {
109
+ return JSON.parse(jsonString.substring(start, index - Number(escape)) + '"');
110
+ }
111
+ catch (e) {
112
+ // SyntaxError: Invalid escape sequence
113
+ return JSON.parse(jsonString.substring(start, jsonString.lastIndexOf("\\")) + '"');
114
+ }
115
+ }
116
+ markPartialJSON("Unterminated string literal");
117
+ };
118
+ const parseObj = () => {
119
+ index++; // skip initial brace
120
+ skipBlank();
121
+ const obj = {};
122
+ try {
123
+ while (jsonString[index] !== "}") {
124
+ skipBlank();
125
+ if (index >= length && options_1.Allow.OBJ & allow)
126
+ return obj;
127
+ const key = parseStr();
128
+ skipBlank();
129
+ index++; // skip colon
130
+ try {
131
+ const value = parseAny();
132
+ obj[key] = value;
133
+ }
134
+ catch (e) {
135
+ if (options_1.Allow.OBJ & allow)
136
+ return obj;
137
+ else
138
+ throw e;
139
+ }
140
+ skipBlank();
141
+ if (jsonString[index] === ",")
142
+ index++; // skip comma
143
+ }
144
+ }
145
+ catch (e) {
146
+ if (options_1.Allow.OBJ & allow)
147
+ return obj;
148
+ else
149
+ markPartialJSON("Expected '}' at end of object");
150
+ }
151
+ index++; // skip final brace
152
+ return obj;
153
+ };
154
+ const parseArr = () => {
155
+ index++; // skip initial bracket
156
+ const arr = [];
157
+ try {
158
+ while (jsonString[index] !== "]") {
159
+ arr.push(parseAny());
160
+ skipBlank();
161
+ if (jsonString[index] === ",") {
162
+ index++; // skip comma
163
+ }
164
+ }
165
+ }
166
+ catch (e) {
167
+ if (options_1.Allow.ARR & allow) {
168
+ return arr;
169
+ }
170
+ markPartialJSON("Expected ']' at end of array");
171
+ }
172
+ index++; // skip final bracket
173
+ return arr;
174
+ };
175
+ const parseNum = () => {
176
+ if (index === 0) {
177
+ if (jsonString === "-")
178
+ throwMalformedError("Not sure what '-' is");
179
+ try {
180
+ return JSON.parse(jsonString);
181
+ }
182
+ catch (e) {
183
+ if (options_1.Allow.NUM & allow)
184
+ try {
185
+ return JSON.parse(jsonString.substring(0, jsonString.lastIndexOf("e")));
186
+ }
187
+ catch (e) { }
188
+ throwMalformedError(String(e));
189
+ }
190
+ }
191
+ const start = index;
192
+ if (jsonString[index] === "-")
193
+ index++;
194
+ while (jsonString[index] && ",]}".indexOf(jsonString[index]) === -1)
195
+ index++;
196
+ if (index == length && !(options_1.Allow.NUM & allow))
197
+ markPartialJSON("Unterminated number literal");
198
+ try {
199
+ return JSON.parse(jsonString.substring(start, index));
200
+ }
201
+ catch (e) {
202
+ if (jsonString.substring(start, index) === "-")
203
+ markPartialJSON("Not sure what '-' is");
204
+ try {
205
+ return JSON.parse(jsonString.substring(start, jsonString.lastIndexOf("e")));
206
+ }
207
+ catch (e) {
208
+ throwMalformedError(String(e));
209
+ }
210
+ }
211
+ };
212
+ const skipBlank = () => {
213
+ while (index < length && " \n\r\t".includes(jsonString[index])) {
214
+ index++;
215
+ }
216
+ };
217
+ return parseAny();
218
+ };
219
+ const parse = parseJSON;
220
+ exports.parse = parse;
@@ -0,0 +1,72 @@
1
+ "use strict";
2
+ /**
3
+ * Sometimes you don't allow every type to be partially parsed.
4
+ * For example, you may not want a partial number because it may increase its size gradually before it's complete.
5
+ * In this case, you can use the `Allow` object to control what types you allow to be partially parsed.
6
+ * @module
7
+ */
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.Allow = exports.ALL = exports.COLLECTION = exports.ATOM = exports.SPECIAL = exports.INF = exports._INFINITY = exports.INFINITY = exports.NAN = exports.BOOL = exports.NULL = exports.OBJ = exports.ARR = exports.NUM = exports.STR = void 0;
10
+ /**
11
+ * allow partial strings like `"hello \u12` to be parsed as `"hello "`
12
+ */
13
+ exports.STR = 0b000000001;
14
+ /**
15
+ * allow partial numbers like `123.` to be parsed as `123`
16
+ */
17
+ exports.NUM = 0b000000010;
18
+ /**
19
+ * allow partial arrays like `[1, 2,` to be parsed as `[1, 2]`
20
+ */
21
+ exports.ARR = 0b000000100;
22
+ /**
23
+ * allow partial objects like `{"a": 1, "b":` to be parsed as `{"a": 1}`
24
+ */
25
+ exports.OBJ = 0b000001000;
26
+ /**
27
+ * allow `nu` to be parsed as `null`
28
+ */
29
+ exports.NULL = 0b000010000;
30
+ /**
31
+ * allow `tr` to be parsed as `true`, and `fa` to be parsed as `false`
32
+ */
33
+ exports.BOOL = 0b000100000;
34
+ /**
35
+ * allow `Na` to be parsed as `NaN`
36
+ */
37
+ exports.NAN = 0b001000000;
38
+ /**
39
+ * allow `Inf` to be parsed as `Infinity`
40
+ */
41
+ exports.INFINITY = 0b010000000;
42
+ /**
43
+ * allow `-Inf` to be parsed as `-Infinity`
44
+ */
45
+ exports._INFINITY = 0b100000000;
46
+ exports.INF = exports.INFINITY | exports._INFINITY;
47
+ exports.SPECIAL = exports.NULL | exports.BOOL | exports.INF | exports.NAN;
48
+ exports.ATOM = exports.STR | exports.NUM | exports.SPECIAL;
49
+ exports.COLLECTION = exports.ARR | exports.OBJ;
50
+ exports.ALL = exports.ATOM | exports.COLLECTION;
51
+ /**
52
+ * Control what types you allow to be partially parsed.
53
+ * The default is to allow all types to be partially parsed, which in most casees is the best option.
54
+ * @example
55
+ * If you don't want to allow partial objects, you can use the following code:
56
+ * ```ts
57
+ * import { Allow, parse } from "partial-json";
58
+ * parse(`[{"a": 1, "b": 2}, {"a": 3,`, Allow.ARR); // [ { a: 1, b: 2 } ]
59
+ * ```
60
+ * Or you can use `~` to disallow a type:
61
+ * ```ts
62
+ * parse(`[{"a": 1, "b": 2}, {"a": 3,`, ~Allow.OBJ); // [ { a: 1, b: 2 } ]
63
+ * ```
64
+ * @example
65
+ * If you don't want to allow partial strings, you can use the following code:
66
+ * ```ts
67
+ * import { Allow, parse } from "partial-json";
68
+ * parse(`["complete string", "incompl`, ~Allow.STR); // [ 'complete string' ]
69
+ * ```
70
+ */
71
+ exports.Allow = { STR: exports.STR, NUM: exports.NUM, ARR: exports.ARR, OBJ: exports.OBJ, NULL: exports.NULL, BOOL: exports.BOOL, NAN: exports.NAN, INFINITY: exports.INFINITY, _INFINITY: exports._INFINITY, INF: exports.INF, SPECIAL: exports.SPECIAL, ATOM: exports.ATOM, COLLECTION: exports.COLLECTION, ALL: exports.ALL };
72
+ exports.default = exports.Allow;
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "partial-json",
3
+ "version": "0.1.7",
4
+ "description": "Parse partial JSON generated by LLM",
5
+ "keywords": [
6
+ "JSON",
7
+ "parser",
8
+ "LLM",
9
+ "nlp"
10
+ ],
11
+ "homepage": "https://promplate.dev/partial-json-parser",
12
+ "repository": "https://github.com/promplate/partial-json-parser-js",
13
+ "license": "MIT",
14
+ "author": "Muspi Merol <me@promplate.dev>",
15
+ "exports": {
16
+ ".": {
17
+ "types": "./dist/index.d.ts",
18
+ "default": "./dist/index.js"
19
+ },
20
+ "./options": {
21
+ "types": "./dist/options.d.ts",
22
+ "default": "./dist/options.js"
23
+ }
24
+ },
25
+ "main": "dist/index.js",
26
+ "module": "dist/index.js",
27
+ "types": "dist/index.d.ts",
28
+ "files": [
29
+ "dist"
30
+ ],
31
+ "scripts": {
32
+ "build": "tsc",
33
+ "coverage": "vitest run --coverage",
34
+ "test": "vitest",
35
+ "test:ui": "vitest --ui"
36
+ },
37
+ "devDependencies": {
38
+ "@vitest/coverage-istanbul": "^1.2.2",
39
+ "@vitest/ui": "^1.2.2",
40
+ "typescript": "^5.3.3",
41
+ "vitest": "^1.2.2"
42
+ }
43
+ }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "switchboard-local",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "type": "module",
5
5
  "description": "Local models in the pi model picker, managed end to end",
6
6
  "keywords": [
@@ -24,11 +24,12 @@
24
24
  ]
25
25
  },
26
26
  "dependencies": {
27
- "@valni/switchboard": "git+https://github.com/Valni-Labs/switchboard-ts.git#d132540a1fd86491ffabb9327ec38bb903f709a3"
27
+ "@valni/switchboard": "git+https://github.com/Valni-Labs/switchboard-ts.git#d132540a1fd86491ffabb9327ec38bb903f709a3",
28
+ "partial-json": "0.1.7"
28
29
  },
29
30
  "devDependencies": {
30
- "@earendil-works/pi-ai": "0.82.1",
31
- "@earendil-works/pi-coding-agent": "0.82.1",
31
+ "@earendil-works/pi-ai": ">=1.0.0",
32
+ "@earendil-works/pi-coding-agent": ">=1.0.0",
32
33
  "typescript": "5.9.3"
33
34
  }
34
35
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@valni/cli",
3
3
  "private": true,
4
- "version": "0.1.5",
4
+ "version": "0.1.7",
5
5
  "description": "Coding agent CLI with read, bash, edit, write tools and session management",
6
6
  "type": "module",
7
7
  "valniConfig": {
@@ -38,11 +38,11 @@
38
38
  "prepublishOnly": "npm run clean && npm run build && npm run shrinkwrap"
39
39
  },
40
40
  "dependencies": {
41
- "@valni/agent-core": "^0.1.5",
42
- "@valni/ai": "^0.1.5",
43
- "@valni/tui": "^0.1.5",
44
- "pi-switchboard": "git+https://github.com/Valni-Labs/pi-switchboard.git#b35310164e79b3f99bfd697622527a8458e1719d",
45
- "switchboard-local": "git+https://github.com/Valni-Labs/switchboard-local.git#bda14d2d85735da4519f4545b598b4c5e97e7d3b",
41
+ "@valni/agent-core": "^0.1.7",
42
+ "@valni/ai": "^0.1.7",
43
+ "@valni/tui": "^0.1.7",
44
+ "pi-switchboard": "git+https://github.com/Valni-Labs/pi-switchboard.git#774f1a7985a755fcd483313b729864aa8a9418c3",
45
+ "switchboard-local": "git+https://github.com/Valni-Labs/switchboard-local.git#4ec07263591a4f329fd12eab04d62d6c455be666",
46
46
  "@silvia-odwyer/photon-node": "0.3.4",
47
47
  "chalk": "5.6.2",
48
48
  "cross-spawn": "7.0.6",
package/runtime/valni CHANGED
@@ -1,4 +1,4 @@
1
1
  [diffend] Oversized file quarantined before diffing.
2
2
  name: package/runtime/valni
3
3
  size: 76058832 bytes
4
- sha256: 9b45d462f5ec1584ac199c4d4056aae97d26bccca546f0040de9724ea86984b1
4
+ sha256: 270e1c1d383b4800d98c432e23186cd63690ca5c8387a97cbcd4bd8057343ad1