Implement extensions/vision/index.ts (factory, registration, unconfigured no-op) #260

Closed
opened 2026-09-17 03:34:28 +00:00 by david · 1 comment
Owner

Summary

Create extensions/vision/index.ts: the extension's default-export factory. It loads .env + process.env, resolves the config, and either registers the vision tool or logs a notice and registers nothing when DEEPSEEK_API_KEY is absent. It also exports a registerVisionTools seam and a createClient injection point for tests.

Background

Depends on: #254, #257, #258

pi loads an extension by importing its file and calling the default export with the ExtensionAPI and any package options (pinned pi docs, docs/extensions.md). Everything below this file is pure library code with injected dependencies; this module is where the real environment meets them.

The unconfigured path is a hard requirement: with no key, pi.registerTool must never be called, so a user without DeepSeek credentials gets a clean pi boot and no phantom tool. This mirrors extensions/mongodb/index.ts, which returns early and registers nothing when MONGODB_URI is missing.

The test seam follows the repo's established shape: registerVisionTools(pi, client, config) is exported so index.test.ts can drive registration with a fake ExtensionAPI and a fake client, and options.createClient lets the factory's own wiring be exercised without a live HTTP client.

There is no session_shutdown work to do — each call is a stateless read-only request that writes nothing.

Documentation Required

A separate process downloads these into the listed folders before this issue is implemented. Check the folder for the actual reference material before starting.

docs/reference/pi-coding-agent/

  • Local package docs: node_modules/@earendil-works/pi-coding-agent/docs/extensions.md — the default-export extension signature (pi: ExtensionAPI, options?) => void | Promise<void>, ExtensionAPI.registerTool, and an example of an extension factory that conditionally registers tools.
  • Local types: node_modules/@earendil-works/pi-coding-agent/dist/**/*.d.ts — the ExtensionAPI and ToolDefinition types to import.

docs/reference/deepseek-api/

Implementation Details

Public surface:

export interface VisionExtensionOptions {
  createClient?: (config: VisionConfig) => VisionClient;
}

export function registerVisionTools(
  pi: ExtensionAPI,
  client: VisionClient,
  config: VisionConfig,
): void;

export default async function (
  pi: ExtensionAPI,
  options?: VisionExtensionOptions,
): Promise<void>;

Factory behaviour:

  1. const env = { ...loadEnvFile(), ...process.env };
  2. const config = resolveConfig(env);
  3. If config is null: console.log("vision extension: DEEPSEEK_API_KEY not set — vision tool not registered.") and return; — no tool, no client, no throw.
  4. Otherwise: const client = options.createClient ? options.createClient(config) : createHttpVisionClient(config); then registerVisionTools(pi, client, config);.

registerVisionTools calls exactly pi.registerTool(visionTool(client, config)) — one registration per call.

Notes:

  • resolveConfig may throw a ToolError (config) for an invalid numeric env var. Let it propagate — that is intentional fail-loud-at-load behaviour and is the factory's documented contract; do not swallow it.
  • Do not add session_shutdown handling: nothing is opened or held.
  • Follow extensions/mongodb/index.ts for doc-comment style and the register*Tools seam.

Test file extensions/vision/index.test.ts (TDD, fake pi object, no network):

  • no key ⇒ the default export resolves, registerTool is never called, and the notice is logged (capture console.log).
  • key present ⇒ exactly one registerTool call and the registered definition's name === "vision".
  • options.createClient is honoured: the injected factory receives the resolved config and its returned client is the one passed to registerVisionTools.
  • registerVisionTools registers exactly one tool with the supplied client/config.
  • An invalid VISION_MAX_TOKENS in the env ⇒ the factory rejects with a ToolError whose category is config (proves config errors are not swallowed).

Acceptance Criteria

  • The default export resolves with no key, logs the unconfigured notice, and never calls pi.registerTool.
  • With a key present, exactly one tool is registered and its name is vision.
  • options.createClient, when supplied, is used instead of the real HTTP client and receives the resolved config.
  • registerVisionTools(pi, client, config) is exported and registers exactly one tool.
  • Config ToolErrors propagate out of the factory rather than being swallowed.
  • extensions/vision/index.test.ts passes with node --test extensions/vision/index.test.ts and makes no network calls.

Test Plan

cd /Users/david/Projects/pi-extensions-and-skills
node --test extensions/vision/index.test.ts

Expected: all tests pass with no network access.

Manual unconfigured boot check (the repo root has no DEEPSEEK_API_KEY by assumption):

node --experimental-strip-types -e "
import('./extensions/vision/index.ts').then(async (m) => {
  const calls = [];
  const pi = { registerTool: (t) => calls.push(t.name) };
  await m.default(pi, {});
  console.log('registered:', JSON.stringify(calls));
})
"

Expected: the unconfigured notice is logged and registered: [].

