Skip to content

refactor: move registry item installation to the core#184

Merged
MihalyToth20 merged 71 commits intomainfrom
dev
Feb 8, 2026
Merged

refactor: move registry item installation to the core#184
MihalyToth20 merged 71 commits intomainfrom
dev

Conversation

@MihalyToth20
Copy link
Collaborator

@MihalyToth20 MihalyToth20 commented Jan 19, 2026

Summary by CodeRabbit

  • New Features
    • Registry install moved into core with inline-file support; OpenCode (oc) subcommands and native‑host installer added; browser config storage for web/extension environments.
  • Bug Fixes
    • Graceful exit when no registry component selected; clearer missing‑component reporting; path validation for serve commands.
  • Documentation
    • Large docs and guides (registry authoring, testing, scaffolding, Git workflow, OpenCode templates/agents, installers).
  • Tests
    • New unit tests for registry API, sorting, and installer flows.
  • Chores
    • Version bumps and packaging/bundling scripts added.

@changeset-bot
Copy link

changeset-bot bot commented Jan 19, 2026

🦋 Changeset detected

Latest commit: 1970010

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 6 packages
Name Type
@calycode/core Minor
@calycode/cli Minor
@repo/types Patch
@calycode/xano-skills Minor
@calycode/browser-consumer Patch
@repo/utils Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 19, 2026

Important

Review skipped

Auto incremental reviews are disabled on this repository.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

  • 🔍 Trigger a full review

Walkthrough

Moves registry functionality from the CLI into core: adds an in-memory registry client with caching, new install-to-Xano flow, registry types and schema updates, CLI now prompts/selects components and delegates installation to core; includes tests, docs, and release changesets.

Changes

