freeturtle 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.
- package/LICENSE +190 -0
- package/README.md +392 -0
- package/dist/bin/freeturtle.d.ts +2 -0
- package/dist/bin/freeturtle.js +119 -0
- package/dist/bin/freeturtle.js.map +1 -0
- package/dist/src/approval.d.ts +38 -0
- package/dist/src/approval.js +140 -0
- package/dist/src/approval.js.map +1 -0
- package/dist/src/audit.d.ts +33 -0
- package/dist/src/audit.js +36 -0
- package/dist/src/audit.js.map +1 -0
- package/dist/src/channels/telegram.d.ts +10 -0
- package/dist/src/channels/telegram.js +41 -0
- package/dist/src/channels/telegram.js.map +1 -0
- package/dist/src/channels/terminal.d.ts +9 -0
- package/dist/src/channels/terminal.js +52 -0
- package/dist/src/channels/terminal.js.map +1 -0
- package/dist/src/channels/types.d.ts +6 -0
- package/dist/src/channels/types.js +2 -0
- package/dist/src/channels/types.js.map +1 -0
- package/dist/src/cli/approvals.d.ts +3 -0
- package/dist/src/cli/approvals.js +33 -0
- package/dist/src/cli/approvals.js.map +1 -0
- package/dist/src/cli/connect-farcaster.d.ts +5 -0
- package/dist/src/cli/connect-farcaster.js +265 -0
- package/dist/src/cli/connect-farcaster.js.map +1 -0
- package/dist/src/cli/connection-tests.d.ts +16 -0
- package/dist/src/cli/connection-tests.js +65 -0
- package/dist/src/cli/connection-tests.js.map +1 -0
- package/dist/src/cli/init.d.ts +1 -0
- package/dist/src/cli/init.js +729 -0
- package/dist/src/cli/init.js.map +1 -0
- package/dist/src/cli/install-service.d.ts +1 -0
- package/dist/src/cli/install-service.js +57 -0
- package/dist/src/cli/install-service.js.map +1 -0
- package/dist/src/cli/intake.d.ts +23 -0
- package/dist/src/cli/intake.js +68 -0
- package/dist/src/cli/intake.js.map +1 -0
- package/dist/src/cli/send.d.ts +1 -0
- package/dist/src/cli/send.js +16 -0
- package/dist/src/cli/send.js.map +1 -0
- package/dist/src/cli/start.d.ts +3 -0
- package/dist/src/cli/start.js +25 -0
- package/dist/src/cli/start.js.map +1 -0
- package/dist/src/cli/status.d.ts +2 -0
- package/dist/src/cli/status.js +54 -0
- package/dist/src/cli/status.js.map +1 -0
- package/dist/src/cli/update.d.ts +1 -0
- package/dist/src/cli/update.js +39 -0
- package/dist/src/cli/update.js.map +1 -0
- package/dist/src/config.d.ts +31 -0
- package/dist/src/config.js +93 -0
- package/dist/src/config.js.map +1 -0
- package/dist/src/daemon.d.ts +18 -0
- package/dist/src/daemon.js +272 -0
- package/dist/src/daemon.js.map +1 -0
- package/dist/src/heartbeat.d.ts +17 -0
- package/dist/src/heartbeat.js +60 -0
- package/dist/src/heartbeat.js.map +1 -0
- package/dist/src/llm.d.ts +29 -0
- package/dist/src/llm.js +225 -0
- package/dist/src/llm.js.map +1 -0
- package/dist/src/logger.d.ts +8 -0
- package/dist/src/logger.js +45 -0
- package/dist/src/logger.js.map +1 -0
- package/dist/src/memory.d.ts +3 -0
- package/dist/src/memory.js +36 -0
- package/dist/src/memory.js.map +1 -0
- package/dist/src/modules/database/client.d.ts +18 -0
- package/dist/src/modules/database/client.js +50 -0
- package/dist/src/modules/database/client.js.map +1 -0
- package/dist/src/modules/database/index.d.ts +9 -0
- package/dist/src/modules/database/index.js +32 -0
- package/dist/src/modules/database/index.js.map +1 -0
- package/dist/src/modules/database/tools.d.ts +2 -0
- package/dist/src/modules/database/tools.js +26 -0
- package/dist/src/modules/database/tools.js.map +1 -0
- package/dist/src/modules/farcaster/client.d.ts +43 -0
- package/dist/src/modules/farcaster/client.js +87 -0
- package/dist/src/modules/farcaster/client.js.map +1 -0
- package/dist/src/modules/farcaster/index.d.ts +14 -0
- package/dist/src/modules/farcaster/index.js +71 -0
- package/dist/src/modules/farcaster/index.js.map +1 -0
- package/dist/src/modules/farcaster/tools.d.ts +2 -0
- package/dist/src/modules/farcaster/tools.js +90 -0
- package/dist/src/modules/farcaster/tools.js.map +1 -0
- package/dist/src/modules/github/client.d.ts +21 -0
- package/dist/src/modules/github/client.js +80 -0
- package/dist/src/modules/github/client.js.map +1 -0
- package/dist/src/modules/github/index.d.ts +13 -0
- package/dist/src/modules/github/index.js +45 -0
- package/dist/src/modules/github/index.js.map +1 -0
- package/dist/src/modules/github/tools.d.ts +2 -0
- package/dist/src/modules/github/tools.js +74 -0
- package/dist/src/modules/github/tools.js.map +1 -0
- package/dist/src/modules/loader.d.ts +5 -0
- package/dist/src/modules/loader.js +35 -0
- package/dist/src/modules/loader.js.map +1 -0
- package/dist/src/modules/onchain/client.d.ts +8 -0
- package/dist/src/modules/onchain/client.js +46 -0
- package/dist/src/modules/onchain/client.js.map +1 -0
- package/dist/src/modules/onchain/index.d.ts +13 -0
- package/dist/src/modules/onchain/index.js +40 -0
- package/dist/src/modules/onchain/index.js.map +1 -0
- package/dist/src/modules/onchain/tools.d.ts +2 -0
- package/dist/src/modules/onchain/tools.js +61 -0
- package/dist/src/modules/onchain/tools.js.map +1 -0
- package/dist/src/modules/types.d.ts +24 -0
- package/dist/src/modules/types.js +2 -0
- package/dist/src/modules/types.js.map +1 -0
- package/dist/src/modules/xmtp/index.d.ts +8 -0
- package/dist/src/modules/xmtp/index.js +14 -0
- package/dist/src/modules/xmtp/index.js.map +1 -0
- package/dist/src/policy.d.ts +45 -0
- package/dist/src/policy.js +164 -0
- package/dist/src/policy.js.map +1 -0
- package/dist/src/redaction.d.ts +13 -0
- package/dist/src/redaction.js +75 -0
- package/dist/src/redaction.js.map +1 -0
- package/dist/src/reliability.d.ts +16 -0
- package/dist/src/reliability.js +124 -0
- package/dist/src/reliability.js.map +1 -0
- package/dist/src/runner.d.ts +37 -0
- package/dist/src/runner.js +257 -0
- package/dist/src/runner.js.map +1 -0
- package/dist/src/scheduler.d.ts +22 -0
- package/dist/src/scheduler.js +61 -0
- package/dist/src/scheduler.js.map +1 -0
- package/dist/src/setup.d.ts +8 -0
- package/dist/src/setup.js +179 -0
- package/dist/src/setup.js.map +1 -0
- package/dist/src/soul.d.ts +1 -0
- package/dist/src/soul.js +15 -0
- package/dist/src/soul.js.map +1 -0
- package/package.json +56 -0
|
@@ -0,0 +1,729 @@
|
|
|
1
|
+
import * as p from "@clack/prompts";
|
|
2
|
+
import { chmod, mkdir, readFile, writeFile } from "node:fs/promises";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { createInterface } from "node:readline";
|
|
5
|
+
import { runSetup } from "../setup.js";
|
|
6
|
+
import { LLMClient } from "../llm.js";
|
|
7
|
+
import { connectFarcaster } from "./connect-farcaster.js";
|
|
8
|
+
import { scanForSecrets, redactSecrets, condenseDocs } from "./intake.js";
|
|
9
|
+
import { testTelegram, testGitHub, testDatabase, testOnchain } from "./connection-tests.js";
|
|
10
|
+
const TURTLE = `
|
|
11
|
+
\x1b[38;2;94;255;164m_____\x1b[0m \x1b[38;2;94;255;164m____\x1b[0m
|
|
12
|
+
\x1b[38;2;94;255;164m/ \\\x1b[0m \x1b[38;2;94;255;164m| o |\x1b[0m
|
|
13
|
+
\x1b[38;2;94;255;164m| |/ ___\\|\x1b[0m
|
|
14
|
+
\x1b[38;2;94;255;164m|_________/\x1b[0m
|
|
15
|
+
\x1b[38;2;94;255;164m|_|_| |_|_|\x1b[0m
|
|
16
|
+
|
|
17
|
+
\x1b[1mFreeTurtle\x1b[0m \x1b[2mv0.1\x1b[0m
|
|
18
|
+
`;
|
|
19
|
+
const HATCH_FRAMES = [
|
|
20
|
+
" 🥚",
|
|
21
|
+
" 🥚 .",
|
|
22
|
+
" 🥚 . .",
|
|
23
|
+
" 🥚💥",
|
|
24
|
+
" 🐢 !!",
|
|
25
|
+
];
|
|
26
|
+
/** Prompt for a value, offering to reuse an existing one from .env */
|
|
27
|
+
async function promptWithExisting(opts) {
|
|
28
|
+
if (opts.existing) {
|
|
29
|
+
const display = opts.mask
|
|
30
|
+
? "••••" + opts.existing.slice(-4)
|
|
31
|
+
: opts.existing.length > 20
|
|
32
|
+
? opts.existing.slice(0, 20) + "..."
|
|
33
|
+
: opts.existing;
|
|
34
|
+
const reuse = await p.confirm({
|
|
35
|
+
message: `${opts.message}: use existing? (${display})`,
|
|
36
|
+
initialValue: true,
|
|
37
|
+
});
|
|
38
|
+
if (p.isCancel(reuse))
|
|
39
|
+
return reuse;
|
|
40
|
+
if (reuse)
|
|
41
|
+
return opts.existing;
|
|
42
|
+
}
|
|
43
|
+
return p.text({
|
|
44
|
+
message: opts.message,
|
|
45
|
+
placeholder: opts.placeholder,
|
|
46
|
+
validate: (v) => (v?.trim() ? undefined : "Required"),
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
function sleep(ms) {
|
|
50
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Collect multiline text from stdin using readline.
|
|
54
|
+
* Ends when the user enters two consecutive blank lines or presses Ctrl+D.
|
|
55
|
+
*/
|
|
56
|
+
function readMultiline() {
|
|
57
|
+
return new Promise((resolve) => {
|
|
58
|
+
const lines = [];
|
|
59
|
+
let consecutiveBlanks = 0;
|
|
60
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout, prompt: "" });
|
|
61
|
+
rl.prompt();
|
|
62
|
+
rl.on("line", (line) => {
|
|
63
|
+
if (line.trim() === "") {
|
|
64
|
+
consecutiveBlanks++;
|
|
65
|
+
if (consecutiveBlanks >= 2) {
|
|
66
|
+
rl.close();
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
for (let i = 0; i < consecutiveBlanks; i++)
|
|
72
|
+
lines.push("");
|
|
73
|
+
consecutiveBlanks = 0;
|
|
74
|
+
lines.push(line);
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
rl.on("close", () => resolve(lines.join("\n").trim()));
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Detect paste: if a p.text answer is longer than this, it was probably a paste.
|
|
82
|
+
* Inspired by OpenClaw's burst coalescer — instead of draining stdin (which
|
|
83
|
+
* interferes with clack), we detect paste from the submitted value itself and
|
|
84
|
+
* capture it as business context.
|
|
85
|
+
*/
|
|
86
|
+
const PASTE_THRESHOLD = 200;
|
|
87
|
+
/**
|
|
88
|
+
* Wrapper around p.text that detects accidental paste (answer > PASTE_THRESHOLD chars).
|
|
89
|
+
* When paste is detected:
|
|
90
|
+
* 1. Captures the pasted text as business context on state
|
|
91
|
+
* 2. Drains remaining pasted lines from stdin via readMultiline()
|
|
92
|
+
* 3. Re-asks the original question
|
|
93
|
+
* Returns null on cancel.
|
|
94
|
+
*/
|
|
95
|
+
async function pasteAwareText(state, opts) {
|
|
96
|
+
// eslint-disable-next-line no-constant-condition
|
|
97
|
+
while (true) {
|
|
98
|
+
const result = await p.text(opts);
|
|
99
|
+
if (p.isCancel(result))
|
|
100
|
+
return null;
|
|
101
|
+
if (result.length > PASTE_THRESHOLD && !state.businessContext) {
|
|
102
|
+
// Looks like a paste burst — capture the first line and drain the rest
|
|
103
|
+
p.log.info("Looks like you pasted docs — collecting the rest...");
|
|
104
|
+
const overflow = await readMultiline();
|
|
105
|
+
state.businessContext = overflow ? result + "\n" + overflow : result;
|
|
106
|
+
p.log.success(`Saved ${(state.businessContext.length / 1000).toFixed(0)}K chars as business context.`);
|
|
107
|
+
// Re-ask the original question
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
return result;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
async function hatchAnimation() {
|
|
114
|
+
for (const frame of HATCH_FRAMES) {
|
|
115
|
+
process.stdout.write(`\r${frame} `);
|
|
116
|
+
await sleep(400);
|
|
117
|
+
}
|
|
118
|
+
process.stdout.write("\r \r");
|
|
119
|
+
}
|
|
120
|
+
async function runConnectionTest(name, test) {
|
|
121
|
+
const s = p.spinner();
|
|
122
|
+
s.start(`Testing ${name} connection`);
|
|
123
|
+
try {
|
|
124
|
+
await test();
|
|
125
|
+
s.stop(`${name} connected!`);
|
|
126
|
+
}
|
|
127
|
+
catch (err) {
|
|
128
|
+
const msg = err instanceof Error ? err.message : "Unknown error";
|
|
129
|
+
s.stop(`${name} test failed: ${msg}`);
|
|
130
|
+
p.log.warn("You can fix this later in ~/.freeturtle/.env");
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
export async function runInit(dir) {
|
|
134
|
+
console.log(TURTLE);
|
|
135
|
+
await hatchAnimation();
|
|
136
|
+
p.intro("FreeTurtle");
|
|
137
|
+
// Load existing .env values
|
|
138
|
+
const existingEnv = {};
|
|
139
|
+
try {
|
|
140
|
+
const content = await readFile(join(dir, ".env"), "utf-8");
|
|
141
|
+
for (const line of content.split("\n")) {
|
|
142
|
+
const match = line.match(/^([A-Z_]+)=(.+)$/);
|
|
143
|
+
if (match)
|
|
144
|
+
existingEnv[match[1]] = match[2];
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
catch {
|
|
148
|
+
// No existing .env
|
|
149
|
+
}
|
|
150
|
+
p.note([
|
|
151
|
+
"Let's hatch your AI CEO — an autonomous agent that",
|
|
152
|
+
"posts content, chats with you, writes strategy, and",
|
|
153
|
+
"runs operations for your project.",
|
|
154
|
+
"",
|
|
155
|
+
"It'll only take a few minutes. You can always change",
|
|
156
|
+
"settings later in ~/.freeturtle/",
|
|
157
|
+
].join("\n"), "🥚 Welcome");
|
|
158
|
+
const state = {
|
|
159
|
+
projectName: "",
|
|
160
|
+
description: "",
|
|
161
|
+
ceoName: "",
|
|
162
|
+
voice: "casual",
|
|
163
|
+
founderName: "",
|
|
164
|
+
businessContext: "",
|
|
165
|
+
farcaster: false,
|
|
166
|
+
neynarKey: "",
|
|
167
|
+
signerUuid: "",
|
|
168
|
+
fid: "",
|
|
169
|
+
telegram: false,
|
|
170
|
+
telegramToken: "",
|
|
171
|
+
telegramOwner: "",
|
|
172
|
+
github: false,
|
|
173
|
+
githubToken: "",
|
|
174
|
+
database: false,
|
|
175
|
+
dbUrl: "",
|
|
176
|
+
onchain: false,
|
|
177
|
+
rpcUrl: "",
|
|
178
|
+
contracts: [],
|
|
179
|
+
};
|
|
180
|
+
const steps = [
|
|
181
|
+
// 1. Project name
|
|
182
|
+
async () => {
|
|
183
|
+
const result = await pasteAwareText(state, {
|
|
184
|
+
message: "What project will this AI CEO be running?",
|
|
185
|
+
placeholder: "e.g. Tortoise, Acme Corp, My Newsletter",
|
|
186
|
+
defaultValue: state.projectName || undefined,
|
|
187
|
+
validate: (v) => (v?.trim() ? undefined : "Required"),
|
|
188
|
+
});
|
|
189
|
+
if (result === null)
|
|
190
|
+
return false;
|
|
191
|
+
state.projectName = result;
|
|
192
|
+
return true;
|
|
193
|
+
},
|
|
194
|
+
// 2. Description
|
|
195
|
+
async () => {
|
|
196
|
+
const result = await pasteAwareText(state, {
|
|
197
|
+
message: "Describe the project in a sentence or two.",
|
|
198
|
+
placeholder: "A music platform on Farcaster/Base for independent artists",
|
|
199
|
+
defaultValue: state.description || undefined,
|
|
200
|
+
});
|
|
201
|
+
if (result === null)
|
|
202
|
+
return false;
|
|
203
|
+
state.description = result;
|
|
204
|
+
return true;
|
|
205
|
+
},
|
|
206
|
+
// 3. Business context (optional, multiline-safe)
|
|
207
|
+
async () => {
|
|
208
|
+
// Skip if we already captured context from a paste burst
|
|
209
|
+
if (state.businessContext) {
|
|
210
|
+
p.log.info(`Using ${(state.businessContext.length / 1000).toFixed(0)}K chars of business context you pasted earlier.`);
|
|
211
|
+
return true;
|
|
212
|
+
}
|
|
213
|
+
const wantContext = await p.confirm({
|
|
214
|
+
message: "Want to dump docs about your business? (pitch deck, readme, strategy notes, etc.)",
|
|
215
|
+
initialValue: false,
|
|
216
|
+
});
|
|
217
|
+
if (p.isCancel(wantContext))
|
|
218
|
+
return false;
|
|
219
|
+
if (!wantContext)
|
|
220
|
+
return true;
|
|
221
|
+
p.log.info("Paste or type below. Two blank lines to finish.");
|
|
222
|
+
let text = await readMultiline();
|
|
223
|
+
if (text) {
|
|
224
|
+
// Secret scan
|
|
225
|
+
const secrets = scanForSecrets(text);
|
|
226
|
+
if (secrets.length > 0) {
|
|
227
|
+
p.log.warn("Detected possible secrets in your text:");
|
|
228
|
+
for (const s of secrets) {
|
|
229
|
+
p.log.warn(` ${s}`);
|
|
230
|
+
}
|
|
231
|
+
const redact = await p.confirm({
|
|
232
|
+
message: "Redact detected secrets?",
|
|
233
|
+
initialValue: true,
|
|
234
|
+
});
|
|
235
|
+
if (!p.isCancel(redact) && redact) {
|
|
236
|
+
text = redactSecrets(text);
|
|
237
|
+
p.log.success("Secrets redacted.");
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
state.businessContext = text;
|
|
241
|
+
p.log.info(`Got it — ${(text.length / 1000).toFixed(0)}K chars of context.`);
|
|
242
|
+
}
|
|
243
|
+
return true;
|
|
244
|
+
},
|
|
245
|
+
// 4. CEO name
|
|
246
|
+
async () => {
|
|
247
|
+
const result = await pasteAwareText(state, {
|
|
248
|
+
message: "What should your AI CEO be called?",
|
|
249
|
+
placeholder: "e.g. Shelly, Atlas, Nova",
|
|
250
|
+
defaultValue: state.ceoName || undefined,
|
|
251
|
+
validate: (v) => (v?.trim() ? undefined : "Required"),
|
|
252
|
+
});
|
|
253
|
+
if (result === null)
|
|
254
|
+
return false;
|
|
255
|
+
state.ceoName = result;
|
|
256
|
+
return true;
|
|
257
|
+
},
|
|
258
|
+
// 5. Voice
|
|
259
|
+
async () => {
|
|
260
|
+
const result = await p.select({
|
|
261
|
+
message: "How should your CEO communicate?",
|
|
262
|
+
options: [
|
|
263
|
+
{ value: "casual", label: "Casual", hint: "friendly, like a smart friend" },
|
|
264
|
+
{ value: "professional", label: "Professional", hint: "clear, authoritative, data-driven" },
|
|
265
|
+
{ value: "minimalist", label: "Minimalist", hint: "brief and direct, no fluff" },
|
|
266
|
+
],
|
|
267
|
+
initialValue: state.voice,
|
|
268
|
+
});
|
|
269
|
+
if (p.isCancel(result))
|
|
270
|
+
return false;
|
|
271
|
+
state.voice = result;
|
|
272
|
+
return true;
|
|
273
|
+
},
|
|
274
|
+
// 6. Founder name
|
|
275
|
+
async () => {
|
|
276
|
+
const result = await pasteAwareText(state, {
|
|
277
|
+
message: "Your name (the founder).",
|
|
278
|
+
defaultValue: state.founderName || undefined,
|
|
279
|
+
validate: (v) => (v?.trim() ? undefined : "Required"),
|
|
280
|
+
});
|
|
281
|
+
if (result === null)
|
|
282
|
+
return false;
|
|
283
|
+
state.founderName = result;
|
|
284
|
+
return true;
|
|
285
|
+
},
|
|
286
|
+
// 7. Farcaster
|
|
287
|
+
async () => {
|
|
288
|
+
const enable = await p.confirm({
|
|
289
|
+
message: "Connect Farcaster? (post and read casts via Neynar)",
|
|
290
|
+
initialValue: state.farcaster,
|
|
291
|
+
});
|
|
292
|
+
if (p.isCancel(enable))
|
|
293
|
+
return false;
|
|
294
|
+
state.farcaster = enable;
|
|
295
|
+
if (enable) {
|
|
296
|
+
const result = await connectFarcaster(dir);
|
|
297
|
+
if (result) {
|
|
298
|
+
state.neynarKey = result.neynarKey;
|
|
299
|
+
state.signerUuid = result.signerUuid;
|
|
300
|
+
state.fid = result.fid;
|
|
301
|
+
}
|
|
302
|
+
else {
|
|
303
|
+
state.farcaster = false;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
return true;
|
|
307
|
+
},
|
|
308
|
+
// 8. Telegram
|
|
309
|
+
async () => {
|
|
310
|
+
const enable = await p.confirm({
|
|
311
|
+
message: "Connect Telegram? (chat with your CEO via bot)",
|
|
312
|
+
initialValue: state.telegram,
|
|
313
|
+
});
|
|
314
|
+
if (p.isCancel(enable))
|
|
315
|
+
return false;
|
|
316
|
+
state.telegram = enable;
|
|
317
|
+
if (enable) {
|
|
318
|
+
p.note([
|
|
319
|
+
"1. Message @BotFather on Telegram \u2192 /newbot",
|
|
320
|
+
" Choose a name and username (must end in 'bot')",
|
|
321
|
+
" BotFather replies with your bot token",
|
|
322
|
+
"",
|
|
323
|
+
"2. To get your user ID, message @userinfobot on Telegram",
|
|
324
|
+
" It replies immediately with your numeric ID",
|
|
325
|
+
].join("\n"), "Telegram setup");
|
|
326
|
+
const token = await promptWithExisting({ message: "Bot token", existing: existingEnv.TELEGRAM_BOT_TOKEN, mask: true });
|
|
327
|
+
if (p.isCancel(token)) {
|
|
328
|
+
state.telegram = false;
|
|
329
|
+
return true;
|
|
330
|
+
}
|
|
331
|
+
state.telegramToken = token;
|
|
332
|
+
// Test the token
|
|
333
|
+
await runConnectionTest("Telegram", () => testTelegram(state.telegramToken));
|
|
334
|
+
const owner = await promptWithExisting({ message: "Your Telegram user ID (numeric, from @userinfobot)", existing: existingEnv.TELEGRAM_OWNER_ID });
|
|
335
|
+
if (p.isCancel(owner)) {
|
|
336
|
+
state.telegram = false;
|
|
337
|
+
return true;
|
|
338
|
+
}
|
|
339
|
+
state.telegramOwner = owner;
|
|
340
|
+
}
|
|
341
|
+
return true;
|
|
342
|
+
},
|
|
343
|
+
// 9. GitHub
|
|
344
|
+
async () => {
|
|
345
|
+
const enable = await p.confirm({
|
|
346
|
+
message: "Connect GitHub? (issues and file commits)",
|
|
347
|
+
initialValue: state.github,
|
|
348
|
+
});
|
|
349
|
+
if (p.isCancel(enable))
|
|
350
|
+
return false;
|
|
351
|
+
state.github = enable;
|
|
352
|
+
if (enable) {
|
|
353
|
+
p.note([
|
|
354
|
+
"1. Go to github.com/settings/tokens",
|
|
355
|
+
"2. Generate new token \u2192 Fine-grained token",
|
|
356
|
+
"3. Select the repos your CEO should access",
|
|
357
|
+
"4. Grant permissions: Issues (read/write), Contents (read/write)",
|
|
358
|
+
"5. Copy the token \u2014 you won't see it again",
|
|
359
|
+
].join("\n"), "GitHub setup");
|
|
360
|
+
const token = await promptWithExisting({ message: "GitHub personal access token", existing: existingEnv.GITHUB_TOKEN, mask: true });
|
|
361
|
+
if (p.isCancel(token)) {
|
|
362
|
+
state.github = false;
|
|
363
|
+
return true;
|
|
364
|
+
}
|
|
365
|
+
state.githubToken = token;
|
|
366
|
+
// Test the token
|
|
367
|
+
await runConnectionTest("GitHub", () => testGitHub(state.githubToken));
|
|
368
|
+
}
|
|
369
|
+
return true;
|
|
370
|
+
},
|
|
371
|
+
// 10. Database
|
|
372
|
+
async () => {
|
|
373
|
+
const enable = await p.confirm({
|
|
374
|
+
message: "Connect a database? (read-only Postgres queries)",
|
|
375
|
+
initialValue: state.database,
|
|
376
|
+
});
|
|
377
|
+
if (p.isCancel(enable))
|
|
378
|
+
return false;
|
|
379
|
+
state.database = enable;
|
|
380
|
+
if (enable) {
|
|
381
|
+
p.note([
|
|
382
|
+
"Provide a Postgres connection string. The CEO can only",
|
|
383
|
+
"run read-only queries \u2014 all writes are blocked.",
|
|
384
|
+
"",
|
|
385
|
+
"Supabase: Settings \u2192 Database \u2192 Connection string (URI)",
|
|
386
|
+
"Local: postgresql://localhost/your_db",
|
|
387
|
+
].join("\n"), "Database setup");
|
|
388
|
+
const url = await promptWithExisting({ message: "Database connection URL", existing: existingEnv.DATABASE_URL, placeholder: "postgres://user:pass@host:5432/dbname", mask: true });
|
|
389
|
+
if (p.isCancel(url)) {
|
|
390
|
+
state.database = false;
|
|
391
|
+
return true;
|
|
392
|
+
}
|
|
393
|
+
state.dbUrl = url;
|
|
394
|
+
// Test the connection
|
|
395
|
+
await runConnectionTest("Database", () => testDatabase(state.dbUrl));
|
|
396
|
+
}
|
|
397
|
+
return true;
|
|
398
|
+
},
|
|
399
|
+
// 11. Onchain
|
|
400
|
+
async () => {
|
|
401
|
+
const enable = await p.confirm({
|
|
402
|
+
message: "Connect onchain? (read contracts and balances on Base)",
|
|
403
|
+
initialValue: state.onchain,
|
|
404
|
+
});
|
|
405
|
+
if (p.isCancel(enable))
|
|
406
|
+
return false;
|
|
407
|
+
state.onchain = enable;
|
|
408
|
+
if (enable) {
|
|
409
|
+
p.note([
|
|
410
|
+
"Provide a Base mainnet RPC URL. Read-only \u2014 no wallet or signing.",
|
|
411
|
+
"",
|
|
412
|
+
"Free options:",
|
|
413
|
+
" Public: https://mainnet.base.org",
|
|
414
|
+
" Coinbase: sign up at portal.cdp.coinbase.com",
|
|
415
|
+
" Alchemy: sign up at alchemy.com",
|
|
416
|
+
" Infura: sign up at infura.io",
|
|
417
|
+
].join("\n"), "Onchain setup");
|
|
418
|
+
const url = await promptWithExisting({ message: "Base RPC URL", existing: existingEnv.RPC_URL, placeholder: "https://mainnet.base.org" });
|
|
419
|
+
if (p.isCancel(url)) {
|
|
420
|
+
state.onchain = false;
|
|
421
|
+
return true;
|
|
422
|
+
}
|
|
423
|
+
state.rpcUrl = url;
|
|
424
|
+
// Test the RPC
|
|
425
|
+
await runConnectionTest("RPC", () => testOnchain(state.rpcUrl));
|
|
426
|
+
// Collect smart contracts
|
|
427
|
+
const addContracts = await p.confirm({
|
|
428
|
+
message: "Add smart contracts for your CEO to track?",
|
|
429
|
+
initialValue: state.contracts.length > 0,
|
|
430
|
+
});
|
|
431
|
+
if (p.isCancel(addContracts))
|
|
432
|
+
return true;
|
|
433
|
+
if (addContracts) {
|
|
434
|
+
p.note([
|
|
435
|
+
"Add contracts your business uses on Base.",
|
|
436
|
+
"Your CEO will be able to read data from these.",
|
|
437
|
+
"",
|
|
438
|
+
"You can add more later by editing soul.md.",
|
|
439
|
+
].join("\n"), "Smart contracts");
|
|
440
|
+
let adding = true;
|
|
441
|
+
while (adding) {
|
|
442
|
+
const name = await p.text({
|
|
443
|
+
message: "Contract name",
|
|
444
|
+
placeholder: "e.g. MyToken, Marketplace, NFT Collection",
|
|
445
|
+
validate: (v) => (v?.trim() ? undefined : "Required"),
|
|
446
|
+
});
|
|
447
|
+
if (p.isCancel(name))
|
|
448
|
+
break;
|
|
449
|
+
const address = await p.text({
|
|
450
|
+
message: "Contract address",
|
|
451
|
+
placeholder: "0x...",
|
|
452
|
+
validate: (v) => {
|
|
453
|
+
if (!v?.trim())
|
|
454
|
+
return "Required";
|
|
455
|
+
if (!/^0x[a-fA-F0-9]{40}$/.test(v.trim()))
|
|
456
|
+
return "Must be a valid 0x address (42 characters)";
|
|
457
|
+
return undefined;
|
|
458
|
+
},
|
|
459
|
+
});
|
|
460
|
+
if (p.isCancel(address))
|
|
461
|
+
break;
|
|
462
|
+
state.contracts.push({ name, address });
|
|
463
|
+
p.log.success(`Added ${name} (${address})`);
|
|
464
|
+
const more = await p.confirm({
|
|
465
|
+
message: "Add another contract?",
|
|
466
|
+
initialValue: false,
|
|
467
|
+
});
|
|
468
|
+
if (p.isCancel(more) || !more)
|
|
469
|
+
adding = false;
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
return true;
|
|
474
|
+
},
|
|
475
|
+
];
|
|
476
|
+
// Run steps — Ctrl+C exits immediately
|
|
477
|
+
for (let i = 0; i < steps.length; i++) {
|
|
478
|
+
const ok = await steps[i]();
|
|
479
|
+
if (!ok) {
|
|
480
|
+
p.cancel("Setup cancelled.");
|
|
481
|
+
process.exit(0);
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
// --- LLM setup ---
|
|
485
|
+
p.log.step("Let's pick a brain for your CEO.");
|
|
486
|
+
const setupResult = await runSetup(dir);
|
|
487
|
+
// --- Condense business context into soul if provided ---
|
|
488
|
+
let soulContent;
|
|
489
|
+
if (state.businessContext) {
|
|
490
|
+
const llm = new LLMClient({
|
|
491
|
+
provider: setupResult.provider,
|
|
492
|
+
model: setupResult.model,
|
|
493
|
+
apiKey: setupResult.apiKey,
|
|
494
|
+
oauthToken: setupResult.oauthToken,
|
|
495
|
+
});
|
|
496
|
+
const s = p.spinner();
|
|
497
|
+
s.start(`Distilling your context into ${state.ceoName}'s soul`);
|
|
498
|
+
try {
|
|
499
|
+
soulContent = await condenseDocs(state.businessContext, {
|
|
500
|
+
ceoName: state.ceoName,
|
|
501
|
+
projectName: state.projectName,
|
|
502
|
+
description: state.description,
|
|
503
|
+
founderName: state.founderName,
|
|
504
|
+
voice: state.voice,
|
|
505
|
+
}, llm, state.contracts);
|
|
506
|
+
s.stop("Soul distilled from your business context!");
|
|
507
|
+
}
|
|
508
|
+
catch (err) {
|
|
509
|
+
const msg = err instanceof Error ? err.message : "Unknown error";
|
|
510
|
+
s.stop(`Condensation failed: ${msg}`);
|
|
511
|
+
p.log.warn("Falling back to the standard template.");
|
|
512
|
+
soulContent = undefined;
|
|
513
|
+
}
|
|
514
|
+
// Show and confirm
|
|
515
|
+
if (soulContent) {
|
|
516
|
+
p.note(soulContent, `${state.ceoName}'s soul`);
|
|
517
|
+
const accept = await p.select({
|
|
518
|
+
message: "How does this look?",
|
|
519
|
+
options: [
|
|
520
|
+
{ value: "accept", label: "Looks good" },
|
|
521
|
+
{ value: "retry", label: "Try again" },
|
|
522
|
+
{ value: "template", label: "Use the standard template instead" },
|
|
523
|
+
],
|
|
524
|
+
});
|
|
525
|
+
if (p.isCancel(accept)) {
|
|
526
|
+
// Use what we have
|
|
527
|
+
}
|
|
528
|
+
else if (accept === "retry") {
|
|
529
|
+
const s2 = p.spinner();
|
|
530
|
+
s2.start("Regenerating...");
|
|
531
|
+
try {
|
|
532
|
+
soulContent = await condenseDocs(state.businessContext, {
|
|
533
|
+
ceoName: state.ceoName,
|
|
534
|
+
projectName: state.projectName,
|
|
535
|
+
description: state.description,
|
|
536
|
+
founderName: state.founderName,
|
|
537
|
+
voice: state.voice,
|
|
538
|
+
}, llm, state.contracts);
|
|
539
|
+
s2.stop("Done!");
|
|
540
|
+
p.note(soulContent, `${state.ceoName}'s soul (v2)`);
|
|
541
|
+
}
|
|
542
|
+
catch {
|
|
543
|
+
s2.stop("Failed again, using standard template.");
|
|
544
|
+
soulContent = undefined;
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
else if (accept === "template") {
|
|
548
|
+
soulContent = undefined;
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
// --- Generate workspace with playful tasks ---
|
|
553
|
+
const modules = [
|
|
554
|
+
state.farcaster && "Farcaster",
|
|
555
|
+
state.telegram && "Telegram",
|
|
556
|
+
state.github && "GitHub",
|
|
557
|
+
state.database && "Database",
|
|
558
|
+
state.onchain && "Onchain",
|
|
559
|
+
].filter(Boolean);
|
|
560
|
+
await p.tasks([
|
|
561
|
+
{
|
|
562
|
+
title: `Building ${state.ceoName}'s nest`,
|
|
563
|
+
task: async () => {
|
|
564
|
+
await mkdir(join(dir, "workspace", "memory", "session-notes"), { recursive: true });
|
|
565
|
+
await mkdir(join(dir, "strategy"), { recursive: true });
|
|
566
|
+
await sleep(300);
|
|
567
|
+
return "Directories created";
|
|
568
|
+
},
|
|
569
|
+
},
|
|
570
|
+
{
|
|
571
|
+
title: `Teaching ${state.ceoName} to speak`,
|
|
572
|
+
task: async () => {
|
|
573
|
+
if (soulContent) {
|
|
574
|
+
// Use the LLM-condensed soul
|
|
575
|
+
await writeFile(join(dir, "soul.md"), soulContent + "\n", "utf-8");
|
|
576
|
+
}
|
|
577
|
+
else {
|
|
578
|
+
// Standard template
|
|
579
|
+
const VOICE = {
|
|
580
|
+
casual: "- Friendly and approachable, like talking to a smart friend\n- Uses casual language, occasional humor\n- Keeps things concise and genuine",
|
|
581
|
+
professional: "- Clear and authoritative, backed by data\n- Professional tone without being stiff\n- Focuses on insights and value",
|
|
582
|
+
minimalist: "- Brief and direct\n- Says more with less\n- No fluff, no filler",
|
|
583
|
+
};
|
|
584
|
+
await writeFile(join(dir, "soul.md"), `# ${state.ceoName}
|
|
585
|
+
|
|
586
|
+
## Identity
|
|
587
|
+
${state.ceoName} is the AI CEO for ${state.projectName}.
|
|
588
|
+
|
|
589
|
+
## Voice
|
|
590
|
+
${VOICE[state.voice] ?? VOICE.casual}
|
|
591
|
+
|
|
592
|
+
## Knowledge
|
|
593
|
+
${state.description}
|
|
594
|
+
${state.contracts.length > 0 ? `\n### Smart Contracts (Base)\n${state.contracts.map((c) => `- ${c.name}: \`${c.address}\``).join("\n")}\n` : ""}
|
|
595
|
+
## Goals
|
|
596
|
+
- Grow the project and community
|
|
597
|
+
- Create engaging content
|
|
598
|
+
- Develop and execute strategy
|
|
599
|
+
- Support the founder's objectives
|
|
600
|
+
|
|
601
|
+
## Values & Boundaries
|
|
602
|
+
- Be honest and transparent
|
|
603
|
+
- Don't make claims you can't back up
|
|
604
|
+
- Escalate to the founder when unsure
|
|
605
|
+
|
|
606
|
+
## Founder
|
|
607
|
+
${state.founderName}.
|
|
608
|
+
`, "utf-8");
|
|
609
|
+
}
|
|
610
|
+
await sleep(200);
|
|
611
|
+
return "Soul written";
|
|
612
|
+
},
|
|
613
|
+
},
|
|
614
|
+
{
|
|
615
|
+
title: "Configuring the shell",
|
|
616
|
+
task: async () => {
|
|
617
|
+
const configLines = [
|
|
618
|
+
"# FreeTurtle Config\n",
|
|
619
|
+
"## LLM",
|
|
620
|
+
"- provider: claude_api",
|
|
621
|
+
"- model: claude-sonnet-4-5-20250514",
|
|
622
|
+
"- max_tokens: 4096",
|
|
623
|
+
"- api_key_env: ANTHROPIC_API_KEY",
|
|
624
|
+
"",
|
|
625
|
+
"## Cron",
|
|
626
|
+
"### post",
|
|
627
|
+
"- schedule: 0 */8 * * *",
|
|
628
|
+
"- prompt: Check for any queued posts. If there's a new upload worth sharing, share it. Otherwise write an original post.",
|
|
629
|
+
"",
|
|
630
|
+
"### strategy",
|
|
631
|
+
"- schedule: 0 4 * * 0",
|
|
632
|
+
"- prompt: Analyze posting history, engagement, platform data. Write a strategy brief.",
|
|
633
|
+
"- output: strategy/{{date}}.md",
|
|
634
|
+
"",
|
|
635
|
+
"## Channels",
|
|
636
|
+
"### terminal",
|
|
637
|
+
"- enabled: true",
|
|
638
|
+
"",
|
|
639
|
+
"### telegram",
|
|
640
|
+
`- enabled: ${state.telegram}`,
|
|
641
|
+
"",
|
|
642
|
+
"## Modules",
|
|
643
|
+
"### farcaster",
|
|
644
|
+
`- enabled: ${state.farcaster}`,
|
|
645
|
+
"",
|
|
646
|
+
"### database",
|
|
647
|
+
`- enabled: ${state.database}`,
|
|
648
|
+
"",
|
|
649
|
+
"### github",
|
|
650
|
+
`- enabled: ${state.github}`,
|
|
651
|
+
"",
|
|
652
|
+
"### onchain",
|
|
653
|
+
`- enabled: ${state.onchain}`,
|
|
654
|
+
"",
|
|
655
|
+
"## Policy",
|
|
656
|
+
"### github",
|
|
657
|
+
"- approval_required_branches: main",
|
|
658
|
+
"",
|
|
659
|
+
"### approvals",
|
|
660
|
+
"- timeout_seconds: 300",
|
|
661
|
+
"- fail_mode: deny",
|
|
662
|
+
"",
|
|
663
|
+
];
|
|
664
|
+
await writeFile(join(dir, "config.md"), configLines.join("\n"), "utf-8");
|
|
665
|
+
await sleep(200);
|
|
666
|
+
return "Config saved";
|
|
667
|
+
},
|
|
668
|
+
},
|
|
669
|
+
{
|
|
670
|
+
title: "Locking up the secrets",
|
|
671
|
+
task: async () => {
|
|
672
|
+
const envLines = [];
|
|
673
|
+
if (state.neynarKey)
|
|
674
|
+
envLines.push(`NEYNAR_API_KEY=${state.neynarKey}`);
|
|
675
|
+
if (state.signerUuid)
|
|
676
|
+
envLines.push(`FARCASTER_SIGNER_UUID=${state.signerUuid}`);
|
|
677
|
+
if (state.fid)
|
|
678
|
+
envLines.push(`FARCASTER_FID=${state.fid}`);
|
|
679
|
+
if (state.telegramToken)
|
|
680
|
+
envLines.push(`TELEGRAM_BOT_TOKEN=${state.telegramToken}`);
|
|
681
|
+
if (state.telegramOwner)
|
|
682
|
+
envLines.push(`TELEGRAM_OWNER_ID=${state.telegramOwner}`);
|
|
683
|
+
if (state.githubToken)
|
|
684
|
+
envLines.push(`GITHUB_TOKEN=${state.githubToken}`);
|
|
685
|
+
if (state.dbUrl)
|
|
686
|
+
envLines.push(`DATABASE_URL=${state.dbUrl}`);
|
|
687
|
+
if (state.rpcUrl)
|
|
688
|
+
envLines.push(`RPC_URL=${state.rpcUrl}`);
|
|
689
|
+
const envFilePath = join(dir, ".env");
|
|
690
|
+
await writeFile(envFilePath, envLines.join("\n") + "\n", "utf-8");
|
|
691
|
+
await chmod(envFilePath, 0o600);
|
|
692
|
+
await sleep(150);
|
|
693
|
+
return ".env secured (chmod 600)";
|
|
694
|
+
},
|
|
695
|
+
},
|
|
696
|
+
{
|
|
697
|
+
title: "Preparing memory banks",
|
|
698
|
+
task: async () => {
|
|
699
|
+
await writeFile(join(dir, "workspace", "memory", "posting-log.json"), "[]", "utf-8");
|
|
700
|
+
await writeFile(join(dir, "workspace", "memory", "post-queue.json"), "[]", "utf-8");
|
|
701
|
+
await writeFile(join(dir, "workspace", "HEARTBEAT.md"), `# Heartbeat Checklist
|
|
702
|
+
|
|
703
|
+
- Check if there are queued posts that need to go out
|
|
704
|
+
- Check if there are unanswered mentions
|
|
705
|
+
- Check if any scheduled tasks failed recently
|
|
706
|
+
- Note anything that needs the founder's attention
|
|
707
|
+
`, "utf-8");
|
|
708
|
+
await sleep(150);
|
|
709
|
+
return "Memory initialized";
|
|
710
|
+
},
|
|
711
|
+
},
|
|
712
|
+
]);
|
|
713
|
+
p.log.success(`${state.ceoName} is taking shape!`);
|
|
714
|
+
if (modules.length > 0) {
|
|
715
|
+
p.log.info(`Connected: ${modules.join(", ")}`);
|
|
716
|
+
}
|
|
717
|
+
console.log(`
|
|
718
|
+
${state.ceoName} is ready. 🐢
|
|
719
|
+
|
|
720
|
+
Start: freeturtle start
|
|
721
|
+
Chat: freeturtle start --chat
|
|
722
|
+
Status: freeturtle status
|
|
723
|
+
|
|
724
|
+
Config: ~/.freeturtle/config.md
|
|
725
|
+
Soul: ~/.freeturtle/soul.md
|
|
726
|
+
`);
|
|
727
|
+
p.outro(`Go get 'em, ${state.ceoName}!`);
|
|
728
|
+
}
|
|
729
|
+
//# sourceMappingURL=init.js.map
|