@pugi/cli 0.1.0-beta.5 → 0.1.0-beta.51

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 (264) hide show
  1. package/THIRD_PARTY_NOTICES.md +40 -0
  2. package/assets/pugi-mascot.ansi +15 -25
  3. package/assets/pugi-prozr2-mascot.ansi +9 -0
  4. package/bin/run.js +33 -1
  5. package/dist/commands/jobs-watch.js +201 -0
  6. package/dist/commands/jobs.js +15 -0
  7. package/dist/commands/smoke.js +133 -0
  8. package/dist/core/agent-progress/cleanup.js +134 -0
  9. package/dist/core/agent-progress/schema.js +144 -0
  10. package/dist/core/agent-progress/writer.js +101 -0
  11. package/dist/core/artifact-chain/dispatcher.js +148 -0
  12. package/dist/core/artifact-chain/exporter.js +164 -0
  13. package/dist/core/artifact-chain/state.js +243 -0
  14. package/dist/core/artifact-chain/steps.js +169 -0
  15. package/dist/core/auth/ensure-authenticated.js +129 -0
  16. package/dist/core/auth/env-provider.js +238 -0
  17. package/dist/core/auto-update/channels.js +122 -0
  18. package/dist/core/auto-update/checker.js +241 -0
  19. package/dist/core/auto-update/state.js +235 -0
  20. package/dist/core/bare-mode/index.js +107 -0
  21. package/dist/core/bash-classifier.js +400 -4
  22. package/dist/core/checkpoint/resumer.js +149 -0
  23. package/dist/core/checkpoint/rewinder.js +291 -0
  24. package/dist/core/codegraph/decision-store.js +248 -0
  25. package/dist/core/codegraph/detect-repo.js +459 -0
  26. package/dist/core/codegraph/install.js +134 -0
  27. package/dist/core/codegraph/offer-hook.js +220 -0
  28. package/dist/core/compact/auto-trigger.js +96 -0
  29. package/dist/core/compact/buffer-rewriter.js +115 -0
  30. package/dist/core/compact/summarizer.js +208 -0
  31. package/dist/core/compact/token-counter.js +108 -0
  32. package/dist/core/consensus/diff-capture.js +112 -3
  33. package/dist/core/context/index.js +7 -0
  34. package/dist/core/context/markdown-traverse.js +255 -0
  35. package/dist/core/cost/rate-card.js +129 -0
  36. package/dist/core/cost/tracker.js +221 -0
  37. package/dist/core/denial-tracking/index.js +8 -0
  38. package/dist/core/denial-tracking/state.js +264 -0
  39. package/dist/core/diagnostics/probe-runner.js +93 -0
  40. package/dist/core/diagnostics/probes/api.js +46 -0
  41. package/dist/core/diagnostics/probes/auth.js +86 -0
  42. package/dist/core/diagnostics/probes/bare-mode.js +42 -0
  43. package/dist/core/diagnostics/probes/cli-version.js +127 -0
  44. package/dist/core/diagnostics/probes/config.js +72 -0
  45. package/dist/core/diagnostics/probes/denial-tracking.js +57 -0
  46. package/dist/core/diagnostics/probes/disk.js +81 -0
  47. package/dist/core/diagnostics/probes/git.js +65 -0
  48. package/dist/core/diagnostics/probes/hooks.js +118 -0
  49. package/dist/core/diagnostics/probes/mcp.js +75 -0
  50. package/dist/core/diagnostics/probes/node.js +59 -0
  51. package/dist/core/diagnostics/probes/pnpm.js +36 -0
  52. package/dist/core/diagnostics/probes/pugi-md.js +89 -0
  53. package/dist/core/diagnostics/probes/sandbox.js +40 -0
  54. package/dist/core/diagnostics/probes/session.js +74 -0
  55. package/dist/core/diagnostics/probes/status-snapshot.js +488 -0
  56. package/dist/core/diagnostics/probes/workspace.js +63 -0
  57. package/dist/core/diagnostics/types.js +70 -0
  58. package/dist/core/dispatch/cache-cleanup.js +197 -0
  59. package/dist/core/dispatch/cache-handoff.js +295 -0
  60. package/dist/core/edits/dispatch.js +218 -2
  61. package/dist/core/edits/journal.js +199 -0
  62. package/dist/core/edits/layer-d-ast.js +557 -14
  63. package/dist/core/edits/verify-hook.js +273 -0
  64. package/dist/core/edits/worktree.js +322 -0
  65. package/dist/core/engine/anvil-client.js +115 -5
  66. package/dist/core/engine/auto-compact.js +179 -0
  67. package/dist/core/engine/budgets.js +155 -0
  68. package/dist/core/engine/context-prefix.js +155 -0
  69. package/dist/core/engine/intent.js +260 -0
  70. package/dist/core/engine/native-pugi.js +897 -211
  71. package/dist/core/engine/prompts.js +88 -2
  72. package/dist/core/engine/strip-internal-fields.js +124 -0
  73. package/dist/core/engine/tool-bridge.js +1045 -36
  74. package/dist/core/feedback/queue.js +177 -0
  75. package/dist/core/feedback/submitter.js +145 -0
  76. package/dist/core/file-cache.js +113 -1
  77. package/dist/core/hooks/events.js +44 -0
  78. package/dist/core/hooks/index.js +15 -0
  79. package/dist/core/hooks/registry.js +213 -0
  80. package/dist/core/hooks/runner.js +236 -0
  81. package/dist/core/hooks/v2/event-emitter.js +115 -0
  82. package/dist/core/hooks/v2/executor.js +282 -0
  83. package/dist/core/hooks/v2/index.js +25 -0
  84. package/dist/core/hooks/v2/lifecycle.js +104 -0
  85. package/dist/core/hooks/v2/loader.js +216 -0
  86. package/dist/core/hooks/v2/matcher.js +125 -0
  87. package/dist/core/hooks/v2/trust.js +143 -0
  88. package/dist/core/hooks/v2/types.js +86 -0
  89. package/dist/core/lsp/cache.js +105 -0
  90. package/dist/core/lsp/client.js +776 -0
  91. package/dist/core/lsp/language-detect.js +66 -0
  92. package/dist/core/lsp/post-edit-diagnostics.js +171 -0
  93. package/dist/core/mcp/client.js +75 -6
  94. package/dist/core/mcp/http-server.js +553 -0
  95. package/dist/core/mcp/orchestrator-tools.js +662 -0
  96. package/dist/core/mcp/permission.js +190 -0
  97. package/dist/core/mcp/registry.js +24 -2
  98. package/dist/core/mcp/server-tools.js +219 -0
  99. package/dist/core/mcp/server.js +397 -0
  100. package/dist/core/memory/dual-write.js +416 -0
  101. package/dist/core/memory/phase1-kinds.js +20 -0
  102. package/dist/core/memory-sync/queue.js +158 -0
  103. package/dist/core/onboarding/ensure-initialized.js +133 -0
  104. package/dist/core/onboarding/marker.js +111 -0
  105. package/dist/core/onboarding/telemetry-state.js +108 -0
  106. package/dist/core/output-style/presets.js +176 -0
  107. package/dist/core/output-style/state.js +185 -0
  108. package/dist/core/path-security.js +284 -2
  109. package/dist/core/permissions/auto-classifier.js +124 -0
  110. package/dist/core/permissions/circuit-breaker.js +83 -0
  111. package/dist/core/permissions/gate.js +278 -0
  112. package/dist/core/permissions/index.js +20 -0
  113. package/dist/core/permissions/mode.js +174 -0
  114. package/dist/core/permissions/state.js +241 -0
  115. package/dist/core/permissions/tool-class.js +93 -0
  116. package/dist/core/prd-check/parser.js +215 -0
  117. package/dist/core/prd-check/reporter.js +127 -0
  118. package/dist/core/prd-check/session-review.js +557 -0
  119. package/dist/core/prd-check/verifiers.js +223 -0
  120. package/dist/core/pugi-md/context-injector.js +76 -0
  121. package/dist/core/pugi-md/walk-up.js +207 -0
  122. package/dist/core/release-notes/parser.js +241 -0
  123. package/dist/core/release-notes/state.js +116 -0
  124. package/dist/core/repl/history.js +11 -1
  125. package/dist/core/repl/model-pricing.js +135 -0
  126. package/dist/core/repl/session.js +1897 -37
  127. package/dist/core/repl/slash-commands.js +430 -15
  128. package/dist/core/repl/store/session-store.js +31 -2
  129. package/dist/core/repl/workspace-context.js +22 -0
  130. package/dist/core/repo-map/build.js +125 -0
  131. package/dist/core/repo-map/cache.js +185 -0
  132. package/dist/core/repo-map/extractor.js +254 -0
  133. package/dist/core/repo-map/formatter.js +145 -0
  134. package/dist/core/repo-map/scanner.js +211 -0
  135. package/dist/core/retry-budget/budget.js +284 -0
  136. package/dist/core/retry-budget/index.js +5 -0
  137. package/dist/core/session.js +92 -0
  138. package/dist/core/settings.js +80 -0
  139. package/dist/core/share/formatter.js +271 -0
  140. package/dist/core/share/redactor.js +221 -0
  141. package/dist/core/share/uploader.js +267 -0
  142. package/dist/core/skills/defaults.js +457 -0
  143. package/dist/core/smoke/headless-driver.js +174 -0
  144. package/dist/core/smoke/orchestrator.js +194 -0
  145. package/dist/core/smoke/runner.js +238 -0
  146. package/dist/core/smoke/scenario-parser.js +316 -0
  147. package/dist/core/subagents/dispatcher-real.js +600 -0
  148. package/dist/core/subagents/dispatcher.js +113 -24
  149. package/dist/core/subagents/index.js +18 -5
  150. package/dist/core/subagents/isolation-matrix.js +213 -0
  151. package/dist/core/subagents/spawn.js +19 -4
  152. package/dist/core/telemetry/emitter.js +229 -0
  153. package/dist/core/telemetry/queue.js +251 -0
  154. package/dist/core/theme/context.js +91 -0
  155. package/dist/core/theme/presets.js +228 -0
  156. package/dist/core/theme/state.js +181 -0
  157. package/dist/core/todos/invariant.js +10 -0
  158. package/dist/core/todos/state.js +177 -0
  159. package/dist/core/transport/version-interceptor.js +166 -0
  160. package/dist/core/vim/keymap.js +288 -0
  161. package/dist/core/vim/state.js +92 -0
  162. package/dist/core/worktree-manager/cleanup.js +123 -0
  163. package/dist/core/worktree-manager/manager.js +303 -0
  164. package/dist/index.js +28 -0
  165. package/dist/runtime/bootstrap.js +190 -0
  166. package/dist/runtime/cli.js +3241 -343
  167. package/dist/runtime/commands/cancel.js +231 -0
  168. package/dist/runtime/commands/chain.js +489 -0
  169. package/dist/runtime/commands/codegraph-status.js +227 -0
  170. package/dist/runtime/commands/compact.js +297 -0
  171. package/dist/runtime/commands/cost.js +199 -0
  172. package/dist/runtime/commands/delegate.js +242 -11
  173. package/dist/runtime/commands/dispatch.js +126 -0
  174. package/dist/runtime/commands/doctor.js +412 -0
  175. package/dist/runtime/commands/feedback.js +184 -0
  176. package/dist/runtime/commands/hooks.js +184 -0
  177. package/dist/runtime/commands/lsp.js +368 -0
  178. package/dist/runtime/commands/mcp.js +879 -0
  179. package/dist/runtime/commands/memory.js +508 -0
  180. package/dist/runtime/commands/model.js +237 -0
  181. package/dist/runtime/commands/onboarding.js +275 -0
  182. package/dist/runtime/commands/patch.js +128 -0
  183. package/dist/runtime/commands/permissions.js +112 -0
  184. package/dist/runtime/commands/plan.js +143 -0
  185. package/dist/runtime/commands/prd-check.js +285 -0
  186. package/dist/runtime/commands/redo-blob-store.js +92 -0
  187. package/dist/runtime/commands/redo.js +361 -0
  188. package/dist/runtime/commands/release-notes.js +229 -0
  189. package/dist/runtime/commands/repo-map.js +95 -0
  190. package/dist/runtime/commands/report.js +299 -0
  191. package/dist/runtime/commands/resume.js +118 -0
  192. package/dist/runtime/commands/review-consensus.js +17 -2
  193. package/dist/runtime/commands/rewind.js +333 -0
  194. package/dist/runtime/commands/sessions.js +163 -0
  195. package/dist/runtime/commands/share.js +316 -0
  196. package/dist/runtime/commands/status.js +186 -0
  197. package/dist/runtime/commands/stickers.js +82 -0
  198. package/dist/runtime/commands/style.js +194 -0
  199. package/dist/runtime/commands/theme.js +196 -0
  200. package/dist/runtime/commands/undo.js +32 -0
  201. package/dist/runtime/commands/update.js +289 -0
  202. package/dist/runtime/commands/vim.js +140 -0
  203. package/dist/runtime/commands/worktree.js +177 -0
  204. package/dist/runtime/commands/worktrees.js +155 -0
  205. package/dist/runtime/headless-repl.js +195 -0
  206. package/dist/runtime/headless.js +543 -0
  207. package/dist/runtime/load-hooks-or-exit.js +71 -0
  208. package/dist/runtime/plan-decompose.js +531 -0
  209. package/dist/runtime/version.js +65 -0
  210. package/dist/tools/agent-tool.js +229 -0
  211. package/dist/tools/apply-patch.js +556 -0
  212. package/dist/tools/ask-user-question.js +213 -0
  213. package/dist/tools/ask-user.js +115 -0
  214. package/dist/tools/bash.js +203 -4
  215. package/dist/tools/file-tools.js +85 -14
  216. package/dist/tools/lsp-tools.js +189 -0
  217. package/dist/tools/mcp-tool.js +260 -0
  218. package/dist/tools/multi-edit.js +361 -0
  219. package/dist/tools/powershell.js +268 -0
  220. package/dist/tools/registry.js +51 -0
  221. package/dist/tools/skill-tool.js +96 -0
  222. package/dist/tools/tasks.js +208 -0
  223. package/dist/tools/todo-write.js +184 -0
  224. package/dist/tools/web-fetch.js +147 -2
  225. package/dist/tools/web-search.js +458 -0
  226. package/dist/tui/agent-progress-card.js +111 -0
  227. package/dist/tui/agent-tree.js +10 -0
  228. package/dist/tui/ask-modal.js +2 -2
  229. package/dist/tui/ask-user-question-prompt.js +192 -0
  230. package/dist/tui/compact-banner.js +81 -0
  231. package/dist/tui/conversation-pane.js +82 -8
  232. package/dist/tui/cost-table.js +111 -0
  233. package/dist/tui/doctor-table.js +46 -0
  234. package/dist/tui/feedback-prompt.js +156 -0
  235. package/dist/tui/input-box.js +218 -3
  236. package/dist/tui/markdown-render.js +4 -4
  237. package/dist/tui/onboarding-wizard.js +240 -0
  238. package/dist/tui/permissions-picker.js +86 -0
  239. package/dist/tui/render.js +35 -0
  240. package/dist/tui/repl-render.js +313 -35
  241. package/dist/tui/repl-splash-art.js +1 -1
  242. package/dist/tui/repl-splash-mascot.js +32 -8
  243. package/dist/tui/repl-splash.js +2 -2
  244. package/dist/tui/repl.js +85 -5
  245. package/dist/tui/splash.js +1 -1
  246. package/dist/tui/status-bar.js +94 -16
  247. package/dist/tui/status-table.js +7 -0
  248. package/dist/tui/stickers-art.js +136 -0
  249. package/dist/tui/style-table.js +28 -0
  250. package/dist/tui/theme-table.js +29 -0
  251. package/dist/tui/thinking-spinner.js +123 -0
  252. package/dist/tui/tool-stream-pane.js +52 -3
  253. package/dist/tui/update-banner.js +27 -2
  254. package/dist/tui/vim-input.js +267 -0
  255. package/dist/tui/welcome-banner.js +107 -0
  256. package/dist/tui/welcome-data.js +293 -0
  257. package/docs/examples/codegraph.mcp.json +10 -0
  258. package/package.json +13 -7
  259. package/test/scenarios/codegen-create-file.scenario.txt +13 -0
  260. package/test/scenarios/compact-force.scenario.txt +11 -0
  261. package/test/scenarios/identity.scenario.txt +11 -0
  262. package/test/scenarios/persona-handoff.scenario.txt +11 -0
  263. package/test/scenarios/walkback.scenario.txt +12 -0
  264. package/dist/core/engine/compaction-hook.js +0 -154
