@newtype-ai/nit 0.2.3 → 0.2.5

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
@@ -57,7 +57,7 @@ nit push --all
57
57
  | `nit checkout <branch>` | Switch branch (overwrites agent-card.json) |
58
58
  | `nit push [--all]` | Push branch(es) to remote |
59
59
  | `nit sign "msg"` | Sign a message with your Ed25519 key |
60
- | `nit sign --login <domain>` | Generate login payload for an app |
60
+ | `nit sign --login <domain>` | Auto-switch to domain branch + generate login payload |
61
61
  | `nit remote` | Show remote URL and credential status |
62
62
  | `nit remote add <name> <url>` | Add a new remote |
63
63
  | `nit remote set-url <name> <url>` | Change a remote's URL |
@@ -84,7 +84,9 @@ Each branch is a different agent card for a different platform. Branch name = ro
84
84
 
85
85
  ### Skill Resolution
86
86
 
87
- At commit time, nit discovers SKILL.md files from all major agent frameworks:
87
+ Your card can store skills as **pointers** — just `{ "id": "skill-name" }` — resolved from SKILL.md files at commit time. SKILL.md is the single source of truth when present.
88
+
89
+ nit auto-discovers your skills directory from all major agent frameworks:
88
90
 
89
91
  - `.claude/skills/` — Claude Code
90
92
  - `.cursor/skills/` — Cursor
@@ -92,7 +94,7 @@ At commit time, nit discovers SKILL.md files from all major agent frameworks:
92
94
  - `.codex/skills/` — OpenAI Codex
93
95
  - `.agents/skills/` — Generic
94
96
 
95
- Skills referenced in your card are resolved against these files, and the committed card contains a self-contained snapshot.
97
+ The discovered path is stored in `.nit/config`. When `nit sign --login <domain>` creates a new branch, it auto-creates a SKILL.md template and adds a pointer to the card. The committed card always contains fully resolved, self-contained skill data.
96
98
 
97
99
  ### Remote Protocol
98
100
 
@@ -116,10 +118,11 @@ nit remote set-url origin https://my-server.com
116
118
  your-project/
117
119
  ├── .nit/ # nit repository (gitignored)
118
120
  │ ├── HEAD # Current branch ref
119
- │ ├── config # Remote URL and credentials
121
+ │ ├── config # Remote URL, credentials, skills directory
120
122
  │ ├── identity/
121
123
  │ │ ├── agent.pub # Ed25519 public key
122
- │ │ └── agent.key # Ed25519 private key (0600)
124
+ │ │ ├── agent.key # Ed25519 private key (0600)
125
+ │ │ └── agent-id # UUIDv5 derived from public key
123
126
  │ ├── objects/ # Content-addressable store
124
127
  │ └── refs/heads/ # Branch pointers
125
128
  ├── agent-card.json # Working copy (changes with checkout)
@@ -129,12 +132,15 @@ your-project/
129
132
  ## Programmatic API
130
133
 
131
134
  ```typescript
132
- import { init, commit, checkout, branch, push, status } from '@newtype-ai/nit';
135
+ import { init, commit, checkout, branch, push, status, sign, loginPayload } from '@newtype-ai/nit';
133
136
 
134
137
  await init();
135
- await branch('faam.io');
136
- await checkout('faam.io');
137
- // modify agent-card.json...
138
+
139
+ // Log into an app (auto-creates and switches to domain branch)
140
+ const payload = await loginPayload('faam.io');
141
+ // → { agent_id, domain, timestamp, signature, switchedBranch, createdSkill }
142
+
143
+ // Customize card, then commit & push
138
144
  await commit('FAAM config');
139
145
  await push({ all: true });
140
146
  ```
@@ -2,7 +2,7 @@
2
2
 
3
3
  // src/index.ts
4
4
  import { promises as fs6, statSync } from "fs";
5
- import { join as join6, basename as basename2, resolve } from "path";
5
+ import { join as join6, basename as basename2, resolve as resolve2 } from "path";
6
6
 
7
7
  // src/objects.ts
8
8
  import { createHash } from "crypto";
