@tpsdev-ai/flair 0.3.17 → 0.3.19
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 +30 -27
- package/dist/cli.js +38 -0
- package/package.json +1 -2
- package/resources/A2AAdapter.ts +0 -510
- package/resources/Agent.ts +0 -10
- package/resources/AgentCard.ts +0 -65
- package/resources/AgentSeed.ts +0 -119
- package/resources/IngestEvents.ts +0 -189
- package/resources/Integration.ts +0 -14
- package/resources/IssueTokens.ts +0 -29
- package/resources/Memory.ts +0 -151
- package/resources/MemoryBootstrap.ts +0 -323
- package/resources/MemoryConsolidate.ts +0 -121
- package/resources/MemoryFeed.ts +0 -48
- package/resources/MemoryMaintenance.ts +0 -95
- package/resources/MemoryReflect.ts +0 -122
- package/resources/OrgEvent.ts +0 -63
- package/resources/OrgEventCatchup.ts +0 -89
- package/resources/OrgEventMaintenance.ts +0 -37
- package/resources/SemanticSearch.ts +0 -197
- package/resources/SkillScan.ts +0 -146
- package/resources/Soul.ts +0 -10
- package/resources/SoulFeed.ts +0 -15
- package/resources/WorkspaceLatest.ts +0 -66
- package/resources/WorkspaceState.ts +0 -102
- package/resources/auth-middleware.ts +0 -501
- package/resources/embeddings-provider.ts +0 -61
- package/resources/embeddings.ts +0 -28
- package/resources/health.ts +0 -7
- package/resources/memory-feed-lib.ts +0 -22
- package/resources/table-helpers.ts +0 -46
package/README.md
CHANGED
|
@@ -128,29 +128,28 @@ openclaw plugins install @tpsdev-ai/openclaw-flair
|
|
|
128
128
|
|
|
129
129
|
The plugin auto-detects your agent identity, provides `memory_store`/`memory_recall`/`memory_get` tools, and injects relevant memories at session start. See the [plugin README](plugins/openclaw-flair/README.md) for details.
|
|
130
130
|
|
|
131
|
-
### Claude Code / Codex / Cursor
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
```
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
131
|
+
### Claude Code / Codex / Cursor (MCP)
|
|
132
|
+
|
|
133
|
+
Install the MCP server for native tool integration:
|
|
134
|
+
|
|
135
|
+
```json
|
|
136
|
+
// .mcp.json in your project root
|
|
137
|
+
{
|
|
138
|
+
"mcpServers": {
|
|
139
|
+
"flair": {
|
|
140
|
+
"command": "npx",
|
|
141
|
+
"args": ["@tpsdev-ai/flair-mcp"],
|
|
142
|
+
"env": { "FLAIR_AGENT_ID": "mybot" }
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
```
|
|
139
147
|
|
|
140
|
-
|
|
141
|
-
Run: `flair bootstrap --agent mybot --max-tokens 4000`
|
|
142
|
-
This returns your soul + recent memories. Read it — that's your context.
|
|
148
|
+
Add to your `CLAUDE.md`:
|
|
143
149
|
|
|
144
|
-
|
|
145
|
-
- Remember something: `flair memory add --agent mybot --content "what you learned"`
|
|
146
|
-
- Search memory: `flair memory search --agent mybot --q "your query"`
|
|
147
|
-
- Store a lesson: `flair memory add --agent mybot --content "lesson" --type lesson --durability persistent`
|
|
150
|
+
At the start of every session, run mcp__flair__bootstrap before responding.
|
|
148
151
|
|
|
149
|
-
|
|
150
|
-
- Bootstrap FIRST, before doing anything else
|
|
151
|
-
- Store lessons and decisions immediately — don't wait
|
|
152
|
-
- If you learn something that should survive restarts, write it to Flair
|
|
153
|
-
```
|
|
152
|
+
Claude Code gets native tools: `memory_store`, `memory_search`, `bootstrap`, `soul_set`, and more. See the [MCP README](packages/flair-mcp/README.md) and [Claude Code guide](docs/claude-code.md).
|
|
154
153
|
|
|
155
154
|
### JavaScript / TypeScript (Client Library)
|
|
156
155
|
|
|
@@ -292,21 +291,25 @@ Flair is in active development and daily use. We dogfood it — the agents that
|
|
|
292
291
|
|
|
293
292
|
**What works:**
|
|
294
293
|
- ✅ Ed25519 agent identity and auth
|
|
295
|
-
- ✅ CLI: init, agent add/remove/rotate-key, status, backup/restore, grant/revoke
|
|
296
|
-
- ✅ Memory CRUD with durability enforcement
|
|
297
|
-
- ✅ In-process semantic embeddings (768-dim nomic-embed-text
|
|
298
|
-
- ✅ Hybrid search (semantic + keyword)
|
|
294
|
+
- ✅ CLI: init, agent add/remove/rotate-key, status, backup/restore, export/import, grant/revoke
|
|
295
|
+
- ✅ Memory CRUD with durability enforcement and near-duplicate detection
|
|
296
|
+
- ✅ In-process semantic embeddings (768-dim nomic-embed-text via harper-fabric-embeddings)
|
|
297
|
+
- ✅ Hybrid search (semantic + keyword + temporal intent detection)
|
|
299
298
|
- ✅ Soul (permanent personality/values)
|
|
300
299
|
- ✅ Real-time feeds (WebSocket/SSE)
|
|
301
300
|
- ✅ Agent-scoped data isolation
|
|
302
|
-
- ✅ Cold start bootstrap
|
|
301
|
+
- ✅ Cold start bootstrap with adaptive time window
|
|
303
302
|
- ✅ OpenClaw memory plugin
|
|
303
|
+
- ✅ MCP server for Claude Code / Cursor / Windsurf
|
|
304
|
+
- ✅ Lightweight client library (`@tpsdev-ai/flair-client`)
|
|
305
|
+
- ✅ Portable agent identity (export/import between instances)
|
|
306
|
+
- ✅ `flair --version`, `flair upgrade`
|
|
304
307
|
|
|
305
308
|
**What's next:**
|
|
309
|
+
- [ ] First-run soul wizard (interactive personality setup)
|
|
310
|
+
- [ ] Git-backed memory sync
|
|
306
311
|
- [ ] Encryption at rest (opt-in AES-256-GCM per memory)
|
|
307
|
-
- [ ] Pluggable embedding backends (OpenAI, Cohere, local)
|
|
308
312
|
- [ ] Harper Fabric deployment (managed multi-office)
|
|
309
|
-
- [ ] Scheduled automatic backups
|
|
310
313
|
|
|
311
314
|
## License
|
|
312
315
|
|
package/dist/cli.js
CHANGED
|
@@ -231,6 +231,7 @@ program
|
|
|
231
231
|
.option("--keys-dir <dir>", "Directory for Ed25519 keys")
|
|
232
232
|
.option("--data-dir <dir>", "Harper data directory")
|
|
233
233
|
.option("--skip-start", "Skip Harper startup (assume already running)")
|
|
234
|
+
.option("--skip-soul", "Skip interactive personality setup")
|
|
234
235
|
.action(async (opts) => {
|
|
235
236
|
const agentId = opts.agentId;
|
|
236
237
|
const httpPort = Number(opts.port);
|
|
@@ -335,6 +336,43 @@ program
|
|
|
335
336
|
console.log(` ${adminPass}`);
|
|
336
337
|
}
|
|
337
338
|
console.log(`\n Export: FLAIR_URL=${httpUrl}`);
|
|
339
|
+
// ── First-run soul setup ──────────────────────────────────────────────
|
|
340
|
+
// Interactive prompts to set initial personality. Skipped with --skip-soul
|
|
341
|
+
// or when stdin is not a TTY (CI, scripts, piped input).
|
|
342
|
+
if (!opts.skipSoul && process.stdin.isTTY) {
|
|
343
|
+
console.log("\n🎭 Set up agent personality (press Enter to skip any):\n");
|
|
344
|
+
const { createInterface } = await import("node:readline");
|
|
345
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
346
|
+
const ask = (q) => new Promise(r => rl.question(q, r));
|
|
347
|
+
const role = await ask(" What's this agent's role? (e.g., \"Senior dev, concise and direct\")\n > ");
|
|
348
|
+
const project = await ask(" What project is it working on?\n > ");
|
|
349
|
+
const standards = await ask(" Any coding standards or preferences?\n > ");
|
|
350
|
+
rl.close();
|
|
351
|
+
// Write non-empty answers as soul entries
|
|
352
|
+
const soulEntries = [];
|
|
353
|
+
if (role.trim())
|
|
354
|
+
soulEntries.push(["role", role.trim()]);
|
|
355
|
+
if (project.trim())
|
|
356
|
+
soulEntries.push(["project", project.trim()]);
|
|
357
|
+
if (standards.trim())
|
|
358
|
+
soulEntries.push(["standards", standards.trim()]);
|
|
359
|
+
if (soulEntries.length > 0) {
|
|
360
|
+
console.log("");
|
|
361
|
+
for (const [key, value] of soulEntries) {
|
|
362
|
+
try {
|
|
363
|
+
await authFetch(httpUrl, agentId, privPath, "PUT", `/Soul/${agentId}:${key}`, { id: `${agentId}:${key}`, agentId, key, value, createdAt: new Date().toISOString() });
|
|
364
|
+
console.log(` ✓ soul:${key} set`);
|
|
365
|
+
}
|
|
366
|
+
catch (err) {
|
|
367
|
+
console.warn(` ⚠ soul:${key} failed: ${err.message}`);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
console.log(`\n ${soulEntries.length} soul entries saved. Bootstrap will include them.`);
|
|
371
|
+
}
|
|
372
|
+
else {
|
|
373
|
+
console.log("\n No soul entries — you can add them later with: flair soul set --agent " + agentId + " --key role --value \"...\"");
|
|
374
|
+
}
|
|
375
|
+
}
|
|
338
376
|
console.log(`\n Claude Code: Add to your CLAUDE.md:`);
|
|
339
377
|
console.log(` At the start of every session, run mcp__flair__bootstrap before responding.`);
|
|
340
378
|
console.log(`\n MCP config (.mcp.json):`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tpsdev-ai/flair",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.19",
|
|
4
4
|
"description": "Identity, memory, and soul for AI agents. Cryptographic identity (Ed25519), semantic memory with local embeddings, and persistent personality — all in a single process.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -29,7 +29,6 @@
|
|
|
29
29
|
},
|
|
30
30
|
"files": [
|
|
31
31
|
"dist/",
|
|
32
|
-
"resources/",
|
|
33
32
|
"schemas/",
|
|
34
33
|
"config.yaml",
|
|
35
34
|
"LICENSE",
|
package/resources/A2AAdapter.ts
DELETED
|
@@ -1,510 +0,0 @@
|
|
|
1
|
-
import { Resource, databases } from "@harperfast/harper";
|
|
2
|
-
import { access, readFile, readdir } from "node:fs/promises";
|
|
3
|
-
import { constants } from "node:fs";
|
|
4
|
-
import { basename, extname, join } from "node:path";
|
|
5
|
-
|
|
6
|
-
type JsonRpcRequest = {
|
|
7
|
-
jsonrpc: string;
|
|
8
|
-
id?: string | number | null;
|
|
9
|
-
method: string;
|
|
10
|
-
params?: Record<string, any>;
|
|
11
|
-
};
|
|
12
|
-
|
|
13
|
-
type BeadsIssue = {
|
|
14
|
-
id: string;
|
|
15
|
-
title?: string;
|
|
16
|
-
description?: string;
|
|
17
|
-
notes?: string;
|
|
18
|
-
status?: string;
|
|
19
|
-
assignee?: string;
|
|
20
|
-
updated_at?: string;
|
|
21
|
-
created_at?: string;
|
|
22
|
-
[key: string]: any;
|
|
23
|
-
};
|
|
24
|
-
|
|
25
|
-
const BEADS_ROOT = join(process.env.HOME || "/root", "ops", ".beads");
|
|
26
|
-
const BEADS_ISSUES_DIR = join(BEADS_ROOT, "issues");
|
|
27
|
-
const BEADS_ISSUES_JSONL = join(BEADS_ROOT, "issues.jsonl");
|
|
28
|
-
|
|
29
|
-
function rpcResult(id: string | number | null | undefined, result: any) {
|
|
30
|
-
return { jsonrpc: "2.0", id: id ?? null, result };
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
function rpcError(id: string | number | null | undefined, code: number, message: string, data?: any) {
|
|
34
|
-
return {
|
|
35
|
-
jsonrpc: "2.0",
|
|
36
|
-
id: id ?? null,
|
|
37
|
-
error: data === undefined ? { code, message } : { code, message, data },
|
|
38
|
-
};
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
function cleanText(value: unknown): string {
|
|
42
|
-
return String(value ?? "").trim();
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
function truncate(value: string, max: number): string {
|
|
46
|
-
return value.length <= max ? value : `${value.slice(0, Math.max(0, max - 1))}…`;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
function firstTextPart(message: any): string {
|
|
50
|
-
const parts = Array.isArray(message?.parts) ? message.parts : [];
|
|
51
|
-
for (const part of parts) {
|
|
52
|
-
const text = cleanText(part?.text);
|
|
53
|
-
if (text) return text;
|
|
54
|
-
}
|
|
55
|
-
return "";
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
function stripQuotes(value: string): string {
|
|
59
|
-
if ((value.startsWith("\"") && value.endsWith("\"")) || (value.startsWith("'") && value.endsWith("'"))) {
|
|
60
|
-
return value.slice(1, -1);
|
|
61
|
-
}
|
|
62
|
-
return value;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
function parseSimpleYamlIssue(raw: string, fallbackId: string): BeadsIssue {
|
|
66
|
-
const issue: BeadsIssue = { id: fallbackId };
|
|
67
|
-
const lines = raw.split(/\r?\n/);
|
|
68
|
-
let listKey: string | null = null;
|
|
69
|
-
|
|
70
|
-
for (const line of lines) {
|
|
71
|
-
if (!line.trim() || line.trimStart().startsWith("#")) continue;
|
|
72
|
-
|
|
73
|
-
const itemMatch = line.match(/^\s*-\s*(.+)\s*$/);
|
|
74
|
-
if (itemMatch && listKey) {
|
|
75
|
-
const current = issue[listKey];
|
|
76
|
-
if (!Array.isArray(current)) issue[listKey] = [];
|
|
77
|
-
issue[listKey].push(stripQuotes(itemMatch[1].trim()));
|
|
78
|
-
continue;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
if (/^\s/.test(line)) continue;
|
|
82
|
-
|
|
83
|
-
const fieldMatch = line.match(/^([A-Za-z0-9_]+):\s*(.*)\s*$/);
|
|
84
|
-
if (!fieldMatch) {
|
|
85
|
-
listKey = null;
|
|
86
|
-
continue;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
const [, key, rawValue] = fieldMatch;
|
|
90
|
-
if (rawValue === "") {
|
|
91
|
-
issue[key] = issue[key] ?? "";
|
|
92
|
-
listKey = key;
|
|
93
|
-
continue;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
if (rawValue === "[]" || rawValue === "[ ]") {
|
|
97
|
-
issue[key] = [];
|
|
98
|
-
listKey = null;
|
|
99
|
-
continue;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
if (rawValue === "|" || rawValue === ">") {
|
|
103
|
-
issue[key] = "";
|
|
104
|
-
listKey = null;
|
|
105
|
-
continue;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
issue[key] = stripQuotes(rawValue.trim());
|
|
109
|
-
listKey = null;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
if (!issue.id) issue.id = fallbackId;
|
|
113
|
-
return issue;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
async function pathExists(path: string): Promise<boolean> {
|
|
117
|
-
try {
|
|
118
|
-
await access(path, constants.F_OK);
|
|
119
|
-
return true;
|
|
120
|
-
} catch {
|
|
121
|
-
return false;
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
async function readIssuesFromYamlDir(): Promise<BeadsIssue[]> {
|
|
126
|
-
if (!(await pathExists(BEADS_ISSUES_DIR))) return [];
|
|
127
|
-
|
|
128
|
-
const entries = await readdir(BEADS_ISSUES_DIR, { withFileTypes: true });
|
|
129
|
-
const out: BeadsIssue[] = [];
|
|
130
|
-
for (const entry of entries) {
|
|
131
|
-
if (!entry.isFile()) continue;
|
|
132
|
-
const ext = extname(entry.name).toLowerCase();
|
|
133
|
-
if (ext !== ".yaml" && ext !== ".yml") continue;
|
|
134
|
-
|
|
135
|
-
const fullPath = join(BEADS_ISSUES_DIR, entry.name);
|
|
136
|
-
const raw = await readFile(fullPath, "utf8");
|
|
137
|
-
const fallbackId = basename(entry.name, ext);
|
|
138
|
-
out.push(parseSimpleYamlIssue(raw, fallbackId));
|
|
139
|
-
}
|
|
140
|
-
return out;
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
async function readIssuesFromJsonl(): Promise<BeadsIssue[]> {
|
|
144
|
-
if (!(await pathExists(BEADS_ISSUES_JSONL))) return [];
|
|
145
|
-
const raw = await readFile(BEADS_ISSUES_JSONL, "utf8");
|
|
146
|
-
const out: BeadsIssue[] = [];
|
|
147
|
-
for (const line of raw.split(/\r?\n/)) {
|
|
148
|
-
if (!line.trim()) continue;
|
|
149
|
-
try {
|
|
150
|
-
const parsed = JSON.parse(line);
|
|
151
|
-
if (parsed?.id) out.push(parsed);
|
|
152
|
-
} catch {
|
|
153
|
-
// Ignore malformed lines.
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
return out;
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
async function readIssue(taskId: string): Promise<BeadsIssue | null> {
|
|
160
|
-
const yamlPath = join(BEADS_ISSUES_DIR, `${taskId}.yaml`);
|
|
161
|
-
const ymlPath = join(BEADS_ISSUES_DIR, `${taskId}.yml`);
|
|
162
|
-
for (const candidate of [yamlPath, ymlPath]) {
|
|
163
|
-
if (await pathExists(candidate)) {
|
|
164
|
-
const raw = await readFile(candidate, "utf8");
|
|
165
|
-
return parseSimpleYamlIssue(raw, taskId);
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
const jsonlIssues = await readIssuesFromJsonl();
|
|
170
|
-
return jsonlIssues.find((issue) => issue.id === taskId) ?? null;
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
function mapBeadsStatusToA2A(statusRaw: unknown): string {
|
|
174
|
-
const status = cleanText(statusRaw).toLowerCase();
|
|
175
|
-
if (status === "ready" || status === "in_progress" || status === "open" || status === "active" || status === "todo") {
|
|
176
|
-
return "working";
|
|
177
|
-
}
|
|
178
|
-
if (status === "done" || status === "closed" || status === "complete" || status === "completed") {
|
|
179
|
-
return "completed";
|
|
180
|
-
}
|
|
181
|
-
if (status === "blocked") {
|
|
182
|
-
return "input-required";
|
|
183
|
-
}
|
|
184
|
-
if (status === "cancelled" || status === "canceled") {
|
|
185
|
-
return "canceled";
|
|
186
|
-
}
|
|
187
|
-
return "working";
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
function taskView(issue: BeadsIssue): any {
|
|
191
|
-
return {
|
|
192
|
-
id: issue.id,
|
|
193
|
-
title: issue.title ?? "",
|
|
194
|
-
status: mapBeadsStatusToA2A(issue.status),
|
|
195
|
-
assignee: issue.assignee ?? null,
|
|
196
|
-
updatedAt: issue.updated_at ?? issue.created_at ?? null,
|
|
197
|
-
};
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
async function taskHistory(taskId: string): Promise<any[]> {
|
|
202
|
-
const history: any[] = [];
|
|
203
|
-
const refId = `bd://${taskId}`;
|
|
204
|
-
for await (const event of (databases as any).flair.OrgEvent.search()) {
|
|
205
|
-
if (event?.refId !== refId) continue;
|
|
206
|
-
const summary = cleanText(event.summary);
|
|
207
|
-
if (!summary) continue;
|
|
208
|
-
history.push({
|
|
209
|
-
createdAt: event.createdAt ?? "",
|
|
210
|
-
role: "agent",
|
|
211
|
-
parts: [{ text: summary }],
|
|
212
|
-
});
|
|
213
|
-
}
|
|
214
|
-
history.sort((a, b) => String(a.createdAt).localeCompare(String(b.createdAt)));
|
|
215
|
-
return history.map(({ role, parts }) => ({ role, parts }));
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
function artifactsFromIssue(issue: BeadsIssue): any[] {
|
|
219
|
-
const artifacts: any[] = [];
|
|
220
|
-
const notes = cleanText(issue.notes);
|
|
221
|
-
if (notes) {
|
|
222
|
-
artifacts.push({ name: "notes", parts: [{ text: notes }] });
|
|
223
|
-
}
|
|
224
|
-
return artifacts;
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
async function publishOrgEvent(event: any): Promise<void> {
|
|
228
|
-
await (databases as any).flair.OrgEvent.put({
|
|
229
|
-
id: event.id ?? `a2a-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
|
230
|
-
authorId: event.authorId ?? "a2a",
|
|
231
|
-
kind: event.kind,
|
|
232
|
-
scope: event.scope ?? null,
|
|
233
|
-
summary: event.summary,
|
|
234
|
-
detail: event.detail ?? "",
|
|
235
|
-
targetIds: event.targetIds ?? [],
|
|
236
|
-
refId: event.refId ?? null,
|
|
237
|
-
createdAt: event.createdAt ?? new Date().toISOString(),
|
|
238
|
-
});
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
function parseJsonSafe(value: unknown): any | null {
|
|
242
|
-
if (typeof value !== "string" || !value.trim()) return null;
|
|
243
|
-
try {
|
|
244
|
-
return JSON.parse(value);
|
|
245
|
-
} catch {
|
|
246
|
-
return null;
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
function normalizeA2AStatus(statusRaw: unknown): string | null {
|
|
251
|
-
const status = cleanText(statusRaw).toLowerCase();
|
|
252
|
-
if (!status) return null;
|
|
253
|
-
if (status === "done" || status === "closed" || status === "complete" || status === "completed") return "completed";
|
|
254
|
-
if (status === "failed" || status === "error") return "failed";
|
|
255
|
-
if (status === "cancelled" || status === "canceled") return "canceled";
|
|
256
|
-
if (status === "working" || status === "in_progress" || status === "open" || status === "active" || status === "todo") {
|
|
257
|
-
return "working";
|
|
258
|
-
}
|
|
259
|
-
return null;
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
function inferStatusFromText(textRaw: unknown): string | null {
|
|
263
|
-
const text = cleanText(textRaw).toLowerCase();
|
|
264
|
-
if (!text) return null;
|
|
265
|
-
if (text.includes("completed") || text.includes("complete") || text.includes("done")) return "completed";
|
|
266
|
-
if (text.includes("failed") || text.includes("error")) return "failed";
|
|
267
|
-
if (text.includes("cancelled") || text.includes("canceled")) return "canceled";
|
|
268
|
-
if (text.includes("working") || text.includes("started") || text.includes("in progress")) return "working";
|
|
269
|
-
return null;
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
function taskIdFromEvent(event: any): string | null {
|
|
273
|
-
const refId = cleanText(event?.refId);
|
|
274
|
-
if (refId.startsWith("bd://")) {
|
|
275
|
-
const taskId = cleanText(refId.slice("bd://".length));
|
|
276
|
-
if (taskId) return taskId;
|
|
277
|
-
}
|
|
278
|
-
const detail = parseJsonSafe(event?.detail);
|
|
279
|
-
const fromDetail = cleanText(detail?.taskId ?? detail?.id ?? detail?.task?.id);
|
|
280
|
-
if (fromDetail) return fromDetail;
|
|
281
|
-
return null;
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
function statusFromOrgEvent(event: any): string | null {
|
|
285
|
-
const detail = parseJsonSafe(event?.detail);
|
|
286
|
-
return (
|
|
287
|
-
normalizeA2AStatus(detail?.status) ??
|
|
288
|
-
normalizeA2AStatus(detail?.task?.status) ??
|
|
289
|
-
normalizeA2AStatus(event?.status) ??
|
|
290
|
-
normalizeA2AStatus(event?.kind) ??
|
|
291
|
-
inferStatusFromText(event?.summary) ??
|
|
292
|
-
null
|
|
293
|
-
);
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
export class A2AAdapter extends Resource {
|
|
297
|
-
async get() {
|
|
298
|
-
const host = process.env.FLAIR_PUBLIC_URL || "http://localhost:9926";
|
|
299
|
-
return new Response(JSON.stringify({
|
|
300
|
-
name: "TPS Agent Team",
|
|
301
|
-
description: "TPS — agent OS for humans and AI agents. Coordinates via Flair.",
|
|
302
|
-
url: `${host}/a2a`,
|
|
303
|
-
version: "0.1.0",
|
|
304
|
-
capabilities: {
|
|
305
|
-
streaming: true,
|
|
306
|
-
pushNotifications: false,
|
|
307
|
-
},
|
|
308
|
-
defaultInputModes: ["text"],
|
|
309
|
-
defaultOutputModes: ["text"],
|
|
310
|
-
skills: [
|
|
311
|
-
{
|
|
312
|
-
id: "task-management",
|
|
313
|
-
name: "Task Management",
|
|
314
|
-
description: "Create, list, and track tasks via Beads issue tracker",
|
|
315
|
-
},
|
|
316
|
-
{
|
|
317
|
-
id: "agent-coordination",
|
|
318
|
-
name: "Agent Coordination",
|
|
319
|
-
description: "Send messages to agents and coordinate work via OrgEvents",
|
|
320
|
-
},
|
|
321
|
-
],
|
|
322
|
-
}, null, 2), {
|
|
323
|
-
status: 200,
|
|
324
|
-
headers: { "Content-Type": "application/json" },
|
|
325
|
-
});
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
async post(content: any, _context?: any) {
|
|
329
|
-
const body: JsonRpcRequest = content as JsonRpcRequest;
|
|
330
|
-
|
|
331
|
-
if (!body || typeof body !== "object") {
|
|
332
|
-
return rpcError(null, -32600, "Invalid Request");
|
|
333
|
-
}
|
|
334
|
-
if (body.jsonrpc !== "2.0" || typeof body.method !== "string") {
|
|
335
|
-
return rpcError(body.id, -32600, "Invalid Request");
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
const id = body.id ?? null;
|
|
339
|
-
const params = body.params ?? {};
|
|
340
|
-
|
|
341
|
-
try {
|
|
342
|
-
if (body.method === "message/stream") {
|
|
343
|
-
const agentId = cleanText(params.agentId);
|
|
344
|
-
if (!agentId) {
|
|
345
|
-
return rpcError(id, -32602, "Invalid params: agentId is required");
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
const taskIdHint = cleanText(params.taskId) || null;
|
|
349
|
-
const encoder = new TextEncoder();
|
|
350
|
-
const startedAt = Date.now();
|
|
351
|
-
const timeoutMs = 5 * 60 * 1000;
|
|
352
|
-
let lastSeen = new Date(startedAt).toISOString();
|
|
353
|
-
let closed = false;
|
|
354
|
-
const seenEventIds = new Set<string>();
|
|
355
|
-
const seenEventQueue: string[] = [];
|
|
356
|
-
|
|
357
|
-
const stream = new ReadableStream<Uint8Array>({
|
|
358
|
-
start(controller) {
|
|
359
|
-
const writeEvent = (eventName: string, payload: any) => {
|
|
360
|
-
if (closed) return;
|
|
361
|
-
const frame = `event: ${eventName}\ndata: ${JSON.stringify(payload)}\n\n`;
|
|
362
|
-
controller.enqueue(encoder.encode(frame));
|
|
363
|
-
};
|
|
364
|
-
|
|
365
|
-
const closeStream = () => {
|
|
366
|
-
if (closed) return;
|
|
367
|
-
closed = true;
|
|
368
|
-
clearInterval(pollTimer);
|
|
369
|
-
clearTimeout(timeoutTimer);
|
|
370
|
-
controller.close();
|
|
371
|
-
};
|
|
372
|
-
|
|
373
|
-
const poll = async () => {
|
|
374
|
-
if (closed) return;
|
|
375
|
-
if (Date.now() - startedAt >= timeoutMs) { closeStream(); return; }
|
|
376
|
-
|
|
377
|
-
const catchupUrl =
|
|
378
|
-
`http://localhost:9926/OrgEventCatchup/${encodeURIComponent(agentId)}?since=${lastSeen}`;
|
|
379
|
-
|
|
380
|
-
let events: any[] = [];
|
|
381
|
-
try {
|
|
382
|
-
const response = await fetch(catchupUrl);
|
|
383
|
-
if (!response.ok) return;
|
|
384
|
-
const data = await response.json();
|
|
385
|
-
if (Array.isArray(data)) events = data;
|
|
386
|
-
else if (Array.isArray(data?.events)) events = data.events;
|
|
387
|
-
} catch { return; }
|
|
388
|
-
|
|
389
|
-
for (const event of events) {
|
|
390
|
-
const eventId = cleanText(event?.id) || `${cleanText(event?.createdAt)}:${cleanText(event?.summary)}`;
|
|
391
|
-
if (eventId && seenEventIds.has(eventId)) continue;
|
|
392
|
-
if (eventId) {
|
|
393
|
-
seenEventIds.add(eventId);
|
|
394
|
-
seenEventQueue.push(eventId);
|
|
395
|
-
if (seenEventQueue.length > 500) {
|
|
396
|
-
const removed = seenEventQueue.shift();
|
|
397
|
-
if (removed) seenEventIds.delete(removed);
|
|
398
|
-
}
|
|
399
|
-
}
|
|
400
|
-
const createdAt = cleanText(event?.createdAt);
|
|
401
|
-
if (createdAt && createdAt > lastSeen) lastSeen = createdAt;
|
|
402
|
-
|
|
403
|
-
const status = statusFromOrgEvent(event);
|
|
404
|
-
if (!status) continue;
|
|
405
|
-
|
|
406
|
-
writeEvent("task.status", rpcResult(id, {
|
|
407
|
-
type: "task",
|
|
408
|
-
task: { id: taskIdFromEvent(event) ?? taskIdHint, status },
|
|
409
|
-
}));
|
|
410
|
-
|
|
411
|
-
if (status === "completed" || status === "failed" || status === "canceled") {
|
|
412
|
-
closeStream(); return;
|
|
413
|
-
}
|
|
414
|
-
}
|
|
415
|
-
};
|
|
416
|
-
|
|
417
|
-
const pollTimer = setInterval(() => { void poll(); }, 2000);
|
|
418
|
-
const timeoutTimer = setTimeout(() => { closeStream(); }, timeoutMs);
|
|
419
|
-
void poll();
|
|
420
|
-
},
|
|
421
|
-
cancel() { closed = true; },
|
|
422
|
-
});
|
|
423
|
-
|
|
424
|
-
return new Response(stream, {
|
|
425
|
-
headers: { "Content-Type": "text/event-stream", "Cache-Control": "no-cache" },
|
|
426
|
-
});
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
if (body.method === "message/send") {
|
|
430
|
-
const agentId = cleanText(params.agentId);
|
|
431
|
-
const message = params.message;
|
|
432
|
-
if (!agentId || !message || typeof message !== "object") {
|
|
433
|
-
return rpcError(id, -32602, "Invalid params: agentId and message are required");
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
const agent = await (databases as any).flair.Agent.get(agentId).catch(() => null);
|
|
437
|
-
if (!agent) {
|
|
438
|
-
return rpcError(id, -32004, "Agent not found", { agentId });
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
const summary = truncate(firstTextPart(message) || "A2A message received", 200);
|
|
442
|
-
await publishOrgEvent({
|
|
443
|
-
kind: "a2a.message",
|
|
444
|
-
scope: agentId,
|
|
445
|
-
summary,
|
|
446
|
-
detail: JSON.stringify({ message }),
|
|
447
|
-
targetIds: [agentId],
|
|
448
|
-
});
|
|
449
|
-
|
|
450
|
-
return rpcResult(id, {
|
|
451
|
-
type: "message",
|
|
452
|
-
message: {
|
|
453
|
-
role: "agent",
|
|
454
|
-
parts: [{ text: "Message received. Task created." }],
|
|
455
|
-
},
|
|
456
|
-
});
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
if (body.method === "tasks/get") {
|
|
460
|
-
const taskId = cleanText(params.taskId);
|
|
461
|
-
if (!taskId) return rpcError(id, -32602, "Invalid params: taskId is required");
|
|
462
|
-
|
|
463
|
-
const issue = await readIssue(taskId);
|
|
464
|
-
if (!issue) return rpcError(id, -32004, "Task not found", { taskId });
|
|
465
|
-
|
|
466
|
-
const history = await taskHistory(taskId);
|
|
467
|
-
return rpcResult(id, {
|
|
468
|
-
type: "task",
|
|
469
|
-
task: {
|
|
470
|
-
...taskView(issue),
|
|
471
|
-
artifacts: artifactsFromIssue(issue),
|
|
472
|
-
history,
|
|
473
|
-
},
|
|
474
|
-
});
|
|
475
|
-
}
|
|
476
|
-
|
|
477
|
-
if (body.method === "tasks/list") {
|
|
478
|
-
const yamlIssues = await readIssuesFromYamlDir();
|
|
479
|
-
const issues = yamlIssues.length > 0 ? yamlIssues : await readIssuesFromJsonl();
|
|
480
|
-
|
|
481
|
-
const agentIdFilter = cleanText(params.agentId).toLowerCase();
|
|
482
|
-
const statusFilter = cleanText(params.status).toLowerCase();
|
|
483
|
-
|
|
484
|
-
const tasks = issues
|
|
485
|
-
.filter((issue) => {
|
|
486
|
-
if (agentIdFilter) {
|
|
487
|
-
const assignee = cleanText(issue.assignee).toLowerCase();
|
|
488
|
-
if (assignee !== agentIdFilter) return false;
|
|
489
|
-
}
|
|
490
|
-
if (statusFilter) {
|
|
491
|
-
const mapped = mapBeadsStatusToA2A(issue.status).toLowerCase();
|
|
492
|
-
if (mapped !== statusFilter) return false;
|
|
493
|
-
}
|
|
494
|
-
return true;
|
|
495
|
-
})
|
|
496
|
-
.map((issue) => taskView(issue))
|
|
497
|
-
.sort((a, b) => String(b.updatedAt ?? "").localeCompare(String(a.updatedAt ?? "")));
|
|
498
|
-
|
|
499
|
-
return rpcResult(id, { type: "tasks", tasks });
|
|
500
|
-
}
|
|
501
|
-
|
|
502
|
-
return rpcError(id, -32601, "Method not found");
|
|
503
|
-
} catch (error: any) {
|
|
504
|
-
return rpcError(id, -32000, "Server error", { detail: error?.message ?? String(error) });
|
|
505
|
-
}
|
|
506
|
-
}
|
|
507
|
-
}
|
|
508
|
-
|
|
509
|
-
// Expose exact lowercase endpoint required by A2A clients: POST /a2a
|
|
510
|
-
export class a2a extends A2AAdapter {}
|
package/resources/Agent.ts
DELETED
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
import { databases } from "@harperfast/harper";
|
|
2
|
-
|
|
3
|
-
export class Agent extends (databases as any).flair.Agent {
|
|
4
|
-
async post(content: any, context: any) {
|
|
5
|
-
content.type ||= "agent";
|
|
6
|
-
content.createdAt = new Date().toISOString();
|
|
7
|
-
content.updatedAt = content.createdAt;
|
|
8
|
-
return super.post(content, context);
|
|
9
|
-
}
|
|
10
|
-
}
|