niahere 0.2.10 → 0.2.12
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/package.json +1 -1
- package/skills/modal-cli/SKILL.md +121 -0
- package/skills/slack/SKILL.md +12 -1
- package/src/cli/channels.ts +22 -0
- package/src/cli/index.ts +1 -1
- package/src/cli/status.ts +10 -0
- package/src/core/daemon.ts +11 -0
- package/src/utils/update.ts +68 -0
package/package.json
CHANGED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: modal-cli
|
|
3
|
+
description: >
|
|
4
|
+
Modal CLI for serverless GPU/cloud compute. Use when user mentions modal, deploying to modal,
|
|
5
|
+
running GPU workloads, or serverless Python functions.
|
|
6
|
+
Check `which modal` first — if missing, offer: `pip install modal && modal setup`.
|
|
7
|
+
Covers run/serve/deploy, apps, containers, secrets, volumes, and workspace management.
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## Prerequisites
|
|
11
|
+
|
|
12
|
+
Before any modal command, verify the CLI is available:
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
which modal
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
**If not found**, ask the user:
|
|
19
|
+
|
|
20
|
+
> Modal CLI not found. Install it?
|
|
21
|
+
>
|
|
22
|
+
> ```bash
|
|
23
|
+
> pip install modal
|
|
24
|
+
> ```
|
|
25
|
+
|
|
26
|
+
After install, run auth setup:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
modal setup
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
This opens a browser for token auth. If `modal` still isn't on PATH after pip install, use:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
python -m modal setup
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
**If found but not authed**, check with:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
modal token info
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
If that errors, run `modal setup` to authenticate.
|
|
45
|
+
|
|
46
|
+
## Core Commands
|
|
47
|
+
|
|
48
|
+
| Command | Purpose |
|
|
49
|
+
|---------|---------|
|
|
50
|
+
| `modal run <file>` | One-off execution of a script/function |
|
|
51
|
+
| `modal serve <file>` | Local dev with hot-reload |
|
|
52
|
+
| `modal deploy <file>` | Production deployment |
|
|
53
|
+
| `modal app list` | List deployed apps |
|
|
54
|
+
| `modal app logs <name>` | Stream logs for a deployed app |
|
|
55
|
+
| `modal app stop <name>` | Stop a running app |
|
|
56
|
+
| `modal shell` | Interactive shell in Modal environment |
|
|
57
|
+
|
|
58
|
+
## Workflows
|
|
59
|
+
|
|
60
|
+
### First-time setup
|
|
61
|
+
```bash
|
|
62
|
+
pip install modal
|
|
63
|
+
modal setup
|
|
64
|
+
modal run hello.py # validate end-to-end
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### Develop and deploy
|
|
68
|
+
```bash
|
|
69
|
+
modal run my_app.py # test locally
|
|
70
|
+
modal serve my_app.py # hot-reload dev
|
|
71
|
+
modal deploy my_app.py # ship it
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### Debug containers
|
|
75
|
+
```bash
|
|
76
|
+
modal container list
|
|
77
|
+
modal container logs <id>
|
|
78
|
+
modal container exec <id> -- bash -lc "nvidia-smi"
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### Manage resources
|
|
82
|
+
```bash
|
|
83
|
+
modal secret create my-secret KEY=value
|
|
84
|
+
modal volume create my-vol
|
|
85
|
+
modal environment create dev
|
|
86
|
+
modal config set-environment dev
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## Auth & Profiles
|
|
90
|
+
|
|
91
|
+
- `modal token info` — check current auth
|
|
92
|
+
- `modal token new` — create token via browser
|
|
93
|
+
- `modal token set` — set token credentials directly
|
|
94
|
+
- `modal profile list` / `activate` — switch workspaces
|
|
95
|
+
- `modal config show` — print effective config
|
|
96
|
+
|
|
97
|
+
Environment variables: `MODAL_TOKEN_ID`, `MODAL_TOKEN_SECRET`, `MODAL_ENVIRONMENT`, `MODAL_PROFILE`
|
|
98
|
+
|
|
99
|
+
## Resource Management
|
|
100
|
+
|
|
101
|
+
- **Secrets**: `modal secret list/create/delete` (supports `.env` import)
|
|
102
|
+
- **Volumes**: `modal volume create/list/ls/put/get/rm/delete`
|
|
103
|
+
- **Dicts**: `modal dict create/list/items/get/clear/delete`
|
|
104
|
+
- **Queues**: `modal queue create/list/peek/len/clear/delete`
|
|
105
|
+
- **NFS**: `modal nfs list/create/ls/put/get/rm/delete`
|
|
106
|
+
- **Environments**: `modal environment list/create/delete/update`
|
|
107
|
+
|
|
108
|
+
## Decision Points
|
|
109
|
+
|
|
110
|
+
- User says "deploy to modal" / "run on GPU" → `modal deploy` or `modal run`
|
|
111
|
+
- User says "check my modal apps" → `modal app list`
|
|
112
|
+
- User says "modal logs" → `modal app logs <name>`
|
|
113
|
+
- User needs interactive debug → `modal shell` or `modal container exec`
|
|
114
|
+
- modal not installed → offer install, don't silently fail
|
|
115
|
+
- modal not authed → run `modal setup`
|
|
116
|
+
|
|
117
|
+
## References
|
|
118
|
+
|
|
119
|
+
- Guide: https://modal.com/docs/guide
|
|
120
|
+
- CLI reference: https://modal.com/docs/reference/cli/
|
|
121
|
+
- Status: https://status.modal.com
|
package/skills/slack/SKILL.md
CHANGED
|
@@ -43,7 +43,18 @@ URL format: `https://<workspace>.slack.com/archives/<CHANNEL_ID>/p<THREAD_TS_NO_
|
|
|
43
43
|
- **Channel ID:** the segment after `/archives/`
|
|
44
44
|
- **Thread TS:** the `p` number with a `.` inserted before the last 6 digits
|
|
45
45
|
|
|
46
|
+
## Beyond these primitives
|
|
47
|
+
|
|
48
|
+
These commands cover common operations but the Slack API has 200+ methods. If you need something not listed here (e.g. managing bookmarks, setting channel topics, pinning messages, scheduling messages, managing user groups):
|
|
49
|
+
|
|
50
|
+
1. **Check `slack.py --help`** for available subcommands first.
|
|
51
|
+
2. **Use the Slack API directly** via `curl` with the bot token from `slack_helper.load_slack_config()["token"]`. The pattern is always: `curl -H "Authorization: Bearer $TOKEN" https://slack.com/api/<method>`.
|
|
52
|
+
3. **Consult the [Slack API docs](https://api.slack.com/methods)** to find the right method and required scopes.
|
|
53
|
+
4. **Compose primitives** to build workflows — "summarize channel" is just `history` + LLM reasoning. "Find discussions about X" is `search` + `thread`.
|
|
54
|
+
|
|
55
|
+
The bot token and auth pattern are consistent across all Slack APIs. Don't limit yourself to what's explicitly in `slack.py` — treat it as a starting point, not a boundary.
|
|
56
|
+
|
|
46
57
|
## Design
|
|
47
58
|
|
|
48
59
|
Each subcommand = one Slack API call. No bundled workflows.
|
|
49
|
-
Features
|
|
60
|
+
Features are outcomes achieved by an agent composing these primitives in a loop.
|
package/src/cli/channels.ts
CHANGED
|
@@ -91,6 +91,28 @@ export async function slackCommand(): Promise<void> {
|
|
|
91
91
|
console.log(` Workspace: ${config.channels.slack.workspace} (${config.channels.slack.workspace_url})`);
|
|
92
92
|
console.log(` Bot: @${config.channels.slack.bot_name} (${config.channels.slack.bot_user_id})`);
|
|
93
93
|
}
|
|
94
|
+
// Verify auth is working
|
|
95
|
+
try {
|
|
96
|
+
const resp = await fetch("https://slack.com/api/auth.test", {
|
|
97
|
+
headers: { Authorization: `Bearer ${config.channels.slack.bot_token}` },
|
|
98
|
+
});
|
|
99
|
+
const data = (await resp.json()) as Record<string, unknown>;
|
|
100
|
+
if (data.ok) {
|
|
101
|
+
console.log(` Auth: \u2713 valid`);
|
|
102
|
+
// Backfill workspace info if missing
|
|
103
|
+
if (!config.channels.slack.workspace) {
|
|
104
|
+
const enriched = await enrichSlackConfig(config.channels.slack.bot_token);
|
|
105
|
+
if (Object.keys(enriched).length > 0) {
|
|
106
|
+
updateRawConfig({ channels: { slack: enriched } });
|
|
107
|
+
console.log(" (workspace info backfilled)");
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
} else {
|
|
111
|
+
console.log(` Auth: \u2717 ${data.error}`);
|
|
112
|
+
}
|
|
113
|
+
} catch (err) {
|
|
114
|
+
console.log(` Auth: \u2717 could not reach Slack API`);
|
|
115
|
+
}
|
|
94
116
|
} else {
|
|
95
117
|
console.log("Slack: not configured");
|
|
96
118
|
}
|
package/src/cli/index.ts
CHANGED
package/src/cli/status.ts
CHANGED
|
@@ -7,6 +7,7 @@ import { Message, ActiveEngine, Job } from "../db/models";
|
|
|
7
7
|
import type { ScheduleType, JobStateStatus, RoomStats } from "../types";
|
|
8
8
|
import { withDb } from "../db/connection";
|
|
9
9
|
import { errMsg } from "../utils/errors";
|
|
10
|
+
import { checkForUpdate } from "../utils/update";
|
|
10
11
|
|
|
11
12
|
type StatusOptions = {
|
|
12
13
|
json: boolean;
|
|
@@ -306,4 +307,13 @@ export async function statusCommand(argv: string[] = []): Promise<void> {
|
|
|
306
307
|
} else {
|
|
307
308
|
console.log("Tip: use --rooms N, --all, or --json for alternate views.");
|
|
308
309
|
}
|
|
310
|
+
|
|
311
|
+
// Check for updates (non-blocking, cached 24h)
|
|
312
|
+
try {
|
|
313
|
+
const { version } = await import("../../package.json");
|
|
314
|
+
const update = await checkForUpdate(version);
|
|
315
|
+
if (update) {
|
|
316
|
+
console.log(`\n⚠ Update available: ${update.current} → ${update.latest} (run \`npm i -g niahere\` to update)`);
|
|
317
|
+
}
|
|
318
|
+
} catch {}
|
|
309
319
|
}
|
package/src/core/daemon.ts
CHANGED
|
@@ -162,6 +162,17 @@ export async function runDaemon(): Promise<void> {
|
|
|
162
162
|
writePid(process.pid);
|
|
163
163
|
log.info({ pid: process.pid }, "daemon started");
|
|
164
164
|
|
|
165
|
+
// Check for updates (non-blocking, logged only)
|
|
166
|
+
try {
|
|
167
|
+
const { checkForUpdate } = await import("../utils/update");
|
|
168
|
+
const { version } = await import("../../package.json");
|
|
169
|
+
const update = await checkForUpdate(version);
|
|
170
|
+
if (update) {
|
|
171
|
+
log.warn({ current: update.current, latest: update.latest }, "update available — run `npm i -g niahere` to update");
|
|
172
|
+
}
|
|
173
|
+
} catch {}
|
|
174
|
+
|
|
175
|
+
|
|
165
176
|
// Startup recovery
|
|
166
177
|
try {
|
|
167
178
|
await runMigrations();
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync } from "fs";
|
|
2
|
+
import { resolve, dirname } from "path";
|
|
3
|
+
import { getNiaHome } from "./paths";
|
|
4
|
+
|
|
5
|
+
const CACHE_TTL_MS = 24 * 60 * 60 * 1000; // 24 hours
|
|
6
|
+
const PACKAGE_NAME = "niahere";
|
|
7
|
+
|
|
8
|
+
type UpdateCache = {
|
|
9
|
+
latest: string;
|
|
10
|
+
checkedAt: number;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
function cachePath(): string {
|
|
14
|
+
return resolve(getNiaHome(), "tmp/update-check.json");
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function readCache(): UpdateCache | null {
|
|
18
|
+
const path = cachePath();
|
|
19
|
+
if (!existsSync(path)) return null;
|
|
20
|
+
try {
|
|
21
|
+
const data = JSON.parse(readFileSync(path, "utf8"));
|
|
22
|
+
if (data.latest && data.checkedAt && Date.now() - data.checkedAt < CACHE_TTL_MS) {
|
|
23
|
+
return data;
|
|
24
|
+
}
|
|
25
|
+
} catch {}
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function writeCache(latest: string): void {
|
|
30
|
+
const path = cachePath();
|
|
31
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
32
|
+
writeFileSync(path, JSON.stringify({ latest, checkedAt: Date.now() }));
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async function fetchLatestVersion(): Promise<string | null> {
|
|
36
|
+
try {
|
|
37
|
+
const resp = await fetch(`https://registry.npmjs.org/${PACKAGE_NAME}/latest`, {
|
|
38
|
+
signal: AbortSignal.timeout(3000),
|
|
39
|
+
});
|
|
40
|
+
if (!resp.ok) return null;
|
|
41
|
+
const data = await resp.json() as { version?: string };
|
|
42
|
+
return data.version ?? null;
|
|
43
|
+
} catch {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function isNewer(latest: string, current: string): boolean {
|
|
49
|
+
const [la, lb, lc] = latest.split(".").map(Number);
|
|
50
|
+
const [ca, cb, cc] = current.split(".").map(Number);
|
|
51
|
+
if (la !== ca) return la > ca;
|
|
52
|
+
if (lb !== cb) return lb > cb;
|
|
53
|
+
return lc > cc;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Check if a newer version is available. Returns update info or null. Non-blocking, cached 24h. */
|
|
57
|
+
export async function checkForUpdate(currentVersion: string): Promise<{ current: string; latest: string } | null> {
|
|
58
|
+
const cached = readCache();
|
|
59
|
+
if (cached) {
|
|
60
|
+
return isNewer(cached.latest, currentVersion) ? { current: currentVersion, latest: cached.latest } : null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const latest = await fetchLatestVersion();
|
|
64
|
+
if (!latest) return null;
|
|
65
|
+
|
|
66
|
+
writeCache(latest);
|
|
67
|
+
return isNewer(latest, currentVersion) ? { current: currentVersion, latest } : null;
|
|
68
|
+
}
|