mcp-context-card 0.5.0
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/.well-known/ai-catalog.json +39 -0
- package/.well-known/fafa +18 -0
- package/AGENTS.md +85 -0
- package/CHANGELOG.md +75 -0
- package/LICENSE +21 -0
- package/README.md +192 -0
- package/dist/agents-md.d.ts +22 -0
- package/dist/agents-md.js +65 -0
- package/dist/author.d.ts +11 -0
- package/dist/author.js +27 -0
- package/dist/bin.d.ts +21 -0
- package/dist/bin.js +70 -0
- package/dist/card-gen.d.ts +1 -0
- package/dist/card-gen.js +14 -0
- package/dist/catalog-gen.d.ts +26 -0
- package/dist/catalog-gen.js +72 -0
- package/dist/constants.d.ts +5 -0
- package/dist/constants.js +5 -0
- package/dist/faf/parse-fafa.d.ts +2 -0
- package/dist/faf/parse-fafa.js +32 -0
- package/dist/faf/parse-fafm.d.ts +7 -0
- package/dist/faf/parse-fafm.js +119 -0
- package/dist/faf/types.d.ts +34 -0
- package/dist/faf/types.js +7 -0
- package/dist/identity.d.ts +35 -0
- package/dist/identity.js +82 -0
- package/dist/md.d.ts +22 -0
- package/dist/md.js +186 -0
- package/dist/memory.d.ts +13 -0
- package/dist/memory.js +12 -0
- package/dist/render-card.d.ts +10 -0
- package/dist/render-card.js +175 -0
- package/dist/server.d.ts +56 -0
- package/dist/server.js +250 -0
- package/dist/transport/http.d.ts +2 -0
- package/dist/transport/http.js +74 -0
- package/docs/MECHANISMS.md +137 -0
- package/docs/TRANSPORT.md +86 -0
- package/docs/WIRING.md +97 -0
- package/docs/card.html +121 -0
- package/docs/img/card.png +0 -0
- package/package.json +77 -0
- package/project.faf +28 -0
- package/project.fafm +46 -0
- package/server.json +49 -0
package/dist/card-gen.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* card-gen - write docs/card.html from the three sources, so the card is
|
|
3
|
+
* browsable on GitHub and screenshot-able for the README. Same renderer as
|
|
4
|
+
* GET /card and the render_context_card tool.
|
|
5
|
+
*/
|
|
6
|
+
import { writeFileSync } from "node:fs";
|
|
7
|
+
import { dirname, join } from "node:path";
|
|
8
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
9
|
+
import { renderCard } from "./render-card.js";
|
|
10
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
11
|
+
const root = join(dirname(fileURLToPath(import.meta.url)), "..");
|
|
12
|
+
writeFileSync(join(root, "docs/card.html"), renderCard(root));
|
|
13
|
+
console.log("wrote docs/card.html — the context card, rendered from AGENTS.md / project.fafm / .well-known/fafa");
|
|
14
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export declare function buildCatalog(root: string): {
|
|
2
|
+
specVersion: string;
|
|
3
|
+
host: {
|
|
4
|
+
displayName: string;
|
|
5
|
+
identifier: string;
|
|
6
|
+
};
|
|
7
|
+
entries: ({
|
|
8
|
+
identifier: string;
|
|
9
|
+
displayName: string;
|
|
10
|
+
type: string;
|
|
11
|
+
mediaType: string;
|
|
12
|
+
description: string;
|
|
13
|
+
url: string;
|
|
14
|
+
_meta?: undefined;
|
|
15
|
+
} | {
|
|
16
|
+
identifier: string;
|
|
17
|
+
displayName: string;
|
|
18
|
+
type: string;
|
|
19
|
+
mediaType: string;
|
|
20
|
+
description: string;
|
|
21
|
+
url: string;
|
|
22
|
+
_meta: {
|
|
23
|
+
"io.github.wolfe-jam.mcp-context-card/iana": string;
|
|
24
|
+
};
|
|
25
|
+
})[];
|
|
26
|
+
};
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* catalog-gen — write `.well-known/ai-catalog.json` FROM the three sources
|
|
3
|
+
* (`AGENTS.md`, `project.fafm`, `.well-known/fafa`) that also back the Server
|
|
4
|
+
* Card `_meta` block. Same three artifacts, second exposure mechanism.
|
|
5
|
+
*
|
|
6
|
+
* Descriptions are derived from real file content (section count, fact count,
|
|
7
|
+
* the agent's own description) — not hand-written blurbs that drift. The CI
|
|
8
|
+
* job `catalog:check` regenerates this and fails on any diff.
|
|
9
|
+
*/
|
|
10
|
+
import { writeFileSync } from "node:fs";
|
|
11
|
+
import { dirname, join } from "node:path";
|
|
12
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
13
|
+
import { parseAgentsMd } from "./agents-md.js";
|
|
14
|
+
import { parseFafm } from "./faf/parse-fafm.js";
|
|
15
|
+
import { parseFafa } from "./faf/parse-fafa.js";
|
|
16
|
+
import { META_NS } from "./identity.js";
|
|
17
|
+
const iana = (t) => `https://www.iana.org/assignments/media-types/${t}`;
|
|
18
|
+
export function buildCatalog(root) {
|
|
19
|
+
const agents = parseAgentsMd(join(root, "AGENTS.md"));
|
|
20
|
+
const fafm = parseFafm(join(root, "project.fafm"));
|
|
21
|
+
const fafa = parseFafa(join(root, ".well-known/fafa"));
|
|
22
|
+
const host = fafa?.name ?? "mcp-context-card";
|
|
23
|
+
return {
|
|
24
|
+
specVersion: "1.0",
|
|
25
|
+
host: {
|
|
26
|
+
displayName: host,
|
|
27
|
+
identifier: "https://github.com/Wolfe-Jam/mcp-context-card",
|
|
28
|
+
},
|
|
29
|
+
entries: [
|
|
30
|
+
{
|
|
31
|
+
identifier: `urn:air:${host}:context`,
|
|
32
|
+
displayName: `${host} — project context (AGENTS.md)`,
|
|
33
|
+
type: "text/markdown",
|
|
34
|
+
mediaType: "text/markdown",
|
|
35
|
+
description: agents
|
|
36
|
+
? (() => {
|
|
37
|
+
const h = agents.sections.filter((s) => s.level > 1).map((s) => s.heading);
|
|
38
|
+
return `Agent instructions for this project — ${h.length} section(s): ${h
|
|
39
|
+
.slice(0, 6)
|
|
40
|
+
.join(", ")}${h.length > 6 ? ", …" : ""}.`;
|
|
41
|
+
})()
|
|
42
|
+
: "Agent instructions for this project (AGENTS.md — not present).",
|
|
43
|
+
url: "./AGENTS.md",
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
identifier: `urn:air:${host}:memory`,
|
|
47
|
+
displayName: `${host} — persistent memory (.fafm)`,
|
|
48
|
+
type: "application/vnd.fafm+yaml",
|
|
49
|
+
mediaType: "application/vnd.fafm+yaml",
|
|
50
|
+
description: `Cross-session memory — ${fafm.facts.length} fact(s), profile "${fafm.profile ?? "?"}". Recall survives a process restart. No de-facto standard for this concern yet.`,
|
|
51
|
+
url: "./project.fafm",
|
|
52
|
+
_meta: { [`${META_NS}/iana`]: iana("application/vnd.fafm+yaml") },
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
identifier: `urn:air:${host}:identity`,
|
|
56
|
+
displayName: `${host} — agent identity (.fafa)`,
|
|
57
|
+
type: "application/vnd.fafa+yaml",
|
|
58
|
+
mediaType: "application/vnd.fafa+yaml",
|
|
59
|
+
description: fafa?.description ??
|
|
60
|
+
`Agent identity card (status: ${fafa?.status ?? "unknown"}).`,
|
|
61
|
+
url: "./.well-known/fafa",
|
|
62
|
+
_meta: { [`${META_NS}/iana`]: iana("application/vnd.fafa+yaml") },
|
|
63
|
+
},
|
|
64
|
+
],
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
// Direct run → write the file.
|
|
68
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
69
|
+
const root = join(dirname(fileURLToPath(import.meta.url)), "..");
|
|
70
|
+
writeFileSync(join(root, ".well-known/ai-catalog.json"), JSON.stringify(buildCatalog(root), null, 2) + "\n");
|
|
71
|
+
console.log("wrote .well-known/ai-catalog.json — 3 sibling entries, derived from AGENTS.md / project.fafm / .well-known/fafa");
|
|
72
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/** Server identity constants, in their own module so any file can import
|
|
2
|
+
* them without pulling in the whole server. */
|
|
3
|
+
export declare const NAME = "mcp-context-card";
|
|
4
|
+
export declare const VERSION = "0.5.0";
|
|
5
|
+
export declare const SERVER_CARD_URI = "mcp-context-card://server-card";
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* parse-fafa — read a `.fafa` agent identity card into a typed shape.
|
|
3
|
+
* `application/vnd.fafa+yaml` — yaml, parsed as yaml.
|
|
4
|
+
*/
|
|
5
|
+
import { readFileSync } from "node:fs";
|
|
6
|
+
import { parse } from "yaml";
|
|
7
|
+
export function parseFafa(path) {
|
|
8
|
+
let doc;
|
|
9
|
+
try {
|
|
10
|
+
doc = parse(readFileSync(path, "utf8")) ?? {};
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
const agent = doc.agent ?? {};
|
|
16
|
+
return {
|
|
17
|
+
version: str(doc.version),
|
|
18
|
+
name: str(agent.name),
|
|
19
|
+
displayName: str(agent.displayName),
|
|
20
|
+
vendor: str(agent.vendor),
|
|
21
|
+
agentVersion: str(agent.version),
|
|
22
|
+
description: str(agent.description),
|
|
23
|
+
status: str(agent.status),
|
|
24
|
+
license: str(agent.license),
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
function str(v) {
|
|
28
|
+
if (v === undefined || v === null)
|
|
29
|
+
return undefined;
|
|
30
|
+
const s = String(v).trim().replace(/\s+/g, " ");
|
|
31
|
+
return s.length ? s : undefined;
|
|
32
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { Memory, MemoryFact } from "./types.js";
|
|
2
|
+
export declare function parseFafm(path: string): Memory;
|
|
3
|
+
export declare function recall(path: string, id: string): MemoryFact | null;
|
|
4
|
+
/** Add a fact, or update its text in place if the id already exists. */
|
|
5
|
+
export declare function remember(path: string, id: string, text: string): void;
|
|
6
|
+
/** Remove a fact by id. Returns true if one was removed. */
|
|
7
|
+
export declare function forget(path: string, id: string): boolean;
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* parse-fafm — read and write a `.fafm` file (persistent memory).
|
|
3
|
+
*
|
|
4
|
+
* Uses the `yaml` package's Document API so remember()/recall() edit the
|
|
5
|
+
* file structurally and stringify back with comments and layout intact.
|
|
6
|
+
* This file is dogfood memory about the repo's own build — the header
|
|
7
|
+
* comments matter, and a `git diff` after a `remember()` should show only
|
|
8
|
+
* the fact that was added, not a whole-file reformat.
|
|
9
|
+
*/
|
|
10
|
+
import { readFileSync, writeFileSync } from "node:fs";
|
|
11
|
+
import { parseDocument, Scalar, YAMLSeq, YAMLMap } from "yaml";
|
|
12
|
+
/** Stringify options that keep the file stable across edits. */
|
|
13
|
+
const OUT = {
|
|
14
|
+
lineWidth: 0, // never fold long scalars onto new lines
|
|
15
|
+
flowCollectionPadding: false, // `["a","b"]`, not `[ "a", "b" ]`
|
|
16
|
+
};
|
|
17
|
+
/** A double-quoted string scalar — the .fafm/.fafa house convention. */
|
|
18
|
+
function q(v) {
|
|
19
|
+
const s = new Scalar(v);
|
|
20
|
+
s.type = Scalar.QUOTE_DOUBLE;
|
|
21
|
+
return s;
|
|
22
|
+
}
|
|
23
|
+
/** A YAMLMap with every string value double-quoted. */
|
|
24
|
+
function factMap(entries) {
|
|
25
|
+
const m = new YAMLMap();
|
|
26
|
+
for (const [k, v] of Object.entries(entries))
|
|
27
|
+
m.set(k, q(v));
|
|
28
|
+
return m;
|
|
29
|
+
}
|
|
30
|
+
function load(path) {
|
|
31
|
+
return parseDocument(readFileSync(path, "utf8"));
|
|
32
|
+
}
|
|
33
|
+
export function parseFafm(path) {
|
|
34
|
+
let doc;
|
|
35
|
+
try {
|
|
36
|
+
doc = load(path);
|
|
37
|
+
if (doc.errors.length)
|
|
38
|
+
return { facts: [] };
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
return { facts: [] };
|
|
42
|
+
}
|
|
43
|
+
let facts = [];
|
|
44
|
+
try {
|
|
45
|
+
const seq = doc.getIn(["memory", "facts"], true);
|
|
46
|
+
const j = seq?.toJSON?.();
|
|
47
|
+
if (Array.isArray(j))
|
|
48
|
+
facts = j;
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
/* leave facts empty */
|
|
52
|
+
}
|
|
53
|
+
return {
|
|
54
|
+
version: doc.get("version"),
|
|
55
|
+
profile: doc.get("profile"),
|
|
56
|
+
namepoint: doc.get("namepoint"),
|
|
57
|
+
facts: facts.map(normFact),
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
export function recall(path, id) {
|
|
61
|
+
return parseFafm(path).facts.find((f) => f.id === id) ?? null;
|
|
62
|
+
}
|
|
63
|
+
/** Add a fact, or update its text in place if the id already exists. */
|
|
64
|
+
export function remember(path, id, text) {
|
|
65
|
+
const doc = load(path);
|
|
66
|
+
let facts = doc.getIn(["memory", "facts"], true);
|
|
67
|
+
if (!facts) {
|
|
68
|
+
if (!doc.has("memory"))
|
|
69
|
+
doc.set("memory", new YAMLMap());
|
|
70
|
+
facts = new YAMLSeq();
|
|
71
|
+
doc.getIn(["memory"], true).set("facts", facts);
|
|
72
|
+
}
|
|
73
|
+
const now = new Date().toISOString();
|
|
74
|
+
for (const item of facts.items) {
|
|
75
|
+
if (item.get("id") === id) {
|
|
76
|
+
item.set("text", q(text));
|
|
77
|
+
item.set("verification_status", q("unverified"));
|
|
78
|
+
doc.set("last_etched", q(now));
|
|
79
|
+
writeFileSync(path, doc.toString(OUT));
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
facts.add(factMap({
|
|
84
|
+
text,
|
|
85
|
+
id,
|
|
86
|
+
type: "session",
|
|
87
|
+
priority: "standard",
|
|
88
|
+
source: "remember()",
|
|
89
|
+
verification_status: "unverified",
|
|
90
|
+
}));
|
|
91
|
+
doc.set("last_etched", q(now));
|
|
92
|
+
writeFileSync(path, doc.toString(OUT));
|
|
93
|
+
}
|
|
94
|
+
/** Remove a fact by id. Returns true if one was removed. */
|
|
95
|
+
export function forget(path, id) {
|
|
96
|
+
const doc = load(path);
|
|
97
|
+
const facts = doc.getIn(["memory", "facts"], true);
|
|
98
|
+
if (!facts)
|
|
99
|
+
return false;
|
|
100
|
+
const idx = facts.items.findIndex((it) => it.get("id") === id);
|
|
101
|
+
if (idx === -1)
|
|
102
|
+
return false;
|
|
103
|
+
facts.delete(idx);
|
|
104
|
+
doc.set("last_etched", q(new Date().toISOString()));
|
|
105
|
+
writeFileSync(path, doc.toString(OUT));
|
|
106
|
+
return true;
|
|
107
|
+
}
|
|
108
|
+
function normFact(f) {
|
|
109
|
+
const o = (f ?? {});
|
|
110
|
+
return {
|
|
111
|
+
id: String(o.id ?? ""),
|
|
112
|
+
text: String(o.text ?? ""),
|
|
113
|
+
type: o.type,
|
|
114
|
+
priority: o.priority,
|
|
115
|
+
tags: Array.isArray(o.tags) ? o.tags.map(String) : undefined,
|
|
116
|
+
source: o.source,
|
|
117
|
+
verification_status: o.verification_status,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared types for the two FAF-family formats this server reads.
|
|
3
|
+
* These are the minimal shapes mcp-context-card needs — not the full specs.
|
|
4
|
+
* Full specs: application/vnd.fafm+yaml, application/vnd.fafa+yaml
|
|
5
|
+
* (both IANA-registered).
|
|
6
|
+
*/
|
|
7
|
+
/** One `.fafm` fact. */
|
|
8
|
+
export interface MemoryFact {
|
|
9
|
+
id: string;
|
|
10
|
+
text: string;
|
|
11
|
+
type?: string;
|
|
12
|
+
priority?: string;
|
|
13
|
+
tags?: string[];
|
|
14
|
+
source?: string;
|
|
15
|
+
verification_status?: string;
|
|
16
|
+
}
|
|
17
|
+
/** A parsed `.fafm` — persistent memory. */
|
|
18
|
+
export interface Memory {
|
|
19
|
+
version?: string;
|
|
20
|
+
profile?: string;
|
|
21
|
+
namepoint?: string;
|
|
22
|
+
facts: MemoryFact[];
|
|
23
|
+
}
|
|
24
|
+
/** A parsed `.fafa` — agent identity card. */
|
|
25
|
+
export interface AgentIdentity {
|
|
26
|
+
version?: string;
|
|
27
|
+
name?: string;
|
|
28
|
+
displayName?: string;
|
|
29
|
+
vendor?: string;
|
|
30
|
+
agentVersion?: string;
|
|
31
|
+
description?: string;
|
|
32
|
+
status?: string;
|
|
33
|
+
license?: string;
|
|
34
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { AgentIdentity } from "./faf/types.js";
|
|
2
|
+
/** The `_meta` key namespace — the publisher's, reverse-DNS. */
|
|
3
|
+
export declare const META_NS = "io.github.wolfe-jam.mcp-context-card";
|
|
4
|
+
export declare function identity(root: string): AgentIdentity | null;
|
|
5
|
+
/**
|
|
6
|
+
* The identity to show: the `.well-known/fafa` card if present — richer and
|
|
7
|
+
* portable — else a thin one from `package.json` (the common project has no
|
|
8
|
+
* `.fafa`). null only when neither exists.
|
|
9
|
+
*/
|
|
10
|
+
export declare function resolveIdentity(root: string): AgentIdentity | null;
|
|
11
|
+
/** Human-readable one-liner for the `whoami` tool. */
|
|
12
|
+
export declare function whoami(root: string): string;
|
|
13
|
+
/**
|
|
14
|
+
* The `_meta` context block — one namespaced key per concern. The `context`
|
|
15
|
+
* concern points at `AGENTS.md` (the de-facto standard, plain Markdown). The
|
|
16
|
+
* other two point at their worked-example artifacts; `memory` carries a note
|
|
17
|
+
* because there is no de-facto standard for it yet.
|
|
18
|
+
*/
|
|
19
|
+
export declare function trinityMeta(): {
|
|
20
|
+
readonly "io.github.wolfe-jam.mcp-context-card/context": {
|
|
21
|
+
readonly source: "AGENTS.md";
|
|
22
|
+
readonly mediaType: "text/markdown";
|
|
23
|
+
};
|
|
24
|
+
readonly "io.github.wolfe-jam.mcp-context-card/memory": {
|
|
25
|
+
readonly source: "project.fafm";
|
|
26
|
+
readonly mediaType: "application/vnd.fafm+yaml";
|
|
27
|
+
readonly iana: string;
|
|
28
|
+
readonly note: "no de-facto standard for agent memory yet — this is one instantiation";
|
|
29
|
+
};
|
|
30
|
+
readonly "io.github.wolfe-jam.mcp-context-card/identity": {
|
|
31
|
+
readonly source: ".well-known/fafa";
|
|
32
|
+
readonly mediaType: "application/vnd.fafa+yaml";
|
|
33
|
+
readonly iana: string;
|
|
34
|
+
};
|
|
35
|
+
};
|
package/dist/identity.js
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* identity — reads this server's own `.well-known/fafa` and reports it, and
|
|
3
|
+
* builds the `_meta` context block for the Server Card.
|
|
4
|
+
*
|
|
5
|
+
* The `_meta` block is three reverse-DNS-namespaced keys — one per concern
|
|
6
|
+
* (context, memory, identity) — each naming a source file, its media type,
|
|
7
|
+
* and (where one exists) its IANA anchor. Same shape a real client reading
|
|
8
|
+
* the Server Card would consume; see server.ts for where it's emitted.
|
|
9
|
+
*/
|
|
10
|
+
import { readFileSync } from "node:fs";
|
|
11
|
+
import { join } from "node:path";
|
|
12
|
+
import { parseFafa } from "./faf/parse-fafa.js";
|
|
13
|
+
/** The `_meta` key namespace — the publisher's, reverse-DNS. */
|
|
14
|
+
export const META_NS = "io.github.wolfe-jam.mcp-context-card";
|
|
15
|
+
const iana = (t) => `https://www.iana.org/assignments/media-types/${t}`;
|
|
16
|
+
export function identity(root) {
|
|
17
|
+
return parseFafa(join(root, ".well-known/fafa"));
|
|
18
|
+
}
|
|
19
|
+
/** Fall back to package.json — most projects have no .fafa, but they have this. */
|
|
20
|
+
function fromPackageJson(root) {
|
|
21
|
+
try {
|
|
22
|
+
const p = JSON.parse(readFileSync(join(root, "package.json"), "utf8"));
|
|
23
|
+
if (!p.name)
|
|
24
|
+
return null;
|
|
25
|
+
return {
|
|
26
|
+
name: String(p.name),
|
|
27
|
+
agentVersion: p.version ? String(p.version) : undefined,
|
|
28
|
+
license: p.license ? String(p.license) : undefined,
|
|
29
|
+
description: p.description ? String(p.description) : undefined,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* The identity to show: the `.well-known/fafa` card if present — richer and
|
|
38
|
+
* portable — else a thin one from `package.json` (the common project has no
|
|
39
|
+
* `.fafa`). null only when neither exists.
|
|
40
|
+
*/
|
|
41
|
+
export function resolveIdentity(root) {
|
|
42
|
+
return identity(root) ?? fromPackageJson(root);
|
|
43
|
+
}
|
|
44
|
+
/** Human-readable one-liner for the `whoami` tool. */
|
|
45
|
+
export function whoami(root) {
|
|
46
|
+
const id = resolveIdentity(root);
|
|
47
|
+
if (!id)
|
|
48
|
+
return "(no .well-known/fafa or package.json found)";
|
|
49
|
+
const parts = [
|
|
50
|
+
id.displayName ?? id.name ?? "(unnamed)",
|
|
51
|
+
id.agentVersion ? `v${id.agentVersion}` : null,
|
|
52
|
+
id.vendor ? `vendor: ${id.vendor}` : null,
|
|
53
|
+
id.status ? `status: ${id.status}` : null,
|
|
54
|
+
id.license ? id.license : null,
|
|
55
|
+
].filter(Boolean);
|
|
56
|
+
return parts.join(" · ") + (id.description ? `\n${id.description}` : "");
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* The `_meta` context block — one namespaced key per concern. The `context`
|
|
60
|
+
* concern points at `AGENTS.md` (the de-facto standard, plain Markdown). The
|
|
61
|
+
* other two point at their worked-example artifacts; `memory` carries a note
|
|
62
|
+
* because there is no de-facto standard for it yet.
|
|
63
|
+
*/
|
|
64
|
+
export function trinityMeta() {
|
|
65
|
+
return {
|
|
66
|
+
[`${META_NS}/context`]: {
|
|
67
|
+
source: "AGENTS.md",
|
|
68
|
+
mediaType: "text/markdown",
|
|
69
|
+
},
|
|
70
|
+
[`${META_NS}/memory`]: {
|
|
71
|
+
source: "project.fafm",
|
|
72
|
+
mediaType: "application/vnd.fafm+yaml",
|
|
73
|
+
iana: iana("application/vnd.fafm+yaml"),
|
|
74
|
+
note: "no de-facto standard for agent memory yet — this is one instantiation",
|
|
75
|
+
},
|
|
76
|
+
[`${META_NS}/identity`]: {
|
|
77
|
+
source: ".well-known/fafa",
|
|
78
|
+
mediaType: "application/vnd.fafa+yaml",
|
|
79
|
+
iana: iana("application/vnd.fafa+yaml"),
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
}
|
package/dist/md.d.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* md - a minimal Markdown to HTML renderer, dependency-free.
|
|
3
|
+
*
|
|
4
|
+
* Covers exactly what an AGENTS.md uses: ATX headings, paragraphs, fenced
|
|
5
|
+
* code, unordered / ordered lists (one nested level), GFM tables,
|
|
6
|
+
* blockquotes, horizontal rules, and inline bold / italic / code / links.
|
|
7
|
+
* Anything it does not recognise becomes an escaped paragraph. It never
|
|
8
|
+
* throws and never emits an unescaped angle bracket.
|
|
9
|
+
*
|
|
10
|
+
* Not spec-compliant CommonMark. If a project's AGENTS.md outgrows it,
|
|
11
|
+
* swap in `marked`.
|
|
12
|
+
*/
|
|
13
|
+
export declare function escapeHtml(s: string): string;
|
|
14
|
+
/** kebab slug for a heading id */
|
|
15
|
+
export declare function slug(s: string): string;
|
|
16
|
+
/**
|
|
17
|
+
* Inline pass: escape first, then pull out code spans as HTML-comment
|
|
18
|
+
* placeholders (impossible in escaped text), then links -> bold -> italic,
|
|
19
|
+
* then restore the code spans. ASCII throughout.
|
|
20
|
+
*/
|
|
21
|
+
export declare function renderInline(src: string): string;
|
|
22
|
+
export declare function renderMarkdown(src: string): string;
|
package/dist/md.js
ADDED
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* md - a minimal Markdown to HTML renderer, dependency-free.
|
|
3
|
+
*
|
|
4
|
+
* Covers exactly what an AGENTS.md uses: ATX headings, paragraphs, fenced
|
|
5
|
+
* code, unordered / ordered lists (one nested level), GFM tables,
|
|
6
|
+
* blockquotes, horizontal rules, and inline bold / italic / code / links.
|
|
7
|
+
* Anything it does not recognise becomes an escaped paragraph. It never
|
|
8
|
+
* throws and never emits an unescaped angle bracket.
|
|
9
|
+
*
|
|
10
|
+
* Not spec-compliant CommonMark. If a project's AGENTS.md outgrows it,
|
|
11
|
+
* swap in `marked`.
|
|
12
|
+
*/
|
|
13
|
+
export function escapeHtml(s) {
|
|
14
|
+
return s
|
|
15
|
+
.replace(/&/g, "&")
|
|
16
|
+
.replace(/</g, "<")
|
|
17
|
+
.replace(/>/g, ">")
|
|
18
|
+
.replace(/"/g, """);
|
|
19
|
+
}
|
|
20
|
+
/** kebab slug for a heading id */
|
|
21
|
+
export function slug(s) {
|
|
22
|
+
return s
|
|
23
|
+
.toLowerCase()
|
|
24
|
+
.replace(/[^\w\s-]/g, "")
|
|
25
|
+
.trim()
|
|
26
|
+
.replace(/\s+/g, "-");
|
|
27
|
+
}
|
|
28
|
+
const SAFE_URL = /^(https?:|mailto:|#|\.?\/)/i;
|
|
29
|
+
/**
|
|
30
|
+
* Inline pass: escape first, then pull out code spans as HTML-comment
|
|
31
|
+
* placeholders (impossible in escaped text), then links -> bold -> italic,
|
|
32
|
+
* then restore the code spans. ASCII throughout.
|
|
33
|
+
*/
|
|
34
|
+
export function renderInline(src) {
|
|
35
|
+
const codes = [];
|
|
36
|
+
let s = escapeHtml(src).replace(/`([^`]+)`/g, (_m, c) => {
|
|
37
|
+
codes.push("<code>" + c + "</code>");
|
|
38
|
+
return "<!--c" + (codes.length - 1) + "-->";
|
|
39
|
+
});
|
|
40
|
+
s = s.replace(/\[([^\]]+)\]\(([^)\s]+)\)/g, (_m, text, url) => {
|
|
41
|
+
// url is already escaped; & is valid inside an href
|
|
42
|
+
return SAFE_URL.test(String(url).replace(/&/g, "&"))
|
|
43
|
+
? '<a href="' + url + '">' + text + "</a>"
|
|
44
|
+
: text;
|
|
45
|
+
});
|
|
46
|
+
s = s.replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>");
|
|
47
|
+
s = s.replace(/__([^_]+)__/g, "<strong>$1</strong>");
|
|
48
|
+
s = s.replace(/(^|[^*])\*([^*\s][^*]*?)\*(?!\*)/g, "$1<em>$2</em>");
|
|
49
|
+
s = s.replace(/(^|[^\w])_([^_\s][^_]*?)_(?![\w])/g, "$1<em>$2</em>");
|
|
50
|
+
s = s.replace(/<!--c(\d+)-->/g, (_m, i) => codes[Number(i)] ?? "");
|
|
51
|
+
return s;
|
|
52
|
+
}
|
|
53
|
+
const HEADING = /^(#{1,6})\s+(.+?)\s*#*\s*$/;
|
|
54
|
+
const FENCE = /^(\s*)(```+|~~~+)(.*)$/;
|
|
55
|
+
const HR = /^ {0,3}([-*_])(?: *\1){2,} *$/;
|
|
56
|
+
const LI = /^(\s*)([-*+]|\d+[.)])\s+(.*)$/;
|
|
57
|
+
const TABLE_SEP = /^\s*\|?[\s:|-]*-[\s:|-]*\|?\s*$/;
|
|
58
|
+
function tableRow(line) {
|
|
59
|
+
return line.trim().replace(/^\|/, "").replace(/\|$/, "").split("|").map((c) => c.trim());
|
|
60
|
+
}
|
|
61
|
+
export function renderMarkdown(src) {
|
|
62
|
+
const lines = src.replace(/\r\n/g, "\n").split("\n");
|
|
63
|
+
const out = [];
|
|
64
|
+
let i = 0;
|
|
65
|
+
const flushPara = (buf) => {
|
|
66
|
+
if (buf.length)
|
|
67
|
+
out.push("<p>" + renderInline(buf.join(" ").trim()) + "</p>");
|
|
68
|
+
buf.length = 0;
|
|
69
|
+
};
|
|
70
|
+
while (i < lines.length) {
|
|
71
|
+
const line = lines[i];
|
|
72
|
+
if (!line.trim()) {
|
|
73
|
+
i++;
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
// fenced code
|
|
77
|
+
const fence = line.match(FENCE);
|
|
78
|
+
if (fence) {
|
|
79
|
+
const close = fence[2][0] === "`" ? /^\s*```+\s*$/ : /^\s*~~~+\s*$/;
|
|
80
|
+
const lang = fence[3].trim();
|
|
81
|
+
const body = [];
|
|
82
|
+
i++;
|
|
83
|
+
while (i < lines.length && !close.test(lines[i])) {
|
|
84
|
+
body.push(lines[i]);
|
|
85
|
+
i++;
|
|
86
|
+
}
|
|
87
|
+
i++; // closing fence
|
|
88
|
+
const cls = lang ? ' class="language-' + escapeHtml(lang) + '"' : "";
|
|
89
|
+
out.push("<pre><code" + cls + ">" + escapeHtml(body.join("\n")) + "</code></pre>");
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
// heading
|
|
93
|
+
const h = line.match(HEADING);
|
|
94
|
+
if (h) {
|
|
95
|
+
const n = h[1].length;
|
|
96
|
+
out.push("<h" + n + ' id="' + slug(h[2]) + '">' + renderInline(h[2].trim()) + "</h" + n + ">");
|
|
97
|
+
i++;
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
// hr
|
|
101
|
+
if (HR.test(line)) {
|
|
102
|
+
out.push("<hr>");
|
|
103
|
+
i++;
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
// blockquote
|
|
107
|
+
if (/^\s*>/.test(line)) {
|
|
108
|
+
const inner = [];
|
|
109
|
+
while (i < lines.length && /^\s*>/.test(lines[i])) {
|
|
110
|
+
inner.push(lines[i].replace(/^\s*>\s?/, ""));
|
|
111
|
+
i++;
|
|
112
|
+
}
|
|
113
|
+
out.push("<blockquote>" + renderMarkdown(inner.join("\n")) + "</blockquote>");
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
// table
|
|
117
|
+
if (line.includes("|") && i + 1 < lines.length && TABLE_SEP.test(lines[i + 1])) {
|
|
118
|
+
const head = tableRow(line);
|
|
119
|
+
i += 2;
|
|
120
|
+
const rows = [];
|
|
121
|
+
while (i < lines.length && lines[i].includes("|") && lines[i].trim()) {
|
|
122
|
+
rows.push(tableRow(lines[i]));
|
|
123
|
+
i++;
|
|
124
|
+
}
|
|
125
|
+
const th = head.map((c) => "<th>" + renderInline(c) + "</th>").join("");
|
|
126
|
+
const trs = rows
|
|
127
|
+
.map((r) => "<tr>" + r.map((c) => "<td>" + renderInline(c) + "</td>").join("") + "</tr>")
|
|
128
|
+
.join("");
|
|
129
|
+
out.push("<table><thead><tr>" + th + "</tr></thead><tbody>" + trs + "</tbody></table>");
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
// list (one nested level)
|
|
133
|
+
if (LI.test(line)) {
|
|
134
|
+
const first = line.match(LI);
|
|
135
|
+
const tag = /\d/.test(first[2]) ? "ol" : "ul";
|
|
136
|
+
const items = [];
|
|
137
|
+
let baseIndent = first[1].length;
|
|
138
|
+
let cur = null;
|
|
139
|
+
const push = () => {
|
|
140
|
+
if (cur) {
|
|
141
|
+
const subHtml = cur.sub.length ? renderMarkdown(cur.sub.join("\n")) : "";
|
|
142
|
+
items.push("<li>" + renderInline(cur.text.join(" ").trim()) + subHtml + "</li>");
|
|
143
|
+
cur = null;
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
while (i < lines.length) {
|
|
147
|
+
const m = lines[i].match(LI);
|
|
148
|
+
if (m && m[1].length <= baseIndent + 1) {
|
|
149
|
+
push();
|
|
150
|
+
baseIndent = m[1].length;
|
|
151
|
+
cur = { text: [m[3]], sub: [] };
|
|
152
|
+
i++;
|
|
153
|
+
}
|
|
154
|
+
else if (m && cur) {
|
|
155
|
+
cur.sub.push(lines[i].slice(baseIndent + 2));
|
|
156
|
+
i++;
|
|
157
|
+
}
|
|
158
|
+
else if (lines[i].trim() && cur && !HEADING.test(lines[i]) && !FENCE.test(lines[i])) {
|
|
159
|
+
cur.text.push(lines[i].trim());
|
|
160
|
+
i++;
|
|
161
|
+
}
|
|
162
|
+
else {
|
|
163
|
+
break;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
push();
|
|
167
|
+
out.push("<" + tag + ">" + items.join("") + "</" + tag + ">");
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
// paragraph
|
|
171
|
+
const buf = [];
|
|
172
|
+
while (i < lines.length &&
|
|
173
|
+
lines[i].trim() &&
|
|
174
|
+
!HEADING.test(lines[i]) &&
|
|
175
|
+
!FENCE.test(lines[i]) &&
|
|
176
|
+
!HR.test(lines[i]) &&
|
|
177
|
+
!LI.test(lines[i]) &&
|
|
178
|
+
!/^\s*>/.test(lines[i]) &&
|
|
179
|
+
!(lines[i].includes("|") && i + 1 < lines.length && TABLE_SEP.test(lines[i + 1]))) {
|
|
180
|
+
buf.push(lines[i]);
|
|
181
|
+
i++;
|
|
182
|
+
}
|
|
183
|
+
flushPara(buf);
|
|
184
|
+
}
|
|
185
|
+
return out.join("\n");
|
|
186
|
+
}
|