Cohort / File(s) Summary
Changesets
\.changeset/forty-phones-rhyme.md, \.changeset/plain-ways-fall.md
Added patch release notes and a chore; included fix for graceful exit when no component selected.
Documentation & Plan
AGENTS.md, plans/registry-refactor-plan.md
Added developer guidance and a phased registry refactor plan.
CLI Registry Command
packages/cli/src/commands/registry/index.ts, packages/cli/src/commands/registry/implementation/registry.ts
Made component args optional; replaced legacy per-file install flow with registry index/item lookup and calls to core.installRegistryItemToXano; explicit handling for missing items and install-summary printing.
CLI Utilities
packages/cli/src/utils/feature-focused/registry/general.ts, packages/cli/src/utils/feature-focused/registry/registry-url-map.ts, packages/cli/src/utils/index.ts
Added interactive promptForComponents(core, registryUrl) using multiselect; adjusted type priority ordering; added resolveInstallUrl and handlers that reject non-installable registry types; updated public exports.
Core Registry API
packages/core/src/features/registry/api.ts, packages/core/src/features/registry/general.ts
New in-memory cached registry client (getRegistryIndex, getRegistryItem, fetchRegistryFileContent, clearRegistryCache) and sortFilesByType helper.
Core Install-to-Xano
packages/core/src/features/registry/install-to-xano.ts, packages/core/src/index.ts
New installRegistryItemToXano with URL resolution, content retrieval (inline or remote), Xano POST flow, per-file result aggregation; added Caly methods to expose registry APIs.
Types & Schema
packages/types/src/index.ts, schemas/registry/registry-item.json
Added RegistryItem and RegistryItemFile types; extended RegistryItemType with `registry:snippet
Tests
packages/core/src/features/registry/__tests__/api.spec.ts, .../general.spec.ts
Added unit tests for registry API, content fetching, and file sorting.
Config & Misc
packages/cli/jest.config.js, AGENTS.md, packages/cli/src/utils/index.ts
Jest config export now augments base config with testPathIgnorePatterns; updated utility exports and large docs additions.

Sequence Diagram

sequenceDiagram
    participant CLI as CLI Command
    participant Core as Core (Caly)
    participant Reg as Registry Server
    participant Xano as Xano API

    CLI->>Core: getRegistryIndex(registryUrl)
    Core->>Reg: GET /index.json
    Reg-->>Core: index.json
    Core-->>CLI: registry index (cached)

    CLI->>CLI: Prompt user for components (multiselect)
    CLI->>Core: getRegistryItem(componentName, registryUrl)
    Core->>Reg: GET /{component}.json
    Reg-->>Core: component JSON
    Core-->>CLI: registry item

    CLI->>Core: installRegistryItemToXano(item, context, registryUrl)
    Core->>Core: sortFilesByType(item.files)
    loop per file
        Core->>Core: fetch content (inline or GET registry file)
        Core->>Core: resolveInstallUrl(type, params)
        Core->>Core: loadToken()
        Core->>Xano: POST install content (Authorization)
        Xano-->>Core: 200/4xx/5xx
        Core->>Core: record installed/failed/skipped
    end
    Core-->>CLI: aggregated results
    CLI->>CLI: print install summary
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related PRs

Poem

🐰
I hopped to the core, where the registry gleams,
Caching the items and stitching the seams.
Files sorted, fetched, and posted with glee,
CLI asked, I picked, then sent them to be.
Huzzah — a rabbit cheers for refactor harmony!

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 53.85% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and specifically describes the main change: moving registry item installation functionality from the CLI package to the core package.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch dev

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
packages/cli/src/commands/registry/index.ts (1)

18-41: Guard against undefined components with optional variadic args.

Optional variadic command arguments in Commander.js can be undefined when omitted. The code uses .argument('[components...]', ...) which passes this potentially undefined value directly to addToXano. If addToXano expects an array, this causes runtime errors. Normalize to an empty array before passing:

Suggested fix
.action(
   withErrorHandler(async (components, options) => {
+     const componentNames = components ?? [];
      if (options.registry) {
         console.log('command registry option: ', options.registry);
         process.env.CALY_REGISTRY_URL = options.registry;
      }
      await addToXano({
-        componentNames: components,
+        componentNames,
         context: {
            instance: options.instance,
            workspace: options.workspace,
            branch: options.branch,
         },
         core,
      });
   })
);
packages/cli/src/utils/feature-focused/registry/general.ts (1)

79-96: Missing return statement in else branch.

When selectedGroup is not found, a new API group is created via metaApiPost but the result is not returned. This will cause the function to return undefined instead of the newly created group.

🐛 Proposed fix
    } else {
-      selectedGroup = await metaApiPost({
+      return await metaApiPost({
          baseUrl: instanceConfig.url,
          token: await core.loadToken(instanceConfig.name),
          path: `/workspace/${workspaceConfig.id}/apigroup`,
          body: {
             name: groupName,
             branch: branchConfig.label,
             swagger: false,
             docs: '',
          },
       });
    }
 }
🤖 Fix all issues with AI agents
In `@packages/cli/src/commands/registry/implementation/registry.ts`:
- Around line 55-64: When iterating registryItem.files before calling
getApiGroupByName, validate that file.apiGroupName is defined and non-empty; if
it's missing, throw or surface a clear error (e.g., include registryItem.id or
file.name for context) instead of calling getApiGroupByName with undefined.
Update the loop that inspects registryItem.files and the branch where file.type
=== 'registry:query' to guard on file.apiGroupName and set file.apiGroupId only
after a successful lookup via getApiGroupByName.

In `@packages/core/src/features/registry/install-to-xano.ts`:
- Around line 71-88: The loop over filesToInstall (computed via
sortFilesByType(item.files || [])) skips items that have no item.files but do
have item.content; change the logic that builds filesToInstall to handle
content-only registry items by falling back to a synthetic file entry: if
sortFilesByType returns an empty array and item.content exists, create a
single-file array e.g. [{ name: item.name || 'inline', content: item.content }]
so the subsequent for (const file of filesToInstall) block (and its fetch logic)
will process inline content; update references to filesToInstall and ensure the
synthetic file provides a path/name and content so existing code paths work
unchanged.

In `@packages/types/src/index.ts`:
- Around line 314-342: The RegistryItemFile interface currently requires path
and lacks the runtime apiGroupId field, preventing valid inline (content-only)
files; update the RegistryItemFile type so path is optional (path?: string) to
allow embedded files that only use content, and add apiGroupId?: string to
capture the installer/CLI-set field; keep type and content as-is and ensure any
downstream code that assumed path is present can handle undefined.
🧹 Nitpick comments (8)
packages/core/src/features/registry/general.ts (1)

1-27: Add explicit typings to avoid implicit-any in strict TS.

If noImplicitAny is enabled, files and typePriority indexing can fail compilation. Consider explicit typing to keep the API clear and robust.

♻️ Suggested typing tweak
-const typePriority = {
+type FileLike = { type?: string };
+
+const typePriority: Record<string, number> = {
    'registry:table': 0,
    'registry:addon': 1,
    'registry:function': 2,
    'registry:apigroup': 3,
    'registry:query': 4,
    'registry:middleware': 5,
    'registry:task': 6,
    'registry:tool': 7,
    'registry:mcp': 8,
    'registry:agent': 9,
    'registry:realtime': 10,
    'registry:workspace/trigger': 11,
    'registry:table/trigger': 12,
    'registry:mcp/trigger': 13,
    'registry:agent/trigger': 14,
    'registry:realtime/trigger': 15,
    'registry:test': 16,
 };
 
-function sortFilesByType(files) {
+function sortFilesByType<T extends FileLike>(files: readonly T[]): T[] {
    return files.slice().sort((a, b) => {
       const aPriority = typePriority[a.type] ?? 99;
       const bPriority = typePriority[b.type] ?? 99;
       return aPriority - bPriority;
    });
 }

Please verify against your TypeScript compiler settings.

packages/cli/src/utils/feature-focused/registry/registry-url-map.ts (1)

37-47: Inconsistent indentation style.

The new entries use 4-space indentation while existing entries use 3-space indentation. Consider aligning with the existing style for consistency.

♻️ Suggested fix
-    'registry:test': ({ workspaceConfig, branchConfig }) =>
-       `/workspace/${workspaceConfig.id}/workflow_test?branch=${branchConfig.label}`,
-    'registry:snippet': () => {
-       throw new Error('registry:snippet items are not directly installable');
-    },
-    'registry:file': () => {
-       throw new Error('registry:file items are not directly installable');
-    },
-    'registry:item': () => {
-       throw new Error('registry:item items are not directly installable');
-    },
+   'registry:test': ({ workspaceConfig, branchConfig }) =>
+      `/workspace/${workspaceConfig.id}/workflow_test?branch=${branchConfig.label}`,
+   'registry:snippet': () => {
+      throw new Error('registry:snippet items are not directly installable');
+   },
+   'registry:file': () => {
+      throw new Error('registry:file items are not directly installable');
+   },
+   'registry:item': () => {
+      throw new Error('registry:item items are not directly installable');
+   },
packages/core/src/features/registry/__tests__/api.spec.ts (1)

65-98: Consider adding error handling test for fetchRegistryFileContent.

The test suite covers the happy path (inline content, fetched content, slash normalization) but doesn't test the error case when fetch fails for file content. Consider adding a test similar to the getRegistryIndex error test for completeness.

💡 Suggested test case
it('should throw error on fetch failure when fetching file content', async () => {
    const item = {};
    (global.fetch as jest.Mock).mockResolvedValueOnce({
        ok: false,
        status: 500,
    });

    await expect(fetchRegistryFileContent(item, 'path/file.js', mockRegistryUrl))
        .rejects.toThrow();
});
packages/core/src/index.ts (2)

538-541: Consider efficiency of getRegistryItem implementation.

This method fetches the entire registry index to find a single item. For large registries, this could be inefficient. The imported getRegistryItem from ./features/registry/api (per the code snippet) appears to fetch items directly. Consider using it instead of re-fetching the index.

♻️ Suggested improvement
+import { getRegistryIndex, getRegistryItem as getRegistryItemFromApi } from './features/registry/api';
...
     async getRegistryItem(componentName: string, registryUrl: string) {
-       const index = await this.getRegistryIndex(registryUrl);
-       return index.find(item => item.name === componentName);
+       return getRegistryItemFromApi(componentName, registryUrl);
     }

543-550: Consider adding proper type definitions.

The item and resolvedContext parameters use any types, reducing type safety. Consider using the proper types from @repo/types (e.g., RegistryItem, InstanceConfig, WorkspaceConfig, BranchConfig).

♻️ Suggested type improvement
-    async installRegistryItemToXano(
-       item: any,
-       resolvedContext: { instanceConfig: any; workspaceConfig: any; branchConfig: any },
-       registryUrl: string,
-    ) {
+    async installRegistryItemToXano(
+       item: RegistryItem,
+       resolvedContext: { instanceConfig: InstanceConfig; workspaceConfig: WorkspaceConfig; branchConfig: BranchConfig },
+       registryUrl: string,
+    ) {
packages/cli/src/utils/feature-focused/registry/general.ts (1)

35-38: Remove debug console.log statements.

The console.log statements appear to be debug output that duplicates information already shown by the multiselect prompt. Consider removing them for cleaner UX.

♻️ Suggested fix
 async function promptForComponents(core, registryUrl) {
      try {
         const registry = await core.getRegistryIndex(registryUrl);
-        console.log('Available components:');
-        registry.items.forEach((item, index) => {
-           console.log(`${index + 1}. ${item.name} - ${item.description}`);
-        });

          const options = registry.items.map((item) => ({
packages/core/src/features/registry/api.ts (1)

6-27: Optional: fetch registry paths in parallel

If multiple paths are requested, you can reduce latency by resolving them concurrently while keeping the cache logic.

♻️ Suggested refactor
 async function fetchRegistry(paths, registryUrl) {
-   const results = [];
-   for (const path of paths) {
-      const cacheKey = `${registryUrl}/${path}`;
-      if (registryCache.has(cacheKey)) {
-         results.push(await registryCache.get(cacheKey));
-         continue;
-      }
-      const promise = fetch(`${registryUrl}/${path}`)
-         .then(async (res) => {
-            if (!res.ok) throw new Error(`Failed to fetch ${path}: ${res.status}`);
-            return res.json();
-         })
-         .catch((err) => {
-            registryCache.delete(cacheKey);
-            throw err;
-         });
-      registryCache.set(cacheKey, promise);
-      const resolvedPromise = await promise;
-      results.push(resolvedPromise);
-   }
-   return results;
+   const promises = paths.map((path) => {
+      const cacheKey = `${registryUrl}/${path}`;
+      if (registryCache.has(cacheKey)) return registryCache.get(cacheKey);
+      const promise = fetch(`${registryUrl}/${path}`)
+         .then(async (res) => {
+            if (!res.ok) throw new Error(`Failed to fetch ${path}: ${res.status}`);
+            return res.json();
+         })
+         .catch((err) => {
+            registryCache.delete(cacheKey);
+            throw err;
+         });
+      registryCache.set(cacheKey, promise);
+      return promise;
+   });
+   return Promise.all(promises);
 }
packages/core/src/features/registry/install-to-xano.ts (1)

102-120: Reuse token (and avoid URL double-slash)

Loading the token per file adds avoidable I/O. Also, installUrl already starts with /workspace, so the current join yields //workspace. Load token once and concatenate without the extra /.

♻️ Suggested refactor
-   for (const file of filesToInstall) {
+   const xanoToken = await core.loadToken(instanceConfig.name);
+   const xanoApiUrl = `${instanceConfig.url}/api:meta`;
+
+   for (const file of filesToInstall) {
       try {
          // Get content: use inline content if present, else fetch from file path
          let content;
          if (file.content) {
             content = file.content;
          } else {
             const normalized = file.path.replace(/^\/+/, '');
             const url = `${registryUrl}/${normalized}`;
             const res = await fetch(url);
             if (!res.ok)
                throw new Error(`Failed to fetch file content: ${file.path} (${res.status})`);
             content = await res.text();
          }
@@
-         const xanoToken = await core.loadToken(instanceConfig.name);
-         const xanoApiUrl = `${instanceConfig.url}/api:meta`;
          const installUrl = resolveInstallUrl(file.type, {
             instanceConfig,
             workspaceConfig,
             branchConfig,
             file,
             apiGroupId,
          });
 
-         const response = await fetch(`${xanoApiUrl}/${installUrl}`, {
+         const response = await fetch(`${xanoApiUrl}${installUrl}`, {
             method: 'POST',
             headers: {
                Authorization: `Bearer ${xanoToken}`,
                'Content-Type': 'text/x-xanoscript',
             },
             body: content,
          });

@MihalyToth20
Copy link
Collaborator Author

@coderabbitai review

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 19, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@MihalyToth20
Copy link
Collaborator Author

@coderabbitai full review

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 19, 2026

✅ Actions performed

Full review triggered.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

🤖 Fix all issues with AI agents
In `@packages/cli/src/commands/registry/implementation/registry.ts`:
- Around line 46-51: The current pushes to results.failed (e.g., where
registryItem is missing in the registry and other spots around lines 99-100)
produce entries that may lack the file field and assume error is an Error with
message; update all places that push into results.failed (look for uses of
results.failed.push in this file, including the branch referencing registryItem
and the other referenced block) to always include a file property (use
componentName or an empty string when unknown) and normalize the error to a
string (if the caught value is an Error use err.message, otherwise stringify
safely via String(err) or JSON.stringify fallback) so each failed entry matches
the declared return shape and downstream code can rely on file:string and
error:string.
- Around line 54-67: The code assumes getApiGroupByName returns a non-null group
and directly accesses apiGroup.id (in the registry item loop in registry.ts);
change this by either updating getApiGroupByName (in
packages/cli/src/utils/feature-focused/registry/general.ts) to always return the
created or found ApiGroup, or add a defensive check after the await: verify
apiGroup is defined and throw a clear error (or create the group) before
assigning file.apiGroupId; reference getApiGroupByName and the local variable
apiGroup and file.apiGroupId when making the fix.

In `@packages/core/src/features/registry/api.ts`:
- Around line 49-58: In fetchRegistryFileContent, don't treat empty-string
inline content as missing by using a truthy check on item.content; replace the
condition that currently checks "if (item.content)" with an explicit
null/undefined check (e.g., item.content !== null && item.content !== undefined
or item.content == null guard) so that empty strings are returned directly and
only null/undefined trigger the remote fetch; keep the rest of the logic
(normalizing filePath, building url, fetching, and error handling) unchanged.

In `@packages/core/src/features/registry/general.ts`:
- Around line 26-31: The comparator in sortFilesByType can receive undefined
priorities for unknown types (e.g., 'registry:unknown'), causing NaN and
unstable ordering; update sortFilesByType to coerce typePriority[a.type] and
typePriority[b.type] to numeric values by providing a default low-priority
number (e.g., a large integer constant) when the lookup is undefined, then use
those numeric priorities in the subtraction; reference the sortFilesByType
function and the typePriority lookup to locate where to apply the default value.

In `@packages/core/src/features/registry/install-to-xano.ts`:
- Around line 124-132: The pushed objects into results.installed and
results.failed do not match the InstallResults shape: include a component field
(e.g., component: file.component || file.path || '<inline>') when pushing to
results.installed and results.failed so each entry satisfies the InstallResults
type, keep response: body for success and error info for failures; also handle
the catch parameter as unknown and extract the message safely (use error
instanceof Error ? error.message : String(error)) before pushing to
results.failed so TypeScript strict mode compiles cleanly.
- Around line 77-91: The loop over filesToInstall can call
file.path.replace(...) when file.path is undefined; update the logic in the
install-to-xano.ts loop (inside the for (const file of filesToInstall) block
around the normalized variable) to first validate that file.path exists when
file.content is falsy and handle the missing path explicitly: either throw a
clear, descriptive error referencing the file (e.g., include file.name or index)
or skip the file, and only call file.path.replace(...) after confirming
file.path is a non-empty string; ensure the error/skip is logged or surfaced so
missing path issues are detectable at runtime.

In `@packages/types/src/index.ts`:
- Around line 317-323: The RegistryItemFile interface's apiGroupId is typed as
string but must match InstallParams.apiGroupId and ApiGroupConfig.id which are
string | number; update RegistryItemFile (the apiGroupId property) to use type
string | number so all three uses (RegistryItemFile.apiGroupId,
InstallParams.apiGroupId, ApiGroupConfig.id) are consistent.
🧹 Nitpick comments (3)
packages/cli/src/commands/registry/implementation/registry.ts (1)

32-40: Avoid double-fetching registry index

getRegistryIndex is called here and again inside promptForComponents, which can trigger an extra network call. Consider allowing promptForComponents to accept a pre-fetched index.

♻️ Suggested refactor (registry.ts + utils)
-   const registryIndex = await core.getRegistryIndex(registryUrl);
-
-   if (!componentNames?.length)
-      componentNames = (await promptForComponents(core, registryUrl)) as string[];
+   const registryIndex = await core.getRegistryIndex(registryUrl);
+
+   if (!componentNames?.length)
+      componentNames = (await promptForComponents(core, registryUrl, registryIndex)) as string[];
-async function promptForComponents(core, registryUrl) {
+async function promptForComponents(core, registryUrl, registryIndex?) {
      try {
-        const registry = await core.getRegistryIndex(registryUrl);
+        const registry = registryIndex ?? await core.getRegistryIndex(registryUrl);
         const options = registry.items.map((item) => ({
            value: item.name,
            label: `${item.name} - ${item.description}`,
         }));
packages/core/src/features/registry/install-to-xano.ts (2)

1-11: Consider using stricter types from @repo/types

The item parameter (line 58) and file property are typed as any. Import and use RegistryItem and RegistryItemFile for better type safety.

♻️ Proposed import changes
-import { BranchConfig, InstanceConfig, WorkspaceConfig } from '@repo/types';
+import { BranchConfig, InstanceConfig, WorkspaceConfig, RegistryItem, RegistryItemFile } from '@repo/types';
 import type { Caly } from '../..';
 import { sortFilesByType } from './general';

 interface InstallParams {
     instanceConfig: InstanceConfig | null;
     workspaceConfig: WorkspaceConfig;
     branchConfig: BranchConfig;
     apiGroupId?: string | number;
-    file?: any;
+    file?: RegistryItemFile;
 }

Then update line 58:

 async function installRegistryItemToXano(
-   item: any,
+   item: RegistryItem,
    resolvedContext: { instanceConfig: any; workspaceConfig: any; branchConfig: any },

64-64: Add explicit type annotation for results

The results object should be typed as InstallResults to ensure the structure remains consistent and catch mismatches at compile time.

♻️ Proposed fix
+import { BranchConfig, InstanceConfig, WorkspaceConfig, InstallResults } from '@repo/types';
 // ...
-    const results = { installed: [], failed: [], skipped: [] };
+    const results: InstallResults = { installed: [], failed: [], skipped: [] };

Comment on lines +46 to +51
if (!registryItem) {
results.failed.push({
component: componentName,
error: `Component '${componentName}' not found in registry`,
});
continue;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Keep failure records consistent with the declared return shape

failed entries can be pushed without a file, and error.message assumes Error. Consider normalizing both to avoid downstream surprises.

🛠️ Suggested fix
          if (!registryItem) {
             results.failed.push({
                component: componentName,
+               file: '<registry>',
                error: `Component '${componentName}' not found in registry`,
             });
             continue;
          }
...
       } catch (error) {
-         results.failed.push({ component: componentName, error: error.message });
+         const message = error instanceof Error ? error.message : String(error);
+         results.failed.push({ component: componentName, file: '<unknown>', error: message });
       }

Also applies to: 99-100

🤖 Prompt for AI Agents
In `@packages/cli/src/commands/registry/implementation/registry.ts` around lines
46 - 51, The current pushes to results.failed (e.g., where registryItem is
missing in the registry and other spots around lines 99-100) produce entries
that may lack the file field and assume error is an Error with message; update
all places that push into results.failed (look for uses of results.failed.push
in this file, including the branch referencing registryItem and the other
referenced block) to always include a file property (use componentName or an
empty string when unknown) and normalize the error to a string (if the caught
value is an Error use err.message, otherwise stringify safely via String(err) or
JSON.stringify fallback) so each failed entry matches the declared return shape
and downstream code can rely on file:string and error:string.

Comment on lines +54 to +67
// Resolve apiGroupIds for query files
if (registryItem.files) {
for (const file of registryItem.files) {
if (file.type === 'registry:query') {
if (!file.apiGroupName) {
throw new Error(`Missing apiGroupName for file ${file.path || 'unnamed'} in registry item ${registryItem.name || registryItem.id}`);
}
const apiGroup = await getApiGroupByName(
file.apiGroupName,
{ instanceConfig, workspaceConfig, branchConfig },
core,
);
file.apiGroupId = apiGroup.id;
}
Copy link
Contributor

@coderabbitai coderabbitai bot Jan 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Ensure apiGroup resolution can’t return undefined

If the API group doesn’t exist, getApiGroupByName currently doesn’t return the created group, so apiGroup.id can throw at runtime. Please make the helper return the created group (or add a defensive check here).

🛠️ Suggested fix (packages/cli/src/utils/feature-focused/registry/general.ts)
    if (selectedGroup) {
       return selectedGroup;
    } else {
       selectedGroup = await metaApiPost({
          baseUrl: instanceConfig.url,
          token: await core.loadToken(instanceConfig.name),
          path: `/workspace/${workspaceConfig.id}/apigroup`,
          body: {
             name: groupName,
             branch: branchConfig.label,
             swagger: false,
             docs: '',
          },
       });
    }
+   return selectedGroup;
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Resolve apiGroupIds for query files
if (registryItem.files) {
for (const file of registryItem.files) {
if (file.type === 'registry:query') {
if (!file.apiGroupName) {
throw new Error(`Missing apiGroupName for file ${file.path || 'unnamed'} in registry item ${registryItem.name || registryItem.id}`);
}
const apiGroup = await getApiGroupByName(
file.apiGroupName,
{ instanceConfig, workspaceConfig, branchConfig },
core,
);
file.apiGroupId = apiGroup.id;
}
if (selectedGroup) {
return selectedGroup;
} else {
selectedGroup = await metaApiPost({
baseUrl: instanceConfig.url,
token: await core.loadToken(instanceConfig.name),
path: `/workspace/${workspaceConfig.id}/apigroup`,
body: {
name: groupName,
branch: branchConfig.label,
swagger: false,
docs: '',
},
});
}
return selectedGroup;
}
🤖 Prompt for AI Agents
In `@packages/cli/src/commands/registry/implementation/registry.ts` around lines
54 - 67, The code assumes getApiGroupByName returns a non-null group and
directly accesses apiGroup.id (in the registry item loop in registry.ts); change
this by either updating getApiGroupByName (in
packages/cli/src/utils/feature-focused/registry/general.ts) to always return the
created or found ApiGroup, or add a defensive check after the await: verify
apiGroup is defined and throw a clear error (or create the group) before
assigning file.apiGroupId; reference getApiGroupByName and the local variable
apiGroup and file.apiGroupId when making the fix.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TODO: add deffensive check.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@MihalyToth20 Sounds good! Adding a defensive check will prevent the runtime error. You have two options:

  1. Update getApiGroupByName in packages/cli/src/utils/feature-focused/registry/general.ts to always return the created group (add return selectedGroup; at the end)
  2. Add a defensive check after calling getApiGroupByName in the registry.ts file to verify apiGroup is defined before accessing apiGroup.id

Feel free to ping me if you need help implementing either approach! 👍

Comment on lines +49 to +58
async function fetchRegistryFileContent(item, filePath, registryUrl) {
if (item.content) {
return item.content;
}
// Remove leading slash if present
const normalized = filePath.replace(/^\/+/, '');
const url = `${registryUrl}/${normalized}`;
const res = await fetch(url);
if (!res.ok) throw new Error(`Failed to fetch file content: ${filePath} (${res.status})`);
return await res.text();
Copy link
Contributor

@coderabbitai coderabbitai bot Jan 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Treat empty inline content as valid

Using a truthy check will skip empty-string content and unnecessarily fetch remote content. Consider a null/undefined check instead.

🛠️ Suggested fix
-   if (item.content) {
+   if (item.content != null) {
       return item.content;
    }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
async function fetchRegistryFileContent(item, filePath, registryUrl) {
if (item.content) {
return item.content;
}
// Remove leading slash if present
const normalized = filePath.replace(/^\/+/, '');
const url = `${registryUrl}/${normalized}`;
const res = await fetch(url);
if (!res.ok) throw new Error(`Failed to fetch file content: ${filePath} (${res.status})`);
return await res.text();
async function fetchRegistryFileContent(item, filePath, registryUrl) {
if (item.content != null) {
return item.content;
}
// Remove leading slash if present
const normalized = filePath.replace(/^\/+/, '');
const url = `${registryUrl}/${normalized}`;
const res = await fetch(url);
if (!res.ok) throw new Error(`Failed to fetch file content: ${filePath} (${res.status})`);
return await res.text();
}
🤖 Prompt for AI Agents
In `@packages/core/src/features/registry/api.ts` around lines 49 - 58, In
fetchRegistryFileContent, don't treat empty-string inline content as missing by
using a truthy check on item.content; replace the condition that currently
checks "if (item.content)" with an explicit null/undefined check (e.g.,
item.content !== null && item.content !== undefined or item.content == null
guard) so that empty strings are returned directly and only null/undefined
trigger the remote fetch; keep the rest of the logic (normalizing filePath,
building url, fetching, and error handling) unchanged.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TODO: ensure we only fetch remote content if it's absolutely needed.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@MihalyToth20, CodeRabbit's suggestion above directly addresses your TODO. The current truthy check if (item.content) treats empty strings as falsy, which means even when inline content is provided (just empty), the function will unnecessarily fetch from remote.

By changing to if (item.content != null), the function will:

  • Return empty strings directly (avoiding the fetch)
  • Only fetch remote when item.content is truly absent (null or undefined)

This ensures remote content is fetched only when absolutely needed—exactly what your TODO calls for. Implementing the suggested fix would resolve this concern and make the TODO unnecessary.

Comment on lines +77 to +91
for (const file of filesToInstall) {
try {
// Get content: use inline content if present, else fetch from file path
let content;
if (file.content) {
content = file.content;
} else {
const normalized = file.path.replace(/^\/+/, '');
const url = `${registryUrl}/${normalized}`;
const res = await fetch(url);
if (!res.ok)
throw new Error(`Failed to fetch file content: ${file.path} (${res.status})`);
content = await res.text();
}

Copy link
Contributor

@coderabbitai coderabbitai bot Jan 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Potential runtime error: file.path may be undefined

At line 84, file.path.replace() is called, but file.path is optional. When file.content is falsy but file.path is also undefined, this will throw.

🛠️ Proposed fix
       try {
          // Get content: use inline content if present, else fetch from file path
          let content;
          if (file.content) {
             content = file.content;
+         } else if (!file.path) {
+            throw new Error(`File entry has neither content nor path`);
          } else {
             const normalized = file.path.replace(/^\/+/, '');
             const url = `${registryUrl}/${normalized}`;
🤖 Prompt for AI Agents
In `@packages/core/src/features/registry/install-to-xano.ts` around lines 77 - 91,
The loop over filesToInstall can call file.path.replace(...) when file.path is
undefined; update the logic in the install-to-xano.ts loop (inside the for
(const file of filesToInstall) block around the normalized variable) to first
validate that file.path exists when file.content is falsy and handle the missing
path explicitly: either throw a clear, descriptive error referencing the file
(e.g., include file.name or index) or skip the file, and only call
file.path.replace(...) after confirming file.path is a non-empty string; ensure
the error/skip is logged or surfaced so missing path issues are detectable at
runtime.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TODO: ensure that at least one of the two is required. Either the file path or the inline content.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@MihalyToth20, thanks for noting this! You're right that the runtime validation is needed here.

While the schema in schemas/registry/registry-item.json already enforces the oneOf constraint (either path or content must be present), the runtime code in install-to-xano.ts should also validate this to provide a clear error message if invalid data somehow gets through.

The fix suggested in the review comment above is a good approach—checking !file.path when file.content is falsy and throwing a descriptive error. This would catch the issue early in the loop and provide better debugging information.

Would you like me to help implement this validation? I can create a code change that adds the runtime check with a clear error message.

Comment on lines +124 to +132
if (response.ok) {
const body = await response.json();
results.installed.push({ file: file.path || '<inline>', response: body });
} else {
results.failed.push({ file: file.path || '<inline>', error: `HTTP ${response.status}` });
}
} catch (error) {
results.failed.push({ file: file.path || '<inline>', error: error.message });
}
Copy link
Contributor

@coderabbitai coderabbitai bot Jan 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Results structure doesn't match InstallResults type

InstallResults (from @repo/types) expects objects with a component field, but the code only pushes { file, response } and { file, error }. This will cause type mismatches if consumers rely on InstallResults.

🛠️ Proposed fix
          if (response.ok) {
             const body = await response.json();
-            results.installed.push({ file: file.path || '<inline>', response: body });
+            results.installed.push({ component: item.name, file: file.path || '<inline>', response: body });
          } else {
-            results.failed.push({ file: file.path || '<inline>', error: `HTTP ${response.status}` });
+            results.failed.push({ component: item.name, file: file.path || '<inline>', error: `HTTP ${response.status}` });
          }
       } catch (error) {
-         results.failed.push({ file: file.path || '<inline>', error: error.message });
+         const message = error instanceof Error ? error.message : String(error);
+         results.failed.push({ component: item.name, file: file.path || '<inline>', error: message });
       }

Also note: error.message on line 131 will fail in TypeScript strict mode since error is unknown. Use error instanceof Error ? error.message : String(error).

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (response.ok) {
const body = await response.json();
results.installed.push({ file: file.path || '<inline>', response: body });
} else {
results.failed.push({ file: file.path || '<inline>', error: `HTTP ${response.status}` });
}
} catch (error) {
results.failed.push({ file: file.path || '<inline>', error: error.message });
}
if (response.ok) {
const body = await response.json();
results.installed.push({ component: item.name, file: file.path || '<inline>', response: body });
} else {
results.failed.push({ component: item.name, file: file.path || '<inline>', error: `HTTP ${response.status}` });
}
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
results.failed.push({ component: item.name, file: file.path || '<inline>', error: message });
}
🤖 Prompt for AI Agents
In `@packages/core/src/features/registry/install-to-xano.ts` around lines 124 -
132, The pushed objects into results.installed and results.failed do not match
the InstallResults shape: include a component field (e.g., component:
file.component || file.path || '<inline>') when pushing to results.installed and
results.failed so each entry satisfies the InstallResults type, keep response:
body for success and error info for failures; also handle the catch parameter as
unknown and extract the message safely (use error instanceof Error ?
error.message : String(error)) before pushing to results.failed so TypeScript
strict mode compiles cleanly.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TODO: ensure consistent and meaningful error messages

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@MihalyToth20, good call on noting this TODO. For consistency in this module, consider:

Error message structure:

  • Include the component/item name for context: ${item.name}
  • Specify the operation that failed: "installation", "fetch", "validation"
  • Add the affected file: ${file.path || '<inline>'}
  • Include actionable details where relevant (HTTP status, missing fields)

TypeScript safety:
In TypeScript strict mode with useUnknownInCatchVariables, caught errors are unknown, so check error instanceof Error before accessing error.message.

Example pattern for this code:

// Fetch error
throw new Error(`[${item.name}] Failed to fetch file '${file.path}': HTTP ${res.status}`);

// Validation error  
throw new Error(`[${item.name}] Missing apiGroupId for query file '${file.path || '<inline>'}'`);

// Safe catch handling
const message = error instanceof Error ? error.message : String(error);
results.failed.push({ 
  component: item.name,
  file: file.path || '<inline>', 
  error: `Installation failed: ${message}` 
});

Would you like me to create an issue to track this TODO for a follow-up PR?

Comment on lines +317 to +323
export interface RegistryItemFile {
path?: string;
content?: string;
type: RegistryItemType;
apiGroupName?: string;
apiGroupId?: string;
meta?: Record<string, any>;
Copy link
Contributor

@coderabbitai coderabbitai bot Jan 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Type inconsistency: apiGroupId should be string | number

The apiGroupId field is typed as string, but InstallParams.apiGroupId in install-to-xano.ts and ApiGroupConfig.id (line 163) use string | number. Align these for consistency.

🛠️ Proposed fix
 export interface RegistryItemFile {
     path?: string;
     content?: string;
     type: RegistryItemType;
     apiGroupName?: string;
-    apiGroupId?: string;
+    apiGroupId?: string | number;
     meta?: Record<string, any>;
 }
🤖 Prompt for AI Agents
In `@packages/types/src/index.ts` around lines 317 - 323, The RegistryItemFile
interface's apiGroupId is typed as string but must match
InstallParams.apiGroupId and ApiGroupConfig.id which are string | number; update
RegistryItemFile (the apiGroupId property) to use type string | number so all
three uses (RegistryItemFile.apiGroupId, InstallParams.apiGroupId,
ApiGroupConfig.id) are consistent.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TODO: fix types, apiGroupId should be passed as number to the install-to-xano methods, as the Xano metadata API expects integer numbers.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@MihalyToth20, thank you for clarifying! I've noted that apiGroupId should be typed as number specifically since the Xano metadata API expects integer values. This is an important constraint to ensure compatibility with the API.

Would you like me to create a GitHub issue to track this type refinement? The fix would involve:

  • Updating RegistryItemFile.apiGroupId to number (not string | number)
  • Ensuring InstallParams.apiGroupId in install-to-xano.ts is also number
  • Reviewing and updating ApiGroupConfig.id for consistency if needed

✏️ Learnings added
Learnt from: MihalyToth20
Repo: calycode/xano-tools PR: 184
File: packages/types/src/index.ts:489-495
Timestamp: 2026-02-07T16:24:34.001Z
Learning: In the codebase, `apiGroupId` should be typed as `number` (not `string` or `string | number`) because the Xano metadata API expects integer numbers for API group identifiers.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
schemas/registry/registry-item.json (1)

51-71: Include registry:file in files[].type enum.

RegistryItemType includes registry:file, but the files enum omits it, so valid items would fail validation.

🛠️ Suggested fix
                         "registry:test",
                         "registry:snippet",
+                        "registry:file",
                         "registry:item"
packages/cli/src/commands/registry/implementation/registry.ts (1)

99-101: Normalize error handling and add missing file field.

The catch block accesses error.message without a type guard and omits the file field. This was noted in a previous review.

Suggested fix
       } catch (error) {
-         results.failed.push({ component: componentName, error: error.message });
+         const message = error instanceof Error ? error.message : String(error);
+         results.failed.push({ component: componentName, file: '<unknown>', error: message });
       }
🤖 Fix all issues with AI agents
In `@AGENTS.md`:
- Around line 136-140: Update the "Test files" bullet in AGENTS.md to remove
"currently no test files exist" and reflect that test files have been added
(e.g., rename the extension example to `.spec.ts` or mention existing files like
`api.spec.ts` and `general.spec.ts`); modify the "Test files" line so it
accurately describes the current test files and extensions instead of stating
none exist.

In `@packages/cli/jest.config.js`:
- Around line 2-5: The current Jest config override replaces
config.testPathIgnorePatterns instead of merging it; update the export so
testPathIgnorePatterns merges the base array with the new pattern (use
config.testPathIgnorePatterns || [] and append
'src/commands/test/implementation/'), ensuring you reference the existing config
symbol and the testPathIgnorePatterns key so existing ignore patterns are
preserved.

In `@packages/cli/src/utils/feature-focused/registry/general.ts`:
- Around line 32-55: In promptForComponents, guard against missing or empty
registry.items returned by core.getRegistryIndex: after obtaining registry from
getRegistryIndex, check that registry && Array.isArray(registry.items) &&
registry.items.length > 0 and if not return an empty array (optionally log an
informational message) before calling registry.items.map or invoking
multiselect; this prevents mapping an undefined value and avoids prompting with
zero options and noisy error logs from the catch block.

In `@packages/core/src/features/registry/install-to-xano.ts`:
- Around line 31-35: The route builders for 'registry:table/trigger',
'registry:mcp/trigger', 'registry:agent/trigger' and 'registry:realtime/trigger'
currently use optional chaining (p.file?.tableId etc.) and can produce URLs with
"undefined"; update each arrow function to explicitly validate the required id
(tableId, mcpId, agentId, realtimeId) on p.file and throw a clear Error
mentioning which ID is missing (e.g. "Missing tableId for
registry:table/trigger") before building the URL; this ensures any call to those
mapping entries fails loudly rather than returning an invalid path.
♻️ Duplicate comments (6)
packages/core/src/features/registry/general.ts (1)

26-30: Guard against unknown registry types to avoid NaN ordering.

typePriority[a.type] can be undefined for unknown types, causing NaN in the comparator and unstable ordering.

🛠️ Suggested fix
-       const aPriority = typePriority[a.type];
-       const bPriority = typePriority[b.type];
+       const aPriority = typePriority[a.type] ?? 99;
+       const bPriority = typePriority[b.type] ?? 99;
        return aPriority - bPriority;
packages/cli/src/commands/registry/implementation/registry.ts (1)

46-51: Keep failure records consistent with declared return shape.

The failed entry pushed here is missing the file field that the return type declares (file: string). This was noted in a previous review.

Suggested fix
          results.failed.push({
             component: componentName,
+            file: '<registry>',
             error: `Component '${componentName}' not found in registry`,
          });
packages/types/src/index.ts (1)

317-323: Align RegistryItemFile.apiGroupId with other ID types (string | number).

This remains inconsistent with ApiGroupConfig.id and InstallParams.apiGroupId, which are string | number. Aligning prevents type friction.

🛠️ Proposed fix
 export interface RegistryItemFile {
     path?: string;
     content?: string;
     type: RegistryItemType;
     apiGroupName?: string;
-    apiGroupId?: string;
+    apiGroupId?: string | number;
     meta?: Record<string, any>;
 }
packages/core/src/features/registry/api.ts (1)

49-52: Treat empty inline content as valid.

Truthy check skips empty-string content and triggers an unnecessary fetch. Use a null/undefined check instead.

🛠️ Suggested fix
-   if (item.content) {
+   if (item.content != null) {
       return item.content;
    }
packages/core/src/features/registry/install-to-xano.ts (2)

79-85: Guard against missing file.path when content is absent.

file.path.replace(...) will throw if file.path is undefined. Validate before use and fail fast with a clear error.

🐛 Suggested fix
          let content;
          if (file.content) {
             content = file.content;
+         } else if (!file.path) {
+            throw new Error('File entry has neither content nor path');
          } else {
             const normalized = file.path.replace(/^\/+/, '');
             const url = `${registryUrl}/${normalized}`;

124-132: Align result entries with InstallResults and handle unknown errors safely.

InstallResults expects a component field, and error.message is unsafe for unknown. This will break typing and can throw at runtime.

🛠️ Suggested fix
          if (response.ok) {
             const body = await response.json();
-            results.installed.push({ file: file.path || '<inline>', response: body });
+            results.installed.push({ component: item.name, file: file.path || '<inline>', response: body });
          } else {
-            results.failed.push({ file: file.path || '<inline>', error: `HTTP ${response.status}` });
+            results.failed.push({ component: item.name, file: file.path || '<inline>', error: `HTTP ${response.status}` });
          }
       } catch (error) {
-         results.failed.push({ file: file.path || '<inline>', error: error.message });
+         const message = error instanceof Error ? error.message : String(error);
+         results.failed.push({ component: item.name, file: file.path || '<inline>', error: message });
       }
🧹 Nitpick comments (6)
packages/core/src/features/registry/__tests__/api.spec.ts (2)

38-63: Missing error handling test for getRegistryItem.

The getRegistryIndex tests include an error case (lines 28-35), but getRegistryItem lacks a similar test for fetch failures. Consider adding a test for consistency.

Suggested test case
+       it('should throw error on fetch failure', async () => {
+           (global.fetch as jest.Mock).mockResolvedValueOnce({
+               ok: false,
+               status: 404,
+           });
+
+           await expect(getRegistryItem('test-item', mockRegistryUrl)).rejects.toThrow();
+       });

9-12: Consider testing cache hit behavior.

clearRegistryCache is called in beforeEach, but there's no test verifying that repeated calls to getRegistryIndex or getRegistryItem use cached results (i.e., fetch is called only once). This would validate the caching optimization works as intended.

packages/cli/src/utils/feature-focused/registry/registry-url-map.ts (1)

3-48: Inconsistent leading slash in URL paths.

Some URL mappings omit the leading slash (lines 4-8: workspace/...), while others include it (lines 13-36: /workspace/...). This inconsistency could cause issues depending on how the base URL is joined. Standardize to one approach.

Example fix (remove leading slashes for consistency with first entries)
    'registry:addon': ({ workspaceConfig, branchConfig }) =>
-      `/workspace/${workspaceConfig.id}/addon?branch=${branchConfig.label}`,
+      `workspace/${workspaceConfig.id}/addon?branch=${branchConfig.label}`,

Apply similar changes to lines 15-36 and 37-38.

packages/cli/src/commands/registry/implementation/registry.ts (1)

92-98: Consider renaming error to reason for skipped entries.

Skipped entries typically represent intentional non-processing (e.g., already exists), not failures. Using error for the skip reason might be misleading. Consider reason or message for clarity.

Suggested change
          for (const skipped of installResults.skipped) {
             results.skipped.push({
                component: componentName,
                file: skipped.file,
-               error: skipped.error,
+               reason: skipped.reason ?? skipped.error,
             });
          }
packages/core/src/index.ts (1)

529-551: Add complete JSDoc documentation for new public API methods.

Per the coding guidelines (AGENTS.md line 123), JSDoc comments are required for public APIs. The new registry methods have minimal or missing documentation. Add parameter descriptions, return types, and example usage.

Example JSDoc for installRegistryItemToXano
+   /**
+    * Installs a registry item to a Xano instance.
+    *
+    * `@param` item - The registry item to install
+    * `@param` resolvedContext - Resolved configuration context
+    * `@param` resolvedContext.instanceConfig - Instance configuration
+    * `@param` resolvedContext.workspaceConfig - Workspace configuration
+    * `@param` resolvedContext.branchConfig - Branch configuration
+    * `@param` registryUrl - Base URL of the registry
+    * `@returns` Promise resolving to installation results with installed, failed, and skipped arrays
+    *
+    * `@example`
+    * ```typescript
+    * const results = await calyInstance.installRegistryItemToXano(
+    *   registryItem,
+    *   { instanceConfig, workspaceConfig, branchConfig },
+    *   'https://registry.example.com'
+    * );
+    * ```
+    */
    async installRegistryItemToXano(
packages/cli/src/utils/feature-focused/registry/general.ts (1)

4-21: Add priorities for the new registry item types to keep ordering deterministic.

The type map doesn’t include the newly added registry:snippet, registry:file, and registry:item, so they fall back to 99 and may interleave unexpectedly with other types. Consider extending the map.

♻️ Suggested update
 const typePriority = {
    'registry:table': 1,
    'registry:addon': 2,
    'registry:function': 3,
    'registry:apigroup': 4,
    'registry:query': 5,
    'registry:middleware': 6,
    'registry:task': 7,
    'registry:tool': 8,
    'registry:mcp': 9,
    'registry:agent': 10,
    'registry:realtime': 11,
    'registry:workspace/trigger': 12,
    'registry:table/trigger': 13,
    'registry:mcp/trigger': 14,
    'registry:agent/trigger': 15,
    'registry:realtime/trigger': 16,
    'registry:test': 17,
+   'registry:snippet': 18,
+   'registry:file': 19,
+   'registry:item': 20,
 };

Comment on lines +136 to +140
- **Test framework**: Jest with ts-jest transformer
- **Test files**: `.test.ts` extension (currently no test files exist)
- **Test configuration**: JSON-based config files for API testing
- **Test environment**: Node.js environment
- **Coverage**: Use `jest-html-reporter` for HTML coverage reports
Copy link
Contributor

@coderabbitai coderabbitai bot Jan 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Update outdated statement about test files.

Line 137 states "currently no test files exist", but this PR adds test files (e.g., api.spec.ts, general.spec.ts). Update this statement to reflect the current state.

Suggested fix
-- **Test files**: `.test.ts` extension (currently no test files exist)
+- **Test files**: `.test.ts` or `.spec.ts` extension
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
- **Test framework**: Jest with ts-jest transformer
- **Test files**: `.test.ts` extension (currently no test files exist)
- **Test configuration**: JSON-based config files for API testing
- **Test environment**: Node.js environment
- **Coverage**: Use `jest-html-reporter` for HTML coverage reports
- **Test framework**: Jest with ts-jest transformer
- **Test files**: `.test.ts` or `.spec.ts` extension
- **Test configuration**: JSON-based config files for API testing
- **Test environment**: Node.js environment
- **Coverage**: Use `jest-html-reporter` for HTML coverage reports
🤖 Prompt for AI Agents
In `@AGENTS.md` around lines 136 - 140, Update the "Test files" bullet in
AGENTS.md to remove "currently no test files exist" and reflect that test files
have been added (e.g., rename the extension example to `.spec.ts` or mention
existing files like `api.spec.ts` and `general.spec.ts`); modify the "Test
files" line so it accurately describes the current test files and extensions
instead of stating none exist.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TODO: update test-related comments / documentation

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@MihalyToth20, sounds good! Since you're planning to update the test-related documentation later, would you like me to create an issue to track this TODO? It would help ensure the documentation update doesn't get forgotten.


🧠 Learnings used
Learnt from: CR
Repo: calycode/xano-tools PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-28T05:59:33.267Z
Learning: Applies to **/*.test.ts?(x) : Use Jest for testing across all packages

Comment on lines +2 to +5
export default {
...config,
testPathIgnorePatterns: ['src/commands/test/implementation/'],
};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Preserve existing Jest ignore patterns instead of overwriting.

Overriding testPathIgnorePatterns can drop base config ignores and unexpectedly run tests. Merge with the base config list.

🛠️ Suggested fix
 export default {
     ...config,
-    testPathIgnorePatterns: ['src/commands/test/implementation/'],
+    testPathIgnorePatterns: [
+        ...(config.testPathIgnorePatterns ?? []),
+        'src/commands/test/implementation/',
+    ],
 };
🤖 Prompt for AI Agents
In `@packages/cli/jest.config.js` around lines 2 - 5, The current Jest config
override replaces config.testPathIgnorePatterns instead of merging it; update
the export so testPathIgnorePatterns merges the base array with the new pattern
(use config.testPathIgnorePatterns || [] and append
'src/commands/test/implementation/'), ensuring you reference the existing config
symbol and the testPathIgnorePatterns key so existing ignore patterns are
preserved.

Comment on lines +32 to +55
async function promptForComponents(core, registryUrl) {
try {
const registry = await core.getRegistryIndex(registryUrl);

const options = registry.items.map((item) => ({
value: item.name,
label: `${item.name} - ${item.description}`,
}));

const selected = await multiselect({
message: 'Select components to add:',
options,
required: true,
});

if (isCancel(selected)) {
return [];
}

return selected;
} catch (error) {
console.error('Failed to fetch available components:', error);
return [];
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Guard against missing/empty registry items before prompting.

If registry.items is missing or empty, the current code either throws (then gets caught) or prompts with no options. Handle this explicitly to avoid confusing UX and noisy error logs.

🔧 Suggested fix
 async function promptForComponents(core, registryUrl) {
      try {
         const registry = await core.getRegistryIndex(registryUrl);
 
-         const options = registry.items.map((item) => ({
+         const items = registry?.items ?? [];
+         if (!items.length) {
+            console.error('No components available in registry index.');
+            return [];
+         }
+
+         const options = items.map((item) => ({
             value: item.name,
-            label: `${item.name} - ${item.description}`,
+            label: item.description ? `${item.name} - ${item.description}` : item.name,
          }));
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
async function promptForComponents(core, registryUrl) {
try {
const registry = await core.getRegistryIndex(registryUrl);
const options = registry.items.map((item) => ({
value: item.name,
label: `${item.name} - ${item.description}`,
}));
const selected = await multiselect({
message: 'Select components to add:',
options,
required: true,
});
if (isCancel(selected)) {
return [];
}
return selected;
} catch (error) {
console.error('Failed to fetch available components:', error);
return [];
}
async function promptForComponents(core, registryUrl) {
try {
const registry = await core.getRegistryIndex(registryUrl);
const items = registry?.items ?? [];
if (!items.length) {
console.error('No components available in registry index.');
return [];
}
const options = items.map((item) => ({
value: item.name,
label: item.description ? `${item.name} - ${item.description}` : item.name,
}));
const selected = await multiselect({
message: 'Select components to add:',
options,
required: true,
});
if (isCancel(selected)) {
return [];
}
return selected;
} catch (error) {
console.error('Failed to fetch available components:', error);
return [];
}
}
🤖 Prompt for AI Agents
In `@packages/cli/src/utils/feature-focused/registry/general.ts` around lines 32 -
55, In promptForComponents, guard against missing or empty registry.items
returned by core.getRegistryIndex: after obtaining registry from
getRegistryIndex, check that registry && Array.isArray(registry.items) &&
registry.items.length > 0 and if not return an empty array (optionally log an
informational message) before calling registry.items.map or invoking
multiselect; this prevents mapping an undefined value and avoids prompting with
zero options and noisy error logs from the catch block.

Comment on lines +31 to +35
'registry:query': (p) => `apigroup/${p.apiGroupId}/query`,
'registry:table/trigger': (p) => `table/${p.file?.tableId}/trigger`,
'registry:mcp/trigger': (p) => `mcp/${p.file?.mcpId}/trigger`,
'registry:agent/trigger': (p) => `agent/${p.file?.agentId}/trigger`,
'registry:realtime/trigger': (p) => `realtime/${p.file?.realtimeId}/trigger`,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Validate required IDs for trigger install URLs.

tableId/mcpId/agentId/realtimeId are optional on file, so the URL can become .../undefined/trigger and fail silently. Throw a clear error when required IDs are missing.

🛠️ Suggested fix
-   'registry:table/trigger': (p) => `table/${p.file?.tableId}/trigger`,
-   'registry:mcp/trigger': (p) => `mcp/${p.file?.mcpId}/trigger`,
-   'registry:agent/trigger': (p) => `agent/${p.file?.agentId}/trigger`,
-   'registry:realtime/trigger': (p) => `realtime/${p.file?.realtimeId}/trigger`,
+   'registry:table/trigger': (p) => {
+      if (!p.file?.tableId) throw new Error('tableId required for table trigger installation');
+      return `table/${p.file.tableId}/trigger`;
+   },
+   'registry:mcp/trigger': (p) => {
+      if (!p.file?.mcpId) throw new Error('mcpId required for MCP trigger installation');
+      return `mcp/${p.file.mcpId}/trigger`;
+   },
+   'registry:agent/trigger': (p) => {
+      if (!p.file?.agentId) throw new Error('agentId required for agent trigger installation');
+      return `agent/${p.file.agentId}/trigger`;
+   },
+   'registry:realtime/trigger': (p) => {
+      if (!p.file?.realtimeId) throw new Error('realtimeId required for realtime trigger installation');
+      return `realtime/${p.file.realtimeId}/trigger`;
+   },
🤖 Prompt for AI Agents
In `@packages/core/src/features/registry/install-to-xano.ts` around lines 31 - 35,
The route builders for 'registry:table/trigger', 'registry:mcp/trigger',
'registry:agent/trigger' and 'registry:realtime/trigger' currently use optional
chaining (p.file?.tableId etc.) and can produce URLs with "undefined"; update
each arrow function to explicitly validate the required id (tableId, mcpId,
agentId, realtimeId) on p.file and throw a clear Error mentioning which ID is
missing (e.g. "Missing tableId for registry:table/trigger") before building the
URL; this ensures any call to those mapping entries fails loudly rather than
returning an invalid path.

@MihalyToth20
Copy link
Collaborator Author

@copilot Review this pull request, collect all identified issues, and rank them according to priority. When ready write them into a new markdown file into the repo /plans directory and commit it.

Copy link
Contributor

Copilot AI commented Feb 7, 2026

@MihalyToth20 I've opened a new pull request, #185, to work on those changes. Once the pull request is ready, I'll request review from you.

@MihalyToth20
Copy link
Collaborator Author

@coderabbitai full review

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 7, 2026

✅ Actions performed

Full review triggered.

@MihalyToth20 MihalyToth20 requested a review from Copilot February 7, 2026 17:55
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 145 out of 160 changed files in this pull request and generated 7 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +15 to +36
const REGISTRY_MAP: Record<string, string | UrlResolver> = {
// Simple static-like paths
'registry:function': (p) => `function`,
'registry:table': (p) => `table`,
'registry:addon': (p) => `addon`,
'registry:apigroup': (p) => `apigroup`,
'registry:middleware': (p) => `middleware`,
'registry:task': (p) => `task`,
'registry:tool': (p) => `tool`,
'registry:mcp': (p) => `mcp`,
'registry:agent': (p) => `agent`,
'registry:realtime': (p) => `realtime`,
'registry:test': (p) => `test`,
'registry:workspace/trigger': (p) => `trigger`,

// Complex/Nested paths
'registry:query': (p) => `apigroup/${p.apiGroupId}/query`,
'registry:table/trigger': (p) => `table/${p.file?.tableId}/trigger`,
'registry:mcp/trigger': (p) => `mcp/${p.file?.mcpId}/trigger`,
'registry:agent/trigger': (p) => `agent/${p.file?.agentId}/trigger`,
'registry:realtime/trigger': (p) => `realtime/${p.file?.realtimeId}/trigger`,
};
Copy link

Copilot AI Feb 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The meta API path segments in REGISTRY_MAP don’t match the CLI’s existing registry-url-map for at least registry:test and registry:realtime* (CLI uses workflow_test and realtime/channel...). This will cause core-based installations to POST to the wrong endpoints for those types. Align these mappings with the known-good CLI URL map (ideally by moving the URL map into core and sharing it) so install-to-xano uses the same endpoints everywhere.

Copilot uses AI. Check for mistakes.
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • Consider the CLI url maps the correct one and implement thise into the Core. If needed move the path resolver into the @repo/utils where the rest of the reusable methods are.

Comment on lines +88 to +99
// Get content: use inline content if present, else fetch from file path
let content;
if (file.content) {
content = file.content;
} else {
const normalized = file.path.replace(/^\/+/, '');
const url = `${registryUrl}/${normalized}`;
const res = await fetch(url);
if (!res.ok)
throw new Error(`Failed to fetch file content: ${file.path} (${res.status})`);
content = await res.text();
}
Copy link

Copilot AI Feb 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This fetch path normalization only strips leading slashes and does not prevent path traversal (e.g. ../...) or encoded traversal; additionally file.path is optional in the new RegistryItemFile type and will throw if absent. Reuse the core validateRegistryPath() / fetchRegistryFileContent() utilities from features/registry/api.ts (or equivalent) and handle the path-missing case explicitly to avoid traversal and runtime crashes.

Copilot uses AI. Check for mistakes.
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • ensure the flow doesn't throw fake errors
  • For path traversal check if we can use a library or more reusable pattern.

Comment on lines +61 to +69
if (!file.apiGroupName) {
throw new Error(`Missing apiGroupName for file ${file.path || 'unnamed'} in registry item ${registryItem.name || registryItem.id}`);
}
const apiGroup = await getApiGroupByName(
file.apiGroupName,
{ instanceConfig, workspaceConfig, branchConfig },
core,
);
file.apiGroupId = apiGroup.id;
Copy link

Copilot AI Feb 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

registryItem.id is referenced but RegistryItem (as defined in @repo/types) doesn’t include an id field, so this will fail type-checking (and may be undefined at runtime). Use only registryItem.name (and/or componentName) in the error message, or extend the RegistryItem type if an id is genuinely present in registry payloads.

Copilot uses AI. Check for mistakes.
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • Remove the registry id reference in the errors, comply to the existing type

Comment on lines 132 to 156
@@ -140,6 +149,8 @@ async function loadTestConfig(testConfigPath) {
const content = await readFile(testConfigPath, 'utf8');
return JSON.parse(content);
} else if (ext === '.js') {
// SECURITY: require() executes the JS file. This is intentional for dynamic configs.
// Users must only load trusted config files.
const config = require(path.resolve(testConfigPath));
return config.default || config;
} else {
Copy link

Copilot AI Feb 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The JSDoc states .ts configs are supported, but the implementation only handles .json and .js and will throw for .ts. Either add actual .ts support (e.g., via tsx/ts-node loader or dynamic import in ESM) or update the docstring and error text to match the real supported extensions.

Copilot uses AI. Check for mistakes.
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • Remove the reference to .ts
  • Create a specification or plan to add .ts test running support (low priority)

import { testRunner } from '../features/testing';
import type { Caly } from '..';
import { ApiGroupConfig, CoreContext, PrepareRequestArgs, AssertDefinition } from '@repo/types';
import { ApiGroupConfig, CoreContext, AssertDefinition, TestConfigEntry, TestResult, TestGroupResult } from '@repo/types';
Copy link

Copilot AI Feb 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AssertDefinition and TestResult are imported but not used anywhere in this file (the signature now uses TestConfigEntry and returns TestGroupResult[]). Removing unused imports will avoid lint/typecheck noise and keep this module minimal.

Suggested change
import { ApiGroupConfig, CoreContext, AssertDefinition, TestConfigEntry, TestResult, TestGroupResult } from '@repo/types';
import { ApiGroupConfig, CoreContext, TestConfigEntry, TestGroupResult } from '@repo/types';

Copilot uses AI. Check for mistakes.
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • Remove unused references

Comment on lines +10 to +23
function validateRegistryPath(inputPath: string): string {
// Remove leading slashes
let normalized = inputPath.replace(/^\/+/, '');

// Block path traversal attempts
// Check for ".." in path components (handles both / and \ separators)
if (/(^|\/)\.\.($|\/|\\)|(^|\\)\.\.($|\/|\\)/.test(normalized)) {
throw new Error(`Invalid registry path: "${inputPath}" contains path traversal`);
}

// Also block encoded traversal attempts
if (normalized.includes('%2e%2e') || normalized.includes('%2E%2E')) {
throw new Error(`Invalid registry path: "${inputPath}" contains encoded path traversal`);
}
Copy link

Copilot AI Feb 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new path traversal protections aren’t covered by unit tests (current tests cover leading slash normalization but not traversal rejection). Add test cases in packages/core/src/features/registry/__tests__/api.spec.ts for inputs like "../secrets", "..\\secrets", and "%2e%2e/evil" to ensure these validations are enforced.

Copilot uses AI. Check for mistakes.
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • After refactoring the path traversal checks rewrite the tests to cover for the new implementation

Comment on lines +529 to +551
// ----- REGISTRY METHODS ----- //
/**
* Get the main registry index.
*/
async getRegistryIndex(registryUrl: string) {
return getRegistryIndex(registryUrl);
}

/**
* Get a specific registry item by name.
*/
async getRegistryItem(componentName: string, registryUrl: string) {
return getRegistryItem(componentName, registryUrl);
}

async installRegistryItemToXano(
item: RegistryItem,
resolvedContext: { instanceConfig: InstanceConfig; workspaceConfig: WorkspaceConfig; branchConfig: BranchConfig },
registryUrl: string,
) {
const results = await installRegistryItemToXano(item, resolvedContext, registryUrl, this);
return results;
}
Copy link

Copilot AI Feb 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new registry methods have inconsistent indentation relative to the rest of the class, which makes the public API additions harder to scan and maintain. It’d also help readability to avoid the imported-function vs method name collision for installRegistryItemToXano (e.g., alias the import), and optionally add explicit return types for these new public methods.

Copilot uses AI. Check for mistakes.
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • Prettify the file, fix indentation

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 10

Note

Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (6)
packages/cli/src/utils/feature-focused/registry/general.ts (1)

59-92: ⚠️ Potential issue | 🟠 Major

Missing return statement for newly created API group.

The getApiGroupByName function creates a new group via metaApiPost when the group doesn't exist, but doesn't return the result. The caller will receive undefined instead of the newly created group.

🐛 Proposed fix
    if (selectedGroup) {
       return selectedGroup;
    } else {
       selectedGroup = await metaApiPost({
          baseUrl: instanceConfig.url,
          token: await core.loadToken(instanceConfig.name),
          path: `/workspace/${workspaceConfig.id}/apigroup`,
          body: {
             name: groupName,
             branch: branchConfig.label,
             swagger: false,
             docs: '',
          },
       });
+      return selectedGroup;
    }
 }
docs/commands/generate.md (1)

3-3: ⚠️ Potential issue | 🟡 Minor

Fix typos in the description.
“Transformative” and “your Xano” are misspelled.

✏️ Suggested edit
-> #### Transforamtive operations that allow you to view you Xano through a fresh set of eyes.
+> #### Transformative operations that allow you to view your Xano through a fresh set of eyes.

Also applies to: 12-12

packages/cli/src/utils/index.ts (1)

2-3: ⚠️ Potential issue | 🟡 Minor

Duplicate export statement.

Lines 2 and 3 both export from './commands/option-sets'. This is likely unintentional and should be removed to avoid confusion.

Suggested fix
 export * from './commands/context-resolution';
 export * from './commands/option-sets';
-export * from './commands/option-sets';
 export * from './commands/project-root-finder';
packages/cli/README.md (1)

130-130: ⚠️ Potential issue | 🟡 Minor

Outdated command reference in GitHub Actions example.

Line 130 still references the old command name generate-oas --all, but the rest of the README uses the new command structure (generate spec --all). This should be updated for consistency.

Suggested fix
              run: |
-                 generate-oas --all
+                 generate spec --all
packages/cli/src/utils/feature-focused/registry/registry-url-map.ts (1)

4-14: ⚠️ Potential issue | 🔴 Critical

Fix inconsistent leading slashes in URL paths—currently causes malformed URLs for first four registry types.

URLs are concatenated directly with ${xanoApiUrl}${installUrl} at install-to-xano.ts line 124. Since xanoApiUrl ends with /api:meta (no trailing slash), URLs without leading slashes produce malformed paths like /api:metaworkspace/... instead of /api:meta/workspace/....

Lines 4–12 (registry:table, registry:function, registry:apigroup, registry:query) are missing the leading /. All other registry types (lines 13–38) correctly include it. Add the leading / to the first four entries to ensure consistent URL formation.

packages/core/src/features/testing/index.ts (1)

227-238: ⚠️ Potential issue | 🟡 Minor

Add type guard for error in catch block.

Accessing error.stack and error.message assumes the caught value is an Error instance. If a non-Error is thrown, this could produce unexpected results.

🛠️ Proposed fix
          } catch (error) {
             const testEnd = Date.now();
             const testDuration = testEnd - testStart;
+            const errorMessage = error instanceof Error 
+               ? (error.stack || error.message) 
+               : String(error);
             results.push({
                path,
                method,
                success: false,
-               errors: error.stack || error.message,
+               errors: errorMessage,
                warnings: null,
                duration: testDuration,
             });
          }
🤖 Fix all issues with AI agents
In @.changeset/loose-pandas-hide.md:
- Line 7: Update the changeset description to reflect the actual PR intent:
replace the generic "chore: updates to testing, to have robust env support, and
better documentation" with a summary that states the refactor moves registry
item installation from the CLI into the core, introduces new registry APIs (core
registry installation and registry-related functions) and updates the CLI to
delegate installation to core; mention any breaking behavior or migration notes
for users of the CLI/registry APIs and keep the tone as a concise changelog
entry matching the PR title "refactor: move registry item installation to the
core".

In `@packages/browser-consumer/esbuild.config.ts`:
- Around line 11-32: The build uses relative paths for entryPoints and outdir
causing CWD-dependent output; update both build invocations in main() to use
absolute paths via resolve(__dirname, ...): set entryPoints to
[resolve(__dirname, 'src/index.ts')] and outdir to resolve(__dirname, 'dist')
(or to the existing distDir if that's already resolve(__dirname, 'dist')), for
both the esmResult and cjsResult builds so the metafile writes (writeFile using
distDir) align with the actual build outputs.

In `@packages/cli/scripts/installer/install.bat`:
- Around line 1-48: The batch installer (install.bat) currently has Unix (LF)
line endings which will break Windows batch parsing; convert this file to
Windows CRLF line endings before merging (ensure the checked-in file uses CRLF),
and add a repo-level rule so future changes stay CRLF (e.g., enable
core.autocrlf or add a .gitattributes entry forcing *.bat eol=crlf); verify the
committed install.bat (the wrapper that checks powershell, uses %~dp0 as
SCRIPT_DIR and conditionally runs install.ps1) contains CRLF line endings and
re-run the static check to confirm the issue is resolved.

In `@packages/cli/src/commands/opencode/implementation.ts`:
- Around line 499-571: The code currently calls killProcessOnPort from
restartServer and the stop branch in handleMessage which can kill unrelated
services; change the logic to only terminate processes we started by tracking
the child PID/handle returned by startServer (use the existing serverProc
variable and a persisted PID from startServer) and avoid calling
killProcessOnPort unless the PID matches our tracked PID; if no tracked PID
exists for the requested port, return/send an error asking the user to pick a
different port instead of force-killing. Update restartServer, the 'stop' branch
in handleMessage, and any startServer result handling to set/clear the tracked
PID/serverProc consistently and only kill that PID when performing stop/restart.
Also ensure restartServer waits for the tracked process to exit before starting
a new one and includes user-facing error messages when refusing to kill unknown
owners.

In `@packages/cli/src/commands/serve/implementation.ts`:
- Around line 5-20: The validatePathArg function currently rejects paths
containing ':' which breaks Windows drive-letter paths like "C:\..."; update
validatePathArg to allow a single colon immediately after a leading drive letter
(e.g., /^[A-Za-z]:/) or strip a leading drive prefix before running the existing
metacharacter regex. Target the validatePathArg function and ensure the logic
only permits that single leading drive colon (not arbitrary colons elsewhere)
before applying the existing unsafe-character test and throwing the same Error
on failure.

In `@packages/cli/src/utils/github-content-fetcher.ts`:
- Around line 131-133: Normalize and sanitize the incoming subpath before
building cacheSubDir: replace backslashes with forward slashes (or use
path.normalize), remove or collapse any '..' segments, then use
path.resolve/path.join to construct cacheSubDir from cacheDir (or
this.defaultCacheDir) and verify with path.relative or startsWith that the
resolved cacheSubDir is inside the base cacheDir; if the check fails, reject the
input or throw an error. Ensure you update the code around cacheDir, cacheSubDir
and subpath in github-content-fetcher.ts to perform these normalization and
validation steps so Windows backslashes or path-traversal attempts cannot escape
the base cache directory.

In `@packages/core/src/features/registry/api.ts`:
- Around line 20-22: The encoded-traversal check in the registry API currently
checks normalized for '%2e%2e' and '%2E%2E' and can miss mixed-case encodings;
update the logic around the normalized variable (used alongside inputPath) to
convert it to a single case (e.g., normalized = normalized.toLowerCase()) before
performing the includes() check for '%2e%2e', or replace the check with a
case-insensitive regex; ensure the thrown Error still references inputPath for
context.

In `@packages/opencode-templates/agents/xano-ai-builder.md`:
- Line 4: Update the provider parameter names and constraints in
xano-ai-builder.md: remove or restrict reasoning_effort where used with gpt-4o
(only allow it for reasoning models like o1 or delete the param for gpt-4o
references), replace any thinking_tokens occurrences with thinkingBudget for
Gemini 2.5 or thinkingLevel for Gemini 3+ and adjust documented ranges to the
provider spec, remove search_grounding and instead document adding google_search
to the tools list to enable grounding, and keep include_thoughts as-is; ensure
model identifiers (e.g., anthropic/claude-sonnet-4-20250514) remain unchanged.

In `@packages/opencode-templates/agents/xano-api-writer.md`:
- Around line 110-116: The db.query calls use quoted table names which
XanoScript disallows; locate the db.query "product" occurrences (the blocks that
assign to $products) and remove the quotes so they read db.query product,
ensuring the where, sort, and return clauses remain unchanged; update both
occurrences mentioned (the blocks producing $products around the given diff
ranges) to use the bare identifier product.

In `@packages/xano-skills/skills/xano-monitoring/SKILL.md`:
- Around line 98-113: Replace invalid XanoScript usages across the file: change
any now() calls to the "now" timestamp literal; update db.add usage such as
db.add performance_log { ... } to include the required wrapper db.add
performance_log { data = { endpoint: $request.path, method = $request.method,
response_time_ms = $duration_ms, query_count = $query_count } } as $result;
convert try/catch blocks to the Xano try_catch form try_catch { try { ... }
catch { /* catch receives $error with message, code, stack */ } }; replace
db.raw calls with db.direct_query { sql = "...", arg = [...] } and replace
http.post usages with the External API Request equivalent from Xano’s visual
builder; and change any log(...) calls to debug.log { value = ... } so all
examples use correct Xano function names and argument shapes.
🟡 Minor comments (37)
packages/cli/LICENSES/opencode-ai.txt-1-1 (1)

1-1: ⚠️ Potential issue | 🟡 Minor

Consider hyphenating the compound adjective.

“open-source” is the conventional compound adjective form in “open-source AI coding agent.”

✏️ Proposed tweak
-OpenCode - The open source AI coding agent
+OpenCode - The open-source AI coding agent
packages/opencode-templates/agents/xano-task-writer.md-31-32 (1)

31-32: ⚠️ Potential issue | 🟡 Minor

Use future/placeholder dates in examples to avoid “past date” confusion.

As of Feb 7, 2026, several example starts_on values are already in the past, which could be misread as required values rather than placeholders. Consider replacing with a generic placeholder or a clearly future date.

Proposed doc tweak
-  schedule = [{starts_on: 2026-01-01 09:00:00+0000, freq: 86400}]
+  schedule = [{starts_on: YYYY-MM-DD 09:00:00+0000, freq: 86400}]

-  starts_on: 2026-01-01 09:00:00+0000,
+  starts_on: YYYY-MM-DD 09:00:00+0000,

-  schedule = [{starts_on: 2026-01-01 00:00:00+0000, freq: 21600}]
+  schedule = [{starts_on: YYYY-MM-DD 00:00:00+0000, freq: 21600}]

-  schedule = [{starts_on: 2026-05-01 23:00:00+0000, freq: 86400}]
+  schedule = [{starts_on: YYYY-MM-DD 23:00:00+0000, freq: 86400}]

-  schedule = [{starts_on: 2026-01-01 23:00:00+0000, freq: 86400}]
+  schedule = [{starts_on: YYYY-MM-DD 23:00:00+0000, freq: 86400}]

-  schedule = [{starts_on: 2026-01-01 09:00:00+0000, freq: 86400}]
+  schedule = [{starts_on: YYYY-MM-DD 09:00:00+0000, freq: 86400}]

Also applies to: 41-44, 149-150, 193-194, 232-233, 279-280

packages/xano-skills/skills/xano-database-best-practices/SKILL.md-110-111 (1)

110-111: ⚠️ Potential issue | 🟡 Minor

Use consistent spelling for “Add-ons”.

“Addons” reads like a typo; use “Add-ons” (or “addons”) for consistency and readability.

✏️ Suggested edit
-- Use Addons for efficient joins
+- Use Add-ons for efficient joins
packages/xano-skills/skills/xano-database-best-practices/SKILL.md-26-27 (1)

26-27: ⚠️ Potential issue | 🟡 Minor

Clarify or soften the time-based default claim.

“Default since late 2025” can become stale or disputed. Consider either removing the time qualifier or replacing it with a precise, verifiable date or “as of” phrasing.

✏️ Suggested wording tweak
-**Standard SQL Format (Default since late 2025):**
+**Standard SQL Format (Default as of 2025-12):**
packages/opencode-templates/agents/xano-api-writer.md-158-171 (1)

158-171: ⚠️ Potential issue | 🟡 Minor

Align operator syntax in examples and operator table.

The operator table lists includes, but the CRUD example uses includes?. Please standardize to the correct operator name so readers don’t copy an invalid variant.

Also applies to: 216-220

packages/xano-skills/skills/caly-xano-cli/SKILL.md-477-477 (1)

477-477: ⚠️ Potential issue | 🟡 Minor

Fix typo in comment.

The word "genreted" should be "generated."

📝 Proposed fix
-# Optionally here do logic that would move your genreted library as an (internal) npm-package for reuse in FE projects
+# Optionally here do logic that would move your generated library as an (internal) npm-package for reuse in FE projects
packages/xano-skills/skills/caly-xano-cli/SKILL.md-232-232 (1)

232-232: ⚠️ Potential issue | 🟡 Minor

Fix grammatical error in warning message.

The phrase "might result with errors" is grammatically incorrect. It should be "might result in errors."

📝 Proposed fix
-**WARNING**: This is destructive! Overwrites all business logic and restores v1 branch. Data is also restored. Error-prone, this action might result with errors, that cannot be resolved without Support from Xano Team.
+**WARNING**: This is destructive! Overwrites all business logic and restores v1 branch. Data is also restored. Error-prone, this action might result in errors that cannot be resolved without support from the Xano Team.
.changeset/loose-pandas-hide.md-2-4 (1)

2-4: ⚠️ Potential issue | 🟡 Minor

Ensure new packages have changeset entries.

This changeset covers patch-level chore work (testing, environment, documentation) for the three listed packages, which is appropriate. However, several new packages added in this commit lack changesets:

  • @calycode/browser-consumer (0.1.0)
  • @calycode/opencode-templates (0.1.0)
  • @calycode/utils

Add changesets for these packages to document their inclusion in the release, or clarify if they should not be versioned separately.

packages/xano-skills/skills/xano-monitoring/SKILL.md-14-16 (1)

14-16: ⚠️ Potential issue | 🟡 Minor

Add language specifiers to fenced code blocks.

Eight code blocks are missing language identifiers, which affects consistent rendering and violates markdown best practices.

📝 Proposed fix: add language specifiers

Line 14:

-```
+```text
 Dashboard → Analytics → Query Performance

**Line 30:**
```diff
-```
+```text
 Sort by:

