@getfamiliar/shared 0.1.2

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.
Files changed (83) hide show
  1. package/build/AgentRun.d.ts +187 -0
  2. package/build/AgentRun.js +1 -0
  3. package/build/AgentRunBus.d.ts +147 -0
  4. package/build/AgentRunBus.js +362 -0
  5. package/build/Calendar.d.ts +450 -0
  6. package/build/Calendar.js +37 -0
  7. package/build/ChatMessage.d.ts +53 -0
  8. package/build/ChatMessage.js +11 -0
  9. package/build/ChatMessageBus.d.ts +110 -0
  10. package/build/ChatMessageBus.js +257 -0
  11. package/build/Config.d.ts +83 -0
  12. package/build/Config.js +1 -0
  13. package/build/ContainerTool.d.ts +34 -0
  14. package/build/ContainerTool.js +1 -0
  15. package/build/CronExpression.d.ts +53 -0
  16. package/build/CronExpression.js +77 -0
  17. package/build/Event.d.ts +280 -0
  18. package/build/Event.js +14 -0
  19. package/build/EventBus.d.ts +198 -0
  20. package/build/EventBus.js +482 -0
  21. package/build/HandlerCatalog.d.ts +75 -0
  22. package/build/HandlerCatalog.js +165 -0
  23. package/build/InferenceEvent.d.ts +30 -0
  24. package/build/InferenceEvent.js +1 -0
  25. package/build/InferenceEventBus.d.ts +39 -0
  26. package/build/InferenceEventBus.js +80 -0
  27. package/build/Mail.d.ts +250 -0
  28. package/build/Mail.js +73 -0
  29. package/build/MailStyleTemplate.d.ts +71 -0
  30. package/build/MailStyleTemplate.js +47 -0
  31. package/build/ModelMetaData.d.ts +63 -0
  32. package/build/ModelMetaData.js +12 -0
  33. package/build/PathGlob.d.ts +20 -0
  34. package/build/PathGlob.js +33 -0
  35. package/build/Plugin.d.ts +709 -0
  36. package/build/Plugin.js +29 -0
  37. package/build/PostgresConnection.d.ts +75 -0
  38. package/build/PostgresConnection.js +162 -0
  39. package/build/ScheduledSubagent.d.ts +49 -0
  40. package/build/ScheduledSubagent.js +1 -0
  41. package/build/ScheduledSubagentBus.d.ts +94 -0
  42. package/build/ScheduledSubagentBus.js +187 -0
  43. package/build/Schema.d.ts +84 -0
  44. package/build/Schema.js +570 -0
  45. package/build/StepResult.d.ts +103 -0
  46. package/build/StepResult.js +1 -0
  47. package/build/StepResultBus.d.ts +64 -0
  48. package/build/StepResultBus.js +169 -0
  49. package/build/Timezone.d.ts +58 -0
  50. package/build/Timezone.js +84 -0
  51. package/build/ToolCall.d.ts +25 -0
  52. package/build/ToolCall.js +1 -0
  53. package/build/ToolCallBus.d.ts +65 -0
  54. package/build/ToolCallBus.js +110 -0
  55. package/build/ToolLevel.d.ts +20 -0
  56. package/build/ToolLevel.js +2 -0
  57. package/build/ToolRunner.d.ts +118 -0
  58. package/build/ToolRunner.js +192 -0
  59. package/build/ToolsExpressionUtils.d.ts +80 -0
  60. package/build/ToolsExpressionUtils.js +102 -0
  61. package/build/WorkspaceFile.d.ts +104 -0
  62. package/build/WorkspaceFile.js +7 -0
  63. package/build/estimateTokens.d.ts +16 -0
  64. package/build/estimateTokens.js +18 -0
  65. package/build/index.d.ts +37 -0
  66. package/build/index.js +26 -0
  67. package/build/logging/Logger.d.ts +66 -0
  68. package/build/logging/Logger.js +90 -0
  69. package/build/markdownHtml.d.ts +14 -0
  70. package/build/markdownHtml.js +21 -0
  71. package/build/markdownTerminal.d.ts +14 -0
  72. package/build/markdownTerminal.js +376 -0
  73. package/build/testing/MockAgentRunBus.d.ts +52 -0
  74. package/build/testing/MockAgentRunBus.js +175 -0
  75. package/build/testing/MockBusStore.d.ts +22 -0
  76. package/build/testing/MockBusStore.js +22 -0
  77. package/build/testing/MockEventBus.d.ts +20 -0
  78. package/build/testing/MockEventBus.js +57 -0
  79. package/build/testing/MockLogger.d.ts +52 -0
  80. package/build/testing/MockLogger.js +65 -0
  81. package/build/testing/index.d.ts +4 -0
  82. package/build/testing/index.js +4 -0
  83. package/package.json +63 -0
