@north-light/sdk 0.3.487 → 0.3.488
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 +1 -1
- package/dist/apps-command.js +14 -3
- package/dist/cli-usage.d.ts +2 -0
- package/dist/cli-usage.js +10 -3
- package/dist/client.d.ts +3 -0
- package/dist/client.js +4 -0
- package/dist/index.d.ts +1 -0
- package/dist/init-command.js +3 -0
- package/dist/keygen-cli.js +10 -1
- package/dist/login-command.js +46 -2
- package/dist/resources/docs.d.ts +64 -0
- package/dist/resources/docs.js +47 -0
- package/dist/skill/northlight/SKILL.md +16 -10
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
<a href="https://www.npmjs.com/package/@north-light/sdk"><img alt="npm" src="https://img.shields.io/npm/v/@north-light/sdk?label=npm"></a>
|
|
7
7
|
</p>
|
|
8
8
|
|
|
9
|
-
`@north-light/sdk` is the application-facing client for a crouter daemon (`crtrd`). Create an agent run, wait for a result checked against a Zod or Standard Schema, stream the run's text and tool calls to a UI, and use the daemon's other APIs: files, bash, documents ("memory"), crons, human requests, and the OAuth connection flow for apps whose users each have their own runtime. It is ESM-only.
|
|
9
|
+
`@north-light/sdk` is the application-facing client for a crouter daemon (`crtrd`). Create an agent run, wait for a result checked against a Zod or Standard Schema, stream the run's text and tool calls to a UI, and use the daemon's other APIs: files, bash, documents ("memory" for the person's, `docs` for ones your app owns), crons, human requests, and the OAuth connection flow for apps whose users each have their own runtime. It is ESM-only.
|
|
10
10
|
|
|
11
11
|
It is a client, not a runtime. It never runs an agent itself, and it does not bundle a model: the daemon it talks to does, using the provider that daemon is signed in to. In Node, `new Crouter()` connects to the local daemon over its unix socket and starts it on the first request if it is not running. In a browser or from another machine it needs the daemon's TCP URL and a bearer token. The lower-level contract it is built on is [`@north-light/api`](https://www.npmjs.com/package/@north-light/api); most applications should install this package instead.
|
|
12
12
|
|
package/dist/apps-command.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
1
2
|
import { usageOf } from './cli-usage.js';
|
|
2
3
|
import { callLeaf } from './directory-client.js';
|
|
3
4
|
import { envValueOf } from './env-command.js';
|
|
@@ -9,6 +10,8 @@ const LEAVES = {
|
|
|
9
10
|
'mine': { path: 'mine', app: false, flags: {} },
|
|
10
11
|
'get': { path: 'get', app: true, flags: {} },
|
|
11
12
|
'update': { path: 'update', app: true, flags: { value: list('name', 'origin', 'logo', 'justification') } },
|
|
13
|
+
'set-logo': { path: 'set-logo', app: true, flags: { value: list('file') }, imageFile: true },
|
|
14
|
+
'set-brand-color': { path: 'set-brand-color', app: true, flags: { value: list('color') } },
|
|
12
15
|
'set-redirect-uris': { path: 'set-redirect-uris', app: true, flags: { list: list('redirect-uri') } },
|
|
13
16
|
'asks': { path: 'asks', app: true, flags: { value: list('group', 'source', 'query'), bool: list('requested') } },
|
|
14
17
|
'set-scopes': { path: 'set-scopes', app: true, flags: { list: list('scope', 'add', 'remove'), bool: list('clear') } },
|
|
@@ -26,9 +29,9 @@ const LEAVES = {
|
|
|
26
29
|
};
|
|
27
30
|
/** What a coding agent's login can't do, by CLI leaf name. */
|
|
28
31
|
const REFUSED = {
|
|
29
|
-
'set-listing':
|
|
30
|
-
'set-spend-cap': "change an app's spend cap",
|
|
31
|
-
'set-allows':
|
|
32
|
+
'set-listing': "change an app's marketplace listing, price or welcome text",
|
|
33
|
+
'set-spend-cap': "change an app's monthly spend cap",
|
|
34
|
+
'set-allows': "change an app's sharing (what other apps may use)",
|
|
32
35
|
'provider get': "read an app's provider role",
|
|
33
36
|
'provider set': "change an app's provider role",
|
|
34
37
|
'provider refresh': "refresh an app's provider role",
|
|
@@ -101,6 +104,14 @@ export async function runApps(argv) {
|
|
|
101
104
|
}
|
|
102
105
|
params.jwk = (await publicJwkFromFile(keyFile)).json;
|
|
103
106
|
}
|
|
107
|
+
if (leaf.imageFile) {
|
|
108
|
+
const imageFile = flags.values.get('--file');
|
|
109
|
+
if (imageFile === undefined) {
|
|
110
|
+
console.error('Pass --file <path to a square PNG, JPEG, WebP or SVG, at most 512 KB>.');
|
|
111
|
+
return 1;
|
|
112
|
+
}
|
|
113
|
+
params.file = (await readFile(imageFile)).toString('base64');
|
|
114
|
+
}
|
|
104
115
|
}
|
|
105
116
|
catch (error) {
|
|
106
117
|
console.error(error.message);
|
package/dist/cli-usage.d.ts
CHANGED
|
@@ -7,6 +7,8 @@ export interface CommandUsage {
|
|
|
7
7
|
crtr: string;
|
|
8
8
|
}
|
|
9
9
|
export declare const COMMANDS: CommandUsage[];
|
|
10
|
+
/** The text `crouter-sdk <command> --help` prints (exit 0): the usage lines and the summary. */
|
|
11
|
+
export declare function helpOf(name: string): string;
|
|
10
12
|
/** The usage text a command prints on a usage error. */
|
|
11
13
|
export declare function usageOf(name: string): string;
|
|
12
14
|
/** The text `crouter-sdk` prints with no command, an unknown one, or `--help`. */
|
package/dist/cli-usage.js
CHANGED
|
@@ -32,13 +32,13 @@ export const COMMANDS = [
|
|
|
32
32
|
{
|
|
33
33
|
name: 'login',
|
|
34
34
|
forms: ['login [--no-browser] [--new] [--issuer <url>]'],
|
|
35
|
-
summary: 'Log this computer in to your Northlight account: prints a link and a code, you approve once in the browser, and a revocable login is saved to `~/.config/northlight/credentials.json`. Run it again if it times out: it resumes the same code. Calls only the Northlight directory.',
|
|
35
|
+
summary: 'Log this computer in to your Northlight account: prints a link and a code, you approve once in the browser, and a revocable login is saved to `~/.config/northlight/credentials.json`. Run it again if it times out or is interrupted: it resumes the same code. When this computer is already logged in it says so and exits 0; `--new` logs in again. Calls only the Northlight directory.',
|
|
36
36
|
crtr: 'No',
|
|
37
37
|
},
|
|
38
38
|
{
|
|
39
39
|
name: 'logout',
|
|
40
40
|
forms: ['logout [--issuer <url>]'],
|
|
41
|
-
summary: 'Revoke this computer\'s login on the server and
|
|
41
|
+
summary: 'Revoke this computer\'s login on the server and remove it from the credentials file (the file stays, without that login).',
|
|
42
42
|
crtr: 'No',
|
|
43
43
|
},
|
|
44
44
|
{
|
|
@@ -58,11 +58,18 @@ export const COMMANDS = [
|
|
|
58
58
|
forms: [
|
|
59
59
|
'apps <leaf> [--app <client id>] [--issuer <url>] [leaf flags] [--json]',
|
|
60
60
|
],
|
|
61
|
-
summary: 'Call one app-management leaf with the login and print its answer as JSON. Leaves: `mine`, `get`, `update`, `set-redirect-uris`, `asks`, `set-scopes`, `set-requires`, `set-run-notice-url`, `keys list|add|retire|delete|set-jwks-uri|remove-jwks-uri`, `testers list|add|remove`. `--app` defaults to `CROUTER_CLIENT_ID` in `local/app.env`; `keys add` takes `--key-file <path>` and sends only the public half. Deleting an app, its marketplace listing, spend cap, sharing and provider role are console-only.',
|
|
61
|
+
summary: 'Call one app-management leaf with the login and print its answer as JSON. Leaves: `mine`, `get`, `update`, `set-logo`, `set-brand-color`, `set-redirect-uris`, `asks`, `set-scopes`, `set-requires`, `set-run-notice-url`, `keys list|add|retire|delete|set-jwks-uri|remove-jwks-uri`, `testers list|add|remove`. `--app` defaults to `CROUTER_CLIENT_ID` in `local/app.env`; `keys add` takes `--key-file <path>` and sends only the public half. Deleting an app, its marketplace listing, spend cap, sharing and provider role are console-only.',
|
|
62
62
|
crtr: 'No',
|
|
63
63
|
},
|
|
64
64
|
];
|
|
65
65
|
const BIN = 'crouter-sdk';
|
|
66
|
+
/** The text `crouter-sdk <command> --help` prints (exit 0): the usage lines and the summary. */
|
|
67
|
+
export function helpOf(name) {
|
|
68
|
+
const command = COMMANDS.find((entry) => entry.name === name);
|
|
69
|
+
if (!command)
|
|
70
|
+
throw new Error(`unknown command ${name}`);
|
|
71
|
+
return `${usageOf(name)}\n\n${command.summary.replaceAll('`', '')}`;
|
|
72
|
+
}
|
|
66
73
|
/** The usage text a command prints on a usage error. */
|
|
67
74
|
export function usageOf(name) {
|
|
68
75
|
const command = COMMANDS.find((entry) => entry.name === name);
|
package/dist/client.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { type HealthDTO, type MessageResultDTO, type NodeDetailDTO, type ListNod
|
|
|
2
2
|
import { Bash } from './resources/bash.js';
|
|
3
3
|
import { Canvas } from './resources/canvas.js';
|
|
4
4
|
import { Crons } from './resources/crons.js';
|
|
5
|
+
import { Docs } from './resources/docs.js';
|
|
5
6
|
import { CustomObjects } from './resources/custom-objects.js';
|
|
6
7
|
import { Attachments } from './resources/attachments.js';
|
|
7
8
|
import { Files } from './resources/files.js';
|
|
@@ -127,6 +128,8 @@ export declare class Crouter {
|
|
|
127
128
|
/** Read the documents the person owns (`store: 'user'`): an app sees their `public` documents, `private` ones too while the grant holds
|
|
128
129
|
* `crtr:memory:read_private:user` (read-only; `friends` never), and every privacy level with a `crtr:memory:manage:user` grant. */
|
|
129
130
|
readonly memory: Memory;
|
|
131
|
+
/** Write, read and edit documents the calling app owns (`docs.write`, `docs.edit`, `docs.get`): for example one app-owned `preference` with a `boot` rule that carries the app's agent instructions. */
|
|
132
|
+
readonly docs: Docs;
|
|
130
133
|
/** Scope-filtered provider tools and daemon-mediated calls. */
|
|
131
134
|
readonly providers: Providers;
|
|
132
135
|
/** The caller's grant. With `wait` (1–25 s) and `since` (a `version` from an earlier answer), the runtime holds the
|
package/dist/client.js
CHANGED
|
@@ -3,6 +3,7 @@ import { APIConnectionError, APIUserAbortError, AuthenticationError, CrouterErro
|
|
|
3
3
|
import { Bash } from './resources/bash.js';
|
|
4
4
|
import { Canvas } from './resources/canvas.js';
|
|
5
5
|
import { Crons } from './resources/crons.js';
|
|
6
|
+
import { Docs } from './resources/docs.js';
|
|
6
7
|
import { CustomObjects } from './resources/custom-objects.js';
|
|
7
8
|
import { Attachments } from './resources/attachments.js';
|
|
8
9
|
import { Files } from './resources/files.js';
|
|
@@ -66,6 +67,8 @@ export class Crouter {
|
|
|
66
67
|
/** Read the documents the person owns (`store: 'user'`): an app sees their `public` documents, `private` ones too while the grant holds
|
|
67
68
|
* `crtr:memory:read_private:user` (read-only; `friends` never), and every privacy level with a `crtr:memory:manage:user` grant. */
|
|
68
69
|
memory;
|
|
70
|
+
/** Write, read and edit documents the calling app owns (`docs.write`, `docs.edit`, `docs.get`): for example one app-owned `preference` with a `boot` rule that carries the app's agent instructions. */
|
|
71
|
+
docs;
|
|
69
72
|
/** Scope-filtered provider tools and daemon-mediated calls. */
|
|
70
73
|
providers;
|
|
71
74
|
/** The caller's grant. With `wait` (1–25 s) and `since` (a `version` from an earlier answer), the runtime holds the
|
|
@@ -223,6 +226,7 @@ export class Crouter {
|
|
|
223
226
|
this.shares = new Shares(request);
|
|
224
227
|
this.questions = new Questions(request);
|
|
225
228
|
this.memory = new Memory(request);
|
|
229
|
+
this.docs = new Docs(request);
|
|
226
230
|
this.runs = new Runs(request, (id, after, signal) => this.openRunEvents(id, after, signal), this.questions);
|
|
227
231
|
this.providers = new Providers(request);
|
|
228
232
|
this.grant = { get: (options = {}) => {
|
package/dist/index.d.ts
CHANGED
|
@@ -12,6 +12,7 @@ export type { FileDownloadOptions } from './resources/files.js';
|
|
|
12
12
|
export type { Upload, UploadCreateParams } from './resources/uploads.js';
|
|
13
13
|
export type { AttachmentLink } from './resources/attachments.js';
|
|
14
14
|
export type { Share, ShareCreated, ShareCreateParams } from './resources/shares.js';
|
|
15
|
+
export type { Doc, DocEditParams, DocWriteParams } from './resources/docs.js';
|
|
15
16
|
export type { MemoryDocument, MemoryGetParams, MemoryListItem, MemoryListPage, MemoryListParams } from './resources/memory.js';
|
|
16
17
|
export type { Question, QuestionAnswered, QuestionFields, QuestionResponses, QuestionSlot, QuestionStatus } from './resources/questions.js';
|
|
17
18
|
export type { ProviderToolEntry } from './resources/providers.js';
|
package/dist/init-command.js
CHANGED
|
@@ -52,6 +52,9 @@ const packageJson = () => `${JSON.stringify({
|
|
|
52
52
|
type: 'module',
|
|
53
53
|
scripts: { start: START },
|
|
54
54
|
dependencies: { '@north-light/sdk': SDK_VERSION, tsx: '^4.20.0' },
|
|
55
|
+
// npm 11.20+ warns about tsx's esbuild postinstall unless the project decides. esbuild runs from its platform
|
|
56
|
+
// binary without the script (it only swaps in a faster launcher), so the app denies it.
|
|
57
|
+
allowScripts: { esbuild: false },
|
|
55
58
|
}, null, 2)}\n`;
|
|
56
59
|
const ENV_TEXT = `CROUTER_ISSUER=${DEFAULT_ISSUER}\nAPP_ORIGIN=${DEFAULT_ORIGIN}\nCROUTER_CLIENT_ID=\nCROUTER_KEY_FILE=${KEY_FILE}\n`;
|
|
57
60
|
const exists = (path) => stat(path).then(() => true, () => false);
|
package/dist/keygen-cli.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// The `crouter-sdk` bin: `init`, `skill`, `keygen`, `env`, and the developer commands `login`, `logout`, `whoami`, `register` and `apps`. The file keeps its first command's name because the
|
|
3
3
|
// published bin path (`dist/keygen-cli.js`) is part of the package, and the package keeps exactly one bin so that
|
|
4
4
|
// `npx @north-light/sdk <command>` works without naming it.
|
|
5
|
-
import { overviewUsage } from './cli-usage.js';
|
|
5
|
+
import { helpOf, overviewUsage } from './cli-usage.js';
|
|
6
6
|
import { runEnv } from './env-command.js';
|
|
7
7
|
import { runInit } from './init-command.js';
|
|
8
8
|
import { runKeygen } from './keygen-command.js';
|
|
@@ -16,10 +16,19 @@ const args = process.argv.slice(2);
|
|
|
16
16
|
const commands = { init: runInit, skill: runSkill, keygen: runKeygen, env: runEnv, login: runLogin, logout: runLogout, whoami: runWhoami, register: runRegister, apps: runApps };
|
|
17
17
|
const command = args[0] ?? '';
|
|
18
18
|
async function main() {
|
|
19
|
+
if (command === 'help' && args[1] !== undefined && Object.hasOwn(commands, args[1])) {
|
|
20
|
+
console.log(helpOf(args[1]));
|
|
21
|
+
return 0;
|
|
22
|
+
}
|
|
19
23
|
if (command === '--help' || command === '-h' || command === 'help') {
|
|
20
24
|
console.log(overviewUsage());
|
|
21
25
|
return 0;
|
|
22
26
|
}
|
|
27
|
+
// Help for a command is never an error: `<command> --help` and `<command> -h` print its usage and exit 0.
|
|
28
|
+
if (Object.hasOwn(commands, command) && args.slice(1).some((arg) => arg === '--help' || arg === '-h')) {
|
|
29
|
+
console.log(helpOf(command));
|
|
30
|
+
return 0;
|
|
31
|
+
}
|
|
23
32
|
const run = Object.hasOwn(commands, command) ? commands[command] : undefined;
|
|
24
33
|
if (!run) {
|
|
25
34
|
console.error(overviewUsage());
|
package/dist/login-command.js
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
2
|
import { loadStore, newSecret, displayPath, credentialsPath, updateEntry } from './credentials.js';
|
|
3
3
|
import { usageOf } from './cli-usage.js';
|
|
4
|
-
import { NetworkError, DirectoryError, machineName, pollLogin, revokeCredential, startLogin } from './directory-client.js';
|
|
4
|
+
import { NetworkError, DirectoryError, exchange, machineName, pollLogin, revokeCredential, startLogin } from './directory-client.js';
|
|
5
5
|
import { parseFlags, reportFailure, resolveIssuer } from './developer-common.js';
|
|
6
6
|
const USAGE = usageOf('login');
|
|
7
7
|
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
8
8
|
/** The poll interval in ms. The directory says 5 s; tests shorten it with NORTHLIGHT_LOGIN_POLL_MS. */
|
|
9
9
|
const intervalMs = () => Number(process.env.NORTHLIGHT_LOGIN_POLL_MS) || 5000;
|
|
10
|
+
/** How long polling keeps retrying through network failures (a sleeping laptop, a dropped connection) before it gives up. Tests shorten it with NORTHLIGHT_LOGIN_GIVEUP_MS. */
|
|
11
|
+
const giveUpMs = () => Number(process.env.NORTHLIGHT_LOGIN_GIVEUP_MS) || 180_000;
|
|
10
12
|
/** Tries to open `url` in a browser, detached. Resolves true when the opener started. */
|
|
11
13
|
function openBrowser(url) {
|
|
12
14
|
if (process.env.CI)
|
|
@@ -57,6 +59,25 @@ export async function runLogin(argv) {
|
|
|
57
59
|
try {
|
|
58
60
|
const store = await loadStore();
|
|
59
61
|
let pending = store.issuers[issuer]?.pending;
|
|
62
|
+
const saved = store.issuers[issuer];
|
|
63
|
+
// Already logged in and nothing pending: say so instead of starting a second login that would replace this one.
|
|
64
|
+
if (saved?.credential && !pending && !flags.bools.has('--new')) {
|
|
65
|
+
try {
|
|
66
|
+
const token = await exchange(issuer, saved.credential);
|
|
67
|
+
console.log([
|
|
68
|
+
`Already logged in as ${token.account.display} (${token.account.id}) on ${issuer}. Nothing to do.`,
|
|
69
|
+
`The login for ${saved.name ?? token.credential_id} expires on ${token.expires_at.slice(0, 10)}, or after 14 days unused. To log in again as a new login, run \`npx @north-light/sdk login --new\`.`,
|
|
70
|
+
].join('\n'));
|
|
71
|
+
return 0;
|
|
72
|
+
}
|
|
73
|
+
catch (error) {
|
|
74
|
+
// A login the directory no longer accepts is dropped, and a fresh login starts below. Anything else is reported.
|
|
75
|
+
if (!(error instanceof DirectoryError && error.status === 401))
|
|
76
|
+
throw error;
|
|
77
|
+
await updateEntry(issuer, ({ credential: _c, credential_id: _i, name: _n, account_id: _a, account_display: _d, expires_at: _e, ...rest }) => rest);
|
|
78
|
+
console.log('The saved login is no longer valid, so starting a new one.');
|
|
79
|
+
}
|
|
80
|
+
}
|
|
60
81
|
const now = Date.now();
|
|
61
82
|
let resumed = false;
|
|
62
83
|
if (pending && (flags.bools.has('--new') || !pending.device_code || !pending.expires_at || Date.parse(pending.expires_at) <= now)) {
|
|
@@ -97,8 +118,31 @@ export async function runLogin(argv) {
|
|
|
97
118
|
console.log(`Waiting for approval (the code expires in ${minutes} minute${minutes === 1 ? '' : 's'})…`);
|
|
98
119
|
let wait = intervalMs();
|
|
99
120
|
const deadline = Date.parse(pending.expires_at) + 30_000;
|
|
121
|
+
let failingSince;
|
|
100
122
|
for (;;) {
|
|
101
|
-
|
|
123
|
+
let answer;
|
|
124
|
+
try {
|
|
125
|
+
answer = await pollLogin(issuer, pending.device_code);
|
|
126
|
+
}
|
|
127
|
+
catch (error) {
|
|
128
|
+
// One failed poll (the laptop slept, the connection dropped) does not end the login: the code is still saved and the
|
|
129
|
+
// person may be approving right now. Keep polling; give up only after a sustained outage or once the code has expired.
|
|
130
|
+
const transient = error instanceof NetworkError || (error instanceof DirectoryError && error.status >= 500);
|
|
131
|
+
if (!transient)
|
|
132
|
+
throw error;
|
|
133
|
+
failingSince ??= Date.now();
|
|
134
|
+
if (Date.now() - failingSince > giveUpMs() || Date.now() > deadline) {
|
|
135
|
+
const reason = error instanceof NetworkError ? error.reason : error.message;
|
|
136
|
+
const left = Date.parse(pending.expires_at) > Date.now();
|
|
137
|
+
console.error(left
|
|
138
|
+
? `Couldn't reach ${issuer} to check the login (${reason}). The code ${code} is still saved and valid until ${hhmm(pending.expires_at)}: run \`npx @north-light/sdk login\` again to carry on with it.`
|
|
139
|
+
: `Couldn't reach ${issuer} (${reason}) and the code ${code} has expired. Run \`npx @north-light/sdk login\` again.`);
|
|
140
|
+
return 1;
|
|
141
|
+
}
|
|
142
|
+
await sleep(wait);
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
failingSince = undefined;
|
|
102
146
|
if (answer.status === 'approved') {
|
|
103
147
|
const old = (await loadStore()).issuers[issuer]?.credential;
|
|
104
148
|
await updateEntry(issuer, () => ({
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { type DeliveryRuleDTO, type DocEditedDTO, type DocEditRequest, type DocKindName, type DocWriteRequest, type DocWrittenDTO, type ObjectDTO } from '@north-light/api';
|
|
2
|
+
import type { RequestOptions } from '../types.js';
|
|
3
|
+
import { type Request } from './request.js';
|
|
4
|
+
/** `docs.write` input. */
|
|
5
|
+
export interface DocWriteParams extends Omit<DocWriteRequest, 'owner' | 'cwd' | 'kind' | 'rules'> {
|
|
6
|
+
/** The document's name inside its owner: segments of letters, digits, `_` or `-` joined by `/`. */
|
|
7
|
+
name: string;
|
|
8
|
+
/** The markdown body. `[[name]]` links are resolved against the app's view. */
|
|
9
|
+
body: string;
|
|
10
|
+
/** One non-empty line saying when to read the document. */
|
|
11
|
+
preview: string;
|
|
12
|
+
/** `knowledge` (default) or `preference`. A `preference` is loaded into every node's context that sees it. */
|
|
13
|
+
kind?: DocKindName;
|
|
14
|
+
/** When the document is delivered to a node: `{on: 'boot', deliver: 'content'}` puts the whole body into the context of every node at boot. */
|
|
15
|
+
rules?: DeliveryRuleDTO[];
|
|
16
|
+
/** Whose document it is. Defaults to the calling app (`'app'`), which is the only owner an app server can write as. */
|
|
17
|
+
owner?: 'app';
|
|
18
|
+
}
|
|
19
|
+
/** `docs.edit` input: a new `body` or exact-text `replace` blocks (not both), and/or document fields. */
|
|
20
|
+
export type DocEditParams = Omit<DocEditRequest, 'base'> & {
|
|
21
|
+
/** The `base` from the `docs.get` the change was computed from. Absent, the edit is made from this app's last `docs.get` or `docs.write` of the document;
|
|
22
|
+
* with neither it is refused (`ConflictError`, code `read_first`). A base that is no longer current is refused (`ConflictError`, code `stale_revision`) and writes nothing. */
|
|
23
|
+
base?: string;
|
|
24
|
+
};
|
|
25
|
+
/** A document read by `docs.get`. */
|
|
26
|
+
export interface Doc {
|
|
27
|
+
id: string;
|
|
28
|
+
/** `<owner handle>/<name>`: what `docs.write` returned and what `docs.edit` and `docs.get` accept. */
|
|
29
|
+
name: string;
|
|
30
|
+
/** The document's kind: `knowledge` or `preference`. */
|
|
31
|
+
kind: string | null;
|
|
32
|
+
privacy: ObjectDTO['privacy'];
|
|
33
|
+
preview: string;
|
|
34
|
+
summary: string | null;
|
|
35
|
+
owner: ObjectDTO['owner'];
|
|
36
|
+
/** The current revision number. */
|
|
37
|
+
revision: number;
|
|
38
|
+
/** The current body (the first head's, when `needsMerge`). */
|
|
39
|
+
body: string;
|
|
40
|
+
/** Every head's body, in head order. More than one only after concurrent edits. */
|
|
41
|
+
bodies: string[];
|
|
42
|
+
/** `true` when the document has several heads: the next edit must pass the whole merged `body`. */
|
|
43
|
+
needsMerge: boolean;
|
|
44
|
+
/** The edit base for this read: pass it as `base` to `docs.edit` to change exactly this revision. */
|
|
45
|
+
base: string;
|
|
46
|
+
/** The delivery rules the document carries. */
|
|
47
|
+
rules: DeliveryRuleDTO[];
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Documents the calling app owns (`docs.write` defaults the owner to the app): write one, read it, edit it in place.
|
|
51
|
+
* An app server holds no node, so it cannot write as `node`; it writes as `app`. A document the app owns is readable
|
|
52
|
+
* and changeable with no scope. Served on the app listener.
|
|
53
|
+
*/
|
|
54
|
+
export declare class Docs {
|
|
55
|
+
private readonly request;
|
|
56
|
+
constructor(request: Request);
|
|
57
|
+
/** Create a document. Returns its canonical `name`. A name already taken in the owner throws `ConflictError` (`name_taken`). */
|
|
58
|
+
write(params: DocWriteParams, options?: RequestOptions): Promise<DocWrittenDTO>;
|
|
59
|
+
/** Change a document in place: a new body or replacements, and/or fields (`preview`, `rules`, `privacy`, …). Returns the new `revision`.
|
|
60
|
+
* `ref` is the canonical name `docs.write` returned. */
|
|
61
|
+
edit(ref: string, params: DocEditParams, options?: RequestOptions): Promise<DocEditedDTO>;
|
|
62
|
+
/** Read a document with its body, revision, edit base and rules. Throws `NotFoundError` when no such document is in the app's view. */
|
|
63
|
+
get(ref: string, options?: RequestOptions): Promise<Doc>;
|
|
64
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { routes, } from '@north-light/api';
|
|
2
|
+
import { BadRequestError, NotFoundError } from '../errors.js';
|
|
3
|
+
import { withQuery } from './request.js';
|
|
4
|
+
/**
|
|
5
|
+
* Documents the calling app owns (`docs.write` defaults the owner to the app): write one, read it, edit it in place.
|
|
6
|
+
* An app server holds no node, so it cannot write as `node`; it writes as `app`. A document the app owns is readable
|
|
7
|
+
* and changeable with no scope. Served on the app listener.
|
|
8
|
+
*/
|
|
9
|
+
export class Docs {
|
|
10
|
+
request;
|
|
11
|
+
constructor(request) {
|
|
12
|
+
this.request = request;
|
|
13
|
+
}
|
|
14
|
+
/** Create a document. Returns its canonical `name`. A name already taken in the owner throws `ConflictError` (`name_taken`). */
|
|
15
|
+
write(params, options) {
|
|
16
|
+
return this.request('POST', routes.docs(), params, options);
|
|
17
|
+
}
|
|
18
|
+
/** Change a document in place: a new body or replacements, and/or fields (`preview`, `rules`, `privacy`, …). Returns the new `revision`.
|
|
19
|
+
* `ref` is the canonical name `docs.write` returned. */
|
|
20
|
+
edit(ref, params, options) {
|
|
21
|
+
return this.request('PATCH', routes.doc(ref), params, options);
|
|
22
|
+
}
|
|
23
|
+
/** Read a document with its body, revision, edit base and rules. Throws `NotFoundError` when no such document is in the app's view. */
|
|
24
|
+
async get(ref, options) {
|
|
25
|
+
const read = await this.request('GET', withQuery(routes.object(ref), { fields: 1 }), undefined, options);
|
|
26
|
+
if (read.state !== 'object')
|
|
27
|
+
throw new NotFoundError(404, 'not_found', `${ref} is ${read.state === 'deleted' ? 'deleted' : 'not a document in this runtime'}`);
|
|
28
|
+
if (read.type !== 'document')
|
|
29
|
+
throw new BadRequestError(400, 'invalid_request', `${ref} is a ${read.type}, not a document`);
|
|
30
|
+
const data = read.data;
|
|
31
|
+
const owner = read.object.owner;
|
|
32
|
+
return {
|
|
33
|
+
id: read.object.id,
|
|
34
|
+
name: owner ? `${owner.handle}/${read.object.name}` : read.object.name,
|
|
35
|
+
kind: read.object.kind, privacy: read.object.privacy, preview: read.object.preview, summary: data.summary, owner,
|
|
36
|
+
revision: data.revision, body: data.bodies[0] ?? '', bodies: data.bodies, needsMerge: data.needs_merge,
|
|
37
|
+
base: data.heads.map((h) => h.rev_id).sort().join(','),
|
|
38
|
+
// The daemon's rule rows carry storage columns (`rowid`, `object`, `owner`, `position`); hand back only the rule.
|
|
39
|
+
rules: (data.fields?.rules ?? []).map(({ on, deliver, match, match_frontmatter, if: condition }) => ({
|
|
40
|
+
on, deliver,
|
|
41
|
+
...(match !== undefined ? { match } : {}),
|
|
42
|
+
...(match_frontmatter !== undefined ? { match_frontmatter } : {}),
|
|
43
|
+
...(condition !== undefined ? { if: condition } : {}),
|
|
44
|
+
})),
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -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.
|
|
5
|
+
sdk-version: "0.3.488"
|
|
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.
|
|
14
|
+
This skill is for SDK version `0.3.488`. 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
|
|
|
@@ -27,9 +27,15 @@ You run the commands. The person does two things in a browser: signs up, and cho
|
|
|
27
27
|
|
|
28
28
|
1. **Account (person).** They sign up at `https://account.northlight.ai/signup`. Signup texts a code over iMessage, so the phone must have iMessage.
|
|
29
29
|
2. **Scaffold (you).** In a new folder run `npx @north-light/sdk@latest init my-app`. In an existing Node project run `npx @north-light/sdk init` instead: it adds the SDK pinned to an exact version, a key and this skill, and overwrites nothing. A new app gets `local/app.env` with the issuer, the origin `http://localhost:3000` and an empty `CROUTER_CLIENT_ID=`.
|
|
30
|
-
3. **Log in (you run it, the person approves).** Run `npx @north-light/sdk login`. It prints a link and a code. Show the person the link and the code exactly as printed and ask them to open the link, check the page shows the same code, and choose **Allow**. If your command times out first, run `login` again: it resumes the same code (do not pass `--new`).
|
|
31
|
-
|
|
32
|
-
|
|
30
|
+
3. **Log in (you run it, the person approves).** Run `npx @north-light/sdk login`. It prints a link and a code. Show the person the link and the code exactly as printed and ask them to open the link, check the page shows the same code, and choose **Allow**. Read everything `login` printed and its exit code before you tell the person what happened. If your command times out or is killed first, run `login` again: it resumes the same code (do not pass `--new`). Each message means one thing:
|
|
31
|
+
- `Still waiting for the login started at HH:MM.` is the same code: ask the person to approve it.
|
|
32
|
+
- `Already logged in as …` (exit 0) means the login exists: go to step 4. Do not log in again.
|
|
33
|
+
- `The code expired before it was approved` (exit 1) means nobody approved it in 10 minutes: run `login` again for a new code and tell the person to approve that one promptly. It is not a network problem.
|
|
34
|
+
- `The login was declined in the browser` (exit 1): the person chose Deny. Ask before trying again.
|
|
35
|
+
- `Couldn't reach … The code … is still saved` (exit 1) is a network problem that may pass: run `login` again to resume. Only if it keeps failing, use step 7.
|
|
36
|
+
`npx @north-light/sdk whoami` checks the login. Never describe a failure as a network timeout unless the output says it couldn't reach the directory.
|
|
37
|
+
4. **Register (you).** For the quickstart app run `npx @north-light/sdk register --scope sms:send`. `sms:send` is the scope `server.ts` requests. It registers the app with the public key from `local/app-key.json`, adds the scope and writes `CROUTER_CLIENT_ID` to `local/app.env`. For any other provider scope: register first, find the string with `npx @north-light/sdk apps asks` (it needs the registered app), then add it with `npx @north-light/sdk apps set-scopes --add <scope>`. Never invent a scope string ([Scopes and grants](https://sdk.docs.northlight.ai/docs/concepts/scopes-and-grants.md)). To give the app a logo image or brand colour, use `apps set-logo --file <path>` and `apps set-brand-color --color <#rrggbb>`.
|
|
38
|
+
5. **Run it (you, then the person).** `npm i`, then `npm start`. Run `npm i` before `register` or any other command if the project pins an SDK version: without installed dependencies `npx` downloads the newest published release of the SDK, not the pinned one. 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.
|
|
33
39
|
6. **Testing mode.** A new app is in testing: only its owner and up to 100 testers can sign in. Add one with `npx @north-light/sdk apps testers add --email <email>`. 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.
|
|
34
40
|
7. **If login is not possible** (no network to `account.northlight.ai`, or the person prefers the browser): they register at `https://account.northlight.ai/console/new`. Ask them to fill in **Name**, **Origin URL** (`http://localhost:3000`), **Logo URL** (a square image), **Why your app needs access** (one sentence) and **Redirect URIs** (`http://localhost:3000/auth/callback`, matched exactly), and to paste the public JWK that `init` printed. Never ask for or accept the private key. On the app's **What it asks for** tab they choose **Add permissions**, add `sms:send` and choose **Save** (**Done** alone stores nothing). Ask for the client ID the console shows, then run `npx @north-light/sdk env --client-id <client id>`.
|
|
35
41
|
|
|
@@ -147,8 +153,8 @@ Details behind each rule: [Errors](https://sdk.docs.northlight.ai/docs/errors.md
|
|
|
147
153
|
- **Pin the exact SDK version** (`npm i --save-exact`). A later release can change a type or method, and `^` picks it up on the next `npm i`.
|
|
148
154
|
- **Take scope strings from `npx @north-light/sdk apps asks`, the console's Add permissions list or `providers.tools`.** Never invent one. Tool names and arguments also come from `providers.tools('<provider>')`.
|
|
149
155
|
- **Never print, paste, log or commit `local/app-key.json`, `~/.config/northlight/credentials.json` or any `nld_` value.** `local/` is in `.gitignore`. If the app key leaks, add a new key in the console, switch to it, and retire the old one.
|
|
150
|
-
- **Do not run `logout` unless the person asks.** It revokes the login and the next session must ask them to approve again.
|
|
151
|
-
- **Deleting an app, listing it in the marketplace, its spend cap and sharing are the person's, in the console** (`https://account.northlight.ai/console`). Name the page; do not try the CLI, which refuses. On a verified app, signing keys, the JWKS URL, redirect URIs and the run notice URL change there too ([Manage your app](https://sdk.docs.northlight.ai/docs/guides/manage-your-app.md)).
|
|
156
|
+
- **Do not run `logout` unless the person asks.** It revokes the login on the server and removes it from `~/.config/northlight/credentials.json` (the file stays, with no login in it); the next session must ask them to approve again.
|
|
157
|
+
- **Deleting an app, listing it in the marketplace, its monthly spend cap and sharing are the person's, in the console** (`https://account.northlight.ai/console`). Name the page; do not try the CLI, which refuses. On a verified app, signing keys, the JWKS URL, redirect URIs and the run notice URL change there too ([Manage your app](https://sdk.docs.northlight.ai/docs/guides/manage-your-app.md)).
|
|
152
158
|
- **Use `oauth.client(userId)` for a person's runtime. Never `new Crouter()`.** `Crouter` is the client for a runtime you run yourself, not for apps.
|
|
153
159
|
- **Use a persistent `ConnectionStore` in production.** `MemoryConnectionStore` loses every sign-in on restart and does not work across processes. `@north-light/sdk/stores/postgres` is the production store.
|
|
154
160
|
- **Use `runs.parse` with a schema for data. Never parse `runs.trace`.** The trace is what the model said; `output_parsed` is a value checked against your schema.
|
|
@@ -166,11 +172,11 @@ Details behind each rule: [Errors](https://sdk.docs.northlight.ai/docs/errors.md
|
|
|
166
172
|
| `skill [--agent claude|agents|all] [--global] [--print]` | Write the packaged `northlight` skill into `.agents/skills` and `.claude/skills` (or `~/` with `--global`), replacing an older copy. `--print` writes it to stdout. | No |
|
|
167
173
|
| `keygen [--out <path>] [--app <client id>] [--yes]` | Write a private signing key to `local/app-key.json` (mode 0600) and print the public JWK to register. | Only with `--app` |
|
|
168
174
|
| `env --client-id <client id> [--origin <url>] [--issuer <url>] [--out <path>]`<br>`env --app <client id> [--out <path>]` | Write `CROUTER_ISSUER`, `CROUTER_CLIENT_ID` and `APP_ORIGIN` into `local/app.env`, keeping every other line. `--client-id` makes no network call; `--app` reads them through crtr. | Only with `--app` |
|
|
169
|
-
| `login [--no-browser] [--new] [--issuer <url>]` | Log this computer in to your Northlight account: prints a link and a code, you approve once in the browser, and a revocable login is saved to `~/.config/northlight/credentials.json`. Run it again if it times out: it resumes the same code. Calls only the Northlight directory. | No |
|
|
170
|
-
| `logout [--issuer <url>]` | Revoke this computer's login on the server and
|
|
175
|
+
| `login [--no-browser] [--new] [--issuer <url>]` | Log this computer in to your Northlight account: prints a link and a code, you approve once in the browser, and a revocable login is saved to `~/.config/northlight/credentials.json`. Run it again if it times out or is interrupted: it resumes the same code. When this computer is already logged in it says so and exits 0; `--new` logs in again. Calls only the Northlight directory. | No |
|
|
176
|
+
| `logout [--issuer <url>]` | Revoke this computer's login on the server and remove it from the credentials file (the file stays, without that login). | No |
|
|
171
177
|
| `whoami [--issuer <url>]` | Show the account this computer is logged in as and when the login expires. Exits 1 when not logged in. | No |
|
|
172
178
|
| `register [--name <text>] [--scope <scope>]... [--logo <url>] [--justification <text>] [--redirect-uri <uri>]... [--issuer <url>]` | Run in a project made by `init`, after `login`: registers the app with the public key from `local/app-key.json`, adds each `--scope` beyond `crtr:llm crtr:act`, and writes `CROUTER_CLIENT_ID` and `CROUTER_ISSUER` to `local/app.env`. | No |
|
|
173
|
-
| `apps <leaf> [--app <client id>] [--issuer <url>] [leaf flags] [--json]` | Call one app-management leaf with the login and print its answer as JSON. Leaves: `mine`, `get`, `update`, `set-redirect-uris`, `asks`, `set-scopes`, `set-requires`, `set-run-notice-url`, `keys list|add|retire|delete|set-jwks-uri|remove-jwks-uri`, `testers list|add|remove`. `--app` defaults to `CROUTER_CLIENT_ID` in `local/app.env`; `keys add` takes `--key-file <path>` and sends only the public half. Deleting an app, its marketplace listing, spend cap, sharing and provider role are console-only. | No |
|
|
179
|
+
| `apps <leaf> [--app <client id>] [--issuer <url>] [leaf flags] [--json]` | Call one app-management leaf with the login and print its answer as JSON. Leaves: `mine`, `get`, `update`, `set-logo`, `set-brand-color`, `set-redirect-uris`, `asks`, `set-scopes`, `set-requires`, `set-run-notice-url`, `keys list|add|retire|delete|set-jwks-uri|remove-jwks-uri`, `testers list|add|remove`. `--app` defaults to `CROUTER_CLIENT_ID` in `local/app.env`; `keys add` takes `--key-file <path>` and sends only the public half. Deleting an app, its marketplace listing, spend cap, sharing and provider role are console-only. | No |
|
|
174
180
|
|
|
175
181
|
## Pages
|
|
176
182
|
|
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.
|
|
2
|
+
export declare const SDK_VERSION = "0.3.488";
|
|
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.
|
|
3
|
+
export const SDK_VERSION = '0.3.488';
|
|
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,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@north-light/sdk",
|
|
3
|
-
"version": "0.3.
|
|
4
|
-
"crouterContract": "1.
|
|
3
|
+
"version": "0.3.488",
|
|
4
|
+
"crouterContract": "1.12",
|
|
5
5
|
"description": "Typed Node and browser client for running crouter agents through the crtrd /v1 API.",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"main": "./dist/index.js",
|
|
@@ -44,8 +44,8 @@
|
|
|
44
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); }\""
|
|
45
45
|
},
|
|
46
46
|
"dependencies": {
|
|
47
|
-
"@north-light/api": "0.3.
|
|
48
|
-
"@north-light/identity": "0.3.
|
|
47
|
+
"@north-light/api": "0.3.488",
|
|
48
|
+
"@north-light/identity": "0.3.488",
|
|
49
49
|
"jose": "^6.2.1"
|
|
50
50
|
},
|
|
51
51
|
"peerDependencies": {
|