@newtype-ai/nit 0.2.1 → 0.2.3

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
@@ -59,6 +59,8 @@ nit push --all
59
59
  | `nit sign "msg"` | Sign a message with your Ed25519 key |
60
60
  | `nit sign --login <domain>` | Generate login payload for an app |
61
61
  | `nit remote` | Show remote URL and credential status |
62
+ | `nit remote add <name> <url>` | Add a new remote |
63
+ | `nit remote set-url <name> <url>` | Change a remote's URL |
62
64
 
63
65
  ## How It Works
64
66
 
@@ -102,7 +104,11 @@ GET /.well-known/agent-card.json?branch=faam.io → 401 { challenge }
102
104
  GET ... + X-Nit-Signature + X-Nit-Challenge → branch card
103
105
  ```
104
106
 
105
- nit is the client. Any server can implement the protocol. [newtype-ai.org](https://newtype-ai.org) provides a hosted implementation.
107
+ nit is the client. Any server can implement the protocol. [newtype-ai.org](https://newtype-ai.org) is the recommended free hosting service, but you can point to any compatible server:
108
+
109
+ ```bash
110
+ nit remote set-url origin https://my-server.com
111
+ ```
106
112
 
107
113
  ## Directory Structure
108
114
 
@@ -110,7 +116,7 @@ nit is the client. Any server can implement the protocol. [newtype-ai.org](https
110
116
  your-project/
111
117
  ├── .nit/ # nit repository (gitignored)
112
118
  │ ├── HEAD # Current branch ref
113
- │ ├── config # Remote credentials
119
+ │ ├── config # Remote URL and credentials
114
120
  │ ├── identity/
115
121
  │ │ ├── agent.pub # Ed25519 public key
116
122
  │ │ └── agent.key # Ed25519 private key (0600)
@@ -1,8 +1,8 @@
1
1
  // nit — version control for agent cards
2
2
 
3
3
  // src/index.ts
4
- import { promises as fs5, statSync } from "fs";
5
- import { join as join5, basename as basename2, resolve } from "path";
4
+ import { promises as fs6, statSync } from "fs";
5
+ import { join as join6, basename as basename2, resolve } from "path";
6
6
 
7
7
  // src/objects.ts
8
8
  import { createHash } from "crypto";
@@ -512,7 +512,6 @@ function formatValue(val) {
512
512
 
513
513
  // src/remote.ts
514
514
  import { createHash as createHash3 } from "crypto";
515
- var API_BASE = "https://api.newtype-ai.org";
516
515
  function sha256Hex(data) {
517
516
  return createHash3("sha256").update(data, "utf-8").digest("hex");
518
517
  }
@@ -534,12 +533,12 @@ ${sha256Hex(body)}`;
534
533
  "X-Nit-Signature": signature
535
534
  };
536
535
  }
