@meshxdata/fops 0.0.1

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.
Files changed (57) hide show
  1. package/README.md +98 -0
  2. package/STRUCTURE.md +43 -0
  3. package/foundation.mjs +16 -0
  4. package/package.json +52 -0
  5. package/src/agent/agent.js +367 -0
  6. package/src/agent/agent.test.js +233 -0
  7. package/src/agent/context.js +143 -0
  8. package/src/agent/context.test.js +81 -0
  9. package/src/agent/index.js +2 -0
  10. package/src/agent/llm.js +127 -0
  11. package/src/agent/llm.test.js +139 -0
  12. package/src/auth/index.js +4 -0
  13. package/src/auth/keychain.js +58 -0
  14. package/src/auth/keychain.test.js +185 -0
  15. package/src/auth/login.js +421 -0
  16. package/src/auth/login.test.js +192 -0
  17. package/src/auth/oauth.js +203 -0
  18. package/src/auth/oauth.test.js +118 -0
  19. package/src/auth/resolve.js +78 -0
  20. package/src/auth/resolve.test.js +153 -0
  21. package/src/commands/index.js +268 -0
  22. package/src/config.js +24 -0
  23. package/src/config.test.js +70 -0
  24. package/src/doctor.js +487 -0
  25. package/src/doctor.test.js +134 -0
  26. package/src/plugins/api.js +37 -0
  27. package/src/plugins/api.test.js +95 -0
  28. package/src/plugins/discovery.js +78 -0
  29. package/src/plugins/discovery.test.js +92 -0
  30. package/src/plugins/hooks.js +13 -0
  31. package/src/plugins/hooks.test.js +118 -0
  32. package/src/plugins/index.js +3 -0
  33. package/src/plugins/loader.js +110 -0
  34. package/src/plugins/manifest.js +26 -0
  35. package/src/plugins/manifest.test.js +106 -0
  36. package/src/plugins/registry.js +14 -0
  37. package/src/plugins/registry.test.js +43 -0
  38. package/src/plugins/skills.js +126 -0
  39. package/src/plugins/skills.test.js +173 -0
  40. package/src/project.js +61 -0
  41. package/src/project.test.js +196 -0
  42. package/src/setup/aws.js +369 -0
  43. package/src/setup/aws.test.js +280 -0
  44. package/src/setup/index.js +3 -0
  45. package/src/setup/setup.js +161 -0
  46. package/src/setup/wizard.js +119 -0
  47. package/src/shell.js +9 -0
  48. package/src/shell.test.js +72 -0
  49. package/src/skills/foundation/SKILL.md +107 -0
  50. package/src/ui/banner.js +56 -0
  51. package/src/ui/banner.test.js +97 -0
  52. package/src/ui/confirm.js +97 -0
  53. package/src/ui/index.js +5 -0
  54. package/src/ui/input.js +199 -0
  55. package/src/ui/spinner.js +170 -0
  56. package/src/ui/spinner.test.js +29 -0
  57. package/src/ui/streaming.js +106 -0
