@selesai/code 0.1.18 → 0.2.0

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 (149) hide show
  1. package/dist/agents/architect.md +4 -3
  2. package/dist/agents/builder.md +5 -4
  3. package/dist/agents/commentator.md +3 -2
  4. package/dist/agents/explorer.md +4 -4
  5. package/dist/agents/recapper.md +4 -3
  6. package/dist/config.d.ts +6 -7
  7. package/dist/config.d.ts.map +1 -1
  8. package/dist/config.js +7 -9
  9. package/dist/config.js.map +1 -1
  10. package/dist/core/agent-session.d.ts.map +1 -1
  11. package/dist/core/agent-session.js +3 -1
  12. package/dist/core/agent-session.js.map +1 -1
  13. package/dist/core/extensions/loader.d.ts.map +1 -1
  14. package/dist/core/extensions/loader.js +2 -0
  15. package/dist/core/extensions/loader.js.map +1 -1
  16. package/dist/core/extensions/types.d.ts +8 -1
  17. package/dist/core/extensions/types.d.ts.map +1 -1
  18. package/dist/core/extensions/types.js.map +1 -1
  19. package/dist/core/skills.d.ts.map +1 -1
  20. package/dist/core/skills.js +25 -11
  21. package/dist/core/skills.js.map +1 -1
  22. package/dist/defaults/settings.json +2 -29
  23. package/dist/extensions/package.json +5 -4
  24. package/dist/extensions/pi-intercom/index.ts +12 -5
  25. package/dist/extensions/pi-powerline-footer/index.ts +4 -1
  26. package/dist/extensions/pi-powerline-footer/tps.ts +286 -0
  27. package/dist/extensions/pi-powerline-footer/welcome.ts +47 -46
  28. package/dist/extensions/pi-subagents/README.md +12 -5
  29. package/dist/extensions/pi-subagents/agents/context-builder.md +2 -2
  30. package/dist/extensions/pi-subagents/agents/researcher.md +4 -5
  31. package/dist/extensions/pi-subagents/package.json +8 -5
  32. package/dist/extensions/pi-subagents/prompts/parallel-context-build.md +1 -1
  33. package/dist/extensions/pi-subagents/prompts/parallel-handoff-plan.md +1 -1
  34. package/dist/extensions/pi-subagents/src/agents/agent-management.ts +1 -1
  35. package/dist/extensions/pi-subagents/src/agents/agents.ts +3 -3
  36. package/dist/extensions/pi-subagents/src/extension/control-notices.ts +1 -1
  37. package/dist/extensions/pi-subagents/src/extension/doctor.ts +3 -3
  38. package/dist/extensions/pi-subagents/src/extension/fanout-child.ts +1 -1
  39. package/dist/extensions/pi-subagents/src/extension/index.ts +29 -2
  40. package/dist/extensions/pi-subagents/src/profiles/profiles.ts +2 -2
  41. package/dist/extensions/pi-subagents/src/runs/background/async-execution.ts +72 -13
  42. package/dist/extensions/pi-subagents/src/runs/background/async-job-tracker.ts +1 -1
  43. package/dist/extensions/pi-subagents/src/runs/background/notify.ts +1 -1
  44. package/dist/extensions/pi-subagents/src/runs/background/subagent-runner.ts +85 -41
  45. package/dist/extensions/pi-subagents/src/runs/foreground/chain-clarify.ts +1 -1
  46. package/dist/extensions/pi-subagents/src/runs/foreground/chain-execution.ts +1 -1
  47. package/dist/extensions/pi-subagents/src/runs/foreground/execution.ts +46 -3
  48. package/dist/extensions/pi-subagents/src/runs/foreground/subagent-executor.ts +2 -2
  49. package/dist/extensions/pi-subagents/src/runs/shared/completion-guard.ts +1 -3
  50. package/dist/extensions/pi-subagents/src/runs/shared/nested-events.ts +2 -2
  51. package/dist/extensions/pi-subagents/src/runs/shared/pi-args.ts +18 -18
  52. package/dist/extensions/pi-subagents/src/runs/shared/pi-spawn.ts +24 -24
  53. package/dist/extensions/pi-subagents/src/runs/shared/spawn-timing.ts +46 -0
  54. package/dist/extensions/pi-subagents/src/runs/shared/structured-output.ts +2 -2
  55. package/dist/extensions/pi-subagents/src/runs/shared/subagent-prompt-runtime.ts +4 -4
  56. package/dist/extensions/pi-subagents/src/shared/fork-context.ts +1 -1
  57. package/dist/extensions/pi-subagents/src/shared/types.ts +6 -6
  58. package/dist/extensions/pi-subagents/src/shared/utils.ts +3 -3
  59. package/dist/extensions/pi-subagents/src/slash/slash-bridge.ts +1 -1
  60. package/dist/extensions/pi-subagents/src/slash/slash-commands.ts +1 -1
  61. package/dist/extensions/pi-subagents/src/tui/render-helpers.ts +1 -1
  62. package/dist/extensions/pi-subagents/src/tui/render.ts +1 -1
  63. package/dist/extensions/pi-subagents/test/unit/spawn-timing.test.ts +45 -0
  64. package/dist/extensions/pi-subagents/tsconfig.runner.json +17 -0
  65. package/dist/extensions/pi-web-agent/package.json +31 -0
  66. package/dist/extensions/pi-web-agent/src/backends/config.ts +205 -0
  67. package/dist/extensions/pi-web-agent/src/backends/doctor.ts +136 -0
  68. package/dist/extensions/pi-web-agent/src/backends/factory.ts +152 -0
  69. package/dist/extensions/pi-web-agent/src/backends/settings-reader.ts +25 -0
  70. package/dist/extensions/pi-web-agent/src/cache/ttl-cache.ts +28 -0
  71. package/dist/extensions/pi-web-agent/src/changelog-notice.ts +133 -0
  72. package/dist/extensions/pi-web-agent/src/commands/web-agent-config.ts +946 -0
  73. package/dist/extensions/pi-web-agent/src/extension.ts +126 -0
  74. package/dist/extensions/pi-web-agent/src/extract/readability.ts +118 -0
  75. package/dist/extensions/pi-web-agent/src/fetch/browser-resolution.ts +199 -0
  76. package/dist/extensions/pi-web-agent/src/fetch/firecrawl-fetch.ts +100 -0
  77. package/dist/extensions/pi-web-agent/src/fetch/headless-fetch.ts +117 -0
  78. package/dist/extensions/pi-web-agent/src/fetch/http-fetch.ts +67 -0
  79. package/dist/extensions/pi-web-agent/src/orchestration/answer-synthesizer.ts +60 -0
  80. package/dist/extensions/pi-web-agent/src/orchestration/candidate-selector.ts +50 -0
  81. package/dist/extensions/pi-web-agent/src/orchestration/direct-url.ts +52 -0
  82. package/dist/extensions/pi-web-agent/src/orchestration/evidence-quality.ts +105 -0
  83. package/dist/extensions/pi-web-agent/src/orchestration/evidence-ranker.ts +45 -0
  84. package/dist/extensions/pi-web-agent/src/orchestration/index.ts +28 -0
  85. package/dist/extensions/pi-web-agent/src/orchestration/query-planner.ts +47 -0
  86. package/dist/extensions/pi-web-agent/src/orchestration/research-orchestrator.ts +376 -0
  87. package/dist/extensions/pi-web-agent/src/orchestration/research-types.ts +64 -0
  88. package/dist/extensions/pi-web-agent/src/orchestration/research-worker.ts +181 -0
  89. package/dist/extensions/pi-web-agent/src/orchestration/source-profile.ts +101 -0
  90. package/dist/extensions/pi-web-agent/src/orchestration/stop-decider.ts +81 -0
  91. package/dist/extensions/pi-web-agent/src/presentation/config-store.ts +208 -0
  92. package/dist/extensions/pi-web-agent/src/presentation/config.ts +75 -0
  93. package/dist/extensions/pi-web-agent/src/presentation/explore-presentation.ts +61 -0
  94. package/dist/extensions/pi-web-agent/src/presentation/fetch-presentation.ts +54 -0
  95. package/dist/extensions/pi-web-agent/src/presentation/search-presentation.ts +41 -0
  96. package/dist/extensions/pi-web-agent/src/presentation/select-view.ts +20 -0
  97. package/dist/extensions/pi-web-agent/src/presentation/types.ts +63 -0
  98. package/dist/extensions/pi-web-agent/src/search/brave.ts +114 -0
  99. package/dist/extensions/pi-web-agent/src/search/duckduckgo.ts +72 -0
  100. package/dist/extensions/pi-web-agent/src/search/searxng.ts +96 -0
  101. package/dist/extensions/pi-web-agent/src/tools/web-explore.ts +71 -0
  102. package/dist/extensions/pi-web-agent/src/tools/web-fetch-headless.ts +31 -0
  103. package/dist/extensions/pi-web-agent/src/tools/web-fetch.ts +31 -0
  104. package/dist/extensions/pi-web-agent/src/tools/web-search.ts +157 -0
  105. package/dist/extensions/pi-web-agent/src/types.ts +88 -0
  106. package/dist/extensions/ponytail/index.js +189 -0
  107. package/dist/extensions/ponytail/package.json +8 -0
  108. package/dist/extensions/ponytail/ponytail-config.cjs +122 -0
  109. package/dist/extensions/ponytail/ponytail-instructions.cjs +94 -0
  110. package/dist/extensions/ponytail/test/extension.test.js +167 -0
  111. package/dist/extensions/ponytail/test/helpers.test.js +92 -0
  112. package/dist/extensions/question/constants.ts +38 -0
  113. package/dist/extensions/question/dialog-adapter.ts +74 -0
  114. package/dist/extensions/question/helpers.ts +102 -0
  115. package/dist/extensions/question/index.ts +671 -0
  116. package/dist/extensions/question/navigation.ts +14 -0
  117. package/dist/extensions/question/package.json +18 -0
  118. package/dist/extensions/question/question-list.ts +498 -0
  119. package/dist/extensions/question/row-layout.ts +162 -0
  120. package/dist/extensions/question/schemas.ts +69 -0
  121. package/dist/extensions/question/selection-mode.ts +49 -0
  122. package/dist/extensions/question/shortcuts.ts +53 -0
  123. package/dist/extensions/question/tests/helpers.test.ts +178 -0
  124. package/dist/extensions/question/tests/question-list.test.ts +230 -0
  125. package/dist/extensions/question/tests/row-layout.test.ts +136 -0
  126. package/dist/extensions/question/tests/shortcuts.test.ts +85 -0
  127. package/dist/extensions/question/tests/ui-protocol.test.ts +143 -0
  128. package/dist/extensions/question/tui-adapter.ts +33 -0
  129. package/dist/extensions/question/types.ts +102 -0
  130. package/dist/extensions/question/ui-protocol.ts +46 -0
  131. package/dist/extensions/tokenin-onboarding.ts +265 -2
  132. package/dist/extensions/web-agent-onboarding.ts +222 -0
  133. package/dist/extensions/workflow/adapter.ts +588 -0
  134. package/dist/extensions/workflow/extension.ts +24 -0
  135. package/dist/extensions/workflow/modes/prototype.ts +156 -0
  136. package/dist/extensions/workflow/modes/quick.ts +138 -0
  137. package/dist/extensions/workflow/package.json +17 -0
  138. package/dist/extensions/workflow/state-machine.ts +386 -0
  139. package/dist/index.d.ts +1 -1
  140. package/dist/index.d.ts.map +1 -1
  141. package/dist/index.js +1 -1
  142. package/dist/index.js.map +1 -1
  143. package/dist/utils/mime.d.ts.map +1 -1
  144. package/dist/utils/mime.js +24 -1
  145. package/dist/utils/mime.js.map +1 -1
  146. package/docs/docs.json +4 -0
  147. package/docs/workflows.md +206 -0
  148. package/package.json +5 -1
  149. package/README.md +0 -198
