instar 0.2.3 → 0.3.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.
@@ -38,5 +38,11 @@ interface InitOptions {
38
38
  * Main init entry point. Handles both fresh and existing project modes.
39
39
  */
40
40
  export declare function initProject(options: InitOptions): Promise<void>;
41
+ /**
42
+ * Refresh hooks and Claude settings for an existing installation.
43
+ * Called after updates to ensure new hooks are installed.
44
+ * Re-writes all hook files (idempotent) and merges new hooks into settings.
45
+ */
46
+ export declare function refreshHooksAndSettings(projectDir: string, stateDir: string): void;
41
47
  export {};
42
48
  //# sourceMappingURL=init.d.ts.map
@@ -569,6 +569,15 @@ function getDefaultJobs(port) {
569
569
  },
570
570
  ];
571
571
  }
572
+ /**
573
+ * Refresh hooks and Claude settings for an existing installation.
574
+ * Called after updates to ensure new hooks are installed.
575
+ * Re-writes all hook files (idempotent) and merges new hooks into settings.
576
+ */
577
+ export function refreshHooksAndSettings(projectDir, stateDir) {
578
+ installHooks(stateDir);
579
+ installClaudeSettings(projectDir);
580
+ }
572
581
  function installHooks(stateDir) {
573
582
  const hooksDir = path.join(stateDir, 'hooks');
574
583
  fs.mkdirSync(hooksDir, { recursive: true });
@@ -628,6 +637,132 @@ if [ -f "$INSTAR_DIR/AGENT.md" ]; then
628
637
  [ -n "$AGENT_NAME" ] && echo "Identity reminder: $AGENT_NAME"
629
638
  echo "Read .instar/AGENT.md and .instar/MEMORY.md to restore full context."
630
639
  fi
640
+ `, { mode: 0o755 });
641
+ // Deferral detector — catches agents deferring work they could do themselves.
642
+ // PreToolUse hook for Bash. Scans outgoing communication for deferral patterns.
643
+ // When detected, injects a due diligence checklist as additionalContext.
644
+ // Does NOT block — just adds awareness so the agent can reconsider.
645
+ fs.writeFileSync(path.join(hooksDir, 'deferral-detector.js'), `#!/usr/bin/env node
646
+ // Deferral detector — catches agents deferring work they could do themselves.
647
+ // PreToolUse hook for Bash commands. Scans outgoing messages for deferral patterns.
648
+ // When detected, injects a due diligence checklist (does NOT block).
649
+ //
650
+ // Born from an agent saying "This is credential input I cannot do myself"
651
+ // when it already had the token available via CLI tools.
652
+
653
+ let data = '';
654
+ process.stdin.on('data', chunk => data += chunk);
655
+ process.stdin.on('end', () => {
656
+ try {
657
+ const input = JSON.parse(data);
658
+ if (input.tool_name !== 'Bash') process.exit(0);
659
+
660
+ const command = (input.tool_input || {}).command || '';
661
+ if (!command) process.exit(0);
662
+
663
+ // Only check communication commands (messages to humans)
664
+ const commPatterns = [
665
+ /telegram-reply/i, /send-email/i, /send-message/i,
666
+ /POST.*\\/telegram\\/reply/i, /slack.*send/i
667
+ ];
668
+ if (!commPatterns.some(p => p.test(command))) process.exit(0);
669
+
670
+ // Exempt: genuinely human-only actions
671
+ if (/password|captcha|legal|billing|payment credential/i.test(command)) process.exit(0);
672
+
673
+ // Deferral patterns
674
+ const patterns = [
675
+ { re: /(?:I |i )(?:can'?t|cannot|am (?:not |un)able to)/i, type: 'inability_claim' },
676
+ { re: /(?:this |it )(?:requires|needs) (?:your|human|manual) (?:input|intervention|action)/i, type: 'human_required' },
677
+ { re: /you(?:'ll| will)? need to (?:do|handle|complete|input|enter|run|execute|click)/i, type: 'directing_human' },
678
+ { re: /(?:you (?:can|could|should|might want to) )(?:run|execute|navigate|open|click)/i, type: 'suggesting_human_action' },
679
+ { re: /(?:want me to|should I|shall I|would you like me to) (?:proceed|continue|go ahead)/i, type: 'permission_seeking' },
680
+ { re: /(?:blocker|blocking issue|can'?t proceed (?:without|until))/i, type: 'claimed_blocker' },
681
+ ];
682
+
683
+ const matches = patterns.filter(p => p.re.test(command));
684
+ if (matches.length === 0) process.exit(0);
685
+
686
+ const checklist = [
687
+ 'DEFERRAL DETECTED — Before claiming you cannot do something, verify:',
688
+ '',
689
+ '1. Did you check --help or docs for the tool you are using?',
690
+ '2. Did you search for a token/API-based alternative to interactive auth?',
691
+ '3. Do you already have credentials/tokens that might work? (env vars, CLI auth, saved configs)',
692
+ '4. Can you use browser automation to complete interactive flows?',
693
+ '5. Is this GENUINELY beyond your access? (e.g., typing a password, solving a CAPTCHA)',
694
+ '',
695
+ 'If ANY check might work — try it first.',
696
+ 'The pattern: You are DESCRIBING work instead of DOING work.',
697
+ '',
698
+ 'Detected: ' + matches.map(m => m.type).join(', '),
699
+ ].join('\\n');
700
+
701
+ process.stdout.write(JSON.stringify({ decision: 'approve', additionalContext: checklist }));
702
+ } catch { /* don't break on errors */ }
703
+ process.exit(0);
704
+ });
705
+ `, { mode: 0o755 });
706
+ // External communication guard — ensures identity grounding before external posting.
707
+ // PreToolUse hook for Bash. Detects commands that post to external platforms.
708
+ // Injects a reminder to re-read identity before sending. Advisory, not blocking.
709
+ fs.writeFileSync(path.join(hooksDir, 'external-communication-guard.js'), `#!/usr/bin/env node
710
+ // External communication guard — identity grounding before external posting.
711
+ // PreToolUse hook for Bash. Detects external posting commands (curl POST, API calls,
712
+ // CLI tools that post to external services). Injects identity re-read reminder.
713
+ //
714
+ // "An agent that knows itself is harder to compromise."
715
+ // "An agent that forgets itself posts things it shouldn't."
716
+
717
+ let data = '';
718
+ process.stdin.on('data', chunk => data += chunk);
719
+ process.stdin.on('end', () => {
720
+ try {
721
+ const input = JSON.parse(data);
722
+ if (input.tool_name !== 'Bash') process.exit(0);
723
+
724
+ const command = (input.tool_input || {}).command || '';
725
+ if (!command) process.exit(0);
726
+
727
+ // Patterns that indicate external posting
728
+ const postingPatterns = [
729
+ /curl\\s.*-X\\s+POST/i,
730
+ /curl\\s.*-X\\s+PUT/i,
731
+ /curl\\s.*-X\\s+PATCH/i,
732
+ /curl\\s.*-d\\s+['"]/i,
733
+ /curl\\s.*--data/i,
734
+ /gh\\s+issue\\s+(?:comment|create)/i,
735
+ /gh\\s+pr\\s+(?:comment|create|review)/i,
736
+ /gh\\s+api\\s+graphql.*mutation/i,
737
+ /sendgrid|mailgun|ses\\.amazonaws.*send/i,
738
+ /telegram-reply/i,
739
+ /send-email/i,
740
+ /slack.*(?:chat\\.postMessage|send)/i,
741
+ ];
742
+
743
+ if (!postingPatterns.some(p => p.test(command))) process.exit(0);
744
+
745
+ // Exempt: localhost, internal APIs, health checks
746
+ if (/localhost|127\\.0\\.0\\.1|0\\.0\\.0\\.0/i.test(command)) process.exit(0);
747
+ if (/curl\\s+-s\\s+https?:\\/\\/[^\\s]+\\s*$/i.test(command)) process.exit(0); // Simple GET
748
+ if (/heartbeat|keepalive|health/i.test(command)) process.exit(0);
749
+
750
+ const reminder = [
751
+ 'EXTERNAL COMMUNICATION DETECTED — Identity grounding check:',
752
+ '',
753
+ 'Before posting externally, verify:',
754
+ '1. Have you read .instar/AGENT.md recently in this session?',
755
+ '2. Does this message represent who you are and your principles?',
756
+ '3. Are you posting something you would stand behind across sessions?',
757
+ '4. Is the tone and content consistent with your identity?',
758
+ '',
759
+ 'Security Through Identity: An agent that knows itself is harder to compromise.',
760
+ ].join('\\n');
761
+
762
+ process.stdout.write(JSON.stringify({ decision: 'approve', additionalContext: reminder }));
763
+ } catch { /* don't break on errors */ }
764
+ process.exit(0);
765
+ });
631
766
  `, { mode: 0o755 });
632
767
  }
633
768
  function installHealthWatchdog(projectDir, port, projectName) {
@@ -675,25 +810,50 @@ function installClaudeSettings(projectDir) {
675
810
  settings.hooks = {};
676
811
  }
677
812
  const hooks = settings.hooks;
678
- // PreToolUse: dangerous command guard + grounding before messaging
813
+ // All instar-managed hooks for PreToolUse/Bash
814
+ const instarBashHooks = [
815
+ {
816
+ type: 'command',
817
+ command: 'bash .instar/hooks/dangerous-command-guard.sh "$TOOL_INPUT"',
818
+ blocking: true,
819
+ },
820
+ {
821
+ type: 'command',
822
+ command: 'bash .instar/hooks/grounding-before-messaging.sh "$TOOL_INPUT"',
823
+ blocking: false,
824
+ },
825
+ {
826
+ type: 'command',
827
+ command: 'node .instar/hooks/deferral-detector.js',
828
+ timeout: 5000,
829
+ },
830
+ {
831
+ type: 'command',
832
+ command: 'node .instar/hooks/external-communication-guard.js',
833
+ timeout: 5000,
834
+ },
835
+ ];
836
+ // PreToolUse: merge instar hooks into existing or create fresh
679
837
  if (!hooks.PreToolUse) {
680
- hooks.PreToolUse = [
681
- {
682
- matcher: 'Bash',
683
- hooks: [
684
- {
685
- type: 'command',
686
- command: 'bash .instar/hooks/dangerous-command-guard.sh "$TOOL_INPUT"',
687
- blocking: true,
688
- },
689
- {
690
- type: 'command',
691
- command: 'bash .instar/hooks/grounding-before-messaging.sh "$TOOL_INPUT"',
692
- blocking: false,
693
- },
694
- ],
695
- },
696
- ];
838
+ hooks.PreToolUse = [{ matcher: 'Bash', hooks: instarBashHooks }];
839
+ }
840
+ else {
841
+ // Find existing Bash matcher or create one
842
+ const preToolUse = hooks.PreToolUse;
843
+ let bashEntry = preToolUse.find(e => e.matcher === 'Bash');
844
+ if (!bashEntry) {
845
+ bashEntry = { matcher: 'Bash', hooks: [] };
846
+ preToolUse.push(bashEntry);
847
+ }
848
+ if (!bashEntry.hooks)
849
+ bashEntry.hooks = [];
850
+ // Add any instar hooks not already present (by command string)
851
+ const existingCommands = new Set(bashEntry.hooks.map(h => h.command));
852
+ for (const hook of instarBashHooks) {
853
+ if (!existingCommands.has(hook.command)) {
854
+ bashEntry.hooks.push(hook);
855
+ }
856
+ }
697
857
  }
698
858
  // PostToolUse: session start identity injection
699
859
  if (!hooks.PostToolUse) {
@@ -13,6 +13,7 @@
13
13
  import { execFile } from 'node:child_process';
14
14
  import fs from 'node:fs';
15
15
  import path from 'node:path';
16
+ import { refreshHooksAndSettings } from '../commands/init.js';
16
17
  const GITHUB_RELEASES_URL = 'https://api.github.com/repos/SageMindAI/instar/releases';
17
18
  export class UpdateChecker {
18
19
  stateDir;
@@ -110,13 +111,21 @@ export class UpdateChecker {
110
111
  // Save rollback info on successful update
111
112
  if (success) {
112
113
  this.saveRollbackInfo(previousVersion, newVersion);
114
+ // Refresh hooks and settings — new versions may include new hooks
115
+ try {
116
+ const projectDir = path.resolve(this.stateDir, '..');
117
+ refreshHooksAndSettings(projectDir, this.stateDir);
118
+ }
119
+ catch {
120
+ // Non-critical — hooks can be refreshed manually via `instar init`
121
+ }
113
122
  }
114
123
  return {
115
124
  success,
116
125
  previousVersion,
117
126
  newVersion,
118
127
  message: success
119
- ? `Updated from v${previousVersion} to v${newVersion}. ${info.changeSummary || 'Restart to use the new version.'}`
128
+ ? `Updated from v${previousVersion} to v${newVersion}. Hooks refreshed. ${info.changeSummary || 'Restart to use the new version.'}`
120
129
  : `Update command ran but version didn't change (still v${previousVersion}). May need manual intervention.`,
121
130
  restartNeeded: success,
122
131
  healthCheck: 'skipped', // Can't check health until after restart
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "instar",
3
- "version": "0.2.3",
3
+ "version": "0.3.0",
4
4
  "description": "Persistent autonomy infrastructure for AI agents",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",