@novaqore/atom 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.
- package/LICENSE +21 -0
- package/README.md +42 -0
- package/index.js +8 -0
- package/package.json +42 -0
- package/src/app.js +73 -0
- package/src/cli.js +45 -0
- package/src/components/Chat/ChatInput.js +84 -0
- package/src/components/Chat/ConfirmDangerousCommand.js +46 -0
- package/src/components/Header.js +64 -0
- package/src/screens/chat.js +371 -0
- package/src/screens/main-menu.js +107 -0
- package/src/screens/onboarding.js +166 -0
- package/src/screens/system.js +175 -0
- package/src/screens/unhinged-warning.js +45 -0
- package/src/setup.js +3 -0
- package/src/tools/bash/tool.js +74 -0
- package/src/tools/index.js +30 -0
- package/src/utils/exec.js +13 -0
- package/src/utils/markdown.js +47 -0
- package/src/utils/paths.js +5 -0
- package/src/utils/runtimes.js +70 -0
- package/src/utils/system-prompt.js +89 -0
- package/src/utils/system.js +106 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 NovaQore LLC
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# Atom
|
|
2
|
+
|
|
3
|
+
Atom is a server agent. It runs on the box it operates, with shell access at the system level and full awareness of its environment: platform, runtime, shell, and working directory. Use it where you want an autonomous agent that reads, writes, and executes across the whole server, not just inside a project folder.
|
|
4
|
+
|
|
5
|
+
> [!WARNING]
|
|
6
|
+
> Atom is not intended for personal machines. It runs with shell access at the system level by design. Only use it in isolated environments such as AWS EC2 instances, DigitalOcean droplets, GCP VMs, Hetzner servers, Linode boxes, or Fly.io machines.
|
|
7
|
+
|
|
8
|
+
> Status: experimental (`v0.0.1`). The CLI scaffold is in place; the agent core is being built next.
|
|
9
|
+
|
|
10
|
+
## Install
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
npm install -g @novaqore/atom
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
This installs the `atom` command globally.
|
|
17
|
+
|
|
18
|
+
## Get your keys
|
|
19
|
+
|
|
20
|
+
Atom runs on the NovaQore AI service. You will need an account and a Quantum Key before you can use the agent.
|
|
21
|
+
|
|
22
|
+
1. Sign up at [novaqore.ai](https://novaqore.ai).
|
|
23
|
+
2. Generate a UID, Key ID, and Quantum Key at [chat.novaqore.ai/keys](https://chat.novaqore.ai/keys).
|
|
24
|
+
3. Keep them somewhere safe. Atom asks for them on first run.
|
|
25
|
+
|
|
26
|
+
## Quick start
|
|
27
|
+
|
|
28
|
+
Once installed, run it from any directory:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
atom
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
The first run walks you through onboarding and asks for your UID, Key ID, and Quantum Key. They are stored locally in `~/.atom/` and only sent to the NovaQore AI service when you chat.
|
|
35
|
+
|
|
36
|
+
## Requirements
|
|
37
|
+
|
|
38
|
+
- Node.js 20 or higher
|
|
39
|
+
|
|
40
|
+
## License
|
|
41
|
+
|
|
42
|
+
MIT © NovaQore LLC. See [LICENSE](LICENSE).
|
package/index.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@novaqore/atom",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "A full-system code agent with root-level access, built for isolated servers.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"atom": "./index.js"
|
|
8
|
+
},
|
|
9
|
+
"main": "./index.js",
|
|
10
|
+
"files": [
|
|
11
|
+
"index.js",
|
|
12
|
+
"src",
|
|
13
|
+
"README.md",
|
|
14
|
+
"LICENSE"
|
|
15
|
+
],
|
|
16
|
+
"engines": {
|
|
17
|
+
"node": ">=20"
|
|
18
|
+
},
|
|
19
|
+
"repository": {
|
|
20
|
+
"type": "git",
|
|
21
|
+
"url": "git+https://github.com/novaqore/atom.git"
|
|
22
|
+
},
|
|
23
|
+
"bugs": {
|
|
24
|
+
"url": "https://github.com/novaqore/atom/issues"
|
|
25
|
+
},
|
|
26
|
+
"homepage": "https://github.com/novaqore/atom#readme",
|
|
27
|
+
"author": "NovaQore LLC",
|
|
28
|
+
"license": "MIT",
|
|
29
|
+
"publishConfig": {
|
|
30
|
+
"access": "public"
|
|
31
|
+
},
|
|
32
|
+
"dependencies": {
|
|
33
|
+
"@novaqore/ai": "^0.5.0",
|
|
34
|
+
"commander": "^14.0.3",
|
|
35
|
+
"ink": "^6.8.0",
|
|
36
|
+
"ink-select-input": "^6.2.0",
|
|
37
|
+
"ink-text-input": "^6.0.0",
|
|
38
|
+
"marked": "^15.0.12",
|
|
39
|
+
"marked-terminal": "^7.3.0",
|
|
40
|
+
"react": "^19.2.6"
|
|
41
|
+
}
|
|
42
|
+
}
|
package/src/app.js
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import React, { useState } from "react";
|
|
2
|
+
import { accessSync } from "node:fs";
|
|
3
|
+
import { unlink } from "node:fs/promises";
|
|
4
|
+
import { OnboardingScreen } from "./screens/onboarding.js";
|
|
5
|
+
import { MainMenuScreen } from "./screens/main-menu.js";
|
|
6
|
+
import { ChatScreen } from "./screens/chat.js";
|
|
7
|
+
import { SystemScreen } from "./screens/system.js";
|
|
8
|
+
import { UnhingedWarningScreen } from "./screens/unhinged-warning.js";
|
|
9
|
+
import { SERVICE_FILE } from "./utils/paths.js";
|
|
10
|
+
|
|
11
|
+
function hasOnboardedSync() {
|
|
12
|
+
try {
|
|
13
|
+
accessSync(SERVICE_FILE);
|
|
14
|
+
return true;
|
|
15
|
+
} catch {
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function App({ version, unhinged }) {
|
|
21
|
+
const [onboarded, setOnboarded] = useState(hasOnboardedSync);
|
|
22
|
+
const [unhingedConfirmed, setUnhingedConfirmed] = useState(!unhinged);
|
|
23
|
+
const [screen, setScreen] = useState(() =>
|
|
24
|
+
unhinged && !unhingedConfirmed ? "unhinged-warning" : "main-menu"
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
const handleReset = async () => {
|
|
28
|
+
await unlink(SERVICE_FILE).catch(() => {});
|
|
29
|
+
setOnboarded(false);
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
if (screen === "unhinged-warning") {
|
|
33
|
+
return React.createElement(UnhingedWarningScreen, {
|
|
34
|
+
onConfirm: () => {
|
|
35
|
+
setUnhingedConfirmed(true);
|
|
36
|
+
setScreen("main-menu");
|
|
37
|
+
},
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
if (screen === "main-menu") {
|
|
41
|
+
return React.createElement(MainMenuScreen, {
|
|
42
|
+
version,
|
|
43
|
+
unhinged,
|
|
44
|
+
onboarded,
|
|
45
|
+
onChat: () => setScreen("chat"),
|
|
46
|
+
onGetStarted: () => setScreen("onboarding"),
|
|
47
|
+
onSystem: () => setScreen("system"),
|
|
48
|
+
onReset: handleReset,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
if (screen === "onboarding") {
|
|
52
|
+
return React.createElement(OnboardingScreen, {
|
|
53
|
+
version,
|
|
54
|
+
unhinged,
|
|
55
|
+
onComplete: () => {
|
|
56
|
+
setOnboarded(true);
|
|
57
|
+
setScreen("main-menu");
|
|
58
|
+
},
|
|
59
|
+
onBack: () => setScreen("main-menu"),
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
if (screen === "chat") {
|
|
63
|
+
return React.createElement(ChatScreen, { version, unhinged });
|
|
64
|
+
}
|
|
65
|
+
if (screen === "system") {
|
|
66
|
+
return React.createElement(SystemScreen, {
|
|
67
|
+
version,
|
|
68
|
+
unhinged,
|
|
69
|
+
onBack: () => setScreen("main-menu"),
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
return null;
|
|
73
|
+
}
|
package/src/cli.js
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { mkdir, readFile } from "node:fs/promises";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
import { Command } from "commander";
|
|
4
|
+
import { render } from "ink";
|
|
5
|
+
import React from "react";
|
|
6
|
+
import { App } from "./app.js";
|
|
7
|
+
import { ATOM_DIR } from "./utils/paths.js";
|
|
8
|
+
|
|
9
|
+
async function readPackageVersion() {
|
|
10
|
+
const pkgUrl = new URL("../package.json", import.meta.url);
|
|
11
|
+
const raw = await readFile(fileURLToPath(pkgUrl), "utf8");
|
|
12
|
+
return JSON.parse(raw).version;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export async function run(argv) {
|
|
16
|
+
const version = await readPackageVersion();
|
|
17
|
+
const program = new Command();
|
|
18
|
+
|
|
19
|
+
program
|
|
20
|
+
.name("atom")
|
|
21
|
+
.description(
|
|
22
|
+
"A full-system code agent with root-level access, built for isolated servers."
|
|
23
|
+
)
|
|
24
|
+
.version(version, "-v, --version", "Output the current version")
|
|
25
|
+
.option("--unhinged", "Bypass confirmation for dangerous commands")
|
|
26
|
+
.addHelpText(
|
|
27
|
+
"after",
|
|
28
|
+
`
|
|
29
|
+
Examples:
|
|
30
|
+
$ atom Start Atom
|
|
31
|
+
$ atom --unhinged Start Atom with no command guardrails
|
|
32
|
+
$ atom -v Print the current version
|
|
33
|
+
|
|
34
|
+
Learn more: https://github.com/novaqore/atom`
|
|
35
|
+
)
|
|
36
|
+
.action(async (opts) => {
|
|
37
|
+
await mkdir(ATOM_DIR, { recursive: true, mode: 0o700 });
|
|
38
|
+
process.stdout.write("\x1Bc");
|
|
39
|
+
render(
|
|
40
|
+
React.createElement(App, { version, unhinged: !!opts.unhinged })
|
|
41
|
+
);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
await program.parseAsync(argv);
|
|
45
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import React, { useState, useEffect } from "react";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { Box, Text } from "ink";
|
|
4
|
+
import TextInput from "ink-text-input";
|
|
5
|
+
import { HOSTNAME, USERNAME } from "../../utils/system.js";
|
|
6
|
+
|
|
7
|
+
const SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
8
|
+
const CONTEXT_TOTAL = 262144;
|
|
9
|
+
|
|
10
|
+
function Spinner() {
|
|
11
|
+
const [i, setI] = useState(0);
|
|
12
|
+
useEffect(() => {
|
|
13
|
+
const id = setInterval(
|
|
14
|
+
() => setI((n) => (n + 1) % SPINNER_FRAMES.length),
|
|
15
|
+
80
|
|
16
|
+
);
|
|
17
|
+
return () => clearInterval(id);
|
|
18
|
+
}, []);
|
|
19
|
+
return React.createElement(Text, { color: "cyan" }, SPINNER_FRAMES[i]);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function shortPath(p) {
|
|
23
|
+
const home = homedir();
|
|
24
|
+
if (p === home) return "~";
|
|
25
|
+
if (p.startsWith(home + "/")) return "~" + p.slice(home.length);
|
|
26
|
+
return p;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function ChatInput({
|
|
30
|
+
value,
|
|
31
|
+
onChange,
|
|
32
|
+
onSubmit,
|
|
33
|
+
cwd,
|
|
34
|
+
totalTokens,
|
|
35
|
+
loading,
|
|
36
|
+
disabled,
|
|
37
|
+
}) {
|
|
38
|
+
return React.createElement(
|
|
39
|
+
Box,
|
|
40
|
+
{ flexDirection: "column" },
|
|
41
|
+
loading
|
|
42
|
+
? React.createElement(
|
|
43
|
+
Box,
|
|
44
|
+
null,
|
|
45
|
+
React.createElement(Spinner),
|
|
46
|
+
React.createElement(Text, { dimColor: true }, " Responding...")
|
|
47
|
+
)
|
|
48
|
+
: null,
|
|
49
|
+
React.createElement(
|
|
50
|
+
Box,
|
|
51
|
+
null,
|
|
52
|
+
React.createElement(
|
|
53
|
+
Text,
|
|
54
|
+
{ color: "white" },
|
|
55
|
+
`${USERNAME}@${HOSTNAME} `
|
|
56
|
+
),
|
|
57
|
+
React.createElement(Text, { color: "yellow" }, shortPath(cwd)),
|
|
58
|
+
React.createElement(
|
|
59
|
+
Text,
|
|
60
|
+
{ dimColor: true },
|
|
61
|
+
` ${totalTokens.toLocaleString()}/${CONTEXT_TOTAL.toLocaleString()} tokens`
|
|
62
|
+
)
|
|
63
|
+
),
|
|
64
|
+
React.createElement(
|
|
65
|
+
Box,
|
|
66
|
+
{
|
|
67
|
+
borderStyle: "single",
|
|
68
|
+
borderTop: true,
|
|
69
|
+
borderBottom: true,
|
|
70
|
+
borderLeft: false,
|
|
71
|
+
borderRight: false,
|
|
72
|
+
borderColor: "gray",
|
|
73
|
+
},
|
|
74
|
+
React.createElement(Text, { dimColor: true }, "❯ "),
|
|
75
|
+
React.createElement(TextInput, {
|
|
76
|
+
value,
|
|
77
|
+
onChange,
|
|
78
|
+
onSubmit,
|
|
79
|
+
showCursor: !disabled,
|
|
80
|
+
focus: !disabled,
|
|
81
|
+
})
|
|
82
|
+
)
|
|
83
|
+
);
|
|
84
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { Box, Text } from "ink";
|
|
3
|
+
import SelectInput from "ink-select-input";
|
|
4
|
+
|
|
5
|
+
const items = [
|
|
6
|
+
{ label: "Yes", value: true },
|
|
7
|
+
{ label: "No", value: false },
|
|
8
|
+
];
|
|
9
|
+
|
|
10
|
+
function YellowIndicator({ isSelected }) {
|
|
11
|
+
return React.createElement(
|
|
12
|
+
Box,
|
|
13
|
+
{ marginRight: 1 },
|
|
14
|
+
isSelected
|
|
15
|
+
? React.createElement(Text, { color: "yellow" }, "❯")
|
|
16
|
+
: React.createElement(Text, null, " ")
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function YellowItem({ isSelected, label }) {
|
|
21
|
+
return React.createElement(
|
|
22
|
+
Text,
|
|
23
|
+
isSelected ? { color: "yellow" } : null,
|
|
24
|
+
label
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function ConfirmDangerousCommand({ command, onAnswer }) {
|
|
29
|
+
return React.createElement(
|
|
30
|
+
Box,
|
|
31
|
+
{
|
|
32
|
+
flexDirection: "column",
|
|
33
|
+
borderStyle: "single",
|
|
34
|
+
borderColor: "red",
|
|
35
|
+
borderLeft: false,
|
|
36
|
+
borderRight: false,
|
|
37
|
+
},
|
|
38
|
+
React.createElement(Text, { color: "red", bold: true }, `Run: ${command}`),
|
|
39
|
+
React.createElement(SelectInput, {
|
|
40
|
+
items,
|
|
41
|
+
onSelect: (item) => onAnswer(item.value),
|
|
42
|
+
indicatorComponent: YellowIndicator,
|
|
43
|
+
itemComponent: YellowItem,
|
|
44
|
+
})
|
|
45
|
+
);
|
|
46
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { arch, cpus, totalmem } from "node:os";
|
|
3
|
+
import { Box, Text } from "ink";
|
|
4
|
+
import { DISK } from "../utils/system.js";
|
|
5
|
+
import { RUNTIMES } from "../utils/runtimes.js";
|
|
6
|
+
|
|
7
|
+
export function Header({ version, unhinged }) {
|
|
8
|
+
const cpuList = cpus();
|
|
9
|
+
const cpuCount = cpuList?.length || 0;
|
|
10
|
+
const cpuSpeed = cpuList?.[0]?.speed || 0;
|
|
11
|
+
const cpuInfo = cpuSpeed
|
|
12
|
+
? `${cpuCount}c @ ${(cpuSpeed / 1000).toFixed(2)} GHz`
|
|
13
|
+
: `${cpuCount} cores`;
|
|
14
|
+
const ramInfo = `${(totalmem() / 1024 ** 3).toFixed(1)} GB`;
|
|
15
|
+
const diskInfo = DISK
|
|
16
|
+
? `${(DISK.used / 1024 ** 3).toFixed(0)}/${(DISK.total / 1024 ** 3).toFixed(0)} GB`
|
|
17
|
+
: null;
|
|
18
|
+
|
|
19
|
+
const runtimeParts = [arch(), `node ${RUNTIMES.node}`];
|
|
20
|
+
if (RUNTIMES.python !== "not installed") {
|
|
21
|
+
runtimeParts.push(`python ${RUNTIMES.python}`);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const hardwareParts = [cpuInfo, ramInfo];
|
|
25
|
+
if (diskInfo) hardwareParts.push(diskInfo);
|
|
26
|
+
|
|
27
|
+
return React.createElement(
|
|
28
|
+
Box,
|
|
29
|
+
{
|
|
30
|
+
borderStyle: "round",
|
|
31
|
+
borderColor: "cyan",
|
|
32
|
+
paddingX: 2,
|
|
33
|
+
marginBottom: 1,
|
|
34
|
+
},
|
|
35
|
+
React.createElement(
|
|
36
|
+
Box,
|
|
37
|
+
{ flexDirection: "column", marginRight: 2 },
|
|
38
|
+
React.createElement(Text, { color: "cyan", bold: true }, " ╲│╱ "),
|
|
39
|
+
React.createElement(Text, { color: "cyan", bold: true }, "─ ● ─"),
|
|
40
|
+
React.createElement(Text, { color: "cyan", bold: true }, " ╱│╲ ")
|
|
41
|
+
),
|
|
42
|
+
React.createElement(
|
|
43
|
+
Box,
|
|
44
|
+
{ flexDirection: "column" },
|
|
45
|
+
React.createElement(
|
|
46
|
+
Box,
|
|
47
|
+
null,
|
|
48
|
+
React.createElement(Text, { color: "cyan", bold: true }, "Atom "),
|
|
49
|
+
React.createElement(Text, { dimColor: true }, `v${version} · `),
|
|
50
|
+
React.createElement(Text, { color: "white", bold: true }, "NovaQore"),
|
|
51
|
+
React.createElement(Text, { dimColor: true }, " · mode: "),
|
|
52
|
+
React.createElement(
|
|
53
|
+
Text,
|
|
54
|
+
unhinged
|
|
55
|
+
? { color: "red", bold: true }
|
|
56
|
+
: { color: "green", bold: true },
|
|
57
|
+
unhinged ? "unhinged" : "normal"
|
|
58
|
+
)
|
|
59
|
+
),
|
|
60
|
+
React.createElement(Text, { dimColor: true }, runtimeParts.join(" · ")),
|
|
61
|
+
React.createElement(Text, { dimColor: true }, hardwareParts.join(" · "))
|
|
62
|
+
)
|
|
63
|
+
);
|
|
64
|
+
}
|