@@ -0,0 +1,152 @@
1
+ import { createFirecrawlFetcher } from '../fetch/firecrawl-fetch.js';
2
+ import { createBraveSearchTool } from '../search/brave.js';
3
+ import { createSearxngSearchTool } from '../search/searxng.js';
4
+ import { buildFetchPresentation } from '../presentation/fetch-presentation.js';
5
+ import { buildSearchPresentation } from '../presentation/search-presentation.js';
6
+ import { createWebFetchHeadlessTool } from '../tools/web-fetch-headless.js';
7
+ import { createWebFetchTool } from '../tools/web-fetch.js';
8
+ import { createWebSearchTool } from '../tools/web-search.js';
9
+ import { readBraveKeyFromSettings } from './settings-reader.js';
10
+ import type { WebFetchHeadlessResponse, WebFetchResponse, WebSearchResponse } from '../types.js';
11
+ import { DEFAULT_BACKEND_CONFIG, type BackendConfig } from './config.js';
12
+
13
+ export type BackendSet = {
14
+ search: (input: { query: string }) => Promise<WebSearchResponse>;
15
+ fetchPage: (input: { url: string }) => Promise<WebFetchResponse>;
16
+ headlessFetch: (input: { url: string }) => Promise<WebFetchHeadlessResponse>;
17
+ };
18
+
19
+ export type BackendFactoryDeps = {
20
+ createDuckDuckGoSearch?: typeof createWebSearchTool;
21
+ createSearxngSearch?: typeof createSearxngSearchTool;
22
+ createBraveSearch?: typeof createBraveSearchTool;
23
+ createHttpFetch?: typeof createWebFetchTool;
24
+ createFirecrawlFetch?: typeof createFirecrawlFetcher;
25
+ createHeadlessFetch?: typeof createWebFetchHeadlessTool;
26
+ };
27
+
28
+ function invalidSearxngSearch() {
29
+ return async function search() {
30
+ const result: WebSearchResponse = {
31
+ status: 'error',
32
+ results: [],
33
+ metadata: { backend: 'searxng', cacheHit: false },
34
+ error: {
35
+ code: 'BACKEND_CONFIG_INVALID',
36
+ message: 'SearXNG search requires backends.search.baseUrl.'
37
+ }
38
+ };
39
+
40
+ return { ...result, presentation: buildSearchPresentation(result) };
41
+ };
42
+ }
43
+
44
+ function invalidFirecrawlFetch() {
45
+ return async function fetchPage(url: string): Promise<WebFetchResponse> {
46
+ const result: WebFetchResponse = {
47
+ status: 'error',
48
+ url,
49
+ metadata: { method: 'firecrawl', cacheHit: false },
50
+ error: {
51
+ code: 'BACKEND_CONFIG_INVALID',
52
+ message: 'Firecrawl fetch requires backends.fetch.baseUrl.'
53
+ }
54
+ };
55
+
56
+ return { ...result, presentation: buildFetchPresentation(result) };
57
+ };
58
+ }
59
+
60
+ function withSearchFallback(
61
+ primary: BackendSet['search'],
62
+ fallback: BackendSet['search'],
63
+ fallbackFrom: 'searxng' | 'brave'
64
+ ): BackendSet['search'] {
65
+ return async (input) => {
66
+ const first = await primary(input);
67
+ if (first.status !== 'error') return first;
68
+
69
+ const second = await fallback(input);
70
+ const result: WebSearchResponse = {
71
+ ...second,
72
+ metadata: {
73
+ ...second.metadata,
74
+ fallbackFrom,
75
+ fallbackReason: first.error?.message ?? `${fallbackFrom} search failed.`
76
+ }
77
+ };
78
+ return { ...result, presentation: buildSearchPresentation(result) };
79
+ };
80
+ }
81
+
82
+ function withFetchFallback(
83
+ primary: BackendSet['fetchPage'],
84
+ fallback: BackendSet['fetchPage']
85
+ ): BackendSet['fetchPage'] {
86
+ return async (input) => {
87
+ const first = await primary(input);
88
+ if (first.status !== 'error' && first.status !== 'needs_headless') return first;
89
+
90
+ const second = await fallback(input);
91
+ const result: WebFetchResponse = {
92
+ ...second,
93
+ metadata: {
94
+ ...second.metadata,
95
+ fallbackFrom: 'firecrawl',
96
+ fallbackReason: first.error?.message ?? 'Firecrawl fetch failed.'
97
+ }
98
+ };
99
+ return { ...result, presentation: buildFetchPresentation(result) };
100
+ };
101
+ }
102
+
103
+ export function createBackendSet(
104
+ config: BackendConfig = DEFAULT_BACKEND_CONFIG,
105
+ deps: BackendFactoryDeps = {}
106
+ ): BackendSet {
107
+ const createDuckDuckGoSearch = deps.createDuckDuckGoSearch ?? createWebSearchTool;
108
+ const createSearxngSearch = deps.createSearxngSearch ?? createSearxngSearchTool;
109
+ const createBraveSearch = deps.createBraveSearch ?? createBraveSearchTool;
110
+ const createHttpFetch = deps.createHttpFetch ?? createWebFetchTool;
111
+ const createFirecrawlFetch = deps.createFirecrawlFetch ?? createFirecrawlFetcher;
112
+ const createHeadlessFetch = deps.createHeadlessFetch ?? createWebFetchHeadlessTool;
113
+
114
+ let search = config.search.provider === 'searxng'
115
+ ? config.search.baseUrl
116
+ ? createSearxngSearch({ baseUrl: config.search.baseUrl, options: config.search.options })
117
+ : invalidSearxngSearch()
118
+ : config.search.provider === 'brave'
119
+ ? createBraveSearch({ apiKey: process.env.PI_WEB_AGENT_BRAVE_API_KEY ?? readBraveKeyFromSettings() })
120
+ : createDuckDuckGoSearch();
121
+
122
+ if (config.search.provider === 'searxng' && config.search.fallback === 'duckduckgo') {
123
+ search = withSearchFallback(search, createDuckDuckGoSearch(), 'searxng');
124
+ }
125
+
126
+ if (config.search.provider === 'brave' && config.search.fallback === 'duckduckgo') {
127
+ search = withSearchFallback(search, createDuckDuckGoSearch(), 'brave');
128
+ }
129
+
130
+ const httpFetch = createHttpFetch();
131
+ let fetchPage = config.fetch.provider === 'firecrawl'
132
+ ? config.fetch.baseUrl
133
+ ? createHttpFetch({
134
+ fetchPage: createFirecrawlFetch({
135
+ baseUrl: config.fetch.baseUrl,
136
+ apiKey: config.fetch.apiKey ?? process.env.PI_WEB_AGENT_FIRECRAWL_API_KEY,
137
+ options: config.fetch.options
138
+ })
139
+ })
140
+ : createHttpFetch({ fetchPage: invalidFirecrawlFetch() })
141
+ : httpFetch;
142
+
143
+ if (config.fetch.provider === 'firecrawl' && config.fetch.fallback === 'http') {
144
+ fetchPage = withFetchFallback(fetchPage, httpFetch);
145
+ }
146
+
147
+ return {
148
+ search,
149
+ fetchPage,
150
+ headlessFetch: createHeadlessFetch()
151
+ };
152
+ }
@@ -0,0 +1,25 @@
1
+ // ponytail: selesai stores the Brave API key in settings.json (webAgent.braveApiKey),
2
+ // set by the bundled web-agent-onboarding extension. Upstream pi-web-agent reads the
3
+ // key only from PI_WEB_AGENT_BRAVE_API_KEY env var. This helper bridges that gap with a
4
+ // minimal settings.json read. Ceiling: if selesai moves web search config elsewhere, this
5
+ // helper needs updating; upgrade path = upstream pi-web-agent adopting a settings hook.
6
+ import { existsSync, readFileSync } from 'node:fs';
7
+ import { getSettingsPath } from '@earendil-works/pi-coding-agent';
8
+
9
+ /**
10
+ * Read the Brave API key from selesai settings.json (webAgent.braveApiKey).
11
+ * Returns undefined if settings.json or the key is absent. Never throws.
12
+ */
13
+ export function readBraveKeyFromSettings(settingsPath: string = getSettingsPath()): string | undefined {
14
+ try {
15
+ if (!existsSync(settingsPath)) return undefined;
16
+ const raw = readFileSync(settingsPath, 'utf-8');
17
+ const parsed = JSON.parse(raw) as Record<string, unknown>;
18
+ const webAgent = parsed.webAgent;
19
+ if (!webAgent || typeof webAgent !== 'object' || Array.isArray(webAgent)) return undefined;
20
+ const apiKey = (webAgent as Record<string, unknown>).braveApiKey;
21
+ return typeof apiKey === 'string' && apiKey.trim() !== '' ? apiKey : undefined;
22
+ } catch {
23
+ return undefined;
24
+ }
25
+ }
@@ -0,0 +1,28 @@
1
+ export function createCacheKey(parts: Array<string | number | boolean>): string {
2
+ return JSON.stringify(parts);
3
+ }
4
+
5
+ export function createTtlCache<T>({
6
+ ttlMs,
7
+ now = () => Date.now()
8
+ }: {
9
+ ttlMs: number;
10
+ now?: () => number;
11
+ }) {
12
+ const entries = new Map<string, { value: T; expiresAt: number }>();
13
+
14
+ return {
15
+ get(key: string): T | undefined {
16
+ const entry = entries.get(key);
17
+ if (!entry) return undefined;
18
+ if (entry.expiresAt <= now()) {
19
+ entries.delete(key);
20
+ return undefined;
21
+ }
22
+ return entry.value;
23
+ },
24
+ set(key: string, value: T): void {
25
+ entries.set(key, { value, expiresAt: now() + ttlMs });
26
+ }
27
+ };
28
+ }
@@ -0,0 +1,133 @@
1
+ import { mkdir, readFile, writeFile } from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+
5
+ export type ChangelogEntry = {
6
+ version: string;
7
+ content: string;
8
+ };
9
+
10
+ type ChangelogState = {
11
+ lastChangelogVersion?: string;
12
+ };
13
+
14
+ type ChangelogOptions = {
15
+ packageRoot?: string;
16
+ statePath?: string;
17
+ };
18
+
19
+ export function parseChangelogEntries(changelog: string): ChangelogEntry[] {
20
+ const lines = changelog.split(/\r?\n/);
21
+ const entries: ChangelogEntry[] = [];
22
+ let currentVersion: string | undefined;
23
+ let currentLines: string[] = [];
24
+
25
+ function flush() {
26
+ if (currentVersion && currentLines.length > 0) {
27
+ entries.push({ version: currentVersion, content: currentLines.join('\n').trim() });
28
+ }
29
+ }
30
+
31
+ for (const line of lines) {
32
+ if (line.startsWith('## ')) {
33
+ flush();
34
+ const match = line.match(/^##\s+\[?(\d+\.\d+\.\d+)\]?/);
35
+ currentVersion = match?.[1];
36
+ currentLines = currentVersion ? [line] : [];
37
+ continue;
38
+ }
39
+
40
+ if (currentVersion) {
41
+ currentLines.push(line);
42
+ }
43
+ }
44
+
45
+ flush();
46
+ return entries;
47
+ }
48
+
49
+ function compareVersions(left: string, right: string): number {
50
+ const leftParts = left.split('.').map(Number);
51
+ const rightParts = right.split('.').map(Number);
52
+
53
+ for (let i = 0; i < 3; i += 1) {
54
+ const diff = (leftParts[i] || 0) - (rightParts[i] || 0);
55
+ if (diff !== 0) return diff;
56
+ }
57
+
58
+ return 0;
59
+ }
60
+
61
+ function defaultPackageRoot() {
62
+ const here = path.dirname(fileURLToPath(import.meta.url));
63
+ return path.basename(here) === 'dist' ? path.dirname(here) : path.dirname(here);
64
+ }
65
+
66
+ function defaultStatePath() {
67
+ const homeDir = process.env.USERPROFILE ?? process.env.HOME ?? '';
68
+ return path.join(homeDir, '.pi', 'agent', 'extensions', 'pi-web-agent', 'state.json');
69
+ }
70
+
71
+ async function readPackageVersion(packageRoot: string): Promise<string | undefined> {
72
+ try {
73
+ const raw = await readFile(path.join(packageRoot, 'package.json'), 'utf8');
74
+ const parsed = JSON.parse(raw) as { version?: unknown };
75
+ return typeof parsed.version === 'string' ? parsed.version : undefined;
76
+ } catch {
77
+ return undefined;
78
+ }
79
+ }
80
+
81
+ async function readChangelogEntries(packageRoot: string): Promise<ChangelogEntry[]> {
82
+ try {
83
+ return parseChangelogEntries(await readFile(path.join(packageRoot, 'CHANGELOG.md'), 'utf8'));
84
+ } catch {
85
+ return [];
86
+ }
87
+ }
88
+
89
+ async function readState(statePath: string): Promise<ChangelogState> {
90
+ try {
91
+ return JSON.parse(await readFile(statePath, 'utf8')) as ChangelogState;
92
+ } catch {
93
+ return {};
94
+ }
95
+ }
96
+
97
+ export async function markChangelogSeen({ statePath = defaultStatePath(), version }: { statePath?: string; version: string }) {
98
+ try {
99
+ await mkdir(path.dirname(statePath), { recursive: true });
100
+ await writeFile(statePath, `${JSON.stringify({ lastChangelogVersion: version }, null, 2)}\n`, 'utf8');
101
+ } catch {
102
+ // Changelog display is best effort. Never block extension startup on state persistence.
103
+ }
104
+ }
105
+
106
+ export async function getUpdateChangelogNotice(options: ChangelogOptions = {}): Promise<string | undefined> {
107
+ const packageRoot = options.packageRoot ?? defaultPackageRoot();
108
+ const statePath = options.statePath ?? defaultStatePath();
109
+ const version = await readPackageVersion(packageRoot);
110
+ if (!version) return undefined;
111
+
112
+ const state = await readState(statePath);
113
+ if (!state.lastChangelogVersion) {
114
+ await markChangelogSeen({ statePath, version });
115
+ return undefined;
116
+ }
117
+
118
+ if (compareVersions(version, state.lastChangelogVersion) <= 0) {
119
+ return undefined;
120
+ }
121
+
122
+ const entries = (await readChangelogEntries(packageRoot))
123
+ .filter((entry) => compareVersions(entry.version, state.lastChangelogVersion || '0.0.0') > 0)
124
+ .map((entry) => entry.content);
125
+
126
+ await markChangelogSeen({ statePath, version });
127
+ return entries.length > 0 ? entries.join('\n\n') : undefined;
128
+ }
129
+
130
+ export async function getLatestChangelogEntry(options: ChangelogOptions = {}): Promise<string | undefined> {
131
+ const entries = await readChangelogEntries(options.packageRoot ?? defaultPackageRoot());
132
+ return entries[0]?.content;
133
+ }