@project-ajax/create 0.0.12 → 0.0.14

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/README.md CHANGED
@@ -4,6 +4,21 @@ A CLI for creating new Project Ajax workers projects.
4
4
 
5
5
  ## Usage
6
6
 
7
+ ### Interactive Mode
8
+
7
9
  ```bash
8
10
  npm init @project-ajax
9
11
  ```
12
+
13
+ ### Non-Interactive Mode
14
+
15
+ For CI environments or scripted usage:
16
+
17
+ ```bash
18
+ npm init @project-ajax -- --directory my-worker --project my-worker
19
+ ```
20
+
21
+ #### Options
22
+
23
+ - `--directory`, `-d` - Path to the new worker project (default: `.`)
24
+ - `--project`, `-p` - Project name (default: `my-worker`)
package/dist/index.js CHANGED
@@ -2,13 +2,14 @@
2
2
 
3
3
  // src/index.ts
4
4
  import fs from "fs";
5
- import os from "os";
6
5
  import path from "path";
6
+ import { fileURLToPath } from "url";
7
7
  import { parseArgs } from "util";
8
+ import * as prompts from "@inquirer/prompts";
8
9
  import chalk from "chalk";
9
- import { execa } from "execa";
10
10
  import ora from "ora";
11
- import prompts from "prompts";
11
+ var __filename = fileURLToPath(import.meta.url);
12
+ var __dirname = path.dirname(__filename);
12
13
  run().then(() => {
13
14
  process.exit(0);
14
15
  }).catch((err) => {
@@ -30,43 +31,35 @@ async function run() {
30
31
  }
31
32
  }
32
33
  });
