* feat(serve): workspace memory and agents CRUD (#4175 Wave 4 PR 16)
Adds the first Wave 4 mutation route surface: workspace-scoped memory
and subagent CRUD over HTTP. Remote clients (TUI / channels / web /
IDE adapters) can now list, read, create, update, and delete subagent
definitions and read / append / replace QWEN.md without disturbing
session state.
Routes:
- GET /workspace/memory (read-only snapshot)
- POST /workspace/memory (append/replace, strict-gated)
- GET /workspace/agents (list project + user + builtin)
- POST /workspace/agents (create-only; 409 on collision)
- GET /workspace/agents/:agentType (full detail incl. systemPrompt)
- POST /workspace/agents/:agentType (update; 403 read-only on builtin)
- DELETE /workspace/agents/:agentType (idempotent for SDK callers)
Mutation paths use mutate({ strict: true }) from PR 15 so they refuse
unauthenticated requests even on no-token loopback defaults. Workspace
mutations validate X-Qwen-Client-Id against bridge.knownClientIds() and
stamp originatorClientId on emitted events.
Capability tags added: workspace_memory, workspace_agents.
New typed events fanned out via bridge.publishWorkspaceEvent (best-
effort to every active session bus; read-after-write is the contract):
- memory_changed { scope, filePath, mode, bytesWritten }
- agent_changed { change, name, level }
writeContextFile.ts is the new core helper that resolves
QWEN.md placement (workspace vs ~/.qwen) and append-vs-replace
semantics. Whitespace-only appends short-circuit before fs.writeFile,
so a no-op POST does not bump mtime or fan out a misleading event.
SubagentManager is wrapped with a CRUD-scoped Config stub via Proxy:
only getSdkMode / getProjectRoot / getActiveExtensions are stubbed
(verified against subagent-manager.ts; getToolRegistry is execution-
path only). Any future Config method touched on a CRUD path throws
immediately so dependency creep is visible.
Auto-memory CRUD, persistent audit log, and the EACCES → NOT_FOUND
unlink mapping in core SubagentManager.deleteSubagent are explicit
follow-ups (PR 16.5 / PR 24 / separate fix).
Validation:
- typecheck: cli + sdk-typescript clean
- vitest: serve 348/348, writeContextFile 10/10, SDK 335/335
- eslint: clean
* fix(serve): address Codex P2 review on PR 16 (#4175 Wave 4 PR 16 follow-up)
Three correctness issues Codex flagged on the just-shipped workspace
memory + agents CRUD surface:
1. Concurrent POST /workspace/memory append no longer loses writes.
Two simultaneous appends would each read the same existing file,
compose new content in JS memory, then race the fs.writeFile —
the later write silently overwrote the earlier appended entry.
Add a per-resolved-path Mutex map (mirroring jsonl-utils.ts's
fileLocks pattern) and wrap the entire read-compose-write
sequence in runExclusive.
2. GET /workspace/agents now reflects out-of-band file changes.
SubagentManager.listSubagents() default served the in-memory cache;
developer / IDE adapter edits to .qwen/agents/*.md never appeared
even though GET /workspace/agents/:agentType always reads disk.
Pass { force: true } so the LIST route walks disk every call,
matching the detail route's "filesystem is the source of truth"
contract.
3. Reject builtin agent names on POST /workspace/agents to prevent
undeleteable shadow files. A client could write a project-level
agent named "general-purpose" — list/load resolved the shadow
first, but SubagentManager.deleteSubagent's name-based builtin
guard (subagent-manager.ts:302) rejected DELETE forever. Add a
BuiltinAgentRegistry.isBuiltinAgent check in parseAgentConfig
so the conflict surfaces at create time instead of trapping the
file beyond the API. The check is case-insensitive, matching the
resolver's case-insensitive cascade.
New tests:
- writeContextFile.test.ts: 10 parallel appends, all 10 entries
must survive in the final file (would fail without the mutex).
- workspaceAgents.test.ts: GET /workspace/agents observes a
freshly-written agent file on the second call (force-refresh
proof); POST with name="general-purpose" returns 422 + the
case-insensitive variant "explore" too.
Validation:
- typecheck: cli + sdk-typescript clean
- vitest: serve 351/351 (was 348, +3 new), writeContextFile 11/11
- eslint: clean
* fix(serve): apply round-1 review fold-in 2a (HIGH + CodeQL) on PR 16
Round-1 inline review (#4249) flagged ~28 items across Copilot,
wenshao, and CodeQL. This commit lands the HIGH-severity correctness
fixes plus the two CodeQL polynomial-regex warnings.
Validation tighten — `parseAgentConfig` + `parseAgentUpdates`:
- Trim leading/trailing whitespace on `name` before passing to
SubagentManager. `" tester "` would otherwise create a frontmatter
name with spaces that case-insensitive lookups can never find.
- Fail-closed (422 invalid_config) on present-but-wrong-type optional
scalars: `model`, `color`, `approvalMode`, `background`. Previously
malformed values silently dropped through validation, masking
client-serialization bugs.
- Validate `approvalMode` against the `APPROVAL_MODES` enum on both
create and update; an unknown value used to 201 with the field
silently omitted from the saved file.
- `runConfig` is now whitelist-sanitized to `{ max_time_minutes,
max_turns }` only; unknown keys are dropped, malformed values
return 422. Previously the whole input object was persisted
verbatim into YAML frontmatter.
- `?scope=` query is fail-closed for repeated values
(`?scope=workspace&scope=global`) — Express parses these as arrays
which the previous `typeof === 'string'` check silently treated as
absent, broadening DELETE/UPDATE semantics from one level to both.
- Empty update body returns 400 invalid_config (previously rewrote
the file + emitted a misleading `agent_changed` event).
- No-op updates (every supplied field already matches `existing`)
return 200 + `changed: false` and SKIP the file rewrite + event
fan-out.
Memory write helper — `writeContextFile.ts`:
- Move whitespace-only no-op detection BEFORE `fs.mkdir`. Without
this, an empty POST still created the parent directory and bumped
its mtime even though `changed: false` was reported.
- Replace two polynomial regex patterns flagged by CodeQL
(`/^\s+|\s+$/g` and `/^\n+|\n+$/g`) with hand-rolled `while` loops.
Same pattern auth.ts:120-125 already uses for the same CodeQL rule.
SDK — `DaemonClient.ts` + `types.ts`:
- `DaemonWriteMemoryResult` gains optional `changed?: boolean` so
typed callers can suppress redundant cache invalidation on no-op
appends. Optional for forward-compat with daemons that predate the
field — undefined treats as "changed: true" (legacy contract).
- `deleteWorkspaceAgent` only swallows 404 when the body's `code`
is `agent_not_found`. A bare 404 (older daemon, misrouted proxy,
generic gateway page) now throws — previously the SDK silently
reported success even when the request never reached a route that
understands workspace agents.
- `updateWorkspaceAgent` adds an optional `scope` parameter
mirroring `deleteWorkspaceAgent`, so callers can target the user-
level definition when a project-level agent shadows it.
Validation:
- typecheck: cli + sdk-typescript clean
- vitest: serve 357/357 + writeContextFile 12/12 = 369/369 passing
(was 362; +7 new)
- eslint: clean
Explicitly NOT applying (out of scope per issue #4175 PR 16
review-resolution policy):
- Copilot's "strict gate after body parser" finding — already
documented as PR 15 review-resolved tradeoff at auth.ts:256-269.
* fix(serve): apply round-1 review fold-in 2b (MEDIUM + tests) on PR 16
MEDIUM hardening:
- Fix the JSDoc on `collectWorkspaceMemoryStatus` to match the
workspace-root-only discovery the implementation actually does
today. The 32-iteration upward walk is reserved for a future
hierarchical mode but breaks after iteration 1 in v1.
- Lower the depth limit on `walkWorkspaceForMemory` from 32 → 12.
Realistic project depth sits well below 8; 12 leaves headroom
without amplifying blast radius from symlink cycles.
- Daemon `Config` Proxy now defines a `has` trap symmetric to the
existing `get` trap. Without it, a future SubagentManager path
doing `'someMethod' in this.config` would silently get `false` and
bypass the safety net the throw-on-unknown-property design
installed.
- Preflight `manager.loadSubagent(name, level)` before
`manager.createSubagent`. The default-path collision check inside
SubagentManager would otherwise miss same-frontmatter-name +
different-filename collisions; the preflight makes 409
agent_already_exists deterministic.
- Multi-level DELETE now emits one `agent_changed` event per level
that actually had a file removed. Previously an unscoped DELETE
removing both project and user shadows would publish only one
event with one level — misleading subscribers using event metadata
for toasts / audit / echo-suppression.
Test additions (covers the new event types + bridge fan-out + SDK
helpers):
- `daemonEvents.test.ts`: predicate narrowing for `memory_changed` /
`agent_changed` (rejects malformed scope/mode/level), reducer
records `lastWorkspaceMutation` + `lastWorkspaceMutationType` with
latest-event-wins semantics and stays non-terminal.
- `httpAcpBridge.test.ts`: `publishWorkspaceEvent` fans out to every
active session bus; `knownClientIds()` aggregates clientIds across
sessions and the returned set is a snapshot (mutating it does not
affect future calls).
- `workspaceAgents.test.ts`: success-path test stamping
`originatorClientId` on the create / update / delete events for a
known client.
- `DaemonClient.test.ts`: 7 round-trip tests for the new SDK helpers
(workspaceMemory, writeWorkspaceMemory, listWorkspaceAgents,
getWorkspaceAgent, createWorkspaceAgent, updateWorkspaceAgent with
scope query, deleteWorkspaceAgent: 204 / structured 404 / bare 404
triage).
- `writeContextFile.test.ts`: replace the 30ms-mtime test with a
`vi.spyOn(fs, 'writeFile')` assertion that the no-op path never
invokes writeFile. Deterministic on every filesystem.
Validation:
- typecheck: cli + sdk-typescript clean
- vitest: serve 363/363 + writeContextFile 12/12 + SDK 347/347
- eslint: clean
Reviewer guide: combined with fold-in 2a (commit
|
||
|---|---|---|
| .. | ||
| scripts | ||
| src | ||
| test/unit | ||
| package.json | ||
| README.md | ||
| tsconfig.build.json | ||
| tsconfig.json | ||
| vitest.config.ts | ||
@qwen-code/sdk
A minimum experimental TypeScript SDK for programmatic access to Qwen Code.
Feel free to submit a feature request/issue/PR.
Installation
npm install @qwen-code/sdk
Requirements
- Node.js >= 22.0.0
From v0.1.1, the CLI is bundled with the SDK. So no standalone CLI installation is needed.
Quick Start
import { query } from '@qwen-code/sdk';
// Single-turn query
const result = query({
prompt: 'What files are in the current directory?',
options: {
cwd: '/path/to/project',
},
});
// Iterate over messages
for await (const message of result) {
if (message.type === 'assistant') {
console.log('Assistant:', message.message.content);
} else if (message.type === 'result') {
console.log('Result:', message.result);
}
}
API Reference
query(config)
Creates a new query session with the Qwen Code.
Parameters
prompt:string | AsyncIterable<SDKUserMessage>- The prompt to send. Use a string for single-turn queries or an async iterable for multi-turn conversations.options:QueryOptions- Configuration options for the query session.
QueryOptions
| Option | Type | Default | Description |
|---|---|---|---|
cwd |
string |
process.cwd() |
The working directory for the query session. Determines the context in which file operations and commands are executed. |
model |
string |
- | The AI model to use (e.g., 'qwen-max', 'qwen-plus', 'qwen-turbo'). Takes precedence over OPENAI_MODEL and QWEN_MODEL environment variables. |
pathToQwenExecutable |
string |
Auto-detected | Path to the Qwen Code executable. Supports multiple formats: 'qwen' (native binary from PATH), '/path/to/qwen' (explicit path), '/path/to/cli.js' (Node.js bundle), 'node:/path/to/cli.js' (force Node.js runtime), 'bun:/path/to/cli.js' (force Bun runtime). If not provided, auto-detects from: QWEN_CODE_CLI_PATH env var, ~/.volta/bin/qwen, ~/.npm-global/bin/qwen, /usr/local/bin/qwen, ~/.local/bin/qwen, ~/node_modules/.bin/qwen, ~/.yarn/bin/qwen. |
permissionMode |
'default' | 'plan' | 'auto-edit' | 'yolo' |
'default' |
Permission mode controlling tool execution approval. See Permission Modes for details. |
canUseTool |
CanUseTool |
- | Custom permission handler for tool execution approval. Invoked when a tool requires confirmation. Must respond within 60 seconds or the request will be auto-denied. See Custom Permission Handler. |
env |
Record<string, string> |
- | Environment variables to pass to the Qwen Code process. Merged with the current process environment. |
systemPrompt |
string | QuerySystemPromptPreset |
- | System prompt configuration for the main session. Use a string to fully override the built-in Qwen Code system prompt, or a preset object to keep the built-in prompt and append extra instructions. |
mcpServers |
Record<string, McpServerConfig> |
- | MCP (Model Context Protocol) servers to connect. Supports external servers (stdio/SSE/HTTP) and SDK-embedded servers. External servers are configured with transport options like command, args, url, httpUrl, etc. SDK servers use { type: 'sdk', name: string, instance: Server }. |
abortController |
AbortController |
- | Controller to cancel the query session. Call abortController.abort() to terminate the session and cleanup resources. |
debug |
boolean |
false |
Enable debug mode for verbose logging from the CLI process. |
maxSessionTurns |
number |
-1 (unlimited) |
Maximum number of conversation turns before the session automatically terminates. A turn consists of a user message and an assistant response. |
coreTools |
string[] |
- | Equivalent to permissions.allow in settings.json as an allowlist. If specified, only these tools will be available to the AI (all other tools are disabled at registry level). Supports tool name aliases and pattern matching. Example: ['Read', 'Edit', 'Bash(git *)']. |
excludeTools |
string[] |
- | Equivalent to permissions.deny in settings.json. Excluded tools return a permission error immediately. Takes highest priority over all other permission settings. Supports tool name aliases and pattern matching: tool name ('write_file'), shell command prefix ('Bash(rm *)'), or path patterns ('Read(.env)', 'Edit(/src/**)'). |
allowedTools |
string[] |
- | Equivalent to permissions.allow in settings.json. Matching tools bypass canUseTool callback and execute automatically. Only applies when tool requires confirmation. Supports same pattern matching as excludeTools. Example: ['ShellTool(git status)', 'ShellTool(npm test)']. |
authType |
'openai' | 'qwen-oauth' |
'openai' |
Authentication type for the AI service. Using 'qwen-oauth' in SDK is not recommended as credentials are stored in ~/.qwen and may need periodic refresh. |
agents |
SubagentConfig[] |
- | Configuration for subagents that can be invoked during the session. Subagents are specialized AI agents for specific tasks or domains. |
includePartialMessages |
boolean |
false |
When true, the SDK emits incomplete messages as they are being generated, allowing real-time streaming of the AI's response. |
resume |
string |
- | Resume a previous session by providing its session ID. Equivalent to CLI's --resume flag. |
sessionId |
string |
- | Specify a session ID for the new session. Ensures SDK and CLI use the same ID without resuming history. Equivalent to CLI's --session-id flag. |
Tip
If you need to configure
coreTools,excludeTools, orallowedTools, it is strongly recommended to read the permissions configuration documentation first, especially the Tool name aliases and Rule syntax examples sections, to understand the available aliases and pattern matching syntax (e.g.,Bash(git *),Read(.env),Edit(/src/**)).
Timeouts
The SDK enforces the following default timeouts:
| Timeout | Default | Description |
|---|---|---|
canUseTool |
1 minute | Maximum time for canUseTool callback to respond. If exceeded, the tool request is auto-denied. |
mcpRequest |
1 minute | Maximum time for SDK MCP tool calls to complete. |
controlRequest |
1 minute | Maximum time for control operations like initialize(), setModel(), setPermissionMode(), and interrupt() to complete. |
streamClose |
1 minute | Maximum time to wait for initialization to complete before closing CLI stdin in multi-turn mode with SDK MCP servers. |
You can customize these timeouts via the timeout option:
const query = qwen.query('Your prompt', {
timeout: {
canUseTool: 60000, // 60 seconds for permission callback
mcpRequest: 600000, // 10 minutes for MCP tool calls
controlRequest: 60000, // 60 seconds for control requests
streamClose: 15000, // 15 seconds for stream close wait
},
});
Experimental Daemon Session Client
DaemonSessionClient is an experimental wrapper for clients that talk to a
running qwen serve daemon over HTTP + SSE. It binds one daemon session so TUI,
channel, IDE, or web backend adapters do not need to pass sessionId into every
call.
import { DaemonClient, DaemonSessionClient } from '@qwen-code/sdk';
const daemon = new DaemonClient({
baseUrl: 'http://127.0.0.1:4170',
token: process.env['QWEN_SERVER_TOKEN'],
});
const caps = await daemon.capabilities();
const session = await DaemonSessionClient.createOrAttach(daemon, {
workspaceCwd: caps.workspaceCwd,
});
const eventController = new AbortController();
const eventTask = (async () => {
for await (const event of session.events({
signal: eventController.signal,
})) {
console.log(event.type, event.data);
}
})();
const result = await session.prompt({
prompt: [{ type: 'text', text: 'Summarize this workspace.' }],
});
eventController.abort();
await eventTask;
console.log(result.stopReason);
session.events() tracks the last seen SSE event id and reuses it on the next
subscription by default. Pass { resume: false } to start a fresh subscription
without sending Last-Event-ID.
When createOrAttach() is called with modelServiceId, the returned session
client seeds its first event subscription with Last-Event-ID: 0. This replays
the daemon ring from the oldest available event so adapters can observe
attach-time model_switch_failed or model_switched events that are not
reported on the create/attach HTTP response. Raw DaemonClient callers should
pass { lastEventId: 0 } on their first subscribeEvents() call when they use
modelServiceId.
The raw event envelope remains available as DaemonEvent with data: unknown.
Adapters that want a v1 typed view can layer the schema helpers on top without
changing the wire stream:
import {
asKnownDaemonEvent,
createDaemonSessionViewState,
reduceDaemonSessionEvent,
} from '@qwen-code/sdk';
let view = createDaemonSessionViewState();
for await (const event of session.events()) {
view = reduceDaemonSessionEvent(view, event);
const known = asKnownDaemonEvent(event);
if (known?.type === 'permission_request') {
console.log(known.data.requestId);
}
}
Message Types
The SDK provides type guards to identify different message types:
import {
isSDKUserMessage,
isSDKAssistantMessage,
isSDKSystemMessage,
isSDKResultMessage,
isSDKPartialAssistantMessage,
} from '@qwen-code/sdk';
for await (const message of result) {
if (isSDKAssistantMessage(message)) {
// Handle assistant message
} else if (isSDKResultMessage(message)) {
// Handle result message
}
}
Query Instance Methods
The Query instance returned by query() provides several methods:
const q = query({ prompt: 'Hello', options: {} });
// Get session ID
const sessionId = q.getSessionId();
// Check if closed
const closed = q.isClosed();
// Interrupt the current operation
await q.interrupt();
// Change permission mode mid-session
await q.setPermissionMode('yolo');
// Change model mid-session
await q.setModel('qwen-max');
// Close the session
await q.close();
Permission Modes
The SDK supports different permission modes for controlling tool execution:
default: Write tools are denied unless approved viacanUseToolcallback or inallowedTools. Read-only tools execute without confirmation.plan: Blocks all write tools, instructing AI to present a plan first.auto-edit: Auto-approve edit tools (edit, write_file) while other tools require confirmation.yolo: All tools execute automatically without confirmation.
Permission Priority Chain
Decision priority (highest first): deny > ask > allow > (default/interactive mode)
The first matching rule wins.
excludeTools/permissions.deny- Blocks tools completely (returns permission error)permissions.ask- Always requires user confirmationpermissionMode: 'plan'- Blocks all non-read-only toolspermissionMode: 'yolo'- Auto-approves all toolsallowedTools/permissions.allow- Auto-approves matching toolscanUseToolcallback - Custom approval logic (if provided, not called for allowed tools)- Default behavior - Auto-deny in SDK mode (write tools require explicit approval)
Examples
Multi-turn Conversation
import { query, type SDKUserMessage } from '@qwen-code/sdk';
async function* generateMessages(): AsyncIterable<SDKUserMessage> {
yield {
type: 'user',
session_id: 'my-session',
message: { role: 'user', content: 'Create a hello.txt file' },
parent_tool_use_id: null,
};
// Wait for some condition or user input
yield {
type: 'user',
session_id: 'my-session',
message: { role: 'user', content: 'Now read the file back' },
parent_tool_use_id: null,
};
}
const result = query({
prompt: generateMessages(),
options: {
permissionMode: 'auto-edit',
},
});
for await (const message of result) {
console.log(message);
}
Custom Permission Handler
import { query, type CanUseTool } from '@qwen-code/sdk';
const canUseTool: CanUseTool = async (toolName, input, { signal }) => {
// Allow all read operations
if (toolName.startsWith('read_')) {
return { behavior: 'allow', updatedInput: input };
}
// Prompt user for write operations (in a real app)
const userApproved = await promptUser(`Allow ${toolName}?`);
if (userApproved) {
return { behavior: 'allow', updatedInput: input };
}
return { behavior: 'deny', message: 'User denied the operation' };
};
const result = query({
prompt: 'Create a new file',
options: {
canUseTool,
},
});
With External MCP Servers
import { query } from '@qwen-code/sdk';
const result = query({
prompt: 'Use the custom tool from my MCP server',
options: {
mcpServers: {
'my-server': {
command: 'node',
args: ['path/to/mcp-server.js'],
env: { PORT: '3000' },
},
},
},
});
Override the System Prompt
import { query } from '@qwen-code/sdk';
const result = query({
prompt: 'Say hello in one sentence.',
options: {
systemPrompt: 'You are a terse assistant. Answer in exactly one sentence.',
},
});
Append to the Built-in System Prompt
import { query } from '@qwen-code/sdk';
const result = query({
prompt: 'Review the current directory.',
options: {
systemPrompt: {
type: 'preset',
preset: 'qwen_code',
append: 'Be terse and focus on concrete findings.',
},
},
});
With SDK-Embedded MCP Servers
The SDK provides tool and createSdkMcpServer to create MCP servers that run in the same process as your SDK application. This is useful when you want to expose custom tools to the AI without running a separate server process.
tool(name, description, inputSchema, handler)
Creates a tool definition with Zod schema type inference.
| Parameter | Type | Description |
|---|---|---|
name |
string |
Tool name (1-64 chars, starts with letter, alphanumeric and underscores) |
description |
string |
Human-readable description of what the tool does |
inputSchema |
ZodRawShape |
Zod schema object defining the tool's input parameters |
handler |
(args, extra) => Promise<Result> |
Async function that executes the tool and returns MCP content blocks |
The handler must return a CallToolResult object with the following structure:
{
content: Array<
| { type: 'text'; text: string }
| { type: 'image'; data: string; mimeType: string }
| { type: 'resource'; uri: string; mimeType?: string; text?: string }
>;
isError?: boolean;
}
createSdkMcpServer(options)
Creates an SDK-embedded MCP server instance.
| Option | Type | Default | Description |
|---|---|---|---|
name |
string |
Required | Unique name for the MCP server |
version |
string |
'1.0.0' |
Server version |
tools |
SdkMcpToolDefinition[] |
- | Array of tools created with tool() |
Returns a McpSdkServerConfigWithInstance object that can be passed directly to the mcpServers option.
Example
import { z } from 'zod';
import { query, tool, createSdkMcpServer } from '@qwen-code/sdk';
// Define a tool with Zod schema
const calculatorTool = tool(
'calculate_sum',
'Add two numbers',
{ a: z.number(), b: z.number() },
async (args) => ({
content: [{ type: 'text', text: String(args.a + args.b) }],
}),
);
// Create the MCP server
const server = createSdkMcpServer({
name: 'calculator',
tools: [calculatorTool],
});
// Use the server in a query
const result = query({
prompt: 'What is 42 + 17?',
options: {
permissionMode: 'yolo',
mcpServers: {
calculator: server,
},
},
});
for await (const message of result) {
console.log(message);
}
Abort a Query
import { query, isAbortError } from '@qwen-code/sdk';
const abortController = new AbortController();
const result = query({
prompt: 'Long running task...',
options: {
abortController,
},
});
// Abort after 5 seconds
setTimeout(() => abortController.abort(), 5000);
try {
for await (const message of result) {
console.log(message);
}
} catch (error) {
if (isAbortError(error)) {
console.log('Query was aborted');
} else {
throw error;
}
}
Error Handling
The SDK provides an AbortError class for handling aborted queries:
import { AbortError, isAbortError } from '@qwen-code/sdk';
try {
// ... query operations
} catch (error) {
if (isAbortError(error)) {
// Handle abort
} else {
// Handle other errors
}
}
FAQ / Troubleshooting
Version 0.1.0 Requirements
If you're using SDK version 0.1.0, please note the following requirements:
Qwen Code Installation Required
Version 0.1.0 requires Qwen Code >= 0.4.0 to be installed separately and accessible in your PATH.
# Install Qwen Code globally
npm install -g qwen-code@^0.4.0
Note: From version 0.1.1 onwards, the CLI is bundled with the SDK, so no separate Qwen Code installation is needed.
License
Apache-2.0 - see LICENSE for details.