tina4-nodejs 3.13.99 → 3.13.101

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.
@@ -6,6 +6,35 @@ export interface AiTool {
6
6
  }
7
7
  export declare const AI_TOOLS: AiTool[];
8
8
  export declare const DEV_SKILL = "tina4-developer-nodejs";
9
+ /**
10
+ * Fetch a set of skill files over the network SYNCHRONOUSLY.
11
+ *
12
+ * Node has no built-in synchronous HTTP, and the whole installer chain
13
+ * (installSelected → installForTool → installClaudeSkills) is synchronous and
14
+ * can't be made async without breaking its existing callers/tests. So we run
15
+ * ONE blocking child `node` process that fetches every URL in parallel with the
16
+ * global `fetch` (Node 18+) and writes each body to all of its destinations.
17
+ * The child prints a JSON array of the URLs it fetched successfully; any fetch
18
+ * failure is skipped, never fatal.
19
+ *
20
+ * MEASURED (2026-08-13): a real GitHub raw-content fetch occasionally drops a
21
+ * request under load (transient DNS/TLS hiccup, not a missing file — every
22
+ * URL here resolves fine on its own) while its siblings in the same batch
23
+ * succeed. One retry pass over only transport failures and transient HTTP
24
+ * statuses, still inside the same child process, fixes that for real installer
25
+ * users too. Permanent 4xx responses are final answers and are not retried.
26
+ *
27
+ * Exported (like `writeOrMerge`/`markersFor`/`skillBlock` above) so
28
+ * aiFetchRetry.test.ts can drive it directly against a real local server —
29
+ * a pure visibility change, no behaviour change.
30
+ *
31
+ * @param jobs one entry per unique URL, with every file path it should land in
32
+ * @returns the set of URLs that were fetched and written to disk
33
+ */
34
+ export declare function downloadSkillsSync(jobs: {
35
+ url: string;
36
+ dests: string[];
37
+ }[]): Set<string>;
9
38
  /**
10
39
  * Install the Tina4 SKILL.md skills into the project AND the global
11
40
  * ~/.claude/skills, fetched from the release ref matching this framework
@@ -0,0 +1,66 @@
1
+ export declare class AiError extends Error {
2
+ }
3
+ export declare class AiConfigError extends AiError {
4
+ }
5
+ export declare class AiTimeoutError extends AiError {
6
+ }
7
+ export declare class AiParseError extends AiError {
8
+ }
9
+ export declare class AiHTTPError extends AiError {
10
+ readonly status: number | null;
11
+ constructor(message: string, status?: number | null);
12
+ }
13
+ export interface ChatResponse {
14
+ text: string;
15
+ model: string;
16
+ usage: {
17
+ promptTokens: number;
18
+ completionTokens: number;
19
+ totalTokens: number;
20
+ };
21
+ finishReason: string | null;
22
+ raw: Record<string, unknown>;
23
+ }
24
+ export interface AiMessage {
25
+ role: "system" | "user" | "assistant";
26
+ content: string;
27
+ }
28
+ export interface AiChatOptions {
29
+ model?: string;
30
+ temperature?: number;
31
+ maxTokens?: number;
32
+ stream?: boolean;
33
+ timeout?: number;
34
+ provider?: "local" | "openai" | "anthropic";
35
+ }
36
+ export interface AiEmbedOptions {
37
+ model?: string;
38
+ timeout?: number;
39
+ provider?: "local" | "openai" | "anthropic";
40
+ }
41
+ export declare class Ai {
42
+ static chat(messages: AiMessage[], options: AiChatOptions & {
43
+ stream: true;
44
+ }): AsyncGenerator<string>;
45
+ static chat(messages: AiMessage[], options?: AiChatOptions & {
46
+ stream?: false;
47
+ }): Promise<ChatResponse>;
48
+ static complete(prompt: string, options?: Omit<AiChatOptions, "stream">): Promise<string>;
49
+ static embed(textOrTexts: string | string[], options?: AiEmbedOptions): Promise<number[] | number[][]>;
50
+ private static validateMessages;
51
+ private static number;
52
+ private static config;
53
+ private static endpoint;
54
+ private static headers;
55
+ private static chatBody;
56
+ private static open;
57
+ private static readBody;
58
+ private static retryDelay;
59
+ private static requestJson;
60
+ private static normalizeChat;
61
+ private static chatResponse;
62
+ private static streamDelta;
63
+ private static streamData;
64
+ private static streamError;
65
+ private static streamRequest;
66
+ }
@@ -55,6 +55,8 @@ export { HtmlElement, htmlElement, addHtmlHelpers, Raw, SafeString } from "./htm
55
55
  export { renderErrorOverlay, isDebugMode } from "./errorOverlay.js";
56
56
  export { AI_TOOLS, isInstalled, showMenu, installSelected, installAll, generateContext } from "./ai.js";
57
57
  export type { AiTool } from "./ai.js";
58
+ export { Ai, AiError, AiConfigError, AiHTTPError, AiTimeoutError, AiParseError } from "./aiClient.js";
59
+ export type { ChatResponse, AiMessage, AiChatOptions, AiEmbedOptions } from "./aiClient.js";
58
60
  export type { ImapMessage, ImapFullMessage, ImapAttachment } from "./messenger.js";
59
61
  export { LiteBackend } from "./queueBackends/liteBackend.js";
60
62
  export { RabbitMQBackend, parseAmqpUrl } from "./queueBackends/rabbitmqBackend.js";
@@ -1,41 +1,6 @@
1
- export declare function quickMetrics(root?: string): Record<string, any>;
2
- /**
3
- * The native metrics engine could not produce a payload.
4
- *
5
- * Thrown instead of falling back to a second implementation.
6
- */
7
1
  export declare class MetricsEngineError extends Error {
8
2
  constructor(message: string);
9
3
  }
