@fusedframes/cli 0.1.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.
@@ -0,0 +1,2 @@
1
+ import { Command } from "commander";
2
+ export declare function registerConfigCommands(program: Command): void;
@@ -0,0 +1,40 @@
1
+ import { readConfig, writeConfig, getConfigInfo } from "../lib/config.js";
2
+ import { outputSuccess, outputError } from "../lib/output.js";
3
+ function readStdin() {
4
+ return new Promise((resolve, reject) => {
5
+ let data = "";
6
+ process.stdin.setEncoding("utf-8");
7
+ process.stdin.on("data", (chunk) => (data += chunk));
8
+ process.stdin.on("end", () => resolve(data.trim()));
9
+ process.stdin.on("error", reject);
10
+ // If stdin is a TTY (interactive terminal), prompt the user
11
+ if (process.stdin.isTTY) {
12
+ process.stderr.write("Enter API key: ");
13
+ }
14
+ });
15
+ }
16
+ export function registerConfigCommands(program) {
17
+ const config = program.command("config").description("Manage CLI configuration");
18
+ config
19
+ .command("set-key [apiKey]")
20
+ .description("Set the API key (reads from stdin if not provided)")
21
+ .action(async (apiKey) => {
22
+ let key = apiKey;
23
+ // If no argument provided, read from stdin
24
+ if (!key) {
25
+ key = await readStdin();
26
+ }
27
+ if (!key) {
28
+ outputError("validation_error", "No API key provided");
29
+ }
30
+ const existing = readConfig();
31
+ writeConfig({ ...existing, apiKey: key });
32
+ outputSuccess({ success: true, message: "API key saved" });
33
+ });
34
+ config
35
+ .command("show")
36
+ .description("Show current configuration")
37
+ .action(() => {
38
+ outputSuccess(getConfigInfo());
39
+ });
40
+ }
@@ -0,0 +1,2 @@
1
+ import { Command } from "commander";
2
+ export declare function registerGraphCommand(program: Command): void;
@@ -0,0 +1,11 @@
1
+ import { request } from "../lib/client.js";
2
+ import { outputSuccess } from "../lib/output.js";
3
+ export function registerGraphCommand(program) {
4
+ program
5
+ .command("graph <libraryId>")
6
+ .description("Get the full pattern graph for a library")
7
+ .action(async (libraryId) => {
8
+ const data = await request(`/libraries/${libraryId}/graph`);
9
+ outputSuccess(data);
10
+ });
11
+ }
@@ -0,0 +1,2 @@
1
+ import { Command } from "commander";
2
+ export declare function registerLibraryCommands(program: Command): void;
@@ -0,0 +1,42 @@
1
+ import { request } from "../lib/client.js";
2
+ import { outputSuccess } from "../lib/output.js";
3
+ export function registerLibraryCommands(program) {
4
+ const libraries = program
5
+ .command("libraries")
6
+ .description("Browse pattern libraries");
7
+ libraries
8
+ .command("list")
9
+ .description("List all accessible pattern libraries")
10
+ .action(async () => {
11
+ const data = await request("/libraries");
12
+ outputSuccess(data);
13
+ });
14
+ libraries
15
+ .command("get <id>")
16
+ .description("Get pattern library detail")
17
+ .action(async (id) => {
18
+ const data = await request(`/libraries/${id}`);
19
+ outputSuccess(data);
20
+ });
21
+ libraries
22
+ .command("categories <id>")
23
+ .description("List categories with pattern counts")
24
+ .action(async (id) => {
25
+ const data = await request(`/libraries/${id}/categories`);
26
+ outputSuccess(data);
27
+ });
28
+ libraries
29
+ .command("tags <id>")
30
+ .description("List tags with pattern counts")
31
+ .action(async (id) => {
32
+ const data = await request(`/libraries/${id}/tags`);
33
+ outputSuccess(data);
34
+ });
35
+ libraries
36
+ .command("applications <id>")
37
+ .description("List applications with pattern counts")
38
+ .action(async (id) => {
39
+ const data = await request(`/libraries/${id}/applications`);
40
+ outputSuccess(data);
41
+ });
42
+ }
@@ -0,0 +1,2 @@
1
+ import { Command } from "commander";
2
+ export declare function registerPatternCommands(program: Command): void;
@@ -0,0 +1,46 @@
1
+ import { request } from "../lib/client.js";
2
+ import { outputSuccess } from "../lib/output.js";
3
+ export function registerPatternCommands(program) {
4
+ const patterns = program
5
+ .command("patterns")
6
+ .description("Query patterns");
7
+ patterns
8
+ .command("list <libraryId>")
9
+ .description("List patterns in a library")
10
+ .option("--category <value>", "Filter by category")
11
+ .option("--tag <value>", "Filter by tag")
12
+ .option("--app <value>", "Filter by application")
13
+ .option("--search <value>", "Search term")
14
+ .option("--page <number>", "Page number", "1")
15
+ .option("--page-size <number>", "Results per page", "20")
16
+ .action(async (libraryId, opts) => {
17
+ const data = await request(`/libraries/${libraryId}/patterns`, {
18
+ category: opts.category,
19
+ tag: opts.tag,
20
+ application: opts.app,
21
+ search: opts.search,
22
+ page: opts.page,
23
+ pageSize: opts.pageSize,
24
+ });
25
+ outputSuccess(data);
26
+ });
27
+ patterns
28
+ .command("get <id>")
29
+ .description("Get full pattern detail with inline edges")
30
+ .action(async (id) => {
31
+ const data = await request(`/patterns/${id}`);
32
+ outputSuccess(data);
33
+ });
34
+ patterns
35
+ .command("evidence <id>")
36
+ .description("Get evidence actions for a pattern")
37
+ .option("--page <number>", "Page number", "1")
38
+ .option("--page-size <number>", "Results per page", "20")
39
+ .action(async (id, opts) => {
40
+ const data = await request(`/patterns/${id}/evidence`, {
41
+ page: opts.page,
42
+ pageSize: opts.pageSize,
43
+ });
44
+ outputSuccess(data);
45
+ });
46
+ }
@@ -0,0 +1,2 @@
1
+ import { Command } from "commander";
2
+ export declare function registerSearchCommand(program: Command): void;
@@ -0,0 +1,23 @@
1
+ import { request } from "../lib/client.js";
2
+ import { outputSuccess } from "../lib/output.js";
3
+ export function registerSearchCommand(program) {
4
+ program
5
+ .command("search <query>")
6
+ .description("Search patterns across all accessible libraries")
7
+ .option("--category <value>", "Filter by category")
8
+ .option("--tag <value>", "Filter by tag")
9
+ .option("--library <value>", "Filter by library ID")
10
+ .option("--page <number>", "Page number", "1")
11
+ .option("--page-size <number>", "Results per page", "20")
12
+ .action(async (query, opts) => {
13
+ const data = await request("/search/patterns", {
14
+ q: query,
15
+ category: opts.category,
16
+ tag: opts.tag,
17
+ libraryId: opts.library,
18
+ page: opts.page,
19
+ pageSize: opts.pageSize,
20
+ });
21
+ outputSuccess(data);
22
+ });
23
+ }
@@ -0,0 +1,2 @@
1
+ import { Command } from "commander";
2
+ export declare function registerTraverseCommand(program: Command): void;
@@ -0,0 +1,18 @@
1
+ import { request } from "../lib/client.js";
2
+ import { outputSuccess } from "../lib/output.js";
3
+ export function registerTraverseCommand(program) {
4
+ program
5
+ .command("traverse <patternId>")
6
+ .description("Traverse edges from a pattern")
7
+ .option("--direction <value>", "Traversal direction (outgoing, incoming, both)", "both")
8
+ .option("--label <value>", "Filter by edge label")
9
+ .option("--depth <number>", "Traversal depth (1-3)", "1")
10
+ .action(async (patternId, opts) => {
11
+ const data = await request(`/patterns/${patternId}/traverse`, {
12
+ direction: opts.direction,
13
+ label: opts.label,
14
+ depth: opts.depth,
15
+ });
16
+ outputSuccess(data);
17
+ });
18
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,55 @@
1
+ #!/usr/bin/env node
2
+ import { Command } from "commander";
3
+ import { registerConfigCommands } from "./commands/config.js";
4
+ import { registerLibraryCommands } from "./commands/libraries.js";
5
+ import { registerPatternCommands } from "./commands/patterns.js";
6
+ import { registerGraphCommand } from "./commands/graph.js";
7
+ import { registerTraverseCommand } from "./commands/traverse.js";
8
+ import { registerSearchCommand } from "./commands/search.js";
9
+ import { outputError } from "./lib/output.js";
10
+ import { FusedFramesError } from "./lib/client.js";
11
+ const program = new Command();
12
+ program
13
+ .name("fusedframes")
14
+ .description("Query FusedFrames behavioural patterns")
15
+ .version("0.1.0");
16
+ // Register all command groups
17
+ registerConfigCommands(program);
18
+ registerLibraryCommands(program);
19
+ registerPatternCommands(program);
20
+ registerGraphCommand(program);
21
+ registerTraverseCommand(program);
22
+ registerSearchCommand(program);
23
+ // Override commander to throw instead of exit, but keep help/version output
24
+ program.exitOverride();
25
+ // Global error handler
26
+ async function main() {
27
+ try {
28
+ await program.parseAsync(process.argv);
29
+ }
30
+ catch (error) {
31
+ if (error instanceof FusedFramesError) {
32
+ outputError(error.code, error.message);
33
+ }
34
+ else if (error instanceof Error) {
35
+ // Commander exits with code 'commander.helpDisplayed' or 'commander.version'
36
+ const commanderError = error;
37
+ if (commanderError.code === "commander.helpDisplayed" ||
38
+ commanderError.code === "commander.version") {
39
+ // These are expected — commander already wrote output
40
+ process.exit(0);
41
+ }
42
+ if (commanderError.code === "commander.missingArgument" ||
43
+ commanderError.code === "commander.unknownCommand" ||
44
+ commanderError.code === "commander.unknownOption" ||
45
+ commanderError.code === "commander.missingMandatoryOptionValue") {
46
+ outputError("validation_error", error.message);
47
+ }
48
+ outputError("error", error.message);
49
+ }
50
+ else {
51
+ outputError("error", "An unexpected error occurred");
52
+ }
53
+ }
54
+ }
55
+ main();
@@ -0,0 +1,6 @@
1
+ export declare class FusedFramesError extends Error {
2
+ code: string;
3
+ status: number;
4
+ constructor(code: string, message: string, status: number);
5
+ }
6
+ export declare function request<T>(path: string, params?: Record<string, string | undefined>): Promise<T>;
@@ -0,0 +1,47 @@
1
+ import { requireApiKey, getApiUrl } from "./config.js";
2
+ export class FusedFramesError extends Error {
3
+ code;
4
+ status;
5
+ constructor(code, message, status) {
6
+ super(message);
7
+ this.code = code;
8
+ this.status = status;
9
+ }
10
+ }
11
+ export async function request(path, params) {
12
+ const apiKey = requireApiKey();
13
+ const baseUrl = getApiUrl();
14
+ // Enforce HTTPS (allow http://localhost for dev)
15
+ if (!baseUrl.startsWith("https://") && !baseUrl.startsWith("http://localhost")) {
16
+ throw new FusedFramesError("config_error", "API URL must use HTTPS. API keys cannot be sent over unencrypted connections.", 0);
17
+ }
18
+ // Build URL with query params
19
+ const url = new URL(path, baseUrl);
20
+ if (params) {
21
+ for (const [key, value] of Object.entries(params)) {
22
+ if (value !== undefined && value !== "") {
23
+ url.searchParams.set(key, value);
24
+ }
25
+ }
26
+ }
27
+ const response = await fetch(url.toString(), {
28
+ method: "GET",
29
+ headers: {
30
+ Authorization: `Bearer ${apiKey}`,
31
+ Accept: "application/json",
32
+ "User-Agent": "@fusedframes/cli/0.1.0",
33
+ },
34
+ signal: AbortSignal.timeout(30_000),
35
+ });
36
+ if (!response.ok) {
37
+ let errorBody;
38
+ try {
39
+ errorBody = (await response.json());
40
+ }
41
+ catch {
42
+ throw new FusedFramesError("server_error", `HTTP ${response.status}`, response.status);
43
+ }
44
+ throw new FusedFramesError(errorBody.error?.code || "unknown", errorBody.error?.message || `HTTP ${response.status}`, response.status);
45
+ }
46
+ return (await response.json());
47
+ }
@@ -0,0 +1,16 @@
1
+ interface Config {
2
+ apiKey?: string;
3
+ }
4
+ export declare function readConfig(): Config;
5
+ export declare function writeConfig(config: Config): void;
6
+ export declare function getApiKey(): string | undefined;
7
+ export declare function getApiUrl(): string;
8
+ export declare function requireApiKey(): string;
9
+ export declare function getConfigInfo(): {
10
+ apiKey: string | null;
11
+ apiKeySource: string;
12
+ apiUrl: string;
13
+ apiUrlSource: string;
14
+ configPath: string;
15
+ };
16
+ export {};
@@ -0,0 +1,58 @@
1
+ import { mkdirSync, readFileSync, writeFileSync } from "fs";
2
+ import { homedir } from "os";
3
+ import { join } from "path";
4
+ function getConfigDir() {
5
+ return join(homedir(), ".config", "fusedframes");
6
+ }
7
+ function getConfigPath() {
8
+ return join(getConfigDir(), "config.json");
9
+ }
10
+ export function readConfig() {
11
+ try {
12
+ const data = readFileSync(getConfigPath(), "utf-8");
13
+ return JSON.parse(data);
14
+ }
15
+ catch {
16
+ return {};
17
+ }
18
+ }
19
+ export function writeConfig(config) {
20
+ const dir = getConfigDir();
21
+ mkdirSync(dir, { recursive: true, mode: 0o700 });
22
+ const path = getConfigPath();
23
+ writeFileSync(path, JSON.stringify(config, null, 2) + "\n", {
24
+ mode: 0o600,
25
+ });
26
+ }
27
+ export function getApiKey() {
28
+ // Env var takes precedence
29
+ if (process.env.FUSEDFRAMES_API_KEY) {
30
+ return process.env.FUSEDFRAMES_API_KEY;
31
+ }
32
+ return readConfig().apiKey;
33
+ }
34
+ export function getApiUrl() {
35
+ return process.env.FUSEDFRAMES_API_URL || "https://api.fusedframes.com";
36
+ }
37
+ export function requireApiKey() {
38
+ const key = getApiKey();
39
+ if (!key) {
40
+ throw new Error("API key not configured. Run: fusedframes config set-key <api-key>");
41
+ }
42
+ return key;
43
+ }
44
+ export function getConfigInfo() {
45
+ const envKey = process.env.FUSEDFRAMES_API_KEY;
46
+ const fileConfig = readConfig();
47
+ return {
48
+ apiKey: envKey
49
+ ? `${envKey.slice(0, 8)}...`
50
+ : fileConfig.apiKey
51
+ ? `${fileConfig.apiKey.slice(0, 8)}...`
52
+ : null,
53
+ apiKeySource: envKey ? "environment" : fileConfig.apiKey ? "config" : "none",
54
+ apiUrl: getApiUrl(),
55
+ apiUrlSource: process.env.FUSEDFRAMES_API_URL ? "environment" : "default",
56
+ configPath: getConfigPath(),
57
+ };
58
+ }
@@ -0,0 +1,2 @@
1
+ export declare function outputSuccess(data: unknown): void;
2
+ export declare function outputError(code: string, message: string): never;
@@ -0,0 +1,7 @@
1
+ export function outputSuccess(data) {
2
+ process.stdout.write(JSON.stringify(data) + "\n");
3
+ }
4
+ export function outputError(code, message) {
5
+ process.stdout.write(JSON.stringify({ error: { code, message } }) + "\n");
6
+ process.exit(1);
7
+ }
@@ -0,0 +1,101 @@
1
+ export interface LibrarySummary {
2
+ id: string;
3
+ name: string;
4
+ description: string | null;
5
+ categories: string[];
6
+ tags: string[];
7
+ patternCount: number;
8
+ createdAt: string;
9
+ updatedAt: string;
10
+ }
11
+ export interface LibraryDetail extends LibrarySummary {
12
+ edgeCount: number;
13
+ }
14
+ export interface CategoryCount {
15
+ name: string;
16
+ patternCount: number;
17
+ }
18
+ export interface TagCount {
19
+ name: string;
20
+ patternCount: number;
21
+ }
22
+ export interface ApplicationCount {
23
+ name: string;
24
+ patternCount: number;
25
+ }
26
+ export interface PatternSummary {
27
+ id: string;
28
+ title: string;
29
+ behaviour: string;
30
+ reasoning: string;
31
+ trigger: string;
32
+ outcome: string;
33
+ category: string;
34
+ tags: string[];
35
+ applications: string | null;
36
+ actionCount: number;
37
+ connectionCount: number;
38
+ firstSeen: string | null;
39
+ lastSeen: string | null;
40
+ createdAt: string;
41
+ updatedAt: string;
42
+ }
43
+ export interface PatternDetail extends PatternSummary {
44
+ library: {
45
+ id: string;
46
+ name: string;
47
+ };
48
+ edges: {
49
+ outgoing: {
50
+ id: string;
51
+ targetPatternId: string;
52
+ targetPatternTitle: string;
53
+ label: string;
54
+ actionCount: number;
55
+ }[];
56
+ incoming: {
57
+ id: string;
58
+ sourcePatternId: string;
59
+ sourcePatternTitle: string;
60
+ label: string;
61
+ actionCount: number;
62
+ }[];
63
+ };
64
+ }
65
+ export interface EvidenceAction {
66
+ id: string;
67
+ question: string;
68
+ response: string | null;
69
+ createdAt: string;
70
+ events: string[];
71
+ }
72
+ export interface GraphEdge {
73
+ id: string;
74
+ sourcePatternId: string;
75
+ targetPatternId: string;
76
+ label: string;
77
+ actionCount: number;
78
+ }
79
+ export interface TraverseNode {
80
+ id: string;
81
+ title: string;
82
+ behaviour: string;
83
+ category: string;
84
+ applications: string | null;
85
+ actionCount: number;
86
+ depth: number;
87
+ }
88
+ export interface SearchPattern {
89
+ id: string;
90
+ title: string;
91
+ behaviour: string;
92
+ category: string;
93
+ tags: string[];
94
+ applications: string | null;
95
+ actionCount: number;
96
+ connectionCount: number;
97
+ library: {
98
+ id: string;
99
+ name: string;
100
+ };
101
+ }
@@ -0,0 +1,2 @@
1
+ // Response types from the FusedFrames API
2
+ export {};
package/package.json ADDED
@@ -0,0 +1,34 @@
1
+ {
2
+ "name": "@fusedframes/cli",
3
+ "version": "0.1.0",
4
+ "description": "CLI for querying FusedFrames behavioural patterns from AI agents",
5
+ "type": "module",
6
+ "bin": {
7
+ "fusedframes": "dist/index.js"
8
+ },
9
+ "files": [
10
+ "dist"
11
+ ],
12
+ "engines": {
13
+ "node": ">=18.0.0"
14
+ },
15
+ "scripts": {
16
+ "build": "tsc && chmod +x dist/index.js",
17
+ "prepublishOnly": "npm run build",
18
+ "typecheck": "tsc --noEmit"
19
+ },
20
+ "dependencies": {
21
+ "commander": "^13.0.0"
22
+ },
23
+ "devDependencies": {
24
+ "typescript": "^5.9.0",
25
+ "@types/node": "^22.0.0"
26
+ },
27
+ "keywords": [
28
+ "fusedframes",
29
+ "cli",
30
+ "patterns",
31
+ "ai-agent"
32
+ ],
33
+ "license": "MIT"
34
+ }