yappr 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.
Files changed (149) hide show
  1. package/.env.example +115 -0
  2. package/config/context/personality.md +7 -0
  3. package/config/context/security.md +10 -0
  4. package/config/hooks/example.ts +47 -0
  5. package/config/hooks/holder.ts +154 -0
  6. package/config/hooks/user-memory.ts +102 -0
  7. package/config/skills/compute/handler.ts +6 -0
  8. package/config/skills/compute/skill.md +7 -0
  9. package/config/skills/cron/handler.ts +89 -0
  10. package/config/skills/cron/skill.md +36 -0
  11. package/config/skills/generate-image/handler.ts +133 -0
  12. package/config/skills/generate-image/skill.md +20 -0
  13. package/config/skills/generate-meme-prompt/handler.ts +40 -0
  14. package/config/skills/generate-meme-prompt/skill.md +23 -0
  15. package/config/skills/stats/handler.ts +76 -0
  16. package/config/skills/stats/skill.md +18 -0
  17. package/config/skills/wallet/handler.ts +56 -0
  18. package/config/skills/wallet/skill.md +17 -0
  19. package/config/skills/x/handler.ts +135 -0
  20. package/config/skills/x/skill.md +163 -0
  21. package/dist/config/hooks/example.d.ts +2 -0
  22. package/dist/config/hooks/example.js +37 -0
  23. package/dist/config/hooks/holder.d.ts +2 -0
  24. package/dist/config/hooks/holder.js +147 -0
  25. package/dist/config/hooks/user-memory.d.ts +2 -0
  26. package/dist/config/hooks/user-memory.js +79 -0
  27. package/dist/config/skills/compute/handler.d.ts +2 -0
  28. package/dist/config/skills/compute/handler.js +5 -0
  29. package/dist/config/skills/cron/handler.d.ts +2 -0
  30. package/dist/config/skills/cron/handler.js +84 -0
  31. package/dist/config/skills/generate-image/handler.d.ts +2 -0
  32. package/dist/config/skills/generate-image/handler.js +122 -0
  33. package/dist/config/skills/generate-meme/handler.d.ts +2 -0
  34. package/dist/config/skills/generate-meme/handler.js +121 -0
  35. package/dist/config/skills/generate-meme-prompt/handler.d.ts +2 -0
  36. package/dist/config/skills/generate-meme-prompt/handler.js +38 -0
  37. package/dist/config/skills/stats/handler.d.ts +2 -0
  38. package/dist/config/skills/stats/handler.js +71 -0
  39. package/dist/config/skills/wallet/handler.d.ts +2 -0
  40. package/dist/config/skills/wallet/handler.js +54 -0
  41. package/dist/config/skills/x/handler.d.ts +2 -0
  42. package/dist/config/skills/x/handler.js +115 -0
  43. package/dist/src/agent-prompt.d.ts +1 -0
  44. package/dist/src/agent-prompt.js +45 -0
  45. package/dist/src/bankr.d.ts +41 -0
  46. package/dist/src/bankr.js +76 -0
  47. package/dist/src/cli/backup.d.ts +7 -0
  48. package/dist/src/cli/backup.js +78 -0
  49. package/dist/src/cli/charts.d.ts +32 -0
  50. package/dist/src/cli/charts.js +222 -0
  51. package/dist/src/cli/config-sync.d.ts +7 -0
  52. package/dist/src/cli/config-sync.js +71 -0
  53. package/dist/src/cli/deploy.d.ts +2 -0
  54. package/dist/src/cli/deploy.js +1059 -0
  55. package/dist/src/cli/env.d.ts +4 -0
  56. package/dist/src/cli/env.js +50 -0
  57. package/dist/src/cli/host-key.d.ts +4 -0
  58. package/dist/src/cli/host-key.js +50 -0
  59. package/dist/src/cli/index.d.ts +2 -0
  60. package/dist/src/cli/index.js +71 -0
  61. package/dist/src/cli/init.d.ts +1 -0
  62. package/dist/src/cli/init.js +51 -0
  63. package/dist/src/cli/ssh.d.ts +2 -0
  64. package/dist/src/cli/ssh.js +141 -0
  65. package/dist/src/cli/status.d.ts +7 -0
  66. package/dist/src/cli/status.js +1184 -0
  67. package/dist/src/cli/tui.d.ts +18 -0
  68. package/dist/src/cli/tui.js +115 -0
  69. package/dist/src/cli/ui.d.ts +30 -0
  70. package/dist/src/cli/ui.js +164 -0
  71. package/dist/src/cli/update.d.ts +1 -0
  72. package/dist/src/cli/update.js +263 -0
  73. package/dist/src/cli/x-login.d.ts +6 -0
  74. package/dist/src/cli/x-login.js +70 -0
  75. package/dist/src/compute.d.ts +11 -0
  76. package/dist/src/compute.js +109 -0
  77. package/dist/src/config-loader.d.ts +19 -0
  78. package/dist/src/config-loader.js +82 -0
  79. package/dist/src/config.d.ts +29 -0
  80. package/dist/src/config.js +68 -0
  81. package/dist/src/cron/capability.d.ts +6 -0
  82. package/dist/src/cron/capability.js +66 -0
  83. package/dist/src/cron/runner.d.ts +2 -0
  84. package/dist/src/cron/runner.js +113 -0
  85. package/dist/src/cron/schedule.d.ts +19 -0
  86. package/dist/src/cron/schedule.js +154 -0
  87. package/dist/src/cron/store.d.ts +46 -0
  88. package/dist/src/cron/store.js +220 -0
  89. package/dist/src/db.d.ts +4 -0
  90. package/dist/src/db.js +53 -0
  91. package/dist/src/hooks/loader.d.ts +1 -0
  92. package/dist/src/hooks/loader.js +17 -0
  93. package/dist/src/hooks/registry.d.ts +17 -0
  94. package/dist/src/hooks/registry.js +78 -0
  95. package/dist/src/hooks/types.d.ts +45 -0
  96. package/dist/src/hooks/types.js +1 -0
  97. package/dist/src/index.d.ts +25 -0
  98. package/dist/src/index.js +35 -0
  99. package/dist/src/llm/index.d.ts +23 -0
  100. package/dist/src/llm/index.js +213 -0
  101. package/dist/src/llm/prompts.d.ts +6 -0
  102. package/dist/src/llm/prompts.js +99 -0
  103. package/dist/src/log.d.ts +2 -0
  104. package/dist/src/log.js +30 -0
  105. package/dist/src/reply/agent.d.ts +20 -0
  106. package/dist/src/reply/agent.js +215 -0
  107. package/dist/src/reply/context-blocks.d.ts +12 -0
  108. package/dist/src/reply/context-blocks.js +22 -0
  109. package/dist/src/reply/gating.d.ts +3 -0
  110. package/dist/src/reply/gating.js +35 -0
  111. package/dist/src/reply/pipeline.d.ts +3 -0
  112. package/dist/src/reply/pipeline.js +144 -0
  113. package/dist/src/reply/poller.d.ts +5 -0
  114. package/dist/src/reply/poller.js +79 -0
  115. package/dist/src/skills/holder-access.d.ts +7 -0
  116. package/dist/src/skills/holder-access.js +53 -0
  117. package/dist/src/skills/loader.d.ts +2 -0
  118. package/dist/src/skills/loader.js +64 -0
  119. package/dist/src/skills/registry.d.ts +4 -0
  120. package/dist/src/skills/registry.js +10 -0
  121. package/dist/src/skills/types.d.ts +16 -0
  122. package/dist/src/skills/types.js +1 -0
  123. package/dist/src/state.d.ts +5 -0
  124. package/dist/src/state.js +26 -0
  125. package/dist/src/stats-cli.d.ts +1 -0
  126. package/dist/src/stats-cli.js +82 -0
  127. package/dist/src/stats.d.ts +41 -0
  128. package/dist/src/stats.js +236 -0
  129. package/dist/src/storage.d.ts +16 -0
  130. package/dist/src/storage.js +107 -0
  131. package/dist/src/treasury/abi.d.ts +99 -0
  132. package/dist/src/treasury/abi.js +71 -0
  133. package/dist/src/treasury/cycle.d.ts +16 -0
  134. package/dist/src/treasury/cycle.js +154 -0
  135. package/dist/src/treasury/index.d.ts +28 -0
  136. package/dist/src/treasury/index.js +222 -0
  137. package/dist/src/util.d.ts +3 -0
  138. package/dist/src/util.js +18 -0
  139. package/dist/src/wallet.d.ts +5 -0
  140. package/dist/src/wallet.js +241 -0
  141. package/dist/src/x/client.d.ts +74 -0
  142. package/dist/src/x/client.js +323 -0
  143. package/dist/src/x/types.d.ts +61 -0
  144. package/dist/src/x/types.js +1 -0
  145. package/dist/src/x402.d.ts +6 -0
  146. package/dist/src/x402.js +11 -0
  147. package/dist/src/yappr.d.ts +1 -0
  148. package/dist/src/yappr.js +85 -0
  149. package/package.json +52 -0
