@rse/ase 0.0.9 → 0.0.10
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/dst/ase-config.js +56 -17
- package/dst/ase-hook.js +83 -0
- package/dst/ase.1 +11 -7
- package/dst/ase.js +2 -0
- package/package.json +1 -1
package/dst/ase-config.js
CHANGED
|
@@ -21,7 +21,6 @@ export const projectClassification = {
|
|
|
21
21
|
},
|
|
22
22
|
process: {
|
|
23
23
|
actors: ["person", "team", "crew"],
|
|
24
|
-
control: ["human", "hitl", "agent"],
|
|
25
24
|
drive: ["spec", "code", "test"]
|
|
26
25
|
},
|
|
27
26
|
result: {
|
|
@@ -48,12 +47,11 @@ export const projectClassificationPresets = {
|
|
|
48
47
|
"project.source.size": "small",
|
|
49
48
|
"project.source.structure": "bare",
|
|
50
49
|
"project.process.actors": "person",
|
|
51
|
-
"project.process.control": "agent",
|
|
52
50
|
"project.process.drive": "spec",
|
|
53
51
|
"project.result.target": "prototype",
|
|
54
52
|
"agent.persona.style": "writer",
|
|
55
53
|
"agent.persona.creativity": "full",
|
|
56
|
-
"agent.process.autonomy": "agent"
|
|
54
|
+
"agent.process.autonomy": "agent"
|
|
57
55
|
},
|
|
58
56
|
pro: {
|
|
59
57
|
"project.id": "example",
|
|
@@ -63,12 +61,11 @@ export const projectClassificationPresets = {
|
|
|
63
61
|
"project.source.size": "medium",
|
|
64
62
|
"project.source.structure": "framework",
|
|
65
63
|
"project.process.actors": "person",
|
|
66
|
-
"project.process.control": "human",
|
|
67
64
|
"project.process.drive": "code",
|
|
68
65
|
"project.result.target": "product",
|
|
69
66
|
"agent.persona.style": "engineer",
|
|
70
67
|
"agent.persona.creativity": "none",
|
|
71
|
-
"agent.process.autonomy": "assistant"
|
|
68
|
+
"agent.process.autonomy": "assistant"
|
|
72
69
|
},
|
|
73
70
|
default: {
|
|
74
71
|
"project.id": "example",
|
|
@@ -78,12 +75,17 @@ export const projectClassificationPresets = {
|
|
|
78
75
|
"project.source.size": "medium",
|
|
79
76
|
"project.source.structure": "framework",
|
|
80
77
|
"project.process.actors": "person",
|
|
81
|
-
"project.process.control": "human",
|
|
82
78
|
"project.process.drive": "code",
|
|
83
79
|
"project.result.target": "product",
|
|
80
|
+
"project.artifact.build": "{etc/**,README.md,AGENTS.md,LICENSE.txt,package.json}",
|
|
81
|
+
"project.artifact.code": "src/**/*",
|
|
82
|
+
"project.artifact.docs": "doc/user/**/*.md",
|
|
83
|
+
"project.artifact.spec": "doc/spec/**/*.md",
|
|
84
|
+
"project.artifact.arch": "doc/arch/**/*.md",
|
|
84
85
|
"agent.persona.style": "engineer",
|
|
85
86
|
"agent.persona.creativity": "none",
|
|
86
87
|
"agent.process.autonomy": "assistant",
|
|
88
|
+
"task.id": "default"
|
|
87
89
|
},
|
|
88
90
|
industry: {
|
|
89
91
|
"project.id": "example",
|
|
@@ -93,14 +95,21 @@ export const projectClassificationPresets = {
|
|
|
93
95
|
"project.source.size": "large",
|
|
94
96
|
"project.source.structure": "framework",
|
|
95
97
|
"project.process.actors": "crew",
|
|
96
|
-
"project.process.control": "hitl",
|
|
97
98
|
"project.process.drive": "code",
|
|
98
99
|
"project.result.target": "mvp",
|
|
99
100
|
"agent.persona.style": "engineer",
|
|
100
101
|
"agent.persona.creativity": "none",
|
|
101
|
-
"agent.process.autonomy": "hotl"
|
|
102
|
+
"agent.process.autonomy": "hotl"
|
|
102
103
|
}
|
|
103
104
|
};
|
|
105
|
+
/* hard-coded map: which scope kinds each variable may be SET on
|
|
106
|
+
(reads always cascade through the full chain, this restricts writes only);
|
|
107
|
+
keys absent from this map default to all non-"default" scope kinds */
|
|
108
|
+
export const configWritableScopes = {
|
|
109
|
+
"task.id": ["session"]
|
|
110
|
+
};
|
|
111
|
+
/* default set of scope kinds writable for any unrestricted key */
|
|
112
|
+
const configWritableScopesDefault = ["user", "project", "task", "session"];
|
|
104
113
|
/* canonical ordering rank of a scope kind */
|
|
105
114
|
const scopeRank = (kind) => ({ default: -1, user: 0, project: 1, task: 2, session: 3 })[kind];
|
|
106
115
|
/* parse a single scope term */
|
|
@@ -180,11 +189,17 @@ export const configSchema = v.nullish(v.strictObject({
|
|
|
180
189
|
})),
|
|
181
190
|
process: v.optional(v.strictObject({
|
|
182
191
|
actors: v.optional(v.picklist(projectClassification.process.actors)),
|
|
183
|
-
control: v.optional(v.picklist(projectClassification.process.control)),
|
|
184
192
|
drive: v.optional(v.picklist(projectClassification.process.drive))
|
|
185
193
|
})),
|
|
186
194
|
result: v.optional(v.strictObject({
|
|
187
195
|
target: v.optional(v.picklist(projectClassification.result.target))
|
|
196
|
+
})),
|
|
197
|
+
artifact: v.optional(v.strictObject({
|
|
198
|
+
build: v.optional(v.pipe(v.string(), v.minLength(1))),
|
|
199
|
+
code: v.optional(v.pipe(v.string(), v.minLength(1))),
|
|
200
|
+
docs: v.optional(v.pipe(v.string(), v.minLength(1))),
|
|
201
|
+
spec: v.optional(v.pipe(v.string(), v.minLength(1))),
|
|
202
|
+
arch: v.optional(v.pipe(v.string(), v.minLength(1)))
|
|
188
203
|
}))
|
|
189
204
|
})),
|
|
190
205
|
agent: v.optional(v.strictObject({
|
|
@@ -195,6 +210,9 @@ export const configSchema = v.nullish(v.strictObject({
|
|
|
195
210
|
process: v.optional(v.strictObject({
|
|
196
211
|
autonomy: v.optional(v.picklist(agentClassification.process.autonomy))
|
|
197
212
|
}))
|
|
213
|
+
})),
|
|
214
|
+
task: v.optional(v.strictObject({
|
|
215
|
+
id: v.optional(v.pipe(v.string(), v.minLength(1)))
|
|
198
216
|
}))
|
|
199
217
|
}));
|
|
200
218
|
/* encapsulate read/write access to a stack of "<name>.yaml" configuration files,
|
|
@@ -259,14 +277,12 @@ export class Config {
|
|
|
259
277
|
(fs.existsSync(path.join(cwd, rel)) ? path.join(cwd, rel) : null);
|
|
260
278
|
return found ?? path.join(top ?? cwd, rel);
|
|
261
279
|
}
|
|
262
|
-
else {
|
|
263
|
-
const
|
|
264
|
-
|
|
265
|
-
if (top !== null)
|
|
266
|
-
return path.join(top, ".ase", sub, term.id, `${name}.yaml`);
|
|
267
|
-
else
|
|
268
|
-
return path.join(this.userConfigDir(), sub, term.id, `${name}.yaml`);
|
|
280
|
+
else if (term.kind === "task") {
|
|
281
|
+
const top = this.gitToplevel() ?? process.cwd();
|
|
282
|
+
return path.join(top, ".ase", "task", term.id, `${name}.yaml`);
|
|
269
283
|
}
|
|
284
|
+
else
|
|
285
|
+
return path.join(os.homedir(), ".ase", "session", term.id, `${name}.yaml`);
|
|
270
286
|
}
|
|
271
287
|
/* upward-walk on filesystem for a file path relative to a start directory,
|
|
272
288
|
bounded above (inclusive) by a stop directory */
|
|
@@ -482,8 +498,26 @@ export class Config {
|
|
|
482
498
|
result.sort((a, b) => a.key.localeCompare(b.key));
|
|
483
499
|
return result;
|
|
484
500
|
}
|
|
501
|
+
/* determine whether a key is writable on a given scope kind */
|
|
502
|
+
isWritableOn(key, kind) {
|
|
503
|
+
if (kind === "default")
|
|
504
|
+
return false;
|
|
505
|
+
const resolved = this.resolveKey(key);
|
|
506
|
+
const allowed = configWritableScopes[resolved] ?? configWritableScopesDefault;
|
|
507
|
+
return allowed.includes(kind);
|
|
508
|
+
}
|
|
509
|
+
/* enforce write-scope policy for the current target scope */
|
|
510
|
+
assertWritable(key) {
|
|
511
|
+
const td = this.docs[this.target];
|
|
512
|
+
const resolved = this.resolveKey(key);
|
|
513
|
+
const allowed = configWritableScopes[resolved] ?? configWritableScopesDefault;
|
|
514
|
+
if (!allowed.includes(td.scope.kind))
|
|
515
|
+
throw new Error(`cannot set "${resolved}" on scope "${Config.scopeLabel(td.scope)}": ` +
|
|
516
|
+
`this key is only writable on scope(s): ${allowed.join(", ")}`);
|
|
517
|
+
}
|
|
485
518
|
/* set a value at a dotted key in the target scope, creating intermediate maps as needed */
|
|
486
519
|
set(key, value) {
|
|
520
|
+
this.assertWritable(key);
|
|
487
521
|
const segments = this.resolveKey(key).split(".");
|
|
488
522
|
const td = this.docs[this.target];
|
|
489
523
|
const next = td.doc.clone();
|
|
@@ -508,6 +542,7 @@ export class Config {
|
|
|
508
542
|
}
|
|
509
543
|
/* delete a value at a dotted key from the target scope */
|
|
510
544
|
delete(key) {
|
|
545
|
+
this.assertWritable(key);
|
|
511
546
|
const td = this.docs[this.target];
|
|
512
547
|
const next = td.doc.clone();
|
|
513
548
|
next.deleteIn(this.resolveKey(key).split("."));
|
|
@@ -554,8 +589,12 @@ export default class ConfigCommand {
|
|
|
554
589
|
throw new Error(`unknown preset "${type}" (expected: default|vibe|pro|industry)`);
|
|
555
590
|
const cfg = new Config("config", configSchema, this.log, scope);
|
|
556
591
|
cfg.read();
|
|
557
|
-
|
|
592
|
+
const targetKind = scope[scope.length - 1].kind;
|
|
593
|
+
for (const [k, val] of Object.entries(preset)) {
|
|
594
|
+
if (!cfg.isWritableOn(k, targetKind))
|
|
595
|
+
continue;
|
|
558
596
|
cfg.set(k, val);
|
|
597
|
+
}
|
|
559
598
|
cfg.write();
|
|
560
599
|
});
|
|
561
600
|
/* register CLI sub-command "ase config list" */
|
package/dst/ase-hook.js
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/*
|
|
2
|
+
** Agentic Software Engineering (ASE)
|
|
3
|
+
** Copyright (c) 2025-2026 Dr. Ralf S. Engelschall <rse@engelschall.com>
|
|
4
|
+
** Licensed under GPL 3.0 <https://spdx.org/licenses/GPL-3.0-only>
|
|
5
|
+
*/
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
import fs from "node:fs";
|
|
8
|
+
import { execaSync } from "execa";
|
|
9
|
+
/* CLI command "ase hook" */
|
|
10
|
+
export default class HookCommand {
|
|
11
|
+
log;
|
|
12
|
+
constructor(log) {
|
|
13
|
+
this.log = log;
|
|
14
|
+
}
|
|
15
|
+
/* handler for "ase hook session-start" */
|
|
16
|
+
doSessionStart() {
|
|
17
|
+
/* determine plugin root */
|
|
18
|
+
const pluginRoot = process.env.CLAUDE_PLUGIN_ROOT ?? "";
|
|
19
|
+
if (pluginRoot === "")
|
|
20
|
+
throw new Error("CLAUDE_PLUGIN_ROOT environment variable is not set");
|
|
21
|
+
/* determine path to external files */
|
|
22
|
+
const filePkg = path.join(pluginRoot, ".claude-plugin", "plugin.json");
|
|
23
|
+
const fileMd = path.join(pluginRoot, "meta", "ase-constitution.md");
|
|
24
|
+
/* read external files */
|
|
25
|
+
const pkg = fs.readFileSync(filePkg, "utf8");
|
|
26
|
+
let md = fs.readFileSync(fileMd, "utf8");
|
|
27
|
+
/* determine own version */
|
|
28
|
+
const version = JSON.parse(pkg).version ?? "";
|
|
29
|
+
/* read session information */
|
|
30
|
+
const stdin = fs.readFileSync(0, "utf8");
|
|
31
|
+
const input = stdin.trim() !== "" ? JSON.parse(stdin) : {};
|
|
32
|
+
/* determine session id */
|
|
33
|
+
const sessionId = input.session_id ?? "";
|
|
34
|
+
/* determine task id */
|
|
35
|
+
const taskId = process.env.ASE_TASK_ID ?? "default";
|
|
36
|
+
try {
|
|
37
|
+
execaSync("ase", ["config", `--scope=session:${sessionId}`, "set", "task.id", taskId], { stdio: ["ignore", "ignore", "ignore"] });
|
|
38
|
+
}
|
|
39
|
+
catch (_e) {
|
|
40
|
+
/* best-effort: ignore failures */
|
|
41
|
+
}
|
|
42
|
+
/* provide session and task id to Claude Code shell commands */
|
|
43
|
+
const envFile = process.env.CLAUDE_ENV_FILE ?? "";
|
|
44
|
+
if (envFile !== "") {
|
|
45
|
+
const script = `export ASE_VERSION="${version}"\n` +
|
|
46
|
+
`export ASE_SESSION_ID="${sessionId}"\n` +
|
|
47
|
+
`export ASE_TASK_ID="${taskId}"\n`;
|
|
48
|
+
fs.appendFileSync(envFile, script, "utf8");
|
|
49
|
+
}
|
|
50
|
+
/* prepend ASE information to constitution markdown */
|
|
51
|
+
md =
|
|
52
|
+
`<ase-version>${version}</ase-version>\n` +
|
|
53
|
+
`<ase-task-id>${taskId}</ase-task-id>\n` +
|
|
54
|
+
`<ase-session-id>${sessionId}</ase-session-id>\n` +
|
|
55
|
+
"\n" + md;
|
|
56
|
+
/* inject markdown into session context */
|
|
57
|
+
process.stdout.write(JSON.stringify({
|
|
58
|
+
"hookSpecificOutput": {
|
|
59
|
+
"hookEventName": "SessionStart",
|
|
60
|
+
"additionalContext": md
|
|
61
|
+
}
|
|
62
|
+
}));
|
|
63
|
+
return 0;
|
|
64
|
+
}
|
|
65
|
+
/* register commands */
|
|
66
|
+
register(program) {
|
|
67
|
+
/* register CLI top-level command "ase hook" */
|
|
68
|
+
const hookCmd = program
|
|
69
|
+
.command("hook")
|
|
70
|
+
.description("Claude Code hook entry points")
|
|
71
|
+
.action(() => {
|
|
72
|
+
hookCmd.outputHelp();
|
|
73
|
+
process.exit(1);
|
|
74
|
+
});
|
|
75
|
+
/* register CLI sub-command "ase hook session-start" */
|
|
76
|
+
hookCmd
|
|
77
|
+
.command("session-start")
|
|
78
|
+
.description("handle Claude Code SessionStart hook event")
|
|
79
|
+
.action(() => {
|
|
80
|
+
process.exit(this.doSessionStart());
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
}
|
package/dst/ase.1
CHANGED
|
@@ -26,7 +26,7 @@ The following top-level command-line options exist:
|
|
|
26
26
|
The following top-level commands exist for configuration handling:
|
|
27
27
|
.RS 0
|
|
28
28
|
.IP \(bu 4
|
|
29
|
-
\fBase config\fR: Manage \fIASE\fR configuration stored in \fB.ase/config.yaml\fR. Without a subcommand, prints usage information. The file is validated against a schema: on read, unknown or invalid entries are warned about and silently dropped from the in-memory view; on set/write, they cause a fatal error. All \fBase config\fR subcommands accept a \fB--scope\fR \fIscope\fR option that selects the scope chain. The \fIscope\fR value is a comma-separated list of scope terms, in any order; each term is one of \fBuser\fR, \fBproject\fR, \fBtask:\fR\fIid\fR, or \fBsession:\fR\fIid\fR (where \fIid\fR matches \fB\[lB]A-Za-z0-9._-\[rB]+\fR). At most one term per kind is allowed. The chain is canonicalized into the fixed inheritance order \fBuser\fR < \fBproject\fR < \fBtask\fR < \fBsession\fR. \fBuser\fR is always implicitly added at the bottom of the chain. \fBproject\fR is implicitly added only when a \fIproject context\fR exists -- i.e. when the current working directory is inside a Git repository, or a \fB.ase\fR directory is found at or above it. Specifying \fBproject\fR explicitly without a project context is an error. Without an explicit \fB--scope\fR, the target defaults to \fBproject\fR when a project context exists, otherwise to \fBuser\fR. Reads cascade from the strongest (rightmost) scope down to the weakest and return the first value that is defined. Writes (\fBset\fR, \fBdelete\fR, \fBedit\fR, \fBinit\fR) are always confined to the strongest (target) scope's own file -- intermediate and weaker scopes are never modified. See \fIFILES\fR below for the resulting paths. Example: \fB--scope task:T1,session:S1\fR yields the chain \fBuser\fR -> \fBproject\fR -> \fBtask:T1\fR -> \fBsession:S1\fR, with \fBsession:S1\fR as the write target.
|
|
29
|
+
\fBase config\fR: Manage \fIASE\fR configuration stored in \fB.ase/config.yaml\fR. Without a subcommand, prints usage information. The file is validated against a schema: on read, unknown or invalid entries are warned about and silently dropped from the in-memory view; on set/write, they cause a fatal error. Recognized keys are grouped under three top-level sections: \fBproject.*\fR (project identity, classification, and artifact globs), \fBagent.*\fR (agent persona and process), and \fBtask.*\fR (currently \fBtask.id\fR, the active task identifier). All \fBase config\fR subcommands accept a \fB--scope\fR \fIscope\fR option that selects the scope chain. The \fIscope\fR value is a comma-separated list of scope terms, in any order; each term is one of \fBuser\fR, \fBproject\fR, \fBtask:\fR\fIid\fR, or \fBsession:\fR\fIid\fR (where \fIid\fR matches \fB\[lB]A-Za-z0-9._-\[rB]+\fR). At most one term per kind is allowed. The chain is canonicalized into the fixed inheritance order \fBuser\fR < \fBproject\fR < \fBtask\fR < \fBsession\fR. \fBuser\fR is always implicitly added at the bottom of the chain. \fBproject\fR is implicitly added only when a \fIproject context\fR exists -- i.e. when the current working directory is inside a Git repository, or a \fB.ase\fR directory is found at or above it. Specifying \fBproject\fR explicitly without a project context is an error. Without an explicit \fB--scope\fR, the target defaults to \fBproject\fR when a project context exists, otherwise to \fBuser\fR. Reads cascade from the strongest (rightmost) scope down to the weakest and return the first value that is defined. Writes (\fBset\fR, \fBdelete\fR, \fBedit\fR, \fBinit\fR) are always confined to the strongest (target) scope's own file -- intermediate and weaker scopes are never modified. See \fIFILES\fR below for the resulting paths. Example: \fB--scope task:T1,session:S1\fR yields the chain \fBuser\fR -> \fBproject\fR -> \fBtask:T1\fR -> \fBsession:S1\fR, with \fBsession:S1\fR as the write target.
|
|
30
30
|
.IP \(bu 4
|
|
31
31
|
\fBase config init\fR \fItype\fR: Initialize \fB.ase/config.yaml\fR with preset values for all recognized keys. The \fItype\fR argument selects the preset: \fBvibe\fR (solo rookie: small black-box prototype, bare code, fully agent-driven, spec-driven, engineer ambition), \fBpro\fR (solo expert: medium white-box product, framework-based, human-controlled, code-driven, artist ambition), or \fBindustry\fR (team crew: large grey-box MVP, framework-based, human-in-the-loop, code-driven, craftsman ambition).
|
|
32
32
|
.IP \(bu 4
|
|
@@ -54,18 +54,22 @@ The following top-level commands exist for service management:
|
|
|
54
54
|
\fBase service stop\fR: Stop the background service via HTTP \fBGET /stop\fR. Exits silently with status 0 on successful stop. If no port is configured or the port is not responding, prints an informational message and exits with status 0.
|
|
55
55
|
.RE 0
|
|
56
56
|
|
|
57
|
-
.SH "FILES"
|
|
57
|
+
.SH "CONFIGURATION FILES"
|
|
58
58
|
.RS 0
|
|
59
59
|
.IP \(bu 4
|
|
60
|
-
\fB
|
|
60
|
+
\fBuser\fR: \fIper-user configuration directory\fR\fB/config.yaml\fR: Per-user \fIASE\fR configuration (scope \fBuser\fR). The per-user configuration directory is \fB~/Library/Application Support/ase\fR on macOS, \fB%APPDATA%\[rs]ase\fR on Windows, and \fB$XDG_CONFIG_HOME/ase\fR (falling back to \fB~/.config/ase\fR) on Linux and other Unix systems.
|
|
61
61
|
.IP \(bu 4
|
|
62
|
-
\
|
|
62
|
+
\fBproject\fR: \fB.ase/config.yaml\fR: Per-project \fIASE\fR configuration (scope \fBproject\fR). Read upward from the current working directory.
|
|
63
63
|
.IP \(bu 4
|
|
64
|
-
\fB.ase/
|
|
64
|
+
\fBtask\fR: \fB.ase/task/\fR\fIid\fR\fB/config.yaml\fR: Per-task \fIASE\fR configuration (scope \fBtask:\fR\fIid\fR), located relative to the Git top-level directory. Outside a Git repository, the file is placed relative to the current working directory.
|
|
65
65
|
.IP \(bu 4
|
|
66
|
-
\
|
|
66
|
+
\fBsession\fR: \fB~/.ase/session/\fR\fIid\fR\fB/config.yaml\fR: Per-session \fIASE\fR configuration (scope \fBsession:\fR\fIid\fR), located under the user's home directory (independent of any project context).
|
|
67
|
+
.RE 0
|
|
68
|
+
|
|
69
|
+
.SH "STATE FILES"
|
|
70
|
+
.RS 0
|
|
67
71
|
.IP \(bu 4
|
|
68
|
-
\fB.ase/service.yaml\fR: Per-project service state.
|
|
72
|
+
\fB.ase/service.yaml\fR: Per-project service state.
|
|
69
73
|
.IP \(bu 4
|
|
70
74
|
\fB.ase/service.log\fR: Stdout/stderr log of the detached background service.
|
|
71
75
|
.RE 0
|
package/dst/ase.js
CHANGED
|
@@ -8,6 +8,7 @@ import { Command, CommanderError, Option } from "commander";
|
|
|
8
8
|
import Log from "./ase-log.js";
|
|
9
9
|
import ConfigCommand from "./ase-config.js";
|
|
10
10
|
import ServiceCommand from "./ase-service.js";
|
|
11
|
+
import HookCommand from "./ase-hook.js";
|
|
11
12
|
import pkg from "../package.json" with { type: "json" };
|
|
12
13
|
/* globally initialize logger */
|
|
13
14
|
const log = new Log("ase", "warning", "-");
|
|
@@ -38,6 +39,7 @@ const main = async () => {
|
|
|
38
39
|
/* register top-level commands */
|
|
39
40
|
new ConfigCommand(log).register(program);
|
|
40
41
|
new ServiceCommand(log).register(program);
|
|
42
|
+
new HookCommand(log).register(program);
|
|
41
43
|
/* parse program arguments */
|
|
42
44
|
await program.parseAsync(process.argv);
|
|
43
45
|
/* gracefully terminate */
|
package/package.json
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
"homepage": "http://github.com/rse/ase",
|
|
7
7
|
"repository": { "url": "git+https://github.com/rse/ase.git", "type": "git" },
|
|
8
8
|
"bugs": { "url": "http://github.com/rse/ase/issues" },
|
|
9
|
-
"version": "0.0.
|
|
9
|
+
"version": "0.0.10",
|
|
10
10
|
"license": "GPL-3.0-only",
|
|
11
11
|
"author": {
|
|
12
12
|
"name": "Dr. Ralf S. Engelschall",
|