Line 58:

-```
+```text
 Seq Scan on orders  (cost=0.00..1000.00 rows=100 width=200)

Line 72:

-```
+```text
 Index Scan using idx_orders_user_id on orders  (cost=0.00..10.00 rows=100 width=200)

Line 336:

-```
+```text
 1. Check Query Analytics for slow endpoints

Line 344:

-```
+```text
 1. Get specific request parameters

Line 363:

-```
+```text
 1. Apply fix (index, code change, etc.)

Line 412:

-```
+```text
 Webhook → External monitoring:

Also applies to: 30-35, 58-62, 72-75, 336-340, 344-348, 363-368, 412-422

packages/xano-skills/README.md-47-63 (1)

47-63: ⚠️ Potential issue | 🟡 Minor

Add language identifier to fenced code block.

The directory structure code block should specify a language identifier for proper rendering and accessibility.

📝 Proposed fix
-```
+```text
 skills/
 ├── caly-xano-cli/
docs/commands/oc-init.md-2-7 (1)

2-7: ⚠️ Potential issue | 🟡 Minor

Fix markdownlint heading increment + command prompt without output.

MD001 triggers because the #### jumps from h1 to h4; MD014 flags $ commands without output. Align heading level and remove the shell prompt (or add output).

🔧 Proposed fix
-> #### Initialize OpenCode native host integration and configuration for use with the CalyCode extension.
+> ## Initialize OpenCode native host integration and configuration for use with the CalyCode extension.
 
 ```term
-$ xano oc init [options]
+xano oc init [options]
</details>

</blockquote></details>
<details>
<summary>docs/commands/oc-run.md-2-7 (1)</summary><blockquote>

`2-7`: _⚠️ Potential issue_ | _🟡 Minor_

**Fix markdownlint heading increment + command prompt without output.**

MD001 triggers because the `####` jumps from h1 to h4; MD014 flags `$` commands without output. Consider using a lower heading level or plain bold text and remove the `$` prompt (or add output).

