3131 * client to the canonical routes above.
3232 */
3333
34+ import { readFileSync } from "node:fs" ;
35+ import { dirname , resolve } from "node:path" ;
36+ import { fileURLToPath } from "node:url" ;
37+
3438const DEFAULT_BASE_URL = "https://api.instanode.dev" ;
3539const DEFAULT_DASHBOARD_URL = "https://instanode.dev" ;
3640
41+ /**
42+ * Module-init User-Agent — resolved once from package.json so every release
43+ * naturally rolls forward without anyone remembering to bump a hardcoded
44+ * string. BugBash B16 F4 (regression of task #176): the previous version
45+ * carried `instanode-mcp/0.11.0` as a literal in two places, which drifted
46+ * out of sync with the published package version. Falling back to "dev" on
47+ * any failure (file missing, malformed JSON) so the client never explodes
48+ * at import time just to read a UA.
49+ */
50+ function resolveUserAgent ( ) : string {
51+ try {
52+ const here = dirname ( fileURLToPath ( import . meta. url ) ) ;
53+ // dist/client.js → repo root; src/client.ts → repo root in dev. Same path.
54+ const pkgPath = resolve ( here , ".." , "package.json" ) ;
55+ const pkgRaw = readFileSync ( pkgPath , "utf8" ) ;
56+ const pkg = JSON . parse ( pkgRaw ) as { name ?: string ; version ?: string } ;
57+ const name = pkg . name && pkg . name . length > 0 ? pkg . name : "instanode-mcp" ;
58+ const version = pkg . version && pkg . version . length > 0 ? pkg . version : "dev" ;
59+ return `${ name } /${ version } ` ;
60+ } catch {
61+ return "instanode-mcp/dev" ;
62+ }
63+ }
64+
65+ const USER_AGENT = resolveUserAgent ( ) ;
66+
3767export interface ClientOptions {
3868 baseURL ?: string ;
3969}
@@ -200,6 +230,23 @@ export interface DeployDeleteResult {
200230 message ?: string ;
201231}
202232
233+ /**
234+ * Response shape from POST /deploy/:id/redeploy.
235+ *
236+ * The live API documents this as a bare 202 with NO body (see openapi.json),
237+ * not a deployment record. The previous client mis-typed it as DeployGetResult
238+ * and the index.ts handler dereferenced `result.item.app_id`, blowing up
239+ * with "Cannot read properties of undefined (reading 'app_id')" on every
240+ * real call. BugBash B16 F1 (regression of task #170): use a body-less type
241+ * and let callers fall back to the caller-supplied id when needed.
242+ */
243+ export interface RedeployResult {
244+ ok : boolean ;
245+ id ?: string ;
246+ status ?: string ;
247+ message ?: string ;
248+ }
249+
203250/** Caller-supplied params for create_deploy. */
204251export interface CreateDeployParams {
205252 /** Base64-encoded gzip tarball (with Dockerfile + source). <50 MB after decode. */
@@ -360,7 +407,7 @@ export class InstantClient {
360407 private headers ( ) : Record < string , string > {
361408 const h : Record < string , string > = {
362409 "Content-Type" : "application/json" ,
363- "User-Agent" : "instanode-mcp/0.11.0" ,
410+ "User-Agent" : USER_AGENT ,
364411 } ;
365412 const tok = this . bearerToken ( ) ;
366413 if ( tok ) {
@@ -375,7 +422,7 @@ export class InstantClient {
375422 */
376423 private authHeaders ( ) : Record < string , string > {
377424 const h : Record < string , string > = {
378- "User-Agent" : "instanode-mcp/0.11.0" ,
425+ "User-Agent" : USER_AGENT ,
379426 } ;
380427 const tok = this . bearerToken ( ) ;
381428 if ( tok ) {
@@ -444,6 +491,19 @@ export class InstantClient {
444491 ) ;
445492 }
446493
494+ // BugBash B16 F1 (regression of task #170 P0-1): empty 2xx bodies used to
495+ // leave `data` as undefined, and any caller that did `result.foo` blew up
496+ // with "Cannot read properties of undefined (reading 'foo')". Eight tools
497+ // hit this: redeploy, delete_resource, delete_deployment, get_deployment,
498+ // list_deployments, list_resources, get_api_token, claim_token — most
499+ // commonly redeploy + delete_*, which the API documents as bare 2xx (no
500+ // body). The previous fix only patched redeploy. Now: any 2xx with an
501+ // empty body returns a safe sentinel `{ok: true}` so the dereferencing
502+ // path stays alive. Callers that need richer fields handle the empty
503+ // case explicitly (see redeploy / deleteResource / deleteDeployment).
504+ if ( data === undefined ) {
505+ return { ok : true } as T ;
506+ }
447507 return data as T ;
448508 }
449509
@@ -510,6 +570,13 @@ export class InstantClient {
510570 ) ;
511571 }
512572
573+ // Same empty-2xx safe sentinel as request<T>(). See the long comment up
574+ // there for the why. requestMultipart() is only used for create_deploy
575+ // today, which always returns a JSON body, but mirroring the safety
576+ // makes the two paths drift-free.
577+ if ( data === undefined ) {
578+ return { ok : true } as T ;
579+ }
513580 return data as T ;
514581 }
515582
@@ -715,14 +782,31 @@ export class InstantClient {
715782 ) ;
716783 }
717784
718- /** POST /deploy/:id/redeploy — rebuild + rolling update an existing app. */
719- async redeploy ( id : string ) : Promise < DeployGetResult > {
720- return this . request < DeployGetResult > (
785+ /**
786+ * POST /deploy/:id/redeploy — rebuild + rolling update an existing app.
787+ *
788+ * The live API returns a bare 202 with no body (see openapi.json). Earlier
789+ * versions of this client typed the response as DeployGetResult and the
790+ * tool handler dereferenced `result.item.app_id`, throwing
791+ * "Cannot read properties of undefined (reading 'app_id')" on every real
792+ * call. BugBash B16 F1 (regression of task #170): the empty-body now
793+ * resolves to `{ok: true}` via the request<T>() empty-2xx sentinel; this
794+ * helper layers the caller-supplied id on top so the tool handler has a
795+ * stable surface to read.
796+ */
797+ async redeploy ( id : string ) : Promise < RedeployResult > {
798+ const raw = await this . request < RedeployResult > (
721799 "POST" ,
722800 `/deploy/${ encodeURIComponent ( id ) } /redeploy` ,
723801 undefined ,
724802 { requireAuth : true }
725803 ) ;
804+ return {
805+ ok : raw . ok ?? true ,
806+ id : raw . id ?? id ,
807+ status : raw . status ?? "building" ,
808+ message : raw . message ,
809+ } ;
726810 }
727811
728812 /** DELETE /deploy/:id — tear down the running pod + remove the record. */
0 commit comments