akm-cli 0.0.22 → 0.0.23
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/dist/cli.js +41 -66
- package/dist/config-cli.js +0 -10
- package/dist/config.js +2 -90
- package/dist/stash-provider-factory.js +8 -23
- package/dist/stash-source-manage.js +82 -0
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -17,7 +17,7 @@ import { agentikitAdd } from "./stash-add";
|
|
|
17
17
|
import { agentikitClone } from "./stash-clone";
|
|
18
18
|
import { agentikitSearch, parseSearchSource } from "./stash-search";
|
|
19
19
|
import { agentikitShowUnified } from "./stash-show";
|
|
20
|
-
import {
|
|
20
|
+
import { addStashSource, listStashSources, removeStashSource } from "./stash-source-manage";
|
|
21
21
|
import { setQuiet, warn } from "./warn";
|
|
22
22
|
// Version: prefer compile-time define, then package.json, then fallback
|
|
23
23
|
const pkgVersion = (() => {
|
|
@@ -814,23 +814,17 @@ const registryCommand = defineCommand({
|
|
|
814
814
|
}),
|
|
815
815
|
},
|
|
816
816
|
});
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
817
|
+
/**
|
|
818
|
+
* Shared subcommand definitions for stash source management.
|
|
819
|
+
* Used by both `akm stash` (preferred) and `akm sources` (legacy alias).
|
|
820
|
+
*/
|
|
821
|
+
function buildSourceSubCommands(outputPrefix) {
|
|
822
|
+
return {
|
|
820
823
|
list: defineCommand({
|
|
821
824
|
meta: { name: "list", description: "List all stash sources" },
|
|
822
825
|
run() {
|
|
823
826
|
return runWithJsonErrors(() => {
|
|
824
|
-
|
|
825
|
-
const localSources = resolveStashSources();
|
|
826
|
-
const stashes = config.stashes ?? [];
|
|
827
|
-
// Legacy fallback: show remoteStashSources if no stashes config
|
|
828
|
-
const legacyRemote = !config.stashes ? (config.remoteStashSources ?? []) : [];
|
|
829
|
-
output("sources", {
|
|
830
|
-
localSources,
|
|
831
|
-
stashes,
|
|
832
|
-
...(legacyRemote.length > 0 ? { remoteSources: legacyRemote } : {}),
|
|
833
|
-
});
|
|
827
|
+
output(`${outputPrefix}`, listStashSources());
|
|
834
828
|
});
|
|
835
829
|
},
|
|
836
830
|
}),
|
|
@@ -844,50 +838,31 @@ const sourcesCommand = defineCommand({
|
|
|
844
838
|
},
|
|
845
839
|
run({ args }) {
|
|
846
840
|
return runWithJsonErrors(() => {
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
throw new UsageError("--provider is required for URL sources (e.g. --provider openviking)");
|
|
857
|
-
}
|
|
858
|
-
if (stashes.some((s) => s.url === args.target)) {
|
|
859
|
-
output("sources-add", { stashes, added: false, message: "Source URL already configured" });
|
|
860
|
-
return;
|
|
861
|
-
}
|
|
862
|
-
const entry = { type: providerType, url: args.target };
|
|
863
|
-
if (args.name)
|
|
864
|
-
entry.name = args.name;
|
|
865
|
-
if (args.options) {
|
|
866
|
-
try {
|
|
867
|
-
entry.options = JSON.parse(args.options);
|
|
868
|
-
}
|
|
869
|
-
catch {
|
|
870
|
-
throw new UsageError("--options must be valid JSON");
|
|
841
|
+
if (args.target.startsWith("http://")) {
|
|
842
|
+
warn("Warning: source URL uses plain HTTP (not HTTPS). For security, prefer https:// to protect against eavesdropping and tampering.");
|
|
843
|
+
}
|
|
844
|
+
let parsedOptions;
|
|
845
|
+
if (args.options) {
|
|
846
|
+
try {
|
|
847
|
+
const parsed = JSON.parse(args.options);
|
|
848
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
849
|
+
throw new UsageError("--options must be a JSON object");
|
|
871
850
|
}
|
|
851
|
+
parsedOptions = parsed;
|
|
872
852
|
}
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
const resolvedPath = path.resolve(args.target);
|
|
878
|
-
if (stashes.some((s) => s.path === resolvedPath)) {
|
|
879
|
-
output("sources-add", { stashes, added: false, message: "Source path already configured" });
|
|
880
|
-
return;
|
|
853
|
+
catch (err) {
|
|
854
|
+
if (err instanceof UsageError)
|
|
855
|
+
throw err;
|
|
856
|
+
throw new UsageError("--options must be valid JSON");
|
|
881
857
|
}
|
|
882
|
-
const entry = { type: "filesystem", path: resolvedPath };
|
|
883
|
-
if (args.name)
|
|
884
|
-
entry.name = args.name;
|
|
885
|
-
stashes.push(entry);
|
|
886
858
|
}
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
859
|
+
const result = addStashSource({
|
|
860
|
+
target: args.target,
|
|
861
|
+
name: args.name,
|
|
862
|
+
providerType: args.provider,
|
|
863
|
+
options: parsedOptions,
|
|
864
|
+
});
|
|
865
|
+
output(`${outputPrefix}-add`, result);
|
|
891
866
|
});
|
|
892
867
|
},
|
|
893
868
|
}),
|
|
@@ -898,21 +873,20 @@ const sourcesCommand = defineCommand({
|
|
|
898
873
|
},
|
|
899
874
|
run({ args }) {
|
|
900
875
|
return runWithJsonErrors(() => {
|
|
901
|
-
const
|
|
902
|
-
|
|
903
|
-
const resolvedTarget = args.target.startsWith("http") ? args.target : path.resolve(args.target);
|
|
904
|
-
const idx = stashes.findIndex((s) => s.url === resolvedTarget || s.path === resolvedTarget || s.name === resolvedTarget);
|
|
905
|
-
if (idx === -1) {
|
|
906
|
-
output("sources-remove", { stashes, removed: false, message: "No matching source found" });
|
|
907
|
-
return;
|
|
908
|
-
}
|
|
909
|
-
const removed = stashes.splice(idx, 1)[0];
|
|
910
|
-
saveConfig({ ...config, stashes });
|
|
911
|
-
output("sources-remove", { stashes, removed: true, entry: removed });
|
|
876
|
+
const result = removeStashSource(args.target);
|
|
877
|
+
output(`${outputPrefix}-remove`, result);
|
|
912
878
|
});
|
|
913
879
|
},
|
|
914
880
|
}),
|
|
915
|
-
}
|
|
881
|
+
};
|
|
882
|
+
}
|
|
883
|
+
const stashCommand = defineCommand({
|
|
884
|
+
meta: { name: "stash", description: "Manage stash sources (local paths and remote providers)" },
|
|
885
|
+
subCommands: buildSourceSubCommands("stash"),
|
|
886
|
+
});
|
|
887
|
+
const sourcesCommand = defineCommand({
|
|
888
|
+
meta: { name: "sources", description: "Manage stash sources (alias for 'akm stash')" },
|
|
889
|
+
subCommands: buildSourceSubCommands("sources"),
|
|
916
890
|
});
|
|
917
891
|
const hintsCommand = defineCommand({
|
|
918
892
|
meta: {
|
|
@@ -949,6 +923,7 @@ const main = defineCommand({
|
|
|
949
923
|
search: searchCommand,
|
|
950
924
|
show: showCommand,
|
|
951
925
|
clone: cloneCommand,
|
|
926
|
+
stash: stashCommand,
|
|
952
927
|
sources: sourcesCommand,
|
|
953
928
|
registry: registryCommand,
|
|
954
929
|
config: configCommand,
|
package/dist/config-cli.js
CHANGED
|
@@ -25,8 +25,6 @@ export function parseConfigValue(key, value) {
|
|
|
25
25
|
return { llm: parseLlmConnectionValue(value) };
|
|
26
26
|
case "registries":
|
|
27
27
|
return { registries: parseRegistriesValue(value) };
|
|
28
|
-
case "remoteStashSources":
|
|
29
|
-
return { remoteStashSources: parseStashesValue(value) };
|
|
30
28
|
case "stashes":
|
|
31
29
|
return { stashes: parseStashesValue(value) };
|
|
32
30
|
case "output.format":
|
|
@@ -51,8 +49,6 @@ export function getConfigValue(config, key) {
|
|
|
51
49
|
return config.llm ?? null;
|
|
52
50
|
case "registries":
|
|
53
51
|
return config.registries ?? DEFAULT_CONFIG.registries ?? [];
|
|
54
|
-
case "remoteStashSources":
|
|
55
|
-
return config.remoteStashSources ?? [];
|
|
56
52
|
case "stashes":
|
|
57
53
|
return config.stashes ?? [];
|
|
58
54
|
case "output.format":
|
|
@@ -71,7 +67,6 @@ export function setConfigValue(config, key, rawValue) {
|
|
|
71
67
|
case "embedding":
|
|
72
68
|
case "llm":
|
|
73
69
|
case "registries":
|
|
74
|
-
case "remoteStashSources":
|
|
75
70
|
case "stashes":
|
|
76
71
|
case "output.format":
|
|
77
72
|
case "output.detail":
|
|
@@ -90,8 +85,6 @@ export function unsetConfigValue(config, key) {
|
|
|
90
85
|
return { ...config, llm: undefined };
|
|
91
86
|
case "registries":
|
|
92
87
|
return { ...config, registries: undefined };
|
|
93
|
-
case "remoteStashSources":
|
|
94
|
-
return { ...config, remoteStashSources: undefined };
|
|
95
88
|
case "stashes":
|
|
96
89
|
return { ...config, stashes: undefined };
|
|
97
90
|
case "output.format":
|
|
@@ -115,11 +108,8 @@ export function listConfig(config) {
|
|
|
115
108
|
result.embedding = config.embedding;
|
|
116
109
|
if (config.llm)
|
|
117
110
|
result.llm = config.llm;
|
|
118
|
-
// Show legacy keys only if they still have content
|
|
119
111
|
if (config.searchPaths?.length)
|
|
120
112
|
result.searchPaths = config.searchPaths;
|
|
121
|
-
if (config.remoteStashSources?.length)
|
|
122
|
-
result.remoteStashSources = config.remoteStashSources;
|
|
123
113
|
return result;
|
|
124
114
|
}
|
|
125
115
|
function mergeConfigValue(config, partial) {
|
package/dist/config.js
CHANGED
|
@@ -23,7 +23,7 @@ export function getConfigPath() {
|
|
|
23
23
|
}
|
|
24
24
|
// ── Load / Save / Update ────────────────────────────────────────────────────
|
|
25
25
|
let cachedConfig;
|
|
26
|
-
export function loadConfig(
|
|
26
|
+
export function loadConfig() {
|
|
27
27
|
const configPath = getConfigPath();
|
|
28
28
|
let stat;
|
|
29
29
|
try {
|
|
@@ -51,94 +51,11 @@ export function loadConfig(opts) {
|
|
|
51
51
|
if (envKey)
|
|
52
52
|
config.llm.apiKey = envKey;
|
|
53
53
|
}
|
|
54
|
-
if (!opts?.readOnly) {
|
|
55
|
-
// Migrate installed[source: "local"] → stashes[type: "filesystem"]
|
|
56
|
-
try {
|
|
57
|
-
migrateLocalInstalledToStashes(config);
|
|
58
|
-
}
|
|
59
|
-
catch (err) {
|
|
60
|
-
console.warn("[agentikit] Warning: config migration (local→stashes) failed:", err instanceof Error ? err.message : String(err));
|
|
61
|
-
}
|
|
62
|
-
// Migrate remoteStashSources → stashes[]
|
|
63
|
-
try {
|
|
64
|
-
migrateRemoteStashSourcesToStashes(config);
|
|
65
|
-
}
|
|
66
|
-
catch (err) {
|
|
67
|
-
console.warn("[agentikit] Warning: config migration (remoteStashSources→stashes) failed:", err instanceof Error ? err.message : String(err));
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
54
|
// Cache the parsed config with its path and mtime for subsequent calls.
|
|
71
55
|
// Reuse the stat already obtained above (avoids a second syscall + TOCTOU gap).
|
|
72
56
|
cachedConfig = { config, path: configPath, mtime: stat.mtimeMs };
|
|
73
57
|
return config;
|
|
74
58
|
}
|
|
75
|
-
/**
|
|
76
|
-
* Migrate installed entries with source "local" to stashes[] as filesystem entries.
|
|
77
|
-
* Local directories are search paths, not registry kits — they don't need version
|
|
78
|
-
* tracking, cache management, or update support.
|
|
79
|
-
*
|
|
80
|
-
* Mutates the config in place and persists to disk if any entries are migrated.
|
|
81
|
-
*/
|
|
82
|
-
function migrateLocalInstalledToStashes(config) {
|
|
83
|
-
const installed = config.installed;
|
|
84
|
-
if (!installed)
|
|
85
|
-
return;
|
|
86
|
-
const localEntries = installed.filter((e) => e.source === "local");
|
|
87
|
-
if (localEntries.length === 0)
|
|
88
|
-
return;
|
|
89
|
-
const stashes = [...(config.stashes ?? [])];
|
|
90
|
-
const existingPaths = new Set(stashes.filter((s) => !!s.path).map((s) => path.resolve(s.path)));
|
|
91
|
-
let migrated = 0;
|
|
92
|
-
for (const entry of localEntries) {
|
|
93
|
-
const resolved = path.resolve(entry.stashRoot);
|
|
94
|
-
if (existingPaths.has(resolved))
|
|
95
|
-
continue;
|
|
96
|
-
stashes.push({
|
|
97
|
-
type: "filesystem",
|
|
98
|
-
path: resolved,
|
|
99
|
-
name: entry.id,
|
|
100
|
-
});
|
|
101
|
-
existingPaths.add(resolved);
|
|
102
|
-
migrated++;
|
|
103
|
-
}
|
|
104
|
-
if (migrated === 0)
|
|
105
|
-
return;
|
|
106
|
-
// Remove local entries from installed, add to stashes
|
|
107
|
-
config.installed = installed.filter((e) => e.source !== "local");
|
|
108
|
-
config.stashes = stashes;
|
|
109
|
-
saveConfig(config);
|
|
110
|
-
}
|
|
111
|
-
/**
|
|
112
|
-
* Migrate remoteStashSources[] to stashes[] entries.
|
|
113
|
-
* Each remote source becomes a typed stash entry (e.g. type: "openviking").
|
|
114
|
-
*
|
|
115
|
-
* Mutates the config in place and persists to disk if any entries are migrated.
|
|
116
|
-
*/
|
|
117
|
-
function migrateRemoteStashSourcesToStashes(config) {
|
|
118
|
-
const remoteSources = config.remoteStashSources;
|
|
119
|
-
if (!remoteSources || remoteSources.length === 0)
|
|
120
|
-
return;
|
|
121
|
-
const stashes = [...(config.stashes ?? [])];
|
|
122
|
-
const existingUrls = new Set(stashes.filter((s) => !!s.url).map((s) => s.url));
|
|
123
|
-
let migrated = 0;
|
|
124
|
-
for (const entry of remoteSources) {
|
|
125
|
-
if (!entry.url || existingUrls.has(entry.url))
|
|
126
|
-
continue;
|
|
127
|
-
stashes.push({
|
|
128
|
-
type: entry.type ?? "openviking",
|
|
129
|
-
url: entry.url,
|
|
130
|
-
name: entry.name,
|
|
131
|
-
options: entry.options,
|
|
132
|
-
});
|
|
133
|
-
existingUrls.add(entry.url);
|
|
134
|
-
migrated++;
|
|
135
|
-
}
|
|
136
|
-
if (migrated === 0)
|
|
137
|
-
return;
|
|
138
|
-
config.stashes = stashes;
|
|
139
|
-
config.remoteStashSources = undefined;
|
|
140
|
-
saveConfig(config);
|
|
141
|
-
}
|
|
142
59
|
export function saveConfig(config) {
|
|
143
60
|
cachedConfig = undefined;
|
|
144
61
|
const configPath = getConfigPath();
|
|
@@ -175,11 +92,9 @@ function sanitizeConfigForWrite(config) {
|
|
|
175
92
|
const { apiKey, ...rest } = config.llm;
|
|
176
93
|
sanitized.llm = rest;
|
|
177
94
|
}
|
|
178
|
-
// Drop empty
|
|
95
|
+
// Drop empty keys to keep config clean
|
|
179
96
|
if (!config.searchPaths?.length)
|
|
180
97
|
delete sanitized.searchPaths;
|
|
181
|
-
if (!config.remoteStashSources?.length)
|
|
182
|
-
delete sanitized.remoteStashSources;
|
|
183
98
|
return sanitized;
|
|
184
99
|
}
|
|
185
100
|
export function updateConfig(partial) {
|
|
@@ -225,9 +140,6 @@ function pickKnownKeys(raw) {
|
|
|
225
140
|
const registries = parseRegistriesConfig(raw.registries);
|
|
226
141
|
if (registries)
|
|
227
142
|
config.registries = registries;
|
|
228
|
-
const remoteStash = parseStashesConfig(raw.remoteStashSources);
|
|
229
|
-
if (remoteStash)
|
|
230
|
-
config.remoteStashSources = remoteStash;
|
|
231
143
|
const stashes = parseStashesConfig(raw.stashes);
|
|
232
144
|
if (stashes)
|
|
233
145
|
config.stashes = stashes;
|
|
@@ -19,33 +19,18 @@ export function resolveStashProviderFactory(type) {
|
|
|
19
19
|
}
|
|
20
20
|
/**
|
|
21
21
|
* Resolve all non-filesystem stash providers from config.
|
|
22
|
-
* Sources come from `stashes` (new) or `remoteStashSources` (legacy).
|
|
23
22
|
* Filesystem entries are excluded — they are handled by resolveStashSources().
|
|
24
23
|
*/
|
|
25
24
|
export function resolveStashProviders(config) {
|
|
26
25
|
const providers = [];
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
if (factory) {
|
|
36
|
-
providers.push(factory(entry));
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
// Legacy config: remoteStashSources[] → map to stash providers
|
|
41
|
-
if (!config.stashes && config.remoteStashSources) {
|
|
42
|
-
for (const entry of config.remoteStashSources) {
|
|
43
|
-
if (entry.enabled === false)
|
|
44
|
-
continue;
|
|
45
|
-
const factory = registry.resolve(entry.type ?? "openviking");
|
|
46
|
-
if (factory) {
|
|
47
|
-
providers.push(factory(entry));
|
|
48
|
-
}
|
|
26
|
+
for (const entry of config.stashes ?? []) {
|
|
27
|
+
if (entry.enabled === false)
|
|
28
|
+
continue;
|
|
29
|
+
if (entry.type === "filesystem")
|
|
30
|
+
continue;
|
|
31
|
+
const factory = registry.resolve(entry.type);
|
|
32
|
+
if (factory) {
|
|
33
|
+
providers.push(factory(entry));
|
|
49
34
|
}
|
|
50
35
|
}
|
|
51
36
|
return providers;
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { loadConfig, saveConfig } from "./config";
|
|
3
|
+
import { UsageError } from "./errors";
|
|
4
|
+
import { resolveStashSources } from "./stash-source";
|
|
5
|
+
// ── Operations ──────────────────────────────────────────────────────────────
|
|
6
|
+
/**
|
|
7
|
+
* Add a stash source (filesystem path or remote provider URL) to config.
|
|
8
|
+
*
|
|
9
|
+
* Filesystem paths are auto-detected when `target` does not start with
|
|
10
|
+
* `http://` or `https://`. URL sources require a `providerType` option
|
|
11
|
+
* (e.g. "openviking").
|
|
12
|
+
*/
|
|
13
|
+
export function addStashSource(opts) {
|
|
14
|
+
const { target, name, providerType, options: providerOptions } = opts;
|
|
15
|
+
const config = loadConfig();
|
|
16
|
+
const stashes = [...(config.stashes ?? [])];
|
|
17
|
+
const isUrl = target.startsWith("http://") || target.startsWith("https://");
|
|
18
|
+
let entry;
|
|
19
|
+
if (isUrl) {
|
|
20
|
+
if (!providerType) {
|
|
21
|
+
throw new UsageError("--provider is required for URL sources (e.g. --provider openviking)");
|
|
22
|
+
}
|
|
23
|
+
// Deduplicate by URL
|
|
24
|
+
if (stashes.some((s) => s.url === target)) {
|
|
25
|
+
return { stashes, added: false, message: "Source URL already configured" };
|
|
26
|
+
}
|
|
27
|
+
entry = { type: providerType, url: target };
|
|
28
|
+
if (name)
|
|
29
|
+
entry.name = name;
|
|
30
|
+
if (providerOptions)
|
|
31
|
+
entry.options = providerOptions;
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
// Filesystem path
|
|
35
|
+
const resolvedPath = path.resolve(target);
|
|
36
|
+
if (stashes.some((s) => s.path && path.resolve(s.path) === resolvedPath)) {
|
|
37
|
+
return { stashes, added: false, message: "Source path already configured" };
|
|
38
|
+
}
|
|
39
|
+
entry = { type: "filesystem", path: resolvedPath };
|
|
40
|
+
if (name)
|
|
41
|
+
entry.name = name;
|
|
42
|
+
}
|
|
43
|
+
stashes.push(entry);
|
|
44
|
+
saveConfig({ ...config, stashes });
|
|
45
|
+
return { stashes, added: true, entry };
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Remove a stash source by URL, path, or name.
|
|
49
|
+
* Match priority: URL > path > name (most specific first).
|
|
50
|
+
*/
|
|
51
|
+
export function removeStashSource(target) {
|
|
52
|
+
const config = loadConfig();
|
|
53
|
+
const stashes = [...(config.stashes ?? [])];
|
|
54
|
+
const isUrl = target.startsWith("http://") || target.startsWith("https://");
|
|
55
|
+
const resolvedPath = !isUrl ? path.resolve(target) : undefined;
|
|
56
|
+
// Try URL match first, then path, then name (most specific → least specific)
|
|
57
|
+
let idx = -1;
|
|
58
|
+
if (isUrl) {
|
|
59
|
+
idx = stashes.findIndex((s) => s.url === target);
|
|
60
|
+
}
|
|
61
|
+
if (idx === -1 && resolvedPath) {
|
|
62
|
+
idx = stashes.findIndex((s) => s.path && path.resolve(s.path) === resolvedPath);
|
|
63
|
+
}
|
|
64
|
+
if (idx === -1) {
|
|
65
|
+
idx = stashes.findIndex((s) => s.name === target);
|
|
66
|
+
}
|
|
67
|
+
if (idx === -1) {
|
|
68
|
+
return { stashes, removed: false, message: "No matching source found" };
|
|
69
|
+
}
|
|
70
|
+
const removed = stashes.splice(idx, 1)[0];
|
|
71
|
+
saveConfig({ ...config, stashes });
|
|
72
|
+
return { stashes, removed: true, entry: removed };
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* List all stash sources (local filesystem + configured stashes).
|
|
76
|
+
*/
|
|
77
|
+
export function listStashSources() {
|
|
78
|
+
const config = loadConfig();
|
|
79
|
+
const localSources = resolveStashSources();
|
|
80
|
+
const stashes = config.stashes ?? [];
|
|
81
|
+
return { localSources, stashes };
|
|
82
|
+
}
|