<details>
<summary>🔧 Proposed fix</summary>

```diff
-> #### Run any OpenCode CLI command (default)
+> ## Run any OpenCode CLI command (default)
 
 ```term
-$ xano oc run [options]
+xano oc run [options]
</details>

</blockquote></details>
<details>
<summary>docs/commands/oc-templates-install.md-2-7 (1)</summary><blockquote>

`2-7`: _⚠️ Potential issue_ | _🟡 Minor_

**Fix markdownlint heading increment + command prompt without output.**

MD001 triggers because the `####` jumps from h1 to h4; MD014 flags `$` commands without output. Align heading level and remove the shell prompt (or add output).

<details>
<summary>🔧 Proposed fix</summary>

```diff
-> #### Install or reinstall OpenCode configuration templates.
+> ## Install or reinstall OpenCode configuration templates.
 
 ```term
-$ xano oc templates install [options]
+xano oc templates install [options]
</details>

</blockquote></details>
<details>
<summary>docs/commands/oc-templates-status.md-2-7 (1)</summary><blockquote>

`2-7`: _⚠️ Potential issue_ | _🟡 Minor_

**Fix markdownlint heading increment + command prompt without output.**

MD001 triggers because the `####` jumps from h1 to h4; MD014 flags `$` commands without output. Align heading level and remove the shell prompt (or add output).

<details>
<summary>🔧 Proposed fix</summary>

```diff
-> #### Show the status of installed OpenCode templates.
+> ## Show the status of installed OpenCode templates.
 
 ```term
-$ xano oc templates status [options]
+xano oc templates status [options]
</details>

</blockquote></details>
<details>
<summary>docs/commands/oc-templates-update.md-2-7 (1)</summary><blockquote>

`2-7`: _⚠️ Potential issue_ | _🟡 Minor_

**Fix markdownlint heading increment + command prompt without output.**

MD001 triggers because the `####` jumps from h1 to h4; MD014 flags `$` commands without output. Align heading level and remove the shell prompt (or add output).

<details>
<summary>🔧 Proposed fix</summary>

```diff
-> #### Update templates by fetching the latest versions from GitHub.
+> ## Update templates by fetching the latest versions from GitHub.
 
 ```term
-$ xano oc templates update [options]
+xano oc templates update [options]
</details>

</blockquote></details>
<details>
<summary>docs/_sidebar.md-37-44 (1)</summary><blockquote>

`37-44`: _⚠️ Potential issue_ | _🟡 Minor_

**Fix list indentation so “templates” nests under “oc”.**

Current indentation breaks the list hierarchy and triggers MD005.  


<details>
<summary>🛠️ Suggested fix</summary>

```diff
- -   **templates**
+  - **templates**
docs/commands/oc-templates-clear-cache.md-1-12 (1)

1-12: ⚠️ Potential issue | 🟡 Minor

Address markdownlint MD001/MD014 warnings.

Heading level jumps from H1 to H4, and $ prompts in code blocks lack output.

🛠️ Suggested fix
-> #### Clear the template cache (templates will be re-downloaded on next install).
+> ## Clear the template cache (templates will be re-downloaded on next install).

-$ xano oc templates clear-cache [options]
+ xano oc templates clear-cache [options]

-$ xano oc templates clear-cache --help
+ xano oc templates clear-cache --help
docs/commands/oc-templates.md-1-12 (1)

1-12: ⚠️ Potential issue | 🟡 Minor

Address markdownlint MD001/MD014 warnings.

Heading level skips H2, and $ prompts are used without output.

🛠️ Suggested fix
-> #### Manage OpenCode configuration templates (agents, commands, instructions).
+> ## Manage OpenCode configuration templates (agents, commands, instructions).

-$ xano oc templates [options]
+ xano oc templates [options]

-$ xano oc templates --help
+ xano oc templates --help
packages/opencode-templates/AGENTS.md-105-119 (1)

105-119: ⚠️ Potential issue | 🟡 Minor

Add a language to the fenced block (MD040).

Line 107 opens a code fence without a language tag. Add text (or similar) to satisfy markdown linting.

🔧 Proposed fix
-```
+```text
 project/
 ├── tables/           # Database schemas
 ├── functions/        # Reusable logic
 │   └── namespace/    # Organized by purpose
 ├── apis/             # REST endpoints
 │   └── group/        # Organized by API group
 ├── addons/           # Data fetchers
 ├── tasks/            # Scheduled jobs
 ├── agents/           # AI agents
 ├── tools/            # AI tools
 └── mcp_servers/      # MCP servers
</details>

</blockquote></details>
<details>
<summary>packages/cli/scripts/dev/install-win.bat-1-97 (1)</summary><blockquote>

`1-97`: _⚠️ Potential issue_ | _🟡 Minor_

**Ensure CRLF line endings for Windows batch compatibility.**  
Static analysis reports LF-only; CMD parsing can break with LF-only files.

</blockquote></details>
<details>
<summary>docs/guides/xanoscript.md-137-161 (1)</summary><blockquote>

`137-161`: _⚠️ Potential issue_ | _🟡 Minor_

**Add a language to the fenced block for markdownlint compliance.**  


<details>
<summary>Suggested fix</summary>

```diff
-```
+```text
 {output-dir}/
 ├── addons/
 │   ├── utility-addon.xs
 │   └── helper-addon.xs
 ├── functions/
 │   ├── auth/
 │   │   ├── login.xs
 │   │   └── verify-token.xs
 │   └── utils/
 │       └── format-date.xs
 ├── apis/
 │   ├── user-api/
 │   │   ├── _group.xs
 │   │   ├── get-users.xs
 │   │   └── create-user.xs
 │   └── product-api/
 │       └── ...
 ├── tables/
 │   ├── user.xs
 │   └── product.xs
 ├── tasks/
 │   └── daily-cleanup.xs
 └── ...
-```
+```
packages/xano-skills/skills/xano-data-access/SKILL.md-279-284 (1)

279-284: ⚠️ Potential issue | 🟡 Minor

Add a language tag to the fenced block for lint compliance.

Line 279 starts a fenced code block without a language specifier; markdownlint flags this.

