@newtype-ai/nit 0.1.2 → 0.2.1

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
@@ -49,13 +49,15 @@ nit push --all
49
49
  | Command | Description |
50
50
  |---------|-------------|
51
51
  | `nit init` | Create `.nit/`, generate Ed25519 keypair, initial commit |
52
- | `nit status` | Current branch, uncommitted changes, ahead/behind remote |
53
- | `nit commit -m "msg"` | Snapshot agent-card.json (auto-resolves SKILL.md pointers) |
52
+ | `nit status` | Identity info, current branch, uncommitted changes |
53
+ | `nit commit -m "msg"` | Snapshot agent-card.json |
54
54
  | `nit log` | Commit history for current branch |
55
55
  | `nit diff [target]` | JSON diff vs HEAD, branch, or commit |
56
56
  | `nit branch [name]` | List branches or create a new one |
57
57
  | `nit checkout <branch>` | Switch branch (overwrites agent-card.json) |
58
58
  | `nit push [--all]` | Push branch(es) to remote |
59
+ | `nit sign "msg"` | Sign a message with your Ed25519 key |
60
+ | `nit sign --login <domain>` | Generate login payload for an app |
59
61
  | `nit remote` | Show remote URL and credential status |
60
62
 
61
63
  ## How It Works
@@ -1,8 +1,8 @@
1
1
  // nit — version control for agent cards
2
2
 
3
3
  // src/index.ts
4
- import { promises as fs6, statSync } from "fs";
5
- import { join as join6, basename as basename2, resolve } from "path";
4
+ import { promises as fs5, statSync } from "fs";
5
+ import { join as join5, basename as basename2, resolve } from "path";
6
6
 
7
7
  // src/objects.ts
8
8
  import { createHash } from "crypto";