33
- const promptQuestions = [];
34
- if (!values.directory) {
35
- promptQuestions.push({
36
- type: "text",
37
- name: "directoryName",
34
+ let directoryName = values.directory;
35
+ let projectName = values.project;
36
+ if (!directoryName) {
37
+ directoryName = await safePrompt({
38
38
  message: "Path to the new worker project:",
39
- initial: "."
39
+ default: ".",
40
+ required: true,
41
+ noTTY: "Provide the path to the new project with --directory"
40
42
  });
41
43
  }
42
- if (!values.project) {
43
- promptQuestions.push({
44
- type: "text",
45
- name: "projectName",
44
+ if (!projectName) {
45
+ projectName = await safePrompt({
46
46
  message: "Project name:",
47
- initial: "my-worker"
47
+ default: "my-worker",
48
+ required: true,
49
+ noTTY: "Provide the project name with --project"
48
50
  });
49
51
  }
50
- let answers;
51
- if (promptQuestions.length > 0) {
52
- answers = await prompts(promptQuestions);
53
- }
54
- const directoryName = values.directory ?? answers?.directoryName;
55
- const projectName = values.project ?? answers?.projectName;
56
52
  if (!directoryName || !projectName) {
57
53
  console.log(chalk.red("Cancelled."));
58
54
  process.exit(1);
59
55
  }
60
56
  const spinner = ora("Setting up template...").start();
61
- let tempDir = null;
62
57
  try {
63
- tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "project-ajax-"));
64
- spinner.text = "Fetching template from repository...";
65
- await pullCode(tempDir);
66
58
  spinner.text = "Preparing destination...";
67
59
  const destPath = prepareDestination(directoryName);
68
60
  spinner.text = "Copying template files...";
69
- copyTemplate(tempDir, destPath);
61
+ const templatePath = getTemplatePath();
62
+ copyTemplate(templatePath, destPath);
70
63
  spinner.text = "Customizing package.json...";
71
64
  customizePackageJson(destPath, projectName);
72
65
  spinner.succeed(chalk.green("Worker project created successfully!"));
@@ -77,46 +70,11 @@ async function run() {
77
70
  chalk.red(`
78
71
  ${err instanceof Error ? err.message : String(err)}`)
79
72
  );
80
- if (err instanceof Error && (err.message.includes("Permission denied") || err.message.includes("publickey"))) {
81
- console.log(
82
- chalk.yellow("\n\u26A0\uFE0F SSH authentication failed. Make sure you have:")
83
- );
84
- console.log(" 1. SSH keys set up with GitHub");
85
- console.log(" 2. Added your SSH key to your GitHub account");
86
- console.log(
87
- " 3. See: https://docs.github.com/en/authentication/connecting-to-github-with-ssh"
88
- );
89
- }
90
73
  process.exit(1);
91
- } finally {
92
- if (tempDir) {
93
- try {
94
- fs.rmSync(tempDir, { recursive: true, force: true });
95
- } catch {
96
- }
97
- }
98
74
  }
99
75
  }
100
- async function pullCode(tempDir) {
101
- await execa("git", ["init"], { cwd: tempDir });
102
- await execa(
103
- "git",
104
- ["remote", "add", "origin", "git@github.com:makenotion/project-ajax.git"],
105
- { cwd: tempDir }
106
- );
107
- await execa("git", ["config", "core.sparseCheckout", "true"], {
108
- cwd: tempDir
109
- });
110
- const sparseCheckoutPath = path.join(
111
- tempDir,
112
- ".git",
113
- "info",
114
- "sparse-checkout"
115
- );
116
- fs.writeFileSync(sparseCheckoutPath, "packages/template/*\n");
117
- await execa("git", ["pull", "--depth=1", "origin", "main"], {
118
- cwd: tempDir
119
- });
76
+ function getTemplatePath() {
77
+ return path.resolve(__dirname, "..", "template");
120
78
  }
121
79
  function prepareDestination(repoName) {
122
80
  const destPath = repoName === "." ? process.cwd() : path.resolve(process.cwd(), repoName);
@@ -132,8 +90,12 @@ function prepareDestination(repoName) {
132
90
  }
133
91
  return destPath;
134
92
  }
135
- function copyTemplate(tempDir, destPath) {
136
- const templatePath = path.join(tempDir, "packages", "template");
93
+ function copyTemplate(templatePath, destPath) {
94
+ if (!fs.existsSync(templatePath)) {
95
+ throw new Error(
96
+ `Template directory not found at ${templatePath}. This is likely a packaging issue.`
97
+ );
98
+ }
137
99
  const templateFiles = fs.readdirSync(templatePath);
138
100
  for (const file of templateFiles) {
139
101
  const srcPath = path.join(templatePath, file);
@@ -169,3 +131,17 @@ function printNextSteps(directoryName) {
169
131
  `);
170
132
  }
171
133
  }
134
+ function safePrompt(config) {
135
+ if (!process.stdin.isTTY) {
136
+ console.error(chalk.red(config.noTTY));
137
+ process.exit(1);
138
+ }
139
+ return prompts.input(config).catch((err) => {
140
+ if (err instanceof Error && err.name === "ExitPromptError") {
141
+ console.log(chalk.red("\u{1F44B} Goodbye!"));
142
+ process.exit(1);
143
+ } else {
144
+ throw err;
145
+ }
146
+ });
147
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@project-ajax/create",
3
- "version": "0.0.12",
3
+ "version": "0.0.14",
4
4
  "description": "Initialize a new Notion Project Ajax extensions repo.",
5
5
  "bin": {
6
6
  "create-ajax": "dist/index.js"
@@ -22,17 +22,16 @@
22
22
  "access": "public"
23
23
  },
24
24
  "files": [
25
- "dist/"
25
+ "dist/",
26
+ "template/"
26
27
  ],
27
28
  "dependencies": {
29
+ "@inquirer/prompts": "^8.0.1",
28
30
  "chalk": "^5.3.0",
29
- "execa": "^8.0.0",
30
- "ora": "^8.0.1",
31
- "prompts": "^2.4.2"
31
+ "ora": "^8.0.1"
32
32
  },
33
33
  "devDependencies": {
34
34
  "@types/node": "^22.19.0",
35
- "@types/prompts": "^2.4.9",
36
35
  "tsup": "^8.5.0",
37
36
  "typescript": "^5.9.3"
38
37
  }
@@ -0,0 +1,700 @@
1
+ # README
2
+
3
+ Use this repo to write extensions to enhance Notion. With Notion extensions, you can write JavaScript that syncs data into Notion collections, adds new slash-commands, or provides new actions to Custom Agents.
4
+
5
+ ## Prerequisites
6
+
7
+ ### Node
8
+
9
+ You need Node >= 22, npm >= 10 installed. (The repo will warn you if you don't.)
10
+
11
+ ### Access to the feature
12
+
13
+ This feature is behind a feature flag (i.e. Statsig gate) in Notion.
14
+
15
+ See a `#proj-ajax` team member to gain access.
16
+
17
+ ## Quickstart
18
+
19
+ In this Quickstart, you’ll:
20
+
21
+ - Clone this extensions template
22
+ - Publish example extensions to your Notion workspace
23
+
24
+ ### 1. Create worker project from template
25
+
26
+ Use `npm init` to setup a project template:
27
+
28
+ ```
29
+ npm init @project-ajax
30
+ ```
31
+
32
+ When prompted, enter a path to the new project, e.g. `my-extensions`.
33
+
34
+ ### 2. Install packages and connect the repo to Notion
35
+
36
+ Install packages:
37
+
38
+ ```
39
+ # or whatever you chose for the pathname
40
+ cd my-extensions && npm i
41
+ ```
42
+
43
+ Now, run `npx workers` to verify the tool has installed:
44
+
45
+ ```
46
+ npx workers
47
+
48
+ USAGE
49
+ workers auth login|show|logout ...
50
+ workers deploy [--config value] [--debug]
51
+ workers secrets set|list|rm ...
52
+ workers --help
53
+ workers --version
54
+
55
+ A CLI for the Project Ajax platform
56
+
57
+ [ ... ]
58
+ ```
59
+
60
+ Then, connect the repo to Notion to publish the example extensions to your Notion workspace:
61
+
62
+ ```
63
+ # Connect to Notion Prod
64
+ npx workers auth login
65
+ # Connect to Notion Dev
66
+ npx workers auth login --env=dev
67
+ ```
68
+
69
+ This command will open a new page with a command that includes a token. Run that command locally. It will look like this:
70
+
71
+ ```
72
+ # example
73
+ npx workers auth login v1.user.wAFygwEZ-[...] --env=dev
74
+ ```
75
+
76
+ > **Note**
77
+ >
78
+ > If the command opens a page that 404s, you likely do not have Statsig access to extensions. See a #proj-virtual-databases team member for access.
79
+
80
+ ### 3. Deploy
81
+
82
+ Run the following command to deploy this repo's example extensions to your Notion workspace:
83
+
84
+ ```
85
+ npx workers deploy
86
+
87
+ Deploying worker...
88
+ Updating worker...
89
+ Building worker bundle...
90
+ Uploading bundle...
91
+ Fetching and saving worker capabilities...
92
+ ✓ Successfully deployed worker
93
+ ```
94
+
95
+ ### 4. Try out the sample worker
96
+
97
+ The sample worker installs three capabilities:
98
+
99
+ - [Synced database](#try-the-synced-database)
100
+ - [Agent tool](#try-the-agent-tool)
101
+ - [Slash command](#try-the-slash-command)
102
+
103
+ #### Try the synced database
104
+
105
+ After deploying, the sync runs automatically and creates a database in your private pages - you can find it in your sidebar or by searching "Sample Tasks".
106
+
107
+ Alternatively, run the sync via the CLI to get the URL:
108
+
109
+ ```
110
+ npx workers exec tasksSync
111
+ ```
112
+
113
+ When the command completes you will see the URL for the `Sample Tasks` database.
114
+
115
+ #### Try the agent tool
116
+
117
+ To exercise the sample agent tool, first navigate to a new or existing custom agent in Notion.
118
+
119
+ On the settings tab, choose Add connection:
120
+
121
+ <img src="./docs/custom-agent-add-connection.png" style="width:400px">
122
+
123
+ Scroll down to find the installed worker which has the name used when you deployed the worker. Click Add.
124
+
125
+ <img src="./docs/custom-agent-select-worker.png" style="width:400px">
126
+
127
+ Save settings, then navigate to the **chat** tab. Our sample tool call allows for searching tasks by id or keyword - try asking Agent to find tasks related to "Ajax" or "worker".
128
+
129
+ #### Try the slash command
130
+
131
+ In your Notion workspace, **first refresh**. Then, type forward-slash (`/insert`) and select the "Insert Sample Task" capability:
132
+
133
+ <img src="./docs/insert-sample-task.png" style="width:300px" />
134
+
135
+ In the search field that appears, type `ajax` to preview matching sample tasks:
136
+
137
+ <img src="./docs/select-ajax.png" style="width:400px" />
138
+
139
+ After execution, the function will insert blocks into the page:
140
+
141
+ <img src="./docs/inserted-blocks.png" style="width:400px" />
142
+
143
+ ## How-to build your own
144
+
145
+ ### 1. Create repo from template
146
+
147
+ If you haven't already, use the GitHub CLI to create a new repo from this template:
148
+
149
+ ```
150
+ gh repo create notion-extensions -p makenotion/project-ajax-template --private
151
+ git clone git@github.com:[YOUR-USER]/notion-extensions my-extensions
152
+ ```
153
+
154
+ Alternatively, you can click "Create from template" on the repo's page [on GitHub](https://github.com/makenotion/project-ajax-template).
155
+
156
+ ### 2. Install packages and connect the repo to Notion
157
+
158
+ Install packages:
159
+
160
+ ```
161
+ cd my-extensions && npm i
162
+ ```
163
+
164
+ Then, connect the repo to Notion to publish the example extensions to your Notion workspace:
165
+
166
+ ```
167
+ # Connect to Notion Prod
168
+ npx workers auth login
169
+ # Connect to Notion Dev
170
+ npx workers auth login --env=dev
171
+ ```
172
+
173
+ This command will open a new page with a command that includes a token. Run that command locally.
174
+
175
+ ### 3. Write your extension(s)
176
+
177
+ You can write three types of extensions: **slash commands**, **syncs**, and **tools**.
178
+
179
+ #### Writing a slash command function
180
+
181
+ [Slash commands](#slash-commands) add custom `/` commands to your Notion workspace.
182
+
183
+ Basic structure:
184
+
185
+ ```typescript
186
+ import { slashCommand } from "@project-ajax/sdk/slashCommand";
187
+
188
+ export const myCommand = slashCommand({
189
+ // Title in slash command menu
190
+ menuTitle: "My Command",
191
+ // Description in slash command menu
192
+ menuDescription: "Description shown in menu",
193
+ search: {
194
+ // Placeholder shown in search bar
195
+ placeholder: "Search...",
196
+ // Debounce when user types into the search bar, in milliseconds
197
+ debounce: 300,
198
+ },
199
+ // Called when the user enters a search query. Return results to show to user in search menu.
200
+ executeSearch: async (query: string) => {
201
+ // Return search results
202
+ return { items: [{ id: "1", title: "Result", description: "..." }] };
203
+ },
204
+ // Called when the user has selected a search item from the search menu.
205
+ executeSelect: async (id: string) => {
206
+ // Return Notion blocks to insert
207
+ return [
208
+ /* array of blocks */
209
+ ];
210
+ },
211
+ });
212
+ ```
213
+
214
+ #### Writing a sync function
215
+
216
+ [Sync functions](#sync) populate Notion collections with data from external sources.
217
+
218
+ Basic structure:
219
+
220
+ ```typescript
221
+ import { sync } from "@project-ajax/sdk/sync";
222
+ import * as Schema from "@project-ajax/sdk/schema";
223
+ import * as Builder from "@project-ajax/sdk/builder";
224
+
225
+ export const mySync = sync({
226
+ // Which field to use in each object as the primary key. Must be unique.
227
+ primaryKeyProperty: "ID",
228
+ // The schema of the collection to create in Notion.
229
+ schema: {
230
+ // Name of the collection to create in Notion.
231
+ dataSourceTitle: "My Data",
232
+ properties: {
233
+ // See `Schema` for the full list of possible column types.
234
+ Title: Schema.title(),
235
+ ID: Schema.richText(),
236
+ },
237
+ },
238
+ execute: async () => {
239
+ // Fetch and return data
240
+ return [
241
+ // Each object must match the shape of `properties` above.
242
+ {
243
+ key: "1",
244
+ properties: {
245
+ Title: Builder.title("Item 1"),
246
+ ID: Builder.richText("1"),
247
+ },
248
+ },
249
+ ];
250
+ },
251
+ });
252
+ ```
253
+
254
+ #### Writing a tool for custom agents
255
+
256
+ [Tools](#tools) provide custom actions that can be invoked by custom agents in your Notion workspace.
257
+
258
+ Basic structure:
259
+
260
+ ```typescript
261
+ import { tool } from "@project-ajax/sdk";
262
+
263
+ export const myTool = tool({
264
+ // Description of what this tool does - shown to the AI agent
265
+ description: "Search for items by keyword or ID",
266
+ // JSON Schema for the input the tool accepts
267
+ schema: {
268
+ type: "object",
269
+ properties: {
270
+ query: {
271
+ type: "string",
272
+ nullable: true,
273
+ description: "The search query"
274
+ },
275
+ limit: {
276
+ type: "number",
277
+ nullable: true,
278
+ description: "Maximum number of results"
279
+ },
280
+ },
281
+ required: [],
282
+ additionalProperties: false,
283
+ },
284
+ // Optional: JSON Schema for the output the tool returns
285
+ outputSchema: {
286
+ type: "object",
287
+ properties: {
288
+ results: {
289
+ type: "array",
290
+ items: { type: "string" },
291
+ },
292
+ },
293
+ required: ["results"],
294
+ additionalProperties: false,
295
+ },
296
+ // The function that executes when the tool is called
297
+ execute: async (input: { query?: string | null; limit?: number | null }) => {
298
+ // Destructure input with default values
299
+ const { query, limit = 10 } = input;
300
+
301
+ // Perform your logic here
302
+ const results = await searchItems(query, limit);
303
+
304
+ // Return data matching your outputSchema (if provided)
305
+ return { results };
306
+ },
307
+ });
308
+ ```
309
+
310
+ ### 4. Deploy your function
311
+
312
+ Deploy your functions to your Notion workspace with [**deploy**](#npx-workers-deploy):
313
+
314
+ ```bash
315
+ npx workers deploy
316
+ ```
317
+
318
+ This will build your worker bundle, upload it to Notion, and make your functions available in your workspace.
319
+
320
+ ### 5. Add secrets
321
+
322
+ Your function might require sensitive values, like API tokens, to run. You can add [**secrets**](#npx-workers-secrets) to your function's run context. Secrets are exposed to your function as environment variables.
323
+
324
+ Run the following to add secrets:
325
+
326
+ ```
327
+ npx workers secrets set MY_SECRET_1=my-secret-value MY_SECRET_2=my-secret-value2
328
+ ```
329
+
330
+ Then, you can reference these secret values in your function code using `process.env`, e.g. `process.env.MY_SECRET_1`.
331
+
332
+ ### 6. Run your function
333
+
334
+ For slash commands, you can test your function in the Notion workspace you deployed to.
335
+
336
+ For sync commands, your function is run automatically in the background. You can also run it via the CLI with [`exec`](#npx-workers-exec):
337
+
338
+ ```
339
+ npx workers exec nameOfSyncCapability
340
+ ```
341
+
342
+ When the command has finished executing, you should see a collection in your Notion sidebar that contains the synced data.
343
+
344
+ You can also test slash commands using [`exec`](#npx-workers-exec). Specify which function inside the slash command you want to run (i.e. `executeSearch` or `executeSelect`). Pass in arguments using `arg=value` syntax:
345
+
346
+ ```
347
+ npx workers exec mySlashCommand executeSearch query="myquery"
348
+ ```
349
+
350
+ ## Extensions reference
351
+
352
+ ### Slash commands
353
+
354
+ You can write an extension that adds a new **slash command** to a Notion workspace. The slash command currently supported is a "search-then-execute" flow.
355
+
356
+ For example, you can write a slash command to insert a Linear ticket:
357
+
358
+ 1. First, after selecting your slash command, the user enters a search query for a particular Linear ticket:
359
+
360
+ <img src="./docs/select-ajax.png" style="width:400px" />
361
+
362
+ 2. After they select a ticket from the list, your extension will insert blocks into the page.
363
+
364
+ The search query is powered by `executeSearch` and the select action triggers `executeSelect`.
365
+
366
+ #### Properties
367
+
368
+ - **`menuTitle`** (string, required): The title that appears in the slash command menu when users type `/`.
369
+ - **`menuDescription`** (string, required): A description of what the slash command does, shown in the menu.
370
+ - **`search`** (object, required): Configuration for the search interface.
371
+ - **`placeholder`** (string, required): Placeholder text shown in the search field.
372
+ - **`debounce`** (number, required): Milliseconds to wait before executing search after user stops typing.
373
+ - **`executeSearch`** (function, required): Async function that handles search queries.
374
+ - **Parameters**: `query` (string) - The user's search input.
375
+ - **Returns**: Promise resolving to an object with `items` array, where each item has:
376
+ - `id` (string): Unique identifier for the item.
377
+ - `title` (string): Title displayed in search results.
378
+ - `description` (string, optional): Additional description text.
379
+ - **`executeSelect`** (function, required): Async function that handles when a user selects an item.
380
+ - **Parameters**: `id` (string) - The id of the selected item.
381
+ - **Returns**: Promise resolving to an array of Notion blocks to insert into the page.
382
+
383
+ ### Sync
384
+
385
+ The **sync** extension allows you to sync any data to a Notion collection. Every time a sync is run, data is upserted or deleted from the target Notion collection so that the collection matches the source.
386
+
387
+ At the moment, syncs do not run automatically. Use **`exec`** to run a sync.
388
+
389
+ #### Properties
390
+
391
+ - **`primaryKeyProperty`** (string, required): The name of the property that serves as the unique identifier for each entry in the collection (e.g. `"id"`). This property must also be defined in the schema.
392
+ - **`schema`** (object, required): Defines the structure of the Notion collection.
393
+ - **`dataSourceTitle`** (string, required): The name of the collection that will be created in Notion. Collections are instantiated in the user's sidebar.
394
+ - **`properties`** (object, required): An object mapping property names to their schema definitions. Use functions from `@project-ajax/sdk/schema` to define property types (e.g., `Schema.title()`, `Schema.richText()`).
395
+ - **`execute`** (function, required): Async function that fetches and returns data to sync.
396
+ - **Parameters**: None (but can access `process.env` for secrets).
397
+ - **Returns**: Promise resolving to an array of entries, where each entry has:
398
+ - `key` (string): Unique identifier for the entry (maps to the `primaryKeyProperty`).
399
+ - `properties` (object): An object mapping property names to their values. Use functions from `@project-ajax/sdk/builder` to construct property values (e.g., `Builder.title()`, `Builder.richText()`).
400
+
401
+ ### Tools
402
+
403
+ The **tool** extension allows custom agents in your Notion workspace to perform actions. When you connect a worker to a custom agent, the agent can invoke your tools based on their descriptions and schemas.
404
+
405
+ Tools use JSON Schema to define their input and output structures, and the SDK validates data automatically.
406
+
407
+ #### Properties
408
+
409
+ - **`description`** (string, required): A clear description of what the tool does. This is shown to the AI agent to help it decide when to use the tool.
410
+ - **`schema`** (JSONSchemaType, required): JSON Schema definition for the tool's input parameters. The input will be validated against this schema before execution.
411
+ - **`outputSchema`** (JSONSchemaType, optional): JSON Schema definition for the tool's output. If provided, the output will be validated against this schema after execution.
412
+ - **`execute`** (function, required): Async function that performs the tool's action.
413
+ - **Parameters**: `input` (typed according to your schema) - The validated input parameters.
414
+ - **Returns**: Promise resolving to output data (validated against `outputSchema` if provided).
415
+
416
+ #### Error handling
417
+
418
+ The tool system automatically handles three types of errors:
419
+
420
+ - **`InvalidToolInputError`**: Thrown when input doesn't match the input schema.
421
+ - **`InvalidToolOutputError`**: Thrown when output doesn't match the output schema (if provided).
422
+ - **`ToolExecutionError`**: Thrown when the execute function throws an error.
423
+
424
+ All errors are automatically caught and returned in a structured format, so you don't need to handle them in your `execute` function.
425
+
426
+ #### JSON Schema types
427
+
428
+ Your schemas should use standard JSON Schema format. Common patterns:
429
+
430
+ **Object with required and optional properties:**
431
+ ```typescript
432
+ {
433
+ type: "object",
434
+ properties: {
435
+ name: { type: "string" },
436
+ age: { type: "number", nullable: true },
437
+ active: { type: "boolean", nullable: true },
438
+ },
439
+ // Only 'name' is required; age and active can be omitted
440
+ required: ["name"],
441
+ additionalProperties: false,
442
+ }
443
+ ```
444
+
445
+ **Arrays:**
446
+ ```typescript
447
+ {
448
+ type: "array",
449
+ items: { type: "string" },
450
+ }
451
+ ```
452
+
453
+ **Enums:**
454
+ ```typescript
455
+ {
456
+ type: "string",
457
+ enum: ["option1", "option2", "option3"],
458
+ }
459
+ ```
460
+
461
+ #### Testing tools
462
+
463
+ You can test tools using [`exec`](#npx-workers-exec):
464
+
465
+ ```bash
466
+ # Execute a tool without arguments
467
+ npx workers exec myTool
468
+
469
+ # Note: Currently, tools cannot accept arguments via CLI due to parser limitations.
470
+ # To test tools with specific inputs, use them with a custom agent in Notion.
471
+ ```
472
+
473
+ ## CLI reference
474
+
475
+ The `workers` CLI provides commands to authenticate, deploy, execute, and manage your Notion functions. All commands support global flags for configuration and debugging.
476
+
477
+ ### Global flags
478
+
479
+ These flags are available on all commands:
480
+
481
+ - **`--config <path>`**: Path to the config file to use (defaults to `workers.json`)
482
+ - **`--debug`**: Enable debug logging
483
+
484
+ ### `npx workers auth`
485
+
486
+ Commands for managing authentication with the Project Ajax platform.
487
+
488
+ #### `npx workers auth login [token] [--env=<env>]`
489
+
490
+ Login to the Project Ajax platform using a Workers API token.
491
+
492
+ **Arguments:**
493
+
494
+ - `token` (optional): A Workers API token. If not provided, opens a browser to create one.
495
+
496
+ **Flags:**
497
+
498
+ - `--env` (optional): The environment to use (`local`, `staging`, `dev`, or `prod`). Defaults to `prod`.
499
+
500
+ **Examples:**
501
+
502
+ ```bash
503
+ # Open browser to create a token
504
+ npx workers auth login
505
+
506
+ # Login with an existing token
507
+ npx workers auth login v1.user.wAFygwEZ-...
508
+
509
+ # Login to dev environment
510
+ npx workers auth login --env=dev
511
+ ```
512
+
513
+ #### `npx workers auth show`
514
+
515
+ Display the currently configured authentication token.
516
+
517
+ **Example:**
518
+
519
+ ```bash
520
+ npx workers auth show
521
+ ```
522
+
523
+ #### `npx workers auth logout`
524
+
525
+ Remove the currently configured authentication token.
526
+
527
+ **Example:**
528
+
529
+ ```bash
530
+ npx workers auth logout
531
+ ```
532
+
533
+ ### `npx workers deploy`
534
+
535
+ Deploy your worker to the Project Ajax platform. This command builds your worker bundle, uploads it to Notion, and saves the worker configuration.
536
+
537
+ **Example:**
538
+
539
+ ```bash
540
+ npx workers deploy
541
+ ```
542
+
543
+ The deploy command will:
544
+
545
+ 1. Build your worker bundle from the current directory
546
+ 2. Upload the bundle to Notion
547
+ 3. Fetch and save worker capabilities
548
+ 4. Store the worker ID in your config file
549
+
550
+ ### `npx workers exec`
551
+
552
+ Execute a worker capability with optional function and arguments.
553
+
554
+ **Usage:**
555
+
556
+ ```bash
557
+ npx workers exec <capabilityName> [functionName] [arg1=value1 arg2=value2...]
558
+ ```
559
+
560
+ **Arguments:**
561
+
562
+ - `capabilityName` (required): Name of the capability to execute
563
+ - `functionName` (optional): For slash commands, specify which function to run (`executeSearch` or `executeSelect`)
564
+ - `arguments` (optional): Key-value pairs as arguments. Supports both `key=value` and `key value` formats.
565
+
566
+ **Flags:**
567
+
568
+ - `--output <mode>`: Output mode for results. Options:
569
+ - `none`: No output
570
+ - `full`: Complete output
571
+ - `pager`: Paginate long output (default)
572
+
573
+ **Examples:**
574
+
575
+ ```bash
576
+ # Execute a sync capability
577
+ npx workers exec tasksSync
578
+
579
+ # Execute a slash command's search function
580
+ npx workers exec taskSearch executeSearch query="ajax"
581
+
582
+ # Execute with specific output mode
583
+ npx workers exec taskSearch executeSearch query="test" --output=full
584
+ ```
585
+
586
+ ### `npx workers capabilities`
587
+
588
+ Commands for managing worker capabilities.
589
+
590
+ #### `npx workers capabilities list`
591
+
592
+ List all capabilities for the deployed worker.
593
+
594
+ **Example:**
595
+
596
+ ```bash
597
+ npx workers capabilities list
598
+ ```
599
+
600
+ This will display all capabilities with their names and types (e.g., `sync`, `slashCommand`).
601
+
602
+ ### `npx workers secrets`
603
+
604
+ Commands for managing worker secrets. Secrets are exposed to your functions as environment variables via `process.env`.
605
+
606
+ #### `npx workers secrets set <key> <value> [<key2> <value2>...]`
607
+
608
+ Set one or more secrets for your worker. Supports multiple formats:
609
+
610
+ **Formats:**
611
+
612
+ - Space-separated: `key value`
613
+ - Equals-separated: `key=value`
614
+ - Colon-separated: `key:value`
615
+
616
+ **Examples:**
617
+
618
+ ```bash
619
+ # Set a single secret
620
+ npx workers secrets set API_KEY my-secret-value
621
+
622
+ # Set multiple secrets (space-separated)
623
+ npx workers secrets set API_KEY my-key API_SECRET my-secret
624
+
625
+ # Set multiple secrets (equals format)
626
+ npx workers secrets set API_KEY=my-key API_SECRET=my-secret
627
+
628
+ # Mix formats
629
+ npx workers secrets set API_KEY=my-key DATABASE_URL my-db-url
630
+ ```
631
+
632
+ #### `npx workers secrets list`
633
+
634
+ List all secret keys for your worker. Note: Secret values are not displayed, only keys and creation timestamps.
635
+
636
+ **Example:**
637
+
638
+ ```bash
639
+ npx workers secrets list
640
+ ```
641
+
642
+ #### `npx workers secrets rm <key>`
643
+
644
+ Remove a secret from your worker.
645
+
646
+ **Arguments:**
647
+
648
+ - `key` (required): The secret key name to remove
649
+
650
+ **Example:**
651
+
652
+ ```bash
653
+ npx workers secrets rm API_KEY
654
+ ```
655
+
656
+ ### `npx workers connect`
657
+
658
+ Commands for managing OAuth connections that store provider tokens as worker secrets. Connections appear in your runtime as environment variables (e.g., `process.env.NOTION_OAUTH_GOOGLE`).
659
+
660
+ #### `npx workers connect providers`
661
+
662
+ List the OAuth providers that are currently available for your workspace.
663
+
664
+ ```bash
665
+ npx workers connect providers
666
+ ```
667
+
668
+ #### `npx workers connect add <provider>`
669
+
670
+ Start the OAuth flow in your browser for the specified provider. The command opens your default browser and also prints a fallback link.
671
+
672
+ ```bash
673
+ npx workers connect add google
674
+ ```
675
+
676
+ #### `npx workers connect list`
677
+
678
+ Display all active OAuth connections for the deployed worker. The output includes the provider name, the environment variable exposed to your code, and when the connection was created.
679
+
680
+ ```bash
681
+ npx workers connect list
682
+ ```
683
+
684
+ #### `npx workers connect rm <provider>`
685
+
686
+ Remove an OAuth connection (and the stored tokens) for the specified provider.
687
+
688
+ ```bash
689
+ npx workers connect rm google
690
+ ```
691
+
692
+ ### Configuration file
693
+
694
+ The CLI stores configuration in a `workers.json` file in your project directory. This file is automatically created and updated by the CLI commands. It contains:
695
+
696
+ - **`environment`**: The current environment (`local`, `staging`, `dev`, or `prod`)
697
+ - **`token`**: Your authentication token
698
+ - **`workerId`**: The ID of your deployed worker
699
+
700
+ You can specify a different config file using the `--config` flag on any command.
Binary file
@@ -0,0 +1,26 @@
1
+ {
2
+ "name": "@project-ajax/template",
3
+ "version": "0.0.0",
4
+ "main": "dist/index.js",
5
+ "private": true,
6
+ "scripts": {
7
+ "build": "tsc",
8
+ "check": "tsc --noEmit",
9
+ "dev": "tsx --watch src/index.ts",
10
+ "start": "node dist/index.js",
11
+ "bump-sdk": "npm install @project-ajax/sdk@latest"
12
+ },
13
+ "engines": {
14
+ "node": ">=22.0.0",
15
+ "npm": ">=10.9.4"
16
+ },
17
+ "devDependencies": {
18
+ "@types/node": "^22.9.0",
19
+ "tsx": "^4.20.6",
20
+ "typescript": "^5.8.0"
21
+ },
22
+ "dependencies": {
23
+ "@project-ajax/cli": ">=0.0.0",
24
+ "@project-ajax/sdk": ">=0.0.0"
25
+ }
26
+ }
@@ -0,0 +1,334 @@
1
+ import { tool } from "@project-ajax/sdk";
2
+ import type {
3
+ Block,
4
+ CalloutBlock,
5
+ ParagraphBlock,
6
+ } from "@project-ajax/sdk/block";
7
+ import * as Builder from "@project-ajax/sdk/builder";
8
+ import * as Schema from "@project-ajax/sdk/schema";
9
+ import { slashCommand } from "@project-ajax/sdk/slashCommand";
10
+ import { sync } from "@project-ajax/sdk/sync";
11
+
12
+ // Sample data for demonstration
13
+ const sampleTasks = [
14
+ {
15
+ id: "task-1",
16
+ title: "Welcome to Project Ajax",
17
+ status: "Completed",
18
+ description: "This is a simple hello world example",
19
+ },
20
+ {
21
+ id: "task-2",
22
+ title: "Build your first worker",
23
+ status: "In Progress",
24
+ description: "Create a sync or slash command worker",
25
+ },
26
+ {
27
+ id: "task-3",
28
+ title: "Deploy to production",
29
+ status: "Todo",
30
+ description: "Share your worker with your team",
31
+ },
32
+ ];
33
+
34
+ // Example sync worker that syncs sample tasks to a database
35
+ export const tasksSync = sync({
36
+ primaryKeyProperty: "Task ID",
37
+
38
+ // Optional: Set to true to delete pages that are not returned from sync executions.
39
+ // By default (false), sync only creates and updates pages, never deletes them.
40
+ // deleteUnreturnedPages: true,
41
+
42
+ schema: {
43
+ dataSourceTitle: "Sample Tasks",
44
+ databaseIcon: Builder.notionIcon("checklist"),
45
+ properties: {
46
+ "Ticket Title": Schema.title(),
47
+ "Task ID": Schema.richText(),
48
+ Description: Schema.richText(),
49
+ Status: Schema.select([
50
+ { name: "Completed", color: "green" },
51
+ { name: "In Progress", color: "blue" },
52
+ { name: "Todo", color: "default" },
53
+ ]),
54
+ },
55
+ },
56
+
57
+ execute: async () => {
58
+ const emojiForStatus = (status: string) => {
59
+ switch (status) {
60
+ case "Completed":
61
+ return Builder.notionIcon("checkmark", "green");
62
+ case "In Progress":
63
+ return Builder.notionIcon("arrow-right", "blue");
64
+ case "Todo":
65
+ return Builder.notionIcon("clock", "lightgray");
66
+ default:
67
+ return Builder.notionIcon("question-mark", "lightgray");
68
+ }
69
+ };
70
+ // Return sample tasks as database entries
71
+ const objects = sampleTasks.map((task) => ({
72
+ key: task.id,
73
+ icon: emojiForStatus(task.status),
74
+ properties: {
75
+ "Ticket Title": Builder.title(task.title),
76
+ "Task ID": Builder.richText(task.id),
77
+ Description: Builder.richText(task.description),
78
+ Status: Builder.select(task.status),
79
+ },
80
+ pageContentMarkdown: `## ${task.title}\n\n${task.description}`,
81
+ }));
82
+
83
+ return {
84
+ objects,
85
+ done: true,
86
+ };
87
+ },
88
+ });
89
+
90
+ // Example slash command that searches and inserts sample tasks
91
+ export const taskSearchSlashCommand = slashCommand({
92
+ menuTitle: "Insert Sample Task",
93
+ menuDescription: "Search for a sample task to insert",
94
+ search: {
95
+ placeholder: "Search for tasks...",
96
+ debounce: 300,
97
+ },
98
+ executeSearch: async (query: string) => {
99
+ // Filter tasks based on the search query
100
+ const filtered = sampleTasks.filter(
101
+ (task) =>
102
+ task.title.toLowerCase().includes(query.toLowerCase()) ||
103
+ task.description.toLowerCase().includes(query.toLowerCase()),
104
+ );
105
+
106
+ // Transform tasks into menu items
107
+ const items = filtered.map((task) => ({
108
+ id: task.id,
109
+ title: task.title,
110
+ description: `${task.status} • ${task.description}`,
111
+ }));
112
+
113
+ return { items };
114
+ },
115
+
116
+ executeSelect: async (taskId: string) => {
117
+ // Find the selected task
118
+ const task = sampleTasks.find((t) => t.id === taskId);
119
+
120
+ if (!task) {
121
+ return [];
122
+ }
123
+
124
+ // Create blocks to insert into the page
125
+ const calloutBlock: CalloutBlock = {
126
+ object: "block",
127
+ type: "callout",
128
+ callout: {
129
+ icon: {
130
+ type: "emoji",
131
+ emoji: "✨",
132
+ },
133
+ rich_text: [
134
+ {
135
+ type: "text",
136
+ text: {
137
+ content: `Task: ${task.id}`,
138
+ link: null,
139
+ },
140
+ plain_text: `Task: ${task.id}`,
141
+ href: null,
142
+ annotations: {
143
+ bold: true,
144
+ italic: false,
145
+ strikethrough: false,
146
+ underline: false,
147
+ code: false,
148
+ color: "default",
149
+ },
150
+ },
151
+ ],
152
+ color: "blue_background",
153
+ },
154
+ };
155
+
156
+ const titleBlock: ParagraphBlock = {
157
+ object: "block",
158
+ type: "paragraph",
159
+ paragraph: {
160
+ rich_text: [
161
+ {
162
+ type: "text",
163
+ text: {
164
+ content: task.title,
165
+ link: null,
166
+ },
167
+ plain_text: task.title,
168
+ href: null,
169
+ annotations: {
170
+ bold: true,
171
+ italic: false,
172
+ strikethrough: false,
173
+ underline: false,
174
+ code: false,
175
+ color: "default",
176
+ },
177
+ },
178
+ ],
179
+ },
180
+ };
181
+
182
+ const statusBlock: ParagraphBlock = {
183
+ object: "block",
184
+ type: "paragraph",
185
+ paragraph: {
186
+ rich_text: [
187
+ {
188
+ type: "text",
189
+ text: {
190
+ content: "Status: ",
191
+ link: null,
192
+ },
193
+ plain_text: "Status: ",
194
+ href: null,
195
+ annotations: {
196
+ bold: true,
197
+ italic: false,
198
+ strikethrough: false,
199
+ underline: false,
200
+ code: false,
201
+ color: "default",
202
+ },
203
+ },
204
+ {
205
+ type: "text",
206
+ text: {
207
+ content: task.status,
208
+ link: null,
209
+ },
210
+ plain_text: task.status,
211
+ href: null,
212
+ annotations: {
213
+ bold: false,
214
+ italic: false,
215
+ strikethrough: false,
216
+ underline: false,
217
+ code: false,
218
+ color:
219
+ task.status === "Completed"
220
+ ? "green"
221
+ : task.status === "In Progress"
222
+ ? "blue"
223
+ : "default",
224
+ },
225
+ },
226
+ ],
227
+ },
228
+ };
229
+
230
+ const descriptionBlock: ParagraphBlock = {
231
+ object: "block",
232
+ type: "paragraph",
233
+ paragraph: {
234
+ rich_text: [
235
+ {
236
+ type: "text",
237
+ text: {
238
+ content: task.description,
239
+ link: null,
240
+ },
241
+ plain_text: task.description,
242
+ href: null,
243
+ annotations: {
244
+ bold: false,
245
+ italic: true,
246
+ strikethrough: false,
247
+ underline: false,
248
+ code: false,
249
+ color: "gray",
250
+ },
251
+ },
252
+ ],
253
+ },
254
+ };
255
+
256
+ const blocks: Block[] = [
257
+ calloutBlock,
258
+ titleBlock,
259
+ statusBlock,
260
+ descriptionBlock,
261
+ ];
262
+
263
+ return blocks;
264
+ },
265
+ });
266
+
267
+ // Example agent tool for retrieving task information
268
+ export const taskSearchTool = tool({
269
+ title: "Task Search",
270
+ description:
271
+ "Look up sample tasks by ID or keyword. Helpful for demonstrating agent tool calls.",
272
+ schema: {
273
+ type: "object",
274
+ properties: {
275
+ taskId: {
276
+ type: "string",
277
+ nullable: true,
278
+ description: "Return a single task that matches the given task ID.",
279
+ },
280
+ query: {
281
+ type: "string",
282
+ nullable: true,
283
+ description:
284
+ "Match search terms against words in the task title or description.",
285
+ },
286
+ },
287
+ required: [],
288
+ additionalProperties: false,
289
+ },
290
+ execute: async (input: { taskId?: string | null; query?: string | null }) => {
291
+ const { taskId, query } = input;
292
+
293
+ let matchingTasks = sampleTasks;
294
+
295
+ if (taskId) {
296
+ matchingTasks = sampleTasks.filter((task) => task.id === taskId);
297
+ } else if (query) {
298
+ const normalizedQuery = query.trim().toLowerCase();
299
+
300
+ const terms = normalizedQuery.split(/\s+/).filter(Boolean);
301
+
302
+ if (terms.length > 0) {
303
+ const scoredTasks = sampleTasks
304
+ .map((task) => {
305
+ const title = task.title.toLowerCase();
306
+ const description = task.description.toLowerCase();
307
+ const matches = terms.reduce((count, term) => {
308
+ return title.includes(term) || description.includes(term)
309
+ ? count + 1
310
+ : count;
311
+ }, 0);
312
+
313
+ return { task, matches };
314
+ })
315
+ .filter(({ matches }) => matches > 0)
316
+ .sort((a, b) => b.matches - a.matches);
317
+
318
+ matchingTasks = scoredTasks.map(({ task }) => task);
319
+ } else {
320
+ matchingTasks = [];
321
+ }
322
+ }
323
+
324
+ return {
325
+ count: matchingTasks.length,
326
+ tasks: matchingTasks.map((task) => ({
327
+ id: task.id,
328
+ title: task.title,
329
+ status: task.status,
330
+ description: task.description,
331
+ })),
332
+ };
333
+ },
334
+ });
@@ -0,0 +1,16 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2020",
4
+ "module": "nodenext",
5
+ "outDir": "./dist",
6
+ "rootDir": "./src",
7
+ "strict": true,
8
+ "esModuleInterop": true,
9
+ "skipLibCheck": true,
10
+ "forceConsistentCasingInFileNames": true,
11
+ "resolveJsonModule": true,
12
+ "moduleResolution": "nodenext"
13
+ },
14
+ "include": ["src/**/*"],
15
+ "exclude": ["node_modules", "dist"]
16
+ }