@try-works/dsh-model-as-tool 0.0.0-stage → 0.1.1
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 +245 -2
- package/catalog/desert-ant-models.json +629 -0
- package/catalog/playbooks.json +531 -0
- package/cordis.patch.yml +39 -0
- package/lib/adapters/index.d.ts +48 -0
- package/lib/adapters/index.d.ts.map +1 -0
- package/lib/adapters/index.js +67 -0
- package/lib/adapters/tongue.d.ts +23 -0
- package/lib/adapters/tongue.d.ts.map +1 -0
- package/lib/adapters/tongue.js +85 -0
- package/lib/adapters/types.d.ts +95 -0
- package/lib/adapters/types.d.ts.map +1 -0
- package/lib/adapters/types.js +85 -0
- package/lib/adapters/voz.d.ts +22 -0
- package/lib/adapters/voz.d.ts.map +1 -0
- package/lib/adapters/voz.js +90 -0
- package/lib/capability.d.ts +58 -0
- package/lib/capability.d.ts.map +1 -0
- package/lib/capability.js +175 -0
- package/lib/catalog.d.ts +37 -0
- package/lib/catalog.d.ts.map +1 -0
- package/lib/catalog.js +217 -0
- package/lib/index.d.ts +120 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +391 -0
- package/lib/playbook.d.ts +42 -0
- package/lib/playbook.d.ts.map +1 -0
- package/lib/playbook.js +65 -0
- package/lib/registrar.d.ts +59 -0
- package/lib/registrar.d.ts.map +1 -0
- package/lib/registrar.js +136 -0
- package/lib/skills.d.ts +25 -0
- package/lib/skills.d.ts.map +1 -0
- package/lib/skills.js +102 -0
- package/lib/state.d.ts +100 -0
- package/lib/state.d.ts.map +1 -0
- package/lib/state.js +160 -0
- package/lib/transport.d.ts +158 -0
- package/lib/transport.d.ts.map +1 -0
- package/lib/transport.js +332 -0
- package/lib/types.d.ts +111 -0
- package/lib/types.d.ts.map +1 -0
- package/lib/types.js +8 -0
- package/package.json +83 -4
package/lib/registrar.js
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The registrar: turns each runnable catalog model into a DSH tool.
|
|
3
|
+
*
|
|
4
|
+
* The registrar owns the translation from the catalog's declarative tool spec
|
|
5
|
+
* into a `defineTool` definition, including the parameter schema, the canonical
|
|
6
|
+
* output contract and the model-facing rendering. Registration goes through
|
|
7
|
+
* `ctx.tools.register`, whose returned disposer is the only unregister API, so
|
|
8
|
+
* the plugin can add or drop a model's tool at runtime as capability changes.
|
|
9
|
+
*
|
|
10
|
+
* @module @try-works/dsh-model-as-tool/registrar
|
|
11
|
+
*/
|
|
12
|
+
import { defineTool } from '@deepseek-ai/dsh-tools';
|
|
13
|
+
/**
|
|
14
|
+
* Compile one declared parameter into the registry's parameter spec.
|
|
15
|
+
* @param type - the declared JSON-level type.
|
|
16
|
+
* @param description - the model-facing description.
|
|
17
|
+
* @param required - whether the parameter must be supplied.
|
|
18
|
+
* @returns the parameter property spec.
|
|
19
|
+
*/
|
|
20
|
+
function parameterSpec(type, description, required) {
|
|
21
|
+
const base = type === 'string[]'
|
|
22
|
+
? { type: 'array', items: { type: 'string' } }
|
|
23
|
+
: { type };
|
|
24
|
+
return {
|
|
25
|
+
...base,
|
|
26
|
+
description,
|
|
27
|
+
...required ? { required: true } : {},
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Build the parameter schema for one model's tool.
|
|
32
|
+
*
|
|
33
|
+
* Every catalog parameter is optional except the model's primary input, which
|
|
34
|
+
* the adapter validates as a required non-empty value. Types come from the
|
|
35
|
+
* catalog declaration, so a numeric or list parameter reaches the model as the
|
|
36
|
+
* type it actually is rather than as a string it has to re-parse.
|
|
37
|
+
* @param entry - the catalog entry.
|
|
38
|
+
* @returns the DSH parameter schema spec.
|
|
39
|
+
*/
|
|
40
|
+
export function buildParameters(entry) {
|
|
41
|
+
const params = entry.tool.params;
|
|
42
|
+
const names = Object.keys(params);
|
|
43
|
+
// The first documented parameter is the model's primary input.
|
|
44
|
+
const primary = names[0];
|
|
45
|
+
const schema = {};
|
|
46
|
+
for (const name of names) {
|
|
47
|
+
const param = params[name];
|
|
48
|
+
if (param === undefined)
|
|
49
|
+
continue;
|
|
50
|
+
schema[name] = parameterSpec(param.type ?? 'string', param.description, name === primary);
|
|
51
|
+
}
|
|
52
|
+
return schema;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Render the tool result for the model.
|
|
56
|
+
*
|
|
57
|
+
* The canonical value is the source of truth; this projection only chooses how
|
|
58
|
+
* to present it. Tongue's shaped result gets a sentence the model can act on
|
|
59
|
+
* directly, while everything else falls back to stable JSON so no information
|
|
60
|
+
* is lost to a lossy humanization.
|
|
61
|
+
* @param entry - the catalog entry.
|
|
62
|
+
* @param value - the canonical value the adapter returned.
|
|
63
|
+
* @returns the model-facing content blocks.
|
|
64
|
+
*/
|
|
65
|
+
export function renderResult(entry, value) {
|
|
66
|
+
if (entry.id === 'tongue' && typeof value.language === 'string') {
|
|
67
|
+
const candidates = Array.isArray(value.candidates) ? value.candidates : [];
|
|
68
|
+
const detail = candidates
|
|
69
|
+
.filter((candidate) => typeof candidate === 'object' && candidate !== null
|
|
70
|
+
&& typeof candidate.language === 'string')
|
|
71
|
+
.map(candidate => `${candidate.language} ${(candidate.probability * 100).toFixed(1)}%`)
|
|
72
|
+
.join(', ');
|
|
73
|
+
const tooClose = value.isTooCloseToCall === true ? ' (too close to call)' : '';
|
|
74
|
+
return [{
|
|
75
|
+
type: 'text',
|
|
76
|
+
text: `Detected language: ${value.language}${tooClose}`
|
|
77
|
+
+ (detail.length > 0 ? `\nCandidates: ${detail}` : ''),
|
|
78
|
+
}];
|
|
79
|
+
}
|
|
80
|
+
if (entry.id === 'voz' && typeof value.text === 'string') {
|
|
81
|
+
const wordCount = typeof value.wordCount === 'number' ? value.wordCount : 0;
|
|
82
|
+
return [{
|
|
83
|
+
type: 'text',
|
|
84
|
+
text: value.text.length > 0
|
|
85
|
+
? value.text
|
|
86
|
+
: `(no speech recognized; ${wordCount} words)`,
|
|
87
|
+
}];
|
|
88
|
+
}
|
|
89
|
+
return [{ type: 'text', text: JSON.stringify(value, null, 2) }];
|
|
90
|
+
}
|
|
91
|
+
/** The JSON-Schema type of the canonical value a model tool returns. */
|
|
92
|
+
function outputSchemaFor(entry) {
|
|
93
|
+
// One open object schema for every model: the adapters return model-specific
|
|
94
|
+
// fields, and the registry validates the declared shape without forbidding a
|
|
95
|
+
// field a future adapter adds.
|
|
96
|
+
void entry;
|
|
97
|
+
return { type: 'object', additionalProperties: true };
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Register one model's tool on the calling context.
|
|
101
|
+
* @param ctx - the context whose tool registry receives the definition; its scope decides visibility.
|
|
102
|
+
* @param entry - the catalog entry to expose.
|
|
103
|
+
* @param adapter - the adapter that invokes the model.
|
|
104
|
+
* @returns the model id, the tool name, and the disposer returned by the registry.
|
|
105
|
+
*/
|
|
106
|
+
export function registerModelTool(ctx, entry, adapter) {
|
|
107
|
+
const definition = defineTool({
|
|
108
|
+
name: entry.tool.name,
|
|
109
|
+
description: entry.tool.description,
|
|
110
|
+
parameters: buildParameters(entry),
|
|
111
|
+
output: {
|
|
112
|
+
schema: outputSchemaFor(entry),
|
|
113
|
+
render: (_args, value) => renderResult(entry, value),
|
|
114
|
+
},
|
|
115
|
+
async execute(args, exec) {
|
|
116
|
+
const value = await adapter.invoke({
|
|
117
|
+
args: args,
|
|
118
|
+
signal: exec.signal,
|
|
119
|
+
});
|
|
120
|
+
return value;
|
|
121
|
+
},
|
|
122
|
+
presentCall(args) {
|
|
123
|
+
const primary = Object.values(args)[0];
|
|
124
|
+
const preview = typeof primary === 'string' ? primary : undefined;
|
|
125
|
+
return {
|
|
126
|
+
card: 'generic',
|
|
127
|
+
title: `${entry.name}${entry.input === 'text' ? '' : ` (${entry.input})`}`,
|
|
128
|
+
kind: 'read',
|
|
129
|
+
...preview !== undefined && entry.input === 'text' ? { rawInput: preview } : {},
|
|
130
|
+
};
|
|
131
|
+
},
|
|
132
|
+
});
|
|
133
|
+
const dispose = ctx.tools.register(definition);
|
|
134
|
+
return { modelId: entry.id, toolName: entry.tool.name, dispose };
|
|
135
|
+
}
|
|
136
|
+
//# sourceMappingURL=registrar.js.map
|
package/lib/skills.d.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Skill publication: teaches an agent when and how to use each model tool.
|
|
3
|
+
*
|
|
4
|
+
* A tool schema tells the model what it *can* call; a skill tells it when the
|
|
5
|
+
* call is the right move, when it is the wrong one, and how the result should be
|
|
6
|
+
* used downstream. This module renders one skill body per registered model from
|
|
7
|
+
* the catalog plus that model's playbook entry.
|
|
8
|
+
*
|
|
9
|
+
* @module @try-works/dsh-model-as-tool/skills
|
|
10
|
+
*/
|
|
11
|
+
import type { ModelCatalogEntry } from './types.ts';
|
|
12
|
+
import type { ModelPlaybook } from './playbook.ts';
|
|
13
|
+
/**
|
|
14
|
+
* Render the instruction body for one model's skill.
|
|
15
|
+
*
|
|
16
|
+
* The body is deliberately procedural: it names the tool, states the trigger
|
|
17
|
+
* conditions, states the anti-triggers, and describes what to do with the
|
|
18
|
+
* result. Sections with no content are omitted rather than rendered empty, so a
|
|
19
|
+
* sparse playbook still produces a clean skill.
|
|
20
|
+
* @param entry - the catalog entry.
|
|
21
|
+
* @param playbook - the model's playbook, when one is authored.
|
|
22
|
+
* @returns the markdown skill body.
|
|
23
|
+
*/
|
|
24
|
+
export declare function renderSkillBody(entry: ModelCatalogEntry, playbook?: ModelPlaybook): string;
|
|
25
|
+
//# sourceMappingURL=skills.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"skills.d.ts","sourceRoot":"","sources":["../src/skills.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAA;AACnD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,eAAe,CAAA;AAElD;;;;;;;;;;GAUG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,iBAAiB,EAAE,QAAQ,CAAC,EAAE,aAAa,GAAG,MAAM,CAiE1F"}
|
package/lib/skills.js
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Skill publication: teaches an agent when and how to use each model tool.
|
|
3
|
+
*
|
|
4
|
+
* A tool schema tells the model what it *can* call; a skill tells it when the
|
|
5
|
+
* call is the right move, when it is the wrong one, and how the result should be
|
|
6
|
+
* used downstream. This module renders one skill body per registered model from
|
|
7
|
+
* the catalog plus that model's playbook entry.
|
|
8
|
+
*
|
|
9
|
+
* @module @try-works/dsh-model-as-tool/skills
|
|
10
|
+
*/
|
|
11
|
+
/**
|
|
12
|
+
* Render the instruction body for one model's skill.
|
|
13
|
+
*
|
|
14
|
+
* The body is deliberately procedural: it names the tool, states the trigger
|
|
15
|
+
* conditions, states the anti-triggers, and describes what to do with the
|
|
16
|
+
* result. Sections with no content are omitted rather than rendered empty, so a
|
|
17
|
+
* sparse playbook still produces a clean skill.
|
|
18
|
+
* @param entry - the catalog entry.
|
|
19
|
+
* @param playbook - the model's playbook, when one is authored.
|
|
20
|
+
* @returns the markdown skill body.
|
|
21
|
+
*/
|
|
22
|
+
export function renderSkillBody(entry, playbook) {
|
|
23
|
+
const lines = [];
|
|
24
|
+
lines.push(`# ${entry.name}`, '');
|
|
25
|
+
lines.push(entry.summary, '');
|
|
26
|
+
lines.push(`Call the \`${entry.tool.name}\` tool. ${entry.tool.returns}`, '');
|
|
27
|
+
lines.push('## When to use', '');
|
|
28
|
+
if (playbook !== undefined && playbook.whenToUse.length > 0) {
|
|
29
|
+
for (const item of playbook.whenToUse)
|
|
30
|
+
lines.push(`- ${item}`);
|
|
31
|
+
}
|
|
32
|
+
else if (entry.skill.whenToUse !== undefined) {
|
|
33
|
+
lines.push(`- ${entry.skill.whenToUse}`);
|
|
34
|
+
}
|
|
35
|
+
else {
|
|
36
|
+
lines.push(`- ${entry.summary}`);
|
|
37
|
+
}
|
|
38
|
+
lines.push('');
|
|
39
|
+
if (playbook !== undefined && playbook.whenNotToUse.length > 0) {
|
|
40
|
+
lines.push('## When not to use', '');
|
|
41
|
+
for (const item of playbook.whenNotToUse)
|
|
42
|
+
lines.push(`- ${item}`);
|
|
43
|
+
lines.push('');
|
|
44
|
+
}
|
|
45
|
+
if (playbook !== undefined && playbook.useCases.length > 0) {
|
|
46
|
+
lines.push('## Use cases', '');
|
|
47
|
+
for (const item of playbook.useCases)
|
|
48
|
+
lines.push(`- ${item}`);
|
|
49
|
+
lines.push('');
|
|
50
|
+
}
|
|
51
|
+
lines.push('## Calling it', '');
|
|
52
|
+
lines.push(`- Tool: \`${entry.tool.name}\``);
|
|
53
|
+
lines.push(`- Primary input: ${entry.input}`);
|
|
54
|
+
lines.push(`- Runs: ${describeRoute(entry)}`);
|
|
55
|
+
lines.push('');
|
|
56
|
+
for (const [name, param] of Object.entries(entry.tool.params)) {
|
|
57
|
+
lines.push(`- \`${name}\` (${param.type ?? 'string'}): ${param.description}`);
|
|
58
|
+
}
|
|
59
|
+
lines.push('');
|
|
60
|
+
if (playbook !== undefined && playbook.workflow.length > 0) {
|
|
61
|
+
lines.push('## Workflow', '');
|
|
62
|
+
playbook.workflow.forEach((step, index) => lines.push(`${index + 1}. ${step}`));
|
|
63
|
+
lines.push('');
|
|
64
|
+
}
|
|
65
|
+
if (playbook !== undefined && playbook.pairsWith.length > 0) {
|
|
66
|
+
lines.push('## Pairs with', '');
|
|
67
|
+
for (const item of playbook.pairsWith)
|
|
68
|
+
lines.push(`- ${item}`);
|
|
69
|
+
lines.push('');
|
|
70
|
+
}
|
|
71
|
+
if (playbook !== undefined && playbook.caveats.length > 0) {
|
|
72
|
+
lines.push('## Caveats', '');
|
|
73
|
+
for (const item of playbook.caveats)
|
|
74
|
+
lines.push(`- ${item}`);
|
|
75
|
+
lines.push('');
|
|
76
|
+
}
|
|
77
|
+
lines.push('## Licensing', '');
|
|
78
|
+
lines.push(`The ${entry.name} weights are licensed under the Desert Ant Labs Source-Available License `
|
|
79
|
+
+ `(https://license.desertant.com/1.0), not an open-source license. Inference is unlimited and free below `
|
|
80
|
+
+ '100,000 monthly active devices per platform per model; attribution is required and the models may not be '
|
|
81
|
+
+ 'redistributed or re-hosted as a service. Run it locally through this tool rather than proxying it to others.', '');
|
|
82
|
+
return lines.join('\n');
|
|
83
|
+
}
|
|
84
|
+
/** A short human description of how this model reaches its runtime. */
|
|
85
|
+
function describeRoute(entry) {
|
|
86
|
+
switch (entry.windowsSupport) {
|
|
87
|
+
case 'native-node':
|
|
88
|
+
return `in-process through ${entry.npmPackage}`;
|
|
89
|
+
case 'onnxruntime-node':
|
|
90
|
+
return `in-process through ${entry.npmPackage} with ${entry.requiresRuntime.join(', ')}`;
|
|
91
|
+
case 'browser-only':
|
|
92
|
+
return `in-process through ${entry.npmPackage}${entry.npmSubpath ?? ''} on linux-x64, linux-arm64 and darwin-arm64`;
|
|
93
|
+
case 'apple-only':
|
|
94
|
+
return 'on Apple platforms only, through the Swift SDK; not available to this process';
|
|
95
|
+
case 'no-sdk':
|
|
96
|
+
return 'nowhere yet: the weights are published but no SDK ships them';
|
|
97
|
+
/* v8 ignore next -- closed union; a new member must fail compilation here. */
|
|
98
|
+
default:
|
|
99
|
+
return 'by an unspecified route';
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
//# sourceMappingURL=skills.js.map
|
package/lib/state.d.ts
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Durable install state: which models the user has enabled or disabled.
|
|
3
|
+
*
|
|
4
|
+
* A plugin's `config` states the *deployment* default — what a profile ships
|
|
5
|
+
* with. This store records the *user's* runtime choices on top of it, so an
|
|
6
|
+
* `enable`/`disable` survives a restart instead of evaporating with the session.
|
|
7
|
+
*
|
|
8
|
+
* The two layers are kept distinct on purpose:
|
|
9
|
+
*
|
|
10
|
+
* - `disabled` always wins, so a user can turn off something the config ships.
|
|
11
|
+
* - `enabled` adds to the config selection, so a user can turn on something the
|
|
12
|
+
* config omitted without editing YAML.
|
|
13
|
+
*
|
|
14
|
+
* Writing to a file rather than back into the profile's `cordis.patch.yml` keeps
|
|
15
|
+
* the plugin out of the user's configuration: a rewrite there would fight the
|
|
16
|
+
* HMR reload and could clobber comments the user wrote.
|
|
17
|
+
*
|
|
18
|
+
* @module @try-works/dsh-model-as-tool/state
|
|
19
|
+
*/
|
|
20
|
+
/** The persisted user choices. */
|
|
21
|
+
export interface PersistedState {
|
|
22
|
+
/** Model ids the user turned on beyond the configured selection. */
|
|
23
|
+
readonly enabled: readonly string[];
|
|
24
|
+
/** Model ids the user turned off, overriding the configured selection. */
|
|
25
|
+
readonly disabled: readonly string[];
|
|
26
|
+
}
|
|
27
|
+
/** An empty state. */
|
|
28
|
+
export declare const EMPTY_STATE: PersistedState;
|
|
29
|
+
/** Read and write the durable state. Injectable so tests need no filesystem. */
|
|
30
|
+
export interface StateStore {
|
|
31
|
+
/** Read the current state; an absent or unreadable store is empty, not an error. */
|
|
32
|
+
read(): PersistedState;
|
|
33
|
+
/** Replace the state, durably. */
|
|
34
|
+
write(state: PersistedState): void;
|
|
35
|
+
}
|
|
36
|
+
/** Raised when a state file exists but cannot be understood. */
|
|
37
|
+
export declare class StateError extends Error {
|
|
38
|
+
readonly name = "StateError";
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Normalize a decoded state document.
|
|
42
|
+
*
|
|
43
|
+
* Unknown keys are dropped and non-string entries are rejected rather than
|
|
44
|
+
* silently ignored: a corrupted list is a bug worth surfacing, while a
|
|
45
|
+
* forward-compatible extra key is not.
|
|
46
|
+
* @param raw - the decoded JSON value.
|
|
47
|
+
* @param source - description of the origin, for messages.
|
|
48
|
+
* @returns the normalized state.
|
|
49
|
+
* @throws StateError when the document is not a valid state.
|
|
50
|
+
*/
|
|
51
|
+
export declare function parseState(raw: unknown, source: string): PersistedState;
|
|
52
|
+
/**
|
|
53
|
+
* Derive the default state file from the environment.
|
|
54
|
+
*
|
|
55
|
+
* DSH sets `DSH_HOME`; falling back to `~/.dsh` matches the launcher's own
|
|
56
|
+
* convention for a process started without it.
|
|
57
|
+
* @param env - the environment to read; defaults to the process environment.
|
|
58
|
+
* @returns the absolute path of the state file.
|
|
59
|
+
*/
|
|
60
|
+
export declare function defaultStateFile(env?: NodeJS.ProcessEnv): string;
|
|
61
|
+
/**
|
|
62
|
+
* Build a file-backed store.
|
|
63
|
+
*
|
|
64
|
+
* Writes go through a temporary file and a rename, so a crash mid-write leaves
|
|
65
|
+
* the previous state intact rather than a truncated document that would read as
|
|
66
|
+
* "everything the user configured is gone".
|
|
67
|
+
* @param file - absolute path of the state file.
|
|
68
|
+
* @returns the store.
|
|
69
|
+
*/
|
|
70
|
+
export declare function createFileStateStore(file: string): StateStore;
|
|
71
|
+
/**
|
|
72
|
+
* Build an in-memory store, for tests and for deployments that asked for no
|
|
73
|
+
* persistence.
|
|
74
|
+
* @param initial - the starting state.
|
|
75
|
+
* @returns the store.
|
|
76
|
+
*/
|
|
77
|
+
export declare function createMemoryStateStore(initial?: PersistedState): StateStore;
|
|
78
|
+
/**
|
|
79
|
+
* Apply one enable/disable choice to a state.
|
|
80
|
+
*
|
|
81
|
+
* The two lists are kept mutually exclusive so the merge rule never has to
|
|
82
|
+
* break a tie: enabling a model removes it from `disabled` and vice versa.
|
|
83
|
+
* @param state - the current state.
|
|
84
|
+
* @param modelId - the model the user acted on.
|
|
85
|
+
* @param enabled - true to turn on, false to turn off.
|
|
86
|
+
* @returns the updated state.
|
|
87
|
+
*/
|
|
88
|
+
export declare function withChoice(state: PersistedState, modelId: string, enabled: boolean): PersistedState;
|
|
89
|
+
/**
|
|
90
|
+
* Decide the effective installed set from the configured selection and the
|
|
91
|
+
* user's persisted choices.
|
|
92
|
+
* @param configured - model ids the config selects (already intersected with what can run).
|
|
93
|
+
* @param state - the persisted user choices.
|
|
94
|
+
* @returns the model ids to install, and those the user turned off.
|
|
95
|
+
*/
|
|
96
|
+
export declare function resolveSelection(configured: readonly string[], state: PersistedState): {
|
|
97
|
+
install: Set<string>;
|
|
98
|
+
disabled: Set<string>;
|
|
99
|
+
};
|
|
100
|
+
//# sourceMappingURL=state.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"state.d.ts","sourceRoot":"","sources":["../src/state.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAMH,kCAAkC;AAClC,MAAM,WAAW,cAAc;IAC7B,oEAAoE;IACpE,QAAQ,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,CAAA;IACnC,0EAA0E;IAC1E,QAAQ,CAAC,QAAQ,EAAE,SAAS,MAAM,EAAE,CAAA;CACrC;AAED,sBAAsB;AACtB,eAAO,MAAM,WAAW,EAAE,cAA8C,CAAA;AAExE,gFAAgF;AAChF,MAAM,WAAW,UAAU;IACzB,oFAAoF;IACpF,IAAI,IAAI,cAAc,CAAA;IACtB,kCAAkC;IAClC,KAAK,CAAC,KAAK,EAAE,cAAc,GAAG,IAAI,CAAA;CACnC;AAED,gEAAgE;AAChE,qBAAa,UAAW,SAAQ,KAAK;IACnC,SAAkB,IAAI,gBAAe;CACtC;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,UAAU,CAAC,GAAG,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,GAAG,cAAc,CAavE;AAED;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,MAAM,CAK7E;AAED;;;;;;;;GAQG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,GAAG,UAAU,CAkC7D;AAED;;;;;GAKG;AACH,wBAAgB,sBAAsB,CAAC,OAAO,GAAE,cAA4B,GAAG,UAAU,CAMxF;AAED;;;;;;;;;GASG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE,cAAc,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,GAAG,cAAc,CAKnG;AAED;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAC9B,UAAU,EAAE,SAAS,MAAM,EAAE,EAC7B,KAAK,EAAE,cAAc,GACpB;IAAE,OAAO,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IAAC,QAAQ,EAAE,GAAG,CAAC,MAAM,CAAC,CAAA;CAAE,CAMjD"}
|
package/lib/state.js
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Durable install state: which models the user has enabled or disabled.
|
|
3
|
+
*
|
|
4
|
+
* A plugin's `config` states the *deployment* default — what a profile ships
|
|
5
|
+
* with. This store records the *user's* runtime choices on top of it, so an
|
|
6
|
+
* `enable`/`disable` survives a restart instead of evaporating with the session.
|
|
7
|
+
*
|
|
8
|
+
* The two layers are kept distinct on purpose:
|
|
9
|
+
*
|
|
10
|
+
* - `disabled` always wins, so a user can turn off something the config ships.
|
|
11
|
+
* - `enabled` adds to the config selection, so a user can turn on something the
|
|
12
|
+
* config omitted without editing YAML.
|
|
13
|
+
*
|
|
14
|
+
* Writing to a file rather than back into the profile's `cordis.patch.yml` keeps
|
|
15
|
+
* the plugin out of the user's configuration: a rewrite there would fight the
|
|
16
|
+
* HMR reload and could clobber comments the user wrote.
|
|
17
|
+
*
|
|
18
|
+
* @module @try-works/dsh-model-as-tool/state
|
|
19
|
+
*/
|
|
20
|
+
import { mkdirSync, readFileSync, renameSync, writeFileSync, rmSync } from 'node:fs';
|
|
21
|
+
import { homedir } from 'node:os';
|
|
22
|
+
import { dirname, join } from 'node:path';
|
|
23
|
+
/** An empty state. */
|
|
24
|
+
export const EMPTY_STATE = { enabled: [], disabled: [] };
|
|
25
|
+
/** Raised when a state file exists but cannot be understood. */
|
|
26
|
+
export class StateError extends Error {
|
|
27
|
+
name = 'StateError';
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Normalize a decoded state document.
|
|
31
|
+
*
|
|
32
|
+
* Unknown keys are dropped and non-string entries are rejected rather than
|
|
33
|
+
* silently ignored: a corrupted list is a bug worth surfacing, while a
|
|
34
|
+
* forward-compatible extra key is not.
|
|
35
|
+
* @param raw - the decoded JSON value.
|
|
36
|
+
* @param source - description of the origin, for messages.
|
|
37
|
+
* @returns the normalized state.
|
|
38
|
+
* @throws StateError when the document is not a valid state.
|
|
39
|
+
*/
|
|
40
|
+
export function parseState(raw, source) {
|
|
41
|
+
if (typeof raw !== 'object' || raw === null || Array.isArray(raw)) {
|
|
42
|
+
throw new StateError(`${source} must contain a JSON object`);
|
|
43
|
+
}
|
|
44
|
+
const record = raw;
|
|
45
|
+
const list = (key) => {
|
|
46
|
+
const value = record[key] ?? [];
|
|
47
|
+
if (!Array.isArray(value) || value.some(item => typeof item !== 'string')) {
|
|
48
|
+
throw new StateError(`${source} field "${key}" must be an array of model ids`);
|
|
49
|
+
}
|
|
50
|
+
return value;
|
|
51
|
+
};
|
|
52
|
+
return { enabled: list('enabled'), disabled: list('disabled') };
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Derive the default state file from the environment.
|
|
56
|
+
*
|
|
57
|
+
* DSH sets `DSH_HOME`; falling back to `~/.dsh` matches the launcher's own
|
|
58
|
+
* convention for a process started without it.
|
|
59
|
+
* @param env - the environment to read; defaults to the process environment.
|
|
60
|
+
* @returns the absolute path of the state file.
|
|
61
|
+
*/
|
|
62
|
+
export function defaultStateFile(env = process.env) {
|
|
63
|
+
const home = env.DSH_HOME !== undefined && env.DSH_HOME.length > 0
|
|
64
|
+
? env.DSH_HOME
|
|
65
|
+
: join(homedir(), '.dsh');
|
|
66
|
+
return join(home, 'model-as-tool', 'state.json');
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Build a file-backed store.
|
|
70
|
+
*
|
|
71
|
+
* Writes go through a temporary file and a rename, so a crash mid-write leaves
|
|
72
|
+
* the previous state intact rather than a truncated document that would read as
|
|
73
|
+
* "everything the user configured is gone".
|
|
74
|
+
* @param file - absolute path of the state file.
|
|
75
|
+
* @returns the store.
|
|
76
|
+
*/
|
|
77
|
+
export function createFileStateStore(file) {
|
|
78
|
+
return {
|
|
79
|
+
read() {
|
|
80
|
+
let text;
|
|
81
|
+
try {
|
|
82
|
+
text = readFileSync(file, 'utf8');
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
// No file yet is the normal first-run case.
|
|
86
|
+
return EMPTY_STATE;
|
|
87
|
+
}
|
|
88
|
+
let parsed;
|
|
89
|
+
try {
|
|
90
|
+
parsed = JSON.parse(text);
|
|
91
|
+
}
|
|
92
|
+
catch (error) {
|
|
93
|
+
throw new StateError(`${file} is not valid JSON: ${error instanceof Error ? error.message : String(error)}`);
|
|
94
|
+
}
|
|
95
|
+
return parseState(parsed, file);
|
|
96
|
+
},
|
|
97
|
+
write(state) {
|
|
98
|
+
const normalized = {
|
|
99
|
+
enabled: [...new Set(state.enabled)].sort(),
|
|
100
|
+
disabled: [...new Set(state.disabled)].sort(),
|
|
101
|
+
};
|
|
102
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
103
|
+
const temporary = `${file}.tmp`;
|
|
104
|
+
writeFileSync(temporary, `${JSON.stringify(normalized, null, 2)}\n`, 'utf8');
|
|
105
|
+
try {
|
|
106
|
+
renameSync(temporary, file);
|
|
107
|
+
}
|
|
108
|
+
catch (error) {
|
|
109
|
+
rmSync(temporary, { force: true });
|
|
110
|
+
throw new StateError(`cannot write ${file}: ${error instanceof Error ? error.message : String(error)}`);
|
|
111
|
+
}
|
|
112
|
+
},
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Build an in-memory store, for tests and for deployments that asked for no
|
|
117
|
+
* persistence.
|
|
118
|
+
* @param initial - the starting state.
|
|
119
|
+
* @returns the store.
|
|
120
|
+
*/
|
|
121
|
+
export function createMemoryStateStore(initial = EMPTY_STATE) {
|
|
122
|
+
let current = initial;
|
|
123
|
+
return {
|
|
124
|
+
read: () => current,
|
|
125
|
+
write: (state) => { current = state; },
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Apply one enable/disable choice to a state.
|
|
130
|
+
*
|
|
131
|
+
* The two lists are kept mutually exclusive so the merge rule never has to
|
|
132
|
+
* break a tie: enabling a model removes it from `disabled` and vice versa.
|
|
133
|
+
* @param state - the current state.
|
|
134
|
+
* @param modelId - the model the user acted on.
|
|
135
|
+
* @param enabled - true to turn on, false to turn off.
|
|
136
|
+
* @returns the updated state.
|
|
137
|
+
*/
|
|
138
|
+
export function withChoice(state, modelId, enabled) {
|
|
139
|
+
const without = (list) => list.filter(id => id !== modelId);
|
|
140
|
+
return enabled
|
|
141
|
+
? { enabled: [...without(state.enabled), modelId], disabled: without(state.disabled) }
|
|
142
|
+
: { enabled: without(state.enabled), disabled: [...without(state.disabled), modelId] };
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Decide the effective installed set from the configured selection and the
|
|
146
|
+
* user's persisted choices.
|
|
147
|
+
* @param configured - model ids the config selects (already intersected with what can run).
|
|
148
|
+
* @param state - the persisted user choices.
|
|
149
|
+
* @returns the model ids to install, and those the user turned off.
|
|
150
|
+
*/
|
|
151
|
+
export function resolveSelection(configured, state) {
|
|
152
|
+
const disabled = new Set(state.disabled);
|
|
153
|
+
const install = new Set(configured);
|
|
154
|
+
for (const id of state.enabled)
|
|
155
|
+
install.add(id);
|
|
156
|
+
for (const id of disabled)
|
|
157
|
+
install.delete(id);
|
|
158
|
+
return { install, disabled };
|
|
159
|
+
}
|
|
160
|
+
//# sourceMappingURL=state.js.map
|