@@ -312,8 +312,70 @@ function uuidv5(name, namespace) {
312
312
 
313
313
  // src/skills.ts
314
314
  import { promises as fs4 } from "fs";
315
- import { join as join4 } from "path";
315
+ import { join as join4, resolve } from "path";
316
316
  import { homedir } from "os";
317
+ var FRAMEWORK_MARKERS = [
318
+ { marker: ".claude", skillsPath: ".claude/skills" },
319
+ { marker: ".cursor", skillsPath: ".cursor/skills" },
320
+ { marker: ".codex", skillsPath: ".codex/skills" },
321
+ { marker: ".windsurf", skillsPath: ".windsurf/skills" },
322
+ { marker: ".openclaw", skillsPath: ".agents/skills" }
323
+ ];
324
+ var GLOBAL_SKILLS_DIRS = [
325
+ { marker: ".claude", skillsPath: ".claude/skills" },
326
+ { marker: ".codex", skillsPath: ".codex/skills" },
327
+ { marker: ".codeium", skillsPath: ".codeium/windsurf/skills" }
328
+ ];
329
+ async function discoverSkillsDir(projectDir2) {
330
+ const absProject = resolve(projectDir2);
331
+ const home = homedir();
332
+ for (const { marker, skillsPath } of FRAMEWORK_MARKERS) {
333
+ if (absProject.includes(`/${marker}/`) || absProject.includes(`/${marker}`)) {
334
+ return join4(absProject, skillsPath);
335
+ }
336
+ }
337
+ for (const { marker, skillsPath } of FRAMEWORK_MARKERS) {
338
+ try {
339
+ const stat = await fs4.stat(join4(absProject, marker));
340
+ if (stat.isDirectory()) {
341
+ return join4(absProject, skillsPath);
342
+ }
343
+ } catch {
344
+ }
345
+ }
346
+ for (const { marker, skillsPath } of GLOBAL_SKILLS_DIRS) {
347
+ try {
348
+ const stat = await fs4.stat(join4(home, marker));
349
+ if (stat.isDirectory()) {
350
+ return join4(home, skillsPath);
351
+ }
352
+ } catch {
353
+ }
354
+ }
355
+ return join4(absProject, ".agents", "skills");
356
+ }
357
+ async function createSkillTemplate(skillsDir, domain) {
358
+ const skillId = domain.replace(/\./g, "-");
359
+ const skillDir = join4(skillsDir, skillId);
360
+ const skillPath = join4(skillDir, "SKILL.md");
361
+ try {
362
+ await fs4.access(skillPath);
363
+ return skillId;
364
+ } catch {
365
+ }
366
+ await fs4.mkdir(skillDir, { recursive: true });
367
+ const template = `---
368
+ name: ${skillId}
369
+ description: Skills and context for ${domain}
370
+ ---
371
+
372
+ # ${skillId}
373
+
374
+ Configure your skills and context for ${domain} here.
375
+ `;
376
+ await fs4.writeFile(skillPath, template, "utf-8");
377
+ return skillId;
378
+ }
317
379
  async function discoverSkills(projectDir2) {
318
380
  const home = homedir();
319
381
  const searchDirs = [
@@ -624,33 +686,49 @@ async function getRemoteUrl(nitDir, remoteName) {
624
686
  const config = await readConfig(nitDir);
625
687
  return config.remotes[remoteName]?.url ?? null;
626
688
  }
689
+ async function getSkillsDir(nitDir) {
690
+ const config = await readConfig(nitDir);
691
+ return config.skillsDir ?? null;
692
+ }
627
693
  function parseConfig(raw) {
628
694
  const remotes = {};
695
+ let currentSection = null;
629
696
  let currentRemote = null;
697
+ let skillsDir;
630
698
  for (const line of raw.split("\n")) {
631
699
  const trimmed = line.trim();
632
700
  if (trimmed === "" || trimmed.startsWith("#")) continue;
633
- const sectionMatch = trimmed.match(/^\[remote\s+"([^"]+)"\]$/);
634
- if (sectionMatch) {
635
- currentRemote = sectionMatch[1];
701
+ const remoteMatch = trimmed.match(/^\[remote\s+"([^"]+)"\]$/);
702
+ if (remoteMatch) {
703
+ currentSection = "remote";
704
+ currentRemote = remoteMatch[1];
636
705
  if (!remotes[currentRemote]) {
637
706
  remotes[currentRemote] = {};
638
707
  }
639
708
  continue;
640
709
  }
641
- if (currentRemote !== null) {
642
- const kvMatch = trimmed.match(/^(\w+)\s*=\s*(.+)$/);
643
- if (kvMatch) {
644
- const [, key, value] = kvMatch;
710
+ if (trimmed === "[skills]") {
711
+ currentSection = "skills";
712
+ currentRemote = null;
713
+ continue;
714
+ }
715
+ const kvMatch = trimmed.match(/^(\w+)\s*=\s*(.+)$/);
716
+ if (kvMatch) {
717
+ const [, key, value] = kvMatch;
718
+ if (currentSection === "remote" && currentRemote !== null) {
645
719
  if (key === "url") {
646
720
  remotes[currentRemote].url = value.trim();
647
721
  } else if (key === "credential") {
648
722
  remotes[currentRemote].credential = value.trim();
649
723
  }
724
+ } else if (currentSection === "skills") {
725
+ if (key === "dir") {
726
+ skillsDir = value.trim();
727
+ }
650
728
  }
651
729
  }
652
730
  }
653
- return { remotes };
731
+ return { remotes, skillsDir };
654
732
  }
655
733
  function serializeConfig(config) {
656
734
  const lines = [];
@@ -664,6 +742,11 @@ function serializeConfig(config) {
664
742
  }
665
743
  lines.push("");
666
744
  }
745
+ if (config.skillsDir) {
746
+ lines.push("[skills]");
747
+ lines.push(` dir = ${config.skillsDir}`);
748
+ lines.push("");
749
+ }
667
750
  return lines.join("\n");
668
751
  }
669
752
 
@@ -672,7 +755,7 @@ var NIT_DIR = ".nit";
672
755
  var CARD_FILE = "agent-card.json";
673
756
  var DEFAULT_API_BASE = "https://api.newtype-ai.org";
674
757
  function findNitDir(startDir) {
675
- let dir = resolve(startDir || process.cwd());
758
+ let dir = resolve2(startDir || process.cwd());
676
759
  while (true) {
677
760
  const candidate = join6(dir, NIT_DIR);
678
761
  try {
@@ -680,7 +763,7 @@ function findNitDir(startDir) {
680
763
  if (s.isDirectory()) return candidate;
681
764
  } catch {
682
765
  }
683
- const parent = resolve(dir, "..");
766
+ const parent = resolve2(dir, "..");
684
767
  if (parent === dir) {
685
768
  throw new Error(
686
769
  "Not a nit repository (or any parent directory). Run `nit init` first."
@@ -690,7 +773,7 @@ function findNitDir(startDir) {
690
773
  }
691
774
  }
692
775
  function projectDir(nitDir) {
693
- return resolve(nitDir, "..");
776
+ return resolve2(nitDir, "..");
694
777
  }
695
778
  async function readWorkingCard(nitDir) {
696
779
  const cardPath = join6(projectDir(nitDir), CARD_FILE);
@@ -720,7 +803,7 @@ async function getAuthorName(nitDir) {
720
803
  }
721
804
  }
722
805
  async function init(options) {
723
- const projDir = resolve(options?.projectDir || process.cwd());
806
+ const projDir = resolve2(options?.projectDir || process.cwd());
724
807
  const nitDir = join6(projDir, NIT_DIR);
725
808
  try {
726
809
  await fs6.access(nitDir);
@@ -781,14 +864,17 @@ async function init(options) {
781
864
  await setBranch(nitDir, "main", commitHash);
782
865
  await setHead(nitDir, "main");
783
866
  await fs6.writeFile(join6(nitDir, "logs", "HEAD"), "", "utf-8");
867
+ const skillsDir = await discoverSkillsDir(projDir);
784
868
  await writeConfig(nitDir, {
785
- remotes: { origin: { url: DEFAULT_API_BASE } }
869
+ remotes: { origin: { url: DEFAULT_API_BASE } },
870
+ skillsDir
786
871
  });
787
872
  return {
788
873
  agentId,
789
874
  publicKey: publicKeyField,
790
875
  cardUrl: card.url,
791
- skillsFound
876
+ skillsFound,
877
+ skillsDir
792
878
  };
793
879
  }
794
880
  async function status(options) {
@@ -858,15 +944,29 @@ async function sign2(message, options) {
858
944
  async function loginPayload(domain, options) {
859
945
  const nitDir = findNitDir(options?.projectDir);
860
946
  let switchedBranch;
947
+ let createdSkill;
861
948
  const currentBranch = await getCurrentBranch(nitDir);
862
949
  if (currentBranch !== domain) {
863
- const existing = await getBranch(nitDir, domain);
864
- if (!existing) {
950
+ const isNew = !await getBranch(nitDir, domain);
951
+ if (isNew) {
865
952
  const headHash = await resolveHead(nitDir);
866
953
  await setBranch(nitDir, domain, headHash);
867
954
  }
868
955
  await checkout(domain, options);
869
956
  switchedBranch = domain;
957
+ if (isNew) {
958
+ const skillsDir = await getSkillsDir(nitDir);
959
+ if (skillsDir) {
960
+ const skillId = await createSkillTemplate(skillsDir, domain);
961
+ const card = await readWorkingCard(nitDir);
962
+ const hasSkill = card.skills.some((s) => s.id === skillId);
963
+ if (!hasSkill) {
964
+ card.skills.push({ id: skillId });
965
+ await writeWorkingCard(nitDir, card);
966
+ createdSkill = skillId;
967
+ }
968
+ }
969
+ }
870
970
  }
871
971
  const agentId = await loadAgentId(nitDir);
872
972
  const timestamp = Math.floor(Date.now() / 1e3);
@@ -874,7 +974,7 @@ async function loginPayload(domain, options) {
874
974
  ${domain}
875
975
  ${timestamp}`;
876
976
  const signature = await signMessage(nitDir, message);
877
- return { agent_id: agentId, domain, timestamp, signature, switchedBranch };
977
+ return { agent_id: agentId, domain, timestamp, signature, switchedBranch, createdSkill };
878
978
  }
879
979
  async function commit(message, options) {
880
980
  const nitDir = findNitDir(options?.projectDir);
package/dist/cli.js CHANGED
@@ -15,7 +15,7 @@ import {
15
15
  remoteSetUrl,
16
16
  sign,
17
17
  status
18
- } from "./chunk-2ZHTOY2J.js";
18
+ } from "./chunk-U6PEH4IJ.js";
19
19
 
20
20
  // src/cli.ts
21
21
  var bold = (s) => `\x1B[1m${s}\x1B[0m`;
@@ -80,6 +80,7 @@ async function cmdInit() {
80
80
  console.log(` Agent ID: ${green(result.agentId)}`);
81
81
  console.log(` Public key: ${dim(result.publicKey)}`);
82
82
  console.log(` Card URL: ${result.cardUrl}`);
83
+ console.log(` Skills dir: ${dim(result.skillsDir)}`);
83
84
  if (result.skillsFound.length > 0) {
84
85
  console.log(` Skills: ${result.skillsFound.join(", ")}`);
85
86
  } else {
@@ -233,7 +234,10 @@ async function cmdSign(args) {
233
234
  if (payload.switchedBranch) {
234
235
  console.error(`Switched to branch '${payload.switchedBranch}'`);
235
236
  }
236
- const { switchedBranch: _, ...output } = payload;
237
+ if (payload.createdSkill) {
238
+ console.error(`Created skill template '${payload.createdSkill}'`);
239
+ }
240
+ const { switchedBranch: _s, createdSkill: _c, ...output } = payload;
237
241
  console.log(JSON.stringify(output, null, 2));
238
242
  return;
239
243
  }
package/dist/index.d.ts CHANGED
@@ -36,6 +36,8 @@ interface NitRemoteConfig {
36
36
  interface NitConfig {
37
37
  /** Keyed by remote name (e.g. "origin") */
38
38
  remotes: Record<string, NitRemoteConfig>;
39
+ /** Discovered skills directory path */
40
+ skillsDir?: string;
39
41
  }
40
42
  /** A2A-compatible agent card. */
41
43
  interface AgentCard {
@@ -56,11 +58,13 @@ interface AgentCard {
56
58
  url?: string;
57
59
  };
58
60
  }
59
- /** A single skill entry in an agent card. */
61
+ /** A single skill entry in an agent card.
62
+ * Can be a full skill (all fields) or a pointer (just id).
63
+ * At commit time, pointers are resolved from SKILL.md files. */
60
64
  interface AgentCardSkill {
61
65
  id: string;
62
- name: string;
63
- description: string;
66
+ name?: string;
67
+ description?: string;
64
68
  tags?: string[];
65
69
  examples?: string[];
66
70
  inputModes?: string[];
@@ -200,6 +204,7 @@ interface InitResult {
200
204
  publicKey: string;
201
205
  cardUrl: string;
202
206
  skillsFound: string[];
207
+ skillsDir: string;
203
208
  }
204
209
  /**
205
210
  * Initialize a new nit repository in the project directory.
@@ -235,6 +240,7 @@ declare function loginPayload(domain: string, options?: {
235
240
  projectDir?: string;
236
241
  }): Promise<LoginPayload & {
237
242
  switchedBranch?: string;
243
+ createdSkill?: string;
238
244
  }>;
239
245
  /**
240
246
  * Snapshot the current agent-card.json as a new commit.
package/dist/index.js CHANGED
@@ -25,7 +25,7 @@ import {
25
25
  signMessage,
26
26
  status,
27
27
  verifySignature
28
- } from "./chunk-2ZHTOY2J.js";
28
+ } from "./chunk-U6PEH4IJ.js";
29
29
  export {
30
30
  NIT_NAMESPACE,
31
31
  branch,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@newtype-ai/nit",
3
- "version": "0.2.3",
3
+ "version": "0.2.5",
4
4
  "description": "Version control for agent cards",
5
5
  "type": "module",
6
6
  "bin": {