create-nola-lang 0.1.11 → 0.1.13

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 (48) hide show
  1. package/README.md +19 -13
  2. package/dist/agents.d.ts +13 -1
  3. package/dist/agents.js +107 -9
  4. package/dist/checkout.d.ts +20 -0
  5. package/dist/checkout.js +48 -1
  6. package/dist/{chunk-MAULRZZD.js → chunk-LUSVIOAN.js} +248 -77
  7. package/dist/flow.d.ts +19 -5
  8. package/dist/flow.js +139 -51
  9. package/dist/ide.d.ts +2 -2
  10. package/dist/ide.js +12 -11
  11. package/dist/index.d.ts +1 -1
  12. package/dist/index.js +5 -1
  13. package/dist/launch.d.ts +2 -0
  14. package/dist/launch.js +1 -0
  15. package/dist/main.js +1 -1
  16. package/dist/providers.d.ts +7 -4
  17. package/dist/providers.js +3 -2
  18. package/dist/registry.d.ts +30 -2
  19. package/dist/registry.js +34 -7
  20. package/dist/scaffold.d.ts +20 -6
  21. package/dist/scaffold.js +42 -13
  22. package/package.json +1 -1
  23. package/skills/nola/SKILL.md +8 -5
  24. package/skills/nola/references/config.md +22 -13
  25. package/skills/nola/references/patterns.md +92 -17
  26. package/skills/nola/references/pitfalls.md +66 -19
  27. package/skills/nola/references/syntax.md +98 -28
  28. package/templates/_providers/typesafe.config.ts +18 -0
  29. package/templates/feature-extraction/README.md +20 -0
  30. package/templates/feature-extraction/nola.config.ts +14 -0
  31. package/templates/feature-extraction/nola.replay.jsonl +2 -0
  32. package/templates/feature-extraction/package.json +22 -0
  33. package/templates/feature-extraction/src/main.tsi +22 -0
  34. package/templates/function-calling/README.md +21 -0
  35. package/templates/function-calling/nola.config.ts +14 -0
  36. package/templates/function-calling/nola.replay.jsonl +1 -0
  37. package/templates/function-calling/package.json +22 -0
  38. package/templates/function-calling/src/main.tsi +12 -0
  39. package/templates/function-calling/src/tickets.ts +17 -0
  40. package/templates/function-calling/tsconfig.json +12 -0
  41. package/templates/{starter → typescript-interop}/nola.config.ts +1 -1
  42. package/templates/{starter → typescript-interop}/src/person.tsi +1 -1
  43. package/templates/typescript-interop/tsconfig.json +12 -0
  44. /package/templates/{starter → feature-extraction}/tsconfig.json +0 -0
  45. /package/templates/{starter → typescript-interop}/README.md +0 -0
  46. /package/templates/{starter → typescript-interop}/nola.replay.jsonl +0 -0
  47. /package/templates/{starter → typescript-interop}/package.json +0 -0
  48. /package/templates/{starter → typescript-interop}/src/main.ts +0 -0
