@aexol/spectral 0.9.167 → 0.9.170

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.
Files changed (43) hide show
  1. package/dist/agent/agents.d.ts.map +1 -1
  2. package/dist/agent/agents.js +30 -42
  3. package/dist/agent/index.d.ts.map +1 -1
  4. package/dist/agent/index.js +7 -0
  5. package/dist/cli.js +6 -0
  6. package/dist/commands/ports.d.ts +13 -0
  7. package/dist/commands/ports.d.ts.map +1 -0
  8. package/dist/commands/ports.js +110 -0
  9. package/dist/extensions/ports/index.d.ts +19 -0
  10. package/dist/extensions/ports/index.d.ts.map +1 -0
  11. package/dist/extensions/ports/index.js +154 -0
  12. package/dist/extensions/ports/platform.d.ts +39 -0
  13. package/dist/extensions/ports/platform.d.ts.map +1 -0
  14. package/dist/extensions/ports/platform.js +255 -0
  15. package/dist/generated/graphql-client.d.ts +39 -0
  16. package/dist/generated/graphql-client.d.ts.map +1 -0
  17. package/dist/generated/graphql-client.js +74 -0
  18. package/dist/generated/scalars.d.ts +24 -0
  19. package/dist/generated/scalars.d.ts.map +1 -0
  20. package/dist/generated/scalars.js +30 -0
  21. package/dist/generated/zeus/const.d.ts +8 -0
  22. package/dist/generated/zeus/const.d.ts.map +1 -0
  23. package/dist/generated/zeus/const.js +1626 -0
  24. package/dist/generated/zeus/index.d.ts +8570 -0
  25. package/dist/generated/zeus/index.d.ts.map +1 -0
  26. package/dist/generated/zeus/index.js +796 -0
  27. package/dist/mcp/ui-stream-types.d.ts +2 -2
  28. package/dist/memory/hooks/compaction-trigger.js +1 -1
  29. package/dist/relay/models-fetch.d.ts +4 -5
  30. package/dist/relay/models-fetch.d.ts.map +1 -1
  31. package/dist/relay/models-fetch.js +50 -79
  32. package/dist/sdk/agent-core/agent-loop.js +1 -1
  33. package/dist/sdk/coding-agent/core/agent-session.js +1 -1
  34. package/dist/sdk/coding-agent/core/extensions/native-extensions.d.ts.map +1 -1
  35. package/dist/sdk/coding-agent/core/extensions/native-extensions.js +11 -0
  36. package/dist/server/agent-bridge.d.ts +8 -6
  37. package/dist/server/agent-bridge.d.ts.map +1 -1
  38. package/dist/server/agent-bridge.js +28 -18
  39. package/dist/server/error-humanizer.d.ts.map +1 -1
  40. package/dist/server/error-humanizer.js +1 -1
  41. package/dist/server/wire.d.ts +2 -0
  42. package/dist/server/wire.d.ts.map +1 -1
  43. package/package.json +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"agents.d.ts","sourceRoot":"","sources":["../../src/agent/agents.ts"],"names":[],"mappings":"AAIA,MAAM,MAAM,UAAU,GAAG,MAAM,GAAG,SAAS,GAAG,MAAM,CAAC;AACrD,MAAM,MAAM,SAAS,GAAG,SAAS,GAAG,UAAU,CAAC;AAC/C,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,IAAI,EAAE,SAAS,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,YAAY,EAAE,MAAM,CAAC;IACrB,MAAM,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,CAAC;IACrC,QAAQ,EAAE,MAAM,CAAC;CAClB;AACD,MAAM,WAAW,oBAAoB;IACnC,MAAM,EAAE,WAAW,EAAE,CAAC;IACtB,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAC;CACjC;AAyDD,wBAAgB,cAAc,CAAC,MAAM,EAAE,WAAW,EAAE,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAO3E;AACD;;;;GAIG;AACH,MAAM,MAAM,uBAAuB,GAAG;IACpC,yEAAyE;IACzE,YAAY,EAAE,IAAI,CAAC;IACnB,6CAA6C;IAC7C,MAAM,EAAE,MAAM,CAAC;CAChB,GAAG;IACF,YAAY,EAAE,KAAK,CAAC;IACpB,4EAA4E;IAC5E,KAAK,EAAE,MAAM,CAAC;CACf,CAAC;AAEF,wBAAsB,kBAAkB,CAAC,IAAI,EAAE;IAC7C,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB,GAAG,OAAO,CAAC,uBAAuB,CAAC,CA8FnC;AAkBD,wBAAgB,cAAc,CAC5B,GAAG,EAAE,MAAM,EACX,KAAK,EAAE,UAAU,EACjB,QAAQ,CAAC,EAAE,MAAM,EACjB,MAAM,CAAC,EAAE,MAAM,GACd,oBAAoB,CAkBtB;AACD,wBAAgB,oBAAoB,CAClC,GAAG,EAAE,MAAM,EACX,KAAK,GAAE,UAAmB,EAC1B,QAAQ,CAAC,EAAE,MAAM,EACjB,MAAM,CAAC,EAAE,MAAM,GACd,WAAW,GAAG,IAAI,CAepB;AACD;;;;;;;;GAQG;AACH,wBAAgB,2BAA2B,CACzC,YAAY,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,EACvC,GAAG,EAAE,MAAM,EACX,QAAQ,CAAC,EAAE,MAAM,EACjB,MAAM,CAAC,EAAE,MAAM,GACd,WAAW,GAAG,IAAI,CAgBpB;AACD;;;;;;GAMG;AACH,wBAAgB,wBAAwB,CACtC,IAAI,EAAE,MAAM,GAAG,SAAS,EACxB,YAAY,EAAE,IAAI,CAChB,WAAW,EACX,MAAM,GAAG,aAAa,GAAG,cAAc,GAAG,OAAO,GAAG,QAAQ,CAC7D,GAAG,IAAI,GACP,MAAM,GAAG,SAAS,CAQpB;AAED;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAChC,KAAK,EAAE,IAAI,CACT,WAAW,EACX,MAAM,GAAG,aAAa,GAAG,cAAc,GAAG,OAAO,GAAG,QAAQ,CAC7D,GAAG,IAAI,GACP,MAAM,CAcR"}
