@groeponline/pi-wishcraft 0.22.2 → 0.23.1
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/CHANGELOG.md +11 -0
- package/package.json +1 -1
- package/src/extension/core/status-export.ts +11 -0
- package/src/extension/session/read-hints.ts +147 -0
- package/src/extension/session/session-lifecycle.ts +12 -4
- package/src/extension/settings/wishcraft-config.ts +1 -0
- package/src/extension/skills/skill-registry.ts +6 -0
- package/src/extension/skills/skill-status.ts +37 -0
- package/src/extension/ui/powerline-menu.ts +0 -7
- package/src/segments/custom.ts +3 -1
- package/src/segments/registry.ts +19 -11
- package/src/segments/system.ts +19 -5
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [0.23.1] - 2026-08-20
|
|
6
|
+
|
|
7
|
+
## [0.23.0] - 2026-08-20
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
- `powerline.skills.count` status export, published on session start and after skill-cache invalidation.
|
|
11
|
+
- Read-tool continuation hints on partial core `read` results when the core output omits a range or offset summary.
|
|
12
|
+
|
|
13
|
+
### Changed
|
|
14
|
+
- Powerline Status menu keeps only ports, TPS, and toggle (removed unimplemented diagnostics entries).
|
|
15
|
+
|
|
5
16
|
## [0.22.2] - 2026-08-20
|
|
6
17
|
|
|
7
18
|
### Fixed
|
package/package.json
CHANGED
|
@@ -9,6 +9,7 @@ export const POWERLINE_STATUS_KEYS = {
|
|
|
9
9
|
preset: "powerline.preset",
|
|
10
10
|
tps: "powerline.tps",
|
|
11
11
|
ports: "powerline.ports",
|
|
12
|
+
skillsCount: "powerline.skills.count",
|
|
12
13
|
} as const;
|
|
13
14
|
|
|
14
15
|
/** Keys never rendered in powerline's own bar (they exist for other extensions). */
|
|
@@ -23,6 +24,8 @@ export interface PowerlineStatusSnapshot {
|
|
|
23
24
|
tps?: string | undefined;
|
|
24
25
|
/** open_ports count as text (`?` when a fleet host probe failed). */
|
|
25
26
|
ports?: string;
|
|
27
|
+
/** Skill catalog length as a decimal string. */
|
|
28
|
+
skillsCount?: string;
|
|
26
29
|
}
|
|
27
30
|
|
|
28
31
|
type StatusPublisherCtx = {
|
|
@@ -47,9 +50,17 @@ export function buildPowerlineStatusExport(
|
|
|
47
50
|
if ("ports" in snapshot) {
|
|
48
51
|
entries.push([POWERLINE_STATUS_KEYS.ports, snapshot.ports]);
|
|
49
52
|
}
|
|
53
|
+
if ("skillsCount" in snapshot) {
|
|
54
|
+
entries.push([POWERLINE_STATUS_KEYS.skillsCount, snapshot.skillsCount]);
|
|
55
|
+
}
|
|
50
56
|
return entries;
|
|
51
57
|
}
|
|
52
58
|
|
|
59
|
+
/** Format a skill-catalog length for the status export. */
|
|
60
|
+
export function formatSkillsCountStatusValue(count: number): string {
|
|
61
|
+
return String(count);
|
|
62
|
+
}
|
|
63
|
+
|
|
53
64
|
/** Publish only the snapshot keys present, via `ctx.ui.setStatus`. */
|
|
54
65
|
export function publishPowerlineStatuses(
|
|
55
66
|
ctx: StatusPublisherCtx | null | undefined,
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { readSettings } from "../settings/settings-io.ts";
|
|
2
|
+
|
|
3
|
+
export interface ReadToolInput {
|
|
4
|
+
offset?: number;
|
|
5
|
+
limit?: number;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface ReadHintDetails {
|
|
9
|
+
truncation?: {
|
|
10
|
+
totalLines?: number;
|
|
11
|
+
outputLines?: number;
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const CORE_SHOWING_LINES =
|
|
16
|
+
/^\[Showing lines \d+[–-]\d+(?: of \d+)?\.(?: Use offset=\d+ to continue\.)?\]$/i;
|
|
17
|
+
const CORE_MORE_LINES =
|
|
18
|
+
/^\[?\d+ more lines in file\. Use offset=\d+ to continue\.?\]?$/i;
|
|
19
|
+
const OWN_READ_HINT = /^\d+ lines, showing \d+[–-]\d+, next offset \d+$/;
|
|
20
|
+
|
|
21
|
+
function isCoreRangeFooter(line: string): boolean {
|
|
22
|
+
const footer = line.trim();
|
|
23
|
+
return (
|
|
24
|
+
CORE_SHOWING_LINES.test(footer) ||
|
|
25
|
+
CORE_MORE_LINES.test(footer) ||
|
|
26
|
+
OWN_READ_HINT.test(footer)
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function coreFooter(text: string): string {
|
|
31
|
+
const lines = text.split("\n");
|
|
32
|
+
while (lines.length > 0 && lines[lines.length - 1]!.trim() === "") lines.pop();
|
|
33
|
+
return lines[lines.length - 1]?.trim() ?? "";
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Default on. `wishcraft.readHints: false` is the opt-out. */
|
|
37
|
+
export function readHintsEnabled(wishcraftSettings: unknown): boolean {
|
|
38
|
+
if (
|
|
39
|
+
!wishcraftSettings ||
|
|
40
|
+
typeof wishcraftSettings !== "object" ||
|
|
41
|
+
Array.isArray(wishcraftSettings)
|
|
42
|
+
) {
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
45
|
+
return (wishcraftSettings as Record<string, unknown>).readHints !== false;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** True when core read output already carries an offset/range continuation line. */
|
|
49
|
+
export function coreReadResultHasRangeSummary(
|
|
50
|
+
text: string,
|
|
51
|
+
_details?: ReadHintDetails,
|
|
52
|
+
): boolean {
|
|
53
|
+
return isCoreRangeFooter(coreFooter(text));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function countContentLines(text: string): number {
|
|
57
|
+
const lines = text.split("\n");
|
|
58
|
+
let end = lines.length;
|
|
59
|
+
while (end > 0 && lines[end - 1] === "") end--;
|
|
60
|
+
if (end > 0 && isCoreRangeFooter(lines[end - 1]!)) end--;
|
|
61
|
+
return end;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Whether to append a one-line read continuation hint to a core read result. */
|
|
65
|
+
export function shouldAppendReadHint(
|
|
66
|
+
input: ReadToolInput | null | undefined,
|
|
67
|
+
text: string,
|
|
68
|
+
details?: ReadHintDetails,
|
|
69
|
+
): boolean {
|
|
70
|
+
if (!input) return false;
|
|
71
|
+
if (input.offset === undefined && input.limit === undefined) return false;
|
|
72
|
+
if (coreReadResultHasRangeSummary(text, details)) return false;
|
|
73
|
+
|
|
74
|
+
const lineCount = countContentLines(text);
|
|
75
|
+
if (lineCount === 0) return false;
|
|
76
|
+
|
|
77
|
+
const start = input.offset ?? 1;
|
|
78
|
+
const end = start + lineCount - 1;
|
|
79
|
+
const total = details?.truncation?.totalLines;
|
|
80
|
+
if (total !== undefined && end >= total) return false;
|
|
81
|
+
if (input.limit !== undefined && lineCount < input.limit) return false;
|
|
82
|
+
|
|
83
|
+
return true;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Format the English continuation hint for a partial read window. */
|
|
87
|
+
export function formatReadHint(
|
|
88
|
+
input: ReadToolInput,
|
|
89
|
+
text: string,
|
|
90
|
+
details?: ReadHintDetails,
|
|
91
|
+
): string {
|
|
92
|
+
const start = input.offset ?? 1;
|
|
93
|
+
const lineCount = countContentLines(text);
|
|
94
|
+
const end = start + lineCount - 1;
|
|
95
|
+
const next = end + 1;
|
|
96
|
+
const total = details?.truncation?.totalLines;
|
|
97
|
+
if (total !== undefined) {
|
|
98
|
+
return `${total} lines, showing ${start}–${end}, next offset ${next}`;
|
|
99
|
+
}
|
|
100
|
+
return `${lineCount} lines, showing ${start}–${end}, next offset ${next}`;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function extractReadToolResultText(content: unknown): string {
|
|
104
|
+
if (typeof content === "string") return content;
|
|
105
|
+
if (!Array.isArray(content)) return "";
|
|
106
|
+
return content.map((block) =>
|
|
107
|
+
block && typeof block === "object" && "text" in block
|
|
108
|
+
? String((block as { text?: unknown }).text ?? "")
|
|
109
|
+
: "",
|
|
110
|
+
).join("\n");
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Append a continuation hint to a core `read` tool_result.
|
|
115
|
+
* Returns a replacement payload; never mutates `event.input` or `event.content`.
|
|
116
|
+
*/
|
|
117
|
+
export function appendReadHintToEvent(event: {
|
|
118
|
+
input?: unknown;
|
|
119
|
+
content?: unknown;
|
|
120
|
+
details?: unknown;
|
|
121
|
+
}): { content: Array<{ type: "text"; text: string }> } | undefined {
|
|
122
|
+
const text = extractReadToolResultText(event.content);
|
|
123
|
+
const input = event.input as ReadToolInput | undefined;
|
|
124
|
+
const details = event.details as ReadHintDetails | undefined;
|
|
125
|
+
if (!shouldAppendReadHint(input, text, details) || !input) return undefined;
|
|
126
|
+
if (!Array.isArray(event.content)) return undefined;
|
|
127
|
+
const hint = formatReadHint(input, text, details);
|
|
128
|
+
return {
|
|
129
|
+
content: [
|
|
130
|
+
...(event.content as Array<{ type: "text"; text: string }>),
|
|
131
|
+
{ type: "text", text: hint },
|
|
132
|
+
],
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** Apply the opt-out and append a hint without mutating `event.input`. */
|
|
137
|
+
export function maybeAppendReadHint(
|
|
138
|
+
event: {
|
|
139
|
+
input?: unknown;
|
|
140
|
+
content?: unknown;
|
|
141
|
+
details?: unknown;
|
|
142
|
+
},
|
|
143
|
+
cwd?: string,
|
|
144
|
+
): { content: Array<{ type: "text"; text: string }> } | undefined {
|
|
145
|
+
if (!readHintsEnabled(readSettings(cwd).wishcraft)) return undefined;
|
|
146
|
+
return appendReadHintToEvent(event);
|
|
147
|
+
}
|
|
@@ -70,10 +70,12 @@ import {
|
|
|
70
70
|
import { CONTEXT_STATUS_RENDER_MS } from "../core/constants.ts";
|
|
71
71
|
import type { RuntimeState } from "../core/types.ts";
|
|
72
72
|
import { isStaleExtensionContextError } from "./stale-context.ts";
|
|
73
|
+
import { dismissWelcome, scheduleDismissWelcome } from "../welcome/welcome-control.ts";
|
|
73
74
|
import {
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
} from "../
|
|
75
|
+
bindSkillsCountPublisher,
|
|
76
|
+
clearSkillsCountPublisher,
|
|
77
|
+
} from "../skills/skill-status.ts";
|
|
78
|
+
import { maybeAppendReadHint } from "./read-hints.ts";
|
|
77
79
|
|
|
78
80
|
/**
|
|
79
81
|
* Fire the configured `powerline.costAlert` warning at most once per session.
|
|
@@ -160,6 +162,7 @@ export function registerSessionLifecycle(
|
|
|
160
162
|
): void {
|
|
161
163
|
// Track session start
|
|
162
164
|
pi.on("session_start", async (event, ctx) => {
|
|
165
|
+
clearSkillsCountPublisher();
|
|
163
166
|
rt.shellSession?.dispose();
|
|
164
167
|
rt.shellSession = null;
|
|
165
168
|
rt.sessionGeneration++;
|
|
@@ -196,6 +199,7 @@ export function registerSessionLifecycle(
|
|
|
196
199
|
|
|
197
200
|
if (ctx.hasUI) {
|
|
198
201
|
ctx.ui.setStatus("stash", undefined);
|
|
202
|
+
bindSkillsCountPublisher(ctx);
|
|
199
203
|
const pendingIdeas = rt.queueStore
|
|
200
204
|
.activeItems(getQueueContext(ctx))
|
|
201
205
|
.filter((item) => item.intent === "idea").length;
|
|
@@ -226,6 +230,7 @@ export function registerSessionLifecycle(
|
|
|
226
230
|
});
|
|
227
231
|
|
|
228
232
|
pi.on("session_shutdown", async (_event, ctx) => {
|
|
233
|
+
clearSkillsCountPublisher();
|
|
229
234
|
rt.sessionGeneration++;
|
|
230
235
|
rt.dismissWelcomeOverlay?.();
|
|
231
236
|
rt.dismissWelcomeOverlay = null;
|
|
@@ -257,7 +262,7 @@ export function registerSessionLifecycle(
|
|
|
257
262
|
});
|
|
258
263
|
|
|
259
264
|
// Invalidate git status on file changes, trigger re-render on potential branch changes
|
|
260
|
-
pi.on("tool_result", async (event) => {
|
|
265
|
+
pi.on("tool_result", async (event, ctx) => {
|
|
261
266
|
if (event.toolName === "write" || event.toolName === "edit") {
|
|
262
267
|
invalidateGitStatus();
|
|
263
268
|
requestStatusRender(rt);
|
|
@@ -266,6 +271,9 @@ export function registerSessionLifecycle(
|
|
|
266
271
|
if (event.toolName === "bash" && event.input?.command) {
|
|
267
272
|
invalidateGitForCommand(rt, String(event.input.command));
|
|
268
273
|
}
|
|
274
|
+
if (event.toolName === "read") {
|
|
275
|
+
return maybeAppendReadHint(event, ctx?.cwd);
|
|
276
|
+
}
|
|
269
277
|
});
|
|
270
278
|
|
|
271
279
|
// Also catch user escape commands (! prefix)
|
|
@@ -139,6 +139,7 @@ export function buildConfigGroups(settings: Record<string, unknown>): ConfigGrou
|
|
|
139
139
|
title: "Skills",
|
|
140
140
|
items: [
|
|
141
141
|
{ label: "Inline-expansie /command en $skill", path: "wishcraft.inlineSkills", kind: "toggle", hint: "nog niet actief zonder herstart" },
|
|
142
|
+
{ label: "Read hints", path: "wishcraft.readHints", kind: "toggle", hint: "off = no continuation hint after partial reads" },
|
|
142
143
|
],
|
|
143
144
|
},
|
|
144
145
|
{
|
|
@@ -96,6 +96,11 @@ let cachedAt = 0;
|
|
|
96
96
|
let cachedEntries: SkillEntry[] | null = null;
|
|
97
97
|
let cachedPathMap: Map<string, string> | null = null;
|
|
98
98
|
let cachedCwd: string | null = null;
|
|
99
|
+
let onCacheInvalidated: (() => void) | null = null;
|
|
100
|
+
|
|
101
|
+
export function setSkillCacheInvalidationHandler(handler: (() => void) | null): void {
|
|
102
|
+
onCacheInvalidated = handler;
|
|
103
|
+
}
|
|
99
104
|
|
|
100
105
|
const usageCache = new Map<string, SkillUsage>();
|
|
101
106
|
let usageLoaded = false;
|
|
@@ -106,6 +111,7 @@ export function invalidateSkillCache(): void {
|
|
|
106
111
|
cachedEntries = null;
|
|
107
112
|
cachedPathMap = null;
|
|
108
113
|
cachedCwd = null;
|
|
114
|
+
onCacheInvalidated?.();
|
|
109
115
|
}
|
|
110
116
|
|
|
111
117
|
/** Legacy prompts/loose-md dirs die inline-invocation altijd al scanden. */
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import {
|
|
2
|
+
formatSkillsCountStatusValue,
|
|
3
|
+
publishPowerlineStatuses,
|
|
4
|
+
} from "../core/status-export.ts";
|
|
5
|
+
import {
|
|
6
|
+
invalidateSkillCache,
|
|
7
|
+
loadSkillCatalog,
|
|
8
|
+
setSkillCacheInvalidationHandler,
|
|
9
|
+
} from "./skill-registry.ts";
|
|
10
|
+
|
|
11
|
+
type SkillsCountCtx = {
|
|
12
|
+
cwd?: string;
|
|
13
|
+
ui?: { setStatus?: (key: string, value: string | undefined) => void };
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
function publishSkillsCount(ctx: SkillsCountCtx): void {
|
|
17
|
+
publishPowerlineStatuses(ctx, {
|
|
18
|
+
skillsCount: formatSkillsCountStatusValue(
|
|
19
|
+
loadSkillCatalog(ctx.cwd ?? process.cwd()).length,
|
|
20
|
+
),
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Drop the session-scoped publisher so later invalidations cannot use a stale UI. */
|
|
25
|
+
export function clearSkillsCountPublisher(): void {
|
|
26
|
+
setSkillCacheInvalidationHandler(null);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Publish `powerline.skills.count` now and after every skill-cache invalidation.
|
|
31
|
+
* Replaces any previous session callback first.
|
|
32
|
+
*/
|
|
33
|
+
export function bindSkillsCountPublisher(ctx: SkillsCountCtx): void {
|
|
34
|
+
clearSkillsCountPublisher();
|
|
35
|
+
setSkillCacheInvalidationHandler(() => publishSkillsCount(ctx));
|
|
36
|
+
invalidateSkillCache();
|
|
37
|
+
}
|
|
@@ -21,13 +21,6 @@ export function buildPowerlineMenuItems(): PowerlineMenuNode[] {
|
|
|
21
21
|
{ id: "ports", label: "Open ports", description: "Full listening list" },
|
|
22
22
|
{ id: "tps", label: "TPS detail", description: "Live 1s window or override" },
|
|
23
23
|
{ id: "toggle", label: "Toggle powerline", description: "Use /powerline to enable or disable" },
|
|
24
|
-
{ id: "cpu", label: "CPU usage", description: "Current process CPU usage" },
|
|
25
|
-
{ id: "memory", label: "Memory usage", description: "Current process memory" },
|
|
26
|
-
{ id: "network", label: "Network status", description: "Network interface status" },
|
|
27
|
-
{ id: "uptime", label: "System uptime", description: "Host uptime" },
|
|
28
|
-
{ id: "version", label: "Version info", description: "Runtime and application versions" },
|
|
29
|
-
{ id: "logs", label: "Recent logs", description: "Show recent status messages" },
|
|
30
|
-
{ id: "diagnostics", label: "Diagnostics", description: "Run powerline diagnostics" },
|
|
31
24
|
],
|
|
32
25
|
},
|
|
33
26
|
];
|
package/src/segments/custom.ts
CHANGED
|
@@ -35,7 +35,9 @@ function runCommandCached(
|
|
|
35
35
|
commandCache.set(command, { at: now, value: out });
|
|
36
36
|
return out;
|
|
37
37
|
} catch {
|
|
38
|
-
|
|
38
|
+
// Preserve command failures so renderSegment can isolate this segment and
|
|
39
|
+
// expose a visible fault marker instead of silently hiding it.
|
|
40
|
+
throw new Error(`Command failed: ${command}`);
|
|
39
41
|
}
|
|
40
42
|
}
|
|
41
43
|
|
package/src/segments/registry.ts
CHANGED
|
@@ -164,16 +164,24 @@ export function renderSegment(
|
|
|
164
164
|
id: StatusLineSegmentId,
|
|
165
165
|
ctx: SegmentContext,
|
|
166
166
|
): RenderedSegment {
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
167
|
+
try {
|
|
168
|
+
let rendered: RenderedSegment;
|
|
169
|
+
if (isCustomSegmentId(id)) {
|
|
170
|
+
const customId = id.slice("custom:".length);
|
|
171
|
+
const computed = customComputedSegments.get(customId);
|
|
172
|
+
rendered = computed
|
|
173
|
+
? computed.render(ctx)
|
|
174
|
+
: renderCustomSegment(id, ctx);
|
|
175
|
+
} else {
|
|
176
|
+
const segment = SEGMENTS[id];
|
|
177
|
+
rendered = segment ? segment.render(ctx) : { content: "", visible: false };
|
|
178
|
+
}
|
|
179
|
+
return applySegmentDecoration(id, ctx, rendered);
|
|
180
|
+
} catch {
|
|
181
|
+
// ponytail: per-segment fault isolation. A throwing segment (most often a
|
|
182
|
+
// user `command` custom segment with a failing script) must not blank the
|
|
183
|
+
// whole footer; surface a visible `!id` marker so the operator sees
|
|
184
|
+
// which segment broke. No per-render log: the bar repaints ~30/s.
|
|
185
|
+
return { content: `!${id}`, visible: true };
|
|
177
186
|
}
|
|
178
|
-
return applySegmentDecoration(id, ctx, rendered);
|
|
179
187
|
}
|
package/src/segments/system.ts
CHANGED
|
@@ -180,7 +180,21 @@ export function countListeningPorts(includeUdp = false, host?: string): number {
|
|
|
180
180
|
return host ? -1 : readProcListeningPorts(includeUdp);
|
|
181
181
|
}
|
|
182
182
|
|
|
183
|
-
|
|
183
|
+
return parseListeningPortsFromText(out).size;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Pure port-count parser shared by `countListeningPorts`. Accepts `ss` and
|
|
188
|
+
* `netstat` output. The port separator is `:` on Linux/`ss`/`lsof` but `.` on
|
|
189
|
+
* macOS `netstat` (`*.5900`, `127.0.0.1.631`); matching both keeps the
|
|
190
|
+
* open-ports segment honest on macOS.
|
|
191
|
+
*
|
|
192
|
+
* ponytail: assumes LISTEN rows always carry a port on the local-address
|
|
193
|
+
* column; a bare IPv4 with no port (not emitted by LISTEN output) would
|
|
194
|
+
* false-match the `.<digits>` form.
|
|
195
|
+
*/
|
|
196
|
+
export function parseListeningPortsFromText(text: string): Set<number> {
|
|
197
|
+
const lines = text
|
|
184
198
|
.split("\n")
|
|
185
199
|
.map((l) => l.trim())
|
|
186
200
|
.filter(Boolean);
|
|
@@ -189,14 +203,14 @@ export function countListeningPorts(includeUdp = false, host?: string): number {
|
|
|
189
203
|
for (const line of lines.slice(start)) {
|
|
190
204
|
// ss/netstat put the local address at different columns; take the first addr:port token
|
|
191
205
|
for (const col of line.split(/\s+/)) {
|
|
192
|
-
const m =
|
|
206
|
+
const m = /(?::|\.)(\d+)$/.exec(col);
|
|
193
207
|
if (m) {
|
|
194
208
|
ports.add(Number(m[1]));
|
|
195
209
|
break;
|
|
196
210
|
}
|
|
197
211
|
}
|
|
198
212
|
}
|
|
199
|
-
return ports
|
|
213
|
+
return ports;
|
|
200
214
|
}
|
|
201
215
|
|
|
202
216
|
function readProcListeningPorts(includeUdp: boolean): number {
|
|
@@ -259,7 +273,7 @@ export function parseOpenPortProcesses(text: string): OpenPortProcess[] {
|
|
|
259
273
|
// numeric Recv-Q there. That tells us which local-address column to use.
|
|
260
274
|
const isSsRow = !/^\d+$/.test(tokens[1] ?? "");
|
|
261
275
|
const local = isSsRow ? tokens[4] : tokens[3];
|
|
262
|
-
const portMatch = local ?
|
|
276
|
+
const portMatch = local ? /(?::|\.)(\d+)$/.exec(local) : null;
|
|
263
277
|
if (!portMatch) continue;
|
|
264
278
|
|
|
265
279
|
let process: string | null = null;
|
|
@@ -275,7 +289,7 @@ export function parseOpenPortProcesses(text: string): OpenPortProcess[] {
|
|
|
275
289
|
entries.push({
|
|
276
290
|
port: Number(portMatch[1]),
|
|
277
291
|
proto: tokens[0].toLowerCase().startsWith("udp") ? "udp" : "tcp",
|
|
278
|
-
address: local.replace(
|
|
292
|
+
address: local.replace(/(?::|\.)(\d+)$/, ""),
|
|
279
293
|
process,
|
|
280
294
|
});
|
|
281
295
|
}
|