Suggested fix
-```
+```text
 API Settings → Caching:
 - Cache Duration: 60 seconds
 - Cache Key: Based on inputs
 - Invalidation: Manual or time-based
-```
+```
packages/xano-skills/skills/xano-security/SKILL.md-307-314 (1)

307-314: ⚠️ Potential issue | 🟡 Minor

Add a language tag to the fenced block for lint compliance.

Line 307 starts a fenced code block without a language specifier; markdownlint flags this.

Suggested fix
-```
+```text
 API Settings:
 1. Select API group
 2. Set Rate Limiting:
    - Requests per minute: 60
    - Per: IP address or Auth token
 3. Save
-```
+```
docs/guides/testing.md-500-514 (1)

500-514: ⚠️ Potential issue | 🟡 Minor

Syntax error in example code.

The "Bad" example has a syntax issue with a semicolon inside the object literal, and the "Good" example has the same issue. This will cause a parsing error if users copy the code.

📝 Proposed fix
 // ❌ Bad
 {
    requestBody: {
-      password: 'mysecret123';
+      password: 'mysecret123'
    }
 }

 // ✅ Good
 {
    requestBody: {
-      password: '{{ENVIRONMENT.TEST_PASSWORD}}';
+      password: '{{ENVIRONMENT.TEST_PASSWORD}}'
    }
 }
packages/cli/scripts/dev/install-unix.sh-78-79 (1)

78-79: ⚠️ Potential issue | 🟡 Minor

Arch Linux: avoid partial upgrades with pacman -Sy

Using pacman -Sy without -u can lead to partial upgrades, which Arch explicitly warns against as it may break dependencies. Use -Syu for a full system upgrade or just -S if the user has recently synced.

♻️ Suggested fix
-            elif [ -f /etc/arch-release ]; then
-                sudo pacman -Sy --noconfirm nodejs npm
+            elif [ -f /etc/arch-release ]; then
+                sudo pacman -S --noconfirm nodejs npm
packages/cli/src/utils/feature-focused/registry/api.ts-20-23 (1)

20-23: ⚠️ Potential issue | 🟡 Minor

Incomplete encoded traversal check: mixed-case variants not blocked

The check only catches %2e%2e and %2E%2E, but misses mixed-case variants like %2E%2e or %2e%2E. Attackers could bypass this check.

🛡️ Suggested fix
    // Also block encoded traversal attempts