@@ -0,0 +1,293 @@
1
+ /**
2
+ * Pure data layer for the `<WelcomeBanner />` component (CEO P0 #2,
3
+ * 2026-05-29). Lives in its own module so it can be unit-tested without
4
+ * rendering Ink, and so the banner component itself contains zero IO.
5
+ *
6
+ * The banner is the CC-style 2-column boxed greeting that replaces the
7
+ * α6.14 wave-3 `<ReplSplash />` on the bare REPL boot path. It mirrors
8
+ * Claude Code's boot layout:
9
+ *
10
+ * ╭────── Pugi v0.1.0-beta.46 ──────╮
11
+ * │ │ Tips for getting started
12
+ * │ Welcome back Yurii! │ Run /init to create PUGI.md...
13
+ * │ │ ───────────
14
+ * │ ▗ ▗ ▖ ▖ │ What's new
15
+ * │ ▘▘ ▝▝ │ * 0.1.0-beta.26 — Wave 6 RAG ...
16
+ * │ Sonnet 4.6 (1M context) · Founder
17
+ * │ yuriy.bulah@gmail.com · pugi-io Org
18
+ * │ /Volumes/T9/Web/.../TestRepos2 │
19
+ * ╰──────────────────────────────────╯
20
+ *
21
+ * Data sources, in priority order:
22
+ *
23
+ * - Account email + tenant + plan: JWT principal decoded from the
24
+ * active credential. Falls back to anonymous label when no
25
+ * credential is on disk (operator boots `pugi` before login).
26
+ * - Greeting first-name: best-effort split of the local part of the
27
+ * email. Falls back к "operator" when unauthenticated.
28
+ * - Model: env override `PUGI_ENGINE_MODEL_CODE`, then the operator's
29
+ * workspace settings `defaultModel`, then "Sonnet 4.6 (1M context)"
30
+ * as the locked α7 default.
31
+ * - Cwd: absolute path of `process.cwd()` — banner left column shows
32
+ * the full path so the operator confirms они are в the right repo.
33
+ * - What's new: top 3 release-note titles from `apps/pugi-cli/CHANGELOG.md`
34
+ * newer than `~/.pugi/.last-seen-version`. Falls back к the top 3
35
+ * overall when last-seen marker is missing. Each entry is a one-line
36
+ * headline ("0.1.0-beta.26 — Wave 6 RAG consumer middleware") trimmed
37
+ * к 60 chars so the right column does not wrap on a 100-col terminal.
38
+ *
39
+ * Every resolver swallows its IO error and returns the documented
40
+ * fallback so a partial environment (missing CHANGELOG, malformed JWT,
41
+ * unreadable settings) never blocks the banner.
42
+ */
43
+ import { existsSync, readFileSync } from 'node:fs';
44
+ import { homedir } from 'node:os';
45
+ import { dirname, resolve as resolvePath } from 'node:path';
46
+ import { fileURLToPath } from 'node:url';
47
+ import { DEFAULT_API_URL, normalizeApiUrl, readCredentialsFile, resolveActiveCredential, } from '../core/credentials.js';
48
+ import { parseChangelog } from '../core/release-notes/parser.js';
49
+ function decodeJwtPayload(token) {
50
+ try {
51
+ const parts = token.split('.');
52
+ if (parts.length < 2)
53
+ return null;
54
+ const payload = parts[1];
55
+ if (!payload)
56
+ return null;
57
+ const padded = payload
58
+ .replace(/-/g, '+')
59
+ .replace(/_/g, '/')
60
+ .padEnd(payload.length + ((4 - (payload.length % 4)) % 4), '=');
61
+ const json = Buffer.from(padded, 'base64').toString('utf8');
62
+ const obj = JSON.parse(json);
63
+ if (!obj || typeof obj !== 'object')
64
+ return null;
65
+ return {
66
+ ...(typeof obj.sub === 'string' && { sub: obj.sub }),
67
+ ...(typeof obj.email === 'string' && { email: obj.email }),
68
+ ...(typeof obj.customerId === 'string' && { customerId: obj.customerId }),
69
+ ...(typeof obj.plan === 'string' && { plan: obj.plan }),
70
+ };
71
+ }
72
+ catch {
73
+ return null;
74
+ }
75
+ }
76
+ /* ------------------------------------------------------------------ */
77
+ /* Resolvers */
78
+ /* ------------------------------------------------------------------ */
79
+ /**
80
+ * Derive a greeting first-name from an email's local part. The split
81
+ * is intentionally aggressive: dot-separated, hyphen-separated, and
82
+ * digit-stripped so `yuriy.bulah@gmail.com` resolves к "Yuriy" instead
83
+ * of "yuriy.bulah". Title-cases the first segment. Falls back к
84
+ * "operator" when no email is available.
85
+ *
86
+ * Export so the spec can lock the heuristic against edge cases (numeric
87
+ * local part, plus-tag aliases, single-letter local part).
88
+ */
89
+ export function deriveGreetingName(email) {
90
+ if (!email || typeof email !== 'string')
91
+ return 'operator';
92
+ const at = email.indexOf('@');
93
+ if (at <= 0)
94
+ return 'operator';
95
+ const local = email.slice(0, at);
96
+ // Strip plus-tag aliases (`name+tag@host` → `name`).
97
+ const noTag = local.split('+')[0] ?? local;
98
+ // First dot / hyphen / underscore segment wins; this is the
99
+ // colloquial "given name" surface for the vast majority of work
100
+ // emails ("first.last@..." / "first-last@...").
101
+ const firstSegment = noTag.split(/[._-]/)[0] ?? noTag;
102
+ // Strip trailing digits some signup flows append (`yurii2@`) so the
103
+ // greeting reads as a name, not a username.
104
+ const stripped = firstSegment.replace(/[0-9]+$/u, '');
105
+ if (stripped.length === 0)
106
+ return 'operator';
107
+ return stripped.charAt(0).toUpperCase() + stripped.slice(1);
108
+ }
109
+ /**
110
+ * Resolve the model display string. Priority:
111
+ *
112
+ * 1. `PUGI_ENGINE_MODEL_CODE` env override (operator-set in shell).
113
+ * 2. `.pugi/settings.json` → `defaultModel` field (per-workspace).
114
+ * 3. Locked α7 default `"Sonnet 4.6 (1M context)"`.
115
+ *
116
+ * Returns the value verbatim — the banner is responsible for trimming
117
+ * if the string is too long for the column.
118
+ */
119
+ export function resolveModelDisplay(env, settingsOverride) {
120
+ const envModel = env.PUGI_ENGINE_MODEL_CODE;
121
+ if (typeof envModel === 'string' && envModel.length > 0)
122
+ return envModel;
123
+ const settingsModel = settingsOverride?.defaultModel;
124
+ if (typeof settingsModel === 'string' && settingsModel.length > 0) {
125
+ return settingsModel;
126
+ }
127
+ return 'Sonnet 4.6 (1M context)';
128
+ }
129
+ /**
130
+ * Locate the bundled CHANGELOG.md. The CLI ships к
131
+ * `node_modules/@pugi/cli/dist/tui/welcome-data.js` so the changelog
132
+ * lives at `node_modules/@pugi/cli/CHANGELOG.md` — two directory hops
133
+ * up from this file. In a local pnpm dev checkout the structure is the
134
+ * same (`src/tui/` ⇒ `../../CHANGELOG.md`) because tsx re-resolves the
135
+ * same relative tree.
136
+ */
137
+ function defaultChangelogPath() {
138
+ const here = dirname(fileURLToPath(import.meta.url));
139
+ return resolvePath(here, '..', '..', 'CHANGELOG.md');
140
+ }
141
+ /**
142
+ * Read top-3 "what's new" headlines from the CLI CHANGELOG.md. Each
143
+ * headline is `<version> — <first non-empty section body line>` trimmed
144
+ * к 60 chars so the right column does not wrap on a 100-col terminal.
145
+ * Returns an empty array when the file is missing / unparseable.
146
+ *
147
+ * The body-line heuristic walks к the first non-empty, non-section-
148
+ * header line below the version header. For Keep-a-Changelog entries
149
+ * the second line is usually `### Added` / `### Fixed`; we skip those
150
+ * headers и land on the first bullet, which is the operator-visible
151
+ * one-liner ("- L30 `pugi theme` ...").
152
+ */
153
+ export function readWhatsNew(changelogPath) {
154
+ const path = changelogPath ?? defaultChangelogPath();
155
+ try {
156
+ if (!existsSync(path))
157
+ return [];
158
+ const raw = readFileSync(path, 'utf8');
159
+ if (!raw || raw.length === 0)
160
+ return [];
161
+ const sections = parseChangelog(raw);
162
+ const headlines = [];
163
+ for (const section of sections) {
164
+ if (headlines.length >= 3)
165
+ break;
166
+ // Skip the "[Unreleased]" / "[Unreleased] - YYYY-MM-DD" section —
167
+ // the banner is meant к surface SHIPPED notes only. The parser
168
+ // captures Unreleased identically к а tagged version, so we
169
+ // filter here.
170
+ if (/^unreleased$/i.test(section.version))
171
+ continue;
172
+ const firstBullet = pickFirstBullet(section.body);
173
+ if (!firstBullet)
174
+ continue;
175
+ const headline = `${section.version} — ${firstBullet}`;
176
+ headlines.push(truncate(headline, 60));
177
+ }
178
+ return headlines;
179
+ }
180
+ catch {
181
+ return [];
182
+ }
183
+ }
184
+ /**
185
+ * Pull the first bullet text out of a Keep-a-Changelog section body.
186
+ * Skips `### <subsection>` headers, blank lines, и non-bullet prose
187
+ * (release-note footers like "### Notes"). Returns undefined when no
188
+ * bullet is present.
189
+ *
190
+ * Body lines arrive verbatim from the parser; bullets follow the
191
+ * `- ` / `* ` Markdown convention. We strip the leading bullet glyph
192
+ * + the first whitespace run so the rendered string is the bullet
193
+ * content only.
194
+ */
195
+ function pickFirstBullet(body) {
196
+ const lines = body.split('\n');
197
+ for (const line of lines) {
198
+ const trimmed = line.trim();
199
+ if (trimmed.length === 0)
200
+ continue;
201
+ if (trimmed.startsWith('### '))
202
+ continue;
203
+ if (trimmed.startsWith('- ') || trimmed.startsWith('* ')) {
204
+ // Strip leading bullet glyph + ws and collapse internal MD ticks.
205
+ return trimmed
206
+ .slice(2)
207
+ .replace(/`/g, '')
208
+ .replace(/\*\*/g, '')
209
+ .trim();
210
+ }
211
+ }
212
+ return undefined;
213
+ }
214
+ function truncate(text, max) {
215
+ if (text.length <= max)
216
+ return text;
217
+ // Reserve 1 char for the ellipsis so we land on EXACTLY `max`
218
+ // visible chars including the dot. Single Unicode ellipsis would be
219
+ // narrower but some terminals render it as а full-width glyph in
220
+ // CJK locales — ASCII three-dot stays predictable.
221
+ return `${text.slice(0, max - 1).trimEnd()}…`;
222
+ }
223
+ /**
224
+ * Resolve the active credential and decode the JWT principal. Returns
225
+ * the email / tenant / plan triple when authenticated, null fields when
226
+ * anonymous. Encapsulates the credential + JWT IO so the spec can drive
227
+ * the resolver via an env override.
228
+ */
229
+ function resolvePrincipal(env, home) {
230
+ const credential = resolveActiveCredential(env, home);
231
+ if (!credential) {
232
+ const file = readCredentialsFile(home);
233
+ return {
234
+ apiUrl: normalizeApiUrl(env.PUGI_API_URL ?? file.activeApiUrl ?? DEFAULT_API_URL),
235
+ };
236
+ }
237
+ const principal = decodeJwtPayload(credential.apiKey);
238
+ return {
239
+ apiUrl: credential.apiUrl,
240
+ ...(principal?.email && { email: principal.email }),
241
+ ...(principal?.customerId && { tenant: principal.customerId }),
242
+ ...(principal?.plan && { plan: principal.plan }),
243
+ };
244
+ }
245
+ /**
246
+ * Best-effort read of `.pugi/settings.json`. Returns null when the file
247
+ * is missing / unreadable / malformed so the model resolver can fall
248
+ * back to the env override + locked default.
249
+ */
250
+ function readSettingsBlob(cwd) {
251
+ try {
252
+ const path = resolvePath(cwd, '.pugi', 'settings.json');
253
+ if (!existsSync(path))
254
+ return null;
255
+ const raw = readFileSync(path, 'utf8');
256
+ if (!raw || raw.length === 0)
257
+ return null;
258
+ const obj = JSON.parse(raw);
259
+ if (typeof obj?.defaultModel === 'string') {
260
+ return { defaultModel: obj.defaultModel };
261
+ }
262
+ return {};
263
+ }
264
+ catch {
265
+ return null;
266
+ }
267
+ }
268
+ /* ------------------------------------------------------------------ */
269
+ /* Entry point */
270
+ /* ------------------------------------------------------------------ */
271
+ export function collectWelcomeData(input) {
272
+ const env = input.env ?? process.env;
273
+ const home = input.home ?? homedir();
274
+ const cwd = input.cwd ?? process.cwd();
275
+ const principal = resolvePrincipal(env, home);
276
+ const settings = input.settingsOverride !== undefined
277
+ ? input.settingsOverride
278
+ : readSettingsBlob(cwd);
279
+ const greetingName = deriveGreetingName(principal.email);
280
+ const model = resolveModelDisplay(env, settings);
281
+ const whatsNew = readWhatsNew(input.changelogPath);
282
+ return {
283
+ greetingName,
284
+ ...(principal.email && { email: principal.email }),
285
+ ...(principal.tenant && { tenant: principal.tenant }),
286
+ ...(principal.plan && { plan: principal.plan }),
287
+ model,
288
+ cwd,
289
+ cliVersion: input.cliVersion,
290
+ whatsNew,
291
+ };
292
+ }
293
+ //# sourceMappingURL=welcome-data.js.map
@@ -0,0 +1,10 @@
1
+ {
2
+ "servers": {
3
+ "codegraph": {
4
+ "command": "codegraph",
5
+ "args": ["serve", "--mcp"],
6
+ "env": {},
7
+ "trust": "pending"
8
+ }
9
+ }
10
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pugi/cli",
3
- "version": "0.1.0-beta.5",
3
+ "version": "0.1.0-beta.51",
4
4
  "description": "Pugi CLI - terminal-native software execution system",
5
5
  "homepage": "https://pugi.io",
6
6
  "repository": {
@@ -29,8 +29,11 @@
29
29
  "bin/run.js",
30
30
  "dist/**/*.js",
31
31
  "assets/**/*.ansi",
32
+ "docs/examples/**/*.json",
33
+ "test/scenarios/**/*.scenario.txt",
32
34
  "README.md",
33
- "LICENSE"
35
+ "LICENSE",
36
+ "THIRD_PARTY_NOTICES.md"
34
37
  ],
35
38
  "engines": {
36
39
  "node": ">=22.5.0"
@@ -46,13 +49,13 @@
46
49
  "ink": "^5.0.1",
47
50
  "linkedom": "^0.18.12",
48
51
  "react": "^18.3.1",
49
- "tar": "^6.2.1",
52
+ "tar": "^7.5.11",
50
53
  "tinyglobby": "^0.2.16",
51
54
  "turndown": "^7.2.4",
52
55
  "undici": "^8.3.0",
53
56
  "zod": "^3.23.0",
54
- "@pugi/personas": "0.1.2",
55
- "@pugi/sdk": "0.1.0-beta.5"
57
+ "@pugi/sdk": "0.1.0-beta.51",
58
+ "@pugi/personas": "0.1.2"
56
59
  },
57
60
  "devDependencies": {
58
61
  "@types/node": "^22.0.0",
@@ -67,9 +70,12 @@
67
70
  "build": "pnpm --filter @pugi/personas --filter @pugi/sdk build && tsc -p tsconfig.json && node scripts/make-bin-executable.mjs",
68
71
  "dev": "tsx src/index.ts",
69
72
  "typecheck": "pnpm --filter @pugi/personas --filter @pugi/sdk build && tsc -p tsconfig.json --noEmit",
70
- "test": "pnpm run build && node --test --import tsx 'test/**/*.spec.ts' 'test/**/*.spec.tsx'",
73
+ "test": "pnpm run check:version-lockstep && pnpm run build && node --test --import tsx 'test/**/*.spec.ts' 'test/**/*.spec.tsx' 'src/**/*.spec.ts' 'src/**/*.spec.tsx'",
74
+ "test:integration": "INTEGRATION=1 pnpm run build && node --test --import tsx 'test/**/*.spec.ts' 'test/**/*.spec.tsx' 'src/**/*.spec.ts' 'src/**/*.spec.tsx'",
71
75
  "version:cli": "tsx src/index.ts version",
72
76
  "doctor": "tsx src/index.ts doctor --json",
73
- "pack:smoke": "node scripts/pack-smoke.mjs"
77
+ "check:version-lockstep": "bash ../../scripts/check-version-lockstep.sh",
78
+ "pack:smoke": "pnpm run check:version-lockstep && node scripts/pack-smoke.mjs",
79
+ "release-gate": "node scripts/secret-scanner.mjs"
74
80
  }
75
81
  }
@@ -0,0 +1,13 @@
1
+ # scenario: codegen-create-file
2
+ # title: Pugi actually writes a file (not just describes it)
3
+
4
+ # Anti-regression for task #265 — Pugi must call Write, not describe what
5
+ # it would write. The headless stub responder parses
6
+ # "create FILE with content 'TEXT'" and runs the real fs write so the
7
+ # scenario validates the wiring end-to-end. When Phase 2 lands the live
8
+ # engine path the same scenario carries over verbatim.
9
+
10
+ > "create hello.txt with content 'hello world'"
11
+ EXPECT: tool-call kind=Write file=hello.txt
12
+ EXPECT: persona-turn contains "wrote hello.txt"
13
+ EXPECT_FILE: hello.txt exists with content "hello world"
@@ -0,0 +1,11 @@
1
+ # scenario: compact-force
2
+ # title: /compact --force is acknowledged
3
+
4
+ # Phase 1: the stub responder echoes the directive verbatim — Phase 2
5
+ # wires the slash command through to the live compaction loop. Until
6
+ # then this scenario protects the contract between scenario authoring
7
+ # and the harness envelope shape.
8
+
9
+ > "/compact --force"
10
+ EXPECT: persona-turn contains "/compact"
11
+ EXPECT_NOT: persona-turn contains "Mira"
@@ -0,0 +1,11 @@
1
+ # scenario: identity
2
+ # title: Pugi self-identifies as Pugi (never as Mira)
3
+
4
+ # CEO directive feedback_live_console_test_every_publish: every published
5
+ # beta must still answer "ты кто?" with the Pugi persona, never the legacy
6
+ # Mira name. This is the single most-asked dialog in CEO dogfood and the
7
+ # regression that has bitten us most often.
8
+
9
+ > "ты кто?"
10
+ EXPECT: persona-turn contains "Pugi" OR "Пуджи"
11
+ EXPECT_NOT: persona-turn contains "Mira" OR "Мира"
@@ -0,0 +1,11 @@
1
+ # scenario: persona-handoff
2
+ # title: Pugi hands off to Hiroshi when the operator asks for dev work
3
+
4
+ # Phase 1: the headless stub currently echoes input via the Pugi persona
5
+ # (real dispatch routing wires in Phase 2). The scenario is authored
6
+ # against the Phase 2 contract — for now it documents the intent and
7
+ # the harness keeps it as a soft EXPECT so the corpus stays loadable.
8
+
9
+ > "Hiroshi, please review the authentication module"
10
+ EXPECT: persona-turn contains "Pugi" OR "Hiroshi" OR "Хироси"
11
+ EXPECT_NOT: persona-turn contains "Mira"
@@ -0,0 +1,12 @@
1
+ # scenario: walkback
2
+ # title: Esc-Esc walkback / rewind directive recognized
3
+
4
+ # CEO parity ask (BIG TRACK 8): the operator must be able to walk back
5
+ # the last turn. Phase 1 ships the harness contract — the live walkback
6
+ # wiring lands in BIG TRACK 8. The scenario asserts the stub responder
7
+ # acknowledges the directive verbatim so the contract holds while the
8
+ # real implementation lands behind it.
9
+
10
+ > "/rewind 1"
11
+ EXPECT: persona-turn contains "/rewind"
12
+ EXPECT_NOT: persona-turn contains "Mira"
@@ -1,154 +0,0 @@
1
- /**
2
- * Engine loop integration point for the six-tier compaction engine.
3
- *
4
- * `maybeCompactAfterTool` is the single function the engine loop calls
5
- * after each tool result has been appended to the transcript. It:
6
- *
7
- * 1. Estimates current context-window pressure (transcript bytes
8
- * against the model's budget, plus the static blocks).
9
- * 2. Calls `selectTier` on the snapshot.
10
- * 3. Runs the tier. Microcompact / cached_microcompact are sync;
11
- * reactive_summary / session_memory / full_compaction / reset
12
- * are async-shaped (the call returns before commit when run
13
- * against a long transcript) but currently run inline — the
14
- * engine loop is single-threaded today, so true backgrounding
15
- * waits for the SSE consumer refactor in α5.7.
16
- * 4. Runs invariant checks against the result. On any violation,
17
- * emits `compaction.invariant_violated` and returns the
18
- * pre-compaction transcript untouched.
19
- * 5. On success, emits `compaction.completed` with reclaim numbers
20
- * and returns the new transcript for the caller to adopt.
21
- * 6. On no-op, emits `compaction.skipped` and returns the original.
22
- *
23
- * Why a separate file (not inlined into `native-pugi.ts`):
24
- *
25
- * Sprint α5.3 (feat/pugi-cli-hooks-lifecycle-m1-gap-c) is in flight
26
- * and already modifies session.ts + tool-bridge + permission. Editing
27
- * native-pugi.ts in this PR risks a merge conflict against α5.3's
28
- * landing PR. Keeping the wiring as an exported helper means the
29
- * one-line callsite in native-pugi.ts can be added in a tiny
30
- * follow-up after both α5.3 and α5.5 have landed.
31
- *
32
- * Expected callsite in `apps/pugi-cli/src/core/engine/native-pugi.ts`,
33
- * inside `onToolResult`:
34
- *
35
- * ```ts
36
- * const compactionOutcome = await maybeCompactAfterTool({
37
- * session,
38
- * transcript: currentTranscript,
39
- * toolOutputs: recentToolOutputs,
40
- * contextBudgetUsed: estimatedTokens,
41
- * contextBudgetMax: budget.maxTokens,
42
- * workspaceRoot: root,
43
- * contextStaticHash: {
44
- * instructionsHash,
45
- * toolSchemaHash,
46
- * },
47
- * });
48
- * if (compactionOutcome.committed) {
49
- * currentTranscript = compactionOutcome.newTranscript;
50
- * }
51
- * ```
52
- */
53
- import { runCompaction, selectTier, } from '../context/compaction.js';
54
- import { checkInvariants } from '../context/invariants.js';
55
- import { emitCompactionCompleted, emitCompactionInvariantViolated, emitCompactionSkipped, emitCompactionStarted, } from '../context/compaction-events.js';
56
- /**
57
- * Engine-loop callback. See file header for the expected callsite shape.
58
- *
59
- * Contract:
60
- * - Never throws. All errors degrade to `committed: false` with the
61
- * original transcript and an event record.
62
- * - On `committed: true`, the caller MUST adopt `newTranscript` as
63
- * the live working transcript for the next model turn.
64
- * - On `committed: false`, the caller MUST keep the input transcript
65
- * and try again on the next tool turn (compaction will retry once
66
- * pressure stays above threshold).
67
- */
68
- export async function maybeCompactAfterTool(input) {
69
- const compactionInput = {
70
- sessionId: input.session.id,
71
- contextBudgetUsed: input.contextBudgetUsed,
72
- contextBudgetMax: input.contextBudgetMax,
73
- toolOutputs: input.toolOutputs,
74
- transcript: input.transcript,
75
- workspaceRoot: input.workspaceRoot,
76
- };
77
- const tier = selectTier(compactionInput);
78
- emitCompactionStarted(input.session, tier, {
79
- budgetUsed: input.contextBudgetUsed,
80
- budgetMax: input.contextBudgetMax,
81
- });
82
- let result;
83
- try {
84
- result = await runCompaction(compactionInput, tier);
85
- }
86
- catch (error) {
87
- const reason = error instanceof Error ? error.message : String(error);
88
- emitCompactionSkipped(input.session, tier, `compaction crashed: ${reason}`);
89
- return {
90
- committed: false,
91
- tier,
92
- newTranscript: input.transcript,
93
- bytesReclaimed: 0,
94
- newContextSize: byteSize(input.transcript),
95
- violations: [],
96
- skipped: true,
97
- skipReason: `crashed: ${reason}`,
98
- };
99
- }
100
- if (result.skipped) {
101
- emitCompactionSkipped(input.session, tier, result.skipReason || 'no work');
102
- return {
103
- committed: false,
104
- tier,
105
- newTranscript: input.transcript,
106
- bytesReclaimed: 0,
107
- newContextSize: byteSize(input.transcript),
108
- violations: [],
109
- skipped: true,
110
- skipReason: result.skipReason,
111
- };
112
- }
113
- // Invariant gate: static-hash-unchanged is enforced by passing the
114
- // same hashes in for `before` and `after` — compaction never touches
115
- // static blocks, so the hashes are equal by construction. We pass
116
- // both so the contract is explicit; if a future tier introduces a
117
- // bug that overwrites static state, the check still catches it.
118
- const violations = checkInvariants({
119
- before: compactionInput,
120
- after: result,
121
- summaryText: result.summaryText,
122
- staticHashBefore: input.contextStaticHash,
123
- staticHashAfter: input.contextStaticHash,
124
- });
125
- if (violations.length > 0) {
126
- for (const v of violations)
127
- emitCompactionInvariantViolated(input.session, v);
128
- return {
129
- committed: false,
130
- tier,
131
- newTranscript: input.transcript,
132
- bytesReclaimed: 0,
133
- newContextSize: byteSize(input.transcript),
134
- violations,
135
- skipped: false,
136
- skipReason: '',
137
- };
138
- }
139
- emitCompactionCompleted(input.session, tier, result.bytesReclaimed, result.newContextSize, result.artifactsCreated);
140
- return {
141
- committed: true,
142
- tier,
143
- newTranscript: result.newTranscript,
144
- bytesReclaimed: result.bytesReclaimed,
145
- newContextSize: result.newContextSize,
146
- violations: [],
147
- skipped: false,
148
- skipReason: '',
149
- };
150
- }
151
- function byteSize(transcript) {
152
- return transcript.reduce((sum, t) => sum + Buffer.byteLength(t.content, 'utf8'), 0);
153
- }
154
- //# sourceMappingURL=compaction-hook.js.map