paseo-plugin-zcode-provider 0.1.0 → 0.2.0

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.
@@ -1,2 +1,2 @@
1
1
  // Generated from package.json by npm prepare / prebuild.
2
- export const PROVIDER_VERSION = "0.1.0";
2
+ export const PROVIDER_VERSION = "0.2.0";
@@ -62,6 +62,23 @@ const operations = new Set([
62
62
  "attachmentChunkV4",
63
63
  "attachmentCommitV4",
64
64
  "attachmentAbortV4",
65
+ "oauth.restoreCachedSessionState",
66
+ "oauth.getActiveProvider",
67
+ "oauth.startOAuthWithPolling",
68
+ "oauth.pollPendingOAuth",
69
+ "oauth.cancelPending",
70
+ "oauth.logout",
71
+ "setting.get",
72
+ "setting.update",
73
+ "provider-settings.getView",
74
+ "provider-settings.refresh",
75
+ "provider-settings.createPersonalProvider",
76
+ "provider-settings.savePersonalProviderOverlay",
77
+ "provider-settings.deletePersonalProvider",
78
+ "provider-settings.addPersonalModel",
79
+ "provider-settings.deletePersonalModel",
80
+ "provider-settings.setPersonalModelEnabled",
81
+ "usage-stats.getEntitlementSnapshot",
65
82
  "event",
66
83
  "catalog",
67
84
  "session.create",
@@ -75,7 +75,7 @@ async function configuredPaths(environment: NodeJS.ProcessEnv): Promise<{
75
75
  if (managed === undefined)
76
76
  throw new AdapterError(
77
77
  "RUNTIME_SETUP_REQUIRED",
78
- "ZCode runtime is not set up. Open Settings → Plugins → zcode-provider to set it up.",
78
+ "ZCode runtime is not set up. Install it in Settings → Plugins → zcode-provider → Settings → Runtime.",
79
79
  );
80
80
  return { ...managed, source: "managed" };
81
81
  }
@@ -33,4 +33,4 @@ export function assessCompatibility(identity: RuntimeIdentity): {
33
33
  }
34
34
 
35
35
  // Last reviewed public changelog release; source versions are not releases.
36
- export const LAST_REVIEWED_ZCODE_RELEASE = "3.14.3";
36
+ export const LAST_REVIEWED_ZCODE_RELEASE = "3.14.4";
@@ -28,6 +28,12 @@ import {
28
28
  import type { Logger } from "../logger.js";
29
29
  import { PROVIDER_VERSION } from "../build-info.js";
30
30
 
31
+ export type SettingsChannel =
32
+ | "oauth"
33
+ | "setting"
34
+ | "provider-settings"
35
+ | "usage-stats";
36
+
31
37
  export interface HostSubscription {
32
38
  readonly id: string;
33
39
  dispose(): Promise<void>;
@@ -265,7 +271,7 @@ export class ZCodeHostBridge implements HostBridge {
265
271
  }) + "\n",
266
272
  );
267
273
  const v4 = helloMessageSchema.parse(
268
- await this.call("zcode-agent", "helloConversationV4", undefined, 10_000),
274
+ await this.call("zcode-agent", "helloConversationV4", [], 10_000),
269
275
  );
270
276
  if (
271
277
  v4.clientMode !== "desktop-continuous" ||
@@ -278,18 +284,20 @@ export class ZCodeHostBridge implements HostBridge {
278
284
  await this.call(
279
285
  "zcode-agent",
280
286
  "initializeConversationV4",
281
- {
282
- kind: "clientHello",
283
- protocolVersion: 3,
284
- clientId: "paseo-zcode-provider",
285
- appVersion: PROVIDER_VERSION,
286
- },
287
+ [
288
+ {
289
+ kind: "clientHello",
290
+ protocolVersion: 3,
291
+ clientId: "paseo-zcode-provider",
292
+ appVersion: PROVIDER_VERSION,
293
+ },
294
+ ],
287
295
  10_000,
288
296
  );
289
297
  await this.call(
290
298
  "zcode-agent",
291
299
  "syncAppRuntimePreferences",
292
- { askUserQuestionAutoResolutionEnabled: false },
300
+ [{ askUserQuestionAutoResolutionEnabled: false }],
293
301
  10_000,
294
302
  );
295
303
  }
@@ -314,7 +322,7 @@ export class ZCodeHostBridge implements HostBridge {
314
322
  private async call(
315
323
  channel: string,
316
324
  method: string,
317
- params: unknown,
325
+ args: readonly unknown[],
318
326
  timeoutMs: number,
319
327
  ): Promise<unknown> {
320
328
  if (!this.client)
@@ -323,13 +331,7 @@ export class ZCodeHostBridge implements HostBridge {
323
331
  let timer: ReturnType<typeof setTimeout> | undefined;
324
332
  try {
325
333
  return await Promise.race([
326
- this.client
327
- .getChannel(channel)
328
- .call(
329
- method,
330
- params === undefined ? [] : [params],
331
- cancellation.token,
332
- ),
334
+ this.client.getChannel(channel).call(method, args, cancellation.token),
333
335
  new Promise<never>((_, reject) => {
334
336
  timer = setTimeout(() => {
335
337
  reject(new AdapterError("NATIVE_TIMEOUT", "ZCode RPC timed out"));
@@ -342,6 +344,12 @@ export class ZCodeHostBridge implements HostBridge {
342
344
  cancellation.dispose();
343
345
  }
344
346
  }
347
+ private async ensureOpen(): Promise<void> {
348
+ await this.ready;
349
+ if (this.failure) throw this.failure;
350
+ if (this.closing)
351
+ throw new AdapterError("NATIVE_EXITED", "ZCode Server is closing");
352
+ }
345
353
  async request<Schema extends z.ZodType>(
346
354
  method: string,
347
355
  params: unknown,
@@ -349,14 +357,12 @@ export class ZCodeHostBridge implements HostBridge {
349
357
  timeoutMs = 30_000,
350
358
  ): Promise<z.output<Schema>> {
351
359
  try {
352
- await this.ready;
353
- if (this.failure) throw this.failure;
354
- if (this.closing)
355
- throw new AdapterError("NATIVE_EXITED", "ZCode Server is closing");
360
+ await this.ensureOpen();
361
+ const args = params === undefined ? [] : [params];
356
362
  let result: unknown;
357
363
  if (method === "readModelSelection")
358
364
  result = projectModelSelection(
359
- await this.call("model-selection", "getView", params, timeoutMs),
365
+ await this.call("model-selection", "getView", args, timeoutMs),
360
366
  );
361
367
  else
362
368
  result = await this.call(
@@ -365,7 +371,7 @@ export class ZCodeHostBridge implements HostBridge {
365
371
  ? "usage-stats"
366
372
  : "zcode-agent",
367
373
  method,
368
- params,
374
+ args,
369
375
  timeoutMs,
370
376
  );
371
377
  return resultSchema.parse(result);
@@ -377,6 +383,27 @@ export class ZCodeHostBridge implements HostBridge {
377
383
  });
378
384
  }
379
385
  }
386
+ /** Calls an official account or settings service with positional arguments. */
387
+ async invoke<Schema extends z.ZodType>(
388
+ channel: SettingsChannel,
389
+ method: string,
390
+ args: readonly unknown[],
391
+ resultSchema: Schema,
392
+ timeoutMs = 30_000,
393
+ ): Promise<z.output<Schema>> {
394
+ try {
395
+ await this.ensureOpen();
396
+ return resultSchema.parse(
397
+ await this.call(channel, method, args, timeoutMs),
398
+ );
399
+ } catch (error) {
400
+ throw diagnosticError(error, {
401
+ ...this.diagnostic,
402
+ stage: "request",
403
+ operation: `${channel}.${method}`,
404
+ });
405
+ }
406
+ }
380
407
  async subscribe(
381
408
  target: { workspacePath: string; sessionId: string },
382
409
  handler: (frame: ConversationTopicWireCandidate) => Promise<void> | void,
@@ -8,6 +8,7 @@ import type {
8
8
  } from "@getpaseo/plugin/server/provider";
9
9
  import type {
10
10
  ConversationRow,
11
+ HookInvocationRow,
11
12
  TimelineMarkerPayload,
12
13
  } from "./vendor/zcode/packages/shared/src/zcode-protocol-v4/rows.js";
13
14
  import type {
@@ -160,13 +161,12 @@ export function rowTimeline(
160
161
  message: `${row.displayName} (${row.artifactType}, ${row.sizeBytes} bytes)`,
161
162
  };
162
163
  case "hookInvocation":
163
- return {
164
- id,
165
- type: "notification",
166
- level: "info",
167
- message: `${row.hookEventName}: ${row.executions.map((h) => `${h.displayName} (${h.outcome ?? h.state})`).join(", ")}`,
168
- };
164
+ return hookTimeline(id, row);
169
165
  case "timelineMarker":
166
+ // Paseo appends every notification it receives, so a changing row is
167
+ // reported once, in its final state.
168
+ if (row.marker.type === "compact" && row.marker.status === "running")
169
+ return;
170
170
  return {
171
171
  id,
172
172
  type: "notification",
@@ -175,6 +175,48 @@ export function rowTimeline(
175
175
  };
176
176
  }
177
177
  }
178
+ // Like ZCode Desktop, which keeps hooks out of the conversation and lists
179
+ // them on request, successful hooks are not shown. Paseo appends every
180
+ // notification it receives, so an invocation is reported once, after it ends,
181
+ // and only when a hook that ran did not succeed.
182
+ function hookTimeline(
183
+ id: string,
184
+ row: HookInvocationRow,
185
+ ): ProviderTimelineItem | undefined {
186
+ if (
187
+ row.state === "running" ||
188
+ row.executions.some((h) => h.state === "running")
189
+ )
190
+ return;
191
+ const problems = row.executions.flatMap((hook) => {
192
+ if (!hook.didExecute) return [];
193
+ const outcome =
194
+ hook.outcome && hook.outcome !== "success"
195
+ ? hook.outcome
196
+ : hook.state === "failed"
197
+ ? "failed"
198
+ : undefined;
199
+ return outcome ? [{ hook, outcome }] : [];
200
+ });
201
+ if (problems.length === 0) return;
202
+ const outcomes = new Set(problems.map((p) => p.outcome));
203
+ return {
204
+ id,
205
+ type: "notification",
206
+ level: outcomes.has("failed")
207
+ ? "error"
208
+ : outcomes.has("blocked") || outcomes.has("timed_out")
209
+ ? "warning"
210
+ : "info",
211
+ message: `${row.hookEventName} hook ${problems
212
+ .map(
213
+ ({ hook, outcome }) =>
214
+ `${outcome.replace("_", " ")}: ${hook.displayName}${hook.blockReason ? ` (${hook.blockReason})` : ""}`,
215
+ )
216
+ .join("; ")}`,
217
+ };
218
+ }
219
+
178
220
  export function usage(state: ConversationSnapshot): ProviderUsage {
179
221
  return {
180
222
  inputTokens: state.usage.cumulative.inputTokens,
@@ -0,0 +1,81 @@
1
+ import type {
2
+ ProviderStatus,
3
+ ProviderStatusRequest,
4
+ } from "@getpaseo/plugin/server/provider";
5
+ import {
6
+ assertRuntimeSupported,
7
+ discoverRuntime,
8
+ } from "./discovery/discover.js";
9
+ import { AdapterError } from "./errors.js";
10
+ import { logger } from "./logger.js";
11
+
12
+ export interface RuntimeStatusOptions {
13
+ readonly environment?: NodeJS.ProcessEnv;
14
+ readonly discover?: typeof discoverRuntime;
15
+ /** How long a result is reused. Paseo asks on every catalog load and agent start. */
16
+ readonly ttlMs?: number;
17
+ readonly now?: () => number;
18
+ }
19
+
20
+ // Same shape as Paseo's built-in provider diagnostics; the daemon appends the
21
+ // model count and snapshot status as further indented lines.
22
+ const format = (entries: readonly [string, string][]) =>
23
+ ["ZCode", ...entries.map(([label, value]) => ` ${label}: ${value}`)].join(
24
+ "\n",
25
+ );
26
+
27
+ async function check(
28
+ environment: NodeJS.ProcessEnv,
29
+ discover: typeof discoverRuntime,
30
+ ): Promise<ProviderStatus> {
31
+ try {
32
+ const runtime = await discover({ environment });
33
+ assertRuntimeSupported(runtime);
34
+ const { appVersion, cliVersion, nodeVersion, platform } = runtime.identity;
35
+ return {
36
+ available: true,
37
+ diagnostic: format([
38
+ [
39
+ "Runtime",
40
+ `${runtime.source === "managed" ? "managed" : "PASEO_ZCODE_RUNTIME"}, Server ${appVersion}, Agent ${cliVersion}`,
41
+ ],
42
+ ["Node.js", nodeVersion],
43
+ ["Platform", platform],
44
+ ]),
45
+ };
46
+ } catch (error) {
47
+ // Discovery errors carry fixed messages; anything else stays in the log.
48
+ if (!(error instanceof AdapterError))
49
+ logger.error("zcode.provider.status.failed", error);
50
+ return {
51
+ available: false,
52
+ diagnostic: format([
53
+ [
54
+ "Error",
55
+ error instanceof AdapterError
56
+ ? error.message
57
+ : "ZCode runtime could not be inspected",
58
+ ],
59
+ ]),
60
+ };
61
+ }
62
+ }
63
+
64
+ /**
65
+ * Availability for Paseo's provider snapshot and diagnostics (Paseo 0.11+;
66
+ * older daemons ignore it). It inspects the runtime without starting a Server,
67
+ * so sign-in problems still surface when a session opens.
68
+ */
69
+ export function createRuntimeStatus(options: RuntimeStatusOptions = {}) {
70
+ const environment = options.environment ?? process.env;
71
+ const discover = options.discover ?? discoverRuntime;
72
+ const ttlMs = options.ttlMs ?? 30_000;
73
+ const now = options.now ?? Date.now;
74
+ let cached: { at: number; result: Promise<ProviderStatus> } | undefined;
75
+ return (_request?: ProviderStatusRequest): Promise<ProviderStatus> => {
76
+ if (cached && now() - cached.at < ttlMs) return cached.result;
77
+ const entry = { at: now(), result: check(environment, discover) };
78
+ cached = entry;
79
+ return entry.result;
80
+ };
81
+ }
@@ -27,6 +27,7 @@ import {
27
27
  runRuntimeSmoke,
28
28
  } from "./discovery/discover.js";
29
29
  import { ZCodeHostBridge, type HostBridge } from "./host/bridge.js";
30
+ import { createRuntimeStatus } from "./provider-status.js";
30
31
  import { logger } from "./logger.js";
31
32
  import { MINIMUM_ZCODE_VERSION } from "./discovery/manifest.js";
32
33
  import {
@@ -68,11 +69,11 @@ export type BridgeFactory = (
68
69
  workspace?: string,
69
70
  ) => Promise<HostBridge>;
70
71
 
71
- async function createHost(
72
+ export async function createHost(
72
73
  environment: Readonly<Record<string, string>>,
73
74
  signal: AbortSignal,
74
75
  workspace?: string,
75
- ): Promise<HostBridge> {
76
+ ): Promise<ZCodeHostBridge> {
76
77
  const env = { ...process.env, ...environment };
77
78
  const runtime = await discoverRuntime({ environment: env, signal });
78
79
  assertRuntimeSupported(runtime);
@@ -114,6 +115,7 @@ interface SessionEntry {
114
115
  export function createZCodeProvider(
115
116
  bridgeFactory: BridgeFactory = createHost,
116
117
  persistenceStore = new SessionPersistenceStore(),
118
+ status: ProviderRegistration["status"] = createRuntimeStatus(),
117
119
  ): ProviderRegistration {
118
120
  return {
119
121
  id: "zcode",
@@ -121,6 +123,9 @@ export function createZCodeProvider(
121
123
  description:
122
124
  "ZCode workspace agent with native planning, tools, and model selection",
123
125
  icon: "icon.svg",
126
+ // No `command`: Paseo would look it up on PATH, but the runtime is the
127
+ // plugin's managed installation or the PASEO_ZCODE_* override.
128
+ status,
124
129
  async connect(request) {
125
130
  if (!request.versions.includes(PROVIDER_PROTOCOL_VERSION))
126
131
  throw new Error("ZCode requires provider protocol version 1");
@@ -812,7 +817,9 @@ function publicError(error: unknown): ProviderError {
812
817
  RUNTIME_DISCOVERY_FAILED:
813
818
  "ZCode installation or required host structure could not be inspected. See the diagnostic for the failure stage.",
814
819
  RUNTIME_SETUP_REQUIRED:
815
- "ZCode runtime is not set up. Open Settings → Plugins → zcode-provider to set it up.",
820
+ "ZCode runtime is not set up. Install it in Settings → Plugins → zcode-provider → Settings → Runtime.",
821
+ AUTH_REQUIRED:
822
+ "No ZCode model provider is usable. Sign in or add a provider in Settings → Plugins → zcode-provider → Settings → Account.",
816
823
  PERSISTENCE_VERSION_UNSUPPORTED:
817
824
  "ZCode persistence version is unsupported. Create a new Paseo ZCode session.",
818
825
  PERSISTENCE_INVALID:
@@ -1,4 +1,3 @@
1
- import { join } from "node:path";
2
1
  import type { RuntimeSetupStatus } from "../shared/runtime-setup.js";
3
2
  import { AdapterError } from "./errors.js";
4
3
  import {
@@ -14,20 +13,6 @@ const LICENSES = {
14
13
  "Unofficial build of ZCode (Apache-2.0); third-party terms in THIRD-PARTY-NOTICES.md",
15
14
  } as const;
16
15
 
17
- // Quoted for the shell a user is most likely to paste into on that OS.
18
- export function loginCommand(
19
- executable: string,
20
- installRoot: string,
21
- windows = process.platform === "win32",
22
- ): string {
23
- const script = join(installRoot, "bin", "zcode.mjs");
24
- return windows
25
- ? `& ${powershellQuote(executable)} ${powershellQuote(script)} login`
26
- : `${posixQuote(executable)} ${posixQuote(script)} login`;
27
- }
28
- const posixQuote = (value: string) => `'${value.replaceAll("'", `'\\''`)}'`;
29
- const powershellQuote = (value: string) => `'${value.replaceAll("'", "''")}'`;
30
-
31
16
  export function createRuntimeSetupHandlers(
32
17
  installer: ManagedRuntimeInstaller,
33
18
  environment: NodeJS.ProcessEnv = process.env,
@@ -63,24 +48,14 @@ export function createRuntimeSetupHandlers(
63
48
  directory: managedRuntimeRoot(environment),
64
49
  components: [],
65
50
  };
66
- const components = [
67
- await component(layout.node),
68
- await component(layout.zcode),
69
- ];
70
51
  return {
71
52
  ...base,
72
53
  supported: true,
73
54
  directory: layout.root,
74
- components,
75
- ...(components.every((entry) => entry.installed)
76
- ? {
77
- loginCommand: loginCommand(
78
- layout.node.executable,
79
- layout.zcode.directory,
80
- layout.platform.startsWith("win-"),
81
- ),
82
- }
83
- : {}),
55
+ components: [
56
+ await component(layout.node),
57
+ await component(layout.zcode),
58
+ ],
84
59
  };
85
60
  },
86
61
  install: async () => ({ started: installer.start() }),