## Summary Create `extensions/vision/index.ts`: the extension's default-export factory. It loads `.env` + `process.env`, resolves the config, and either registers the `vision` tool or logs a notice and registers nothing when `DEEPSEEK_API_KEY` is absent. It also exports a `registerVisionTools` seam and a `createClient` injection point for tests. ## Background **Depends on:** #254, #257, #258 pi loads an extension by importing its file and calling the default export with the `ExtensionAPI` and any package options (pinned pi docs, `docs/extensions.md`). Everything below this file is pure library code with injected dependencies; this module is where the real environment meets them. The unconfigured path is a hard requirement: with no key, `pi.registerTool` must never be called, so a user without DeepSeek credentials gets a clean pi boot and no phantom tool. This mirrors `extensions/mongodb/index.ts`, which returns early and registers nothing when `MONGODB_URI` is missing. The test seam follows the repo's established shape: `registerVisionTools(pi, client, config)` is exported so `index.test.ts` can drive registration with a fake `ExtensionAPI` and a fake client, and `options.createClient` lets the factory's own wiring be exercised without a live HTTP client. There is no `session_shutdown` work to do — each call is a stateless read-only request that writes nothing. ## Documentation Required A separate process downloads these into the listed folders before this issue is implemented. Check the folder for the actual reference material before starting. **`docs/reference/pi-coding-agent/`** - Local package docs: `node_modules/@earendil-works/pi-coding-agent/docs/extensions.md` — the default-export extension signature `(pi: ExtensionAPI, options?) => void | Promise<void>`, `ExtensionAPI.registerTool`, and an example of an extension factory that conditionally registers tools. - Local types: `node_modules/@earendil-works/pi-coding-agent/dist/**/*.d.ts` — the `ExtensionAPI` and `ToolDefinition` types to import. **`docs/reference/deepseek-api/`** - https://api-docs.deepseek.com/quick_start/pricing/ — confirms `deepseek-flash`/`https://api.deepseek.com` as defaults (referenced from `config.ts`). ## Implementation Details Public surface: ```ts export interface VisionExtensionOptions { createClient?: (config: VisionConfig) => VisionClient; } export function registerVisionTools( pi: ExtensionAPI, client: VisionClient, config: VisionConfig, ): void; export default async function ( pi: ExtensionAPI, options?: VisionExtensionOptions, ): Promise<void>; ``` Factory behaviour: 1. `const env = { ...loadEnvFile(), ...process.env };` 2. `const config = resolveConfig(env);` 3. If `config` is `null`: `console.log("vision extension: DEEPSEEK_API_KEY not set — vision tool not registered.")` and `return;` — no tool, no client, no throw. 4. Otherwise: `const client = options.createClient ? options.createClient(config) : createHttpVisionClient(config);` then `registerVisionTools(pi, client, config);`. `registerVisionTools` calls exactly `pi.registerTool(visionTool(client, config))` — one registration per call. Notes: - `resolveConfig` may throw a `ToolError` (`config`) for an invalid numeric env var. Let it propagate — that is intentional fail-loud-at-load behaviour and is the factory's documented contract; do not swallow it. - Do not add `session_shutdown` handling: nothing is opened or held. - Follow `extensions/mongodb/index.ts` for doc-comment style and the `register*Tools` seam. Test file `extensions/vision/index.test.ts` (TDD, fake `pi` object, no network): - no key ⇒ the default export resolves, `registerTool` is never called, and the notice is logged (capture `console.log`). - key present ⇒ exactly one `registerTool` call and the registered definition's `name === "vision"`. - `options.createClient` is honoured: the injected factory receives the resolved `config` and its returned client is the one passed to `registerVisionTools`. - `registerVisionTools` registers exactly one tool with the supplied client/config. - An invalid `VISION_MAX_TOKENS` in the env ⇒ the factory rejects with a `ToolError` whose category is `config` (proves config errors are not swallowed). ## Acceptance Criteria - [ ] The default export resolves with no key, logs the unconfigured notice, and never calls `pi.registerTool`. - [ ] With a key present, exactly one tool is registered and its `name` is `vision`. - [ ] `options.createClient`, when supplied, is used instead of the real HTTP client and receives the resolved config. - [ ] `registerVisionTools(pi, client, config)` is exported and registers exactly one tool. - [ ] Config `ToolError`s propagate out of the factory rather than being swallowed. - [ ] `extensions/vision/index.test.ts` passes with `node --test extensions/vision/index.test.ts` and makes no network calls. ## Test Plan ```bash cd /Users/david/Projects/pi-extensions-and-skills node --test extensions/vision/index.test.ts ``` Expected: all tests pass with no network access. Manual unconfigured boot check (the repo root has no `DEEPSEEK_API_KEY` by assumption): ```bash node --experimental-strip-types -e " import('./extensions/vision/index.ts').then(async (m) => { const calls = []; const pi = { registerTool: (t) => calls.push(t.name) }; await m.default(pi, {}); console.log('registered:', JSON.stringify(calls)); }) " ``` Expected: the unconfigured notice is logged and `registered: []`.
david self-assigned this 2026-09-17 03:34:28 +00:00
david closed this issue 2026-09-17 11:25:00 +00:00
Author
Owner

pi-loop opened and merged a pull request for this issue: #273

pi-loop opened and merged a pull request for this issue: https://git.excelera.net/david/pi-extensions-and-skills/pulls/273
Sign in to join this conversation.
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
david/pi-extensions-and-skills#260
No description provided.