@xhulz/nina 0.33.0 → 0.35.0
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 +4 -2
- package/bin/nina.mjs +219 -48
- package/package.json +1 -1
- package/releases/0.34.0/core/GAPS.md +125 -0
- package/releases/0.34.0/core/answered.json +29 -0
- package/releases/0.34.0/core/defaults/tree/.claude/agents/architect.md +1 -0
- package/releases/0.34.0/core/defaults/tree/.claude/agents/dba.md +3 -0
- package/releases/0.34.0/core/defaults/tree/.claude/agents/devops.md +1 -0
- package/releases/0.34.0/core/defaults/tree/.claude/agents/implementer.md +1 -0
- package/releases/0.34.0/core/defaults/tree/.claude/agents/integration-tester.md +3 -0
- package/releases/0.34.0/core/defaults/tree/.claude/agents/qa.md +2 -0
- package/releases/0.34.0/core/defaults/tree/.claude/agents/reviewer.md +3 -0
- package/releases/0.34.0/core/defaults/tree/.claude/agents/secops.md +3 -0
- package/releases/0.34.0/core/defaults/tree/.claude/agents/solidity-auditor.md +3 -0
- package/releases/0.34.0/core/defaults/tree/.claude/agents/solidity-dev.md +3 -0
- package/releases/0.34.0/core/tree/.claude/agents/architect.md +127 -0
- package/releases/0.34.0/core/tree/.claude/agents/dba.md +110 -0
- package/releases/0.34.0/core/tree/.claude/agents/devops.md +114 -0
- package/releases/0.34.0/core/tree/.claude/agents/implementer.md +156 -0
- package/releases/0.34.0/core/tree/.claude/agents/integration-tester.md +168 -0
- package/releases/0.34.0/core/tree/.claude/agents/planner.md +132 -0
- package/releases/0.34.0/core/tree/.claude/agents/qa.md +148 -0
- package/releases/0.34.0/core/tree/.claude/agents/reviewer.md +183 -0
- package/releases/0.34.0/core/tree/.claude/agents/secops.md +121 -0
- package/releases/0.34.0/core/tree/.claude/agents/solidity-auditor.md +126 -0
- package/releases/0.34.0/core/tree/.claude/agents/solidity-dev.md +123 -0
- package/releases/0.34.0/core/tree/.claude/agents-overview.md +64 -0
- package/releases/0.34.0/core/tree/.claude/frontend.md +10 -0
- package/releases/0.34.0/core/tree/.claude/graph.md +75 -0
- package/releases/0.34.0/core/tree/.claude/patterns.md +213 -0
- package/releases/0.34.0/core/tree/.claude/pills/README.md +146 -0
- package/releases/0.34.0/core/tree/.claude/pipeline.md +101 -0
- package/releases/0.34.0/core/tree/.claude/retrieval.md +103 -0
- package/releases/0.34.0/core/tree/.claude/router.md +198 -0
- package/releases/0.34.0/core/tree/.claude/templates/integration.md +45 -0
- package/releases/0.34.0/core/tree/CLAUDE.md +161 -0
- package/releases/0.34.0/core/tree/scripts/cost-watch.mjs +18 -0
- package/releases/0.34.0/core/tree/scripts/edit-guard.mjs +16 -0
- package/releases/0.34.0/core/tree/scripts/harness-check.mjs +71 -0
- package/releases/0.34.0/core/tree/scripts/loop-gate.mjs +19 -0
- package/releases/0.34.0/core/vocabulary.json +13 -0
- package/releases/0.34.0/surfaces/blockchain/tree/.claude/agents/architect.md +5 -0
- package/releases/0.34.0/surfaces/blockchain/tree/.claude/agents/devops.md +5 -0
- package/releases/0.34.0/surfaces/blockchain/tree/.claude/agents/reviewer.md +5 -0
- package/releases/0.34.0/surfaces/blockchain/tree/.claude/agents/solidity-auditor.md +18 -0
- package/releases/0.34.0/surfaces/blockchain/tree/.claude/agents/solidity-dev.md +16 -0
- package/releases/0.34.0/surfaces/blockchain/tree/.claude/agents-overview.md +6 -0
- package/releases/0.34.0/surfaces/blockchain/tree/.claude/graph.md +11 -0
- package/releases/0.34.0/surfaces/blockchain/tree/.claude/router.md +6 -0
- package/releases/0.34.0/surfaces/blockchain/tree/CLAUDE.md +12 -0
- package/releases/0.34.0/surfaces/db/tree/.claude/agents/architect.md +5 -0
- package/releases/0.34.0/surfaces/db/tree/.claude/agents/dba.md +50 -0
- package/releases/0.34.0/surfaces/db/tree/.claude/agents/devops.md +5 -0
- package/releases/0.34.0/surfaces/db/tree/.claude/agents/implementer.md +2 -0
- package/releases/0.34.0/surfaces/db/tree/.claude/agents/integration-tester.md +10 -0
- package/releases/0.34.0/surfaces/db/tree/.claude/agents/planner.md +2 -0
- package/releases/0.34.0/surfaces/db/tree/.claude/agents/reviewer.md +22 -0
- package/releases/0.34.0/surfaces/db/tree/.claude/agents/secops.md +2 -0
- package/releases/0.34.0/surfaces/db/tree/.claude/agents-overview.md +5 -0
- package/releases/0.34.0/surfaces/db/tree/.claude/graph.md +8 -0
- package/releases/0.34.0/surfaces/db/tree/.claude/patterns.md +19 -0
- package/releases/0.34.0/surfaces/db/tree/.claude/pipeline.md +13 -0
- package/releases/0.34.0/surfaces/db/tree/.claude/retrieval.md +5 -0
- package/releases/0.34.0/surfaces/db/tree/.claude/router.md +4 -0
- package/releases/0.34.0/surfaces/db/tree/CLAUDE.md +17 -0
- package/releases/0.34.0/surfaces/edge-cf/tree/.claude/agents/architect.md +14 -0
- package/releases/0.34.0/surfaces/edge-cf/tree/.claude/agents/dba.md +2 -0
- package/releases/0.34.0/surfaces/edge-cf/tree/.claude/agents/devops.md +32 -0
- package/releases/0.34.0/surfaces/edge-cf/tree/.claude/agents/implementer.md +14 -0
- package/releases/0.34.0/surfaces/edge-cf/tree/.claude/agents/integration-tester.md +11 -0
- package/releases/0.34.0/surfaces/edge-cf/tree/.claude/agents/qa.md +3 -0
- package/releases/0.34.0/surfaces/edge-cf/tree/.claude/agents/reviewer.md +8 -0
- package/releases/0.34.0/surfaces/edge-cf/tree/.claude/agents/secops.md +8 -0
- package/releases/0.34.0/surfaces/edge-cf/tree/.claude/patterns.md +17 -0
- package/releases/0.34.0/surfaces/edge-cf/tree/.claude/retrieval.md +8 -0
- package/releases/0.34.0/surfaces/edge-cf/tree/CLAUDE.md +27 -0
- package/releases/0.34.0/surfaces/frontend/tree/.claude/agents/architect.md +20 -0
- package/releases/0.34.0/surfaces/frontend/tree/.claude/agents/devops.md +28 -0
- package/releases/0.34.0/surfaces/frontend/tree/.claude/agents/implementer.md +25 -0
- package/releases/0.34.0/surfaces/frontend/tree/.claude/agents/qa.md +17 -0
- package/releases/0.34.0/surfaces/frontend/tree/.claude/agents/reviewer.md +56 -0
- package/releases/0.34.0/surfaces/frontend/tree/.claude/patterns.md +8 -0
- package/releases/0.34.0/surfaces/frontend/tree/.claude/retrieval.md +2 -0
- package/releases/0.34.0/surfaces/frontend/tree/.claude/router.md +14 -0
- package/releases/0.34.0/surfaces/frontend/tree/CLAUDE.md +5 -0
- package/releases/0.34.0/surfaces/integrations/tree/.claude/agents/architect.md +16 -0
- package/releases/0.34.0/surfaces/integrations/tree/.claude/agents/implementer.md +12 -0
- package/releases/0.34.0/surfaces/integrations/tree/.claude/agents/integration-tester.md +20 -0
- package/releases/0.34.0/surfaces/integrations/tree/.claude/agents/planner.md +2 -0
- package/releases/0.34.0/surfaces/integrations/tree/.claude/agents/reviewer.md +21 -0
- package/releases/0.34.0/surfaces/integrations/tree/.claude/agents/secops.md +2 -0
- package/releases/0.34.0/surfaces/integrations/tree/.claude/agents-overview.md +8 -0
- package/releases/0.34.0/surfaces/integrations/tree/.claude/graph.md +10 -0
- package/releases/0.34.0/surfaces/integrations/tree/.claude/patterns.md +38 -0
- package/releases/0.34.0/surfaces/integrations/tree/.claude/pipeline.md +16 -0
- package/releases/0.34.0/surfaces/integrations/tree/.claude/retrieval.md +16 -0
- package/releases/0.34.0/surfaces/integrations/tree/.claude/router.md +6 -0
- package/releases/0.34.0/surfaces/integrations/tree/CLAUDE.md +17 -0
- package/releases/0.34.0/surfaces/money/tree/.claude/agents/architect.md +12 -0
- package/releases/0.34.0/surfaces/money/tree/.claude/agents/dba.md +14 -0
- package/releases/0.34.0/surfaces/money/tree/.claude/agents/implementer.md +11 -0
- package/releases/0.34.0/surfaces/money/tree/.claude/agents/integration-tester.md +3 -0
- package/releases/0.34.0/surfaces/money/tree/.claude/agents/reviewer.md +23 -0
- package/releases/0.34.0/surfaces/money/tree/.claude/agents/secops.md +5 -0
- package/releases/0.34.0/surfaces/money/tree/.claude/agents/solidity-auditor.md +4 -0
- package/releases/0.34.0/surfaces/money/tree/.claude/agents/solidity-dev.md +4 -0
- package/releases/0.34.0/surfaces/money/tree/.claude/graph.md +2 -0
- package/releases/0.34.0/surfaces/money/tree/.claude/patterns.md +49 -0
- package/releases/0.34.0/surfaces/money/tree/.claude/pipeline.md +2 -0
- package/releases/0.34.0/surfaces/money/tree/.claude/retrieval.md +2 -0
- package/releases/0.34.0/surfaces/money/tree/CLAUDE.md +11 -0
- package/releases/0.34.0/surfaces/pii/tree/.claude/agents/dba.md +2 -0
- package/releases/0.34.0/surfaces/pii/tree/.claude/agents/devops.md +8 -0
- package/releases/0.34.0/surfaces/pii/tree/.claude/agents/reviewer.md +8 -0
- package/releases/0.34.0/surfaces/pii/tree/.claude/agents/secops.md +2 -0
- package/releases/0.34.0/surfaces/pii/tree/.claude/agents/solidity-auditor.md +5 -0
- package/releases/0.34.0/surfaces/pii/tree/CLAUDE.md +5 -0
- package/releases/0.34.0/surfaces/prisma/tree/.claude/agents/architect.md +2 -0
- package/releases/0.34.0/surfaces/prisma/tree/.claude/agents/dba.md +39 -0
- package/releases/0.34.0/surfaces/prisma/tree/.claude/agents/devops.md +11 -0
- package/releases/0.34.0/surfaces/prisma/tree/.claude/agents/implementer.md +2 -0
- package/releases/0.34.0/surfaces/prisma/tree/.claude/agents/integration-tester.md +2 -0
- package/releases/0.34.0/surfaces/prisma/tree/.claude/agents/reviewer.md +2 -0
- package/releases/0.34.0/surfaces/prisma/tree/.claude/patterns.md +15 -0
- package/releases/0.34.0/surfaces/prisma/tree/CLAUDE.md +5 -0
- package/releases/0.35.0/core/GAPS.md +125 -0
- package/releases/0.35.0/core/answered.json +29 -0
- package/releases/0.35.0/core/defaults/tree/.claude/agents/architect.md +1 -0
- package/releases/0.35.0/core/defaults/tree/.claude/agents/dba.md +3 -0
- package/releases/0.35.0/core/defaults/tree/.claude/agents/devops.md +1 -0
- package/releases/0.35.0/core/defaults/tree/.claude/agents/implementer.md +1 -0
- package/releases/0.35.0/core/defaults/tree/.claude/agents/integration-tester.md +3 -0
- package/releases/0.35.0/core/defaults/tree/.claude/agents/qa.md +2 -0
- package/releases/0.35.0/core/defaults/tree/.claude/agents/reviewer.md +3 -0
- package/releases/0.35.0/core/defaults/tree/.claude/agents/secops.md +3 -0
- package/releases/0.35.0/core/defaults/tree/.claude/agents/solidity-auditor.md +3 -0
- package/releases/0.35.0/core/defaults/tree/.claude/agents/solidity-dev.md +3 -0
- package/releases/0.35.0/core/tree/.claude/agents/architect.md +127 -0
- package/releases/0.35.0/core/tree/.claude/agents/dba.md +110 -0
- package/releases/0.35.0/core/tree/.claude/agents/devops.md +114 -0
- package/releases/0.35.0/core/tree/.claude/agents/implementer.md +156 -0
- package/releases/0.35.0/core/tree/.claude/agents/integration-tester.md +168 -0
- package/releases/0.35.0/core/tree/.claude/agents/planner.md +132 -0
- package/releases/0.35.0/core/tree/.claude/agents/qa.md +153 -0
- package/releases/0.35.0/core/tree/.claude/agents/reviewer.md +183 -0
- package/releases/0.35.0/core/tree/.claude/agents/secops.md +121 -0
- package/releases/0.35.0/core/tree/.claude/agents/solidity-auditor.md +126 -0
- package/releases/0.35.0/core/tree/.claude/agents/solidity-dev.md +123 -0
- package/releases/0.35.0/core/tree/.claude/agents-overview.md +64 -0
- package/releases/0.35.0/core/tree/.claude/frontend.md +10 -0
- package/releases/0.35.0/core/tree/.claude/graph.md +75 -0
- package/releases/0.35.0/core/tree/.claude/patterns.md +213 -0
- package/releases/0.35.0/core/tree/.claude/pills/README.md +146 -0
- package/releases/0.35.0/core/tree/.claude/pipeline.md +101 -0
- package/releases/0.35.0/core/tree/.claude/retrieval.md +103 -0
- package/releases/0.35.0/core/tree/.claude/router.md +198 -0
- package/releases/0.35.0/core/tree/.claude/templates/integration.md +45 -0
- package/releases/0.35.0/core/tree/CLAUDE.md +161 -0
- package/releases/0.35.0/core/tree/scripts/cost-watch.mjs +18 -0
- package/releases/0.35.0/core/tree/scripts/edit-guard.mjs +16 -0
- package/releases/0.35.0/core/tree/scripts/harness-check.mjs +71 -0
- package/releases/0.35.0/core/tree/scripts/loop-gate.mjs +19 -0
- package/releases/0.35.0/core/vocabulary.json +13 -0
- package/releases/0.35.0/surfaces/blockchain/tree/.claude/agents/architect.md +5 -0
- package/releases/0.35.0/surfaces/blockchain/tree/.claude/agents/devops.md +5 -0
- package/releases/0.35.0/surfaces/blockchain/tree/.claude/agents/reviewer.md +5 -0
- package/releases/0.35.0/surfaces/blockchain/tree/.claude/agents/solidity-auditor.md +18 -0
- package/releases/0.35.0/surfaces/blockchain/tree/.claude/agents/solidity-dev.md +16 -0
- package/releases/0.35.0/surfaces/blockchain/tree/.claude/agents-overview.md +6 -0
- package/releases/0.35.0/surfaces/blockchain/tree/.claude/graph.md +11 -0
- package/releases/0.35.0/surfaces/blockchain/tree/.claude/router.md +6 -0
- package/releases/0.35.0/surfaces/blockchain/tree/CLAUDE.md +12 -0
- package/releases/0.35.0/surfaces/db/tree/.claude/agents/architect.md +5 -0
- package/releases/0.35.0/surfaces/db/tree/.claude/agents/dba.md +50 -0
- package/releases/0.35.0/surfaces/db/tree/.claude/agents/devops.md +5 -0
- package/releases/0.35.0/surfaces/db/tree/.claude/agents/implementer.md +2 -0
- package/releases/0.35.0/surfaces/db/tree/.claude/agents/integration-tester.md +10 -0
- package/releases/0.35.0/surfaces/db/tree/.claude/agents/planner.md +2 -0
- package/releases/0.35.0/surfaces/db/tree/.claude/agents/reviewer.md +22 -0
- package/releases/0.35.0/surfaces/db/tree/.claude/agents/secops.md +2 -0
- package/releases/0.35.0/surfaces/db/tree/.claude/agents-overview.md +5 -0
- package/releases/0.35.0/surfaces/db/tree/.claude/graph.md +8 -0
- package/releases/0.35.0/surfaces/db/tree/.claude/patterns.md +19 -0
- package/releases/0.35.0/surfaces/db/tree/.claude/pipeline.md +13 -0
- package/releases/0.35.0/surfaces/db/tree/.claude/retrieval.md +5 -0
- package/releases/0.35.0/surfaces/db/tree/.claude/router.md +4 -0
- package/releases/0.35.0/surfaces/db/tree/CLAUDE.md +17 -0
- package/releases/0.35.0/surfaces/edge-cf/tree/.claude/agents/architect.md +14 -0
- package/releases/0.35.0/surfaces/edge-cf/tree/.claude/agents/dba.md +2 -0
- package/releases/0.35.0/surfaces/edge-cf/tree/.claude/agents/devops.md +32 -0
- package/releases/0.35.0/surfaces/edge-cf/tree/.claude/agents/implementer.md +14 -0
- package/releases/0.35.0/surfaces/edge-cf/tree/.claude/agents/integration-tester.md +11 -0
- package/releases/0.35.0/surfaces/edge-cf/tree/.claude/agents/qa.md +3 -0
- package/releases/0.35.0/surfaces/edge-cf/tree/.claude/agents/reviewer.md +8 -0
- package/releases/0.35.0/surfaces/edge-cf/tree/.claude/agents/secops.md +8 -0
- package/releases/0.35.0/surfaces/edge-cf/tree/.claude/patterns.md +17 -0
- package/releases/0.35.0/surfaces/edge-cf/tree/.claude/retrieval.md +8 -0
- package/releases/0.35.0/surfaces/edge-cf/tree/CLAUDE.md +27 -0
- package/releases/0.35.0/surfaces/frontend/tree/.claude/agents/architect.md +20 -0
- package/releases/0.35.0/surfaces/frontend/tree/.claude/agents/devops.md +28 -0
- package/releases/0.35.0/surfaces/frontend/tree/.claude/agents/implementer.md +25 -0
- package/releases/0.35.0/surfaces/frontend/tree/.claude/agents/qa.md +17 -0
- package/releases/0.35.0/surfaces/frontend/tree/.claude/agents/reviewer.md +56 -0
- package/releases/0.35.0/surfaces/frontend/tree/.claude/patterns.md +8 -0
- package/releases/0.35.0/surfaces/frontend/tree/.claude/retrieval.md +2 -0
- package/releases/0.35.0/surfaces/frontend/tree/.claude/router.md +14 -0
- package/releases/0.35.0/surfaces/frontend/tree/CLAUDE.md +5 -0
- package/releases/0.35.0/surfaces/integrations/tree/.claude/agents/architect.md +16 -0
- package/releases/0.35.0/surfaces/integrations/tree/.claude/agents/implementer.md +12 -0
- package/releases/0.35.0/surfaces/integrations/tree/.claude/agents/integration-tester.md +20 -0
- package/releases/0.35.0/surfaces/integrations/tree/.claude/agents/planner.md +2 -0
- package/releases/0.35.0/surfaces/integrations/tree/.claude/agents/reviewer.md +21 -0
- package/releases/0.35.0/surfaces/integrations/tree/.claude/agents/secops.md +2 -0
- package/releases/0.35.0/surfaces/integrations/tree/.claude/agents-overview.md +8 -0
- package/releases/0.35.0/surfaces/integrations/tree/.claude/graph.md +10 -0
- package/releases/0.35.0/surfaces/integrations/tree/.claude/patterns.md +38 -0
- package/releases/0.35.0/surfaces/integrations/tree/.claude/pipeline.md +16 -0
- package/releases/0.35.0/surfaces/integrations/tree/.claude/retrieval.md +16 -0
- package/releases/0.35.0/surfaces/integrations/tree/.claude/router.md +6 -0
- package/releases/0.35.0/surfaces/integrations/tree/CLAUDE.md +17 -0
- package/releases/0.35.0/surfaces/money/tree/.claude/agents/architect.md +12 -0
- package/releases/0.35.0/surfaces/money/tree/.claude/agents/dba.md +14 -0
- package/releases/0.35.0/surfaces/money/tree/.claude/agents/implementer.md +11 -0
- package/releases/0.35.0/surfaces/money/tree/.claude/agents/integration-tester.md +3 -0
- package/releases/0.35.0/surfaces/money/tree/.claude/agents/reviewer.md +23 -0
- package/releases/0.35.0/surfaces/money/tree/.claude/agents/secops.md +5 -0
- package/releases/0.35.0/surfaces/money/tree/.claude/agents/solidity-auditor.md +4 -0
- package/releases/0.35.0/surfaces/money/tree/.claude/agents/solidity-dev.md +4 -0
- package/releases/0.35.0/surfaces/money/tree/.claude/graph.md +2 -0
- package/releases/0.35.0/surfaces/money/tree/.claude/patterns.md +49 -0
- package/releases/0.35.0/surfaces/money/tree/.claude/pipeline.md +2 -0
- package/releases/0.35.0/surfaces/money/tree/.claude/retrieval.md +2 -0
- package/releases/0.35.0/surfaces/money/tree/CLAUDE.md +11 -0
- package/releases/0.35.0/surfaces/pii/tree/.claude/agents/dba.md +2 -0
- package/releases/0.35.0/surfaces/pii/tree/.claude/agents/devops.md +8 -0
- package/releases/0.35.0/surfaces/pii/tree/.claude/agents/reviewer.md +8 -0
- package/releases/0.35.0/surfaces/pii/tree/.claude/agents/secops.md +2 -0
- package/releases/0.35.0/surfaces/pii/tree/.claude/agents/solidity-auditor.md +5 -0
- package/releases/0.35.0/surfaces/pii/tree/CLAUDE.md +5 -0
- package/releases/0.35.0/surfaces/prisma/tree/.claude/agents/architect.md +2 -0
- package/releases/0.35.0/surfaces/prisma/tree/.claude/agents/dba.md +39 -0
- package/releases/0.35.0/surfaces/prisma/tree/.claude/agents/devops.md +11 -0
- package/releases/0.35.0/surfaces/prisma/tree/.claude/agents/implementer.md +2 -0
- package/releases/0.35.0/surfaces/prisma/tree/.claude/agents/integration-tester.md +2 -0
- package/releases/0.35.0/surfaces/prisma/tree/.claude/agents/reviewer.md +2 -0
- package/releases/0.35.0/surfaces/prisma/tree/.claude/patterns.md +15 -0
- package/releases/0.35.0/surfaces/prisma/tree/CLAUDE.md +5 -0
- package/src/agentrun.mjs +91 -12
- package/src/commands/check.mjs +12 -8
- package/src/commands/compose.mjs +85 -9
- package/src/commands/export.mjs +9 -7
- package/src/commands/init.mjs +13 -4
- package/src/commands/langfuse.mjs +25 -10
- package/src/commands/learn.mjs +12 -3
- package/src/commands/release.mjs +21 -3
- package/src/commands/stats.mjs +64 -12
- package/src/commands/upgrade.mjs +131 -25
- package/src/guard.mjs +7 -37
- package/src/langfuse.mjs +31 -11
- package/src/paths.mjs +50 -1
- package/src/prices.mjs +7 -7
- package/src/transcripts.mjs +27 -1
- package/src/vocabulary.mjs +12 -1
- package/src/wiring.mjs +106 -41
package/src/agentrun.mjs
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
import { createReadStream, existsSync } from 'node:fs';
|
|
14
14
|
import { join } from 'node:path';
|
|
15
15
|
import { createInterface } from 'node:readline';
|
|
16
|
-
import { roundsOf, tokensOf } from './transcripts.mjs';
|
|
16
|
+
import { promptOf, roundsOf, tokensOf } from './transcripts.mjs';
|
|
17
17
|
|
|
18
18
|
/** How much of a prompt, a report or a message's text is sent, and of a tool call's input or result. */
|
|
19
19
|
export const TEXT_CHARS = 20_000;
|
|
@@ -30,18 +30,74 @@ export function clip(text, chars) {
|
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
/** Names whose value is a credential when they are assigned one. */
|
|
33
|
-
const SECRET_NAME = /(?:secret|token|passw(?:or)?d|
|
|
33
|
+
const SECRET_NAME = /(?:secret|token(?![a-z])|passw(?:or)?d|(?:^|[_-])pass$|api[_-]?key|private[_-]?key|access[_-]?key|signing[_-]?key|credential|dsn$)/i;
|
|
34
|
+
|
|
35
|
+
/** Names that say where a secret is, or what kind, rather than holding one: `--password-file`, `TOKEN_TYPE`. */
|
|
36
|
+
const ABOUT_A_SECRET = /[_-](?:file|path|dir|type|count|length|size|limit|url|endpoint)$/i;
|
|
34
37
|
|
|
35
38
|
/** Token formats that are credentials on sight. */
|
|
36
|
-
const SECRET_TOKEN =
|
|
39
|
+
const SECRET_TOKEN = new RegExp(
|
|
40
|
+
[
|
|
41
|
+
'sk-(?:ant-|lf-|proj-)?[A-Za-z0-9_-]{16,}',
|
|
42
|
+
'gh[pousr]_[A-Za-z0-9]{30,}',
|
|
43
|
+
'github_pat_[A-Za-z0-9_]{30,}',
|
|
44
|
+
'glpat-[A-Za-z0-9_-]{20,}',
|
|
45
|
+
'xox[abprs]-[A-Za-z0-9-]{10,}',
|
|
46
|
+
'(?:AKIA|ASIA)[0-9A-Z]{16}',
|
|
47
|
+
'AIza[0-9A-Za-z_-]{35}',
|
|
48
|
+
'(?:sk|rk)_(?:live|test)_[A-Za-z0-9]{16,}',
|
|
49
|
+
'whsec_[A-Za-z0-9+/=]{16,}',
|
|
50
|
+
'npm_[A-Za-z0-9]{30,}',
|
|
51
|
+
'hf_[A-Za-z0-9]{30,}',
|
|
52
|
+
'SG\\.[A-Za-z0-9_-]{16,}\\.[A-Za-z0-9_-]{16,}',
|
|
53
|
+
'eyJ[A-Za-z0-9_-]{8,}\\.eyJ[A-Za-z0-9_-]{8,}\\.[A-Za-z0-9_-]{8,}',
|
|
54
|
+
]
|
|
55
|
+
.map((p) => `\\b${p}`)
|
|
56
|
+
.join('|'),
|
|
57
|
+
'g',
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
/** `scheme://user:password@host` — the password, however the URL is named. */
|
|
61
|
+
const URL_CREDENTIAL = /\b([a-z][a-z0-9+.-]{1,20}:\/\/[^\s:/@]{0,64}):[^\s@/]{1,256}@/gi;
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* `NAME=value`, `NAME: value`, `"name": "value"` — the name and what joins it to a value of six characters or
|
|
65
|
+
* more, and not the value: {@link redact} takes that only for a secret's name. Every part is bounded, so no
|
|
66
|
+
* position costs more than a few dozen steps, whatever follows it.
|
|
67
|
+
*/
|
|
68
|
+
const ASSIGNMENT = /\b([A-Za-z0-9_-]{0,40})(["']?\s{0,3}[:=]\s{0,3}["']?)(?=[^\s"'`,;]{6})/g;
|
|
69
|
+
|
|
70
|
+
/** The value that follows an assignment, read from where {@link ASSIGNMENT} stopped. */
|
|
71
|
+
const VALUE = /[^\s"'`,;]+/y;
|
|
37
72
|
|
|
38
|
-
/**
|
|
39
|
-
const
|
|
73
|
+
/** The same with the value in quotes, which may hold spaces: `password: "correct horse battery"`. */
|
|
74
|
+
const QUOTED = /\b([A-Za-z0-9_-]{1,40})(["']?\s{0,3}[:=]\s{0,3})(["'])([^"'\n]{1,256})\3/g;
|
|
75
|
+
|
|
76
|
+
/** A command-line option with its value after a space — `--password hunter2`, `--api-key "a b"`. */
|
|
77
|
+
const FLAG = /(--?[A-Za-z0-9][A-Za-z0-9_-]{0,40})\s{1,3}(?:(["'])([^"'\n]{1,256})\2|([^\s"'`]{3,}))/g;
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* curl's and friends' `-u user:password`, quoted or not. The user has to look like one — `date -u +%H:%M`
|
|
81
|
+
* is a format — and a password of digits alone is a group: `docker run -u 1000:1000`.
|
|
82
|
+
*/
|
|
83
|
+
const USER_FLAG = /((?:^|\s)(?:-u|--user)\s{1,3}[A-Za-z0-9_][A-Za-z0-9._@-]{0,63}):([^\s"']{1,256})/g;
|
|
84
|
+
|
|
85
|
+
/** The same in quotes, where the password may hold spaces. */
|
|
86
|
+
const USER_QUOTED = /((?:^|\s)(?:-u|--user)\s{1,3}(["'])[A-Za-z0-9_][A-Za-z0-9._@-]{0,63}):[^"'\n]{1,256}\2/g;
|
|
87
|
+
|
|
88
|
+
/** A cookie header's whole value: every cookie in it may be a session. */
|
|
89
|
+
const COOKIE = /\b((?:Set-)?Cookie["']?\s{0,3}:\s{0,3})[^\n"]{1,2000}/gi;
|
|
90
|
+
|
|
91
|
+
/** An `Authorization` value under any scheme, or none — `ApiKey …` and a bare token as much as `Bearer …`. */
|
|
92
|
+
const AUTHORIZATION = /\b((?:Proxy-)?Authorization["']?\s{0,3}[:=]\s{0,3}["']?)(?:([A-Za-z][A-Za-z0-9_-]{0,20})\s+)?[^\s"',;]{8,}/gi;
|
|
93
|
+
|
|
94
|
+
/** Whether a name and the value assigned to it make a credential. A count is not one: `max_tokens: 100000`. */
|
|
95
|
+
const secretPair = (name, value) => SECRET_NAME.test(name) && !ABOUT_A_SECRET.test(name) && !/^\d+$/.test(value) && value !== '[redacted]';
|
|
40
96
|
|
|
41
97
|
/**
|
|
42
98
|
* Text with the secrets it obviously carries masked: private keys, the token formats of the common
|
|
43
|
-
* providers, an `Authorization` value, and anything assigned to a name that says it
|
|
44
|
-
* of digits only is left: `max_tokens: 100000` is not a credential.
|
|
99
|
+
* providers, the password in a URL, an `Authorization` value, and anything assigned to a name that says it
|
|
100
|
+
* is a secret. A value of digits only is left: `max_tokens: 100000` is not a credential.
|
|
45
101
|
*
|
|
46
102
|
* @param {string} text
|
|
47
103
|
* @returns {string}
|
|
@@ -57,10 +113,34 @@ export function redact(text) {
|
|
|
57
113
|
out = `${out.slice(0, start)}[redacted private key]${close === -1 ? '' : out.slice(close + 5)}`;
|
|
58
114
|
}
|
|
59
115
|
out = out.replace(SECRET_TOKEN, '[redacted]');
|
|
60
|
-
out = out.replace(
|
|
61
|
-
|
|
62
|
-
|
|
116
|
+
out = out.replace(URL_CREDENTIAL, '$1:[redacted]@');
|
|
117
|
+
out = out.replace(/\b(Bearer|Basic|Token|Bot)\s+[A-Za-z0-9._~+/=-]{16,}/g, '$1 [redacted]');
|
|
118
|
+
out = out.replace(AUTHORIZATION, (whole, head, scheme) => `${head}${scheme ? `${scheme} ` : ''}[redacted]`);
|
|
119
|
+
out = out.replace(COOKIE, '$1[redacted]');
|
|
120
|
+
out = out.replace(USER_QUOTED, (whole, head, quote) => `${head}:[redacted]${quote}`);
|
|
121
|
+
out = out.replace(USER_FLAG, (whole, head, password) => (/^\d+$/.test(password) || password === '[redacted]' ? whole : `${head}:[redacted]`));
|
|
122
|
+
// The shell is where most of a stage's input is written, and it passes a secret after a space.
|
|
123
|
+
out = out.replace(FLAG, (whole, flag, quote, quoted, bare) =>
|
|
124
|
+
secretPair(flag.replace(/^-+/, ''), quoted ?? bare) ? `${flag} ${quote ?? ''}[redacted]${quote ?? ''}` : whole,
|
|
63
125
|
);
|
|
126
|
+
out = out.replace(QUOTED, (whole, name, sep, quote, value) => (secretPair(name, value) ? `${name}${sep}${quote}[redacted]${quote}` : whole));
|
|
127
|
+
// By hand rather than by `replace`: an assignment to a name that is not a secret's leaves its value to be
|
|
128
|
+
// read, so one inside it still is. Replaced whole, `{"content":"API_KEY=…"}` took the key's value with it.
|
|
129
|
+
// The value is taken once, and only for a secret's name, which keeps the whole pass linear.
|
|
130
|
+
let masked = '';
|
|
131
|
+
let last = 0;
|
|
132
|
+
ASSIGNMENT.lastIndex = 0;
|
|
133
|
+
for (let m = ASSIGNMENT.exec(out); m !== null; m = ASSIGNMENT.exec(out)) {
|
|
134
|
+
const [head, name, sep] = m;
|
|
135
|
+
if (!SECRET_NAME.test(name)) continue;
|
|
136
|
+
VALUE.lastIndex = m.index + head.length;
|
|
137
|
+
const value = VALUE.exec(out)?.[0] ?? '';
|
|
138
|
+
if (!secretPair(name, value)) continue;
|
|
139
|
+
masked += `${out.slice(last, m.index)}${name}${sep}[redacted]`;
|
|
140
|
+
last = VALUE.lastIndex;
|
|
141
|
+
ASSIGNMENT.lastIndex = last;
|
|
142
|
+
}
|
|
143
|
+
return masked + out.slice(last);
|
|
64
144
|
}
|
|
65
145
|
|
|
66
146
|
/** A tool result's text: a string, or the text blocks of a list, with images named rather than sent. */
|
|
@@ -103,8 +183,7 @@ export async function readRun(file, round = 1) {
|
|
|
103
183
|
}
|
|
104
184
|
const at = row.timestamp ?? null;
|
|
105
185
|
const content = row.message?.content;
|
|
106
|
-
|
|
107
|
-
if (row.type === 'user' && (round > 1 || !row.isMeta) && typeof content === 'string' && prompt === null) prompt = content;
|
|
186
|
+
if (prompt === null) prompt = promptOf(row, round);
|
|
108
187
|
if (row.type === 'user' && Array.isArray(content)) {
|
|
109
188
|
for (const block of content) {
|
|
110
189
|
const call = block?.type === 'tool_result' ? tools.get(block.tool_use_id) : null;
|
package/src/commands/check.mjs
CHANGED
|
@@ -9,15 +9,15 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import { readFile, readdir } from 'node:fs/promises';
|
|
12
|
-
import { HARNESS, legacyHint } from '../paths.mjs';
|
|
12
|
+
import { HARNESS, describeMove, interruptedMove, legacyHint } from '../paths.mjs';
|
|
13
13
|
import { expectedUnfilled } from '../expected.mjs';
|
|
14
14
|
import { parseGraph, validateGraph } from '../graph.mjs';
|
|
15
15
|
import { frontmatterFindings, installedSkills, modelFindings, toolFindings } from '../tools.mjs';
|
|
16
16
|
import { HOOK_SCRIPTS, missingWiring, shippedScripts } from '../wiring.mjs';
|
|
17
17
|
import { existsSync } from 'node:fs';
|
|
18
18
|
import { join, resolve, sep } from 'node:path';
|
|
19
|
-
import { REQUIRES, SLOT, defaultedSlots, layerRootFor, stripWhy, walk } from './compose.mjs';
|
|
20
|
-
import { defaultVocabulary } from '../vocabulary.mjs';
|
|
19
|
+
import { REQUIRES, SLOT, defaultedSlots, layerRootFor, noticeOf, readLayerText, stripWhy, walk } from './compose.mjs';
|
|
20
|
+
import { PLACEHOLDER, defaultVocabulary } from '../vocabulary.mjs';
|
|
21
21
|
import { NEEDS } from '../surfaces.mjs';
|
|
22
22
|
|
|
23
23
|
/** Fields every declared integration must carry, and why each one matters. */
|
|
@@ -56,13 +56,13 @@ export async function referencedVocabulary(layerRoot, surfaces, target) {
|
|
|
56
56
|
const t = await readFile(join(layerRoot, 'surfaces', s, 'tree', rel), 'utf8').catch(() => null);
|
|
57
57
|
if (t !== null) texts.push(t);
|
|
58
58
|
}
|
|
59
|
-
for (const [, name] of stripWhy(texts.join('\n')).matchAll(
|
|
59
|
+
for (const [, name] of stripWhy(texts.join('\n')).matchAll(PLACEHOLDER)) found.add(name);
|
|
60
60
|
}
|
|
61
61
|
if (target) {
|
|
62
62
|
const projectTree = join(target, HARNESS, 'project', 'tree');
|
|
63
63
|
for (const rel of await walk(projectTree)) {
|
|
64
|
-
const text = stripWhy(await
|
|
65
|
-
for (const [, name] of text.matchAll(
|
|
64
|
+
const text = stripWhy((await readLayerText(join(projectTree, rel))) ?? '');
|
|
65
|
+
for (const [, name] of text.matchAll(PLACEHOLDER)) found.add(name);
|
|
66
66
|
}
|
|
67
67
|
}
|
|
68
68
|
return found;
|
|
@@ -100,7 +100,7 @@ export async function filledSlots(target) {
|
|
|
100
100
|
const tree = join(target, HARNESS, 'project', 'tree');
|
|
101
101
|
const filled = new Set();
|
|
102
102
|
for (const rel of await walk(tree)) {
|
|
103
|
-
for (const line of (await
|
|
103
|
+
for (const line of ((await readLayerText(join(tree, rel))) ?? '').split('\n')) {
|
|
104
104
|
const marker = SLOT.exec(line);
|
|
105
105
|
if (marker) filled.add(`${rel} ${marker[1]}`);
|
|
106
106
|
}
|
|
@@ -230,6 +230,10 @@ export async function check(argv, ctx) {
|
|
|
230
230
|
return 1;
|
|
231
231
|
}
|
|
232
232
|
|
|
233
|
+
// The move measures this check itself while its journal is written, and that journal is not a problem then.
|
|
234
|
+
const stopped = process.env.NINA_UPGRADE ? null : interruptedMove(target);
|
|
235
|
+
if (stopped) problems.push(`${describeMove(stopped)} \`nina upgrade --abort\` undoes it`);
|
|
236
|
+
|
|
233
237
|
const surfaces = profile.surfaces ?? [];
|
|
234
238
|
const available = (await readdir(join(resolved.dir, 'surfaces'), { withFileTypes: true }))
|
|
235
239
|
.filter((e) => e.isDirectory())
|
|
@@ -350,7 +354,7 @@ export async function check(argv, ctx) {
|
|
|
350
354
|
// them; they are outside the pipeline graph, not a stage the graph forgot.
|
|
351
355
|
for (const file of (await readdir(agentsDir).catch(() => [])).filter((f) => f.endsWith('.md'))) {
|
|
352
356
|
const text = await readFile(join(agentsDir, file), 'utf8');
|
|
353
|
-
if (text
|
|
357
|
+
if (noticeOf(text) !== null) specs.set(file.replace(/\.md$/, ''), text);
|
|
354
358
|
}
|
|
355
359
|
// A core with a graph and no agent specs has no roles to name — an empty set, not a crash that
|
|
356
360
|
// `upgrade` would read as a new failure and roll a move back for.
|
package/src/commands/compose.mjs
CHANGED
|
@@ -16,12 +16,12 @@
|
|
|
16
16
|
* sit unenforceable for three months here.
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
|
-
import { mkdir, readFile, readdir, writeFile } from 'node:fs/promises';
|
|
19
|
+
import { mkdir, readFile, readdir, rm, writeFile } from 'node:fs/promises';
|
|
20
20
|
import { HARNESS, legacyHint } from '../paths.mjs';
|
|
21
21
|
import { expectedUnfilled } from '../expected.mjs';
|
|
22
22
|
import { existsSync, lstatSync, readFileSync } from 'node:fs';
|
|
23
23
|
import { dirname, join, relative, resolve } from 'node:path';
|
|
24
|
-
import { defaultVocabulary } from '../vocabulary.mjs';
|
|
24
|
+
import { NAME, defaultVocabulary } from '../vocabulary.mjs';
|
|
25
25
|
|
|
26
26
|
/**
|
|
27
27
|
* Matches a slot marker on its own line, with an optional label.
|
|
@@ -139,6 +139,48 @@ export function generatedNotice(rel, slots, script = false) {
|
|
|
139
139
|
return script ? lines.map((line) => `// ${line}`).join('\n') : `<!-- ${lines.join('\n ')} -->`;
|
|
140
140
|
}
|
|
141
141
|
|
|
142
|
+
/** Where compose keeps a copy of each file it stops composing when a surface leaves the profile. */
|
|
143
|
+
export const REMOVED = join(HARNESS, 'removed');
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* How far into a file the notice is looked for: below a frontmatter block, or a shebang. The compose
|
|
147
|
+
* suite holds every composed file's notice inside it.
|
|
148
|
+
*/
|
|
149
|
+
export const NOTICE_HEAD = 60;
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* A composed file's notice, as one sentence without its comment syntax, or null when it has none.
|
|
153
|
+
*
|
|
154
|
+
* This is the one test of whether a file was composed. A file that only mentions the notice — a CLAUDE.md
|
|
155
|
+
* written by hand that explains the harness, say — is not composed: read as "contains the words", compose
|
|
156
|
+
* took such a file for its own and wrote over it with no copy kept.
|
|
157
|
+
*
|
|
158
|
+
* @param {string} text - The file.
|
|
159
|
+
* @returns {string|null}
|
|
160
|
+
*/
|
|
161
|
+
export function noticeOf(text) {
|
|
162
|
+
const lines = String(text).split('\n').slice(0, NOTICE_HEAD);
|
|
163
|
+
const at = lines.findIndex((l) => /^(<!--|\/\/) nina:generated/.test(l));
|
|
164
|
+
if (at < 0) return null;
|
|
165
|
+
const block = [];
|
|
166
|
+
if (lines[at].startsWith('//')) {
|
|
167
|
+
for (const line of lines.slice(at)) {
|
|
168
|
+
if (!line.startsWith('//')) break;
|
|
169
|
+
block.push(line.replace(/^\/\/ ?/, ''));
|
|
170
|
+
}
|
|
171
|
+
} else {
|
|
172
|
+
// The notice's own delimiters, taken off by position: this reads text the compiler wrote, it filters
|
|
173
|
+
// nothing, and a pattern for it read to a scanner as an HTML sanitizer that misses `--!>`.
|
|
174
|
+
for (const line of lines.slice(at)) {
|
|
175
|
+
const open = line.startsWith('<!-- ') ? line.slice('<!-- '.length) : line;
|
|
176
|
+
const end = open.lastIndexOf('-->');
|
|
177
|
+
block.push(end >= 0 ? open.slice(0, end) : open);
|
|
178
|
+
if (end >= 0) break;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
return block.map((l) => l.trim()).join(' ');
|
|
182
|
+
}
|
|
183
|
+
|
|
142
184
|
/**
|
|
143
185
|
* Puts the notice at the top of a composed file, below its frontmatter where it has any.
|
|
144
186
|
*
|
|
@@ -241,6 +283,20 @@ export async function walk(dir) {
|
|
|
241
283
|
return out;
|
|
242
284
|
}
|
|
243
285
|
|
|
286
|
+
/**
|
|
287
|
+
* A layer file's text, read the way every reader of the layers must read it: with a byte-order mark taken
|
|
288
|
+
* off and Windows line ends made `\n`. A fragment saved with CRLF matched no slot marker — the marker's
|
|
289
|
+
* pattern ends at `-->` and the line ended in a `\r` — so the whole file composed to nothing, and the only
|
|
290
|
+
* sign was the count of unfilled slots. A BOM did the same to the first slot.
|
|
291
|
+
*
|
|
292
|
+
* @param {string} path - A file in a layer's tree.
|
|
293
|
+
* @returns {Promise<string|null>} Null when there is no such file.
|
|
294
|
+
*/
|
|
295
|
+
export async function readLayerText(path) {
|
|
296
|
+
const text = await readFile(path, 'utf8').catch(() => null);
|
|
297
|
+
return text === null ? null : text.replace(/^\uFEFF/, '').replace(/\r\n?/g, '\n');
|
|
298
|
+
}
|
|
299
|
+
|
|
244
300
|
/**
|
|
245
301
|
* Reads the fragments one layer declares for one file.
|
|
246
302
|
*
|
|
@@ -254,7 +310,7 @@ export async function walk(dir) {
|
|
|
254
310
|
*/
|
|
255
311
|
async function readFragments(path) {
|
|
256
312
|
const bySlot = new Map();
|
|
257
|
-
const text = await
|
|
313
|
+
const text = await readLayerText(path);
|
|
258
314
|
if (text === null) return bySlot;
|
|
259
315
|
let current = null;
|
|
260
316
|
const buffer = [];
|
|
@@ -350,7 +406,7 @@ export function projectOwned(target, paths) {
|
|
|
350
406
|
}
|
|
351
407
|
const stamped = p.endsWith('.md') || SCRIPT.test(p);
|
|
352
408
|
// Read without following links, a link is not a file: it is the project's, whatever it points at.
|
|
353
|
-
if (!entry.isFile() || (stamped &&
|
|
409
|
+
if (!entry.isFile() || (stamped && noticeOf(readFileSync(join(target, p), 'utf8')) === null)) out.push(p);
|
|
354
410
|
}
|
|
355
411
|
return out.sort();
|
|
356
412
|
}
|
|
@@ -366,13 +422,15 @@ export function projectOwned(target, paths) {
|
|
|
366
422
|
* @param {string} target - The project directory.
|
|
367
423
|
* @param {{root: string}} ctx - CLI context; `root` is the NINA install directory.
|
|
368
424
|
* @param {{check?: boolean}} [options] - `check` compares without writing.
|
|
369
|
-
* @returns {Promise<{error?: string, written: string[], differ: string[], unfilled: string[], skipped: string[]}>}
|
|
425
|
+
* @returns {Promise<{error?: string, written: string[], differ: string[], unfilled: string[], skipped: string[], removed?: string[]}>}
|
|
370
426
|
*/
|
|
371
427
|
export async function composeProject(target, ctx, options = {}) {
|
|
372
428
|
const check = options.check === true;
|
|
373
429
|
const written = [];
|
|
374
430
|
const differ = [];
|
|
375
431
|
const skipped = [];
|
|
432
|
+
/** Files composed for a surface this profile no longer declares, moved to {@link REMOVED}. */
|
|
433
|
+
const removed = [];
|
|
376
434
|
|
|
377
435
|
const profilePath = join(target, HARNESS, 'profile.json');
|
|
378
436
|
if (!existsSync(profilePath)) {
|
|
@@ -425,9 +483,23 @@ export async function composeProject(target, ctx, options = {}) {
|
|
|
425
483
|
if (requires) {
|
|
426
484
|
core = core.slice(requires[0].length);
|
|
427
485
|
if (!surfaces.includes(requires[1])) {
|
|
428
|
-
// The project has no such surface, so the file does not exist for it.
|
|
486
|
+
// The project has no such surface, so the file does not exist for it. One composed for it before the
|
|
487
|
+
// surface left the profile is still dispatched as a stage the graph no longer has, and was reported as
|
|
488
|
+
// drift that no compose cleared: it goes, with a copy kept. A file there with no notice is the project's.
|
|
429
489
|
skipped.push(rel);
|
|
430
|
-
|
|
490
|
+
let stale = false;
|
|
491
|
+
try {
|
|
492
|
+
stale = lstatSync(dest).isFile() && noticeOf(await readFile(dest, 'utf8')) !== null;
|
|
493
|
+
} catch {
|
|
494
|
+
// Not there.
|
|
495
|
+
}
|
|
496
|
+
if (stale && check) differ.push(`${rel} (present, but no '${requires[1]}' surface)`);
|
|
497
|
+
if (stale && !check) {
|
|
498
|
+
await mkdir(dirname(join(target, REMOVED, rel)), { recursive: true });
|
|
499
|
+
await writeFile(join(target, REMOVED, rel), await readFile(dest));
|
|
500
|
+
await rm(dest);
|
|
501
|
+
removed.push(rel);
|
|
502
|
+
}
|
|
431
503
|
continue;
|
|
432
504
|
}
|
|
433
505
|
}
|
|
@@ -500,7 +572,7 @@ export async function composeProject(target, ctx, options = {}) {
|
|
|
500
572
|
// A name the project deferred, with its reason, reads as not decided: a stage that meets it asks
|
|
501
573
|
// instead of guessing, and the raw placeholder it would otherwise read looks like an editing slip.
|
|
502
574
|
for (const [name, why] of Object.entries(profile.deferred ?? {})) {
|
|
503
|
-
if (typeof why === 'string' && why.trim() &&
|
|
575
|
+
if (typeof why === 'string' && why.trim() && NAME.test(name)) text = text.split(`{{${name}}}`).join(`[${name}: not decided yet]`);
|
|
504
576
|
}
|
|
505
577
|
|
|
506
578
|
// Only what has a comment syntax the notice knows. A layer that one day composes JSON
|
|
@@ -518,7 +590,7 @@ export async function composeProject(target, ctx, options = {}) {
|
|
|
518
590
|
if (previous !== null && previous !== text) differ.push(rel);
|
|
519
591
|
}
|
|
520
592
|
|
|
521
|
-
return { written, differ, unfilled, skipped, malformed };
|
|
593
|
+
return { written, differ, unfilled, skipped, malformed, removed };
|
|
522
594
|
}
|
|
523
595
|
|
|
524
596
|
/**
|
|
@@ -577,6 +649,10 @@ export async function compose(argv, ctx) {
|
|
|
577
649
|
return bad === 0 ? 0 : 1;
|
|
578
650
|
}
|
|
579
651
|
console.log(`composed ${result.written.length} file(s) → ${relative(process.cwd(), target) || '.'}`);
|
|
652
|
+
if (result.removed.length > 0) {
|
|
653
|
+
console.log(` removed ${result.removed.length} file(s) composed for a surface this profile no longer declares — a copy of each is in ${REMOVED}/:`);
|
|
654
|
+
for (const rel of result.removed) console.log(` ${rel}`);
|
|
655
|
+
}
|
|
580
656
|
// The one place a pointer to `nina where` costs nothing. Put it in the notice instead and
|
|
581
657
|
// every composed file in every project differs until it recomposes — for a pointer, not a
|
|
582
658
|
// fix. This line is written to no file, so it can say what it likes, and it reaches whoever
|
package/src/commands/export.mjs
CHANGED
|
@@ -25,7 +25,7 @@ import { spawn } from 'node:child_process';
|
|
|
25
25
|
import { join } from 'node:path';
|
|
26
26
|
import { fileURLToPath } from 'node:url';
|
|
27
27
|
import { readRun, runFile } from '../agentrun.mjs';
|
|
28
|
-
import { BATCH, SETTLE_MINUTES, postScore, postScores, postSpans, scoreOf, settled, spanOf, spansOf } from '../langfuse.mjs';
|
|
28
|
+
import { BATCH, SETTLE_MINUTES, contextMode, postScore, postScores, postSpans, scoreOf, settled, spanOf, spansOf } from '../langfuse.mjs';
|
|
29
29
|
import { runOf, transcriptsOf } from '../transcripts.mjs';
|
|
30
30
|
import { exportsDir, snapshotsDir } from '../paths.mjs';
|
|
31
31
|
import { readConfig, statusPath, targetOf } from './langfuse.mjs';
|
|
@@ -214,7 +214,7 @@ export async function exportCommand(argv, ctx = {}) {
|
|
|
214
214
|
const release = await lock(only);
|
|
215
215
|
if (!release) return 0;
|
|
216
216
|
try {
|
|
217
|
-
const result = await exportProject({ slug: only, name: projectName(only), file: join(snapshotsDir(), `${only}.jsonl`), target, now: ctx.now, since: setting.since, quiet: true, content: setting
|
|
217
|
+
const result = await exportProject({ slug: only, name: projectName(only), file: join(snapshotsDir(), `${only}.jsonl`), target, now: ctx.now, since: setting.since, quiet: true, content: contextMode(setting) });
|
|
218
218
|
if (result.spans > 0 || result.scores > 0 || result.error) {
|
|
219
219
|
// The same failure again is not news: it was reported once, and repeating it every turn it is
|
|
220
220
|
// retried would be the noise the detectors exist to remove.
|
|
@@ -251,7 +251,7 @@ export async function exportCommand(argv, ctx = {}) {
|
|
|
251
251
|
continue;
|
|
252
252
|
}
|
|
253
253
|
try {
|
|
254
|
-
const content =
|
|
254
|
+
const content = contextMode(config.projects[slug]);
|
|
255
255
|
if ((await exportProject({ slug, name, file: join(dir, `${slug}.jsonl`), target, dry, now: ctx.now, content })).error) failed = true;
|
|
256
256
|
} finally {
|
|
257
257
|
await release();
|
|
@@ -265,7 +265,7 @@ export async function exportCommand(argv, ctx = {}) {
|
|
|
265
265
|
*
|
|
266
266
|
* @returns {Promise<{spans: number, scores: number, error: string|null}>} What went, and why the rest did not.
|
|
267
267
|
*/
|
|
268
|
-
async function exportProject({ slug, name, file, target, dry, now, since = '', quiet = false, content =
|
|
268
|
+
async function exportProject({ slug, name, file, target, dry, now, since = '', quiet = false, content = null }) {
|
|
269
269
|
const say = quiet ? () => {} : (line) => console.log(line);
|
|
270
270
|
const records = await recordsOf(file);
|
|
271
271
|
const sent = await sentOf(slug);
|
|
@@ -282,7 +282,8 @@ async function exportProject({ slug, name, file, target, dry, now, since = '', q
|
|
|
282
282
|
const tail = () => (notes.length ? ` — ${notes.join('; ')}` : '');
|
|
283
283
|
|
|
284
284
|
if (dry) {
|
|
285
|
-
|
|
285
|
+
const what = content === 'prompts' ? " with each stage's prompt and report" : content ? " with each stage's context" : '';
|
|
286
|
+
say(` ${name}: would send ${todo.spans.length} trace(s)${what} and ${todo.scores.length} score(s) to ${target.host}${tail()}`);
|
|
286
287
|
if (todo.spans[0]) say(` e.g. ${JSON.stringify(spanOf(todo.spans[0], name))}`);
|
|
287
288
|
return { spans: 0, scores: 0, error: null };
|
|
288
289
|
}
|
|
@@ -319,11 +320,12 @@ async function exportProject({ slug, name, file, target, dry, now, since = '', q
|
|
|
319
320
|
if (error) break;
|
|
320
321
|
const run = content ? await readRun(runFile(transcriptsOf(slug), record), record.round ?? 1) : null;
|
|
321
322
|
if (content && !run) bare += 1;
|
|
322
|
-
|
|
323
|
+
const sending = { prompts: content === 'prompts' };
|
|
324
|
+
let built = spansOf(record, name, run, {}, sending);
|
|
323
325
|
let size = JSON.stringify(built).length;
|
|
324
326
|
for (const limits of SMALLER) {
|
|
325
327
|
if (size <= MAX_BODY) break;
|
|
326
|
-
built = spansOf(record, name, run, limits);
|
|
328
|
+
built = spansOf(record, name, run, limits, sending);
|
|
327
329
|
size = JSON.stringify(built).length;
|
|
328
330
|
}
|
|
329
331
|
// Cutting its texts bounds each one, not how many calls a stage made. One that still does not fit
|
package/src/commands/init.mjs
CHANGED
|
@@ -20,7 +20,7 @@ import { existsSync, readFileSync } from 'node:fs';
|
|
|
20
20
|
import { join, resolve } from 'node:path';
|
|
21
21
|
import { REQUIRES, SLOT, byVersion, composeProject, composedPaths, defaultedSlots, layerRootFor, projectOwned, walk } from './compose.mjs';
|
|
22
22
|
import { owedDocuments } from './check.mjs';
|
|
23
|
-
import { defaultVocabulary } from '../vocabulary.mjs';
|
|
23
|
+
import { PLACEHOLDER, defaultVocabulary } from '../vocabulary.mjs';
|
|
24
24
|
import { PINK, useColor } from '../banner.mjs';
|
|
25
25
|
import { DETECTABLE, NEEDS } from '../surfaces.mjs';
|
|
26
26
|
|
|
@@ -273,8 +273,17 @@ export async function init(argv, ctx) {
|
|
|
273
273
|
let previous = null;
|
|
274
274
|
try {
|
|
275
275
|
previous = JSON.parse(readFileSync(join(harness, 'profile.json'), 'utf8'));
|
|
276
|
-
} catch {
|
|
277
|
-
// None
|
|
276
|
+
} catch (error) {
|
|
277
|
+
// None: there is nothing to keep. One that is there but cannot be read still holds the pin and every
|
|
278
|
+
// value the project declared — read as "none", a trailing comma moved the pin to the newest release and
|
|
279
|
+
// wrote every vocabulary value away.
|
|
280
|
+
if (error.code !== 'ENOENT') {
|
|
281
|
+
console.error(
|
|
282
|
+
` ${HARNESS}/profile.json cannot be read (${error.message}). Starting over would write a new one over it and keep ` +
|
|
283
|
+
'nothing it declares — fix it and run this again, or move it aside to start from nothing.\n',
|
|
284
|
+
);
|
|
285
|
+
return 1;
|
|
286
|
+
}
|
|
278
287
|
}
|
|
279
288
|
if (previous?.core && arg('--core') && arg('--core') !== previous.core) {
|
|
280
289
|
console.error(` ${target} pins ${previous.core}. A pin moves with \`nina upgrade --to ${arg('--core')}\`, which says what the move costs and rolls it back when it fails.\n`);
|
|
@@ -341,7 +350,7 @@ export async function init(argv, ctx) {
|
|
|
341
350
|
const released = await defaultedSlots(resolved.dir);
|
|
342
351
|
for (const { rel, core, fragments } of files) {
|
|
343
352
|
const lines = core.split('\n');
|
|
344
|
-
for (const [, name] of [core, ...fragments].join('\n').matchAll(
|
|
353
|
+
for (const [, name] of [core, ...fragments].join('\n').matchAll(PLACEHOLDER)) {
|
|
345
354
|
vocabulary.add(name);
|
|
346
355
|
}
|
|
347
356
|
/** The nearest non-empty neighbour, naming it when it is another slot. */
|
|
@@ -2,14 +2,15 @@
|
|
|
2
2
|
* `nina langfuse` — the keys, set once, and which projects send their runs to Langfuse on their own.
|
|
3
3
|
*
|
|
4
4
|
* nina langfuse login [--host <url>] ask for the keys, check them against Langfuse, keep them
|
|
5
|
-
* nina langfuse on [--project <dir>] [--content] send this project's runs after every turn
|
|
5
|
+
* nina langfuse on [--project <dir>] [--content | --prompts] send this project's runs after every turn
|
|
6
6
|
* nina langfuse off [--project <dir>]
|
|
7
7
|
* nina langfuse status
|
|
8
8
|
*
|
|
9
9
|
* Once a project is on, nobody runs an export: the `lessons` detector every composed project runs
|
|
10
10
|
* already snapshots the project each turn, and it starts the export in the background whenever a run
|
|
11
11
|
* is ready to go. What it sends is metadata unless the project is turned on with `--content`, which adds
|
|
12
|
-
* each stage's own context
|
|
12
|
+
* each stage's own context, or with `--prompts`, which adds only what passed between the agents: the
|
|
13
|
+
* prompt each stage was given and the report it handed back — see `src/langfuse.mjs`.
|
|
13
14
|
*
|
|
14
15
|
* The keys live in `~/.nina/exports/langfuse.json`, readable by the owner only, beside the record of what
|
|
15
16
|
* was sent; `LANGFUSE_PUBLIC_KEY`, `LANGFUSE_SECRET_KEY` and `LANGFUSE_HOST` override them. They are asked
|
|
@@ -21,20 +22,26 @@
|
|
|
21
22
|
import { chmod, mkdir, readFile, rename, rm, writeFile } from 'node:fs/promises';
|
|
22
23
|
import { join, resolve } from 'node:path';
|
|
23
24
|
import { createInterface } from 'node:readline';
|
|
24
|
-
import { DEFAULT_HOST, projectOf } from '../langfuse.mjs';
|
|
25
|
+
import { DEFAULT_HOST, contextMode, projectOf } from '../langfuse.mjs';
|
|
25
26
|
import { exportsDir, slugFor } from '../paths.mjs';
|
|
26
27
|
import { projectName } from './stats.mjs';
|
|
27
28
|
|
|
28
29
|
/** Where the keys and the switches live. */
|
|
29
30
|
export const configPath = () => join(exportsDir(), 'langfuse.json');
|
|
30
31
|
|
|
32
|
+
/** What a project sends, in words, by `contextMode`. */
|
|
33
|
+
const LABELS = { full: "with each stage's own context", prompts: "with each stage's prompt and report", none: 'as metadata only' };
|
|
34
|
+
|
|
35
|
+
/** The same, as `status` lists it. */
|
|
36
|
+
const SHORT = { full: 'with context', prompts: 'with prompts and reports', none: 'metadata only' };
|
|
37
|
+
|
|
31
38
|
/** Where the background export leaves what it last did, per project. */
|
|
32
39
|
export const statusPath = (slug) => join(exportsDir(), 'langfuse', `${slug}.status.json`);
|
|
33
40
|
|
|
34
41
|
/**
|
|
35
42
|
* The configuration on disk: keys, host, and the projects that are on.
|
|
36
43
|
*
|
|
37
|
-
* @returns {Promise<{publicKey?: string, secretKey?: string, host?: string, projects: Record<string, {content: boolean, since: string}>}>}
|
|
44
|
+
* @returns {Promise<{publicKey?: string, secretKey?: string, host?: string, projects: Record<string, {content: boolean|'prompts', since: string}>}>}
|
|
38
45
|
*/
|
|
39
46
|
export async function readConfig() {
|
|
40
47
|
try {
|
|
@@ -113,7 +120,7 @@ export async function langfuse(argv, ctx = {}) {
|
|
|
113
120
|
}
|
|
114
121
|
await writeConfig({ ...config, host, publicKey, secretKey });
|
|
115
122
|
console.log(` ✓ the keys open "${opened.project}" at ${host}; kept in ${configPath()}, readable by you only`);
|
|
116
|
-
console.log(' next, in each project that should send: `nina langfuse on` (add `--content` for each stage\'s
|
|
123
|
+
console.log(' next, in each project that should send: `nina langfuse on` (add `--prompts` for what passed between the agents, or `--content` for each stage\'s whole context)\n');
|
|
117
124
|
return 0;
|
|
118
125
|
}
|
|
119
126
|
|
|
@@ -123,13 +130,21 @@ export async function langfuse(argv, ctx = {}) {
|
|
|
123
130
|
console.error(' no keys yet — `nina langfuse login` first\n');
|
|
124
131
|
return 2;
|
|
125
132
|
}
|
|
126
|
-
|
|
133
|
+
if (argv.includes('--content') && argv.includes('--prompts')) {
|
|
134
|
+
console.error(' --content already sends the prompts; pass one of the two\n');
|
|
135
|
+
return 2;
|
|
136
|
+
}
|
|
137
|
+
const content = argv.includes('--content') ? true : argv.includes('--prompts') ? 'prompts' : false;
|
|
127
138
|
await writeConfig({ ...config, projects: { ...config.projects, [slug]: { content, since: new Date().toISOString() } } });
|
|
128
|
-
console.log(` ✓ ${project} sends its runs to ${target.host} from now on, after every turn, ${content
|
|
129
|
-
if (content) {
|
|
139
|
+
console.log(` ✓ ${project} sends its runs to ${target.host} from now on, after every turn, ${LABELS[contextMode({ content }) ?? 'none']}`);
|
|
140
|
+
if (content === true) {
|
|
130
141
|
console.log(" each stage's prompt, messages, tool calls and report go with it — code and whatever the stages read included;");
|
|
131
142
|
console.log(' obvious secrets are masked, and nothing more is: turn it on only where that code may leave this machine');
|
|
132
143
|
}
|
|
144
|
+
if (content === 'prompts') {
|
|
145
|
+
console.log(" each stage's prompt and report go with it, and nothing it read, wrote or called between them;");
|
|
146
|
+
console.log(' a prompt carries whatever the orchestrator put in it, diffs and code included, and only obvious secrets are masked');
|
|
147
|
+
}
|
|
133
148
|
console.log(` the runs from before now stay here; \`nina export --langfuse --project ${slug}\` sends them, once`);
|
|
134
149
|
console.log(' it runs from the `lessons` detector in this project\'s hooks — `nina check` says whether they are wired\n');
|
|
135
150
|
return 0;
|
|
@@ -150,12 +165,12 @@ export async function langfuse(argv, ctx = {}) {
|
|
|
150
165
|
for (const [name, setting] of on) {
|
|
151
166
|
const last = JSON.parse(await readFile(statusPath(name), 'utf8').catch(() => 'null'));
|
|
152
167
|
const said = !last ? 'nothing sent yet' : last.error ? `✗ ${last.at}: ${last.error}` : `last sent ${last.at}: ${last.spans} trace(s), ${last.scores} score(s)`;
|
|
153
|
-
console.log(` ${projectName(name)}: ${setting
|
|
168
|
+
console.log(` ${projectName(name)}: ${SHORT[contextMode(setting) ?? 'none']}, since ${setting.since.slice(0, 10)} — ${said}`);
|
|
154
169
|
}
|
|
155
170
|
console.log('');
|
|
156
171
|
return 0;
|
|
157
172
|
}
|
|
158
173
|
|
|
159
|
-
console.error(' usage: nina langfuse login [--host <url>] | on [--project <dir>] [--content] | off [--project <dir>] | status\n');
|
|
174
|
+
console.error(' usage: nina langfuse login [--host <url>] | on [--project <dir>] [--content | --prompts] | off [--project <dir>] | status\n');
|
|
160
175
|
return 2;
|
|
161
176
|
}
|
package/src/commands/learn.mjs
CHANGED
|
@@ -305,11 +305,20 @@ async function answerRequest(target, file, answer) {
|
|
|
305
305
|
*/
|
|
306
306
|
async function close(target, which) {
|
|
307
307
|
const dir = join(target, HARNESS, 'requests');
|
|
308
|
-
const
|
|
309
|
-
|
|
310
|
-
|
|
308
|
+
const files = (await readdir(dir).catch(() => [])).filter((f) => f.endsWith('.md'));
|
|
309
|
+
// The name exactly, else a prefix of one — and exactly one, as `requests --answer` asks. A prefix alone let
|
|
310
|
+
// an empty name match every file: a bare `--close` closed whichever request sorted first and retired its pill.
|
|
311
|
+
const exact = files.filter((f) => f === which || join(dir, f) === resolve(target, which));
|
|
312
|
+
const matches = exact.length > 0 ? exact : files.filter((f) => which && f.startsWith(which));
|
|
313
|
+
if (matches.length !== 1) {
|
|
314
|
+
console.error(
|
|
315
|
+
matches.length === 0
|
|
316
|
+
? ` no request matching "${which}" under ${HARNESS}/requests/ — \`nina learn\` lists them\n`
|
|
317
|
+
: ` "${which}" matches ${matches.length} requests — name one:\n${matches.map((f) => ` ${f}`).join('\n')}\n`,
|
|
318
|
+
);
|
|
311
319
|
return 1;
|
|
312
320
|
}
|
|
321
|
+
const [file] = matches;
|
|
313
322
|
const fields = frontmatter(await readFile(join(dir, file), 'utf8')) ?? {};
|
|
314
323
|
const { pill } = await answerRequest(target, file, { status: 'closed', retire: true });
|
|
315
324
|
console.log(` closed ${HARNESS}/requests/${file}`);
|
package/src/commands/release.mjs
CHANGED
|
@@ -9,9 +9,10 @@
|
|
|
9
9
|
* Releases are never rewritten. Fixing a release means cutting the next one.
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
-
import { cp, mkdir, readFile, readdir, writeFile } from 'node:fs/promises';
|
|
12
|
+
import { cp, mkdir, readFile, readdir, rename, rm, writeFile } from 'node:fs/promises';
|
|
13
13
|
import { existsSync } from 'node:fs';
|
|
14
14
|
import { join } from 'node:path';
|
|
15
|
+
import { byVersion } from './compose.mjs';
|
|
15
16
|
|
|
16
17
|
/** A release name: dotted numbers, so the directory sorts and reads like a version. */
|
|
17
18
|
const VERSION = /^\d+\.\d+\.\d+$/;
|
|
@@ -80,11 +81,28 @@ export async function release(argv, ctx) {
|
|
|
80
81
|
console.error(` ${version} already exists — a release is immutable; cut the next one.\n`);
|
|
81
82
|
return 1;
|
|
82
83
|
}
|
|
84
|
+
// Above the newest, or `init` pins something older than what the package says it is, and the package's
|
|
85
|
+
// version moves backwards with the cut.
|
|
86
|
+
const newest = (await readdir(join(ctx.root, 'releases'), { withFileTypes: true }).catch(() => []))
|
|
87
|
+
.filter((e) => e.isDirectory() && VERSION.test(e.name))
|
|
88
|
+
.map((e) => e.name)
|
|
89
|
+
.sort(byVersion)
|
|
90
|
+
.at(-1);
|
|
91
|
+
if (newest && byVersion(version, newest) < 0) {
|
|
92
|
+
console.error(` ${newest} is the newest release — a release is cut above it, not below.\n`);
|
|
93
|
+
return 1;
|
|
94
|
+
}
|
|
83
95
|
|
|
84
|
-
|
|
96
|
+
// Frozen beside the releases and moved in whole: a cut stopped half-way used to leave a partial release,
|
|
97
|
+
// immutable by name, which `init` then pinned as the newest.
|
|
98
|
+
const partial = join(ctx.root, `.release-${version}.partial`);
|
|
99
|
+
await rm(partial, { recursive: true, force: true });
|
|
100
|
+
await mkdir(partial, { recursive: true });
|
|
85
101
|
for (const layer of ['core', 'surfaces']) {
|
|
86
|
-
await cp(join(ctx.root, layer), join(
|
|
102
|
+
await cp(join(ctx.root, layer), join(partial, layer), { recursive: true });
|
|
87
103
|
}
|
|
104
|
+
await mkdir(join(ctx.root, 'releases'), { recursive: true });
|
|
105
|
+
await rename(partial, dest);
|
|
88
106
|
|
|
89
107
|
const surfaces = (await readdir(join(dest, 'surfaces'), { withFileTypes: true }))
|
|
90
108
|
.filter((e) => e.isDirectory())
|