anon-pi 0.1.1 → 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/src/cli.ts CHANGED
@@ -1,12 +1,27 @@
1
1
  #!/usr/bin/env node
2
- // anon-pi CLI: resolve the run plan (pure), do the one filesystem side-effect
3
- // (seed the session config if absent), then exec `netcage run ...` with inherited
4
- // stdio so the interactive pi session (-it) passes through the terminal cleanly.
2
+ // anon-pi CLI. Two commands:
3
+ // anon-pi [WORKDIR] resolve the run plan (pure) and exec `netcage run ...`
4
+ // with inherited stdio (so -it is a real interactive TTY).
5
+ // The seed models.json is mounted read-only and copied
6
+ // into the container's ~/.pi/agent by the run command, so
7
+ // it layers onto the image's config (extensions survive).
8
+ // anon-pi import generate the seed models.json from the host models.json,
9
+ // carrying only the provider that serves ANON_PI_LLM.
5
10
 
6
- import {cpSync, existsSync, mkdirSync} from 'node:fs';
11
+ import {existsSync, mkdirSync, readFileSync, writeFileSync} from 'node:fs';
7
12
  import {spawnSync} from 'node:child_process';
8
- import {dirname} from 'node:path';
9
- import {AnonPiError, buildRunPlan, envFromProcess, HELP} from './anon-pi.js';
13
+ import {join} from 'node:path';
14
+ import {
15
+ AnonPiError,
16
+ buildRunPlan,
17
+ envFromProcess,
18
+ HELP,
19
+ MODELS_FILE,
20
+ pickProviderForLlm,
21
+ resolveConfigSeed,
22
+ resolveSourceModelsPath,
23
+ type PiModelsFile,
24
+ } from './anon-pi.js';
10
25
 
