Skip to content

Commit 8e3b07a

Browse files
authored
chore(release): v2026.3.7.1 (#9)
* chore(release): v2026.3.7.1 * feat: add mcp output schemas and cli introspection contracts
1 parent c8ddaf1 commit 8e3b07a

9 files changed

Lines changed: 650 additions & 36 deletions

File tree

SKILL.md

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -546,3 +546,69 @@ xint/
546546
└── references/
547547
└── x-api.md (X API endpoint reference)
548548
```
549+
550+
## Package API Tools
551+
552+
The Package API provides agent memory package management:
553+
554+
| Tool | Purpose | Auth |
555+
|------|---------|------|
556+
| `xint_package_create` | Create ingest job from topic query | XINT_PACKAGE_API_KEY |
557+
| `xint_package_status` | Get package metadata + freshness | XINT_PACKAGE_API_KEY |
558+
| `xint_package_query` | Query packages, return claims + citations | XINT_PACKAGE_API_KEY |
559+
| `xint_package_refresh` | Trigger new snapshot | XINT_PACKAGE_API_KEY |
560+
| `xint_package_search` | Search package catalog | XINT_PACKAGE_API_KEY |
561+
| `xint_package_publish` | Publish to shared catalog | XINT_PACKAGE_API_KEY |
562+
563+
**Workflow:**
564+
1. `xint_package_create` -> creates package with topic query + sources
565+
2. `xint_package_status` -> poll until status is "ready"
566+
3. `xint_package_query` -> retrieve claims with citations
567+
4. `xint_package_refresh` -> trigger re-ingest when data is stale
568+
5. `xint_package_publish` -> share to catalog when quality is confirmed
569+
570+
## Agent Patterns
571+
572+
### Token Budget Awareness
573+
- Use `--quick` flag for initial discovery (1 page, 1hr cache, noise filter)
574+
- Use `--fields id,text,metrics.likes` to reduce response size
575+
- Prefer `xint_search` with `limit: 5` for quick checks
576+
- Use `xint_costs` to check budget before expensive operations
577+
578+
### Batch Operations
579+
- Search + profile in sequence, not parallel (rate limit: 350ms between requests)
580+
- Use `xint_watch` for polling instead of repeated searches
581+
- Combine `xint_report` for topic intelligence instead of multiple searches
582+
583+
### Context Window Management
584+
- `xint_search` with limit=15: ~3KB response
585+
- `xint_profile` with count=20: ~4KB response
586+
- `xint_article`: 1-10KB depending on article length
587+
- `xint_trends`: ~2KB response
588+
- Use `--fields` flag to reduce output to only needed fields
589+
590+
## Error Recovery Matrix
591+
592+
| Error Code | Retryable | Agent Action | Example |
593+
|-----------|-----------|-------------|---------|
594+
| `RATE_LIMITED` | Yes | Wait `retry_after_ms`, then retry | 429 from X API |
595+
| `AUTH_FAILED` | No | Stop, report missing credential | Missing X_BEARER_TOKEN |
596+
| `NOT_FOUND` | No | Skip resource, try alternative | Deleted tweet |
597+
| `BUDGET_DENIED` | No | Stop, use `xint costs budget set N` | Daily limit exceeded |
598+
| `POLICY_DENIED` | No | Stop, escalate to user | Need --policy=engagement |
599+
| `VALIDATION_ERROR` | No | Fix parameter, retry | Invalid tweet_id format |
600+
| `TIMEOUT` | Yes | Retry after 5s | Network timeout |
601+
| `API_ERROR` | If 5xx | Retry after 30s for 5xx, stop for 4xx | X API outage |
602+
603+
## Fallback Chain
604+
605+
When a tool fails, try the next option:
606+
607+
1. `xint_search` (X API v2, fast, real-time)
608+
2. `xint_xsearch` (xAI Grok search, AI-enhanced, requires XAI_API_KEY)
609+
3. Cached results from previous searches (15min TTL)
610+
611+
For article fetching:
612+
1. `xint_article` with tweet URL (extracts inline X Article)
613+
2. `xint_article` with article URL (web fetch)
614+
3. `xint_search` for tweets about the topic

lib/action_result.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,23 @@
11
import type { ActionResultType } from "./actions";
2+
import type { XintError } from "./errors";
23

34
export type ActionExecutionKind = Extract<ActionResultType, "success" | "error" | "info">;
45

6+
export type Pagination = {
7+
total: number;
8+
returned: number;
9+
has_more: boolean;
10+
};
11+
512
export type ActionExecutionResult<T = unknown> = {
613
type: ActionExecutionKind;
714
message: string;
815
data?: T;
916
fallbackUsed: boolean;
17+
error?: XintError;
18+
cached?: boolean;
19+
cost?: number;
20+
pagination?: Pagination;
1021
};
1122

1223
export function actionSuccess<T>(message: string, data?: T, fallbackUsed = false): ActionExecutionResult<T> {
@@ -17,6 +28,6 @@ export function actionInfo<T>(message: string, data?: T, fallbackUsed = false):
1728
return { type: "info", message, data, fallbackUsed };
1829
}
1930

20-
export function actionError(message: string): ActionExecutionResult<never> {
21-
return { type: "error", message, fallbackUsed: false };
31+
export function actionError(message: string, error?: XintError): ActionExecutionResult<never> {
32+
return { type: "error", message, fallbackUsed: false, error };
2233
}

lib/capabilities.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,17 @@ export function getCapabilitiesManifest() {
6969
content_type: "application/json",
7070
supports_compact: true,
7171
},
72+
introspection: {
73+
describe: "xint <command> --describe",
74+
schema: "xint <command> --schema",
75+
fields: "--fields id,text,metrics.likes",
76+
dry_run: "--dry-run (mutation commands only)",
77+
},
78+
rate_limits: {
79+
requests_per_15min: 450,
80+
delay_between_requests_ms: 350,
81+
daily_budget_default_usd: 50,
82+
},
7283
constraints: {
7384
x_api_only: true,
7485
xai_grok_only: true,

lib/errors.ts

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
/**
2+
* Structured error types for agent-consumable error responses.
3+
*/
4+
5+
export type XintErrorCode =
6+
| "RATE_LIMITED"
7+
| "AUTH_FAILED"
8+
| "NOT_FOUND"
9+
| "BUDGET_DENIED"
10+
| "POLICY_DENIED"
11+
| "VALIDATION_ERROR"
12+
| "TIMEOUT"
13+
| "API_ERROR";
14+
15+
export interface XintError {
16+
code: XintErrorCode;
17+
message: string;
18+
retryable: boolean;
19+
retry_after_ms?: number;
20+
suggestion?: string;
21+
failing_input?: string;
22+
}
23+
24+
export function rateLimited(waitMs: number): XintError {
25+
return {
26+
code: "RATE_LIMITED",
27+
message: `Rate limited. Retry after ${waitMs}ms.`,
28+
retryable: true,
29+
retry_after_ms: waitMs,
30+
suggestion: `Wait ${waitMs}ms before retrying.`,
31+
};
32+
}
33+
34+
export function authFailed(detail?: string): XintError {
35+
return {
36+
code: "AUTH_FAILED",
37+
message: detail || "Authentication failed.",
38+
retryable: false,
39+
suggestion: "Set X_BEARER_TOKEN env var or run 'xint auth setup'.",
40+
};
41+
}
42+
43+
export function notFound(resource?: string): XintError {
44+
return {
45+
code: "NOT_FOUND",
46+
message: resource ? `Not found: ${resource}` : "Resource not found.",
47+
retryable: false,
48+
suggestion: "The tweet or user may have been deleted.",
49+
failing_input: resource,
50+
};
51+
}
52+
53+
export function budgetDenied(spent: number, limit: number, remaining: number): XintError {
54+
return {
55+
code: "BUDGET_DENIED",
56+
message: `Daily budget exceeded ($${spent.toFixed(2)} / $${limit.toFixed(2)}).`,
57+
retryable: false,
58+
suggestion: "Use 'xint costs budget set N' to increase the daily limit.",
59+
};
60+
}
61+
62+
export function policyDenied(tool: string, current: string, required: string): XintError {
63+
return {
64+
code: "POLICY_DENIED",
65+
message: `Tool '${tool}' requires '${required}' policy mode (current: '${current}').`,
66+
retryable: false,
67+
suggestion: `Start MCP with --policy=${required}.`,
68+
failing_input: tool,
69+
};
70+
}
71+
72+
export function validationError(param: string, reason: string): XintError {
73+
return {
74+
code: "VALIDATION_ERROR",
75+
message: `Parameter '${param}': ${reason}`,
76+
retryable: false,
77+
suggestion: `Fix parameter '${param}': ${reason}`,
78+
failing_input: param,
79+
};
80+
}
81+
82+
export function timeout(detail?: string): XintError {
83+
return {
84+
code: "TIMEOUT",
85+
message: detail || "Request timed out.",
86+
retryable: true,
87+
retry_after_ms: 5000,
88+
suggestion: "Retry in 5s.",
89+
};
90+
}
91+
92+
export function apiError(status: number, detail?: string): XintError {
93+
return {
94+
code: "API_ERROR",
95+
message: detail || `X API error (HTTP ${status}).`,
96+
retryable: status >= 500,
97+
retry_after_ms: status >= 500 ? 30000 : undefined,
98+
suggestion: status >= 500 ? "X API issues. Retry in 30s." : undefined,
99+
};
100+
}
101+
102+
/**
103+
* Classify an error from API calls into a structured XintError.
104+
* Parses error message patterns to detect HTTP status codes.
105+
*/
106+
export function classifyError(err: unknown): XintError {
107+
const msg = err instanceof Error ? err.message : String(err);
108+
109+
// Rate limited (429)
110+
const rateMatch = msg.match(/Rate limited.*?(\d+)s/i);
111+
if (rateMatch || msg.includes("429")) {
112+
const waitSec = rateMatch ? parseInt(rateMatch[1]) : 60;
113+
return rateLimited(waitSec * 1000);
114+
}
115+
116+
// Auth failed (401)
117+
if (msg.includes("401") || msg.includes("OAuth token rejected") || msg.includes("X_BEARER_TOKEN not found")) {
118+
return authFailed(msg);
119+
}
120+
121+
// Not found (404)
122+
if (msg.includes("404") || msg.includes("not found")) {
123+
return notFound();
124+
}
125+
126+
// Budget denied
127+
if (msg.includes("BUDGET_DENIED") || msg.includes("budget exceeded")) {
128+
try {
129+
const parsed = JSON.parse(msg);
130+
return budgetDenied(parsed.spent_usd || 0, parsed.limit_usd || 0, parsed.remaining_usd || 0);
131+
} catch {
132+
return budgetDenied(0, 0, 0);
133+
}
134+
}
135+
136+
// Policy denied
137+
if (msg.includes("POLICY_DENIED")) {
138+
try {
139+
const parsed = JSON.parse(msg);
140+
return policyDenied(parsed.message || "", parsed.policy_mode || "", parsed.required_mode || "");
141+
} catch {
142+
return policyDenied("unknown", "unknown", "unknown");
143+
}
144+
}
145+
146+
// Timeout
147+
if (msg.toLowerCase().includes("timeout") || msg.toLowerCase().includes("timed out")) {
148+
return timeout(msg);
149+
}
150+
151+
// Generic API error — try to extract status
152+
const statusMatch = msg.match(/X API (\d{3})/);
153+
if (statusMatch) {
154+
return apiError(parseInt(statusMatch[1]), msg);
155+
}
156+
157+
// Fallback
158+
return apiError(0, msg);
159+
}

lib/format.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -255,3 +255,46 @@ export function formatJsonl(tweets: Tweet[]): string {
255255
export function formatResultsJson(tweets: Tweet[]): string {
256256
return JSON.stringify(tweets, null, 2);
257257
}
258+
259+
/**
260+
* Filter objects to only include specified dot-separated field paths.
261+
* Usage: filterFields(data, "id,text,metrics.likes")
262+
*/
263+
export function filterFields<T>(data: T, fields: string): T {
264+
if (!data || typeof data !== "object") return data;
265+
const fieldPaths = fields.split(",").map(f => f.trim());
266+
267+
function pick(obj: Record<string, unknown>, paths: string[]): Record<string, unknown> {
268+
const result: Record<string, unknown> = {};
269+
for (const path of paths) {
270+
const parts = path.split(".");
271+
let current: unknown = obj;
272+
for (const part of parts) {
273+
if (current && typeof current === "object" && !Array.isArray(current)) {
274+
current = (current as Record<string, unknown>)[part];
275+
} else {
276+
current = undefined;
277+
break;
278+
}
279+
}
280+
if (current !== undefined) {
281+
let target = result;
282+
for (let i = 0; i < parts.length - 1; i++) {
283+
if (!target[parts[i]] || typeof target[parts[i]] !== "object") {
284+
target[parts[i]] = {};
285+
}
286+
target = target[parts[i]] as Record<string, unknown>;
287+
}
288+
target[parts[parts.length - 1]] = current;
289+
}
290+
}
291+
return result;
292+
}
293+
294+
if (Array.isArray(data)) {
295+
return data.map(item =>
296+
item && typeof item === "object" ? pick(item as Record<string, unknown>, fieldPaths) : item
297+
) as T;
298+
}
299+
return pick(data as Record<string, unknown>, fieldPaths) as T;
300+
}

0 commit comments

Comments
 (0)