package/dist/registry.js CHANGED
@@ -1,5 +1,26 @@
1
+ /** What a template runs unless it names its own entry: the plain-TS main. */
2
+ export const DEFAULT_ENTRY = "src/main.ts";
1
3
  export const TEMPLATES = [
2
- { name: "starter", label: "typed extraction, runs offline out of the box", source: "builtin" },
4
+ { name: "feature-extraction", label: "extract typed data from context", source: "builtin", entry: "src/main.tsi" },
5
+ {
6
+ name: "function-calling",
7
+ label: "call an async function from a TypeScript file",
8
+ source: "builtin",
9
+ entry: "src/main.tsi",
10
+ },
11
+ {
12
+ name: "typescript-interop",
13
+ label: "an infer function example, imported and awaited from plain TypeScript",
14
+ source: "builtin",
15
+ },
16
+ {
17
+ name: "triage-ticket",
18
+ label: "ticket triage on a non-chat model: literal unions and booleans as typed questions",
19
+ source: "example",
20
+ featured: true,
21
+ entry: "src/main.tsi",
22
+ provider: { label: "typesafe.ai", envVar: "TYPESAFE_API_KEY" },
23
+ },
3
24
  { name: "empty", label: "nola.config + tsconfig only, bring your own code", source: "builtin" },
4
25
  { name: "file-ticket", label: "call intents: the model fills a function's arguments", source: "example" },
5
26
  { name: "extract-resume", label: "nested arrays of objects, JSDoc schema descriptions", source: "example" },
@@ -7,17 +28,23 @@ export const TEMPLATES = [
7
28
  { name: "classify-message", label: "closed label sets: union alias, string enum, inline union", source: "example" },
8
29
  { name: "chain-of-thought", label: "two asks sharing accumulating context", source: "example" },
9
30
  { name: "research-notes", label: "TS control flow orchestrating nola functions", source: "example" },
10
- {
11
- name: "triage-ticket",
12
- label: "ticket triage: literal unions and booleans as typed questions",
13
- source: "example",
14
- provider: { label: "typesafe.ai", envVar: "TYPESAFE_API_KEY" },
15
- },
16
31
  ];
32
+ /** The first menu's templates: the builtins and the featured example, in registry order. */
33
+ export function featuredNames() {
34
+ return TEMPLATES.filter((t) => t.source === "builtin" || t.featured).map((t) => t.name);
35
+ }
36
+ /** The examples behind "More examples…": every example the first menu does not carry. */
37
+ export function exampleNames() {
38
+ return TEMPLATES.filter((t) => t.source === "example" && !t.featured).map((t) => t.name);
39
+ }
17
40
  export function templateByName(name) {
18
41
  return TEMPLATES.find((t) => t.name === name);
19
42
  }
20
43
  export function templateNames() {
21
44
  return TEMPLATES.map((t) => t.name);
22
45
  }
46
+ /** The template's entry file — what `start` runs, launch.json runs and VS Code opens. */
47
+ export function entryFile(template) {
48
+ return (template === undefined ? undefined : templateByName(template)?.entry) ?? DEFAULT_ENTRY;
49
+ }
23
50
  //# sourceMappingURL=registry.js.map
@@ -6,14 +6,14 @@ export interface ScaffoldResult {
6
6
  }
7
7
  export interface ScaffoldOptions {
8
8
  name?: string;
9
- /** template name; default "starter" */
9
+ /** template name; default "feature-extraction" */
10
10
  template?: string;
11
11
  /** remove existing files in a non-empty target first (set only after an interactive confirm) */
12
12
  force?: boolean;
13
13
  /**
14
14
  * The inference provider chosen in the wizard; default "none" (the
15
15
  * template's own config). Any live provider replaces `nola.config.ts` with
16
- * that provider's config, skips the starter's replay ledger, and renders
16
+ * that provider's config, skips the offline templates' replay ledger, and renders
17
17
  * the matching README notes.
18
18
  */
19
19
  provider?: ProviderId;
@@ -28,6 +28,19 @@ export interface ScaffoldOptions {
28
28
  }
29
29
  /** The config a live provider choice writes over the template's own (`templates/_providers/<id>.config.ts`). */
30
30
  export declare function providerConfigUrl(provider: Exclude<ProviderId, "none">): URL;
31
+ /**
32
+ * The env var a scaffold's config reads: the chosen vendor's, else — under
33
+ * "none" — the one the template's own config names (a pinned template such
34
+ * as triage-ticket). Undefined for nola (the trial key lands in `.env`
35
+ * itself) and for an offline scaffold.
36
+ */
37
+ export declare function vendorEnvVar(template: string, provider: ProviderId): string | undefined;
38
+ /**
39
+ * The `.env.example` a vendor scaffold gets: the key's slot, to copy to
40
+ * `.env` and fill in. `.env.example` is the one env file the recommended
41
+ * `.gitignore` keeps trackable (`!.env.example`), so it can be committed.
42
+ */
43
+ export declare function envExample(envVar: string): string;
31
44
  /**
32
45
  * Add the recommended `.gitignore` to an example's files. Examples are copied
33
46
  * verbatim from `examples/`, where the monorepo's root ignore file covers them,
@@ -35,10 +48,11 @@ export declare function providerConfigUrl(provider: Exclude<ProviderId, "none">)
35
48
  */
36
49
  export declare function withRecommendedGitignore(files: Map<string, string>): Promise<Map<string, string>>;
37
50
  /**
38
- * The comment that opens src/main.ts — the file the scaffold lands the user
39
- * on (VS Code opens it as the active editor), so it carries the first three
40
- * things to do. The VS Code variant only ships with the editor step's
41
- * .vscode files, which are what make F5 and the extension prompt real.
51
+ * The comment that opens the entry file (src/main.ts, or the `.tsi` entry
52
+ * of a one-file template) — the file the scaffold lands the user on (VS Code opens it
53
+ * as the active editor), so it carries the first three things to do. The VS
54
+ * Code variant only ships with the editor step's .vscode files, which are
55
+ * what make F5 and the extension prompt real.
42
56
  */
43
57
  export declare function nextStepsComment(ide: "vscode" | "none", template: string): string;
44
58
  export declare function ownVersion(): Promise<string>;
package/dist/scaffold.js CHANGED
@@ -11,6 +11,23 @@ const TEMPLATES_DIR = fileURLToPath(new URL("../templates/", import.meta.url));
11
11
  export function providerConfigUrl(provider) {
12
12
  return new URL(`../templates/_providers/${provider}.config.ts`, import.meta.url);
13
13
  }
14
+ /**
15
+ * The env var a scaffold's config reads: the chosen vendor's, else — under
16
+ * "none" — the one the template's own config names (a pinned template such
17
+ * as triage-ticket). Undefined for nola (the trial key lands in `.env`
18
+ * itself) and for an offline scaffold.
19
+ */
20
+ export function vendorEnvVar(template, provider) {
21
+ return providerById(provider)?.envVar ?? (provider === "none" ? templateByName(template)?.provider?.envVar : undefined);
22
+ }
23
+ /**
24
+ * The `.env.example` a vendor scaffold gets: the key's slot, to copy to
25
+ * `.env` and fill in. `.env.example` is the one env file the recommended
26
+ * `.gitignore` keeps trackable (`!.env.example`), so it can be committed.
27
+ */
28
+ export function envExample(envVar) {
29
+ return `# Copy to .env and fill in — \`nola run\` applies .env before evaluating nola.config.ts.\n${envVar}=\n`;
30
+ }
14
31
  /** _gitignore ships underscored (npm pack strips nested .gitignore files). */
15
32
  const RENAMES = { _gitignore: ".gitignore" };
16
33
  /** The ONE recommended .gitignore — every scaffold gets it; no template keeps a copy of its own. */
@@ -26,20 +43,25 @@ export async function withRecommendedGitignore(files) {
26
43
  return files;
27
44
  }
28
45
  /** Files whose __NAME__/__VERSION__, README-note and __NEXT_STEPS__ placeholders are substituted. */
29
- const SUBSTITUTED = new Set(["package.json", "README.md", "main.ts"]);
46
+ const SUBSTITUTED = new Set(["package.json", "README.md", "main.ts", "main.tsi"]);
30
47
  /**
31
- * The comment that opens src/main.ts — the file the scaffold lands the user
32
- * on (VS Code opens it as the active editor), so it carries the first three
33
- * things to do. The VS Code variant only ships with the editor step's
34
- * .vscode files, which are what make F5 and the extension prompt real.
48
+ * The comment that opens the entry file (src/main.ts, or the `.tsi` entry
49
+ * of a one-file template) — the file the scaffold lands the user on (VS Code opens it
50
+ * as the active editor), so it carries the first three things to do. The VS
51
+ * Code variant only ships with the editor step's .vscode files, which are
52
+ * what make F5 and the extension prompt real.
35
53
  */
36
54
  export function nextStepsComment(ide, template) {
37
55
  if (ide === "vscode") {
38
- const breakpointIn = template === "starter" ? "src/person.tsi" : "your .tsi file";
56
+ const breakpointIn = templateByName(template)?.entry
57
+ ? "on the `ask` line below"
58
+ : template === "typescript-interop"
59
+ ? "in src/person.tsi"
60
+ : "in your .tsi file";
39
61
  return [
40
62
  "// Next steps in VS Code:",
41
63
  "// 1. Press F5 to run this file (.vscode/launch.json is already set up).",
42
- `// 2. Set a breakpoint in ${breakpointIn} and press F5 again to step through the ask.`,
64
+ `// 2. Set a breakpoint ${breakpointIn} and press F5 again to step through the ask.`,
43
65
  '// 3. Install the recommended "Nola" extension when VS Code offers it — IntelliSense,',
44
66
  "// go to definition and diagnostics inside .tsi files.",
45
67
  ].join("\n");
@@ -50,14 +72,14 @@ export function nextStepsComment(ide, template) {
50
72
  "// 2. Set up your editor (VS Code extension, debugging): https://nola.sh/docs/start/editor-setup/",
51
73
  ].join("\n");
52
74
  }
53
- /** Starter files that only make sense for the offline (replay) configuration. */
75
+ /** Template files that only make sense for the offline (replay) configuration. */
54
76
  const OFFLINE_ONLY = new Set(["nola.replay.jsonl"]);
55
77
  const README_NOTES = {
56
78
  offline: {
57
79
  START_NOTE: "works offline, no API key needed",
58
- PROVIDER_NOTE: "The starter runs offline: `nola.config.ts` replays answers from the committed\n" +
80
+ PROVIDER_NOTE: "This project runs offline: `nola.config.ts` replays answers from the committed\n" +
59
81
  "`nola.replay.jsonl` ledger. The ledger is keyed by the exact prompt, so once\n" +
60
- "you edit `src/person.tsi` or add your own asks, switch the config to a real\n" +
82
+ "you edit the `.tsi` file or add your own asks, switch the config to a real\n" +
61
83
  "model (see the comment in `nola.config.ts`): `model: \"nola\"` with a key from\n" +
62
84
  "`npx nola-lang key` (25 free runs), or your own provider and its key.",
63
85
  },
@@ -81,8 +103,8 @@ function readmeNotes(provider) {
81
103
  return {
82
104
  START_NOTE: `set ${def.envVar} in .env first`,
83
105
  PROVIDER_NOTE: `\`nola.config.ts\` sets \`model: ${def.model}\` — ${def.label} serves inference with the key it reads\n` +
84
- `from \`${def.envVar}\`. Put \`${def.envVar}=…\` in \`.env\` (git-ignored; \`nola run\` applies it) before the\n` +
85
- "first `npm start`, or switch models in `nola.config.ts`.",
106
+ `from \`${def.envVar}\`. Copy \`.env.example\` to \`.env\` (git-ignored; \`nola run\` applies it) and fill in\n` +
107
+ `\`${def.envVar}\` before the first \`npm start\`, or switch models in \`nola.config.ts\`.`,
86
108
  };
87
109
  }
88
110
  export async function ownVersion() {
@@ -108,19 +130,22 @@ async function prepareTarget(absRoot, force) {
108
130
  export async function scaffold(targetDir, opts = {}) {
109
131
  const root = targetDir;
110
132
  const absRoot = resolve(targetDir);
111
- const template = opts.template ?? "starter";
133
+ const template = opts.template ?? "feature-extraction";
112
134
  const def = templateByName(template);
113
135
  if (!def)
114
136
  throw new Error(`unknown template "${template}" (valid: ${templateNames().join(", ")})`);
115
137
  await prepareTarget(absRoot, opts.force ?? false);
116
138
  const name = opts.name ?? basename(absRoot);
117
139
  const version = await ownVersion();
140
+ const envVar = vendorEnvVar(template, opts.provider ?? "none");
118
141
  if (def.source === "example") {
119
142
  const dev = await devExamplesDir();
120
143
  const exampleFiles = await withRecommendedGitignore(dev ? await collectExampleFromDisk(dev, template) : await fetchExampleFromGitHub(template, version));
121
144
  const manifest = exampleFiles.get("package.json");
122
145
  if (manifest)
123
146
  exampleFiles.set("package.json", rewriteExamplePackageJson(manifest, { name, version }));
147
+ if (envVar !== undefined && !exampleFiles.has(".env.example"))
148
+ exampleFiles.set(".env.example", envExample(envVar));
124
149
  for (const [relPath, content] of exampleFiles) {
125
150
  const target = join(absRoot, relPath);
126
151
  await mkdir(join(target, ".."), { recursive: true });
@@ -168,6 +193,10 @@ export async function scaffold(targetDir, opts = {}) {
168
193
  await writeFile(join(absRoot, ".gitignore"), await readFile(GITIGNORE_URL, "utf8"));
169
194
  files.push(".gitignore");
170
195
  }
196
+ if (envVar !== undefined) {
197
+ await writeFile(join(absRoot, ".env.example"), envExample(envVar));
198
+ files.push(".env.example");
199
+ }
171
200
  return { root, files: files.sort() };
172
201
  }
173
202
  //# sourceMappingURL=scaffold.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-nola-lang",
3
- "version": "0.1.11",
3
+ "version": "0.1.13",
4
4
  "description": "Scaffold a new Nola project (npm create nola, or npm create nola-lang)",
5
5
  "keywords": [
6
6
  "nola",
@@ -22,7 +22,7 @@ import type { Person } from "./types.js";
22
22
  // thenable Intent<T>. `await`ing it (or `ask`) runs the inference.
23
23
  infer function extractPerson(.text: string) {
24
24
  // `ask` resolves an intent the way `await` resolves a promise.
25
- const person = ask ..`Extract the person described in the text`<Person>;
25
+ const person = ask `Extract the person described in the text`<Person>;
26
26
  return person;
27
27
  }
28
28
  ```
@@ -33,10 +33,13 @@ infer function extractPerson(.text: string) {
33
33
  - `.name: T` parameters are CONTEXT parameters: their values are shown to
34
34
  the LLM. Plain (no dot) parameters are ordinary values the LLM never
35
35
  sees. `.` is only legal on infer-function parameters. Rule of thumb:
36
- ONE dot in (`.name` — the value flows into the model), TWO dots out
37
- (`` ..`prompt` `` — a value comes out of it).
38
- - `` ask ..`prompt`<T> `` — an extractor: asks the LLM for a `T`.
39
- `${...}` interpolation works inside the backticks.
36
+ one dot marks a value flowing INTO the model (`.name`); a template after
37
+ `ask` is a value coming OUT.
38
+ - `` ask `prompt`<T> `` — an extractor: asks the LLM for a `T`.
39
+ `${...}` interpolation works inside the backticks. Anywhere that is NOT
40
+ directly after `ask` — a stored intent, a call-intent argument, an
41
+ object/array literal — spell it `` ..`prompt`<T> `` (a typed template
42
+ there without the dots is NOLA2014); `` ask ..`prompt` `` is still legal.
40
43
  - `` ask fn`hint`(...) `` — or a plain call whose arguments contain an
41
44
  extractor — is a call intent (the LLM fills the extractor-shaped
42
45
  arguments, then the function runs). Only `` fn`hint`(...) `` carries
@@ -42,11 +42,18 @@ export default defineConfig({
42
42
  `apiKeyEnv` (default `"OPENAI_API_KEY"`), `baseUrl` and `fetch`.
43
43
 
44
44
  `typesafe()` (typesafe.ai's Jev, reads `TYPESAFE_API_KEY`, model defaults to
45
- `jev-latest`) is NOT a chat model: it serves only asks whose output type is a
46
- string or number literal union, a boolean, or a flat object of those, and
47
- fails definitively before the network on anything else. Use it behind
48
- `fallback([typesafe(), openai({ model: "gpt-5-mini" })])` so other asks
49
- escalate to a general model. Never scaffold it as the only model.
45
+ `jev-latest`) is NOT a chat model: it is the DECISION model that answers the
46
+ intrinsic `Choice<{…}>` / `Scale<[…]>` / `Prob` types (see syntax.md) beside
47
+ the plain forms — a string or number literal union, a boolean, or a flat object
48
+ of those — and fails definitively before the network on anything else. The
49
+ contextual values (`.params`, `const .x`) travel as its JSON state and each
50
+ property's JSDoc is that question's instructions. An ask whose type contains a
51
+ `Choice` / `Scale` / `Prob` on a chat model is NOLA3018 before the network, so
52
+ route those by name (`model: { default: openai(…), decision: typesafe() }` +
53
+ `ask with decision`), or use `fallback([typesafe(), openai({ model:
54
+ "gpt-5-mini" })])` so plain asks escalate. `threshold` (default 0.5, or
55
+ `.withParams({ providerOptions: { threshold } })` per ask) is the plain-boolean
56
+ cut-off: `true` strictly above it. Never scaffold it as the only model.
50
57
 
51
58
  `nola` is a NAMESPACE, not a function (`nola({})` is a type error).
52
59
  The platform model is written as the STRING `"nola"` — the Nola platform
@@ -78,9 +85,11 @@ is implied — traces go to a Nola console only when `telemetry` lists its
78
85
  URL (or `nola.tracer()`).
79
86
 
80
87
  `npm create nola` asks *Select an inference provider:* right after the
81
- template — Nola (25 free runs) first, then OpenAI / Anthropic / Gemini, then
82
- skip — and for Nola writes this config plus the key into `.env` (a vendor gets
83
- its own config, key left to the user); in an existing project `npx nola-lang
88
+ template — `nola: dev` (25 free hosted runs, no API key, suited for dev experiments) first, then OpenAI / Anthropic / Gemini /
89
+ typesafe.ai (its row brackets the caveat: literal unions and booleans only), then
90
+ skip — and for `nola: dev` writes this config plus the key into `.env` (a vendor gets
91
+ its own config and a `.env.example` with the key's empty slot, e.g. `OPENAI_API_KEY=`,
92
+ to copy to `.env`; the key itself is left to the user); in an existing project `npx nola-lang
84
93
  key` mints one (and `npx nola-lang init --add --provider nola` does that plus
85
94
  the config and deps).
86
95
  A machine gets one anonymous trial; later projects sign in
@@ -115,8 +124,8 @@ export default defineConfig({
115
124
 
116
125
  ```tsi
117
126
  export infer function summarize(.text: string) {
118
- const draft = ask with fast ..`a rough summary`<string>;
119
- return ask with careful ..`a polished summary of: ${draft}`<string>;
127
+ const draft = ask with fast `a rough summary`<string>;
128
+ return ask with careful `a polished summary of: ${draft}`<string>;
120
129
  }
121
130
  ```
122
131
 
@@ -223,11 +232,11 @@ the providers package:
223
232
  "check": "nola check"
224
233
  },
225
234
  "dependencies": {
226
- "@nola-lang/providers": "^0.1.11",
227
- "@nola-lang/runtime": "^0.1.11"
235
+ "@nola-lang/providers": "^0.1.13",
236
+ "@nola-lang/runtime": "^0.1.13"
228
237
  },
229
238
  "devDependencies": {
230
- "nola-lang": "^0.1.11",
239
+ "nola-lang": "^0.1.13",
231
240
  "typescript": "^5.6.0"
232
241
  },
233
242
  "engines": { "node": ">=22.18" }
@@ -1,10 +1,85 @@
1
1
  # Nola patterns — worked examples
2
2
 
3
- ## The starter project, end to end
3
+ ## The feature-extraction project: one .tsi file is the program
4
4
 
5
- This is the shape every Nola project takes: `.tsi` files hold the infer
5
+ The smallest Nola program is a single `.tsi` file run directly — `ask` is
6
+ legal at the top level, a bare template literal as the FIRST statement is
7
+ the instruction for the whole file, and `const .x` bindings are context the
8
+ model sees at every ask that follows them (`npm create nola` scaffolds this
9
+ shape as `feature-extraction`):
10
+
11
+ ```tsi
12
+ // src/main.tsi — run with: nola run src/main.tsi
13
+ `You read short professional bios. Answer from the text alone; never invent facts.`
14
+
15
+ interface Person {
16
+ name: string;
17
+ age: number;
18
+ employer: string;
19
+ job: string;
20
+ }
21
+
22
+ const .message = "Alice Smith, 32, is a staff engineer at Acme Corp working on distributed systems.";
23
+
24
+ const person = ask `the person described in the text`<Person>;
25
+
26
+ // declared after the first ask, so only the second ask sees it — one answer feeds the next
27
+ const .role = person.job;
28
+ const seniority = ask `the seniority level the role implies`<"junior" | "mid" | "senior" | "staff">;
29
+
30
+ console.log(JSON.stringify({ ...person, seniority }));
31
+ ```
32
+
33
+ Rules that matter here: the instruction literal must be the very first
34
+ statement (a comment before it is fine, an `import` or a type is not); a
35
+ `.` binding is visible to the asks declared after it in the same or an
36
+ enclosing block, never to its own initializer; `ask` at the top level is
37
+ legal in the module body and top-level blocks/loops, not inside a plain
38
+ function or callback (NOLA2001). When a script outgrows one file, move the
39
+ asks into an `infer function` and call it from plain TypeScript — the shape
40
+ below.
41
+
42
+ ## The function-calling project: a top-level call intent
43
+
44
+ The same one-file shape, with the other feature (`npm create nola` scaffolds
45
+ it as `function-calling`): the top-level `ask` is a CALL INTENT — the model
46
+ fills the extractor-shaped arguments of an ordinary async function that lives
47
+ in a plain `.ts` file next door, and the call runs with them.
48
+
49
+ ```ts
50
+ // src/tickets.ts — plain TypeScript, nothing Nola about it
51
+ export interface Ticket { id: string; title: string; priority: number }
52
+ const tickets: Ticket[] = [];
53
+ export async function createTicket(title: string, priority: number): Promise<Ticket> {
54
+ const ticket = { id: `T-${tickets.length + 1}`, title, priority };
55
+ tickets.push(ticket);
56
+ return ticket;
57
+ }
58
+ ```
59
+
60
+ ```tsi
61
+ // src/main.tsi — run with: nola run src/main.tsi
62
+ import { createTicket } from "./tickets.js";
63
+
64
+ const .message = "Hi, I can't log in since this morning and I have a customer demo in an hour — please help!";
65
+
66
+ const ticket = ask createTicket(..`a short ticket title`<string>, ..`priority 1-5, where 1 is most urgent`<number>);
67
+
68
+ console.log(JSON.stringify(ticket));
69
+ ```
70
+
71
+ `ask` yields the callee's SETTLED value — a `Ticket`, not a `Promise<Ticket>`.
72
+ The import uses the NodeNext `./tickets.js` specifier for the on-disk
73
+ `tickets.ts`. Note there is no first-line instruction here: an `import` is a
74
+ statement, so a template literal placed after it is not the file's first
75
+ statement and would be a no-op.
76
+
77
+ ## The typescript-interop project, end to end
78
+
79
+ This is the shape a Nola library or app takes: `.tsi` files hold the infer
6
80
  functions, a plain `.ts` entry point calls them, and `nola run` executes the
7
- entry with the loader and `nola.config.ts` in place.
81
+ entry with the loader and `nola.config.ts` in place (`npm create nola`
82
+ scaffolds it as `typescript-interop`).
8
83
 
9
84
  ```
10
85
  my-app/
@@ -27,7 +102,7 @@ export interface Person {
27
102
  }
28
103
 
29
104
  export infer function extractPerson(.message: string) {
30
- const person = ask ..`the person described in the text`<Person>;
105
+ const person = ask `the person described in the text`<Person>;
31
106
  return person;
32
107
  }
33
108
  ```
@@ -69,8 +144,8 @@ export infer function solve(.problem: string) {
69
144
  // Both asks see `problem` (the contextual parameter). They do NOT see each
70
145
  // other's answers automatically — the reasoning is handed to the second ask
71
146
  // explicitly through `${}`.
72
- const reasoning = ask ..`think step by step about the problem before answering`;
73
- const answer = ask ..`the final numeric answer, given this reasoning: ${reasoning}`<number>;
147
+ const reasoning = ask `think step by step about the problem before answering`;
148
+ const answer = ask `the final numeric answer, given this reasoning: ${reasoning}`<number>;
74
149
  return { reasoning, answer };
75
150
  }
76
151
  ```
@@ -83,8 +158,8 @@ later prompt:
83
158
  export type Category = "billing" | "refund" | "fraud" | "other";
84
159
 
85
160
  export infer function classifyMessage(.message: string) {
86
- const category = ask ..`the category of the customer message`<Category>;
87
- const urgent = ask ..`does the message need urgent attention`<"yes" | "no">;
161
+ const category = ask `the category of the customer message`<Category>;
162
+ const urgent = ask `does the message need urgent attention`<"yes" | "no">;
88
163
 
89
164
  // plain TS from here on
90
165
  if (category === "fraud") return { category, urgent: true, escalate: true };
@@ -103,11 +178,11 @@ export interface Conclusion {
103
178
  }
104
179
 
105
180
  export infer function nextQuery(.question: string, .notes: string[]) {
106
- return ask ..`the single best search query to advance the research; keywords only`<string>;
181
+ return ask `the single best search query to advance the research; keywords only`<string>;
107
182
  }
108
183
 
109
184
  export infer function conclude(.question: string, .notes: string[]) {
110
- return ask ..`answer the research question using only the collected notes`<Conclusion>;
185
+ return ask `answer the research question using only the collected notes`<Conclusion>;
111
186
  }
112
187
  ```
113
188
 
@@ -204,7 +279,7 @@ export interface Invoice {
204
279
  }
205
280
 
206
281
  export infer function extractInvoice(.document: string) {
207
- return ask ..`the invoice data from the document`<Invoice>;
282
+ return ask `the invoice data from the document`<Invoice>;
208
283
  }
209
284
  ```
210
285
 
@@ -221,9 +296,9 @@ export enum Sentiment {
221
296
  }
222
297
 
223
298
  export infer function triage(.message: string) {
224
- const category = ask ..`the category of the customer message`<Category>;
225
- const sentiment = ask ..`the overall sentiment of the message`<Sentiment>;
226
- const urgent = ask ..`does the message need urgent attention`<"yes" | "no">;
299
+ const category = ask `the category of the customer message`<Category>;
300
+ const sentiment = ask `the overall sentiment of the message`<Sentiment>;
301
+ const urgent = ask `does the message need urgent attention`<"yes" | "no">;
227
302
  return { category, sentiment, urgent: urgent === "yes" };
228
303
  }
229
304
  ```
@@ -248,7 +323,7 @@ export interface Person {
248
323
  import type { Person } from "./models.js";
249
324
 
250
325
  export infer function extractPerson(.text: string) {
251
- return ask ..`the person described in the text`<Person>;
326
+ return ask `the person described in the text`<Person>;
252
327
  }
253
328
  ```
254
329
 
@@ -272,7 +347,7 @@ export type TreeNode = {
272
347
  };
273
348
 
274
349
  export infer function parseTree(.input: string) {
275
- return ask ..`the tree structure described in the input`<TreeNode>;
350
+ return ask `the tree structure described in the input`<TreeNode>;
276
351
  }
277
352
  ```
278
353
 
@@ -282,7 +357,7 @@ export infer function parseTree(.input: string) {
282
357
  export type CalendarEvent = { title: string; at: Date };
283
358
 
284
359
  export infer function nextEvent(.calendar: string) {
285
- const event = ask ..`the next event on the calendar`<CalendarEvent>;
360
+ const event = ask `the next event on the calendar`<CalendarEvent>;
286
361
  const when: Date = event.at; // a Date, not a string
287
362
  return when;
288
363
  }