@@ -0,0 +1,118 @@
1
+ /**
2
+ * Tool-call runner: the shared machinery every agent-facing tool uses
3
+ * to package its result. Three flavors — JSON object, JSON Lines, free
4
+ * text — picked by the tool's `execute` based on what shape it wants
5
+ * the agent to see.
6
+ *
7
+ * Two responsibilities only:
8
+ *
9
+ * - **Truncation / offloading.** When the serialized result's
10
+ * estimated token count (see {@link estimateTokens}) exceeds
11
+ * {@link ToolRunContext.limit}, the runner spills the *full*
12
+ * result to a scratch file via {@link ToolRunContext.spill} and
13
+ * returns either a sentinel ({@link OffloadedJson} for the JSON
14
+ * runner) or a partial-with-marker (JSONL / text runners). The
15
+ * spilled file always carries the complete content so the agent
16
+ * can `fs_read` it for the missing portion.
17
+ * - **Letting throws propagate.** Failure is signalled by throwing.
18
+ * The Vercel AI SDK turns any throw into a `tool-error` block;
19
+ * tools that want a clean message catch and re-throw a
20
+ * {@link ToolError} inside their own body. The runners do not
21
+ * transform errors.
22
+ *
23
+ * Note: success bodies are *not* wrapped in `{ok:true,...}`. The agent
24
+ * receives the bare value (object, JSONL string, or text string).
25
+ */
26
+ import { Buffer } from "node:buffer";
27
+ /**
28
+ * Default token cap when no config override and no frontmatter override
29
+ * is set. Also doubles as the upper bound in the model-relative offload
30
+ * threshold `min(0.25 * contextLimit, cap)` computed in the agent runner,
31
+ * and as the host gateway's fallback when a caller sends no limit.
32
+ */
33
+ export declare const DEFAULT_TOOL_CALL_OFFLOADING_LIMIT = 16000;
34
+ /**
35
+ * Convenience throwable for tool bodies that want to attach a stable
36
+ * machine-readable `code` (e.g. `"ErrorItemNotFound"`) and optional
37
+ * HTTP-flavored `status` alongside the human message. The Vercel SDK
38
+ * doesn't surface these fields in `tool-error` blocks, but the gateway
39
+ * transports them across HTTP so transcripts and tests can match on
40
+ * `code` instead of fuzzy-matching on `message`.
41
+ *
42
+ * Plain `Error` throws work too — they propagate verbatim with
43
+ * `code = "ToolError"` synthesized at the wire boundary.
44
+ */
45
+ export declare class ToolError extends Error {
46
+ readonly code: string;
47
+ readonly status?: number | undefined;
48
+ constructor(code: string, message: string, status?: number | undefined);
49
+ }
50
+ /**
51
+ * Per-call context handed to every runner. Constructed by whichever
52
+ * side (host gateway / container tool wrapper) owns the agentrun.
53
+ *
54
+ * - `limit`: token budget for the inline response (compared against
55
+ * {@link estimateTokens} of the serialized result). Resolved per-call
56
+ * from handler frontmatter → env var / config → default.
57
+ * - `spill`: writes the full result under `/scratch/<event-id>/` and
58
+ * returns the absolute path the agent can later `fs_read`.
59
+ * Implementations dedupe basenames so concurrent calls don't
60
+ * clobber each other.
61
+ */
62
+ export interface ToolRunContext {
63
+ readonly limit: number;
64
+ spill(suggestedName: string, contents: Buffer): Promise<string>;
65
+ }
66
+ /**
67
+ * Sentinel returned by {@link runJsonTool} when the serialized result
68
+ * exceeds the byte budget. The agent should `fs_read` the path for
69
+ * the actual content.
70
+ */
71
+ export interface OffloadedJson {
72
+ readonly truncated: true;
73
+ readonly fullResultAt: string;
74
+ readonly reason: string;
75
+ }
76
+ /**
77
+ * Run a tool whose result is a single JSON object. If the
78
+ * `JSON.stringify`'d result's estimated token count is at or below the
79
+ * limit, returns the object verbatim. Otherwise spills the full JSON to
80
+ * scratch and returns {@link OffloadedJson}.
81
+ *
82
+ * No partial JSON is exposed — an oversized result is either fully
83
+ * inline or fully offloaded. The body is expected to throw on failure;
84
+ * the runner does not catch.
85
+ */
86
+ export declare function runJsonTool(body: () => Promise<object>, ctx: ToolRunContext): Promise<object>;
87
+ /**
88
+ * Run a tool whose result is a stream of JSON objects, serialized as
89
+ * newline-delimited JSON. If the total fits within the limit, returns
90
+ * all lines joined with `\n`. Otherwise spills the full JSONL to
91
+ * scratch and returns the leading whole lines that fit, followed by a
92
+ * marker line:
93
+ *
94
+ * `{"truncated":true,"fullResultAt":"<path>","omittedLines":<N>}`
95
+ *
96
+ * Edge case: when even the first line plus the marker exceeds the
97
+ * limit, only the marker is returned (`omittedLines` then equals the
98
+ * total line count).
99
+ */
100
+ export declare function runJsonLinesTool(body: () => Promise<Iterable<object> | AsyncIterable<object>>, ctx: ToolRunContext): Promise<string>;
101
+ /**
102
+ * Run a tool whose result is free text. If the text fits the limit,
103
+ * returns it verbatim. Otherwise spills the full text to scratch and
104
+ * returns a truncated prefix followed by a footer pointing at the
105
+ * scratch path:
106
+ *
107
+ * `<truncated text>\n\n[truncated; full result at <path>]`
108
+ *
109
+ * The truncation respects UTF-8 code-point boundaries so multi-byte
110
+ * characters never get split.
111
+ */
112
+ export declare function runTextTool(body: () => Promise<string>, ctx: ToolRunContext): Promise<string>;
113
+ /**
114
+ * Truncate `str` to at most `maxBytes` UTF-8 bytes without splitting a
115
+ * code point. Surrogate pairs (code points beyond the BMP) are kept
116
+ * together.
117
+ */
118
+ export declare function truncateUtf8(str: string, maxBytes: number): string;
@@ -0,0 +1,192 @@
1
+ /**
2
+ * Tool-call runner: the shared machinery every agent-facing tool uses
3
+ * to package its result. Three flavors — JSON object, JSON Lines, free
4
+ * text — picked by the tool's `execute` based on what shape it wants
5
+ * the agent to see.
6
+ *
7
+ * Two responsibilities only:
8
+ *
9
+ * - **Truncation / offloading.** When the serialized result's
10
+ * estimated token count (see {@link estimateTokens}) exceeds
11
+ * {@link ToolRunContext.limit}, the runner spills the *full*
12
+ * result to a scratch file via {@link ToolRunContext.spill} and
13
+ * returns either a sentinel ({@link OffloadedJson} for the JSON
14
+ * runner) or a partial-with-marker (JSONL / text runners). The
15
+ * spilled file always carries the complete content so the agent
16
+ * can `fs_read` it for the missing portion.
17
+ * - **Letting throws propagate.** Failure is signalled by throwing.
18
+ * The Vercel AI SDK turns any throw into a `tool-error` block;
19
+ * tools that want a clean message catch and re-throw a
20
+ * {@link ToolError} inside their own body. The runners do not
21
+ * transform errors.
22
+ *
23
+ * Note: success bodies are *not* wrapped in `{ok:true,...}`. The agent
24
+ * receives the bare value (object, JSONL string, or text string).
25
+ */
26
+ import { Buffer } from "node:buffer";
27
+ import { estimateTokens } from "./estimateTokens.js";
28
+ /**
29
+ * Default token cap when no config override and no frontmatter override
30
+ * is set. Also doubles as the upper bound in the model-relative offload
31
+ * threshold `min(0.25 * contextLimit, cap)` computed in the agent runner,
32
+ * and as the host gateway's fallback when a caller sends no limit.
33
+ */
34
+ export const DEFAULT_TOOL_CALL_OFFLOADING_LIMIT = 16000;
35
+ /**
36
+ * Convenience throwable for tool bodies that want to attach a stable
37
+ * machine-readable `code` (e.g. `"ErrorItemNotFound"`) and optional
38
+ * HTTP-flavored `status` alongside the human message. The Vercel SDK
39
+ * doesn't surface these fields in `tool-error` blocks, but the gateway
40
+ * transports them across HTTP so transcripts and tests can match on
41
+ * `code` instead of fuzzy-matching on `message`.
42
+ *
43
+ * Plain `Error` throws work too — they propagate verbatim with
44
+ * `code = "ToolError"` synthesized at the wire boundary.
45
+ */
46
+ export class ToolError extends Error {
47
+ code;
48
+ status;
49
+ constructor(code, message, status) {
50
+ super(message);
51
+ this.code = code;
52
+ this.status = status;
53
+ this.name = "ToolError";
54
+ }
55
+ }
56
+ /**
57
+ * Run a tool whose result is a single JSON object. If the
58
+ * `JSON.stringify`'d result's estimated token count is at or below the
59
+ * limit, returns the object verbatim. Otherwise spills the full JSON to
60
+ * scratch and returns {@link OffloadedJson}.
61
+ *
62
+ * No partial JSON is exposed — an oversized result is either fully
63
+ * inline or fully offloaded. The body is expected to throw on failure;
64
+ * the runner does not catch.
65
+ */
66
+ export async function runJsonTool(body, ctx) {
67
+ const result = await body();
68
+ const serialized = JSON.stringify(result);
69
+ if (estimateTokens(serialized) <= ctx.limit) {
70
+ return result;
71
+ }
72
+ const fullResultAt = await ctx.spill("result.json", Buffer.from(serialized, "utf8"));
73
+ return {
74
+ truncated: true,
75
+ fullResultAt,
76
+ reason: `result exceeded ${ctx.limit} tokens — fs_read the path (it's paginated)`,
77
+ };
78
+ }
79
+ /**
80
+ * Run a tool whose result is a stream of JSON objects, serialized as
81
+ * newline-delimited JSON. If the total fits within the limit, returns
82
+ * all lines joined with `\n`. Otherwise spills the full JSONL to
83
+ * scratch and returns the leading whole lines that fit, followed by a
84
+ * marker line:
85
+ *
86
+ * `{"truncated":true,"fullResultAt":"<path>","omittedLines":<N>}`
87
+ *
88
+ * Edge case: when even the first line plus the marker exceeds the
89
+ * limit, only the marker is returned (`omittedLines` then equals the
90
+ * total line count).
91
+ */
92
+ export async function runJsonLinesTool(body, ctx) {
93
+ const source = await body();
94
+ const serialized = [];
95
+ for await (const item of asAsyncIterable(source)) {
96
+ serialized.push(JSON.stringify(item));
97
+ }
98
+ const inline = serialized.join("\n");
99
+ if (estimateTokens(inline) <= ctx.limit) {
100
+ return inline;
101
+ }
102
+ const fullText = inline;
103
+ const fullResultAt = await ctx.spill("result.jsonl", Buffer.from(fullText, "utf8"));
104
+ // Budget the marker assuming worst-case omittedLines (the total
105
+ // line count). Marker can only get shorter as `omittedLines`
106
+ // shrinks, so this is a safe upper bound.
107
+ const upperBoundMarker = JSON.stringify({
108
+ truncated: true,
109
+ fullResultAt,
110
+ omittedLines: serialized.length,
111
+ });
112
+ const markerBudget = estimateTokens(upperBoundMarker);
113
+ const kept = [];
114
+ let usedTokens = 0;
115
+ for (const line of serialized) {
116
+ const separator = kept.length > 0 ? 1 : 0; // newline before this line (~1 token)
117
+ const newlineBeforeMarker = 1; // newline between last kept line and marker
118
+ const addTokens = separator + estimateTokens(line);
119
+ if (usedTokens + addTokens + newlineBeforeMarker + markerBudget > ctx.limit) {
120
+ break;
121
+ }
122
+ kept.push(line);
123
+ usedTokens += addTokens;
124
+ }
125
+ const omittedLines = serialized.length - kept.length;
126
+ const marker = JSON.stringify({ truncated: true, fullResultAt, omittedLines });
127
+ if (kept.length === 0) {
128
+ return marker;
129
+ }
130
+ return `${kept.join("\n")}\n${marker}`;
131
+ }
132
+ /**
133
+ * Run a tool whose result is free text. If the text fits the limit,
134
+ * returns it verbatim. Otherwise spills the full text to scratch and
135
+ * returns a truncated prefix followed by a footer pointing at the
136
+ * scratch path:
137
+ *
138
+ * `<truncated text>\n\n[truncated; full result at <path>]`
139
+ *
140
+ * The truncation respects UTF-8 code-point boundaries so multi-byte
141
+ * characters never get split.
142
+ */
143
+ export async function runTextTool(body, ctx) {
144
+ const text = await body();
145
+ if (estimateTokens(text) <= ctx.limit) {
146
+ return text;
147
+ }
148
+ const fullResultAt = await ctx.spill("result.txt", Buffer.from(text, "utf8"));
149
+ const footer = `\n\n[truncated; full result at ${fullResultAt}]`;
150
+ const footerTokens = estimateTokens(footer);
151
+ // The budget is in tokens; `truncateUtf8` bounds by character count.
152
+ // estimateTokens(s) = ceil(len/4), so the char budget for the prefix
153
+ // is the remaining token budget × 4. Bounding `truncateUtf8` by that
154
+ // char count is slightly conservative for multi-byte text — fine for
155
+ // a placeholder estimator backing a truncation marker.
156
+ const prefixCharBudget = Math.max(0, (ctx.limit - footerTokens) * 4);
157
+ const prefix = truncateUtf8(text, prefixCharBudget);
158
+ return `${prefix}${footer}`;
159
+ }
160
+ /**
161
+ * Truncate `str` to at most `maxBytes` UTF-8 bytes without splitting a
162
+ * code point. Surrogate pairs (code points beyond the BMP) are kept
163
+ * together.
164
+ */
165
+ export function truncateUtf8(str, maxBytes) {
166
+ if (maxBytes <= 0) {
167
+ return "";
168
+ }
169
+ let bytes = 0;
170
+ let charIdx = 0;
171
+ while (charIdx < str.length) {
172
+ const cp = str.codePointAt(charIdx);
173
+ if (cp === undefined) {
174
+ break;
175
+ }
176
+ const cpBytes = cp < 0x80 ? 1 : cp < 0x800 ? 2 : cp < 0x10000 ? 3 : 4;
177
+ if (bytes + cpBytes > maxBytes) {
178
+ break;
179
+ }
180
+ bytes += cpBytes;
181
+ charIdx += cp >= 0x10000 ? 2 : 1;
182
+ }
183
+ return str.slice(0, charIdx);
184
+ }
185
+ /** Lift a sync iterable into an async one so we can `for await` over both. */
186
+ async function* asAsyncIterable(source) {
187
+ if (Symbol.asyncIterator in source) {
188
+ yield* source;
189
+ return;
190
+ }
191
+ yield* source;
192
+ }
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Cross-cutting constants for tool-group naming. A handler's `tools:`
3
+ * frontmatter lists explicit tool names, `*`-globs, or group names;
4
+ * the resolver lives in the container
5
+ * (`container/src/tools/ToolsExpressionParser.ts`). The pieces
6
+ * re-exported here are the ones the host-side `mcp.yml` linter and the
7
+ * plugin tool registry also need to enforce — namely the reserved
8
+ * group names and the shape every declarable name must match.
9
+ *
10
+ * Group names are otherwise *open*: any identifier matching
11
+ * {@link IDENT_PATTERN} is a valid group, and a tool joins it by
12
+ * listing the name in `PluginTool.groups`. The "predefined" groups
13
+ * (`core`, `fs`, `reflection`, …) are not hardcoded sets — they
14
+ * emerge from the union of every tool that declares them.
15
+ */
16
+ /** Built-in group: every key in the available pool (system ∪ MCP). Reserved. */
17
+ export declare const ALL_GROUP_NAME = "all";
18
+ /** Built-in group: every MCP-tool key. Reserved. */
19
+ export declare const MCP_GROUP_NAME = "mcp";
20
+ /** Built-in group: empty set — used to override a parent's `tools:`. Reserved. */
21
+ export declare const NONE_GROUP_NAME = "none";
22
+ /**
23
+ * Reserved plugin id for host-owned tools registered without a
24
+ * plugin-id prefix (the "bare-key" path — `cal_get_events` rather
25
+ * than `core_cal_get_events`). It is a **registration sentinel, not
26
+ * an addressable tool group**: it deliberately collides in spelling
27
+ * with the curated `core` group, so it must never be turned into a
28
+ * plugin-id auto-group (doing so would shadow the curated `core`
29
+ * group — the union of container built-ins and tools declaring
30
+ * `groups: ["core"]` — with the full set of host-owned tools). Host
31
+ * built-in tools join curated groups through their own `groups`
32
+ * field (`["cal"]`, `["mail"]`, `["reflection"]`, …) instead.
33
+ */
34
+ export declare const CORE_PLUGIN_ID = "core";
35
+ /**
36
+ * Names the resolver handles directly and that therefore cannot
37
+ * appear as a declarable group, an MCP id, or a plugin id. Using one
38
+ * as an MCP id in `mcp.yml` is rejected by the loader; declaring it in
39
+ * `PluginTool.groups` is rejected by the plugin tool registry.
40
+ */
41
+ export declare const RESERVED_GROUP_NAMES: ReadonlySet<string>;
42
+ /**
43
+ * Pattern any group name — declarable, MCP-id-derived, or
44
+ * plugin-id-derived — must match.
45
+ *
46
+ * Lowercase alphanumeric only, leading letter, no underscores and
47
+ * no hyphens. The exclusion of `_` is load-bearing: tool keys are
48
+ * always shaped `${id}_${name}` (plugin tools) or a bare alnum-only
49
+ * stem (built-ins) and contain at least one underscore in the
50
+ * namespaced form, so an underscore-free bareword can never collide
51
+ * with a tool key — the tool-group vs tool-pattern classifier in
52
+ * `resolveTools` uses that to tell them apart structurally.
53
+ */
54
+ export declare const IDENT_PATTERN: RegExp;
55
+ /**
56
+ * Validate a declared group name (used by the host plugin tool
57
+ * registry and the container's built-in tool registration). Throws
58
+ * with a self-contained message if the name is reserved or fails
59
+ * {@link IDENT_PATTERN}.
60
+ */
61
+ export declare function validateGroupName(name: string): void;
62
+ /**
63
+ * Match one tool-name glob pattern against a single tool name. A
64
+ * pattern without `*` is an exact match; `*` is a wildcard for any
65
+ * character sequence (including `_`, since tool names contain `_`).
66
+ * Fully anchored (`^…$`) — `comment_*` matches `comment_reply` but not
67
+ * `commentary`. This is the shared semantics used by the container's
68
+ * `tools:` frontmatter resolver and by the `mcp.yml` allow/deny/level
69
+ * globs.
70
+ *
71
+ * @param pattern The glob pattern (bareword or `*`-containing).
72
+ * @param name The tool name to test.
73
+ * @returns `true` when the name matches the pattern.
74
+ */
75
+ export declare function toolPatternMatches(pattern: string, name: string): boolean;
76
+ /**
77
+ * `true` when `name` matches any of the glob `patterns`. Empty list ⇒
78
+ * `false`. See {@link toolPatternMatches} for the per-pattern semantics.
79
+ */
80
+ export declare function matchesAnyToolPattern(patterns: readonly string[], name: string): boolean;
@@ -0,0 +1,102 @@
1
+ /**
2
+ * Cross-cutting constants for tool-group naming. A handler's `tools:`
3
+ * frontmatter lists explicit tool names, `*`-globs, or group names;
4
+ * the resolver lives in the container
5
+ * (`container/src/tools/ToolsExpressionParser.ts`). The pieces
6
+ * re-exported here are the ones the host-side `mcp.yml` linter and the
7
+ * plugin tool registry also need to enforce — namely the reserved
8
+ * group names and the shape every declarable name must match.
9
+ *
10
+ * Group names are otherwise *open*: any identifier matching
11
+ * {@link IDENT_PATTERN} is a valid group, and a tool joins it by
12
+ * listing the name in `PluginTool.groups`. The "predefined" groups
13
+ * (`core`, `fs`, `reflection`, …) are not hardcoded sets — they
14
+ * emerge from the union of every tool that declares them.
15
+ */
16
+ /** Built-in group: every key in the available pool (system ∪ MCP). Reserved. */
17
+ export const ALL_GROUP_NAME = "all";
18
+ /** Built-in group: every MCP-tool key. Reserved. */
19
+ export const MCP_GROUP_NAME = "mcp";
20
+ /** Built-in group: empty set — used to override a parent's `tools:`. Reserved. */
21
+ export const NONE_GROUP_NAME = "none";
22
+ /**
23
+ * Reserved plugin id for host-owned tools registered without a
24
+ * plugin-id prefix (the "bare-key" path — `cal_get_events` rather
25
+ * than `core_cal_get_events`). It is a **registration sentinel, not
26
+ * an addressable tool group**: it deliberately collides in spelling
27
+ * with the curated `core` group, so it must never be turned into a
28
+ * plugin-id auto-group (doing so would shadow the curated `core`
29
+ * group — the union of container built-ins and tools declaring
30
+ * `groups: ["core"]` — with the full set of host-owned tools). Host
31
+ * built-in tools join curated groups through their own `groups`
32
+ * field (`["cal"]`, `["mail"]`, `["reflection"]`, …) instead.
33
+ */
34
+ export const CORE_PLUGIN_ID = "core";
35
+ /**
36
+ * Names the resolver handles directly and that therefore cannot
37
+ * appear as a declarable group, an MCP id, or a plugin id. Using one
38
+ * as an MCP id in `mcp.yml` is rejected by the loader; declaring it in
39
+ * `PluginTool.groups` is rejected by the plugin tool registry.
40
+ */
41
+ export const RESERVED_GROUP_NAMES = new Set([
42
+ ALL_GROUP_NAME,
43
+ MCP_GROUP_NAME,
44
+ NONE_GROUP_NAME,
45
+ ]);
46
+ /**
47
+ * Pattern any group name — declarable, MCP-id-derived, or
48
+ * plugin-id-derived — must match.
49
+ *
50
+ * Lowercase alphanumeric only, leading letter, no underscores and
51
+ * no hyphens. The exclusion of `_` is load-bearing: tool keys are
52
+ * always shaped `${id}_${name}` (plugin tools) or a bare alnum-only
53
+ * stem (built-ins) and contain at least one underscore in the
54
+ * namespaced form, so an underscore-free bareword can never collide
55
+ * with a tool key — the tool-group vs tool-pattern classifier in
56
+ * `resolveTools` uses that to tell them apart structurally.
57
+ */
58
+ export const IDENT_PATTERN = /^[a-z][a-z0-9]*$/;
59
+ /**
60
+ * Validate a declared group name (used by the host plugin tool
61
+ * registry and the container's built-in tool registration). Throws
62
+ * with a self-contained message if the name is reserved or fails
63
+ * {@link IDENT_PATTERN}.
64
+ */
65
+ export function validateGroupName(name) {
66
+ if (!IDENT_PATTERN.test(name)) {
67
+ throw new Error(`tool group name "${name}" is not a valid tool group name (must match ${IDENT_PATTERN})`);
68
+ }
69
+ if (RESERVED_GROUP_NAMES.has(name)) {
70
+ throw new Error(`tool group name "${name}" is a reserved tool group name (${[...RESERVED_GROUP_NAMES].join(", ")})`);
71
+ }
72
+ }
73
+ /**
74
+ * Match one tool-name glob pattern against a single tool name. A
75
+ * pattern without `*` is an exact match; `*` is a wildcard for any
76
+ * character sequence (including `_`, since tool names contain `_`).
77
+ * Fully anchored (`^…$`) — `comment_*` matches `comment_reply` but not
78
+ * `commentary`. This is the shared semantics used by the container's
79
+ * `tools:` frontmatter resolver and by the `mcp.yml` allow/deny/level
80
+ * globs.
81
+ *
82
+ * @param pattern The glob pattern (bareword or `*`-containing).
83
+ * @param name The tool name to test.
84
+ * @returns `true` when the name matches the pattern.
85
+ */
86
+ export function toolPatternMatches(pattern, name) {
87
+ if (!pattern.includes("*")) {
88
+ return pattern === name;
89
+ }
90
+ const regex = new RegExp(`^${pattern
91
+ .split("*")
92
+ .map((part) => part.replace(/[.+?^${}()|[\]\\]/g, "\\$&"))
93
+ .join(".*")}$`);
94
+ return regex.test(name);
95
+ }
96
+ /**
97
+ * `true` when `name` matches any of the glob `patterns`. Empty list ⇒
98
+ * `false`. See {@link toolPatternMatches} for the per-pattern semantics.
99
+ */
100
+ export function matchesAnyToolPattern(patterns, name) {
101
+ return patterns.some((pattern) => toolPatternMatches(pattern, name));
102
+ }
@@ -0,0 +1,104 @@
1
+ /**
2
+ * Public types describing the workspace-file surface plugins observe via
3
+ * `ctx.workspace.*`. The concrete watcher lives in the host
4
+ * (`host/src/workspace/WorkspaceWatcher.ts`); host and plugins share the
5
+ * same `WorkspaceFile` shape — no projection layer.
6
+ */
7
+ /**
8
+ * Filter describing which workspace files a subscriber cares about.
9
+ *
10
+ * All fields are optional and combined with AND semantics. An empty
11
+ * filter matches every file in scope (the watcher's scope is markdown
12
+ * only — see {@link WorkspaceWatcherApi}).
13
+ */
14
+ export interface WorkspaceFileFilter {
15
+ /**
16
+ * Frontmatter key → search expression. The file matches iff every
17
+ * key listed here is present in the file's YAML frontmatter AND
18
+ * the stringified value matches the expression.
19
+ *
20
+ * The expression is a substring matcher with `*` as the any-chars
21
+ * wildcard. `"*"` alone means "key present with any value". `"foo"`
22
+ * means "value contains the substring `foo`". `"foo*bar"` means
23
+ * "value contains `foo` somewhere, then `bar` later".
24
+ */
25
+ readonly frontmatter?: Readonly<Record<string, string>>;
26
+ /**
27
+ * Optional substring-with-`*` glob applied to the workspace-relative
28
+ * path. Same wildcard grammar as the frontmatter values.
29
+ */
30
+ readonly pathGlob?: string;
31
+ }
32
+ /**
33
+ * A workspace file observed by the watcher.
34
+ *
35
+ * Returned by {@link WorkspaceWatcherApi.listMarkdownFiles} (with `kind`
36
+ * omitted) and dispatched to {@link WorkspaceWatcherApi.onMarkdownFileUpdate}
37
+ * callbacks (with `kind` set to the transition that triggered the
38
+ * notification).
39
+ *
40
+ * `removed` may be synthetic: a file whose frontmatter changed such
41
+ * that it no longer matches a subscription's filter is reported as
42
+ * `removed` even though the file still exists on disk. The file is
43
+ * gone *from the subscriber's point of view*, which is the only thing
44
+ * the consumer can act on.
45
+ */
46
+ export interface WorkspaceFile {
47
+ /** Path relative to the workspace root, POSIX separators. */
48
+ readonly relativePath: string;
49
+ /** Absolute path on disk. */
50
+ readonly absolutePath: string;
51
+ /**
52
+ * Transition kind. Always set on `onMarkdownFileUpdate` callbacks
53
+ * (`"added"` on first match — creation or a frontmatter edit that
54
+ * brings the file into scope; `"changed"` for content edits while
55
+ * still in scope; `"removed"` on the reverse transition). Always
56
+ * omitted on `listMarkdownFiles` results.
57
+ */
58
+ readonly kind?: "added" | "changed" | "removed";
59
+ }
60
+ /**
61
+ * The `ctx.workspace` surface plugins use to observe workspace files.
62
+ *
63
+ * **Scope: markdown only.** Only `.md` files under the workspace root
64
+ * are tracked. Both methods below ignore every other extension; an
65
+ * empty filter still only matches `.md` files. (The method names
66
+ * spell this out — `listMarkdownFiles`, `onMarkdownFileUpdate` — so
67
+ * the scope is impossible to miss at the call site.)
68
+ *
69
+ * The intended pattern is snapshot-then-diff: call
70
+ * {@link listMarkdownFiles} once for the baseline, then
71
+ * {@link onMarkdownFileUpdate} for live transitions. Both calls take
72
+ * the same {@link WorkspaceFileFilter}; together they give a plugin a
73
+ * complete view (baseline + live transitions) without the watcher
74
+ * having to re-announce pre-existing files on subscription.
75
+ *
76
+ * Example:
77
+ *
78
+ * const snapshot = await ctx.workspace.listMarkdownFiles(filter);
79
+ * for (const file of snapshot) { ...seed... }
80
+ * const unsub = ctx.workspace.onMarkdownFileUpdate(filter, handleDelta);
81
+ *
82
+ * Only available inside the daemon — CLI subcommands run without a
83
+ * live watcher, so both methods throw with an explicit message in
84
+ * that mode.
85
+ */
86
+ export interface WorkspaceWatcherApi {
87
+ /**
88
+ * Snapshot of every workspace `.md` file currently matching the
89
+ * filter. Reads from the watcher's in-memory cache (no fresh disk
90
+ * scan); chokidar keeps that cache continuously in sync with disk.
91
+ * Returned entries have `kind` omitted. Pair with
92
+ * {@link onMarkdownFileUpdate} — see the interface-level doc.
93
+ */
94
+ listMarkdownFiles(filter: WorkspaceFileFilter): Promise<readonly WorkspaceFile[]>;
95
+ /**
96
+ * Subscribe to `.md` file transitions matching the filter. The
97
+ * callback fires per transition with `kind` always set. Pre-existing
98
+ * matches at subscription time are **not** replayed — call
99
+ * {@link listMarkdownFiles} for the baseline.
100
+ *
101
+ * Returns the unsubscribe handle.
102
+ */
103
+ onMarkdownFileUpdate(filter: WorkspaceFileFilter, callback: (file: WorkspaceFile) => void): () => void;
104
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Public types describing the workspace-file surface plugins observe via
3
+ * `ctx.workspace.*`. The concrete watcher lives in the host
4
+ * (`host/src/workspace/WorkspaceWatcher.ts`); host and plugins share the
5
+ * same `WorkspaceFile` shape — no projection layer.
6
+ */
7
+ export {};
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Estimate the number of tokens a string occupies in a model's context
3
+ * window. Used to bound tool-result offloading (see {@link import("./ToolRunner.js")})
4
+ * and active context-window management in the container's agent loop.
5
+ *
6
+ * The current heuristic is a coarse `~4 characters per token` — fast,
7
+ * dependency-free, and good enough for the budgeting decisions it backs
8
+ * (offload-or-not, drop-or-not). It deliberately over- nor under-counts
9
+ * by much for typical English/JSON payloads. A real tokenizer
10
+ * (e.g. tiktoken) can be slotted in here later without touching callers.
11
+ *
12
+ * @param text The string to estimate.
13
+ * @returns The estimated token count (always a non-negative integer;
14
+ * `0` for the empty string).
15
+ */
16
+ export declare function estimateTokens(text: string): number;
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Estimate the number of tokens a string occupies in a model's context
3
+ * window. Used to bound tool-result offloading (see {@link import("./ToolRunner.js")})
4
+ * and active context-window management in the container's agent loop.
5
+ *
6
+ * The current heuristic is a coarse `~4 characters per token` — fast,
7
+ * dependency-free, and good enough for the budgeting decisions it backs
8
+ * (offload-or-not, drop-or-not). It deliberately over- nor under-counts
9
+ * by much for typical English/JSON payloads. A real tokenizer
10
+ * (e.g. tiktoken) can be slotted in here later without touching callers.
11
+ *
12
+ * @param text The string to estimate.
13
+ * @returns The estimated token count (always a non-negative integer;
14
+ * `0` for the empty string).
15
+ */
16
+ export function estimateTokens(text) {
17
+ return Math.ceil(text.length / 4);
18
+ }