pi-ui-extend 1.0.41 → 1.0.45
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/README.md +50 -13
- package/dist/app/app.d.ts +13 -0
- package/dist/app/app.js +158 -8
- package/dist/app/cli/install.js +2 -2
- package/dist/app/commands/command-controller.d.ts +4 -0
- package/dist/app/commands/command-controller.js +11 -0
- package/dist/app/commands/command-git-actions.d.ts +25 -0
- package/dist/app/commands/command-git-actions.js +381 -0
- package/dist/app/commands/command-host.d.ts +4 -0
- package/dist/app/commands/command-host.js +5 -2
- package/dist/app/commands/command-model-actions.d.ts +1 -0
- package/dist/app/commands/command-model-actions.js +57 -35
- package/dist/app/commands/command-navigation-actions.d.ts +1 -0
- package/dist/app/commands/command-navigation-actions.js +16 -3
- package/dist/app/commands/command-registry.d.ts +2 -0
- package/dist/app/commands/command-registry.js +15 -1
- package/dist/app/commands/command-session-actions.js +9 -3
- package/dist/app/commands/reload-context-inventory.d.ts +11 -0
- package/dist/app/commands/reload-context-inventory.js +70 -0
- package/dist/app/extensions/extension-actions-controller.d.ts +1 -0
- package/dist/app/extensions/extension-actions-controller.js +6 -0
- package/dist/app/extensions/subagent-catalog-state.d.ts +9 -0
- package/dist/app/extensions/subagent-catalog-state.js +23 -0
- package/dist/app/input/autocomplete-controller.js +61 -34
- package/dist/app/input/input-action-controller.d.ts +2 -0
- package/dist/app/input/input-action-controller.js +8 -1
- package/dist/app/input/input-controller.d.ts +2 -1
- package/dist/app/input/input-controller.js +7 -2
- package/dist/app/input/prompt-enhancer-controller.js +53 -35
- package/dist/app/input/voice-controller.d.ts +41 -45
- package/dist/app/input/voice-controller.js +351 -419
- package/dist/app/model/model-usage-controller.js +17 -7
- package/dist/app/model/model-usage-status.d.ts +4 -1
- package/dist/app/model/model-usage-status.js +270 -46
- package/dist/app/popup/menu-items-controller.d.ts +13 -3
- package/dist/app/popup/menu-items-controller.js +37 -21
- package/dist/app/popup/popup-action-controller.d.ts +12 -2
- package/dist/app/popup/popup-action-controller.js +77 -24
- package/dist/app/popup/popup-menu-controller.d.ts +36 -14
- package/dist/app/popup/popup-menu-controller.js +239 -69
- package/dist/app/rendering/dcp-stats.d.ts +9 -4
- package/dist/app/rendering/dcp-stats.js +40 -425
- package/dist/app/rendering/editor-panels.js +10 -4
- package/dist/app/rendering/popup-menu-renderer.d.ts +3 -5
- package/dist/app/rendering/popup-menu-renderer.js +42 -37
- package/dist/app/rendering/render-controller.js +23 -2
- package/dist/app/rendering/status-line-renderer.d.ts +5 -0
- package/dist/app/rendering/status-line-renderer.js +41 -5
- package/dist/app/rendering/tab-line-renderer.js +26 -20
- package/dist/app/runtime.d.ts +12 -1
- package/dist/app/runtime.js +120 -12
- package/dist/app/screen/mouse-controller.d.ts +2 -0
- package/dist/app/screen/mouse-controller.js +17 -7
- package/dist/app/screen/status-controller.d.ts +4 -0
- package/dist/app/screen/status-controller.js +5 -0
- package/dist/app/session/lazy-session-manager.js +34 -0
- package/dist/app/session/session-event-controller.d.ts +1 -0
- package/dist/app/session/session-event-controller.js +10 -1
- package/dist/app/session/session-history.d.ts +1 -0
- package/dist/app/session/session-history.js +12 -1
- package/dist/app/session/session-lifecycle-controller.d.ts +7 -1
- package/dist/app/session/session-lifecycle-controller.js +16 -1
- package/dist/app/session/tabs-controller.d.ts +26 -1
- package/dist/app/session/tabs-controller.js +454 -147
- package/dist/app/subagents/subagents-files.js +60 -1
- package/dist/app/subagents/subagents-model.d.ts +1 -0
- package/dist/app/subagents/subagents-model.js +18 -2
- package/dist/app/subagents/subagents-widget-controller.d.ts +1 -0
- package/dist/app/subagents/subagents-widget-controller.js +6 -0
- package/dist/app/types.d.ts +16 -1
- package/dist/app/workspace/workspace-actions-controller.js +10 -2
- package/dist/app/workspace/workspace-undo.d.ts +1 -0
- package/dist/app/workspace/workspace-undo.js +1 -0
- package/dist/bundled-extensions/question/index.js +9 -1
- package/dist/bundled-extensions/question/remote.d.ts +4 -0
- package/dist/bundled-extensions/question/remote.js +33 -0
- package/dist/bundled-extensions/telegram-connector/bot.d.ts +43 -0
- package/dist/bundled-extensions/telegram-connector/bot.js +166 -0
- package/dist/bundled-extensions/telegram-connector/config.d.ts +8 -0
- package/dist/bundled-extensions/telegram-connector/config.js +87 -0
- package/dist/bundled-extensions/telegram-connector/coordinator.d.ts +66 -0
- package/dist/bundled-extensions/telegram-connector/coordinator.js +413 -0
- package/dist/bundled-extensions/telegram-connector/index.d.ts +3 -0
- package/dist/bundled-extensions/telegram-connector/index.js +195 -0
- package/dist/bundled-extensions/terminal-bell/index.d.ts +0 -8
- package/dist/bundled-extensions/terminal-bell/index.js +0 -76
- package/dist/bundled-extensions/workspace-undo/index.d.ts +26 -0
- package/dist/bundled-extensions/workspace-undo/index.js +191 -0
- package/dist/config.d.ts +21 -2
- package/dist/config.js +159 -32
- package/dist/default-pix-config.js +25 -5
- package/dist/schemas/index.d.ts +1 -0
- package/dist/schemas/index.js +1 -0
- package/dist/schemas/pi-tools-suite-schema.d.ts +88 -62
- package/dist/schemas/pi-tools-suite-schema.js +54 -83
- package/dist/schemas/pix-schema.d.ts +19 -2
- package/dist/schemas/pix-schema.js +47 -5
- package/dist/schemas/tasks-schema.d.ts +18 -0
- package/dist/schemas/tasks-schema.js +41 -0
- package/docs/concurrency.md +9 -1
- package/docs/desktop-mvp.md +22 -6
- package/docs/desktop-task-manager.md +148 -84
- package/docs/release.md +30 -6
- package/external/pi-tools-suite/README.md +197 -84
- package/external/pi-tools-suite/docs/evals.md +1 -1
- package/external/pi-tools-suite/docs/session-recovery.md +47 -14
- package/external/pi-tools-suite/docs/subagent-model-pools.md +48 -30
- package/external/pi-tools-suite/docs/ui-qa-subagent.md +440 -0
- package/external/pi-tools-suite/package.json +6 -1
- package/external/pi-tools-suite/src/antigravity-auth/auth-store.ts +2 -1
- package/external/pi-tools-suite/src/antigravity-auth/constants.ts +9 -3
- package/external/pi-tools-suite/src/antigravity-auth/headers.ts +41 -3
- package/external/pi-tools-suite/src/antigravity-auth/models.ts +94 -21
- package/external/pi-tools-suite/src/antigravity-auth/oauth.ts +5 -17
- package/external/pi-tools-suite/src/antigravity-auth/payload.ts +72 -7
- package/external/pi-tools-suite/src/antigravity-auth/stream.ts +13 -1
- package/external/pi-tools-suite/src/async-subagents/agents/frontier-review.md +23 -0
- package/external/pi-tools-suite/src/async-subagents/agents/implement.md +1 -1
- package/external/pi-tools-suite/src/async-subagents/agents/presets.jsonc +16 -0
- package/external/pi-tools-suite/src/async-subagents/agents/research.md +5 -3
- package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/backends/browser.mjs +346 -0
- package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/backends/desktop.mjs +1038 -0
- package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/backends/tui.mjs +759 -0
- package/external/pi-tools-suite/src/async-subagents/agents/{browser-qa → ui-qa/browser}/scripts/browser-qa-runner.mjs +32 -23
- package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/drivers/chrome-devtools/chrome-devtools-provider.mjs +895 -0
- package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/drivers/linux/linux-atspi.py +494 -0
- package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/drivers/macos/macos-accessibility.swift +1085 -0
- package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/drivers/native-terminal/bridge-client.mjs +50 -0
- package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/drivers/native-terminal/native-terminal-host.mjs +501 -0
- package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/drivers/windows/windows-uia.ps1 +454 -0
- package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/guides/browser-auth.md +114 -0
- package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/guides/browser-chrome-devtools.md +91 -0
- package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/guides/browser-playwright.md +145 -0
- package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/guides/browser.md +82 -0
- package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/guides/desktop-linux.md +26 -0
- package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/guides/desktop-macos.md +29 -0
- package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/guides/desktop-windows.md +23 -0
- package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/guides/desktop.md +54 -0
- package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/guides/tui-native-terminal.md +59 -0
- package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/guides/tui-pty.md +37 -0
- package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/guides/tui.md +60 -0
- package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/scripts/ui-qa-runner.mjs +535 -0
- package/external/pi-tools-suite/src/async-subagents/agents/ui-qa.md +75 -0
- package/external/pi-tools-suite/src/async-subagents/commands.ts +15 -71
- package/external/pi-tools-suite/src/async-subagents/core/activity.ts +33 -0
- package/external/pi-tools-suite/src/async-subagents/core/agent-catalog.ts +5 -4
- package/external/pi-tools-suite/src/async-subagents/core/agent-strategy.ts +1 -1
- package/external/pi-tools-suite/src/async-subagents/core/agents-dir.ts +8 -5
- package/external/pi-tools-suite/src/async-subagents/core/browser-qa.ts +16 -2
- package/external/pi-tools-suite/src/async-subagents/core/config.ts +160 -327
- package/external/pi-tools-suite/src/async-subagents/core/model-selection.ts +2 -1
- package/external/pi-tools-suite/src/async-subagents/core/registry.ts +158 -33
- package/external/pi-tools-suite/src/async-subagents/core/routing.ts +21 -5
- package/external/pi-tools-suite/src/async-subagents/core/spawn.ts +40 -26
- package/external/pi-tools-suite/src/async-subagents/core/state.ts +40 -0
- package/external/pi-tools-suite/src/async-subagents/core/types.ts +7 -0
- package/external/pi-tools-suite/src/async-subagents/core/ultrawork-auto.ts +51 -45
- package/external/pi-tools-suite/src/async-subagents/index.ts +69 -5
- package/external/pi-tools-suite/src/async-subagents/lib.ts +16 -9
- package/external/pi-tools-suite/src/async-subagents/tools/spawn.ts +8 -5
- package/external/pi-tools-suite/src/async-subagents/tools/subagents.ts +5 -4
- package/external/pi-tools-suite/src/async-subagents/types.ts +1 -0
- package/external/pi-tools-suite/src/coding-discipline/index.ts +90 -59
- package/external/pi-tools-suite/src/config.ts +55 -1
- package/external/pi-tools-suite/src/context-gateway/accounting-log.ts +282 -0
- package/external/pi-tools-suite/src/context-gateway/config.ts +77 -3
- package/external/pi-tools-suite/src/context-gateway/efficiency.ts +414 -0
- package/external/pi-tools-suite/src/context-gateway/enforcement.ts +222 -0
- package/external/pi-tools-suite/src/context-gateway/index.ts +206 -19
- package/external/pi-tools-suite/src/context-gateway/storeless-capabilities.ts +7 -7
- package/external/pi-tools-suite/src/context-gateway/telemetry.ts +101 -28
- package/external/pi-tools-suite/src/context-gateway/types.ts +16 -3
- package/external/pi-tools-suite/src/context-inventory.ts +99 -0
- package/external/pi-tools-suite/src/dcp/auto-compress-budget.ts +39 -4
- package/external/pi-tools-suite/src/dcp/auto-compress.ts +128 -49
- package/external/pi-tools-suite/src/dcp/commands.ts +32 -156
- package/external/pi-tools-suite/src/dcp/compress-tool.ts +177 -46
- package/external/pi-tools-suite/src/dcp/compression-blocks.ts +6 -58
- package/external/pi-tools-suite/src/dcp/compression-preview.ts +9 -0
- package/external/pi-tools-suite/src/dcp/compression-progress.ts +22 -0
- package/external/pi-tools-suite/src/dcp/config.ts +114 -17
- package/external/pi-tools-suite/src/dcp/conversation-index.ts +36 -9
- package/external/pi-tools-suite/src/dcp/diagnostics.ts +41 -0
- package/external/pi-tools-suite/src/dcp/fresh-tool-results.ts +38 -0
- package/external/pi-tools-suite/src/dcp/index.ts +338 -66
- package/external/pi-tools-suite/src/dcp/journal.ts +126 -4
- package/external/pi-tools-suite/src/dcp/progress-controller.ts +2 -1
- package/external/pi-tools-suite/src/dcp/prompts.ts +83 -192
- package/external/pi-tools-suite/src/dcp/protected-continuity.ts +175 -0
- package/external/pi-tools-suite/src/dcp/pruner-candidates.ts +93 -3
- package/external/pi-tools-suite/src/dcp/pruner-message-ids.ts +6 -10
- package/external/pi-tools-suite/src/dcp/pruner-nudge.ts +36 -33
- package/external/pi-tools-suite/src/dcp/pruner-tools.ts +6 -3
- package/external/pi-tools-suite/src/dcp/pruner.ts +1 -0
- package/external/pi-tools-suite/src/dcp/routine-pressure.ts +86 -0
- package/external/pi-tools-suite/src/dcp/state.ts +9 -0
- package/external/pi-tools-suite/src/dcp/statistics.d.ts +8 -0
- package/external/pi-tools-suite/src/dcp/statistics.js +156 -0
- package/external/pi-tools-suite/src/default-pi-tools-suite-config.ts +78 -22
- package/external/pi-tools-suite/src/index.ts +7 -1
- package/external/pi-tools-suite/src/lib/project.ts +36 -1
- package/external/pi-tools-suite/src/model-tools/index.ts +10 -7
- package/external/pi-tools-suite/src/repo-discovery/index.ts +304 -4
- package/external/pi-tools-suite/src/resource-registry/index.ts +2551 -0
- package/external/pi-tools-suite/src/session-recovery/index.ts +17 -0
- package/external/pi-tools-suite/src/shell-command-policy.ts +219 -0
- package/external/pi-tools-suite/src/todo/index.ts +21 -0
- package/external/pi-tools-suite/src/todo/todo.ts +19 -1
- package/external/pi-tools-suite/src/tool-descriptions.ts +47 -19
- package/package.json +10 -9
- package/schemas/pi-tools-suite.json +466 -287
- package/schemas/pix.json +129 -11
- package/schemas/tasks.json +131 -0
- package/skills/simplify/SKILL.md +33 -5
- package/docs/desktop-markdown-media.md +0 -77
- package/external/pi-tools-suite/docs/browser-qa-subagent.md +0 -177
- package/external/pi-tools-suite/docs/dcp-emergency-current-turn.md +0 -102
- package/external/pi-tools-suite/src/async-subagents/agents/browser-qa.md +0 -598
- package/external/pi-tools-suite/src/async-subagents/async-subagents.sample.jsonc +0 -54
- package/external/pi-tools-suite/src/skill-installer/index.ts +0 -333
- package/skills/playwright-cli/SKILL.md +0 -420
- package/skills/playwright-cli/references/element-attributes.md +0 -23
- package/skills/playwright-cli/references/playwright-tests.md +0 -50
- package/skills/playwright-cli/references/request-mocking.md +0 -87
- package/skills/playwright-cli/references/running-code.md +0 -241
- package/skills/playwright-cli/references/session-management.md +0 -273
- package/skills/playwright-cli/references/spec-driven-testing.md +0 -311
- package/skills/playwright-cli/references/storage-state.md +0 -290
- package/skills/playwright-cli/references/test-generation.md +0 -142
- package/skills/playwright-cli/references/tracing.md +0 -154
- package/skills/playwright-cli/references/video-recording.md +0 -147
- package/skills/spec-lite/SKILL.md +0 -140
- /package/external/pi-tools-suite/src/async-subagents/agents/{browser-qa → ui-qa/browser}/examples/qa-auth.example.jsonc +0 -0
- /package/external/pi-tools-suite/src/async-subagents/agents/{browser-qa → ui-qa/browser}/examples/qa-flow.example.jsonc +0 -0
- /package/external/pi-tools-suite/src/async-subagents/agents/{browser-qa → ui-qa/browser}/vendor/fflate.LICENSE +0 -0
- /package/external/pi-tools-suite/src/async-subagents/agents/{browser-qa → ui-qa/browser}/vendor/fflate.mjs +0 -0
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
import * as fs from "node:fs/promises";
|
|
2
|
+
import * as os from "node:os";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
|
|
5
|
+
import type { ContextGatewayResolvedConfig } from "./types.js";
|
|
6
|
+
|
|
7
|
+
const TRUE_ENV_RE = /^(1|true|yes|on)$/i;
|
|
8
|
+
const FALSE_ENV_RE = /^(0|false|no|off)$/i;
|
|
9
|
+
const DEFAULT_MAX_BYTES = 5 * 1024 * 1024;
|
|
10
|
+
const DEFAULT_MAX_BACKUPS = 3;
|
|
11
|
+
const MIN_MAX_BACKUPS = 1;
|
|
12
|
+
const MAX_MAX_BYTES = 64 * 1024 * 1024;
|
|
13
|
+
const MAX_MAX_BACKUPS = 100;
|
|
14
|
+
const ROTATION_CHECK_INTERVAL = 32;
|
|
15
|
+
const MAX_TRACKED_LOG_PATHS = 64;
|
|
16
|
+
const MAX_REPORTED_FAILURES = 32;
|
|
17
|
+
const ROTATION_LOCK_STALE_MS = 30_000;
|
|
18
|
+
|
|
19
|
+
interface RotationLockOwner {
|
|
20
|
+
pid: number;
|
|
21
|
+
token: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function truthyEnv(value: string | undefined): boolean | undefined {
|
|
25
|
+
if (value === undefined) return undefined;
|
|
26
|
+
const trimmed = value.trim();
|
|
27
|
+
if (TRUE_ENV_RE.test(trimmed)) return true;
|
|
28
|
+
if (FALSE_ENV_RE.test(trimmed)) return false;
|
|
29
|
+
return undefined;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function positiveIntEnv(value: string | undefined): number | undefined {
|
|
33
|
+
if (value === undefined) return undefined;
|
|
34
|
+
const normalized = value.trim();
|
|
35
|
+
if (!/^\d+$/.test(normalized)) return undefined;
|
|
36
|
+
const parsed = Number(normalized);
|
|
37
|
+
return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : undefined;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function contextGatewayAccountingLogEnabled(config: ContextGatewayResolvedConfig): boolean {
|
|
41
|
+
return truthyEnv(process.env.PI_CONTEXT_GATEWAY_ACCOUNTING_LOG_ENABLED)
|
|
42
|
+
?? config.accountingLog?.enabled
|
|
43
|
+
?? true;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function contextGatewayAccountingLogPath(): string {
|
|
47
|
+
const explicit = process.env.PI_CONTEXT_GATEWAY_ACCOUNTING_LOG?.trim();
|
|
48
|
+
if (explicit) return explicit;
|
|
49
|
+
const agentDir = process.env.PI_AGENT_DIR || path.join(os.homedir(), ".pi", "agent");
|
|
50
|
+
return path.join(agentDir, "context-gateway-accounting.jsonl");
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function contextGatewayAccountingLogMaxBytes(config: ContextGatewayResolvedConfig): number {
|
|
54
|
+
const envValue = positiveIntEnv(process.env.PI_CONTEXT_GATEWAY_ACCOUNTING_MAX_BYTES);
|
|
55
|
+
return (envValue !== undefined && envValue <= MAX_MAX_BYTES ? envValue : undefined)
|
|
56
|
+
?? config.accountingLog?.maxBytes
|
|
57
|
+
?? DEFAULT_MAX_BYTES;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function contextGatewayAccountingLogMaxBackups(config: ContextGatewayResolvedConfig): number {
|
|
61
|
+
const envValue = positiveIntEnv(process.env.PI_CONTEXT_GATEWAY_ACCOUNTING_MAX_BACKUPS);
|
|
62
|
+
const value = (envValue !== undefined && envValue <= MAX_MAX_BACKUPS ? envValue : undefined)
|
|
63
|
+
?? config.accountingLog?.maxBackups
|
|
64
|
+
?? DEFAULT_MAX_BACKUPS;
|
|
65
|
+
return Math.max(MIN_MAX_BACKUPS, Math.floor(value));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
let writeChain: Promise<void> = Promise.resolve();
|
|
69
|
+
const ensuredDirs = new Set<string>();
|
|
70
|
+
const appendsSinceRotationCheck = new Map<string, number>();
|
|
71
|
+
const reportedFailures = new Set<string>();
|
|
72
|
+
|
|
73
|
+
function errorCode(error: unknown): string {
|
|
74
|
+
if (!error || typeof error !== "object" || !("code" in error)) return "unknown";
|
|
75
|
+
const code = (error as { code?: unknown }).code;
|
|
76
|
+
return typeof code === "string" ? code : "unknown";
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function reportFailure(operation: string, error: unknown): void {
|
|
80
|
+
const key = `${operation}:${errorCode(error)}`;
|
|
81
|
+
if (reportedFailures.has(key)) return;
|
|
82
|
+
if (reportedFailures.size >= MAX_REPORTED_FAILURES) reportedFailures.clear();
|
|
83
|
+
reportedFailures.add(key);
|
|
84
|
+
process.emitWarning(
|
|
85
|
+
`Context Gateway accounting ${operation} failed (${errorCode(error)}); accounting remains best-effort.`,
|
|
86
|
+
{ code: "PI_CONTEXT_GATEWAY_ACCOUNTING_FAILURE" },
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function setAppendCount(logPath: string, count: number): void {
|
|
91
|
+
if (!appendsSinceRotationCheck.has(logPath) && appendsSinceRotationCheck.size >= MAX_TRACKED_LOG_PATHS) {
|
|
92
|
+
const oldest = appendsSinceRotationCheck.keys().next().value;
|
|
93
|
+
if (oldest !== undefined) appendsSinceRotationCheck.delete(oldest);
|
|
94
|
+
}
|
|
95
|
+
appendsSinceRotationCheck.set(logPath, count);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async function ensureLogDir(logPath: string): Promise<void> {
|
|
99
|
+
const dir = path.dirname(logPath);
|
|
100
|
+
if (ensuredDirs.delete(dir)) {
|
|
101
|
+
ensuredDirs.add(dir);
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
await fs.mkdir(dir, { recursive: true });
|
|
105
|
+
if (ensuredDirs.size >= MAX_TRACKED_LOG_PATHS) {
|
|
106
|
+
const oldest = ensuredDirs.values().next().value;
|
|
107
|
+
if (oldest !== undefined) ensuredDirs.delete(oldest);
|
|
108
|
+
}
|
|
109
|
+
ensuredDirs.add(dir);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function rotationLockOwner(): RotationLockOwner {
|
|
113
|
+
return {
|
|
114
|
+
pid: process.pid,
|
|
115
|
+
token: `${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}`,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function isRotationLockOwner(value: unknown): value is RotationLockOwner {
|
|
120
|
+
if (!value || typeof value !== "object") return false;
|
|
121
|
+
const candidate = value as Partial<RotationLockOwner>;
|
|
122
|
+
return Number.isInteger(candidate.pid) && (candidate.pid ?? 0) > 0 && typeof candidate.token === "string";
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async function readRotationLockOwner(lockPath: string): Promise<RotationLockOwner | undefined> {
|
|
126
|
+
try {
|
|
127
|
+
const parsed: unknown = JSON.parse(await fs.readFile(lockPath, "utf8"));
|
|
128
|
+
return isRotationLockOwner(parsed) ? parsed : undefined;
|
|
129
|
+
} catch {
|
|
130
|
+
return undefined;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function isProcessAlive(pid: number): boolean {
|
|
135
|
+
try {
|
|
136
|
+
process.kill(pid, 0);
|
|
137
|
+
return true;
|
|
138
|
+
} catch (error) {
|
|
139
|
+
return errorCode(error) !== "ESRCH";
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
async function createRotationLock(lockPath: string, owner: RotationLockOwner): Promise<boolean> {
|
|
144
|
+
let handle: Awaited<ReturnType<typeof fs.open>>;
|
|
145
|
+
try {
|
|
146
|
+
handle = await fs.open(lockPath, "wx", 0o600);
|
|
147
|
+
} catch (error) {
|
|
148
|
+
if (errorCode(error) === "EEXIST") return false;
|
|
149
|
+
throw error;
|
|
150
|
+
}
|
|
151
|
+
try {
|
|
152
|
+
await handle.writeFile(JSON.stringify(owner), "utf8");
|
|
153
|
+
} catch (error) {
|
|
154
|
+
await handle.close();
|
|
155
|
+
await fs.unlink(lockPath).catch(() => {});
|
|
156
|
+
throw error;
|
|
157
|
+
}
|
|
158
|
+
await handle.close();
|
|
159
|
+
return true;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
async function acquireRotationLock(lockPath: string, owner: RotationLockOwner): Promise<boolean> {
|
|
163
|
+
if (await createRotationLock(lockPath, owner)) return true;
|
|
164
|
+
|
|
165
|
+
let lockStat: Awaited<ReturnType<typeof fs.stat>>;
|
|
166
|
+
try {
|
|
167
|
+
lockStat = await fs.stat(lockPath);
|
|
168
|
+
} catch (error) {
|
|
169
|
+
if (errorCode(error) === "ENOENT") return createRotationLock(lockPath, owner);
|
|
170
|
+
throw error;
|
|
171
|
+
}
|
|
172
|
+
const existingOwner = await readRotationLockOwner(lockPath);
|
|
173
|
+
if (existingOwner && isProcessAlive(existingOwner.pid)) return false;
|
|
174
|
+
if (!existingOwner && Date.now() - lockStat.mtimeMs <= ROTATION_LOCK_STALE_MS) return false;
|
|
175
|
+
|
|
176
|
+
const quarantinePath = `${lockPath}.stale-${owner.token}`;
|
|
177
|
+
try {
|
|
178
|
+
await fs.rename(lockPath, quarantinePath);
|
|
179
|
+
} catch (error) {
|
|
180
|
+
if (errorCode(error) === "ENOENT") return createRotationLock(lockPath, owner);
|
|
181
|
+
throw error;
|
|
182
|
+
}
|
|
183
|
+
const movedStat = await fs.stat(quarantinePath);
|
|
184
|
+
if (movedStat.dev !== lockStat.dev || movedStat.ino !== lockStat.ino) {
|
|
185
|
+
await fs.rename(quarantinePath, lockPath).catch(() => {});
|
|
186
|
+
return false;
|
|
187
|
+
}
|
|
188
|
+
await fs.rm(quarantinePath, { recursive: true, force: true });
|
|
189
|
+
return createRotationLock(lockPath, owner);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
async function releaseRotationLock(lockPath: string, owner: RotationLockOwner): Promise<void> {
|
|
193
|
+
const currentOwner = await readRotationLockOwner(lockPath);
|
|
194
|
+
if (currentOwner?.token !== owner.token) {
|
|
195
|
+
reportFailure("rotation lock ownership", { code: "EBUSY" });
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
await fs.unlink(lockPath);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
async function rotateIfNeeded(logPath: string, maxBytes: number, maxBackups: number): Promise<void> {
|
|
202
|
+
const lockPath = `${logPath}.rotation.lock`;
|
|
203
|
+
const lockOwner = rotationLockOwner();
|
|
204
|
+
let locked = false;
|
|
205
|
+
try {
|
|
206
|
+
locked = await acquireRotationLock(lockPath, lockOwner);
|
|
207
|
+
if (!locked) {
|
|
208
|
+
reportFailure("rotation lock", { code: "EBUSY" });
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
let stat: Awaited<ReturnType<typeof fs.stat>>;
|
|
213
|
+
try {
|
|
214
|
+
stat = await fs.stat(logPath);
|
|
215
|
+
} catch (error) {
|
|
216
|
+
if (errorCode(error) === "ENOENT") return;
|
|
217
|
+
throw error;
|
|
218
|
+
}
|
|
219
|
+
if (stat.size < maxBytes) return;
|
|
220
|
+
|
|
221
|
+
const dir = path.dirname(logPath);
|
|
222
|
+
const base = path.basename(logPath);
|
|
223
|
+
await fs.rm(path.join(dir, `${base}.${maxBackups}`), { force: true });
|
|
224
|
+
for (let index = maxBackups - 1; index >= 1; index--) {
|
|
225
|
+
try {
|
|
226
|
+
await fs.rename(path.join(dir, `${base}.${index}`), path.join(dir, `${base}.${index + 1}`));
|
|
227
|
+
} catch (error) {
|
|
228
|
+
if (errorCode(error) !== "ENOENT") throw error;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
await fs.rename(logPath, path.join(dir, `${base}.1`));
|
|
232
|
+
} catch (error) {
|
|
233
|
+
reportFailure("rotation", error);
|
|
234
|
+
} finally {
|
|
235
|
+
if (locked) {
|
|
236
|
+
await releaseRotationLock(lockPath, lockOwner).catch((error) => reportFailure("rotation unlock", error));
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
async function appendRecord(
|
|
242
|
+
logPath: string,
|
|
243
|
+
record: Record<string, unknown>,
|
|
244
|
+
maxBytes: number,
|
|
245
|
+
maxBackups: number,
|
|
246
|
+
): Promise<void> {
|
|
247
|
+
await ensureLogDir(logPath);
|
|
248
|
+
const appendCount = appendsSinceRotationCheck.get(logPath) ?? ROTATION_CHECK_INTERVAL;
|
|
249
|
+
if (appendCount >= ROTATION_CHECK_INTERVAL) {
|
|
250
|
+
await rotateIfNeeded(logPath, maxBytes, maxBackups);
|
|
251
|
+
setAppendCount(logPath, 0);
|
|
252
|
+
}
|
|
253
|
+
await fs.appendFile(logPath, `${JSON.stringify(record)}\n`, "utf8");
|
|
254
|
+
setAppendCount(logPath, (appendsSinceRotationCheck.get(logPath) ?? 0) + 1);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
export function writeContextGatewayAccountingLog(
|
|
258
|
+
config: ContextGatewayResolvedConfig,
|
|
259
|
+
event: string,
|
|
260
|
+
details: Record<string, unknown> = {},
|
|
261
|
+
): void {
|
|
262
|
+
if (!contextGatewayAccountingLogEnabled(config)) return;
|
|
263
|
+
const record = {
|
|
264
|
+
version: 1,
|
|
265
|
+
ts: new Date().toISOString(),
|
|
266
|
+
event,
|
|
267
|
+
...details,
|
|
268
|
+
};
|
|
269
|
+
const logPath = contextGatewayAccountingLogPath();
|
|
270
|
+
const maxBytes = contextGatewayAccountingLogMaxBytes(config);
|
|
271
|
+
const maxBackups = contextGatewayAccountingLogMaxBackups(config);
|
|
272
|
+
writeChain = writeChain
|
|
273
|
+
.then(() => appendRecord(logPath, record, maxBytes, maxBackups))
|
|
274
|
+
.catch((error) => {
|
|
275
|
+
// Accounting must never affect the session, provider call, or tool outcome.
|
|
276
|
+
reportFailure("write", error);
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
export function contextGatewayAccountingLogDrain(): Promise<void> {
|
|
281
|
+
return writeChain;
|
|
282
|
+
}
|
|
@@ -4,9 +4,11 @@ import { parse as parseJsonc } from "jsonc-parser";
|
|
|
4
4
|
|
|
5
5
|
import { getPiToolsSuiteUserConfigPath } from "../config.js";
|
|
6
6
|
import type {
|
|
7
|
+
ContextGatewayAccountingLogConfig,
|
|
7
8
|
ContextGatewayBudgets,
|
|
8
9
|
ContextGatewayMode,
|
|
9
10
|
ContextGatewayResolvedConfig,
|
|
11
|
+
ContextGatewayToolClass,
|
|
10
12
|
} from "./types.js";
|
|
11
13
|
|
|
12
14
|
export const DEFAULT_CONTEXT_GATEWAY_BUDGETS: Readonly<ContextGatewayBudgets> = Object.freeze({
|
|
@@ -17,8 +19,22 @@ export const DEFAULT_CONTEXT_GATEWAY_BUDGETS: Readonly<ContextGatewayBudgets> =
|
|
|
17
19
|
maxSearchMatches: 12,
|
|
18
20
|
});
|
|
19
21
|
|
|
20
|
-
export const
|
|
21
|
-
|
|
22
|
+
export const DEFAULT_CONTEXT_GATEWAY_ACCOUNTING_LOG: Readonly<ContextGatewayAccountingLogConfig> = Object.freeze({
|
|
23
|
+
enabled: true,
|
|
24
|
+
maxBytes: 5 * 1024 * 1024,
|
|
25
|
+
maxBackups: 3,
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
export function contextGatewayBudgetForClass(
|
|
29
|
+
toolClass: ContextGatewayToolClass,
|
|
30
|
+
budgets: ContextGatewayBudgets,
|
|
31
|
+
): number {
|
|
32
|
+
if (toolClass === "code-read") return budgets.maxExactReadBytes;
|
|
33
|
+
if (["repo-search", "repo-ast", "repo-structure", "ast-grep"].includes(toolClass)) {
|
|
34
|
+
return budgets.maxSearchBytes;
|
|
35
|
+
}
|
|
36
|
+
return budgets.maxResultBytes;
|
|
37
|
+
}
|
|
22
38
|
|
|
23
39
|
const MODES = new Set<ContextGatewayMode>(["off", "observe", "enforce"]);
|
|
24
40
|
const MAX_BYTE_BUDGET = 64 * 1024 * 1024;
|
|
@@ -57,6 +73,24 @@ function parseMode(value: unknown): ContextGatewayMode | undefined {
|
|
|
57
73
|
return MODES.has(normalized) ? normalized : undefined;
|
|
58
74
|
}
|
|
59
75
|
|
|
76
|
+
function parseBoolean(value: unknown): boolean | undefined {
|
|
77
|
+
return typeof value === "boolean" ? value : undefined;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function parseBooleanEnv(value: string): boolean | undefined {
|
|
81
|
+
const normalized = value.trim().toLowerCase();
|
|
82
|
+
if (["1", "true", "yes", "on"].includes(normalized)) return true;
|
|
83
|
+
if (["0", "false", "no", "off"].includes(normalized)) return false;
|
|
84
|
+
return undefined;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function parsePositiveIntegerEnv(value: string): number | undefined {
|
|
88
|
+
const normalized = value.trim();
|
|
89
|
+
if (!/^\d+$/.test(normalized)) return undefined;
|
|
90
|
+
const parsed = Number(normalized);
|
|
91
|
+
return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : undefined;
|
|
92
|
+
}
|
|
93
|
+
|
|
60
94
|
function mergeBudget(
|
|
61
95
|
budgets: ContextGatewayBudgets,
|
|
62
96
|
issues: string[],
|
|
@@ -79,6 +113,7 @@ export function loadContextGatewayConfig(
|
|
|
79
113
|
): ContextGatewayResolvedConfig {
|
|
80
114
|
let mode: ContextGatewayMode = "off";
|
|
81
115
|
const budgets: ContextGatewayBudgets = { ...DEFAULT_CONTEXT_GATEWAY_BUDGETS };
|
|
116
|
+
const accountingLog: ContextGatewayAccountingLogConfig = { ...DEFAULT_CONTEXT_GATEWAY_ACCOUNTING_LOG };
|
|
82
117
|
const issues: string[] = [];
|
|
83
118
|
|
|
84
119
|
const layers = [getPiToolsSuiteUserConfigPath(homeDir)];
|
|
@@ -99,6 +134,29 @@ export function loadContextGatewayConfig(
|
|
|
99
134
|
mergeBudget(budgets, issues, key, section.budgets[key]);
|
|
100
135
|
}
|
|
101
136
|
}
|
|
137
|
+
if (isRecord(section.accountingLog)) {
|
|
138
|
+
if (Object.prototype.hasOwnProperty.call(section.accountingLog, "enabled")) {
|
|
139
|
+
const enabled = parseBoolean(section.accountingLog.enabled);
|
|
140
|
+
if (enabled !== undefined) accountingLog.enabled = enabled;
|
|
141
|
+
else issues.push(`contextGateway.accountingLog.enabled must be boolean; keeping ${accountingLog.enabled}.`);
|
|
142
|
+
}
|
|
143
|
+
const maxBytes = section.accountingLog.maxBytes;
|
|
144
|
+
if (maxBytes !== undefined) {
|
|
145
|
+
if (typeof maxBytes === "number" && Number.isSafeInteger(maxBytes) && maxBytes > 0 && maxBytes <= MAX_BYTE_BUDGET) {
|
|
146
|
+
accountingLog.maxBytes = maxBytes;
|
|
147
|
+
} else {
|
|
148
|
+
issues.push(`contextGateway.accountingLog.maxBytes must be an integer in 1..${MAX_BYTE_BUDGET}; keeping ${accountingLog.maxBytes}.`);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
const maxBackups = section.accountingLog.maxBackups;
|
|
152
|
+
if (maxBackups !== undefined) {
|
|
153
|
+
if (typeof maxBackups === "number" && Number.isSafeInteger(maxBackups) && maxBackups >= 1 && maxBackups <= 100) {
|
|
154
|
+
accountingLog.maxBackups = maxBackups;
|
|
155
|
+
} else {
|
|
156
|
+
issues.push(`contextGateway.accountingLog.maxBackups must be an integer in 1..100; keeping ${accountingLog.maxBackups}.`);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
102
160
|
}
|
|
103
161
|
|
|
104
162
|
if (env.PI_CONTEXT_GATEWAY_MODE !== undefined) {
|
|
@@ -107,5 +165,21 @@ export function loadContextGatewayConfig(
|
|
|
107
165
|
else issues.push("PI_CONTEXT_GATEWAY_MODE must be off, observe, or enforce; ignoring it.");
|
|
108
166
|
}
|
|
109
167
|
|
|
110
|
-
|
|
168
|
+
if (env.PI_CONTEXT_GATEWAY_ACCOUNTING_LOG_ENABLED !== undefined) {
|
|
169
|
+
const enabled = parseBooleanEnv(env.PI_CONTEXT_GATEWAY_ACCOUNTING_LOG_ENABLED);
|
|
170
|
+
if (enabled !== undefined) accountingLog.enabled = enabled;
|
|
171
|
+
else issues.push("PI_CONTEXT_GATEWAY_ACCOUNTING_LOG_ENABLED must be true or false; ignoring it.");
|
|
172
|
+
}
|
|
173
|
+
if (env.PI_CONTEXT_GATEWAY_ACCOUNTING_MAX_BYTES !== undefined) {
|
|
174
|
+
const maxBytes = parsePositiveIntegerEnv(env.PI_CONTEXT_GATEWAY_ACCOUNTING_MAX_BYTES);
|
|
175
|
+
if (maxBytes !== undefined && maxBytes <= MAX_BYTE_BUDGET) accountingLog.maxBytes = maxBytes;
|
|
176
|
+
else issues.push(`PI_CONTEXT_GATEWAY_ACCOUNTING_MAX_BYTES must be an integer in 1..${MAX_BYTE_BUDGET}; ignoring it.`);
|
|
177
|
+
}
|
|
178
|
+
if (env.PI_CONTEXT_GATEWAY_ACCOUNTING_MAX_BACKUPS !== undefined) {
|
|
179
|
+
const maxBackups = parsePositiveIntegerEnv(env.PI_CONTEXT_GATEWAY_ACCOUNTING_MAX_BACKUPS);
|
|
180
|
+
if (maxBackups !== undefined && maxBackups <= 100) accountingLog.maxBackups = maxBackups;
|
|
181
|
+
else issues.push("PI_CONTEXT_GATEWAY_ACCOUNTING_MAX_BACKUPS must be an integer in 1..100; ignoring it.");
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return { mode, budgets, accountingLog, issues };
|
|
111
185
|
}
|