11
26
  function main(argv: string[]): number {
12
27
  const args = argv.slice(2);
@@ -16,11 +31,23 @@ function main(argv: string[]): number {
16
31
  return 0;
17
32
  }
18
33
 
19
- // The only positional is the optional workdir. Reject stray flags so a typo
20
- // (e.g. --allow-direct) is not silently swallowed: anon-pi owns the netcage
21
- // argv, extra flags are not passed through.
34
+ // Subcommand dispatch: the first bare token may be `import`.
35
+ if (args[0] === 'import') {
36
+ return runImport(args.slice(1));
37
+ }
38
+
39
+ return runLaunch(args);
40
+ }
41
+
42
+ // --- anon-pi [WORKDIR] : launch pi jailed -----------------------------------
43
+ function runLaunch(args: string[]): number {
44
+ // One optional positional (the workdir) + the --ephemeral flag. Reject other
45
+ // flags so a typo is not silently swallowed: anon-pi owns the netcage argv.
46
+ const ephemeralFlag = args.includes('--ephemeral') || args.includes('--eph');
22
47
  const positionals = args.filter((a) => !a.startsWith('-'));
23
- const flags = args.filter((a) => a.startsWith('-'));
48
+ const flags = args.filter(
49
+ (a) => a.startsWith('-') && a !== '--ephemeral' && a !== '--eph',
50
+ );
24
51
  if (flags.length > 0) {
25
52
  process.stderr.write(
26
53
  `anon-pi: unknown option(s): ${flags.join(' ')}\nRun \`anon-pi --help\`.\n`,
@@ -35,6 +62,7 @@ function main(argv: string[]): number {
35
62
  }
36
63
 
37
64
  const env = envFromProcess(process.env);
65
+ if (ephemeralFlag) env.ephemeral = true;
38
66
 
39
67
  let plan;
40
68
  try {
@@ -56,19 +84,23 @@ function main(argv: string[]): number {
56
84
  return 1;
57
85
  }
58
86
 
59
- // The one side-effect: seed the per-session config from the canonical seed the
60
- // FIRST time this workdir is used. Reuse-if-present, seed-if-absent.
61
- if (plan.needsSeed) {
62
- mkdirSync(dirname(plan.sessionAgentDir), {recursive: true});
63
- cpSync(plan.configSeed, plan.sessionAgentDir, {recursive: true});
87
+ mkdirSync(plan.workdir, {recursive: true});
88
+ if (env.ephemeral) {
89
+ // No host state dir: pi writes to the container's own --rm layer, so the
90
+ // session leaves NO trace on the host and there is nothing to clean up.
64
91
  process.stderr.write(
65
- `anon-pi: seeded session config -> ${plan.sessionAgentDir}\n`,
92
+ 'anon-pi: ephemeral session (nothing persisted; no host state)\n',
66
93
  );
94
+ } else {
95
+ // Persistent mode: create the per-workdir state home to mount.
96
+ mkdirSync(plan.stateDir, {recursive: true});
97
+ if (plan.fresh) {
98
+ process.stderr.write(
99
+ `anon-pi: new session home ${plan.stateDir} (seeding on first launch)\n`,
100
+ );
101
+ }
67
102
  }
68
103
 
69
- // Ensure the workdir exists (a fresh named folder is fine).
70
- mkdirSync(plan.workdir, {recursive: true});
71
-
72
104
  // Hand off to netcage with inherited stdio so -it is a real interactive TTY.
73
105
  const res = spawnSync('netcage', plan.netcageArgs, {stdio: 'inherit'});
74
106
  if (res.error) {
@@ -81,6 +113,84 @@ function main(argv: string[]): number {
81
113
  return res.status ?? 1;
82
114
  }
83
115
 
116
+ // --- anon-pi import : write the seed models.json ----------------------------
117
+ function runImport(args: string[]): number {
118
+ const force = args.includes('--force') || args.includes('-f');
119
+ const stray = args.filter(
120
+ (a) => a.startsWith('-') && a !== '--force' && a !== '-f',
121
+ );
122
+ if (stray.length > 0) {
123
+ process.stderr.write(
124
+ `anon-pi import: unknown option(s): ${stray.join(' ')}\nRun \`anon-pi --help\`.\n`,
125
+ );
126
+ return 2;
127
+ }
128
+
129
+ const env = envFromProcess(process.env);
130
+
131
+ if (!env.llmDirect || env.llmDirect.trim() === '') {
132
+ process.stderr.write(
133
+ 'anon-pi import: set ANON_PI_LLM to the RFC1918/link-local IP[:port] of the local\n' +
134
+ 'model whose provider should be imported (e.g. ANON_PI_LLM=192.168.1.150:8080).\n',
135
+ );
136
+ return 1;
137
+ }
138
+
139
+ const source = resolveSourceModelsPath(env);
140
+ if (!existsSync(source)) {
141
+ process.stderr.write(
142
+ `anon-pi import: host models.json not found at ${source}.\n` +
143
+ 'Set ANON_PI_SOURCE_MODELS to your pi models.json, or run pi once to create it.\n',
144
+ );
145
+ return 1;
146
+ }
147
+
148
+ let hostModels: PiModelsFile;
149
+ try {
150
+ hostModels = JSON.parse(readFileSync(source, 'utf8')) as PiModelsFile;
151
+ } catch (e) {
152
+ process.stderr.write(
153
+ `anon-pi import: could not parse ${source}: ${(e as Error).message}\n`,
154
+ );
155
+ return 1;
156
+ }
157
+
158
+ let result;
159
+ try {
160
+ result = pickProviderForLlm(hostModels, env.llmDirect);
161
+ } catch (e) {
162
+ if (e instanceof AnonPiError) {
163
+ process.stderr.write(e.message + '\n');
164
+ return 1;
165
+ }
166
+ throw e;
167
+ }
168
+
169
+ const seedDir = resolveConfigSeed(env);
170
+ const dest = join(seedDir, MODELS_FILE);
171
+ if (existsSync(dest) && !force) {
172
+ process.stderr.write(
173
+ `anon-pi import: ${dest} already exists. Re-run with --force to overwrite.\n`,
174
+ );
175
+ return 1;
176
+ }
177
+
178
+ if (result.apiKeyLooksReal) {
179
+ process.stderr.write(
180
+ `anon-pi import: WARNING: provider "${result.name}" carries a real-looking apiKey; it\n` +
181
+ 'will be written into the seed. For a local model this is usually fine, but review\n' +
182
+ `${dest} if that key identifies you.\n`,
183
+ );
184
+ }
185
+
186
+ mkdirSync(seedDir, {recursive: true});
187
+ writeFileSync(dest, JSON.stringify(result.models, null, 2) + '\n');
188
+ process.stderr.write(
189
+ `anon-pi import: wrote ${dest} (provider "${result.name}"). Run \`anon-pi\` to launch.\n`,
190
+ );
191
+ return 0;
192
+ }
193
+
84
194
  function hasNetcage(): boolean {
85
195
  const which = spawnSync(
86
196
  process.platform === 'win32' ? 'where' : 'command',