@linxiraos/pi-utils 1.1.23 → 1.1.25

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,11 +2,13 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
- ## [1.1.23] - 2026-10-01
5
+ ## [1.1.25] - 2026-10-03
6
6
 
7
- ## [1.1.22] - 2026-09-30
7
+ - 版本线推进至 1.1.25;本版无独立用户可见变化。
8
8
 
9
- - **CLI bin 命名**:新增 `CLI_BIN_NAME`(zeta-c),Usage/进程标题/补全驱动统一;配置根 `.zeta`、`ZETA_*`、`APP_NAME` 不变。
9
+ ## [1.1.24] - 2026-10-03
10
+
11
+ - v18.4.11 集成:dirs.ts XDG 解析增量(skill-descriptions/predict/global daemon)+ 插件存储契约(`pi.storage` 三目录、`adoptLegacyFileOnce` 公共化);插件锁文件名保持上游兼容。
10
12
 
11
13
  ## [1.1.18] - 2026-09-22
12
14
 
@@ -1,7 +1,7 @@
1
- OMP THIRD-PARTY NOTICES
1
+ ZETA THIRD-PARTY NOTICES
2
2
  =======================
3
3
 
4
- This generated, self-contained aggregate accompanies OMP release artifacts.
4
+ This generated, self-contained aggregate accompanies Zeta release artifacts.
5
5
  It does not replace package-level license files. First-party licensing grants
6
6
  no rights in third-party code, fonts, data, or marks identified below.
7
7
 
@@ -1046,7 +1046,7 @@ These packages participate only in generation/web build tooling and are not ship
1046
1046
 
1047
1047
  SCOPED NON-PERMISSIVE PACKAGES
1048
1048
  ------------------------------
1049
- The exact @img/sharp-libvips-* 1.2.4 all-target family is LGPL-3.0-or-later. It is not distributed in OMP npm bundles or standalone assets: @huggingface/transformers is always external, installed into a user-side versioned runtime on demand, and sharp is stubbed for speech/text. The dynamically loaded native library remains replaceable. Corresponding source is available from https://github.com/lovell/sharp-libvips.git and upstream libvips sources.
1049
+ The exact @img/sharp-libvips-* 1.2.4 all-target family is LGPL-3.0-or-later. It is not distributed in Zeta npm bundles or standalone assets: @huggingface/transformers is always external, installed into a user-side versioned runtime on demand, and sharp is stubbed for speech/text. The dynamically loaded native library remains replaceable. Corresponding source is available from https://github.com/lovell/sharp-libvips.git and upstream libvips sources.
1050
1050
 
1051
1051
  lightningcss 1.32.0 and its exact target leaves are MPL-2.0 build-only dependencies. MPL is not globally allowed and these packages are absent from runtime artifacts.
1052
1052
 