10
- export declare const SEVERITY_RANK: Record<string, number>;
11
- /**
12
- * Return [directory to scan, scanMode] for any metrics producer.
13
- *
14
- * The engine is language-agnostic and cannot know which directory holds a
15
- * framework package, so root resolution and the "framework" label stay here,
16
- * shared by the census and the engine adapter so the two never disagree.
17
- */
18
- export declare function resolveScanTarget(root?: string): [string, string];
19
- /** Absolute path to the tina4 CLI binary, or null when it is not installed. */
20
4
  export declare function enginePath(): string | null;
21
- /** Full code analysis from the native engine, shaped for the dashboard. */
22
5
  export declare function fullAnalysis(root?: string): Record<string, any>;
23
- export interface OffendersResult {
24
- offenders: Record<string, any>[];
25
- summary: Record<string, any>;
26
- }
27
- /**
28
- * Top code-health offenders from the native engine.
29
- *
30
- * The engine ranks and severity-tags them, and its own --fail-on gate reads the
31
- * same list, so the CLI and the dashboard can never disagree about what counts
32
- * as an offender.
33
- */
34
- export declare function offenders(root?: string, top?: number): OffendersResult;
35
- /**
36
- * Per-file metrics from the native engine.
37
- *
38
- * The engine accepts a single file for --path, so one code path serves both the
39
- * whole-tree scan and one file.
40
- */
41
6
  export declare function fileDetail(filePath: string): Record<string, any>;
@@ -14,6 +14,15 @@ export interface LiveResponse {
14
14
  }
15
15
  /** WebSocket broadcaster hook wired by @tina4/core so pushLive can broadcast. */
16
16
  export type LiveBroadcaster = (wsPath: string | null, name: string, envelope: string) => void;