@@ -0,0 +1,4 @@
1
+ export declare function isUnset(value: string | undefined): boolean;
2
+ export declare function setEnvVarInContent(content: string, key: string, value: string): string;
3
+ export declare function removeEnvVarInContent(content: string, key: string): string;
4
+ export declare function setEnvVar(envPath: string, key: string, value: string): Promise<void>;
@@ -0,0 +1,50 @@
1
+ // .env read/write helpers for the deploy flow: prompt-collected values and
2
+ // deploy-generated state (instance id, one-time SSH password) are persisted here
3
+ // so a failed run can resume without losing anything unrecoverable.
4
+ import { readFile, writeFile } from "node:fs/promises";
5
+ // A value that should be treated as "not set" — empty or an .env.example placeholder.
6
+ export function isUnset(value) {
7
+ return !value || /^(bk_|0x)?\.\.\.$/.test(value) || value === "...";
8
+ }
9
+ // Quote a value so dotenv reads it back verbatim: unquoted values are trimmed and
10
+ // truncated at an inline " #" — fatal for generated secrets like the one-time SSH
11
+ // password, which can't be re-fetched. Plain values stay unquoted.
12
+ function quoteEnvValue(value) {
13
+ if (!/[#'"\s]/.test(value))
14
+ return value;
15
+ if (!value.includes("'"))
16
+ return `'${value}'`;
17
+ if (!value.includes('"'))
18
+ return `"${value}"`;
19
+ return value; // contains both quote kinds — leave as-is rather than corrupt it
20
+ }
21
+ export function setEnvVarInContent(content, key, value) {
22
+ const line = `${key}=${quoteEnvValue(value)}`;
23
+ // The replacements below MUST use a function: a plain replacement string would
24
+ // have its `$`-sequences ($&, $', $1, …) expanded by String.replace, silently
25
+ // mangling any value (passwords!) that contains them.
26
+ // Existing uncommented assignment — overwrite in place.
27
+ const active = new RegExp(`^${key}=.*$`, "m");
28
+ if (active.test(content))
29
+ return content.replace(active, () => line);
30
+ // Commented placeholder (e.g. "# KEY=" or "#KEY=...") — uncomment in place
31
+ // so the file stays clean instead of growing a duplicate at the bottom.
32
+ const commented = new RegExp(`^#\\s*${key}=.*$`, "m");
33
+ if (commented.test(content))
34
+ return content.replace(commented, () => line);
35
+ if (!content)
36
+ return `${line}\n`;
37
+ return content.endsWith("\n") ? `${content}${line}\n` : `${content}\n${line}\n`;
38
+ }
39
+ // Drop every assignment of `key` — used to keep credentials the server has no use
40
+ // for (its own root password) out of the uploaded .env.
41
+ export function removeEnvVarInContent(content, key) {
42
+ return content.replace(new RegExp(`^${key}=.*\\n?`, "gm"), "");
43
+ }
44
+ // Persist `key=value` into the .env file AND the live process.env, so later steps
45
+ // in the same run see it without re-reading the file.
46
+ export async function setEnvVar(envPath, key, value) {
47
+ const content = await readFile(envPath, "utf8").catch(() => "");
48
+ await writeFile(envPath, setEnvVarInContent(content, key, value));
49
+ process.env[key] = value;
50
+ }
@@ -0,0 +1,4 @@
1
+ export declare function hostKeyConfig(host: string): {
2
+ hostHash: "sha256";
3
+ hostVerifier: (hash: string) => boolean;
4
+ };
@@ -0,0 +1,50 @@
1
+ // Trust-on-first-use SSH host-key pinning for the CLI's node-ssh connections
2
+ // (deploy / status / ssh). ssh2 accepts ANY host key by default, and the target
3
+ // IP comes from a third-party compute API — without pinning, a reassigned or
4
+ // spoofed IP would silently receive the root password (and then the full .env).
5
+ // The first connection records the host's key fingerprint in .yappr-known-hosts
6
+ // (next to .env; one "host fingerprint" line per host); later connections refuse
7
+ // a changed key instead of proceeding.
8
+ import { readFileSync, writeFileSync } from "node:fs";
9
+ import { resolve } from "node:path";
10
+ const KNOWN_HOSTS_PATH = resolve(process.cwd(), ".yappr-known-hosts");
11
+ function loadKnownHosts() {
12
+ const map = new Map();
13
+ try {
14
+ for (const line of readFileSync(KNOWN_HOSTS_PATH, "utf8").split("\n")) {
15
+ const [host, fp] = line.trim().split(/\s+/);
16
+ if (host && fp)
17
+ map.set(host, fp);
18
+ }
19
+ }
20
+ catch { /* no file yet */ }
21
+ return map;
22
+ }
23
+ function saveKnownHosts(map) {
24
+ const body = [...map].map(([h, fp]) => `${h} ${fp}`).join("\n") + "\n";
25
+ try {
26
+ writeFileSync(KNOWN_HOSTS_PATH, body, { mode: 0o600 });
27
+ }
28
+ catch { /* best-effort */ }
29
+ }
30
+ // ssh2 connect-config fragment — spread into every `ssh.connect({...})`. With
31
+ // `hostHash` set, the verifier receives the host key's sha256 hash as a string.
32
+ export function hostKeyConfig(host) {
33
+ return {
34
+ hostHash: "sha256",
35
+ hostVerifier: (hash) => {
36
+ const known = loadKnownHosts();
37
+ const pinned = known.get(host);
38
+ if (!pinned) {
39
+ known.set(host, hash);
40
+ saveKnownHosts(known);
41
+ return true;
42
+ }
43
+ if (pinned === hash)
44
+ return true;
45
+ console.error(`\n SSH host key for ${host} changed (pinned ${pinned}, got ${hash}).`);
46
+ console.error(` If the instance was reprovisioned, remove the ${host} line from ${KNOWN_HOSTS_PATH} and retry.`);
47
+ return false;
48
+ },
49
+ };
50
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
@@ -0,0 +1,71 @@
1
+ #!/usr/bin/env node
2
+ // The `yappr` command. A thin dispatcher; each subcommand lazy-imports its module so
3
+ // `init` (used before deps/config exist) never loads the engine.
4
+ function help() {
5
+ console.log(`yappr — self-funding X reply agent
6
+
7
+ Usage: yappr <command> [args]
8
+
9
+ init [dir] Scaffold a project (config/ + .env.example) into dir (default: .)
10
+ start Run the agent — loads ./config and ./.env
11
+ update Update the engine + sync new skills/hooks/context into ./config
12
+ deploy Provision + deploy to an x402 compute instance
13
+ status [id] Live dashboard for the deployed instance
14
+ ssh [id] Open an interactive shell on the deployed instance
15
+ help Show this help
16
+ `);
17
+ }
18
+ // Hand control to a subcommand module's run(), making its process.argv look like a
19
+ // direct invocation (drop the subcommand token) so its own argv parsing still works.
20
+ async function delegate(path) {
21
+ process.argv.splice(2, 1);
22
+ const mod = (await import(path));
23
+ await mod.run();
24
+ }
25
+ const cmd = process.argv[2];
26
+ try {
27
+ switch (cmd) {
28
+ case "init": {
29
+ const { runInit } = await import("./init.js");
30
+ await runInit(process.argv[3]);
31
+ break;
32
+ }
33
+ case "start":
34
+ // Booting the agent is a side effect of importing its entry module.
35
+ await import("../yappr.js");
36
+ break;
37
+ case "update":
38
+ await delegate("./update.js");
39
+ break;
40
+ case "deploy":
41
+ await delegate("./deploy.js");
42
+ break;
43
+ case "status":
44
+ await delegate("./status.js");
45
+ break;
46
+ case "ssh":
47
+ await delegate("./ssh.js");
48
+ break;
49
+ case undefined:
50
+ case "help":
51
+ case "--help":
52
+ case "-h":
53
+ help();
54
+ break;
55
+ default:
56
+ console.error(`yappr: unknown command "${cmd}"\n`);
57
+ help();
58
+ process.exit(1);
59
+ }
60
+ }
61
+ catch (err) {
62
+ // Ctrl-C inside an inquirer prompt rejects with ExitPromptError — that's the
63
+ // user quitting, not a failure: exit quietly instead of dumping a stack trace.
64
+ if (err?.name === "ExitPromptError") {
65
+ console.log("\n Aborted.");
66
+ process.exit(0);
67
+ }
68
+ console.error(`\n ✗ ${err?.message ?? err}`);
69
+ process.exit(1);
70
+ }
71
+ export {};
@@ -0,0 +1 @@
1
+ export declare function runInit(targetArg?: string): Promise<void>;
@@ -0,0 +1,51 @@
1
+ import { cp, mkdir, access } from "node:fs/promises";
2
+ import { basename, dirname, resolve, join } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { manifestForDir, saveManifest } from "./config-sync.js";
5
+ // The installed package ships a starter config/ + .env.example (see package.json
6
+ // "files"); init copies them into the user's project so they have something useful
7
+ // to edit. Package root is three levels up from dist/src/cli/init.js (and from
8
+ // src/cli/init.ts in the repo).
9
+ const here = dirname(fileURLToPath(import.meta.url));
10
+ const PKG_ROOT = resolve(here, "..", "..", "..");
11
+ async function exists(p) {
12
+ try {
13
+ await access(p);
14
+ return true;
15
+ }
16
+ catch {
17
+ return false;
18
+ }
19
+ }
20
+ // Scaffold a new project. Never clobbers existing files — config is the user's once
21
+ // it's there, so re-running init is safe.
22
+ export async function runInit(targetArg = ".") {
23
+ const target = resolve(process.cwd(), targetArg);
24
+ await mkdir(target, { recursive: true });
25
+ const destConfig = join(target, "config");
26
+ if (await exists(destConfig)) {
27
+ console.log("• config/ already exists — left untouched");
28
+ }
29
+ else {
30
+ // Skip hidden files (e.g. a stray .DS_Store) so the scaffold stays clean.
31
+ await cp(join(PKG_ROOT, "config"), destConfig, {
32
+ recursive: true,
33
+ filter: (src) => !basename(src).startsWith("."),
34
+ });
35
+ // Record what we scaffolded so `yappr update` can later tell which files the user
36
+ // edited from those still pristine, and fast-forward only the untouched ones.
37
+ await saveManifest(destConfig, await manifestForDir(destConfig));
38
+ console.log("• scaffolded config/ — starter skills, hooks and context (edit or delete freely)");
39
+ }
40
+ const destEnv = join(target, ".env");
41
+ const examplePath = join(PKG_ROOT, ".env.example");
42
+ if (await exists(destEnv)) {
43
+ console.log("• .env already exists — left untouched");
44
+ }
45
+ else if (await exists(examplePath)) {
46
+ await cp(examplePath, destEnv);
47
+ console.log("• created .env from .env.example — fill in your keys");
48
+ }
49
+ const where = targetArg === "." ? "" : `cd ${targetArg} && `;
50
+ console.log(`\nDone. Next:\n ${where}# edit .env, then run the agent\n npx yappr start\n`);
51
+ }
@@ -0,0 +1,2 @@
1
+ import "dotenv/config";
2
+ export declare function run(): Promise<void>;
@@ -0,0 +1,141 @@
1
+ // Manual SSH into the deployed yappr compute instance (the `yappr ssh` command).
2
+ //
3
+ // yappr ssh # uses COMPUTE_INSTANCE_ID from .env
4
+ // yappr ssh <instanceId> # override the instance id
5
+ //
6
+ // Resolves the instance IP and one-time root password from the compute API
7
+ // (wallet-signature auth via the Bankr key), then opens an interactive shell
8
+ // using the password automatically — no prompt. Uses the ssh2 client bundled
9
+ // with node-ssh, so there's no dependency on `sshpass`.
10
+ import "dotenv/config";
11
+ import { spawn } from "node:child_process";
12
+ import { resolve } from "node:path";
13
+ import { fileURLToPath } from "node:url";
14
+ import ora from "ora";
15
+ import { NodeSSH } from "node-ssh";
16
+ import { resolveEvmAddress, fetchComputeInstance, fetchOneTimePassword, computeInstanceIp, computeInstancePassword, } from "../compute.js";
17
+ import { hostKeyConfig } from "./host-key.js";
18
+ // Open a fully interactive PTY shell over the established connection, wiring it
19
+ // to the local terminal (raw mode, resize forwarding). Resolves with the remote
20
+ // shell's exit code.
21
+ async function interactiveShell(ssh) {
22
+ const conn = ssh.connection;
23
+ if (!conn)
24
+ throw new Error("SSH connection not established");
25
+ return new Promise((resolve, reject) => {
26
+ conn.shell({ term: process.env.TERM || "xterm-256color" }, (err, stream) => {
27
+ if (err)
28
+ return reject(err);
29
+ const stdin = process.stdin;
30
+ const wasRaw = !!stdin.isRaw;
31
+ if (stdin.isTTY)
32
+ stdin.setRawMode(true);
33
+ stdin.resume();
34
+ stdin.pipe(stream);
35
+ stream.pipe(process.stdout);
36
+ stream.stderr.pipe(process.stderr);
37
+ const syncWindow = () => stream.setWindow(process.stdout.rows ?? 24, process.stdout.columns ?? 80, 0, 0);
38
+ process.stdout.on("resize", syncWindow);
39
+ syncWindow();
40
+ let exitCode = 0;
41
+ stream.on("exit", (code) => { exitCode = code ?? 0; });
42
+ stream.on("close", () => {
43
+ process.stdout.off("resize", syncWindow);
44
+ stdin.unpipe(stream);
45
+ if (stdin.isTTY)
46
+ stdin.setRawMode(wasRaw);
47
+ stdin.pause();
48
+ resolve(exitCode);
49
+ });
50
+ stream.on("error", reject);
51
+ });
52
+ });
53
+ }
54
+ // Connect with a password via the bundled ssh2 client and run an interactive
55
+ // shell. Returns the remote shell's exit code.
56
+ async function connectAndShell(ip, pw) {
57
+ const ssh = new NodeSSH();
58
+ await ssh.connect({ host: ip, username: "root", password: pw, tryKeyboard: true, ...hostKeyConfig(ip) });
59
+ const code = await interactiveShell(ssh);
60
+ ssh.dispose();
61
+ return code;
62
+ }
63
+ async function main() {
64
+ const instanceIdArg = process.argv[2];
65
+ // Fast path: if the deploy already cached the IP (COMPUTE_HOST) and password
66
+ // (COMPUTE_SSH_PASSWORD), connect directly with ZERO Bankr/compute API calls.
67
+ // Only when using the default instance (no explicit id override), since the
68
+ // cached IP belongs to COMPUTE_INSTANCE_ID. Any connection failure (e.g. the
69
+ // instance was reprovisioned and the cached IP is stale) falls through to the
70
+ // full resolve-from-API path below.
71
+ const cachedIp = process.env.COMPUTE_HOST;
72
+ const cachedPw = process.env.COMPUTE_SSH_PASSWORD;
73
+ if (!instanceIdArg && cachedIp && cachedPw) {
74
+ try {
75
+ console.log(`Using cached credentials. Connecting to root@${cachedIp}…\n`);
76
+ const code = await connectAndShell(cachedIp, cachedPw);
77
+ process.exit(code);
78
+ }
79
+ catch (err) {
80
+ console.warn(`Cached connection failed (${err instanceof Error ? err.message : String(err)}); resolving from the compute API…\n`);
81
+ }
82
+ }
83
+ // Slow path: resolve the IP + one-time password from the compute API (this is
84
+ // what requires the Bankr key + wallet signatures).
85
+ const apiKey = process.env.BANKR_API_KEY;
86
+ if (!apiKey)
87
+ throw new Error("BANKR_API_KEY not set in .env");
88
+ const instanceId = instanceIdArg || process.env.COMPUTE_INSTANCE_ID;
89
+ if (!instanceId)
90
+ throw new Error("No instance id — pass one as an argument or set COMPUTE_INSTANCE_ID in .env");
91
+ const spinner = ora("Resolving instance credentials…").start();
92
+ let ip;
93
+ let pw;
94
+ try {
95
+ const address = await resolveEvmAddress(apiKey);
96
+ const instance = await fetchComputeInstance(apiKey, address, instanceId);
97
+ ip = computeInstanceIp(instance);
98
+ if (!ip)
99
+ throw new Error(`Instance has no IP yet (status: ${instance?.status ?? instance?.order?.status ?? "unknown"})`);
100
+ // The one-time password can only be fetched ONCE per instance. The deploy
101
+ // saves it to .env after fetching, so prefer that before hitting the API.
102
+ pw = computeInstancePassword(instance) || process.env.COMPUTE_SSH_PASSWORD || undefined;
103
+ if (!pw)
104
+ pw = await fetchOneTimePassword(apiKey, address, instanceId);
105
+ spinner.succeed("Credentials resolved");
106
+ }
107
+ catch (err) {
108
+ spinner.fail();
109
+ throw err;
110
+ }
111
+ console.log(`\nInstance: ${instanceId}`);
112
+ console.log(`IP: ${ip}`);
113
+ // With a password we can log in directly via the ssh2 client (no prompt).
114
+ if (pw) {
115
+ console.log(`Connecting to root@${ip}…\n`);
116
+ const code = await connectAndShell(ip, pw);
117
+ process.exit(code);
118
+ }
119
+ // No password available — fall back to the system ssh client (will prompt / use keys).
120
+ console.warn("Password unavailable — falling back to system ssh (key auth or prompt).\n");
121
+ const child = spawn("ssh", ["-o", "StrictHostKeyChecking=accept-new", `root@${ip}`], { stdio: "inherit" });
122
+ child.on("exit", (code) => process.exit(code ?? 0));
123
+ }
124
+ export async function run() {
125
+ await main();
126
+ }
127
+ // Direct invocation (`tsx src/cli/ssh.ts`) — the bin calls run() instead.
128
+ const isMain = (() => {
129
+ try {
130
+ return fileURLToPath(import.meta.url) === resolve(process.argv[1] ?? "");
131
+ }
132
+ catch {
133
+ return false;
134
+ }
135
+ })();
136
+ if (isMain) {
137
+ run().catch((err) => {
138
+ console.error("Error:", err instanceof Error ? err.message : String(err));
139
+ process.exit(1);
140
+ });
141
+ }
@@ -0,0 +1,7 @@
1
+ import "dotenv/config";
2
+ export declare function runStatus(target: {
3
+ ip: string;
4
+ password?: string;
5
+ handle?: string;
6
+ }): Promise<void>;
7
+ export declare function run(): Promise<void>;