@north-light/sdk 0.3.477 → 0.3.479

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.
@@ -1,6 +1,6 @@
1
1
  import { spawnSync } from 'node:child_process';
2
2
  import { existsSync } from 'node:fs';
3
- import { mkdir, open, readFile, stat } from 'node:fs/promises';
3
+ import { appendFile, mkdir, open, readFile, stat } from 'node:fs/promises';
4
4
  import { homedir } from 'node:os';
5
5
  import { dirname, join, relative } from 'node:path';
6
6
  import { assetPath } from './assets.js';
@@ -65,6 +65,25 @@ async function createFile(path, text, mode = 0o644) {
65
65
  await file.close();
66
66
  }
67
67
  }
68
+ /** The `.gitignore` lines that already keep `local/` (the key and env file) out of git. */
69
+ const IGNORES_LOCAL = new Set(['local', 'local/', '/local', '/local/', 'local/*', '/local/*', 'local/**', '/local/**']);
70
+ /** Makes sure `<root>/.gitignore` ignores `local/`: creates it, or appends the entry once. Returns what it did. */
71
+ async function ensureLocalIgnored(root) {
72
+ const path = join(root, '.gitignore');
73
+ const current = await readFile(path, 'utf8').catch((error) => {
74
+ if (error.code === 'ENOENT')
75
+ return undefined;
76
+ throw error;
77
+ });
78
+ if (current === undefined) {
79
+ await createFile(path, 'local/\n');
80
+ return 'created';
81
+ }
82
+ if (current.split(/\r?\n/).some((line) => IGNORES_LOCAL.has(line.trim())))
83
+ return 'present';
84
+ await appendFile(path, `${current === '' || current.endsWith('\n') ? '' : '\n'}local/\n`);
85
+ return 'added';
86
+ }
68
87
  /** Runs `npm i --save-exact @north-light/sdk@<this version>` in `cwd` (no shell). */
