@pugi/cli 0.1.0-beta.87 → 0.1.0-beta.88
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 +36 -0
- package/LICENSE +1 -1
- package/dist/core/agents/registry.js +1 -1
- package/dist/core/checkpoints/shadow-git.js +1 -1
- package/dist/core/context/compaction.js +1 -1
- package/dist/core/denial-tracking/state.js +1 -1
- package/dist/core/edits/fuzzy-ladder.js +1 -1
- package/dist/core/edits/layer-a-fuzzy-apply.js +1 -1
- package/dist/core/engine/anvil-client.js +13 -2
- package/dist/core/mcp/server-tools.js +1 -1
- package/dist/core/mcp/server.js +1 -1
- package/dist/core/memory/secret-scanner.js +6 -6
- package/dist/core/onboarding/ensure-initialized.js +1 -1
- package/dist/core/plans/plan-artifact.js +2 -2
- package/dist/core/repl/cap-warning.js +1 -1
- package/dist/core/routing/pre-flight-estimator.js +1 -1
- package/dist/core/settings.js +12 -0
- package/dist/index.js +8 -0
- package/dist/runtime/cli.js +68 -20
- package/dist/runtime/commands/config.js +41 -7
- package/dist/runtime/sigint-guard.js +272 -0
- package/dist/runtime/version.js +1 -1
- package/dist/skills/bundled/batch.js +2 -2
- package/dist/skills/bundled/index.js +3 -3
- package/dist/skills/bundled/loop.js +2 -2
- package/dist/skills/bundled/remember.js +1 -1
- package/dist/skills/bundled/simplify.js +1 -1
- package/dist/skills/bundled/skillify.js +2 -2
- package/dist/skills/bundled/stuck.js +1 -1
- package/dist/skills/bundled/verify.js +2 -2
- package/dist/testing/vcr.js +2 -2
- package/dist/tools/ask-user-question.js +66 -0
- package/dist/tools/bash.js +2 -2
- package/dist/tools/powershell.js +1 -1
- package/dist/tui/ask-user-question-chips.js +257 -0
- package/dist/tui/welcome-data.js +4 -4
- package/package.json +5 -4
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,42 @@ releases listed first. Section format: `## [<version>] - <YYYY-MM-DD>`.
|
|
|
7
7
|
The bundled `pugi release-notes` command parses this file and renders sections
|
|
8
8
|
strictly newer than `~/.pugi/.last-seen-version` after every upgrade.
|
|
9
9
|
|
|
10
|
+
## [0.1.0-beta.88] - 2026-06-02
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
- **Pugi identity intro no longer chants on later turns**. The output gate now
|
|
14
|
+
strips the canonical long-form intro ("I'm Pugi - your engineering copilot.
|
|
15
|
+
Tell me what you need...") and its Russian variant on mid-thread replies.
|
|
16
|
+
Operators were seeing the intro re-emit on turn 2+ after a session-resume
|
|
17
|
+
or autonomous tick.
|
|
18
|
+
- **Ctrl+C double-press exit guard**. A single Ctrl+C in the REPL no longer
|
|
19
|
+
kills the CLI. First press prompts "Press Ctrl+C again to exit (within 2s)
|
|
20
|
+
or any key to continue". Second press within 2s exits cleanly; any other
|
|
21
|
+
key cancels. Headless mode emits a `session-end` envelope and exits 0.
|
|
22
|
+
- **Persona no longer over-clarifies trivial creative tasks**. Asking for a
|
|
23
|
+
well-known game / demo / todo app now picks reasonable defaults and starts
|
|
24
|
+
building in one turn. Ambiguous tasks (auth, deploy targets) still ask
|
|
25
|
+
≤ 3 short choices via the `ask_user_question` tool.
|
|
26
|
+
|
|
27
|
+
### Added
|
|
28
|
+
- **Short-format AskUserQuestion chip renderer**. Up to 3 questions render
|
|
29
|
+
side-by-side as Ink chips with ▸ default highlight, ↑↓ in-question nav,
|
|
30
|
+
←→/Tab between questions, 1-9 jump, [s] skip-with-default, [Esc] cancel,
|
|
31
|
+
[Enter] commit. Labels truncate at 5 words / 22 chars. Caller can opt
|
|
32
|
+
into a non-TTY numbered fallback via `forceFallback`.
|
|
33
|
+
|
|
34
|
+
### Security
|
|
35
|
+
- **npm publish leak-gate hardening**. `prepublishOnly` now runs a
|
|
36
|
+
banned-string scan against the staged tarball (`tools/scrub/scan-tarball.sh`)
|
|
37
|
+
covering personal names, competitor refs, internal codenames, engineering
|
|
38
|
+
provenance, absolute paths, brand legacy, and secret-token shapes.
|
|
39
|
+
Per-line `[pugi-leak-ok]` allowlist marker for legitimate references;
|
|
40
|
+
path allowlist for LICENSE / THIRD_PARTY_NOTICES files where MIT requires
|
|
41
|
+
copyright-holder name. Synthetic injection regression test runs in CI.
|
|
42
|
+
|
|
43
|
+
### Chore
|
|
44
|
+
- MIT LICENSE copyright holder set to `Pugi.io` (was personal name).
|
|
45
|
+
|
|
10
46
|
## [0.1.0-beta.26] - 2026-05-27
|
|
11
47
|
|
|
12
48
|
### Added
|
package/LICENSE
CHANGED
|
@@ -30,7 +30,7 @@ function requirePersona(slug) {
|
|
|
30
30
|
/**
|
|
31
31
|
* CLI-only role-to-persona mapping. Roles are dispatcher-facing strings;
|
|
32
32
|
* personas come from the brand-canonical THE_TEN. Vera (qa) intentionally
|
|
33
|
-
* dual-roles as verifier + reviewer per
|
|
33
|
+
* dual-roles as verifier + reviewer per — the cabinet's review
|
|
34
34
|
* pipeline already merges the two surfaces.
|
|
35
35
|
*/
|
|
36
36
|
export const SUBAGENT_REGISTRY = [
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Per-task shadow git repo — file-state checkpoint surface.
|
|
3
3
|
*
|
|
4
4
|
* Inspired by Cline `CheckpointTracker.ts` (Apache-2.0).
|
|
5
|
-
*
|
|
5
|
+
* independent implementation TypeScript implementation following Pugi conventions.
|
|
6
6
|
*
|
|
7
7
|
* Goal: every Pugi-orchestrated file mutation lands in a per-task
|
|
8
8
|
* shadow git history kept entirely separate from the user's real
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* L11 — DenialTrackingState surface (the upstream tool parity).
|
|
3
3
|
*
|
|
4
|
-
* Per the upstream /
|
|
4
|
+
* Per the upstream / the upstream behavior (``
|
|
5
5
|
* §5.2): the upstream tool's `QueryEngine.ts` maintains a per-session
|
|
6
6
|
* `DenialTrackingState` that records every tool-dispatch denial.
|
|
7
7
|
* Subsequent turns receive a compact reminder so the model does not
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
* >= 0.8 to count as a match; below that we fail LOUD rather
|
|
31
31
|
* than write the wrong region.
|
|
32
32
|
*
|
|
33
|
-
* Inspired by Aider editblock_coder.py (Apache-2.0).
|
|
33
|
+
* Inspired by Aider editblock_coder.py (Apache-2.0). independent implementation
|
|
34
34
|
* implementation; no Aider source code copied.
|
|
35
35
|
*
|
|
36
36
|
* Pure functions throughout — every tier returns a structured result
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
* - `identical_replacement` — search and replace are identical;
|
|
20
20
|
* would no-op; surfaced LOUD.
|
|
21
21
|
*
|
|
22
|
-
* Inspired by Aider editblock_coder.py (Apache-2.0).
|
|
22
|
+
* Inspired by Aider editblock_coder.py (Apache-2.0). independent implementation
|
|
23
23
|
* implementation; no Aider source code copied.
|
|
24
24
|
*/
|
|
25
25
|
import { existsSync, readFileSync, renameSync, writeFileSync, unlinkSync } from 'node:fs';
|
|
@@ -64,11 +64,22 @@ export class AnvilEngineLoopClient {
|
|
|
64
64
|
// PR-CLI-SERVER-VERSION-HANDSHAKE . Stamp the outbound
|
|
65
65
|
// X-Pugi-Cli-Version header so the admin-api middleware can
|
|
66
66
|
// decide whether to honour, soft-warn, or 426 this request.
|
|
67
|
-
|
|
67
|
+
// PUGI-260: also stamp `X-Pugi-Context-Tier: 1m` when the
|
|
68
|
+
// operator opted into the long-context lane. The server reads
|
|
69
|
+
// either the body's `contextTier` field OR this header (header is
|
|
70
|
+
// a fallback for older runtimes / non-CLI clients), so emitting
|
|
71
|
+
// both is harmless и belt-and-suspenders. Only emitted for the
|
|
72
|
+
// `'1m'` value — the absence of the header is wire-equivalent к
|
|
73
|
+
// `'standard'`, keeping the default-lane path header-free.
|
|
74
|
+
const baseHeaders = {
|
|
68
75
|
'content-type': 'application/json',
|
|
69
76
|
authorization: `Bearer ${this.config.apiKey}`,
|
|
70
77
|
'user-agent': 'pugi-cli/0.0.1',
|
|
71
|
-
}
|
|
78
|
+
};
|
|
79
|
+
if (options.contextTier === '1m') {
|
|
80
|
+
baseHeaders['x-pugi-context-tier'] = '1m';
|
|
81
|
+
}
|
|
82
|
+
const outboundHeaders = injectClientVersionHeader(baseHeaders, PUGI_CLI_VERSION);
|
|
72
83
|
const res = await fetch(url, {
|
|
73
84
|
method: 'POST',
|
|
74
85
|
headers: outboundHeaders,
|
|
@@ -6,7 +6,7 @@ import { bashToolSync } from '../../tools/bash.js';
|
|
|
6
6
|
* The shapes intentionally mirror the engine-loop tool schemas in
|
|
7
7
|
* `core/engine/tool-bridge.ts` so an MCP client and the Pugi engine see
|
|
8
8
|
* the same parameter contracts. This is the "Pugi as MCP server"
|
|
9
|
-
* surface — other agents (the upstream tool, Codex,
|
|
9
|
+
* surface — other agents (the upstream tool, Codex, peer tooling) call these to
|
|
10
10
|
* read / mutate the workspace through us, with all our security gates
|
|
11
11
|
* (path containment, plan-mode refusal, bash classifier, settings) in
|
|
12
12
|
* the loop.
|
package/dist/core/mcp/server.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { EventEmitter } from 'node:events';
|
|
2
2
|
/**
|
|
3
3
|
* Pugi MCP server (β4 M2) — exposes Pugi's native tool surface to other
|
|
4
|
-
* agents (the upstream tool,
|
|
4
|
+
* agents (the upstream tool, peer tooling, Codex CLI, any client that speaks
|
|
5
5
|
* MCP).
|
|
6
6
|
*
|
|
7
7
|
* Transport-agnostic core. The stdio entry-point lives at the bottom of
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Persona-memory secret scanner (backlog,
|
|
2
|
+
* Persona-memory secret scanner (backlog, hardening).
|
|
3
3
|
*
|
|
4
4
|
* Defense against API keys / credentials accidentally landing in shared
|
|
5
5
|
* persona memory. A naive operator typing `pugi memory write fact "Use
|
|
6
6
|
* API key sk-ant-..."` would silently persist that secret to the
|
|
7
|
-
* `
|
|
7
|
+
* `the platform database.persona_memory` table — visible to every persona, every
|
|
8
8
|
* recall query, every dual-write sink. This module is the chokepoint
|
|
9
9
|
* that refuses such writes.
|
|
10
10
|
*
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
* 3. GitHub PAT / installation tokens (ghp_/ghs_/gho_/ghu_)
|
|
26
26
|
* high
|
|
27
27
|
* 4. AWS access key id AKIA… high
|
|
28
|
-
* 5. Plane API token plane_api_… high
|
|
28
|
+
* 5. Plane API token plane_api_… high [pugi-leak-ok]
|
|
29
29
|
* 6. npm token npm_… high
|
|
30
30
|
* 7. Slack token xox[bpoars]-… high
|
|
31
31
|
* 8. Stripe secret key sk_(live|test)_… high
|
|
@@ -59,10 +59,10 @@
|
|
|
59
59
|
* instead of reject. The scanner exposes `redactSecrets` for that
|
|
60
60
|
* caller — see `runtime/commands/memory.ts`.
|
|
61
61
|
*
|
|
62
|
-
* #
|
|
62
|
+
* # independent implementation provenance
|
|
63
63
|
*
|
|
64
64
|
* Inspired by the the upstream tool teamMemorySync.secretScanner pattern
|
|
65
|
-
* (intel from leak-research memos).
|
|
65
|
+
* (intel from leak-research memos). independent implementation TypeScript
|
|
66
66
|
* implementation — no upstream code reused. Pattern vocabulary was
|
|
67
67
|
* cross-referenced against the existing
|
|
68
68
|
* `apps/pugi-cli/scripts/secret-scanner.mjs` tarball gate so a single
|
|
@@ -124,7 +124,7 @@ const SECRET_RULES = [
|
|
|
124
124
|
pattern: 'plane-api-token',
|
|
125
125
|
// Plane (project management) personal API tokens. Bounded to 20+
|
|
126
126
|
// url-safe chars after the prefix.
|
|
127
|
-
regex: /\bplane_api_[A-Za-z0-9]{20,}(?![A-Za-z0-9])/g,
|
|
127
|
+
regex: /\bplane_api_[A-Za-z0-9]{20,}(?![A-Za-z0-9])/g, // [pugi-leak-ok]
|
|
128
128
|
confidence: 'high',
|
|
129
129
|
},
|
|
130
130
|
{
|
|
@@ -119,7 +119,7 @@ export async function ensureInitialized(opts) {
|
|
|
119
119
|
write(`No Pugi workspace found at ${root}.\n`);
|
|
120
120
|
const answer = (await opts.prompt('Initialize a new Pugi workspace here? (Y/n) ')).trim().toLowerCase();
|
|
121
121
|
// Default = yes (empty input OR leading 'y'). Anything else = no.
|
|
122
|
-
// Mirrors the gh CLI /
|
|
122
|
+
// Mirrors the gh CLI / the upstream prompt convention where the upper-
|
|
123
123
|
// case option in `(Y/n)` is the default-on-Enter answer.
|
|
124
124
|
const acceptedShort = answer === '' || answer === 'y' || answer === 'yes';
|
|
125
125
|
if (!acceptedShort) {
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Plan-as-FILE artifact store (Pugi backlog).
|
|
3
3
|
*
|
|
4
4
|
* Pattern absorbed from the the upstream tool `ExitPlanMode` leak intel
|
|
5
|
-
* (
|
|
5
|
+
* (independent implementation TypeScript reimplementation — no source was copied; only
|
|
6
6
|
* the file-as-artifact concept). When Pugi enters plan-mode the engine
|
|
7
7
|
* routes the plan body to `.pugi/plans/<plan-id>.md` instead of the
|
|
8
8
|
* message stream so it survives `/compact`, becomes diffable across
|
|
@@ -205,7 +205,7 @@ function yamlScalar(value) {
|
|
|
205
205
|
// Empty string is also disambiguated by quoting.
|
|
206
206
|
if (value.length === 0)
|
|
207
207
|
return '""';
|
|
208
|
-
if (/[
|
|
208
|
+
if (/[-"]/.test(value)) {
|
|
209
209
|
return JSON.stringify(value);
|
|
210
210
|
}
|
|
211
211
|
if (/[:#\n\t]/.test(value)) {
|
package/dist/core/settings.js
CHANGED
|
@@ -165,6 +165,18 @@ const pugiSettingsSchema = z.object({
|
|
|
165
165
|
// keeps Zod's strip-pass from swallowing it before the chain reader
|
|
166
166
|
// sees it. See `hook-chains.ts` for the full schema.
|
|
167
167
|
hooks: z.any().optional(),
|
|
168
|
+
// PUGI-260 — persistent default for the 1M context tier opt-in.
|
|
169
|
+
// `pugi config set context.tier 1m` writes this; per-invocation
|
|
170
|
+
// `--context-tier=...` flags override it. When omitted, the CLI
|
|
171
|
+
// sends no `contextTier` field на the wire (server treats as
|
|
172
|
+
// `standard` routing). The closed enum mirrors the CLI flag и the
|
|
173
|
+
// admin-api DTO; an unrecognised value triggers a Zod parse error
|
|
174
|
+
// at load time rather than a silent fallback.
|
|
175
|
+
context: z
|
|
176
|
+
.object({
|
|
177
|
+
tier: z.enum(['1m', 'standard']).optional(),
|
|
178
|
+
})
|
|
179
|
+
.optional(),
|
|
168
180
|
});
|
|
169
181
|
/**
|
|
170
182
|
* #20 — the upstream tool drop-in compat ingest.
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { runCli } from './runtime/cli.js';
|
|
3
3
|
import { PugiCliUpgradeRequiredError } from './core/transport/version-interceptor.js';
|
|
4
|
+
import { installSigintGuard } from './runtime/sigint-guard.js';
|
|
5
|
+
// PUGI-469 — install the top-level double-press Ctrl+C exit guard
|
|
6
|
+
// BEFORE any other init. Operators reported single ^C exiting the
|
|
7
|
+
// CLI; the guard requires a second press inside a 2s window (or
|
|
8
|
+
// emits a clean session-end envelope on the headless path) so a
|
|
9
|
+
// stray keystroke never kills the session. See the module header
|
|
10
|
+
// in `runtime/sigint-guard.ts` for the full behavior spec.
|
|
11
|
+
installSigintGuard();
|
|
4
12
|
runCli(process.argv.slice(2)).catch((error) => {
|
|
5
13
|
// PR-CLI-SERVER-VERSION-HANDSHAKE . When the admin-api returns
|
|
6
14
|
// 426 Upgrade Required, the engine transport throws a typed
|
package/dist/runtime/cli.js
CHANGED
|
@@ -49,7 +49,7 @@ import { runUndoCommand } from './commands/undo.js';
|
|
|
49
49
|
import { runCompactCommand } from './commands/compact.js';
|
|
50
50
|
import { runRewindCommand } from './commands/rewind.js';
|
|
51
51
|
import { runSessionsCommand } from './commands/sessions.js';
|
|
52
|
-
// Day 4
|
|
52
|
+
// Day 4 : persona-memory operator surface (list / recall / write /
|
|
53
53
|
// forget / sync). The runner is shared by `pugi memory` top-level and the
|
|
54
54
|
// in-REPL `/memory` slash so the two surfaces stay single-sourced.
|
|
55
55
|
import { runMemoryCommand } from './commands/memory.js';
|
|
@@ -64,7 +64,7 @@ import { runRecipeCommand } from './commands/recipe.js';
|
|
|
64
64
|
import { installDefaultSkills } from '../core/skills/defaults.js';
|
|
65
65
|
// Backlog : bundled-skills batch 1 (stuck / simplify /
|
|
66
66
|
// remember). Backlog : batch 2 (batch / verify / loop /
|
|
67
|
-
// skillify,
|
|
67
|
+
// skillify, external independent implementation).
|
|
68
68
|
// Imported through the dedicated registry so future batches only append
|
|
69
69
|
// to one barrel.
|
|
70
70
|
import { runRememberCommand, runSimplifyCommand, runStuckCommand, runBatchCommand, runVerifyCommand, runLoopCommand, runSkillifyCommand, } from '../skills/bundled/index.js';
|
|
@@ -151,7 +151,7 @@ const handlers = {
|
|
|
151
151
|
logout,
|
|
152
152
|
lsp: dispatchLsp,
|
|
153
153
|
mcp: dispatchMcp,
|
|
154
|
-
//
|
|
154
|
+
// Day 4: `pugi memory list|recall|write|forget|sync`. Routes
|
|
155
155
|
// to `runMemoryCommand` (admin-api `/api/persona-memory` + offline
|
|
156
156
|
// queue at `~/.pugi/memory-queue.jsonl`).
|
|
157
157
|
memory: dispatchMemory,
|
|
@@ -233,7 +233,7 @@ const handlers = {
|
|
|
233
233
|
// when no approval gate is wired. The existing `pugi memory write`
|
|
234
234
|
// surface keeps its silent-enqueue behaviour for back-compat.
|
|
235
235
|
remember: dispatchRemember,
|
|
236
|
-
// Backlog — bundled-skills batch 2 (
|
|
236
|
+
// Backlog — bundled-skills batch 2 (external independent implementation).
|
|
237
237
|
// `pugi batch` fans out a YAML recipe of independent engine tasks
|
|
238
238
|
// through up to --concurrency=N subprocesses (hard cap 30 per the
|
|
239
239
|
// Mac safety carve-out). Aggregates results into
|
|
@@ -447,7 +447,7 @@ async function dispatchPrivacy(args, flags, _session) {
|
|
|
447
447
|
});
|
|
448
448
|
}
|
|
449
449
|
/**
|
|
450
|
-
*
|
|
450
|
+
* Day 4 — `pugi memory <sub>` top-level dispatcher.
|
|
451
451
|
*
|
|
452
452
|
* Forwards to the shared `runMemoryCommand` runner. Exit codes:
|
|
453
453
|
*
|
|
@@ -1597,7 +1597,7 @@ export async function runCli(argv) {
|
|
|
1597
1597
|
// stderr line above the alt-screen flicker would race against the
|
|
1598
1598
|
// banner paint on slow terminals.
|
|
1599
1599
|
// Bare `pugi` on a TTY enters the REPL-by-default agentic session
|
|
1600
|
-
// (Sprint ,
|
|
1600
|
+
// (Sprint , ). The REPL is the customer-facing surface
|
|
1601
1601
|
// that brings Pugi to parity with the upstream tool / Codex CLI. When the
|
|
1602
1602
|
// operator has no credentials yet, we fall back to the splash
|
|
1603
1603
|
// so the install-time `pugi` surface still shows the wordmark +
|
|
@@ -1824,7 +1824,7 @@ function parseArgs(argv) {
|
|
|
1824
1824
|
}
|
|
1825
1825
|
else if (arg === '--council') {
|
|
1826
1826
|
// Backlog — opt-in council mode (Karpathy llm-council
|
|
1827
|
-
// pattern, MIT
|
|
1827
|
+
// pattern, MIT independent implementation TS port). Pairs with `--triple
|
|
1828
1828
|
// --commit <SHA>`; the dispatch wraps the multi-provider
|
|
1829
1829
|
// fan-out with an anonymous peer-review stage + chairman
|
|
1830
1830
|
// synthesis. Costs ~2× tokens; explicit opt-in only.
|
|
@@ -4101,7 +4101,7 @@ async function performTripleProviderReview(root, session, flags, prompt) {
|
|
|
4101
4101
|
// Server-side the controller also accepts `?council=true` query
|
|
4102
4102
|
// string + `mode: 'council'` body shorthand; the CLI sends the
|
|
4103
4103
|
// explicit boolean for forward compatibility. Inspired by
|
|
4104
|
-
// Karpathy llm-council pattern (MIT).
|
|
4104
|
+
// Karpathy llm-council pattern (MIT). independent implementation TS port.
|
|
4105
4105
|
...(flags.council ? { council: true } : {}),
|
|
4106
4106
|
});
|
|
4107
4107
|
writeFileSync(requestPath, `${JSON.stringify(requestBody, null, 2)}\n`, {
|
|
@@ -4961,6 +4961,38 @@ export function setHeadlessWriters(writers) {
|
|
|
4961
4961
|
headlessStdoutWriter = writers.stdout ?? null;
|
|
4962
4962
|
headlessStderrWriter = writers.stderr ?? null;
|
|
4963
4963
|
}
|
|
4964
|
+
/**
|
|
4965
|
+
* PUGI-260 — read the persisted `contextTier` default из
|
|
4966
|
+
* `~/.pugi/config.json` (written by `pugi config set context.tier 1m`).
|
|
4967
|
+
* Returns `undefined` when the config file is missing, the key is
|
|
4968
|
+
* unset, or the value is invalid. Failures are silent — a malformed
|
|
4969
|
+
* persisted config must NEVER break the per-invocation dispatch path;
|
|
4970
|
+
* the flag default just falls back к "no preference" and the request
|
|
4971
|
+
* goes out on the standard lane.
|
|
4972
|
+
*
|
|
4973
|
+
* Kept inline (rather than importing from `commands/config.ts`) so the
|
|
4974
|
+
* dispatch path does not pull в the full `pugi config` command tree
|
|
4975
|
+
* during a `pugi code "..."` cold start — the import graph stays narrow.
|
|
4976
|
+
*/
|
|
4977
|
+
function readPersistedContextTier() {
|
|
4978
|
+
try {
|
|
4979
|
+
const home = process.env.PUGI_HOME ?? resolve(homedir(), '.pugi');
|
|
4980
|
+
const path = resolve(home, 'config.json');
|
|
4981
|
+
if (!existsSync(path))
|
|
4982
|
+
return undefined;
|
|
4983
|
+
const raw = readFileSync(path, 'utf8');
|
|
4984
|
+
if (raw.trim() === '')
|
|
4985
|
+
return undefined;
|
|
4986
|
+
const parsed = JSON.parse(raw);
|
|
4987
|
+
if (parsed.contextTier === '1m' || parsed.contextTier === 'standard') {
|
|
4988
|
+
return parsed.contextTier;
|
|
4989
|
+
}
|
|
4990
|
+
return undefined;
|
|
4991
|
+
}
|
|
4992
|
+
catch {
|
|
4993
|
+
return undefined;
|
|
4994
|
+
}
|
|
4995
|
+
}
|
|
4964
4996
|
function runEngineTask(kind) {
|
|
4965
4997
|
return async (args, flags, session) => {
|
|
4966
4998
|
const label = commandLabel(kind);
|
|
@@ -5250,18 +5282,29 @@ function runEngineTask(kind) {
|
|
|
5250
5282
|
// point); `allowParallelAgents=false` strips the `agent` tool from
|
|
5251
5283
|
// the schema so quick / standard tiers cannot accidentally fan out.
|
|
5252
5284
|
intensityProfile,
|
|
5253
|
-
//
|
|
5285
|
+
// PUGI-260 — 1M context tier opt-in. The operator passes
|
|
5254
5286
|
// `--context-tier=1m` to request the long-context lane. Server
|
|
5255
|
-
// enforces Team
|
|
5256
|
-
// the `pugi-1m` alias. Lower tiers receive HTTP 402
|
|
5257
|
-
//
|
|
5258
|
-
// `
|
|
5259
|
-
//
|
|
5260
|
-
//
|
|
5261
|
-
//
|
|
5262
|
-
|
|
5263
|
-
|
|
5264
|
-
|
|
5287
|
+
// enforces Builder ($99) или Team ($199) entitlement и rewrites
|
|
5288
|
+
// the model к the `pugi-1m` alias. Lower tiers receive HTTP 402
|
|
5289
|
+
// with `reason: 'context_tier_requires_upgrade'` + the
|
|
5290
|
+
// `X-Pugi-Quota-Exceeded: context_tier_requires_upgrade` response
|
|
5291
|
+
// header rendered by the dispatch error handler.
|
|
5292
|
+
//
|
|
5293
|
+
// Resolution order:
|
|
5294
|
+
// 1. `--context-tier=1m|standard` flag (per-invocation override).
|
|
5295
|
+
// 2. `~/.pugi/config.json::contextTier` (persistent default,
|
|
5296
|
+
// set via `pugi config set context.tier 1m`).
|
|
5297
|
+
// 3. Omitted on the wire — admin-api treats omitted == standard.
|
|
5298
|
+
//
|
|
5299
|
+
// `'standard'` is wire-equivalent to omitted (the gate short-
|
|
5300
|
+
// circuits when `request.contextTier !== '1m'`), so the wire stays
|
|
5301
|
+
// clean for callers that omit the flag entirely.
|
|
5302
|
+
...(() => {
|
|
5303
|
+
const effectiveTier = flags.contextTier ?? readPersistedContextTier();
|
|
5304
|
+
return effectiveTier !== undefined
|
|
5305
|
+
? { contextTier: effectiveTier }
|
|
5306
|
+
: {};
|
|
5307
|
+
})(),
|
|
5265
5308
|
});
|
|
5266
5309
|
const toolCallId = recordToolCall(session, `engine:${adapter.name}`, `${label}: ${prompt}`);
|
|
5267
5310
|
const taskId = `${kind}-${Date.now()}`;
|
|
@@ -7810,7 +7853,12 @@ export const __test__ = {
|
|
|
7810
7853
|
// Backlog — `parseArgs` exposed under the test namespace so the
|
|
7811
7854
|
// council-flag spec can assert flag-to-CliFlags mapping without
|
|
7812
7855
|
// standing up a full execFileSync harness. Inspired by Karpathy
|
|
7813
|
-
// llm-council pattern (MIT).
|
|
7856
|
+
// llm-council pattern (MIT). independent implementation TS port.
|
|
7814
7857
|
parseArgs,
|
|
7858
|
+
// PUGI-260 — exposed под the test namespace so the persisted-
|
|
7859
|
+
// contextTier-fallback spec can exercise the actual reader (с its
|
|
7860
|
+
// env-driven $PUGI_HOME redirect) without going через the full
|
|
7861
|
+
// engine-task dispatch path.
|
|
7862
|
+
readPersistedContextTier,
|
|
7815
7863
|
};
|
|
7816
7864
|
//# sourceMappingURL=cli.js.map
|
|
@@ -39,9 +39,41 @@ const configSchema = z
|
|
|
39
39
|
privacy: z.enum(['local-only', 'metadata', 'full']).optional(),
|
|
40
40
|
model: z.string().nullable().optional(),
|
|
41
41
|
preferredEndpoint: z.string().url().optional(),
|
|
42
|
+
// PUGI-260 — persistent default for the 1M context tier opt-in.
|
|
43
|
+
// `pugi config set contextTier 1m` (or the dotted form
|
|
44
|
+
// `context.tier 1m`) writes this; per-invocation `--context-tier=...`
|
|
45
|
+
// flags override it at request time. Closed enum mirrors the CLI
|
|
46
|
+
// flag и the admin-api DTO so a typo here surfaces as a Zod parse
|
|
47
|
+
// error при load, not a silent fallback. Stored on the flat user-
|
|
48
|
+
// level config (~/.pugi/config.json) so all workspaces inherit the
|
|
49
|
+
// same default — operators с consistent long-context workloads
|
|
50
|
+
// (large monorepos, audits) set it once instead of remembering к
|
|
51
|
+
// pass --context-tier=1m on every dispatch.
|
|
52
|
+
contextTier: z.enum(['1m', 'standard']).optional(),
|
|
42
53
|
})
|
|
43
54
|
.strict();
|
|
44
|
-
const CONFIG_KEYS = [
|
|
55
|
+
const CONFIG_KEYS = [
|
|
56
|
+
'permissionMode',
|
|
57
|
+
'privacy',
|
|
58
|
+
'model',
|
|
59
|
+
'preferredEndpoint',
|
|
60
|
+
// PUGI-260 — exposed на `pugi config list` so operators see the
|
|
61
|
+
// current default. Hidden synonym `context.tier` accepted by
|
|
62
|
+
// runConfigSet / runConfigGet for a dotted-key familiar UX.
|
|
63
|
+
'contextTier',
|
|
64
|
+
];
|
|
65
|
+
/**
|
|
66
|
+
* PUGI-260: legacy / nested key aliasing. `pugi config set context.tier 1m`
|
|
67
|
+
* is the documented form в the feat doc; we normalise it onto the flat
|
|
68
|
+
* `contextTier` key before the strict-schema validation так future
|
|
69
|
+
* settings.json migrations keep one canonical key. Mirrors the
|
|
70
|
+
* legacy privacy-mode aliasing that already lives in the file.
|
|
71
|
+
*/
|
|
72
|
+
function normaliseConfigKey(raw) {
|
|
73
|
+
if (raw === 'context.tier')
|
|
74
|
+
return 'contextTier';
|
|
75
|
+
return raw;
|
|
76
|
+
}
|
|
45
77
|
export async function runConfigCommand(args, ctx) {
|
|
46
78
|
const sub = args[0];
|
|
47
79
|
if (!sub || sub === '--help' || sub === '-h') {
|
|
@@ -178,25 +210,27 @@ function isConfigKey(value) {
|
|
|
178
210
|
return CONFIG_KEYS.includes(value);
|
|
179
211
|
}
|
|
180
212
|
function runConfigGet(args, ctx) {
|
|
181
|
-
const
|
|
182
|
-
if (!
|
|
213
|
+
const rawKey = args[0];
|
|
214
|
+
if (!rawKey)
|
|
183
215
|
throw new Error('pugi config get requires a key.');
|
|
216
|
+
const key = normaliseConfigKey(rawKey);
|
|
184
217
|
if (!isConfigKey(key)) {
|
|
185
|
-
throw new Error(`Unknown config key "${
|
|
218
|
+
throw new Error(`Unknown config key "${rawKey}". Allowed: ${CONFIG_KEYS.join(', ')}.`);
|
|
186
219
|
}
|
|
187
220
|
const config = readConfig();
|
|
188
221
|
const value = config[key] ?? null;
|
|
189
222
|
ctx.writeOutput({ command: 'config.get', key, value }, value === null || value === undefined ? `${key} = (unset)` : `${key} = ${String(value)}`);
|
|
190
223
|
}
|
|
191
224
|
function runConfigSet(args, ctx) {
|
|
192
|
-
const
|
|
225
|
+
const rawKey = args[0];
|
|
193
226
|
const value = args.slice(1).join(' ');
|
|
194
|
-
if (!
|
|
227
|
+
if (!rawKey)
|
|
195
228
|
throw new Error('pugi config set requires a key.');
|
|
196
229
|
if (value.length === 0)
|
|
197
230
|
throw new Error('pugi config set requires a value.');
|
|
231
|
+
const key = normaliseConfigKey(rawKey);
|
|
198
232
|
if (!isConfigKey(key)) {
|
|
199
|
-
throw new Error(`Unknown config key "${
|
|
233
|
+
throw new Error(`Unknown config key "${rawKey}". Allowed: ${CONFIG_KEYS.join(', ')}.`);
|
|
200
234
|
}
|
|
201
235
|
const current = readConfig();
|
|
202
236
|
// Build the candidate and validate via the schema so an invalid value
|