niahere 0.2.91 → 0.3.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/README.md +2 -1
- package/package.json +1 -1
- package/skills/nia-phone/SKILL.md +34 -17
- package/src/channels/phone/index.ts +6 -3
- package/src/cli/phone.ts +12 -8
- package/src/commands/init.ts +67 -0
- package/src/mcp/tools.ts +1 -1
package/README.md
CHANGED
|
@@ -36,13 +36,14 @@ nia start # starts daemon + registers OS service
|
|
|
36
36
|
|
|
37
37
|
- **Telegram** — message your agent from your phone, typing indicator while processing
|
|
38
38
|
- **Slack** — Socket Mode bot with thread awareness, thinking emoji, watch channels for proactive monitoring
|
|
39
|
+
- **Phone (voice)** — Twilio + OpenAI Realtime. Inbound calls from allowlisted contacts and outbound calls via `place_call` MCP tool. Scheduled jobs can dial you (morning standup, evening retro, escalation). See `/nia-phone` skill.
|
|
39
40
|
- **Terminal chat** — REPL with session resume support
|
|
40
41
|
- **Scheduled jobs** — recurring jobs and crons that run Claude and can message you back. Stateful by default (working memory), per-job model routing for cost savings
|
|
41
42
|
- **Persona system** — customizable identity, soul, owner profile, rules, and memory (preloaded every session)
|
|
42
43
|
- **Agents** — domain specialists (marketer, senior-dev) via Claude Agent SDK subagents
|
|
43
44
|
- **Skills** — loads skills from multiple directories, invokable as slash commands
|
|
44
45
|
- **Cross-platform service** — launchd (macOS), systemd (Linux), service-aware restart
|
|
45
|
-
- **MCP tools** —
|
|
46
|
+
- **MCP tools** — 21 tools for job management, messaging, memory, rules, channel control, and outbound phone calls
|
|
46
47
|
- **Background memory consolidation** — stages memory candidates from conversations automatically
|
|
47
48
|
- **Session summaries** — optional handoff notes between sessions for continuity
|
|
48
49
|
- **Backups** — `nia backup` with auto-backup before updates
|
package/package.json
CHANGED
|
@@ -26,26 +26,43 @@ Voice to the OpenAI Realtime API. It exposes:
|
|
|
26
26
|
Transcripts persist to the `messages` table with `channel = 'phone'` and
|
|
27
27
|
`room = phone-<callSid>`.
|
|
28
28
|
|
|
29
|
-
##
|
|
29
|
+
## Configuration
|
|
30
30
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
31
|
+
Phone config lives in `~/.niahere/config.yaml` under `channels.phone` —
|
|
32
|
+
same place as `channels.telegram` and `channels.slack`. Every field is
|
|
33
|
+
overridable by the matching env var if you prefer `.env` for secrets.
|
|
34
|
+
|
|
35
|
+
```yaml
|
|
36
|
+
# ~/.niahere/config.yaml
|
|
37
|
+
channels:
|
|
38
|
+
phone:
|
|
39
|
+
twilio_sid: AC... # Account SID — or an API Key SID (SK…)
|
|
40
|
+
twilio_secret: ... # Auth Token if SID is AC, API Key Secret if SID is SK
|
|
41
|
+
twilio_auth_token:
|
|
42
|
+
... # Required when twilio_sid is an API Key (SK…); signs webhooks.
|
|
43
|
+
# Omit if twilio_secret is already the Auth Token.
|
|
44
|
+
from_number: "+1..." # Your Twilio number (E.164)
|
|
45
|
+
owner_number: "+91..." # Owner's phone (E.164) — highest-trust caller
|
|
46
|
+
public_base_url: https://nia.example.com # No trailing slash
|
|
47
|
+
openai_api_key: sk-proj-... # For the Realtime voice loop
|
|
48
|
+
|
|
49
|
+
# Optional
|
|
50
|
+
port: 7079 # Local port the webhook server binds to
|
|
51
|
+
allowlist: ["+12025550100"] # Extra allowed inbound callers (E.164)
|
|
52
|
+
voice: marin # Realtime voice (marin | cedar | shimmer | coral | alloy | ash | …)
|
|
53
|
+
realtime_model: gpt-realtime
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Env overrides (use these if you'd rather keep secrets in `.env`):
|
|
57
|
+
|
|
58
|
+
```
|
|
59
|
+
TWILIO_SID, TWILIO_SECRET, TWILIO_AUTH_TOKEN
|
|
60
|
+
PHONE_FROM_NUMBER, PRIMARY_PHONE_USER
|
|
61
|
+
PUBLIC_BASE_URL, OPENAI_API_KEY
|
|
62
|
+
PHONE_PORT, PHONE_ALLOWLIST (comma-separated), PHONE_VOICE, PHONE_REALTIME_MODEL
|
|
46
63
|
```
|
|
47
64
|
|
|
48
|
-
`nia phone status` prints which
|
|
65
|
+
`nia phone status` prints which fields are set / missing.
|
|
49
66
|
|
|
50
67
|
## Cloudflared named tunnel (production)
|
|
51
68
|
|
|
@@ -370,17 +370,20 @@ class PhoneChannel implements Channel {
|
|
|
370
370
|
private requireCreds(): { accountSid: string; authToken: string } {
|
|
371
371
|
const sid = this.cfg.twilio_sid;
|
|
372
372
|
const secret = this.cfg.twilio_secret;
|
|
373
|
-
if (!sid || !secret)
|
|
373
|
+
if (!sid || !secret)
|
|
374
|
+
throw new Error("phone: channels.phone.twilio_sid and twilio_secret not set (config.yaml or env)");
|
|
374
375
|
return { accountSid: sid, authToken: secret };
|
|
375
376
|
}
|
|
376
377
|
|
|
377
378
|
private requirePublicBaseUrl(): string {
|
|
378
|
-
if (!this.cfg.public_base_url)
|
|
379
|
+
if (!this.cfg.public_base_url)
|
|
380
|
+
throw new Error("phone: channels.phone.public_base_url not set (config.yaml or PUBLIC_BASE_URL env)");
|
|
379
381
|
return this.cfg.public_base_url;
|
|
380
382
|
}
|
|
381
383
|
|
|
382
384
|
private requireFromNumber(): string {
|
|
383
|
-
if (!this.cfg.from_number)
|
|
385
|
+
if (!this.cfg.from_number)
|
|
386
|
+
throw new Error("phone: channels.phone.from_number not set (config.yaml or PHONE_FROM_NUMBER env)");
|
|
384
387
|
return this.cfg.from_number;
|
|
385
388
|
}
|
|
386
389
|
|
package/src/cli/phone.ts
CHANGED
|
@@ -45,7 +45,7 @@ async function phoneCallCommand(): Promise<void> {
|
|
|
45
45
|
const channel = createPhoneChannel();
|
|
46
46
|
if (!channel) {
|
|
47
47
|
fail(
|
|
48
|
-
"Phone channel not configured.
|
|
48
|
+
"Phone channel not configured. Set channels.phone.{twilio_sid,twilio_secret,from_number} in ~/.niahere/config.yaml (also channels.phone.{openai_api_key,public_base_url} for the realtime voice loop). Env vars TWILIO_SID / TWILIO_SECRET / PHONE_FROM_NUMBER / OPENAI_API_KEY / PUBLIC_BASE_URL override if you prefer .env.",
|
|
49
49
|
);
|
|
50
50
|
}
|
|
51
51
|
|
|
@@ -53,13 +53,13 @@ async function phoneCallCommand(): Promise<void> {
|
|
|
53
53
|
const cfg = getConfig().channels.phone;
|
|
54
54
|
console.log(`${ICON_PASS} phone server up on :${cfg.port}`);
|
|
55
55
|
if (!cfg.public_base_url) {
|
|
56
|
-
console.log(`${ICON_WARN}
|
|
57
|
-
console.log(` Start cloudflared (or your tunnel) and set
|
|
56
|
+
console.log(`${ICON_WARN} public_base_url not set — Twilio cannot reach this server.`);
|
|
57
|
+
console.log(` Start cloudflared (or your tunnel) and set channels.phone.public_base_url in config.yaml.`);
|
|
58
58
|
await channel!.stop();
|
|
59
59
|
process.exit(1);
|
|
60
60
|
}
|
|
61
61
|
if (!cfg.openai_api_key) {
|
|
62
|
-
console.log(`${ICON_WARN}
|
|
62
|
+
console.log(`${ICON_WARN} openai_api_key not set — realtime voice loop will fall back to TwiML <Say>.`);
|
|
63
63
|
}
|
|
64
64
|
|
|
65
65
|
console.log(` dialing ${number} ...`);
|
|
@@ -119,9 +119,13 @@ function helpText(): string {
|
|
|
119
119
|
" phone server, dials, waits, prints transcript.",
|
|
120
120
|
" status Show phone channel configuration.",
|
|
121
121
|
"",
|
|
122
|
-
"
|
|
123
|
-
"
|
|
124
|
-
"
|
|
125
|
-
"
|
|
122
|
+
"Config lives in ~/.niahere/config.yaml under channels.phone:",
|
|
123
|
+
" twilio_sid, twilio_secret, from_number (required)",
|
|
124
|
+
" openai_api_key, public_base_url (required for realtime voice loop)",
|
|
125
|
+
" twilio_auth_token (required if twilio_sid is an API Key SID)",
|
|
126
|
+
" port, voice, realtime_model, allowlist (optional)",
|
|
127
|
+
"",
|
|
128
|
+
"Each field can be overridden by the matching env var (TWILIO_SID, OPENAI_API_KEY, etc.)",
|
|
129
|
+
"if you prefer .env. See the nia-phone skill for full deploy walkthrough.",
|
|
126
130
|
].join("\n");
|
|
127
131
|
}
|
package/src/commands/init.ts
CHANGED
|
@@ -215,6 +215,60 @@ export async function runInit(): Promise<void> {
|
|
|
215
215
|
}
|
|
216
216
|
}
|
|
217
217
|
|
|
218
|
+
// Phone (Twilio Voice + OpenAI Realtime)
|
|
219
|
+
const exPh = (exCh.phone || {}) as Record<string, unknown>;
|
|
220
|
+
let phoneTwilioSid = (exPh.twilio_sid as string) || "";
|
|
221
|
+
let phoneTwilioSecret = (exPh.twilio_secret as string) || "";
|
|
222
|
+
let phoneTwilioAuthToken = (exPh.twilio_auth_token as string) || "";
|
|
223
|
+
let phoneFromNumber = (exPh.from_number as string) || "";
|
|
224
|
+
let phoneOwnerNumber = (exPh.owner_number as string) || "";
|
|
225
|
+
let phonePublicBaseUrl = (exPh.public_base_url as string) || "";
|
|
226
|
+
let phoneOpenAiKey = (exPh.openai_api_key as string) || "";
|
|
227
|
+
let phoneVoice = (exPh.voice as string) || "";
|
|
228
|
+
|
|
229
|
+
const existingPhoneSid = phoneTwilioSid;
|
|
230
|
+
if (existingPhoneSid) {
|
|
231
|
+
const masked = `...${existingPhoneSid.slice(-6)}`;
|
|
232
|
+
const reconfigure = await ask(rl, `\nPhone (Twilio + Realtime): configured (${masked}). Reconfigure? (y/n)`, "n");
|
|
233
|
+
if (reconfigure.toLowerCase() === "y") {
|
|
234
|
+
phoneTwilioSid = (await ask(rl, "Twilio SID (AC… or SK…)", phoneTwilioSid)) || phoneTwilioSid;
|
|
235
|
+
phoneTwilioSecret =
|
|
236
|
+
(await ask(rl, "Twilio Secret (Auth Token if AC, API Key Secret if SK)", phoneTwilioSecret)) ||
|
|
237
|
+
phoneTwilioSecret;
|
|
238
|
+
if (phoneTwilioSid.startsWith("SK")) {
|
|
239
|
+
phoneTwilioAuthToken =
|
|
240
|
+
(await ask(rl, "Twilio Auth Token (account-level — needed for webhook signing)", phoneTwilioAuthToken)) ||
|
|
241
|
+
phoneTwilioAuthToken;
|
|
242
|
+
}
|
|
243
|
+
phoneFromNumber =
|
|
244
|
+
(await ask(rl, "Twilio number to dial from (E.164, e.g. +13025551234)", phoneFromNumber)) || phoneFromNumber;
|
|
245
|
+
phoneOwnerNumber = (await ask(rl, "Your phone (E.164)", phoneOwnerNumber)) || phoneOwnerNumber;
|
|
246
|
+
phonePublicBaseUrl =
|
|
247
|
+
(await ask(rl, "Public base URL (cloudflared/ngrok https://…)", phonePublicBaseUrl)) || phonePublicBaseUrl;
|
|
248
|
+
phoneOpenAiKey = (await ask(rl, "OpenAI API key (for Realtime voice loop)", phoneOpenAiKey)) || phoneOpenAiKey;
|
|
249
|
+
phoneVoice =
|
|
250
|
+
(await ask(rl, "Realtime voice (marin, cedar, shimmer, coral, alloy…)", phoneVoice || "marin")) || phoneVoice;
|
|
251
|
+
}
|
|
252
|
+
} else {
|
|
253
|
+
const setupPhone = await ask(rl, "\nSet up phone (Twilio + OpenAI Realtime voice calls)? (y/n)", "n");
|
|
254
|
+
if (setupPhone.toLowerCase() === "y") {
|
|
255
|
+
console.log(" You'll need: a Twilio voice number, your phone number, an OpenAI API key, and a public tunnel.");
|
|
256
|
+
console.log(" See /nia-phone skill for the full deploy walkthrough.\n");
|
|
257
|
+
phoneTwilioSid = await ask(rl, "Twilio SID (AC… or SK…)", "");
|
|
258
|
+
if (phoneTwilioSid) {
|
|
259
|
+
phoneTwilioSecret = await ask(rl, "Twilio Secret (Auth Token if AC, API Key Secret if SK)", "");
|
|
260
|
+
if (phoneTwilioSid.startsWith("SK")) {
|
|
261
|
+
phoneTwilioAuthToken = await ask(rl, "Twilio Auth Token (account-level — for webhook signing)", "");
|
|
262
|
+
}
|
|
263
|
+
phoneFromNumber = await ask(rl, "Twilio number to dial from (E.164, e.g. +13025551234)", "");
|
|
264
|
+
phoneOwnerNumber = await ask(rl, "Your phone (E.164)", "");
|
|
265
|
+
phonePublicBaseUrl = await ask(rl, "Public base URL (cloudflared/ngrok https://…)", "");
|
|
266
|
+
phoneOpenAiKey = await ask(rl, "OpenAI API key", "");
|
|
267
|
+
phoneVoice = await ask(rl, "Realtime voice", "marin");
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
218
272
|
// Gemini API key (for image generation)
|
|
219
273
|
let geminiApiKey = "";
|
|
220
274
|
const existingGemini = (existing.gemini_api_key as string) || "";
|
|
@@ -430,6 +484,19 @@ export async function runInit(): Promise<void> {
|
|
|
430
484
|
if (slackBotToken && !telegramToken) {
|
|
431
485
|
channels.default = "slack";
|
|
432
486
|
}
|
|
487
|
+
if (phoneTwilioSid && phoneTwilioSecret && phoneFromNumber) {
|
|
488
|
+
const ph: Record<string, unknown> = {
|
|
489
|
+
twilio_sid: phoneTwilioSid,
|
|
490
|
+
twilio_secret: phoneTwilioSecret,
|
|
491
|
+
from_number: phoneFromNumber,
|
|
492
|
+
};
|
|
493
|
+
if (phoneTwilioAuthToken) ph.twilio_auth_token = phoneTwilioAuthToken;
|
|
494
|
+
if (phoneOwnerNumber) ph.owner_number = phoneOwnerNumber;
|
|
495
|
+
if (phonePublicBaseUrl) ph.public_base_url = phonePublicBaseUrl.replace(/\/$/, "");
|
|
496
|
+
if (phoneOpenAiKey) ph.openai_api_key = phoneOpenAiKey;
|
|
497
|
+
if (phoneVoice && phoneVoice !== "marin") ph.voice = phoneVoice;
|
|
498
|
+
channels.phone = ph;
|
|
499
|
+
}
|
|
433
500
|
if (Object.keys(channels).length > 0) {
|
|
434
501
|
config.channels = channels;
|
|
435
502
|
}
|
package/src/mcp/tools.ts
CHANGED
|
@@ -480,7 +480,7 @@ export async function placeCall(args: {
|
|
|
480
480
|
const { getPhoneChannel } = await import("../channels/phone");
|
|
481
481
|
const phone = getPhoneChannel();
|
|
482
482
|
if (!phone) {
|
|
483
|
-
return "Phone channel is not configured.
|
|
483
|
+
return "Phone channel is not configured. Add a channels.phone block to ~/.niahere/config.yaml with twilio_sid, twilio_secret, from_number, public_base_url, openai_api_key (or set the matching env vars in .env), then restart the daemon.";
|
|
484
484
|
}
|
|
485
485
|
try {
|
|
486
486
|
const result = await phone.placeCall({
|