1
+ {"version":3,"file":"agents.d.ts","sourceRoot":"","sources":["../../src/agent/agents.ts"],"names":[],"mappings":"AAUA,MAAM,MAAM,UAAU,GAAG,MAAM,GAAG,SAAS,GAAG,MAAM,CAAC;AACrD,MAAM,MAAM,SAAS,GAAG,SAAS,GAAG,UAAU,CAAC;AAC/C,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,IAAI,EAAE,SAAS,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,YAAY,EAAE,MAAM,CAAC;IACrB,MAAM,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,CAAC;IACrC,QAAQ,EAAE,MAAM,CAAC;CAClB;AACD,MAAM,WAAW,oBAAoB;IACnC,MAAM,EAAE,WAAW,EAAE,CAAC;IACtB,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAC;CACjC;AAyDD,wBAAgB,cAAc,CAAC,MAAM,EAAE,WAAW,EAAE,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAO3E;AACD;;;;GAIG;AACH,MAAM,MAAM,uBAAuB,GAAG;IACpC,yEAAyE;IACzE,YAAY,EAAE,IAAI,CAAC;IACnB,6CAA6C;IAC7C,MAAM,EAAE,MAAM,CAAC;CAChB,GAAG;IACF,YAAY,EAAE,KAAK,CAAC;IACpB,4EAA4E;IAC5E,KAAK,EAAE,MAAM,CAAC;CACf,CAAC;AAEF,wBAAsB,kBAAkB,CAAC,IAAI,EAAE;IAC7C,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB,GAAG,OAAO,CAAC,uBAAuB,CAAC,CA2EnC;AAkBD,wBAAgB,cAAc,CAC5B,GAAG,EAAE,MAAM,EACX,KAAK,EAAE,UAAU,EACjB,QAAQ,CAAC,EAAE,MAAM,EACjB,MAAM,CAAC,EAAE,MAAM,GACd,oBAAoB,CAkBtB;AACD,wBAAgB,oBAAoB,CAClC,GAAG,EAAE,MAAM,EACX,KAAK,GAAE,UAAmB,EAC1B,QAAQ,CAAC,EAAE,MAAM,EACjB,MAAM,CAAC,EAAE,MAAM,GACd,WAAW,GAAG,IAAI,CAepB;AACD;;;;;;;;GAQG;AACH,wBAAgB,2BAA2B,CACzC,YAAY,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,EACvC,GAAG,EAAE,MAAM,EACX,QAAQ,CAAC,EAAE,MAAM,EACjB,MAAM,CAAC,EAAE,MAAM,GACd,WAAW,GAAG,IAAI,CAgBpB;AACD;;;;;;GAMG;AACH,wBAAgB,wBAAwB,CACtC,IAAI,EAAE,MAAM,GAAG,SAAS,EACxB,YAAY,EAAE,IAAI,CAChB,WAAW,EACX,MAAM,GAAG,aAAa,GAAG,cAAc,GAAG,OAAO,GAAG,QAAQ,CAC7D,GAAG,IAAI,GACP,MAAM,GAAG,SAAS,CAQpB;AAED;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAChC,KAAK,EAAE,IAAI,CACT,WAAW,EACX,MAAM,GAAG,aAAa,GAAG,cAAc,GAAG,OAAO,GAAG,QAAQ,CAC7D,GAAG,IAAI,GACP,MAAM,CAcR"}
@@ -2,6 +2,7 @@ import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
3
  import chalk from "chalk";
4
4
  import { getAgentDir, parseFrontmatter } from "../sdk/coding-agent/index.js";