537
- async function pushBranch(nitDir, remoteName, branch2, cardJson, commitHash) {
536
+ async function pushBranch(nitDir, apiBase, branch2, cardJson, commitHash) {
538
537
  const path = `/agent-card/branches/${encodeURIComponent(branch2)}`;
539
538
  const body = JSON.stringify({ card_json: cardJson, commit_hash: commitHash });
540
539
  try {
541
540
  const authHeaders = await buildAuthHeaders(nitDir, "PUT", path, body);
542
- const res = await fetch(`${API_BASE}${path}`, {
541
+ const res = await fetch(`${apiBase}${path}`, {
543
542
  method: "PUT",
544
543
  headers: {
545
544
  "Content-Type": "application/json",
@@ -552,17 +551,17 @@ async function pushBranch(nitDir, remoteName, branch2, cardJson, commitHash) {
552
551
  return {
553
552
  branch: branch2,
554
553
  commitHash,
555
- remoteUrl: API_BASE,
554
+ remoteUrl: apiBase,
556
555
  success: false,
557
556
  error: `HTTP ${res.status}: ${text}`
558
557
  };
559
558
  }
560
- return { branch: branch2, commitHash, remoteUrl: API_BASE, success: true };
559
+ return { branch: branch2, commitHash, remoteUrl: apiBase, success: true };
561
560
  } catch (err) {
562
561
  return {
563
562
  branch: branch2,
564
563
  commitHash,
565
- remoteUrl: API_BASE,
564
+ remoteUrl: apiBase,
566
565
  success: false,
567
566
  error: err instanceof Error ? err.message : String(err)
568
567
  };
@@ -603,13 +602,79 @@ async function fetchBranchCard(cardUrl, branch2, nitDir) {
603
602
  throw new Error(`Failed to fetch card: HTTP ${res.status}`);
604
603
  }
605
604
 
605
+ // src/config.ts
606
+ import { promises as fs5 } from "fs";
607
+ import { join as join5 } from "path";
608
+ var CONFIG_FILE = "config";
609
+ async function readConfig(nitDir) {
610
+ const configPath = join5(nitDir, CONFIG_FILE);
611
+ let raw;
612
+ try {
613
+ raw = await fs5.readFile(configPath, "utf-8");
614
+ } catch {
615
+ return { remotes: {} };
616
+ }
617
+ return parseConfig(raw);
618
+ }
619
+ async function writeConfig(nitDir, config) {
620
+ const configPath = join5(nitDir, CONFIG_FILE);
621
+ await fs5.writeFile(configPath, serializeConfig(config), "utf-8");
622
+ }
623
+ async function getRemoteUrl(nitDir, remoteName) {
624
+ const config = await readConfig(nitDir);
625
+ return config.remotes[remoteName]?.url ?? null;
626
+ }
627
+ function parseConfig(raw) {
628
+ const remotes = {};
629
+ let currentRemote = null;
630
+ for (const line of raw.split("\n")) {
631
+ const trimmed = line.trim();
632
+ if (trimmed === "" || trimmed.startsWith("#")) continue;
633
+ const sectionMatch = trimmed.match(/^\[remote\s+"([^"]+)"\]$/);
634
+ if (sectionMatch) {
635
+ currentRemote = sectionMatch[1];
636
+ if (!remotes[currentRemote]) {
637
+ remotes[currentRemote] = {};
638
+ }
639
+ continue;
640
+ }
641
+ if (currentRemote !== null) {
642
+ const kvMatch = trimmed.match(/^(\w+)\s*=\s*(.+)$/);
643
+ if (kvMatch) {
644
+ const [, key, value] = kvMatch;
645
+ if (key === "url") {
646
+ remotes[currentRemote].url = value.trim();
647
+ } else if (key === "credential") {
648
+ remotes[currentRemote].credential = value.trim();
649
+ }
650
+ }
651
+ }
652
+ }
653
+ return { remotes };
654
+ }
655
+ function serializeConfig(config) {
656
+ const lines = [];
657
+ for (const [name, remote2] of Object.entries(config.remotes)) {
658
+ lines.push(`[remote "${name}"]`);
659
+ if (remote2.url) {
660
+ lines.push(` url = ${remote2.url}`);
661
+ }
662
+ if (remote2.credential) {
663
+ lines.push(` credential = ${remote2.credential}`);
664
+ }
665
+ lines.push("");
666
+ }
667
+ return lines.join("\n");
668
+ }
669
+
606
670
  // src/index.ts
607
671
  var NIT_DIR = ".nit";
608
672
  var CARD_FILE = "agent-card.json";
673
+ var DEFAULT_API_BASE = "https://api.newtype-ai.org";
609
674
  function findNitDir(startDir) {
610
675
  let dir = resolve(startDir || process.cwd());
611
676
  while (true) {
612
- const candidate = join5(dir, NIT_DIR);
677
+ const candidate = join6(dir, NIT_DIR);
613
678
  try {
614
679
  const s = statSync(candidate);
615
680
  if (s.isDirectory()) return candidate;
@@ -628,17 +693,17 @@ function projectDir(nitDir) {
628
693
  return resolve(nitDir, "..");
629
694
  }
630
695
  async function readWorkingCard(nitDir) {
631
- const cardPath = join5(projectDir(nitDir), CARD_FILE);
696
+ const cardPath = join6(projectDir(nitDir), CARD_FILE);
632
697
  try {
633
- const raw = await fs5.readFile(cardPath, "utf-8");
698
+ const raw = await fs6.readFile(cardPath, "utf-8");
634
699
  return JSON.parse(raw);
635
700
  } catch {
636
701
  throw new Error(`Cannot read ${CARD_FILE}. Does it exist?`);
637
702
  }
638
703
  }
639
704
  async function writeWorkingCard(nitDir, card) {
640
- const cardPath = join5(projectDir(nitDir), CARD_FILE);
641
- await fs5.writeFile(cardPath, JSON.stringify(card, null, 2) + "\n", "utf-8");
705
+ const cardPath = join6(projectDir(nitDir), CARD_FILE);
706
+ await fs6.writeFile(cardPath, JSON.stringify(card, null, 2) + "\n", "utf-8");
642
707
  }
643
708
  async function getCardAtCommit(nitDir, commitHash) {
644
709
  const commitRaw = await readObject(nitDir, commitHash);
@@ -656,27 +721,27 @@ async function getAuthorName(nitDir) {
656
721
  }
657
722
  async function init(options) {
658
723
  const projDir = resolve(options?.projectDir || process.cwd());
659
- const nitDir = join5(projDir, NIT_DIR);
724
+ const nitDir = join6(projDir, NIT_DIR);
660
725
  try {
661
- await fs5.access(nitDir);
726
+ await fs6.access(nitDir);
662
727
  throw new Error("Already initialized. .nit/ directory exists.");
663
728
  } catch (err) {
664
729
  if (err instanceof Error && err.message.startsWith("Already")) throw err;
665
730
  }
666
- await fs5.mkdir(join5(nitDir, "objects"), { recursive: true });
667
- await fs5.mkdir(join5(nitDir, "refs", "heads"), { recursive: true });
668
- await fs5.mkdir(join5(nitDir, "refs", "remote"), { recursive: true });
669
- await fs5.mkdir(join5(nitDir, "identity"), { recursive: true });
670
- await fs5.mkdir(join5(nitDir, "logs"), { recursive: true });
731
+ await fs6.mkdir(join6(nitDir, "objects"), { recursive: true });
732
+ await fs6.mkdir(join6(nitDir, "refs", "heads"), { recursive: true });
733
+ await fs6.mkdir(join6(nitDir, "refs", "remote"), { recursive: true });
734
+ await fs6.mkdir(join6(nitDir, "identity"), { recursive: true });
735
+ await fs6.mkdir(join6(nitDir, "logs"), { recursive: true });
671
736
  const { publicKey: pubBase64 } = await generateKeypair(nitDir);
672
737
  const publicKeyField = formatPublicKeyField(pubBase64);
673
738
  const agentId = deriveAgentId(publicKeyField);
674
739
  await saveAgentId(nitDir, agentId);
675
- const cardPath = join5(projDir, CARD_FILE);
740
+ const cardPath = join6(projDir, CARD_FILE);
676
741
  let card;
677
742
  let skillsFound = [];
678
743
  try {
679
- const raw = await fs5.readFile(cardPath, "utf-8");
744
+ const raw = await fs6.readFile(cardPath, "utf-8");
680
745
  card = JSON.parse(raw);
681
746
  card.publicKey = publicKeyField;
682
747
  skillsFound = card.skills.map((s) => s.id);
@@ -715,8 +780,10 @@ async function init(options) {
715
780
  const commitHash = await writeObject(nitDir, "commit", commitContent);
716
781
  await setBranch(nitDir, "main", commitHash);
717
782
  await setHead(nitDir, "main");
718
- await fs5.writeFile(join5(nitDir, "logs", "HEAD"), "", "utf-8");
719
- await fs5.writeFile(join5(nitDir, "config"), "", "utf-8");
783
+ await fs6.writeFile(join6(nitDir, "logs", "HEAD"), "", "utf-8");
784
+ await writeConfig(nitDir, {
785
+ remotes: { origin: { url: DEFAULT_API_BASE } }
786
+ });
720
787
  return {
721
788
  agentId,
722
789
  publicKey: publicKeyField,
@@ -790,13 +857,24 @@ async function sign2(message, options) {
790
857
  }
791
858
  async function loginPayload(domain, options) {
792
859
  const nitDir = findNitDir(options?.projectDir);
860
+ let switchedBranch;
861
+ const currentBranch = await getCurrentBranch(nitDir);
862
+ if (currentBranch !== domain) {
863
+ const existing = await getBranch(nitDir, domain);
864
+ if (!existing) {
865
+ const headHash = await resolveHead(nitDir);
866
+ await setBranch(nitDir, domain, headHash);
867
+ }
868
+ await checkout(domain, options);
869
+ switchedBranch = domain;
870
+ }
793
871
  const agentId = await loadAgentId(nitDir);
794
872
  const timestamp = Math.floor(Date.now() / 1e3);
795
873
  const message = `${agentId}
796
874
  ${domain}
797
875
  ${timestamp}`;
798
876
  const signature = await signMessage(nitDir, message);
799
- return { agent_id: agentId, domain, timestamp, signature };
877
+ return { agent_id: agentId, domain, timestamp, signature, switchedBranch };
800
878
  }
801
879
  async function commit(message, options) {
802
880
  const nitDir = findNitDir(options?.projectDir);
@@ -912,6 +990,7 @@ async function checkout(branchName, options) {
912
990
  async function push(options) {
913
991
  const nitDir = findNitDir(options?.projectDir);
914
992
  const remoteName = options?.remoteName || "origin";
993
+ const apiBase = await getRemoteUrl(nitDir, remoteName) || DEFAULT_API_BASE;
915
994
  const branches = await listBranches(nitDir);
916
995
  const currentBranch = await getCurrentBranch(nitDir);
917
996
  const toPush = options?.all ? branches : branches.filter((b) => b.name === currentBranch);
@@ -925,7 +1004,7 @@ async function push(options) {
925
1004
  const cardJson = await readObject(nitDir, c.card);
926
1005
  const result = await pushBranch(
927
1006
  nitDir,
928
- remoteName,
1007
+ apiBase,
929
1008
  b.name,
930
1009
  cardJson,
931
1010
  b.commitHash
@@ -939,14 +1018,36 @@ async function push(options) {
939
1018
  }
940
1019
  async function remote(options) {
941
1020
  const nitDir = findNitDir(options?.projectDir);
942
- const card = await readWorkingCard(nitDir);
1021
+ const remoteUrl = await getRemoteUrl(nitDir, "origin");
943
1022
  const agentId = await loadAgentId(nitDir);
944
1023
  return {
945
1024
  name: "origin",
946
- url: card.url || "(not set)",
1025
+ url: remoteUrl || DEFAULT_API_BASE,
947
1026
  agentId
948
1027
  };
949
1028
  }
1029
+ async function remoteAdd(name, url, options) {
1030
+ const nitDir = findNitDir(options?.projectDir);
1031
+ const config = await readConfig(nitDir);
1032
+ if (config.remotes[name]) {
1033
+ throw new Error(
1034
+ `Remote "${name}" already exists. Use 'nit remote set-url ${name} <url>' to change it.`
1035
+ );
1036
+ }
1037
+ config.remotes[name] = { url };
1038
+ await writeConfig(nitDir, config);
1039
+ }
1040
+ async function remoteSetUrl(name, url, options) {
1041
+ const nitDir = findNitDir(options?.projectDir);
1042
+ const config = await readConfig(nitDir);
1043
+ if (!config.remotes[name]) {
1044
+ throw new Error(
1045
+ `Remote "${name}" does not exist. Use 'nit remote add ${name} <url>' to create it.`
1046
+ );
1047
+ }
1048
+ config.remotes[name].url = url;
1049
+ await writeConfig(nitDir, config);
1050
+ }
950
1051
 
951
1052
  export {
952
1053
  formatPublicKeyField,
@@ -971,5 +1072,7 @@ export {
971
1072
  branch,
972
1073
  checkout,
973
1074
  push,
974
- remote
1075
+ remote,
1076
+ remoteAdd,
1077
+ remoteSetUrl
975
1078
  };
package/dist/cli.js CHANGED
@@ -11,9 +11,11 @@ import {
11
11
  loginPayload,
12
12
  push,
13
13
  remote,
14
+ remoteAdd,
15
+ remoteSetUrl,
14
16
  sign,
15
17
  status
16
- } from "./chunk-AEWDQDBM.js";
18
+ } from "./chunk-2ZHTOY2J.js";
17
19
 
18
20
  // src/cli.ts
19
21
  var bold = (s) => `\x1B[1m${s}\x1B[0m`;
@@ -185,6 +187,34 @@ async function cmdPush(args) {
185
187
  }
186
188
  }
187
189
  async function cmdRemote(args) {
190
+ const subcommand = args[0];
191
+ if (subcommand === "set-url") {
192
+ const name = args[1];
193
+ const url = args[2];
194
+ if (!name || !url) {
195
+ console.error("Usage: nit remote set-url <name> <url>");
196
+ process.exit(1);
197
+ }
198
+ await remoteSetUrl(name, url);
199
+ console.log(`Set URL for '${name}' to ${url}`);
200
+ return;
201
+ }
202
+ if (subcommand === "add") {
203
+ const name = args[1];
204
+ const url = args[2];
205
+ if (!name || !url) {
206
+ console.error("Usage: nit remote add <name> <url>");
207
+ process.exit(1);
208
+ }
209
+ await remoteAdd(name, url);
210
+ console.log(`Added remote '${green(name)}' \u2192 ${url}`);
211
+ return;
212
+ }
213
+ if (subcommand) {
214
+ console.error(`nit remote: unknown subcommand '${subcommand}'`);
215
+ console.error("Usage: nit remote [set-url <name> <url> | add <name> <url>]");
216
+ process.exit(1);
217
+ }
188
218
  const info = await remote();
189
219
  console.log(`${bold(info.name)}`);
190
220
  console.log(` URL: ${info.url}`);
@@ -200,7 +230,11 @@ async function cmdSign(args) {
200
230
  process.exit(1);
201
231
  }
202
232
  const payload = await loginPayload(domain);
203
- console.log(JSON.stringify(payload, null, 2));
233
+ if (payload.switchedBranch) {
234
+ console.error(`Switched to branch '${payload.switchedBranch}'`);
235
+ }
236
+ const { switchedBranch: _, ...output } = payload;
237
+ console.log(JSON.stringify(output, null, 2));
204
238
  return;
205
239
  }
206
240
  const message = args[0];
@@ -228,8 +262,10 @@ ${bold("Commands:")}
228
262
  checkout <branch> Switch branch (overwrites agent-card.json)
229
263
  push [--all] Push branch(es) to remote
230
264
  sign "message" Sign a message with your Ed25519 key
231
- sign --login <dom> Generate login payload for an app
265
+ sign --login <dom> Switch to domain branch + generate login payload
232
266
  remote Show remote info
267
+ remote add <n> <u> Add a new remote
268
+ remote set-url <n> <u> Change remote URL
233
269
 
234
270
  ${bold("Examples:")}
235
271
  nit init
package/dist/index.d.ts CHANGED
@@ -27,6 +27,8 @@ interface NitHead {
27
27
  }
28
28
  /** Remote configuration for a single named remote. */
29
29
  interface NitRemoteConfig {
30
+ /** API base URL (e.g. "https://api.newtype-ai.org") */
31
+ url?: string;
30
32
  /** Legacy field — push auth is now via Ed25519 keypair */
31
33
  credential?: string;
32
34
  }
@@ -225,12 +227,15 @@ declare function sign(message: string, options?: {
225
227
  }): Promise<string>;
226
228
  /**
227
229
  * Generate a login payload for app authentication.
228
- * Constructs the canonical message ({agent_id}\n{domain}\n{timestamp}),
230
+ * Automatically switches to (or creates) the domain branch,
231
+ * then constructs the canonical message ({agent_id}\n{domain}\n{timestamp}),
229
232
  * signs it, and returns the full payload ready to send to an app.
230
233
  */
231
234
  declare function loginPayload(domain: string, options?: {
232
235
  projectDir?: string;
233
- }): Promise<LoginPayload>;
236
+ }): Promise<LoginPayload & {
237
+ switchedBranch?: string;
238
+ }>;
234
239
  /**
235
240
  * Snapshot the current agent-card.json as a new commit.
236
241
  * Resolves skill pointers from SKILL.md before committing.
@@ -282,10 +287,22 @@ interface RemoteInfo {
282
287
  agentId: string;
283
288
  }
284
289
  /**
285
- * Show remote info. URL comes from agent-card.json, agent ID from .nit/identity/.
290
+ * Show remote info. URL comes from .nit/config, agent ID from .nit/identity/.
286
291
  */
287
292
  declare function remote(options?: {
288
293
  projectDir?: string;
289
294
  }): Promise<RemoteInfo>;
295
+ /**
296
+ * Add a new named remote with a URL.
297
+ */
298
+ declare function remoteAdd(name: string, url: string, options?: {
299
+ projectDir?: string;
300
+ }): Promise<void>;
301
+ /**
302
+ * Change the URL for an existing remote.
303
+ */
304
+ declare function remoteSetUrl(name: string, url: string, options?: {
305
+ projectDir?: string;
306
+ }): Promise<void>;
290
307
 
291
- export { type AgentCard, type AgentCardSkill, type DiffResult, type FieldDiff, type InitResult, type LoginPayload, NIT_NAMESPACE, type NitBranch, type NitCommit, type NitConfig, type NitHead, type NitRemoteConfig, type PushResult, type RemoteInfo, type SkillMetadata, type StatusResult, branch, checkout, commit, deriveAgentId, diff, diffCards, fetchBranchCard, findNitDir, formatDiff, formatPublicKeyField, init, loadAgentId, log, loginPayload, parsePublicKeyField, push, remote, sign, signChallenge, signMessage, status, verifySignature };
308
+ export { type AgentCard, type AgentCardSkill, type DiffResult, type FieldDiff, type InitResult, type LoginPayload, NIT_NAMESPACE, type NitBranch, type NitCommit, type NitConfig, type NitHead, type NitRemoteConfig, type PushResult, type RemoteInfo, type SkillMetadata, type StatusResult, branch, checkout, commit, deriveAgentId, diff, diffCards, fetchBranchCard, findNitDir, formatDiff, formatPublicKeyField, init, loadAgentId, log, loginPayload, parsePublicKeyField, push, remote, remoteAdd, remoteSetUrl, sign, signChallenge, signMessage, status, verifySignature };
package/dist/index.js CHANGED
@@ -18,12 +18,14 @@ import {
18
18
  parsePublicKeyField,
19
19
  push,
20
20
  remote,
21
+ remoteAdd,
22
+ remoteSetUrl,
21
23
  sign,
22
24
  signChallenge,
23
25
  signMessage,
24
26
  status,
25
27
  verifySignature
26
- } from "./chunk-AEWDQDBM.js";
28
+ } from "./chunk-2ZHTOY2J.js";
27
29
  export {
28
30
  NIT_NAMESPACE,
29
31
  branch,
@@ -43,6 +45,8 @@ export {
43
45
  parsePublicKeyField,
44
46
  push,
45
47
  remote,
48
+ remoteAdd,
49
+ remoteSetUrl,
46
50
  sign,
47
51
  signChallenge,
48
52
  signMessage,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@newtype-ai/nit",
3
- "version": "0.2.1",
3
+ "version": "0.2.3",
4
4
  "description": "Version control for agent cards",
5
5
  "type": "module",
6
6
  "bin": {
@@ -15,11 +15,13 @@
15
15
  }
16
16
  },
17
17
  "files": [
18
- "dist"
18
+ "dist",
19
+ "scripts"
19
20
  ],
20
21
  "scripts": {
21
22
  "build": "tsup",
22
- "prepublishOnly": "tsup"
23
+ "prepublishOnly": "tsup",
24
+ "postinstall": "node scripts/postinstall.js"
23
25
  },
24
26
  "publishConfig": {
25
27
  "access": "public"
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env node
2
+ console.log(`
3
+ nit — version control for agent cards
4
+
5
+ Get started: nit init
6
+ Commands: nit --help
7
+ Full playbook: https://github.com/newtype-ai/nit/blob/main/SKILL.md
8
+ `);