create-nola-lang 0.1.14 → 0.1.15

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/dist/add.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import type { Runtime } from "./package-manager.js";
1
2
  import { type ProviderId } from "./providers.js";
2
3
  export interface AddResult {
3
4
  /** files written, e.g. ["nola.config.ts", "package.json"] */
@@ -17,6 +18,8 @@ export interface AddOptions {
17
18
  * empty template's — the nola key step rewrites it afterwards (`applyTrial`).
18
19
  */
19
20
  provider?: ProviderId;
21
+ /** "bun" also merges @nola-lang/bun into devDependencies; the bunfig lines are suggested by the flow's outro, never written */
22
+ runtime?: Runtime;
20
23
  }
21
24
  /**
22
25
  * Retrofit an existing npm project: write nola.config.ts (unless present) and
package/dist/add.js CHANGED
@@ -61,7 +61,8 @@ export async function addNola(targetDir, opts = {}) {
61
61
  await writeFile(configPath, await readFile(vendor ? providerConfigUrl(vendor.id) : EMPTY_CONFIG_URL, "utf8"));
62
62
  wrote.push("nola.config.ts");
63
63
  }
64
- for (const [section, name, fixedRange] of REQUIRED) {
64
+ const required = opts.runtime === "bun" ? [...REQUIRED, ["devDependencies", "@nola-lang/bun", null]] : REQUIRED;
65
+ for (const [section, name, fixedRange] of required) {
65
66
  const existing = existingRange(name);
66
67
  if (existing !== undefined) {
67
68
  skipped.push(`${name} already present (${existing}) — left untouched`);
package/dist/checkout.js CHANGED
@@ -28,6 +28,7 @@ export async function checkoutRoot() {
28
28
  const LINKS = [
29
29
  ["@nola-lang/runtime", "runtime"],
30
30
  ["@nola-lang/providers", "providers"],
31
+ ["@nola-lang/bun", "bun"],
31
32
  ["nola-lang", "nola-lang"],
32
33
  ];
33
34
  /**
@@ -52,6 +52,7 @@ async function checkoutRoot() {
52
52
  var LINKS = [
53
53
  ["@nola-lang/runtime", "runtime"],
54
54
  ["@nola-lang/providers", "providers"],
55
+ ["@nola-lang/bun", "bun"],
55
56
  ["nola-lang", "nola-lang"]
56
57
  ];
57
58
  var LINK_CHECKOUT_ENV = "NOLA_LINK_CHECKOUT";
@@ -184,6 +185,13 @@ var TEMPLATES = [
184
185
  featured: true,
185
186
  entry: "src/main.tsi"
186
187
  },
188
+ {
189
+ name: "tool-use",
190
+ label: "offer functions as tools: the model calls them as it needs before it answers",
191
+ source: "example",
192
+ featured: true,
193
+ entry: "src/main.tsi"
194
+ },
187
195
  {
188
196
  name: "agent-loop",
189
197
  label: "an agent loop in one .tsi file: a top-level while with an ask, and a context statement read on every pass",
@@ -246,17 +254,34 @@ function envExample(envVar) {
246
254
  ${envVar}=
247
255
  `;
248
256
  }
257
+ var BUNFIG_TOML = 'preload = ["@nola-lang/bun/preload"]\n\n[test]\npreload = ["@nola-lang/bun/preload"]\n';
258
+ var BUN_ENGINE = ">=1.3.0";
259
+ function applyBunRuntime(content, opts) {
260
+ const pkg = JSON.parse(content);
261
+ const scripts = pkg.scripts ?? {};
262
+ scripts.start = `bun ${opts.entry}`;
263
+ pkg.scripts = scripts;
264
+ const dev = pkg.devDependencies ?? {};
265
+ dev["@nola-lang/bun"] = `^${opts.version}`;
266
+ pkg.devDependencies = dev;
267
+ const engines = pkg.engines ?? {};
268
+ engines.bun = BUN_ENGINE;
269
+ pkg.engines = engines;
270
+ return `${JSON.stringify(pkg, null, 2)}
271
+ `;
272
+ }
249
273
  var GITIGNORE_URL = new URL("../templates/_gitignore", import.meta.url);
250
274
  async function withRecommendedGitignore(files) {
251
275
  if (!files.has(".gitignore")) files.set(".gitignore", await readFile3(GITIGNORE_URL, "utf8"));
252
276
  return files;
253
277
  }
254
- function nextStepsComment(ide, template) {
278
+ function nextStepsComment(ide, template, runtime = "node") {
255
279
  if (ide === "vscode") {
256
280
  const breakpointIn = template === "empty" ? "on your first `ask` in this file" : templateByName(template)?.entry ? "on the `ask` line below" : template === "typescript-interop" ? "in src/person.tsi" : "in your .tsi file";
281
+ const runLine = runtime === "bun" ? "// 1. Run this file: bun start (F5 runs it too, under Node's loader \u2014 .vscode/launch.json is already set up)." : "// 1. Press F5 to run this file (.vscode/launch.json is already set up).";
257
282
  return [
258
283
  "// Next steps in VS Code:",
259
- "// 1. Press F5 to run this file (.vscode/launch.json is already set up).",
284
+ runLine,
260
285
  `// 2. Set a breakpoint ${breakpointIn} and press F5 again to step through the ask.`,
261
286
  '// 3. Install the recommended "Nola" extension when VS Code offers it \u2014 IntelliSense,',
262
287
  "// go to definition and diagnostics inside .tsi files."
@@ -264,7 +289,7 @@ function nextStepsComment(ide, template) {
264
289
  }
265
290
  return [
266
291
  "// Next steps:",
267
- "// 1. Run this file: npm start",
292
+ `// 1. Run this file: ${runtime === "bun" ? "bun start" : "npm start"}`,
268
293
  "// 2. Set up your editor (VS Code extension, debugging): https://nola.sh/docs/start/editor-setup/"
269
294
  ].join("\n");
270
295
  }
@@ -296,18 +321,24 @@ async function scaffold(targetDir, opts = {}) {
296
321
  await prepareTarget(absRoot, opts.force ?? false);
297
322
  const name = opts.name ?? basename(absRoot);
298
323
  const provider = opts.provider ?? "none";
324
+ const runtime = opts.runtime ?? "node";
325
+ const entry = entryFile(template);
299
326
  if (provider !== "none") {
300
327
  for (const offline of OFFLINE_ONLY) files.delete(offline);
301
328
  files.set("nola.config.ts", await readFile3(providerConfigUrl(provider), "utf8"));
302
329
  }
303
330
  const manifest = files.get("package.json");
304
331
  if (manifest) files.set("package.json", rewriteExamplePackageJson(manifest, { name, version }));
332
+ if (runtime === "bun") {
333
+ files.set("bunfig.toml", BUNFIG_TOML);
334
+ const current = files.get("package.json");
335
+ if (current !== void 0) files.set("package.json", applyBunRuntime(current, { entry, version }));
336
+ }
305
337
  await withRecommendedGitignore(files);
306
338
  const envVar = vendorEnvVar(template, provider);
307
339
  if (envVar !== void 0 && !files.has(".env.example")) files.set(".env.example", envExample(envVar));
308
- const entry = entryFile(template);
309
340
  const program = files.get(entry);
310
- if (program !== void 0) files.set(entry, `${nextStepsComment(opts.ide ?? "none", template)}
341
+ if (program !== void 0) files.set(entry, `${nextStepsComment(opts.ide ?? "none", template, runtime)}
311
342
 
312
343
  ${program}`);
313
344
  for (const [relPath, content] of files) {
@@ -368,7 +399,8 @@ async function addNola(targetDir, opts = {}) {
368
399
  await writeFile3(configPath, await readFile4(vendor ? providerConfigUrl(vendor.id) : EMPTY_CONFIG_URL, "utf8"));
369
400
  wrote.push("nola.config.ts");
370
401
  }
371
- for (const [section, name, fixedRange] of REQUIRED) {
402
+ const required = opts.runtime === "bun" ? [...REQUIRED, ["devDependencies", "@nola-lang/bun", null]] : REQUIRED;
403
+ for (const [section, name, fixedRange] of required) {
372
404
  const existing = existingRange(name);
373
405
  if (existing !== void 0) {
374
406
  skipped.push(`${name} already present (${existing}) \u2014 left untouched`);
@@ -1558,6 +1590,12 @@ function detectPackageManager(userAgent = process.env.npm_config_user_agent) {
1558
1590
  function packageManagerCommands(pm) {
1559
1591
  return { install: `${pm} install`, start: `${pm} start`, run: `${pm} run` };
1560
1592
  }
1593
+ function isRuntime(value) {
1594
+ return value === "node" || value === "bun";
1595
+ }
1596
+ function defaultRuntime(pm) {
1597
+ return pm === "bun" ? "bun" : "node";
1598
+ }
1561
1599
 
1562
1600
  // src/flow.ts
1563
1601
  var DEFAULT_DIR = "nola-app";
@@ -1674,11 +1712,18 @@ async function resolveProvider(input, prompter, template) {
1674
1712
  return choice;
1675
1713
  }
1676
1714
  }
1715
+ function resolveRuntime(input) {
1716
+ if (input.runtime !== void 0) {
1717
+ if (!isRuntime(input.runtime)) throw new Error(`invalid --runtime "${input.runtime}" (valid: node, bun)`);
1718
+ return input.runtime;
1719
+ }
1720
+ return defaultRuntime(input.packageManager ?? "npm");
1721
+ }
1677
1722
  async function resolveExtras(input, prompter, dir, template) {
1678
1723
  const provider = await resolveProvider(input, prompter, template);
1679
1724
  if (provider === null) return null;
1680
1725
  const setup = await resolveSetup(input, prompter, dir);
1681
- return setup === null ? null : { provider, ...setup };
1726
+ return setup === null ? null : { provider, ...setup, runtime: resolveRuntime(input) };
1682
1727
  }
1683
1728
  async function resolveScaffoldOptions(input, prompter) {
1684
1729
  if (input.add && input.template) {
@@ -1689,6 +1734,9 @@ async function resolveScaffoldOptions(input, prompter) {
1689
1734
  if (input.ide !== void 0 && input.ide !== "vscode" && input.ide !== "none") {
1690
1735
  throw new Error(`invalid --ide "${input.ide}" (valid: vscode, none)`);
1691
1736
  }
1737
+ if (input.runtime !== void 0 && !isRuntime(input.runtime)) {
1738
+ throw new Error(`invalid --runtime "${input.runtime}" (valid: node, bun)`);
1739
+ }
1692
1740
  if (input.agents !== void 0) parseAgentsFlag(input.agents);
1693
1741
  flaggedProvider(input);
1694
1742
  if (input.add) {
@@ -1802,6 +1850,10 @@ var FLOW_OPTIONS = {
1802
1850
  },
1803
1851
  add: { type: "boolean", description: "add Nola to the existing project in [dir] instead of scaffolding" },
1804
1852
  ide: { type: "string", description: "editor setup: vscode (the default) | none" },
1853
+ runtime: {
1854
+ type: "string",
1855
+ description: "runtime the project runs on: node | bun (default: bun when bun ran the scaffolder \u2014 bunfig.toml preload + `bun src/main.tsi`)"
1856
+ },
1805
1857
  agents: {
1806
1858
  type: "string",
1807
1859
  description: `agent skill files: ${AGENT_IDS.join(",")} | all | none (default: claude,universal)`
@@ -1961,6 +2013,8 @@ async function runFlow(args, opts = {}) {
1961
2013
  agents: args.agents,
1962
2014
  provider: args.provider,
1963
2015
  trial: args.trial,
2016
+ runtime: args.runtime,
2017
+ packageManager: pm,
1964
2018
  keyPath,
1965
2019
  signIn: signInNow,
1966
2020
  cwd: opts.cwd,
@@ -1974,7 +2028,7 @@ async function runFlow(args, opts = {}) {
1974
2028
  }
1975
2029
  if (outcome.kind === "add") {
1976
2030
  const grant = outcome.provider === "nola" ? await obtainTrial(prompter, opts, interactive) : null;
1977
- const result = await addNola(outcome.dir, { provider: outcome.provider });
2031
+ const result = await addNola(outcome.dir, { provider: outcome.provider, runtime: outcome.runtime });
1978
2032
  const trial = grant ? await applyTrial(outcome.dir, { apiKey: grant.apiKey, hasConfig: !result.wrote.includes("nola.config.ts") }) : { wrote: [], skipped: [] };
1979
2033
  const ide = outcome.ide === "vscode" ? await writeVscodeSetup(outcome.dir) : { wrote: [], skipped: [] };
1980
2034
  const ag = outcome.agents.length > 0 ? await writeAgentSkills(outcome.dir, outcome.agents) : { wrote: [], removed: [], skipped: [] };
@@ -1987,7 +2041,10 @@ async function runFlow(args, opts = {}) {
1987
2041
  }
1988
2042
  const lines = [
1989
2043
  packageManagerCommands(pm).install,
1990
- 'optional start script: "start": "nola run src/main.ts"',
2044
+ ...outcome.runtime === "bun" ? [
2045
+ 'bunfig.toml: preload = ["@nola-lang/bun/preload"] (runs .tsi from source under Bun)',
2046
+ 'optional start script: "start": "bun src/main.ts"'
2047
+ ] : ['optional start script: "start": "nola run src/main.ts"'],
1991
2048
  'tsconfig tip: directory-style include (e.g. ["src"]) lets the editor see .tsi files',
1992
2049
  commented(CONSOLE_COMMAND, CONSOLE_NOTE)
1993
2050
  ];
@@ -2002,7 +2059,13 @@ Next steps:
2002
2059
  try {
2003
2060
  const grant = outcome.provider === "nola" ? await obtainTrial(prompter, opts, interactive) : null;
2004
2061
  const provider = outcome.provider === "nola" ? grant ? "nola" : "none" : outcome.provider;
2005
- const { files } = await scaffold(outcome.dir, { template: outcome.template, force: outcome.force, provider, ide: outcome.ide });
2062
+ const { files } = await scaffold(outcome.dir, {
2063
+ template: outcome.template,
2064
+ force: outcome.force,
2065
+ provider,
2066
+ ide: outcome.ide,
2067
+ runtime: outcome.runtime
2068
+ });
2006
2069
  let trialWrote = [];
2007
2070
  if (grant) {
2008
2071
  const trial = await applyTrial(outcome.dir, { apiKey: grant.apiKey, hasConfig: false });
package/dist/flow.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { type AgentId } from "./agents.js";
2
2
  import { type CommandSpec } from "./cli.js";
3
3
  import { type Launcher } from "./launch.js";
4
- import { type PackageManager } from "./package-manager.js";
4
+ import { type PackageManager, type Runtime } from "./package-manager.js";
5
5
  import { type ProviderId } from "./providers.js";
6
6
  export interface PrompterOption {
7
7
  value: string;
@@ -45,6 +45,10 @@ export interface FlowInput {
45
45
  provider?: string;
46
46
  /** --trial / --no-trial: shorthand for --provider nola / --provider none */
47
47
  trial?: boolean;
48
+ /** --runtime <node|bun>; undefined = bun when bun ran the scaffolder, else node */
49
+ runtime?: string;
50
+ /** the manager that invoked us — decides the default runtime */
51
+ packageManager?: PackageManager;
48
52
  /** how the nola row is worded, decided by runFlow from ~/.nola (spec 2026-09-04-cli-sign-in-design.md §5); default trial */
49
53
  keyPath?: KeyPath;
50
54
  /**
@@ -75,12 +79,14 @@ export type FlowOutcome = {
75
79
  provider: ProviderId;
76
80
  ide: "vscode" | "none";
77
81
  agents: AgentId[];
82
+ runtime: Runtime;
78
83
  } | {
79
84
  kind: "add";
80
85
  dir: string;
81
86
  provider: ProviderId;
82
87
  ide: "vscode" | "none";
83
88
  agents: AgentId[];
89
+ runtime: Runtime;
84
90
  } | {
85
91
  kind: "cancelled";
86
92
  };
@@ -130,6 +136,7 @@ export interface RunFlowArgs {
130
136
  agents?: string;
131
137
  provider?: string;
132
138
  trial?: boolean;
139
+ runtime?: string;
133
140
  }
134
141
  /**
135
142
  * The flow's flags, declared ONCE for both bins (`npm create nola-lang` and
@@ -148,6 +155,10 @@ export declare const FLOW_OPTIONS: {
148
155
  readonly type: "string";
149
156
  readonly description: "editor setup: vscode (the default) | none";
150
157
  };
158
+ readonly runtime: {
159
+ readonly type: "string";
160
+ readonly description: "runtime the project runs on: node | bun (default: bun when bun ran the scaffolder — bunfig.toml preload + `bun src/main.tsi`)";
161
+ };
151
162
  readonly agents: {
152
163
  readonly type: "string";
153
164
  readonly description: `agent skill files: ${string} | all | none (default: claude,universal)`;
package/dist/flow.js CHANGED
@@ -16,7 +16,7 @@ import { acquireKey, SignInRequiredError } from "./key.js";
16
16
  import { realLauncher } from "./launch.js";
17
17
  import { nodeVersionWarning } from "./node-version.js";
18
18
  import { openBrowser } from "./open-url.js";
19
- import { detectPackageManager, packageManagerCommands } from "./package-manager.js";
19
+ import { defaultRuntime, detectPackageManager, isRuntime, packageManagerCommands } from "./package-manager.js";
20
20
  import { isProviderId, PROVIDERS, providerById, providerIds } from "./providers.js";
21
21
  import { entryFile, exampleNames, featuredNames, templateByName, templateNames } from "./registry.js";
22
22
  import { ownVersion, scaffold, vendorEnvVar } from "./scaffold.js";
@@ -230,13 +230,22 @@ async function resolveProvider(input, prompter, template) {
230
230
  return choice;
231
231
  }
232
232
  }
233
+ /** `--runtime`, else the manager's default (bun → bun). Validated up front like the other flags. */
234
+ function resolveRuntime(input) {
235
+ if (input.runtime !== undefined) {
236
+ if (!isRuntime(input.runtime))
237
+ throw new Error(`invalid --runtime "${input.runtime}" (valid: node, bun)`);
238
+ return input.runtime;
239
+ }
240
+ return defaultRuntime(input.packageManager ?? "npm");
241
+ }
233
242
  /** The provider and setup (editor + agents) questions, in that order, shared by every outcome site; null = cancelled. */
234
243
  async function resolveExtras(input, prompter, dir, template) {
235
244
  const provider = await resolveProvider(input, prompter, template);
236
245
  if (provider === null)
237
246
  return null;
238
247
  const setup = await resolveSetup(input, prompter, dir);
239
- return setup === null ? null : { provider, ...setup };
248
+ return setup === null ? null : { provider, ...setup, runtime: resolveRuntime(input) };
240
249
  }
241
250
  /** Args fill prompts; whatever is missing is asked (never asked non-interactively). */
242
251
  export async function resolveScaffoldOptions(input, prompter) {
@@ -246,6 +255,9 @@ export async function resolveScaffoldOptions(input, prompter) {
246
255
  if (input.ide !== undefined && input.ide !== "vscode" && input.ide !== "none") {
247
256
  throw new Error(`invalid --ide "${input.ide}" (valid: vscode, none)`);
248
257
  }
258
+ if (input.runtime !== undefined && !isRuntime(input.runtime)) {
259
+ throw new Error(`invalid --runtime "${input.runtime}" (valid: node, bun)`);
260
+ }
249
261
  if (input.agents !== undefined)
250
262
  parseAgentsFlag(input.agents); // throws on unknown ids
251
263
  flaggedProvider(input); // throws on an unknown id or a contradiction
@@ -377,6 +389,10 @@ export const FLOW_OPTIONS = {
377
389
  },
378
390
  add: { type: "boolean", description: "add Nola to the existing project in [dir] instead of scaffolding" },
379
391
  ide: { type: "string", description: "editor setup: vscode (the default) | none" },
392
+ runtime: {
393
+ type: "string",
394
+ description: "runtime the project runs on: node | bun (default: bun when bun ran the scaffolder — bunfig.toml preload + `bun src/main.tsi`)",
395
+ },
380
396
  agents: {
381
397
  type: "string",
382
398
  description: `agent skill files: ${AGENT_IDS.join(",")} | all | none (default: claude,universal)`,
@@ -596,6 +612,8 @@ export async function runFlow(args, opts = {}) {
596
612
  agents: args.agents,
597
613
  provider: args.provider,
598
614
  trial: args.trial,
615
+ runtime: args.runtime,
616
+ packageManager: pm,
599
617
  keyPath,
600
618
  signIn: signInNow,
601
619
  cwd: opts.cwd,
@@ -609,7 +627,7 @@ export async function runFlow(args, opts = {}) {
609
627
  }
610
628
  if (outcome.kind === "add") {
611
629
  const grant = outcome.provider === "nola" ? await obtainTrial(prompter, opts, interactive) : null;
612
- const result = await addNola(outcome.dir, { provider: outcome.provider });
630
+ const result = await addNola(outcome.dir, { provider: outcome.provider, runtime: outcome.runtime });
613
631
  const trial = grant
614
632
  ? await applyTrial(outcome.dir, { apiKey: grant.apiKey, hasConfig: !result.wrote.includes("nola.config.ts") })
615
633
  : { wrote: [], skipped: [] };
@@ -629,7 +647,12 @@ export async function runFlow(args, opts = {}) {
629
647
  }
630
648
  const lines = [
631
649
  packageManagerCommands(pm).install,
632
- 'optional start script: "start": "nola run src/main.ts"',
650
+ ...(outcome.runtime === "bun"
651
+ ? [
652
+ 'bunfig.toml: preload = ["@nola-lang/bun/preload"] (runs .tsi from source under Bun)',
653
+ 'optional start script: "start": "bun src/main.ts"',
654
+ ]
655
+ : ['optional start script: "start": "nola run src/main.ts"']),
633
656
  'tsconfig tip: directory-style include (e.g. ["src"]) lets the editor see .tsi files',
634
657
  commented(CONSOLE_COMMAND, CONSOLE_NOTE),
635
658
  ];
@@ -644,7 +667,13 @@ export async function runFlow(args, opts = {}) {
644
667
  const grant = outcome.provider === "nola" ? await obtainTrial(prompter, opts, interactive) : null;
645
668
  // A nola choice without a key (declined sign-in, API failure) scaffolds the plain template — the note above says why.
646
669
  const provider = outcome.provider === "nola" ? (grant ? "nola" : "none") : outcome.provider;
647
- const { files } = await scaffold(outcome.dir, { template: outcome.template, force: outcome.force, provider, ide: outcome.ide });
670
+ const { files } = await scaffold(outcome.dir, {
671
+ template: outcome.template,
672
+ force: outcome.force,
673
+ provider,
674
+ ide: outcome.ide,
675
+ runtime: outcome.runtime,
676
+ });
648
677
  let trialWrote = [];
649
678
  if (grant) {
650
679
  const trial = await applyTrial(outcome.dir, { apiKey: grant.apiKey, hasConfig: false });
package/dist/index.js CHANGED
@@ -87,7 +87,7 @@ import {
87
87
  writeEnvKey,
88
88
  writeSession,
89
89
  writeVscodeSetup
90
- } from "./chunk-CB26Z47Y.js";
90
+ } from "./chunk-UTVGG44K.js";
91
91
  import {
92
92
  clackPrompter
93
93
  } from "./chunk-7IQIRMK6.js";
package/dist/main.js CHANGED
@@ -3,7 +3,7 @@ import {
3
3
  CREATE_COMMAND,
4
4
  dispatch,
5
5
  ownVersion
6
- } from "./chunk-CB26Z47Y.js";
6
+ } from "./chunk-UTVGG44K.js";
7
7
  import "./chunk-FPS2V3S4.js";
8
8
 
9
9
  // src/main.ts
@@ -14,4 +14,9 @@ export interface PackageManagerCommands {
14
14
  run: string;
15
15
  }
16
16
  export declare function packageManagerCommands(pm: PackageManager): PackageManagerCommands;
17
+ /** The runtime a scaffold targets: Node (the loader, `nola run`) or Bun (`@nola-lang/bun`'s preload, `bun src/main.tsi`). */
18
+ export type Runtime = "node" | "bun";
19
+ export declare function isRuntime(value: string): value is Runtime;
20
+ /** `bun create nola` produces a Bun project unless `--runtime node` says otherwise; every other manager scaffolds for Node. */
21
+ export declare function defaultRuntime(pm: PackageManager): Runtime;
17
22
  //# sourceMappingURL=package-manager.d.ts.map
@@ -5,4 +5,11 @@ export function detectPackageManager(userAgent = process.env.npm_config_user_age
5
5
  export function packageManagerCommands(pm) {
6
6
  return { install: `${pm} install`, start: `${pm} start`, run: `${pm} run` };
7
7
  }
8
+ export function isRuntime(value) {
9
+ return value === "node" || value === "bun";
10
+ }
11
+ /** `bun create nola` produces a Bun project unless `--runtime node` says otherwise; every other manager scaffolds for Node. */
12
+ export function defaultRuntime(pm) {
13
+ return pm === "bun" ? "bun" : "node";
14
+ }
8
15
  //# sourceMappingURL=package-manager.js.map
@@ -10,8 +10,8 @@
10
10
  * chosen provider's config and the next-steps comment on top.
11
11
  *
12
12
  * The first menu leads (in this order) and is organized BY FEATURE:
13
- * `feature-extraction` (the default) and `function-calling` are each one
14
- * `.tsi` file with a top-level ask — the two things Nola does —
13
+ * `feature-extraction` (the default), `function-calling` and `tool-use` are
14
+ * each one `.tsi` file with a top-level ask — the three things Nola does —
15
15
  * `agent-loop` is the same shape around a top-level `while`,
16
16
  * `typescript-interop` is the infer-function shape called from plain
17
17
  * TypeScript, `triage-ticket` the one-file shape on a non-chat vendor, then
package/dist/registry.js CHANGED
@@ -15,6 +15,13 @@ export const TEMPLATES = [
15
15
  featured: true,
16
16
  entry: "src/main.tsi",
17
17
  },
18
+ {
19
+ name: "tool-use",
20
+ label: "offer functions as tools: the model calls them as it needs before it answers",
21
+ source: "example",
22
+ featured: true,
23
+ entry: "src/main.tsi",
24
+ },
18
25
  {
19
26
  name: "agent-loop",
20
27
  label: "an agent loop in one .tsi file: a top-level while with an ask, and a context statement read on every pass",
@@ -1,3 +1,4 @@
1
+ import type { Runtime } from "./package-manager.js";
1
2
  import { type ProviderId } from "./providers.js";
2
3
  export interface ScaffoldResult {
3
4
  root: string;
@@ -23,6 +24,12 @@ export interface ScaffoldOptions {
23
24
  * files the flow writes make true); without one it stays editor-neutral.
24
25
  */
25
26
  ide?: "vscode" | "none";
27
+ /**
28
+ * The runtime the project runs on; default "node". "bun" adds bunfig.toml with the
29
+ * @nola-lang/bun preload, a `bun <entry>` start script, the devDependency and
30
+ * engines.bun — build/check stay the Node CLI (spec 2026-10-14-bun-support).
31
+ */
32
+ runtime?: Runtime;
26
33
  }
27
34
  /** The config a live provider choice writes over the template's own (`templates/_providers/<id>.config.ts`). */
28
35
  export declare function providerConfigUrl(provider: Exclude<ProviderId, "none">): URL;
@@ -39,6 +46,15 @@ export declare function vendorEnvVar(template: string, provider: ProviderId): st
39
46
  * `.gitignore` keeps trackable (`!.env.example`), so it can be committed.
40
47
  */
41
48
  export declare function envExample(envVar: string): string;
49
+ /** The Bun project's bunfig.toml: the preload for `bun src/main.tsi` and for `bun test`. */
50
+ export declare const BUNFIG_TOML = "preload = [\"@nola-lang/bun/preload\"]\n\n[test]\npreload = [\"@nola-lang/bun/preload\"]\n";
51
+ /** The Bun floor @nola-lang/bun needs (process.getBuiltinModule). */
52
+ export declare const BUN_ENGINE = ">=1.3.0";
53
+ /** A Bun project's manifest additions on top of the template's: the start script, the devDependency, engines.bun. */
54
+ export declare function applyBunRuntime(content: string, opts: {
55
+ entry: string;
56
+ version: string;
57
+ }): string;
42
58
  /**
43
59
  * Add the recommended `.gitignore` to a template's files. Templates are
44
60
  * copied as committed — examples from `examples/`, where the monorepo's root
@@ -53,7 +69,7 @@ export declare function withRecommendedGitignore(files: Map<string, string>): Pr
53
69
  * the editor step's .vscode files, which are what make F5 and the extension
54
70
  * prompt real.
55
71
  */
56
- export declare function nextStepsComment(ide: "vscode" | "none", template: string): string;
72
+ export declare function nextStepsComment(ide: "vscode" | "none", template: string, runtime?: Runtime): string;
57
73
  export declare function ownVersion(): Promise<string>;
58
74
  /**
59
75
  * Lay a template down into `targetDir`. One implementation serves both entry
package/dist/scaffold.js CHANGED
@@ -29,6 +29,24 @@ export function vendorEnvVar(template, provider) {
29
29
  export function envExample(envVar) {
30
30
  return `# Copy to .env and fill in — \`nola run\` applies .env before evaluating nola.config.ts.\n${envVar}=\n`;
31
31
  }
32
+ /** The Bun project's bunfig.toml: the preload for `bun src/main.tsi` and for `bun test`. */
33
+ export const BUNFIG_TOML = 'preload = ["@nola-lang/bun/preload"]\n\n[test]\npreload = ["@nola-lang/bun/preload"]\n';
34
+ /** The Bun floor @nola-lang/bun needs (process.getBuiltinModule). */
35
+ export const BUN_ENGINE = ">=1.3.0";
36
+ /** A Bun project's manifest additions on top of the template's: the start script, the devDependency, engines.bun. */
37
+ export function applyBunRuntime(content, opts) {
38
+ const pkg = JSON.parse(content);
39
+ const scripts = pkg.scripts ?? {};
40
+ scripts.start = `bun ${opts.entry}`;
41
+ pkg.scripts = scripts;
42
+ const dev = pkg.devDependencies ?? {};
43
+ dev["@nola-lang/bun"] = `^${opts.version}`;
44
+ pkg.devDependencies = dev;
45
+ const engines = pkg.engines ?? {};
46
+ engines.bun = BUN_ENGINE;
47
+ pkg.engines = engines;
48
+ return `${JSON.stringify(pkg, null, 2)}\n`;
49
+ }
32
50
  /** The ONE recommended .gitignore — every scaffold gets it; no template keeps a copy of its own (_gitignore ships underscored: npm pack strips nested .gitignore files). */
33
51
  const GITIGNORE_URL = new URL("../templates/_gitignore", import.meta.url);
34
52
  /**
@@ -49,7 +67,7 @@ export async function withRecommendedGitignore(files) {
49
67
  * the editor step's .vscode files, which are what make F5 and the extension
50
68
  * prompt real.
51
69
  */
52
- export function nextStepsComment(ide, template) {
70
+ export function nextStepsComment(ide, template, runtime = "node") {
53
71
  if (ide === "vscode") {
54
72
  // `empty`'s stub has no ask yet — the breakpoint goes on the one the user writes.
55
73
  const breakpointIn = template === "empty"
@@ -59,9 +77,13 @@ export function nextStepsComment(ide, template) {
59
77
  : template === "typescript-interop"
60
78
  ? "in src/person.tsi"
61
79
  : "in your .tsi file";
80
+ // A Bun project still debugs under Node's loader: the .vscode files are valid whenever Node is present.
81
+ const runLine = runtime === "bun"
82
+ ? "// 1. Run this file: bun start (F5 runs it too, under Node's loader — .vscode/launch.json is already set up)."
83
+ : "// 1. Press F5 to run this file (.vscode/launch.json is already set up).";
62
84
  return [
63
85
  "// Next steps in VS Code:",
64
- "// 1. Press F5 to run this file (.vscode/launch.json is already set up).",
86
+ runLine,
65
87
  `// 2. Set a breakpoint ${breakpointIn} and press F5 again to step through the ask.`,
66
88
  '// 3. Install the recommended "Nola" extension when VS Code offers it — IntelliSense,',
67
89
  "// go to definition and diagnostics inside .tsi files.",
@@ -69,7 +91,7 @@ export function nextStepsComment(ide, template) {
69
91
  }
70
92
  return [
71
93
  "// Next steps:",
72
- "// 1. Run this file: npm start",
94
+ `// 1. Run this file: ${runtime === "bun" ? "bun start" : "npm start"}`,
73
95
  "// 2. Set up your editor (VS Code extension, debugging): https://nola.sh/docs/start/editor-setup/",
74
96
  ].join("\n");
75
97
  }
@@ -124,6 +146,8 @@ export async function scaffold(targetDir, opts = {}) {
124
146
  await prepareTarget(absRoot, opts.force ?? false);
125
147
  const name = opts.name ?? basename(absRoot);
126
148
  const provider = opts.provider ?? "none";
149
+ const runtime = opts.runtime ?? "node";
150
+ const entry = entryFile(template);
127
151
  // A live provider's config replaces the template's own; the ledger it replayed goes with it.
128
152
  if (provider !== "none") {
129
153
  for (const offline of OFFLINE_ONLY)
@@ -133,15 +157,21 @@ export async function scaffold(targetDir, opts = {}) {
133
157
  const manifest = files.get("package.json");
134
158
  if (manifest)
135
159
  files.set("package.json", rewriteExamplePackageJson(manifest, { name, version }));
160
+ // A Bun project (spec 2026-10-14-bun-support): the preload, a `bun <entry>` start script, the devDependency, engines.bun.
161
+ if (runtime === "bun") {
162
+ files.set("bunfig.toml", BUNFIG_TOML);
163
+ const current = files.get("package.json");
164
+ if (current !== undefined)
165
+ files.set("package.json", applyBunRuntime(current, { entry, version }));
166
+ }
136
167
  await withRecommendedGitignore(files);
137
168
  const envVar = vendorEnvVar(template, provider);
138
169
  if (envVar !== undefined && !files.has(".env.example"))
139
170
  files.set(".env.example", envExample(envVar));
140
171
  // The entry file greets the user with the next steps — it is what VS Code opens.
141
- const entry = entryFile(template);
142
172
  const program = files.get(entry);
143
173
  if (program !== undefined)
144
- files.set(entry, `${nextStepsComment(opts.ide ?? "none", template)}\n\n${program}`);
174
+ files.set(entry, `${nextStepsComment(opts.ide ?? "none", template, runtime)}\n\n${program}`);
145
175
  for (const [relPath, content] of files) {
146
176
  const target = join(absRoot, relPath);
147
177
  await mkdir(join(target, ".."), { recursive: true });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-nola-lang",
3
- "version": "0.1.14",
3
+ "version": "0.1.15",
4
4
  "description": "Scaffold a new Nola project (npm create nola, or npm create nola-lang)",
5
5
  "keywords": [
6
6
  "nola",
@@ -45,8 +45,8 @@ infer function extractPerson(.text: string) {
45
45
  one dot marks a value flowing INTO the model (`.name`); a template after
46
46
  `ask` is a value coming OUT.
47
47
  - `` ask `prompt`: T `` — an extractor: asks the LLM for a `T`. The colon
48
- is glued to the closing backtick; `` ask `prompt`<T> `` is the same
49
- construct (write `: T` unless a page shows otherwise).
48
+ is on the closing backtick's line (glued or spaced); `` ask `prompt`<T> ``
49
+ is the same construct (write `: T` unless a page shows otherwise).
50
50
  `${...}` interpolation works inside the backticks. The `..` is implied
51
51
  directly after `ask` and in a call's argument list — a typed template
52
52
  that starts an argument, or a value nested in a plain object/array
@@ -59,6 +59,24 @@ infer function extractPerson(.text: string) {
59
59
  extractor — is a call intent (the LLM fills the extractor-shaped
60
60
  arguments, then the function runs). Only `` fn`hint`(...) `` carries
61
61
  instruction text.
62
+ - `` ask `use` admin `and` cfg.defaults `go`: T `` — an ASK SEQUENCE: a
63
+ bare value between texts is named data the model sees where it stands
64
+ (`${x}` stays your words); `` ask `words` fn(`slot`: T) `` is a call
65
+ intent with words; several `[lists]` union with your words between them;
66
+ the type glues to the last part. The kind follows the parts: texts and
67
+ data → extract, one call → function calling, lists → tool use; a list
68
+ beside a call is reserved (NOLA2019).
69
+ - `` ask `task` [searchFiles, fs.readFile]: T `` — a TOOL-USE ask: the
70
+ functions in brackets between the template and the type are tools the
71
+ model may call, as often as it needs, before it answers; Nola runs each
72
+ call, hands the result back and asks again, the typed answer ends the
73
+ loop. Parentheses RUN a function once (a call intent), brackets OFFER
74
+ functions. Entries are names or member chains (a call written there is
75
+ reserved, NOLA1022); the contract derives from the signature and JSDoc;
76
+ `.withMaxTurns(n)` / config `ask.maxTurns` (10) cap the turns (NOLA3020);
77
+ a tool that throws ends the ask (NOLA3021); the model must support tool
78
+ calls — openai/anthropic/google/"nola", or `mockProvider(…, { tools:
79
+ true })` (NOLA3019 otherwise).
62
80
  - `ask with <modelName> <intent>` — routes one ask through a named model
63
81
  from `nola.config.ts`. The name must be a static identifier. When the
64
82
  platform serves inference (`model: "nola"`) any unconfigured
@@ -96,6 +114,11 @@ infer function extractPerson(.text: string) {
96
114
  `import { f } from "./x.tsi"`. Imports of plain TS use NodeNext style:
97
115
  `import { g } from "./y.js"` (the `.js` extension, even though the source
98
116
  file is `.ts`).
117
+ - `.tsi` runs from source through the Node loader (`nola run`, Node ≥ 22.18)
118
+ or, on Bun, through `@nola-lang/bun`'s preload — `preload =
119
+ ["@nola-lang/bun/preload"]` in `bunfig.toml`, then `bun src/main.tsi`;
120
+ `nola check` / `nola build` stay on Node (`references/config.md`, "Running
121
+ on Bun").
99
122
  - Every `export type` / `export interface` in a `.tsi` is ALSO a runtime
100
123
  value (`User.toJsonSchema()`, `User.validate(v)`, `User.parse(v)`,
101
124
  `User["~standard"]` — Standard Schema AND Standard JSON Schema, so
@@ -124,5 +147,5 @@ Read these before non-trivial work:
124
147
 
125
148
  - `references/syntax.md` — full grammar and semantics of every construct
126
149
  - `references/patterns.md` — worked examples (extraction, call intents)
127
- - `references/config.md` — `nola.config.ts`, providers, project layout
150
+ - `references/config.md` — `nola.config.ts`, providers, project layout, running on Bun
128
151
  - `references/pitfalls.md` — common errors (NOLA codes) and their fixes
@@ -39,7 +39,9 @@ export default defineConfig({
39
39
  ```
40
40
 
41
41
  `openai({ model })` requires an explicit model; optional fields are `apiKey`,
42
- `apiKeyEnv` (default `"OPENAI_API_KEY"`), `baseUrl` and `fetch`.
42
+ `apiKeyEnv` (default `"OPENAI_API_KEY"`), `baseUrl` and `fetch`. With a custom
43
+ `fetch`, `apiKey`/`apiKeyEnv` are optional: the fetch authenticates, and without
44
+ a key no vendor auth header is sent (the same rule for every vendor factory).
43
45
 
44
46
  `typesafe()` (typesafe.ai's Jev, reads `TYPESAFE_API_KEY`, model defaults to
45
47
  `jev-latest`) is NOT a chat model: it is the DECISION model that answers the
@@ -232,11 +234,11 @@ the providers package:
232
234
  "check": "nola check"
233
235
  },
234
236
  "dependencies": {
235
- "@nola-lang/providers": "^0.1.14",
236
- "@nola-lang/runtime": "^0.1.14"
237
+ "@nola-lang/providers": "^0.1.15",
238
+ "@nola-lang/runtime": "^0.1.15"
237
239
  },
238
240
  "devDependencies": {
239
- "nola-lang": "^0.1.14",
241
+ "nola-lang": "^0.1.15",
240
242
  "typescript": "^5.6.0"
241
243
  },
242
244
  "engines": { "node": ">=22.18" }
@@ -270,6 +272,33 @@ for you). Do not mix versions.
270
272
  a bare `npx nola` outside a project resolves an unrelated npm package named
271
273
  `nola`.)
272
274
 
275
+ ### Running on Bun
276
+
277
+ `bun create nola` scaffolds a Bun project: `@nola-lang/bun` in devDependencies,
278
+ `bunfig.toml` with its preload, `start: "bun src/main.tsi"`. Any Node project
279
+ becomes one by adding the same three things:
280
+
281
+ ```toml
282
+ # bunfig.toml
283
+ preload = ["@nola-lang/bun/preload"]
284
+
285
+ [test]
286
+ preload = ["@nola-lang/bun/preload"]
287
+ ```
288
+
289
+ - `bun src/main.tsi` / `bun --watch src/main.tsi` run `.tsi` from source;
290
+ `bun test` files import `.tsi` modules. Bun ≥ 1.3.
291
+ - One-step bundles and executables: a build script with
292
+ `Bun.build({ entrypoints: ["src/main.tsi"], plugins: [nola()], target: "bun",
293
+ compile: { outfile: "my-app" } })` (`import { nola } from "@nola-lang/bun"`).
294
+ - `nola build` output runs under `bun` with nothing added
295
+ (`bun dist/src/main.tsi.js`, `bun build --compile dist/src/main.tsi.js`).
296
+ - `nola check`, `nola build`, `nola console` run on Node (`bun run check`
297
+ launches them there). `nola run` is the Node loader — under Bun it is
298
+ NOLA3015; use the preload. No source map rides a Bun plugin: stack-trace
299
+ lines through `.tsi` are right at module level and in plain functions, ONE
300
+ TOO HIGH inside an `infer function` body, and columns inside an `ask` drift.
301
+
273
302
  ## tsconfig.json
274
303
 
275
304
  ```json
@@ -258,6 +258,68 @@ call intent awaits a promise-returning callee itself (`ask` ≈ `await`), so
258
258
  ask, `.withRetry(n)` on a call intent re-invokes it on failure — only use it
259
259
  when the target is idempotent.
260
260
 
261
+ ## The tool-use project: offer functions, let the model call them
262
+
263
+ The same one-file shape for the third feature (`npm create nola` scaffolds
264
+ it as `tool-use`): the top-level `ask` offers two plain-TypeScript functions
265
+ as tools. The model calls them as it needs — here it lists a folder, reads
266
+ a file to be sure, then answers — and the typed answer is what `ask` yields.
267
+
268
+ ```ts
269
+ // src/tools.ts — plain TypeScript; the JSDoc is the contract the model sees
270
+ /**
271
+ * Lists the files under a directory on disc D.
272
+ * @param dir an absolute directory path, such as D:/docs
273
+ * @param pattern keeps only the paths that contain it
274
+ */
275
+ export async function searchFiles(dir: string, pattern?: string): Promise<string[]> { /* … */ return []; }
276
+
277
+ /** Reads a file's text. */
278
+ export async function readFile(path: string): Promise<string> { /* … */ return ""; }
279
+ ```
280
+
281
+ ```tsi
282
+ // src/main.tsi — run with: nola run src/main.tsi
283
+ import { readFile, searchFiles } from "./tools.js";
284
+
285
+ const files = ask `the files on disc D that belong to a US visa application` [searchFiles, readFile]: string[];
286
+
287
+ console.log(JSON.stringify(files));
288
+ ```
289
+
290
+ Parentheses run a function once with model-filled arguments (the
291
+ function-calling project); brackets offer functions the model may call zero
292
+ or more times. Cap the loop with `.withMaxTurns(n)` when a task should not
293
+ wander; return a failure as a value (`{ error: "no such file" }`) when the
294
+ model should recover from it, since a thrown error ends the ask.
295
+
296
+ ## Data in place: a bare value in the instruction
297
+
298
+ When the model needs a value beside the words that explain it, write the
299
+ value bare between texts (an ask sequence): it reaches the model as a
300
+ named input block where you wrote it. A `${}` hole splices the value into
301
+ your words instead — pick by what the model should treat it as.
302
+
303
+ ```tsi
304
+ // src/main.tsi
305
+ interface Person {
306
+ name: string;
307
+ }
308
+ declare const project: { id: string; lead: string };
309
+ declare const policy: string;
310
+
311
+ // data where it stands: <input name="project"> … </input>, then your next words
312
+ export const owner = ask `the person responsible for` project `given this policy` policy: Person;
313
+
314
+ // words: the id is spliced into the sentence
315
+ export const lead = ask `the lead of project ${project.id}`: string;
316
+ ```
317
+
318
+ The same shape puts words on a call (`` ask `register the applicant` createUser(`user id`: string) ``)
319
+ and words between tool lists (`` ask `find` [searchFiles] `or, when stale,` [readDir] `done`: string[] ``).
320
+ The kind follows the parts, never the data: texts and data extract, a call
321
+ with a typed slot is function calling, lists are tool use.
322
+
261
323
  ## An agent loop with a live context statement
262
324
 
263
325
  A context statement is read at EACH ask that sees it, so one placed before a
@@ -135,10 +135,11 @@ export const stored = ..`the user's full name`: string; // stored: dots
135
135
 
136
136
  > expected a type after the extractor's `:` on the same line — write `…`: T.
137
137
 
138
- The colon spelling `` ask `the ticket id`: string `` needs the colon glued to
139
- the backtick (a colon after a space is a ternary's, never the extractor's) and
140
- the type on the colon's line. Two more traps come with it, both from the type
141
- having no closing delimiter:
138
+ The colon spelling `` ask `the ticket id`: string `` takes the colon glued,
139
+ spaced or on the next line (`` `p` : string `` and `` `p`\n : string `` are the
140
+ same ask — a line break alone never ends an ask) and needs the type on the
141
+ colon's line. Two more traps come with it, both from the type having no
142
+ closing delimiter:
142
143
 
143
144
  ```tsi
144
145
  interface User { name: string }
@@ -164,6 +165,12 @@ comparison). Chain intent methods after a parenthesized extractor
164
165
  > parentheses, or assign it to a local first (`await`, `ask` and `this` cannot
165
166
  > be values).
166
167
 
168
+ The same code, with ask-site wording, covers an ask sequence: `this`,
169
+ `await`, `ask`, `function` or `class` where a value would start after a
170
+ text, a value ending in a backtick (`` ask `p` new Foo `more` ``), and text
171
+ after the type (`` ask `p`: T `more` `` — the type ends the sequence; move
172
+ the text before `: T`).
173
+
167
174
  ```tsi
168
175
  export infer function total(.items: string[], a: number, b: number) {
169
176
  // WRONG — an operator after a value
@@ -345,6 +352,101 @@ A bare template with no type, `` createTicket(`a short ticket title`, 2) ``,
345
352
  is not a slot at all: it is a plain string argument and the call stays an
346
353
  ordinary call — no error, and no model request.
347
354
 
355
+ ## NOLA1021 / NOLA1022 — a tool list holds functions
356
+
357
+ > a tool list holds functions — a name, a member, an expression that is one function, or a group (a module or an object of functions, an array of them), bare or spread; a literal cannot be one, and the list is not empty.
358
+
359
+ The brackets after an ask's template are a TOOL LIST (tool use). An entry is
360
+ a value: a name or dotted chain (one function, or a group — a module, an
361
+ object of functions, an array of them), a spread of a group, or an
362
+ expression that is one function. A literal (a string, a number, an arrow,
363
+ an object or array literal) or an empty list is NOLA1021, and a call written
364
+ in the list — describing the arguments in place — is reserved (NOLA1022):
365
+
366
+ ```tsi
367
+ // not-checked — WRONG
368
+ const a = ask `find the files` [searchFiles, "readFile"]: string[]; // NOLA1021
369
+ const b = ask `get the user` [getUser(`the id`: string)]: User; // NOLA1022 — reserved
370
+ ```
371
+
372
+ ```tsi
373
+ // RIGHT
374
+ const a = ask `find the files` [searchFiles, fs.readFile]: string[];
375
+ const g = ask `find the files` [fileTools, ...moreTools]: string[]; // a group by name, a group flattened
376
+ const b = ask `get the user` [getUser]: User; // the arguments come from getUser's signature
377
+ ```
378
+
379
+ ## NOLA1023 — a typed ask in a ternary's consequent
380
+
381
+ > a typed extractor directly in a ternary's consequent — parenthesize it: `cond ? (ask `p`: T) : y`.
382
+
383
+ Two colons with different meanings would sit side by side, so the typed ask
384
+ goes in parentheses (an untyped ask, a call intent and the `<T>` form need
385
+ none; the alternate position is free):
386
+
387
+ ```tsi
388
+ // not-checked — WRONG
389
+ const t = cond ? ask `the title`: string : "untitled";
390
+ ```
391
+
392
+ ```tsi
393
+ // RIGHT
394
+ const t = cond ? (ask `the title`: string) : "untitled";
395
+ ```
396
+
397
+ ## NOLA2018 — a tool list entry the compiler cannot use
398
+
399
+ An entry that is neither one function nor a group (a value, a type-only
400
+ import, a class, a union of function types — `const f = cond ? a : b` —
401
+ `any`), a function under a spread, a group reached through an expression
402
+ (`[groups[0]]` — name it or spread it), a method reached through a call
403
+ (`api.client().search` — name the object first), a group with nothing
404
+ callable, or
405
+ two tools that would share one name on the wire (`a.b` beside `a_b`; the
406
+ same function twice; a spread's `search` beside another). Name a function
407
+ or a group, keep the names distinct, offer colliding groups bare so their
408
+ names carry the prefix. A parameter the walk cannot derive — a rest
409
+ parameter, a `Map`, a generic parameter — is NOLA2002 at the entry; a group
410
+ member the walk cannot use fails the group, naming the member.
411
+
412
+ ## NOLA2019 — a reserved shape of the ask sequence
413
+
414
+ Tools beside a call: `` ask `use` [searchFiles] `then` createUser(`id`: string) ``
415
+ is the kind where the loop runs and the call's slots are the answer —
416
+ designed, not built. Offer the function as a tool, or make the call its own
417
+ ask after the tool-use ask. Also a `..` sequence inside a call's slot
418
+ (`` fn(..`t` user: string) ``): a slot's instruction is one text.
419
+
420
+ ## NOLA2020 — an ask sequence that cannot mean one thing
421
+
422
+ Two calls with typed slots (an ask makes one call — split the asks); a
423
+ `: T` beside a call (the call's result is the type — drop it); text after
424
+ a bare call (`` ask f(`x`: string) `more` `` — put the words first:
425
+ `` ask `more` f(`x`: string) ``); a call with typed slots inside a data
426
+ value (`` ask `use` (createUser(`id`: string)) `go`: T `` — write it bare
427
+ between texts, it is the action, not data).
428
+
429
+ ## NOLA3019 – NOLA3022 — tool use at run time
430
+
431
+ - NOLA3019: the resolved model cannot request tool calls (a hand-written
432
+ `LanguageModel`, a mock without `{ tools: true }`, `typesafe()`, a vendor
433
+ model with `toolUse: false`). Route the
434
+ ask to `openai` / `anthropic` / `google` / `"nola"`, or brand the mock.
435
+ Refused before the network.
436
+ - NOLA3020: the model kept calling tools past the cap (`.withMaxTurns(n)`,
437
+ else `ask.maxTurns`, 10). Raise the cap, give it tools that answer in fewer
438
+ calls, or sharpen the task. Definitive; the transcript rides the error.
439
+ - NOLA3021: a tool threw. Fix the tool, or return the failure as a value
440
+ when the model should recover from it. Invalid arguments never reach the
441
+ function — they go back to the model as that call's result.
442
+ - NOLA3022: the list could not be built when the ask ran — a value that is
443
+ not what its type promised (a group member that is not a function, a
444
+ tuple of another length), a function with no name (an anonymous arrow in
445
+ an array), or two tools with one wire name. Before the network.
446
+
447
+ `.withRetry(n)` on a tool-use ask re-runs the WHOLE loop, tools included:
448
+ only with tools that are safe to repeat.
449
+
348
450
  ## NOLA3010 — bare `await` on a raw extract or call intent
349
451
 
350
452
  > extract/call intents carry no construction scope — only `ask` supplies their
@@ -465,13 +567,19 @@ export infer function route(.ticket: string) {
465
567
  }
466
568
  ```
467
569
 
468
- ## NOLA3015 — running `.tsi` under Bun or Deno
570
+ ## NOLA3015 — the Node loader under Bun or Deno
571
+
572
+ `nola run` and `node --import nola-lang/register` ARE Node's module-hooks API;
573
+ Bun and Deno do not run them. On Bun, run `.tsi` from source through
574
+ `@nola-lang/bun` instead: `preload = ["@nola-lang/bun/preload"]` in
575
+ `bunfig.toml`, then `bun src/main.tsi` (see config.md, "Running on Bun").
576
+ Deno cannot load `.tsi` at all — run on Node. Any package manager is fine
577
+ (`bun install`, `bun run start`: the `nola` bin is a Node script).
578
+
579
+ ## NOLA3023 — Bun older than 1.3
469
580
 
470
- The loader is Node's module-hooks API; Bun and Deno do not run it. Any package
471
- manager is fine (`bun install`, `bun run start`, `pnpm start`, `yarn start` —
472
- the `nola` bin is a Node script), but `bun --bun`, `bun src/main.ts` and
473
- `deno run` cannot load `.tsi`. Run on Node: `nola run` or
474
- `node --import nola-lang/register src/main.ts`.
581
+ `@nola-lang/bun` needs `process.getBuiltinModule` (Bun ≥ 1.3). Run
582
+ `bun upgrade`.
475
583
 
476
584
  ## Other things that will not parse
477
585
 
@@ -481,6 +589,12 @@ the `nola` bin is a Node script), but `bun --bun`, `bun src/main.ts` and
481
589
  - `infer` on a method, arrow function or function expression — top-level
482
590
  function declarations only.
483
591
  - `fn(..)` — the bare derive-all call form is reserved (NOLA1004).
484
- - `` ask `p` : T `` — a colon after a space is not the extractor's type; only a
485
- colon glued to the closing backtick is (`` `p`: T ``). In a ternary the
486
- spaced colon is the separator.
592
+ - `` cond ? ask `p` : y `` — a colon on the extractor's line is its type,
593
+ spaced or glued, so the ask reads `y` as its type and the ternary loses its
594
+ alternate: NOLA1023. A typed ask standing bare as a consequent is the same
595
+ code — parenthesize it: `` cond ? (ask `p`: T) : y ``, on one line or
596
+ three: the formatted `` cond\n ? ask `p`\n : y `` reads `y` as the type
597
+ too, since a line break alone never ends an ask.
598
+ - `` fn(`x` [tools]: T) `` — a tool list is never a call slot; a bracket
599
+ there keeps its JavaScript meaning. The list goes on the ask site or the
600
+ stored `..` form only.
@@ -236,16 +236,22 @@ export infer function lookup(text: string) {
236
236
  - With no type, the extractor asks for free text: the wire schema is a plain
237
237
  string and the static TS type is `any`. Give every extractor an explicit
238
238
  type unless you deliberately want unconstrained prose.
239
- - TWO SPELLINGS OF THE TYPE, one construct. `: T` — a colon GLUED to the
240
- closing backtick, like an annotation — is the taught form and what every
241
- example writes. `<T>` — a type argument after the backtick — is the same
242
- construct: `` ask `the ticket id`<string> ``, `` ..`x`<T> ``,
239
+ - TWO SPELLINGS OF THE TYPE, one construct. `: T` — a colon after the
240
+ closing backtick on the same line, glued like an annotation or spaced
241
+ (`` `p`: T `` and `` `p` : T `` are the same ask) — is the taught form and
242
+ what every example writes. `<T>` — a type argument after the backtick — is
243
+ the same construct: `` ask `the ticket id`<string> ``, `` ..`x`<T> ``,
243
244
  `` fn(`x`<T>) ``, `` ask `q`<Choice<C>> `` — same AST, same emit, same
244
245
  definition, same ledger entries; legal everywhere `: T` is. Rules for the
245
- colon: a colon after whitespace is never the extractor's (so
246
- `` cond ? ask `p` : fallback `` stays a ternary over an untyped ask, and
247
- `` cond ? ask `p`: number : fallback `` asks for a number); the type starts
248
- on the colon's line (NOLA1018 otherwise); a type NAME followed by `.`
246
+ colon: it may follow glued, spaced, or on the next line (a line break
247
+ alone never ends an ask); an ask directly
248
+ in a ternary's consequent is parenthesized, on one line or three,
249
+ `` cond ? (ask `p`: number) : fallback `` — bare it is NOLA1023, because
250
+ the colon after the ask is its type and `` cond ? ask `p` : fallback ``
251
+ would read `fallback` as that type (a call intent and the `<T>` form need
252
+ no parentheses, and the alternate position is free:
253
+ `` cond ? y : ask `p`: number ``); the
254
+ type starts on the colon's line (NOLA1018 otherwise); a type NAME followed by `.`
249
255
  continues the type (`` `p`: User.withRetry(2) `` is the type
250
256
  `User.withRetry` called with 2) — chain intent methods after a
251
257
  parenthesized extractor (`` ask (..`p`: User).withRetry(2) ``), a
@@ -256,7 +262,7 @@ export infer function lookup(text: string) {
256
262
  starts an argument, or a property value / element of a plain object or
257
263
  array literal written in the arguments: `` fn(`x`: T) ``,
258
264
  `` api.save({ note: `x`: T }) ``, `` fn([`x`: T]) ``. Everywhere else it is
259
- required: `` const i = ..`x`: T ``, `` fn(cond ? ..`x`: T : y) ``,
265
+ required: `` const i = ..`x`: T ``, `` fn(cond ? (..`x`: T) : y) ``,
260
266
  `` fn(...[..`x`: T]) ``, `` new Foo(..`x`: T) ``,
261
267
  `` ask (..`x`: T).withRetry(2) `` (parenthesized: the template is no longer
262
268
  a first token — `` fn((`x`: T)) `` is a syntax error too). A `` `x`<T> ``
@@ -441,6 +447,125 @@ Two consequences to keep in mind:
441
447
  round trips only. Once the arguments are filled, the callee's own promise
442
448
  runs to completion, the same as a plain `await fn()` in your code.
443
449
 
450
+ ## Tool use
451
+
452
+ A tool-use ask offers the model functions it may call before it answers —
453
+ a bracketed list of function names between the task and the type:
454
+
455
+ ```tsi
456
+ import { readFile, searchFiles } from "./tools.js";
457
+
458
+ export infer function find(.query: string) {
459
+ return ask `the files on disc D that belong to a US visa application` [searchFiles, readFile]: string[];
460
+ }
461
+ ```
462
+
463
+ ```tsi
464
+ import * as fileTools from "./tools.js";
465
+ import { dbTools } from "./db.js";
466
+
467
+ const report = ask `analyze this report` [fileTools, dbTools]: Report; // the model sees fileTools.search, dbTools.search, …
468
+ const files = ask `the matching files` [...fileTools]: string[]; // flat: search, readFile
469
+ ```
470
+
471
+ Brackets OFFER, parentheses RUN: `` ask getUser(`id`: string) `` is a call
472
+ intent (getUser runs exactly once, the value is what it returned);
473
+ `` ask `the user's name` [getUser]: string `` offers getUser (the model
474
+ calls it zero or more times, the value is the model's answer). Reach for
475
+ the call when you want the function's own result, for the list when the
476
+ answer needs lookups first.
477
+
478
+ - Each provider call is a TURN: the model answers or requests tool calls;
479
+ Nola validates the arguments against the function's parameters, runs
480
+ the calls in the reply's order, renders each result (`fmt`: a string
481
+ verbatim, anything else JSON) and asks again with the transcript. An
482
+ unknown tool name or invalid arguments never reach the function — they
483
+ go back as that call's result. The answer then gets the usual validation
484
+ and single correction turn (not counted against the cap).
485
+ - Entries are VALUES, judged by the type checker: a name or dotted chain
486
+ names one function (`searchFiles`, `fs.readFile`) or a GROUP — a module
487
+ namespace, an object of functions, an array of them — whose functions
488
+ the model sees as `group.member`; `[...group]` offers them flat by their
489
+ own names; any other expression that is one function (`tools[0]`) is
490
+ named by the function. Group tools by what they are for: two groups
491
+ sharing a `search` never collide, a flat list with two does (NOLA2018).
492
+ The contract derives from each signature: one object over the
493
+ parameters, by name in order; optional/defaulted parameters optional; the
494
+ JSDoc summary is the description, `@param` tags describe the properties.
495
+ A member chain keeps its receiver (its object must be a plain chain —
496
+ a name, `this`, dotted / indexed / optional access; `api.client().search`
497
+ is NOLA2018, name the object first); a group's functions are called on
498
+ the group. One declared signature per tool: a union of functions, `any`,
499
+ `unknown`, a class, a group with nothing callable, a group reached
500
+ through an expression (name it or spread it): NOLA2018. An underivable
501
+ parameter (a rest parameter, a Map): NOLA2002 at the entry; a group
502
+ member it cannot use fails the group, naming the member. At run time a
503
+ list whose values do not match their types, an unnamed function or a
504
+ name collision is NOLA3022 before the network.
505
+ - The cap: `.withMaxTurns(n)` on the intent, else config `ask.maxTurns`
506
+ (10); past it NOLA3020, definitive, transcript attached. A throwing tool
507
+ is NOLA3021 with the error as `cause`; return a failure as a value when
508
+ the model should recover from it. `.withRetry(n)` re-runs the WHOLE
509
+ loop, tools included; `.withTimeout` bounds provider calls only.
510
+ - The model must support tool calls: `openai`, `anthropic`, `google`,
511
+ `model: "nola"`; `mockProvider(…, { tools: true })` with `toolCalls([{
512
+ name, args }])` values scripting tool turns; `replay()` of a recorded
513
+ ledger. Anything else is NOLA3019 before the network; `fallback` /
514
+ `roundRobin` skip members without support on a tool-use ask. A vendor
515
+ factory's `toolUse` option, beside `structuredOutputs`, names the
516
+ strategy (`"native"`, the default and the only one today) or marks a
517
+ host without tool calling (`toolUse: false` — unbranded, NOLA3019,
518
+ skipped by a group); a group mixing strategies is NOLA3003.
519
+ - Where the list is legal: directly after the template at the ask site
520
+ (`ask`, `ask with <name>`) and on the stored form (`` ..`p` [fn]: T ``);
521
+ the type follows the list (`]: T` — or `] : T` — or `]<T>`). Never in a call's
522
+ slots. A call-shaped entry (`` [getUser(`id`: string)] ``) is reserved
523
+ (NOLA1022); a literal or an empty list is NOLA1021. In a ternary's
524
+ consequent, parenthesize the typed ask (NOLA1023), as always.
525
+ - Receipts carry `turns` (each call's arguments, result or error,
526
+ duration); telemetry fires `onToolCallStart` / `onToolCallEnd`; the
527
+ terminal trace and `nola console` show the calls under the ask.
528
+
529
+ ## The ask sequence
530
+
531
+ After the first text, texts and values alternate (spec 2026-10-12):
532
+
533
+ ```tsi
534
+ const owner = ask `the person responsible for` project `given this policy` policy: Person;
535
+ const user = ask `register the applicant` createUser(`user id`: string, `email`: string);
536
+ const files = ask `find` [searchFiles, readFile] `or, when stale,` [readDir] `and return paths`: string[];
537
+ ```
538
+
539
+ - A bare value (a name, a member chain, a literal, `new X()`, a plain call,
540
+ a parenthesized expression) is DATA: the model reads an `<input name>`
541
+ block where you wrote it, named by what you wrote, with its type on the
542
+ wire; `${x}` inside a text stays your words. Prefer a hole for a literal.
543
+ - A call with a typed slot is the ACTION: the kind is function calling, the
544
+ call's result is the value, no `: T`. A `[list]` offers tools; several
545
+ lists union (the same entry in two lists is one tool), and the words
546
+ between them are guidance, nothing enforces "or".
547
+ - The kind follows the parts: texts and data → extract; one call → function
548
+ calling; one or more lists → tool use; a list beside a call is reserved
549
+ (NOLA2019); two calls, `: T` beside a call, text after a bare call
550
+ (`` ask f(`x`: T) `more` `` — put the words first), or a call with typed
551
+ slots inside a data value: NOLA2020.
552
+ - Two values never touch (`user [x]` is an index, `user (x)` a call); text
553
+ first; `: T` follows the last part (a backtick, `]` or the value) — glued,
554
+ spaced, or on its own line; `<T>` only after a text or a list. `this`,
555
+ `await`, `ask`, an unparenthesized function and a value ending in a
556
+ template are NOLA1020.
557
+ - An ask may span lines, one part per line: a line break alone never ends
558
+ it. After a text the next part may start on the next line, and the type
559
+ may stand on its own line; the ask ends at `;`, a closer, an operator, a
560
+ statement keyword (`const`, `return`, `type X`, `let x`, …) or its type.
561
+ After a VALUE a name on the next line is a new statement, as in JS. The
562
+ one trap: semicolon-free code after a call sequence that ends on a text —
563
+ `` …`done`\nnotify(files) `` takes `notify(files)` as a value; end the ask
564
+ with `;`.
565
+ A call's slot stays one text (a `..` sequence there is NOLA2019). A
566
+ sequence lowers to an instruction ARRAY of parts; a one-text ask lowers
567
+ as before.
568
+
444
569
  ## Values in an instruction: `.x` and `${x}`
445
570
 
446
571
  Two spellings put a value in front of the model — pick by what the model