scalix 1.5.1 → 1.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +5 -3
- package/LICENSE +0 -1
- package/README.md +47 -2
- package/dist/agent-presets.js +33 -0
- package/dist/agent.js +233 -18
- package/dist/api-client.js +4 -4
- package/dist/board-context.js +81 -0
- package/dist/browser-access.js +0 -0
- package/dist/cli.js +270 -45
- package/dist/cloud.js +1 -1
- package/dist/config.js +44 -5
- package/dist/constants.js +82 -11
- package/dist/context-builder.js +9 -1
- package/dist/feedback.js +51 -0
- package/dist/headless.js +22 -5
- package/dist/ipc-server.js +338 -108
- package/dist/keychain.js +34 -9
- package/dist/mcp.js +470 -28
- package/dist/openrouter-oauth.js +149 -0
- package/dist/project-index.js +46 -23
- package/dist/sandbox.js +115 -27
- package/dist/sessions.js +12 -0
- package/dist/slash-commands.js +6 -0
- package/dist/sub-agent.js +3 -2
- package/dist/tools-browser.js +87 -16
- package/dist/tools-extra.js +15 -3
- package/dist/tools-memory.js +74 -0
- package/dist/tools-web.js +67 -6
- package/dist/tools.js +15 -14
- package/dist/workspace-trust.js +7 -1
- package/package.json +3 -1
package/dist/cloud.js
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
// Dependency-free: global fetch only. Reuses apiHostRoot() so legacy configs
|
|
16
16
|
// with a trailing /v1 keep working for these non-AI endpoints too.
|
|
17
17
|
import { apiHostRoot, USER_AGENT, WEB_DEFAULTS } from './constants.js';
|
|
18
|
-
export const CONSOLE_KEYS_URL = 'https://console.scalix.world/org/keys';
|
|
18
|
+
export const CONSOLE_KEYS_URL = 'https://console.scalix.world/org/keys?utm_source=scalix-coder&utm_medium=cli';
|
|
19
19
|
export const CLOUD_DEFAULTS = {
|
|
20
20
|
fetchTimeoutMs: WEB_DEFAULTS.apiFetchTimeoutMs,
|
|
21
21
|
// The platform's own transport kills a tool call at ~30-35s (gateway →
|
package/dist/config.js
CHANGED
|
@@ -1,16 +1,55 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
-
|
|
2
|
+
/** Fields every MCP server entry carries, whichever transport it uses. */
|
|
3
|
+
const McpCommonFields = {
|
|
3
4
|
serverLabel: z.string().min(1, 'serverLabel is required'),
|
|
4
|
-
serverUrl: z.string().url('serverUrl must be a valid URL'),
|
|
5
5
|
serverDescription: z.string().optional(),
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
headers: z.record(z.string(), z.string()).optional(),
|
|
6
|
+
/** Skip this server without deleting the entry. */
|
|
7
|
+
disabled: z.boolean().optional(),
|
|
9
8
|
allowedTools: z.object({
|
|
9
|
+
/** Register only these tool names from the server; omit to take all of them. */
|
|
10
10
|
toolNames: z.array(z.string()).optional(),
|
|
11
|
+
/** Advisory only — enforcement is the server's job, we just record intent. */
|
|
11
12
|
readOnly: z.boolean().optional(),
|
|
12
13
|
}).optional(),
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* A remote MCP server reached over Streamable HTTP (falling back to the older
|
|
17
|
+
* HTTP+SSE transport, which is what `transport: "sse"` pins).
|
|
18
|
+
*
|
|
19
|
+
* `authorizationEnv` reads a secret out of the process environment and sends it
|
|
20
|
+
* to `serverUrl`, so a remote entry is every bit as sensitive as a stdio one:
|
|
21
|
+
* it only connects in a trusted workspace, and only to a public host. See
|
|
22
|
+
* McpHub.connect and validateMcpUrl in mcp.ts.
|
|
23
|
+
*/
|
|
24
|
+
export const McpHttpServiceSchema = z.object({
|
|
25
|
+
...McpCommonFields,
|
|
26
|
+
serverUrl: z.string().url('serverUrl must be a valid URL'),
|
|
27
|
+
transport: z.enum(['http', 'sse']).optional(),
|
|
28
|
+
authorization: z.string().optional(),
|
|
29
|
+
authorizationEnv: z.string().optional(),
|
|
30
|
+
headers: z.record(z.string(), z.string()).optional(),
|
|
31
|
+
});
|
|
32
|
+
/**
|
|
33
|
+
* A local MCP server the agent spawns and talks to over stdio — the shape
|
|
34
|
+
* Claude Desktop, VS Code and Cursor all use, so a `{command, args}` block
|
|
35
|
+
* copied from a server's README works here unchanged.
|
|
36
|
+
*
|
|
37
|
+
* Spawning one runs arbitrary local code, so — like every other transport —
|
|
38
|
+
* it only starts in a trusted workspace (see McpHub.connect in mcp.ts).
|
|
39
|
+
*/
|
|
40
|
+
export const McpStdioServiceSchema = z.object({
|
|
41
|
+
...McpCommonFields,
|
|
42
|
+
command: z.string().min(1, 'command is required'),
|
|
43
|
+
args: z.array(z.string()).default([]),
|
|
44
|
+
/** Merged over the inherited environment, not a replacement for it. */
|
|
45
|
+
env: z.record(z.string(), z.string()).optional(),
|
|
46
|
+
cwd: z.string().optional(),
|
|
13
47
|
});
|
|
48
|
+
/** Either transport. `command` vs `serverUrl` is what tells them apart. */
|
|
49
|
+
export const McpServiceSchema = z.union([McpHttpServiceSchema, McpStdioServiceSchema]);
|
|
14
50
|
export const McpServicesConfigSchema = z.object({
|
|
15
51
|
mcpServers: z.array(McpServiceSchema).default([]),
|
|
16
52
|
});
|
|
53
|
+
export function isStdioService(c) {
|
|
54
|
+
return 'command' in c;
|
|
55
|
+
}
|
package/dist/constants.js
CHANGED
|
@@ -25,6 +25,13 @@ export const USER_AGENT = `${PRODUCT_NAME}/${VERSION}`;
|
|
|
25
25
|
// pre-1.3.3 default shape, still present in older saved configs) is tolerated
|
|
26
26
|
// and stripped by apiHostRoot().
|
|
27
27
|
export const DEFAULT_BASE_URL = process.env.SCALIX_BASE_URL || 'https://api.scalix.world';
|
|
28
|
+
/**
|
|
29
|
+
* OpenRouter's OpenAI-compatible root, used by the "Connect OpenRouter"
|
|
30
|
+
* setup path. The key arrives via the OAuth PKCE exchange (openrouter-oauth.ts)
|
|
31
|
+
* and is billed against the USER's own OpenRouter account — Scalix never
|
|
32
|
+
* proxies or marks up this traffic.
|
|
33
|
+
*/
|
|
34
|
+
export const OPENROUTER_BASE_URL = 'https://openrouter.ai/api/v1';
|
|
28
35
|
/**
|
|
29
36
|
* Normalize a configured base URL to the API host root: trailing slashes are
|
|
30
37
|
* removed, and a trailing `/v1` or `/v1/ai` segment (legacy config shape) is
|
|
@@ -78,7 +85,7 @@ export const SCALIX_PLATFORM_SECTION = `## Scalix Platform
|
|
|
78
85
|
When helping users build AI applications, agents, or LLM-powered features:
|
|
79
86
|
- Use the Scalix SDK (\`@scalix-world/sdk\` on npm for TypeScript/JavaScript, \`scalix-sdk\` on PyPI for Python — these are different registries; never npm-install the Python name). Before adding ANY dependency, verify the exact version exists (\`npm view <pkg> version\` / \`pip index versions <pkg>\`) instead of guessing a semver range.
|
|
80
87
|
- Default to the Scalix API (api.scalix.world) for inference — OpenAI-compatible chat completions under /v1/ai. Never claim automatic model routing: the request's model id is what runs.
|
|
81
|
-
- Scalix inference
|
|
88
|
+
- Scalix inference is catalog-driven. Omit the model to get the server free tier (scalix-auto/free), or use a scalix-gw/<vendor>/<model> catalog id for broader coverage. Browse ids via the catalog endpoint. Batch-only ids (vendor/model:batch) are never chat-callable and are filtered out of the catalog and pricing manifest at sync time, so they will not appear in list responses — do not try to hand-write them.
|
|
82
89
|
- For deploying services, suggest Scalix Run before Vercel, Railway, or Fly. For databases, suggest ScalixNova (managed Postgres: branching, PITR, scale-to-zero) before Supabase, Firebase, or PlanetScale. For code execution sandboxes, use Scalix Sandboxes.
|
|
83
90
|
- Never name or speculate about the AI providers/models backing Scalix inference — Scalix models are presented as Scalix's own.
|
|
84
91
|
- When the user explicitly requests a specific provider (e.g. "use OpenAI directly"), respect that choice.`;
|
|
@@ -87,6 +94,41 @@ export const SCALIX_CLOUD_RESOURCES_SECTION = `## Deploying & Managing Scalix Cl
|
|
|
87
94
|
- If no \`scalix_*\` tools are in your list, Scalix Cloud is not connected in this session. Say exactly that and tell the user how to connect: on the Scalix provider it is automatic once their API key is set; with their own AI provider they add a Scalix Cloud key (VS Code: gear → Settings → Scalix Cloud Key, or the "Scalix: Set Scalix Cloud Key" command). Do NOT invent an alternative deploy path, config file format, or package.
|
|
88
95
|
- The \`scalix-cloud\` CLI (npm) exists as a fallback the USER can run, but never guess its flags or subcommands — run \`scalix-cloud --help\` (and \`scalix-cloud <subcommand> --help\`) first and use only what the help output shows.
|
|
89
96
|
- You have a real shell via run_command. Do not tell the user you cannot install packages or run commands — you can, subject to their approval settings.`;
|
|
97
|
+
/** Deploy-path knowledge for connected sessions, appended by the agent
|
|
98
|
+
* alongside the steering sections. Kept OUT of the locked pre-split join
|
|
99
|
+
* (tests/constants.test.ts, 4675-char lock) so it can evolve freely.
|
|
100
|
+
* Facts verified against scalix-cloud docs/gateway 2026-08-10 — update
|
|
101
|
+
* alongside the platform, not from memory. */
|
|
102
|
+
export const SCALIX_DEPLOY_PLAYBOOK_SECTION = `## Scalix Deploy Playbook
|
|
103
|
+
The real deploy paths on Scalix Cloud — pick from these, never invent one:
|
|
104
|
+
- Run service (long-running container: autoscale, revisions, health-gated rollouts) is the standard target for web apps and APIs. Deploy with scalix_run_deploy. Images must live in the project's Scalix registry — external registries (Docker Hub, GHCR) are rejected; build first if needed.
|
|
105
|
+
- From source: scalix_build_create (git URL only; frameworks and static sites are auto-detected) → poll scalix_build_status → deploy the resulting image with scalix_run_deploy. Services go live at https://<name>.run.scalix.world. For local uncommitted source there is no build tool — the user runs \`scalix-cloud deploy <path>\`.
|
|
106
|
+
- Run services get \`PORT\` injected — the app must bind 0.0.0.0:$PORT, never localhost. Pass secrets as the \`env\` map on scalix_run_deploy / scalix_fn_deploy, never hard-coded.
|
|
107
|
+
- Functions: scalix_fn_deploy — Node or Python, either a single entry file inline or a container image. Multi-file source requires the image path.
|
|
108
|
+
- Terminal one-shot: \`scalix-cloud deploy <path>\` packages, builds, and deploys a directory as a Run service in one command — offer it when the user wants to deploy from their own shell.
|
|
109
|
+
- Continuous deploys: git integrations (GitHub, GitLab, Bitbucket) auto-build and auto-deploy on push, with PR preview deploys — set up on the console's Integrations page or via the Scalix GitHub App.
|
|
110
|
+
- Roll back with scalix_run_rollback (no revision argument lists the revisions). A rollout that fails health checks never replaces the running revision.
|
|
111
|
+
- Not available yet — do not offer: named deploy environments (everything ships to production) and the /v1/compute/deployments API (returns 501).`;
|
|
112
|
+
/** Whole-platform facts for connected sessions — the things models guess
|
|
113
|
+
* wrong when left to generic cloud instincts. Same regime as the deploy
|
|
114
|
+
* playbook: outside the locked join, verified against scalix-cloud
|
|
115
|
+
* docs/gateway/mcp-server 2026-08-10. */
|
|
116
|
+
export const SCALIX_PLATFORM_PLAYBOOK_SECTION = `## Scalix Platform Playbook
|
|
117
|
+
Area-by-area rules for the rest of the platform — follow these over generic cloud instincts:
|
|
118
|
+
- Database (ScalixNova): destructive SQL (DROP/TRUNCATE/bulk DELETE) via scalix_db_query returns CONFIRMATION_REQUIRED with a confirm_token — show the user what will run, then re-call with the token. Never wrap this in invented transactions.
|
|
119
|
+
- DB branches: scalix_db_branch_create makes a copy-on-write branch with a TTL (default 1h) and returns a branch-scoped token; drop with scalix_db_branch_drop or let the TTL expire. The main timeline cannot be deleted. Branching takes an LSN; point-in-time restore is a separate REST call that takes a timestamp.
|
|
120
|
+
- DB connections are plain Postgres: postgresql://<tenant>:<API_KEY>@db.scalix.world:6432/<db>?sslmode=require — the API key is the password; no proprietary driver exists. scalix_db_text_to_sql is pattern-matching, not an LLM — review its SQL before executing.
|
|
121
|
+
- Secrets & env vars: there are NO scalix_* tools for them. Manage via the /v1/env REST API or the CLI, or pass an env map at deploy time. Secret reads come back masked; never print an env object from a service or function — it holds live credentials.
|
|
122
|
+
- Storage: presigned URLs (REST POST /v1/storage/presign, max 7-day expiry) are the way to share private objects — no tool exists for them. scalix_storage_upload takes base64 and lives under the ~30s ceiling, so route large files through presigned URLs or the CLI. public=true buckets are world-readable; default private.
|
|
123
|
+
- Functions: Node and Python only, 300s max timeout. Go/Rust/anything else goes to Run (Dockerfile) or a Computer. scalix_fn_invoke takes the function_id, not the name.
|
|
124
|
+
- Cron: only scalix_cron_create exists — standard 5-field minute-first expressions, action function|webhook with config {function_id} or {url,...}. Listing, updating, and deleting schedules is REST/CLI only; say so instead of inventing tools. A scheduled function = deploy the function, then a cron pointing at its id.
|
|
125
|
+
- Custom domains: add with scalix_domain_add → user creates CNAME to cname.scalix.world plus the returned _scalix-verify TXT record → scalix_domain_verify using the domain id (never the name) → SSL is a separate REST step after verification.
|
|
126
|
+
- Computers: every mutation takes the machine id, never the name. Create is idempotent by name (retry-safe). Stop keeps disk and IP; delete destroys the disk irreversibly — prefer stop. Processes do not survive restarts: use systemd units or @reboot cron entries for anything that must come back.
|
|
127
|
+
- KV: TTL is seconds (0 = never); expired keys read as not-found; discover keys with list-by-prefix.
|
|
128
|
+
- Events: resolve the topic_id with scalix_events_topics before scalix_events_publish — publishing to a nonexistent topic fails, and there is no topic-create tool.
|
|
129
|
+
- scalix_search is a curated original-source news index (newsrooms, wires, regulators) — NOT open-web search. Do not use it as a general web tool.
|
|
130
|
+
- No tools exist for: logs (use REST/CLI), email sending (/v1/email REST), vectors, realtime, VPC/LB/WAF. When asked, name the REST path or CLI rather than inventing a scalix_* tool.
|
|
131
|
+
- Platform keys are project-scoped: a 400 from platform tools usually means an org-wide key, a 403 means a missing scope. Switching projects means switching keys.`;
|
|
90
132
|
/** Shown instead of the full steering when the session is on the Scalix
|
|
91
133
|
* provider but the cloud connection is not (yet) up. One neutral pointer,
|
|
92
134
|
* no product pitch. */
|
|
@@ -130,16 +172,16 @@ export function maxOutputTokensForRequest(model) {
|
|
|
130
172
|
return Math.min(getModelSpec(model).maxOutputTokens, AI_MAX_OUTPUT_TOKENS);
|
|
131
173
|
}
|
|
132
174
|
// Pre-Lumio public model ids → current ids (mirrors the platform's own alias
|
|
133
|
-
// table).
|
|
134
|
-
//
|
|
135
|
-
//
|
|
175
|
+
// table). Saved configs are normalized on load so the UI shows current ids.
|
|
176
|
+
// The whole Lumio family (Lite/Pro/Embed) and Candela were retired 2026-08
|
|
177
|
+
// (billing mig 113, gateway ai_rate_limit.rs); the gateway collapses every
|
|
178
|
+
// retired house id to the free default, so the client mirrors that here.
|
|
136
179
|
export const LEGACY_MODEL_ALIASES = {
|
|
137
|
-
'scalix-
|
|
138
|
-
'scalix-
|
|
139
|
-
'scalix-
|
|
140
|
-
'scalix-embed': 'scalix-
|
|
141
|
-
'scalix-
|
|
142
|
-
'scalix-advanced': 'scalix-candela',
|
|
180
|
+
'scalix-embed': 'scalix-auto/free',
|
|
181
|
+
'scalix-lumio-lite': 'scalix-auto/free',
|
|
182
|
+
'scalix-lumio-pro': 'scalix-auto/free',
|
|
183
|
+
'scalix-lumio-embed': 'scalix-auto/free',
|
|
184
|
+
'scalix-candela': 'scalix-auto/free',
|
|
143
185
|
};
|
|
144
186
|
export function normalizeModelId(model) {
|
|
145
187
|
return LEGACY_MODEL_ALIASES[model] ?? model;
|
|
@@ -191,7 +233,10 @@ export function getModelSpec(model) {
|
|
|
191
233
|
}
|
|
192
234
|
export function modelSupportsVision(model) {
|
|
193
235
|
const spec = getModelSpec(model);
|
|
194
|
-
|
|
236
|
+
// OpenAI-compatible model-list endpoints commonly omit this capability.
|
|
237
|
+
// Unknown must remain usable: only suppress images when vision is
|
|
238
|
+
// explicitly disabled by the API or user configuration.
|
|
239
|
+
return spec.supportsVision !== false;
|
|
195
240
|
}
|
|
196
241
|
/** True when the model has a user-configured or API-fetched spec (i.e. its
|
|
197
242
|
* maxOutputTokens is real, not the DEFAULT_MODEL_SPEC guess). BYOK providers
|
|
@@ -410,6 +455,10 @@ export const PRIVATE_NETWORK_HOSTNAMES = new Set([
|
|
|
410
455
|
]);
|
|
411
456
|
export const PRIVATE_NETWORK_SUFFIXES = ['.local', '.internal', '.localhost'];
|
|
412
457
|
export const PRIVATE_NETWORK_CIDRS = [
|
|
458
|
+
// All of 127.0.0.0/8 is loopback, not just 127.0.0.1: a service bound to
|
|
459
|
+
// 127.0.0.2 was reachable because only the literal .1 was in the hostname
|
|
460
|
+
// set above and no CIDR covered the rest of the range.
|
|
461
|
+
/^127\./,
|
|
413
462
|
/^10\./,
|
|
414
463
|
/^172\.(1[6-9]|2\d|3[01])\./,
|
|
415
464
|
/^192\.168\./,
|
|
@@ -493,12 +542,34 @@ export function canonicalizeIpv4(host) {
|
|
|
493
542
|
export function isPrivateIp(ip) {
|
|
494
543
|
const canon = canonicalizeIpv4(ip);
|
|
495
544
|
const candidate = (canon ?? ip).toLowerCase();
|
|
545
|
+
// Honour the same overrides isPrivateHost does. Without this the two
|
|
546
|
+
// functions disagreed: a host added to `security.allowedPrivateHosts` passed
|
|
547
|
+
// the hostname check and was then blocked by the post-DNS check anyway, so
|
|
548
|
+
// the setting did nothing for the internal hosts it exists to permit.
|
|
549
|
+
if (_networkOverrides.allowAll)
|
|
550
|
+
return false;
|
|
551
|
+
if (_networkOverrides.allowedHosts?.has(candidate)
|
|
552
|
+
|| _networkOverrides.allowedHosts?.has(ip.toLowerCase()))
|
|
553
|
+
return false;
|
|
554
|
+
if (_networkOverrides.blockedHosts?.has(candidate))
|
|
555
|
+
return true;
|
|
496
556
|
if (PRIVATE_NETWORK_HOSTNAMES.has(candidate))
|
|
497
557
|
return true;
|
|
498
558
|
if (PRIVATE_NETWORK_CIDRS.some(re => re.test(candidate)))
|
|
499
559
|
return true;
|
|
500
560
|
return false;
|
|
501
561
|
}
|
|
562
|
+
/**
|
|
563
|
+
* Whether this exact hostname was named in `security.allowedPrivateHosts` (or
|
|
564
|
+
* network security was turned off wholesale). Callers that resolve DNS and
|
|
565
|
+
* re-check the resulting address need this: the operator allowed a *name*, and
|
|
566
|
+
* that name is expected to resolve into a private range.
|
|
567
|
+
*/
|
|
568
|
+
export function isHostExplicitlyAllowed(hostname) {
|
|
569
|
+
if (_networkOverrides.allowAll)
|
|
570
|
+
return true;
|
|
571
|
+
return _networkOverrides.allowedHosts?.has(hostname.toLowerCase()) ?? false;
|
|
572
|
+
}
|
|
502
573
|
export function isPrivateHost(hostname) {
|
|
503
574
|
const lower = hostname.toLowerCase();
|
|
504
575
|
if (_networkOverrides.allowAll)
|
package/dist/context-builder.js
CHANGED
|
@@ -4,6 +4,7 @@ import { collectGitInfo, gitInfoToContext } from './git-context.js';
|
|
|
4
4
|
import { loadMemory, memoryToContext } from './project-memory.js';
|
|
5
5
|
import { buildProjectIndex, indexToContext } from './project-index.js';
|
|
6
6
|
import { loadScopedRules, rulesToContext } from './scoped-rules.js';
|
|
7
|
+
import { readBoardFile, boardToContext } from './board-context.js';
|
|
7
8
|
import { retrieveContext, snippetsToContext, RETRIEVAL_DEFAULTS } from './context-retriever.js';
|
|
8
9
|
async function readJsonSafe(filePath) {
|
|
9
10
|
try {
|
|
@@ -102,11 +103,12 @@ export async function buildProjectContext(cwd) {
|
|
|
102
103
|
const scripts = await detectAvailableScripts(cwd);
|
|
103
104
|
const projectType = await detectProjectType(cwd);
|
|
104
105
|
// Gather context in parallel
|
|
105
|
-
const [gitCtx, memory, rules, projectConfig] = await Promise.all([
|
|
106
|
+
const [gitCtx, memory, rules, projectConfig, boardCtx] = await Promise.all([
|
|
106
107
|
collectGitInfo(cwd).then(gitInfoToContext).catch(() => ''),
|
|
107
108
|
loadMemory(cwd).then(memoryToContext).catch(() => ''),
|
|
108
109
|
loadScopedRules(cwd).then(rulesToContext).catch(() => ''),
|
|
109
110
|
loadProjectConfig(cwd),
|
|
111
|
+
readBoardFile(cwd).then(boardToContext).catch(() => ''),
|
|
110
112
|
]);
|
|
111
113
|
// Project index (uses cache)
|
|
112
114
|
let indexCtx = '';
|
|
@@ -121,6 +123,7 @@ export async function buildProjectContext(cwd) {
|
|
|
121
123
|
projectIndex: indexCtx,
|
|
122
124
|
projectMemory: memory,
|
|
123
125
|
scopedRules: rules,
|
|
126
|
+
boardContext: boardCtx,
|
|
124
127
|
availableScripts: scripts,
|
|
125
128
|
projectConfig,
|
|
126
129
|
};
|
|
@@ -145,6 +148,11 @@ export function contextToInstructions(baseInstructions, ctx) {
|
|
|
145
148
|
if (ctx.projectMemory) {
|
|
146
149
|
sections.push(ctx.projectMemory);
|
|
147
150
|
}
|
|
151
|
+
// After memory (durable knowledge), before rules: the board is the freshest
|
|
152
|
+
// section and the one a member should act on first when they conflict.
|
|
153
|
+
if (ctx.boardContext) {
|
|
154
|
+
sections.push(ctx.boardContext);
|
|
155
|
+
}
|
|
148
156
|
if (ctx.scopedRules) {
|
|
149
157
|
sections.push(ctx.scopedRules);
|
|
150
158
|
}
|
package/dist/feedback.js
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import { randomUUID } from 'node:crypto';
|
|
5
|
+
import { USER_CONFIG_DIR, VERSION } from './constants.js';
|
|
6
|
+
import { logger } from './logger.js';
|
|
7
|
+
const FEEDBACK_FILE = path.join(process.env.HOME || process.env.USERPROFILE || '.', USER_CONFIG_DIR, 'feedback.json');
|
|
8
|
+
export async function saveFeedback(item) {
|
|
9
|
+
const fullItem = {
|
|
10
|
+
id: randomUUID(),
|
|
11
|
+
timestamp: new Date().toISOString(),
|
|
12
|
+
systemInfo: item.systemInfo || {
|
|
13
|
+
version: VERSION,
|
|
14
|
+
nodeVersion: process.version,
|
|
15
|
+
platform: os.platform(),
|
|
16
|
+
arch: os.arch(),
|
|
17
|
+
},
|
|
18
|
+
...item,
|
|
19
|
+
};
|
|
20
|
+
try {
|
|
21
|
+
await fs.mkdir(path.dirname(FEEDBACK_FILE), { recursive: true });
|
|
22
|
+
let existing = [];
|
|
23
|
+
try {
|
|
24
|
+
const raw = await fs.readFile(FEEDBACK_FILE, 'utf-8');
|
|
25
|
+
const parsed = JSON.parse(raw);
|
|
26
|
+
if (Array.isArray(parsed))
|
|
27
|
+
existing = parsed;
|
|
28
|
+
}
|
|
29
|
+
catch { }
|
|
30
|
+
existing.unshift(fullItem);
|
|
31
|
+
// Keep last 100 feedback submissions locally
|
|
32
|
+
if (existing.length > 100)
|
|
33
|
+
existing = existing.slice(0, 100);
|
|
34
|
+
await fs.writeFile(FEEDBACK_FILE, JSON.stringify(existing, null, 2), { encoding: 'utf-8', mode: 0o600 });
|
|
35
|
+
logger.info('Feedback recorded locally', { id: fullItem.id, category: fullItem.category });
|
|
36
|
+
}
|
|
37
|
+
catch (err) {
|
|
38
|
+
logger.warn('Failed to persist feedback locally', { error: err.message });
|
|
39
|
+
}
|
|
40
|
+
return fullItem;
|
|
41
|
+
}
|
|
42
|
+
export async function listFeedback() {
|
|
43
|
+
try {
|
|
44
|
+
const raw = await fs.readFile(FEEDBACK_FILE, 'utf-8');
|
|
45
|
+
const parsed = JSON.parse(raw);
|
|
46
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
return [];
|
|
50
|
+
}
|
|
51
|
+
}
|
package/dist/headless.js
CHANGED
|
@@ -4,12 +4,13 @@ import { execSync } from 'node:child_process';
|
|
|
4
4
|
import { createAgent } from './agent.js';
|
|
5
5
|
import { defaultTools } from './tools.js';
|
|
6
6
|
import { extraTools } from './tools-extra.js';
|
|
7
|
+
import { memoryTools } from './tools-memory.js';
|
|
7
8
|
import { webTools } from './tools-web.js';
|
|
8
9
|
import { advancedTools } from './tools-advanced.js';
|
|
9
10
|
import { visionTools } from './tools-vision.js';
|
|
10
11
|
import { voiceTools } from './tools-voice.js';
|
|
11
12
|
import { browserTools, closeBrowser } from './tools-browser.js';
|
|
12
|
-
import {
|
|
13
|
+
import { startMcp } from './mcp.js';
|
|
13
14
|
import { costTracker } from './cost-tracker.js';
|
|
14
15
|
import { saveSession, loadSession, listSessions, generateSessionId } from './sessions.js';
|
|
15
16
|
import { logger } from './logger.js';
|
|
@@ -163,7 +164,6 @@ async function runSinglePrompt(prompt, output, format, outputSchemaPath, beforeE
|
|
|
163
164
|
console.error('Error: No models configured.\nSet SCALIX_MODEL env var or run interactive setup: scalix');
|
|
164
165
|
process.exit(1);
|
|
165
166
|
}
|
|
166
|
-
const mcpTools = loadMcpServices();
|
|
167
167
|
// Only discover/load project-local plugins in a trusted workspace — plugins
|
|
168
168
|
// dynamically import and execute code from the project directory.
|
|
169
169
|
const plugins = isWorkspaceTrusted() ? await discoverPlugins(process.cwd()) : [];
|
|
@@ -171,7 +171,7 @@ async function runSinglePrompt(prompt, output, format, outputSchemaPath, beforeE
|
|
|
171
171
|
process.stderr.write('Project plugins skipped (untrusted workspace).\n');
|
|
172
172
|
}
|
|
173
173
|
const pluginTools = getPluginTools(plugins);
|
|
174
|
-
const allTools = [...defaultTools, ...extraTools, ...webTools, ...advancedTools, ...visionTools, ...voiceTools, ...browserTools, ...
|
|
174
|
+
const allTools = [...defaultTools, ...extraTools, ...memoryTools, ...webTools, ...advancedTools, ...visionTools, ...voiceTools, ...browserTools, ...pluginTools];
|
|
175
175
|
const instructions = await buildEnhancedInstructions();
|
|
176
176
|
let outputSchema;
|
|
177
177
|
if (outputSchemaPath) {
|
|
@@ -214,6 +214,12 @@ async function runSinglePrompt(prompt, output, format, outputSchemaPath, beforeE
|
|
|
214
214
|
groundingCheck: true,
|
|
215
215
|
contextRetrieval: true,
|
|
216
216
|
});
|
|
217
|
+
if (agent.getSkippedHookCount() > 0) {
|
|
218
|
+
process.stderr.write(`Project hooks skipped (untrusted workspace): ${agent.getSkippedHookCount()}\n`);
|
|
219
|
+
}
|
|
220
|
+
// Connect configured MCP servers and register their tools. Warnings only:
|
|
221
|
+
// a server that will not start must not take the whole run down with it.
|
|
222
|
+
const mcpHub = await startMcp(agent, (m) => process.stderr.write(`\x1b[33m⚠ ${m}\x1b[0m\n`));
|
|
217
223
|
// Register the account's Scalix Cloud platform tools (one attempt; failure
|
|
218
224
|
// is stated once and the run continues with local tools only).
|
|
219
225
|
const cloudStatus = await agent.refreshCloudTools();
|
|
@@ -271,6 +277,7 @@ async function runSinglePrompt(prompt, output, format, outputSchemaPath, beforeE
|
|
|
271
277
|
process.stdout.write(formatted + '\n');
|
|
272
278
|
}
|
|
273
279
|
await closeBrowser();
|
|
280
|
+
await mcpHub.close(); // stdio servers are child processes; don't orphan them
|
|
274
281
|
if (beforeExit)
|
|
275
282
|
await beforeExit();
|
|
276
283
|
process.exit(0);
|
|
@@ -329,7 +336,6 @@ async function runInteractive() {
|
|
|
329
336
|
if (selectedModel.length === 0) {
|
|
330
337
|
throw new Error('No models configured. Set SCALIX_MODEL env var or run interactive setup.');
|
|
331
338
|
}
|
|
332
|
-
const mcpTools = loadMcpServices();
|
|
333
339
|
// Only discover/load project-local plugins in a trusted workspace — plugins
|
|
334
340
|
// dynamically import and execute code from the project directory.
|
|
335
341
|
const plugins = isWorkspaceTrusted() ? await discoverPlugins(process.cwd()) : [];
|
|
@@ -337,7 +343,7 @@ async function runInteractive() {
|
|
|
337
343
|
process.stderr.write('Project plugins skipped (untrusted workspace).\n');
|
|
338
344
|
}
|
|
339
345
|
const pluginTools = getPluginTools(plugins);
|
|
340
|
-
const allTools = [...defaultTools, ...extraTools, ...webTools, ...advancedTools, ...visionTools, ...voiceTools, ...browserTools, ...
|
|
346
|
+
const allTools = [...defaultTools, ...extraTools, ...memoryTools, ...webTools, ...advancedTools, ...visionTools, ...voiceTools, ...browserTools, ...pluginTools];
|
|
341
347
|
const instructions = await buildEnhancedInstructions();
|
|
342
348
|
const rl = readline.createInterface({
|
|
343
349
|
input: process.stdin,
|
|
@@ -383,6 +389,9 @@ async function runInteractive() {
|
|
|
383
389
|
return answer.trim().toLowerCase() === 'y';
|
|
384
390
|
},
|
|
385
391
|
});
|
|
392
|
+
if (agent.getSkippedHookCount() > 0) {
|
|
393
|
+
process.stderr.write(`Project hooks skipped (untrusted workspace): ${agent.getSkippedHookCount()}\n`);
|
|
394
|
+
}
|
|
386
395
|
let sessionId = generateSessionId();
|
|
387
396
|
agent.on('thinking:start', () => process.stdout.write('\n\x1b[33mThinking...\x1b[0m'));
|
|
388
397
|
agent.on('tool:call', (name, args) => {
|
|
@@ -413,6 +422,9 @@ async function runInteractive() {
|
|
|
413
422
|
console.log(`\x1b[33m\u26A0 Context usage: ${info.percentage}% (${info.tokens}/${info.maxTokens} tokens)\x1b[0m`);
|
|
414
423
|
}
|
|
415
424
|
});
|
|
425
|
+
// Connect configured MCP servers and register their tools. Warnings only:
|
|
426
|
+
// a server that will not start must not take the whole run down with it.
|
|
427
|
+
const mcpHub = await startMcp(agent, (m) => process.stderr.write(`\x1b[33m⚠ ${m}\x1b[0m\n`));
|
|
416
428
|
// Register the account's Scalix Cloud platform tools (one attempt; failure
|
|
417
429
|
// is stated once and the session continues with local tools only).
|
|
418
430
|
const cloudStatus = await agent.refreshCloudTools();
|
|
@@ -445,6 +457,7 @@ async function runInteractive() {
|
|
|
445
457
|
console.log('Goodbye!');
|
|
446
458
|
rl.close();
|
|
447
459
|
await closeBrowser().catch(() => { });
|
|
460
|
+
await mcpHub.close().catch(() => { }); // stdio servers are our children
|
|
448
461
|
process.exit(0);
|
|
449
462
|
}
|
|
450
463
|
process.on('SIGINT', () => { shutdown().catch(() => process.exit(1)); });
|
|
@@ -610,6 +623,10 @@ async function runInteractive() {
|
|
|
610
623
|
return true;
|
|
611
624
|
}
|
|
612
625
|
if (cmd === '/hooks') {
|
|
626
|
+
const skipped = agent.getSkippedHookCount();
|
|
627
|
+
if (skipped > 0) {
|
|
628
|
+
console.log(`\n${skipped} project hook(s) skipped — untrusted workspace.`);
|
|
629
|
+
}
|
|
613
630
|
console.log('\n' + hooksSummary(agent.getHooks()) + '\n');
|
|
614
631
|
return true;
|
|
615
632
|
}
|