@tomflow/chatloop-extension 0.3.29 → 0.3.33
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 +20 -164
- package/bin/cli.mjs +53 -6
- package/bin/native-host.mjs +11 -2
- package/extension/acceptance.js +105 -3
- package/extension/actions/clean.js +120 -0
- package/extension/actions/common.js +13 -4
- package/extension/actions/continue.js +3 -1
- package/extension/actions/handoff.js +16 -3
- package/extension/actions/model-intent.js +53 -0
- package/extension/actions/start.js +145 -0
- package/extension/actions/status.js +22 -0
- package/extension/background.js +310 -13
- package/extension/clean.html +5 -0
- package/extension/manifest.json +1 -1
- package/extension/popup.html +1 -1
- package/extension/popup.js +11 -0
- package/extension/start.html +6 -0
- package/extension/status.html +6 -0
- package/lib/bridge.mjs +62 -3
- package/lib/session-file.mjs +74 -0
- package/package.json +2 -2
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// Single-Mac durable session pointer. Extension Worker is the only admission/sending authority.
|
|
2
|
+
// This file is a checked, minimal projection, not a second executor or task engine.
|
|
3
|
+
import {openSync,closeSync,readFileSync,writeFileSync,renameSync,unlinkSync,lstatSync,existsSync,mkdirSync} from 'node:fs';
|
|
4
|
+
import {homedir} from 'node:os';
|
|
5
|
+
import {join,dirname} from 'node:path';
|
|
6
|
+
import {randomUUID} from 'node:crypto';
|
|
7
|
+
|
|
8
|
+
const ID=/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
9
|
+
const CHAT=/^[0-9a-f-]{12,}$/i;
|
|
10
|
+
const PROJECT=/^[a-zA-Z0-9][a-zA-Z0-9._-]{0,127}$/;
|
|
11
|
+
const PHASE=new Set(['STARTING','RESERVED','PENDING','WAITING_REPLY','WAITING_INTERVAL','DONE','BLOCKED','WALL_CLOCK_HOLD']);
|
|
12
|
+
const fail=code=>Object.assign(Error(code),{code});
|
|
13
|
+
export const sessionPath=()=>join(homedir(),'.chat-loop','session','active-session.json');
|
|
14
|
+
const ensurePrivate=dir=>{
|
|
15
|
+
if(!existsSync(dir))mkdirSync(dir,{recursive:true,mode:0o700});
|
|
16
|
+
const st=lstatSync(dir);
|
|
17
|
+
if(!st.isDirectory()||st.isSymbolicLink()||(process.getuid&&st.uid!==process.getuid())||
|
|
18
|
+
(process.platform!=='win32'&&(st.mode&0o077)))throw fail('SESSION_DIRECTORY_UNTRUSTED');
|
|
19
|
+
};
|
|
20
|
+
const validate=r=>{
|
|
21
|
+
if(!r||typeof r!=='object'||Array.isArray(r)||
|
|
22
|
+
!ID.test(r.sessionId||'')||!PROJECT.test(r.projectId||'')||
|
|
23
|
+
!PHASE.has(r.phase)||!Number.isSafeInteger(r.generation)||r.generation<0||
|
|
24
|
+
!Number.isSafeInteger(r.updatedAt)||r.updatedAt<=0)
|
|
25
|
+
throw fail('SESSION_INVALID');
|
|
26
|
+
if(r.activeChatId!==null&&(typeof r.activeChatId!=='string'||!CHAT.test(r.activeChatId)))
|
|
27
|
+
throw fail('SESSION_CHAT_INVALID');
|
|
28
|
+
if(r.activeChatUrl!==null&&(typeof r.activeChatUrl!=='string'
|
|
29
|
+
|| !/^https:\/\/chatgpt\.com\/(?:c\/[0-9a-f-]{12,}|g\/[^/]+\/c\/[0-9a-f-]{12,})$/i.test(r.activeChatUrl)))
|
|
30
|
+
throw fail('SESSION_CHAT_INVALID');
|
|
31
|
+
if((r.activeChatId===null)!==(r.activeChatUrl===null))
|
|
32
|
+
throw fail('SESSION_CHAT_INVALID');
|
|
33
|
+
if(r.activeChatUrl!==null&&!r.activeChatUrl.endsWith('/'+r.activeChatId))
|
|
34
|
+
throw fail('SESSION_CHAT_INVALID');
|
|
35
|
+
if(r.activeChatId===null&&r.phase==='WAITING_REPLY')throw fail('SESSION_CHAT_INVALID');
|
|
36
|
+
const keys=['sessionId','projectId','phase','generation','activeChatId','activeChatUrl','updatedAt'];
|
|
37
|
+
if(Object.keys(r).some(k=>!keys.includes(k)))throw fail('SESSION_EXTRA_FIELDS');
|
|
38
|
+
return Object.fromEntries(keys.map(k=>[k,r[k]]));
|
|
39
|
+
};
|
|
40
|
+
export function readSession(){
|
|
41
|
+
const file=sessionPath();
|
|
42
|
+
if(!existsSync(file))return null;
|
|
43
|
+
ensurePrivate(dirname(file));
|
|
44
|
+
const st=lstatSync(file);
|
|
45
|
+
if(!st.isFile()||st.isSymbolicLink()||(process.getuid&&st.uid!==process.getuid())
|
|
46
|
+
||(process.platform!=='win32'&&(st.mode&0o077)))throw fail('SESSION_FILE_UNTRUSTED');
|
|
47
|
+
try{return validate(JSON.parse(readFileSync(file,'utf8')));}
|
|
48
|
+
catch(e){if(e.code)throw e;throw fail('SESSION_FILE_CORRUPT');}
|
|
49
|
+
}
|
|
50
|
+
export function writeSession(input){
|
|
51
|
+
const next=validate(input),file=sessionPath(),dir=dirname(file),lock=file+'.lock';
|
|
52
|
+
ensurePrivate(dir);
|
|
53
|
+
let fd;
|
|
54
|
+
try{fd=openSync(lock,'wx',0o600);}catch{throw fail('SESSION_CONCURRENT_UPDATE');}
|
|
55
|
+
try{
|
|
56
|
+
const old=readSession();
|
|
57
|
+
if(old&&old.sessionId!==next.sessionId&&old.phase!=='DONE')
|
|
58
|
+
throw fail('SESSION_ALREADY_ACTIVE');
|
|
59
|
+
if(old&&old.sessionId===next.sessionId){
|
|
60
|
+
if(next.generation<old.generation)throw fail('SESSION_STALE_GENERATION');
|
|
61
|
+
if(old.phase==='DONE'&&next.phase!=='DONE'||old.phase==='BLOCKED'
|
|
62
|
+
&& !['BLOCKED','DONE'].includes(next.phase))
|
|
63
|
+
throw fail('SESSION_TERMINAL_LOCKED');
|
|
64
|
+
if(old.activeChatId!==null&&next.generation===old.generation&&
|
|
65
|
+
next.activeChatId!==null&&old.activeChatId!==next.activeChatId)
|
|
66
|
+
throw fail('SESSION_CHAT_CHANGED_WITHOUT_HANDOFF');
|
|
67
|
+
}
|
|
68
|
+
const temp=join(dir,'.active-session-'+randomUUID()+'.tmp');
|
|
69
|
+
try{writeFileSync(temp,JSON.stringify(next,null,2)+'\n',{flag:'wx',mode:0o600});
|
|
70
|
+
renameSync(temp,file);
|
|
71
|
+
}catch(e){try{unlinkSync(temp);}catch{}throw e;}
|
|
72
|
+
return next;
|
|
73
|
+
}finally{closeSync(fd);unlinkSync(lock);}
|
|
74
|
+
}
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tomflow/chatloop-extension",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.33",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"description": "Chat Loop Extension package:
|
|
5
|
+
"description": "Chat Loop Extension package: nine formal Action pages and its own native CLI bridge",
|
|
6
6
|
"bin": {
|
|
7
7
|
"chatloop-extension": "bin/cli.mjs",
|
|
8
8
|
"chat-loop-browser": "bin/cli.mjs"
|