-   if (normalized.includes('%2e%2e') || normalized.includes('%2E%2E')) {
+   if (normalized.toLowerCase().includes('%2e%2e')) {
       throw new Error(`Invalid registry path: "${inputPath}" contains encoded path traversal`);
    }
packages/cli/scripts/installer/install.bat-30-36 (1)

30-36: ⚠️ Potential issue | 🟡 Minor

Arguments not forwarded in remote download path

Line 32 passes %* to the local PowerShell script, but line 35 using irm|iex doesn't forward any arguments. Users expecting -Version or -Uninstall flags to work in the remote case will be surprised.

🛠️ Suggested fix
 if exist "%SCRIPT_DIR%install.ps1" (
     echo [INFO] Running local PowerShell installer...
     powershell -NoProfile -ExecutionPolicy Bypass -File "%SCRIPT_DIR%install.ps1" %*
 ) else (
     echo [INFO] Downloading and running PowerShell installer...
-    powershell -NoProfile -ExecutionPolicy Bypass -Command "irm https://get.calycode.com/install.ps1 | iex"
+    powershell -NoProfile -ExecutionPolicy Bypass -Command "& { $script = irm https://get.calycode.com/install.ps1; Invoke-Expression $script } %*"
 )

Note: Passing arguments through iex requires additional handling. Consider downloading the script to a temp file first, then executing with arguments.

packages/cli/scripts/README.md-7-17 (1)

7-17: ⚠️ Potential issue | 🟡 Minor

Add a language for the directory tree fence.

markdownlint flags MD040 here. Use text (or bash) to satisfy the rule.

✅ Suggested fix
-```
+```text
 scripts/
 ├── installer/          # Production installers (for end-users)
 │   ├── install.sh      # Unix (macOS, Linux) installer
 │   ├── install.ps1     # Windows PowerShell installer
 │   └── install.bat     # Windows batch wrapper (launches PowerShell)
 ├── dev/                # Development scripts (for CLI developers)
 │   ├── install-unix.sh # Unix development setup
 │   └── install-win.bat # Windows development setup
 └── README.md           # This file
</details>

</blockquote></details>
<details>
<summary>packages/cli/scripts/installer/install.sh-37-63 (1)</summary><blockquote>

`37-63`: _⚠️ Potential issue_ | _🟡 Minor_

**Validate `--version` has a value before shifting.**

With `set -u`, calling `--version` without a value will abort on an unbound `$2`. Add a simple guard to keep the error message clean.

<details>
<summary>✅ Suggested fix</summary>

```diff
         --version|-v)
-            VERSION="$2"
-            shift 2
+            if [[ -z "${2:-}" || "$2" == -* ]]; then
+                fatal "Missing value for --version"
+            fi
+            VERSION="$2"
+            shift 2
             ;;
packages/browser-consumer/src/__tests__/browser-config-storage.test.ts-100-106 (1)

100-106: ⚠️ Potential issue | 🟡 Minor

Test assertion will fail: readFile returns Uint8Array, not string.

The readFile implementation (line 147-156 in browser-config-storage.ts) always returns Uint8Array. The test expects a string comparison which will fail.

🐛 Proposed fix
     describe('writeFile and readFile', () => {
         it('should write and read file', async () => {
             const content = 'test content';
             await storage.writeFile('test.txt', content);
             const readContent = await storage.readFile('test.txt');
-            expect(readContent).toBe(content);
+            // readFile returns Uint8Array, decode to compare
+            const decoded = new TextDecoder().decode(readContent as Uint8Array);
+            expect(decoded).toBe(content);
         });
docs/commands/oc.md-4-4 (1)

4-4: ⚠️ Potential issue | 🟡 Minor

Hyphenate “open-source” in the description.
Compound adjective should be hyphenated in both occurrences.

✏️ Suggested edit
-  Powered by OpenCode - The open source AI coding agent.
+  Powered by OpenCode - The open-source AI coding agent.

Also applies to: 16-16

docs/guides/scaffolding.md-350-363 (1)

350-363: ⚠️ Potential issue | 🟡 Minor

Add a language tag to the fenced block (MD040).
Use text for inline format examples.

✏️ Suggested edit
-```
+```text
 category/component-name
 
 Good:
 - auth/jwt-verify
 - utils/string-helpers
 - tables/user
 - api/user-management
 
 Bad:
 - myFunction
 - StringHelpers
 - USER_TABLE
</details>

</blockquote></details>
<details>
<summary>docs/guides/scaffolding.md-260-288 (1)</summary><blockquote>

`260-288`: _⚠️ Potential issue_ | _🟡 Minor_

**Add a language tag to the fenced block (MD040).**  
Use `text` for directory trees.  

<details>
<summary>✏️ Suggested edit</summary>

```diff
-```
+```text
 my-registry/
 ├── registry.json
 ├── items/
 │   ├── auth/
 │   │   ├── login/
 │   │   │   ├── definition.json
 │   │   │   └── login.xs
 │   │   ├── register/
 │   │   │   ├── definition.json
 │   │   │   └── register.xs
 │   │   └── middleware/
 │   │       ├── definition.json
 │   │       └── auth-middleware.xs
 │   ├── utils/
 │   │   ├── string-helpers/
 │   │   │   ├── definition.json
 │   │   │   └── helpers.xs
 │   │   └── date-utils/
 │   │       ├── definition.json
 │   │       └── date.xs
 │   └── tables/
 │       ├── user/
 │       │   ├── definition.json
 │       │   └── user.xs
 │       └── session/
 │           ├── definition.json
 │           └── session.xs
</details>

</blockquote></details>
<details>
<summary>docs/guides/scaffolding.md-71-79 (1)</summary><blockquote>

`71-79`: _⚠️ Potential issue_ | _🟡 Minor_

**Add a language tag to the fenced block (MD040).**  
Use `text` for directory trees.  

<details>
<summary>✏️ Suggested edit</summary>

```diff
-```
+```text
 my-components/
 ├── registry.json           # Registry configuration
 ├── items/
 │   └── sample-function/
 │       ├── definition.json # Component metadata
 │       └── function.xs     # XanoScript code
 └── README.md               # Documentation
</details>

</blockquote></details>
<details>
<summary>plans/opencode-native-host-analysis.md-180-186 (1)</summary><blockquote>

`180-186`: _⚠️ Potential issue_ | _🟡 Minor_

**Malformed markdown table.**

The table rows contain unescaped pipe characters inside cells, causing rendering issues. The version check column values like `^v1[8-9] ^v2` and `^v(18 | 19 | 2[0-9])` break the table structure.


<details>
<summary>📝 Suggested fix</summary>

```diff
-| Script                    | Node Version Check         |
-| ------------------------- | -------------------------- | --- | --------- |
-| `dev/install-win.bat`     | Yes (regex `^v1[8-9] ^v2`) |
-| `dev/install-unix.sh`     | Yes (numeric comparison)   |
-| `installer/install.bat`   | No (just checks existence) |
-| `installer/install.sh`    | Yes (regex `^v(18          | 19  | 2[0-9])`) |
-| `scripts/install-win.bat` | No                         |
+| Script                    | Node Version Check                      |
+| ------------------------- | --------------------------------------- |
+| `dev/install-win.bat`     | Yes (regex `^v1[8-9] ^v2`)              |
+| `dev/install-unix.sh`     | Yes (numeric comparison)                |
+| `installer/install.bat`   | No (just checks existence)              |
+| `installer/install.sh`    | Yes (regex `^v(18\|19\|2[0-9])`)        |
+| `scripts/install-win.bat` | No                                      |
packages/cli/src/utils/github-content-fetcher.ts-332-349 (1)

332-349: ⚠️ Potential issue | 🟡 Minor

Require both owner and repo for targeted cache clears.

With only one provided, the computed directory includes undefined and could remove an unintended path.

🧹 Suggested guard
       const { owner, repo, subpath } = options;
+      if (!owner || !repo) {
+         throw new Error('clearCache requires both owner and repo for targeted deletion');
+      }
       const targetDir = path.join(
          cacheDir,
          `${owner}-${repo}`,
          subpath?.replace(/\//g, '-') || '',
       );
packages/cli/src/commands/opencode/index.ts-164-173 (1)

164-173: ⚠️ Potential issue | 🟡 Minor

Validate --port before passing to serveOpencode.

parseInt can yield NaN or out‑of‑range values; reject invalid ports early.

✅ Suggested guard
       .action(async (options) => {
-         await serveOpencode({
-            port: options.port ? parseInt(options.port, 10) : undefined,
-            detach: options.detach,
-         });
+         const port = options.port ? Number(options.port) : undefined;
+         if (port != null && (!Number.isInteger(port) || port < 1 || port > 65535)) {
+            throw new Error('--port must be an integer between 1 and 65535');
+         }
+         await serveOpencode({
+            port: port ?? undefined,
+            detach: options.detach,
+         });
       });
🧹 Nitpick comments (38)
packages/xano-skills/skills/xano-query-performance/SKILL.md (2)

12-15: Add languages to fenced code blocks to satisfy Markdown linting.
Missing language identifiers trigger MD040; please annotate the fences.

Proposed fix
-```
+```text
 Standard SQL format → Full optimization available
 JSONB format → Limited indexing (GIN only)
-```
+```

-```
+```text
 Steps:
 1. Database → [table name] → Indexes tab
 2. Click "Create Index"
 3. Select field(s) to index
 4. Choose index type:
    - Index: Standard B-tree (most common)
    - Unique: Enforces uniqueness (email, slug)
    - Spatial: Geographic queries
    - Search: Full-text search (GIN)
 5. Save
-```
+```

-```
+```text
 Index fields: user_id (ASC), status (ASC), created_at (DESC)
-```
+```

-```
+```text
 Standard SQL: B-tree indexes on any column
 JSONB: GIN index on entire 'data' column only

 JSONB supports: data @> '{"status": "active"}'
 JSONB does NOT support: Efficient range queries, partial indexes
-```
+```

-```
+```text
 Dashboard → Analytics → Query Performance
-```
+```

Also applies to: 95-106, 121-123, 127-133, 302-305


330-334: Prefer Markdown links over raw URLs in the Resources section.
Raw URLs hurt readability; link text also survives formatting changes.

Proposed fix
-## Resources
-
-- Xano Indexing Guide: https://docs.xano.com/the-database/database-performance-and-maintenance/indexing
-- Database Performance: https://docs.xano.com/the-database/database-performance-and-maintenance
-- Query Optimization Tutorial: https://www.xano.com/learn/database-index-basics-speed-up-queries/
+## Resources
+
+- [Xano Indexing Guide](https://docs.xano.com/the-database/database-performance-and-maintenance/indexing)
+- [Database Performance](https://docs.xano.com/the-database/database-performance-and-maintenance)
+- [Query Optimization Tutorial](https://www.xano.com/learn/database-index-basics-speed-up-queries/)
packages/opencode-templates/agents/xano-task-writer.md (1)

55-64: Clarify that “Monthly = 2,592,000 seconds” is a 30‑day approximation.

Months aren’t fixed-length; the table can mislead users into drift. Consider noting it’s a 30‑day approximation and recommend date-based logic for true month‑end scheduling.

Proposed doc tweak
-| Monthly | `2592000` | Monthly billing |
+| Monthly (approx. 30 days) | `2592000` | Monthly billing (use date-based logic for true month-end) |
packages/cli/esbuild.config.ts (1)

28-28: Explicit keepNames: false is redundant but harmless.

Since minify: true is already set, esbuild defaults keepNames to false anyway. Adding it explicitly documents the intent, which is fine.

One consideration: with both keepNames: false and sourcemap: false, stack traces in production errors will show mangled function names with no way to map them back. For a CLI tool where users may report errors, preserving names (keepNames: true) can aid debugging at a small bundle size cost.

packages/xano-skills/skills/caly-xano-cli/SKILL.md (1)

382-391: Document the optional component selection flow.

According to the PR changes, the registry add command now accepts optional variadic components. Consider adding an example showing the interactive selection flow when no component names are provided.

📝 Suggested addition

Add before line 385:

# Interactive component selection
xano registry add

This would help users discover the interactive mode introduced by the registry refactor.

packages/opencode-templates/agents/api-designer.md (1)

73-95: Optional: Specify language for the code block.

Adding a language identifier to the code block (e.g., text or markdown) would improve rendering consistency and satisfy markdown linters.

📝 Proposed improvement
-```
+```text
 Endpoint: [METHOD] /path/to/resource
 Description: What this endpoint does
.changeset/forty-phones-rhyme.md (1)

1-7: Changeset correctly reflects the core refactor, but verify PR scope.

This changeset aligns with the PR title and appropriately uses patch-level bumps for the internal refactoring of moving registry installation logic to core.

However, this PR includes 5 changesets describing distinct changes (styling revamp, OpenCode integration, registry refactor, agent loading, graceful exit fix). While technically valid, consider whether this PR's scope is broader than a single focused change.

packages/xano-skills/README.md (1)

69-69: Optional: Consider hyphenating compound adjective.

When "Open source" modifies "AI coding agent", it functions as a compound adjective and style guides recommend hyphenation: "Open-source AI coding agent".

packages/xano-skills/skills/xano-schema-design/SKILL.md (1)

43-79: Consider adding language hints to fenced code blocks.

Several code blocks showing table schemas and UI steps lack language specifiers. While these aren't executable code, adding hints like text or plaintext improves accessibility and prevents linter warnings.

📝 Example fix for Lines 43-46
-```
+```text
 orders table:
 | id | customer_name | customer_email | product_name | product_price |
</details>

</blockquote></details>
<details>
<summary>packages/cli/scripts/dev/install-unix.sh (1)</summary><blockquote>

`61-65`: **Security consideration: piping remote scripts to bash**

Lines 63 and 69/72 download and execute scripts directly via `curl | bash`. While common for installers, this pattern bypasses integrity verification. Consider adding checksum verification or at minimum documenting the trust assumption in the script header.

For the Homebrew install specifically, you could verify the script before execution or link to Homebrew's official verification guidance.

</blockquote></details>
<details>
<summary>packages/cli/src/utils/feature-focused/registry/api.ts (1)</summary><blockquote>

`46-46`: **Consider normalizing trailing slashes on `REGISTRY_URL`**

If `CALY_REGISTRY_URL` ends with a trailing slash, the constructed URL will have a double slash (e.g., `http://example.com/registry//file.json`). While most servers handle this, some may not.

<details>
<summary>♻️ Optional fix</summary>

```diff
 async function fetchRegistry(paths: string[]) {
-   const REGISTRY_URL = process.env.CALY_REGISTRY_URL || 'http://localhost:5500/registry';
+   const REGISTRY_URL = (process.env.CALY_REGISTRY_URL || 'http://localhost:5500/registry').replace(/\/+$/, '');
packages/cli/scripts/installer/install.ps1 (1)

273-318: Uninstall error handling could be more verbose

In Invoke-Uninstall, errors are suppressed with 2>$null on line 280. Consider logging when uninstall operations fail rather than silently continuing, to help users diagnose partial uninstall states.

♻️ Optional improvement
     # Remove npm package
     if (Get-Command xano -ErrorAction SilentlyContinue) {
         Write-Log "Removing $PackageName package..." "INFO"
-        npm uninstall -g $PackageName 2>$null
+        $uninstallResult = npm uninstall -g $PackageName 2>&1
+        if ($LASTEXITCODE -ne 0) {
+            Write-Log "npm uninstall returned: $uninstallResult" "WARN"
+        }
     }
packages/core/src/features/registry/install-to-xano.ts (1)

57-62: Consider using RegistryItem type instead of any

The item parameter is typed as any, which bypasses TypeScript's type checking. Since RegistryItem is imported in related files and available from @repo/types, using it here would improve type safety.

-   item: any,
+   item: RegistryItem,
examples/test-config.example.js (1)

84-88: Clarify the relationship between store keys and ENVIRONMENT prefix

The store array uses keys like AUTH_TOKEN (line 86), but subsequent tests reference {{ENVIRONMENT.AUTH_TOKEN}} (line 116). Consider adding a brief comment explaining that stored values are accessed via the ENVIRONMENT prefix to help users understand the convention.

📝 Suggested documentation addition
       // Extract auth token for subsequent requests
+      // Note: Stored values are accessed as {{ENVIRONMENT.<key>}} in subsequent requests
       store: [
          { key: 'AUTH_TOKEN', path: '$.authToken' },
packages/core/src/implementations/run-tests.ts (1)

3-3: Unused import: AssertDefinition.

AssertDefinition is imported but not used in this file.

🧹 Proposed fix
-import { ApiGroupConfig, CoreContext, AssertDefinition, TestConfigEntry, TestResult, TestGroupResult } from '@repo/types';
+import { ApiGroupConfig, CoreContext, TestConfigEntry, TestGroupResult } from '@repo/types';
packages/browser-consumer/src/indexeddb-utils.ts (1)

192-197: Placeholder migration function should be documented or removed.

The migrateDB function is a no-op placeholder that only logs. Consider adding a // TODO: comment or removing it until needed to avoid confusion.

📝 Proposed improvement
 // Migration support (for future versions)
+// TODO: Implement migration logic when DB_VERSION is incremented
 export async function migrateDB(fromVersion: number, toVersion: number): Promise<void> {
-    // Placeholder for migration logic
-    console.log(`Migrating DB from ${fromVersion} to ${toVersion}`);
-    // Implement migration steps here
+    // Placeholder for future migration logic
+    console.warn(`DB migration from ${fromVersion} to ${toVersion} not yet implemented`);
 }
packages/browser-consumer/src/browser-config-storage.ts (3)

16-19: Duplicated GlobalConfig interface.

This interface is identical to the one exported from ./indexeddb-utils. Import it instead of redefining to maintain DRY.

♻️ Proposed fix
-import type { ConfigStorage, InstanceConfig, CoreContext, WorkspaceConfig, BranchConfig } from '@repo/types';
+import type { ConfigStorage, InstanceConfig, WorkspaceConfig, BranchConfig } from '@repo/types';
 import {
     initDB,
     getGlobalConfig,
     setGlobalConfig,
     getInstanceConfig,
     setInstanceConfig,
     getToken,
     setToken,
     getFile,
     setFile,
     deleteFile,
     listFiles,
+    GlobalConfig,
 } from './indexeddb-utils';
-
-interface GlobalConfig {
-    currentContext: CoreContext;
-    instances: string[];
-}

103-123: Potential stale cache in synchronous loadMergedConfig.

This method is synchronous and relies on cachedInstanceConfigs, but the cache is only populated during ensureDirs() or loadInstanceConfig(). If a caller hasn't called these methods first, the cache may be stale or empty. Consider documenting this requirement or making the method async.


196-211: Unexpected side effect: tarExtract persists all files to IndexedDB.

The method both returns extracted files and writes them to IndexedDB (line 207). This implicit persistence may be unexpected. Consider making this behavior explicit via a parameter or documenting it clearly.

📝 Proposed improvement
-    async tarExtract(tarGzBuffer: Uint8Array): Promise<{ [filename: string]: Uint8Array | string }> {
+    /**
+     * Extracts a tar archive and optionally persists files to IndexedDB.
+     * `@param` tarGzBuffer - The tar archive buffer
+     * `@param` persist - Whether to persist extracted files to IndexedDB (default: true)
+     */
+    async tarExtract(tarGzBuffer: Uint8Array, persist = true): Promise<{ [filename: string]: Uint8Array | string }> {
         // Use js-untar for browser-compatible tar extraction
         const { untar } = await import('js-untar');
         const files = await untar(tarGzBuffer);
         const result: { [filename: string]: Uint8Array | string } = {};
         for (const file of files) {
             if (file.isFile) {
                 // Convert buffer to Uint8Array if needed
                 const content = file.buffer instanceof Uint8Array ? file.buffer : new Uint8Array(file.buffer);
                 result[file.name] = content;
-                // Optionally store in IndexedDB
-                await setFile(file.name, content);
+                if (persist) {
+                    await setFile(file.name, content);
+                }
             }
         }
         return result;
     }
packages/cli/src/commands/registry/index.ts (1)

29-31: Consider removing or gating the debug log.

The console.log('command registry option: ', options.registry) will output to stdout in production usage. For a cleaner CLI experience, consider removing this or gating it behind a --verbose flag.

♻️ Proposed fix
         withErrorHandler(async (components, options) => {
            if (options.registry) {
-               console.log('command registry option: ', options.registry);
               process.env.CALY_REGISTRY_URL = options.registry;
            }
packages/cli/src/index.ts (1)

4-12: Consider more precise argument detection.

The current check args.includes('opencode') && args.includes('native-host') could theoretically match unintended argument combinations (e.g., if these strings appear as option values). For robustness, consider checking for consecutive arguments or the specific subcommand pattern.

♻️ Proposed refinement for more precise matching
 const args = process.argv;
-if (args.includes('opencode') && args.includes('native-host')) {
+const opcodeIdx = args.indexOf('opencode');
+if (opcodeIdx !== -1 && args[opcodeIdx + 1] === 'native-host') {
    startNativeHost();
 } else {
    program.parseAsync();
 }

That said, the current implementation is pragmatic and unlikely to cause issues given the CLI's command structure.

plans/browser-consumer-plan.md (1)

13-23: Add language specifier to the fenced code block.

The package structure code block should specify a language for proper rendering and linting compliance.

📝 Suggested fix
-```
+```text
 packages/browser-consumer/
 ├── src/
 │   ├── index.ts                 # Main exports
packages/core/src/features/registry/__tests__/api.spec.ts (1)

14-36: Consider adding a test for caching behavior.

The getRegistryIndex function includes in-memory caching per the AI summary, but there's no test verifying that subsequent calls return the cached value without making additional fetch requests.

🧪 Suggested test case
it('should return cached index on subsequent calls', async () => {
    const mockIndex = { items: ['item1', 'item2'] };
    (global.fetch as jest.Mock).mockResolvedValueOnce({
        ok: true,
        json: () => Promise.resolve(mockIndex),
    });

    const result1 = await getRegistryIndex(mockRegistryUrl);
    const result2 = await getRegistryIndex(mockRegistryUrl);

    expect(global.fetch).toHaveBeenCalledTimes(1);
    expect(result1).toEqual(mockIndex);
    expect(result2).toEqual(mockIndex);
});
plans/opencode-native-host-analysis.md (1)

31-65: Add language specifier for ASCII diagram.

The architecture diagram code block should specify a language (e.g., text or plaintext) for proper rendering.

📝 Suggested fix
-```
+```text
 Chrome Extension
        │
packages/core/src/features/registry/__tests__/install-to-xano.spec.ts (1)

131-147: Consider adding tests for null workspaceConfig and branchConfig.

The test covers instanceConfig: null, but the AI summary indicates the function validates required context including workspace and branch configs. Adding similar tests for these scenarios would improve coverage.

🧪 Suggested additional test cases
it('should throw when workspaceConfig is null', async () => {
   const item = {
      name: 'test',
      type: 'registry:function',
      content: 'test',
      files: [],
   };

   await expect(
      installRegistryItemToXano(
         item,
         { ...mockContext, workspaceConfig: null } as any,
         'https://registry.example.com',
         createMockCore(),
      ),
   ).rejects.toThrow('workspaceConfig is required');
});

it('should throw when branchConfig is null', async () => {
   const item = {
      name: 'test',
      type: 'registry:function',
      content: 'test',
      files: [],
   };

   await expect(
      installRegistryItemToXano(
         item,
         { ...mockContext, branchConfig: null } as any,
         'https://registry.example.com',
         createMockCore(),
      ),
   ).rejects.toThrow('branchConfig is required');
});
docs/guides/git-workflow.md (3)

87-103: Add language specifier for directory structure.

📝 Suggested fix
-```
+```text
 project/
 ├── schema/

120-131: Add language specifier for branching diagram.

📝 Suggested fix
-```
+```text
 main (production)
 │

349-365: Add language specifier for commit message format.

📝 Suggested fix
-```
+```text
 type(scope): description
 
 Types:
docs/guides/patterns.md (2)

84-98: Add language specifiers for pseudocode blocks.

These code blocks contain pseudocode/examples that should have language specifiers for proper rendering.

📝 Suggested fixes
-```
+```text
 // Pseudocode for logging function
 function log_to_axiom(level, message, data) {
-```
+```text
 // In your Xano functions
 call: log_to_axiom("info", "User logged in", { user_id: $user.id })

201-209: Specify JSON language for error response example.

This code block contains a JSON structure and should use the json language specifier.

📝 Suggested fix
-```
+```json
 // Error response structure
 {
   "error": true,

Note: If keeping the comment, use jsonc (JSON with comments) or move the comment outside the code block.

docs/guides/registry-authoring.md (1)

50-58: Consider adding language specifier to fenced code blocks.

The static analysis tool flagged several code blocks without language specifiers (lines 50, 123, 358, 543, 592). For directory tree structures like this one, you can use text or plaintext to suppress the warning while preserving readability.

Example fix
-```
+```text
 my-registry/
 ├── registry.json           # Registry metadata
packages/cli/build-sea.ts (2)

2-2: Remove unused imports.

rmSync and chmodSync are imported but never used in this file.

Suggested fix
-import { copyFileSync, existsSync, mkdirSync, rmSync, chmodSync } from 'fs';
+import { copyFileSync, existsSync, mkdirSync } from 'fs';

50-59: Consider capturing error details for debugging.

When blob injection fails, the error is logged but the message may not be informative since error could be an Error object. Consider extracting the message for clearer output.

Suggested improvement
 } catch (error) {
-    console.error('Failed to inject blob:', error);
+    console.error('Failed to inject blob:', error instanceof Error ? error.message : error);
     process.exit(1);
 }
packages/cli/src/utils/feature-focused/registry/registry-url-map.ts (1)

37-48: Indentation differs from the rest of the file.

Lines 37-48 use 4-space indentation while the rest of the file uses 3-space. Consider aligning for consistency.

plans/create-native-bundle.md (1)

20-35: Consider adding language specifiers to fenced code blocks.

Static analysis flagged two code blocks without language specifiers (lines 20 and 30). For ASCII diagrams or plain text flows, use text or plaintext to satisfy markdown linting rules.

📝 Proposed fix
-```
+```text
 Extension → JSON message → Native Host → `npx opencode-ai@latest serve --port 4096`
-```
+```text
 1. Download calycode-x64.exe / calycode-macos (one binary)
packages/core/src/index.ts (2)

529-551: Inconsistent indentation in new registry methods.

The new registry methods use 4-space indentation, while the rest of the class uses 3-space indentation. This creates visual inconsistency.

🔧 Proposed fix for consistent indentation
    // ----- REGISTRY METHODS ----- //
-    /**
-     * Get the main registry index.
-     */
-    async getRegistryIndex(registryUrl: string) {
-       return getRegistryIndex(registryUrl);
-    }
+   /**
+    * Get the main registry index.
+    */
+   async getRegistryIndex(registryUrl: string) {
+      return getRegistryIndex(registryUrl);
+   }

-    /**
-     * Get a specific registry item by name.
-     */
-    async getRegistryItem(componentName: string, registryUrl: string) {
-        return getRegistryItem(componentName, registryUrl);
-    }
+   /**
+    * Get a specific registry item by name.
+    */
+   async getRegistryItem(componentName: string, registryUrl: string) {
+      return getRegistryItem(componentName, registryUrl);
+   }

-    async installRegistryItemToXano(
-        item: RegistryItem,
-        resolvedContext: { instanceConfig: InstanceConfig; workspaceConfig: WorkspaceConfig; branchConfig: BranchConfig },
-        registryUrl: string,
-    ) {
-       const results = await installRegistryItemToXano(item, resolvedContext, registryUrl, this);
-       return results;
-    }
+   async installRegistryItemToXano(
+      item: RegistryItem,
+      resolvedContext: { instanceConfig: InstanceConfig; workspaceConfig: WorkspaceConfig; branchConfig: BranchConfig },
+      registryUrl: string,
+   ) {
+      const results = await installRegistryItemToXano(item, resolvedContext, registryUrl, this);
+      return results;
+   }

544-551: Consider adding JSDoc for installRegistryItemToXano.

Other methods in this class have comprehensive JSDoc with @param and @returns annotations. Adding documentation here would maintain consistency and help consumers understand the method's contract.

packages/cli/src/commands/test/implementation/test.ts (1)

283-303: Inconsistent indentation in CI mode block.

This block uses 4-space indentation while the rest of the file uses 3-space indentation.

🔧 Proposed fix
-    // CI mode: determine exit code
-    if (ciMode) {
-       const hasFailures = totalFailed > 0;
-       const hasWarnings = failOnWarnings && totalWarnings > 0;
-       
-       if (hasFailures || hasWarnings) {
-          const reasons = [];
-          if (hasFailures) reasons.push(`${totalFailed} test(s) failed`);
-          if (hasWarnings) reasons.push(`${totalWarnings} warning(s)`);
-          
-          outro(`Tests failed: ${reasons.join(', ')}`);
-          log.error(`\nCI mode: Exiting with code 1 due to ${reasons.join(' and ')}`);
-          // Return exit code instead of calling process.exit() directly
-          // Let the CLI commander action handle the actual process exit
-          return 1;
-       } else {
-          outro(`All ${totalPassed} tests passed!`);
-       }
-    }
-
-    return totalFailed > 0 ? 1 : 0;
+   // CI mode: determine exit code
+   if (ciMode) {
+      const hasFailures = totalFailed > 0;
+      const hasWarnings = failOnWarnings && totalWarnings > 0;
+
+      if (hasFailures || hasWarnings) {
+         const reasons = [];
+         if (hasFailures) reasons.push(`${totalFailed} test(s) failed`);
+         if (hasWarnings) reasons.push(`${totalWarnings} warning(s)`);
+
+         outro(`Tests failed: ${reasons.join(', ')}`);
+         log.error(`\nCI mode: Exiting with code 1 due to ${reasons.join(' and ')}`);
+         // Return exit code instead of calling process.exit() directly
+         // Let the CLI commander action handle the actual process exit
+         return 1;
+      } else {
+         outro(`All ${totalPassed} tests passed!`);
+      }
+   }
+
+   return totalFailed > 0 ? 1 : 0;

"@calycode/cli": patch
---

chore: updates to testing, to have robust env support, and better documentation
Copy link
Contributor

@coderabbitai coderabbitai bot Feb 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Changeset description doesn't match PR objectives.

The description states "chore: updates to testing, to have robust env support, and better documentation", but the PR title is "refactor: move registry item installation to the core". The description should accurately reflect the primary changes: moving registry installation logic from CLI to core, introducing new registry APIs, and refactoring the CLI to delegate to core.

📝 Suggested description rewrite
-chore: updates to testing, to have robust env support, and better documentation
+refactor: move registry item installation logic to core package
+
+Relocates registry installation functionality from CLI to core, adding new APIs for registry index fetching, item retrieval, and Xano installation. The CLI now delegates to core for registry operations. Also includes enhancements to testing, environment support, and documentation.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
chore: updates to testing, to have robust env support, and better documentation
refactor: move registry item installation logic to core package
Relocates registry installation functionality from CLI to core, adding new APIs for registry index fetching, item retrieval, and Xano installation. The CLI now delegates to core for registry operations. Also includes enhancements to testing, environment support, and documentation.
🤖 Prompt for AI Agents
In @.changeset/loose-pandas-hide.md at line 7, Update the changeset description
to reflect the actual PR intent: replace the generic "chore: updates to testing,
to have robust env support, and better documentation" with a summary that states
the refactor moves registry item installation from the CLI into the core,
introduces new registry APIs (core registry installation and registry-related
functions) and updates the CLI to delegate installation to core; mention any
breaking behavior or migration notes for users of the CLI/registry APIs and keep
the tone as a concise changelog entry matching the PR title "refactor: move
registry item installation to the core".

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • Create updated chabgeset files according to the available commit messages and changed files (we forgot to properly update the changesets along the way)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧩 Analysis chain

@MihalyToth20 Good plan! I can help you review the commit messages to identify what changesets should be created. Let me gather the commit history from this PR.


🏁 Script executed:

#!/bin/bash
# Get the list of commits in the PR branch that are not in main
git log --oneline main..dev

echo ""
echo "=== Detailed commit messages ==="
git log --format="%H%n%s%n%b%n---" main..dev | head -100

Length of output: 413


🏁 Script executed:

#!/bin/bash
# Get commits from the PR using GitHub CLI
gh pr view 184 --json commits --jq '.commits[] | "[\(.oid[0:7])] \(.messageHeadline)\n\(.messageBody)"'

echo ""
echo "=== Files changed in PR ==="
gh pr view 184 --json files --jq '.files[] | .path' | sort

Length of output: 7658


🏁 Script executed:

#!/bin/bash
# Read all changeset files to see what's already documented
echo "=== Existing Changeset Files ==="
for file in .changeset/*.md; do
  if [[ "$file" != *"README"* ]] && [[ -f "$file" ]]; then
    echo ""
    echo "--- $(basename "$file") ---"
    cat "$file"
  fi
done

Length of output: 1207


@MihalyToth20 Great! I've analyzed the commits and existing changesets. Here's what I found:

📊 Current Coverage Analysis

Covered by existing changesets:

  • ✅ Registry refactor → forty-phones-rhyme.md (correctly describes the PR title)
  • ✅ OpenCode/native host features → dull-facts-lose.md
  • ✅ Skills features → large-bugs-drop.md + ten-regions-switch.md
  • ✅ Help command styling → light-crabs-march.md
  • ✅ Registry graceful exit → plain-ways-fall.md
  • ✅ Remote Xano templates → spicy-bags-guess.md
  • ⚠️ Test/env/docs improvements → loose-pandas-hide.md (needs better description)

❌ Missing changeset:

  • Browser consumer package (@calycode/browser-consumer) - This is a new package with significant changes but no changeset!

🔧 Recommended Actions

1. Create new changeset for browser-consumer

The new packages/browser-consumer/ package (IndexedDB config storage for browser extensions) needs its own changeset:

---
"@calycode/browser-consumer": minor
---

feat: add browser-consumer package for IndexedDB-based config storage

Introduces a new package to support browser extensions with IndexedDB utilities and config storage capabilities.

2. Update loose-pandas-hide.md description

Replace the generic description with something more specific:

chore: enhance test runner with environment handling, add test examples and improve documentation

Improves test configuration with robust environment variable support, adds test config examples, and updates documentation guides for testing, git workflow, patterns, scaffolding, and xanoscript.

3. Consider consolidating skills changesets

You have two skills-related changesets (large-bugs-drop.md and ten-regions-switch.md). Consider whether these should be one changeset or if they represent distinct features.

Would you like me to help draft the complete updated changeset files?

Comment on lines +11 to +32
async function main() {
const esmResult = await build({
...baseConfig,
entryPoints: ['src/index.ts'],
outdir: 'dist',
format: 'esm',
outExtension: { '.js': '.js' },
sourcemap: false,
metafile: true,
});
const cjsResult = await build({
...baseConfig,
entryPoints: ['src/index.ts'],
outdir: 'dist',
format: 'cjs',
outExtension: { '.js': '.cjs' },
sourcemap: false,
metafile: true,
});

await writeFile(resolve(distDir, 'esm-meta.json'), JSON.stringify(esmResult.metafile, null, 2));
await writeFile(resolve(distDir, 'cjs-meta.json'), JSON.stringify(cjsResult.metafile, null, 2));
Copy link
Contributor

@coderabbitai coderabbitai bot Feb 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cat -n packages/browser-consumer/esbuild.config.ts | head -50

Repository: calycode/xano-tools

Length of output: 1437


🏁 Script executed:

cat packages/browser-consumer/package.json

Repository: calycode/xano-tools

Length of output: 1958


🏁 Script executed:

git ls-files packages/browser-consumer/ | head -20

Repository: calycode/xano-tools

Length of output: 520


🏁 Script executed:

cat packages/browser-consumer/tsconfig.json

Repository: calycode/xano-tools

Length of output: 328


🏁 Script executed:

cat esbuild.base.ts

Repository: calycode/xano-tools

Length of output: 466


🏁 Script executed:

cat -n pnpm-workspace.yaml 2>/dev/null || echo "No pnpm-workspace.yaml found"

Repository: calycode/xano-tools

Length of output: 102


🏁 Script executed:

find packages -name "esbuild.config.ts" -type f

Repository: calycode/xano-tools

Length of output: 202


🏁 Script executed:

for file in $(find packages -name "esbuild.config.ts" -type f); do
  echo "=== $file ==="
  head -20 "$file" | grep -E "(entryPoints|outdir|resolve\(__dirname)"
done

Repository: calycode/xano-tools

Length of output: 681


🏁 Script executed:

cat packages/cli/esbuild.config.ts | head -35

Repository: calycode/xano-tools

Length of output: 1059


Make build paths absolute to avoid CWD-dependent output.

entryPoints: ['src/index.ts'] and outdir: 'dist' are relative to the process CWD, while the metafile write uses distDir derived from this file's directory. When the script runs from the repo root (standard in monorepos), the build outputs and metafiles will land in different folders. Use resolve(__dirname, ...) for both entryPoints and outdir to match the pattern already used in packages/cli/esbuild.config.ts.

🛠️ Proposed fix
+const rootDir = resolve(__dirname);
       const esmResult = await build({
          ...baseConfig,
-         entryPoints: ['src/index.ts'],
+         entryPoints: [resolve(rootDir, 'src/index.ts')],
-         outdir: 'dist',
+         outdir: distDir,
       const cjsResult = await build({
          ...baseConfig,
-         entryPoints: ['src/index.ts'],
+         entryPoints: [resolve(rootDir, 'src/index.ts')],
-         outdir: 'dist',
+         outdir: distDir,
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
async function main() {
const esmResult = await build({
...baseConfig,
entryPoints: ['src/index.ts'],
outdir: 'dist',
format: 'esm',
outExtension: { '.js': '.js' },
sourcemap: false,
metafile: true,
});
const cjsResult = await build({
...baseConfig,
entryPoints: ['src/index.ts'],
outdir: 'dist',
format: 'cjs',
outExtension: { '.js': '.cjs' },
sourcemap: false,
metafile: true,
});
await writeFile(resolve(distDir, 'esm-meta.json'), JSON.stringify(esmResult.metafile, null, 2));
await writeFile(resolve(distDir, 'cjs-meta.json'), JSON.stringify(cjsResult.metafile, null, 2));
async function main() {
const rootDir = resolve(__dirname);
const esmResult = await build({
...baseConfig,
entryPoints: [resolve(rootDir, 'src/index.ts')],
outdir: distDir,
format: 'esm',
outExtension: { '.js': '.js' },
sourcemap: false,
metafile: true,
});
const cjsResult = await build({
...baseConfig,
entryPoints: [resolve(rootDir, 'src/index.ts')],
outdir: distDir,
format: 'cjs',
outExtension: { '.js': '.cjs' },
sourcemap: false,
metafile: true,
});
await writeFile(resolve(distDir, 'esm-meta.json'), JSON.stringify(esmResult.metafile, null, 2));
await writeFile(resolve(distDir, 'cjs-meta.json'), JSON.stringify(cjsResult.metafile, null, 2));
🤖 Prompt for AI Agents
In `@packages/browser-consumer/esbuild.config.ts` around lines 11 - 32, The build
uses relative paths for entryPoints and outdir causing CWD-dependent output;
update both build invocations in main() to use absolute paths via
resolve(__dirname, ...): set entryPoints to [resolve(__dirname, 'src/index.ts')]
and outdir to resolve(__dirname, 'dist') (or to the existing distDir if that's
already resolve(__dirname, 'dist')), for both the esmResult and cjsResult builds
so the metafile writes (writeFile using distDir) align with the actual build
outputs.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • Fix file path resolution issue, pick examples from the cli and the core packages

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧩 Analysis chain

@MihalyToth20, I'll gather the correct patterns from the cli and core packages for you!


🏁 Script executed:

#!/bin/bash
# Get the full esbuild config from cli package
echo "=== packages/cli/esbuild.config.ts ==="
cat packages/cli/esbuild.config.ts
echo ""
echo ""

# Get the full esbuild config from core package
echo "=== packages/core/esbuild.config.ts ==="
cat packages/core/esbuild.config.ts

Length of output: 2602


@MihalyToth20, here's what I found:

Correct Pattern (from packages/cli/esbuild.config.ts):

const __dirname = dirname(fileURLToPath(import.meta.url));
const rootDir = resolve(__dirname);
const distDir = resolve(__dirname, 'dist');

const result = await build({
  entryPoints: [resolve(rootDir, 'src/index.ts')],  // ✅ Absolute path
  outfile: resolve(distDir, 'index.cjs'),            // ✅ Absolute path
  // ... other options
});

await writeFile(resolve(distDir, 'meta.json'), ...); // ✅ Absolute path

Issue Found (both packages/core/esbuild.config.ts AND packages/browser-consumer/esbuild.config.ts):

const distDir = resolve(__dirname, 'dist');

await build({
  entryPoints: ['src/index.ts'],  // ❌ Relative to CWD
  outdir: 'dist',                 // ❌ Relative to CWD
  // ...
});

await writeFile(resolve(distDir, 'esm-meta.json'), ...); // ✅ Uses absolute distDir

The problem: When the build runs from the repo root, entryPoints and outdir resolve relative to the repo root, but the metafile writes use distDir which points to the package directory. They end up in different locations.

🔧 Fix for packages/browser-consumer/esbuild.config.ts:

 const __dirname = dirname(fileURLToPath(import.meta.url));
+const rootDir = resolve(__dirname);
 const distDir = resolve(__dirname, 'dist');

 async function main() {
    const esmResult = await build({
       ...baseConfig,
-      entryPoints: ['src/index.ts'],
+      entryPoints: [resolve(rootDir, 'src/index.ts')],
-      outdir: 'dist',
+      outdir: distDir,
       format: 'esm',
       // ...
    });
    const cjsResult = await build({
       ...baseConfig,
-      entryPoints: ['src/index.ts'],
+      entryPoints: [resolve(rootDir, 'src/index.ts')],
-      outdir: 'dist',
+      outdir: distDir,
       format: 'cjs',
       // ...
    });

Note: The packages/core/esbuild.config.ts file has the exact same issue and should be fixed the same way! 🔍


🧠 Learnings used
Learnt from: CR
Repo: calycode/xano-tools PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-28T05:59:33.267Z
Learning: Use Esbuild or Rollup for bundling distribution packages

Learnt from: CR
Repo: calycode/xano-tools PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-28T05:59:33.267Z
Learning: Applies to packages/cli/src/index.ts : CLI entry point is packages/cli/src/index.ts (source) and dist/index.cjs (compiled distribution)

Learnt from: CR
Repo: calycode/xano-tools PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-28T05:59:33.267Z
Learning: Applies to packages/*/src/**/*.ts?(x) : Configure and use Eslint and Prettier for linting and formatting with default configs

Learnt from: CR
Repo: calycode/xano-tools PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-28T05:59:33.267Z
Learning: Applies to packages/core/src/index.ts : Core logic entry point is packages/core/src/index.ts

Learnt from: CR
Repo: calycode/xano-tools PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-28T05:59:33.267Z
Learning: Applies to packages/*/src/**/*.ts?(x) : Use TypeScript for all source code in the monorepo

Comment on lines +1 to +48
@echo off
REM ============================================================================
REM CalyCode CLI Installer (Windows)
REM ============================================================================
REM This is a wrapper script that launches the PowerShell installer.
REM For direct PowerShell usage, run:
REM irm https://get.calycode.com/install.ps1 | iex
REM ============================================================================

setlocal

echo.
echo CalyCode CLI Installer
echo ======================
echo.

REM Check if PowerShell is available
where powershell >nul 2>nul
if %ERRORLEVEL% NEQ 0 (
echo [ERROR] PowerShell is required but not found in PATH.
echo Please install PowerShell or run the installer manually.
pause
exit /b 1
)

REM Get the directory where this script is located
set "SCRIPT_DIR=%~dp0"

REM Check if the PowerShell script exists locally
if exist "%SCRIPT_DIR%install.ps1" (
echo [INFO] Running local PowerShell installer...
powershell -NoProfile -ExecutionPolicy Bypass -File "%SCRIPT_DIR%install.ps1" %*
) else (
echo [INFO] Downloading and running PowerShell installer...
powershell -NoProfile -ExecutionPolicy Bypass -Command "irm https://get.calycode.com/install.ps1 | iex"
)

if %ERRORLEVEL% NEQ 0 (
echo.
echo [ERROR] Installation failed with error code %ERRORLEVEL%
echo.
pause
exit /b %ERRORLEVEL%
)

echo.
echo Press any key to exit...
pause >nul
Copy link
Contributor

@coderabbitai coderabbitai bot Feb 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Critical: Unix line endings will break batch file execution on Windows

Static analysis detected Unix line endings (LF-only) in this batch file. Windows batch parser has known issues with LF-only line endings, particularly around GOTO/CALL label parsing and 512-byte boundary bugs. This can cause silent failures or unexpected behavior.

Convert to Windows line endings (CRLF) before merging. You can configure git with:

git config core.autocrlf true

Or add to .gitattributes:

*.bat text eol=crlf
🧰 Tools
🪛 Blinter (1.0.112)

[error] 1-1: Unix line endings detected. Explanation: Batch file uses Unix line endings (LF-only) which can cause GOTO/CALL label parsing failures and script malfunction due to Windows batch parser 512-byte boundary bugs. Recommendation: Convert file to Windows line endings (CRLF). Use tools like dos2unix, notepad++, or configure git with 'git config core.autocrlf true'. Context: File uses Unix line endings (LF-only) - 48 LF sequences found

(E018)


[error] 32-32: PowerShell execution policy bypass. Explanation: Bypassing PowerShell execution policy can allow malicious scripts to run. Recommendation: Avoid using -ExecutionPolicy Bypass unless absolutely necessary. Context: PowerShell execution policy bypass detected

(SEC009)

🤖 Prompt for AI Agents
In `@packages/cli/scripts/installer/install.bat` around lines 1 - 48, The batch
installer (install.bat) currently has Unix (LF) line endings which will break
Windows batch parsing; convert this file to Windows CRLF line endings before
merging (ensure the checked-in file uses CRLF), and add a repo-level rule so
future changes stay CRLF (e.g., enable core.autocrlf or add a .gitattributes
entry forcing *.bat eol=crlf); verify the committed install.bat (the wrapper
that checks powershell, uses %~dp0 as SCRIPT_DIR and conditionally runs
install.ps1) contains CRLF line endings and re-run the static check to confirm
the issue is resolved.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I haven't experienced failures with these on my windows machine, ignore for now.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@MihalyToth20, understood! I'll skip this issue for now since it's working fine on your Windows setup. The line ending concern is environment-specific and may not affect all configurations.

Comment on lines +499 to +571
const restartServer = async (port: number = 4096, extraOrigins: string[] = []) => {
logger.log('Restart requested', { port, extraOrigins });

// Kill existing server process if we have a reference
if (serverProc) {
logger.log('Killing existing server process for restart...');
serverProc.kill();
serverProc = null;
// Give it a moment to release the port
await new Promise((resolve) => setTimeout(resolve, 500));
}

// Kill any orphan process on the port (handles lost references)
logger.log('Checking for orphan processes on port...');
const killed = killProcessOnPort(port, logger);
if (killed) {
logger.log('Killed orphan process(es) on port, waiting for port release...');
// Give more time for port to be released after force kill
await new Promise((resolve) => setTimeout(resolve, 1000));
}

// Verify port is actually free now
const serverUrl = `http://localhost:${port}`;
try {
await fetch(serverUrl);
// If we get here, something is still running on the port
logger.error('Port still in use after kill attempts');
sendMessage({
status: 'error',
message: `Port ${port} still in use after cleanup attempts. Please try again or use a different port.`
});
return;
} catch (e) {
// Good, nothing running - port is free
logger.log('Port is now free, starting server...');
}

// Start fresh with new config
await startServer(port, extraOrigins);
};

const handleMessage = (msg: any) => {
logger.log('Received message', msg);

try {
if (msg.type === 'ping') {
sendMessage({ type: 'pong', timestamp: Date.now() });
} else if (msg.type === 'start') {
const port = msg.port ? parseInt(msg.port, 10) : 4096;
const origins = Array.isArray(msg.origins) ? msg.origins : [];
startServer(port, origins);
} else if (msg.type === 'restart') {
// Restart the server with new origins - used when CORS configuration needs updating
const port = msg.port ? parseInt(msg.port, 10) : 4096;
const origins = Array.isArray(msg.origins) ? msg.origins : [];
restartServer(port, origins);
} else if (msg.type === 'stop') {
const port = msg.port ? parseInt(msg.port, 10) : 4096;
logger.log('Stop requested', { port, hasServerProc: !!serverProc });

// Kill by process reference if we have it
if (serverProc) {
logger.log('Killing server process by reference...');
serverProc.kill();
serverProc = null;
}

// Also kill any orphan process on the port (handles lost references)
const killed = killProcessOnPort(port, logger);
if (killed) {
logger.log('Killed orphan process(es) on port');
}

Copy link
Contributor

@coderabbitai coderabbitai bot Feb 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Avoid killing arbitrary processes bound to the port.

killProcessOnPort is invoked on restart/stop and will terminate any listener on that port, even if it’s unrelated to OpenCode. That’s risky for users who already have services on 4096. Prefer killing only a process you started (PID tracking) or return a clear error asking for a different port.

✅ Suggested direction (guard against unknown owners)
-      // Kill any orphan process on the port (handles lost references)
-      logger.log('Checking for orphan processes on port...');
-      const killed = killProcessOnPort(port, logger);
-      if (killed) {
-         logger.log('Killed orphan process(es) on port, waiting for port release...');
-         // Give more time for port to be released after force kill
-         await new Promise((resolve) => setTimeout(resolve, 1000));
-      }
+      // Only try a port-level kill if we previously started the server.
+      const hadServerProc = !!serverProc;
+      if (!hadServerProc) {
+         sendMessage({
+            status: 'error',
+            message: `Port ${port} is in use by another process. Please stop it or choose a different port.`,
+         });
+         return;
+      }
+      logger.log('Checking for orphan processes on port...');
+      const killed = killProcessOnPort(port, logger);
+      if (killed) {
+         logger.log('Killed orphan process(es) on port, waiting for port release...');
+         await new Promise((resolve) => setTimeout(resolve, 1000));
+      }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const restartServer = async (port: number = 4096, extraOrigins: string[] = []) => {
logger.log('Restart requested', { port, extraOrigins });
// Kill existing server process if we have a reference
if (serverProc) {
logger.log('Killing existing server process for restart...');
serverProc.kill();
serverProc = null;
// Give it a moment to release the port
await new Promise((resolve) => setTimeout(resolve, 500));
}
// Kill any orphan process on the port (handles lost references)
logger.log('Checking for orphan processes on port...');
const killed = killProcessOnPort(port, logger);
if (killed) {
logger.log('Killed orphan process(es) on port, waiting for port release...');
// Give more time for port to be released after force kill
await new Promise((resolve) => setTimeout(resolve, 1000));
}
// Verify port is actually free now
const serverUrl = `http://localhost:${port}`;
try {
await fetch(serverUrl);
// If we get here, something is still running on the port
logger.error('Port still in use after kill attempts');
sendMessage({
status: 'error',
message: `Port ${port} still in use after cleanup attempts. Please try again or use a different port.`
});
return;
} catch (e) {
// Good, nothing running - port is free
logger.log('Port is now free, starting server...');
}
// Start fresh with new config
await startServer(port, extraOrigins);
};
const handleMessage = (msg: any) => {
logger.log('Received message', msg);
try {
if (msg.type === 'ping') {
sendMessage({ type: 'pong', timestamp: Date.now() });
} else if (msg.type === 'start') {
const port = msg.port ? parseInt(msg.port, 10) : 4096;
const origins = Array.isArray(msg.origins) ? msg.origins : [];
startServer(port, origins);
} else if (msg.type === 'restart') {
// Restart the server with new origins - used when CORS configuration needs updating
const port = msg.port ? parseInt(msg.port, 10) : 4096;
const origins = Array.isArray(msg.origins) ? msg.origins : [];
restartServer(port, origins);
} else if (msg.type === 'stop') {
const port = msg.port ? parseInt(msg.port, 10) : 4096;
logger.log('Stop requested', { port, hasServerProc: !!serverProc });
// Kill by process reference if we have it
if (serverProc) {
logger.log('Killing server process by reference...');
serverProc.kill();
serverProc = null;
}
// Also kill any orphan process on the port (handles lost references)
const killed = killProcessOnPort(port, logger);
if (killed) {
logger.log('Killed orphan process(es) on port');
}
const restartServer = async (port: number = 4096, extraOrigins: string[] = []) => {
logger.log('Restart requested', { port, extraOrigins });
// Track if we had a server process before killing it
const hadServerProc = !!serverProc;
// Kill existing server process if we have a reference
if (serverProc) {
logger.log('Killing existing server process for restart...');
serverProc.kill();
serverProc = null;
// Give it a moment to release the port
await new Promise((resolve) => setTimeout(resolve, 500));
}
// Only try a port-level kill if we previously started the server.
if (!hadServerProc) {
sendMessage({
status: 'error',
message: `Port ${port} is in use by another process. Please stop it or choose a different port.`,
});
return;
}
// Kill any orphan process on the port (handles lost references)
logger.log('Checking for orphan processes on port...');
const killed = killProcessOnPort(port, logger);
if (killed) {
logger.log('Killed orphan process(es) on port, waiting for port release...');
// Give more time for port to be released after force kill
await new Promise((resolve) => setTimeout(resolve, 1000));
}
// Verify port is actually free now
const serverUrl = `http://localhost:${port}`;
try {
await fetch(serverUrl);
// If we get here, something is still running on the port
logger.error('Port still in use after kill attempts');
sendMessage({
status: 'error',
message: `Port ${port} still in use after cleanup attempts. Please try again or use a different port.`
});
return;
} catch (e) {
// Good, nothing running - port is free
logger.log('Port is now free, starting server...');
}
// Start fresh with new config
await startServer(port, extraOrigins);
};
🤖 Prompt for AI Agents
In `@packages/cli/src/commands/opencode/implementation.ts` around lines 499 - 571,
The code currently calls killProcessOnPort from restartServer and the stop
branch in handleMessage which can kill unrelated services; change the logic to
only terminate processes we started by tracking the child PID/handle returned by
startServer (use the existing serverProc variable and a persisted PID from
startServer) and avoid calling killProcessOnPort unless the PID matches our
tracked PID; if no tracked PID exists for the requested port, return/send an
error asking the user to pick a different port instead of force-killing. Update
restartServer, the 'stop' branch in handleMessage, and any startServer result
handling to set/clear the tracked PID/serverProc consistently and only kill that
PID when performing stop/restart. Also ensure restartServer waits for the
tracked process to exit before starting a new one and includes user-facing error
messages when refusing to kill unknown owners.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Valid concern. We need to ensure that a chrome extension that calls the native host can control the creation and recreation of the process.

So far we've seen that the extension can drift from the running process (maybe it was a mistake in the setup on my machine), and as a result restarting the connection could result in the native host respondibg with status running.

I guess the sokution would then just be to force the killing again from the extension side.

  • Analyze the issue and write the analysis into the "plans" directory

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧩 Analysis chain

@MihalyToth20, I'll analyze the native host process management issue and create a comprehensive document in the plans directory. Let me first check the existing structure.


🏁 Script executed:

#!/bin/bash
# Check the plans directory structure to understand naming conventions
ls -la plans/ 2>/dev/null || echo "Plans directory not found"

# Also check if there's any existing documentation about the native host
fd -t f "native" plans/ 2>/dev/null || echo "No native host related plans found"

Length of output: 985


🏁 Script executed:

#!/bin/bash
# Read the existing native host analysis to see if it covers the state drift issue
cat plans/opencode-native-host-analysis.md

Length of output: 12816


Perfect! I can see there's already an architecture analysis. Now I'll create a focused analysis document specifically about the state synchronization and process management issue you've identified.

Task: Create Native Host State Management Analysis

I'll create a comprehensive analysis document in the plans directory that covers:

  1. Problem Definition: State drift between Chrome extension and native host
  2. Root Causes: Process lifecycle tracking limitations
  3. Current Risks: Indiscriminate port-based process killing
  4. Proposed Solutions: PID tracking, state recovery, graceful handling
  5. Implementation Strategy: Changes needed in both native host and extension

I've created a comprehensive analysis document that covers:

  1. Problem Definition - Clear description of state drift and its impacts
  2. Current Architecture - Process hierarchy and state variables
  3. State Drift Scenarios - Four concrete scenarios showing how things go wrong
  4. Root Causes - Five fundamental issues including stateless host and port-based identification
  5. Current Risks - Categorized by severity (critical arbitrary process termination!)
  6. Proposed Solutions - Five detailed solutions:
    • Solution 1 (Recommended): PID tracking with persistence
    • Solution 2: Lock file with metadata
    • Solution 3: Health check protocol
    • Solution 4: State recovery on reconnect
    • Solution 5: Graceful port conflict handling
  7. Implementation Plan - 5 phases with concrete tasks and checkboxes
  8. Testing Strategy - Unit tests, integration tests, manual test scenarios
  9. Security Considerations - Vulnerabilities and mitigations
  10. Success Metrics - KPIs to measure improvement

The document emphasizes that Solution 1 (PID Tracking) should be implemented immediately to address the critical security risk of killing arbitrary processes.


✏️ Learnings added
Learnt from: MihalyToth20
Repo: calycode/xano-tools PR: 184
File: packages/cli/src/commands/opencode/implementation.ts:499-571
Timestamp: 2026-02-07T20:53:06.194Z
Learning: In the OpenCode native host implementation at `packages/cli/src/commands/opencode/implementation.ts`, the state synchronization between the Chrome extension and the spawned OpenCode server process can drift because the native host's `serverProc` variable is in-memory only and lost on restart, and the current `killProcessOnPort()` function kills ANY process on the port without verifying ownership. The recommended solution is to persist the server's PID to a file (`~/.calycode/opencode/server.pid`) and only kill processes with tracked PIDs, refusing to kill unknown processes with a clear error message asking the user to choose a different port.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


💡 For code changes across multiple files, use @coderabbitai plan in an issue comment

Comment on lines +5 to +20
/**
* Validates a path argument to prevent command injection.
* Ensures the path doesn't contain shell metacharacters.
* @param value - Path value to validate
* @param name - Name of the parameter for error messages
* @throws {Error} if path contains potentially dangerous characters
*/
function validatePathArg(value: string, name: string): void {
// Block shell metacharacters that could be used for command injection
// Allow alphanumeric, path separators, dots, hyphens, underscores, and spaces
if (/[;&|`$(){}[\]<>!#*?]/.test(value)) {
throw new Error(
`Invalid ${name}: "${value}". Path contains potentially unsafe characters.`
);
}
}
Copy link
Contributor

@coderabbitai coderabbitai bot Feb 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Fix Windows drive-letter paths in validatePathArg.
: is required in paths like C:\..., so current validation will throw on Windows. Consider stripping a leading drive prefix before validation (or allowing a single colon after the drive letter).

Suggested fix
 function validatePathArg(value: string, name: string): void {
-   // Block shell metacharacters that could be used for command injection
-   // Allow alphanumeric, path separators, dots, hyphens, underscores, and spaces
-   if (/[;&|`$(){}[\]<>!#*?]/.test(value)) {
+   // Block shell metacharacters that could be used for command injection
+   // Allow alphanumeric, path separators, dots, hyphens, underscores, and spaces
+   const isWindows = process.platform === 'win32';
+   const normalized = isWindows ? value.replace(/^[a-zA-Z]:/, '') : value;
+   if (/[;&|`$(){}[\]<>!#*?]/.test(normalized)) {
       throw new Error(
          `Invalid ${name}: "${value}". Path contains potentially unsafe characters.`
       );
    }
 }
🤖 Prompt for AI Agents
In `@packages/cli/src/commands/serve/implementation.ts` around lines 5 - 20, The
validatePathArg function currently rejects paths containing ':' which breaks
Windows drive-letter paths like "C:\..."; update validatePathArg to allow a
single colon immediately after a leading drive letter (e.g., /^[A-Za-z]:/) or
strip a leading drive prefix before running the existing metacharacter regex.
Target the validatePathArg function and ensure the logic only permits that
single leading drive colon (not arbitrary colons elsewhere) before applying the
existing unsafe-character test and throwing the same Error on failure.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • Reqrite path validation logic as requested in other comments as well.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Search for other instances of spawn usage that might need path validation
rg -n "spawn\(" --type=ts -C 3

echo "---"

# Search for other path validation or security validation functions
rg -n "validate.*[Pp]ath|[Pp]ath.*[Vv]alidat" --type=ts -C 2

echo "---"

# Search for other instances where user input might be passed to shell commands
rg -n "getSpawnOptions|shell.*true" --type=ts -C 3

Length of output: 21470


@MihalyToth20 Thank you for confirming! I've identified the path validation functions in the codebase that may need attention:

Shell Injection Prevention (Windows path issue)

  1. packages/cli/src/commands/serve/implementation.ts (current file)
    • validatePathArg() - Used for specHtmlPath and root parameters before passing to spawn
    • Issue: Blocks : which breaks Windows drive letters like C:\...

Path Traversal Prevention (different security concern)

These handle URL/filesystem traversal attacks and have different requirements:

  1. packages/cli/src/utils/github-content-fetcher.ts

    • validatePathWithinBase() - Ensures filesystem paths stay within base directory
  2. packages/cli/src/utils/feature-focused/registry/api.ts

    • validateRegistryPath() - Prevents .. in registry URL paths
  3. packages/core/src/features/registry/api.ts

    • validateRegistryPath() - Same as above, core package version

Other spawn Usage (may need validation)

  1. packages/cli/src/features/code-gen/open-api-generator.ts (line 60)
    • Uses spawn with shell: true and cliBin + args - check if user-provided paths need validation

The primary fix for the Windows drive letter issue should focus on validatePathArg() in the serve/implementation.ts file. Let me know if you'd like guidance on the specific regex pattern to use! 🐰


🧠 Learnings used
Learnt from: CR
Repo: calycode/xano-tools PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-28T05:59:33.267Z
Learning: Applies to packages/cli/src/index.ts : CLI entry point is packages/cli/src/index.ts (source) and dist/index.cjs (compiled distribution)

Comment on lines +131 to +133
const cacheDir = options.cacheDir || this.defaultCacheDir;
const cacheSubDir = path.join(cacheDir, `${owner}-${repo}`, subpath.replace(/\//g, '-'));

Copy link
Contributor

@coderabbitai coderabbitai bot Feb 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Prevent cache directory traversal via subpath (Windows backslashes).

Only / is replaced, so subpath containing \ or .. can escape cacheDir on Windows. Normalize separators and validate the final subdir is within the base cache directory.

🛡️ Proposed fix
-      const cacheSubDir = path.join(cacheDir, `${owner}-${repo}`, subpath.replace(/\//g, '-'));
+      const safeSubpath = subpath.replace(/[\\/]/g, '-');
+      const cacheSubDir = validatePathWithinBase(
+         cacheDir,
+         path.join(`${owner}-${repo}`, safeSubpath),
+      );
🤖 Prompt for AI Agents
In `@packages/cli/src/utils/github-content-fetcher.ts` around lines 131 - 133,
Normalize and sanitize the incoming subpath before building cacheSubDir: replace
backslashes with forward slashes (or use path.normalize), remove or collapse any
'..' segments, then use path.resolve/path.join to construct cacheSubDir from
cacheDir (or this.defaultCacheDir) and verify with path.relative or startsWith
that the resolved cacheSubDir is inside the base cacheDir; if the check fails,
reject the input or throw an error. Ensure you update the code around cacheDir,
cacheSubDir and subpath in github-content-fetcher.ts to perform these
normalization and validation steps so Windows backslashes or path-traversal
attempts cannot escape the base cache directory.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • Ensure this windows path traversal is handled

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@MihalyToth20, thanks for tracking this! The Windows path traversal issue is an important security concern that should be addressed.

When you're ready to implement the fix, the solution is straightforward:

  1. Replace both / and \ with dashes in the subpath
  2. Use the existing validatePathWithinBase helper to ensure the resulting path stays within the cache directory

Would you like me to generate the code changes for this fix, or do you prefer to implement it yourself?

Comment on lines +20 to +22
// Also block encoded traversal attempts
if (normalized.includes('%2e%2e') || normalized.includes('%2E%2E')) {
throw new Error(`Invalid registry path: "${inputPath}" contains encoded path traversal`);
Copy link
Contributor

@coderabbitai coderabbitai bot Feb 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Harden encoded traversal checks against mixed‑case encodings.

Percent‑encoding is case‑insensitive, so %2e%2E can bypass the current check. Normalize to lowercase before checking.

🛡️ Proposed fix
-   if (normalized.includes('%2e%2e') || normalized.includes('%2E%2E')) {
+   const normalizedLower = normalized.toLowerCase();
+   if (normalizedLower.includes('%2e%2e')) {
       throw new Error(`Invalid registry path: "${inputPath}" contains encoded path traversal`);
    }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Also block encoded traversal attempts
if (normalized.includes('%2e%2e') || normalized.includes('%2E%2E')) {
throw new Error(`Invalid registry path: "${inputPath}" contains encoded path traversal`);
// Also block encoded traversal attempts
const normalizedLower = normalized.toLowerCase();
if (normalizedLower.includes('%2e%2e')) {
throw new Error(`Invalid registry path: "${inputPath}" contains encoded path traversal`);
}
🤖 Prompt for AI Agents
In `@packages/core/src/features/registry/api.ts` around lines 20 - 22, The
encoded-traversal check in the registry API currently checks normalized for
'%2e%2e' and '%2E%2E' and can miss mixed-case encodings; update the logic around
the normalized variable (used alongside inputPath) to convert it to a single
case (e.g., normalized = normalized.toLowerCase()) before performing the
includes() check for '%2e%2e', or replace the check with a case-insensitive
regex; ensure the thrown Error still references inputPath for context.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • See the other tasks about traversal checks and refactor

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@MihalyToth20, sounds good! When you're ready to tackle the traversal check refactor across the codebase, I can help identify all the locations that need attention. Just let me know if you'd like me to search for other path validation or traversal check patterns, or if you'd prefer to create a tracking issue for this work.

---
description: Build XanoScript AI agents, MCP servers, and tools for AI-powered automation and intelligent features.
mode: subagent
model: anthropic/claude-sonnet-4-20250514
Copy link
Contributor

@coderabbitai coderabbitai bot Feb 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cat -n packages/opencode-templates/agents/xano-ai-builder.md

Repository: calycode/xano-tools

Length of output: 12265


🌐 Web query:

Anthropic Claude Sonnet 4 current model IDs 2025

💡 Result:

Claude Sonnet 4 (2025) model IDs

  • Anthropic API (snapshot / versioned ID): claude-sonnet-4-20250514 [1][2]
  • Anthropic API (alias): claude-sonnet-4-0 → points to claude-sonnet-4-20250514 [2]
  • AWS Bedrock model ID: anthropic.claude-sonnet-4-20250514-v1:0 [1][2]
  • Google Vertex AI model ID: claude-sonnet-4@20250514 [1][2]

How to confirm what’s currently available in your account: call GET /v1/models (it returns entries like id: "claude-sonnet-4-20250514" for “Claude Sonnet 4”). [3]

Sources
[1] Anthropic docs — Models overview / model names
[2] Anthropic docs — All models / aliases table
[3] Anthropic docs — List Models (GET /v1/models)


🌐 Web query:

OpenAI GPT-4o model ID reasoning_effort parameter 2025

💡 Result:

GPT‑4o model ID (API, 2025)

  • Use the stable alias: gpt-4o (recommended in OpenAI’s “which model should I use” FAQ). [1]
  • Snapshot IDs you’ll also see (esp. in fine-tuning / historical references) include:
    • gpt-4o-2024-05-13 [2]
    • gpt-4o-2024-08-06 [2][3]

reasoning_effort parameter (and whether it applies to GPT‑4o)

  • reasoning_effort is a parameter intended for reasoning models; supported values are none, minimal, low, medium, high, xhigh (defaults to medium in that API reference). [4]
  • GPT‑4o is a non‑reasoning (“legacy/frontier”) model; OpenAI’s guidance indicates the “reasoning effort” control is not for GPT‑4o (it’s called out as a GPT‑5-specific control as reasoning.effort, and “legacy models do not support” those controls). [5]

Practical takeaway: for GPT‑4o, set model: "gpt-4o" (or a snapshot like gpt-4o-2024-08-06) and control output with token limits (e.g., max_output_tokens / max_completion_tokens) rather than reasoning_effort. [1][5]

Sources

[1] OpenAI API FAQ – “Which model should I use?” (gpt-4o) (platform.openai.com)
[2] GPT‑4o & GPT‑4 fine-tuning page (mentions gpt-4o-2024-05-13, gpt-4o-2024-08-06) (openai.com)
[3] “Fine-tuning now available for GPT‑4o” (select gpt-4o-2024-08-06) (openai.com)
[4] API reference showing reasoning_effort values/default (platform.openai.com)
[5] Help Center: “reasoning.effort” controls only on GPT‑5; legacy models don’t support (help.openai.com)

Citations:


🌐 Web query:

Google Gemini thinking_tokens include_thoughts search_grounding parameters 2025

💡 Result:

“thinking_tokens” / thinking_tokens

There isn’t a request parameter literally named thinking_tokens in the Gemini API. The count of thinking tokens used is reported in the response as usageMetadata.thoughtsTokenCount (often what people mean by “thinking tokens”). [1]

include_thoughts / includeThoughts

To return thought summaries (not raw chain-of-thought), enable:

  • SDK (snake_case): thinking_config=ThinkingConfig(include_thoughts=True) [2]
  • JSON / JS (camelCase): "thinkingConfig": {"includeThoughts": true} [2]

The thought summary then appears as a normal content part with a boolean like part.thought == true. [2]

Thinking controls (2025+)

  • Gemini 2.5: use thinkingBudget (and you can set 0 to disable for models that support it; -1 enables “dynamic thinking”). [3]
  • Gemini 3: use thinkingLevel (recommended for Gemini 3+). [4]

“search_grounding” / grounding with Google Search

There isn’t a request parameter literally named search_grounding. To ground with Google Search you enable the Google Search tool:

  • REST JSON:
    "tools": [{"google_search": {}}] [5]
  • SDK: tools=[Tool(google_search=GoogleSearch())] [5]

Grounded responses include groundingMetadata (queries used + source chunks/supports for citations). [6]

Sources:
[1] Vertex AI “Thinking” docs (usageMetadata.thoughtsTokenCount) (docs.cloud.google.com)
[2] Gemini API “Thinking” docs (includeThoughts / thought parts) (ai.google.dev)
[3] Gemini API “Thinking” docs (thinkingBudget, ranges, 0 / -1 behavior) (ai.google.dev)
[4] Gemini API “Thinking” docs (thinkingLevel for Gemini 3) (ai.google.dev)
[5] Gemini API “Grounding with Google Search” docs (tools: google_search) (ai.google.dev)
[6] Gemini API “Grounding with Google Search” docs (groundingMetadata) (ai.google.dev)

Citations:


🌐 Web query:

Anthropic Claude Sonnet 4.5 model ID claude-sonnet-4-5-20250929

💡 Result:

claude-sonnet-4-5-20250929 is the version-pinned Anthropic API model ID for Claude Sonnet 4.5, corresponding to the Sep 29, 2025 Sonnet 4.5 release date. [1][2]

If you want the rolling “latest Sonnet 4.5” instead of a pinned version, Anthropic’s public guidance is to use the alias claude-sonnet-4-5. [1]

This pinned ID also appears as the model identifier in third-party distributions (e.g., Microsoft Foundry’s catalog/examples). [3]

Sources
[1] Anthropic model page (Sonnet 4.5)
[2] Anthropic announcement (Sep 29, 2025)
[3] Microsoft Foundry model catalog page for Claude Sonnet 4.5


Fix provider parameter names and configurations—several are unsupported or incorrect.

The documentation includes invalid parameter names and configurations that contradict official provider APIs:

  1. OpenAI (lines 101–102): reasoning_effort is not supported for gpt-4o. This parameter is only for reasoning models (e.g., o1). Remove it or clarify that it applies only to reasoning-capable models.

  2. Gemini (lines 78, 86): thinking_tokens is not a valid parameter name. Use thinkingBudget (for Gemini 2.5) or thinkingLevel (for Gemini 3+). The documented range (0-24576, -1 for dynamic) does not match the API spec.

  3. Gemini (line 80, 63): search_grounding is not a valid parameter. Enable grounding by adding google_search to the tools list instead.

  4. Gemini (line 79): include_thoughts is correct; keep it as-is.

The Anthropic and core model identifiers are correct.

Also applies to: 69–81, 84–89, 92–104, 106–110, 113–123, 126–128

🤖 Prompt for AI Agents
In `@packages/opencode-templates/agents/xano-ai-builder.md` at line 4, Update the
provider parameter names and constraints in xano-ai-builder.md: remove or
restrict reasoning_effort where used with gpt-4o (only allow it for reasoning
models like o1 or delete the param for gpt-4o references), replace any
thinking_tokens occurrences with thinkingBudget for Gemini 2.5 or thinkingLevel
for Gemini 3+ and adjust documented ranges to the provider spec, remove
search_grounding and instead document adding google_search to the tools list to
enable grounding, and keep include_thoughts as-is; ensure model identifiers
(e.g., anthropic/claude-sonnet-4-20250514) remain unchanged.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • Remove models from the agebts and commands markdown files. Let's allow the user what model they want to use.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find all markdown files in the agents and commands directories
echo "=== Markdown files in agents/ ==="
fd -e md . packages/opencode-templates/agents/

echo -e "\n=== Markdown files in commands/ ==="
fd -e md . packages/opencode-templates/commands/

echo -e "\n=== Searching for 'model:' in YAML frontmatter ==="
# Search for model specifications in YAML frontmatter
rg -n "^model:" packages/opencode-templates/agents/ packages/opencode-templates/commands/ --type md -C 2

Length of output: 6127


🏁 Script executed:

#!/bin/bash
# Double-check commands directory for any model specifications
echo "=== Checking commands directory for any 'model' occurrences ==="
rg -i "model" packages/opencode-templates/commands/ --type md -C 1

echo -e "\n=== Also checking for any provider/model config patterns ==="
rg -i "(anthropic|openai|claude|gpt-)" packages/opencode-templates/commands/ --type md -C 1

Length of output: 125


@MihalyToth20, I'll remove the hardcoded model specifications from all agent markdown files so users can choose their preferred models.

Task: Remove hardcoded model specifications from agent markdown files

  • Remove model: line from YAML frontmatter in all 10 agent files
  • Commands directory is clean (no model specifications found)
  • This allows users to configure their preferred model at runtime

💡 For code changes across multiple files, use @coderabbitai plan in an issue comment

Comment on lines +98 to +113
```xanoscript
// At start of endpoint
now() as $start_time

// ... endpoint logic ...

// At end of endpoint
now() - $start_time as $duration_ms

db.add performance_log {
endpoint = $request.path,
method = $request.method,
response_time_ms = $duration_ms,
query_count = $query_count
}
```
Copy link
Contributor

@coderabbitai coderabbitai bot Feb 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🌐 Web query:

XanoScript syntax documentation Xano functions now() db.add variable assignment

💡 Result:

Variable assignment (Create Variable) in XanoScript

Use the var statement and assign with value = ... (variables typically start with $). (docs.xano.com)

var $total {
  value = $input.quantity * $input.price_per_item
}

db.add (insert a record)

db.add <table_name> { data = { ... } } as <var> inserts a row and stores the inserted record in a variable. (docs.xano.com)

db.add user {
  data = {
    created_at: "now",
    name: $input.name,
    email: $input.email
  }
} as $newUser

“now()” / current time in XanoScript

In XanoScript examples, the “current time” is represented as the literal "now" (commonly used for timestamp fields), and you can pipe it through timestamp filters (example shown subtracting months). (docs.xano.com)

var $cutoff {
  value = ("now"|timestamp_subtract_months:1)
}

(Also useful context: Xano stores timestamps as Unix epoch milliseconds.) (docs.xano.com)

Citations:


🌐 Web query:

Xano database operations db.add db.raw http.post json_encode

💡 Result:

In Xano, the equivalents of the things you listed are:

db.add (insert a row)

Use Database Operations → Add Record / XanoScript db.add to insert a new record by passing a data object. (docs.xano.com)

db.add user {
  data = {
    created_at: "now",
    name: $input.name,
    email: $input.email
  }
} as $new_user

db.raw (raw SQL)

Xano doesn’t expose a db.raw primitive like many code frameworks do; the “raw SQL” feature in Xano is Direct Database Query (visual builder) / XanoScript db.direct_query, where you provide SQL and (optionally) ? placeholders with statement arguments. (docs.xano.com)

db.direct_query {
  sql = "SELECT * FROM x52_245 WHERE id = ?"
  response_type = "single"
  arg = [$input.id]
} as $row

http.post (HTTP POST)

In Xano, outbound HTTP calls are done with External API Request (method = POST, plus headers + params/body). (docs.xano.com)

json_encode

Xano has a json_encode conversion filter that turns a value (object/array) into JSON text (string). (docs.xano.com)

When to use json_encode with an HTTP POST

  • If the API request builder is sending a structured JSON body already, you often just provide an object in params/body.
  • If the API specifically requires a raw JSON string body, encode your object with json_encode and set Content-Type: application/json. (Xano’s External API Request supports configuring headers and body/params.) (docs.xano.com)

If you tell me whether you’re using XanoScript or the visual function stack, and what the target API expects (example request body + headers), I can show the exact function stack steps / code.

Citations:


🌐 Web query:

XanoScript try catch error handling log function

💡 Result:

In XanoScript, wrap anything that might fail inside try_catch, and use debug.log inside catch to record the error without stopping execution (unless you re-throw).

try_catch {
  try {
    function.run risky_function { input = { foo: "bar" } } as $result
  }
  catch {
    debug.log { value = $error }   // log the error object
    // optionally re-throw a cleaner error to the caller:
    // throw { name = "inputerror" value = "Something went wrong" }
  }
  finally {
    debug.log { value = "cleanup" }
  }
}
  • debug.log { value = ... } writes to the Debug Logs tab when using Run & Debug (it doesn’t affect execution). [1]
  • try_catch runs catch only if try errors; finally always runs (optional). [1]
  • In the visual Function Stack Try/Catch, the catch block exposes error details via code, message, and result. [2]
  • To intentionally fail (and optionally be caught by an outer try_catch), use throw { name = "...", value = "..." }. [1]

Sources: XanoScript Utility Functions (Try/Catch, Debug Log, Throw) [1]; Function Stack Utility Functions (Try/Catch error vars) [2].


🏁 Script executed:

cat -n packages/xano-skills/skills/xano-monitoring/SKILL.md | sed -n '98,113p'

Repository: calycode/xano-tools

Length of output: 473


🏁 Script executed:

cat -n packages/xano-skills/skills/xano-monitoring/SKILL.md | sed -n '145,167p'

Repository: calycode/xano-tools

Length of output: 725


🏁 Script executed:

cat -n packages/xano-skills/skills/xano-monitoring/SKILL.md | sed -n '298,310p'

Repository: calycode/xano-tools

Length of output: 623


🏁 Script executed:

cat -n packages/xano-skills/skills/xano-monitoring/SKILL.md | sed -n '314,328p'

Repository: calycode/xano-tools

Length of output: 503


🏁 Script executed:

cat -n packages/xano-skills/skills/xano-monitoring/SKILL.md | sed -n '352,359p'

Repository: calycode/xano-tools

Length of output: 279


Correct multiple XanoScript syntax errors across all code examples.

The examples contain several syntax errors that conflict with official Xano documentation:

  • Lines 98–113 & other blocks: now() is not a function—use the string literal "now" for timestamps. db.add requires data = { ... } wrapper: db.add performance_log { data = { endpoint: $request.path, ... } } as $result
  • Line 149: try/catch must wrap inside try_catch { try { ... } catch { ... } }. The catch block receives $error with message, code, and stack properties.
  • Lines 298–310: db.raw does not exist; use db.direct_query { sql = "...", arg = [...] } for raw SQL. http.post does not exist; use the External API Request function (visual builder) or equivalent XanoScript.
  • Lines 352–359: log() function does not exist; use debug.log { value = ... } for debug output.

All code blocks need corrections to match official Xano function signatures and XanoScript syntax.

🤖 Prompt for AI Agents
In `@packages/xano-skills/skills/xano-monitoring/SKILL.md` around lines 98 - 113,
Replace invalid XanoScript usages across the file: change any now() calls to the
"now" timestamp literal; update db.add usage such as db.add performance_log {
... } to include the required wrapper db.add performance_log { data = {
endpoint: $request.path, method = $request.method, response_time_ms =
$duration_ms, query_count = $query_count } } as $result; convert try/catch
blocks to the Xano try_catch form try_catch { try { ... } catch { /* catch
receives $error with message, code, stack */ } }; replace db.raw calls with
db.direct_query { sql = "...", arg = [...] } and replace http.post usages with
the External API Request equivalent from Xano’s visual builder; and change any
log(...) calls to debug.log { value = ... } so all examples use correct Xano
function names and argument shapes.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • Fix the xanoscript syntax errors

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@MihalyToth20 Thank you for acknowledging this! If you need any clarification on the correct XanoScript syntax while making the fixes, feel free to ask. The key corrections needed are:

  1. now()"now" — timestamps use string literals
  2. db.add requires data wrapperdb.add table_name { data = { ... } }
  3. try/catchtry_catch { try { ... } catch { ... } } — use the structured form
  4. db.rawdb.direct_query — with sql and arg parameters
  5. http.post → External API Request — use Xano's visual builder equivalent
  6. log()debug.log { value = ... } — correct function name

Let me know if you'd like me to provide corrected code examples for any specific section!

* Initial plan

* fix: address critical and major PR review comments

- Fix path traversal by using fetchRegistryFileContent from api.ts
- Fix file.path undefined error with explicit validation
- Fix results structure to match InstallResults type with component field
- Fix error handling to properly detect error instances
- Validate required IDs for trigger install URLs
- Fix apiGroupId type to string | number for consistency
- Fix getApiGroupByName to return created group
- Guard against empty registry items in promptForComponents
- Fix Jest config to preserve base ignore patterns
- Update AGENTS.md to reflect test files exist
- Fix treat empty inline content as valid with null check
- Fix registryItem.id reference error

Co-authored-by: MihalyToth20 <34799518+MihalyToth20@users.noreply.github.com>

* fix: align registry URL mappings with CLI and fix test

- Update registry URL map to match CLI paths (workflow_test, mcp_server, realtime/channel, branch params)
- Fix test to use 'error' field instead of 'reason' for skipped results
- Add Jest config to ignore dist directory in core tests

Co-authored-by: MihalyToth20 <34799518+MihalyToth20@users.noreply.github.com>

* fix: codegen, name sanitization

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: MihalyToth20 <34799518+MihalyToth20@users.noreply.github.com>
Co-authored-by: Mihály Tóth <t.mihaly@icloud.com>
@MihalyToth20 MihalyToth20 merged commit 68e8bc4 into main Feb 8, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants