release: 0.1.0-alpha.1 - #148
Conversation
…onfig.json This allows sdks to be built using tsgo - see microsoft/typescript-go#474
…kage‘s public interface
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Release version 0.1.0-alpha.1 and add a Terminal49 TypeScript client with MCP server CLI, tools, API resources, tests, and CI workflowsThis release introduces a generated TypeScript API client for Terminal49, a packaged MCP server with a CLI and transport options, and a full set of API resources and MCP tools for key endpoints; it also adds testing, build scripts, and CI workflows with distribution configs and documentation.
Key files: src/index.ts, src/client.ts, packages/mcp-server/src/index.ts, packages/mcp-server/src/server.ts, packages/mcp-server/src/tools/index.ts, package.json, .github/workflows/ci.yml 📍Where to StartStart with the MCP server CLI entrypoint 📊 Macroscope summarized 8fac8a8. 12 files reviewed, 47 issues evaluated, 28 issues filtered, 12 comments posted🗂️ Filtered Issuesjest.config.ts — 0 comments posted, 3 evaluated, 3 filtered
scripts/utils/check-is-in-git-install.sh — 0 comments posted, 1 evaluated, 1 filtered
scripts/utils/git-swap.sh — 1 comment posted, 4 evaluated, 2 filtered
scripts/utils/upload-artifact.sh — 1 comment posted, 10 evaluated, 9 filtered
src/client.ts — 7 comments posted, 12 evaluated, 5 filtered
src/core/error.ts — 1 comment posted, 5 evaluated, 3 filtered
src/internal/shims.ts — 2 comments posted, 9 evaluated, 5 filtered
|
| this.error = error; | ||
| } | ||
|
|
||
| private static makeMessage(status: number | undefined, error: any, message: string | undefined) { |
There was a problem hiding this comment.
APIError.makeMessage uses JSON.stringify on error.message and error without guarding against serialization errors. If these contain circular references or BigInt, JSON.stringify will throw, causing the APIError constructor to throw and masking the original error.
Consider wrapping the JSON.stringify calls in a try/catch and falling back to a safe string (e.g., String(value) or a generic placeholder). This would align with the safer pattern used by castToError and prevent crashes in error-handling paths.
- private static makeMessage(status: number | undefined, error: any, message: string | undefined) {
- const msg =
- error?.message ?
- typeof error.message === 'string' ?
- error.message
- : JSON.stringify(error.message)
- : error ? JSON.stringify(error)
- : message;
-
- if (status && msg) {
- return `${status} ${msg}`;
- }
- if (status) {
- return `${status} status code (no body)`;
- }
- if (msg) {
- return msg;
- }
- return '(no status code or body)';
- }
+ private static makeMessage(status: number | undefined, error: any, message: string | undefined) {
+ const safeStringify = (v: any): string | undefined => {
+ if (v == null) return undefined;
+ try {
+ return typeof v === 'string' ? v : JSON.stringify(v);
+ } catch {
+ try {
+ return String(v);
+ } catch {
+ return undefined;
+ }
+ }
+ };
+
+ const msgFromErrorMessage =
+ error && 'message' in (error as any)
+ ? typeof (error as any).message === 'string'
+ ? (error as any).message
+ : safeStringify((error as any).message)
+ : undefined;
+
+ const msg = msgFromErrorMessage ?? (error ? safeStringify(error) : message);
+
+ if (status && msg) {
+ return `${status} ${msg}`;
+ }
+ if (status) {
+ return `${status} status code (no body)`;
+ }
+ if (msg) {
+ return msg;
+ }
+ return '(no status code or body)';
+ }
🚀 Reply to ask Macroscope to explain or update this suggestion.
👍 Helpful? React to give us feedback.
| } | ||
|
|
||
| if (typeof query === 'object' && query && !Array.isArray(query)) { | ||
| url.search = this.stringifyQuery(query as Record<string, unknown>); |
There was a problem hiding this comment.
buildURL() replaces the entire query string via url.search = ..., which discards any existing query parameters present in path. If path already includes queries, they are not merged with defaultQuery/query, leading to lost caller-specified params.
Consider merging query into url.searchParams instead of overwriting url.search. This preserves existing params from path while allowing defaultQuery/query to override or add keys.
- url.search = this.stringifyQuery(query as Record<string, unknown>);
+ const q = this.stringifyQuery(query as Record<string, unknown>);
+ if (q.length > 0) {
+ const newParams = new URLSearchParams(q);
+ for (const [key, value] of newParams) {
+ url.searchParams.set(key, value);
+ }
+ }🚀 Reply to ask Macroscope to explain or update this suggestion.
👍 Helpful? React to give us feedback.
| # we want the final file structure for git installs to match the npm installs, so we | ||
|
|
||
| # delete everything except ./dist and ./node_modules | ||
| find . -maxdepth 1 -mindepth 1 ! -name 'dist' ! -name 'node_modules' -exec rm -rf '{}' + |
There was a problem hiding this comment.
The find deletion on the top level also removes the repository’s .git directory and other dotfiles/directories (e.g., .npmrc, .env, .github). This breaks Git in the repo and can remove important configuration.
Consider excluding hidden entries (e.g., add ! -name '.*', and/or explicitly ! -name '.git') so Git metadata and other dotfiles are preserved during the cleanup.
-find . -maxdepth 1 -mindepth 1 ! -name 'dist' ! -name 'node_modules' -exec rm -rf '{}' +
+find . -maxdepth 1 -mindepth 1 ! -name 'dist' ! -name 'node_modules' ! -name '.*' -exec rm -rf '{}' +🚀 Reply to ask Macroscope to explain or update this suggestion.
👍 Helpful? React to give us feedback.
| if (stream[Symbol.asyncIterator]) return stream; | ||
|
|
||
| const reader = stream.getReader(); | ||
| return { |
There was a problem hiding this comment.
The async iterator returned by stream in file:src/internal/shims.ts doesn’t track a finished state, so after observing { done: true } (line 67) or calling return() (line 68), further next() calls still invoke reader.read() on a released reader and return() still calls reader.cancel(), both of which can throw. Consider tracking a local finished flag and making next() immediately return { done: true, value: undefined } and return() a no-op once the iterator is complete.
+ let finished = false;
- if (result?.done) reader.releaseLock(); // release lock when stream becomes closed
+ if (result?.done) { finished = true; reader.releaseLock(); } // release lock when stream becomes closed
- reader.releaseLock(); // release lock when stream becomes errored
+ finished = true;
+ reader.releaseLock(); // release lock when stream becomes errored
- async return() {
- const cancelPromise = reader.cancel();
- reader.releaseLock();
- await cancelPromise;
- return { done: true, value: undefined };
- },
+ async return() {
+ if (finished) {
+ return { done: true, value: undefined };
+ }
+ const cancelPromise = reader.cancel();
+ reader.releaseLock();
+ await cancelPromise;
+ finished = true;
+ return { done: true, value: undefined };
+ },🚀 Reply to ask Macroscope to explain or update this suggestion.
👍 Helpful? React to give us feedback.
| * This polyfill was pulled from https://github.com/MattiasBuelens/web-streams-polyfill/pull/122#issuecomment-1627354490 | ||
| */ | ||
| export function ReadableStreamToAsyncIterable<T>(stream: any): AsyncIterableIterator<T> { | ||
| if (stream[Symbol.asyncIterator]) return stream; |
There was a problem hiding this comment.
Returning stream when it only implements Symbol.asyncIterator but isn’t itself an async iterator breaks the AsyncIterableIterator<T> contract and can cause runtime errors when callers call next() on the returned value.
Consider invoking stream[Symbol.asyncIterator]() and returning that iterator, so the function consistently returns an object with next() per the declared return type.
- if (stream[Symbol.asyncIterator]) return stream;
+ if (typeof stream?.[Symbol.asyncIterator] === 'function') return stream[Symbol.asyncIterator]();🚀 Reply to ask Macroscope to explain or update this suggestion.
👍 Helpful? React to give us feedback.
| controller: AbortController, | ||
| ): Promise<Response> { | ||
| const { signal, method, ...options } = init || {}; | ||
| if (signal) signal.addEventListener('abort', () => controller.abort()); |
There was a problem hiding this comment.
Abort propagation misses an already-aborted init.signal. If the signal is aborted before fetchWithTimeout runs, the 'abort' listener won't fire, so the internal controller isn't aborted and the request proceeds, breaking the cancellation contract.
Consider aborting the internal AbortController upfront when signal?.aborted is true, before setting the timeout and calling fetch, in addition to registering the 'abort' listener.
+ if (signal?.aborted) controller.abort();🚀 Reply to ask Macroscope to explain or update this suggestion.
👍 Helpful? React to give us feedback.
| @@ -0,0 +1,27 @@ | |||
| #!/usr/bin/env bash | |||
| set -exuo pipefail | |||
There was a problem hiding this comment.
set -x and curl -v will print sensitive data (the Authorization header and potentially the signed URL). This can leak secrets to logs and CI output.
Consider disabling tracing when running commands that include secrets, or removing -x entirely for this script. Similarly, avoid -v; you can use curl's --fail/-sS/-w options to check status without printing headers, and adjust the status check to read the exit code or a minimal status string.
-set -exuo pipefail
+set -euo pipefail
-
-UPLOAD_RESPONSE=$(curl -v -X PUT \
+UPLOAD_RESPONSE=$(curl -sS -o /dev/null -w "HTTP %{http_code}" -X PUT \
-H "Content-Type: application/gzip" \
- --data-binary "@dist/$TARBALL" "$SIGNED_URL" 2>&1)
+ --data-binary "@dist/$TARBALL" "$SIGNED_URL")
-
-if echo "$UPLOAD_RESPONSE" | grep -q "HTTP/[0-9.]* 200"; then
+if echo "$UPLOAD_RESPONSE" | grep -q "200"; then🚀 Reply to ask Macroscope to explain or update this suggestion.
👍 Helpful? React to give us feedback.
| controller: AbortController, | ||
| ): Promise<Response> { | ||
| const { signal, method, ...options } = init || {}; | ||
| if (signal) signal.addEventListener('abort', () => controller.abort()); |
There was a problem hiding this comment.
fetchWithTimeout adds an abort listener to init.signal but never removes it. Over many calls with a long-lived signal, listeners accumulate and leak memory.
Consider storing the handler in a variable, handling the already-aborted case, and removing the listener in the finally block so each registration has a matching cleanup.
- if (signal) signal.addEventListener('abort', () => controller.abort());
+ let abortHandler: (() => void) | undefined;
+ if (signal) {
+ if (signal.aborted) {
+ controller.abort();
+ } else {
+ abortHandler = () => controller.abort();
+ signal.addEventListener('abort', abortHandler);
+ }
+ }
-
- const timeout = setTimeout(() => controller.abort(), ms);
+
+ const timeout = setTimeout(() => controller.abort(), ms);
@@
- clearTimeout(timeout);
+ clearTimeout(timeout);
+ if (signal && abortHandler) {
+ signal.removeEventListener('abort', abortHandler);
+ }
🚀 Reply to ask Macroscope to explain or update this suggestion.
👍 Helpful? React to give us feedback.
| options.timeout = options.timeout ?? this.timeout; | ||
| const { bodyHeaders, body } = this.buildBody({ options }); | ||
| const reqHeaders = await this.buildHeaders({ options: inputOptions, method, bodyHeaders, retryCount }); | ||
|
|
There was a problem hiding this comment.
buildRequest normalizes options.timeout but then calls buildHeaders with inputOptions. This can produce stale externally visible headers (like X-Stainless-Timeout) and auth-related headers that don’t match the actual request configuration.
Consider passing the normalized options into buildHeaders so headers reflect the final request options, consistent with how buildBody already uses the normalized options.
- const reqHeaders = await this.buildHeaders({ options: inputOptions, method, bodyHeaders, retryCount });
+ const reqHeaders = await this.buildHeaders({ options, method, bodyHeaders, retryCount });🚀 Reply to ask Macroscope to explain or update this suggestion.
👍 Helpful? React to give us feedback.
| baseURL: baseURL || `https://api.terminal49.com/v2`, | ||
| }; | ||
|
|
||
| this.baseURL = options.baseURL!; |
There was a problem hiding this comment.
The constructor only throws when apiKey === undefined, so an empty string (e.g., '') passes through and results in Authorization: ''. This contradicts the error message ("missing or empty") and causes confusing downstream auth failures instead of failing fast.
Consider treating empty or whitespace-only API keys as invalid too, e.g., check for apiKey == null || apiKey.trim() === '' so initialization fails early with a clear error.
- if (apiKey === undefined) {
+ if (apiKey == null || apiKey.trim() === '') {🚀 Reply to ask Macroscope to explain or update this suggestion.
👍 Helpful? React to give us feedback.
Automated Release PR
0.1.0-alpha.1 (2025-10-23)
Full Changelog: v0.0.1-alpha.0...v0.1.0-alpha.1
Features
Bug Fixes
Performance Improvements
Chores
compilerOptions.baseUrlfrom tsconfig.json (2dc6c24)docs_searchtool at other URLs (7203f41)Documentation
Refactors
This pull request is managed by Stainless's GitHub App.
The semver version number is based on included commit messages. Alternatively, you can manually set the version number in the title of this pull request.
For a better experience, it is recommended to use either rebase-merge or squash-merge when merging this pull request.
🔗 Stainless website
📚 Read the docs
🙋 Reach out for help or questions