17
+ /**
18
+ * Cache for parsed filter chains: expr string -> [variable, filters].
19
+ * Exported (like TEMPLATE_CACHE_MAX) so the ADR-0004 bound has something for
20
+ * a test to inspect directly — module-level state has no instance to read
21
+ * off, unlike `compiled`/`compiledStrings`/`fragmentCache`.
22
+ */
23
+ export declare const filterChainCache: Map<string, [string, [string, unknown[]][]]>;
24
+ /** Cache for parsed dotted/bracket paths: expr string -> [parts, fromBracket]. Exported for the same reason as filterChainCache. */
25
+ export declare const pathParseCache: Map<string, [string[], boolean[]]>;
17
26
  /**
18
27
  * Hard cap on the template caches — `compiled` and `compiledStrings`
19
28
  * (ADR-0004, parity with PHP/Python/Ruby TEMPLATE_CACHE_MAX).
@@ -26,6 +35,18 @@ export type LiveBroadcaster = (wsPath: string | null, name: string, envelope: st
26
35
  * dynamically adds an entry per distinct string.
27
36
  */
28
37
  export declare const TEMPLATE_CACHE_MAX = 256;
38
+ /**
39
+ * Hard cap on every per-expression memo cache — `filterChainCache` and
40
+ * `pathParseCache` (ADR-0004, parity with PHP's MEMO_CACHE_MAX and the
41
+ * Python master's `@lru_cache(maxsize=1024)` on the equivalent module-level
42
+ * parsers). Deliberately higher than TEMPLATE_CACHE_MAX: one entry here is a
43
+ * small parsed-path array, orders of magnitude smaller than a token list.
44
+ *
45
+ * Also reused for `fragmentCache` (the `{% cache %}` tag's runtime store):
46
+ * TEMPLATE_CACHE_MAX, not this one — a rendered fragment is a whole HTML
47
+ * string, the same order of magnitude as a compiled template.
48
+ */
49
+ export declare const MEMO_CACHE_MAX = 1024;
29
50
  /**
30
51
  * Set the session ID used by formToken() / form_token() for CSRF session binding.
31
52
  */