@@ -28,11 +28,16 @@ export declare function withTimeout<T>(promise: Promise<T>, ms: number, timeout:
28
28
  * timer (`delayMs`, or a microtask at 0), and every push before it fires joins
29
29
  * the same batch and shares the same promise. Used to keep hot paths off
30
30
  * synchronous storage (prompt history, model perf).
31
+ *
32
+ * `unref` lets a long batch window not hold the process open; the owner must
33
+ * then {@link flush} on shutdown or the pending batch is lost.
31
34
  */
32
35
  export declare class AsyncDrain<T> {
33
36
  #private;
34
37
  readonly delayMs: number;
35
- constructor(delayMs?: number);
38
+ constructor(delayMs?: number, options?: {
39
+ unref?: boolean;
40
+ });
36
41
  /** Queue `value`; `hnd` receives the whole batch when the window closes. */
37
42
  push(value: T, hnd: (values: T[]) => Promise<void> | void): Promise<void>;
38
43
  /** Runs the pending batch handler immediately and returns its completion promise. */
@@ -1,12 +1,12 @@
1
1
  /**
2
- * Centralized path helpers for zeta config directories.
2
+ * Centralized path helpers for zeta-c config directories.
3
3
  *
4
4
  * Uses PI_CONFIG_DIR (default ".zeta") for the config root and
5
5
  * ZETA_CODING_AGENT_DIR to override the agent directory.
6
6
  *
7
7
  * On Linux, if XDG_DATA_HOME / XDG_STATE_HOME / XDG_CACHE_HOME environment
8
8
  * variables are set, paths are redirected to XDG-compliant locations under
9
- * $XDG_*_HOME/zeta/. This requires running `zeta config migrate` first to
9
+ * $XDG_*_HOME/zeta/. This requires running `zeta-c config migrate` first to
10
10
  * move data to the new locations. No filesystem existence checks are performed
11
11
  * — if the env var is set, zeta trusts that the migration has been done.
12
12
  */
@@ -26,7 +26,7 @@ export declare const CONFIG_DIR_NAME: string;
26
26
  export declare const MAIN_CONFIG_FILENAMES: readonly ["config.yml", "config.yaml"];
27
27
  /** Version (e.g. "1.0.0") */
28
28
  export declare const VERSION: string;
29
- /** Default User-Agent header string (e.g. "zeta/1.1.5") */
29
+ /** Default User-Agent header string (e.g. "zeta/17.2.12") */
30
30
  export declare const USER_AGENT: string;
31
31
  /** Minimum Bun version */
32
32
  export declare const MIN_BUN_VERSION: string;
@@ -44,7 +44,7 @@ export declare function normalizeProfileName(profile: string | undefined): strin
44
44
  * validation/normalization to {@link normalizeProfileName} (which throws on a
45
45
  * syntactically invalid value).
46
46
  */
47
- export declare function resolveProfileEnv(profile: string | undefined): string | undefined;
47
+ export declare function resolveProfileEnv(zeta: string | undefined): string | undefined;
48
48
  /** Profile-independent config root (~/.zeta), shared by every zeta profile. */
49
49
  export declare function getBaseConfigRoot(): string;
50
50
  export declare function resolveEquivalentPath(inputPath: string): string;
@@ -126,6 +126,7 @@ export declare function __resetProfileSnapshotForTests(): void;
126
126
  * back.
127
127
  */
128
128
  export declare function __resetDirsFromEnvForTests(): void;
129
+ /** Activate a named profile. Passing undefined or "default" returns to the default profile. */
129
130
  export declare function setProfile(profile: string | undefined): void;
130
131
  /** Get the active named profile. Undefined means the default profile. */
131
132
  export declare function getActiveProfile(): string | undefined;
@@ -141,7 +142,7 @@ export declare function getReportsDir(): string;
141
142
  export declare function getLogsDir(): string;
142
143
  /**
143
144
  * Local-timezone `YYYY-MM-DD` day key (zero-padded), formatted exactly like
144
- * the rotating log sink's file naming: log files are named `omp.<day>.<pid>.log`
145
+ * the rotating log sink's file naming: log files are named `zeta.<day>.<pid>.log`
145
146
  * with the LOCAL day, not the UTC day `toISOString()` yields. Anything that
146
147
  * computes "today's" log path or matches same-day log files by name must use
147
148
  * this key, or between local midnight and UTC midnight it points at files that
@@ -165,13 +166,45 @@ export declare function getPluginsDir(home?: string): string;
165
166
  export declare function getPluginsNodeModules(home?: string): string;
166
167
  /** Plugin manifest (~/.zeta/plugins/package.json). */
167
168
  export declare function getPluginsPackageJson(home?: string): string;
168
- /** Plugin lock file (~/.zeta/plugins/omp-plugins.lock.json). */
169
+ /** Plugin lock file (~/.zeta/plugins/omp-plugins.lock.json). The on-disk
170
+ * filename stays upstream-compatible so existing installs keep their runtime
171
+ * state (enabled features, versions) across upgrades — never rebrand it. */
169
172
  export declare function getPluginsLockfile(home?: string): string;
173
+ /**
174
+ * Normalize a plugin id into a safe single path segment (spec §4.2): lowercased,
175
+ * every character outside `[a-z0-9._-]` (including `/` and `\`, so scoped npm
176
+ * names like `@scope/name` collapse to `-scope-name`) replaced with `-`.
177
+ * Guarantees the result cannot traverse: it never contains a separator, and
178
+ * degenerate segments (`.`, `..`, empty) throw instead of escaping.
179
+ */
180
+ export declare function normalizePluginId(pluginId: string): string;
181
+ /** Per-plugin persistent data dir (~/.zeta/plugins/data/<id>). User data: cleared on uninstall, kept across upgrades. */
182
+ export declare function getPluginDataDir(pluginId: string, home?: string): string;
183
+ /** Per-plugin rebuildable cache dir (~/.zeta/plugins/cache/<id>). May be wiped by GC at any time. */
184
+ export declare function getPluginCacheDir(pluginId: string, home?: string): string;
185
+ /** Per-plugin runtime state dir (~/.zeta/plugins/state/<id>). Locks, cursors, migration markers. */
186
+ export declare function getPluginStateDir(pluginId: string, home?: string): string;
187
+ /**
188
+ * Adopt a legacy config file into its new home: one-shot best-effort copy
189
+ * (spec §4.5). Copies only when the target does not exist yet and the legacy
190
+ * file does; the legacy file is left in place for older versions sharing the
191
+ * profile. Returns whether a copy happened this call. Opportunistic: a copy
192
+ * race or unwritable target dir falls back to a fresh file at the new path.
193
+ */
194
+ export declare function adoptLegacyFile(legacyPath: string, targetPath: string): boolean;
195
+ /**
196
+ * One-shot legacy migration with a marker file (spec §4.5, the
197
+ * pi-messenger `migrations/` precedent, generalized): the first call copies
198
+ * `legacyPath` → `targetPath` (when legacy data exists) and writes a
199
+ * `migratedAt` marker at `markerPath`; every later call is a no-op regardless
200
+ * of filesystem state. Returns whether a copy happened this call.
201
+ */
202
+ export declare function adoptLegacyFileOnce(legacyPath: string, targetPath: string, markerPath: string): boolean;
170
203
  /** Get the remote mount directory (~/.zeta/remote). */
171
204
  export declare function getRemoteDir(): string;
172
205
  /**
173
206
  * Relocate the base directory for agent-managed worktrees (PR checkouts, task
174
- * isolation, and `omp worktree` cleanup all read the same base). Driven by the
207
+ * isolation, and `zeta worktree` cleanup all read the same base). Driven by the
175
208
  * `worktree.base` setting in coding-agent; pass `undefined`/empty to clear and
176
209
  * fall back to `OMP_WORKTREE_DIR` or the `~/.zeta/wt` default.
177
210
  *
@@ -277,10 +310,12 @@ export declare function getModelDbPath(agentDir?: string): string;
277
310
  export declare function getTinyModelsCacheDir(agentDir?: string): string;
278
311
  /** Get the document conversion cache directory (~/.zeta/agent/cache/document-conversions; XDG default: $XDG_CACHE_HOME/zeta/cache/document-conversions). */
279
312
  export declare function getDocumentConversionCacheDir(agentDir?: string): string;
280
- /** Get the per-project composer speculative cache directory (~/.zeta/agent/cache/composer; XDG default: $XDG_CACHE_HOME/zeta/cache/composer). */
281
- export declare function getComposerCacheDir(agentDir?: string): string;
282
313
  /** Get the composer speculative cache database (~/.zeta/agent/cache/composer.db; XDG default: $XDG_CACHE_HOME/zeta/cache/composer.db). */
283
314
  export declare function getComposerCacheDbPath(agentDir?: string): string;
315
+ /** Get the skill descriptions database (~/.zeta/agent/skill-descriptions.db; XDG default: $XDG_DATA_HOME/zeta/skill-descriptions.db). */
316
+ export declare function getSkillDescriptionsDbPath(agentDir?: string): string;
317
+ /** Get the text-predict engine state directory (~/.zeta/agent/predict/<method>; XDG default: $XDG_DATA_HOME/zeta/predict/<method>). Adopts legacy engine state on first XDG resolution. */
318
+ export declare function getPredictStateDir(agentDir: string | undefined, method: string): string;
284
319
  /** Get the sessions directory (~/.zeta/agent/sessions). */
285
320
  export declare function getSessionsDir(agentDir?: string): string;
286
321
  /** Get the content-addressed blob store directory (~/.zeta/agent/blobs). */
@@ -318,7 +353,7 @@ export declare function getTinyWorkerRuntimeDir(): string;
318
353
  export declare function getDaemonRuntimeRoot(): string;
319
354
  /** Get the daemon runtime directory for a project (~/.zeta/run/daemons/<hash>; XDG default: $XDG_STATE_HOME/zeta/run/daemons/<hash>). */
320
355
  export declare function getDaemonRuntimeDir(projectDir: string): string;
321
- /** Root directory containing every machine-global daemon service scope. */
356
+ /** Root directory containing every machine-global daemon service scope (~/.zeta/run/daemons/global; XDG default: $XDG_STATE_HOME/zeta/run/daemons/global). Shared across profiles. */
322
357
  export declare function getGlobalDaemonRuntimeRoot(): string;
323
358
  /** Get a profile-independent runtime directory for a machine-global daemon service. */
324
359
  export declare function getGlobalDaemonRuntimeDir(service: string): string;
@@ -328,6 +363,10 @@ export declare function getProviderInFlightRoot(): string;
328
363
  export declare function getMarketplacesRegistryPath(): string;
329
364
  /** Get the project-level Python modules directory (.zeta/modules). */
330
365
  export declare function getProjectModulesDir(cwd?: string): string;
366
+ /** Get the project-level tracking directory (<project>/.zeta/tracking). */
367
+ export declare function getProjectTrackingDir(cwd?: string): string;
368
+ /** Get the global tracking index path (~/.zeta/agent/tracking-index.json). */
369
+ export declare function getTrackingIndexPath(agentDir?: string): string;
331
370
  /** Get the project-level prompts directory (.zeta/prompts). */
332
371
  export declare function getProjectPromptsDir(cwd?: string): string;
333
372
  /** Get the project-level plugin overrides path (.zeta/plugin-overrides.json). */
@@ -358,9 +397,5 @@ export declare function getAppName(): string;
358
397
  * profile / `getInstallId` call order.
359
398
  */
360
399
  export declare function getInstallId(): string;
361
- /** Get the project-level tracking directory (<project>/.zeta/tracking). */
362
- export declare function getProjectTrackingDir(cwd?: string): string;
363
- /** Get the global tracking index path (~/.zeta/agent/tracking-index.json). */
364
- export declare function getTrackingIndexPath(agentDir?: string): string;
365
400
  /** Test-only: clear cached install id. Never call from production code. */
366
401
  export declare function __resetInstallIdCacheForTests(): void;
@@ -14,6 +14,8 @@ export interface FileLockHandle {
14
14
  }
15
15
  declare function getLockPath(filePath: string): string;
16
16
  declare function tryAcquireLock(lockPath: string): NativeFileLock | null;
17
+ /** Acquire an exclusive lease without waiting; `null` while another holder owns it. */
18
+ export declare function tryAcquireFileLock(filePath: string): FileLockHandle | null;
17
19
  /** Acquire an exclusive lease; callers must release it when their operation ends. */
18
20
  export declare function acquireFileLock(filePath: string, options?: FileLockOptions): Promise<FileLockHandle>;
19
21
  /** Run `fn` while holding an OS-backed exclusive lock for `filePath`. */
@@ -42,3 +42,5 @@ export declare function pluralize(label: string, count: number): string;
42
42
  * Format a ratio as a percentage.
43
43
  */
44
44
  export declare function formatPercent(ratio: number): string;
45
+ /** Round premium-request counters without losing legitimate fractional requests. */
46
+ export declare function normalizePremiumRequests(value: number): number;
@@ -1,14 +1,25 @@
1
+ /** Write batching for {@link RotatingFileSink}. */
2
+ export interface RotatingFileBatchOptions {
3
+ /** Longest a buffered record waits before an unref'd timer writes it. */
4
+ readonly intervalMs: number;
5
+ /** Buffered UTF-8 bytes at which the batch is written immediately. */
6
+ readonly maxBytes: number;
7
+ }
1
8
  /** Configuration for a process-local rotating file sink. */
2
9
  export interface RotatingFileOptions {
3
10
  readonly directory: string;
4
11
  readonly filenamePrefix: string;
5
12
  readonly filenameSuffix: string;
6
- readonly auditFile: string;
7
13
  readonly maxBytes: number;
8
14
  readonly maxFiles: number;
15
+ /**
16
+ * Buffer records and write them together instead of one write per record.
17
+ * Without it every record is written as it arrives.
18
+ */
19
+ readonly batch?: RotatingFileBatchOptions;
9
20
  /**
10
21
  * Called with the new active file path whenever the sink opens a different
11
- * file — construction, local-day rotation, and size rotation alike.
22
+ * file — first write, local-day rotation, and size rotation alike.
12
23
  *
13
24
  * Consumers that hold their own descriptor on the active log (the macOS
14
25
  * stderr guard dup2s it onto fd 2) use this to follow the sink instead of
@@ -16,12 +27,31 @@ export interface RotatingFileOptions {
16
27
  */
17
28
  readonly onRotate?: (filePath: string) => void;
18
29
  }
19
- /** Synchronous append sink with local-day and size rotation plus bounded retention. */
30
+ /**
31
+ * Synchronous append sink with local-day and size rotation plus bounded retention.
32
+ *
33
+ * Construction performs no filesystem I/O: the first file is created when the
34
+ * first record is written out, so a process that never logs leaves nothing on
35
+ * disk. Every file name embeds the owning process's suffix (its PID), so the
36
+ * set of files this sink created is tracked in memory; files left by earlier
37
+ * processes are pruned by name pattern in the logger, not by this sink.
38
+ *
39
+ * With {@link RotatingFileOptions.batch}, records are buffered and written in
40
+ * one `write` per batch. Buffered bytes already count toward the active file's
41
+ * size, and the buffer is written to its file before any rotation, so file
42
+ * selection matches an unbatched sink record for record.
43
+ */
20
44
  export declare class RotatingFileSink {
21
45
  #private;
22
46
  constructor(options: RotatingFileOptions);
23
- /** Append one already-formatted log record. */
24
- write(line: string): void;
25
- /** Stop accepting records. Synchronous writes require no drain phase. */
47
+ /**
48
+ * Append one already-formatted log record. A batching sink buffers it unless
49
+ * `flushNow` is set or the buffer reached its size threshold; either way the
50
+ * whole buffer, this record included, is then written in order.
51
+ */
52
+ write(line: string, flushNow?: boolean): void;
53
+ /** Write every buffered record to the active file now. */
54
+ flush(): void;
55
+ /** Write buffered records, then stop accepting records. */
26
56
  close(): void;
27
57
  }
@@ -11,10 +11,19 @@ export interface LogEvent {
11
11
  export type LogSink = (event: LogEvent) => void;
12
12
  /** Register an out-of-band log sink and return a disposer. */
13
13
  export declare function registerLogSink(sink: LogSink): () => void;
14
+ /**
15
+ * Write buffered file records to disk now. The file transport batches records
16
+ * (one write per second or per 64 KiB) and writes `warn`/`error` records
17
+ * immediately; process exit flushes on its own. Call this before handing the
18
+ * log file to something that reads it in-process. Never throws.
19
+ */
20
+ export declare function flush(): void;
14
21
  /**
15
22
  * Replace the active log transports. Pass `console: true, file: false` for
16
23
  * long-running services (the auth broker, etc.) that want their structured
17
24
  * logs piped into a process supervisor instead of the rotating file.
25
+ * The previous file transport writes its buffered records before closing, and
26
+ * the file level (`ZETA_LOG_LEVEL`) is re-read on each reconfiguration.
18
27
  */
19
28
  export declare function setTransports(opts: {
20
29
  console?: boolean;
@@ -184,16 +184,40 @@ export type TokensList = Token[] & {
184
184
  /** Context supplied to extension tokenizers. */
185
185
  export interface TokenizerThis {
186
186
  lexer: Lexer;
187
+ /**
188
+ * For an inline tokenizer, the whole inline source, such as a paragraph's text. The `src` it receives is the part
189
+ * of it before `end` that starts at `end - src.length`: inside a link label or the text of emphasis, `end` is where
190
+ * that text ends, right before its closer, and a token there must not reach past it. Inline tokenizers get one
191
+ * context object per inline source, the same for every call while the lexer works through it and the labels and
192
+ * emphasis inside it, so a tokenizer can keep state for it in a `WeakMap`.
193
+ */
194
+ source?: string;
195
+ /** For an inline tokenizer, where the text the lexer works through ends in `source` (see `source`). */
196
+ end?: number;
187
197
  }
188
198
  /** A tokenizer extension callback. */
189
199
  export type TokenizerExtensionFunction = (this: TokenizerThis, src: string, tokens: Token[] | TokensList) => Tokens.Generic | undefined;
190
200
  /** A tokenizer extension start hint. */
191
201
  export type TokenizerStartFunction = (this: TokenizerThis, src: string) => number | void;
202
+ /**
203
+ * An offset form of a start hint: the first index at or after `from` in `src` where the extension's tokenizer could
204
+ * match, or `undefined` (not -1) when there is none; anything else below `from` throws. The test at each index must
205
+ * read `src` only from that index on, so the answer for `from` is also the answer for every later offset up to it,
206
+ * and must still pass where `src` goes on past its end: for a link label or the text of emphasis, the lexer asks on
207
+ * the inline source it lies in, then asks again on the label or text at the index it got, if that lies inside.
208
+ */
209
+ export type TokenizerStartFromFunction = (this: TokenizerThis, src: string, from: number) => number | undefined;
192
210
  /** An inline or block tokenizer extension. */
193
211
  export interface TokenizerExtension {
194
212
  name: string;
195
213
  level: "block" | "inline";
196
214
  start?: TokenizerStartFunction;
215
+ /**
216
+ * Replaces `start` for inline text when present. Inline lexing calls it on the whole inline source and reuses
217
+ * the answer until lexing passes it, inside link labels and emphasis too, instead of calling `start` on the rest
218
+ * of the source at every text step, so a hint that stops at its answer keeps a long paragraph linear.
219
+ */
220
+ startFrom?: TokenizerStartFromFunction;
197
221
  tokenizer: TokenizerExtensionFunction;
198
222
  childTokens?: string[];
199
223
  }
@@ -0,0 +1,119 @@
1
+ /**
2
+ * Where brackets, emphasis, code spans, autolinks and inline HTML close in inline Markdown. The indexes are built
3
+ * over one root inline source, each on first use, and shared by every source lexed inside it (a link label, the text
4
+ * of emphasis), so lexing nested sources builds no index of its own and keeps none alive per nesting level.
5
+ */
6
+ export declare const PUNCTUATION: RegExp;
7
+ /** Whether an odd run of backslashes right before `index` escapes the character there. */
8
+ export declare function escapedAt(src: string, index: number): boolean;
9
+ export declare function canOpenDelimiter(src: string, index: number, width: number, marker: string, previous?: string): boolean;
10
+ export declare function canCloseDelimiter(src: string, index: number, marker: string): boolean;
11
+ /**
12
+ * Where a bracket opened in the root source closes. A search starts right after the opening bracket, and a bracket
13
+ * never begins an escape, so the escape pairing from there is the pairing from the start of the root source. The
14
+ * closer is then the first unescaped closing bracket at or after the start whose depth (openers minus closers
15
+ * before it, from the start of the root source) equals the depth at the start.
16
+ */
17
+ declare class BracketDepths {
18
+ #private;
19
+ constructor(src: string, open: number, close: number);
20
+ /** The offset of the bracket closing the one just before `start`, or -1. */
21
+ closeAfter(start: number): number;
22
+ /** Whether an unescaped closing bracket lies at or after `from`, right after a bracket, and before `to`. */
23
+ closesWithin(from: number, to: number): boolean;
24
+ }
25
+ /**
26
+ * The delimiters the emphasis rule's closer walk visits for one marker and width in the root source, typed the way
27
+ * the walk types them: a delimiter that can close is a closer, else one that can open is an opener. For width 1 they
28
+ * are every unescaped marker. For width 2 they are marker pairs taken two at a time from the start of each run of
29
+ * markers (after it when an odd run of backslashes escapes the start), which is where a walk that reaches the run
30
+ * from before it enters. The closer for an opener is then the first closer after it whose depth (openers minus
31
+ * closers before it) equals the depth where its walk begins, the first point where the walk's nesting count would
32
+ * drop below zero.
33
+ */
34
+ declare class EmphasisDelimiters {
35
+ #private;
36
+ constructor(src: string, marker: string, width: number);
37
+ /**
38
+ * Where the emphasis opened by the delimiter at `opener` closes in a source that ends at `end`, or -1 when the
39
+ * closer the walk meets first does not lie wholly before `end`. The walk reads nothing before its start. Past
40
+ * `end` it reads only for the source's last delimiter, which cannot open there and closes as it would at the end
41
+ * of the source: the sources inline lexing nests end right before a marker or "]", and the closing test reads
42
+ * either punctuation there like the end.
43
+ */
44
+ closeFor(opener: number, end: number): number;
45
+ }
46
+ /**
47
+ * The runs of backticks in the root source, for the code span rule. A span opened by `width` backticks closes at the
48
+ * first `width` backticks after its opening run that no backslash escapes: the start of a later run of at least
49
+ * `width`, or, in a run whose first backtick a backslash escapes, the backticks from its `width`th on, since the
50
+ * search for a closer goes on `width` past an escaped one.
51
+ */
52
+ declare class CodeRuns {
53
+ #private;
54
+ constructor(src: string);
55
+ /** Where the run of backticks holding the backtick at `at` ends. */
56
+ endOf(at: number): number;
57
+ /** Where the closer of a code span opened by `width` backticks that end at `from` starts, or -1. */
58
+ closerAfter(from: number, width: number): number;
59
+ }
60
+ /** The closer indexes of one root inline source, each built on first use. */
61
+ export declare class CloserIndexes {
62
+ #private;
63
+ constructor(src: string);
64
+ get square(): BracketDepths;
65
+ get round(): BracketDepths;
66
+ emphasis(marker: string, width: number): EmphasisDelimiters;
67
+ /**
68
+ * For each offset, where the HTML rule's scan for a tag's end from there stops: the first ">" outside quotes,
69
+ * where a quote runs to the next quote of its kind, or -1 at the end of the source.
70
+ */
71
+ get tagEnds(): Int32Array;
72
+ /** Every offset where "-->" starts, ascending. */
73
+ get commentEnds(): number[];
74
+ get codeRuns(): CodeRuns;
75
+ /**
76
+ * The first " " or ">" at or after `from`, or `Infinity`. Asked at offsets that never decrease, as the lexer's
77
+ * position does, so each search starts past the last one's answer.
78
+ */
79
+ spaceOrAngleFrom(from: number): number;
80
+ /** Every offset of a backslash before ASCII punctuation, ascending: where unescaping drops a backslash. */
81
+ get escapes(): number[];
82
+ }
83
+ /**
84
+ * The link, emphasis, code span, autolink and inline HTML rules' closer lookups for one inline source, which is the
85
+ * root source of `indexes` or a part of it that ends at `end`. Offsets are relative to `rest`, a suffix of the
86
+ * source. A closer the root source places at or past `end` does not exist in the source.
87
+ */
88
+ export declare class InlineClosers {
89
+ #private;
90
+ /** Where the source ends in the root source. */
91
+ readonly end: number;
92
+ constructor(indexes: CloserIndexes, end: number);
93
+ /** The closers of a part of `rest` that ends at `end`, lexed as its own source. */
94
+ nested(rest: string, end: number): InlineClosers;
95
+ /** Where the "[" just before `start` closes, or -1. */
96
+ closeSquare(rest: string, start: number): number;
97
+ /** Where the "(" just before `start` closes, or -1. */
98
+ closeRound(rest: string, start: number): number;
99
+ /** Whether `rest.slice(from, to)`, which starts right after a "[", holds an unescaped "]". */
100
+ squareCloserWithin(rest: string, from: number, to: number): boolean;
101
+ /** Whether `rest.slice(from, to)` holds a backslash before ASCII punctuation, which unescaping drops. */
102
+ escapeWithin(rest: string, from: number, to: number): boolean;
103
+ /** Where the emphasis opened by the `width` markers at the start of `rest` closes, or -1. */
104
+ closeEmphasis(rest: string, marker: string, width: number): number;
105
+ /** How many backticks start `rest`, which starts with one. */
106
+ codeOpenerWidth(rest: string): number;
107
+ /** Where the code span opened by the `width` backticks at the start of `rest` closes, or -1. */
108
+ closeCode(rest: string, width: number): number;
109
+ /**
110
+ * Where the URL autolink opened at the start of `rest` ends, its scheme ending at `from`: the first " " or ">"
111
+ * after the scheme if it is a ">" with text before it, or -1. One past the end of `rest` reads as no ">".
112
+ */
113
+ closeUrlAutolink(rest: string, from: number): number;
114
+ /** Where the HTML tag opened by the "<" at the start of `rest` ends: the first ">" after it outside quotes, or -1. */
115
+ closeTag(rest: string): number;
116
+ /** Where the first "-->" after the "<!--" at the start of `rest` starts, or -1. */
117
+ closeComment(rest: string): number;
118
+ }
119
+ export {};
@@ -0,0 +1,70 @@
1
+ /**
2
+ * The plain-text step of inline lexing over one inline source: where text can end ({@link TextStops}, which also
3
+ * tells the bare-URL rule where it can match) and how it is appended ({@link TextRun}).
4
+ */
5
+ import type { Lexer, Token, TokenizerExtension, TokenizerStartFromFunction } from "./core.js";
6
+ /**
7
+ * The places where plain text can end in one root inline source, as offsets into it, shared by every source lexed
8
+ * inside it (a link label, the text of emphasis). Each stop is the first offset at or after a search offset where a
9
+ * test passes, and each test reads the source only from its own offset on, so a found stop stays the answer for every
10
+ * later search offset up to it. A stop is searched for again only after lexing passes it, starting past it, so all
11
+ * the text steps of a paragraph, at every nesting level, read it a bounded number of times instead of once per step.
12
+ * Search offsets never decrease: the lexer asks at its position for the bare-URL rule and one past it for the text
13
+ * step, its position only grows, and a nested source is lexed from its start to its end before the lexer goes on
14
+ * past it.
15
+ *
16
+ * A nested source is the part of the root source before `end`, and a stop that holds there is a stop of the root
17
+ * source whose match ends by `end`. The first root stop after an offset tells whether there is one: when its match
18
+ * runs past `end`, no stop that ends by `end` follows it, for the reason each search gives.
19
+ */
20
+ declare class RootTextStops {
21
+ #private;
22
+ constructor(src: string);
23
+ /** The first character that can start another token at or after `from`. */
24
+ tokenCharFrom(from: number): number;
25
+ /** The first bare-URL scheme at or after `from` that ends by `end`. */
26
+ schemeFrom(from: number, end: number): number;
27
+ /** The "@" of the run {@link mailFrom} last found. */
28
+ get mailAt(): number;
29
+ /** The first offset at or after `from` that starts a match of `/[A-Za-z0-9._+-]+@/` whose "@" is before `end`. */
30
+ mailFrom(from: number, end: number): number;
31
+ /** The first offset at or after `from` that starts a match of `/(?: {2,}|\\)\n/` whose "\n" is before `end`. */
32
+ hardBreakFrom(from: number, end: number): number;
33
+ /** Where `extension`'s `startFrom` hint points at or after `from`, asked again only once lexing passes it. */
34
+ startFrom(extension: TokenizerExtension, startFrom: TokenizerStartFromFunction, lexer: Lexer, from: number): number;
35
+ }
36
+ /**
37
+ * The places where one inline source's plain text can end, as offsets into that source: its root source's stops
38
+ * that hold before the source's end. The source is a root source, or a part of one lexed as its own source (a link
39
+ * label, the text of emphasis), which ends right before the delimiter that closes it.
40
+ */
41
+ export declare class TextStops {
42
+ #private;
43
+ /** The stops of `src`, a root source, or else the part of `root`'s source that ends at `end`. */
44
+ constructor(src: string, lexer: Lexer, root?: RootTextStops, end?: number);
45
+ /** The stops of `src`, the part of this source's root source that ends at `end`, lexed as its own source. */
46
+ nested(src: string, end: number): TextStops;
47
+ /**
48
+ * Whether the bare-URL rule can match at `pos`: its first alternative needs a scheme there, and its second a
49
+ * run of e-mail local characters from `pos` to an "@" (the only "@" such a run can reach) with a dotted domain
50
+ * right after that "@", which is the same test for every offset of the run.
51
+ */
52
+ bareUrlCanStart(pos: number): boolean;
53
+ /**
54
+ * Length of the plain text at the start of `rest`, a suffix of the source: the distance to the nearest later
55
+ * offset where another token could start or an inline extension's start hint points.
56
+ */
57
+ textLength(rest: string): number;
58
+ }
59
+ /**
60
+ * Appends plain text to one inline source's token list, merging into a text token before it as marked does. A text
61
+ * token this run created grows by taking a longer slice of the source instead of by concatenation, so its raw stays
62
+ * a flat string: reading its last character (the emphasis rule's look-behind) does not copy the whole token.
63
+ */
64
+ export declare class TextRun {
65
+ #private;
66
+ constructor(src: string);
67
+ /** Appends the source text from `start` to `end`. */
68
+ append(tokens: Token[], start: number, end: number): void;
69
+ }
70
+ export {};
@@ -41,5 +41,27 @@ export declare function mathOpenerAt(source: string, at: number): MathOpener | u
41
41
  * though a backslash precedes it.
42
42
  */
43
43
  export declare function mathSpanAt(source: string, at: number, from?: number): MathSpan | undefined;
44
+ /**
45
+ * The math spans of one source, found as {@link mathSpanAt} finds them with the escape scan stopping at the opener
46
+ * (the caller has consumed the escapes before it). A closer scan also answers for the openers of its kind that it
47
+ * passed, which meet the same closer or none, so asking at every opener of a long run reads the run once.
48
+ */
49
+ export declare class MathSpans {
50
+ #private;
51
+ constructor(source: string);
52
+ /** The span opened at `at` that ends by `end`, or `undefined`. */
53
+ spanAt(at: number, end?: number): MathSpan | undefined;
54
+ }
55
+ /**
56
+ * The span opened at the start of `src`, with `end` counted from there, for a marked inline tokenizer: `src` is the
57
+ * part of `context.source` before `context.end` that the tokenizer received, and the scans are remembered per
58
+ * context, which marked keeps for one inline source and the link labels and emphasis inside it. Only a span that ends
59
+ * by `context.end` counts. Inside a label or emphasis, `context.end` holds its closer (`]`, `*` or `_`), which is not
60
+ * a digit, so a `$` right before it closes there as it would at the end of the text.
61
+ */
62
+ export declare function mathSpanInContext(context: {
63
+ source?: string;
64
+ end?: number;
65
+ }, src: string): MathSpan | undefined;
44
66
  /** The own-line display block starting at offset 0, or `undefined`. */
45
67
  export declare function mathBlockAt(source: string): MathBlock | undefined;
@@ -46,6 +46,10 @@ export declare const NATIVE_PROCESS_EXIT: unique symbol;
46
46
  * guard and loops the rejection storm (#11789). After restoring, `reallyExit`
47
47
  * (the low-level primitive) is preferred; `process.exit` and finally `SIGKILL`
48
48
  * are fallbacks so a poisoned or absent chain can never leave the process alive.
49
+ *
50
+ * Buffered log records are written first: `reallyExit` skips the `exit` event
51
+ * the logger otherwise flushes on, which would drop the last batch on every
52
+ * signal and fatal exit.
49
53
  */
50
54
  export declare function exitProcess(code: number): never;
51
55
  /** User-facing command printed before fatal cleanup so interrupted work can be resumed. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@linxiraos/pi-utils",
3
- "version": "1.1.23",
3
+ "version": "1.1.25",
4
4
  "description": "Shared utilities for pi packages",
5
5
  "keywords": [
6
6
  "utilities",
@@ -54,7 +54,7 @@
54
54
  "fmt": "oxfmt --no-error-on-unmatched-pattern 'src/**/*.{ts,tsx}' '{test,bench,examples,scripts}/**/*.ts' '*.ts'"
55
55
  },
56
56
  "dependencies": {
57
- "@linxiraos/pi-natives": "1.1.23"
57
+ "@linxiraos/pi-natives": "1.1.25"
58
58
  },
59
59
  "devDependencies": {
60
60
  "@types/bun": "^1.3.14"
package/src/async.ts CHANGED
@@ -91,13 +91,22 @@ export function withTimeout<T>(
91
91
  * timer (`delayMs`, or a microtask at 0), and every push before it fires joins
92
92
  * the same batch and shares the same promise. Used to keep hot paths off
93
93
  * synchronous storage (prompt history, model perf).
94
+ *
95
+ * `unref` lets a long batch window not hold the process open; the owner must
96
+ * then {@link flush} on shutdown or the pending batch is lost.
94
97
  */
95
98
  export class AsyncDrain<T> {
96
99
  #queue?: T[];
97
100
  #promise = Promise.resolve();
98
101
  #flush?: () => void;
102
+ readonly #unref: boolean;
99
103
 
100
- constructor(readonly delayMs: number = 0) {}
104
+ constructor(
105
+ readonly delayMs: number = 0,
106
+ options?: { unref?: boolean },
107
+ ) {
108
+ this.#unref = options?.unref === true;
109
+ }
101
110
 
102
111
  /** Queue `value`; `hnd` receives the whole batch when the window closes. */
103
112
  push(value: T, hnd: (values: T[]) => Promise<void> | void): Promise<void> {
@@ -119,6 +128,7 @@ export class AsyncDrain<T> {
119
128
  };
120
129
  if (this.delayMs > 0) {
121
130
  const timer = setTimeout(exec, this.delayMs);
131
+ if (this.#unref) timer.unref();
122
132
  this.#flush = () => {
123
133
  clearTimeout(timer);
124
134
  exec();