@@ -0,0 +1,97 @@
1
+ import React, { useState } from "react";
2
+ import { render, Box, Text, useInput } from "ink";
3
+
4
+ const h = React.createElement;
5
+
6
+ /**
7
+ * Select prompt component (arrow-key list picker like Claude Code)
8
+ */
9
+ export function SelectPrompt({ message, options, onResult }) {
10
+ const [cursor, setCursor] = useState(0);
11
+ const [done, setDone] = useState(false);
12
+
13
+ useInput((ch, key) => {
14
+ if (done) return;
15
+
16
+ if (key.upArrow) {
17
+ setCursor((c) => (c <= 0 ? options.length - 1 : c - 1));
18
+ return;
19
+ }
20
+ if (key.downArrow) {
21
+ setCursor((c) => (c >= options.length - 1 ? 0 : c + 1));
22
+ return;
23
+ }
24
+
25
+ if (key.return) {
26
+ setDone(true);
27
+ onResult(options[cursor]);
28
+ return;
29
+ }
30
+
31
+ if (key.escape || (key.ctrl && ch === "c")) {
32
+ setDone(true);
33
+ onResult(null);
34
+ return;
35
+ }
36
+ });
37
+
38
+ if (done) {
39
+ return h(Box, null,
40
+ h(Text, { color: "green" }, "? "),
41
+ h(Text, null, message + " "),
42
+ h(Text, { color: "cyan" }, options[cursor]?.label || "")
43
+ );
44
+ }
45
+
46
+ return h(Box, { flexDirection: "column" },
47
+ h(Box, null,
48
+ h(Text, { color: "green" }, "? "),
49
+ h(Text, { bold: true }, message)
50
+ ),
51
+ ...options.map((opt, i) =>
52
+ h(Box, { key: i },
53
+ h(Text, { color: i === cursor ? "cyan" : "gray" },
54
+ ` ${i === cursor ? "❯" : " "} ${opt.label}`
55
+ )
56
+ )
57
+ )
58
+ );
59
+ }
60
+
61
+ /**
62
+ * Show a select option picker.
63
+ * options: Array of { label, value } or strings
64
+ * Returns selected option's value, or null on escape.
65
+ */
66
+ export async function selectOption(message, options) {
67
+ const normalized = options.map((o) =>
68
+ typeof o === "string" ? { label: o, value: o } : o
69
+ );
70
+
71
+ return new Promise((resolve) => {
72
+ let resolved = false;
73
+ const onResult = (selected) => {
74
+ if (resolved) return;
75
+ resolved = true;
76
+ clear();
77
+ unmount();
78
+ setTimeout(() => resolve(selected ? selected.value : null), 50);
79
+ };
80
+ const { unmount, clear } = render(
81
+ h(SelectPrompt, { message, options: normalized, onResult })
82
+ );
83
+ });
84
+ }
85
+
86
+ /**
87
+ * Show a confirm prompt (Yes/No picker).
88
+ * Returns true/false.
89
+ */
90
+ export async function confirm(message, defaultValue = false) {
91
+ const options = defaultValue
92
+ ? [{ label: "Yes", value: true }, { label: "No", value: false }]
93
+ : [{ label: "No", value: false }, { label: "Yes", value: true }];
94
+
95
+ const result = await selectOption(message, options);
96
+ return result === null ? defaultValue : result;
97
+ }
@@ -0,0 +1,5 @@
1
+ export { BANNER, QUOTES, renderBanner, getRandomQuote } from "./banner.js";
2
+ export { ThinkingSpinner, renderSpinner, renderThinking, VERBS } from "./spinner.js";
3
+ export { ResponseBox, renderResponse, StreamingResponse, renderStreaming } from "./streaming.js";
4
+ export { InputBox, StandaloneInput, promptInput, clearInputHistory } from "./input.js";
5
+ export { SelectPrompt, selectOption, confirm } from "./confirm.js";
@@ -0,0 +1,199 @@
1
+ import React, { useState, useEffect } from "react";
2
+ import { render, Box, Text, useInput, useApp } from "ink";
3
+
4
+ const h = React.createElement;
5
+
6
+ /**
7
+ * Input box with history support
8
+ */
9
+ export function InputBox({ onSubmit, onExit, history = [], placeholder = "Type a message..." }) {
10
+ const [input, setInput] = useState("");
11
+ const [historyIndex, setHistoryIndex] = useState(-1);
12
+ const [cursorVisible, setCursorVisible] = useState(true);
13
+ const { exit } = useApp();
14
+
15
+ // Blinking cursor effect
16
+ useEffect(() => {
17
+ const interval = setInterval(() => {
18
+ setCursorVisible(v => !v);
19
+ }, 500);
20
+ return () => clearInterval(interval);
21
+ }, []);
22
+
23
+ useInput((ch, key) => {
24
+ if (key.return) {
25
+ if (input.trim()) {
26
+ onSubmit(input.trim());
27
+ setInput("");
28
+ setHistoryIndex(-1);
29
+ }
30
+ return;
31
+ }
32
+
33
+ if (key.escape || (key.ctrl && ch === "c")) {
34
+ if (onExit) onExit();
35
+ exit();
36
+ return;
37
+ }
38
+
39
+ if (key.upArrow) {
40
+ if (history.length > 0 && historyIndex < history.length - 1) {
41
+ const newIndex = historyIndex + 1;
42
+ setHistoryIndex(newIndex);
43
+ setInput(history[history.length - 1 - newIndex] || "");
44
+ }
45
+ return;
46
+ }
47
+
48
+ if (key.downArrow) {
49
+ if (historyIndex > 0) {
50
+ const newIndex = historyIndex - 1;
51
+ setHistoryIndex(newIndex);
52
+ setInput(history[history.length - 1 - newIndex] || "");
53
+ } else if (historyIndex === 0) {
54
+ setHistoryIndex(-1);
55
+ setInput("");
56
+ }
57
+ return;
58
+ }
59
+
60
+ if (key.backspace || key.delete) {
61
+ setInput(input.slice(0, -1));
62
+ setHistoryIndex(-1);
63
+ return;
64
+ }
65
+
66
+ if (ch && !key.ctrl && !key.meta) {
67
+ setInput(input + ch);
68
+ setHistoryIndex(-1);
69
+ }
70
+ });
71
+
72
+ const cursor = cursorVisible ? "▋" : " ";
73
+ const displayText = input || "";
74
+
75
+ return h(Box, {
76
+ flexDirection: "column",
77
+ borderStyle: "round",
78
+ borderColor: "cyan",
79
+ paddingX: 1,
80
+ marginTop: 1,
81
+ },
82
+ h(Box, null,
83
+ h(Text, { color: "cyan", bold: true }, "❯ "),
84
+ h(Text, null, displayText),
85
+ h(Text, { color: "cyan" }, cursor)
86
+ ),
87
+ !input && h(Text, { dimColor: true }, placeholder)
88
+ );
89
+ }
90
+
91
+ // State for standalone input
92
+ let inputState = { resolve: null, history: [] };
93
+
94
+ export function StandaloneInput({ placeholder, onResult }) {
95
+ const [input, setInput] = useState("");
96
+ const [historyIndex, setHistoryIndex] = useState(-1);
97
+ const [cursorVisible, setCursorVisible] = useState(true);
98
+ const [done, setDone] = useState(false);
99
+
100
+ useEffect(() => {
101
+ const interval = setInterval(() => setCursorVisible(v => !v), 500);
102
+ return () => clearInterval(interval);
103
+ }, []);
104
+
105
+ useInput((ch, key) => {
106
+ if (done) return;
107
+
108
+ if (key.return && input.trim()) {
109
+ inputState.history.push(input.trim());
110
+ setDone(true);
111
+ onResult(input.trim());
112
+ return;
113
+ }
114
+
115
+ if (key.escape || (key.ctrl && ch === "c")) {
116
+ setDone(true);
117
+ onResult(null);
118
+ return;
119
+ }
120
+
121
+ if (key.upArrow && inputState.history.length > 0) {
122
+ if (historyIndex < inputState.history.length - 1) {
123
+ const idx = historyIndex + 1;
124
+ setHistoryIndex(idx);
125
+ setInput(inputState.history[inputState.history.length - 1 - idx] || "");
126
+ }
127
+ return;
128
+ }
129
+
130
+ if (key.downArrow) {
131
+ if (historyIndex > 0) {
132
+ const idx = historyIndex - 1;
133
+ setHistoryIndex(idx);
134
+ setInput(inputState.history[inputState.history.length - 1 - idx] || "");
135
+ } else if (historyIndex === 0) {
136
+ setHistoryIndex(-1);
137
+ setInput("");
138
+ }
139
+ return;
140
+ }
141
+
142
+ if (key.backspace || key.delete) {
143
+ setInput(i => i.slice(0, -1));
144
+ setHistoryIndex(-1);
145
+ return;
146
+ }
147
+
148
+ if (ch && !key.ctrl && !key.meta) {
149
+ setInput(i => i + ch);
150
+ setHistoryIndex(-1);
151
+ }
152
+ });
153
+
154
+ return h(Box, {
155
+ flexDirection: "column",
156
+ borderStyle: "round",
157
+ borderColor: "cyan",
158
+ paddingX: 1,
159
+ },
160
+ h(Box, null,
161
+ h(Text, { color: "cyan", bold: true }, "❯ "),
162
+ h(Text, null, input),
163
+ h(Text, { color: "cyan" }, cursorVisible ? "▋" : " ")
164
+ ),
165
+ !input && placeholder && h(Text, { dimColor: true }, placeholder)
166
+ );
167
+ }
168
+
169
+ /**
170
+ * Prompt for input with history support
171
+ * Returns the input string or null if cancelled
172
+ */
173
+ export async function promptInput(placeholder = "Type a message... (↑↓ history, esc to exit)") {
174
+ return new Promise((resolve) => {
175
+ let resolved = false;
176
+ let userInput = null;
177
+ const onResult = (result) => {
178
+ if (resolved) return;
179
+ resolved = true;
180
+ userInput = result;
181
+ clear();
182
+ unmount();
183
+ // Echo what user typed and reset terminal
184
+ process.stdout.write("\x1b[0m\n");
185
+ if (result) {
186
+ console.log("\x1b[36m❯\x1b[0m " + result + "\n");
187
+ }
188
+ setTimeout(() => resolve(result), 50);
189
+ };
190
+ const { unmount, clear } = render(h(StandaloneInput, { placeholder, onResult }));
191
+ });
192
+ }
193
+
194
+ /**
195
+ * Clear input history
196
+ */
197
+ export function clearInputHistory() {
198
+ inputState.history = [];
199
+ }
@@ -0,0 +1,170 @@
1
+ import React, { useState, useEffect } from "react";
2
+ import { render, Box, Text } from "ink";
3
+ import chalk from "chalk";
4
+
5
+ const h = React.createElement;
6
+
7
+ const SPARKLE_FRAMES = ["✻", "✼", "✻", "✦"];
8
+ const SPARKLE_INTERVAL = 120;
9
+ const INTENT_LINE = chalk.cyan("⏺") + chalk.gray(" Thinking...");
10
+
11
+ // Claude-style verbs for the spinner
12
+ export const VERBS = [
13
+ "Hacking the mainframe",
14
+ "Decrypting signals",
15
+ "Tracing packets",
16
+ "Scanning ports",
17
+ "Parsing logs",
18
+ "Interrogating containers",
19
+ "Brute-forcing a solution",
20
+ "Reverse engineering",
21
+ "Infiltrating the stack",
22
+ "Compiling intel",
23
+ "Sniffing traffic",
24
+ "Cracking the cipher",
25
+ "Exfiltrating data",
26
+ "Pivoting laterally",
27
+ "Enumerating services",
28
+ ];
29
+
30
+ function getRandomVerb() {
31
+ return VERBS[Math.floor(Math.random() * VERBS.length)];
32
+ }
33
+
34
+ /**
35
+ * Spinner component with rotating verbs
36
+ */
37
+ export function ThinkingSpinner({ message }) {
38
+ const [verb, setVerb] = useState(getRandomVerb());
39
+ const [frame, setFrame] = useState(0);
40
+
41
+ useEffect(() => {
42
+ const interval = setInterval(() => {
43
+ setVerb(getRandomVerb());
44
+ }, 2000);
45
+ return () => clearInterval(interval);
46
+ }, []);
47
+
48
+ useEffect(() => {
49
+ const interval = setInterval(() => {
50
+ setFrame((f) => (f + 1) % SPARKLE_FRAMES.length);
51
+ }, SPARKLE_INTERVAL);
52
+ return () => clearInterval(interval);
53
+ }, []);
54
+
55
+ return h(Box, { flexDirection: "column" },
56
+ h(Box, null,
57
+ h(Text, { color: "cyan" }, "⏺"),
58
+ h(Text, { color: "gray" }, " Thinking...")
59
+ ),
60
+ h(Box, null,
61
+ h(Text, { color: "magenta" }, SPARKLE_FRAMES[frame]),
62
+ h(Text, { color: "gray" }, ` ${message || `${verb}…`} `),
63
+ h(Text, { dimColor: true }, "(esc to interrupt)")
64
+ )
65
+ );
66
+ }
67
+
68
+ /**
69
+ * Render the thinking spinner
70
+ * Returns a function to update/stop it
71
+ */
72
+ export function renderSpinner(message) {
73
+ const { rerender, unmount, clear } = render(
74
+ h(ThinkingSpinner, { message })
75
+ );
76
+
77
+ return {
78
+ update: (newMessage) => {
79
+ rerender(h(ThinkingSpinner, { message: newMessage }));
80
+ },
81
+ stop: () => {
82
+ clear();
83
+ unmount();
84
+ // Re-print intent line as static text so it persists after Ink clears
85
+ console.log(INTENT_LINE);
86
+ },
87
+ };
88
+ }
89
+
90
+ /**
91
+ * Thinking display component - shows what the agent is doing
92
+ */
93
+ function ThinkingDisplay({ status, detail, content }) {
94
+ const [verb, setVerb] = useState(getRandomVerb());
95
+ const [frame, setFrame] = useState(0);
96
+
97
+ useEffect(() => {
98
+ const interval = setInterval(() => setVerb(getRandomVerb()), 2000);
99
+ return () => clearInterval(interval);
100
+ }, []);
101
+
102
+ useEffect(() => {
103
+ const interval = setInterval(() => {
104
+ setFrame((f) => (f + 1) % SPARKLE_FRAMES.length);
105
+ }, SPARKLE_INTERVAL);
106
+ return () => clearInterval(interval);
107
+ }, []);
108
+
109
+ return h(Box, { flexDirection: "column" },
110
+ // Status line with spinner
111
+ h(Box, null,
112
+ h(Text, { color: "magenta" }, SPARKLE_FRAMES[frame]),
113
+ h(Text, { color: "yellow" }, ` ${status || verb}... `),
114
+ detail && h(Text, { dimColor: true }, detail)
115
+ ),
116
+ // Content preview (truncated)
117
+ content && h(Box, { marginTop: 1, marginLeft: 2 },
118
+ h(Text, { dimColor: true },
119
+ content.length > 100 ? content.slice(0, 100) + "..." : content
120
+ )
121
+ )
122
+ );
123
+ }
124
+
125
+ // State for thinking display
126
+ let thinkingState = { status: "", detail: "", content: "", rerender: null };
127
+
128
+ /**
129
+ * Render thinking display
130
+ * Returns controls to update status and content
131
+ */
132
+ export function renderThinking() {
133
+ thinkingState = { status: "", detail: "", content: "" };
134
+
135
+ const update = () => {
136
+ if (thinkingState.rerender) {
137
+ thinkingState.rerender(h(ThinkingDisplay, {
138
+ status: thinkingState.status,
139
+ detail: thinkingState.detail,
140
+ content: thinkingState.content,
141
+ }));
142
+ }
143
+ };
144
+
145
+ const { rerender, unmount, clear } = render(
146
+ h(ThinkingDisplay, { status: "", detail: "", content: "" })
147
+ );
148
+ thinkingState.rerender = rerender;
149
+
150
+ return {
151
+ setStatus: (status, detail = "") => {
152
+ thinkingState.status = status;
153
+ thinkingState.detail = detail;
154
+ update();
155
+ },
156
+ setContent: (content) => {
157
+ thinkingState.content = content;
158
+ update();
159
+ },
160
+ appendContent: (text) => {
161
+ thinkingState.content += text;
162
+ update();
163
+ },
164
+ stop: () => {
165
+ clear();
166
+ unmount();
167
+ thinkingState.rerender = null;
168
+ },
169
+ };
170
+ }
@@ -0,0 +1,29 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { VERBS } from "./spinner.js";
3
+
4
+ describe("ui/spinner", () => {
5
+ describe("VERBS", () => {
6
+ it("is a non-empty array of strings", () => {
7
+ expect(Array.isArray(VERBS)).toBe(true);
8
+ expect(VERBS.length).toBeGreaterThan(5);
9
+ for (const v of VERBS) {
10
+ expect(typeof v).toBe("string");
11
+ expect(v.length).toBeGreaterThan(0);
12
+ }
13
+ });
14
+
15
+ it("has no duplicates", () => {
16
+ const unique = new Set(VERBS);
17
+ expect(unique.size).toBe(VERBS.length);
18
+ });
19
+
20
+ it("contains hacker-themed verbs", () => {
21
+ const hasHacker = VERBS.some((v) =>
22
+ v.toLowerCase().includes("hack") ||
23
+ v.toLowerCase().includes("decrypt") ||
24
+ v.toLowerCase().includes("crack")
25
+ );
26
+ expect(hasHacker).toBe(true);
27
+ });
28
+ });
29
+ });
@@ -0,0 +1,106 @@
1
+ import React, { useState, useEffect } from "react";
2
+ import { render, Box, Text } from "ink";
3
+ import { ThinkingSpinner } from "./spinner.js";
4
+
5
+ const h = React.createElement;
6
+
7
+ /**
8
+ * Response box component
9
+ */
10
+ export function ResponseBox({ content, title = "Claude" }) {
11
+ return h(Box, {
12
+ flexDirection: "column",
13
+ borderStyle: "round",
14
+ borderColor: "gray",
15
+ paddingX: 1,
16
+ marginY: 1,
17
+ },
18
+ h(Box, { marginBottom: 1 },
19
+ h(Text, { bold: true, color: "cyan" }, title)
20
+ ),
21
+ h(Text, null, content)
22
+ );
23
+ }
24
+
25
+ /**
26
+ * Render a response in a box
27
+ */
28
+ export function renderResponse(content, title) {
29
+ const { unmount, clear } = render(
30
+ h(ResponseBox, { content, title })
31
+ );
32
+
33
+ return {
34
+ stop: () => {
35
+ clear();
36
+ unmount();
37
+ },
38
+ };
39
+ }
40
+
41
+ // Global state for streaming (workaround for lack of refs in functional approach)
42
+ let streamingState = { content: "", thinking: true, rerender: null };
43
+
44
+ /**
45
+ * Streaming response component
46
+ */
47
+ export function StreamingResponse({ title = "Claude" }) {
48
+ const [content, setContent] = useState(streamingState.content);
49
+ const [thinking, setThinking] = useState(streamingState.thinking);
50
+
51
+ useEffect(() => {
52
+ streamingState.setContent = setContent;
53
+ streamingState.setThinking = setThinking;
54
+ }, []);
55
+
56
+ if (thinking) {
57
+ return h(ThinkingSpinner, null);
58
+ }
59
+
60
+ if (!content) {
61
+ return null;
62
+ }
63
+
64
+ return h(Box, {
65
+ flexDirection: "column",
66
+ borderStyle: "round",
67
+ borderColor: "gray",
68
+ paddingX: 1,
69
+ marginTop: 1,
70
+ },
71
+ h(Box, { marginBottom: 1 },
72
+ h(Text, { bold: true, color: "cyan" }, title)
73
+ ),
74
+ h(Text, null, content)
75
+ );
76
+ }
77
+
78
+ /**
79
+ * Render streaming response
80
+ * Returns controls to append text and finish
81
+ */
82
+ export function renderStreaming(title = "Claude") {
83
+ // Reset state
84
+ streamingState = { content: "", thinking: true, setContent: null, setThinking: null };
85
+
86
+ const { unmount, clear } = render(h(StreamingResponse, { title }));
87
+
88
+ return {
89
+ append: (text) => {
90
+ streamingState.content += text;
91
+ if (streamingState.setContent) {
92
+ streamingState.setContent(streamingState.content);
93
+ }
94
+ },
95
+ setThinking: (val) => {
96
+ streamingState.thinking = val;
97
+ if (streamingState.setThinking) {
98
+ streamingState.setThinking(val);
99
+ }
100
+ },
101
+ stop: () => {
102
+ clear();
103
+ unmount();
104
+ },
105
+ };
106
+ }