@kitelev/exocortex-cli 15.163.0 → 15.165.0
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/bin/ai-task-runner +39 -0
- package/bin/ai-task-worker +127 -0
- package/dist/index.js +2 -2
- package/package.json +4 -1
- package/scripts/postinstall.cjs +28 -0
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# ai-task-runner — executed inside spawned tmux session per claimed task.
|
|
3
|
+
# Phase 6: real claude -p --dangerously-skip-permissions invocation.
|
|
4
|
+
|
|
5
|
+
set -euo pipefail
|
|
6
|
+
|
|
7
|
+
TASK_FILE="${1:?task file required}"
|
|
8
|
+
SESSION_LOG="${2:?session log required}"
|
|
9
|
+
|
|
10
|
+
mkdir -p "$(dirname "$SESSION_LOG")"
|
|
11
|
+
exec >>"$SESSION_LOG" 2>&1
|
|
12
|
+
|
|
13
|
+
echo "[runner] $(date '+%Y-%m-%dT%H:%M:%S%z'): start task=$TASK_FILE"
|
|
14
|
+
|
|
15
|
+
# Extract metadata from task frontmatter
|
|
16
|
+
eval "$(/usr/bin/python3 - "$TASK_FILE" <<'PYEOF'
|
|
17
|
+
import sys, re
|
|
18
|
+
fp = sys.argv[1]
|
|
19
|
+
content = open(fp, encoding='utf-8').read()
|
|
20
|
+
end = content.find('\n---', 3)
|
|
21
|
+
fm = content[3:end]
|
|
22
|
+
def get(key):
|
|
23
|
+
m = re.search(rf'^{re.escape(key)}\s*:\s*(.*)$', fm, re.MULTILINE)
|
|
24
|
+
return m.group(1).strip().strip('"') if m else ''
|
|
25
|
+
model = get('aiTask__Task_model') or 'sonnet'
|
|
26
|
+
timeout = get('aiTask__Task_timeoutMinutes') or '30'
|
|
27
|
+
print(f"TASK_MODEL={model}")
|
|
28
|
+
print(f"TASK_TIMEOUT={timeout}")
|
|
29
|
+
PYEOF
|
|
30
|
+
)"
|
|
31
|
+
|
|
32
|
+
# Build prompt: full task file content (frontmatter + body)
|
|
33
|
+
PROMPT="$(< "$TASK_FILE")"
|
|
34
|
+
|
|
35
|
+
echo "[runner] $(date '+%Y-%m-%dT%H:%M:%S%z'): spawning claude model=$TASK_MODEL timeout=${TASK_TIMEOUT}m"
|
|
36
|
+
|
|
37
|
+
# Run real claude -p — exec replaces this process
|
|
38
|
+
exec env -u CLAUDECODE -u CLAUDE_CODE_ENTRYPOINT -u CLAUDE_CODE_EXECPATH \
|
|
39
|
+
claude --dangerously-skip-permissions --model "$TASK_MODEL" -p "$PROMPT"
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# ai-task-worker — scan vault for delegated aiTask, claim atomically, spawn tmux session.
|
|
3
|
+
#
|
|
4
|
+
# Picks tasks where ems__Effort_status=Backlog AND aiTask__Task_delegated="true" AND no claimedBy.
|
|
5
|
+
# Atomic claim via frontmatter update (writes Doing+claimedBy+claimedAt+sessionLog).
|
|
6
|
+
# Spawns a detached tmux session named claude-child-<uuid> executing ai-task-runner,
|
|
7
|
+
# which calls real claude -p --dangerously-skip-permissions with task content as prompt.
|
|
8
|
+
|
|
9
|
+
set -euo pipefail
|
|
10
|
+
|
|
11
|
+
VAULT_DIR="${EXOCORTEX_VAULT:-/Users/kitelev/vault-2025}"
|
|
12
|
+
LOG_DIR="$HOME/.exocortex/logs"
|
|
13
|
+
mkdir -p "$LOG_DIR"
|
|
14
|
+
LOG="$LOG_DIR/aitask-worker.log"
|
|
15
|
+
RUNNER="$HOME/.exocortex/bin/ai-task-runner"
|
|
16
|
+
|
|
17
|
+
log() { echo "[ai-task-worker] $(date '+%Y-%m-%dT%H:%M:%S%z'): $*" | tee -a "$LOG"; }
|
|
18
|
+
|
|
19
|
+
log "Scan start (vault=$VAULT_DIR)"
|
|
20
|
+
|
|
21
|
+
/usr/bin/python3 - "$VAULT_DIR" "$LOG" "$RUNNER" "$$" <<'PYEOF'
|
|
22
|
+
import os, re, sys, datetime, tempfile, shutil, subprocess
|
|
23
|
+
|
|
24
|
+
vault, log_path, runner, parent_pid = sys.argv[1:5]
|
|
25
|
+
|
|
26
|
+
def log(msg):
|
|
27
|
+
ts = datetime.datetime.now().strftime('%Y-%m-%dT%H:%M:%S%z')
|
|
28
|
+
line = f"[ai-task-worker] {ts}: {msg}"
|
|
29
|
+
print(line)
|
|
30
|
+
open(log_path, 'a').write(line + "\n")
|
|
31
|
+
|
|
32
|
+
def now_iso5():
|
|
33
|
+
tz5 = datetime.timezone(datetime.timedelta(hours=5))
|
|
34
|
+
return datetime.datetime.now(tz5).strftime('%Y-%m-%dT%H:%M:%S+0500')
|
|
35
|
+
|
|
36
|
+
def split_fm(text):
|
|
37
|
+
if not text.startswith('---'):
|
|
38
|
+
return None, text
|
|
39
|
+
end = text.find('\n---', 3)
|
|
40
|
+
if end == -1:
|
|
41
|
+
return None, text
|
|
42
|
+
return text[3:end], text[end+4:]
|
|
43
|
+
|
|
44
|
+
def set_field(fm, key, val):
|
|
45
|
+
pat = re.compile(rf'^({re.escape(key)})\s*:.*$', re.MULTILINE)
|
|
46
|
+
if pat.search(fm):
|
|
47
|
+
return pat.sub(f'{key}: {val}', fm, 1)
|
|
48
|
+
return fm.rstrip('\n') + f'\n{key}: {val}\n'
|
|
49
|
+
|
|
50
|
+
def get_field(fm, key):
|
|
51
|
+
m = re.search(rf'^{re.escape(key)}\s*:\s*(.*)$', fm, re.MULTILINE)
|
|
52
|
+
return m.group(1).strip() if m else ''
|
|
53
|
+
|
|
54
|
+
def write_atomic(path, fm, body):
|
|
55
|
+
d = os.path.dirname(path)
|
|
56
|
+
with tempfile.NamedTemporaryFile('w', dir=d, delete=False, suffix='.tmp', encoding='utf-8') as t:
|
|
57
|
+
t.write(f"---{fm}\n---{body}")
|
|
58
|
+
tmp = t.name
|
|
59
|
+
shutil.move(tmp, path)
|
|
60
|
+
|
|
61
|
+
DELEG_PAT = re.compile(r'aiTask__Task_delegated\s*:\s*"?true"?', re.IGNORECASE)
|
|
62
|
+
BACKLOG_PAT = re.compile(r'ems__Effort_status\s*:\s*["\[]*ems__EffortStatusBacklog')
|
|
63
|
+
CLAIMED_PAT = re.compile(r'^aiTask__Task_claimedBy\s*:', re.MULTILINE)
|
|
64
|
+
|
|
65
|
+
candidates = []
|
|
66
|
+
for dp, dns, fns in os.walk(vault):
|
|
67
|
+
dns[:] = [d for d in dns if not d.startswith('.')]
|
|
68
|
+
for fn in fns:
|
|
69
|
+
if not fn.endswith('.md'):
|
|
70
|
+
continue
|
|
71
|
+
fp = os.path.join(dp, fn)
|
|
72
|
+
try:
|
|
73
|
+
content = open(fp, encoding='utf-8').read()
|
|
74
|
+
except Exception:
|
|
75
|
+
continue
|
|
76
|
+
if not content.startswith('---'):
|
|
77
|
+
continue
|
|
78
|
+
fm, _ = split_fm(content)
|
|
79
|
+
if fm is None:
|
|
80
|
+
continue
|
|
81
|
+
if not DELEG_PAT.search(fm):
|
|
82
|
+
continue
|
|
83
|
+
if not BACKLOG_PAT.search(fm):
|
|
84
|
+
continue
|
|
85
|
+
if CLAIMED_PAT.search(fm):
|
|
86
|
+
continue
|
|
87
|
+
candidates.append(fp)
|
|
88
|
+
|
|
89
|
+
log(f"Candidates: {len(candidates)}")
|
|
90
|
+
|
|
91
|
+
for fp in candidates:
|
|
92
|
+
try:
|
|
93
|
+
content = open(fp, encoding='utf-8').read()
|
|
94
|
+
fm, body = split_fm(content)
|
|
95
|
+
if fm is None:
|
|
96
|
+
continue
|
|
97
|
+
uid = get_field(fm, 'exo__Asset_uid').strip('"')
|
|
98
|
+
label = get_field(fm, 'exo__Asset_label').strip('"')
|
|
99
|
+
session_name = f"claude-child-{uid}"
|
|
100
|
+
session_log = f"{os.path.expanduser('~')}/.exocortex/logs/aitask-session-{uid}.log"
|
|
101
|
+
ts = now_iso5()
|
|
102
|
+
|
|
103
|
+
# Atomic claim — set Doing + claimedBy + claimedAt + sessionLog + startTimestamp + updatedAt
|
|
104
|
+
new_fm = fm
|
|
105
|
+
new_fm = set_field(new_fm, 'ems__Effort_status', '"[[ems__EffortStatusDoing]]"')
|
|
106
|
+
new_fm = set_field(new_fm, 'aiTask__Task_claimedBy', f'"{parent_pid}"')
|
|
107
|
+
new_fm = set_field(new_fm, 'aiTask__Task_claimedAt', f'"{ts}"')
|
|
108
|
+
new_fm = set_field(new_fm, 'aiTask__Task_sessionLog', f'"{session_log}"')
|
|
109
|
+
if 'ems__Effort_startTimestamp' not in new_fm:
|
|
110
|
+
new_fm = set_field(new_fm, 'ems__Effort_startTimestamp', ts)
|
|
111
|
+
new_fm = set_field(new_fm, 'exo__Asset_updatedAt', ts)
|
|
112
|
+
write_atomic(fp, new_fm, body)
|
|
113
|
+
log(f"CLAIMED {uid} ({label}) → session={session_name}")
|
|
114
|
+
|
|
115
|
+
# Spawn detached tmux session named claude-child-<full-uuid>
|
|
116
|
+
cmd = ['tmux', 'new-session', '-d', '-s', session_name,
|
|
117
|
+
'bash', '-l', '-c', f"{runner} '{fp}' '{session_log}' 2>&1 | tee -a '{session_log}'"]
|
|
118
|
+
try:
|
|
119
|
+
subprocess.run(cmd, check=True)
|
|
120
|
+
log(f"SPAWNED tmux session {session_name}")
|
|
121
|
+
except subprocess.CalledProcessError as e:
|
|
122
|
+
log(f"SPAWN_FAIL {uid}: {e}")
|
|
123
|
+
except Exception as e:
|
|
124
|
+
log(f"ERROR {fp}: {e}")
|
|
125
|
+
|
|
126
|
+
log("Scan done")
|
|
127
|
+
PYEOF
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// @kitelev/exocortex-cli v15.
|
|
2
|
+
// @kitelev/exocortex-cli v15.165.0
|
|
3
3
|
// CLI tool for Exocortex knowledge management system - SPARQL queries, task management, and more
|
|
4
4
|
// License: MIT
|
|
5
5
|
|
|
@@ -802,7 +802,7 @@ exo__BacklinksTableBlock_columns:${f==="[]"?" []":f}
|
|
|
802
802
|
`),i=!1;for(let s of r){if(s.match(/^aliases:\s*$/)){i=!0;continue}if(i){let c=s.match(/^\s+-\s+"?([^"]*)"?\s*$/);c?t.push(c[1].trim()):!s.startsWith(" ")&&!s.startsWith(" ")&&(i=!1)}let a=s.match(/^aliases:\s+"?([^"\n]+)"?\s*$/);a&&a[1].trim()&&t.push(a[1].trim())}return t.filter(Boolean)}o(jX,"extractAliases");function $X(n,e){return n.includes("/concepts/")}o($X,"isConceptFile");function Cd(n){let e=[],t=(0,Ln.readdirSync)(n);for(let r of t){if(r.startsWith(".")||r==="node_modules")continue;let i=(0,ba.join)(n,r);(0,Ln.statSync)(i).isDirectory()?e.push(...Cd(i)):r.endsWith(".md")&&e.push(i)}return e}o(Cd,"walkMdFiles");function i2(n){let e=Cd(n),t=[];for(let r of e){let i;try{i=(0,Ln.readFileSync)(r,"utf-8")}catch{continue}if(!$X(r,i))continue;let s=Ay(i),a=s.exo__Asset_uid??"";if(!a)continue;let c=EN(s,r),l=jX(i);t.push({uid:a,label:c,aliases:l,filePath:r})}return t}o(i2,"loadConcepts");var TN=4,BX=new Set(["the","and","for","but","with","this","that","from","have","been","are","was","will","can","not","all","any","both","each","few","more","most","other","some","such","than","then","when","where","which","while","who","how","its","into","over"]);function VX(n){return n.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")}o(VX,"escapeRegex");function vN(n,e){return e.length<TN||BX.has(e.toLowerCase())?!1:new RegExp(`\\b${VX(e)}\\b`,"i").test(n)}o(vN,"matchesWordBoundary");function Su(n,e){let t=n.toLowerCase(),r=e.toLowerCase();return r?t===r?1:r.length>=TN&&t.includes(r)?.85:0:0}o(Su,"scoreMatch");function UX(n,e,t,r){let i=n.toLowerCase(),s=e.toLowerCase(),a=t,c=[];for(let l of r){let u=0,f="body_substring",d=Su(i,l.label);if(d>u&&(u=d,f=d===1?"label_exact":"label_substring"),u<1){(vN(a,l.label)||vN(s,l.label))&&.9>u&&(u=.9,f="body_word_exact");let h=Su(s,l.label),p=h===1?.75:h>0?.65:0;p>u&&(u=p,f=h===1?"description_exact":"description_substring");let y=Su(a,l.label),b=y===1?.6:y>0?.5:0;b>u&&(u=b,f=y===1?"body_exact":"body_substring")}for(let h of l.aliases){let p=Su(i,h);if(p>0){let w=p*.9;w>u&&(u=w,f=p===1?"alias_label_exact":"alias_label_substring")}let y=Su(s,h);if(y>0){let w=y===1?.675:y*.65*.9;w>u&&(u=w,f="alias_description_substring")}let b=Su(a,h);if(b>0){let w=b===1?.54:b*.5*.9;w>u&&(u=w,f="alias_body_substring")}}u>0&&c.push({concept_uid:l.uid,concept_label:l.label,concept_file:l.filePath,confidence:Math.round(u*100)/100,match_type:f})}return c.sort((l,u)=>u.confidence-l.confidence),c.slice(0,3)}o(UX,"computeCandidates");function qX(n,e){return n.confidence>=e&&(n.match_type==="label_exact"||n.match_type==="alias_label_exact"||n.match_type==="body_word_exact")}o(qX,"isAutoApproved");async function WX(n){let e=(0,ba.resolve)(n.aiKnowDir),t=(0,ba.resolve)(n.vault??(0,ba.join)(e,"..","..","..")),r=n.autoThreshold??.8,i=i2(t),s=Cd(e),a=[];for(let c of s){let l;try{l=(0,Ln.readFileSync)(c,"utf-8")}catch{continue}let u=Ay(l),f=u.exo__Asset_uid??"";if(!f)continue;let d=EN(u,c),h=u.exo__Asset_description??"",p=n2(l),y=UX(d,h,p,i),b=y[0],w=b?qX(b,r):!1;a.push({aiKnow_uid:f,aiKnow_label:d,aiKnow_file:c,candidates:y,auto_approved:w,...w?{auto_approved_candidate:b}:{}})}return a}o(WX,"runBackfillSuggest");function GX(n,e){let t=e.substring(0,e.lastIndexOf("/"));(0,Ln.existsSync)(t)||(0,Ln.mkdirSync)(t,{recursive:!0});let r=n.map(i=>JSON.stringify(i)).join(`
|
|
803
803
|
`);(0,Ln.writeFileSync)(e,r,"utf-8")}o(GX,"writeJsonl");function AN(){let n=(0,ba.join)((0,wN.homedir)(),".cache","exocortex","backfill-candidates.jsonl");return new ce("suggest").description("Suggest concept backfill candidates for aiKnow assets").requiredOption("--aiKnow-dir <path>","Path to aiKnow assets directory").option("--vault <path>","Path to Obsidian vault (for finding concepts)").option("--output <path>","Output JSONL path",n).option("--auto-threshold <number>","Auto-approve confidence threshold","0.8").option("--dry-run","Dry-run mode: output JSONL but do not write to vault (default)",!0).action(async e=>{let t=parseFloat(e.autoThreshold);if(isNaN(t)||t<0||t>1){console.error("\u274C --auto-threshold must be a number between 0 and 1"),process.exitCode=1;return}let r=(0,ba.resolve)(e.aiKnowDir);if(!(0,Ln.existsSync)(r)){console.error(`\u274C aiKnow directory not found: ${r}`),process.exitCode=1;return}console.log("\u{1F50D} Loading concepts from vault...");let i=e.vault?(0,ba.resolve)(e.vault):void 0,s=Date.now(),a=await WX({aiKnowDir:r,vault:i,output:e.output,autoThreshold:t,dryRun:e.dryRun}),c=((Date.now()-s)/1e3).toFixed(2),l=a.filter(f=>f.auto_approved).length,u=a.filter(f=>f.candidates.length>0).length;GX(a,e.output),console.log(`\u2705 Processed ${a.length} aiKnow assets in ${c}s`),console.log(` ${u} assets have concept candidates`),console.log(` ${l} assets auto-approved (confidence \u2265 ${t})`),console.log(`\u{1F4DD} Output: ${e.output}`),e.dryRun&&console.log("\u2139\uFE0F Dry-run mode: no writes to vault")})}o(AN,"backfillSuggestCommand");var Cy=require("fs"),xy=require("path");var zX=new Set(["about","above","after","again","against","also","always","another","around","back","been","before","being","below","between","both","came","change","could","data","does","done","down","each","every","file","first","fixed","from","full","func","given","have","help","here","high","hook","html","http","https","info","into","issue","just","keep","know","last","like","line","list","load","look","made","make","many","more","most","much","must","need","next","none","note","null","only","open","over","page","part","pass","path","plan","plus","port","post","prev","prop","push","read","real","repo","rest","return","rule","runs","same","save","self","send","sets","show","side","size","slow","some","sort","step","such","sure","take","than","that","them","then","there","these","they","this","time","todo","true","type","under","used","user","uses","using","uuid","very","view","wait","want","ways","well","were","what","when","where","which","while","will","with","work","your","zero","eto","eto","pri","kak","tak","vse","dlya","pro"]);function HX(n){return n.toLowerCase().split(/[^a-z0-9_-]+/).filter(e=>e.length>=4&&!zX.has(e)&&/[a-z]/.test(e))}o(HX,"tokenize");function QX(n){let e=new Set;for(let t of n){e.add(t.label.toLowerCase());for(let r of t.aliases)e.add(r.toLowerCase())}return e}o(QX,"buildConceptTermSet");function KX(n,e,t){let r=Cd(n),i=new Map;for(let s of r){let a;try{a=(0,Cy.readFileSync)(s,"utf-8")}catch{continue}let l=Ay(a).exo__Asset_uid??"";if(!l)continue;let u=n2(a),f=HX(u),d=new Set;for(let h of f){if(d.has(h)||(d.add(h),e.has(h)))continue;let p=i.get(h)??{count:0,uids:[]};p.count++,p.uids.length<3&&p.uids.push(l),i.set(h,p)}}return Array.from(i.entries()).map(([s,{count:a,uids:c}])=>({term:s,frequency:a,sample_uids:c})).sort((s,a)=>a.frequency-s.frequency).slice(0,t)}o(KX,"extractOrphanTerms");function xN(){return new ce("orphan-terms").description("Extract high-frequency terms from aiKnow assets that have no matching ims__Concept").requiredOption("--aiKnow-dir <path>","Path to aiKnow assets directory").option("--vault <path>","Path to Obsidian vault (for finding concepts)").option("--top <number>","Number of top orphan terms to return","20").action(n=>{let e=parseInt(n.top,10);if(isNaN(e)||e<1){console.error("\u274C --top must be a positive integer"),process.exitCode=1;return}let t=(0,xy.resolve)(n.aiKnowDir);if(!(0,Cy.existsSync)(t)){console.error(`\u274C aiKnow directory not found: ${t}`),process.exitCode=1;return}let r=n.vault?(0,xy.resolve)(n.vault):(0,xy.resolve)(t,"..","..","..");console.log("\u{1F50D} Loading concepts from vault...");let i=i2(r),s=QX(i);console.log(` ${i.length} concepts loaded (${s.size} unique terms/aliases)`),console.log("\u{1F4D6} Scanning aiKnow assets for orphan terms...");let a=Date.now(),c=KX(t,s,e),l=((Date.now()-a)/1e3).toFixed(2);console.log(`
|
|
804
804
|
\u2705 Top ${c.length} orphan terms (${l}s):
|
|
805
|
-
`),console.log(`${"Rank".padEnd(6)}${"Term".padEnd(30)}${"Freq".padEnd(8)}Sample UIDs`),console.log("-".repeat(90)),c.forEach((u,f)=>{let d=`#${f+1}`.padEnd(6),h=u.term.padEnd(30),p=String(u.frequency).padEnd(8),y=u.sample_uids.join(", ");console.log(`${d}${h}${p}${y}`)})})}o(xN,"backfillOrphanTermsCommand");function CN(){let n=new ce("backfill").description("Concept backfill tools for aiKnow assets");return n.addCommand(AN()),n.addCommand(xN()),n}o(CN,"backfillCommand");function IN(n){n.addCommand(KO()),n.addCommand(ZO()),n.addCommand(eF())}o(IN,"addQuerySubcommands");function RN(n){let e=new ce;e.name("exocortex").description("CLI tool for Exocortex knowledge management system").version(n??"15.
|
|
805
|
+
`),console.log(`${"Rank".padEnd(6)}${"Term".padEnd(30)}${"Freq".padEnd(8)}Sample UIDs`),console.log("-".repeat(90)),c.forEach((u,f)=>{let d=`#${f+1}`.padEnd(6),h=u.term.padEnd(30),p=String(u.frequency).padEnd(8),y=u.sample_uids.join(", ");console.log(`${d}${h}${p}${y}`)})})}o(xN,"backfillOrphanTermsCommand");function CN(){let n=new ce("backfill").description("Concept backfill tools for aiKnow assets");return n.addCommand(AN()),n.addCommand(xN()),n}o(CN,"backfillCommand");function IN(n){n.addCommand(KO()),n.addCommand(ZO()),n.addCommand(eF())}o(IN,"addQuerySubcommands");function RN(n){let e=new ce;e.name("exocortex").description("CLI tool for Exocortex knowledge management system").version(n??"15.165.0");let t=e.command("exoql").description("ExoQL query execution and cache management");IN(t);let r=e.command("sparql").description("(deprecated) Use 'exoql' instead");return IN(r),r.hook("preAction",()=>{console.error('\u26A0\uFE0F "sparql" is deprecated. Use "exoql" instead.')}),e.addCommand(cD()),e.addCommand(fD()),e.addCommand(dD()),e.addCommand(pD()),e.addCommand(mD()),e.addCommand(bD()),e.addCommand(TD()),e.addCommand(MD()),e.addCommand(BD()),e.addCommand(GD()),e.addCommand(QD()),e.addCommand(KD()),e.addCommand(XD()),e.addCommand(cN()),e.addCommand(dN()),e.addCommand(SN()),e.addCommand(bN()),e.addCommand(CN()),e}o(RN,"createProgram");RN().parse();0&&(module.exports={createProgram});
|
|
806
806
|
/*! Bundled license information:
|
|
807
807
|
|
|
808
808
|
reflect-metadata/Reflect.js:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kitelev/exocortex-cli",
|
|
3
|
-
"version": "15.
|
|
3
|
+
"version": "15.165.0",
|
|
4
4
|
"description": "CLI tool for Exocortex knowledge management system - SPARQL queries, task management, and more",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -8,9 +8,12 @@
|
|
|
8
8
|
},
|
|
9
9
|
"files": [
|
|
10
10
|
"dist",
|
|
11
|
+
"bin",
|
|
12
|
+
"scripts",
|
|
11
13
|
"README.md"
|
|
12
14
|
],
|
|
13
15
|
"scripts": {
|
|
16
|
+
"postinstall": "node ./scripts/postinstall.cjs",
|
|
14
17
|
"build": "npm run build:bundle",
|
|
15
18
|
"build:tsc": "tsc",
|
|
16
19
|
"build:bundle": "node esbuild.config.mjs production",
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
// Idempotent postinstall: copies ai-task-{runner,worker} to ~/.exocortex/bin/
|
|
4
|
+
|
|
5
|
+
const fs = require('fs');
|
|
6
|
+
const path = require('path');
|
|
7
|
+
const os = require('os');
|
|
8
|
+
|
|
9
|
+
const BIN_SRC = path.join(__dirname, '..', 'bin');
|
|
10
|
+
const BIN_DST = path.join(os.homedir(), '.exocortex', 'bin');
|
|
11
|
+
|
|
12
|
+
const SCRIPTS = ['ai-task-runner', 'ai-task-worker'];
|
|
13
|
+
|
|
14
|
+
try {
|
|
15
|
+
fs.mkdirSync(BIN_DST, { recursive: true });
|
|
16
|
+
for (const name of SCRIPTS) {
|
|
17
|
+
const src = path.join(BIN_SRC, name);
|
|
18
|
+
const dst = path.join(BIN_DST, name);
|
|
19
|
+
if (!fs.existsSync(src)) continue;
|
|
20
|
+
fs.copyFileSync(src, dst);
|
|
21
|
+
fs.chmodSync(dst, 0o755);
|
|
22
|
+
console.log(`[postinstall] installed ${name} → ${dst}`);
|
|
23
|
+
}
|
|
24
|
+
} catch (err) {
|
|
25
|
+
// Non-fatal: CI environments may not have HOME
|
|
26
|
+
console.error(`[postinstall] skipped (${err.message})`);
|
|
27
|
+
process.exit(0);
|
|
28
|
+
}
|