@arjunkhera/atlas 0.3.0 → 0.3.2
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/.claude-plugin/plugin.json +1 -1
- package/door/cli.mjs +5 -1
- package/package.json +1 -1
- package/work/mcp.mjs +47 -5
package/door/cli.mjs
CHANGED
|
@@ -51,6 +51,10 @@ Atlas ${ATLAS_VERSION}. Check version ${CHECK_VERSION}; it reads spec ${Object.k
|
|
|
51
51
|
A person merges every file the tooling writer writes.
|
|
52
52
|
`;
|
|
53
53
|
|
|
54
|
+
// A switch never takes the next word as its value, so it may come before a
|
|
55
|
+
// path: `atlas ste --share page.md` reads page.md (a miss on 6 October).
|
|
56
|
+
const SWITCHES = new Set(['help', 'share', 'replace-unknown', 'workflow', 'dry-run', 'local', 'skip-global', 'yes']);
|
|
57
|
+
|
|
54
58
|
function options(argv) {
|
|
55
59
|
const out = { _: [] };
|
|
56
60
|
for (let index = 0; index < argv.length; index += 1) {
|
|
@@ -58,7 +62,7 @@ function options(argv) {
|
|
|
58
62
|
if (!flag.startsWith('--')) { out._.push(flag); continue; }
|
|
59
63
|
const key = flag.slice(2);
|
|
60
64
|
const value = argv[index + 1];
|
|
61
|
-
if (value === undefined || value.startsWith('--')) { out[key] = true; continue; }
|
|
65
|
+
if (SWITCHES.has(key) || value === undefined || value.startsWith('--')) { out[key] = true; continue; }
|
|
62
66
|
out[key] = value;
|
|
63
67
|
index += 1;
|
|
64
68
|
}
|
package/package.json
CHANGED
package/work/mcp.mjs
CHANGED
|
@@ -6,11 +6,14 @@
|
|
|
6
6
|
// Row F11 (amendment A1.9): the server ships in the Atlas plugin. Claude Code
|
|
7
7
|
// starts it from the plugin's .mcp.json, which sets every variable it reads.
|
|
8
8
|
// The Engram address and key arrive together, as one setting, in
|
|
9
|
-
// ATLAS_ENGRAM.
|
|
9
|
+
// ATLAS_ENGRAM. When that setting arrives empty, the server reads the same
|
|
10
|
+
// value from one owner-only file, ~/.config/atlas/engram-connection (atlas#101:
|
|
11
|
+
// Claude Desktop starts the server with the sensitive setting empty). It reads
|
|
12
|
+
// them from nowhere else.
|
|
10
13
|
import { execFileSync } from 'node:child_process';
|
|
11
14
|
import { randomUUID } from 'node:crypto';
|
|
12
15
|
import { resolve, isAbsolute, dirname, join, delimiter } from 'node:path';
|
|
13
|
-
import { existsSync, realpathSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
16
|
+
import { existsSync, realpathSync, readFileSync, writeFileSync, mkdirSync, statSync, openSync, fstatSync, closeSync, constants as fsConstants } from 'node:fs';
|
|
14
17
|
import { fileURLToPath } from 'node:url';
|
|
15
18
|
import { createEngramClient } from './lib/client.mjs';
|
|
16
19
|
import { createActionStore } from './lib/action-store.mjs';
|
|
@@ -70,9 +73,48 @@ export const TOOLS = [
|
|
|
70
73
|
// It never throws. A missing, malformed or expired connection is a state, and
|
|
71
74
|
// the server still starts and lists its tools; each tool then answers with the
|
|
72
75
|
// sentence below (fix six of the F11 study).
|
|
73
|
-
|
|
76
|
+
//
|
|
77
|
+
// The setting wins. The file is read only when the setting is empty, and only
|
|
78
|
+
// when it is a plain file that the owner owns and no one else can read. The
|
|
79
|
+
// server opens it once with O_NOFOLLOW and checks that open file, so no swap
|
|
80
|
+
// can slip in between the check and the read. A read error is a refusal, never
|
|
81
|
+
// a throw.
|
|
82
|
+
const CONNECTION_HELP = 'Set it in the Atlas plugin settings, or in the file ~/.config/atlas/engram-connection, as "https://<host> <key>".';
|
|
83
|
+
const CONNECTION_FILE_LIMIT = 16 * 1024;
|
|
84
|
+
export function connectionFile(env) { return env.HOME ? join(env.HOME, '.config', 'atlas', 'engram-connection') : null; }
|
|
85
|
+
function readConnectionFile(env) {
|
|
86
|
+
const path = connectionFile(env);
|
|
87
|
+
if (!path) return { raw: '' };
|
|
88
|
+
const owner = typeof process.getuid === 'function' ? process.getuid() : null;
|
|
89
|
+
let fd;
|
|
90
|
+
try {
|
|
91
|
+
const folder = statSync(dirname(path), { throwIfNoEntry: false });
|
|
92
|
+
if (!folder) return { raw: '' };
|
|
93
|
+
if ((owner !== null && folder.uid !== owner) || folder.mode & 0o022) return { refused: `Other users can change ${dirname(path)}. Run: chmod 700 ${dirname(path)}` };
|
|
94
|
+
try { fd = openSync(path, fsConstants.O_RDONLY | fsConstants.O_NOFOLLOW | fsConstants.O_NONBLOCK); } catch (error) {
|
|
95
|
+
if (error.code === 'ENOENT') return { raw: '' };
|
|
96
|
+
if (error.code === 'ELOOP') return { refused: `${path} is not a plain file.` };
|
|
97
|
+
throw error;
|
|
98
|
+
}
|
|
99
|
+
const info = fstatSync(fd);
|
|
100
|
+
if (!info.isFile()) return { refused: `${path} is not a plain file.` };
|
|
101
|
+
if (owner !== null && info.uid !== owner) return { refused: `${path} belongs to another user.` };
|
|
102
|
+
if (info.mode & 0o077) return { refused: `Other users can read ${path}. Run: chmod 600 ${path}` };
|
|
103
|
+
if (info.size > CONNECTION_FILE_LIMIT) return { refused: `${path} is too large to be a connection line.` };
|
|
104
|
+
return { raw: readFileSync(fd, 'utf8').trim() };
|
|
105
|
+
} catch (error) {
|
|
106
|
+
return { refused: `Atlas could not read ${path} (${error.code ?? 'error'}).` };
|
|
107
|
+
} finally {
|
|
108
|
+
if (fd !== undefined) closeSync(fd);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
74
111
|
export function readConnection(env, { allowLoopbackHttp = false, now = Date.now() } = {}) {
|
|
75
|
-
|
|
112
|
+
let raw = unset(env.ATLAS_ENGRAM) ? '' : String(env.ATLAS_ENGRAM).trim();
|
|
113
|
+
if (!raw) {
|
|
114
|
+
const file = readConnectionFile(env);
|
|
115
|
+
if (file.refused) return { state: 'bad', message: `The Engram connection file is refused. ${file.refused}` };
|
|
116
|
+
raw = file.raw;
|
|
117
|
+
}
|
|
76
118
|
if (!raw) return { state: 'missing', message: `No Engram connection is set. ${CONNECTION_HELP}` };
|
|
77
119
|
const parts = raw.split(/\s+/);
|
|
78
120
|
if (parts.length !== 2) return { state: 'bad', message: `The Engram connection is not in the form "https://<host> <key>". ${CONNECTION_HELP}` };
|
|
@@ -87,7 +129,7 @@ export function readConnection(env, { allowLoopbackHttp = false, now = Date.now(
|
|
|
87
129
|
if (claims.principal_type !== 'user' || !claims.jti || !claims.exp) return { state: 'bad', message: `The Engram key must be a user token with a token id. ${CONNECTION_HELP}` };
|
|
88
130
|
if (claims.exp <= now / 1000) {
|
|
89
131
|
const day = new Date(claims.exp * 1000).toISOString().slice(0, 10);
|
|
90
|
-
return { state: 'expired', message: `The Engram key expired on ${day}. Enter a new one in the Atlas plugin settings.`, expired: day };
|
|
132
|
+
return { state: 'expired', message: `The Engram key expired on ${day}. Enter a new one in the Atlas plugin settings, or in ~/.config/atlas/engram-connection.`, expired: day };
|
|
91
133
|
}
|
|
92
134
|
return { state: 'ok', url: url.origin, token, tokenId: claims.jti, expires: new Date(claims.exp * 1000).toISOString().slice(0, 10) };
|
|
93
135
|
}
|