pipcode 0.0.0-stage → 2.1.1
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/LICENSE +47 -0
- package/README.md +44 -2
- package/auto.js +40 -0
- package/bin/setup.js +31 -0
- package/bin/teardown.js +18 -0
- package/grammars/tree-sitter-javascript.wasm +0 -0
- package/grammars/tree-sitter-python.wasm +0 -0
- package/grammars/tree-sitter-tsx.wasm +0 -0
- package/grammars/tree-sitter-typescript.wasm +0 -0
- package/index.js +39 -0
- package/package.json +45 -4
- package/src/auth/auth-email.js +32 -0
- package/src/auth/trusted-key.js +13 -0
- package/src/auto/adjust.js +41 -0
- package/src/auto/amnesia.js +37 -0
- package/src/auto/anonymize-tasks.js +13 -0
- package/src/auto/apply-diff-backed.js +10 -0
- package/src/auto/apply-diff.js +60 -0
- package/src/auto/ask-guard.js +16 -0
- package/src/auto/ask-pause.js +36 -0
- package/src/auto/ask-protocol.js +6 -0
- package/src/auto/ask-user.js +29 -0
- package/src/auto/backup-touched.js +30 -0
- package/src/auto/budget.js +52 -0
- package/src/auto/cancel.js +20 -0
- package/src/auto/constants.js +30 -0
- package/src/auto/crypto-util.js +17 -0
- package/src/auto/decompose.js +47 -0
- package/src/auto/diff.js +19 -0
- package/src/auto/fs-list.js +34 -0
- package/src/auto/git-base.js +16 -0
- package/src/auto/git-init.js +39 -0
- package/src/auto/git-restore.js +34 -0
- package/src/auto/git-scope.js +10 -0
- package/src/auto/git.js +38 -0
- package/src/auto/mission-docs.js +29 -0
- package/src/auto/mission-line.js +22 -0
- package/src/auto/node-check.js +30 -0
- package/src/auto/observe.js +23 -0
- package/src/auto/orchestrator.js +30 -0
- package/src/auto/patch-ask.js +16 -0
- package/src/auto/paths.js +18 -0
- package/src/auto/plan.js +42 -0
- package/src/auto/pricing.js +49 -0
- package/src/auto/project-files.js +12 -0
- package/src/auto/prompts.js +24 -0
- package/src/auto/quote-format.js +33 -0
- package/src/auto/retry.js +40 -0
- package/src/auto/run-lock.js +40 -0
- package/src/auto/run-loop.js +38 -0
- package/src/auto/run.js +49 -0
- package/src/auto/snapshot-gate.js +51 -0
- package/src/auto/snapshot-paths.js +33 -0
- package/src/auto/snapshot-restore.js +59 -0
- package/src/auto/snapshot-take.js +30 -0
- package/src/auto/state.js +52 -0
- package/src/auto/step-commit.js +46 -0
- package/src/auto/step-green.js +38 -0
- package/src/auto/step-loop.js +56 -0
- package/src/auto/step-prep.js +60 -0
- package/src/auto/step-qa.js +53 -0
- package/src/auto/step.js +30 -0
- package/src/auto/stop-options.js +34 -0
- package/src/auto/stop-report.js +17 -0
- package/src/auto/tasks.js +44 -0
- package/src/cli/apply-loop.js +29 -0
- package/src/cli/apply-toml.js +26 -0
- package/src/cli/apply.js +45 -0
- package/src/cli/ask-prepare.js +37 -0
- package/src/cli/auth-branches.js +53 -0
- package/src/cli/auth-interactive.js +46 -0
- package/src/cli/auto-ask.js +33 -0
- package/src/cli/auto-audit.js +16 -0
- package/src/cli/auto-cli.js +46 -0
- package/src/cli/auto-consent.js +22 -0
- package/src/cli/auto-flags.js +39 -0
- package/src/cli/auto-gate-session.js +27 -0
- package/src/cli/auto-gate.js +34 -0
- package/src/cli/auto-loader.js +17 -0
- package/src/cli/auto-plan.js +36 -0
- package/src/cli/auto-preflight.js +35 -0
- package/src/cli/auto-quote.js +28 -0
- package/src/cli/auto-report.js +36 -0
- package/src/cli/auto-resume.js +28 -0
- package/src/cli/auto-rules.js +20 -0
- package/src/cli/auto-run.js +26 -0
- package/src/cli/auto-task.js +26 -0
- package/src/cli/banner.js +20 -0
- package/src/cli/centralize.js +23 -0
- package/src/cli/collect-step.js +25 -0
- package/src/cli/config-pairs.js +46 -0
- package/src/cli/config.js +51 -0
- package/src/cli/detect-os.js +11 -0
- package/src/cli/dispatch.js +36 -0
- package/src/cli/env-file.js +44 -0
- package/src/cli/env-load.js +28 -0
- package/src/cli/env-parse.js +39 -0
- package/src/cli/env-rebuild.js +33 -0
- package/src/cli/gitignore-env.js +46 -0
- package/src/cli/help.js +28 -0
- package/src/cli/ide-entries.js +42 -0
- package/src/cli/ide-paths.js +37 -0
- package/src/cli/ide-roots.js +25 -0
- package/src/cli/interactive.js +26 -0
- package/src/cli/jetbrains.js +40 -0
- package/src/cli/legacy-entry.js +17 -0
- package/src/cli/main.js +42 -0
- package/src/cli/memory-cli.js +15 -0
- package/src/cli/prompts.js +24 -0
- package/src/cli/server-entry.js +42 -0
- package/src/cli/setup-args.js +39 -0
- package/src/cli/target-manual.js +24 -0
- package/src/cli/target-select.js +51 -0
- package/src/cli/toml-merge.js +56 -0
- package/src/cli/trial-check.js +15 -0
- package/src/cli/welcome.js +7 -0
- package/src/core/abort-signal.js +38 -0
- package/src/core/auto-deps.js +29 -0
- package/src/core/cache-ttl.js +21 -0
- package/src/core/config-base.js +38 -0
- package/src/core/config-cache.js +50 -0
- package/src/core/config.js +58 -0
- package/src/core/constants.js +31 -0
- package/src/core/crypto.js +45 -0
- package/src/core/dlp.js +51 -0
- package/src/core/doctrine-all.js +31 -0
- package/src/core/doctrine-caches.js +10 -0
- package/src/core/doctrine-fetch.js +49 -0
- package/src/core/doctrine-one.js +18 -0
- package/src/core/doctrine.js +39 -0
- package/src/core/dual-stream-parser.js +55 -0
- package/src/core/exports.js +28 -0
- package/src/core/google-route.js +17 -0
- package/src/core/identity.js +23 -0
- package/src/core/lang.js +19 -0
- package/src/core/leak-guard.js +40 -0
- package/src/core/llm-builder-anthropic.js +26 -0
- package/src/core/llm-builder-gemini.js +22 -0
- package/src/core/llm-builder-openai.js +26 -0
- package/src/core/llm-call.js +53 -0
- package/src/core/llm-config-secondary.js +28 -0
- package/src/core/llm-config.js +32 -0
- package/src/core/llm-errors.js +25 -0
- package/src/core/llm-exchange.js +27 -0
- package/src/core/llm-extract.js +52 -0
- package/src/core/llm-label.js +33 -0
- package/src/core/llm-ollama-request.js +32 -0
- package/src/core/llm-ollama.js +42 -0
- package/src/core/llm-prompts.js +14 -0
- package/src/core/llm-providers.js +26 -0
- package/src/core/llm-request.js +17 -0
- package/src/core/llm-retry-429.js +23 -0
- package/src/core/llm-stream-events.js +29 -0
- package/src/core/llm-stream.js +42 -0
- package/src/core/llm-unbounded.js +15 -0
- package/src/core/llm-warmup.js +44 -0
- package/src/core/manifest.js +42 -0
- package/src/core/master-prompt.js +11 -0
- package/src/core/model-id.js +19 -0
- package/src/core/ollama-lines.js +48 -0
- package/src/core/ollama.js +43 -0
- package/src/core/pkg-manager.js +35 -0
- package/src/core/process-guards.js +47 -0
- package/src/core/progress-emit.js +25 -0
- package/src/core/purge-key.js +20 -0
- package/src/core/reasoning-cut.js +19 -0
- package/src/core/reasoning-models.js +8 -0
- package/src/core/session-net.js +60 -0
- package/src/core/session-resilient.js +44 -0
- package/src/core/session.js +49 -0
- package/src/core/sse-parser.js +25 -0
- package/src/core/stdout-guard.js +11 -0
- package/src/core/tier.js +48 -0
- package/src/core/trial-resolve.js +22 -0
- package/src/core/trial-session.js +60 -0
- package/src/core/trial-status.js +35 -0
- package/src/core/usage-cost.js +25 -0
- package/src/core/usage-extract.js +28 -0
- package/src/core/usage-meter.js +41 -0
- package/src/engine/analyze-need.js +24 -0
- package/src/engine/analyze-ux.js +42 -0
- package/src/engine/ask-history.js +43 -0
- package/src/engine/ask-llm.js +40 -0
- package/src/engine/ask-prompt.js +48 -0
- package/src/engine/ask-rules.js +13 -0
- package/src/engine/ast-call-rules-danger.js +24 -0
- package/src/engine/ast-call-rules-inject.js +29 -0
- package/src/engine/ast-context.js +28 -0
- package/src/engine/ast-import-helpers.js +46 -0
- package/src/engine/ast-lang.js +24 -0
- package/src/engine/ast-node-helpers.js +51 -0
- package/src/engine/ast-rules-calls.js +16 -0
- package/src/engine/ast-rules-graph.js +58 -0
- package/src/engine/ast-rules-js.js +30 -0
- package/src/engine/ast-rules-jsx-py.js +26 -0
- package/src/engine/ast-runtime.js +60 -0
- package/src/engine/ast-scan.js +48 -0
- package/src/engine/audit-citations.js +28 -0
- package/src/engine/audit-facts.js +47 -0
- package/src/engine/audit-report.js +40 -0
- package/src/engine/audit-task.js +37 -0
- package/src/engine/cap-cache.js +50 -0
- package/src/engine/cap-eval.js +49 -0
- package/src/engine/cap-http.js +32 -0
- package/src/engine/cap-meta-anthropic.js +15 -0
- package/src/engine/cap-meta-ollama.js +15 -0
- package/src/engine/cap-meta-openrouter.js +19 -0
- package/src/engine/cap-meta.js +19 -0
- package/src/engine/cap-probe.js +34 -0
- package/src/engine/cap-types.js +29 -0
- package/src/engine/chat-doctrine.js +31 -0
- package/src/engine/checkpoint-control.js +23 -0
- package/src/engine/checkpoint-detect.js +19 -0
- package/src/engine/checkpoint-rules.js +19 -0
- package/src/engine/code-decls.js +12 -0
- package/src/engine/code-graph.js +52 -0
- package/src/engine/coerce.js +8 -0
- package/src/engine/collect.js +21 -0
- package/src/engine/compute-impact.js +17 -0
- package/src/engine/context-files.js +42 -0
- package/src/engine/context-fit.js +28 -0
- package/src/engine/context-margin.js +37 -0
- package/src/engine/context-relay.js +60 -0
- package/src/engine/control-duo.js +38 -0
- package/src/engine/control-phase.js +32 -0
- package/src/engine/control-prompt.js +25 -0
- package/src/engine/control-verdict.js +36 -0
- package/src/engine/default-steps.js +7 -0
- package/src/engine/deps-parse-js.js +49 -0
- package/src/engine/deps-parse-more.js +30 -0
- package/src/engine/deps-parse-other.js +41 -0
- package/src/engine/deps-parse.js +31 -0
- package/src/engine/det-builders-1.js +38 -0
- package/src/engine/det-builders-2.js +50 -0
- package/src/engine/detect-domains.js +27 -0
- package/src/engine/diff-files.js +35 -0
- package/src/engine/doc-suite.js +39 -0
- package/src/engine/doctrine-prefix.js +13 -0
- package/src/engine/domain-regex.js +11 -0
- package/src/engine/exports.js +35 -0
- package/src/engine/extract-deterministic.js +33 -0
- package/src/engine/extract-llm.js +48 -0
- package/src/engine/facts-binary.js +12 -0
- package/src/engine/facts-embedded.js +18 -0
- package/src/engine/facts-functions.js +21 -0
- package/src/engine/facts-html.js +22 -0
- package/src/engine/facts-inventory.js +33 -0
- package/src/engine/facts-lang.js +34 -0
- package/src/engine/facts-measure.js +43 -0
- package/src/engine/facts-patterns.js +28 -0
- package/src/engine/facts-project.js +26 -0
- package/src/engine/facts-size.js +26 -0
- package/src/engine/facts-wordpress.js +36 -0
- package/src/engine/generate-architecture-guide.js +46 -0
- package/src/engine/generate-readme.js +38 -0
- package/src/engine/generate-runbook.js +47 -0
- package/src/engine/git-facts.js +16 -0
- package/src/engine/history-purge.js +16 -0
- package/src/engine/json-balance.js +37 -0
- package/src/engine/json-safe.js +13 -0
- package/src/engine/line-numbers.js +16 -0
- package/src/engine/md-build-mid.js +35 -0
- package/src/engine/md-build-top.js +58 -0
- package/src/engine/md-build.js +36 -0
- package/src/engine/md-parse-helpers.js +7 -0
- package/src/engine/md-parse-pillars.js +38 -0
- package/src/engine/md-parse-ux-design.js +31 -0
- package/src/engine/md-parse.js +33 -0
- package/src/engine/mission-context-prefix.js +30 -0
- package/src/engine/mission-guard.js +36 -0
- package/src/engine/mission-helpers.js +23 -0
- package/src/engine/mission-log.js +56 -0
- package/src/engine/mission-paths.js +7 -0
- package/src/engine/mission-store.js +51 -0
- package/src/engine/normalize-extraction.js +35 -0
- package/src/engine/normalize-impact-design.js +39 -0
- package/src/engine/normalize-pillars.js +36 -0
- package/src/engine/orchestrate-route.js +39 -0
- package/src/engine/orchestrator-build.js +37 -0
- package/src/engine/orchestrator-phases.js +55 -0
- package/src/engine/orchestrator.js +40 -0
- package/src/engine/osv.js +43 -0
- package/src/engine/parse-json-loose.js +36 -0
- package/src/engine/patch-file-sections.js +28 -0
- package/src/engine/patch-scan-cheat.js +52 -0
- package/src/engine/patch-scan-prep.js +37 -0
- package/src/engine/patch-scan-rules.js +51 -0
- package/src/engine/patch-scan.js +38 -0
- package/src/engine/patch-secret-guard.js +50 -0
- package/src/engine/patch-ui-guard.js +49 -0
- package/src/engine/pattern-sanitize.js +23 -0
- package/src/engine/plan-draft.js +37 -0
- package/src/engine/plan-mission.js +60 -0
- package/src/engine/plan-reply-legacy.js +34 -0
- package/src/engine/plan-reply-pillars.js +34 -0
- package/src/engine/plan-reply-ux.js +17 -0
- package/src/engine/plan-reply.js +28 -0
- package/src/engine/plan-summary.js +16 -0
- package/src/engine/preflight-text.js +27 -0
- package/src/engine/preflight.js +27 -0
- package/src/engine/project-brief.js +41 -0
- package/src/engine/project-context.js +49 -0
- package/src/engine/project-instructions.js +19 -0
- package/src/engine/py-imports.js +31 -0
- package/src/engine/relay-analysis.js +44 -0
- package/src/engine/relay-digest.js +42 -0
- package/src/engine/route-llm.js +25 -0
- package/src/engine/rule-text.js +12 -0
- package/src/engine/self-assessment.js +33 -0
- package/src/engine/steps-block.js +19 -0
- package/src/engine/ux-violations-leak.js +43 -0
- package/src/engine/vuln-facts.js +37 -0
- package/src/fs/atomic-text.js +45 -0
- package/src/fs/atomic-write.js +21 -0
- package/src/fs/backup-once.js +27 -0
- package/src/fs/config-path.js +39 -0
- package/src/fs/config-read.js +23 -0
- package/src/fs/config-write.js +60 -0
- package/src/fs/home-root.js +55 -0
- package/src/fs/load-config.js +36 -0
- package/src/fs/os-errors.js +26 -0
- package/src/fs/permissions.js +48 -0
- package/src/mcp/boot-warmup.js +9 -0
- package/src/mcp/call-tool.js +20 -0
- package/src/mcp/client-info.js +35 -0
- package/src/mcp/engine-rules.js +16 -0
- package/src/mcp/handle-dlp.js +37 -0
- package/src/mcp/handle-licence.js +46 -0
- package/src/mcp/handle-list.js +15 -0
- package/src/mcp/handle-memory.js +37 -0
- package/src/mcp/handle-skill-fetch.js +50 -0
- package/src/mcp/handle-skill.js +38 -0
- package/src/mcp/licence-text.js +29 -0
- package/src/mcp/prompt-licence.js +11 -0
- package/src/mcp/server-create.js +33 -0
- package/src/mcp/server-main.js +39 -0
- package/src/mcp/tool-dispatch.js +28 -0
- package/src/mcp/tool-dlp.js +20 -0
- package/src/mcp/tool-licence.js +17 -0
- package/src/mcp/tool-memory.js +32 -0
- package/src/mcp/tool-skill.js +24 -0
- package/src/mcp/tools-list.js +13 -0
- package/src/memory/exports.js +5 -0
- package/src/memory/memory-filter.js +32 -0
- package/src/memory/memory-gate.js +21 -0
- package/src/memory/memory-store.js +44 -0
- package/src/net/auth-browser.js +49 -0
- package/src/net/auth-endpoints.js +33 -0
- package/src/net/local-fetch.js +31 -0
- package/src/net/loopback.js +8 -0
- package/src/net/proxy-env.js +59 -0
- package/src/project-memory/batches.js +17 -0
- package/src/project-memory/cahiers.js +42 -0
- package/src/project-memory/decision-settle.js +15 -0
- package/src/project-memory/decisions.js +36 -0
- package/src/project-memory/index-build.js +35 -0
- package/src/project-memory/memory-build.js +16 -0
- package/src/project-memory/memory-tags.js +40 -0
- package/src/project-memory/pending.js +24 -0
- package/src/project-memory/qa-apply.js +39 -0
- package/src/project-memory/qa-tags.js +28 -0
- package/src/project-memory/recall.js +38 -0
- package/src/project-memory/store.js +37 -0
- package/src/project-memory/window.js +13 -0
- package/src/rules/rules-advisor.js +17 -0
- package/src/rules/rules-frugality.js +17 -0
- package/src/rules/rules-load.js +56 -0
- package/src/rules/rules-project.js +16 -0
- package/src/rules/rules-registry.js +32 -0
- package/src/rules/rules-validate-pricing.js +22 -0
- package/src/rules/rules-validate-scan.js +45 -0
- package/src/rules/rules-validate.js +53 -0
- package/src/teardown/consent.js +31 -0
- package/src/teardown/flags.js +39 -0
- package/src/teardown/main.js +41 -0
- package/src/teardown/plan.js +31 -0
- package/src/teardown/remove-entry.js +39 -0
- package/src/teardown/remove-file.js +37 -0
- package/src/teardown/remove-toml.js +33 -0
- package/src/teardown/report.js +34 -0
- package/src/types/ast.d.ts +40 -0
- package/src/types/auto-ask.d.ts +6 -0
- package/src/types/auto-orch.d.ts +59 -0
- package/src/types/auto.d.ts +45 -0
- package/src/types/capability.d.ts +12 -0
- package/src/types/cli-auto-gate.d.ts +45 -0
- package/src/types/cli-auto.d.ts +42 -0
- package/src/types/config.d.ts +53 -0
- package/src/types/control.d.ts +56 -0
- package/src/types/docs.d.ts +6 -0
- package/src/types/doctrine.d.ts +22 -0
- package/src/types/llm.d.ts +28 -0
- package/src/types/mcp.d.ts +32 -0
- package/src/types/memory.d.ts +17 -0
- package/src/types/mission.d.ts +46 -0
- package/src/types/orchestrator.d.ts +33 -0
- package/src/types/routing.d.ts +25 -0
- package/src/types/rules.d.ts +30 -0
- package/src/types/snapshots.d.ts +25 -0
- package/src/types/teardown.d.ts +13 -0
- package/src/types/usage.d.ts +30 -0
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// src/core/session-net.js — requestSession (POST /v1/session Worker) + ensureSession (cache + renouvellement anticipé).
|
|
2
|
+
import { API_BASE, FETCH_TIMEOUT_MS, SESSION_RENEW_MARGIN_SEC } from "./constants.js";
|
|
3
|
+
import { machineFp } from "./identity.js";
|
|
4
|
+
import { parseSessionResponse } from "./session.js";
|
|
5
|
+
|
|
6
|
+
/** @type {LicenseSession | null} */
|
|
7
|
+
let sessionCache = null;
|
|
8
|
+
|
|
9
|
+
/** @param {PlugConfig} cfg @returns {Promise<LicenseSession>} */
|
|
10
|
+
export async function requestSession(cfg) {
|
|
11
|
+
const clientId = (cfg && cfg.clientId) || "";
|
|
12
|
+
const licenseKey = process.env.PLUGCTO_LICENSE_KEY || (cfg && cfg.licenseKey) || "";
|
|
13
|
+
if (!licenseKey) {
|
|
14
|
+
const err = /** @type {EnrichedError} */ (new Error("Aucune clé de licence configurée."));
|
|
15
|
+
err.code = "no_license";
|
|
16
|
+
throw err;
|
|
17
|
+
}
|
|
18
|
+
const url = `${API_BASE}/v1/session`;
|
|
19
|
+
const controller = new AbortController();
|
|
20
|
+
const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
|
|
21
|
+
try {
|
|
22
|
+
const res = await fetch(url, {
|
|
23
|
+
method: "POST",
|
|
24
|
+
signal: controller.signal,
|
|
25
|
+
headers: {
|
|
26
|
+
Accept: "application/json",
|
|
27
|
+
"Content-Type": "application/json",
|
|
28
|
+
"X-PlugCTO-Client": "thin/1.0",
|
|
29
|
+
"X-PlugCTO-Client-Id": clientId,
|
|
30
|
+
"X-PlugCTO-License": licenseKey,
|
|
31
|
+
"X-PlugCTO-Machine-Fp": machineFp(),
|
|
32
|
+
},
|
|
33
|
+
body: JSON.stringify({ license_key: licenseKey }),
|
|
34
|
+
});
|
|
35
|
+
return parseSessionResponse(res, await res.text());
|
|
36
|
+
} catch (err) {
|
|
37
|
+
const e0 = /** @type {EnrichedError} */ (err);
|
|
38
|
+
if (e0 && e0.name === "AbortError") {
|
|
39
|
+
const e = /** @type {EnrichedError} */ (new Error(`Timeout (${FETCH_TIMEOUT_MS}ms) sur ${url}.`));
|
|
40
|
+
e.code = "timeout";
|
|
41
|
+
throw e;
|
|
42
|
+
}
|
|
43
|
+
throw err;
|
|
44
|
+
} finally {
|
|
45
|
+
clearTimeout(timer);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Renvoie un token valide (cache + renouvellement anticipé). Purge anti-hostage.
|
|
50
|
+
/** @param {PlugConfig} cfg @returns {Promise<LicenseSession>} */
|
|
51
|
+
export async function ensureSession(cfg) {
|
|
52
|
+
const now = Math.floor(Date.now() / 1000);
|
|
53
|
+
if (sessionCache && sessionCache.expSec - now > SESSION_RENEW_MARGIN_SEC) return sessionCache;
|
|
54
|
+
sessionCache = await requestSession(cfg);
|
|
55
|
+
return sessionCache;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Purge Anti-Hostage : vide instantanément le cache RAM du token de session.
|
|
59
|
+
export function purgeSessionCache() { sessionCache = null; }
|
|
60
|
+
// MODULE_COMPLETE
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// src/core/session-resilient.js — Résolution de session résiliente MUTUALISÉE
|
|
2
|
+
// (gates Auto / Engine / Mémoire). Avec licence → ensureSession ; sans licence
|
|
3
|
+
// (ou Polar injoinable) → jeton d'essai SERVEUR /v1/trial (jamais de grant
|
|
4
|
+
// local, doctrine Zero Trust §3). Hors essai : 402/403/429 = licence INVALIDÉE
|
|
5
|
+
// → PURGE anti-hostage immédiate AVANT l'erreur ; autre échec = serveur
|
|
6
|
+
// indisponible (transitoire : pas de purge).
|
|
7
|
+
import { ensureSession } from "./session-net.js";
|
|
8
|
+
import { resolveTier } from "./tier.js";
|
|
9
|
+
import { resolveTrialOrLite } from "./trial-resolve.js";
|
|
10
|
+
import { purgeLicenseKey } from "./purge-key.js";
|
|
11
|
+
import { PRO_UPGRADE_URL } from "./constants.js";
|
|
12
|
+
|
|
13
|
+
/** @param {EnrichedError} e @returns {McpTextContent[]} */
|
|
14
|
+
function sessionFailure(e) {
|
|
15
|
+
if (e.code === "no_license") {
|
|
16
|
+
return [{ type: "text", text: `🔒 Licence requise. Débloquez PipCode Pro ($14.99 USD à vie, Polar.sh) : ${PRO_UPGRADE_URL}` }];
|
|
17
|
+
}
|
|
18
|
+
if (e.status === 402 || e.status === 403 || e.status === 429) {
|
|
19
|
+
return [{ type: "text", text: e.message || `Session refusée par le serveur (HTTP ${e.status}).` }];
|
|
20
|
+
}
|
|
21
|
+
return [{ type: "text", text: `Serveur de licence indisponible. ${e.message || "Erreur inconnue."} Vérifiez votre réseau et la valeur de PLUGCTO_API_BASE.` }];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** @param {PlugConfig} cfg @returns {Promise<SessionResolution>} */
|
|
25
|
+
export async function resolveSessionResilient(cfg) {
|
|
26
|
+
const hasLicense = !!(process.env.PLUGCTO_LICENSE_KEY || cfg.licenseKey);
|
|
27
|
+
if (!hasLicense) return resolveTrialOrLite(cfg);
|
|
28
|
+
try {
|
|
29
|
+
const session = await ensureSession(cfg);
|
|
30
|
+
return { session, tier: resolveTier(cfg, session) };
|
|
31
|
+
} catch (err) {
|
|
32
|
+
const e = /** @type {EnrichedError} */ (err);
|
|
33
|
+
// Polar injoinable : l'essai serveur peut rester actif (résilience). On ne
|
|
34
|
+
// retourne QUE si l'essai est accordé — sinon la purge anti-hostage ci-dessous.
|
|
35
|
+
const t = await resolveTrialOrLite(cfg);
|
|
36
|
+
if (t.tier === "trial") return t;
|
|
37
|
+
// Licence invalidée par le serveur → purge instantanée de la clé (anti-hostage).
|
|
38
|
+
if (e.status === 402 || e.status === 403 || e.status === 429) {
|
|
39
|
+
await purgeLicenseKey(cfg, `licence refusée par le serveur (HTTP ${e.status})`);
|
|
40
|
+
}
|
|
41
|
+
return { session: null, tier: "lite", failure: sessionFailure(e) };
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
// MODULE_COMPLETE
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// src/core/session.js — Crypto token session : décodage base64url + vérification Ed25519 + parsing réponse Worker.
|
|
2
|
+
import crypto from "node:crypto";
|
|
3
|
+
import { TRUSTED_PUBLIC_KEY } from "./crypto.js";
|
|
4
|
+
|
|
5
|
+
/** @param {string} s @returns {Buffer} */
|
|
6
|
+
function b64urlDecode(s) {
|
|
7
|
+
const pad = s.length % 4 === 0 ? "" : "=".repeat(4 - (s.length % 4));
|
|
8
|
+
return Buffer.from(s.replace(/-/g, "+").replace(/_/g, "/") + pad, "base64");
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** @param {string} token @returns {LicenseSessionPayload | null} */
|
|
12
|
+
export function verifySessionToken(token) {
|
|
13
|
+
try {
|
|
14
|
+
const parts = String(token).split(".");
|
|
15
|
+
if (parts.length !== 2) return null;
|
|
16
|
+
const [payloadB64, sigB64] = parts;
|
|
17
|
+
const payloadJson = b64urlDecode(payloadB64).toString("utf8");
|
|
18
|
+
const sig = b64urlDecode(sigB64);
|
|
19
|
+
const ok = crypto.verify(null, Buffer.from(payloadJson, "utf8"), TRUSTED_PUBLIC_KEY, sig);
|
|
20
|
+
if (!ok) return null;
|
|
21
|
+
const p = /** @type {LicenseSessionPayload} */ (JSON.parse(payloadJson));
|
|
22
|
+
if (typeof p.exp !== "number") return null;
|
|
23
|
+
if (p.exp <= Math.floor(Date.now() / 1000)) return null;
|
|
24
|
+
return p;
|
|
25
|
+
} catch {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Parse + vérifie crypto la réponse Worker. Retourne la session ou jette une Error enrichie.
|
|
31
|
+
/** @param {Response} res @param {string} bodyText @returns {LicenseSession} */
|
|
32
|
+
export function parseSessionResponse(res, bodyText) {
|
|
33
|
+
let parsed = null;
|
|
34
|
+
try { parsed = /** @type {{ token?: string, message?: string } | null} */ (JSON.parse(bodyText)); } catch { /* corps non-JSON : parsed reste null */ }
|
|
35
|
+
if (!res.ok) {
|
|
36
|
+
const msg = (parsed && parsed.message) || `HTTP ${res.status}`;
|
|
37
|
+
const err = /** @type {EnrichedError} */ (new Error(msg));
|
|
38
|
+
err.status = res.status;
|
|
39
|
+
err.payload = parsed;
|
|
40
|
+
throw err;
|
|
41
|
+
}
|
|
42
|
+
if (!parsed || typeof parsed.token !== "string") {
|
|
43
|
+
throw new Error("Réponse session malformée (champ `token` manquant).");
|
|
44
|
+
}
|
|
45
|
+
const verified = verifySessionToken(parsed.token);
|
|
46
|
+
if (!verified) throw new Error("Token de session invalide (signature Ed25519 rejetée ou expiré).");
|
|
47
|
+
return { token: parsed.token, tier: verified.tier, expSec: verified.exp };
|
|
48
|
+
}
|
|
49
|
+
// MODULE_COMPLETE
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// src/core/sse-parser.js — Analyseur de flux SSE (Server-Sent Events) : les morceaux réseau arrivent coupés n'importe où ; un événement se termine par une
|
|
2
|
+
// ligne vide ; seules les lignes « data: » comptent (commentaires et « event: » ignorés : le JSON porte déjà son type). Pur.
|
|
3
|
+
|
|
4
|
+
export class SseParser {
|
|
5
|
+
/** @param {(data: string) => void} onData appelé pour chaque événement complet (données jointes par des retours ligne) */
|
|
6
|
+
constructor(onData) { this.onData = onData; this.buf = ""; }
|
|
7
|
+
|
|
8
|
+
/** @param {string} chunk */
|
|
9
|
+
push(chunk) {
|
|
10
|
+
this.buf += chunk;
|
|
11
|
+
const blocks = this.buf.split(/\r?\n\r?\n/);
|
|
12
|
+
this.buf = blocks.pop() ?? "";
|
|
13
|
+
for (const block of blocks) this.#event(block);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Fin du flux : un dernier événement non terminé par une ligne vide est tout de même lu. */
|
|
17
|
+
end() { const rest = this.buf; this.buf = ""; if (rest.trim()) this.#event(rest); }
|
|
18
|
+
|
|
19
|
+
/** @param {string} block */
|
|
20
|
+
#event(block) {
|
|
21
|
+
const data = block.split(/\r?\n/).filter((l) => l.startsWith("data:")).map((l) => l.slice(5).replace(/^ /, "")).join("\n");
|
|
22
|
+
if (data) this.onData(data);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
// MODULE_COMPLETE
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// src/core/stdout-guard.js — Hygiène stdio du serveur MCP : stdout est le
|
|
2
|
+
// canal protocole JSON-RPC, écrit uniquement par le SDK de transport ; les
|
|
3
|
+
// logs applicatifs vont sur stderr. Garantie réelle : console.log est
|
|
4
|
+
// redirigé vers stderr. process.stdout n'est PAS wrappé — il reste la
|
|
5
|
+
// propriété exclusive du SDK (et intact à l'import pour le runner de tests).
|
|
6
|
+
|
|
7
|
+
/** Redirige console.log vers stderr. @returns {void} */
|
|
8
|
+
export function installStdoutGuard() {
|
|
9
|
+
console.log = (...args) => console.error("[stdout-guard]", ...args);
|
|
10
|
+
}
|
|
11
|
+
// MODULE_COMPLETE
|
package/src/core/tier.js
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// src/core/tier.js — Modèle 3 états (Trial / Lite / Pro Polar.sh) + messages de transition.
|
|
2
|
+
// resolveTier : pro/trial UNIQUEMENT via token signé serveur (essai /v1/trial) — zéro droit sur l'horloge locale (Zero Trust §3).
|
|
3
|
+
// isWithinTrial : UX uniquement. maybeTrialExpiredNotice : transition Trial→Lite UNE FOIS (persiste le flag) + purge anti-hostage.
|
|
4
|
+
|
|
5
|
+
import { TRIAL_HOURS, PRO_UPGRADE_URL } from "./constants.js";
|
|
6
|
+
import { saveConfig } from "./config-base.js";
|
|
7
|
+
import { purgeLicenseKey } from "./purge-key.js";
|
|
8
|
+
|
|
9
|
+
// Message Freemium : fiches / Mode Auto / Audit / 2ᵉ IA / mémoire bloqués en Lite. Volontairement COURT : un constat et le lien (le chat gratuit reste ouvert).
|
|
10
|
+
export const LITE_BLOCK_TEXT = `🔒 Fonction Pro. Licence Pro : ${PRO_UPGRADE_URL}`;
|
|
11
|
+
|
|
12
|
+
// Message de transition Trial → Lite (non-bloquant, append au content de la réponse MCP).
|
|
13
|
+
export const TRIAL_EXPIRED_NOTICE = `Version d'essai terminée. Licence Pro : ${PRO_UPGRADE_URL}`;
|
|
14
|
+
|
|
15
|
+
// Trial actif si moins de TRIAL_HOURS (120 h = 5 jours) depuis l'installation.
|
|
16
|
+
/** @param {PlugConfig} cfg @returns {boolean} */
|
|
17
|
+
export function isWithinTrial(cfg) {
|
|
18
|
+
const installed = Date.parse(cfg.installedAt);
|
|
19
|
+
if (Number.isNaN(installed)) return false;
|
|
20
|
+
return Date.now() - installed < TRIAL_HOURS * 3600 * 1000;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** @param {PlugConfig} _cfg @param {LicenseSession | null} session @returns {"pro" | "trial" | "lite"} */
|
|
24
|
+
export function resolveTier(_cfg, session) {
|
|
25
|
+
if (session && (session.tier === "pro" || session.tier === "trial")) return /** @type {"pro" | "trial"} */ (session.tier);
|
|
26
|
+
return "lite";
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Licence configurée (env PLUGCTO_LICENSE_KEY ou config) ?
|
|
30
|
+
/** @param {PlugConfig} cfg @returns {boolean} */
|
|
31
|
+
export function hasLicenseKey(cfg) {
|
|
32
|
+
return !!(process.env.PLUGCTO_LICENSE_KEY || (cfg && cfg.licenseKey));
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Notice Trial→Lite UNE FOIS : retourne le texte si transition fraîche, persiste, puis "".
|
|
36
|
+
/** @param {PlugConfig} cfg @returns {Promise<string>} */
|
|
37
|
+
export async function maybeTrialExpiredNotice(cfg) {
|
|
38
|
+
if (isWithinTrial(cfg)) return "";
|
|
39
|
+
if (cfg.trialExpiredNotified) return "";
|
|
40
|
+
if (hasLicenseKey(cfg)) return "";
|
|
41
|
+
cfg.trialExpiredNotified = true;
|
|
42
|
+
// Anti-Hostage : à l'expiration de l'essai, purge de toute clé résiduelle
|
|
43
|
+
// (config + cache RAM) avant le basculement en plan gratuit.
|
|
44
|
+
await purgeLicenseKey(cfg, "essai expiré — basculement plan gratuit");
|
|
45
|
+
await saveConfig(cfg);
|
|
46
|
+
return TRIAL_EXPIRED_NOTICE;
|
|
47
|
+
}
|
|
48
|
+
// MODULE_COMPLETE
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// src/core/trial-resolve.js — Résolution d'essai résiliente : jeton serveur ou
|
|
2
|
+
// bascule Lite. JAMAIS de grant local (Zero Trust, doctrine CLAUDE.md §3) :
|
|
3
|
+
// 402 du serveur = essai terminé (état NORMAL, sans failure — les gates
|
|
4
|
+
// affichent leur message Lite dédié) ; autre échec = serveur indisponible.
|
|
5
|
+
|
|
6
|
+
import { ensureTrialSession } from "./trial-session.js";
|
|
7
|
+
|
|
8
|
+
/** @param {PlugConfig} cfg @returns {Promise<SessionResolution>} */
|
|
9
|
+
export async function resolveTrialOrLite(cfg) {
|
|
10
|
+
try {
|
|
11
|
+
const session = await ensureTrialSession(cfg);
|
|
12
|
+
return { session, tier: "trial" };
|
|
13
|
+
} catch (err) {
|
|
14
|
+
const e = /** @type {EnrichedError} */ (err);
|
|
15
|
+
if (e && e.status === 402) return { session: null, tier: "lite" };
|
|
16
|
+
return {
|
|
17
|
+
session: null, tier: "lite",
|
|
18
|
+
failure: [{ type: "text", text: `Serveur d'essai indisponible. ${(e && e.message) || "Erreur inconnue."} Vérifiez votre réseau et la valeur de PLUGCTO_API_BASE.` }],
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
// MODULE_COMPLETE
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// src/core/trial-session.js — Jeton d'essai signé Ed25519 (POST /v1/trial Worker).
|
|
2
|
+
// Zero Trust (doctrine CLAUDE.md §3) : le droit d'essai vient UNIQUEMENT du
|
|
3
|
+
// serveur (fenêtre KV 120 h, horloge serveur) — jamais de l'horloge locale.
|
|
4
|
+
// Token lié à l'empreinte machine (non transférable) ; iat contrôlé (skew 5 min).
|
|
5
|
+
|
|
6
|
+
import crypto from "node:crypto";
|
|
7
|
+
import { API_BASE, FETCH_TIMEOUT_MS, SESSION_GRACE_SEC, SESSION_RENEW_MARGIN_SEC } from "./constants.js";
|
|
8
|
+
import { machineFp } from "./identity.js";
|
|
9
|
+
import { parseSessionResponse, verifySessionToken } from "./session.js";
|
|
10
|
+
|
|
11
|
+
/** @type {LicenseSession | null} */
|
|
12
|
+
let trialCache = null;
|
|
13
|
+
|
|
14
|
+
// Empreinte d'essai attendue : miroir EXACT du worker (sha256 base64 STANDARD du machineFp).
|
|
15
|
+
/** @returns {string} */
|
|
16
|
+
function expectedTrialFp() {
|
|
17
|
+
return "trial:" + crypto.createHash("sha256").update(machineFp()).digest("base64").replace(/=+$/, "");
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Contrôles Zero Trust du token délivré : tier, empreinte machine, iat.
|
|
21
|
+
/** @param {LicenseSession} s @returns {LicenseSession} */
|
|
22
|
+
function verifyTrialPayload(s) {
|
|
23
|
+
const p = verifySessionToken(s.token);
|
|
24
|
+
if (!p || p.tier !== "trial" || p.fp !== expectedTrialFp()) throw new Error("Token d'essai invalide (tier ou empreinte machine rejetée).");
|
|
25
|
+
if (p.iat > Math.floor(Date.now() / 1000) + SESSION_GRACE_SEC) throw new Error("Token d'essai rejeté (horloge locale en retrait de la date d'émission serveur).");
|
|
26
|
+
return s;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// POST /v1/trial — headers Client-Id + Machine-Fp (aucune clé, aucun secret).
|
|
30
|
+
/** @param {PlugConfig} cfg @returns {Promise<LicenseSession>} */
|
|
31
|
+
export async function requestTrialSession(cfg) {
|
|
32
|
+
const controller = new AbortController();
|
|
33
|
+
const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
|
|
34
|
+
try {
|
|
35
|
+
const res = await fetch(`${API_BASE}/v1/trial`, {
|
|
36
|
+
method: "POST", signal: controller.signal,
|
|
37
|
+
headers: {
|
|
38
|
+
Accept: "application/json", "Content-Type": "application/json",
|
|
39
|
+
"X-PlugCTO-Client": "thin/1.0", "X-PlugCTO-Client-Id": (cfg && cfg.clientId) || "",
|
|
40
|
+
"X-PlugCTO-Machine-Fp": machineFp(),
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
return verifyTrialPayload(parseSessionResponse(res, await res.text()));
|
|
44
|
+
} finally {
|
|
45
|
+
clearTimeout(timer);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Renvoie un token d'essai valide (cache RAM + renouvellement anticipé 60 s).
|
|
50
|
+
/** @param {PlugConfig} cfg @returns {Promise<LicenseSession>} */
|
|
51
|
+
export async function ensureTrialSession(cfg) {
|
|
52
|
+
const now = Math.floor(Date.now() / 1000);
|
|
53
|
+
if (trialCache && trialCache.expSec - now > SESSION_RENEW_MARGIN_SEC) return trialCache;
|
|
54
|
+
trialCache = await requestTrialSession(cfg);
|
|
55
|
+
return trialCache;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Purge Anti-Hostage : vide le cache RAM du token d'essai.
|
|
59
|
+
export function purgeTrialCache() { trialCache = null; }
|
|
60
|
+
// MODULE_COMPLETE
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// src/core/trial-status.js — État de l'essai tel que le SERVEUR le tranche (POST /v1/trial).
|
|
2
|
+
// Lecture seule pour l'affichage : aucun droit n'est accordé ici (l'accès reste porté par le
|
|
3
|
+
// jeton signé du moteur). Un 402 = essai terminé, définitif (aucune réouverture côté client).
|
|
4
|
+
// debt: 12 lignes, mutualiser l'appel avec trial-session.requestTrialSession si un 3e appelant apparaît.
|
|
5
|
+
|
|
6
|
+
import { API_BASE, FETCH_TIMEOUT_MS } from "./constants.js";
|
|
7
|
+
import { machineFp } from "./identity.js";
|
|
8
|
+
import { parseSessionResponse } from "./session.js";
|
|
9
|
+
|
|
10
|
+
const isoOrUndefined = (/** @type {unknown} */ v) => (typeof v === "string" && !Number.isNaN(Date.parse(v)) ? new Date(v).toISOString() : undefined);
|
|
11
|
+
const httpUrl = (/** @type {unknown} */ v) => (typeof v === "string" && /^https:\/\/[^\s]+$/.test(v) ? v : undefined);
|
|
12
|
+
|
|
13
|
+
/** @param {PlugConfig} cfg @returns {Promise<TrialStatus>} */
|
|
14
|
+
export async function trialStatus(cfg) {
|
|
15
|
+
const controller = new AbortController();
|
|
16
|
+
const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
|
|
17
|
+
try {
|
|
18
|
+
const res = await fetch(`${API_BASE}/v1/trial`, {
|
|
19
|
+
method: "POST", signal: controller.signal,
|
|
20
|
+
headers: { Accept: "application/json", "X-PlugCTO-Client": "thin/1.0", "X-PlugCTO-Client-Id": (cfg && cfg.clientId) || "", "X-PlugCTO-Machine-Fp": machineFp() },
|
|
21
|
+
});
|
|
22
|
+
const text = await res.text();
|
|
23
|
+
parseSessionResponse(res, text); // vérifie la signature Ed25519 du jeton ; lève sur 402/erreur.
|
|
24
|
+
/** @type {{ trial_ends_at?: unknown }} */
|
|
25
|
+
const body = JSON.parse(text);
|
|
26
|
+
return { state: "active", endsAt: isoOrUndefined(body.trial_ends_at) };
|
|
27
|
+
} catch (e) {
|
|
28
|
+
const err = /** @type {EnrichedError} */ (e);
|
|
29
|
+
if (err.status === 402) return { state: "expired", upgrade: httpUrl(err.payload && /** @type {{ upgrade?: unknown }} */ (err.payload).upgrade) };
|
|
30
|
+
return { state: "error", message: err.name === "AbortError" ? "serveur injoignable (délai dépassé)" : String(err.message).slice(0, 200) };
|
|
31
|
+
} finally {
|
|
32
|
+
clearTimeout(timer);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
// MODULE_COMPLETE
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// src/core/usage-cost.js — Coût d'un cumul d'usage, sans AUCUNE valeur inventée. Sources, par priorité :
|
|
2
|
+
// (1) coût facturé rapporté par l'API elle-même (OpenRouter) quand TOUS les appels l'ont fourni ; (2) modèle local
|
|
3
|
+
// (Ollama / localhost) = 0 exactement ; (3) grille tarifaire servie par le serveur pour ce modèle (USD / million de
|
|
4
|
+
// tokens, date d'effet jointe ; lecture de cache et écriture de cache facturées à leur propre tarif quand la grille le
|
|
5
|
+
// précise) ; sinon inconnu (null) — l'interface affiche alors « n/d », jamais un prix supposé.
|
|
6
|
+
|
|
7
|
+
const LOCAL = /^https?:\/\/(localhost|127\.0\.0\.1|\[::1\])(:|\/|$)/i;
|
|
8
|
+
|
|
9
|
+
/** @param {string} base @returns {boolean} */
|
|
10
|
+
export const isLocalBase = (base) => LOCAL.test(String(base || ""));
|
|
11
|
+
|
|
12
|
+
/** Entrée de grille d'un modèle ; l'identifiant Gemini « models/xxx » est ramené à « xxx ». @param {PricingGrid} grid @param {string} model @returns {PriceEntry | undefined} */
|
|
13
|
+
const priceOf = (grid, model) => grid.models[model] ?? grid.models[model.replace(/^models\//, "")];
|
|
14
|
+
|
|
15
|
+
/** @param {UsageEntry} e @param {PricingGrid | null} grid @returns {Pick<UsageReportEntry, "cost" | "costSource" | "grid">} */
|
|
16
|
+
export function costOf(e, grid) {
|
|
17
|
+
if (e.calls > 0 && e.apiCostCalls === e.calls) return { cost: e.apiCost, costSource: "api", grid: null };
|
|
18
|
+
if (e.local) return { cost: 0, costSource: "local", grid: null };
|
|
19
|
+
const p = grid && priceOf(grid, e.model);
|
|
20
|
+
if (!grid || !p) return { cost: null, costSource: null, grid: null };
|
|
21
|
+
const plain = e.input - e.cached - e.cacheWrite; // tokens d'entrée au tarif normal
|
|
22
|
+
const cost = (plain * p.in + e.cached * (p.cachedIn ?? p.in) + e.cacheWrite * (p.cacheWriteIn ?? p.in) + e.output * p.out) / 1e6;
|
|
23
|
+
return { cost, costSource: "grille", grid: { in: p.in, out: p.out, cachedIn: p.cachedIn ?? null, asOf: grid.asOf, note: p.note ?? null } };
|
|
24
|
+
}
|
|
25
|
+
// MODULE_COMPLETE
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// src/core/usage-extract.js — Usage RÉEL rapporté par l'API du fournisseur dans sa réponse (jamais estimé) :
|
|
2
|
+
// OpenAI/Zhipu/OpenRouter/Ollama (usage.prompt_tokens / completion_tokens, détail cache, coût OpenRouter), Anthropic
|
|
3
|
+
// (usage.input_tokens / output_tokens + lectures de cache) et Gemini (usageMetadata). Champ absent → 0 ou null :
|
|
4
|
+
// aucune valeur inventée. Pur.
|
|
5
|
+
|
|
6
|
+
const num = (/** @type {unknown} */ v) => (typeof v === "number" && Number.isFinite(v) && v >= 0 ? v : 0);
|
|
7
|
+
|
|
8
|
+
/** @param {string} provider @param {unknown} body réponse JSON déjà analysée @returns {UsageSample | null} */
|
|
9
|
+
export function extractUsage(provider, body) {
|
|
10
|
+
const b = /** @type {ProviderUsageBody | null} */ (body && typeof body === "object" ? body : null);
|
|
11
|
+
if (!b) return null;
|
|
12
|
+
if (provider === "anthropic") {
|
|
13
|
+
const u = b.usage;
|
|
14
|
+
if (!u) return null;
|
|
15
|
+
const cached = num(u.cache_read_input_tokens);
|
|
16
|
+
const cacheWrite = num(u.cache_creation_input_tokens);
|
|
17
|
+
return { input: num(u.input_tokens) + cached + cacheWrite, output: num(u.output_tokens), cached, cacheWrite, cost: null };
|
|
18
|
+
}
|
|
19
|
+
if (provider === "gemini") {
|
|
20
|
+
const u = b.usageMetadata;
|
|
21
|
+
return u ? { input: num(u.promptTokenCount), output: num(u.candidatesTokenCount), cached: num(u.cachedContentTokenCount), cacheWrite: 0, cost: null } : null;
|
|
22
|
+
}
|
|
23
|
+
const u = b.usage; // openai / zhipu et compatibles (OpenRouter, Ollama, proxy)
|
|
24
|
+
if (!u) return null;
|
|
25
|
+
const cost = typeof u.cost === "number" && Number.isFinite(u.cost) && u.cost >= 0 ? u.cost : null; // coût facturé, rapporté par OpenRouter
|
|
26
|
+
return { input: num(u.prompt_tokens), output: num(u.completion_tokens), cached: num(u.prompt_tokens_details?.cached_tokens), cacheWrite: 0, cost };
|
|
27
|
+
}
|
|
28
|
+
// MODULE_COMPLETE
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// src/core/usage-meter.js — Compteur d'usage RÉEL du processus : chaque appel modèle réussi y dépose les tokens
|
|
2
|
+
// rapportés par l'API (jamais estimés). À la sortie, SI l'extension le demande (PLUGCTO_EMIT_USAGE=1 — jamais dans un
|
|
3
|
+
// terminal ordinaire : PipCode n'impose rien à l'environnement hôte), le relevé est écrit sur stderr, en une ligne
|
|
4
|
+
// `PLUGCTO_USAGE <json>`, par écriture synchrone (fiable même après process.exit).
|
|
5
|
+
import fs from "node:fs";
|
|
6
|
+
import { extractUsage } from "./usage-extract.js";
|
|
7
|
+
import { costOf, isLocalBase } from "./usage-cost.js";
|
|
8
|
+
import { getRules } from "../rules/rules-registry.js";
|
|
9
|
+
|
|
10
|
+
export const USAGE_PREFIX = "PLUGCTO_USAGE ";
|
|
11
|
+
/** @type {Map<string, UsageEntry>} */
|
|
12
|
+
const entries = new Map();
|
|
13
|
+
let hooked = false;
|
|
14
|
+
|
|
15
|
+
/** Dépose l'usage d'une réponse fournisseur réussie. @param {string} provider @param {string} model @param {string} base @param {string} bodyText */
|
|
16
|
+
export function recordUsage(provider, model, base, bodyText) {
|
|
17
|
+
/** @type {UsageSample | null} */ let s = null;
|
|
18
|
+
try { s = extractUsage(provider, JSON.parse(bodyText)); } catch { /* corps non-JSON : aucun usage à compter */ }
|
|
19
|
+
if (!s) return;
|
|
20
|
+
const local = isLocalBase(base);
|
|
21
|
+
const key = `${provider}|${model}|${local}`;
|
|
22
|
+
const e = entries.get(key) ?? { provider, model, local, calls: 0, input: 0, output: 0, cached: 0, cacheWrite: 0, apiCost: 0, apiCostCalls: 0 };
|
|
23
|
+
entries.set(key, { ...e, calls: e.calls + 1, input: e.input + s.input, output: e.output + s.output, cached: e.cached + s.cached, cacheWrite: e.cacheWrite + s.cacheWrite, apiCost: e.apiCost + (s.cost ?? 0), apiCostCalls: e.apiCostCalls + (s.cost === null ? 0 : 1) });
|
|
24
|
+
hookExit();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Relevé courant (coûts calculés avec la grille du serveur si les règles sont chargées). @returns {{ v: 1, entries: UsageReportEntry[] }} */
|
|
28
|
+
export function usageReport() {
|
|
29
|
+
/** @type {PricingGrid | null} */ let grid = null;
|
|
30
|
+
try { grid = getRules().pricing; } catch { /* règles non chargées : pas de grille, coût inconnu sauf API/local */ }
|
|
31
|
+
return { v: 1, entries: [...entries.values()].map((e) => ({ provider: e.provider, model: e.model, calls: e.calls, input: e.input, output: e.output, cached: e.cached, ...costOf(e, grid) })) };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function hookExit() {
|
|
35
|
+
if (hooked || process.env.PLUGCTO_EMIT_USAGE !== "1") return;
|
|
36
|
+
hooked = true;
|
|
37
|
+
process.on("exit", () => {
|
|
38
|
+
try { fs.writeSync(2, `${USAGE_PREFIX}${JSON.stringify(usageReport())}\n`); } catch { /* stderr fermé : rien à signaler */ }
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
// MODULE_COMPLETE
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// src/engine/analyze-need.js — Analyse un besoin en matrice métier structurée.
|
|
2
|
+
import { getRules } from "../rules/rules-registry.js";
|
|
3
|
+
import { detectDomains, mapDomainsToSlugs } from "./detect-domains.js";
|
|
4
|
+
import { analyzeUxDesign } from "./analyze-ux.js";
|
|
5
|
+
|
|
6
|
+
// Retourne { need, domainKeys, domains, roles, regles, contraintes, failles, fiches,
|
|
7
|
+
// uxDirectives, designSystem }. uxDirectives + designSystem viennent de analyzeUxDesign.
|
|
8
|
+
/** @param {string} needText @returns {{ need: string, domainKeys: string[], domains: { key: string, label: string, roles: string[], regles: string[], contraintes: string[], failles: string[] }[], roles: string[], regles: string[], contraintes: string[], failles: string[], fiches: string[], uxDirectives: string[], designSystem: object }} */
|
|
9
|
+
export function analyzeNeed(needText) {
|
|
10
|
+
const need = String(needText || "").trim();
|
|
11
|
+
const keys = detectDomains(need);
|
|
12
|
+
const domains = keys.map((k) => {
|
|
13
|
+
const d = getRules().domains[k];
|
|
14
|
+
return { key: k, label: d.label, roles: d.roles, regles: d.regles, contraintes: d.contraintes, failles: d.failles };
|
|
15
|
+
});
|
|
16
|
+
const roles = [...new Set(domains.flatMap((d) => d.roles))];
|
|
17
|
+
const regles = domains.flatMap((d) => d.regles.map((r) => `[${d.label}] ${r}`));
|
|
18
|
+
const contraintes = domains.flatMap((d) => d.contraintes.map((c) => `[${d.label}] ${c}`));
|
|
19
|
+
const failles = domains.flatMap((d) => d.failles.map((f) => `[${d.label}] ${f}`));
|
|
20
|
+
const fiches = mapDomainsToSlugs(keys);
|
|
21
|
+
const { uxDirectives, designSystem } = analyzeUxDesign(keys);
|
|
22
|
+
return { need, domainKeys: keys, domains, roles, regles, contraintes, failles, fiches, uxDirectives, designSystem };
|
|
23
|
+
}
|
|
24
|
+
// MODULE_COMPLETE
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// src/engine/analyze-ux.js — Charte visuelle + directives UX contextuelles au secteur.
|
|
2
|
+
import { getRules } from "../rules/rules-registry.js";
|
|
3
|
+
import { ensureDataLeakGuard } from "./ux-violations-leak.js";
|
|
4
|
+
|
|
5
|
+
/** @type {UxDesignEntry} */
|
|
6
|
+
const EMPTY_FAMILY = { family: "", style: "", density: "", palette: "", microInteractions: "", uxRules: [], designViolations: [] };
|
|
7
|
+
|
|
8
|
+
// Détermine la charte visuelle + directives UX adaptées aux domaines détectés.
|
|
9
|
+
// uxDirectives : fusion (dédupliquée) des uxRules. designSystem : famille du
|
|
10
|
+
// domaine principal (1er détecté) ; violations fusionnées (dédup par règle).
|
|
11
|
+
/** @param {string[]} domainKeys @returns {{ uxDirectives: string[], designSystem: { family: string, style: string, density: string, palette: string, microInteractions: string, wcag: string, violations: DesignViolation[] } }} */
|
|
12
|
+
export function analyzeUxDesign(domainKeys) {
|
|
13
|
+
const keys = Array.isArray(domainKeys) && domainKeys.length ? domainKeys : ["logiciel_generique"];
|
|
14
|
+
/** @type {string[]} */
|
|
15
|
+
const uxDirectives = [];
|
|
16
|
+
/** @type {DesignViolation[]} */
|
|
17
|
+
const violations = [];
|
|
18
|
+
const ux = getRules().ux; // lot Pro absent (compte gratuit) : famille vide, aucune directive UX.
|
|
19
|
+
const family = (/** @type {string} */ k) => ux.design[k] || ux.design.logiciel_generique || EMPTY_FAMILY;
|
|
20
|
+
const primary = family(keys[0]);
|
|
21
|
+
for (const k of keys) {
|
|
22
|
+
const d = family(k);
|
|
23
|
+
for (const r of (d.uxRules || [])) {
|
|
24
|
+
if (!uxDirectives.includes(r)) uxDirectives.push(r);
|
|
25
|
+
}
|
|
26
|
+
for (const v of (d.designViolations || [])) {
|
|
27
|
+
if (!violations.some((x) => x.rule === v.rule)) {
|
|
28
|
+
violations.push({ rule: v.rule, patterns: [...(v.patterns || [])] });
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
for (const r of ux.universal) {
|
|
33
|
+
if (!uxDirectives.includes(r)) uxDirectives.push(r);
|
|
34
|
+
}
|
|
35
|
+
const designSystem = {
|
|
36
|
+
family: primary.family, style: primary.style, density: primary.density,
|
|
37
|
+
palette: primary.palette, microInteractions: primary.microInteractions,
|
|
38
|
+
wcag: "WCAG 2.1 AA", violations: ensureDataLeakGuard(violations),
|
|
39
|
+
};
|
|
40
|
+
return { uxDirectives, designSystem };
|
|
41
|
+
}
|
|
42
|
+
// MODULE_COMPLETE
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// src/engine/ask-history.js — Mémoire de conversation du chat (ChatView) : les
|
|
2
|
+
// derniers tours user/assistant, transmis par l'extension via PLUGCTO_ASK_HISTORY
|
|
3
|
+
// (JSON), sont mis en forme pour l'unique appel LLM de `auto ask`. Donnée NON
|
|
4
|
+
// FIABLE (Zero Trust §3) : JSON illisible, rôle inconnu ou texte non-chaîne →
|
|
5
|
+
// ignoré ; volume borné (tours, taille par tour, taille totale). Jamais de
|
|
6
|
+
// fiche ni de doctrine ici : uniquement la conversation de l'utilisateur.
|
|
7
|
+
|
|
8
|
+
import { purgeReasoning } from "./history-purge.js";
|
|
9
|
+
|
|
10
|
+
const MAX_ENTRIES = 6; // contexte transmis au modèle : consigne + question + 6 derniers messages au plus
|
|
11
|
+
const ENTRY_CAP = 1500;
|
|
12
|
+
const TOTAL_CAP = 8000;
|
|
13
|
+
const LABEL = { user: "Utilisateur", assistant: "PipCode" };
|
|
14
|
+
|
|
15
|
+
/** Entrée valide (rôle connu + texte non vide) → ligne « Rôle : texte » bornée. */
|
|
16
|
+
/** @param {unknown} e @returns {string} */
|
|
17
|
+
function lineOf(e) {
|
|
18
|
+
const o = /** @type {{ r?: unknown, t?: unknown }} */ (e && typeof e === "object" ? e : {});
|
|
19
|
+
if (typeof o.r !== "string" || !Object.hasOwn(LABEL, o.r) || typeof o.t !== "string") return "";
|
|
20
|
+
const text = purgeReasoning(o.t).trim().slice(0, ENTRY_CAP); // jamais de raisonnement dans l'historique réinjecté
|
|
21
|
+
return text ? `${LABEL[/** @type {"user" | "assistant"} */ (o.r)]} : ${text}` : "";
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Historique formaté ("" si absent/illisible), les tours RÉCENTS priment sur le budget. */
|
|
25
|
+
/** @param {string | undefined} raw @returns {string} */
|
|
26
|
+
export function formatAskHistory(raw) {
|
|
27
|
+
if (!raw) return "";
|
|
28
|
+
/** @type {unknown} */
|
|
29
|
+
let parsed;
|
|
30
|
+
try { parsed = JSON.parse(raw); } catch { return ""; }
|
|
31
|
+
if (!Array.isArray(parsed)) return "";
|
|
32
|
+
const kept = [];
|
|
33
|
+
let size = 0;
|
|
34
|
+
for (const e of parsed.slice(-MAX_ENTRIES).reverse()) {
|
|
35
|
+
const line = lineOf(e);
|
|
36
|
+
if (!line) continue;
|
|
37
|
+
if (size + line.length > TOTAL_CAP) break;
|
|
38
|
+
size += line.length;
|
|
39
|
+
kept.unshift(line);
|
|
40
|
+
}
|
|
41
|
+
return kept.join("\n");
|
|
42
|
+
}
|
|
43
|
+
// MODULE_COMPLETE
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// src/engine/ask-llm.js — Conversation pure « Parlez à votre CTO » (ChatView v2).
|
|
2
|
+
// UN seul appel LLM BYOK borné : avis/conseil/réponse, SANS plan, SANS écriture
|
|
3
|
+
// disque, SANS exécution. Zéro état, zéro fichier touché (conversation seulement).
|
|
4
|
+
import { createDeltaEmitter } from "../core/progress-emit.js";
|
|
5
|
+
import { callLLM } from "../core/llm-call.js";
|
|
6
|
+
import { chatRules } from "./ask-rules.js";
|
|
7
|
+
import { withLanguage } from "../core/lang.js";
|
|
8
|
+
import { LeakGuard } from "../core/leak-guard.js";
|
|
9
|
+
import { ASK_MAX_TOKENS, ASK_SYSTEM, askUserText } from "./ask-prompt.js";
|
|
10
|
+
|
|
11
|
+
/** Erreur typée (code + message, jamais de stack exposée à l'appelant). */
|
|
12
|
+
/** @param {string} code @param {string} message @returns {EnrichedError} */
|
|
13
|
+
function askError(code, message) {
|
|
14
|
+
const err = /** @type {EnrichedError} */ (new Error(message));
|
|
15
|
+
err.code = code;
|
|
16
|
+
return err;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Appel conversationnel unique. Validation stricte des entrées (Zero Trust §3).
|
|
20
|
+
/** @param {{ primary: ProviderPrimary | null }} pcfg @param {string} question @param {string} [missionCtx] @param {string} [history] tours précédents (conversation seulement) @param {string} [projectCtx] contexte projet réel (arborescence, relations, fichiers) @param {string} [instructions] CLAUDE.md du client (prompt système, priorité haute) @param {string} [doctrine] fiches métier choisies pour cette question (prompt système, confidentielles) @returns {Promise<string>} */
|
|
21
|
+
export async function askLLM(pcfg, question, missionCtx = "", history = "", projectCtx = "", instructions = "", doctrine = "") {
|
|
22
|
+
const q = String(question || "").trim();
|
|
23
|
+
if (!q) throw askError("ask_empty", "Question vide : rien à demander au CTO.");
|
|
24
|
+
const primary = pcfg && pcfg.primary;
|
|
25
|
+
if (!primary) throw askError("no_provider", "Aucun provider BYOK configuré.");
|
|
26
|
+
const userText = askUserText({ question: q, missionCtx: String(missionCtx || ""), history: String(history || "") });
|
|
27
|
+
const model = primary.smartModel || primary.model;
|
|
28
|
+
const { frugality, advisor, project } = chatRules(primary.base); // règles du serveur ; allégées pour un modèle local
|
|
29
|
+
const guard = new LeakGuard([frugality, advisor, project, doctrine]); // règles du serveur : jamais recopiées dans la réponse (ni dans l'aperçu en direct)
|
|
30
|
+
const deltas = createDeltaEmitter(); // chat dans VS Code : la réponse s'affiche jeton par jeton (sinon null : réponse d'un bloc)
|
|
31
|
+
const live = deltas ? (/** @type {string} */ t) => { const safe = guard.push(t); if (safe) deltas.push(safe); } : undefined;
|
|
32
|
+
try {
|
|
33
|
+
const raw = await callLLM({
|
|
34
|
+
provider: primary.provider, key: primary.key, model, base: primary.base, onDelta: live,
|
|
35
|
+
systemPrompt: withLanguage([ASK_SYSTEM, instructions, frugality, advisor, project, doctrine].filter(Boolean).join("\n\n")), cachePrefix: String(projectCtx || "").trim(), userText, maxTokens: ASK_MAX_TOKENS, showThinking: true,
|
|
36
|
+
});
|
|
37
|
+
return guard.sanitize(raw);
|
|
38
|
+
} finally { const rest = guard.end(); if (rest) deltas?.push(rest); deltas?.flush(); }
|
|
39
|
+
}
|
|
40
|
+
// MODULE_COMPLETE
|