@@ -95,20 +116,16 @@ export declare class Frond {
95
116
  sandbox(filters?: string[], tags?: string[], vars?: string[]): Frond;
96
117
  unsandbox(): Frond;
97
118
  /**
98
- * Register a custom filter. The filter is persisted at class level
99
- * so new instances created by hot-reload inherit it automatically;
100
- * the live instance's local filter map also receives the addition
101
- * immediately. Mirrors Python's _ClassOrInstanceMethod dual-call.
119
+ * Register a custom filter on this instance only. Use the static method
120
+ * for process-global registration. tina4: ADR-0052.
102
121
  */
103
122
  addFilter(name: string, fn: FilterFn): void;
104
123
  /**
105
- * Register a global variable available in all templates. Persisted
106
- * at class level — see ``addFilter`` for the dual-call semantics.
124
+ * Register a global variable on this instance only.
107
125
  */
108
126
  addGlobal(name: string, value: unknown): void;
109
127
  /**
110
- * Register a custom test. Persisted at class level — see
111
- * ``addFilter`` for the dual-call semantics.
128
+ * Register a custom test on this instance only.
112
129
  */
113
130
  addTest(name: string, fn: TestFn): void;
114
131
  /**
@@ -143,6 +160,31 @@ export declare class Frond {
143
160
  private executeWithSource;
144
161
  private execute;
145
162
  private extractBlocks;
163
+ /**
164
+ * Depth-aware block substitution against `source` (typically the
165
+ * fully-resolved root template).
166
+ *
167
+ * A single regex `.replace()` pass (the flat `pattern` this replaces in
168
+ * renderWithBlocks) pairs an OUTER block's open tag with the FIRST
169
+ * `{% endblock %}` found -- which, when the outer block wraps a NESTED
170
+ * `{% block %}`, is the nested block's own close tag, not the outer's.
171
+ * That silently truncates the outer block's captured content and drops
172
+ * everything after the inner endblock (the root-nested-block
173
+ * content-loss bug). This scans with an open/close depth counter
174
+ * instead (mirroring extractBlocks), so an outer block always captures
175
+ * its FULL body, nested child blocks included.
176
+ *
177
+ * The content chosen for each block -- the child override in `blocks`
178
+ * if present, else the block's own default body -- is then recursively
179
+ * substituted against the SAME `blocks` map before being tokenized and
180
+ * rendered, so a block nested inside another block resolves correctly
181
+ * regardless of which template in the inheritance chain declared the
182
+ * nesting (the root, an intermediate, however many levels deep).
183
+ *
184
+ * `{{ parent() }}` / `{{ super() }}` inside a block still render that
185
+ * block's OWN default content at this level (lazy, on first call).
186
+ */
187
+ private substituteBlocks;
146
188
  private renderWithBlocks;
147
189
  private renderTokens;
148
190
  /**
@@ -1,160 +0,0 @@
1
- /**
2
- * CLI command: metrics — Rank top code-quality offenders.
3
- *
4
- * Exposes the existing static analyzer (cyclomatic complexity, maintainability,
5
- * large-file, too-many-functions, and the now-precise has_tests) on the CLI as
6
- * a self-monitoring tool.
7
- *
8
- * tina4nodejs metrics # human report, scans src/ (or framework)
9
- * tina4nodejs metrics --top 10 # only the worst 10
10
- * tina4nodejs metrics --path packages/core/src # scan a specific directory
11
- * tina4nodejs metrics --json # machine-readable for CI (ONLY json printed)
12
- * tina4nodejs metrics --fail-on warn # exit 1 if any warn/error offender
13
- * tina4nodejs metrics --fail-on error # exit 1 only on error-severity
14
- *
15
- * Returns the intended process exit code (the caller exits). Splitting "compute
16
- * exit code" from "exit the process" keeps the handler unit-testable.
17
- */
18
- import { offenders, MetricsEngineError } from "../../../core/src/metrics.js";
19
-
20
- type Flags = {
21
- top: number;
22
- json: boolean;
23
- path: string;
24
- failOn: "warn" | "error" | null;
25
- };
26
-
27
- /** Parse `--top N`, `--json`, `--fail-on warn|error`, `--path DIR` out of argv. */
28
- function parseFlags(args: string[]): Flags | { error: string } {
29
- const flags: Flags = { top: 20, json: false, path: "src", failOn: null };
30
-
31
- for (let i = 0; i < args.length; i++) {
32
- const a = args[i];
33
- switch (a) {
34
- case "--json":
35
- flags.json = true;
36
- break;
37
- case "--top": {
38
- const v = args[++i];
39
- if (v === undefined || !/^\d+$/.test(v)) {
40
- return { error: `--top expects a number (got '${v ?? ""}')` };
41
- }
42
- flags.top = parseInt(v, 10);
43
- break;
44
- }
45
- case "--path": {
46
- const v = args[++i];
47
- if (v === undefined) return { error: "--path expects a directory" };
48
- flags.path = v;
49
- break;
50
- }
51
- case "--fail-on": {
52
- const v = args[++i];
53
- if (v !== "warn" && v !== "error") {
54
- return { error: `invalid --fail-on '${v ?? ""}' (use warn or error)` };
55
- }
56
- flags.failOn = v;
57
- break;
58
- }
59
- default:
60
- return { error: `unknown option '${a}'` };
61
- }
62
- }
63
-
64
- return flags;
65
- }
66
-
67
- /**
68
- * Run the metrics report. Returns the process exit code; does NOT call
69
- * process.exit (the bin wrapper does). 0 = ok / below threshold, 1 = gated
70
- * failure, 2 = bad arguments / analysis error.
71
- */
72
- export function runMetrics(args: string[] = []): number {
73
- const parsed = parseFlags(args);
74
- if ("error" in parsed) {
75
- console.log(` ${parsed.error}`);
76
- return 2;
77
- }
78
- const { top, json, path, failOn } = parsed;
79
-
80
- // ONE engine run. Ask for every offender and slice for display: the gate must
81
- // read the FULL set, not the printed top-N, and the old second call re-ran the
82
- // whole analysis (its "mtime-cached" comment stopped being true when the
83
- // in-process analyzer and its cache were deleted -- ADR-0002).
84
- let result;
85
- try {
86
- result = offenders(path, Number.MAX_SAFE_INTEGER);
87
- } catch (e) {
88
- if (e instanceof MetricsEngineError) {
89
- console.error(` metrics error: ${e.message}`);
90
- return 2;
91
- }
92
- throw e;
93
- }
94
- const summary = result.summary;
95
- const allOffenders = result.offenders;
96
- const found = allOffenders.slice(0, top);
97
-
98
- const severities = new Set(allOffenders.map((o) => o.severity));
99
- let exitCode = 0;
100
- if (failOn === "warn" && (severities.has("warn") || severities.has("error"))) {
101
- exitCode = 1;
102
- } else if (failOn === "error" && severities.has("error")) {
103
- exitCode = 1;
104
- }
105
-
106
- if (json) {
107
- // Print ONLY the JSON — machine-readable for CI / tooling.
108
- console.log(JSON.stringify({ summary, offenders: found }, null, 2));
109
- return exitCode;
110
- }
111
-
112
- // ── Human report ──────────────────────────────────────────────────
113
- const useColor = Boolean(process.stdout.isTTY);
114
- const c = (text: string, code: string): string =>
115
- useColor ? `\x1b[${code}m${text}\x1b[0m` : text;
116
- const sevColor: Record<string, string> = { error: "31", warn: "33", info: "2" }; // red / yellow / dim
117
-
118
- console.log("");
119
- console.log(` Tina4 Metrics — ${summary.scan_mode} scan (${summary.scan_root})`);
120
- console.log(
121
- ` files: ${summary.files_analyzed} ` +
122
- `functions: ${summary.total_functions} ` +
123
- `avg complexity: ${summary.avg_complexity} ` +
124
- `avg maintainability: ${summary.avg_maintainability}`
125
- );
126
- console.log(
127
- ` offenders: ${summary.total_offenders} total` +
128
- (found.length ? ` (showing top ${found.length})` : "")
129
- );
130
- console.log("");
131
-
132
- if (found.length === 0) {
133
- console.log(" " + c("✓ no offenders — clean", "32"));
134
- console.log("");
135
- return exitCode;
136
- }
137
-
138
- // Column widths so the table lines up.
139
- const locs = found.map((o) => `${o.file}:${o.line}`);
140
- const locW = Math.max("FILE:LINE".length, ...locs.map((s) => s.length));
141
- const kindW = Math.max("KIND".length, ...found.map((o) => o.kind.length));
142
-
143
- const pad = (s: string, w: number) => s.padEnd(w);
144
- const header = ` ${pad("#", 3)} ${pad("SEVERITY", 8)} ${pad("KIND", kindW)} ${pad(
145
- "FILE:LINE",
146
- locW
147
- )} DETAIL`;
148
- console.log(c(header, "1"));
149
- console.log(" " + "-".repeat(header.length - 2));
150
- found.forEach((o, idx) => {
151
- const i = idx + 1;
152
- const sevCell = c(pad(o.severity, 8), sevColor[o.severity]);
153
- console.log(
154
- ` ${String(i).padStart(3)} ${sevCell} ${pad(o.kind, kindW)} ` +
155
- `${pad(locs[idx], locW)} ${o.detail}`
156
- );
157
- });
158
- console.log("");
159
- return exitCode;
160
- }
@@ -1,6 +0,0 @@
1
- /**
2
- * Run the metrics report. Returns the process exit code; does NOT call
3
- * process.exit (the bin wrapper does). 0 = ok / below threshold, 1 = gated
4
- * failure, 2 = bad arguments / analysis error.
5
- */
6
- export declare function runMetrics(args?: string[]): number;