5
+ import { classifyGraphqlError, createBackendClient, graphqlErrorMessage, scalars, } from "../generated/graphql-client.js";
5
6
  function loadAgentsFromDir(dir, source) {
6
7
  const agents = [];
7
8
  if (!fs.existsSync(dir))
@@ -70,48 +71,25 @@ export async function refreshCloudAgents(opts) {
70
71
  let refresh;
71
72
  refresh = (async () => {
72
73
  try {
73
- const response = await fetch(`${opts.backendUrl.replace(/\/$/, "")}/graphql`, {
74
- method: "POST",
75
- headers: {
76
- "content-type": "application/json",
77
- authorization: `Bearer ${opts.machineJwt}`,
78
- "x-team-id": teamId,
79
- },
80
- body: JSON.stringify({
81
- query: `query CloudAgents($teamId: ID!) { agents(teamId: $teamId) { name description systemPrompt model mode scope } }`,
82
- variables: { teamId },
83
- }),
74
+ const client = createBackendClient({
75
+ backendUrl: opts.backendUrl,
76
+ machineJwt: opts.machineJwt,
77
+ teamId,
84
78
  });
85
- // HTTP 401/403: the machine JWT was rejected outright (expired,
86
- // revoked, or signed with a stale secret). Surface as auth-rejected so
87
- // the caller can trigger a forced re-registration instead of looping
88
- // on the same dead token.
89
- if (response.status === 401 || response.status === 403) {
90
- const reason = `Cloud agents: backend rejected machine JWT (HTTP ${response.status})`;
91
- console.warn(reason);
92
- return { authRejected: true, reason };
93
- }
94
- if (!response.ok) {
95
- console.warn(`Cloud agents: backend returned HTTP ${response.status}`);
96
- return { authRejected: false, count: 0 };
97
- }
98
- const payload = (await response.json());
99
- // GraphQL commonly returns 200 with an `errors` array when auth fails
100
- // (e.g. "Authentication required", "Not authorized for this team").
101
- // Treat any auth-flavored error as auth-rejected so the caller can
102
- // recover; other GraphQL errors are non-fatal (no agents loaded).
103
- if (payload.errors && payload.errors.length > 0) {
104
- const msg = payload.errors.map((e) => e.message ?? "").join("; ");
105
- const looksLikeAuth = /authentication required|not authorized|unauthor/i.test(msg);
106
- const reason = `Cloud agents: backend rejected machine JWT (${msg})`;
107
- if (looksLikeAuth) {
108
- console.warn(reason);
109
- return { authRejected: true, reason };
110
- }
111
- console.warn(reason);
112
- return { authRejected: false, count: 0 };
113
- }
114
- const fetched = payload.data?.agents ?? [];
79
+ const result = await client("query", { scalars })({
80
+ agents: [
81
+ { teamId },
82
+ {
83
+ name: true,
84
+ description: true,
85
+ systemPrompt: true,
86
+ model: true,
87
+ mode: true,
88
+ scope: true,
89
+ },
90
+ ],
91
+ });
92
+ const fetched = result.agents ?? [];
115
93
  const agents = fetched
116
94
  .filter((a) => typeof a.name === "string" &&
117
95
  typeof a.description === "string" &&
@@ -133,8 +111,18 @@ export async function refreshCloudAgents(opts) {
133
111
  return { authRejected: false, count: agents.length };
134
112
  }
135
113
  catch (err) {
114
+ // Zeus surfaces a GraphQL `errors` array as `GraphQLError` (HTTP 200)
115
+ // and non-2xx responses as an error too. Treat auth-flavored failures
116
+ // as auth-rejected so the caller can trigger a forced re-registration
117
+ // instead of looping on the same dead token.
118
+ const classified = classifyGraphqlError(err);
119
+ if (classified.authRejected) {
120
+ const reason = `Cloud agents: backend rejected machine JWT (${classified.message})`;
121
+ console.warn(reason);
122
+ return { authRejected: true, reason };
123
+ }
136
124
  // Network errors are non-fatal — local agents remain available offline.
137
- const msg = err instanceof Error ? err.message : String(err);
125
+ const msg = graphqlErrorMessage(err);
138
126
  console.warn(`Cloud agents: refresh failed (${msg})`);
139
127
  return { authRejected: false, count: 0 };
140
128
  }
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/agent/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAEH,OAAO,KAAK,EAIX,SAAS,EAET,MAAM,4BAA4B,CAAC;AAUpC,OAAO,EACN,KAAK,YAAY,EACjB,KAAK,gBAAgB,EACrB,MAAM,8BAA8B,CAAC;AAGtC,OAAO,EACN,KAAK,WAAW,EAKhB,MAAM,aAAa,CAAC;AAErB,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAOrD,OAAO,KAAK,EAAE,gBAAgB,IAAI,oBAAoB,EAAE,MAAM,8CAA8C,CAAC;AAsG7G;;;;;;;;;GASG;AACH,wBAAgB,yBAAyB,CACxC,gBAAgB,EAAE,MAAM,GAAG,SAAS,EACpC,KAAK,EAAE,IAAI,CAAC,WAAW,EAAE,MAAM,GAAG,aAAa,GAAG,cAAc,GAAG,OAAO,GAAG,QAAQ,CAAC,GACpF,MAAM,CAYR;AAgID;;;GAGG;AACH,wBAAgB,oCAAoC,CACnD,cAAc,EAAE,gBAAgB,CAAC,gBAAgB,CAAC,GAChD,IAAI,CAAC,oBAAoB,CAAC,gBAAgB,CAAC,EAAE,WAAW,CAAC,CAK3D;AAED,wBAAgB,oBAAoB,CACnC,SAAS,EAAE,MAAM,EAAE,GAAG,SAAS,EAC/B,GAAG,EAAE,MAAM,EACX,GAAG,EAAE,YAAY,EACjB,GAAG,EAAE,gBAAgB,GACnB,SAAS,EAAE,CAwCb;AAkbD,MAAM,CAAC,OAAO,UAAU,iBAAiB,CACxC,GAAG,EAAE,YAAY,EACjB,IAAI,CAAC,EAAE;IACN,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;IAC1B,iBAAiB,CAAC,EAAE,CAAC,KAAK,EAAE,WAAW,KAAK,IAAI,CAAC;IACjD,MAAM,CAAC,EAAE,MAAM,CAAC;CAChB,GACC,IAAI,CAgaN"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/agent/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAEH,OAAO,KAAK,EAIX,SAAS,EAET,MAAM,4BAA4B,CAAC;AAUpC,OAAO,EACN,KAAK,YAAY,EACjB,KAAK,gBAAgB,EACrB,MAAM,8BAA8B,CAAC;AAGtC,OAAO,EACN,KAAK,WAAW,EAKhB,MAAM,aAAa,CAAC;AAErB,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAOrD,OAAO,KAAK,EAAE,gBAAgB,IAAI,oBAAoB,EAAE,MAAM,8CAA8C,CAAC;AAsG7G;;;;;;;;;GASG;AACH,wBAAgB,yBAAyB,CACxC,gBAAgB,EAAE,MAAM,GAAG,SAAS,EACpC,KAAK,EAAE,IAAI,CAAC,WAAW,EAAE,MAAM,GAAG,aAAa,GAAG,cAAc,GAAG,OAAO,GAAG,QAAQ,CAAC,GACpF,MAAM,CAYR;AAkID;;;GAGG;AACH,wBAAgB,oCAAoC,CACnD,cAAc,EAAE,gBAAgB,CAAC,gBAAgB,CAAC,GAChD,IAAI,CAAC,oBAAoB,CAAC,gBAAgB,CAAC,EAAE,WAAW,CAAC,CAK3D;AAED,wBAAgB,oBAAoB,CACnC,SAAS,EAAE,MAAM,EAAE,GAAG,SAAS,EAC/B,GAAG,EAAE,MAAM,EACX,GAAG,EAAE,YAAY,EACjB,GAAG,EAAE,gBAAgB,GACnB,SAAS,EAAE,CAwCb;AAicD,MAAM,CAAC,OAAO,UAAU,iBAAiB,CACxC,GAAG,EAAE,YAAY,EACjB,IAAI,CAAC,EAAE;IACN,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;IAC1B,iBAAiB,CAAC,EAAE,CAAC,KAAK,EAAE,WAAW,KAAK,IAAI,CAAC;IACjD,MAAM,CAAC,EAAE,MAAM,CAAC;CAChB,GACC,IAAI,CAgaN"}
@@ -420,12 +420,16 @@ async function runSingleAgent(defaultCwd, agents, agentName, task, cwd, step, si
420
420
  // Accumulated thinking deltas for the live subagent sheet. This stays outside
421
421
  // final `SingleResult` so the public subagent tool_result contract is unchanged.
422
422
  let thinkingText = "";
423
+ // Responses API reasoning metadata that lets the UI classify accumulated
424
+ // subagent thinking as activity (mirrors the main-session thinking metadata).
425
+ let thinkingMetadata;
423
426
  const emitUpdate = () => {
424
427
  if (onUpdate) {
425
428
  const liveResult = {
426
429
  ...currentResult,
427
430
  ...(streamingText ? { streamingText } : {}),
428
431
  ...(thinkingText ? { thinkingText } : {}),
432
+ ...(thinkingMetadata ? { thinkingMetadata } : {}),
429
433
  };
430
434
  onUpdate({
431
435
  content: [{ type: "text", text: getFinalOutput(currentResult.messages) || streamingText || (thinkingText ? "(thinking...)" : "(running...)") }],
@@ -489,6 +493,9 @@ async function runSingleAgent(defaultCwd, agents, agentName, task, cwd, step, si
489
493
  }
490
494
  else if (inner?.type === "thinking_delta" && inner.delta) {
491
495
  thinkingText += inner.delta;
496
+ if (inner.metadata) {
497
+ thinkingMetadata = { ...(thinkingMetadata ?? {}), ...inner.metadata };
498
+ }
492
499
  emitUpdate();
493
500
  }
494
501
  continue;
package/dist/cli.js CHANGED
@@ -36,6 +36,7 @@ function printHeader() {
36
36
  " spectral bind Link this directory to an Aexol Studio project",
37
37
  " spectral unbind Remove the Aexol Studio project binding",
38
38
  " spectral update Check for and install the latest Spectral version",
39
+ " spectral ports Diagnose port collisions across local projects",
39
40
  " spectral --version Print version and exit",
40
41
  " spectral --help Print this help and exit",
41
42
  "",
@@ -113,6 +114,11 @@ async function main() {
113
114
  await runUpdate(args.slice(1));
114
115
  process.exit(0);
115
116
  }
117
+ if (first === "ports") {
118
+ const { runPorts } = await import("./commands/ports.js");
119
+ await runPorts(args.slice(1));
120
+ process.exit(0);
121
+ }
116
122
  // No TUI/interactive mode is supported. All other invocations must use
117
123
  // explicit subcommands.
118
124
  process.stderr.write(`spectral: unknown command "${first ?? ""}".\n` +
@@ -0,0 +1,13 @@
1
+ /**
2
+ * `spectral ports` — diagnose port collisions across local projects.
3
+ *
4
+ * Usage:
5
+ * spectral ports list every listening TCP port + owner
6
+ * spectral ports check <p>... check specific ports and suggest free ones
7
+ * spectral ports free [start] find the first free port from `start`
8
+ *
9
+ * Shares the exact same platform logic as the native `ports` extension, so
10
+ * results are identical whether run from the shell or from inside the agent.
11
+ */
12
+ export declare function runPorts(args: string[]): Promise<void>;
13
+ //# sourceMappingURL=ports.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ports.d.ts","sourceRoot":"","sources":["../../src/commands/ports.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AA0CH,wBAAsB,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CA0E5D"}
@@ -0,0 +1,110 @@
1
+ /**
2
+ * `spectral ports` — diagnose port collisions across local projects.
3
+ *
4
+ * Usage:
5
+ * spectral ports list every listening TCP port + owner
6
+ * spectral ports check <p>... check specific ports and suggest free ones
7
+ * spectral ports free [start] find the first free port from `start`
8
+ *
9
+ * Shares the exact same platform logic as the native `ports` extension, so
10
+ * results are identical whether run from the shell or from inside the agent.
11
+ */
12
+ import pc from "picocolors";
13
+ import { findFreePort, isPortFree, knownPortLabel, scanPorts, } from "../extensions/ports/platform.js";
14
+ function renderTable(entries) {
15
+ if (entries.length === 0)
16
+ return "No listening TCP ports found.";
17
+ const dedup = new Map();
18
+ for (const e of entries) {
19
+ const existing = dedup.get(e.port);
20
+ if (!existing || (existing.address === "0.0.0.0" && e.address !== "0.0.0.0")) {
21
+ dedup.set(e.port, { address: e.address, process: e.process, pid: e.pid });
22
+ }
23
+ }
24
+ const rows = [...dedup.entries()].sort((a, b) => a[0] - b[0]);
25
+ const portW = Math.max(5, ...rows.map(([p]) => String(p).length));
26
+ const lines = [
27
+ `${rows.length} listening TCP port(s):`,
28
+ "",
29
+ `${"Port".padStart(portW)} ${"Bind".padEnd(22)} Process`,
30
+ `${"─".repeat(portW)} ${"─".repeat(22)} ${"─".repeat(30)}`,
31
+ ];
32
+ for (const [port, info] of rows) {
33
+ const known = knownPortLabel(port);
34
+ const label = known ? ` ${known}` : "";
35
+ const owner = info.process ? `${info.process}${info.pid !== null ? ` (pid ${info.pid})` : ""}` : "unknown";
36
+ const bind = info.address === "0.0.0.0" || info.address === "::" || info.address === "::1"
37
+ ? "all" : info.address;
38
+ lines.push(`${String(port).padStart(portW)} ${bind.padEnd(22)} ${owner}${label}`);
39
+ }
40
+ return lines.join("\n");
41
+ }
42
+ export async function runPorts(args) {
43
+ const sub = args[0];
44
+ if (sub === "check") {
45
+ const ports = args
46
+ .slice(1)
47
+ .map((p) => Number(p))
48
+ .filter((p) => Number.isInteger(p) && p > 0);
49
+ if (ports.length === 0) {
50
+ process.stderr.write(pc.red("Usage: spectral ports check <port> [<port>...]\n"));
51
+ process.exit(1);
52
+ }
53
+ const scan = await scanPorts();
54
+ const owners = new Map();
55
+ for (const e of scan.entries) {
56
+ if (!owners.has(e.port))
57
+ owners.set(e.port, { process: e.process, pid: e.pid });
58
+ }
59
+ for (const port of ports) {
60
+ const owner = owners.get(port);
61
+ const free = owner === undefined && (await isPortFree(port));
62
+ if (free) {
63
+ process.stdout.write(pc.green(`✓ ${port} is free\n`));
64
+ }
65
+ else {
66
+ const who = owner?.process ? `${owner.process}${owner.pid !== null ? ` (pid ${owner.pid})` : ""}` : "already bound";
67
+ const label = knownPortLabel(port) ? ` — ${knownPortLabel(port)}` : "";
68
+ const alternative = await findFreePort(port);
69
+ process.stdout.write(pc.red(`✗ ${port} is taken by ${who}${label}\n`));
70
+ process.stdout.write(pc.dim(` first free port from ${port}: ${alternative ?? "none found"}\n`));
71
+ }
72
+ }
73
+ return;
74
+ }
75
+ if (sub === "free") {
76
+ const start = Number(args[1]) || 3000;
77
+ const port = await findFreePort(start);
78
+ if (port === null) {
79
+ process.stderr.write(pc.red(`No free port found from ${start}\n`));
80
+ process.exit(1);
81
+ }
82
+ const label = knownPortLabel(port) ? ` (usually: ${knownPortLabel(port)})` : "";
83
+ process.stdout.write(pc.green(`✓ First free port from ${start}: ${port}${label}\n`));
84
+ return;
85
+ }
86
+ if (sub === "--help" || sub === "-h") {
87
+ process.stdout.write([
88
+ "Usage:",
89
+ " spectral ports list listening TCP ports + owners",
90
+ " spectral ports check <p>... check specific ports and suggest free ones",
91
+ " spectral ports free [start] find first free port from `start` (default 3000)",
92
+ "",
93
+ ].join("\n"));
94
+ process.exit(0);
95
+ }
96
+ // Default: full scan.
97
+ if (sub === undefined) {
98
+ try {
99
+ const scan = await scanPorts();
100
+ process.stdout.write(`${renderTable(scan.entries)}\n`);
101
+ }
102
+ catch (err) {
103
+ process.stderr.write(pc.red(`Port scan failed: ${err instanceof Error ? err.message : String(err)}\n`));
104
+ process.exit(1);
105
+ }
106
+ return;
107
+ }
108
+ process.stderr.write(pc.red(`Unknown ports subcommand: ${sub}\n`));
109
+ process.exit(1);
110
+ }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Ports — native spectral extension.
3
+ *
4
+ * Helps resolve the classic "many projects on one machine" problem where
5
+ * Postgres, NATS, Debezium, MinIO, Redis, etc. collide on the same ports.
6
+ *
7
+ * Registers three read-only/free tools:
8
+ * - ports_scan list every listening TCP port + owning process
9
+ * - ports_check check specific ports (status + owner + suggestion)
10
+ * - ports_find_free find the first free port from a starting point
11
+ *
12
+ * No external dependencies. On macOS we parse `lsof`, on Linux `ss`/`netstat`,
13
+ * and on Windows `netstat`. Port reservation for CI/docker is left to the
14
+ * user's compose/env files; this extension is the diagnostic + allocation
15
+ * layer that gives the agent the facts it needs to fix collisions.
16
+ */
17
+ import type { ExtensionAPI } from "../../sdk/coding-agent/index.js";
18
+ export default function portsExtension(ext: ExtensionAPI): Promise<void>;
19
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/extensions/ports/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAIH,OAAO,KAAK,EAAmB,YAAY,EAAkB,MAAM,iCAAiC,CAAC;AAuDrG,wBAA8B,cAAc,CAAC,GAAG,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CA+F7E"}
@@ -0,0 +1,154 @@
1
+ /**
2
+ * Ports — native spectral extension.
3
+ *
4
+ * Helps resolve the classic "many projects on one machine" problem where
5
+ * Postgres, NATS, Debezium, MinIO, Redis, etc. collide on the same ports.
6
+ *
7
+ * Registers three read-only/free tools:
8
+ * - ports_scan list every listening TCP port + owning process
9
+ * - ports_check check specific ports (status + owner + suggestion)
10
+ * - ports_find_free find the first free port from a starting point
11
+ *
12
+ * No external dependencies. On macOS we parse `lsof`, on Linux `ss`/`netstat`,
13
+ * and on Windows `netstat`. Port reservation for CI/docker is left to the
14
+ * user's compose/env files; this extension is the diagnostic + allocation
15
+ * layer that gives the agent the facts it needs to fix collisions.
16
+ */
17
+ import { Type } from "typebox";
18
+ import { knownPortLabel, scanPorts, isPortFree, findFreePort } from "./platform.js";
19
+ function errorResult(msg) {
20
+ return { content: [{ type: "text", text: `❌ ${msg}` }], details: { isError: true, error: msg } };
21
+ }
22
+ function renderScan(entries) {
23
+ if (entries.length === 0)
24
+ return "No listening TCP ports found.";
25
+ const dedup = new Map();
26
+ for (const e of entries) {
27
+ // Prefer a non-wildcard address when the same port appears twice (IPv4 + IPv6).
28
+ const existing = dedup.get(e.port);
29
+ if (!existing || (existing.address === "0.0.0.0" && e.address !== "0.0.0.0")) {
30
+ dedup.set(e.port, { address: e.address, process: e.process, pid: e.pid });
31
+ }
32
+ }
33
+ const rows = [...dedup.entries()].sort((a, b) => a[0] - b[0]);
34
+ const portW = Math.max(5, ...rows.map(([p]) => String(p).length));
35
+ const rowsText = rows.map(([port, info]) => {
36
+ const known = knownPortLabel(port);
37
+ const label = known ? ` ${known}` : "";
38
+ const owner = info.process ? `${info.process}${info.pid !== null ? ` (pid ${info.pid})` : ""}` : "unknown";
39
+ const bind = info.address === "0.0.0.0" || info.address === "::" || info.address === "::1"
40
+ ? "all" : info.address;
41
+ return `${String(port).padStart(portW)} ${bind.padEnd(22)} ${owner}${label}`;
42
+ });
43
+ return [
44
+ `${rows.length} listening TCP port(s):`,
45
+ "",
46
+ `${"Port".padStart(portW)} ${"Bind".padEnd(22)} Process`,
47
+ `${"─".repeat(portW)} ${"─".repeat(22)} ${"─".repeat(30)}`,
48
+ ...rowsText,
49
+ ].join("\n");
50
+ }
51
+ const scanSchema = Type.Object({});
52
+ const checkSchema = Type.Object({
53
+ ports: Type.Array(Type.Number(), { description: "Port numbers to check (e.g. [5432, 4222, 9000])." }),
54
+ host: Type.Optional(Type.String({ default: "0.0.0.0", description: "Bind host to test for free ports. Default: 0.0.0.0." })),
55
+ });
56
+ const findSchema = Type.Object({
57
+ start: Type.Optional(Type.Number({ default: 3000, description: "Start searching from this port. Default: 3000." })),
58
+ host: Type.Optional(Type.String({ default: "0.0.0.0", description: "Bind host to test. Default: 0.0.0.0." })),
59
+ max: Type.Optional(Type.Number({ default: 65535, description: "Upper bound. Default: 65535." })),
60
+ });
61
+ export default async function portsExtension(ext) {
62
+ const scanTool = {
63
+ name: "ports_scan",
64
+ label: "Scan Ports",
65
+ description: "List every listening TCP port on this machine with its bind address and owning process. " +
66
+ "Use this when services from multiple projects (Postgres, NATS, Debezium, MinIO, Redis, etc.) " +
67
+ "may be colliding on the same ports. Read-only.",
68
+ promptSnippet: "`ports_scan` — list listening TCP ports and their owning processes",
69
+ parameters: scanSchema,
70
+ async execute(_toolCallId, _params) {
71
+ try {
72
+ const result = await scanPorts();
73
+ return {
74
+ content: [{ type: "text", text: renderScan(result.entries) }],
75
+ details: { count: result.entries.length, platform: result.platform, command: result.command },
76
+ };
77
+ }
78
+ catch (err) {
79
+ return errorResult(err instanceof Error ? err.message : String(err));
80
+ }
81
+ },
82
+ };
83
+ const checkTool = {
84
+ name: "ports_check",
85
+ label: "Check Ports",
86
+ description: "Check whether specific TCP ports are free or taken. Returns the owner process for taken ports " +
87
+ "and the first free alternative for requested ones. Use before starting another project's stack.",
88
+ promptSnippet: "`ports_check` — check status of specific ports and suggest free alternatives",
89
+ parameters: checkSchema,
90
+ async execute(_toolCallId, params) {
91
+ const ports = params.ports.map((p) => Number(p)).filter((p) => Number.isInteger(p) && p > 0);
92
+ if (ports.length === 0)
93
+ return errorResult("Provide at least one valid port in `ports`.");
94
+ const host = params.host ?? "0.0.0.0";
95
+ try {
96
+ const scan = await scanPorts();
97
+ const owners = new Map();
98
+ for (const e of scan.entries) {
99
+ if (!owners.has(e.port))
100
+ owners.set(e.port, { process: e.process, pid: e.pid });
101
+ }
102
+ const lines = [];
103
+ for (const port of ports) {
104
+ const owner = owners.get(port);
105
+ const free = owner === undefined && (await isPortFree(port, host));
106
+ if (free) {
107
+ lines.push(`✓ ${port} is free (bindable on ${host}).`);
108
+ }
109
+ else {
110
+ const who = owner?.process ? `${owner.process}${owner.pid !== null ? ` (pid ${owner.pid})` : ""}` : "already bound";
111
+ const label = knownPortLabel(port) ? ` — ${knownPortLabel(port)}` : "";
112
+ const alternative = await findFreePort(port, 65535, host);
113
+ lines.push(`✗ ${port} is taken by ${who}${label}. First free port from ${port}: ${alternative ?? "none found"}.`);
114
+ }
115
+ }
116
+ return { content: [{ type: "text", text: lines.join("\n") }], details: { ports, host } };
117
+ }
118
+ catch (err) {
119
+ return errorResult(err instanceof Error ? err.message : String(err));
120
+ }
121
+ },
122
+ };
123
+ const findTool = {
124
+ name: "ports_find_free",
125
+ label: "Find Free Port",
126
+ description: "Find the first free TCP port at or after a starting port. Useful when allocating a per-project " +
127
+ "offset for a new stack without colliding with services already running on this machine.",
128
+ promptSnippet: "`ports_find_free` — find the first free port from a starting point",
129
+ parameters: findSchema,
130
+ async execute(_toolCallId, params) {
131
+ const start = Number(params.start) || 3000;
132
+ const max = Number(params.max) || 65535;
133
+ const host = params.host ?? "0.0.0.0";
134
+ try {
135
+ const port = await findFreePort(start, max, host);
136
+ if (port === null) {
137
+ return { content: [{ type: "text", text: `No free port found between ${start} and ${max} on ${host}.` }], details: { start, max, host } };
138
+ }
139
+ const label = knownPortLabel(port) ? ` (usually: ${knownPortLabel(port)})` : "";
140
+ return {
141
+ content: [{ type: "text", text: `✓ First free port from ${start} on ${host}: ${port}${label}.` }],
142
+ details: { port, start, max, host },
143
+ };
144
+ }
145
+ catch (err) {
146
+ return errorResult(err instanceof Error ? err.message : String(err));
147
+ }
148
+ },
149
+ };
150
+ ext.registerTool(scanTool);
151
+ ext.registerTool(checkTool);
152
+ ext.registerTool(findTool);
153
+ process.stderr.write(`[ports] Registered ports_scan + ports_check + ports_find_free (platform: ${process.platform}).\n`);
154
+ }
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Cross-platform port introspection for the ports extension.
3
+ *
4
+ * Zero external dependencies. We shell out to the OS's native tooling and
5
+ * parse its output, so a machine with many projects/services can be inspected
6
+ * without requiring `lsof` from JS bindings or admin privileges.
7
+ *
8
+ * Detection order per platform:
9
+ * - darwin: lsof -nP -iTCP -sTCP:LISTEN
10
+ * - linux: ss -ltnp (fallback: netstat -ltnp)
11
+ * - win32: netstat -ano (pids are resolved in a second best-effort pass)
12
+ */
13
+ export interface PortEntry {
14
+ port: number;
15
+ protocol: "tcp" | "udp";
16
+ address: string;
17
+ pid: number | null;
18
+ process: string | null;
19
+ }
20
+ export interface PortScanResult {
21
+ platform: string;
22
+ command: string;
23
+ entries: PortEntry[];
24
+ }
25
+ /** Well-known services so the report can label ports humans recognize. */
26
+ export declare const KNOWN_PORTS: Record<number, string>;
27
+ export declare function knownPortLabel(port: number): string | undefined;
28
+ export declare function scanPorts(): Promise<PortScanResult>;
29
+ /**
30
+ * Check a single port using a real bind attempt, which is the only reliable
31
+ * cross-platform way to know whether a port is actually free (a service may be
32
+ * listening only on a specific interface, so a "scan" alone is not enough).
33
+ */
34
+ export declare function isPortFree(port: number, host?: string): Promise<boolean>;
35
+ /**
36
+ * Find the first free port at or after `start`, bounded by `max`.
37
+ */
38
+ export declare function findFreePort(start: number, max?: number, host?: string): Promise<number | null>;
39
+ //# sourceMappingURL=platform.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"platform.d.ts","sourceRoot":"","sources":["../../../src/extensions/ports/platform.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAQH,MAAM,WAAW,SAAS;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,KAAK,GAAG,KAAK,CAAC;IACxB,OAAO,EAAE,MAAM,CAAC;IAChB,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IACnB,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;CACvB;AAED,MAAM,WAAW,cAAc;IAC9B,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,SAAS,EAAE,CAAC;CACrB;AAED,0EAA0E;AAC1E,eAAO,MAAM,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAuBrC,CAAC;AAEX,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAE/D;AA+JD,wBAAsB,SAAS,IAAI,OAAO,CAAC,cAAc,CAAC,CA+BzD;AAED;;;;GAIG;AACH,wBAAsB,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,SAAY,GAAG,OAAO,CAAC,OAAO,CAAC,CAWjF;AAED;;GAEG;AACH,wBAAsB,YAAY,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,SAAQ,EAAE,IAAI,SAAY,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAKvG"}