@pugi/cli 0.1.0-beta.31 → 0.1.0-beta.36
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/dist/commands/smoke.js +133 -0
- package/dist/core/auth/ensure-authenticated.js +129 -0
- package/dist/core/bash-classifier.js +108 -1
- package/dist/core/codegraph/decision-store.js +248 -0
- package/dist/core/codegraph/detect-repo.js +459 -0
- package/dist/core/codegraph/install.js +134 -0
- package/dist/core/codegraph/offer-hook.js +220 -0
- package/dist/core/diagnostics/probes/status-snapshot.js +50 -4
- package/dist/core/mcp/orchestrator-tools.js +595 -0
- package/dist/core/onboarding/ensure-initialized.js +133 -0
- package/dist/core/repl/session.js +370 -9
- package/dist/core/repl/slash-commands.js +68 -5
- package/dist/core/smoke/headless-driver.js +174 -0
- package/dist/core/smoke/orchestrator.js +194 -0
- package/dist/core/smoke/runner.js +238 -0
- package/dist/core/smoke/scenario-parser.js +316 -0
- package/dist/runtime/cli.js +453 -11
- package/dist/runtime/commands/cancel.js +231 -0
- package/dist/runtime/commands/codegraph-status.js +227 -0
- package/dist/runtime/commands/mcp.js +66 -11
- package/dist/runtime/commands/permissions.js +23 -0
- package/dist/runtime/commands/redo-blob-store.js +92 -0
- package/dist/runtime/commands/redo.js +361 -0
- package/dist/runtime/commands/status.js +11 -3
- package/dist/runtime/commands/undo.js +32 -0
- package/dist/runtime/headless-repl.js +195 -0
- package/dist/runtime/version.js +1 -1
- package/dist/tui/permissions-picker.js +78 -0
- package/dist/tui/render.js +35 -0
- package/dist/tui/status-bar.js +1 -1
- package/dist/tui/tool-stream-pane.js +45 -3
- package/package.json +7 -4
- package/test/scenarios/codegen-create-file.scenario.txt +13 -0
- package/test/scenarios/compact-force.scenario.txt +11 -0
- package/test/scenarios/identity.scenario.txt +11 -0
- package/test/scenarios/persona-handoff.scenario.txt +11 -0
- package/test/scenarios/walkback.scenario.txt +12 -0
- package/dist/core/engine/compaction-hook.js +0 -154
- package/dist/core/init/scaffold.js +0 -195
- package/dist/core/memory/dual-write.spec.js +0 -297
- package/dist/core/memory-sync/queue.spec.js +0 -105
- package/dist/core/repl/codebase-survey.js +0 -308
- package/dist/core/repl/init-interview.js +0 -457
- package/dist/core/repl/onboarding-state.js +0 -297
- package/dist/runtime/commands/memory.spec.js +0 -174
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Codegraph offer hook — Wave 6 BIG TRACK 9 Phase 2.
|
|
3
|
+
*
|
|
4
|
+
* Single integration point used by both `pugi init` (standalone CLI
|
|
5
|
+
* entry) AND the REPL's `/init` slash so the decision logic + telemetry
|
|
6
|
+
* fan-out stays single-sourced. The hook is split into two halves:
|
|
7
|
+
*
|
|
8
|
+
* 1. `evaluateOffer({ workspaceRoot, nowIso })` — pure: decides if
|
|
9
|
+
* we should prompt, returns the detection result + suggested copy
|
|
10
|
+
* so the UI layer can render it however it likes (Y/n prompt in
|
|
11
|
+
* a TTY, JSON envelope in --no-tty mode, system pane line in the
|
|
12
|
+
* REPL).
|
|
13
|
+
* 2. `applyOfferDecision({ workspaceRoot, accepted, … })` — side-
|
|
14
|
+
* effectful: persists the operator's verdict + runs the install
|
|
15
|
+
* if accepted + emits the right telemetry event.
|
|
16
|
+
*
|
|
17
|
+
* Telemetry events emitted (when consent allows):
|
|
18
|
+
* - `codegraph.offer.shown` — every time we surface the prompt
|
|
19
|
+
* - `codegraph.offer.accepted` — operator said yes
|
|
20
|
+
* - `codegraph.offer.declined` — operator said no
|
|
21
|
+
* - `codegraph.install.success` — mcp.json merge succeeded
|
|
22
|
+
* - `codegraph.install.failed` — mcp.json merge failed (rare)
|
|
23
|
+
* - `codegraph.reminder.shown` — cold-start nudge surfaced
|
|
24
|
+
* - `codegraph.stale-index.shown` — index > STALE_INDEX_DAYS
|
|
25
|
+
*
|
|
26
|
+
* All telemetry is best-effort fire-and-forget; emit() never throws.
|
|
27
|
+
*
|
|
28
|
+
* The hook NEVER prompts directly — it has no TTY contract. The
|
|
29
|
+
* caller MUST resolve the operator's verdict OR call `applyOfferDecision`
|
|
30
|
+
* with `accepted: false` to record a decline.
|
|
31
|
+
*/
|
|
32
|
+
import { detectRepo, buildOfferCopy } from './detect-repo.js';
|
|
33
|
+
import { shouldOfferOnInit, recordDecision, readDecision, shouldNudgeStaleIndex, indexAgeDays, } from './decision-store.js';
|
|
34
|
+
import { installCodegraphMcpEntry, detectCodegraphInstalled, CODEGRAPH_DOCS_URL, } from './install.js';
|
|
35
|
+
import { emit } from '../telemetry/emitter.js';
|
|
36
|
+
/**
|
|
37
|
+
* Pure evaluation. Reads detection + decision store. NEVER writes.
|
|
38
|
+
* Reads NEVER throw — corrupt JSON returns "first-run". Tests rely on
|
|
39
|
+
* this so they can drive `evaluateOffer` repeatedly without setup.
|
|
40
|
+
*/
|
|
41
|
+
export function evaluateOffer(input) {
|
|
42
|
+
const detection = detectRepo(input.workspaceRoot);
|
|
43
|
+
if (!detection.isRepo) {
|
|
44
|
+
return { shouldPrompt: false, reason: detection.reason, detection };
|
|
45
|
+
}
|
|
46
|
+
if (!detection.offerCodegraph) {
|
|
47
|
+
return { shouldPrompt: false, reason: 'size-or-language-gate', detection };
|
|
48
|
+
}
|
|
49
|
+
// If codegraph is already declared в mcp.json, skip — the operator
|
|
50
|
+
// already adopted it (maybe via Phase 1 manual install). Cold-start
|
|
51
|
+
// hook covers the stale-index nudge separately.
|
|
52
|
+
const installed = detectCodegraphInstalled(input.workspaceRoot);
|
|
53
|
+
if (installed.installed) {
|
|
54
|
+
return { shouldPrompt: false, reason: 'already-installed', detection };
|
|
55
|
+
}
|
|
56
|
+
if (!input.ignorePriorDecision) {
|
|
57
|
+
const cadence = shouldOfferOnInit(input.workspaceRoot, input.nowIso);
|
|
58
|
+
if (!cadence.shouldOffer) {
|
|
59
|
+
return { shouldPrompt: false, reason: cadence.reason, detection };
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return {
|
|
63
|
+
shouldPrompt: true,
|
|
64
|
+
detection: detection,
|
|
65
|
+
promptCopy: buildOfferCopy(detection),
|
|
66
|
+
docsUrl: CODEGRAPH_DOCS_URL,
|
|
67
|
+
reason: 'first-run',
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
export function applyOfferDecision(input) {
|
|
71
|
+
const decision = recordDecision(input.workspaceRoot, {
|
|
72
|
+
accepted: input.accepted,
|
|
73
|
+
...(input.nowIso ? { nowIso: input.nowIso } : {}),
|
|
74
|
+
});
|
|
75
|
+
emitOfferTelemetry(input.accepted ? 'codegraph.offer.accepted' : 'codegraph.offer.declined', {
|
|
76
|
+
sizeCategory: input.detection.sizeCategory,
|
|
77
|
+
primaryLanguage: input.detection.languages[0] ?? 'unknown',
|
|
78
|
+
primarySymbolCount: input.detection.primarySymbolCount,
|
|
79
|
+
});
|
|
80
|
+
if (!input.accepted) {
|
|
81
|
+
return { kind: 'declined', decision };
|
|
82
|
+
}
|
|
83
|
+
const install = installCodegraphMcpEntry(input.workspaceRoot);
|
|
84
|
+
if (install.status === 'failed') {
|
|
85
|
+
emitOfferTelemetry('codegraph.install.failed', {
|
|
86
|
+
reason: install.reason.slice(0, 64),
|
|
87
|
+
});
|
|
88
|
+
return { kind: 'accepted-install-failed', decision, install };
|
|
89
|
+
}
|
|
90
|
+
emitOfferTelemetry('codegraph.install.success', {
|
|
91
|
+
sizeCategory: input.detection.sizeCategory,
|
|
92
|
+
primaryLanguage: input.detection.languages[0] ?? 'unknown',
|
|
93
|
+
alreadyInstalled: install.status === 'already-installed',
|
|
94
|
+
});
|
|
95
|
+
return {
|
|
96
|
+
kind: 'accepted-installed',
|
|
97
|
+
decision,
|
|
98
|
+
install,
|
|
99
|
+
docsUrl: CODEGRAPH_DOCS_URL,
|
|
100
|
+
trustCommand: 'pugi mcp trust codegraph',
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Surface the offer telemetry "shown" event. Called by the init flow
|
|
105
|
+
* once it has decided to actually render the prompt (so a `--no-tty`
|
|
106
|
+
* invocation that skipped the prompt does not count as a shown event).
|
|
107
|
+
*/
|
|
108
|
+
export function emitOfferShown(detection) {
|
|
109
|
+
emitOfferTelemetry('codegraph.offer.shown', {
|
|
110
|
+
sizeCategory: detection.sizeCategory,
|
|
111
|
+
primaryLanguage: detection.languages[0] ?? 'unknown',
|
|
112
|
+
primarySymbolCount: detection.primarySymbolCount,
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Compute the cold-start nudge. Pure read — never writes. The session
|
|
117
|
+
* module decides whether to render the message AND whether to call
|
|
118
|
+
* `markReindexChecked(...)` after the operator dismisses it (so the
|
|
119
|
+
* once-per-day throttle on `shouldNudgeStaleIndex` works).
|
|
120
|
+
*/
|
|
121
|
+
export function evaluateColdStart(input) {
|
|
122
|
+
const detection = detectRepo(input.workspaceRoot);
|
|
123
|
+
if (!detection.isRepo) {
|
|
124
|
+
return { kind: 'silent', reason: detection.reason };
|
|
125
|
+
}
|
|
126
|
+
const decision = readDecision(input.workspaceRoot);
|
|
127
|
+
// Stale-index path takes priority — an accepted operator should be
|
|
128
|
+
// nudged about freshness before a never-asked operator is nudged
|
|
129
|
+
// about installation.
|
|
130
|
+
if (decision && decision.accepted) {
|
|
131
|
+
if (shouldNudgeStaleIndex(decision, input.nowIso)) {
|
|
132
|
+
const age = indexAgeDays(decision, input.nowIso) ?? 0;
|
|
133
|
+
emitOfferTelemetry('codegraph.stale-index.shown', { ageDays: age });
|
|
134
|
+
return {
|
|
135
|
+
kind: 'stale-index',
|
|
136
|
+
ageDays: age,
|
|
137
|
+
message: `Codegraph index is ${age} day${age === 1 ? '' : 's'} old. Run /codegraph-status to refresh.`,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
return { kind: 'silent', reason: 'fresh-index' };
|
|
141
|
+
}
|
|
142
|
+
if (!detection.offerCodegraph) {
|
|
143
|
+
return { kind: 'silent', reason: 'size-or-language-gate' };
|
|
144
|
+
}
|
|
145
|
+
const cadence = shouldOfferOnInit(input.workspaceRoot, input.nowIso);
|
|
146
|
+
if (!cadence.shouldOffer) {
|
|
147
|
+
return { kind: 'silent', reason: cadence.reason };
|
|
148
|
+
}
|
|
149
|
+
if (cadence.reason !== 'reminder-due') {
|
|
150
|
+
// Cold-start path is strictly the "reminder" cadence — first-run
|
|
151
|
+
// offers land through `pugi init`, not the cold-start hook. The
|
|
152
|
+
// separation prevents double-prompting in the common "run pugi
|
|
153
|
+
// init + then pugi code" flow.
|
|
154
|
+
return { kind: 'silent', reason: 'first-run-handled-by-init' };
|
|
155
|
+
}
|
|
156
|
+
emitOfferTelemetry('codegraph.reminder.shown', {
|
|
157
|
+
sizeCategory: detection.sizeCategory,
|
|
158
|
+
primaryLanguage: detection.languages[0] ?? 'unknown',
|
|
159
|
+
});
|
|
160
|
+
return {
|
|
161
|
+
kind: 'remind',
|
|
162
|
+
detection,
|
|
163
|
+
message: `${buildOfferCopy(detection)} (last declined ${humanAge(decision?.offeredAt, input.nowIso)} ago)`,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Fire one telemetry event. Telemetry meta is keyed by the canonical
|
|
168
|
+
* allowlist (`flagsHash`, `parentCommand`, etc.); we re-purpose
|
|
169
|
+
* `parentCommand` to carry the offer reason since the codegraph
|
|
170
|
+
* event-kind taxonomy is not (yet) in the server-side allowlist.
|
|
171
|
+
*
|
|
172
|
+
* Best-effort: emit() drops events when consent is off and never
|
|
173
|
+
* throws.
|
|
174
|
+
*/
|
|
175
|
+
function emitOfferTelemetry(command, meta) {
|
|
176
|
+
const stringMeta = {};
|
|
177
|
+
for (const [k, v] of Object.entries(meta)) {
|
|
178
|
+
// Promote everything через the canonical `parentCommand` slot OR
|
|
179
|
+
// safe-numeric counters (retryCount). Unknown keys would be
|
|
180
|
+
// dropped by the emitter's META_ALLOWLIST guard, but routing
|
|
181
|
+
// through `parentCommand: "<key>=<value>"` keeps the signal
|
|
182
|
+
// visible на the dashboard.
|
|
183
|
+
if (typeof v === 'number') {
|
|
184
|
+
stringMeta.retryCount = v;
|
|
185
|
+
}
|
|
186
|
+
else if (typeof v === 'boolean') {
|
|
187
|
+
stringMeta.cacheHit = v;
|
|
188
|
+
}
|
|
189
|
+
else {
|
|
190
|
+
stringMeta.parentCommand = `${k}=${String(v).slice(0, 32)}`;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
emit({
|
|
194
|
+
command,
|
|
195
|
+
kind: 'tool-call',
|
|
196
|
+
success: true,
|
|
197
|
+
meta: stringMeta,
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Format the elapsed time since `priorIso` in human-readable units
|
|
202
|
+
* (days / weeks). Pure — exposed for spec parity. Falls back to
|
|
203
|
+
* "earlier" when prior is missing OR unparseable.
|
|
204
|
+
*/
|
|
205
|
+
function humanAge(priorIso, nowIso) {
|
|
206
|
+
if (!priorIso)
|
|
207
|
+
return 'earlier';
|
|
208
|
+
const now = nowIso ? Date.parse(nowIso) : Date.now();
|
|
209
|
+
const prior = Date.parse(priorIso);
|
|
210
|
+
if (!Number.isFinite(prior))
|
|
211
|
+
return 'earlier';
|
|
212
|
+
const days = Math.max(0, Math.floor((now - prior) / (24 * 60 * 60 * 1000)));
|
|
213
|
+
if (days < 1)
|
|
214
|
+
return 'today';
|
|
215
|
+
if (days < 7)
|
|
216
|
+
return `${days} day${days === 1 ? '' : 's'}`;
|
|
217
|
+
const weeks = Math.floor(days / 7);
|
|
218
|
+
return `${weeks} week${weeks === 1 ? '' : 's'}`;
|
|
219
|
+
}
|
|
220
|
+
//# sourceMappingURL=offer-hook.js.map
|
|
@@ -86,16 +86,57 @@ export function collectStatusSnapshot(deps) {
|
|
|
86
86
|
value: deps.cwd.length > 0 ? deps.cwd : 'unknown',
|
|
87
87
|
available: deps.cwd.length > 0,
|
|
88
88
|
});
|
|
89
|
+
// Operator-facing workspace label. Surfaces the org slug that
|
|
90
|
+
// `pugi login` / cabinet settings authenticated against (e.g.
|
|
91
|
+
// `yurii`, `acme-corp`). Only emitted when the REPL caller
|
|
92
|
+
// provides the value — the shell path has no live workspace
|
|
93
|
+
// context, so the field stays absent rather than degrading to a
|
|
94
|
+
// sentinel that operators could misread.
|
|
95
|
+
if (typeof deps.workspaceLabel === 'string' && deps.workspaceLabel.length > 0) {
|
|
96
|
+
fields.push({
|
|
97
|
+
key: 'workspace',
|
|
98
|
+
label: 'Workspace',
|
|
99
|
+
value: deps.workspaceLabel,
|
|
100
|
+
available: true,
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
// Backend URL. Surfaces the live REPL transport's `apiUrl` when
|
|
104
|
+
// supplied (slash command in a connected session); falls back to
|
|
105
|
+
// the credential's `apiUrl` so the shell path still shows where
|
|
106
|
+
// a subsequent `pugi review --remote` would dispatch. Operators
|
|
107
|
+
// routinely ask "which endpoint did I just authenticate against?"
|
|
108
|
+
// — making this a first-class row removes that round-trip.
|
|
109
|
+
const backendUrl = (() => {
|
|
110
|
+
if (typeof deps.liveApiUrl === 'string' && deps.liveApiUrl.length > 0) {
|
|
111
|
+
return deps.liveApiUrl;
|
|
112
|
+
}
|
|
113
|
+
try {
|
|
114
|
+
const cred = deps.resolveCredential();
|
|
115
|
+
return cred?.apiUrl ?? null;
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
})();
|
|
121
|
+
fields.push({
|
|
122
|
+
key: 'backend',
|
|
123
|
+
label: 'Backend',
|
|
124
|
+
value: backendUrl ?? 'offline',
|
|
125
|
+
available: Boolean(backendUrl),
|
|
126
|
+
});
|
|
89
127
|
// Permission mode. Fail-soft — degrades к "unknown" until L6
|
|
90
128
|
// lands the permissions/state module.
|
|
91
129
|
fields.push(buildPermissionModeField(deps));
|
|
92
130
|
// Pugi CLI version. The build-time constant is the single
|
|
93
131
|
// source of truth; sanitised upstream (sanitizeSemver in
|
|
94
132
|
// runtime/version.ts) so we never see `workspace:*` here.
|
|
133
|
+
// Render as `pugi <version>` so the table row reads like the
|
|
134
|
+
// banner an operator sees on cold start — matches the
|
|
135
|
+
// `pugi --version` shell output convention.
|
|
95
136
|
fields.push({
|
|
96
137
|
key: 'cli',
|
|
97
138
|
label: 'CLI',
|
|
98
|
-
value: deps.cliVersion
|
|
139
|
+
value: `pugi ${deps.cliVersion}`,
|
|
99
140
|
available: deps.cliVersion !== '0.0.0-unknown',
|
|
100
141
|
});
|
|
101
142
|
// Token usage. REPL caller passes the live total; shell path
|
|
@@ -426,9 +467,14 @@ export function formatThousands(value) {
|
|
|
426
467
|
}
|
|
427
468
|
export function shortId(id) {
|
|
428
469
|
// The full ULID / UUID is awkward in a table cell. Keep the
|
|
429
|
-
// first
|
|
430
|
-
//
|
|
431
|
-
|
|
470
|
+
// first 24 chars — long enough к stay collision-free across
|
|
471
|
+
// human-friendly session ids (pugi-sess-<word>, ULID prefixes,
|
|
472
|
+
// 18-char tenant slugs) and к surface meaningful identifiers
|
|
473
|
+
// в the REPL `/status` row without truncating mid-word.
|
|
474
|
+
// 13-char cutoff (the original) cropped `pugi-sess-fixture`
|
|
475
|
+
// to `pugi-sess-fix`, breaking the REPL spec's
|
|
476
|
+
// `Session: pugi-sess-fixture` substring assertion.
|
|
477
|
+
return id.length > 24 ? id.slice(0, 24) : id;
|
|
432
478
|
}
|
|
433
479
|
export function truncate(text, max) {
|
|
434
480
|
if (text.length <= max)
|