outsrc 0.2.0 → 0.2.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/README.md +103 -1
- package/dist/cli.js +128 -7
- package/dist/mailbox.d.ts +106 -1
- package/dist/mailbox.js +98 -33
- package/dist/model-cache.d.ts +29 -0
- package/dist/model-cache.js +62 -0
- package/dist/models.d.ts +10 -0
- package/dist/models.js +108 -0
- package/dist/server.js +7 -2
- package/dist/state.d.ts +24 -0
- package/dist/state.js +10 -0
- package/dist/streams.d.ts +49 -0
- package/dist/streams.js +388 -0
- package/dist/types.d.ts +12 -2
- package/dist/update-check.d.ts +18 -0
- package/dist/update-check.js +84 -0
- package/dist/updates.d.ts +78 -0
- package/dist/updates.js +136 -0
- package/dist/usage.d.ts +105 -0
- package/dist/usage.js +106 -0
- package/dist/version.d.ts +13 -0
- package/dist/version.js +45 -0
- package/dist/wrapper.js +53 -9
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -51,7 +51,7 @@ Install it globally rather than running it through `npx`: agents launch the inst
|
|
|
51
51
|
|
|
52
52
|
For agents and scripts, `outsrc init --json --repo <path> [--repo <path>] [--local claude,codex,grok] [--max-jobs <n|unlimited>] [--max-run-minutes <n|unlimited>]` prints one JSON event per line and exits with code 3 when a person has to act, such as logging in to a CLI. Rerun it after that step. Without `--local`, nothing is registered. A `settings` event lists the limits, repositories and targets so the agent can review them with the owner.
|
|
53
53
|
|
|
54
|
-
`outsrc doctor` rechecks the configuration. `OUTSRC_HOME` moves the state directory away from `~/.outsrc`, which outsrc keeps readable by you only.
|
|
54
|
+
`outsrc doctor` rechecks the configuration and asks the npm registry for the latest `outsrc`. Its JSON adds `update_available` (true only when the installed version is behind npm) and `version`: `{status, installed, latest}`, where `status` is `current`, `outdated`, `ahead` or `unknown`. Without `--json` it also prints one line to stderr, such as `outsrc 0.2.0 is installed; 0.3.0 is available. Upgrade with: npm install -g outsrc@latest`. Doctor never upgrades. If the registry cannot be reached within 3 seconds, `status` is `unknown` with an `error`, `update_available` is false and the exit code is unaffected. `OUTSRC_HOME` moves the state directory away from `~/.outsrc`, which outsrc keeps readable by you only.
|
|
55
55
|
|
|
56
56
|
## Settings
|
|
57
57
|
|
|
@@ -101,6 +101,7 @@ The MCP tools and the CLI commands return the same JSON.
|
|
|
101
101
|
| `threads` | This caller's threads and their status |
|
|
102
102
|
| `settings` | Every setting with its value, default and meaning (read-only) |
|
|
103
103
|
| `history` | Every run in a thread, with its message and result |
|
|
104
|
+
| `usage` | Local usage for today and the last 7 days, including totals by target |
|
|
104
105
|
| `log` | A slice of a run's log |
|
|
105
106
|
| `diff` | Changes since the thread started, committed or not (256 KiB by default) |
|
|
106
107
|
| `stop` | Cancel a running thread |
|
|
@@ -108,6 +109,47 @@ The MCP tools and the CLI commands return the same JSON.
|
|
|
108
109
|
|
|
109
110
|
CLI only: `outsrc config set` and `unset` change settings, `outsrc prune` deletes finished worktrees past their retention period, `outsrc migrate` imports threads from earlier versions, and `outsrc plugins` checks the plugin engines.
|
|
110
111
|
|
|
112
|
+
### Local usage and spend
|
|
113
|
+
|
|
114
|
+
Run `outsrc usage --caller grok --json` or call the MCP `usage` tool. Omit `--caller` on the CLI to include all callers. The command reads saved runs on this computer. It does not contact providers, scrape dashboards, or send usage to a cloud service.
|
|
115
|
+
|
|
116
|
+
Every successful `inbox` response and every run in `history` includes these fields, including working, waiting, failed, and cancelled runs:
|
|
117
|
+
|
|
118
|
+
```json
|
|
119
|
+
{
|
|
120
|
+
"target": "grok-plugin",
|
|
121
|
+
"effort": null,
|
|
122
|
+
"model": "grok-4.7-build",
|
|
123
|
+
"usage": {
|
|
124
|
+
"tokens_in": 30655,
|
|
125
|
+
"tokens_out": 369,
|
|
126
|
+
"estimated_cost_usd": 0.021794,
|
|
127
|
+
"wall_minutes": 2
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
The token and cost values above come from the sanitized Grok review fixture; wall minutes illustrate the shape. `tokens_in` and `tokens_out` are the vendor's reported input/output counters. Separate cache and reasoning counters are not added. `estimated_cost_usd` is the vendor-reported USD cost estimate; outsrc does not apply a price table or infer a subscription charge. `model` is the vendor-reported model, or the sole model named in its `modelUsage` metadata, falling back to the explicit model passed in the invocation. It is null when neither is known. `effort` is the explicit setting passed in that run's invocation, not a guessed vendor default. Paths that ignore model or effort, including Codex plugin branch reviews, report null for those fields.
|
|
133
|
+
|
|
134
|
+
Claude and Grok native JSON result envelopes expose counters and cost when provided. Codex native JSONL `turn.completed.usage` events expose counters; cost stays null. Grok plugin review/critique envelopes expose usage through `result` or `grok.stdout`, counted once even when both contain it. The pinned Grok plugin 0.2.0 task path invokes the CLI with plain output, so task tokens and cost are null. Codex plugin 1.0.5 discards usage notifications, so its task and review tokens and cost are null. Their saved job results repeat the emitted payload and contain no additional usage to recover. Custom adapters have no supported usage contract, so their tokens and cost are null. Model-generated answer text is never interpreted as usage metadata. The engine pins remain unchanged.
|
|
135
|
+
|
|
136
|
+
`wall_minutes` measures elapsed wrapper time, including setup, agent execution, plugin teardown, and result collection. It excludes time before the wrapper starts. Working runs, old records, and interrupted runs without a recorded duration have null wall minutes. Old records require no migration. Missing or invalid vendor metadata produces explicit nulls, never zero or invented numbers. A reported zero remains zero. Each run saves reported usage in `usage.json` as stdout arrives, then includes it in `result.json`. Deadline failures retain stdout usage. Cancellation and wrapper interruption recover the saved usage without reading mixed stdout/stderr logs. A later commit or result collection failure also retains it.
|
|
137
|
+
|
|
138
|
+
The usage report has `as_of`, `today`, and `last_7_days`. Today starts at midnight in the local process time zone; the 7-day window is the preceding 168 hours. Runs are assigned by their submission time (`created_at`), including continuations, working runs, and retained discarded threads. Each window includes `since`, `runs`, the four numeric metrics, and a sorted `by_target` array with the same counts and metrics. Each metric is `{ "total": number | null, "missing_runs": number }`. A total is null if any included run lacks that metric, so known partial spend cannot appear as a complete total. Empty windows have zero runs and zero totals. Each saved run is counted once.
|
|
139
|
+
|
|
140
|
+
`npm run verify` reruns the recorded-envelope finished-run checks through source and built CLI/MCP surfaces. `npm run smoke:native` requires non-null input/output counters on each succeeded continuation. `npm run smoke:plugins` requires Grok review counters and explicit nulls on pinned plugin paths that cannot report usage. The live smoke commands use provider accounts.
|
|
141
|
+
|
|
142
|
+
## Watching jobs live
|
|
143
|
+
|
|
144
|
+
`outsrc streams` (or `outsrc watch`) opens a grid of live agent logs in your browser, served from `127.0.0.1` on the machine that runs the jobs.
|
|
145
|
+
|
|
146
|
+
- One pane per working thread. The header shows the short thread id, target, status, branch and run age. The body follows the tail of the latest run's `run.log`. Scroll up to pause; click "paused" to follow again.
|
|
147
|
+
- New threads appear without a restart. With no working threads the page says so and keeps watching.
|
|
148
|
+
- Finished threads stop growing and move to a muted strip for 15 minutes (`--recent-minutes`). Click one to show its log.
|
|
149
|
+
- `--thread <id>` shows one thread only, finished or not. `--port <n>` fixes the port. `--no-open` prints the URL without opening a browser.
|
|
150
|
+
|
|
151
|
+
The viewer reads `$OUTSRC_HOME/threads` (default `~/.outsrc`) about once a second, and only while a page is open. It never calls a vendor CLI or a model, writes nothing, and does not change what `send` or `inbox` report. The URL carries a random token. Stopping the viewer (Ctrl-C) leaves jobs running. It keeps no state of its own, so it outlives bot and outsrc upgrades and shows nothing once `~/.outsrc` is removed.
|
|
152
|
+
|
|
111
153
|
## Repository options
|
|
112
154
|
|
|
113
155
|
```toml
|
|
@@ -138,6 +180,24 @@ A custom target prints one JSON object with `kind` (`completed`, `needs_input` o
|
|
|
138
180
|
|
|
139
181
|
Each finding has `priority` (`P0` to `P3`), `title`, `body`, `path` (or null) and `line` (or null).
|
|
140
182
|
|
|
183
|
+
### Model cache
|
|
184
|
+
|
|
185
|
+
A target with `models` in its config uses those. For every other target outsrc asks the vendor CLI for its models (a model-list command, not a model run, so it spends no tokens) and keeps the answer in `~/.outsrc/models.json` (`$OUTSRC_HOME/models.json`). `list_targets` reads that file, so a warm cache starts no vendor CLI. Each target shows `models_refreshed_at`, the time its entry was written, or null when its models come from config.
|
|
186
|
+
|
|
187
|
+
- Miss: a target with no entry, or whose `adapter` or `command` changed since the entry was written, is discovered once and written back. On a first install the first `list_targets` (or `init`) fills the cache.
|
|
188
|
+
- Stale: entries do not expire. outsrc serves an old entry until something refreshes it, so schedule a refresh.
|
|
189
|
+
- Failed listing: a missing or logged-out CLI is cached as `models: null` until the next refresh.
|
|
190
|
+
|
|
191
|
+
Refresh with `outsrc models refresh` (all targets) or `outsrc models refresh --target <name>`. `outsrc models` prints the cache. `outsrc list_targets --refresh`, or `list_targets` with `{"refresh": true}` over MCP, rediscovers before answering. To refresh twice a day with cron:
|
|
192
|
+
|
|
193
|
+
```
|
|
194
|
+
0 7,19 * * * /usr/bin/env outsrc models refresh >/dev/null 2>&1
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
On macOS a launchd agent with `StartCalendarInterval` at the same hours works the same way. Both need `PATH` to include the vendor CLIs.
|
|
198
|
+
|
|
199
|
+
`send` refuses a `model` that is not in the target's configured or cached list, and the error lists the allowed models. When the cache has no list for the target (`models: null`) outsrc cannot check, and passes the model to the vendor CLI unchanged.
|
|
200
|
+
|
|
141
201
|
## Vendor plugin engines
|
|
142
202
|
|
|
143
203
|
The `codex-plugin` and `grok-plugin` adapters drive the vendors' own Claude Code plugins, [openai/codex-plugin-cc](https://github.com/openai/codex-plugin-cc) and [xai-org/grok-build-plugin-cc](https://github.com/xai-org/grok-build-plugin-cc), both Apache-2.0. The plugin launches the CLI and supplies the review prompts, output schema and, for Codex, the built-in reviewer. outsrc keeps the mailbox, worktrees, repository list and filtered environment. It runs the engine scripts directly; no Claude Code session is involved.
|
|
@@ -167,6 +227,48 @@ The plugin scripts are the vendors' internal CLIs, not a documented API. outsrc
|
|
|
167
227
|
|
|
168
228
|
To move a pin, run `outsrc engines pin <codex-plugin|grok-plugin> <commit|branch|tag>`. It updates `engines.lock.json` only if the contract check passes. Then run `npm run smoke:plugins` and keep the change only if that passes too.
|
|
169
229
|
|
|
230
|
+
## Update checks
|
|
231
|
+
|
|
232
|
+
`outsrc updates refresh` checks whether Claude Code, Codex, Grok Build and outsrc itself are behind their latest release. It runs `claude --version`, `codex --version`, `grok --version`, `grok update --check --json` and reads the npm registry. It calls no model and costs no tokens. It never installs anything: you upgrade by hand, or ask the bot to after you say yes.
|
|
233
|
+
|
|
234
|
+
`outsrc updates` prints the last result without checking again, and `outsrc doctor` includes the same summary under `updates`.
|
|
235
|
+
|
|
236
|
+
State lives in two files under `~/.outsrc` (or `OUTSRC_HOME`), both readable by you only:
|
|
237
|
+
|
|
238
|
+
- `updates.json`: when the last check ran, each product's installed and latest version (or the error), and `pending`, the products that are behind. Only `refresh` writes it. A pending entry keeps its `first_seen_at` until a newer release appears. A failed check keeps the previous entry.
|
|
239
|
+
- `updates-ack.json`: the notifications already shown to you. Only `outsrc updates ack` writes it.
|
|
240
|
+
|
|
241
|
+
Each pending update is a notification with the id `<product>@<latest>`, for example `claude@2.1.286`. `refresh` lists the ones it found for the first time under `new`, and `unread` lists every pending notification not yet acknowledged. A rerun that finds nothing newer announces nothing, and an acknowledged version stays quiet until a newer one ships.
|
|
242
|
+
|
|
243
|
+
Run it a few times a day. cron and launchd start jobs with a short `PATH` that usually lacks `node` and the CLIs, and a CLI that cannot be found counts as not installed. Give the job your shell's `PATH` (the output of `echo $PATH`). With cron:
|
|
244
|
+
|
|
245
|
+
```
|
|
246
|
+
PATH=<your PATH>
|
|
247
|
+
17 */6 * * * outsrc updates refresh >/dev/null 2>&1
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
On macOS with launchd, save this as `~/Library/LaunchAgents/ing.outsrc.updates.plist`, fill in `PATH` and the `outsrc` path (the output of `command -v outsrc`), then run `launchctl load` on the file:
|
|
251
|
+
|
|
252
|
+
```xml
|
|
253
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
254
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
255
|
+
<plist version="1.0">
|
|
256
|
+
<dict>
|
|
257
|
+
<key>Label</key><string>ing.outsrc.updates</string>
|
|
258
|
+
<key>ProgramArguments</key>
|
|
259
|
+
<array><string>/usr/local/bin/outsrc</string><string>updates</string><string>refresh</string></array>
|
|
260
|
+
<key>EnvironmentVariables</key>
|
|
261
|
+
<dict><key>PATH</key><string>your PATH</string></dict>
|
|
262
|
+
<key>StartInterval</key><integer>21600</integer>
|
|
263
|
+
<key>RunAtLoad</key><true/>
|
|
264
|
+
</dict>
|
|
265
|
+
</plist>
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
### For bot authors
|
|
269
|
+
|
|
270
|
+
When the bot next talks to its owner, it runs `outsrc updates --json` and, for each `unread` entry, tells the owner the product, installed version and latest version, and asks whether to upgrade. If the bot also reports to another agent, it forwards the same fields (`product`, `installed`, `latest`) there. After the owner has seen them, the bot runs `outsrc updates ack <id>...` (or `--all`) so they are not repeated. The bot upgrades only after the owner says yes.
|
|
271
|
+
|
|
170
272
|
## Limits
|
|
171
273
|
|
|
172
274
|
outsrc checks that thread and run paths stay inside its state directory, and it signals a process only if it is still the job outsrc started. By default at most 4 threads work at once and a run is stopped after 2 hours (both settings). A run log stops growing at 1 MiB, and `log` returns at most 8 KiB.
|
package/dist/cli.js
CHANGED
|
@@ -1,13 +1,18 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
2
3
|
import { defaultConfigPath, defaultHome, loadConfigFile } from "./config.js";
|
|
3
4
|
import { migrateLegacy } from "./migrate.js";
|
|
5
|
+
import { modelCachePath, readModelCache } from "./model-cache.js";
|
|
4
6
|
import { changeSetting, listSettings } from "./settings.js";
|
|
5
7
|
import { defaultInitEnv, parseInitArgs, runInit } from "./init.js";
|
|
6
8
|
import { createMailbox } from "./mailbox.js";
|
|
7
9
|
import { installEngine, managedScript, readLock, resolveCommit, writeLockEntry } from "./engines.js";
|
|
8
10
|
import { checkPluginContract, pluginVersion } from "./plugin-contract.js";
|
|
9
11
|
import { isPluginAdapter, resolvePluginScript } from "./plugins.js";
|
|
10
|
-
import {
|
|
12
|
+
import { startStreamServer } from "./streams.js";
|
|
13
|
+
import { DEFAULT_CALLER, parseCaller, parseThreadId } from "./types.js";
|
|
14
|
+
import { checkForUpdate, describeUpdate, installedVersion, registryFetchLatest, updateAvailable } from "./update-check.js";
|
|
15
|
+
import { ack, ackFile, defaultProbe, readUpdates, refresh, unread, updatesFile } from "./updates.js";
|
|
11
16
|
function pluginReports(explicit) {
|
|
12
17
|
return ["codex-plugin", "grok-plugin"].flatMap((adapter) => {
|
|
13
18
|
const script = explicit[adapter] ?? managedScript(defaultHome(), adapter) ?? resolvePluginScript(adapter);
|
|
@@ -36,6 +41,8 @@ function parseMailboxArgs(argv) {
|
|
|
36
41
|
rest = callerFlag.rest;
|
|
37
42
|
const json = hasFlag(rest, "--json");
|
|
38
43
|
rest = rest.filter((arg) => arg !== "--json");
|
|
44
|
+
const refresh = hasFlag(rest, "--refresh");
|
|
45
|
+
rest = rest.filter((arg) => arg !== "--refresh");
|
|
39
46
|
const names = ["--repo", "--target", "--message", "--thread", "--thread-id", "--request-id", "--model", "--effort", "--kind", "--base", "--ref", "--run-id", "--offset", "--limit"];
|
|
40
47
|
const flags = {};
|
|
41
48
|
for (const name of names) {
|
|
@@ -49,6 +56,7 @@ function parseMailboxArgs(argv) {
|
|
|
49
56
|
return {
|
|
50
57
|
caller: callerFlag.value !== undefined ? parseCaller(callerFlag.value) : undefined,
|
|
51
58
|
json,
|
|
59
|
+
refresh,
|
|
52
60
|
flags,
|
|
53
61
|
positionals: rest,
|
|
54
62
|
};
|
|
@@ -59,30 +67,98 @@ function writeJson(body, pretty = true) {
|
|
|
59
67
|
const HELP = `outsrc init [--repo <path>]... [--yes] [--json] [--plugins|--no-plugins] [--local claude,codex,grok]
|
|
60
68
|
[--max-jobs <n|unlimited>] [--max-run-minutes <n|unlimited>]
|
|
61
69
|
config [list] | config set <key> <value> | config unset <key>
|
|
62
|
-
doctor | list_repos | list_targets | targets | threads | prune | migrate
|
|
70
|
+
doctor [--json] | list_repos | list_targets [--refresh] | targets | threads | prune | migrate
|
|
71
|
+
models [list] | models refresh [--target <name>]
|
|
63
72
|
plugins [--codex <script>] [--grok <script>]
|
|
73
|
+
updates | updates refresh | updates ack <product@version>... | updates ack --all
|
|
64
74
|
engines pin <codex-plugin|grok-plugin> <commit|branch|tag>
|
|
65
75
|
send --repo <alias> --target <name> --message <text> [--caller <id>] [--json]
|
|
66
76
|
[--thread-id <id>] [--request-id <id>] [--model <m>] [--effort <e>]
|
|
67
77
|
[--kind task|review|adversarial_review] [--base <ref>] [--ref <ref>]
|
|
68
78
|
inbox <thread_id> [--caller <id>] [--json]
|
|
69
79
|
history <thread_id> [--caller <id>] [--json]
|
|
80
|
+
usage [--caller <id>] [--json]
|
|
70
81
|
log <thread_id> [--run-id <id>] [--offset <n>] [--limit <n>] [--caller <id>] [--json]
|
|
71
82
|
diff <thread_id> [--limit <n>] [--caller <id>] [--json]
|
|
72
83
|
stop <thread_id> [--caller <id>] [--json]
|
|
73
84
|
discard <thread_id> [--caller <id>] [--json]
|
|
85
|
+
streams [--thread <id>] [--port <n>] [--recent-minutes <n>] [--no-open] (alias: watch)
|
|
74
86
|
|
|
75
87
|
init sets up config and optional local MCP registration for local CLIs.
|
|
76
88
|
Public install is the Outsrc bot from Bot Exchange; this CLI is for bot authors and advanced operators.
|
|
77
89
|
|
|
90
|
+
streams opens a local browser grid of live run.log tails, one pane per working thread. It only reads
|
|
91
|
+
files under the outsrc home; it never calls a vendor CLI or a model. See outsrc streams --help.
|
|
92
|
+
|
|
78
93
|
Mailbox commands print JSON (same shapes as the stdio MCP tools). Pass --caller so each
|
|
79
|
-
bot only sees its own threads; omit it on doctor/list_*/threads to act as the owner.
|
|
94
|
+
bot only sees its own threads; omit it on doctor/list_*/threads/usage to act as the owner.
|
|
80
95
|
`;
|
|
96
|
+
const STREAMS_HELP = `outsrc streams [--thread <id>] [--port <n>] [--recent-minutes <n>] [--no-open]
|
|
97
|
+
outsrc watch (same command)
|
|
98
|
+
|
|
99
|
+
Serves a grid of live run logs on http://127.0.0.1 and opens it in your browser.
|
|
100
|
+
One pane per working thread: short id, target, status, branch and age, then the tail of the
|
|
101
|
+
latest run's run.log. Panes follow new output; scroll up to pause, click "paused" to follow again.
|
|
102
|
+
New threads appear without a restart. Finished threads stop growing and move to a muted strip
|
|
103
|
+
(click one to show its log). Threads finished more than --recent-minutes ago (default 15) are hidden.
|
|
104
|
+
|
|
105
|
+
--thread <id> Show only this thread, finished or not; waits for it if it does not exist yet
|
|
106
|
+
--port <n> Listen on this port (default: a free one)
|
|
107
|
+
--recent-minutes <n> How long finished threads stay in the strip (default 15)
|
|
108
|
+
--no-open Print the URL without opening a browser
|
|
109
|
+
|
|
110
|
+
The viewer reads $OUTSRC_HOME/threads (default ~/.outsrc) about once a second, only while a page is
|
|
111
|
+
open. It never calls a vendor CLI or a model and writes nothing. The URL carries a random token and
|
|
112
|
+
the server listens on 127.0.0.1 only. Ctrl-C stops the viewer; jobs keep running.
|
|
113
|
+
`;
|
|
114
|
+
function openBrowser(url) {
|
|
115
|
+
const opener = process.platform === "darwin" ? "open" : process.platform === "win32" ? "explorer" : "xdg-open";
|
|
116
|
+
try {
|
|
117
|
+
spawn(opener, [url], { detached: true, stdio: "ignore" }).on("error", () => { }).unref();
|
|
118
|
+
}
|
|
119
|
+
catch { /* the URL is printed anyway */ }
|
|
120
|
+
}
|
|
121
|
+
async function runStreams(argv) {
|
|
122
|
+
if (hasFlag(argv, "--help") || hasFlag(argv, "-h")) {
|
|
123
|
+
process.stdout.write(STREAMS_HELP);
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
let rest = argv.filter((arg) => arg !== "--no-open");
|
|
127
|
+
const thread = takeFlag(rest, "--thread");
|
|
128
|
+
rest = thread.rest;
|
|
129
|
+
const port = takeFlag(rest, "--port");
|
|
130
|
+
rest = port.rest;
|
|
131
|
+
const recent = takeFlag(rest, "--recent-minutes");
|
|
132
|
+
rest = recent.rest;
|
|
133
|
+
if (rest.length)
|
|
134
|
+
throw new Error(`unknown argument: ${rest[0]}\n\n${STREAMS_HELP}`);
|
|
135
|
+
const portNumber = port.value === undefined ? 0 : Number(port.value);
|
|
136
|
+
if (!Number.isInteger(portNumber) || portNumber < 0 || portNumber > 65535)
|
|
137
|
+
throw new Error("--port must be 0-65535");
|
|
138
|
+
const recentMinutes = recent.value === undefined ? 15 : Number(recent.value);
|
|
139
|
+
if (!Number.isFinite(recentMinutes) || recentMinutes < 0)
|
|
140
|
+
throw new Error("--recent-minutes must be a number of minutes");
|
|
141
|
+
const server = await startStreamServer({
|
|
142
|
+
home: defaultHome(), port: portNumber, recentMs: recentMinutes * 60_000,
|
|
143
|
+
...(thread.value !== undefined ? { thread: parseThreadId(thread.value) } : {}),
|
|
144
|
+
});
|
|
145
|
+
process.stdout.write(`outsrc streams: ${server.url}\nWatching ${defaultHome()}/threads. Ctrl-C stops the viewer; jobs keep running.\n`);
|
|
146
|
+
if (!hasFlag(argv, "--no-open"))
|
|
147
|
+
openBrowser(server.url);
|
|
148
|
+
await new Promise((resolve) => {
|
|
149
|
+
const stop = () => { void server.close().then(resolve); };
|
|
150
|
+
process.once("SIGINT", stop);
|
|
151
|
+
process.once("SIGTERM", stop);
|
|
152
|
+
});
|
|
153
|
+
}
|
|
81
154
|
const command = process.argv[2] ?? "help";
|
|
82
155
|
try {
|
|
83
156
|
if (command === "help" || command === "--help" || command === "-h") {
|
|
84
157
|
process.stdout.write(HELP);
|
|
85
158
|
}
|
|
159
|
+
else if (command === "streams" || command === "watch") {
|
|
160
|
+
await runStreams(process.argv.slice(3));
|
|
161
|
+
}
|
|
86
162
|
else if (command === "init") {
|
|
87
163
|
const options = parseInitArgs(process.argv.slice(3));
|
|
88
164
|
const env = defaultInitEnv(defaultConfigPath(defaultHome()), options.json);
|
|
@@ -113,6 +189,25 @@ try {
|
|
|
113
189
|
writeJson({ pinned: true, entry, next: "Run npm run smoke:plugins. Commit engines.lock.json if it passes; revert it if not." });
|
|
114
190
|
}
|
|
115
191
|
}
|
|
192
|
+
else if (command === "updates") {
|
|
193
|
+
const [sub, ...rest] = process.argv.slice(3).filter((arg) => arg !== "--json");
|
|
194
|
+
const home = defaultHome();
|
|
195
|
+
const files = { state: updatesFile(home), acks: ackFile(home) };
|
|
196
|
+
if (sub === undefined || sub === "list") {
|
|
197
|
+
const state = readUpdates(home);
|
|
198
|
+
writeJson({ ...state, unread: unread(home, state), files });
|
|
199
|
+
}
|
|
200
|
+
else if (sub === "refresh" && rest.length === 0) {
|
|
201
|
+
const { state, fresh } = await refresh(home, defaultProbe);
|
|
202
|
+
writeJson({ ...state, new: fresh, unread: unread(home, state), files });
|
|
203
|
+
}
|
|
204
|
+
else if (sub === "ack" && rest.length > 0) {
|
|
205
|
+
const all = rest.includes("--all");
|
|
206
|
+
writeJson({ acked: ack(home, all ? "all" : rest.filter((arg) => arg !== "--all")) });
|
|
207
|
+
}
|
|
208
|
+
else
|
|
209
|
+
throw new Error("usage: outsrc updates [list] | updates refresh | updates ack <product@version>... | updates ack --all");
|
|
210
|
+
}
|
|
116
211
|
else if (command === "config") {
|
|
117
212
|
const [sub, key, value, ...extra] = process.argv.slice(3).filter((arg) => arg !== "--json");
|
|
118
213
|
const configPath = defaultConfigPath(defaultHome());
|
|
@@ -144,22 +239,30 @@ try {
|
|
|
144
239
|
const parsed = parseMailboxArgs(process.argv.slice(3));
|
|
145
240
|
// Owner CLI (no --caller) sees every thread for doctor/list/threads/prune.
|
|
146
241
|
// Mailbox mutations and per-thread reads default to "local" when --caller is omitted.
|
|
147
|
-
const ownerCommands = new Set(["doctor", "list_repos", "repos", "list_targets", "targets", "threads", "prune", "migrate"]);
|
|
242
|
+
const ownerCommands = new Set(["doctor", "list_repos", "repos", "list_targets", "targets", "models", "threads", "usage", "prune", "migrate"]);
|
|
148
243
|
const caller = parsed.caller ?? (ownerCommands.has(command) ? undefined : DEFAULT_CALLER);
|
|
149
244
|
const box = createMailbox({ home, config, ...(caller !== undefined ? { caller } : {}) });
|
|
150
245
|
switch (command) {
|
|
151
246
|
case "doctor": {
|
|
152
247
|
const targets = box.listTargets().targets;
|
|
153
248
|
const plugins = Object.values(config.targets).flatMap((target) => target.adapter && isPluginAdapter(target.adapter) && target.command ? [checkPluginContract(target.adapter, target.command)] : []);
|
|
249
|
+
const version = await checkForUpdate(installedVersion(), registryFetchLatest());
|
|
250
|
+
// Reads the last `outsrc updates refresh`; the only network call doctor makes is the npm probe above.
|
|
251
|
+
const updatesState = readUpdates(home);
|
|
154
252
|
const report = {
|
|
253
|
+
update_available: updateAvailable(version),
|
|
254
|
+
version,
|
|
155
255
|
node: process.version,
|
|
156
256
|
config: defaultConfigPath(home),
|
|
157
257
|
repositories: box.listRepos().repos,
|
|
158
258
|
targets,
|
|
159
259
|
plugins,
|
|
160
|
-
|
|
260
|
+
updates: { checked_at: updatesState.checked_at, pending: updatesState.pending, unread: unread(home, updatesState) },
|
|
261
|
+
note: "Checks configuration, executable discovery, the vendor plugin CLI contract and whether npm has a newer outsrc. Does not upgrade, authenticate providers or establish an execution security boundary.",
|
|
161
262
|
};
|
|
162
263
|
writeJson(report);
|
|
264
|
+
if (!parsed.json)
|
|
265
|
+
process.stderr.write(`${describeUpdate(version)}\n`);
|
|
163
266
|
if (targets.some((target) => !target.available) || plugins.some((plugin) => plugin.status === "broken"))
|
|
164
267
|
process.exitCode = 1;
|
|
165
268
|
break;
|
|
@@ -170,11 +273,29 @@ try {
|
|
|
170
273
|
break;
|
|
171
274
|
case "list_targets":
|
|
172
275
|
case "targets":
|
|
173
|
-
writeJson(box.listTargets());
|
|
276
|
+
writeJson(box.listTargets({ refresh: parsed.refresh }));
|
|
277
|
+
break;
|
|
278
|
+
case "models": {
|
|
279
|
+
const [sub, ...extra] = parsed.positionals;
|
|
280
|
+
if (extra.length || (sub !== undefined && sub !== "list" && sub !== "refresh"))
|
|
281
|
+
throw new Error("usage: outsrc models [list] | models refresh [--target <name>]");
|
|
282
|
+
const target = parsed.flags["--target"];
|
|
283
|
+
if (sub === "refresh")
|
|
284
|
+
writeJson({ path: modelCachePath(home), ...box.refreshModels(target) });
|
|
285
|
+
else
|
|
286
|
+
writeJson({ path: modelCachePath(home), targets: readModelCache(home) });
|
|
174
287
|
break;
|
|
288
|
+
}
|
|
175
289
|
case "threads":
|
|
176
290
|
writeJson(box.threads());
|
|
177
291
|
break;
|
|
292
|
+
case "usage": {
|
|
293
|
+
const result = box.usage();
|
|
294
|
+
writeJson(result);
|
|
295
|
+
if (!result.ok)
|
|
296
|
+
process.exitCode = 1;
|
|
297
|
+
break;
|
|
298
|
+
}
|
|
178
299
|
case "migrate": {
|
|
179
300
|
const migrated = migrateLegacy({ home, config });
|
|
180
301
|
writeJson(migrated);
|
|
@@ -294,7 +415,7 @@ try {
|
|
|
294
415
|
catch (error) {
|
|
295
416
|
const message = error instanceof Error ? error.message : String(error);
|
|
296
417
|
process.exitCode = 1;
|
|
297
|
-
const mailbox = new Set(["send", "inbox", "history", "log", "diff", "stop", "discard"]);
|
|
418
|
+
const mailbox = new Set(["send", "inbox", "history", "usage", "log", "diff", "stop", "discard"]);
|
|
298
419
|
if (mailbox.has(command))
|
|
299
420
|
process.stdout.write(`${JSON.stringify({ ok: false, error: message }, null, 2)}\n`);
|
|
300
421
|
else
|
package/dist/mailbox.d.ts
CHANGED
|
@@ -7,7 +7,9 @@ export declare function createMailbox(ctx: MailboxContext): {
|
|
|
7
7
|
path: string;
|
|
8
8
|
}[];
|
|
9
9
|
};
|
|
10
|
-
listTargets: (
|
|
10
|
+
listTargets: (options?: {
|
|
11
|
+
refresh?: boolean;
|
|
12
|
+
}) => {
|
|
11
13
|
targets: {
|
|
12
14
|
name: string;
|
|
13
15
|
adapter: import("./adapters.js").Adapter;
|
|
@@ -16,6 +18,7 @@ export declare function createMailbox(ctx: MailboxContext): {
|
|
|
16
18
|
default: string;
|
|
17
19
|
allowed: string[];
|
|
18
20
|
} | null;
|
|
21
|
+
models_refreshed_at: string | null;
|
|
19
22
|
effort: {
|
|
20
23
|
default: string;
|
|
21
24
|
allowed: string[];
|
|
@@ -25,6 +28,17 @@ export declare function createMailbox(ctx: MailboxContext): {
|
|
|
25
28
|
resume: boolean;
|
|
26
29
|
}[];
|
|
27
30
|
};
|
|
31
|
+
refreshModels: (name?: string) => {
|
|
32
|
+
refreshed: Record<string, {
|
|
33
|
+
adapter: string;
|
|
34
|
+
command: string;
|
|
35
|
+
refreshedAt: string;
|
|
36
|
+
models: {
|
|
37
|
+
default: string;
|
|
38
|
+
allowed: string[];
|
|
39
|
+
} | null;
|
|
40
|
+
}>;
|
|
41
|
+
};
|
|
28
42
|
send: (input: SendInput) => Promise<SendResult>;
|
|
29
43
|
inbox: (threadId: string) => InboxResult;
|
|
30
44
|
threads: () => {
|
|
@@ -52,11 +66,102 @@ export declare function createMailbox(ctx: MailboxContext): {
|
|
|
52
66
|
ok: boolean;
|
|
53
67
|
error: string;
|
|
54
68
|
};
|
|
69
|
+
usage(): {
|
|
70
|
+
ok: boolean;
|
|
71
|
+
as_of: string;
|
|
72
|
+
today: {
|
|
73
|
+
runs: number;
|
|
74
|
+
tokens_in: {
|
|
75
|
+
total: number | null;
|
|
76
|
+
missing_runs: number;
|
|
77
|
+
};
|
|
78
|
+
tokens_out: {
|
|
79
|
+
total: number | null;
|
|
80
|
+
missing_runs: number;
|
|
81
|
+
};
|
|
82
|
+
estimated_cost_usd: {
|
|
83
|
+
total: number | null;
|
|
84
|
+
missing_runs: number;
|
|
85
|
+
};
|
|
86
|
+
wall_minutes: {
|
|
87
|
+
total: number | null;
|
|
88
|
+
missing_runs: number;
|
|
89
|
+
};
|
|
90
|
+
since: string;
|
|
91
|
+
by_target: {
|
|
92
|
+
runs: number;
|
|
93
|
+
tokens_in: {
|
|
94
|
+
total: number | null;
|
|
95
|
+
missing_runs: number;
|
|
96
|
+
};
|
|
97
|
+
tokens_out: {
|
|
98
|
+
total: number | null;
|
|
99
|
+
missing_runs: number;
|
|
100
|
+
};
|
|
101
|
+
estimated_cost_usd: {
|
|
102
|
+
total: number | null;
|
|
103
|
+
missing_runs: number;
|
|
104
|
+
};
|
|
105
|
+
wall_minutes: {
|
|
106
|
+
total: number | null;
|
|
107
|
+
missing_runs: number;
|
|
108
|
+
};
|
|
109
|
+
target: string;
|
|
110
|
+
}[];
|
|
111
|
+
};
|
|
112
|
+
last_7_days: {
|
|
113
|
+
runs: number;
|
|
114
|
+
tokens_in: {
|
|
115
|
+
total: number | null;
|
|
116
|
+
missing_runs: number;
|
|
117
|
+
};
|
|
118
|
+
tokens_out: {
|
|
119
|
+
total: number | null;
|
|
120
|
+
missing_runs: number;
|
|
121
|
+
};
|
|
122
|
+
estimated_cost_usd: {
|
|
123
|
+
total: number | null;
|
|
124
|
+
missing_runs: number;
|
|
125
|
+
};
|
|
126
|
+
wall_minutes: {
|
|
127
|
+
total: number | null;
|
|
128
|
+
missing_runs: number;
|
|
129
|
+
};
|
|
130
|
+
since: string;
|
|
131
|
+
by_target: {
|
|
132
|
+
runs: number;
|
|
133
|
+
tokens_in: {
|
|
134
|
+
total: number | null;
|
|
135
|
+
missing_runs: number;
|
|
136
|
+
};
|
|
137
|
+
tokens_out: {
|
|
138
|
+
total: number | null;
|
|
139
|
+
missing_runs: number;
|
|
140
|
+
};
|
|
141
|
+
estimated_cost_usd: {
|
|
142
|
+
total: number | null;
|
|
143
|
+
missing_runs: number;
|
|
144
|
+
};
|
|
145
|
+
wall_minutes: {
|
|
146
|
+
total: number | null;
|
|
147
|
+
missing_runs: number;
|
|
148
|
+
};
|
|
149
|
+
target: string;
|
|
150
|
+
}[];
|
|
151
|
+
};
|
|
152
|
+
} | {
|
|
153
|
+
ok: boolean;
|
|
154
|
+
error: string;
|
|
155
|
+
};
|
|
55
156
|
history(threadId: string): {
|
|
56
157
|
error?: never;
|
|
57
158
|
ok: boolean;
|
|
58
159
|
thread_id: ThreadId;
|
|
59
160
|
runs: {
|
|
161
|
+
target: string;
|
|
162
|
+
effort: string | null;
|
|
163
|
+
model: string | null;
|
|
164
|
+
usage: Omit<import("./usage.js").RunUsage, "model" | "effort">;
|
|
60
165
|
run_id: import("./types.js").RunId;
|
|
61
166
|
request_id: string | null;
|
|
62
167
|
created_at: string;
|