@drkmttr/adhere 0.15.0 → 0.17.5
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 +273 -130
- package/bin/adhere.js +19 -4
- package/package.json +34 -12
- package/skills/adhere/SKILL.md +180 -31
- package/skills/adhere-setup/SKILL.md +142 -16
- package/src/cli.ts +56 -23
- package/src/config.ts +49 -16
- package/src/index.ts +14 -0
- package/src/markdown.ts +9 -5
- package/src/providers/cloudflare.ts +84 -0
- package/src/providers/jev.ts +66 -0
- package/src/providers/openai.ts +89 -0
- package/src/providers/post.ts +53 -0
- package/src/providers/provider.ts +131 -0
- package/src/providers/providers.test.ts +276 -0
- package/src/providers/systemOne.ts +95 -0
- package/src/services/AdhereConfig.ts +2 -4
- package/src/services/Credentials.ts +101 -51
- package/src/services/Jev.http.ts +165 -170
- package/src/services/Jev.provider.test.ts +207 -0
- package/src/services/Jev.reads.test.ts +439 -0
- package/src/services/Jev.ts +205 -113
- package/src/services/NativeLsp.ts +289 -0
- package/src/services/SourceReads.test.ts +1040 -0
- package/src/services/SourceReads.ts +437 -0
- package/src/services/SourceWalker.ts +26 -59
- package/src/workflows/audit.ts +440 -265
- package/src/workflows/format.ts +74 -17
- package/src/workspaces.test.ts +73 -0
- package/src/workspaces.ts +91 -0
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { SavedKey } from "#providers/provider.ts";
|
|
1
2
|
import { Config, Context, Effect, FileSystem, Layer, Option, Path, Redacted, Schema } from "effect";
|
|
2
3
|
|
|
3
4
|
/** No key to send, or the saved key cannot be read or written. */
|
|
@@ -6,37 +7,67 @@ export class CredentialsUnavailable extends Schema.TaggedError<CredentialsUnavai
|
|
|
6
7
|
{ message: Schema.String },
|
|
7
8
|
) {}
|
|
8
9
|
|
|
9
|
-
/**
|
|
10
|
+
/** An API key is one word: a space or a second line is a bad paste. */
|
|
10
11
|
const ApiKey = Schema.String.pipe(Schema.check(Schema.isPattern(/^\S+$/)));
|
|
11
12
|
|
|
12
|
-
/** What `adhere login` saves. */
|
|
13
|
+
/** What `adhere login` saves: TypeSafe AI's key, and with `--openai`, OpenAI's. */
|
|
13
14
|
class SavedCredentials extends Schema.Class<SavedCredentials>("SavedCredentials")({
|
|
14
|
-
apiKey: Schema.RedactedFromValue(ApiKey),
|
|
15
|
+
apiKey: Schema.optionalKey(Schema.RedactedFromValue(ApiKey)),
|
|
16
|
+
openaiApiKey: Schema.optionalKey(Schema.RedactedFromValue(ApiKey)),
|
|
15
17
|
}) {}
|
|
16
18
|
|
|
17
19
|
const SavedCredentialsJson = Schema.fromJsonString(SavedCredentials);
|
|
18
20
|
|
|
19
|
-
/** The
|
|
21
|
+
/** The saved keys, by the name a provider asks for them by. */
|
|
22
|
+
type Saved = Partial<Record<SavedKey, Redacted.Redacted<string>>>;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Each key: the variable that wins over the saved one, how `adhere login`
|
|
26
|
+
* asks for it and names it, and what a run says when there is none, or when
|
|
27
|
+
* the file holds none.
|
|
28
|
+
*/
|
|
29
|
+
export const KEYS = {
|
|
30
|
+
typesafe: {
|
|
31
|
+
variable: "TYPESAFE_API_KEY",
|
|
32
|
+
label: "TypeSafe AI API key",
|
|
33
|
+
said: "API key",
|
|
34
|
+
missing: "No TypeSafe AI API key. Run `adhere login` to save one, or set TYPESAFE_API_KEY.",
|
|
35
|
+
absent: (file: string) => `${file} holds no API key. Run \`adhere login\` to save one again.`,
|
|
36
|
+
},
|
|
37
|
+
openai: {
|
|
38
|
+
variable: "OPENAI_API_KEY",
|
|
39
|
+
label: "OpenAI API key",
|
|
40
|
+
said: "OpenAI API key",
|
|
41
|
+
missing: "No OpenAI API key. Run `adhere login --openai` to save one, or set OPENAI_API_KEY.",
|
|
42
|
+
absent: (file: string) =>
|
|
43
|
+
`${file} holds no OpenAI API key. Run \`adhere login --openai\` to save one.`,
|
|
44
|
+
},
|
|
45
|
+
} as const satisfies Record<SavedKey, unknown>;
|
|
46
|
+
|
|
47
|
+
/** The API keys providers ask with: where a request gets one, and where `adhere login` keeps them. */
|
|
20
48
|
export class Credentials extends Context.Service<
|
|
21
49
|
Credentials,
|
|
22
50
|
{
|
|
23
|
-
/** `
|
|
24
|
-
readonly
|
|
25
|
-
|
|
51
|
+
/** `name`'s key: its variable, such as `TYPESAFE_API_KEY`, when set, otherwise the one `adhere login` saved. */
|
|
52
|
+
readonly key: (
|
|
53
|
+
name: SavedKey,
|
|
54
|
+
) => Effect.Effect<Redacted.Redacted<string>, CredentialsUnavailable>;
|
|
55
|
+
/** Where the saved keys live, whether or not there are any. */
|
|
26
56
|
readonly file: Effect.Effect<string, CredentialsUnavailable>;
|
|
27
|
-
/** Saves
|
|
57
|
+
/** Saves `name`'s key for later runs, beside the other, readable only by the user. */
|
|
28
58
|
readonly save: (
|
|
59
|
+
name: SavedKey,
|
|
29
60
|
apiKey: Redacted.Redacted<string>,
|
|
30
61
|
) => Effect.Effect<void, CredentialsUnavailable>;
|
|
31
|
-
/** Deletes
|
|
32
|
-
readonly remove: Effect.Effect<boolean, CredentialsUnavailable>;
|
|
62
|
+
/** Deletes `name`'s saved key, and the file with no key left in it. False when there was none. */
|
|
63
|
+
readonly remove: (name: SavedKey) => Effect.Effect<boolean, CredentialsUnavailable>;
|
|
33
64
|
}
|
|
34
65
|
>()("@drkmttr/adhere/services/Credentials") {}
|
|
35
66
|
|
|
36
67
|
const refused = (message: string) => CredentialsUnavailable.make({ message });
|
|
37
68
|
|
|
38
69
|
/**
|
|
39
|
-
* The
|
|
70
|
+
* The keys live in `$XDG_CONFIG_HOME/adhere/credentials.json`, or under
|
|
40
71
|
* `~/.config` without it, on every platform. The file is written whole to a
|
|
41
72
|
* temporary file created readable only by the user, then renamed over the
|
|
42
73
|
* old one, so no reader sees half a key or a key with looser permissions.
|
|
@@ -59,65 +90,84 @@ export const CredentialsLive = Layer.effect(Credentials)(
|
|
|
59
90
|
),
|
|
60
91
|
);
|
|
61
92
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
const
|
|
93
|
+
/**
|
|
94
|
+
* The keys the file holds, or nothing when there is no file. A file that
|
|
95
|
+
* does not read as keys refuses, so nothing writes over what it holds.
|
|
96
|
+
*/
|
|
97
|
+
const savedIn = Effect.fn("Credentials.savedIn")(function* (target: string) {
|
|
98
|
+
const text = yield* Effect.gen(function* () {
|
|
99
|
+
return (yield* fs.exists(target)) ? yield* fs.readFileString(target) : undefined;
|
|
100
|
+
}).pipe(Effect.mapError((problem) => refused(`Cannot read ${target}: ${problem.message}`)));
|
|
101
|
+
if (text === undefined) return undefined;
|
|
102
|
+
const { apiKey, openaiApiKey } = yield* Schema.decodeUnknownEffect(SavedCredentialsJson)(
|
|
103
|
+
text,
|
|
104
|
+
).pipe(
|
|
72
105
|
Effect.mapError(() =>
|
|
73
|
-
refused(
|
|
106
|
+
refused(
|
|
107
|
+
`${target} does not hold keys as \`adhere login\` saves them. Fix it, or delete it and run \`adhere login\` again.`,
|
|
108
|
+
),
|
|
74
109
|
),
|
|
75
110
|
);
|
|
76
|
-
return
|
|
111
|
+
return {
|
|
112
|
+
...(apiKey === undefined ? {} : { typesafe: apiKey }),
|
|
113
|
+
...(openaiApiKey === undefined ? {} : { openai: openaiApiKey }),
|
|
114
|
+
} satisfies Saved;
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
/** Writes the keys whole, as the layer's comment says; the file goes when none is left. */
|
|
118
|
+
const write = Effect.fn("Credentials.write")(function* (target: string, saved: Saved) {
|
|
119
|
+
const temporary = `${target}.tmp`;
|
|
120
|
+
const json = yield* Schema.encodeEffect(SavedCredentialsJson)(
|
|
121
|
+
new SavedCredentials({
|
|
122
|
+
...(saved.typesafe === undefined ? {} : { apiKey: saved.typesafe }),
|
|
123
|
+
...(saved.openai === undefined ? {} : { openaiApiKey: saved.openai }),
|
|
124
|
+
}),
|
|
125
|
+
).pipe(Effect.orDie);
|
|
126
|
+
yield* Effect.gen(function* () {
|
|
127
|
+
if (saved.typesafe === undefined && saved.openai === undefined) {
|
|
128
|
+
return yield* fs.remove(target, { force: true });
|
|
129
|
+
}
|
|
130
|
+
yield* fs.makeDirectory(path.dirname(target), { recursive: true, mode: 0o700 });
|
|
131
|
+
// A mode applies only when a file is created: never reuse a leftover.
|
|
132
|
+
yield* fs.remove(temporary, { force: true });
|
|
133
|
+
yield* fs.writeFileString(temporary, `${json}\n`, { mode: 0o600 });
|
|
134
|
+
yield* fs.rename(temporary, target);
|
|
135
|
+
}).pipe(Effect.mapError((problem) => refused(`Cannot save ${target}: ${problem.message}`)));
|
|
77
136
|
});
|
|
78
137
|
|
|
79
138
|
// Read when a request needs the key, not before: a cached run needs none.
|
|
80
|
-
const
|
|
81
|
-
const
|
|
82
|
-
|
|
139
|
+
const key = Effect.fn("Credentials.key")(function* (name: SavedKey) {
|
|
140
|
+
const { variable, missing, absent } = KEYS[name];
|
|
141
|
+
const fromEnvironment = yield* Config.option(Config.Redacted(variable)).pipe(
|
|
142
|
+
Effect.mapError((problem) => refused(`Cannot read ${variable}: ${problem.message}`)),
|
|
83
143
|
);
|
|
84
144
|
if (Option.isSome(fromEnvironment)) return fromEnvironment.value;
|
|
85
145
|
const target = yield* file;
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
);
|
|
90
|
-
}
|
|
91
|
-
return yield* readSaved(target);
|
|
146
|
+
const saved = yield* savedIn(target);
|
|
147
|
+
if (saved === undefined) return yield* refused(missing);
|
|
148
|
+
return saved[name] ?? (yield* refused(absent(target)));
|
|
92
149
|
});
|
|
93
150
|
|
|
94
|
-
const save = Effect.fn("Credentials.save")(function* (
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
151
|
+
const save = Effect.fn("Credentials.save")(function* (
|
|
152
|
+
name: SavedKey,
|
|
153
|
+
apiKey: Redacted.Redacted<string>,
|
|
154
|
+
) {
|
|
155
|
+
yield* Schema.decodeUnknownEffect(ApiKey)(Redacted.value(apiKey)).pipe(
|
|
98
156
|
Effect.mapError(() => refused("An API key is one word, with no spaces or line breaks.")),
|
|
99
157
|
);
|
|
100
|
-
const json = yield* Schema.encodeEffect(SavedCredentialsJson)(saved).pipe(Effect.orDie);
|
|
101
158
|
const target = yield* file;
|
|
102
|
-
|
|
103
|
-
yield* Effect.gen(function* () {
|
|
104
|
-
yield* fs.makeDirectory(path.dirname(target), { recursive: true, mode: 0o700 });
|
|
105
|
-
// A mode applies only when a file is created: never reuse a leftover.
|
|
106
|
-
yield* fs.remove(temporary, { force: true });
|
|
107
|
-
yield* fs.writeFileString(temporary, `${json}\n`, { mode: 0o600 });
|
|
108
|
-
yield* fs.rename(temporary, target);
|
|
109
|
-
}).pipe(Effect.mapError((problem) => refused(`Cannot save ${target}: ${problem.message}`)));
|
|
159
|
+
yield* write(target, { ...(yield* savedIn(target)), [name]: apiKey });
|
|
110
160
|
});
|
|
111
161
|
|
|
112
|
-
const remove = Effect.fn("Credentials.remove")(function* () {
|
|
162
|
+
const remove = Effect.fn("Credentials.remove")(function* (name: SavedKey) {
|
|
113
163
|
const target = yield* file;
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
164
|
+
const rest: Saved = { ...(yield* savedIn(target)) };
|
|
165
|
+
if (rest[name] === undefined) return false;
|
|
166
|
+
delete rest[name];
|
|
167
|
+
yield* write(target, rest);
|
|
118
168
|
return true;
|
|
119
169
|
});
|
|
120
170
|
|
|
121
|
-
return Credentials.of({
|
|
171
|
+
return Credentials.of({ key, file, save, remove });
|
|
122
172
|
}),
|
|
123
173
|
);
|