@rse/ase 0.0.16 → 0.0.17

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 CHANGED
@@ -151,7 +151,7 @@ const hasProjectContext = () => {
151
151
  "project" term is implicitly added only when a project context
152
152
  exists (Git repository or ".ase" directory at or above cwd), and
153
153
  an explicit "project" term requires that same context */
154
- const parseScope = (value) => {
154
+ export const parseScope = (value) => {
155
155
  const projectActive = hasProjectContext();
156
156
  const input = (value === undefined || value === "") ?
157
157
  (projectActive ? "project" : "user") :
package/dst/ase-hook.js CHANGED
@@ -6,14 +6,40 @@
6
6
  import path from "node:path";
7
7
  import fs from "node:fs";
8
8
  import { execaSync } from "execa";
9
+ import Version from "./ase-version.js";
10
+ import { Config, configSchema, parseScope } from "./ase-config.js";
9
11
  /* CLI command "ase hook" */
10
12
  export default class HookCommand {
11
13
  log;
12
14
  constructor(log) {
13
15
  this.log = log;
14
16
  }
17
+ /* recursively expand "@<path>" file references in a Markdown text,
18
+ resolving paths relative to the directory of the containing file */
19
+ expandReferences(text, baseDir, visited = new Set()) {
20
+ return text.replace(/@([^\s]+)/g, (match, ref) => {
21
+ let resolved = ref;
22
+ if (resolved.startsWith("~/"))
23
+ resolved = path.join(process.env.HOME ?? "", resolved.slice(2));
24
+ const abs = path.isAbsolute(resolved) ? resolved : path.resolve(baseDir, resolved);
25
+ if (visited.has(abs))
26
+ return match;
27
+ if (!fs.existsSync(abs))
28
+ return match;
29
+ let content;
30
+ try {
31
+ content = fs.readFileSync(abs, "utf8");
32
+ }
33
+ catch (_e) {
34
+ return match;
35
+ }
36
+ const next = new Set(visited);
37
+ next.add(abs);
38
+ return this.expandReferences(content, path.dirname(abs), next);
39
+ });
40
+ }
15
41
  /* handler for "ase hook session-start" */
16
- doSessionStart() {
42
+ async doSessionStart() {
17
43
  /* determine plugin root */
18
44
  const pluginRoot = process.env.CLAUDE_PLUGIN_ROOT ?? "";
19
45
  if (pluginRoot === "")
@@ -25,34 +51,85 @@ export default class HookCommand {
25
51
  const pkg = fs.readFileSync(filePkg, "utf8");
26
52
  let md = fs.readFileSync(fileMd, "utf8");
27
53
  /* determine own version */
28
- const version = JSON.parse(pkg).version ?? "";
54
+ const versionCurrentPlugin = JSON.parse(pkg).version ?? "";
55
+ const versionCurrentTool = Version.current();
56
+ const versionLatestTool = await Version.latest();
57
+ /* sanity check situation */
58
+ const versionHints = [];
59
+ if (versionCurrentPlugin !== versionCurrentTool)
60
+ versionHints.push("**WARNING:** version *mismatch*: " +
61
+ `tool: **${versionCurrentPlugin}**, plugin: **${versionCurrentTool}**`);
62
+ if (versionCurrentTool !== versionLatestTool)
63
+ versionHints.push(`**NOTICE:** *latest* version: **${versionLatestTool}**, please update!`);
64
+ if (process.env.ASE_SETUP_DEV !== undefined)
65
+ versionHints.push("**NOTICE:** *development* setup");
66
+ const versionHint = versionHints.length > 0 ? "(" + versionHints.join(", ") + ")" : "";
29
67
  /* read session information */
30
68
  const stdin = fs.readFileSync(0, "utf8");
31
69
  const input = stdin.trim() !== "" ? JSON.parse(stdin) : {};
32
70
  /* determine session id */
33
71
  const sessionId = input.session_id ?? "";
72
+ /* establish config context */
73
+ const cfg = new Config("config", configSchema, this.log, parseScope(`session:${sessionId}`));
74
+ try {
75
+ cfg.read();
76
+ }
77
+ catch (_e) {
78
+ /* best-effort: ignore failures */
79
+ }
34
80
  /* determine task id */
35
81
  const taskId = process.env.ASE_TASK_ID ?? "default";
36
82
  try {
37
- execaSync("ase", ["config", `--scope=session:${sessionId}`, "set", "task.id", taskId], { stdio: ["ignore", "ignore", "ignore"] });
83
+ cfg.set("task.id", taskId);
84
+ cfg.write();
38
85
  }
39
86
  catch (_e) {
40
87
  /* best-effort: ignore failures */
41
88
  }
42
- /* provide session and task id to Claude Code shell commands */
89
+ /* determine project id */
90
+ const cwd = input.cwd ?? process.cwd();
91
+ let projectDir = cwd;
92
+ try {
93
+ const result = execaSync("git", ["rev-parse", "--show-toplevel"], {
94
+ stderr: "ignore", cwd
95
+ });
96
+ if (result.stdout.trim() !== "")
97
+ projectDir = result.stdout.trim();
98
+ }
99
+ catch {
100
+ /* not inside a Git working tree */
101
+ }
102
+ const projectId = path.basename(projectDir);
103
+ /* determine user id */
104
+ const userId = process.env.USER ?? process.env.LOGNAME ?? "unknown";
105
+ /* determine agent persona style */
106
+ let persona = "engineer";
107
+ const val = cfg.get("agent.persona.style");
108
+ if (typeof val === "string")
109
+ persona = val;
110
+ /* provide ASE information to Claude Code shell commands */
43
111
  const envFile = process.env.CLAUDE_ENV_FILE ?? "";
44
112
  if (envFile !== "") {
45
- const script = `export ASE_VERSION="${version}"\n` +
46
- `export ASE_SESSION_ID="${sessionId}"\n` +
47
- `export ASE_TASK_ID="${taskId}"\n`;
113
+ const script = `export ASE_VERSION="${versionCurrentPlugin}"\n` +
114
+ `export ASE_USER_ID="${userId}"\n` +
115
+ `export ASE_PROJECT_ID="${projectId}"\n` +
116
+ `export ASE_TASK_ID="${taskId}"\n` +
117
+ `export ASE_SESSION_ID="${sessionId}"\n`;
48
118
  fs.appendFileSync(envFile, script, "utf8");
49
119
  }
50
120
  /* prepend ASE information to constitution markdown */
51
121
  md =
52
- `<ase-version>${version}</ase-version>\n` +
122
+ `<ase-version>${versionCurrentPlugin}</ase-version>\n` +
123
+ `<ase-version-hint>${versionHint}</ase-version-hint>\n` +
124
+ `<ase-persona-style>${persona}</ase-persona-style>\n` +
125
+ `<ase-user-id>${userId}</ase-user-id>\n` +
126
+ `<ase-project-id>${projectId}</ase-project-id>\n` +
53
127
  `<ase-task-id>${taskId}</ase-task-id>\n` +
54
128
  `<ase-session-id>${sessionId}</ase-session-id>\n` +
55
129
  "\n" + md;
130
+ /* expand all @<file> references manually */
131
+ md = this.expandReferences(md, path.dirname(fileMd));
132
+ fs.writeFileSync("/tmp/xxx", md, "utf8");
56
133
  /* inject markdown into session context */
57
134
  process.stdout.write(JSON.stringify({
58
135
  "hookSpecificOutput": {
@@ -110,8 +187,8 @@ export default class HookCommand {
110
187
  hookCmd
111
188
  .command("session-start")
112
189
  .description("handle Claude Code SessionStart hook event")
113
- .action(() => {
114
- process.exit(this.doSessionStart());
190
+ .action(async () => {
191
+ process.exit(await this.doSessionStart());
115
192
  });
116
193
  /* register CLI sub-command "ase hook pre-tool-use" */
117
194
  hookCmd
package/dst/ase-setup.js CHANGED
@@ -6,28 +6,42 @@
6
6
  import path from "node:path";
7
7
  import { fileURLToPath } from "node:url";
8
8
  import { execa } from "execa";
9
- import pkg from "../package.json" with { type: "json" };
9
+ import which from "which";
10
+ import Version from "./ase-version.js";
10
11
  /* CLI command "ase setup" */
11
12
  export default class SetupCommand {
12
13
  log;
13
14
  constructor(log) {
14
15
  this.log = log;
15
16
  }
16
- /* run a sub-process with inherited stdio so users see live output */
17
- async run(cmd, args, cwd) {
18
- this.log.write("info", `setup: running: $ ${cmd} ${args.join(" ")}` +
19
- (cwd !== undefined ? ` (cwd: ${cwd})` : ""));
20
- await execa(cmd, args, { stdio: "inherit", cwd });
17
+ /* ensure a tool is available */
18
+ async ensureTool(tool) {
19
+ return which(tool).catch(() => {
20
+ throw new Error(`mandatory tool "${tool}" not found in $PATH`);
21
+ });
21
22
  }
22
- /* capture stdout of a sub-process */
23
- async capture(cmd, args, cwd) {
24
- this.log.write("info", `setup: running: $ ${cmd} ${args.join(" ")}` +
23
+ /* run a sub-process, suppressing output on success and emitting it on failure */
24
+ async run(cmd, args, cwd) {
25
+ this.log.write("info", `setup: $ ${cmd} ${args.join(" ")}` +
25
26
  (cwd !== undefined ? ` (cwd: ${cwd})` : ""));
26
- const r = await execa(cmd, args, { stdio: ["ignore", "pipe", "pipe"], cwd });
27
- return r.stdout.trim();
27
+ await execa(cmd, args, { stdio: "pipe", cwd }).catch((err) => {
28
+ const exitCode = typeof err?.exitCode === "number" ? err.exitCode : -1;
29
+ this.log.write("error", `setup: command failed: exit code: ${exitCode}`);
30
+ if (typeof err?.stdout === "string" && err.stdout.length > 0) {
31
+ this.log.write("error", "setup: command failed: stdout:");
32
+ process.stdout.write(err.stdout);
33
+ }
34
+ if (typeof err?.stderr === "string" && err.stderr.length > 0) {
35
+ this.log.write("error", "setup: command failed: stderr:");
36
+ process.stderr.write(err.stderr);
37
+ }
38
+ throw err;
39
+ });
28
40
  }
29
41
  /* handler for "ase setup install" */
30
42
  async doInstall(dev) {
43
+ await this.ensureTool("npm");
44
+ await this.ensureTool("claude");
31
45
  this.log.write("info", `setup: install${dev ? "[dev]" : ""}: ` +
32
46
  `installing ASE Claude Code plugin (origin: ${dev ? "local" : "remote"})`);
33
47
  const source = dev ? process.cwd() : "rse/ase";
@@ -37,6 +51,8 @@ export default class SetupCommand {
37
51
  }
38
52
  /* handler for "ase setup update" */
39
53
  async doUpdate(force, dev) {
54
+ await this.ensureTool("npm");
55
+ await this.ensureTool("claude");
40
56
  if (dev) {
41
57
  /* update ASE CLI Tool */
42
58
  this.log.write("info", "setup: update[dev]: re-build ASE CLI tool (origin: local)");
@@ -52,15 +68,8 @@ export default class SetupCommand {
52
68
  }
53
69
  else {
54
70
  /* perform NPM version check */
55
- const current = pkg.version;
56
- let latest = "";
57
- try {
58
- latest = await this.capture("npm", ["view", "@rse/ase", "version"]);
59
- }
60
- catch (err) {
61
- const message = err instanceof Error ? err.message : String(err);
62
- this.log.write("warning", `setup: update: failed to query latest ASE version: ${message}`);
63
- }
71
+ const current = Version.current();
72
+ const latest = await Version.latest();
64
73
  if (!force && latest !== "" && latest === current) {
65
74
  this.log.write("info", `setup: update: ASE already at latest version ${current}`);
66
75
  return 0;
@@ -77,6 +86,8 @@ export default class SetupCommand {
77
86
  }
78
87
  /* handler for "ase setup uninstall" */
79
88
  async doUninstall(dev) {
89
+ await this.ensureTool("npm");
90
+ await this.ensureTool("claude");
80
91
  /* uninstall ASE Claude Code plugin */
81
92
  this.log.write("info", `setup: uninstall${dev ? "[dev]" : ""}: ` +
82
93
  `uninstalling ASE Claude Code plugin (origin: ${dev ? "local" : "remote"})`);
@@ -0,0 +1,27 @@
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 { execa } from "execa";
7
+ import pkg from "../package.json" with { type: "json" };
8
+ /* determination of current and available ASE versions */
9
+ export default class Version {
10
+ /* return current ASE version */
11
+ static current() {
12
+ return pkg.version;
13
+ }
14
+ /* return latest ASE version available on the NPM registry */
15
+ static async latest() {
16
+ let latest = "";
17
+ try {
18
+ const r = await execa("npm", ["view", "@rse/ase", "version"], { stdio: ["ignore", "pipe", "pipe"] });
19
+ latest = r.stdout.trim();
20
+ }
21
+ catch (err) {
22
+ const message = err instanceof Error ? err.message : String(err);
23
+ throw new Error(`failed to query latest ASE version: ${message}`, { cause: err });
24
+ }
25
+ return latest;
26
+ }
27
+ }
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.16",
9
+ "version": "0.0.17",
10
10
  "license": "GPL-3.0-only",
11
11
  "author": {
12
12
  "name": "Dr. Ralf S. Engelschall",
@@ -32,7 +32,8 @@
32
32
  "shx": "0.4.0",
33
33
 
34
34
  "@types/node": "25.6.0",
35
- "@types/luxon": "3.7.1"
35
+ "@types/luxon": "3.7.1",
36
+ "@types/which": "3.0.4"
36
37
  },
37
38
  "dependencies": {
38
39
  "commander": "14.0.3",
@@ -48,7 +49,8 @@
48
49
  "pretty-ms": "9.3.0",
49
50
  "luxon": "3.7.2",
50
51
  "@modelcontextprotocol/sdk": "1.29.0",
51
- "zod": "4.4.2"
52
+ "zod": "4.4.2",
53
+ "which": "6.0.1"
52
54
  },
53
55
  "engines": {
54
56
  "npm": ">=10.0.0",