69
88
  function installSdk(cwd) {
70
89
  const result = spawnSync('npm', ['i', '--save-exact', `@north-light/sdk@${SDK_VERSION}`], { cwd, stdio: ['ignore', 2, 2] });
@@ -125,6 +144,8 @@ export async function runInit(argv, options = {}) {
125
144
  for (const [name, text] of Object.entries(scaffold))
126
145
  await createFile(join(root, name), text, name === ENV_FILE ? 0o600 : 0o644);
127
146
  }
147
+ // Before the key exists, so a key is never on disk in a directory git would pick up. A new app's scaffold already wrote it.
148
+ const ignored = existingProject ? await ensureLocalIgnored(root) : 'present';
128
149
  let publicJwk;
129
150
  if (!(await exists(keyPath)))
130
151
  publicJwk = JSON.stringify((await createKeyFile(keyPath)).publicJwk);
@@ -134,7 +155,8 @@ export async function runInit(argv, options = {}) {
134
155
  say([
135
156
  existingProject ? `Added @north-light/sdk@${SDK_VERSION}, a signing key and the \`northlight\` skill to ${shown(root)}.` : `Created a Northlight app in ${shown(root)}:`,
136
157
  ...(existingProject ? [] : [' package.json server.ts .gitignore local/app.env', '']),
137
- `Private key: ${shown(keyPath)} (mode 0600, kept out of git). Never paste or commit it.`,
158
+ ...(ignored === 'present' ? [] : [`${ignored === 'created' ? 'Created' : 'Updated'} ${shown(join(root, '.gitignore'))} so git ignores local/.`]),
159
+ `Private key: ${shown(keyPath)} (mode 0600, ignored by git). Never paste or commit it.`,
138
160
  'Skill:',
139
161
  ...skillPaths.map((path) => ` ${globalSkill ? path : shown(path)}`),
140
162
  '',
@@ -2,7 +2,7 @@
2
2
  name: northlight
3
3
  description: "Use when building, debugging or shipping an app that signs people in with Northlight, starts runs on their Northlight runtime, or calls Northlight providers (sms, search, integrations, context, device) with @north-light/sdk."
4
4
  metadata:
5
- sdk-version: "0.3.477"
5
+ sdk-version: "0.3.479"
6
6
  ---
7
7
 
8
8
  # Building a Northlight app
@@ -11,7 +11,7 @@ An app on Northlight signs people in with their Northlight account, then starts
11
11
 
12
12
  ## First: check this skill matches the SDK
13
13
 
14
- This skill is for SDK version `0.3.477`. If the project has `node_modules/@north-light/sdk/package.json`, compare its `version` with that number. If they differ, run `npx @north-light/sdk skill` in the project, then read the rewritten `.agents/skills/northlight/SKILL.md` (or `.claude/skills/northlight/SKILL.md`) instead of this copy. That command needs no account and no crtr. A project that has no SDK yet starts with `npx @north-light/sdk@latest init <dir>`.
14
+ This skill is for SDK version `0.3.479`. If the project has `node_modules/@north-light/sdk/package.json`, compare its `version` with that number. If they differ, run `npx @north-light/sdk skill` in the project, then read the rewritten `.agents/skills/northlight/SKILL.md` (or `.claude/skills/northlight/SKILL.md`) instead of this copy. That command needs no account and no crtr. A project that has no SDK yet starts with `npx @north-light/sdk@latest init <dir>`.
15
15
 
16
16
  ## Where things run
17
17
 
@@ -36,7 +36,7 @@ You do the command-line steps. The person does the console steps in a browser: a
36
36
  - **Public key**: the JWK that `init` printed. Never ask for, accept or paste the private key.
37
37
 
38
38
  The console then shows the **client ID**. Ask for it.
39
- 4. **Say what the app asks for (person).** On the app's **What it asks for** tab, **Add permissions**, then add every provider scope the code requests (the quickstart needs `sms:send`). A new app starts with `crtr:llm` and `crtr:act`. Without a scope listed here the consent page does not offer it and the provider call is refused ([Scopes and grants](https://sdk.docs.northlight.ai/docs/concepts/scopes-and-grants.md)).
39
+ 4. **Say what the app asks for (person).** On the app's **What it asks for** tab, **Add permissions**, then add every provider scope the code requests (the quickstart needs `sms:send`: choose Texting, tick it, choose **Done**, then choose **Save**; **Done** alone stores nothing). A new app starts with `crtr:llm` and `crtr:act`. Without a scope listed here the consent page does not offer it and the provider call is refused ([Scopes and grants](https://sdk.docs.northlight.ai/docs/concepts/scopes-and-grants.md)).
40
40
  5. **Write the env file (you).** `npx @north-light/sdk env --client-id <client id>`. It sets `CROUTER_ISSUER`, `CROUTER_CLIENT_ID` and `APP_ORIGIN` in `local/app.env` and keeps every other line.
41
41
  6. **Run it (you).** `npm i`, then `node --env-file=local/app.env --import tsx server.ts`. Ask the person to open `http://localhost:3000/auth/login`, sign in, and choose Allow. The page then starts a run and sends them a text.
42
42
  7. **Testing mode.** A new app is in testing: only its owner and up to 100 testers can sign in. Testers are added by email or phone on the console's **Testers** tab. Anyone else's callback gets `error=access_denied` with `error_description=app_in_testing` ([Testing mode](https://sdk.docs.northlight.ai/docs/concepts/testing-mode.md)). Going public needs verification: email support@northlight.ai.
@@ -95,7 +95,9 @@ Keep `codeVerifier` and `nonce` per sign-in, keyed by `state`, and pass the same
95
95
 
96
96
  ```ts
97
97
  const runtime = await oauth.client(userId); // a client on this person's runtime, from their stored connection
98
- const started = await runtime.runs.start({prompt: 'Reply with exactly the word: pong', scopes: ['crtr:llm', 'crtr:act']});
98
+ // The prompt is the instruction; the message stored with it joins the first turn, so `reply` can follow that turn.
99
+ const started = await runtime.runs.start({prompt: 'Reply with exactly the word: pong', message: 'ping', scopes: ['crtr:llm', 'crtr:act']});
100
+ const reply = await runtime.runs.reply(started.run_id, started.message!).collect(); // reply.text is what the agent said
99
101
  const run = await runtime.runs.wait(started.run_id); // status is 'idle' once the reply is done
100
102
 
101
103
  // Texts the signed-in person's own verified phone through the sms provider (50 per person per UTC day).
@@ -65,11 +65,14 @@ const server = createServer(async (req, res) => {
65
65
 
66
66
  if (url.pathname === '/run' && req.method === 'POST') {
67
67
  const runtime = await oauth.client(userId); // a client on this person's runtime, from their stored connection
68
- const started = await runtime.runs.start({prompt: 'Reply with exactly the word: pong', scopes: ['crtr:llm', 'crtr:act']});
68
+ // The prompt is the instruction; the message stored with it joins the first turn, so `reply` can follow that turn.
69
+ const started = await runtime.runs.start({prompt: 'Reply with exactly the word: pong', message: 'ping', scopes: ['crtr:llm', 'crtr:act']});
70
+ const reply = await runtime.runs.reply(started.run_id, started.message!).collect(); // reply.text is what the agent said
69
71
  const run = await runtime.runs.wait(started.run_id); // status is 'idle' once the reply is done
70
72
  // Texts the signed-in person's own verified phone through the sms provider (50 per person per UTC day).
71
73
  const sent = await runtime.providers.call({provider: 'sms', tool: 'send', arguments: {body: 'Hello from my first Northlight app.'}});
72
- return page(res, 200, `<h1>Done</h1><p>The run finished with status <code>${escapeHtml(run.status)}</code>. A text is on its way to your phone.</p><pre>${escapeHtml(JSON.stringify({run: run.run_id, outcome: run.outcome, report: run.last_report, text: sent}, null, 2))}</pre><p><a href="/">Back</a></p>`);
74
+ const {message_id} = sent as {message_id: string}; // the provider also returns a note meant for agents; it is not shown here
75
+ return page(res, 200, `<h1>Done</h1><p>The run finished with status <code>${escapeHtml(run.status)}</code> and replied <code>${escapeHtml(reply.text)}</code>. A text is on its way to your phone.</p><pre>${escapeHtml(JSON.stringify({run: run.run_id, status: run.status, reply: reply.text, message_id}, null, 2))}</pre><p><a href="/">Back</a></p>`);
73
76
  }
74
77
  return page(res, 200, `<h1>Signed in</h1><p>You are <code>${escapeHtml(userId)}</code>.</p><form method="post" action="/run"><button>Start a run and send me a text</button></form>`);
75
78
  } catch (error) {
package/dist/version.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  /** This SDK's release version. Informational only: compatibility is the contract level, not this number. */
2
- export declare const SDK_VERSION = "0.3.477";
2
+ export declare const SDK_VERSION = "0.3.479";
3
3
  /** The release sent in `Crouter-Minimum-Runtime-Version` on every request. It is frozen, so a runtime from before the
4
4
  * contract level existed (which only knows this header) refuses this SDK, while every later runtime accepts it. */
5
5
  export declare const LEGACY_FLOOR = "0.3.423";
package/dist/version.js CHANGED
@@ -1,6 +1,6 @@
1
1
  // SDK_VERSION is written by scripts/release-version.mjs on every release; do not edit by hand.
2
2
  /** This SDK's release version. Informational only: compatibility is the contract level, not this number. */
3
- export const SDK_VERSION = '0.3.477';
3
+ export const SDK_VERSION = '0.3.479';
4
4
  // LEGACY_FLOOR is written once by `scripts/contract.mjs release`, at the first release that carries the contract level
5
5
  // (the `0.0.0` placeholder is replaced there and never again); do not edit by hand.
6
6
  /** The release sent in `Crouter-Minimum-Runtime-Version` on every request. It is frozen, so a runtime from before the
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@north-light/sdk",
3
- "version": "0.3.477",
3
+ "version": "0.3.479",
4
4
  "crouterContract": "1.7",
5
5
  "description": "Typed Node and browser client for running crouter agents through the crtrd /v1 API.",
6
6
  "type": "module",
@@ -35,14 +35,17 @@
35
35
  "dist",
36
36
  "README.md"
37
37
  ],
38
+ "engines": {
39
+ "node": ">=20.6.0"
40
+ },
38
41
  "sideEffects": false,
39
42
  "scripts": {
40
43
  "build": "tsc -p tsconfig.json && node scripts/build-skill.mjs && chmod 755 dist/keygen-cli.js",
41
44
  "prepublishOnly": "node -e \"if (process.env.npm_config_tag !== 'next') { console.error('@north-light/sdk publishes only with --tag next; scripts/promote-sdk.mjs is the only mover of latest.'); process.exit(1); }\""
42
45
  },
43
46
  "dependencies": {
44
- "@north-light/api": "0.3.477",
45
- "@north-light/identity": "0.3.477",
47
+ "@north-light/api": "0.3.479",
48
+ "@north-light/identity": "0.3.479",
46
49
  "jose": "^6.2.1"
47
50
  },
48
51
  "peerDependencies": {