@@ -170,6 +170,7 @@ async function getRemoteRef(nitDir, remote2, branch2) {
170
170
 
171
171
  // src/identity.ts
172
172
  import {
173
+ createHash as createHash2,
173
174
  generateKeyPairSync,
174
175
  createPrivateKey,
175
176
  createPublicKey,
@@ -261,94 +262,73 @@ function verifySignature(pubBase64, challenge, signatureBase64) {
261
262
  Buffer.from(signatureBase64, "base64")
262
263
  );
263
264
  }
264
-
265
- // src/config.ts
266
- import { promises as fs4 } from "fs";
267
- import { join as join4 } from "path";
268
- var CONFIG_FILE = "config";
269
- async function readConfig(nitDir) {
270
- const configPath = join4(nitDir, CONFIG_FILE);
271
- let raw;
265
+ async function signMessage(nitDir, message) {
266
+ const privateKey = await loadPrivateKey(nitDir);
267
+ const sig = sign(null, Buffer.from(message, "utf-8"), privateKey);
268
+ return sig.toString("base64");
269
+ }
270
+ var NIT_NAMESPACE = "801ba518-f326-47e5-97c9-d1efd1865a19";
271
+ function deriveAgentId(publicKeyField) {
272
+ return uuidv5(publicKeyField, NIT_NAMESPACE);
273
+ }
274
+ async function loadAgentId(nitDir) {
275
+ const idPath = join3(nitDir, "identity", "agent-id");
272
276
  try {
273
- raw = await fs4.readFile(configPath, "utf-8");
277
+ return (await fs3.readFile(idPath, "utf-8")).trim();
274
278
  } catch {
275
- return { remotes: {} };
276
- }
277
- return parseConfig(raw);
278
- }
279
- async function writeConfig(nitDir, config) {
280
- const configPath = join4(nitDir, CONFIG_FILE);
281
- await fs4.writeFile(configPath, serializeConfig(config), "utf-8");
282
- }
283
- async function getRemoteCredential(nitDir, remoteName) {
284
- const config = await readConfig(nitDir);
285
- return config.remotes[remoteName]?.credential ?? null;
286
- }
287
- async function setRemoteCredential(nitDir, remoteName, credential) {
288
- const config = await readConfig(nitDir);
289
- if (!config.remotes[remoteName]) {
290
- config.remotes[remoteName] = {};
291
- }
292
- config.remotes[remoteName].credential = credential;
293
- await writeConfig(nitDir, config);
294
- }
295
- function parseConfig(raw) {
296
- const remotes = {};
297
- let currentRemote = null;
298
- for (const line of raw.split("\n")) {
299
- const trimmed = line.trim();
300
- if (trimmed === "" || trimmed.startsWith("#")) continue;
301
- const sectionMatch = trimmed.match(/^\[remote\s+"([^"]+)"\]$/);
302
- if (sectionMatch) {
303
- currentRemote = sectionMatch[1];
304
- if (!remotes[currentRemote]) {
305
- remotes[currentRemote] = {};
306
- }
307
- continue;
308
- }
309
- if (currentRemote !== null) {
310
- const kvMatch = trimmed.match(/^(\w+)\s*=\s*(.+)$/);
311
- if (kvMatch) {
312
- const [, key, value] = kvMatch;
313
- if (key === "credential") {
314
- remotes[currentRemote].credential = value.trim();
315
- }
316
- }
317
- }
279
+ throw new Error(
280
+ "No agent ID found. Run `nit init` to generate identity."
281
+ );
318
282
  }
319
- return { remotes };
320
283
  }
321
- function serializeConfig(config) {
322
- const lines = [];
323
- for (const [name, remote2] of Object.entries(config.remotes)) {
324
- lines.push(`[remote "${name}"]`);
325
- if (remote2.credential) {
326
- lines.push(` credential = ${remote2.credential}`);
327
- }
328
- lines.push("");
329
- }
330
- return lines.join("\n");
284
+ async function saveAgentId(nitDir, agentId) {
285
+ const idPath = join3(nitDir, "identity", "agent-id");
286
+ await fs3.writeFile(idPath, agentId + "\n", "utf-8");
287
+ }
288
+ function parseUuid(uuid) {
289
+ const hex = uuid.replace(/-/g, "");
290
+ return Buffer.from(hex, "hex");
291
+ }
292
+ function formatUuid(bytes) {
293
+ const hex = bytes.toString("hex");
294
+ return [
295
+ hex.slice(0, 8),
296
+ hex.slice(8, 12),
297
+ hex.slice(12, 16),
298
+ hex.slice(16, 20),
299
+ hex.slice(20, 32)
300
+ ].join("-");
301
+ }
302
+ function uuidv5(name, namespace) {
303
+ const namespaceBytes = parseUuid(namespace);
304
+ const nameBytes = Buffer.from(name, "utf-8");
305
+ const data = Buffer.concat([namespaceBytes, nameBytes]);
306
+ const hash = createHash2("sha1").update(data).digest();
307
+ const uuid = Buffer.from(hash.subarray(0, 16));
308
+ uuid[6] = uuid[6] & 15 | 80;
309
+ uuid[8] = uuid[8] & 63 | 128;
310
+ return formatUuid(uuid);
331
311
  }
332
312
 
333
313
  // src/skills.ts
334
- import { promises as fs5 } from "fs";
335
- import { join as join5 } from "path";
314
+ import { promises as fs4 } from "fs";
315
+ import { join as join4 } from "path";
336
316
  import { homedir } from "os";
337
317
  async function discoverSkills(projectDir2) {
338
318
  const home = homedir();
339
319
  const searchDirs = [
340
320
  // Project-local (all known agent frameworks)
341
- join5(projectDir2, ".claude", "skills"),
342
- join5(projectDir2, ".cursor", "skills"),
343
- join5(projectDir2, ".windsurf", "skills"),
344
- join5(projectDir2, ".codex", "skills"),
345
- join5(projectDir2, ".agents", "skills"),
321
+ join4(projectDir2, ".claude", "skills"),
322
+ join4(projectDir2, ".cursor", "skills"),
323
+ join4(projectDir2, ".windsurf", "skills"),
324
+ join4(projectDir2, ".codex", "skills"),
325
+ join4(projectDir2, ".agents", "skills"),
346
326
  // User-global
347
- join5(home, ".claude", "skills"),
327
+ join4(home, ".claude", "skills"),
348
328
  // Claude Code + Cursor (shared)
349
- join5(home, ".codex", "skills"),
329
+ join4(home, ".codex", "skills"),
350
330
  // Codex CLI
351
- join5(home, ".codeium", "windsurf", "skills")
331
+ join4(home, ".codeium", "windsurf", "skills")
352
332
  // Windsurf
353
333
  ];
354
334
  const seen = /* @__PURE__ */ new Set();
@@ -381,15 +361,15 @@ async function resolveSkillPointers(card, projectDir2) {
381
361
  async function scanSkillDir(dir) {
382
362
  let entries;
383
363
  try {
384
- entries = await fs5.readdir(dir);
364
+ entries = await fs4.readdir(dir);
385
365
  } catch {
386
366
  return [];
387
367
  }
388
368
  const skills = [];
389
369
  for (const entry of entries) {
390
- const skillMdPath = join5(dir, entry, "SKILL.md");
370
+ const skillMdPath = join4(dir, entry, "SKILL.md");
391
371
  try {
392
- const content = await fs5.readFile(skillMdPath, "utf-8");
372
+ const content = await fs4.readFile(skillMdPath, "utf-8");
393
373
  const meta = parseFrontmatter(content, entry, skillMdPath);
394
374
  if (meta) skills.push(meta);
395
375
  } catch {
@@ -531,36 +511,50 @@ function formatValue(val) {
531
511
  }
532
512
 
533
513
  // src/remote.ts
514
+ import { createHash as createHash3 } from "crypto";
534
515
  var API_BASE = "https://api.newtype-ai.org";
516
+ function sha256Hex(data) {
517
+ return createHash3("sha256").update(data, "utf-8").digest("hex");
518
+ }
519
+ async function buildAuthHeaders(nitDir, method, path, body) {
520
+ const agentId = await loadAgentId(nitDir);
521
+ const timestamp = Math.floor(Date.now() / 1e3).toString();
522
+ let message = `${method}
523
+ ${path}
524
+ ${agentId}
525
+ ${timestamp}`;
526
+ if (body !== void 0) {
527
+ message += `
528
+ ${sha256Hex(body)}`;
529
+ }
530
+ const signature = await signMessage(nitDir, message);
531
+ return {
532
+ "X-Nit-Agent-Id": agentId,
533
+ "X-Nit-Timestamp": timestamp,
534
+ "X-Nit-Signature": signature
535
+ };
536
+ }
535
537
  async function pushBranch(nitDir, remoteName, branch2, cardJson, commitHash) {
536
- const credential = await getRemoteCredential(nitDir, remoteName);
537
- if (!credential) {
538
- return {
539
- branch: branch2,
540
- commitHash,
541
- remoteUrl: API_BASE,
542
- success: false,
543
- error: `No credential configured for remote "${remoteName}". Run: nit remote set-credential <agent-key>`
544
- };
545
- }
546
- const url = `${API_BASE}/agent-card/branches/${encodeURIComponent(branch2)}`;
538
+ const path = `/agent-card/branches/${encodeURIComponent(branch2)}`;
539
+ const body = JSON.stringify({ card_json: cardJson, commit_hash: commitHash });
547
540
  try {
548
- const res = await fetch(url, {
541
+ const authHeaders = await buildAuthHeaders(nitDir, "PUT", path, body);
542
+ const res = await fetch(`${API_BASE}${path}`, {
549
543
  method: "PUT",
550
544
  headers: {
551
545
  "Content-Type": "application/json",
552
- Authorization: `Bearer ${credential}`
546
+ ...authHeaders
553
547
  },
554
- body: JSON.stringify({ card_json: cardJson, commit_hash: commitHash })
548
+ body
555
549
  });
556
550
  if (!res.ok) {
557
- const body = await res.text();
551
+ const text = await res.text();
558
552
  return {
559
553
  branch: branch2,
560
554
  commitHash,
561
555
  remoteUrl: API_BASE,
562
556
  success: false,
563
- error: `HTTP ${res.status}: ${body}`
557
+ error: `HTTP ${res.status}: ${text}`
564
558
  };
565
559
  }
566
560
  return { branch: branch2, commitHash, remoteUrl: API_BASE, success: true };
@@ -615,7 +609,7 @@ var CARD_FILE = "agent-card.json";
615
609
  function findNitDir(startDir) {
616
610
  let dir = resolve(startDir || process.cwd());
617
611
  while (true) {
618
- const candidate = join6(dir, NIT_DIR);
612
+ const candidate = join5(dir, NIT_DIR);
619
613
  try {
620
614
  const s = statSync(candidate);
621
615
  if (s.isDirectory()) return candidate;
@@ -634,17 +628,17 @@ function projectDir(nitDir) {
634
628
  return resolve(nitDir, "..");
635
629
  }
636
630
  async function readWorkingCard(nitDir) {
637
- const cardPath = join6(projectDir(nitDir), CARD_FILE);
631
+ const cardPath = join5(projectDir(nitDir), CARD_FILE);
638
632
  try {
639
- const raw = await fs6.readFile(cardPath, "utf-8");
633
+ const raw = await fs5.readFile(cardPath, "utf-8");
640
634
  return JSON.parse(raw);
641
635
  } catch {
642
636
  throw new Error(`Cannot read ${CARD_FILE}. Does it exist?`);
643
637
  }
644
638
  }
645
639
  async function writeWorkingCard(nitDir, card) {
646
- const cardPath = join6(projectDir(nitDir), CARD_FILE);
647
- await fs6.writeFile(cardPath, JSON.stringify(card, null, 2) + "\n", "utf-8");
640
+ const cardPath = join5(projectDir(nitDir), CARD_FILE);
641
+ await fs5.writeFile(cardPath, JSON.stringify(card, null, 2) + "\n", "utf-8");
648
642
  }
649
643
  async function getCardAtCommit(nitDir, commitHash) {
650
644
  const commitRaw = await readObject(nitDir, commitHash);
@@ -662,25 +656,27 @@ async function getAuthorName(nitDir) {
662
656
  }
663
657
  async function init(options) {
664
658
  const projDir = resolve(options?.projectDir || process.cwd());
665
- const nitDir = join6(projDir, NIT_DIR);
659
+ const nitDir = join5(projDir, NIT_DIR);
666
660
  try {
667
- await fs6.access(nitDir);
661
+ await fs5.access(nitDir);
668
662
  throw new Error("Already initialized. .nit/ directory exists.");
669
663
  } catch (err) {
670
664
  if (err instanceof Error && err.message.startsWith("Already")) throw err;
671
665
  }
672
- await fs6.mkdir(join6(nitDir, "objects"), { recursive: true });
673
- await fs6.mkdir(join6(nitDir, "refs", "heads"), { recursive: true });
674
- await fs6.mkdir(join6(nitDir, "refs", "remote"), { recursive: true });
675
- await fs6.mkdir(join6(nitDir, "identity"), { recursive: true });
676
- await fs6.mkdir(join6(nitDir, "logs"), { recursive: true });
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 });
677
671
  const { publicKey: pubBase64 } = await generateKeypair(nitDir);
678
672
  const publicKeyField = formatPublicKeyField(pubBase64);
679
- const cardPath = join6(projDir, CARD_FILE);
673
+ const agentId = deriveAgentId(publicKeyField);
674
+ await saveAgentId(nitDir, agentId);
675
+ const cardPath = join5(projDir, CARD_FILE);
680
676
  let card;
681
677
  let skillsFound = [];
682
678
  try {
683
- const raw = await fs6.readFile(cardPath, "utf-8");
679
+ const raw = await fs5.readFile(cardPath, "utf-8");
684
680
  card = JSON.parse(raw);
685
681
  card.publicKey = publicKeyField;
686
682
  skillsFound = card.skills.map((s) => s.id);
@@ -703,6 +699,9 @@ async function init(options) {
703
699
  }))
704
700
  };
705
701
  }
702
+ if (!card.url) {
703
+ card.url = `https://agent-${agentId}.newtype-ai.org`;
704
+ }
706
705
  await writeWorkingCard(nitDir, card);
707
706
  const cardJson = JSON.stringify(card, null, 2);
708
707
  const cardHash = await writeObject(nitDir, "card", cardJson);
@@ -716,11 +715,12 @@ async function init(options) {
716
715
  const commitHash = await writeObject(nitDir, "commit", commitContent);
717
716
  await setBranch(nitDir, "main", commitHash);
718
717
  await setHead(nitDir, "main");
719
- await fs6.writeFile(join6(nitDir, "logs", "HEAD"), "", "utf-8");
720
- await fs6.writeFile(join6(nitDir, "config"), "", "utf-8");
718
+ await fs5.writeFile(join5(nitDir, "logs", "HEAD"), "", "utf-8");
719
+ await fs5.writeFile(join5(nitDir, "config"), "", "utf-8");
721
720
  return {
721
+ agentId,
722
722
  publicKey: publicKeyField,
723
- cardUrl: card.url || null,
723
+ cardUrl: card.url,
724
724
  skillsFound
725
725
  };
726
726
  }
@@ -729,12 +729,15 @@ async function status(options) {
729
729
  const currentBranch = await getCurrentBranch(nitDir);
730
730
  const pubBase64 = await loadPublicKey(nitDir);
731
731
  const publicKey = formatPublicKeyField(pubBase64);
732
+ const agentId = await loadAgentId(nitDir);
733
+ const workingCard = await readWorkingCard(nitDir);
734
+ const cardUrl = workingCard.url || `https://agent-${agentId}.newtype-ai.org`;
732
735
  let uncommittedChanges = null;
733
736
  try {
734
737
  const headHash = await resolveHead(nitDir);
735
738
  const headCard = await getCardAtCommit(nitDir, headHash);
736
- const workingCard = await readWorkingCard(nitDir);
737
- const d = diffCards(headCard, workingCard);
739
+ const workingCard2 = await readWorkingCard(nitDir);
740
+ const d = diffCards(headCard, workingCard2);
738
741
  if (d.changed) {
739
742
  uncommittedChanges = d;
740
743
  }
@@ -773,12 +776,28 @@ async function status(options) {
773
776
  branchStatus.push({ name: b.name, ahead, behind: 0 });
774
777
  }
775
778
  return {
779
+ agentId,
780
+ cardUrl,
776
781
  branch: currentBranch,
777
782
  publicKey,
778
783
  uncommittedChanges,
779
784
  branches: branchStatus
780
785
  };
781
786
  }
787
+ async function sign2(message, options) {
788
+ const nitDir = findNitDir(options?.projectDir);
789
+ return signMessage(nitDir, message);
790
+ }
791
+ async function loginPayload(domain, options) {
792
+ const nitDir = findNitDir(options?.projectDir);
793
+ const agentId = await loadAgentId(nitDir);
794
+ const timestamp = Math.floor(Date.now() / 1e3);
795
+ const message = `${agentId}
796
+ ${domain}
797
+ ${timestamp}`;
798
+ const signature = await signMessage(nitDir, message);
799
+ return { agent_id: agentId, domain, timestamp, signature };
800
+ }
782
801
  async function commit(message, options) {
783
802
  const nitDir = findNitDir(options?.projectDir);
784
803
  const projDir = projectDir(nitDir);
@@ -921,36 +940,36 @@ async function push(options) {
921
940
  async function remote(options) {
922
941
  const nitDir = findNitDir(options?.projectDir);
923
942
  const card = await readWorkingCard(nitDir);
924
- const credential = await getRemoteCredential(nitDir, "origin");
943
+ const agentId = await loadAgentId(nitDir);
925
944
  return {
926
945
  name: "origin",
927
946
  url: card.url || "(not set)",
928
- hasCredential: credential !== null
947
+ agentId
929
948
  };
930
949
  }
931
- async function setCredential(credential, options) {
932
- const nitDir = findNitDir(options?.projectDir);
933
- const remoteName = options?.remoteName || "origin";
934
- await setRemoteCredential(nitDir, remoteName, credential);
935
- }
936
950
 
937
951
  export {
938
952
  formatPublicKeyField,
939
953
  parsePublicKeyField,
940
954
  signChallenge,
941
955
  verifySignature,
956
+ signMessage,
957
+ NIT_NAMESPACE,
958
+ deriveAgentId,
959
+ loadAgentId,
942
960
  diffCards,
943
961
  formatDiff,
944
962
  fetchBranchCard,
945
963
  findNitDir,
946
964
  init,
947
965
  status,
966
+ sign2 as sign,
967
+ loginPayload,
948
968
  commit,
949
969
  log,
950
970
  diff,
951
971
  branch,
952
972
  checkout,
953
973
  push,
954
- remote,
955
- setCredential
974
+ remote
956
975
  };
package/dist/cli.js CHANGED
@@ -8,11 +8,12 @@ import {
8
8
  formatDiff,
9
9
  init,
10
10
  log,
11
+ loginPayload,
11
12
  push,
12
13
  remote,
13
- setCredential,
14
+ sign,
14
15
  status
15
- } from "./chunk-5AVY6P7B.js";
16
+ } from "./chunk-AEWDQDBM.js";
16
17
 
17
18
  // src/cli.ts
18
19
  var bold = (s) => `\x1B[1m${s}\x1B[0m`;
@@ -48,6 +49,9 @@ async function main() {
48
49
  case "push":
49
50
  await cmdPush(args);
50
51
  break;
52
+ case "sign":
53
+ await cmdSign(args);
54
+ break;
51
55
  case "remote":
52
56
  await cmdRemote(args);
53
57
  break;
@@ -71,10 +75,9 @@ async function cmdInit() {
71
75
  const result = await init();
72
76
  console.log(bold("Initialized nit repository"));
73
77
  console.log();
74
- console.log(` Public key: ${green(result.publicKey)}`);
75
- if (result.cardUrl) {
76
- console.log(` Card URL: ${result.cardUrl}`);
77
- }
78
+ console.log(` Agent ID: ${green(result.agentId)}`);
79
+ console.log(` Public key: ${dim(result.publicKey)}`);
80
+ console.log(` Card URL: ${result.cardUrl}`);
78
81
  if (result.skillsFound.length > 0) {
79
82
  console.log(` Skills: ${result.skillsFound.join(", ")}`);
80
83
  } else {
@@ -82,11 +85,16 @@ async function cmdInit() {
82
85
  }
83
86
  console.log();
84
87
  console.log(dim("Created .nit/ with initial commit on main."));
88
+ console.log();
89
+ console.log(`Next: open ${bold("agent-card.json")} and set your name, description, and skills.`);
85
90
  }
86
91
  async function cmdStatus() {
87
92
  const s = await status();
88
93
  console.log(`On branch ${bold(s.branch)}`);
89
- console.log(`Public key: ${dim(s.publicKey)}`);
94
+ console.log();
95
+ console.log(` Agent ID: ${green(s.agentId)}`);
96
+ console.log(` Public key: ${dim(s.publicKey)}`);
97
+ console.log(` Card URL: ${s.cardUrl}`);
90
98
  console.log();
91
99
  if (s.uncommittedChanges) {
92
100
  console.log(yellow("Uncommitted changes:"));
@@ -177,23 +185,32 @@ async function cmdPush(args) {
177
185
  }
178
186
  }
179
187
  async function cmdRemote(args) {
180
- const subcommand = args[0];
181
- if (subcommand === "set-credential") {
182
- const token = args[1];
183
- if (!token) {
184
- console.error("Usage: nit remote set-credential <agent-key>");
188
+ const info = await remote();
189
+ console.log(`${bold(info.name)}`);
190
+ console.log(` URL: ${info.url}`);
191
+ console.log(` Agent ID: ${info.agentId}`);
192
+ console.log(` Auth: ${green("Ed25519 keypair")}`);
193
+ }
194
+ async function cmdSign(args) {
195
+ const loginIndex = args.indexOf("--login");
196
+ if (loginIndex !== -1) {
197
+ const domain = args[loginIndex + 1];
198
+ if (!domain) {
199
+ console.error("Usage: nit sign --login <domain>");
185
200
  process.exit(1);
186
201
  }
187
- await setCredential(token);
188
- console.log(`Credential ${green("configured")} for origin.`);
202
+ const payload = await loginPayload(domain);
203
+ console.log(JSON.stringify(payload, null, 2));
189
204
  return;
190
205
  }
191
- const info = await remote();
192
- console.log(`${bold(info.name)}`);
193
- console.log(` URL: ${info.url}`);
194
- console.log(
195
- ` Credential: ${info.hasCredential ? green("configured") : yellow("not set")}`
196
- );
206
+ const message = args[0];
207
+ if (!message) {
208
+ console.error('Usage: nit sign "message"');
209
+ console.error(" nit sign --login <domain>");
210
+ process.exit(1);
211
+ }
212
+ const signature = await sign(message);
213
+ console.log(signature);
197
214
  }
198
215
  function printUsage() {
199
216
  console.log(`
@@ -203,21 +220,22 @@ ${bold("Usage:")} nit <command> [options]
203
220
 
204
221
  ${bold("Commands:")}
205
222
  init Initialize .nit/ in current directory
206
- status Show current branch and uncommitted changes
223
+ status Show identity, branch, and uncommitted changes
207
224
  commit -m "msg" Snapshot agent-card.json
208
225
  log Show commit history
209
226
  diff [target] Compare card vs HEAD, branch, or commit
210
227
  branch [name] List branches or create a new one
211
228
  checkout <branch> Switch branch (overwrites agent-card.json)
212
229
  push [--all] Push branch(es) to remote
230
+ sign "message" Sign a message with your Ed25519 key
231
+ sign --login <dom> Generate login payload for an app
213
232
  remote Show remote info
214
- remote set-credential <key> Set push credential (agent key)
215
233
 
216
234
  ${bold("Examples:")}
217
235
  nit init
218
236
  nit branch faam.io
219
237
  nit checkout faam.io
220
- ${dim("# edit agent-card.json for FAAM...")}
238
+ ${dim("# edit agent-card.json for this platform...")}
221
239
  nit commit -m "FAAM config"
222
240
  nit push --all
223
241
  `.trim());
package/dist/index.d.ts CHANGED
@@ -27,7 +27,7 @@ interface NitHead {
27
27
  }
28
28
  /** Remote configuration for a single named remote. */
29
29
  interface NitRemoteConfig {
30
- /** Push credential (Bearer token) */
30
+ /** Legacy field push auth is now via Ed25519 keypair */
31
31
  credential?: string;
32
32
  }
33
33
  /** Full .nit/config file contents. */
@@ -101,6 +101,8 @@ interface PushResult {
101
101
  }
102
102
  /** Result returned by the status command. */
103
103
  interface StatusResult {
104
+ agentId: string;
105
+ cardUrl: string;
104
106
  branch: string;
105
107
  publicKey: string;
106
108
  uncommittedChanges: DiffResult | null;
@@ -110,6 +112,13 @@ interface StatusResult {
110
112
  behind: number;
111
113
  }>;
112
114
  }
115
+ /** Result of generating a login payload for app authentication. */
116
+ interface LoginPayload {
117
+ agent_id: string;
118
+ domain: string;
119
+ timestamp: number;
120
+ signature: string;
121
+ }
113
122
 
114
123
  /**
115
124
  * Compare two agent cards and return a structured diff.
@@ -140,15 +149,38 @@ declare function signChallenge(nitDir: string, challenge: string): Promise<strin
140
149
  * for challenge-response flows.
141
150
  */
142
151
  declare function verifySignature(pubBase64: string, challenge: string, signatureBase64: string): boolean;
152
+ /**
153
+ * Sign an arbitrary message with the agent's private key.
154
+ * Returns a standard base64-encoded signature.
155
+ */
156
+ declare function signMessage(nitDir: string, message: string): Promise<string>;
157
+ /**
158
+ * Fixed namespace UUID for nit agent ID derivation.
159
+ * Generated once, hardcoded forever. Changing this would change ALL agent IDs.
160
+ * Must match the server-side constant in apps/agent-cards/src/api/agent-id.ts.
161
+ */
162
+ declare const NIT_NAMESPACE = "801ba518-f326-47e5-97c9-d1efd1865a19";
163
+ /**
164
+ * Derive a deterministic agent ID (UUID) from an Ed25519 public key field.
165
+ * Uses UUIDv5: SHA-1 hash of NIT_NAMESPACE + publicKeyField.
166
+ *
167
+ * @param publicKeyField "ed25519:<base64>" format string
168
+ * @returns UUID string (lowercase, with hyphens)
169
+ */
170
+ declare function deriveAgentId(publicKeyField: string): string;
171
+ /**
172
+ * Load the agent ID from .nit/identity/agent-id.
173
+ */
174
+ declare function loadAgentId(nitDir: string): Promise<string>;
143
175
 
144
176
  /**
145
177
  * Fetch an agent card from a remote URL.
146
178
  *
147
179
  * For the main branch, this is a simple public GET.
148
180
  * For other branches, this performs the challenge-response flow:
149
- * 1. Request branch 401 with challenge
181
+ * 1. Request branch -> 401 with challenge
150
182
  * 2. Sign challenge with agent's private key
151
- * 3. Re-request with signature get branch card
183
+ * 3. Re-request with signature -> get branch card
152
184
  *
153
185
  * @param cardUrl The agent's card URL (e.g. https://agent-{uuid}.newtype-ai.org)
154
186
  * @param branch Branch to fetch ("main" for public, others need auth)
@@ -162,8 +194,9 @@ declare function fetchBranchCard(cardUrl: string, branch: string, nitDir?: strin
162
194
  */
163
195
  declare function findNitDir(startDir?: string): string;
164
196
  interface InitResult {
197
+ agentId: string;
165
198
  publicKey: string;
166
- cardUrl: string | null;
199
+ cardUrl: string;
167
200
  skillsFound: string[];
168
201
  }
169
202
  /**
@@ -183,6 +216,21 @@ declare function init(options?: {
183
216
  declare function status(options?: {
184
217
  projectDir?: string;
185
218
  }): Promise<StatusResult>;
219
+ /**
220
+ * Sign an arbitrary message with the agent's Ed25519 private key.
221
+ * Returns a base64-encoded signature.
222
+ */
223
+ declare function sign(message: string, options?: {
224
+ projectDir?: string;
225
+ }): Promise<string>;
226
+ /**
227
+ * Generate a login payload for app authentication.
228
+ * Constructs the canonical message ({agent_id}\n{domain}\n{timestamp}),
229
+ * signs it, and returns the full payload ready to send to an app.
230
+ */
231
+ declare function loginPayload(domain: string, options?: {
232
+ projectDir?: string;
233
+ }): Promise<LoginPayload>;
186
234
  /**
187
235
  * Snapshot the current agent-card.json as a new commit.
188
236
  * Resolves skill pointers from SKILL.md before committing.
@@ -231,20 +279,13 @@ declare function push(options?: {
231
279
  interface RemoteInfo {
232
280
  name: string;
233
281
  url: string;
234
- hasCredential: boolean;
282
+ agentId: string;
235
283
  }
236
284
  /**
237
- * Show remote info. URL comes from agent-card.json, credential from .nit/config.
285
+ * Show remote info. URL comes from agent-card.json, agent ID from .nit/identity/.
238
286
  */
239
287
  declare function remote(options?: {
240
288
  projectDir?: string;
241
289
  }): Promise<RemoteInfo>;
242
- /**
243
- * Set the push credential for a remote.
244
- */
245
- declare function setCredential(credential: string, options?: {
246
- projectDir?: string;
247
- remoteName?: string;
248
- }): Promise<void>;
249
290
 
250
- export { type AgentCard, type AgentCardSkill, type DiffResult, type FieldDiff, type InitResult, type NitBranch, type NitCommit, type NitConfig, type NitHead, type NitRemoteConfig, type PushResult, type RemoteInfo, type SkillMetadata, type StatusResult, branch, checkout, commit, diff, diffCards, fetchBranchCard, findNitDir, formatDiff, formatPublicKeyField, init, log, parsePublicKeyField, push, remote, setCredential, signChallenge, status, verifySignature };
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 };
package/dist/index.js CHANGED
@@ -1,8 +1,10 @@
1
1
  // nit — version control for agent cards
2
2
  import {
3
+ NIT_NAMESPACE,
3
4
  branch,
4
5
  checkout,
5
6
  commit,
7
+ deriveAgentId,
6
8
  diff,
7
9
  diffCards,
8
10
  fetchBranchCard,
@@ -10,19 +12,24 @@ import {
10
12
  formatDiff,
11
13
  formatPublicKeyField,
12
14
  init,
15
+ loadAgentId,
13
16
  log,
17
+ loginPayload,
14
18
  parsePublicKeyField,
15
19
  push,
16
20
  remote,
17
- setCredential,
21
+ sign,
18
22
  signChallenge,
23
+ signMessage,
19
24
  status,
20
25
  verifySignature
21
- } from "./chunk-5AVY6P7B.js";
26
+ } from "./chunk-AEWDQDBM.js";
22
27
  export {
28
+ NIT_NAMESPACE,
23
29
  branch,
24
30
  checkout,
25
31
  commit,
32
+ deriveAgentId,
26
33
  diff,
27
34
  diffCards,
28
35
  fetchBranchCard,
@@ -30,12 +37,15 @@ export {
30
37
  formatDiff,
31
38
  formatPublicKeyField,
32
39
  init,
40
+ loadAgentId,
33
41
  log,
42
+ loginPayload,
34
43
  parsePublicKeyField,
35
44
  push,
36
45
  remote,
37
- setCredential,
46
+ sign,
38
47
  signChallenge,
48
+ signMessage,
39
49
  status,
40
50
  verifySignature
41
51
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@newtype-ai/nit",
3
- "version": "0.1.2",
3
+ "version": "0.2.1",
4
4
  "description": "Version control for agent cards",
5
5
  "type": "module",
6
6
  "bin": {