aegiscode 6.3.2 → 6.5.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 +77 -11
- package/bin/aegiscode.js +161 -2
- package/package.json +2 -2
- package/scripts/predist.mjs +5 -0
- package/src/app.js +178 -43
- package/src/cloudsync.js +401 -0
- package/src/commands.js +285 -17
- package/src/config.js +8 -6
- package/src/credentials.js +30 -0
- package/src/history.js +120 -8
- package/src/screens.js +159 -9
- package/src/secret.js +56 -0
- package/src/shared.js +45 -0
- package/vendor/client/credentials.js +386 -0
- package/vendor/client/session-store.js +418 -0
package/README.md
CHANGED
|
@@ -38,12 +38,26 @@ npm install -g aegiscode
|
|
|
38
38
|
node cli/bin/aegiscode.js
|
|
39
39
|
```
|
|
40
40
|
|
|
41
|
-
Requires Node 18+.
|
|
41
|
+
Requires Node 18+. Save your key once — it goes to `~/.aegiscode/credentials.json`
|
|
42
|
+
(mode `0600`) and is then picked up by every later launch, script and shell:
|
|
42
43
|
|
|
43
44
|
```bash
|
|
44
|
-
|
|
45
|
+
aegiscode login # prompts, no echo
|
|
46
|
+
aegiscode login "aegis_..." # or inline
|
|
47
|
+
aegiscode key status # masked key + which source is in use
|
|
48
|
+
aegiscode logout # remove it
|
|
45
49
|
```
|
|
46
50
|
|
|
51
|
+
`AEGIS_API_KEY` still works and still wins (useful in CI, or for a one-off);
|
|
52
|
+
`aegiscode --key <key>` applies to a single run and is never saved. A key an
|
|
53
|
+
older AEGIS CLI left in `~/.aegiscode/config.json` is picked up and copied into
|
|
54
|
+
the 0600 store automatically — `/cloud status` says so if the plaintext copy is
|
|
55
|
+
still there.
|
|
56
|
+
|
|
57
|
+
That one file is shared: the MCP plugin and AEGIS Desktop read the same store, so
|
|
58
|
+
signing in here signs you in everywhere (see [One memory, shared with AEGIS
|
|
59
|
+
Desktop](#one-memory-shared-with-aegis-desktop)).
|
|
60
|
+
|
|
47
61
|
## Use
|
|
48
62
|
|
|
49
63
|
```bash
|
|
@@ -101,8 +115,8 @@ command for command — 75 entries across nine categories:
|
|
|
101
115
|
| Session & context | `/clear` `/compact` `/cost` `/exit` `/new` `/recap` `/resume` `/rewind` `/agents` `/status` `/teleport` `/version` `/clone` `/schedule` |
|
|
102
116
|
| Workspace | `/run` `/build` `/cd` `/copy` `/init` `/review` `/prs` |
|
|
103
117
|
| Model & behavior | `/model` `/effort` `/thinking` `/theme` `/vim` `/router` `/confirm` `/yolo` `/permissions` `/hooks` `/skills` `/mcp` |
|
|
104
|
-
| Data | `/context` `/export` `/tokens` |
|
|
105
|
-
| Auth | `/credentials` `/byok` `/byok-set` `/byok-rm` |
|
|
118
|
+
| Data | `/context` `/export` `/tokens` `/sync` |
|
|
119
|
+
| Auth | `/key` `/login` `/logout` `/credentials` `/byok` `/byok-set` `/byok-rm` |
|
|
106
120
|
| Support | `/help` `/doctor` `/troubleshooting` `/feedback` `/bug` `/issue` `/onboarding` `/benchmark` `/release-notes` `/billing` `/cloud` |
|
|
107
121
|
| Aegis plugin | `/aegis-ask` `/aegis-status` `/aegis-recall` `/aegis-remember` `/memory` `/aegis-council` `/aegis-multi` `/aegis-print` `/aegis-import` `/tool` |
|
|
108
122
|
| Fun | `/radio` `/waifu` |
|
|
@@ -119,10 +133,60 @@ the project and writes `AEGIS.md`, `/export` writes the transcript out,
|
|
|
119
133
|
`/resume` and `/rewind` read the session store, `/cd` moves the working
|
|
120
134
|
directory, `/doctor` runs diagnostics.
|
|
121
135
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
136
|
+
## Account key and cloud sync
|
|
137
|
+
|
|
138
|
+
Three ways in, one store: `aegiscode login <key>`, `/key <api_key>` (or a bare
|
|
139
|
+
`/key` to paste it echo-off), and `$AEGIS_API_KEY`. The first two persist;
|
|
140
|
+
the env var outranks the store and needs no persistence. `/login` used to be an
|
|
141
|
+
unavailable Claude Code auth-loop command pointing at `/byok-set` — which stores
|
|
142
|
+
a *provider* key, so that advice sent the AEGIS key into the wrong slot. It is
|
|
143
|
+
now the in-band way to set the account key, and `/logout` removes it.
|
|
144
|
+
|
|
145
|
+
Conversation sync is the same `conversationSyncPush/Pull` surface the desktop
|
|
146
|
+
uses, over the sessions this host keeps in `~/.aegiscode/history.jsonl`:
|
|
147
|
+
|
|
148
|
+
```bash
|
|
149
|
+
/sync # push what is pending, then pull — the one you want
|
|
150
|
+
/sync status # local / pending / in-sync counts, last push and pull
|
|
151
|
+
/sync on | off # auto-push after each turn (off by default)
|
|
152
|
+
/cloud # key + sync state in one panel
|
|
153
|
+
/cloud activate # turn on cloud memory for the account
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
Sync is **off by default** on purpose: the server charges a push for the growth
|
|
157
|
+
of a session against the plan's synced-token ceiling, so nothing is uploaded
|
|
158
|
+
until you ask. A refusal (HTTP 402) is reported as a quota refusal with the way
|
|
159
|
+
out, not as "sync failed". Imported remote sessions land in the same store
|
|
160
|
+
`/resume` reads, and are marked `imported` with estimated token counts — never
|
|
161
|
+
passed off as measured here.
|
|
162
|
+
|
|
163
|
+
## One memory, shared with AEGIS Desktop
|
|
164
|
+
|
|
165
|
+
`~/.aegiscode/` is the data dir for **every** AEGIS host — this CLI, AEGIS
|
|
166
|
+
Desktop and the MCP plugin built on the same account:
|
|
167
|
+
|
|
168
|
+
| File | What it holds | Written by |
|
|
169
|
+
|---|---|---|
|
|
170
|
+
| `credentials.json` | the account key, mode 0600 | `aegiscode login`, `/key`, the desktop's Settings pane |
|
|
171
|
+
| `sessions.json` | every conversation, one record per session | both hosts, live |
|
|
172
|
+
| `history.jsonl` | this host's per-exchange ledger (feeds `/cost`) | the CLI |
|
|
173
|
+
| `config.json` | preferences, permissions | the CLI |
|
|
174
|
+
|
|
175
|
+
Sign in once and all three hosts are signed in: they resolve the key with the
|
|
176
|
+
same precedence (`$AEGIS_API_KEY` → `credentials.json` → an older `config.json`),
|
|
177
|
+
and when two hosts hold a key the **most recently saved one wins** — so rotating
|
|
178
|
+
it from the terminal does not leave the app 401-ing on a stale string.
|
|
179
|
+
|
|
180
|
+
Sessions are shared too, and not only through the cloud. `/resume` lists a
|
|
181
|
+
thread typed in the desktop, and the desktop's session list shows a thread typed
|
|
182
|
+
here — same file, no sync and no network. An upgrade adopts a desktop install's
|
|
183
|
+
private `sessions.json` into the shared store once, so no existing conversation
|
|
184
|
+
is lost. Records carry `origin`, and terminal sessions are **not** enrolled in
|
|
185
|
+
the desktop's push queue (that would spend the account's synced-token quota as a
|
|
186
|
+
side effect of typing in a shell); this host's own `/sync` covers them.
|
|
187
|
+
|
|
188
|
+
There are currently **no** `unavailable` commands: every entry in the registry
|
|
189
|
+
either runs or is a real handler that says honestly what it cannot do.
|
|
126
190
|
|
|
127
191
|
## The chatflow
|
|
128
192
|
|
|
@@ -181,9 +245,11 @@ What the loop does, in the order a turn happens:
|
|
|
181
245
|
`/resume` the session list, `?` the shortcut grid, `ctrl+o` permissions, and a
|
|
182
246
|
centred Yes/No dialog when a mutating tool needs approval. `Esc` closes an
|
|
183
247
|
overlay, and clears the input line when nothing is open.
|
|
184
|
-
- **Every turn is persisted** to `~/.aegiscode/history.jsonl
|
|
185
|
-
|
|
186
|
-
|
|
248
|
+
- **Every turn is persisted** to `~/.aegiscode/history.jsonl` and mirrored into
|
|
249
|
+
the shared `~/.aegiscode/sessions.json`, with a transcript checkpoint alongside
|
|
250
|
+
them, so `/resume`, `/cost`, `/clear` and `/rewind` all have something real to
|
|
251
|
+
read — and the desktop app sees the same conversations. On exit the session
|
|
252
|
+
prints how to come back to it.
|
|
187
253
|
|
|
188
254
|
Anything that is not a real terminal — a pipe, `-p`, a CI run — stays a linear
|
|
189
255
|
transcript written once to scrollback, so output remains pipeable and
|
package/bin/aegiscode.js
CHANGED
|
@@ -24,10 +24,16 @@ Usage:
|
|
|
24
24
|
aegiscode -p "question" same, explicit
|
|
25
25
|
echo "q" | aegiscode -p - read the prompt from stdin
|
|
26
26
|
|
|
27
|
+
Account:
|
|
28
|
+
aegiscode login [<key>] save your AEGIS API key (prompts, no echo, if omitted)
|
|
29
|
+
aegiscode logout remove the saved key
|
|
30
|
+
aegiscode key status show which key is in use and where it came from
|
|
31
|
+
|
|
27
32
|
Options:
|
|
28
33
|
-m, --model <id> pin a model id (see /models; default: server choice)
|
|
29
34
|
--base <url> API base (default $AEGIS_API_BASE or aegiscloud.org)
|
|
30
|
-
--key <key> API key for
|
|
35
|
+
--key <key> API key for THIS RUN only — it is not saved. Use
|
|
36
|
+
"aegiscode login" to store one for good.
|
|
31
37
|
--json with -p: emit JSON instead of text
|
|
32
38
|
--no-stream buffer the answer instead of streaming it
|
|
33
39
|
--max-tokens <n> output ceiling hint
|
|
@@ -59,9 +65,22 @@ function parseArgs(argv) {
|
|
|
59
65
|
prompt: null,
|
|
60
66
|
help: false,
|
|
61
67
|
version: false,
|
|
68
|
+
command: null,
|
|
69
|
+
commandArg: null,
|
|
62
70
|
};
|
|
63
71
|
const rest = [];
|
|
64
72
|
|
|
73
|
+
// An account subcommand is `argv[0]` and nothing else — position 0 only, so
|
|
74
|
+
// `aegiscode -p "key"` (a one-shot prompt whose text happens to be one of
|
|
75
|
+
// these words) is never hijacked into an account operation.
|
|
76
|
+
const head = argv[0];
|
|
77
|
+
if (head && !head.startsWith('-') && ACCOUNT_COMMANDS.has(head)) {
|
|
78
|
+
const arg = argv[1];
|
|
79
|
+
opts.command = head;
|
|
80
|
+
opts.commandArg = arg && !arg.startsWith('-') ? arg : null;
|
|
81
|
+
argv = argv.slice(opts.commandArg ? 2 : 1);
|
|
82
|
+
}
|
|
83
|
+
|
|
65
84
|
for (let i = 0; i < argv.length; i++) {
|
|
66
85
|
const a = argv[i];
|
|
67
86
|
const next = () => {
|
|
@@ -127,6 +146,134 @@ function parseArgs(argv) {
|
|
|
127
146
|
return opts;
|
|
128
147
|
}
|
|
129
148
|
|
|
149
|
+
/**
|
|
150
|
+
* Account subcommands: the in-band way to give this host a key.
|
|
151
|
+
*
|
|
152
|
+
* They exist because an `AEGIS_API_KEY` export was previously the *only* way
|
|
153
|
+
* in, and an export does not survive a new terminal. `--key` remains purely
|
|
154
|
+
* per-run (CI, a rotation test) so there is exactly one thing a user has to
|
|
155
|
+
* remember: `aegiscode login` saves it, `aegiscode logout` removes it.
|
|
156
|
+
*/
|
|
157
|
+
const ACCOUNT_COMMANDS = new Set(['login', 'logout', 'key']);
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Run `login` / `logout` / `key` and return a process exit code.
|
|
161
|
+
*
|
|
162
|
+
* Exit codes are meaningful because these are the commands a script calls:
|
|
163
|
+
* 0 = done, 1 = the server refused the credential, 2 = no credential and no
|
|
164
|
+
* way to ask for one (no TTY).
|
|
165
|
+
*/
|
|
166
|
+
async function runAccountCommand(command, arg, io = {}) {
|
|
167
|
+
const stdout = io.stdout || process.stdout;
|
|
168
|
+
const stderr = io.stderr || process.stderr;
|
|
169
|
+
const stdin = io.stdin || process.stdin;
|
|
170
|
+
const { credentials, readSecret, maskKey } = loadAccountDeps();
|
|
171
|
+
|
|
172
|
+
const where = () => credentials.credentialsPath();
|
|
173
|
+
|
|
174
|
+
if (command === 'logout' || (command === 'key' && ['clear', 'remove', 'rm'].includes(String(arg || '').toLowerCase()))) {
|
|
175
|
+
const res = credentials.clearApiKey();
|
|
176
|
+
stdout.write(
|
|
177
|
+
res.cleared
|
|
178
|
+
? `aegiscode: key removed from ${res.path}\n`
|
|
179
|
+
: 'aegiscode: no saved key to remove\n'
|
|
180
|
+
);
|
|
181
|
+
if (credentials.legacyKeyOnDisk()) {
|
|
182
|
+
stdout.write(
|
|
183
|
+
'aegiscode: note — a plaintext copy is also in config.json, written by an older ' +
|
|
184
|
+
'AEGIS CLI; delete its "aegiscloud" block as well to finish removing it\n'
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
return 0;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (command === 'key' && ['status', 'show', ''].includes(String(arg || '').toLowerCase())) {
|
|
191
|
+
const st = credentials.keyStatus();
|
|
192
|
+
if (io.json) {
|
|
193
|
+
stdout.write(
|
|
194
|
+
JSON.stringify(
|
|
195
|
+
{
|
|
196
|
+
configured: st.configured,
|
|
197
|
+
source: st.source,
|
|
198
|
+
source_label: credentials.sourceLabel(st.source),
|
|
199
|
+
path: st.path,
|
|
200
|
+
file_mode: st.fileMode,
|
|
201
|
+
masked: st.configured ? maskKey(st.key) : null,
|
|
202
|
+
memory_token: st.memoryToken,
|
|
203
|
+
verified_at: st.verifiedAt,
|
|
204
|
+
plaintext_copy_in_config: st.legacyPlaintext,
|
|
205
|
+
},
|
|
206
|
+
null,
|
|
207
|
+
2
|
|
208
|
+
) + '\n'
|
|
209
|
+
);
|
|
210
|
+
return st.configured ? 0 : 1;
|
|
211
|
+
}
|
|
212
|
+
stdout.write(
|
|
213
|
+
[
|
|
214
|
+
`key: ${st.configured ? maskKey(st.key) : 'not set'}`,
|
|
215
|
+
`source: ${credentials.sourceLabel(st.source)}`,
|
|
216
|
+
`file: ${st.path}${st.fileMode ? ` (${st.fileMode})` : ''}`,
|
|
217
|
+
`memory: ${st.memoryToken ? 'token held (cloud sync ready)' : 'no token'}`,
|
|
218
|
+
st.verifiedAt ? `verified: ${st.verifiedAt}` : null,
|
|
219
|
+
st.legacyPlaintext
|
|
220
|
+
? `note: a plaintext copy also sits in config.json — re-save with \`aegiscode login\` to move it`
|
|
221
|
+
: null,
|
|
222
|
+
]
|
|
223
|
+
.filter(Boolean)
|
|
224
|
+
.join('\n') + '\n'
|
|
225
|
+
);
|
|
226
|
+
return st.configured ? 0 : 1;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// `login [<key>]` and `key <api_key>`: take the key inline or ask for it.
|
|
230
|
+
let key = arg && arg !== 'status' ? String(arg).trim() : '';
|
|
231
|
+
if (!key) {
|
|
232
|
+
if (!stdin.isTTY) {
|
|
233
|
+
stderr.write(
|
|
234
|
+
'aegiscode: no terminal to prompt on — pass the key: `aegiscode login <api_key>`\n' +
|
|
235
|
+
' (or set AEGIS_API_KEY for a single run)\n'
|
|
236
|
+
);
|
|
237
|
+
return 2;
|
|
238
|
+
}
|
|
239
|
+
key = await readSecret('AEGIS API key (kept off screen, Enter to cancel): ', { stdin, stdout });
|
|
240
|
+
}
|
|
241
|
+
if (!key) {
|
|
242
|
+
stderr.write('aegiscode: no key given — nothing saved\n');
|
|
243
|
+
return 2;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const { createApp } = require('../src/app.js');
|
|
247
|
+
const app = createApp({ interactive: false });
|
|
248
|
+
const res = await app.setApiKey(key);
|
|
249
|
+
if (!res.ok) {
|
|
250
|
+
stderr.write(`aegiscode: ${res.message || 'that key could not be saved'}\n`);
|
|
251
|
+
return 2;
|
|
252
|
+
}
|
|
253
|
+
const who = res.account && (res.account.email || res.account.plan);
|
|
254
|
+
stdout.write(`aegiscode: key saved to ${res.path}${who ? ` — ${who}` : ''}\n`);
|
|
255
|
+
if (res.error) {
|
|
256
|
+
const status = res.error.status;
|
|
257
|
+
stderr.write(`aegiscode: the account check failed: ${res.error.message}\n`);
|
|
258
|
+
if (status === 401 || status === 403) {
|
|
259
|
+
stderr.write('aegiscode: the key is stored but the server refused it — re-run with a fresh key\n');
|
|
260
|
+
return 1;
|
|
261
|
+
}
|
|
262
|
+
stderr.write('aegiscode: (stored anyway — the server could not be reached to confirm it)\n');
|
|
263
|
+
return 0;
|
|
264
|
+
}
|
|
265
|
+
stdout.write('aegiscode: verified — run `aegiscode` to start a session\n');
|
|
266
|
+
return 0;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/** Lazily required so `--version`/`--help` stay dependency-free and instant. */
|
|
270
|
+
function loadAccountDeps() {
|
|
271
|
+
const credentials = require('../src/credentials.js');
|
|
272
|
+
const { readSecret } = require('../src/secret.js');
|
|
273
|
+
const { maskKey } = require('../src/format.js');
|
|
274
|
+
return { credentials, readSecret, maskKey };
|
|
275
|
+
}
|
|
276
|
+
|
|
130
277
|
function readStdin() {
|
|
131
278
|
return new Promise((resolve) => {
|
|
132
279
|
let buf = '';
|
|
@@ -163,6 +310,18 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
163
310
|
if (opts.base) process.env.AEGIS_API_BASE = opts.base;
|
|
164
311
|
if (opts.key) process.env.AEGIS_API_KEY = opts.key;
|
|
165
312
|
|
|
313
|
+
// Account subcommands run before a session is built: they are one-shot,
|
|
314
|
+
// non-interactive, and must work in a script (`aegiscode login "$KEY"`) as
|
|
315
|
+
// well as at a prompt.
|
|
316
|
+
if (ACCOUNT_COMMANDS.has(opts.command)) {
|
|
317
|
+
return runAccountCommand(opts.command, opts.commandArg, {
|
|
318
|
+
json: opts.json,
|
|
319
|
+
stdin: process.stdin,
|
|
320
|
+
stdout: process.stdout,
|
|
321
|
+
stderr: process.stderr,
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
|
|
166
325
|
const { createApp } = require('../src/app.js');
|
|
167
326
|
|
|
168
327
|
let prompt = opts.prompt;
|
|
@@ -214,4 +373,4 @@ if (require.main === module) {
|
|
|
214
373
|
});
|
|
215
374
|
}
|
|
216
375
|
|
|
217
|
-
module.exports = { main, parseArgs, HELP };
|
|
376
|
+
module.exports = { main, parseArgs, HELP, ACCOUNT_COMMANDS, runAccountCommand };
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "aegiscode",
|
|
3
3
|
"productName": "AEGIS Code",
|
|
4
|
-
"version": "6.
|
|
5
|
-
"description": "aegiscode
|
|
4
|
+
"version": "6.5.0",
|
|
5
|
+
"description": "aegiscode \u2014 the command-line version of AEGIS Desktop. The shared tool surface in your shell, over the same thin transport and tool registry as the MCP plugin and the desktop app. Ships transport + UI only; no brain.",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "AEGIS Code",
|
|
8
8
|
"email": "nborneklint@gmail.com"
|
package/scripts/predist.mjs
CHANGED
|
@@ -38,6 +38,11 @@ const VENDOR = path.join(CLI_DIR, 'vendor');
|
|
|
38
38
|
const FILES = [
|
|
39
39
|
'client/aegis.js',
|
|
40
40
|
'client/foreign-memory.js',
|
|
41
|
+
// The account credential store and the unified session store. Shared with the
|
|
42
|
+
// desktop app and the MCP plugin so all three see one key and one session
|
|
43
|
+
// list; `src/shared.js` resolves them from here in an installed package.
|
|
44
|
+
'client/credentials.js',
|
|
45
|
+
'client/session-store.js',
|
|
41
46
|
'mcp/tools.js',
|
|
42
47
|
'desktop/renderer/usage.js',
|
|
43
48
|
// The agent-loop engine (persistent shell, editFile/grep/exec, Task
|
package/src/app.js
CHANGED
|
@@ -27,6 +27,9 @@ const { GLYPH, VERBS, themeOf, RESET, THEME_TABLE } = require('./theme.js');
|
|
|
27
27
|
const { LiveRegion, termWidth, w } = require('./screen.js');
|
|
28
28
|
const { parseLine, COMMANDS, visibleCommands } = require('./commands.js');
|
|
29
29
|
const { updateConfig, loadPermissions, loadConfig, configExists } = require('./config.js');
|
|
30
|
+
const credentials = require('./credentials.js');
|
|
31
|
+
const cloudsync = require('./cloudsync.js');
|
|
32
|
+
const { readSecret } = require('./secret.js');
|
|
30
33
|
const { normalizeModelCatalog, pickerEntries, catalogIds } = require('./models.js');
|
|
31
34
|
const { appendHistory, readSessionTranscript, readOwnSessions } = require('./history.js');
|
|
32
35
|
const { snapshotCheckpoint } = require('./checkpoint.js');
|
|
@@ -50,7 +53,12 @@ function createApp(options = {}) {
|
|
|
50
53
|
};
|
|
51
54
|
const out = options.out || process.stdout;
|
|
52
55
|
const err = options.err || process.stderr;
|
|
53
|
-
|
|
56
|
+
// The key comes from the credential store (env → credentials.json →
|
|
57
|
+
// config.json) rather than from the environment alone: an export does not
|
|
58
|
+
// survive a new shell, and until it is *stored* every one of those launches
|
|
59
|
+
// is "no key" with no in-band way to set one. An injected client keeps
|
|
60
|
+
// whatever key it was built with.
|
|
61
|
+
const client = options.client || createClient(credentials.clientOptions());
|
|
54
62
|
const { TOOLS, toolList } = options.tools || createTools(client);
|
|
55
63
|
|
|
56
64
|
// Tool-approval gate (exec/writeFile/editFile confirm before running).
|
|
@@ -151,6 +159,83 @@ function createApp(options = {}) {
|
|
|
151
159
|
}
|
|
152
160
|
}
|
|
153
161
|
|
|
162
|
+
// ── the AEGIS account key ─────────────────────────────────────────────────
|
|
163
|
+
//
|
|
164
|
+
// Set/forget at runtime, for `/key` and `aegiscode login`. Both paths are the
|
|
165
|
+
// same three steps — persist, apply to the live client, invalidate the model
|
|
166
|
+
// catalog — because a key that is stored but not applied leaves the current
|
|
167
|
+
// session failing, and one that is applied but not stored is gone next launch.
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Persist an account key and start using it immediately.
|
|
171
|
+
*
|
|
172
|
+
* `verify` spends one `/api/verify-api-key` round trip to catch a bad key at
|
|
173
|
+
* the moment it is entered rather than at the first prompt, and is also how
|
|
174
|
+
* the memory token (cloud sync's credential) is obtained — so a successful
|
|
175
|
+
* login leaves both credentials in the store.
|
|
176
|
+
*/
|
|
177
|
+
async function setApiKey(raw, { verify = true } = {}) {
|
|
178
|
+
const saved = credentials.saveApiKey(raw);
|
|
179
|
+
if (!saved.ok) return saved;
|
|
180
|
+
client.setApiKey(saved.key);
|
|
181
|
+
modelCache.models = [];
|
|
182
|
+
modelCache.at = 0;
|
|
183
|
+
let account = null;
|
|
184
|
+
let error = null;
|
|
185
|
+
if (verify) {
|
|
186
|
+
try {
|
|
187
|
+
const info = (await client.verifyApiKey()) || {};
|
|
188
|
+
account = info;
|
|
189
|
+
const patch = { verifiedAt: new Date().toISOString() };
|
|
190
|
+
if (info.memory_token) patch.memoryToken = info.memory_token;
|
|
191
|
+
if (info.plan) patch.plan = info.plan;
|
|
192
|
+
patch.account = { plan: info.plan || null, email: info.email || null, valid: info.valid !== false };
|
|
193
|
+
credentials.writeCredentials(patch);
|
|
194
|
+
if (info.plan) session.plan = info.plan;
|
|
195
|
+
if (info.email) session.account = { ...(session.account || {}), email: info.email };
|
|
196
|
+
} catch (e) {
|
|
197
|
+
error = e;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
return { ...saved, account, error };
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/** Drop the stored key and stop authenticating this session. */
|
|
204
|
+
function forgetApiKey() {
|
|
205
|
+
const res = credentials.clearApiKey();
|
|
206
|
+
client.setApiKey('');
|
|
207
|
+
modelCache.models = [];
|
|
208
|
+
modelCache.at = 0;
|
|
209
|
+
return res;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function cloudSyncEnabled() {
|
|
213
|
+
try {
|
|
214
|
+
return loadConfig().cloudSync === true;
|
|
215
|
+
} catch {
|
|
216
|
+
return false;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function setCloudSync(on) {
|
|
221
|
+
return updateConfig({ cloudSync: on === true });
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Copy a key found in config.json into the 0600 store, once, so the next run
|
|
226
|
+
* reads the tight copy. Never deletes the original — that file belongs to
|
|
227
|
+
* another product and holds the user's key.
|
|
228
|
+
*/
|
|
229
|
+
function adoptLegacyKeyOnce() {
|
|
230
|
+
try {
|
|
231
|
+
const resolved = credentials.resolveApiKey();
|
|
232
|
+
if (resolved.source !== 'config') return [];
|
|
233
|
+
return credentials.adoptLegacy().adopted;
|
|
234
|
+
} catch {
|
|
235
|
+
return [];
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
154
239
|
// ── the AEGIS Cloud model catalog ──────────────────────────────────────────
|
|
155
240
|
//
|
|
156
241
|
// `/model` (and the alt+p chord that dispatches it) reads the pinnable ids
|
|
@@ -448,7 +533,7 @@ function createApp(options = {}) {
|
|
|
448
533
|
render.renderNotice(
|
|
449
534
|
ctx(),
|
|
450
535
|
'error',
|
|
451
|
-
|
|
536
|
+
`no AEGIS account key — ${credentials.HOW_TO_SET} (or set $${credentials.KEY_ENV})`
|
|
452
537
|
)
|
|
453
538
|
);
|
|
454
539
|
return;
|
|
@@ -506,7 +591,7 @@ function createApp(options = {}) {
|
|
|
506
591
|
const tool = TOOLS[name];
|
|
507
592
|
if (!tool) throw new Error(`unknown tool: ${name}`);
|
|
508
593
|
if (!client.apiKey) {
|
|
509
|
-
throw new Error(
|
|
594
|
+
throw new Error(`no AEGIS account key — ${credentials.HOW_TO_SET}`);
|
|
510
595
|
}
|
|
511
596
|
const text = await tool.run(args || {});
|
|
512
597
|
emit(render.renderToolResult(ctx(), name, text, width()));
|
|
@@ -725,6 +810,17 @@ function createApp(options = {}) {
|
|
|
725
810
|
TOOLS,
|
|
726
811
|
saveConfig: (patch) => updateConfig(patch),
|
|
727
812
|
showThemePicker: () => showThemePicker(),
|
|
813
|
+
// Credential + cloud-sync surface for the /key, /cloud and /sync commands.
|
|
814
|
+
// Defined here on the app's context so the chatflow's Object.assign-based
|
|
815
|
+
// context inherits them too — one definition, both hosts.
|
|
816
|
+
keyStatus: () => credentials.keyStatus(),
|
|
817
|
+
setApiKey: (raw, o) => setApiKey(raw, o),
|
|
818
|
+
forgetApiKey: () => forgetApiKey(),
|
|
819
|
+
readSecret: (text) => readSecret(text, { stdin: process.stdin, stdout: out }),
|
|
820
|
+
cloudsync,
|
|
821
|
+
cloudSyncEnabled: () => cloudSyncEnabled(),
|
|
822
|
+
setCloudSync: (on) => setCloudSync(on),
|
|
823
|
+
adoptLegacyKey: () => adoptLegacyKeyOnce(),
|
|
728
824
|
};
|
|
729
825
|
Object.defineProperty(c, 'sessionId', {
|
|
730
826
|
enumerable: true,
|
|
@@ -813,7 +909,7 @@ function createApp(options = {}) {
|
|
|
813
909
|
try {
|
|
814
910
|
let args = cmd.build(arg);
|
|
815
911
|
if (cmd.secret) {
|
|
816
|
-
const key = await
|
|
912
|
+
const key = await askSecret(`provider key for ${args.provider}: `);
|
|
817
913
|
if (!key) {
|
|
818
914
|
emit(render.renderNotice(ctx(), 'warn', 'empty key — nothing sent'));
|
|
819
915
|
return true;
|
|
@@ -828,52 +924,26 @@ function createApp(options = {}) {
|
|
|
828
924
|
return true;
|
|
829
925
|
}
|
|
830
926
|
|
|
831
|
-
/**
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
stdin.resume();
|
|
843
|
-
const onData = (chunk) => {
|
|
844
|
-
for (const ch of chunk.toString('utf8')) {
|
|
845
|
-
if (ch === '\r' || ch === '\n') {
|
|
846
|
-
stdin.setRawMode(false);
|
|
847
|
-
stdin.removeListener('data', onData);
|
|
848
|
-
out.write('\n');
|
|
849
|
-
return resolve(buf);
|
|
850
|
-
}
|
|
851
|
-
if (ch === '\x03') {
|
|
852
|
-
// ctrl+c
|
|
853
|
-
stdin.setRawMode(false);
|
|
854
|
-
stdin.removeListener('data', onData);
|
|
855
|
-
out.write('\n');
|
|
856
|
-
return resolve('');
|
|
857
|
-
}
|
|
858
|
-
if (ch === '\u007f') {
|
|
859
|
-
buf = buf.slice(0, -1);
|
|
860
|
-
out.write('\b \b');
|
|
861
|
-
continue;
|
|
862
|
-
}
|
|
863
|
-
buf += ch;
|
|
864
|
-
out.write('•');
|
|
865
|
-
}
|
|
866
|
-
};
|
|
867
|
-
stdin.on('data', onData);
|
|
868
|
-
});
|
|
927
|
+
/**
|
|
928
|
+
* Ask for a secret on this session's streams.
|
|
929
|
+
*
|
|
930
|
+
* The prompt loop itself lives in secret.js, shared with `aegiscode login` in
|
|
931
|
+
* the bin — one implementation, so the terminal host and the non-interactive
|
|
932
|
+
* entry point cannot differ in whether a key is echoed or masked. Only the
|
|
933
|
+
* prompt's colouring is the session's business.
|
|
934
|
+
*/
|
|
935
|
+
function askSecret(promptText) {
|
|
936
|
+
const t = themeOf(ctx());
|
|
937
|
+
return readSecret(t.gray + promptText + RESET, { stdin: process.stdin, stdout: out });
|
|
869
938
|
}
|
|
870
939
|
|
|
871
940
|
// --- entry points ---------------------------------------------------------
|
|
872
941
|
|
|
873
942
|
/** Non-interactive: one prompt, plain output, exit code. */
|
|
874
943
|
async function runOnce(prompt, { json = false } = {}) {
|
|
944
|
+
adoptLegacyKeyOnce();
|
|
875
945
|
if (!client.apiKey) {
|
|
876
|
-
err.write(
|
|
946
|
+
err.write(`aegiscode: no AEGIS account key. ${credentials.HOW_TO_SET}, or set $${credentials.KEY_ENV}.\n`);
|
|
877
947
|
return 2;
|
|
878
948
|
}
|
|
879
949
|
const res = await ask(prompt);
|
|
@@ -958,6 +1028,39 @@ function createApp(options = {}) {
|
|
|
958
1028
|
} catch {
|
|
959
1029
|
/* persistence is best-effort */
|
|
960
1030
|
}
|
|
1031
|
+
// Auto-sync hook: one place, reached by both the linear REPL and the
|
|
1032
|
+
// chatflow's session loop, and only when the user turned it on. Off by
|
|
1033
|
+
// default because a push is billed against the plan's synced-token ceiling
|
|
1034
|
+
// (aegis1 charges the *growth* of a session), so uploading is a choice.
|
|
1035
|
+
if (cloudSyncEnabled()) autoSync();
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
/**
|
|
1039
|
+
* Push after a turn, in the background.
|
|
1040
|
+
*
|
|
1041
|
+
* Deliberately not awaited and deliberately quiet on success: this runs after
|
|
1042
|
+
* every turn, and a line per turn would be noise. Failures are reported once
|
|
1043
|
+
* per session per session-cycle — a quota refusal that repeats forever is a
|
|
1044
|
+
* wall of text, not information — and never thrown, because the user's turn
|
|
1045
|
+
* already succeeded by the time this runs.
|
|
1046
|
+
*/
|
|
1047
|
+
let autoSyncNotes = new Set();
|
|
1048
|
+
function autoSync() {
|
|
1049
|
+
if (!client.apiKey) return;
|
|
1050
|
+
Promise.resolve()
|
|
1051
|
+
.then(() => cloudsync.push(client))
|
|
1052
|
+
.then((res) => {
|
|
1053
|
+
if (res && res.failed && res.failed.length) {
|
|
1054
|
+
const f = res.failed[0];
|
|
1055
|
+
if (autoSyncNotes.has(f.kind)) return;
|
|
1056
|
+
autoSyncNotes.add(f.kind);
|
|
1057
|
+
const hint = f.hint ? ` — ${f.hint}` : '';
|
|
1058
|
+
emit(render.renderNotice(ctx(), f.kind === 'quota' ? 'warn' : 'error', `cloud sync: ${f.message}${hint}`));
|
|
1059
|
+
}
|
|
1060
|
+
})
|
|
1061
|
+
.catch(() => {
|
|
1062
|
+
/* offline is not worth interrupting a session over */
|
|
1063
|
+
});
|
|
961
1064
|
}
|
|
962
1065
|
|
|
963
1066
|
/** One line of session accounting, for ctrl+t and the meta row. */ function tokenSummary() {
|
|
@@ -1115,6 +1218,7 @@ function createApp(options = {}) {
|
|
|
1115
1218
|
process.stdout.isTTY;
|
|
1116
1219
|
if (tty) {
|
|
1117
1220
|
restorePrefs();
|
|
1221
|
+
adoptLegacyKeyOnce();
|
|
1118
1222
|
// Onboarding runs in the normal buffer, before the session takes the
|
|
1119
1223
|
// alternate screen — the reference's order. A declined trust check must
|
|
1120
1224
|
// abort: the reference returns without ever reaching `session(ctx)`.
|
|
@@ -1122,8 +1226,31 @@ function createApp(options = {}) {
|
|
|
1122
1226
|
continue: !!options.continue,
|
|
1123
1227
|
seen: options.seen || configExists,
|
|
1124
1228
|
save: (patch) => updateConfig(patch),
|
|
1229
|
+
// With no key the session can do nothing, so ask for it here where the
|
|
1230
|
+
// user is already answering questions — verified on submit, and saved
|
|
1231
|
+
// 0600 so the next launch (and every script) has it.
|
|
1232
|
+
needsKey: () => !client.apiKey,
|
|
1233
|
+
submitKey: (key) => setApiKey(key),
|
|
1125
1234
|
});
|
|
1126
1235
|
if (!onboard.ok) return 0;
|
|
1236
|
+
if (onboard.key && onboard.key.set) {
|
|
1237
|
+
emit(
|
|
1238
|
+
render.renderNotice(
|
|
1239
|
+
ctx(),
|
|
1240
|
+
'info',
|
|
1241
|
+
`key saved to ${credentials.credentialsPath()} — no export needed from now on`
|
|
1242
|
+
)
|
|
1243
|
+
);
|
|
1244
|
+
}
|
|
1245
|
+
if (onboard.key && onboard.key.skipped && !client.apiKey) {
|
|
1246
|
+
emit(
|
|
1247
|
+
render.renderNotice(
|
|
1248
|
+
ctx(),
|
|
1249
|
+
'warn',
|
|
1250
|
+
`no key saved — /key <api_key> or \`aegiscode login\` whenever you are ready`
|
|
1251
|
+
)
|
|
1252
|
+
);
|
|
1253
|
+
}
|
|
1127
1254
|
// A stored pin the platform does not advertise routes elsewhere in
|
|
1128
1255
|
// silence (see validatePinnedModel) — checked once, here, where the user
|
|
1129
1256
|
// can act on it. Not on `-p`: a network round-trip ahead of the first
|
|
@@ -1143,6 +1270,7 @@ function createApp(options = {}) {
|
|
|
1143
1270
|
return 0;
|
|
1144
1271
|
}
|
|
1145
1272
|
restorePrefs();
|
|
1273
|
+
adoptLegacyKeyOnce();
|
|
1146
1274
|
return runLinearRepl();
|
|
1147
1275
|
}
|
|
1148
1276
|
|
|
@@ -1168,6 +1296,13 @@ function createApp(options = {}) {
|
|
|
1168
1296
|
recordTurn,
|
|
1169
1297
|
persistTurn,
|
|
1170
1298
|
restorePrefs,
|
|
1299
|
+
setApiKey,
|
|
1300
|
+
forgetApiKey,
|
|
1301
|
+
adoptLegacyKeyOnce,
|
|
1302
|
+
cloudSyncEnabled,
|
|
1303
|
+
setCloudSync,
|
|
1304
|
+
cloudsync,
|
|
1305
|
+
keyStatus: () => credentials.keyStatus(),
|
|
1171
1306
|
sessionId: () => commandCtx.sessionId,
|
|
1172
1307
|
tokenSummary,
|
|
1173
1308
|
resumeSession,
|